@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,316 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { isErrnoException } from "../../errno.js";
|
|
5
|
+
export const HEART_FILE = "heart";
|
|
6
|
+
const HEART_SCHEMA_VERSION = 1;
|
|
7
|
+
export const HEART_BUSY_TIMEOUT_MS = 5_000;
|
|
8
|
+
export class ProjectionHeartError extends Error {
|
|
9
|
+
code = "PROJECTION_HEART_ERROR";
|
|
10
|
+
constructor(message, options) {
|
|
11
|
+
super(message, options);
|
|
12
|
+
this.name = "ProjectionHeartError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
const HEART_SCHEMA_SQL = `
|
|
16
|
+
CREATE TABLE heart_schema (
|
|
17
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
18
|
+
version INTEGER NOT NULL CHECK (version = ${HEART_SCHEMA_VERSION})
|
|
19
|
+
) STRICT
|
|
20
|
+
`;
|
|
21
|
+
const IDENTITY_TABLE_SQL = `
|
|
22
|
+
CREATE TABLE projection_identity (
|
|
23
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
24
|
+
version INTEGER NOT NULL CHECK (version = 1),
|
|
25
|
+
pid INTEGER NOT NULL CHECK (pid >= 0),
|
|
26
|
+
akuma TEXT NOT NULL CHECK (length(trim(akuma)) > 0),
|
|
27
|
+
provider TEXT NOT NULL CHECK (length(trim(provider)) > 0),
|
|
28
|
+
authority_json TEXT NOT NULL CHECK (json_valid(authority_json)),
|
|
29
|
+
binding_json TEXT NOT NULL CHECK (json_valid(binding_json)),
|
|
30
|
+
minted_at TEXT NOT NULL CHECK (length(trim(minted_at)) > 0),
|
|
31
|
+
workspace_root TEXT NOT NULL CHECK (length(trim(workspace_root)) > 0),
|
|
32
|
+
effective_policy_json TEXT CHECK (
|
|
33
|
+
effective_policy_json IS NULL OR json_valid(effective_policy_json)
|
|
34
|
+
)
|
|
35
|
+
) STRICT
|
|
36
|
+
`;
|
|
37
|
+
const RECORD_TABLE_SQL = `
|
|
38
|
+
CREATE TABLE generation_records (
|
|
39
|
+
seq INTEGER PRIMARY KEY CHECK (seq > 0),
|
|
40
|
+
kind TEXT NOT NULL CHECK (kind IN ('launch', 'adoption', 'verdict')),
|
|
41
|
+
execution_id TEXT NOT NULL CHECK (length(trim(execution_id)) > 0),
|
|
42
|
+
payload_json TEXT NOT NULL CHECK (json_valid(payload_json))
|
|
43
|
+
) STRICT
|
|
44
|
+
`;
|
|
45
|
+
const TELL_TABLE_SQL = `
|
|
46
|
+
CREATE TABLE projection_tells (
|
|
47
|
+
tell_id TEXT PRIMARY KEY CHECK (length(trim(tell_id)) > 0),
|
|
48
|
+
text TEXT NOT NULL,
|
|
49
|
+
created_at TEXT NOT NULL CHECK (length(trim(created_at)) > 0),
|
|
50
|
+
effort TEXT CHECK (effort IS NULL OR length(trim(effort)) > 0),
|
|
51
|
+
state TEXT NOT NULL CHECK (
|
|
52
|
+
state IN ('inbox', 'inflight', 'submitted', 'consumed', 'delivered')
|
|
53
|
+
),
|
|
54
|
+
execution_id TEXT CHECK (
|
|
55
|
+
execution_id IS NULL OR length(trim(execution_id)) > 0
|
|
56
|
+
),
|
|
57
|
+
fence_json TEXT CHECK (fence_json IS NULL OR json_valid(fence_json)),
|
|
58
|
+
replayed_from_json TEXT CHECK (
|
|
59
|
+
replayed_from_json IS NULL OR json_valid(replayed_from_json)
|
|
60
|
+
),
|
|
61
|
+
delivered_at TEXT CHECK (
|
|
62
|
+
delivered_at IS NULL OR length(trim(delivered_at)) > 0
|
|
63
|
+
)
|
|
64
|
+
) STRICT
|
|
65
|
+
`;
|
|
66
|
+
const EXPECTED_OBJECT_SIGNATURE = "table:generation_records,table:heart_schema,table:projection_identity,table:projection_tells";
|
|
67
|
+
const EXPECTED_RECORD_COLUMNS = "seq:INTEGER:0:1,kind:TEXT:1:0,execution_id:TEXT:1:0,payload_json:TEXT:1:0";
|
|
68
|
+
const EXPECTED_IDENTITY_COLUMNS = "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";
|
|
69
|
+
const EXPECTED_TELL_COLUMNS = "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";
|
|
70
|
+
export function heartPath(projectionDirectory) {
|
|
71
|
+
return path.join(projectionDirectory, HEART_FILE);
|
|
72
|
+
}
|
|
73
|
+
function rollbackQuietly(database) {
|
|
74
|
+
try {
|
|
75
|
+
database.exec("ROLLBACK");
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Preserve the write or schema failure that caused the rollback.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function assertParentDirectory(projectionDirectory) {
|
|
82
|
+
let stat;
|
|
83
|
+
try {
|
|
84
|
+
stat = fs.statSync(projectionDirectory);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw new ProjectionHeartError(`projection heart parent directory does not exist at ${projectionDirectory}`, { cause: error });
|
|
88
|
+
}
|
|
89
|
+
if (!stat.isDirectory()) {
|
|
90
|
+
throw new ProjectionHeartError("projection heart parent must be a directory");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function assertRegularHeartFile(databasePath) {
|
|
94
|
+
let stat;
|
|
95
|
+
try {
|
|
96
|
+
stat = fs.lstatSync(databasePath);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
throw new ProjectionHeartError(`projection heart does not exist at ${databasePath}`, { cause: error });
|
|
100
|
+
}
|
|
101
|
+
if (!stat.isFile()) {
|
|
102
|
+
throw new ProjectionHeartError("projection heart must be a regular file");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export function configureHeartBusyTimeout(database) {
|
|
106
|
+
database.exec(`PRAGMA busy_timeout = ${HEART_BUSY_TIMEOUT_MS}`);
|
|
107
|
+
}
|
|
108
|
+
function configureWritable(database) {
|
|
109
|
+
configureHeartBusyTimeout(database);
|
|
110
|
+
database.exec("PRAGMA journal_mode = DELETE");
|
|
111
|
+
database.exec("PRAGMA synchronous = FULL");
|
|
112
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
113
|
+
database.exec("PRAGMA trusted_schema = OFF");
|
|
114
|
+
}
|
|
115
|
+
function columnSignature(database, table) {
|
|
116
|
+
const columns = database.prepare(`PRAGMA table_info(${table})`).all();
|
|
117
|
+
return columns
|
|
118
|
+
.map((column) => `${column.name}:${column.type}:${Number(column.notnull)}:${Number(column.pk)}`)
|
|
119
|
+
.join(",");
|
|
120
|
+
}
|
|
121
|
+
function validateSchemaShape(database) {
|
|
122
|
+
const objects = database.prepare(`
|
|
123
|
+
SELECT name, type
|
|
124
|
+
FROM sqlite_schema
|
|
125
|
+
WHERE name NOT LIKE 'sqlite_%'
|
|
126
|
+
ORDER BY name
|
|
127
|
+
`).all();
|
|
128
|
+
const signature = objects.map((object) => `${object.type}:${object.name}`).join(",");
|
|
129
|
+
if (signature !== EXPECTED_OBJECT_SIGNATURE) {
|
|
130
|
+
throw new ProjectionHeartError(`projection heart schema contains unexpected objects: ${signature || "none"}`);
|
|
131
|
+
}
|
|
132
|
+
const metadata = database.prepare("SELECT singleton, version FROM heart_schema").all();
|
|
133
|
+
if (metadata.length !== 1
|
|
134
|
+
|| Number(metadata[0].singleton) !== 1
|
|
135
|
+
|| Number(metadata[0].version) !== HEART_SCHEMA_VERSION) {
|
|
136
|
+
throw new ProjectionHeartError("projection heart schema metadata is malformed");
|
|
137
|
+
}
|
|
138
|
+
if (columnSignature(database, "generation_records") !== EXPECTED_RECORD_COLUMNS) {
|
|
139
|
+
throw new ProjectionHeartError("projection heart generation_records schema is malformed");
|
|
140
|
+
}
|
|
141
|
+
if (columnSignature(database, "projection_identity") !== EXPECTED_IDENTITY_COLUMNS) {
|
|
142
|
+
throw new ProjectionHeartError("projection heart projection_identity schema is malformed");
|
|
143
|
+
}
|
|
144
|
+
if (columnSignature(database, "projection_tells") !== EXPECTED_TELL_COLUMNS) {
|
|
145
|
+
throw new ProjectionHeartError("projection heart projection_tells schema is malformed");
|
|
146
|
+
}
|
|
147
|
+
const quickCheck = database.prepare("PRAGMA quick_check").get();
|
|
148
|
+
if (!quickCheck || Object.values(quickCheck)[0] !== "ok") {
|
|
149
|
+
throw new ProjectionHeartError("projection heart database failed quick_check");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function installSchema(database) {
|
|
153
|
+
database.exec("BEGIN EXCLUSIVE");
|
|
154
|
+
try {
|
|
155
|
+
const objects = database.prepare(`
|
|
156
|
+
SELECT name, type
|
|
157
|
+
FROM sqlite_schema
|
|
158
|
+
WHERE name NOT LIKE 'sqlite_%'
|
|
159
|
+
ORDER BY name
|
|
160
|
+
`).all();
|
|
161
|
+
if (objects.length !== 0) {
|
|
162
|
+
throw new ProjectionHeartError("projection heart already contains schema objects");
|
|
163
|
+
}
|
|
164
|
+
database.exec(HEART_SCHEMA_SQL);
|
|
165
|
+
database.exec(IDENTITY_TABLE_SQL);
|
|
166
|
+
database.exec(RECORD_TABLE_SQL);
|
|
167
|
+
database.exec(TELL_TABLE_SQL);
|
|
168
|
+
database.prepare("INSERT INTO heart_schema (singleton, version) VALUES (1, ?)")
|
|
169
|
+
.run(HEART_SCHEMA_VERSION);
|
|
170
|
+
validateSchemaShape(database);
|
|
171
|
+
database.exec("COMMIT");
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
rollbackQuietly(database);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
export class ProjectionHeart {
|
|
179
|
+
databasePath;
|
|
180
|
+
readOnly;
|
|
181
|
+
#database;
|
|
182
|
+
#closed = false;
|
|
183
|
+
constructor(databasePath, database, readOnly) {
|
|
184
|
+
this.databasePath = databasePath;
|
|
185
|
+
this.#database = database;
|
|
186
|
+
this.readOnly = readOnly;
|
|
187
|
+
}
|
|
188
|
+
/** Create a brand-new heart database with schema v1. Fails if the file already exists. */
|
|
189
|
+
static create(projectionDirectory) {
|
|
190
|
+
assertParentDirectory(projectionDirectory);
|
|
191
|
+
const databasePath = heartPath(projectionDirectory);
|
|
192
|
+
let reserved = false;
|
|
193
|
+
try {
|
|
194
|
+
const fd = fs.openSync(databasePath, "wx");
|
|
195
|
+
fs.closeSync(fd);
|
|
196
|
+
reserved = true;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
200
|
+
throw new ProjectionHeartError(`projection heart already exists at ${databasePath}`, { cause: error });
|
|
201
|
+
}
|
|
202
|
+
throw new ProjectionHeartError(`cannot create projection heart at ${databasePath}`, { cause: error });
|
|
203
|
+
}
|
|
204
|
+
let database;
|
|
205
|
+
try {
|
|
206
|
+
database = new DatabaseSync(databasePath, {
|
|
207
|
+
enableForeignKeyConstraints: true,
|
|
208
|
+
enableDoubleQuotedStringLiterals: false,
|
|
209
|
+
allowExtension: false,
|
|
210
|
+
});
|
|
211
|
+
configureWritable(database);
|
|
212
|
+
installSchema(database);
|
|
213
|
+
return new ProjectionHeart(databasePath, database, false);
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
try {
|
|
217
|
+
database?.close();
|
|
218
|
+
}
|
|
219
|
+
catch { /* preserve the causal create failure */ }
|
|
220
|
+
if (reserved) {
|
|
221
|
+
try {
|
|
222
|
+
fs.unlinkSync(databasePath);
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Leave a partial file only if unlink itself fails.
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (error instanceof ProjectionHeartError)
|
|
229
|
+
throw error;
|
|
230
|
+
throw new ProjectionHeartError(`cannot create projection heart at ${databasePath}`, { cause: error });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/** Open an existing heart read-only. Never creates, mutates, or migrates. */
|
|
234
|
+
static openReadOnly(projectionDirectory) {
|
|
235
|
+
assertParentDirectory(projectionDirectory);
|
|
236
|
+
const databasePath = heartPath(projectionDirectory);
|
|
237
|
+
assertRegularHeartFile(databasePath);
|
|
238
|
+
let database;
|
|
239
|
+
try {
|
|
240
|
+
database = new DatabaseSync(databasePath, {
|
|
241
|
+
readOnly: true,
|
|
242
|
+
enableDoubleQuotedStringLiterals: false,
|
|
243
|
+
allowExtension: false,
|
|
244
|
+
});
|
|
245
|
+
configureHeartBusyTimeout(database);
|
|
246
|
+
validateSchemaShape(database);
|
|
247
|
+
return new ProjectionHeart(databasePath, database, true);
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
try {
|
|
251
|
+
database?.close();
|
|
252
|
+
}
|
|
253
|
+
catch { /* preserve open failure */ }
|
|
254
|
+
if (error instanceof ProjectionHeartError)
|
|
255
|
+
throw error;
|
|
256
|
+
throw new ProjectionHeartError(`cannot open projection heart at ${databasePath}`, { cause: error });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Open an existing heart for read/write. Never creates or migrates. */
|
|
260
|
+
static openWritable(projectionDirectory) {
|
|
261
|
+
assertParentDirectory(projectionDirectory);
|
|
262
|
+
const databasePath = heartPath(projectionDirectory);
|
|
263
|
+
assertRegularHeartFile(databasePath);
|
|
264
|
+
let database;
|
|
265
|
+
try {
|
|
266
|
+
database = new DatabaseSync(databasePath, {
|
|
267
|
+
enableForeignKeyConstraints: true,
|
|
268
|
+
enableDoubleQuotedStringLiterals: false,
|
|
269
|
+
allowExtension: false,
|
|
270
|
+
});
|
|
271
|
+
configureWritable(database);
|
|
272
|
+
validateSchemaShape(database);
|
|
273
|
+
return new ProjectionHeart(databasePath, database, false);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
try {
|
|
277
|
+
database?.close();
|
|
278
|
+
}
|
|
279
|
+
catch { /* preserve open failure */ }
|
|
280
|
+
if (error instanceof ProjectionHeartError)
|
|
281
|
+
throw error;
|
|
282
|
+
throw new ProjectionHeartError(`cannot open projection heart at ${databasePath}`, { cause: error });
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
read(fn) {
|
|
286
|
+
this.#assertOpen();
|
|
287
|
+
return fn(this.#database);
|
|
288
|
+
}
|
|
289
|
+
write(fn) {
|
|
290
|
+
this.#assertOpen();
|
|
291
|
+
if (this.readOnly) {
|
|
292
|
+
throw new ProjectionHeartError("projection heart is read-only");
|
|
293
|
+
}
|
|
294
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
295
|
+
try {
|
|
296
|
+
const result = fn(this.#database);
|
|
297
|
+
this.#database.exec("COMMIT");
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
rollbackQuietly(this.#database);
|
|
302
|
+
throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
close() {
|
|
306
|
+
if (this.#closed)
|
|
307
|
+
return;
|
|
308
|
+
this.#closed = true;
|
|
309
|
+
this.#database.close();
|
|
310
|
+
}
|
|
311
|
+
#assertOpen() {
|
|
312
|
+
if (this.#closed) {
|
|
313
|
+
throw new ProjectionHeartError("projection heart is closed");
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { parseExecutionPact, } from "../execution-pact.js";
|
|
2
|
+
import { ProjectionHeartError } from "./heart.js";
|
|
3
|
+
/**
|
|
4
|
+
* Insert the single immutable identity row.
|
|
5
|
+
* There is no product UPDATE path; a second insert fails.
|
|
6
|
+
*/
|
|
7
|
+
export function insertProjectionIdentity(heart, identity) {
|
|
8
|
+
const location = `${heart.databasePath}#projection_identity`;
|
|
9
|
+
const validated = parseExecutionPact(identity, location);
|
|
10
|
+
heart.write((database) => {
|
|
11
|
+
const existing = database.prepare("SELECT COUNT(*) AS count FROM projection_identity").get();
|
|
12
|
+
if (Number(existing?.count ?? 0) !== 0) {
|
|
13
|
+
throw new ProjectionHeartError("projection identity is already initialized");
|
|
14
|
+
}
|
|
15
|
+
database.prepare(`
|
|
16
|
+
INSERT INTO projection_identity (
|
|
17
|
+
singleton,
|
|
18
|
+
version,
|
|
19
|
+
pid,
|
|
20
|
+
akuma,
|
|
21
|
+
provider,
|
|
22
|
+
authority_json,
|
|
23
|
+
binding_json,
|
|
24
|
+
minted_at,
|
|
25
|
+
workspace_root,
|
|
26
|
+
effective_policy_json
|
|
27
|
+
) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
28
|
+
`).run(validated.version, validated.pid, validated.akuma, validated.provider, JSON.stringify(validated.authority), JSON.stringify(validated.binding), validated.mintedAt, validated.workspaceRoot, validated.effectivePolicy === undefined
|
|
29
|
+
? null
|
|
30
|
+
: JSON.stringify(validated.effectivePolicy));
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Read the single immutable identity row through the existing pact parser. */
|
|
34
|
+
export function readProjectionIdentity(heart) {
|
|
35
|
+
const location = `${heart.databasePath}#projection_identity`;
|
|
36
|
+
return heart.read((database) => {
|
|
37
|
+
const rows = database.prepare(`
|
|
38
|
+
SELECT
|
|
39
|
+
version,
|
|
40
|
+
pid,
|
|
41
|
+
akuma,
|
|
42
|
+
provider,
|
|
43
|
+
authority_json,
|
|
44
|
+
binding_json,
|
|
45
|
+
minted_at,
|
|
46
|
+
workspace_root,
|
|
47
|
+
effective_policy_json
|
|
48
|
+
FROM projection_identity
|
|
49
|
+
`).all();
|
|
50
|
+
if (rows.length !== 1) {
|
|
51
|
+
throw new ProjectionHeartError(`projection identity row count must be 1, found ${rows.length}`);
|
|
52
|
+
}
|
|
53
|
+
const row = rows[0];
|
|
54
|
+
let authority;
|
|
55
|
+
let binding;
|
|
56
|
+
let effectivePolicy;
|
|
57
|
+
try {
|
|
58
|
+
authority = JSON.parse(row.authority_json);
|
|
59
|
+
binding = JSON.parse(row.binding_json);
|
|
60
|
+
effectivePolicy = row.effective_policy_json === null
|
|
61
|
+
? undefined
|
|
62
|
+
: JSON.parse(row.effective_policy_json);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
throw new ProjectionHeartError("projection identity contains malformed JSON", {
|
|
66
|
+
cause: error,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return parseExecutionPact({
|
|
70
|
+
version: Number(row.version),
|
|
71
|
+
pid: Number(row.pid),
|
|
72
|
+
akuma: row.akuma,
|
|
73
|
+
provider: row.provider,
|
|
74
|
+
authority,
|
|
75
|
+
binding,
|
|
76
|
+
mintedAt: row.minted_at,
|
|
77
|
+
workspaceRoot: row.workspace_root,
|
|
78
|
+
...(effectivePolicy === undefined ? {} : { effectivePolicy }),
|
|
79
|
+
}, location);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { isErrnoException } from "../../errno.js";
|
|
5
|
+
export const LEASH_FILE = "leash";
|
|
6
|
+
export class ProjectionLeashError extends Error {
|
|
7
|
+
code = "PROJECTION_LEASH_ERROR";
|
|
8
|
+
constructor(message, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.name = "ProjectionLeashError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function leashPath(projectionDirectory) {
|
|
14
|
+
return path.join(projectionDirectory, LEASH_FILE);
|
|
15
|
+
}
|
|
16
|
+
function errorDetail(error) {
|
|
17
|
+
return error instanceof Error ? error.message : String(error);
|
|
18
|
+
}
|
|
19
|
+
function readJournalMode(database) {
|
|
20
|
+
const row = database.prepare("PRAGMA journal_mode").get();
|
|
21
|
+
return typeof row?.journal_mode === "string" ? row.journal_mode.toLowerCase() : undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Birth-time initialization for the projection leash file.
|
|
25
|
+
* Creates a brand-new extensionless rollback-journal SQLite database with the
|
|
26
|
+
* minimal kernel-lock anchor schema, then closes all handles before return.
|
|
27
|
+
* Fails if the target already exists. Stores zero product facts.
|
|
28
|
+
*/
|
|
29
|
+
export function initializeProjectionLeash(projectionDirectory) {
|
|
30
|
+
let parent;
|
|
31
|
+
try {
|
|
32
|
+
parent = fs.statSync(projectionDirectory);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
throw new ProjectionLeashError(`projection leash parent directory does not exist at ${projectionDirectory}`, { cause: error });
|
|
36
|
+
}
|
|
37
|
+
if (!parent.isDirectory()) {
|
|
38
|
+
throw new ProjectionLeashError("projection leash parent must be a directory");
|
|
39
|
+
}
|
|
40
|
+
const target = leashPath(projectionDirectory);
|
|
41
|
+
let reserved = false;
|
|
42
|
+
try {
|
|
43
|
+
const fd = fs.openSync(target, "wx");
|
|
44
|
+
fs.closeSync(fd);
|
|
45
|
+
reserved = true;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
49
|
+
throw new ProjectionLeashError(`projection leash already exists at ${target}`, { cause: error });
|
|
50
|
+
}
|
|
51
|
+
throw new ProjectionLeashError(`projection leash could not be initialized: ${errorDetail(error)}`, { cause: error });
|
|
52
|
+
}
|
|
53
|
+
let database;
|
|
54
|
+
try {
|
|
55
|
+
database = new DatabaseSync(target, {
|
|
56
|
+
enableDoubleQuotedStringLiterals: false,
|
|
57
|
+
allowExtension: false,
|
|
58
|
+
});
|
|
59
|
+
database.exec("PRAGMA busy_timeout = 0");
|
|
60
|
+
database.exec("PRAGMA journal_mode = DELETE");
|
|
61
|
+
database.exec("CREATE TABLE lock_anchor (id INTEGER PRIMARY KEY CHECK (id = 1))");
|
|
62
|
+
if (readJournalMode(database) !== "delete") {
|
|
63
|
+
throw new ProjectionLeashError("projection leash does not use rollback-journal mode");
|
|
64
|
+
}
|
|
65
|
+
database.prepare("SELECT id FROM lock_anchor LIMIT 1").get();
|
|
66
|
+
database.close();
|
|
67
|
+
database = undefined;
|
|
68
|
+
return target;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
try {
|
|
72
|
+
database?.close();
|
|
73
|
+
}
|
|
74
|
+
catch { /* preserve the causal initialization failure */ }
|
|
75
|
+
if (reserved) {
|
|
76
|
+
try {
|
|
77
|
+
fs.unlinkSync(target);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Leave a partial file only if unlink itself fails.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (error instanceof ProjectionLeashError)
|
|
84
|
+
throw error;
|
|
85
|
+
throw new ProjectionLeashError(`projection leash could not be initialized: ${errorDetail(error)}`, { cause: error });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isErrnoException } from "../../errno.js";
|
|
4
|
+
import { ProjectionStateError, randomHex8 } from "../atomic-publish.js";
|
|
5
|
+
import { isoFromNowMs, PROJECTION_ROOT_SEGMENTS, projectionDir, projectionRoot, } from "../projection-core.js";
|
|
6
|
+
import { parseExecutionAuthoritySelection, parseExecutionPact, parseExecutionPactFields, } from "../execution-pact.js";
|
|
7
|
+
import { ProjectionHeart } from "./heart.js";
|
|
8
|
+
import { insertProjectionIdentity } from "./identity.js";
|
|
9
|
+
import { initializeProjectionLeash } from "./leash.js";
|
|
10
|
+
export class ProjectionMintCollisionError extends Error {
|
|
11
|
+
code = "PROJECTION_MINT_COLLISION";
|
|
12
|
+
constructor(message, options) {
|
|
13
|
+
super(message, options);
|
|
14
|
+
this.name = "ProjectionMintCollisionError";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
const DEFAULT_MINT_ATTEMPTS = 16;
|
|
18
|
+
function assertMintSlug(slug) {
|
|
19
|
+
const normalized = slug.trim();
|
|
20
|
+
if (!normalized) {
|
|
21
|
+
throw new ProjectionStateError("projection slug cannot be empty");
|
|
22
|
+
}
|
|
23
|
+
if (normalized.includes("/") || normalized.includes("\0")) {
|
|
24
|
+
throw new ProjectionStateError(`invalid projection slug: ${slug}`);
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validate mint inputs that can fail, then reserve one private staging directory.
|
|
30
|
+
* Dot-prefixed staging is never a published projection address.
|
|
31
|
+
*/
|
|
32
|
+
function reserveProjectionDirectory(input) {
|
|
33
|
+
const slug = assertMintSlug(input.slug);
|
|
34
|
+
if (!Number.isInteger(input.pid) || input.pid < 0) {
|
|
35
|
+
throw new ProjectionStateError("projection pid must be a non-negative integer");
|
|
36
|
+
}
|
|
37
|
+
if (!Number.isSafeInteger(input.nowMs) || input.nowMs < 0) {
|
|
38
|
+
throw new ProjectionStateError("nowMs must be a safe non-negative integer");
|
|
39
|
+
}
|
|
40
|
+
const maxAttempts = input.maxAttempts ?? DEFAULT_MINT_ATTEMPTS;
|
|
41
|
+
if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
|
|
42
|
+
throw new ProjectionStateError("maxAttempts must be a positive integer");
|
|
43
|
+
}
|
|
44
|
+
const root = projectionRoot(input.cwd);
|
|
45
|
+
fs.mkdirSync(root, { recursive: true });
|
|
46
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
47
|
+
const hex = (input.randomHex?.(attempt) ?? randomHex8()).toLowerCase();
|
|
48
|
+
if (!/^[0-9a-f]{8}$/.test(hex)) {
|
|
49
|
+
throw new ProjectionStateError(`projection id random segment must be 8 hex chars, got ${hex}`);
|
|
50
|
+
}
|
|
51
|
+
const id = `${slug}-${hex}`;
|
|
52
|
+
const dir = projectionDir(input.cwd, id);
|
|
53
|
+
const stagingDir = path.join(root, `.mint-${id}`);
|
|
54
|
+
if (fs.existsSync(dir))
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
fs.mkdirSync(stagingDir);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (isErrnoException(error) && error.code === "EEXIST") {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
if (fs.existsSync(dir)) {
|
|
66
|
+
fs.rmdirSync(stagingDir);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
return { id, dir, stagingDir };
|
|
70
|
+
}
|
|
71
|
+
throw new ProjectionMintCollisionError(`exhausted ${maxAttempts} projection id attempts under ${PROJECTION_ROOT_SEGMENTS.join("/")} (PROJECTION_MINT_COLLISION)`);
|
|
72
|
+
}
|
|
73
|
+
/** Build the identity fields shared by both authority variants. */
|
|
74
|
+
function buildMintPactFields(input) {
|
|
75
|
+
return {
|
|
76
|
+
version: 1,
|
|
77
|
+
pid: input.pid,
|
|
78
|
+
akuma: input.akuma,
|
|
79
|
+
provider: input.provider,
|
|
80
|
+
binding: input.binding,
|
|
81
|
+
mintedAt: isoFromNowMs(input.nowMs),
|
|
82
|
+
workspaceRoot: input.workspaceRoot ?? path.resolve(input.cwd),
|
|
83
|
+
...(input.effectivePolicy ? { effectivePolicy: input.effectivePolicy } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Build both hard projection organs in private staging, close every database
|
|
88
|
+
* handle, then publish with one directory rename. Rename is the sole publication
|
|
89
|
+
* boundary; no fallible work runs after it.
|
|
90
|
+
*/
|
|
91
|
+
function publishReservedProjection(pact, reserved) {
|
|
92
|
+
let published = false;
|
|
93
|
+
try {
|
|
94
|
+
const heart = ProjectionHeart.create(reserved.stagingDir);
|
|
95
|
+
try {
|
|
96
|
+
insertProjectionIdentity(heart, pact);
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
heart.close();
|
|
100
|
+
}
|
|
101
|
+
initializeProjectionLeash(reserved.stagingDir);
|
|
102
|
+
fs.renameSync(reserved.stagingDir, reserved.dir);
|
|
103
|
+
published = true;
|
|
104
|
+
return { id: reserved.id, dir: reserved.dir, pact };
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
if (!published) {
|
|
108
|
+
// Failure removes only this mint's staging.
|
|
109
|
+
fs.rmSync(reserved.stagingDir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Mint a user-authority projection directory.
|
|
115
|
+
* Order: validate → reserve staging → heart + identity + leash → close → atomic rename.
|
|
116
|
+
*/
|
|
117
|
+
export function mintProjection(input) {
|
|
118
|
+
const authority = parseExecutionAuthoritySelection(input.authority, "projection identity", "user");
|
|
119
|
+
const fields = parseExecutionPactFields(buildMintPactFields(input));
|
|
120
|
+
const pact = parseExecutionPact({
|
|
121
|
+
version: fields.version,
|
|
122
|
+
pid: fields.pid,
|
|
123
|
+
akuma: fields.akuma,
|
|
124
|
+
provider: fields.provider,
|
|
125
|
+
authority,
|
|
126
|
+
binding: fields.binding,
|
|
127
|
+
mintedAt: fields.mintedAt,
|
|
128
|
+
workspaceRoot: fields.workspaceRoot,
|
|
129
|
+
...(fields.effectivePolicy !== undefined ? { effectivePolicy: fields.effectivePolicy } : {}),
|
|
130
|
+
});
|
|
131
|
+
const reserved = reserveProjectionDirectory(input);
|
|
132
|
+
return publishReservedProjection(pact, reserved);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Mint a repository-authority projection.
|
|
136
|
+
* Order: validate complete identity → reserve staging → heart + identity + leash → close → atomic rename.
|
|
137
|
+
*/
|
|
138
|
+
export async function mintRepositoryProjection(input) {
|
|
139
|
+
parseExecutionAuthoritySelection(input.authority, "projection identity", "repository");
|
|
140
|
+
const fields = parseExecutionPactFields(buildMintPactFields(input));
|
|
141
|
+
const pact = parseExecutionPact({
|
|
142
|
+
version: fields.version,
|
|
143
|
+
pid: fields.pid,
|
|
144
|
+
akuma: fields.akuma,
|
|
145
|
+
provider: fields.provider,
|
|
146
|
+
authority: { kind: "repository" },
|
|
147
|
+
binding: fields.binding,
|
|
148
|
+
mintedAt: fields.mintedAt,
|
|
149
|
+
workspaceRoot: fields.workspaceRoot,
|
|
150
|
+
...(fields.effectivePolicy !== undefined ? { effectivePolicy: fields.effectivePolicy } : {}),
|
|
151
|
+
});
|
|
152
|
+
const reserved = reserveProjectionDirectory(input);
|
|
153
|
+
return publishReservedProjection(pact, reserved);
|
|
154
|
+
}
|