@hardkas/core 0.6.0-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 CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
3
 
3
4
  /**
4
5
  * Lightweight branded/nominal types for domain-critical strings/numbers.
@@ -328,6 +329,11 @@ type UnknownEventPayload = {
328
329
  readonly type: "unknown";
329
330
  readonly data: Record<string, unknown>;
330
331
  };
332
+ /**
333
+ * Attaches the canonical Event Ledger appender to the core event bus.
334
+ * This guarantees that all formal EventEnvelopes are persisted to events.jsonl.
335
+ */
336
+ declare function attachLedgerAppender(workspaceRoot: string): () => void;
331
337
 
332
338
  /**
333
339
  * @deprecated Use Brand from domain-types.js instead.
@@ -458,6 +464,7 @@ interface AcquireLockArgs {
458
464
  declare const LOCK_ORDER: string[];
459
465
  /**
460
466
  * Acquires a named lock for the workspace.
467
+ * Supports automatic stale lock recovery when the holding process is dead.
461
468
  */
462
469
  declare function acquireLock(args: AcquireLockArgs): Promise<LockHandle>;
463
470
  /**
@@ -553,6 +560,85 @@ declare function readSnapshotManifest(snapshotDir: string): Promise<SnapshotMani
553
560
  */
554
561
  declare function deterministicCompare(a: string, b: string): number;
555
562
 
563
+ type TelemetrySubsystem = "lock" | "fs" | "replay" | "normalization" | "query-store" | "lineage" | "projection" | "unknown";
564
+ type AnomalyType = "LOCK_CONTENTION" | "STALE_LOCK_RECOVERY" | "FS_RETRY" | "NORMALIZATION_COLLISION" | "REPLAY_RECONCILIATION" | "EXTERNAL_MUTATION" | "PATH_TRAVERSAL_ATTEMPT" | "ORPHAN_PROJECTION_RECOVERY";
565
+ type Severity = "low" | "medium" | "high" | "critical";
566
+ interface AnomalyEvent {
567
+ timestamp: string;
568
+ seed?: number | undefined;
569
+ caseId?: string | undefined;
570
+ bucket?: string | undefined;
571
+ anomalyType: AnomalyType;
572
+ severity: Severity;
573
+ subsystem: TelemetrySubsystem;
574
+ details: string;
575
+ sandbox?: string | undefined;
576
+ }
577
+
578
+ declare class TelemetryManager {
579
+ private rootDir;
580
+ private currentContext;
581
+ private preservedSandboxes;
582
+ constructor(rootDir?: string);
583
+ init(rootDir: string): void;
584
+ setContext(context: {
585
+ seed?: number;
586
+ caseId?: string;
587
+ bucket?: string;
588
+ }): void;
589
+ clearContext(): void;
590
+ getContext(): {
591
+ seed?: number;
592
+ caseId?: string;
593
+ bucket?: string;
594
+ };
595
+ logAnomaly(anomalyType: AnomalyType, severity: Severity, subsystem: TelemetrySubsystem, details: string, sandboxOverride?: string): void;
596
+ shouldPreserveSandbox(sandboxDir: string): boolean;
597
+ }
598
+ declare const telemetryContextStorage: AsyncLocalStorage<TelemetryManager>;
599
+ declare const globalTelemetry: TelemetryManager;
600
+ declare function getTelemetry(): TelemetryManager;
601
+ declare class TelemetryProxy {
602
+ logAnomaly(anomalyType: AnomalyType, severity: Severity, subsystem: TelemetrySubsystem, details: string, sandboxOverride?: string): void;
603
+ init(rootDir: string): void;
604
+ setContext(context: {
605
+ seed?: number;
606
+ caseId?: string;
607
+ bucket?: string;
608
+ }): void;
609
+ clearContext(): void;
610
+ getContext(): {
611
+ seed?: number;
612
+ caseId?: string;
613
+ bucket?: string;
614
+ };
615
+ shouldPreserveSandbox(sandboxDir: string): boolean;
616
+ }
617
+ declare const EnvironmentTelemetry: TelemetryProxy;
618
+
619
+ interface RotationResult {
620
+ rotated: boolean;
621
+ archivePath?: string;
622
+ bytesRotated?: number;
623
+ reason?: string;
624
+ }
625
+ declare class TelemetryRotator {
626
+ private static readonly DEFAULT_MAX_SIZE_BYTES;
627
+ /**
628
+ * Rotates the telemetry stream if it exceeds the maximum size.
629
+ * This is a safe operation that renames the active file to an archive directory.
630
+ */
631
+ static rotateIfNeeded(rootDir: string, maxSizeBytes?: number): RotationResult;
632
+ /**
633
+ * Forces a rotation regardless of file size.
634
+ */
635
+ static forceRotate(rootDir: string): RotationResult;
636
+ /**
637
+ * Lists all archived telemetry segments.
638
+ */
639
+ static listArchivedSegments(rootDir: string): string[];
640
+ }
641
+
556
642
  interface DeterministicClock {
557
643
  now(): number;
558
644
  }
@@ -567,6 +653,7 @@ interface RuntimeContext {
567
653
  clock: DeterministicClock;
568
654
  random: DeterministicRandom;
569
655
  ids: IdProvider;
656
+ telemetry: TelemetryManager;
570
657
  }
571
658
  /**
572
659
  * A default system runtime context (for non-deterministic contexts like dev server or CLI entry points)
@@ -574,6 +661,139 @@ interface RuntimeContext {
574
661
  */
575
662
  declare const systemRuntimeContext: RuntimeContext;
576
663
 
664
+ /**
665
+ * Formal Artifact Status Lattice
666
+ *
667
+ * UNKNOWN: Unreadable, ambiguous, partially classified, migration-pending states.
668
+ * PROJECTED: An artifact read from disk / state whose truth has not yet been verified.
669
+ * STALE: An artifact whose dependencies/lineage has drifted since it was verified.
670
+ * VERIFIED: Integrity, signature, and internal capability constraints are verified.
671
+ * REPLAY_VERIFIED: Full lineage and determinism verified via an active replay.
672
+ * CORRUPTED: Irreparable semantic or cryptographic corruption detected.
673
+ * QUARANTINED: Corrupted or malicious artifact safely isolated from runtime.
674
+ */
675
+ type ArtifactStatus = "UNKNOWN" | "PROJECTED" | "STALE" | "VERIFIED" | "REPLAY_VERIFIED" | "CORRUPTED" | "QUARANTINED";
676
+ /**
677
+ * Baseline schema version is 1.
678
+ */
679
+ type SchemaVersion = 1;
680
+ interface SemanticIdentity {
681
+ /** The unique artifact ID */
682
+ artifactId: string;
683
+ /** The semantic hash of the artifact content */
684
+ semanticHash: string;
685
+ /** Formal schema version */
686
+ schemaVersion: SchemaVersion;
687
+ /** Current status in the lattice */
688
+ status: ArtifactStatus;
689
+ }
690
+
691
+ /**
692
+ * Throws a loud runtime error if a transition is invalid.
693
+ * Invariant: `artifact_status_transitions_are_semantically_valid`
694
+ */
695
+ declare function validateStatusTransition(from: ArtifactStatus, to: ArtifactStatus): void;
696
+
697
+ interface ReplayContext {
698
+ semanticHash: string;
699
+ lineageId: string;
700
+ replayHash: string;
701
+ }
702
+ /**
703
+ * Resolves a canonical artifact.
704
+ * Implicit 'latest' is STRICTLY FORBIDDEN.
705
+ * You must provide an explicit artifactId, lineageId, or semanticHash.
706
+ * Invariant: `canonical_resolution_never_depends_on_implicit_latest`
707
+ */
708
+ declare function resolveCanonicalArtifact(params: {
709
+ artifactId?: string;
710
+ lineageId?: string;
711
+ semanticHash?: string;
712
+ }): string;
713
+ /**
714
+ * Verifies that the artifact content integrity is valid.
715
+ */
716
+ declare function verifyArtifactIntegrity(identity: SemanticIdentity, computedHash: string): void;
717
+ /**
718
+ * Verifies the artifact via active replay, isolating it from ambient state.
719
+ * Invariant: `replay_isolated_from_ambient_runtime_state`
720
+ */
721
+ declare function verifyReplay(identity: SemanticIdentity, replayCtx: ReplayContext): ArtifactStatus;
722
+ declare function verifyProjectionFreshness(identity: SemanticIdentity, currentLineageHead: string): boolean;
723
+ declare function classifyArtifactStatus(identity: SemanticIdentity, isReadable: boolean, isCorrupted: boolean): ArtifactStatus;
724
+ declare function resolveLineage(artifactId: string): string[];
725
+ declare function verifyCapabilityBoundary(identity: SemanticIdentity, capability: string): void;
726
+
727
+ interface MigrationResult {
728
+ migratedIdentity: SemanticIdentity;
729
+ success: boolean;
730
+ error?: string;
731
+ }
732
+ /**
733
+ * Validates that an artifact migration preserves identity and lineage semantics.
734
+ * Invariant: `schema_evolution_preserves_semantic_identity`
735
+ */
736
+ declare function verifyMigrationIntegrity(preMigration: SemanticIdentity, postMigration: SemanticIdentity): void;
737
+ /**
738
+ * Handles migrating an artifact to a newer schema version.
739
+ * Currently, only schemaVersion: 1 exists as the baseline.
740
+ */
741
+ declare function migrateArtifact(identity: SemanticIdentity, targetVersion: SchemaVersion): MigrationResult;
742
+ /**
743
+ * Compares lineage before and after migration to ensure continuity.
744
+ */
745
+ declare function comparePrePostMigrationLineage(preLineageId: string, postLineageId: string): void;
746
+
747
+ interface SemanticDriftReport {
748
+ hasDrift: boolean;
749
+ conflictingSubsystem?: string;
750
+ exactReplayCommand?: string;
751
+ severity: "NONE" | "CRITICAL";
752
+ details?: string;
753
+ }
754
+ /**
755
+ * Compares the truth across different subsystems.
756
+ * If multiple subsystems disagree about truth, that is a CRITICAL semantic failure.
757
+ * Invariant: `subsystems_cannot_disagree_about_canonical_truth`
758
+ */
759
+ declare function detectSemanticDrift(dashboardView: SemanticIdentity, queryStoreView: SemanticIdentity, replayView: SemanticIdentity, filesystemView: SemanticIdentity): SemanticDriftReport;
760
+ /**
761
+ * Asserts no semantic drift exists. Fails loudly.
762
+ */
763
+ declare function assertNoSemanticDrift(dashboardView: SemanticIdentity, queryStoreView: SemanticIdentity, replayView: SemanticIdentity, filesystemView: SemanticIdentity): void;
764
+
765
+ declare class AppendCoordinator {
766
+ /**
767
+ * Safely appends a line to a JSONL log under process coordination locks.
768
+ * Performs an immediate fsync to ensure data durability.
769
+ * Also repairs the trailing line if it is corrupted, emitting an anomaly.
770
+ */
771
+ static appendAtomic(filePath: string, line: string, rootDir: string): void;
772
+ /**
773
+ * Scans a JSONL stream for corruption, truncating malformed trailing lines.
774
+ */
775
+ static recoverCorruptedTail(filePath: string): {
776
+ repaired: boolean;
777
+ linesDiscarded: number;
778
+ originalTail: string;
779
+ };
780
+ }
781
+
782
+ declare const CURRENT_RUNTIME_VERSION = "0.6.1-alpha";
783
+ declare const MIN_SUPPORTED_VERSION = "0.5.0-alpha";
784
+ interface MigrationStatus {
785
+ needsMigration: boolean;
786
+ canDowngrade: boolean;
787
+ currentVersion: string;
788
+ }
789
+ declare class MigrationManager {
790
+ static checkVersion(rootDir: string): MigrationStatus;
791
+ static migrate(rootDir: string, dryRun?: boolean): void;
792
+ private static writeVersion;
793
+ private static backupWorkspace;
794
+ private static compareSemver;
795
+ }
796
+
577
797
  declare const SOMPI_PER_KAS = 100000000n;
578
798
  declare const kaspaNetworkIdSchema: z.ZodEnum<["mainnet", "testnet-10", "testnet-11", "testnet-12", "simnet", "simnet-1", "devnet", "simulated"]>;
579
799
  type NetworkId = Brand<z.infer<typeof kaspaNetworkIdSchema>, "NetworkId">;
@@ -664,4 +884,4 @@ declare function parseHardkasConfig(input: unknown): HardkasConfig;
664
884
  declare function parseKasToSompi(input: string): bigint;
665
885
  declare function formatSompi(amountSompi: bigint): string;
666
886
 
667
- export { type AcquireLockArgs, type ArtifactId, type ArtifactType, ArtifactTypeSchema, type Brand, type Branded, type ContentHash, type CoreEvent, type CoreEventListener, type CorrelationId, type CorruptionCode, type CorruptionIssue, type CorruptionSeverity, type CreateSnapshotOptions, type DaaScore, type DeterministicClock, type DeterministicDiff, type DeterministicRandom, type EventDomain, type EventEnvelope, type EventId, type EventKind, type EventPayloadByKind, type EventSequence, type ExecutionMode, ExecutionModeSchema, type HardkasConfig, HardkasError, type IdProvider, type IntegrityStatus, type InvariantDomain, type InvariantSeverity, InvariantViolationError, type KaspaAddress, LOCK_ORDER, type LayeredReplayDiff, type LineageId, type LockHandle, type LockMetadata, type NetworkId, NetworkIdSchema, type RpcEndpointId, type RuntimeContext, type RuntimeNoiseDiff, SOMPI_PER_KAS, type SnapshotManifest, type StampedEvent, type StateProvenance, type StructuralDiff, type TxId, type UnknownEventPayload, type WorkflowId, type WriteFileAtomicOptions, acquireLock, artifactTypeSchema, asArtifactId, asContentHash, asCorrelationId, asDaaScore, asEventId, asEventSequence, asKaspaAddress, asLineageId, asNetworkId, asRpcEndpointId, asTxId, asWorkflowId, clearLock, coreEvents, createEventEnvelope, createSnapshot, deterministicCompare, diffReplays, executionModeSchema, formatCorruptionIssue, formatSompi, hardkasConfigSchema, isProcessAlive, kaspaNetworkIdSchema, listLocks, maskSecrets, parseHardkasConfig, parseKasToSompi, readSnapshotManifest, redactSecret, systemRuntimeContext, validateEventEnvelope, withLock, withLocks, writeFileAtomic, writeFileAtomicSync };
887
+ export { type AcquireLockArgs, type AnomalyEvent, type AnomalyType, AppendCoordinator, type ArtifactId, type ArtifactStatus, type ArtifactType, ArtifactTypeSchema, type Brand, type Branded, CURRENT_RUNTIME_VERSION, type ContentHash, type CoreEvent, type CoreEventListener, type CorrelationId, type CorruptionCode, type CorruptionIssue, type CorruptionSeverity, type CreateSnapshotOptions, type DaaScore, type DeterministicClock, type DeterministicDiff, type DeterministicRandom, EnvironmentTelemetry, type EventDomain, type EventEnvelope, type EventId, type EventKind, type EventPayloadByKind, type EventSequence, type ExecutionMode, ExecutionModeSchema, type HardkasConfig, HardkasError, type IdProvider, type IntegrityStatus, type InvariantDomain, type InvariantSeverity, InvariantViolationError, type KaspaAddress, LOCK_ORDER, type LayeredReplayDiff, type LineageId, type LockHandle, type LockMetadata, MIN_SUPPORTED_VERSION, MigrationManager, type MigrationResult, type MigrationStatus, type NetworkId, NetworkIdSchema, type ReplayContext, type RpcEndpointId, type RuntimeContext, type RuntimeNoiseDiff, SOMPI_PER_KAS, type SchemaVersion, type SemanticDriftReport, type SemanticIdentity, type Severity, type SnapshotManifest, type StampedEvent, type StateProvenance, type StructuralDiff, TelemetryManager, TelemetryRotator, type TelemetrySubsystem, type TxId, type UnknownEventPayload, type WorkflowId, type WriteFileAtomicOptions, acquireLock, artifactTypeSchema, asArtifactId, asContentHash, asCorrelationId, asDaaScore, asEventId, asEventSequence, asKaspaAddress, asLineageId, asNetworkId, asRpcEndpointId, asTxId, asWorkflowId, assertNoSemanticDrift, attachLedgerAppender, classifyArtifactStatus, clearLock, comparePrePostMigrationLineage, coreEvents, createEventEnvelope, createSnapshot, detectSemanticDrift, deterministicCompare, diffReplays, executionModeSchema, formatCorruptionIssue, formatSompi, getTelemetry, globalTelemetry, hardkasConfigSchema, isProcessAlive, kaspaNetworkIdSchema, listLocks, maskSecrets, migrateArtifact, parseHardkasConfig, parseKasToSompi, readSnapshotManifest, redactSecret, resolveCanonicalArtifact, resolveLineage, systemRuntimeContext, telemetryContextStorage, validateEventEnvelope, validateStatusTransition, verifyArtifactIntegrity, verifyCapabilityBoundary, verifyMigrationIntegrity, verifyProjectionFreshness, verifyReplay, withLock, withLocks, writeFileAtomic, writeFileAtomicSync };
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) {
@@ -66,6 +303,25 @@ function validateEventEnvelope(event) {
66
303
  if (typeof event.payload !== "object") return false;
67
304
  return true;
68
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
+ }
69
325
 
70
326
  // src/domain-types.ts
71
327
  var asTxId = (id) => id;
@@ -116,34 +372,35 @@ function redactSecret(value) {
116
372
  }
117
373
 
118
374
  // src/fs.ts
119
- import fs from "fs";
120
- import path from "path";
121
- import crypto2 from "crypto";
375
+ import fs2 from "fs";
376
+ import path4 from "path";
377
+ import crypto3 from "crypto";
122
378
  async function writeFileAtomic(targetPath, data, options = {}) {
123
- const dir = path.dirname(targetPath);
124
- const base = path.basename(targetPath);
125
- const tempPath = path.join(dir, `.tmp.${base}.${crypto2.randomUUID()}`);
379
+ const dir = path4.dirname(targetPath);
380
+ const base = path4.basename(targetPath);
381
+ const tempPath = path4.join(dir, `.tmp.${base}.${crypto3.randomUUID()}`);
126
382
  let fd = null;
127
383
  try {
128
- if (!fs.existsSync(dir)) {
129
- fs.mkdirSync(dir, { recursive: true });
384
+ if (!fs2.existsSync(dir)) {
385
+ fs2.mkdirSync(dir, { recursive: true });
130
386
  }
131
- fd = fs.openSync(tempPath, "w", options.mode);
387
+ fd = fs2.openSync(tempPath, "w", options.mode);
132
388
  const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
133
- fs.writeSync(fd, buffer, 0, buffer.length);
134
- fs.fsyncSync(fd);
135
- fs.closeSync(fd);
389
+ fs2.writeSync(fd, buffer, 0, buffer.length);
390
+ fs2.fsyncSync(fd);
391
+ fs2.closeSync(fd);
136
392
  fd = null;
137
393
  let attempts = 0;
138
394
  const maxAttempts = process.platform === "win32" ? 5 : 1;
139
395
  while (attempts < maxAttempts) {
140
396
  try {
141
- fs.renameSync(tempPath, targetPath);
397
+ fs2.renameSync(tempPath, targetPath);
142
398
  break;
143
399
  } catch (e) {
144
400
  attempts++;
145
401
  if (attempts >= maxAttempts) throw e;
146
402
  if (e.code === "EPERM" || e.code === "EBUSY") {
403
+ EnvironmentTelemetry.logAnomaly("FS_RETRY", "low", "fs", `Retrying rename of ${targetPath} due to ${e.code}`);
147
404
  await new Promise((resolve) => setTimeout(resolve, 10 * attempts));
148
405
  continue;
149
406
  }
@@ -153,11 +410,11 @@ async function writeFileAtomic(targetPath, data, options = {}) {
153
410
  if (options.fsyncParent && process.platform !== "win32") {
154
411
  let dirFd = null;
155
412
  try {
156
- dirFd = fs.openSync(dir, "r");
157
- fs.fsyncSync(dirFd);
413
+ dirFd = fs2.openSync(dir, "r");
414
+ fs2.fsyncSync(dirFd);
158
415
  } catch (e) {
159
416
  } finally {
160
- if (dirFd !== null) fs.closeSync(dirFd);
417
+ if (dirFd !== null) fs2.closeSync(dirFd);
161
418
  }
162
419
  }
163
420
  } catch (err) {
@@ -167,45 +424,46 @@ async function writeFileAtomic(targetPath, data, options = {}) {
167
424
  { cause: err }
168
425
  );
169
426
  } finally {
170
- if (fs.existsSync(tempPath)) {
427
+ if (fs2.existsSync(tempPath)) {
171
428
  try {
172
- fs.unlinkSync(tempPath);
429
+ fs2.unlinkSync(tempPath);
173
430
  } catch (e) {
174
431
  }
175
432
  }
176
433
  if (fd !== null) {
177
434
  try {
178
- fs.closeSync(fd);
435
+ fs2.closeSync(fd);
179
436
  } catch (e) {
180
437
  }
181
438
  }
182
439
  }
183
440
  }
184
441
  function writeFileAtomicSync(targetPath, data, options = {}) {
185
- const dir = path.dirname(targetPath);
186
- const base = path.basename(targetPath);
187
- const tempPath = path.join(dir, `.tmp.${base}.${crypto2.randomUUID()}`);
442
+ const dir = path4.dirname(targetPath);
443
+ const base = path4.basename(targetPath);
444
+ const tempPath = path4.join(dir, `.tmp.${base}.${crypto3.randomUUID()}`);
188
445
  let fd = null;
189
446
  try {
190
- if (!fs.existsSync(dir)) {
191
- fs.mkdirSync(dir, { recursive: true });
447
+ if (!fs2.existsSync(dir)) {
448
+ fs2.mkdirSync(dir, { recursive: true });
192
449
  }
193
- fd = fs.openSync(tempPath, "w", options.mode);
450
+ fd = fs2.openSync(tempPath, "w", options.mode);
194
451
  const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
195
- fs.writeSync(fd, buffer, 0, buffer.length);
196
- fs.fsyncSync(fd);
197
- fs.closeSync(fd);
452
+ fs2.writeSync(fd, buffer, 0, buffer.length);
453
+ fs2.fsyncSync(fd);
454
+ fs2.closeSync(fd);
198
455
  fd = null;
199
456
  let attempts = 0;
200
457
  const maxAttempts = process.platform === "win32" ? 5 : 1;
201
458
  while (attempts < maxAttempts) {
202
459
  try {
203
- fs.renameSync(tempPath, targetPath);
460
+ fs2.renameSync(tempPath, targetPath);
204
461
  break;
205
462
  } catch (e) {
206
463
  attempts++;
207
464
  if (attempts >= maxAttempts) throw e;
208
465
  if (e.code === "EPERM" || e.code === "EBUSY") {
466
+ EnvironmentTelemetry.logAnomaly("FS_RETRY", "low", "fs", `Retrying rename sync of ${targetPath} due to ${e.code}`);
209
467
  continue;
210
468
  }
211
469
  throw e;
@@ -214,11 +472,11 @@ function writeFileAtomicSync(targetPath, data, options = {}) {
214
472
  if (options.fsyncParent && process.platform !== "win32") {
215
473
  let dirFd = null;
216
474
  try {
217
- dirFd = fs.openSync(dir, "r");
218
- fs.fsyncSync(dirFd);
475
+ dirFd = fs2.openSync(dir, "r");
476
+ fs2.fsyncSync(dirFd);
219
477
  } catch (e) {
220
478
  } finally {
221
- if (dirFd !== null) fs.closeSync(dirFd);
479
+ if (dirFd !== null) fs2.closeSync(dirFd);
222
480
  }
223
481
  }
224
482
  } catch (err) {
@@ -228,15 +486,15 @@ function writeFileAtomicSync(targetPath, data, options = {}) {
228
486
  { cause: err }
229
487
  );
230
488
  } finally {
231
- if (fs.existsSync(tempPath)) {
489
+ if (fs2.existsSync(tempPath)) {
232
490
  try {
233
- fs.unlinkSync(tempPath);
491
+ fs2.unlinkSync(tempPath);
234
492
  } catch (e) {
235
493
  }
236
494
  }
237
495
  if (fd !== null) {
238
496
  try {
239
- fs.closeSync(fd);
497
+ fs2.closeSync(fd);
240
498
  } catch (e) {
241
499
  }
242
500
  }
@@ -262,8 +520,8 @@ function formatCorruptionIssue(issue) {
262
520
  }
263
521
 
264
522
  // src/lock.ts
265
- import fs2 from "fs";
266
- import path2 from "path";
523
+ import fs3 from "fs";
524
+ import path5 from "path";
267
525
  import os from "os";
268
526
  var LOCK_ORDER = [
269
527
  "workspace",
@@ -274,13 +532,14 @@ var LOCK_ORDER = [
274
532
  "query-store"
275
533
  ];
276
534
  async function acquireLock(args) {
277
- const lockDir = path2.join(args.rootDir, ".hardkas", "locks");
278
- const lockPath = path2.join(lockDir, `${args.name}.lock`);
535
+ const lockDir = path5.join(args.rootDir, ".hardkas", "locks");
536
+ const lockPath = path5.join(lockDir, `${args.name}.lock`);
279
537
  const timeoutMs = args.timeoutMs ?? 3e4;
280
538
  const pollMs = args.pollMs ?? 250;
281
539
  const start = Date.now();
282
- if (!fs2.existsSync(lockDir)) {
283
- fs2.mkdirSync(lockDir, { recursive: true });
540
+ let staleRecoveryAttempted = false;
541
+ if (!fs3.existsSync(lockDir)) {
542
+ fs3.mkdirSync(lockDir, { recursive: true });
284
543
  }
285
544
  while (true) {
286
545
  try {
@@ -294,18 +553,18 @@ async function acquireLock(args) {
294
553
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
295
554
  expiresAt: null
296
555
  };
297
- const fd = fs2.openSync(lockPath, "wx");
298
- fs2.writeSync(fd, JSON.stringify(metadata, null, 2));
299
- fs2.closeSync(fd);
556
+ const fd = fs3.openSync(lockPath, "wx");
557
+ fs3.writeSync(fd, JSON.stringify(metadata, null, 2));
558
+ fs3.closeSync(fd);
300
559
  return {
301
560
  path: lockPath,
302
561
  metadata,
303
562
  release: async () => {
304
- if (fs2.existsSync(lockPath)) {
563
+ if (fs3.existsSync(lockPath)) {
305
564
  try {
306
- const current = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
565
+ const current = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
307
566
  if (current.pid === process.pid) {
308
- fs2.unlinkSync(lockPath);
567
+ fs3.unlinkSync(lockPath);
309
568
  }
310
569
  } catch (e) {
311
570
  }
@@ -316,12 +575,49 @@ async function acquireLock(args) {
316
575
  if (e.code === "EEXIST") {
317
576
  let existingMetadata = null;
318
577
  try {
319
- existingMetadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
578
+ existingMetadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
320
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
+ }
321
602
  throw new HardkasError("LOCK_METADATA_INVALID", `Lock file at ${lockPath} is corrupted.`, { cause: err });
322
603
  }
323
604
  if (existingMetadata) {
324
- const isAlive = isProcessAlive(existingMetadata.pid);
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
+ }
325
621
  if (!isAlive) {
326
622
  throw new HardkasError(
327
623
  "STALE_LOCK",
@@ -330,6 +626,7 @@ async function acquireLock(args) {
330
626
  );
331
627
  }
332
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);
333
630
  await new Promise((resolve) => setTimeout(resolve, pollMs));
334
631
  continue;
335
632
  }
@@ -375,20 +672,22 @@ function isProcessAlive(pid) {
375
672
  process.kill(pid, 0);
376
673
  return true;
377
674
  } catch (e) {
378
- return e.code !== "ESRCH";
675
+ if (e.code === "EPERM") return true;
676
+ if (e.code === "ESRCH") return false;
677
+ return true;
379
678
  }
380
679
  }
381
680
  function listLocks(rootDir) {
382
- const lockDir = path2.join(rootDir, ".hardkas", "locks");
383
- if (!fs2.existsSync(lockDir)) return [];
384
- const files = fs2.readdirSync(lockDir).filter((f) => f.endsWith(".lock"));
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"));
385
684
  const result = [];
386
685
  for (const file of files) {
387
- const lockPath = path2.join(lockDir, file);
686
+ const lockPath = path5.join(lockDir, file);
388
687
  try {
389
- const metadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
688
+ const metadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
390
689
  result.push({
391
- name: path2.basename(file, ".lock"),
690
+ name: path5.basename(file, ".lock"),
392
691
  metadata,
393
692
  path: lockPath,
394
693
  isAlive: metadata.hostname === os.hostname() ? isProcessAlive(metadata.pid) : true
@@ -400,15 +699,15 @@ function listLocks(rootDir) {
400
699
  return result;
401
700
  }
402
701
  function clearLock(rootDir, name, options = {}) {
403
- const lockDir = path2.join(rootDir, ".hardkas", "locks");
404
- const lockPath = path2.join(lockDir, `${name}.lock`);
405
- if (!fs2.existsSync(lockPath)) return { cleared: false, reason: "Lock not found" };
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" };
406
705
  let metadata;
407
706
  try {
408
- metadata = JSON.parse(fs2.readFileSync(lockPath, "utf-8"));
707
+ metadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
409
708
  } catch (e) {
410
709
  if (options.force) {
411
- fs2.unlinkSync(lockPath);
710
+ fs3.unlinkSync(lockPath);
412
711
  return { cleared: true };
413
712
  }
414
713
  return { cleared: false, reason: "Corrupt metadata (use --force to clear)" };
@@ -421,7 +720,7 @@ function clearLock(rootDir, name, options = {}) {
421
720
  } else if (!options.force) {
422
721
  return { cleared: false, reason: "Lock is potentially active. Use --force or --if-dead." };
423
722
  }
424
- fs2.unlinkSync(lockPath);
723
+ fs3.unlinkSync(lockPath);
425
724
  return { cleared: true };
426
725
  }
427
726
 
@@ -479,31 +778,31 @@ function diffReplays(replayA, replayB) {
479
778
  }
480
779
 
481
780
  // src/snapshot.ts
482
- import fs3 from "fs/promises";
483
- import path3 from "path";
781
+ import fs4 from "fs/promises";
782
+ import path6 from "path";
484
783
  async function createSnapshot(options) {
485
784
  const { hardkasDir, outputDir, deterministicScope = "local-only" } = options;
486
- await fs3.mkdir(outputDir, { recursive: true });
487
- await fs3.mkdir(path3.join(outputDir, "artifacts"), { recursive: true });
488
- await fs3.mkdir(path3.join(outputDir, "projections"), { recursive: true });
489
- await fs3.mkdir(path3.join(outputDir, "events"), { recursive: true });
490
- await fs3.mkdir(path3.join(outputDir, "replay"), { recursive: true });
491
- await fs3.mkdir(path3.join(outputDir, "metadata"), { recursive: true });
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 });
492
791
  let included = 0;
493
792
  let excluded = 0;
494
793
  let corrupted = 0;
495
- const artifactsDir = path3.join(hardkasDir, "artifacts");
794
+ const artifactsDir = path6.join(hardkasDir, "artifacts");
496
795
  try {
497
- const list = await fs3.readdir(artifactsDir);
796
+ const list = await fs4.readdir(artifactsDir);
498
797
  for (const f of list) {
499
798
  if (f.endsWith(".json")) {
500
- const src = path3.join(artifactsDir, f);
501
- const dest = path3.join(outputDir, "artifacts", f);
799
+ const src = path6.join(artifactsDir, f);
800
+ const dest = path6.join(outputDir, "artifacts", f);
502
801
  try {
503
- const content = await fs3.readFile(src, "utf-8");
802
+ const content = await fs4.readFile(src, "utf-8");
504
803
  const parsed = JSON.parse(content);
505
804
  if (parsed.schema && parsed.schema.startsWith("hardkas.")) {
506
- await fs3.copyFile(src, dest);
805
+ await fs4.copyFile(src, dest);
507
806
  included++;
508
807
  } else {
509
808
  excluded++;
@@ -516,19 +815,19 @@ async function createSnapshot(options) {
516
815
  } catch {
517
816
  }
518
817
  try {
519
- const eventsLog = path3.join(hardkasDir, "events.jsonl");
520
- await fs3.copyFile(eventsLog, path3.join(outputDir, "events", "events.jsonl"));
818
+ const eventsLog = path6.join(hardkasDir, "events.jsonl");
819
+ await fs4.copyFile(eventsLog, path6.join(outputDir, "events", "events.jsonl"));
521
820
  } catch {
522
821
  }
523
822
  try {
524
- const dbPath = path3.join(hardkasDir, "store.db");
525
- await fs3.copyFile(dbPath, path3.join(outputDir, "projections", "store.db"));
823
+ const dbPath = path6.join(hardkasDir, "store.db");
824
+ await fs4.copyFile(dbPath, path6.join(outputDir, "projections", "store.db"));
526
825
  } catch {
527
826
  }
528
827
  const manifest = {
529
828
  snapshotVersion: 1,
530
829
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
531
- hardkasVersion: "0.6.0-alpha",
830
+ hardkasVersion: "0.6.1-alpha",
532
831
  stateAuthority: "filesystem",
533
832
  projectionAuthority: "sqlite",
534
833
  deterministicScope,
@@ -537,16 +836,16 @@ async function createSnapshot(options) {
537
836
  excludedArtifacts: excluded,
538
837
  corruptedArtifacts: corrupted
539
838
  };
540
- await fs3.writeFile(
541
- path3.join(outputDir, "manifest.json"),
839
+ await fs4.writeFile(
840
+ path6.join(outputDir, "manifest.json"),
542
841
  JSON.stringify(manifest, null, 2),
543
842
  "utf-8"
544
843
  );
545
844
  return manifest;
546
845
  }
547
846
  async function readSnapshotManifest(snapshotDir) {
548
- const manifestPath = path3.join(snapshotDir, "manifest.json");
549
- const content = await fs3.readFile(manifestPath, "utf-8");
847
+ const manifestPath = path6.join(snapshotDir, "manifest.json");
848
+ const content = await fs4.readFile(manifestPath, "utf-8");
550
849
  return JSON.parse(content);
551
850
  }
552
851
 
@@ -555,6 +854,65 @@ function deterministicCompare(a, b) {
555
854
  return a < b ? -1 : a > b ? 1 : 0;
556
855
  }
557
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
+
558
916
  // src/runtime-context.ts
559
917
  var systemRuntimeContext = {
560
918
  clock: {
@@ -566,6 +924,210 @@ var systemRuntimeContext = {
566
924
  ids: {
567
925
  execution: () => `exec_${Date.now().toString(36)}`,
568
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;
569
1131
  }
570
1132
  };
571
1133
 
@@ -661,13 +1223,20 @@ function formatSompi(amountSompi) {
661
1223
  return `${sign}${whole}.${fractional.toString().padStart(8, "0")} KAS`;
662
1224
  }
663
1225
  export {
1226
+ AppendCoordinator,
664
1227
  ArtifactTypeSchema,
1228
+ CURRENT_RUNTIME_VERSION,
1229
+ EnvironmentTelemetry,
665
1230
  ExecutionModeSchema,
666
1231
  HardkasError,
667
1232
  InvariantViolationError,
668
1233
  LOCK_ORDER,
1234
+ MIN_SUPPORTED_VERSION,
1235
+ MigrationManager,
669
1236
  NetworkIdSchema,
670
1237
  SOMPI_PER_KAS,
1238
+ TelemetryManager,
1239
+ TelemetryRotator,
671
1240
  acquireLock,
672
1241
  artifactTypeSchema,
673
1242
  asArtifactId,
@@ -682,26 +1251,43 @@ export {
682
1251
  asRpcEndpointId,
683
1252
  asTxId,
684
1253
  asWorkflowId,
1254
+ assertNoSemanticDrift,
1255
+ attachLedgerAppender,
1256
+ classifyArtifactStatus,
685
1257
  clearLock,
1258
+ comparePrePostMigrationLineage,
686
1259
  coreEvents,
687
1260
  createEventEnvelope,
688
1261
  createSnapshot,
1262
+ detectSemanticDrift,
689
1263
  deterministicCompare,
690
1264
  diffReplays,
691
1265
  executionModeSchema,
692
1266
  formatCorruptionIssue,
693
1267
  formatSompi,
1268
+ getTelemetry,
1269
+ globalTelemetry,
694
1270
  hardkasConfigSchema,
695
1271
  isProcessAlive,
696
1272
  kaspaNetworkIdSchema,
697
1273
  listLocks,
698
1274
  maskSecrets,
1275
+ migrateArtifact,
699
1276
  parseHardkasConfig,
700
1277
  parseKasToSompi,
701
1278
  readSnapshotManifest,
702
1279
  redactSecret,
1280
+ resolveCanonicalArtifact,
1281
+ resolveLineage,
703
1282
  systemRuntimeContext,
1283
+ telemetryContextStorage,
704
1284
  validateEventEnvelope,
1285
+ validateStatusTransition,
1286
+ verifyArtifactIntegrity,
1287
+ verifyCapabilityBoundary,
1288
+ verifyMigrationIntegrity,
1289
+ verifyProjectionFreshness,
1290
+ verifyReplay,
705
1291
  withLock,
706
1292
  withLocks,
707
1293
  writeFileAtomic,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hardkas/core",
3
- "version": "0.6.0-alpha",
3
+ "version": "0.6.1-alpha",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",