@blxzer/cursor-trellis-core 0.4.2 → 0.4.3
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/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/task/adapter-middleware.d.ts +91 -0
- package/dist/task/adapter-middleware.d.ts.map +1 -0
- package/dist/task/adapter-middleware.js +384 -0
- package/dist/task/adapter-middleware.js.map +1 -0
- package/dist/task/contract-migrate.d.ts +29 -0
- package/dist/task/contract-migrate.d.ts.map +1 -0
- package/dist/task/contract-migrate.js +207 -0
- package/dist/task/contract-migrate.js.map +1 -0
- package/dist/task/full-quality.d.ts +89 -0
- package/dist/task/full-quality.d.ts.map +1 -0
- package/dist/task/full-quality.js +379 -0
- package/dist/task/full-quality.js.map +1 -0
- package/dist/task/index.d.ts +20 -0
- package/dist/task/index.d.ts.map +1 -1
- package/dist/task/index.js +10 -0
- package/dist/task/index.js.map +1 -1
- package/dist/task/kernel-cli.d.ts +44 -0
- package/dist/task/kernel-cli.d.ts.map +1 -0
- package/dist/task/kernel-cli.js +287 -0
- package/dist/task/kernel-cli.js.map +1 -0
- package/dist/task/kernel-contract.d.ts +113 -0
- package/dist/task/kernel-contract.d.ts.map +1 -0
- package/dist/task/kernel-contract.js +332 -0
- package/dist/task/kernel-contract.js.map +1 -0
- package/dist/task/kernel-store.d.ts +99 -0
- package/dist/task/kernel-store.d.ts.map +1 -0
- package/dist/task/kernel-store.js +735 -0
- package/dist/task/kernel-store.js.map +1 -0
- package/dist/task/lite-context-pack.d.ts +71 -0
- package/dist/task/lite-context-pack.d.ts.map +1 -0
- package/dist/task/lite-context-pack.js +175 -0
- package/dist/task/lite-context-pack.js.map +1 -0
- package/dist/task/ondemand-topology.d.ts +100 -0
- package/dist/task/ondemand-topology.d.ts.map +1 -0
- package/dist/task/ondemand-topology.js +418 -0
- package/dist/task/ondemand-topology.js.map +1 -0
- package/dist/task/p36-artifact-migrate.d.ts +61 -0
- package/dist/task/p36-artifact-migrate.d.ts.map +1 -0
- package/dist/task/p36-artifact-migrate.js +336 -0
- package/dist/task/p36-artifact-migrate.js.map +1 -0
- package/dist/task/p36-wave-c.d.ts +39 -0
- package/dist/task/p36-wave-c.d.ts.map +1 -0
- package/dist/task/p36-wave-c.js +100 -0
- package/dist/task/p36-wave-c.js.map +1 -0
- package/dist/task/records.d.ts +9 -0
- package/dist/task/records.d.ts.map +1 -1
- package/dist/task/records.js +11 -0
- package/dist/task/records.js.map +1 -1
- package/package.json +1 -6
- package/dist/rpc/broker/audit.d.ts +0 -16
- package/dist/rpc/broker/audit.d.ts.map +0 -1
- package/dist/rpc/broker/audit.js +0 -33
- package/dist/rpc/broker/audit.js.map +0 -1
- package/dist/rpc/broker/in-process.d.ts +0 -62
- package/dist/rpc/broker/in-process.d.ts.map +0 -1
- package/dist/rpc/broker/in-process.js +0 -490
- package/dist/rpc/broker/in-process.js.map +0 -1
- package/dist/rpc/broker/journal.d.ts +0 -11
- package/dist/rpc/broker/journal.d.ts.map +0 -1
- package/dist/rpc/broker/journal.js +0 -17
- package/dist/rpc/broker/journal.js.map +0 -1
- package/dist/rpc/contract/errors.d.ts +0 -18
- package/dist/rpc/contract/errors.d.ts.map +0 -1
- package/dist/rpc/contract/errors.js +0 -55
- package/dist/rpc/contract/errors.js.map +0 -1
- package/dist/rpc/contract/parse.d.ts +0 -9
- package/dist/rpc/contract/parse.d.ts.map +0 -1
- package/dist/rpc/contract/parse.js +0 -144
- package/dist/rpc/contract/parse.js.map +0 -1
- package/dist/rpc/contract/topics.d.ts +0 -11
- package/dist/rpc/contract/topics.d.ts.map +0 -1
- package/dist/rpc/contract/topics.js +0 -42
- package/dist/rpc/contract/topics.js.map +0 -1
- package/dist/rpc/contract/types.d.ts +0 -54
- package/dist/rpc/contract/types.d.ts.map +0 -1
- package/dist/rpc/contract/types.js +0 -39
- package/dist/rpc/contract/types.js.map +0 -1
- package/dist/rpc/index.d.ts +0 -10
- package/dist/rpc/index.d.ts.map +0 -1
- package/dist/rpc/index.js +0 -11
- package/dist/rpc/index.js.map +0 -1
- package/dist/rpc/transport/localhost-server.d.ts +0 -36
- package/dist/rpc/transport/localhost-server.d.ts.map +0 -1
- package/dist/rpc/transport/localhost-server.js +0 -275
- package/dist/rpc/transport/localhost-server.js.map +0 -1
- package/dist/rpc/transport/websocket.d.ts +0 -20
- package/dist/rpc/transport/websocket.d.ts.map +0 -1
- package/dist/rpc/transport/websocket.js +0 -156
- package/dist/rpc/transport/websocket.js.map +0 -1
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-local Kernel store: atomic snapshot write (state + audit in one
|
|
3
|
+
* file) with optimistic revision and a process lock.
|
|
4
|
+
*
|
|
5
|
+
* Stage 1 does not mutate `task.json.status` — legacy lifecycle remains
|
|
6
|
+
* a read projection. `writeTaskRecord` stays the low-level task.json
|
|
7
|
+
* primitive; this module is the new core-state write entry.
|
|
8
|
+
*/
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { assertFullQualityForPhase, assertIndependentCheckGateRecord, normalizeRequiredControlsInExtras, } from "./full-quality.js";
|
|
12
|
+
import { normalizeStage6InExtrasAndAssert } from "./adapter-middleware.js";
|
|
13
|
+
import { normalizeStage5InExtrasAndAssert } from "./ondemand-topology.js";
|
|
14
|
+
import { stripRetiredExtras, stripRetiredMeta } from "./contract-migrate.js";
|
|
15
|
+
import { loadTaskRecord, writeTaskRecord } from "./records.js";
|
|
16
|
+
import { TASK_RECORD_FIELD_ORDER, isPlainObject, taskRecordSchema, } from "./schema.js";
|
|
17
|
+
import { KERNEL_JSON_BASENAME, KERNEL_SCHEMA_VERSION, KernelError, deriveStateForPhase, emptyKernelGates, hookIsPresent, hopsToClose, hopsToExecute, isKernelPhase, isLegalPhaseEdge, isNonNegativeInt, kernelPhaseToLegacyStatus, parseKernelSnapshot, projectLegacyStatus, requireNonEmptyString, } from "./kernel-contract.js";
|
|
18
|
+
const LOCK_BASENAME = "kernel.json.lock";
|
|
19
|
+
const LOCK_MAX_WAIT_MS = 5000;
|
|
20
|
+
const LOCK_RETRY_MS = 25;
|
|
21
|
+
let kernelAfterWriteHook = null;
|
|
22
|
+
/** Test-only interceptor between kernel.json persist and task.json projection. */
|
|
23
|
+
export function setKernelAfterWriteHook(hook) {
|
|
24
|
+
kernelAfterWriteHook = hook;
|
|
25
|
+
}
|
|
26
|
+
export function resolveTaskDir(taskDir, cwd) {
|
|
27
|
+
if (path.isAbsolute(taskDir))
|
|
28
|
+
return taskDir;
|
|
29
|
+
return path.resolve(cwd ?? process.cwd(), taskDir);
|
|
30
|
+
}
|
|
31
|
+
export function kernelJsonPath(taskDir) {
|
|
32
|
+
return path.join(taskDir, KERNEL_JSON_BASENAME);
|
|
33
|
+
}
|
|
34
|
+
export function readKernel(options) {
|
|
35
|
+
const dir = resolveTaskDir(options.taskDir, options.cwd);
|
|
36
|
+
assertTaskJson(dir);
|
|
37
|
+
return withKernelLock(dir, () => readKernelUnlocked(dir));
|
|
38
|
+
}
|
|
39
|
+
export function applyKernelTransition(request) {
|
|
40
|
+
validateTransitionHooks(request);
|
|
41
|
+
const dir = resolveTaskDir(request.taskDir, request.cwd);
|
|
42
|
+
const actor = requireNonEmptyString(request.actor, "actor");
|
|
43
|
+
const idempotencyKey = requireNonEmptyString(request.idempotencyKey, "idempotencyKey");
|
|
44
|
+
if (!isKernelPhase(request.targetPhase)) {
|
|
45
|
+
throw new KernelError("INVALID_REQUEST", "targetPhase is not a Kernel phase");
|
|
46
|
+
}
|
|
47
|
+
if (!isNonNegativeInt(request.expectedRevision)) {
|
|
48
|
+
throw new KernelError("INVALID_REQUEST", "expectedRevision must be a non-negative integer");
|
|
49
|
+
}
|
|
50
|
+
const evidence = request.evidence === undefined || request.evidence === null
|
|
51
|
+
? null
|
|
52
|
+
: requireNonEmptyString(request.evidence, "evidence");
|
|
53
|
+
assertTaskJson(dir);
|
|
54
|
+
return withKernelLock(dir, () => {
|
|
55
|
+
const current = readKernelUnlocked(dir);
|
|
56
|
+
const prior = current.kernel.audit.find((event) => event.idempotencyKey === idempotencyKey);
|
|
57
|
+
if (prior) {
|
|
58
|
+
if (prior.to.phase !== request.targetPhase) {
|
|
59
|
+
throw new KernelError("IDEMPOTENCY_MISMATCH", `idempotencyKey ${idempotencyKey} already applied to ${prior.to.phase}`);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...current,
|
|
63
|
+
idempotent: true,
|
|
64
|
+
audit: prior,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (request.expectedRevision !== current.kernel.revision) {
|
|
68
|
+
throw new KernelError("REVISION_CONFLICT", `expected revision ${request.expectedRevision} but kernel is at ${current.kernel.revision}`);
|
|
69
|
+
}
|
|
70
|
+
if (!isLegalPhaseEdge(current.kernel.phase, request.targetPhase)) {
|
|
71
|
+
throw new KernelError("INVALID_TRANSITION", `illegal Kernel edge ${current.kernel.phase} -> ${request.targetPhase}`);
|
|
72
|
+
}
|
|
73
|
+
const fromState = {
|
|
74
|
+
phase: current.kernel.phase,
|
|
75
|
+
condition: current.kernel.condition,
|
|
76
|
+
outcome: current.kernel.outcome,
|
|
77
|
+
revision: current.kernel.revision,
|
|
78
|
+
};
|
|
79
|
+
const nextState = deriveStateForPhase(request.targetPhase);
|
|
80
|
+
const nextRevision = current.kernel.revision + 1;
|
|
81
|
+
const audit = {
|
|
82
|
+
id: crypto.randomUUID(),
|
|
83
|
+
at: new Date().toISOString(),
|
|
84
|
+
actor,
|
|
85
|
+
idempotencyKey,
|
|
86
|
+
evidence,
|
|
87
|
+
from: fromState,
|
|
88
|
+
to: { ...nextState, revision: nextRevision },
|
|
89
|
+
};
|
|
90
|
+
const next = {
|
|
91
|
+
schemaVersion: KERNEL_SCHEMA_VERSION,
|
|
92
|
+
identity: { taskId: current.kernel.identity.taskId },
|
|
93
|
+
revision: nextRevision,
|
|
94
|
+
phase: nextState.phase,
|
|
95
|
+
condition: nextState.condition,
|
|
96
|
+
outcome: nextState.outcome,
|
|
97
|
+
audit: [...current.kernel.audit, audit],
|
|
98
|
+
gates: current.kernel.gates,
|
|
99
|
+
projection: current.kernel.projection,
|
|
100
|
+
};
|
|
101
|
+
atomicWriteFile(kernelJsonPath(dir), `${JSON.stringify(next, null, 2)}\n`);
|
|
102
|
+
const legacy = current.legacy;
|
|
103
|
+
return {
|
|
104
|
+
kernel: next,
|
|
105
|
+
persisted: true,
|
|
106
|
+
legacy,
|
|
107
|
+
idempotent: false,
|
|
108
|
+
audit,
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function validateTransitionHooks(request) {
|
|
113
|
+
if (hookIsPresent(request.gate)) {
|
|
114
|
+
throw new KernelError("GATE_HOOK_UNIMPLEMENTED", "Gate hooks are Stage 1 placeholders and cannot be treated as passing");
|
|
115
|
+
}
|
|
116
|
+
if (hookIsPresent(request.policy)) {
|
|
117
|
+
throw new KernelError("POLICY_HOOK_UNIMPLEMENTED", "Policy hooks are Stage 1 placeholders and cannot be treated as passing");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function assertTaskJson(dir) {
|
|
121
|
+
if (!fs.existsSync(path.join(dir, "task.json"))) {
|
|
122
|
+
throw new KernelError("NOT_FOUND", `task.json not found in ${dir}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function readKernelUnlocked(dir) {
|
|
126
|
+
const file = kernelJsonPath(dir);
|
|
127
|
+
const taskFile = path.join(dir, "task.json");
|
|
128
|
+
if (!fs.existsSync(taskFile)) {
|
|
129
|
+
if (!fs.existsSync(file)) {
|
|
130
|
+
throw new KernelError("NOT_FOUND", `task.json not found in ${dir}`);
|
|
131
|
+
}
|
|
132
|
+
const kernel = loadKernelFile(file);
|
|
133
|
+
return {
|
|
134
|
+
kernel,
|
|
135
|
+
persisted: true,
|
|
136
|
+
legacy: legacyFromSnapshot(kernel),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
const record = loadTaskRecord({ taskDir: dir });
|
|
140
|
+
const legacy = {
|
|
141
|
+
status: record.status,
|
|
142
|
+
id: record.id,
|
|
143
|
+
name: record.name,
|
|
144
|
+
title: record.title,
|
|
145
|
+
};
|
|
146
|
+
if (!fs.existsSync(file)) {
|
|
147
|
+
const projected = projectLegacyStatus(record.status);
|
|
148
|
+
const kernel = {
|
|
149
|
+
schemaVersion: KERNEL_SCHEMA_VERSION,
|
|
150
|
+
identity: { taskId: record.id },
|
|
151
|
+
revision: 0,
|
|
152
|
+
phase: projected.phase,
|
|
153
|
+
condition: projected.condition,
|
|
154
|
+
outcome: projected.outcome,
|
|
155
|
+
audit: [],
|
|
156
|
+
gates: emptyKernelGates(),
|
|
157
|
+
projection: null,
|
|
158
|
+
};
|
|
159
|
+
return { kernel, persisted: false, legacy };
|
|
160
|
+
}
|
|
161
|
+
const kernel = loadKernelFile(file);
|
|
162
|
+
if (kernel.identity.taskId !== record.id) {
|
|
163
|
+
throw new KernelError("CORRUPT_STATE", `kernel identity ${kernel.identity.taskId} does not match task.json id ${record.id}`);
|
|
164
|
+
}
|
|
165
|
+
return { kernel, persisted: true, legacy };
|
|
166
|
+
}
|
|
167
|
+
function withKernelLock(taskDir, fn) {
|
|
168
|
+
const lockFile = path.join(taskDir, LOCK_BASENAME);
|
|
169
|
+
acquireLockSync(lockFile);
|
|
170
|
+
try {
|
|
171
|
+
return fn();
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
releaseLockSync(lockFile);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function acquireLockSync(lockFile) {
|
|
178
|
+
fs.mkdirSync(path.dirname(lockFile), { recursive: true });
|
|
179
|
+
const deadline = Date.now() + LOCK_MAX_WAIT_MS;
|
|
180
|
+
while (true) {
|
|
181
|
+
try {
|
|
182
|
+
const fd = fs.openSync(lockFile, "wx");
|
|
183
|
+
fs.writeSync(fd, String(process.pid));
|
|
184
|
+
fs.closeSync(fd);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
if (err.code !== "EEXIST")
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
191
|
+
stealStaleLock(lockFile);
|
|
192
|
+
if (Date.now() >= deadline) {
|
|
193
|
+
throw new KernelError("LOCK_TIMEOUT", `Failed to acquire kernel lock ${lockFile} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
194
|
+
}
|
|
195
|
+
sleepSync(LOCK_RETRY_MS);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
function stealStaleLock(lockFile) {
|
|
199
|
+
let holderPid = 0;
|
|
200
|
+
try {
|
|
201
|
+
holderPid = Number(fs.readFileSync(lockFile, "utf-8").trim());
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (!holderPid || pidAlive(holderPid))
|
|
207
|
+
return;
|
|
208
|
+
try {
|
|
209
|
+
fs.unlinkSync(lockFile);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
// lost the race
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function releaseLockSync(lockFile) {
|
|
216
|
+
try {
|
|
217
|
+
const content = fs.readFileSync(lockFile, "utf-8").trim();
|
|
218
|
+
if (content === String(process.pid)) {
|
|
219
|
+
fs.unlinkSync(lockFile);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
// already gone
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
function pidAlive(pid) {
|
|
227
|
+
try {
|
|
228
|
+
process.kill(pid, 0);
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function sleepSync(ms) {
|
|
236
|
+
const sab = new SharedArrayBuffer(4);
|
|
237
|
+
Atomics.wait(new Int32Array(sab), 0, 0, ms);
|
|
238
|
+
}
|
|
239
|
+
function atomicWriteFile(targetPath, contents) {
|
|
240
|
+
const dir = path.dirname(targetPath);
|
|
241
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
242
|
+
const tmpPath = path.join(dir, `.${path.basename(targetPath)}.${process.pid}.${Date.now()}.tmp`);
|
|
243
|
+
fs.writeFileSync(tmpPath, contents, "utf-8");
|
|
244
|
+
try {
|
|
245
|
+
fs.renameSync(tmpPath, targetPath);
|
|
246
|
+
}
|
|
247
|
+
catch (err) {
|
|
248
|
+
const code = err.code;
|
|
249
|
+
if (code === "EEXIST" || code === "EPERM" || process.platform === "win32") {
|
|
250
|
+
fs.copyFileSync(tmpPath, targetPath);
|
|
251
|
+
fs.unlinkSync(tmpPath);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
fs.unlinkSync(tmpPath);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
// ignore leftover tmp
|
|
259
|
+
}
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function loadKernelFile(file) {
|
|
264
|
+
const raw = fs.readFileSync(file, "utf-8");
|
|
265
|
+
let parsed;
|
|
266
|
+
try {
|
|
267
|
+
parsed = JSON.parse(raw);
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
throw new KernelError("CORRUPT_STATE", `Failed to parse ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
271
|
+
}
|
|
272
|
+
return parseKernelSnapshot(parsed);
|
|
273
|
+
}
|
|
274
|
+
function legacyFromSnapshot(kernel) {
|
|
275
|
+
const record = kernel.projection?.record;
|
|
276
|
+
if (record) {
|
|
277
|
+
return {
|
|
278
|
+
status: kernel.projection?.status ?? record.status,
|
|
279
|
+
id: record.id,
|
|
280
|
+
name: record.name,
|
|
281
|
+
title: record.title,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
status: kernelPhaseToLegacyStatus(kernel.phase),
|
|
286
|
+
id: kernel.identity.taskId,
|
|
287
|
+
name: kernel.identity.taskId,
|
|
288
|
+
title: kernel.identity.taskId,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function cloneJsonObject(value) {
|
|
292
|
+
return JSON.parse(JSON.stringify(value));
|
|
293
|
+
}
|
|
294
|
+
function mergeExtras(current, incoming, record, phase = "patch") {
|
|
295
|
+
const merged = stripRetiredExtras({
|
|
296
|
+
...cloneJsonObject(current),
|
|
297
|
+
...(incoming === undefined ? {} : cloneJsonObject(incoming)),
|
|
298
|
+
});
|
|
299
|
+
normalizeRequiredControlsInExtras(merged);
|
|
300
|
+
normalizeStage5InExtrasAndAssert(merged, record ?? {}, phase);
|
|
301
|
+
normalizeStage6InExtrasAndAssert(merged, phase);
|
|
302
|
+
return merged;
|
|
303
|
+
}
|
|
304
|
+
function requireEvidence(value, field) {
|
|
305
|
+
if (value === undefined || value === null)
|
|
306
|
+
return null;
|
|
307
|
+
return requireNonEmptyString(value, field);
|
|
308
|
+
}
|
|
309
|
+
function persistKernelSnapshot(dir, snapshot) {
|
|
310
|
+
atomicWriteFile(kernelJsonPath(dir), `${JSON.stringify(snapshot, null, 2)}\n`);
|
|
311
|
+
}
|
|
312
|
+
function commitKernelAndProject(dir, snapshot) {
|
|
313
|
+
if (!snapshot.projection) {
|
|
314
|
+
throw new KernelError("INVALID_REQUEST", "Kernel command requires a legacy projection");
|
|
315
|
+
}
|
|
316
|
+
persistKernelSnapshot(dir, snapshot);
|
|
317
|
+
try {
|
|
318
|
+
if (kernelAfterWriteHook) {
|
|
319
|
+
kernelAfterWriteHook();
|
|
320
|
+
}
|
|
321
|
+
writeTaskRecord({
|
|
322
|
+
taskDir: dir,
|
|
323
|
+
record: snapshot.projection.record,
|
|
324
|
+
extra: snapshot.projection.extras,
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
if (err instanceof KernelError && err.code === "HALF_CONVERSION") {
|
|
329
|
+
throw err;
|
|
330
|
+
}
|
|
331
|
+
throw new KernelError("HALF_CONVERSION", `kernel.json committed at revision ${snapshot.revision} but task.json projection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
332
|
+
}
|
|
333
|
+
return {
|
|
334
|
+
kernel: snapshot,
|
|
335
|
+
persisted: true,
|
|
336
|
+
legacy: legacyFromSnapshot(snapshot),
|
|
337
|
+
idempotent: false,
|
|
338
|
+
audit: snapshot.audit[snapshot.audit.length - 1],
|
|
339
|
+
projected: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function hopKernelSnapshot(current, hops, meta) {
|
|
343
|
+
const prior = current.audit.find((event) => event.idempotencyKey === meta.idempotencyKey);
|
|
344
|
+
if (prior) {
|
|
345
|
+
return { snapshot: current, audit: prior, idempotent: true };
|
|
346
|
+
}
|
|
347
|
+
if (hops.length === 0) {
|
|
348
|
+
const nextRevision = current.revision + 1;
|
|
349
|
+
const state = {
|
|
350
|
+
phase: current.phase,
|
|
351
|
+
condition: current.condition,
|
|
352
|
+
outcome: current.outcome,
|
|
353
|
+
};
|
|
354
|
+
const audit = {
|
|
355
|
+
id: crypto.randomUUID(),
|
|
356
|
+
at: new Date().toISOString(),
|
|
357
|
+
actor: meta.actor,
|
|
358
|
+
idempotencyKey: meta.idempotencyKey,
|
|
359
|
+
evidence: meta.evidence,
|
|
360
|
+
from: { ...state, revision: current.revision },
|
|
361
|
+
to: { ...state, revision: nextRevision },
|
|
362
|
+
};
|
|
363
|
+
return {
|
|
364
|
+
snapshot: {
|
|
365
|
+
...current,
|
|
366
|
+
revision: nextRevision,
|
|
367
|
+
audit: [...current.audit, audit],
|
|
368
|
+
},
|
|
369
|
+
audit,
|
|
370
|
+
idempotent: false,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
let snapshot = current;
|
|
374
|
+
let lastAudit;
|
|
375
|
+
hops.forEach((target, index) => {
|
|
376
|
+
const key = index === hops.length - 1
|
|
377
|
+
? meta.idempotencyKey
|
|
378
|
+
: `${meta.idempotencyKey}#${target}`;
|
|
379
|
+
if (!isLegalPhaseEdge(snapshot.phase, target)) {
|
|
380
|
+
throw new KernelError("INVALID_TRANSITION", `illegal Kernel edge ${snapshot.phase} -> ${target}`);
|
|
381
|
+
}
|
|
382
|
+
const fromState = {
|
|
383
|
+
phase: snapshot.phase,
|
|
384
|
+
condition: snapshot.condition,
|
|
385
|
+
outcome: snapshot.outcome,
|
|
386
|
+
revision: snapshot.revision,
|
|
387
|
+
};
|
|
388
|
+
const nextState = deriveStateForPhase(target);
|
|
389
|
+
const nextRevision = snapshot.revision + 1;
|
|
390
|
+
lastAudit = {
|
|
391
|
+
id: crypto.randomUUID(),
|
|
392
|
+
at: new Date().toISOString(),
|
|
393
|
+
actor: meta.actor,
|
|
394
|
+
idempotencyKey: key,
|
|
395
|
+
evidence: meta.evidence,
|
|
396
|
+
from: fromState,
|
|
397
|
+
to: { ...nextState, revision: nextRevision },
|
|
398
|
+
};
|
|
399
|
+
snapshot = {
|
|
400
|
+
...snapshot,
|
|
401
|
+
revision: nextRevision,
|
|
402
|
+
phase: nextState.phase,
|
|
403
|
+
condition: nextState.condition,
|
|
404
|
+
outcome: nextState.outcome,
|
|
405
|
+
audit: [...snapshot.audit, lastAudit],
|
|
406
|
+
};
|
|
407
|
+
});
|
|
408
|
+
if (!lastAudit) {
|
|
409
|
+
throw new KernelError("INVALID_REQUEST", "Kernel hop produced no audit event");
|
|
410
|
+
}
|
|
411
|
+
return { snapshot, audit: lastAudit, idempotent: false };
|
|
412
|
+
}
|
|
413
|
+
function attachProjection(snapshot, record, extras, status) {
|
|
414
|
+
const projection = {
|
|
415
|
+
status,
|
|
416
|
+
record: taskRecordSchema.parse({
|
|
417
|
+
...record,
|
|
418
|
+
status,
|
|
419
|
+
meta: stripRetiredMeta(record.meta),
|
|
420
|
+
}),
|
|
421
|
+
extras: cloneJsonObject(stripRetiredExtras(extras)),
|
|
422
|
+
};
|
|
423
|
+
return { ...snapshot, projection };
|
|
424
|
+
}
|
|
425
|
+
function expectRevision(current, expected) {
|
|
426
|
+
if (expected !== current.revision) {
|
|
427
|
+
throw new KernelError("REVISION_CONFLICT", `expected revision ${expected} but kernel is at ${current.revision}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function assertHonestGateRecord(record) {
|
|
431
|
+
const result = record.result;
|
|
432
|
+
if (result !== "PASS" && result !== "FAIL" && result !== "SKIPPED") {
|
|
433
|
+
throw new KernelError("INVALID_REQUEST", "gate record.result must be PASS, FAIL, or SKIPPED");
|
|
434
|
+
}
|
|
435
|
+
if (result === "PASS") {
|
|
436
|
+
if (typeof record.evidence !== "string" || record.evidence.trim() === "") {
|
|
437
|
+
throw new KernelError("INVALID_REQUEST", "PASS gate records require non-empty evidence (no fake-green)");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (result === "SKIPPED") {
|
|
441
|
+
const skip = record.approved_skip;
|
|
442
|
+
if (!skip ||
|
|
443
|
+
typeof skip !== "object" ||
|
|
444
|
+
Array.isArray(skip) ||
|
|
445
|
+
typeof skip.approved_by !== "string" ||
|
|
446
|
+
typeof skip.reason !== "string") {
|
|
447
|
+
throw new KernelError("INVALID_REQUEST", "SKIPPED gate records require approved_skip.approved_by and reason");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
export function applyKernelCreate(request) {
|
|
452
|
+
validateTransitionHooks(request);
|
|
453
|
+
const dir = resolveTaskDir(request.taskDir, request.cwd);
|
|
454
|
+
const actor = requireNonEmptyString(request.actor, "actor");
|
|
455
|
+
const idempotencyKey = requireNonEmptyString(request.idempotencyKey, "idempotencyKey");
|
|
456
|
+
const record = taskRecordSchema.parse({
|
|
457
|
+
...request.record,
|
|
458
|
+
status: request.record.status || "planning",
|
|
459
|
+
});
|
|
460
|
+
const extras = mergeExtras({}, request.extras, record, "create");
|
|
461
|
+
const evidence = requireEvidence(request.evidence, "evidence");
|
|
462
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
463
|
+
return withKernelLock(dir, () => {
|
|
464
|
+
if (fs.existsSync(kernelJsonPath(dir))) {
|
|
465
|
+
const current = readKernelUnlocked(dir);
|
|
466
|
+
const prior = current.kernel.audit.find((event) => event.idempotencyKey === idempotencyKey);
|
|
467
|
+
if (prior) {
|
|
468
|
+
if (!fs.existsSync(path.join(dir, "task.json")) && current.kernel.projection) {
|
|
469
|
+
return commitKernelAndProject(dir, current.kernel);
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
...current,
|
|
473
|
+
idempotent: true,
|
|
474
|
+
audit: prior,
|
|
475
|
+
projected: fs.existsSync(path.join(dir, "task.json")),
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
throw new KernelError("INVALID_REQUEST", `kernel.json already exists for ${current.kernel.identity.taskId}`);
|
|
479
|
+
}
|
|
480
|
+
const openState = deriveStateForPhase("open");
|
|
481
|
+
const defineState = deriveStateForPhase("define");
|
|
482
|
+
const audit = {
|
|
483
|
+
id: crypto.randomUUID(),
|
|
484
|
+
at: new Date().toISOString(),
|
|
485
|
+
actor,
|
|
486
|
+
idempotencyKey,
|
|
487
|
+
evidence,
|
|
488
|
+
from: { ...openState, revision: 0 },
|
|
489
|
+
to: { ...defineState, revision: 1 },
|
|
490
|
+
};
|
|
491
|
+
const snapshot = {
|
|
492
|
+
schemaVersion: KERNEL_SCHEMA_VERSION,
|
|
493
|
+
identity: { taskId: record.id },
|
|
494
|
+
revision: 1,
|
|
495
|
+
phase: defineState.phase,
|
|
496
|
+
condition: defineState.condition,
|
|
497
|
+
outcome: defineState.outcome,
|
|
498
|
+
audit: [audit],
|
|
499
|
+
gates: emptyKernelGates(),
|
|
500
|
+
projection: null,
|
|
501
|
+
};
|
|
502
|
+
const next = attachProjection(snapshot, record, extras, "planning");
|
|
503
|
+
const result = commitKernelAndProject(dir, next);
|
|
504
|
+
return { ...result, audit, idempotent: false };
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
export function applyKernelStart(request) {
|
|
508
|
+
validateTransitionHooks(request);
|
|
509
|
+
const dir = resolveTaskDir(request.taskDir, request.cwd);
|
|
510
|
+
const actor = requireNonEmptyString(request.actor, "actor");
|
|
511
|
+
const idempotencyKey = requireNonEmptyString(request.idempotencyKey, "idempotencyKey");
|
|
512
|
+
if (!isNonNegativeInt(request.expectedRevision)) {
|
|
513
|
+
throw new KernelError("INVALID_REQUEST", "expectedRevision must be a non-negative integer");
|
|
514
|
+
}
|
|
515
|
+
const record = taskRecordSchema.parse(request.record);
|
|
516
|
+
const evidence = requireEvidence(request.evidence, "evidence");
|
|
517
|
+
return withKernelLock(dir, () => {
|
|
518
|
+
const current = readKernelUnlocked(dir);
|
|
519
|
+
const prior = current.kernel.audit.find((event) => event.idempotencyKey === idempotencyKey);
|
|
520
|
+
if (prior) {
|
|
521
|
+
if (!fs.existsSync(path.join(dir, "task.json")) && current.kernel.projection) {
|
|
522
|
+
return commitKernelAndProject(dir, current.kernel);
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
...current,
|
|
526
|
+
idempotent: true,
|
|
527
|
+
audit: prior,
|
|
528
|
+
projected: true,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
expectRevision(current.kernel, request.expectedRevision);
|
|
532
|
+
const extras = mergeExtras(current.kernel.projection?.extras ?? {}, request.extras, record, "start");
|
|
533
|
+
assertFullQualityForPhase(dir, extras, "start");
|
|
534
|
+
const hops = hopsToExecute(current.kernel.phase);
|
|
535
|
+
const hopped = hopKernelSnapshot(current.kernel, hops, {
|
|
536
|
+
actor,
|
|
537
|
+
idempotencyKey,
|
|
538
|
+
evidence,
|
|
539
|
+
});
|
|
540
|
+
const next = attachProjection(hopped.snapshot, record, extras, "in_progress");
|
|
541
|
+
const result = commitKernelAndProject(dir, next);
|
|
542
|
+
return { ...result, audit: hopped.audit, idempotent: hopped.idempotent };
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
export function applyKernelRecordGate(request) {
|
|
546
|
+
const dir = resolveTaskDir(request.taskDir, request.cwd);
|
|
547
|
+
const actor = requireNonEmptyString(request.actor, "actor");
|
|
548
|
+
const idempotencyKey = requireNonEmptyString(request.idempotencyKey, "idempotencyKey");
|
|
549
|
+
const transition = requireNonEmptyString(request.transition, "transition");
|
|
550
|
+
const gateName = requireNonEmptyString(request.gateName, "gate");
|
|
551
|
+
if (!isNonNegativeInt(request.expectedRevision)) {
|
|
552
|
+
throw new KernelError("INVALID_REQUEST", "expectedRevision must be a non-negative integer");
|
|
553
|
+
}
|
|
554
|
+
assertHonestGateRecord(request.record);
|
|
555
|
+
if (gateName === "independent-check") {
|
|
556
|
+
assertIndependentCheckGateRecord(request.record);
|
|
557
|
+
}
|
|
558
|
+
const evidence = requireEvidence(request.evidence, "evidence") ??
|
|
559
|
+
(typeof request.record.evidence === "string"
|
|
560
|
+
? request.record.evidence
|
|
561
|
+
: null);
|
|
562
|
+
return withKernelLock(dir, () => {
|
|
563
|
+
const current = readKernelUnlocked(dir);
|
|
564
|
+
const prior = current.kernel.audit.find((event) => event.idempotencyKey === idempotencyKey);
|
|
565
|
+
if (prior) {
|
|
566
|
+
if (!fs.existsSync(path.join(dir, "task.json")) && current.kernel.projection) {
|
|
567
|
+
return commitKernelAndProject(dir, current.kernel);
|
|
568
|
+
}
|
|
569
|
+
return {
|
|
570
|
+
...current,
|
|
571
|
+
idempotent: true,
|
|
572
|
+
audit: prior,
|
|
573
|
+
projected: true,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
expectRevision(current.kernel, request.expectedRevision);
|
|
577
|
+
const gates = {
|
|
578
|
+
schemaVersion: 1,
|
|
579
|
+
transitions: {
|
|
580
|
+
...current.kernel.gates.transitions,
|
|
581
|
+
[transition]: {
|
|
582
|
+
...(current.kernel.gates.transitions[transition] ?? {}),
|
|
583
|
+
[gateName]: cloneJsonObject(request.record),
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
};
|
|
587
|
+
const hopped = hopKernelSnapshot(current.kernel, [], {
|
|
588
|
+
actor,
|
|
589
|
+
idempotencyKey,
|
|
590
|
+
evidence,
|
|
591
|
+
});
|
|
592
|
+
const baseRecord = current.kernel.projection?.record ??
|
|
593
|
+
loadTaskRecord({ taskDir: dir });
|
|
594
|
+
const extras = mergeExtras(current.kernel.projection?.extras ?? {}, request.extras, baseRecord, "patch");
|
|
595
|
+
extras.quality_gate_results =
|
|
596
|
+
extras.quality_gate_results ??
|
|
597
|
+
{
|
|
598
|
+
schema_version: 1,
|
|
599
|
+
transitions: gates.transitions,
|
|
600
|
+
};
|
|
601
|
+
if (gateName === "independent-check" && extras.independent_check === undefined) {
|
|
602
|
+
extras.independent_check = {
|
|
603
|
+
schema_version: 1,
|
|
604
|
+
mode: request.record.mode ?? request.record.assurance,
|
|
605
|
+
readonly: true,
|
|
606
|
+
result: request.record.result,
|
|
607
|
+
evidence: request.record.evidence,
|
|
608
|
+
independent_worker: request.record.independent_worker === true,
|
|
609
|
+
code_fingerprint: request.record.code_fingerprint ?? "",
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
const next = attachProjection({ ...hopped.snapshot, gates }, baseRecord, extras, baseRecord.status);
|
|
613
|
+
const result = commitKernelAndProject(dir, next);
|
|
614
|
+
return { ...result, audit: hopped.audit, idempotent: false };
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
export function applyKernelArchive(request) {
|
|
618
|
+
validateTransitionHooks(request);
|
|
619
|
+
const dir = resolveTaskDir(request.taskDir, request.cwd);
|
|
620
|
+
const actor = requireNonEmptyString(request.actor, "actor");
|
|
621
|
+
const idempotencyKey = requireNonEmptyString(request.idempotencyKey, "idempotencyKey");
|
|
622
|
+
if (!isNonNegativeInt(request.expectedRevision)) {
|
|
623
|
+
throw new KernelError("INVALID_REQUEST", "expectedRevision must be a non-negative integer");
|
|
624
|
+
}
|
|
625
|
+
const record = taskRecordSchema.parse(request.record);
|
|
626
|
+
const evidence = requireEvidence(request.evidence, "evidence");
|
|
627
|
+
return withKernelLock(dir, () => {
|
|
628
|
+
const current = readKernelUnlocked(dir);
|
|
629
|
+
const prior = current.kernel.audit.find((event) => event.idempotencyKey === idempotencyKey);
|
|
630
|
+
if (prior) {
|
|
631
|
+
if (!fs.existsSync(path.join(dir, "task.json")) && current.kernel.projection) {
|
|
632
|
+
return commitKernelAndProject(dir, current.kernel);
|
|
633
|
+
}
|
|
634
|
+
return {
|
|
635
|
+
...current,
|
|
636
|
+
idempotent: true,
|
|
637
|
+
audit: prior,
|
|
638
|
+
projected: true,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
expectRevision(current.kernel, request.expectedRevision);
|
|
642
|
+
const extras = mergeExtras(current.kernel.projection?.extras ?? {}, request.extras, record, "archive");
|
|
643
|
+
assertFullQualityForPhase(dir, extras, "archive");
|
|
644
|
+
const hops = hopsToClose(current.kernel.phase);
|
|
645
|
+
const hopped = hopKernelSnapshot(current.kernel, hops, {
|
|
646
|
+
actor,
|
|
647
|
+
idempotencyKey,
|
|
648
|
+
evidence,
|
|
649
|
+
});
|
|
650
|
+
const next = attachProjection(hopped.snapshot, record, extras, "completed");
|
|
651
|
+
const result = commitKernelAndProject(dir, next);
|
|
652
|
+
return { ...result, audit: hopped.audit, idempotent: hopped.idempotent };
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
function mergeCanonicalRecord(base, patch) {
|
|
656
|
+
if (patch === undefined || patch === null)
|
|
657
|
+
return base;
|
|
658
|
+
if (!isPlainObject(patch)) {
|
|
659
|
+
throw new KernelError("INVALID_REQUEST", "record must be a JSON object");
|
|
660
|
+
}
|
|
661
|
+
const merged = { ...base };
|
|
662
|
+
for (const field of TASK_RECORD_FIELD_ORDER) {
|
|
663
|
+
if (!(field in patch))
|
|
664
|
+
continue;
|
|
665
|
+
if (field === "meta" && isPlainObject(patch.meta) && isPlainObject(base.meta)) {
|
|
666
|
+
merged.meta = { ...base.meta, ...patch.meta };
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
merged[field] = patch[field];
|
|
670
|
+
}
|
|
671
|
+
try {
|
|
672
|
+
return taskRecordSchema.parse(merged);
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
throw new KernelError("INVALID_REQUEST", `record is not a canonical task.json shape: ${err instanceof Error ? err.message : String(err)}`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function assertNoLifecycleStatusHop(from, to) {
|
|
679
|
+
if (from === to)
|
|
680
|
+
return;
|
|
681
|
+
throw new KernelError("INVALID_TRANSITION", `Kernel patch cannot change status (${from} -> ${to}); use start or archive`);
|
|
682
|
+
}
|
|
683
|
+
function currentProjectionRecord(dir, current) {
|
|
684
|
+
if (current.kernel.projection?.record) {
|
|
685
|
+
return current.kernel.projection.record;
|
|
686
|
+
}
|
|
687
|
+
return loadTaskRecord({ taskDir: dir });
|
|
688
|
+
}
|
|
689
|
+
export function applyKernelPatch(request) {
|
|
690
|
+
validateTransitionHooks(request);
|
|
691
|
+
const dir = resolveTaskDir(request.taskDir, request.cwd);
|
|
692
|
+
const actor = requireNonEmptyString(request.actor, "actor");
|
|
693
|
+
const idempotencyKey = requireNonEmptyString(request.idempotencyKey, "idempotencyKey");
|
|
694
|
+
if (!isNonNegativeInt(request.expectedRevision)) {
|
|
695
|
+
throw new KernelError("INVALID_REQUEST", "expectedRevision must be a non-negative integer");
|
|
696
|
+
}
|
|
697
|
+
if (request.record === undefined && request.extras === undefined) {
|
|
698
|
+
throw new KernelError("INVALID_REQUEST", "patch requires record and/or extras");
|
|
699
|
+
}
|
|
700
|
+
const extrasPatch = request.extras === undefined ? undefined : cloneJsonObject(request.extras);
|
|
701
|
+
const evidence = requireEvidence(request.evidence, "evidence");
|
|
702
|
+
return withKernelLock(dir, () => {
|
|
703
|
+
const current = readKernelUnlocked(dir);
|
|
704
|
+
const prior = current.kernel.audit.find((event) => event.idempotencyKey === idempotencyKey);
|
|
705
|
+
if (prior) {
|
|
706
|
+
if (!fs.existsSync(path.join(dir, "task.json")) && current.kernel.projection) {
|
|
707
|
+
return commitKernelAndProject(dir, current.kernel);
|
|
708
|
+
}
|
|
709
|
+
return {
|
|
710
|
+
...current,
|
|
711
|
+
idempotent: true,
|
|
712
|
+
audit: prior,
|
|
713
|
+
projected: true,
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
expectRevision(current.kernel, request.expectedRevision);
|
|
717
|
+
const baseRecord = currentProjectionRecord(dir, current);
|
|
718
|
+
const nextRecord = mergeCanonicalRecord(baseRecord, request.record);
|
|
719
|
+
assertNoLifecycleStatusHop(baseRecord.status, nextRecord.status);
|
|
720
|
+
const baseExtras = cloneJsonObject(current.kernel.projection?.extras ?? {});
|
|
721
|
+
const extras = extrasPatch === undefined ? baseExtras : { ...baseExtras, ...extrasPatch };
|
|
722
|
+
normalizeRequiredControlsInExtras(extras);
|
|
723
|
+
normalizeStage5InExtrasAndAssert(extras, nextRecord, "patch");
|
|
724
|
+
normalizeStage6InExtrasAndAssert(extras, "patch");
|
|
725
|
+
const hopped = hopKernelSnapshot(current.kernel, [], {
|
|
726
|
+
actor,
|
|
727
|
+
idempotencyKey,
|
|
728
|
+
evidence,
|
|
729
|
+
});
|
|
730
|
+
const next = attachProjection(hopped.snapshot, nextRecord, extras, nextRecord.status);
|
|
731
|
+
const result = commitKernelAndProject(dir, next);
|
|
732
|
+
return { ...result, audit: hopped.audit, idempotent: false };
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
//# sourceMappingURL=kernel-store.js.map
|