@davidahmann/mill 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +335 -0
- package/dist/audit/repository.d.ts +8 -0
- package/dist/audit/repository.d.ts.map +1 -0
- package/dist/audit/repository.js +232 -0
- package/dist/audit/repository.js.map +1 -0
- package/dist/cli-program.d.ts +12 -0
- package/dist/cli-program.d.ts.map +1 -0
- package/dist/cli-program.js +1020 -0
- package/dist/cli-program.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/lock.d.ts +12 -0
- package/dist/config/lock.d.ts.map +1 -0
- package/dist/config/lock.js +83 -0
- package/dist/config/lock.js.map +1 -0
- package/dist/contracts/canonical.d.ts +7 -0
- package/dist/contracts/canonical.d.ts.map +1 -0
- package/dist/contracts/canonical.js +30 -0
- package/dist/contracts/canonical.js.map +1 -0
- package/dist/contracts/schemas.d.ts +2705 -0
- package/dist/contracts/schemas.d.ts.map +1 -0
- package/dist/contracts/schemas.js +988 -0
- package/dist/contracts/schemas.js.map +1 -0
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +138 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +31 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/intake/prd.d.ts +14 -0
- package/dist/intake/prd.d.ts.map +1 -0
- package/dist/intake/prd.js +39 -0
- package/dist/intake/prd.js.map +1 -0
- package/dist/planning/impact.d.ts +59 -0
- package/dist/planning/impact.d.ts.map +1 -0
- package/dist/planning/impact.js +400 -0
- package/dist/planning/impact.js.map +1 -0
- package/dist/planning/specification.d.ts +42 -0
- package/dist/planning/specification.d.ts.map +1 -0
- package/dist/planning/specification.js +235 -0
- package/dist/planning/specification.js.map +1 -0
- package/dist/qualification/public-alpha.d.ts +16 -0
- package/dist/qualification/public-alpha.d.ts.map +1 -0
- package/dist/qualification/public-alpha.js +128 -0
- package/dist/qualification/public-alpha.js.map +1 -0
- package/dist/recipes/node-typescript-next-web.d.ts +19 -0
- package/dist/recipes/node-typescript-next-web.d.ts.map +1 -0
- package/dist/recipes/node-typescript-next-web.js +136 -0
- package/dist/recipes/node-typescript-next-web.js.map +1 -0
- package/dist/repository/integration.d.ts +58 -0
- package/dist/repository/integration.d.ts.map +1 -0
- package/dist/repository/integration.js +1385 -0
- package/dist/repository/integration.js.map +1 -0
- package/dist/repository/scan.d.ts +27 -0
- package/dist/repository/scan.d.ts.map +1 -0
- package/dist/repository/scan.js +388 -0
- package/dist/repository/scan.js.map +1 -0
- package/dist/result.d.ts +23 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/result.js +24 -0
- package/dist/result.js.map +1 -0
- package/dist/runtime/codex.d.ts +28 -0
- package/dist/runtime/codex.d.ts.map +1 -0
- package/dist/runtime/codex.js +439 -0
- package/dist/runtime/codex.js.map +1 -0
- package/dist/runtime/context.d.ts +10 -0
- package/dist/runtime/context.d.ts.map +1 -0
- package/dist/runtime/context.js +151 -0
- package/dist/runtime/context.js.map +1 -0
- package/dist/runtime/delivery.d.ts +58 -0
- package/dist/runtime/delivery.d.ts.map +1 -0
- package/dist/runtime/delivery.js +1073 -0
- package/dist/runtime/delivery.js.map +1 -0
- package/dist/runtime/dependencies.d.ts +19 -0
- package/dist/runtime/dependencies.d.ts.map +1 -0
- package/dist/runtime/dependencies.js +495 -0
- package/dist/runtime/dependencies.js.map +1 -0
- package/dist/runtime/github.d.ts +112 -0
- package/dist/runtime/github.d.ts.map +1 -0
- package/dist/runtime/github.js +510 -0
- package/dist/runtime/github.js.map +1 -0
- package/dist/runtime/inputs.d.ts +24 -0
- package/dist/runtime/inputs.d.ts.map +1 -0
- package/dist/runtime/inputs.js +237 -0
- package/dist/runtime/inputs.js.map +1 -0
- package/dist/runtime/lease.d.ts +11 -0
- package/dist/runtime/lease.d.ts.map +1 -0
- package/dist/runtime/lease.js +52 -0
- package/dist/runtime/lease.js.map +1 -0
- package/dist/runtime/lifecycle.d.ts +87 -0
- package/dist/runtime/lifecycle.d.ts.map +1 -0
- package/dist/runtime/lifecycle.js +1024 -0
- package/dist/runtime/lifecycle.js.map +1 -0
- package/dist/runtime/process.d.ts +39 -0
- package/dist/runtime/process.d.ts.map +1 -0
- package/dist/runtime/process.js +304 -0
- package/dist/runtime/process.js.map +1 -0
- package/dist/runtime/repository.d.ts +36 -0
- package/dist/runtime/repository.d.ts.map +1 -0
- package/dist/runtime/repository.js +458 -0
- package/dist/runtime/repository.js.map +1 -0
- package/dist/runtime/state.d.ts +121 -0
- package/dist/runtime/state.d.ts.map +1 -0
- package/dist/runtime/state.js +1017 -0
- package/dist/runtime/state.js.map +1 -0
- package/dist/runtime/verifier.d.ts +31 -0
- package/dist/runtime/verifier.d.ts.map +1 -0
- package/dist/runtime/verifier.js +478 -0
- package/dist/runtime/verifier.js.map +1 -0
- package/dist/runtime/worker.d.ts +67 -0
- package/dist/runtime/worker.d.ts.map +1 -0
- package/dist/runtime/worker.js +34 -0
- package/dist/runtime/worker.js.map +1 -0
- package/dist/security/safe-path.d.ts +3 -0
- package/dist/security/safe-path.d.ts.map +1 -0
- package/dist/security/safe-path.js +102 -0
- package/dist/security/safe-path.js.map +1 -0
- package/dist/version.d.ts +4 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/workflows/founder.d.ts +33 -0
- package/dist/workflows/founder.d.ts.map +1 -0
- package/dist/workflows/founder.js +422 -0
- package/dist/workflows/founder.js.map +1 -0
- package/package.json +79 -0
- package/recipes/node-typescript-next-web/.github/workflows/ci.yml +34 -0
- package/recipes/node-typescript-next-web/.node-version +1 -0
- package/recipes/node-typescript-next-web/.tool-versions +1 -0
- package/recipes/node-typescript-next-web/AGENTS.md +44 -0
- package/recipes/node-typescript-next-web/README.md +16 -0
- package/recipes/node-typescript-next-web/WORKFLOW.md +6 -0
- package/recipes/node-typescript-next-web/app/api/health/route.ts +5 -0
- package/recipes/node-typescript-next-web/app/layout.tsx +19 -0
- package/recipes/node-typescript-next-web/app/page.tsx +12 -0
- package/recipes/node-typescript-next-web/app/styles.css +21 -0
- package/recipes/node-typescript-next-web/docs/development.md +16 -0
- package/recipes/node-typescript-next-web/eslint.config.mjs +9 -0
- package/recipes/node-typescript-next-web/gitignore.template +5 -0
- package/recipes/node-typescript-next-web/next-env.d.ts +7 -0
- package/recipes/node-typescript-next-web/next.config.ts +11 -0
- package/recipes/node-typescript-next-web/package-lock.json +7540 -0
- package/recipes/node-typescript-next-web/package.json +39 -0
- package/recipes/node-typescript-next-web/playwright.config.ts +23 -0
- package/recipes/node-typescript-next-web/recipe.yaml +71 -0
- package/recipes/node-typescript-next-web/scripts/check-package.mjs +3 -0
- package/recipes/node-typescript-next-web/src/health.ts +3 -0
- package/recipes/node-typescript-next-web/test/browser/home.spec.ts +14 -0
- package/recipes/node-typescript-next-web/test/integration/health-route.test.ts +11 -0
- package/recipes/node-typescript-next-web/test/unit/health.test.ts +9 -0
- package/recipes/node-typescript-next-web/tsconfig.json +33 -0
- package/schemas/README.md +64 -0
- package/schemas/audit-report.schema.json +59 -0
- package/schemas/blueprint.schema.json +34 -0
- package/schemas/context-manifest.schema.json +88 -0
- package/schemas/delivery-record.schema.json +266 -0
- package/schemas/impact-manifest.schema.json +146 -0
- package/schemas/managed-repository.schema.json +36 -0
- package/schemas/mill-config.schema.json +212 -0
- package/schemas/mill-lock.schema.json +96 -0
- package/schemas/outcome-plan.schema.json +51 -0
- package/schemas/product-contract.schema.json +181 -0
- package/schemas/public-alpha-qualification.schema.json +391 -0
- package/schemas/recipe-manifest.schema.json +187 -0
- package/schemas/release-evidence.schema.json +170 -0
- package/schemas/repository-integration-plan.schema.json +156 -0
- package/schemas/review-result.schema.json +57 -0
- package/schemas/scenario-set.schema.json +84 -0
- package/schemas/source-manifest.schema.json +96 -0
- package/schemas/specification-proposal.schema.json +406 -0
- package/schemas/support-tuple.schema.json +107 -0
- package/schemas/task-packet.schema.json +395 -0
- package/schemas/validation-evidence.schema.json +125 -0
- package/schemas/worker-invocation.schema.json +119 -0
- package/schemas/worker-profile.schema.json +53 -0
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { access, chmod, copyFile, lstat, mkdir, readdir, rename, rm, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { backup, DatabaseSync } from "node:sqlite";
|
|
7
|
+
import { ExitCode, MillError } from "../errors.js";
|
|
8
|
+
import { isWithin } from "../security/safe-path.js";
|
|
9
|
+
import { acquireExclusiveLease } from "./lease.js";
|
|
10
|
+
export function publicRunRecord(run) {
|
|
11
|
+
const publicRun = { ...run };
|
|
12
|
+
delete publicRun.worktreePath;
|
|
13
|
+
delete publicRun.contextJson;
|
|
14
|
+
delete publicRun.controlJson;
|
|
15
|
+
delete publicRun.activeProcessId;
|
|
16
|
+
delete publicRun.activeProcessGroup;
|
|
17
|
+
delete publicRun.activeProcessIdentity;
|
|
18
|
+
delete publicRun.deliveryJson;
|
|
19
|
+
delete publicRun.remoteFeedbackJson;
|
|
20
|
+
return publicRun;
|
|
21
|
+
}
|
|
22
|
+
const terminal = new Set(["closed", "cancelled", "failed", "stale"]);
|
|
23
|
+
export function isTerminalRun(status) {
|
|
24
|
+
return terminal.has(status);
|
|
25
|
+
}
|
|
26
|
+
export function isPurgeSafeRun(status) {
|
|
27
|
+
return status === "reviewed" || isTerminalRun(status);
|
|
28
|
+
}
|
|
29
|
+
const transitions = {
|
|
30
|
+
approved: ["ready", "blocked", "cancelled", "failed"],
|
|
31
|
+
ready: ["running", "blocked", "cancelled", "failed", "stale"],
|
|
32
|
+
running: ["committed", "blocked", "cancelled", "failed", "stale"],
|
|
33
|
+
committed: ["verified", "blocked", "cancelled", "failed", "stale"],
|
|
34
|
+
verified: ["reviewed", "blocked", "cancelled", "failed", "stale"],
|
|
35
|
+
reviewed: ["proposing", "cancelled", "failed", "stale"],
|
|
36
|
+
proposing: [
|
|
37
|
+
"effect_unknown",
|
|
38
|
+
"awaiting_ci",
|
|
39
|
+
"blocked",
|
|
40
|
+
"cancelled",
|
|
41
|
+
"failed",
|
|
42
|
+
"stale",
|
|
43
|
+
],
|
|
44
|
+
effect_unknown: [
|
|
45
|
+
"proposing",
|
|
46
|
+
"awaiting_ci",
|
|
47
|
+
"blocked",
|
|
48
|
+
"cancelled",
|
|
49
|
+
"failed",
|
|
50
|
+
"stale",
|
|
51
|
+
],
|
|
52
|
+
awaiting_ci: [
|
|
53
|
+
"awaiting_human",
|
|
54
|
+
"blocked",
|
|
55
|
+
"effect_unknown",
|
|
56
|
+
"cancelled",
|
|
57
|
+
"failed",
|
|
58
|
+
"stale",
|
|
59
|
+
],
|
|
60
|
+
awaiting_human: ["merged", "blocked", "failed", "stale"],
|
|
61
|
+
merged: ["post_merge_verified", "blocked", "failed", "stale"],
|
|
62
|
+
post_merge_verified: ["closed", "blocked", "failed", "stale"],
|
|
63
|
+
closed: [],
|
|
64
|
+
blocked: [
|
|
65
|
+
"ready",
|
|
66
|
+
"running",
|
|
67
|
+
"committed",
|
|
68
|
+
"verified",
|
|
69
|
+
"proposing",
|
|
70
|
+
"awaiting_ci",
|
|
71
|
+
"awaiting_human",
|
|
72
|
+
"merged",
|
|
73
|
+
"post_merge_verified",
|
|
74
|
+
"cancelled",
|
|
75
|
+
"failed",
|
|
76
|
+
"stale",
|
|
77
|
+
],
|
|
78
|
+
cancelled: [],
|
|
79
|
+
failed: [],
|
|
80
|
+
stale: [],
|
|
81
|
+
};
|
|
82
|
+
function stateRoot() {
|
|
83
|
+
const configured = process.env.MILL_STATE_HOME;
|
|
84
|
+
if (configured !== undefined) {
|
|
85
|
+
if (!path.isAbsolute(configured)) {
|
|
86
|
+
throw new MillError("INVALID_STATE_HOME", "MILL_STATE_HOME must be absolute.", ExitCode.configuration);
|
|
87
|
+
}
|
|
88
|
+
return configured;
|
|
89
|
+
}
|
|
90
|
+
if (process.platform === "darwin") {
|
|
91
|
+
return path.join(homedir(), "Library", "Application Support", "mill");
|
|
92
|
+
}
|
|
93
|
+
const xdg = process.env.XDG_STATE_HOME;
|
|
94
|
+
return xdg !== undefined && path.isAbsolute(xdg)
|
|
95
|
+
? path.join(xdg, "mill")
|
|
96
|
+
: path.join(homedir(), ".local", "state", "mill");
|
|
97
|
+
}
|
|
98
|
+
function namespace(repositoryId, commonDirectory) {
|
|
99
|
+
return createHash("sha256")
|
|
100
|
+
.update(`${repositoryId}\0${path.resolve(commonDirectory)}`, "utf8")
|
|
101
|
+
.digest("hex");
|
|
102
|
+
}
|
|
103
|
+
export function repositoryStateDirectory(repositoryId, commonDirectory) {
|
|
104
|
+
return path.join(stateRoot(), "repositories", namespace(repositoryId, commonDirectory));
|
|
105
|
+
}
|
|
106
|
+
function fromRow(row) {
|
|
107
|
+
return {
|
|
108
|
+
id: row.id,
|
|
109
|
+
repositoryId: row.repository_id,
|
|
110
|
+
taskId: row.task_id,
|
|
111
|
+
taskDigest: row.task_digest,
|
|
112
|
+
configDigest: row.config_digest,
|
|
113
|
+
status: row.status,
|
|
114
|
+
baseCommit: row.base_commit,
|
|
115
|
+
...(row.worktree_path === null ? {} : { worktreePath: row.worktree_path }),
|
|
116
|
+
...(row.context_digest === null
|
|
117
|
+
? {}
|
|
118
|
+
: { contextDigest: row.context_digest }),
|
|
119
|
+
...(row.context_json === null ? {} : { contextJson: row.context_json }),
|
|
120
|
+
...(row.control_json === null ? {} : { controlJson: row.control_json }),
|
|
121
|
+
...(row.candidate_commit === null
|
|
122
|
+
? {}
|
|
123
|
+
: { candidateCommit: row.candidate_commit }),
|
|
124
|
+
...(row.candidate_tree === null
|
|
125
|
+
? {}
|
|
126
|
+
: { candidateTree: row.candidate_tree }),
|
|
127
|
+
deadlineAt: row.deadline_at,
|
|
128
|
+
...(row.active_process_id === null
|
|
129
|
+
? {}
|
|
130
|
+
: { activeProcessId: row.active_process_id }),
|
|
131
|
+
...(row.active_pid === null ? {} : { activePid: row.active_pid }),
|
|
132
|
+
...(row.active_process_group === null
|
|
133
|
+
? {}
|
|
134
|
+
: { activeProcessGroup: row.active_process_group }),
|
|
135
|
+
...(row.active_process_identity === null
|
|
136
|
+
? {}
|
|
137
|
+
: { activeProcessIdentity: row.active_process_identity }),
|
|
138
|
+
cancelRequested: row.cancel_requested === 1,
|
|
139
|
+
repairCount: row.repair_count,
|
|
140
|
+
attemptCount: row.attempt_count,
|
|
141
|
+
...(row.block_code === null ? {} : { blockCode: row.block_code }),
|
|
142
|
+
...(row.validation_json === null
|
|
143
|
+
? {}
|
|
144
|
+
: { validationJson: row.validation_json }),
|
|
145
|
+
...(row.review_json === null ? {} : { reviewJson: row.review_json }),
|
|
146
|
+
...(row.delivery_json === null ? {} : { deliveryJson: row.delivery_json }),
|
|
147
|
+
...(row.remote_feedback_json === null
|
|
148
|
+
? {}
|
|
149
|
+
: { remoteFeedbackJson: row.remote_feedback_json }),
|
|
150
|
+
createdAt: row.created_at,
|
|
151
|
+
updatedAt: row.updated_at,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export class StateStore {
|
|
155
|
+
directory;
|
|
156
|
+
databasePath;
|
|
157
|
+
worktreesDirectory;
|
|
158
|
+
#database;
|
|
159
|
+
#closed = false;
|
|
160
|
+
constructor(directory, database) {
|
|
161
|
+
this.directory = directory;
|
|
162
|
+
this.databasePath = path.join(directory, "state.sqlite3");
|
|
163
|
+
this.worktreesDirectory = path.join(directory, "worktrees");
|
|
164
|
+
this.#database = database;
|
|
165
|
+
}
|
|
166
|
+
static async open(repositoryId, commonDirectory) {
|
|
167
|
+
const directory = repositoryStateDirectory(repositoryId, commonDirectory);
|
|
168
|
+
await mkdir(path.join(directory, "worktrees"), {
|
|
169
|
+
recursive: true,
|
|
170
|
+
mode: 0o700,
|
|
171
|
+
});
|
|
172
|
+
await chmod(directory, 0o700);
|
|
173
|
+
await chmod(path.join(directory, "worktrees"), 0o700);
|
|
174
|
+
const databasePath = path.join(directory, "state.sqlite3");
|
|
175
|
+
const database = new DatabaseSync(databasePath, {
|
|
176
|
+
timeout: 5_000,
|
|
177
|
+
allowExtension: false,
|
|
178
|
+
enableDoubleQuotedStringLiterals: false,
|
|
179
|
+
});
|
|
180
|
+
database.exec(`
|
|
181
|
+
PRAGMA journal_mode = WAL;
|
|
182
|
+
PRAGMA synchronous = FULL;
|
|
183
|
+
PRAGMA foreign_keys = ON;
|
|
184
|
+
PRAGMA trusted_schema = OFF;
|
|
185
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
186
|
+
key TEXT PRIMARY KEY,
|
|
187
|
+
value TEXT NOT NULL
|
|
188
|
+
) STRICT;
|
|
189
|
+
INSERT OR IGNORE INTO metadata(key, value) VALUES ('schema_version', '2');
|
|
190
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
191
|
+
id TEXT PRIMARY KEY,
|
|
192
|
+
repository_id TEXT NOT NULL,
|
|
193
|
+
task_id TEXT NOT NULL,
|
|
194
|
+
task_digest TEXT NOT NULL,
|
|
195
|
+
config_digest TEXT NOT NULL,
|
|
196
|
+
status TEXT NOT NULL,
|
|
197
|
+
base_commit TEXT NOT NULL,
|
|
198
|
+
worktree_path TEXT,
|
|
199
|
+
context_digest TEXT,
|
|
200
|
+
context_json TEXT,
|
|
201
|
+
control_json TEXT,
|
|
202
|
+
candidate_commit TEXT,
|
|
203
|
+
candidate_tree TEXT,
|
|
204
|
+
deadline_at TEXT NOT NULL,
|
|
205
|
+
active_process_id TEXT,
|
|
206
|
+
active_pid INTEGER,
|
|
207
|
+
active_process_group INTEGER,
|
|
208
|
+
active_process_identity TEXT,
|
|
209
|
+
cancel_requested INTEGER NOT NULL DEFAULT 0 CHECK(cancel_requested IN (0, 1)),
|
|
210
|
+
repair_count INTEGER NOT NULL DEFAULT 0 CHECK(repair_count BETWEEN 0 AND 1),
|
|
211
|
+
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK(attempt_count BETWEEN 0 AND 2),
|
|
212
|
+
block_code TEXT,
|
|
213
|
+
validation_json TEXT,
|
|
214
|
+
review_json TEXT,
|
|
215
|
+
delivery_json TEXT,
|
|
216
|
+
remote_feedback_json TEXT,
|
|
217
|
+
created_at TEXT NOT NULL,
|
|
218
|
+
updated_at TEXT NOT NULL
|
|
219
|
+
) STRICT;
|
|
220
|
+
CREATE TABLE IF NOT EXISTS run_events (
|
|
221
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
222
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
223
|
+
occurred_at TEXT NOT NULL,
|
|
224
|
+
type TEXT NOT NULL,
|
|
225
|
+
data_json TEXT NOT NULL
|
|
226
|
+
) STRICT;
|
|
227
|
+
CREATE TABLE IF NOT EXISTS baseline_qualifications (
|
|
228
|
+
approval_digest TEXT PRIMARY KEY,
|
|
229
|
+
repository_id TEXT NOT NULL,
|
|
230
|
+
task_digest TEXT NOT NULL,
|
|
231
|
+
config_digest TEXT NOT NULL,
|
|
232
|
+
base_commit TEXT NOT NULL,
|
|
233
|
+
evidence_digest TEXT NOT NULL,
|
|
234
|
+
created_at TEXT NOT NULL
|
|
235
|
+
) STRICT;
|
|
236
|
+
CREATE TABLE IF NOT EXISTS worker_invocations (
|
|
237
|
+
id TEXT PRIMARY KEY,
|
|
238
|
+
run_id TEXT NOT NULL REFERENCES runs(id),
|
|
239
|
+
phase TEXT NOT NULL,
|
|
240
|
+
envelope_digest TEXT NOT NULL,
|
|
241
|
+
envelope_json TEXT NOT NULL,
|
|
242
|
+
created_at TEXT NOT NULL
|
|
243
|
+
) STRICT;
|
|
244
|
+
CREATE TABLE IF NOT EXISTS worker_invocation_events (
|
|
245
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
246
|
+
invocation_id TEXT NOT NULL REFERENCES worker_invocations(id),
|
|
247
|
+
occurred_at TEXT NOT NULL,
|
|
248
|
+
type TEXT NOT NULL,
|
|
249
|
+
data_json TEXT NOT NULL
|
|
250
|
+
) STRICT;
|
|
251
|
+
CREATE TRIGGER IF NOT EXISTS run_events_no_update
|
|
252
|
+
BEFORE UPDATE ON run_events BEGIN SELECT RAISE(ABORT, 'run events are append-only'); END;
|
|
253
|
+
CREATE TRIGGER IF NOT EXISTS run_events_no_delete
|
|
254
|
+
BEFORE DELETE ON run_events BEGIN SELECT RAISE(ABORT, 'run events are append-only'); END;
|
|
255
|
+
CREATE TRIGGER IF NOT EXISTS worker_invocations_no_update
|
|
256
|
+
BEFORE UPDATE ON worker_invocations BEGIN SELECT RAISE(ABORT, 'worker invocations are immutable'); END;
|
|
257
|
+
CREATE TRIGGER IF NOT EXISTS worker_invocations_no_delete
|
|
258
|
+
BEFORE DELETE ON worker_invocations BEGIN SELECT RAISE(ABORT, 'worker invocations are immutable'); END;
|
|
259
|
+
CREATE TRIGGER IF NOT EXISTS worker_invocation_events_no_update
|
|
260
|
+
BEFORE UPDATE ON worker_invocation_events BEGIN SELECT RAISE(ABORT, 'worker invocation events are append-only'); END;
|
|
261
|
+
CREATE TRIGGER IF NOT EXISTS worker_invocation_events_no_delete
|
|
262
|
+
BEFORE DELETE ON worker_invocation_events BEGIN SELECT RAISE(ABORT, 'worker invocation events are append-only'); END;
|
|
263
|
+
`);
|
|
264
|
+
const runColumns = database
|
|
265
|
+
.prepare("PRAGMA table_info(runs)")
|
|
266
|
+
.all();
|
|
267
|
+
for (const column of [
|
|
268
|
+
"active_process_id TEXT",
|
|
269
|
+
"active_process_group INTEGER",
|
|
270
|
+
"active_process_identity TEXT",
|
|
271
|
+
"delivery_json TEXT",
|
|
272
|
+
"remote_feedback_json TEXT",
|
|
273
|
+
]) {
|
|
274
|
+
const name = column.split(" ")[0];
|
|
275
|
+
if (!runColumns.some((candidate) => candidate.name === name)) {
|
|
276
|
+
database.exec(`ALTER TABLE runs ADD COLUMN ${column}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const version = database
|
|
280
|
+
.prepare("SELECT value FROM metadata WHERE key = 'schema_version'")
|
|
281
|
+
.get();
|
|
282
|
+
if (version?.value !== "1" && version?.value !== "2") {
|
|
283
|
+
database.close();
|
|
284
|
+
throw new MillError("UNSUPPORTED_STATE_SCHEMA", "Operational state uses an unsupported schema version.", ExitCode.configuration);
|
|
285
|
+
}
|
|
286
|
+
if (version.value === "1") {
|
|
287
|
+
database
|
|
288
|
+
.prepare("UPDATE metadata SET value = '2' WHERE key = 'schema_version'")
|
|
289
|
+
.run();
|
|
290
|
+
}
|
|
291
|
+
await chmod(databasePath, 0o600);
|
|
292
|
+
return new StateStore(directory, database);
|
|
293
|
+
}
|
|
294
|
+
close() {
|
|
295
|
+
if (!this.#closed) {
|
|
296
|
+
this.#database.close();
|
|
297
|
+
this.#closed = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
createRun(input) {
|
|
301
|
+
const id = randomUUID();
|
|
302
|
+
const now = new Date().toISOString();
|
|
303
|
+
this.#transaction(() => {
|
|
304
|
+
this.#database
|
|
305
|
+
.prepare(`INSERT INTO runs(
|
|
306
|
+
id, repository_id, task_id, task_digest, config_digest, status,
|
|
307
|
+
base_commit, deadline_at, created_at, updated_at
|
|
308
|
+
) VALUES (?, ?, ?, ?, ?, 'approved', ?, ?, ?, ?)`)
|
|
309
|
+
.run(id, input.repositoryId, input.taskId, input.taskDigest, input.configDigest, input.baseCommit, input.deadlineAt, now, now);
|
|
310
|
+
this.#event(id, "run.created", { status: "approved" });
|
|
311
|
+
});
|
|
312
|
+
return this.getRun(id);
|
|
313
|
+
}
|
|
314
|
+
getRun(id) {
|
|
315
|
+
const row = this.#database
|
|
316
|
+
.prepare("SELECT * FROM runs WHERE id = ?")
|
|
317
|
+
.get(id);
|
|
318
|
+
if (row === undefined) {
|
|
319
|
+
throw new MillError("RUN_NOT_FOUND", `Run not found: ${id}`, ExitCode.data);
|
|
320
|
+
}
|
|
321
|
+
return fromRow(row);
|
|
322
|
+
}
|
|
323
|
+
latestRun() {
|
|
324
|
+
const row = this.#database
|
|
325
|
+
.prepare("SELECT * FROM runs ORDER BY created_at DESC LIMIT 1")
|
|
326
|
+
.get();
|
|
327
|
+
return row === undefined ? undefined : fromRow(row);
|
|
328
|
+
}
|
|
329
|
+
runs() {
|
|
330
|
+
return this.#database
|
|
331
|
+
.prepare("SELECT * FROM runs ORDER BY created_at")
|
|
332
|
+
.all().map(fromRow);
|
|
333
|
+
}
|
|
334
|
+
transition(id, to, eventType, details = {}) {
|
|
335
|
+
this.#transaction(() => {
|
|
336
|
+
const current = this.getRun(id);
|
|
337
|
+
if (!transitions[current.status].includes(to)) {
|
|
338
|
+
throw new MillError("INVALID_RUN_TRANSITION", `Cannot transition run from ${current.status} to ${to}.`, ExitCode.configuration);
|
|
339
|
+
}
|
|
340
|
+
if (to === "running" && current.cancelRequested) {
|
|
341
|
+
throw new MillError("OPERATOR_CANCELLED", "A cancelled run cannot start or resume execution.", ExitCode.temporary);
|
|
342
|
+
}
|
|
343
|
+
if (to === "cancelled" && current.activeProcessId !== undefined) {
|
|
344
|
+
throw new MillError("CANCELLATION_IN_PROGRESS", "The active execution must exit or be reconciled before cancellation is terminal.", ExitCode.temporary);
|
|
345
|
+
}
|
|
346
|
+
const now = new Date().toISOString();
|
|
347
|
+
this.#database
|
|
348
|
+
.prepare("UPDATE runs SET status = ?, block_code = ?, updated_at = ? WHERE id = ?")
|
|
349
|
+
.run(to, typeof details.code === "string" ? details.code : null, now, id);
|
|
350
|
+
this.#event(id, eventType, { from: current.status, to, ...details });
|
|
351
|
+
});
|
|
352
|
+
return this.getRun(id);
|
|
353
|
+
}
|
|
354
|
+
setWorkspace(id, worktreePath, contextDigest, contextJson, controlJson) {
|
|
355
|
+
this.#transaction(() => {
|
|
356
|
+
const current = this.getRun(id);
|
|
357
|
+
if (current.status !== "ready" ||
|
|
358
|
+
current.worktreePath !== undefined ||
|
|
359
|
+
current.contextDigest !== undefined ||
|
|
360
|
+
current.contextJson !== undefined ||
|
|
361
|
+
current.controlJson !== undefined) {
|
|
362
|
+
throw new MillError("INVALID_RUN_TRANSITION", "A workspace can be bound exactly once while the run is ready.", ExitCode.configuration);
|
|
363
|
+
}
|
|
364
|
+
this.#database
|
|
365
|
+
.prepare("UPDATE runs SET worktree_path = ?, context_digest = ?, context_json = ?, control_json = ?, updated_at = ? WHERE id = ?")
|
|
366
|
+
.run(worktreePath, contextDigest, contextJson, controlJson, new Date().toISOString(), id);
|
|
367
|
+
this.#event(id, "workspace.created", { contextDigest });
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
commitCandidate(id, commit, tree, invocationId) {
|
|
371
|
+
this.#transaction(() => {
|
|
372
|
+
const current = this.getRun(id);
|
|
373
|
+
if (current.cancelRequested) {
|
|
374
|
+
throw new MillError("OPERATOR_CANCELLED", "A cancelled run cannot publish a candidate commit.", ExitCode.temporary);
|
|
375
|
+
}
|
|
376
|
+
if (current.status !== "running") {
|
|
377
|
+
throw new MillError("INVALID_RUN_TRANSITION", `Cannot commit a candidate from ${current.status}.`, ExitCode.configuration);
|
|
378
|
+
}
|
|
379
|
+
if (invocationId !== undefined) {
|
|
380
|
+
const invocation = this.#database
|
|
381
|
+
.prepare("SELECT run_id, phase FROM worker_invocations WHERE id = ?")
|
|
382
|
+
.get(invocationId);
|
|
383
|
+
if (invocation?.run_id !== id ||
|
|
384
|
+
(invocation.phase !== "build" && invocation.phase !== "repair") ||
|
|
385
|
+
this.workerInvocationStatus(invocationId) !== "launch_started") {
|
|
386
|
+
throw new MillError("WORKER_INVOCATION_SETTLEMENT_CONFLICT", "Candidate publication requires its one started mutating worker invocation.", ExitCode.configuration);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
this.#database
|
|
390
|
+
.prepare(`UPDATE runs SET candidate_commit = ?, candidate_tree = ?,
|
|
391
|
+
validation_json = NULL, review_json = NULL,
|
|
392
|
+
remote_feedback_json = NULL, status = 'committed',
|
|
393
|
+
block_code = NULL, updated_at = ? WHERE id = ?`)
|
|
394
|
+
.run(commit, tree, new Date().toISOString(), id);
|
|
395
|
+
this.#event(id, "candidate.committed", {
|
|
396
|
+
from: current.status,
|
|
397
|
+
to: "committed",
|
|
398
|
+
commit,
|
|
399
|
+
tree,
|
|
400
|
+
});
|
|
401
|
+
if (invocationId !== undefined) {
|
|
402
|
+
this.#invocationEvent(invocationId, "settled", {
|
|
403
|
+
outcome: "completed",
|
|
404
|
+
candidateCommit: commit,
|
|
405
|
+
candidateTree: tree,
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
return this.getRun(id);
|
|
410
|
+
}
|
|
411
|
+
completeValidation(id, value, passed) {
|
|
412
|
+
this.#transaction(() => {
|
|
413
|
+
const current = this.getRun(id);
|
|
414
|
+
if (current.cancelRequested) {
|
|
415
|
+
throw new MillError("OPERATOR_CANCELLED", "A cancelled run cannot publish validation evidence.", ExitCode.temporary);
|
|
416
|
+
}
|
|
417
|
+
if (current.status !== "committed") {
|
|
418
|
+
throw new MillError("INVALID_RUN_TRANSITION", `Cannot record validation from ${current.status}.`, ExitCode.configuration);
|
|
419
|
+
}
|
|
420
|
+
const status = passed ? "verified" : "blocked";
|
|
421
|
+
const code = passed ? null : "VALIDATION_FAILED";
|
|
422
|
+
this.#database
|
|
423
|
+
.prepare("UPDATE runs SET validation_json = ?, status = ?, block_code = ?, updated_at = ? WHERE id = ?")
|
|
424
|
+
.run(value, status, code, new Date().toISOString(), id);
|
|
425
|
+
this.#event(id, passed ? "validation.passed" : "validation.failed", {
|
|
426
|
+
from: current.status,
|
|
427
|
+
to: status,
|
|
428
|
+
...(code === null ? {} : { code }),
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
return this.getRun(id);
|
|
432
|
+
}
|
|
433
|
+
completeReview(id, value, findings, nonConverged, invocationId, completionDetails = {}) {
|
|
434
|
+
this.#transaction(() => {
|
|
435
|
+
const current = this.getRun(id);
|
|
436
|
+
if (current.cancelRequested) {
|
|
437
|
+
throw new MillError("OPERATOR_CANCELLED", "A cancelled run cannot publish review evidence.", ExitCode.temporary);
|
|
438
|
+
}
|
|
439
|
+
if (current.status !== "verified") {
|
|
440
|
+
throw new MillError("INVALID_RUN_TRANSITION", `Cannot record review from ${current.status}.`, ExitCode.configuration);
|
|
441
|
+
}
|
|
442
|
+
const invocation = this.#database
|
|
443
|
+
.prepare("SELECT run_id, phase FROM worker_invocations WHERE id = ?")
|
|
444
|
+
.get(invocationId);
|
|
445
|
+
if (invocation?.run_id !== id ||
|
|
446
|
+
invocation.phase !== "review" ||
|
|
447
|
+
this.workerInvocationStatus(invocationId) !== "launch_started") {
|
|
448
|
+
throw new MillError("WORKER_INVOCATION_SETTLEMENT_CONFLICT", "Review publication requires its one started reviewer invocation.", ExitCode.configuration);
|
|
449
|
+
}
|
|
450
|
+
const status = findings === 0 ? "reviewed" : "blocked";
|
|
451
|
+
const code = findings === 0
|
|
452
|
+
? null
|
|
453
|
+
: nonConverged
|
|
454
|
+
? "REVIEW_NON_CONVERGENCE"
|
|
455
|
+
: "REVIEW_FINDINGS";
|
|
456
|
+
this.#database
|
|
457
|
+
.prepare("UPDATE runs SET review_json = ?, status = ?, block_code = ?, updated_at = ? WHERE id = ?")
|
|
458
|
+
.run(value, status, code, new Date().toISOString(), id);
|
|
459
|
+
this.#event(id, "review.completed", {
|
|
460
|
+
...completionDetails,
|
|
461
|
+
candidateCommit: current.candidateCommit ?? null,
|
|
462
|
+
findings,
|
|
463
|
+
});
|
|
464
|
+
this.#event(id, findings === 0 ? "review.passed" : "review.blocked", {
|
|
465
|
+
from: current.status,
|
|
466
|
+
to: status,
|
|
467
|
+
findings,
|
|
468
|
+
...(code === null ? {} : { code }),
|
|
469
|
+
});
|
|
470
|
+
this.#invocationEvent(invocationId, "settled", {
|
|
471
|
+
outcome: "completed",
|
|
472
|
+
candidateCommit: current.candidateCommit ?? null,
|
|
473
|
+
findings,
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
return this.getRun(id);
|
|
477
|
+
}
|
|
478
|
+
setDelivery(id, deliveryJson, eventType, details = {}) {
|
|
479
|
+
this.#transaction(() => {
|
|
480
|
+
this.getRun(id);
|
|
481
|
+
this.#database
|
|
482
|
+
.prepare("UPDATE runs SET delivery_json = ?, updated_at = ? WHERE id = ?")
|
|
483
|
+
.run(deliveryJson, new Date().toISOString(), id);
|
|
484
|
+
this.#event(id, eventType, details);
|
|
485
|
+
});
|
|
486
|
+
return this.getRun(id);
|
|
487
|
+
}
|
|
488
|
+
setRemoteFeedback(id, feedbackJson) {
|
|
489
|
+
this.#transaction(() => {
|
|
490
|
+
this.getRun(id);
|
|
491
|
+
this.#database
|
|
492
|
+
.prepare("UPDATE runs SET remote_feedback_json = ?, updated_at = ? WHERE id = ?")
|
|
493
|
+
.run(feedbackJson, new Date().toISOString(), id);
|
|
494
|
+
this.#event(id, "remote.feedback_recorded", {});
|
|
495
|
+
});
|
|
496
|
+
return this.getRun(id);
|
|
497
|
+
}
|
|
498
|
+
replaceBlocker(id, code, eventType, details = {}) {
|
|
499
|
+
this.#transaction(() => {
|
|
500
|
+
const current = this.getRun(id);
|
|
501
|
+
if (current.status !== "blocked") {
|
|
502
|
+
throw new MillError("INVALID_RUN_TRANSITION", `Cannot replace a blocker while run is ${current.status}.`, ExitCode.configuration);
|
|
503
|
+
}
|
|
504
|
+
this.#database
|
|
505
|
+
.prepare("UPDATE runs SET block_code = ?, updated_at = ? WHERE id = ?")
|
|
506
|
+
.run(code, new Date().toISOString(), id);
|
|
507
|
+
this.#event(id, eventType, {
|
|
508
|
+
from: current.blockCode ?? null,
|
|
509
|
+
to: code,
|
|
510
|
+
...details,
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
return this.getRun(id);
|
|
514
|
+
}
|
|
515
|
+
beginRepair(id) {
|
|
516
|
+
this.#transaction(() => {
|
|
517
|
+
const current = this.getRun(id);
|
|
518
|
+
if (current.cancelRequested) {
|
|
519
|
+
throw new MillError("OPERATOR_CANCELLED", "A cancelled run cannot begin a repair execution.", ExitCode.temporary);
|
|
520
|
+
}
|
|
521
|
+
if (current.status !== "blocked" || current.repairCount >= 1) {
|
|
522
|
+
throw new MillError("REPAIR_BUDGET_EXHAUSTED", "The single systemic repair budget is exhausted or the run is not blocked.", ExitCode.configuration);
|
|
523
|
+
}
|
|
524
|
+
this.#database
|
|
525
|
+
.prepare("UPDATE runs SET repair_count = repair_count + 1, status = 'running', block_code = NULL, updated_at = ? WHERE id = ?")
|
|
526
|
+
.run(new Date().toISOString(), id);
|
|
527
|
+
this.#event(id, "repair.started", {
|
|
528
|
+
from: current.status,
|
|
529
|
+
to: "running",
|
|
530
|
+
repairCount: current.repairCount + 1,
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
return this.getRun(id);
|
|
534
|
+
}
|
|
535
|
+
beginReviewAttempt(id, maximum) {
|
|
536
|
+
let attempt = 0;
|
|
537
|
+
this.#transaction(() => {
|
|
538
|
+
const current = this.getRun(id);
|
|
539
|
+
if (current.cancelRequested) {
|
|
540
|
+
throw new MillError("OPERATOR_CANCELLED", "A cancelled run cannot begin a review attempt.", ExitCode.temporary);
|
|
541
|
+
}
|
|
542
|
+
if (current.status !== "verified") {
|
|
543
|
+
throw new MillError("INVALID_RUN_TRANSITION", `Cannot begin review from ${current.status}.`, ExitCode.configuration);
|
|
544
|
+
}
|
|
545
|
+
if (current.candidateCommit === undefined) {
|
|
546
|
+
throw new MillError("CANDIDATE_MISSING", "A review attempt requires an exact committed candidate.", ExitCode.configuration);
|
|
547
|
+
}
|
|
548
|
+
const row = this.#database
|
|
549
|
+
.prepare(`SELECT COUNT(*) AS count FROM run_events
|
|
550
|
+
WHERE run_id = ? AND type = 'review.started'
|
|
551
|
+
AND json_extract(data_json, '$.candidateCommit') = ?`)
|
|
552
|
+
.get(id, current.candidateCommit);
|
|
553
|
+
if (row.count >= maximum) {
|
|
554
|
+
throw new MillError("REVIEW_RETRY_BUDGET_EXHAUSTED", "The bounded review attempt budget is exhausted.", ExitCode.configuration);
|
|
555
|
+
}
|
|
556
|
+
attempt = row.count + 1;
|
|
557
|
+
this.#event(id, "review.started", {
|
|
558
|
+
candidateCommit: current.candidateCommit,
|
|
559
|
+
attempt,
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
return attempt;
|
|
563
|
+
}
|
|
564
|
+
recordBaselineQualification(input) {
|
|
565
|
+
this.#transaction(() => {
|
|
566
|
+
this.#database
|
|
567
|
+
.prepare(`INSERT OR IGNORE INTO baseline_qualifications(
|
|
568
|
+
approval_digest, repository_id, task_digest, config_digest,
|
|
569
|
+
base_commit, evidence_digest, created_at
|
|
570
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
571
|
+
.run(input.approvalDigest, input.repositoryId, input.taskDigest, input.configDigest, input.baseCommit, input.evidenceDigest, new Date().toISOString());
|
|
572
|
+
const stored = this.#database
|
|
573
|
+
.prepare(`SELECT repository_id, task_digest, config_digest, base_commit,
|
|
574
|
+
evidence_digest
|
|
575
|
+
FROM baseline_qualifications WHERE approval_digest = ?`)
|
|
576
|
+
.get(input.approvalDigest);
|
|
577
|
+
if (stored?.repository_id !== input.repositoryId ||
|
|
578
|
+
stored.task_digest !== input.taskDigest ||
|
|
579
|
+
stored.config_digest !== input.configDigest ||
|
|
580
|
+
stored.base_commit !== input.baseCommit ||
|
|
581
|
+
stored.evidence_digest !== input.evidenceDigest) {
|
|
582
|
+
throw new MillError("QUALIFICATION_IDENTITY_COLLISION", "Baseline qualification identity conflicts with stored state.", ExitCode.io);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
hasBaselineQualification(input) {
|
|
587
|
+
const row = this.#database
|
|
588
|
+
.prepare(`SELECT 1 AS present FROM baseline_qualifications
|
|
589
|
+
WHERE approval_digest = ? AND repository_id = ? AND task_digest = ?
|
|
590
|
+
AND config_digest = ? AND base_commit = ?`)
|
|
591
|
+
.get(input.approvalDigest, input.repositoryId, input.taskDigest, input.configDigest, input.baseCommit);
|
|
592
|
+
return row?.present === 1;
|
|
593
|
+
}
|
|
594
|
+
beginBuilderAttempt(id, maximum) {
|
|
595
|
+
if (this.getRun(id).cancelRequested) {
|
|
596
|
+
throw new MillError("OPERATOR_CANCELLED", "A cancelled run cannot consume another builder attempt.", ExitCode.temporary);
|
|
597
|
+
}
|
|
598
|
+
const result = this.#database
|
|
599
|
+
.prepare(`UPDATE runs SET attempt_count = attempt_count + 1, updated_at = ?
|
|
600
|
+
WHERE id = ? AND attempt_count < ? AND cancel_requested = 0`)
|
|
601
|
+
.run(new Date().toISOString(), id, maximum);
|
|
602
|
+
if (result.changes !== 1) {
|
|
603
|
+
if (this.getRun(id).cancelRequested) {
|
|
604
|
+
throw new MillError("OPERATOR_CANCELLED", "A cancelled run cannot consume another builder attempt.", ExitCode.temporary);
|
|
605
|
+
}
|
|
606
|
+
throw new MillError("BUILDER_RETRY_BUDGET_EXHAUSTED", "The builder retry budget is exhausted.", ExitCode.configuration);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
setActiveProcess(id, process) {
|
|
610
|
+
if (process !== null &&
|
|
611
|
+
(!/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(process.id) ||
|
|
612
|
+
!Number.isSafeInteger(process.pid) ||
|
|
613
|
+
process.pid <= 0 ||
|
|
614
|
+
process.processGroup !== process.pid ||
|
|
615
|
+
!/^sha256:[a-f0-9]{64}$/u.test(process.identity))) {
|
|
616
|
+
throw new MillError("INVALID_PROCESS_IDENTITY", "An active PID and its opaque process-start identity must be stored together.", ExitCode.configuration);
|
|
617
|
+
}
|
|
618
|
+
if (process === null) {
|
|
619
|
+
this.#database
|
|
620
|
+
.prepare("UPDATE runs SET active_process_id = NULL, active_pid = NULL, active_process_group = NULL, active_process_identity = NULL, updated_at = ? WHERE id = ?")
|
|
621
|
+
.run(new Date().toISOString(), id);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const result = this.#database
|
|
625
|
+
.prepare(`UPDATE runs SET active_process_id = ?, active_pid = ?,
|
|
626
|
+
active_process_group = ?, active_process_identity = ?, updated_at = ?
|
|
627
|
+
WHERE id = ? AND cancel_requested = 0 AND active_process_id IS NULL`)
|
|
628
|
+
.run(process.id, process.pid, process.processGroup, process.identity, new Date().toISOString(), id);
|
|
629
|
+
if (result.changes !== 1) {
|
|
630
|
+
throw new MillError("ACTIVE_PROCESS_BINDING_REJECTED", "Cancellation or another active execution won before process binding.", ExitCode.temporary);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
clearActiveProcess(id, processId) {
|
|
634
|
+
this.#database
|
|
635
|
+
.prepare(`UPDATE runs SET active_process_id = NULL, active_pid = NULL,
|
|
636
|
+
active_process_group = NULL, active_process_identity = NULL,
|
|
637
|
+
updated_at = ? WHERE id = ? AND active_process_id = ?`)
|
|
638
|
+
.run(new Date().toISOString(), id, processId);
|
|
639
|
+
}
|
|
640
|
+
requestCancellation(id) {
|
|
641
|
+
this.#transaction(() => {
|
|
642
|
+
const run = this.getRun(id);
|
|
643
|
+
if (terminal.has(run.status))
|
|
644
|
+
return;
|
|
645
|
+
this.#database
|
|
646
|
+
.prepare("UPDATE runs SET cancel_requested = 1, updated_at = ? WHERE id = ?")
|
|
647
|
+
.run(new Date().toISOString(), id);
|
|
648
|
+
this.#event(id, "run.cancellation_requested", {});
|
|
649
|
+
});
|
|
650
|
+
return this.getRun(id);
|
|
651
|
+
}
|
|
652
|
+
async backup() {
|
|
653
|
+
const destination = path.join(this.directory, `state-backup-${new Date().toISOString().replaceAll(/[:.]/gu, "-")}.sqlite3`);
|
|
654
|
+
await backup(this.#database, destination);
|
|
655
|
+
await chmod(destination, 0o600);
|
|
656
|
+
return destination;
|
|
657
|
+
}
|
|
658
|
+
events(id) {
|
|
659
|
+
return this.#database
|
|
660
|
+
.prepare("SELECT sequence, occurred_at, type, data_json FROM run_events WHERE run_id = ? ORDER BY sequence")
|
|
661
|
+
.all(id)
|
|
662
|
+
.map((row) => {
|
|
663
|
+
const item = row;
|
|
664
|
+
return {
|
|
665
|
+
sequence: item.sequence,
|
|
666
|
+
occurredAt: item.occurred_at,
|
|
667
|
+
type: item.type,
|
|
668
|
+
data: JSON.parse(item.data_json),
|
|
669
|
+
};
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
recordEvent(id, type, data) {
|
|
673
|
+
this.#transaction(() => this.#event(id, type, data));
|
|
674
|
+
}
|
|
675
|
+
admitWorkerInvocation(input) {
|
|
676
|
+
let disposition = "existing";
|
|
677
|
+
this.#transaction(() => {
|
|
678
|
+
this.getRun(input.runId);
|
|
679
|
+
const result = this.#database
|
|
680
|
+
.prepare(`INSERT OR IGNORE INTO worker_invocations(
|
|
681
|
+
id, run_id, phase, envelope_digest, envelope_json, created_at
|
|
682
|
+
) VALUES (?, ?, ?, ?, ?, ?)`)
|
|
683
|
+
.run(input.invocationId, input.runId, input.phase, input.envelopeDigest, input.envelopeJson, new Date().toISOString());
|
|
684
|
+
const stored = this.#database
|
|
685
|
+
.prepare(`SELECT run_id, phase, envelope_digest, envelope_json
|
|
686
|
+
FROM worker_invocations WHERE id = ?`)
|
|
687
|
+
.get(input.invocationId);
|
|
688
|
+
if (stored?.run_id !== input.runId ||
|
|
689
|
+
stored.phase !== input.phase ||
|
|
690
|
+
stored.envelope_digest !== input.envelopeDigest ||
|
|
691
|
+
stored.envelope_json !== input.envelopeJson) {
|
|
692
|
+
throw new MillError("WORKER_INVOCATION_IDENTITY_CONFLICT", "Worker invocation identity was reused with a different immutable envelope.", ExitCode.configuration);
|
|
693
|
+
}
|
|
694
|
+
if (result.changes === 1) {
|
|
695
|
+
disposition = "created";
|
|
696
|
+
this.#invocationEvent(input.invocationId, "admitted", {
|
|
697
|
+
envelopeDigest: input.envelopeDigest,
|
|
698
|
+
});
|
|
699
|
+
this.#event(input.runId, "worker.admitted", {
|
|
700
|
+
invocationId: input.invocationId,
|
|
701
|
+
phase: input.phase,
|
|
702
|
+
envelopeDigest: input.envelopeDigest,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return disposition;
|
|
707
|
+
}
|
|
708
|
+
workerInvocationStatus(invocationId) {
|
|
709
|
+
const exists = this.#database
|
|
710
|
+
.prepare("SELECT 1 AS present FROM worker_invocations WHERE id = ?")
|
|
711
|
+
.get(invocationId);
|
|
712
|
+
if (exists?.present !== 1) {
|
|
713
|
+
throw new MillError("WORKER_INVOCATION_NOT_FOUND", "Worker invocation admission was not found.", ExitCode.data);
|
|
714
|
+
}
|
|
715
|
+
const events = this.#database
|
|
716
|
+
.prepare(`SELECT type FROM worker_invocation_events
|
|
717
|
+
WHERE invocation_id = ? ORDER BY sequence`)
|
|
718
|
+
.all(invocationId);
|
|
719
|
+
if (events.some((event) => event.type === "reconciled")) {
|
|
720
|
+
return "reconciled";
|
|
721
|
+
}
|
|
722
|
+
if (events.some((event) => event.type === "uncertain"))
|
|
723
|
+
return "uncertain";
|
|
724
|
+
if (events.some((event) => event.type === "settled"))
|
|
725
|
+
return "settled";
|
|
726
|
+
if (events.some((event) => event.type === "launch_started")) {
|
|
727
|
+
return "launch_started";
|
|
728
|
+
}
|
|
729
|
+
return "admitted";
|
|
730
|
+
}
|
|
731
|
+
markWorkerLaunchStarted(invocationId) {
|
|
732
|
+
this.#transaction(() => {
|
|
733
|
+
const status = this.workerInvocationStatus(invocationId);
|
|
734
|
+
if (status !== "admitted") {
|
|
735
|
+
throw new MillError("WORKER_INVOCATION_POSSIBLY_STARTED", "A worker invocation with this identity may already have started and cannot be replayed.", ExitCode.temporary, { status });
|
|
736
|
+
}
|
|
737
|
+
this.#invocationEvent(invocationId, "launch_started", {});
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
settleWorkerInvocation(invocationId, outcome, details = {}) {
|
|
741
|
+
this.#transaction(() => {
|
|
742
|
+
const status = this.workerInvocationStatus(invocationId);
|
|
743
|
+
if (status !== "launch_started") {
|
|
744
|
+
throw new MillError("WORKER_INVOCATION_SETTLEMENT_CONFLICT", "Worker settlement requires exactly one started, unsettled invocation.", ExitCode.configuration, { status });
|
|
745
|
+
}
|
|
746
|
+
this.#invocationEvent(invocationId, outcome === "uncertain" ? "uncertain" : "settled", { outcome, ...details });
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
recordWorkerProcessExit(runId, invocationId, processId) {
|
|
750
|
+
this.#transaction(() => {
|
|
751
|
+
this.getRun(runId);
|
|
752
|
+
const invocation = this.#database
|
|
753
|
+
.prepare("SELECT run_id FROM worker_invocations WHERE id = ?")
|
|
754
|
+
.get(invocationId);
|
|
755
|
+
if (invocation?.run_id !== runId ||
|
|
756
|
+
this.workerInvocationStatus(invocationId) !== "launch_started") {
|
|
757
|
+
throw new MillError("WORKER_PROCESS_EXIT_CONFLICT", "Process exit evidence requires its one started, unsettled worker invocation.", ExitCode.configuration);
|
|
758
|
+
}
|
|
759
|
+
const active = this.#database
|
|
760
|
+
.prepare("SELECT active_process_id FROM runs WHERE id = ?")
|
|
761
|
+
.get(runId);
|
|
762
|
+
if ((processId !== undefined && active.active_process_id !== processId) ||
|
|
763
|
+
(processId === undefined && active.active_process_id !== null)) {
|
|
764
|
+
throw new MillError("WORKER_PROCESS_EXIT_CONFLICT", "Process exit evidence does not match the active worker identity.", ExitCode.configuration);
|
|
765
|
+
}
|
|
766
|
+
this.#invocationEvent(invocationId, "process_exited", {
|
|
767
|
+
processId: processId ?? null,
|
|
768
|
+
});
|
|
769
|
+
this.#database
|
|
770
|
+
.prepare(`UPDATE runs SET active_process_id = NULL, active_pid = NULL,
|
|
771
|
+
active_process_group = NULL, active_process_identity = NULL,
|
|
772
|
+
updated_at = ? WHERE id = ?`)
|
|
773
|
+
.run(new Date().toISOString(), runId);
|
|
774
|
+
this.#event(runId, "worker.process_exited", { invocationId });
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
unresolvedMutatingWorkerInvocations(runId) {
|
|
778
|
+
this.getRun(runId);
|
|
779
|
+
const rows = this.#database
|
|
780
|
+
.prepare(`SELECT id, phase FROM worker_invocations
|
|
781
|
+
WHERE run_id = ? AND phase IN ('build', 'repair')
|
|
782
|
+
ORDER BY created_at, id`)
|
|
783
|
+
.all(runId);
|
|
784
|
+
return rows.flatMap((row) => {
|
|
785
|
+
const status = this.workerInvocationStatus(row.id);
|
|
786
|
+
if (status !== "launch_started" && status !== "uncertain")
|
|
787
|
+
return [];
|
|
788
|
+
const uncertain = this.#database
|
|
789
|
+
.prepare(`SELECT data_json FROM worker_invocation_events
|
|
790
|
+
WHERE invocation_id = ? AND type = 'uncertain'
|
|
791
|
+
ORDER BY sequence DESC LIMIT 1`)
|
|
792
|
+
.get(row.id);
|
|
793
|
+
const observedExit = this.#database
|
|
794
|
+
.prepare(`SELECT 1 AS present FROM worker_invocation_events
|
|
795
|
+
WHERE invocation_id = ? AND type = 'process_exited' LIMIT 1`)
|
|
796
|
+
.get(row.id);
|
|
797
|
+
let processExited = observedExit?.present === 1;
|
|
798
|
+
if (uncertain !== undefined) {
|
|
799
|
+
try {
|
|
800
|
+
const details = JSON.parse(uncertain.data_json);
|
|
801
|
+
processExited = processExited || details.processExited === true;
|
|
802
|
+
}
|
|
803
|
+
catch {
|
|
804
|
+
// Preserve separately journaled process-exit evidence.
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return [
|
|
808
|
+
{
|
|
809
|
+
invocationId: row.id,
|
|
810
|
+
phase: row.phase,
|
|
811
|
+
status,
|
|
812
|
+
processExited,
|
|
813
|
+
},
|
|
814
|
+
];
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
reconcileWorkerInvocation(runId, invocationId, reason) {
|
|
818
|
+
this.#transaction(() => {
|
|
819
|
+
this.getRun(runId);
|
|
820
|
+
const invocation = this.#database
|
|
821
|
+
.prepare(`SELECT run_id, phase FROM worker_invocations WHERE id = ?`)
|
|
822
|
+
.get(invocationId);
|
|
823
|
+
if (invocation?.run_id !== runId ||
|
|
824
|
+
(invocation.phase !== "build" && invocation.phase !== "repair")) {
|
|
825
|
+
throw new MillError("WORKER_INVOCATION_RECONCILIATION_CONFLICT", "Worker invocation is not a mutating admission for this run.", ExitCode.configuration);
|
|
826
|
+
}
|
|
827
|
+
const status = this.workerInvocationStatus(invocationId);
|
|
828
|
+
if (status !== "launch_started" && status !== "uncertain") {
|
|
829
|
+
throw new MillError("WORKER_INVOCATION_RECONCILIATION_CONFLICT", "Only an unresolved mutating worker invocation can be reconciled.", ExitCode.configuration, { status });
|
|
830
|
+
}
|
|
831
|
+
this.#invocationEvent(invocationId, "reconciled", { reason });
|
|
832
|
+
this.#event(runId, "worker.reconciled", {
|
|
833
|
+
invocationId,
|
|
834
|
+
phase: invocation.phase,
|
|
835
|
+
reason,
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
#transaction(action) {
|
|
840
|
+
try {
|
|
841
|
+
this.#database.exec("BEGIN IMMEDIATE");
|
|
842
|
+
action();
|
|
843
|
+
this.#database.exec("COMMIT");
|
|
844
|
+
}
|
|
845
|
+
catch (error) {
|
|
846
|
+
try {
|
|
847
|
+
this.#database.exec("ROLLBACK");
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
// Preserve the original state error.
|
|
851
|
+
}
|
|
852
|
+
if (error instanceof MillError)
|
|
853
|
+
throw error;
|
|
854
|
+
throw new MillError("STATE_WRITE_FAILED", "Operational state transaction failed.", ExitCode.io, { cause: String(error) });
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
#event(id, type, data) {
|
|
858
|
+
this.#database
|
|
859
|
+
.prepare("INSERT INTO run_events(run_id, occurred_at, type, data_json) VALUES (?, ?, ?, ?)")
|
|
860
|
+
.run(id, new Date().toISOString(), type, JSON.stringify(data));
|
|
861
|
+
}
|
|
862
|
+
#invocationEvent(invocationId, type, data) {
|
|
863
|
+
this.#database
|
|
864
|
+
.prepare(`INSERT INTO worker_invocation_events(
|
|
865
|
+
invocation_id, occurred_at, type, data_json
|
|
866
|
+
) VALUES (?, ?, ?, ?)`)
|
|
867
|
+
.run(invocationId, new Date().toISOString(), type, JSON.stringify(data));
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
export async function acquireWriterLease(store) {
|
|
871
|
+
const leasePath = path.join(store.directory, "writer-lease.sqlite3");
|
|
872
|
+
return await acquireExclusiveLease({
|
|
873
|
+
path: leasePath,
|
|
874
|
+
activeCode: "WRITER_ALREADY_ACTIVE",
|
|
875
|
+
activeMessage: "Another Mill writer is active for this repository.",
|
|
876
|
+
unavailableCode: "WRITER_LEASE_UNAVAILABLE",
|
|
877
|
+
unavailableMessage: "The repository writer lease could not be acquired safely.",
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
export async function restoreStateBackup(repositoryId, commonDirectory, backupPath) {
|
|
881
|
+
const directory = repositoryStateDirectory(repositoryId, commonDirectory);
|
|
882
|
+
const resolvedBackup = path.resolve(backupPath);
|
|
883
|
+
if (!isWithin(directory, resolvedBackup)) {
|
|
884
|
+
throw new MillError("INVALID_STATE_BACKUP", "State backup must be a Mill-owned file in this repository namespace.", ExitCode.configuration);
|
|
885
|
+
}
|
|
886
|
+
const information = await lstat(resolvedBackup);
|
|
887
|
+
if (!information.isFile() || information.isSymbolicLink()) {
|
|
888
|
+
throw new MillError("INVALID_STATE_BACKUP", "State backup is not a regular file.", ExitCode.configuration);
|
|
889
|
+
}
|
|
890
|
+
const databasePath = path.join(directory, "state.sqlite3");
|
|
891
|
+
const temporaryPath = path.join(directory, `restore-${randomUUID()}.sqlite3`);
|
|
892
|
+
const expectedWorktrees = new Set();
|
|
893
|
+
let quarantineManifest;
|
|
894
|
+
const moved = [];
|
|
895
|
+
try {
|
|
896
|
+
await copyFile(resolvedBackup, temporaryPath, constants.COPYFILE_EXCL);
|
|
897
|
+
await chmod(temporaryPath, 0o600);
|
|
898
|
+
let candidate;
|
|
899
|
+
try {
|
|
900
|
+
candidate = new DatabaseSync(temporaryPath, {
|
|
901
|
+
readOnly: true,
|
|
902
|
+
allowExtension: false,
|
|
903
|
+
enableDoubleQuotedStringLiterals: false,
|
|
904
|
+
});
|
|
905
|
+
candidate.exec("PRAGMA trusted_schema = OFF; PRAGMA foreign_keys = ON;");
|
|
906
|
+
const integrity = candidate.prepare("PRAGMA integrity_check").get();
|
|
907
|
+
const version = candidate
|
|
908
|
+
.prepare("SELECT value FROM metadata WHERE key = 'schema_version'")
|
|
909
|
+
.get();
|
|
910
|
+
const requiredObjects = candidate
|
|
911
|
+
.prepare(`SELECT name FROM sqlite_schema
|
|
912
|
+
WHERE (type = 'table' AND name IN ('metadata', 'runs', 'run_events'))
|
|
913
|
+
OR (type = 'table' AND name = 'baseline_qualifications')
|
|
914
|
+
OR (type = 'table' AND name IN ('worker_invocations', 'worker_invocation_events'))
|
|
915
|
+
OR (type = 'trigger' AND name IN (
|
|
916
|
+
'run_events_no_update', 'run_events_no_delete',
|
|
917
|
+
'worker_invocations_no_update', 'worker_invocations_no_delete',
|
|
918
|
+
'worker_invocation_events_no_update', 'worker_invocation_events_no_delete'
|
|
919
|
+
))`)
|
|
920
|
+
.all();
|
|
921
|
+
const worktrees = candidate
|
|
922
|
+
.prepare("SELECT worktree_path FROM runs WHERE worktree_path IS NOT NULL")
|
|
923
|
+
.all();
|
|
924
|
+
if (integrity?.integrity_check !== "ok" ||
|
|
925
|
+
(version?.value !== "1" && version?.value !== "2") ||
|
|
926
|
+
new Set(requiredObjects.map((object) => object.name)).size !==
|
|
927
|
+
(version.value === "1" ? 6 : 12)) {
|
|
928
|
+
throw new Error("backup integrity, schema version, or objects invalid");
|
|
929
|
+
}
|
|
930
|
+
const worktreesDirectory = path.join(directory, "worktrees");
|
|
931
|
+
for (const row of worktrees) {
|
|
932
|
+
const resolved = path.resolve(row.worktree_path);
|
|
933
|
+
if (!isWithin(worktreesDirectory, resolved)) {
|
|
934
|
+
throw new Error("backup references a worktree outside Mill state");
|
|
935
|
+
}
|
|
936
|
+
expectedWorktrees.add(resolved);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
catch (error) {
|
|
940
|
+
throw new MillError("INVALID_STATE_BACKUP", "State backup failed integrity and schema validation.", ExitCode.data, { cause: String(error) });
|
|
941
|
+
}
|
|
942
|
+
finally {
|
|
943
|
+
candidate?.close();
|
|
944
|
+
}
|
|
945
|
+
const worktreesDirectory = path.join(directory, "worktrees");
|
|
946
|
+
const entries = await readdir(worktreesDirectory, { withFileTypes: true });
|
|
947
|
+
const orphaned = entries
|
|
948
|
+
.map((entry) => ({
|
|
949
|
+
entry,
|
|
950
|
+
original: path.join(worktreesDirectory, entry.name),
|
|
951
|
+
}))
|
|
952
|
+
.filter(({ original }) => !expectedWorktrees.has(path.resolve(original)));
|
|
953
|
+
if (orphaned.length > 0) {
|
|
954
|
+
for (const { entry } of orphaned) {
|
|
955
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) {
|
|
956
|
+
throw new MillError("UNSAFE_ORPHANED_WORKTREE", "Restore found an unclassified entry in the Mill worktree directory.", ExitCode.configuration, { name: entry.name });
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
const quarantineId = `restore-${new Date()
|
|
960
|
+
.toISOString()
|
|
961
|
+
.replaceAll(/[:.]/gu, "-")}-${randomUUID()}`;
|
|
962
|
+
const quarantineDirectory = path.join(directory, "quarantine", quarantineId);
|
|
963
|
+
await mkdir(quarantineDirectory, { recursive: true, mode: 0o700 });
|
|
964
|
+
await chmod(quarantineDirectory, 0o700);
|
|
965
|
+
quarantineManifest = path.join(quarantineDirectory, "manifest.json");
|
|
966
|
+
const planned = orphaned.map(({ entry, original }) => ({
|
|
967
|
+
original,
|
|
968
|
+
quarantined: path.join(quarantineDirectory, entry.name),
|
|
969
|
+
}));
|
|
970
|
+
await writeFile(quarantineManifest, `${JSON.stringify({
|
|
971
|
+
schemaVersion: "1",
|
|
972
|
+
repositoryId,
|
|
973
|
+
backupPath: resolvedBackup,
|
|
974
|
+
protocol: "database_swap_commit_point",
|
|
975
|
+
worktrees: planned,
|
|
976
|
+
}, null, 2)}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
977
|
+
for (const item of planned) {
|
|
978
|
+
await rename(item.original, item.quarantined);
|
|
979
|
+
moved.push(item);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
await rm(`${databasePath}-wal`, { force: true });
|
|
983
|
+
await rm(`${databasePath}-shm`, { force: true });
|
|
984
|
+
await rename(temporaryPath, databasePath);
|
|
985
|
+
return {
|
|
986
|
+
quarantinedCount: moved.length,
|
|
987
|
+
...(quarantineManifest === undefined ? {} : { quarantineManifest }),
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
catch (error) {
|
|
991
|
+
for (const item of moved.toReversed()) {
|
|
992
|
+
try {
|
|
993
|
+
await rename(item.quarantined, item.original);
|
|
994
|
+
}
|
|
995
|
+
catch {
|
|
996
|
+
// The recovery manifest preserves attended recovery evidence.
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
throw error;
|
|
1000
|
+
}
|
|
1001
|
+
finally {
|
|
1002
|
+
await rm(temporaryPath, { force: true });
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
export async function purgeRepositoryState(repositoryId, commonDirectory) {
|
|
1006
|
+
const directory = repositoryStateDirectory(repositoryId, commonDirectory);
|
|
1007
|
+
try {
|
|
1008
|
+
await access(directory);
|
|
1009
|
+
}
|
|
1010
|
+
catch (error) {
|
|
1011
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
1012
|
+
return;
|
|
1013
|
+
throw error;
|
|
1014
|
+
}
|
|
1015
|
+
await rm(directory, { recursive: true });
|
|
1016
|
+
}
|
|
1017
|
+
//# sourceMappingURL=state.js.map
|