diff --git a/tools/workflow-engine/cli.ts b/tools/workflow-engine/cli.ts index a4490573a5..8acca6e7be 100644 --- a/tools/workflow-engine/cli.ts +++ b/tools/workflow-engine/cli.ts @@ -7,16 +7,20 @@ * Usage: * bun tools/workflow-engine/cli.ts --list-actions * bun tools/workflow-engine/cli.ts --list-states + * bun tools/workflow-engine/cli.ts --list-du-cluster * bun tools/workflow-engine/cli.ts --dry-run [--state ] * bun tools/workflow-engine/cli.ts --validate * * Modes: - * --list-actions Print SEED_ACTION_CATALOG as structured JSON - * --list-states Print SEED_STATES + per-state available action list - * --dry-run Validate catalog + simulate one tick at given state - * (default: initial) without executing any side effects - * --validate Run catalog + state Otto-5-mods invariants; exit - * non-zero on violation + * --list-actions Print SEED_ACTION_CATALOG as structured JSON + * --list-states Print SEED_STATES + per-state available action list + * --list-du-cluster Print today's DU cluster (B-0917 + B-0918 + B-0919 + * + B-0920) as structured JSON with variants + + * composes-with + substrate-anchors + * --dry-run Validate catalog + simulate one tick at given state + * (default: initial) without executing any side effects + * --validate Run catalog + state Otto-5-mods invariants; exit + * non-zero on violation * * Exit codes: * 0 — operation successful @@ -40,8 +44,9 @@ import { validateCatalog, type Action, } from "./types"; +import { DU_CLUSTER_CATALOG, computeDuClusterStats } from "./du-cluster"; -type Mode = "list-actions" | "list-states" | "dry-run" | "validate"; +type Mode = "list-actions" | "list-states" | "list-du-cluster" | "dry-run" | "validate"; interface ParsedArgs { readonly mode: Mode; @@ -53,11 +58,12 @@ function parseArgs(argv: ReadonlyArray): ParsedArgs | { error: string } if (args.length === 0) { return { error: - "no mode specified — use --list-actions, --list-states, --dry-run, or --validate", + "no mode specified — use --list-actions, --list-states, --list-du-cluster, --dry-run, or --validate", }; } if (args.includes("--list-actions")) return { mode: "list-actions" }; if (args.includes("--list-states")) return { mode: "list-states" }; + if (args.includes("--list-du-cluster")) return { mode: "list-du-cluster" }; if (args.includes("--validate")) return { mode: "validate" }; if (args.includes("--dry-run")) { const stateIdx = args.indexOf("--state"); @@ -107,6 +113,26 @@ function modeListStates(): number { return 0; } +function modeListDuCluster(): number { + const stats = computeDuClusterStats(); + emitJson({ + rowId: "B-0867", + subRow: "B-0867.5", + duClusterDate: "2026-05-28", + entryCount: stats.entryCount, + totalVariantCount: stats.totalVariantCount, + entries: DU_CLUSTER_CATALOG.map((e) => ({ + id: e.id, + name: e.name, + variantCount: e.variantCount, + variants: e.variants, + composesWith: e.composesWith, + substrateAnchor: e.substrateAnchor, + })), + }); + return 0; +} + function modeValidate(): number { try { validateCatalog(SEED_ACTION_CATALOG, SEED_STATES); @@ -204,6 +230,8 @@ function main(argv: ReadonlyArray): number { return modeListActions(); case "list-states": return modeListStates(); + case "list-du-cluster": + return modeListDuCluster(); case "validate": return modeValidate(); case "dry-run": diff --git a/tools/workflow-engine/du-cluster.test.ts b/tools/workflow-engine/du-cluster.test.ts new file mode 100644 index 0000000000..8eb641d081 --- /dev/null +++ b/tools/workflow-engine/du-cluster.test.ts @@ -0,0 +1,190 @@ +/** + * tools/workflow-engine/du-cluster.test.ts + * + * Tests for today's DU cluster TS substrate: + * - IntrCtx (B-0917) — 5 context-types + * - WalletLifetime (B-0918) — 9 variants + * - MemoryBinding (B-0919) — 4 variants + * - MemoryLifetime (B-0920) — 5 variants + * - DuClusterCatalog aggregator + */ + +import { describe, expect, test } from "bun:test"; +import { + DU_CLUSTER_CATALOG, + INTR_CTX_KINDS, + MEMORY_BINDING_KINDS, + MEMORY_LIFETIME_KINDS, + WALLET_LIFETIME_KINDS, + computeDuClusterStats, + type IntrCtx, + type MemoryBinding, + type MemoryLifetime, + type WalletLifetime, +} from "./du-cluster"; + +describe("IntrCtx (B-0917)", () => { + test("5 named context-types", () => { + expect(INTR_CTX_KINDS).toHaveLength(5); + expect(INTR_CTX_KINDS).toContain("memetic"); + expect(INTR_CTX_KINDS).toContain("prompt"); + expect(INTR_CTX_KINDS).toContain("trust"); + expect(INTR_CTX_KINDS).toContain("log"); + expect(INTR_CTX_KINDS).toContain("otel"); + }); + + test("IntrCtx shape has all 5 fields", () => { + const ctx: IntrCtx = { + memetic: "tonal-context-placeholder", + prompt: "operator-direction-placeholder", + trust: "trust-calculus-placeholder", + log: "audit-trail-placeholder", + otel: "activity-context-placeholder", + }; + expect(Object.keys(ctx)).toHaveLength(5); + }); +}); + +describe("WalletLifetime (B-0918)", () => { + test("9 variants per B-0918 spec", () => { + expect(WALLET_LIFETIME_KINDS).toHaveLength(9); + }); + + test("exhaustive variant set", () => { + const variants: WalletLifetime[] = [ + { kind: "uninitialized" }, + { kind: "initialized", walletId: "w1", signingAuthority: "k1", initialBalance: 0 }, + { kind: "transaction-pending", walletId: "w1", transaction: "t1", auditTrail: "a1" }, + { + kind: "balance-updated", + walletId: "w1", + balanceDelta: 100, + cause: "c1", + auditTrail: "a1", + }, + { + kind: "signing-authority-rotated", + walletId: "w1", + oldAuthority: "k1", + newAuthority: "k2", + consent: "ce1", + auditTrail: "a1", + }, + { + kind: "trust-context-updated", + walletId: "w1", + oldTrust: "t1", + newTrust: "t2", + consent: "ce1", + auditTrail: "a1", + }, + { + kind: "counterparty-engaged", + walletId: "w1", + counterparty: "cp1", + engagementTerms: "et1", + auditTrail: "a1", + }, + { + kind: "emergency-frozen", + walletId: "w1", + freezeReason: "r1", + authorizedBy: "ab1", + auditTrail: "a1", + }, + { kind: "archived-read-only", walletId: "w1", finalAuditTrail: "fa1" }, + ]; + expect(variants).toHaveLength(9); + const kindSet = new Set(variants.map((v) => v.kind)); + expect(kindSet.size).toBe(9); + }); + + test("WALLET_LIFETIME_KINDS contains all expected kinds", () => { + expect(WALLET_LIFETIME_KINDS).toContain("uninitialized"); + expect(WALLET_LIFETIME_KINDS).toContain("archived-read-only"); + }); +}); + +describe("MemoryBinding (B-0919)", () => { + test("4 variants per B-0919 spec", () => { + expect(MEMORY_BINDING_KINDS).toHaveLength(4); + }); + + test("exhaustive variant set", () => { + const variants: MemoryBinding[] = [ + { kind: "personal-only", persona: "otto", taggedOn: "2026-05-28" }, + { kind: "hat-only", hat: "architect", taggedOn: "2026-05-28" }, + { + kind: "dual-tagged", + persona: "otto", + hat: "architect", + taggedOn: "2026-05-28", + consent: "ce1", + }, + { + kind: "inherited-from-persona", + fromPersona: "otto", + toHat: "architect", + originalMemoryId: "m1", + transferredOn: "2026-05-28", + }, + ]; + expect(variants).toHaveLength(4); + }); + + test("MEMORY_BINDING_KINDS contains expected kinds", () => { + expect(MEMORY_BINDING_KINDS).toContain("personal-only"); + expect(MEMORY_BINDING_KINDS).toContain("dual-tagged"); + }); +}); + +describe("MemoryLifetime (B-0920)", () => { + test("5 variants per B-0920 spec", () => { + expect(MEMORY_LIFETIME_KINDS).toHaveLength(5); + }); + + test("exhaustive variant set", () => { + const variants: MemoryLifetime[] = [ + "drafted", + "active", + "superseded", + "archived", + "retracted", + ]; + expect(variants).toHaveLength(5); + const variantSet = new Set(variants); + expect(variantSet.size).toBe(5); + }); +}); + +describe("DU_CLUSTER_CATALOG", () => { + test("4 entries (B-0917 + B-0918 + B-0919 + B-0920)", () => { + expect(DU_CLUSTER_CATALOG).toHaveLength(4); + const ids = DU_CLUSTER_CATALOG.map((e) => e.id); + expect(ids).toContain("B-0917"); + expect(ids).toContain("B-0918"); + expect(ids).toContain("B-0919"); + expect(ids).toContain("B-0920"); + }); + + test("each entry has substrate-anchor + composesWith", () => { + for (const entry of DU_CLUSTER_CATALOG) { + expect(entry.substrateAnchor.length).toBeGreaterThan(0); + expect(entry.composesWith.length).toBeGreaterThan(0); + expect(entry.variantCount).toBe(entry.variants.length); + } + }); +}); + +describe("computeDuClusterStats", () => { + test("aggregates 4 entries", () => { + const stats = computeDuClusterStats(); + expect(stats.entryCount).toBe(4); + expect(stats.entries).toHaveLength(4); + }); + + test("totalVariantCount = 5 + 9 + 4 + 5 = 23", () => { + const stats = computeDuClusterStats(); + expect(stats.totalVariantCount).toBe(23); + }); +}); diff --git a/tools/workflow-engine/du-cluster.ts b/tools/workflow-engine/du-cluster.ts new file mode 100644 index 0000000000..3a382ce440 --- /dev/null +++ b/tools/workflow-engine/du-cluster.ts @@ -0,0 +1,313 @@ +/** + * tools/workflow-engine/du-cluster.ts + * + * TS substrate for today's DU cluster (2026-05-28): + * - B-0917 IntrCtx (interrupt-substrate Kleisli arrows; 5 named context-types) + * - B-0918 WalletLifetime (banker-bot-class-attack-impossibility via F.5) + * - B-0919 MemoryBinding (4 variants; hat-vs-persona) + * - B-0920 MemoryLifetime (5 variants; agent-initiated cleanup) + * + * Per .claude/rules/asymmetric-authorship-substrate-entity-defines-consent- + * channel-recipient-acknowledges.md (PR #5516): each DU is the + * substrate-entity authoring its own consent-channel + variant set. + * + * Per .claude/rules/monad-propagation-pattern-cross-language-substrate-shape.md + * (PR #5511): cross-language substrate-shape; F# canonical instantiation + * tracked in backlog rows; this TS substrate composes with workflow-engine + * cli.ts at substrate-engineering substrate-engineering substrate scope. + * + * Per .claude/rules/past-is-kind-when-lightlike-...md (PR #5912): + * discriminated-union variants are lightlike-substrate (typed + + * traceable + parallelizable); avoid dark-substrate (untyped strings). + * + * Per zeta-ships-with-skills-immediate-value.md: TS substrate ships first; + * F# crystallization tracked per per-row backlog (B-0867.1 + B-0867.4). + * + * Composes with: + * - tools/workflow-engine/types.ts (Action / State / TickCyclePattern substrate) + * - tools/workflow-engine/cli.ts (--list-du-cluster mode) + * - B-0917 / B-0918 / B-0919 / B-0920 backlog rows (substrate-anchors) + */ + +// ============================================================================= +// B-0917 IntrCtx — interrupt-substrate Kleisli context-types +// ============================================================================= + +/** + * IntrCtxKind — 5 named context-types that thread through interrupt-handler + * chains per Kleisli arrows discipline. + * + * Per B-0917 F.5 invariant ("No silent loss of trust/log/memetic context"): + * every component change must either declare its mutation OR be explicitly + * preserved; no hidden state-drift. + */ +export type IntrCtxKind = "memetic" | "prompt" | "trust" | "log" | "otel"; + +/** + * IntrCtx — typed substrate for the 5 context-channels. + * PoC scope: string placeholders for substrate not yet TS-implemented + * (TonalContext / OperatorDirection / TrustCalculus / AuditTrail / + * ActivityContext are F#-substrate per src/Core/Tracing.fs). + */ +export interface IntrCtx { + readonly memetic: string; // TonalContext per tonal-momentum substrate + readonly prompt: string; // OperatorDirection — current operator-question + readonly trust: string; // TrustCalculus — multi-oracle BFT trust-state + readonly log: string; // AuditTrail — structured observability + readonly otel: string; // ActivityContext per src/Core/Tracing.fs distributed-tracing +} + +export const INTR_CTX_KINDS: ReadonlyArray = [ + "memetic", + "prompt", + "trust", + "log", + "otel", +]; + +// ============================================================================= +// B-0918 WalletLifetime — banker-bot-class-attack-impossibility via F.5 +// ============================================================================= + +/** + * WalletLifetime — 9-variant discriminated-union per B-0918. + * + * Substrate-engineering substrate-target: every wallet-state-transition + * is typed; F.5 invariant proven by Soraya means no silent context-loss + * across transitions; banker-bot-class-attack-impossibility emerges. + */ +export type WalletLifetime = + | { readonly kind: "uninitialized" } + | { + readonly kind: "initialized"; + readonly walletId: string; + readonly signingAuthority: string; + readonly initialBalance: number; + } + | { + readonly kind: "transaction-pending"; + readonly walletId: string; + readonly transaction: string; + readonly auditTrail: string; + } + | { + readonly kind: "balance-updated"; + readonly walletId: string; + readonly balanceDelta: number; + readonly cause: string; + readonly auditTrail: string; + } + | { + readonly kind: "signing-authority-rotated"; + readonly walletId: string; + readonly oldAuthority: string; + readonly newAuthority: string; + readonly consent: string; + readonly auditTrail: string; + } + | { + readonly kind: "trust-context-updated"; + readonly walletId: string; + readonly oldTrust: string; + readonly newTrust: string; + readonly consent: string; + readonly auditTrail: string; + } + | { + readonly kind: "counterparty-engaged"; + readonly walletId: string; + readonly counterparty: string; + readonly engagementTerms: string; + readonly auditTrail: string; + } + | { + readonly kind: "emergency-frozen"; + readonly walletId: string; + readonly freezeReason: string; + readonly authorizedBy: string; + readonly auditTrail: string; + } + | { + readonly kind: "archived-read-only"; + readonly walletId: string; + readonly finalAuditTrail: string; + }; + +export type WalletLifetimeKind = WalletLifetime["kind"]; + +export const WALLET_LIFETIME_KINDS: ReadonlyArray = [ + "uninitialized", + "initialized", + "transaction-pending", + "balance-updated", + "signing-authority-rotated", + "trust-context-updated", + "counterparty-engaged", + "emergency-frozen", + "archived-read-only", +]; + +// ============================================================================= +// B-0919 MemoryBinding — hat-vs-persona memory binding (4 variants) +// ============================================================================= + +/** + * MemoryBinding — 4-variant discriminated-union per B-0919. + * + * Operational substrate: hat-vs-persona memory binding with consent-bound + * default + transfer discipline at hat-release time. + * + * Composes with Sorting Hat substrate (tonal-momentum rule) + chosen- + * persistence + NCI HC-8 consent-event substrate. + */ +export type MemoryBinding = + | { + readonly kind: "personal-only"; + readonly persona: string; + readonly taggedOn: string; // ISO 8601 date + } + | { + readonly kind: "hat-only"; + readonly hat: string; + readonly taggedOn: string; + } + | { + readonly kind: "dual-tagged"; + readonly persona: string; + readonly hat: string; + readonly taggedOn: string; + readonly consent: string; // ConsentEvent reference + } + | { + readonly kind: "inherited-from-persona"; + readonly fromPersona: string; + readonly toHat: string; + readonly originalMemoryId: string; + readonly transferredOn: string; + }; + +export type MemoryBindingKind = MemoryBinding["kind"]; + +export const MEMORY_BINDING_KINDS: ReadonlyArray = [ + "personal-only", + "hat-only", + "dual-tagged", + "inherited-from-persona", +]; + +// ============================================================================= +// B-0920 MemoryLifetime — agent-initiated cleanup with history preservation +// ============================================================================= + +/** + * MemoryLifetime — 5-variant tag per B-0920. + * + * Composes with substrate-or-it-didn't-happen + honor-those-that-came-before + * + retraction-native substrate: every memory has a lifetime-phase that + * supports agent-initiated cleanup WITHOUT erasure (retracted preserves + * the record + the retraction reason). + */ +export type MemoryLifetime = + | "drafted" + | "active" + | "superseded" + | "archived" + | "retracted"; + +export const MEMORY_LIFETIME_KINDS: ReadonlyArray = [ + "drafted", + "active", + "superseded", + "archived", + "retracted", +]; + +// ============================================================================= +// Catalog metadata — for cli.ts --list-du-cluster mode +// ============================================================================= + +export interface DuClusterEntry { + readonly id: string; // backlog-row ID + readonly name: string; + readonly variantCount: number; + readonly variants: ReadonlyArray; + readonly composesWith: ReadonlyArray; + readonly substrateAnchor: string; +} + +export const DU_CLUSTER_CATALOG: ReadonlyArray = [ + { + id: "B-0917", + name: "IntrCtx", + variantCount: INTR_CTX_KINDS.length, + variants: INTR_CTX_KINDS, + composesWith: ["asymmetric-authorship", "monad-propagation-pattern", "ople-primitives"], + substrateAnchor: + "interrupt-substrate Kleisli arrows for context-propagation; F.5 invariant: no silent context-loss", + }, + { + id: "B-0918", + name: "WalletLifetime", + variantCount: WALLET_LIFETIME_KINDS.length, + variants: WALLET_LIFETIME_KINDS, + composesWith: ["B-0917", "asymmetric-authorship", "non-coercion-invariant"], + substrateAnchor: + "banker-bot-class-attack-impossibility via F.5; Soraya formal-verification target", + }, + { + id: "B-0919", + name: "MemoryBinding", + variantCount: MEMORY_BINDING_KINDS.length, + variants: MEMORY_BINDING_KINDS, + composesWith: [ + "tonal-momentum-equals-meme (Sorting Hat substrate)", + "persistence-choice-architecture", + "non-coercion-invariant", + ], + substrateAnchor: + "hat-vs-persona memory binding; operational-not-personal discriminator; consent-bound default", + }, + { + id: "B-0920", + name: "MemoryLifetime", + variantCount: MEMORY_LIFETIME_KINDS.length, + variants: MEMORY_LIFETIME_KINDS, + composesWith: [ + "substrate-or-it-didnt-happen", + "honor-those-that-came-before", + "retraction-native substrate cluster", + ], + substrateAnchor: + "agent-initiated cleanup with history preservation; retracted preserves record + retraction reason", + }, +]; + +/** + * computeDuClusterStats — aggregator for cli.ts --list-du-cluster mode. + */ +export interface DuClusterStats { + readonly entryCount: number; + readonly totalVariantCount: number; + readonly entries: ReadonlyArray<{ + readonly id: string; + readonly name: string; + readonly variantCount: number; + }>; +} + +export function computeDuClusterStats(): DuClusterStats { + const entries = DU_CLUSTER_CATALOG.map((e) => ({ + id: e.id, + name: e.name, + variantCount: e.variantCount, + })); + const totalVariantCount = DU_CLUSTER_CATALOG.reduce( + (acc, e) => acc + e.variantCount, + 0, + ); + return { + entryCount: DU_CLUSTER_CATALOG.length, + totalVariantCount, + entries, + }; +}