@astrosheep/keiyaku 1.0.0 → 1.0.2
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 +73 -56
- package/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms_v2.js +103 -0
- package/build/agents/harness/activity-values.js +54 -0
- package/build/agents/harness/event-channel.js +55 -20
- package/build/agents/harness/events.js +47 -1
- package/build/agents/harness/execution-handle.js +292 -61
- package/build/agents/harness/index.js +103 -121
- package/build/agents/harness/pump.js +36 -47
- package/build/agents/harness/runtime.js +3 -0
- package/build/agents/launch-snapshot_v2.js +348 -0
- package/build/agents/opencode-sdk.js +42 -169
- package/build/agents/providers/claude-agent-sdk.js +240 -79
- package/build/agents/providers/codex-app-server.js +280 -72
- package/build/agents/providers/codex-sdk.js +159 -41
- package/build/agents/providers/event-type-tail.js +16 -0
- package/build/agents/providers/opencode-sdk.js +165 -59
- package/build/agents/providers/pi.js +254 -42
- package/build/agents/selector_v2.js +122 -0
- package/build/cli/commands/akuma.js +120 -42
- package/build/cli/completion.js +6 -7
- package/build/cli/flags.js +36 -10
- package/build/cli/help.js +42 -26
- package/build/cli/index.js +225 -93
- package/build/cli/parse.js +194 -75
- package/build/cli/projection-address.js +11 -0
- package/build/cli/render/address.js +88 -0
- package/build/cli/render/arc.js +10 -10
- package/build/cli/render/call.js +59 -0
- package/build/cli/render/compact-text.js +3 -0
- package/build/cli/render/errors.js +4 -2
- package/build/cli/render/format.js +6 -0
- package/build/cli/render/line-width.js +70 -0
- package/build/cli/render/misc.js +7 -4
- package/build/cli/render/petition.js +4 -3
- package/build/cli/render/projection-activity.js +201 -0
- package/build/cli/render/shared.js +46 -12
- package/build/cli/render/status.js +59 -10
- package/build/cli/render/tool-ledger-rollup.js +74 -0
- package/build/cli/render/tool-presentation.js +97 -0
- package/build/cli/render/wait.js +229 -0
- package/build/cli/skills-install.js +42 -24
- package/build/cli/subagent-guard.js +1 -1
- package/build/config/akuma-loader_v2.js +222 -0
- package/build/config/env-keys.js +0 -4
- package/build/config/env.js +0 -6
- package/build/config/settings.js +325 -12
- package/build/core/addressing.js +224 -0
- package/build/core/amend.js +30 -47
- package/build/core/arc.js +68 -60
- package/build/core/bind.js +105 -25
- package/build/core/call-persist.js +69 -0
- package/build/core/call.js +658 -0
- package/build/core/claim.js +72 -44
- package/build/core/command-io.js +19 -19
- package/build/core/context.js +11 -36
- package/build/core/draft.js +2 -3
- package/build/core/entry.js +101 -6
- package/build/core/execution-coordinate.js +62 -0
- package/build/core/execution-pact.js +141 -0
- package/build/core/forfeit.js +2 -3
- package/build/core/hints.js +10 -10
- package/build/core/ids.js +6 -0
- package/build/core/ledger.js +16 -1
- package/build/core/petition-claim-gates.js +14 -12
- package/build/core/petition-claim.js +1 -0
- package/build/core/petition-run.js +6 -2
- package/build/core/petition.js +63 -21
- package/build/core/places.js +106 -13
- package/build/core/projection/generation.js +412 -0
- package/build/core/projection/heart.js +316 -0
- package/build/core/projection/identity.js +81 -0
- package/build/core/projection/leash.js +87 -0
- package/build/core/projection/mint.js +154 -0
- package/build/core/projection-activity.js +219 -0
- package/build/core/projection-coordinate.js +35 -0
- package/build/core/projection-core.js +69 -463
- package/build/core/projection-generation-continuation.js +31 -0
- package/build/core/projection-generation-execution.js +148 -0
- package/build/core/projection-generation-identity.js +17 -0
- package/build/core/projection-generation-launcher.js +90 -0
- package/build/core/projection-generation-process.js +43 -0
- package/build/core/projection-generation-runner.js +144 -0
- package/build/core/projection-generation-runtime.js +15 -0
- package/build/core/projection-generation-store.js +707 -0
- package/build/core/projection-identity.js +11 -0
- package/build/core/projection-life-observer.js +87 -0
- package/build/core/projection-life-protocol.js +95 -0
- package/build/core/projection-mint.js +96 -24
- package/build/core/projection-runner-lock.js +230 -0
- package/build/core/projection-status.js +207 -107
- package/build/core/projection-tells.js +401 -0
- package/build/core/projection-wait.js +155 -0
- package/build/core/projection-wake.js +109 -231
- package/build/core/queue_v2.js +342 -0
- package/build/core/registry.js +6 -3
- package/build/core/render.js +63 -1
- package/build/core/renew.js +80 -53
- package/build/core/scope.js +186 -137
- package/build/core/seal.js +16 -19
- package/build/core/status.js +88 -25
- package/build/core/stored-agent-event.js +101 -0
- package/build/core/transcripts.js +214 -228
- package/build/core/verdict.js +27 -15
- package/build/core/worktree-bootstrap.js +187 -0
- package/build/core/worktree-path.js +72 -18
- package/build/flow-error.js +4 -6
- package/build/generated/version.js +1 -1
- package/build/git/branches.js +6 -1
- package/build/index.js +8 -5
- package/build/keiyaku.js +1 -3
- package/package.json +6 -5
- package/skills/keiyaku/SKILL.md +6 -47
- package/skills/keiyaku-akuma/SKILL.md +75 -0
- package/build/agents/effective-policy.js +0 -32
- package/build/agents/index.js +0 -114
- package/build/agents/selector.js +0 -28
- package/build/cli/render/summon.js +0 -17
- package/build/cli/render/tell.js +0 -11
- package/build/config/akuma-loader.js +0 -268
- package/build/core/queue.js +0 -73
- package/build/core/summon-persist.js +0 -73
- package/build/core/summon.js +0 -377
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { GENERATION_VERDICT_STATES, } from "./projection-life-protocol.js";
|
|
5
|
+
import { parseExecutionPact, } from "./execution-pact.js";
|
|
6
|
+
import { configureHeartBusyTimeout } from "./projection/heart.js";
|
|
7
|
+
export { GENERATION_VERDICT_STATES };
|
|
8
|
+
export const PROJECTION_GENERATION_STORE_FILE = "heart";
|
|
9
|
+
export const PROJECTION_GENERATION_SCHEMA_VERSION = 1;
|
|
10
|
+
const SCHEMA_TABLE_SQL = `
|
|
11
|
+
CREATE TABLE generation_schema (
|
|
12
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
13
|
+
version INTEGER NOT NULL CHECK (version = ${PROJECTION_GENERATION_SCHEMA_VERSION})
|
|
14
|
+
) STRICT
|
|
15
|
+
`;
|
|
16
|
+
const RECORD_TABLE_SQL = `
|
|
17
|
+
CREATE TABLE generation_records (
|
|
18
|
+
seq INTEGER PRIMARY KEY CHECK (seq > 0),
|
|
19
|
+
kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'verdict')),
|
|
20
|
+
execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
|
|
21
|
+
facts_json TEXT NOT NULL CHECK (json_valid(facts_json))
|
|
22
|
+
) STRICT
|
|
23
|
+
`;
|
|
24
|
+
const IDENTITY_TABLE_SQL = `
|
|
25
|
+
CREATE TABLE projection_identity (
|
|
26
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
27
|
+
version INTEGER NOT NULL CHECK (version = 1),
|
|
28
|
+
pid INTEGER NOT NULL CHECK (pid >= 0),
|
|
29
|
+
akuma TEXT NOT NULL CHECK (length(trim(akuma)) > 0),
|
|
30
|
+
provider TEXT NOT NULL CHECK (length(trim(provider)) > 0),
|
|
31
|
+
authority_json TEXT NOT NULL CHECK (json_valid(authority_json)),
|
|
32
|
+
binding_json TEXT NOT NULL CHECK (json_valid(binding_json)),
|
|
33
|
+
minted_at TEXT NOT NULL CHECK (length(trim(minted_at)) > 0),
|
|
34
|
+
workspace_root TEXT NOT NULL CHECK (length(trim(workspace_root)) > 0),
|
|
35
|
+
effective_policy_json TEXT CHECK (
|
|
36
|
+
effective_policy_json IS NULL OR json_valid(effective_policy_json)
|
|
37
|
+
)
|
|
38
|
+
) STRICT
|
|
39
|
+
`;
|
|
40
|
+
const TELL_TABLE_SQL = `
|
|
41
|
+
CREATE TABLE projection_tells (
|
|
42
|
+
tell_id TEXT PRIMARY KEY CHECK (length(trim(tell_id)) > 0),
|
|
43
|
+
text TEXT NOT NULL,
|
|
44
|
+
created_at TEXT NOT NULL CHECK (length(trim(created_at)) > 0),
|
|
45
|
+
effort TEXT CHECK (effort IS NULL OR length(trim(effort)) > 0),
|
|
46
|
+
state TEXT NOT NULL CHECK (
|
|
47
|
+
state IN ('inbox', 'inflight', 'submitted', 'consumed', 'delivered')
|
|
48
|
+
),
|
|
49
|
+
execution_id TEXT CHECK (
|
|
50
|
+
execution_id IS NULL OR length(trim(execution_id)) > 0
|
|
51
|
+
),
|
|
52
|
+
fence_json TEXT CHECK (fence_json IS NULL OR json_valid(fence_json)),
|
|
53
|
+
replayed_from_json TEXT CHECK (
|
|
54
|
+
replayed_from_json IS NULL OR json_valid(replayed_from_json)
|
|
55
|
+
),
|
|
56
|
+
delivered_at TEXT CHECK (
|
|
57
|
+
delivered_at IS NULL OR length(trim(delivered_at)) > 0
|
|
58
|
+
)
|
|
59
|
+
) STRICT
|
|
60
|
+
`;
|
|
61
|
+
const VERDICTS = new Set(GENERATION_VERDICT_STATES);
|
|
62
|
+
export class ProjectionGenerationStoreError extends Error {
|
|
63
|
+
code = "PROJECTION_GENERATION_STORE_ERROR";
|
|
64
|
+
constructor(message, options) {
|
|
65
|
+
super(message, options);
|
|
66
|
+
this.name = "ProjectionGenerationStoreError";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function isPlainObject(value) {
|
|
70
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
71
|
+
return false;
|
|
72
|
+
const prototype = Object.getPrototypeOf(value);
|
|
73
|
+
return prototype === Object.prototype || prototype === null;
|
|
74
|
+
}
|
|
75
|
+
function assertJsonValue(value, location, ancestors) {
|
|
76
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
77
|
+
return;
|
|
78
|
+
if (typeof value === "number") {
|
|
79
|
+
if (!Number.isFinite(value))
|
|
80
|
+
throw new ProjectionGenerationStoreError(`${location} contains a non-finite number`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value !== "object") {
|
|
84
|
+
throw new ProjectionGenerationStoreError(`${location} contains a non-JSON value`);
|
|
85
|
+
}
|
|
86
|
+
if (ancestors.has(value))
|
|
87
|
+
throw new ProjectionGenerationStoreError(`${location} contains a cycle`);
|
|
88
|
+
ancestors.add(value);
|
|
89
|
+
try {
|
|
90
|
+
if (Array.isArray(value)) {
|
|
91
|
+
value.forEach((item, index) => assertJsonValue(item, `${location}[${index}]`, ancestors));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!isPlainObject(value))
|
|
95
|
+
throw new ProjectionGenerationStoreError(`${location} must contain only plain JSON objects`);
|
|
96
|
+
for (const [key, item] of Object.entries(value)) {
|
|
97
|
+
assertJsonValue(item, `${location}.${key}`, ancestors);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
ancestors.delete(value);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function cloneFacts(value, location) {
|
|
105
|
+
if (!isPlainObject(value))
|
|
106
|
+
throw new ProjectionGenerationStoreError(`${location} must be a JSON object`);
|
|
107
|
+
assertJsonValue(value, location, new Set());
|
|
108
|
+
return JSON.parse(JSON.stringify(value));
|
|
109
|
+
}
|
|
110
|
+
function assertExecutionId(executionId) {
|
|
111
|
+
if (typeof executionId !== "string" || executionId.trim() === "") {
|
|
112
|
+
throw new ProjectionGenerationStoreError("executionId must be a non-blank string");
|
|
113
|
+
}
|
|
114
|
+
return executionId;
|
|
115
|
+
}
|
|
116
|
+
function assertNonNegativeSafeInteger(value, location) {
|
|
117
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
118
|
+
throw new ProjectionGenerationStoreError(`${location} must be a non-negative safe integer`);
|
|
119
|
+
}
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
function assertPositiveSafeInteger(value, location) {
|
|
123
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
124
|
+
throw new ProjectionGenerationStoreError(`${location} must be a positive safe integer`);
|
|
125
|
+
}
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
function assertIsoTimestamp(value, location) {
|
|
129
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
130
|
+
throw new ProjectionGenerationStoreError(`${location} must be an ISO-8601 timestamp`);
|
|
131
|
+
}
|
|
132
|
+
const milliseconds = Date.parse(value);
|
|
133
|
+
if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString() !== value) {
|
|
134
|
+
throw new ProjectionGenerationStoreError(`${location} must be a canonical ISO-8601 timestamp`);
|
|
135
|
+
}
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
138
|
+
function validateTellFence(value, location) {
|
|
139
|
+
if (!Array.isArray(value))
|
|
140
|
+
throw new ProjectionGenerationStoreError(`${location} must be an array`);
|
|
141
|
+
const seen = new Set();
|
|
142
|
+
return value.map((tellId, index) => {
|
|
143
|
+
if (typeof tellId !== "string" || tellId.trim() === "") {
|
|
144
|
+
throw new ProjectionGenerationStoreError(`${location}[${index}] must be a non-blank string`);
|
|
145
|
+
}
|
|
146
|
+
if (seen.has(tellId))
|
|
147
|
+
throw new ProjectionGenerationStoreError(`${location} contains duplicate tell '${tellId}'`);
|
|
148
|
+
seen.add(tellId);
|
|
149
|
+
return tellId;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function validateLaunchFacts(value, location) {
|
|
153
|
+
const facts = cloneFacts(value, location);
|
|
154
|
+
facts.createdAt = assertIsoTimestamp(facts.createdAt, `${location}.createdAt`);
|
|
155
|
+
if (facts.tellFence !== undefined)
|
|
156
|
+
facts.tellFence = validateTellFence(facts.tellFence, `${location}.tellFence`);
|
|
157
|
+
if ("state" in facts)
|
|
158
|
+
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
159
|
+
return facts;
|
|
160
|
+
}
|
|
161
|
+
function validateAdoptionFacts(value, location) {
|
|
162
|
+
const facts = cloneFacts(value, location);
|
|
163
|
+
if ("state" in facts)
|
|
164
|
+
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
165
|
+
return facts;
|
|
166
|
+
}
|
|
167
|
+
function validateVerdictFacts(value, location) {
|
|
168
|
+
const facts = cloneFacts(value, location);
|
|
169
|
+
if (typeof facts.state !== "string" || !VERDICTS.has(facts.state)) {
|
|
170
|
+
throw new ProjectionGenerationStoreError(`${location}.state is not in the closed verdict vocabulary`);
|
|
171
|
+
}
|
|
172
|
+
return facts;
|
|
173
|
+
}
|
|
174
|
+
function parseStoredRow(row) {
|
|
175
|
+
const seq = typeof row.seq === "bigint" ? Number(row.seq) : row.seq;
|
|
176
|
+
if (!Number.isSafeInteger(seq) || seq <= 0) {
|
|
177
|
+
throw new ProjectionGenerationStoreError(`generation row has invalid seq '${String(row.seq)}'`);
|
|
178
|
+
}
|
|
179
|
+
if (row.kind !== "launch" && row.kind !== "adoption" && row.kind !== "verdict") {
|
|
180
|
+
throw new ProjectionGenerationStoreError(`generation row ${seq} has unknown kind '${row.kind}'`);
|
|
181
|
+
}
|
|
182
|
+
const executionId = assertExecutionId(row.execution_id);
|
|
183
|
+
let decoded;
|
|
184
|
+
try {
|
|
185
|
+
decoded = JSON.parse(row.facts_json);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw new ProjectionGenerationStoreError(`generation row ${seq} contains malformed facts JSON`, { cause: error });
|
|
189
|
+
}
|
|
190
|
+
const location = `generation row ${seq} facts`;
|
|
191
|
+
const facts = row.kind === "launch"
|
|
192
|
+
? validateLaunchFacts(decoded, location)
|
|
193
|
+
: row.kind === "adoption"
|
|
194
|
+
? validateAdoptionFacts(decoded, location)
|
|
195
|
+
: validateVerdictFacts(decoded, location);
|
|
196
|
+
return { seq, kind: row.kind, executionId, facts };
|
|
197
|
+
}
|
|
198
|
+
function validateHistory(records) {
|
|
199
|
+
let current = null;
|
|
200
|
+
const launchIds = new Set();
|
|
201
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
202
|
+
const record = records[index];
|
|
203
|
+
const expectedSeq = index + 1;
|
|
204
|
+
if (record.seq !== expectedSeq) {
|
|
205
|
+
throw new ProjectionGenerationStoreError(`generation history is not dense: expected seq ${expectedSeq}, found ${record.seq}`);
|
|
206
|
+
}
|
|
207
|
+
if (record.kind === "launch") {
|
|
208
|
+
if (current && !current.verdict) {
|
|
209
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} launches before execution '${current.launch.executionId}' is terminal`);
|
|
210
|
+
}
|
|
211
|
+
if (launchIds.has(record.executionId)) {
|
|
212
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} reuses executionId '${record.executionId}'`);
|
|
213
|
+
}
|
|
214
|
+
launchIds.add(record.executionId);
|
|
215
|
+
current = {
|
|
216
|
+
launch: record,
|
|
217
|
+
};
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (!current)
|
|
221
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} appears before any launch`);
|
|
222
|
+
if (record.executionId !== current.launch.executionId) {
|
|
223
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} does not join the current launch`);
|
|
224
|
+
}
|
|
225
|
+
if (record.kind === "adoption") {
|
|
226
|
+
if (current.adoption)
|
|
227
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates adoption`);
|
|
228
|
+
if (current.verdict)
|
|
229
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} adopts a terminal execution`);
|
|
230
|
+
current.adoption = record;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (current.verdict)
|
|
234
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates verdict`);
|
|
235
|
+
current.verdict = record;
|
|
236
|
+
}
|
|
237
|
+
return current;
|
|
238
|
+
}
|
|
239
|
+
function pragmaNumber(database, pragma) {
|
|
240
|
+
const row = database.prepare(`PRAGMA ${pragma}`).get();
|
|
241
|
+
const value = row ? Object.values(row)[0] : undefined;
|
|
242
|
+
if (typeof value !== "number" && typeof value !== "bigint") {
|
|
243
|
+
throw new ProjectionGenerationStoreError(`PRAGMA ${pragma} returned an invalid value`);
|
|
244
|
+
}
|
|
245
|
+
return Number(value);
|
|
246
|
+
}
|
|
247
|
+
function rollbackQuietly(database) {
|
|
248
|
+
try {
|
|
249
|
+
database.exec("ROLLBACK");
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Preserve the transition or schema failure that caused the rollback.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export class ProjectionGenerationStore {
|
|
256
|
+
databasePath;
|
|
257
|
+
readOnly;
|
|
258
|
+
#database;
|
|
259
|
+
#closed = false;
|
|
260
|
+
constructor(databasePath, database, readOnly) {
|
|
261
|
+
this.databasePath = databasePath;
|
|
262
|
+
this.#database = database;
|
|
263
|
+
this.readOnly = readOnly;
|
|
264
|
+
}
|
|
265
|
+
static open(projectionDirectory) {
|
|
266
|
+
const stat = fs.statSync(projectionDirectory);
|
|
267
|
+
if (!stat.isDirectory())
|
|
268
|
+
throw new ProjectionGenerationStoreError("projection generation store parent must be a directory");
|
|
269
|
+
const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
|
|
270
|
+
let database;
|
|
271
|
+
try {
|
|
272
|
+
database = new DatabaseSync(databasePath, {
|
|
273
|
+
enableForeignKeyConstraints: true,
|
|
274
|
+
enableDoubleQuotedStringLiterals: false,
|
|
275
|
+
allowExtension: false,
|
|
276
|
+
});
|
|
277
|
+
configureHeartBusyTimeout(database);
|
|
278
|
+
// Rollback journal keeps observers genuinely read-only. A read-only WAL
|
|
279
|
+
// connection creates/updates -wal and -shm coordination files, while
|
|
280
|
+
// immutable WAL reads can miss concurrently committed facts.
|
|
281
|
+
database.exec("PRAGMA journal_mode = DELETE");
|
|
282
|
+
database.exec("PRAGMA synchronous = FULL");
|
|
283
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
284
|
+
database.exec("PRAGMA trusted_schema = OFF");
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
try {
|
|
288
|
+
database?.close();
|
|
289
|
+
}
|
|
290
|
+
catch { /* preserve the open/configuration failure */ }
|
|
291
|
+
throw new ProjectionGenerationStoreError(`cannot open projection generation store at ${databasePath}`, { cause: error });
|
|
292
|
+
}
|
|
293
|
+
const store = new ProjectionGenerationStore(databasePath, database, false);
|
|
294
|
+
try {
|
|
295
|
+
store.#initializeOrValidateSchema();
|
|
296
|
+
store.#readValidatedHistory();
|
|
297
|
+
return store;
|
|
298
|
+
}
|
|
299
|
+
catch (error) {
|
|
300
|
+
database.close();
|
|
301
|
+
if (error instanceof ProjectionGenerationStoreError)
|
|
302
|
+
throw error;
|
|
303
|
+
throw new ProjectionGenerationStoreError(`invalid projection generation store at ${databasePath}`, { cause: error });
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
static openReadOnly(projectionDirectory) {
|
|
307
|
+
const stat = fs.statSync(projectionDirectory);
|
|
308
|
+
if (!stat.isDirectory())
|
|
309
|
+
throw new ProjectionGenerationStoreError("projection generation store parent must be a directory");
|
|
310
|
+
const databasePath = path.join(projectionDirectory, PROJECTION_GENERATION_STORE_FILE);
|
|
311
|
+
try {
|
|
312
|
+
if (!fs.lstatSync(databasePath).isFile()) {
|
|
313
|
+
throw new ProjectionGenerationStoreError("projection generation store must be a regular file");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (error instanceof ProjectionGenerationStoreError)
|
|
318
|
+
throw error;
|
|
319
|
+
throw new ProjectionGenerationStoreError(`projection generation store does not exist at ${databasePath}`, { cause: error });
|
|
320
|
+
}
|
|
321
|
+
let database;
|
|
322
|
+
try {
|
|
323
|
+
database = new DatabaseSync(databasePath, {
|
|
324
|
+
readOnly: true,
|
|
325
|
+
enableDoubleQuotedStringLiterals: false,
|
|
326
|
+
allowExtension: false,
|
|
327
|
+
});
|
|
328
|
+
configureHeartBusyTimeout(database);
|
|
329
|
+
const store = new ProjectionGenerationStore(databasePath, database, true);
|
|
330
|
+
store.#validateSchemaShape();
|
|
331
|
+
store.#readValidatedHistory();
|
|
332
|
+
return store;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
try {
|
|
336
|
+
database?.close();
|
|
337
|
+
}
|
|
338
|
+
catch { /* preserve the read/open failure */ }
|
|
339
|
+
if (error instanceof ProjectionGenerationStoreError)
|
|
340
|
+
throw error;
|
|
341
|
+
throw new ProjectionGenerationStoreError(`cannot read projection generation store at ${databasePath}`, { cause: error });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
close() {
|
|
345
|
+
if (this.#closed)
|
|
346
|
+
return;
|
|
347
|
+
this.#closed = true;
|
|
348
|
+
this.#database.close();
|
|
349
|
+
}
|
|
350
|
+
readRecords() {
|
|
351
|
+
this.#assertOpen();
|
|
352
|
+
return this.#readValidatedHistory().records;
|
|
353
|
+
}
|
|
354
|
+
readCurrentGeneration() {
|
|
355
|
+
this.#assertOpen();
|
|
356
|
+
return this.#readValidatedHistory().current;
|
|
357
|
+
}
|
|
358
|
+
initializeIdentity(identity) {
|
|
359
|
+
this.#assertOpen();
|
|
360
|
+
if (this.readOnly) {
|
|
361
|
+
throw new ProjectionGenerationStoreError("projection identity store is read-only");
|
|
362
|
+
}
|
|
363
|
+
const validated = parseExecutionPact(identity, `${this.databasePath}#projection_identity`);
|
|
364
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
365
|
+
try {
|
|
366
|
+
const existing = this.#database.prepare("SELECT COUNT(*) AS count FROM projection_identity").get();
|
|
367
|
+
if (Number(existing?.count ?? 0) !== 0) {
|
|
368
|
+
throw new ProjectionGenerationStoreError("projection identity is already initialized");
|
|
369
|
+
}
|
|
370
|
+
this.#database.prepare(`
|
|
371
|
+
INSERT INTO projection_identity (
|
|
372
|
+
singleton,
|
|
373
|
+
version,
|
|
374
|
+
pid,
|
|
375
|
+
akuma,
|
|
376
|
+
provider,
|
|
377
|
+
authority_json,
|
|
378
|
+
binding_json,
|
|
379
|
+
minted_at,
|
|
380
|
+
workspace_root,
|
|
381
|
+
effective_policy_json
|
|
382
|
+
) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
383
|
+
`).run(validated.version, validated.pid, validated.akuma, validated.provider, JSON.stringify(validated.authority), JSON.stringify(validated.binding), validated.mintedAt, validated.workspaceRoot, validated.effectivePolicy === undefined
|
|
384
|
+
? null
|
|
385
|
+
: JSON.stringify(validated.effectivePolicy));
|
|
386
|
+
this.#database.exec("COMMIT");
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
rollbackQuietly(this.#database);
|
|
390
|
+
throw error;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
readIdentity() {
|
|
394
|
+
this.#assertOpen();
|
|
395
|
+
const rows = this.#database.prepare(`
|
|
396
|
+
SELECT
|
|
397
|
+
version,
|
|
398
|
+
pid,
|
|
399
|
+
akuma,
|
|
400
|
+
provider,
|
|
401
|
+
authority_json,
|
|
402
|
+
binding_json,
|
|
403
|
+
minted_at,
|
|
404
|
+
workspace_root,
|
|
405
|
+
effective_policy_json
|
|
406
|
+
FROM projection_identity
|
|
407
|
+
`).all();
|
|
408
|
+
if (rows.length !== 1) {
|
|
409
|
+
throw new ProjectionGenerationStoreError(`projection identity row count must be 1, found ${rows.length}`);
|
|
410
|
+
}
|
|
411
|
+
const row = rows[0];
|
|
412
|
+
let authority;
|
|
413
|
+
let binding;
|
|
414
|
+
let effectivePolicy;
|
|
415
|
+
try {
|
|
416
|
+
authority = JSON.parse(row.authority_json);
|
|
417
|
+
binding = JSON.parse(row.binding_json);
|
|
418
|
+
effectivePolicy = row.effective_policy_json === null
|
|
419
|
+
? undefined
|
|
420
|
+
: JSON.parse(row.effective_policy_json);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
throw new ProjectionGenerationStoreError("projection identity contains malformed JSON", {
|
|
424
|
+
cause: error,
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return parseExecutionPact({
|
|
428
|
+
version: Number(row.version),
|
|
429
|
+
pid: Number(row.pid),
|
|
430
|
+
akuma: row.akuma,
|
|
431
|
+
provider: row.provider,
|
|
432
|
+
authority,
|
|
433
|
+
binding,
|
|
434
|
+
mintedAt: row.minted_at,
|
|
435
|
+
workspaceRoot: row.workspace_root,
|
|
436
|
+
...(effectivePolicy === undefined ? {} : { effectivePolicy }),
|
|
437
|
+
}, `${this.databasePath}#projection_identity`);
|
|
438
|
+
}
|
|
439
|
+
launchIfSettled(input) {
|
|
440
|
+
const executionId = assertExecutionId(input.executionId);
|
|
441
|
+
const facts = validateLaunchFacts(input.facts, "launch facts");
|
|
442
|
+
return this.#writeTransition(() => {
|
|
443
|
+
const history = this.#readValidatedHistory();
|
|
444
|
+
if (history.records.some((record) => record.kind === "launch" && record.executionId === executionId)) {
|
|
445
|
+
return this.#rejected("execution-id-already-used", history.current);
|
|
446
|
+
}
|
|
447
|
+
if (history.current && !history.current.verdict) {
|
|
448
|
+
return this.#rejected("current-generation-open", history.current);
|
|
449
|
+
}
|
|
450
|
+
return this.#committed([this.#append("launch", executionId, facts, history.records.length + 1)]);
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
adoptionIfCurrent(input) {
|
|
454
|
+
const executionId = assertExecutionId(input.executionId);
|
|
455
|
+
const facts = validateAdoptionFacts(input.facts, "adoption facts");
|
|
456
|
+
return this.#writeTransition(() => {
|
|
457
|
+
const history = this.#readValidatedHistory();
|
|
458
|
+
const current = history.current;
|
|
459
|
+
if (!current)
|
|
460
|
+
return this.#rejected("no-current-launch", null);
|
|
461
|
+
if (current.launch.executionId !== executionId)
|
|
462
|
+
return this.#rejected("not-current", current);
|
|
463
|
+
if (current.verdict)
|
|
464
|
+
return this.#rejected("already-terminal", current);
|
|
465
|
+
if (current.adoption)
|
|
466
|
+
return this.#rejected("already-adopted", current);
|
|
467
|
+
return this.#committed([this.#append("adoption", executionId, facts, history.records.length + 1)]);
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
verdictIfOpen(input) {
|
|
471
|
+
const executionId = assertExecutionId(input.executionId);
|
|
472
|
+
if (!VERDICTS.has(input.verdict))
|
|
473
|
+
throw new ProjectionGenerationStoreError(`unknown verdict '${String(input.verdict)}'`);
|
|
474
|
+
const facts = validateVerdictFacts({ ...(input.facts ?? {}), state: input.verdict }, "verdict facts");
|
|
475
|
+
return this.#writeTransition(() => {
|
|
476
|
+
const history = this.#readValidatedHistory();
|
|
477
|
+
const current = history.current;
|
|
478
|
+
if (!current)
|
|
479
|
+
return this.#rejected("no-current-launch", null);
|
|
480
|
+
if (current.launch.executionId !== executionId)
|
|
481
|
+
return this.#rejected("not-current", current);
|
|
482
|
+
if (current.verdict)
|
|
483
|
+
return this.#rejected("already-terminal", current);
|
|
484
|
+
if (input.verdict === "launch-failed" && current.adoption) {
|
|
485
|
+
return this.#rejected("already-adopted", current);
|
|
486
|
+
}
|
|
487
|
+
return this.#committed([this.#append("verdict", executionId, facts, history.records.length + 1)]);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
supersedeStartupTimeout(input) {
|
|
491
|
+
const executionId = assertExecutionId(input.executionId);
|
|
492
|
+
const successorExecutionId = assertExecutionId(input.successorExecutionId);
|
|
493
|
+
const observedAtMs = assertNonNegativeSafeInteger(input.observedAtMs, "observedAtMs");
|
|
494
|
+
const startupTimeoutMs = assertPositiveSafeInteger(input.startupTimeoutMs, "startupTimeoutMs");
|
|
495
|
+
const diagnostic = cloneFacts(input.diagnostic, "startup-timeout diagnostic");
|
|
496
|
+
const successorFacts = validateLaunchFacts(input.successorFacts, "successor launch facts");
|
|
497
|
+
return this.#writeTransition(() => {
|
|
498
|
+
const history = this.#readValidatedHistory();
|
|
499
|
+
const current = history.current;
|
|
500
|
+
if (!current)
|
|
501
|
+
return this.#rejected("no-current-launch", null);
|
|
502
|
+
if (current.launch.executionId !== executionId)
|
|
503
|
+
return this.#rejected("not-current", current);
|
|
504
|
+
if (current.verdict)
|
|
505
|
+
return this.#rejected("already-terminal", current);
|
|
506
|
+
if (current.adoption)
|
|
507
|
+
return this.#rejected("already-adopted", current);
|
|
508
|
+
if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
|
|
509
|
+
return this.#rejected("execution-id-already-used", current);
|
|
510
|
+
}
|
|
511
|
+
const launchedAtMs = Date.parse(current.launch.facts.createdAt);
|
|
512
|
+
if (observedAtMs - launchedAtMs < startupTimeoutMs) {
|
|
513
|
+
return this.#rejected("startup-timeout-not-elapsed", current);
|
|
514
|
+
}
|
|
515
|
+
const verdictFacts = validateVerdictFacts({ ...diagnostic, state: "launch-failed" }, "startup-timeout verdict facts");
|
|
516
|
+
const firstSeq = history.records.length + 1;
|
|
517
|
+
return this.#committed([
|
|
518
|
+
this.#append("verdict", executionId, verdictFacts, firstSeq),
|
|
519
|
+
this.#append("launch", successorExecutionId, successorFacts, firstSeq + 1),
|
|
520
|
+
]);
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
replaceDeadRunner(input) {
|
|
524
|
+
const executionId = assertExecutionId(input.executionId);
|
|
525
|
+
const successorExecutionId = assertExecutionId(input.successorExecutionId);
|
|
526
|
+
const releasedLockEvidence = cloneFacts(input.releasedLockEvidence, "released runner-lock evidence");
|
|
527
|
+
if (Object.keys(releasedLockEvidence).length === 0) {
|
|
528
|
+
throw new ProjectionGenerationStoreError("released runner-lock evidence must not be empty");
|
|
529
|
+
}
|
|
530
|
+
const successorFacts = validateLaunchFacts(input.successorFacts, "dead-runner successor launch facts");
|
|
531
|
+
return this.#writeTransition(() => {
|
|
532
|
+
const history = this.#readValidatedHistory();
|
|
533
|
+
const current = history.current;
|
|
534
|
+
if (!current)
|
|
535
|
+
return this.#rejected("no-current-launch", null);
|
|
536
|
+
if (current.launch.executionId !== executionId)
|
|
537
|
+
return this.#rejected("not-current", current);
|
|
538
|
+
if (current.verdict)
|
|
539
|
+
return this.#rejected("already-terminal", current);
|
|
540
|
+
if (!current.adoption)
|
|
541
|
+
return this.#rejected("not-adopted", current);
|
|
542
|
+
if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
|
|
543
|
+
return this.#rejected("execution-id-already-used", current);
|
|
544
|
+
}
|
|
545
|
+
const verdictFacts = validateVerdictFacts({ state: "lost-runner", releasedLockEvidence }, "dead-runner verdict facts");
|
|
546
|
+
const firstSeq = history.records.length + 1;
|
|
547
|
+
return this.#committed([
|
|
548
|
+
this.#append("verdict", executionId, verdictFacts, firstSeq),
|
|
549
|
+
this.#append("launch", successorExecutionId, successorFacts, firstSeq + 1),
|
|
550
|
+
]);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
handoff(input) {
|
|
554
|
+
const executionId = assertExecutionId(input.executionId);
|
|
555
|
+
const successorExecutionId = assertExecutionId(input.successorExecutionId);
|
|
556
|
+
const completionFacts = cloneFacts(input.completionFacts, "handoff completion facts");
|
|
557
|
+
if ("state" in completionFacts)
|
|
558
|
+
throw new ProjectionGenerationStoreError("handoff completion facts.state is reserved");
|
|
559
|
+
const successorFactsBase = validateLaunchFacts(input.successorFacts, "handoff successor launch facts");
|
|
560
|
+
if (successorFactsBase.tellFence !== undefined) {
|
|
561
|
+
throw new ProjectionGenerationStoreError("handoff successor launch facts.tellFence must be supplied only through handoff.tellFence");
|
|
562
|
+
}
|
|
563
|
+
const tellFence = validateTellFence([...input.tellFence], "handoff tellFence");
|
|
564
|
+
const successorFacts = validateLaunchFacts({ ...successorFactsBase, tellFence }, "handoff successor launch facts");
|
|
565
|
+
return this.#writeTransition(() => {
|
|
566
|
+
const history = this.#readValidatedHistory();
|
|
567
|
+
const current = history.current;
|
|
568
|
+
if (!current)
|
|
569
|
+
return this.#rejected("no-current-launch", null);
|
|
570
|
+
if (current.launch.executionId !== executionId)
|
|
571
|
+
return this.#rejected("not-current", current);
|
|
572
|
+
if (current.verdict)
|
|
573
|
+
return this.#rejected("already-terminal", current);
|
|
574
|
+
if (!current.adoption)
|
|
575
|
+
return this.#rejected("not-adopted", current);
|
|
576
|
+
if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
|
|
577
|
+
return this.#rejected("execution-id-already-used", current);
|
|
578
|
+
}
|
|
579
|
+
const verdictFacts = validateVerdictFacts({ ...completionFacts, state: "completed" }, "handoff verdict facts");
|
|
580
|
+
const firstSeq = history.records.length + 1;
|
|
581
|
+
return this.#committed([
|
|
582
|
+
this.#append("verdict", executionId, verdictFacts, firstSeq),
|
|
583
|
+
this.#append("launch", successorExecutionId, successorFacts, firstSeq + 1),
|
|
584
|
+
]);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
#initializeOrValidateSchema() {
|
|
588
|
+
this.#database.exec("BEGIN EXCLUSIVE");
|
|
589
|
+
try {
|
|
590
|
+
const version = pragmaNumber(this.#database, "user_version");
|
|
591
|
+
const objects = this.#database.prepare(`
|
|
592
|
+
SELECT name, type
|
|
593
|
+
FROM sqlite_schema
|
|
594
|
+
WHERE name NOT LIKE 'sqlite_%'
|
|
595
|
+
ORDER BY name
|
|
596
|
+
`).all();
|
|
597
|
+
if (version === 0 && objects.length === 0) {
|
|
598
|
+
this.#database.exec(SCHEMA_TABLE_SQL);
|
|
599
|
+
this.#database.exec(RECORD_TABLE_SQL);
|
|
600
|
+
this.#database.exec(IDENTITY_TABLE_SQL);
|
|
601
|
+
this.#database.exec(TELL_TABLE_SQL);
|
|
602
|
+
this.#database.prepare("INSERT INTO generation_schema (singleton, version) VALUES (1, ?)")
|
|
603
|
+
.run(PROJECTION_GENERATION_SCHEMA_VERSION);
|
|
604
|
+
this.#database.exec(`PRAGMA user_version = ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
|
|
605
|
+
}
|
|
606
|
+
else if (version !== PROJECTION_GENERATION_SCHEMA_VERSION) {
|
|
607
|
+
throw new ProjectionGenerationStoreError(`projection generation schema version ${version} is not supported; expected ${PROJECTION_GENERATION_SCHEMA_VERSION}`);
|
|
608
|
+
}
|
|
609
|
+
this.#validateSchemaShape();
|
|
610
|
+
this.#database.exec("COMMIT");
|
|
611
|
+
}
|
|
612
|
+
catch (error) {
|
|
613
|
+
rollbackQuietly(this.#database);
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
#validateSchemaShape() {
|
|
618
|
+
const objects = this.#database.prepare(`
|
|
619
|
+
SELECT name, type
|
|
620
|
+
FROM sqlite_schema
|
|
621
|
+
WHERE name NOT LIKE 'sqlite_%'
|
|
622
|
+
ORDER BY name
|
|
623
|
+
`).all();
|
|
624
|
+
const signature = objects.map((object) => `${object.type}:${object.name}`).join(",");
|
|
625
|
+
if (signature !== "table:generation_records,table:generation_schema,table:projection_identity,table:projection_tells") {
|
|
626
|
+
throw new ProjectionGenerationStoreError(`projection generation schema contains unexpected objects: ${signature || "none"}`);
|
|
627
|
+
}
|
|
628
|
+
const metadata = this.#database.prepare("SELECT singleton, version FROM generation_schema").all();
|
|
629
|
+
if (metadata.length !== 1 || Number(metadata[0].singleton) !== 1 || Number(metadata[0].version) !== PROJECTION_GENERATION_SCHEMA_VERSION) {
|
|
630
|
+
throw new ProjectionGenerationStoreError("projection generation schema metadata is malformed");
|
|
631
|
+
}
|
|
632
|
+
const recordColumns = this.#database.prepare("PRAGMA table_info(generation_records)").all();
|
|
633
|
+
const columnSignature = recordColumns
|
|
634
|
+
.map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
|
|
635
|
+
.join(",");
|
|
636
|
+
if (columnSignature !== "seq:INTEGER:0:1,kind:TEXT:1:0,execution_id:TEXT:1:0,facts_json:TEXT:1:0") {
|
|
637
|
+
throw new ProjectionGenerationStoreError("projection generation record schema is malformed");
|
|
638
|
+
}
|
|
639
|
+
const identityColumns = this.#database.prepare("PRAGMA table_info(projection_identity)").all();
|
|
640
|
+
const identitySignature = identityColumns
|
|
641
|
+
.map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
|
|
642
|
+
.join(",");
|
|
643
|
+
if (identitySignature !== "singleton:INTEGER:0:1,version:INTEGER:1:0,pid:INTEGER:1:0,akuma:TEXT:1:0,provider:TEXT:1:0,authority_json:TEXT:1:0,binding_json:TEXT:1:0,minted_at:TEXT:1:0,workspace_root:TEXT:1:0,effective_policy_json:TEXT:0:0") {
|
|
644
|
+
throw new ProjectionGenerationStoreError("projection identity schema is malformed");
|
|
645
|
+
}
|
|
646
|
+
const tellColumns = this.#database.prepare("PRAGMA table_info(projection_tells)").all();
|
|
647
|
+
const tellSignature = tellColumns
|
|
648
|
+
.map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
|
|
649
|
+
.join(",");
|
|
650
|
+
if (tellSignature !== "tell_id:TEXT:1:1,text:TEXT:1:0,created_at:TEXT:1:0,effort:TEXT:0:0,state:TEXT:1:0,execution_id:TEXT:0:0,fence_json:TEXT:0:0,replayed_from_json:TEXT:0:0,delivered_at:TEXT:0:0") {
|
|
651
|
+
throw new ProjectionGenerationStoreError("projection tell schema is malformed");
|
|
652
|
+
}
|
|
653
|
+
const quickCheck = this.#database.prepare("PRAGMA quick_check").get();
|
|
654
|
+
if (!quickCheck || Object.values(quickCheck)[0] !== "ok") {
|
|
655
|
+
throw new ProjectionGenerationStoreError("projection generation database failed quick_check");
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
#readValidatedHistory() {
|
|
659
|
+
const rows = this.#database.prepare(`
|
|
660
|
+
SELECT seq, kind, execution_id, facts_json
|
|
661
|
+
FROM generation_records
|
|
662
|
+
ORDER BY seq
|
|
663
|
+
`).all();
|
|
664
|
+
const records = rows.map(parseStoredRow);
|
|
665
|
+
return { records, current: validateHistory(records) };
|
|
666
|
+
}
|
|
667
|
+
#writeTransition(run) {
|
|
668
|
+
this.#assertOpen();
|
|
669
|
+
if (this.readOnly) {
|
|
670
|
+
throw new ProjectionGenerationStoreError("projection generation store is read-only");
|
|
671
|
+
}
|
|
672
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
673
|
+
try {
|
|
674
|
+
const result = run();
|
|
675
|
+
this.#database.exec("COMMIT");
|
|
676
|
+
return result;
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
rollbackQuietly(this.#database);
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
#append(kind, executionId, facts, seq) {
|
|
684
|
+
const encoded = JSON.stringify(facts);
|
|
685
|
+
this.#database.prepare(`
|
|
686
|
+
INSERT INTO generation_records (seq, kind, execution_id, facts_json)
|
|
687
|
+
VALUES (?, ?, ?, ?)
|
|
688
|
+
`).run(seq, kind, executionId, encoded);
|
|
689
|
+
return { seq, kind, executionId, facts: JSON.parse(encoded) };
|
|
690
|
+
}
|
|
691
|
+
#committed(records) {
|
|
692
|
+
return { status: "committed", records };
|
|
693
|
+
}
|
|
694
|
+
#rejected(reason, current) {
|
|
695
|
+
return { status: "rejected", reason, current };
|
|
696
|
+
}
|
|
697
|
+
#assertOpen() {
|
|
698
|
+
if (this.#closed)
|
|
699
|
+
throw new ProjectionGenerationStoreError("projection generation store is closed");
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
export function openProjectionGenerationStore(projectionDirectory) {
|
|
703
|
+
return ProjectionGenerationStore.open(projectionDirectory);
|
|
704
|
+
}
|
|
705
|
+
export function openProjectionGenerationStoreReadOnly(projectionDirectory) {
|
|
706
|
+
return ProjectionGenerationStore.openReadOnly(projectionDirectory);
|
|
707
|
+
}
|