@lovelaces-io/storyteller 0.3.0 → 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,744 @@
1
+ /**
2
+ * Which default audience a storyteller registers.
3
+ *
4
+ * - `text` — colorized console output for a person watching
5
+ * - `ndjson` — one JSON object per line for a program reading
6
+ */
7
+ type OutputFormat = "text" | "ndjson";
8
+ /**
9
+ * A level written any of the ways people and agents actually write it.
10
+ * `report("...", { level: "warn" })` should not be a type error.
11
+ */
12
+ type LevelInput = StoryLevel | "info" | "information" | "warn" | "warning" | "oops" | "error";
13
+ /**
14
+ * Resolve any accepted level spelling to a stored level label.
15
+ *
16
+ * @param input - A level in any accepted spelling
17
+ * @returns The canonical StoryLevel, defaulting to Information
18
+ */
19
+ declare function toStoryLevel(input?: LevelInput): StoryLevel;
20
+ /**
21
+ * Read an environment variable, tolerating runtimes that have no environment at all.
22
+ *
23
+ * @param name - Variable name
24
+ * @returns The trimmed value, or undefined when unset or unavailable
25
+ */
26
+ declare function readEnvironmentValue(name: string): string | undefined;
27
+ /**
28
+ * Resolve the minimum level to deliver, from an explicit option then `STORYTELLER_LEVEL`.
29
+ *
30
+ * @param requested - Explicit level, if the caller set one
31
+ * @returns The threshold level, defaulting to Information (deliver everything)
32
+ */
33
+ declare function resolveMinimumLevel(requested?: StoryLevel | string): StoryLevel;
34
+ /**
35
+ * Check whether a level clears the configured minimum.
36
+ *
37
+ * @param level - The emission's level
38
+ * @param minimum - The configured threshold
39
+ */
40
+ declare function meetsLevel(level: StoryLevel, minimum: StoryLevel): boolean;
41
+ /**
42
+ * Resolve which default audience to register, from an explicit option then
43
+ * `STORYTELLER_FORMAT`.
44
+ *
45
+ * Deliberately not inferred from whether stdout is a TTY: output that silently
46
+ * changes shape when a process is piped is a debugging afternoon nobody asked for.
47
+ *
48
+ * @param requested - Explicit format, if the caller set one
49
+ * @returns The format, defaulting to text
50
+ */
51
+ declare function resolveOutputFormat(requested?: OutputFormat): OutputFormat;
52
+ /**
53
+ * Resolve whether to colorize, from an explicit option then `STORYTELLER_COLOR`.
54
+ *
55
+ * @param requested - Explicit choice, if the caller set one
56
+ * @returns Whether colors should be used, defaulting to true
57
+ */
58
+ declare function resolveColors(requested?: boolean): boolean;
59
+
60
+ /**
61
+ * Redaction by value, not only by key.
62
+ *
63
+ * Key-name matching catches `password: "hunter2"`. It does not catch a Stripe
64
+ * key inside an error message, a bearer token in a URL, or a private key
65
+ * pasted into a note. Once stories are persisted and read back by agents, a
66
+ * leaked secret is durable and retrievable, so the value itself has to be
67
+ * recognised too.
68
+ *
69
+ * Defense in depth, not a guarantee: these are recognisable formats and
70
+ * suspicious positions. A secret that looks like an ordinary word passes.
71
+ * The balance leans toward keeping records readable — a viewer full of
72
+ * `[redacted]` is no viewer — with a strict mode for the other trade.
73
+ */
74
+
75
+ /** Marker written in place of a redacted value or span */
76
+ declare const REDACTED = "[redacted]";
77
+ /** How hard to look inside values. `off` leaves key matching only. */
78
+ type RedactionStrictness = "off" | "balanced" | "strict";
79
+
80
+ /** A value that survives JSON.stringify with no loss and no throwing */
81
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
82
+ [key: string]: JsonValue;
83
+ };
84
+ type NormalizeOptions = {
85
+ /** How many levels deep to descend before replacing the value with a truncation marker */
86
+ maxDepth?: number;
87
+ /** How many array entries to keep before truncating */
88
+ maxArrayLength?: number;
89
+ /** How many object properties to keep before truncating */
90
+ maxProperties?: number;
91
+ /** How many characters of a string to keep before truncating */
92
+ maxStringLength?: number;
93
+ /** Property names whose values are replaced with the redaction marker */
94
+ redactKeys?: string[];
95
+ /** Set false to keep secret-shaped values as-is */
96
+ redact?: boolean;
97
+ /**
98
+ * How hard to look inside values for secrets: `balanced` (default) redacts
99
+ * recognisable token formats and random-looking strings under secret-shaped
100
+ * keys; `strict` also redacts any long random-looking run; `off` matches key
101
+ * names only.
102
+ */
103
+ redactValues?: RedactionStrictness;
104
+ };
105
+
106
+ /**
107
+ * Property names whose values are replaced with {@link REDACTED}.
108
+ * Matching ignores case and separators, so `apiKey`, `api_key` and `API-KEY` all match.
109
+ */
110
+ declare const DEFAULT_REDACT_KEYS: string[];
111
+ /**
112
+ * Convert any value into a JSON-safe structure suitable for a story record.
113
+ *
114
+ * Handles the shapes real code actually holds — errors, dates, maps, sets, class
115
+ * instances, binary buffers, circular references, throwing getters — and never throws,
116
+ * so a hostile object logged by a caller cannot break the delivery pipeline.
117
+ *
118
+ * Data dropped for size is replaced with an explicit `@truncated` marker rather than
119
+ * disappearing silently, so a consumer can tell the difference between "this was empty"
120
+ * and "this was too big".
121
+ *
122
+ * @param input - Any value
123
+ * @param options - Depth, size and redaction limits
124
+ * @returns A value that JSON.stringify can always serialize
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * normalizeValue({ user: new Map([["id", 1]]), apiKey: "sk-live-abc" });
129
+ * // { user: { "@type": "Map", entries: { id: 1 } }, apiKey: "[redacted]" }
130
+ * ```
131
+ */
132
+ declare function normalizeValue(input: unknown, options?: NormalizeOptions): JsonValue;
133
+ /**
134
+ * Convert an unknown thrown value into a serializable StoryError,
135
+ * following the `cause` chain and collecting AggregateError members.
136
+ *
137
+ * @param rawError - Any thrown or rejected value
138
+ * @param options - Depth, size and redaction limits applied to attached data
139
+ * @returns A StoryError safe to store and serialize
140
+ */
141
+ declare function normalizeError(rawError: unknown, options?: NormalizeOptions): StoryError;
142
+
143
+ /** Human-readable level labels stored in story records */
144
+ type StoryLevel = "Information" | "Warning" | "Error";
145
+ /**
146
+ * A stored context value. Always JSON-safe — whatever the caller passed in has
147
+ * already been through the normalizer by the time it reaches a record.
148
+ */
149
+ type StoryContextValue = JsonValue;
150
+ /** Context accepted from callers. Anything goes; the normalizer makes it storable. */
151
+ type StoryContextInput = unknown;
152
+ type StoryError = {
153
+ name?: string;
154
+ message?: string;
155
+ stack?: string;
156
+ cause?: JsonValue;
157
+ /** Members of an AggregateError */
158
+ errors?: StoryError[];
159
+ };
160
+ /** Origin as stored on a record */
161
+ type StoryOrigin = {
162
+ who?: StoryContextValue;
163
+ what?: StoryContextValue;
164
+ where?: StoryContextValue;
165
+ };
166
+ /** Origin as accepted from callers */
167
+ type StoryOriginInput = {
168
+ who?: StoryContextInput;
169
+ what?: StoryContextInput;
170
+ where?: StoryContextInput;
171
+ };
172
+ type StoryNote = {
173
+ timestamp: string;
174
+ /**
175
+ * Position within the story, assigned when the note is taken. Gap-free from 0.
176
+ * Optional so records written before sequencing existed still typecheck.
177
+ */
178
+ sequence?: number;
179
+ note: string;
180
+ /** Omitted when the note carries the story's default Information level */
181
+ level?: StoryLevel;
182
+ who?: StoryContextValue;
183
+ what?: StoryContextValue;
184
+ where?: StoryContextValue;
185
+ error?: StoryError;
186
+ };
187
+ type StoryEventBase = {
188
+ timestamp: string;
189
+ level: StoryLevel;
190
+ title: string;
191
+ /**
192
+ * Correlates every note emission with the story it belongs to. Always set on
193
+ * events this library builds; optional so older stored records still typecheck.
194
+ */
195
+ storyId?: string;
196
+ /**
197
+ * The story this one is a chapter of. Absent on a top-level story.
198
+ * Following this field reconstructs the tree of a nested run.
199
+ */
200
+ parentStoryId?: string;
201
+ origin?: StoryOrigin;
202
+ notes: StoryNote[];
203
+ durationMs?: number;
204
+ /**
205
+ * How many emissions were dropped for back-pressure while this story was being
206
+ * collected. Present only when something was actually lost, so the loss shows up
207
+ * in the record instead of vanishing.
208
+ */
209
+ droppedEmissions?: number;
210
+ error?: StoryError;
211
+ };
212
+ type ReportOptions = {
213
+ timezone?: string;
214
+ locale?: string;
215
+ detail?: "brief" | "normal" | "full";
216
+ noteLimit?: number;
217
+ showData?: boolean;
218
+ colors?: boolean;
219
+ };
220
+ /** @deprecated Use ReportOptions instead */
221
+ type StorySummaryOptions = ReportOptions;
222
+ type PreviewOptions = ReportOptions & {
223
+ title?: string;
224
+ level?: StoryLevel;
225
+ error?: unknown;
226
+ };
227
+ /** @deprecated Use PreviewOptions instead */
228
+ type StoryPreviewOptions = PreviewOptions;
229
+ type ReportNote = {
230
+ timestamp: string;
231
+ when: string;
232
+ note: string;
233
+ text: string;
234
+ who?: StoryContextValue;
235
+ what?: StoryContextValue;
236
+ where?: StoryContextValue;
237
+ error?: StoryError;
238
+ };
239
+ /** @deprecated Use ReportNote instead */
240
+ type StorySummaryNote = ReportNote;
241
+ type StoryReport = {
242
+ title: string;
243
+ level: StoryLevel;
244
+ when: string;
245
+ durationMs?: number;
246
+ duration?: string;
247
+ origin?: StoryEventBase["origin"];
248
+ notes: ReportNote[];
249
+ error?: StoryError;
250
+ };
251
+ /** @deprecated Use StoryReport instead */
252
+ type StorySummaryData = StoryReport;
253
+ type FormattedReport = {
254
+ text: string;
255
+ data: StoryReport;
256
+ };
257
+ /** @deprecated Use FormattedReport instead */
258
+ type StorySummary = FormattedReport;
259
+ type StoryEvent = StoryEventBase & {
260
+ kind: "story";
261
+ summarize: (options?: ReportOptions) => FormattedReport;
262
+ };
263
+ /** @deprecated Use StoryEvent — the story-shaped emission */
264
+ type StoryEmission = StoryEvent;
265
+ /** The two things an audience can hear */
266
+ type EmissionKind = "note" | "story";
267
+ /**
268
+ * A single beat, delivered the moment it happens when narration is live.
269
+ *
270
+ * `storyId` and `sequence` are what make streaming lossless: a consumer holding
271
+ * the beats of a story can order and group them back into the record that
272
+ * collected narration would have produced.
273
+ */
274
+ type NoteEmission = StoryNote & {
275
+ kind: "note";
276
+ storyId: string;
277
+ parentStoryId?: string;
278
+ sequence: number;
279
+ level: StoryLevel;
280
+ origin?: StoryOrigin;
281
+ };
282
+ type Emission = NoteEmission | StoryEvent;
283
+ /** The emission type an audience receives, given the kinds it hears */
284
+ type EmissionOf<Kind extends EmissionKind> = Kind extends "note" ? NoteEmission : StoryEvent;
285
+ /**
286
+ * An audience, typed by what it hears.
287
+ *
288
+ * With no `hears`, it hears stories only and `accepts`/`hear` receive a
289
+ * `StoryEvent` — so an audience written before live narration existed compiles
290
+ * unchanged, including one that hands the event to a helper typed for
291
+ * `StoryEvent`. `hears: ["note"]` gives `NoteEmission`; `["note", "story"]`
292
+ * gives the union, and the code narrows on `kind`.
293
+ */
294
+ type AudienceMember<Kind extends EmissionKind = "story"> = {
295
+ name: string;
296
+ /** Which emission kinds this audience wants. Defaults to `["story"]`. */
297
+ hears?: Kind[];
298
+ accepts?(emission: EmissionOf<Kind>): boolean;
299
+ hear(emission: EmissionOf<Kind>): void | Promise<void>;
300
+ };
301
+ /** Any audience, whatever it hears — the shape the registry stores */
302
+ type AnyAudienceMember = AudienceMember<EmissionKind>;
303
+ /**
304
+ * How a storyteller narrates.
305
+ *
306
+ * - `collected` — beats are buffered and leave as one story record (the default)
307
+ * - `live` — each beat is emitted as it happens, and the story still lands at the end
308
+ *
309
+ * Live narration adds emissions, it never removes them: a consumer that only wants
310
+ * beats says so with `hears: ["note"]` rather than by silencing the record.
311
+ */
312
+ type Narration = "collected" | "live";
313
+ /** @deprecated `both` is now the behavior of `live` — beats stream and the story still lands */
314
+ type NarrationInput = Narration | "both";
315
+ type NoteData = {
316
+ who?: StoryContextInput;
317
+ what?: StoryContextInput;
318
+ where?: StoryContextInput;
319
+ error?: unknown;
320
+ /** Level for this beat alone. Defaults to Information. */
321
+ level?: LevelInput;
322
+ /** Emit this beat immediately even when narration is collected */
323
+ live?: boolean;
324
+ /** Deliver this beat only to the named audiences */
325
+ to?: string[];
326
+ };
327
+ type FinishOptions = {
328
+ /** Defaults to Information */
329
+ level?: LevelInput;
330
+ /** The error that ended the story, normalized onto the record */
331
+ error?: unknown;
332
+ };
333
+ type ChapterOptions = {
334
+ /** Merged over the parent's origin */
335
+ origin?: StoryOriginInput;
336
+ /** Defaults to the parent's setting */
337
+ narration?: NarrationInput;
338
+ /** Defaults to the parent's setting */
339
+ level?: LevelInput;
340
+ /** Defaults to the parent's handler */
341
+ onAudienceError?: AudienceErrorHandler;
342
+ /** Defaults to the parent's bound */
343
+ maxInFlight?: number;
344
+ };
345
+ type StorytellerOptions = {
346
+ origin?: StoryOriginInput;
347
+ audiences?: AnyAudienceMember[];
348
+ /** Defaults to `STORYTELLER_NARRATION`, then `collected` */
349
+ narration?: NarrationInput;
350
+ /**
351
+ * Which default audience to register: colorized text for a person, NDJSON for a
352
+ * program. Defaults to `STORYTELLER_FORMAT`, then `text`.
353
+ */
354
+ format?: OutputFormat;
355
+ /**
356
+ * Share another storyteller's audience registry instead of creating one.
357
+ * Audiences added to it later reach this storyteller too. When given, no
358
+ * default audience is registered — the registry already has whatever it has.
359
+ */
360
+ audience?: AudienceRegistry;
361
+ /** The story this one is a chapter of. Set by `chapter()`. */
362
+ parentStoryId?: string;
363
+ /**
364
+ * Drop emissions below this level before they reach any audience.
365
+ * Defaults to `STORYTELLER_LEVEL`, then Information (deliver everything).
366
+ */
367
+ level?: LevelInput;
368
+ /**
369
+ * Called when an audience throws or rejects. Without one, a single throttled
370
+ * warning per audience goes to the console — a logging library that loses
371
+ * records in silence is worse than one that complains.
372
+ */
373
+ onAudienceError?: AudienceErrorHandler;
374
+ /**
375
+ * Cap on deliveries in flight to a single audience at once. Live narration is
376
+ * fire-and-forget, so a slow audience would otherwise grow an unbounded queue.
377
+ * Past the cap, emissions are dropped and counted on the closing story.
378
+ */
379
+ maxInFlight?: number;
380
+ };
381
+ /** Called when an audience member throws or rejects while hearing an emission */
382
+ type AudienceErrorHandler = (error: unknown, member: AnyAudienceMember, emission: Emission) => void;
383
+ /** Manages the set of audience members that receive story events */
384
+ declare class AudienceRegistry {
385
+ private members;
386
+ /** Register an audience member, replacing any existing member with the same name */
387
+ add<Kind extends EmissionKind = "story">(member: AudienceMember<Kind>): this;
388
+ /** Remove an audience member by name */
389
+ remove(name: string): this;
390
+ /** Return all registered audience members */
391
+ getAll(): AnyAudienceMember[];
392
+ /** Return only the audience members matching the given names */
393
+ getOnly(names: string[]): AnyAudienceMember[];
394
+ /** Check if an audience member is registered by name */
395
+ has(name: string): boolean;
396
+ /** List the names of all registered audience members */
397
+ names(): string[];
398
+ }
399
+ /**
400
+ * Collects timestamped notes and emits them as one structured story — and, when
401
+ * narration is live, emits each note the moment it is taken.
402
+ *
403
+ * @example
404
+ * ```ts
405
+ * const story = new Storyteller({ origin: { who: "api-server" }, narration: "live" });
406
+ * story.report("Request received", { what: { path: "/checkout" } });
407
+ * story.report("Validated cart");
408
+ * story.finish("Checkout started");
409
+ * ```
410
+ */
411
+ declare class Storyteller {
412
+ readonly audience: AudienceRegistry;
413
+ private readonly origin?;
414
+ private readonly parentStoryId?;
415
+ private notes;
416
+ private narration;
417
+ private readonly minimumLevel;
418
+ private readonly onAudienceError;
419
+ private readonly maxInFlight;
420
+ /** Deliveries currently awaiting each audience, keyed by audience name */
421
+ private readonly inFlight;
422
+ /** Emissions dropped for back-pressure since the current story began */
423
+ private droppedEmissions;
424
+ /** Identifies the story currently being collected; regenerated after each telling */
425
+ private storyId;
426
+ /** Position of the next note within the current story */
427
+ private nextSequence;
428
+ constructor(options?: StorytellerOptions);
429
+ /**
430
+ * Switch between collected and live narration at runtime.
431
+ * Takes effect on the next note; already-buffered notes are not replayed.
432
+ *
433
+ * @param narration - `collected` to buffer, `live` to emit each note as it happens
434
+ * @returns `this` for chaining
435
+ */
436
+ narrate(narration: NarrationInput): this;
437
+ /** The id of the story currently being collected */
438
+ get currentStoryId(): string;
439
+ /**
440
+ * Start a chapter: a child storyteller whose stories are linked back to this
441
+ * one by `parentStoryId`.
442
+ *
443
+ * Real work nests — an agent spawns subtasks, a batch runs per-item operations.
444
+ * A chapter keeps each of those a complete story in its own right while leaving
445
+ * the run reconstructable as a tree.
446
+ *
447
+ * The child shares this storyteller's audience registry, so audiences added
448
+ * later reach it too, and inherits narration, level and delivery settings.
449
+ * Its stories are separate records — a chapter is not folded into the parent's
450
+ * notes.
451
+ *
452
+ * @param options - Origin to merge over the parent's, and any setting to override
453
+ * @returns A child Storyteller
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * for (const account of accounts) {
458
+ * const chapter = story.chapter({ origin: { what: account.id } });
459
+ * chapter.report("Fetching invoices");
460
+ * chapter.finish(`Synced ${account.id}`);
461
+ * }
462
+ * ```
463
+ */
464
+ chapter(options?: ChapterOptions): Storyteller;
465
+ /**
466
+ * Report a beat of the current story.
467
+ *
468
+ * In collected narration the beat is buffered and leaves with the story. In live
469
+ * narration it is emitted the moment you call this, so whoever is tuned in sees
470
+ * the work as it happens.
471
+ *
472
+ * Accepts anything, not just a string — pass an error, an API response, a Map, a
473
+ * class instance — and the value is normalized into a storable shape with the note
474
+ * text derived from it.
475
+ *
476
+ * @param input - What happened: a message, or any value to describe
477
+ * @param data - Optional context: who did it, what was involved, where it happened, any error
478
+ * @returns `this` for chaining
479
+ *
480
+ * @example
481
+ * ```ts
482
+ * story.report("Card charged", { what: { amount: "$42" }, where: "stripe" });
483
+ * story.report(await response.json());
484
+ * ```
485
+ */
486
+ report(input: unknown, data?: NoteData): this;
487
+ /** Clear all accumulated notes without emitting a story, and start a new story id */
488
+ reset(): this;
489
+ /** Preview the current notes as a formatted report without emitting or clearing them */
490
+ summarize(options?: PreviewOptions): FormattedReport;
491
+ /**
492
+ * Finish the story: emit everything collected so far as one record, and start fresh.
493
+ *
494
+ * @param title - What the story was about
495
+ * @param options - Level, and the error that ended it
496
+ * @returns A one-shot handle whose `.to()` overrides the audience list — call it
497
+ * synchronously, delivery happens on the next microtask
498
+ *
499
+ * @example
500
+ * ```ts
501
+ * story.finish("Sync complete");
502
+ * story.finish("Sync failed", { level: "oops", error }).to("db");
503
+ * ```
504
+ */
505
+ finish(title: string, options?: FinishOptions): {
506
+ to: (...names: string[]) => void;
507
+ };
508
+ /** @deprecated Use `finish(title)`. Removed at 1.0. */
509
+ tell(title: string): {
510
+ to: (...names: string[]) => void;
511
+ };
512
+ /** @deprecated Use `finish(title, { level: "warn" })`. Removed at 1.0. */
513
+ warn(title: string): {
514
+ to: (...names: string[]) => void;
515
+ };
516
+ /** @deprecated Use `finish(title, { level: "oops", error })`. Removed at 1.0. */
517
+ oops(title: string, error?: unknown): {
518
+ to: (...names: string[]) => void;
519
+ };
520
+ /** @deprecated Use `report()`. Removed at 1.0. */
521
+ note(input: unknown, data?: NoteData): this;
522
+ /** Emit a single note to the audiences listening for notes */
523
+ private emitNote;
524
+ /** Build a story event and schedule delivery, returning a handle to override the audience list */
525
+ private createDelivery;
526
+ /** Assemble the story event from current notes and start a fresh story */
527
+ private buildEvent;
528
+ /** Begin a new story: fresh id, sequence back to zero */
529
+ private startNewStory;
530
+ /** Deliver an emission to the audience members listening for its kind */
531
+ private deliver;
532
+ /** Run an audience's accepts() without letting a throw from it lose the emission */
533
+ private acceptsSafely;
534
+ /**
535
+ * Hand an emission to one audience, keeping its failures and its slowness
536
+ * contained: a throw is reported rather than swallowed, and a backlog is dropped
537
+ * rather than grown without limit.
538
+ */
539
+ private hearSafely;
540
+ /** Report an audience failure without ever letting it reach caller code */
541
+ private handleAudienceError;
542
+ }
543
+
544
+ /**
545
+ * The StoryStore contract: where stories go, and how they come back.
546
+ *
547
+ * A story is the unit of retrieval — self-contained, ordered, small enough to
548
+ * fit in a context window. The contract is deliberately small: append, get by
549
+ * id, query by structured criteria, walk chapters, and forget. Every adapter
550
+ * (in memory, a file, SQLite, Postgres) answers the same questions, so the
551
+ * questions — not each adapter's query language — are the contract.
552
+ */
553
+
554
+ /** A story as kept: the record without the `summarize` method, with an id it can be fetched by */
555
+ type StoredStory = StoryEventBase & {
556
+ storyId: string;
557
+ };
558
+ /**
559
+ * Structured criteria. Every field narrows; an empty query matches everything.
560
+ * Built by hand or by the query vocabulary; adapters receive this object,
561
+ * never a string to parse.
562
+ */
563
+ type StoryQuery = {
564
+ /** Stories whose timestamp is at or after this instant */
565
+ since?: Date;
566
+ /** Stories whose timestamp is before this instant */
567
+ until?: Date;
568
+ /** Exactly this level, or any of these */
569
+ level?: StoryLevel | StoryLevel[];
570
+ /** At least this level: Warning matches Warning and Error */
571
+ minimumLevel?: StoryLevel;
572
+ /** Case-insensitive match over the title, note text, scalar context and error messages */
573
+ about?: string;
574
+ /** Case-insensitive match over the origin's who / what / where */
575
+ from?: string;
576
+ /** Chapters of this story */
577
+ parentStoryId?: string;
578
+ /** Stories that took longer than this */
579
+ slowerThanMs?: number;
580
+ /** Stories that carry an error or closed at Error level */
581
+ failed?: boolean;
582
+ limit?: number;
583
+ offset?: number;
584
+ /** Newest first unless told otherwise */
585
+ order?: "newest" | "oldest";
586
+ };
587
+ type StoryStore = {
588
+ /** Keep a story. Replaces an earlier record with the same id. Never throws synchronously. */
589
+ append(event: StoryEvent | StoredStory): Promise<void>;
590
+ /** One story, complete, by id */
591
+ get(storyId: string): Promise<StoredStory | undefined>;
592
+ /** Stories matching the criteria, newest first by default */
593
+ query(criteria?: StoryQuery): Promise<StoredStory[]>;
594
+ /** The chapters of a story, in the order they were told */
595
+ children(parentStoryId: string): Promise<StoredStory[]>;
596
+ /** Forget stories that began before the boundary; returns how many were removed */
597
+ prune(before: Date): Promise<number>;
598
+ };
599
+ type ToStoredStoryOptions = {
600
+ /**
601
+ * Redaction at the storage boundary. The normalizer already redacted at
602
+ * capture; this pass covers a record fed to a store by hand, or one
603
+ * normalized with redaction off, before it becomes durable. Default
604
+ * `balanced`; `off` trusts the record as given.
605
+ */
606
+ redactValues?: RedactionStrictness;
607
+ };
608
+ /**
609
+ * The record as a store keeps it: a deep copy of the JSON-safe fields, so a
610
+ * caller holding the event cannot change what was stored, and no method rides
611
+ * along. A record without an id — one written before ids existed — is given
612
+ * one. Secrets are redacted once more on the way in: persisted is the moment
613
+ * a leaked value stops being a line that scrolled past.
614
+ */
615
+ declare function toStoredStory(event: StoryEvent | StoredStory, options?: ToStoredStoryOptions): StoredStory;
616
+ /**
617
+ * Everything a full-text search over a story should see: the title, each
618
+ * note's text, scalar context values, and error messages. Adapters store this
619
+ * as the canonical `search_text` column; the in-memory matcher searches it.
620
+ */
621
+ declare function storySearchText(story: StoredStory): string;
622
+ /** The origin as three text columns: scalars as-is, objects as JSON */
623
+ declare function flattenOrigin(origin?: StoryOrigin): {
624
+ who?: string;
625
+ what?: string;
626
+ where?: string;
627
+ };
628
+ /**
629
+ * The canonical schema. An adapter that stores these columns can answer every
630
+ * StoryQuery; `record` carries the whole story so `get` returns it unchanged.
631
+ * Deliberately not a normalized notes table — nothing yet demands one.
632
+ */
633
+ type CanonicalRow = {
634
+ story_id: string;
635
+ parent_story_id: string | null;
636
+ timestamp: string;
637
+ level: StoryLevel;
638
+ title: string;
639
+ origin_who: string | null;
640
+ origin_what: string | null;
641
+ origin_where: string | null;
642
+ duration_ms: number | null;
643
+ error_message: string | null;
644
+ /** The notes, as JSON */
645
+ notes: string;
646
+ /** Derived: what `about` searches */
647
+ search_text: string;
648
+ /** The whole story, as JSON */
649
+ record: string;
650
+ };
651
+ declare function canonicalRow(event: StoryEvent | StoredStory): CanonicalRow;
652
+ /** Does one story satisfy the criteria? The reference matcher every adapter is measured against. */
653
+ declare function matchesQuery(story: StoredStory, query?: StoryQuery): boolean;
654
+ /** Filter, order and page a set of stories in memory — what a store without an index does */
655
+ declare function applyQuery(stories: Iterable<StoredStory>, query?: StoryQuery): StoredStory[];
656
+
657
+ /**
658
+ * The query vocabulary: ask a store a question in the library's own words.
659
+ *
660
+ * await stories(store).about("checkout").from("payment-service").failing().since("24h");
661
+ *
662
+ * The same words have to read well aloud and be easy for a model to write.
663
+ * Every clause returns a new builder; a builder compiles to a StoryQuery, so
664
+ * an adapter receives structured criteria and never a string to parse.
665
+ */
666
+
667
+ /** `"30s"`, `"5m"`, `"24h"`, `"7d"`, `"2w"`, or milliseconds */
668
+ type DurationInput = string | number;
669
+ /** A duration as milliseconds. Throws on something that is not a duration, so a typo cannot become "since forever". */
670
+ declare function parseDuration(input: DurationInput): number;
671
+ type StoriesOptions = {
672
+ /** The clock `since("24h")` counts back from. Injectable for tests. */
673
+ now?: () => Date;
674
+ };
675
+ /**
676
+ * A question in progress. Immutable: every clause returns a new one.
677
+ * Awaiting it runs `.all()`.
678
+ *
679
+ * Being thenable has one consequence: returning a builder from an `async`
680
+ * function resolves it into its results. Return the store, or the
681
+ * `toQuery()` object, when a caller should get the question rather than
682
+ * the answer.
683
+ */
684
+ declare class StoryQueryBuilder implements PromiseLike<StoredStory[]> {
685
+ private readonly store;
686
+ private readonly query;
687
+ private readonly now;
688
+ constructor(store: StoryStore, query: StoryQuery, now: () => Date);
689
+ private with;
690
+ private instant;
691
+ /** Title, note text, scalar context or error message mentions this */
692
+ about(text: string): StoryQueryBuilder;
693
+ /** The origin — who, what or where — mentions this */
694
+ from(origin: string): StoryQueryBuilder;
695
+ /** Exactly this level. Accepts the aliases: `"info"`, `"warn"`, `"oops"`. */
696
+ level(level: LevelInput): StoryQueryBuilder;
697
+ /** This level or worse */
698
+ atLeast(level: LevelInput): StoryQueryBuilder;
699
+ /** Carried an error, or closed at Error level */
700
+ failing(): StoryQueryBuilder;
701
+ /** Neither an error nor closed at Error level */
702
+ succeeding(): StoryQueryBuilder;
703
+ /** Took longer than this: `"5s"`, `"2m"`, or milliseconds */
704
+ slowerThan(duration: DurationInput): StoryQueryBuilder;
705
+ /** Began within this long ago (`"24h"`), or at or after this date */
706
+ since(when: DurationInput | Date): StoryQueryBuilder;
707
+ /** Began before this long ago, or before this date */
708
+ until(when: DurationInput | Date): StoryQueryBuilder;
709
+ /** The chapters of this story */
710
+ under(parentStoryId: string): StoryQueryBuilder;
711
+ /** Most recent first — the default */
712
+ newest(): StoryQueryBuilder;
713
+ /** Earliest first */
714
+ oldest(): StoryQueryBuilder;
715
+ /** At most this many */
716
+ limit(count: number): StoryQueryBuilder;
717
+ /** Skip this many first */
718
+ skip(count: number): StoryQueryBuilder;
719
+ /** The structured criteria this question compiles to — what an adapter receives */
720
+ toQuery(): StoryQuery;
721
+ /** Every matching story */
722
+ all(): Promise<StoredStory[]>;
723
+ /** The first match, or nothing */
724
+ first(): Promise<StoredStory | undefined>;
725
+ /** How many match, ignoring paging */
726
+ count(): Promise<number>;
727
+ /** An awaited builder resolves to `.all()` */
728
+ then<Result = StoredStory[], Failure = never>(onFulfilled?: ((stories: StoredStory[]) => Result | PromiseLike<Result>) | null, onRejected?: ((reason: unknown) => Failure | PromiseLike<Failure>) | null): Promise<Result | Failure>;
729
+ }
730
+ /**
731
+ * Start a question against a store.
732
+ *
733
+ * @example
734
+ * ```ts
735
+ * const recent = stories(store);
736
+ * await recent.failing().since("1h");
737
+ * await recent.about("checkout").from("payment-service").level("oops").since("24h");
738
+ * await recent.slowerThan("5s").since("7d").oldest().limit(10);
739
+ * await recent.under(parentStoryId).count();
740
+ * ```
741
+ */
742
+ declare function stories(store: StoryStore, options?: StoriesOptions): StoryQueryBuilder;
743
+
744
+ export { type DurationInput as $, type AudienceMember as A, type AudienceErrorHandler as B, type ChapterOptions as C, AudienceRegistry as D, type EmissionKind as E, type FormattedReport as F, type NormalizeOptions as G, DEFAULT_REDACT_KEYS as H, normalizeValue as I, type JsonValue as J, normalizeError as K, type LevelInput as L, REDACTED as M, type NarrationInput as N, type CanonicalRow as O, type PreviewOptions as P, type StoredStory as Q, type ReportOptions as R, type StoryEventBase as S, type StoryQuery as T, type ToStoredStoryOptions as U, applyQuery as V, canonicalRow as W, flattenOrigin as X, matchesQuery as Y, storySearchText as Z, toStoredStory as _, type StoryOriginInput as a, type StoriesOptions as a0, parseDuration as a1, stories as a2, StoryQueryBuilder as a3, type OutputFormat as a4, toStoryLevel as a5, readEnvironmentValue as a6, resolveMinimumLevel as a7, meetsLevel as a8, resolveOutputFormat as a9, resolveColors as aa, Storyteller as b, type StoryEvent as c, type StoryLevel as d, type StoryStore as e, type StoryOrigin as f, type StoryError as g, type StoryContextValue as h, type StoryContextInput as i, type StoryNote as j, type StorySummaryOptions as k, type StoryPreviewOptions as l, type ReportNote as m, type StorySummaryNote as n, type StoryReport as o, type StorySummaryData as p, type StorySummary as q, type StoryEmission as r, type NoteEmission as s, type Emission as t, type EmissionOf as u, type AnyAudienceMember as v, type Narration as w, type NoteData as x, type FinishOptions as y, type StorytellerOptions as z };