import type { CritiqueCoverage, CritiqueFinding, CritiqueTile } from "./critique.js"; import type { PageReviewBudgetCoverage, PageReviewCapturePlan, PageReviewEvidenceContext, PageReviewNativeTile, PageReviewRect } from "./page-review-contracts.js"; import type { QaContext, QaSignature } from "./qa-plan.js"; export type { PageReviewRect } from "./page-review-contracts.js"; export declare const PAGE_REVIEW_PACK_SCHEMA = "harnery-page-review/v2"; export declare const PAGE_REVIEW_FINDINGS_SCHEMA = "harnery-page-review-findings/v3"; /** Directory name a qa-run gives its pack inside the run directory. */ export declare const PAGE_REVIEW_PACK_DIRNAME = "pack"; export declare const PAGE_REVIEW_MANIFEST_FILENAME = "manifest.json"; export declare const PAGE_REVIEW_REVIEW_FILENAME = "review.md"; export declare const PAGE_REVIEW_FINDINGS_FILENAME = "findings.json"; export declare const PAGE_REVIEW_FINDINGS_SCHEMA_FILENAME = "findings.schema.json"; /** How long a pack lives after its judge (or its capture, when no judge * runs) before the whole directory is deleted. Ruled 2026-09-02. */ export declare const PAGE_REVIEW_DEFAULT_RETENTION_MINUTES = 90; /** The only file left behind when an expired pack is deleted. */ export declare const PAGE_REVIEW_EXPIRED_STUB_FILENAME = "pack-expired.json"; /** Reviewer dispositions a machine finding can carry in `findings.json`. */ export declare const PAGE_REVIEW_DISPOSITIONS: readonly ["confirmed", "artifact", "not-a-defect", "duplicate-of-gate"]; export type PageReviewDisposition = (typeof PAGE_REVIEW_DISPOSITIONS)[number]; /** Schema id and file name of the reviewed-outcome record `review-pack verdict` writes. */ export declare const PAGE_REVIEW_VERDICT_SCHEMA = "harnery-page-review-verdict/v2"; export declare const PAGE_REVIEW_VERDICT_FILENAME = "verdict.json"; export declare const PAGE_REVIEW_FINDING_SEVERITIES: readonly ["critical", "high", "medium", "low", "info"]; export type PageReviewFindingSeverity = (typeof PAGE_REVIEW_FINDING_SEVERITIES)[number]; export declare const PAGE_REVIEW_FINDING_CATEGORIES: readonly ["layout", "typography", "contrast", "content", "image", "interaction", "accessibility", "responsiveness", "render-artifact", "coverage", "other"]; export type PageReviewFindingCategory = (typeof PAGE_REVIEW_FINDING_CATEGORIES)[number]; export declare const PAGE_REVIEW_SUBAGENT_MODELS: readonly ["GPT-5.6 Luna", "Composer 2.5", "Haiku 4.5"]; export type PageReviewSubagentModel = (typeof PAGE_REVIEW_SUBAGENT_MODELS)[number]; /** One review-subagent finding in `findings.json`. */ export interface PageReviewReviewerFinding { id: string; severity: PageReviewFindingSeverity; category: PageReviewFindingCategory; context_id: string; /** Tile ids (`T012`) or pack-relative file paths; never empty. */ evidence: string[]; observation: string; recommendation?: string; confidence?: number; } /** A reviewer's verdict on one machine finding in `evidence/critique.json`. */ export interface PageReviewDispositionRecord { /** `/#`, n = 0-based position among the tile's findings. */ target: string; disposition: PageReviewDisposition; note?: string; by?: string; at?: string; } /** One review subagent's assigned and completed primary-tile work. */ export interface PageReviewDelegatedReviewRecord { reviewer: string; model: PageReviewSubagentModel; /** `/` entries assigned to this subagent. */ assigned_tiles: string[]; /** Assigned entries the subagent actually opened at native pixels. */ completed_tiles: string[]; status: "complete" | "incomplete"; } /** The delegated-review `findings.json` document. */ export interface PageReviewFindingsDocument { schema: string; schema_path?: string; target: string; reviewer: string | null; reviewed_at: string | null; machine_evidence_digest?: string | null; delegated_reviews: PageReviewDelegatedReviewRecord[]; findings: PageReviewReviewerFinding[]; dispositions?: PageReviewDispositionRecord[]; } /** What `resolvePackVerdict` derives from the machine critique plus the reviewer's file. */ export interface PageReviewVerdict { machine_outcome: "pass" | "fail" | "skipped" | "incomplete"; reviewed_outcome: "pass" | "fail" | "skipped" | "incomplete"; /** Machine `high` findings across every judged context. */ high_total: number; high_confirmed: number; /** Highs dispositioned `artifact`, `not-a-defect`, or `duplicate-of-gate`. */ high_dismissed: number; /** Highs with no disposition; they still count against the page. */ high_open: number; /** Review-subagent findings at `critical` or `high`; each counts against the page. */ reviewer_high: number; /** Dispositions whose target matched a machine finding. */ dispositions_applied: number; /** Disposition targets that name no machine finding in the critique. */ unmatched_dispositions: string[]; /** Primary tiles the inspection plan requires. */ primary_tiles_total: number; /** Required tiles covered by a completed review-subagent record. */ primary_tiles_reviewed: number; /** Required `/` entries with no completed subagent review. */ uncovered_primary_tiles: string[]; } export interface PageReviewVerdictDocument extends PageReviewVerdict { schema: string; reviewed_at: string; } /** How a context's tiles were cut: from one full-page screenshot (the * default) or from per-band scrolled viewport captures (the fallback when the * fidelity probe proved the full-page screenshot wrong). */ export interface PageReviewCaptureFidelity { source: "full-page" | "scrolled-bands"; /** Bands re-shot by scroll-and-clip and compared with the full-page capture. */ probed: Array<{ tile_id: string; scrollY: number; height: number; mismatch_ratio: number; }>; /** Tile ids whose probe exceeded the mismatch threshold. */ mismatched: string[]; mismatch_threshold: number; } /** Pack expiry as written into the manifest. `managed` is false for a pack * written to an explicit `--out`; such a pack is never deleted automatically. */ export interface PageReviewRetention { expires_at: string; managed: boolean; } /** One tile as stored in the pack. `id` is stable within its context * (`T001`, `T002`, …) and is what a finding cites. `file` is pack-relative. */ export interface PageReviewTileRecord { id: string; /** Tiler index (position in the capture's tile list). */ index: number; label: string; /** Selector the tile was cut for; absent for full-page bands. */ scope?: string; /** `band` (full-page band), `scope` (selector tile), or `hit-band` (a band * captured past the tile cap because a gate hit lands in it). Absent on * packs written before this field existed; treat as `band`/`scope` by * whether `scope` is set. */ kind?: "band" | "scope" | "hit-band"; x: number; scrollY: number; width: number; height: number; file: string; sha256: string; bytes: number; } /** One tile region re-captured at a higher device scale factor after the * original capture (`review-pack expand`). The source tile is untouched; this * record sits beside it. `file` is pack-relative. Optional and additive on the * v1 context record. */ export interface PageReviewExpandedTileRecord { /** Source tile id (`T012`). */ tile: string; /** Device scale factor the region was rendered at (2 = twice the pixels). */ dpr: number; /** Pixel size of the expanded PNG (source tile size × dpr, clamped). */ width: number; height: number; file: string; sha256: string; bytes: number; captured_at: string; } /** The per-context contact sheet: every tile downscaled into one grid PNG * for orientation. Reading order is row-major (left to right, then top to * bottom) in tile id order; each cell carries its tile id stamped in its * label band. Optional and additive on the v1 context record. */ export interface PageReviewContactSheetRecord { file: string; sha256: string; bytes: number; width: number; height: number; columns: number; rows: number; cell_width: number; cell_height: number; order: "row-major"; } export interface PageReviewContextRecord { id: string; viewport: string; theme: "light" | "dark"; state: string; /** Rendered URL after navigation. */ url: string; title?: string; captured_at: string; page: { width: number; height: number; }; viewport_size: { width: number; height: number; }; dpr: number; recipe_version: string; full_page_scale: 0.5; capture_plan?: PageReviewCapturePlan; allocation_coverage?: PageReviewBudgetCoverage; evidence_context?: PageReviewEvidenceContext; capture_incomplete?: boolean; /** Full-page band coverage (what the tiler kept of the page). */ coverage: CritiqueCoverage; scopes: Array<{ selector: string; tiles: number; }>; /** Pack-relative file paths. `dom` ends in `.gz` (gzip) for packs written * after the footprint change and in `.html` (plain) for older packs. */ files: { full_page: string; dom: string; signature: string; tiles: string; context: string; }; dom_sha256: string; tiles: PageReviewTileRecord[]; /** Higher-DPR re-captures of single tiles, in the order they were made. */ expanded?: PageReviewExpandedTileRecord[]; /** Downscaled grid of every tile (`contacts.png`); absent for a context with no tiles. */ contact_sheet?: PageReviewContactSheetRecord; /** Where the tiles came from and what the fidelity probe saw. Absent on * packs written before the probe existed (tiles came from the full page). */ capture_fidelity?: PageReviewCaptureFidelity; /** Bands captured past the tile cap because a gate hit lands in them. */ hit_bands?: number; } /** What a capture hands the pack for one context. `tiles` are the full-page * bands; `scopeTiles` are selector tiles, one entry per selector. */ export interface PageReviewContextCapture { context: QaContext & { id?: string; }; url: string; title?: string; fullPage: Buffer; pageWidth: number; pageHeight: number; viewportSize: { width: number; height: number; }; dpr: number; recipeVersion: string; capturePlan?: PageReviewCapturePlan; allocationCoverage?: PageReviewBudgetCoverage; captureIncomplete?: boolean; tiles: CritiqueTile[]; coverage: CritiqueCoverage; scopeTiles?: Array<{ selector: string; tiles: CritiqueTile[]; }>; signature: QaSignature; domHtml: string; capturedAt?: string; /** Result of the capture-fidelity probe (capture-fidelity.ts); absent when * the capture did not probe. Copied onto the record as `capture_fidelity`. */ captureFidelity?: PageReviewCaptureFidelity; /** Bands cut past the tile cap because a gate hit lands in them (their * tiles sit in `tiles` after the kept bands, labelled `hit band N`). */ hitBands?: number; } /** One context's machine critique, as the judge records it into the pack. */ export interface PageReviewCritiqueRecord { context_id: string; provider: string; tiles_total: number; tiles_reviewed: number; tiles_reused: number; outcome: "pass" | "fail" | "skipped" | "incomplete"; findings: Array; coverage: CritiqueCoverage; error?: string; evidence?: { schema: string; context: PageReviewEvidenceContext; tiles: Array>; }; } /** One gate finding that carries a document-space rectangle (CSS px at the * capture's device scale factor 1, the same space as tile `x`/`scrollY`). */ export interface PageReviewGateHit { /** Check family the hit came from: runts, contrast, truncation, clip, … */ rule: string; /** Short locator: the element label plus the check's own detail. */ label: string; rect: PageReviewRect; } export interface PageReviewGateRecord { context_id: string; check_id: string; outcome: "passed" | "failed" | "unknown"; failures: string[]; /** Rectangles the gate's envelope recorded; optional and additive. */ hits?: PageReviewGateHit[]; } export interface PageReviewInspectionGateHit extends PageReviewGateHit { check_id: string; /** Tile ids whose rect intersects the hit; empty when no tile covers it. */ tiles: string[]; } export interface PageReviewInspectionPlan { schema: string; purpose: string; contexts: Array<{ context_id: string; full_page: string; /** Tiles review subagents must open for a complete review; everything else is drill-down. */ primary_tiles: Array<{ id: string; file: string; reason: string; }>; drilldown_tiles: number; /** Every gate hit with a rectangle, mapped to the tiles that show it. */ gate_hits: PageReviewInspectionGateHit[]; }>; } export interface PageReviewPackManifest { schema: string; created_at: string; target: string; tested_revision?: string; tool: { name: string; version?: string; }; contexts: PageReviewContextRecord[]; gates: PageReviewGateRecord[]; critique: PageReviewCritiqueRecord[] | null; pool?: { concurrency: number; wall_time_ms: number; provider: string; }; not_checked: Array<{ check: string; reason: string; }>; warnings: string[]; /** Pack expiry; absent until the writer knows when the judge finished. */ retention?: PageReviewRetention; /** Bytes on disk across every file in the pack at finalize time. */ size_bytes?: number; files: { review: string; findings: string; findings_schema: string; inspection_plan: string; coverage: string; index: string; inventory: string; critique: string; }; } export declare function packPaths(packDir: string): { dir: string; manifest: string; review: string; findings: string; findingsSchema: string; evidenceDir: string; inspectionPlan: string; coverage: string; index: string; inventory: string; critique: string; verdict: string; contextsDir: string; contextDir: (contextId: string) => string; }; export declare function sha256Hex(buffer: Buffer | string): string; export declare function tileId(position: number): string; /** Deterministic file name for an expanded tile: `T012@2x.png`. */ export declare function expandedTileFilename(tileId: string, dpr: number): string; /** * Crop one region out of a PNG buffer in pixel space. The rect is clamped to * the image so an off-by-one never throws; the result is at least 1×1. */ export declare function cropPngRegion(buffer: Buffer, rect: { x: number; y: number; width: number; height: number; }): { png: Buffer; width: number; height: number; }; /** * Locate one tile across a pack's contexts. With `contextId` the lookup is * exact; without it the tile id must be unique across the given contexts, * otherwise the caller has to name the context. */ export declare function findPackTile(contexts: PageReviewContextRecord[], tileId: string, contextId?: string): { context: PageReviewContextRecord; tile: PageReviewTileRecord; }; /** * Write one tile region re-rendered at `dpr` into an existing context: * `tiles/@x.png` cropped from `fullPage` (a screenshot of the same * page at that device scale factor, so the source tile's CSS-pixel rect is * multiplied by `dpr`). Existing tiles are never touched; the context record * gains or replaces one `expanded` entry for that tile + dpr and is rewritten. */ export declare function writePackExpandedTile(packDir: string, contextId: string, input: { tileId: string; dpr: number; capturedAt?: string; } & ({ fullPage: Buffer; region?: undefined; } | { fullPage?: undefined; /** The tile's region already rendered at `dpr` (a scrolled viewport * capture, `Browser.captureRegionByScroll`); written as is. */ region: Buffer; })): { record: PageReviewContextRecord; expanded: PageReviewExpandedTileRecord; }; /** * Write one context's capture into the pack: `full-page.png`, `dom.html.gz`, * `signature.json`, `tiles/T001.png…`, `tiles.json`, and `context.json`. * Full-page bands come first, then each scope's tiles in selector order, so * tile ids are stable for a given capture. Returns the record written. */ export declare function writePackContext(packDir: string, capture: PageReviewContextCapture): PageReviewContextRecord; export declare const CONTACT_SHEET_FILENAME = "contacts.png"; /** Tiles per row. Four 320 px cells plus gutters stay under a 1600 px sheet. */ export declare const CONTACT_SHEET_COLUMNS = 4; /** * Build one contact sheet from tile PNGs, in the order given: a fixed grid of * `CONTACT_SHEET_COLUMNS` cells per row, each cell a label band stamped with * the tile id above the tile downscaled (box filter, aspect kept, never * upscaled) to fit the cell. Row-major reading order. */ export declare function buildContactSheet(tiles: Array<{ id: string; png: Buffer; }>): { png: Buffer; width: number; height: number; columns: number; rows: number; cell_width: number; cell_height: number; }; /** * Write (or rewrite) a context's `contacts.png` from its tiles and record it * on the context. Tile bytes are read from the pack unless supplied. A * context with no tiles gets no sheet and its record is returned unchanged. */ export declare function writePackContactSheet(packDir: string, record: PageReviewContextRecord, tilePngs?: Map): PageReviewContextRecord; /** Context ids present in the pack, in directory order. */ export declare function listPackContexts(packDir: string): string[]; export declare function readPackContext(packDir: string, contextId: string): PageReviewContextRecord; /** * Load a context's tiles back as `CritiqueTile`s (PNG bytes read from disk * and base64-encoded), verifying each file against its recorded digest so a * judge never reviews a tile that was altered after capture. */ export declare function readPackTiles(packDir: string, record: PageReviewContextRecord): Array; /** Half-scale navigation image; never native evidence. */ export declare function readPackOverview(packDir: string, record: PageReviewContextRecord): Buffer; export declare function readPackSignature(packDir: string, record: PageReviewContextRecord): QaSignature; /** Read a context's serialized DOM, inflating a gzip-compressed `dom.html.gz` * and reading an older plain `dom.html` as-is. */ export declare function readPackDom(packDir: string, record: PageReviewContextRecord): string; export declare function readPackManifest(packDir: string): PageReviewPackManifest; /** Read the bounded primary-tile plan used to prove delegated review coverage. */ export declare function readPackInspectionPlan(packDir: string): PageReviewInspectionPlan; export declare function readPackFindings(packDir: string): PageReviewFindingsDocument; /** Write `findings.json` atomically: a temp file beside it, then a rename. */ export declare function writePackFindings(packDir: string, doc: PageReviewFindingsDocument): void; /** * Check a findings document against the rules `findingsSchemaDocument()` * states, by hand so the toolkit tier carries no validator dependency. * Returns every violation found; an empty array means the document is valid. */ export declare function validateFindingsDocument(doc: unknown): string[]; /** `/#` for the n-th finding of a tile in one context. */ export declare function dispositionTarget(contextId: string, tileId: string, n: number): string; /** * Every machine finding in the critique, keyed by its disposition target. * `n` counts a tile's findings in critique order, all severities included, so * a reviewer can point at the second finding on T006 as `ctx/T006#1`. */ export declare function machineFindingTargets(critique: PageReviewCritiqueRecord[] | null): Map; /** * Combine the immutable machine critique with the reviewer's dispositions and * findings into one reviewed outcome. A machine `high` counts against the page * unless a reviewer dispositioned it `artifact`, `not-a-defect`, or * `duplicate-of-gate`; `confirmed` and undispositioned highs count; so does a * review-subagent finding at `critical` or `high`. Nothing here rewrites the * critique: `evidence/critique.json` stays the machine record. */ export declare function resolvePackVerdict(manifest: Pick & Partial>, inspectionPlan: PageReviewInspectionPlan, findings: PageReviewFindingsDocument): PageReviewVerdict; /** The verdict a previous `review-pack verdict` wrote, or undefined when none exists. */ export declare function readPackVerdict(packDir: string): PageReviewVerdictDocument | undefined; export interface FinalizePageReviewPackInput { packDir: string; target: string; tested_revision?: string; contexts: PageReviewContextRecord[]; gates?: PageReviewGateRecord[]; /** Null or absent until the judge has run. */ critique?: PageReviewCritiqueRecord[] | null; pool?: { concurrency: number; wall_time_ms: number; provider: string; }; not_checked?: Array<{ check: string; reason: string; }>; warnings?: string[]; tool?: { name: string; version?: string; }; /** Command the reader can run to judge the pack, shown in review.md. */ judgeCommand?: string; createdAt?: string; /** Pack expiry to record; omit to keep whatever the manifest already says. */ retention?: PageReviewRetention; } export declare function findingsSchemaDocument(): Record; /** Most hits one envelope contributes; a runaway sweep must not bloat the pack. */ export declare const GATE_HITS_PER_ENVELOPE = 50; /** * Pull every rectangle-bearing finding out of a `browse` JSON envelope so the * inspection plan can point at the tiles that show it. Covers the checks * whose results carry a document-space rect: runts, truncation, contrast, * placeholder, image, clip, overlap, crowd, align, gap, overflow, and * target-size (`hit`). Anything without a rect is skipped; the gate's * `failures` lines still describe it. Capped at `GATE_HITS_PER_ENVELOPE`. */ export declare function gateHitsFromEnvelope(envelope: Record | undefined, maxHits?: number): PageReviewGateHit[]; /** Tile ids whose rect intersects `rect` (both in document-space px). */ export declare function tilesCoveringRect(tiles: PageReviewTileRecord[], rect: PageReviewRect): string[]; export declare function buildInspectionPlan(contexts: PageReviewContextRecord[], critique: PageReviewCritiqueRecord[] | null | undefined, gates?: PageReviewGateRecord[]): PageReviewInspectionPlan; /** * Write the pack's navigation layer: manifest, evidence files, the findings * skeleton and schema, and `review.md`. An unchanged judge result preserves * the review; changed machine evidence archives it before starting a fresh * review, so a previous finding position cannot acquire a new meaning. */ export declare function finalizePageReviewPack(input: FinalizePageReviewPackInput): { manifest: string; review: string; }; export declare function renderReviewMarkdown(manifest: PageReviewPackManifest, plan: PageReviewInspectionPlan, judgeCommand?: string, verdict?: PageReviewVerdictDocument): string; /** Schema of the stub left behind when an expired pack is deleted. */ export declare const PAGE_REVIEW_EXPIRED_SCHEMA = "harnery-page-review-expired/v1"; /** What remains of a deleted pack: enough for a result document's * `review_pack.dir` to explain itself without the evidence. */ export interface PageReviewExpiredStub { schema: typeof PAGE_REVIEW_EXPIRED_SCHEMA; target: string; created_at: string; expires_at: string; deleted_at: string; /** Contexts the pack held before deletion. */ contexts: number; /** Aggregate of the manifest's critique outcomes; null when no judge ran. */ machine_outcome: PageReviewCritiqueRecord["outcome"] | null; result: PageReviewVerdictDocument | null; findings_summary: { machine_high: number; machine_medium: number; machine_low: number; reviewer_findings: number; dispositions: number; } | null; } /** One pack as seen by the expiry sweep. `expires_at`, `size_bytes`, and * `managed` are null when the manifest does not carry them; such a pack is * never deleted. */ export interface PageReviewPackRow { dir: string; target: string; created_at: string; expires_at: string | null; size_bytes: number | null; managed: boolean | null; /** `retention.expires_at` is in the past (at the sweep's `now`). */ expired: boolean; } export interface DeleteExpiredPacksInput { /** Directories to search; see `findPageReviewPacks` for the shape searched. */ roots: string[]; /** Sweep clock (default: the wall clock). */ now?: Date; /** Also delete packs whose manifest says `managed: false` (an explicit * `--out`). Default false: only the store's own packs are touched. */ includeUnmanaged?: boolean; /** Report without touching the filesystem. */ dryRun: boolean; } export interface DeleteExpiredPacksResult { /** Every pack found under the roots, deletable or not. */ candidates: PageReviewPackRow[]; /** Packs removed by this call; under `dryRun`, the packs that would be. */ deleted: PageReviewPackRow[]; } /** * Every page review pack under the given roots. A pack is a directory whose * `manifest.json` carries the pack schema. Each root is searched as: the root * itself, each immediate child workspace, and `run-/pack` under the root and * under each workspace (the layout of the managed artifact store, where a * `review-pack` workspace is itself the pack and a `qa-run` workspace holds * one pack per run). Nothing deeper is walked. */ export declare function findPageReviewPacks(roots: string[]): string[]; /** Whether the sweep may delete this pack: expired, and either managed by the * store or explicitly included. A pack without retention never qualifies. */ export declare function isPackDeletable(row: PageReviewPackRow, includeUnmanaged?: boolean): boolean; /** One verdict for the whole pack from its per-context critique rows: any * fail is a fail, otherwise the weakest non-pass outcome, otherwise pass. */ export declare function aggregateMachineOutcome(critique: PageReviewCritiqueRecord[] | null | undefined): PageReviewCritiqueRecord["outcome"] | null; export declare function deleteExpiredPacks(input: DeleteExpiredPacksInput): DeleteExpiredPacksResult; //# sourceMappingURL=page-review-pack.d.ts.map