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
|
@@ -12,9 +12,12 @@
|
|
|
12
12
|
// Storage is content-under-a-key, like visual-diff baselines: a directory per
|
|
13
13
|
// target, a subdirectory per context, atomic writes (tmp + rename) so a
|
|
14
14
|
// crashed writer never leaves a half-baseline the next run would trust.
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
15
16
|
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
|
|
16
17
|
import { homedir } from "node:os";
|
|
17
18
|
import { resolve } from "node:path";
|
|
19
|
+
import { PAGE_REVIEW_DECISIONS_SCHEMA, } from "./page-review-contracts.js";
|
|
20
|
+
import { machineFindingKey, validateNativeEvidence } from "./page-review-evidence.js";
|
|
18
21
|
import { fnv1a32 } from "./qa-plan.js";
|
|
19
22
|
const SNAPSHOT_DIR = resolve(homedir(), ".cache", "harnery", "qa-snapshots");
|
|
20
23
|
/** Filesystem-safe key for a target URL/file + render context. The digest
|
|
@@ -47,72 +50,280 @@ function contextDirName(context) {
|
|
|
47
50
|
function snapshotDir(target, context, opts) {
|
|
48
51
|
return resolve(opts.root ?? SNAPSHOT_DIR, qaSnapshotKey(target), contextDirName(context));
|
|
49
52
|
}
|
|
53
|
+
/** Serialize both owned slots across processes. A stuck writer fails closed. */
|
|
54
|
+
function withStoreLock(dir, operation) {
|
|
55
|
+
mkdirSync(dir, { recursive: true });
|
|
56
|
+
const lock = resolve(dir, ".write-lock");
|
|
57
|
+
const deadline = Date.now() + 5_000;
|
|
58
|
+
while (true) {
|
|
59
|
+
try {
|
|
60
|
+
mkdirSync(lock);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error.code !== "EEXIST")
|
|
65
|
+
throw error;
|
|
66
|
+
if (Date.now() >= deadline)
|
|
67
|
+
throw new Error(`Snapshot writer lock busy: ${dir}`);
|
|
68
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return operation();
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
rmSync(lock, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function validateDecisionSlot(target, context, slot) {
|
|
79
|
+
const only = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
|
|
80
|
+
if (slot.schema !== PAGE_REVIEW_DECISIONS_SCHEMA ||
|
|
81
|
+
slot.complete !== true ||
|
|
82
|
+
slot.target !== target ||
|
|
83
|
+
slot.context.viewport !== context.viewport ||
|
|
84
|
+
slot.context.theme !== context.theme ||
|
|
85
|
+
slot.context.state !== context.state ||
|
|
86
|
+
slot.context.critique_contract_version !== 2 ||
|
|
87
|
+
![slot.context.viewport_width, slot.context.viewport_height, slot.context.dpr].every((n) => Number.isFinite(n) && n > 0) ||
|
|
88
|
+
!/^[a-f0-9]{64}$/.test(slot.context.rubric_digest) ||
|
|
89
|
+
!slot.context.recipe_version ||
|
|
90
|
+
!only(slot, [
|
|
91
|
+
"schema",
|
|
92
|
+
"complete",
|
|
93
|
+
"target",
|
|
94
|
+
"context",
|
|
95
|
+
"source_run",
|
|
96
|
+
"source_revision",
|
|
97
|
+
"reviewer",
|
|
98
|
+
"reviewed_at",
|
|
99
|
+
"decisions",
|
|
100
|
+
]) ||
|
|
101
|
+
!only(slot.context, [
|
|
102
|
+
"viewport",
|
|
103
|
+
"viewport_width",
|
|
104
|
+
"viewport_height",
|
|
105
|
+
"theme",
|
|
106
|
+
"state",
|
|
107
|
+
"dpr",
|
|
108
|
+
"recipe_version",
|
|
109
|
+
"rubric_digest",
|
|
110
|
+
"critique_contract_version",
|
|
111
|
+
]) ||
|
|
112
|
+
!slot.reviewer?.trim() ||
|
|
113
|
+
!Number.isFinite(Date.parse(slot.reviewed_at)) ||
|
|
114
|
+
!slot.source_run ||
|
|
115
|
+
!Array.isArray(slot.decisions))
|
|
116
|
+
throw new Error("Invalid durable review decision slot");
|
|
117
|
+
const keys = new Set();
|
|
118
|
+
for (const decision of slot.decisions) {
|
|
119
|
+
if (!decision.reviewer?.trim() ||
|
|
120
|
+
!Number.isFinite(Date.parse(decision.at)) ||
|
|
121
|
+
keys.has(decision.finding_key) ||
|
|
122
|
+
!/^[a-f0-9]{64}$/.test(decision.tile_pixel_digest) ||
|
|
123
|
+
!only(decision, [
|
|
124
|
+
"finding_key",
|
|
125
|
+
"tile_pixel_digest",
|
|
126
|
+
"rect",
|
|
127
|
+
"finding",
|
|
128
|
+
"disposition",
|
|
129
|
+
"reviewer",
|
|
130
|
+
"at",
|
|
131
|
+
"note",
|
|
132
|
+
]) ||
|
|
133
|
+
!only(decision.finding, ["severity", "category", "description"]) ||
|
|
134
|
+
!only(decision.rect, ["x", "y", "width", "height"]) ||
|
|
135
|
+
![decision.rect.x, decision.rect.y, decision.rect.width, decision.rect.height].every(Number.isInteger) ||
|
|
136
|
+
decision.rect.x < 0 ||
|
|
137
|
+
decision.rect.y < 0 ||
|
|
138
|
+
decision.rect.width < 1 ||
|
|
139
|
+
decision.rect.height < 1 ||
|
|
140
|
+
!["artifact", "not-a-defect", "confirmed", "duplicate-of-gate"].includes(decision.disposition) ||
|
|
141
|
+
decision.finding.category === "provider-error" ||
|
|
142
|
+
machineFindingKey(slot.context, { rect: decision.rect, pixel_digest: decision.tile_pixel_digest }, decision.finding) !== decision.finding_key) {
|
|
143
|
+
throw new Error("Invalid durable review finding identity");
|
|
144
|
+
}
|
|
145
|
+
keys.add(decision.finding_key);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export function loadReviewDecisions(target, context, opts = {}) {
|
|
149
|
+
const dir = snapshotDir(target, context, opts);
|
|
150
|
+
if (!existsSync(dir))
|
|
151
|
+
return null;
|
|
152
|
+
try {
|
|
153
|
+
return withStoreLock(dir, () => {
|
|
154
|
+
const slot = JSON.parse(readFileSync(resolve(dir, "review-decisions.json"), "utf8"));
|
|
155
|
+
validateDecisionSlot(target, context, slot);
|
|
156
|
+
return slot;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Decisions contain text and digests only; replacing a baseline cannot touch this slot. */
|
|
164
|
+
export function saveReviewDecisions(target, context, slot, opts = {}) {
|
|
165
|
+
validateDecisionSlot(target, context, slot);
|
|
166
|
+
const dir = snapshotDir(target, context, opts);
|
|
167
|
+
withStoreLock(dir, () => {
|
|
168
|
+
const path = resolve(dir, "review-decisions.json");
|
|
169
|
+
const temp = `${path}.tmp-${randomUUID()}`;
|
|
170
|
+
try {
|
|
171
|
+
writeFileSync(temp, `${JSON.stringify(slot)}\n`);
|
|
172
|
+
renameSync(temp, path);
|
|
173
|
+
}
|
|
174
|
+
finally {
|
|
175
|
+
rmSync(temp, { force: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
}
|
|
50
179
|
/**
|
|
51
180
|
* Persist a QA snapshot atomically. The whole context directory is staged as
|
|
52
181
|
* `<dir>.tmp-<pid>` and renamed into place, replacing any prior snapshot for
|
|
53
182
|
* the same target + context — a reader never observes a partial write.
|
|
54
183
|
*/
|
|
55
184
|
export function saveQaSnapshot(target, context, content, opts = {}) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
185
|
+
const container = snapshotDir(target, context, opts);
|
|
186
|
+
return withStoreLock(container, () => {
|
|
187
|
+
const dir = resolve(container, "baseline");
|
|
188
|
+
const staging = `${dir}.tmp-${randomUUID()}`;
|
|
189
|
+
const previous = `${dir}.previous`;
|
|
190
|
+
if (content.tileEvidence && content.screenshotPng)
|
|
191
|
+
throw new Error("A pack native-tile baseline must not retain a full-page image");
|
|
192
|
+
if (content.tileEvidence && !validateNativeEvidence(content.tileEvidence))
|
|
193
|
+
throw new Error("Invalid native tile baseline evidence");
|
|
194
|
+
mkdirSync(staging, { recursive: true });
|
|
195
|
+
try {
|
|
196
|
+
writeFileSync(resolve(staging, "signature.json"), `${JSON.stringify(content.signature)}\n`);
|
|
197
|
+
if (content.domHtml !== undefined) {
|
|
198
|
+
writeFileSync(resolve(staging, "dom.html"), content.domHtml);
|
|
199
|
+
}
|
|
200
|
+
if (content.screenshotPng !== undefined) {
|
|
201
|
+
writeFileSync(resolve(staging, "screenshot.png"), content.screenshotPng);
|
|
202
|
+
}
|
|
203
|
+
if (content.critique !== undefined) {
|
|
204
|
+
writeFileSync(resolve(staging, "critique.json"), `${JSON.stringify(content.critique)}\n`);
|
|
205
|
+
}
|
|
206
|
+
if (content.tileEvidence) {
|
|
207
|
+
const tiles = content.tileEvidence.tiles.map(({ png, ...tile }, i) => {
|
|
208
|
+
const file = `tile-${i}.png`;
|
|
209
|
+
writeFileSync(resolve(staging, file), png);
|
|
210
|
+
return { ...tile, file };
|
|
211
|
+
});
|
|
212
|
+
writeFileSync(resolve(staging, "native-evidence.json"), JSON.stringify({ ...content.tileEvidence, tiles }));
|
|
213
|
+
}
|
|
214
|
+
// Recovery of a writer interrupted after moving its old baseline.
|
|
215
|
+
if (!existsSync(dir) && existsSync(previous))
|
|
216
|
+
renameSync(previous, dir);
|
|
217
|
+
rmSync(previous, { recursive: true, force: true });
|
|
218
|
+
if (existsSync(dir))
|
|
219
|
+
renameSync(dir, previous);
|
|
220
|
+
try {
|
|
221
|
+
renameSync(staging, dir);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (existsSync(previous))
|
|
225
|
+
renameSync(previous, dir);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
rmSync(previous, { recursive: true, force: true });
|
|
229
|
+
// Remove superseded legacy baseline files, never the independent decision slot.
|
|
230
|
+
for (const name of [
|
|
231
|
+
"signature.json",
|
|
232
|
+
"dom.html",
|
|
233
|
+
"screenshot.png",
|
|
234
|
+
"critique.json",
|
|
235
|
+
"native-evidence.json",
|
|
236
|
+
]) {
|
|
237
|
+
rmSync(resolve(container, name), { force: true });
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
signature: content.signature,
|
|
241
|
+
path: dir,
|
|
242
|
+
...(content.domHtml !== undefined ? { domHtmlPath: resolve(dir, "dom.html") } : {}),
|
|
243
|
+
...(content.screenshotPng !== undefined
|
|
244
|
+
? { screenshotPath: resolve(dir, "screenshot.png") }
|
|
245
|
+
: {}),
|
|
246
|
+
...(content.critique ? { critique: content.critique } : {}),
|
|
247
|
+
...(content.tileEvidence ? { tileEvidence: content.tileEvidence } : {}),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
rmSync(staging, { recursive: true, force: true });
|
|
252
|
+
}
|
|
253
|
+
});
|
|
80
254
|
}
|
|
81
255
|
/** Load the persisted snapshot for a target + context, or null. A corrupt
|
|
82
256
|
* signature file returns null (and the caller re-acquires a baseline) rather
|
|
83
257
|
* than throwing — a bad cache entry must never block QA. */
|
|
84
258
|
export function loadQaSnapshot(target, context, opts = {}) {
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
if (!existsSync(sigPath))
|
|
259
|
+
const container = snapshotDir(target, context, opts);
|
|
260
|
+
if (!existsSync(container))
|
|
88
261
|
return null;
|
|
89
262
|
try {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
263
|
+
return withStoreLock(container, () => {
|
|
264
|
+
const baseline = resolve(container, "baseline");
|
|
265
|
+
const dir = existsSync(baseline)
|
|
266
|
+
? baseline
|
|
267
|
+
: existsSync(`${baseline}.previous`)
|
|
268
|
+
? `${baseline}.previous`
|
|
269
|
+
: container;
|
|
270
|
+
const sigPath = resolve(dir, "signature.json");
|
|
271
|
+
if (!existsSync(sigPath))
|
|
272
|
+
return null;
|
|
99
273
|
try {
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
|
|
274
|
+
const signature = JSON.parse(readFileSync(sigPath, "utf8"));
|
|
275
|
+
if (!signature ||
|
|
276
|
+
!Array.isArray(signature.nodes) ||
|
|
277
|
+
!Array.isArray(signature.stylesheets)) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
const domHtmlPath = resolve(dir, "dom.html");
|
|
281
|
+
const screenshotPath = resolve(dir, "screenshot.png");
|
|
282
|
+
const critiquePath = resolve(dir, "critique.json");
|
|
283
|
+
let critique;
|
|
284
|
+
let tileEvidence;
|
|
285
|
+
try {
|
|
286
|
+
const raw = JSON.parse(readFileSync(resolve(dir, "native-evidence.json"), "utf8"));
|
|
287
|
+
const candidate = {
|
|
288
|
+
...raw,
|
|
289
|
+
tiles: raw.tiles.map((tile) => {
|
|
290
|
+
if (!/^tile-\d+\.png$/.test(tile.file))
|
|
291
|
+
throw new Error("Invalid native tile path");
|
|
292
|
+
return { ...tile, png: readFileSync(resolve(dir, tile.file)) };
|
|
293
|
+
}),
|
|
294
|
+
};
|
|
295
|
+
if (validateNativeEvidence(candidate))
|
|
296
|
+
tileEvidence = candidate;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
/* Missing or corrupt evidence is a cache miss. */
|
|
103
300
|
}
|
|
301
|
+
if (existsSync(critiquePath)) {
|
|
302
|
+
try {
|
|
303
|
+
const parsed = JSON.parse(readFileSync(critiquePath, "utf8"));
|
|
304
|
+
if (parsed &&
|
|
305
|
+
typeof parsed.contract_version === "number" &&
|
|
306
|
+
Array.isArray(parsed.tiles)) {
|
|
307
|
+
critique = parsed;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// corrupt critique.json degrades to a reuse miss, never an error
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
signature,
|
|
316
|
+
path: dir,
|
|
317
|
+
...(existsSync(domHtmlPath) ? { domHtmlPath } : {}),
|
|
318
|
+
...(existsSync(screenshotPath) ? { screenshotPath } : {}),
|
|
319
|
+
...(critique ? { critique } : {}),
|
|
320
|
+
...(tileEvidence ? { tileEvidence } : {}),
|
|
321
|
+
};
|
|
104
322
|
}
|
|
105
323
|
catch {
|
|
106
|
-
|
|
324
|
+
return null;
|
|
107
325
|
}
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
signature,
|
|
111
|
-
path: dir,
|
|
112
|
-
...(existsSync(domHtmlPath) ? { domHtmlPath } : {}),
|
|
113
|
-
...(existsSync(screenshotPath) ? { screenshotPath } : {}),
|
|
114
|
-
...(critique ? { critique } : {}),
|
|
115
|
-
};
|
|
326
|
+
});
|
|
116
327
|
}
|
|
117
328
|
catch {
|
|
118
329
|
return null;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { PageReviewCapturePlan, PageReviewContextAllocation } from "./page-review-contracts.js";
|
|
2
|
+
/** Publish a complete context atomically; failed writes stay outside discoverable contexts. */
|
|
3
|
+
export declare function writeCaptureTransaction<T>(packDir: string, contextId: string, write: (stage: string) => T): T;
|
|
4
|
+
/** A preliminary allocation reserves pixels; it never supplies current source evidence. */
|
|
5
|
+
export declare function allocateCaptureReservation(reservation: PageReviewContextAllocation, current: PageReviewCapturePlan): PageReviewContextAllocation;
|
|
6
|
+
export declare class CaptureSourceChanged extends Error {
|
|
7
|
+
constructor(message?: string);
|
|
8
|
+
}
|
|
9
|
+
/** Gate-hit annotations may change, but source and every native rectangle must stay fixed. */
|
|
10
|
+
export declare function assertGatePlanStable(before: PageReviewCapturePlan, after: PageReviewCapturePlan): void;
|
|
11
|
+
/** One retry recollects every gate and allocation; no previous green result survives. */
|
|
12
|
+
export declare function runCaptureTransaction<T>(collectAndAllocate: (attempt: number) => Promise<void>, capture: () => Promise<T>, recordFailure: (attempt: number, error: unknown) => void): Promise<T>;
|
|
13
|
+
//# sourceMappingURL=review-capture-transaction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-capture-transaction.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/review-capture-transaction.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,qBAAqB,EACrB,2BAA2B,EAC5B,MAAM,4BAA4B,CAAC;AAGpC,+FAA+F;AAC/F,wBAAgB,uBAAuB,CAAC,CAAC,EACvC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,CAAC,GAC1B,CAAC,CAgBH;AAED,2FAA2F;AAC3F,wBAAgB,0BAA0B,CACxC,WAAW,EAAE,2BAA2B,EACxC,OAAO,EAAE,qBAAqB,GAC7B,2BAA2B,CA6B7B;AAED,qBAAa,oBAAqB,SAAQ,KAAK;gBACjC,OAAO,SAAiE;CAIrF;AAED,8FAA8F;AAC9F,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,qBAAqB,GAC3B,IAAI,CAKN;AAED,yFAAyF;AACzF,wBAAsB,qBAAqB,CAAC,CAAC,EAC3C,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,EACtD,OAAO,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACzB,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,GACvD,OAAO,CAAC,CAAC,CAAC,CAWZ"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { allocateTileBudget, capturePlanDigest, validatePageReviewAllocation, } from "./page-review-budget.js";
|
|
4
|
+
import { packPaths } from "./page-review-pack.js";
|
|
5
|
+
/** Publish a complete context atomically; failed writes stay outside discoverable contexts. */
|
|
6
|
+
export function writeCaptureTransaction(packDir, contextId, write) {
|
|
7
|
+
mkdirSync(packDir, { recursive: true, mode: 0o700 });
|
|
8
|
+
const stage = mkdtempSync(join(packDir, ".pending-capture-"));
|
|
9
|
+
try {
|
|
10
|
+
const result = write(stage);
|
|
11
|
+
const destination = packPaths(packDir).contextDir(contextId);
|
|
12
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
13
|
+
if (existsSync(destination))
|
|
14
|
+
throw new Error("Capture context already exists; refusing to replace prior evidence.");
|
|
15
|
+
renameSync(packPaths(stage).contextDir(contextId), destination);
|
|
16
|
+
rmSync(stage, { recursive: true });
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
if (error instanceof Error)
|
|
21
|
+
error.message += ` Incomplete capture retained at ${stage}.`;
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** A preliminary allocation reserves pixels; it never supplies current source evidence. */
|
|
26
|
+
export function allocateCaptureReservation(reservation, current) {
|
|
27
|
+
validatePageReviewAllocation(reservation, reservation.plan);
|
|
28
|
+
for (const key of [
|
|
29
|
+
"context_id",
|
|
30
|
+
"viewport",
|
|
31
|
+
"viewport_width",
|
|
32
|
+
"viewport_height",
|
|
33
|
+
"theme",
|
|
34
|
+
"state",
|
|
35
|
+
"dpr",
|
|
36
|
+
"recipe_version",
|
|
37
|
+
]) {
|
|
38
|
+
if (reservation.plan[key] !== current[key])
|
|
39
|
+
throw new Error(`Capture reservation changed ${key}.`);
|
|
40
|
+
}
|
|
41
|
+
if (reservation.plan.required_scopes.some((scope) => !current.required_scopes.includes(scope)))
|
|
42
|
+
throw new Error("Capture reservation lost a required scope.");
|
|
43
|
+
// Reservations sum to the original global allocation, even when its per-context
|
|
44
|
+
// ceiling was higher than the number of tiles actually assigned to that context.
|
|
45
|
+
const ceiling = reservation.selected_ids.length;
|
|
46
|
+
const allocation = allocateTileBudget([current], ceiling, { [current.context_id]: ceiling })
|
|
47
|
+
.contexts[0];
|
|
48
|
+
if (allocation.coverage.capped ||
|
|
49
|
+
allocation.coverage.omitted_scopes.length ||
|
|
50
|
+
allocation.coverage.uncovered_intervals.length)
|
|
51
|
+
throw new Error("Fresh native capture coverage exceeds the reserved tile budget.");
|
|
52
|
+
return validatePageReviewAllocation(allocation, current);
|
|
53
|
+
}
|
|
54
|
+
export class CaptureSourceChanged extends Error {
|
|
55
|
+
constructor(message = "Page source changed during the gate and capture transaction.") {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "CaptureSourceChanged";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Gate-hit annotations may change, but source and every native rectangle must stay fixed. */
|
|
61
|
+
export function assertGatePlanStable(before, after) {
|
|
62
|
+
if (capturePlanDigest(before) !== capturePlanDigest(after))
|
|
63
|
+
throw new CaptureSourceChanged("Page source or geometry changed while deterministic gates were running.");
|
|
64
|
+
}
|
|
65
|
+
/** One retry recollects every gate and allocation; no previous green result survives. */
|
|
66
|
+
export async function runCaptureTransaction(collectAndAllocate, capture, recordFailure) {
|
|
67
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
68
|
+
try {
|
|
69
|
+
await collectAndAllocate(attempt);
|
|
70
|
+
return await capture();
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
recordFailure(attempt, error);
|
|
74
|
+
if (!(error instanceof CaptureSourceChanged) || attempt === 2)
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw new Error("Capture transaction did not complete.");
|
|
79
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"standalone-html.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/standalone-html.ts"],"names":[],"mappings":"AAmBA,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,QAAa,CAAC;AAErD,iEAAiE;AACjE,eAAO,MAAM,gCAAgC,QAAkB,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,gBAAgB,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,yBAAyB,IAAI,CAC3C,IAAI,EAAE,qBAAqB,KACxB,OAAO,CAAC,oBAAoB,CAAC,
|
|
1
|
+
{"version":3,"file":"standalone-html.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/standalone-html.ts"],"names":[],"mappings":"AAmBA,mFAAmF;AACnF,eAAO,MAAM,0BAA0B,QAAa,CAAC;AAErD,iEAAiE;AACjE,eAAO,MAAM,gCAAgC,QAAkB,CAAC;AAEhE,MAAM,WAAW,qBAAqB;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,gBAAgB,EAAE,MAAM,CAAC;IACzB,wEAAwE;IACxE,eAAe,EAAE,MAAM,CAAC;IACxB,6CAA6C;IAC7C,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,wBAAgB,yBAAyB,IAAI,CAC3C,IAAI,EAAE,qBAAqB,KACxB,OAAO,CAAC,oBAAoB,CAAC,CAyQjC"}
|
|
@@ -45,7 +45,9 @@ export function buildStandaloneHtmlScript() {
|
|
|
45
45
|
return cached;
|
|
46
46
|
let result = null;
|
|
47
47
|
try {
|
|
48
|
-
|
|
48
|
+
// Retain access to authenticated same-origin assets without imposing
|
|
49
|
+
// credentialed CORS on public assets served by another origin.
|
|
50
|
+
const res = await fetch(url, { credentials: "same-origin" });
|
|
49
51
|
if (res.ok) {
|
|
50
52
|
const buf = await res.arrayBuffer();
|
|
51
53
|
const fits = buf.byteLength <= maxResourceBytes &&
|
|
@@ -137,7 +139,7 @@ export function buildStandaloneHtmlScript() {
|
|
|
137
139
|
}
|
|
138
140
|
let css = null;
|
|
139
141
|
try {
|
|
140
|
-
const res = await fetch(url, { credentials: "
|
|
142
|
+
const res = await fetch(url, { credentials: "same-origin" });
|
|
141
143
|
if (res.ok)
|
|
142
144
|
css = await res.text();
|
|
143
145
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export interface CaptureFingerprint {
|
|
2
|
+
version: string;
|
|
3
|
+
sha256: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CaptureThumbnailAssociation {
|
|
6
|
+
schema_version: 2;
|
|
7
|
+
producer: "browser-standalone-capture";
|
|
8
|
+
source: CaptureFingerprint & {
|
|
9
|
+
path: string;
|
|
10
|
+
};
|
|
11
|
+
preview: CaptureFingerprint & {
|
|
12
|
+
path: string;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/** Take immediately after the screenshot writer completes, before asynchronous
|
|
16
|
+
* HTML serialization. A later writer cannot silently substitute its PNG. */
|
|
17
|
+
export declare function captureThumbnailFingerprint(pngPath: string): CaptureFingerprint | null;
|
|
18
|
+
/** Associate only the full-document, standalone HTML and exact PNG produced by
|
|
19
|
+
* this capture operation. No basename inference and no reads of source pages.
|
|
20
|
+
* A linked asset, selector fragment, changed output, oversize file or output
|
|
21
|
+
* outside a managed artifact workspace safely skips registration.
|
|
22
|
+
* The hidden sidecar binds canonical repository paths, checked file versions
|
|
23
|
+
* and SHA256 receipts. Web readers also require an empty local asset graph;
|
|
24
|
+
* manual registrations bind their own complete dependency graph instead. */
|
|
25
|
+
export declare function registerCaptureThumbnail(input: {
|
|
26
|
+
htmlPath: string;
|
|
27
|
+
pngPath: string;
|
|
28
|
+
html: string;
|
|
29
|
+
preview: CaptureFingerprint;
|
|
30
|
+
fullDocument: boolean;
|
|
31
|
+
stylesheetsLinked: number;
|
|
32
|
+
resourcesLinked: number;
|
|
33
|
+
}): string | null;
|
|
34
|
+
//# sourceMappingURL=thumbnail-association.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thumbnail-association.d.ts","sourceRoot":"","sources":["../../../src/lib/browser/thumbnail-association.ts"],"names":[],"mappings":"AAiBA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,WAAW,2BAA2B;IAC1C,cAAc,EAAE,CAAC,CAAC;IAClB,QAAQ,EAAE,4BAA4B,CAAC;IACvC,MAAM,EAAE,kBAAkB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,OAAO,EAAE,kBAAkB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CAChD;AA8BD;4EAC4E;AAC5E,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,kBAAkB,GAAG,IAAI,CAMtF;AAwBD;;;;;;4EAM4E;AAC5E,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,kBAAkB,CAAC;IAC5B,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;CACzB,GAAG,MAAM,GAAG,IAAI,CAsFhB"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync, realpathSync, renameSync, statSync, unlinkSync, writeSync, } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
const MAX_HTML = 2 * 1024 * 1024;
|
|
5
|
+
const MAX_PNG = 32 * 1024 * 1024;
|
|
6
|
+
function version(fd) {
|
|
7
|
+
const s = fstatSync(fd);
|
|
8
|
+
return `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}:${s.ctimeMs}`;
|
|
9
|
+
}
|
|
10
|
+
function fingerprint(filename, maxBytes) {
|
|
11
|
+
const canonical = realpathSync(filename);
|
|
12
|
+
const before = statSync(canonical);
|
|
13
|
+
if (!before.isFile() || before.size > maxBytes)
|
|
14
|
+
return null;
|
|
15
|
+
const fd = openSync(canonical, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
16
|
+
try {
|
|
17
|
+
const opened = fstatSync(fd);
|
|
18
|
+
if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino)
|
|
19
|
+
return null;
|
|
20
|
+
const initial = version(fd);
|
|
21
|
+
const hash = createHash("sha256");
|
|
22
|
+
const bytes = Buffer.alloc(64 * 1024);
|
|
23
|
+
let offset = 0;
|
|
24
|
+
while (offset < before.size) {
|
|
25
|
+
const count = readSync(fd, bytes, 0, Math.min(bytes.length, before.size - offset), offset);
|
|
26
|
+
if (!count)
|
|
27
|
+
return null;
|
|
28
|
+
hash.update(bytes.subarray(0, count));
|
|
29
|
+
offset += count;
|
|
30
|
+
}
|
|
31
|
+
if (version(fd) !== initial || realpathSync(filename) !== canonical)
|
|
32
|
+
return null;
|
|
33
|
+
return { version: initial, sha256: hash.digest("hex") };
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
closeSync(fd);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Take immediately after the screenshot writer completes, before asynchronous
|
|
40
|
+
* HTML serialization. A later writer cannot silently substitute its PNG. */
|
|
41
|
+
export function captureThumbnailFingerprint(pngPath) {
|
|
42
|
+
try {
|
|
43
|
+
return fingerprint(pngPath, MAX_PNG);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function workspaceFor(filename) {
|
|
50
|
+
for (let current = path.dirname(filename); path.dirname(current) !== current; current = path.dirname(current)) {
|
|
51
|
+
const artifacts = path.dirname(current);
|
|
52
|
+
const state = path.dirname(artifacts);
|
|
53
|
+
if (path.basename(artifacts) === "artifacts" && path.basename(state) === ".harnery")
|
|
54
|
+
return { root: path.dirname(state), workspace: current };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function inside(root, filename) {
|
|
59
|
+
const relative = path.relative(root, filename);
|
|
60
|
+
return (relative !== "" &&
|
|
61
|
+
relative !== ".." &&
|
|
62
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
63
|
+
!path.isAbsolute(relative));
|
|
64
|
+
}
|
|
65
|
+
/** Associate only the full-document, standalone HTML and exact PNG produced by
|
|
66
|
+
* this capture operation. No basename inference and no reads of source pages.
|
|
67
|
+
* A linked asset, selector fragment, changed output, oversize file or output
|
|
68
|
+
* outside a managed artifact workspace safely skips registration.
|
|
69
|
+
* The hidden sidecar binds canonical repository paths, checked file versions
|
|
70
|
+
* and SHA256 receipts. Web readers also require an empty local asset graph;
|
|
71
|
+
* manual registrations bind their own complete dependency graph instead. */
|
|
72
|
+
export function registerCaptureThumbnail(input) {
|
|
73
|
+
if (!input.fullDocument || input.stylesheetsLinked !== 0 || input.resourcesLinked !== 0)
|
|
74
|
+
return null;
|
|
75
|
+
try {
|
|
76
|
+
const htmlPath = realpathSync(input.htmlPath);
|
|
77
|
+
const pngPath = realpathSync(input.pngPath);
|
|
78
|
+
if (!/\.html?$/i.test(htmlPath) || !/\.png$/i.test(pngPath))
|
|
79
|
+
return null;
|
|
80
|
+
const owner = workspaceFor(htmlPath);
|
|
81
|
+
if (!owner || !inside(owner.workspace, pngPath))
|
|
82
|
+
return null;
|
|
83
|
+
const source = fingerprint(htmlPath, MAX_HTML);
|
|
84
|
+
const preview = fingerprint(pngPath, MAX_PNG);
|
|
85
|
+
if (!source ||
|
|
86
|
+
!preview ||
|
|
87
|
+
source.sha256 !== createHash("sha256").update(input.html).digest("hex") ||
|
|
88
|
+
preview.version !== input.preview.version ||
|
|
89
|
+
preview.sha256 !== input.preview.sha256)
|
|
90
|
+
return null;
|
|
91
|
+
const sourceRelative = path.relative(owner.root, htmlPath).split(path.sep).join("/");
|
|
92
|
+
const previewRelative = path.relative(owner.root, pngPath).split(path.sep).join("/");
|
|
93
|
+
const association = {
|
|
94
|
+
schema_version: 2,
|
|
95
|
+
producer: "browser-standalone-capture",
|
|
96
|
+
source: { path: sourceRelative, ...source },
|
|
97
|
+
preview: { path: previewRelative, ...preview },
|
|
98
|
+
};
|
|
99
|
+
const name = `.thumbnail-preview-${createHash("sha256").update(sourceRelative).digest("hex").slice(0, 24)}.json`;
|
|
100
|
+
const directoryFd = openSync(owner.workspace, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
101
|
+
try {
|
|
102
|
+
let anchor;
|
|
103
|
+
for (const prefix of ["/proc/self/fd", "/dev/fd"]) {
|
|
104
|
+
const candidate = `${prefix}/${directoryFd}`;
|
|
105
|
+
try {
|
|
106
|
+
if (realpathSync(candidate) === owner.workspace) {
|
|
107
|
+
anchor = candidate;
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
/* unsupported descriptor namespace */
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (!anchor)
|
|
116
|
+
return null;
|
|
117
|
+
const target = path.join(anchor, name);
|
|
118
|
+
const temporary = `${target}.${randomUUID()}.tmp`;
|
|
119
|
+
const fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
|
|
120
|
+
try {
|
|
121
|
+
const bytes = Buffer.from(`${JSON.stringify(association)}\n`);
|
|
122
|
+
if (bytes.length > 16 * 1024)
|
|
123
|
+
return null;
|
|
124
|
+
let offset = 0;
|
|
125
|
+
while (offset < bytes.length)
|
|
126
|
+
offset += writeSync(fd, bytes, offset, bytes.length - offset);
|
|
127
|
+
if (realpathSync(anchor) !== owner.workspace ||
|
|
128
|
+
realpathSync(owner.workspace) !== owner.workspace)
|
|
129
|
+
return null;
|
|
130
|
+
// Stat again after the write, without reading another potentially large
|
|
131
|
+
// image. ctime detects same-size edits with a restored mtime.
|
|
132
|
+
const matches = (filename, expected) => {
|
|
133
|
+
const s = statSync(filename);
|
|
134
|
+
return `${s.dev}:${s.ino}:${s.size}:${s.mtimeMs}:${s.ctimeMs}` === expected;
|
|
135
|
+
};
|
|
136
|
+
if (!matches(htmlPath, source.version) || !matches(pngPath, preview.version))
|
|
137
|
+
return null;
|
|
138
|
+
renameSync(temporary, target);
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
closeSync(fd);
|
|
142
|
+
try {
|
|
143
|
+
unlinkSync(temporary);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
/* renamed */
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
closeSync(directoryFd);
|
|
152
|
+
}
|
|
153
|
+
return path.relative(owner.root, path.join(owner.workspace, name)).split(path.sep).join("/");
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|