Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions core/archive/__tests__/archive.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { describe, expect, it } from "vitest";
import { archiveRecord, restoreRecord, type ArchiveRecord, type ArchiveStore } from "@/core/archive";
describe("lifecycle archival", () => { it("archives and restores idempotently", () => { let value: ArchiveRecord<string> = { id: "r1", schemaVersion: 1, state: "active", value: "safe" }; const store: ArchiveStore<string> = { read: () => value, write: (next) => { value = next; } }; expect(archiveRecord(store, "r1", "2026-01-01").state).toBe("archived"); expect(archiveRecord(store, "r1")).toEqual(value); expect(restoreRecord(store, "r1", "2026-01-02").state).toBe("restored"); }); });
13 changes: 13 additions & 0 deletions core/archive/archive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export type ArchiveState = "active" | "archived" | "restored";
export interface ArchiveRecord<T> { readonly id: string; readonly schemaVersion: number; readonly state: ArchiveState; readonly value: T; readonly archivedAt?: string; readonly restoredAt?: string; }
export interface ArchiveStore<T> { read(id: string): ArchiveRecord<T> | undefined; write(record: ArchiveRecord<T>): void; }

export function archiveRecord<T>(store: ArchiveStore<T>, id: string, now = new Date().toISOString()): ArchiveRecord<T> | undefined {
const current = store.read(id); if (!current || current.state === "archived") return current;
const archived = { ...current, state: "archived" as const, archivedAt: now }; store.write(archived); return archived;
}
export function restoreRecord<T>(store: ArchiveStore<T>, id: string, now = new Date().toISOString()): ArchiveRecord<T> | undefined {
const current = store.read(id); if (!current || current.state !== "archived") return current;
const restored = { ...current, state: "restored" as const, restoredAt: now }; store.write(restored); return restored;
}
export function isArchived<T>(record: ArchiveRecord<T> | undefined): boolean { return record?.state === "archived"; }
1 change: 1 addition & 0 deletions core/archive/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./archive";
3 changes: 3 additions & 0 deletions core/telemetry/__tests__/protection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { describe, expect, it } from "vitest";
import { protectTelemetry } from "@/core/telemetry/protection";
describe("telemetry protection", () => { it("redacts sensitive fields and samples", () => { expect(protectTelemetry({ user: "u", token: "secret" }, { random: () => 0, sampleRate: 1 }).event?.token).toBe("[REDACTED]"); expect(protectTelemetry({ x: 1 }, { random: () => 1, sampleRate: .5 }).event).toBeNull(); }); });
10 changes: 10 additions & 0 deletions core/telemetry/protection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export interface TelemetryProtectionOptions { readonly sampleRate?: number; readonly maxCardinality?: number; readonly redactKeys?: readonly string[]; readonly random?: () => number; }
export interface ProtectedTelemetry { readonly sampled: boolean; readonly event: Record<string, unknown> | null; }
const DEFAULT_REDACT_KEYS = ["secret", "token", "password", "seed", "authorization", "credential"];
export function protectTelemetry(event: Record<string, unknown>, options: TelemetryProtectionOptions = {}): ProtectedTelemetry {
const rate = Math.min(1, Math.max(0, options.sampleRate ?? 1)); const sampled = (options.random ?? Math.random)() < rate;
if (!sampled) return { sampled: false, event: null };
const keys = new Set((options.redactKeys ?? DEFAULT_REDACT_KEYS).map((key) => key.toLowerCase()));
const entries = Object.entries(event).slice(0, options.maxCardinality ?? 100);
return { sampled: true, event: Object.fromEntries(entries.map(([key, value]) => [key, keys.has(key.toLowerCase()) || /secret|token|password|seed|auth|credential/i.test(key) ? "[REDACTED]" : value])) };
}
4 changes: 4 additions & 0 deletions core/testing/__tests__/chaos-concurrency.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { describe, expect, it } from "vitest";
import { runChaosScenario } from "@/core/testing/chaos";
import { detectLostUpdate } from "@/core/testing/concurrency";
describe("deterministic workflow harnesses", () => { it("reproduces interruption from a seed", async () => { expect((await runChaosScenario([{ id: "a", run: () => undefined }, { id: "b", run: () => undefined }], 7, 1)).interruptedAt).toBe("b"); }); it("reports a lost update", () => { let value = 0; const report = detectLostUpdate({ id: "counter", read: () => value, write: (next) => { value = Number(next); }, mutate: (current) => Number(current) + 1 }); expect(report.lostUpdate).toBe(true); }); });
3 changes: 3 additions & 0 deletions core/testing/chaos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ChaosStep { readonly id: string; readonly run: () => void | Promise<void>; }
export interface ChaosResult { readonly seed: number; readonly completed: readonly string[]; readonly interruptedAt: string | null; }
export async function runChaosScenario(steps: readonly ChaosStep[], seed: number, interruptAfter: number): Promise<ChaosResult> { const completed: string[] = []; for (let i = 0; i < steps.length; i += 1) { if (i === interruptAfter) return { seed, completed, interruptedAt: steps[i].id }; await steps[i].run(); completed.push(steps[i].id); } return { seed, completed, interruptedAt: null }; }
3 changes: 3 additions & 0 deletions core/testing/concurrency.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface ConcurrentMutation { readonly id: string; readonly read: () => unknown; readonly write: (value: unknown) => void; readonly mutate: (value: unknown) => unknown; }
export interface RaceReport { readonly lostUpdate: boolean; readonly finalValue: unknown; readonly expectedValue: unknown; }
export function detectLostUpdate(mutation: ConcurrentMutation): RaceReport { const first = mutation.read(); const second = mutation.read(); const firstNext = mutation.mutate(first); const secondNext = mutation.mutate(second); mutation.write(firstNext); mutation.write(secondNext); return { lostUpdate: Object.is(secondNext, firstNext), finalValue: secondNext, expectedValue: mutation.mutate(firstNext) }; }
8 changes: 8 additions & 0 deletions docs/WORKFLOW_RESILIENCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Workflow resilience

The resilience harnesses are deterministic and dependency-free. `archiveRecord`
and `restoreRecord` make lifecycle transitions explicit and idempotent;
`protectTelemetry` bounds event cardinality, applies field redaction, and uses
an injected sampler; `runChaosScenario` reproduces an interrupted sequence from
a seed; and `detectLostUpdate` exercises the read/modify/write interleaving
that race-prone mutations must protect.