Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/review/src/call-stack-diff.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { frameIdentity } from "./call-stack-frames";
import type { Frame } from "./review-api/document";
import { evidenceLocation } from "./source.js";

export type CallStackSide = "base" | "head";

Expand Down Expand Up @@ -121,7 +122,7 @@ export function callStackEvidenceErrors(

for (const row of rows) {
if (row.change === "unchanged") continue;
const { file, fromLine, toLine } = row.frame.source;
const { file, fromLine, toLine } = evidenceLocation(row.frame.source);
const side: CallStackSide = row.change === "removed" ? "base" : "head";
const lines = changedLines(file, side);
const relevant = row.change === "removed" ? lines?.deleted : lines?.added;
Expand Down
7 changes: 4 additions & 3 deletions packages/review/src/call-stack-frames.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type CallStackEntry, isCallsAssertion } from "./authoring";
import type { Frame } from "./review-api/document";
import { evidenceLocation } from "./source.js";

/** Legacy call stacks list anchors and `calls()` hops; the document stores
* canonical frames. The anchor id doubles as the matching key, so a frame
Expand Down Expand Up @@ -27,7 +28,7 @@ export function callStackFrames(entries: readonly CallStackEntry[]): Frame[] {
export function frameIdentity(frame: Frame): string {
return (
frame.key ??
`${frame.source.file}:${frame.source.fromLine}-${frame.source.toLine}`
`${evidenceLocation(frame.source).file}:${evidenceLocation(frame.source).fromLine}-${evidenceLocation(frame.source).toLine}`
);
}

Expand All @@ -36,7 +37,7 @@ export function frameName(frame: Frame): string {
frame.label ??
frame.key ??
frame.id ??
frame.source.file.split("/").pop() ??
frame.source.file
evidenceLocation(frame.source).file.split("/").pop() ??
evidenceLocation(frame.source).file
);
}
13 changes: 11 additions & 2 deletions packages/review/src/review-api/diagram-lenses.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import type { ReviewDiffLens } from "@dev.fast/review-protocol";
import type {
ReviewDiffLens,
ReviewDiffLensTarget,
} from "@dev.fast/review-protocol";

import { evidenceTargets } from "../source.js";
import {
type Block,
type Source,
elements,
sourceReferences,
evidenceReferences,
} from "./document.js";

export interface DiagramLens {
id: string;
title: string;
kind: string;
sources: Source[];
targets: ReviewDiffLensTarget[];
fileCount?: number;
wholeFiles?: boolean;
}
Expand All @@ -33,6 +39,9 @@ export function diagramLenses(document: Block[]): DiagramLens[] {
id: block.id!,
title: block.type === "software_map" ? "Software map" : block.title,
kind: block.type,
targets: evidenceTargets(
evidenceReferences([block]).map((ref) => ref.source),
),
sources: sourceReferences([block]).map((ref) => ref.source),
},
];
Expand All @@ -49,7 +58,7 @@ export function nativeLens(
title: lens.title,
reviewId,
version,
ranges: lens.sources,
targets: lens.targets,
wholeFiles: lens.wholeFiles ?? false,
};
}
14 changes: 11 additions & 3 deletions packages/review/src/review-api/document-text.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { evidenceSources, type CodeEvidence } from "../source.js";
import { fileLensTargets } from "./blocks/file_lens.js";
import {
type Element,
Expand All @@ -7,8 +8,13 @@ import {
} from "./document.js";
import type { Snapshot } from "./store.js";

const sourceText = (source: Source) =>
`${source.side}/${source.file}:${source.fromLine}-${source.toLine}`;
const sourceText = (evidence: CodeEvidence) =>
evidenceSources(evidence)
.map(
(source) =>
`${source.side}/${source.file}:${source.fromLine}-${source.toLine}`,
)
.join(", ");

/** A reading view of saved content, not another document format to maintain. */
export function documentText(
Expand Down Expand Up @@ -159,7 +165,9 @@ export function documentText(
if (target.kind === "files")
detail(`Files: ${target.patterns.join(", ")}`);
else
for (const source of target.sources)
for (const source of target.kind === "ranges"
? target.sources
: target.results)
detail(`Range: ${sourceText(source)}`);
}
break;
Expand Down
39 changes: 33 additions & 6 deletions packages/review/src/review-api/document.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { z } from "zod";

import { markdownNodes, markdownText, parseMarkdown } from "../markdown.js";
import { type Source, sourceSchema } from "../source.js";
import {
type Source,
type CodeEvidence,
evidenceSources,
sourceSchema,
} from "../source.js";
import { fileLensTargets } from "./blocks/file_lens.js";
import { type Block, blockSchema } from "./blocks/index.js";
import { type Step, stepSchema } from "./blocks/sequence.js";
import { ReviewInputError } from "./input-error.js";

export { ReviewInputError } from "./input-error.js";

export { type Source, sourceSchema };
export { type Source, type CodeEvidence, sourceSchema };

export {
type Block,
Expand Down Expand Up @@ -130,10 +135,10 @@ export function resourceReferences(document: Block[]): Block[] {
* `tolerant` skips malformed Markdown source links instead of rejecting, for
* content that is already stored.
*/
export function sourceReferences(
export function evidenceReferences(
document: Block[],
{ tolerant = false }: { tolerant?: boolean } = {},
): { id: string; source: Source; label?: string; peek?: boolean }[] {
): { id: string; source: CodeEvidence; label?: string; peek?: boolean }[] {
const reject = (message: string): [] => {
if (tolerant) return [];
throw new ReviewInputError(message);
Expand Down Expand Up @@ -180,13 +185,23 @@ export function sourceReferences(
);

if (element.type === "file_lens")
return fileLensTargets(element).flatMap((target, index) =>
return fileLensTargets(element).flatMap<{
id: string;
source: CodeEvidence;
peek?: boolean;
}>((target, index) =>
target.kind === "ranges"
? target.sources.map((source, range) => ({
id: `${element.id}:target:${index}:${range}`,
source,
}))
: [],
: target.kind === "results"
? target.results.map((source, result) => ({
id: `${element.id}:target:${index}:${result}`,
source,
peek: true,
}))
: [],
);

if (element.type === "call_stack_diff")
Expand Down Expand Up @@ -490,3 +505,15 @@ export function rewriteSourceLinks(

return markdown;
}

export function sourceReferences(
document: Block[],
options: { tolerant?: boolean } = {},
): { id: string; source: Source; label?: string; peek?: boolean }[] {
return evidenceReferences(document, options).flatMap((reference) =>
evidenceSources(reference.source).map((source) => ({
...reference,
source,
})),
);
}
25 changes: 19 additions & 6 deletions packages/review/src/review-api/file-lenses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { posix } from "node:path";

import type { Source } from "../source.js";
import type { ReviewDiffLensTarget } from "@dev.fast/review-protocol";

import { evidenceSources, type Source } from "../source.js";
import {
type CoverageFile,
coverageSources,
Expand Down Expand Up @@ -31,12 +33,22 @@ export function resolveFileLens(
fileSources: ReadonlyMap<string, Source[]>,
) {
const targets = fileLensTargets(block);
const selected = targets.flatMap((target) =>
const displayTargets: ReviewDiffLensTarget[] = targets.map((target) => {
if (target.kind === "results") return target;
return {
kind: "ranges",
ranges:
target.kind === "ranges"
? target.sources
: files
.filter((file) => matchesFileLens(target.patterns, file))
.flatMap((file) => fileSources.get(file.path) ?? []),
};
});
const selected = displayTargets.flatMap((target) =>
target.kind === "ranges"
? target.sources
: files
.filter((file) => matchesFileLens(target.patterns, file))
.flatMap((file) => fileSources.get(file.path) ?? []),
? [...target.ranges]
: target.results.flatMap(evidenceSources),
);
const groups = new Map<string, Source[]>();
for (const source of selected) {
Expand Down Expand Up @@ -67,6 +79,7 @@ export function resolveFileLens(
),
).size;
return {
targets: displayTargets,
sources,
fileCount,
wholeFiles: targets.every((target) => target.kind === "files"),
Expand Down
8 changes: 6 additions & 2 deletions packages/review/src/review-api/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { mountSharingHost } from "../sharing/host.js";
import type { SharedReviewStore } from "../sharing/import.js";
import { SharedReviewData } from "../sharing/routes.js";
import { scopedCoverage } from "../viewed-coverage.js";

import { authoringTools } from "./authoring-tools.js";
import { documentText } from "./document-text.js";
import { ReviewInputError, sourceSchema } from "./document.js";
Expand Down Expand Up @@ -765,7 +764,12 @@ export function createReviewApi(
);
});
app.post("/commands", async (context) => {
const input = await readBoundedRequestJson(context.req.raw);
// Authored diffr results carry complete source and structural trees. A
// multi-step edit may include several; retain a finite streaming limit.
const input = await readBoundedRequestJson(
context.req.raw,
8 * 1024 * 1024,
);
const command = sharedCommandSchema.safeParse(input);

if (
Expand Down
101 changes: 101 additions & 0 deletions packages/review/src/review-api/local-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2480,3 +2480,104 @@ it("keeps live language identity across edits but replaces it with a checkout at
rmSync(moved, { recursive: true, force: true });
}
});

it("saves diffr evidence without its worktrees, reopens it, and marks it stale after repinning", async () => {
const text = git("show", `${pins.head}:example.ts`) + "\n";
const evidence = {
display: "rhs",
scope: {
repo: "/does-not-exist",
baseWorktree: { commitId: pins.base, path: "/does-not-exist/base" },
headWorktree: { commitId: pins.head, path: "/does-not-exist/head" },
},
file: {
rhs: {
path: "example.ts",
oid: git("rev-parse", `${pins.head}:example.ts`),
mode: "100644",
},
},
sources: {
rhs: {
text,
regions: [
{
kind: "leaf",
id: 1,
fold_state_id: 1,
alignment_id: 1,
start: { line: 0, column: 0 },
end: { line: 2, column: 0 },
search_highlights: [{ line: 1, start_column: 0, end_column: 26 }],
},
],
},
},
};
const { reviewId } = await local.store.execute(
command({ type: "create", title: "Direct evidence", pins }),
);
await insert(reviewId, { type: "code_peek", source: evidence });
await insert(reviewId, {
type: "file_lens",
title: "Mixed glob and result",
targets: [
{ kind: "files", patterns: ["literal*.ts"] },
{ kind: "results", results: [evidence] },
],
});
await insert(reviewId, {
type: "sequence",
title: "Evidence",
actors: { a: "A", b: "B" },
steps: [{ from: "a", to: "b", label: "Inspect", source: evidence }],
});
const app = createReviewApi(local.store, local.data);
const progress = await (await app.request(`/${reviewId}/progress`)).json();
const lens = progress.diagrams.find(
(lens: { title: string }) => lens.title === "Mixed glob and result",
);
expect(
lens.targets[0].ranges.map((range: { file: string }) => range.file).sort(),
).toEqual(["literal1.ts", "literal[1].ts"]);
expect(lens.targets[1].results).toEqual([evidence]);
expect(lens.targets[1].results[0].sources.lhs).toBeUndefined();
const saved = local.store.read(reviewId);
await local.store.close();
await local.data.close();
local = openLocalReviewStore(database);
expect(local.store.read(reviewId).document).toEqual(saved.document);
for (const corrupt of [
{
...evidence,
scope: {
...evidence.scope,
headWorktree: { ...evidence.scope.headWorktree, commitId: pins.base },
},
},
{
...evidence,
sources: {
rhs: {
...evidence.sources.rhs,
text: text.replace("value = 2", "value = 9"),
},
},
},
{
...evidence,
file: { rhs: { ...evidence.file.rhs, oid: "0".repeat(40) } },
},
])
await expect(
insert(reviewId, { type: "code_peek", source: corrupt }),
).rejects.toThrow();
expect(local.store.read(reviewId).version).toBe(saved.version);
await local.store.execute(
command({ type: "repin", reviewId, pins: { ...pins, head: pins.base } }),
);
expect(local.store.read(reviewId).staleSources).toHaveLength(3);
expect(local.store.read(reviewId, saved.version).document).toEqual(
saved.document,
);
});
Loading
Loading