@astrosheep/keiyaku 2.8.4 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -15
- package/build/.tsbuildinfo +1 -1
- package/build/agents/{call-terms_v2.js → call-terms.js} +12 -8
- package/build/agents/harness/control-types.js +1 -0
- package/build/agents/harness/execution-handle.js +23 -3
- package/build/agents/harness/index.js +6 -607
- package/build/agents/harness/outcome.js +141 -0
- package/build/agents/harness/projection.js +310 -0
- package/build/agents/harness/router.js +201 -0
- package/build/agents/harness/runtime.js +2 -1
- package/build/agents/{launch-snapshot_v2.js → launch-snapshot/model.js} +69 -74
- package/build/agents/launch-snapshot/resolve.js +72 -0
- package/build/agents/opencode-sdk.js +24 -3
- package/build/agents/providers/{claude-agent-sdk.js → claude-agent-sdk/adapter.js} +46 -368
- package/build/agents/providers/claude-agent-sdk/events.js +218 -0
- package/build/agents/providers/claude-agent-sdk/session.js +158 -0
- package/build/agents/providers/codex-app-server/adapter.js +347 -0
- package/build/agents/providers/codex-app-server/events.js +352 -0
- package/build/agents/providers/codex-app-server/session.js +266 -0
- package/build/agents/providers/codex-sdk.js +23 -5
- package/build/agents/providers/opencode-sdk/adapter.js +344 -0
- package/build/agents/providers/opencode-sdk/events.js +354 -0
- package/build/agents/providers/opencode-sdk/session.js +180 -0
- package/build/agents/providers/pi/adapter.js +269 -0
- package/build/agents/providers/pi/checkpoint.js +41 -0
- package/build/agents/providers/pi/events.js +282 -0
- package/build/agents/{selector_v2.js → selector.js} +8 -6
- package/build/cli/commands/akuma/akuma/handler.js +4 -0
- package/build/cli/commands/akuma/akuma/meta.js +16 -0
- package/build/cli/commands/akuma/catalog.js +8 -0
- package/build/cli/commands/akuma/list/handler.js +15 -0
- package/build/cli/commands/akuma/list/meta-ls.js +9 -0
- package/build/cli/commands/akuma/view/handler.js +34 -0
- package/build/cli/commands/akuma/view/meta.js +8 -0
- package/build/cli/commands/akuma.js +1 -1
- package/build/cli/commands/contract/amend/handler.js +10 -0
- package/build/cli/commands/contract/amend/meta.js +12 -0
- package/build/cli/commands/contract/arc/handler.js +9 -0
- package/build/cli/commands/contract/arc/meta.js +26 -0
- package/build/cli/commands/contract/bind/handler.js +23 -0
- package/build/cli/commands/contract/bind/meta.js +30 -0
- package/build/cli/commands/contract/catalog.js +16 -0
- package/build/cli/commands/contract/forfeit/handler.js +13 -0
- package/build/cli/commands/contract/forfeit/meta.js +12 -0
- package/build/cli/commands/contract/log/handler.js +12 -0
- package/build/cli/commands/contract/log/meta.js +12 -0
- package/build/cli/commands/contract/petition/handler.js +26 -0
- package/build/cli/commands/contract/petition/meta.js +23 -0
- package/build/cli/commands/contract/renew/handler.js +10 -0
- package/build/cli/commands/contract/renew/meta.js +17 -0
- package/build/cli/commands/projection/call/handler.js +39 -0
- package/build/cli/commands/projection/call/meta.js +9 -0
- package/build/cli/commands/projection/catalog.js +14 -0
- package/build/cli/commands/projection/kill/handler.js +35 -0
- package/build/cli/commands/projection/kill/meta.js +11 -0
- package/build/cli/commands/projection/revive/handler.js +53 -0
- package/build/cli/commands/projection/revive/meta.js +9 -0
- package/build/cli/commands/projection/status/handler.js +23 -0
- package/build/cli/commands/projection/status/meta.js +9 -0
- package/build/cli/commands/projection/tell/handler.js +34 -0
- package/build/cli/commands/projection/tell/meta.js +9 -0
- package/build/cli/commands/projection/wait/handler.js +21 -0
- package/build/cli/commands/projection/wait/meta.js +11 -0
- package/build/cli/commands/registry.js +100 -0
- package/build/cli/commands/shared.js +71 -0
- package/build/cli/commands/skills/catalog.js +6 -0
- package/build/cli/commands/skills/install/handler.js +10 -0
- package/build/cli/commands/skills/install/meta.js +9 -0
- package/build/cli/commands/skills/skills/handler.js +4 -0
- package/build/cli/commands/skills/skills/meta.js +10 -0
- package/build/cli/commands/system/catalog.js +8 -0
- package/build/cli/commands/system/completion/handler.js +10 -0
- package/build/cli/commands/system/completion/meta.js +9 -0
- package/build/cli/commands/system/dump-env/handler.js +3 -0
- package/build/cli/commands/system/dump-env/meta.js +10 -0
- package/build/cli/commands/system/guide/handler.js +4 -0
- package/build/cli/commands/system/guide/meta.js +9 -0
- package/build/cli/commands/task/add/handler.js +21 -0
- package/build/cli/commands/task/add/meta.js +8 -0
- package/build/cli/commands/task/catalog.js +22 -0
- package/build/cli/commands/task/doctor/handler.js +4 -0
- package/build/cli/commands/task/doctor/meta.js +8 -0
- package/build/cli/commands/task/done/handler.js +17 -0
- package/build/cli/commands/task/done/meta.js +8 -0
- package/build/cli/commands/task/drop/handler.js +6 -0
- package/build/cli/commands/task/drop/meta.js +8 -0
- package/build/cli/commands/task/ls/handler.js +13 -0
- package/build/cli/commands/task/ls/meta.js +8 -0
- package/build/cli/commands/task/shared.js +131 -0
- package/build/cli/commands/task/start/handler.js +6 -0
- package/build/cli/commands/task/start/meta.js +8 -0
- package/build/cli/commands/task/stop/handler.js +17 -0
- package/build/cli/commands/task/stop/meta.js +8 -0
- package/build/cli/commands/task/task/handler.js +5 -0
- package/build/cli/commands/task/task/meta.js +11 -0
- package/build/cli/commands/task/update/handler.js +18 -0
- package/build/cli/commands/task/update/meta.js +8 -0
- package/build/cli/commands/task/view/handler.js +14 -0
- package/build/cli/commands/task/view/meta.js +8 -0
- package/build/cli/completion.js +26 -27
- package/build/cli/flags.js +79 -2
- package/build/cli/help.js +27 -49
- package/build/cli/index.js +121 -374
- package/build/cli/output-stream-error-policy.js +17 -0
- package/build/cli/parse.js +159 -72
- package/build/cli/projection-address.js +5 -0
- package/build/cli/render/arc.js +1 -1
- package/build/cli/render/errors.js +4 -3
- package/build/cli/render/format.js +14 -0
- package/build/cli/render/kanshi.js +106 -0
- package/build/cli/render/petition.js +24 -1
- package/build/cli/render/projection-activity.js +5 -1
- package/build/cli/render/shared.js +2 -2
- package/build/cli/render/status.js +79 -13
- package/build/cli/render/task.js +113 -0
- package/build/cli/render/wait.js +23 -3
- package/build/cli/types.js +1 -1
- package/build/config/{akuma-loader_v2.js → akuma-loader.js} +2 -2
- package/build/config/env.js +9 -4
- package/build/config/settings/disease.js +26 -0
- package/build/config/settings/knobs.js +8 -0
- package/build/config/settings/loader.js +244 -0
- package/build/config/settings/schema.js +276 -0
- package/build/core/addressing.js +1 -1
- package/build/core/amend.js +3 -3
- package/build/core/arc.js +5 -4
- package/build/core/bind-workspace.js +123 -0
- package/build/core/bind.js +75 -155
- package/build/core/call/call.js +224 -0
- package/build/core/call/context.js +238 -0
- package/build/core/call/execution.js +217 -0
- package/build/core/call/prompt.js +42 -0
- package/build/core/call/reference.js +24 -0
- package/build/core/call-persist.js +1 -1
- package/build/core/claim-delivery.js +247 -0
- package/build/core/claim.js +39 -277
- package/build/core/context.js +2 -1
- package/build/core/{render.js → contract-view.js} +12 -140
- package/build/core/draft.js +107 -9
- package/build/core/entry.js +9 -1
- package/build/core/execution-coordinate.js +4 -20
- package/build/core/execution-pact.js +16 -31
- package/build/core/forfeit.js +35 -22
- package/build/core/hints.js +2 -2
- package/build/core/log.js +2 -2
- package/build/core/petition-claim-gates.js +8 -6
- package/build/core/petition-preview.js +97 -0
- package/build/core/petition-run.js +3 -2
- package/build/core/petition.js +39 -26
- package/build/core/projection/generation/database.js +306 -0
- package/build/core/projection/generation/identity.js +84 -0
- package/build/core/projection/generation/model.js +201 -0
- package/build/core/projection/generation/store.js +73 -0
- package/build/core/projection/generation/transitions.js +258 -0
- package/build/core/projection/heart.js +0 -307
- package/build/core/projection/tell/database.js +109 -0
- package/build/core/projection/tell/model.js +97 -0
- package/build/core/{projection-tells.js → projection/tell/store.js} +3 -158
- package/build/core/projection-activity.js +23 -2
- package/build/core/projection-core.js +2 -1
- package/build/core/projection-execution-observer.js +259 -0
- package/build/core/projection-generation-continuation.js +1 -1
- package/build/core/projection-generation-execution.js +90 -20
- package/build/core/projection-generation-launcher.js +1 -1
- package/build/core/projection-generation-runner.js +198 -4
- package/build/core/projection-identity.js +1 -1
- package/build/core/projection-kill.js +50 -17
- package/build/core/projection-life-observer.js +1 -1
- package/build/core/projection-life-protocol.js +10 -0
- package/build/core/projection-mint.js +1 -1
- package/build/core/projection-status.js +45 -32
- package/build/core/projection-wait.js +12 -2
- package/build/core/projection-wake.js +5 -2
- package/build/core/{queue_v2.js → queue.js} +2 -1
- package/build/core/registry.js +1 -1
- package/build/core/renew.js +7 -7
- package/build/core/response-artifact-catalog-sqlite.js +112 -0
- package/build/core/response-artifact-catalog.js +7 -0
- package/build/core/seal.js +44 -16
- package/build/core/status/board.js +286 -0
- package/build/core/status/drift.js +99 -0
- package/build/core/status/lifecycle.js +84 -0
- package/build/core/status/reconciliation.js +131 -0
- package/build/core/stored-agent-event.js +28 -4
- package/build/core/target-ref.js +10 -0
- package/build/core/task/board.js +175 -0
- package/build/core/task/commands.js +211 -0
- package/build/core/task/document.js +189 -0
- package/build/core/task/model.js +1 -0
- package/build/core/task/settlement-git.js +207 -0
- package/build/core/task/settlement-policy.js +82 -0
- package/build/core/task/source-board.js +84 -0
- package/build/core/task/source-model.js +1 -0
- package/build/core/task/validation.js +44 -0
- package/build/core/task-contract.js +224 -0
- package/build/core/task-doctor.js +14 -0
- package/build/core/task-git-actor.js +38 -0
- package/build/core/task-git-runtime.js +34 -0
- package/build/core/task-git-store.js +375 -0
- package/build/core/task-repository.js +1 -0
- package/build/core/task-store-repository.js +86 -0
- package/build/core/task-store.js +1 -0
- package/build/core/task-worktree.js +51 -0
- package/build/core/task.js +4 -0
- package/build/core/transcripts.js +138 -56
- package/build/core/validation.js +17 -0
- package/build/core/verdict.js +2 -2
- package/build/core/worktree-path.js +24 -6
- package/build/flow-error.js +3 -2
- package/build/generated/version.js +1 -1
- package/build/git/commits.js +3 -0
- package/build/git/diff/parsers.js +0 -94
- package/build/index.js +2 -0
- package/build/keiyaku-constants.js +2 -0
- package/build/keiyaku.js +4 -3
- package/package.json +3 -2
- package/skills/keiyaku/SKILL.md +1 -1
- package/skills/keiyaku-akuma/SKILL.md +11 -6
- package/build/agents/harness/pump.js +0 -158
- package/build/agents/providers/codex-app-server.js +0 -864
- package/build/agents/providers/opencode-sdk.js +0 -363
- package/build/agents/providers/pi.js +0 -557
- package/build/config/settings.js +0 -533
- package/build/core/call.js +0 -686
- package/build/core/projection/generation.js +0 -412
- package/build/core/projection/identity.js +0 -81
- package/build/core/projection/mint.js +0 -174
- package/build/core/projection-generation-store.js +0 -707
- package/build/core/status.js +0 -539
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { GENERATION_VERDICT_STATES, } from "../../projection-life-protocol.js";
|
|
2
|
+
export { GENERATION_VERDICT_STATES };
|
|
3
|
+
export const VERDICTS = new Set(GENERATION_VERDICT_STATES);
|
|
4
|
+
export class ProjectionGenerationStoreError extends Error {
|
|
5
|
+
code = "PROJECTION_GENERATION_STORE_ERROR";
|
|
6
|
+
constructor(message, options) {
|
|
7
|
+
super(message, options);
|
|
8
|
+
this.name = "ProjectionGenerationStoreError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function isPlainObject(value) {
|
|
12
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
13
|
+
return false;
|
|
14
|
+
const prototype = Object.getPrototypeOf(value);
|
|
15
|
+
return prototype === Object.prototype || prototype === null;
|
|
16
|
+
}
|
|
17
|
+
export function assertJsonValue(value, location, ancestors) {
|
|
18
|
+
if (value === null || typeof value === "string" || typeof value === "boolean")
|
|
19
|
+
return;
|
|
20
|
+
if (typeof value === "number") {
|
|
21
|
+
if (!Number.isFinite(value))
|
|
22
|
+
throw new ProjectionGenerationStoreError(`${location} contains a non-finite number`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "object") {
|
|
26
|
+
throw new ProjectionGenerationStoreError(`${location} contains a non-JSON value`);
|
|
27
|
+
}
|
|
28
|
+
if (ancestors.has(value))
|
|
29
|
+
throw new ProjectionGenerationStoreError(`${location} contains a cycle`);
|
|
30
|
+
ancestors.add(value);
|
|
31
|
+
try {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
value.forEach((item, index) => assertJsonValue(item, `${location}[${index}]`, ancestors));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (!isPlainObject(value))
|
|
37
|
+
throw new ProjectionGenerationStoreError(`${location} must contain only plain JSON objects`);
|
|
38
|
+
for (const [key, item] of Object.entries(value)) {
|
|
39
|
+
assertJsonValue(item, `${location}.${key}`, ancestors);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
ancestors.delete(value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function cloneFacts(value, location) {
|
|
47
|
+
if (!isPlainObject(value))
|
|
48
|
+
throw new ProjectionGenerationStoreError(`${location} must be a JSON object`);
|
|
49
|
+
assertJsonValue(value, location, new Set());
|
|
50
|
+
return JSON.parse(JSON.stringify(value));
|
|
51
|
+
}
|
|
52
|
+
export function assertExecutionId(executionId) {
|
|
53
|
+
if (typeof executionId !== "string" || executionId.trim() === "") {
|
|
54
|
+
throw new ProjectionGenerationStoreError("executionId must be a non-blank string");
|
|
55
|
+
}
|
|
56
|
+
return executionId;
|
|
57
|
+
}
|
|
58
|
+
export function assertNonNegativeSafeInteger(value, location) {
|
|
59
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
60
|
+
throw new ProjectionGenerationStoreError(`${location} must be a non-negative safe integer`);
|
|
61
|
+
}
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
export function assertPositiveSafeInteger(value, location) {
|
|
65
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
66
|
+
throw new ProjectionGenerationStoreError(`${location} must be a positive safe integer`);
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
export function assertIsoTimestamp(value, location) {
|
|
71
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
72
|
+
throw new ProjectionGenerationStoreError(`${location} must be an ISO-8601 timestamp`);
|
|
73
|
+
}
|
|
74
|
+
const milliseconds = Date.parse(value);
|
|
75
|
+
if (!Number.isFinite(milliseconds) || new Date(milliseconds).toISOString() !== value) {
|
|
76
|
+
throw new ProjectionGenerationStoreError(`${location} must be a canonical ISO-8601 timestamp`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
export function validateTellFence(value, location) {
|
|
81
|
+
if (!Array.isArray(value))
|
|
82
|
+
throw new ProjectionGenerationStoreError(`${location} must be an array`);
|
|
83
|
+
const seen = new Set();
|
|
84
|
+
return value.map((tellId, index) => {
|
|
85
|
+
if (typeof tellId !== "string" || tellId.trim() === "") {
|
|
86
|
+
throw new ProjectionGenerationStoreError(`${location}[${index}] must be a non-blank string`);
|
|
87
|
+
}
|
|
88
|
+
if (seen.has(tellId))
|
|
89
|
+
throw new ProjectionGenerationStoreError(`${location} contains duplicate tell '${tellId}'`);
|
|
90
|
+
seen.add(tellId);
|
|
91
|
+
return tellId;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
export function validateLaunchFacts(value, location) {
|
|
95
|
+
const facts = cloneFacts(value, location);
|
|
96
|
+
facts.createdAt = assertIsoTimestamp(facts.createdAt, `${location}.createdAt`);
|
|
97
|
+
if (facts.tellFence !== undefined)
|
|
98
|
+
facts.tellFence = validateTellFence(facts.tellFence, `${location}.tellFence`);
|
|
99
|
+
if ("state" in facts)
|
|
100
|
+
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
101
|
+
return facts;
|
|
102
|
+
}
|
|
103
|
+
export function validateAdoptionFacts(value, location) {
|
|
104
|
+
const facts = cloneFacts(value, location);
|
|
105
|
+
if ("state" in facts)
|
|
106
|
+
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
107
|
+
return facts;
|
|
108
|
+
}
|
|
109
|
+
export function validateKillIntentFacts(value, location) {
|
|
110
|
+
const facts = cloneFacts(value, location);
|
|
111
|
+
if (facts.operatorAction !== "kill") {
|
|
112
|
+
throw new ProjectionGenerationStoreError(`${location}.operatorAction must be exactly 'kill'`);
|
|
113
|
+
}
|
|
114
|
+
facts.killedAt = assertIsoTimestamp(facts.killedAt, `${location}.killedAt`);
|
|
115
|
+
if ("state" in facts)
|
|
116
|
+
throw new ProjectionGenerationStoreError(`${location}.state is reserved for verdict records`);
|
|
117
|
+
return facts;
|
|
118
|
+
}
|
|
119
|
+
export function validateVerdictFacts(value, location) {
|
|
120
|
+
const facts = cloneFacts(value, location);
|
|
121
|
+
if (typeof facts.state !== "string" || !VERDICTS.has(facts.state)) {
|
|
122
|
+
throw new ProjectionGenerationStoreError(`${location}.state is not in the closed verdict vocabulary`);
|
|
123
|
+
}
|
|
124
|
+
return facts;
|
|
125
|
+
}
|
|
126
|
+
export function parseStoredRow(row) {
|
|
127
|
+
const seq = typeof row.seq === "bigint" ? Number(row.seq) : row.seq;
|
|
128
|
+
if (!Number.isSafeInteger(seq) || seq <= 0) {
|
|
129
|
+
throw new ProjectionGenerationStoreError(`generation row has invalid seq '${String(row.seq)}'`);
|
|
130
|
+
}
|
|
131
|
+
if (row.kind !== "launch"
|
|
132
|
+
&& row.kind !== "adoption"
|
|
133
|
+
&& row.kind !== "kill-intent"
|
|
134
|
+
&& row.kind !== "verdict") {
|
|
135
|
+
throw new ProjectionGenerationStoreError(`generation row ${seq} has unknown kind '${row.kind}'`);
|
|
136
|
+
}
|
|
137
|
+
const executionId = assertExecutionId(row.execution_id);
|
|
138
|
+
let decoded;
|
|
139
|
+
try {
|
|
140
|
+
decoded = JSON.parse(row.facts_json);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
throw new ProjectionGenerationStoreError(`generation row ${seq} contains malformed facts JSON`, { cause: error });
|
|
144
|
+
}
|
|
145
|
+
const location = `generation row ${seq} facts`;
|
|
146
|
+
const facts = row.kind === "launch"
|
|
147
|
+
? validateLaunchFacts(decoded, location)
|
|
148
|
+
: row.kind === "adoption"
|
|
149
|
+
? validateAdoptionFacts(decoded, location)
|
|
150
|
+
: row.kind === "kill-intent"
|
|
151
|
+
? validateKillIntentFacts(decoded, location)
|
|
152
|
+
: validateVerdictFacts(decoded, location);
|
|
153
|
+
return { seq, kind: row.kind, executionId, facts };
|
|
154
|
+
}
|
|
155
|
+
export function validateHistory(records) {
|
|
156
|
+
let current = null;
|
|
157
|
+
const launchIds = new Set();
|
|
158
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
159
|
+
const record = records[index];
|
|
160
|
+
const expectedSeq = index + 1;
|
|
161
|
+
if (record.seq !== expectedSeq) {
|
|
162
|
+
throw new ProjectionGenerationStoreError(`generation history is not dense: expected seq ${expectedSeq}, found ${record.seq}`);
|
|
163
|
+
}
|
|
164
|
+
if (record.kind === "launch") {
|
|
165
|
+
if (current && !current.verdict) {
|
|
166
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} launches before execution '${current.launch.executionId}' is terminal`);
|
|
167
|
+
}
|
|
168
|
+
if (launchIds.has(record.executionId)) {
|
|
169
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} reuses executionId '${record.executionId}'`);
|
|
170
|
+
}
|
|
171
|
+
launchIds.add(record.executionId);
|
|
172
|
+
current = { launch: record };
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!current)
|
|
176
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} appears before any launch`);
|
|
177
|
+
if (record.executionId !== current.launch.executionId) {
|
|
178
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} does not join the current launch`);
|
|
179
|
+
}
|
|
180
|
+
if (record.kind === "adoption") {
|
|
181
|
+
if (current.adoption)
|
|
182
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates adoption`);
|
|
183
|
+
if (current.verdict)
|
|
184
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} adopts a terminal execution`);
|
|
185
|
+
current.adoption = record;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (record.kind === "kill-intent") {
|
|
189
|
+
if (current.killIntent)
|
|
190
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates kill-intent`);
|
|
191
|
+
if (current.verdict)
|
|
192
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} records kill-intent after verdict`);
|
|
193
|
+
current.killIntent = record;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (current.verdict)
|
|
197
|
+
throw new ProjectionGenerationStoreError(`generation row ${record.seq} duplicates verdict`);
|
|
198
|
+
current.verdict = record;
|
|
199
|
+
}
|
|
200
|
+
return current;
|
|
201
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, assertDatabaseOpen, closeDatabase, isProjectionGenerationSchemaV1Residue, openMutableDatabase, openReadOnlyDatabase, readValidatedHistory, } from "./database.js";
|
|
2
|
+
import { initializeIdentity as initializeProjectionIdentity, readIdentity as readProjectionIdentity, } from "./identity.js";
|
|
3
|
+
import * as transitions from "./transitions.js";
|
|
4
|
+
export { PROJECTION_GENERATION_SCHEMA_VERSION, PROJECTION_GENERATION_STORE_FILE, isProjectionGenerationSchemaV1Residue, };
|
|
5
|
+
export { GENERATION_VERDICT_STATES, ProjectionGenerationStoreError, } from "./model.js";
|
|
6
|
+
export class ProjectionGenerationStore {
|
|
7
|
+
databasePath;
|
|
8
|
+
readOnly;
|
|
9
|
+
#database;
|
|
10
|
+
constructor(databasePath, database, readOnly) {
|
|
11
|
+
this.databasePath = databasePath;
|
|
12
|
+
this.#database = database;
|
|
13
|
+
this.readOnly = readOnly;
|
|
14
|
+
}
|
|
15
|
+
static open(projectionDirectory) {
|
|
16
|
+
const { databasePath, database } = openMutableDatabase(projectionDirectory);
|
|
17
|
+
return new ProjectionGenerationStore(databasePath, database, false);
|
|
18
|
+
}
|
|
19
|
+
static openReadOnly(projectionDirectory) {
|
|
20
|
+
const { databasePath, database } = openReadOnlyDatabase(projectionDirectory);
|
|
21
|
+
return new ProjectionGenerationStore(databasePath, database, true);
|
|
22
|
+
}
|
|
23
|
+
close() {
|
|
24
|
+
closeDatabase(this.#database);
|
|
25
|
+
}
|
|
26
|
+
readRecords() {
|
|
27
|
+
assertDatabaseOpen(this.#database);
|
|
28
|
+
return readValidatedHistory(this.#database).records;
|
|
29
|
+
}
|
|
30
|
+
readCurrentGeneration() {
|
|
31
|
+
assertDatabaseOpen(this.#database);
|
|
32
|
+
return readValidatedHistory(this.#database).current;
|
|
33
|
+
}
|
|
34
|
+
initializeIdentity(identity) {
|
|
35
|
+
initializeProjectionIdentity(this.#database, this.databasePath, this.readOnly, identity);
|
|
36
|
+
}
|
|
37
|
+
readIdentity() {
|
|
38
|
+
return readProjectionIdentity(this.#database, this.databasePath);
|
|
39
|
+
}
|
|
40
|
+
launchIfSettled(input) {
|
|
41
|
+
return transitions.launchIfSettled(this.#database, this.readOnly, input);
|
|
42
|
+
}
|
|
43
|
+
adoptionIfCurrent(input) {
|
|
44
|
+
return transitions.adoptionIfCurrent(this.#database, this.readOnly, input);
|
|
45
|
+
}
|
|
46
|
+
requestKillIfOpen(input) {
|
|
47
|
+
return transitions.requestKillIfOpen(this.#database, this.readOnly, input);
|
|
48
|
+
}
|
|
49
|
+
runnerKilledIfRequested(input) {
|
|
50
|
+
return transitions.runnerKilledIfRequested(this.#database, this.readOnly, input);
|
|
51
|
+
}
|
|
52
|
+
settleKillIfRunnerDead(input) {
|
|
53
|
+
return transitions.settleKillIfRunnerDead(this.#database, this.readOnly, input);
|
|
54
|
+
}
|
|
55
|
+
verdictIfOpen(input) {
|
|
56
|
+
return transitions.verdictIfOpen(this.#database, this.readOnly, input);
|
|
57
|
+
}
|
|
58
|
+
supersedeStartupTimeout(input) {
|
|
59
|
+
return transitions.supersedeStartupTimeout(this.#database, this.readOnly, input);
|
|
60
|
+
}
|
|
61
|
+
replaceDeadRunner(input) {
|
|
62
|
+
return transitions.replaceDeadRunner(this.#database, this.readOnly, input);
|
|
63
|
+
}
|
|
64
|
+
handoff(input) {
|
|
65
|
+
return transitions.handoff(this.#database, this.readOnly, input);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export function openProjectionGenerationStore(projectionDirectory) {
|
|
69
|
+
return ProjectionGenerationStore.open(projectionDirectory);
|
|
70
|
+
}
|
|
71
|
+
export function openProjectionGenerationStoreReadOnly(projectionDirectory) {
|
|
72
|
+
return ProjectionGenerationStore.openReadOnly(projectionDirectory);
|
|
73
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { appendRecord, readValidatedHistory, writeTransition, } from "./database.js";
|
|
2
|
+
import { ProjectionGenerationStoreError, VERDICTS, assertExecutionId, assertIsoTimestamp, assertNonNegativeSafeInteger, assertPositiveSafeInteger, cloneFacts, validateAdoptionFacts, validateKillIntentFacts, validateLaunchFacts, validateTellFence, validateVerdictFacts, } from "./model.js";
|
|
3
|
+
function committed(records) {
|
|
4
|
+
return { status: "committed", records };
|
|
5
|
+
}
|
|
6
|
+
function rejected(reason, current) {
|
|
7
|
+
return { status: "rejected", reason, current };
|
|
8
|
+
}
|
|
9
|
+
export function launchIfSettled(database, readOnly, input) {
|
|
10
|
+
const executionId = assertExecutionId(input.executionId);
|
|
11
|
+
const facts = validateLaunchFacts(input.facts, "launch facts");
|
|
12
|
+
return writeTransition(database, readOnly, () => {
|
|
13
|
+
const history = readValidatedHistory(database);
|
|
14
|
+
if (history.records.some((record) => record.kind === "launch" && record.executionId === executionId)) {
|
|
15
|
+
return rejected("execution-id-already-used", history.current);
|
|
16
|
+
}
|
|
17
|
+
if (history.current && !history.current.verdict) {
|
|
18
|
+
return rejected("current-generation-open", history.current);
|
|
19
|
+
}
|
|
20
|
+
return committed([appendRecord(database, "launch", executionId, facts, history.records.length + 1)]);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function adoptionIfCurrent(database, readOnly, input) {
|
|
24
|
+
const executionId = assertExecutionId(input.executionId);
|
|
25
|
+
const facts = validateAdoptionFacts(input.facts, "adoption facts");
|
|
26
|
+
return writeTransition(database, readOnly, () => {
|
|
27
|
+
const history = readValidatedHistory(database);
|
|
28
|
+
const current = history.current;
|
|
29
|
+
if (!current)
|
|
30
|
+
return rejected("no-current-launch", null);
|
|
31
|
+
if (current.launch.executionId !== executionId)
|
|
32
|
+
return rejected("not-current", current);
|
|
33
|
+
if (current.verdict)
|
|
34
|
+
return rejected("already-terminal", current);
|
|
35
|
+
if (current.adoption)
|
|
36
|
+
return rejected("already-adopted", current);
|
|
37
|
+
return committed([appendRecord(database, "adoption", executionId, facts, history.records.length + 1)]);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export function requestKillIfOpen(database, readOnly, input) {
|
|
41
|
+
const executionId = assertExecutionId(input.executionId);
|
|
42
|
+
const killedAt = assertIsoTimestamp(input.killedAt, "killedAt");
|
|
43
|
+
return writeTransition(database, readOnly, () => {
|
|
44
|
+
const history = readValidatedHistory(database);
|
|
45
|
+
const current = history.current;
|
|
46
|
+
if (!current)
|
|
47
|
+
return rejected("no-current-launch", null);
|
|
48
|
+
if (current.launch.executionId !== executionId)
|
|
49
|
+
return rejected("not-current", current);
|
|
50
|
+
if (current.verdict)
|
|
51
|
+
return rejected("already-terminal", current);
|
|
52
|
+
if (current.killIntent)
|
|
53
|
+
return rejected("kill-requested", current);
|
|
54
|
+
const facts = validateKillIntentFacts({ operatorAction: "kill", killedAt }, "kill-intent facts");
|
|
55
|
+
return committed([appendRecord(database, "kill-intent", executionId, facts, history.records.length + 1)]);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export function runnerKilledIfRequested(database, readOnly, input) {
|
|
59
|
+
const executionId = assertExecutionId(input.executionId);
|
|
60
|
+
const callerFacts = cloneFacts(input.facts ?? {}, "killed verdict facts");
|
|
61
|
+
if ("state" in callerFacts) {
|
|
62
|
+
throw new ProjectionGenerationStoreError("killed verdict facts.state is reserved");
|
|
63
|
+
}
|
|
64
|
+
if ("killIntentSeq" in callerFacts) {
|
|
65
|
+
throw new ProjectionGenerationStoreError("killed verdict facts.killIntentSeq is reserved");
|
|
66
|
+
}
|
|
67
|
+
if ("killedAt" in callerFacts) {
|
|
68
|
+
throw new ProjectionGenerationStoreError("killed verdict facts.killedAt is reserved");
|
|
69
|
+
}
|
|
70
|
+
return writeTransition(database, readOnly, () => {
|
|
71
|
+
const history = readValidatedHistory(database);
|
|
72
|
+
const current = history.current;
|
|
73
|
+
if (!current)
|
|
74
|
+
return rejected("no-current-launch", null);
|
|
75
|
+
if (current.launch.executionId !== executionId)
|
|
76
|
+
return rejected("not-current", current);
|
|
77
|
+
if (current.verdict)
|
|
78
|
+
return rejected("already-terminal", current);
|
|
79
|
+
if (!current.killIntent)
|
|
80
|
+
return rejected("kill-not-requested", current);
|
|
81
|
+
const facts = validateVerdictFacts({
|
|
82
|
+
...callerFacts,
|
|
83
|
+
state: "killed",
|
|
84
|
+
killIntentSeq: current.killIntent.seq,
|
|
85
|
+
killedAt: current.killIntent.facts.killedAt,
|
|
86
|
+
}, "killed verdict facts");
|
|
87
|
+
return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function settleKillIfRunnerDead(database, readOnly, input) {
|
|
91
|
+
const executionId = assertExecutionId(input.executionId);
|
|
92
|
+
const releasedLockEvidence = cloneFacts(input.releasedLockEvidence, "released runner-lock evidence");
|
|
93
|
+
if (Object.keys(releasedLockEvidence).length === 0) {
|
|
94
|
+
throw new ProjectionGenerationStoreError("released runner-lock evidence must not be empty");
|
|
95
|
+
}
|
|
96
|
+
const callerFacts = cloneFacts(input.facts ?? {}, "killed verdict facts");
|
|
97
|
+
if ("state" in callerFacts) {
|
|
98
|
+
throw new ProjectionGenerationStoreError("killed verdict facts.state is reserved");
|
|
99
|
+
}
|
|
100
|
+
if ("killIntentSeq" in callerFacts) {
|
|
101
|
+
throw new ProjectionGenerationStoreError("killed verdict facts.killIntentSeq is reserved");
|
|
102
|
+
}
|
|
103
|
+
if ("killedAt" in callerFacts) {
|
|
104
|
+
throw new ProjectionGenerationStoreError("killed verdict facts.killedAt is reserved");
|
|
105
|
+
}
|
|
106
|
+
return writeTransition(database, readOnly, () => {
|
|
107
|
+
const history = readValidatedHistory(database);
|
|
108
|
+
const current = history.current;
|
|
109
|
+
if (!current)
|
|
110
|
+
return rejected("no-current-launch", null);
|
|
111
|
+
if (current.launch.executionId !== executionId)
|
|
112
|
+
return rejected("not-current", current);
|
|
113
|
+
if (current.verdict)
|
|
114
|
+
return rejected("already-terminal", current);
|
|
115
|
+
if (!current.killIntent)
|
|
116
|
+
return rejected("kill-not-requested", current);
|
|
117
|
+
const facts = validateVerdictFacts({
|
|
118
|
+
...callerFacts,
|
|
119
|
+
state: "killed",
|
|
120
|
+
killIntentSeq: current.killIntent.seq,
|
|
121
|
+
killedAt: current.killIntent.facts.killedAt,
|
|
122
|
+
releasedLockEvidence,
|
|
123
|
+
}, "killed verdict facts");
|
|
124
|
+
return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
export function verdictIfOpen(database, readOnly, input) {
|
|
128
|
+
const executionId = assertExecutionId(input.executionId);
|
|
129
|
+
if (!VERDICTS.has(input.verdict))
|
|
130
|
+
throw new ProjectionGenerationStoreError(`unknown verdict '${String(input.verdict)}'`);
|
|
131
|
+
if (input.verdict === "killed") {
|
|
132
|
+
throw new ProjectionGenerationStoreError("verdict 'killed' is reserved to runnerKilledIfRequested and settleKillIfRunnerDead");
|
|
133
|
+
}
|
|
134
|
+
const facts = validateVerdictFacts({ ...(input.facts ?? {}), state: input.verdict }, "verdict facts");
|
|
135
|
+
return writeTransition(database, readOnly, () => {
|
|
136
|
+
const history = readValidatedHistory(database);
|
|
137
|
+
const current = history.current;
|
|
138
|
+
if (!current)
|
|
139
|
+
return rejected("no-current-launch", null);
|
|
140
|
+
if (current.launch.executionId !== executionId)
|
|
141
|
+
return rejected("not-current", current);
|
|
142
|
+
if (current.verdict)
|
|
143
|
+
return rejected("already-terminal", current);
|
|
144
|
+
if (current.killIntent) {
|
|
145
|
+
const killEscalationFailed = input.verdict === "failed"
|
|
146
|
+
&& input.facts?.operatorAction === "kill";
|
|
147
|
+
if (!killEscalationFailed)
|
|
148
|
+
return rejected("kill-requested", current);
|
|
149
|
+
}
|
|
150
|
+
if (input.verdict === "launch-failed" && current.adoption) {
|
|
151
|
+
return rejected("already-adopted", current);
|
|
152
|
+
}
|
|
153
|
+
return committed([appendRecord(database, "verdict", executionId, facts, history.records.length + 1)]);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
export function supersedeStartupTimeout(database, readOnly, input) {
|
|
157
|
+
const executionId = assertExecutionId(input.executionId);
|
|
158
|
+
const successorExecutionId = assertExecutionId(input.successorExecutionId);
|
|
159
|
+
const observedAtMs = assertNonNegativeSafeInteger(input.observedAtMs, "observedAtMs");
|
|
160
|
+
const startupTimeoutMs = assertPositiveSafeInteger(input.startupTimeoutMs, "startupTimeoutMs");
|
|
161
|
+
const diagnostic = cloneFacts(input.diagnostic, "startup-timeout diagnostic");
|
|
162
|
+
const successorFacts = validateLaunchFacts(input.successorFacts, "successor launch facts");
|
|
163
|
+
return writeTransition(database, readOnly, () => {
|
|
164
|
+
const history = readValidatedHistory(database);
|
|
165
|
+
const current = history.current;
|
|
166
|
+
if (!current)
|
|
167
|
+
return rejected("no-current-launch", null);
|
|
168
|
+
if (current.launch.executionId !== executionId)
|
|
169
|
+
return rejected("not-current", current);
|
|
170
|
+
if (current.verdict)
|
|
171
|
+
return rejected("already-terminal", current);
|
|
172
|
+
if (current.killIntent)
|
|
173
|
+
return rejected("kill-requested", current);
|
|
174
|
+
if (current.adoption)
|
|
175
|
+
return rejected("already-adopted", current);
|
|
176
|
+
if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
|
|
177
|
+
return rejected("execution-id-already-used", current);
|
|
178
|
+
}
|
|
179
|
+
const launchedAtMs = Date.parse(current.launch.facts.createdAt);
|
|
180
|
+
if (observedAtMs - launchedAtMs < startupTimeoutMs) {
|
|
181
|
+
return rejected("startup-timeout-not-elapsed", current);
|
|
182
|
+
}
|
|
183
|
+
const verdictFacts = validateVerdictFacts({ ...diagnostic, state: "launch-failed" }, "startup-timeout verdict facts");
|
|
184
|
+
const firstSeq = history.records.length + 1;
|
|
185
|
+
return committed([
|
|
186
|
+
appendRecord(database, "verdict", executionId, verdictFacts, firstSeq),
|
|
187
|
+
appendRecord(database, "launch", successorExecutionId, successorFacts, firstSeq + 1),
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
export function replaceDeadRunner(database, readOnly, input) {
|
|
192
|
+
const executionId = assertExecutionId(input.executionId);
|
|
193
|
+
const successorExecutionId = assertExecutionId(input.successorExecutionId);
|
|
194
|
+
const releasedLockEvidence = cloneFacts(input.releasedLockEvidence, "released runner-lock evidence");
|
|
195
|
+
if (Object.keys(releasedLockEvidence).length === 0) {
|
|
196
|
+
throw new ProjectionGenerationStoreError("released runner-lock evidence must not be empty");
|
|
197
|
+
}
|
|
198
|
+
const successorFacts = validateLaunchFacts(input.successorFacts, "dead-runner successor launch facts");
|
|
199
|
+
return writeTransition(database, readOnly, () => {
|
|
200
|
+
const history = readValidatedHistory(database);
|
|
201
|
+
const current = history.current;
|
|
202
|
+
if (!current)
|
|
203
|
+
return rejected("no-current-launch", null);
|
|
204
|
+
if (current.launch.executionId !== executionId)
|
|
205
|
+
return rejected("not-current", current);
|
|
206
|
+
if (current.verdict)
|
|
207
|
+
return rejected("already-terminal", current);
|
|
208
|
+
if (current.killIntent)
|
|
209
|
+
return rejected("kill-requested", current);
|
|
210
|
+
if (!current.adoption)
|
|
211
|
+
return rejected("not-adopted", current);
|
|
212
|
+
if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
|
|
213
|
+
return rejected("execution-id-already-used", current);
|
|
214
|
+
}
|
|
215
|
+
const verdictFacts = validateVerdictFacts({ state: "lost-runner", releasedLockEvidence }, "dead-runner verdict facts");
|
|
216
|
+
const firstSeq = history.records.length + 1;
|
|
217
|
+
return committed([
|
|
218
|
+
appendRecord(database, "verdict", executionId, verdictFacts, firstSeq),
|
|
219
|
+
appendRecord(database, "launch", successorExecutionId, successorFacts, firstSeq + 1),
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
export function handoff(database, readOnly, input) {
|
|
224
|
+
const executionId = assertExecutionId(input.executionId);
|
|
225
|
+
const successorExecutionId = assertExecutionId(input.successorExecutionId);
|
|
226
|
+
const completionFacts = cloneFacts(input.completionFacts, "handoff completion facts");
|
|
227
|
+
if ("state" in completionFacts)
|
|
228
|
+
throw new ProjectionGenerationStoreError("handoff completion facts.state is reserved");
|
|
229
|
+
const successorFactsBase = validateLaunchFacts(input.successorFacts, "handoff successor launch facts");
|
|
230
|
+
if (successorFactsBase.tellFence !== undefined) {
|
|
231
|
+
throw new ProjectionGenerationStoreError("handoff successor launch facts.tellFence must be supplied only through handoff.tellFence");
|
|
232
|
+
}
|
|
233
|
+
const tellFence = validateTellFence([...input.tellFence], "handoff tellFence");
|
|
234
|
+
const successorFacts = validateLaunchFacts({ ...successorFactsBase, tellFence }, "handoff successor launch facts");
|
|
235
|
+
return writeTransition(database, readOnly, () => {
|
|
236
|
+
const history = readValidatedHistory(database);
|
|
237
|
+
const current = history.current;
|
|
238
|
+
if (!current)
|
|
239
|
+
return rejected("no-current-launch", null);
|
|
240
|
+
if (current.launch.executionId !== executionId)
|
|
241
|
+
return rejected("not-current", current);
|
|
242
|
+
if (current.verdict)
|
|
243
|
+
return rejected("already-terminal", current);
|
|
244
|
+
if (current.killIntent)
|
|
245
|
+
return rejected("kill-requested", current);
|
|
246
|
+
if (!current.adoption)
|
|
247
|
+
return rejected("not-adopted", current);
|
|
248
|
+
if (history.records.some((record) => record.kind === "launch" && record.executionId === successorExecutionId)) {
|
|
249
|
+
return rejected("execution-id-already-used", current);
|
|
250
|
+
}
|
|
251
|
+
const verdictFacts = validateVerdictFacts({ ...completionFacts, state: "completed" }, "handoff verdict facts");
|
|
252
|
+
const firstSeq = history.records.length + 1;
|
|
253
|
+
return committed([
|
|
254
|
+
appendRecord(database, "verdict", executionId, verdictFacts, firstSeq),
|
|
255
|
+
appendRecord(database, "launch", successorExecutionId, successorFacts, firstSeq + 1),
|
|
256
|
+
]);
|
|
257
|
+
});
|
|
258
|
+
}
|