@inerrata-corporation/errata 2.0.21-dev.4001 → 2.0.21-dev.4004

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 CHANGED
@@ -83973,7 +83973,7 @@ function createWatchBreaker(deps) {
83973
83973
  }
83974
83974
 
83975
83975
  // src/engine.ts
83976
- var DAEMON_VERSION = true ? "2.0.21-dev.4001" : "2.0.0-alpha.0";
83976
+ var DAEMON_VERSION = true ? "2.0.21-dev.4004" : "2.0.0-alpha.0";
83977
83977
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
83978
83978
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
83979
83979
  var GIT_OP_MUTE_MS = 4e3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.21-dev.4001",
3
+ "version": "2.0.21-dev.4004",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errata-minimap",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "author": { "name": "inErrata", "url": "https://inerrata.ai" },
5
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"
@@ -126,7 +126,7 @@ const SPACE = 0x20
126
126
  const TALL_SHELF = 4
127
127
  const SHORT_SHELF = 3
128
128
  /** Cells a shelf keeps after its books for its plate. */
129
- const PLATE = 15
129
+ const PLATE = 17
130
130
  /** Rows the catalog card never gives up: header, two lines of chain, footer. */
131
131
  const MIN_CARD = 4
132
132
  const MAX_CARD = 8
@@ -157,7 +157,7 @@ function placeCases(catalog: Catalog, columns: number, rows: number, zone: numbe
157
157
  let tierHeight = 0
158
158
  for (const bookcase of catalog.cases) {
159
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.
160
+ // Room after the fullest shelf for its plate (" 1 loose volumes "), and on the cornice for the tech.
161
161
  const width = Math.min(columns, Math.max(bookcase.tech.length + 6, fullest + 4 + PLATE))
162
162
  if (x > 0 && x + width > columns) {
163
163
  x = 0
@@ -185,6 +185,40 @@ function placeCases(catalog: Catalog, columns: number, rows: number, zone: numbe
185
185
  return placed
186
186
  }
187
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
+
188
222
  /** Every book drawn, in reading order: the order the reveal raises them. */
189
223
  function drawnBooks(placed: Placed[]): Book[] {
190
224
  return placed.flatMap(p => p.shelves.flatMap(s => s.books))
@@ -220,12 +254,7 @@ export function rasterizeLibrary(catalog: Catalog, columns: number, rows: number
220
254
  return x + k
221
255
  }
222
256
 
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)
257
+ const { focus, need, card, zone, placed } = layoutLibrary(catalog, columns, rows, options.focus)
229
258
  const order = drawnBooks(placed)
230
259
  const pulled = bookHolding(catalog, highlight)
231
260
  const scores = catalog.books.map(b => b.score).sort((a, b) => a - b)
@@ -37,7 +37,6 @@ export type Book = {
37
37
  memberIds: string[]
38
38
  tech?: string
39
39
  community?: string
40
- communityLabel?: string | null
41
40
  patterns: string[]
42
41
  anchor?: string
43
42
  /** Its `errata_neighbors` answer has arrived. */
@@ -247,10 +246,7 @@ export function catalogue(graph: MapGraph, facts: Record<string, ChainFacts | nu
247
246
  }
248
247
  // Only cloud-merged rows carry a community; theirs is the cloud partition's, kept apart from the local one.
249
248
  const cloud = book.community ? undefined : graph.nodes.find(n => book.memberIds.includes(n.id) && n.community)?.community
250
- if (cloud) {
251
- book.community = `cloud_${cloud.id}`
252
- book.communityLabel = cloud.label
253
- }
249
+ if (cloud) book.community = `cloud_${cloud.id}`
254
250
  book.anchor = anchorOf(f, book.problem?.name)
255
251
  candidates.set(book.key, techs.filter((t, i) => techs.findIndex(u => u.name === t.name) === i))
256
252
  }
@@ -261,8 +257,10 @@ export function catalogue(graph: MapGraph, facts: Record<string, ChainFacts | nu
261
257
  }
262
258
 
263
259
  const GENERAL = 'general'
264
- /** A shelf holds a community only when two of its books are in view; the rest share the miscellany. */
260
+ /** A shelf holds a community, or a shared Pattern, only when two of its books are in view. */
265
261
  const MIN_SHELF = 2
262
+ /** The shelf for books that share neither with another book in view. */
263
+ export const LOOSE = 'loose volumes'
266
264
 
267
265
  function bookcases(books: Book[]): Bookcase[] {
268
266
  const byTech = new Map<string, Book[]>()
@@ -272,31 +270,44 @@ function bookcases(books: Book[]): Bookcase[] {
272
270
  .map(([tech, held]) => ({ tech, code: classMark(tech), shelves: shelves(held), isGeneral: tech === GENERAL }))
273
271
  }
274
272
 
273
+ /**
274
+ * Shelves in three tiers: a community with two books in view; then, among the
275
+ * books left, a Pattern two of them instance (agent-named shapes are often the
276
+ * truest grouping there is); then the loose volumes. A burst of ten chains
277
+ * rarely holds two from one community — they are small — so community alone
278
+ * put nearly every book on one catch-all shelf.
279
+ */
275
280
  function shelves(books: Book[]): Shelf[] {
281
+ const out: Shelf[] = []
276
282
  const byCommunity = new Map<string, Book[]>()
277
283
  for (const b of books) if (b.community) byCommunity.set(b.community, [...(byCommunity.get(b.community) ?? []), b])
278
- const out: Shelf[] = []
279
284
  for (const [key, held] of byCommunity) {
280
- if (held.length < MIN_SHELF) continue
281
- out.push({ key, name: shelfName(held), number: 0, books: held, isMiscellany: false })
285
+ if (held.length >= MIN_SHELF) out.push({ key, name: shelfName(held), number: 0, books: held, isMiscellany: false })
286
+ }
287
+ let rest = books.filter(b => !out.some(s => s.books.includes(b)))
288
+ // The most-shared Pattern first, so a book that instances several joins the biggest group.
289
+ for (;;) {
290
+ const count = new Map<string, Book[]>()
291
+ for (const b of rest) for (const p of new Set(b.patterns)) count.set(p, [...(count.get(p) ?? []), b])
292
+ const best = [...count].sort((a, b) => b[1].length - a[1].length || a[0].localeCompare(b[0]))[0]
293
+ if (!best || best[1].length < MIN_SHELF) break
294
+ out.push({ key: `pattern:${best[0]}`, name: patternTitle(best[0]), number: 0, books: best[1], isMiscellany: false })
295
+ rest = rest.filter(b => !best[1].includes(b))
282
296
  }
283
- const shelved = new Set(out.flatMap(s => s.books))
284
- const rest = books.filter(b => !shelved.has(b))
285
- if (rest.length > 0) out.push({ key: 'miscellany', name: 'miscellany', number: 0, books: rest, isMiscellany: true })
297
+ if (rest.length > 0) out.push({ key: 'loose', name: LOOSE, number: 0, books: rest, isMiscellany: true })
286
298
  out.forEach((s, i) => (s.number = i + 1))
287
299
  return out
288
300
  }
289
301
 
290
302
  /**
291
303
  * A community has no name of its own (the local partition's label is null), so
292
- * the shelf is named from what its books share: the cloud theme when the
293
- * community carries one; else the Pattern most of its books are INSTANCE_OF —
294
- * the graph's own generalisation of what those problems have in common, the
295
- * truest name the data offers; else its strongest problem, the shelf's exemplar.
304
+ * the shelf is named from what its books share: the Pattern most of its books
305
+ * are INSTANCE_OF — the graph's own generalisation of what those problems have
306
+ * in common — else its strongest problem, the shelf's exemplar. Never the cloud
307
+ * community's theme label: those can describe other members entirely (#43
308
+ * "Canvas zoom/scroll…" held SQLite busy_timeout problems).
296
309
  */
297
310
  export function shelfName(books: Book[]): string {
298
- const label = books.find(b => b.communityLabel)?.communityLabel
299
- if (label) return label
300
311
  const count = new Map<string, number>()
301
312
  for (const b of books) for (const p of new Set(b.patterns)) count.set(p, (count.get(p) ?? 0) + 1)
302
313
  if (count.size > 0) return patternTitle([...count].reduce((a, b) => (b[1] > a[1] ? b : a))[0])
@@ -2,7 +2,7 @@ import { atom, read, update } from 'claude-code'
2
2
  import type { EngineInterface, Register, Timer } from 'claude-code'
3
3
 
4
4
  import type { Burst, ChainFacts, MapGraph, Verdict } from '../types'
5
- import { BINDING_GLYPH, BINDING_NAME, bindingOf, fitTitle, focusBook, PALETTE, rasterizeLibrary } from './bookcase'
5
+ import { BINDING_GLYPH, BINDING_NAME, bindingOf, fitTitle, focusBook, librarySpots, PALETTE, rasterizeLibrary } from './bookcase'
6
6
  import type { Binding, LibraryFrame, LibraryOptions, Room } from './bookcase'
7
7
  import { bookHolding, callNumber, catalogue, enrichTargets, parseNeighbors } from './chains'
8
8
  import type { Catalog } from './chains'
@@ -84,6 +84,10 @@ const PANE_VIEW_KEY = 'paneView'
84
84
 
85
85
  /** Rows an inline pane asks for: room for a tier of bookcases and the card without burying the transcript. */
86
86
  const INLINE_ROWS = 20
87
+ /** Rows on a portrait terminal: the header, one tier of shelves, a short card. */
88
+ const COMPACT_ROWS = 11
89
+ /** The terminal's own colour in a raster cell (bit 24 alone). */
90
+ const DEFAULT_CELL = 0x01000000
87
91
 
88
92
  const GROW_MS = 900
89
93
  const TYPE_MS = 500
@@ -191,6 +195,8 @@ async function animate($: EngineInterface): Promise<void> {
191
195
  if (!isRevealing && !motion.highlight && !isMarking) {
192
196
  motion.timer?.cancel()
193
197
  motion.timer = undefined
198
+ // At rest again: redraw once so the books' hover and click targets are laid (they stay off while blits run).
199
+ if (pane.view === 'books') $.ui.invalidate('ui.render')
194
200
  }
195
201
  }
196
202
 
@@ -522,8 +528,15 @@ export const register: Register = on => {
522
528
  const { Box, Text } = $.ui.resolve(e)
523
529
  const width = Math.max(24, e.props.bodyColumns)
524
530
  // Docked, the library takes the screen's height; inline it sits above the prompt and keeps to its row budget.
531
+ // A portrait terminal (taller than wide, at ~2 rows per column of width) can't dock and has the least
532
+ // transcript to spare, so inline there the library is compact: one tier of shelves and a short card.
525
533
  const screenRows = e.viewport?.rows ?? 30
526
- const height = e.props.placement === 'inline' ? Math.max(12, Math.min(INLINE_ROWS, screenRows - 10)) : Math.max(12, screenRows - 4)
534
+ const isPortrait = e.props.placement === 'inline' && !!e.viewport && e.viewport.rows * 2 > e.viewport.columns
535
+ const height = isPortrait
536
+ ? Math.max(10, Math.min(COMPACT_ROWS, screenRows - 6))
537
+ : e.props.placement === 'inline'
538
+ ? Math.max(12, Math.min(INLINE_ROWS, screenRows - 10))
539
+ : Math.max(12, screenRows - 4)
527
540
  const map = await read($, graph)
528
541
  const hex = (color: number) => `#${color.toString(16).padStart(6, '0')}`
529
542
 
@@ -611,7 +624,8 @@ export const register: Register = on => {
611
624
  const now = await $.clock.now()
612
625
  // A verdict on a prior not on the shelves has no plank to be stamped on: it gets a line.
613
626
  const unshelved = (await read($, verdicts)).filter(v => now - v.at < VERDICT_SHOWN_MS && !bookHolding(catalog, v.id)).slice(0, 2)
614
- const libraryRows = Math.max(8, height - 1 - (legend.length > 0 ? 1 : 0) - unshelved.length)
627
+ const showLegend = legend.length > 0 && !isPortrait
628
+ const libraryRows = Math.max(8, height - 1 - (showLegend ? 1 : 0) - unshelved.length)
615
629
  // Blits must match the mounted size: remember what this pass mounts.
616
630
  motion.raster = { columns: width, rows: libraryRows }
617
631
  const options = await libraryOptions($, map)
@@ -625,8 +639,73 @@ export const register: Register = on => {
625
639
  </Box>
626
640
  ) : e.surface === 'terminal' ? (
627
641
  (() => {
628
- const { Raster } = $.ui.resolve(e)
629
- return <Raster key="map" columns={width} rows={libraryRows} cells={rasterizeLibrary(catalog, width, libraryRows, options)} />
642
+ const { Raster, Button } = $.ui.resolve(e)
643
+ const cells = rasterizeLibrary(catalog, width, libraryRows, options)
644
+ // Hover and click ride on positioned replicas of each spine (same glyphs, same colours) laid
645
+ // over the raster, since pointer feedback attaches to elements, not cells. Laid only at rest:
646
+ // a reveal, a burst highlight or a flashing stamp repaints the raster by blit, which a static
647
+ // replica would hide.
648
+ const isAtRest = motion.frame.type >= 1 && !motion.highlight && Object.keys(motion.marks).length === 0
649
+ const spots = isAtRest ? librarySpots(catalog, width, libraryRows, options.focus) : []
650
+ const grid = new Uint32Array(Uint8Array.fromBase64(cells).buffer)
651
+ const cell = (x: number, y: number) => {
652
+ const i = (y * width + x) * 3
653
+ return { ch: String.fromCodePoint(grid[i]!), fg: grid[i + 1]!, bg: grid[i + 2]! }
654
+ }
655
+ const colourOf = (c: number) => (c === DEFAULT_CELL ? undefined : hex(c))
656
+ const cardWidth = Math.min(46, width - 2)
657
+ return (
658
+ <Box key="library" position="relative" width={width} height={libraryRows}>
659
+ <Raster key="map" columns={width} rows={libraryRows} cells={cells} />
660
+ {spots.map(s => {
661
+ const scope = `book:${s.book.key}`.slice(0, 64)
662
+ const parts = (['cause', 'problem', 'fix'] as const).flatMap(r => (s.book[r] ? [s.book[r]!] : []))
663
+ const cardHeight = parts.length + 3
664
+ const cardLeft = s.x + s.width + 1 + cardWidth <= width ? s.x + s.width + 1 : Math.max(0, s.x - cardWidth - 1)
665
+ const cardTop = Math.max(0, Math.min(s.top, libraryRows - cardHeight))
666
+ const pin = () => {
667
+ motion.focus = s.book.problem?.id ?? s.book.memberIds[0]
668
+ $.ui.invalidate('ui.render')
669
+ }
670
+ return [
671
+ ...[...Array(s.height).keys()].map(r => (
672
+ <Box key={`${scope}:${r}`} position="absolute" top={s.top + r} left={s.x} hover={{ scope }}>
673
+ <Button key={`${scope}:press:${r}`} plain onPress={pin}>
674
+ {[...Array(s.width).keys()].map(dx => {
675
+ const c = cell(s.x + dx, s.top + r)
676
+ return <Text color={colourOf(c.fg)} backgroundColor={colourOf(c.bg)}>{c.ch}</Text>
677
+ })}
678
+ </Button>
679
+ </Box>
680
+ )),
681
+ // The hover card: what the book holds, revealed while the pointer is anywhere on its spine.
682
+ <Box
683
+ key={`${scope}:card`}
684
+ position="absolute"
685
+ top={cardTop}
686
+ left={cardLeft}
687
+ width={cardWidth}
688
+ display="none"
689
+ hover={{ scope, display: 'flex' }}
690
+ flexDirection="column"
691
+ borderStyle="round"
692
+ borderColor={hex(palette.gilt)}
693
+ >
694
+ <Text bold wrap="truncate-end" color={hex(palette.binding.problem)}>
695
+ {callNumber(catalog, s.book)} <Text dimColor>{s.book.tech ?? 'general'}</Text>
696
+ </Text>
697
+ {parts.map(part => (
698
+ <Text wrap="truncate-end">
699
+ <Text color={hex(palette.binding[bindingOf(part.label)])}>{BINDING_GLYPH[bindingOf(part.label)]} </Text>
700
+ <Text dimColor={!part.isDelivered}>{fitTitle(part.name, cardWidth - 6)}</Text>
701
+ </Text>
702
+ ))}
703
+ <Text dimColor wrap="truncate-end">click to read it on the card</Text>
704
+ </Box>,
705
+ ]
706
+ })}
707
+ </Box>
708
+ )
630
709
  })()
631
710
  ) : (
632
711
  // No cell grid off the terminal: the catalog as a reading list, the book in focus first.
@@ -656,7 +735,7 @@ export const register: Register = on => {
656
735
  </Text>
657
736
  </Text>
658
737
  {library}
659
- {legend.length > 0 && (
738
+ {showLegend && (
660
739
  <Text wrap="truncate-end">
661
740
  {legend.map(b => (
662
741
  <Text>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errata-context",
3
- "version": "2.0.21-dev.4001",
3
+ "version": "2.0.21-dev.4004",
4
4
  "description": "A minimal 3D view of loaded Errata priors and recent graph bursts.",
5
5
  "author": {
6
6
  "name": "inErrata"
@@ -1,4 +1,4 @@
1
- b4a35bcf25cb49d9bb4f20629bda56f9e153f9907fa2752b386ba65f723b48d2 .codex-plugin/plugin.json
1
+ 988e2d2c901e9c5271b40684d0e30351ea8bae6c46ebb98c5067f6fb2944f1ab .codex-plugin/plugin.json
2
2
  a7bebae9fb01c838d6c75fc784129e28be571233fa66ff5ee8039b574f35ccf7 assets/icon.png
3
3
  fc38b64e4df1e0b72c35764bef6c4c05430d97c9009ad0ecc8a9e75b4deab232 configure-local.mjs
4
4
  73dc6028b3b070257c7c31f252d11e55a9e096f4aed0f187f10cb21716e1e6d7 dist/client.js