@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,136 @@
1
+ import type { DiffBudget, DiffCoverageReason } from './types.ts';
2
+
3
+ interface DiffBudgetUsage {
4
+ readonly files: number;
5
+ readonly hunks: number;
6
+ readonly lines: number;
7
+ readonly bytes: number;
8
+ }
9
+
10
+ interface MutableDiffBudgetUsage {
11
+ files: number;
12
+ hunks: number;
13
+ lines: number;
14
+ bytes: number;
15
+ }
16
+
17
+ interface DiffBudgetCheckpoint {
18
+ readonly allowed: boolean;
19
+ readonly reason: DiffCoverageReason;
20
+ }
21
+
22
+ function takeReason(current: DiffCoverageReason, next: DiffCoverageReason): DiffCoverageReason {
23
+ if (current !== 'none') {
24
+ return current;
25
+ }
26
+ return next;
27
+ }
28
+
29
+ export class DiffBudgetTracker {
30
+ private reason: DiffCoverageReason = 'none';
31
+ private readonly usageMutable: MutableDiffBudgetUsage = {
32
+ files: 0,
33
+ hunks: 0,
34
+ lines: 0,
35
+ bytes: 0,
36
+ };
37
+ private readonly startedAtMs: number;
38
+
39
+ constructor(
40
+ private readonly budget: DiffBudget,
41
+ nowMs = Date.now(),
42
+ ) {
43
+ this.startedAtMs = nowMs;
44
+ }
45
+
46
+ usage(): DiffBudgetUsage {
47
+ return {
48
+ files: this.usageMutable.files,
49
+ hunks: this.usageMutable.hunks,
50
+ lines: this.usageMutable.lines,
51
+ bytes: this.usageMutable.bytes,
52
+ };
53
+ }
54
+
55
+ limitReason(): DiffCoverageReason {
56
+ return this.reason;
57
+ }
58
+
59
+ elapsedMs(nowMs = Date.now()): number {
60
+ return Math.max(0, nowMs - this.startedAtMs);
61
+ }
62
+
63
+ checkRuntime(nowMs = Date.now()): DiffBudgetCheckpoint {
64
+ if (this.elapsedMs(nowMs) >= this.budget.maxRuntimeMs) {
65
+ this.reason = takeReason(this.reason, 'max-runtime-ms');
66
+ return {
67
+ allowed: false,
68
+ reason: this.reason,
69
+ };
70
+ }
71
+ return {
72
+ allowed: true,
73
+ reason: this.reason,
74
+ };
75
+ }
76
+
77
+ addBytes(value: number): DiffBudgetCheckpoint {
78
+ this.usageMutable.bytes += Math.max(0, Math.floor(value));
79
+ if (this.usageMutable.bytes > this.budget.maxBytes) {
80
+ this.reason = takeReason(this.reason, 'max-bytes');
81
+ return {
82
+ allowed: false,
83
+ reason: this.reason,
84
+ };
85
+ }
86
+ return {
87
+ allowed: true,
88
+ reason: this.reason,
89
+ };
90
+ }
91
+
92
+ takeFile(): DiffBudgetCheckpoint {
93
+ if (this.usageMutable.files >= this.budget.maxFiles) {
94
+ this.reason = takeReason(this.reason, 'max-files');
95
+ return {
96
+ allowed: false,
97
+ reason: this.reason,
98
+ };
99
+ }
100
+ this.usageMutable.files += 1;
101
+ return {
102
+ allowed: true,
103
+ reason: this.reason,
104
+ };
105
+ }
106
+
107
+ takeHunk(): DiffBudgetCheckpoint {
108
+ if (this.usageMutable.hunks >= this.budget.maxHunks) {
109
+ this.reason = takeReason(this.reason, 'max-hunks');
110
+ return {
111
+ allowed: false,
112
+ reason: this.reason,
113
+ };
114
+ }
115
+ this.usageMutable.hunks += 1;
116
+ return {
117
+ allowed: true,
118
+ reason: this.reason,
119
+ };
120
+ }
121
+
122
+ takeLine(): DiffBudgetCheckpoint {
123
+ if (this.usageMutable.lines >= this.budget.maxLines) {
124
+ this.reason = takeReason(this.reason, 'max-lines');
125
+ return {
126
+ allowed: false,
127
+ reason: this.reason,
128
+ };
129
+ }
130
+ this.usageMutable.lines += 1;
131
+ return {
132
+ allowed: true,
133
+ reason: this.reason,
134
+ };
135
+ }
136
+ }
@@ -0,0 +1,289 @@
1
+ import { DiffBudgetTracker } from './budget.ts';
2
+ import { computeDiffId } from './hash.ts';
3
+ import {
4
+ buildGitDiffArgs,
5
+ readGitDiffPreflight,
6
+ resolveRangeBaseRef,
7
+ streamGitLines,
8
+ type GitDiffInvocationOptions,
9
+ } from './git-invoke.ts';
10
+ import { GitDiffPatchParser } from './git-parse.ts';
11
+ import type {
12
+ DiffBuildOptions,
13
+ DiffBuildResult,
14
+ DiffBuilder,
15
+ DiffCoverage,
16
+ DiffCoverageReason,
17
+ DiffStreamEvent,
18
+ NormalizedDiff,
19
+ } from './types.ts';
20
+
21
+ interface BuildHooks {
22
+ emit(event: DiffStreamEvent): void;
23
+ }
24
+
25
+ interface NormalizedBuildOptions {
26
+ readonly cwd: string;
27
+ readonly mode: DiffBuildOptions['mode'];
28
+ readonly baseRef: string | null;
29
+ readonly headRef: string | null;
30
+ readonly includeGenerated: boolean;
31
+ readonly includeBinary: boolean;
32
+ readonly noRenames: boolean;
33
+ readonly renameLimit: number | null;
34
+ readonly budget: DiffBuildOptions['budget'];
35
+ }
36
+
37
+ function normalizeOptions(options: DiffBuildOptions): NormalizedBuildOptions {
38
+ return {
39
+ cwd: options.cwd,
40
+ mode: options.mode,
41
+ baseRef: options.baseRef ?? null,
42
+ headRef: options.headRef ?? null,
43
+ includeGenerated: options.includeGenerated ?? false,
44
+ includeBinary: options.includeBinary ?? false,
45
+ noRenames: options.git?.noRenames ?? true,
46
+ renameLimit:
47
+ typeof options.git?.renameLimit === 'number' && Number.isFinite(options.git.renameLimit)
48
+ ? Math.max(0, Math.floor(options.git.renameLimit))
49
+ : null,
50
+ budget: options.budget,
51
+ };
52
+ }
53
+
54
+ function toInvocationOptions(options: NormalizedBuildOptions): GitDiffInvocationOptions {
55
+ return {
56
+ cwd: options.cwd,
57
+ mode: options.mode,
58
+ baseRef: options.baseRef,
59
+ headRef: options.headRef,
60
+ noRenames: options.noRenames,
61
+ renameLimit: options.renameLimit,
62
+ };
63
+ }
64
+
65
+ async function resolveRangeRefs(options: NormalizedBuildOptions): Promise<{
66
+ readonly baseRef: string | null;
67
+ readonly headRef: string | null;
68
+ }> {
69
+ if (options.mode !== 'range') {
70
+ return {
71
+ baseRef: options.baseRef,
72
+ headRef: options.headRef,
73
+ };
74
+ }
75
+
76
+ const headRef = options.headRef ?? 'HEAD';
77
+ if (options.baseRef !== null) {
78
+ return {
79
+ baseRef: options.baseRef,
80
+ headRef,
81
+ };
82
+ }
83
+
84
+ const baseRef = await resolveRangeBaseRef({
85
+ cwd: options.cwd,
86
+ headRef,
87
+ timeoutMs: options.budget.maxRuntimeMs,
88
+ });
89
+ return {
90
+ baseRef,
91
+ headRef,
92
+ };
93
+ }
94
+
95
+ function resolveCoverage(input: {
96
+ filesObservedInPreflight: number;
97
+ filesIncluded: number;
98
+ filesSkippedByFilter: number;
99
+ filesTruncatedByBudget: number;
100
+ limitReason: DiffCoverageReason;
101
+ }): DiffCoverage {
102
+ const remainder = Math.max(
103
+ 0,
104
+ input.filesObservedInPreflight - input.filesIncluded - input.filesSkippedByFilter,
105
+ );
106
+ const truncatedByLimit = input.limitReason !== 'none' ? remainder : 0;
107
+ const truncatedFiles = Math.max(input.filesTruncatedByBudget, truncatedByLimit);
108
+ const skippedFiles = Math.max(
109
+ input.filesSkippedByFilter,
110
+ input.filesObservedInPreflight - input.filesIncluded - truncatedFiles,
111
+ );
112
+ return {
113
+ complete: input.limitReason === 'none' && truncatedFiles === 0,
114
+ truncated: input.limitReason !== 'none' || truncatedFiles > 0,
115
+ skippedFiles,
116
+ truncatedFiles,
117
+ reason: input.limitReason,
118
+ };
119
+ }
120
+
121
+ async function runBuild(
122
+ rawOptions: DiffBuildOptions,
123
+ hooks?: BuildHooks,
124
+ ): Promise<DiffBuildResult> {
125
+ const options = normalizeOptions(rawOptions);
126
+ const startedAtMs = Date.now();
127
+ hooks?.emit({
128
+ type: 'start',
129
+ mode: options.mode,
130
+ });
131
+ const resolvedRefs = await resolveRangeRefs(options);
132
+ const invocation = toInvocationOptions({
133
+ ...options,
134
+ baseRef: resolvedRefs.baseRef,
135
+ headRef: resolvedRefs.headRef,
136
+ });
137
+ const preflight = await readGitDiffPreflight(invocation, options.budget.maxRuntimeMs);
138
+
139
+ const budget = new DiffBudgetTracker(options.budget, startedAtMs);
140
+ const parser = new GitDiffPatchParser({
141
+ includeGenerated: options.includeGenerated,
142
+ includeBinary: options.includeBinary,
143
+ budget,
144
+ onHunk: (fileId, hunk) => {
145
+ hooks?.emit({
146
+ type: 'hunk',
147
+ fileId,
148
+ hunk,
149
+ });
150
+ const usage = budget.usage();
151
+ hooks?.emit({
152
+ type: 'progress',
153
+ files: usage.files,
154
+ hunks: usage.hunks,
155
+ lines: usage.lines,
156
+ });
157
+ },
158
+ onFile: (file) => {
159
+ hooks?.emit({
160
+ type: 'file',
161
+ file,
162
+ });
163
+ const usage = budget.usage();
164
+ hooks?.emit({
165
+ type: 'progress',
166
+ files: usage.files,
167
+ hunks: usage.hunks,
168
+ lines: usage.lines,
169
+ });
170
+ },
171
+ });
172
+
173
+ const patchResult = await streamGitLines({
174
+ cwd: options.cwd,
175
+ args: buildGitDiffArgs(invocation, 'patch'),
176
+ timeoutMs: options.budget.maxRuntimeMs,
177
+ onBytes: (bytes) => budget.addBytes(bytes).allowed,
178
+ onLine: (line) => parser.pushLine(line),
179
+ });
180
+ if (patchResult.exitCode !== 0 && !patchResult.aborted)
181
+ throw new Error(`git diff --patch failed: ${patchResult.stderr || 'unknown error'}`);
182
+
183
+ const parsed = parser.finish();
184
+ const limitReason: DiffCoverageReason =
185
+ parsed.limitReason === 'none' && patchResult.timedOut ? 'max-runtime-ms' : parsed.limitReason;
186
+ const coverage = resolveCoverage({
187
+ filesObservedInPreflight: preflight.filesChanged,
188
+ filesIncluded: parsed.files.length,
189
+ filesSkippedByFilter: parsed.skippedFiles,
190
+ filesTruncatedByBudget: parsed.truncatedFiles,
191
+ limitReason,
192
+ });
193
+ const generatedAt = new Date().toISOString();
194
+ const diff: NormalizedDiff = {
195
+ spec: {
196
+ diffId: computeDiffId(options.mode, invocation.baseRef, invocation.headRef, parsed.files),
197
+ mode: options.mode,
198
+ baseRef: invocation.baseRef,
199
+ headRef: invocation.headRef,
200
+ generatedAt,
201
+ },
202
+ files: parsed.files,
203
+ totals: parsed.totals,
204
+ coverage,
205
+ };
206
+
207
+ const parseWarnings = [...parsed.warnings];
208
+ const expectedTotal = parsed.files.length + coverage.skippedFiles + coverage.truncatedFiles;
209
+ if (expectedTotal !== preflight.filesChanged) {
210
+ parseWarnings.push(
211
+ `preflight mismatch: filesChanged=${preflight.filesChanged} accounted=${expectedTotal}`,
212
+ );
213
+ }
214
+
215
+ hooks?.emit({
216
+ type: 'coverage',
217
+ coverage,
218
+ });
219
+ hooks?.emit({
220
+ type: 'complete',
221
+ diff,
222
+ });
223
+
224
+ return {
225
+ diff,
226
+ diagnostics: {
227
+ elapsedMs: Math.max(0, Date.now() - startedAtMs),
228
+ peakBufferBytes: patchResult.peakLineBufferBytes,
229
+ parseWarnings,
230
+ },
231
+ };
232
+ }
233
+
234
+ async function* streamDiff(options: DiffBuildOptions): AsyncIterable<DiffStreamEvent> {
235
+ const events: DiffStreamEvent[] = [];
236
+ let notifyNext: (() => void) | null = null;
237
+ let done = false;
238
+ let failure: unknown = null;
239
+
240
+ const notify = (): void => {
241
+ const callback = notifyNext;
242
+ notifyNext = null;
243
+ callback?.();
244
+ };
245
+
246
+ void runBuild(options, {
247
+ emit(event) {
248
+ events.push(event);
249
+ notify();
250
+ },
251
+ })
252
+ .then(() => {
253
+ done = true;
254
+ notify();
255
+ })
256
+ .catch((error: unknown) => {
257
+ failure = error;
258
+ done = true;
259
+ notify();
260
+ });
261
+
262
+ while (true) {
263
+ while (events.length > 0) {
264
+ const event = events.shift();
265
+ if (event !== undefined) {
266
+ yield event;
267
+ }
268
+ }
269
+
270
+ if (failure !== null) {
271
+ throw failure;
272
+ }
273
+
274
+ if (done) {
275
+ return;
276
+ }
277
+
278
+ await new Promise<void>((resolve) => {
279
+ notifyNext = resolve;
280
+ });
281
+ }
282
+ }
283
+
284
+ export function createDiffBuilder(): DiffBuilder {
285
+ return {
286
+ build: async (options: DiffBuildOptions): Promise<DiffBuildResult> => await runBuild(options),
287
+ stream: streamDiff,
288
+ };
289
+ }
@@ -0,0 +1,146 @@
1
+ import { computeDiffChunkId } from './hash.ts';
2
+ import type { ChunkPolicy, DiffChunk, DiffChunker, DiffHunk, NormalizedDiff } from './types.ts';
3
+
4
+ function normalizeChunkPolicy(policy: ChunkPolicy): ChunkPolicy {
5
+ return {
6
+ maxHunksPerChunk: Math.max(1, Math.floor(policy.maxHunksPerChunk)),
7
+ maxLinesPerChunk: Math.max(1, Math.floor(policy.maxLinesPerChunk)),
8
+ maxApproxTokensPerChunk: Math.max(1, Math.floor(policy.maxApproxTokensPerChunk)),
9
+ };
10
+ }
11
+
12
+ function hunkApproxBytes(hunk: DiffHunk): number {
13
+ let bytes = Buffer.byteLength(hunk.header);
14
+ for (const line of hunk.lines) {
15
+ bytes += Buffer.byteLength(line.text) + 1;
16
+ }
17
+ return bytes;
18
+ }
19
+
20
+ function approxTokensFromBytes(bytes: number): number {
21
+ return Math.max(1, Math.ceil(bytes / 4));
22
+ }
23
+
24
+ function policySignature(policy: ChunkPolicy): string {
25
+ return `${policy.maxHunksPerChunk}:${policy.maxLinesPerChunk}:${policy.maxApproxTokensPerChunk}`;
26
+ }
27
+
28
+ function finalizeChunk(
29
+ fileId: string,
30
+ path: string,
31
+ sequence: number,
32
+ totalForFile: number,
33
+ hunks: readonly DiffHunk[],
34
+ policySig: string,
35
+ ): DiffChunk {
36
+ const hunkIds: string[] = [];
37
+ for (const hunk of hunks) {
38
+ hunkIds.push(hunk.hunkId);
39
+ }
40
+ let approxBytes = Buffer.byteLength(path);
41
+ for (const hunk of hunks) {
42
+ approxBytes += hunkApproxBytes(hunk);
43
+ }
44
+ return {
45
+ chunkId: computeDiffChunkId(fileId, sequence, hunkIds, policySig),
46
+ fileId,
47
+ path,
48
+ sequence,
49
+ totalForFile,
50
+ hunkIds,
51
+ approxTokens: approxTokensFromBytes(approxBytes),
52
+ approxBytes,
53
+ payload: {
54
+ fileHeader: path,
55
+ hunks,
56
+ },
57
+ };
58
+ }
59
+
60
+ function chunkSingleFile(
61
+ fileId: string,
62
+ path: string,
63
+ hunks: readonly DiffHunk[],
64
+ policy: ChunkPolicy,
65
+ ): readonly DiffChunk[] {
66
+ const policySig = policySignature(policy);
67
+ const chunksHunks: DiffHunk[][] = [];
68
+ let current: DiffHunk[] = [];
69
+ let currentLines = 0;
70
+ let currentApproxTokens = 0;
71
+
72
+ for (const hunk of hunks) {
73
+ const hunkLines = hunk.lineCount;
74
+ const hunkTokens = approxTokensFromBytes(hunkApproxBytes(hunk));
75
+ const wouldExceed =
76
+ current.length >= policy.maxHunksPerChunk ||
77
+ currentLines + hunkLines > policy.maxLinesPerChunk ||
78
+ currentApproxTokens + hunkTokens > policy.maxApproxTokensPerChunk;
79
+ if (wouldExceed && current.length > 0) {
80
+ chunksHunks.push(current);
81
+ current = [];
82
+ currentLines = 0;
83
+ currentApproxTokens = 0;
84
+ }
85
+ current.push(hunk);
86
+ currentLines += hunkLines;
87
+ currentApproxTokens += hunkTokens;
88
+ const mustFlush =
89
+ current.length >= policy.maxHunksPerChunk ||
90
+ currentLines >= policy.maxLinesPerChunk ||
91
+ currentApproxTokens >= policy.maxApproxTokensPerChunk;
92
+ if (mustFlush) {
93
+ chunksHunks.push(current);
94
+ current = [];
95
+ currentLines = 0;
96
+ currentApproxTokens = 0;
97
+ }
98
+ }
99
+ if (current.length > 0) {
100
+ chunksHunks.push(current);
101
+ }
102
+
103
+ const totalForFile = chunksHunks.length;
104
+ const chunks: DiffChunk[] = [];
105
+ for (let index = 0; index < chunksHunks.length; index += 1) {
106
+ const hunkChunk = chunksHunks[index]!;
107
+ chunks.push(finalizeChunk(fileId, path, index + 1, totalForFile, hunkChunk, policySig));
108
+ }
109
+ return chunks;
110
+ }
111
+
112
+ function chunkDiff(diff: NormalizedDiff, policy: ChunkPolicy): readonly DiffChunk[] {
113
+ const normalizedPolicy = normalizeChunkPolicy(policy);
114
+ const chunks: DiffChunk[] = [];
115
+ for (const file of diff.files) {
116
+ if (file.isBinary || file.hunks.length === 0) {
117
+ continue;
118
+ }
119
+ const path = file.newPath ?? file.oldPath ?? file.fileId;
120
+ chunks.push(...chunkSingleFile(file.fileId, path, file.hunks, normalizedPolicy));
121
+ }
122
+ return chunks;
123
+ }
124
+
125
+ async function* streamDiffChunks(
126
+ diff: NormalizedDiff,
127
+ policy: ChunkPolicy,
128
+ ): AsyncIterable<DiffChunk> {
129
+ const normalizedPolicy = normalizeChunkPolicy(policy);
130
+ for (const file of diff.files) {
131
+ if (file.isBinary || file.hunks.length === 0) {
132
+ continue;
133
+ }
134
+ const path = file.newPath ?? file.oldPath ?? file.fileId;
135
+ for (const chunk of chunkSingleFile(file.fileId, path, file.hunks, normalizedPolicy)) {
136
+ yield chunk;
137
+ }
138
+ }
139
+ }
140
+
141
+ export function createDiffChunker(): DiffChunker {
142
+ return {
143
+ chunk: chunkDiff,
144
+ streamChunks: streamDiffChunks,
145
+ };
146
+ }