@reticlehq/server 3.1.0 → 3.3.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/SKILL.md +30 -28
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +37 -5
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +38 -7
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +131 -19
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +3 -7
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +3 -23
- package/dist/command/cli/launch/cli-launch.js +29 -70
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
- package/dist/command/cli/ports/resolve/cli-port.js +50 -2
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +29 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +3 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +661 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +62 -0
- package/dist/features/harness/platform-config.js +97 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +80 -130
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +55 -10
- package/dist/language/flows/flow-replay-run.js +217 -58
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +29 -11
- package/dist/language/flows/flow-replay.js +221 -203
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +15 -10
- package/dist/language/flows/flows.js +17 -4
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +13 -9
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +21 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -4
- package/dist/memory/cloud/cloud-sync.js +11 -6
- package/dist/memory/cloud/harness-config.d.ts +12 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +35 -2
- package/dist/memory/cloud/sync-daemon.js +45 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +25 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +253 -141
- package/dist/memory/intent/intent-shard.d.ts +22 -23
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +71 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +91 -2
- package/dist/memory/journal/on-disk/retention.js +181 -11
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +17 -4
- package/dist/memory/journal/session-end.js +85 -7
- package/dist/memory/journal/session-journal.d.ts +122 -5
- package/dist/memory/journal/session-journal.js +489 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +72 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +20 -3
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +13 -0
- package/dist/portal/session/session-manager.js +42 -3
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +26 -10
- package/dist/portal/session/session.js +48 -49
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +23 -12
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +49 -8
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +74 -21
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +43 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +50 -3
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +82 -9
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +69 -15
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +23 -3
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +70 -30
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/telemetry/verification-of.js +31 -3
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +11 -9
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +144 -4
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/tools/query.mdx +10 -10
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +2 -2
- package/docs/what-is-recorded.md +62 -0
- package/package.json +5 -5
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The intent ledger on disk: one directory per subject, and a flow's name IS its subject.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* .reticle/intent/index.json every id, its subject and one line — derived
|
|
5
|
+
* .reticle/intent/<subject>/intent.json the full records for that subject
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The ONE place intents are stored. `IntentStore` is the domain API over it (declare, bind,
|
|
8
|
+
* discharge) and never writes a file of its own; `reticle_intent`'s record/get/index/subject read
|
|
9
|
+
* this directly. Two stores kept in two files is how an intent written through one action became
|
|
10
|
+
* invisible to the other, and an agent told its own intent does not exist concludes it does not.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Git-checked on purpose: a human sees in review when an intent is narrowed to match what was easy
|
|
13
|
+
* to prove. Writes are byte-stable, and a write that changes nothing writes nothing, because a ledger
|
|
14
|
+
* that churns on every run is one whose diffs nobody reads.
|
|
14
15
|
*
|
|
15
|
-
* ## Migration
|
|
16
|
+
* ## Migration happens on the first write, and it removes what it moved
|
|
16
17
|
*
|
|
17
|
-
* `.reticle/intent.json`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* is a
|
|
18
|
+
* The old single `.reticle/intent.json` (and any interim `intent/<subject>.json`) is read until then,
|
|
19
|
+
* so nothing is invisible before the move. The first write folds every record into its directory and
|
|
20
|
+
* deletes the old files, so the change shows up in one reviewable diff. A legacy file that does not
|
|
21
|
+
* PARSE is never deleted: a hand-merge leaves conflict markers, it reads as empty, and removing it
|
|
22
|
+
* would destroy the only copy of every intent inside.
|
|
21
23
|
*/
|
|
22
24
|
import { type Intent } from '@reticlehq/core/artifacts';
|
|
23
25
|
import type { FileSystemPort } from '../project/fs/fs-port.js';
|
|
24
26
|
import type { Clock } from '../../machine/clock.js';
|
|
25
27
|
import { IntentStatus, type IntentIndex, type IntentRecord } from './intent-shard.js';
|
|
26
|
-
/** What a caller supplies to write an intent
|
|
28
|
+
/** What a caller supplies to write an intent through `record`. Everything optional is optional. */
|
|
27
29
|
interface IntentInput {
|
|
28
30
|
id: string;
|
|
29
31
|
statement: string;
|
|
@@ -34,51 +36,35 @@ interface IntentInput {
|
|
|
34
36
|
surface?: Intent['surface'];
|
|
35
37
|
binding?: unknown;
|
|
36
38
|
}
|
|
39
|
+
/** The ledger-level facts a write may set beside the intent itself. */
|
|
40
|
+
export interface RecordFacts {
|
|
41
|
+
subject?: string | undefined;
|
|
42
|
+
status?: IntentStatus | undefined;
|
|
43
|
+
why?: string | undefined;
|
|
44
|
+
source?: string | undefined;
|
|
45
|
+
}
|
|
37
46
|
export declare class IntentShardStore {
|
|
38
47
|
#private;
|
|
39
48
|
constructor(fs: FileSystemPort, root: string, clock: Clock);
|
|
40
|
-
/**
|
|
41
|
-
* Every record: the shards, plus any legacy intent the shards have not absorbed.
|
|
42
|
-
*
|
|
43
|
-
* Sharded wins on a collision. An id present in both has been migrated and possibly edited since,
|
|
44
|
-
* and letting the flat copy overwrite that would silently revert the edit.
|
|
45
|
-
*/
|
|
46
|
-
/**
|
|
47
|
-
* Every record this ledger holds, from BOTH files, deduped by id.
|
|
48
|
-
*
|
|
49
|
-
* Public because the ledger is one logical thing kept in two places, and the tool that offers
|
|
50
|
-
* `list` has to be able to see all of it. It was private while `record` wrote to shards and `list`
|
|
51
|
-
* read the flat file, which meant an intent written through one action was invisible to the other
|
|
52
|
-
* — and an agent told its own recorded intent does not exist concludes the intent does not exist,
|
|
53
|
-
* not that there are two stores.
|
|
54
|
-
*/
|
|
49
|
+
/** Every record: the directories, plus any legacy intent not yet moved. Directories win. */
|
|
55
50
|
all(): Promise<IntentRecord[]>;
|
|
56
|
-
/**
|
|
57
|
-
* The cheap read: one line per intent, and the call an agent makes at the start of a session.
|
|
58
|
-
*
|
|
59
|
-
* Includes anything still only in the legacy flat file, so the very first call on an unmigrated
|
|
60
|
-
* project already answers completely — which is what makes the migration invisible rather than a
|
|
61
|
-
* step somebody has to remember to run.
|
|
62
|
-
*/
|
|
51
|
+
/** The cheap read: one line per intent. Derived from disk, never from the stored copy. */
|
|
63
52
|
index(): Promise<IntentIndex>;
|
|
64
53
|
/** Every record for one subject — the working read, once an agent knows what it is touching. */
|
|
65
54
|
subject(name: string): Promise<IntentRecord[]>;
|
|
66
55
|
/** One record by id, or null. */
|
|
67
56
|
get(id: string): Promise<IntentRecord | null>;
|
|
68
57
|
/**
|
|
69
|
-
*
|
|
58
|
+
* Create or change ONE intent atomically: read, transform, write, under the ledger's lock.
|
|
70
59
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
60
|
+
* `transform` returns the next intent, or undefined for "leave it as it is". A result equal to
|
|
61
|
+
* what is stored writes nothing. Returns the stored record, or undefined when nothing was there
|
|
62
|
+
* and nothing was created.
|
|
74
63
|
*/
|
|
64
|
+
upsert(id: string, transform: (existing: IntentRecord | undefined) => Intent | undefined, facts?: RecordFacts): Promise<IntentRecord | undefined>;
|
|
65
|
+
/** Write one intent through `reticle_intent { action: "record" }`, merging onto what is stored. */
|
|
75
66
|
record(input: IntentInput): Promise<IntentRecord>;
|
|
76
|
-
/**
|
|
77
|
-
* Fold the flat file into shards on disk, so the move is visible in a diff rather than implicit.
|
|
78
|
-
*
|
|
79
|
-
* Idempotent, and it does not delete `.reticle/intent.json`: an older build still reads it, and a
|
|
80
|
-
* migration that removes its own source cannot be checked afterwards.
|
|
81
|
-
*/
|
|
67
|
+
/** `reticle_intent { action: "migrate" }`: do now what the next write would do anyway. */
|
|
82
68
|
migrate(): Promise<{
|
|
83
69
|
migrated: number;
|
|
84
70
|
subjects: string[];
|
|
@@ -1,32 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The intent ledger on disk: one directory per subject, and a flow's name IS its subject.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* .reticle/intent/index.json every id, its subject and one line — derived
|
|
5
|
+
* .reticle/intent/<subject>/intent.json the full records for that subject
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* The ONE place intents are stored. `IntentStore` is the domain API over it (declare, bind,
|
|
8
|
+
* discharge) and never writes a file of its own; `reticle_intent`'s record/get/index/subject read
|
|
9
|
+
* this directly. Two stores kept in two files is how an intent written through one action became
|
|
10
|
+
* invisible to the other, and an agent told its own intent does not exist concludes it does not.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Git-checked on purpose: a human sees in review when an intent is narrowed to match what was easy
|
|
13
|
+
* to prove. Writes are byte-stable, and a write that changes nothing writes nothing, because a ledger
|
|
14
|
+
* that churns on every run is one whose diffs nobody reads.
|
|
14
15
|
*
|
|
15
|
-
* ## Migration
|
|
16
|
+
* ## Migration happens on the first write, and it removes what it moved
|
|
16
17
|
*
|
|
17
|
-
* `.reticle/intent.json`
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* is a
|
|
18
|
+
* The old single `.reticle/intent.json` (and any interim `intent/<subject>.json`) is read until then,
|
|
19
|
+
* so nothing is invisible before the move. The first write folds every record into its directory and
|
|
20
|
+
* deletes the old files, so the change shows up in one reviewable diff. A legacy file that does not
|
|
21
|
+
* PARSE is never deleted: a hand-merge leaves conflict markers, it reads as empty, and removing it
|
|
22
|
+
* would destroy the only copy of every intent inside.
|
|
21
23
|
*/
|
|
22
|
-
import {
|
|
24
|
+
import { IntentFileSchema, IntentState, amendIntent, isActionLabel, } from '@reticlehq/core/artifacts';
|
|
25
|
+
import { IntentDir, ReticleDir } from '@reticlehq/core';
|
|
23
26
|
import { reticleDirPaths } from '../project/dir/reticle-dir.js';
|
|
24
27
|
import { withFileLock } from '../project/file-lock.js';
|
|
25
28
|
import { subjectFor } from './intent-subject.js';
|
|
26
|
-
import { emptyShard, indexFrom, IntentShardSchema, IntentStatus, recordFromIntent, serialise, shardsFrom,
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
29
|
+
import { emptyShard, indexFrom, IntentShardSchema, IntentStatus, recordFromIntent, serialise, shardsFrom, statusFromState, } from './intent-shard.js';
|
|
30
|
+
import { writeFileAtomic } from '../project/fs/write-atomic.js';
|
|
31
|
+
const JSON_SUFFIX = '.json';
|
|
32
|
+
/** Beside an unparseable shard: its bytes, kept before it is rewritten. See `#shardForWrite`. */
|
|
33
|
+
const UNREADABLE_SUFFIX = '.unreadable-';
|
|
34
|
+
/** The intent fields a record is compared on — `updatedAt` alone changing is not a change. */
|
|
35
|
+
const comparable = (record) => serialise({ ...record, updatedAt: 0 });
|
|
30
36
|
export class IntentShardStore {
|
|
31
37
|
#fs;
|
|
32
38
|
#root;
|
|
@@ -37,178 +43,284 @@ export class IntentShardStore {
|
|
|
37
43
|
this.#clock = clock;
|
|
38
44
|
}
|
|
39
45
|
#dir() {
|
|
40
|
-
return `${reticleDirPaths(this.#root).root}/${
|
|
46
|
+
return `${reticleDirPaths(this.#root).root}/${ReticleDir.INTENT_SUBDIR}`;
|
|
41
47
|
}
|
|
42
48
|
#indexPath() {
|
|
43
|
-
return `${this.#dir()}/${INDEX_FILE}`;
|
|
49
|
+
return `${this.#dir()}/${IntentDir.INDEX_FILE}`;
|
|
44
50
|
}
|
|
45
51
|
#shardPath(subject) {
|
|
46
|
-
return `${this.#dir()}/${subject}
|
|
52
|
+
return `${this.#dir()}/${subject}/${IntentDir.SHARD_FILE}`;
|
|
47
53
|
}
|
|
48
|
-
/**
|
|
49
|
-
async #
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
/** Parse one legacy file, remembering whether it parsed — only a parsed file may be deleted. */
|
|
55
|
+
async #readLegacy(pathOf, toRecords) {
|
|
56
|
+
let path = '';
|
|
57
|
+
let text;
|
|
58
|
+
try {
|
|
59
|
+
// Resolved INSIDE the try: with no known project root there is no path, and that must read as
|
|
60
|
+
// "no legacy file" — the old store failed soft here, and a caller only asking what is still
|
|
61
|
+
// open must not be taken down by it.
|
|
62
|
+
path = pathOf();
|
|
63
|
+
text = await this.#fs.readFile(path);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return { path, records: [], parsed: true };
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return { path, records: toRecords(JSON.parse(text)), parsed: true };
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return { path, records: [], parsed: false };
|
|
73
|
+
}
|
|
52
74
|
}
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
/** The old single file, and any interim `intent/<subject>.json` from before directories. */
|
|
76
|
+
async #legacySources() {
|
|
77
|
+
const flat = await this.#readLegacy(() => reticleDirPaths(this.#root).intent, (raw) => Object.values(IntentFileSchema.parse(raw).intents).map(recordFromIntent));
|
|
78
|
+
const interim = await Promise.all((await this.#entries())
|
|
79
|
+
.filter((e) => e.endsWith(JSON_SUFFIX) && IntentDir.INDEX_FILE !== e)
|
|
80
|
+
.map((e) => this.#readLegacy(() => `${this.#dir()}/${e}`, (raw) => Object.values(IntentShardSchema.parse(raw).intents))));
|
|
81
|
+
return [flat, ...interim].filter((l) => l.records.length > 0 || !l.parsed);
|
|
82
|
+
}
|
|
83
|
+
async #entries() {
|
|
84
|
+
try {
|
|
85
|
+
return await this.#fs.readdir(this.#dir());
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
55
90
|
}
|
|
56
91
|
/**
|
|
57
92
|
* Which subjects exist, read from the DIRECTORY rather than the index.
|
|
58
93
|
*
|
|
59
|
-
* The index
|
|
60
|
-
*
|
|
61
|
-
* the
|
|
62
|
-
* mention it yet. Listing the directory also makes the index self-healing: delete it and the next
|
|
63
|
-
* read rebuilds it from what is actually there.
|
|
94
|
+
* The index is derived from the shards, so discovering shards through it would be circular: a new
|
|
95
|
+
* subject was once invisible to the very write that created it. Listing the directory also makes
|
|
96
|
+
* the index self-healing — delete it and the next write rebuilds it from what is actually there.
|
|
64
97
|
*/
|
|
65
98
|
async #subjectsOnDisk() {
|
|
99
|
+
const subjects = [];
|
|
100
|
+
for (const entry of await this.#entries()) {
|
|
101
|
+
if (entry.endsWith(JSON_SUFFIX))
|
|
102
|
+
continue;
|
|
103
|
+
if (await this.#fs.exists(this.#shardPath(entry)))
|
|
104
|
+
subjects.push(entry);
|
|
105
|
+
}
|
|
106
|
+
return subjects;
|
|
107
|
+
}
|
|
108
|
+
async #readShard(subject) {
|
|
66
109
|
try {
|
|
67
|
-
|
|
68
|
-
return entries
|
|
69
|
-
.filter((e) => e.endsWith(SHARD_SUFFIX) && INDEX_FILE !== e)
|
|
70
|
-
.map((e) => e.slice(0, -SHARD_SUFFIX.length));
|
|
110
|
+
return IntentShardSchema.parse(JSON.parse(await this.#fs.readFile(this.#shardPath(subject))));
|
|
71
111
|
}
|
|
72
112
|
catch {
|
|
73
|
-
|
|
74
|
-
return [];
|
|
113
|
+
return emptyShard(subject);
|
|
75
114
|
}
|
|
76
115
|
}
|
|
77
116
|
/**
|
|
78
|
-
*
|
|
117
|
+
* A shard about to be rewritten, read so that nothing in it can be lost.
|
|
79
118
|
*
|
|
80
|
-
*
|
|
81
|
-
* and
|
|
119
|
+
* `#readShard` answers "what is recorded here" and treats an unparseable shard as empty, which is
|
|
120
|
+
* right for reading and wrong for rewriting: the next write put "empty plus one record" over a file
|
|
121
|
+
* that held every other intent for that subject (#994). An unreadable shard's bytes are copied
|
|
122
|
+
* beside it, untouched, before it is replaced, the same rule migration keeps for a legacy file it
|
|
123
|
+
* cannot parse. A missing shard is simply empty.
|
|
82
124
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
async #shardForWrite(subject) {
|
|
126
|
+
const path = this.#shardPath(subject);
|
|
127
|
+
let text;
|
|
128
|
+
try {
|
|
129
|
+
text = await this.#fs.readFile(path);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return emptyShard(subject);
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
return IntentShardSchema.parse(JSON.parse(text));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
await writeFileAtomic(this.#fs, `${path}${UNREADABLE_SUFFIX}${String(this.#clock.now())}`, text);
|
|
139
|
+
return emptyShard(subject);
|
|
140
|
+
}
|
|
94
141
|
}
|
|
95
|
-
async #
|
|
142
|
+
async #storedRecords() {
|
|
96
143
|
const shards = await Promise.all((await this.#subjectsOnDisk()).map((s) => this.#readShard(s)));
|
|
144
|
+
return shards.flatMap((s) => Object.values(s.intents));
|
|
145
|
+
}
|
|
146
|
+
/** Every record: the directories, plus any legacy intent not yet moved. Directories win. */
|
|
147
|
+
async all() {
|
|
97
148
|
const merged = new Map();
|
|
98
|
-
for (const
|
|
99
|
-
|
|
100
|
-
for (const shard of shards) {
|
|
101
|
-
for (const record of Object.values(shard.intents))
|
|
149
|
+
for (const legacy of await this.#legacySources()) {
|
|
150
|
+
for (const record of legacy.records)
|
|
102
151
|
merged.set(record.id, record);
|
|
103
152
|
}
|
|
104
|
-
|
|
153
|
+
for (const record of await this.#storedRecords())
|
|
154
|
+
merged.set(record.id, record);
|
|
155
|
+
return [...merged.values()].sort((a, b) => a.declaredAt - b.declaredAt || a.id.localeCompare(b.id));
|
|
105
156
|
}
|
|
106
|
-
/**
|
|
107
|
-
* The cheap read: one line per intent, and the call an agent makes at the start of a session.
|
|
108
|
-
*
|
|
109
|
-
* Includes anything still only in the legacy flat file, so the very first call on an unmigrated
|
|
110
|
-
* project already answers completely — which is what makes the migration invisible rather than a
|
|
111
|
-
* step somebody has to remember to run.
|
|
112
|
-
*/
|
|
157
|
+
/** The cheap read: one line per intent. Derived from disk, never from the stored copy. */
|
|
113
158
|
async index() {
|
|
114
|
-
|
|
115
|
-
// readers outside this process; inside it, trusting it would let a stale one hide a real record.
|
|
116
|
-
return indexFrom(shardsFrom(await this.#all()));
|
|
117
|
-
}
|
|
118
|
-
/** Records already in shards, ignoring the legacy file — what migration diffs against. */
|
|
119
|
-
async #storedRecords() {
|
|
120
|
-
const shards = await Promise.all((await this.#subjectsOnDisk()).map((s) => this.#readShard(s)));
|
|
121
|
-
return shards.flatMap((s) => Object.values(s.intents));
|
|
159
|
+
return indexFrom(shardsFrom(await this.all()));
|
|
122
160
|
}
|
|
123
161
|
/** Every record for one subject — the working read, once an agent knows what it is touching. */
|
|
124
162
|
async subject(name) {
|
|
125
|
-
|
|
126
|
-
return all.filter((r) => name === r.subject);
|
|
163
|
+
return (await this.all()).filter((r) => name === r.subject);
|
|
127
164
|
}
|
|
128
165
|
/** One record by id, or null. */
|
|
129
166
|
async get(id) {
|
|
130
|
-
|
|
131
|
-
return all.find((r) => id === r.id) ?? null;
|
|
167
|
+
return (await this.all()).find((r) => id === r.id) ?? null;
|
|
132
168
|
}
|
|
133
169
|
/**
|
|
134
|
-
*
|
|
170
|
+
* Create or change ONE intent atomically: read, transform, write, under the ledger's lock.
|
|
135
171
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
172
|
+
* `transform` returns the next intent, or undefined for "leave it as it is". A result equal to
|
|
173
|
+
* what is stored writes nothing. Returns the stored record, or undefined when nothing was there
|
|
174
|
+
* and nothing was created.
|
|
139
175
|
*/
|
|
140
|
-
async
|
|
176
|
+
async upsert(id, transform, facts = {}) {
|
|
141
177
|
return withFileLock(this.#indexPath(), async () => {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
subject,
|
|
154
|
-
status: IntentStatus.PROPOSED,
|
|
155
|
-
}),
|
|
156
|
-
statement: input.statement,
|
|
157
|
-
subject,
|
|
158
|
-
status: input.status ?? existing?.status ?? IntentStatus.PROPOSED,
|
|
159
|
-
updatedAt: now,
|
|
160
|
-
...(input.why === undefined ? {} : { why: input.why }),
|
|
161
|
-
...(input.source === undefined ? {} : { source: input.source }),
|
|
162
|
-
...(input.surface === undefined ? {} : { surface: input.surface }),
|
|
163
|
-
...(input.binding === undefined ? {} : { binding: input.binding }),
|
|
164
|
-
};
|
|
165
|
-
await this.#write(next, existing?.subject);
|
|
166
|
-
return next;
|
|
178
|
+
await this.#migrateLegacy();
|
|
179
|
+
await this.#retireLabels();
|
|
180
|
+
const existing = (await this.#storedRecords()).find((r) => id === r.id);
|
|
181
|
+
const next = transform(existing);
|
|
182
|
+
if (next === undefined)
|
|
183
|
+
return existing;
|
|
184
|
+
const record = this.#recordFrom(amendIntent(existing, next), existing, facts);
|
|
185
|
+
if (existing !== undefined && comparable(existing) === comparable(record))
|
|
186
|
+
return existing;
|
|
187
|
+
await this.#write(record, existing?.subject);
|
|
188
|
+
return record;
|
|
167
189
|
});
|
|
168
190
|
}
|
|
169
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* The ledger-level fields of a record: where it lives and how settled it is.
|
|
193
|
+
*
|
|
194
|
+
* A flow's name IS its subject, so an intent a flow claims moves into that flow's directory.
|
|
195
|
+
* Otherwise an explicit subject wins, then the one it already had, then inference.
|
|
196
|
+
*/
|
|
197
|
+
#recordFrom(next, existing, facts) {
|
|
198
|
+
const flow = next.surface?.flow;
|
|
199
|
+
const subject = flow !== undefined
|
|
200
|
+
? subjectFor({ surface: { flow } })
|
|
201
|
+
: (facts.subject ??
|
|
202
|
+
existing?.subject ??
|
|
203
|
+
subjectFor({ surface: next.surface, binding: next.binding }));
|
|
204
|
+
const why = facts.why ?? existing?.why;
|
|
205
|
+
const source = facts.source ?? existing?.source;
|
|
206
|
+
return {
|
|
207
|
+
...existing,
|
|
208
|
+
...next,
|
|
209
|
+
subject,
|
|
210
|
+
status: this.#statusOf(next, existing, facts.status),
|
|
211
|
+
updatedAt: this.#clock.now(),
|
|
212
|
+
...(why === undefined ? {} : { why }),
|
|
213
|
+
...(source === undefined ? {} : { source }),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* How settled the record is. A verdict proving it makes it proved; losing that proof (the words
|
|
218
|
+
* changed, a different check was bound) steps it back rather than leaving a stale "proved".
|
|
219
|
+
*/
|
|
220
|
+
#statusOf(next, existing, asked) {
|
|
221
|
+
if (asked !== undefined)
|
|
222
|
+
return asked;
|
|
223
|
+
if (IntentState.PROVED === next.state)
|
|
224
|
+
return IntentStatus.PROVED;
|
|
225
|
+
if (IntentStatus.PROVED === existing?.status)
|
|
226
|
+
return statusFromState(next.state);
|
|
227
|
+
return existing?.status ?? IntentStatus.PROPOSED;
|
|
228
|
+
}
|
|
229
|
+
/** Write one intent through `reticle_intent { action: "record" }`, merging onto what is stored. */
|
|
230
|
+
async record(input) {
|
|
231
|
+
const now = this.#clock.now();
|
|
232
|
+
const stored = await this.upsert(input.id, (existing) => ({
|
|
233
|
+
...(existing ?? { id: input.id, state: IntentState.DECLARED, declaredAt: now }),
|
|
234
|
+
statement: input.statement,
|
|
235
|
+
...(input.surface === undefined ? {} : { surface: input.surface }),
|
|
236
|
+
...(input.binding === undefined ? {} : { binding: input.binding }),
|
|
237
|
+
}), { subject: input.subject, status: input.status, why: input.why, source: input.source });
|
|
238
|
+
// upsert only returns undefined when the transform declines, and this one never does.
|
|
239
|
+
return stored;
|
|
240
|
+
}
|
|
241
|
+
/** Persist one record, rewriting its shard and the index, and removing it from its old shard. */
|
|
170
242
|
async #write(record, previousSubject) {
|
|
171
|
-
await this.#fs.mkdir(this.#dir());
|
|
172
243
|
if (previousSubject !== undefined && record.subject !== previousSubject) {
|
|
173
|
-
const old = await this.#
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
await this.#fs.writeFile(this.#shardPath(previousSubject), serialise({ ...old, intents: rest }));
|
|
177
|
-
}
|
|
244
|
+
const old = await this.#shardForWrite(previousSubject);
|
|
245
|
+
const { [record.id]: _moved, ...rest } = old.intents;
|
|
246
|
+
await this.#putShard({ ...old, intents: rest });
|
|
178
247
|
}
|
|
179
|
-
const shard = await this.#
|
|
248
|
+
const shard = await this.#shardForWrite(record.subject);
|
|
180
249
|
shard.intents[record.id] = record;
|
|
181
|
-
await this.#
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
250
|
+
await this.#putShard(shard);
|
|
251
|
+
await this.#writeIndex();
|
|
252
|
+
}
|
|
253
|
+
/** Write a shard, or remove it when it no longer holds anything. */
|
|
254
|
+
async #putShard(shard) {
|
|
255
|
+
const path = this.#shardPath(shard.subject);
|
|
256
|
+
if (0 === Object.keys(shard.intents).length) {
|
|
257
|
+
await this.#fs.rm(path).catch(() => undefined);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Checked against the schema the reader applies, BEFORE it reaches the disk: a record the
|
|
261
|
+
// reader would refuse makes the whole shard unreadable, and every other intent in it with it.
|
|
262
|
+
// Refused here, loudly, where the one bad write is still the caller's to fix (#1020).
|
|
263
|
+
const valid = IntentShardSchema.parse(shard);
|
|
264
|
+
await this.#fs.mkdir(`${this.#dir()}/${shard.subject}`);
|
|
265
|
+
await writeFileAtomic(this.#fs, path, serialise(valid));
|
|
266
|
+
}
|
|
267
|
+
async #writeIndex() {
|
|
268
|
+
await this.#fs.mkdir(this.#dir());
|
|
269
|
+
const index = indexFrom(shardsFrom(await this.#storedRecords()));
|
|
270
|
+
await writeFileAtomic(this.#fs, this.#indexPath(), serialise(index));
|
|
186
271
|
}
|
|
187
272
|
/**
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* Idempotent, and it does not delete `.reticle/intent.json`: an older build still reads it, and a
|
|
191
|
-
* migration that removes its own source cannot be checked afterwards.
|
|
273
|
+
* Move every legacy record into its directory, then remove the legacy files that parsed.
|
|
274
|
+
* Called under the lock by every write, so the first write after an upgrade is the migration.
|
|
192
275
|
*/
|
|
276
|
+
async #migrateLegacy() {
|
|
277
|
+
const sources = await this.#legacySources();
|
|
278
|
+
if (0 === sources.length)
|
|
279
|
+
return { migrated: [] };
|
|
280
|
+
const known = new Set((await this.#storedRecords()).map((r) => r.id));
|
|
281
|
+
const incoming = sources.flatMap((s) => s.records).filter((r) => !known.has(r.id));
|
|
282
|
+
for (const shard of shardsFrom(incoming)) {
|
|
283
|
+
const current = await this.#shardForWrite(shard.subject);
|
|
284
|
+
await this.#putShard({ ...current, intents: { ...shard.intents, ...current.intents } });
|
|
285
|
+
}
|
|
286
|
+
for (const source of sources) {
|
|
287
|
+
if (source.parsed)
|
|
288
|
+
await this.#fs.rm(source.path).catch(() => undefined);
|
|
289
|
+
}
|
|
290
|
+
await this.#writeIndex();
|
|
291
|
+
return { migrated: incoming };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Mark every stored step label stale: "click button \"Cancel\"" describes what was DONE, not a
|
|
295
|
+
* rule. Kept rather than deleted, because removing a record loses the fact it was ever written.
|
|
296
|
+
* Runs with every write, and writes nothing once there is nothing left to retire.
|
|
297
|
+
*/
|
|
298
|
+
async #retireLabels() {
|
|
299
|
+
const labels = (await this.#storedRecords()).filter((r) => IntentStatus.STALE !== r.status && isActionLabel(r.statement));
|
|
300
|
+
if (0 === labels.length)
|
|
301
|
+
return 0;
|
|
302
|
+
for (const subject of new Set(labels.map((r) => r.subject))) {
|
|
303
|
+
const shard = await this.#shardForWrite(subject);
|
|
304
|
+
for (const record of labels.filter((r) => subject === r.subject)) {
|
|
305
|
+
shard.intents[record.id] = {
|
|
306
|
+
...record,
|
|
307
|
+
status: IntentStatus.STALE,
|
|
308
|
+
updatedAt: this.#clock.now(),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
await this.#putShard(shard);
|
|
312
|
+
}
|
|
313
|
+
await this.#writeIndex();
|
|
314
|
+
return labels.length;
|
|
315
|
+
}
|
|
316
|
+
/** `reticle_intent { action: "migrate" }`: do now what the next write would do anyway. */
|
|
193
317
|
async migrate() {
|
|
194
318
|
return withFileLock(this.#indexPath(), async () => {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
return { migrated: 0, subjects: [] };
|
|
198
|
-
const stored = await this.#storedRecords();
|
|
199
|
-
const known = new Set(stored.map((r) => r.id));
|
|
200
|
-
const incoming = [...legacy.values()].filter((r) => !known.has(r.id));
|
|
201
|
-
if (0 === incoming.length)
|
|
202
|
-
return { migrated: 0, subjects: [] };
|
|
203
|
-
const shards = shardsFrom([...stored, ...incoming]);
|
|
204
|
-
await this.#fs.mkdir(this.#dir());
|
|
205
|
-
for (const shard of shards) {
|
|
206
|
-
await this.#fs.writeFile(this.#shardPath(shard.subject), serialise(shard));
|
|
207
|
-
}
|
|
208
|
-
await this.#fs.writeFile(this.#indexPath(), serialise(indexFrom(shards)));
|
|
319
|
+
const { migrated } = await this.#migrateLegacy();
|
|
320
|
+
await this.#retireLabels();
|
|
209
321
|
return {
|
|
210
|
-
migrated:
|
|
211
|
-
subjects: [...new Set(
|
|
322
|
+
migrated: migrated.length,
|
|
323
|
+
subjects: [...new Set(migrated.map((r) => r.subject))].sort(),
|
|
212
324
|
};
|
|
213
325
|
});
|
|
214
326
|
}
|