From c581182b1d0ed6dd4b47a379b6c0eac20aea5de9 Mon Sep 17 00:00:00 2001 From: Sidharth Menon Date: Sat, 19 Sep 2026 14:49:37 -0700 Subject: [PATCH 1/2] Validate and persist submitted evidence in existing snapshots Validate repository pins and Git blob identity before retaining supplied trees inline in versioned document JSON. Preserve stale evidence on repin, upgrade legacy reads, and accept bounded multi-result authoring requests. No new tables or resource-upload flow. AI assistance: reorganized with Codex. --- packages/review/src/review-api/document.ts | 39 +++++-- packages/review/src/review-api/http.ts | 8 +- .../review/src/review-api/local-data.test.ts | 101 ++++++++++++++++++ packages/review/src/review-api/local-data.ts | 81 +++++++++++++- .../review/src/review-api/review-api.test.ts | 2 +- packages/review/src/review-api/store.ts | 40 ++++--- 6 files changed, 245 insertions(+), 26 deletions(-) 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/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/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}`, ); }, ), From 92f0620bed0f5c32c54fe4e8f072d587d55c783e Mon Sep 17 00:00:00 2001 From: Sidharth Menon Date: Sat, 19 Sep 2026 14:49:37 -0700 Subject: [PATCH 2/2] Preserve evidence targets through lenses, progress and publication Separate display payloads from coordinate projections used for navigation and coverage. Carry mixed range/result targets through file and diagram lenses, progress, document text and publication consumers. AI assistance: reorganized with Codex. --- packages/review/src/call-stack-diff.ts | 3 ++- packages/review/src/call-stack-frames.ts | 7 +++--- .../review/src/review-api/diagram-lenses.ts | 13 ++++++++-- .../review/src/review-api/document-text.ts | 14 ++++++++--- packages/review/src/review-api/file-lenses.ts | 25 ++++++++++++++----- .../review/src/review-api/review-progress.ts | 15 +++++++++-- .../review/src/review-publication-audit.ts | 3 ++- 7 files changed, 62 insertions(+), 18 deletions(-) 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/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/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-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)) {