@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.
- package/README.md +38 -0
- package/dist/analyzers/verify-engine.js +22 -19
- package/dist/analyzers/verify-engine.js.map +2 -2
- package/dist/cli.js +101 -27
- package/dist/cli.js.map +3 -3
- package/dist/coding-agents/agents/claude-code.js +3 -1
- package/dist/coding-agents/agents/claude-code.js.map +2 -2
- package/dist/coding-agents/claude-code-client.js +2 -1
- package/dist/coding-agents/claude-code-client.js.map +2 -2
- package/dist/coding-agents/copilot-client.js +17 -12
- package/dist/coding-agents/copilot-client.js.map +2 -2
- package/dist/coding-agents/default-permissions.js +122 -0
- package/dist/coding-agents/default-permissions.js.map +7 -0
- package/dist/commands/add.js +10 -6
- package/dist/commands/add.js.map +2 -2
- package/dist/commands/analyzer.js +18 -10
- package/dist/commands/analyzer.js.map +2 -2
- package/dist/commands/audit.js +6 -6
- package/dist/commands/audit.js.map +2 -2
- package/dist/commands/changeset.js +238 -145
- package/dist/commands/changeset.js.map +2 -2
- package/dist/commands/delete.js +45 -18
- package/dist/commands/delete.js.map +3 -3
- package/dist/commands/element.js +3 -2
- package/dist/commands/element.js.map +2 -2
- package/dist/commands/farm.js +657 -0
- package/dist/commands/farm.js.map +7 -0
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +2 -2
- package/dist/commands/mcp.js +107 -0
- package/dist/commands/mcp.js.map +7 -0
- package/dist/commands/relationship.js +53 -30
- package/dist/commands/relationship.js.map +2 -2
- package/dist/commands/reports.js +1 -1
- package/dist/commands/reports.js.map +2 -2
- package/dist/commands/update.js +19 -6
- package/dist/commands/update.js.map +2 -2
- package/dist/commands/validate.js +41 -3
- package/dist/commands/validate.js.map +2 -2
- package/dist/commands/version.js +2 -2
- package/dist/commands/version.js.map +2 -2
- package/dist/config.js +83 -0
- package/dist/config.js.map +7 -0
- package/dist/core/annotation-store.js +138 -0
- package/dist/core/annotation-store.js.map +7 -0
- package/dist/core/farm-manifest.js +134 -0
- package/dist/core/farm-manifest.js.map +7 -0
- package/dist/core/farm-sync-engine.js +346 -0
- package/dist/core/farm-sync-engine.js.map +7 -0
- package/dist/core/farm-sync-state.js +159 -0
- package/dist/core/farm-sync-state.js.map +7 -0
- package/dist/core/manifest.js +6 -3
- package/dist/core/manifest.js.map +2 -2
- package/dist/core/migration-registry.js +55 -1
- package/dist/core/migration-registry.js.map +2 -2
- package/dist/core/model.js +177 -16
- package/dist/core/model.js.map +3 -3
- package/dist/core/virtual-projection.js +1 -0
- package/dist/core/virtual-projection.js.map +2 -2
- package/dist/generated/node-types.js +1 -1
- package/dist/generated/node-types.js.map +2 -2
- package/dist/generated/relationship-index.js +45 -9
- package/dist/generated/relationship-index.js.map +2 -2
- package/dist/integrations/claude_code/docs/MCP_SERVER.md +55 -0
- package/dist/integrations/claude_code/docs/README.md +4 -0
- package/dist/mcp/api-key-manager.js +98 -0
- package/dist/mcp/api-key-manager.js.map +7 -0
- package/dist/mcp/http-transport.js +176 -0
- package/dist/mcp/http-transport.js.map +7 -0
- package/dist/mcp/resource-registry.js +97 -0
- package/dist/mcp/resource-registry.js.map +7 -0
- package/dist/mcp/tool-registry.js +44 -0
- package/dist/mcp/tool-registry.js.map +7 -0
- package/dist/mcp/tools/annotation-create.js +39 -0
- package/dist/mcp/tools/annotation-create.js.map +7 -0
- package/dist/mcp/tools/annotation-delete.js +29 -0
- package/dist/mcp/tools/annotation-delete.js.map +7 -0
- package/dist/mcp/tools/annotation-get.js +29 -0
- package/dist/mcp/tools/annotation-get.js.map +7 -0
- package/dist/mcp/tools/annotation-list.js +24 -0
- package/dist/mcp/tools/annotation-list.js.map +7 -0
- package/dist/mcp/tools/annotation-reply.js +34 -0
- package/dist/mcp/tools/annotation-reply.js.map +7 -0
- package/dist/mcp/tools/annotation-shared.js +10 -0
- package/dist/mcp/tools/annotation-shared.js.map +7 -0
- package/dist/mcp/tools/annotation-update.js +39 -0
- package/dist/mcp/tools/annotation-update.js.map +7 -0
- package/dist/mcp/tools/changeset-list.js +33 -0
- package/dist/mcp/tools/changeset-list.js.map +7 -0
- package/dist/mcp/tools/changeset-show.js +39 -0
- package/dist/mcp/tools/changeset-show.js.map +7 -0
- package/dist/mcp/tools/chat-launch.js +50 -0
- package/dist/mcp/tools/chat-launch.js.map +7 -0
- package/dist/mcp/tools/chat-status.js +26 -0
- package/dist/mcp/tools/chat-status.js.map +7 -0
- package/dist/mcp/tools/index.js +47 -0
- package/dist/mcp/tools/index.js.map +7 -0
- package/dist/mcp/tools/model-add.js +106 -0
- package/dist/mcp/tools/model-add.js.map +7 -0
- package/dist/mcp/tools/model-delete.js +99 -0
- package/dist/mcp/tools/model-delete.js.map +7 -0
- package/dist/mcp/tools/model-export.js +72 -0
- package/dist/mcp/tools/model-export.js.map +7 -0
- package/dist/mcp/tools/model-info.js +51 -0
- package/dist/mcp/tools/model-info.js.map +7 -0
- package/dist/mcp/tools/model-list.js +53 -0
- package/dist/mcp/tools/model-list.js.map +7 -0
- package/dist/mcp/tools/model-reload.js +34 -0
- package/dist/mcp/tools/model-reload.js.map +7 -0
- package/dist/mcp/tools/model-search.js +88 -0
- package/dist/mcp/tools/model-search.js.map +7 -0
- package/dist/mcp/tools/model-show.js +41 -0
- package/dist/mcp/tools/model-show.js.map +7 -0
- package/dist/mcp/tools/model-stats.js +22 -0
- package/dist/mcp/tools/model-stats.js.map +7 -0
- package/dist/mcp/tools/model-trace.js +55 -0
- package/dist/mcp/tools/model-trace.js.map +7 -0
- package/dist/mcp/tools/model-update.js +105 -0
- package/dist/mcp/tools/model-update.js.map +7 -0
- package/dist/mcp/tools/model-validate.js +78 -0
- package/dist/mcp/tools/model-validate.js.map +7 -0
- package/dist/mcp/tools/shared.js +103 -0
- package/dist/mcp/tools/shared.js.map +7 -0
- package/dist/reports/model-layer-report-generator.js +5 -3
- package/dist/reports/model-layer-report-generator.js.map +2 -2
- package/dist/reports/model-readme-generator.js +116 -0
- package/dist/reports/model-readme-generator.js.map +7 -0
- package/dist/reports/model-report-data.js +37 -1
- package/dist/reports/model-report-data.js.map +2 -2
- package/dist/reports/model-report-orchestrator.js +29 -6
- package/dist/reports/model-report-orchestrator.js.map +2 -2
- package/dist/reports/model-report-utils.js +9 -0
- package/dist/reports/model-report-utils.js.map +7 -0
- package/dist/schemas/bundled/analyzers/cbm.json +2 -2
- package/dist/schemas/bundled/analyzers/manifest.json +2 -2
- package/dist/schemas/bundled/api.json +1 -1
- package/dist/schemas/bundled/apm.json +1 -1
- package/dist/schemas/bundled/application.json +12 -1
- package/dist/schemas/bundled/base.json +1 -1
- package/dist/schemas/bundled/business.json +1 -1
- package/dist/schemas/bundled/data-model.json +38 -2
- package/dist/schemas/bundled/data-store.json +1 -1
- package/dist/schemas/bundled/manifest.json +4 -4
- package/dist/schemas/bundled/motivation.json +1 -1
- package/dist/schemas/bundled/navigation.json +1 -1
- package/dist/schemas/bundled/product.json +1 -1
- package/dist/schemas/bundled/security.json +1 -1
- package/dist/schemas/bundled/technology.json +1 -1
- package/dist/schemas/bundled/testing.json +1 -1
- package/dist/schemas/bundled/ux.json +1 -1
- package/dist/server/server.js +22 -3
- package/dist/server/server.js.map +2 -2
- package/dist/telemetry/config.js +2 -40
- package/dist/telemetry/config.js.map +2 -2
- package/dist/telemetry/index.js +1 -1
- package/dist/types/index.js.map +2 -2
- package/dist/utils/ansi-suppressor.js +25 -0
- package/dist/utils/ansi-suppressor.js.map +7 -0
- package/dist/utils/color-utils.js +34 -0
- package/dist/utils/color-utils.js.map +7 -0
- package/dist/utils/element-reference-shape.js +15 -1
- package/dist/utils/element-reference-shape.js.map +3 -3
- package/dist/utils/errors.js +69 -10
- package/dist/utils/errors.js.map +2 -2
- package/dist/utils/globals.js +17 -1
- package/dist/utils/globals.js.map +2 -2
- package/dist/utils/project-paths.js +24 -2
- package/dist/utils/project-paths.js.map +2 -2
- package/dist/utils/reference-path-parser.js +49 -0
- package/dist/utils/reference-path-parser.js.map +7 -0
- package/dist/utils/source-reference.js +5 -0
- package/dist/utils/source-reference.js.map +2 -2
- package/dist/utils/spec-version.js +10 -2
- package/dist/utils/spec-version.js.map +2 -2
- package/dist/validators/composed-reference-validator.js +262 -0
- package/dist/validators/composed-reference-validator.js.map +7 -0
- package/dist/validators/reference-validator.js +59 -8
- package/dist/validators/reference-validator.js.map +3 -3
- package/dist/validators/relationship-schema-validator.js +30 -2
- package/dist/validators/relationship-schema-validator.js.map +3 -3
- 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
|
+
}
|
package/dist/core/manifest.js
CHANGED
|
@@ -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
|
|
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
|