@openprose/reactor-cradle 0.1.0-rc.1
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/LICENSE +21 -0
- package/README.md +270 -0
- package/dist/assert/index.d.ts +35 -0
- package/dist/assert/index.d.ts.map +1 -0
- package/dist/assert/index.js +398 -0
- package/dist/baselines/cost-thesis/index.d.ts +103 -0
- package/dist/baselines/cost-thesis/index.d.ts.map +1 -0
- package/dist/baselines/cost-thesis/index.js +337 -0
- package/dist/baselines/naive-loop/index.d.ts +46 -0
- package/dist/baselines/naive-loop/index.d.ts.map +1 -0
- package/dist/baselines/naive-loop/index.js +188 -0
- package/dist/baselines/no-memo/index.d.ts +84 -0
- package/dist/baselines/no-memo/index.d.ts.map +1 -0
- package/dist/baselines/no-memo/index.js +226 -0
- package/dist/doubles/clock.d.ts +9 -0
- package/dist/doubles/clock.d.ts.map +1 -0
- package/dist/doubles/clock.js +42 -0
- package/dist/doubles/storage.d.ts +24 -0
- package/dist/doubles/storage.d.ts.map +1 -0
- package/dist/doubles/storage.js +191 -0
- package/dist/eval/index.d.ts +204 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +596 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/policy-author/index.d.ts +103 -0
- package/dist/policy-author/index.d.ts.map +1 -0
- package/dist/policy-author/index.js +358 -0
- package/dist/policy-drift/index.d.ts +64 -0
- package/dist/policy-drift/index.d.ts.map +1 -0
- package/dist/policy-drift/index.js +495 -0
- package/dist/policy-replay/index.d.ts +106 -0
- package/dist/policy-replay/index.d.ts.map +1 -0
- package/dist/policy-replay/index.js +361 -0
- package/dist/provider-parity/index.d.ts +91 -0
- package/dist/provider-parity/index.d.ts.map +1 -0
- package/dist/provider-parity/index.js +439 -0
- package/dist/recompile/index.d.ts +161 -0
- package/dist/recompile/index.d.ts.map +1 -0
- package/dist/recompile/index.js +690 -0
- package/dist/release-candidate/index.d.ts +139 -0
- package/dist/release-candidate/index.d.ts.map +1 -0
- package/dist/release-candidate/index.js +553 -0
- package/dist/release-parity/index.d.ts +80 -0
- package/dist/release-parity/index.d.ts.map +1 -0
- package/dist/release-parity/index.js +1035 -0
- package/dist/replay/model-gateway.d.ts +25 -0
- package/dist/replay/model-gateway.d.ts.map +1 -0
- package/dist/replay/model-gateway.js +166 -0
- package/dist/replay/parity.d.ts +110 -0
- package/dist/replay/parity.d.ts.map +1 -0
- package/dist/replay/parity.js +232 -0
- package/dist/rollback/index.d.ts +106 -0
- package/dist/rollback/index.d.ts.map +1 -0
- package/dist/rollback/index.js +694 -0
- package/dist/scenario/parser.d.ts +11 -0
- package/dist/scenario/parser.d.ts.map +1 -0
- package/dist/scenario/parser.js +490 -0
- package/dist/scenario/runner.d.ts +12 -0
- package/dist/scenario/runner.d.ts.map +1 -0
- package/dist/scenario/runner.js +345 -0
- package/dist/scenario/synthetic-world-adapter.d.ts +4 -0
- package/dist/scenario/synthetic-world-adapter.d.ts.map +1 -0
- package/dist/scenario/synthetic-world-adapter.js +82 -0
- package/dist/scenario/time.d.ts +4 -0
- package/dist/scenario/time.d.ts.map +1 -0
- package/dist/scenario/time.js +45 -0
- package/dist/scenario/types.d.ts +149 -0
- package/dist/scenario/types.d.ts.map +1 -0
- package/dist/scenario/types.js +5 -0
- package/dist/spikes/index.d.ts +10 -0
- package/dist/spikes/index.d.ts.map +1 -0
- package/dist/spikes/index.js +29 -0
- package/dist/spikes/k1-ensemble-spread.d.ts +88 -0
- package/dist/spikes/k1-ensemble-spread.d.ts.map +1 -0
- package/dist/spikes/k1-ensemble-spread.js +396 -0
- package/dist/spikes/k2-policy-author.d.ts +25 -0
- package/dist/spikes/k2-policy-author.d.ts.map +1 -0
- package/dist/spikes/k2-policy-author.js +503 -0
- package/dist/spikes/live-refresh.d.ts +150 -0
- package/dist/spikes/live-refresh.d.ts.map +1 -0
- package/dist/spikes/live-refresh.js +511 -0
- package/dist/world/index.d.ts +2 -0
- package/dist/world/index.d.ts.map +1 -0
- package/dist/world/index.js +17 -0
- package/dist/world/synthetic-world.d.ts +104 -0
- package/dist/world/synthetic-world.d.ts.map +1 -0
- package/dist/world/synthetic-world.js +449 -0
- package/package.json +139 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NO_MEMO_BASELINE_GENERATED_AT_V0 = exports.NO_MEMO_BASELINE_SOURCE_FIXTURE_V0 = exports.NO_MEMO_BASELINE_CASSETTE_PATH_V0 = exports.NO_MEMO_BASELINE_WORLD_PROFILE_V0 = exports.NO_MEMO_BASELINE_SCENARIO_ID_V0 = exports.NO_MEMO_BASELINE_VARIANT_V0 = exports.NO_MEMO_BASELINE_SUMMARY_VERSION_V0 = exports.NO_MEMO_BASELINE_SUMMARY_SCHEMA_V0 = void 0;
|
|
4
|
+
exports.createNoMemoStaticBaselineFromReactorRunV0 = createNoMemoStaticBaselineFromReactorRunV0;
|
|
5
|
+
exports.runNoMemoW7StaticBaselineV0 = runNoMemoW7StaticBaselineV0;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
exports.NO_MEMO_BASELINE_SUMMARY_SCHEMA_V0 = "openprose.reactor-cradle.baseline.no-memo-summary";
|
|
8
|
+
exports.NO_MEMO_BASELINE_SUMMARY_VERSION_V0 = 0;
|
|
9
|
+
exports.NO_MEMO_BASELINE_VARIANT_V0 = "reactor-no-memo";
|
|
10
|
+
exports.NO_MEMO_BASELINE_SCENARIO_ID_V0 = "incident-briefing-static-zero";
|
|
11
|
+
exports.NO_MEMO_BASELINE_WORLD_PROFILE_V0 = "static";
|
|
12
|
+
exports.NO_MEMO_BASELINE_CASSETTE_PATH_V0 = "./c2-static-zero.model-cassette.json";
|
|
13
|
+
exports.NO_MEMO_BASELINE_SOURCE_FIXTURE_V0 = "src/__tests__/fixtures/c2-static-zero.scenario";
|
|
14
|
+
exports.NO_MEMO_BASELINE_GENERATED_AT_V0 = "2026-05-20T00:00:00.000Z";
|
|
15
|
+
const OBSERVED_W7_STATIC_RECEIPT_PROFILES = Object.freeze([
|
|
16
|
+
{
|
|
17
|
+
as_of: "2026-05-18T12:00:00.000Z",
|
|
18
|
+
event_cause: "real-input",
|
|
19
|
+
source_provider: "cradle",
|
|
20
|
+
source_model: "deterministic-bootstrap",
|
|
21
|
+
source_tokens: tokensFromFreshReused(41, 0),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
as_of: "2026-05-18T12:15:00.000Z",
|
|
25
|
+
event_cause: "forecast-recheck",
|
|
26
|
+
recheck_kind: "evidence-age",
|
|
27
|
+
source_provider: "memo",
|
|
28
|
+
source_model: "memo",
|
|
29
|
+
source_tokens: tokensFromFreshReused(0, 41),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
as_of: "2026-05-18T18:00:00.000Z",
|
|
33
|
+
event_cause: "forecast-recheck",
|
|
34
|
+
recheck_kind: "plan-age",
|
|
35
|
+
source_provider: "cradle",
|
|
36
|
+
source_model: "deterministic-plan-audit",
|
|
37
|
+
source_tokens: tokensFromFreshReused(5, 0),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
as_of: "2026-05-19T12:00:00.000Z",
|
|
41
|
+
event_cause: "forecast-recheck",
|
|
42
|
+
recheck_kind: "evidence-age",
|
|
43
|
+
source_provider: "memo",
|
|
44
|
+
source_model: "memo",
|
|
45
|
+
source_tokens: tokensFromFreshReused(0, 5),
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
const BASELINE_NOTES = Object.freeze([
|
|
49
|
+
"Derived from the Reactor static-world receipt schedule for the same incident-briefing-static-zero scenario and cassette.",
|
|
50
|
+
"The ablation leaves the Reactor forecast/status schedule intact, but disables memo credit by charging every token-bearing receipt total as fresh judge work.",
|
|
51
|
+
"Memo-hit receipts are treated as memo misses: reused tokens become fresh tokens and model_invocation_count increments for that turn.",
|
|
52
|
+
]);
|
|
53
|
+
function createNoMemoStaticBaselineFromReactorRunV0(input) {
|
|
54
|
+
const run = input.reactor_run;
|
|
55
|
+
assertExpectedStaticRun(run);
|
|
56
|
+
return createNoMemoStaticSummary({
|
|
57
|
+
initial_instant: run.initial_instant,
|
|
58
|
+
final_instant: run.final_instant,
|
|
59
|
+
source_fixture: input.source_fixture ?? exports.NO_MEMO_BASELINE_SOURCE_FIXTURE_V0,
|
|
60
|
+
scenario_script_turn_count: run.trace.length,
|
|
61
|
+
turns: run.receipt_log.entries.map((receipt, index) => createNoMemoTurnFromReceipt(receipt, index)),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function runNoMemoW7StaticBaselineV0(input = {}) {
|
|
65
|
+
if (input.reactor_run !== undefined) {
|
|
66
|
+
return createNoMemoStaticBaselineFromReactorRunV0({
|
|
67
|
+
reactor_run: input.reactor_run,
|
|
68
|
+
...(input.source_fixture === undefined
|
|
69
|
+
? {}
|
|
70
|
+
: { source_fixture: input.source_fixture }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return createNoMemoStaticSummary({
|
|
74
|
+
initial_instant: "2026-05-18T12:00:00.000Z",
|
|
75
|
+
final_instant: "2026-05-19T12:00:00.000Z",
|
|
76
|
+
source_fixture: input.source_fixture ?? exports.NO_MEMO_BASELINE_SOURCE_FIXTURE_V0,
|
|
77
|
+
scenario_script_turn_count: 5,
|
|
78
|
+
turns: OBSERVED_W7_STATIC_RECEIPT_PROFILES.map((profile, index) => createNoMemoTurnFromObservedProfile(profile, index)),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function createNoMemoStaticSummary(input) {
|
|
82
|
+
const tokens = sumTurnTokens(input.turns);
|
|
83
|
+
const payload = {
|
|
84
|
+
schema: exports.NO_MEMO_BASELINE_SUMMARY_SCHEMA_V0,
|
|
85
|
+
v: exports.NO_MEMO_BASELINE_SUMMARY_VERSION_V0,
|
|
86
|
+
summary_type: "runtime-derived-static-no-memo-replay",
|
|
87
|
+
generated_at: exports.NO_MEMO_BASELINE_GENERATED_AT_V0,
|
|
88
|
+
variant: exports.NO_MEMO_BASELINE_VARIANT_V0,
|
|
89
|
+
scenario: {
|
|
90
|
+
id: exports.NO_MEMO_BASELINE_SCENARIO_ID_V0,
|
|
91
|
+
profile: exports.NO_MEMO_BASELINE_WORLD_PROFILE_V0,
|
|
92
|
+
initial_instant: input.initial_instant,
|
|
93
|
+
final_instant: input.final_instant,
|
|
94
|
+
source_fixture: input.source_fixture,
|
|
95
|
+
cassette_path: exports.NO_MEMO_BASELINE_CASSETTE_PATH_V0,
|
|
96
|
+
scenario_script_turn_count: input.scenario_script_turn_count,
|
|
97
|
+
},
|
|
98
|
+
receipt_count: input.turns.length,
|
|
99
|
+
turn_count: input.turns.length,
|
|
100
|
+
memo_hit_equivalent_count: input.turns.filter((turn) => turn.runtime_equivalent === "memo-hit-equivalent").length,
|
|
101
|
+
model_invocation_count: sumModelInvocations(input.turns),
|
|
102
|
+
tokens,
|
|
103
|
+
ratio: tokenRatio(tokens),
|
|
104
|
+
assumptions: {
|
|
105
|
+
replay_mode: "same-runtime-receipt-log-with-memo-disabled-equivalent",
|
|
106
|
+
memo_hit_handling: "token-bearing-receipts-charge-total-as-fresh",
|
|
107
|
+
runtime_behavior_changed: false,
|
|
108
|
+
excluded_invocations: [
|
|
109
|
+
"scenario setup cassette model step at t=0 is not a Reactor receipt and is excluded from the baseline ratio",
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
turns: Object.freeze([...input.turns]),
|
|
113
|
+
notes: BASELINE_NOTES,
|
|
114
|
+
};
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
...payload,
|
|
117
|
+
content_hash: contentHash(payload),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function createNoMemoTurnFromReceipt(receipt, index) {
|
|
121
|
+
const sourceTokens = tokensFromFreshReused(receipt.cost.tokens.fresh, receipt.cost.tokens.reused);
|
|
122
|
+
return createNoMemoTurn({
|
|
123
|
+
index,
|
|
124
|
+
as_of: receipt.core.as_of,
|
|
125
|
+
event_cause: receipt.core.event_cause,
|
|
126
|
+
...(receipt.core.recheck_kind === undefined
|
|
127
|
+
? {}
|
|
128
|
+
: { recheck_kind: receipt.core.recheck_kind }),
|
|
129
|
+
source_provider: receipt.cost.provider,
|
|
130
|
+
source_model: receipt.cost.model,
|
|
131
|
+
source_tokens: sourceTokens,
|
|
132
|
+
source_receipt_hash: receipt.content_hash,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function createNoMemoTurnFromObservedProfile(profile, index) {
|
|
136
|
+
return createNoMemoTurn({
|
|
137
|
+
index,
|
|
138
|
+
as_of: profile.as_of,
|
|
139
|
+
event_cause: profile.event_cause,
|
|
140
|
+
...(profile.recheck_kind === undefined
|
|
141
|
+
? {}
|
|
142
|
+
: { recheck_kind: profile.recheck_kind }),
|
|
143
|
+
source_provider: profile.source_provider,
|
|
144
|
+
source_model: profile.source_model,
|
|
145
|
+
source_tokens: profile.source_tokens,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function createNoMemoTurn(input) {
|
|
149
|
+
const tokens = tokensFromFreshReused(input.source_tokens.total, 0);
|
|
150
|
+
const runtimeEquivalent = input.source_provider === "memo"
|
|
151
|
+
? "memo-hit-equivalent"
|
|
152
|
+
: "model-invocation-equivalent";
|
|
153
|
+
return Object.freeze({
|
|
154
|
+
index: input.index,
|
|
155
|
+
as_of: input.as_of,
|
|
156
|
+
event_cause: input.event_cause,
|
|
157
|
+
...(input.recheck_kind === undefined
|
|
158
|
+
? {}
|
|
159
|
+
: { recheck_kind: input.recheck_kind }),
|
|
160
|
+
runtime_equivalent: runtimeEquivalent,
|
|
161
|
+
no_memo_outcome: "fresh-judge",
|
|
162
|
+
model_invocation_count: tokens.total === 0 ? 0 : 1,
|
|
163
|
+
tokens,
|
|
164
|
+
source_tokens: input.source_tokens,
|
|
165
|
+
source_provider: input.source_provider,
|
|
166
|
+
source_model: input.source_model,
|
|
167
|
+
...(input.source_receipt_hash === undefined
|
|
168
|
+
? {}
|
|
169
|
+
: { source_receipt_hash: input.source_receipt_hash }),
|
|
170
|
+
deterministic_profile: input.source_model,
|
|
171
|
+
note: noMemoTurnNote(runtimeEquivalent, input.source_tokens),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
function noMemoTurnNote(runtimeEquivalent, sourceTokens) {
|
|
175
|
+
if (runtimeEquivalent === "memo-hit-equivalent") {
|
|
176
|
+
return `Runtime memo-hit receipt is treated as a memo miss; ${sourceTokens.reused} reused token(s) become fresh judge work.`;
|
|
177
|
+
}
|
|
178
|
+
return "Runtime model invocation remains fresh judge work in the no-memo baseline.";
|
|
179
|
+
}
|
|
180
|
+
function assertExpectedStaticRun(run) {
|
|
181
|
+
if (run.scenario_id !== exports.NO_MEMO_BASELINE_SCENARIO_ID_V0) {
|
|
182
|
+
throw new Error(`no-memo static baseline requires scenario_id=${exports.NO_MEMO_BASELINE_SCENARIO_ID_V0}; received ${run.scenario_id}`);
|
|
183
|
+
}
|
|
184
|
+
if (run.world_profile !== exports.NO_MEMO_BASELINE_WORLD_PROFILE_V0) {
|
|
185
|
+
throw new Error(`no-memo static baseline requires world_profile=${exports.NO_MEMO_BASELINE_WORLD_PROFILE_V0}; received ${run.world_profile}`);
|
|
186
|
+
}
|
|
187
|
+
if (run.cassette_path !== exports.NO_MEMO_BASELINE_CASSETTE_PATH_V0) {
|
|
188
|
+
throw new Error(`no-memo static baseline requires cassette_path=${exports.NO_MEMO_BASELINE_CASSETTE_PATH_V0}; received ${run.cassette_path}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function sumTurnTokens(turns) {
|
|
192
|
+
const fresh = turns.reduce((sum, turn) => sum + turn.tokens.fresh, 0);
|
|
193
|
+
const reused = turns.reduce((sum, turn) => sum + turn.tokens.reused, 0);
|
|
194
|
+
return tokensFromFreshReused(fresh, reused);
|
|
195
|
+
}
|
|
196
|
+
function sumModelInvocations(turns) {
|
|
197
|
+
return turns.reduce((sum, turn) => sum + turn.model_invocation_count, 0);
|
|
198
|
+
}
|
|
199
|
+
function tokensFromFreshReused(fresh, reused) {
|
|
200
|
+
assertTokenCount(fresh, "fresh");
|
|
201
|
+
assertTokenCount(reused, "reused");
|
|
202
|
+
return Object.freeze({
|
|
203
|
+
fresh,
|
|
204
|
+
reused,
|
|
205
|
+
total: fresh + reused,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function assertTokenCount(value, label) {
|
|
209
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
210
|
+
throw new Error(`no-memo baseline token count ${label} must be a non-negative integer`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function tokenRatio(tokens) {
|
|
214
|
+
return Object.freeze({
|
|
215
|
+
fresh: tokens.fresh,
|
|
216
|
+
reused: tokens.reused,
|
|
217
|
+
label: `${tokens.fresh}:${tokens.reused}`,
|
|
218
|
+
reused_is_zero: tokens.reused === 0,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function contentHash(payload) {
|
|
222
|
+
const digest = (0, node_crypto_1.createHash)("sha256")
|
|
223
|
+
.update(JSON.stringify(payload))
|
|
224
|
+
.digest("hex");
|
|
225
|
+
return `sha256:${digest}`;
|
|
226
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactorClockAdapterV0 } from "@openprose/reactor/sdk";
|
|
2
|
+
export declare class VirtualClock implements ReactorClockAdapterV0 {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(initialInstant: string);
|
|
5
|
+
now(): string;
|
|
6
|
+
advanceMs(ms: number): void;
|
|
7
|
+
set(instant: string): void;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=clock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../../src/doubles/clock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAKpE,qBAAa,YAAa,YAAW,qBAAqB;;gBAG5C,cAAc,EAAE,MAAM;IAIlC,GAAG,IAAI,MAAM;IAIb,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAkB3B,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;CAG3B"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VirtualClock = void 0;
|
|
4
|
+
const ISO_INSTANT_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/;
|
|
5
|
+
class VirtualClock {
|
|
6
|
+
#epochMs;
|
|
7
|
+
constructor(initialInstant) {
|
|
8
|
+
this.#epochMs = parseReplayableInstant(initialInstant, "initialInstant");
|
|
9
|
+
}
|
|
10
|
+
now() {
|
|
11
|
+
return new Date(this.#epochMs).toISOString();
|
|
12
|
+
}
|
|
13
|
+
advanceMs(ms) {
|
|
14
|
+
if (!Number.isSafeInteger(ms) || ms < 0) {
|
|
15
|
+
throw new RangeError("advanceMs requires a non-negative safe integer millisecond interval");
|
|
16
|
+
}
|
|
17
|
+
const nextEpochMs = this.#epochMs + ms;
|
|
18
|
+
if (!Number.isSafeInteger(nextEpochMs) ||
|
|
19
|
+
!Number.isFinite(new Date(nextEpochMs).getTime())) {
|
|
20
|
+
throw new RangeError("advanceMs would move the clock outside Date range");
|
|
21
|
+
}
|
|
22
|
+
this.#epochMs = nextEpochMs;
|
|
23
|
+
}
|
|
24
|
+
set(instant) {
|
|
25
|
+
this.#epochMs = parseReplayableInstant(instant, "instant");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.VirtualClock = VirtualClock;
|
|
29
|
+
function parseReplayableInstant(instant, label) {
|
|
30
|
+
if (!ISO_INSTANT_PATTERN.test(instant)) {
|
|
31
|
+
throw new RangeError(`${label} must be an ISO-8601 UTC instant`);
|
|
32
|
+
}
|
|
33
|
+
const epochMs = Date.parse(instant);
|
|
34
|
+
if (!Number.isFinite(epochMs)) {
|
|
35
|
+
throw new RangeError(`${label} must be a valid ISO-8601 UTC instant`);
|
|
36
|
+
}
|
|
37
|
+
const canonical = new Date(epochMs).toISOString();
|
|
38
|
+
if (instant !== canonical && instant !== canonical.replace(".000Z", "Z")) {
|
|
39
|
+
throw new RangeError(`${label} must be a valid ISO-8601 UTC instant`);
|
|
40
|
+
}
|
|
41
|
+
return epochMs;
|
|
42
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReceiptV0 } from "@openprose/reactor/receipt";
|
|
2
|
+
import type { ReactorRegistrySnapshotV0, ReactorStorageAdapterV0 } from "@openprose/reactor/sdk";
|
|
3
|
+
export interface FileSystemReactorStorageOptionsV0 {
|
|
4
|
+
readonly rootDir: string;
|
|
5
|
+
readonly registry?: ReactorRegistrySnapshotV0;
|
|
6
|
+
readonly initialReceipts?: readonly ReceiptV0[];
|
|
7
|
+
}
|
|
8
|
+
export declare class InMemoryReactorStorage implements ReactorStorageAdapterV0 {
|
|
9
|
+
#private;
|
|
10
|
+
constructor(registry: ReactorRegistrySnapshotV0, initialReceipts?: readonly ReceiptV0[]);
|
|
11
|
+
appendReceipt(receipt: ReceiptV0): void;
|
|
12
|
+
listReceipts(): readonly ReceiptV0[];
|
|
13
|
+
readRegistry(): ReactorRegistrySnapshotV0;
|
|
14
|
+
}
|
|
15
|
+
export declare class FileSystemReactorStorage implements ReactorStorageAdapterV0 {
|
|
16
|
+
#private;
|
|
17
|
+
static readonly registryFileName = "registry.json";
|
|
18
|
+
static readonly receiptsFileName = "receipts.json";
|
|
19
|
+
constructor(input: FileSystemReactorStorageOptionsV0);
|
|
20
|
+
appendReceipt(receipt: ReceiptV0): void;
|
|
21
|
+
listReceipts(): readonly ReceiptV0[];
|
|
22
|
+
readRegistry(): ReactorRegistrySnapshotV0;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/doubles/storage.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,KAAK,EACV,yBAAyB,EACzB,uBAAuB,EACxB,MAAM,wBAAwB,CAAC;AAEhC,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,CAAC,EAAE,yBAAyB,CAAC;IAC9C,QAAQ,CAAC,eAAe,CAAC,EAAE,SAAS,SAAS,EAAE,CAAC;CACjD;AAED,qBAAa,sBAAuB,YAAW,uBAAuB;;gBAKlE,QAAQ,EAAE,yBAAyB,EACnC,eAAe,GAAE,SAAS,SAAS,EAAO;IAQ5C,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI;IAIvC,YAAY,IAAI,SAAS,SAAS,EAAE;IAMpC,YAAY,IAAI,yBAAyB;CAG1C;AAED,qBAAa,wBAAyB,YAAW,uBAAuB;;IACtE,MAAM,CAAC,QAAQ,CAAC,gBAAgB,mBAAmB;IACnD,MAAM,CAAC,QAAQ,CAAC,gBAAgB,mBAAmB;gBAKvC,KAAK,EAAE,iCAAiC;IAiCpD,aAAa,CAAC,OAAO,EAAE,SAAS,GAAG,IAAI;IASvC,YAAY,IAAI,SAAS,SAAS,EAAE;IAIpC,YAAY,IAAI,yBAAyB;CAG1C"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileSystemReactorStorage = exports.InMemoryReactorStorage = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
class InMemoryReactorStorage {
|
|
7
|
+
#receipts;
|
|
8
|
+
#registry;
|
|
9
|
+
constructor(registry, initialReceipts = []) {
|
|
10
|
+
this.#registry = cloneDeterministicJson(registry, "registry");
|
|
11
|
+
this.#receipts = initialReceipts.map((receipt) => cloneDeterministicJson(receipt, "receipt"));
|
|
12
|
+
}
|
|
13
|
+
appendReceipt(receipt) {
|
|
14
|
+
this.#receipts.push(cloneDeterministicJson(receipt, "receipt"));
|
|
15
|
+
}
|
|
16
|
+
listReceipts() {
|
|
17
|
+
return deepFreezeJson(this.#receipts.map((receipt) => cloneDeterministicJson(receipt, "receipt")));
|
|
18
|
+
}
|
|
19
|
+
readRegistry() {
|
|
20
|
+
return deepFreezeJson(cloneDeterministicJson(this.#registry, "registry"));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
exports.InMemoryReactorStorage = InMemoryReactorStorage;
|
|
24
|
+
class FileSystemReactorStorage {
|
|
25
|
+
static registryFileName = "registry.json";
|
|
26
|
+
static receiptsFileName = "receipts.json";
|
|
27
|
+
#registryPath;
|
|
28
|
+
#receiptsPath;
|
|
29
|
+
constructor(input) {
|
|
30
|
+
assertNonEmptyString(input.rootDir, "rootDir");
|
|
31
|
+
(0, node_fs_1.mkdirSync)(input.rootDir, { recursive: true });
|
|
32
|
+
this.#registryPath = (0, node_path_1.join)(input.rootDir, FileSystemReactorStorage.registryFileName);
|
|
33
|
+
this.#receiptsPath = (0, node_path_1.join)(input.rootDir, FileSystemReactorStorage.receiptsFileName);
|
|
34
|
+
if (input.registry !== undefined) {
|
|
35
|
+
writeJsonFile(this.#registryPath, input.registry, FileSystemReactorStorage.registryFileName);
|
|
36
|
+
}
|
|
37
|
+
const shouldInitializeReceipts = input.initialReceipts !== undefined ||
|
|
38
|
+
(input.registry !== undefined && !(0, node_fs_1.existsSync)(this.#receiptsPath));
|
|
39
|
+
if (shouldInitializeReceipts) {
|
|
40
|
+
writeJsonFile(this.#receiptsPath, input.initialReceipts ?? [], FileSystemReactorStorage.receiptsFileName);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
appendReceipt(receipt) {
|
|
44
|
+
const receipts = readReceiptsFile(this.#receiptsPath);
|
|
45
|
+
writeJsonFile(this.#receiptsPath, [...receipts, cloneDeterministicJson(receipt, "receipt")], FileSystemReactorStorage.receiptsFileName);
|
|
46
|
+
}
|
|
47
|
+
listReceipts() {
|
|
48
|
+
return deepFreezeJson(readReceiptsFile(this.#receiptsPath));
|
|
49
|
+
}
|
|
50
|
+
readRegistry() {
|
|
51
|
+
return deepFreezeJson(readRegistryFile(this.#registryPath));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.FileSystemReactorStorage = FileSystemReactorStorage;
|
|
55
|
+
function readRegistryFile(path) {
|
|
56
|
+
const registry = readJsonRecordFile(path, FileSystemReactorStorage.registryFileName);
|
|
57
|
+
assertStringField(registry, "policy_artifact_namespace", "registry.policy_artifact_namespace");
|
|
58
|
+
assertStringField(registry, "policy_artifact_revision", "registry.policy_artifact_revision");
|
|
59
|
+
return registry;
|
|
60
|
+
}
|
|
61
|
+
function readReceiptsFile(path) {
|
|
62
|
+
const receipts = readJsonArrayFile(path, FileSystemReactorStorage.receiptsFileName);
|
|
63
|
+
receipts.forEach((receipt, index) => {
|
|
64
|
+
if (!isPlainRecord(receipt)) {
|
|
65
|
+
throw new Error(`filesystem storage ${FileSystemReactorStorage.receiptsFileName} entry ${index} must be a JSON object`);
|
|
66
|
+
}
|
|
67
|
+
assertStringField(receipt, "content_hash", `receipts[${index}].content_hash`);
|
|
68
|
+
});
|
|
69
|
+
return receipts;
|
|
70
|
+
}
|
|
71
|
+
function readJsonRecordFile(path, label) {
|
|
72
|
+
const value = readJsonFile(path, label);
|
|
73
|
+
if (!isPlainRecord(value)) {
|
|
74
|
+
throw new Error(`filesystem storage ${label} must contain a JSON object`);
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
function readJsonArrayFile(path, label) {
|
|
79
|
+
const value = readJsonFile(path, label);
|
|
80
|
+
if (!Array.isArray(value)) {
|
|
81
|
+
throw new Error(`filesystem storage ${label} must contain a JSON array`);
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function readJsonFile(path, label) {
|
|
86
|
+
let bytes;
|
|
87
|
+
try {
|
|
88
|
+
bytes = (0, node_fs_1.readFileSync)(path, "utf8");
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
throw new Error(`filesystem storage ${label} read failed: ${errorMessage(error)}`);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(bytes);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw new Error(`filesystem storage ${label} must contain valid JSON: ${errorMessage(error)}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function writeJsonFile(path, value, label) {
|
|
101
|
+
try {
|
|
102
|
+
(0, node_fs_1.writeFileSync)(path, `${renderDeterministicJson(value)}\n`, "utf8");
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throw new Error(`filesystem storage ${label} write failed: ${errorMessage(error)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function cloneDeterministicJson(value, label) {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(renderDeterministicJson(value));
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
throw new Error(`${label} must be deterministic JSON: ${errorMessage(error)}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function deepFreezeJson(value) {
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
value.forEach((item) => deepFreezeJson(item));
|
|
119
|
+
return Object.freeze(value);
|
|
120
|
+
}
|
|
121
|
+
if (isPlainRecord(value)) {
|
|
122
|
+
Object.values(value).forEach((item) => deepFreezeJson(item));
|
|
123
|
+
return Object.freeze(value);
|
|
124
|
+
}
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
function renderDeterministicJson(value) {
|
|
128
|
+
if (value === null) {
|
|
129
|
+
return "null";
|
|
130
|
+
}
|
|
131
|
+
switch (typeof value) {
|
|
132
|
+
case "boolean":
|
|
133
|
+
case "number":
|
|
134
|
+
case "string": {
|
|
135
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
136
|
+
throw new TypeError("Cannot render non-finite numbers");
|
|
137
|
+
}
|
|
138
|
+
const rendered = JSON.stringify(value);
|
|
139
|
+
if (rendered === undefined) {
|
|
140
|
+
throw new TypeError(`Cannot render ${typeof value}`);
|
|
141
|
+
}
|
|
142
|
+
return rendered;
|
|
143
|
+
}
|
|
144
|
+
case "object":
|
|
145
|
+
if (Array.isArray(value)) {
|
|
146
|
+
return `[${value.map((item) => renderDeterministicJson(item)).join(",")}]`;
|
|
147
|
+
}
|
|
148
|
+
if (!isPlainRecord(value)) {
|
|
149
|
+
throw new TypeError("Cannot render non-plain objects");
|
|
150
|
+
}
|
|
151
|
+
return renderDeterministicJsonObject(value);
|
|
152
|
+
case "undefined":
|
|
153
|
+
case "bigint":
|
|
154
|
+
case "function":
|
|
155
|
+
case "symbol":
|
|
156
|
+
throw new TypeError(`Cannot render ${typeof value}`);
|
|
157
|
+
}
|
|
158
|
+
throw new TypeError("Cannot render unknown value");
|
|
159
|
+
}
|
|
160
|
+
function renderDeterministicJsonObject(value) {
|
|
161
|
+
const fields = [];
|
|
162
|
+
for (const key of Object.keys(value).sort()) {
|
|
163
|
+
const item = value[key];
|
|
164
|
+
if (item === undefined) {
|
|
165
|
+
throw new TypeError(`Cannot render undefined field ${key}`);
|
|
166
|
+
}
|
|
167
|
+
fields.push(`${JSON.stringify(key)}:${renderDeterministicJson(item)}`);
|
|
168
|
+
}
|
|
169
|
+
return `{${fields.join(",")}}`;
|
|
170
|
+
}
|
|
171
|
+
function assertStringField(value, key, label) {
|
|
172
|
+
const field = value[key];
|
|
173
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
174
|
+
throw new Error(`filesystem storage ${label} must be a non-empty string`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function assertNonEmptyString(value, label) {
|
|
178
|
+
if (value.trim().length === 0) {
|
|
179
|
+
throw new Error(`${label} must be non-empty`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function isPlainRecord(value) {
|
|
183
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const prototype = Object.getPrototypeOf(value);
|
|
187
|
+
return prototype === Object.prototype || prototype === null;
|
|
188
|
+
}
|
|
189
|
+
function errorMessage(error) {
|
|
190
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
191
|
+
}
|