@farmslot/recipe-runner 0.23.0
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.
- package/CHANGELOG.md +255 -0
- package/LICENSE +21 -0
- package/README.md +274 -0
- package/bin/farmslot-visual-review.mjs +152 -0
- package/dist/adapters/app-lifecycle.d.ts +41 -0
- package/dist/adapters/app-lifecycle.d.ts.map +1 -0
- package/dist/adapters/app-lifecycle.js +224 -0
- package/dist/adapters/app-lifecycle.js.map +1 -0
- package/dist/adapters/core.d.ts +5 -0
- package/dist/adapters/core.d.ts.map +1 -0
- package/dist/adapters/core.js +433 -0
- package/dist/adapters/core.js.map +1 -0
- package/dist/adapters/gesture.d.ts +12 -0
- package/dist/adapters/gesture.d.ts.map +1 -0
- package/dist/adapters/gesture.js +76 -0
- package/dist/adapters/gesture.js.map +1 -0
- package/dist/adapters/ui.d.ts +33 -0
- package/dist/adapters/ui.d.ts.map +1 -0
- package/dist/adapters/ui.js +130 -0
- package/dist/adapters/ui.js.map +1 -0
- package/dist/cli/error-output.d.ts +6 -0
- package/dist/cli/error-output.d.ts.map +1 -0
- package/dist/cli/error-output.js +19 -0
- package/dist/cli/error-output.js.map +1 -0
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +29 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/run-command.d.ts +8 -0
- package/dist/cli/run-command.d.ts.map +1 -0
- package/dist/cli/run-command.js +253 -0
- package/dist/cli/run-command.js.map +1 -0
- package/dist/cli/support.d.ts +31 -0
- package/dist/cli/support.d.ts.map +1 -0
- package/dist/cli/support.js +297 -0
- package/dist/cli/support.js.map +1 -0
- package/dist/cli/validate-command.d.ts +4 -0
- package/dist/cli/validate-command.d.ts.map +1 -0
- package/dist/cli/validate-command.js +52 -0
- package/dist/cli/validate-command.js.map +1 -0
- package/dist/core/compose.d.ts +23 -0
- package/dist/core/compose.d.ts.map +1 -0
- package/dist/core/compose.js +104 -0
- package/dist/core/compose.js.map +1 -0
- package/dist/core/execution.d.ts +44 -0
- package/dist/core/execution.d.ts.map +1 -0
- package/dist/core/execution.js +304 -0
- package/dist/core/execution.js.map +1 -0
- package/dist/core/failure.d.ts +23 -0
- package/dist/core/failure.d.ts.map +1 -0
- package/dist/core/failure.js +43 -0
- package/dist/core/failure.js.map +1 -0
- package/dist/core/graph.d.ts +12 -0
- package/dist/core/graph.d.ts.map +1 -0
- package/dist/core/graph.js +65 -0
- package/dist/core/graph.js.map +1 -0
- package/dist/core/hud.d.ts +11 -0
- package/dist/core/hud.d.ts.map +1 -0
- package/dist/core/hud.js +120 -0
- package/dist/core/hud.js.map +1 -0
- package/dist/core/invocation.d.ts +13 -0
- package/dist/core/invocation.d.ts.map +1 -0
- package/dist/core/invocation.js +68 -0
- package/dist/core/invocation.js.map +1 -0
- package/dist/core/json.d.ts +9 -0
- package/dist/core/json.d.ts.map +1 -0
- package/dist/core/json.js +74 -0
- package/dist/core/json.js.map +1 -0
- package/dist/core/library-manifest.d.ts +53 -0
- package/dist/core/library-manifest.d.ts.map +1 -0
- package/dist/core/library-manifest.js +263 -0
- package/dist/core/library-manifest.js.map +1 -0
- package/dist/core/library.d.ts +80 -0
- package/dist/core/library.d.ts.map +1 -0
- package/dist/core/library.js +321 -0
- package/dist/core/library.js.map +1 -0
- package/dist/core/parameters.d.ts +5 -0
- package/dist/core/parameters.d.ts.map +1 -0
- package/dist/core/parameters.js +57 -0
- package/dist/core/parameters.js.map +1 -0
- package/dist/core/passive-observations.d.ts +25 -0
- package/dist/core/passive-observations.d.ts.map +1 -0
- package/dist/core/passive-observations.js +62 -0
- package/dist/core/passive-observations.js.map +1 -0
- package/dist/core/path.d.ts +7 -0
- package/dist/core/path.d.ts.map +1 -0
- package/dist/core/path.js +147 -0
- package/dist/core/path.js.map +1 -0
- package/dist/core/recording-cleanup.d.ts +10 -0
- package/dist/core/recording-cleanup.d.ts.map +1 -0
- package/dist/core/recording-cleanup.js +32 -0
- package/dist/core/recording-cleanup.js.map +1 -0
- package/dist/core/resolution-error.d.ts +7 -0
- package/dist/core/resolution-error.d.ts.map +1 -0
- package/dist/core/resolution-error.js +9 -0
- package/dist/core/resolution-error.js.map +1 -0
- package/dist/core/runner.d.ts +4 -0
- package/dist/core/runner.d.ts.map +1 -0
- package/dist/core/runner.js +738 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/core/scroll-to.d.ts +111 -0
- package/dist/core/scroll-to.d.ts.map +1 -0
- package/dist/core/scroll-to.js +353 -0
- package/dist/core/scroll-to.js.map +1 -0
- package/dist/core/suite.d.ts +25 -0
- package/dist/core/suite.d.ts.map +1 -0
- package/dist/core/suite.js +76 -0
- package/dist/core/suite.js.map +1 -0
- package/dist/core/trace.d.ts +5 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +25 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/core/trust-error.d.ts +9 -0
- package/dist/core/trust-error.d.ts.map +1 -0
- package/dist/core/trust-error.js +18 -0
- package/dist/core/trust-error.js.map +1 -0
- package/dist/core/trust-input.d.ts +20 -0
- package/dist/core/trust-input.d.ts.map +1 -0
- package/dist/core/trust-input.js +68 -0
- package/dist/core/trust-input.js.map +1 -0
- package/dist/core/trust.d.ts +28 -0
- package/dist/core/trust.d.ts.map +1 -0
- package/dist/core/trust.js +284 -0
- package/dist/core/trust.js.map +1 -0
- package/dist/core/types.d.ts +319 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +2 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/validation.d.ts +4 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +18 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/node/writers.d.ts +27 -0
- package/dist/node/writers.d.ts.map +1 -0
- package/dist/node/writers.js +99 -0
- package/dist/node/writers.js.map +1 -0
- package/dist/recording/android-mirror.d.ts +10 -0
- package/dist/recording/android-mirror.d.ts.map +1 -0
- package/dist/recording/android-mirror.js +180 -0
- package/dist/recording/android-mirror.js.map +1 -0
- package/dist/recording/capture-helper-timing.d.ts +4 -0
- package/dist/recording/capture-helper-timing.d.ts.map +1 -0
- package/dist/recording/capture-helper-timing.js +47 -0
- package/dist/recording/capture-helper-timing.js.map +1 -0
- package/dist/recording/capture-helper.d.ts +10 -0
- package/dist/recording/capture-helper.d.ts.map +1 -0
- package/dist/recording/capture-helper.js +335 -0
- package/dist/recording/capture-helper.js.map +1 -0
- package/dist/recording/cdp-video-recorder.d.ts +9 -0
- package/dist/recording/cdp-video-recorder.d.ts.map +1 -0
- package/dist/recording/cdp-video-recorder.js +319 -0
- package/dist/recording/cdp-video-recorder.js.map +1 -0
- package/dist/recording/frame-sampler.d.ts +19 -0
- package/dist/recording/frame-sampler.d.ts.map +1 -0
- package/dist/recording/frame-sampler.js +29 -0
- package/dist/recording/frame-sampler.js.map +1 -0
- package/dist/recording/timeline.d.ts +15 -0
- package/dist/recording/timeline.d.ts.map +1 -0
- package/dist/recording/timeline.js +88 -0
- package/dist/recording/timeline.js.map +1 -0
- package/dist/runtime/browser-extension.d.ts +17 -0
- package/dist/runtime/browser-extension.d.ts.map +1 -0
- package/dist/runtime/browser-extension.js +42 -0
- package/dist/runtime/browser-extension.js.map +1 -0
- package/dist/runtime/cdp.d.ts +138 -0
- package/dist/runtime/cdp.d.ts.map +1 -0
- package/dist/runtime/cdp.js +1429 -0
- package/dist/runtime/cdp.js.map +1 -0
- package/dist/runtime/decision-types.d.ts +23 -0
- package/dist/runtime/decision-types.d.ts.map +1 -0
- package/dist/runtime/decision-types.js +2 -0
- package/dist/runtime/decision-types.js.map +1 -0
- package/dist/runtime/deps-readiness.d.ts +21 -0
- package/dist/runtime/deps-readiness.d.ts.map +1 -0
- package/dist/runtime/deps-readiness.js +141 -0
- package/dist/runtime/deps-readiness.js.map +1 -0
- package/dist/runtime/log-analysis.d.ts +48 -0
- package/dist/runtime/log-analysis.d.ts.map +1 -0
- package/dist/runtime/log-analysis.js +128 -0
- package/dist/runtime/log-analysis.js.map +1 -0
- package/dist/runtime/metro-probe.d.ts +5 -0
- package/dist/runtime/metro-probe.d.ts.map +1 -0
- package/dist/runtime/metro-probe.js +24 -0
- package/dist/runtime/metro-probe.js.map +1 -0
- package/dist/runtime/operation.d.ts +32 -0
- package/dist/runtime/operation.d.ts.map +1 -0
- package/dist/runtime/operation.js +184 -0
- package/dist/runtime/operation.js.map +1 -0
- package/dist/runtime/orchestrate-up.d.ts +22 -0
- package/dist/runtime/orchestrate-up.d.ts.map +1 -0
- package/dist/runtime/orchestrate-up.js +35 -0
- package/dist/runtime/orchestrate-up.js.map +1 -0
- package/dist/runtime/react-native-bridge.d.ts +17 -0
- package/dist/runtime/react-native-bridge.d.ts.map +1 -0
- package/dist/runtime/react-native-bridge.js +57 -0
- package/dist/runtime/react-native-bridge.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/package.json +163 -0
- package/test/fixtures/scroll-to-visible/README.md +39 -0
- package/visual-review/feedback-draft.mjs +82 -0
- package/visual-review/index.d.mts +41 -0
- package/visual-review/index.mjs +4 -0
- package/visual-review/recipe-board.mjs +142 -0
- package/visual-review/review-board.mjs +981 -0
- package/visual-review/server.mjs +69 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Restores a portable feedback document into the board's editable draft. The function is
|
|
3
|
+
* serialized into the static board script, so it must stay self-contained.
|
|
4
|
+
*/
|
|
5
|
+
export function feedbackDraftFromDocument(source, document) {
|
|
6
|
+
if (
|
|
7
|
+
!document ||
|
|
8
|
+
document.kind !== 'visual-review-feedback' ||
|
|
9
|
+
document.version !== 1 ||
|
|
10
|
+
!Array.isArray(document.surfaceNotes) ||
|
|
11
|
+
!Array.isArray(document.annotations)
|
|
12
|
+
) {
|
|
13
|
+
throw new Error('Not a visual-review-feedback v1 document.');
|
|
14
|
+
}
|
|
15
|
+
if (document.source?.id !== source.id) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'Feedback belongs to source ' + document.source?.id + ', not ' + source.id + '.',
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
if (document.source.capturedAt !== source.capturedAt) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'Feedback was written for the capture of ' +
|
|
23
|
+
document.source.capturedAt +
|
|
24
|
+
'; this board shows the capture of ' +
|
|
25
|
+
source.capturedAt +
|
|
26
|
+
'.',
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
const surfaceIds = new Set(source.surfaces.map((surface) => surface.id));
|
|
30
|
+
const captureKeys = new Set(
|
|
31
|
+
source.surfaces.flatMap((surface) =>
|
|
32
|
+
surface.captures.map((capture) => surface.id + ':' + capture.id),
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
const isUnit = (value) => typeof value === 'number' && value >= 0 && value <= 1;
|
|
36
|
+
const isExtent = (start, size) =>
|
|
37
|
+
isUnit(start) && typeof size === 'number' && size > 0 && start + size <= 1;
|
|
38
|
+
const ids = new Set();
|
|
39
|
+
const invalid = [];
|
|
40
|
+
const surfaceNotes = {};
|
|
41
|
+
document.surfaceNotes.forEach((note, index) => {
|
|
42
|
+
const valid =
|
|
43
|
+
surfaceIds.has(note?.surfaceId) &&
|
|
44
|
+
!Object.hasOwn(surfaceNotes, note.surfaceId) &&
|
|
45
|
+
typeof note.body === 'string' &&
|
|
46
|
+
note.body.trim() !== '';
|
|
47
|
+
if (valid) {
|
|
48
|
+
surfaceNotes[note.surfaceId] = note.body;
|
|
49
|
+
} else {
|
|
50
|
+
invalid.push('surfaceNotes[' + index + ']');
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
document.annotations.forEach((annotation, index) => {
|
|
54
|
+
const valid =
|
|
55
|
+
captureKeys.has(annotation?.surfaceId + ':' + annotation?.captureId) &&
|
|
56
|
+
typeof annotation.id === 'string' &&
|
|
57
|
+
annotation.id.trim() !== '' &&
|
|
58
|
+
!ids.has(annotation.id) &&
|
|
59
|
+
typeof annotation.body === 'string' &&
|
|
60
|
+
annotation.body.trim() !== '' &&
|
|
61
|
+
// Same #rrggbb rule as the protocol's feedback validator; this function cannot import it.
|
|
62
|
+
(annotation.color == null || /^#[0-9a-f]{6}$/iu.test(String(annotation.color))) &&
|
|
63
|
+
(annotation.shape === 'point'
|
|
64
|
+
? isUnit(annotation.x) && isUnit(annotation.y)
|
|
65
|
+
: annotation.shape === 'area' &&
|
|
66
|
+
isExtent(annotation.x, annotation.width) &&
|
|
67
|
+
isExtent(annotation.y, annotation.height));
|
|
68
|
+
if (valid) ids.add(annotation.id);
|
|
69
|
+
else invalid.push('annotations[' + index + ']');
|
|
70
|
+
});
|
|
71
|
+
// Exports never contain these (blank entries are dropped on export), so any invalid entry
|
|
72
|
+
// means the wrong or an edited file.
|
|
73
|
+
if (invalid.length) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
'Feedback has invalid entries: ' + invalid.join(', ') + '. Nothing was opened.',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
surfaceNotes,
|
|
80
|
+
annotations: document.annotations.map((annotation) => ({ ...annotation })),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { VisualReviewFeedbackDraft, VisualReviewSourceDocument } from '@farmslot/protocol';
|
|
2
|
+
|
|
3
|
+
export interface GenerateReviewBoardOptions {
|
|
4
|
+
outputDir: string;
|
|
5
|
+
source: VisualReviewSourceDocument;
|
|
6
|
+
storageKey: string;
|
|
7
|
+
defaultPlatform?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function visualReviewFeedbackStorageKey(source: VisualReviewSourceDocument): string;
|
|
11
|
+
|
|
12
|
+
export interface VisualReviewServer {
|
|
13
|
+
url: string;
|
|
14
|
+
host: string;
|
|
15
|
+
port: number;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Reopens a portable feedback document as the board's editable draft for `source`. */
|
|
20
|
+
export function feedbackDraftFromDocument(
|
|
21
|
+
source: VisualReviewSourceDocument,
|
|
22
|
+
document: unknown,
|
|
23
|
+
): VisualReviewFeedbackDraft;
|
|
24
|
+
export function generateReviewBoard(options: GenerateReviewBoardOptions): void;
|
|
25
|
+
export function buildRecipeReviewBoard(options: {
|
|
26
|
+
artifactsDir: string;
|
|
27
|
+
outputDir: string;
|
|
28
|
+
platform: string;
|
|
29
|
+
recipePath: string;
|
|
30
|
+
sourceId: string;
|
|
31
|
+
project?: string;
|
|
32
|
+
runId?: string;
|
|
33
|
+
surfaceLocations?: Record<string, string>;
|
|
34
|
+
title?: string;
|
|
35
|
+
storageKey?: string;
|
|
36
|
+
}): VisualReviewSourceDocument;
|
|
37
|
+
export function serveReviewBoard(options: {
|
|
38
|
+
directory: string;
|
|
39
|
+
host?: string;
|
|
40
|
+
port?: number;
|
|
41
|
+
}): Promise<VisualReviewServer>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { generateReviewBoard, visualReviewFeedbackStorageKey } from './review-board.mjs';
|
|
5
|
+
|
|
6
|
+
function visualCaptureNodes(recipe, surfaceLocations) {
|
|
7
|
+
return Object.entries(recipe.workflow?.nodes ?? {})
|
|
8
|
+
.filter(
|
|
9
|
+
([, node]) =>
|
|
10
|
+
node.action === 'ui.capture_surface' ||
|
|
11
|
+
(node.action === 'ui.screenshot' && node.visual_review),
|
|
12
|
+
)
|
|
13
|
+
.map(([nodeId, node]) => ({
|
|
14
|
+
id: nodeId,
|
|
15
|
+
title: node.label ?? nodeId,
|
|
16
|
+
nodeId,
|
|
17
|
+
proofTargets: node.proves ?? [],
|
|
18
|
+
...(surfaceLocations[nodeId] ? { location: surfaceLocations[nodeId] } : {}),
|
|
19
|
+
...(node.visual_review?.parent ? { parentId: node.visual_review.parent } : {}),
|
|
20
|
+
navigation: node.visual_review?.navigation ?? [],
|
|
21
|
+
...(node.visual_review?.related ? { relatedSurfaceIds: node.visual_review.related } : {}),
|
|
22
|
+
imageTemplate: node.path ?? `screenshots/${nodeId}.png`,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function resolveArtifact(root, relativePath, label) {
|
|
27
|
+
const resolvedRoot = path.resolve(root);
|
|
28
|
+
const resolved = path.resolve(resolvedRoot, relativePath);
|
|
29
|
+
if (resolved !== resolvedRoot && !resolved.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
30
|
+
throw new Error(`${label} must stay inside ${resolvedRoot}.`);
|
|
31
|
+
}
|
|
32
|
+
return resolved;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildRecipeReviewBoard({
|
|
36
|
+
artifactsDir,
|
|
37
|
+
outputDir,
|
|
38
|
+
platform,
|
|
39
|
+
recipePath,
|
|
40
|
+
sourceId,
|
|
41
|
+
project,
|
|
42
|
+
runId,
|
|
43
|
+
surfaceLocations = {},
|
|
44
|
+
title,
|
|
45
|
+
storageKey,
|
|
46
|
+
}) {
|
|
47
|
+
const recipe = JSON.parse(readFileSync(recipePath, 'utf8'));
|
|
48
|
+
const captures = visualCaptureNodes(recipe, surfaceLocations);
|
|
49
|
+
if (captures.length === 0) {
|
|
50
|
+
throw new Error(`Recipe has no visual review capture nodes: ${recipePath}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const resolvedOutputDir = path.resolve(outputDir);
|
|
54
|
+
mkdirSync(resolvedOutputDir, { recursive: true });
|
|
55
|
+
const sourcePath = path.join(resolvedOutputDir, 'visual-review-source.json');
|
|
56
|
+
const previous = existsSync(sourcePath) ? JSON.parse(readFileSync(sourcePath, 'utf8')) : null;
|
|
57
|
+
const previousSurfaces =
|
|
58
|
+
previous?.kind === 'visual-review-source' && previous.id === sourceId ? previous.surfaces : [];
|
|
59
|
+
const previousNavigationEdges =
|
|
60
|
+
previous?.kind === 'visual-review-source' && previous.id === sourceId
|
|
61
|
+
? (previous.navigationEdges ?? [])
|
|
62
|
+
: [];
|
|
63
|
+
const byId = new Map(previousSurfaces.map((surface) => [surface.id, surface]));
|
|
64
|
+
const imageCopies = [];
|
|
65
|
+
|
|
66
|
+
for (const capture of captures) {
|
|
67
|
+
const relativeImagePath = capture.imageTemplate.replaceAll('{{params.platform}}', platform);
|
|
68
|
+
const sourceImagePath = resolveArtifact(
|
|
69
|
+
artifactsDir,
|
|
70
|
+
relativeImagePath,
|
|
71
|
+
'Recipe artifact path',
|
|
72
|
+
);
|
|
73
|
+
if (!existsSync(sourceImagePath)) {
|
|
74
|
+
throw new Error(`Recipe capture artifact is missing: ${sourceImagePath}`);
|
|
75
|
+
}
|
|
76
|
+
const destinationPath = resolveArtifact(
|
|
77
|
+
resolvedOutputDir,
|
|
78
|
+
relativeImagePath,
|
|
79
|
+
'Review image path',
|
|
80
|
+
);
|
|
81
|
+
imageCopies.push({ destinationPath, sourceImagePath });
|
|
82
|
+
|
|
83
|
+
const existing = byId.get(capture.id);
|
|
84
|
+
const captureId = `${platform}-${capture.id}`;
|
|
85
|
+
byId.set(capture.id, {
|
|
86
|
+
id: capture.id,
|
|
87
|
+
title: capture.title,
|
|
88
|
+
nodeId: capture.nodeId,
|
|
89
|
+
proofTargets: capture.proofTargets,
|
|
90
|
+
...(capture.location ? { location: capture.location } : {}),
|
|
91
|
+
...(capture.parentId ? { parentId: capture.parentId } : {}),
|
|
92
|
+
...(capture.relatedSurfaceIds ? { relatedSurfaceIds: capture.relatedSurfaceIds } : {}),
|
|
93
|
+
captures: [
|
|
94
|
+
...(existing?.captures ?? []).filter((candidate) => candidate.id !== captureId),
|
|
95
|
+
{
|
|
96
|
+
id: captureId,
|
|
97
|
+
platform,
|
|
98
|
+
image: { path: relativeImagePath, mimeType: 'image/png' },
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const surfaces = [...byId.values()];
|
|
105
|
+
const replacedTargets = new Set(captures.map((capture) => capture.id));
|
|
106
|
+
const navigationEdges = [
|
|
107
|
+
...previousNavigationEdges.filter((edge) => !replacedTargets.has(edge.toSurfaceId)),
|
|
108
|
+
...captures.flatMap((capture) =>
|
|
109
|
+
capture.navigation.map((edge) => ({
|
|
110
|
+
fromSurfaceId: edge.from,
|
|
111
|
+
toSurfaceId: capture.id,
|
|
112
|
+
kind: edge.kind,
|
|
113
|
+
})),
|
|
114
|
+
),
|
|
115
|
+
];
|
|
116
|
+
const platforms = [
|
|
117
|
+
...new Set(surfaces.flatMap((surface) => surface.captures.map((capture) => capture.platform))),
|
|
118
|
+
].sort();
|
|
119
|
+
const source = {
|
|
120
|
+
version: 1,
|
|
121
|
+
kind: 'visual-review-source',
|
|
122
|
+
id: sourceId,
|
|
123
|
+
title: title ?? recipe.title ?? sourceId,
|
|
124
|
+
capturedAt: new Date().toISOString(),
|
|
125
|
+
description: `${surfaces.length} surface${surfaces.length === 1 ? '' : 's'} · ${platforms.join('/')}`,
|
|
126
|
+
...(project ? { project } : {}),
|
|
127
|
+
...(runId ? { runId } : {}),
|
|
128
|
+
surfaces,
|
|
129
|
+
...(navigationEdges.length > 0 ? { navigationEdges } : {}),
|
|
130
|
+
};
|
|
131
|
+
generateReviewBoard({
|
|
132
|
+
outputDir: resolvedOutputDir,
|
|
133
|
+
source,
|
|
134
|
+
storageKey: storageKey ?? visualReviewFeedbackStorageKey(source),
|
|
135
|
+
defaultPlatform: platform,
|
|
136
|
+
});
|
|
137
|
+
for (const { destinationPath, sourceImagePath } of imageCopies) {
|
|
138
|
+
mkdirSync(path.dirname(destinationPath), { recursive: true });
|
|
139
|
+
if (sourceImagePath !== destinationPath) copyFileSync(sourceImagePath, destinationPath);
|
|
140
|
+
}
|
|
141
|
+
return source;
|
|
142
|
+
}
|