@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,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Commit Hash System for Trace
|
|
3
|
+
* Git-like hashing for agent checkout capability
|
|
4
|
+
* Each commit is uniquely addressable by SHA-256 hash
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as crypto from 'crypto';
|
|
8
|
+
|
|
9
|
+
export interface CommitHash {
|
|
10
|
+
full: string; // Full SHA-256 hash
|
|
11
|
+
short: string; // First 7 characters (like git)
|
|
12
|
+
commitId: string; // Unique identifier
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface Commit {
|
|
16
|
+
hash: CommitHash;
|
|
17
|
+
parent?: string; // Parent commit hash
|
|
18
|
+
author: string; // Agent ID
|
|
19
|
+
message: string;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
files: Map<string, string>; // filename -> content hash
|
|
22
|
+
signature: string; // Agent signature
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CommitIndex {
|
|
26
|
+
hash: string;
|
|
27
|
+
parent?: string;
|
|
28
|
+
author: string;
|
|
29
|
+
message: string;
|
|
30
|
+
timestamp: number;
|
|
31
|
+
fileCount: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class CommitHashSystem {
|
|
35
|
+
private commits: Map<string, Commit> = new Map();
|
|
36
|
+
private hashIndex: Map<string, CommitIndex> = new Map();
|
|
37
|
+
private parentChild: Map<string, string[]> = new Map(); // parent -> [children]
|
|
38
|
+
private authCommits: Map<string, string[]> = new Map(); // author -> [commits]
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create commit hash from content
|
|
42
|
+
*/
|
|
43
|
+
static createHash(
|
|
44
|
+
author: string,
|
|
45
|
+
message: string,
|
|
46
|
+
files: Map<string, string>,
|
|
47
|
+
parent?: string
|
|
48
|
+
): CommitHash {
|
|
49
|
+
// Git-like hash: tree + parent + author + message + timestamp
|
|
50
|
+
const content = [
|
|
51
|
+
author,
|
|
52
|
+
message,
|
|
53
|
+
parent || 'root',
|
|
54
|
+
Array.from(files.entries())
|
|
55
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
56
|
+
.map(([f, h]) => `${f}:${h}`)
|
|
57
|
+
.join('|'),
|
|
58
|
+
Date.now().toString(),
|
|
59
|
+
].join('\n');
|
|
60
|
+
|
|
61
|
+
const full = crypto.createHash('sha256').update(content).digest('hex');
|
|
62
|
+
const short = full.substring(0, 7);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
full,
|
|
66
|
+
short,
|
|
67
|
+
commitId: short,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Record commit in system
|
|
73
|
+
*/
|
|
74
|
+
recordCommit(
|
|
75
|
+
author: string,
|
|
76
|
+
message: string,
|
|
77
|
+
files: Map<string, string>,
|
|
78
|
+
signature: string,
|
|
79
|
+
parent?: string
|
|
80
|
+
): CommitHash {
|
|
81
|
+
const hash = CommitHashSystem.createHash(author, message, files, parent);
|
|
82
|
+
|
|
83
|
+
const commit: Commit = {
|
|
84
|
+
hash,
|
|
85
|
+
parent,
|
|
86
|
+
author,
|
|
87
|
+
message,
|
|
88
|
+
timestamp: Date.now(),
|
|
89
|
+
files: new Map(files),
|
|
90
|
+
signature,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
this.commits.set(hash.full, commit);
|
|
94
|
+
|
|
95
|
+
// Index for fast lookup
|
|
96
|
+
this.hashIndex.set(hash.full, {
|
|
97
|
+
hash: hash.full,
|
|
98
|
+
parent,
|
|
99
|
+
author,
|
|
100
|
+
message,
|
|
101
|
+
timestamp: commit.timestamp,
|
|
102
|
+
fileCount: files.size,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Parent-child tracking
|
|
106
|
+
if (parent) {
|
|
107
|
+
if (!this.parentChild.has(parent)) {
|
|
108
|
+
this.parentChild.set(parent, []);
|
|
109
|
+
}
|
|
110
|
+
this.parentChild.get(parent)!.push(hash.full);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Author tracking
|
|
114
|
+
if (!this.authCommits.has(author)) {
|
|
115
|
+
this.authCommits.set(author, []);
|
|
116
|
+
}
|
|
117
|
+
this.authCommits.get(author)!.push(hash.full);
|
|
118
|
+
|
|
119
|
+
return hash;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get commit by hash (full or short)
|
|
124
|
+
*/
|
|
125
|
+
getCommit(hash: string): Commit | undefined {
|
|
126
|
+
// Try full hash first
|
|
127
|
+
if (this.commits.has(hash)) {
|
|
128
|
+
return this.commits.get(hash);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Try short hash
|
|
132
|
+
for (const [fullHash, commit] of this.commits) {
|
|
133
|
+
if (fullHash.startsWith(hash)) {
|
|
134
|
+
return commit;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get commit index (lightweight lookup)
|
|
143
|
+
*/
|
|
144
|
+
getCommitIndex(hash: string): CommitIndex | undefined {
|
|
145
|
+
if (this.hashIndex.has(hash)) {
|
|
146
|
+
return this.hashIndex.get(hash);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Try short hash
|
|
150
|
+
for (const [fullHash, index] of this.hashIndex) {
|
|
151
|
+
if (fullHash.startsWith(hash)) {
|
|
152
|
+
return index;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve short hash to full hash
|
|
161
|
+
*/
|
|
162
|
+
resolveHash(shortHash: string): string | undefined {
|
|
163
|
+
for (const fullHash of this.commits.keys()) {
|
|
164
|
+
if (fullHash.startsWith(shortHash)) {
|
|
165
|
+
return fullHash;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get commit chain (ancestry)
|
|
173
|
+
*/
|
|
174
|
+
getChain(hash: string): Commit[] {
|
|
175
|
+
const chain: Commit[] = [];
|
|
176
|
+
let current = this.getCommit(hash);
|
|
177
|
+
|
|
178
|
+
while (current) {
|
|
179
|
+
chain.push(current);
|
|
180
|
+
if (current.parent) {
|
|
181
|
+
current = this.getCommit(current.parent);
|
|
182
|
+
} else {
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return chain;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get children of commit (for branching)
|
|
192
|
+
*/
|
|
193
|
+
getChildren(hash: string): Commit[] {
|
|
194
|
+
const fullHash = this.resolveHash(hash) || hash;
|
|
195
|
+
const childHashes = this.parentChild.get(fullHash) || [];
|
|
196
|
+
return childHashes
|
|
197
|
+
.map(h => this.getCommit(h))
|
|
198
|
+
.filter((c): c is Commit => c !== undefined);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get all commits by author
|
|
203
|
+
*/
|
|
204
|
+
getCommitsByAuthor(author: string): Commit[] {
|
|
205
|
+
const hashes = this.authCommits.get(author) || [];
|
|
206
|
+
return hashes
|
|
207
|
+
.map(h => this.getCommit(h))
|
|
208
|
+
.filter((c): c is Commit => c !== undefined);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Verify commit integrity
|
|
213
|
+
*/
|
|
214
|
+
verifyCommit(hash: string, expectedSignature: string): boolean {
|
|
215
|
+
const commit = this.getCommit(hash);
|
|
216
|
+
if (!commit) return false;
|
|
217
|
+
|
|
218
|
+
// Verify hash matches content
|
|
219
|
+
const recomputed = CommitHashSystem.createHash(
|
|
220
|
+
commit.author,
|
|
221
|
+
commit.message,
|
|
222
|
+
commit.files,
|
|
223
|
+
commit.parent
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
return recomputed.full === (this.resolveHash(hash) || hash) && commit.signature === expectedSignature;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Check if commit is ancestor of another
|
|
231
|
+
*/
|
|
232
|
+
isAncestor(potentialAncestor: string, commit: string): boolean {
|
|
233
|
+
const chain = this.getChain(commit);
|
|
234
|
+
const ancestorFullHash = this.resolveHash(potentialAncestor) || potentialAncestor;
|
|
235
|
+
return chain.some(c => c.hash.full === ancestorFullHash);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get commit range (for diff, log, etc)
|
|
240
|
+
*/
|
|
241
|
+
getRange(fromHash: string, toHash: string): Commit[] {
|
|
242
|
+
const fromChain = this.getChain(fromHash);
|
|
243
|
+
const toChain = this.getChain(toHash);
|
|
244
|
+
|
|
245
|
+
// Find common ancestor
|
|
246
|
+
const toSet = new Set(toChain.map(c => c.hash.full));
|
|
247
|
+
let commonAncestor = null;
|
|
248
|
+
for (const commit of fromChain) {
|
|
249
|
+
if (toSet.has(commit.hash.full)) {
|
|
250
|
+
commonAncestor = commit.hash.full;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!commonAncestor) return [];
|
|
256
|
+
|
|
257
|
+
// Get commits from commonAncestor to toHash
|
|
258
|
+
const result: Commit[] = [];
|
|
259
|
+
let current = this.getCommit(toHash);
|
|
260
|
+
while (current && current.hash.full !== commonAncestor) {
|
|
261
|
+
result.push(current);
|
|
262
|
+
current = current.parent ? this.getCommit(current.parent) : null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return result.reverse();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get all commits (for log)
|
|
270
|
+
*/
|
|
271
|
+
getAllCommits(): CommitIndex[] {
|
|
272
|
+
return Array.from(this.hashIndex.values());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get statistics
|
|
277
|
+
*/
|
|
278
|
+
getStats(): {
|
|
279
|
+
totalCommits: number;
|
|
280
|
+
uniqueAuthors: number;
|
|
281
|
+
branchCount: number;
|
|
282
|
+
oldestCommit?: CommitIndex;
|
|
283
|
+
newestCommit?: CommitIndex;
|
|
284
|
+
} {
|
|
285
|
+
const commits = Array.from(this.hashIndex.values());
|
|
286
|
+
const authors = new Set(commits.map(c => c.author));
|
|
287
|
+
const rootCommits = commits.filter(c => !c.parent);
|
|
288
|
+
|
|
289
|
+
let oldest = commits[0];
|
|
290
|
+
let newest = commits[0];
|
|
291
|
+
|
|
292
|
+
for (const commit of commits) {
|
|
293
|
+
if (commit.timestamp < oldest.timestamp) oldest = commit;
|
|
294
|
+
if (commit.timestamp > newest.timestamp) newest = commit;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
totalCommits: commits.length,
|
|
299
|
+
uniqueAuthors: authors.size,
|
|
300
|
+
branchCount: rootCommits.length,
|
|
301
|
+
oldestCommit: oldest,
|
|
302
|
+
newestCommit: newest,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Validate entire history
|
|
308
|
+
*/
|
|
309
|
+
validateHistory(): { valid: boolean; errors: string[] } {
|
|
310
|
+
const errors: string[] = [];
|
|
311
|
+
|
|
312
|
+
for (const [hash, commit] of this.commits) {
|
|
313
|
+
// Verify parent exists
|
|
314
|
+
if (commit.parent) {
|
|
315
|
+
if (!this.commits.has(commit.parent)) {
|
|
316
|
+
errors.push(`Commit ${hash} has missing parent ${commit.parent}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Verify hash consistency
|
|
321
|
+
const recomputed = CommitHashSystem.createHash(
|
|
322
|
+
commit.author,
|
|
323
|
+
commit.message,
|
|
324
|
+
commit.files,
|
|
325
|
+
commit.parent
|
|
326
|
+
);
|
|
327
|
+
if (recomputed.full !== hash) {
|
|
328
|
+
errors.push(`Commit ${hash} has inconsistent hash`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Verify parent-child index
|
|
333
|
+
for (const [parent, children] of this.parentChild) {
|
|
334
|
+
if (!this.commits.has(parent)) {
|
|
335
|
+
errors.push(`Parent index references non-existent commit ${parent}`);
|
|
336
|
+
}
|
|
337
|
+
for (const child of children) {
|
|
338
|
+
if (!this.commits.has(child)) {
|
|
339
|
+
errors.push(`Child index references non-existent commit ${child}`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
valid: errors.length === 0,
|
|
346
|
+
errors,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export default CommitHashSystem;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compression Engine for Trace
|
|
3
|
+
* Delta-based storage for fast commits and minimal file size
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Delta {
|
|
7
|
+
type: 'add' | 'modify' | 'delete';
|
|
8
|
+
path: string;
|
|
9
|
+
hash: string;
|
|
10
|
+
baseHash?: string;
|
|
11
|
+
size: number;
|
|
12
|
+
compressed: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CompressionStats {
|
|
16
|
+
originalSize: number;
|
|
17
|
+
compressedSize: number;
|
|
18
|
+
ratio: number;
|
|
19
|
+
deltas: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class CompressionEngine {
|
|
23
|
+
/**
|
|
24
|
+
* Calculate delta between two file versions
|
|
25
|
+
*/
|
|
26
|
+
static calculateDelta(oldContent: string, newContent: string): string {
|
|
27
|
+
if (oldContent === newContent) return '';
|
|
28
|
+
|
|
29
|
+
const oldLines = oldContent.split('\n');
|
|
30
|
+
const newLines = newContent.split('\n');
|
|
31
|
+
const delta: string[] = [];
|
|
32
|
+
|
|
33
|
+
// Simple line-based diff (production would use Levenshtein or similar)
|
|
34
|
+
for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
|
|
35
|
+
const oldLine = oldLines[i];
|
|
36
|
+
const newLine = newLines[i];
|
|
37
|
+
|
|
38
|
+
if (oldLine !== newLine) {
|
|
39
|
+
if (oldLine !== undefined) delta.push(`-${i}:${oldLine}`);
|
|
40
|
+
if (newLine !== undefined) delta.push(`+${i}:${newLine}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return delta.join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compress content using deflate-like algorithm (mock)
|
|
49
|
+
*/
|
|
50
|
+
static compress(content: string): { compressed: string; ratio: number } {
|
|
51
|
+
// In production, use zlib or similar
|
|
52
|
+
const compressed = Buffer.from(content).toString('base64');
|
|
53
|
+
const ratio = compressed.length > 0 ? content.length / compressed.length : 1;
|
|
54
|
+
|
|
55
|
+
return { compressed, ratio };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Decompress content
|
|
60
|
+
*/
|
|
61
|
+
static decompress(compressed: string): string {
|
|
62
|
+
try {
|
|
63
|
+
return Buffer.from(compressed, 'base64').toString('utf-8');
|
|
64
|
+
} catch {
|
|
65
|
+
return compressed;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Calculate hash (simple checksum)
|
|
71
|
+
*/
|
|
72
|
+
static hash(content: string): string {
|
|
73
|
+
let hash = 0;
|
|
74
|
+
for (let i = 0; i < content.length; i++) {
|
|
75
|
+
const char = content.charCodeAt(i);
|
|
76
|
+
hash = (hash << 5) - hash + char;
|
|
77
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
78
|
+
}
|
|
79
|
+
return Math.abs(hash).toString(36);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create delta from two files
|
|
84
|
+
*/
|
|
85
|
+
static createDelta(path: string, oldContent: string, newContent: string): Delta {
|
|
86
|
+
const delta = this.calculateDelta(oldContent, newContent);
|
|
87
|
+
const { compressed, ratio } = this.compress(delta);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
type: 'modify',
|
|
91
|
+
path,
|
|
92
|
+
hash: this.hash(newContent),
|
|
93
|
+
baseHash: this.hash(oldContent),
|
|
94
|
+
size: newContent.length,
|
|
95
|
+
compressed: ratio > 1,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Apply delta to reconstruct file
|
|
101
|
+
*/
|
|
102
|
+
static applyDelta(baseContent: string, delta: string): string {
|
|
103
|
+
const lines = baseContent.split('\n');
|
|
104
|
+
const changes = delta.split('\n');
|
|
105
|
+
|
|
106
|
+
for (const change of changes) {
|
|
107
|
+
if (change.startsWith('-')) {
|
|
108
|
+
const [, lineNumStr, ...rest] = change.split(':');
|
|
109
|
+
const lineNum = parseInt(lineNumStr, 10);
|
|
110
|
+
if (lineNum >= 0 && lineNum < lines.length) {
|
|
111
|
+
lines[lineNum] = '';
|
|
112
|
+
}
|
|
113
|
+
} else if (change.startsWith('+')) {
|
|
114
|
+
const [, lineNumStr, ...rest] = change.split(':');
|
|
115
|
+
const lineNum = parseInt(lineNumStr, 10);
|
|
116
|
+
const newLine = rest.join(':');
|
|
117
|
+
if (lineNum >= 0) {
|
|
118
|
+
lines[lineNum] = newLine;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return lines.join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Compress file set
|
|
128
|
+
*/
|
|
129
|
+
static compressFileSet(files: Map<string, string>): { deltas: Delta[]; stats: CompressionStats } {
|
|
130
|
+
const deltas: Delta[] = [];
|
|
131
|
+
let originalSize = 0;
|
|
132
|
+
let compressedSize = 0;
|
|
133
|
+
|
|
134
|
+
for (const [path, content] of files) {
|
|
135
|
+
originalSize += content.length;
|
|
136
|
+
|
|
137
|
+
const { compressed } = this.compress(content);
|
|
138
|
+
compressedSize += compressed.length;
|
|
139
|
+
|
|
140
|
+
const hash = this.hash(content);
|
|
141
|
+
|
|
142
|
+
deltas.push({
|
|
143
|
+
type: 'add',
|
|
144
|
+
path,
|
|
145
|
+
hash,
|
|
146
|
+
size: content.length,
|
|
147
|
+
compressed: compressed.length < content.length,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
deltas,
|
|
153
|
+
stats: {
|
|
154
|
+
originalSize,
|
|
155
|
+
compressedSize,
|
|
156
|
+
ratio: originalSize > 0 ? originalSize / compressedSize : 1,
|
|
157
|
+
deltas: deltas.length,
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Estimate compression benefit
|
|
164
|
+
*/
|
|
165
|
+
static estimateBenefit(fileCount: number, avgFileSize: number): { savings: string; ratio: string } {
|
|
166
|
+
const originalSize = fileCount * avgFileSize;
|
|
167
|
+
// Typical compression ratio ~1.5-2x
|
|
168
|
+
const estimatedCompressed = originalSize / 1.7;
|
|
169
|
+
const savings = `${((originalSize - estimatedCompressed) / originalSize * 100).toFixed(1)}%`;
|
|
170
|
+
const ratio = `${(originalSize / estimatedCompressed).toFixed(2)}x`;
|
|
171
|
+
|
|
172
|
+
return { savings, ratio };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export default CompressionEngine;
|