@archastro/astroshot-review 0.2.1

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/bin/astroshot-review.mjs +17 -0
  4. package/dist/cli.d.ts +12 -0
  5. package/dist/cli.js +240 -0
  6. package/dist/data/friction.d.ts +22 -0
  7. package/dist/data/friction.js +278 -0
  8. package/dist/data/hash-cache.d.ts +17 -0
  9. package/dist/data/hash-cache.js +51 -0
  10. package/dist/data/index-cache.d.ts +22 -0
  11. package/dist/data/index-cache.js +64 -0
  12. package/dist/data/manifest.d.ts +41 -0
  13. package/dist/data/manifest.js +105 -0
  14. package/dist/data/model.d.ts +96 -0
  15. package/dist/data/model.js +1 -0
  16. package/dist/data/paths.d.ts +27 -0
  17. package/dist/data/paths.js +98 -0
  18. package/dist/data/review-store.d.ts +67 -0
  19. package/dist/data/review-store.js +237 -0
  20. package/dist/data/scan.d.ts +32 -0
  21. package/dist/data/scan.js +227 -0
  22. package/dist/data/store.d.ts +92 -0
  23. package/dist/data/store.js +408 -0
  24. package/dist/data/watcher.d.ts +37 -0
  25. package/dist/data/watcher.js +126 -0
  26. package/dist/images/halfblocks.d.ts +10 -0
  27. package/dist/images/halfblocks.js +21 -0
  28. package/dist/images/png.d.ts +14 -0
  29. package/dist/images/png.js +45 -0
  30. package/dist/images/scale.d.ts +31 -0
  31. package/dist/images/scale.js +102 -0
  32. package/dist/images/service.d.ts +54 -0
  33. package/dist/images/service.js +163 -0
  34. package/dist/images/worker.d.ts +21 -0
  35. package/dist/images/worker.js +30 -0
  36. package/dist/index.d.ts +12 -0
  37. package/dist/index.js +9 -0
  38. package/dist/terminal/graphics-stdout.d.ts +9 -0
  39. package/dist/terminal/graphics-stdout.js +36 -0
  40. package/dist/terminal/herdr.d.ts +61 -0
  41. package/dist/terminal/herdr.js +327 -0
  42. package/dist/terminal/image-layer.d.ts +122 -0
  43. package/dist/terminal/image-layer.js +471 -0
  44. package/dist/terminal/kitty.d.ts +74 -0
  45. package/dist/terminal/kitty.js +112 -0
  46. package/dist/terminal/probe.d.ts +49 -0
  47. package/dist/terminal/probe.js +206 -0
  48. package/dist/ui/app.d.ts +6 -0
  49. package/dist/ui/app.js +695 -0
  50. package/dist/ui/chrome.d.ts +53 -0
  51. package/dist/ui/chrome.js +69 -0
  52. package/dist/ui/context.d.ts +16 -0
  53. package/dist/ui/context.js +8 -0
  54. package/dist/ui/detail.d.ts +33 -0
  55. package/dist/ui/detail.js +39 -0
  56. package/dist/ui/friction.d.ts +42 -0
  57. package/dist/ui/friction.js +84 -0
  58. package/dist/ui/help.d.ts +4 -0
  59. package/dist/ui/help.js +61 -0
  60. package/dist/ui/hooks.d.ts +9 -0
  61. package/dist/ui/hooks.js +32 -0
  62. package/dist/ui/movie-player.d.ts +29 -0
  63. package/dist/ui/movie-player.js +119 -0
  64. package/dist/ui/picture.d.ts +19 -0
  65. package/dist/ui/picture.js +100 -0
  66. package/dist/ui/selectors.d.ts +26 -0
  67. package/dist/ui/selectors.js +69 -0
  68. package/dist/ui/settings.d.ts +5 -0
  69. package/dist/ui/settings.js +17 -0
  70. package/dist/ui/stream.d.ts +38 -0
  71. package/dist/ui/stream.js +118 -0
  72. package/dist/ui/system.d.ts +4 -0
  73. package/dist/ui/system.js +34 -0
  74. package/dist/ui/takeover.d.ts +26 -0
  75. package/dist/ui/takeover.js +29 -0
  76. package/dist/ui/text-input.d.ts +8 -0
  77. package/dist/ui/text-input.js +74 -0
  78. package/dist/ui/theme.d.ts +21 -0
  79. package/dist/ui/theme.js +63 -0
  80. package/dist/video/ffmpeg.d.ts +54 -0
  81. package/dist/video/ffmpeg.js +206 -0
  82. package/package.json +71 -0
@@ -0,0 +1,22 @@
1
+ import type { HashRecord } from "./hash-cache.js";
2
+ export interface IndexDocument {
3
+ version: 1;
4
+ roots: string[];
5
+ astroshotDirs: string[];
6
+ arrivalOrder: string[];
7
+ hashes: Record<string, HashRecord>;
8
+ updatedAt: string;
9
+ /** When the last deep walk of every root finished (ISO), if ever. */
10
+ fullScanAt?: string;
11
+ }
12
+ export declare function indexCachePath(env?: NodeJS.ProcessEnv): string;
13
+ export declare function loadIndex(roots: string[], filePath?: string): Promise<IndexDocument | null>;
14
+ export declare function saveIndex(document: IndexDocument, filePath?: string): Promise<void>;
15
+ /**
16
+ * Keep known paths in their prior relative order; new paths go first, sorted
17
+ * newest capture first. Paths that vanished are dropped.
18
+ */
19
+ export declare function reconcileArrivalOrder(previous: string[], shots: Array<{
20
+ path: string;
21
+ capturedAt: number;
22
+ }>): string[];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Durable index so the tray opens instantly: known `.astroshot` directories,
3
+ * the newest-first arrival order, and memoized image hashes.
4
+ */
5
+ import fs from "node:fs";
6
+ import os from "node:os";
7
+ import path from "node:path";
8
+ export function indexCachePath(env = process.env) {
9
+ if (env.ASTROSHOT_REVIEW_CACHE_DIR)
10
+ return path.join(env.ASTROSHOT_REVIEW_CACHE_DIR, "index.json");
11
+ const base = env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
12
+ return path.join(base, "astroshot-review", "index.json");
13
+ }
14
+ function normalizedRoots(roots) {
15
+ return [...roots].sort();
16
+ }
17
+ export async function loadIndex(roots, filePath = indexCachePath()) {
18
+ let raw;
19
+ try {
20
+ raw = await fs.promises.readFile(filePath, "utf8");
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ try {
26
+ const parsed = JSON.parse(raw);
27
+ if (parsed.version !== 1 || !Array.isArray(parsed.roots))
28
+ return null;
29
+ if (JSON.stringify(normalizedRoots(parsed.roots)) !== JSON.stringify(normalizedRoots(roots)))
30
+ return null;
31
+ return {
32
+ version: 1,
33
+ roots: parsed.roots,
34
+ astroshotDirs: Array.isArray(parsed.astroshotDirs) ? parsed.astroshotDirs : [],
35
+ arrivalOrder: Array.isArray(parsed.arrivalOrder) ? parsed.arrivalOrder : [],
36
+ hashes: parsed.hashes && typeof parsed.hashes === "object" ? parsed.hashes : {},
37
+ updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : "",
38
+ fullScanAt: typeof parsed.fullScanAt === "string" ? parsed.fullScanAt : undefined,
39
+ };
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ export async function saveIndex(document, filePath = indexCachePath()) {
46
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true });
47
+ const temp = `${filePath}.${process.pid}.tmp`;
48
+ await fs.promises.writeFile(temp, JSON.stringify(document));
49
+ await fs.promises.rename(temp, filePath);
50
+ }
51
+ /**
52
+ * Keep known paths in their prior relative order; new paths go first, sorted
53
+ * newest capture first. Paths that vanished are dropped.
54
+ */
55
+ export function reconcileArrivalOrder(previous, shots) {
56
+ const present = new Map(shots.map((shot) => [shot.path, shot.capturedAt]));
57
+ const kept = previous.filter((entry) => present.has(entry));
58
+ const known = new Set(kept);
59
+ const fresh = shots
60
+ .filter((shot) => !known.has(shot.path))
61
+ .sort((a, b) => b.capturedAt - a.capturedAt)
62
+ .map((shot) => shot.path);
63
+ return [...fresh, ...kept];
64
+ }
@@ -0,0 +1,41 @@
1
+ import type { Chapter, FeatureStatus } from "./model.js";
2
+ export interface ManifestChapter {
3
+ slug?: string;
4
+ title?: string;
5
+ t_ms?: number;
6
+ tMs?: number;
7
+ }
8
+ export interface ManifestShot {
9
+ id?: string;
10
+ file?: string;
11
+ slug?: string;
12
+ title?: string;
13
+ description?: string;
14
+ captured_at?: string;
15
+ url?: string;
16
+ viewport?: unknown;
17
+ kind?: string;
18
+ video?: string;
19
+ poster?: string;
20
+ duration_ms?: number;
21
+ source?: string;
22
+ chapters?: ManifestChapter[];
23
+ }
24
+ export interface FeatureManifest {
25
+ version?: number;
26
+ feature?: string;
27
+ run_id?: string;
28
+ status?: string;
29
+ description?: string;
30
+ shots?: ManifestShot[];
31
+ }
32
+ export declare function readManifest(featureDir: string): Promise<FeatureManifest | null>;
33
+ /** First match wins: file, poster, id (sequence), slug. */
34
+ export declare function matchManifestShot(manifest: FeatureManifest | null, fileName: string): ManifestShot | null;
35
+ export declare function parseFeatureStatus(raw: string | undefined | null): FeatureStatus | null;
36
+ export declare function parseIsoDate(value: string | undefined | null): number | null;
37
+ export declare function chaptersOf(entry: ManifestShot | null): Chapter[];
38
+ /** `2.8s` under a minute, `m:ss` above; null when unknown. */
39
+ export declare function durationLabel(durationMs: number | null | undefined): string | null;
40
+ /** Chapter timestamp label used by the detail card. */
41
+ export declare function chapterTimeLabel(tMs: number | undefined): string;
@@ -0,0 +1,105 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { sequenceAndSlug } from "./paths.js";
4
+ function isRecord(value) {
5
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6
+ }
7
+ export async function readManifest(featureDir) {
8
+ let raw;
9
+ try {
10
+ raw = await fs.promises.readFile(path.join(featureDir, "manifest.json"), "utf8");
11
+ }
12
+ catch {
13
+ return null;
14
+ }
15
+ try {
16
+ const parsed = JSON.parse(raw);
17
+ if (!isRecord(parsed))
18
+ return null;
19
+ const manifest = parsed;
20
+ if (manifest.shots !== undefined && !Array.isArray(manifest.shots))
21
+ manifest.shots = [];
22
+ return manifest;
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ /** First match wins: file, poster, id (sequence), slug. */
29
+ export function matchManifestShot(manifest, fileName) {
30
+ const shots = manifest?.shots ?? [];
31
+ if (shots.length === 0)
32
+ return null;
33
+ const { sequence, slug } = sequenceAndSlug(fileName);
34
+ return (shots.find((shot) => shot.file === fileName) ??
35
+ shots.find((shot) => shot.poster === fileName) ??
36
+ (sequence ? shots.find((shot) => shot.id === sequence) : undefined) ??
37
+ shots.find((shot) => shot.slug === slug) ??
38
+ null);
39
+ }
40
+ export function parseFeatureStatus(raw) {
41
+ switch ((raw ?? "").toLowerCase()) {
42
+ case "running":
43
+ case "run":
44
+ case "in_progress":
45
+ case "in-progress":
46
+ return "running";
47
+ case "pass":
48
+ case "passed":
49
+ case "ok":
50
+ case "success":
51
+ return "pass";
52
+ case "fail":
53
+ case "failed":
54
+ case "error":
55
+ return "fail";
56
+ case "idle":
57
+ case "pending":
58
+ return "idle";
59
+ default:
60
+ return null;
61
+ }
62
+ }
63
+ export function parseIsoDate(value) {
64
+ if (!value)
65
+ return null;
66
+ const time = Date.parse(value);
67
+ return Number.isFinite(time) ? time : null;
68
+ }
69
+ export function chaptersOf(entry) {
70
+ if (!entry?.chapters || !Array.isArray(entry.chapters))
71
+ return [];
72
+ return entry.chapters.filter(isRecord).map((chapter) => ({
73
+ slug: typeof chapter.slug === "string" ? chapter.slug : undefined,
74
+ title: typeof chapter.title === "string" ? chapter.title : undefined,
75
+ tMs: typeof chapter.t_ms === "number"
76
+ ? chapter.t_ms
77
+ : typeof chapter.tMs === "number"
78
+ ? chapter.tMs
79
+ : undefined,
80
+ }));
81
+ }
82
+ /** `2.8s` under a minute, `m:ss` above; null when unknown. */
83
+ export function durationLabel(durationMs) {
84
+ if (durationMs === null || durationMs === undefined || durationMs <= 0)
85
+ return null;
86
+ const seconds = durationMs / 1000;
87
+ if (seconds < 60) {
88
+ const rounded = Math.round(seconds * 10) / 10;
89
+ return Number.isInteger(rounded) ? `${rounded}s` : `${rounded.toFixed(1)}s`;
90
+ }
91
+ const minutes = Math.floor(seconds / 60);
92
+ const rest = Math.floor(seconds % 60);
93
+ return `${minutes}:${String(rest).padStart(2, "0")}`;
94
+ }
95
+ /** Chapter timestamp label used by the detail card. */
96
+ export function chapterTimeLabel(tMs) {
97
+ if (tMs === undefined || tMs < 0)
98
+ return "—";
99
+ const seconds = tMs / 1000;
100
+ if (seconds < 60)
101
+ return `${seconds.toFixed(1)}s`;
102
+ const minutes = Math.floor(seconds / 60);
103
+ const rest = Math.floor(seconds % 60);
104
+ return `${minutes}:${String(rest).padStart(2, "0")}`;
105
+ }
@@ -0,0 +1,96 @@
1
+ export type FeatureStatus = "running" | "pass" | "fail" | "idle";
2
+ export interface Chapter {
3
+ slug?: string;
4
+ title?: string;
5
+ tMs?: number;
6
+ }
7
+ export interface ReviewComment {
8
+ id: string;
9
+ body: string;
10
+ createdAt: string;
11
+ }
12
+ export type ReviewState = "seen" | "pending";
13
+ export interface ReviewSnapshot {
14
+ state: ReviewState;
15
+ /** Raw decision from disk, before hash scoping. */
16
+ decision: string | null;
17
+ hashMatches: boolean;
18
+ /** A decision exists but the bytes changed since it was recorded. */
19
+ isStale: boolean;
20
+ comments: ReviewComment[];
21
+ reviewedAt: string | null;
22
+ }
23
+ export interface Shot {
24
+ /** Absolute image path; stable identity. */
25
+ id: string;
26
+ path: string;
27
+ fileName: string;
28
+ worktreePath: string;
29
+ worktree: string;
30
+ worktreeShort: string;
31
+ feature: string;
32
+ featureDir: string;
33
+ sequence: string | null;
34
+ slug: string;
35
+ title: string;
36
+ description: string;
37
+ url: string | null;
38
+ runId: string | null;
39
+ status: FeatureStatus | null;
40
+ /** Epoch milliseconds. */
41
+ capturedAt: number;
42
+ mtimeMs: number;
43
+ isMovie: boolean;
44
+ videoFileName: string | null;
45
+ /** Absolute path when the video exists on disk. */
46
+ videoPath: string | null;
47
+ durationMs: number | null;
48
+ source: string | null;
49
+ chapters: Chapter[];
50
+ review: ReviewSnapshot | null;
51
+ }
52
+ export interface FrictionStep {
53
+ id: string;
54
+ step: number;
55
+ stepId: string;
56
+ title: string;
57
+ description: string;
58
+ transcript: string;
59
+ /** Absolute paths that exist on disk. */
60
+ screenshots: string[];
61
+ good: string[];
62
+ improve: string[];
63
+ url: string | null;
64
+ capturedAt: string | null;
65
+ }
66
+ export interface FrictionRun {
67
+ runId: string;
68
+ directory: string;
69
+ logPath: string | null;
70
+ capturedAt: number;
71
+ status: string | null;
72
+ steps: FrictionStep[];
73
+ review: ReviewSnapshot | null;
74
+ }
75
+ export interface FrictionLog {
76
+ /** `<worktreePath>::<slug>` */
77
+ id: string;
78
+ slug: string;
79
+ directory: string;
80
+ worktreePath: string;
81
+ worktree: string;
82
+ worktreeShort: string;
83
+ title: string;
84
+ description: string;
85
+ status: string | null;
86
+ updatedAt: number;
87
+ promptPath: string | null;
88
+ runs: FrictionRun[];
89
+ }
90
+ export interface AstroshotTree {
91
+ astroshotDir: string;
92
+ worktreePath: string;
93
+ worktree: string;
94
+ shots: Shot[];
95
+ frictionLogs: FrictionLog[];
96
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ export declare const ASTROSHOT_DIR = ".astroshot";
2
+ export declare const FRICTION_DIR = "friction-logs";
3
+ /** Directories the scanner never descends into (mirrors the macOS app). */
4
+ export declare const SKIP_DIRECTORIES: Set<string>;
5
+ export declare const MAX_SCAN_DEPTH = 10;
6
+ export declare const IMAGE_EXTENSIONS: Set<string>;
7
+ export declare const VIDEO_EXTENSIONS: string[];
8
+ export declare function extensionOf(fileName: string): string;
9
+ export declare function isImageFile(fileName: string): boolean;
10
+ export interface ShotPath {
11
+ worktreePath: string;
12
+ worktree: string;
13
+ feature: string;
14
+ featureDir: string;
15
+ fileName: string;
16
+ }
17
+ /** Accept only `<worktree>/.astroshot/<feature>/<image>`; friction logs are excluded. */
18
+ export declare function parseShotPath(imagePath: string): ShotPath | null;
19
+ export declare function sequenceAndSlug(fileName: string): {
20
+ sequence: string | null;
21
+ slug: string;
22
+ };
23
+ export declare function humanize(slug: string): string;
24
+ export declare function worktreeShort(name: string): string;
25
+ export declare function isInsideFrictionLogs(filePath: string): boolean;
26
+ export declare function frictionLogsDir(astroshotDir: string): string;
27
+ export declare function abbreviateHome(filePath: string, home: string): string;
@@ -0,0 +1,98 @@
1
+ import path from "node:path";
2
+ export const ASTROSHOT_DIR = ".astroshot";
3
+ export const FRICTION_DIR = "friction-logs";
4
+ /** Directories the scanner never descends into (mirrors the macOS app). */
5
+ export const SKIP_DIRECTORIES = new Set([
6
+ "node_modules",
7
+ ".git",
8
+ "DerivedData",
9
+ "build",
10
+ ".build",
11
+ "Pods",
12
+ ".next",
13
+ "dist",
14
+ "out",
15
+ "target",
16
+ "vendor",
17
+ "Checkouts",
18
+ "xcuserdata",
19
+ ".turbo",
20
+ ".cache",
21
+ "coverage",
22
+ "tmp",
23
+ ".pnpm-store",
24
+ "Carthage",
25
+ "bazel-bin",
26
+ "bazel-out",
27
+ "bazel-testlogs",
28
+ ".gradle",
29
+ ]);
30
+ export const MAX_SCAN_DEPTH = 10;
31
+ export const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "gif"]);
32
+ export const VIDEO_EXTENSIONS = ["webm", "mp4", "mov", "m4v"];
33
+ export function extensionOf(fileName) {
34
+ return path.extname(fileName).slice(1).toLowerCase();
35
+ }
36
+ export function isImageFile(fileName) {
37
+ return IMAGE_EXTENSIONS.has(extensionOf(fileName));
38
+ }
39
+ /** Accept only `<worktree>/.astroshot/<feature>/<image>`; friction logs are excluded. */
40
+ export function parseShotPath(imagePath) {
41
+ const fileName = path.basename(imagePath);
42
+ if (!isImageFile(fileName))
43
+ return null;
44
+ const featureDir = path.dirname(imagePath);
45
+ const feature = path.basename(featureDir);
46
+ if (!feature || feature === ASTROSHOT_DIR || feature === FRICTION_DIR)
47
+ return null;
48
+ const astroshotDir = path.dirname(featureDir);
49
+ if (path.basename(astroshotDir) !== ASTROSHOT_DIR)
50
+ return null;
51
+ if (imagePath.split(path.sep).includes(FRICTION_DIR))
52
+ return null;
53
+ const worktreePath = path.dirname(astroshotDir);
54
+ return {
55
+ worktreePath,
56
+ worktree: path.basename(worktreePath),
57
+ feature,
58
+ featureDir,
59
+ fileName,
60
+ };
61
+ }
62
+ export function sequenceAndSlug(fileName) {
63
+ const stem = fileName.replace(/\.[^.]+$/, "");
64
+ const dash = stem.indexOf("-");
65
+ if (dash > 0) {
66
+ const head = stem.slice(0, dash);
67
+ if (/^\d+$/.test(head))
68
+ return { sequence: head, slug: stem.slice(dash + 1) };
69
+ }
70
+ return { sequence: null, slug: stem };
71
+ }
72
+ export function humanize(slug) {
73
+ return slug
74
+ .split(/[-_]+/)
75
+ .filter(Boolean)
76
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
77
+ .join(" ");
78
+ }
79
+ export function worktreeShort(name) {
80
+ const match = /wt\d+/.exec(name);
81
+ if (match)
82
+ return match[0];
83
+ return name.length <= 8 ? name : name.slice(0, 6);
84
+ }
85
+ export function isInsideFrictionLogs(filePath) {
86
+ const parts = filePath.split(path.sep);
87
+ const index = parts.indexOf(ASTROSHOT_DIR);
88
+ return index !== -1 && parts[index + 1] === FRICTION_DIR;
89
+ }
90
+ export function frictionLogsDir(astroshotDir) {
91
+ return path.join(astroshotDir, FRICTION_DIR);
92
+ }
93
+ export function abbreviateHome(filePath, home) {
94
+ if (filePath === home)
95
+ return "~";
96
+ const prefix = home.endsWith(path.sep) ? home : `${home}${path.sep}`;
97
+ return filePath.startsWith(prefix) ? `~${path.sep}${filePath.slice(prefix.length)}` : filePath;
98
+ }
@@ -0,0 +1,67 @@
1
+ import type { ReviewComment, ReviewSnapshot } from "./model.js";
2
+ export declare const REVIEW_FILE = "review.json";
3
+ export interface ReviewEntry {
4
+ decision?: string;
5
+ reviewed_at?: string;
6
+ image_sha256?: string;
7
+ comments?: ReviewComment[];
8
+ }
9
+ export interface ReviewDocument {
10
+ version: number;
11
+ run_id?: string;
12
+ updated_at?: string;
13
+ reviews: Record<string, StoredEntry>;
14
+ }
15
+ interface StoredComment {
16
+ id: string;
17
+ body: string;
18
+ created_at: string;
19
+ }
20
+ interface StoredEntry {
21
+ decision?: string;
22
+ reviewed_at?: string;
23
+ image_sha256?: string;
24
+ comments?: StoredComment[];
25
+ }
26
+ export declare class UnsupportedReviewVersion extends Error {
27
+ constructor(version: unknown);
28
+ }
29
+ export declare function emptyDocument(): ReviewDocument;
30
+ /** Missing file → empty document. Malformed JSON → null (treated as unreadable). */
31
+ export declare function readReviewDocument(directory: string): Promise<ReviewDocument | null>;
32
+ export declare function parseReviewDocument(raw: string): ReviewDocument | null;
33
+ export declare function sha256File(filePath: string): Promise<string>;
34
+ export declare function sha256Bytes(bytes: Buffer): string;
35
+ /** The entry that applies to `fileName`, or null when the run gate rejects it. */
36
+ export declare function scopedEntry(document: ReviewDocument, fileName: string, expectedRunId: string | null): StoredEntry | null;
37
+ /** Whether validating this entry needs the image's current hash. */
38
+ export declare function entryNeedsHash(entry: StoredEntry | null): boolean;
39
+ /**
40
+ * The seen/stale truth table from the app's `ReviewSnapshot`:
41
+ * hash mismatch hides the decision but keeps comments and flags staleness.
42
+ */
43
+ export declare function snapshotFromEntry(entry: StoredEntry | null, currentSha256: string | null): ReviewSnapshot;
44
+ export declare function nowIso(date?: Date): string;
45
+ export declare function newCommentId(): string;
46
+ export declare function serializeReviewDocument(document: ReviewDocument): string;
47
+ export declare function writeReviewDocument(directory: string, document: ReviewDocument): Promise<void>;
48
+ /** A run-id change starts a fresh review map, exactly like the app. */
49
+ export declare function resetReviewsIfNeeded(document: ReviewDocument, runId: string | null): void;
50
+ export interface ReviewWriteRequest {
51
+ /** Directory holding review.json (feature dir, or friction run dir). */
52
+ directory: string;
53
+ /** Key inside `reviews` (image file name, or `log.jsonl`). */
54
+ fileName: string;
55
+ runId: string | null;
56
+ /** Absolute path of the bytes to hash when marking seen. */
57
+ targetPath: string;
58
+ }
59
+ export declare function markSeen(request: ReviewWriteRequest, options?: {
60
+ comment?: string;
61
+ now?: Date;
62
+ }): Promise<ReviewSnapshot>;
63
+ export declare function addComment(request: ReviewWriteRequest, body: string, options?: {
64
+ currentSha256?: string | null;
65
+ now?: Date;
66
+ }): Promise<ReviewSnapshot>;
67
+ export {};