trellis 1.0.7 → 2.0.5

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +533 -82
  3. package/bin/trellis.mjs +2 -0
  4. package/dist/cli/index.js +4718 -0
  5. package/dist/core/index.js +12 -0
  6. package/dist/decisions/index.js +19 -0
  7. package/dist/embeddings/index.js +43 -0
  8. package/dist/index-1j1anhmr.js +4038 -0
  9. package/dist/index-3s0eak0p.js +1556 -0
  10. package/dist/index-8pce39mh.js +272 -0
  11. package/dist/index-a76rekgs.js +67 -0
  12. package/dist/index-cy9k1g6v.js +684 -0
  13. package/dist/index-fd4e26s4.js +69 -0
  14. package/dist/{store/eav-store.js → index-gkvhzm9f.js} +4 -6
  15. package/dist/index-gnw8d7d6.js +51 -0
  16. package/dist/index-vkpkfwhq.js +817 -0
  17. package/dist/index.js +118 -2876
  18. package/dist/links/index.js +55 -0
  19. package/dist/transformers-m9je15kg.js +32491 -0
  20. package/dist/vcs/index.js +110 -0
  21. package/logo.png +0 -0
  22. package/logo.svg +9 -0
  23. package/package.json +79 -76
  24. package/src/cli/index.ts +2340 -0
  25. package/src/core/index.ts +35 -0
  26. package/src/core/kernel/middleware.ts +44 -0
  27. package/src/core/persist/backend.ts +64 -0
  28. package/src/core/store/eav-store.ts +467 -0
  29. package/src/decisions/auto-capture.ts +136 -0
  30. package/src/decisions/hooks.ts +163 -0
  31. package/src/decisions/index.ts +261 -0
  32. package/src/decisions/types.ts +103 -0
  33. package/src/embeddings/chunker.ts +327 -0
  34. package/src/embeddings/index.ts +41 -0
  35. package/src/embeddings/model.ts +95 -0
  36. package/src/embeddings/search.ts +305 -0
  37. package/src/embeddings/store.ts +313 -0
  38. package/src/embeddings/types.ts +85 -0
  39. package/src/engine.ts +1083 -0
  40. package/src/garden/cluster.ts +330 -0
  41. package/src/garden/garden.ts +306 -0
  42. package/src/garden/index.ts +29 -0
  43. package/src/git/git-exporter.ts +286 -0
  44. package/src/git/git-importer.ts +329 -0
  45. package/src/git/git-reader.ts +189 -0
  46. package/src/git/index.ts +22 -0
  47. package/src/identity/governance.ts +211 -0
  48. package/src/identity/identity.ts +224 -0
  49. package/src/identity/index.ts +30 -0
  50. package/src/identity/signing-middleware.ts +97 -0
  51. package/src/index.ts +20 -0
  52. package/src/links/index.ts +49 -0
  53. package/src/links/lifecycle.ts +400 -0
  54. package/src/links/parser.ts +484 -0
  55. package/src/links/ref-index.ts +186 -0
  56. package/src/links/resolver.ts +314 -0
  57. package/src/links/types.ts +108 -0
  58. package/src/mcp/index.ts +22 -0
  59. package/src/mcp/server.ts +1278 -0
  60. package/src/semantic/csharp-parser.ts +493 -0
  61. package/src/semantic/go-parser.ts +585 -0
  62. package/src/semantic/index.ts +34 -0
  63. package/src/semantic/java-parser.ts +456 -0
  64. package/src/semantic/python-parser.ts +659 -0
  65. package/src/semantic/ruby-parser.ts +446 -0
  66. package/src/semantic/rust-parser.ts +784 -0
  67. package/src/semantic/semantic-merge.ts +210 -0
  68. package/src/semantic/ts-parser.ts +681 -0
  69. package/src/semantic/types.ts +175 -0
  70. package/src/sync/index.ts +32 -0
  71. package/src/sync/memory-transport.ts +66 -0
  72. package/src/sync/reconciler.ts +237 -0
  73. package/src/sync/sync-engine.ts +258 -0
  74. package/src/sync/types.ts +104 -0
  75. package/src/vcs/blob-store.ts +124 -0
  76. package/src/vcs/branch.ts +150 -0
  77. package/src/vcs/checkpoint.ts +64 -0
  78. package/src/vcs/decompose.ts +469 -0
  79. package/src/vcs/diff.ts +409 -0
  80. package/src/vcs/engine-context.ts +26 -0
  81. package/src/vcs/index.ts +23 -0
  82. package/src/vcs/issue.ts +800 -0
  83. package/src/vcs/merge.ts +425 -0
  84. package/src/vcs/milestone.ts +124 -0
  85. package/src/vcs/ops.ts +59 -0
  86. package/src/vcs/types.ts +213 -0
  87. package/src/vcs/vcs-middleware.ts +81 -0
  88. package/src/watcher/fs-watcher.ts +217 -0
  89. package/src/watcher/index.ts +9 -0
  90. package/src/watcher/ingestion.ts +116 -0
  91. package/dist/ai/index.js +0 -688
  92. package/dist/cli/server.js +0 -3321
  93. package/dist/cli/tql.js +0 -5282
  94. package/dist/client/tql-client.js +0 -108
  95. package/dist/graph/index.js +0 -2248
  96. package/dist/kernel/logic-middleware.js +0 -179
  97. package/dist/kernel/middleware.js +0 -0
  98. package/dist/kernel/operations.js +0 -32
  99. package/dist/kernel/schema-middleware.js +0 -34
  100. package/dist/kernel/security-middleware.js +0 -53
  101. package/dist/kernel/trellis-kernel.js +0 -2239
  102. package/dist/kernel/workspace.js +0 -91
  103. package/dist/persist/backend.js +0 -0
  104. package/dist/persist/sqlite-backend.js +0 -123
  105. package/dist/query/index.js +0 -1643
  106. package/dist/server/index.js +0 -3309
  107. package/dist/workflows/index.js +0 -3160
@@ -0,0 +1,213 @@
1
+ /**
2
+ * TrellisVCS Type Definitions
3
+ *
4
+ * VCS-specific operation kinds, payloads, and entity types
5
+ * that extend the trellis-core kernel primitives.
6
+ */
7
+
8
+ import type { KernelOp } from '../core/persist/backend.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // VCS Operation Kinds
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export type VcsOpKind =
15
+ // Tier 0: File-level operations
16
+ | 'vcs:fileAdd'
17
+ | 'vcs:fileModify'
18
+ | 'vcs:fileDelete'
19
+ | 'vcs:fileRename'
20
+ // Tier 1: Structural operations
21
+ | 'vcs:dirAdd'
22
+ | 'vcs:dirDelete'
23
+ // VCS control operations
24
+ | 'vcs:branchCreate'
25
+ | 'vcs:branchDelete'
26
+ | 'vcs:branchAdvance'
27
+ | 'vcs:milestoneCreate'
28
+ | 'vcs:checkpointCreate'
29
+ | 'vcs:merge'
30
+ // Tier 2: AST-level semantic patches (future)
31
+ | 'vcs:symbolRename'
32
+ | 'vcs:symbolMove'
33
+ | 'vcs:symbolExtract'
34
+ | 'vcs:signatureChange'
35
+ // Issue tracking
36
+ | 'vcs:issueCreate'
37
+ | 'vcs:issueUpdate'
38
+ | 'vcs:issueStart'
39
+ | 'vcs:issuePause'
40
+ | 'vcs:issueResume'
41
+ | 'vcs:issueClose'
42
+ | 'vcs:issueReopen'
43
+ | 'vcs:criterionAdd'
44
+ | 'vcs:criterionUpdate'
45
+ // Issue blocking
46
+ | 'vcs:issueBlock'
47
+ | 'vcs:issueUnblock'
48
+ // Decision traces
49
+ | 'vcs:decisionRecord';
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // VCS Operation Payload
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export interface VcsPayload {
56
+ // File operations
57
+ filePath?: string;
58
+ oldFilePath?: string;
59
+ contentHash?: string;
60
+ oldContentHash?: string;
61
+ size?: number;
62
+ language?: string;
63
+
64
+ // Branch operations
65
+ branchName?: string;
66
+ targetOpHash?: string;
67
+ sourceBranch?: string;
68
+ baseBranch?: string;
69
+
70
+ // Milestone operations
71
+ milestoneId?: string;
72
+ message?: string;
73
+ fromOpHash?: string;
74
+ toOpHash?: string;
75
+
76
+ // Checkpoint operations
77
+ trigger?: 'green-build' | 'interval' | 'op-count' | 'manual';
78
+
79
+ // Signature
80
+ signature?: string;
81
+ signedBy?: string;
82
+
83
+ // Issue tracking
84
+ issueId?: string;
85
+ issueTitle?: string;
86
+ issueStatus?: 'backlog' | 'queue' | 'in_progress' | 'paused' | 'closed';
87
+ issuePriority?: 'critical' | 'high' | 'medium' | 'low';
88
+ issueLabels?: string[];
89
+ parentIssueId?: string;
90
+ issueDescription?: string;
91
+ issueAssignee?: string;
92
+ pauseNote?: string;
93
+ blockedByIssueId?: string;
94
+
95
+ // Decision traces
96
+ decisionId?: string;
97
+ decisionContext?: string;
98
+ decisionRationale?: string;
99
+ decisionAlternatives?: string;
100
+ decisionToolName?: string;
101
+ decisionToolInput?: string;
102
+ decisionToolOutput?: string;
103
+
104
+ // Acceptance criteria
105
+ criterionId?: string;
106
+ criterionDescription?: string;
107
+ criterionCommand?: string;
108
+ criterionStatus?: 'pending' | 'passed' | 'failed';
109
+ criterionOutput?: string;
110
+ }
111
+
112
+ /**
113
+ * A VcsOp mirrors KernelOp but widens `kind` to accept VCS-specific strings.
114
+ * We don't extend KernelOp directly because the kernel types `kind` as a
115
+ * narrow union; our VCS kinds are a superset.
116
+ */
117
+ export interface VcsOp {
118
+ hash: string;
119
+ kind: VcsOpKind | string;
120
+ timestamp: string;
121
+ agentId: string;
122
+ previousHash?: string;
123
+ facts?: import('../core/store/eav-store.js').Fact[];
124
+ links?: import('../core/store/eav-store.js').Link[];
125
+ vcs?: VcsPayload;
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // File Change Events (from watcher)
130
+ // ---------------------------------------------------------------------------
131
+
132
+ export interface FileChangeEvent {
133
+ type: 'add' | 'modify' | 'delete' | 'rename';
134
+ path: string;
135
+ oldPath?: string;
136
+ contentHash?: string;
137
+ oldContentHash?: string;
138
+ size?: number;
139
+ timestamp: string;
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Entity ID Helpers
144
+ // ---------------------------------------------------------------------------
145
+
146
+ export function fileEntityId(path: string): string {
147
+ return `file:${path}`;
148
+ }
149
+
150
+ export function dirEntityId(path: string): string {
151
+ return `dir:${path}`;
152
+ }
153
+
154
+ export function branchEntityId(name: string): string {
155
+ return `branch:${name}`;
156
+ }
157
+
158
+ export function milestoneEntityId(hash: string): string {
159
+ return `milestone:${hash}`;
160
+ }
161
+
162
+ export function checkpointEntityId(hash: string): string {
163
+ return `checkpoint:${hash}`;
164
+ }
165
+
166
+ export function issueEntityId(id: string): string {
167
+ return id.startsWith('issue:') ? id : `issue:${id}`;
168
+ }
169
+
170
+ export function criterionEntityId(issueId: string, index: number): string {
171
+ const bare = issueId.replace(/^issue:/, '');
172
+ return `criterion:${bare}:ac-${index}`;
173
+ }
174
+
175
+ export function decisionEntityId(id: string): string {
176
+ return id.startsWith('decision:') ? id : `decision:${id}`;
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Repository Config
181
+ // ---------------------------------------------------------------------------
182
+
183
+ export interface TrellisVcsConfig {
184
+ /** Absolute path to the repository root. */
185
+ rootPath: string;
186
+
187
+ /** Glob patterns to ignore (e.g. ['node_modules', '.git', '*.log']). */
188
+ ignorePatterns: string[];
189
+
190
+ /** Debounce interval for file watcher in ms. */
191
+ debounceMs: number;
192
+
193
+ /** Name of the default branch. */
194
+ defaultBranch: string;
195
+
196
+ /** Path to the .trellis database file. */
197
+ dbPath: string;
198
+ }
199
+
200
+ export const DEFAULT_CONFIG: Omit<TrellisVcsConfig, 'rootPath'> = {
201
+ ignorePatterns: [
202
+ 'node_modules',
203
+ '.git',
204
+ '.trellis',
205
+ 'dist',
206
+ 'build',
207
+ '.DS_Store',
208
+ '*.log',
209
+ ],
210
+ debounceMs: 300,
211
+ defaultBranch: 'main',
212
+ dbPath: '.trellis/trellis.db',
213
+ };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * VCS Middleware
3
+ *
4
+ * Intercepts VcsOps in the kernel middleware chain and decomposes
5
+ * them into primitive EAV store operations before passing to the
6
+ * next middleware (or the store).
7
+ */
8
+
9
+ import type {
10
+ KernelMiddleware,
11
+ MiddlewareContext,
12
+ OpMiddlewareNext,
13
+ } from '../core/kernel/middleware.js';
14
+ import type { KernelOp } from '../core/persist/backend.js';
15
+ import { decompose } from './decompose.js';
16
+ import { isVcsOp } from './ops.js';
17
+ import type { VcsOp } from './types.js';
18
+
19
+ export class VcsMiddleware implements KernelMiddleware {
20
+ name = 'vcs';
21
+
22
+ async handleOp(
23
+ op: KernelOp,
24
+ ctx: MiddlewareContext,
25
+ next: OpMiddlewareNext,
26
+ ): Promise<void> {
27
+ if (!isVcsOp(op as any)) {
28
+ return next(op, ctx);
29
+ }
30
+
31
+ const vcsOp = op as unknown as VcsOp;
32
+ const decomposed = decompose(vcsOp);
33
+
34
+ // Apply the decomposed primitive operations to the store.
35
+ // We create synthetic KernelOps for each batch and pass them through.
36
+ if (decomposed.deleteFacts.length > 0) {
37
+ await next(
38
+ {
39
+ ...op,
40
+ kind: 'deleteFacts' as any,
41
+ facts: decomposed.deleteFacts,
42
+ links: undefined,
43
+ } as KernelOp,
44
+ ctx,
45
+ );
46
+ }
47
+ if (decomposed.deleteLinks.length > 0) {
48
+ await next(
49
+ {
50
+ ...op,
51
+ kind: 'deleteLinks' as any,
52
+ facts: undefined,
53
+ links: decomposed.deleteLinks,
54
+ } as KernelOp,
55
+ ctx,
56
+ );
57
+ }
58
+ if (decomposed.addFacts.length > 0) {
59
+ await next(
60
+ {
61
+ ...op,
62
+ kind: 'addFacts' as any,
63
+ facts: decomposed.addFacts,
64
+ links: undefined,
65
+ } as KernelOp,
66
+ ctx,
67
+ );
68
+ }
69
+ if (decomposed.addLinks.length > 0) {
70
+ await next(
71
+ {
72
+ ...op,
73
+ kind: 'addLinks' as any,
74
+ facts: undefined,
75
+ links: decomposed.addLinks,
76
+ } as KernelOp,
77
+ ctx,
78
+ );
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Filesystem Watcher
3
+ *
4
+ * Monitors a directory tree for changes using Bun's fs.watch.
5
+ * Debounces rapid events and filters ignored paths.
6
+ * Emits FileChangeEvents to a callback.
7
+ */
8
+
9
+ import { watch, type FSWatcher } from 'fs';
10
+ import { readdir, stat, readFile } from 'fs/promises';
11
+ import { join, relative } from 'path';
12
+ import type { FileChangeEvent } from '../vcs/types.js';
13
+
14
+ export interface FileWatcherConfig {
15
+ rootPath: string;
16
+ ignorePatterns: string[];
17
+ debounceMs: number;
18
+ onEvent: (event: FileChangeEvent) => void | Promise<void>;
19
+ }
20
+
21
+ /**
22
+ * Computes SHA-256 content hash for a file.
23
+ */
24
+ async function hashFile(filePath: string): Promise<string> {
25
+ const content = await readFile(filePath);
26
+ const hashBuffer = await crypto.subtle.digest('SHA-256', content);
27
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
28
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
29
+ }
30
+
31
+ /**
32
+ * Checks if a path matches any ignore pattern (simple glob-like matching).
33
+ */
34
+ function shouldIgnore(relPath: string, patterns: string[]): boolean {
35
+ for (const pattern of patterns) {
36
+ // Simple matching: exact segment match or extension glob
37
+ if (pattern.startsWith('*.')) {
38
+ const ext = pattern.slice(1); // e.g. '.log'
39
+ if (relPath.endsWith(ext)) return true;
40
+ } else if (relPath.includes(pattern)) {
41
+ return true;
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+
47
+ export class FileWatcher {
48
+ private config: FileWatcherConfig;
49
+ private watchers: FSWatcher[] = [];
50
+ private debounceTimers = new Map<string, ReturnType<typeof setTimeout>>();
51
+ private knownFiles = new Map<string, string>(); // relPath → contentHash
52
+ private running = false;
53
+
54
+ constructor(config: FileWatcherConfig) {
55
+ this.config = config;
56
+ }
57
+
58
+ /**
59
+ * Scans the directory tree and builds an initial map of all tracked files.
60
+ * Returns the list of FileChangeEvents for the initial state (all adds).
61
+ */
62
+ async scan(): Promise<FileChangeEvent[]> {
63
+ const events: FileChangeEvent[] = [];
64
+ const entries = await this.walkDir(this.config.rootPath);
65
+
66
+ for (const absPath of entries) {
67
+ const relPath = relative(this.config.rootPath, absPath);
68
+ if (shouldIgnore(relPath, this.config.ignorePatterns)) continue;
69
+
70
+ try {
71
+ const hash = await hashFile(absPath);
72
+ const stats = await stat(absPath);
73
+ this.knownFiles.set(relPath, hash);
74
+ events.push({
75
+ type: 'add',
76
+ path: relPath,
77
+ contentHash: hash,
78
+ size: stats.size,
79
+ timestamp: new Date().toISOString(),
80
+ });
81
+ } catch {
82
+ // File may have been deleted between scan and hash
83
+ }
84
+ }
85
+
86
+ return events;
87
+ }
88
+
89
+ /**
90
+ * Starts watching the directory tree for changes.
91
+ */
92
+ start(): void {
93
+ if (this.running) return;
94
+ this.running = true;
95
+
96
+ try {
97
+ const watcher = watch(
98
+ this.config.rootPath,
99
+ { recursive: true },
100
+ (eventType, filename) => {
101
+ if (!filename) return;
102
+ const relPath = filename.toString();
103
+ if (shouldIgnore(relPath, this.config.ignorePatterns)) return;
104
+ this.debouncedHandle(relPath);
105
+ },
106
+ );
107
+ this.watchers.push(watcher);
108
+ } catch {
109
+ // recursive watch not supported on all platforms; fall back gracefully
110
+ console.warn('Recursive watch not supported; using scan-based polling.');
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Stops all watchers.
116
+ */
117
+ stop(): void {
118
+ this.running = false;
119
+ for (const w of this.watchers) {
120
+ w.close();
121
+ }
122
+ this.watchers = [];
123
+ for (const timer of this.debounceTimers.values()) {
124
+ clearTimeout(timer);
125
+ }
126
+ this.debounceTimers.clear();
127
+ }
128
+
129
+ /**
130
+ * Returns the current known file map (path → contentHash).
131
+ */
132
+ getKnownFiles(): Map<string, string> {
133
+ return new Map(this.knownFiles);
134
+ }
135
+
136
+ private debouncedHandle(relPath: string): void {
137
+ const existing = this.debounceTimers.get(relPath);
138
+ if (existing) clearTimeout(existing);
139
+
140
+ const timer = setTimeout(async () => {
141
+ this.debounceTimers.delete(relPath);
142
+ await this.handleChange(relPath);
143
+ }, this.config.debounceMs);
144
+
145
+ this.debounceTimers.set(relPath, timer);
146
+ }
147
+
148
+ private async handleChange(relPath: string): Promise<void> {
149
+ const absPath = join(this.config.rootPath, relPath);
150
+ const known = this.knownFiles.get(relPath);
151
+
152
+ try {
153
+ const stats = await stat(absPath);
154
+ if (!stats.isFile()) return;
155
+
156
+ const hash = await hashFile(absPath);
157
+
158
+ if (!known) {
159
+ // New file
160
+ this.knownFiles.set(relPath, hash);
161
+ await this.config.onEvent({
162
+ type: 'add',
163
+ path: relPath,
164
+ contentHash: hash,
165
+ size: stats.size,
166
+ timestamp: new Date().toISOString(),
167
+ });
168
+ } else if (known !== hash) {
169
+ // Modified file
170
+ const oldHash = known;
171
+ this.knownFiles.set(relPath, hash);
172
+ await this.config.onEvent({
173
+ type: 'modify',
174
+ path: relPath,
175
+ contentHash: hash,
176
+ oldContentHash: oldHash,
177
+ size: stats.size,
178
+ timestamp: new Date().toISOString(),
179
+ });
180
+ }
181
+ // If hash is the same, no event (unchanged)
182
+ } catch {
183
+ // File doesn't exist → it was deleted
184
+ if (known) {
185
+ this.knownFiles.delete(relPath);
186
+ await this.config.onEvent({
187
+ type: 'delete',
188
+ path: relPath,
189
+ contentHash: known,
190
+ timestamp: new Date().toISOString(),
191
+ });
192
+ }
193
+ }
194
+ }
195
+
196
+ private async walkDir(dir: string): Promise<string[]> {
197
+ const results: string[] = [];
198
+ try {
199
+ const entries = await readdir(dir, { withFileTypes: true });
200
+ for (const entry of entries) {
201
+ const fullPath = join(dir, entry.name);
202
+ const relFromRoot = relative(this.config.rootPath, fullPath);
203
+ if (shouldIgnore(relFromRoot, this.config.ignorePatterns)) continue;
204
+
205
+ if (entry.isDirectory()) {
206
+ const sub = await this.walkDir(fullPath);
207
+ results.push(...sub);
208
+ } else if (entry.isFile()) {
209
+ results.push(fullPath);
210
+ }
211
+ }
212
+ } catch {
213
+ // Permission error or deleted dir
214
+ }
215
+ return results;
216
+ }
217
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * File Watcher
3
+ *
4
+ * Monitors a directory for filesystem changes and emits FileChangeEvents.
5
+ * Uses Bun's built-in fs.watch for zero-dependency file monitoring.
6
+ */
7
+
8
+ export { FileWatcher } from './fs-watcher.js';
9
+ export { Ingestion } from './ingestion.js';
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Ingestion Pipeline
3
+ *
4
+ * Bridges the file watcher and the kernel: converts FileChangeEvents
5
+ * into VcsOps and applies them to the kernel via mutate().
6
+ */
7
+
8
+ import type { FileChangeEvent, VcsOpKind } from '../vcs/types.js';
9
+ import { createVcsOp } from '../vcs/ops.js';
10
+ import type { VcsOp } from '../vcs/types.js';
11
+ import { extname } from 'path';
12
+
13
+ // Simple language detection from file extension
14
+ const EXT_LANGUAGE: Record<string, string> = {
15
+ '.ts': 'typescript',
16
+ '.tsx': 'typescript',
17
+ '.js': 'javascript',
18
+ '.jsx': 'javascript',
19
+ '.py': 'python',
20
+ '.rs': 'rust',
21
+ '.go': 'go',
22
+ '.rb': 'ruby',
23
+ '.java': 'java',
24
+ '.c': 'c',
25
+ '.cpp': 'cpp',
26
+ '.h': 'c',
27
+ '.hpp': 'cpp',
28
+ '.cs': 'csharp',
29
+ '.swift': 'swift',
30
+ '.kt': 'kotlin',
31
+ '.md': 'markdown',
32
+ '.json': 'json',
33
+ '.yaml': 'yaml',
34
+ '.yml': 'yaml',
35
+ '.toml': 'toml',
36
+ '.html': 'html',
37
+ '.css': 'css',
38
+ '.scss': 'scss',
39
+ '.vue': 'vue',
40
+ '.svelte': 'svelte',
41
+ };
42
+
43
+ function detectLanguage(filePath: string): string | undefined {
44
+ const ext = extname(filePath).toLowerCase();
45
+ return EXT_LANGUAGE[ext];
46
+ }
47
+
48
+ export class Ingestion {
49
+ private agentId: string;
50
+ private lastOpHash: string | undefined;
51
+ private onOp: (op: VcsOp) => void | Promise<void>;
52
+
53
+ constructor(opts: {
54
+ agentId: string;
55
+ lastOpHash?: string;
56
+ onOp: (op: VcsOp) => void | Promise<void>;
57
+ }) {
58
+ this.agentId = opts.agentId;
59
+ this.lastOpHash = opts.lastOpHash;
60
+ this.onOp = opts.onOp;
61
+ }
62
+
63
+ /**
64
+ * Processes a single FileChangeEvent, producing and emitting a VcsOp.
65
+ */
66
+ async process(event: FileChangeEvent): Promise<VcsOp> {
67
+ let kind: VcsOpKind;
68
+
69
+ switch (event.type) {
70
+ case 'add':
71
+ kind = 'vcs:fileAdd';
72
+ break;
73
+ case 'modify':
74
+ kind = 'vcs:fileModify';
75
+ break;
76
+ case 'delete':
77
+ kind = 'vcs:fileDelete';
78
+ break;
79
+ case 'rename':
80
+ kind = 'vcs:fileRename';
81
+ break;
82
+ }
83
+
84
+ const op = await createVcsOp(kind, {
85
+ agentId: this.agentId,
86
+ previousHash: this.lastOpHash,
87
+ vcs: {
88
+ filePath: event.path,
89
+ oldFilePath: event.oldPath,
90
+ contentHash: event.contentHash,
91
+ oldContentHash: event.oldContentHash,
92
+ size: event.size,
93
+ language: detectLanguage(event.path),
94
+ },
95
+ });
96
+
97
+ this.lastOpHash = op.hash;
98
+ await this.onOp(op);
99
+ return op;
100
+ }
101
+
102
+ /**
103
+ * Processes a batch of FileChangeEvents in order.
104
+ */
105
+ async processBatch(events: FileChangeEvent[]): Promise<VcsOp[]> {
106
+ const ops: VcsOp[] = [];
107
+ for (const event of events) {
108
+ ops.push(await this.process(event));
109
+ }
110
+ return ops;
111
+ }
112
+
113
+ getLastOpHash(): string | undefined {
114
+ return this.lastOpHash;
115
+ }
116
+ }