@jmoyers/harness 0.1.10 → 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 (239) hide show
  1. package/README.md +31 -35
  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/{src/ui/modals/manager.ts → packages/harness-ui/src/modal-manager.ts} +94 -64
  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 -3721
  38. package/scripts/control-plane-daemon.ts +24 -2
  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 -3007
  43. package/scripts/nim-tui-smoke.ts +748 -0
  44. package/src/cli/auth/runtime.ts +948 -0
  45. package/src/cli/default-gateway-pointer.ts +193 -0
  46. package/src/cli/gateway/runtime.ts +1872 -0
  47. package/src/cli/parsing/flags.ts +23 -0
  48. package/src/cli/parsing/session.ts +42 -0
  49. package/src/cli/runtime/context.ts +193 -0
  50. package/src/cli/runtime-app/application.ts +392 -0
  51. package/src/cli/runtime-infra/gateway-control.ts +729 -0
  52. package/{scripts/harness-inspector.ts → src/cli/workflows/inspector.ts} +14 -11
  53. package/src/cli/workflows/runtime.ts +965 -0
  54. package/src/clients/tui/left-rail-interactions.ts +519 -0
  55. package/src/clients/tui/main-pane-interactions.ts +509 -0
  56. package/src/clients/tui/modal-input-routing.ts +71 -0
  57. package/src/clients/tui/render-snapshot-adapter.ts +88 -0
  58. package/src/clients/web/synced-selectors.ts +132 -0
  59. package/src/codex/live-session.ts +82 -29
  60. package/src/config/config-core.ts +361 -10
  61. package/src/config/harness-paths.ts +4 -7
  62. package/src/config/harness-runtime-migration.ts +142 -19
  63. package/src/config/harness.config.template.jsonc +33 -0
  64. package/src/config/secrets-core.ts +92 -4
  65. package/src/control-plane/agent-realtime-api.ts +82 -427
  66. package/src/control-plane/prompt/thread-title-namer.ts +49 -23
  67. package/src/control-plane/session-summary.ts +10 -81
  68. package/src/control-plane/status/reducer-base.ts +12 -12
  69. package/src/control-plane/status/reducers/claude-status-reducer.ts +3 -3
  70. package/src/control-plane/status/reducers/codex-status-reducer.ts +4 -4
  71. package/src/control-plane/status/reducers/cursor-status-reducer.ts +3 -3
  72. package/src/control-plane/stream-client.ts +12 -2
  73. package/src/control-plane/stream-command-parser.ts +83 -143
  74. package/src/control-plane/stream-protocol.ts +53 -37
  75. package/src/control-plane/stream-server-background.ts +18 -2
  76. package/src/control-plane/stream-server-command.ts +376 -69
  77. package/src/control-plane/stream-server-session-runtime.ts +3 -2
  78. package/src/control-plane/stream-server.ts +943 -80
  79. package/src/control-plane/stream-session-runtime-types.ts +41 -0
  80. package/src/{mux/live-mux/control-plane-records.ts → core/contracts/records.ts} +24 -97
  81. package/src/core/state/observed-stream-cursor.ts +43 -0
  82. package/src/core/state/synced-observed-state.ts +273 -0
  83. package/src/core/store/harness-synced-store.ts +81 -0
  84. package/src/diff/budget.ts +136 -0
  85. package/src/diff/build.ts +289 -0
  86. package/src/diff/chunker.ts +146 -0
  87. package/src/diff/git-invoke.ts +315 -0
  88. package/src/diff/git-parse.ts +472 -0
  89. package/src/diff/hash.ts +70 -0
  90. package/src/diff/index.ts +24 -0
  91. package/src/diff/normalize.ts +134 -0
  92. package/src/diff/types.ts +178 -0
  93. package/src/diff-ui/args.ts +346 -0
  94. package/src/diff-ui/commands.ts +123 -0
  95. package/src/diff-ui/finder.ts +94 -0
  96. package/src/diff-ui/highlight.ts +127 -0
  97. package/src/diff-ui/index.ts +2 -0
  98. package/src/diff-ui/model.ts +141 -0
  99. package/src/diff-ui/pager.ts +412 -0
  100. package/src/diff-ui/render.ts +337 -0
  101. package/src/diff-ui/runtime.ts +379 -0
  102. package/src/diff-ui/state.ts +224 -0
  103. package/src/diff-ui/types.ts +236 -0
  104. package/src/domain/conversations.ts +11 -7
  105. package/src/domain/workspace.ts +76 -4
  106. package/src/mux/control-plane-op-queue.ts +93 -7
  107. package/src/mux/conversation-rail.ts +28 -71
  108. package/src/mux/dual-pane-core.ts +13 -13
  109. package/src/mux/harness-core-ui.ts +313 -42
  110. package/src/mux/input-shortcuts.ts +22 -112
  111. package/src/mux/keybinding-catalog.ts +340 -0
  112. package/src/mux/keybinding-registry.ts +103 -0
  113. package/src/mux/live-mux/command-menu-open-in.ts +280 -0
  114. package/src/mux/live-mux/command-menu.ts +167 -4
  115. package/src/mux/live-mux/conversation-state.ts +13 -0
  116. package/src/mux/live-mux/directory-resolution.ts +1 -1
  117. package/src/mux/live-mux/git-parsing.ts +16 -0
  118. package/src/mux/live-mux/git-snapshot.ts +33 -2
  119. package/src/mux/live-mux/global-shortcut-handlers.ts +6 -0
  120. package/src/mux/live-mux/home-pane-drop.ts +1 -1
  121. package/src/mux/live-mux/home-pane-pointer.ts +10 -0
  122. package/src/mux/live-mux/input-forwarding.ts +59 -2
  123. package/src/mux/live-mux/left-nav-activation.ts +124 -7
  124. package/src/mux/live-mux/left-nav.ts +35 -0
  125. package/src/mux/live-mux/link-click.ts +292 -0
  126. package/src/mux/live-mux/modal-command-menu-handler.ts +46 -9
  127. package/src/mux/live-mux/modal-conversation-handlers.ts +5 -1
  128. package/src/mux/live-mux/modal-input-reducers.ts +106 -8
  129. package/src/mux/live-mux/modal-overlays.ts +210 -31
  130. package/src/mux/live-mux/modal-pointer.ts +3 -7
  131. package/src/mux/live-mux/modal-prompt-handlers.ts +107 -1
  132. package/src/mux/live-mux/modal-release-notes-handler.ts +111 -0
  133. package/src/mux/live-mux/modal-task-editor-handler.ts +16 -11
  134. package/src/mux/live-mux/pointer-routing.ts +5 -2
  135. package/src/mux/live-mux/project-pane-pointer.ts +8 -0
  136. package/src/mux/live-mux/rail-layout.ts +33 -30
  137. package/src/mux/live-mux/release-notes.ts +383 -0
  138. package/src/mux/live-mux/render-trace-analysis.ts +52 -7
  139. package/src/mux/live-mux/repository-folding.ts +3 -0
  140. package/src/mux/live-mux/selection.ts +0 -4
  141. package/src/mux/live-mux/session-diagnostics-paths.ts +21 -0
  142. package/src/mux/project-pane-github-review.ts +271 -0
  143. package/src/mux/render-frame.ts +4 -0
  144. package/src/mux/runtime-app/codex-live-mux-runtime.ts +5191 -0
  145. package/src/mux/task-composer.ts +21 -14
  146. package/src/mux/task-focused-pane.ts +118 -117
  147. package/src/mux/task-screen-keybindings.ts +19 -82
  148. package/src/mux/workspace-rail-model.ts +270 -104
  149. package/src/mux/workspace-rail.ts +45 -22
  150. package/src/pty/session-broker.ts +1 -1
  151. package/{scripts → src/recording}/terminal-recording-gif-lib.ts +2 -2
  152. package/src/services/control-plane.ts +50 -32
  153. package/src/services/conversation-lifecycle.ts +118 -87
  154. package/src/services/conversation-startup-hydration.ts +20 -12
  155. package/src/services/directory-hydration.ts +21 -16
  156. package/src/services/event-persistence.ts +7 -0
  157. package/src/services/left-rail-pointer-handler.ts +329 -0
  158. package/src/services/mux-ui-state-persistence.ts +5 -1
  159. package/src/services/recording.ts +34 -26
  160. package/src/services/runtime-command-menu-agent-tools.ts +1 -1
  161. package/src/services/runtime-control-actions.ts +79 -61
  162. package/src/services/runtime-control-plane-ops.ts +122 -83
  163. package/src/services/runtime-conversation-actions.ts +40 -26
  164. package/src/services/runtime-conversation-activation.ts +82 -30
  165. package/src/services/runtime-conversation-starter.ts +80 -48
  166. package/src/services/runtime-conversation-title-edit.ts +91 -80
  167. package/src/services/runtime-envelope-handler.ts +107 -105
  168. package/src/services/runtime-git-state.ts +42 -29
  169. package/src/services/runtime-layout-resize.ts +3 -1
  170. package/src/services/runtime-left-rail-render.ts +99 -63
  171. package/src/services/runtime-nim-cli-session.ts +438 -0
  172. package/src/services/runtime-nim-session.ts +705 -0
  173. package/src/services/runtime-nim-tool-bridge.ts +141 -0
  174. package/src/services/runtime-observed-event-projection-pipeline.ts +45 -0
  175. package/src/services/runtime-process-wiring.ts +29 -36
  176. package/src/services/runtime-project-pane-github-review-cache.ts +164 -0
  177. package/src/services/runtime-render-flush.ts +63 -70
  178. package/src/services/runtime-render-lifecycle.ts +65 -64
  179. package/src/services/runtime-render-orchestrator.ts +55 -45
  180. package/src/services/runtime-render-pipeline.ts +106 -103
  181. package/src/services/runtime-render-state.ts +62 -49
  182. package/src/services/runtime-repository-actions.ts +97 -70
  183. package/src/services/runtime-right-pane-render.ts +80 -53
  184. package/src/services/runtime-shutdown.ts +38 -35
  185. package/src/services/runtime-stream-subscriptions.ts +35 -27
  186. package/src/services/runtime-task-composer-persistence.ts +71 -59
  187. package/src/services/runtime-task-composer-snapshot.ts +14 -0
  188. package/src/services/runtime-task-editor-actions.ts +46 -29
  189. package/src/services/runtime-task-pane-actions.ts +220 -134
  190. package/src/services/runtime-task-pane-shortcuts.ts +323 -123
  191. package/src/services/runtime-workspace-observed-effect-queue.ts +25 -0
  192. package/src/services/runtime-workspace-observed-events.ts +33 -184
  193. package/src/services/runtime-workspace-observed-transition-policy.ts +228 -0
  194. package/src/services/session-diagnostics-store.ts +217 -0
  195. package/src/services/startup-background-resume.ts +26 -21
  196. package/src/services/startup-orchestrator.ts +16 -13
  197. package/src/services/startup-paint-tracker.ts +29 -21
  198. package/src/services/startup-persisted-conversation-queue.ts +19 -13
  199. package/src/services/startup-settled-gate.ts +25 -15
  200. package/src/services/startup-shutdown.ts +18 -22
  201. package/src/services/startup-state-hydration.ts +44 -34
  202. package/src/services/startup-visibility.ts +12 -4
  203. package/src/services/task-pane-selection-actions.ts +89 -72
  204. package/src/services/task-planning-hydration.ts +24 -18
  205. package/src/services/task-planning-observed-events.ts +50 -52
  206. package/src/services/workspace-observed-events.ts +66 -63
  207. package/src/storage/storage-lifecycle-core.ts +438 -0
  208. package/src/store/control-plane-store-normalize.ts +33 -242
  209. package/src/store/control-plane-store-types.ts +1 -35
  210. package/src/store/control-plane-store.ts +396 -56
  211. package/src/store/event-store.ts +397 -3
  212. package/src/terminal/snapshot-oracle.ts +207 -94
  213. package/src/ui/mux-theme.ts +112 -8
  214. package/src/ui/panes/home-gridfire.ts +40 -31
  215. package/src/ui/panes/home.ts +10 -2
  216. package/src/ui/panes/nim.ts +315 -0
  217. package/src/mux/live-mux/actions-task.ts +0 -115
  218. package/src/mux/live-mux/left-rail-actions.ts +0 -118
  219. package/src/mux/live-mux/left-rail-conversation-click.ts +0 -82
  220. package/src/mux/live-mux/left-rail-pointer.ts +0 -74
  221. package/src/mux/live-mux/task-pane-shortcuts.ts +0 -206
  222. package/src/services/runtime-directory-actions.ts +0 -164
  223. package/src/services/runtime-input-pipeline.ts +0 -50
  224. package/src/services/runtime-input-router.ts +0 -189
  225. package/src/services/runtime-main-pane-input.ts +0 -230
  226. package/src/services/runtime-modal-input.ts +0 -119
  227. package/src/services/runtime-navigation-input.ts +0 -197
  228. package/src/services/runtime-rail-input.ts +0 -278
  229. package/src/services/runtime-task-pane.ts +0 -62
  230. package/src/services/runtime-workspace-actions.ts +0 -158
  231. package/src/ui/conversation-input-forwarder.ts +0 -114
  232. package/src/ui/conversation-selection-input.ts +0 -103
  233. package/src/ui/global-shortcut-input.ts +0 -89
  234. package/src/ui/input.ts +0 -238
  235. package/src/ui/kit.ts +0 -509
  236. package/src/ui/left-nav-input.ts +0 -80
  237. package/src/ui/left-rail-pointer-input.ts +0 -148
  238. package/src/ui/repository-fold-input.ts +0 -91
  239. 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
+ }