@dev.fast/whiteboard 0.2.3 → 0.2.4

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 (34) hide show
  1. package/dist/{account-alias-DLE9XZhi.js → account-alias-_1_7vROx.js} +1 -1
  2. package/dist/agent-cli-BR2j-2n5.js +2 -0
  3. package/dist/{agent-cli-DRRQfHQ-.js → agent-cli-BrFqhnzM.js} +6 -6
  4. package/dist/{agent-client-DoaODH6I.js → agent-client-B_b1GtUc.js} +2 -2
  5. package/dist/{authoring-tools-F_q8cC7J.js → authoring-tools-DvIO906l.js} +2 -2
  6. package/dist/build-info.json +1 -1
  7. package/dist/{cli-DtolttUF.js → cli-gRGxkwc0.js} +1 -1
  8. package/dist/{cli-runner-Cwg5PeH2.js → cli-runner-Cjl8h376.js} +11 -11
  9. package/dist/cli.js +2 -2
  10. package/dist/{client-D01H2KOV.js → client-BFClfQLO.js} +2 -2
  11. package/dist/{desktop-discovery-DK2HoBy5.js → desktop-discovery-C-Qz54wO.js} +1 -1
  12. package/dist/{headless-host-Dd_-zkC4.js → headless-host-DcWaxx9P.js} +2 -2
  13. package/dist/{local-data-D-EoJfPx.js → local-data-Bm84Xdck.js} +72 -78
  14. package/dist/{mcp-BAE5rMgB.js → mcp-BDy9Aybd.js} +3 -3
  15. package/dist/{process-error-telemetry-mzRG988B.js → process-error-telemetry-Dzh5a3s_.js} +23 -17
  16. package/dist/profile-BQGaX8aX.js +2 -0
  17. package/dist/{profile-WzZu7Spt.js → profile-RDEioF8b.js} +1 -1
  18. package/dist/{request-origin-DlHTFCrQ.js → request-origin-DA4K1qWY.js} +1 -1
  19. package/dist/{review-telemetry-Pp4ZsVrh.js → review-telemetry-DBy3eVCo.js} +2 -2
  20. package/dist/server/desktop-host.js +10 -10
  21. package/dist/sharing/index.d.ts +13 -12
  22. package/dist/sharing/index.js +1 -1
  23. package/dist/{src-DHiDJQDy.js → src-WCbabvdS.js} +3 -2
  24. package/dist/{stored-document-migration-T9Uj5MkN.js → stored-document-migration-BxZWQQjv.js} +1 -1
  25. package/dist/{tool-failure-Djeeax6q.js → tool-failure-DJmTieh5.js} +1 -1
  26. package/package.json +5 -5
  27. package/src/review-api/comparison-coverage.ts +192 -165
  28. package/src/review-api/http.ts +11 -0
  29. package/src/review-api/local-data.ts +9 -3
  30. package/src/review-api/review-progress.ts +19 -9
  31. package/src/server/diffr-config.ts +1 -1
  32. package/src/server/structural-diff.ts +10 -9
  33. package/dist/agent-cli-50KJgMKi.js +0 -2
  34. package/dist/profile-Cqztt-9-.js +0 -2
@@ -1,20 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as diffFileSummaries, b as resolveRevision, i as diff, x as splitGitPatchFiles } from "../src-CmdiBL20.js";
3
3
  import { c as jsonString, i as jsonBoolean, n as isJsonObject, p as isObjectValue, s as jsonProperty, u as parseJsonText } from "../src-X9phtB2j.js";
4
- import { i as SharedReviewStore } from "../client-D01H2KOV.js";
5
- import { c as ReviewBugReportRequestSchema, l as parseReviewBugReportResponse, o as parseZod, s as ReviewBugReportMetaV2Schema, t as parseReviewCliInstallApplyRequest, v as reviewDiffrSummarizerInputSchema } from "../src-DHiDJQDy.js";
6
- import { C as resourceReferences, p as documentSchema } from "../stored-document-migration-T9Uj5MkN.js";
4
+ import { i as SharedReviewStore } from "../client-BFClfQLO.js";
5
+ import { c as ReviewBugReportRequestSchema, l as parseReviewBugReportResponse, o as parseZod, s as ReviewBugReportMetaV2Schema, t as parseReviewCliInstallApplyRequest, v as reviewDiffrSummarizerInputSchema } from "../src-WCbabvdS.js";
6
+ import { C as resourceReferences, p as documentSchema } from "../stored-document-migration-BxZWQQjv.js";
7
7
  import { t as ReviewInputError } from "../input-error-OLgB_h31.js";
8
8
  import { A as devReviewHome, P as writePrivateJsonAtomic } from "../s3-config-CAxhO9u_.js";
9
9
  import { U as shellQuote, i as reviewInstanceDiscoveryPath, o as reviewLegacyDiscoveryPath } from "../review-home-paths-6zZH-1c9.js";
10
10
  import { i as readReviewPackageVersion, t as findReviewPackageRoot } from "../package-paths-B6-zxvIO.js";
11
- import { c as reviewInstanceIdentity, t as REVIEW_INSTANCE_ENV } from "../desktop-discovery-DK2HoBy5.js";
12
- import { t as openReviewProfile } from "../profile-WzZu7Spt.js";
13
- import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-Pp4ZsVrh.js";
14
- import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-DlHTFCrQ.js";
15
- import { c as removeLegacyReviewSkills, d as skipCliInstall, i as finishCliInstallUpdate, l as resetCliInstall, n as applyCliInstall, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-DLE9XZhi.js";
16
- import { _ as setDiffrConfigValue, a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, g as saveDiffrSummarizer, h as readDiffrConfig, i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson, v as testDiffrSummarizer } from "../process-error-telemetry-mzRG988B.js";
17
- import { c as readQuerySchemas } from "../authoring-tools-F_q8cC7J.js";
11
+ import { c as reviewInstanceIdentity, t as REVIEW_INSTANCE_ENV } from "../desktop-discovery-C-Qz54wO.js";
12
+ import { t as openReviewProfile } from "../profile-RDEioF8b.js";
13
+ import { r as reviewTelemetryChannel, t as ReviewTelemetry } from "../review-telemetry-DBy3eVCo.js";
14
+ import { o as REVIEW_TELEMETRY_TABS } from "../request-origin-DA4K1qWY.js";
15
+ import { c as removeLegacyReviewSkills, d as skipCliInstall, i as finishCliInstallUpdate, l as resetCliInstall, n as applyCliInstall, r as declineCliInstall, s as removeCliInstall, t as aliasInstallationToAccount, u as resolveCliInstallStatus } from "../account-alias-_1_7vROx.js";
16
+ import { _ as setDiffrConfigValue, a as serverJson, c as recordClientError, d as HttpJsonError, f as ReviewServerError, g as saveDiffrSummarizer, h as readDiffrConfig, i as createWhiteboardCore, l as createNodeRequestListener, m as migrateDiffrConfig, n as installProcessErrorTelemetry, o as captureSanitizedUiTelemetry, p as GlobalReviewDesktopVerbRelay, r as serverCrashReportPath, s as clientErrorsForSession, t as drainServerCrashReport, u as readBoundedRequestJson, v as testDiffrSummarizer } from "../process-error-telemetry-Dzh5a3s_.js";
17
+ import { c as readQuerySchemas } from "../authoring-tools-DvIO906l.js";
18
18
  import crypto, { createHash, randomBytes, randomUUID } from "node:crypto";
19
19
  import { z } from "zod";
20
20
  import fs, { existsSync } from "node:fs";
@@ -67,7 +67,7 @@ interface LocalVcsCommitSummary {
67
67
  deletions: number;
68
68
  }
69
69
  //#endregion
70
- //#region ../../node_modules/@dev.fast/diffr/dist/contract.d.ts
70
+ //#region ../../diffr/diffr-ts/dist/contract.d.ts
71
71
  /** A pairing always has at least one side; additions/deletions omit the other. */
72
72
  type StructuralPairing<T> = {
73
73
  lhs: T;
@@ -171,6 +171,8 @@ type StructuralRegion = {
171
171
  type StructuralNode = {
172
172
  kind: "leaf";
173
173
  alignment_id: number;
174
+ /** The partner leaf's `id` on the other side; absent when added or deleted. */
175
+ pair?: number;
174
176
  changed?: StructuralSpan[];
175
177
  } | {
176
178
  kind: "fold";
@@ -310,7 +312,7 @@ declare const fileLineRangeSchema: z.ZodObject<{
310
312
  type FileLineRange = z.infer<typeof fileLineRangeSchema>;
311
313
  //#endregion
312
314
  //#region src/lens-selection.d.ts
313
- type AlignmentRow$1 = readonly [number | null, number | null];
315
+ type AlignmentRow = readonly [number | null, number | null];
314
316
  //#endregion
315
317
  //#region src/viewed-coverage.d.ts
316
318
  /** Zero-based, half-open intervals; always normalized at the storage boundary. */
@@ -319,7 +321,7 @@ interface Coverage {
319
321
  base: LineInterval[];
320
322
  head: LineInterval[];
321
323
  }
322
- interface CoverageFile$1 {
324
+ interface CoverageFile {
323
325
  path: string;
324
326
  previousPath?: string;
325
327
  fingerprint: string;
@@ -1038,9 +1040,11 @@ interface WrittenComponent {
1038
1040
  //#region src/review-api/comparison-coverage.d.ts
1039
1041
  type CoverageMode = "structural" | "textual";
1040
1042
  interface ComparisonCoverage {
1041
- files: CoverageFile$1[];
1043
+ /** All Git changes are counted, even if folding is still loading. */
1044
+ counted: boolean;
1045
+ files: CoverageFile[];
1042
1046
  fileSources: Map<string, FileLineRange[]>;
1043
- alignments: Map<string, readonly AlignmentRow$1[]>;
1047
+ alignments: Map<string, readonly AlignmentRow[]>;
1044
1048
  }
1045
1049
  //#endregion
1046
1050
  //#region src/review-api/pull-request.d.ts
@@ -1694,6 +1698,7 @@ declare class LocalReviewData {
1694
1698
  coverageSnapshot(reviewId: string, pins: Pins, mode: CoverageMode): {
1695
1699
  pending: boolean;
1696
1700
  comparison: ComparisonCoverage | {
1701
+ counted: false;
1697
1702
  files: never[];
1698
1703
  fileSources: Map<any, any>;
1699
1704
  alignments: Map<any, any>;
@@ -1701,14 +1706,10 @@ declare class LocalReviewData {
1701
1706
  };
1702
1707
  private coverageNotification;
1703
1708
  private notifyCoverage;
1704
- coverage(reviewId: string, pins: Pins, mode: CoverageMode): Promise<{
1705
- files: CoverageFile[];
1706
- fileSources: Map<string, FileLineRange[]>;
1707
- alignments: Map<string, readonly AlignmentRow[]>;
1708
- }>;
1709
+ coverage(reviewId: string, pins: Pins, mode: CoverageMode): Promise<ComparisonCoverage>;
1709
1710
  changes(pins: Pins): Promise<LocalVcsDiffFileSummary[]>;
1710
- changes(pins: Pins, file: string): Promise<string>;
1711
- changes(pins: Pins, file?: string): Promise<LocalVcsDiffFileSummary[] | string>;
1711
+ changes(pins: Pins, file: string, previousPath?: string): Promise<string>;
1712
+ changes(pins: Pins, file?: string, previousPath?: string): Promise<LocalVcsDiffFileSummary[] | string>;
1712
1713
  private summaries;
1713
1714
  /** Raw Git patch text for exact filenames, or every change. */
1714
1715
  private rawPatch;
@@ -1,2 +1,2 @@
1
- import { a as sharedReviewId, c as verifyShareRepository, i as SharedReviewStore, l as exportShare, n as ShareClient, o as validateShareBundle } from "../client-D01H2KOV.js";
1
+ import { a as sharedReviewId, c as verifyShareRepository, i as SharedReviewStore, l as exportShare, n as ShareClient, o as validateShareBundle } from "../client-BFClfQLO.js";
2
2
  export { ShareClient, SharedReviewStore, exportShare, sharedReviewId, validateShareBundle, verifyShareRepository };
@@ -1,7 +1,7 @@
1
1
  import { n as isJsonObject } from "./src-X9phtB2j.js";
2
2
  import { _ as ReviewAgentTraceEventSchema, v as ReviewAgentTraceSessionSchema } from "./src-CGP5ytbV.js";
3
3
  import { z } from "zod";
4
- //#region ../../node_modules/@dev.fast/diffr/dist/contract.js
4
+ //#region ../../diffr/diffr-ts/dist/contract.js
5
5
  /**
6
6
  * Mirrors crates/diffr-core/src/protocol/mod.rs and crates/diffr-core/src/pairing.rs. See ../README.md for sync checks.
7
7
  * Import only zod: Review embeds this file in generated source.
@@ -128,6 +128,7 @@ const StructuralRegionSchema = z.lazy(() => z.object({
128
128
  const StructuralNodeSchema = z.lazy(() => z.union([z.object({
129
129
  kind: z.literal("leaf"),
130
130
  alignment_id: structuralU32,
131
+ pair: structuralU32.optional(),
131
132
  changed: z.array(StructuralSpanSchema).optional()
132
133
  }), z.object({
133
134
  kind: z.literal("fold"),
@@ -158,7 +159,7 @@ const StructuralLineCountsSchema = z.lazy(() => z.object({
158
159
  removed: structuralU32
159
160
  }));
160
161
  //#endregion
161
- //#region ../../node_modules/@dev.fast/diffr/dist/stream.js
162
+ //#region ../../diffr/diffr-ts/dist/stream.js
162
163
  function decodeStructuralDiffEvent(line) {
163
164
  try {
164
165
  return StructuralDiffEventSchema.parse(JSON.parse(line));
@@ -1,5 +1,5 @@
1
1
  import { f as isNumberValue, l as jsonValueSchema, m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
- import "./src-DHiDJQDy.js";
2
+ import "./src-WCbabvdS.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
4
  import { z } from "zod";
5
5
  import { fromMarkdown } from "mdast-util-from-markdown";
@@ -1,5 +1,5 @@
1
1
  import { m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
- import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-DoaODH6I.js";
2
+ import { n as callAuthoringTool, s as ReviewApiError, t as ToolText } from "./agent-client-B_b1GtUc.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
4
  //#region src/review-api/public-tools.ts
5
5
  function publicDescription(text) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dev.fast/whiteboard",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,7 +43,6 @@
43
43
  "@agentclientprotocol/claude-agent-acp": "0.84.0",
44
44
  "@agentclientprotocol/codex-acp": "2.0.0",
45
45
  "@agentclientprotocol/sdk": "1.5.1",
46
- "@dev.fast/diffr": "0.1.15",
47
46
  "@hono/node-server": "2.0.12",
48
47
  "@img/sharp-wasm32": "0.35.5",
49
48
  "@modelcontextprotocol/sdk": "1.31.0",
@@ -59,7 +58,8 @@
59
58
  "semver": "7.8.5",
60
59
  "sharp": "0.35.5",
61
60
  "smol-toml": "1.9.0",
62
- "zod": "4.6.5"
61
+ "zod": "4.6.5",
62
+ "@dev.fast/diffr": "0.1.17"
63
63
  },
64
64
  "devDependencies": {
65
65
  "@dev.fast/local-vcs": "^0.1.0",
@@ -85,14 +85,14 @@
85
85
  "repo": "devdotfast/review",
86
86
  "docs": "packages/review/instructions/authoring.md"
87
87
  },
88
- "gitHead": "29a00b6b1c647529bfe19e0571cae2336a44356b",
88
+ "gitHead": "5b00f0e66d034e187a29d270b3690a37b57ccf7b",
89
89
  "scripts": {
90
90
  "prebuild": "pnpm run build:workspace-deps",
91
91
  "build": "node --max-old-space-size=8192 node_modules/tsdown/dist/run.mjs",
92
92
  "build:tutorial-assets": "tsx scripts/build-tutorial-assets.ts",
93
93
  "ensure:tutorial-assets": "tsx scripts/ensure-tutorial-assets.ts",
94
94
  "check:tutorial": "tsx scripts/check-tutorial.ts",
95
- "build:workspace-deps": "pnpm --filter @dev.fast/json --filter @dev.fast/local-vcs --filter @dev.fast/review-protocol --filter @dev.fast/review-share-protocol --filter @dev.fast/trace-protocol --filter @dev.fast/trace-core build",
95
+ "build:workspace-deps": "pnpm --filter @dev.fast/diffr --filter @dev.fast/json --filter @dev.fast/local-vcs --filter @dev.fast/review-protocol --filter @dev.fast/review-share-protocol --filter @dev.fast/trace-protocol --filter @dev.fast/trace-core build",
96
96
  "pretypecheck": "pnpm run ensure:tutorial-assets",
97
97
  "pretest": "pnpm run ensure:tutorial-assets",
98
98
  "pretest:node": "pnpm run ensure:tutorial-assets",
@@ -29,6 +29,8 @@ const hash = (parts: (string | null)[]) =>
29
29
  createHash("sha256").update(JSON.stringify(parts)).digest("hex");
30
30
 
31
31
  export interface ComparisonCoverage {
32
+ /** All Git changes are counted, even if folding is still loading. */
33
+ counted: boolean;
32
34
  files: CoverageFile[];
33
35
  fileSources: Map<string, FileLineRange[]>;
34
36
  alignments: Map<string, readonly AlignmentRow[]>;
@@ -42,196 +44,221 @@ export async function comparisonCoverage(
42
44
  mode: CoverageMode,
43
45
  signal: AbortSignal,
44
46
  publish?: (coverage: ComparisonCoverage) => void,
45
- ) {
47
+ ): Promise<ComparisonCoverage> {
46
48
  const fileSources = new Map<string, FileLineRange[]>();
47
49
  const alignments = new Map<string, readonly AlignmentRow[]>();
48
50
 
49
51
  const files: CoverageFile[] = [];
50
52
 
51
- if (mode === "textual") {
52
- for (const file of await data.changes(pins)) {
53
- signal.throwIfAborted();
54
- const patch = await data.changes(pins, file.path);
55
- const changed = emptyCoverage();
56
-
57
- for (const hunk of parseUnifiedPatch(file.path, patch))
58
- for (const line of hunk.lines) {
59
- if (line.kind === "add")
60
- changed.head.push([line.newLine! - 1, line.newLine!]);
61
-
62
- if (line.kind === "remove")
63
- changed.base.push([line.oldLine! - 1, line.oldLine!]);
64
- }
65
-
66
- changed.base = unionIntervals(changed.base);
67
- changed.head = unionIntervals(changed.head);
68
-
69
- const [base, head] = await Promise.all([
70
- file.status === "added"
71
- ? null
72
- : data
73
- .file(pins, "base", file.previousPath ?? file.path)
74
- .then((value) => value.text)
75
- .catch(() => null),
76
- file.status === "deleted"
77
- ? null
78
- : data
79
- .file(pins, "head", file.path)
80
- .then((value) => value.text)
81
- .catch(() => null),
82
- ]);
83
-
84
- alignments.set(
85
- file.path,
86
- textualRows(
87
- file.path,
88
- patch,
89
- base === null ? 0 : base.split("\n").length,
90
- head === null ? 0 : head.split("\n").length,
91
- ),
92
- );
53
+ for (const file of await data.changes(pins)) {
54
+ signal.throwIfAborted();
55
+ const patch = await data.changes(pins, file.path, file.previousPath);
56
+ const changed = emptyCoverage();
93
57
 
94
- const readable =
95
- (file.status === "added" || base !== null) &&
96
- (file.status === "deleted" || head !== null);
97
-
98
- const fingerprint = hash([
99
- pins.repositoryId,
100
- base,
101
- head,
102
- ...(readable ? [] : [pins.base, pins.head, patch]),
103
- ]);
104
-
105
- fileSources.set(file.path, [
106
- ...(base !== null
107
- ? [
108
- {
109
- side: "base" as const,
110
- file: file.previousPath ?? file.path,
111
- fromLine: 1,
112
- toLine: base.split("\n").length,
113
- },
114
- ]
115
- : []),
116
- ...(head !== null
117
- ? [
118
- {
119
- side: "head" as const,
120
- file: file.path,
121
- fromLine: 1,
122
- toLine: head.split("\n").length,
123
- },
124
- ]
125
- : []),
126
- ]);
127
- files.push({
128
- path: file.path,
129
- previousPath: file.previousPath,
130
- fingerprint,
131
- changed,
132
- viewed: emptyCoverage(),
133
- });
58
+ for (const hunk of parseUnifiedPatch(file.path, patch))
59
+ for (const line of hunk.lines) {
60
+ if (line.kind === "add")
61
+ changed.head.push([line.newLine! - 1, line.newLine!]);
62
+
63
+ if (line.kind === "remove")
64
+ changed.base.push([line.oldLine! - 1, line.oldLine!]);
65
+ }
66
+
67
+ changed.base = unionIntervals(changed.base);
68
+ changed.head = unionIntervals(changed.head);
69
+
70
+ const [base, head] = await Promise.all([
71
+ file.status === "added"
72
+ ? null
73
+ : data
74
+ .file(pins, "base", file.previousPath ?? file.path)
75
+ .then((value) => value.text)
76
+ .catch(() => null),
77
+ file.status === "deleted"
78
+ ? null
79
+ : data
80
+ .file(pins, "head", file.path)
81
+ .then((value) => value.text)
82
+ .catch(() => null),
83
+ ]);
84
+
85
+ alignments.set(
86
+ file.path,
87
+ textualRows(
88
+ file.path,
89
+ patch,
90
+ base === null ? 0 : base.split("\n").length,
91
+ head === null ? 0 : head.split("\n").length,
92
+ ),
93
+ );
94
+
95
+ const readable =
96
+ (file.status === "added" || base !== null) &&
97
+ (file.status === "deleted" || head !== null);
98
+
99
+ const fingerprint = hash([
100
+ pins.repositoryId,
101
+ base,
102
+ head,
103
+ ...(readable ? [] : [pins.base, pins.head, patch]),
104
+ ]);
105
+
106
+ fileSources.set(file.path, [
107
+ ...(base !== null
108
+ ? [
109
+ {
110
+ side: "base" as const,
111
+ file: file.previousPath ?? file.path,
112
+ fromLine: 1,
113
+ toLine: base.split("\n").length,
114
+ },
115
+ ]
116
+ : []),
117
+ ...(head !== null
118
+ ? [
119
+ {
120
+ side: "head" as const,
121
+ file: file.path,
122
+ fromLine: 1,
123
+ toLine: head.split("\n").length,
124
+ },
125
+ ]
126
+ : []),
127
+ ]);
128
+ files.push({
129
+ path: file.path,
130
+ previousPath: file.previousPath,
131
+ fingerprint,
132
+ changed,
133
+ viewed: emptyCoverage(),
134
+ });
135
+
136
+ if (mode === "textual")
134
137
  publish?.({
138
+ counted: false,
135
139
  files: [...files],
136
140
  fileSources: new Map(fileSources),
137
141
  alignments: new Map(alignments),
138
142
  });
139
- }
140
- } else {
141
- const remaining = new Set<string>();
142
-
143
- for await (const event of data.structuralChanges({
144
- reviewId,
145
- pins,
146
- signal,
147
- })) {
148
- if (event.type === "start") {
149
- for (const entry of event.files)
150
- remaining.add((entry.file.rhs ?? entry.file.lhs)!.path);
151
-
152
- if (!remaining.size) break;
153
- continue;
154
- }
143
+ }
155
144
 
156
- if (event.type === "complete" && (event.failed || event.aborted))
157
- throw new Error(
158
- event.aborted?.message ?? "Structural coverage is incomplete.",
159
- );
145
+ const coverage: ComparisonCoverage = {
146
+ files,
147
+ fileSources,
148
+ alignments,
149
+ counted: true,
150
+ };
160
151
 
161
- if (event.type !== "file") continue;
152
+ if (mode === "textual") return coverage;
162
153
 
163
- const path = (event.file.rhs ?? event.file.lhs)!.path;
154
+ // Keep Git's totals and file identities while structural results add folding.
155
+ const publishCurrent = () =>
156
+ publish?.({ ...coverage, files: [...coverage.files] });
164
157
 
165
- if (!remaining.delete(path))
166
- throw new Error(`Unexpected structural result: ${path}`);
158
+ publishCurrent();
167
159
 
168
- if (event.error) {
169
- if (event.error.code !== "unsupported_file_type")
170
- throw new Error(`Cannot count ${path}: ${event.error.message}`);
160
+ const baseFiles = new Map(
161
+ coverage.files.map((file, index) => [
162
+ file.previousPath ?? file.path,
163
+ index,
164
+ ]),
165
+ );
171
166
 
172
- if (!remaining.size) break;
173
- continue;
174
- }
167
+ const headFiles = new Map(
168
+ coverage.files.map((file, index) => [file.path, index]),
169
+ );
175
170
 
176
- const diff = event.diff;
171
+ const remaining = new Set<string>();
177
172
 
178
- const previousPath =
179
- event.file.lhs?.path !== path ? event.file.lhs?.path : undefined;
180
-
181
- const base = diff.type === "text" ? (diff.lhs?.text ?? null) : null;
182
- const head = diff.type === "text" ? (diff.rhs?.text ?? null) : null;
183
-
184
- const fingerprint = hash([
185
- pins.repositoryId,
186
- base,
187
- head,
188
- ...(diff.type === "binary"
189
- ? [event.file.lhs?.oid ?? null, event.file.rhs?.oid ?? null]
190
- : []),
191
- ]);
192
-
193
- const sources: FileLineRange[] = [];
194
-
195
- if (base !== null)
196
- sources.push({
197
- side: "base",
198
- file: event.file.lhs!.path,
199
- fromLine: 1,
200
- toLine: base.split("\n").length,
201
- });
202
-
203
- if (head !== null)
204
- sources.push({
205
- side: "head",
206
- file: path,
207
- fromLine: 1,
208
- toLine: head.split("\n").length,
209
- });
210
- fileSources.set(path, sources);
211
-
212
- if (diff.type === "text") alignments.set(path, structuralRows(diff));
213
- files.push({
214
- path,
215
- previousPath,
216
- fingerprint,
217
- changed:
218
- diff.type === "text" ? diff.structural_changes : emptyCoverage(),
219
- folded: foldedChanges(diff),
220
- viewed: emptyCoverage(),
221
- });
222
- publish?.({
223
- files: [...files],
224
- fileSources: new Map(fileSources),
225
- alignments: new Map(alignments),
226
- });
173
+ for await (const event of data.structuralChanges({
174
+ reviewId,
175
+ pins,
176
+ signal,
177
+ })) {
178
+ if (event.type === "start") {
179
+ for (const entry of event.files)
180
+ remaining.add((entry.file.rhs ?? entry.file.lhs)!.path);
227
181
 
228
182
  if (!remaining.size) break;
183
+ continue;
229
184
  }
230
185
 
231
- if (remaining.size) throw new Error("Structural coverage is incomplete.");
186
+ if (event.type === "complete" && (event.failed || event.aborted))
187
+ throw new Error(
188
+ event.aborted?.message ?? "Structural coverage is incomplete.",
189
+ );
190
+
191
+ if (event.type !== "file") continue;
192
+
193
+ const path = (event.file.rhs ?? event.file.lhs)!.path;
194
+
195
+ if (!remaining.delete(path))
196
+ throw new Error(`Unexpected structural result: ${path}`);
197
+
198
+ if (event.error) {
199
+ if (
200
+ event.error.code !== "unsupported_file_type" &&
201
+ event.error.code !== "not_utf8"
202
+ )
203
+ throw new Error(`Cannot count ${path}: ${event.error.message}`);
204
+ } else if (event.diff.type === "text") {
205
+ const diff = event.diff;
206
+ const folded = foldedChanges(diff);
207
+
208
+ // Match each side separately: diffr can split a Git rename into two files.
209
+ for (const side of ["base", "head"] as const) {
210
+ const source = side === "base" ? event.file.lhs : event.file.rhs;
211
+
212
+ const index =
213
+ source && (side === "base" ? baseFiles : headFiles).get(source.path);
214
+
215
+ if (index === undefined) continue;
216
+ const file = coverage.files[index];
217
+
218
+ const visible = subtractIntervals(
219
+ diff.structural_changes[side],
220
+ folded[side],
221
+ );
222
+
223
+ coverage.files[index] = {
224
+ ...file,
225
+ folded: {
226
+ ...(file.folded ?? emptyCoverage()),
227
+ [side]: subtractIntervals(file.changed[side], visible),
228
+ },
229
+ };
230
+ }
231
+
232
+ const baseIndex = event.file.lhs && baseFiles.get(event.file.lhs.path);
233
+ const headIndex = event.file.rhs && headFiles.get(event.file.rhs.path);
234
+ const index = headIndex ?? baseIndex;
235
+
236
+ if (index !== undefined) {
237
+ const sources =
238
+ coverage.fileSources.get(coverage.files[index].path) ?? [];
239
+
240
+ if (
241
+ sources.every(
242
+ (source) =>
243
+ source.file ===
244
+ (source.side === "base" ? event.file.lhs : event.file.rhs)?.path,
245
+ )
246
+ )
247
+ coverage.alignments = new Map(coverage.alignments).set(
248
+ coverage.files[index].path,
249
+ structuralRows(diff),
250
+ );
251
+ }
252
+
253
+ publishCurrent();
254
+ }
255
+
256
+ if (!remaining.size) break;
232
257
  }
233
258
 
234
- return { files, fileSources, alignments };
259
+ if (remaining.size) throw new Error("Structural coverage is incomplete.");
260
+
261
+ return coverage;
235
262
  }
236
263
 
237
264
  /**
@@ -389,12 +389,22 @@ export function createReviewApi(
389
389
 
390
390
  const snapshot = store.read(id);
391
391
 
392
+ const pins = snapshot.pins
393
+ ? (await data.resolveSource(snapshot)).pins
394
+ : undefined;
395
+
396
+ const coverage = pins && data.coverageSnapshot(id, pins, input.mode);
397
+
398
+ const partial =
399
+ coverage && coverage.pending ? coverage.comparison : undefined;
400
+
392
401
  const progress = await reviewProgress(
393
402
  store,
394
403
  data,
395
404
  snapshot,
396
405
  context.req.raw.signal,
397
406
  input.mode,
407
+ partial,
398
408
  );
399
409
 
400
410
  const files = input.files.map((update) => {
@@ -427,6 +437,7 @@ export function createReviewApi(
427
437
  store.read(id, input.version),
428
438
  context.req.raw.signal,
429
439
  input.mode,
440
+ partial,
430
441
  ),
431
442
  );
432
443
  });
@@ -1409,6 +1409,7 @@ export class LocalReviewData {
1409
1409
  return {
1410
1410
  pending,
1411
1411
  comparison: entry.partial ?? {
1412
+ counted: false,
1412
1413
  files: [],
1413
1414
  fileSources: new Map(),
1414
1415
  alignments: new Map(),
@@ -1496,16 +1497,21 @@ export class LocalReviewData {
1496
1497
  }
1497
1498
 
1498
1499
  changes(pins: Pins): Promise<LocalVcsDiffFileSummary[]>;
1499
- changes(pins: Pins, file: string): Promise<string>;
1500
+ changes(pins: Pins, file: string, previousPath?: string): Promise<string>;
1500
1501
  changes(
1501
1502
  pins: Pins,
1502
1503
  file?: string,
1504
+ previousPath?: string,
1503
1505
  ): Promise<LocalVcsDiffFileSummary[] | string>;
1504
- async changes(pins: Pins, file?: string) {
1506
+ async changes(pins: Pins, file?: string, previousPath?: string) {
1505
1507
  if (file === undefined) return this.summaries(pins);
1506
1508
  checkRelativePath(file);
1507
1509
 
1508
- return this.rawPatch(pins, { paths: [file] });
1510
+ if (previousPath) checkRelativePath(previousPath);
1511
+
1512
+ return this.rawPatch(pins, {
1513
+ paths: previousPath ? [previousPath, file] : [file],
1514
+ });
1509
1515
  }
1510
1516
  private async summaries(pins: Pins) {
1511
1517
  if (pins.worktreeRevision) {