@fabricorg/platform 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -0
  3. package/dist/actions/index.d.ts +114 -0
  4. package/dist/actions/index.js +59 -0
  5. package/dist/actions/index.js.map +1 -0
  6. package/dist/adapters/index.d.ts +83 -0
  7. package/dist/adapters/index.js +101 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/displays/index.d.ts +34 -0
  10. package/dist/displays/index.js +89 -0
  11. package/dist/displays/index.js.map +1 -0
  12. package/dist/events/index.d.ts +35 -0
  13. package/dist/events/index.js +55 -0
  14. package/dist/events/index.js.map +1 -0
  15. package/dist/evidence/index.d.ts +43 -0
  16. package/dist/evidence/index.js +3 -0
  17. package/dist/evidence/index.js.map +1 -0
  18. package/dist/ids/index.d.ts +19 -0
  19. package/dist/ids/index.js +55 -0
  20. package/dist/ids/index.js.map +1 -0
  21. package/dist/index.d.ts +13 -0
  22. package/dist/index.js +1357 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/modules/index.d.ts +73 -0
  25. package/dist/modules/index.js +375 -0
  26. package/dist/modules/index.js.map +1 -0
  27. package/dist/objects/index.d.ts +7 -0
  28. package/dist/objects/index.js +30 -0
  29. package/dist/objects/index.js.map +1 -0
  30. package/dist/policies/index.d.ts +143 -0
  31. package/dist/policies/index.js +544 -0
  32. package/dist/policies/index.js.map +1 -0
  33. package/dist/privacy/index.d.ts +77 -0
  34. package/dist/privacy/index.js +6 -0
  35. package/dist/privacy/index.js.map +1 -0
  36. package/dist/projections/index.d.ts +52 -0
  37. package/dist/projections/index.js +181 -0
  38. package/dist/projections/index.js.map +1 -0
  39. package/dist/state-machines/index.d.ts +60 -0
  40. package/dist/state-machines/index.js +67 -0
  41. package/dist/state-machines/index.js.map +1 -0
  42. package/dist/testing/index.d.ts +19 -0
  43. package/dist/testing/index.js +58 -0
  44. package/dist/testing/index.js.map +1 -0
  45. package/package.json +113 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fabric
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # @fabricorg/platform
2
+
3
+ Generic runtime for ontology-based business applications. Provides the action lifecycle, policy dispatch, state machine validation, adapter retry/circuit-breaker, event append, and ID generation that any vertical (lending, legal, healthcare, insurance, etc.) can build on.
4
+
5
+ ## Portability Scope
6
+
7
+ **`@fabricorg/platform` is portable.** It has zero runtime dependencies, no database client, no ORM, and no vertical vocabulary. Every contract is parameterized by `TDb` so consumers bind their own persistence client.
8
+
9
+ **Vertical packages (e.g. `@repo/lending`) are NOT portable.** They bind `TDb` to a concrete client (e.g. `Prisma.TransactionClient`), reference vertical entity types, and assume a specific database schema. To run a vertical against a different ORM, fork the vertical — not the platform.
10
+
11
+ The platform is enforced clean by `boundary.test.ts`, which forbids:
12
+ - `@repo/database`, `@repo/ontology`, `@repo/adapters` imports
13
+ - the literal `Prisma`, the field name `prisma:`, and the type default `TDb = any`
14
+ - any vertical vocabulary (`lending.`, `borrower`, `lender`, `LeadApplication`, `Offer`, `Vehicle`, `Party`, `Obligation`, `auto-refi`, etc.)
15
+ - any "register default" hook (`registerDefaultPolicies`, etc.)
16
+
17
+ ## Subpath Exports
18
+
19
+ | Path | Contents |
20
+ |---|---|
21
+ | `@fabricorg/platform` | re-exports everything below |
22
+ | `@fabricorg/platform/actions` | `ActionDefinition<TDb>`, `ActionContext<TDb>`, `ActorType`, `assertActorType`, `SagaImplementation`, `SchemaValidator` |
23
+ | `@fabricorg/platform/adapters` | `AdapterRegistry`, `AdapterImplementation`, retry/circuit-breaker primitives |
24
+ | `@fabricorg/platform/events` | `AssetEventEnvelope`, `EventTypeRegistry` |
25
+ | `@fabricorg/platform/ids` | `createFabricId`, `FABRIC_ID_PREFIXES`, `parseFabricId` |
26
+ | `@fabricorg/platform/modules` | `FabricModule<TDb>`, `ModuleRegistry`, `createModuleRegistry`, `registerFabricModules` |
27
+ | `@fabricorg/platform/policies` | `PolicyEvaluator<TDb>`, `PolicyContext<TDb>`, code/data/hybrid evaluation engine |
28
+ | `@fabricorg/platform/state-machines` | `StateMachineDefinition`, `validateTransition`, engine |
29
+ | `@fabricorg/platform/projections` | replay/snapshot framework |
30
+ | `@fabricorg/platform/displays` | `DisplayRendererRegistration`, display engine |
31
+ | `@fabricorg/platform/privacy` | redaction, anonymization-pipeline, data-classification contracts |
32
+ | `@fabricorg/platform/evidence` | compliance evidence packet envelope |
33
+ | `@fabricorg/platform/objects` | shared object-type contracts |
34
+ | `@fabricorg/platform/testing` | test fixtures (in-memory adapter registry, fake ActionContext, deterministic IDs) |
35
+
36
+ ## Action Lifecycle
37
+
38
+ Every domain mutation flows through a single platform pipeline:
39
+
40
+ 1. `evaluatePolicies` — dispatch each policy ID via `evaluatePolicyDefinitions` (code/data/hybrid). Aggregate `block` halts the invocation.
41
+ 2. **If `actionDef.kind === "saga"`** — dispatch to a registered saga implementation that orchestrates child actions and returns a `SagaCompletion`.
42
+ 3. **Otherwise** — build an `ActionContext<TDb>`, run `actionDef.handler(ctx, params)` inside a single transaction. The handler parses its own input via its declared schema (single source of truth — handlers honor their schema contract). State-machine binding validates transitions before the handler runs. ZodError thrown by handler parsing is translated to `validation_failed` invocation status.
43
+ 4. `executeAdapters` — for each `actionDef.adapterSteps`, look up the implementation in the `AdapterRegistry` and run via `executeWithAdapterRetry` (idempotency-aware retry, exponential backoff, circuit breaker, dead-letter queue on terminal failure).
44
+ 5. `completeActionInvocation` — mark completed/failed and emit telemetry.
45
+
46
+ `validateActionParameters` exists as a standalone activity for preview/dry-run paths but is **not** part of the execute pipeline — parameter validation happens once, in the handler, via its own schema.
47
+
48
+ ## Module Contract
49
+
50
+ Verticals declare modules as `FabricModule<TDb>`:
51
+
52
+ ```ts
53
+ import type { FabricModule } from "@fabricorg/platform/modules";
54
+ import type { Prisma } from "@repo/database";
55
+
56
+ export const lendingModule: FabricModule<Prisma.TransactionClient> = {
57
+ namespace: "lending",
58
+ version: "1.0.0",
59
+ objectTypes: ["Vehicle", "Party", "Location", "Document"],
60
+ actions: [...],
61
+ policies: [...],
62
+ stateMachines: [...],
63
+ displayRenderers: [...],
64
+ };
65
+ ```
66
+
67
+ Apps compose modules explicitly at startup:
68
+
69
+ ```ts
70
+ import { registerFabricModules } from "@fabricorg/platform/modules";
71
+ import { lendingModule } from "@repo/lending";
72
+ import { messagingModule } from "@repo/messaging";
73
+
74
+ registerFabricModules([messagingModule, lendingModule]);
75
+ ```
76
+
77
+ There are no implicit/default modules. The platform never auto-loads a vertical. Each vertical owns its full domain ontology — there is no "shared core ontology" package; cross-industry governance object types (`AssetEvent`, `ActionInvocation`, `PolicyEvaluation`, `ApprovalRequest`, `ConsentRecord`, `AgentRun`) are pre-registered by the platform itself.
78
+
79
+ ## Saga Actions
80
+
81
+ Multi-step orchestrations declare `kind: "saga"` on the `ActionDefinition` and ship a `SagaImplementation` alongside:
82
+
83
+ ```ts
84
+ import type { SagaImplementation } from "@fabricorg/platform/actions";
85
+
86
+ export const ingestSaga: SagaImplementation = async (ctx, runChild) => {
87
+ const child = await runChild({
88
+ suffix: "step-1",
89
+ actionId: "lending.receive_lead_payload",
90
+ parameters: { ... },
91
+ });
92
+ // ... orchestrate further child actions ...
93
+ return {
94
+ resultData: { ... },
95
+ event: { eventType: "...", subjectType: "...", subjectId: "...", payload: ... },
96
+ };
97
+ };
98
+ ```
99
+
100
+ The host app's saga registry maps action IDs to implementations. The worker dispatches generically — no platform code or worker code references vertical action IDs.
101
+
102
+ ## Versioning
103
+
104
+ `0.x` while contracts evolve. `1.0` only after at least two materially different verticals validate the API.
@@ -0,0 +1,114 @@
1
+ type ActionId = `${string}.${string}`;
2
+ interface SchemaParseIssue {
3
+ path: PropertyKey[];
4
+ message: string;
5
+ }
6
+ type SchemaParseResult<T = unknown> = {
7
+ success: true;
8
+ data: T;
9
+ } | {
10
+ success: false;
11
+ error: {
12
+ issues: SchemaParseIssue[];
13
+ };
14
+ };
15
+ interface SchemaValidator<T = unknown> {
16
+ parse(input: unknown): T;
17
+ safeParse(input: unknown): SchemaParseResult<T>;
18
+ }
19
+ type ActorType = "natural_person" | "agent" | "system" | "service_account" | "external_system" | "integration";
20
+ declare const ACTOR_TYPES: readonly ActorType[];
21
+ declare function isActorType(value: string): value is ActorType;
22
+ declare function assertActorType(value: string): asserts value is ActorType;
23
+ type ActionStatus = "pending" | "running" | "completed" | "failed" | "blocked_by_policy" | "waiting_for_approval" | "validation_failed";
24
+ interface FabricRuntimeServices {
25
+ readonly [serviceName: string]: unknown;
26
+ }
27
+ interface ActionContext<TDb = unknown> {
28
+ actionInvocationId: string;
29
+ tenantId: string;
30
+ spaceId: string;
31
+ actorId: string;
32
+ actorType: ActorType;
33
+ correlationId: string;
34
+ causationId?: string;
35
+ db: TDb;
36
+ services?: FabricRuntimeServices;
37
+ }
38
+ interface ActionResult {
39
+ success: boolean;
40
+ data?: Record<string, unknown>;
41
+ error?: string;
42
+ }
43
+ type ActionHandler<TDb = unknown> = (ctx: ActionContext<TDb>, params: unknown) => Promise<ActionResult>;
44
+ interface AdapterStepRetryPolicy {
45
+ idempotent: boolean;
46
+ maxAttempts?: number;
47
+ initialDelayMs?: number;
48
+ backoffMultiplier?: number;
49
+ maxDelayMs?: number;
50
+ }
51
+ interface AdapterStep {
52
+ adapterType: string;
53
+ operation: string;
54
+ retryPolicy?: AdapterStepRetryPolicy;
55
+ getInput: (params: unknown, handlerResult: Record<string, unknown>) => Record<string, unknown> | undefined;
56
+ }
57
+ interface ActionStateMachineBinding {
58
+ entityType: string;
59
+ targetState: string | ((params: unknown) => string);
60
+ getEntityId: (params: unknown) => string | undefined;
61
+ }
62
+ interface ActionDefinition<TDb = unknown> {
63
+ actionId: ActionId;
64
+ namespace: string;
65
+ version: number;
66
+ schema: SchemaValidator;
67
+ handler: ActionHandler<TDb>;
68
+ requiredRoles?: string[];
69
+ requiredPermissions?: string[];
70
+ policies?: `${string}.v${number}`[];
71
+ emitsEvents: string[];
72
+ idempotent: boolean;
73
+ mutatesDomain: boolean;
74
+ stateMachine?: ActionStateMachineBinding;
75
+ adapterSteps?: AdapterStep[];
76
+ /**
77
+ * If "saga", the action is a multi-step orchestration. The worker dispatches
78
+ * to a registered saga implementation instead of running `handler` directly.
79
+ */
80
+ kind?: "atomic" | "saga";
81
+ }
82
+ interface SagaContext {
83
+ actionInvocationId: string;
84
+ tenantId: string;
85
+ spaceId: string;
86
+ parameters: unknown;
87
+ }
88
+ interface SagaChildActionInput {
89
+ suffix: string;
90
+ actionId: string;
91
+ parameters: Record<string, unknown>;
92
+ }
93
+ type SagaRunChild = (input: SagaChildActionInput) => Promise<Record<string, unknown>>;
94
+ interface SagaCompletionEvent {
95
+ eventType: string;
96
+ subjectType: string;
97
+ subjectId: string;
98
+ payload: unknown;
99
+ eventSchemaVersion?: number;
100
+ }
101
+ interface SagaCompletion {
102
+ resultData: Record<string, unknown>;
103
+ event?: SagaCompletionEvent;
104
+ }
105
+ type SagaImplementation = (ctx: SagaContext, runChild: SagaRunChild) => Promise<SagaCompletion>;
106
+ declare function assertActionId(actionId: string): asserts actionId is ActionId;
107
+ declare function registerAction(definition: ActionDefinition): void;
108
+ declare function resolveAction(actionId: ActionId): ActionDefinition | undefined;
109
+ declare function isRegisteredActionId(actionId: string): actionId is ActionId;
110
+ declare function resolveRegisteredAction(actionId: string): ActionDefinition | undefined;
111
+ declare function getRegisteredActionIds(): ActionId[];
112
+ declare function registerActionIfMissing(definition: ActionDefinition): void;
113
+
114
+ export { ACTOR_TYPES, type ActionContext, type ActionDefinition, type ActionHandler, type ActionId, type ActionResult, type ActionStateMachineBinding, type ActionStatus, type ActorType, type AdapterStep, type AdapterStepRetryPolicy, type FabricRuntimeServices, type SagaChildActionInput, type SagaCompletion, type SagaCompletionEvent, type SagaContext, type SagaImplementation, type SagaRunChild, type SchemaParseIssue, type SchemaParseResult, type SchemaValidator, assertActionId, assertActorType, getRegisteredActionIds, isActorType, isRegisteredActionId, registerAction, registerActionIfMissing, resolveAction, resolveRegisteredAction };
@@ -0,0 +1,59 @@
1
+ // actions/index.ts
2
+ var ACTOR_TYPES = [
3
+ "natural_person",
4
+ "agent",
5
+ "system",
6
+ "service_account",
7
+ "external_system",
8
+ "integration"
9
+ ];
10
+ function isActorType(value) {
11
+ return ACTOR_TYPES.includes(value);
12
+ }
13
+ function assertActorType(value) {
14
+ if (!isActorType(value)) {
15
+ throw new Error(`Invalid actorType: ${value}`);
16
+ }
17
+ }
18
+ var registry = /* @__PURE__ */ new Map();
19
+ function assertActionId(actionId) {
20
+ if (!/^[a-z][a-z0-9-]*\.[a-z][a-z0-9_]*$/.test(actionId)) {
21
+ throw new Error(`Invalid action ID: ${actionId}`);
22
+ }
23
+ }
24
+ function registerAction(definition) {
25
+ assertActionId(definition.actionId);
26
+ if (registry.has(definition.actionId)) {
27
+ throw new Error(`Action "${definition.actionId}" is already registered`);
28
+ }
29
+ if (definition.mutatesDomain && definition.emitsEvents.length === 0) {
30
+ throw new Error(
31
+ `Action "${definition.actionId}" mutates domain but emits no events. Every mutating action must emit at least one AssetEvent.`
32
+ );
33
+ }
34
+ registry.set(definition.actionId, definition);
35
+ }
36
+ function resolveAction(actionId) {
37
+ return registry.get(actionId);
38
+ }
39
+ function isRegisteredActionId(actionId) {
40
+ return registry.has(actionId);
41
+ }
42
+ function resolveRegisteredAction(actionId) {
43
+ if (!isRegisteredActionId(actionId)) {
44
+ return void 0;
45
+ }
46
+ return resolveAction(actionId);
47
+ }
48
+ function getRegisteredActionIds() {
49
+ return Array.from(registry.keys());
50
+ }
51
+ function registerActionIfMissing(definition) {
52
+ if (!registry.has(definition.actionId)) {
53
+ registerAction(definition);
54
+ }
55
+ }
56
+
57
+ export { ACTOR_TYPES, assertActionId, assertActorType, getRegisteredActionIds, isActorType, isRegisteredActionId, registerAction, registerActionIfMissing, resolveAction, resolveRegisteredAction };
58
+ //# sourceMappingURL=index.js.map
59
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../actions/index.ts"],"names":[],"mappings":";AAwBO,IAAM,WAAA,GAAoC;AAAA,EAChD,gBAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA;AACD;AAEO,SAAS,YAAY,KAAA,EAAmC;AAC9D,EAAA,OAAQ,WAAA,CAAkC,SAAS,KAAK,CAAA;AACzD;AAEO,SAAS,gBAAgB,KAAA,EAA2C;AAC1E,EAAA,IAAI,CAAC,WAAA,CAAY,KAAK,CAAA,EAAG;AACxB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,KAAK,CAAA,CAAE,CAAA;AAAA,EAC9C;AACD;AAoHA,IAAM,QAAA,uBAAe,GAAA,EAAgC;AAE9C,SAAS,eAAe,QAAA,EAAgD;AAC9E,EAAA,IAAI,CAAC,oCAAA,CAAqC,IAAA,CAAK,QAAQ,CAAA,EAAG;AACzD,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,mBAAA,EAAsB,QAAQ,CAAA,CAAE,CAAA;AAAA,EACjD;AACD;AAEO,SAAS,eAAe,UAAA,EAAoC;AAClE,EAAA,cAAA,CAAe,WAAW,QAAQ,CAAA;AAElC,EAAA,IAAI,QAAA,CAAS,GAAA,CAAI,UAAA,CAAW,QAAQ,CAAA,EAAG;AACtC,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,QAAA,EAAW,UAAA,CAAW,QAAQ,CAAA,uBAAA,CAAyB,CAAA;AAAA,EACxE;AAEA,EAAA,IAAI,UAAA,CAAW,aAAA,IAAiB,UAAA,CAAW,WAAA,CAAY,WAAW,CAAA,EAAG;AACpE,IAAA,MAAM,IAAI,KAAA;AAAA,MACT,CAAA,QAAA,EAAW,WAAW,QAAQ,CAAA,8FAAA;AAAA,KAE/B;AAAA,EACD;AAEA,EAAA,QAAA,CAAS,GAAA,CAAI,UAAA,CAAW,QAAA,EAAU,UAAU,CAAA;AAC7C;AAEO,SAAS,cAAc,QAAA,EAAkD;AAC/E,EAAA,OAAO,QAAA,CAAS,IAAI,QAAQ,CAAA;AAC7B;AAEO,SAAS,qBAAqB,QAAA,EAAwC;AAC5E,EAAA,OAAO,QAAA,CAAS,IAAI,QAAoB,CAAA;AACzC;AAEO,SAAS,wBAAwB,QAAA,EAAgD;AACvF,EAAA,IAAI,CAAC,oBAAA,CAAqB,QAAQ,CAAA,EAAG;AACpC,IAAA,OAAO,MAAA;AAAA,EACR;AAEA,EAAA,OAAO,cAAc,QAAQ,CAAA;AAC9B;AAEO,SAAS,sBAAA,GAAqC;AACpD,EAAA,OAAO,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,IAAA,EAAM,CAAA;AAClC;AAEO,SAAS,wBAAwB,UAAA,EAAoC;AAC3E,EAAA,IAAI,CAAC,QAAA,CAAS,GAAA,CAAI,UAAA,CAAW,QAAQ,CAAA,EAAG;AACvC,IAAA,cAAA,CAAe,UAAU,CAAA;AAAA,EAC1B;AACD","file":"index.js","sourcesContent":["export type ActionId = `${string}.${string}`;\n\nexport interface SchemaParseIssue {\n\tpath: PropertyKey[];\n\tmessage: string;\n}\n\nexport type SchemaParseResult<T = unknown> =\n\t| { success: true; data: T }\n\t| { success: false; error: { issues: SchemaParseIssue[] } };\n\nexport interface SchemaValidator<T = unknown> {\n\tparse(input: unknown): T;\n\tsafeParse(input: unknown): SchemaParseResult<T>;\n}\n\nexport type ActorType =\n\t| \"natural_person\"\n\t| \"agent\"\n\t| \"system\"\n\t| \"service_account\"\n\t| \"external_system\"\n\t| \"integration\";\n\nexport const ACTOR_TYPES: readonly ActorType[] = [\n\t\"natural_person\",\n\t\"agent\",\n\t\"system\",\n\t\"service_account\",\n\t\"external_system\",\n\t\"integration\",\n] as const;\n\nexport function isActorType(value: string): value is ActorType {\n\treturn (ACTOR_TYPES as readonly string[]).includes(value);\n}\n\nexport function assertActorType(value: string): asserts value is ActorType {\n\tif (!isActorType(value)) {\n\t\tthrow new Error(`Invalid actorType: ${value}`);\n\t}\n}\n\nexport type ActionStatus =\n\t| \"pending\"\n\t| \"running\"\n\t| \"completed\"\n\t| \"failed\"\n\t| \"blocked_by_policy\"\n\t| \"waiting_for_approval\"\n\t| \"validation_failed\";\n\nexport interface FabricRuntimeServices {\n\treadonly [serviceName: string]: unknown;\n}\n\nexport interface ActionContext<TDb = unknown> {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tactorId: string;\n\tactorType: ActorType;\n\tcorrelationId: string;\n\tcausationId?: string;\n\tdb: TDb;\n\tservices?: FabricRuntimeServices;\n}\n\nexport interface ActionResult {\n\tsuccess: boolean;\n\tdata?: Record<string, unknown>;\n\terror?: string;\n}\n\nexport type ActionHandler<TDb = unknown> = (\n\tctx: ActionContext<TDb>,\n\tparams: unknown,\n) => Promise<ActionResult>;\n\nexport interface AdapterStepRetryPolicy {\n\tidempotent: boolean;\n\tmaxAttempts?: number;\n\tinitialDelayMs?: number;\n\tbackoffMultiplier?: number;\n\tmaxDelayMs?: number;\n}\n\nexport interface AdapterStep {\n\tadapterType: string;\n\toperation: string;\n\tretryPolicy?: AdapterStepRetryPolicy;\n\tgetInput: (\n\t\tparams: unknown,\n\t\thandlerResult: Record<string, unknown>,\n\t) => Record<string, unknown> | undefined;\n}\n\nexport interface ActionStateMachineBinding {\n\tentityType: string;\n\ttargetState: string | ((params: unknown) => string);\n\tgetEntityId: (params: unknown) => string | undefined;\n}\n\nexport interface ActionDefinition<TDb = unknown> {\n\tactionId: ActionId;\n\tnamespace: string;\n\tversion: number;\n\tschema: SchemaValidator;\n\thandler: ActionHandler<TDb>;\n\trequiredRoles?: string[];\n\trequiredPermissions?: string[];\n\tpolicies?: `${string}.v${number}`[];\n\temitsEvents: string[];\n\tidempotent: boolean;\n\tmutatesDomain: boolean;\n\tstateMachine?: ActionStateMachineBinding;\n\tadapterSteps?: AdapterStep[];\n\t/**\n\t * If \"saga\", the action is a multi-step orchestration. The worker dispatches\n\t * to a registered saga implementation instead of running `handler` directly.\n\t */\n\tkind?: \"atomic\" | \"saga\";\n}\n\nexport interface SagaContext {\n\tactionInvocationId: string;\n\ttenantId: string;\n\tspaceId: string;\n\tparameters: unknown;\n}\n\nexport interface SagaChildActionInput {\n\tsuffix: string;\n\tactionId: string;\n\tparameters: Record<string, unknown>;\n}\n\nexport type SagaRunChild = (input: SagaChildActionInput) => Promise<Record<string, unknown>>;\n\nexport interface SagaCompletionEvent {\n\teventType: string;\n\tsubjectType: string;\n\tsubjectId: string;\n\tpayload: unknown;\n\teventSchemaVersion?: number;\n}\n\nexport interface SagaCompletion {\n\tresultData: Record<string, unknown>;\n\tevent?: SagaCompletionEvent;\n}\n\nexport type SagaImplementation = (\n\tctx: SagaContext,\n\trunChild: SagaRunChild,\n) => Promise<SagaCompletion>;\n\nconst registry = new Map<ActionId, ActionDefinition>();\n\nexport function assertActionId(actionId: string): asserts actionId is ActionId {\n\tif (!/^[a-z][a-z0-9-]*\\.[a-z][a-z0-9_]*$/.test(actionId)) {\n\t\tthrow new Error(`Invalid action ID: ${actionId}`);\n\t}\n}\n\nexport function registerAction(definition: ActionDefinition): void {\n\tassertActionId(definition.actionId);\n\n\tif (registry.has(definition.actionId)) {\n\t\tthrow new Error(`Action \"${definition.actionId}\" is already registered`);\n\t}\n\n\tif (definition.mutatesDomain && definition.emitsEvents.length === 0) {\n\t\tthrow new Error(\n\t\t\t`Action \"${definition.actionId}\" mutates domain but emits no events. ` +\n\t\t\t\t`Every mutating action must emit at least one AssetEvent.`,\n\t\t);\n\t}\n\n\tregistry.set(definition.actionId, definition);\n}\n\nexport function resolveAction(actionId: ActionId): ActionDefinition | undefined {\n\treturn registry.get(actionId);\n}\n\nexport function isRegisteredActionId(actionId: string): actionId is ActionId {\n\treturn registry.has(actionId as ActionId);\n}\n\nexport function resolveRegisteredAction(actionId: string): ActionDefinition | undefined {\n\tif (!isRegisteredActionId(actionId)) {\n\t\treturn undefined;\n\t}\n\n\treturn resolveAction(actionId);\n}\n\nexport function getRegisteredActionIds(): ActionId[] {\n\treturn Array.from(registry.keys());\n}\n\nexport function registerActionIfMissing(definition: ActionDefinition): void {\n\tif (!registry.has(definition.actionId)) {\n\t\tregisterAction(definition);\n\t}\n}\n"]}
@@ -0,0 +1,83 @@
1
+ interface AdapterExecutionContext {
2
+ tenantId: string;
3
+ spaceId: string;
4
+ actionInvocationId: string;
5
+ adapterInvocationId: string;
6
+ correlationId: string;
7
+ causationId?: string;
8
+ attempt: number;
9
+ maxAttempts: number;
10
+ }
11
+ interface AdapterExecutionResult {
12
+ success: boolean;
13
+ correlationId?: string;
14
+ error?: string;
15
+ }
16
+ type AdapterExecutor = (input: Record<string, unknown>, context: AdapterExecutionContext) => Promise<AdapterExecutionResult>;
17
+ interface AdapterRetryPolicy {
18
+ /** External calls must explicitly opt into retry by being idempotent. */
19
+ idempotent: boolean;
20
+ /** Total attempts including the first call. Non-idempotent policies are forced to 1. */
21
+ maxAttempts?: number;
22
+ /** First backoff delay in milliseconds. */
23
+ initialDelayMs?: number;
24
+ /** Exponential multiplier applied after each failed attempt. */
25
+ backoffMultiplier?: number;
26
+ /** Upper bound for any single retry delay in milliseconds. */
27
+ maxDelayMs?: number;
28
+ }
29
+ interface ResolvedAdapterRetryPolicy {
30
+ idempotent: boolean;
31
+ maxAttempts: number;
32
+ initialDelayMs: number;
33
+ backoffMultiplier: number;
34
+ maxDelayMs: number;
35
+ }
36
+ interface AdapterCircuitBreakerConfig {
37
+ mode: "monitor" | "enforce";
38
+ failureThreshold: number;
39
+ windowMs: number;
40
+ cooldownMs: number;
41
+ minimumSamples?: number;
42
+ }
43
+ interface AdapterImplementation {
44
+ adapterType: string;
45
+ operation: string;
46
+ vendor: string;
47
+ /** Whether this adapter operation can be safely retried by default. */
48
+ idempotent: boolean;
49
+ retryPolicy?: AdapterRetryPolicy;
50
+ /** Defaults to monitor-only; enforce mode must be explicitly configured. */
51
+ circuitBreaker?: AdapterCircuitBreakerConfig;
52
+ execute: AdapterExecutor;
53
+ }
54
+ interface AdapterAttempt<T> {
55
+ attempt: number;
56
+ result?: T;
57
+ error?: string;
58
+ willRetry: boolean;
59
+ nextDelayMs?: number;
60
+ }
61
+ interface ExecuteWithRetryOptions<T> {
62
+ policy?: AdapterRetryPolicy;
63
+ defaultIdempotent: boolean;
64
+ execute: (attempt: number, maxAttempts: number) => Promise<T>;
65
+ isSuccessful: (result: T) => boolean;
66
+ getError: (result: T) => string | undefined;
67
+ sleep?: (delayMs: number) => Promise<void>;
68
+ onAttempt?: (attempt: AdapterAttempt<T>) => Promise<void> | void;
69
+ }
70
+ declare class MissingAdapterRegistrationError extends Error {
71
+ constructor(adapterType: string, operation: string);
72
+ }
73
+ declare class AdapterRegistry {
74
+ private readonly adapters;
75
+ register(adapter: AdapterImplementation): void;
76
+ resolve(adapterType: string, operation: string): AdapterImplementation | undefined;
77
+ require(adapterType: string, operation: string): AdapterImplementation;
78
+ }
79
+ declare function resolveAdapterRetryPolicy(policy: AdapterRetryPolicy | undefined, defaultIdempotent: boolean): ResolvedAdapterRetryPolicy;
80
+ declare function getRetryDelayMs(attempt: number, policy: ResolvedAdapterRetryPolicy): number;
81
+ declare function executeWithAdapterRetry<T>(options: ExecuteWithRetryOptions<T>): Promise<T>;
82
+
83
+ export { type AdapterAttempt, type AdapterCircuitBreakerConfig, type AdapterExecutionContext, type AdapterExecutionResult, type AdapterExecutor, type AdapterImplementation, AdapterRegistry, type AdapterRetryPolicy, type ExecuteWithRetryOptions, MissingAdapterRegistrationError, type ResolvedAdapterRetryPolicy, executeWithAdapterRetry, getRetryDelayMs, resolveAdapterRetryPolicy };
@@ -0,0 +1,101 @@
1
+ // adapters/index.ts
2
+ function adapterKey(adapterType, operation) {
3
+ return `${adapterType}:${operation}`;
4
+ }
5
+ var MissingAdapterRegistrationError = class extends Error {
6
+ constructor(adapterType, operation) {
7
+ super(`No adapter registered for adapterType "${adapterType}" and operation "${operation}"`);
8
+ this.name = "MissingAdapterRegistrationError";
9
+ }
10
+ };
11
+ var AdapterRegistry = class {
12
+ adapters = /* @__PURE__ */ new Map();
13
+ register(adapter) {
14
+ const key = adapterKey(adapter.adapterType, adapter.operation);
15
+ if (this.adapters.has(key)) {
16
+ throw new Error(
17
+ `Adapter already registered for adapterType "${adapter.adapterType}" and operation "${adapter.operation}"`
18
+ );
19
+ }
20
+ this.adapters.set(key, adapter);
21
+ }
22
+ resolve(adapterType, operation) {
23
+ return this.adapters.get(adapterKey(adapterType, operation));
24
+ }
25
+ require(adapterType, operation) {
26
+ const adapter = this.resolve(adapterType, operation);
27
+ if (!adapter) {
28
+ throw new MissingAdapterRegistrationError(adapterType, operation);
29
+ }
30
+ return adapter;
31
+ }
32
+ };
33
+ var DEFAULT_RETRY_POLICY = {
34
+ maxAttempts: 3,
35
+ initialDelayMs: 100,
36
+ backoffMultiplier: 2,
37
+ maxDelayMs: 1e3
38
+ };
39
+ var NO_RETRY_POLICY = {
40
+ idempotent: false,
41
+ maxAttempts: 1,
42
+ initialDelayMs: 0,
43
+ backoffMultiplier: 1,
44
+ maxDelayMs: 0
45
+ };
46
+ function resolveAdapterRetryPolicy(policy, defaultIdempotent) {
47
+ const idempotent = policy?.idempotent ?? defaultIdempotent;
48
+ if (!idempotent) {
49
+ return NO_RETRY_POLICY;
50
+ }
51
+ return {
52
+ idempotent: true,
53
+ maxAttempts: Math.max(1, Math.min(policy?.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts, 5)),
54
+ initialDelayMs: Math.max(0, policy?.initialDelayMs ?? DEFAULT_RETRY_POLICY.initialDelayMs),
55
+ backoffMultiplier: Math.max(
56
+ 1,
57
+ policy?.backoffMultiplier ?? DEFAULT_RETRY_POLICY.backoffMultiplier
58
+ ),
59
+ maxDelayMs: Math.max(0, policy?.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs)
60
+ };
61
+ }
62
+ function getRetryDelayMs(attempt, policy) {
63
+ const exponentialDelay = policy.initialDelayMs * policy.backoffMultiplier ** Math.max(0, attempt - 1);
64
+ return Math.min(exponentialDelay, policy.maxDelayMs);
65
+ }
66
+ async function executeWithAdapterRetry(options) {
67
+ const policy = resolveAdapterRetryPolicy(options.policy, options.defaultIdempotent);
68
+ const sleep = options.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));
69
+ let lastError;
70
+ for (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {
71
+ try {
72
+ const result = await options.execute(attempt, policy.maxAttempts);
73
+ if (options.isSuccessful(result)) {
74
+ await options.onAttempt?.({ attempt, result, willRetry: false });
75
+ return result;
76
+ }
77
+ lastError = options.getError(result) ?? "Adapter returned unsuccessful result";
78
+ const willRetry = policy.idempotent && attempt < policy.maxAttempts;
79
+ const nextDelayMs = willRetry ? getRetryDelayMs(attempt, policy) : void 0;
80
+ await options.onAttempt?.({ attempt, result, error: lastError, willRetry, nextDelayMs });
81
+ if (!willRetry) {
82
+ return result;
83
+ }
84
+ await sleep(nextDelayMs ?? 0);
85
+ } catch (error) {
86
+ lastError = error instanceof Error ? error.message : String(error);
87
+ const willRetry = policy.idempotent && attempt < policy.maxAttempts;
88
+ const nextDelayMs = willRetry ? getRetryDelayMs(attempt, policy) : void 0;
89
+ await options.onAttempt?.({ attempt, error: lastError, willRetry, nextDelayMs });
90
+ if (!willRetry) {
91
+ throw error;
92
+ }
93
+ await sleep(nextDelayMs ?? 0);
94
+ }
95
+ }
96
+ throw new Error(lastError ?? "Adapter retry exhausted");
97
+ }
98
+
99
+ export { AdapterRegistry, MissingAdapterRegistrationError, executeWithAdapterRetry, getRetryDelayMs, resolveAdapterRetryPolicy };
100
+ //# sourceMappingURL=index.js.map
101
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../adapters/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["export interface AdapterExecutionContext {\n\ttenantId: string;\n\tspaceId: string;\n\tactionInvocationId: string;\n\tadapterInvocationId: string;\n\tcorrelationId: string;\n\tcausationId?: string;\n\tattempt: number;\n\tmaxAttempts: number;\n}\n\nexport interface AdapterExecutionResult {\n\tsuccess: boolean;\n\tcorrelationId?: string;\n\terror?: string;\n}\n\nexport type AdapterExecutor = (\n\tinput: Record<string, unknown>,\n\tcontext: AdapterExecutionContext,\n) => Promise<AdapterExecutionResult>;\n\nexport interface AdapterRetryPolicy {\n\t/** External calls must explicitly opt into retry by being idempotent. */\n\tidempotent: boolean;\n\t/** Total attempts including the first call. Non-idempotent policies are forced to 1. */\n\tmaxAttempts?: number;\n\t/** First backoff delay in milliseconds. */\n\tinitialDelayMs?: number;\n\t/** Exponential multiplier applied after each failed attempt. */\n\tbackoffMultiplier?: number;\n\t/** Upper bound for any single retry delay in milliseconds. */\n\tmaxDelayMs?: number;\n}\n\nexport interface ResolvedAdapterRetryPolicy {\n\tidempotent: boolean;\n\tmaxAttempts: number;\n\tinitialDelayMs: number;\n\tbackoffMultiplier: number;\n\tmaxDelayMs: number;\n}\n\nexport interface AdapterCircuitBreakerConfig {\n\tmode: \"monitor\" | \"enforce\";\n\tfailureThreshold: number;\n\twindowMs: number;\n\tcooldownMs: number;\n\tminimumSamples?: number;\n}\n\nexport interface AdapterImplementation {\n\tadapterType: string;\n\toperation: string;\n\tvendor: string;\n\t/** Whether this adapter operation can be safely retried by default. */\n\tidempotent: boolean;\n\tretryPolicy?: AdapterRetryPolicy;\n\t/** Defaults to monitor-only; enforce mode must be explicitly configured. */\n\tcircuitBreaker?: AdapterCircuitBreakerConfig;\n\texecute: AdapterExecutor;\n}\n\nexport interface AdapterAttempt<T> {\n\tattempt: number;\n\tresult?: T;\n\terror?: string;\n\twillRetry: boolean;\n\tnextDelayMs?: number;\n}\n\nexport interface ExecuteWithRetryOptions<T> {\n\tpolicy?: AdapterRetryPolicy;\n\tdefaultIdempotent: boolean;\n\texecute: (attempt: number, maxAttempts: number) => Promise<T>;\n\tisSuccessful: (result: T) => boolean;\n\tgetError: (result: T) => string | undefined;\n\tsleep?: (delayMs: number) => Promise<void>;\n\tonAttempt?: (attempt: AdapterAttempt<T>) => Promise<void> | void;\n}\n\nfunction adapterKey(adapterType: string, operation: string): string {\n\treturn `${adapterType}:${operation}`;\n}\n\nexport class MissingAdapterRegistrationError extends Error {\n\tconstructor(adapterType: string, operation: string) {\n\t\tsuper(`No adapter registered for adapterType \"${adapterType}\" and operation \"${operation}\"`);\n\t\tthis.name = \"MissingAdapterRegistrationError\";\n\t}\n}\n\nexport class AdapterRegistry {\n\tprivate readonly adapters = new Map<string, AdapterImplementation>();\n\n\tregister(adapter: AdapterImplementation): void {\n\t\tconst key = adapterKey(adapter.adapterType, adapter.operation);\n\t\tif (this.adapters.has(key)) {\n\t\t\tthrow new Error(\n\t\t\t\t`Adapter already registered for adapterType \"${adapter.adapterType}\" and operation \"${adapter.operation}\"`,\n\t\t\t);\n\t\t}\n\n\t\tthis.adapters.set(key, adapter);\n\t}\n\n\tresolve(adapterType: string, operation: string): AdapterImplementation | undefined {\n\t\treturn this.adapters.get(adapterKey(adapterType, operation));\n\t}\n\n\trequire(adapterType: string, operation: string): AdapterImplementation {\n\t\tconst adapter = this.resolve(adapterType, operation);\n\t\tif (!adapter) {\n\t\t\tthrow new MissingAdapterRegistrationError(adapterType, operation);\n\t\t}\n\n\t\treturn adapter;\n\t}\n}\n\nconst DEFAULT_RETRY_POLICY: ResolvedAdapterRetryPolicy = {\n\tidempotent: true,\n\tmaxAttempts: 3,\n\tinitialDelayMs: 100,\n\tbackoffMultiplier: 2,\n\tmaxDelayMs: 1_000,\n};\n\nconst NO_RETRY_POLICY: ResolvedAdapterRetryPolicy = {\n\tidempotent: false,\n\tmaxAttempts: 1,\n\tinitialDelayMs: 0,\n\tbackoffMultiplier: 1,\n\tmaxDelayMs: 0,\n};\n\nexport function resolveAdapterRetryPolicy(\n\tpolicy: AdapterRetryPolicy | undefined,\n\tdefaultIdempotent: boolean,\n): ResolvedAdapterRetryPolicy {\n\tconst idempotent = policy?.idempotent ?? defaultIdempotent;\n\n\tif (!idempotent) {\n\t\treturn NO_RETRY_POLICY;\n\t}\n\n\treturn {\n\t\tidempotent: true,\n\t\tmaxAttempts: Math.max(1, Math.min(policy?.maxAttempts ?? DEFAULT_RETRY_POLICY.maxAttempts, 5)),\n\t\tinitialDelayMs: Math.max(0, policy?.initialDelayMs ?? DEFAULT_RETRY_POLICY.initialDelayMs),\n\t\tbackoffMultiplier: Math.max(\n\t\t\t1,\n\t\t\tpolicy?.backoffMultiplier ?? DEFAULT_RETRY_POLICY.backoffMultiplier,\n\t\t),\n\t\tmaxDelayMs: Math.max(0, policy?.maxDelayMs ?? DEFAULT_RETRY_POLICY.maxDelayMs),\n\t};\n}\n\nexport function getRetryDelayMs(attempt: number, policy: ResolvedAdapterRetryPolicy): number {\n\tconst exponentialDelay =\n\t\tpolicy.initialDelayMs * policy.backoffMultiplier ** Math.max(0, attempt - 1);\n\n\treturn Math.min(exponentialDelay, policy.maxDelayMs);\n}\n\nexport async function executeWithAdapterRetry<T>(options: ExecuteWithRetryOptions<T>): Promise<T> {\n\tconst policy = resolveAdapterRetryPolicy(options.policy, options.defaultIdempotent);\n\tconst sleep =\n\t\toptions.sleep ?? ((delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs)));\n\tlet lastError: string | undefined;\n\n\tfor (let attempt = 1; attempt <= policy.maxAttempts; attempt++) {\n\t\ttry {\n\t\t\tconst result = await options.execute(attempt, policy.maxAttempts);\n\t\t\tif (options.isSuccessful(result)) {\n\t\t\t\tawait options.onAttempt?.({ attempt, result, willRetry: false });\n\t\t\t\treturn result;\n\t\t\t}\n\n\t\t\tlastError = options.getError(result) ?? \"Adapter returned unsuccessful result\";\n\t\t\tconst willRetry = policy.idempotent && attempt < policy.maxAttempts;\n\t\t\tconst nextDelayMs = willRetry ? getRetryDelayMs(attempt, policy) : undefined;\n\t\t\tawait options.onAttempt?.({ attempt, result, error: lastError, willRetry, nextDelayMs });\n\n\t\t\tif (!willRetry) {\n\t\t\t\treturn result;\n\t\t\t}\n\n\t\t\tawait sleep(nextDelayMs ?? 0);\n\t\t} catch (error) {\n\t\t\tlastError = error instanceof Error ? error.message : String(error);\n\t\t\tconst willRetry = policy.idempotent && attempt < policy.maxAttempts;\n\t\t\tconst nextDelayMs = willRetry ? getRetryDelayMs(attempt, policy) : undefined;\n\t\t\tawait options.onAttempt?.({ attempt, error: lastError, willRetry, nextDelayMs });\n\n\t\t\tif (!willRetry) {\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tawait sleep(nextDelayMs ?? 0);\n\t\t}\n\t}\n\n\tthrow new Error(lastError ?? \"Adapter retry exhausted\");\n}\n"]}
@@ -0,0 +1,34 @@
1
+ interface DisplayDescriptor {
2
+ title: string;
3
+ subtitle?: string;
4
+ stateLabel?: string;
5
+ primaryFacts?: Array<{
6
+ label: string;
7
+ value: string;
8
+ }>;
9
+ }
10
+ interface CachedDisplayDescriptor extends DisplayDescriptor {
11
+ _cachedDisplay?: {
12
+ source: "event_payload" | "projection_snapshot";
13
+ cursor?: string | null;
14
+ checksum?: string;
15
+ generatedAt?: string;
16
+ };
17
+ }
18
+ type DisplayDatabaseClient = any;
19
+ interface DisplayContext<TDb = DisplayDatabaseClient> {
20
+ tenantId: string;
21
+ spaceId: string;
22
+ db: TDb;
23
+ }
24
+ type DisplayRenderer = (payload: unknown, ctx?: DisplayContext) => DisplayDescriptor | Promise<DisplayDescriptor>;
25
+ interface DisplayRendererRegistration {
26
+ eventType: string;
27
+ renderer: DisplayRenderer;
28
+ }
29
+ declare function registerDisplayRenderer(eventType: string, renderer: DisplayRenderer): void;
30
+ declare function resolveDisplayRenderer(eventType: string): DisplayRenderer | undefined;
31
+ declare function denormalizeDisplaySnapshot(payload: unknown): CachedDisplayDescriptor | undefined;
32
+ declare function renderEventDisplay(eventType: string, payload: unknown, ctx?: DisplayContext): Promise<CachedDisplayDescriptor>;
33
+
34
+ export { type CachedDisplayDescriptor, type DisplayContext, type DisplayDatabaseClient, type DisplayDescriptor, type DisplayRenderer, type DisplayRendererRegistration, denormalizeDisplaySnapshot, registerDisplayRenderer, renderEventDisplay, resolveDisplayRenderer };
@@ -0,0 +1,89 @@
1
+ import assert from 'assert';
2
+
3
+ // displays/index.ts
4
+ var registry = /* @__PURE__ */ new Map();
5
+ function registerDisplayRenderer(eventType, renderer) {
6
+ registry.set(eventType, renderer);
7
+ }
8
+ function resolveDisplayRenderer(eventType) {
9
+ return registry.get(eventType);
10
+ }
11
+ function denormalizeDisplaySnapshot(payload) {
12
+ if (payload !== null && typeof payload === "object" && "_displaySnapshot" in payload && payload._displaySnapshot !== null && typeof payload._displaySnapshot === "object" && "title" in payload._displaySnapshot) {
13
+ return payload._displaySnapshot;
14
+ }
15
+ if (payload !== null && typeof payload === "object" && "_projectionSnapshot" in payload && payload._projectionSnapshot !== null && typeof payload._projectionSnapshot === "object" && "display" in payload._projectionSnapshot && payload._projectionSnapshot.display !== null && typeof payload._projectionSnapshot.display === "object" && "title" in payload._projectionSnapshot.display) {
16
+ const snapshot = payload._projectionSnapshot;
17
+ return {
18
+ ...snapshot.display,
19
+ _cachedDisplay: {
20
+ source: "projection_snapshot",
21
+ cursor: snapshot.eventCursor,
22
+ checksum: snapshot.checksum,
23
+ generatedAt: snapshot.updatedAt
24
+ }
25
+ };
26
+ }
27
+ return void 0;
28
+ }
29
+ async function renderEventDisplay(eventType, payload, ctx) {
30
+ const snapshot = denormalizeDisplaySnapshot(payload);
31
+ if (snapshot) {
32
+ return snapshot;
33
+ }
34
+ const renderer = resolveDisplayRenderer(eventType);
35
+ if (!renderer) {
36
+ return {
37
+ title: eventType,
38
+ subtitle: "No display renderer registered for this event type."
39
+ };
40
+ }
41
+ return await renderer(payload, ctx);
42
+ }
43
+ async function _runInlineTests() {
44
+ const testEventType = "__TestEvent__";
45
+ registerDisplayRenderer(testEventType, (payload) => ({
46
+ title: `Test: ${payload.name}`,
47
+ subtitle: "registered renderer"
48
+ }));
49
+ const renderer = resolveDisplayRenderer(testEventType);
50
+ assert.ok(renderer, "resolveDisplayRenderer should return the registered renderer");
51
+ const result = await renderEventDisplay(testEventType, { name: "Alice" });
52
+ assert.strictEqual(result.title, "Test: Alice");
53
+ assert.strictEqual(result.subtitle, "registered renderer");
54
+ const snapshot = {
55
+ title: "Snapshot Title",
56
+ subtitle: "snapshot subtitle",
57
+ stateLabel: "archived",
58
+ primaryFacts: [{ label: "ID", value: "123" }]
59
+ };
60
+ const snapshotResult = await renderEventDisplay(testEventType, {
61
+ _displaySnapshot: snapshot
62
+ });
63
+ assert.deepStrictEqual(snapshotResult, snapshot);
64
+ const fallback = await renderEventDisplay("__UnknownEvent__", {});
65
+ assert.strictEqual(fallback.title, "__UnknownEvent__");
66
+ assert.strictEqual(fallback.subtitle, "No display renderer registered for this event type.");
67
+ registerDisplayRenderer("__AsyncEvent__", async (_payload, ctx) => {
68
+ return {
69
+ title: `Async: ${ctx?.tenantId ?? "no-ctx"}`
70
+ };
71
+ });
72
+ const asyncResult = await renderEventDisplay(
73
+ "__AsyncEvent__",
74
+ {},
75
+ { tenantId: "t-1", spaceId: "s-1", db: {} }
76
+ );
77
+ assert.strictEqual(asyncResult.title, "Async: t-1");
78
+ console.log("\u2705 display engine inline tests passed");
79
+ }
80
+ if (import.meta.url === `file://${process.argv[1]}`) {
81
+ _runInlineTests().catch((err) => {
82
+ console.error("\u274C display engine inline tests failed", err);
83
+ process.exit(1);
84
+ });
85
+ }
86
+
87
+ export { denormalizeDisplaySnapshot, registerDisplayRenderer, renderEventDisplay, resolveDisplayRenderer };
88
+ //# sourceMappingURL=index.js.map
89
+ //# sourceMappingURL=index.js.map