harnery 0.38.0 → 0.40.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/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +32 -8
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +97 -6
- package/dist/commands/backup.d.ts +16 -0
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +239 -33
- package/dist/commands/browse.d.ts +8 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +466 -231
- package/dist/commands/diagnostics.d.ts.map +1 -1
- package/dist/commands/diagnostics.js +6 -3
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +20 -4
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +26 -3
- package/dist/commands/qa-status.d.ts.map +1 -1
- package/dist/commands/qa-status.js +7 -2
- package/dist/commands/resources.d.ts.map +1 -1
- package/dist/commands/resources.js +23 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +98 -23
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +2 -2
- package/dist/core/adapters/profiles.d.ts +1 -1
- package/dist/core/agents/canonical-emit.d.ts +3 -2
- package/dist/core/agents/canonical-emit.d.ts.map +1 -1
- package/dist/core/agents/canonical-emit.js +2 -0
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +5 -3
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -2
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.d.ts +7 -5
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +63 -8
- package/dist/core/agents/session-finalizer-v3.d.ts +2 -2
- package/dist/core/agents/session-finalizer-v3.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -3
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +2 -1
- package/dist/core/agents/state/names.d.ts +35 -2
- package/dist/core/agents/state/names.d.ts.map +1 -1
- package/dist/core/agents/state/names.js +80 -5
- package/dist/core/agents/state/stale-sweep.d.ts +2 -1
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +23 -6
- package/dist/core/agents/turn-elapsed.d.ts +51 -0
- package/dist/core/agents/turn-elapsed.d.ts.map +1 -0
- package/dist/core/agents/turn-elapsed.js +91 -0
- package/dist/core/artifacts/activity.d.ts +14 -0
- package/dist/core/artifacts/activity.d.ts.map +1 -0
- package/dist/core/artifacts/activity.js +52 -0
- package/dist/core/artifacts/delivery-card.d.ts +39 -0
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -0
- package/dist/core/artifacts/delivery-card.js +282 -0
- package/dist/core/artifacts/index.d.ts +42 -7
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +288 -76
- package/dist/core/artifacts/mutation-lock.d.ts +2 -0
- package/dist/core/artifacts/mutation-lock.d.ts.map +1 -0
- package/dist/core/artifacts/mutation-lock.js +91 -0
- package/dist/core/backup/host-snapshot.d.ts +18 -0
- package/dist/core/backup/host-snapshot.d.ts.map +1 -0
- package/dist/core/backup/host-snapshot.js +64 -0
- package/dist/core/config.d.ts +20 -5
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +44 -10
- package/dist/core/diagnostics/advice.d.ts +38 -3
- package/dist/core/diagnostics/advice.d.ts.map +1 -1
- package/dist/core/diagnostics/advice.js +103 -99
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +111 -47
- package/dist/core/diagnostics/comparison.d.ts.map +1 -1
- package/dist/core/diagnostics/comparison.js +3 -2
- package/dist/core/diagnostics/contract.d.ts +15 -31
- package/dist/core/diagnostics/contract.d.ts.map +1 -1
- package/dist/core/diagnostics/contract.js +2 -5
- package/dist/core/diagnostics/index.d.ts +2 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -1
- package/dist/core/diagnostics/index.js +2 -0
- package/dist/core/diagnostics/pressure-contract.d.ts +260 -0
- package/dist/core/diagnostics/pressure-contract.d.ts.map +1 -0
- package/dist/core/diagnostics/pressure-contract.js +70 -0
- package/dist/core/diagnostics/pressure.d.ts +25 -0
- package/dist/core/diagnostics/pressure.d.ts.map +1 -0
- package/dist/core/diagnostics/pressure.js +1138 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -1
- package/dist/core/diagnostics/replay.js +90 -10
- package/dist/core/events/v3/adapter-id.d.ts +6 -0
- package/dist/core/events/v3/adapter-id.d.ts.map +1 -0
- package/dist/core/events/v3/adapter-id.js +7 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -1
- package/dist/core/events/v3/archive-retention.js +3 -2
- package/dist/core/events/v3/bootstrap.d.ts +41 -4
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +120 -17
- package/dist/core/events/v3/capabilities.d.ts +16 -9
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +37 -2
- package/dist/core/events/v3/capability-drift.d.ts +2 -2
- package/dist/core/events/v3/capability-drift.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.d.ts +48 -5
- package/dist/core/events/v3/control-witness.d.ts.map +1 -1
- package/dist/core/events/v3/control-witness.js +91 -36
- package/dist/core/events/v3/control.d.ts.map +1 -1
- package/dist/core/events/v3/control.js +22 -2
- package/dist/core/events/v3/index.d.ts +2 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +2 -1
- package/dist/core/events/v3/live-feed.d.ts +7 -1
- package/dist/core/events/v3/live-feed.d.ts.map +1 -1
- package/dist/core/events/v3/live-feed.js +21 -3
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +10 -2
- package/dist/core/events/v3/producers/command-recorder.d.ts +2 -2
- package/dist/core/events/v3/producers/command-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/command-recorder.js +2 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +2 -2
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +2 -1
- package/dist/core/events/v3/producers/hook-base.d.ts +2 -2
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +15 -5
- package/dist/core/events/v3/producers/hook.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook.js +17 -4
- package/dist/core/events/v3/producers/intake.d.ts +4 -4
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +3 -2
- package/dist/core/events/v3/producers/producer-state-observer.d.ts.map +1 -1
- package/dist/core/events/v3/producers/producer-state-observer.js +2 -1
- package/dist/core/events/v3/producers/recorder.d.ts +16 -5
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +31 -9
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +10 -13
- package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
- package/dist/core/events/v3/runtime-build.js +3 -2
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts +2 -2
- package/dist/core/events/v3/runtime-telemetry-capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/turn-telemetry.d.ts +2 -2
- package/dist/core/events/v3/turn-telemetry.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.d.ts +16 -14
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +36 -38
- package/dist/core/hooks/backup-schedule.d.ts +34 -0
- package/dist/core/hooks/backup-schedule.d.ts.map +1 -0
- package/dist/core/hooks/backup-schedule.js +114 -0
- package/dist/core/hooks/cli.js +61 -18
- package/dist/core/hooks/codex-wsl-bridge.d.ts +2 -0
- package/dist/core/hooks/codex-wsl-bridge.d.ts.map +1 -1
- package/dist/core/hooks/codex-wsl-bridge.js +18 -4
- package/dist/core/hooks/index.d.ts +1 -0
- package/dist/core/hooks/index.d.ts.map +1 -1
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/prompt-context/contract.d.ts +61 -0
- package/dist/core/hooks/prompt-context/contract.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/contract.js +102 -0
- package/dist/core/hooks/prompt-context/index.d.ts +5 -0
- package/dist/core/hooks/prompt-context/index.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/index.js +2 -0
- package/dist/core/hooks/prompt-context/runner.d.ts +24 -0
- package/dist/core/hooks/prompt-context/runner.d.ts.map +1 -0
- package/dist/core/hooks/prompt-context/runner.js +249 -0
- package/dist/core/qa-artifacts.d.ts +3 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -1
- package/dist/core/qa-artifacts.js +25 -3
- package/dist/core/resources/contract.d.ts +69 -3
- package/dist/core/resources/contract.d.ts.map +1 -1
- package/dist/core/resources/contract.js +1 -1
- package/dist/core/resources/index.d.ts +1 -0
- package/dist/core/resources/index.d.ts.map +1 -1
- package/dist/core/resources/index.js +1 -0
- package/dist/core/resources/native.d.ts +36 -0
- package/dist/core/resources/native.d.ts.map +1 -0
- package/dist/core/resources/native.js +418 -0
- package/dist/core/resources/sampler.d.ts +2 -0
- package/dist/core/resources/sampler.d.ts.map +1 -1
- package/dist/core/resources/sampler.js +31 -1
- package/dist/core/resources/status.d.ts +61 -0
- package/dist/core/resources/status.d.ts.map +1 -0
- package/dist/core/resources/status.js +536 -0
- package/dist/core/resources/system.d.ts +27 -0
- package/dist/core/resources/system.d.ts.map +1 -0
- package/dist/core/resources/system.js +439 -0
- package/dist/core/semantic/contract.d.ts +13 -13
- package/dist/core/semantic/service.d.ts +2 -0
- package/dist/core/semantic/service.d.ts.map +1 -1
- package/dist/core/semantic/service.js +27 -4
- package/dist/core/storage/builtins.js +41 -0
- package/dist/core/supervisor/contract.d.ts +41 -3
- package/dist/core/supervisor/contract.d.ts.map +1 -1
- package/dist/core/supervisor/contract.js +3 -2
- package/dist/core/supervisor/findings.d.ts +3 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -1
- package/dist/core/supervisor/findings.js +88 -0
- package/dist/core/supervisor/history.d.ts.map +1 -1
- package/dist/core/supervisor/history.js +14 -0
- package/dist/core/supervisor/ledger-state.d.ts +55 -0
- package/dist/core/supervisor/ledger-state.d.ts.map +1 -0
- package/dist/core/supervisor/ledger-state.js +0 -0
- package/dist/core/supervisor/service.d.ts.map +1 -1
- package/dist/core/supervisor/service.js +120 -7
- package/dist/core/supervisor/storage.d.ts +3 -1
- package/dist/core/supervisor/storage.d.ts.map +1 -1
- package/dist/core/supervisor/storage.js +4 -0
- package/dist/core/workflow/admission.d.ts +3 -0
- package/dist/core/workflow/admission.d.ts.map +1 -1
- package/dist/core/workflow/admission.js +28 -16
- package/dist/core/workflow/engine.js +2 -2
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +6 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +4 -3
- package/dist/lib/browser/capture-fidelity.d.ts +7 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -1
- package/dist/lib/browser/capture-fidelity.js +17 -0
- package/dist/lib/browser/client.d.ts +9 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +85 -37
- package/dist/lib/browser/index.d.ts +3 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +2 -1
- package/dist/lib/browser/page-review-budget.d.ts +13 -0
- package/dist/lib/browser/page-review-budget.d.ts.map +1 -0
- package/dist/lib/browser/page-review-budget.js +251 -0
- package/dist/lib/browser/page-review-contracts.d.ts +127 -0
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -0
- package/dist/lib/browser/page-review-contracts.js +5 -0
- package/dist/lib/browser/page-review-evidence.d.ts +48 -0
- package/dist/lib/browser/page-review-evidence.d.ts.map +1 -0
- package/dist/lib/browser/page-review-evidence.js +340 -0
- package/dist/lib/browser/page-review-judge.d.ts +1 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +46 -38
- package/dist/lib/browser/page-review-pack.d.ts +44 -22
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +116 -36
- package/dist/lib/browser/page-review-source.d.ts +12 -0
- package/dist/lib/browser/page-review-source.d.ts.map +1 -0
- package/dist/lib/browser/page-review-source.js +22 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +35 -9
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +48 -14
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +215 -68
- package/dist/lib/browser/qa-snapshot.d.ts +6 -0
- package/dist/lib/browser/qa-snapshot.d.ts.map +1 -1
- package/dist/lib/browser/qa-snapshot.js +259 -48
- package/dist/lib/browser/review-capture-transaction.d.ts +13 -0
- package/dist/lib/browser/review-capture-transaction.d.ts.map +1 -0
- package/dist/lib/browser/review-capture-transaction.js +79 -0
- package/dist/lib/browser/standalone-html.d.ts.map +1 -1
- package/dist/lib/browser/standalone-html.js +4 -2
- package/dist/lib/browser/thumbnail-association.d.ts +34 -0
- package/dist/lib/browser/thumbnail-association.d.ts.map +1 -0
- package/dist/lib/browser/thumbnail-association.js +158 -0
- package/dist/lib/browser/tiling.d.ts +30 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +38 -0
- package/dist/lib/docs-metadata-managed.d.ts.map +1 -1
- package/dist/lib/docs-metadata-managed.js +4 -1
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +7 -1
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +1 -0
- package/dist/lib/local-file-url.d.ts +12 -0
- package/dist/lib/local-file-url.d.ts.map +1 -1
- package/dist/lib/local-file-url.js +16 -2
- package/dist/lib/tunnel/state.d.ts +19 -4
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +53 -13
- package/package.json +7 -4
- package/schemas/config.schema.json +69 -2
- package/src/commands/agents.ts +42 -9
- package/src/commands/artifacts.ts +130 -6
- package/src/commands/backup.ts +287 -34
- package/src/commands/browse.ts +564 -267
- package/src/commands/diagnostics.ts +6 -3
- package/src/commands/ledger-v3.ts +20 -3
- package/src/commands/qa-run.ts +37 -3
- package/src/commands/qa-status.ts +11 -2
- package/src/commands/resources.ts +23 -3
- package/src/commands/review-pack.ts +165 -62
- package/src/commands/tunnel.ts +2 -1
- package/src/core/agents/canonical-emit.ts +4 -2
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/live-authority-v3.ts +7 -3
- package/src/core/agents/live-lifecycle-v3.ts +2 -2
- package/src/core/agents/render/prompt-context.ts +68 -14
- package/src/core/agents/session-finalizer-v3.ts +2 -2
- package/src/core/agents/state/live-coordination-view.ts +4 -4
- package/src/core/agents/state/names.ts +92 -7
- package/src/core/agents/state/stale-sweep.ts +22 -8
- package/src/core/agents/turn-elapsed.ts +135 -0
- package/src/core/artifacts/activity.ts +71 -0
- package/src/core/artifacts/delivery-card.ts +375 -0
- package/src/core/artifacts/index.ts +385 -91
- package/src/core/artifacts/mutation-lock.ts +106 -0
- package/src/core/backup/host-snapshot.ts +76 -0
- package/src/core/config.ts +66 -13
- package/src/core/diagnostics/advice.ts +148 -120
- package/src/core/diagnostics/bundle.ts +138 -52
- package/src/core/diagnostics/comparison.ts +6 -2
- package/src/core/diagnostics/contract.ts +39 -42
- package/src/core/diagnostics/index.ts +2 -0
- package/src/core/diagnostics/pressure-contract.ts +294 -0
- package/src/core/diagnostics/pressure.ts +1361 -0
- package/src/core/diagnostics/replay.ts +116 -11
- package/src/core/events/v3/adapter-id.ts +12 -0
- package/src/core/events/v3/archive-retention.ts +3 -6
- package/src/core/events/v3/bootstrap.ts +163 -16
- package/src/core/events/v3/capabilities.ts +75 -25
- package/src/core/events/v3/capability-drift.ts +2 -2
- package/src/core/events/v3/control-witness.ts +172 -41
- package/src/core/events/v3/control.ts +34 -2
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/live-feed.ts +23 -5
- package/src/core/events/v3/live-routing.ts +14 -2
- package/src/core/events/v3/producers/command-recorder.ts +4 -4
- package/src/core/events/v3/producers/coordination-recorder.ts +4 -4
- package/src/core/events/v3/producers/hook-base.ts +21 -16
- package/src/core/events/v3/producers/hook.ts +21 -4
- package/src/core/events/v3/producers/intake.ts +6 -6
- package/src/core/events/v3/producers/producer-state-observer.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +50 -21
- package/src/core/events/v3/reader.ts +14 -13
- package/src/core/events/v3/runtime-build.ts +3 -2
- package/src/core/events/v3/runtime-telemetry-capabilities.ts +3 -3
- package/src/core/events/v3/turn-telemetry.ts +4 -4
- package/src/core/hooks/adapter/output.ts +50 -36
- package/src/core/hooks/backup-schedule.ts +147 -0
- package/src/core/hooks/cli.ts +65 -14
- package/src/core/hooks/codex-wsl-bridge.ts +21 -5
- package/src/core/hooks/index.ts +1 -0
- package/src/core/hooks/prompt-context/contract.ts +177 -0
- package/src/core/hooks/prompt-context/index.ts +22 -0
- package/src/core/hooks/prompt-context/runner.ts +303 -0
- package/src/core/qa-artifacts.ts +29 -3
- package/src/core/resources/contract.ts +76 -3
- package/src/core/resources/index.ts +1 -0
- package/src/core/resources/native.ts +481 -0
- package/src/core/resources/sampler.ts +46 -1
- package/src/core/resources/status.ts +717 -0
- package/src/core/resources/system.ts +530 -0
- package/src/core/semantic/service.ts +37 -3
- package/src/core/storage/builtins.ts +41 -0
- package/src/core/supervisor/contract.ts +45 -3
- package/src/core/supervisor/findings.ts +159 -0
- package/src/core/supervisor/history.ts +15 -0
- package/src/core/supervisor/ledger-state.ts +0 -0
- package/src/core/supervisor/service.ts +133 -5
- package/src/core/supervisor/storage.ts +7 -0
- package/src/core/workflow/admission.ts +33 -17
- package/src/core/workflow/engine.ts +2 -2
- package/src/core/workflow/live-session-v3.ts +6 -8
- package/src/core/workflow/proof.ts +4 -3
- package/src/lib/browser/capture-fidelity.ts +23 -0
- package/src/lib/browser/client.ts +96 -44
- package/src/lib/browser/index.ts +15 -1
- package/src/lib/browser/page-review-budget.ts +309 -0
- package/src/lib/browser/page-review-contracts.ts +129 -0
- package/src/lib/browser/page-review-evidence.ts +465 -0
- package/src/lib/browser/page-review-judge.ts +56 -40
- package/src/lib/browser/page-review-pack.ts +181 -43
- package/src/lib/browser/page-review-source.ts +30 -0
- package/src/lib/browser/qa-run-contracts.ts +62 -18
- package/src/lib/browser/qa-run.ts +259 -71
- package/src/lib/browser/qa-snapshot.ts +289 -48
- package/src/lib/browser/review-capture-transaction.ts +106 -0
- package/src/lib/browser/standalone-html.ts +4 -2
- package/src/lib/browser/thumbnail-association.ts +190 -0
- package/src/lib/browser/tiling.ts +63 -0
- package/src/lib/docs-metadata-managed.ts +4 -1
- package/src/lib/identities/assume.ts +12 -4
- package/src/lib/instructions/templates.ts +1 -0
- package/src/lib/local-file-url.ts +17 -2
- package/src/lib/tunnel/state.ts +70 -13
package/dist/commands/browse.js
CHANGED
|
@@ -3,13 +3,19 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
4
|
import { PNG } from "pngjs";
|
|
5
5
|
import { resolveBinName } from "../core/config.js";
|
|
6
|
-
import { CAPTURE_FIDELITY_MISMATCH_THRESHOLD, compareBand, decideFidelity, pngDimensions, } from "../lib/browser/capture-fidelity.js";
|
|
6
|
+
import { CAPTURE_FIDELITY_MISMATCH_THRESHOLD, compareBand, cropNativePng, decideFidelity, pngDimensions, } from "../lib/browser/capture-fidelity.js";
|
|
7
7
|
import { Browser, browserProxyFromEnv, browserProxyGateFromEnv, captureDevOverlay, DEFAULT_CRITIQUE_RUBRIC, extractObservedIp, parseAssertSpec, runCritique, tilesFromFullPage, WEBRTC_PROXY_ONLY_ARG, writeNetscapeCookieFile, wslHeadedLaunchArgs, } from "../lib/browser/index.js";
|
|
8
|
-
import {
|
|
8
|
+
import { allocateTileBudget, validatePageReviewAllocation, } from "../lib/browser/page-review-budget.js";
|
|
9
|
+
import { PAGE_REVIEW_CAPTURE_PLAN_SCHEMA, } from "../lib/browser/page-review-contracts.js";
|
|
10
|
+
import { findPackTile, gateHitsFromEnvelope, readPackContext, tileId, writePackContext, writePackExpandedTile, } from "../lib/browser/page-review-pack.js";
|
|
11
|
+
import { retainPageReviewSourceIdentity } from "../lib/browser/page-review-source.js";
|
|
9
12
|
import { buildQaManifest, classifySignatures, } from "../lib/browser/qa-plan.js";
|
|
10
13
|
import { DEFAULT_REUSE_MISMATCH_RATIO, planCritiqueReuse, QA_CRITIQUE_CONTRACT_VERSION, rubricDigest, } from "../lib/browser/qa-reuse.js";
|
|
11
14
|
import { loadQaSnapshot, resolveQaBaseline, saveQaSnapshot } from "../lib/browser/qa-snapshot.js";
|
|
15
|
+
import { allocateCaptureReservation, assertGatePlanStable, CaptureSourceChanged, runCaptureTransaction, writeCaptureTransaction, } from "../lib/browser/review-capture-transaction.js";
|
|
12
16
|
import { startBrowserSessionServer, } from "../lib/browser/session-control.js";
|
|
17
|
+
import { captureThumbnailFingerprint, registerCaptureThumbnail, } from "../lib/browser/thumbnail-association.js";
|
|
18
|
+
import { reviewCandidateRects } from "../lib/browser/tiling.js";
|
|
13
19
|
import { diffAgainstBaseline, saveBaseline, } from "../lib/browser/visual-diff.js";
|
|
14
20
|
import { applyExtraCookies, CookieJar } from "../lib/cookies/index.js";
|
|
15
21
|
/**
|
|
@@ -244,17 +250,19 @@ export function registerBrowseCommand(program, emitParam, context, binName = res
|
|
|
244
250
|
"screenshot, critique tiles as PNG files, serialized DOM, QA signature) and make no " +
|
|
245
251
|
"vision call. A judge stage (`review-pack judge`, or qa-run) reviews the tiles from disk " +
|
|
246
252
|
"after every browser has closed. Tiling knobs are the --check-critique-* flags.")
|
|
253
|
+
.option("--review-pack-plan", "Emit all native capture candidates and gate associations without capturing tiles. " +
|
|
254
|
+
"With --out, retain private source identity inputs beside the output for diagnostics.")
|
|
255
|
+
.option("--review-pack-allocation <json-file>", "Capture exactly the selected IDs in a verified context allocation.")
|
|
256
|
+
.option("--review-pack-reservation <json-file>", "Reserve the preliminary allocation's tile count, then rerun supplied gates and allocate against the same page before capture. Full source checks and native coverage remain required.")
|
|
247
257
|
.option("--review-pack-context <id>", "Context id inside the pack (default <viewport>-<theme>-<state> from --viewport, " +
|
|
248
258
|
"--qa-theme, --qa-state).")
|
|
249
259
|
.option("--review-pack-scope <selector>", "Also tile one screenshot per element matching <selector> into the pack (repeatable).", (value, previous = []) => [...previous, value], [])
|
|
250
|
-
.option("--no-review-pack-bands", "Skip full-page bands in the pack and keep only --review-pack-scope tiles (a scoped " +
|
|
251
|
-
"visual pass).")
|
|
252
260
|
.option("--review-pack-expand <tile-id>", "Instead of capturing a context, re-render ONE existing tile of --review-pack-context " +
|
|
253
261
|
"(e.g. T012) at --device-scale-factor and write it beside the original as " +
|
|
254
262
|
"tiles/<tile>@<dpr>x.png. Existing tiles are untouched.")
|
|
255
|
-
.option("--review-pack-hit-rect <x,y,w,h>", "Document-space rectangle (integer px) a deterministic gate already flagged.
|
|
256
|
-
"
|
|
257
|
-
`
|
|
263
|
+
.option("--review-pack-hit-rect <x,y,w,h>", "Document-space rectangle (integer px) a deterministic gate already flagged. " +
|
|
264
|
+
"Intersecting bands are prioritized within the hard tile cap " +
|
|
265
|
+
`(repeatable, at most ${REVIEW_PACK_HIT_RECT_MAX}).`, (value, previous = []) => [...previous, value], [])
|
|
258
266
|
.option("--device-scale-factor <n>", "Device scale factor for the browser context (default 1). 2 renders every screenshot " +
|
|
259
267
|
"at twice the pixels; pair with --review-pack-expand for a sharper look at one tile.")
|
|
260
268
|
.option("--no-dev-overlay", "Skip auto-capture of Next.js dev-overlay issues. Default: capture every queued error (kind/code/message/stack) when a <nextjs-portal> shadow root is present. Necessary because Next.js 16 + React 19 route hydration errors + most React warnings through onCaughtError → next-devtools' errorQueue, NOT through console.error, so Playwright's standard listener doesn't see them. Surfaces them in the JSON envelope under `devOverlay`.")
|
|
@@ -291,6 +299,13 @@ async function runBrowse(url, opts, context) {
|
|
|
291
299
|
const headed = opts.login || opts.headed;
|
|
292
300
|
const viewport = parseViewport(opts.viewport ?? "desktop");
|
|
293
301
|
const deviceScaleFactor = parseDeviceScaleFactor(opts.deviceScaleFactor);
|
|
302
|
+
if (opts.reviewPackAllocation && !opts.reviewPack) {
|
|
303
|
+
throw new Error("--review-pack-allocation requires --review-pack <dir>.");
|
|
304
|
+
}
|
|
305
|
+
if (opts.reviewPackReservation &&
|
|
306
|
+
(!opts.reviewPack || opts.reviewPackAllocation || opts.reviewPackExpand || opts.checkCritique)) {
|
|
307
|
+
throw new Error("--review-pack-reservation requires an ordinary --review-pack capture without allocation, expansion or inline critique.");
|
|
308
|
+
}
|
|
294
309
|
if (opts.reviewPackExpand !== undefined) {
|
|
295
310
|
if (opts.reviewPack === undefined || opts.reviewPackContext === undefined) {
|
|
296
311
|
throw new Error("--review-pack-expand requires --review-pack <dir> and --review-pack-context <id>.");
|
|
@@ -371,110 +386,182 @@ async function runBrowse(url, opts, context) {
|
|
|
371
386
|
if (opts.evaluate) {
|
|
372
387
|
evalResult = await browser.evaluate(opts.evaluate);
|
|
373
388
|
}
|
|
389
|
+
if (opts.reviewPackPlan || opts.reviewPack)
|
|
390
|
+
await browser.waitForReviewReady();
|
|
374
391
|
// Run visibility checks AFTER any --batch interactions but BEFORE the
|
|
375
392
|
// screenshot. Annotation injection happens between sampling and capture
|
|
376
393
|
// so the boxes show on the saved PNG; they're cleared post-screenshot
|
|
377
394
|
// so the live profile state isn't polluted.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
if (opts.
|
|
384
|
-
await browser.
|
|
395
|
+
const collectGates = async (attempt = 0) => {
|
|
396
|
+
const before = opts.reviewPackReservation
|
|
397
|
+
? await buildReviewCapturePlan(browser, opts)
|
|
398
|
+
: undefined;
|
|
399
|
+
let visibility;
|
|
400
|
+
if (opts.checkVisible && opts.checkVisible.length > 0) {
|
|
401
|
+
visibility = await browser.checkVisibility(opts.checkVisible, {
|
|
402
|
+
sampleGrid: Number.parseInt(opts.checkVisibleSampleGrid ?? "3", 10),
|
|
403
|
+
});
|
|
404
|
+
if (opts.checkVisibleAnnotate !== false && !opts.reviewPackPlan && !opts.reviewPack) {
|
|
405
|
+
await browser.annotateVisibility(visibility);
|
|
406
|
+
}
|
|
385
407
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
widths = await browser.checkWidth(opts.checkWidth);
|
|
390
|
-
}
|
|
391
|
-
let overflow;
|
|
392
|
-
if (opts.checkOverflow) {
|
|
393
|
-
overflow = await browser.checkOverflow();
|
|
394
|
-
}
|
|
395
|
-
let runts;
|
|
396
|
-
if (opts.checkRunts) {
|
|
397
|
-
runts = await browser.checkRunts({
|
|
398
|
-
scope: typeof opts.checkRunts === "string" ? opts.checkRunts : null,
|
|
399
|
-
minChars: Number.parseInt(opts.checkRuntsMinChars ?? "40", 10),
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
const hasLayoutLint = (opts.checkAlign?.length ?? 0) > 0 ||
|
|
403
|
-
(opts.checkGap?.length ?? 0) > 0 ||
|
|
404
|
-
(opts.checkClip?.length ?? 0) > 0 ||
|
|
405
|
-
(opts.checkOverlap?.length ?? 0) > 0 ||
|
|
406
|
-
(opts.checkCrowd?.length ?? 0) > 0;
|
|
407
|
-
let layoutLint;
|
|
408
|
-
if (hasLayoutLint) {
|
|
409
|
-
layoutLint = await browser.checkLayoutLint({
|
|
410
|
-
align: (opts.checkAlign ?? []).map((selector) => ({
|
|
411
|
-
selector,
|
|
412
|
-
axis: alignAxis,
|
|
413
|
-
tolerancePx: alignThreshold,
|
|
414
|
-
})),
|
|
415
|
-
gap: (opts.checkGap ?? []).map((selector) => ({
|
|
416
|
-
selector,
|
|
417
|
-
axis: gapAxis,
|
|
418
|
-
tolerancePx: gapThreshold,
|
|
419
|
-
expectedGapPx: gapExpected,
|
|
420
|
-
})),
|
|
421
|
-
clip: (opts.checkClip ?? []).map((selector) => ({
|
|
422
|
-
selector,
|
|
423
|
-
tolerancePx: clipThreshold,
|
|
424
|
-
})),
|
|
425
|
-
overlap: (opts.checkOverlap ?? []).map((selector) => ({
|
|
426
|
-
selector,
|
|
427
|
-
tolerancePx: overlapThreshold,
|
|
428
|
-
})),
|
|
429
|
-
crowd: (opts.checkCrowd ?? []).map((selector) => ({
|
|
430
|
-
selector,
|
|
431
|
-
minGapPx: crowdMin,
|
|
432
|
-
})),
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
let hit;
|
|
436
|
-
if (opts.checkHit && opts.checkHit.length > 0) {
|
|
437
|
-
hit = await browser.checkTargetSize(opts.checkHit, hitProfile);
|
|
438
|
-
}
|
|
439
|
-
const hasContentChecks = opts.checkPlaceholder !== undefined ||
|
|
440
|
-
opts.checkImages !== undefined ||
|
|
441
|
-
opts.checkTruncation !== undefined ||
|
|
442
|
-
opts.checkContrast !== undefined;
|
|
443
|
-
let content;
|
|
444
|
-
if (hasContentChecks) {
|
|
445
|
-
content = await browser.checkContent({
|
|
446
|
-
placeholder: opts.checkPlaceholder !== undefined
|
|
447
|
-
? { scope: contentScope(opts.checkPlaceholder) }
|
|
448
|
-
: null,
|
|
449
|
-
image: opts.checkImages !== undefined
|
|
450
|
-
? {
|
|
451
|
-
scope: contentScope(opts.checkImages),
|
|
452
|
-
tolerance: parseNonNegativeNumber(opts.checkImagesTolerance ?? "0.1", "--check-images-tolerance"),
|
|
453
|
-
}
|
|
454
|
-
: null,
|
|
455
|
-
truncation: opts.checkTruncation !== undefined
|
|
456
|
-
? {
|
|
457
|
-
scope: contentScope(opts.checkTruncation),
|
|
458
|
-
tolerance: parseNonNegativeNumber(opts.checkTruncationTolerance ?? "2", "--check-truncation-tolerance"),
|
|
459
|
-
}
|
|
460
|
-
: null,
|
|
461
|
-
contrast: opts.checkContrast !== undefined ? { scope: contentScope(opts.checkContrast) } : null,
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
// Diff-aware QA planning. Signature capture must also happen BEFORE any
|
|
465
|
-
// annotation overlays are injected — an annotation box is a DOM change.
|
|
466
|
-
let qaPlan;
|
|
467
|
-
let qaCapture;
|
|
468
|
-
if (opts.qaPlan || opts.qaSnapshot) {
|
|
469
|
-
const qa = await runQaPlanning(browser, url, navResult.url, opts);
|
|
470
|
-
qaPlan = qa.report;
|
|
471
|
-
qaCapture = qa.capture;
|
|
472
|
-
if (qaPlan?.manifest) {
|
|
473
|
-
const m = qaPlan.manifest;
|
|
474
|
-
emit.log(`qa-plan: class=${m.change_class} scopes=${m.scopes.map((s) => s.selector).join(",") || "-"} ` +
|
|
475
|
-
`contexts=${m.contexts.length} model-calls<=${m.predicted.model_calls_ceiling} baseline=${m.baseline_source}`, "info");
|
|
408
|
+
let widths;
|
|
409
|
+
if (opts.checkWidth && opts.checkWidth.length > 0) {
|
|
410
|
+
widths = await browser.checkWidth(opts.checkWidth);
|
|
476
411
|
}
|
|
477
|
-
|
|
412
|
+
let overflow;
|
|
413
|
+
if (opts.checkOverflow) {
|
|
414
|
+
overflow = await browser.checkOverflow();
|
|
415
|
+
}
|
|
416
|
+
let runts;
|
|
417
|
+
if (opts.checkRunts) {
|
|
418
|
+
runts = await browser.checkRunts({
|
|
419
|
+
scope: typeof opts.checkRunts === "string" ? opts.checkRunts : null,
|
|
420
|
+
minChars: Number.parseInt(opts.checkRuntsMinChars ?? "40", 10),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
const hasLayoutLint = (opts.checkAlign?.length ?? 0) > 0 ||
|
|
424
|
+
(opts.checkGap?.length ?? 0) > 0 ||
|
|
425
|
+
(opts.checkClip?.length ?? 0) > 0 ||
|
|
426
|
+
(opts.checkOverlap?.length ?? 0) > 0 ||
|
|
427
|
+
(opts.checkCrowd?.length ?? 0) > 0;
|
|
428
|
+
let layoutLint;
|
|
429
|
+
if (hasLayoutLint) {
|
|
430
|
+
layoutLint = await browser.checkLayoutLint({
|
|
431
|
+
align: (opts.checkAlign ?? []).map((selector) => ({
|
|
432
|
+
selector,
|
|
433
|
+
axis: alignAxis,
|
|
434
|
+
tolerancePx: alignThreshold,
|
|
435
|
+
})),
|
|
436
|
+
gap: (opts.checkGap ?? []).map((selector) => ({
|
|
437
|
+
selector,
|
|
438
|
+
axis: gapAxis,
|
|
439
|
+
tolerancePx: gapThreshold,
|
|
440
|
+
expectedGapPx: gapExpected,
|
|
441
|
+
})),
|
|
442
|
+
clip: (opts.checkClip ?? []).map((selector) => ({
|
|
443
|
+
selector,
|
|
444
|
+
tolerancePx: clipThreshold,
|
|
445
|
+
})),
|
|
446
|
+
overlap: (opts.checkOverlap ?? []).map((selector) => ({
|
|
447
|
+
selector,
|
|
448
|
+
tolerancePx: overlapThreshold,
|
|
449
|
+
})),
|
|
450
|
+
crowd: (opts.checkCrowd ?? []).map((selector) => ({
|
|
451
|
+
selector,
|
|
452
|
+
minGapPx: crowdMin,
|
|
453
|
+
})),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
let hit;
|
|
457
|
+
if (opts.checkHit && opts.checkHit.length > 0) {
|
|
458
|
+
hit = await browser.checkTargetSize(opts.checkHit, hitProfile);
|
|
459
|
+
}
|
|
460
|
+
const hasContentChecks = opts.checkPlaceholder !== undefined ||
|
|
461
|
+
opts.checkImages !== undefined ||
|
|
462
|
+
opts.checkTruncation !== undefined ||
|
|
463
|
+
opts.checkContrast !== undefined;
|
|
464
|
+
let content;
|
|
465
|
+
if (hasContentChecks) {
|
|
466
|
+
content = await browser.checkContent({
|
|
467
|
+
placeholder: opts.checkPlaceholder !== undefined
|
|
468
|
+
? { scope: contentScope(opts.checkPlaceholder) }
|
|
469
|
+
: null,
|
|
470
|
+
image: opts.checkImages !== undefined
|
|
471
|
+
? {
|
|
472
|
+
scope: contentScope(opts.checkImages),
|
|
473
|
+
tolerance: parseNonNegativeNumber(opts.checkImagesTolerance ?? "0.1", "--check-images-tolerance"),
|
|
474
|
+
}
|
|
475
|
+
: null,
|
|
476
|
+
truncation: opts.checkTruncation !== undefined
|
|
477
|
+
? {
|
|
478
|
+
scope: contentScope(opts.checkTruncation),
|
|
479
|
+
tolerance: parseNonNegativeNumber(opts.checkTruncationTolerance ?? "2", "--check-truncation-tolerance"),
|
|
480
|
+
}
|
|
481
|
+
: null,
|
|
482
|
+
contrast: opts.checkContrast !== undefined ? { scope: contentScope(opts.checkContrast) } : null,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
// Diff-aware QA planning. Signature capture must also happen BEFORE any
|
|
486
|
+
// annotation overlays are injected — an annotation box is a DOM change.
|
|
487
|
+
let qaPlan;
|
|
488
|
+
let qaCapture;
|
|
489
|
+
if (opts.qaPlan || opts.qaSnapshot) {
|
|
490
|
+
const qa = await runQaPlanning(browser, url, navResult.url, opts);
|
|
491
|
+
qaPlan = qa.report;
|
|
492
|
+
qaCapture = qa.capture;
|
|
493
|
+
if (qaPlan?.manifest) {
|
|
494
|
+
const m = qaPlan.manifest;
|
|
495
|
+
emit.log(`qa-plan: class=${m.change_class} scopes=${m.scopes.map((s) => s.selector).join(",") || "-"} ` +
|
|
496
|
+
`contexts=${m.contexts.length} model-calls<=${m.predicted.model_calls_ceiling} baseline=${m.baseline_source}`, "info");
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
let asserts;
|
|
500
|
+
if (opts.assert && opts.assert.length > 0) {
|
|
501
|
+
const specs = opts.assert.map(parseAssertSpec);
|
|
502
|
+
asserts = await browser.checkAsserts(specs);
|
|
503
|
+
}
|
|
504
|
+
if (opts.reviewPackPlan || opts.reviewPackReservation) {
|
|
505
|
+
const gateEnvelope = { runts, overflow, hit, ...layoutLint };
|
|
506
|
+
if (content)
|
|
507
|
+
assignContent(gateEnvelope, content);
|
|
508
|
+
opts.capturePlanResult = await buildReviewCapturePlan(browser, opts);
|
|
509
|
+
const hits = gateHitsFromEnvelope(gateEnvelope, Number.POSITIVE_INFINITY);
|
|
510
|
+
for (const candidate of opts.capturePlanResult.candidates) {
|
|
511
|
+
const r = candidate.rect;
|
|
512
|
+
candidate.gate_hits = hits.flatMap((hit, index) => {
|
|
513
|
+
const dpr = opts.capturePlanResult.dpr;
|
|
514
|
+
const h = {
|
|
515
|
+
x: hit.rect.x * dpr,
|
|
516
|
+
y: hit.rect.y * dpr,
|
|
517
|
+
width: Math.max(1, hit.rect.width * dpr),
|
|
518
|
+
height: Math.max(1, hit.rect.height * dpr),
|
|
519
|
+
};
|
|
520
|
+
return r.x < h.x + h.width &&
|
|
521
|
+
r.x + r.width > h.x &&
|
|
522
|
+
r.y < h.y + h.height &&
|
|
523
|
+
r.y + r.height > h.y
|
|
524
|
+
? [{ check_id: `${hit.rule}:${index + 1}`, severity: "high" }]
|
|
525
|
+
: [];
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
if (opts.reviewPackReservation) {
|
|
530
|
+
const prefix = opts.out ?? resolve(opts.reviewPack, opts.reviewPackContext ?? "capture");
|
|
531
|
+
mkdirSync(dirname(prefix), { recursive: true, mode: 0o700 });
|
|
532
|
+
writeFileSync(`${prefix}.attempt-${attempt}.gates.json`, JSON.stringify({
|
|
533
|
+
attempt,
|
|
534
|
+
source_before: before?.source_digest,
|
|
535
|
+
source_after: opts.capturePlanResult?.source_digest,
|
|
536
|
+
source_evidence: opts.captureSourceEvidence,
|
|
537
|
+
visibility,
|
|
538
|
+
widths,
|
|
539
|
+
overflow,
|
|
540
|
+
runts,
|
|
541
|
+
layoutLint,
|
|
542
|
+
hit,
|
|
543
|
+
content,
|
|
544
|
+
asserts,
|
|
545
|
+
}, null, 2), { mode: 0o600 });
|
|
546
|
+
}
|
|
547
|
+
if (before)
|
|
548
|
+
assertGatePlanStable(before, opts.capturePlanResult);
|
|
549
|
+
return {
|
|
550
|
+
visibility,
|
|
551
|
+
widths,
|
|
552
|
+
overflow,
|
|
553
|
+
runts,
|
|
554
|
+
layoutLint,
|
|
555
|
+
hit,
|
|
556
|
+
content,
|
|
557
|
+
qaPlan,
|
|
558
|
+
qaCapture,
|
|
559
|
+
asserts,
|
|
560
|
+
};
|
|
561
|
+
};
|
|
562
|
+
let { visibility, widths, overflow, runts, layoutLint, hit, content, qaPlan, qaCapture, asserts, } = opts.reviewPackReservation
|
|
563
|
+
? {}
|
|
564
|
+
: await collectGates();
|
|
478
565
|
// Vision critique. Capture tiles BEFORE any annotation overlays are injected
|
|
479
566
|
// so the model sees the real page, not our boxes.
|
|
480
567
|
let critique;
|
|
@@ -542,6 +629,52 @@ async function runBrowse(url, opts, context) {
|
|
|
542
629
|
if (opts.reviewPack !== undefined && opts.reviewPackExpand !== undefined) {
|
|
543
630
|
reviewPack = await expandReviewPackTile(browser, opts, deviceScaleFactor ?? 1);
|
|
544
631
|
}
|
|
632
|
+
else if (opts.reviewPackReservation) {
|
|
633
|
+
const reservation = JSON.parse(readFileSync(resolve(opts.reviewPackReservation), "utf8"));
|
|
634
|
+
opts.captureTransaction = { attempts: [] };
|
|
635
|
+
const persistTransaction = () => {
|
|
636
|
+
const prefix = opts.out ?? resolve(opts.reviewPack, opts.reviewPackContext ?? "capture");
|
|
637
|
+
mkdirSync(dirname(prefix), { recursive: true, mode: 0o700 });
|
|
638
|
+
writeFileSync(`${prefix}.capture-transaction.json`, JSON.stringify(opts.captureTransaction, null, 2), { mode: 0o600 });
|
|
639
|
+
};
|
|
640
|
+
reviewPack = await runCaptureTransaction(async (attempt) => {
|
|
641
|
+
await browser.waitForReviewReady();
|
|
642
|
+
({
|
|
643
|
+
visibility,
|
|
644
|
+
widths,
|
|
645
|
+
overflow,
|
|
646
|
+
runts,
|
|
647
|
+
layoutLint,
|
|
648
|
+
hit,
|
|
649
|
+
content,
|
|
650
|
+
qaPlan,
|
|
651
|
+
qaCapture,
|
|
652
|
+
asserts,
|
|
653
|
+
} = await collectGates(attempt));
|
|
654
|
+
opts.captureTransactionAllocation = allocateCaptureReservation(reservation, opts.capturePlanResult);
|
|
655
|
+
opts.captureTransaction.attempts.push({
|
|
656
|
+
attempt,
|
|
657
|
+
status: "gated",
|
|
658
|
+
source_digest: opts.capturePlanResult.source_digest,
|
|
659
|
+
});
|
|
660
|
+
persistTransaction();
|
|
661
|
+
}, async () => {
|
|
662
|
+
const captured = await captureReviewPackContext(browser, url, navResult, opts, qaCapture);
|
|
663
|
+
opts.captureTransaction.source_digest =
|
|
664
|
+
opts.captureTransactionAllocation.plan.source_digest;
|
|
665
|
+
opts.captureTransaction.attempts.at(-1).status = "captured";
|
|
666
|
+
persistTransaction();
|
|
667
|
+
return captured;
|
|
668
|
+
}, (attempt, error) => {
|
|
669
|
+
opts.captureTransaction.attempts.push({
|
|
670
|
+
attempt,
|
|
671
|
+
status: "failed",
|
|
672
|
+
error: error instanceof Error ? error.message : String(error),
|
|
673
|
+
source_digest: opts.capturePlanResult?.source_digest,
|
|
674
|
+
});
|
|
675
|
+
persistTransaction();
|
|
676
|
+
});
|
|
677
|
+
}
|
|
545
678
|
else if (opts.reviewPack !== undefined) {
|
|
546
679
|
reviewPack = await captureReviewPackContext(browser, url, navResult, opts, qaCapture);
|
|
547
680
|
}
|
|
@@ -590,11 +723,6 @@ async function runBrowse(url, opts, context) {
|
|
|
590
723
|
`[${qaCapture.context.viewport}/${qaCapture.context.theme}/${qaCapture.context.state}]` +
|
|
591
724
|
`${persistedCritique ? " (with critique results)" : ""}`, "info");
|
|
592
725
|
}
|
|
593
|
-
let asserts;
|
|
594
|
-
if (opts.assert && opts.assert.length > 0) {
|
|
595
|
-
const specs = opts.assert.map(parseAssertSpec);
|
|
596
|
-
asserts = await browser.checkAsserts(specs);
|
|
597
|
-
}
|
|
598
726
|
const widthThreshold = Number.parseFloat(opts.checkWidthThreshold ?? "0.9");
|
|
599
727
|
const annotateWidth = widths && opts.checkWidthAnnotate !== false;
|
|
600
728
|
const annotateOverflow = overflow && opts.checkOverflowAnnotate !== false;
|
|
@@ -930,6 +1058,12 @@ async function runPrintMode(browser, navResult, opts, evalResult, visibility, wi
|
|
|
930
1058
|
result.qaReuse = qaReuse;
|
|
931
1059
|
if (reviewPack)
|
|
932
1060
|
result.reviewPack = reviewPack;
|
|
1061
|
+
if (opts.capturePlanResult)
|
|
1062
|
+
result.review_pack_capture_plan = opts.capturePlanResult;
|
|
1063
|
+
if (opts.captureSourceEvidence)
|
|
1064
|
+
result.review_pack_source_evidence = opts.captureSourceEvidence;
|
|
1065
|
+
if (opts.captureTransaction)
|
|
1066
|
+
result.review_pack_transaction = opts.captureTransaction;
|
|
933
1067
|
if (batchResult && batchResult.clipboardReads.length > 0) {
|
|
934
1068
|
result.batchClipboardReads = batchResult.clipboardReads;
|
|
935
1069
|
}
|
|
@@ -958,6 +1092,7 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
958
1092
|
const skipScreenshot = opts.screenshot === false;
|
|
959
1093
|
let pngPath;
|
|
960
1094
|
let pngBytes;
|
|
1095
|
+
let pngFingerprint = null;
|
|
961
1096
|
let captureEvalResult;
|
|
962
1097
|
let captureViewport;
|
|
963
1098
|
let captureEvidence;
|
|
@@ -975,6 +1110,8 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
975
1110
|
else {
|
|
976
1111
|
pngBytes = await browser.screenshot(pngPath, { fullPage: opts.fullPage !== false });
|
|
977
1112
|
}
|
|
1113
|
+
if (!opts.selector && opts.fullPage !== false)
|
|
1114
|
+
pngFingerprint = captureThumbnailFingerprint(pngPath);
|
|
978
1115
|
written.push(pngPath);
|
|
979
1116
|
}
|
|
980
1117
|
else if (opts.captureEvaluate) {
|
|
@@ -999,6 +1136,17 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
999
1136
|
}
|
|
1000
1137
|
}
|
|
1001
1138
|
written.push(htmlPath);
|
|
1139
|
+
const thumbnailAssociation = snapshot && pngPath && pngFingerprint
|
|
1140
|
+
? registerCaptureThumbnail({
|
|
1141
|
+
htmlPath,
|
|
1142
|
+
pngPath,
|
|
1143
|
+
html: snapshot.html,
|
|
1144
|
+
preview: pngFingerprint,
|
|
1145
|
+
fullDocument: !opts.selector && opts.fullPage !== false,
|
|
1146
|
+
stylesheetsLinked: snapshot.stylesheetsLinked,
|
|
1147
|
+
resourcesLinked: snapshot.resourcesLinked,
|
|
1148
|
+
})
|
|
1149
|
+
: null;
|
|
1002
1150
|
const diag = browser.diagnostics();
|
|
1003
1151
|
const jsonPath = `${prefix}.json`;
|
|
1004
1152
|
const envelope = {
|
|
@@ -1014,6 +1162,8 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
1014
1162
|
};
|
|
1015
1163
|
if (pngBytes !== undefined)
|
|
1016
1164
|
envelope.screenshotBytes = pngBytes;
|
|
1165
|
+
if (thumbnailAssociation)
|
|
1166
|
+
envelope.thumbnailAssociation = thumbnailAssociation;
|
|
1017
1167
|
if (snapshot) {
|
|
1018
1168
|
envelope.htmlSnapshot = {
|
|
1019
1169
|
stylesheetsInlined: snapshot.stylesheetsInlined,
|
|
@@ -1063,6 +1213,12 @@ async function runTrioMode(browser, navResult, opts, evalResult, visibility, wid
|
|
|
1063
1213
|
envelope.qaReuse = qaReuse;
|
|
1064
1214
|
if (reviewPack)
|
|
1065
1215
|
envelope.reviewPack = reviewPack;
|
|
1216
|
+
if (opts.capturePlanResult)
|
|
1217
|
+
envelope.review_pack_capture_plan = opts.capturePlanResult;
|
|
1218
|
+
if (opts.captureSourceEvidence)
|
|
1219
|
+
envelope.review_pack_source_evidence = opts.captureSourceEvidence;
|
|
1220
|
+
if (opts.captureTransaction)
|
|
1221
|
+
envelope.review_pack_transaction = opts.captureTransaction;
|
|
1066
1222
|
if (batchResult && batchResult.clipboardReads.length > 0) {
|
|
1067
1223
|
envelope.batchClipboardReads = batchResult.clipboardReads;
|
|
1068
1224
|
}
|
|
@@ -1517,12 +1673,14 @@ async function expandReviewPackTile(browser, opts, dpr) {
|
|
|
1517
1673
|
const packDir = opts.reviewPack;
|
|
1518
1674
|
const contextId = opts.reviewPackContext;
|
|
1519
1675
|
const tileId = opts.reviewPackExpand;
|
|
1520
|
-
const
|
|
1676
|
+
const originalContext = readPackContext(packDir, contextId);
|
|
1677
|
+
const { tile } = findPackTile([originalContext], tileId, contextId);
|
|
1678
|
+
const originalDpr = originalContext.dpr;
|
|
1521
1679
|
const region = await browser.captureRegionByScroll({
|
|
1522
|
-
x: tile.x,
|
|
1523
|
-
y: tile.scrollY,
|
|
1524
|
-
width: tile.width,
|
|
1525
|
-
height: tile.height,
|
|
1680
|
+
x: tile.x / originalDpr,
|
|
1681
|
+
y: tile.scrollY / originalDpr,
|
|
1682
|
+
width: tile.width / originalDpr,
|
|
1683
|
+
height: tile.height / originalDpr,
|
|
1526
1684
|
});
|
|
1527
1685
|
const { record, expanded } = writePackExpandedTile(packDir, contextId, {
|
|
1528
1686
|
tileId,
|
|
@@ -1547,9 +1705,9 @@ async function expandReviewPackTile(browser, opts, dpr) {
|
|
|
1547
1705
|
* agree the tiles stand and the record says `full-page`; when any disagrees,
|
|
1548
1706
|
* every band is re-cut from its scrolled capture (same rects, same ids, only
|
|
1549
1707
|
* the pixels change) and the record says `scrolled-bands`. A probe that
|
|
1550
|
-
* cannot run, or a re-capture
|
|
1551
|
-
*
|
|
1552
|
-
*
|
|
1708
|
+
* cannot run, or a re-capture with the wrong size, leaves a missing probe;
|
|
1709
|
+
* the caller refuses to save that context. Runs while the capture browser
|
|
1710
|
+
* is open; the judge never opens one.
|
|
1553
1711
|
*/
|
|
1554
1712
|
async function reconcileCaptureFidelity(browser, tiles) {
|
|
1555
1713
|
const threshold = CAPTURE_FIDELITY_MISMATCH_THRESHOLD;
|
|
@@ -1564,30 +1722,22 @@ async function reconcileCaptureFidelity(browser, tiles) {
|
|
|
1564
1722
|
height: tile.height / dpr,
|
|
1565
1723
|
});
|
|
1566
1724
|
const shots = new Map();
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
try {
|
|
1573
|
-
const captured = await browser.captureRegionByScroll(cssRect(tile));
|
|
1574
|
-
const size = pngDimensions(captured);
|
|
1725
|
+
try {
|
|
1726
|
+
const captured = await browser.captureRegionsByScroll(tiles.map(cssRect));
|
|
1727
|
+
for (const [position, shot] of captured.entries()) {
|
|
1728
|
+
const tile = tiles[position];
|
|
1729
|
+
const size = pngDimensions(shot);
|
|
1575
1730
|
if (Math.abs(size.width - tile.width) > 1 || Math.abs(size.height - tile.height) > 1) {
|
|
1576
|
-
warnings.push(`
|
|
1577
|
-
`${size.width}×${size.height} px for a ${tile.width}×${tile.height} px band; ` +
|
|
1578
|
-
"its full-page crop was kept");
|
|
1579
|
-
}
|
|
1580
|
-
else {
|
|
1581
|
-
shot = captured;
|
|
1731
|
+
warnings.push(`Native scrolled capture of ${tileId(position)} has different dimensions; probe incomplete.`);
|
|
1582
1732
|
}
|
|
1733
|
+
else
|
|
1734
|
+
shots.set(position, shot);
|
|
1583
1735
|
}
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
return shot;
|
|
1590
|
-
};
|
|
1736
|
+
}
|
|
1737
|
+
catch (error) {
|
|
1738
|
+
warnings.push(`Native scrolled capture failed: ${error instanceof Error ? error.message : String(error)}; probe incomplete.`);
|
|
1739
|
+
}
|
|
1740
|
+
const shoot = async (position) => shots.get(position);
|
|
1591
1741
|
// Every band is probed. A two-band sample (top and middle) missed a raster
|
|
1592
1742
|
// artifact that sat elsewhere on the page, and once every band has been
|
|
1593
1743
|
// shot by scrolling, the fallback below costs nothing extra: the same shots
|
|
@@ -1622,112 +1772,198 @@ async function reconcileCaptureFidelity(browser, tiles) {
|
|
|
1622
1772
|
}
|
|
1623
1773
|
/**
|
|
1624
1774
|
* Capture this render into a page review pack: one full-page screenshot,
|
|
1625
|
-
*
|
|
1775
|
+
* budgeted native tiles, one candidate set per
|
|
1626
1776
|
* --review-pack-scope selector, the serialized DOM, and the QA signature —
|
|
1627
1777
|
* all written to disk, with no vision call. The judge stage reads them back
|
|
1628
1778
|
* once the browser is gone.
|
|
1629
1779
|
*/
|
|
1780
|
+
export const REVIEW_CAPTURE_RECIPE = "native-full-page-scroll-probe/v2";
|
|
1781
|
+
async function buildReviewCapturePlan(browser, opts) {
|
|
1782
|
+
await browser.currentPage.evaluate(() => document.fonts.ready);
|
|
1783
|
+
const geometry = await browser.currentPage.evaluate(() => ({
|
|
1784
|
+
dpr: window.devicePixelRatio || 1,
|
|
1785
|
+
width: Math.max(document.documentElement.scrollWidth, document.body?.scrollWidth ?? 0),
|
|
1786
|
+
height: Math.max(document.documentElement.scrollHeight, document.body?.scrollHeight ?? 0),
|
|
1787
|
+
viewportWidth: window.innerWidth,
|
|
1788
|
+
viewportHeight: window.innerHeight,
|
|
1789
|
+
}));
|
|
1790
|
+
const { dpr } = geometry;
|
|
1791
|
+
const signature = await browser.qaSignature();
|
|
1792
|
+
// Attribute insertion order is serialization noise, not a source change.
|
|
1793
|
+
// Parse an inert document so canonicalization cannot mutate the rendered page.
|
|
1794
|
+
const canonicalDom = await browser.currentPage.evaluate((html) => {
|
|
1795
|
+
const document = new DOMParser().parseFromString(html, "text/html");
|
|
1796
|
+
for (const el of Array.from(document.querySelectorAll("*"))) {
|
|
1797
|
+
const attrs = Array.from(el.attributes)
|
|
1798
|
+
.map((a) => ({ name: a.name, value: a.value, ns: a.namespaceURI }))
|
|
1799
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
1800
|
+
for (const a of Array.from(el.attributes))
|
|
1801
|
+
el.removeAttributeNode(a);
|
|
1802
|
+
for (const a of attrs)
|
|
1803
|
+
el.setAttributeNS(a.ns, a.name, a.value);
|
|
1804
|
+
}
|
|
1805
|
+
return document.documentElement.outerHTML;
|
|
1806
|
+
}, signature.domHtml);
|
|
1807
|
+
const source = retainPageReviewSourceIdentity({ nodes: signature.nodes, stylesheets: signature.stylesheets, dom: canonicalDom }, opts.out ?? (opts.reviewPack ? resolve(opts.reviewPack, "source") : undefined));
|
|
1808
|
+
opts.captureSourceEvidence = source.evidence;
|
|
1809
|
+
const atoms = (await browser.visualAtoms()).map((a) => ({
|
|
1810
|
+
...a,
|
|
1811
|
+
top: Math.round(a.top * dpr),
|
|
1812
|
+
bottom: Math.round(a.bottom * dpr),
|
|
1813
|
+
}));
|
|
1814
|
+
const { band, overlap } = critiqueTilingKnobs(opts);
|
|
1815
|
+
const scopes = [];
|
|
1816
|
+
for (const selector of opts.reviewPackScope ?? []) {
|
|
1817
|
+
const rects = (await browser.elementTiles(selector)).map((r) => ({
|
|
1818
|
+
...r,
|
|
1819
|
+
x: Math.round(r.x * dpr),
|
|
1820
|
+
y: Math.round(r.y * dpr),
|
|
1821
|
+
width: Math.round(r.width * dpr),
|
|
1822
|
+
height: Math.round(r.height * dpr),
|
|
1823
|
+
}));
|
|
1824
|
+
scopes.push({ selector, rects });
|
|
1825
|
+
}
|
|
1826
|
+
const theme = opts.qaTheme === "dark" ? "dark" : "light";
|
|
1827
|
+
const viewport = opts.viewport ?? "desktop", state = opts.qaState ?? "default";
|
|
1828
|
+
return {
|
|
1829
|
+
schema: PAGE_REVIEW_CAPTURE_PLAN_SCHEMA,
|
|
1830
|
+
context_id: opts.reviewPackContext ?? `${viewport}--${theme}--${state}`,
|
|
1831
|
+
viewport,
|
|
1832
|
+
viewport_width: geometry.viewportWidth,
|
|
1833
|
+
viewport_height: geometry.viewportHeight,
|
|
1834
|
+
theme,
|
|
1835
|
+
state,
|
|
1836
|
+
dpr,
|
|
1837
|
+
page_width: Math.round(geometry.width * dpr),
|
|
1838
|
+
page_height: Math.round(geometry.height * dpr),
|
|
1839
|
+
source_digest: source.digest,
|
|
1840
|
+
recipe_version: REVIEW_CAPTURE_RECIPE,
|
|
1841
|
+
required_scopes: opts.reviewPackScope ?? [],
|
|
1842
|
+
candidates: reviewCandidateRects({
|
|
1843
|
+
width: Math.round(geometry.width * dpr),
|
|
1844
|
+
height: Math.round(geometry.height * dpr),
|
|
1845
|
+
bandHeight: band,
|
|
1846
|
+
overlap,
|
|
1847
|
+
atoms,
|
|
1848
|
+
scopes,
|
|
1849
|
+
}),
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1630
1852
|
async function captureReviewPackContext(browser, targetArg, navResult, opts, qaCapture) {
|
|
1631
1853
|
const packDir = opts.reviewPack;
|
|
1632
|
-
const
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1854
|
+
const plan = await buildReviewCapturePlan(browser, opts);
|
|
1855
|
+
let allocation;
|
|
1856
|
+
if (opts.captureTransactionAllocation) {
|
|
1857
|
+
try {
|
|
1858
|
+
allocation = validatePageReviewAllocation(opts.captureTransactionAllocation, plan);
|
|
1859
|
+
}
|
|
1860
|
+
catch (error) {
|
|
1861
|
+
throw new CaptureSourceChanged(String(error));
|
|
1862
|
+
}
|
|
1640
1863
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
: undefined;
|
|
1650
|
-
if (banded?.coverage.capped) {
|
|
1651
|
-
emit.log(`review-pack: page banded to the ${maxTiles}-tile cap (raise --check-critique-max-tiles for full coverage)`, "warn");
|
|
1652
|
-
}
|
|
1653
|
-
if (banded && banded.hitBands > 0) {
|
|
1654
|
-
emit.log(`review-pack: ${banded.hitBands} band(s) below the tile cap captured for ${hitRects.length} gate-hit rect(s)`, "info");
|
|
1655
|
-
}
|
|
1656
|
-
// Fidelity: prove the full-page screenshot against scrolled viewport
|
|
1657
|
-
// renders while the browser is still open; on disagreement the bands are
|
|
1658
|
-
// re-cut from scrolled captures (see reconcileCaptureFidelity).
|
|
1659
|
-
let bandTiles = banded?.tiles ?? [];
|
|
1660
|
-
let captureFidelity;
|
|
1661
|
-
const fidelityWarnings = [];
|
|
1662
|
-
if (bandTiles.length > 0) {
|
|
1663
|
-
const reconciled = await reconcileCaptureFidelity(browser, bandTiles);
|
|
1664
|
-
bandTiles = reconciled.tiles;
|
|
1665
|
-
captureFidelity = reconciled.fidelity;
|
|
1666
|
-
fidelityWarnings.push(...reconciled.warnings);
|
|
1667
|
-
for (const warning of reconciled.warnings)
|
|
1668
|
-
emit.log(warning, "warn");
|
|
1669
|
-
emit.log(`review-pack: capture fidelity ${captureFidelity.source} ` +
|
|
1670
|
-
`(${captureFidelity.probed.length} band(s) probed, ${captureFidelity.mismatched.length} mismatched)`, "info");
|
|
1671
|
-
}
|
|
1672
|
-
const scopeTiles = [];
|
|
1673
|
-
for (const selector of opts.reviewPackScope ?? []) {
|
|
1674
|
-
const elementRects = await browser.elementTiles(selector);
|
|
1675
|
-
const scoped = tilesFromFullPage(buffer, { ...tiling, elementRects });
|
|
1676
|
-
if (elementRects.length > maxTiles) {
|
|
1677
|
-
emit.log(`review-pack: ${elementRects.length} elements matched ${selector}, capped to ${maxTiles} tiles`, "warn");
|
|
1678
|
-
}
|
|
1679
|
-
scopeTiles.push({ selector, tiles: scoped.tiles, coverage: scoped.coverage });
|
|
1680
|
-
}
|
|
1681
|
-
// Coverage: the bands when present, else the scopes folded together
|
|
1682
|
-
// (heights worst-case, band counts add, capped sticky).
|
|
1683
|
-
let coverage = banded?.coverage;
|
|
1684
|
-
if (!coverage) {
|
|
1685
|
-
for (const scope of scopeTiles) {
|
|
1686
|
-
coverage = coverage
|
|
1687
|
-
? {
|
|
1688
|
-
page_height_px: Math.max(coverage.page_height_px, scope.coverage.page_height_px),
|
|
1689
|
-
reviewed_height_px: Math.min(coverage.reviewed_height_px, scope.coverage.reviewed_height_px),
|
|
1690
|
-
bands_total: coverage.bands_total + scope.coverage.bands_total,
|
|
1691
|
-
bands_reviewed: coverage.bands_reviewed + scope.coverage.bands_reviewed,
|
|
1692
|
-
capped: coverage.capped || scope.coverage.capped,
|
|
1693
|
-
}
|
|
1694
|
-
: scope.coverage;
|
|
1864
|
+
else if (opts.reviewPackAllocation) {
|
|
1865
|
+
try {
|
|
1866
|
+
allocation = validatePageReviewAllocation(JSON.parse(readFileSync(resolve(opts.reviewPackAllocation), "utf8")), plan);
|
|
1867
|
+
}
|
|
1868
|
+
catch (error) {
|
|
1869
|
+
if (error instanceof Error && opts.captureSourceEvidence)
|
|
1870
|
+
error.message += ` Current source identity: ${opts.captureSourceEvidence.path} (sha256 ${plan.source_digest}).`;
|
|
1871
|
+
throw error;
|
|
1695
1872
|
}
|
|
1696
1873
|
}
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1874
|
+
else {
|
|
1875
|
+
// Standalone capture retains its 24-tile default; it still obeys one hard total.
|
|
1876
|
+
const maxTiles = critiqueTilingKnobs(opts).maxTiles;
|
|
1877
|
+
const hits = parseReviewPackHitRects(opts.reviewPackHitRect);
|
|
1878
|
+
for (const c of plan.candidates) {
|
|
1879
|
+
c.gate_hits = hits.flatMap((h, i) => c.rect.x < (h.x + h.width) * plan.dpr &&
|
|
1880
|
+
c.rect.x + c.rect.width > h.x * plan.dpr &&
|
|
1881
|
+
c.rect.y < (h.y + h.height) * plan.dpr &&
|
|
1882
|
+
c.rect.y + c.rect.height > h.y * plan.dpr
|
|
1883
|
+
? [{ check_id: `hit:${i + 1}`, severity: "high" }]
|
|
1884
|
+
: []);
|
|
1885
|
+
}
|
|
1886
|
+
allocation = allocateTileBudget([plan], maxTiles).contexts[0];
|
|
1887
|
+
}
|
|
1888
|
+
const start = performance.now();
|
|
1889
|
+
const buffer = await browser.fullPageScreenshotBuffer();
|
|
1890
|
+
const size = pngDimensions(buffer);
|
|
1891
|
+
if (size.width !== plan.page_width || size.height !== plan.page_height)
|
|
1892
|
+
throw opts.captureTransactionAllocation
|
|
1893
|
+
? new CaptureSourceChanged("Page review image geometry changed after planning.")
|
|
1894
|
+
: new Error("Page review image geometry changed after planning; rerun the review.");
|
|
1895
|
+
const decoded = PNG.sync.read(buffer);
|
|
1896
|
+
const fullPageMs = performance.now() - start;
|
|
1897
|
+
const byId = new Map(plan.candidates.map((c) => [c.id, c]));
|
|
1898
|
+
const selected = allocation.selected_ids.map((id) => byId.get(id));
|
|
1899
|
+
const cropStart = performance.now();
|
|
1900
|
+
const tiles = selected.map((c, index) => ({
|
|
1901
|
+
index,
|
|
1902
|
+
label: c.label,
|
|
1903
|
+
x: c.rect.x,
|
|
1904
|
+
scrollY: c.rect.y,
|
|
1905
|
+
width: c.rect.width,
|
|
1906
|
+
height: c.rect.height,
|
|
1907
|
+
pngBase64: cropNativePng(decoded, c.rect).toString("base64"),
|
|
1700
1908
|
}));
|
|
1909
|
+
const cropMs = performance.now() - cropStart;
|
|
1910
|
+
const probeStart = performance.now();
|
|
1911
|
+
const reconciled = await reconcileCaptureFidelity(browser, tiles);
|
|
1912
|
+
const probeMs = performance.now() - probeStart;
|
|
1913
|
+
// A lazy image can change the document bottom while a native probe is running.
|
|
1914
|
+
// Revalidate even failed probes so a changed plan gets the same bounded retry.
|
|
1915
|
+
if (opts.captureTransactionAllocation) {
|
|
1916
|
+
const after = await buildReviewCapturePlan(browser, opts);
|
|
1917
|
+
try {
|
|
1918
|
+
validatePageReviewAllocation(allocation, after);
|
|
1919
|
+
}
|
|
1920
|
+
catch (error) {
|
|
1921
|
+
throw new CaptureSourceChanged(String(error));
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
if (reconciled.fidelity.probed.length !== tiles.length)
|
|
1925
|
+
throw new Error("Page review native fidelity probe incomplete; no context was saved. " +
|
|
1926
|
+
reconciled.warnings.join("; "));
|
|
1927
|
+
const bandTiles = [];
|
|
1928
|
+
const scopes = new Map();
|
|
1929
|
+
for (const [index, c] of selected.entries()) {
|
|
1930
|
+
const tile = reconciled.tiles[index];
|
|
1931
|
+
if (!c.scope)
|
|
1932
|
+
bandTiles.push(tile);
|
|
1933
|
+
else {
|
|
1934
|
+
const group = scopes.get(c.scope) ?? [];
|
|
1935
|
+
group.push(tile);
|
|
1936
|
+
scopes.set(c.scope, group);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1701
1939
|
const capture = qaCapture ?? (await captureQaState(browser, targetArg, navResult.url, opts));
|
|
1702
|
-
const
|
|
1703
|
-
context: {
|
|
1704
|
-
...capture.context,
|
|
1705
|
-
...(opts.reviewPackContext ? { id: opts.reviewPackContext } : {}),
|
|
1706
|
-
},
|
|
1940
|
+
const writeContext = (destination) => writePackContext(destination, {
|
|
1941
|
+
context: { ...capture.context, id: plan.context_id },
|
|
1707
1942
|
url: navResult.url,
|
|
1708
1943
|
title: navResult.title,
|
|
1709
1944
|
fullPage: buffer,
|
|
1710
|
-
pageWidth:
|
|
1711
|
-
pageHeight:
|
|
1945
|
+
pageWidth: plan.page_width / plan.dpr,
|
|
1946
|
+
pageHeight: plan.page_height / plan.dpr,
|
|
1712
1947
|
tiles: bandTiles,
|
|
1713
|
-
coverage: coverage
|
|
1714
|
-
|
|
1715
|
-
reviewed_height_px: 0,
|
|
1716
|
-
bands_total: 0,
|
|
1717
|
-
bands_reviewed: 0,
|
|
1718
|
-
capped: false,
|
|
1719
|
-
},
|
|
1720
|
-
scopeTiles: scopeTiles.map(({ selector, tiles }) => ({ selector, tiles })),
|
|
1948
|
+
coverage: allocation.coverage,
|
|
1949
|
+
scopeTiles: [...scopes].map(([selector, tiles]) => ({ selector, tiles })),
|
|
1721
1950
|
signature: capture.signature,
|
|
1722
1951
|
domHtml: capture.domHtml,
|
|
1723
|
-
|
|
1724
|
-
|
|
1952
|
+
captureFidelity: reconciled.fidelity,
|
|
1953
|
+
viewportSize: { width: plan.viewport_width, height: plan.viewport_height },
|
|
1954
|
+
dpr: plan.dpr,
|
|
1955
|
+
recipeVersion: plan.recipe_version,
|
|
1956
|
+
allocationCoverage: allocation.coverage,
|
|
1957
|
+
capturePlan: allocation.plan,
|
|
1958
|
+
hitBands: 0,
|
|
1725
1959
|
});
|
|
1726
|
-
|
|
1727
|
-
(
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1960
|
+
const record = opts.captureTransactionAllocation
|
|
1961
|
+
? writeCaptureTransaction(packDir, plan.context_id, writeContext)
|
|
1962
|
+
: writeContext(packDir);
|
|
1963
|
+
const warnings = [...reconciled.warnings];
|
|
1964
|
+
if (allocation.coverage.capped)
|
|
1965
|
+
warnings.push(`Review coverage is incomplete: ${allocation.coverage.uncovered_intervals.length} uncovered intervals and ${allocation.coverage.omitted_scopes.length} omitted scopes.`);
|
|
1966
|
+
emit.log(`review-pack: ${record.id} captured ${record.tiles.length} tiles; full-page ${Math.round(fullPageMs)} ms, crop ${Math.round(cropMs)} ms, native probe ${Math.round(probeMs)} ms`, "info");
|
|
1731
1967
|
return {
|
|
1732
1968
|
dir: packDir,
|
|
1733
1969
|
context_id: record.id,
|
|
@@ -1735,9 +1971,8 @@ async function captureReviewPackContext(browser, targetArg, navResult, opts, qaC
|
|
|
1735
1971
|
scopes: record.scopes,
|
|
1736
1972
|
coverage: record.coverage,
|
|
1737
1973
|
files: record.files,
|
|
1738
|
-
|
|
1739
|
-
...(
|
|
1740
|
-
...(fidelityWarnings.length > 0 ? { warnings: fidelityWarnings } : {}),
|
|
1974
|
+
capture_fidelity: reconciled.fidelity,
|
|
1975
|
+
...(warnings.length ? { warnings } : {}),
|
|
1741
1976
|
};
|
|
1742
1977
|
}
|
|
1743
1978
|
async function captureCritiqueTiles(browser, opts, tileBudgetPx) {
|
|
@@ -1858,7 +2093,7 @@ async function waitForTerminalEnter(signal) {
|
|
|
1858
2093
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1859
2094
|
});
|
|
1860
2095
|
}
|
|
1861
|
-
/** Upper bound on --review-pack-hit-rect values
|
|
2096
|
+
/** Upper bound on manually supplied --review-pack-hit-rect values. */
|
|
1862
2097
|
const REVIEW_PACK_HIT_RECT_MAX = 50;
|
|
1863
2098
|
/** `x,y,w,h` integer rectangles from repeated --review-pack-hit-rect flags. */
|
|
1864
2099
|
function parseReviewPackHitRects(specs) {
|