diff --git a/packages/review/src/call-stack-diff.ts b/packages/review/src/call-stack-diff.ts index 121fc0631..1608127e1 100644 --- a/packages/review/src/call-stack-diff.ts +++ b/packages/review/src/call-stack-diff.ts @@ -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"; @@ -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; diff --git a/packages/review/src/call-stack-frames.ts b/packages/review/src/call-stack-frames.ts index a698fae0b..60ee6d45d 100644 --- a/packages/review/src/call-stack-frames.ts +++ b/packages/review/src/call-stack-frames.ts @@ -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 @@ -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}` ); } @@ -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 ); } diff --git a/packages/review/src/review-api/diagram-lenses.ts b/packages/review/src/review-api/diagram-lenses.ts index 50f4b9062..5a9586c1c 100644 --- a/packages/review/src/review-api/diagram-lenses.ts +++ b/packages/review/src/review-api/diagram-lenses.ts @@ -1,10 +1,15 @@ -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 { @@ -12,6 +17,7 @@ export interface DiagramLens { title: string; kind: string; sources: Source[]; + targets: ReviewDiffLensTarget[]; fileCount?: number; wholeFiles?: boolean; } @@ -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), }, ]; @@ -49,7 +58,7 @@ export function nativeLens( title: lens.title, reviewId, version, - ranges: lens.sources, + targets: lens.targets, wholeFiles: lens.wholeFiles ?? false, }; } diff --git a/packages/review/src/review-api/document-text.ts b/packages/review/src/review-api/document-text.ts index 2e1a10dad..1628c7d41 100644 --- a/packages/review/src/review-api/document-text.ts +++ b/packages/review/src/review-api/document-text.ts @@ -1,3 +1,4 @@ +import { evidenceSources, type CodeEvidence } from "../source.js"; import { fileLensTargets } from "./blocks/file_lens.js"; import { type Element, @@ -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( @@ -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; diff --git a/packages/review/src/review-api/document.ts b/packages/review/src/review-api/document.ts index 2b21f4cb4..efc69cbed 100644 --- a/packages/review/src/review-api/document.ts +++ b/packages/review/src/review-api/document.ts @@ -1,7 +1,12 @@ 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"; @@ -9,7 +14,7 @@ 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, @@ -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); @@ -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") @@ -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, + })), + ); +} diff --git a/packages/review/src/review-api/file-lenses.ts b/packages/review/src/review-api/file-lenses.ts index aff2c0ec3..a6810673e 100644 --- a/packages/review/src/review-api/file-lenses.ts +++ b/packages/review/src/review-api/file-lenses.ts @@ -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, @@ -31,12 +33,22 @@ export function resolveFileLens( fileSources: ReadonlyMap, ) { 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(); for (const source of selected) { @@ -67,6 +79,7 @@ export function resolveFileLens( ), ).size; return { + targets: displayTargets, sources, fileCount, wholeFiles: targets.every((target) => target.kind === "files"), diff --git a/packages/review/src/review-api/http.ts b/packages/review/src/review-api/http.ts index f21adc5c5..5b14d5c21 100644 --- a/packages/review/src/review-api/http.ts +++ b/packages/review/src/review-api/http.ts @@ -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"; @@ -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 ( diff --git a/packages/review/src/review-api/local-data.test.ts b/packages/review/src/review-api/local-data.test.ts index a0c183c58..1b3339758 100644 --- a/packages/review/src/review-api/local-data.test.ts +++ b/packages/review/src/review-api/local-data.test.ts @@ -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, + ); +}); diff --git a/packages/review/src/review-api/local-data.ts b/packages/review/src/review-api/local-data.ts index 693f7e4ac..1bb0ec2d6 100644 --- a/packages/review/src/review-api/local-data.ts +++ b/packages/review/src/review-api/local-data.ts @@ -1,5 +1,8 @@ +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; import { type FSWatcher, existsSync, watch } from "node:fs"; import { readFile, realpath, stat } from "node:fs/promises"; +import { promisify } from "node:util"; import type { JsonObject } from "@dev.fast/json"; import { @@ -23,6 +26,7 @@ import type { ReviewLanguageEnvironment, ReviewSourceEntry, } from "@dev.fast/review-protocol"; +import { searchResultDataSchema } from "diffr/schema"; import { z } from "zod"; import { textIncludesQuote } from "../evidence.js"; @@ -34,6 +38,7 @@ import { SoftwareModelValidationError, defineSoftwareMap, } from "../software-map-model.js"; +import type { CodeEvidence } from "../source.js"; import { SourceRangeError, checkSourcePath, @@ -50,6 +55,7 @@ import { elements, pinsSchema, sourceReferences, + evidenceReferences, sourceSchema, } from "./document.js"; import { decodeImage } from "./image-decode.js"; @@ -438,11 +444,13 @@ export class LocalReviewData { // Live references retain their authored coordinates. Only diagnose ranges // that no longer exist; the author decides how to update changed source. - for (const reference of sourceReferences(snapshot.document, { + for (const reference of evidenceReferences(snapshot.document, { tolerant: true, })) { try { - await this.quote(pins, reference.source); + await this.validateSource(pins, reference.source, { + peek: reference.peek === true, + }); } catch (error) { if (!(error instanceof ReviewInputError)) throw error; projected.staleSources!.push(reference.id); @@ -678,7 +686,70 @@ export class LocalReviewData { } /** Every source reference must exist at the pins; only code peeks must also * show something. */ - async validateSource(pins: Pins, source: Source, options: { peek: boolean }) { + async validateSource( + pins: Pins, + source: CodeEvidence, + options: { peek: boolean }, + ) { + if ("display" in source) { + const parsed = searchResultDataSchema.safeParse(source); + if (!parsed.success) throw new ReviewInputError(parsed.error.message); + const result = parsed.data; + if ( + result.scope.baseWorktree.commitId !== pins.base || + result.scope.headWorktree.commitId !== pins.head + ) + throw new ReviewInputError( + "Diffr evidence belongs to different comparison pins; replace it after repinning.", + ); + const vcs = await this.vcs(pins.repositoryId); + const gitDirectory = vcs && (await gitCommonDir(vcs.rootPath)); + if (!gitDirectory) + throw new ReviewInputError( + "Pinned Git repository is unavailable.", + 404, + ); + for (const [key, side] of [ + ["lhs", "base"], + ["rhs", "head"], + ] as const) { + const file = result.file[key], + content = result.sources.same ?? result.sources[key]; + if (!file || !content) continue; + const { stdout: entry } = await promisify(execFile)( + "git", + [ + "--git-dir", + gitDirectory, + "ls-tree", + "-z", + pins[side], + "--", + `:(literal)${file.path}`, + ], + { encoding: "utf8" }, + ); + if (entry !== `${file.mode} blob ${file.oid}\t${file.path}\0`) + throw new ReviewInputError( + "Diffr evidence file identity differs from the pinned tree.", + ); + const original = await this.file(pins, side, file.path); + if (original.text !== content.text) + throw new ReviewInputError( + "Diffr evidence text differs from the pinned source.", + ); + const bytes = Buffer.from(content.text); + const oid = createHash(file.oid.length === 64 ? "sha256" : "sha1") + .update(`blob ${bytes.length}\0`) + .update(bytes) + .digest("hex"); + if (oid !== file.oid) + throw new ReviewInputError( + "Diffr evidence blob identity differs from the pinned source.", + ); + } + return; + } const quote = await this.quote(pins, source); if (options.peek) @@ -688,7 +759,7 @@ export class LocalReviewData { * the problem becomes a warning instead of a rejection. */ async validateSourceTolerant( pins: Pins, - source: Source, + source: CodeEvidence, options: { peek: boolean }, ): Promise { try { @@ -697,7 +768,7 @@ export class LocalReviewData { return null; } catch (error) { if (error instanceof ReviewInputError) - return `${source.side}/${source.file}#L${source.fromLine}-L${source.toLine}: ${error.message}`; + return `${"display" in source ? "Diffr evidence" : `${source.side}/${source.file}#L${source.fromLine}-L${source.toLine}`}: ${error.message}`; throw error; } } diff --git a/packages/review/src/review-api/review-api.test.ts b/packages/review/src/review-api/review-api.test.ts index a5af13fba..df3fdac7d 100644 --- a/packages/review/src/review-api/review-api.test.ts +++ b/packages/review/src/review-api/review-api.test.ts @@ -949,7 +949,7 @@ it("serves the experiment through the real desktop HTTP server and existing auth await fetch(url + "/commands", { method: "POST", headers, - body: JSON.stringify({ padding: "x".repeat(1024 * 1024) }), + body: JSON.stringify({ padding: "x".repeat(8 * 1024 * 1024) }), }) ).status, ).toBe(413); diff --git a/packages/review/src/review-api/review-progress.ts b/packages/review/src/review-api/review-progress.ts index 3c2ad27ff..a0359365f 100644 --- a/packages/review/src/review-api/review-progress.ts +++ b/packages/review/src/review-api/review-progress.ts @@ -10,7 +10,7 @@ import { unionIntervals, } from "../viewed-coverage.js"; import { type DiagramLens, diagramLenses } from "./diagram-lenses.js"; -import { elements } from "./document.js"; +import { elements, evidenceReferences, type Block } from "./document.js"; import { resolveFileLens, uncategorizedSources } from "./file-lenses.js"; import type { LocalReviewData } from "./local-data.js"; import type { ReviewStore, Snapshot } from "./store.js"; @@ -132,10 +132,18 @@ export async function reviewProgress( const diagrams = await Promise.all( diagramLenses(snapshot.document).map(async (lens) => { const block = elements(snapshot.document).find( - (block) => block.id === lens.id, + (block): block is Block => + block.type !== "step" && block.id === lens.id, )!; try { + await Promise.all( + evidenceReferences([block]).map((ref) => + data.validateSource(snapshot.pins, ref.source, { + peek: ref.peek === true, + }), + ), + ); if (block.type === "file_lens") { // Explicit ranges use the same pin validation as diagram evidence. await Promise.all( @@ -177,6 +185,8 @@ export async function reviewProgress( ]); } + if (block.type === "software_map") + lens.targets = [{ kind: "ranges", ranges: lens.sources }]; await Promise.all( lens.sources.map((source) => data.quote(snapshot.pins, source)), ); @@ -205,6 +215,7 @@ export async function reviewProgress( title: "Uncategorized changes", kind: "file_lens", sources: uncategorized, + targets: [{ kind: "ranges", ranges: uncategorized }], wholeFiles: false, fileCount: new Set( uncategorized.map( diff --git a/packages/review/src/review-api/store.ts b/packages/review/src/review-api/store.ts index 6319224d4..bf9c310bc 100644 --- a/packages/review/src/review-api/store.ts +++ b/packages/review/src/review-api/store.ts @@ -5,6 +5,7 @@ import { isDeepStrictEqual } from "node:util"; import type { ReviewApiSummary } from "@dev.fast/review-protocol"; import { z } from "zod"; +import { upgradeStoredEvidence } from "../source.js"; import { type Coverage, coverageSchema, @@ -18,6 +19,7 @@ import { ReviewInputError, type ReviewTarget, type Source, + type CodeEvidence, applyEdit, assignFreshIds, checkReferences, @@ -26,8 +28,8 @@ import { elements, pinsSchema, resourceReferences, + evidenceReferences, reviewTargetSchema, - sourceReferences, } from "./document.js"; const reviewId = z.string().min(1); @@ -146,14 +148,14 @@ export interface ReviewProviders { validatePins(pins: Pins): Promise; validateSource( pins: Pins, - source: Source, + source: CodeEvidence, options: { peek: boolean }, ): Promise; validateResource(pins: Pins, block: Block): Promise; /** Import only: report a problem as a warning instead of rejecting. */ validateSourceTolerant?( pins: Pins, - source: Source, + source: CodeEvidence, options: { peek: boolean }, ): Promise; } @@ -503,7 +505,9 @@ export class ReviewStore { if (!row) throw new ReviewInputError("Review or version not found.", 404); // SAFETY: versions contains only snapshots validated by execute before committing. - const snapshot = JSON.parse(String(row.snapshot)) as Snapshot; + const snapshot = upgradeStoredEvidence( + JSON.parse(String(row.snapshot)), + ) as Snapshot; snapshot.target ??= { kind: "commits", repositoryId: snapshot.pins.repositoryId, @@ -812,14 +816,14 @@ export class ReviewStore { if (snapshot.staleSources?.length) { const oldSources = new Map( - sourceReferences(previous!.document).map((item) => [ + evidenceReferences(previous!.document).map((item) => [ item.id, JSON.stringify(item.source), ]), ); const newSources = new Map( - sourceReferences(snapshot.document).map((item) => [ + evidenceReferences(snapshot.document).map((item) => [ item.id, JSON.stringify(item.source), ]), @@ -988,7 +992,7 @@ export class ReviewStore { const seen = new Set(); - for (const { source, peek } of sourceReferences(document, { + for (const { source, peek } of evidenceReferences(document, { tolerant: true, })) { const key = JSON.stringify(source); @@ -1092,16 +1096,19 @@ export class ReviewStore { const warnings: string[] = []; const references = (document: Block[], tolerant = false) => { - const sources = new Map(); + const sources = new Map< + string, + { source: CodeEvidence; peek: boolean } + >(); const resources = new Map(); - const add = (source: Source, peek: boolean) => { + const add = (source: CodeEvidence, peek: boolean) => { const key = JSON.stringify(source); const kept = sources.get(key); sources.set(key, { source, peek: peek || (kept?.peek ?? false) }); }; - for (const { source, peek } of sourceReferences(document, { tolerant })) + for (const { source, peek } of evidenceReferences(document, { tolerant })) add(source, peek === true); for (const block of resourceReferences(document)) @@ -1135,7 +1142,7 @@ export class ReviewStore { () => { if (repin) warnings.push( - `${source.side}/${source.file}#L${source.fromLine}-L${source.toLine}: source pins changed; verify that this range still supports the document.`, + `${"display" in source ? `diffr:${source.file.rhs?.path ?? source.file.lhs?.path}` : `${source.side}/${source.file}#L${source.fromLine}-L${source.toLine}`}: source pins changed; verify that this range still supports the document.`, ); }, (error) => { @@ -1144,8 +1151,17 @@ export class ReviewStore { !(error instanceof ReviewInputError) ) throw error; + if ("display" in source) { + const stale = new Set(snapshot.staleSources ?? []); + for (const reference of evidenceReferences(snapshot.document, { + tolerant: true, + })) + if (JSON.stringify(reference.source) === key) + stale.add(reference.id); + snapshot.staleSources = [...stale]; + } warnings.push( - `${source.side}/${source.file}#L${source.fromLine}-L${source.toLine}: ${error.message}`, + `${"display" in source ? `diffr:${source.file.rhs?.path ?? source.file.lhs?.path}` : `${source.side}/${source.file}#L${source.fromLine}-L${source.toLine}`}: ${error.message}`, ); }, ), diff --git a/packages/review/src/review-publication-audit.ts b/packages/review/src/review-publication-audit.ts index 490a6cac9..5ee83b726 100644 --- a/packages/review/src/review-publication-audit.ts +++ b/packages/review/src/review-publication-audit.ts @@ -58,6 +58,7 @@ import { requireVisibleSource, sliceSourceRange, } from "./source"; +import { evidenceLocation } from "./source.js"; import { span, startSpan } from "./startup-trace"; export interface ReviewPublishSourceTarget { @@ -358,7 +359,7 @@ async function validateCallStackEvidence(input: { for (const row of rows) { if (row.change === "unchanged") continue; const side: CallStackSide = row.change === "removed" ? "base" : "head"; - const file = row.frame.source.file; + const file = evidenceLocation(row.frame.source).file; const key = `${side}\0${file}`; if (!changedLines.has(key)) {