@documentation-robotics/cli 0.1.13 → 0.2.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 (181) hide show
  1. package/README.md +38 -0
  2. package/dist/analyzers/verify-engine.js +22 -19
  3. package/dist/analyzers/verify-engine.js.map +2 -2
  4. package/dist/cli.js +101 -27
  5. package/dist/cli.js.map +3 -3
  6. package/dist/coding-agents/agents/claude-code.js +3 -1
  7. package/dist/coding-agents/agents/claude-code.js.map +2 -2
  8. package/dist/coding-agents/claude-code-client.js +2 -1
  9. package/dist/coding-agents/claude-code-client.js.map +2 -2
  10. package/dist/coding-agents/copilot-client.js +17 -12
  11. package/dist/coding-agents/copilot-client.js.map +2 -2
  12. package/dist/coding-agents/default-permissions.js +122 -0
  13. package/dist/coding-agents/default-permissions.js.map +7 -0
  14. package/dist/commands/add.js +10 -6
  15. package/dist/commands/add.js.map +2 -2
  16. package/dist/commands/analyzer.js +18 -10
  17. package/dist/commands/analyzer.js.map +2 -2
  18. package/dist/commands/audit.js +6 -6
  19. package/dist/commands/audit.js.map +2 -2
  20. package/dist/commands/changeset.js +238 -145
  21. package/dist/commands/changeset.js.map +2 -2
  22. package/dist/commands/delete.js +45 -18
  23. package/dist/commands/delete.js.map +3 -3
  24. package/dist/commands/element.js +3 -2
  25. package/dist/commands/element.js.map +2 -2
  26. package/dist/commands/farm.js +657 -0
  27. package/dist/commands/farm.js.map +7 -0
  28. package/dist/commands/init.js +11 -1
  29. package/dist/commands/init.js.map +2 -2
  30. package/dist/commands/mcp.js +107 -0
  31. package/dist/commands/mcp.js.map +7 -0
  32. package/dist/commands/relationship.js +53 -30
  33. package/dist/commands/relationship.js.map +2 -2
  34. package/dist/commands/reports.js +1 -1
  35. package/dist/commands/reports.js.map +2 -2
  36. package/dist/commands/update.js +19 -6
  37. package/dist/commands/update.js.map +2 -2
  38. package/dist/commands/validate.js +41 -3
  39. package/dist/commands/validate.js.map +2 -2
  40. package/dist/commands/version.js +2 -2
  41. package/dist/commands/version.js.map +2 -2
  42. package/dist/config.js +83 -0
  43. package/dist/config.js.map +7 -0
  44. package/dist/core/annotation-store.js +138 -0
  45. package/dist/core/annotation-store.js.map +7 -0
  46. package/dist/core/farm-manifest.js +134 -0
  47. package/dist/core/farm-manifest.js.map +7 -0
  48. package/dist/core/farm-sync-engine.js +346 -0
  49. package/dist/core/farm-sync-engine.js.map +7 -0
  50. package/dist/core/farm-sync-state.js +159 -0
  51. package/dist/core/farm-sync-state.js.map +7 -0
  52. package/dist/core/manifest.js +6 -3
  53. package/dist/core/manifest.js.map +2 -2
  54. package/dist/core/migration-registry.js +55 -1
  55. package/dist/core/migration-registry.js.map +2 -2
  56. package/dist/core/model.js +177 -16
  57. package/dist/core/model.js.map +3 -3
  58. package/dist/core/virtual-projection.js +1 -0
  59. package/dist/core/virtual-projection.js.map +2 -2
  60. package/dist/generated/node-types.js +1 -1
  61. package/dist/generated/node-types.js.map +2 -2
  62. package/dist/generated/relationship-index.js +45 -9
  63. package/dist/generated/relationship-index.js.map +2 -2
  64. package/dist/integrations/claude_code/docs/MCP_SERVER.md +55 -0
  65. package/dist/integrations/claude_code/docs/README.md +4 -0
  66. package/dist/mcp/api-key-manager.js +98 -0
  67. package/dist/mcp/api-key-manager.js.map +7 -0
  68. package/dist/mcp/http-transport.js +176 -0
  69. package/dist/mcp/http-transport.js.map +7 -0
  70. package/dist/mcp/resource-registry.js +97 -0
  71. package/dist/mcp/resource-registry.js.map +7 -0
  72. package/dist/mcp/tool-registry.js +44 -0
  73. package/dist/mcp/tool-registry.js.map +7 -0
  74. package/dist/mcp/tools/annotation-create.js +39 -0
  75. package/dist/mcp/tools/annotation-create.js.map +7 -0
  76. package/dist/mcp/tools/annotation-delete.js +29 -0
  77. package/dist/mcp/tools/annotation-delete.js.map +7 -0
  78. package/dist/mcp/tools/annotation-get.js +29 -0
  79. package/dist/mcp/tools/annotation-get.js.map +7 -0
  80. package/dist/mcp/tools/annotation-list.js +24 -0
  81. package/dist/mcp/tools/annotation-list.js.map +7 -0
  82. package/dist/mcp/tools/annotation-reply.js +34 -0
  83. package/dist/mcp/tools/annotation-reply.js.map +7 -0
  84. package/dist/mcp/tools/annotation-shared.js +10 -0
  85. package/dist/mcp/tools/annotation-shared.js.map +7 -0
  86. package/dist/mcp/tools/annotation-update.js +39 -0
  87. package/dist/mcp/tools/annotation-update.js.map +7 -0
  88. package/dist/mcp/tools/changeset-list.js +33 -0
  89. package/dist/mcp/tools/changeset-list.js.map +7 -0
  90. package/dist/mcp/tools/changeset-show.js +39 -0
  91. package/dist/mcp/tools/changeset-show.js.map +7 -0
  92. package/dist/mcp/tools/chat-launch.js +50 -0
  93. package/dist/mcp/tools/chat-launch.js.map +7 -0
  94. package/dist/mcp/tools/chat-status.js +26 -0
  95. package/dist/mcp/tools/chat-status.js.map +7 -0
  96. package/dist/mcp/tools/index.js +47 -0
  97. package/dist/mcp/tools/index.js.map +7 -0
  98. package/dist/mcp/tools/model-add.js +106 -0
  99. package/dist/mcp/tools/model-add.js.map +7 -0
  100. package/dist/mcp/tools/model-delete.js +99 -0
  101. package/dist/mcp/tools/model-delete.js.map +7 -0
  102. package/dist/mcp/tools/model-export.js +72 -0
  103. package/dist/mcp/tools/model-export.js.map +7 -0
  104. package/dist/mcp/tools/model-info.js +51 -0
  105. package/dist/mcp/tools/model-info.js.map +7 -0
  106. package/dist/mcp/tools/model-list.js +53 -0
  107. package/dist/mcp/tools/model-list.js.map +7 -0
  108. package/dist/mcp/tools/model-reload.js +34 -0
  109. package/dist/mcp/tools/model-reload.js.map +7 -0
  110. package/dist/mcp/tools/model-search.js +88 -0
  111. package/dist/mcp/tools/model-search.js.map +7 -0
  112. package/dist/mcp/tools/model-show.js +41 -0
  113. package/dist/mcp/tools/model-show.js.map +7 -0
  114. package/dist/mcp/tools/model-stats.js +22 -0
  115. package/dist/mcp/tools/model-stats.js.map +7 -0
  116. package/dist/mcp/tools/model-trace.js +55 -0
  117. package/dist/mcp/tools/model-trace.js.map +7 -0
  118. package/dist/mcp/tools/model-update.js +105 -0
  119. package/dist/mcp/tools/model-update.js.map +7 -0
  120. package/dist/mcp/tools/model-validate.js +78 -0
  121. package/dist/mcp/tools/model-validate.js.map +7 -0
  122. package/dist/mcp/tools/shared.js +103 -0
  123. package/dist/mcp/tools/shared.js.map +7 -0
  124. package/dist/reports/model-layer-report-generator.js +5 -3
  125. package/dist/reports/model-layer-report-generator.js.map +2 -2
  126. package/dist/reports/model-readme-generator.js +116 -0
  127. package/dist/reports/model-readme-generator.js.map +7 -0
  128. package/dist/reports/model-report-data.js +37 -1
  129. package/dist/reports/model-report-data.js.map +2 -2
  130. package/dist/reports/model-report-orchestrator.js +29 -6
  131. package/dist/reports/model-report-orchestrator.js.map +2 -2
  132. package/dist/reports/model-report-utils.js +9 -0
  133. package/dist/reports/model-report-utils.js.map +7 -0
  134. package/dist/schemas/bundled/analyzers/cbm.json +2 -2
  135. package/dist/schemas/bundled/analyzers/manifest.json +2 -2
  136. package/dist/schemas/bundled/api.json +1 -1
  137. package/dist/schemas/bundled/apm.json +1 -1
  138. package/dist/schemas/bundled/application.json +12 -1
  139. package/dist/schemas/bundled/base.json +1 -1
  140. package/dist/schemas/bundled/business.json +1 -1
  141. package/dist/schemas/bundled/data-model.json +38 -2
  142. package/dist/schemas/bundled/data-store.json +1 -1
  143. package/dist/schemas/bundled/manifest.json +4 -4
  144. package/dist/schemas/bundled/motivation.json +1 -1
  145. package/dist/schemas/bundled/navigation.json +1 -1
  146. package/dist/schemas/bundled/product.json +1 -1
  147. package/dist/schemas/bundled/security.json +1 -1
  148. package/dist/schemas/bundled/technology.json +1 -1
  149. package/dist/schemas/bundled/testing.json +1 -1
  150. package/dist/schemas/bundled/ux.json +1 -1
  151. package/dist/server/server.js +22 -3
  152. package/dist/server/server.js.map +2 -2
  153. package/dist/telemetry/config.js +2 -40
  154. package/dist/telemetry/config.js.map +2 -2
  155. package/dist/telemetry/index.js +1 -1
  156. package/dist/types/index.js.map +2 -2
  157. package/dist/utils/ansi-suppressor.js +25 -0
  158. package/dist/utils/ansi-suppressor.js.map +7 -0
  159. package/dist/utils/color-utils.js +34 -0
  160. package/dist/utils/color-utils.js.map +7 -0
  161. package/dist/utils/element-reference-shape.js +15 -1
  162. package/dist/utils/element-reference-shape.js.map +3 -3
  163. package/dist/utils/errors.js +69 -10
  164. package/dist/utils/errors.js.map +2 -2
  165. package/dist/utils/globals.js +17 -1
  166. package/dist/utils/globals.js.map +2 -2
  167. package/dist/utils/project-paths.js +24 -2
  168. package/dist/utils/project-paths.js.map +2 -2
  169. package/dist/utils/reference-path-parser.js +49 -0
  170. package/dist/utils/reference-path-parser.js.map +7 -0
  171. package/dist/utils/source-reference.js +5 -0
  172. package/dist/utils/source-reference.js.map +2 -2
  173. package/dist/utils/spec-version.js +10 -2
  174. package/dist/utils/spec-version.js.map +2 -2
  175. package/dist/validators/composed-reference-validator.js +262 -0
  176. package/dist/validators/composed-reference-validator.js.map +7 -0
  177. package/dist/validators/reference-validator.js +59 -8
  178. package/dist/validators/reference-validator.js.map +3 -3
  179. package/dist/validators/relationship-schema-validator.js +30 -2
  180. package/dist/validators/relationship-schema-validator.js.map +3 -3
  181. package/package.json +6 -3
@@ -0,0 +1,346 @@
1
+ import path from "path";
2
+ import { execSync, execFileSync } from "child_process";
3
+ import { fileExists, ensureDir } from "../utils/file-io.js";
4
+ import { FarmSyncState } from "./farm-sync-state.js";
5
+ import { StagingAreaManager } from "./staging-area.js";
6
+ import { getErrorMessage } from "../utils/errors.js";
7
+ class FarmSyncEngine {
8
+ farmRoot;
9
+ model;
10
+ constructor(farmRoot, model) {
11
+ this.farmRoot = farmRoot, this.model = model || null;
12
+ }
13
+ /**
14
+ * Pull latest changes from remote for a codebase
15
+ * @param codebasePath - Relative path to codebase folder
16
+ * @returns Updated commit SHA
17
+ */
18
+ async pullCodebase(codebasePath) {
19
+ const fullPath = path.join(this.farmRoot, codebasePath);
20
+ if (!await fileExists(fullPath))
21
+ throw new Error(`Codebase directory not found: ${codebasePath}`);
22
+ try {
23
+ try {
24
+ execSync("git fetch --prune", { cwd: fullPath, stdio: "pipe" });
25
+ } catch (fetchError) {
26
+ const fetchMsg = getErrorMessage(fetchError);
27
+ if (!(fetchMsg.includes("No remote repository specified") || fetchMsg.includes("no remote")))
28
+ throw new Error(`git fetch failed: ${fetchMsg}`);
29
+ }
30
+ try {
31
+ execSync("git pull --no-rebase", { cwd: fullPath, stdio: "pipe" });
32
+ } catch (pullError) {
33
+ const pullMsg = getErrorMessage(pullError);
34
+ if (!(pullMsg.includes("No remote repository specified") || pullMsg.includes("no remote") || pullMsg.includes("no tracking information")))
35
+ throw new Error(`git pull failed: ${pullMsg}`);
36
+ }
37
+ return execSync("git rev-parse HEAD", { cwd: fullPath, encoding: "utf-8" }).trim();
38
+ } catch (error) {
39
+ throw new Error(`Failed to pull codebase '${codebasePath}': ${getErrorMessage(error)}`);
40
+ }
41
+ }
42
+ /**
43
+ * Get the current HEAD commit SHA for a codebase
44
+ */
45
+ async getCurrentCommit(codebasePath) {
46
+ const fullPath = path.join(this.farmRoot, codebasePath);
47
+ if (!await fileExists(fullPath))
48
+ throw new Error(`Codebase directory not found: ${codebasePath}`);
49
+ try {
50
+ return execSync("git rev-parse HEAD", { cwd: fullPath, encoding: "utf-8" }).trim();
51
+ } catch (error) {
52
+ throw new Error(`Failed to get current commit for '${codebasePath}': ${getErrorMessage(error)}`);
53
+ }
54
+ }
55
+ /**
56
+ * Compute diff between two commits
57
+ * Commit SHAs are validated before use to prevent command injection
58
+ */
59
+ async computeDiff(codebasePath, fromCommit, toCommit) {
60
+ const fullPath = path.join(this.farmRoot, codebasePath);
61
+ if (!await fileExists(fullPath))
62
+ throw new Error(`Codebase directory not found: ${codebasePath}`);
63
+ const commitShaRegex = /^[0-9a-f]{7,40}$/i;
64
+ if (!commitShaRegex.test(fromCommit) || !commitShaRegex.test(toCommit))
65
+ throw new Error(`Invalid commit SHA format. Expected 7-40 character hex string. Got: ${fromCommit}, ${toCommit}`);
66
+ try {
67
+ const diffOutput = execFileSync("git", ["diff", "--name-status", `${fromCommit}...${toCommit}`], {
68
+ cwd: fullPath,
69
+ encoding: "utf-8"
70
+ }), added = [], modified = [], deleted = [];
71
+ for (const line of diffOutput.trim().split(`
72
+ `)) {
73
+ if (!line)
74
+ continue;
75
+ const [status, ...fileParts] = line.split(" "), filePath = fileParts.join(" ");
76
+ switch (status) {
77
+ case "A":
78
+ added.push(filePath);
79
+ break;
80
+ case "M":
81
+ modified.push(filePath);
82
+ break;
83
+ case "D":
84
+ deleted.push(filePath);
85
+ break;
86
+ default:
87
+ if (status.startsWith("R")) {
88
+ const [oldPath, newPath] = fileParts;
89
+ deleted.push(oldPath), added.push(newPath);
90
+ } else status.startsWith("C") && added.push(fileParts[1]);
91
+ break;
92
+ }
93
+ }
94
+ return { added, modified, deleted };
95
+ } catch (error) {
96
+ throw new Error(`Failed to compute diff for '${codebasePath}': ${getErrorMessage(error)}`);
97
+ }
98
+ }
99
+ /**
100
+ * Map changed files to model elements using source references
101
+ * Looks at each model element's source references and matches against changed files
102
+ * Returns empty results if model is not available
103
+ */
104
+ async mapFilesToElements() {
105
+ if (!this.model)
106
+ return { confident: [], ambiguous: [] };
107
+ const confident = /* @__PURE__ */ new Map(), ambiguous = /* @__PURE__ */ new Map();
108
+ for (const layer of this.model.layers.values())
109
+ for (const element of layer.listElements()) {
110
+ if (!element.source_reference)
111
+ continue;
112
+ const sourceRef = element.source_reference;
113
+ if (!(!sourceRef.locations || sourceRef.locations.length === 0))
114
+ for (const location of sourceRef.locations) {
115
+ const filePath = location.file;
116
+ if (!confident.has(filePath) && !ambiguous.has(filePath)) {
117
+ const mapping = {
118
+ filePath,
119
+ possibleElements: [
120
+ {
121
+ elementId: element.id,
122
+ layer: layer.name,
123
+ confidence: 100,
124
+ // Exact match from source reference
125
+ sourceRef: location
126
+ }
127
+ ]
128
+ };
129
+ confident.set(filePath, mapping);
130
+ } else if (confident.has(filePath)) {
131
+ const existing = confident.get(filePath);
132
+ confident.delete(filePath), existing.possibleElements.push({
133
+ elementId: element.id,
134
+ layer: layer.name,
135
+ confidence: 100,
136
+ sourceRef: location
137
+ }), ambiguous.set(filePath, existing);
138
+ } else
139
+ ambiguous.get(filePath).possibleElements.push({
140
+ elementId: element.id,
141
+ layer: layer.name,
142
+ confidence: 100,
143
+ sourceRef: location
144
+ });
145
+ }
146
+ }
147
+ return {
148
+ confident: Array.from(confident.values()),
149
+ ambiguous: Array.from(ambiguous.values())
150
+ };
151
+ }
152
+ /**
153
+ * Generate a changeset from file-to-element mappings
154
+ * Creates add/update/delete changes based on the diff and mappings
155
+ * Stages changes into the changeset for review before commit
156
+ */
157
+ async generateChangeset(project, diff, mappings, options = {}) {
158
+ if (!this.model)
159
+ throw new Error("Model is required for changeset generation");
160
+ const projectModelPath = path.join(this.farmRoot, project.model), stagingManager = new StagingAreaManager(projectModelPath, this.model), changesetName = `farm-sync-${project.name}-${Date.now()}`, changeset = await stagingManager.create(changesetName, `Auto-generated from farm sync of ${project.name}`);
161
+ if (!changeset.id)
162
+ throw new Error("Failed to create changeset - no ID assigned");
163
+ const warnings = [];
164
+ let changeCount = 0;
165
+ const confidentMap = new Map(mappings.confident.map((m) => [m.filePath, m]));
166
+ for (const filePath of diff.added) {
167
+ const mapping = confidentMap.get(filePath);
168
+ if (mapping && mapping.possibleElements.length === 1) {
169
+ const element = mapping.possibleElements[0];
170
+ await stagingManager.stage(changeset.id, {
171
+ type: "add",
172
+ elementId: element.elementId,
173
+ layerName: element.layer,
174
+ after: {
175
+ sourceFile: filePath,
176
+ syncSource: "codebase",
177
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
178
+ },
179
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
180
+ }), warnings.push(`File added: ${filePath} -> ${element.elementId} (staged for review, verify content accuracy)`), changeCount++;
181
+ }
182
+ }
183
+ for (const filePath of diff.modified) {
184
+ const mapping = confidentMap.get(filePath);
185
+ if (mapping && mapping.possibleElements.length === 1) {
186
+ const element = mapping.possibleElements[0];
187
+ await stagingManager.stage(changeset.id, {
188
+ type: "update",
189
+ elementId: element.elementId,
190
+ layerName: element.layer,
191
+ before: {
192
+ sourceFile: filePath,
193
+ syncSource: "codebase"
194
+ },
195
+ after: {
196
+ sourceFile: filePath,
197
+ syncSource: "codebase",
198
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
199
+ },
200
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
201
+ }), warnings.push(`File modified: ${filePath} -> ${element.elementId} (staged for review)`), changeCount++;
202
+ }
203
+ }
204
+ for (const filePath of diff.deleted) {
205
+ const mapping = confidentMap.get(filePath);
206
+ if (mapping && mapping.possibleElements.length === 1) {
207
+ const element = mapping.possibleElements[0];
208
+ await stagingManager.stage(changeset.id, {
209
+ type: "delete",
210
+ elementId: element.elementId,
211
+ layerName: element.layer,
212
+ before: {
213
+ sourceFile: filePath,
214
+ syncSource: "codebase"
215
+ },
216
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
217
+ }), warnings.push(`File deleted: ${filePath} -> ${element.elementId} (staged for review)`), changeCount++;
218
+ }
219
+ }
220
+ if (mappings.ambiguous.length > 0 && !options.force)
221
+ for (const ambiguity of mappings.ambiguous)
222
+ warnings.push(`Ambiguous mapping: ${ambiguity.filePath} -> ${ambiguity.possibleElements.map((e) => e.elementId).join(", ")} (skipped, use --force to proceed)`);
223
+ return {
224
+ changesetId: changeset.id,
225
+ changeCount,
226
+ warnings
227
+ };
228
+ }
229
+ /**
230
+ * Execute a complete sync operation for a project
231
+ */
232
+ async syncProject(project, options = {}) {
233
+ const codebasePath = project.source, notes = [];
234
+ options.verbose && !options.dryRun && notes.push(`Pulling latest from ${codebasePath}...`);
235
+ const currentCommit = options.dryRun ? await this.getCurrentCommit(codebasePath) : await this.pullCodebase(codebasePath);
236
+ options.dryRun && notes.push("(DRY RUN - skipping git pull)");
237
+ const syncStatePath = path.join(this.farmRoot, project.model, ".farm-sync.yaml");
238
+ await ensureDir(path.dirname(syncStatePath));
239
+ const syncState = await FarmSyncState.loadOrCreate(syncStatePath, project.name), previousCommit = syncState.lastSyncCommit;
240
+ if (notes.push(`Current commit: ${currentCommit}`), notes.push(`Previous sync commit: ${previousCommit || "none"}`), !previousCommit)
241
+ return notes.push("Initial sync - recording baseline only"), options.dryRun ? notes.push("(DRY RUN - sync state not recorded)") : (syncState.recordSync({
242
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
243
+ commit: currentCommit,
244
+ status: "success",
245
+ files_changed: 0,
246
+ elements_affected: 0,
247
+ notes: "Initial sync, no changes to sync"
248
+ }), await syncState.save(syncStatePath)), {
249
+ status: "success",
250
+ projectName: project.name,
251
+ commitsBefore: "none",
252
+ commitsAfter: currentCommit.substring(0, 8),
253
+ filesChanged: { added: [], modified: [], deleted: [] },
254
+ elementMappings: [],
255
+ ambiguities: [],
256
+ changeCount: 0,
257
+ notes,
258
+ dryRun: options.dryRun
259
+ };
260
+ options.verbose && notes.push(`Computing diff between ${previousCommit.substring(0, 8)} and ${currentCommit.substring(0, 8)}...`);
261
+ const diff = await this.computeDiff(codebasePath, previousCommit, currentCommit);
262
+ if (diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0)
263
+ return notes.push("No changes detected"), options.dryRun ? notes.push("(DRY RUN - sync state not recorded)") : (syncState.recordSync({
264
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
265
+ commit: currentCommit,
266
+ status: "success",
267
+ files_changed: 0,
268
+ elements_affected: 0,
269
+ notes: "No changes to sync"
270
+ }), await syncState.save(syncStatePath)), {
271
+ status: "success",
272
+ projectName: project.name,
273
+ commitsBefore: previousCommit.substring(0, 8),
274
+ commitsAfter: currentCommit.substring(0, 8),
275
+ filesChanged: diff,
276
+ elementMappings: [],
277
+ ambiguities: [],
278
+ changeCount: 0,
279
+ notes,
280
+ dryRun: options.dryRun
281
+ };
282
+ if (notes.push(`Files changed: ${diff.added.length} added, ${diff.modified.length} modified, ${diff.deleted.length} deleted`), !this.model)
283
+ return notes.push("CRITICAL: Model is not loaded, cannot map files to elements"), options.dryRun ? notes.push("(DRY RUN - sync state not recorded)") : (syncState.recordSync({
284
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
285
+ commit: previousCommit,
286
+ // Don't advance lastSyncCommit - stay at previous commit
287
+ status: "failed",
288
+ files_changed: diff.added.length + diff.modified.length + diff.deleted.length,
289
+ elements_affected: 0,
290
+ notes: "Model not loaded - file changes not synced. Load the model and retry to process these files."
291
+ }), await syncState.save(syncStatePath)), {
292
+ status: "failed",
293
+ projectName: project.name,
294
+ commitsBefore: previousCommit.substring(0, 8),
295
+ commitsAfter: currentCommit.substring(0, 8),
296
+ filesChanged: diff,
297
+ elementMappings: [],
298
+ ambiguities: [],
299
+ changeCount: 0,
300
+ notes,
301
+ dryRun: options.dryRun
302
+ };
303
+ options.verbose && notes.push("Mapping changed files to model elements...");
304
+ const mappings = await this.mapFilesToElements();
305
+ mappings.ambiguous.length > 0 && (syncState.recordAmbiguities(mappings.ambiguous.map((m) => ({
306
+ filePath: m.filePath,
307
+ possibleElements: m.possibleElements.map((e) => ({
308
+ elementId: e.elementId,
309
+ layer: e.layer,
310
+ confidence: e.confidence
311
+ }))
312
+ }))), notes.push(`Ambiguities detected: ${mappings.ambiguous.length} files`));
313
+ let changesetId, changeCount = 0;
314
+ if (!options.dryRun) {
315
+ const changesetResult = await this.generateChangeset(project, diff, mappings, options);
316
+ changesetId = changesetResult.changesetId, changeCount = changesetResult.changeCount, notes.push(...changesetResult.warnings);
317
+ }
318
+ const syncStatus = mappings.ambiguous.length > 0 && !options.force ? "partial" : "success";
319
+ return options.dryRun ? notes.push("(DRY RUN - sync state not recorded)") : (syncState.recordSync({
320
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
321
+ commit: currentCommit,
322
+ status: syncStatus,
323
+ changeset: changesetId,
324
+ files_changed: diff.added.length + diff.modified.length + diff.deleted.length,
325
+ elements_affected: mappings.confident.length + mappings.ambiguous.length,
326
+ notes: notes.join(`
327
+ `)
328
+ }), await syncState.save(syncStatePath)), {
329
+ status: syncStatus,
330
+ projectName: project.name,
331
+ commitsBefore: previousCommit.substring(0, 8),
332
+ commitsAfter: currentCommit.substring(0, 8),
333
+ filesChanged: diff,
334
+ elementMappings: mappings.confident,
335
+ ambiguities: mappings.ambiguous,
336
+ changesetId,
337
+ changeCount,
338
+ notes,
339
+ dryRun: options.dryRun
340
+ };
341
+ }
342
+ }
343
+ export {
344
+ FarmSyncEngine
345
+ };
346
+ //# sourceMappingURL=farm-sync-engine.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/farm-sync-engine.ts"],
4
+ "sourcesContent": ["/**\n * Farm Sync Engine - Orchestrates headless sync pipeline\n *\n * Implements:\n * - Git pull for tracked codebases\n * - Diff computation between sync commits\n * - File-to-element mapping via source references\n * - Changeset generation for affected elements\n * - Staged changeset integration\n */\n\nimport path from \"path\";\nimport { execSync, execFileSync } from \"child_process\";\nimport { fileExists, ensureDir } from \"../utils/file-io.js\";\nimport { FarmSyncState } from \"./farm-sync-state.js\";\nimport { Model } from \"./model.js\";\nimport { StagingAreaManager } from \"./staging-area.js\";\nimport { getErrorMessage } from \"../utils/errors.js\";\nimport type { FarmProject } from \"./farm-manifest.js\";\n\n/**\n * Options for sync operation\n */\nexport interface SyncOptions {\n dryRun?: boolean; // Preview without creating changeset\n force?: boolean; // Proceed despite ambiguities\n verbose?: boolean; // Detailed output\n}\n\n/**\n * Result of file diff analysis\n */\nexport interface FileDiffResult {\n added: string[];\n modified: string[];\n deleted: string[];\n}\n\n/**\n * Mapping of file path to model elements\n */\nexport interface FileElementMapping {\n filePath: string;\n possibleElements: Array<{\n elementId: string;\n layer: string;\n confidence: number; // 0-100\n sourceRef: {\n file: string;\n symbol?: string;\n };\n }>;\n}\n\n/**\n * Result of a sync operation\n */\nexport interface SyncResult {\n status: \"success\" | \"partial\" | \"failed\";\n projectName: string;\n commitsBefore: string;\n commitsAfter: string;\n filesChanged: FileDiffResult;\n elementMappings: FileElementMapping[];\n ambiguities: FileElementMapping[]; // Mappings with low confidence\n changesetId?: string; // ID of generated changeset if applicable\n changeCount: number; // Total staged changes\n notes: string[];\n dryRun?: boolean; // True if this was a dry run (no state changed)\n}\n\n/**\n * FarmSyncEngine - Orchestrates the sync pipeline\n * Model parameter is optional - only required for sync operations, not for pull\n */\nexport class FarmSyncEngine {\n private farmRoot: string;\n private model: Model | null;\n\n constructor(farmRoot: string, model?: Model) {\n this.farmRoot = farmRoot;\n this.model = model || null;\n }\n\n /**\n * Pull latest changes from remote for a codebase\n * @param codebasePath - Relative path to codebase folder\n * @returns Updated commit SHA\n */\n async pullCodebase(codebasePath: string): Promise<string> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n try {\n // Try to fetch latest if remote exists\n try {\n execSync(\"git fetch --prune\", { cwd: fullPath, stdio: \"pipe\" });\n } catch (fetchError) {\n const fetchMsg = getErrorMessage(fetchError);\n // Only treat \"no remote\" as acceptable; re-throw auth, merge, and network errors\n const isNoRemote = fetchMsg.includes(\"No remote repository specified\") ||\n fetchMsg.includes(\"no remote\");\n if (!isNoRemote) {\n throw new Error(`git fetch failed: ${fetchMsg}`);\n }\n }\n\n // Try to pull if remote exists\n try {\n execSync(\"git pull --no-rebase\", { cwd: fullPath, stdio: \"pipe\" });\n } catch (pullError) {\n const pullMsg = getErrorMessage(pullError);\n // Only treat \"no remote\" or \"no tracking information\" as acceptable; re-throw auth, merge, and network errors\n const isAcceptable = pullMsg.includes(\"No remote repository specified\") ||\n pullMsg.includes(\"no remote\") ||\n pullMsg.includes(\"no tracking information\");\n if (!isAcceptable) {\n throw new Error(`git pull failed: ${pullMsg}`);\n }\n }\n\n // Get current HEAD commit\n const commit = execSync(\"git rev-parse HEAD\", { cwd: fullPath, encoding: \"utf-8\" }).trim();\n\n return commit;\n } catch (error) {\n throw new Error(`Failed to pull codebase '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Get the current HEAD commit SHA for a codebase\n */\n async getCurrentCommit(codebasePath: string): Promise<string> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n try {\n const commit = execSync(\"git rev-parse HEAD\", { cwd: fullPath, encoding: \"utf-8\" }).trim();\n return commit;\n } catch (error) {\n throw new Error(`Failed to get current commit for '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Compute diff between two commits\n * Commit SHAs are validated before use to prevent command injection\n */\n async computeDiff(codebasePath: string, fromCommit: string, toCommit: string): Promise<FileDiffResult> {\n const fullPath = path.join(this.farmRoot, codebasePath);\n\n if (!(await fileExists(fullPath))) {\n throw new Error(`Codebase directory not found: ${codebasePath}`);\n }\n\n // Validate commit SHAs to prevent command injection\n const commitShaRegex = /^[0-9a-f]{7,40}$/i;\n if (!commitShaRegex.test(fromCommit) || !commitShaRegex.test(toCommit)) {\n throw new Error(\n `Invalid commit SHA format. Expected 7-40 character hex string. Got: ${fromCommit}, ${toCommit}`\n );\n }\n\n try {\n // Get diff status between commits using execFileSync to avoid shell interpretation\n const diffOutput = execFileSync(\"git\", [\"diff\", \"--name-status\", `${fromCommit}...${toCommit}`], {\n cwd: fullPath,\n encoding: \"utf-8\",\n });\n\n const added: string[] = [];\n const modified: string[] = [];\n const deleted: string[] = [];\n\n for (const line of diffOutput.trim().split(\"\\n\")) {\n if (!line) continue;\n\n const [status, ...fileParts] = line.split(\"\\t\");\n const filePath = fileParts.join(\"\\t\"); // Handle paths with tabs\n\n switch (status) {\n case \"A\":\n added.push(filePath);\n break;\n case \"M\":\n modified.push(filePath);\n break;\n case \"D\":\n deleted.push(filePath);\n break;\n default:\n if (status.startsWith(\"R\")) {\n // Handle renames (R100, R087, R095, etc. - any similarity index)\n // Report as both a delete of the old name and add of the new name\n const [oldPath, newPath] = fileParts;\n deleted.push(oldPath);\n added.push(newPath);\n } else if (status.startsWith(\"C\")) {\n // Handle copies (C100, C087, etc. - any similarity index)\n // Report as add of new file\n added.push(fileParts[1]);\n }\n break;\n // Ignore other statuses\n }\n }\n\n return { added, modified, deleted };\n } catch (error) {\n throw new Error(`Failed to compute diff for '${codebasePath}': ${getErrorMessage(error)}`);\n }\n }\n\n /**\n * Map changed files to model elements using source references\n * Looks at each model element's source references and matches against changed files\n * Returns empty results if model is not available\n */\n async mapFilesToElements(): Promise<{ confident: FileElementMapping[]; ambiguous: FileElementMapping[] }> {\n if (!this.model) {\n return { confident: [], ambiguous: [] };\n }\n\n const confident: Map<string, FileElementMapping> = new Map();\n const ambiguous: Map<string, FileElementMapping> = new Map();\n\n // Iterate through all layers and elements\n for (const layer of this.model.layers.values()) {\n for (const element of layer.listElements()) {\n if (!element.source_reference) continue;\n\n const sourceRef = element.source_reference;\n if (!sourceRef.locations || sourceRef.locations.length === 0) continue;\n\n // Match each source location against potential changed files\n for (const location of sourceRef.locations) {\n // Use file path as key\n const filePath = location.file;\n\n if (!confident.has(filePath) && !ambiguous.has(filePath)) {\n const mapping: FileElementMapping = {\n filePath,\n possibleElements: [\n {\n elementId: element.id,\n layer: layer.name,\n confidence: 100, // Exact match from source reference\n sourceRef: location,\n },\n ],\n };\n confident.set(filePath, mapping);\n } else if (confident.has(filePath)) {\n // Multiple elements reference the same file - mark as ambiguous\n const existing = confident.get(filePath)!;\n confident.delete(filePath);\n\n existing.possibleElements.push({\n elementId: element.id,\n layer: layer.name,\n confidence: 100,\n sourceRef: location,\n });\n\n ambiguous.set(filePath, existing);\n } else {\n // Already ambiguous, add another possibility\n const existing = ambiguous.get(filePath)!;\n existing.possibleElements.push({\n elementId: element.id,\n layer: layer.name,\n confidence: 100,\n sourceRef: location,\n });\n }\n }\n }\n }\n\n return {\n confident: Array.from(confident.values()),\n ambiguous: Array.from(ambiguous.values()),\n };\n }\n\n /**\n * Generate a changeset from file-to-element mappings\n * Creates add/update/delete changes based on the diff and mappings\n * Stages changes into the changeset for review before commit\n */\n async generateChangeset(\n project: FarmProject,\n diff: FileDiffResult,\n mappings: { confident: FileElementMapping[]; ambiguous: FileElementMapping[] },\n options: SyncOptions = {}\n ): Promise<{ changesetId: string; changeCount: number; warnings: string[] }> {\n if (!this.model) {\n throw new Error(\"Model is required for changeset generation\");\n }\n\n const projectModelPath = path.join(this.farmRoot, project.model);\n const stagingManager = new StagingAreaManager(projectModelPath, this.model);\n\n // Create a new changeset\n const changesetName = `farm-sync-${project.name}-${Date.now()}`;\n const changeset = await stagingManager.create(changesetName, `Auto-generated from farm sync of ${project.name}`);\n\n if (!changeset.id) {\n throw new Error(\"Failed to create changeset - no ID assigned\");\n }\n\n const warnings: string[] = [];\n let changeCount = 0;\n\n // Create a map of file path -> confident mapping for quick lookup\n const confidentMap = new Map(mappings.confident.map((m) => [m.filePath, m]));\n\n // Process added files\n for (const filePath of diff.added) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"add\",\n elementId: element.elementId,\n layerName: element.layer,\n after: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n timestamp: new Date().toISOString(),\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(\n `File added: ${filePath} -> ${element.elementId} (staged for review, verify content accuracy)`\n );\n changeCount++;\n }\n }\n\n // Process modified files\n for (const filePath of diff.modified) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"update\",\n elementId: element.elementId,\n layerName: element.layer,\n before: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n },\n after: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n timestamp: new Date().toISOString(),\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(`File modified: ${filePath} -> ${element.elementId} (staged for review)`);\n changeCount++;\n }\n }\n\n // Process deleted files\n for (const filePath of diff.deleted) {\n const mapping = confidentMap.get(filePath);\n if (mapping && mapping.possibleElements.length === 1) {\n const element = mapping.possibleElements[0];\n // Stage the change\n await stagingManager.stage(changeset.id, {\n type: \"delete\",\n elementId: element.elementId,\n layerName: element.layer,\n before: {\n sourceFile: filePath,\n syncSource: \"codebase\",\n },\n timestamp: new Date().toISOString(),\n });\n warnings.push(`File deleted: ${filePath} -> ${element.elementId} (staged for review)`);\n changeCount++;\n }\n }\n\n // Flag ambiguities\n if (mappings.ambiguous.length > 0 && !options.force) {\n for (const ambiguity of mappings.ambiguous) {\n warnings.push(\n `Ambiguous mapping: ${ambiguity.filePath} -> ${ambiguity.possibleElements.map((e) => e.elementId).join(\", \")} (skipped, use --force to proceed)`\n );\n }\n }\n\n return {\n changesetId: changeset.id,\n changeCount,\n warnings,\n };\n }\n\n /**\n * Execute a complete sync operation for a project\n */\n async syncProject(project: FarmProject, options: SyncOptions = {}): Promise<SyncResult> {\n const codebasePath = project.source;\n const notes: string[] = [];\n\n // Step 1: Get current commit (pull only if not dry run)\n if (options.verbose && !options.dryRun) {\n notes.push(`Pulling latest from ${codebasePath}...`);\n }\n const currentCommit = options.dryRun\n ? await this.getCurrentCommit(codebasePath)\n : await this.pullCodebase(codebasePath);\n if (options.dryRun) {\n notes.push(\"(DRY RUN - skipping git pull)\");\n }\n\n // Step 2: Load sync state\n const syncStatePath = path.join(this.farmRoot, project.model, \".farm-sync.yaml\");\n await ensureDir(path.dirname(syncStatePath));\n const syncState = await FarmSyncState.loadOrCreate(syncStatePath, project.name);\n\n const previousCommit = syncState.lastSyncCommit;\n notes.push(`Current commit: ${currentCommit}`);\n notes.push(`Previous sync commit: ${previousCommit || \"none\"}`);\n\n // Step 3: If no previous sync, this is initial sync - just record state\n if (!previousCommit) {\n notes.push(\"Initial sync - recording baseline only\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: \"success\",\n files_changed: 0,\n elements_affected: 0,\n notes: \"Initial sync, no changes to sync\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"success\",\n projectName: project.name,\n commitsBefore: \"none\",\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: { added: [], modified: [], deleted: [] },\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n // Step 4: Compute diff\n if (options.verbose) {\n notes.push(`Computing diff between ${previousCommit.substring(0, 8)} and ${currentCommit.substring(0, 8)}...`);\n }\n const diff = await this.computeDiff(codebasePath, previousCommit, currentCommit);\n\n // If no changes, return early\n if (diff.added.length === 0 && diff.modified.length === 0 && diff.deleted.length === 0) {\n notes.push(\"No changes detected\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: \"success\",\n files_changed: 0,\n elements_affected: 0,\n notes: \"No changes to sync\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"success\",\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n notes.push(\n `Files changed: ${diff.added.length} added, ${diff.modified.length} modified, ${diff.deleted.length} deleted`\n );\n\n // Step 5: Check that model is available before attempting to map files\n if (!this.model) {\n notes.push(\"CRITICAL: Model is not loaded, cannot map files to elements\");\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: previousCommit, // Don't advance lastSyncCommit - stay at previous commit\n status: \"failed\",\n files_changed: diff.added.length + diff.modified.length + diff.deleted.length,\n elements_affected: 0,\n notes: \"Model not loaded - file changes not synced. Load the model and retry to process these files.\",\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: \"failed\",\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: [],\n ambiguities: [],\n changeCount: 0,\n notes,\n dryRun: options.dryRun,\n };\n }\n\n // Step 6: Map files to elements\n if (options.verbose) {\n notes.push(\"Mapping changed files to model elements...\");\n }\n const mappings = await this.mapFilesToElements();\n\n // Step 7: Record ambiguities in sync state\n if (mappings.ambiguous.length > 0) {\n syncState.recordAmbiguities(\n mappings.ambiguous.map((m) => ({\n filePath: m.filePath,\n possibleElements: m.possibleElements.map((e) => ({\n elementId: e.elementId,\n layer: e.layer,\n confidence: e.confidence,\n })),\n }))\n );\n notes.push(`Ambiguities detected: ${mappings.ambiguous.length} files`);\n }\n\n // Step 8: Generate changeset\n let changesetId: string | undefined;\n let changeCount = 0;\n\n if (!options.dryRun) {\n const changesetResult = await this.generateChangeset(project, diff, mappings, options);\n changesetId = changesetResult.changesetId;\n changeCount = changesetResult.changeCount;\n notes.push(...changesetResult.warnings);\n }\n\n // Step 9: Record sync in state (skip for dry run)\n const syncStatus = mappings.ambiguous.length > 0 && !options.force ? \"partial\" : \"success\";\n\n if (!options.dryRun) {\n syncState.recordSync({\n timestamp: new Date().toISOString(),\n commit: currentCommit,\n status: syncStatus,\n changeset: changesetId,\n files_changed: diff.added.length + diff.modified.length + diff.deleted.length,\n elements_affected: mappings.confident.length + mappings.ambiguous.length,\n notes: notes.join(\"\\n\"),\n });\n await syncState.save(syncStatePath);\n } else {\n notes.push(\"(DRY RUN - sync state not recorded)\");\n }\n\n return {\n status: syncStatus,\n projectName: project.name,\n commitsBefore: previousCommit.substring(0, 8),\n commitsAfter: currentCommit.substring(0, 8),\n filesChanged: diff,\n elementMappings: mappings.confident,\n ambiguities: mappings.ambiguous,\n changesetId,\n changeCount,\n notes,\n dryRun: options.dryRun,\n };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,159 @@
1
+ import yaml from "yaml";
2
+ import { readFile, writeFile, fileExists } from "../utils/file-io.js";
3
+ class FarmSyncState {
4
+ projectName;
5
+ lastSyncCommit;
6
+ lastSyncTimestamp;
7
+ syncHistory;
8
+ ambiguities;
9
+ filePath;
10
+ constructor(data) {
11
+ this.projectName = data.project_name, this.lastSyncCommit = data.last_sync_commit, this.lastSyncTimestamp = data.last_sync_timestamp, this.syncHistory = data.sync_history || [], this.ambiguities = data.ambiguities?.map((a) => ({
12
+ filePath: a.file_path,
13
+ possibleElements: a.possible_elements.map((e) => ({
14
+ elementId: e.element_id,
15
+ layer: e.layer,
16
+ confidence: e.confidence
17
+ }))
18
+ })) || [];
19
+ }
20
+ /**
21
+ * Load farm sync state from file
22
+ * @param filePath - Path to .farm-sync.yaml
23
+ * @returns FarmSyncState instance
24
+ */
25
+ static async load(filePath) {
26
+ if (!await fileExists(filePath))
27
+ throw new Error(`Farm sync state file not found: ${filePath}`);
28
+ const content = await readFile(filePath), rawData = yaml.parse(content);
29
+ if (!rawData)
30
+ throw new Error(`Invalid farm sync state: file is empty or contains invalid YAML at ${filePath}`);
31
+ const data = {
32
+ project_name: rawData.project_name || rawData.projectName,
33
+ last_sync_commit: rawData.last_sync_commit || rawData.lastSyncCommit,
34
+ last_sync_timestamp: rawData.last_sync_timestamp || rawData.lastSyncTimestamp,
35
+ sync_history: (rawData.sync_history || rawData.syncHistory || []).map((record) => ({
36
+ timestamp: record.timestamp,
37
+ commit: record.commit,
38
+ changeset: record.changeset || record.changesetId,
39
+ files_changed: record.files_changed || record.filesChanged,
40
+ elements_affected: record.elements_affected || record.elementsAffected,
41
+ status: record.status,
42
+ notes: record.notes
43
+ })),
44
+ ambiguities: (rawData.ambiguities || []).map((a) => ({
45
+ file_path: a.file_path || a.filePath,
46
+ possible_elements: (a.possible_elements || a.possibleElements || []).map((e) => ({
47
+ element_id: e.element_id || e.elementId,
48
+ layer: e.layer,
49
+ confidence: e.confidence
50
+ }))
51
+ }))
52
+ };
53
+ if (!data.project_name)
54
+ throw new Error("Farm sync state must have a 'project_name' field");
55
+ const state = new FarmSyncState(data);
56
+ return state.filePath = filePath, state;
57
+ }
58
+ /**
59
+ * Create a new farm sync state
60
+ * @param projectName - Name of the project
61
+ * @param options - Optional initial state
62
+ * @returns FarmSyncState instance
63
+ */
64
+ static create(projectName, options = {}) {
65
+ return new FarmSyncState({
66
+ project_name: projectName,
67
+ last_sync_commit: options.lastSyncCommit,
68
+ last_sync_timestamp: options.lastSyncTimestamp,
69
+ sync_history: [],
70
+ ambiguities: []
71
+ });
72
+ }
73
+ /**
74
+ * Load or create farm sync state
75
+ * @param filePath - Path to .farm-sync.yaml
76
+ * @param projectName - Project name (used if creating new)
77
+ * @returns FarmSyncState instance
78
+ */
79
+ static async loadOrCreate(filePath, projectName) {
80
+ return await fileExists(filePath) ? this.load(filePath) : this.create(projectName);
81
+ }
82
+ /**
83
+ * Save state to file
84
+ * @param filePath - Path where to save .farm-sync.yaml
85
+ */
86
+ async save(filePath) {
87
+ const targetPath = filePath || this.filePath;
88
+ if (!targetPath)
89
+ throw new Error("No file path specified for saving farm sync state");
90
+ const data = this.toJSON(), yamlContent = yaml.stringify(data, { lineWidth: 0 });
91
+ await writeFile(targetPath, yamlContent), this.filePath = targetPath;
92
+ }
93
+ /**
94
+ * Record a sync (successful or failed)
95
+ */
96
+ recordSync(record) {
97
+ const status = record.status || "success";
98
+ (status === "success" || status === "partial") && (this.lastSyncCommit = record.commit, this.lastSyncTimestamp = record.timestamp), this.syncHistory.push({
99
+ timestamp: record.timestamp,
100
+ commit: record.commit,
101
+ status,
102
+ changeset: record.changeset,
103
+ files_changed: record.files_changed,
104
+ elements_affected: record.elements_affected,
105
+ notes: record.notes
106
+ }), this.syncHistory.length > 100 && (this.syncHistory = this.syncHistory.slice(-100));
107
+ }
108
+ /**
109
+ * Record ambiguous file-to-element mappings for review
110
+ */
111
+ recordAmbiguities(ambiguities) {
112
+ this.ambiguities = ambiguities;
113
+ }
114
+ /**
115
+ * Clear ambiguities after review
116
+ */
117
+ clearAmbiguities() {
118
+ this.ambiguities = [];
119
+ }
120
+ /**
121
+ * Get the last sync record if any
122
+ */
123
+ getLastSync() {
124
+ return this.syncHistory.length > 0 ? this.syncHistory[this.syncHistory.length - 1] : void 0;
125
+ }
126
+ /**
127
+ * Serialize to JSON representation for saving
128
+ */
129
+ toJSON() {
130
+ return {
131
+ project_name: this.projectName,
132
+ last_sync_commit: this.lastSyncCommit,
133
+ last_sync_timestamp: this.lastSyncTimestamp,
134
+ sync_history: this.syncHistory.map((record) => ({
135
+ timestamp: record.timestamp,
136
+ commit: record.commit,
137
+ changeset: record.changeset,
138
+ files_changed: record.files_changed,
139
+ elements_affected: record.elements_affected,
140
+ status: record.status,
141
+ notes: record.notes
142
+ })),
143
+ ...this.ambiguities.length > 0 && {
144
+ ambiguities: this.ambiguities.map((a) => ({
145
+ file_path: a.filePath,
146
+ possible_elements: a.possibleElements.map((e) => ({
147
+ element_id: e.elementId,
148
+ layer: e.layer,
149
+ confidence: e.confidence
150
+ }))
151
+ }))
152
+ }
153
+ };
154
+ }
155
+ }
156
+ export {
157
+ FarmSyncState
158
+ };
159
+ //# sourceMappingURL=farm-sync-state.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/core/farm-sync-state.ts"],
4
+ "sourcesContent": ["/**\n * Farm Sync State - Manages .farm-sync.yaml per project\n *\n * Tracks sync history, last sync commit, and timestamps for each project.\n */\n\nimport yaml from \"yaml\";\nimport { readFile, writeFile, fileExists } from \"../utils/file-io.js\";\n\n/**\n * Represents a single sync record\n */\nexport interface SyncRecord {\n timestamp: string; // ISO 8601 timestamp\n commit: string; // Commit SHA where sync occurred\n changeset?: string; // ID of generated changeset\n files_changed?: number; // Number of files that changed\n elements_affected?: number; // Number of model elements affected\n status: \"success\" | \"partial\" | \"failed\"; // Status of the sync\n notes?: string; // Optional notes about the sync\n}\n\n/**\n * Farm sync state for a single project\n */\nexport interface FarmSyncStateData {\n project_name: string;\n last_sync_commit?: string; // Last successful sync commit SHA\n last_sync_timestamp?: string; // ISO 8601 timestamp\n sync_history: SyncRecord[]; // Complete history of syncs\n ambiguities?: Array<{\n file_path: string;\n possible_elements: Array<{\n element_id: string;\n layer: string;\n confidence: number; // 0-100 confidence score\n }>;\n }>; // Ambiguous file-to-element mappings flagged for review\n}\n\n/**\n * FarmSyncState class - Load, validate, and save farm sync state per project\n */\nexport class FarmSyncState {\n projectName: string;\n lastSyncCommit?: string;\n lastSyncTimestamp?: string;\n syncHistory: SyncRecord[];\n ambiguities: Array<{\n filePath: string;\n possibleElements: Array<{\n elementId: string;\n layer: string;\n confidence: number;\n }>;\n }>;\n filePath?: string;\n\n constructor(data: FarmSyncStateData) {\n this.projectName = data.project_name;\n this.lastSyncCommit = data.last_sync_commit;\n this.lastSyncTimestamp = data.last_sync_timestamp;\n this.syncHistory = data.sync_history || [];\n this.ambiguities = data.ambiguities?.map(a => ({\n filePath: a.file_path,\n possibleElements: a.possible_elements.map(e => ({\n elementId: e.element_id,\n layer: e.layer,\n confidence: e.confidence,\n })),\n })) || [];\n }\n\n /**\n * Load farm sync state from file\n * @param filePath - Path to .farm-sync.yaml\n * @returns FarmSyncState instance\n */\n static async load(filePath: string): Promise<FarmSyncState> {\n if (!(await fileExists(filePath))) {\n throw new Error(`Farm sync state file not found: ${filePath}`);\n }\n\n const content = await readFile(filePath);\n const rawData = yaml.parse(content) as any;\n\n if (!rawData) {\n throw new Error(\n `Invalid farm sync state: file is empty or contains invalid YAML at ${filePath}`\n );\n }\n\n // Handle both old (camelCase) and new (snake_case) formats for backward compatibility\n const data: FarmSyncStateData = {\n project_name: rawData.project_name || rawData.projectName,\n last_sync_commit: rawData.last_sync_commit || rawData.lastSyncCommit,\n last_sync_timestamp: rawData.last_sync_timestamp || rawData.lastSyncTimestamp,\n sync_history: (rawData.sync_history || rawData.syncHistory || []).map((record: any) => ({\n timestamp: record.timestamp,\n commit: record.commit,\n changeset: record.changeset || record.changesetId,\n files_changed: record.files_changed || record.filesChanged,\n elements_affected: record.elements_affected || record.elementsAffected,\n status: record.status,\n notes: record.notes,\n })),\n ambiguities: (rawData.ambiguities || []).map((a: any) => ({\n file_path: a.file_path || a.filePath,\n possible_elements: (a.possible_elements || a.possibleElements || []).map((e: any) => ({\n element_id: e.element_id || e.elementId,\n layer: e.layer,\n confidence: e.confidence,\n })),\n })),\n };\n\n if (!data.project_name) {\n throw new Error(\"Farm sync state must have a 'project_name' field\");\n }\n\n const state = new FarmSyncState(data);\n state.filePath = filePath;\n return state;\n }\n\n /**\n * Create a new farm sync state\n * @param projectName - Name of the project\n * @param options - Optional initial state\n * @returns FarmSyncState instance\n */\n static create(\n projectName: string,\n options: {\n lastSyncCommit?: string;\n lastSyncTimestamp?: string;\n } = {}\n ): FarmSyncState {\n return new FarmSyncState({\n project_name: projectName,\n last_sync_commit: options.lastSyncCommit,\n last_sync_timestamp: options.lastSyncTimestamp,\n sync_history: [],\n ambiguities: [],\n });\n }\n\n /**\n * Load or create farm sync state\n * @param filePath - Path to .farm-sync.yaml\n * @param projectName - Project name (used if creating new)\n * @returns FarmSyncState instance\n */\n static async loadOrCreate(filePath: string, projectName: string): Promise<FarmSyncState> {\n if (await fileExists(filePath)) {\n return this.load(filePath);\n }\n return this.create(projectName);\n }\n\n /**\n * Save state to file\n * @param filePath - Path where to save .farm-sync.yaml\n */\n async save(filePath?: string): Promise<void> {\n const targetPath = filePath || this.filePath;\n if (!targetPath) {\n throw new Error(\"No file path specified for saving farm sync state\");\n }\n\n const data = this.toJSON();\n const yamlContent = yaml.stringify(data, { lineWidth: 0 });\n\n await writeFile(targetPath, yamlContent);\n this.filePath = targetPath;\n }\n\n /**\n * Record a sync (successful or failed)\n */\n recordSync(record: Omit<SyncRecord, \"status\"> & { status?: \"success\" | \"partial\" | \"failed\" }): void {\n const status = record.status || \"success\";\n\n // Update lastSyncCommit on successful and partial syncs to avoid reprocessing confident files.\n // Failed syncs do not advance the pointer (will retry from same point).\n // Partial syncs advance because confident files have been processed and staged;\n // ambiguous files are tracked separately for manual review.\n if (status === \"success\" || status === \"partial\") {\n this.lastSyncCommit = record.commit;\n this.lastSyncTimestamp = record.timestamp;\n }\n\n this.syncHistory.push({\n timestamp: record.timestamp,\n commit: record.commit,\n status,\n changeset: record.changeset,\n files_changed: record.files_changed,\n elements_affected: record.elements_affected,\n notes: record.notes,\n });\n\n // Keep history trimmed to last 100 syncs\n if (this.syncHistory.length > 100) {\n this.syncHistory = this.syncHistory.slice(-100);\n }\n }\n\n /**\n * Record ambiguous file-to-element mappings for review\n */\n recordAmbiguities(\n ambiguities: Array<{\n filePath: string;\n possibleElements: Array<{\n elementId: string;\n layer: string;\n confidence: number;\n }>;\n }>\n ): void {\n this.ambiguities = ambiguities;\n }\n\n /**\n * Clear ambiguities after review\n */\n clearAmbiguities(): void {\n this.ambiguities = [];\n }\n\n /**\n * Get the last sync record if any\n */\n getLastSync(): SyncRecord | undefined {\n return this.syncHistory.length > 0 ? this.syncHistory[this.syncHistory.length - 1] : undefined;\n }\n\n /**\n * Serialize to JSON representation for saving\n */\n toJSON(): FarmSyncStateData {\n return {\n project_name: this.projectName,\n last_sync_commit: this.lastSyncCommit,\n last_sync_timestamp: this.lastSyncTimestamp,\n sync_history: this.syncHistory.map(record => ({\n timestamp: record.timestamp,\n commit: record.commit,\n changeset: record.changeset,\n files_changed: record.files_changed,\n elements_affected: record.elements_affected,\n status: record.status,\n notes: record.notes,\n })),\n ...(this.ambiguities.length > 0 && {\n ambiguities: this.ambiguities.map(a => ({\n file_path: a.filePath,\n possible_elements: a.possibleElements.map(e => ({\n element_id: e.elementId,\n layer: e.layer,\n confidence: e.confidence,\n })),\n }))\n }),\n };\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -5,10 +5,13 @@ class Manifest {
5
5
  author;
6
6
  created;
7
7
  modified;
8
+ cliVersion;
8
9
  specVersion;
9
10
  changeset_history;
11
+ models;
12
+ codebase_path;
10
13
  constructor(data) {
11
- this.name = data.name, this.version = data.version, this.description = data.description, this.author = data.author, this.created = data.created ?? (/* @__PURE__ */ new Date()).toISOString(), this.modified = data.modified ?? (/* @__PURE__ */ new Date()).toISOString(), this.specVersion = data.specVersion, this.changeset_history = this.migrateChangesetHistory(data.changeset_history);
14
+ this.name = data.name, this.version = data.version, this.description = data.description, this.author = data.author, this.created = data.created ?? (/* @__PURE__ */ new Date()).toISOString(), this.modified = data.modified ?? (/* @__PURE__ */ new Date()).toISOString(), this.cliVersion = data.cliVersion, this.specVersion = data.specVersion, this.models = data.models, this.codebase_path = data.codebase_path, this.changeset_history = this.migrateChangesetHistory(data.changeset_history);
12
15
  }
13
16
  /**
14
17
  * Migrate changeset history from legacy field names to new format
@@ -45,7 +48,7 @@ class Manifest {
45
48
  * Serialize to JSON representation
46
49
  *
47
50
  * Converts the Manifest to a JSON-serializable object that includes project
48
- * metadata and changeset history.
51
+ * metadata, changeset history, and external model declarations.
49
52
  *
50
53
  * @returns ManifestData object suitable for JSON serialization
51
54
  */
@@ -56,7 +59,7 @@ class Manifest {
56
59
  created: this.created,
57
60
  modified: this.modified
58
61
  };
59
- return this.description && (result.description = this.description), this.author && (result.author = this.author), this.specVersion && (result.specVersion = this.specVersion), this.changeset_history && this.changeset_history.length > 0 && (result.changeset_history = this.changeset_history), result;
62
+ return this.description && (result.description = this.description), this.author && (result.author = this.author), this.cliVersion && (result.cliVersion = this.cliVersion), this.specVersion && (result.specVersion = this.specVersion), this.codebase_path && (result.codebase_path = this.codebase_path), this.changeset_history && this.changeset_history.length > 0 && (result.changeset_history = this.changeset_history), this.models && Object.keys(this.models).length > 0 && (result.models = this.models), result;
60
63
  }
61
64
  /**
62
65
  * Create Manifest from JSON string