@zoebuildsai/trace 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/.gitignore +115 -0
  2. package/.trace/progress.json +22 -0
  3. package/README.md +466 -0
  4. package/RELEASE-NOTES-1.5.0.md +410 -0
  5. package/STATUS.md +245 -0
  6. package/dist/auto-commit.d.ts +66 -0
  7. package/dist/auto-commit.d.ts.map +1 -0
  8. package/dist/auto-commit.js +180 -0
  9. package/dist/auto-commit.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +246 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands.d.ts +46 -0
  15. package/dist/commands.d.ts.map +1 -0
  16. package/dist/commands.js +256 -0
  17. package/dist/commands.js.map +1 -0
  18. package/dist/diff.d.ts +23 -0
  19. package/dist/diff.d.ts.map +1 -0
  20. package/dist/diff.js +106 -0
  21. package/dist/diff.js.map +1 -0
  22. package/dist/github.d.ts.map +1 -0
  23. package/dist/github.js.map +1 -0
  24. package/dist/index-cache.d.ts +35 -0
  25. package/dist/index-cache.d.ts.map +1 -0
  26. package/dist/index-cache.js +114 -0
  27. package/dist/index-cache.js.map +1 -0
  28. package/dist/index.d.ts +15 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +25 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/storage.d.ts +45 -0
  33. package/dist/storage.d.ts.map +1 -0
  34. package/dist/storage.js +151 -0
  35. package/dist/storage.js.map +1 -0
  36. package/dist/sync.d.ts +60 -0
  37. package/dist/sync.js +184 -0
  38. package/dist/tags.d.ts +85 -0
  39. package/dist/tags.d.ts.map +1 -0
  40. package/dist/tags.js +219 -0
  41. package/dist/tags.js.map +1 -0
  42. package/dist/types.d.ts +102 -0
  43. package/dist/types.d.ts.map +1 -0
  44. package/dist/types.js +6 -0
  45. package/dist/types.js.map +1 -0
  46. package/docs/.nojekyll +0 -0
  47. package/docs/README.md +73 -0
  48. package/docs/_config.yml +2 -0
  49. package/docs/index.html +960 -0
  50. package/docs-website/package.json +20 -0
  51. package/jest.config.js +21 -0
  52. package/package.json +50 -0
  53. package/scripts/init.ts +290 -0
  54. package/src/agent-audit.ts +270 -0
  55. package/src/agent-checkout.ts +227 -0
  56. package/src/agent-coordination.ts +318 -0
  57. package/src/async-queue.ts +203 -0
  58. package/src/auto-branching.ts +279 -0
  59. package/src/auto-commit.ts +166 -0
  60. package/src/cherry-pick.ts +252 -0
  61. package/src/chunked-upload.ts +224 -0
  62. package/src/cli-v2.ts +335 -0
  63. package/src/cli.ts +318 -0
  64. package/src/cliff-detection.ts +232 -0
  65. package/src/commands.ts +267 -0
  66. package/src/commit-hash-system.ts +351 -0
  67. package/src/compression.ts +176 -0
  68. package/src/conflict-resolution-ui.ts +277 -0
  69. package/src/conflict-visualization.ts +238 -0
  70. package/src/diff-formatter.ts +184 -0
  71. package/src/diff.ts +124 -0
  72. package/src/distributed-coordination.ts +273 -0
  73. package/src/git-interop.ts +316 -0
  74. package/src/index-cache.ts +88 -0
  75. package/src/index.ts +38 -0
  76. package/src/merge-engine.ts +143 -0
  77. package/src/message-search.ts +370 -0
  78. package/src/performance-monitoring.ts +236 -0
  79. package/src/rebase.ts +327 -0
  80. package/src/rollback.ts +215 -0
  81. package/src/semantic-grouping.ts +245 -0
  82. package/src/stage-area.ts +324 -0
  83. package/src/stash.ts +278 -0
  84. package/src/storage.ts +131 -0
  85. package/src/sync.ts +205 -0
  86. package/src/tags.ts +244 -0
  87. package/src/types.ts +119 -0
  88. package/src/webhooks.ts +119 -0
  89. package/src/workspace-isolation.ts +298 -0
  90. package/tests/auto-commit.test.ts +308 -0
  91. package/tests/checkout.test.ts +136 -0
  92. package/tests/commit.test.ts +118 -0
  93. package/tests/diff.test.ts +191 -0
  94. package/tests/github.test.ts +94 -0
  95. package/tests/integration.test.ts +267 -0
  96. package/tests/log.test.ts +125 -0
  97. package/tests/phase2-integration.test.ts +370 -0
  98. package/tests/storage.test.ts +167 -0
  99. package/tests/tags.test.ts +477 -0
  100. package/tests/types.test.ts +75 -0
  101. package/tests/v1.1/agent-audit.test.ts +472 -0
  102. package/tests/v1.1/agent-coordination.test.ts +308 -0
  103. package/tests/v1.1/async-queue.test.ts +253 -0
  104. package/tests/v1.1/comprehensive.test.ts +521 -0
  105. package/tests/v1.1/diff-formatter.test.ts +238 -0
  106. package/tests/v1.1/integration.test.ts +389 -0
  107. package/tests/v1.1/onboarding.test.ts +365 -0
  108. package/tests/v1.1/rollback.test.ts +370 -0
  109. package/tests/v1.1/semantic-grouping.test.ts +230 -0
  110. package/tests/v1.2/chunked-upload.test.ts +301 -0
  111. package/tests/v1.2/cliff-detection.test.ts +272 -0
  112. package/tests/v1.2/commit-hash-system.test.ts +288 -0
  113. package/tests/v1.2/compression.test.ts +220 -0
  114. package/tests/v1.2/conflict-visualization.test.ts +263 -0
  115. package/tests/v1.2/distributed.test.ts +261 -0
  116. package/tests/v1.2/performance-monitoring.test.ts +328 -0
  117. package/tests/v1.3/auto-branching.test.ts +270 -0
  118. package/tests/v1.3/message-search.test.ts +264 -0
  119. package/tests/v1.3/stage-area.test.ts +330 -0
  120. package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
  121. package/tests/v1.4/cli.test.ts +171 -0
  122. package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
  123. package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
  124. package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
  125. package/tests/v1.4/workspace-isolation.test.ts +268 -0
  126. package/tests/v1.5/agent-coordination.real.test.ts +401 -0
  127. package/tests/v1.5/cli-v2.test.ts +354 -0
  128. package/tests/v1.5/git-interop.real.test.ts +358 -0
  129. package/tests/v1.5/integration-testing.real.test.ts +440 -0
  130. package/tsconfig.json +26 -0
@@ -0,0 +1,88 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { IndexCache as IndexCacheType, CachedFile } from './types';
4
+ import { Storage } from './storage';
5
+
6
+ export class IndexCache {
7
+ private cache: Map<string, IndexCacheType> = new Map();
8
+ private storage: Storage;
9
+ private cacheDir: string;
10
+ private ttl: number;
11
+
12
+ constructor(storage: Storage, cacheDir: string = path.join(process.env.HOME || '', '.openclaw/memory-git/.cache'), ttl: number = 5000) {
13
+ this.storage = storage;
14
+ this.cacheDir = cacheDir;
15
+ this.ttl = ttl;
16
+ this.ensureDir();
17
+ }
18
+
19
+ private ensureDir(): void {
20
+ if (!fs.existsSync(this.cacheDir)) {
21
+ fs.mkdirSync(this.cacheDir, { recursive: true });
22
+ }
23
+ }
24
+
25
+ /**
26
+ * Load or create cache entry
27
+ */
28
+ getOrCreate(commitHash: string): IndexCacheType {
29
+ if (!this.cache.has(commitHash)) {
30
+ this.cache.set(commitHash, {
31
+ timestamp: Date.now(),
32
+ files: new Map(),
33
+ currentCommit: commitHash,
34
+ });
35
+ }
36
+ return this.cache.get(commitHash)!;
37
+ }
38
+
39
+ /**
40
+ * Check if cache is still valid
41
+ */
42
+ isValid(commitHash: string): boolean {
43
+ const cached = this.cache.get(commitHash);
44
+ if (!cached) return false;
45
+ return Date.now() - cached.timestamp < this.ttl;
46
+ }
47
+
48
+ /**
49
+ * Mark file as cached
50
+ */
51
+ markFile(commitHash: string, filePath: string, hash: string, modified: boolean = false): void {
52
+ const cacheEntry = this.getOrCreate(commitHash);
53
+ cacheEntry.files.set(filePath, {
54
+ path: filePath,
55
+ hash,
56
+ lastSeen: Date.now(),
57
+ modified,
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Get cached file info
63
+ */
64
+ getFile(commitHash: string, filePath: string): CachedFile | null {
65
+ const cacheEntry = this.cache.get(commitHash);
66
+ if (!cacheEntry) return null;
67
+ return cacheEntry.files.get(filePath) || null;
68
+ }
69
+
70
+ /**
71
+ * Invalidate cache for commit
72
+ */
73
+ invalidate(commitHash: string): void {
74
+ this.cache.delete(commitHash);
75
+ }
76
+
77
+ /**
78
+ * Clear all expired entries
79
+ */
80
+ prune(): void {
81
+ const now = Date.now();
82
+ for (const [hash, entry] of this.cache.entries()) {
83
+ if (now - entry.timestamp > this.ttl) {
84
+ this.cache.delete(hash);
85
+ }
86
+ }
87
+ }
88
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Trace - Agent-native version control with remote sync
3
+ */
4
+
5
+ export { Storage } from './storage';
6
+ export { Differ } from './diff';
7
+ export { IndexCache } from './index-cache';
8
+ export { TraceCommands } from './commands';
9
+ export { AutoCommitter } from './auto-commit';
10
+ export { TagManager } from './tags';
11
+ export { TraceSync } from './sync';
12
+
13
+ export type {
14
+ CommitObject,
15
+ TreeObject,
16
+ FileEntry,
17
+ DiffResult,
18
+ DiffChange,
19
+ DiffStats,
20
+ LineDiff,
21
+ StatusResult,
22
+ LogEntry,
23
+ CommitOptions,
24
+ CheckoutOptions,
25
+ DiffOptions,
26
+ IndexCache as IndexCacheType,
27
+ CachedFile,
28
+ AutoCommitMetadata,
29
+ TagInfo,
30
+ BranchInfo,
31
+ } from './types';
32
+
33
+ // Default export
34
+ import { TraceCommands } from './commands';
35
+
36
+ const defaultInstance = new TraceCommands();
37
+
38
+ export default defaultInstance;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Merge Engine for Trace
3
+ * Intelligent 3-way merge for multi-agent conflict resolution
4
+ */
5
+
6
+ export interface FileMerge {
7
+ path: string;
8
+ baseVersion: string;
9
+ theirVersion: string;
10
+ ourVersion: string;
11
+ mergedContent?: string;
12
+ conflicts?: ConflictHunk[];
13
+ strategy: 'auto' | 'theirs' | 'ours' | 'manual';
14
+ }
15
+
16
+ export interface ConflictHunk {
17
+ startLine: number;
18
+ endLine: number;
19
+ ours: string[];
20
+ theirs: string[];
21
+ }
22
+
23
+ export class MergeEngine {
24
+ /**
25
+ * 3-way merge: base, ours, theirs
26
+ */
27
+ static merge(base: string, ours: string, theirs: string, strategy: 'auto' | 'theirs' | 'ours' = 'auto'): FileMerge {
28
+ const baseLine = base.split('\n');
29
+ const ourLines = ours.split('\n');
30
+ const theirLines = theirs.split('\n');
31
+
32
+ const conflicts = this.findConflicts(baseLine, ourLines, theirLines);
33
+
34
+ let mergedContent = ours;
35
+ let resolvedStrategy = strategy;
36
+
37
+ if (conflicts.length === 0) {
38
+ // No conflicts, merge is clean
39
+ mergedContent = this.applyDiff(baseLine, theirLines, ourLines);
40
+ resolvedStrategy = 'auto';
41
+ } else {
42
+ switch (strategy) {
43
+ case 'auto':
44
+ resolvedStrategy = 'manual'; // Can't auto-resolve with conflicts
45
+ break;
46
+ case 'theirs':
47
+ mergedContent = theirs;
48
+ break;
49
+ case 'ours':
50
+ mergedContent = ours;
51
+ break;
52
+ case 'manual':
53
+ // Return conflicts for manual resolution
54
+ break;
55
+ }
56
+ }
57
+
58
+ return {
59
+ path: '',
60
+ baseVersion: '',
61
+ theirVersion: '',
62
+ ourVersion: '',
63
+ mergedContent,
64
+ conflicts: conflicts.length > 0 ? conflicts : undefined,
65
+ strategy: resolvedStrategy,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Find conflicting hunks (lines that differ between versions)
71
+ */
72
+ private static findConflicts(
73
+ base: string[],
74
+ ours: string[],
75
+ theirs: string[]
76
+ ): ConflictHunk[] {
77
+ const conflicts: ConflictHunk[] = [];
78
+
79
+ // Simple conflict detection: find regions where ours differs from base AND theirs differs from base
80
+ for (let i = 0; i < Math.max(base.length, ours.length, theirs.length); i++) {
81
+ const baseLine = base[i] || '';
82
+ const ourLine = ours[i] || '';
83
+ const theirLine = theirs[i] || '';
84
+
85
+ // Conflict if both sides changed the same line differently
86
+ if (baseLine !== ourLine && baseLine !== theirLine && ourLine !== theirLine) {
87
+ conflicts.push({
88
+ startLine: i,
89
+ endLine: i + 1,
90
+ ours: [ourLine],
91
+ theirs: [theirLine],
92
+ });
93
+ }
94
+ }
95
+
96
+ return conflicts;
97
+ }
98
+
99
+ /**
100
+ * Apply diff to base (simplified)
101
+ */
102
+ private static applyDiff(base: string[], target: string[], reference: string[]): string {
103
+ // If one side didn't change from base, take the other side's changes
104
+ let result = [...base];
105
+
106
+ // If reference (ours) is same as base, take target (theirs)
107
+ if (base.length === reference.length && base.every((l, i) => l === reference[i])) {
108
+ result = target;
109
+ }
110
+
111
+ return result.join('\n');
112
+ }
113
+
114
+ /**
115
+ * Resolve conflict: choose winner based on strategy
116
+ */
117
+ static resolveHunk(hunk: ConflictHunk, strategy: 'theirs' | 'ours'): string[] {
118
+ return strategy === 'theirs' ? hunk.theirs : hunk.ours;
119
+ }
120
+
121
+ /**
122
+ * Check if merge is clean (no conflicts)
123
+ */
124
+ static isClean(merge: FileMerge): boolean {
125
+ return !merge.conflicts || merge.conflicts.length === 0;
126
+ }
127
+
128
+ /**
129
+ * Get merge quality score (0-100)
130
+ */
131
+ static getQualityScore(merge: FileMerge): number {
132
+ if (this.isClean(merge)) return 100;
133
+
134
+ const conflictCount = merge.conflicts?.length || 0;
135
+ const totalLines = merge.ourVersion.split('\n').length;
136
+
137
+ // Score based on conflict density
138
+ const conflictDensity = conflictCount / totalLines;
139
+ return Math.max(0, 100 - conflictDensity * 500);
140
+ }
141
+ }
142
+
143
+ export default MergeEngine;
@@ -0,0 +1,370 @@
1
+ /**
2
+ * Message Search for Trace
3
+ * Full-text search across commit history
4
+ */
5
+
6
+ export interface SearchResult {
7
+ commitHash: string;
8
+ author: string;
9
+ timestamp: number;
10
+ message: string;
11
+ matchedFields: string[];
12
+ relevanceScore: number;
13
+ }
14
+
15
+ export interface SearchQuery {
16
+ text?: string;
17
+ author?: string;
18
+ dateAfter?: number;
19
+ dateBefore?: number;
20
+ type?: 'feature' | 'fix' | 'docs' | 'test' | 'refactor' | 'chore' | 'security';
21
+ files?: string[];
22
+ limit?: number;
23
+ }
24
+
25
+ export class MessageSearch {
26
+ private commits: Map<
27
+ string,
28
+ {
29
+ hash: string;
30
+ message: string;
31
+ author: string;
32
+ timestamp: number;
33
+ files: string[];
34
+ type: string;
35
+ }
36
+ > = new Map();
37
+
38
+ private index: Map<string, Set<string>> = new Map(); // word -> commit hashes
39
+ private authorIndex: Map<string, Set<string>> = new Map(); // author -> commit hashes
40
+ private typeIndex: Map<string, Set<string>> = new Map(); // type -> commit hashes
41
+ private fileIndex: Map<string, Set<string>> = new Map(); // file -> commit hashes
42
+
43
+ /**
44
+ * Index commit for search
45
+ */
46
+ indexCommit(
47
+ hash: string,
48
+ message: string,
49
+ author: string,
50
+ timestamp: number,
51
+ files: string[] = [],
52
+ type: string = 'chore'
53
+ ): void {
54
+ this.commits.set(hash, {
55
+ hash,
56
+ message,
57
+ author,
58
+ timestamp,
59
+ files,
60
+ type,
61
+ });
62
+
63
+ // Index words from message
64
+ const words = this.tokenize(message);
65
+ for (const word of words) {
66
+ if (!this.index.has(word)) {
67
+ this.index.set(word, new Set());
68
+ }
69
+ this.index.get(word)!.add(hash);
70
+ }
71
+
72
+ // Index author
73
+ if (!this.authorIndex.has(author)) {
74
+ this.authorIndex.set(author, new Set());
75
+ }
76
+ this.authorIndex.get(author)!.add(hash);
77
+
78
+ // Index type
79
+ if (!this.typeIndex.has(type)) {
80
+ this.typeIndex.set(type, new Set());
81
+ }
82
+ this.typeIndex.get(type)!.add(hash);
83
+
84
+ // Index files
85
+ for (const file of files) {
86
+ if (!this.fileIndex.has(file)) {
87
+ this.fileIndex.set(file, new Set());
88
+ }
89
+ this.fileIndex.get(file)!.add(hash);
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Search commits
95
+ */
96
+ search(query: SearchQuery): SearchResult[] {
97
+ let results = new Set<string>();
98
+ let firstFilter = true;
99
+
100
+ // Text search
101
+ if (query.text) {
102
+ const words = this.tokenize(query.text);
103
+ for (const word of words) {
104
+ const matches = this.index.get(word) || new Set();
105
+ if (firstFilter) {
106
+ results = new Set(matches);
107
+ firstFilter = false;
108
+ } else {
109
+ // Intersect with results
110
+ results = new Set([...results].filter(r => matches.has(r)));
111
+ }
112
+ }
113
+ }
114
+
115
+ // Author filter
116
+ if (query.author) {
117
+ const authorMatches = this.authorIndex.get(query.author) || new Set();
118
+ if (firstFilter) {
119
+ results = new Set(authorMatches);
120
+ firstFilter = false;
121
+ } else {
122
+ results = new Set([...results].filter(r => authorMatches.has(r)));
123
+ }
124
+ }
125
+
126
+ // Type filter
127
+ if (query.type) {
128
+ const typeMatches = this.typeIndex.get(query.type) || new Set();
129
+ if (firstFilter) {
130
+ results = new Set(typeMatches);
131
+ firstFilter = false;
132
+ } else {
133
+ results = new Set([...results].filter(r => typeMatches.has(r)));
134
+ }
135
+ }
136
+
137
+ // File filter
138
+ if (query.files && query.files.length > 0) {
139
+ const fileMatches = new Set<string>();
140
+ for (const file of query.files) {
141
+ const matches = this.fileIndex.get(file) || new Set();
142
+ matches.forEach(m => fileMatches.add(m));
143
+ }
144
+
145
+ if (firstFilter) {
146
+ results = fileMatches;
147
+ firstFilter = false;
148
+ } else {
149
+ results = new Set([...results].filter(r => fileMatches.has(r)));
150
+ }
151
+ }
152
+
153
+ // Date range filter
154
+ const filtered = Array.from(results)
155
+ .map(hash => this.commits.get(hash)!)
156
+ .filter(commit => {
157
+ if (query.dateAfter && commit.timestamp < query.dateAfter) return false;
158
+ if (query.dateBefore && commit.timestamp > query.dateBefore) return false;
159
+ return true;
160
+ });
161
+
162
+ // Score and sort
163
+ const scored = filtered.map(commit => ({
164
+ commitHash: commit.hash,
165
+ author: commit.author,
166
+ timestamp: commit.timestamp,
167
+ message: commit.message,
168
+ matchedFields: this.getMatchedFields(commit, query),
169
+ relevanceScore: this.calculateScore(commit, query),
170
+ }));
171
+
172
+ scored.sort((a, b) => b.relevanceScore - a.relevanceScore);
173
+
174
+ const limit = query.limit || 20;
175
+ return scored.slice(0, limit);
176
+ }
177
+
178
+ /**
179
+ * Tokenize message for indexing
180
+ */
181
+ private tokenize(text: string): string[] {
182
+ return text
183
+ .toLowerCase()
184
+ .replace(/[^\w\s#-]/g, '')
185
+ .split(/\s+/)
186
+ .filter(word => word.length > 2 && !this.isStopword(word));
187
+ }
188
+
189
+ /**
190
+ * Check if word is stopword
191
+ */
192
+ private isStopword(word: string): boolean {
193
+ const stopwords = new Set([
194
+ 'the', 'a', 'an', 'and', 'or', 'but', 'is', 'was', 'are',
195
+ 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did',
196
+ 'will', 'would', 'could', 'should', 'may', 'might', 'can',
197
+ 'for', 'with', 'to', 'of', 'in', 'on', 'at', 'by', 'from',
198
+ ]);
199
+ return stopwords.has(word);
200
+ }
201
+
202
+ /**
203
+ * Get matched fields for a commit
204
+ */
205
+ private getMatchedFields(commit: any, query: SearchQuery): string[] {
206
+ const matched: string[] = [];
207
+
208
+ if (query.text) {
209
+ const words = this.tokenize(query.text);
210
+ if (words.some(w => commit.message.toLowerCase().includes(w))) {
211
+ matched.push('message');
212
+ }
213
+ }
214
+
215
+ if (query.author && commit.author === query.author) {
216
+ matched.push('author');
217
+ }
218
+
219
+ if (query.type && commit.type === query.type) {
220
+ matched.push('type');
221
+ }
222
+
223
+ return matched;
224
+ }
225
+
226
+ /**
227
+ * Calculate relevance score
228
+ */
229
+ private calculateScore(commit: any, query: SearchQuery): number {
230
+ let score = 0;
231
+
232
+ // Text relevance (count matching words)
233
+ if (query.text) {
234
+ const words = this.tokenize(query.text);
235
+ const messageWords = this.tokenize(commit.message);
236
+ const matches = words.filter(w => messageWords.includes(w)).length;
237
+ score += matches * 10;
238
+ }
239
+
240
+ // Exact type match
241
+ if (query.type && commit.type === query.type) {
242
+ score += 5;
243
+ }
244
+
245
+ // Exact author match
246
+ if (query.author && commit.author === query.author) {
247
+ score += 3;
248
+ }
249
+
250
+ // Recency (more recent = higher score)
251
+ const ageInDays = (Date.now() - commit.timestamp) / (1000 * 60 * 60 * 24);
252
+ score += Math.max(0, 2 - ageInDays / 30); // Decay over month
253
+
254
+ return score;
255
+ }
256
+
257
+ /**
258
+ * Search by regex pattern
259
+ */
260
+ searchByPattern(pattern: RegExp, limit: number = 20): SearchResult[] {
261
+ const matches: SearchResult[] = [];
262
+
263
+ for (const [hash, commit] of this.commits) {
264
+ if (pattern.test(commit.message)) {
265
+ matches.push({
266
+ commitHash: hash,
267
+ author: commit.author,
268
+ timestamp: commit.timestamp,
269
+ message: commit.message,
270
+ matchedFields: ['message'],
271
+ relevanceScore: 1.0,
272
+ });
273
+ }
274
+
275
+ if (matches.length >= limit) break;
276
+ }
277
+
278
+ return matches;
279
+ }
280
+
281
+ /**
282
+ * Get commits by author
283
+ */
284
+ getByAuthor(author: string, limit: number = 20): SearchResult[] {
285
+ const hashes = this.authorIndex.get(author) || new Set();
286
+ return Array.from(hashes)
287
+ .slice(0, limit)
288
+ .map(hash => {
289
+ const commit = this.commits.get(hash)!;
290
+ return {
291
+ commitHash: hash,
292
+ author: commit.author,
293
+ timestamp: commit.timestamp,
294
+ message: commit.message,
295
+ matchedFields: ['author'],
296
+ relevanceScore: 1.0,
297
+ };
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Get commits by type
303
+ */
304
+ getByType(type: string, limit: number = 20): SearchResult[] {
305
+ const hashes = this.typeIndex.get(type) || new Set();
306
+ return Array.from(hashes)
307
+ .slice(0, limit)
308
+ .map(hash => {
309
+ const commit = this.commits.get(hash)!;
310
+ return {
311
+ commitHash: hash,
312
+ author: commit.author,
313
+ timestamp: commit.timestamp,
314
+ message: commit.message,
315
+ matchedFields: ['type'],
316
+ relevanceScore: 1.0,
317
+ };
318
+ });
319
+ }
320
+
321
+ /**
322
+ * Get commits that touched file
323
+ */
324
+ getByFile(file: string, limit: number = 20): SearchResult[] {
325
+ const hashes = this.fileIndex.get(file) || new Set();
326
+ return Array.from(hashes)
327
+ .slice(0, limit)
328
+ .map(hash => {
329
+ const commit = this.commits.get(hash)!;
330
+ return {
331
+ commitHash: hash,
332
+ author: commit.author,
333
+ timestamp: commit.timestamp,
334
+ message: commit.message,
335
+ matchedFields: ['file'],
336
+ relevanceScore: 1.0,
337
+ };
338
+ });
339
+ }
340
+
341
+ /**
342
+ * Get search stats
343
+ */
344
+ getStats(): {
345
+ totalIndexedCommits: number;
346
+ uniqueAuthors: number;
347
+ uniqueFiles: number;
348
+ uniqueWords: number;
349
+ } {
350
+ return {
351
+ totalIndexedCommits: this.commits.size,
352
+ uniqueAuthors: this.authorIndex.size,
353
+ uniqueFiles: this.fileIndex.size,
354
+ uniqueWords: this.index.size,
355
+ };
356
+ }
357
+
358
+ /**
359
+ * Clear index (for testing)
360
+ */
361
+ clearIndex(): void {
362
+ this.commits.clear();
363
+ this.index.clear();
364
+ this.authorIndex.clear();
365
+ this.typeIndex.clear();
366
+ this.fileIndex.clear();
367
+ }
368
+ }
369
+
370
+ export default MessageSearch;