@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 +221 -1
- package/dist/index.js +671 -85
- package/package.json +1 -1
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
|
|
120
|
-
import
|
|
121
|
-
import
|
|
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 =
|
|
124
|
-
const base =
|
|
125
|
-
const tempPath =
|
|
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 (!
|
|
129
|
-
|
|
384
|
+
if (!fs2.existsSync(dir)) {
|
|
385
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
130
386
|
}
|
|
131
|
-
fd =
|
|
387
|
+
fd = fs2.openSync(tempPath, "w", options.mode);
|
|
132
388
|
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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 =
|
|
157
|
-
|
|
413
|
+
dirFd = fs2.openSync(dir, "r");
|
|
414
|
+
fs2.fsyncSync(dirFd);
|
|
158
415
|
} catch (e) {
|
|
159
416
|
} finally {
|
|
160
|
-
if (dirFd !== null)
|
|
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 (
|
|
427
|
+
if (fs2.existsSync(tempPath)) {
|
|
171
428
|
try {
|
|
172
|
-
|
|
429
|
+
fs2.unlinkSync(tempPath);
|
|
173
430
|
} catch (e) {
|
|
174
431
|
}
|
|
175
432
|
}
|
|
176
433
|
if (fd !== null) {
|
|
177
434
|
try {
|
|
178
|
-
|
|
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 =
|
|
186
|
-
const base =
|
|
187
|
-
const tempPath =
|
|
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 (!
|
|
191
|
-
|
|
447
|
+
if (!fs2.existsSync(dir)) {
|
|
448
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
192
449
|
}
|
|
193
|
-
fd =
|
|
450
|
+
fd = fs2.openSync(tempPath, "w", options.mode);
|
|
194
451
|
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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 =
|
|
218
|
-
|
|
475
|
+
dirFd = fs2.openSync(dir, "r");
|
|
476
|
+
fs2.fsyncSync(dirFd);
|
|
219
477
|
} catch (e) {
|
|
220
478
|
} finally {
|
|
221
|
-
if (dirFd !== null)
|
|
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 (
|
|
489
|
+
if (fs2.existsSync(tempPath)) {
|
|
232
490
|
try {
|
|
233
|
-
|
|
491
|
+
fs2.unlinkSync(tempPath);
|
|
234
492
|
} catch (e) {
|
|
235
493
|
}
|
|
236
494
|
}
|
|
237
495
|
if (fd !== null) {
|
|
238
496
|
try {
|
|
239
|
-
|
|
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
|
|
266
|
-
import
|
|
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 =
|
|
278
|
-
const lockPath =
|
|
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
|
-
|
|
283
|
-
|
|
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 =
|
|
298
|
-
|
|
299
|
-
|
|
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 (
|
|
563
|
+
if (fs3.existsSync(lockPath)) {
|
|
305
564
|
try {
|
|
306
|
-
const current = JSON.parse(
|
|
565
|
+
const current = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
307
566
|
if (current.pid === process.pid) {
|
|
308
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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 =
|
|
383
|
-
if (!
|
|
384
|
-
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"));
|
|
385
684
|
const result = [];
|
|
386
685
|
for (const file of files) {
|
|
387
|
-
const lockPath =
|
|
686
|
+
const lockPath = path5.join(lockDir, file);
|
|
388
687
|
try {
|
|
389
|
-
const metadata = JSON.parse(
|
|
688
|
+
const metadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
390
689
|
result.push({
|
|
391
|
-
name:
|
|
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 =
|
|
404
|
-
const lockPath =
|
|
405
|
-
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" };
|
|
406
705
|
let metadata;
|
|
407
706
|
try {
|
|
408
|
-
metadata = JSON.parse(
|
|
707
|
+
metadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
409
708
|
} catch (e) {
|
|
410
709
|
if (options.force) {
|
|
411
|
-
|
|
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
|
-
|
|
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
|
|
483
|
-
import
|
|
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
|
|
487
|
-
await
|
|
488
|
-
await
|
|
489
|
-
await
|
|
490
|
-
await
|
|
491
|
-
await
|
|
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 =
|
|
794
|
+
const artifactsDir = path6.join(hardkasDir, "artifacts");
|
|
496
795
|
try {
|
|
497
|
-
const list = await
|
|
796
|
+
const list = await fs4.readdir(artifactsDir);
|
|
498
797
|
for (const f of list) {
|
|
499
798
|
if (f.endsWith(".json")) {
|
|
500
|
-
const src =
|
|
501
|
-
const dest =
|
|
799
|
+
const src = path6.join(artifactsDir, f);
|
|
800
|
+
const dest = path6.join(outputDir, "artifacts", f);
|
|
502
801
|
try {
|
|
503
|
-
const content = await
|
|
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
|
|
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 =
|
|
520
|
-
await
|
|
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 =
|
|
525
|
-
await
|
|
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.
|
|
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
|
|
541
|
-
|
|
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 =
|
|
549
|
-
const content = await
|
|
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,
|