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/garden/cluster.ts
DELETED
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Idea Garden — Cluster Detection
|
|
3
|
-
*
|
|
4
|
-
* DESIGN.md §7.2–7.4 — Identifies "idea clusters": contiguous sequences
|
|
5
|
-
* of ops that were never incorporated into a milestone and were later
|
|
6
|
-
* diverged from.
|
|
7
|
-
*
|
|
8
|
-
* Three detection heuristics:
|
|
9
|
-
* 1. Context-switch detection (file-set shift)
|
|
10
|
-
* 2. Branch abandonment (stale un-milestoned ops)
|
|
11
|
-
* 3. Revert detection (ops undone by later ops)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import type { VcsOp } from '../vcs/types.js';
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Types
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
export interface IdeaCluster {
|
|
21
|
-
id: string;
|
|
22
|
-
ops: VcsOp[];
|
|
23
|
-
firstOp: string;
|
|
24
|
-
lastOp: string;
|
|
25
|
-
affectedFiles: string[];
|
|
26
|
-
affectedSymbols: string[]; // Tier 2 — empty for now
|
|
27
|
-
estimatedIntent: string;
|
|
28
|
-
createdAt: string;
|
|
29
|
-
abandonedAt: string;
|
|
30
|
-
status: 'abandoned' | 'draft' | 'revived';
|
|
31
|
-
/** Which heuristic detected this cluster. */
|
|
32
|
-
detectedBy: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ClusterDetector {
|
|
36
|
-
name: string;
|
|
37
|
-
detect(ops: VcsOp[], milestonedOpHashes: Set<string>): IdeaCluster[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Helpers
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
|
-
/** VcsOp kinds that represent file-level work (not control ops). */
|
|
45
|
-
const FILE_OP_KINDS = new Set([
|
|
46
|
-
'vcs:fileAdd',
|
|
47
|
-
'vcs:fileModify',
|
|
48
|
-
'vcs:fileDelete',
|
|
49
|
-
'vcs:fileRename',
|
|
50
|
-
]);
|
|
51
|
-
|
|
52
|
-
function isFileOp(op: VcsOp): boolean {
|
|
53
|
-
return FILE_OP_KINDS.has(op.kind);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function extractFiles(ops: VcsOp[]): string[] {
|
|
57
|
-
const files = new Set<string>();
|
|
58
|
-
for (const op of ops) {
|
|
59
|
-
if (op.vcs?.filePath) files.add(op.vcs.filePath);
|
|
60
|
-
if (op.vcs?.oldFilePath) files.add(op.vcs.oldFilePath);
|
|
61
|
-
}
|
|
62
|
-
return [...files];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function generateClusterId(prefix: string, ops: VcsOp[]): string {
|
|
66
|
-
const hash = ops[0]?.hash?.slice(0, 8) ?? 'unknown';
|
|
67
|
-
return `cluster:${prefix}-${hash}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ---------------------------------------------------------------------------
|
|
71
|
-
// 1. Context-switch detector
|
|
72
|
-
// ---------------------------------------------------------------------------
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Detects clusters when the set of files being modified shifts abruptly.
|
|
76
|
-
* Groups consecutive un-milestoned file ops by "file affinity" — when
|
|
77
|
-
* the overlap between the current working set and the next op drops to zero,
|
|
78
|
-
* a new group starts. Groups that are followed by a different group become
|
|
79
|
-
* candidate clusters.
|
|
80
|
-
*/
|
|
81
|
-
export const contextSwitchDetector: ClusterDetector = {
|
|
82
|
-
name: 'context-switch',
|
|
83
|
-
|
|
84
|
-
detect(ops: VcsOp[], milestonedOpHashes: Set<string>): IdeaCluster[] {
|
|
85
|
-
// Filter to un-milestoned file ops
|
|
86
|
-
const fileOps = ops.filter(
|
|
87
|
-
(o) => isFileOp(o) && !milestonedOpHashes.has(o.hash),
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
if (fileOps.length < 2) return [];
|
|
91
|
-
|
|
92
|
-
// Group by file affinity windows
|
|
93
|
-
const groups: VcsOp[][] = [];
|
|
94
|
-
let currentGroup: VcsOp[] = [];
|
|
95
|
-
let currentFiles = new Set<string>();
|
|
96
|
-
|
|
97
|
-
for (const op of fileOps) {
|
|
98
|
-
const opFile = op.vcs?.filePath;
|
|
99
|
-
if (!opFile) continue;
|
|
100
|
-
|
|
101
|
-
// Get directory prefix for affinity comparison
|
|
102
|
-
const opDir = opFile.split('/').slice(0, -1).join('/') || '.';
|
|
103
|
-
|
|
104
|
-
if (currentGroup.length === 0) {
|
|
105
|
-
currentGroup.push(op);
|
|
106
|
-
currentFiles.add(opDir);
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Check if this op's directory overlaps with the current working set
|
|
111
|
-
const currentDirs = [...currentFiles];
|
|
112
|
-
const hasOverlap = currentDirs.some(
|
|
113
|
-
(d) => opDir.startsWith(d) || d.startsWith(opDir) || d === opDir,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
if (hasOverlap) {
|
|
117
|
-
currentGroup.push(op);
|
|
118
|
-
currentFiles.add(opDir);
|
|
119
|
-
} else {
|
|
120
|
-
// Context switch — close current group and start new
|
|
121
|
-
if (currentGroup.length > 0) {
|
|
122
|
-
groups.push(currentGroup);
|
|
123
|
-
}
|
|
124
|
-
currentGroup = [op];
|
|
125
|
-
currentFiles = new Set([opDir]);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (currentGroup.length > 0) {
|
|
130
|
-
groups.push(currentGroup);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// All groups except the last (most recent) are candidates
|
|
134
|
-
const clusters: IdeaCluster[] = [];
|
|
135
|
-
for (let i = 0; i < groups.length - 1; i++) {
|
|
136
|
-
const group = groups[i];
|
|
137
|
-
if (group.length < 2) continue; // Skip trivially small groups
|
|
138
|
-
|
|
139
|
-
clusters.push({
|
|
140
|
-
id: generateClusterId('ctx', group),
|
|
141
|
-
ops: group,
|
|
142
|
-
firstOp: group[0].hash,
|
|
143
|
-
lastOp: group[group.length - 1].hash,
|
|
144
|
-
affectedFiles: extractFiles(group),
|
|
145
|
-
affectedSymbols: [],
|
|
146
|
-
estimatedIntent: '',
|
|
147
|
-
createdAt: group[0].timestamp,
|
|
148
|
-
abandonedAt: group[group.length - 1].timestamp,
|
|
149
|
-
status: 'abandoned',
|
|
150
|
-
detectedBy: 'context-switch',
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return clusters;
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
// 2. Revert detector
|
|
160
|
-
// ---------------------------------------------------------------------------
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Detects clusters where a file's content hash returns to a prior value,
|
|
164
|
-
* indicating the intermediate ops were "reverted."
|
|
165
|
-
*/
|
|
166
|
-
export const revertDetector: ClusterDetector = {
|
|
167
|
-
name: 'revert',
|
|
168
|
-
|
|
169
|
-
detect(ops: VcsOp[], milestonedOpHashes: Set<string>): IdeaCluster[] {
|
|
170
|
-
const clusters: IdeaCluster[] = [];
|
|
171
|
-
|
|
172
|
-
// Track content hash history per file
|
|
173
|
-
const hashHistory = new Map<string, { hash: string; opIdx: number }[]>();
|
|
174
|
-
|
|
175
|
-
for (let i = 0; i < ops.length; i++) {
|
|
176
|
-
const op = ops[i];
|
|
177
|
-
if (!isFileOp(op) || !op.vcs?.filePath || !op.vcs?.contentHash) continue;
|
|
178
|
-
|
|
179
|
-
const filePath = op.vcs.filePath;
|
|
180
|
-
if (!hashHistory.has(filePath)) {
|
|
181
|
-
hashHistory.set(filePath, []);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const history = hashHistory.get(filePath)!;
|
|
185
|
-
const currentHash = op.vcs.contentHash;
|
|
186
|
-
|
|
187
|
-
// Check if this hash appeared before (revert)
|
|
188
|
-
const priorIdx = history.findIndex((h) => h.hash === currentHash);
|
|
189
|
-
if (priorIdx >= 0 && priorIdx < history.length - 1) {
|
|
190
|
-
// Ops between priorIdx+1 and the current position were "reverted"
|
|
191
|
-
const revertedStartIdx = history[priorIdx + 1].opIdx;
|
|
192
|
-
const revertedEndIdx = history[history.length - 1].opIdx;
|
|
193
|
-
const revertedOps = ops
|
|
194
|
-
.slice(revertedStartIdx, revertedEndIdx + 1)
|
|
195
|
-
.filter(
|
|
196
|
-
(o) =>
|
|
197
|
-
isFileOp(o) &&
|
|
198
|
-
o.vcs?.filePath === filePath &&
|
|
199
|
-
!milestonedOpHashes.has(o.hash),
|
|
200
|
-
);
|
|
201
|
-
|
|
202
|
-
if (revertedOps.length >= 2) {
|
|
203
|
-
clusters.push({
|
|
204
|
-
id: generateClusterId('rev', revertedOps),
|
|
205
|
-
ops: revertedOps,
|
|
206
|
-
firstOp: revertedOps[0].hash,
|
|
207
|
-
lastOp: revertedOps[revertedOps.length - 1].hash,
|
|
208
|
-
affectedFiles: [filePath],
|
|
209
|
-
affectedSymbols: [],
|
|
210
|
-
estimatedIntent: '',
|
|
211
|
-
createdAt: revertedOps[0].timestamp,
|
|
212
|
-
abandonedAt: op.timestamp,
|
|
213
|
-
status: 'abandoned',
|
|
214
|
-
detectedBy: 'revert',
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
history.push({ hash: currentHash, opIdx: i });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return clusters;
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
// ---------------------------------------------------------------------------
|
|
227
|
-
// 3. Stale-branch detector
|
|
228
|
-
// ---------------------------------------------------------------------------
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Detects un-milestoned ops on branches that haven't seen activity recently.
|
|
232
|
-
* Since we operate on a linear op stream for now, this looks for gaps
|
|
233
|
-
* where file ops stop and then resume on different files.
|
|
234
|
-
*/
|
|
235
|
-
export const staleBranchDetector: ClusterDetector = {
|
|
236
|
-
name: 'stale-branch',
|
|
237
|
-
|
|
238
|
-
detect(ops: VcsOp[], milestonedOpHashes: Set<string>): IdeaCluster[] {
|
|
239
|
-
const clusters: IdeaCluster[] = [];
|
|
240
|
-
|
|
241
|
-
// Find branch create ops and their un-milestoned file ops
|
|
242
|
-
const branchOps = new Map<string, VcsOp[]>();
|
|
243
|
-
|
|
244
|
-
let currentBranch = 'main';
|
|
245
|
-
for (const op of ops) {
|
|
246
|
-
if (op.kind === 'vcs:branchCreate' && op.vcs?.branchName) {
|
|
247
|
-
currentBranch = op.vcs.branchName;
|
|
248
|
-
if (!branchOps.has(currentBranch)) {
|
|
249
|
-
branchOps.set(currentBranch, []);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (isFileOp(op) && !milestonedOpHashes.has(op.hash)) {
|
|
254
|
-
if (!branchOps.has(currentBranch)) {
|
|
255
|
-
branchOps.set(currentBranch, []);
|
|
256
|
-
}
|
|
257
|
-
branchOps.get(currentBranch)!.push(op);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// Check each branch for stale un-milestoned work
|
|
262
|
-
for (const [branchName, fileOps] of branchOps) {
|
|
263
|
-
if (branchName === 'main') continue; // Don't flag main
|
|
264
|
-
if (fileOps.length < 2) continue;
|
|
265
|
-
|
|
266
|
-
const lastOpTime = new Date(fileOps[fileOps.length - 1].timestamp).getTime();
|
|
267
|
-
const now = Date.now();
|
|
268
|
-
const daysSinceLastOp = (now - lastOpTime) / (1000 * 60 * 60 * 24);
|
|
269
|
-
|
|
270
|
-
// Flag if no activity for > 7 days (configurable later)
|
|
271
|
-
if (daysSinceLastOp > 7) {
|
|
272
|
-
clusters.push({
|
|
273
|
-
id: generateClusterId('stale', fileOps),
|
|
274
|
-
ops: fileOps,
|
|
275
|
-
firstOp: fileOps[0].hash,
|
|
276
|
-
lastOp: fileOps[fileOps.length - 1].hash,
|
|
277
|
-
affectedFiles: extractFiles(fileOps),
|
|
278
|
-
affectedSymbols: [],
|
|
279
|
-
estimatedIntent: '',
|
|
280
|
-
createdAt: fileOps[0].timestamp,
|
|
281
|
-
abandonedAt: fileOps[fileOps.length - 1].timestamp,
|
|
282
|
-
status: 'abandoned',
|
|
283
|
-
detectedBy: 'stale-branch',
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return clusters;
|
|
289
|
-
},
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
// ---------------------------------------------------------------------------
|
|
293
|
-
// Composite detector
|
|
294
|
-
// ---------------------------------------------------------------------------
|
|
295
|
-
|
|
296
|
-
/** All built-in detectors. */
|
|
297
|
-
export const defaultDetectors: ClusterDetector[] = [
|
|
298
|
-
contextSwitchDetector,
|
|
299
|
-
revertDetector,
|
|
300
|
-
staleBranchDetector,
|
|
301
|
-
];
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Run all detectors and merge results (dedup by cluster ID).
|
|
305
|
-
*/
|
|
306
|
-
export function detectClusters(
|
|
307
|
-
ops: VcsOp[],
|
|
308
|
-
milestonedOpHashes: Set<string>,
|
|
309
|
-
detectors: ClusterDetector[] = defaultDetectors,
|
|
310
|
-
): IdeaCluster[] {
|
|
311
|
-
const seen = new Set<string>();
|
|
312
|
-
const results: IdeaCluster[] = [];
|
|
313
|
-
|
|
314
|
-
for (const detector of detectors) {
|
|
315
|
-
const clusters = detector.detect(ops, milestonedOpHashes);
|
|
316
|
-
for (const cluster of clusters) {
|
|
317
|
-
if (!seen.has(cluster.id)) {
|
|
318
|
-
seen.add(cluster.id);
|
|
319
|
-
results.push(cluster);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Sort by creation time (oldest first)
|
|
325
|
-
results.sort(
|
|
326
|
-
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(),
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
return results;
|
|
330
|
-
}
|
package/src/garden/garden.ts
DELETED
|
@@ -1,306 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Idea Garden — Query Layer + Revive
|
|
3
|
-
*
|
|
4
|
-
* DESIGN.md §7.4–7.5 — Search and revive idea clusters.
|
|
5
|
-
*
|
|
6
|
-
* The Garden is a query layer over the causal stream that surfaces
|
|
7
|
-
* abandoned work as searchable idea clusters. It also provides the
|
|
8
|
-
* ability to revive clusters into new branches.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { VcsOp } from '../vcs/types.js';
|
|
12
|
-
import {
|
|
13
|
-
detectClusters,
|
|
14
|
-
defaultDetectors,
|
|
15
|
-
type IdeaCluster,
|
|
16
|
-
type ClusterDetector,
|
|
17
|
-
} from './cluster.js';
|
|
18
|
-
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
// Types
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
|
|
23
|
-
export interface ScoredCluster {
|
|
24
|
-
cluster: IdeaCluster;
|
|
25
|
-
score: number;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface GardenSearchOpts {
|
|
29
|
-
/** Filter by affected file path (substring match). */
|
|
30
|
-
file?: string;
|
|
31
|
-
/** Filter by keyword in affected files or estimated intent. */
|
|
32
|
-
keyword?: string;
|
|
33
|
-
/** Filter by cluster status. */
|
|
34
|
-
status?: IdeaCluster['status'];
|
|
35
|
-
/** Maximum results to return. */
|
|
36
|
-
limit?: number;
|
|
37
|
-
/** Use vector similarity when an EmbeddingManager is available (default: true). */
|
|
38
|
-
semantic?: boolean;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Optional embedding manager interface for vector-enhanced search.
|
|
43
|
-
* Avoids hard dependency on the embeddings module.
|
|
44
|
-
*/
|
|
45
|
-
export interface GardenEmbedder {
|
|
46
|
-
search(
|
|
47
|
-
query: string,
|
|
48
|
-
opts?: { limit?: number; filePrefix?: string },
|
|
49
|
-
): Promise<
|
|
50
|
-
Array<{ chunk: { filePath?: string; content: string }; score: number }>
|
|
51
|
-
>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface GardenContext {
|
|
55
|
-
/** All ops in the stream. */
|
|
56
|
-
readAllOps(): VcsOp[];
|
|
57
|
-
/** Set of op hashes that belong to a milestone range. */
|
|
58
|
-
getMilestonedOpHashes(): Set<string>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// Garden
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
export class IdeaGarden {
|
|
66
|
-
private ctx: GardenContext;
|
|
67
|
-
private detectors: ClusterDetector[];
|
|
68
|
-
private _cache: IdeaCluster[] | null = null;
|
|
69
|
-
private _revivedIds = new Set<string>();
|
|
70
|
-
private _embedder: GardenEmbedder | null = null;
|
|
71
|
-
|
|
72
|
-
constructor(ctx: GardenContext, detectors?: ClusterDetector[]) {
|
|
73
|
-
this.ctx = ctx;
|
|
74
|
-
this.detectors = detectors ?? defaultDetectors;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Attach an embedding manager for vector-enhanced search.
|
|
79
|
-
*/
|
|
80
|
-
setEmbedder(embedder: GardenEmbedder | null): void {
|
|
81
|
-
this._embedder = embedder;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Invalidate the cluster cache (call after new ops are added).
|
|
86
|
-
*/
|
|
87
|
-
invalidate(): void {
|
|
88
|
-
this._cache = null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Detect and return all idea clusters.
|
|
93
|
-
*/
|
|
94
|
-
listClusters(): IdeaCluster[] {
|
|
95
|
-
if (!this._cache) {
|
|
96
|
-
const ops = this.ctx.readAllOps();
|
|
97
|
-
const milestoned = this.ctx.getMilestonedOpHashes();
|
|
98
|
-
this._cache = detectClusters(ops, milestoned, this.detectors);
|
|
99
|
-
|
|
100
|
-
// Apply revived status
|
|
101
|
-
for (const cluster of this._cache) {
|
|
102
|
-
if (this._revivedIds.has(cluster.id)) {
|
|
103
|
-
cluster.status = 'revived';
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return this._cache;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Get a single cluster by ID.
|
|
112
|
-
*/
|
|
113
|
-
getCluster(clusterId: string): IdeaCluster | null {
|
|
114
|
-
return this.listClusters().find((c) => c.id === clusterId) ?? null;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Search clusters with filters (synchronous keyword search).
|
|
119
|
-
*/
|
|
120
|
-
search(opts: GardenSearchOpts = {}): IdeaCluster[] {
|
|
121
|
-
let clusters = this.listClusters();
|
|
122
|
-
|
|
123
|
-
if (opts.status) {
|
|
124
|
-
clusters = clusters.filter((c) => c.status === opts.status);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (opts.file) {
|
|
128
|
-
const fileTerm = opts.file.toLowerCase();
|
|
129
|
-
clusters = clusters.filter((c) =>
|
|
130
|
-
c.affectedFiles.some((f) => f.toLowerCase().includes(fileTerm)),
|
|
131
|
-
);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
if (opts.keyword) {
|
|
135
|
-
const kw = opts.keyword.toLowerCase();
|
|
136
|
-
clusters = clusters.filter((c) => {
|
|
137
|
-
// Search in file paths
|
|
138
|
-
if (c.affectedFiles.some((f) => f.toLowerCase().includes(kw)))
|
|
139
|
-
return true;
|
|
140
|
-
// Search in estimated intent
|
|
141
|
-
if (c.estimatedIntent.toLowerCase().includes(kw)) return true;
|
|
142
|
-
// Search in op kinds
|
|
143
|
-
if (c.ops.some((o) => o.kind.toLowerCase().includes(kw))) return true;
|
|
144
|
-
return false;
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (opts.limit && opts.limit > 0) {
|
|
149
|
-
clusters = clusters.slice(0, opts.limit);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return clusters;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Vector-enhanced search: uses embeddings to find clusters whose affected
|
|
157
|
-
* files are semantically similar to the query. Falls back to keyword search
|
|
158
|
-
* if no embedder is attached.
|
|
159
|
-
*/
|
|
160
|
-
async semanticSearch(opts: GardenSearchOpts = {}): Promise<ScoredCluster[]> {
|
|
161
|
-
// Start with keyword-filtered clusters
|
|
162
|
-
const keywordResults = this.search({ ...opts, limit: undefined });
|
|
163
|
-
|
|
164
|
-
// If no embedder or semantic explicitly disabled, wrap as scored
|
|
165
|
-
if (!this._embedder || opts.semantic === false) {
|
|
166
|
-
const scored = keywordResults.map((c) => ({ cluster: c, score: 1.0 }));
|
|
167
|
-
if (opts.limit && opts.limit > 0) return scored.slice(0, opts.limit);
|
|
168
|
-
return scored;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Use embeddings to score clusters by file similarity
|
|
172
|
-
const query = opts.keyword ?? opts.file ?? '';
|
|
173
|
-
if (!query) {
|
|
174
|
-
const scored = keywordResults.map((c) => ({ cluster: c, score: 1.0 }));
|
|
175
|
-
if (opts.limit && opts.limit > 0) return scored.slice(0, opts.limit);
|
|
176
|
-
return scored;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const allClusters = this.listClusters();
|
|
180
|
-
const embeddingResults = await this._embedder.search(query, { limit: 50 });
|
|
181
|
-
|
|
182
|
-
// Build file → score map from embedding results
|
|
183
|
-
const fileScores = new Map<string, number>();
|
|
184
|
-
for (const r of embeddingResults) {
|
|
185
|
-
if (r.chunk.filePath) {
|
|
186
|
-
const existing = fileScores.get(r.chunk.filePath) ?? 0;
|
|
187
|
-
fileScores.set(r.chunk.filePath, Math.max(existing, r.score));
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Score each cluster by max file similarity
|
|
192
|
-
const scored: ScoredCluster[] = [];
|
|
193
|
-
const keywordIds = new Set(keywordResults.map((c) => c.id));
|
|
194
|
-
|
|
195
|
-
for (const cluster of allClusters) {
|
|
196
|
-
// Apply status and file filters
|
|
197
|
-
if (opts.status && cluster.status !== opts.status) continue;
|
|
198
|
-
if (opts.file) {
|
|
199
|
-
const fileTerm = opts.file.toLowerCase();
|
|
200
|
-
if (
|
|
201
|
-
!cluster.affectedFiles.some((f) => f.toLowerCase().includes(fileTerm))
|
|
202
|
-
)
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
let maxScore = 0;
|
|
207
|
-
for (const file of cluster.affectedFiles) {
|
|
208
|
-
const s = fileScores.get(file) ?? 0;
|
|
209
|
-
if (s > maxScore) maxScore = s;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Boost keyword matches
|
|
213
|
-
if (keywordIds.has(cluster.id)) {
|
|
214
|
-
maxScore = Math.max(maxScore, 0.5);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (maxScore > 0) {
|
|
218
|
-
scored.push({ cluster, score: maxScore });
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Sort by score descending
|
|
223
|
-
scored.sort((a, b) => b.score - a.score);
|
|
224
|
-
|
|
225
|
-
if (opts.limit && opts.limit > 0) return scored.slice(0, opts.limit);
|
|
226
|
-
return scored;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Mark a cluster as revived. Returns the ops to replay.
|
|
231
|
-
*/
|
|
232
|
-
revive(clusterId: string): VcsOp[] | null {
|
|
233
|
-
const cluster = this.getCluster(clusterId);
|
|
234
|
-
if (!cluster) return null;
|
|
235
|
-
|
|
236
|
-
cluster.status = 'revived';
|
|
237
|
-
this._revivedIds.add(clusterId);
|
|
238
|
-
this.invalidate();
|
|
239
|
-
|
|
240
|
-
return cluster.ops;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Get summary statistics for the garden.
|
|
245
|
-
*/
|
|
246
|
-
stats(): {
|
|
247
|
-
total: number;
|
|
248
|
-
abandoned: number;
|
|
249
|
-
draft: number;
|
|
250
|
-
revived: number;
|
|
251
|
-
totalOps: number;
|
|
252
|
-
totalFiles: number;
|
|
253
|
-
} {
|
|
254
|
-
const clusters = this.listClusters();
|
|
255
|
-
const allFiles = new Set<string>();
|
|
256
|
-
let totalOps = 0;
|
|
257
|
-
|
|
258
|
-
for (const c of clusters) {
|
|
259
|
-
totalOps += c.ops.length;
|
|
260
|
-
for (const f of c.affectedFiles) allFiles.add(f);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return {
|
|
264
|
-
total: clusters.length,
|
|
265
|
-
abandoned: clusters.filter((c) => c.status === 'abandoned').length,
|
|
266
|
-
draft: clusters.filter((c) => c.status === 'draft').length,
|
|
267
|
-
revived: clusters.filter((c) => c.status === 'revived').length,
|
|
268
|
-
totalOps,
|
|
269
|
-
totalFiles: allFiles.size,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// ---------------------------------------------------------------------------
|
|
275
|
-
// Helper: Build milestoned op hash set from ops
|
|
276
|
-
// ---------------------------------------------------------------------------
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Build a set of op hashes that fall within milestone ranges.
|
|
280
|
-
* Used by the engine to provide GardenContext.getMilestonedOpHashes().
|
|
281
|
-
*/
|
|
282
|
-
export function buildMilestonedOpHashes(ops: VcsOp[]): Set<string> {
|
|
283
|
-
const milestoned = new Set<string>();
|
|
284
|
-
const milestoneOps = ops.filter((o) => o.kind === 'vcs:milestoneCreate');
|
|
285
|
-
|
|
286
|
-
for (const mOp of milestoneOps) {
|
|
287
|
-
const from = mOp.vcs?.fromOpHash;
|
|
288
|
-
const to = mOp.vcs?.toOpHash;
|
|
289
|
-
|
|
290
|
-
if (!from || !to) continue;
|
|
291
|
-
|
|
292
|
-
const fromIdx = ops.findIndex((o) => o.hash === from);
|
|
293
|
-
const toIdx = ops.findIndex((o) => o.hash === to);
|
|
294
|
-
|
|
295
|
-
if (fromIdx >= 0 && toIdx >= 0) {
|
|
296
|
-
for (let i = fromIdx; i <= toIdx; i++) {
|
|
297
|
-
milestoned.add(ops[i].hash);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Also mark the milestone op itself
|
|
302
|
-
milestoned.add(mOp.hash);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
return milestoned;
|
|
306
|
-
}
|
package/src/garden/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Idea Garden — Public Surface
|
|
3
|
-
*
|
|
4
|
-
* @module garden
|
|
5
|
-
*
|
|
6
|
-
* Re-exports cluster detection heuristics (context-switch, revert, stale-branch)
|
|
7
|
-
* and the {@link IdeaGarden} query/revive layer.
|
|
8
|
-
*
|
|
9
|
-
* @see DESIGN.md §7 for the full Idea Garden specification.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
export {
|
|
13
|
-
detectClusters,
|
|
14
|
-
defaultDetectors,
|
|
15
|
-
contextSwitchDetector,
|
|
16
|
-
revertDetector,
|
|
17
|
-
staleBranchDetector,
|
|
18
|
-
} from './cluster.js';
|
|
19
|
-
|
|
20
|
-
export type { IdeaCluster, ClusterDetector } from './cluster.js';
|
|
21
|
-
|
|
22
|
-
export { IdeaGarden, buildMilestonedOpHashes } from './garden.js';
|
|
23
|
-
|
|
24
|
-
export type {
|
|
25
|
-
ScoredCluster,
|
|
26
|
-
GardenSearchOpts,
|
|
27
|
-
GardenContext,
|
|
28
|
-
GardenEmbedder,
|
|
29
|
-
} from './garden.js';
|