@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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 +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- 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 +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- 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-tests.js +325 -68
- 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/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- 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 +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- 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 +16 -9
- 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 +547 -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/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/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- 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 +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- 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 +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- 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 +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- 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 +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- 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 +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- 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 +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- 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 +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -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
|
+
}
|