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.
Files changed (96) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/embeddings/index.js +1 -1
  3. package/dist/{index-7gvjxt27.js → index-2917tjd8.js} +1 -1
  4. package/package.json +2 -10
  5. package/dist/transformers.node-bx3q9d7k.js +0 -33130
  6. package/src/cli/index.ts +0 -3356
  7. package/src/core/agents/harness.ts +0 -380
  8. package/src/core/agents/index.ts +0 -18
  9. package/src/core/agents/types.ts +0 -90
  10. package/src/core/index.ts +0 -118
  11. package/src/core/kernel/middleware.ts +0 -44
  12. package/src/core/kernel/trellis-kernel.ts +0 -593
  13. package/src/core/ontology/builtins.ts +0 -248
  14. package/src/core/ontology/index.ts +0 -34
  15. package/src/core/ontology/registry.ts +0 -209
  16. package/src/core/ontology/types.ts +0 -124
  17. package/src/core/ontology/validator.ts +0 -382
  18. package/src/core/persist/backend.ts +0 -74
  19. package/src/core/persist/sqlite-backend.ts +0 -298
  20. package/src/core/plugins/index.ts +0 -17
  21. package/src/core/plugins/registry.ts +0 -322
  22. package/src/core/plugins/types.ts +0 -126
  23. package/src/core/query/datalog.ts +0 -188
  24. package/src/core/query/engine.ts +0 -370
  25. package/src/core/query/index.ts +0 -34
  26. package/src/core/query/parser.ts +0 -481
  27. package/src/core/query/types.ts +0 -200
  28. package/src/core/store/eav-store.ts +0 -467
  29. package/src/decisions/auto-capture.ts +0 -136
  30. package/src/decisions/hooks.ts +0 -163
  31. package/src/decisions/index.ts +0 -261
  32. package/src/decisions/types.ts +0 -103
  33. package/src/embeddings/auto-embed.ts +0 -248
  34. package/src/embeddings/chunker.ts +0 -327
  35. package/src/embeddings/index.ts +0 -48
  36. package/src/embeddings/model.ts +0 -112
  37. package/src/embeddings/search.ts +0 -305
  38. package/src/embeddings/store.ts +0 -313
  39. package/src/embeddings/types.ts +0 -92
  40. package/src/engine.ts +0 -1125
  41. package/src/garden/cluster.ts +0 -330
  42. package/src/garden/garden.ts +0 -306
  43. package/src/garden/index.ts +0 -29
  44. package/src/git/git-exporter.ts +0 -286
  45. package/src/git/git-importer.ts +0 -329
  46. package/src/git/git-reader.ts +0 -189
  47. package/src/git/index.ts +0 -22
  48. package/src/identity/governance.ts +0 -211
  49. package/src/identity/identity.ts +0 -224
  50. package/src/identity/index.ts +0 -30
  51. package/src/identity/signing-middleware.ts +0 -97
  52. package/src/index.ts +0 -29
  53. package/src/links/index.ts +0 -49
  54. package/src/links/lifecycle.ts +0 -400
  55. package/src/links/parser.ts +0 -484
  56. package/src/links/ref-index.ts +0 -186
  57. package/src/links/resolver.ts +0 -314
  58. package/src/links/types.ts +0 -108
  59. package/src/mcp/index.ts +0 -22
  60. package/src/mcp/server.ts +0 -1278
  61. package/src/semantic/csharp-parser.ts +0 -493
  62. package/src/semantic/go-parser.ts +0 -585
  63. package/src/semantic/index.ts +0 -34
  64. package/src/semantic/java-parser.ts +0 -456
  65. package/src/semantic/python-parser.ts +0 -659
  66. package/src/semantic/ruby-parser.ts +0 -446
  67. package/src/semantic/rust-parser.ts +0 -784
  68. package/src/semantic/semantic-merge.ts +0 -210
  69. package/src/semantic/ts-parser.ts +0 -681
  70. package/src/semantic/types.ts +0 -175
  71. package/src/sync/http-transport.ts +0 -144
  72. package/src/sync/index.ts +0 -43
  73. package/src/sync/memory-transport.ts +0 -66
  74. package/src/sync/multi-repo.ts +0 -200
  75. package/src/sync/reconciler.ts +0 -237
  76. package/src/sync/sync-engine.ts +0 -258
  77. package/src/sync/types.ts +0 -104
  78. package/src/sync/ws-transport.ts +0 -145
  79. package/src/ui/client.html +0 -695
  80. package/src/ui/server.ts +0 -419
  81. package/src/vcs/blob-store.ts +0 -124
  82. package/src/vcs/branch.ts +0 -150
  83. package/src/vcs/checkpoint.ts +0 -64
  84. package/src/vcs/decompose.ts +0 -469
  85. package/src/vcs/diff.ts +0 -409
  86. package/src/vcs/engine-context.ts +0 -26
  87. package/src/vcs/index.ts +0 -23
  88. package/src/vcs/issue.ts +0 -800
  89. package/src/vcs/merge.ts +0 -425
  90. package/src/vcs/milestone.ts +0 -124
  91. package/src/vcs/ops.ts +0 -59
  92. package/src/vcs/types.ts +0 -213
  93. package/src/vcs/vcs-middleware.ts +0 -81
  94. package/src/watcher/fs-watcher.ts +0 -255
  95. package/src/watcher/index.ts +0 -9
  96. package/src/watcher/ingestion.ts +0 -116
@@ -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
- }
@@ -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
- }
@@ -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';