@jmoyers/harness 0.1.11 → 0.1.20

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 (232) hide show
  1. package/README.md +31 -39
  2. package/package.json +31 -11
  3. package/packages/harness-ai/src/anthropic-protocol.ts +68 -68
  4. package/packages/harness-ai/src/stream-text.ts +13 -91
  5. package/packages/harness-ui/src/frame-primitives.ts +158 -0
  6. package/packages/harness-ui/src/index.ts +18 -0
  7. package/packages/harness-ui/src/interaction/conversation-input-forwarder.ts +221 -0
  8. package/packages/harness-ui/src/interaction/conversation-selection-input.ts +213 -0
  9. package/packages/harness-ui/src/interaction/global-shortcut-input.ts +172 -0
  10. package/{src/ui → packages/harness-ui/src/interaction}/input-preflight.ts +10 -12
  11. package/{src/ui → packages/harness-ui/src/interaction}/input-token-router.ts +120 -69
  12. package/packages/harness-ui/src/interaction/input.ts +420 -0
  13. package/packages/harness-ui/src/interaction/left-nav-input.ts +166 -0
  14. package/{src/ui → packages/harness-ui/src/interaction}/main-pane-pointer-input.ts +91 -23
  15. package/{src/ui → packages/harness-ui/src/interaction}/pointer-routing-input.ts +112 -48
  16. package/packages/harness-ui/src/interaction/rail-pointer-input.ts +62 -0
  17. package/packages/harness-ui/src/interaction/repository-fold-input.ts +118 -0
  18. package/packages/harness-ui/src/kit.ts +476 -0
  19. package/packages/harness-ui/src/layout.ts +238 -0
  20. package/packages/harness-ui/src/modal-manager.ts +222 -0
  21. package/{src/ui → packages/harness-ui/src}/screen.ts +53 -26
  22. package/packages/harness-ui/src/surface.ts +252 -0
  23. package/packages/harness-ui/src/text-layout.ts +210 -0
  24. package/packages/nim-core/src/contracts.ts +239 -0
  25. package/packages/nim-core/src/event-store.ts +299 -0
  26. package/packages/nim-core/src/events.ts +53 -0
  27. package/packages/nim-core/src/index.ts +9 -0
  28. package/packages/nim-core/src/provider-router.ts +129 -0
  29. package/packages/nim-core/src/providers/anthropic-driver.ts +291 -0
  30. package/packages/nim-core/src/runtime-factory.ts +49 -0
  31. package/packages/nim-core/src/runtime.ts +1797 -0
  32. package/packages/nim-core/src/session-store.ts +516 -0
  33. package/packages/nim-core/src/telemetry.ts +48 -0
  34. package/packages/nim-test-tui/src/index.ts +150 -0
  35. package/packages/nim-ui-core/src/index.ts +1 -0
  36. package/packages/nim-ui-core/src/projection.ts +87 -0
  37. package/scripts/codex-live-mux-runtime.ts +2 -3872
  38. package/scripts/control-plane-daemon.ts +11 -0
  39. package/scripts/harness-bin.js +5 -0
  40. package/scripts/harness-commands.ts +300 -0
  41. package/scripts/harness-runtime.ts +82 -0
  42. package/scripts/harness.ts +33 -3019
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/gateway/runtime.ts +1872 -0
  46. package/src/cli/parsing/flags.ts +23 -0
  47. package/src/cli/parsing/session.ts +42 -0
  48. package/src/cli/runtime/context.ts +193 -0
  49. package/src/cli/runtime-app/application.ts +392 -0
  50. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  51. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  52. package/src/cli/workflows/runtime.ts +965 -0
  53. package/src/clients/tui/left-rail-interactions.ts +519 -0
  54. package/src/clients/tui/main-pane-interactions.ts +509 -0
  55. package/src/clients/tui/modal-input-routing.ts +71 -0
  56. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  57. package/src/clients/web/synced-selectors.ts +132 -0
  58. package/src/codex/live-session.ts +82 -29
  59. package/src/config/config-core.ts +348 -8
  60. package/src/config/harness.config.template.jsonc +33 -0
  61. package/src/control-plane/agent-realtime-api.ts +82 -427
  62. package/src/control-plane/session-summary.ts +10 -81
  63. package/src/control-plane/status/reducer-base.ts +12 -12
  64. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  65. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  66. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  67. package/src/control-plane/stream-client.ts +12 -2
  68. package/src/control-plane/stream-command-parser.ts +83 -143
  69. package/src/control-plane/stream-protocol.ts +53 -37
  70. package/src/control-plane/stream-server-command.ts +376 -69
  71. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  72. package/src/control-plane/stream-server.ts +864 -70
  73. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  74. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  75. package/src/core/state/observed-stream-cursor.ts +43 -0
  76. package/src/core/state/synced-observed-state.ts +273 -0
  77. package/src/core/store/harness-synced-store.ts +81 -0
  78. package/src/diff/budget.ts +136 -0
  79. package/src/diff/build.ts +289 -0
  80. package/src/diff/chunker.ts +146 -0
  81. package/src/diff/git-invoke.ts +315 -0
  82. package/src/diff/git-parse.ts +472 -0
  83. package/src/diff/hash.ts +70 -0
  84. package/src/diff/index.ts +24 -0
  85. package/src/diff/normalize.ts +134 -0
  86. package/src/diff/types.ts +178 -0
  87. package/src/diff-ui/args.ts +346 -0
  88. package/src/diff-ui/commands.ts +123 -0
  89. package/src/diff-ui/finder.ts +94 -0
  90. package/src/diff-ui/highlight.ts +127 -0
  91. package/src/diff-ui/index.ts +2 -0
  92. package/src/diff-ui/model.ts +141 -0
  93. package/src/diff-ui/pager.ts +412 -0
  94. package/src/diff-ui/render.ts +337 -0
  95. package/src/diff-ui/runtime.ts +379 -0
  96. package/src/diff-ui/state.ts +224 -0
  97. package/src/diff-ui/types.ts +236 -0
  98. package/src/domain/workspace.ts +68 -5
  99. package/src/mux/control-plane-op-queue.ts +93 -7
  100. package/src/mux/conversation-rail.ts +28 -71
  101. package/src/mux/dual-pane-core.ts +13 -13
  102. package/src/mux/harness-core-ui.ts +313 -42
  103. package/src/mux/input-shortcuts.ts +13 -131
  104. package/src/mux/keybinding-catalog.ts +340 -0
  105. package/src/mux/keybinding-registry.ts +103 -0
  106. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  107. package/src/mux/live-mux/command-menu.ts +167 -4
  108. package/src/mux/live-mux/conversation-state.ts +13 -0
  109. package/src/mux/live-mux/directory-resolution.ts +1 -1
  110. package/src/mux/live-mux/git-snapshot.ts +33 -2
  111. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  112. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  113. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  114. package/src/mux/live-mux/input-forwarding.ts +59 -2
  115. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  116. package/src/mux/live-mux/left-nav.ts +35 -0
  117. package/src/mux/live-mux/link-click.ts +292 -0
  118. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  119. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  120. package/src/mux/live-mux/modal-input-reducers.ts +77 -12
  121. package/src/mux/live-mux/modal-overlays.ts +168 -34
  122. package/src/mux/live-mux/modal-pointer.ts +3 -7
  123. package/src/mux/live-mux/modal-prompt-handlers.ts +23 -2
  124. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  125. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  126. package/src/mux/live-mux/pointer-routing.ts +5 -2
  127. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  128. package/src/mux/live-mux/rail-layout.ts +33 -30
  129. package/src/mux/live-mux/release-notes.ts +383 -0
  130. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  131. package/src/mux/live-mux/repository-folding.ts +3 -0
  132. package/src/mux/live-mux/selection.ts +0 -4
  133. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  134. package/src/mux/project-pane-github-review.ts +271 -0
  135. package/src/mux/render-frame.ts +4 -0
  136. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  137. package/src/mux/task-composer.ts +21 -14
  138. package/src/mux/task-focused-pane.ts +118 -117
  139. package/src/mux/task-screen-keybindings.ts +10 -101
  140. package/src/mux/workspace-rail-model.ts +270 -104
  141. package/src/mux/workspace-rail.ts +45 -22
  142. package/src/pty/session-broker.ts +1 -1
  143. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  144. package/src/services/control-plane.ts +50 -32
  145. package/src/services/conversation-lifecycle.ts +118 -87
  146. package/src/services/conversation-startup-hydration.ts +20 -12
  147. package/src/services/directory-hydration.ts +21 -16
  148. package/src/services/event-persistence.ts +7 -0
  149. package/src/services/left-rail-pointer-handler.ts +329 -0
  150. package/src/services/mux-ui-state-persistence.ts +5 -1
  151. package/src/services/recording.ts +34 -26
  152. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  153. package/src/services/runtime-control-actions.ts +79 -61
  154. package/src/services/runtime-control-plane-ops.ts +122 -83
  155. package/src/services/runtime-conversation-actions.ts +40 -26
  156. package/src/services/runtime-conversation-activation.ts +73 -46
  157. package/src/services/runtime-conversation-starter.ts +53 -45
  158. package/src/services/runtime-conversation-title-edit.ts +91 -80
  159. package/src/services/runtime-envelope-handler.ts +107 -105
  160. package/src/services/runtime-git-state.ts +42 -29
  161. package/src/services/runtime-layout-resize.ts +3 -1
  162. package/src/services/runtime-left-rail-render.ts +99 -63
  163. package/src/services/runtime-nim-cli-session.ts +438 -0
  164. package/src/services/runtime-nim-session.ts +705 -0
  165. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  166. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  167. package/src/services/runtime-process-wiring.ts +29 -36
  168. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  169. package/src/services/runtime-render-flush.ts +63 -70
  170. package/src/services/runtime-render-lifecycle.ts +65 -64
  171. package/src/services/runtime-render-orchestrator.ts +55 -45
  172. package/src/services/runtime-render-pipeline.ts +106 -103
  173. package/src/services/runtime-render-state.ts +62 -49
  174. package/src/services/runtime-repository-actions.ts +97 -72
  175. package/src/services/runtime-right-pane-render.ts +80 -53
  176. package/src/services/runtime-shutdown.ts +38 -35
  177. package/src/services/runtime-stream-subscriptions.ts +35 -27
  178. package/src/services/runtime-task-composer-persistence.ts +71 -59
  179. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  180. package/src/services/runtime-task-editor-actions.ts +46 -29
  181. package/src/services/runtime-task-pane-actions.ts +220 -134
  182. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  183. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  184. package/src/services/runtime-workspace-observed-events.ts +33 -184
  185. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  186. package/src/services/session-diagnostics-store.ts +217 -0
  187. package/src/services/startup-background-resume.ts +26 -21
  188. package/src/services/startup-orchestrator.ts +16 -13
  189. package/src/services/startup-paint-tracker.ts +29 -21
  190. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  191. package/src/services/startup-settled-gate.ts +25 -15
  192. package/src/services/startup-shutdown.ts +18 -22
  193. package/src/services/startup-state-hydration.ts +44 -34
  194. package/src/services/startup-visibility.ts +12 -4
  195. package/src/services/task-pane-selection-actions.ts +89 -72
  196. package/src/services/task-planning-hydration.ts +24 -18
  197. package/src/services/task-planning-observed-events.ts +50 -52
  198. package/src/services/workspace-observed-events.ts +66 -63
  199. package/src/storage/storage-lifecycle-core.ts +438 -0
  200. package/src/store/control-plane-store-normalize.ts +33 -242
  201. package/src/store/control-plane-store-types.ts +1 -35
  202. package/src/store/control-plane-store.ts +360 -56
  203. package/src/store/event-store.ts +366 -8
  204. package/src/terminal/snapshot-oracle.ts +207 -94
  205. package/src/ui/mux-theme.ts +112 -8
  206. package/src/ui/panes/home-gridfire.ts +40 -31
  207. package/src/ui/panes/home.ts +10 -2
  208. package/src/ui/panes/nim.ts +315 -0
  209. package/src/mux/live-mux/actions-task.ts +0 -115
  210. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  211. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -85
  212. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  213. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  214. package/src/services/runtime-directory-actions.ts +0 -164
  215. package/src/services/runtime-input-pipeline.ts +0 -50
  216. package/src/services/runtime-input-router.ts +0 -195
  217. package/src/services/runtime-main-pane-input.ts +0 -230
  218. package/src/services/runtime-modal-input.ts +0 -137
  219. package/src/services/runtime-navigation-input.ts +0 -197
  220. package/src/services/runtime-rail-input.ts +0 -279
  221. package/src/services/runtime-task-pane.ts +0 -62
  222. package/src/services/runtime-workspace-actions.ts +0 -158
  223. package/src/ui/conversation-input-forwarder.ts +0 -114
  224. package/src/ui/conversation-selection-input.ts +0 -103
  225. package/src/ui/global-shortcut-input.ts +0 -89
  226. package/src/ui/input.ts +0 -269
  227. package/src/ui/kit.ts +0 -509
  228. package/src/ui/left-nav-input.ts +0 -80
  229. package/src/ui/left-rail-pointer-input.ts +0 -148
  230. package/src/ui/modals/manager.ts +0 -218
  231. package/src/ui/repository-fold-input.ts +0 -91
  232. package/src/ui/surface.ts +0 -224
@@ -0,0 +1,472 @@
1
+ import { computeDiffFileId, computeDiffHunkId, serializeHunkLinesForHash } from './hash.ts';
2
+ import {
3
+ inferLanguageFromPath,
4
+ isGeneratedPath,
5
+ normalizeDiffPath,
6
+ resolveFileChangeType,
7
+ } from './normalize.ts';
8
+ import type { DiffBudgetTracker } from './budget.ts';
9
+ import type {
10
+ DiffCoverageReason,
11
+ DiffFile,
12
+ DiffHunk,
13
+ DiffLine,
14
+ DiffTotals,
15
+ FileChangeType,
16
+ } from './types.ts';
17
+
18
+ interface MutableHunk {
19
+ oldStart: number;
20
+ oldCount: number;
21
+ newStart: number;
22
+ newCount: number;
23
+ header: string;
24
+ lines: DiffLine[];
25
+ addCount: number;
26
+ delCount: number;
27
+ oldCursor: number;
28
+ newCursor: number;
29
+ }
30
+
31
+ interface MutableFile {
32
+ changeTypeHint: FileChangeType | null;
33
+ oldPath: string | null;
34
+ newPath: string | null;
35
+ isBinary: boolean;
36
+ isGenerated: boolean;
37
+ isTooLarge: boolean;
38
+ hunks: MutableHunk[];
39
+ }
40
+
41
+ function parseHunkHeader(line: string): {
42
+ oldStart: number;
43
+ oldCount: number;
44
+ newStart: number;
45
+ newCount: number;
46
+ header: string;
47
+ } | null {
48
+ const matched = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/u.exec(line);
49
+ if (matched === null) {
50
+ return null;
51
+ }
52
+ const oldStart = Number.parseInt(matched[1]!, 10);
53
+ const oldCount = Number.parseInt(matched[2] ?? '1', 10);
54
+ const newStart = Number.parseInt(matched[3]!, 10);
55
+ const newCount = Number.parseInt(matched[4] ?? '1', 10);
56
+ const suffix = matched[5] ?? '';
57
+ return {
58
+ oldStart,
59
+ oldCount,
60
+ newStart,
61
+ newCount,
62
+ header: `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@${suffix}`,
63
+ };
64
+ }
65
+
66
+ function parseTokenList(raw: string): readonly string[] {
67
+ const tokens: string[] = [];
68
+ let index = 0;
69
+ while (index < raw.length) {
70
+ while (index < raw.length && raw[index] === ' ') {
71
+ index += 1;
72
+ }
73
+ if (index >= raw.length) {
74
+ break;
75
+ }
76
+ if (raw[index] === '"') {
77
+ index += 1;
78
+ let token = '';
79
+ while (index < raw.length) {
80
+ const char = raw[index]!;
81
+ if (char === '\\') {
82
+ const next = raw[index + 1] ?? '';
83
+ if (next.length > 0) {
84
+ token += next;
85
+ index += 2;
86
+ continue;
87
+ }
88
+ }
89
+ if (char === '"') {
90
+ index += 1;
91
+ break;
92
+ }
93
+ token += char;
94
+ index += 1;
95
+ }
96
+ tokens.push(token);
97
+ continue;
98
+ }
99
+ let token = '';
100
+ while (index < raw.length && raw[index] !== ' ') {
101
+ token += raw[index]!;
102
+ index += 1;
103
+ }
104
+ tokens.push(token);
105
+ }
106
+ return tokens;
107
+ }
108
+
109
+ function stripGitPrefix(value: string, prefix: 'a/' | 'b/'): string {
110
+ if (value.startsWith(prefix)) {
111
+ return value.slice(prefix.length);
112
+ }
113
+ return value;
114
+ }
115
+
116
+ function parseDiffGitPaths(
117
+ line: string,
118
+ ): { oldPath: string | null; newPath: string | null } | null {
119
+ const prefix = 'diff --git ';
120
+ if (!line.startsWith(prefix)) {
121
+ return null;
122
+ }
123
+ const tokens = parseTokenList(line.slice(prefix.length));
124
+ if (tokens.length < 2) {
125
+ return null;
126
+ }
127
+ const oldToken = tokens[0]!;
128
+ const newToken = tokens[1]!;
129
+ return {
130
+ oldPath: normalizeDiffPath(stripGitPrefix(oldToken, 'a/')),
131
+ newPath: normalizeDiffPath(stripGitPrefix(newToken, 'b/')),
132
+ };
133
+ }
134
+
135
+ function parsePatchPathLine(line: string, marker: '--- ' | '+++ '): string | null | undefined {
136
+ if (!line.startsWith(marker)) {
137
+ return undefined;
138
+ }
139
+ const raw = line.slice(marker.length).split('\t')[0] ?? '';
140
+ if (raw === '/dev/null') {
141
+ return null;
142
+ }
143
+ if (raw.startsWith('a/')) {
144
+ return raw.slice(2);
145
+ }
146
+ if (raw.startsWith('b/')) {
147
+ return raw.slice(2);
148
+ }
149
+ return raw;
150
+ }
151
+
152
+ interface GitDiffPatchParserOptions {
153
+ readonly includeGenerated: boolean;
154
+ readonly includeBinary: boolean;
155
+ readonly budget: DiffBudgetTracker;
156
+ readonly onFile?: (file: DiffFile) => void;
157
+ readonly onHunk?: (fileId: string, hunk: DiffHunk) => void;
158
+ }
159
+
160
+ interface GitDiffPatchParserResult {
161
+ readonly files: readonly DiffFile[];
162
+ readonly totals: DiffTotals;
163
+ readonly warnings: readonly string[];
164
+ readonly skippedFiles: number;
165
+ readonly truncatedFiles: number;
166
+ readonly limitReason: DiffCoverageReason;
167
+ }
168
+
169
+ export class GitDiffPatchParser {
170
+ private currentFile: MutableFile | null = null;
171
+ private currentHunk: MutableHunk | null = null;
172
+ private readonly files: DiffFile[] = [];
173
+ private readonly warnings: string[] = [];
174
+ private skippedFiles = 0;
175
+ private truncatedFiles = 0;
176
+ private halted = false;
177
+
178
+ constructor(private readonly options: GitDiffPatchParserOptions) {}
179
+
180
+ pushLine(line: string): boolean {
181
+ if (this.halted) {
182
+ return false;
183
+ }
184
+ const runtimeCheck = this.options.budget.checkRuntime();
185
+ if (!runtimeCheck.allowed) {
186
+ this.halted = true;
187
+ this.markTruncatedCurrentFile();
188
+ return false;
189
+ }
190
+
191
+ const parsedPaths = parseDiffGitPaths(line);
192
+ if (parsedPaths !== null) {
193
+ this.finalizeCurrentFile();
194
+ const generated = isGeneratedPath(parsedPaths.newPath ?? parsedPaths.oldPath);
195
+ this.currentFile = {
196
+ changeTypeHint: null,
197
+ oldPath: parsedPaths.oldPath,
198
+ newPath: parsedPaths.newPath,
199
+ isBinary: false,
200
+ isGenerated: generated,
201
+ isTooLarge: false,
202
+ hunks: [],
203
+ };
204
+ this.currentHunk = null;
205
+ if (generated && !this.options.includeGenerated) {
206
+ this.skippedFiles += 1;
207
+ } else {
208
+ const tookFile = this.options.budget.takeFile();
209
+ if (!tookFile.allowed) {
210
+ this.markTruncatedCurrentFile();
211
+ this.halted = true;
212
+ return false;
213
+ }
214
+ }
215
+ return true;
216
+ }
217
+
218
+ if (this.currentFile === null) {
219
+ return true;
220
+ }
221
+ if (this.shouldSkipCurrentFile()) {
222
+ return true;
223
+ }
224
+
225
+ const oldPathPatch = parsePatchPathLine(line, '--- ');
226
+ if (oldPathPatch !== undefined) {
227
+ this.currentFile.oldPath = normalizeDiffPath(oldPathPatch);
228
+ return true;
229
+ }
230
+ const newPathPatch = parsePatchPathLine(line, '+++ ');
231
+ if (newPathPatch !== undefined) {
232
+ this.currentFile.newPath = normalizeDiffPath(newPathPatch);
233
+ return true;
234
+ }
235
+
236
+ if (line.startsWith('new file mode ')) {
237
+ this.currentFile.changeTypeHint = 'added';
238
+ return true;
239
+ }
240
+ if (line.startsWith('deleted file mode ')) {
241
+ this.currentFile.changeTypeHint = 'deleted';
242
+ return true;
243
+ }
244
+ if (line.startsWith('rename from ')) {
245
+ this.currentFile.changeTypeHint = 'renamed';
246
+ this.currentFile.oldPath = normalizeDiffPath(line.slice('rename from '.length));
247
+ return true;
248
+ }
249
+ if (line.startsWith('rename to ')) {
250
+ this.currentFile.changeTypeHint = 'renamed';
251
+ this.currentFile.newPath = normalizeDiffPath(line.slice('rename to '.length));
252
+ return true;
253
+ }
254
+ if (line.startsWith('copy from ')) {
255
+ this.currentFile.changeTypeHint = 'copied';
256
+ this.currentFile.oldPath = normalizeDiffPath(line.slice('copy from '.length));
257
+ return true;
258
+ }
259
+ if (line.startsWith('copy to ')) {
260
+ this.currentFile.changeTypeHint = 'copied';
261
+ this.currentFile.newPath = normalizeDiffPath(line.slice('copy to '.length));
262
+ return true;
263
+ }
264
+ if (line.startsWith('similarity index ')) {
265
+ return true;
266
+ }
267
+ if (line.startsWith('dissimilarity index ')) {
268
+ return true;
269
+ }
270
+ if (line.startsWith('Binary files ') || line === 'GIT binary patch') {
271
+ this.currentFile.isBinary = true;
272
+ this.currentFile.changeTypeHint = 'binary';
273
+ if (!this.options.includeBinary) {
274
+ this.skippedFiles += 1;
275
+ }
276
+ return true;
277
+ }
278
+ if (line.startsWith('old mode ') || line.startsWith('new mode ')) {
279
+ if (
280
+ this.currentFile.changeTypeHint === null ||
281
+ this.currentFile.changeTypeHint === 'unknown' ||
282
+ this.currentFile.changeTypeHint === 'modified'
283
+ ) {
284
+ this.currentFile.changeTypeHint = 'type-change';
285
+ }
286
+ return true;
287
+ }
288
+
289
+ const parsedHunk = parseHunkHeader(line);
290
+ if (parsedHunk !== null) {
291
+ const tookHunk = this.options.budget.takeHunk();
292
+ if (!tookHunk.allowed) {
293
+ this.markTruncatedCurrentFile();
294
+ this.halted = true;
295
+ return false;
296
+ }
297
+ this.finalizeCurrentHunk();
298
+ this.currentHunk = {
299
+ oldStart: parsedHunk.oldStart,
300
+ oldCount: parsedHunk.oldCount,
301
+ newStart: parsedHunk.newStart,
302
+ newCount: parsedHunk.newCount,
303
+ header: parsedHunk.header,
304
+ lines: [],
305
+ addCount: 0,
306
+ delCount: 0,
307
+ oldCursor: parsedHunk.oldStart,
308
+ newCursor: parsedHunk.newStart,
309
+ };
310
+ return true;
311
+ }
312
+
313
+ if (this.currentHunk !== null) {
314
+ if (line.startsWith('\')) {
315
+ return true;
316
+ }
317
+ const first = line[0] ?? '';
318
+ if (first !== ' ' && first !== '+' && first !== '-') {
319
+ return true;
320
+ }
321
+ const tookLine = this.options.budget.takeLine();
322
+ if (!tookLine.allowed) {
323
+ this.markTruncatedCurrentFile();
324
+ this.halted = true;
325
+ return false;
326
+ }
327
+ const normalizedText = line.slice(1);
328
+ let normalizedLine: DiffLine;
329
+ if (first === '+') {
330
+ normalizedLine = {
331
+ kind: 'add',
332
+ oldLine: null,
333
+ newLine: this.currentHunk.newCursor,
334
+ text: normalizedText,
335
+ };
336
+ this.currentHunk.addCount += 1;
337
+ this.currentHunk.newCursor += 1;
338
+ } else if (first === '-') {
339
+ normalizedLine = {
340
+ kind: 'del',
341
+ oldLine: this.currentHunk.oldCursor,
342
+ newLine: null,
343
+ text: normalizedText,
344
+ };
345
+ this.currentHunk.delCount += 1;
346
+ this.currentHunk.oldCursor += 1;
347
+ } else {
348
+ normalizedLine = {
349
+ kind: 'context',
350
+ oldLine: this.currentHunk.oldCursor,
351
+ newLine: this.currentHunk.newCursor,
352
+ text: normalizedText,
353
+ };
354
+ this.currentHunk.oldCursor += 1;
355
+ this.currentHunk.newCursor += 1;
356
+ }
357
+ this.currentHunk.lines.push(normalizedLine);
358
+ }
359
+ return true;
360
+ }
361
+
362
+ finish(): GitDiffPatchParserResult {
363
+ this.finalizeCurrentFile();
364
+ const totals: DiffTotals = {
365
+ filesChanged: this.files.length,
366
+ additions: this.files.reduce((sum, file) => sum + file.additions, 0),
367
+ deletions: this.files.reduce((sum, file) => sum + file.deletions, 0),
368
+ binaryFiles: this.files.reduce((sum, file) => sum + (file.isBinary ? 1 : 0), 0),
369
+ generatedFiles: this.files.reduce((sum, file) => sum + (file.isGenerated ? 1 : 0), 0),
370
+ hunks: this.files.reduce((sum, file) => sum + file.hunks.length, 0),
371
+ lines: this.files.reduce(
372
+ (sum, file) => sum + file.hunks.reduce((hunkSum, hunk) => hunkSum + hunk.lineCount, 0),
373
+ 0,
374
+ ),
375
+ };
376
+ return {
377
+ files: this.files,
378
+ totals,
379
+ warnings: this.warnings,
380
+ skippedFiles: this.skippedFiles,
381
+ truncatedFiles: this.truncatedFiles,
382
+ limitReason: this.options.budget.limitReason(),
383
+ };
384
+ }
385
+
386
+ private shouldSkipCurrentFile(): boolean {
387
+ if (this.currentFile === null) {
388
+ return false;
389
+ }
390
+ if (this.currentFile.isGenerated && !this.options.includeGenerated) {
391
+ return true;
392
+ }
393
+ if (this.currentFile.isBinary && !this.options.includeBinary) {
394
+ return true;
395
+ }
396
+ return false;
397
+ }
398
+
399
+ private markTruncatedCurrentFile(): void {
400
+ if (this.currentFile === null) {
401
+ return;
402
+ }
403
+ this.currentFile.isTooLarge = true;
404
+ this.truncatedFiles += 1;
405
+ }
406
+
407
+ private finalizeCurrentHunk(): void {
408
+ if (this.currentFile === null || this.currentHunk === null) {
409
+ return;
410
+ }
411
+ this.currentFile.hunks.push(this.currentHunk);
412
+ this.currentHunk = null;
413
+ }
414
+
415
+ private finalizeCurrentFile(): void {
416
+ if (this.currentFile === null) {
417
+ return;
418
+ }
419
+ this.finalizeCurrentHunk();
420
+ if (this.shouldSkipCurrentFile()) {
421
+ this.currentFile = null;
422
+ return;
423
+ }
424
+ const resolvedOldPath = normalizeDiffPath(this.currentFile.oldPath);
425
+ const resolvedNewPath = normalizeDiffPath(this.currentFile.newPath);
426
+ const changeType = resolveFileChangeType({
427
+ fromHeader: this.currentFile.changeTypeHint,
428
+ oldPath: resolvedOldPath,
429
+ newPath: resolvedNewPath,
430
+ isBinary: this.currentFile.isBinary,
431
+ });
432
+ const fileId = computeDiffFileId(changeType, resolvedOldPath, resolvedNewPath);
433
+ const finalHunks: DiffHunk[] = [];
434
+ for (const hunk of this.currentFile.hunks) {
435
+ const diffHunk: DiffHunk = {
436
+ hunkId: computeDiffHunkId(fileId, hunk.header, serializeHunkLinesForHash(hunk.lines)),
437
+ oldStart: hunk.oldStart,
438
+ oldCount: hunk.oldCount,
439
+ newStart: hunk.newStart,
440
+ newCount: hunk.newCount,
441
+ header: hunk.header,
442
+ lines: hunk.lines,
443
+ lineCount: hunk.lines.length,
444
+ addCount: hunk.addCount,
445
+ delCount: hunk.delCount,
446
+ };
447
+ finalHunks.push(diffHunk);
448
+ this.options.onHunk?.(fileId, diffHunk);
449
+ }
450
+ if (resolvedOldPath === null && resolvedNewPath === null) {
451
+ this.warnings.push('skipped file record with null oldPath/newPath');
452
+ this.currentFile = null;
453
+ return;
454
+ }
455
+ const diffFile: DiffFile = {
456
+ fileId,
457
+ changeType,
458
+ oldPath: resolvedOldPath,
459
+ newPath: resolvedNewPath,
460
+ language: inferLanguageFromPath(resolvedNewPath ?? resolvedOldPath),
461
+ isBinary: this.currentFile.isBinary,
462
+ isGenerated: this.currentFile.isGenerated,
463
+ isTooLarge: this.currentFile.isTooLarge,
464
+ additions: finalHunks.reduce((sum, hunk) => sum + hunk.addCount, 0),
465
+ deletions: finalHunks.reduce((sum, hunk) => sum + hunk.delCount, 0),
466
+ hunks: finalHunks,
467
+ };
468
+ this.files.push(diffFile);
469
+ this.options.onFile?.(diffFile);
470
+ this.currentFile = null;
471
+ }
472
+ }
@@ -0,0 +1,70 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { DiffFile, DiffHunk, DiffMode } from './types.ts';
3
+
4
+ function hashStrings(prefix: string, parts: readonly string[]): string {
5
+ const hash = createHash('sha256');
6
+ hash.update(prefix);
7
+ hash.update('\n');
8
+ for (const part of parts) {
9
+ hash.update(part);
10
+ hash.update('\n');
11
+ }
12
+ return hash.digest('hex');
13
+ }
14
+
15
+ function normalized(value: string | null): string {
16
+ return value ?? '';
17
+ }
18
+
19
+ export function computeDiffFileId(
20
+ changeType: string,
21
+ oldPath: string | null,
22
+ newPath: string | null,
23
+ ): string {
24
+ return hashStrings('diff-file', [changeType, normalized(oldPath), normalized(newPath)]);
25
+ }
26
+
27
+ export function computeDiffHunkId(
28
+ fileId: string,
29
+ header: string,
30
+ serializedLines: readonly string[],
31
+ ): string {
32
+ return hashStrings('diff-hunk', [fileId, header, ...serializedLines]);
33
+ }
34
+
35
+ export function serializeHunkLinesForHash(lines: DiffHunk['lines']): readonly string[] {
36
+ const serialized: string[] = [];
37
+ for (const line of lines) {
38
+ serialized.push(
39
+ `${line.kind}:${String(line.oldLine ?? '')}:${String(line.newLine ?? '')}:${line.text}`,
40
+ );
41
+ }
42
+ return serialized;
43
+ }
44
+
45
+ export function computeDiffId(
46
+ mode: DiffMode,
47
+ baseRef: string | null,
48
+ headRef: string | null,
49
+ files: readonly DiffFile[],
50
+ ): string {
51
+ const parts: string[] = [mode, normalized(baseRef), normalized(headRef)];
52
+ for (const file of files) {
53
+ parts.push(
54
+ `${file.fileId}:${file.changeType}:${normalized(file.oldPath)}:${normalized(file.newPath)}`,
55
+ );
56
+ for (const hunk of file.hunks) {
57
+ parts.push(hunk.hunkId);
58
+ }
59
+ }
60
+ return hashStrings('diff', parts);
61
+ }
62
+
63
+ export function computeDiffChunkId(
64
+ fileId: string,
65
+ sequence: number,
66
+ hunkIds: readonly string[],
67
+ policySignature: string,
68
+ ): string {
69
+ return hashStrings('diff-chunk', [fileId, String(sequence), policySignature, ...hunkIds]);
70
+ }
@@ -0,0 +1,24 @@
1
+ export { createDiffBuilder } from './build.ts';
2
+ export { createDiffChunker } from './chunker.ts';
3
+ export { DEFAULT_CHUNK_POLICY, DEFAULT_DIFF_BUDGET } from './types.ts';
4
+ export type {
5
+ ChunkPolicy,
6
+ DiffBudget,
7
+ DiffBuildDiagnostics,
8
+ DiffBuildOptions,
9
+ DiffBuildResult,
10
+ DiffBuilder,
11
+ DiffChunk,
12
+ DiffChunker,
13
+ DiffCoverage,
14
+ DiffCoverageReason,
15
+ DiffFile,
16
+ DiffHunk,
17
+ DiffLine,
18
+ DiffMode,
19
+ DiffSpec,
20
+ DiffStreamEvent,
21
+ DiffTotals,
22
+ FileChangeType,
23
+ NormalizedDiff,
24
+ } from './types.ts';
@@ -0,0 +1,134 @@
1
+ import type { FileChangeType } from './types.ts';
2
+
3
+ function trimToNull(value: string | null): string | null {
4
+ if (value === null) {
5
+ return null;
6
+ }
7
+ const trimmed = value.trim();
8
+ return trimmed.length === 0 ? null : trimmed;
9
+ }
10
+
11
+ export function normalizeDiffPath(value: string | null): string | null {
12
+ const trimmed = trimToNull(value);
13
+ if (trimmed === null || trimmed === '/dev/null') {
14
+ return null;
15
+ }
16
+ return trimmed;
17
+ }
18
+
19
+ export function inferLanguageFromPath(path: string | null): string | null {
20
+ if (path === null) {
21
+ return null;
22
+ }
23
+ const lower = path.toLowerCase();
24
+ if (lower.endsWith('.ts') || lower.endsWith('.tsx')) {
25
+ return 'typescript';
26
+ }
27
+ if (lower.endsWith('.js') || lower.endsWith('.jsx') || lower.endsWith('.mjs')) {
28
+ return 'javascript';
29
+ }
30
+ if (lower.endsWith('.rs')) {
31
+ return 'rust';
32
+ }
33
+ if (lower.endsWith('.go')) {
34
+ return 'go';
35
+ }
36
+ if (lower.endsWith('.py')) {
37
+ return 'python';
38
+ }
39
+ if (lower.endsWith('.java')) {
40
+ return 'java';
41
+ }
42
+ if (lower.endsWith('.kt')) {
43
+ return 'kotlin';
44
+ }
45
+ if (lower.endsWith('.rb')) {
46
+ return 'ruby';
47
+ }
48
+ if (lower.endsWith('.c') || lower.endsWith('.h')) {
49
+ return 'c';
50
+ }
51
+ if (lower.endsWith('.cc') || lower.endsWith('.cpp') || lower.endsWith('.hpp')) {
52
+ return 'cpp';
53
+ }
54
+ if (lower.endsWith('.json')) {
55
+ return 'json';
56
+ }
57
+ if (lower.endsWith('.jsonc')) {
58
+ return 'jsonc';
59
+ }
60
+ if (lower.endsWith('.md')) {
61
+ return 'markdown';
62
+ }
63
+ if (lower.endsWith('.yaml') || lower.endsWith('.yml')) {
64
+ return 'yaml';
65
+ }
66
+ if (lower.endsWith('.toml')) {
67
+ return 'toml';
68
+ }
69
+ if (lower.endsWith('.sh') || lower.endsWith('.bash') || lower.endsWith('.zsh')) {
70
+ return 'shell';
71
+ }
72
+ if (lower.endsWith('.sql')) {
73
+ return 'sql';
74
+ }
75
+ return null;
76
+ }
77
+
78
+ function hasGeneratedSegment(path: string): boolean {
79
+ const lower = path.toLowerCase();
80
+ return (
81
+ lower.startsWith('dist/') ||
82
+ lower.startsWith('build/') ||
83
+ lower.startsWith('coverage/') ||
84
+ lower.startsWith('.next/') ||
85
+ lower.startsWith('out/') ||
86
+ lower.startsWith('vendor/') ||
87
+ lower.startsWith('node_modules/') ||
88
+ lower.includes('/dist/') ||
89
+ lower.includes('/build/') ||
90
+ lower.includes('/coverage/') ||
91
+ lower.includes('/.next/') ||
92
+ lower.includes('/out/') ||
93
+ lower.includes('/vendor/') ||
94
+ lower.includes('/node_modules/')
95
+ );
96
+ }
97
+
98
+ export function isGeneratedPath(path: string | null): boolean {
99
+ if (path === null) {
100
+ return false;
101
+ }
102
+ const lower = path.toLowerCase();
103
+ return (
104
+ hasGeneratedSegment(path) ||
105
+ lower.endsWith('.min.js') ||
106
+ lower.endsWith('.min.css') ||
107
+ lower.endsWith('.lock') ||
108
+ lower === 'package-lock.json' ||
109
+ lower === 'bun.lock' ||
110
+ lower.endsWith('.generated.ts') ||
111
+ lower.endsWith('.gen.ts')
112
+ );
113
+ }
114
+
115
+ export function resolveFileChangeType(input: {
116
+ fromHeader: FileChangeType | null;
117
+ oldPath: string | null;
118
+ newPath: string | null;
119
+ isBinary: boolean;
120
+ }): FileChangeType {
121
+ if (input.isBinary) {
122
+ return 'binary';
123
+ }
124
+ if (input.fromHeader !== null && input.fromHeader !== 'unknown') {
125
+ return input.fromHeader;
126
+ }
127
+ if (input.oldPath === null && input.newPath !== null) {
128
+ return 'added';
129
+ }
130
+ if (input.oldPath !== null && input.newPath === null) {
131
+ return 'deleted';
132
+ }
133
+ return 'modified';
134
+ }