@lovelaces-io/storyteller 0.2.0 → 0.3.1

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.
package/AGENTS.md ADDED
@@ -0,0 +1,262 @@
1
+ # Storyteller — Agent Guide
2
+
3
+ ## Overview
4
+
5
+ Storyteller (`@lovelaces-io/storyteller`) is a lightweight TypeScript logging library with zero production dependencies. You report beats of work as they happen; it keeps them and emits them as one structured record when the work finishes. Records go to pluggable audiences.
6
+
7
+ Version: 0.3.1 (pre-1.0, API may change). Dual output: ESM + CJS.
8
+
9
+ ## Narrate your work
10
+
11
+ If you are an agent doing a multi-step task, this is the pattern. Report each step as you take it, then finish once:
12
+
13
+ ```typescript
14
+ import { Storyteller } from "@lovelaces-io/storyteller";
15
+
16
+ const story = new Storyteller({
17
+ origin: { who: "sync-agent", where: { service: "billing" } },
18
+ narration: "live",
19
+ });
20
+
21
+ story.report("Reading config");
22
+ story.report("Fetching invoices", { what: { source: "stripe", page: 1 } });
23
+ story.report("Rate limited, backing off", { level: "warn" });
24
+ story.report("Retry succeeded", { what: { attempt: 2 } });
25
+
26
+ story.finish("Sync complete");
27
+ ```
28
+
29
+ In `live` narration each `report()` is emitted the moment you call it, so whoever is watching sees the work in progress. The full record still lands at `finish()`. Nothing is lost either way.
30
+
31
+ ## Choosing a narration mode
32
+
33
+ | You want | Use |
34
+ |---|---|
35
+ | One record per operation, for storage or audit | `collected` (the default) |
36
+ | Progress visible while the work runs | `live` |
37
+ | To decide without touching the code | leave it unset, set `STORYTELLER_NARRATION=live` |
38
+
39
+ `live` never removes an emission — it adds the beats and still delivers the story. A consumer that wants only beats says so with `hears: ["note"]`, rather than silencing the record.
40
+
41
+ Switch at runtime with `story.narrate("live")`, or push a single urgent beat out of an otherwise collected story with `story.report("...", { live: true })`.
42
+
43
+ ## Streaming loses nothing
44
+
45
+ Every beat carries `storyId` and `sequence`. Beats from one story share its `storyId`, and `sequence` is gap-free from 0, assigned at the moment you call `report()`.
46
+
47
+ That means a consumer holding the streamed beats can order and group them back into exactly the `notes` array the story record would have contained. **Order by `sequence`, never by arrival time** — audiences are async and a slow one lands late.
48
+
49
+ ## Nested work: chapters
50
+
51
+ Real work nests. An agent spawns subtasks; a batch runs per-item operations. Use `chapter()` so each piece is a complete story in its own right while the whole run stays reconstructable:
52
+
53
+ ```typescript
54
+ story.report("Starting sync");
55
+
56
+ for (const account of accounts) {
57
+ const chapter = story.chapter({ origin: { what: account.id } });
58
+ chapter.report("Fetching invoices");
59
+ chapter.report("Reconciling");
60
+ chapter.finish(`Synced ${account.id}`);
61
+ }
62
+
63
+ story.finish("Sync complete");
64
+ ```
65
+
66
+ Each chapter emits its own record carrying `parentStoryId`. Follow that field to rebuild the tree. A chapter shares the parent's audiences — including any added later — and inherits narration, level and delivery settings; pass options to override.
67
+
68
+ Chapters are **not** folded into the parent's notes. One record per story stays true, and a nested story is still a story.
69
+
70
+ ## Report anything
71
+
72
+ `report()` takes any value, not just a string. Do not pre-flatten your data:
73
+
74
+ ```typescript
75
+ story.report(await response.json());
76
+ story.report(caughtError);
77
+ story.report(new Map([["region", "us-east"]]));
78
+ story.report({ message: "Job queued", jobId: 7 }); // "message" becomes the note text
79
+ ```
80
+
81
+ Whatever you pass is normalized into something storable: errors keep their `cause` chain, dates become ISO strings, class instances get an `@type` tag, circular references become `[Circular → path]`, and secret-looking keys (`password`, `apiKey`, `token`, …) become `[redacted]`.
82
+
83
+ Values dropped for size are replaced with an explicit `{ "@truncated": { kind, omitted } }` marker, so you can tell "this was empty" from "this was too big".
84
+
85
+ The normalizer never throws. A hostile object cannot break the pipeline.
86
+
87
+ ## Output a program can read
88
+
89
+ For machine consumption, use NDJSON — one JSON object per line, nothing else on the channel:
90
+
91
+ ```typescript
92
+ import { ndjsonAudience } from "@lovelaces-io/storyteller";
93
+
94
+ story.audience.remove("console");
95
+ story.audience.add(ndjsonAudience({ stream: process.stderr }));
96
+ ```
97
+
98
+ Or set `STORYTELLER_FORMAT=ndjson` and change no code at all.
99
+
100
+ ## Environment variables
101
+
102
+ | Variable | Values | Effect |
103
+ |---|---|---|
104
+ | `STORYTELLER_NARRATION` | `collected` \| `live` | Whether beats stream |
105
+ | `STORYTELLER_FORMAT` | `text` \| `ndjson` | Which default audience is registered |
106
+ | `STORYTELLER_LEVEL` | `info` \| `warn` \| `oops` | Minimum level delivered |
107
+ | `STORYTELLER_COLOR` | `0` \| `1` | Force colors off or on |
108
+ | `STORYTELLER_DEPRECATION_WARNINGS` | `1` | Warn when deprecated methods are called |
109
+
110
+ Unrecognized values fall back to the default rather than throwing.
111
+
112
+ ## Error handling
113
+
114
+ Pass the caught value to `finish()`. It is normalized automatically:
115
+
116
+ ```typescript
117
+ const story = new Storyteller({ origin: { who: "sync-job" } });
118
+
119
+ story.report("Starting sync");
120
+ try {
121
+ const records = await getRecords();
122
+ story.report("Retrieved records", { what: { count: records.length } });
123
+ await writeRecords(records);
124
+ story.finish("Sync finished");
125
+ } catch (error) {
126
+ story.finish("Sync failed", { level: "oops", error });
127
+ }
128
+ ```
129
+
130
+ ## Two output modes, two narration modes
131
+
132
+ These are different axes and it matters that you keep them straight:
133
+
134
+ | | Collected | Live |
135
+ |---|---|---|
136
+ | **Story** (JSON record) | one record at the end | beats stream as JSON, record still lands |
137
+ | **Report** (formatted text) | one grouped block at the end | one compact line per beat |
138
+
139
+ *Story* vs *report* is **what the output looks like**. *Collected* vs *live* is **when it comes out**.
140
+
141
+ `JSON.stringify(event)` gives you the story record — a complete DB row, no assembly required. `formatStory(event)` gives you the human-readable report.
142
+
143
+ ## API
144
+
145
+ ```typescript
146
+ story.report(input, context?) // a beat; returns `this` for chaining
147
+ story.finish(title, options?) // emit the collected story; returns a `.to()` handle
148
+ story.narrate(mode) // switch narration at runtime
149
+ story.chapter(options?) // a child storyteller, linked by parentStoryId
150
+ story.reset() // drop the notes, start a new story id
151
+ story.summarize(options?) // preview without emitting
152
+ story.currentStoryId // the id beats are being tagged with
153
+ story.audience.add/remove/has/names
154
+ ```
155
+
156
+ `context`: `{ who, what, where, error, level, live, to }`
157
+ `options`: `{ level, error }`
158
+ `level` accepts `"info"`, `"warn"`, `"oops"`, `"error"`, or the stored labels.
159
+
160
+ ### Deprecated — removed at 1.0
161
+
162
+ | Old | New |
163
+ |---|---|
164
+ | `note(text, context?)` | `report(input, context?)` |
165
+ | `tell(title)` | `finish(title)` |
166
+ | `warn(title)` | `finish(title, { level: "warn" })` |
167
+ | `oops(title, error?)` | `finish(title, { level: "oops", error })` |
168
+
169
+ The aliases behave identically. `tell` will not be reintroduced with a new meaning.
170
+
171
+ ## Audiences
172
+
173
+ An audience declares which emission kinds it wants. **`hears` defaults to `["story"]`**, so an audience written before live narration existed keeps hearing only stories:
174
+
175
+ ```typescript
176
+ story.audience.add({
177
+ name: "metrics",
178
+ hears: ["note"], // beats only
179
+ accepts: (emission) => emission.level !== "Information",
180
+ hear: (emission) => send(emission),
181
+ });
182
+ ```
183
+
184
+ Built in: `consoleAudience()` (notes and stories, registered by default), `dbAudience(insert)` (stories only, warn and oops), `ndjsonAudience(options)` (notes and stories).
185
+
186
+ When an audience throws, the failure is reported through `onAudienceError` rather than swallowed, and never propagates into your code. When an audience is too slow, emissions past `maxInFlight` are dropped and counted in `droppedEmissions` on the closing story — so the loss shows up in the record instead of vanishing.
187
+
188
+ ## Architecture
189
+
190
+ ```
191
+ src/
192
+ storyteller.ts — core class, types, event building, delivery
193
+ normalize.ts — turns any value into something storable
194
+ environment.ts — env-var config, level resolution
195
+ formatting.ts — formatStory(), presentation logic
196
+ useStoryteller.ts — singleton pattern
197
+ utils.ts — ANSI codes, getLevelColor, formatOrigin, summarizeContext
198
+ audiences/
199
+ consoleAudience.ts — compact line per beat, grouped block per story
200
+ dbAudience.ts — persists warn/oops stories via insert callback
201
+ ndjsonAudience.ts — one JSON object per line
202
+ report/
203
+ writeStoryReport.ts — multi-story report, grouped by day
204
+ cli.ts — `storyteller init`, CommonJS-only so __dirname resolves
205
+ index.ts — public API barrel export
206
+ snippets/
207
+ agents-section.md — the guidance block consumers paste into their AGENTS.md
208
+ ```
209
+
210
+ `snippets/agents-section.md` is the single source for that block. It is embedded
211
+ verbatim in README.md and written by `storyteller init`, and `npm run check:snippet`
212
+ fails the build if the copies drift or it outgrows its 40-line budget. Edit the
213
+ snippet, never a copy.
214
+
215
+ Types are defined in `storyteller.ts` and `normalize.ts`. Formatting utilities live in `utils.ts` — do not duplicate them elsewhere.
216
+
217
+ ## Code standards
218
+
219
+ This repo follows [Lovelaces](https://lovelaces.io) coding standards:
220
+
221
+ - **Descriptive names** — no abbreviations. `options` not `opts`, `error` not `err`, `timestamp` not `ts`.
222
+ - **No single-letter variables.**
223
+ - **JSDoc on every public export.**
224
+ - **No `as any` casts** — use proper type narrowing.
225
+ - **Comments explain why, not what.**
226
+ - **Zero production dependencies** — a hard constraint.
227
+
228
+ ## Anti-patterns
229
+
230
+ ### Do not order streamed beats by arrival time
231
+
232
+ Audiences are async. Two beats can land out of order. `sequence` is assigned synchronously and is the only correct ordering key.
233
+
234
+ ### Do not pre-stringify your data
235
+
236
+ `report()` normalizes anything you give it. `JSON.stringify`-ing first loses structure and can throw on a circular reference before Storyteller ever sees it.
237
+
238
+ ### Do not store the .to() return value
239
+
240
+ The object returned by `finish()` is a one-shot delivery handle. Delivery happens on the next microtask, so `.to()` must be called immediately and synchronously — not after an `await`.
241
+
242
+ ```typescript
243
+ // Wrong — delivery may have already happened
244
+ const handle = story.finish("Done");
245
+ await someAsyncWork();
246
+ handle.to("db");
247
+
248
+ // Correct
249
+ story.finish("Done").to("db");
250
+ ```
251
+
252
+ ### Do not mix presentation with storage
253
+
254
+ Storage audiences should receive the raw emission. Do not format before storing — format when reading.
255
+
256
+ ### Do not create a Storyteller per step
257
+
258
+ One instance per logical operation. Multiple instances fragment your work across disconnected stories with different `storyId`s. Use `useStoryteller()` for shared access, or pass one instance through the call chain.
259
+
260
+ ### Do not report after finishing
261
+
262
+ `finish()` clears the notes and starts a new story id. Beats reported afterwards belong to the next story.
package/README.md CHANGED
@@ -1,12 +1,14 @@
1
+ <p align="left"><img src="site/public/storyteller-logo.svg" alt="" width="56" height="56" /></p>
2
+
1
3
  # Storyteller
2
4
 
3
5
  [![npm](https://img.shields.io/npm/v/@lovelaces-io/storyteller)](https://www.npmjs.com/package/@lovelaces-io/storyteller)
4
6
  [![license](https://img.shields.io/npm/l/@lovelaces-io/storyteller)](LICENSE)
5
7
  [![zero deps](https://img.shields.io/badge/dependencies-0-brightgreen)](package.json)
6
8
 
7
- Lightweight TypeScript logging library that treats logs as **stories** — grouped notes emitted as a single structured event.
9
+ Lightweight TypeScript logging library that treats logs as **stories** — beats reported as they happen, emitted as a single structured record.
8
10
 
9
- Zero dependencies. TypeScript-first. One record per story.
11
+ Zero dependencies. TypeScript-first. One record per story — and a live stream when you want to watch it happen.
10
12
 
11
13
  ## Why Storyteller?
12
14
 
@@ -53,34 +55,143 @@ const story = new Storyteller({
53
55
  origin: { who: "checkout-service", where: { app: "web" } },
54
56
  });
55
57
 
56
- story.note("User submitted payment", { what: { amount: 49.99 } });
57
- story.note("Charging card", { where: "stripe" });
58
- story.tell("Payment completed");
58
+ story.report("User submitted payment", { what: { amount: 49.99 } });
59
+ story.report("Charging card", { where: "stripe" });
60
+ story.finish("Payment completed");
61
+ ```
62
+
63
+ Beats are collected, sorted chronologically, and emitted as one structured record to your audiences.
64
+
65
+ ## Watch It Happen
66
+
67
+ Set narration to `live` and each beat is emitted the moment you report it — the record still lands at the end.
68
+
69
+ ```ts
70
+ const story = new Storyteller({
71
+ origin: { who: "sync-agent" },
72
+ narration: "live",
73
+ });
74
+
75
+ story.report("Fetching invoices", { what: { source: "stripe" } });
76
+ story.report("Rate limited, backing off", { level: "warn" });
77
+ story.finish("Sync complete");
78
+ ```
79
+
80
+ ```
81
+ 05:36:50 info sync-agent Fetching invoices {source=stripe}
82
+ 05:36:50 warn sync-agent Rate limited, backing off
83
+ ```
84
+
85
+ Or leave the code alone and set `STORYTELLER_NARRATION=live`.
86
+
87
+ Every beat carries `storyId` and a gap-free `sequence`, so a consumer holding the stream can reassemble exactly the record collected narration would have produced. Nothing is lost either way.
88
+
89
+ ## Feed It Anything
90
+
91
+ `report()` takes any value. No pre-flattening, no defensive stringifying.
92
+
93
+ ```ts
94
+ story.report(await response.json());
95
+ story.report(caughtError); // cause chain preserved
96
+ story.report(new Map([["region", "us-east"]]));
97
+ story.report({ message: "Job queued", jobId: 7 });
98
+ ```
99
+
100
+ Circular references become `[Circular → path]`. Secret-looking keys become `[redacted]`. Oversized values get an explicit `{ "@truncated": … }` marker rather than disappearing. The normalizer never throws — a hostile object cannot break your logging.
101
+
102
+ ## Nested Work
103
+
104
+ Real work nests. `chapter()` gives you a child storyteller whose stories link back to the parent:
105
+
106
+ ```ts
107
+ story.report("Starting sync");
108
+
109
+ for (const account of accounts) {
110
+ const chapter = story.chapter({ origin: { what: account.id } });
111
+ chapter.report("Fetching invoices");
112
+ chapter.finish(`Synced ${account.id}`);
113
+ }
114
+
115
+ story.finish("Sync complete");
116
+ ```
117
+
118
+ Each chapter is a complete record of its own, carrying `parentStoryId`. Follow that field to rebuild the whole run as a tree. Chapters share the parent's audiences and inherit its settings.
119
+
120
+ ## Teach Your Agents
121
+
122
+ Run `npx @lovelaces-io/storyteller init` to set up a project in one command — it installs the package, writes a configured storyteller, and adds the block below to your `AGENTS.md`.
123
+
124
+ Or paste it yourself. Every agent that works in the repo will read it:
125
+
126
+ <details>
127
+ <summary>Agent guidance block</summary>
128
+
129
+ ## Logging with Storyteller
130
+
131
+ This project uses [Storyteller](https://github.com/lovelaces-io/storyteller). Narrate multi-step work as you do it, then finish once.
132
+
133
+ ```ts
134
+ import { useStoryteller } from "@lovelaces-io/storyteller";
135
+
136
+ const story = useStoryteller({ origin: { who: "sync-job" } });
137
+
138
+ story.report("Fetching invoices", { what: { source: "stripe" } });
139
+ story.report("Rate limited, backing off", { level: "warn" });
140
+ story.report(await response.json());
141
+
142
+ story.finish("Sync complete");
143
+ // on failure: story.finish("Sync failed", { level: "oops", error });
144
+ ```
145
+
146
+ For nested work, open a chapter. Each becomes its own record, linked to the parent:
147
+
148
+ ```ts
149
+ for (const account of accounts) {
150
+ const chapter = story.chapter({ origin: { what: account.id } });
151
+ chapter.report("Reconciling");
152
+ chapter.finish(`Synced ${account.id}`);
153
+ }
59
154
  ```
60
155
 
61
- Notes are collected, sorted chronologically, and emitted as one structured event to your audiences.
156
+ Things that are easy to get wrong:
157
+
158
+ - **Hand it the object.** `report()` takes any value — errors, API responses, Maps, class instances — and structures it safely, including circular references. Never `JSON.stringify` first.
159
+ - **One storyteller per logical operation**, not one per step. Separate instances fragment the work into disconnected stories.
160
+ - **Order beats by `sequence`, not arrival time.** Audiences are async and a slow one lands late.
161
+ - **`.to()` is synchronous.** Call it immediately after `finish()`, never after an `await`.
162
+ - **Report before finishing.** `finish()` clears the notes; anything reported after belongs to the next story.
163
+
164
+ Set `STORYTELLER_NARRATION=live` to watch beats stream as they happen, or `STORYTELLER_FORMAT=ndjson` for one JSON object per line.
62
165
 
63
- ## Two Output Modes
166
+ </details>
64
167
 
65
- | Mode | What it is | Use it for |
66
- |------|-----------|------------|
67
- | **Story** (JSON) | Clean serializable record | DB storage, monitoring, audit logs |
68
- | **Report** (text) | Colorized human-readable output | Console, log files, debugging |
168
+ ## Two Axes, Not One
69
169
 
70
- `JSON.stringify(event)` gives you the story record. `formatStory(event)` gives you the report.
170
+ **Story vs report** is *what the output looks like*. **Collected vs live** is *when it comes out*. They combine freely:
71
171
 
72
- ## Three Levels
172
+ | | Collected | Live |
173
+ |---|---|---|
174
+ | **Story** (JSON) | one record at the end | beats stream as JSON, record still lands |
175
+ | **Report** (text) | one grouped block at the end | one compact line per beat |
176
+
177
+ `JSON.stringify(event)` gives you the story record — a complete DB row, no assembly. `formatStory(event)` gives you the report.
178
+
179
+ ## Levels
73
180
 
74
181
  ```ts
75
- story.tell("Payment completed"); // all good
76
- story.warn("Payment slow but succeeded"); // heads up
77
- story.oops("Payment failed", new Error()); // something broke
182
+ story.finish("Payment completed"); // all good
183
+ story.finish("Payment slow but succeeded", { level: "warn" }); // heads up
184
+ story.finish("Payment failed", { level: "oops", error }); // something broke
78
185
  ```
79
186
 
80
- ## Context on Every Note
187
+ Levels work on individual beats too: `story.report("Retrying", { level: "warn" })`.
188
+
189
+ `level` accepts `"info"`, `"warn"`, `"oops"`, `"error"`, or the stored labels.
190
+
191
+ ## Context on Every Beat
81
192
 
82
193
  ```ts
83
- story.note("Write failed", {
194
+ story.report("Write failed", {
84
195
  who: { id: "user:99" },
85
196
  what: { field: "email" },
86
197
  where: "primary-db",
@@ -93,32 +204,88 @@ story.note("Write failed", {
93
204
  Stories are delivered to **audiences**. Console is included by default.
94
205
 
95
206
  ```ts
96
- import { dbAudience } from "@lovelaces-io/storyteller";
207
+ import { dbAudience, ndjsonAudience } from "@lovelaces-io/storyteller";
97
208
 
98
- // Store warn and oops events in your database
209
+ // Store warn and oops records in your database
99
210
  story.audience.add(
100
211
  dbAudience(async (event) => await db.insert("logs", event))
101
212
  );
102
213
 
214
+ // One JSON object per line, for a program to read
215
+ story.audience.add(ndjsonAudience({ stream: process.stderr }));
216
+
103
217
  // Target specific audiences
104
- story.oops("Critical failure", error).to("console", "db");
218
+ story.finish("Critical failure", { level: "oops", error }).to("console", "db");
219
+ ```
220
+
221
+ An audience declares which emission kinds it wants. `hears` defaults to `["story"]`, so audiences written before live narration keep working unchanged.
222
+
223
+ ```ts
224
+ story.audience.add({
225
+ name: "metrics",
226
+ hears: ["note"],
227
+ hear: (emission) => send(emission),
228
+ });
229
+ ```
230
+
231
+ Audiences are small. Only the failures, straight to a Discord channel — fifteen lines, no dependency:
232
+
233
+ ```ts
234
+ story.audience.add({
235
+ name: "discord",
236
+ accepts: (event) => event.level === "Error",
237
+ hear: async (event) => {
238
+ await fetch(process.env.DISCORD_WEBHOOK_URL!, {
239
+ method: "POST",
240
+ headers: { "content-type": "application/json" },
241
+ body: JSON.stringify({
242
+ content: "```\n" + event.summarize({ colors: false, detail: "brief" }).text + "\n```",
243
+ }),
244
+ });
245
+ },
246
+ });
105
247
  ```
106
248
 
249
+ When an audience throws, the failure is reported rather than swallowed, and never reaches your code. When one is too slow, emissions past `maxInFlight` are dropped and counted in `droppedEmissions` on the closing record — visible loss beats silent loss.
250
+
251
+ ## Configuration
252
+
253
+ Every option can also come from the environment, so you can change behavior without touching code:
254
+
255
+ | Variable | Values | Effect |
256
+ |---|---|---|
257
+ | `STORYTELLER_NARRATION` | `collected` \| `live` | Whether beats stream |
258
+ | `STORYTELLER_FORMAT` | `text` \| `ndjson` | Which default audience is registered |
259
+ | `STORYTELLER_LEVEL` | `info` \| `warn` \| `oops` | Minimum level delivered |
260
+ | `STORYTELLER_COLOR` | `0` \| `1` | Force colors off or on |
261
+
107
262
  ## Quick Reference
108
263
 
109
264
  | Method | Returns | Description |
110
265
  |--------|---------|-------------|
111
- | `note(text, context?)` | `this` | Add a timestamped note with optional who/what/where/error |
112
- | `tell(title)` | `{ to }` | Tell a success story |
113
- | `warn(title)` | `{ to }` | Tell a cautionary story |
114
- | `oops(title, error?)` | `{ to }` | Tell an error story |
115
- | `reset()` | `this` | Clear notes without telling a story |
116
- | `summarize(options?)` | `FormattedReport` | Preview current notes as a formatted report |
266
+ | `report(input, context?)` | `this` | Report a beat any value, optional who/what/where/error/level |
267
+ | `finish(title, options?)` | `{ to }` | Emit the collected story |
268
+ | `narrate(mode)` | `this` | Switch narration at runtime |
269
+ | `chapter(options?)` | `Storyteller` | A child storyteller, linked by `parentStoryId` |
270
+ | `reset()` | `this` | Clear beats without emitting |
271
+ | `summarize(options?)` | `FormattedReport` | Preview current beats as a formatted report |
272
+ | `currentStoryId` | `string` | The id beats are being tagged with |
117
273
  | `audience.add(member)` | `this` | Register an audience |
118
274
  | `audience.remove(name)` | `this` | Unregister an audience |
119
275
  | `audience.has(name)` | `boolean` | Check if an audience is listening |
120
276
  | `audience.names()` | `string[]` | List who's listening |
121
277
 
278
+ ### Deprecated — removed at 1.0
279
+
280
+ | Old | New |
281
+ |---|---|
282
+ | `note(text, context?)` | `report(input, context?)` |
283
+ | `tell(title)` | `finish(title)` |
284
+ | `warn(title)` | `finish(title, { level: "warn" })` |
285
+ | `oops(title, error?)` | `finish(title, { level: "oops", error })` |
286
+
287
+ The aliases behave identically and stay silent unless you set `STORYTELLER_DEPRECATION_WARNINGS=1`. `tell` will not be reintroduced with a new meaning.
288
+
122
289
  ## Shared Instance
123
290
 
124
291
  ```ts