taphound 0.2.0-dev.1 → 0.2.0-dev.12
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/README.md +49 -149
- package/README.zh-CN.md +47 -147
- package/assets/skills/taphound-case-suite/SKILL.md +363 -0
- package/assets/skills/taphound-case-suite/schemas/base-flow-record.schema.json +22 -0
- package/assets/skills/taphound-case-suite/schemas/case-catalog.schema.json +26 -0
- package/assets/skills/taphound-case-suite/schemas/case-ledger.schema.json +158 -0
- package/assets/skills/taphound-case-suite/schemas/suite-input.schema.json +41 -0
- package/assets/skills/taphound-case-suite/schemas/transition.schema.json +61 -0
- package/assets/skills/taphound-case-suite/scripts/ledger.mjs +1207 -0
- package/assets/skills/taphound-case-suite/templates/base-flow-record.example.json +20 -0
- package/assets/skills/taphound-case-suite/templates/suite-input.example.json +27 -0
- package/assets/skills/taphound-case-suite/templates/transition.example.json +8 -0
- package/assets/skills/taphound-flash/SKILL.md +115 -0
- package/assets/skills/taphound-flash/scripts/flash.mjs +585 -0
- package/assets/skills/taphound-flash/templates/flash-plan.example.json +15 -0
- package/assets/skills/taphound-journey-brief-author/CONTEXT-GUIDE.md +471 -0
- package/assets/skills/taphound-journey-brief-author/SKILL.md +494 -0
- package/assets/skills/taphound-journey-brief-author/prompts/brief-author-role.md +96 -0
- package/assets/skills/taphound-journey-brief-author/prompts/brief-author-role.zh-CN.md +80 -0
- package/assets/skills/taphound-journey-brief-author/prompts/context-analyze-project.md +309 -0
- package/assets/skills/taphound-journey-brief-author/schemas/context-refresh-result.json +123 -0
- package/assets/skills/taphound-journey-brief-author/schemas/project-context-module.json +192 -0
- package/assets/skills/taphound-journey-brief-author/schemas/project-context.json +152 -0
- package/assets/skills/taphound-journey-brief-author/templates/project-context-module.example.json +58 -0
- package/assets/skills/taphound-journey-brief-author/templates/project-context.example.json +51 -0
- package/assets/skills/taphound-journey-brief-author/templates/taphound-journey-brief.template.md +63 -0
- package/assets/skills/taphound-journey-generator/SKILL.md +507 -0
- package/assets/skills/{taphound-ai-journey → taphound-journey-generator}/prompts/check-completion.md +3 -0
- package/assets/skills/taphound-journey-generator/prompts/consume-journey-brief.md +52 -0
- package/assets/skills/taphound-journey-generator/prompts/generate-step.md +211 -0
- package/assets/skills/taphound-journey-generator/prompts/select-flow.md +91 -0
- package/assets/skills/taphound-journey-generator/references/bridge.md +35 -0
- package/assets/skills/taphound-journey-generator/references/mid-session.md +44 -0
- package/assets/skills/taphound-journey-generator/schemas/external-flow.json +37 -0
- package/assets/skills/taphound-journey-generator/schemas/flow.json +28 -0
- package/assets/skills/taphound-journey-generator/schemas/journey-source.json +25 -0
- package/assets/skills/{taphound-ai-journey → taphound-journey-generator}/schemas/observe-output.json +13 -4
- package/assets/skills/{taphound-ai-journey → taphound-journey-generator}/schemas/proposed-step-envelope.json +127 -5
- package/assets/skills/taphound-journey-generator/scripts/envelope.mjs +578 -0
- package/assets/skills/taphound-journey-generator/templates/external-flow.example.json +22 -0
- package/assets/skills/taphound-journey-generator/templates/flow.example.json +22 -0
- package/assets/skills/taphound-journey-generator/templates/journey-source.example.json +17 -0
- package/assets/skills/taphound-journey-generator/templates/taphound-journey-brief.example.md +70 -0
- package/assets/skills/taphound-verify-change/SKILL.md +91 -0
- package/assets/skills/taphound-verify-change/references/accept.md +38 -0
- package/assets/skills/taphound-verify-change/references/preserve.md +357 -0
- package/assets/skills/taphound-verify-change/schemas/workflow-manifest.schema.json +295 -0
- package/assets/skills/taphound-verify-change/scripts/handoff.mjs +482 -0
- package/assets/skills/taphound-verify-change/scripts/ui-refactor.mjs +505 -0
- package/dist/adapters/adb/adb-adapter.d.ts +16 -2
- package/dist/adapters/adb/adb-adapter.js +199 -0
- package/dist/adapters/adb/system-uiautomator-snapshot-provider.d.ts +13 -0
- package/dist/adapters/adb/system-uiautomator-snapshot-provider.js +122 -0
- package/dist/adapters/adb/ui-automator-parser.d.ts +3 -0
- package/dist/adapters/adb/ui-automator-parser.js +160 -0
- package/dist/adapters/adb/window-topology-parser.d.ts +2 -0
- package/dist/adapters/adb/window-topology-parser.js +101 -0
- package/dist/adapters/android-cli/android-cli-adapter.d.ts +16 -8
- package/dist/adapters/android-cli/android-cli-adapter.js +137 -15
- package/dist/adapters/android-cli/android-cli-snapshot-provider.d.ts +9 -0
- package/dist/adapters/android-cli/android-cli-snapshot-provider.js +99 -0
- package/dist/adapters/android-cli/layout-parser.js +12 -8
- package/dist/adapters/appium/appium-doctor.d.ts +7 -0
- package/dist/adapters/appium/appium-doctor.js +128 -0
- package/dist/adapters/appium/appium-ui-snapshot-provider.d.ts +42 -0
- package/dist/adapters/appium/appium-ui-snapshot-provider.js +309 -0
- package/dist/adapters/camera/camera-probe-adapter.d.ts +18 -0
- package/dist/adapters/camera/camera-probe-adapter.js +286 -0
- package/dist/adapters/filesystem/artifact-store.js +40 -1
- package/dist/adapters/filesystem/context-document-writer.d.ts +15 -0
- package/dist/adapters/filesystem/context-document-writer.js +112 -0
- package/dist/adapters/filesystem/diagnostics-journal.d.ts +17 -0
- package/dist/adapters/filesystem/diagnostics-journal.js +130 -0
- package/dist/adapters/filesystem/external-flow-registry.d.ts +17 -0
- package/dist/adapters/filesystem/external-flow-registry.js +325 -0
- package/dist/adapters/filesystem/generation-session-store.d.ts +17 -11
- package/dist/adapters/filesystem/generation-session-store.js +281 -855
- package/dist/adapters/filesystem/generation-store/session-lock.d.ts +29 -0
- package/dist/adapters/filesystem/generation-store/session-lock.js +202 -0
- package/dist/adapters/filesystem/generation-store/session-transitions.d.ts +24 -0
- package/dist/adapters/filesystem/generation-store/session-transitions.js +340 -0
- package/dist/adapters/filesystem/generation-store/store-files.d.ts +50 -0
- package/dist/adapters/filesystem/generation-store/store-files.js +363 -0
- package/dist/adapters/filesystem/journey-composition-store.d.ts +23 -0
- package/dist/adapters/filesystem/journey-composition-store.js +151 -0
- package/dist/adapters/filesystem/knowledge-registry.d.ts +12 -0
- package/dist/adapters/filesystem/knowledge-registry.js +223 -0
- package/dist/adapters/filesystem/project-bound-file.js +3 -5
- package/dist/adapters/filesystem/project-file-inspector.d.ts +2 -0
- package/dist/adapters/filesystem/project-file-inspector.js +57 -28
- package/dist/adapters/filesystem/project-identity-inspector.d.ts +7 -0
- package/dist/adapters/filesystem/project-identity-inspector.js +215 -0
- package/dist/adapters/filesystem/project-inventory-inspector.d.ts +8 -0
- package/dist/adapters/filesystem/project-inventory-inspector.js +93 -0
- package/dist/adapters/filesystem/project-module-discoverer.d.ts +6 -0
- package/dist/adapters/filesystem/project-module-discoverer.js +158 -0
- package/dist/adapters/filesystem/skill-installer.d.ts +7 -2
- package/dist/adapters/filesystem/skill-installer.js +40 -11
- package/dist/adapters/filesystem/workspace-layout.d.ts +7 -0
- package/dist/adapters/filesystem/workspace-layout.js +57 -0
- package/dist/adapters/git/node-git-diff.d.ts +17 -0
- package/dist/adapters/git/node-git-diff.js +114 -0
- package/dist/adapters/process/node-detached-process-launcher.d.ts +6 -0
- package/dist/adapters/process/node-detached-process-launcher.js +96 -0
- package/dist/adapters/process/node-process-runner.js +11 -2
- package/dist/adapters/prompt/inquirer-align-prompt.d.ts +27 -0
- package/dist/adapters/prompt/inquirer-align-prompt.js +65 -0
- package/dist/adapters/prompt/inquirer-generation-prompt.js +3 -0
- package/dist/adapters/prompt/inquirer-recorder-prompt.d.ts +9 -1
- package/dist/adapters/prompt/inquirer-recorder-prompt.js +63 -0
- package/dist/adapters/runtime/adb-runtime-backend.d.ts +67 -0
- package/dist/adapters/runtime/adb-runtime-backend.js +206 -0
- package/dist/adapters/runtime/fake-runtime-backend.d.ts +87 -0
- package/dist/adapters/runtime/fake-runtime-backend.js +240 -0
- package/dist/adapters/runtime/mobile-mcp/mcp-tool-client.d.ts +45 -0
- package/dist/adapters/runtime/mobile-mcp/mcp-tool-client.js +153 -0
- package/dist/adapters/runtime/mobile-mcp/mobile-mcp-errors.d.ts +23 -0
- package/dist/adapters/runtime/mobile-mcp/mobile-mcp-errors.js +39 -0
- package/dist/adapters/runtime/mobile-mcp/mobile-mcp-responses.d.ts +20 -0
- package/dist/adapters/runtime/mobile-mcp/mobile-mcp-responses.js +180 -0
- package/dist/adapters/runtime/mobile-mcp/mobile-mcp-runtime-backend.d.ts +78 -0
- package/dist/adapters/runtime/mobile-mcp/mobile-mcp-runtime-backend.js +379 -0
- package/dist/adapters/runtime/mobile-mcp/mobile-mcp-tools.d.ts +46 -0
- package/dist/adapters/runtime/session-adb-view.d.ts +53 -0
- package/dist/adapters/runtime/session-adb-view.js +193 -0
- package/dist/adapters/runtime/session-backed-ports.d.ts +21 -0
- package/dist/adapters/runtime/session-backed-ports.js +43 -0
- package/dist/adapters/runtime/shared-session-runtime-backend.d.ts +12 -0
- package/dist/adapters/runtime/shared-session-runtime-backend.js +64 -0
- package/dist/adapters/ui/auto-ui-snapshot-provider.d.ts +8 -0
- package/dist/adapters/ui/auto-ui-snapshot-provider.js +48 -0
- package/dist/adapters/ui/device-layout-temp.d.ts +6 -0
- package/dist/adapters/ui/device-layout-temp.js +18 -0
- package/dist/adapters/ui/device-ui-environment.d.ts +13 -0
- package/dist/adapters/ui/device-ui-environment.js +81 -0
- package/dist/adapters/ui/layout-normalization.d.ts +9 -0
- package/dist/adapters/ui/layout-normalization.js +44 -0
- package/dist/adapters/ui/ui-snapshot-error.d.ts +11 -0
- package/dist/adapters/ui/ui-snapshot-error.js +12 -0
- package/dist/adapters/ui/ui-snapshot-support.d.ts +15 -0
- package/dist/adapters/ui/ui-snapshot-support.js +12 -0
- package/dist/application/align/align-service.d.ts +42 -0
- package/dist/application/align/align-service.js +106 -0
- package/dist/application/assertion/expectation-evaluator.d.ts +18 -5
- package/dist/application/assertion/expectation-evaluator.js +156 -18
- package/dist/application/assertion/guarded-expectation.d.ts +45 -0
- package/dist/application/assertion/guarded-expectation.js +74 -0
- package/dist/application/checkpoint/baseline-capturer.d.ts +26 -0
- package/dist/application/checkpoint/baseline-capturer.js +163 -0
- package/dist/application/checkpoint/baseline-error.d.ts +5 -0
- package/dist/application/checkpoint/baseline-error.js +8 -0
- package/dist/application/checkpoint/baseline-service.d.ts +31 -0
- package/dist/application/checkpoint/baseline-service.js +147 -0
- package/dist/application/checkpoint/checkpoint-evaluator.d.ts +35 -0
- package/dist/application/checkpoint/checkpoint-evaluator.js +415 -0
- package/dist/application/checkpoint/regression-comparator.d.ts +18 -0
- package/dist/application/checkpoint/regression-comparator.js +192 -0
- package/dist/application/collector/logcat-collector.d.ts +22 -1
- package/dist/application/collector/logcat-collector.js +103 -15
- package/dist/application/collector/logcat-event.d.ts +8 -0
- package/dist/application/collector/logcat-event.js +79 -0
- package/dist/application/collector/logcat-stop.d.ts +2 -0
- package/dist/application/collector/logcat-stop.js +11 -0
- package/dist/application/context/context-generator.d.ts +45 -0
- package/dist/application/context/context-generator.js +228 -0
- package/dist/application/context/context-loader.d.ts +40 -0
- package/dist/application/context/context-loader.js +223 -0
- package/dist/application/context/context-refresher.d.ts +59 -0
- package/dist/application/context/context-refresher.js +349 -0
- package/dist/application/context/context-rehasher.d.ts +36 -0
- package/dist/application/context/context-rehasher.js +110 -0
- package/dist/application/context/context-validator.d.ts +23 -1
- package/dist/application/context/context-validator.js +116 -9
- package/dist/application/context/evidence-hash.d.ts +1 -0
- package/dist/application/context/evidence-hash.js +125 -0
- package/dist/application/context/evidence-match.d.ts +9 -0
- package/dist/application/context/evidence-match.js +8 -0
- package/dist/application/context/shard-identity.d.ts +2 -0
- package/dist/application/context/shard-identity.js +7 -0
- package/dist/application/contract/contract-loader.d.ts +25 -0
- package/dist/application/contract/contract-loader.js +68 -0
- package/dist/application/contract/contract-review.d.ts +25 -0
- package/dist/application/contract/contract-review.js +51 -0
- package/dist/application/contract/contract-verifier.d.ts +53 -0
- package/dist/application/contract/contract-verifier.js +645 -0
- package/dist/application/devices/resolve-device-assignments.d.ts +21 -0
- package/dist/application/devices/resolve-device-assignments.js +60 -0
- package/dist/application/diagnosis/failure-classifier.d.ts +23 -0
- package/dist/application/diagnosis/failure-classifier.js +179 -0
- package/dist/application/diagnostics/diagnostics-exporter.d.ts +29 -0
- package/dist/application/diagnostics/diagnostics-exporter.js +283 -0
- package/dist/application/diagnostics/ui-capture-telemetry.d.ts +16 -0
- package/dist/application/diagnostics/ui-capture-telemetry.js +81 -0
- package/dist/application/doctor/doctor-service.d.ts +36 -3
- package/dist/application/doctor/doctor-service.js +255 -25
- package/dist/application/generation/generation-app-preparer.d.ts +20 -0
- package/dist/application/generation/generation-app-preparer.js +28 -0
- package/dist/application/generation/generation-config-service.d.ts +21 -0
- package/dist/application/generation/generation-config-service.js +53 -0
- package/dist/application/generation/generation-confirmation-service.d.ts +14 -3
- package/dist/application/generation/generation-confirmation-service.js +109 -24
- package/dist/application/generation/generation-context-snapshot.d.ts +6 -0
- package/dist/application/generation/generation-context-snapshot.js +29 -0
- package/dist/application/generation/generation-finalizer.d.ts +15 -9
- package/dist/application/generation/generation-finalizer.js +167 -70
- package/dist/application/generation/generation-publisher.js +12 -8
- package/dist/application/generation/generation-recovery-service.d.ts +35 -0
- package/dist/application/generation/generation-recovery-service.js +144 -0
- package/dist/application/generation/generation-reopen-service.d.ts +15 -0
- package/dist/application/generation/generation-reopen-service.js +37 -0
- package/dist/application/generation/generation-replace-service.d.ts +32 -0
- package/dist/application/generation/generation-replace-service.js +116 -0
- package/dist/application/generation/generation-starter.d.ts +44 -4
- package/dist/application/generation/generation-starter.js +175 -8
- package/dist/application/generation/generation-step-executor.d.ts +67 -5
- package/dist/application/generation/generation-step-executor.js +471 -108
- package/dist/application/generation/proposed-step-validator.js +96 -30
- package/dist/application/generation/replay-policy-loader.d.ts +16 -0
- package/dist/application/generation/replay-policy-loader.js +29 -0
- package/dist/application/generation/risk-evaluator.d.ts +4 -1
- package/dist/application/generation/risk-evaluator.js +118 -3
- package/dist/application/generation/runtime-observer.d.ts +39 -7
- package/dist/application/generation/runtime-observer.js +193 -24
- package/dist/application/impact/impact-resolver.d.ts +32 -0
- package/dist/application/impact/impact-resolver.js +160 -0
- package/dist/application/init/init-service.js +26 -13
- package/dist/application/interaction/action-executor.d.ts +3 -2
- package/dist/application/interaction/action-executor.js +11 -1
- package/dist/application/interaction/action-target.d.ts +17 -0
- package/dist/application/interaction/action-target.js +40 -0
- package/dist/application/interaction/external-step-runner.d.ts +153 -0
- package/dist/application/interaction/external-step-runner.js +332 -0
- package/dist/application/interaction/fallback-resolver.d.ts +6 -3
- package/dist/application/interaction/fallback-resolver.js +7 -5
- package/dist/application/interaction/scroll-to-executor.d.ts +17 -3
- package/dist/application/interaction/scroll-to-executor.js +118 -36
- package/dist/application/journey/external-flow-resolver.d.ts +25 -0
- package/dist/application/journey/external-flow-resolver.js +22 -0
- package/dist/application/journey/journey-check-service.d.ts +54 -0
- package/dist/application/journey/journey-check-service.js +207 -0
- package/dist/application/journey/journey-promoter.d.ts +26 -0
- package/dist/application/journey/journey-promoter.js +123 -0
- package/dist/application/journey/journey-resolver.d.ts +42 -0
- package/dist/application/journey/journey-resolver.js +239 -0
- package/dist/application/journey/journey-retirer.d.ts +25 -0
- package/dist/application/journey/journey-retirer.js +87 -0
- package/dist/application/knowledge/anchor-resolver.d.ts +36 -0
- package/dist/application/knowledge/anchor-resolver.js +110 -0
- package/dist/application/knowledge/knowledge-loader.d.ts +16 -0
- package/dist/application/knowledge/knowledge-loader.js +36 -0
- package/dist/application/locator/layout-failure-summary.d.ts +2 -0
- package/dist/application/locator/layout-failure-summary.js +67 -0
- package/dist/application/locator/layout-traversal.d.ts +11 -0
- package/dist/application/locator/layout-traversal.js +18 -0
- package/dist/application/locator/locator-resolver.d.ts +13 -1
- package/dist/application/locator/locator-resolver.js +182 -18
- package/dist/application/observe/observe-service.d.ts +20 -0
- package/dist/application/observe/observe-service.js +91 -0
- package/dist/application/project/project-describer.d.ts +14 -6
- package/dist/application/project/project-describer.js +38 -5
- package/dist/application/recognition/screen-detector.d.ts +28 -0
- package/dist/application/recognition/screen-detector.js +111 -0
- package/dist/application/recorder/locator-selector.js +151 -17
- package/dist/application/recorder/recorder-service.d.ts +16 -4
- package/dist/application/recorder/recorder-service.js +420 -128
- package/dist/application/report/report-writer.d.ts +5 -0
- package/dist/application/report/report-writer.js +17 -1
- package/dist/application/runtime/cold-launch.d.ts +28 -0
- package/dist/application/runtime/cold-launch.js +59 -0
- package/dist/application/runtime/step-runner.d.ts +49 -4
- package/dist/application/runtime/step-runner.js +721 -130
- package/dist/application/runtime/verify-runtime.d.ts +57 -7
- package/dist/application/runtime/verify-runtime.js +577 -175
- package/dist/application/ui/cached-ui-snapshot-provider.d.ts +45 -0
- package/dist/application/ui/cached-ui-snapshot-provider.js +111 -0
- package/dist/application/ui/observed-ui-snapshot-provider.d.ts +15 -0
- package/dist/application/ui/observed-ui-snapshot-provider.js +66 -0
- package/dist/application/ui/ui-snapshot-lifecycle.d.ts +2 -0
- package/dist/application/ui/ui-snapshot-lifecycle.js +8 -0
- package/dist/application/ui/ui-stability-probe.d.ts +9 -0
- package/dist/application/ui/ui-stability-probe.js +24 -0
- package/dist/application/wait/idle-advice.d.ts +6 -0
- package/dist/application/wait/idle-advice.js +27 -0
- package/dist/application/wait/idle-profiles.d.ts +15 -0
- package/dist/application/wait/idle-profiles.js +75 -0
- package/dist/application/wait/idle-waiter.d.ts +28 -4
- package/dist/application/wait/idle-waiter.js +201 -15
- package/dist/cli/commands/align.d.ts +3 -0
- package/dist/cli/commands/align.js +124 -0
- package/dist/cli/commands/baseline.d.ts +3 -0
- package/dist/cli/commands/baseline.js +130 -0
- package/dist/cli/commands/context.js +300 -27
- package/dist/cli/commands/contract.d.ts +3 -0
- package/dist/cli/commands/contract.js +120 -0
- package/dist/cli/commands/diagnose.d.ts +3 -0
- package/dist/cli/commands/diagnose.js +84 -0
- package/dist/cli/commands/doctor.js +16 -9
- package/dist/cli/commands/failure.d.ts +3 -0
- package/dist/cli/commands/failure.js +63 -0
- package/dist/cli/commands/generation/lifecycle-commands.d.ts +5 -0
- package/dist/cli/commands/generation/lifecycle-commands.js +211 -0
- package/dist/cli/commands/generation/session-commands.d.ts +11 -0
- package/dist/cli/commands/generation/session-commands.js +472 -0
- package/dist/cli/commands/generation/shared.d.ts +1414 -0
- package/dist/cli/commands/generation/shared.js +291 -0
- package/dist/cli/commands/generation/step-commands.d.ts +8 -0
- package/dist/cli/commands/generation/step-commands.js +315 -0
- package/dist/cli/commands/generation.js +10 -507
- package/dist/cli/commands/impact.d.ts +3 -0
- package/dist/cli/commands/impact.js +87 -0
- package/dist/cli/commands/init.js +13 -6
- package/dist/cli/commands/journey.d.ts +3 -0
- package/dist/cli/commands/journey.js +382 -0
- package/dist/cli/commands/knowledge.d.ts +3 -0
- package/dist/cli/commands/knowledge.js +93 -0
- package/dist/cli/commands/observe.d.ts +3 -0
- package/dist/cli/commands/observe.js +127 -0
- package/dist/cli/commands/project.js +2 -1
- package/dist/cli/commands/record.js +18 -5
- package/dist/cli/commands/verify.js +326 -64
- package/dist/cli/dependencies.d.ts +136 -1
- package/dist/cli/dependencies.js +504 -52
- package/dist/cli/diagnostics-recorder.d.ts +31 -0
- package/dist/cli/diagnostics-recorder.js +82 -0
- package/dist/cli/diff-verification.d.ts +41 -0
- package/dist/cli/diff-verification.js +192 -0
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +107 -2
- package/dist/cli/output.d.ts +5 -3
- package/dist/cli/output.js +12 -0
- package/dist/cli/program.js +21 -1
- package/dist/cli/project-root.d.ts +1 -0
- package/dist/cli/project-root.js +6 -0
- package/dist/cli/runtime-selection.d.ts +22 -0
- package/dist/cli/runtime-selection.js +92 -0
- package/dist/cli/version.d.ts +1 -0
- package/dist/cli/version.js +14 -0
- package/dist/cli/workspace-guard.d.ts +2 -0
- package/dist/cli/workspace-guard.js +3 -0
- package/dist/domain/binding-reference.d.ts +4 -0
- package/dist/domain/binding-reference.js +23 -0
- package/dist/domain/checkpoint.d.ts +404 -0
- package/dist/domain/checkpoint.js +297 -0
- package/dist/domain/config.d.ts +107 -1
- package/dist/domain/config.js +52 -6
- package/dist/domain/contract.d.ts +413 -0
- package/dist/domain/contract.js +196 -0
- package/dist/domain/diagnostics.d.ts +1396 -0
- package/dist/domain/diagnostics.js +193 -0
- package/dist/domain/external-flow.d.ts +493 -0
- package/dist/domain/external-flow.js +15 -0
- package/dist/domain/failure-classification.d.ts +176 -0
- package/dist/domain/failure-classification.js +169 -0
- package/dist/domain/failure.d.ts +2 -1
- package/dist/domain/failure.js +123 -0
- package/dist/domain/generation.d.ts +1671 -132
- package/dist/domain/generation.js +202 -13
- package/dist/domain/geometry.d.ts +13 -0
- package/dist/domain/geometry.js +16 -0
- package/dist/domain/impact.d.ts +71 -0
- package/dist/domain/impact.js +76 -0
- package/dist/domain/init.d.ts +12 -4
- package/dist/domain/init.js +24 -18
- package/dist/domain/journey-composition.d.ts +2285 -0
- package/dist/domain/journey-composition.js +85 -0
- package/dist/domain/journey-lifecycle.d.ts +10 -0
- package/dist/domain/journey-lifecycle.js +9 -0
- package/dist/domain/journey.d.ts +2419 -212
- package/dist/domain/journey.js +384 -14
- package/dist/domain/knowledge.d.ts +165 -0
- package/dist/domain/knowledge.js +188 -0
- package/dist/domain/layout.d.ts +34 -5
- package/dist/domain/layout.js +64 -6
- package/dist/domain/locator-evidence.d.ts +3 -0
- package/dist/domain/locator-evidence.js +36 -0
- package/dist/domain/logcat-event.d.ts +54 -0
- package/dist/domain/logcat-event.js +40 -0
- package/dist/domain/observation.d.ts +33 -0
- package/dist/domain/observation.js +18 -0
- package/dist/domain/project-context.d.ts +219 -1
- package/dist/domain/project-context.js +209 -6
- package/dist/domain/proposed-step.d.ts +340 -60
- package/dist/domain/proposed-step.js +23 -3
- package/dist/domain/report.d.ts +1455 -28
- package/dist/domain/report.js +211 -14
- package/dist/domain/runtime-snapshot.d.ts +58 -1
- package/dist/domain/runtime-snapshot.js +14 -3
- package/dist/domain/runtime.d.ts +50 -0
- package/dist/domain/runtime.js +36 -0
- package/dist/domain/system-app-profiles.d.ts +6 -0
- package/dist/domain/system-app-profiles.js +43 -0
- package/dist/domain/ui-backend.d.ts +28 -0
- package/dist/domain/ui-backend.js +33 -0
- package/dist/domain/ui-cache.d.ts +11 -0
- package/dist/domain/ui-cache.js +10 -0
- package/dist/domain/verify-receipt.d.ts +17 -0
- package/dist/domain/verify-receipt.js +17 -0
- package/dist/domain/window-hierarchy.d.ts +84 -0
- package/dist/domain/window-hierarchy.js +172 -0
- package/dist/domain/workflow-manifest.d.ts +77 -0
- package/dist/domain/workflow-manifest.js +136 -0
- package/dist/domain/workspace.d.ts +30 -0
- package/dist/domain/workspace.js +74 -0
- package/dist/ports/adb.d.ts +34 -1
- package/dist/ports/align-prompt.d.ts +11 -0
- package/dist/ports/align-prompt.js +6 -0
- package/dist/ports/anchor-resolver.d.ts +30 -0
- package/dist/ports/anchor-resolver.js +1 -0
- package/dist/ports/annotated-screen-resolver.d.ts +4 -0
- package/dist/ports/annotated-screen-resolver.js +1 -0
- package/dist/ports/camera-probe.d.ts +23 -0
- package/dist/ports/camera-probe.js +8 -0
- package/dist/ports/context-document-writer.d.ts +24 -0
- package/dist/ports/context-document-writer.js +1 -0
- package/dist/ports/detached-process-launcher.d.ts +12 -0
- package/dist/ports/detached-process-launcher.js +1 -0
- package/dist/ports/diagnostics.d.ts +15 -0
- package/dist/ports/diagnostics.js +1 -0
- package/dist/ports/external-flow-registry.d.ts +37 -0
- package/dist/ports/external-flow-registry.js +1 -0
- package/dist/ports/generation-prompt.d.ts +2 -1
- package/dist/ports/generation-session-store.d.ts +12 -2
- package/dist/ports/git-diff.d.ts +9 -0
- package/dist/ports/git-diff.js +1 -0
- package/dist/ports/journey-composition-store.d.ts +17 -0
- package/dist/ports/journey-composition-store.js +1 -0
- package/dist/ports/knowledge-registry.d.ts +20 -0
- package/dist/ports/knowledge-registry.js +1 -0
- package/dist/ports/process-runner.d.ts +7 -0
- package/dist/ports/project-file-inspector.d.ts +1 -0
- package/dist/ports/project-identity-inspector.d.ts +24 -0
- package/dist/ports/project-identity-inspector.js +1 -0
- package/dist/ports/project-inventory-inspector.d.ts +21 -0
- package/dist/ports/project-inventory-inspector.js +1 -0
- package/dist/ports/project-module-discoverer.d.ts +26 -0
- package/dist/ports/project-module-discoverer.js +1 -0
- package/dist/ports/recorder-prompt.d.ts +10 -1
- package/dist/ports/runtime-backend.d.ts +89 -0
- package/dist/ports/runtime-backend.js +13 -0
- package/dist/ports/runtime-capability.d.ts +12 -0
- package/dist/ports/runtime-capability.js +20 -0
- package/dist/ports/runtime-session-ports.d.ts +25 -0
- package/dist/ports/runtime-session-ports.js +1 -0
- package/dist/ports/screenshot.d.ts +11 -0
- package/dist/ports/screenshot.js +1 -0
- package/dist/ports/skill-installer.d.ts +2 -2
- package/dist/ports/ui-snapshot.d.ts +35 -0
- package/dist/ports/ui-snapshot.js +1 -0
- package/dist/ports/ui-stability.d.ts +19 -0
- package/dist/ports/ui-stability.js +1 -0
- package/dist/ports/workspace-layout.d.ts +4 -0
- package/dist/ports/workspace-layout.js +1 -0
- package/dist/shared/errors.d.ts +2 -0
- package/dist/shared/errors.js +22 -0
- package/dist/shared/paths.d.ts +2 -0
- package/dist/shared/paths.js +24 -0
- package/dist/shared/strings.d.ts +2 -0
- package/dist/shared/strings.js +16 -0
- package/package.json +7 -2
- package/assets/skills/taphound-ai-journey/GUIDE.md +0 -862
- package/assets/skills/taphound-ai-journey/SKILL.md +0 -377
- package/assets/skills/taphound-ai-journey/prompts/analyze-project.md +0 -526
- package/assets/skills/taphound-ai-journey/prompts/generate-step.md +0 -109
- package/assets/skills/taphound-ai-journey/schemas/project-context.json +0 -81
- package/assets/skills/taphound-ai-journey/templates/project-context.example.json +0 -49
- package/dist/ports/android-cli.d.ts +0 -21
- /package/dist/{ports/android-cli.js → adapters/runtime/mobile-mcp/mobile-mcp-tools.js} +0 -0
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { constants, link, lstat, mkdir, open, readdir,
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { constants, link, lstat, mkdir, open, readdir, rename, rm, unlink } from "node:fs/promises";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { GenerationSessionSchema, PendingConfirmationSchema, isGenerationConfirmationExpired } from "../../domain/generation.js";
|
|
5
|
+
import { BUILD_DIR, GENERATIONS_DIR, TAPHOUND_DIR, activeGenerationBundleName } from "../../domain/workspace.js";
|
|
5
6
|
import { GenerationSessionStoreError } from "../../ports/generation-session-store.js";
|
|
7
|
+
import { isErrnoException } from "../../shared/errors.js";
|
|
8
|
+
import { compareStrings } from "../../shared/strings.js";
|
|
9
|
+
import { ensureBuildIgnored } from "./workspace-layout.js";
|
|
10
|
+
import { SessionLock } from "./generation-store/session-lock.js";
|
|
11
|
+
import { assertArchiveTransition, assertBundlePublishableTransition, assertConfirmationTransition, assertCoreIdentityPreserved, assertId, assertLatestSnapshotPreserved, assertOrdinaryTransition, assertRecoveryTransition, assertSnapshotTransition, assertVerificationCompletionTransition, assertVerificationRecoveryTransition, assertVerificationReopenTransition, parseInFlight, parseSession, parseVerificationPhase, sameInFlight, sessionId, transitionStableState, validateExpectedRevision, validateInitialRevision, validateNextRevision } from "./generation-store/session-transitions.js";
|
|
12
|
+
import { asStoreIoError, assertContained, assertEvidenceNamespaceAvailable, canonicalizeEvidence, captureDirectoryEvidence, captureStoreDirectory, createOrRequireDirectory, evidenceEntrySnapshot, evidenceNotFound, fileIdentity, pathExists, readBoundState, readStateFromDirectory, requireRealDirectory, sameIdentity, sameSnapshotMetadata, serializeJson, snapshotMetadata, syncDirectory, validateEvidencePath, verifyDirectoryEvidence, verifyStoreDirectory, writeStateAtomically } from "./generation-store/store-files.js";
|
|
6
13
|
const DEFAULT_OPTIONS = {
|
|
7
14
|
lockTimeoutMs: 2_000,
|
|
8
15
|
lockRetryMs: 10
|
|
9
16
|
};
|
|
17
|
+
function activeBundleName(id) {
|
|
18
|
+
return activeGenerationBundleName(id);
|
|
19
|
+
}
|
|
10
20
|
const HOOK_NAMES = [
|
|
11
21
|
"beforeLockStagingWrite",
|
|
12
22
|
"beforeLockInstall",
|
|
@@ -44,6 +54,11 @@ function parseStoreConfiguration(projectRoot, options) {
|
|
|
44
54
|
if (nowInput !== undefined && typeof nowInput !== "function") {
|
|
45
55
|
throw new TypeError("now must be a function");
|
|
46
56
|
}
|
|
57
|
+
const generationRootInput = Reflect.get(options, "generationRoot");
|
|
58
|
+
if (generationRootInput !== undefined
|
|
59
|
+
&& typeof generationRootInput !== "string") {
|
|
60
|
+
throw new TypeError("generationRoot must be a string");
|
|
61
|
+
}
|
|
47
62
|
const hooksInput = Reflect.get(options, "hooks");
|
|
48
63
|
const hooks = {};
|
|
49
64
|
if (hooksInput !== undefined) {
|
|
@@ -64,7 +79,10 @@ function parseStoreConfiguration(projectRoot, options) {
|
|
|
64
79
|
projectRoot: resolve(projectRoot),
|
|
65
80
|
options: {
|
|
66
81
|
lockTimeoutMs: lockTimeoutMs,
|
|
67
|
-
lockRetryMs: lockRetryMs
|
|
82
|
+
lockRetryMs: lockRetryMs,
|
|
83
|
+
...(generationRootInput === undefined
|
|
84
|
+
? {}
|
|
85
|
+
: { generationRoot: generationRootInput })
|
|
68
86
|
},
|
|
69
87
|
now: (nowInput ?? (() => new Date())),
|
|
70
88
|
hooks
|
|
@@ -74,610 +92,6 @@ function parseStoreConfiguration(projectRoot, options) {
|
|
|
74
92
|
throw new GenerationSessionStoreError("IO_ERROR", "Generation session store configuration is invalid", { cause: error });
|
|
75
93
|
}
|
|
76
94
|
}
|
|
77
|
-
function snapshotMetadata(stats) {
|
|
78
|
-
return {
|
|
79
|
-
dev: stats.dev,
|
|
80
|
-
ino: stats.ino,
|
|
81
|
-
size: stats.size,
|
|
82
|
-
mtimeNs: stats.mtimeNs,
|
|
83
|
-
ctimeNs: stats.ctimeNs
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
function sameSnapshotMetadata(left, right) {
|
|
87
|
-
return (sameIdentity(left, right)
|
|
88
|
-
&& left.size === right.size
|
|
89
|
-
&& left.mtimeNs === right.mtimeNs
|
|
90
|
-
&& left.ctimeNs === right.ctimeNs);
|
|
91
|
-
}
|
|
92
|
-
function evidenceEntryType(entry) {
|
|
93
|
-
if (entry.isFile())
|
|
94
|
-
return "file";
|
|
95
|
-
if (entry.isDirectory())
|
|
96
|
-
return "directory";
|
|
97
|
-
if (entry.isSymbolicLink())
|
|
98
|
-
return "symlink";
|
|
99
|
-
return "other";
|
|
100
|
-
}
|
|
101
|
-
function evidenceEntrySnapshot(entries) {
|
|
102
|
-
return entries
|
|
103
|
-
.map((entry) => `${entry.name}\0${evidenceEntryType(entry)}`)
|
|
104
|
-
.sort();
|
|
105
|
-
}
|
|
106
|
-
function sameIdentity(left, right) {
|
|
107
|
-
return left.dev === right.dev && left.ino === right.ino;
|
|
108
|
-
}
|
|
109
|
-
async function captureStoreDirectory(path) {
|
|
110
|
-
const stats = await lstat(path, { bigint: true });
|
|
111
|
-
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
112
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation store path is not a real directory: ${path}`);
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
path,
|
|
116
|
-
canonicalPath: await realpath(path),
|
|
117
|
-
identity: { dev: stats.dev, ino: stats.ino }
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
async function verifyStoreDirectory(expected) {
|
|
121
|
-
const current = await captureStoreDirectory(expected.path);
|
|
122
|
-
if (current.canonicalPath !== expected.canonicalPath
|
|
123
|
-
|| !sameIdentity(current.identity, expected.identity)) {
|
|
124
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation store directory identity changed: ${expected.path}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
async function captureStoreFile(path) {
|
|
128
|
-
const stats = await lstat(path, { bigint: true });
|
|
129
|
-
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
130
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation store path is not a real file: ${path}`);
|
|
131
|
-
}
|
|
132
|
-
return { dev: stats.dev, ino: stats.ino };
|
|
133
|
-
}
|
|
134
|
-
async function captureOptionalStoreFile(path) {
|
|
135
|
-
try {
|
|
136
|
-
return await captureStoreFile(path);
|
|
137
|
-
}
|
|
138
|
-
catch (error) {
|
|
139
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async function verifyOptionalStoreFile(path, expected) {
|
|
146
|
-
const current = await captureOptionalStoreFile(path);
|
|
147
|
-
if ((current === null) !== (expected === null)
|
|
148
|
-
|| (current !== null
|
|
149
|
-
&& expected !== null
|
|
150
|
-
&& !sameIdentity(current, expected))) {
|
|
151
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation state file identity changed: ${path}`);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function isNodeError(error) {
|
|
155
|
-
return error instanceof Error && "code" in error;
|
|
156
|
-
}
|
|
157
|
-
function asStoreIoError(error, action) {
|
|
158
|
-
if (error instanceof GenerationSessionStoreError) {
|
|
159
|
-
return error;
|
|
160
|
-
}
|
|
161
|
-
return new GenerationSessionStoreError("IO_ERROR", `Generation session store failed to ${action}`, { cause: error });
|
|
162
|
-
}
|
|
163
|
-
function assertId(id) {
|
|
164
|
-
if (typeof id !== "string"
|
|
165
|
-
|| !/^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(id)) {
|
|
166
|
-
throw new GenerationSessionStoreError("INVALID_ID", "Invalid generation session id");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function parseSession(value, classifyInvalidId = false) {
|
|
170
|
-
try {
|
|
171
|
-
return GenerationSessionSchema.parse(value);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
if (classifyInvalidId && value !== null && typeof value === "object") {
|
|
175
|
-
try {
|
|
176
|
-
const id = Reflect.get(value, "id");
|
|
177
|
-
if (typeof id === "string") {
|
|
178
|
-
assertId(id);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
catch (idError) {
|
|
182
|
-
if (idError instanceof GenerationSessionStoreError) {
|
|
183
|
-
throw idError;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
throw new GenerationSessionStoreError("INVALID_SESSION", "Generation session state is invalid", { cause: error });
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
function sessionId(value) {
|
|
191
|
-
const id = value.id;
|
|
192
|
-
assertId(id);
|
|
193
|
-
return id;
|
|
194
|
-
}
|
|
195
|
-
function validateExpectedRevision(revision) {
|
|
196
|
-
if (!Number.isSafeInteger(revision) || revision < 0) {
|
|
197
|
-
throw new GenerationSessionStoreError("INVALID_REVISION", "Invalid expected generation revision");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function validateInitialRevision(session) {
|
|
201
|
-
if (session.revision !== 0) {
|
|
202
|
-
throw new GenerationSessionStoreError("INVALID_REVISION", "A new generation session must start at revision 0");
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function validateNextRevision(id, expectedRevision, next) {
|
|
206
|
-
validateExpectedRevision(expectedRevision);
|
|
207
|
-
if (expectedRevision === Number.MAX_SAFE_INTEGER) {
|
|
208
|
-
throw new GenerationSessionStoreError("INVALID_REVISION", "Generation revision cannot increment beyond Number.MAX_SAFE_INTEGER");
|
|
209
|
-
}
|
|
210
|
-
if (sessionId(next) !== id) {
|
|
211
|
-
throw new GenerationSessionStoreError("INVALID_ID", "Updated generation session id does not match the requested id");
|
|
212
|
-
}
|
|
213
|
-
if (next.revision !== expectedRevision + 1) {
|
|
214
|
-
throw new GenerationSessionStoreError("INVALID_REVISION", "Next generation revision must increment expectedRevision by exactly one");
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
function parseInFlight(value) {
|
|
218
|
-
try {
|
|
219
|
-
return GenerationInFlightSchema.parse(value);
|
|
220
|
-
}
|
|
221
|
-
catch (error) {
|
|
222
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Expected inFlight record is invalid", { cause: error });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
function sameInFlight(left, right) {
|
|
226
|
-
return JSON.stringify(left) === JSON.stringify(right);
|
|
227
|
-
}
|
|
228
|
-
function transitionStableState(session) {
|
|
229
|
-
return {
|
|
230
|
-
version: session.version,
|
|
231
|
-
id: session.id,
|
|
232
|
-
bindings: session.bindings,
|
|
233
|
-
target: session.target,
|
|
234
|
-
variables: session.variables,
|
|
235
|
-
candidateSteps: session.candidateSteps,
|
|
236
|
-
candidateSources: session.candidateSources,
|
|
237
|
-
pendingConfirmation: session.pendingConfirmation,
|
|
238
|
-
verification: session.verification,
|
|
239
|
-
publication: session.publication
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
function assertCoreIdentityPreserved(current, next) {
|
|
243
|
-
if (JSON.stringify(generationCoreIdentity(current))
|
|
244
|
-
!== JSON.stringify(generationCoreIdentity(next))) {
|
|
245
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Generation Core identity is immutable");
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
function assertLatestSnapshotPreserved(current, next) {
|
|
249
|
-
if (current.bindings.snapshotHash !== next.bindings.snapshotHash) {
|
|
250
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Latest snapshot may only change through commitSnapshot");
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
function assertSnapshotTransition(current, next) {
|
|
254
|
-
assertCoreIdentityPreserved(current, next);
|
|
255
|
-
const currentBindings = {
|
|
256
|
-
projectHash: current.bindings.projectHash,
|
|
257
|
-
configHash: current.bindings.configHash,
|
|
258
|
-
contextHash: current.bindings.contextHash
|
|
259
|
-
};
|
|
260
|
-
const { snapshotHash: nextSnapshotHash, ...nextBindings } = next.bindings;
|
|
261
|
-
if (current.state !== "active"
|
|
262
|
-
|| current.inFlight !== null
|
|
263
|
-
|| current.pendingConfirmation !== null
|
|
264
|
-
|| current.verification.status !== "notRun"
|
|
265
|
-
|| current.publication.status !== "notRun"
|
|
266
|
-
|| next.state !== "active"
|
|
267
|
-
|| next.inFlight !== null
|
|
268
|
-
|| next.pendingConfirmation !== null
|
|
269
|
-
|| nextSnapshotHash === null
|
|
270
|
-
|| JSON.stringify(currentBindings) !== JSON.stringify(nextBindings)
|
|
271
|
-
|| JSON.stringify({
|
|
272
|
-
...transitionStableState(current),
|
|
273
|
-
bindings: undefined
|
|
274
|
-
}) !== JSON.stringify({
|
|
275
|
-
...transitionStableState(next),
|
|
276
|
-
bindings: undefined
|
|
277
|
-
})) {
|
|
278
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Snapshot commit may only replace the active snapshot binding");
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
function assertOrdinaryTransition(current, next) {
|
|
282
|
-
assertCoreIdentityPreserved(current, next);
|
|
283
|
-
assertLatestSnapshotPreserved(current, next);
|
|
284
|
-
if (current.verification.status !== "notRun"
|
|
285
|
-
|| current.publication.status !== "notRun"
|
|
286
|
-
|| JSON.stringify(current.verification) !== JSON.stringify(next.verification)
|
|
287
|
-
|| JSON.stringify(current.publication) !== JSON.stringify(next.publication)) {
|
|
288
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification and publication require explicit transitions");
|
|
289
|
-
}
|
|
290
|
-
if (current.state === "recoveryRequired") {
|
|
291
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Recovery-required state must use the explicit recovery transition");
|
|
292
|
-
}
|
|
293
|
-
if (JSON.stringify(current.pendingConfirmation)
|
|
294
|
-
!== JSON.stringify(next.pendingConfirmation)) {
|
|
295
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Pending confirmation must use the explicit confirmation transition");
|
|
296
|
-
}
|
|
297
|
-
const stableStateMatches = JSON.stringify(transitionStableState(current))
|
|
298
|
-
=== JSON.stringify(transitionStableState(next));
|
|
299
|
-
if (current.inFlight !== null) {
|
|
300
|
-
if (next.state === "recoveryRequired"
|
|
301
|
-
&& sameInFlight(current.inFlight, next.inFlight)
|
|
302
|
-
&& stableStateMatches) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Persisted inFlight may only be marked recoveryRequired without mutation");
|
|
306
|
-
}
|
|
307
|
-
if (next.state === "recoveryRequired"
|
|
308
|
-
|| (next.inFlight !== null && !stableStateMatches)) {
|
|
309
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Starting inFlight cannot mutate candidate or result state");
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function verificationStableState(session) {
|
|
313
|
-
return {
|
|
314
|
-
...transitionStableState(session),
|
|
315
|
-
verification: undefined
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
function assertVerificationCompletionTransition(current, next, status) {
|
|
319
|
-
assertCoreIdentityPreserved(current, next);
|
|
320
|
-
assertLatestSnapshotPreserved(current, next);
|
|
321
|
-
if (current.state !== "active"
|
|
322
|
-
|| next.state !== "active"
|
|
323
|
-
|| current.inFlight !== null
|
|
324
|
-
|| next.inFlight !== null
|
|
325
|
-
|| current.pendingConfirmation !== null
|
|
326
|
-
|| next.pendingConfirmation !== null
|
|
327
|
-
|| current.verification.status !== "running"
|
|
328
|
-
|| next.verification.status !== status
|
|
329
|
-
|| next.publication.status !== "notRun"
|
|
330
|
-
|| JSON.stringify(verificationStableState(current))
|
|
331
|
-
!== JSON.stringify(verificationStableState(next))) {
|
|
332
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", `Verification completion must record ${status} from the running attempt`);
|
|
333
|
-
}
|
|
334
|
-
if (next.verification.status === "passed"
|
|
335
|
-
&& next.verification.attemptId !== current.verification.attemptId) {
|
|
336
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification completion attempt does not match");
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
function assertBundlePublishableTransition(current, next) {
|
|
340
|
-
assertCoreIdentityPreserved(current, next);
|
|
341
|
-
assertLatestSnapshotPreserved(current, next);
|
|
342
|
-
if (current.state !== "active"
|
|
343
|
-
|| next.state !== "active"
|
|
344
|
-
|| current.inFlight !== null
|
|
345
|
-
|| next.inFlight !== null
|
|
346
|
-
|| current.pendingConfirmation !== null
|
|
347
|
-
|| next.pendingConfirmation !== null
|
|
348
|
-
|| current.verification.status !== "passed"
|
|
349
|
-
|| JSON.stringify(current.verification) !== JSON.stringify(next.verification)
|
|
350
|
-
|| current.publication.status !== "notRun"
|
|
351
|
-
|| next.publication.status !== "published"
|
|
352
|
-
|| JSON.stringify({
|
|
353
|
-
...transitionStableState(current),
|
|
354
|
-
publication: undefined
|
|
355
|
-
}) !== JSON.stringify({
|
|
356
|
-
...transitionStableState(next),
|
|
357
|
-
publication: undefined
|
|
358
|
-
})) {
|
|
359
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Bundle publication must be explicitly marked after passed verification");
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
function confirmationStableState(session) {
|
|
363
|
-
const stable = transitionStableState(session);
|
|
364
|
-
return { ...stable, pendingConfirmation: undefined };
|
|
365
|
-
}
|
|
366
|
-
function assertConfirmationTransition(current, next) {
|
|
367
|
-
assertCoreIdentityPreserved(current, next);
|
|
368
|
-
assertLatestSnapshotPreserved(current, next);
|
|
369
|
-
if (current.state !== "active"
|
|
370
|
-
|| next.state !== "active"
|
|
371
|
-
|| current.inFlight !== null
|
|
372
|
-
|| next.inFlight !== null
|
|
373
|
-
|| current.verification.status !== "notRun"
|
|
374
|
-
|| current.publication.status !== "notRun"
|
|
375
|
-
|| JSON.stringify(confirmationStableState(current))
|
|
376
|
-
!== JSON.stringify(confirmationStableState(next))) {
|
|
377
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Confirmation transition may only change pending confirmation state");
|
|
378
|
-
}
|
|
379
|
-
const before = current.pendingConfirmation;
|
|
380
|
-
const after = next.pendingConfirmation;
|
|
381
|
-
if (before === null) {
|
|
382
|
-
if (after !== null && after.status === "pending") {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
else if (after === null) {
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
else if (before.status === "pending"
|
|
390
|
-
&& after.status === "approved"
|
|
391
|
-
&& JSON.stringify({ ...before, status: undefined })
|
|
392
|
-
=== JSON.stringify({ ...after, status: undefined })) {
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Invalid pending confirmation lifecycle transition");
|
|
396
|
-
}
|
|
397
|
-
function assertRecoveryTransition(current, next) {
|
|
398
|
-
assertCoreIdentityPreserved(current, next);
|
|
399
|
-
assertLatestSnapshotPreserved(current, next);
|
|
400
|
-
if (current.state !== "recoveryRequired"
|
|
401
|
-
|| current.inFlight === null
|
|
402
|
-
|| next.state !== "active"
|
|
403
|
-
|| next.inFlight !== null
|
|
404
|
-
|| JSON.stringify(transitionStableState(current))
|
|
405
|
-
!== JSON.stringify(transitionStableState(next))) {
|
|
406
|
-
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Recovery must only clear preserved inFlight evidence and reactivate state");
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
async function pathExists(path) {
|
|
410
|
-
try {
|
|
411
|
-
await lstat(path);
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
416
|
-
return false;
|
|
417
|
-
}
|
|
418
|
-
throw error;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
async function requireRealDirectory(path) {
|
|
422
|
-
const stats = await lstat(path);
|
|
423
|
-
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
424
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation store path is not a real directory: ${path}`);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
async function createOrRequireDirectory(path) {
|
|
428
|
-
let created = false;
|
|
429
|
-
try {
|
|
430
|
-
await mkdir(path);
|
|
431
|
-
created = true;
|
|
432
|
-
}
|
|
433
|
-
catch (error) {
|
|
434
|
-
if (!isNodeError(error) || error.code !== "EEXIST") {
|
|
435
|
-
throw error;
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
await requireRealDirectory(path);
|
|
439
|
-
return created;
|
|
440
|
-
}
|
|
441
|
-
async function syncDirectory(path) {
|
|
442
|
-
const handle = await open(path, constants.O_RDONLY);
|
|
443
|
-
try {
|
|
444
|
-
await handle.sync();
|
|
445
|
-
}
|
|
446
|
-
finally {
|
|
447
|
-
await handle.close();
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
function serializeJson(value) {
|
|
451
|
-
return `${JSON.stringify(value, null, 2)}\n`;
|
|
452
|
-
}
|
|
453
|
-
function canonicalizeEvidenceValue(value, ancestors = new Set()) {
|
|
454
|
-
if (value === null
|
|
455
|
-
|| typeof value === "string"
|
|
456
|
-
|| typeof value === "boolean") {
|
|
457
|
-
return value;
|
|
458
|
-
}
|
|
459
|
-
if (typeof value === "number") {
|
|
460
|
-
if (Number.isFinite(value)) {
|
|
461
|
-
return value;
|
|
462
|
-
}
|
|
463
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence JSON cannot contain non-finite numbers");
|
|
464
|
-
}
|
|
465
|
-
if (typeof value !== "object") {
|
|
466
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence value must be JSON serializable");
|
|
467
|
-
}
|
|
468
|
-
if (ancestors.has(value)) {
|
|
469
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence JSON cannot contain cycles");
|
|
470
|
-
}
|
|
471
|
-
ancestors.add(value);
|
|
472
|
-
try {
|
|
473
|
-
if (Array.isArray(value)) {
|
|
474
|
-
return value.map((item) => canonicalizeEvidenceValue(item, ancestors));
|
|
475
|
-
}
|
|
476
|
-
const prototype = Object.getPrototypeOf(value);
|
|
477
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
478
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence value must contain only JSON objects");
|
|
479
|
-
}
|
|
480
|
-
return Object.fromEntries(Object.keys(value)
|
|
481
|
-
.sort((left, right) => left.localeCompare(right))
|
|
482
|
-
.map((key) => [
|
|
483
|
-
key,
|
|
484
|
-
canonicalizeEvidenceValue(value[key], ancestors)
|
|
485
|
-
]));
|
|
486
|
-
}
|
|
487
|
-
finally {
|
|
488
|
-
ancestors.delete(value);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
function canonicalizeEvidence(value) {
|
|
492
|
-
try {
|
|
493
|
-
return canonicalizeEvidenceValue(value);
|
|
494
|
-
}
|
|
495
|
-
catch (error) {
|
|
496
|
-
if (error instanceof GenerationSessionStoreError) {
|
|
497
|
-
throw error;
|
|
498
|
-
}
|
|
499
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE", "Evidence value cannot be safely inspected", { cause: error });
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
function validateEvidencePath(relativePath) {
|
|
503
|
-
if (typeof relativePath !== "string"
|
|
504
|
-
|| relativePath.length === 0
|
|
505
|
-
|| relativePath.includes("\\")
|
|
506
|
-
|| relativePath.includes("\0")
|
|
507
|
-
|| isAbsolute(relativePath)
|
|
508
|
-
|| posix.isAbsolute(relativePath)) {
|
|
509
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", "Invalid generation evidence path");
|
|
510
|
-
}
|
|
511
|
-
const segments = relativePath.split("/");
|
|
512
|
-
if (segments.some((segment) => (segment.length === 0
|
|
513
|
-
|| segment === "."
|
|
514
|
-
|| segment === ".."))
|
|
515
|
-
|| posix.normalize(relativePath) !== relativePath) {
|
|
516
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", "Invalid generation evidence path");
|
|
517
|
-
}
|
|
518
|
-
return segments;
|
|
519
|
-
}
|
|
520
|
-
function assertContained(root, candidate, allowRoot = false) {
|
|
521
|
-
const fromRoot = relative(root, candidate);
|
|
522
|
-
if ((!allowRoot && fromRoot.length === 0)
|
|
523
|
-
|| fromRoot.startsWith("..")
|
|
524
|
-
|| isAbsolute(fromRoot)) {
|
|
525
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence path escapes generation session: ${candidate}`);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
async function captureDirectoryEvidence(path) {
|
|
529
|
-
const stats = await lstat(path, { bigint: true });
|
|
530
|
-
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
531
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence parent is not a real directory: ${path}`);
|
|
532
|
-
}
|
|
533
|
-
return {
|
|
534
|
-
path,
|
|
535
|
-
canonicalPath: await realpath(path),
|
|
536
|
-
identity: { dev: stats.dev, ino: stats.ino }
|
|
537
|
-
};
|
|
538
|
-
}
|
|
539
|
-
async function verifyDirectoryEvidence(activeCanonicalPath, directories) {
|
|
540
|
-
for (const evidence of directories) {
|
|
541
|
-
const current = await captureDirectoryEvidence(evidence.path);
|
|
542
|
-
assertContained(activeCanonicalPath, current.canonicalPath, true);
|
|
543
|
-
if (current.canonicalPath !== evidence.canonicalPath
|
|
544
|
-
|| current.identity.dev !== evidence.identity.dev
|
|
545
|
-
|| current.identity.ino !== evidence.identity.ino) {
|
|
546
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence parent identity changed: ${evidence.path}`);
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
async function assertEvidenceNamespaceAvailable(activeDirectory, activeEvidence, inFlight) {
|
|
551
|
-
const relativePath = `evidence/steps/${String(inFlight.stepIndex)}-${inFlight.attemptId}`;
|
|
552
|
-
const segments = validateEvidencePath(relativePath);
|
|
553
|
-
await verifyStoreDirectory(activeEvidence);
|
|
554
|
-
let parent = activeDirectory;
|
|
555
|
-
const directories = [activeEvidence];
|
|
556
|
-
for (const [index, segment] of segments.entries()) {
|
|
557
|
-
const candidate = join(parent, segment);
|
|
558
|
-
assertContained(activeDirectory, candidate);
|
|
559
|
-
let stats;
|
|
560
|
-
try {
|
|
561
|
-
stats = await lstat(candidate);
|
|
562
|
-
}
|
|
563
|
-
catch (error) {
|
|
564
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
565
|
-
await verifyDirectoryEvidence(activeEvidence.canonicalPath, directories);
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
throw error;
|
|
569
|
-
}
|
|
570
|
-
if (index === segments.length - 1) {
|
|
571
|
-
throw new GenerationSessionStoreError("EVIDENCE_ALREADY_EXISTS", `Generation attempt evidence namespace already exists: ${relativePath}`);
|
|
572
|
-
}
|
|
573
|
-
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
574
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Attempt evidence parent is not a safe directory: ${relativePath}`);
|
|
575
|
-
}
|
|
576
|
-
const evidence = await captureDirectoryEvidence(candidate);
|
|
577
|
-
assertContained(activeEvidence.canonicalPath, evidence.canonicalPath, true);
|
|
578
|
-
directories.push(evidence);
|
|
579
|
-
parent = candidate;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
async function fileIdentity(path) {
|
|
583
|
-
const stats = await lstat(path, { bigint: true });
|
|
584
|
-
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
585
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence output is not a regular file: ${path}`);
|
|
586
|
-
}
|
|
587
|
-
return { dev: stats.dev, ino: stats.ino };
|
|
588
|
-
}
|
|
589
|
-
function evidenceNotFound(relativePath, cause) {
|
|
590
|
-
return new GenerationSessionStoreError("EVIDENCE_NOT_FOUND", `Generation evidence does not exist: ${relativePath}`, { cause });
|
|
591
|
-
}
|
|
592
|
-
async function readStateFromDirectory(directory, afterOpen, expectedDirectory) {
|
|
593
|
-
const directoryEvidence = expectedDirectory
|
|
594
|
-
?? await captureStoreDirectory(directory);
|
|
595
|
-
await verifyStoreDirectory(directoryEvidence);
|
|
596
|
-
const statePath = join(directory, "state.json");
|
|
597
|
-
let text;
|
|
598
|
-
try {
|
|
599
|
-
const handle = await open(statePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
600
|
-
try {
|
|
601
|
-
const openedStats = await handle.stat({ bigint: true });
|
|
602
|
-
if (!openedStats.isFile()) {
|
|
603
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation state is not a regular file: ${statePath}`);
|
|
604
|
-
}
|
|
605
|
-
const openedIdentity = {
|
|
606
|
-
dev: openedStats.dev,
|
|
607
|
-
ino: openedStats.ino
|
|
608
|
-
};
|
|
609
|
-
await afterOpen?.();
|
|
610
|
-
await verifyStoreDirectory(directoryEvidence);
|
|
611
|
-
const pathIdentity = await captureStoreFile(statePath);
|
|
612
|
-
if (!sameIdentity(openedIdentity, pathIdentity)) {
|
|
613
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation state file identity changed: ${statePath}`);
|
|
614
|
-
}
|
|
615
|
-
text = await handle.readFile("utf8");
|
|
616
|
-
}
|
|
617
|
-
finally {
|
|
618
|
-
await handle.close();
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
catch (error) {
|
|
622
|
-
if (error instanceof GenerationSessionStoreError) {
|
|
623
|
-
throw error;
|
|
624
|
-
}
|
|
625
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Unable to read generation session state: ${statePath}`, { cause: error });
|
|
626
|
-
}
|
|
627
|
-
try {
|
|
628
|
-
return parseSession(JSON.parse(text));
|
|
629
|
-
}
|
|
630
|
-
catch (error) {
|
|
631
|
-
if (error instanceof GenerationSessionStoreError) {
|
|
632
|
-
throw error;
|
|
633
|
-
}
|
|
634
|
-
throw new GenerationSessionStoreError("INVALID_SESSION", `Unable to parse generation session state: ${statePath}`, { cause: error });
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
async function readBoundState(directory, id, afterOpen, expectedDirectory) {
|
|
638
|
-
const session = await readStateFromDirectory(directory, afterOpen, expectedDirectory);
|
|
639
|
-
if (sessionId(session) !== id) {
|
|
640
|
-
throw new GenerationSessionStoreError("INVALID_SESSION", "Persisted generation session id does not match its directory");
|
|
641
|
-
}
|
|
642
|
-
return session;
|
|
643
|
-
}
|
|
644
|
-
async function writeStateAtomically(directory, session, sync = syncDirectory, beforeRename, expectedDirectory, beforeInstall) {
|
|
645
|
-
const directoryEvidence = expectedDirectory
|
|
646
|
-
?? await captureStoreDirectory(directory);
|
|
647
|
-
await verifyStoreDirectory(directoryEvidence);
|
|
648
|
-
const statePath = join(directory, "state.json");
|
|
649
|
-
const originalStateIdentity = await captureOptionalStoreFile(statePath);
|
|
650
|
-
const temporaryPath = join(directory, `.state.json.${randomUUID()}.tmp`);
|
|
651
|
-
let handle;
|
|
652
|
-
try {
|
|
653
|
-
handle = await open(temporaryPath, constants.O_WRONLY
|
|
654
|
-
| constants.O_CREAT
|
|
655
|
-
| constants.O_EXCL
|
|
656
|
-
| constants.O_NOFOLLOW, 0o600);
|
|
657
|
-
await handle.writeFile(serializeJson(session), "utf8");
|
|
658
|
-
await handle.sync();
|
|
659
|
-
await handle.close();
|
|
660
|
-
handle = undefined;
|
|
661
|
-
const temporaryIdentity = await captureStoreFile(temporaryPath);
|
|
662
|
-
await beforeRename?.();
|
|
663
|
-
await verifyStoreDirectory(directoryEvidence);
|
|
664
|
-
await verifyOptionalStoreFile(statePath, originalStateIdentity);
|
|
665
|
-
await beforeInstall?.();
|
|
666
|
-
await rename(temporaryPath, statePath);
|
|
667
|
-
const installedIdentity = await captureStoreFile(statePath);
|
|
668
|
-
if (!sameIdentity(temporaryIdentity, installedIdentity)) {
|
|
669
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation state install identity changed: ${statePath}`);
|
|
670
|
-
}
|
|
671
|
-
await sync(directory);
|
|
672
|
-
}
|
|
673
|
-
catch (error) {
|
|
674
|
-
if (handle !== undefined) {
|
|
675
|
-
await handle.close().catch(() => undefined);
|
|
676
|
-
}
|
|
677
|
-
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
678
|
-
throw error;
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
95
|
export class FileSystemGenerationSessionStore {
|
|
682
96
|
projectRoot;
|
|
683
97
|
generationRoot;
|
|
@@ -685,14 +99,26 @@ export class FileSystemGenerationSessionStore {
|
|
|
685
99
|
options;
|
|
686
100
|
now;
|
|
687
101
|
hooks;
|
|
102
|
+
customGenerationRoot;
|
|
103
|
+
lock;
|
|
688
104
|
constructor(projectRoot, options = {}) {
|
|
689
105
|
const configuration = parseStoreConfiguration(projectRoot, options);
|
|
690
106
|
this.projectRoot = configuration.projectRoot;
|
|
691
|
-
this.generationRoot =
|
|
107
|
+
this.generationRoot = typeof configuration.options.generationRoot === "string"
|
|
108
|
+
? resolve(configuration.options.generationRoot)
|
|
109
|
+
: join(this.projectRoot, GENERATIONS_DIR);
|
|
110
|
+
this.customGenerationRoot = this.generationRoot !== join(this.projectRoot, GENERATIONS_DIR);
|
|
692
111
|
this.locksRoot = join(this.generationRoot, ".locks");
|
|
693
112
|
this.options = configuration.options;
|
|
694
113
|
this.now = configuration.now;
|
|
695
114
|
this.hooks = configuration.hooks;
|
|
115
|
+
this.lock = new SessionLock({
|
|
116
|
+
generationRoot: this.generationRoot,
|
|
117
|
+
locksRoot: this.locksRoot,
|
|
118
|
+
lockTimeoutMs: this.options.lockTimeoutMs,
|
|
119
|
+
lockRetryMs: this.options.lockRetryMs,
|
|
120
|
+
hooks: this.hooks
|
|
121
|
+
});
|
|
696
122
|
}
|
|
697
123
|
create = async (input) => {
|
|
698
124
|
const session = parseSession(input, true);
|
|
@@ -836,7 +262,13 @@ export class FileSystemGenerationSessionStore {
|
|
|
836
262
|
|| (approvedConfirmation !== undefined
|
|
837
263
|
&& (approvedConfirmation.status !== "approved"
|
|
838
264
|
|| approvedConfirmation.proposalHash !== inFlight.proposalHash
|
|
839
|
-
|| approvedConfirmation.snapshotHash !== inFlight.snapshotHash))
|
|
265
|
+
|| approvedConfirmation.snapshotHash !== inFlight.snapshotHash))
|
|
266
|
+
|| JSON.stringify(inFlight.confirmation) !== JSON.stringify(approvedConfirmation === undefined
|
|
267
|
+
? undefined
|
|
268
|
+
: {
|
|
269
|
+
challengeId: approvedConfirmation.challengeId,
|
|
270
|
+
approvalMode: approvedConfirmation.approvalMode ?? "localTty"
|
|
271
|
+
})) {
|
|
840
272
|
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Step begin requires exact active state and approved confirmation");
|
|
841
273
|
}
|
|
842
274
|
const next = GenerationSessionSchema.parse({
|
|
@@ -851,8 +283,7 @@ export class FileSystemGenerationSessionStore {
|
|
|
851
283
|
const now = this.now();
|
|
852
284
|
if (!(now instanceof Date)
|
|
853
285
|
|| !Number.isFinite(now.getTime())
|
|
854
|
-
|| now
|
|
855
|
-
>= new Date(approvedConfirmation.expiresAt).getTime()) {
|
|
286
|
+
|| isGenerationConfirmationExpired(approvedConfirmation, now)) {
|
|
856
287
|
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Approved confirmation expired before step begin");
|
|
857
288
|
}
|
|
858
289
|
}
|
|
@@ -938,7 +369,7 @@ export class FileSystemGenerationSessionStore {
|
|
|
938
369
|
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
939
370
|
});
|
|
940
371
|
};
|
|
941
|
-
beginVerification = async (id, expectedRevision, attemptId) => {
|
|
372
|
+
beginVerification = async (id, expectedRevision, attemptId, owner) => {
|
|
942
373
|
assertId(id);
|
|
943
374
|
validateExpectedRevision(expectedRevision);
|
|
944
375
|
assertId(attemptId);
|
|
@@ -971,7 +402,81 @@ export class FileSystemGenerationSessionStore {
|
|
|
971
402
|
const next = GenerationSessionSchema.parse({
|
|
972
403
|
...current,
|
|
973
404
|
revision: current.revision + 1,
|
|
974
|
-
verification: {
|
|
405
|
+
verification: {
|
|
406
|
+
status: "running",
|
|
407
|
+
attemptId,
|
|
408
|
+
...(owner === undefined
|
|
409
|
+
? {}
|
|
410
|
+
: { ownerPid: owner.pid, startedAt: owner.startedAt })
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
414
|
+
return next;
|
|
415
|
+
});
|
|
416
|
+
};
|
|
417
|
+
updateVerificationPhase = async (id, attemptId, phase) => {
|
|
418
|
+
assertId(id);
|
|
419
|
+
assertId(attemptId);
|
|
420
|
+
const parsedPhase = parseVerificationPhase(phase);
|
|
421
|
+
await this.ensureGenerationRoot();
|
|
422
|
+
await this.withLock(id, async () => {
|
|
423
|
+
const activeDirectory = this.activeDirectory(id);
|
|
424
|
+
if (!await pathExists(activeDirectory)) {
|
|
425
|
+
if (await pathExists(this.finalDirectory(id))) {
|
|
426
|
+
throw new GenerationSessionStoreError("SESSION_PUBLISHED", `Published generation session cannot update verification phase: ${id}`);
|
|
427
|
+
}
|
|
428
|
+
throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
|
|
429
|
+
}
|
|
430
|
+
const activeEvidence = await captureStoreDirectory(activeDirectory);
|
|
431
|
+
const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
|
|
432
|
+
if (current.state !== "active"
|
|
433
|
+
|| current.verification.status !== "running"
|
|
434
|
+
|| current.verification.attemptId !== attemptId
|
|
435
|
+
|| current.publication.status !== "notRun") {
|
|
436
|
+
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification phase updates require the exact active running attempt");
|
|
437
|
+
}
|
|
438
|
+
const next = GenerationSessionSchema.parse({
|
|
439
|
+
...current,
|
|
440
|
+
verification: {
|
|
441
|
+
...current.verification,
|
|
442
|
+
phase: parsedPhase
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
446
|
+
});
|
|
447
|
+
};
|
|
448
|
+
abortVerification = async (id, expectedRevision) => {
|
|
449
|
+
assertId(id);
|
|
450
|
+
validateExpectedRevision(expectedRevision);
|
|
451
|
+
await this.ensureGenerationRoot();
|
|
452
|
+
return this.withLock(id, async () => {
|
|
453
|
+
const activeDirectory = this.activeDirectory(id);
|
|
454
|
+
if (!await pathExists(activeDirectory)) {
|
|
455
|
+
if (await pathExists(this.finalDirectory(id))) {
|
|
456
|
+
throw new GenerationSessionStoreError("SESSION_PUBLISHED", `Published generation session cannot abort verification: ${id}`);
|
|
457
|
+
}
|
|
458
|
+
throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
|
|
459
|
+
}
|
|
460
|
+
const activeEvidence = await captureStoreDirectory(activeDirectory);
|
|
461
|
+
const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
|
|
462
|
+
if (current.revision !== expectedRevision) {
|
|
463
|
+
throw new GenerationSessionStoreError("REVISION_CONFLICT", `Expected generation revision ${String(expectedRevision)}, found ${String(current.revision)}`);
|
|
464
|
+
}
|
|
465
|
+
if (current.state !== "active"
|
|
466
|
+
|| current.inFlight !== null
|
|
467
|
+
|| current.pendingConfirmation !== null
|
|
468
|
+
|| current.verification.status !== "running"
|
|
469
|
+
|| current.publication.status !== "notRun") {
|
|
470
|
+
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification abort requires an active running attempt");
|
|
471
|
+
}
|
|
472
|
+
if (await pathExists(join(activeDirectory, "verification", "receipt.json"))
|
|
473
|
+
|| await pathExists(join(activeDirectory, "verification", "report.json"))) {
|
|
474
|
+
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification abort is forbidden after immutable evidence exists");
|
|
475
|
+
}
|
|
476
|
+
const next = GenerationSessionSchema.parse({
|
|
477
|
+
...current,
|
|
478
|
+
revision: current.revision + 1,
|
|
479
|
+
verification: { status: "notRun" }
|
|
975
480
|
});
|
|
976
481
|
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
977
482
|
return next;
|
|
@@ -983,6 +488,52 @@ export class FileSystemGenerationSessionStore {
|
|
|
983
488
|
failVerification = async (id, expectedRevision, input) => {
|
|
984
489
|
await this.writeVerificationTransition(id, expectedRevision, input, "failed");
|
|
985
490
|
};
|
|
491
|
+
recoverVerification = async (id, expectedRevision, input) => {
|
|
492
|
+
assertId(id);
|
|
493
|
+
const next = parseSession(input, true);
|
|
494
|
+
validateNextRevision(id, expectedRevision, next);
|
|
495
|
+
await this.ensureGenerationRoot();
|
|
496
|
+
await this.withLock(id, async () => {
|
|
497
|
+
const activeDirectory = this.activeDirectory(id);
|
|
498
|
+
if (!await pathExists(activeDirectory)) {
|
|
499
|
+
throw new GenerationSessionStoreError(await pathExists(this.finalDirectory(id))
|
|
500
|
+
? "SESSION_PUBLISHED"
|
|
501
|
+
: "SESSION_NOT_FOUND", `Generation session cannot recover verification: ${id}`);
|
|
502
|
+
}
|
|
503
|
+
const activeEvidence = await captureStoreDirectory(activeDirectory);
|
|
504
|
+
const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
|
|
505
|
+
if (current.revision !== expectedRevision) {
|
|
506
|
+
throw new GenerationSessionStoreError("REVISION_CONFLICT", `Expected generation revision ${String(expectedRevision)}, found ${String(current.revision)}`);
|
|
507
|
+
}
|
|
508
|
+
if (await pathExists(join(activeDirectory, "verification", "receipt.json"))
|
|
509
|
+
|| await pathExists(join(activeDirectory, "verification", "report.json"))) {
|
|
510
|
+
throw new GenerationSessionStoreError("INVALID_TRANSITION", "Verification recovery is forbidden after immutable evidence exists");
|
|
511
|
+
}
|
|
512
|
+
assertVerificationRecoveryTransition(current, next);
|
|
513
|
+
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
reopenVerification = async (id, expectedRevision, input) => {
|
|
517
|
+
assertId(id);
|
|
518
|
+
const next = parseSession(input, true);
|
|
519
|
+
validateNextRevision(id, expectedRevision, next);
|
|
520
|
+
await this.ensureGenerationRoot();
|
|
521
|
+
await this.withLock(id, async () => {
|
|
522
|
+
const activeDirectory = this.activeDirectory(id);
|
|
523
|
+
if (!await pathExists(activeDirectory)) {
|
|
524
|
+
throw new GenerationSessionStoreError(await pathExists(this.finalDirectory(id))
|
|
525
|
+
? "SESSION_PUBLISHED"
|
|
526
|
+
: "SESSION_NOT_FOUND", `Generation session cannot reopen verification: ${id}`);
|
|
527
|
+
}
|
|
528
|
+
const activeEvidence = await captureStoreDirectory(activeDirectory);
|
|
529
|
+
const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
|
|
530
|
+
if (current.revision !== expectedRevision) {
|
|
531
|
+
throw new GenerationSessionStoreError("REVISION_CONFLICT", `Expected generation revision ${String(expectedRevision)}, found ${String(current.revision)}`);
|
|
532
|
+
}
|
|
533
|
+
assertVerificationReopenTransition(current, next);
|
|
534
|
+
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
535
|
+
});
|
|
536
|
+
};
|
|
986
537
|
markBundlePublishable = async (id, expectedRevision, input) => {
|
|
987
538
|
assertId(id);
|
|
988
539
|
const next = parseSession(input, true);
|
|
@@ -1005,6 +556,48 @@ export class FileSystemGenerationSessionStore {
|
|
|
1005
556
|
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
1006
557
|
});
|
|
1007
558
|
};
|
|
559
|
+
archive = async (id, expectedRevision, input) => {
|
|
560
|
+
assertId(id);
|
|
561
|
+
const next = parseSession(input, true);
|
|
562
|
+
validateNextRevision(id, expectedRevision, next);
|
|
563
|
+
await this.ensureGenerationRoot();
|
|
564
|
+
await this.withLock(id, async () => {
|
|
565
|
+
const activeDirectory = this.activeDirectory(id);
|
|
566
|
+
if (!await pathExists(activeDirectory)) {
|
|
567
|
+
if (await pathExists(this.finalDirectory(id))) {
|
|
568
|
+
throw new GenerationSessionStoreError("SESSION_PUBLISHED", `Published generation session cannot be archived: ${id}`);
|
|
569
|
+
}
|
|
570
|
+
throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
|
|
571
|
+
}
|
|
572
|
+
const activeEvidence = await captureStoreDirectory(activeDirectory);
|
|
573
|
+
const current = await readBoundState(activeDirectory, id, this.hooks.afterStateOpen, activeEvidence);
|
|
574
|
+
if (current.revision !== expectedRevision) {
|
|
575
|
+
throw new GenerationSessionStoreError("REVISION_CONFLICT", `Expected generation revision ${String(expectedRevision)}, found ${String(current.revision)}`);
|
|
576
|
+
}
|
|
577
|
+
assertArchiveTransition(current, next);
|
|
578
|
+
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
579
|
+
});
|
|
580
|
+
};
|
|
581
|
+
list = async () => {
|
|
582
|
+
await this.ensureGenerationRoot();
|
|
583
|
+
const entries = await readdir(this.generationRoot, {
|
|
584
|
+
withFileTypes: true
|
|
585
|
+
});
|
|
586
|
+
const sessions = [];
|
|
587
|
+
for (const entry of entries) {
|
|
588
|
+
if (!entry.isDirectory() || entry.name === ".locks") {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const directory = join(this.generationRoot, entry.name);
|
|
592
|
+
const statePath = join(directory, "state.json");
|
|
593
|
+
if (!await pathExists(statePath)) {
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const session = await readStateFromDirectory(directory);
|
|
597
|
+
sessions.push(session);
|
|
598
|
+
}
|
|
599
|
+
return sessions.sort((left, right) => left.id.localeCompare(right.id));
|
|
600
|
+
};
|
|
1008
601
|
writeEvidence = async (id, relativePath, value) => {
|
|
1009
602
|
const canonicalValue = canonicalizeEvidence(value);
|
|
1010
603
|
await this.produceEvidence(id, relativePath, async (temporaryPath) => {
|
|
@@ -1129,7 +722,7 @@ export class FileSystemGenerationSessionStore {
|
|
|
1129
722
|
// Preserve any path that cannot be proven to be our own link.
|
|
1130
723
|
}
|
|
1131
724
|
}
|
|
1132
|
-
if (
|
|
725
|
+
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
1133
726
|
throw new GenerationSessionStoreError("EVIDENCE_ALREADY_EXISTS", `Generation evidence already exists: ${relativePath}`, { cause: error });
|
|
1134
727
|
}
|
|
1135
728
|
throw error;
|
|
@@ -1140,67 +733,10 @@ export class FileSystemGenerationSessionStore {
|
|
|
1140
733
|
});
|
|
1141
734
|
};
|
|
1142
735
|
readEvidence = async (id, relativePath) => {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
return this.
|
|
1147
|
-
const finalDirectory = this.finalDirectory(id);
|
|
1148
|
-
const activeDirectory = this.activeDirectory(id);
|
|
1149
|
-
const directory = await pathExists(finalDirectory)
|
|
1150
|
-
? finalDirectory
|
|
1151
|
-
: activeDirectory;
|
|
1152
|
-
if (!await pathExists(directory)) {
|
|
1153
|
-
throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
|
|
1154
|
-
}
|
|
1155
|
-
const rootEvidence = await captureStoreDirectory(directory);
|
|
1156
|
-
await readBoundState(directory, id, this.hooks.afterStateOpen, rootEvidence);
|
|
1157
|
-
let current = directory;
|
|
1158
|
-
const directories = [rootEvidence];
|
|
1159
|
-
for (const segment of segments.slice(0, -1)) {
|
|
1160
|
-
current = join(current, segment);
|
|
1161
|
-
let evidence;
|
|
1162
|
-
try {
|
|
1163
|
-
evidence = await captureDirectoryEvidence(current);
|
|
1164
|
-
}
|
|
1165
|
-
catch (error) {
|
|
1166
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1167
|
-
throw evidenceNotFound(relativePath, error);
|
|
1168
|
-
}
|
|
1169
|
-
throw error;
|
|
1170
|
-
}
|
|
1171
|
-
assertContained(rootEvidence.canonicalPath, evidence.canonicalPath);
|
|
1172
|
-
directories.push(evidence);
|
|
1173
|
-
}
|
|
1174
|
-
const path = join(current, segments.at(-1));
|
|
1175
|
-
assertContained(directory, path);
|
|
1176
|
-
await verifyDirectoryEvidence(rootEvidence.canonicalPath, directories);
|
|
1177
|
-
let handle;
|
|
1178
|
-
try {
|
|
1179
|
-
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1180
|
-
}
|
|
1181
|
-
catch (error) {
|
|
1182
|
-
if (isNodeError(error) && error.code === "ENOENT") {
|
|
1183
|
-
throw evidenceNotFound(relativePath, error);
|
|
1184
|
-
}
|
|
1185
|
-
throw error;
|
|
1186
|
-
}
|
|
1187
|
-
try {
|
|
1188
|
-
const stats = await handle.stat({ bigint: true });
|
|
1189
|
-
if (!stats.isFile()) {
|
|
1190
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence is not a regular file: ${relativePath}`);
|
|
1191
|
-
}
|
|
1192
|
-
const openedIdentity = { dev: stats.dev, ino: stats.ino };
|
|
1193
|
-
const pathIdentity = await fileIdentity(path);
|
|
1194
|
-
if (!sameIdentity(openedIdentity, pathIdentity)) {
|
|
1195
|
-
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence file identity changed: ${relativePath}`);
|
|
1196
|
-
}
|
|
1197
|
-
await verifyDirectoryEvidence(rootEvidence.canonicalPath, directories);
|
|
1198
|
-
return await handle.readFile();
|
|
1199
|
-
}
|
|
1200
|
-
finally {
|
|
1201
|
-
await handle.close();
|
|
1202
|
-
}
|
|
1203
|
-
});
|
|
736
|
+
return this.withVerifiedEvidence(id, relativePath, ({ handle }) => handle.readFile());
|
|
737
|
+
};
|
|
738
|
+
evidenceReference = async (id, relativePath) => {
|
|
739
|
+
return this.withVerifiedEvidence(id, relativePath, ({ published, segments }) => (`${GENERATIONS_DIR}/${published ? id : activeBundleName(id)}/${segments.join("/")}`));
|
|
1204
740
|
};
|
|
1205
741
|
listEvidence = async (id) => {
|
|
1206
742
|
assertId(id);
|
|
@@ -1248,7 +784,7 @@ export class FileSystemGenerationSessionStore {
|
|
|
1248
784
|
stats = await lstat(childPath, { bigint: true });
|
|
1249
785
|
}
|
|
1250
786
|
catch (error) {
|
|
1251
|
-
if (
|
|
787
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
1252
788
|
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Generation evidence disappeared during snapshot: ${relativePath}`);
|
|
1253
789
|
}
|
|
1254
790
|
throw error;
|
|
@@ -1312,7 +848,7 @@ export class FileSystemGenerationSessionStore {
|
|
|
1312
848
|
await verifyStoreDirectory(directoryEvidence);
|
|
1313
849
|
};
|
|
1314
850
|
await visit(directory, []);
|
|
1315
|
-
return files.sort((left, right) => left.path
|
|
851
|
+
return files.sort((left, right) => compareStrings(left.path, right.path));
|
|
1316
852
|
});
|
|
1317
853
|
};
|
|
1318
854
|
publish = async (id) => {
|
|
@@ -1351,7 +887,7 @@ export class FileSystemGenerationSessionStore {
|
|
|
1351
887
|
}
|
|
1352
888
|
}
|
|
1353
889
|
catch (error) {
|
|
1354
|
-
if (
|
|
890
|
+
if (isErrnoException(error)
|
|
1355
891
|
&& (error.code === "EEXIST"
|
|
1356
892
|
|| error.code === "ENOTEMPTY")) {
|
|
1357
893
|
throw new GenerationSessionStoreError("PUBLISH_DESTINATION_EXISTS", `Generation publish destination already exists: ${finalDirectory}`, { cause: error });
|
|
@@ -1364,17 +900,28 @@ export class FileSystemGenerationSessionStore {
|
|
|
1364
900
|
};
|
|
1365
901
|
ensureGenerationRoot = async () => {
|
|
1366
902
|
try {
|
|
903
|
+
if (this.customGenerationRoot) {
|
|
904
|
+
await mkdir(dirname(this.generationRoot), { recursive: true });
|
|
905
|
+
await createOrRequireDirectory(this.generationRoot);
|
|
906
|
+
await createOrRequireDirectory(this.locksRoot);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
1367
909
|
await requireRealDirectory(this.projectRoot);
|
|
1368
|
-
const taphoundDirectory = join(this.projectRoot,
|
|
910
|
+
const taphoundDirectory = join(this.projectRoot, TAPHOUND_DIR);
|
|
1369
911
|
if (await createOrRequireDirectory(taphoundDirectory)) {
|
|
1370
912
|
await this.syncDirectory(this.projectRoot);
|
|
1371
913
|
}
|
|
1372
|
-
|
|
914
|
+
const buildDirectory = join(this.projectRoot, BUILD_DIR);
|
|
915
|
+
if (await createOrRequireDirectory(buildDirectory)) {
|
|
1373
916
|
await this.syncDirectory(taphoundDirectory);
|
|
1374
917
|
}
|
|
918
|
+
if (await createOrRequireDirectory(this.generationRoot)) {
|
|
919
|
+
await this.syncDirectory(buildDirectory);
|
|
920
|
+
}
|
|
1375
921
|
if (await createOrRequireDirectory(this.locksRoot)) {
|
|
1376
922
|
await this.syncDirectory(this.generationRoot);
|
|
1377
923
|
}
|
|
924
|
+
await ensureBuildIgnored(this.projectRoot);
|
|
1378
925
|
}
|
|
1379
926
|
catch (error) {
|
|
1380
927
|
throw asStoreIoError(error, "initialize its generation directory");
|
|
@@ -1402,195 +949,74 @@ export class FileSystemGenerationSessionStore {
|
|
|
1402
949
|
await writeStateAtomically(activeDirectory, next, this.syncDirectory, this.hooks.beforeStateRename, activeEvidence);
|
|
1403
950
|
});
|
|
1404
951
|
};
|
|
1405
|
-
activeDirectory = (id) => (join(this.generationRoot,
|
|
952
|
+
activeDirectory = (id) => (join(this.generationRoot, activeBundleName(id)));
|
|
1406
953
|
finalDirectory = (id) => (join(this.generationRoot, id));
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
await this.
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
locksRootEvidence = await captureStoreDirectory(this.locksRoot);
|
|
1419
|
-
await this.acquireLock(id, { pid: process.pid, token }, generationRootEvidence, locksRootEvidence);
|
|
1420
|
-
await verifyStoreDirectory(generationRootEvidence);
|
|
1421
|
-
await verifyStoreDirectory(locksRootEvidence);
|
|
1422
|
-
}
|
|
1423
|
-
catch (error) {
|
|
1424
|
-
throw asStoreIoError(error, "acquire its exclusive lock");
|
|
1425
|
-
}
|
|
1426
|
-
let outcome;
|
|
1427
|
-
let releaseError;
|
|
1428
|
-
try {
|
|
1429
|
-
outcome = {
|
|
1430
|
-
status: "succeeded",
|
|
1431
|
-
value: await operation()
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
catch (error) {
|
|
1435
|
-
outcome = { status: "failed", error };
|
|
1436
|
-
}
|
|
1437
|
-
finally {
|
|
1438
|
-
try {
|
|
1439
|
-
await this.releaseLock(id, token);
|
|
1440
|
-
}
|
|
1441
|
-
catch (error) {
|
|
1442
|
-
releaseError = error;
|
|
1443
|
-
}
|
|
1444
|
-
}
|
|
1445
|
-
if (outcome.status === "failed") {
|
|
1446
|
-
throw asStoreIoError(outcome.error, "complete a locked operation");
|
|
1447
|
-
}
|
|
1448
|
-
if (releaseError !== undefined) {
|
|
1449
|
-
throw asStoreIoError(releaseError, "release its exclusive lock");
|
|
1450
|
-
}
|
|
1451
|
-
return outcome.value;
|
|
1452
|
-
};
|
|
1453
|
-
acquireLock = async (id, owner, generationRootEvidence, locksRootEvidence) => {
|
|
1454
|
-
const lockPath = this.lockPath(id);
|
|
1455
|
-
const deadline = Date.now() + this.options.lockTimeoutMs;
|
|
1456
|
-
for (;;) {
|
|
1457
|
-
if (await this.tryInstallLock(id, lockPath, owner, generationRootEvidence, locksRootEvidence)) {
|
|
1458
|
-
return;
|
|
1459
|
-
}
|
|
1460
|
-
await this.reapDeadOwnerLock(id, lockPath);
|
|
1461
|
-
if (Date.now() >= deadline) {
|
|
1462
|
-
throw new GenerationSessionStoreError("LOCK_TIMEOUT", `Timed out acquiring generation session lock: ${id}`);
|
|
1463
|
-
}
|
|
1464
|
-
await new Promise((resolveDelay) => {
|
|
1465
|
-
setTimeout(resolveDelay, this.options.lockRetryMs);
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
};
|
|
1469
|
-
tryInstallLock = async (id, lockPath, owner, generationRootEvidence, locksRootEvidence) => {
|
|
1470
|
-
const stagingPath = join(this.locksRoot, `.${id}.lock.acquire-${randomUUID()}.tmp`);
|
|
1471
|
-
try {
|
|
1472
|
-
const handle = await open(stagingPath, "wx", 0o600);
|
|
1473
|
-
try {
|
|
1474
|
-
await this.hooks.beforeLockStagingWrite?.();
|
|
1475
|
-
await handle.writeFile(serializeJson(owner), "utf8");
|
|
1476
|
-
await handle.sync();
|
|
954
|
+
withVerifiedEvidence = async (id, relativePath, operation) => {
|
|
955
|
+
assertId(id);
|
|
956
|
+
const segments = validateEvidencePath(relativePath);
|
|
957
|
+
await this.ensureGenerationRoot();
|
|
958
|
+
return this.withLock(id, async () => {
|
|
959
|
+
const finalDirectory = this.finalDirectory(id);
|
|
960
|
+
const activeDirectory = this.activeDirectory(id);
|
|
961
|
+
const published = await pathExists(finalDirectory);
|
|
962
|
+
const directory = published ? finalDirectory : activeDirectory;
|
|
963
|
+
if (!await pathExists(directory)) {
|
|
964
|
+
throw new GenerationSessionStoreError("SESSION_NOT_FOUND", `Generation session does not exist: ${id}`);
|
|
1477
965
|
}
|
|
1478
|
-
|
|
1479
|
-
|
|
966
|
+
const rootEvidence = await captureStoreDirectory(directory);
|
|
967
|
+
await readBoundState(directory, id, this.hooks.afterStateOpen, rootEvidence);
|
|
968
|
+
let current = directory;
|
|
969
|
+
const directories = [rootEvidence];
|
|
970
|
+
for (const segment of segments.slice(0, -1)) {
|
|
971
|
+
current = join(current, segment);
|
|
972
|
+
let evidence;
|
|
973
|
+
try {
|
|
974
|
+
evidence = await captureDirectoryEvidence(current);
|
|
975
|
+
}
|
|
976
|
+
catch (error) {
|
|
977
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
978
|
+
throw evidenceNotFound(relativePath, error);
|
|
979
|
+
}
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
assertContained(rootEvidence.canonicalPath, evidence.canonicalPath);
|
|
983
|
+
directories.push(evidence);
|
|
1480
984
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
await
|
|
985
|
+
const path = join(current, segments.at(-1));
|
|
986
|
+
assertContained(directory, path);
|
|
987
|
+
await verifyDirectoryEvidence(rootEvidence.canonicalPath, directories);
|
|
988
|
+
let handle;
|
|
1484
989
|
try {
|
|
1485
|
-
await
|
|
990
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1486
991
|
}
|
|
1487
992
|
catch (error) {
|
|
1488
|
-
if (
|
|
1489
|
-
|
|
993
|
+
if (isErrnoException(error) && error.code === "ENOENT") {
|
|
994
|
+
throw evidenceNotFound(relativePath, error);
|
|
1490
995
|
}
|
|
1491
996
|
throw error;
|
|
1492
997
|
}
|
|
1493
998
|
try {
|
|
1494
|
-
await
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
}
|
|
1498
|
-
catch (error) {
|
|
1499
|
-
await this.releaseLock(id, owner.token).catch(() => undefined);
|
|
1500
|
-
throw error;
|
|
1501
|
-
}
|
|
1502
|
-
return true;
|
|
1503
|
-
}
|
|
1504
|
-
finally {
|
|
1505
|
-
await unlink(stagingPath).catch(() => undefined);
|
|
1506
|
-
}
|
|
1507
|
-
};
|
|
1508
|
-
reapDeadOwnerLock = async (id, lockPath) => {
|
|
1509
|
-
try {
|
|
1510
|
-
const identity = await this.fileIdentity(lockPath);
|
|
1511
|
-
const owner = await this.readLockOwner(lockPath);
|
|
1512
|
-
if (owner === null || this.isProcessAlive(owner.pid)) {
|
|
1513
|
-
return;
|
|
1514
|
-
}
|
|
1515
|
-
const tombstone = join(this.locksRoot, `.${id}.lock.reap-${randomUUID()}`);
|
|
1516
|
-
await rename(lockPath, tombstone);
|
|
1517
|
-
const movedIdentity = await this.fileIdentity(tombstone);
|
|
1518
|
-
if (movedIdentity.dev !== identity.dev
|
|
1519
|
-
|| movedIdentity.ino !== identity.ino) {
|
|
1520
|
-
await rename(tombstone, lockPath).catch(() => undefined);
|
|
1521
|
-
return;
|
|
1522
|
-
}
|
|
1523
|
-
await this.hooks.afterLockTombstoneRename?.();
|
|
1524
|
-
await unlink(tombstone);
|
|
1525
|
-
await this.syncDirectory(this.locksRoot);
|
|
1526
|
-
}
|
|
1527
|
-
catch (error) {
|
|
1528
|
-
if (!isNodeError(error) || error.code !== "ENOENT") {
|
|
1529
|
-
throw error;
|
|
1530
|
-
}
|
|
1531
|
-
}
|
|
1532
|
-
};
|
|
1533
|
-
releaseLock = async (id, token) => {
|
|
1534
|
-
const lockPath = this.lockPath(id);
|
|
1535
|
-
try {
|
|
1536
|
-
const identity = await this.fileIdentity(lockPath);
|
|
1537
|
-
const current = await this.readLockOwner(lockPath);
|
|
1538
|
-
if (current?.pid === process.pid && current.token === token) {
|
|
1539
|
-
const tombstone = join(this.locksRoot, `.${id}.lock.release-${randomUUID()}`);
|
|
1540
|
-
await rename(lockPath, tombstone);
|
|
1541
|
-
const movedIdentity = await this.fileIdentity(tombstone);
|
|
1542
|
-
if (movedIdentity.dev === identity.dev
|
|
1543
|
-
&& movedIdentity.ino === identity.ino) {
|
|
1544
|
-
await unlink(tombstone);
|
|
1545
|
-
await this.syncDirectory(this.locksRoot);
|
|
999
|
+
const stats = await handle.stat({ bigint: true });
|
|
1000
|
+
if (!stats.isFile()) {
|
|
1001
|
+
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence is not a regular file: ${relativePath}`);
|
|
1546
1002
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1003
|
+
const openedIdentity = { dev: stats.dev, ino: stats.ino };
|
|
1004
|
+
const pathIdentity = await fileIdentity(path);
|
|
1005
|
+
if (!sameIdentity(openedIdentity, pathIdentity)) {
|
|
1006
|
+
throw new GenerationSessionStoreError("INVALID_EVIDENCE_PATH", `Evidence file identity changed: ${relativePath}`);
|
|
1549
1007
|
}
|
|
1008
|
+
await verifyDirectoryEvidence(rootEvidence.canonicalPath, directories);
|
|
1009
|
+
return await operation({ handle, published, segments });
|
|
1550
1010
|
}
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
if (!isNodeError(error) || error.code !== "ENOENT") {
|
|
1554
|
-
throw error;
|
|
1011
|
+
finally {
|
|
1012
|
+
await handle.close();
|
|
1555
1013
|
}
|
|
1556
|
-
}
|
|
1557
|
-
};
|
|
1558
|
-
readLockOwner = async (lockPath) => {
|
|
1559
|
-
const handle = await open(lockPath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
1560
|
-
try {
|
|
1561
|
-
const value = JSON.parse(await handle.readFile("utf8"));
|
|
1562
|
-
if (value === null
|
|
1563
|
-
|| typeof value !== "object"
|
|
1564
|
-
|| !Number.isSafeInteger(value.pid)
|
|
1565
|
-
|| value.pid <= 0
|
|
1566
|
-
|| typeof value.token !== "string"
|
|
1567
|
-
|| value.token.length === 0) {
|
|
1568
|
-
return null;
|
|
1569
|
-
}
|
|
1570
|
-
return value;
|
|
1571
|
-
}
|
|
1572
|
-
catch {
|
|
1573
|
-
return null;
|
|
1574
|
-
}
|
|
1575
|
-
finally {
|
|
1576
|
-
await handle.close();
|
|
1577
|
-
}
|
|
1578
|
-
};
|
|
1579
|
-
fileIdentity = async (path) => {
|
|
1580
|
-
const stats = await lstat(path, { bigint: true });
|
|
1581
|
-
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
1582
|
-
throw new GenerationSessionStoreError("IO_ERROR", `Generation lock path is not a real file: ${path}`);
|
|
1583
|
-
}
|
|
1584
|
-
return { dev: stats.dev, ino: stats.ino };
|
|
1014
|
+
});
|
|
1585
1015
|
};
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
}
|
|
1591
|
-
catch (error) {
|
|
1592
|
-
return !isNodeError(error) || error.code !== "ESRCH";
|
|
1593
|
-
}
|
|
1016
|
+
withLock = (id, operation) => this.lock.withLock(id, operation);
|
|
1017
|
+
syncDirectory = async (path) => {
|
|
1018
|
+
await syncDirectory(path);
|
|
1019
|
+
await this.hooks.afterDirectorySync?.(path);
|
|
1594
1020
|
};
|
|
1595
1021
|
assertPublishable(session) {
|
|
1596
1022
|
if (session.verification.status !== "passed"
|