@inerrata-corporation/errata 2.0.21 → 2.0.22-dev.4049

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "errata-minimap",
3
- "version": "0.1.0",
3
+ "version": "0.4.1",
4
4
  "author": { "name": "inErrata", "url": "https://inerrata.ai" },
5
- "description": "Side pane: an errata graph minimap of the latest burst neighborhood that entered the agent's context.",
5
+ "description": "The errata library: the causal chains of the latest burst that entered the agent's context, in bookcases by technology with a catalog card — and errata's inline tags, tidied in the chat.",
6
6
  "types": "./types/index.d.ts"
7
7
  }
@@ -0,0 +1,450 @@
1
+ import type { Book, Bookcase, Catalog, Part, Shelf } from './chains'
2
+ import { bookHolding, callNumber, placeOf } from './chains'
3
+ import type { JudgmentKind } from './minimap'
4
+
5
+ /**
6
+ * The burst as a library, after the splash's Arched Library (SPL-library):
7
+ * BOOKCASES by technology, SHELVES by community, and every BOOK one causal
8
+ * chain standing upright — its spine banded top to bottom in the chain's own
9
+ * order, cause (gilt) over problem (oxblood) over fix (green). An upright
10
+ * spine cannot carry a title anyone could read, so the spines are a wordless
11
+ * glance — how many chains, how complete (a full chain stands tallest and
12
+ * thickest, a lone problem is a thin pamphlet), where they cluster — and the
13
+ * CATALOG CARD under the cases reads the book in focus in full.
14
+ *
15
+ * Two rooms, one token set, as on the site: the READING ROOM (parchment
16
+ * bindings, for a light terminal) and THE STACKS (the library after hours,
17
+ * for a dark one). Colours are lifted from apps/splash/src/network/network.css.
18
+ */
19
+ export type Room = 'reading' | 'stacks'
20
+
21
+ export type Binding = 'problem' | 'cause' | 'fix' | 'claim' | 'pattern' | 'code'
22
+
23
+ export type Palette = {
24
+ binding: Record<Binding, number>
25
+ /** Lettering on a wooden plate. */
26
+ clothInk: number
27
+ /** A book being burst off flashes gilt. */
28
+ gilt: number
29
+ /** The bookmark in the book the card reads: not gilt, which is the cause's own binding at night. */
30
+ ribbon: number
31
+ wood: number
32
+ /** The catalog card: its stock, its ink, and the ink of a part completed from the graph (not in context). */
33
+ card: number
34
+ cardInk: number
35
+ cardFaint: number
36
+ verdict: Record<JudgmentKind, number>
37
+ }
38
+
39
+ export const PALETTE: Record<Room, Palette> = {
40
+ reading: {
41
+ binding: { problem: 0x7a2e24, cause: 0x8a6a1f, fix: 0x2a4e3c, claim: 0x5c3a50, pattern: 0x3e5a73, code: 0x8a6e52 },
42
+ clothInk: 0xf7f2e8,
43
+ gilt: 0xc9a24a,
44
+ ribbon: 0x2b2620,
45
+ wood: 0x8a6e52,
46
+ card: 0xefe6d2,
47
+ cardInk: 0x2b2620,
48
+ cardFaint: 0x8c8170,
49
+ verdict: { cite: 0x3e5a73, confirm: 0x2a4e3c, fix: 0x2a4e3c, refute: 0x7a2e24 },
50
+ },
51
+ stacks: {
52
+ binding: { problem: 0xe08a74, cause: 0xe2c376, fix: 0xa7d0b0, claim: 0xcba3bf, pattern: 0x9dbcd8, code: 0xbba98c },
53
+ clothInk: 0xf7f2e8,
54
+ gilt: 0xe2c376,
55
+ ribbon: 0xf7f2e8,
56
+ wood: 0x6b543d,
57
+ card: 0x1f3a2d,
58
+ cardInk: 0xf7f2e8,
59
+ cardFaint: 0x93a89a,
60
+ verdict: { cite: 0x9dbcd8, confirm: 0xa7d0b0, fix: 0xa7d0b0, refute: 0xe08a74 },
61
+ },
62
+ }
63
+
64
+ const BINDING_OF: Record<string, Binding> = {
65
+ Problem: 'problem',
66
+ Vulnerability: 'problem',
67
+ AntiPattern: 'problem',
68
+ RootCause: 'cause',
69
+ Constraint: 'cause',
70
+ Solution: 'fix',
71
+ Technique: 'fix',
72
+ Claim: 'claim',
73
+ Pattern: 'pattern',
74
+ }
75
+
76
+ export function bindingOf(label: string): Binding {
77
+ return BINDING_OF[label] ?? 'code'
78
+ }
79
+
80
+ /** What the legend calls each binding. */
81
+ export const BINDING_NAME: Record<Binding, string> = {
82
+ problem: 'problem',
83
+ cause: 'cause',
84
+ fix: 'fix',
85
+ claim: 'claim',
86
+ pattern: 'pattern',
87
+ code: 'code',
88
+ }
89
+
90
+ /** The mark a part carries on the catalog card, and the status line's tally glyph. */
91
+ export const BINDING_GLYPH: Record<Binding, string> = {
92
+ problem: '◆',
93
+ cause: '▲',
94
+ fix: '●',
95
+ claim: '◇',
96
+ pattern: '✦',
97
+ code: '§',
98
+ }
99
+
100
+ export type LibraryFrame = { grow: number; type: number }
101
+
102
+ export type LibraryOptions = {
103
+ room?: Room
104
+ /** How far the reveal has run: books rise onto their shelves (`grow`), then the card inks in (`type`). */
105
+ frame?: LibraryFrame
106
+ /** The node whose book the card reads (a verdict's, the seed's); default: the seed's book, else the strongest. */
107
+ focus?: string
108
+ /** A node the agent is bursting off: its book flashes gilt. */
109
+ highlight?: string
110
+ isPulseOn?: boolean
111
+ /** Priors the agent just judged: their stamp flashes. */
112
+ marks?: Record<string, JudgmentKind>
113
+ isMarkOn?: boolean
114
+ /** Every recent verdict, stamped on the plank under its book like a date-due card. */
115
+ stamps?: Record<string, JudgmentKind>
116
+ }
117
+
118
+ /** Who gets the card's lines first when the chain will not fit: the problem, then its fix, then its cause. */
119
+ const PRIORITY = { problem: 0, fix: 1, cause: 2 } as const
120
+ const STAMP: Record<JudgmentKind, string> = { cite: '◎', confirm: '✓', fix: '✓', refute: '✗' }
121
+ const VERB: Record<JudgmentKind, string> = { cite: 'cited', confirm: 'confirmed', fix: 'resolved', refute: 'refuted' }
122
+ const DEFAULT = 0x01000000
123
+ const SPACE = 0x20
124
+
125
+ /** Book rows above each plank: a full chain stands 3.5 rows; the half-row above it holds the bookmark. */
126
+ const TALL_SHELF = 4
127
+ const SHORT_SHELF = 3
128
+ /** Cells a shelf keeps after its books for its plate. */
129
+ const PLATE = 17
130
+ /** Rows the catalog card never gives up: header, two lines of chain, footer. */
131
+ const MIN_CARD = 4
132
+ const MAX_CARD = 8
133
+
134
+ /** The focus book: an asked node's, else the seed's, else the strongest. */
135
+ export function focusBook(catalog: Catalog, focus?: string): Book | undefined {
136
+ return bookHolding(catalog, focus) ?? catalog.books.find(b => b.isSeed) ?? catalog.books[0]
137
+ }
138
+
139
+ /** Thin (one part) or thick (a chain of two or three). */
140
+ const widthOf = (b: Book) => (partsOf(b).length > 1 ? 2 : 1)
141
+
142
+ function partsOf(b: Book): [('cause' | 'problem' | 'fix'), Part][] {
143
+ return (['cause', 'problem', 'fix'] as const).flatMap(r => (b[r] ? [[r, b[r]] as [typeof r, Part]] : []))
144
+ }
145
+
146
+ type Placed = { bookcase: Bookcase; x: number; y: number; width: number; shelves: { shelf: Shelf; books: Book[]; hidden: number }[]; hidden: number }
147
+
148
+ /**
149
+ * Cases packed left to right, wrapping into another tier when the pane is
150
+ * narrow; each case as wide as its fullest shelf (books overflow into a `+n`
151
+ * on the plank), as tall as the rows allow (lower shelves give way first).
152
+ */
153
+ function placeCases(catalog: Catalog, columns: number, rows: number, zone: number): Placed[] {
154
+ const placed: Placed[] = []
155
+ let x = 0
156
+ let y = 0
157
+ let tierHeight = 0
158
+ for (const bookcase of catalog.cases) {
159
+ const fullest = Math.max(...bookcase.shelves.map(s => s.books.reduce((w, b) => w + widthOf(b), 0)))
160
+ // Room after the fullest shelf for its plate (" 1 loose volumes "), and on the cornice for the tech.
161
+ const width = Math.min(columns, Math.max(bookcase.tech.length + 6, fullest + 4 + PLATE))
162
+ if (x > 0 && x + width > columns) {
163
+ x = 0
164
+ y += tierHeight
165
+ tierHeight = 0
166
+ }
167
+ const room = Math.floor((rows - y - 1) / (zone + 1))
168
+ if (room < 1) break
169
+ const kept = bookcase.shelves.slice(0, room)
170
+ const shelves = kept.map(shelf => {
171
+ const books: Book[] = []
172
+ let used = 0
173
+ for (const b of shelf.books) {
174
+ if (used + widthOf(b) > width - 4) break
175
+ books.push(b)
176
+ used += widthOf(b)
177
+ }
178
+ return { shelf, books, hidden: shelf.books.length - books.length }
179
+ })
180
+ const hidden = bookcase.shelves.slice(room).reduce((n, s) => n + s.books.length, 0)
181
+ placed.push({ bookcase, x, y, width, shelves, hidden })
182
+ tierHeight = Math.max(tierHeight, 1 + shelves.length * (zone + 1))
183
+ x += width + 1
184
+ }
185
+ return placed
186
+ }
187
+
188
+ /** The library's geometry: the card it keeps, the shelf height, where every case stands. */
189
+ function layoutLibrary(catalog: Catalog, columns: number, rows: number, focusId?: string) {
190
+ const focus = focusBook(catalog, focusId)
191
+ // The card asks for its chain in full (header, every wrapped line, footer); the cases get the rest, at least a third.
192
+ const need = focus ? 2 + partsOf(focus).reduce((n, [, part]) => n + wrap(part.name, columns - 5).length, 0) : 0
193
+ const card = Math.max(MIN_CARD, Math.min(need, Math.max(MAX_CARD, Math.round(rows * 0.6)), rows - Math.ceil(rows / 3)))
194
+ const zone = rows - card >= 1 + TALL_SHELF + 1 ? TALL_SHELF : SHORT_SHELF
195
+ return { focus, need, card, zone, placed: placeCases(catalog, columns, rows - card, zone) }
196
+ }
197
+
198
+ /** Where a drawn book stands, in cells from the library's top-left: its spine's whole zone above the plank. */
199
+ export type BookSpot = { book: Book; x: number; top: number; width: number; height: number }
200
+
201
+ /**
202
+ * Every drawn book's spot, from the same layout `rasterizeLibrary` draws: the
203
+ * hover and click targets laid over the spines, so they can never disagree
204
+ * with the picture about where a book is.
205
+ */
206
+ export function librarySpots(catalog: Catalog, columns: number, rows: number, focusId?: string): BookSpot[] {
207
+ const { zone, placed } = layoutLibrary(catalog, columns, rows, focusId)
208
+ const spots: BookSpot[] = []
209
+ for (const c of placed) {
210
+ c.shelves.forEach(({ books }, si) => {
211
+ const plank = c.y + 1 + si * (zone + 1) + zone
212
+ let x = c.x + 2
213
+ for (const book of books) {
214
+ spots.push({ book, x, top: plank - zone, width: widthOf(book), height: zone })
215
+ x += widthOf(book)
216
+ }
217
+ })
218
+ }
219
+ return spots
220
+ }
221
+
222
+ /** Every book drawn, in reading order: the order the reveal raises them. */
223
+ function drawnBooks(placed: Placed[]): Book[] {
224
+ return placed.flatMap(p => p.shelves.flatMap(s => s.books))
225
+ }
226
+
227
+ /** Rows the cases take. */
228
+ function casesHeight(placed: Placed[], zone: number): number {
229
+ return placed.reduce((h, p) => Math.max(h, p.y + 1 + p.shelves.length * (zone + 1)), 0)
230
+ }
231
+
232
+ function shade(color: number, by: number): number {
233
+ const ch = (s: number) => Math.round(((color >> s) & 0xff) * (1 - by))
234
+ return (ch(16) << 16) | (ch(8) << 8) | ch(0)
235
+ }
236
+
237
+ /**
238
+ * The library as Raster cells (`[codePoint, fg, bg]` u32 triplets, row-major,
239
+ * base64). Every frame of the reveal draws a prefix of the finished picture,
240
+ * so nothing jumps as it fills: cases first, then each book rises onto its
241
+ * shelf in reading order, then the card inks in.
242
+ */
243
+ export function rasterizeLibrary(catalog: Catalog, columns: number, rows: number, options: LibraryOptions = {}): string {
244
+ const { room = 'stacks', frame = { grow: 1, type: 1 }, highlight, isPulseOn = true, marks = {}, isMarkOn = true, stamps = {} } = options
245
+ const p = PALETTE[room]
246
+ const words = new Uint32Array(columns * rows * 3)
247
+ for (let i = 0; i < columns * rows; i++) words.set([SPACE, DEFAULT, DEFAULT], i * 3)
248
+ const put = (x: number, y: number, ch: string | number, fg: number, bg = DEFAULT) => {
249
+ if (x >= 0 && y >= 0 && x < columns && y < rows) words.set([typeof ch === 'number' ? ch : ch.codePointAt(0)!, fg, bg], (y * columns + x) * 3)
250
+ }
251
+ const write = (x: number, y: number, text: string, fg: number, bg = DEFAULT) => {
252
+ let k = 0
253
+ for (const ch of text) put(x + k++, y, ch, fg, bg)
254
+ return x + k
255
+ }
256
+
257
+ const { focus, need, card, zone, placed } = layoutLibrary(catalog, columns, rows, options.focus)
258
+ const order = drawnBooks(placed)
259
+ const pulled = bookHolding(catalog, highlight)
260
+ const scores = catalog.books.map(b => b.score).sort((a, b) => a - b)
261
+ const medianScore = scores[Math.floor(scores.length / 2)] ?? 0
262
+ const verdictOf = (b: Book) => {
263
+ for (const id of b.memberIds) if (marks[id]) return { kind: marks[id]!, isFresh: true }
264
+ for (const id of b.memberIds) if (stamps[id]) return { kind: stamps[id]!, isFresh: false }
265
+ return undefined
266
+ }
267
+
268
+ for (const c of placed) {
269
+ // The cornice, its plate naming the technology.
270
+ for (let x = c.x; x < c.x + c.width; x++) put(x, c.y, '▄', p.wood)
271
+ const plate = ` ${fit(c.bookcase.tech, c.width - 4)} `
272
+ write(c.x + 1, c.y, plate, p.clothInk, p.wood)
273
+ if (c.hidden > 0) write(c.x + c.width - String(c.hidden).length - 3, c.y, ` +${c.hidden}`, p.clothInk, p.wood)
274
+ c.shelves.forEach(({ shelf, books, hidden }, si) => {
275
+ const plank = c.y + 1 + si * (zone + 1) + zone
276
+ for (let y = plank - zone; y < plank; y++) {
277
+ put(c.x, y, '▕', p.wood)
278
+ put(c.x + c.width - 1, y, '▏', p.wood)
279
+ }
280
+ for (let x = c.x; x < c.x + c.width; x++) put(x, plank, '▀', p.wood)
281
+ let x = c.x + 2
282
+ for (const book of books) {
283
+ const shown = Math.max(0, Math.min(1, frame.grow * order.length - order.indexOf(book)))
284
+ drawBook(book, x, plank, shown)
285
+ const verdict = verdictOf(book)
286
+ if (verdict && shown >= 1) {
287
+ const color = p.verdict[verdict.kind]
288
+ const isFlashing = verdict.isFresh && isMarkOn
289
+ put(x, plank, STAMP[verdict.kind], isFlashing ? p.clothInk : color, isFlashing ? color : DEFAULT)
290
+ }
291
+ x += widthOf(book)
292
+ }
293
+ // The shelf's plate, after its books: its number and the name its community earns (chains.ts shelfName).
294
+ const tail = hidden > 0 ? ` +${hidden}` : ''
295
+ const room = c.x + c.width - 1 - (x + 1)
296
+ const label = ` ${shelf.number} ${fit(shelf.name, room - 4 - tail.length)}${tail} `
297
+ if (room >= 6) write(x + 1, plank, label.slice(0, room), p.clothInk, p.wood)
298
+ })
299
+ }
300
+
301
+ function drawBook(book: Book, x: number, plank: number, shown: number): void {
302
+ const parts = partsOf(book)
303
+ const full = 2 * zone - 1
304
+ // Height in half-rows: a full chain stands tallest, each missing part a half-row shorter; the weaker half of the shelf a half-row lower still.
305
+ const height = Math.max(3, full - (3 - parts.length) - (book.score < medianScore ? 1 : 0))
306
+ const risen = Math.round(height * shown)
307
+ const side = Math.max(1, Math.round(height / 4))
308
+ // Bands bottom → top: fix, problem, cause; the problem takes what the ends leave.
309
+ const bands: number[] = []
310
+ for (const [role, part] of [...parts].reverse()) {
311
+ const span = parts.length === 1 ? height : role === 'problem' ? height - side * (parts.length - 1) : side
312
+ const color = book === pulled && isPulseOn ? p.gilt : p.binding[bindingOf(part.label)]
313
+ for (let k = 0; k < span; k++) bands.push(color)
314
+ }
315
+ const width = widthOf(book)
316
+ for (let col = 0; col < width; col++) {
317
+ const colorAt = (h: number): number | undefined => {
318
+ if (h < risen) return col === 0 ? bands[h] : shade(bands[h]!, 0.38)
319
+ if (h === risen && book === focus && col === 0 && shown >= 1) return p.ribbon
320
+ return undefined
321
+ }
322
+ for (let r = 0; r < zone; r++) {
323
+ const lower = colorAt(2 * r)
324
+ const upper = colorAt(2 * r + 1)
325
+ const y = plank - 1 - r
326
+ if (width === 1) {
327
+ // A pamphlet: the left half of the cell, so neighbours never merge into one block.
328
+ if (lower !== undefined && upper !== undefined) put(x, y, upper === lower ? '▌' : '▀', upper, upper === lower ? DEFAULT : lower)
329
+ else if (lower !== undefined) put(x, y, '▖', lower)
330
+ else if (upper !== undefined) put(x, y, '▘', upper)
331
+ } else if (lower !== undefined && upper !== undefined) put(x + col, y, '▀', upper, lower)
332
+ else if (lower !== undefined) put(x + col, y, '▄', lower)
333
+ else if (upper !== undefined) put(x + col, y, '▀', upper)
334
+ }
335
+ }
336
+ }
337
+ if (focus) drawCard(focus, casesHeight(placed, zone))
338
+
339
+ function drawCard(book: Book, top: number): void {
340
+ const height = Math.min(rows - top, Math.max(card, need))
341
+ if (height < 3) return
342
+ for (let y = top; y < top + height; y++) for (let x = 0; x < columns; x++) put(x, y, SPACE, p.cardInk, p.card)
343
+ const inked = (text: string) => [...text].slice(0, Math.ceil([...text].length * frame.type)).join('')
344
+ const place = placeOf(catalog, book)
345
+ // Header: the call number in the catalog card's red, the case, the shelf.
346
+ const call = callNumber(catalog, book)
347
+ let x = write(1, top, inked(call), p.binding.problem, p.card)
348
+ x = write(x, top, inked(` ${place?.bookcase.tech ?? ''}`), p.cardInk, p.card)
349
+ const shelfText = place ? `shelf ${place.shelf.number} · ${place.shelf.name}` : ''
350
+ const room = columns - x - 3
351
+ if (room >= 10) {
352
+ const s = fit(shelfText, room)
353
+ write(columns - 1 - s.length, top, inked(s), p.cardFaint, p.card)
354
+ }
355
+ // Body: the chain in its order, each part wrapped; the problem gets spare lines first.
356
+ const parts = partsOf(book)
357
+ const body = height - 2
358
+ const textWidth = columns - 5
359
+ const wrapped = parts.map(([, part]) => wrap(part.name, textWidth))
360
+ const lines = parts.map(() => 0)
361
+ const byNeed = [...parts.keys()].sort((a, b) => PRIORITY[parts[a]![0]] - PRIORITY[parts[b]![0]])
362
+ let spare = body
363
+ while (spare > 0 && byNeed.some(i => lines[i]! < wrapped[i]!.length)) {
364
+ for (const i of byNeed) {
365
+ if (spare > 0 && lines[i]! < wrapped[i]!.length) {
366
+ lines[i]!++
367
+ spare--
368
+ }
369
+ }
370
+ }
371
+ let y = top + 1
372
+ parts.forEach(([, part], i) => {
373
+ const color = p.binding[bindingOf(part.label)]
374
+ const ink = part.isDelivered ? p.cardInk : p.cardFaint
375
+ const text = wrapped[i]!.slice(0, lines[i])
376
+ if (lines[i]! < wrapped[i]!.length && text.length > 0) text[text.length - 1] = clip(text[text.length - 1]!, textWidth)
377
+ text.forEach((line, k) => {
378
+ if (k === 0) put(2, y, BINDING_GLYPH[bindingOf(part.label)], color, p.card)
379
+ write(4, y, inked(line), ink, p.card)
380
+ y++
381
+ })
382
+ })
383
+ // Footer: where the chain is anchored, what else it holds, and the agent's verdict.
384
+ const footY = top + height - 1
385
+ const verdict = verdictOf(book)
386
+ const stamp = verdict ? ` ${STAMP[verdict.kind]} ${VERB[verdict.kind]} ` : ''
387
+ const more = [book.moreCauses > 0 ? `+${book.moreCauses} cause${book.moreCauses > 1 ? 's' : ''}` : '', book.moreFixes > 0 ? `+${book.moreFixes} fix${book.moreFixes > 1 ? 'es' : ''}` : '']
388
+ .filter(Boolean)
389
+ .join(' ')
390
+ const facts = [book.anchor ? `@ ${book.anchor}` : '', more, book.isCatalogued ? '' : 'cataloguing…'].filter(Boolean).join(' · ')
391
+ write(2, footY, inked(fit(facts, columns - 4 - stamp.length)), p.cardFaint, p.card)
392
+ if (verdict) {
393
+ const color = p.verdict[verdict.kind]
394
+ const isFlashing = verdict.isFresh && isMarkOn
395
+ write(columns - 1 - stamp.length, footY, stamp, isFlashing ? p.card : color, isFlashing ? color : p.card)
396
+ }
397
+ }
398
+
399
+ return new Uint8Array(words.buffer).toBase64()
400
+ }
401
+
402
+ /** One run of width-1 printable characters (a wide one would shift every cell after it). */
403
+ function flat(text: string): string {
404
+ return text
405
+ .replace(/[–—]/g, '-')
406
+ .replace(/×/g, 'x')
407
+ .replace(/→/g, '->')
408
+ .replace(/…/g, '...')
409
+ .replace(/[‘’]/g, "'")
410
+ .replace(/[“”]/g, '"')
411
+ .replace(/[^\x20-\x7e\s·]/g, '')
412
+ .replace(/\s+/g, ' ')
413
+ .trim()
414
+ }
415
+
416
+ function clip(text: string, max: number): string {
417
+ if (max <= 1) return ''
418
+ return text.length < max ? `${text}~` : `${text.slice(0, max - 1)}~`
419
+ }
420
+
421
+ /** A title that fits `max` cells, the tail clipped with `~`. */
422
+ export function fitTitle(name: string, max: number): string {
423
+ const f = flat(name)
424
+ if (max <= 1) return ''
425
+ return f.length <= max ? f : `${f.slice(0, max - 1)}~`
426
+ }
427
+ const fit = fitTitle
428
+
429
+ /** Word-wrapped lines of at most `width` cells; a word longer than a line is broken. */
430
+ export function wrap(text: string, width: number): string[] {
431
+ const lines: string[] = []
432
+ let line = ''
433
+ for (let word of flat(text).split(' ')) {
434
+ while (word.length > width) {
435
+ if (line) lines.push(line)
436
+ lines.push(word.slice(0, width))
437
+ word = word.slice(width)
438
+ line = ''
439
+ }
440
+ if (!word) continue
441
+ if (!line) line = word
442
+ else if (line.length + 1 + word.length <= width) line += ` ${word}`
443
+ else {
444
+ lines.push(line)
445
+ line = word
446
+ }
447
+ }
448
+ if (line) lines.push(line)
449
+ return lines
450
+ }