trellis 2.0.13 → 2.1.2
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/dist/cli/index.js +1 -1
- package/dist/embeddings/index.js +1 -1
- package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
- package/package.json +2 -10
- package/dist/transformers.node-bx3q9d7k.js +0 -33130
- package/src/cli/index.ts +0 -3356
- package/src/core/agents/harness.ts +0 -380
- package/src/core/agents/index.ts +0 -18
- package/src/core/agents/types.ts +0 -90
- package/src/core/index.ts +0 -118
- package/src/core/kernel/middleware.ts +0 -44
- package/src/core/kernel/trellis-kernel.ts +0 -593
- package/src/core/ontology/builtins.ts +0 -248
- package/src/core/ontology/index.ts +0 -34
- package/src/core/ontology/registry.ts +0 -209
- package/src/core/ontology/types.ts +0 -124
- package/src/core/ontology/validator.ts +0 -382
- package/src/core/persist/backend.ts +0 -74
- package/src/core/persist/sqlite-backend.ts +0 -298
- package/src/core/plugins/index.ts +0 -17
- package/src/core/plugins/registry.ts +0 -322
- package/src/core/plugins/types.ts +0 -126
- package/src/core/query/datalog.ts +0 -188
- package/src/core/query/engine.ts +0 -370
- package/src/core/query/index.ts +0 -34
- package/src/core/query/parser.ts +0 -481
- package/src/core/query/types.ts +0 -200
- package/src/core/store/eav-store.ts +0 -467
- package/src/decisions/auto-capture.ts +0 -136
- package/src/decisions/hooks.ts +0 -163
- package/src/decisions/index.ts +0 -261
- package/src/decisions/types.ts +0 -103
- package/src/embeddings/auto-embed.ts +0 -248
- package/src/embeddings/chunker.ts +0 -327
- package/src/embeddings/index.ts +0 -48
- package/src/embeddings/model.ts +0 -112
- package/src/embeddings/search.ts +0 -305
- package/src/embeddings/store.ts +0 -313
- package/src/embeddings/types.ts +0 -92
- package/src/engine.ts +0 -1125
- package/src/garden/cluster.ts +0 -330
- package/src/garden/garden.ts +0 -306
- package/src/garden/index.ts +0 -29
- package/src/git/git-exporter.ts +0 -286
- package/src/git/git-importer.ts +0 -329
- package/src/git/git-reader.ts +0 -189
- package/src/git/index.ts +0 -22
- package/src/identity/governance.ts +0 -211
- package/src/identity/identity.ts +0 -224
- package/src/identity/index.ts +0 -30
- package/src/identity/signing-middleware.ts +0 -97
- package/src/index.ts +0 -29
- package/src/links/index.ts +0 -49
- package/src/links/lifecycle.ts +0 -400
- package/src/links/parser.ts +0 -484
- package/src/links/ref-index.ts +0 -186
- package/src/links/resolver.ts +0 -314
- package/src/links/types.ts +0 -108
- package/src/mcp/index.ts +0 -22
- package/src/mcp/server.ts +0 -1278
- package/src/semantic/csharp-parser.ts +0 -493
- package/src/semantic/go-parser.ts +0 -585
- package/src/semantic/index.ts +0 -34
- package/src/semantic/java-parser.ts +0 -456
- package/src/semantic/python-parser.ts +0 -659
- package/src/semantic/ruby-parser.ts +0 -446
- package/src/semantic/rust-parser.ts +0 -784
- package/src/semantic/semantic-merge.ts +0 -210
- package/src/semantic/ts-parser.ts +0 -681
- package/src/semantic/types.ts +0 -175
- package/src/sync/http-transport.ts +0 -144
- package/src/sync/index.ts +0 -43
- package/src/sync/memory-transport.ts +0 -66
- package/src/sync/multi-repo.ts +0 -200
- package/src/sync/reconciler.ts +0 -237
- package/src/sync/sync-engine.ts +0 -258
- package/src/sync/types.ts +0 -104
- package/src/sync/ws-transport.ts +0 -145
- package/src/ui/client.html +0 -695
- package/src/ui/server.ts +0 -419
- package/src/vcs/blob-store.ts +0 -124
- package/src/vcs/branch.ts +0 -150
- package/src/vcs/checkpoint.ts +0 -64
- package/src/vcs/decompose.ts +0 -469
- package/src/vcs/diff.ts +0 -409
- package/src/vcs/engine-context.ts +0 -26
- package/src/vcs/index.ts +0 -23
- package/src/vcs/issue.ts +0 -800
- package/src/vcs/merge.ts +0 -425
- package/src/vcs/milestone.ts +0 -124
- package/src/vcs/ops.ts +0 -59
- package/src/vcs/types.ts +0 -213
- package/src/vcs/vcs-middleware.ts +0 -81
- package/src/watcher/fs-watcher.ts +0 -255
- package/src/watcher/index.ts +0 -9
- package/src/watcher/ingestion.ts +0 -116
package/src/vcs/merge.ts
DELETED
|
@@ -1,425 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Merge Engine
|
|
3
|
-
*
|
|
4
|
-
* Three-way file-level merge with text-based fallback (Tier 0 / P3).
|
|
5
|
-
* Merges a source branch into the current branch by:
|
|
6
|
-
* 1. Finding the common ancestor (fork point) in the op stream
|
|
7
|
-
* 2. Building file states at ancestor, ours, and theirs
|
|
8
|
-
* 3. Producing a merged file state or conflicts
|
|
9
|
-
*
|
|
10
|
-
* DESIGN.md §4.4 — Patch Commutativity and Conflict Detection
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { VcsOp } from './types.js';
|
|
14
|
-
import type { BlobStore } from './blob-store.js';
|
|
15
|
-
import { buildFileStateAtOp, type FileState } from './diff.js';
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Types
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
export interface MergeConflict {
|
|
22
|
-
path: string;
|
|
23
|
-
kind: 'modify-modify' | 'modify-delete' | 'add-add';
|
|
24
|
-
/** Content from the current (ours) branch. */
|
|
25
|
-
ours?: string;
|
|
26
|
-
/** Content from the source (theirs) branch. */
|
|
27
|
-
theirs?: string;
|
|
28
|
-
/** Content from the common ancestor. */
|
|
29
|
-
base?: string;
|
|
30
|
-
/** For text conflicts: attempted merge with conflict markers. */
|
|
31
|
-
mergedWithMarkers?: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface MergeResult {
|
|
35
|
-
/** True if merge completed without conflicts. */
|
|
36
|
-
clean: boolean;
|
|
37
|
-
/** Merged file states to apply (path → content string). */
|
|
38
|
-
mergedFiles: Map<string, string | null>; // null = delete
|
|
39
|
-
/** Conflicts requiring manual resolution. */
|
|
40
|
-
conflicts: MergeConflict[];
|
|
41
|
-
/** Summary stats. */
|
|
42
|
-
stats: {
|
|
43
|
-
added: number;
|
|
44
|
-
modified: number;
|
|
45
|
-
deleted: number;
|
|
46
|
-
conflicted: number;
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
// Three-way merge
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Perform a three-way merge given ancestor, ours, and theirs file states.
|
|
56
|
-
*/
|
|
57
|
-
export function threeWayMerge(
|
|
58
|
-
base: Map<string, FileState>,
|
|
59
|
-
ours: Map<string, FileState>,
|
|
60
|
-
theirs: Map<string, FileState>,
|
|
61
|
-
blobStore?: BlobStore | null,
|
|
62
|
-
): MergeResult {
|
|
63
|
-
const mergedFiles = new Map<string, string | null>();
|
|
64
|
-
const conflicts: MergeConflict[] = [];
|
|
65
|
-
|
|
66
|
-
// Collect all file paths across all three states
|
|
67
|
-
const allPaths = new Set<string>();
|
|
68
|
-
for (const [p, s] of base) if (!s.deleted) allPaths.add(p);
|
|
69
|
-
for (const [p, s] of ours) if (!s.deleted) allPaths.add(p);
|
|
70
|
-
for (const [p, s] of theirs) if (!s.deleted) allPaths.add(p);
|
|
71
|
-
|
|
72
|
-
// Also track deleted paths
|
|
73
|
-
for (const [p, s] of ours) if (s.deleted) allPaths.add(p);
|
|
74
|
-
for (const [p, s] of theirs) if (s.deleted) allPaths.add(p);
|
|
75
|
-
|
|
76
|
-
for (const path of allPaths) {
|
|
77
|
-
const b = base.get(path);
|
|
78
|
-
const o = ours.get(path);
|
|
79
|
-
const t = theirs.get(path);
|
|
80
|
-
|
|
81
|
-
const baseExists = b && !b.deleted;
|
|
82
|
-
const oursExists = o && !o.deleted;
|
|
83
|
-
const theirsExists = t && !t.deleted;
|
|
84
|
-
|
|
85
|
-
const baseHash = baseExists ? b.contentHash : undefined;
|
|
86
|
-
const oursHash = oursExists ? o.contentHash : undefined;
|
|
87
|
-
const theirsHash = theirsExists ? t.contentHash : undefined;
|
|
88
|
-
|
|
89
|
-
// Neither side changed
|
|
90
|
-
if (oursHash === theirsHash) {
|
|
91
|
-
// Both same — no-op (keep ours)
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Only ours changed (theirs same as base)
|
|
96
|
-
if (theirsHash === baseHash && oursHash !== baseHash) {
|
|
97
|
-
if (!oursExists) {
|
|
98
|
-
mergedFiles.set(path, null); // we deleted
|
|
99
|
-
}
|
|
100
|
-
// else keep ours (already in our state)
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Only theirs changed (ours same as base)
|
|
105
|
-
if (oursHash === baseHash && theirsHash !== baseHash) {
|
|
106
|
-
if (!theirsExists) {
|
|
107
|
-
mergedFiles.set(path, null); // they deleted
|
|
108
|
-
} else if (theirsHash && blobStore) {
|
|
109
|
-
const content = blobStore.get(theirsHash);
|
|
110
|
-
if (content) {
|
|
111
|
-
mergedFiles.set(path, content.toString('utf-8'));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Both sides changed — potential conflict
|
|
118
|
-
|
|
119
|
-
// Case: both added (not in base)
|
|
120
|
-
if (!baseExists && oursExists && theirsExists) {
|
|
121
|
-
if (oursHash === theirsHash) {
|
|
122
|
-
continue; // identical add — no conflict
|
|
123
|
-
}
|
|
124
|
-
const oursContent = oursHash && blobStore ? blobStore.get(oursHash)?.toString('utf-8') : undefined;
|
|
125
|
-
const theirsContent = theirsHash && blobStore ? blobStore.get(theirsHash)?.toString('utf-8') : undefined;
|
|
126
|
-
|
|
127
|
-
// Try text merge with empty base
|
|
128
|
-
if (oursContent !== undefined && theirsContent !== undefined) {
|
|
129
|
-
const textResult = threeWayTextMerge('', oursContent, theirsContent);
|
|
130
|
-
if (textResult.clean) {
|
|
131
|
-
mergedFiles.set(path, textResult.merged);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
conflicts.push({
|
|
135
|
-
path,
|
|
136
|
-
kind: 'add-add',
|
|
137
|
-
ours: oursContent,
|
|
138
|
-
theirs: theirsContent,
|
|
139
|
-
mergedWithMarkers: textResult.merged,
|
|
140
|
-
});
|
|
141
|
-
} else {
|
|
142
|
-
conflicts.push({ path, kind: 'add-add', ours: oursContent, theirs: theirsContent });
|
|
143
|
-
}
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Case: one side deleted, other modified
|
|
148
|
-
if (oursExists && !theirsExists) {
|
|
149
|
-
conflicts.push({
|
|
150
|
-
path,
|
|
151
|
-
kind: 'modify-delete',
|
|
152
|
-
ours: oursHash && blobStore ? blobStore.get(oursHash)?.toString('utf-8') : undefined,
|
|
153
|
-
});
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (!oursExists && theirsExists) {
|
|
157
|
-
conflicts.push({
|
|
158
|
-
path,
|
|
159
|
-
kind: 'modify-delete',
|
|
160
|
-
theirs: theirsHash && blobStore ? blobStore.get(theirsHash)?.toString('utf-8') : undefined,
|
|
161
|
-
});
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Case: both modified (both exist, different hashes)
|
|
166
|
-
if (oursExists && theirsExists && oursHash !== theirsHash) {
|
|
167
|
-
const baseContent = baseHash && blobStore ? blobStore.get(baseHash)?.toString('utf-8') : undefined;
|
|
168
|
-
const oursContent = oursHash && blobStore ? blobStore.get(oursHash)?.toString('utf-8') : undefined;
|
|
169
|
-
const theirsContent = theirsHash && blobStore ? blobStore.get(theirsHash)?.toString('utf-8') : undefined;
|
|
170
|
-
|
|
171
|
-
if (baseContent !== undefined && oursContent !== undefined && theirsContent !== undefined) {
|
|
172
|
-
const textResult = threeWayTextMerge(baseContent, oursContent, theirsContent);
|
|
173
|
-
if (textResult.clean) {
|
|
174
|
-
mergedFiles.set(path, textResult.merged);
|
|
175
|
-
} else {
|
|
176
|
-
conflicts.push({
|
|
177
|
-
path,
|
|
178
|
-
kind: 'modify-modify',
|
|
179
|
-
base: baseContent,
|
|
180
|
-
ours: oursContent,
|
|
181
|
-
theirs: theirsContent,
|
|
182
|
-
mergedWithMarkers: textResult.merged,
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
conflicts.push({
|
|
187
|
-
path,
|
|
188
|
-
kind: 'modify-modify',
|
|
189
|
-
base: baseContent,
|
|
190
|
-
ours: oursContent,
|
|
191
|
-
theirs: theirsContent,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const added = [...mergedFiles.values()].filter((v) => v !== null).length;
|
|
199
|
-
const deleted = [...mergedFiles.values()].filter((v) => v === null).length;
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
clean: conflicts.length === 0,
|
|
203
|
-
mergedFiles,
|
|
204
|
-
conflicts,
|
|
205
|
-
stats: {
|
|
206
|
-
added,
|
|
207
|
-
modified: added, // in a merge context, additions from theirs are "modified"
|
|
208
|
-
deleted,
|
|
209
|
-
conflicted: conflicts.length,
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
// Three-way text merge
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
export interface TextMergeResult {
|
|
219
|
-
clean: boolean;
|
|
220
|
-
merged: string;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Three-way line-level text merge.
|
|
225
|
-
* Uses a simple approach: diff base→ours and base→theirs, then interleave.
|
|
226
|
-
* Produces conflict markers when both sides change the same region.
|
|
227
|
-
*/
|
|
228
|
-
export function threeWayTextMerge(
|
|
229
|
-
baseText: string,
|
|
230
|
-
oursText: string,
|
|
231
|
-
theirsText: string,
|
|
232
|
-
): TextMergeResult {
|
|
233
|
-
const baseLines = baseText.split('\n');
|
|
234
|
-
const oursLines = oursText.split('\n');
|
|
235
|
-
const theirsLines = theirsText.split('\n');
|
|
236
|
-
|
|
237
|
-
// Build change maps: line index in base → what each side did
|
|
238
|
-
const oursChanges = computeLineChanges(baseLines, oursLines);
|
|
239
|
-
const theirsChanges = computeLineChanges(baseLines, theirsLines);
|
|
240
|
-
|
|
241
|
-
const result: string[] = [];
|
|
242
|
-
let clean = true;
|
|
243
|
-
|
|
244
|
-
let baseIdx = 0;
|
|
245
|
-
let oursIdx = 0;
|
|
246
|
-
let theirsIdx = 0;
|
|
247
|
-
|
|
248
|
-
while (baseIdx < baseLines.length || oursIdx < oursLines.length || theirsIdx < theirsLines.length) {
|
|
249
|
-
const oursChange = oursChanges.get(baseIdx);
|
|
250
|
-
const theirsChange = theirsChanges.get(baseIdx);
|
|
251
|
-
|
|
252
|
-
if (baseIdx >= baseLines.length) {
|
|
253
|
-
// Past base — append remaining from both sides
|
|
254
|
-
// Ours remaining
|
|
255
|
-
while (oursIdx < oursLines.length) {
|
|
256
|
-
result.push(oursLines[oursIdx++]);
|
|
257
|
-
}
|
|
258
|
-
// Theirs remaining
|
|
259
|
-
while (theirsIdx < theirsLines.length) {
|
|
260
|
-
result.push(theirsLines[theirsIdx++]);
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (!oursChange && !theirsChange) {
|
|
266
|
-
// Both kept the line unchanged
|
|
267
|
-
result.push(baseLines[baseIdx]);
|
|
268
|
-
baseIdx++;
|
|
269
|
-
oursIdx++;
|
|
270
|
-
theirsIdx++;
|
|
271
|
-
} else if (oursChange && !theirsChange) {
|
|
272
|
-
// Only ours changed
|
|
273
|
-
applyChange(oursChange, result);
|
|
274
|
-
baseIdx += oursChange.baseCount;
|
|
275
|
-
oursIdx += oursChange.newCount;
|
|
276
|
-
theirsIdx += oursChange.baseCount;
|
|
277
|
-
} else if (!oursChange && theirsChange) {
|
|
278
|
-
// Only theirs changed
|
|
279
|
-
applyChange(theirsChange, result);
|
|
280
|
-
baseIdx += theirsChange.baseCount;
|
|
281
|
-
oursIdx += theirsChange.baseCount;
|
|
282
|
-
theirsIdx += theirsChange.newCount;
|
|
283
|
-
} else if (oursChange && theirsChange) {
|
|
284
|
-
// Both changed — check if identical
|
|
285
|
-
if (
|
|
286
|
-
oursChange.baseCount === theirsChange.baseCount &&
|
|
287
|
-
oursChange.newLines.join('\n') === theirsChange.newLines.join('\n')
|
|
288
|
-
) {
|
|
289
|
-
// Identical change — apply once
|
|
290
|
-
applyChange(oursChange, result);
|
|
291
|
-
baseIdx += oursChange.baseCount;
|
|
292
|
-
oursIdx += oursChange.newCount;
|
|
293
|
-
theirsIdx += theirsChange.newCount;
|
|
294
|
-
} else {
|
|
295
|
-
// Conflict
|
|
296
|
-
clean = false;
|
|
297
|
-
result.push('<<<<<<< ours');
|
|
298
|
-
for (const line of oursChange.newLines) result.push(line);
|
|
299
|
-
result.push('=======');
|
|
300
|
-
for (const line of theirsChange.newLines) result.push(line);
|
|
301
|
-
result.push('>>>>>>> theirs');
|
|
302
|
-
baseIdx += Math.max(oursChange.baseCount, theirsChange.baseCount);
|
|
303
|
-
oursIdx += oursChange.newCount;
|
|
304
|
-
theirsIdx += theirsChange.newCount;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return { clean, merged: result.join('\n') };
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// ---------------------------------------------------------------------------
|
|
313
|
-
// Line change detection
|
|
314
|
-
// ---------------------------------------------------------------------------
|
|
315
|
-
|
|
316
|
-
interface LineChange {
|
|
317
|
-
baseStart: number;
|
|
318
|
-
baseCount: number;
|
|
319
|
-
newCount: number;
|
|
320
|
-
newLines: string[];
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function applyChange(change: LineChange, result: string[]): void {
|
|
324
|
-
for (const line of change.newLines) {
|
|
325
|
-
result.push(line);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Compute a map of base line index → change region.
|
|
331
|
-
* Uses LCS (longest common subsequence) to detect changed regions.
|
|
332
|
-
*/
|
|
333
|
-
function computeLineChanges(
|
|
334
|
-
baseLines: string[],
|
|
335
|
-
newLines: string[],
|
|
336
|
-
): Map<number, LineChange> {
|
|
337
|
-
const changes = new Map<number, LineChange>();
|
|
338
|
-
const matches = lcsMatch(baseLines, newLines);
|
|
339
|
-
|
|
340
|
-
let baseIdx = 0;
|
|
341
|
-
let newIdx = 0;
|
|
342
|
-
|
|
343
|
-
for (const match of matches) {
|
|
344
|
-
// Process gap before this match
|
|
345
|
-
if (baseIdx < match.baseIdx || newIdx < match.newIdx) {
|
|
346
|
-
const baseCount = match.baseIdx - baseIdx;
|
|
347
|
-
const newCount = match.newIdx - newIdx;
|
|
348
|
-
if (baseCount > 0 || newCount > 0) {
|
|
349
|
-
changes.set(baseIdx, {
|
|
350
|
-
baseStart: baseIdx,
|
|
351
|
-
baseCount,
|
|
352
|
-
newCount,
|
|
353
|
-
newLines: newLines.slice(newIdx, newIdx + newCount),
|
|
354
|
-
});
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
baseIdx = match.baseIdx + 1;
|
|
358
|
-
newIdx = match.newIdx + 1;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Handle trailing gap
|
|
362
|
-
if (baseIdx < baseLines.length || newIdx < newLines.length) {
|
|
363
|
-
const baseCount = baseLines.length - baseIdx;
|
|
364
|
-
const newCount = newLines.length - newIdx;
|
|
365
|
-
if (baseCount > 0 || newCount > 0) {
|
|
366
|
-
changes.set(baseIdx, {
|
|
367
|
-
baseStart: baseIdx,
|
|
368
|
-
baseCount,
|
|
369
|
-
newCount,
|
|
370
|
-
newLines: newLines.slice(newIdx),
|
|
371
|
-
});
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return changes;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
interface LCSMatch {
|
|
379
|
-
baseIdx: number;
|
|
380
|
-
newIdx: number;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Find the LCS (longest common subsequence) matches between two line arrays.
|
|
385
|
-
* Returns an ordered list of matched line index pairs.
|
|
386
|
-
*/
|
|
387
|
-
function lcsMatch(a: string[], b: string[]): LCSMatch[] {
|
|
388
|
-
const n = a.length;
|
|
389
|
-
const m = b.length;
|
|
390
|
-
|
|
391
|
-
if (n === 0 || m === 0) return [];
|
|
392
|
-
|
|
393
|
-
// DP table
|
|
394
|
-
const dp: number[][] = Array.from({ length: n + 1 }, () =>
|
|
395
|
-
new Array(m + 1).fill(0),
|
|
396
|
-
);
|
|
397
|
-
|
|
398
|
-
for (let i = 1; i <= n; i++) {
|
|
399
|
-
for (let j = 1; j <= m; j++) {
|
|
400
|
-
if (a[i - 1] === b[j - 1]) {
|
|
401
|
-
dp[i][j] = dp[i - 1][j - 1] + 1;
|
|
402
|
-
} else {
|
|
403
|
-
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Backtrack
|
|
409
|
-
const matches: LCSMatch[] = [];
|
|
410
|
-
let i = n;
|
|
411
|
-
let j = m;
|
|
412
|
-
while (i > 0 && j > 0) {
|
|
413
|
-
if (a[i - 1] === b[j - 1]) {
|
|
414
|
-
matches.unshift({ baseIdx: i - 1, newIdx: j - 1 });
|
|
415
|
-
i--;
|
|
416
|
-
j--;
|
|
417
|
-
} else if (dp[i - 1][j] > dp[i][j - 1]) {
|
|
418
|
-
i--;
|
|
419
|
-
} else {
|
|
420
|
-
j--;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
return matches;
|
|
425
|
-
}
|
package/src/vcs/milestone.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Milestone Module
|
|
3
|
-
*
|
|
4
|
-
* Extracted from engine.ts per DESIGN.md §8.1.
|
|
5
|
-
* Handles milestone creation, listing, and op-range computation.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { createVcsOp } from './ops.js';
|
|
9
|
-
import type { VcsOp } from './types.js';
|
|
10
|
-
import type { EngineContext } from './engine-context.js';
|
|
11
|
-
|
|
12
|
-
// ---------------------------------------------------------------------------
|
|
13
|
-
// Types
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
|
|
16
|
-
export interface MilestoneInfo {
|
|
17
|
-
id: string;
|
|
18
|
-
message?: string;
|
|
19
|
-
createdAt?: string;
|
|
20
|
-
createdBy?: string;
|
|
21
|
-
fromOpHash?: string;
|
|
22
|
-
toOpHash?: string;
|
|
23
|
-
affectedFiles: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
// Operations
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Create a milestone spanning a range of ops.
|
|
32
|
-
* If no fromOpHash is specified, spans from the last milestone (or start).
|
|
33
|
-
*/
|
|
34
|
-
export async function createMilestone(
|
|
35
|
-
ctx: EngineContext,
|
|
36
|
-
message: string,
|
|
37
|
-
opts?: {
|
|
38
|
-
fromOpHash?: string;
|
|
39
|
-
toOpHash?: string;
|
|
40
|
-
},
|
|
41
|
-
): Promise<VcsOp> {
|
|
42
|
-
const ops = ctx.readAllOps();
|
|
43
|
-
const toOpHash = opts?.toOpHash ?? ops[ops.length - 1]?.hash;
|
|
44
|
-
|
|
45
|
-
// Find the start: either specified, or the op after the last milestone
|
|
46
|
-
let fromOpHash = opts?.fromOpHash;
|
|
47
|
-
if (!fromOpHash) {
|
|
48
|
-
const milestones = ops.filter((o) => o.kind === 'vcs:milestoneCreate');
|
|
49
|
-
if (milestones.length > 0) {
|
|
50
|
-
const lastMilestone = milestones[milestones.length - 1];
|
|
51
|
-
fromOpHash = lastMilestone.vcs?.toOpHash ?? lastMilestone.hash;
|
|
52
|
-
} else {
|
|
53
|
-
fromOpHash = ops[0]?.hash;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Generate milestone ID
|
|
58
|
-
const idBase = `${message}:${Date.now()}`;
|
|
59
|
-
const msgUint8 = new TextEncoder().encode(idBase);
|
|
60
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
|
|
61
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
62
|
-
const hashHex = hashArray
|
|
63
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
64
|
-
.join('');
|
|
65
|
-
const milestoneId = `milestone:${hashHex.slice(0, 12)}`;
|
|
66
|
-
|
|
67
|
-
// Determine affected files in the range
|
|
68
|
-
const fromIdx = ops.findIndex((o) => o.hash === fromOpHash);
|
|
69
|
-
const toIdx = ops.findIndex((o) => o.hash === toOpHash);
|
|
70
|
-
const rangeOps =
|
|
71
|
-
fromIdx >= 0 && toIdx >= 0 ? ops.slice(fromIdx, toIdx + 1) : ops;
|
|
72
|
-
const affectedFiles = [
|
|
73
|
-
...new Set(
|
|
74
|
-
rangeOps.filter((o) => o.vcs?.filePath).map((o) => o.vcs!.filePath!),
|
|
75
|
-
),
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
const op = await createVcsOp('vcs:milestoneCreate', {
|
|
79
|
-
agentId: ctx.agentId,
|
|
80
|
-
previousHash: ctx.getLastOp()?.hash,
|
|
81
|
-
vcs: {
|
|
82
|
-
milestoneId,
|
|
83
|
-
message,
|
|
84
|
-
fromOpHash,
|
|
85
|
-
toOpHash,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
ctx.applyOp(op);
|
|
89
|
-
|
|
90
|
-
// Store affected files as multi-valued facts
|
|
91
|
-
for (const file of affectedFiles) {
|
|
92
|
-
ctx.store.addFacts([{ e: milestoneId, a: 'affectsFile', v: file }]);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return op;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* List all milestones from the EAV store.
|
|
100
|
-
*/
|
|
101
|
-
export function listMilestones(ctx: EngineContext): MilestoneInfo[] {
|
|
102
|
-
const milestoneFacts = ctx.store
|
|
103
|
-
.getFactsByAttribute('type')
|
|
104
|
-
.filter((f) => f.v === 'Milestone');
|
|
105
|
-
|
|
106
|
-
return milestoneFacts.map((f) => {
|
|
107
|
-
const facts = ctx.store.getFactsByEntity(f.e);
|
|
108
|
-
const get = (attr: string) =>
|
|
109
|
-
facts.find((ef) => ef.a === attr)?.v as string | undefined;
|
|
110
|
-
const affectedFiles = facts
|
|
111
|
-
.filter((ef) => ef.a === 'affectsFile')
|
|
112
|
-
.map((ef) => ef.v as string);
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
id: f.e,
|
|
116
|
-
message: get('message'),
|
|
117
|
-
createdAt: get('createdAt'),
|
|
118
|
-
createdBy: get('createdBy'),
|
|
119
|
-
fromOpHash: get('fromOpHash'),
|
|
120
|
-
toOpHash: get('toOpHash'),
|
|
121
|
-
affectedFiles,
|
|
122
|
-
};
|
|
123
|
-
});
|
|
124
|
-
}
|
package/src/vcs/ops.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VCS Operation Constructors
|
|
3
|
-
*
|
|
4
|
-
* Helpers to create content-addressed VcsOps with proper
|
|
5
|
-
* causality chaining and metadata.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { VcsOp, VcsOpKind, VcsPayload } from './types.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Creates a VcsOp with full metadata, hash, and causal chain link.
|
|
12
|
-
*/
|
|
13
|
-
export async function createVcsOp(
|
|
14
|
-
kind: VcsOpKind,
|
|
15
|
-
params: {
|
|
16
|
-
agentId: string;
|
|
17
|
-
previousHash?: string;
|
|
18
|
-
vcs: VcsPayload;
|
|
19
|
-
},
|
|
20
|
-
): Promise<VcsOp> {
|
|
21
|
-
const opBase = {
|
|
22
|
-
kind,
|
|
23
|
-
timestamp: new Date().toISOString(),
|
|
24
|
-
agentId: params.agentId,
|
|
25
|
-
previousHash: params.previousHash,
|
|
26
|
-
vcs: params.vcs,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
// Hash covers the full op including VCS payload for content-addressability.
|
|
30
|
-
const content = JSON.stringify(opBase);
|
|
31
|
-
const msgUint8 = new TextEncoder().encode(content);
|
|
32
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
|
|
33
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
34
|
-
const hashHex = hashArray
|
|
35
|
-
.map((b) => b.toString(16).padStart(2, '0'))
|
|
36
|
-
.join('');
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
...opBase,
|
|
40
|
-
hash: `trellis:op:${hashHex}`,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Checks whether a KernelOp is a VcsOp (has a vcs payload).
|
|
46
|
-
*/
|
|
47
|
-
export function isVcsOp(op: { kind: string; vcs?: unknown }): op is VcsOp {
|
|
48
|
-
return (
|
|
49
|
-
op.vcs !== undefined ||
|
|
50
|
-
(typeof op.kind === 'string' && op.kind.startsWith('vcs:'))
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Checks whether an op kind is a VCS kind.
|
|
56
|
-
*/
|
|
57
|
-
export function isVcsOpKind(kind: string): kind is VcsOpKind {
|
|
58
|
-
return kind.startsWith('vcs:');
|
|
59
|
-
}
|