@hardkas/core 0.5.5-alpha → 0.6.1-alpha
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 +402 -14
- package/dist/index.js +807 -65
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,244 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
|
|
4
|
+
// src/append-coordinator.ts
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path2 from "path";
|
|
7
|
+
|
|
8
|
+
// src/telemetry.ts
|
|
9
|
+
import path from "path";
|
|
10
|
+
import crypto2 from "crypto";
|
|
11
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
12
|
+
var TelemetryManager = class {
|
|
13
|
+
rootDir = null;
|
|
14
|
+
currentContext = {};
|
|
15
|
+
// Track sandboxes that need to be preserved because they hit severe anomalies
|
|
16
|
+
preservedSandboxes = /* @__PURE__ */ new Set();
|
|
17
|
+
constructor(rootDir) {
|
|
18
|
+
if (rootDir) this.rootDir = rootDir;
|
|
19
|
+
}
|
|
20
|
+
init(rootDir) {
|
|
21
|
+
this.rootDir = rootDir;
|
|
22
|
+
}
|
|
23
|
+
setContext(context) {
|
|
24
|
+
this.currentContext = { ...this.currentContext, ...context };
|
|
25
|
+
}
|
|
26
|
+
clearContext() {
|
|
27
|
+
this.currentContext = {};
|
|
28
|
+
}
|
|
29
|
+
getContext() {
|
|
30
|
+
return this.currentContext;
|
|
31
|
+
}
|
|
32
|
+
logAnomaly(anomalyType, severity, subsystem, details, sandboxOverride) {
|
|
33
|
+
const logDir = this.rootDir ? path.join(this.rootDir, ".hardkas", "telemetry") : sandboxOverride ? path.join(sandboxOverride, ".hardkas", "telemetry") : null;
|
|
34
|
+
if (!logDir) return;
|
|
35
|
+
const nowStr = (/* @__PURE__ */ new Date()).toISOString();
|
|
36
|
+
const runId = this.currentContext.seed ? `run-${this.currentContext.seed}` : "run-core";
|
|
37
|
+
const bucket = this.currentContext.bucket || "core";
|
|
38
|
+
let mappedSeverity = "nominal";
|
|
39
|
+
if (severity === "medium") mappedSeverity = "elevated";
|
|
40
|
+
else if (severity === "high" || severity === "critical") mappedSeverity = "critical";
|
|
41
|
+
const canonicalPayloadRaw = JSON.stringify({
|
|
42
|
+
runId,
|
|
43
|
+
bucket,
|
|
44
|
+
type: anomalyType,
|
|
45
|
+
severity: mappedSeverity,
|
|
46
|
+
caseId: this.currentContext.caseId,
|
|
47
|
+
payload: {
|
|
48
|
+
subsystem,
|
|
49
|
+
details,
|
|
50
|
+
sandbox: sandboxOverride
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
const eventHash = crypto2.createHash("sha256").update(canonicalPayloadRaw).digest("hex").slice(0, 32);
|
|
54
|
+
const eventIdRaw = `${eventHash}-${nowStr}`;
|
|
55
|
+
const eventId = crypto2.createHash("sha256").update(eventIdRaw).digest("hex").slice(0, 32);
|
|
56
|
+
const event = {
|
|
57
|
+
schemaVersion: "hardkas.telemetry.v1",
|
|
58
|
+
eventId,
|
|
59
|
+
eventHash,
|
|
60
|
+
timestamp: nowStr,
|
|
61
|
+
source: "core-runtime",
|
|
62
|
+
runId,
|
|
63
|
+
bucket,
|
|
64
|
+
type: anomalyType,
|
|
65
|
+
severity: mappedSeverity,
|
|
66
|
+
caseId: this.currentContext.caseId,
|
|
67
|
+
payload: {
|
|
68
|
+
subsystem,
|
|
69
|
+
details,
|
|
70
|
+
sandbox: sandboxOverride
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
if (severity === "high" || severity === "critical" || anomalyType === "REPLAY_RECONCILIATION" || anomalyType === "NORMALIZATION_COLLISION") {
|
|
74
|
+
if (sandboxOverride) {
|
|
75
|
+
this.preservedSandboxes.add(sandboxOverride);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const logFile = path.join(logDir, "telemetry.jsonl");
|
|
80
|
+
const root = this.rootDir || sandboxOverride || process.cwd();
|
|
81
|
+
AppendCoordinator.appendAtomic(logFile, JSON.stringify(event), root);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
shouldPreserveSandbox(sandboxDir) {
|
|
86
|
+
return this.preservedSandboxes.has(sandboxDir);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var telemetryContextStorage = new AsyncLocalStorage();
|
|
90
|
+
var globalTelemetry = new TelemetryManager();
|
|
91
|
+
function getTelemetry() {
|
|
92
|
+
return telemetryContextStorage.getStore() || globalTelemetry;
|
|
93
|
+
}
|
|
94
|
+
var TelemetryProxy = class {
|
|
95
|
+
logAnomaly(anomalyType, severity, subsystem, details, sandboxOverride) {
|
|
96
|
+
return getTelemetry().logAnomaly(anomalyType, severity, subsystem, details, sandboxOverride);
|
|
97
|
+
}
|
|
98
|
+
init(rootDir) {
|
|
99
|
+
return getTelemetry().init(rootDir);
|
|
100
|
+
}
|
|
101
|
+
setContext(context) {
|
|
102
|
+
return getTelemetry().setContext(context);
|
|
103
|
+
}
|
|
104
|
+
clearContext() {
|
|
105
|
+
return getTelemetry().clearContext();
|
|
106
|
+
}
|
|
107
|
+
getContext() {
|
|
108
|
+
return getTelemetry().getContext();
|
|
109
|
+
}
|
|
110
|
+
shouldPreserveSandbox(sandboxDir) {
|
|
111
|
+
return getTelemetry().shouldPreserveSandbox(sandboxDir);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
var EnvironmentTelemetry = new TelemetryProxy();
|
|
115
|
+
|
|
116
|
+
// src/append-coordinator.ts
|
|
117
|
+
var AppendCoordinator = class _AppendCoordinator {
|
|
118
|
+
/**
|
|
119
|
+
* Safely appends a line to a JSONL log under process coordination locks.
|
|
120
|
+
* Performs an immediate fsync to ensure data durability.
|
|
121
|
+
* Also repairs the trailing line if it is corrupted, emitting an anomaly.
|
|
122
|
+
*/
|
|
123
|
+
static appendAtomic(filePath, line, rootDir) {
|
|
124
|
+
const lockDir = path2.join(rootDir, ".hardkas", "locks");
|
|
125
|
+
if (!fs.existsSync(lockDir)) {
|
|
126
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
127
|
+
}
|
|
128
|
+
const logBase = path2.basename(filePath);
|
|
129
|
+
const lockPath = path2.join(lockDir, `append-${logBase}.lock`);
|
|
130
|
+
let fd = null;
|
|
131
|
+
let repaired = false;
|
|
132
|
+
let linesDiscarded = 0;
|
|
133
|
+
let originalTail = "";
|
|
134
|
+
try {
|
|
135
|
+
const start = Date.now();
|
|
136
|
+
const timeoutMs = 1e4;
|
|
137
|
+
while (true) {
|
|
138
|
+
try {
|
|
139
|
+
fd = fs.openSync(lockPath, "wx");
|
|
140
|
+
break;
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (e.code === "EEXIST") {
|
|
143
|
+
if (Date.now() - start > timeoutMs) {
|
|
144
|
+
throw new Error(`[AppendCoordinator] Timeout waiting for lock on ${lockPath}`);
|
|
145
|
+
}
|
|
146
|
+
const sleepMs = 5 + Math.floor(Math.random() * 15);
|
|
147
|
+
const sharedBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
148
|
+
Atomics.wait(sharedBuf, 0, 0, sleepMs);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
throw e;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, time: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
155
|
+
const recovery = _AppendCoordinator.recoverCorruptedTail(filePath);
|
|
156
|
+
if (recovery.repaired) {
|
|
157
|
+
repaired = true;
|
|
158
|
+
linesDiscarded = recovery.linesDiscarded;
|
|
159
|
+
originalTail = recovery.originalTail;
|
|
160
|
+
}
|
|
161
|
+
const logDir = path2.dirname(filePath);
|
|
162
|
+
if (!fs.existsSync(logDir)) {
|
|
163
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
164
|
+
}
|
|
165
|
+
const logFd = fs.openSync(filePath, "a");
|
|
166
|
+
const buffer = Buffer.from(line.endsWith("\n") ? line : line + "\n", "utf-8");
|
|
167
|
+
fs.writeSync(logFd, buffer, 0, buffer.length);
|
|
168
|
+
fs.fsyncSync(logFd);
|
|
169
|
+
fs.closeSync(logFd);
|
|
170
|
+
} finally {
|
|
171
|
+
if (fd !== null) {
|
|
172
|
+
fs.closeSync(fd);
|
|
173
|
+
try {
|
|
174
|
+
fs.unlinkSync(lockPath);
|
|
175
|
+
} catch {
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (repaired) {
|
|
180
|
+
try {
|
|
181
|
+
const telemetry = getTelemetry();
|
|
182
|
+
telemetry.logAnomaly(
|
|
183
|
+
"EXTERNAL_MUTATION",
|
|
184
|
+
"medium",
|
|
185
|
+
"fs",
|
|
186
|
+
`Recovered corrupted trailing line in ${logBase}. Discarded ${linesDiscarded} malformed bytes. Original tail snippet: "${originalTail.slice(0, 60)}..."`,
|
|
187
|
+
rootDir
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Scans a JSONL stream for corruption, truncating malformed trailing lines.
|
|
195
|
+
*/
|
|
196
|
+
static recoverCorruptedTail(filePath) {
|
|
197
|
+
if (!fs.existsSync(filePath)) return { repaired: false, linesDiscarded: 0, originalTail: "" };
|
|
198
|
+
const stat = fs.statSync(filePath);
|
|
199
|
+
if (stat.size === 0) return { repaired: false, linesDiscarded: 0, originalTail: "" };
|
|
200
|
+
const TAIL_SIZE = 4096;
|
|
201
|
+
const readStart = Math.max(0, stat.size - TAIL_SIZE);
|
|
202
|
+
const fd = fs.openSync(filePath, "r");
|
|
203
|
+
const buf = Buffer.alloc(Math.min(TAIL_SIZE, stat.size));
|
|
204
|
+
fs.readSync(fd, buf, 0, buf.length, readStart);
|
|
205
|
+
fs.closeSync(fd);
|
|
206
|
+
const tail = buf.toString("utf-8");
|
|
207
|
+
const lines = tail.split("\n");
|
|
208
|
+
if (lines.length === 0) return { repaired: false, linesDiscarded: 0, originalTail: "" };
|
|
209
|
+
let lastLine = "";
|
|
210
|
+
let lastLineIdx = -1;
|
|
211
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
212
|
+
const l = lines[i].trim();
|
|
213
|
+
if (l) {
|
|
214
|
+
lastLine = l;
|
|
215
|
+
lastLineIdx = i;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!lastLine) return { repaired: false, linesDiscarded: 0, originalTail: "" };
|
|
220
|
+
if (readStart > 0 && lastLineIdx === 0) {
|
|
221
|
+
return { repaired: false, linesDiscarded: 0, originalTail: "" };
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
JSON.parse(lastLine);
|
|
225
|
+
return { repaired: false, linesDiscarded: 0, originalTail: "" };
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const linesAfterCorrupt = lines.slice(lastLineIdx).join("\n");
|
|
228
|
+
const bytesToRemove = Buffer.byteLength(linesAfterCorrupt, "utf-8");
|
|
229
|
+
const truncateTo = stat.size - bytesToRemove;
|
|
230
|
+
fs.truncateSync(filePath, truncateTo > 0 ? truncateTo : 0);
|
|
231
|
+
return {
|
|
232
|
+
repaired: true,
|
|
233
|
+
linesDiscarded: stat.size - truncateTo,
|
|
234
|
+
originalTail: lastLine
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
4
240
|
// src/events.ts
|
|
241
|
+
import path3 from "path";
|
|
5
242
|
var CoreEventBus = class {
|
|
6
243
|
listeners = [];
|
|
7
244
|
on(listener) {
|
|
@@ -37,13 +274,16 @@ var CoreEventBus = class {
|
|
|
37
274
|
};
|
|
38
275
|
var coreEvents = new CoreEventBus();
|
|
39
276
|
function createEventEnvelope(params) {
|
|
277
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
40
278
|
return {
|
|
41
279
|
schema: "hardkas.event",
|
|
42
280
|
version: "1.0.0",
|
|
43
281
|
eventId: params.eventId || crypto.randomUUID(),
|
|
44
282
|
domain: params.domain,
|
|
45
283
|
kind: params.kind,
|
|
46
|
-
timestamp
|
|
284
|
+
timestamp,
|
|
285
|
+
emittedAt: timestamp,
|
|
286
|
+
sourceSubsystem: params.sourceSubsystem,
|
|
47
287
|
workflowId: params.workflowId,
|
|
48
288
|
correlationId: params.correlationId,
|
|
49
289
|
causationId: params.causationId,
|
|
@@ -51,7 +291,8 @@ function createEventEnvelope(params) {
|
|
|
51
291
|
txId: params.txId,
|
|
52
292
|
networkId: params.networkId,
|
|
53
293
|
payload: params.payload,
|
|
54
|
-
|
|
294
|
+
sequenceNumber: params.sequenceNumber,
|
|
295
|
+
globalOffset: params.globalOffset
|
|
55
296
|
};
|
|
56
297
|
}
|
|
57
298
|
function validateEventEnvelope(event) {
|
|
@@ -62,6 +303,25 @@ function validateEventEnvelope(event) {
|
|
|
62
303
|
if (typeof event.payload !== "object") return false;
|
|
63
304
|
return true;
|
|
64
305
|
}
|
|
306
|
+
function attachLedgerAppender(workspaceRoot) {
|
|
307
|
+
const seenEventIds = /* @__PURE__ */ new Set();
|
|
308
|
+
const eventsFile = path3.join(workspaceRoot, "events.jsonl");
|
|
309
|
+
return coreEvents.on((event) => {
|
|
310
|
+
if (seenEventIds.has(event.eventId)) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
seenEventIds.add(event.eventId);
|
|
314
|
+
if (seenEventIds.size > 1e5) {
|
|
315
|
+
const iterator = seenEventIds.keys();
|
|
316
|
+
for (let i = 0; i < 1e4; i++) seenEventIds.delete(iterator.next().value);
|
|
317
|
+
}
|
|
318
|
+
const payload = JSON.stringify(event) + "\n";
|
|
319
|
+
try {
|
|
320
|
+
AppendCoordinator.appendAtomic(eventsFile, payload, workspaceRoot);
|
|
321
|
+
} catch (e) {
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
}
|
|
65
325
|
|
|
66
326
|
// src/domain-types.ts
|
|
67
327
|
var asTxId = (id) => id;
|
|
@@ -112,33 +372,35 @@ function redactSecret(value) {
|
|
|
112
372
|
}
|
|
113
373
|
|
|
114
374
|
// src/fs.ts
|
|
115
|
-
import
|
|
116
|
-
import
|
|
375
|
+
import fs2 from "fs";
|
|
376
|
+
import path4 from "path";
|
|
377
|
+
import crypto3 from "crypto";
|
|
117
378
|
async function writeFileAtomic(targetPath, data, options = {}) {
|
|
118
|
-
const dir =
|
|
119
|
-
const base =
|
|
120
|
-
const tempPath =
|
|
379
|
+
const dir = path4.dirname(targetPath);
|
|
380
|
+
const base = path4.basename(targetPath);
|
|
381
|
+
const tempPath = path4.join(dir, `.tmp.${base}.${crypto3.randomUUID()}`);
|
|
121
382
|
let fd = null;
|
|
122
383
|
try {
|
|
123
|
-
if (!
|
|
124
|
-
|
|
384
|
+
if (!fs2.existsSync(dir)) {
|
|
385
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
125
386
|
}
|
|
126
|
-
fd =
|
|
387
|
+
fd = fs2.openSync(tempPath, "w", options.mode);
|
|
127
388
|
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
389
|
+
fs2.writeSync(fd, buffer, 0, buffer.length);
|
|
390
|
+
fs2.fsyncSync(fd);
|
|
391
|
+
fs2.closeSync(fd);
|
|
131
392
|
fd = null;
|
|
132
393
|
let attempts = 0;
|
|
133
394
|
const maxAttempts = process.platform === "win32" ? 5 : 1;
|
|
134
395
|
while (attempts < maxAttempts) {
|
|
135
396
|
try {
|
|
136
|
-
|
|
397
|
+
fs2.renameSync(tempPath, targetPath);
|
|
137
398
|
break;
|
|
138
399
|
} catch (e) {
|
|
139
400
|
attempts++;
|
|
140
401
|
if (attempts >= maxAttempts) throw e;
|
|
141
402
|
if (e.code === "EPERM" || e.code === "EBUSY") {
|
|
403
|
+
EnvironmentTelemetry.logAnomaly("FS_RETRY", "low", "fs", `Retrying rename of ${targetPath} due to ${e.code}`);
|
|
142
404
|
await new Promise((resolve) => setTimeout(resolve, 10 * attempts));
|
|
143
405
|
continue;
|
|
144
406
|
}
|
|
@@ -148,11 +410,11 @@ async function writeFileAtomic(targetPath, data, options = {}) {
|
|
|
148
410
|
if (options.fsyncParent && process.platform !== "win32") {
|
|
149
411
|
let dirFd = null;
|
|
150
412
|
try {
|
|
151
|
-
dirFd =
|
|
152
|
-
|
|
413
|
+
dirFd = fs2.openSync(dir, "r");
|
|
414
|
+
fs2.fsyncSync(dirFd);
|
|
153
415
|
} catch (e) {
|
|
154
416
|
} finally {
|
|
155
|
-
if (dirFd !== null)
|
|
417
|
+
if (dirFd !== null) fs2.closeSync(dirFd);
|
|
156
418
|
}
|
|
157
419
|
}
|
|
158
420
|
} catch (err) {
|
|
@@ -162,45 +424,46 @@ async function writeFileAtomic(targetPath, data, options = {}) {
|
|
|
162
424
|
{ cause: err }
|
|
163
425
|
);
|
|
164
426
|
} finally {
|
|
165
|
-
if (
|
|
427
|
+
if (fs2.existsSync(tempPath)) {
|
|
166
428
|
try {
|
|
167
|
-
|
|
429
|
+
fs2.unlinkSync(tempPath);
|
|
168
430
|
} catch (e) {
|
|
169
431
|
}
|
|
170
432
|
}
|
|
171
433
|
if (fd !== null) {
|
|
172
434
|
try {
|
|
173
|
-
|
|
435
|
+
fs2.closeSync(fd);
|
|
174
436
|
} catch (e) {
|
|
175
437
|
}
|
|
176
438
|
}
|
|
177
439
|
}
|
|
178
440
|
}
|
|
179
441
|
function writeFileAtomicSync(targetPath, data, options = {}) {
|
|
180
|
-
const dir =
|
|
181
|
-
const base =
|
|
182
|
-
const tempPath =
|
|
442
|
+
const dir = path4.dirname(targetPath);
|
|
443
|
+
const base = path4.basename(targetPath);
|
|
444
|
+
const tempPath = path4.join(dir, `.tmp.${base}.${crypto3.randomUUID()}`);
|
|
183
445
|
let fd = null;
|
|
184
446
|
try {
|
|
185
|
-
if (!
|
|
186
|
-
|
|
447
|
+
if (!fs2.existsSync(dir)) {
|
|
448
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
187
449
|
}
|
|
188
|
-
fd =
|
|
450
|
+
fd = fs2.openSync(tempPath, "w", options.mode);
|
|
189
451
|
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
452
|
+
fs2.writeSync(fd, buffer, 0, buffer.length);
|
|
453
|
+
fs2.fsyncSync(fd);
|
|
454
|
+
fs2.closeSync(fd);
|
|
193
455
|
fd = null;
|
|
194
456
|
let attempts = 0;
|
|
195
457
|
const maxAttempts = process.platform === "win32" ? 5 : 1;
|
|
196
458
|
while (attempts < maxAttempts) {
|
|
197
459
|
try {
|
|
198
|
-
|
|
460
|
+
fs2.renameSync(tempPath, targetPath);
|
|
199
461
|
break;
|
|
200
462
|
} catch (e) {
|
|
201
463
|
attempts++;
|
|
202
464
|
if (attempts >= maxAttempts) throw e;
|
|
203
465
|
if (e.code === "EPERM" || e.code === "EBUSY") {
|
|
466
|
+
EnvironmentTelemetry.logAnomaly("FS_RETRY", "low", "fs", `Retrying rename sync of ${targetPath} due to ${e.code}`);
|
|
204
467
|
continue;
|
|
205
468
|
}
|
|
206
469
|
throw e;
|
|
@@ -209,11 +472,11 @@ function writeFileAtomicSync(targetPath, data, options = {}) {
|
|
|
209
472
|
if (options.fsyncParent && process.platform !== "win32") {
|
|
210
473
|
let dirFd = null;
|
|
211
474
|
try {
|
|
212
|
-
dirFd =
|
|
213
|
-
|
|
475
|
+
dirFd = fs2.openSync(dir, "r");
|
|
476
|
+
fs2.fsyncSync(dirFd);
|
|
214
477
|
} catch (e) {
|
|
215
478
|
} finally {
|
|
216
|
-
if (dirFd !== null)
|
|
479
|
+
if (dirFd !== null) fs2.closeSync(dirFd);
|
|
217
480
|
}
|
|
218
481
|
}
|
|
219
482
|
} catch (err) {
|
|
@@ -223,15 +486,15 @@ function writeFileAtomicSync(targetPath, data, options = {}) {
|
|
|
223
486
|
{ cause: err }
|
|
224
487
|
);
|
|
225
488
|
} finally {
|
|
226
|
-
if (
|
|
489
|
+
if (fs2.existsSync(tempPath)) {
|
|
227
490
|
try {
|
|
228
|
-
|
|
491
|
+
fs2.unlinkSync(tempPath);
|
|
229
492
|
} catch (e) {
|
|
230
493
|
}
|
|
231
494
|
}
|
|
232
495
|
if (fd !== null) {
|
|
233
496
|
try {
|
|
234
|
-
|
|
497
|
+
fs2.closeSync(fd);
|
|
235
498
|
} catch (e) {
|
|
236
499
|
}
|
|
237
500
|
}
|
|
@@ -257,8 +520,8 @@ function formatCorruptionIssue(issue) {
|
|
|
257
520
|
}
|
|
258
521
|
|
|
259
522
|
// src/lock.ts
|
|
260
|
-
import
|
|
261
|
-
import
|
|
523
|
+
import fs3 from "fs";
|
|
524
|
+
import path5 from "path";
|
|
262
525
|
import os from "os";
|
|
263
526
|
var LOCK_ORDER = [
|
|
264
527
|
"workspace",
|
|
@@ -269,13 +532,14 @@ var LOCK_ORDER = [
|
|
|
269
532
|
"query-store"
|
|
270
533
|
];
|
|
271
534
|
async function acquireLock(args) {
|
|
272
|
-
const lockDir =
|
|
273
|
-
const lockPath =
|
|
535
|
+
const lockDir = path5.join(args.rootDir, ".hardkas", "locks");
|
|
536
|
+
const lockPath = path5.join(lockDir, `${args.name}.lock`);
|
|
274
537
|
const timeoutMs = args.timeoutMs ?? 3e4;
|
|
275
538
|
const pollMs = args.pollMs ?? 250;
|
|
276
539
|
const start = Date.now();
|
|
277
|
-
|
|
278
|
-
|
|
540
|
+
let staleRecoveryAttempted = false;
|
|
541
|
+
if (!fs3.existsSync(lockDir)) {
|
|
542
|
+
fs3.mkdirSync(lockDir, { recursive: true });
|
|
279
543
|
}
|
|
280
544
|
while (true) {
|
|
281
545
|
try {
|
|
@@ -289,18 +553,18 @@ async function acquireLock(args) {
|
|
|
289
553
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
290
554
|
expiresAt: null
|
|
291
555
|
};
|
|
292
|
-
const fd =
|
|
293
|
-
|
|
294
|
-
|
|
556
|
+
const fd = fs3.openSync(lockPath, "wx");
|
|
557
|
+
fs3.writeSync(fd, JSON.stringify(metadata, null, 2));
|
|
558
|
+
fs3.closeSync(fd);
|
|
295
559
|
return {
|
|
296
560
|
path: lockPath,
|
|
297
561
|
metadata,
|
|
298
562
|
release: async () => {
|
|
299
|
-
if (
|
|
563
|
+
if (fs3.existsSync(lockPath)) {
|
|
300
564
|
try {
|
|
301
|
-
const current = JSON.parse(
|
|
565
|
+
const current = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
302
566
|
if (current.pid === process.pid) {
|
|
303
|
-
|
|
567
|
+
fs3.unlinkSync(lockPath);
|
|
304
568
|
}
|
|
305
569
|
} catch (e) {
|
|
306
570
|
}
|
|
@@ -311,12 +575,49 @@ async function acquireLock(args) {
|
|
|
311
575
|
if (e.code === "EEXIST") {
|
|
312
576
|
let existingMetadata = null;
|
|
313
577
|
try {
|
|
314
|
-
existingMetadata = JSON.parse(
|
|
578
|
+
existingMetadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
315
579
|
} catch (err) {
|
|
580
|
+
const LOCK_CREATION_GRACE_MS = 2e3;
|
|
581
|
+
let stats = null;
|
|
582
|
+
try {
|
|
583
|
+
stats = fs3.statSync(lockPath);
|
|
584
|
+
} catch {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
588
|
+
if (ageMs < LOCK_CREATION_GRACE_MS) {
|
|
589
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (!staleRecoveryAttempted) {
|
|
593
|
+
staleRecoveryAttempted = true;
|
|
594
|
+
try {
|
|
595
|
+
fs3.unlinkSync(lockPath);
|
|
596
|
+
EnvironmentTelemetry.logAnomaly("STALE_LOCK_RECOVERY", "medium", "lock", `Recovered corrupted lock file at ${lockPath} (Age: ${ageMs}ms)`, args.rootDir);
|
|
597
|
+
continue;
|
|
598
|
+
} catch {
|
|
599
|
+
throw new HardkasError("LOCK_METADATA_INVALID", `Lock file at ${lockPath} is corrupted and cannot be recovered.`, { cause: err });
|
|
600
|
+
}
|
|
601
|
+
}
|
|
316
602
|
throw new HardkasError("LOCK_METADATA_INVALID", `Lock file at ${lockPath} is corrupted.`, { cause: err });
|
|
317
603
|
}
|
|
318
604
|
if (existingMetadata) {
|
|
319
|
-
const
|
|
605
|
+
const isLocal = existingMetadata.hostname === os.hostname();
|
|
606
|
+
const isAlive = isLocal ? isProcessAlive(existingMetadata.pid) : true;
|
|
607
|
+
if (!isAlive && !staleRecoveryAttempted) {
|
|
608
|
+
staleRecoveryAttempted = true;
|
|
609
|
+
try {
|
|
610
|
+
fs3.unlinkSync(lockPath);
|
|
611
|
+
EnvironmentTelemetry.logAnomaly("STALE_LOCK_RECOVERY", "medium", "lock", `Recovered lock held by dead process (PID: ${existingMetadata.pid})`, args.rootDir);
|
|
612
|
+
continue;
|
|
613
|
+
} catch (unlinkErr) {
|
|
614
|
+
throw new HardkasError(
|
|
615
|
+
"STALE_LOCK",
|
|
616
|
+
`Workspace is locked by a dead process (PID: ${existingMetadata.pid}). Failed to auto-recover: ${unlinkErr}`,
|
|
617
|
+
{ cause: existingMetadata }
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
320
621
|
if (!isAlive) {
|
|
321
622
|
throw new HardkasError(
|
|
322
623
|
"STALE_LOCK",
|
|
@@ -325,6 +626,7 @@ async function acquireLock(args) {
|
|
|
325
626
|
);
|
|
326
627
|
}
|
|
327
628
|
if (args.wait && Date.now() - start < timeoutMs) {
|
|
629
|
+
EnvironmentTelemetry.logAnomaly("LOCK_CONTENTION", "low", "lock", `Waiting for lock ${args.name} held by PID ${existingMetadata.pid}`, args.rootDir);
|
|
328
630
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
329
631
|
continue;
|
|
330
632
|
}
|
|
@@ -370,20 +672,22 @@ function isProcessAlive(pid) {
|
|
|
370
672
|
process.kill(pid, 0);
|
|
371
673
|
return true;
|
|
372
674
|
} catch (e) {
|
|
373
|
-
|
|
675
|
+
if (e.code === "EPERM") return true;
|
|
676
|
+
if (e.code === "ESRCH") return false;
|
|
677
|
+
return true;
|
|
374
678
|
}
|
|
375
679
|
}
|
|
376
680
|
function listLocks(rootDir) {
|
|
377
|
-
const lockDir =
|
|
378
|
-
if (!
|
|
379
|
-
const files =
|
|
681
|
+
const lockDir = path5.join(rootDir, ".hardkas", "locks");
|
|
682
|
+
if (!fs3.existsSync(lockDir)) return [];
|
|
683
|
+
const files = fs3.readdirSync(lockDir).filter((f) => f.endsWith(".lock"));
|
|
380
684
|
const result = [];
|
|
381
685
|
for (const file of files) {
|
|
382
|
-
const lockPath =
|
|
686
|
+
const lockPath = path5.join(lockDir, file);
|
|
383
687
|
try {
|
|
384
|
-
const metadata = JSON.parse(
|
|
688
|
+
const metadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
385
689
|
result.push({
|
|
386
|
-
name:
|
|
690
|
+
name: path5.basename(file, ".lock"),
|
|
387
691
|
metadata,
|
|
388
692
|
path: lockPath,
|
|
389
693
|
isAlive: metadata.hostname === os.hostname() ? isProcessAlive(metadata.pid) : true
|
|
@@ -395,15 +699,15 @@ function listLocks(rootDir) {
|
|
|
395
699
|
return result;
|
|
396
700
|
}
|
|
397
701
|
function clearLock(rootDir, name, options = {}) {
|
|
398
|
-
const lockDir =
|
|
399
|
-
const lockPath =
|
|
400
|
-
if (!
|
|
702
|
+
const lockDir = path5.join(rootDir, ".hardkas", "locks");
|
|
703
|
+
const lockPath = path5.join(lockDir, `${name}.lock`);
|
|
704
|
+
if (!fs3.existsSync(lockPath)) return { cleared: false, reason: "Lock not found" };
|
|
401
705
|
let metadata;
|
|
402
706
|
try {
|
|
403
|
-
metadata = JSON.parse(
|
|
707
|
+
metadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
404
708
|
} catch (e) {
|
|
405
709
|
if (options.force) {
|
|
406
|
-
|
|
710
|
+
fs3.unlinkSync(lockPath);
|
|
407
711
|
return { cleared: true };
|
|
408
712
|
}
|
|
409
713
|
return { cleared: false, reason: "Corrupt metadata (use --force to clear)" };
|
|
@@ -416,10 +720,417 @@ function clearLock(rootDir, name, options = {}) {
|
|
|
416
720
|
} else if (!options.force) {
|
|
417
721
|
return { cleared: false, reason: "Lock is potentially active. Use --force or --if-dead." };
|
|
418
722
|
}
|
|
419
|
-
|
|
723
|
+
fs3.unlinkSync(lockPath);
|
|
420
724
|
return { cleared: true };
|
|
421
725
|
}
|
|
422
726
|
|
|
727
|
+
// src/replay.ts
|
|
728
|
+
function diffReplays(replayA, replayB) {
|
|
729
|
+
const diff = {
|
|
730
|
+
schema: "hardkas.replayDiff.v1",
|
|
731
|
+
structural: {
|
|
732
|
+
missingArtifacts: [],
|
|
733
|
+
excludedArtifacts: [],
|
|
734
|
+
missingProjections: []
|
|
735
|
+
},
|
|
736
|
+
deterministic: {
|
|
737
|
+
stateRootDiverged: false,
|
|
738
|
+
lineageDiverged: false,
|
|
739
|
+
graphDiverged: false,
|
|
740
|
+
differences: []
|
|
741
|
+
},
|
|
742
|
+
observational: {
|
|
743
|
+
timestampShifts: [],
|
|
744
|
+
eventOrderingShifts: [],
|
|
745
|
+
metadataDrift: []
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
if (replayA.artifacts?.length !== replayB.artifacts?.length) {
|
|
749
|
+
diff.structural.missingArtifacts.push("artifact_count_mismatch");
|
|
750
|
+
}
|
|
751
|
+
const stateA = replayA.stateRoot || replayA.postStateHash;
|
|
752
|
+
const stateB = replayB.stateRoot || replayB.postStateHash;
|
|
753
|
+
if (stateA !== stateB) {
|
|
754
|
+
diff.deterministic.stateRootDiverged = true;
|
|
755
|
+
diff.deterministic.differences.push({
|
|
756
|
+
path: "stateRoot",
|
|
757
|
+
a: stateA,
|
|
758
|
+
b: stateB
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
if (replayA.amountSompi !== void 0 && replayB.amountSompi !== void 0 && replayA.amountSompi !== replayB.amountSompi) {
|
|
762
|
+
diff.deterministic.differences.push({
|
|
763
|
+
path: "amountSompi",
|
|
764
|
+
a: replayA.amountSompi,
|
|
765
|
+
b: replayB.amountSompi
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
const tsA = replayA.timestamp || replayA.createdAt;
|
|
769
|
+
const tsB = replayB.timestamp || replayB.createdAt;
|
|
770
|
+
if (tsA && tsB) {
|
|
771
|
+
const timeA = new Date(tsA).getTime();
|
|
772
|
+
const timeB = new Date(tsB).getTime();
|
|
773
|
+
if (timeA !== timeB) {
|
|
774
|
+
diff.observational.timestampShifts.push({ path: "timestamp", shiftMs: Math.abs(timeA - timeB) });
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return diff;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/snapshot.ts
|
|
781
|
+
import fs4 from "fs/promises";
|
|
782
|
+
import path6 from "path";
|
|
783
|
+
async function createSnapshot(options) {
|
|
784
|
+
const { hardkasDir, outputDir, deterministicScope = "local-only" } = options;
|
|
785
|
+
await fs4.mkdir(outputDir, { recursive: true });
|
|
786
|
+
await fs4.mkdir(path6.join(outputDir, "artifacts"), { recursive: true });
|
|
787
|
+
await fs4.mkdir(path6.join(outputDir, "projections"), { recursive: true });
|
|
788
|
+
await fs4.mkdir(path6.join(outputDir, "events"), { recursive: true });
|
|
789
|
+
await fs4.mkdir(path6.join(outputDir, "replay"), { recursive: true });
|
|
790
|
+
await fs4.mkdir(path6.join(outputDir, "metadata"), { recursive: true });
|
|
791
|
+
let included = 0;
|
|
792
|
+
let excluded = 0;
|
|
793
|
+
let corrupted = 0;
|
|
794
|
+
const artifactsDir = path6.join(hardkasDir, "artifacts");
|
|
795
|
+
try {
|
|
796
|
+
const list = await fs4.readdir(artifactsDir);
|
|
797
|
+
for (const f of list) {
|
|
798
|
+
if (f.endsWith(".json")) {
|
|
799
|
+
const src = path6.join(artifactsDir, f);
|
|
800
|
+
const dest = path6.join(outputDir, "artifacts", f);
|
|
801
|
+
try {
|
|
802
|
+
const content = await fs4.readFile(src, "utf-8");
|
|
803
|
+
const parsed = JSON.parse(content);
|
|
804
|
+
if (parsed.schema && parsed.schema.startsWith("hardkas.")) {
|
|
805
|
+
await fs4.copyFile(src, dest);
|
|
806
|
+
included++;
|
|
807
|
+
} else {
|
|
808
|
+
excluded++;
|
|
809
|
+
}
|
|
810
|
+
} catch {
|
|
811
|
+
corrupted++;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
} catch {
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const eventsLog = path6.join(hardkasDir, "events.jsonl");
|
|
819
|
+
await fs4.copyFile(eventsLog, path6.join(outputDir, "events", "events.jsonl"));
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const dbPath = path6.join(hardkasDir, "store.db");
|
|
824
|
+
await fs4.copyFile(dbPath, path6.join(outputDir, "projections", "store.db"));
|
|
825
|
+
} catch {
|
|
826
|
+
}
|
|
827
|
+
const manifest = {
|
|
828
|
+
snapshotVersion: 1,
|
|
829
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
830
|
+
hardkasVersion: "0.6.1-alpha",
|
|
831
|
+
stateAuthority: "filesystem",
|
|
832
|
+
projectionAuthority: "sqlite",
|
|
833
|
+
deterministicScope,
|
|
834
|
+
consensusValidated: deterministicScope === "consensus-validated",
|
|
835
|
+
includedArtifacts: included,
|
|
836
|
+
excludedArtifacts: excluded,
|
|
837
|
+
corruptedArtifacts: corrupted
|
|
838
|
+
};
|
|
839
|
+
await fs4.writeFile(
|
|
840
|
+
path6.join(outputDir, "manifest.json"),
|
|
841
|
+
JSON.stringify(manifest, null, 2),
|
|
842
|
+
"utf-8"
|
|
843
|
+
);
|
|
844
|
+
return manifest;
|
|
845
|
+
}
|
|
846
|
+
async function readSnapshotManifest(snapshotDir) {
|
|
847
|
+
const manifestPath = path6.join(snapshotDir, "manifest.json");
|
|
848
|
+
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
849
|
+
return JSON.parse(content);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// src/deterministic.ts
|
|
853
|
+
function deterministicCompare(a, b) {
|
|
854
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/retention.ts
|
|
858
|
+
import fs5 from "fs";
|
|
859
|
+
import path7 from "path";
|
|
860
|
+
var TelemetryRotator = class {
|
|
861
|
+
static DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
862
|
+
// 10MB
|
|
863
|
+
/**
|
|
864
|
+
* Rotates the telemetry stream if it exceeds the maximum size.
|
|
865
|
+
* This is a safe operation that renames the active file to an archive directory.
|
|
866
|
+
*/
|
|
867
|
+
static rotateIfNeeded(rootDir, maxSizeBytes = this.DEFAULT_MAX_SIZE_BYTES) {
|
|
868
|
+
const telemetryDir = path7.join(rootDir, ".hardkas", "telemetry");
|
|
869
|
+
const activeFile = path7.join(telemetryDir, "telemetry.jsonl");
|
|
870
|
+
if (!fs5.existsSync(activeFile)) {
|
|
871
|
+
return { rotated: false, reason: "File does not exist" };
|
|
872
|
+
}
|
|
873
|
+
const stats = fs5.statSync(activeFile);
|
|
874
|
+
if (stats.size < maxSizeBytes) {
|
|
875
|
+
return { rotated: false, reason: `File size (${stats.size}) is below threshold (${maxSizeBytes})` };
|
|
876
|
+
}
|
|
877
|
+
return this.forceRotate(rootDir);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Forces a rotation regardless of file size.
|
|
881
|
+
*/
|
|
882
|
+
static forceRotate(rootDir) {
|
|
883
|
+
const telemetryDir = path7.join(rootDir, ".hardkas", "telemetry");
|
|
884
|
+
const activeFile = path7.join(telemetryDir, "telemetry.jsonl");
|
|
885
|
+
if (!fs5.existsSync(activeFile)) {
|
|
886
|
+
return { rotated: false, reason: "File does not exist" };
|
|
887
|
+
}
|
|
888
|
+
const archiveDir = path7.join(telemetryDir, "archive");
|
|
889
|
+
if (!fs5.existsSync(archiveDir)) {
|
|
890
|
+
fs5.mkdirSync(archiveDir, { recursive: true });
|
|
891
|
+
}
|
|
892
|
+
const stats = fs5.statSync(activeFile);
|
|
893
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
894
|
+
const archiveFile = path7.join(archiveDir, `telemetry-${timestamp}.jsonl`);
|
|
895
|
+
try {
|
|
896
|
+
fs5.renameSync(activeFile, archiveFile);
|
|
897
|
+
return {
|
|
898
|
+
rotated: true,
|
|
899
|
+
archivePath: archiveFile,
|
|
900
|
+
bytesRotated: stats.size
|
|
901
|
+
};
|
|
902
|
+
} catch (err) {
|
|
903
|
+
return { rotated: false, reason: `Rename failed: ${err.message}` };
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Lists all archived telemetry segments.
|
|
908
|
+
*/
|
|
909
|
+
static listArchivedSegments(rootDir) {
|
|
910
|
+
const archiveDir = path7.join(rootDir, ".hardkas", "telemetry", "archive");
|
|
911
|
+
if (!fs5.existsSync(archiveDir)) return [];
|
|
912
|
+
return fs5.readdirSync(archiveDir).filter((f) => f.startsWith("telemetry-") && f.endsWith(".jsonl")).sort();
|
|
913
|
+
}
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
// src/runtime-context.ts
|
|
917
|
+
var systemRuntimeContext = {
|
|
918
|
+
clock: {
|
|
919
|
+
now: () => Date.now()
|
|
920
|
+
},
|
|
921
|
+
random: {
|
|
922
|
+
next: () => Math.random()
|
|
923
|
+
},
|
|
924
|
+
ids: {
|
|
925
|
+
execution: () => `exec_${Date.now().toString(36)}`,
|
|
926
|
+
workflow: () => `wf_${Date.now().toString(36)}`
|
|
927
|
+
},
|
|
928
|
+
telemetry: globalTelemetry
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
// src/semantics/status.ts
|
|
932
|
+
var LEGAL_TRANSITIONS = {
|
|
933
|
+
UNKNOWN: ["PROJECTED", "QUARANTINED", "CORRUPTED"],
|
|
934
|
+
PROJECTED: ["VERIFIED", "CORRUPTED"],
|
|
935
|
+
VERIFIED: ["STALE", "REPLAY_VERIFIED", "CORRUPTED"],
|
|
936
|
+
STALE: ["VERIFIED", "REPLAY_VERIFIED", "CORRUPTED"],
|
|
937
|
+
REPLAY_VERIFIED: ["STALE", "CORRUPTED"],
|
|
938
|
+
CORRUPTED: ["QUARANTINED"],
|
|
939
|
+
QUARANTINED: []
|
|
940
|
+
// Terminal state
|
|
941
|
+
};
|
|
942
|
+
function validateStatusTransition(from, to) {
|
|
943
|
+
if (from === to) return;
|
|
944
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
945
|
+
if (!allowed.includes(to)) {
|
|
946
|
+
throw new Error(
|
|
947
|
+
`[CRITICAL SEMANTIC ERROR] Illegal artifact status transition attempted: ${from} -> ${to}. This is a violation of the semantic artifact status lattice.`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/semantics/api.ts
|
|
953
|
+
function resolveCanonicalArtifact(params) {
|
|
954
|
+
if (!params.artifactId && !params.lineageId && !params.semanticHash) {
|
|
955
|
+
throw new Error(
|
|
956
|
+
`[CRITICAL SEMANTIC ERROR] Implicit resolution forbidden. You must pin resolution by providing an explicit artifactId, lineageId, or semanticHash.`
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
return params.artifactId || params.semanticHash || params.lineageId || "";
|
|
960
|
+
}
|
|
961
|
+
function verifyArtifactIntegrity(identity, computedHash) {
|
|
962
|
+
if (identity.semanticHash !== computedHash) {
|
|
963
|
+
throw new Error(
|
|
964
|
+
`[CRITICAL SEMANTIC ERROR] Integrity mismatch for artifact ${identity.artifactId}: expected hash ${identity.semanticHash}, got ${computedHash}`
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function verifyReplay(identity, replayCtx) {
|
|
969
|
+
if (identity.semanticHash !== replayCtx.semanticHash) {
|
|
970
|
+
throw new Error(`[CRITICAL SEMANTIC ERROR] Replay semantic hash divergence: expected ${identity.semanticHash}, got ${replayCtx.semanticHash}`);
|
|
971
|
+
}
|
|
972
|
+
validateStatusTransition(identity.status, "REPLAY_VERIFIED");
|
|
973
|
+
return "REPLAY_VERIFIED";
|
|
974
|
+
}
|
|
975
|
+
function verifyProjectionFreshness(identity, currentLineageHead) {
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
function classifyArtifactStatus(identity, isReadable, isCorrupted) {
|
|
979
|
+
if (isCorrupted) return "CORRUPTED";
|
|
980
|
+
if (!isReadable) return "UNKNOWN";
|
|
981
|
+
if (identity.status === "UNKNOWN") return "PROJECTED";
|
|
982
|
+
return identity.status;
|
|
983
|
+
}
|
|
984
|
+
function resolveLineage(artifactId) {
|
|
985
|
+
return [artifactId];
|
|
986
|
+
}
|
|
987
|
+
function verifyCapabilityBoundary(identity, capability) {
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/semantics/migration.ts
|
|
991
|
+
function verifyMigrationIntegrity(preMigration, postMigration) {
|
|
992
|
+
if (preMigration.artifactId !== postMigration.artifactId) {
|
|
993
|
+
throw new Error(`[CRITICAL SEMANTIC ERROR] Migration unexpectedly altered canonical artifact ID: ${preMigration.artifactId} -> ${postMigration.artifactId}`);
|
|
994
|
+
}
|
|
995
|
+
if (postMigration.schemaVersion < preMigration.schemaVersion) {
|
|
996
|
+
throw new Error(`[CRITICAL SEMANTIC ERROR] Invalid migration: schema downgraded from ${preMigration.schemaVersion} to ${postMigration.schemaVersion}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
function migrateArtifact(identity, targetVersion) {
|
|
1000
|
+
if (identity.schemaVersion === targetVersion) {
|
|
1001
|
+
return { migratedIdentity: identity, success: true };
|
|
1002
|
+
}
|
|
1003
|
+
return {
|
|
1004
|
+
migratedIdentity: identity,
|
|
1005
|
+
success: false,
|
|
1006
|
+
error: `No migration path from ${identity.schemaVersion} to ${targetVersion}`
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
function comparePrePostMigrationLineage(preLineageId, postLineageId) {
|
|
1010
|
+
if (preLineageId !== postLineageId) {
|
|
1011
|
+
throw new Error(`[CRITICAL SEMANTIC ERROR] Lineage broken across schema migration: ${preLineageId} -> ${postLineageId}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// src/semantics/drift.ts
|
|
1016
|
+
function detectSemanticDrift(dashboardView, queryStoreView, replayView, filesystemView) {
|
|
1017
|
+
const views = {
|
|
1018
|
+
Dashboard: dashboardView,
|
|
1019
|
+
QueryStore: queryStoreView,
|
|
1020
|
+
Replay: replayView,
|
|
1021
|
+
Filesystem: filesystemView
|
|
1022
|
+
};
|
|
1023
|
+
let referenceView = filesystemView;
|
|
1024
|
+
for (const [subsystem, view] of Object.entries(views)) {
|
|
1025
|
+
if (view.semanticHash !== referenceView.semanticHash) {
|
|
1026
|
+
return {
|
|
1027
|
+
hasDrift: true,
|
|
1028
|
+
conflictingSubsystem: subsystem,
|
|
1029
|
+
exactReplayCommand: `hardkas verify-replay --artifact ${referenceView.artifactId}`,
|
|
1030
|
+
severity: "CRITICAL",
|
|
1031
|
+
details: `Hash mismatch: ${subsystem} (${view.semanticHash}) vs Reference (${referenceView.semanticHash})`
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
if (view.status !== referenceView.status) {
|
|
1035
|
+
if (subsystem === "Dashboard" && view.status === "VERIFIED" && replayView.status === "STALE") {
|
|
1036
|
+
return {
|
|
1037
|
+
hasDrift: true,
|
|
1038
|
+
conflictingSubsystem: subsystem,
|
|
1039
|
+
exactReplayCommand: `hardkas verify-replay --artifact ${referenceView.artifactId}`,
|
|
1040
|
+
severity: "CRITICAL",
|
|
1041
|
+
details: `Dashboard claims VERIFIED but Replay claims STALE.`
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
return { hasDrift: false, severity: "NONE" };
|
|
1047
|
+
}
|
|
1048
|
+
function assertNoSemanticDrift(dashboardView, queryStoreView, replayView, filesystemView) {
|
|
1049
|
+
const report = detectSemanticDrift(dashboardView, queryStoreView, replayView, filesystemView);
|
|
1050
|
+
if (report.hasDrift) {
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
`[CRITICAL SEMANTIC DRIFT] Subsystem disagreement detected.
|
|
1053
|
+
Conflicting Subsystem: ${report.conflictingSubsystem}
|
|
1054
|
+
Details: ${report.details}
|
|
1055
|
+
Resolution Command: ${report.exactReplayCommand}`
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// src/migrations.ts
|
|
1061
|
+
import fs6 from "fs";
|
|
1062
|
+
import path8 from "path";
|
|
1063
|
+
var CURRENT_RUNTIME_VERSION = "0.6.1-alpha";
|
|
1064
|
+
var MIN_SUPPORTED_VERSION = "0.5.0-alpha";
|
|
1065
|
+
var MigrationManager = class {
|
|
1066
|
+
static checkVersion(rootDir) {
|
|
1067
|
+
const versionFile = path8.join(rootDir, ".hardkas", "version.json");
|
|
1068
|
+
if (!fs6.existsSync(versionFile)) {
|
|
1069
|
+
this.writeVersion(rootDir, CURRENT_RUNTIME_VERSION);
|
|
1070
|
+
return { needsMigration: false, canDowngrade: true, currentVersion: CURRENT_RUNTIME_VERSION };
|
|
1071
|
+
}
|
|
1072
|
+
try {
|
|
1073
|
+
const data = JSON.parse(fs6.readFileSync(versionFile, "utf-8"));
|
|
1074
|
+
const wsVersion = data.runtimeVersion || "0.0.0";
|
|
1075
|
+
if (wsVersion === CURRENT_RUNTIME_VERSION) {
|
|
1076
|
+
return { needsMigration: false, canDowngrade: true, currentVersion: wsVersion };
|
|
1077
|
+
}
|
|
1078
|
+
if (this.compareSemver(wsVersion, CURRENT_RUNTIME_VERSION) > 0) {
|
|
1079
|
+
return { needsMigration: false, canDowngrade: false, currentVersion: wsVersion };
|
|
1080
|
+
}
|
|
1081
|
+
if (this.compareSemver(wsVersion, MIN_SUPPORTED_VERSION) < 0) {
|
|
1082
|
+
throw new HardkasError("MIGRATION_UNSUPPORTED", `Workspace version ${wsVersion} is too old to migrate to ${CURRENT_RUNTIME_VERSION}`);
|
|
1083
|
+
}
|
|
1084
|
+
return { needsMigration: true, canDowngrade: true, currentVersion: wsVersion };
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
if (err instanceof HardkasError) throw err;
|
|
1087
|
+
throw new HardkasError("MIGRATION_ERROR", `Failed to parse version.json: ${err.message}`);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
static migrate(rootDir, dryRun = false) {
|
|
1091
|
+
const status = this.checkVersion(rootDir);
|
|
1092
|
+
if (!status.canDowngrade) {
|
|
1093
|
+
throw new HardkasError("DOWNGRADE_REFUSED", `Cannot safely downgrade from workspace version ${status.currentVersion} to runtime version ${CURRENT_RUNTIME_VERSION}.`);
|
|
1094
|
+
}
|
|
1095
|
+
if (!status.needsMigration) return;
|
|
1096
|
+
if (dryRun) {
|
|
1097
|
+
console.log(`[DRY-RUN] Would migrate workspace from ${status.currentVersion} to ${CURRENT_RUNTIME_VERSION}`);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
this.backupWorkspace(rootDir);
|
|
1101
|
+
try {
|
|
1102
|
+
this.writeVersion(rootDir, CURRENT_RUNTIME_VERSION);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
getTelemetry().logAnomaly("EXTERNAL_MUTATION", "critical", "projection", `Migration failed: ${err.message}`);
|
|
1105
|
+
throw new HardkasError("MIGRATION_FAILED", `Migration failed, workspace might be corrupted: ${err.message}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
static writeVersion(rootDir, version) {
|
|
1109
|
+
const versionFile = path8.join(rootDir, ".hardkas", "version.json");
|
|
1110
|
+
if (!fs6.existsSync(path8.dirname(versionFile))) {
|
|
1111
|
+
fs6.mkdirSync(path8.dirname(versionFile), { recursive: true });
|
|
1112
|
+
}
|
|
1113
|
+
fs6.writeFileSync(versionFile, JSON.stringify({ runtimeVersion: version }, null, 2));
|
|
1114
|
+
}
|
|
1115
|
+
static backupWorkspace(rootDir) {
|
|
1116
|
+
const hardkasDir = path8.join(rootDir, ".hardkas");
|
|
1117
|
+
const backupDir = path8.join(rootDir, `.hardkas-backup-${Date.now()}`);
|
|
1118
|
+
if (fs6.existsSync(hardkasDir)) {
|
|
1119
|
+
fs6.cpSync(hardkasDir, backupDir, { recursive: true });
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
static compareSemver(v1, v2) {
|
|
1123
|
+
const parse = (v) => v.replace("-alpha", "").split(".").map(Number);
|
|
1124
|
+
const p1 = parse(v1);
|
|
1125
|
+
const p2 = parse(v2);
|
|
1126
|
+
for (let i = 0; i < 3; i++) {
|
|
1127
|
+
if ((p1[i] || 0) > (p2[i] || 0)) return 1;
|
|
1128
|
+
if ((p1[i] || 0) < (p2[i] || 0)) return -1;
|
|
1129
|
+
}
|
|
1130
|
+
return 0;
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
|
|
423
1134
|
// src/index.ts
|
|
424
1135
|
var SOMPI_PER_KAS = 100000000n;
|
|
425
1136
|
var kaspaNetworkIdSchema = z.enum([
|
|
@@ -429,7 +1140,8 @@ var kaspaNetworkIdSchema = z.enum([
|
|
|
429
1140
|
"testnet-12",
|
|
430
1141
|
"simnet",
|
|
431
1142
|
"simnet-1",
|
|
432
|
-
"devnet"
|
|
1143
|
+
"devnet",
|
|
1144
|
+
"simulated"
|
|
433
1145
|
]);
|
|
434
1146
|
var executionModeSchema = z.enum([
|
|
435
1147
|
"simulated",
|
|
@@ -441,7 +1153,8 @@ var artifactTypeSchema = z.enum([
|
|
|
441
1153
|
"signedTx",
|
|
442
1154
|
"txReceipt",
|
|
443
1155
|
"txTrace",
|
|
444
|
-
"snapshot"
|
|
1156
|
+
"snapshot",
|
|
1157
|
+
"workflow.v1"
|
|
445
1158
|
]);
|
|
446
1159
|
var NetworkIdSchema = kaspaNetworkIdSchema;
|
|
447
1160
|
var ExecutionModeSchema = executionModeSchema;
|
|
@@ -510,13 +1223,20 @@ function formatSompi(amountSompi) {
|
|
|
510
1223
|
return `${sign}${whole}.${fractional.toString().padStart(8, "0")} KAS`;
|
|
511
1224
|
}
|
|
512
1225
|
export {
|
|
1226
|
+
AppendCoordinator,
|
|
513
1227
|
ArtifactTypeSchema,
|
|
1228
|
+
CURRENT_RUNTIME_VERSION,
|
|
1229
|
+
EnvironmentTelemetry,
|
|
514
1230
|
ExecutionModeSchema,
|
|
515
1231
|
HardkasError,
|
|
516
1232
|
InvariantViolationError,
|
|
517
1233
|
LOCK_ORDER,
|
|
1234
|
+
MIN_SUPPORTED_VERSION,
|
|
1235
|
+
MigrationManager,
|
|
518
1236
|
NetworkIdSchema,
|
|
519
1237
|
SOMPI_PER_KAS,
|
|
1238
|
+
TelemetryManager,
|
|
1239
|
+
TelemetryRotator,
|
|
520
1240
|
acquireLock,
|
|
521
1241
|
artifactTypeSchema,
|
|
522
1242
|
asArtifactId,
|
|
@@ -531,21 +1251,43 @@ export {
|
|
|
531
1251
|
asRpcEndpointId,
|
|
532
1252
|
asTxId,
|
|
533
1253
|
asWorkflowId,
|
|
1254
|
+
assertNoSemanticDrift,
|
|
1255
|
+
attachLedgerAppender,
|
|
1256
|
+
classifyArtifactStatus,
|
|
534
1257
|
clearLock,
|
|
1258
|
+
comparePrePostMigrationLineage,
|
|
535
1259
|
coreEvents,
|
|
536
1260
|
createEventEnvelope,
|
|
1261
|
+
createSnapshot,
|
|
1262
|
+
detectSemanticDrift,
|
|
1263
|
+
deterministicCompare,
|
|
1264
|
+
diffReplays,
|
|
537
1265
|
executionModeSchema,
|
|
538
1266
|
formatCorruptionIssue,
|
|
539
1267
|
formatSompi,
|
|
1268
|
+
getTelemetry,
|
|
1269
|
+
globalTelemetry,
|
|
540
1270
|
hardkasConfigSchema,
|
|
541
1271
|
isProcessAlive,
|
|
542
1272
|
kaspaNetworkIdSchema,
|
|
543
1273
|
listLocks,
|
|
544
1274
|
maskSecrets,
|
|
1275
|
+
migrateArtifact,
|
|
545
1276
|
parseHardkasConfig,
|
|
546
1277
|
parseKasToSompi,
|
|
1278
|
+
readSnapshotManifest,
|
|
547
1279
|
redactSecret,
|
|
1280
|
+
resolveCanonicalArtifact,
|
|
1281
|
+
resolveLineage,
|
|
1282
|
+
systemRuntimeContext,
|
|
1283
|
+
telemetryContextStorage,
|
|
548
1284
|
validateEventEnvelope,
|
|
1285
|
+
validateStatusTransition,
|
|
1286
|
+
verifyArtifactIntegrity,
|
|
1287
|
+
verifyCapabilityBoundary,
|
|
1288
|
+
verifyMigrationIntegrity,
|
|
1289
|
+
verifyProjectionFreshness,
|
|
1290
|
+
verifyReplay,
|
|
549
1291
|
withLock,
|
|
550
1292
|
withLocks,
|
|
551
1293
|
writeFileAtomic,
|