@danceiny/gotry 0.0.1-rc.9 → 0.2.0-rc.27
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 +172 -219
- package/README.zh-CN.md +222 -0
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +1744 -0
- package/bin/gotry-inner.js +733 -83
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +49 -0
- package/bin/gotry.js +1 -1
- package/client/client.js +203 -0
- package/cordis.gotry-patch.yml +80 -56
- package/data/airline-airports.json +39 -0
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +101 -146
- package/dist/capabilities/anything.js +135 -94
- package/dist/capabilities/artifacts.js +370 -0
- package/dist/capabilities/channel-health.js +131 -0
- package/dist/capabilities/channel-registry.js +256 -0
- package/dist/capabilities/doctor.js +546 -0
- package/dist/capabilities/effect.js +394 -0
- package/dist/capabilities/external-event.js +149 -0
- package/dist/capabilities/fact-log.js +55 -0
- package/dist/capabilities/flyai-config.js +337 -0
- package/dist/capabilities/flyai.js +947 -110
- package/dist/capabilities/fx-contract.js +293 -0
- package/dist/capabilities/geo-atlas.js +246 -0
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +260 -123
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/itinerary-artifact.js +190 -0
- package/dist/capabilities/itinerary-deck-artifact.js +194 -0
- package/dist/capabilities/itinerary-deck-export.js +339 -0
- package/dist/capabilities/lavish-local.js +924 -0
- package/dist/capabilities/model-override.js +38 -0
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +191 -0
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +120 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +222 -0
- package/dist/capabilities/session/adapters/dida-portal.js +274 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +342 -0
- package/dist/capabilities/session/benchmark.js +189 -0
- package/dist/capabilities/session/bridge-ledger.js +224 -0
- package/dist/capabilities/session/extension-bridge.js +636 -0
- package/dist/capabilities/session/extension-channel.js +109 -0
- package/dist/capabilities/session/extension-distribution.js +230 -0
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +114 -0
- package/dist/capabilities/session/health-watch.js +215 -0
- package/dist/capabilities/session/read-guard.js +75 -24
- package/dist/capabilities/session/static-flight-golden.js +150 -0
- package/dist/capabilities/session/transport.js +97 -83
- package/dist/capabilities/session/wizard.js +117 -0
- package/dist/capabilities/session-consent.js +178 -0
- package/dist/capabilities/session-login.js +170 -0
- package/dist/capabilities/session-search.js +635 -44
- package/dist/capabilities/spawn-bounded.js +203 -0
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +140 -0
- package/dist/capabilities/weather.js +243 -138
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +103 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +242 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +185 -0
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-error-tests.js +354 -0
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +50 -50
- package/dist/scripts/anything-tests.js +116 -60
- package/dist/scripts/artifact-client-contract-tests.js +220 -0
- package/dist/scripts/artifacts-capability-tests.js +727 -0
- package/dist/scripts/async-collect.js +50 -11
- package/dist/scripts/benchmark-environment-bridge-e2e.js +741 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +1452 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +126 -0
- package/dist/scripts/booking-copilot-availability-policy-tests.js +265 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +66 -0
- package/dist/scripts/booking-copilot-cleanup-proof-helper.js +76 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +144 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +424 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +2839 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +309 -0
- package/dist/scripts/booking-copilot-dsh-readiness-proof.js +238 -0
- package/dist/scripts/booking-copilot-dsh-warmer-proof.js +123 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +156 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +70 -0
- package/dist/scripts/booking-copilot-intent-proof-tests.js +422 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +184 -0
- package/dist/scripts/booking-copilot-planner-live-probe.js +95 -0
- package/dist/scripts/booking-copilot-proof-runtime.js +42 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +210 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +2349 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +784 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +94 -0
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +125 -0
- package/dist/scripts/booking-surface-contract-proof-tests.js +212 -0
- package/dist/scripts/bootstrap-tests.js +1479 -0
- package/dist/scripts/bridge-events-tests.js +239 -0
- package/dist/scripts/bridge-ledger-tests.js +428 -0
- package/dist/scripts/build-changelog.js +238 -0
- package/dist/scripts/build-metrics-report.js +331 -0
- package/dist/scripts/changelog-tests.js +118 -0
- package/dist/scripts/channel-probe-tests.js +103 -0
- package/dist/scripts/channel-probe.js +169 -0
- package/dist/scripts/channel-registry-tests.js +208 -0
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-runner-stop-tests.js +311 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +564 -0
- package/dist/scripts/dsh-artifact-e2e.js +279 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +675 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +182 -0
- package/dist/scripts/dsh-runtime-closure.js +162 -0
- package/dist/scripts/dsh-session-v3-migration-proof.js +109 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +150 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +539 -0
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +174 -0
- package/dist/scripts/evaluation-contract-tests.js +268 -0
- package/dist/scripts/extension-distribution-cli.js +39 -0
- package/dist/scripts/extension-distribution-tests.js +443 -0
- package/dist/scripts/extension-tests.js +988 -0
- package/dist/scripts/external-event-tests.js +350 -0
- package/dist/scripts/fact-gate-tests.js +869 -0
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-setup-tests.js +228 -0
- package/dist/scripts/flyai-setup-tool-tests.js +54 -0
- package/dist/scripts/flyai-tests.js +692 -0
- package/dist/scripts/flyai-tool-registration-contract-tests.js +91 -0
- package/dist/scripts/fx-contract-tests.js +201 -0
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/geo-atlas-tests.js +283 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +206 -0
- package/dist/scripts/gotry-try-demo-tests.js +79 -0
- package/dist/scripts/gotry-try-demo.js +49 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +159 -0
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +121 -69
- package/dist/scripts/health-watch-cli.js +73 -0
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +41 -0
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +569 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/issue436-persisted-routing-e2e.js +247 -0
- package/dist/scripts/itinerary-artifact-tests.js +368 -0
- package/dist/scripts/itinerary-deck-artifact-tests.js +416 -0
- package/dist/scripts/itinerary-deck-export-tests.js +552 -0
- package/dist/scripts/itinerary-deck-tests.js +435 -0
- package/dist/scripts/itinerary-html-tests.js +426 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/lavish-local-tests.js +1226 -0
- package/dist/scripts/lavish-product-tools-tests.js +537 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +382 -246
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/managed-dsh-cleanup-diagnostic-proof.js +184 -0
- package/dist/scripts/managed-dsh-close-failure-proof.js +172 -0
- package/dist/scripts/managed-dsh-run-port-proof.js +153 -0
- package/dist/scripts/managed-dsh-terminal-proof.js +252 -0
- package/dist/scripts/map-tools-vendor-package-proof.js +365 -0
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +631 -0
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +151 -0
- package/dist/scripts/model-override-e2e.js +137 -0
- package/dist/scripts/nightly-evidence-tests.js +161 -0
- package/dist/scripts/nightly-evidence.js +286 -0
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/observability-tests.js +381 -0
- package/dist/scripts/onboarding-tests.js +161 -0
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +49 -0
- package/dist/scripts/pnpm-dsh-closure-proof.js +17 -0
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +340 -0
- package/dist/scripts/price-drift-watch.js +447 -0
- package/dist/scripts/probe-poi-tests.js +53 -45
- package/dist/scripts/product-metrics.js +483 -0
- package/dist/scripts/publish-preverify.js +110 -0
- package/dist/scripts/realtime-pricing-tests.js +76 -0
- package/dist/scripts/recall-tests.js +513 -0
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +18 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +20 -20
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +180 -0
- package/dist/scripts/session-dida-live-e2e.js +199 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-link-tests.js +478 -0
- package/dist/scripts/session-login.js +27 -46
- package/dist/scripts/session-tests.js +796 -96
- package/dist/scripts/session-zones-tests.js +358 -0
- package/dist/scripts/sf-live-benchmark.js +367 -0
- package/dist/scripts/sf-live-challenge-stop-tests.js +453 -0
- package/dist/scripts/sf-live-cli-tests.js +92 -0
- package/dist/scripts/sf-live-options.js +46 -0
- package/dist/scripts/sf-manifest-tests.js +30 -0
- package/dist/scripts/sf-manifest.js +33 -0
- package/dist/scripts/sf-soft-score-tests.js +67 -0
- package/dist/scripts/sf-summary-tests.js +324 -0
- package/dist/scripts/sf-summary.js +525 -0
- package/dist/scripts/share-tests.js +432 -0
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +28 -11
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +21 -0
- package/dist/scripts/smoke.js +690 -246
- package/dist/scripts/spawn-bounded-abort-tests.js +241 -0
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +550 -199
- package/dist/scripts/state-repair-cli.js +66 -0
- package/dist/scripts/state-repair-tests.js +140 -0
- package/dist/scripts/static-golden-tests.js +165 -0
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +169 -0
- package/dist/scripts/turn-handoff-collect.js +161 -0
- package/dist/scripts/turn-policy-tests.js +55 -0
- package/dist/scripts/typed-contract-canary.js +176 -0
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +62 -0
- package/dist/scripts/weather-tests.js +276 -41
- package/dist/scripts/wish-channel-gate-tests.js +54 -0
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +41 -0
- package/dist/src/artifact-gate.js +829 -0
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +92 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +551 -0
- package/dist/src/benchmark-agent-conformance.js +578 -0
- package/dist/src/benchmark-environment-bridge.js +515 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +166 -0
- package/dist/src/benchmark-tool-isolation.js +151 -0
- package/dist/src/bookable-facts.js +533 -0
- package/dist/src/booking-saga.js +97 -0
- package/dist/src/booking-surface/availability-policy.js +697 -0
- package/dist/src/booking-surface/booking-intent.js +43 -0
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/canonical-schema.js +116 -0
- package/dist/src/booking-surface/contracts.js +75 -0
- package/dist/src/booking-surface/dsh-planner.js +1854 -0
- package/dist/src/booking-surface/dsh-plugin.js +378 -0
- package/dist/src/booking-surface/dsh-worker.js +98 -0
- package/dist/src/booking-surface/error-codes.js +231 -0
- package/dist/src/booking-surface/index.js +9 -0
- package/dist/src/booking-surface/managed-dsh-cleanup-diagnostic.js +97 -0
- package/dist/src/booking-surface/managed-dsh-run-port.js +244 -0
- package/dist/src/booking-surface/profile.js +24 -0
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +1968 -0
- package/dist/src/booking-surface/server.js +674 -0
- package/dist/src/booking-surface/startup.js +168 -0
- package/dist/src/booking-surface/validation.js +261 -0
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +97 -111
- package/dist/src/engine.js +276 -193
- package/dist/src/evaluation-cadence.js +97 -0
- package/dist/src/evaluation-contracts.js +283 -0
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/flyai-setup-tool.js +70 -0
- package/dist/src/gotry-backend.js +125 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +88 -0
- package/dist/src/index.js +2227 -882
- package/dist/src/itinerary-deck.js +139 -0
- package/dist/src/itinerary-doc-shared.js +661 -0
- package/dist/src/itinerary-html.js +131 -0
- package/dist/src/journey.js +155 -108
- package/dist/src/lavish-tools.js +400 -0
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +319 -160
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +103 -0
- package/dist/src/recall/card.js +74 -0
- package/dist/src/recall/evaluator.js +140 -0
- package/dist/src/recall/integration.js +17 -0
- package/dist/src/recall/tick.js +166 -0
- package/dist/src/session-link/plan-it-action.js +48 -0
- package/dist/src/session-link/session-link.js +248 -0
- package/dist/src/session-zones.js +507 -0
- package/dist/src/share/adapters.js +51 -0
- package/dist/src/share/share-consent.js +76 -0
- package/dist/src/share/share-deck.js +58 -0
- package/dist/src/share/share-token.js +146 -0
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +435 -516
- package/dist/src/state-repair.js +299 -0
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +287 -0
- package/dist/src/turn-policy.js +118 -0
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +535 -359
- package/dist/src/wish-pool.js +42 -12
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +284 -0
- package/extension/README.md +21 -0
- package/extension/README.zh-CN.md +21 -0
- package/extension/background.js +463 -0
- package/extension/content-bridge.js +270 -0
- package/extension/content-main.js +111 -0
- package/extension/manifest.json +60 -0
- package/package.json +407 -14
- package/schemas/booking.intent.schema.json +193 -0
- package/schemas/booking.surface.schema.json +2622 -0
- package/ts/capabilities/agent-reach-deep.ts +114 -0
- package/ts/capabilities/agent-reach.ts +198 -0
- package/ts/capabilities/anything.ts +255 -0
- package/ts/capabilities/artifacts.ts +428 -0
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/flyai-config.ts +388 -0
- package/ts/capabilities/flyai.ts +1274 -0
- package/ts/capabilities/hbcli.ts +253 -71
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/itinerary-artifact.ts +214 -0
- package/ts/capabilities/itinerary-deck-artifact.ts +218 -0
- package/ts/capabilities/itinerary-deck-export.ts +378 -0
- package/ts/capabilities/opensky.ts +121 -0
- package/ts/capabilities/session/action-cache.ts +148 -0
- package/ts/capabilities/session/adapters/ctrip-flight.ts +181 -0
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/dida-portal.ts +338 -0
- package/ts/capabilities/session/adapters/meituan-local.ts +70 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +392 -0
- package/ts/capabilities/session/benchmark.ts +276 -0
- package/ts/capabilities/session/bridge-ledger.ts +333 -0
- package/ts/capabilities/session/extension-bridge.ts +794 -0
- package/ts/capabilities/session/extension-channel.ts +144 -0
- package/ts/capabilities/session/extension-distribution.ts +266 -0
- package/ts/capabilities/session/extract.ts +54 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +264 -0
- package/ts/capabilities/session/read-guard.ts +141 -0
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/transport.ts +146 -0
- package/ts/capabilities/session/wizard.ts +140 -0
- package/ts/capabilities/session-consent.ts +211 -0
- package/ts/capabilities/session-login.ts +208 -0
- package/ts/capabilities/session-search.ts +893 -0
- package/ts/capabilities/weather.ts +330 -0
- package/ts/dsh-runtime/vendor/README.md +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +405 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +404 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +1584 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/icon.svg +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +595 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +58 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +131 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/geo.js +232 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +120 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +40 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/staticmap-route.js +115 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +267 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +90 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/geo.d.ts +61 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/staticmap-route.d.ts +31 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +75 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +68 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +97 -0
- package/ts/package.json +293 -3
- package/ts/scripts/async-collect.ts +91 -0
- package/ts/scripts/map-tools-vendor-package-proof.ts +404 -0
- package/ts/scripts/skeleton-check.ts +1 -1
- package/ts/scripts/skeleton-integration-test.ts +23 -1
- package/ts/scripts/state-cli.ts +568 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +991 -0
- package/ts/src/benchmark-agent-conformance.ts +619 -0
- package/ts/src/benchmark-environment-bridge.ts +538 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/bookable-facts.ts +837 -0
- package/ts/src/booking-saga.ts +133 -0
- package/ts/src/booking-surface/availability-policy.ts +522 -0
- package/ts/src/booking-surface/booking-intent.ts +86 -0
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/canonical-schema.js +116 -0
- package/ts/src/booking-surface/contracts.ts +475 -0
- package/ts/src/booking-surface/dsh-planner.ts +1949 -0
- package/ts/src/booking-surface/dsh-plugin.js +378 -0
- package/ts/src/booking-surface/dsh-worker.ts +110 -0
- package/ts/src/booking-surface/error-codes.ts +241 -0
- package/ts/src/booking-surface/index.ts +9 -0
- package/ts/src/booking-surface/managed-dsh-cleanup-diagnostic.ts +151 -0
- package/ts/src/booking-surface/managed-dsh-run-port.ts +273 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/runtime.ts +1793 -0
- package/ts/src/booking-surface/server.ts +733 -0
- package/ts/src/booking-surface/startup.ts +201 -0
- package/ts/src/booking-surface/validation.ts +217 -0
- package/ts/src/companions.ts +113 -0
- package/ts/src/contracts.ts +13 -3
- package/ts/src/dsh-llm.ts +44 -11
- package/ts/src/flyai-setup-tool.ts +77 -0
- package/ts/src/index.ts +1948 -198
- package/ts/src/itinerary-deck.ts +189 -0
- package/ts/src/itinerary-doc-shared.ts +741 -0
- package/ts/src/itinerary-html.ts +171 -0
- package/ts/src/loop.ts +223 -25
- package/ts/src/memory-capture.ts +283 -0
- package/ts/src/memory-decay.ts +55 -0
- package/ts/src/memory-utility.ts +70 -0
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/recall/card.ts +106 -0
- package/ts/src/recall/evaluator.ts +170 -0
- package/ts/src/recall/integration.ts +35 -0
- package/ts/src/recall/tick.ts +213 -0
- package/ts/src/session-link/plan-it-action.ts +78 -0
- package/ts/src/session-link/session-link.ts +275 -0
- package/ts/src/share/adapters.ts +93 -0
- package/ts/src/share/share-consent.ts +105 -0
- package/ts/src/share/share-deck.ts +85 -0
- package/ts/src/share/share-token.ts +166 -0
- package/ts/src/slot-spec.ts +172 -0
- package/ts/src/state-ledger.ts +912 -0
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +338 -0
- package/ts/src/tool-packet.ts +32 -0
- package/ts/src/travel-slots.ts +219 -0
- package/ts/src/travel-timeline.ts +83 -0
- package/ts/src/turn-deadline.ts +366 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/unified.ts +354 -88
- package/ts/src/wish-pool.ts +79 -0
- package/dist/scripts/session-attach-wait.js +0 -41
|
@@ -0,0 +1,1310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explicit opt-in M4 planning lifecycle observation collector.
|
|
3
|
+
*
|
|
4
|
+
* This module owns the deterministic contract behind scripts/memory-lifecycle.ts:
|
|
5
|
+
* - every subject/flow/evidence identifier is HMAC-SHA256 pseudonymous;
|
|
6
|
+
* - the wait-code vocabulary is frozen at dataset init;
|
|
7
|
+
* - lifecycle events are append-only and idempotent;
|
|
8
|
+
* - exports target the memory-value scorer input contract without claiming source attestation.
|
|
9
|
+
*/
|
|
10
|
+
import { createHash, createHmac, randomBytes } from 'node:crypto';
|
|
11
|
+
import { chmodSync, closeSync, constants, existsSync, ftruncateSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, statSync, truncateSync, unlinkSync, writeSync, } from 'node:fs';
|
|
12
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
13
|
+
export const MEMORY_LIFECYCLE_STORE_SCHEMA = 'memory_lifecycle_observations.v1';
|
|
14
|
+
export const MEMORY_LIFECYCLE_MANIFEST_SCHEMA = 'memory_lifecycle_manifest.v1';
|
|
15
|
+
export const MEMORY_LIFECYCLE_EVENT_SCHEMA = 'memory_lifecycle_event.v1';
|
|
16
|
+
export const MEMORY_VALUE_FIXTURE_SCHEMA = 'memory_value_fixture.v1';
|
|
17
|
+
export const MEMORY_VALUE_SOURCE_REVIEW_SCHEMA = 'memory_value_source_review.v1';
|
|
18
|
+
export const HMAC_REF_PATTERN = /^hmac-sha256:[0-9a-f]{64}$/;
|
|
19
|
+
export const SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
20
|
+
export const WAIT_CODE_PATTERN = /^[a-z][a-z0-9_]{0,31}$/;
|
|
21
|
+
export class MemoryLifecycleError extends Error {
|
|
22
|
+
code;
|
|
23
|
+
constructor(code) {
|
|
24
|
+
super(code);
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export const systemClock = {
|
|
29
|
+
now: () => new Date(),
|
|
30
|
+
};
|
|
31
|
+
function fail(code) {
|
|
32
|
+
throw new MemoryLifecycleError(code);
|
|
33
|
+
}
|
|
34
|
+
function isObject(value) {
|
|
35
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
function nonEmptyString(value) {
|
|
38
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
39
|
+
}
|
|
40
|
+
function assertRawLabel(value, limit = 512) {
|
|
41
|
+
if (!nonEmptyString(value))
|
|
42
|
+
fail('invalid_input');
|
|
43
|
+
if (value.length > limit)
|
|
44
|
+
fail('invalid_input');
|
|
45
|
+
if (/[\u0000-\u001f\u007f]/u.test(value))
|
|
46
|
+
fail('invalid_input');
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
function assertIsoDate(date) {
|
|
50
|
+
const ms = date.getTime();
|
|
51
|
+
if (!Number.isFinite(ms))
|
|
52
|
+
fail('invalid_input');
|
|
53
|
+
return date.toISOString();
|
|
54
|
+
}
|
|
55
|
+
function parseIsoMs(value) {
|
|
56
|
+
const ms = Date.parse(value);
|
|
57
|
+
if (!Number.isFinite(ms))
|
|
58
|
+
fail('invalid_store');
|
|
59
|
+
if (new Date(ms).toISOString() !== value)
|
|
60
|
+
fail('invalid_store');
|
|
61
|
+
return ms;
|
|
62
|
+
}
|
|
63
|
+
function normalizeHmacKey(key) {
|
|
64
|
+
if (!nonEmptyString(key))
|
|
65
|
+
fail('missing_hmac_key');
|
|
66
|
+
const trimmed = key.trim();
|
|
67
|
+
if (trimmed.length < 32)
|
|
68
|
+
fail('invalid_hmac_key');
|
|
69
|
+
if (/[\u0000-\u001f\u007f]/u.test(trimmed))
|
|
70
|
+
fail('invalid_hmac_key');
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
function normalizeWaitCodes(codes) {
|
|
74
|
+
if (!Array.isArray(codes) || codes.length === 0)
|
|
75
|
+
fail('invalid_input');
|
|
76
|
+
const seen = new Set();
|
|
77
|
+
for (const raw of codes) {
|
|
78
|
+
const code = assertRawLabel(raw, 32);
|
|
79
|
+
if (!WAIT_CODE_PATTERN.test(code))
|
|
80
|
+
fail('invalid_input');
|
|
81
|
+
if (seen.has(code))
|
|
82
|
+
fail('invalid_input');
|
|
83
|
+
seen.add(code);
|
|
84
|
+
}
|
|
85
|
+
return [...seen].sort();
|
|
86
|
+
}
|
|
87
|
+
function assertSourceKind(value) {
|
|
88
|
+
if (value !== 'synthetic_fixture' && value !== 'observed_private')
|
|
89
|
+
fail('invalid_input');
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function assertRefluxKind(value) {
|
|
93
|
+
if (value !== 'recalled' && value !== 'verified_outcome')
|
|
94
|
+
fail('invalid_input');
|
|
95
|
+
return value;
|
|
96
|
+
}
|
|
97
|
+
function assertPreferenceConsumer(value) {
|
|
98
|
+
if (value !== 'ranking' && value !== 'explanation')
|
|
99
|
+
fail('invalid_input');
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function assertHmacRef(value) {
|
|
103
|
+
if (!nonEmptyString(value) || !HMAC_REF_PATTERN.test(value))
|
|
104
|
+
fail('invalid_store');
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
function assertEventObject(value) {
|
|
108
|
+
if (!isObject(value))
|
|
109
|
+
fail('invalid_store');
|
|
110
|
+
if (value.schema_version !== MEMORY_LIFECYCLE_EVENT_SCHEMA)
|
|
111
|
+
fail('invalid_store');
|
|
112
|
+
assertHmacRef(value.event_ref);
|
|
113
|
+
if (!nonEmptyString(value.kind))
|
|
114
|
+
fail('invalid_store');
|
|
115
|
+
if (!nonEmptyString(value.occurred_at))
|
|
116
|
+
fail('invalid_store');
|
|
117
|
+
parseIsoMs(value.occurred_at);
|
|
118
|
+
assertHmacRef(value.consent_ref);
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
function normalizePolicyPath(path) {
|
|
122
|
+
return resolve(path).replace(/\\/g, '/');
|
|
123
|
+
}
|
|
124
|
+
function assertSafePolicyPath(path) {
|
|
125
|
+
const normalized = normalizePolicyPath(path);
|
|
126
|
+
if (normalized === '/' || normalized.endsWith('/.git') || normalized.includes('/.git/'))
|
|
127
|
+
fail('unsafe_state_root');
|
|
128
|
+
if (normalized.endsWith('/ts/dsh-runtime') || normalized.includes('/ts/dsh-runtime/'))
|
|
129
|
+
fail('unsafe_state_root');
|
|
130
|
+
}
|
|
131
|
+
function nearestExistingAncestor(path) {
|
|
132
|
+
let current = resolve(path);
|
|
133
|
+
for (;;) {
|
|
134
|
+
if (existsSync(current))
|
|
135
|
+
return current;
|
|
136
|
+
const parent = dirname(current);
|
|
137
|
+
if (parent === current)
|
|
138
|
+
return current;
|
|
139
|
+
current = parent;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function realpathOrUnsafe(path) {
|
|
143
|
+
try {
|
|
144
|
+
return realpathSync.native(path);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
fail('unsafe_state_root');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function sameOrChildPath(parent, child) {
|
|
151
|
+
const distance = relative(parent, child);
|
|
152
|
+
return distance === '' || (!distance.startsWith('..') && !isAbsolute(distance));
|
|
153
|
+
}
|
|
154
|
+
function assertSafeStateRoot(stateRoot) {
|
|
155
|
+
if (!nonEmptyString(stateRoot))
|
|
156
|
+
fail('missing_state_root');
|
|
157
|
+
const resolved = resolve(stateRoot);
|
|
158
|
+
assertSafePolicyPath(resolved);
|
|
159
|
+
assertSafePolicyPath(realpathOrUnsafe(nearestExistingAncestor(resolved)));
|
|
160
|
+
return resolved;
|
|
161
|
+
}
|
|
162
|
+
function assertManagedStoreIsolation(storeRoot) {
|
|
163
|
+
const resolvedStoreRoot = resolve(storeRoot);
|
|
164
|
+
const stateRoot = resolve(resolvedStoreRoot, '..', '..');
|
|
165
|
+
assertSafePolicyPath(stateRoot);
|
|
166
|
+
assertSafePolicyPath(resolvedStoreRoot);
|
|
167
|
+
assertSafePolicyPath(realpathOrUnsafe(nearestExistingAncestor(stateRoot)));
|
|
168
|
+
if (!existsSync(stateRoot)) {
|
|
169
|
+
assertSafePolicyPath(realpathOrUnsafe(nearestExistingAncestor(resolvedStoreRoot)));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const stateRootReal = realpathOrUnsafe(stateRoot);
|
|
173
|
+
assertSafePolicyPath(stateRootReal);
|
|
174
|
+
for (const managedPath of [join(stateRoot, 'gotry-state'), resolvedStoreRoot]) {
|
|
175
|
+
const existing = nearestExistingAncestor(managedPath);
|
|
176
|
+
const existingReal = realpathOrUnsafe(existing);
|
|
177
|
+
assertSafePolicyPath(existingReal);
|
|
178
|
+
if (!sameOrChildPath(stateRootReal, existingReal))
|
|
179
|
+
fail('unsafe_state_root');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function prepareCommon(options) {
|
|
183
|
+
const stateRoot = assertSafeStateRoot(options.stateRoot);
|
|
184
|
+
if (!nonEmptyString(options.consent))
|
|
185
|
+
fail('missing_consent');
|
|
186
|
+
const hmacKey = normalizeHmacKey(options.hmacKey);
|
|
187
|
+
const consent = assertRawLabel(options.consent, 512);
|
|
188
|
+
return {
|
|
189
|
+
consentRef: pseudonymousRef(hmacKey, 'consent', [consent]),
|
|
190
|
+
hmacKey,
|
|
191
|
+
stateRoot,
|
|
192
|
+
storeRoot: join(stateRoot, 'gotry-state', 'memory-lifecycle'),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export function pseudonymousRef(hmacKey, kind, parts) {
|
|
196
|
+
const key = normalizeHmacKey(hmacKey);
|
|
197
|
+
const hmac = createHmac('sha256', key);
|
|
198
|
+
hmac.update('gotry-memory-lifecycle.v1');
|
|
199
|
+
hmac.update('\0');
|
|
200
|
+
hmac.update(assertRawLabel(kind, 64));
|
|
201
|
+
for (const part of parts) {
|
|
202
|
+
hmac.update('\0');
|
|
203
|
+
hmac.update(assertRawLabel(part, 2048));
|
|
204
|
+
}
|
|
205
|
+
return `hmac-sha256:${hmac.digest('hex')}`;
|
|
206
|
+
}
|
|
207
|
+
export function canonicalJson(value) {
|
|
208
|
+
if (Array.isArray(value))
|
|
209
|
+
return `[${value.map(canonicalJson).join(',')}]`;
|
|
210
|
+
if (isObject(value)) {
|
|
211
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
|
|
212
|
+
}
|
|
213
|
+
return JSON.stringify(value);
|
|
214
|
+
}
|
|
215
|
+
export function sha256Hex(value) {
|
|
216
|
+
return createHash('sha256').update(value).digest('hex');
|
|
217
|
+
}
|
|
218
|
+
export function memoryValueSummaryDigest(input) {
|
|
219
|
+
const payload = isObject(input)
|
|
220
|
+
? Object.fromEntries(Object.entries(input).filter(([key]) => key !== 'source_review'))
|
|
221
|
+
: input;
|
|
222
|
+
return sha256Hex(canonicalJson(payload));
|
|
223
|
+
}
|
|
224
|
+
function eventRef(hmacKey, kind, parts) {
|
|
225
|
+
return pseudonymousRef(hmacKey, `event:${kind}`, parts);
|
|
226
|
+
}
|
|
227
|
+
function subjectRef(hmacKey, subject) {
|
|
228
|
+
return pseudonymousRef(hmacKey, 'subject', [subject]);
|
|
229
|
+
}
|
|
230
|
+
function flowRef(hmacKey, subject, flow) {
|
|
231
|
+
return pseudonymousRef(hmacKey, 'flow', [subjectRef(hmacKey, subject), flow]);
|
|
232
|
+
}
|
|
233
|
+
function waitRef(hmacKey, subject, flow, wait) {
|
|
234
|
+
return pseudonymousRef(hmacKey, 'external-wait', [flowRef(hmacKey, subject, flow), wait]);
|
|
235
|
+
}
|
|
236
|
+
function experienceRef(hmacKey, experience) {
|
|
237
|
+
return pseudonymousRef(hmacKey, 'experience', [experience]);
|
|
238
|
+
}
|
|
239
|
+
function evidenceRef(hmacKey, evidence) {
|
|
240
|
+
return pseudonymousRef(hmacKey, 'evidence', [evidence]);
|
|
241
|
+
}
|
|
242
|
+
function assertionRef(hmacKey, assertion) {
|
|
243
|
+
return pseudonymousRef(hmacKey, 'preference-assertion', [assertion]);
|
|
244
|
+
}
|
|
245
|
+
function datasetRef(hmacKey, dataset) {
|
|
246
|
+
return pseudonymousRef(hmacKey, 'dataset', [dataset]);
|
|
247
|
+
}
|
|
248
|
+
function datasetKeyVerifier(hmacKey) {
|
|
249
|
+
return pseudonymousRef(hmacKey, 'dataset-key-verifier', [MEMORY_LIFECYCLE_STORE_SCHEMA]);
|
|
250
|
+
}
|
|
251
|
+
const OPEN_NOFOLLOW = typeof constants.O_NOFOLLOW === 'number' ? constants.O_NOFOLLOW : 0;
|
|
252
|
+
let tempFileCounter = 0;
|
|
253
|
+
function ensurePrivateDir(path) {
|
|
254
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
255
|
+
try {
|
|
256
|
+
chmodSync(path, 0o700);
|
|
257
|
+
}
|
|
258
|
+
catch { /* best effort on non-POSIX filesystems */ }
|
|
259
|
+
}
|
|
260
|
+
function pathExistsByLstat(path) {
|
|
261
|
+
try {
|
|
262
|
+
lstatSync(path);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (isNodeError(error, 'ENOENT'))
|
|
267
|
+
return false;
|
|
268
|
+
fail('unsafe_state_root');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function assertNoSymlinkLeaf(path) {
|
|
272
|
+
try {
|
|
273
|
+
if (lstatSync(path).isSymbolicLink())
|
|
274
|
+
fail('unsafe_state_root');
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
if (isNodeError(error, 'ENOENT'))
|
|
278
|
+
return;
|
|
279
|
+
if (error instanceof MemoryLifecycleError)
|
|
280
|
+
throw error;
|
|
281
|
+
fail('unsafe_state_root');
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function assertManagedFilePath(storeRoot, path) {
|
|
285
|
+
const resolvedStoreRoot = resolve(storeRoot);
|
|
286
|
+
const resolvedPath = resolve(path);
|
|
287
|
+
if (!sameOrChildPath(resolvedStoreRoot, resolvedPath))
|
|
288
|
+
fail('unsafe_state_root');
|
|
289
|
+
assertSafePolicyPath(resolvedPath);
|
|
290
|
+
assertManagedStoreIsolation(resolvedStoreRoot);
|
|
291
|
+
const stateRootReal = realpathOrUnsafe(resolve(resolvedStoreRoot, '..', '..'));
|
|
292
|
+
const parentReal = realpathOrUnsafe(nearestExistingAncestor(dirname(resolvedPath)));
|
|
293
|
+
assertSafePolicyPath(parentReal);
|
|
294
|
+
if (!sameOrChildPath(stateRootReal, parentReal))
|
|
295
|
+
fail('unsafe_state_root');
|
|
296
|
+
assertNoSymlinkLeaf(resolvedPath);
|
|
297
|
+
}
|
|
298
|
+
function assertOutputPath(path) {
|
|
299
|
+
const resolvedPath = resolve(path);
|
|
300
|
+
assertSafePolicyPath(resolvedPath);
|
|
301
|
+
const parent = dirname(resolvedPath);
|
|
302
|
+
assertSafePolicyPath(parent);
|
|
303
|
+
assertSafePolicyPath(realpathOrUnsafe(nearestExistingAncestor(parent)));
|
|
304
|
+
if (existsSync(parent))
|
|
305
|
+
assertSafePolicyPath(realpathOrUnsafe(parent));
|
|
306
|
+
return resolvedPath;
|
|
307
|
+
}
|
|
308
|
+
function privateTempPath(path) {
|
|
309
|
+
tempFileCounter += 1;
|
|
310
|
+
return join(dirname(path), `.${basename(path)}.${process.pid}.${Date.now()}.${tempFileCounter}.${randomBytes(6).toString('hex')}.tmp`);
|
|
311
|
+
}
|
|
312
|
+
function writeAllSync(fd, content) {
|
|
313
|
+
const bytes = Buffer.isBuffer(content) ? content : Buffer.from(content);
|
|
314
|
+
let offset = 0;
|
|
315
|
+
while (offset < bytes.length) {
|
|
316
|
+
const written = writeSync(fd, bytes, offset, bytes.length - offset);
|
|
317
|
+
if (!Number.isInteger(written) || written <= 0)
|
|
318
|
+
fail('internal_error');
|
|
319
|
+
offset += written;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
function writePrivateTempFile(path, content) {
|
|
323
|
+
let fd = -1;
|
|
324
|
+
let created = false;
|
|
325
|
+
let caught;
|
|
326
|
+
try {
|
|
327
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | OPEN_NOFOLLOW, 0o600);
|
|
328
|
+
created = true;
|
|
329
|
+
writeAllSync(fd, content);
|
|
330
|
+
}
|
|
331
|
+
catch (error) {
|
|
332
|
+
caught = error;
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
if (fd >= 0) {
|
|
336
|
+
try {
|
|
337
|
+
closeSync(fd);
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
caught ??= error;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (caught !== undefined && created) {
|
|
344
|
+
try {
|
|
345
|
+
unlinkSync(path);
|
|
346
|
+
}
|
|
347
|
+
catch { /* best effort temp cleanup */ }
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (caught !== undefined) {
|
|
351
|
+
if (caught instanceof MemoryLifecycleError)
|
|
352
|
+
throw caught;
|
|
353
|
+
if (isNodeError(caught, 'ELOOP'))
|
|
354
|
+
fail('unsafe_state_root');
|
|
355
|
+
fail('internal_error');
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
chmodSync(path, 0o600);
|
|
359
|
+
}
|
|
360
|
+
catch { /* best effort */ }
|
|
361
|
+
}
|
|
362
|
+
function publishTempNoOverwrite(tempPath, finalPath, existsCode) {
|
|
363
|
+
try {
|
|
364
|
+
linkSync(tempPath, finalPath);
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
if (isNodeError(error, 'EEXIST'))
|
|
368
|
+
fail(existsCode);
|
|
369
|
+
fail('internal_error');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function writeManagedFileNoOverwrite(storeRoot, path, content, existsCode) {
|
|
373
|
+
assertManagedFilePath(storeRoot, path);
|
|
374
|
+
ensurePrivateDir(dirname(path));
|
|
375
|
+
assertManagedFilePath(storeRoot, path);
|
|
376
|
+
if (pathExistsByLstat(path))
|
|
377
|
+
fail(existsCode);
|
|
378
|
+
const tempPath = privateTempPath(path);
|
|
379
|
+
let tempCreated = false;
|
|
380
|
+
try {
|
|
381
|
+
assertManagedFilePath(storeRoot, tempPath);
|
|
382
|
+
writePrivateTempFile(tempPath, content);
|
|
383
|
+
tempCreated = true;
|
|
384
|
+
assertManagedFilePath(storeRoot, path);
|
|
385
|
+
publishTempNoOverwrite(tempPath, path, existsCode);
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
if (tempCreated) {
|
|
389
|
+
try {
|
|
390
|
+
unlinkSync(tempPath);
|
|
391
|
+
}
|
|
392
|
+
catch { /* best effort temp cleanup */ }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
try {
|
|
396
|
+
chmodSync(path, 0o600);
|
|
397
|
+
}
|
|
398
|
+
catch { /* best effort */ }
|
|
399
|
+
}
|
|
400
|
+
function writePrivateFileNoOverwrite(path, content) {
|
|
401
|
+
const finalPath = assertOutputPath(path);
|
|
402
|
+
ensurePrivateDir(dirname(finalPath));
|
|
403
|
+
assertOutputPath(finalPath);
|
|
404
|
+
if (pathExistsByLstat(finalPath))
|
|
405
|
+
fail('output_exists');
|
|
406
|
+
const tempPath = privateTempPath(finalPath);
|
|
407
|
+
let tempCreated = false;
|
|
408
|
+
try {
|
|
409
|
+
assertOutputPath(tempPath);
|
|
410
|
+
writePrivateTempFile(tempPath, content);
|
|
411
|
+
tempCreated = true;
|
|
412
|
+
assertOutputPath(finalPath);
|
|
413
|
+
if (pathExistsByLstat(finalPath))
|
|
414
|
+
fail('output_exists');
|
|
415
|
+
publishTempNoOverwrite(tempPath, finalPath, 'output_exists');
|
|
416
|
+
}
|
|
417
|
+
finally {
|
|
418
|
+
if (tempCreated) {
|
|
419
|
+
try {
|
|
420
|
+
unlinkSync(tempPath);
|
|
421
|
+
}
|
|
422
|
+
catch { /* best effort temp cleanup */ }
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
chmodSync(finalPath, 0o600);
|
|
427
|
+
}
|
|
428
|
+
catch { /* best effort */ }
|
|
429
|
+
}
|
|
430
|
+
function appendPrivateLine(storeRoot, path, line) {
|
|
431
|
+
assertManagedFilePath(storeRoot, path);
|
|
432
|
+
ensurePrivateDir(dirname(path));
|
|
433
|
+
assertManagedFilePath(storeRoot, path);
|
|
434
|
+
const existedBefore = pathExistsByLstat(path);
|
|
435
|
+
const originalSize = existedBefore ? statSync(path).size : 0;
|
|
436
|
+
const payload = `${line}\n`;
|
|
437
|
+
let fd = -1;
|
|
438
|
+
let caught;
|
|
439
|
+
try {
|
|
440
|
+
fd = openSync(path, constants.O_WRONLY | constants.O_CREAT | constants.O_APPEND | OPEN_NOFOLLOW, 0o600);
|
|
441
|
+
writeAllSync(fd, payload);
|
|
442
|
+
}
|
|
443
|
+
catch (error) {
|
|
444
|
+
caught = error;
|
|
445
|
+
if (fd >= 0) {
|
|
446
|
+
try {
|
|
447
|
+
ftruncateSync(fd, originalSize);
|
|
448
|
+
}
|
|
449
|
+
catch { /* keep original error */ }
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
if (fd >= 0) {
|
|
454
|
+
try {
|
|
455
|
+
closeSync(fd);
|
|
456
|
+
}
|
|
457
|
+
catch { /* close is non-semantic after append/truncate */ }
|
|
458
|
+
}
|
|
459
|
+
if (caught !== undefined && !existedBefore) {
|
|
460
|
+
try {
|
|
461
|
+
unlinkSync(path);
|
|
462
|
+
}
|
|
463
|
+
catch { /* best effort rollback */ }
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (caught !== undefined) {
|
|
467
|
+
if (caught instanceof MemoryLifecycleError)
|
|
468
|
+
throw caught;
|
|
469
|
+
if (isNodeError(caught, 'ELOOP'))
|
|
470
|
+
fail('unsafe_state_root');
|
|
471
|
+
fail('internal_error');
|
|
472
|
+
}
|
|
473
|
+
try {
|
|
474
|
+
chmodSync(path, 0o600);
|
|
475
|
+
}
|
|
476
|
+
catch { /* best effort */ }
|
|
477
|
+
}
|
|
478
|
+
function isNodeError(error, code) {
|
|
479
|
+
return isObject(error) && error.code === code;
|
|
480
|
+
}
|
|
481
|
+
function withStoreLock(storeRoot, createStore, body) {
|
|
482
|
+
assertManagedStoreIsolation(storeRoot);
|
|
483
|
+
if (createStore)
|
|
484
|
+
ensurePrivateDir(storeRoot);
|
|
485
|
+
else if (!existsSync(storeRoot))
|
|
486
|
+
fail('dataset_not_initialized');
|
|
487
|
+
assertManagedStoreIsolation(storeRoot);
|
|
488
|
+
const lockPath = join(storeRoot, '.writer.lock');
|
|
489
|
+
assertManagedFilePath(storeRoot, lockPath);
|
|
490
|
+
let fd = -1;
|
|
491
|
+
let createdLock = false;
|
|
492
|
+
try {
|
|
493
|
+
fd = openSync(lockPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | OPEN_NOFOLLOW, 0o600);
|
|
494
|
+
createdLock = true;
|
|
495
|
+
writeAllSync(fd, JSON.stringify({ schema_version: 'memory_lifecycle_lock.v1', pid: process.pid }));
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
if (fd >= 0) {
|
|
499
|
+
try {
|
|
500
|
+
closeSync(fd);
|
|
501
|
+
}
|
|
502
|
+
catch { /* preserve original error */ }
|
|
503
|
+
fd = -1;
|
|
504
|
+
}
|
|
505
|
+
if (createdLock) {
|
|
506
|
+
try {
|
|
507
|
+
unlinkSync(lockPath);
|
|
508
|
+
}
|
|
509
|
+
catch { /* best effort for this writer's failed lock */ }
|
|
510
|
+
}
|
|
511
|
+
if (isNodeError(error, 'EEXIST'))
|
|
512
|
+
fail('lock_busy');
|
|
513
|
+
fail('internal_error');
|
|
514
|
+
}
|
|
515
|
+
finally {
|
|
516
|
+
if (fd >= 0)
|
|
517
|
+
closeSync(fd);
|
|
518
|
+
}
|
|
519
|
+
try {
|
|
520
|
+
assertManagedStoreIsolation(storeRoot);
|
|
521
|
+
return body();
|
|
522
|
+
}
|
|
523
|
+
finally {
|
|
524
|
+
try {
|
|
525
|
+
unlinkSync(lockPath);
|
|
526
|
+
}
|
|
527
|
+
catch { /* lock cleanup is best effort */ }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function manifestPath(storeRoot) {
|
|
531
|
+
return join(storeRoot, 'manifest.json');
|
|
532
|
+
}
|
|
533
|
+
function eventsPath(storeRoot) {
|
|
534
|
+
return join(storeRoot, 'events.jsonl');
|
|
535
|
+
}
|
|
536
|
+
function readManifest(storeRoot) {
|
|
537
|
+
const path = manifestPath(storeRoot);
|
|
538
|
+
assertManagedFilePath(storeRoot, path);
|
|
539
|
+
if (!existsSync(path))
|
|
540
|
+
fail('dataset_not_initialized');
|
|
541
|
+
let parsed;
|
|
542
|
+
try {
|
|
543
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
544
|
+
}
|
|
545
|
+
catch {
|
|
546
|
+
fail('invalid_store');
|
|
547
|
+
}
|
|
548
|
+
return assertManifest(parsed);
|
|
549
|
+
}
|
|
550
|
+
function assertManifest(value) {
|
|
551
|
+
if (!isObject(value))
|
|
552
|
+
fail('invalid_store');
|
|
553
|
+
if (value.schema_version !== MEMORY_LIFECYCLE_MANIFEST_SCHEMA)
|
|
554
|
+
fail('invalid_store');
|
|
555
|
+
if (value.collector_schema !== MEMORY_LIFECYCLE_STORE_SCHEMA)
|
|
556
|
+
fail('invalid_store');
|
|
557
|
+
const ref = assertHmacRef(value.dataset_ref);
|
|
558
|
+
const keyVerifier = assertHmacRef(value.dataset_key_verifier);
|
|
559
|
+
const source = value.source_kind;
|
|
560
|
+
if (source !== 'synthetic_fixture' && source !== 'observed_private')
|
|
561
|
+
fail('invalid_store');
|
|
562
|
+
if (!nonEmptyString(value.created_at))
|
|
563
|
+
fail('invalid_store');
|
|
564
|
+
parseIsoMs(value.created_at);
|
|
565
|
+
const consentRef = assertHmacRef(value.consent_ref);
|
|
566
|
+
if (!isObject(value.measurement_policy))
|
|
567
|
+
fail('invalid_store');
|
|
568
|
+
const policy = value.measurement_policy;
|
|
569
|
+
if (policy.quantile_method !== 'nearest_rank')
|
|
570
|
+
fail('invalid_store');
|
|
571
|
+
if (!Array.isArray(policy.predeclared_external_wait_codes))
|
|
572
|
+
fail('invalid_store');
|
|
573
|
+
const waitCodes = normalizeWaitCodes(policy.predeclared_external_wait_codes.map(String));
|
|
574
|
+
if (policy.minimum_pair_count_for_exit !== 5)
|
|
575
|
+
fail('invalid_store');
|
|
576
|
+
if (policy.target_median_reduction_ratio !== 0.5)
|
|
577
|
+
fail('invalid_store');
|
|
578
|
+
if (!isObject(value.p4) || value.p4.state !== 'closed' || !isObject(value.p4.triggers))
|
|
579
|
+
fail('invalid_store');
|
|
580
|
+
if (value.p4.triggers.real_usage !== false || value.p4.triggers.multi_user !== false)
|
|
581
|
+
fail('invalid_store');
|
|
582
|
+
return {
|
|
583
|
+
schema_version: MEMORY_LIFECYCLE_MANIFEST_SCHEMA,
|
|
584
|
+
collector_schema: MEMORY_LIFECYCLE_STORE_SCHEMA,
|
|
585
|
+
dataset_ref: ref,
|
|
586
|
+
dataset_key_verifier: keyVerifier,
|
|
587
|
+
source_kind: source,
|
|
588
|
+
created_at: value.created_at,
|
|
589
|
+
consent_ref: consentRef,
|
|
590
|
+
measurement_policy: {
|
|
591
|
+
quantile_method: 'nearest_rank',
|
|
592
|
+
predeclared_external_wait_codes: waitCodes,
|
|
593
|
+
minimum_pair_count_for_exit: 5,
|
|
594
|
+
target_median_reduction_ratio: 0.5,
|
|
595
|
+
},
|
|
596
|
+
p4: { state: 'closed', triggers: { real_usage: false, multi_user: false } },
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
function readEvents(storeRoot, recoverUncommittedTail = false) {
|
|
600
|
+
const path = eventsPath(storeRoot);
|
|
601
|
+
assertManagedFilePath(storeRoot, path);
|
|
602
|
+
if (!existsSync(path))
|
|
603
|
+
return [];
|
|
604
|
+
let raw;
|
|
605
|
+
try {
|
|
606
|
+
raw = readFileSync(path, 'utf8');
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
fail('invalid_store');
|
|
610
|
+
}
|
|
611
|
+
if (recoverUncommittedTail)
|
|
612
|
+
raw = recoverCommittedEventLog(storeRoot, path, raw);
|
|
613
|
+
return parseEventLines(raw.split('\n'));
|
|
614
|
+
}
|
|
615
|
+
function recoverCommittedEventLog(storeRoot, path, raw) {
|
|
616
|
+
if (raw === '' || raw.endsWith('\n'))
|
|
617
|
+
return raw;
|
|
618
|
+
const committedLength = raw.lastIndexOf('\n') + 1;
|
|
619
|
+
const committedRaw = raw.slice(0, committedLength);
|
|
620
|
+
parseEventLines(committedRaw.split('\n'));
|
|
621
|
+
const tail = raw.slice(committedLength);
|
|
622
|
+
if (tail.trim() !== '') {
|
|
623
|
+
try {
|
|
624
|
+
parseEventLines([...committedRaw.split('\n').filter(line => line.trim() !== ''), tail]);
|
|
625
|
+
appendPrivateLine(storeRoot, path, '');
|
|
626
|
+
return `${raw}\n`;
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
if (!(error instanceof MemoryLifecycleError) || error.code !== 'invalid_store')
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
try {
|
|
634
|
+
assertManagedFilePath(storeRoot, path);
|
|
635
|
+
truncateSync(path, committedLength);
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
if (error instanceof MemoryLifecycleError)
|
|
639
|
+
throw error;
|
|
640
|
+
fail('invalid_store');
|
|
641
|
+
}
|
|
642
|
+
return committedRaw;
|
|
643
|
+
}
|
|
644
|
+
function parseEventLines(lines) {
|
|
645
|
+
const events = [];
|
|
646
|
+
const seen = new Map();
|
|
647
|
+
for (const line of lines) {
|
|
648
|
+
if (line.trim() === '')
|
|
649
|
+
continue;
|
|
650
|
+
let parsed;
|
|
651
|
+
try {
|
|
652
|
+
parsed = JSON.parse(line);
|
|
653
|
+
}
|
|
654
|
+
catch {
|
|
655
|
+
fail('invalid_store');
|
|
656
|
+
}
|
|
657
|
+
const event = assertLifecycleEvent(parsed);
|
|
658
|
+
const canonical = canonicalJson(event);
|
|
659
|
+
const prior = seen.get(event.event_ref);
|
|
660
|
+
if (prior !== undefined) {
|
|
661
|
+
if (prior !== canonical)
|
|
662
|
+
fail('invalid_store');
|
|
663
|
+
continue;
|
|
664
|
+
}
|
|
665
|
+
seen.set(event.event_ref, canonical);
|
|
666
|
+
events.push(event);
|
|
667
|
+
}
|
|
668
|
+
return events;
|
|
669
|
+
}
|
|
670
|
+
function assertLifecycleEvent(value) {
|
|
671
|
+
const raw = assertEventObject(value);
|
|
672
|
+
switch (raw.kind) {
|
|
673
|
+
case 'flow_started':
|
|
674
|
+
if (raw.eligible !== true)
|
|
675
|
+
fail('invalid_store');
|
|
676
|
+
return {
|
|
677
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
678
|
+
event_ref: raw.event_ref,
|
|
679
|
+
kind: 'flow_started',
|
|
680
|
+
occurred_at: raw.occurred_at,
|
|
681
|
+
consent_ref: raw.consent_ref,
|
|
682
|
+
subject_ref: assertHmacRef(raw.subject_ref),
|
|
683
|
+
flow_ref: assertHmacRef(raw.flow_ref),
|
|
684
|
+
eligible: true,
|
|
685
|
+
};
|
|
686
|
+
case 'external_wait_started':
|
|
687
|
+
if (!nonEmptyString(raw.code) || !WAIT_CODE_PATTERN.test(raw.code))
|
|
688
|
+
fail('invalid_store');
|
|
689
|
+
return {
|
|
690
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
691
|
+
event_ref: raw.event_ref,
|
|
692
|
+
kind: 'external_wait_started',
|
|
693
|
+
occurred_at: raw.occurred_at,
|
|
694
|
+
consent_ref: raw.consent_ref,
|
|
695
|
+
subject_ref: assertHmacRef(raw.subject_ref),
|
|
696
|
+
flow_ref: assertHmacRef(raw.flow_ref),
|
|
697
|
+
wait_ref: assertHmacRef(raw.wait_ref),
|
|
698
|
+
code: raw.code,
|
|
699
|
+
};
|
|
700
|
+
case 'external_wait_ended':
|
|
701
|
+
return {
|
|
702
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
703
|
+
event_ref: raw.event_ref,
|
|
704
|
+
kind: 'external_wait_ended',
|
|
705
|
+
occurred_at: raw.occurred_at,
|
|
706
|
+
consent_ref: raw.consent_ref,
|
|
707
|
+
subject_ref: assertHmacRef(raw.subject_ref),
|
|
708
|
+
flow_ref: assertHmacRef(raw.flow_ref),
|
|
709
|
+
wait_ref: assertHmacRef(raw.wait_ref),
|
|
710
|
+
};
|
|
711
|
+
case 'flow_completed':
|
|
712
|
+
if (raw.status !== 'completed')
|
|
713
|
+
fail('invalid_store');
|
|
714
|
+
return {
|
|
715
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
716
|
+
event_ref: raw.event_ref,
|
|
717
|
+
kind: 'flow_completed',
|
|
718
|
+
occurred_at: raw.occurred_at,
|
|
719
|
+
consent_ref: raw.consent_ref,
|
|
720
|
+
subject_ref: assertHmacRef(raw.subject_ref),
|
|
721
|
+
flow_ref: assertHmacRef(raw.flow_ref),
|
|
722
|
+
status: 'completed',
|
|
723
|
+
};
|
|
724
|
+
case 'experience_reflux_recorded': {
|
|
725
|
+
const kind = raw.reflux_kind;
|
|
726
|
+
if (kind !== 'recalled' && kind !== 'verified_outcome')
|
|
727
|
+
fail('invalid_store');
|
|
728
|
+
return {
|
|
729
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
730
|
+
event_ref: raw.event_ref,
|
|
731
|
+
kind: 'experience_reflux_recorded',
|
|
732
|
+
occurred_at: raw.occurred_at,
|
|
733
|
+
consent_ref: raw.consent_ref,
|
|
734
|
+
reflux_kind: kind,
|
|
735
|
+
experience_ref: assertHmacRef(raw.experience_ref),
|
|
736
|
+
evidence_ref: assertHmacRef(raw.evidence_ref),
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
case 'preference_recorded': {
|
|
740
|
+
const consumer = raw.consumer;
|
|
741
|
+
if (consumer !== 'ranking' && consumer !== 'explanation')
|
|
742
|
+
fail('invalid_store');
|
|
743
|
+
if (raw.hard_filter !== false)
|
|
744
|
+
fail('invalid_store');
|
|
745
|
+
return {
|
|
746
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
747
|
+
event_ref: raw.event_ref,
|
|
748
|
+
kind: 'preference_recorded',
|
|
749
|
+
occurred_at: raw.occurred_at,
|
|
750
|
+
consent_ref: raw.consent_ref,
|
|
751
|
+
assertion_ref: assertHmacRef(raw.assertion_ref),
|
|
752
|
+
evidence_ref: assertHmacRef(raw.evidence_ref),
|
|
753
|
+
consumer,
|
|
754
|
+
hard_filter: false,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
default:
|
|
758
|
+
fail('invalid_store');
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
function eventExists(events, ref) {
|
|
762
|
+
return events.some(event => event.event_ref === ref);
|
|
763
|
+
}
|
|
764
|
+
function appendEvent(storeRoot, events, event) {
|
|
765
|
+
assertCandidateProjection(events, event);
|
|
766
|
+
assertManagedStoreIsolation(storeRoot);
|
|
767
|
+
appendPrivateLine(storeRoot, eventsPath(storeRoot), JSON.stringify(event));
|
|
768
|
+
}
|
|
769
|
+
function assertCandidateProjection(events, event) {
|
|
770
|
+
try {
|
|
771
|
+
projectLifecycleEvents([...events, event]);
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
if (error instanceof MemoryLifecycleError && error.code === 'invalid_store')
|
|
775
|
+
fail('invalid_transition');
|
|
776
|
+
throw error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
export function projectLifecycleEvents(events) {
|
|
780
|
+
const flows = new Map();
|
|
781
|
+
const flowsBySubject = new Map();
|
|
782
|
+
const refluxEvents = [];
|
|
783
|
+
const preferences = [];
|
|
784
|
+
const refluxKinds = new Set();
|
|
785
|
+
const assertionRefs = new Set();
|
|
786
|
+
for (const event of events) {
|
|
787
|
+
switch (event.kind) {
|
|
788
|
+
case 'flow_started': {
|
|
789
|
+
if (flows.has(event.flow_ref))
|
|
790
|
+
fail('invalid_store');
|
|
791
|
+
const subjectFlows = flowsBySubject.get(event.subject_ref) ?? [];
|
|
792
|
+
if (subjectFlows.some(existing => existing.completedAt === undefined))
|
|
793
|
+
fail('invalid_store');
|
|
794
|
+
const startedAtMs = parseIsoMs(event.occurred_at);
|
|
795
|
+
const lastCompleted = subjectFlows
|
|
796
|
+
.filter(existing => existing.completedAt !== undefined)
|
|
797
|
+
.sort(compareFlows)
|
|
798
|
+
.at(-1);
|
|
799
|
+
if (lastCompleted && startedAtMs <= parseIsoMs(lastCompleted.completedAt))
|
|
800
|
+
fail('invalid_store');
|
|
801
|
+
const flow = {
|
|
802
|
+
subjectRef: event.subject_ref,
|
|
803
|
+
flowRef: event.flow_ref,
|
|
804
|
+
eligible: true,
|
|
805
|
+
startedAt: event.occurred_at,
|
|
806
|
+
waits: new Map(),
|
|
807
|
+
};
|
|
808
|
+
subjectFlows.push(flow);
|
|
809
|
+
flowsBySubject.set(event.subject_ref, subjectFlows);
|
|
810
|
+
flows.set(event.flow_ref, flow);
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
case 'external_wait_started': {
|
|
814
|
+
const flow = flows.get(event.flow_ref);
|
|
815
|
+
if (!flow || flow.subjectRef !== event.subject_ref || flow.completedAt !== undefined)
|
|
816
|
+
fail('invalid_store');
|
|
817
|
+
if (flow.waits.has(event.wait_ref))
|
|
818
|
+
fail('invalid_store');
|
|
819
|
+
if ([...flow.waits.values()].some(wait => wait.completedAt === undefined))
|
|
820
|
+
fail('invalid_store');
|
|
821
|
+
const startedAtMs = parseIsoMs(event.occurred_at);
|
|
822
|
+
if (startedAtMs < parseIsoMs(flow.startedAt))
|
|
823
|
+
fail('invalid_store');
|
|
824
|
+
for (const wait of flow.waits.values()) {
|
|
825
|
+
if (wait.completedAt && startedAtMs < parseIsoMs(wait.completedAt))
|
|
826
|
+
fail('invalid_store');
|
|
827
|
+
}
|
|
828
|
+
flow.waits.set(event.wait_ref, { waitRef: event.wait_ref, code: event.code, startedAt: event.occurred_at });
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
case 'external_wait_ended': {
|
|
832
|
+
const flow = flows.get(event.flow_ref);
|
|
833
|
+
const wait = flow?.waits.get(event.wait_ref);
|
|
834
|
+
if (!flow || flow.subjectRef !== event.subject_ref || flow.completedAt !== undefined || !wait || wait.completedAt !== undefined) {
|
|
835
|
+
fail('invalid_store');
|
|
836
|
+
}
|
|
837
|
+
if (parseIsoMs(event.occurred_at) <= parseIsoMs(wait.startedAt))
|
|
838
|
+
fail('invalid_store');
|
|
839
|
+
wait.completedAt = event.occurred_at;
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
case 'flow_completed': {
|
|
843
|
+
const flow = flows.get(event.flow_ref);
|
|
844
|
+
if (!flow || flow.subjectRef !== event.subject_ref || flow.completedAt !== undefined)
|
|
845
|
+
fail('invalid_store');
|
|
846
|
+
if ([...flow.waits.values()].some(wait => wait.completedAt === undefined))
|
|
847
|
+
fail('invalid_store');
|
|
848
|
+
if (parseIsoMs(event.occurred_at) <= parseIsoMs(flow.startedAt))
|
|
849
|
+
fail('invalid_store');
|
|
850
|
+
const completedAtMs = parseIsoMs(event.occurred_at);
|
|
851
|
+
for (const wait of flow.waits.values()) {
|
|
852
|
+
if (parseIsoMs(wait.startedAt) < parseIsoMs(flow.startedAt) || parseIsoMs(wait.completedAt) > completedAtMs)
|
|
853
|
+
fail('invalid_store');
|
|
854
|
+
}
|
|
855
|
+
flow.completedAt = event.occurred_at;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
case 'experience_reflux_recorded': {
|
|
859
|
+
const key = `${event.reflux_kind}:${event.experience_ref}`;
|
|
860
|
+
if (refluxKinds.has(key))
|
|
861
|
+
fail('invalid_store');
|
|
862
|
+
refluxKinds.add(key);
|
|
863
|
+
refluxEvents.push({ refluxKind: event.reflux_kind, experienceRef: event.experience_ref, evidenceRef: event.evidence_ref });
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
case 'preference_recorded':
|
|
867
|
+
if (assertionRefs.has(event.assertion_ref))
|
|
868
|
+
fail('invalid_store');
|
|
869
|
+
assertionRefs.add(event.assertion_ref);
|
|
870
|
+
preferences.push({
|
|
871
|
+
assertionRef: event.assertion_ref,
|
|
872
|
+
evidenceRef: event.evidence_ref,
|
|
873
|
+
consumer: event.consumer,
|
|
874
|
+
hardFilter: false,
|
|
875
|
+
});
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return { flows, flowsBySubject, refluxEvents, preferences };
|
|
880
|
+
}
|
|
881
|
+
function buildManifest(options, common, clock) {
|
|
882
|
+
return {
|
|
883
|
+
schema_version: MEMORY_LIFECYCLE_MANIFEST_SCHEMA,
|
|
884
|
+
collector_schema: MEMORY_LIFECYCLE_STORE_SCHEMA,
|
|
885
|
+
dataset_ref: datasetRef(common.hmacKey, assertRawLabel(options.dataset)),
|
|
886
|
+
dataset_key_verifier: datasetKeyVerifier(common.hmacKey),
|
|
887
|
+
source_kind: assertSourceKind(options.sourceKind),
|
|
888
|
+
created_at: assertIsoDate(clock.now()),
|
|
889
|
+
consent_ref: common.consentRef,
|
|
890
|
+
measurement_policy: {
|
|
891
|
+
quantile_method: 'nearest_rank',
|
|
892
|
+
predeclared_external_wait_codes: normalizeWaitCodes(options.waitCodes),
|
|
893
|
+
minimum_pair_count_for_exit: 5,
|
|
894
|
+
target_median_reduction_ratio: 0.5,
|
|
895
|
+
},
|
|
896
|
+
p4: { state: 'closed', triggers: { real_usage: false, multi_user: false } },
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
function sameFrozenDataset(existing, next) {
|
|
900
|
+
return existing.dataset_ref === next.dataset_ref
|
|
901
|
+
&& existing.dataset_key_verifier === next.dataset_key_verifier
|
|
902
|
+
&& existing.source_kind === next.source_kind
|
|
903
|
+
&& existing.consent_ref === next.consent_ref
|
|
904
|
+
&& canonicalJson(existing.measurement_policy) === canonicalJson(next.measurement_policy);
|
|
905
|
+
}
|
|
906
|
+
function assertDatasetKeyMatches(manifest, hmacKey) {
|
|
907
|
+
if (manifest.dataset_key_verifier !== datasetKeyVerifier(hmacKey))
|
|
908
|
+
fail('dataset_key_mismatch');
|
|
909
|
+
}
|
|
910
|
+
function assertDatasetConsentMatches(manifest, consentRef) {
|
|
911
|
+
if (manifest.consent_ref !== consentRef)
|
|
912
|
+
fail('dataset_consent_mismatch');
|
|
913
|
+
}
|
|
914
|
+
function assertEventsBelongToDataset(manifest, events) {
|
|
915
|
+
if (events.some(event => event.consent_ref !== manifest.consent_ref))
|
|
916
|
+
fail('invalid_store');
|
|
917
|
+
}
|
|
918
|
+
export function initMemoryLifecycleDataset(options, clock = systemClock) {
|
|
919
|
+
const common = prepareCommon(options);
|
|
920
|
+
const next = buildManifest(options, common, clock);
|
|
921
|
+
return withStoreLock(common.storeRoot, true, () => {
|
|
922
|
+
const path = manifestPath(common.storeRoot);
|
|
923
|
+
if (existsSync(path)) {
|
|
924
|
+
const existing = readManifest(common.storeRoot);
|
|
925
|
+
assertDatasetKeyMatches(existing, common.hmacKey);
|
|
926
|
+
if (existing.source_kind !== next.source_kind)
|
|
927
|
+
fail('dataset_source_mismatch');
|
|
928
|
+
if (existing.consent_ref !== next.consent_ref)
|
|
929
|
+
fail('dataset_consent_mismatch');
|
|
930
|
+
if (!sameFrozenDataset(existing, next))
|
|
931
|
+
fail('dataset_already_initialized');
|
|
932
|
+
return {
|
|
933
|
+
schema: 'memory_lifecycle_cli_result.v1',
|
|
934
|
+
ok: true,
|
|
935
|
+
command: 'init',
|
|
936
|
+
status: 'unchanged',
|
|
937
|
+
dataset_ref: existing.dataset_ref,
|
|
938
|
+
source_kind: existing.source_kind,
|
|
939
|
+
wait_code_count: existing.measurement_policy.predeclared_external_wait_codes.length,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
writeManagedFileNoOverwrite(common.storeRoot, path, `${JSON.stringify(next, null, 2)}\n`, 'dataset_already_initialized');
|
|
943
|
+
return {
|
|
944
|
+
schema: 'memory_lifecycle_cli_result.v1',
|
|
945
|
+
ok: true,
|
|
946
|
+
command: 'init',
|
|
947
|
+
status: 'recorded',
|
|
948
|
+
dataset_ref: next.dataset_ref,
|
|
949
|
+
source_kind: next.source_kind,
|
|
950
|
+
wait_code_count: next.measurement_policy.predeclared_external_wait_codes.length,
|
|
951
|
+
};
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
function loadLockedStore(common) {
|
|
955
|
+
const manifest = readManifest(common.storeRoot);
|
|
956
|
+
assertDatasetKeyMatches(manifest, common.hmacKey);
|
|
957
|
+
assertDatasetConsentMatches(manifest, common.consentRef);
|
|
958
|
+
const events = readEvents(common.storeRoot, true);
|
|
959
|
+
assertEventsBelongToDataset(manifest, events);
|
|
960
|
+
return { manifest, events, projection: projectLifecycleEvents(events) };
|
|
961
|
+
}
|
|
962
|
+
function completedSubjectFlows(projection, subject) {
|
|
963
|
+
return (projection.flowsBySubject.get(subject) ?? [])
|
|
964
|
+
.filter(flow => flow.completedAt !== undefined)
|
|
965
|
+
.sort(compareFlows);
|
|
966
|
+
}
|
|
967
|
+
function compareFlows(a, b) {
|
|
968
|
+
return parseIsoMs(a.startedAt) - parseIsoMs(b.startedAt) || parseIsoMs(a.completedAt ?? a.startedAt) - parseIsoMs(b.completedAt ?? b.startedAt) || a.flowRef.localeCompare(b.flowRef);
|
|
969
|
+
}
|
|
970
|
+
function activeSubjectFlow(projection, subject) {
|
|
971
|
+
return (projection.flowsBySubject.get(subject) ?? []).find(flow => flow.completedAt === undefined);
|
|
972
|
+
}
|
|
973
|
+
export function startMemoryLifecycleFlow(options, clock = systemClock) {
|
|
974
|
+
const common = prepareCommon(options);
|
|
975
|
+
const subject = subjectRef(common.hmacKey, assertRawLabel(options.subject));
|
|
976
|
+
const flow = flowRef(common.hmacKey, options.subject, assertRawLabel(options.flow));
|
|
977
|
+
const ref = eventRef(common.hmacKey, 'flow_started', [subject, flow]);
|
|
978
|
+
return withStoreLock(common.storeRoot, false, () => {
|
|
979
|
+
const { events, projection } = loadLockedStore(common);
|
|
980
|
+
if (eventExists(events, ref))
|
|
981
|
+
return eventResult('start', 'unchanged', ref);
|
|
982
|
+
if (activeSubjectFlow(projection, subject))
|
|
983
|
+
fail('invalid_transition');
|
|
984
|
+
if (completedSubjectFlows(projection, subject).length >= 2)
|
|
985
|
+
fail('flow_limit_reached');
|
|
986
|
+
if (projection.flows.has(flow))
|
|
987
|
+
fail('invalid_transition');
|
|
988
|
+
const event = {
|
|
989
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
990
|
+
event_ref: ref,
|
|
991
|
+
kind: 'flow_started',
|
|
992
|
+
occurred_at: assertIsoDate(clock.now()),
|
|
993
|
+
consent_ref: common.consentRef,
|
|
994
|
+
subject_ref: subject,
|
|
995
|
+
flow_ref: flow,
|
|
996
|
+
eligible: true,
|
|
997
|
+
};
|
|
998
|
+
appendEvent(common.storeRoot, events, event);
|
|
999
|
+
return eventResult('start', 'recorded', ref);
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
export function startExternalWait(options, clock = systemClock) {
|
|
1003
|
+
const common = prepareCommon(options);
|
|
1004
|
+
const subject = subjectRef(common.hmacKey, assertRawLabel(options.subject));
|
|
1005
|
+
const flow = flowRef(common.hmacKey, options.subject, assertRawLabel(options.flow));
|
|
1006
|
+
const wait = waitRef(common.hmacKey, options.subject, options.flow, assertRawLabel(options.wait));
|
|
1007
|
+
const code = assertRawLabel(options.code, 32);
|
|
1008
|
+
if (!WAIT_CODE_PATTERN.test(code))
|
|
1009
|
+
fail('invalid_input');
|
|
1010
|
+
const ref = eventRef(common.hmacKey, 'external_wait_started', [subject, flow, wait, code]);
|
|
1011
|
+
return withStoreLock(common.storeRoot, false, () => {
|
|
1012
|
+
const { manifest, events, projection } = loadLockedStore(common);
|
|
1013
|
+
if (eventExists(events, ref))
|
|
1014
|
+
return eventResult('wait-start', 'unchanged', ref);
|
|
1015
|
+
if (!manifest.measurement_policy.predeclared_external_wait_codes.includes(code))
|
|
1016
|
+
fail('invalid_input');
|
|
1017
|
+
const active = activeSubjectFlow(projection, subject);
|
|
1018
|
+
if (!active || active.flowRef !== flow)
|
|
1019
|
+
fail('invalid_transition');
|
|
1020
|
+
if (active.waits.has(wait))
|
|
1021
|
+
fail('invalid_transition');
|
|
1022
|
+
if ([...active.waits.values()].some(existing => existing.completedAt === undefined))
|
|
1023
|
+
fail('invalid_transition');
|
|
1024
|
+
const occurredAt = assertIsoDate(clock.now());
|
|
1025
|
+
if (parseIsoMs(occurredAt) < parseIsoMs(active.startedAt))
|
|
1026
|
+
fail('invalid_transition');
|
|
1027
|
+
const event = {
|
|
1028
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
1029
|
+
event_ref: ref,
|
|
1030
|
+
kind: 'external_wait_started',
|
|
1031
|
+
occurred_at: occurredAt,
|
|
1032
|
+
consent_ref: common.consentRef,
|
|
1033
|
+
subject_ref: subject,
|
|
1034
|
+
flow_ref: flow,
|
|
1035
|
+
wait_ref: wait,
|
|
1036
|
+
code,
|
|
1037
|
+
};
|
|
1038
|
+
appendEvent(common.storeRoot, events, event);
|
|
1039
|
+
return eventResult('wait-start', 'recorded', ref);
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
export function endExternalWait(options, clock = systemClock) {
|
|
1043
|
+
const common = prepareCommon(options);
|
|
1044
|
+
const subject = subjectRef(common.hmacKey, assertRawLabel(options.subject));
|
|
1045
|
+
const flow = flowRef(common.hmacKey, options.subject, assertRawLabel(options.flow));
|
|
1046
|
+
const wait = waitRef(common.hmacKey, options.subject, options.flow, assertRawLabel(options.wait));
|
|
1047
|
+
const ref = eventRef(common.hmacKey, 'external_wait_ended', [subject, flow, wait]);
|
|
1048
|
+
return withStoreLock(common.storeRoot, false, () => {
|
|
1049
|
+
const { events, projection } = loadLockedStore(common);
|
|
1050
|
+
if (eventExists(events, ref))
|
|
1051
|
+
return eventResult('wait-end', 'unchanged', ref);
|
|
1052
|
+
const active = activeSubjectFlow(projection, subject);
|
|
1053
|
+
const interval = active?.waits.get(wait);
|
|
1054
|
+
if (!active || active.flowRef !== flow || !interval || interval.completedAt !== undefined)
|
|
1055
|
+
fail('invalid_transition');
|
|
1056
|
+
const occurredAt = assertIsoDate(clock.now());
|
|
1057
|
+
if (parseIsoMs(occurredAt) <= parseIsoMs(interval.startedAt))
|
|
1058
|
+
fail('invalid_transition');
|
|
1059
|
+
const event = {
|
|
1060
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
1061
|
+
event_ref: ref,
|
|
1062
|
+
kind: 'external_wait_ended',
|
|
1063
|
+
occurred_at: occurredAt,
|
|
1064
|
+
consent_ref: common.consentRef,
|
|
1065
|
+
subject_ref: subject,
|
|
1066
|
+
flow_ref: flow,
|
|
1067
|
+
wait_ref: wait,
|
|
1068
|
+
};
|
|
1069
|
+
appendEvent(common.storeRoot, events, event);
|
|
1070
|
+
return eventResult('wait-end', 'recorded', ref);
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
export function completeMemoryLifecycleFlow(options, clock = systemClock) {
|
|
1074
|
+
const common = prepareCommon(options);
|
|
1075
|
+
const subject = subjectRef(common.hmacKey, assertRawLabel(options.subject));
|
|
1076
|
+
const flow = flowRef(common.hmacKey, options.subject, assertRawLabel(options.flow));
|
|
1077
|
+
const ref = eventRef(common.hmacKey, 'flow_completed', [subject, flow]);
|
|
1078
|
+
return withStoreLock(common.storeRoot, false, () => {
|
|
1079
|
+
const { events, projection } = loadLockedStore(common);
|
|
1080
|
+
if (eventExists(events, ref))
|
|
1081
|
+
return eventResult('complete', 'unchanged', ref);
|
|
1082
|
+
const active = activeSubjectFlow(projection, subject);
|
|
1083
|
+
if (!active || active.flowRef !== flow)
|
|
1084
|
+
fail('invalid_transition');
|
|
1085
|
+
if ([...active.waits.values()].some(wait => wait.completedAt === undefined))
|
|
1086
|
+
fail('invalid_transition');
|
|
1087
|
+
const occurredAt = assertIsoDate(clock.now());
|
|
1088
|
+
if (parseIsoMs(occurredAt) <= parseIsoMs(active.startedAt))
|
|
1089
|
+
fail('invalid_transition');
|
|
1090
|
+
const event = {
|
|
1091
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
1092
|
+
event_ref: ref,
|
|
1093
|
+
kind: 'flow_completed',
|
|
1094
|
+
occurred_at: occurredAt,
|
|
1095
|
+
consent_ref: common.consentRef,
|
|
1096
|
+
subject_ref: subject,
|
|
1097
|
+
flow_ref: flow,
|
|
1098
|
+
status: 'completed',
|
|
1099
|
+
};
|
|
1100
|
+
appendEvent(common.storeRoot, events, event);
|
|
1101
|
+
return eventResult('complete', 'recorded', ref);
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
export function recordExperienceReflux(options, clock = systemClock) {
|
|
1105
|
+
const common = prepareCommon(options);
|
|
1106
|
+
const kind = assertRefluxKind(options.kind);
|
|
1107
|
+
const experience = experienceRef(common.hmacKey, assertRawLabel(options.experience));
|
|
1108
|
+
const evidence = evidenceRef(common.hmacKey, assertRawLabel(options.evidence, 2048));
|
|
1109
|
+
const ref = eventRef(common.hmacKey, 'experience_reflux_recorded', [kind, experience, evidence]);
|
|
1110
|
+
return withStoreLock(common.storeRoot, false, () => {
|
|
1111
|
+
const { events, projection } = loadLockedStore(common);
|
|
1112
|
+
if (eventExists(events, ref))
|
|
1113
|
+
return eventResult('record-reflux', 'unchanged', ref);
|
|
1114
|
+
if (projection.refluxEvents.some(event => event.experienceRef === experience && event.refluxKind === kind))
|
|
1115
|
+
fail('invalid_transition');
|
|
1116
|
+
if (kind === 'verified_outcome' && !projection.refluxEvents.some(event => event.experienceRef === experience && event.refluxKind === 'recalled')) {
|
|
1117
|
+
fail('invalid_transition');
|
|
1118
|
+
}
|
|
1119
|
+
const event = {
|
|
1120
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
1121
|
+
event_ref: ref,
|
|
1122
|
+
kind: 'experience_reflux_recorded',
|
|
1123
|
+
occurred_at: assertIsoDate(clock.now()),
|
|
1124
|
+
consent_ref: common.consentRef,
|
|
1125
|
+
reflux_kind: kind,
|
|
1126
|
+
experience_ref: experience,
|
|
1127
|
+
evidence_ref: evidence,
|
|
1128
|
+
};
|
|
1129
|
+
appendEvent(common.storeRoot, events, event);
|
|
1130
|
+
return eventResult('record-reflux', 'recorded', ref);
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
export function recordPreferenceAssertion(options, clock = systemClock) {
|
|
1134
|
+
const common = prepareCommon(options);
|
|
1135
|
+
const consumer = assertPreferenceConsumer(options.consumer);
|
|
1136
|
+
const assertion = assertionRef(common.hmacKey, assertRawLabel(options.assertion));
|
|
1137
|
+
const evidence = evidenceRef(common.hmacKey, assertRawLabel(options.evidence, 2048));
|
|
1138
|
+
const ref = eventRef(common.hmacKey, 'preference_recorded', [assertion, evidence, consumer]);
|
|
1139
|
+
return withStoreLock(common.storeRoot, false, () => {
|
|
1140
|
+
const { events, projection } = loadLockedStore(common);
|
|
1141
|
+
if (eventExists(events, ref))
|
|
1142
|
+
return eventResult('record-preference', 'unchanged', ref);
|
|
1143
|
+
if (projection.preferences.some(preference => preference.assertionRef === assertion))
|
|
1144
|
+
fail('invalid_transition');
|
|
1145
|
+
const event = {
|
|
1146
|
+
schema_version: MEMORY_LIFECYCLE_EVENT_SCHEMA,
|
|
1147
|
+
event_ref: ref,
|
|
1148
|
+
kind: 'preference_recorded',
|
|
1149
|
+
occurred_at: assertIsoDate(clock.now()),
|
|
1150
|
+
consent_ref: common.consentRef,
|
|
1151
|
+
assertion_ref: assertion,
|
|
1152
|
+
evidence_ref: evidence,
|
|
1153
|
+
consumer,
|
|
1154
|
+
hard_filter: false,
|
|
1155
|
+
};
|
|
1156
|
+
appendEvent(common.storeRoot, events, event);
|
|
1157
|
+
return eventResult('record-preference', 'recorded', ref);
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
function eventResult(command, status, ref) {
|
|
1161
|
+
return { schema: 'memory_lifecycle_cli_result.v1', ok: true, command, status, event_ref: ref };
|
|
1162
|
+
}
|
|
1163
|
+
export function buildMemoryValueFixture(manifest, events, hmacKey) {
|
|
1164
|
+
assertDatasetKeyMatches(manifest, hmacKey);
|
|
1165
|
+
assertEventsBelongToDataset(manifest, events);
|
|
1166
|
+
const projection = projectLifecycleEvents(events);
|
|
1167
|
+
const pairs = [];
|
|
1168
|
+
for (const [subject, flows] of [...projection.flowsBySubject.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1169
|
+
const completed = flows.filter(flow => flow.completedAt !== undefined).sort(compareFlows);
|
|
1170
|
+
if (completed.length < 2)
|
|
1171
|
+
continue;
|
|
1172
|
+
const first = completed[0];
|
|
1173
|
+
const returning = completed[1];
|
|
1174
|
+
if (parseIsoMs(returning.startedAt) <= parseIsoMs(first.completedAt))
|
|
1175
|
+
fail('invalid_store');
|
|
1176
|
+
const pairId = pseudonymousRef(hmacKey, 'pair', [subject, first.flowRef, returning.flowRef]);
|
|
1177
|
+
pairs.push({
|
|
1178
|
+
pair_id: pairId,
|
|
1179
|
+
subject_ref: subject,
|
|
1180
|
+
first: flowToMemoryValue(first, 1),
|
|
1181
|
+
returning: flowToMemoryValue(returning, 2),
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
const base = {
|
|
1185
|
+
schema: MEMORY_VALUE_FIXTURE_SCHEMA,
|
|
1186
|
+
evidence_kind: manifest.source_kind,
|
|
1187
|
+
measurement_policy: manifest.measurement_policy,
|
|
1188
|
+
pairs,
|
|
1189
|
+
experience_reflux_events: projection.refluxEvents
|
|
1190
|
+
.slice()
|
|
1191
|
+
.sort((a, b) => a.experienceRef.localeCompare(b.experienceRef) || a.refluxKind.localeCompare(b.refluxKind))
|
|
1192
|
+
.map(event => ({ experience_id: event.experienceRef, kind: event.refluxKind, evidence_ref: event.evidenceRef })),
|
|
1193
|
+
preference_assertions: projection.preferences
|
|
1194
|
+
.slice()
|
|
1195
|
+
.sort((a, b) => a.assertionRef.localeCompare(b.assertionRef))
|
|
1196
|
+
.map(preference => ({
|
|
1197
|
+
assertion_id: preference.assertionRef,
|
|
1198
|
+
evidence_ref: preference.evidenceRef,
|
|
1199
|
+
consumer: preference.consumer,
|
|
1200
|
+
hard_filter: false,
|
|
1201
|
+
})),
|
|
1202
|
+
p4: manifest.p4,
|
|
1203
|
+
};
|
|
1204
|
+
const summaryDigest = memoryValueSummaryDigest(base);
|
|
1205
|
+
return {
|
|
1206
|
+
schema: base.schema,
|
|
1207
|
+
evidence_kind: base.evidence_kind,
|
|
1208
|
+
source_review: sourceReviewFor(manifest.source_kind, summaryDigest),
|
|
1209
|
+
measurement_policy: base.measurement_policy,
|
|
1210
|
+
pairs: base.pairs,
|
|
1211
|
+
experience_reflux_events: base.experience_reflux_events,
|
|
1212
|
+
preference_assertions: base.preference_assertions,
|
|
1213
|
+
p4: base.p4,
|
|
1214
|
+
};
|
|
1215
|
+
}
|
|
1216
|
+
function flowToMemoryValue(flow, eligiblePlanningIndex) {
|
|
1217
|
+
if (!flow.completedAt)
|
|
1218
|
+
fail('invalid_store');
|
|
1219
|
+
const waits = [...flow.waits.values()]
|
|
1220
|
+
.sort((a, b) => parseIsoMs(a.startedAt) - parseIsoMs(b.startedAt) || a.waitRef.localeCompare(b.waitRef))
|
|
1221
|
+
.map(wait => {
|
|
1222
|
+
if (!wait.completedAt)
|
|
1223
|
+
fail('invalid_store');
|
|
1224
|
+
return { code: wait.code, started_at: wait.startedAt, completed_at: wait.completedAt };
|
|
1225
|
+
});
|
|
1226
|
+
for (let index = 1; index < waits.length; index += 1) {
|
|
1227
|
+
if (parseIsoMs(waits[index].started_at) < parseIsoMs(waits[index - 1].completed_at))
|
|
1228
|
+
fail('invalid_store');
|
|
1229
|
+
}
|
|
1230
|
+
return {
|
|
1231
|
+
flow_id: flow.flowRef,
|
|
1232
|
+
eligible_planning_index: eligiblePlanningIndex,
|
|
1233
|
+
eligible: true,
|
|
1234
|
+
status: 'completed',
|
|
1235
|
+
started_at: flow.startedAt,
|
|
1236
|
+
completed_at: flow.completedAt,
|
|
1237
|
+
external_waits: waits,
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function sourceReviewFor(sourceKind, summaryDigest) {
|
|
1241
|
+
if (!SHA256_PATTERN.test(summaryDigest))
|
|
1242
|
+
fail('internal_error');
|
|
1243
|
+
if (sourceKind === 'synthetic_fixture') {
|
|
1244
|
+
return {
|
|
1245
|
+
schema_version: MEMORY_VALUE_SOURCE_REVIEW_SCHEMA,
|
|
1246
|
+
state: 'not_required_for_synthetic',
|
|
1247
|
+
reviewed_at: null,
|
|
1248
|
+
reviewer_ref: null,
|
|
1249
|
+
attestation_ref: null,
|
|
1250
|
+
private_source_digest_sha256: null,
|
|
1251
|
+
reviewed_summary_digest_sha256: null,
|
|
1252
|
+
checks: {
|
|
1253
|
+
raw_private_material_excluded: false,
|
|
1254
|
+
paired_cohort_source_reviewed: false,
|
|
1255
|
+
summary_matches_private_source: false,
|
|
1256
|
+
no_synthetic_or_test_subjects: false,
|
|
1257
|
+
},
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
return {
|
|
1261
|
+
schema_version: MEMORY_VALUE_SOURCE_REVIEW_SCHEMA,
|
|
1262
|
+
state: 'candidate',
|
|
1263
|
+
reviewed_at: null,
|
|
1264
|
+
reviewer_ref: null,
|
|
1265
|
+
attestation_ref: null,
|
|
1266
|
+
private_source_digest_sha256: null,
|
|
1267
|
+
reviewed_summary_digest_sha256: summaryDigest,
|
|
1268
|
+
checks: {
|
|
1269
|
+
raw_private_material_excluded: true,
|
|
1270
|
+
paired_cohort_source_reviewed: false,
|
|
1271
|
+
summary_matches_private_source: false,
|
|
1272
|
+
no_synthetic_or_test_subjects: false,
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
export function exportMemoryLifecycleFixture(options) {
|
|
1277
|
+
const common = prepareCommon(options);
|
|
1278
|
+
return withStoreLock(common.storeRoot, false, () => {
|
|
1279
|
+
const { manifest, events } = loadLockedStore(common);
|
|
1280
|
+
const fixture = buildMemoryValueFixture(manifest, events, common.hmacKey);
|
|
1281
|
+
if (options.out) {
|
|
1282
|
+
const outputPath = resolve(options.out);
|
|
1283
|
+
writePrivateFileNoOverwrite(outputPath, `${JSON.stringify(fixture, null, 2)}\n`);
|
|
1284
|
+
}
|
|
1285
|
+
return {
|
|
1286
|
+
fixture,
|
|
1287
|
+
result: {
|
|
1288
|
+
schema: 'memory_lifecycle_cli_result.v1',
|
|
1289
|
+
ok: true,
|
|
1290
|
+
command: 'export',
|
|
1291
|
+
status: 'recorded',
|
|
1292
|
+
dataset_ref: manifest.dataset_ref,
|
|
1293
|
+
source_kind: manifest.source_kind,
|
|
1294
|
+
pair_count: fixture.pairs.length,
|
|
1295
|
+
output_written: Boolean(options.out),
|
|
1296
|
+
},
|
|
1297
|
+
};
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
export function readStoreForTests(stateRoot) {
|
|
1301
|
+
const safeRoot = assertSafeStateRoot(stateRoot);
|
|
1302
|
+
const storeRoot = join(safeRoot, 'gotry-state', 'memory-lifecycle');
|
|
1303
|
+
return { manifest: readManifest(storeRoot), events: readEvents(storeRoot) };
|
|
1304
|
+
}
|
|
1305
|
+
export function storeRootForStateRoot(stateRoot) {
|
|
1306
|
+
return join(assertSafeStateRoot(stateRoot), 'gotry-state', 'memory-lifecycle');
|
|
1307
|
+
}
|
|
1308
|
+
export function fileMode(path) {
|
|
1309
|
+
return statSync(path).mode & 0o777;
|
|
1310
|
+
}
|