harnery 0.36.0 → 0.38.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/commander.d.ts +9 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commander.js +10 -1
- package/dist/commands/admission.d.ts +21 -0
- package/dist/commands/admission.d.ts.map +1 -0
- package/dist/commands/admission.js +565 -0
- package/dist/commands/agents.d.ts +7 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +148 -47
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +73 -2
- package/dist/commands/browse-ai.d.ts +2 -2
- package/dist/commands/browse-ai.d.ts.map +1 -1
- package/dist/commands/browse-ai.js +6 -4
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +360 -21
- package/dist/commands/diagnostics.d.ts +4 -0
- package/dist/commands/diagnostics.d.ts.map +1 -0
- package/dist/commands/diagnostics.js +229 -0
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/files.d.ts +12 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -1
- package/dist/commands/governor.d.ts.map +1 -1
- package/dist/commands/governor.js +4 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +71 -2
- package/dist/commands/qa-record.d.ts +144 -0
- package/dist/commands/qa-record.d.ts.map +1 -0
- package/dist/commands/qa-record.js +0 -0
- package/dist/commands/qa-run.d.ts +8 -0
- package/dist/commands/qa-run.d.ts.map +1 -0
- package/dist/commands/qa-run.js +396 -0
- package/dist/commands/qa-status.d.ts +71 -0
- package/dist/commands/qa-status.d.ts.map +1 -0
- package/dist/commands/qa-status.js +490 -0
- package/dist/commands/qa-verify.d.ts +40 -0
- package/dist/commands/qa-verify.d.ts.map +1 -0
- package/dist/commands/qa-verify.js +180 -0
- package/dist/commands/resources.d.ts +4 -0
- package/dist/commands/resources.d.ts.map +1 -0
- package/dist/commands/resources.js +29 -0
- package/dist/commands/review-pack.d.ts +4 -0
- package/dist/commands/review-pack.d.ts.map +1 -0
- package/dist/commands/review-pack.js +1001 -0
- package/dist/commands/supervisor.d.ts +4 -0
- package/dist/commands/supervisor.d.ts.map +1 -0
- package/dist/commands/supervisor.js +101 -0
- package/dist/commands/web.d.ts.map +1 -1
- package/dist/commands/web.js +17 -0
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +4 -0
- package/dist/core/agents/cli.js +7 -3
- package/dist/core/agents/health.d.ts +17 -0
- package/dist/core/agents/health.d.ts.map +1 -0
- package/dist/core/agents/health.js +109 -0
- package/dist/core/agents/mailbox.d.ts +92 -0
- package/dist/core/agents/mailbox.d.ts.map +1 -0
- package/dist/core/agents/mailbox.js +325 -0
- package/dist/core/agents/qa-signal.d.ts +111 -0
- package/dist/core/agents/qa-signal.d.ts.map +1 -0
- package/dist/core/agents/qa-signal.js +231 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts +44 -0
- package/dist/core/agents/reconcile-coordination-v3.d.ts.map +1 -0
- package/dist/core/agents/reconcile-coordination-v3.js +58 -0
- package/dist/core/agents/render/prompt-context.d.ts +5 -0
- package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
- package/dist/core/agents/render/prompt-context.js +34 -13
- package/dist/core/agents/render/session-context.d.ts +8 -0
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +34 -3
- package/dist/core/agents/rules/commit-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/commit-conflict.js +50 -1
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +6 -10
- package/dist/core/agents/session-finalizer-v3.js +4 -1
- package/dist/core/agents/session-name-display.d.ts +20 -5
- package/dist/core/agents/session-name-display.d.ts.map +1 -1
- package/dist/core/agents/session-name-display.js +67 -7
- package/dist/core/agents/state/heartbeat-reader.d.ts +7 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.d.ts +11 -0
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +17 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +1 -0
- package/dist/core/agents/state/live-coordination-writer.js +5 -0
- package/dist/core/artifacts/constants.d.ts +1 -1
- package/dist/core/artifacts/constants.js +1 -1
- package/dist/core/artifacts/index.d.ts +82 -6
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +410 -15
- package/dist/core/config.d.ts +20 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +59 -0
- package/dist/core/diagnostics/advice.d.ts +9 -0
- package/dist/core/diagnostics/advice.d.ts.map +1 -0
- package/dist/core/diagnostics/advice.js +112 -0
- package/dist/core/diagnostics/bundle.d.ts +51 -0
- package/dist/core/diagnostics/bundle.d.ts.map +1 -0
- package/dist/core/diagnostics/bundle.js +968 -0
- package/dist/core/diagnostics/comparison.d.ts +5 -0
- package/dist/core/diagnostics/comparison.d.ts.map +1 -0
- package/dist/core/diagnostics/comparison.js +444 -0
- package/dist/core/diagnostics/contract.d.ts +245 -0
- package/dist/core/diagnostics/contract.d.ts.map +1 -0
- package/dist/core/diagnostics/contract.js +18 -0
- package/dist/core/diagnostics/identity.d.ts +3 -0
- package/dist/core/diagnostics/identity.d.ts.map +1 -0
- package/dist/core/diagnostics/identity.js +10 -0
- package/dist/core/diagnostics/index.d.ts +8 -0
- package/dist/core/diagnostics/index.d.ts.map +1 -0
- package/dist/core/diagnostics/index.js +7 -0
- package/dist/core/diagnostics/replay.d.ts +8 -0
- package/dist/core/diagnostics/replay.d.ts.map +1 -0
- package/dist/core/diagnostics/replay.js +213 -0
- package/dist/core/diagnostics/sanitize.d.ts +9 -0
- package/dist/core/diagnostics/sanitize.d.ts.map +1 -0
- package/dist/core/diagnostics/sanitize.js +84 -0
- package/dist/core/events/legacy-storage/compression.d.ts +13 -0
- package/dist/core/events/legacy-storage/compression.d.ts.map +1 -0
- package/dist/core/events/legacy-storage/compression.js +107 -0
- package/dist/core/events/legacy-storage/index.d.ts +1 -0
- package/dist/core/events/legacy-storage/index.d.ts.map +1 -1
- package/dist/core/events/legacy-storage/index.js +1 -0
- package/dist/core/events/v3/archive-retention.d.ts +33 -0
- package/dist/core/events/v3/archive-retention.d.ts.map +1 -0
- package/dist/core/events/v3/archive-retention.js +195 -0
- package/dist/core/events/v3/bootstrap.d.ts.map +1 -1
- package/dist/core/events/v3/bootstrap.js +10 -0
- package/dist/core/events/v3/capabilities.d.ts +1 -1
- package/dist/core/events/v3/capabilities.d.ts.map +1 -1
- package/dist/core/events/v3/capabilities.js +1 -0
- package/dist/core/events/v3/coordination-view.d.ts +3 -0
- package/dist/core/events/v3/coordination-view.d.ts.map +1 -1
- package/dist/core/events/v3/coordination-view.js +69 -8
- package/dist/core/events/v3/index.d.ts +1 -0
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -0
- package/dist/core/events/v3/live-routing.d.ts.map +1 -1
- package/dist/core/events/v3/live-routing.js +1 -0
- package/dist/core/events/v3/producers/hook-base.d.ts +1 -1
- package/dist/core/events/v3/producers/hook-base.d.ts.map +1 -1
- package/dist/core/events/v3/producers/hook-base.js +13 -2
- package/dist/core/events/v3/producers/intake.d.ts.map +1 -1
- package/dist/core/events/v3/producers/intake.js +10 -2
- package/dist/core/events/v3/producers/recorder.d.ts +39 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +350 -17
- package/dist/core/hooks/adapter/output.d.ts +8 -0
- package/dist/core/hooks/adapter/output.d.ts.map +1 -1
- package/dist/core/hooks/adapter/output.js +11 -1
- package/dist/core/hooks/cli.js +303 -219
- package/dist/core/hooks/health.d.ts +59 -0
- package/dist/core/hooks/health.d.ts.map +1 -0
- package/dist/core/hooks/health.js +84 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +10 -3
- package/dist/core/hooks/session-name-presence.d.ts.map +1 -1
- package/dist/core/hooks/session-name-presence.js +4 -1
- package/dist/core/qa-artifacts.d.ts +20 -0
- package/dist/core/qa-artifacts.d.ts.map +1 -0
- package/dist/core/qa-artifacts.js +110 -0
- package/dist/core/resources/contract.d.ts +96 -0
- package/dist/core/resources/contract.d.ts.map +1 -0
- package/dist/core/resources/contract.js +2 -0
- package/dist/core/resources/index.d.ts +6 -0
- package/dist/core/resources/index.d.ts.map +1 -0
- package/dist/core/resources/index.js +5 -0
- package/dist/core/resources/sampler.d.ts +34 -0
- package/dist/core/resources/sampler.d.ts.map +1 -0
- package/dist/core/resources/sampler.js +458 -0
- package/dist/core/resources/service-status.d.ts +3 -0
- package/dist/core/resources/service-status.d.ts.map +1 -0
- package/dist/core/resources/service-status.js +48 -0
- package/dist/core/resources/service.d.ts +26 -0
- package/dist/core/resources/service.d.ts.map +1 -0
- package/dist/core/resources/service.js +292 -0
- package/dist/core/resources/storage.d.ts +13 -0
- package/dist/core/resources/storage.d.ts.map +1 -0
- package/dist/core/resources/storage.js +30 -0
- package/dist/core/storage/atomic-json.d.ts +3 -0
- package/dist/core/storage/atomic-json.d.ts.map +1 -0
- package/dist/core/storage/atomic-json.js +15 -0
- package/dist/core/storage/builtins.d.ts.map +1 -1
- package/dist/core/storage/builtins.js +45 -6
- package/dist/core/storage/logger.d.ts +2 -0
- package/dist/core/storage/logger.d.ts.map +1 -1
- package/dist/core/storage/logger.js +2 -0
- package/dist/core/storage/query.d.ts +14 -0
- package/dist/core/storage/query.d.ts.map +1 -1
- package/dist/core/storage/query.js +51 -0
- package/dist/core/supervisor/activity.d.ts +4 -0
- package/dist/core/supervisor/activity.d.ts.map +1 -0
- package/dist/core/supervisor/activity.js +141 -0
- package/dist/core/supervisor/contract.d.ts +268 -0
- package/dist/core/supervisor/contract.d.ts.map +1 -0
- package/dist/core/supervisor/contract.js +35 -0
- package/dist/core/supervisor/explanations.d.ts +4 -0
- package/dist/core/supervisor/explanations.d.ts.map +1 -0
- package/dist/core/supervisor/explanations.js +71 -0
- package/dist/core/supervisor/findings.d.ts +20 -0
- package/dist/core/supervisor/findings.d.ts.map +1 -0
- package/dist/core/supervisor/findings.js +399 -0
- package/dist/core/supervisor/history.d.ts +9 -0
- package/dist/core/supervisor/history.d.ts.map +1 -0
- package/dist/core/supervisor/history.js +46 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts +14 -0
- package/dist/core/supervisor/hook-health-alerts.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-alerts.js +70 -0
- package/dist/core/supervisor/hook-health-storage.d.ts +5 -0
- package/dist/core/supervisor/hook-health-storage.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health-storage.js +24 -0
- package/dist/core/supervisor/hook-health.d.ts +72 -0
- package/dist/core/supervisor/hook-health.d.ts.map +1 -0
- package/dist/core/supervisor/hook-health.js +203 -0
- package/dist/core/supervisor/hooks.d.ts +10 -0
- package/dist/core/supervisor/hooks.d.ts.map +1 -0
- package/dist/core/supervisor/hooks.js +28 -0
- package/dist/core/supervisor/index.d.ts +15 -0
- package/dist/core/supervisor/index.d.ts.map +1 -0
- package/dist/core/supervisor/index.js +14 -0
- package/dist/core/supervisor/log-feed.d.ts +9 -0
- package/dist/core/supervisor/log-feed.d.ts.map +1 -0
- package/dist/core/supervisor/log-feed.js +103 -0
- package/dist/core/supervisor/service.d.ts +29 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +450 -0
- package/dist/core/supervisor/services.d.ts +11 -0
- package/dist/core/supervisor/services.d.ts.map +1 -0
- package/dist/core/supervisor/services.js +155 -0
- package/dist/core/supervisor/status.d.ts +3 -0
- package/dist/core/supervisor/status.d.ts.map +1 -0
- package/dist/core/supervisor/status.js +48 -0
- package/dist/core/supervisor/storage.d.ts +30 -0
- package/dist/core/supervisor/storage.d.ts.map +1 -0
- package/dist/core/supervisor/storage.js +73 -0
- package/dist/core/supervisor/timeline.d.ts +3 -0
- package/dist/core/supervisor/timeline.d.ts.map +1 -0
- package/dist/core/supervisor/timeline.js +87 -0
- package/dist/core/workflow/admission.d.ts +23 -0
- package/dist/core/workflow/admission.d.ts.map +1 -0
- package/dist/core/workflow/admission.js +136 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +77 -2
- package/dist/core/workflow/index.d.ts +3 -2
- package/dist/core/workflow/index.d.ts.map +1 -1
- package/dist/core/workflow/index.js +2 -1
- package/dist/core/workflow/proof.d.ts +2 -1
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +39 -1
- package/dist/core/workflow/run-state.d.ts +3 -2
- package/dist/core/workflow/run-state.d.ts.map +1 -1
- package/dist/core/workflow/run-state.js +8 -1
- package/dist/core/workflow/types.d.ts +33 -1
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/types.js +2 -1
- package/dist/lib/admission.d.ts +71 -0
- package/dist/lib/admission.d.ts.map +1 -0
- package/dist/lib/admission.js +264 -0
- package/dist/lib/agent-browser/client.d.ts +1 -1
- package/dist/lib/agent-browser/client.d.ts.map +1 -1
- package/dist/lib/agent-browser/client.js +1 -5
- package/dist/lib/browser/capture-fidelity.d.ts +39 -0
- package/dist/lib/browser/capture-fidelity.d.ts.map +1 -0
- package/dist/lib/browser/capture-fidelity.js +84 -0
- package/dist/lib/browser/client.d.ts +46 -1
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +168 -9
- package/dist/lib/browser/critique.d.ts +38 -1
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +34 -6
- package/dist/lib/browser/index.d.ts +5 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +4 -0
- package/dist/lib/browser/page-review-judge.d.ts +64 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -0
- package/dist/lib/browser/page-review-judge.js +270 -0
- package/dist/lib/browser/page-review-pack.d.ts +613 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -0
- package/dist/lib/browser/page-review-pack.js +1751 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +377 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -0
- package/dist/lib/browser/qa-run-contracts.js +361 -0
- package/dist/lib/browser/qa-run.d.ts +141 -0
- package/dist/lib/browser/qa-run.d.ts.map +1 -0
- package/dist/lib/browser/qa-run.js +1283 -0
- package/dist/lib/browser/request-diagnostics.d.ts +13 -0
- package/dist/lib/browser/request-diagnostics.d.ts.map +1 -0
- package/dist/lib/browser/request-diagnostics.js +18 -0
- package/dist/lib/browser/tiling.d.ts +19 -0
- package/dist/lib/browser/tiling.d.ts.map +1 -1
- package/dist/lib/browser/tiling.js +28 -0
- package/dist/lib/cookies/client.d.ts +9 -0
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +197 -44
- package/dist/lib/cookies/extra.d.ts +18 -0
- package/dist/lib/cookies/extra.d.ts.map +1 -0
- package/dist/lib/cookies/extra.js +14 -0
- package/dist/lib/cookies/index.d.ts +2 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +2 -1
- package/dist/lib/coord-root-id.d.ts +9 -0
- package/dist/lib/coord-root-id.d.ts.map +1 -0
- package/dist/lib/coord-root-id.js +17 -0
- package/dist/lib/durable-job.d.ts +124 -0
- package/dist/lib/durable-job.d.ts.map +1 -0
- package/dist/lib/durable-job.js +296 -0
- package/dist/lib/http/client.d.ts +7 -1
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +2 -0
- package/dist/lib/instructions/apply.d.ts +3 -1
- package/dist/lib/instructions/apply.d.ts.map +1 -1
- package/dist/lib/instructions/apply.js +74 -3
- package/dist/lib/instructions/templates.d.ts.map +1 -1
- package/dist/lib/instructions/templates.js +11 -2
- package/package.json +19 -2
- package/src/commander.ts +75 -1
- package/src/commands/admission.ts +699 -0
- package/src/commands/agents.ts +189 -50
- package/src/commands/artifacts.ts +132 -18
- package/src/commands/browse-ai.ts +10 -5
- package/src/commands/browse.ts +496 -20
- package/src/commands/diagnostics.ts +271 -0
- package/src/commands/fetch.ts +1 -0
- package/src/commands/files.ts +102 -2
- package/src/commands/governor.ts +8 -0
- package/src/commands/ledger-v3.ts +82 -0
- package/src/commands/qa-record.ts +682 -0
- package/src/commands/qa-run.ts +503 -0
- package/src/commands/qa-status.ts +608 -0
- package/src/commands/qa-verify.ts +238 -0
- package/src/commands/resources.ts +38 -0
- package/src/commands/review-pack.ts +1281 -0
- package/src/commands/supervisor.ts +141 -0
- package/src/commands/web.ts +19 -0
- package/src/commands/workflow.ts +8 -0
- package/src/core/agents/cli.ts +7 -3
- package/src/core/agents/health.ts +142 -0
- package/src/core/agents/mailbox.ts +373 -0
- package/src/core/agents/qa-signal.ts +261 -0
- package/src/core/agents/reconcile-coordination-v3.ts +77 -0
- package/src/core/agents/render/prompt-context.ts +42 -12
- package/src/core/agents/render/session-context.ts +37 -3
- package/src/core/agents/rules/commit-conflict.ts +58 -1
- package/src/core/agents/rules/stop-hook.ts +6 -12
- package/src/core/agents/session-finalizer-v3.ts +4 -1
- package/src/core/agents/session-name-display.ts +78 -7
- package/src/core/agents/state/heartbeat-reader.ts +7 -0
- package/src/core/agents/state/heartbeat-writer.ts +23 -0
- package/src/core/agents/state/live-coordination-view.ts +1 -0
- package/src/core/agents/state/live-coordination-writer.ts +5 -0
- package/src/core/artifacts/constants.ts +1 -1
- package/src/core/artifacts/index.ts +565 -30
- package/src/core/config.ts +134 -2
- package/src/core/diagnostics/advice.ts +147 -0
- package/src/core/diagnostics/bundle.ts +1250 -0
- package/src/core/diagnostics/comparison.ts +565 -0
- package/src/core/diagnostics/contract.ts +311 -0
- package/src/core/diagnostics/identity.ts +11 -0
- package/src/core/diagnostics/index.ts +7 -0
- package/src/core/diagnostics/replay.ts +314 -0
- package/src/core/diagnostics/sanitize.ts +97 -0
- package/src/core/events/legacy-storage/compression.ts +130 -0
- package/src/core/events/legacy-storage/index.ts +1 -0
- package/src/core/events/v3/archive-retention.ts +251 -0
- package/src/core/events/v3/bootstrap.ts +10 -0
- package/src/core/events/v3/capabilities.ts +2 -0
- package/src/core/events/v3/coordination-view.ts +100 -11
- package/src/core/events/v3/index.ts +9 -0
- package/src/core/events/v3/live-routing.ts +1 -0
- package/src/core/events/v3/producers/hook-base.ts +14 -2
- package/src/core/events/v3/producers/intake.ts +10 -2
- package/src/core/events/v3/producers/recorder.ts +420 -17
- package/src/core/hooks/adapter/output.ts +12 -1
- package/src/core/hooks/cli.ts +332 -247
- package/src/core/hooks/health.ts +146 -0
- package/src/core/hooks/resolve/transcript.ts +10 -3
- package/src/core/hooks/session-name-presence.ts +6 -1
- package/src/core/qa-artifacts.ts +126 -0
- package/src/core/resources/contract.ts +105 -0
- package/src/core/resources/index.ts +5 -0
- package/src/core/resources/sampler.ts +590 -0
- package/src/core/resources/service-status.ts +62 -0
- package/src/core/resources/service.ts +337 -0
- package/src/core/resources/storage.ts +44 -0
- package/src/core/storage/atomic-json.ts +15 -0
- package/src/core/storage/builtins.ts +62 -6
- package/src/core/storage/logger.ts +2 -0
- package/src/core/storage/query.ts +58 -0
- package/src/core/supervisor/activity.ts +193 -0
- package/src/core/supervisor/contract.ts +312 -0
- package/src/core/supervisor/explanations.ts +104 -0
- package/src/core/supervisor/findings.ts +788 -0
- package/src/core/supervisor/history.ts +61 -0
- package/src/core/supervisor/hook-health-alerts.ts +108 -0
- package/src/core/supervisor/hook-health-storage.ts +25 -0
- package/src/core/supervisor/hook-health.ts +301 -0
- package/src/core/supervisor/hooks.ts +35 -0
- package/src/core/supervisor/index.ts +14 -0
- package/src/core/supervisor/log-feed.ts +131 -0
- package/src/core/supervisor/service.ts +511 -0
- package/src/core/supervisor/services.ts +203 -0
- package/src/core/supervisor/status.ts +59 -0
- package/src/core/supervisor/storage.ts +126 -0
- package/src/core/supervisor/timeline.ts +133 -0
- package/src/core/workflow/admission.ts +215 -0
- package/src/core/workflow/engine.ts +93 -1
- package/src/core/workflow/index.ts +10 -0
- package/src/core/workflow/proof.ts +54 -1
- package/src/core/workflow/run-state.ts +13 -1
- package/src/core/workflow/types.ts +39 -1
- package/src/lib/admission.ts +347 -0
- package/src/lib/agent-browser/client.ts +2 -10
- package/src/lib/browser/capture-fidelity.ts +98 -0
- package/src/lib/browser/client.ts +212 -10
- package/src/lib/browser/critique.ts +62 -7
- package/src/lib/browser/index.ts +65 -0
- package/src/lib/browser/page-review-judge.ts +360 -0
- package/src/lib/browser/page-review-pack.ts +2384 -0
- package/src/lib/browser/qa-run-contracts.ts +743 -0
- package/src/lib/browser/qa-run.ts +1462 -0
- package/src/lib/browser/request-diagnostics.ts +27 -0
- package/src/lib/browser/tiling.ts +32 -0
- package/src/lib/cookies/client.ts +228 -42
- package/src/lib/cookies/extra.ts +28 -0
- package/src/lib/cookies/index.ts +2 -0
- package/src/lib/coord-root-id.ts +23 -0
- package/src/lib/durable-job.ts +407 -0
- package/src/lib/http/client.ts +13 -1
- package/src/lib/instructions/apply.ts +81 -2
- package/src/lib/instructions/templates.ts +11 -2
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// Machine-wide admission control for heavy jobs (browser QA matrices,
|
|
2
|
+
// production builds): a crash-safe FIFO slot queue with no daemon and no
|
|
3
|
+
// distributed-lock pretensions. State is plain files on one machine.
|
|
4
|
+
//
|
|
5
|
+
// Layout, per resource under a caller-supplied base directory:
|
|
6
|
+
//
|
|
7
|
+
// <dir>/<resource>/tickets/<epoch_ms-13pad>-<pid>-<rand>.json waiters
|
|
8
|
+
// <dir>/<resource>/held/<same filename>.json holders
|
|
9
|
+
//
|
|
10
|
+
// Lexical filename order is enqueue order in BOTH directories. Admission is
|
|
11
|
+
// an atomic rename tickets/ → held/. A waiter may rename only when the live
|
|
12
|
+
// held count is under capacity AND its ticket is among the first
|
|
13
|
+
// (capacity − heldCount) live tickets. After renaming it re-lists held/: if
|
|
14
|
+
// live holders exceed capacity and its file is not among the capacity
|
|
15
|
+
// lexically-oldest, it renames itself back and rejoins the queue — the
|
|
16
|
+
// transient over-admission race self-corrects. Any participant prunes entries
|
|
17
|
+
// whose PID is dead or whose age exceeds the TTL. Release unlinks the held
|
|
18
|
+
// file; process death leaves a dead-PID file for the next participant to
|
|
19
|
+
// prune.
|
|
20
|
+
//
|
|
21
|
+
// Toolkit tier: this module must not import src/core (layering check).
|
|
22
|
+
|
|
23
|
+
import { randomUUID } from "node:crypto";
|
|
24
|
+
import {
|
|
25
|
+
mkdirSync,
|
|
26
|
+
readdirSync,
|
|
27
|
+
readFileSync,
|
|
28
|
+
renameSync,
|
|
29
|
+
unlinkSync,
|
|
30
|
+
writeFileSync,
|
|
31
|
+
} from "node:fs";
|
|
32
|
+
import { tmpdir } from "node:os";
|
|
33
|
+
import { join } from "node:path";
|
|
34
|
+
import { coordEnv } from "./env.ts";
|
|
35
|
+
|
|
36
|
+
export interface AdmissionConfig {
|
|
37
|
+
/** Base directory holding one subdirectory per resource. */
|
|
38
|
+
dir: string;
|
|
39
|
+
/** Resource name, e.g. "browser-qa" or "build". */
|
|
40
|
+
resource: string;
|
|
41
|
+
/** Maximum concurrent holders. */
|
|
42
|
+
capacity: number;
|
|
43
|
+
/** Poll interval while waiting (default 500ms). */
|
|
44
|
+
pollMs?: number;
|
|
45
|
+
/** Entries older than this are pruned regardless of PID (default 6h). */
|
|
46
|
+
ttlMs?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface AdmissionEntry {
|
|
50
|
+
pid: number;
|
|
51
|
+
label: string;
|
|
52
|
+
created_at: string;
|
|
53
|
+
/** Set on holders once admitted. */
|
|
54
|
+
acquired_at?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AdmissionWaitInfo {
|
|
58
|
+
/** 1-based position in the ticket queue. */
|
|
59
|
+
position: number;
|
|
60
|
+
holders: AdmissionEntry[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AdmissionAcquireOptions {
|
|
64
|
+
/** Human-readable holder description shown in status listings. */
|
|
65
|
+
label: string;
|
|
66
|
+
/** Maximum wait before AdmissionTimeoutError (default 20 minutes). */
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
/** Progress callback, invoked at most once per poll while waiting. */
|
|
69
|
+
onWait?: (info: AdmissionWaitInfo) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AdmissionHandle {
|
|
73
|
+
/** The held entry's filename (diagnostics). */
|
|
74
|
+
entry: string;
|
|
75
|
+
/** Milliseconds spent waiting in the queue. */
|
|
76
|
+
waitedMs: number;
|
|
77
|
+
/** Give the slot back. Safe to call more than once. */
|
|
78
|
+
release: () => void;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class AdmissionTimeoutError extends Error {
|
|
82
|
+
readonly holders: AdmissionEntry[];
|
|
83
|
+
constructor(resource: string, timeoutMs: number, holders: AdmissionEntry[]) {
|
|
84
|
+
const labels = holders.map((holder) => `${holder.label} (pid ${holder.pid})`).join(", ");
|
|
85
|
+
const seconds =
|
|
86
|
+
timeoutMs < 10_000 ? (timeoutMs / 1000).toFixed(1) : String(Math.round(timeoutMs / 1000));
|
|
87
|
+
super(
|
|
88
|
+
`no ${resource} slot became free within ${seconds}s; ` +
|
|
89
|
+
`current holder(s): ${labels || "none (queue contention)"}`,
|
|
90
|
+
);
|
|
91
|
+
this.name = "AdmissionTimeoutError";
|
|
92
|
+
this.holders = holders;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const DEFAULT_POLL_MS = 500;
|
|
97
|
+
const DEFAULT_TTL_MS = 6 * 60 * 60 * 1000;
|
|
98
|
+
const DEFAULT_TIMEOUT_MS = 20 * 60 * 1000;
|
|
99
|
+
/** An unparseable entry younger than this is left alone (it may be mid-write
|
|
100
|
+
* by a non-atomic writer); older, it is torn garbage and gets pruned. */
|
|
101
|
+
const UNPARSEABLE_GRACE_MS = 60 * 1000;
|
|
102
|
+
|
|
103
|
+
/** Is a PID alive on this machine? EPERM counts as alive; only ESRCH counts
|
|
104
|
+
* as dead (fail toward respecting holders rather than stealing slots). */
|
|
105
|
+
export function pidAlive(pid: number): boolean {
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, 0);
|
|
108
|
+
return true;
|
|
109
|
+
} catch (err: unknown) {
|
|
110
|
+
// EPERM = exists but not ours = alive. ESRCH (and anything else) = treat
|
|
111
|
+
// as dead only on ESRCH; unknown errors count as alive (fail toward
|
|
112
|
+
// respecting holders rather than stealing their slot).
|
|
113
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
114
|
+
if (code === "ESRCH") return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function ticketsDir(config: AdmissionConfig): string {
|
|
120
|
+
return join(config.dir, config.resource, "tickets");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function heldDir(config: AdmissionConfig): string {
|
|
124
|
+
return join(config.dir, config.resource, "held");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ensureDirs(config: AdmissionConfig): void {
|
|
128
|
+
mkdirSync(ticketsDir(config), { recursive: true });
|
|
129
|
+
mkdirSync(heldDir(config), { recursive: true });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function writeEntry(dir: string, name: string, entry: AdmissionEntry): void {
|
|
133
|
+
const tmp = join(dir, `.${name}.${randomUUID().slice(0, 8)}.tmp`);
|
|
134
|
+
writeFileSync(tmp, `${JSON.stringify(entry, null, 2)}\n`);
|
|
135
|
+
renameSync(tmp, join(dir, name));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface LiveEntry {
|
|
139
|
+
name: string;
|
|
140
|
+
entry: AdmissionEntry;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** List live entries in lexical (= enqueue) order, pruning dead-PID, expired,
|
|
144
|
+
* and torn files as a side effect. Every participant prunes, so a crashed
|
|
145
|
+
* holder's slot frees as soon as anyone else looks. */
|
|
146
|
+
function listLive(dir: string, ttlMs: number): LiveEntry[] {
|
|
147
|
+
let names: string[];
|
|
148
|
+
try {
|
|
149
|
+
names = readdirSync(dir)
|
|
150
|
+
.filter((name) => name.endsWith(".json"))
|
|
151
|
+
.sort();
|
|
152
|
+
} catch {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const live: LiveEntry[] = [];
|
|
157
|
+
for (const name of names) {
|
|
158
|
+
const path = join(dir, name);
|
|
159
|
+
let entry: AdmissionEntry | undefined;
|
|
160
|
+
try {
|
|
161
|
+
const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
162
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
163
|
+
const record = parsed as Record<string, unknown>;
|
|
164
|
+
if (typeof record.pid === "number" && typeof record.created_at === "string") {
|
|
165
|
+
entry = {
|
|
166
|
+
pid: record.pid,
|
|
167
|
+
label: typeof record.label === "string" ? record.label : "",
|
|
168
|
+
created_at: record.created_at,
|
|
169
|
+
...(typeof record.acquired_at === "string" ? { acquired_at: record.acquired_at } : {}),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} catch {
|
|
174
|
+
// fall through: unparseable
|
|
175
|
+
}
|
|
176
|
+
if (!entry) {
|
|
177
|
+
// Torn or foreign file: prune once it is clearly not mid-write. The
|
|
178
|
+
// enqueue timestamp is the filename prefix, so age is known without
|
|
179
|
+
// content.
|
|
180
|
+
const stamp = Number.parseInt(name.slice(0, 13), 10);
|
|
181
|
+
if (!Number.isFinite(stamp) || now - stamp > UNPARSEABLE_GRACE_MS) {
|
|
182
|
+
try {
|
|
183
|
+
unlinkSync(path);
|
|
184
|
+
} catch {
|
|
185
|
+
// a peer pruned it first
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const age = now - Date.parse(entry.created_at);
|
|
191
|
+
if (!pidAlive(entry.pid) || Number.isNaN(age) || age > ttlMs) {
|
|
192
|
+
try {
|
|
193
|
+
unlinkSync(path);
|
|
194
|
+
} catch {
|
|
195
|
+
// a peer pruned it first
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
live.push({ name, entry });
|
|
200
|
+
}
|
|
201
|
+
return live;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function sleep(ms: number): Promise<void> {
|
|
205
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Join the FIFO queue for one resource and resolve once a slot is held.
|
|
210
|
+
* Throws AdmissionTimeoutError (with the holders snapshot) when no slot
|
|
211
|
+
* frees within the timeout — the caller decides whether that is fatal.
|
|
212
|
+
*/
|
|
213
|
+
export async function acquireAdmission(
|
|
214
|
+
config: AdmissionConfig,
|
|
215
|
+
options: AdmissionAcquireOptions,
|
|
216
|
+
): Promise<AdmissionHandle> {
|
|
217
|
+
if (!Number.isInteger(config.capacity) || config.capacity < 1) {
|
|
218
|
+
throw new Error(`admission capacity must be a positive integer, got ${config.capacity}`);
|
|
219
|
+
}
|
|
220
|
+
ensureDirs(config);
|
|
221
|
+
const pollMs = config.pollMs ?? DEFAULT_POLL_MS;
|
|
222
|
+
const ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
|
|
223
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
224
|
+
const tickets = ticketsDir(config);
|
|
225
|
+
const held = heldDir(config);
|
|
226
|
+
|
|
227
|
+
const myEntry: AdmissionEntry = {
|
|
228
|
+
pid: process.pid,
|
|
229
|
+
label: options.label,
|
|
230
|
+
created_at: new Date().toISOString(),
|
|
231
|
+
};
|
|
232
|
+
const myName = `${String(Date.now()).padStart(13, "0")}-${process.pid}-${randomUUID().slice(0, 8)}.json`;
|
|
233
|
+
writeEntry(tickets, myName, myEntry);
|
|
234
|
+
|
|
235
|
+
const startedWaiting = Date.now();
|
|
236
|
+
const deadline = startedWaiting + timeoutMs;
|
|
237
|
+
try {
|
|
238
|
+
while (true) {
|
|
239
|
+
const liveHeld = listLive(held, ttlMs);
|
|
240
|
+
const liveTickets = listLive(tickets, ttlMs);
|
|
241
|
+
const myIndex = liveTickets.findIndex((ticket) => ticket.name === myName);
|
|
242
|
+
if (myIndex === -1) {
|
|
243
|
+
// Pruned by a peer (should not happen while we are alive) or lost to
|
|
244
|
+
// a race; re-enqueue at the back rather than failing the caller.
|
|
245
|
+
writeEntry(tickets, myName, myEntry);
|
|
246
|
+
await sleep(pollMs);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const free = config.capacity - liveHeld.length;
|
|
250
|
+
if (free > 0 && myIndex < free) {
|
|
251
|
+
renameSync(join(tickets, myName), join(held, myName));
|
|
252
|
+
// Verify: the rename can transiently over-admit when a slot freed
|
|
253
|
+
// between two waiters' listings. Lexical order (= enqueue order)
|
|
254
|
+
// decides who stays; the loser rejoins the queue with its original
|
|
255
|
+
// priority because the filename keeps its enqueue timestamp.
|
|
256
|
+
const heldNow = listLive(held, ttlMs);
|
|
257
|
+
if (heldNow.length > config.capacity) {
|
|
258
|
+
const keepers = new Set(
|
|
259
|
+
heldNow.slice(0, config.capacity).map((holderEntry) => holderEntry.name),
|
|
260
|
+
);
|
|
261
|
+
if (!keepers.has(myName)) {
|
|
262
|
+
renameSync(join(held, myName), join(tickets, myName));
|
|
263
|
+
await sleep(pollMs);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
writeEntry(held, myName, { ...myEntry, acquired_at: new Date().toISOString() });
|
|
268
|
+
let released = false;
|
|
269
|
+
return {
|
|
270
|
+
entry: myName,
|
|
271
|
+
waitedMs: Date.now() - startedWaiting,
|
|
272
|
+
release: () => {
|
|
273
|
+
if (released) return;
|
|
274
|
+
released = true;
|
|
275
|
+
try {
|
|
276
|
+
unlinkSync(join(held, myName));
|
|
277
|
+
} catch {
|
|
278
|
+
// already pruned
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
options.onWait?.({
|
|
284
|
+
position: myIndex + 1,
|
|
285
|
+
holders: liveHeld.map((holderEntry) => holderEntry.entry),
|
|
286
|
+
});
|
|
287
|
+
if (Date.now() >= deadline) {
|
|
288
|
+
throw new AdmissionTimeoutError(
|
|
289
|
+
config.resource,
|
|
290
|
+
timeoutMs,
|
|
291
|
+
liveHeld.map((holderEntry) => holderEntry.entry),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
await sleep(pollMs);
|
|
295
|
+
}
|
|
296
|
+
} finally {
|
|
297
|
+
// On any non-admitted exit (timeout, caller abort via throw), leave no
|
|
298
|
+
// ticket behind. A successful acquire has already moved the file out.
|
|
299
|
+
try {
|
|
300
|
+
unlinkSync(join(tickets, myName));
|
|
301
|
+
} catch {
|
|
302
|
+
// admitted (file moved) or already pruned
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface AdmissionStatus {
|
|
308
|
+
resource: string;
|
|
309
|
+
capacity?: number;
|
|
310
|
+
holders: AdmissionEntry[];
|
|
311
|
+
waiters: AdmissionEntry[];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Snapshot the queue for one resource, pruning dead entries on the way. */
|
|
315
|
+
export function admissionStatus(
|
|
316
|
+
config: Pick<AdmissionConfig, "dir" | "resource" | "ttlMs">,
|
|
317
|
+
): AdmissionStatus {
|
|
318
|
+
const ttlMs = config.ttlMs ?? DEFAULT_TTL_MS;
|
|
319
|
+
const full: AdmissionConfig = { ...config, capacity: 1 };
|
|
320
|
+
return {
|
|
321
|
+
resource: config.resource,
|
|
322
|
+
holders: listLive(heldDir(full), ttlMs).map((holderEntry) => holderEntry.entry),
|
|
323
|
+
waiters: listLive(ticketsDir(full), ttlMs).map((ticket) => ticket.entry),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Base directory for admission queue state. tmpdir clears on reboot, which
|
|
329
|
+
* is correct — stale queue state must not outlive the boot. Override with
|
|
330
|
+
* HARNERY_ADMISSION_DIR. Host CLIs and harn commands must share this helper
|
|
331
|
+
* so they join the same machine-wide queues.
|
|
332
|
+
*/
|
|
333
|
+
export function admissionBaseDir(): string {
|
|
334
|
+
return coordEnv("ADMISSION_DIR") ?? join(tmpdir(), "harnery-admission");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Resources present under an admission base directory. */
|
|
338
|
+
export function listAdmissionResources(dir: string): string[] {
|
|
339
|
+
try {
|
|
340
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
341
|
+
.filter((entry) => entry.isDirectory())
|
|
342
|
+
.map((entry) => entry.name)
|
|
343
|
+
.sort();
|
|
344
|
+
} catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
import { type SpawnSyncOptions, spawnSync } from "node:child_process";
|
|
2
2
|
import { writeFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
type CookieJar,
|
|
5
|
-
type CookieStore,
|
|
6
|
-
type Cookie as JarCookie,
|
|
7
|
-
mergeCookies,
|
|
8
|
-
} from "../cookies/index.ts";
|
|
3
|
+
import type { CookieJar, Cookie as JarCookie } from "../cookies/index.ts";
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
6
|
* Thin wrapper over Vercel Labs' `agent-browser` Rust CLI.
|
|
@@ -216,10 +211,7 @@ export class AgentBrowser {
|
|
|
216
211
|
const parsed = JSON.parse(result.stdout);
|
|
217
212
|
const cookies: JarCookie[] = parsed.data?.cookies ?? parsed.cookies ?? [];
|
|
218
213
|
if (!Array.isArray(cookies) || cookies.length === 0) return { saved: 0 };
|
|
219
|
-
|
|
220
|
-
const store: CookieStore = jar.load();
|
|
221
|
-
const merged = mergeCookies(store, cookies);
|
|
222
|
-
jar.save(merged);
|
|
214
|
+
this.opts.jar.merge(cookies);
|
|
223
215
|
return { saved: cookies.length };
|
|
224
216
|
} catch {
|
|
225
217
|
return { saved: 0 };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Capture-fidelity probe for page review packs (pure: no browser, no src/core).
|
|
2
|
+
//
|
|
3
|
+
// A full-page screenshot of a very tall page can carry raster artifacts (a
|
|
4
|
+
// blank block, a callout clipped mid-sentence) that a viewport render of the
|
|
5
|
+
// same region does not show. While the capture browser is still open, the
|
|
6
|
+
// capture branch re-shoots every band by scroll-and-clip and pixel compares
|
|
7
|
+
// each with the crop the tiler took from the full-page image. When any band
|
|
8
|
+
// disagrees, the tiles are re-cut from those scrolled captures and the
|
|
9
|
+
// context records `capture_fidelity.source = "scrolled-bands"`.
|
|
10
|
+
//
|
|
11
|
+
// This module owns the parts that need no browser: the comparison, the
|
|
12
|
+
// decision, and the row stitch that joins viewport-height pieces into one
|
|
13
|
+
// band capture. `client.ts` owns the scroll-and-clip itself.
|
|
14
|
+
|
|
15
|
+
import { PNG } from "pngjs";
|
|
16
|
+
import type { PageReviewCaptureFidelity } from "./page-review-pack.js";
|
|
17
|
+
import { DEFAULT_REUSE_MISMATCH_RATIO, rectMismatch } from "./qa-reuse.js";
|
|
18
|
+
|
|
19
|
+
/** The band-diff reuse threshold doubles as the fidelity threshold: a probe
|
|
20
|
+
* that would count as "unchanged" for reuse counts as "agrees" here. */
|
|
21
|
+
export const CAPTURE_FIDELITY_MISMATCH_THRESHOLD = DEFAULT_REUSE_MISMATCH_RATIO;
|
|
22
|
+
|
|
23
|
+
export type FidelityProbe = PageReviewCaptureFidelity["probed"][number];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pixel-compare a band cropped from the full-page screenshot with the same
|
|
27
|
+
* band captured by scrolling. `mismatch_ratio` is measured over the area the
|
|
28
|
+
* two images share (pixelmatch, anti-aliasing tolerant, the same math as
|
|
29
|
+
* band-diff reuse). `size_mismatch` is set when the two differ by more than a
|
|
30
|
+
* rounding pixel in either dimension; the caller treats that probe as
|
|
31
|
+
* inconclusive rather than as proof the full page is wrong, because a page
|
|
32
|
+
* wider than the viewport cannot be rendered whole by scrolling.
|
|
33
|
+
*/
|
|
34
|
+
export function compareBand(
|
|
35
|
+
fullPageCrop: PNG,
|
|
36
|
+
scrolledCapture: PNG,
|
|
37
|
+
threshold: number = CAPTURE_FIDELITY_MISMATCH_THRESHOLD,
|
|
38
|
+
): { mismatch_ratio: number; exceeds: boolean; size_mismatch: boolean } {
|
|
39
|
+
const width = Math.min(fullPageCrop.width, scrolledCapture.width);
|
|
40
|
+
const height = Math.min(fullPageCrop.height, scrolledCapture.height);
|
|
41
|
+
const size_mismatch =
|
|
42
|
+
Math.abs(fullPageCrop.width - scrolledCapture.width) > 1 ||
|
|
43
|
+
Math.abs(fullPageCrop.height - scrolledCapture.height) > 1;
|
|
44
|
+
if (width <= 0 || height <= 0) return { mismatch_ratio: 1, exceeds: true, size_mismatch: true };
|
|
45
|
+
const cmp = rectMismatch(fullPageCrop, scrolledCapture, { x: 0, y: 0, width, height });
|
|
46
|
+
return { mismatch_ratio: cmp.ratio, exceeds: cmp.ratio > threshold, size_mismatch };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Turn the probe results into the record the context carries. Any probe over
|
|
51
|
+
* the threshold means the full-page screenshot is not trustworthy for this
|
|
52
|
+
* context and the tiles must come from scrolled captures.
|
|
53
|
+
*/
|
|
54
|
+
export function decideFidelity(
|
|
55
|
+
probes: readonly FidelityProbe[],
|
|
56
|
+
threshold: number = CAPTURE_FIDELITY_MISMATCH_THRESHOLD,
|
|
57
|
+
): PageReviewCaptureFidelity {
|
|
58
|
+
const mismatched = probes
|
|
59
|
+
.filter((probe) => probe.mismatch_ratio > threshold)
|
|
60
|
+
.map((probe) => probe.tile_id);
|
|
61
|
+
return {
|
|
62
|
+
source: mismatched.length > 0 ? "scrolled-bands" : "full-page",
|
|
63
|
+
probed: probes.map((probe) => ({ ...probe })),
|
|
64
|
+
mismatched,
|
|
65
|
+
mismatch_threshold: threshold,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Join PNG pieces top to bottom into one image. The output takes the first
|
|
71
|
+
* piece's width; a narrower piece leaves transparent pixels on its right, a
|
|
72
|
+
* wider one is cropped. Used to assemble a band taller than the viewport from
|
|
73
|
+
* viewport-height captures (see `Browser.captureRegionByScroll`).
|
|
74
|
+
*/
|
|
75
|
+
export function stitchPngRows(pieces: readonly Buffer[]): Buffer {
|
|
76
|
+
const first = pieces[0];
|
|
77
|
+
if (!first) throw new Error("stitchPngRows: at least one piece is required");
|
|
78
|
+
if (pieces.length === 1) return first;
|
|
79
|
+
const decoded = pieces.map((piece) => PNG.sync.read(piece));
|
|
80
|
+
const width = (decoded[0] as PNG).width;
|
|
81
|
+
const height = decoded.reduce((sum, piece) => sum + piece.height, 0);
|
|
82
|
+
const out = new PNG({ width, height });
|
|
83
|
+
let row = 0;
|
|
84
|
+
for (const piece of decoded) {
|
|
85
|
+
const copyWidth = Math.min(width, piece.width);
|
|
86
|
+
for (let y = 0; y < piece.height; y++) {
|
|
87
|
+
const srcStart = y * piece.width * 4;
|
|
88
|
+
piece.data.copy(out.data, (row + y) * width * 4, srcStart, srcStart + copyWidth * 4);
|
|
89
|
+
}
|
|
90
|
+
row += piece.height;
|
|
91
|
+
}
|
|
92
|
+
return PNG.sync.write(out);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Width and height from a PNG header, without decoding the image. */
|
|
96
|
+
export function pngDimensions(buffer: Buffer): { width: number; height: number } {
|
|
97
|
+
return { width: buffer.readUInt32BE(16), height: buffer.readUInt32BE(20) };
|
|
98
|
+
}
|