@ubercode/chronicler 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.
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Global constants used throughout Chronicler
3
+ */
4
+ /**
5
+ * Log level priority mapping
6
+ * Lower numbers = higher priority/severity
7
+ */
8
+ declare const LOG_LEVELS: {
9
+ readonly fatal: 0;
10
+ readonly critical: 1;
11
+ readonly alert: 2;
12
+ readonly error: 3;
13
+ readonly warn: 4;
14
+ readonly audit: 5;
15
+ readonly info: 6;
16
+ readonly debug: 7;
17
+ readonly trace: 8;
18
+ };
19
+ /**
20
+ * Union of all valid log level names, derived from {@link LOG_LEVELS}.
21
+ */
22
+ type LogLevel = keyof typeof LOG_LEVELS;
23
+
24
+ /**
25
+ * Field type definitions with compile-time type safety
26
+ */
27
+ /**
28
+ * Field builder with compile-time type inference
29
+ */
30
+ interface FieldBuilder<T extends string, R extends boolean> {
31
+ readonly _type: T;
32
+ readonly _required: R;
33
+ readonly _doc: string | undefined;
34
+ }
35
+ /**
36
+ * Field builder with optional marker
37
+ */
38
+ interface OptionalFieldBuilder<T extends string> extends FieldBuilder<T, false> {
39
+ readonly doc: (description: string) => OptionalFieldBuilder<T>;
40
+ }
41
+ /**
42
+ * Field builder with required marker (default)
43
+ */
44
+ interface RequiredFieldBuilder<T extends string> extends FieldBuilder<T, true> {
45
+ readonly optional: () => OptionalFieldBuilder<T>;
46
+ readonly doc: (description: string) => RequiredFieldBuilder<T>;
47
+ }
48
+ /**
49
+ * Field type builders — use these to define fields in events.
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const event = defineEvent({
54
+ * key: 'user.created',
55
+ * level: 'info',
56
+ * message: 'User created',
57
+ * fields: {
58
+ * userId: field.string().doc('User ID'),
59
+ * age: field.number().optional().doc('User age'),
60
+ * isActive: field.boolean(),
61
+ * error: field.error().optional(),
62
+ * },
63
+ * });
64
+ * ```
65
+ */
66
+ declare const field: {
67
+ readonly string: () => RequiredFieldBuilder<"string">;
68
+ readonly number: () => RequiredFieldBuilder<"number">;
69
+ readonly boolean: () => RequiredFieldBuilder<"boolean">;
70
+ readonly error: () => RequiredFieldBuilder<"error">;
71
+ };
72
+ /**
73
+ * Utility type to simplify intersections
74
+ */
75
+ type Simplify<T> = {
76
+ [K in keyof T]: T[K];
77
+ } & {};
78
+ /**
79
+ * Infer the TypeScript type from a field builder
80
+ */
81
+ type InferFieldType<F> = F extends FieldBuilder<infer T, boolean> ? T extends 'string' ? string : T extends 'number' ? number : T extends 'boolean' ? boolean : T extends 'error' ? Error | string : never : never;
82
+ /**
83
+ * Build required fields object
84
+ */
85
+ type BuildRequired<F extends Record<string, FieldBuilder<string, boolean>>> = {
86
+ [K in keyof F as F[K]['_required'] extends true ? K : never]: InferFieldType<F[K]>;
87
+ };
88
+ /**
89
+ * Build optional fields object
90
+ */
91
+ type BuildOptional<F extends Record<string, FieldBuilder<string, boolean>>> = {
92
+ [K in keyof F as F[K]['_required'] extends false ? K : never]?: InferFieldType<F[K]>;
93
+ };
94
+ /**
95
+ * Infer complete field types from field builders
96
+ * Required fields become required properties, optional fields become optional
97
+ */
98
+ type InferFields<F extends Record<string, FieldBuilder<string, boolean>>> = Simplify<BuildRequired<F> & BuildOptional<F>>;
99
+
100
+ /**
101
+ * Event definition with compile-time type safety
102
+ */
103
+ interface EventDefinition<Key extends string = string, Fields extends Record<string, FieldBuilder<string, boolean>> = Record<string, FieldBuilder<string, boolean>>> {
104
+ readonly key: Key;
105
+ readonly level: LogLevel;
106
+ readonly message: string;
107
+ readonly doc?: string;
108
+ readonly fields?: Fields;
109
+ }
110
+ /**
111
+ * Helper to extract field types from an event definition
112
+ */
113
+ type EventFields<E> = E extends EventDefinition<string, infer F> ? F extends Record<string, FieldBuilder<string, boolean>> ? InferFields<F> : Record<string, never> : never;
114
+ type EventRecord = Record<string, EventDefinition<string, Record<string, FieldBuilder<string, boolean>>>>;
115
+ interface SystemEventGroup {
116
+ readonly key: string;
117
+ readonly type: 'system';
118
+ readonly doc?: string;
119
+ readonly events?: EventRecord;
120
+ readonly groups?: Record<string, SystemEventGroup | CorrelationEventGroup>;
121
+ }
122
+ interface CorrelationEventGroup {
123
+ readonly key: string;
124
+ readonly type: 'correlation';
125
+ readonly doc?: string;
126
+ readonly timeout?: number;
127
+ readonly events?: EventRecord;
128
+ readonly groups?: Record<string, SystemEventGroup | CorrelationEventGroup>;
129
+ }
130
+ /** Field definitions for auto-generated correlation lifecycle events. */
131
+ declare const correlationAutoFields: {
132
+ start: {};
133
+ complete: {
134
+ duration: OptionalFieldBuilder<"number">;
135
+ };
136
+ fail: {
137
+ duration: OptionalFieldBuilder<"number">;
138
+ error: OptionalFieldBuilder<"error">;
139
+ };
140
+ timeout: {};
141
+ };
142
+ type CorrelationAutoEvents = {
143
+ [Key in keyof typeof correlationAutoFields]: EventDefinition<string, (typeof correlationAutoFields)[Key]>;
144
+ };
145
+ type WithAutoEvents<Event extends EventRecord | undefined> = (Event extends EventRecord ? Event : Record<never, EventDefinition<string, Record<string, FieldBuilder<string, boolean>>>>) & CorrelationAutoEvents;
146
+ /**
147
+ * Define an event with compile-time type safety.
148
+ *
149
+ * `as const` is **not required** — `defineEvent` uses TypeScript `const` generic
150
+ * parameters (TS 5.0+), so literal types and field builders are narrowed automatically.
151
+ * You may still add `as const` if you prefer, but it has no effect.
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * const userCreated = defineEvent({
156
+ * key: 'user.created',
157
+ * level: 'info',
158
+ * message: 'User created',
159
+ * doc: 'Emitted when a new user is created',
160
+ * fields: {
161
+ * userId: field.string().doc('User ID'),
162
+ * email: field.string(),
163
+ * age: field.number().optional(),
164
+ * },
165
+ * });
166
+ * ```
167
+ *
168
+ * @param event - Event definition with key, level, message, optional doc and fields
169
+ * @returns The same event definition, typed for compile-time inference
170
+ * @throws {Error} If the event key does not match the required dotted camelCase format
171
+ */
172
+ declare const defineEvent: <const Key extends string, const Fields extends Record<string, FieldBuilder<string, boolean>>>(event: EventDefinition<Key, Fields>) => EventDefinition<Key, Fields>;
173
+ /**
174
+ * Define a system or correlation event group for organizational purposes.
175
+ *
176
+ * Groups provide a namespace hierarchy for events. For correlation groups,
177
+ * prefer {@link defineCorrelationGroup} which adds automatic lifecycle events.
178
+ *
179
+ * @param group - Event group definition (system or correlation)
180
+ * @returns The same group definition, typed for compile-time inference
181
+ */
182
+ declare const defineEventGroup: <Group extends SystemEventGroup | CorrelationEventGroup>(group: Group) => Group;
183
+ /**
184
+ * Define a correlation event group with automatic lifecycle events
185
+ *
186
+ * **What is a correlation group?**
187
+ * A correlation represents a logical unit of work with a defined lifecycle
188
+ * (start, complete, timeout). Common examples: HTTP requests, batch jobs, workflows.
189
+ *
190
+ * **Automatic events added:**
191
+ * - `{key}.start` - Emitted when startCorrelation() is called
192
+ * - `{key}.complete` - Emitted when complete() is called (includes duration)
193
+ * - `{key}.fail` - Emitted when fail() is called (includes duration and error)
194
+ * - `{key}.timeout` - Emitted if no activity within timeout period
195
+ *
196
+ * **Timeout behavior:**
197
+ * - Defaults to 5 minutes (300,000ms) if not specified
198
+ * - Timer resets on ANY activity (log events, fork creation)
199
+ * - Set to 0 to disable timeout
200
+ *
201
+ * **Type safety:**
202
+ * TypeScript will infer all event keys and field types. The return type
203
+ * includes both your events and the auto-generated lifecycle events.
204
+ *
205
+ * @param group - Correlation group definition
206
+ * @returns Normalized group with auto-events and default timeout
207
+ *
208
+ * @example
209
+ * ```typescript
210
+ * const requestGroup = defineCorrelationGroup({
211
+ * key: 'api.request',
212
+ * doc: 'HTTP request lifecycle',
213
+ * timeout: 30_000, // 30 seconds
214
+ * events: {
215
+ * validated: defineEvent({
216
+ * key: 'api.request.validated',
217
+ * level: 'debug',
218
+ * message: 'Request validated',
219
+ * doc: 'Validation passed',
220
+ * }),
221
+ * processed: defineEvent({
222
+ * key: 'api.request.processed',
223
+ * level: 'info',
224
+ * message: 'Request processed',
225
+ * doc: 'Processing complete',
226
+ * fields: { statusCode: { type: 'number', required: true } },
227
+ * }),
228
+ * },
229
+ * });
230
+ *
231
+ * // Auto-events available:
232
+ * // - requestGroup.events.start
233
+ * // - requestGroup.events.complete
234
+ * // - requestGroup.events.timeout
235
+ * // - requestGroup.events.validated (your event)
236
+ * // - requestGroup.events.processed (your event)
237
+ * ```
238
+ */
239
+ declare const defineCorrelationGroup: <Group extends CorrelationEventGroup>(group: Group) => Omit<Group, "events" | "timeout"> & {
240
+ events: WithAutoEvents<Group["events"]>;
241
+ timeout: number;
242
+ };
243
+
244
+ interface ValidationMetadata {
245
+ readonly missingFields?: string[];
246
+ readonly typeErrors?: string[];
247
+ readonly invalidValues?: string[];
248
+ readonly unknownFields?: string[];
249
+ }
250
+
251
+ interface LogPayload {
252
+ readonly eventKey: string;
253
+ readonly fields: Record<string, unknown>;
254
+ readonly correlationId: string;
255
+ readonly forkId: string;
256
+ readonly metadata: Record<string, unknown>;
257
+ readonly timestamp: string;
258
+ readonly _validation?: ValidationMetadata;
259
+ }
260
+ type LogBackend = Record<LogLevel, (message: string, payload: LogPayload) => void>;
261
+ /**
262
+ * Create a zero-config backend that logs to the console.
263
+ *
264
+ * Maps each of the 9 Chronicler levels to the appropriate `console` method:
265
+ * fatal/critical/alert/error → `console.error`, warn → `console.warn`,
266
+ * audit/info → `console.info`, debug/trace → `console.debug`.
267
+ *
268
+ * @returns A fully populated LogBackend using console methods for all levels
269
+ */
270
+ declare const createConsoleBackend: () => LogBackend;
271
+ /**
272
+ * Create a backend from a partial set of handlers.
273
+ *
274
+ * For each missing level, the fallback chain is tried in order (e.g. `fatal` →
275
+ * `critical` → `error` → `warn` → `info`). If no fallback is provided either,
276
+ * the corresponding `console` method is used.
277
+ *
278
+ * @param partial - Partial backend with handlers for a subset of log levels
279
+ * @returns A fully populated LogBackend with fallbacks applied for missing levels
280
+ */
281
+ declare const createBackend: (partial: Partial<LogBackend>) => LogBackend;
282
+ /**
283
+ * A routing rule that pairs a backend with an optional filter.
284
+ *
285
+ * When `filter` is omitted the backend receives all events.
286
+ * When provided, the backend only receives events for which `filter` returns `true`.
287
+ */
288
+ interface BackendRoute {
289
+ readonly backend: LogBackend;
290
+ readonly filter?: (level: LogLevel, payload: LogPayload) => boolean;
291
+ }
292
+ /**
293
+ * Create a backend that routes events to multiple backends based on filter rules.
294
+ *
295
+ * Each route pairs a backend with an optional filter function. Events are
296
+ * dispatched to every route whose filter matches (or to all routes without a
297
+ * filter). This enables splitting logs into separate streams — for example,
298
+ * maintenance/debug logs to stdout and audit events to a dedicated store.
299
+ *
300
+ * @param routes - One or more backend routes with optional filters
301
+ * @returns A single LogBackend that fans out to the matching routes
302
+ * @throws {Error} If no routes are provided
303
+ *
304
+ * @example
305
+ * ```typescript
306
+ * const router = createRouterBackend([
307
+ * { backend: consoleBackend, filter: (level, payload) => !payload.eventKey.startsWith('audit.') },
308
+ * { backend: auditBackend, filter: (level, payload) => payload.eventKey.startsWith('audit.') },
309
+ * ]);
310
+ *
311
+ * const chronicle = createChronicle({ backend: router, metadata: { appName: 'my-app' } });
312
+ * ```
313
+ */
314
+ declare const createRouterBackend: (routes: BackendRoute[]) => LogBackend;
315
+
316
+ type ContextValue = string | number | boolean | null;
317
+ type ContextRecord = Record<string, ContextValue>;
318
+ interface ContextCollisionDetail {
319
+ readonly key: string;
320
+ readonly existingValue: ContextValue;
321
+ readonly attemptedValue: ContextValue;
322
+ }
323
+ interface ContextValidationResult {
324
+ readonly reserved: string[];
325
+ readonly collisionDetails: ContextCollisionDetail[];
326
+ readonly dropped: string[];
327
+ }
328
+
329
+ interface ChroniclerLimits {
330
+ readonly maxContextKeys?: number;
331
+ /**
332
+ * Maximum fork nesting depth. A depth of N means N levels of nesting
333
+ * from root (e.g. depth 3 allows root → child → grandchild → great-grandchild).
334
+ * Defaults to {@link DEFAULT_MAX_FORK_DEPTH}.
335
+ */
336
+ readonly maxForkDepth?: number;
337
+ readonly maxActiveCorrelations?: number;
338
+ }
339
+ interface ChroniclerConfig {
340
+ readonly backend?: LogBackend;
341
+ readonly metadata: ContextRecord;
342
+ readonly correlationIdGenerator?: () => string;
343
+ readonly limits?: ChroniclerLimits;
344
+ /**
345
+ * When `true`, throws a `ChroniclerError` with code `FIELD_VALIDATION`
346
+ * for field validation errors (missing required fields, type mismatches).
347
+ * Useful for CI/CD enforcement. Defaults to `false`.
348
+ */
349
+ readonly strict?: boolean;
350
+ /**
351
+ * Minimum log level to emit. Events below this level are silently dropped.
352
+ * Uses priority ordering: fatal(0) > critical(1) > ... > trace(8).
353
+ * Defaults to `'trace'` (all events emitted).
354
+ */
355
+ readonly minLevel?: LogLevel;
356
+ }
357
+ interface Chronicler {
358
+ /** Emit a typed event. Fields are validated against the event definition. */
359
+ event<E extends EventDefinition>(event: E, fields: EventFields<E>): void;
360
+ /**
361
+ * Untyped escape hatch — log at any level without a pre-defined event.
362
+ * Useful for incremental adoption or ad-hoc debugging.
363
+ */
364
+ log(level: LogLevel, message: string, fields?: Record<string, unknown>): void;
365
+ /** Add key-value context that is attached to all subsequent events. */
366
+ addContext(context: ContextRecord): ContextValidationResult;
367
+ /** Start a correlation — a logical unit of work with lifecycle events. */
368
+ startCorrelation(group: CorrelationEventGroup, metadata?: ContextRecord): CorrelationChronicle;
369
+ /** Create an isolated child chronicle that inherits context. */
370
+ fork(context?: ContextRecord): Chronicler;
371
+ }
372
+ /**
373
+ * A correlation represents a logical unit of work with a defined lifecycle.
374
+ *
375
+ * Unlike a root Chronicler, a correlation:
376
+ * - Has a single shared correlation ID for all events
377
+ * - Has lifecycle events (start, complete, fail, timeout)
378
+ * - Can timeout if not completed within the configured duration
379
+ * - Cannot start nested correlations (use fork() for parallel work within a correlation)
380
+ */
381
+ interface CorrelationChronicle {
382
+ /** Emit a typed event within this correlation. */
383
+ event<E extends EventDefinition>(event: E, fields: EventFields<E>): void;
384
+ /**
385
+ * Untyped escape hatch — log at any level without a pre-defined event.
386
+ * Useful for incremental adoption or ad-hoc debugging.
387
+ */
388
+ log(level: LogLevel, message: string, fields?: Record<string, unknown>): void;
389
+ /** Add key-value context that is attached to all subsequent events. */
390
+ addContext(context: ContextRecord): ContextValidationResult;
391
+ /** Create an isolated child chronicle that inherits context. */
392
+ fork(context?: ContextRecord): Chronicler;
393
+ /** Mark the correlation as successfully completed. Emits the `.complete` event. */
394
+ complete(fields?: Record<string, unknown>): void;
395
+ /** Mark the correlation as failed. Emits the `.fail` event at error level. */
396
+ fail(error?: unknown, fields?: Record<string, unknown>): void;
397
+ /** Mark the correlation as timed out. Called automatically by the timer. */
398
+ timeout(): void;
399
+ }
400
+ /**
401
+ * Create a root Chronicler instance.
402
+ *
403
+ * This is the main entry point for the library. The returned `Chronicler`
404
+ * can log events, add context, start correlations, and create forks.
405
+ *
406
+ * @param config - Chronicler configuration with optional backend, metadata, and optional correlation settings
407
+ * @returns A configured `Chronicler` instance
408
+ * @throws {ChroniclerError} `UNSUPPORTED_LOG_LEVEL` if the backend is missing required methods
409
+ * @throws {ChroniclerError} `RESERVED_FIELD` if `config.metadata` contains reserved field names
410
+ */
411
+ declare const createChronicle: (config: ChroniclerConfig) => Chronicler;
412
+
413
+ type ChroniclerErrorCode = 'UNSUPPORTED_LOG_LEVEL' | 'RESERVED_FIELD' | 'BACKEND_METHOD' | 'FORK_DEPTH_EXCEEDED' | 'CORRELATION_LIMIT_EXCEEDED' | 'FIELD_VALIDATION';
414
+ /**
415
+ * Typed error class for Chronicler configuration and runtime failures.
416
+ * Uses a discriminator `code` to identify the specific error category.
417
+ */
418
+ declare class ChroniclerError extends Error {
419
+ readonly code: ChroniclerErrorCode;
420
+ /**
421
+ * @param code - Machine-readable error category discriminator
422
+ * @param message - Human-readable description of the error
423
+ */
424
+ constructor(code: ChroniclerErrorCode, message: string);
425
+ }
426
+
427
+ export { type BackendRoute, type Chronicler, type ChroniclerConfig, ChroniclerError, type ChroniclerErrorCode, type ChroniclerLimits, type ContextCollisionDetail, type ContextRecord, type ContextValidationResult, type CorrelationChronicle, type CorrelationEventGroup, type EventDefinition, type EventFields, type FieldBuilder, type InferFieldType, type InferFields, type LogBackend, type LogLevel, type LogPayload, type SystemEventGroup, createBackend, createChronicle, createConsoleBackend, createRouterBackend, defineCorrelationGroup, defineEvent, defineEventGroup, field };