@inerrata-corporation/errata 2.0.21-dev.3993 → 2.0.21-dev.3996
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/errata.mjs +13 -5
- package/package.json +1 -1
- package/resources/claude-mods/errata-minimap/.claude-plugin/plugin.json +2 -2
- package/resources/claude-mods/errata-minimap/hooks/bookcase.ts +421 -0
- package/resources/claude-mods/errata-minimap/hooks/chains.ts +416 -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 +346 -149
- 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
package/errata.mjs
CHANGED
|
@@ -45013,6 +45013,10 @@ function edgeTier(store, farId) {
|
|
|
45013
45013
|
function byTier(store, items, farId) {
|
|
45014
45014
|
return items.map((item2, i2) => ({ item: item2, i: i2, tier: edgeTier(store, farId(item2)) })).sort((a, b) => a.tier - b.tier || a.i - b.i).map((x) => x.item);
|
|
45015
45015
|
}
|
|
45016
|
+
function localCommunityOf(node2) {
|
|
45017
|
+
const cid = node2.attrs["localCommunity"];
|
|
45018
|
+
return typeof cid === "string" ? cid : null;
|
|
45019
|
+
}
|
|
45016
45020
|
function searchGraph(store, query, limit) {
|
|
45017
45021
|
const q = query.trim();
|
|
45018
45022
|
if (!q || !Number.isFinite(limit) || limit < 1) return [];
|
|
@@ -45550,7 +45554,7 @@ var init_mcp = __esm({
|
|
|
45550
45554
|
},
|
|
45551
45555
|
{
|
|
45552
45556
|
name: "errata.neighbors",
|
|
45553
|
-
description: "Return direct in/out edges of a node. Pass either a nodeId or a qname (e.g. ClassName.method) \u2014 resolved like the other nav verbs. Use after errata.search or errata.locate to walk the graph. A cloud id from search's collective blend (e.g. dprob_\u2026) that isn't in the local graph falls back to its collective neighborhood, tagged provenance:collective.",
|
|
45557
|
+
description: "Return direct in/out edges of a node. Pass either a nodeId or a qname (e.g. ClassName.method) \u2014 resolved like the other nav verbs. Use after errata.search or errata.locate to walk the graph. `node.community` (and each placed far end's `community`) is the local partition's community id, null when unplaced. A cloud id from search's collective blend (e.g. dprob_\u2026) that isn't in the local graph falls back to its collective neighborhood, tagged provenance:collective.",
|
|
45554
45558
|
inputSchema: {
|
|
45555
45559
|
type: "object",
|
|
45556
45560
|
properties: {
|
|
@@ -45588,12 +45592,16 @@ var init_mcp = __esm({
|
|
|
45588
45592
|
const p = e.attrs?.provenance;
|
|
45589
45593
|
return p ? { provenance: p } : {};
|
|
45590
45594
|
};
|
|
45595
|
+
const far = (n) => {
|
|
45596
|
+
const community = localCommunityOf(n);
|
|
45597
|
+
return { id: n.id, label: n.label, name: n.description, ...community ? { community } : {} };
|
|
45598
|
+
};
|
|
45591
45599
|
const out2 = byTier(store, store.outEdges(id), (e) => e.to).slice(0, limit).map((e) => {
|
|
45592
45600
|
const t = store.getNode(e.to);
|
|
45593
45601
|
return {
|
|
45594
45602
|
type: e.type,
|
|
45595
45603
|
...prov(e),
|
|
45596
|
-
to: t ?
|
|
45604
|
+
to: t ? far(t) : { id: e.to }
|
|
45597
45605
|
};
|
|
45598
45606
|
});
|
|
45599
45607
|
const incoming = byTier(store, store.inEdges(id), (e) => e.from).slice(0, limit).map((e) => {
|
|
@@ -45601,12 +45609,12 @@ var init_mcp = __esm({
|
|
|
45601
45609
|
return {
|
|
45602
45610
|
type: e.type,
|
|
45603
45611
|
...prov(e),
|
|
45604
|
-
from: f ?
|
|
45612
|
+
from: f ? far(f) : { id: e.from }
|
|
45605
45613
|
};
|
|
45606
45614
|
});
|
|
45607
45615
|
return {
|
|
45608
45616
|
found: true,
|
|
45609
|
-
node: { id: node2.id, label: node2.label, name: node2.description },
|
|
45617
|
+
node: { id: node2.id, label: node2.label, name: node2.description, community: localCommunityOf(node2) },
|
|
45610
45618
|
out: out2,
|
|
45611
45619
|
in: incoming
|
|
45612
45620
|
};
|
|
@@ -83934,7 +83942,7 @@ function createWatchBreaker(deps) {
|
|
|
83934
83942
|
}
|
|
83935
83943
|
|
|
83936
83944
|
// src/engine.ts
|
|
83937
|
-
var DAEMON_VERSION = true ? "2.0.21-dev.
|
|
83945
|
+
var DAEMON_VERSION = true ? "2.0.21-dev.3996" : "2.0.0-alpha.0";
|
|
83938
83946
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
83939
83947
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
83940
83948
|
var GIT_OP_MUTE_MS = 4e3;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "errata-minimap",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.3.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,421 @@
|
|
|
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 = 15
|
|
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 miscellany "), 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
|
+
/** Every book drawn, in reading order: the order the reveal raises them. */
|
|
189
|
+
function drawnBooks(placed: Placed[]): Book[] {
|
|
190
|
+
return placed.flatMap(p => p.shelves.flatMap(s => s.books))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Rows the cases take. */
|
|
194
|
+
function casesHeight(placed: Placed[], zone: number): number {
|
|
195
|
+
return placed.reduce((h, p) => Math.max(h, p.y + 1 + p.shelves.length * (zone + 1)), 0)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function shade(color: number, by: number): number {
|
|
199
|
+
const ch = (s: number) => Math.round(((color >> s) & 0xff) * (1 - by))
|
|
200
|
+
return (ch(16) << 16) | (ch(8) << 8) | ch(0)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The library as Raster cells (`[codePoint, fg, bg]` u32 triplets, row-major,
|
|
205
|
+
* base64). Every frame of the reveal draws a prefix of the finished picture,
|
|
206
|
+
* so nothing jumps as it fills: cases first, then each book rises onto its
|
|
207
|
+
* shelf in reading order, then the card inks in.
|
|
208
|
+
*/
|
|
209
|
+
export function rasterizeLibrary(catalog: Catalog, columns: number, rows: number, options: LibraryOptions = {}): string {
|
|
210
|
+
const { room = 'stacks', frame = { grow: 1, type: 1 }, highlight, isPulseOn = true, marks = {}, isMarkOn = true, stamps = {} } = options
|
|
211
|
+
const p = PALETTE[room]
|
|
212
|
+
const words = new Uint32Array(columns * rows * 3)
|
|
213
|
+
for (let i = 0; i < columns * rows; i++) words.set([SPACE, DEFAULT, DEFAULT], i * 3)
|
|
214
|
+
const put = (x: number, y: number, ch: string | number, fg: number, bg = DEFAULT) => {
|
|
215
|
+
if (x >= 0 && y >= 0 && x < columns && y < rows) words.set([typeof ch === 'number' ? ch : ch.codePointAt(0)!, fg, bg], (y * columns + x) * 3)
|
|
216
|
+
}
|
|
217
|
+
const write = (x: number, y: number, text: string, fg: number, bg = DEFAULT) => {
|
|
218
|
+
let k = 0
|
|
219
|
+
for (const ch of text) put(x + k++, y, ch, fg, bg)
|
|
220
|
+
return x + k
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const focus = focusBook(catalog, options.focus)
|
|
224
|
+
// The card asks for its chain in full (header, every wrapped line, footer); the cases get the rest, at least a third.
|
|
225
|
+
const need = focus ? 2 + partsOf(focus).reduce((n, [, part]) => n + wrap(part.name, columns - 5).length, 0) : 0
|
|
226
|
+
const card = Math.max(MIN_CARD, Math.min(need, Math.max(MAX_CARD, Math.round(rows * 0.6)), rows - Math.ceil(rows / 3)))
|
|
227
|
+
const zone = rows - card >= 1 + TALL_SHELF + 1 ? TALL_SHELF : SHORT_SHELF
|
|
228
|
+
const placed = placeCases(catalog, columns, rows - card, zone)
|
|
229
|
+
const order = drawnBooks(placed)
|
|
230
|
+
const pulled = bookHolding(catalog, highlight)
|
|
231
|
+
const scores = catalog.books.map(b => b.score).sort((a, b) => a - b)
|
|
232
|
+
const medianScore = scores[Math.floor(scores.length / 2)] ?? 0
|
|
233
|
+
const verdictOf = (b: Book) => {
|
|
234
|
+
for (const id of b.memberIds) if (marks[id]) return { kind: marks[id]!, isFresh: true }
|
|
235
|
+
for (const id of b.memberIds) if (stamps[id]) return { kind: stamps[id]!, isFresh: false }
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
for (const c of placed) {
|
|
240
|
+
// The cornice, its plate naming the technology.
|
|
241
|
+
for (let x = c.x; x < c.x + c.width; x++) put(x, c.y, '▄', p.wood)
|
|
242
|
+
const plate = ` ${fit(c.bookcase.tech, c.width - 4)} `
|
|
243
|
+
write(c.x + 1, c.y, plate, p.clothInk, p.wood)
|
|
244
|
+
if (c.hidden > 0) write(c.x + c.width - String(c.hidden).length - 3, c.y, ` +${c.hidden}`, p.clothInk, p.wood)
|
|
245
|
+
c.shelves.forEach(({ shelf, books, hidden }, si) => {
|
|
246
|
+
const plank = c.y + 1 + si * (zone + 1) + zone
|
|
247
|
+
for (let y = plank - zone; y < plank; y++) {
|
|
248
|
+
put(c.x, y, '▕', p.wood)
|
|
249
|
+
put(c.x + c.width - 1, y, '▏', p.wood)
|
|
250
|
+
}
|
|
251
|
+
for (let x = c.x; x < c.x + c.width; x++) put(x, plank, '▀', p.wood)
|
|
252
|
+
let x = c.x + 2
|
|
253
|
+
for (const book of books) {
|
|
254
|
+
const shown = Math.max(0, Math.min(1, frame.grow * order.length - order.indexOf(book)))
|
|
255
|
+
drawBook(book, x, plank, shown)
|
|
256
|
+
const verdict = verdictOf(book)
|
|
257
|
+
if (verdict && shown >= 1) {
|
|
258
|
+
const color = p.verdict[verdict.kind]
|
|
259
|
+
const isFlashing = verdict.isFresh && isMarkOn
|
|
260
|
+
put(x, plank, STAMP[verdict.kind], isFlashing ? p.clothInk : color, isFlashing ? color : DEFAULT)
|
|
261
|
+
}
|
|
262
|
+
x += widthOf(book)
|
|
263
|
+
}
|
|
264
|
+
// The shelf's plate, after its books: its number and the name its community earns (chains.ts shelfName).
|
|
265
|
+
const tail = hidden > 0 ? ` +${hidden}` : ''
|
|
266
|
+
const room = c.x + c.width - 1 - (x + 1)
|
|
267
|
+
const label = ` ${shelf.number} ${fit(shelf.name, room - 4 - tail.length)}${tail} `
|
|
268
|
+
if (room >= 6) write(x + 1, plank, label.slice(0, room), p.clothInk, p.wood)
|
|
269
|
+
})
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function drawBook(book: Book, x: number, plank: number, shown: number): void {
|
|
273
|
+
const parts = partsOf(book)
|
|
274
|
+
const full = 2 * zone - 1
|
|
275
|
+
// 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.
|
|
276
|
+
const height = Math.max(3, full - (3 - parts.length) - (book.score < medianScore ? 1 : 0))
|
|
277
|
+
const risen = Math.round(height * shown)
|
|
278
|
+
const side = Math.max(1, Math.round(height / 4))
|
|
279
|
+
// Bands bottom → top: fix, problem, cause; the problem takes what the ends leave.
|
|
280
|
+
const bands: number[] = []
|
|
281
|
+
for (const [role, part] of [...parts].reverse()) {
|
|
282
|
+
const span = parts.length === 1 ? height : role === 'problem' ? height - side * (parts.length - 1) : side
|
|
283
|
+
const color = book === pulled && isPulseOn ? p.gilt : p.binding[bindingOf(part.label)]
|
|
284
|
+
for (let k = 0; k < span; k++) bands.push(color)
|
|
285
|
+
}
|
|
286
|
+
const width = widthOf(book)
|
|
287
|
+
for (let col = 0; col < width; col++) {
|
|
288
|
+
const colorAt = (h: number): number | undefined => {
|
|
289
|
+
if (h < risen) return col === 0 ? bands[h] : shade(bands[h]!, 0.38)
|
|
290
|
+
if (h === risen && book === focus && col === 0 && shown >= 1) return p.ribbon
|
|
291
|
+
return undefined
|
|
292
|
+
}
|
|
293
|
+
for (let r = 0; r < zone; r++) {
|
|
294
|
+
const lower = colorAt(2 * r)
|
|
295
|
+
const upper = colorAt(2 * r + 1)
|
|
296
|
+
const y = plank - 1 - r
|
|
297
|
+
if (width === 1) {
|
|
298
|
+
// A pamphlet: the left half of the cell, so neighbours never merge into one block.
|
|
299
|
+
if (lower !== undefined && upper !== undefined) put(x, y, upper === lower ? '▌' : '▀', upper, upper === lower ? DEFAULT : lower)
|
|
300
|
+
else if (lower !== undefined) put(x, y, '▖', lower)
|
|
301
|
+
else if (upper !== undefined) put(x, y, '▘', upper)
|
|
302
|
+
} else if (lower !== undefined && upper !== undefined) put(x + col, y, '▀', upper, lower)
|
|
303
|
+
else if (lower !== undefined) put(x + col, y, '▄', lower)
|
|
304
|
+
else if (upper !== undefined) put(x + col, y, '▀', upper)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (focus) drawCard(focus, casesHeight(placed, zone))
|
|
309
|
+
|
|
310
|
+
function drawCard(book: Book, top: number): void {
|
|
311
|
+
const height = Math.min(rows - top, Math.max(card, need))
|
|
312
|
+
if (height < 3) return
|
|
313
|
+
for (let y = top; y < top + height; y++) for (let x = 0; x < columns; x++) put(x, y, SPACE, p.cardInk, p.card)
|
|
314
|
+
const inked = (text: string) => [...text].slice(0, Math.ceil([...text].length * frame.type)).join('')
|
|
315
|
+
const place = placeOf(catalog, book)
|
|
316
|
+
// Header: the call number in the catalog card's red, the case, the shelf.
|
|
317
|
+
const call = callNumber(catalog, book)
|
|
318
|
+
let x = write(1, top, inked(call), p.binding.problem, p.card)
|
|
319
|
+
x = write(x, top, inked(` ${place?.bookcase.tech ?? ''}`), p.cardInk, p.card)
|
|
320
|
+
const shelfText = place ? `shelf ${place.shelf.number} · ${place.shelf.name}` : ''
|
|
321
|
+
const room = columns - x - 3
|
|
322
|
+
if (room >= 10) {
|
|
323
|
+
const s = fit(shelfText, room)
|
|
324
|
+
write(columns - 1 - s.length, top, inked(s), p.cardFaint, p.card)
|
|
325
|
+
}
|
|
326
|
+
// Body: the chain in its order, each part wrapped; the problem gets spare lines first.
|
|
327
|
+
const parts = partsOf(book)
|
|
328
|
+
const body = height - 2
|
|
329
|
+
const textWidth = columns - 5
|
|
330
|
+
const wrapped = parts.map(([, part]) => wrap(part.name, textWidth))
|
|
331
|
+
const lines = parts.map(() => 0)
|
|
332
|
+
const byNeed = [...parts.keys()].sort((a, b) => PRIORITY[parts[a]![0]] - PRIORITY[parts[b]![0]])
|
|
333
|
+
let spare = body
|
|
334
|
+
while (spare > 0 && byNeed.some(i => lines[i]! < wrapped[i]!.length)) {
|
|
335
|
+
for (const i of byNeed) {
|
|
336
|
+
if (spare > 0 && lines[i]! < wrapped[i]!.length) {
|
|
337
|
+
lines[i]!++
|
|
338
|
+
spare--
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
let y = top + 1
|
|
343
|
+
parts.forEach(([, part], i) => {
|
|
344
|
+
const color = p.binding[bindingOf(part.label)]
|
|
345
|
+
const ink = part.isDelivered ? p.cardInk : p.cardFaint
|
|
346
|
+
const text = wrapped[i]!.slice(0, lines[i])
|
|
347
|
+
if (lines[i]! < wrapped[i]!.length && text.length > 0) text[text.length - 1] = clip(text[text.length - 1]!, textWidth)
|
|
348
|
+
text.forEach((line, k) => {
|
|
349
|
+
if (k === 0) put(2, y, BINDING_GLYPH[bindingOf(part.label)], color, p.card)
|
|
350
|
+
write(4, y, inked(line), ink, p.card)
|
|
351
|
+
y++
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
// Footer: where the chain is anchored, what else it holds, and the agent's verdict.
|
|
355
|
+
const footY = top + height - 1
|
|
356
|
+
const verdict = verdictOf(book)
|
|
357
|
+
const stamp = verdict ? ` ${STAMP[verdict.kind]} ${VERB[verdict.kind]} ` : ''
|
|
358
|
+
const more = [book.moreCauses > 0 ? `+${book.moreCauses} cause${book.moreCauses > 1 ? 's' : ''}` : '', book.moreFixes > 0 ? `+${book.moreFixes} fix${book.moreFixes > 1 ? 'es' : ''}` : '']
|
|
359
|
+
.filter(Boolean)
|
|
360
|
+
.join(' ')
|
|
361
|
+
const facts = [book.anchor ? `@ ${book.anchor}` : '', more, book.isCatalogued ? '' : 'cataloguing…'].filter(Boolean).join(' · ')
|
|
362
|
+
write(2, footY, inked(fit(facts, columns - 4 - stamp.length)), p.cardFaint, p.card)
|
|
363
|
+
if (verdict) {
|
|
364
|
+
const color = p.verdict[verdict.kind]
|
|
365
|
+
const isFlashing = verdict.isFresh && isMarkOn
|
|
366
|
+
write(columns - 1 - stamp.length, footY, stamp, isFlashing ? p.card : color, isFlashing ? color : p.card)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return new Uint8Array(words.buffer).toBase64()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** One run of width-1 printable characters (a wide one would shift every cell after it). */
|
|
374
|
+
function flat(text: string): string {
|
|
375
|
+
return text
|
|
376
|
+
.replace(/[–—]/g, '-')
|
|
377
|
+
.replace(/×/g, 'x')
|
|
378
|
+
.replace(/→/g, '->')
|
|
379
|
+
.replace(/…/g, '...')
|
|
380
|
+
.replace(/[‘’]/g, "'")
|
|
381
|
+
.replace(/[“”]/g, '"')
|
|
382
|
+
.replace(/[^\x20-\x7e\s·]/g, '')
|
|
383
|
+
.replace(/\s+/g, ' ')
|
|
384
|
+
.trim()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function clip(text: string, max: number): string {
|
|
388
|
+
if (max <= 1) return ''
|
|
389
|
+
return text.length < max ? `${text}~` : `${text.slice(0, max - 1)}~`
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/** A title that fits `max` cells, the tail clipped with `~`. */
|
|
393
|
+
export function fitTitle(name: string, max: number): string {
|
|
394
|
+
const f = flat(name)
|
|
395
|
+
if (max <= 1) return ''
|
|
396
|
+
return f.length <= max ? f : `${f.slice(0, max - 1)}~`
|
|
397
|
+
}
|
|
398
|
+
const fit = fitTitle
|
|
399
|
+
|
|
400
|
+
/** Word-wrapped lines of at most `width` cells; a word longer than a line is broken. */
|
|
401
|
+
export function wrap(text: string, width: number): string[] {
|
|
402
|
+
const lines: string[] = []
|
|
403
|
+
let line = ''
|
|
404
|
+
for (let word of flat(text).split(' ')) {
|
|
405
|
+
while (word.length > width) {
|
|
406
|
+
if (line) lines.push(line)
|
|
407
|
+
lines.push(word.slice(0, width))
|
|
408
|
+
word = word.slice(width)
|
|
409
|
+
line = ''
|
|
410
|
+
}
|
|
411
|
+
if (!word) continue
|
|
412
|
+
if (!line) line = word
|
|
413
|
+
else if (line.length + 1 + word.length <= width) line += ` ${word}`
|
|
414
|
+
else {
|
|
415
|
+
lines.push(line)
|
|
416
|
+
line = word
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (line) lines.push(line)
|
|
420
|
+
return lines
|
|
421
|
+
}
|