@hardkas/core 0.6.0-alpha → 0.7.0-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 +228 -1
- package/dist/index.js +723 -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,146 @@ 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
|
+
private static _lastRecovery;
|
|
767
|
+
/**
|
|
768
|
+
* Safely appends a line to a JSONL log under process coordination locks.
|
|
769
|
+
* Performs an immediate fsync to ensure data durability.
|
|
770
|
+
* Also repairs the trailing line if it is corrupted, emitting an anomaly.
|
|
771
|
+
*/
|
|
772
|
+
static appendAtomic(filePath: string, line: string, rootDir: string): void;
|
|
773
|
+
/**
|
|
774
|
+
* Scans a JSONL stream for corruption, truncating malformed trailing lines.
|
|
775
|
+
* Utilizes a backward newline scanning logic with a rolling buffer,
|
|
776
|
+
* supporting lines of arbitrary size and only truncating the last complete
|
|
777
|
+
* valid JSONL boundary if a parse failure is detected.
|
|
778
|
+
*/
|
|
779
|
+
static recoverCorruptedTail(filePath: string): {
|
|
780
|
+
repaired: boolean;
|
|
781
|
+
linesDiscarded: number;
|
|
782
|
+
originalTail: string;
|
|
783
|
+
originalSize: number;
|
|
784
|
+
recoveredSize: number;
|
|
785
|
+
reason: string;
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
declare const CURRENT_RUNTIME_VERSION = "0.7.0-alpha";
|
|
790
|
+
declare const MIN_SUPPORTED_VERSION = "0.5.0-alpha";
|
|
791
|
+
interface MigrationStatus {
|
|
792
|
+
needsMigration: boolean;
|
|
793
|
+
canDowngrade: boolean;
|
|
794
|
+
currentVersion: string;
|
|
795
|
+
}
|
|
796
|
+
declare class MigrationManager {
|
|
797
|
+
static checkVersion(rootDir: string): MigrationStatus;
|
|
798
|
+
static migrate(rootDir: string, dryRun?: boolean): void;
|
|
799
|
+
private static writeVersion;
|
|
800
|
+
private static backupWorkspace;
|
|
801
|
+
private static compareSemver;
|
|
802
|
+
}
|
|
803
|
+
|
|
577
804
|
declare const SOMPI_PER_KAS = 100000000n;
|
|
578
805
|
declare const kaspaNetworkIdSchema: z.ZodEnum<["mainnet", "testnet-10", "testnet-11", "testnet-12", "simnet", "simnet-1", "devnet", "simulated"]>;
|
|
579
806
|
type NetworkId = Brand<z.infer<typeof kaspaNetworkIdSchema>, "NetworkId">;
|
|
@@ -664,4 +891,4 @@ declare function parseHardkasConfig(input: unknown): HardkasConfig;
|
|
|
664
891
|
declare function parseKasToSompi(input: string): bigint;
|
|
665
892
|
declare function formatSompi(amountSompi: bigint): string;
|
|
666
893
|
|
|
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 };
|
|
894
|
+
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,296 @@
|
|
|
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
|
+
static _lastRecovery = null;
|
|
119
|
+
/**
|
|
120
|
+
* Safely appends a line to a JSONL log under process coordination locks.
|
|
121
|
+
* Performs an immediate fsync to ensure data durability.
|
|
122
|
+
* Also repairs the trailing line if it is corrupted, emitting an anomaly.
|
|
123
|
+
*/
|
|
124
|
+
static appendAtomic(filePath, line, rootDir) {
|
|
125
|
+
const lockDir = path2.join(rootDir, ".hardkas", "locks");
|
|
126
|
+
if (!fs.existsSync(lockDir)) {
|
|
127
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
128
|
+
}
|
|
129
|
+
const logBase = path2.basename(filePath);
|
|
130
|
+
const lockPath = path2.join(lockDir, `append-${logBase}.lock`);
|
|
131
|
+
let fd = null;
|
|
132
|
+
let repaired = false;
|
|
133
|
+
let linesDiscarded = 0;
|
|
134
|
+
let originalTail = "";
|
|
135
|
+
try {
|
|
136
|
+
const start = Date.now();
|
|
137
|
+
const timeoutMs = 1e4;
|
|
138
|
+
while (true) {
|
|
139
|
+
try {
|
|
140
|
+
fd = fs.openSync(lockPath, "wx");
|
|
141
|
+
break;
|
|
142
|
+
} catch (e) {
|
|
143
|
+
if (e.code === "EEXIST") {
|
|
144
|
+
if (Date.now() - start > timeoutMs) {
|
|
145
|
+
throw new Error(`[AppendCoordinator] Timeout waiting for lock on ${lockPath}`);
|
|
146
|
+
}
|
|
147
|
+
const sleepMs = 5 + Math.floor(Math.random() * 15);
|
|
148
|
+
const sharedBuf = new Int32Array(new SharedArrayBuffer(4));
|
|
149
|
+
Atomics.wait(sharedBuf, 0, 0, sleepMs);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
throw e;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, time: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
156
|
+
const recovery = _AppendCoordinator.recoverCorruptedTail(filePath);
|
|
157
|
+
if (recovery.repaired) {
|
|
158
|
+
repaired = true;
|
|
159
|
+
linesDiscarded = recovery.linesDiscarded;
|
|
160
|
+
originalTail = recovery.originalTail;
|
|
161
|
+
_AppendCoordinator._lastRecovery = recovery;
|
|
162
|
+
}
|
|
163
|
+
const logDir = path2.dirname(filePath);
|
|
164
|
+
if (!fs.existsSync(logDir)) {
|
|
165
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
166
|
+
}
|
|
167
|
+
const logFd = fs.openSync(filePath, "a");
|
|
168
|
+
const buffer = Buffer.from(line.endsWith("\n") ? line : line + "\n", "utf-8");
|
|
169
|
+
fs.writeSync(logFd, buffer, 0, buffer.length);
|
|
170
|
+
fs.fsyncSync(logFd);
|
|
171
|
+
fs.closeSync(logFd);
|
|
172
|
+
} finally {
|
|
173
|
+
if (fd !== null) {
|
|
174
|
+
fs.closeSync(fd);
|
|
175
|
+
try {
|
|
176
|
+
fs.unlinkSync(lockPath);
|
|
177
|
+
} catch {
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (repaired) {
|
|
182
|
+
try {
|
|
183
|
+
const recovery = _AppendCoordinator._lastRecovery;
|
|
184
|
+
const telemetry = getTelemetry();
|
|
185
|
+
telemetry.logAnomaly(
|
|
186
|
+
"EXTERNAL_MUTATION",
|
|
187
|
+
"medium",
|
|
188
|
+
"fs",
|
|
189
|
+
`Recovered corrupted tail in ${logBase}. Original size: ${recovery.originalSize} bytes, Recovered size: ${recovery.recoveredSize} bytes, Truncated bytes: ${linesDiscarded}. Reason: ${recovery.reason}. Original tail snippet: "${originalTail.slice(0, 60)}..."`,
|
|
190
|
+
rootDir
|
|
191
|
+
);
|
|
192
|
+
} catch {
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Scans a JSONL stream for corruption, truncating malformed trailing lines.
|
|
198
|
+
* Utilizes a backward newline scanning logic with a rolling buffer,
|
|
199
|
+
* supporting lines of arbitrary size and only truncating the last complete
|
|
200
|
+
* valid JSONL boundary if a parse failure is detected.
|
|
201
|
+
*/
|
|
202
|
+
static recoverCorruptedTail(filePath) {
|
|
203
|
+
const defaultRes = {
|
|
204
|
+
repaired: false,
|
|
205
|
+
linesDiscarded: 0,
|
|
206
|
+
originalTail: "",
|
|
207
|
+
originalSize: 0,
|
|
208
|
+
recoveredSize: 0,
|
|
209
|
+
reason: ""
|
|
210
|
+
};
|
|
211
|
+
if (!fs.existsSync(filePath)) return defaultRes;
|
|
212
|
+
const stat = fs.statSync(filePath);
|
|
213
|
+
defaultRes.originalSize = stat.size;
|
|
214
|
+
defaultRes.recoveredSize = stat.size;
|
|
215
|
+
if (stat.size === 0) return defaultRes;
|
|
216
|
+
const fd = fs.openSync(filePath, "r");
|
|
217
|
+
try {
|
|
218
|
+
let lastCharPos = -1;
|
|
219
|
+
let precedingNewlinePos = -1;
|
|
220
|
+
const CHUNK_SIZE = 64 * 1024;
|
|
221
|
+
let position = stat.size;
|
|
222
|
+
const buffer = Buffer.alloc(CHUNK_SIZE);
|
|
223
|
+
outer1: while (position > 0) {
|
|
224
|
+
const readLength = Math.min(CHUNK_SIZE, position);
|
|
225
|
+
position -= readLength;
|
|
226
|
+
fs.readSync(fd, buffer, 0, readLength, position);
|
|
227
|
+
for (let i = readLength - 1; i >= 0; i--) {
|
|
228
|
+
const charCode = buffer[i];
|
|
229
|
+
if (charCode !== 32 && charCode !== 9 && charCode !== 10 && charCode !== 13) {
|
|
230
|
+
lastCharPos = position + i;
|
|
231
|
+
break outer1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (lastCharPos === -1) {
|
|
236
|
+
fs.closeSync(fd);
|
|
237
|
+
fs.truncateSync(filePath, 0);
|
|
238
|
+
return {
|
|
239
|
+
repaired: true,
|
|
240
|
+
linesDiscarded: stat.size,
|
|
241
|
+
originalTail: "",
|
|
242
|
+
originalSize: stat.size,
|
|
243
|
+
recoveredSize: 0,
|
|
244
|
+
reason: "File only contained whitespaces or newlines"
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
position = lastCharPos;
|
|
248
|
+
outer2: while (position > 0) {
|
|
249
|
+
const readLength = Math.min(CHUNK_SIZE, position);
|
|
250
|
+
position -= readLength;
|
|
251
|
+
fs.readSync(fd, buffer, 0, readLength, position);
|
|
252
|
+
for (let i = readLength - 1; i >= 0; i--) {
|
|
253
|
+
if (buffer[i] === 10) {
|
|
254
|
+
precedingNewlinePos = position + i;
|
|
255
|
+
break outer2;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const lastLineStart = precedingNewlinePos === -1 ? 0 : precedingNewlinePos + 1;
|
|
260
|
+
const lastLineEnd = lastCharPos + 1;
|
|
261
|
+
const lastLineLength = lastLineEnd - lastLineStart;
|
|
262
|
+
const lastLineBuf = Buffer.alloc(lastLineLength);
|
|
263
|
+
fs.readSync(fd, lastLineBuf, 0, lastLineLength, lastLineStart);
|
|
264
|
+
fs.closeSync(fd);
|
|
265
|
+
const lastLine = lastLineBuf.toString("utf-8");
|
|
266
|
+
try {
|
|
267
|
+
JSON.parse(lastLine);
|
|
268
|
+
return defaultRes;
|
|
269
|
+
} catch (err) {
|
|
270
|
+
const truncateTo = lastLineStart;
|
|
271
|
+
const discardedBytes = stat.size - truncateTo;
|
|
272
|
+
fs.truncateSync(filePath, truncateTo);
|
|
273
|
+
return {
|
|
274
|
+
repaired: true,
|
|
275
|
+
linesDiscarded: discardedBytes,
|
|
276
|
+
originalTail: lastLine,
|
|
277
|
+
originalSize: stat.size,
|
|
278
|
+
recoveredSize: truncateTo,
|
|
279
|
+
reason: err instanceof Error ? err.message : "Invalid JSON syntax"
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
} catch (e) {
|
|
283
|
+
try {
|
|
284
|
+
fs.closeSync(fd);
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
4
292
|
// src/events.ts
|
|
293
|
+
import path3 from "path";
|
|
5
294
|
var CoreEventBus = class {
|
|
6
295
|
listeners = [];
|
|
7
296
|
on(listener) {
|
|
@@ -66,6 +355,25 @@ function validateEventEnvelope(event) {
|
|
|
66
355
|
if (typeof event.payload !== "object") return false;
|
|
67
356
|
return true;
|
|
68
357
|
}
|
|
358
|
+
function attachLedgerAppender(workspaceRoot) {
|
|
359
|
+
const seenEventIds = /* @__PURE__ */ new Set();
|
|
360
|
+
const eventsFile = path3.join(workspaceRoot, "events.jsonl");
|
|
361
|
+
return coreEvents.on((event) => {
|
|
362
|
+
if (seenEventIds.has(event.eventId)) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
seenEventIds.add(event.eventId);
|
|
366
|
+
if (seenEventIds.size > 1e5) {
|
|
367
|
+
const iterator = seenEventIds.keys();
|
|
368
|
+
for (let i = 0; i < 1e4; i++) seenEventIds.delete(iterator.next().value);
|
|
369
|
+
}
|
|
370
|
+
const payload = JSON.stringify(event) + "\n";
|
|
371
|
+
try {
|
|
372
|
+
AppendCoordinator.appendAtomic(eventsFile, payload, workspaceRoot);
|
|
373
|
+
} catch (e) {
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
}
|
|
69
377
|
|
|
70
378
|
// src/domain-types.ts
|
|
71
379
|
var asTxId = (id) => id;
|
|
@@ -116,34 +424,35 @@ function redactSecret(value) {
|
|
|
116
424
|
}
|
|
117
425
|
|
|
118
426
|
// src/fs.ts
|
|
119
|
-
import
|
|
120
|
-
import
|
|
121
|
-
import
|
|
427
|
+
import fs2 from "fs";
|
|
428
|
+
import path4 from "path";
|
|
429
|
+
import crypto3 from "crypto";
|
|
122
430
|
async function writeFileAtomic(targetPath, data, options = {}) {
|
|
123
|
-
const dir =
|
|
124
|
-
const base =
|
|
125
|
-
const tempPath =
|
|
431
|
+
const dir = path4.dirname(targetPath);
|
|
432
|
+
const base = path4.basename(targetPath);
|
|
433
|
+
const tempPath = path4.join(dir, `.tmp.${base}.${crypto3.randomUUID()}`);
|
|
126
434
|
let fd = null;
|
|
127
435
|
try {
|
|
128
|
-
if (!
|
|
129
|
-
|
|
436
|
+
if (!fs2.existsSync(dir)) {
|
|
437
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
130
438
|
}
|
|
131
|
-
fd =
|
|
439
|
+
fd = fs2.openSync(tempPath, "w", options.mode);
|
|
132
440
|
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
441
|
+
fs2.writeSync(fd, buffer, 0, buffer.length);
|
|
442
|
+
fs2.fsyncSync(fd);
|
|
443
|
+
fs2.closeSync(fd);
|
|
136
444
|
fd = null;
|
|
137
445
|
let attempts = 0;
|
|
138
446
|
const maxAttempts = process.platform === "win32" ? 5 : 1;
|
|
139
447
|
while (attempts < maxAttempts) {
|
|
140
448
|
try {
|
|
141
|
-
|
|
449
|
+
fs2.renameSync(tempPath, targetPath);
|
|
142
450
|
break;
|
|
143
451
|
} catch (e) {
|
|
144
452
|
attempts++;
|
|
145
453
|
if (attempts >= maxAttempts) throw e;
|
|
146
454
|
if (e.code === "EPERM" || e.code === "EBUSY") {
|
|
455
|
+
EnvironmentTelemetry.logAnomaly("FS_RETRY", "low", "fs", `Retrying rename of ${targetPath} due to ${e.code}`);
|
|
147
456
|
await new Promise((resolve) => setTimeout(resolve, 10 * attempts));
|
|
148
457
|
continue;
|
|
149
458
|
}
|
|
@@ -153,11 +462,11 @@ async function writeFileAtomic(targetPath, data, options = {}) {
|
|
|
153
462
|
if (options.fsyncParent && process.platform !== "win32") {
|
|
154
463
|
let dirFd = null;
|
|
155
464
|
try {
|
|
156
|
-
dirFd =
|
|
157
|
-
|
|
465
|
+
dirFd = fs2.openSync(dir, "r");
|
|
466
|
+
fs2.fsyncSync(dirFd);
|
|
158
467
|
} catch (e) {
|
|
159
468
|
} finally {
|
|
160
|
-
if (dirFd !== null)
|
|
469
|
+
if (dirFd !== null) fs2.closeSync(dirFd);
|
|
161
470
|
}
|
|
162
471
|
}
|
|
163
472
|
} catch (err) {
|
|
@@ -167,45 +476,46 @@ async function writeFileAtomic(targetPath, data, options = {}) {
|
|
|
167
476
|
{ cause: err }
|
|
168
477
|
);
|
|
169
478
|
} finally {
|
|
170
|
-
if (
|
|
479
|
+
if (fs2.existsSync(tempPath)) {
|
|
171
480
|
try {
|
|
172
|
-
|
|
481
|
+
fs2.unlinkSync(tempPath);
|
|
173
482
|
} catch (e) {
|
|
174
483
|
}
|
|
175
484
|
}
|
|
176
485
|
if (fd !== null) {
|
|
177
486
|
try {
|
|
178
|
-
|
|
487
|
+
fs2.closeSync(fd);
|
|
179
488
|
} catch (e) {
|
|
180
489
|
}
|
|
181
490
|
}
|
|
182
491
|
}
|
|
183
492
|
}
|
|
184
493
|
function writeFileAtomicSync(targetPath, data, options = {}) {
|
|
185
|
-
const dir =
|
|
186
|
-
const base =
|
|
187
|
-
const tempPath =
|
|
494
|
+
const dir = path4.dirname(targetPath);
|
|
495
|
+
const base = path4.basename(targetPath);
|
|
496
|
+
const tempPath = path4.join(dir, `.tmp.${base}.${crypto3.randomUUID()}`);
|
|
188
497
|
let fd = null;
|
|
189
498
|
try {
|
|
190
|
-
if (!
|
|
191
|
-
|
|
499
|
+
if (!fs2.existsSync(dir)) {
|
|
500
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
192
501
|
}
|
|
193
|
-
fd =
|
|
502
|
+
fd = fs2.openSync(tempPath, "w", options.mode);
|
|
194
503
|
const buffer = typeof data === "string" ? Buffer.from(data, options.encoding || "utf-8") : data;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
504
|
+
fs2.writeSync(fd, buffer, 0, buffer.length);
|
|
505
|
+
fs2.fsyncSync(fd);
|
|
506
|
+
fs2.closeSync(fd);
|
|
198
507
|
fd = null;
|
|
199
508
|
let attempts = 0;
|
|
200
509
|
const maxAttempts = process.platform === "win32" ? 5 : 1;
|
|
201
510
|
while (attempts < maxAttempts) {
|
|
202
511
|
try {
|
|
203
|
-
|
|
512
|
+
fs2.renameSync(tempPath, targetPath);
|
|
204
513
|
break;
|
|
205
514
|
} catch (e) {
|
|
206
515
|
attempts++;
|
|
207
516
|
if (attempts >= maxAttempts) throw e;
|
|
208
517
|
if (e.code === "EPERM" || e.code === "EBUSY") {
|
|
518
|
+
EnvironmentTelemetry.logAnomaly("FS_RETRY", "low", "fs", `Retrying rename sync of ${targetPath} due to ${e.code}`);
|
|
209
519
|
continue;
|
|
210
520
|
}
|
|
211
521
|
throw e;
|
|
@@ -214,11 +524,11 @@ function writeFileAtomicSync(targetPath, data, options = {}) {
|
|
|
214
524
|
if (options.fsyncParent && process.platform !== "win32") {
|
|
215
525
|
let dirFd = null;
|
|
216
526
|
try {
|
|
217
|
-
dirFd =
|
|
218
|
-
|
|
527
|
+
dirFd = fs2.openSync(dir, "r");
|
|
528
|
+
fs2.fsyncSync(dirFd);
|
|
219
529
|
} catch (e) {
|
|
220
530
|
} finally {
|
|
221
|
-
if (dirFd !== null)
|
|
531
|
+
if (dirFd !== null) fs2.closeSync(dirFd);
|
|
222
532
|
}
|
|
223
533
|
}
|
|
224
534
|
} catch (err) {
|
|
@@ -228,15 +538,15 @@ function writeFileAtomicSync(targetPath, data, options = {}) {
|
|
|
228
538
|
{ cause: err }
|
|
229
539
|
);
|
|
230
540
|
} finally {
|
|
231
|
-
if (
|
|
541
|
+
if (fs2.existsSync(tempPath)) {
|
|
232
542
|
try {
|
|
233
|
-
|
|
543
|
+
fs2.unlinkSync(tempPath);
|
|
234
544
|
} catch (e) {
|
|
235
545
|
}
|
|
236
546
|
}
|
|
237
547
|
if (fd !== null) {
|
|
238
548
|
try {
|
|
239
|
-
|
|
549
|
+
fs2.closeSync(fd);
|
|
240
550
|
} catch (e) {
|
|
241
551
|
}
|
|
242
552
|
}
|
|
@@ -262,8 +572,8 @@ function formatCorruptionIssue(issue) {
|
|
|
262
572
|
}
|
|
263
573
|
|
|
264
574
|
// src/lock.ts
|
|
265
|
-
import
|
|
266
|
-
import
|
|
575
|
+
import fs3 from "fs";
|
|
576
|
+
import path5 from "path";
|
|
267
577
|
import os from "os";
|
|
268
578
|
var LOCK_ORDER = [
|
|
269
579
|
"workspace",
|
|
@@ -274,13 +584,14 @@ var LOCK_ORDER = [
|
|
|
274
584
|
"query-store"
|
|
275
585
|
];
|
|
276
586
|
async function acquireLock(args) {
|
|
277
|
-
const lockDir =
|
|
278
|
-
const lockPath =
|
|
587
|
+
const lockDir = path5.join(args.rootDir, ".hardkas", "locks");
|
|
588
|
+
const lockPath = path5.join(lockDir, `${args.name}.lock`);
|
|
279
589
|
const timeoutMs = args.timeoutMs ?? 3e4;
|
|
280
590
|
const pollMs = args.pollMs ?? 250;
|
|
281
591
|
const start = Date.now();
|
|
282
|
-
|
|
283
|
-
|
|
592
|
+
let staleRecoveryAttempted = false;
|
|
593
|
+
if (!fs3.existsSync(lockDir)) {
|
|
594
|
+
fs3.mkdirSync(lockDir, { recursive: true });
|
|
284
595
|
}
|
|
285
596
|
while (true) {
|
|
286
597
|
try {
|
|
@@ -294,18 +605,18 @@ async function acquireLock(args) {
|
|
|
294
605
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
295
606
|
expiresAt: null
|
|
296
607
|
};
|
|
297
|
-
const fd =
|
|
298
|
-
|
|
299
|
-
|
|
608
|
+
const fd = fs3.openSync(lockPath, "wx");
|
|
609
|
+
fs3.writeSync(fd, JSON.stringify(metadata, null, 2));
|
|
610
|
+
fs3.closeSync(fd);
|
|
300
611
|
return {
|
|
301
612
|
path: lockPath,
|
|
302
613
|
metadata,
|
|
303
614
|
release: async () => {
|
|
304
|
-
if (
|
|
615
|
+
if (fs3.existsSync(lockPath)) {
|
|
305
616
|
try {
|
|
306
|
-
const current = JSON.parse(
|
|
617
|
+
const current = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
307
618
|
if (current.pid === process.pid) {
|
|
308
|
-
|
|
619
|
+
fs3.unlinkSync(lockPath);
|
|
309
620
|
}
|
|
310
621
|
} catch (e) {
|
|
311
622
|
}
|
|
@@ -316,12 +627,49 @@ async function acquireLock(args) {
|
|
|
316
627
|
if (e.code === "EEXIST") {
|
|
317
628
|
let existingMetadata = null;
|
|
318
629
|
try {
|
|
319
|
-
existingMetadata = JSON.parse(
|
|
630
|
+
existingMetadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
320
631
|
} catch (err) {
|
|
632
|
+
const LOCK_CREATION_GRACE_MS = 2e3;
|
|
633
|
+
let stats = null;
|
|
634
|
+
try {
|
|
635
|
+
stats = fs3.statSync(lockPath);
|
|
636
|
+
} catch {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
const ageMs = Date.now() - stats.mtimeMs;
|
|
640
|
+
if (ageMs < LOCK_CREATION_GRACE_MS) {
|
|
641
|
+
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (!staleRecoveryAttempted) {
|
|
645
|
+
staleRecoveryAttempted = true;
|
|
646
|
+
try {
|
|
647
|
+
fs3.unlinkSync(lockPath);
|
|
648
|
+
EnvironmentTelemetry.logAnomaly("STALE_LOCK_RECOVERY", "medium", "lock", `Recovered corrupted lock file at ${lockPath} (Age: ${ageMs}ms)`, args.rootDir);
|
|
649
|
+
continue;
|
|
650
|
+
} catch {
|
|
651
|
+
throw new HardkasError("LOCK_METADATA_INVALID", `Lock file at ${lockPath} is corrupted and cannot be recovered.`, { cause: err });
|
|
652
|
+
}
|
|
653
|
+
}
|
|
321
654
|
throw new HardkasError("LOCK_METADATA_INVALID", `Lock file at ${lockPath} is corrupted.`, { cause: err });
|
|
322
655
|
}
|
|
323
656
|
if (existingMetadata) {
|
|
324
|
-
const
|
|
657
|
+
const isLocal = existingMetadata.hostname === os.hostname();
|
|
658
|
+
const isAlive = isLocal ? isProcessAlive(existingMetadata.pid) : true;
|
|
659
|
+
if (!isAlive && !staleRecoveryAttempted) {
|
|
660
|
+
staleRecoveryAttempted = true;
|
|
661
|
+
try {
|
|
662
|
+
fs3.unlinkSync(lockPath);
|
|
663
|
+
EnvironmentTelemetry.logAnomaly("STALE_LOCK_RECOVERY", "medium", "lock", `Recovered lock held by dead process (PID: ${existingMetadata.pid})`, args.rootDir);
|
|
664
|
+
continue;
|
|
665
|
+
} catch (unlinkErr) {
|
|
666
|
+
throw new HardkasError(
|
|
667
|
+
"STALE_LOCK",
|
|
668
|
+
`Workspace is locked by a dead process (PID: ${existingMetadata.pid}). Failed to auto-recover: ${unlinkErr}`,
|
|
669
|
+
{ cause: existingMetadata }
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
325
673
|
if (!isAlive) {
|
|
326
674
|
throw new HardkasError(
|
|
327
675
|
"STALE_LOCK",
|
|
@@ -330,6 +678,7 @@ async function acquireLock(args) {
|
|
|
330
678
|
);
|
|
331
679
|
}
|
|
332
680
|
if (args.wait && Date.now() - start < timeoutMs) {
|
|
681
|
+
EnvironmentTelemetry.logAnomaly("LOCK_CONTENTION", "low", "lock", `Waiting for lock ${args.name} held by PID ${existingMetadata.pid}`, args.rootDir);
|
|
333
682
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
334
683
|
continue;
|
|
335
684
|
}
|
|
@@ -375,20 +724,22 @@ function isProcessAlive(pid) {
|
|
|
375
724
|
process.kill(pid, 0);
|
|
376
725
|
return true;
|
|
377
726
|
} catch (e) {
|
|
378
|
-
|
|
727
|
+
if (e.code === "EPERM") return true;
|
|
728
|
+
if (e.code === "ESRCH") return false;
|
|
729
|
+
return true;
|
|
379
730
|
}
|
|
380
731
|
}
|
|
381
732
|
function listLocks(rootDir) {
|
|
382
|
-
const lockDir =
|
|
383
|
-
if (!
|
|
384
|
-
const files =
|
|
733
|
+
const lockDir = path5.join(rootDir, ".hardkas", "locks");
|
|
734
|
+
if (!fs3.existsSync(lockDir)) return [];
|
|
735
|
+
const files = fs3.readdirSync(lockDir).filter((f) => f.endsWith(".lock"));
|
|
385
736
|
const result = [];
|
|
386
737
|
for (const file of files) {
|
|
387
|
-
const lockPath =
|
|
738
|
+
const lockPath = path5.join(lockDir, file);
|
|
388
739
|
try {
|
|
389
|
-
const metadata = JSON.parse(
|
|
740
|
+
const metadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
390
741
|
result.push({
|
|
391
|
-
name:
|
|
742
|
+
name: path5.basename(file, ".lock"),
|
|
392
743
|
metadata,
|
|
393
744
|
path: lockPath,
|
|
394
745
|
isAlive: metadata.hostname === os.hostname() ? isProcessAlive(metadata.pid) : true
|
|
@@ -400,15 +751,15 @@ function listLocks(rootDir) {
|
|
|
400
751
|
return result;
|
|
401
752
|
}
|
|
402
753
|
function clearLock(rootDir, name, options = {}) {
|
|
403
|
-
const lockDir =
|
|
404
|
-
const lockPath =
|
|
405
|
-
if (!
|
|
754
|
+
const lockDir = path5.join(rootDir, ".hardkas", "locks");
|
|
755
|
+
const lockPath = path5.join(lockDir, `${name}.lock`);
|
|
756
|
+
if (!fs3.existsSync(lockPath)) return { cleared: false, reason: "Lock not found" };
|
|
406
757
|
let metadata;
|
|
407
758
|
try {
|
|
408
|
-
metadata = JSON.parse(
|
|
759
|
+
metadata = JSON.parse(fs3.readFileSync(lockPath, "utf-8"));
|
|
409
760
|
} catch (e) {
|
|
410
761
|
if (options.force) {
|
|
411
|
-
|
|
762
|
+
fs3.unlinkSync(lockPath);
|
|
412
763
|
return { cleared: true };
|
|
413
764
|
}
|
|
414
765
|
return { cleared: false, reason: "Corrupt metadata (use --force to clear)" };
|
|
@@ -421,7 +772,7 @@ function clearLock(rootDir, name, options = {}) {
|
|
|
421
772
|
} else if (!options.force) {
|
|
422
773
|
return { cleared: false, reason: "Lock is potentially active. Use --force or --if-dead." };
|
|
423
774
|
}
|
|
424
|
-
|
|
775
|
+
fs3.unlinkSync(lockPath);
|
|
425
776
|
return { cleared: true };
|
|
426
777
|
}
|
|
427
778
|
|
|
@@ -479,31 +830,31 @@ function diffReplays(replayA, replayB) {
|
|
|
479
830
|
}
|
|
480
831
|
|
|
481
832
|
// src/snapshot.ts
|
|
482
|
-
import
|
|
483
|
-
import
|
|
833
|
+
import fs4 from "fs/promises";
|
|
834
|
+
import path6 from "path";
|
|
484
835
|
async function createSnapshot(options) {
|
|
485
836
|
const { hardkasDir, outputDir, deterministicScope = "local-only" } = options;
|
|
486
|
-
await
|
|
487
|
-
await
|
|
488
|
-
await
|
|
489
|
-
await
|
|
490
|
-
await
|
|
491
|
-
await
|
|
837
|
+
await fs4.mkdir(outputDir, { recursive: true });
|
|
838
|
+
await fs4.mkdir(path6.join(outputDir, "artifacts"), { recursive: true });
|
|
839
|
+
await fs4.mkdir(path6.join(outputDir, "projections"), { recursive: true });
|
|
840
|
+
await fs4.mkdir(path6.join(outputDir, "events"), { recursive: true });
|
|
841
|
+
await fs4.mkdir(path6.join(outputDir, "replay"), { recursive: true });
|
|
842
|
+
await fs4.mkdir(path6.join(outputDir, "metadata"), { recursive: true });
|
|
492
843
|
let included = 0;
|
|
493
844
|
let excluded = 0;
|
|
494
845
|
let corrupted = 0;
|
|
495
|
-
const artifactsDir =
|
|
846
|
+
const artifactsDir = path6.join(hardkasDir, "artifacts");
|
|
496
847
|
try {
|
|
497
|
-
const list = await
|
|
848
|
+
const list = await fs4.readdir(artifactsDir);
|
|
498
849
|
for (const f of list) {
|
|
499
850
|
if (f.endsWith(".json")) {
|
|
500
|
-
const src =
|
|
501
|
-
const dest =
|
|
851
|
+
const src = path6.join(artifactsDir, f);
|
|
852
|
+
const dest = path6.join(outputDir, "artifacts", f);
|
|
502
853
|
try {
|
|
503
|
-
const content = await
|
|
854
|
+
const content = await fs4.readFile(src, "utf-8");
|
|
504
855
|
const parsed = JSON.parse(content);
|
|
505
856
|
if (parsed.schema && parsed.schema.startsWith("hardkas.")) {
|
|
506
|
-
await
|
|
857
|
+
await fs4.copyFile(src, dest);
|
|
507
858
|
included++;
|
|
508
859
|
} else {
|
|
509
860
|
excluded++;
|
|
@@ -516,19 +867,19 @@ async function createSnapshot(options) {
|
|
|
516
867
|
} catch {
|
|
517
868
|
}
|
|
518
869
|
try {
|
|
519
|
-
const eventsLog =
|
|
520
|
-
await
|
|
870
|
+
const eventsLog = path6.join(hardkasDir, "events.jsonl");
|
|
871
|
+
await fs4.copyFile(eventsLog, path6.join(outputDir, "events", "events.jsonl"));
|
|
521
872
|
} catch {
|
|
522
873
|
}
|
|
523
874
|
try {
|
|
524
|
-
const dbPath =
|
|
525
|
-
await
|
|
875
|
+
const dbPath = path6.join(hardkasDir, "store.db");
|
|
876
|
+
await fs4.copyFile(dbPath, path6.join(outputDir, "projections", "store.db"));
|
|
526
877
|
} catch {
|
|
527
878
|
}
|
|
528
879
|
const manifest = {
|
|
529
880
|
snapshotVersion: 1,
|
|
530
881
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
531
|
-
hardkasVersion: "0.
|
|
882
|
+
hardkasVersion: "0.7.0-alpha",
|
|
532
883
|
stateAuthority: "filesystem",
|
|
533
884
|
projectionAuthority: "sqlite",
|
|
534
885
|
deterministicScope,
|
|
@@ -537,16 +888,16 @@ async function createSnapshot(options) {
|
|
|
537
888
|
excludedArtifacts: excluded,
|
|
538
889
|
corruptedArtifacts: corrupted
|
|
539
890
|
};
|
|
540
|
-
await
|
|
541
|
-
|
|
891
|
+
await fs4.writeFile(
|
|
892
|
+
path6.join(outputDir, "manifest.json"),
|
|
542
893
|
JSON.stringify(manifest, null, 2),
|
|
543
894
|
"utf-8"
|
|
544
895
|
);
|
|
545
896
|
return manifest;
|
|
546
897
|
}
|
|
547
898
|
async function readSnapshotManifest(snapshotDir) {
|
|
548
|
-
const manifestPath =
|
|
549
|
-
const content = await
|
|
899
|
+
const manifestPath = path6.join(snapshotDir, "manifest.json");
|
|
900
|
+
const content = await fs4.readFile(manifestPath, "utf-8");
|
|
550
901
|
return JSON.parse(content);
|
|
551
902
|
}
|
|
552
903
|
|
|
@@ -555,6 +906,65 @@ function deterministicCompare(a, b) {
|
|
|
555
906
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
556
907
|
}
|
|
557
908
|
|
|
909
|
+
// src/retention.ts
|
|
910
|
+
import fs5 from "fs";
|
|
911
|
+
import path7 from "path";
|
|
912
|
+
var TelemetryRotator = class {
|
|
913
|
+
static DEFAULT_MAX_SIZE_BYTES = 10 * 1024 * 1024;
|
|
914
|
+
// 10MB
|
|
915
|
+
/**
|
|
916
|
+
* Rotates the telemetry stream if it exceeds the maximum size.
|
|
917
|
+
* This is a safe operation that renames the active file to an archive directory.
|
|
918
|
+
*/
|
|
919
|
+
static rotateIfNeeded(rootDir, maxSizeBytes = this.DEFAULT_MAX_SIZE_BYTES) {
|
|
920
|
+
const telemetryDir = path7.join(rootDir, ".hardkas", "telemetry");
|
|
921
|
+
const activeFile = path7.join(telemetryDir, "telemetry.jsonl");
|
|
922
|
+
if (!fs5.existsSync(activeFile)) {
|
|
923
|
+
return { rotated: false, reason: "File does not exist" };
|
|
924
|
+
}
|
|
925
|
+
const stats = fs5.statSync(activeFile);
|
|
926
|
+
if (stats.size < maxSizeBytes) {
|
|
927
|
+
return { rotated: false, reason: `File size (${stats.size}) is below threshold (${maxSizeBytes})` };
|
|
928
|
+
}
|
|
929
|
+
return this.forceRotate(rootDir);
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Forces a rotation regardless of file size.
|
|
933
|
+
*/
|
|
934
|
+
static forceRotate(rootDir) {
|
|
935
|
+
const telemetryDir = path7.join(rootDir, ".hardkas", "telemetry");
|
|
936
|
+
const activeFile = path7.join(telemetryDir, "telemetry.jsonl");
|
|
937
|
+
if (!fs5.existsSync(activeFile)) {
|
|
938
|
+
return { rotated: false, reason: "File does not exist" };
|
|
939
|
+
}
|
|
940
|
+
const archiveDir = path7.join(telemetryDir, "archive");
|
|
941
|
+
if (!fs5.existsSync(archiveDir)) {
|
|
942
|
+
fs5.mkdirSync(archiveDir, { recursive: true });
|
|
943
|
+
}
|
|
944
|
+
const stats = fs5.statSync(activeFile);
|
|
945
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
946
|
+
const archiveFile = path7.join(archiveDir, `telemetry-${timestamp}.jsonl`);
|
|
947
|
+
try {
|
|
948
|
+
fs5.renameSync(activeFile, archiveFile);
|
|
949
|
+
return {
|
|
950
|
+
rotated: true,
|
|
951
|
+
archivePath: archiveFile,
|
|
952
|
+
bytesRotated: stats.size
|
|
953
|
+
};
|
|
954
|
+
} catch (err) {
|
|
955
|
+
return { rotated: false, reason: `Rename failed: ${err.message}` };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Lists all archived telemetry segments.
|
|
960
|
+
*/
|
|
961
|
+
static listArchivedSegments(rootDir) {
|
|
962
|
+
const archiveDir = path7.join(rootDir, ".hardkas", "telemetry", "archive");
|
|
963
|
+
if (!fs5.existsSync(archiveDir)) return [];
|
|
964
|
+
return fs5.readdirSync(archiveDir).filter((f) => f.startsWith("telemetry-") && f.endsWith(".jsonl")).sort();
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
558
968
|
// src/runtime-context.ts
|
|
559
969
|
var systemRuntimeContext = {
|
|
560
970
|
clock: {
|
|
@@ -566,6 +976,210 @@ var systemRuntimeContext = {
|
|
|
566
976
|
ids: {
|
|
567
977
|
execution: () => `exec_${Date.now().toString(36)}`,
|
|
568
978
|
workflow: () => `wf_${Date.now().toString(36)}`
|
|
979
|
+
},
|
|
980
|
+
telemetry: globalTelemetry
|
|
981
|
+
};
|
|
982
|
+
|
|
983
|
+
// src/semantics/status.ts
|
|
984
|
+
var LEGAL_TRANSITIONS = {
|
|
985
|
+
UNKNOWN: ["PROJECTED", "QUARANTINED", "CORRUPTED"],
|
|
986
|
+
PROJECTED: ["VERIFIED", "CORRUPTED"],
|
|
987
|
+
VERIFIED: ["STALE", "REPLAY_VERIFIED", "CORRUPTED"],
|
|
988
|
+
STALE: ["VERIFIED", "REPLAY_VERIFIED", "CORRUPTED"],
|
|
989
|
+
REPLAY_VERIFIED: ["STALE", "CORRUPTED"],
|
|
990
|
+
CORRUPTED: ["QUARANTINED"],
|
|
991
|
+
QUARANTINED: []
|
|
992
|
+
// Terminal state
|
|
993
|
+
};
|
|
994
|
+
function validateStatusTransition(from, to) {
|
|
995
|
+
if (from === to) return;
|
|
996
|
+
const allowed = LEGAL_TRANSITIONS[from];
|
|
997
|
+
if (!allowed.includes(to)) {
|
|
998
|
+
throw new Error(
|
|
999
|
+
`[CRITICAL SEMANTIC ERROR] Illegal artifact status transition attempted: ${from} -> ${to}. This is a violation of the semantic artifact status lattice.`
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// src/semantics/api.ts
|
|
1005
|
+
function resolveCanonicalArtifact(params) {
|
|
1006
|
+
if (!params.artifactId && !params.lineageId && !params.semanticHash) {
|
|
1007
|
+
throw new Error(
|
|
1008
|
+
`[CRITICAL SEMANTIC ERROR] Implicit resolution forbidden. You must pin resolution by providing an explicit artifactId, lineageId, or semanticHash.`
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
return params.artifactId || params.semanticHash || params.lineageId || "";
|
|
1012
|
+
}
|
|
1013
|
+
function verifyArtifactIntegrity(identity, computedHash) {
|
|
1014
|
+
if (identity.semanticHash !== computedHash) {
|
|
1015
|
+
throw new Error(
|
|
1016
|
+
`[CRITICAL SEMANTIC ERROR] Integrity mismatch for artifact ${identity.artifactId}: expected hash ${identity.semanticHash}, got ${computedHash}`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function verifyReplay(identity, replayCtx) {
|
|
1021
|
+
if (identity.semanticHash !== replayCtx.semanticHash) {
|
|
1022
|
+
throw new Error(`[CRITICAL SEMANTIC ERROR] Replay semantic hash divergence: expected ${identity.semanticHash}, got ${replayCtx.semanticHash}`);
|
|
1023
|
+
}
|
|
1024
|
+
validateStatusTransition(identity.status, "REPLAY_VERIFIED");
|
|
1025
|
+
return "REPLAY_VERIFIED";
|
|
1026
|
+
}
|
|
1027
|
+
function verifyProjectionFreshness(identity, currentLineageHead) {
|
|
1028
|
+
return true;
|
|
1029
|
+
}
|
|
1030
|
+
function classifyArtifactStatus(identity, isReadable, isCorrupted) {
|
|
1031
|
+
if (isCorrupted) return "CORRUPTED";
|
|
1032
|
+
if (!isReadable) return "UNKNOWN";
|
|
1033
|
+
if (identity.status === "UNKNOWN") return "PROJECTED";
|
|
1034
|
+
return identity.status;
|
|
1035
|
+
}
|
|
1036
|
+
function resolveLineage(artifactId) {
|
|
1037
|
+
return [artifactId];
|
|
1038
|
+
}
|
|
1039
|
+
function verifyCapabilityBoundary(identity, capability) {
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
// src/semantics/migration.ts
|
|
1043
|
+
function verifyMigrationIntegrity(preMigration, postMigration) {
|
|
1044
|
+
if (preMigration.artifactId !== postMigration.artifactId) {
|
|
1045
|
+
throw new Error(`[CRITICAL SEMANTIC ERROR] Migration unexpectedly altered canonical artifact ID: ${preMigration.artifactId} -> ${postMigration.artifactId}`);
|
|
1046
|
+
}
|
|
1047
|
+
if (postMigration.schemaVersion < preMigration.schemaVersion) {
|
|
1048
|
+
throw new Error(`[CRITICAL SEMANTIC ERROR] Invalid migration: schema downgraded from ${preMigration.schemaVersion} to ${postMigration.schemaVersion}`);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function migrateArtifact(identity, targetVersion) {
|
|
1052
|
+
if (identity.schemaVersion === targetVersion) {
|
|
1053
|
+
return { migratedIdentity: identity, success: true };
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
migratedIdentity: identity,
|
|
1057
|
+
success: false,
|
|
1058
|
+
error: `No migration path from ${identity.schemaVersion} to ${targetVersion}`
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
function comparePrePostMigrationLineage(preLineageId, postLineageId) {
|
|
1062
|
+
if (preLineageId !== postLineageId) {
|
|
1063
|
+
throw new Error(`[CRITICAL SEMANTIC ERROR] Lineage broken across schema migration: ${preLineageId} -> ${postLineageId}`);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/semantics/drift.ts
|
|
1068
|
+
function detectSemanticDrift(dashboardView, queryStoreView, replayView, filesystemView) {
|
|
1069
|
+
const views = {
|
|
1070
|
+
Dashboard: dashboardView,
|
|
1071
|
+
QueryStore: queryStoreView,
|
|
1072
|
+
Replay: replayView,
|
|
1073
|
+
Filesystem: filesystemView
|
|
1074
|
+
};
|
|
1075
|
+
let referenceView = filesystemView;
|
|
1076
|
+
for (const [subsystem, view] of Object.entries(views)) {
|
|
1077
|
+
if (view.semanticHash !== referenceView.semanticHash) {
|
|
1078
|
+
return {
|
|
1079
|
+
hasDrift: true,
|
|
1080
|
+
conflictingSubsystem: subsystem,
|
|
1081
|
+
exactReplayCommand: `hardkas verify-replay --artifact ${referenceView.artifactId}`,
|
|
1082
|
+
severity: "CRITICAL",
|
|
1083
|
+
details: `Hash mismatch: ${subsystem} (${view.semanticHash}) vs Reference (${referenceView.semanticHash})`
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
if (view.status !== referenceView.status) {
|
|
1087
|
+
if (subsystem === "Dashboard" && view.status === "VERIFIED" && replayView.status === "STALE") {
|
|
1088
|
+
return {
|
|
1089
|
+
hasDrift: true,
|
|
1090
|
+
conflictingSubsystem: subsystem,
|
|
1091
|
+
exactReplayCommand: `hardkas verify-replay --artifact ${referenceView.artifactId}`,
|
|
1092
|
+
severity: "CRITICAL",
|
|
1093
|
+
details: `Dashboard claims VERIFIED but Replay claims STALE.`
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return { hasDrift: false, severity: "NONE" };
|
|
1099
|
+
}
|
|
1100
|
+
function assertNoSemanticDrift(dashboardView, queryStoreView, replayView, filesystemView) {
|
|
1101
|
+
const report = detectSemanticDrift(dashboardView, queryStoreView, replayView, filesystemView);
|
|
1102
|
+
if (report.hasDrift) {
|
|
1103
|
+
throw new Error(
|
|
1104
|
+
`[CRITICAL SEMANTIC DRIFT] Subsystem disagreement detected.
|
|
1105
|
+
Conflicting Subsystem: ${report.conflictingSubsystem}
|
|
1106
|
+
Details: ${report.details}
|
|
1107
|
+
Resolution Command: ${report.exactReplayCommand}`
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// src/migrations.ts
|
|
1113
|
+
import fs6 from "fs";
|
|
1114
|
+
import path8 from "path";
|
|
1115
|
+
var CURRENT_RUNTIME_VERSION = "0.7.0-alpha";
|
|
1116
|
+
var MIN_SUPPORTED_VERSION = "0.5.0-alpha";
|
|
1117
|
+
var MigrationManager = class {
|
|
1118
|
+
static checkVersion(rootDir) {
|
|
1119
|
+
const versionFile = path8.join(rootDir, ".hardkas", "version.json");
|
|
1120
|
+
if (!fs6.existsSync(versionFile)) {
|
|
1121
|
+
this.writeVersion(rootDir, CURRENT_RUNTIME_VERSION);
|
|
1122
|
+
return { needsMigration: false, canDowngrade: true, currentVersion: CURRENT_RUNTIME_VERSION };
|
|
1123
|
+
}
|
|
1124
|
+
try {
|
|
1125
|
+
const data = JSON.parse(fs6.readFileSync(versionFile, "utf-8"));
|
|
1126
|
+
const wsVersion = data.runtimeVersion || "0.0.0";
|
|
1127
|
+
if (wsVersion === CURRENT_RUNTIME_VERSION) {
|
|
1128
|
+
return { needsMigration: false, canDowngrade: true, currentVersion: wsVersion };
|
|
1129
|
+
}
|
|
1130
|
+
if (this.compareSemver(wsVersion, CURRENT_RUNTIME_VERSION) > 0) {
|
|
1131
|
+
return { needsMigration: false, canDowngrade: false, currentVersion: wsVersion };
|
|
1132
|
+
}
|
|
1133
|
+
if (this.compareSemver(wsVersion, MIN_SUPPORTED_VERSION) < 0) {
|
|
1134
|
+
throw new HardkasError("MIGRATION_UNSUPPORTED", `Workspace version ${wsVersion} is too old to migrate to ${CURRENT_RUNTIME_VERSION}`);
|
|
1135
|
+
}
|
|
1136
|
+
return { needsMigration: true, canDowngrade: true, currentVersion: wsVersion };
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
if (err instanceof HardkasError) throw err;
|
|
1139
|
+
throw new HardkasError("MIGRATION_ERROR", `Failed to parse version.json: ${err.message}`);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
static migrate(rootDir, dryRun = false) {
|
|
1143
|
+
const status = this.checkVersion(rootDir);
|
|
1144
|
+
if (!status.canDowngrade) {
|
|
1145
|
+
throw new HardkasError("DOWNGRADE_REFUSED", `Cannot safely downgrade from workspace version ${status.currentVersion} to runtime version ${CURRENT_RUNTIME_VERSION}.`);
|
|
1146
|
+
}
|
|
1147
|
+
if (!status.needsMigration) return;
|
|
1148
|
+
if (dryRun) {
|
|
1149
|
+
console.log(`[DRY-RUN] Would migrate workspace from ${status.currentVersion} to ${CURRENT_RUNTIME_VERSION}`);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
this.backupWorkspace(rootDir);
|
|
1153
|
+
try {
|
|
1154
|
+
this.writeVersion(rootDir, CURRENT_RUNTIME_VERSION);
|
|
1155
|
+
} catch (err) {
|
|
1156
|
+
getTelemetry().logAnomaly("EXTERNAL_MUTATION", "critical", "projection", `Migration failed: ${err.message}`);
|
|
1157
|
+
throw new HardkasError("MIGRATION_FAILED", `Migration failed, workspace might be corrupted: ${err.message}`);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
static writeVersion(rootDir, version) {
|
|
1161
|
+
const versionFile = path8.join(rootDir, ".hardkas", "version.json");
|
|
1162
|
+
if (!fs6.existsSync(path8.dirname(versionFile))) {
|
|
1163
|
+
fs6.mkdirSync(path8.dirname(versionFile), { recursive: true });
|
|
1164
|
+
}
|
|
1165
|
+
fs6.writeFileSync(versionFile, JSON.stringify({ runtimeVersion: version }, null, 2));
|
|
1166
|
+
}
|
|
1167
|
+
static backupWorkspace(rootDir) {
|
|
1168
|
+
const hardkasDir = path8.join(rootDir, ".hardkas");
|
|
1169
|
+
const backupDir = path8.join(rootDir, `.hardkas-backup-${Date.now()}`);
|
|
1170
|
+
if (fs6.existsSync(hardkasDir)) {
|
|
1171
|
+
fs6.cpSync(hardkasDir, backupDir, { recursive: true });
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
static compareSemver(v1, v2) {
|
|
1175
|
+
const parse = (v) => v.replace("-alpha", "").split(".").map(Number);
|
|
1176
|
+
const p1 = parse(v1);
|
|
1177
|
+
const p2 = parse(v2);
|
|
1178
|
+
for (let i = 0; i < 3; i++) {
|
|
1179
|
+
if ((p1[i] || 0) > (p2[i] || 0)) return 1;
|
|
1180
|
+
if ((p1[i] || 0) < (p2[i] || 0)) return -1;
|
|
1181
|
+
}
|
|
1182
|
+
return 0;
|
|
569
1183
|
}
|
|
570
1184
|
};
|
|
571
1185
|
|
|
@@ -661,13 +1275,20 @@ function formatSompi(amountSompi) {
|
|
|
661
1275
|
return `${sign}${whole}.${fractional.toString().padStart(8, "0")} KAS`;
|
|
662
1276
|
}
|
|
663
1277
|
export {
|
|
1278
|
+
AppendCoordinator,
|
|
664
1279
|
ArtifactTypeSchema,
|
|
1280
|
+
CURRENT_RUNTIME_VERSION,
|
|
1281
|
+
EnvironmentTelemetry,
|
|
665
1282
|
ExecutionModeSchema,
|
|
666
1283
|
HardkasError,
|
|
667
1284
|
InvariantViolationError,
|
|
668
1285
|
LOCK_ORDER,
|
|
1286
|
+
MIN_SUPPORTED_VERSION,
|
|
1287
|
+
MigrationManager,
|
|
669
1288
|
NetworkIdSchema,
|
|
670
1289
|
SOMPI_PER_KAS,
|
|
1290
|
+
TelemetryManager,
|
|
1291
|
+
TelemetryRotator,
|
|
671
1292
|
acquireLock,
|
|
672
1293
|
artifactTypeSchema,
|
|
673
1294
|
asArtifactId,
|
|
@@ -682,26 +1303,43 @@ export {
|
|
|
682
1303
|
asRpcEndpointId,
|
|
683
1304
|
asTxId,
|
|
684
1305
|
asWorkflowId,
|
|
1306
|
+
assertNoSemanticDrift,
|
|
1307
|
+
attachLedgerAppender,
|
|
1308
|
+
classifyArtifactStatus,
|
|
685
1309
|
clearLock,
|
|
1310
|
+
comparePrePostMigrationLineage,
|
|
686
1311
|
coreEvents,
|
|
687
1312
|
createEventEnvelope,
|
|
688
1313
|
createSnapshot,
|
|
1314
|
+
detectSemanticDrift,
|
|
689
1315
|
deterministicCompare,
|
|
690
1316
|
diffReplays,
|
|
691
1317
|
executionModeSchema,
|
|
692
1318
|
formatCorruptionIssue,
|
|
693
1319
|
formatSompi,
|
|
1320
|
+
getTelemetry,
|
|
1321
|
+
globalTelemetry,
|
|
694
1322
|
hardkasConfigSchema,
|
|
695
1323
|
isProcessAlive,
|
|
696
1324
|
kaspaNetworkIdSchema,
|
|
697
1325
|
listLocks,
|
|
698
1326
|
maskSecrets,
|
|
1327
|
+
migrateArtifact,
|
|
699
1328
|
parseHardkasConfig,
|
|
700
1329
|
parseKasToSompi,
|
|
701
1330
|
readSnapshotManifest,
|
|
702
1331
|
redactSecret,
|
|
1332
|
+
resolveCanonicalArtifact,
|
|
1333
|
+
resolveLineage,
|
|
703
1334
|
systemRuntimeContext,
|
|
1335
|
+
telemetryContextStorage,
|
|
704
1336
|
validateEventEnvelope,
|
|
1337
|
+
validateStatusTransition,
|
|
1338
|
+
verifyArtifactIntegrity,
|
|
1339
|
+
verifyCapabilityBoundary,
|
|
1340
|
+
verifyMigrationIntegrity,
|
|
1341
|
+
verifyProjectionFreshness,
|
|
1342
|
+
verifyReplay,
|
|
705
1343
|
withLock,
|
|
706
1344
|
withLocks,
|
|
707
1345
|
writeFileAtomic,
|