@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.
- package/.gitignore +115 -0
- package/.trace/progress.json +22 -0
- package/README.md +466 -0
- package/RELEASE-NOTES-1.5.0.md +410 -0
- package/STATUS.md +245 -0
- package/dist/auto-commit.d.ts +66 -0
- package/dist/auto-commit.d.ts.map +1 -0
- package/dist/auto-commit.js +180 -0
- package/dist/auto-commit.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +246 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +46 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +256 -0
- package/dist/commands.js.map +1 -0
- package/dist/diff.d.ts +23 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +106 -0
- package/dist/diff.js.map +1 -0
- package/dist/github.d.ts.map +1 -0
- package/dist/github.js.map +1 -0
- package/dist/index-cache.d.ts +35 -0
- package/dist/index-cache.d.ts.map +1 -0
- package/dist/index-cache.js +114 -0
- package/dist/index-cache.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/storage.d.ts +45 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +151 -0
- package/dist/storage.js.map +1 -0
- package/dist/sync.d.ts +60 -0
- package/dist/sync.js +184 -0
- package/dist/tags.d.ts +85 -0
- package/dist/tags.d.ts.map +1 -0
- package/dist/tags.js +219 -0
- package/dist/tags.js.map +1 -0
- package/dist/types.d.ts +102 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/docs/.nojekyll +0 -0
- package/docs/README.md +73 -0
- package/docs/_config.yml +2 -0
- package/docs/index.html +960 -0
- package/docs-website/package.json +20 -0
- package/jest.config.js +21 -0
- package/package.json +50 -0
- package/scripts/init.ts +290 -0
- package/src/agent-audit.ts +270 -0
- package/src/agent-checkout.ts +227 -0
- package/src/agent-coordination.ts +318 -0
- package/src/async-queue.ts +203 -0
- package/src/auto-branching.ts +279 -0
- package/src/auto-commit.ts +166 -0
- package/src/cherry-pick.ts +252 -0
- package/src/chunked-upload.ts +224 -0
- package/src/cli-v2.ts +335 -0
- package/src/cli.ts +318 -0
- package/src/cliff-detection.ts +232 -0
- package/src/commands.ts +267 -0
- package/src/commit-hash-system.ts +351 -0
- package/src/compression.ts +176 -0
- package/src/conflict-resolution-ui.ts +277 -0
- package/src/conflict-visualization.ts +238 -0
- package/src/diff-formatter.ts +184 -0
- package/src/diff.ts +124 -0
- package/src/distributed-coordination.ts +273 -0
- package/src/git-interop.ts +316 -0
- package/src/index-cache.ts +88 -0
- package/src/index.ts +38 -0
- package/src/merge-engine.ts +143 -0
- package/src/message-search.ts +370 -0
- package/src/performance-monitoring.ts +236 -0
- package/src/rebase.ts +327 -0
- package/src/rollback.ts +215 -0
- package/src/semantic-grouping.ts +245 -0
- package/src/stage-area.ts +324 -0
- package/src/stash.ts +278 -0
- package/src/storage.ts +131 -0
- package/src/sync.ts +205 -0
- package/src/tags.ts +244 -0
- package/src/types.ts +119 -0
- package/src/webhooks.ts +119 -0
- package/src/workspace-isolation.ts +298 -0
- package/tests/auto-commit.test.ts +308 -0
- package/tests/checkout.test.ts +136 -0
- package/tests/commit.test.ts +118 -0
- package/tests/diff.test.ts +191 -0
- package/tests/github.test.ts +94 -0
- package/tests/integration.test.ts +267 -0
- package/tests/log.test.ts +125 -0
- package/tests/phase2-integration.test.ts +370 -0
- package/tests/storage.test.ts +167 -0
- package/tests/tags.test.ts +477 -0
- package/tests/types.test.ts +75 -0
- package/tests/v1.1/agent-audit.test.ts +472 -0
- package/tests/v1.1/agent-coordination.test.ts +308 -0
- package/tests/v1.1/async-queue.test.ts +253 -0
- package/tests/v1.1/comprehensive.test.ts +521 -0
- package/tests/v1.1/diff-formatter.test.ts +238 -0
- package/tests/v1.1/integration.test.ts +389 -0
- package/tests/v1.1/onboarding.test.ts +365 -0
- package/tests/v1.1/rollback.test.ts +370 -0
- package/tests/v1.1/semantic-grouping.test.ts +230 -0
- package/tests/v1.2/chunked-upload.test.ts +301 -0
- package/tests/v1.2/cliff-detection.test.ts +272 -0
- package/tests/v1.2/commit-hash-system.test.ts +288 -0
- package/tests/v1.2/compression.test.ts +220 -0
- package/tests/v1.2/conflict-visualization.test.ts +263 -0
- package/tests/v1.2/distributed.test.ts +261 -0
- package/tests/v1.2/performance-monitoring.test.ts +328 -0
- package/tests/v1.3/auto-branching.test.ts +270 -0
- package/tests/v1.3/message-search.test.ts +264 -0
- package/tests/v1.3/stage-area.test.ts +330 -0
- package/tests/v1.3/stash-rebase-cherry-pick.test.ts +361 -0
- package/tests/v1.4/cli.test.ts +171 -0
- package/tests/v1.4/conflict-resolution-advanced.test.ts +429 -0
- package/tests/v1.4/conflict-resolution-ui.test.ts +286 -0
- package/tests/v1.4/workspace-isolation-advanced.test.ts +382 -0
- package/tests/v1.4/workspace-isolation.test.ts +268 -0
- package/tests/v1.5/agent-coordination.real.test.ts +401 -0
- package/tests/v1.5/cli-v2.test.ts +354 -0
- package/tests/v1.5/git-interop.real.test.ts +358 -0
- package/tests/v1.5/integration-testing.real.test.ts +440 -0
- 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;
|