@fiducian/conductor 0.4.0

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.
@@ -0,0 +1,15 @@
1
+ import type { StoryContext } from "./types.js";
2
+ /**
3
+ * ACTORCHESTRA — propagate Fiducia story context across fetch calls automatically.
4
+ */
5
+ export declare class Actorchestra {
6
+ private active?;
7
+ /** Run a block with an active story context (used by fetch wrapper). */
8
+ runWithContext<T>(ctx: StoryContext, fn: () => T): T;
9
+ getActiveContext(): StoryContext | undefined;
10
+ /** Inject X-Fiducia-Context on outbound fetch when a context is active. */
11
+ wrapFetch(baseFetch?: typeof fetch): typeof fetch;
12
+ /** Merge context header into plain header objects (Express/Node handlers). */
13
+ injectHeaders(ctx: StoryContext, headers?: Record<string, string>): Record<string, string>;
14
+ }
15
+ export declare const actorchestra: Actorchestra;
@@ -0,0 +1,37 @@
1
+ import { CONTEXT_HEADER, headerFromContext } from "./types.js";
2
+ /**
3
+ * ACTORCHESTRA — propagate Fiducia story context across fetch calls automatically.
4
+ */
5
+ export class Actorchestra {
6
+ active;
7
+ /** Run a block with an active story context (used by fetch wrapper). */
8
+ runWithContext(ctx, fn) {
9
+ const prev = this.active;
10
+ this.active = ctx;
11
+ try {
12
+ return fn();
13
+ }
14
+ finally {
15
+ this.active = prev;
16
+ }
17
+ }
18
+ getActiveContext() {
19
+ return this.active;
20
+ }
21
+ /** Inject X-Fiducia-Context on outbound fetch when a context is active. */
22
+ wrapFetch(baseFetch = globalThis.fetch) {
23
+ const self = this;
24
+ return async (input, init) => {
25
+ const headers = new Headers(init?.headers);
26
+ if (self.active) {
27
+ headers.set(CONTEXT_HEADER, headerFromContext(self.active));
28
+ }
29
+ return baseFetch(input, { ...init, headers });
30
+ };
31
+ }
32
+ /** Merge context header into plain header objects (Express/Node handlers). */
33
+ injectHeaders(ctx, headers = {}) {
34
+ return { ...headers, [CONTEXT_HEADER]: headerFromContext(ctx) };
35
+ }
36
+ }
37
+ export const actorchestra = new Actorchestra();
@@ -0,0 +1,16 @@
1
+ import type { ContextId, StoryContext, TraceEvent } from "./types.js";
2
+ export type { ContextId, StoryContext, TraceEvent, Violation, Verdict } from "./types.js";
3
+ export { CONTEXT_HEADER, contextFromHeader, headerFromContext } from "./types.js";
4
+ export { Actorchestra, actorchestra } from "./actorchestra.js";
5
+ export type EventListener = (event: TraceEvent) => void;
6
+ export declare class Conductor {
7
+ private listeners;
8
+ private events;
9
+ startStory(parentContext?: ContextId): StoryContext;
10
+ startStoryWithId(contextId: ContextId, parentContext?: ContextId): StoryContext;
11
+ emit(ctx: StoryContext, message: Omit<TraceEvent, "contextId" | "timestamp">): TraceEvent;
12
+ subscribe(listener: EventListener): () => void;
13
+ getEvents(): readonly TraceEvent[];
14
+ getEventsForContext(contextId: ContextId): TraceEvent[];
15
+ clear(): void;
16
+ }
package/dist/index.js ADDED
@@ -0,0 +1,41 @@
1
+ import { randomUUID } from "node:crypto";
2
+ export { CONTEXT_HEADER, contextFromHeader, headerFromContext } from "./types.js";
3
+ export { Actorchestra, actorchestra } from "./actorchestra.js";
4
+ export class Conductor {
5
+ listeners = new Set();
6
+ events = [];
7
+ startStory(parentContext) {
8
+ return this.startStoryWithId(randomUUID(), parentContext);
9
+ }
10
+ startStoryWithId(contextId, parentContext) {
11
+ return {
12
+ contextId,
13
+ parentContext,
14
+ };
15
+ }
16
+ emit(ctx, message) {
17
+ const event = {
18
+ contextId: ctx.contextId,
19
+ timestamp: Date.now(),
20
+ ...message,
21
+ };
22
+ this.events.push(event);
23
+ for (const listener of this.listeners) {
24
+ listener(event);
25
+ }
26
+ return event;
27
+ }
28
+ subscribe(listener) {
29
+ this.listeners.add(listener);
30
+ return () => this.listeners.delete(listener);
31
+ }
32
+ getEvents() {
33
+ return this.events;
34
+ }
35
+ getEventsForContext(contextId) {
36
+ return this.events.filter((e) => e.contextId === contextId);
37
+ }
38
+ clear() {
39
+ this.events = [];
40
+ }
41
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { Conductor } from "./index.js";
4
+ test("Conductor assigns context and emits events", () => {
5
+ const conductor = new Conductor();
6
+ const seen = [];
7
+ conductor.subscribe((e) => seen.push(`${e.from}->${e.to}:${e.op}`));
8
+ const ctx = conductor.startStory();
9
+ conductor.emit(ctx, {
10
+ from: "client",
11
+ to: "order",
12
+ op: "place",
13
+ payload: { orderId: "1" },
14
+ });
15
+ assert.equal(seen.length, 1);
16
+ assert.equal(conductor.getEventsForContext(ctx.contextId).length, 1);
17
+ });
@@ -0,0 +1,23 @@
1
+ export type ContextId = string;
2
+ export declare const CONTEXT_HEADER = "x-fiducia-context";
3
+ export interface StoryContext {
4
+ contextId: ContextId;
5
+ parentContext?: ContextId;
6
+ }
7
+ export declare function headerFromContext(ctx: StoryContext): string;
8
+ export declare function contextFromHeader(header: string | undefined): StoryContext | undefined;
9
+ export interface TraceEvent {
10
+ contextId: ContextId;
11
+ from: string;
12
+ to: string;
13
+ op: string;
14
+ payload: Record<string, unknown>;
15
+ timestamp: number;
16
+ }
17
+ export type Verdict = "pass" | "violated" | "inconclusive";
18
+ export interface Violation {
19
+ contextId: ContextId;
20
+ message: string;
21
+ step?: number;
22
+ event?: TraceEvent;
23
+ }
package/dist/types.js ADDED
@@ -0,0 +1,9 @@
1
+ export const CONTEXT_HEADER = "x-fiducia-context";
2
+ export function headerFromContext(ctx) {
3
+ return ctx.contextId;
4
+ }
5
+ export function contextFromHeader(header) {
6
+ if (!header)
7
+ return undefined;
8
+ return { contextId: header };
9
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@fiducian/conductor",
3
+ "version": "0.4.0",
4
+ "description": "Story context IDs and event bus for FiduciaTrace causal chain checking",
5
+ "license": "AGPL-3.0-or-later",
6
+ "keywords": ["fiducia", "fiducia", "causal", "distributed-systems", "context"],
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/thesnmc/Fiducian.git",
10
+ "directory": "trace/packages/conductor"
11
+ },
12
+ "publishConfig": {
13
+ "access": "public"
14
+ },
15
+ "files": ["dist"],
16
+ "type": "module",
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "test": "node --test dist/**/*.test.js",
28
+ "prepublishOnly": "npm run build"
29
+ }
30
+ }