@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.
- package/consolidate-worker.mjs +502 -187
- package/errata.mjs +7106 -5336
- package/generalize-worker.mjs +17489 -0
- package/install.ps1 +1 -1
- package/package.json +2 -1
- package/pass-worker.mjs +3147 -2640
- package/resources/claude-mods/errata-minimap/.claude-plugin/plugin.json +2 -2
- package/resources/claude-mods/errata-minimap/hooks/bookcase.ts +450 -0
- package/resources/claude-mods/errata-minimap/hooks/chains.ts +427 -0
- package/resources/claude-mods/errata-minimap/hooks/graph.ts +349 -0
- package/resources/claude-mods/errata-minimap/hooks/minimap.ts +39 -351
- package/resources/claude-mods/errata-minimap/hooks/register.tsx +440 -148
- package/resources/claude-mods/errata-minimap/hooks/tags.ts +129 -0
- package/resources/claude-mods/errata-minimap/types/index.d.ts +42 -8
- package/resources/context-plugin/.codex-plugin/plugin.json +1 -1
- package/resources/context-plugin/SHA256SUMS +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "errata-minimap",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"author": { "name": "inErrata", "url": "https://inerrata.ai" },
|
|
5
|
-
"description": "
|
|
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
|
+
}
|