@inerrata-corporation/errata 2.0.21-dev.4009 → 2.0.21-dev.4014

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.
@@ -17371,7 +17371,7 @@ var SqliteGraphStore = class {
17371
17371
  if (!stmt) {
17372
17372
  const cols = attrKeys.map((k, i) => `, json_extract(attrs_json, '$.${k}') AS a${i}`).join("");
17373
17373
  stmt = this.db.prepare(
17374
- `SELECT id, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
17374
+ `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
17375
17375
  );
17376
17376
  this.fieldStmts.set(cacheKey, stmt);
17377
17377
  }
@@ -17383,6 +17383,7 @@ var SqliteGraphStore = class {
17383
17383
  });
17384
17384
  return {
17385
17385
  id: r["id"],
17386
+ description: r["description"] ?? "",
17386
17387
  community: r["community"] ?? null,
17387
17388
  extractionConfidence: r["extraction_confidence"],
17388
17389
  attrs
@@ -17515,6 +17516,19 @@ var SqliteGraphStore = class {
17515
17516
  * read attrs and never the vector. `findNodesByLabel` decodes every embedding
17516
17517
  * blob; the liveness watch's coverage pass spent 3.4s of a 4.3s main-thread
17517
17518
  * hold doing exactly that on a 227k-node store (2026-09-24). */
17519
+ findNodeStampsByLabel(label, attrKey) {
17520
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attrKey)) throw new Error(`findNodeStampsByLabel: bad attr key ${attrKey}`);
17521
+ const rows = this.db.prepare(
17522
+ `SELECT id, created_at, created_at_seq, json_type(attrs_json, '$.${attrKey}') IS NOT NULL AS has
17523
+ FROM nodes WHERE label = ? AND valid_to IS NULL`
17524
+ ).all(label);
17525
+ return rows.map((r) => ({
17526
+ id: r.id,
17527
+ createdAt: r.created_at,
17528
+ ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
17529
+ has: r.has === 1
17530
+ }));
17531
+ }
17518
17532
  findNodeAttrsByLabel(label) {
17519
17533
  const rows = this.stmts.attrsByLabel.all(label);
17520
17534
  return rows.map((r) => ({
@@ -19599,7 +19613,7 @@ var DOC_COMMENT_KINDS = /* @__PURE__ */ new Set(["docstring", "explanation"]);
19599
19613
  var MAX_DOC_CHARS = 600;
19600
19614
  function buildDocIndex(store) {
19601
19615
  const byName = /* @__PURE__ */ new Map();
19602
- for (const c of store.findNodesByLabel("Comment")) {
19616
+ for (const c of store.findNodeFieldsByLabel("Comment", ["attachedTo", "kind", "text"])) {
19603
19617
  const name = c.attrs["attachedTo"];
19604
19618
  const kind = c.attrs["kind"];
19605
19619
  if (typeof name !== "string" || !name) continue;
@@ -19615,7 +19629,7 @@ function buildSymbolLexicon(store) {
19615
19629
  const docs = buildDocIndex(store);
19616
19630
  const candidates = [];
19617
19631
  for (const [label, kind] of SYMBOL_LABELS) {
19618
- for (const n of store.findNodesByLabel(label)) {
19632
+ for (const n of store.findNodeFieldsByLabel(label, ["qname"])) {
19619
19633
  const qname = String(n.attrs["qname"] ?? "");
19620
19634
  const doc = (n.description ? docs.get(n.description) : void 0) ?? (qname ? docs.get(qname) : void 0);
19621
19635
  for (const name of [n.description, qname]) {
@@ -19721,7 +19735,7 @@ function buildFileLexicon(store) {
19721
19735
  const basenameCount = /* @__PURE__ */ new Map();
19722
19736
  const stemRelPaths = /* @__PURE__ */ new Map();
19723
19737
  const entries = [];
19724
- for (const n of store.findNodesByLabel("File")) {
19738
+ for (const n of store.findNodeFieldsByLabel("File", ["relPath"])) {
19725
19739
  const relPath = typeof n.attrs["relPath"] === "string" && n.attrs["relPath"].length > 0 ? n.attrs["relPath"] : n.description;
19726
19740
  if (!relPath) continue;
19727
19741
  const basename = relPath.split("/").pop();
@@ -20043,9 +20057,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
20043
20057
  collapsedEdges: 0
20044
20058
  };
20045
20059
  const sharedCauses = /* @__PURE__ */ new Map();
20046
- const causalQueue = store.findNodesByLabel("Problem").filter(
20047
- (n) => n.attrs["source"] !== "cloud" && n.attrs["resolvedAs"] !== PROBLEM_RESOLUTION.FALSE_POSITIVE && !ignored(n.description)
20048
- ).map((n) => n.id);
20060
+ const causalQueue = store.findNodeFieldsByLabel("Problem", ["source", "resolvedAs"]).filter((r) => r.attrs["source"] !== "cloud" && r.attrs["resolvedAs"] !== PROBLEM_RESOLUTION.FALSE_POSITIVE).map((r) => r.id).filter((id) => ignorePatterns.length === 0 || !ignored(store.getNode(id)?.description ?? ""));
20049
20061
  const causalSeen = new Set(causalQueue);
20050
20062
  for (let i = 0; i < causalQueue.length; i++) {
20051
20063
  const id = causalQueue[i];
@@ -20061,7 +20073,20 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
20061
20073
  for (const label of INSTANCE_LABELS) {
20062
20074
  const ref = [];
20063
20075
  includedByLabel.set(label, ref);
20064
- const population = label === "RootCause" && opts.sharedStore ? [...store.findNodesByLabel(label), ...sharedCauses.values()] : store.findNodesByLabel(label);
20076
+ const local = [];
20077
+ for (const row of store.findNodeFieldsByLabel(label, ["source", "privateFinding"])) {
20078
+ if (row.attrs["source"] !== "cloud") {
20079
+ const n = store.getNode(row.id);
20080
+ if (n) local.push(n);
20081
+ continue;
20082
+ }
20083
+ if (seen.has(row.id) || walked.has(row.id)) continue;
20084
+ walked.add(row.id);
20085
+ composition.candidates++;
20086
+ if ((row.attrs["privateFinding"] || label === "Vulnerability") && !opts.project) composition.privateTargetMissing++;
20087
+ else composition.cloudSourced++;
20088
+ }
20089
+ const population = label === "RootCause" && opts.sharedStore ? [...local, ...sharedCauses.values()] : local;
20065
20090
  for (const n of population) {
20066
20091
  if (seen.has(n.id)) continue;
20067
20092
  if (walked.has(n.id)) continue;
package/errata.mjs CHANGED
@@ -20104,7 +20104,7 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
20104
20104
  if (!stmt) {
20105
20105
  const cols = attrKeys.map((k, i2) => `, json_extract(attrs_json, '$.${k}') AS a${i2}`).join("");
20106
20106
  stmt = this.db.prepare(
20107
- `SELECT id, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
20107
+ `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
20108
20108
  );
20109
20109
  this.fieldStmts.set(cacheKey2, stmt);
20110
20110
  }
@@ -20116,6 +20116,7 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
20116
20116
  });
20117
20117
  return {
20118
20118
  id: r["id"],
20119
+ description: r["description"] ?? "",
20119
20120
  community: r["community"] ?? null,
20120
20121
  extractionConfidence: r["extraction_confidence"],
20121
20122
  attrs
@@ -20248,6 +20249,19 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
20248
20249
  * read attrs and never the vector. `findNodesByLabel` decodes every embedding
20249
20250
  * blob; the liveness watch's coverage pass spent 3.4s of a 4.3s main-thread
20250
20251
  * hold doing exactly that on a 227k-node store (2026-09-24). */
20252
+ findNodeStampsByLabel(label, attrKey) {
20253
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attrKey)) throw new Error(`findNodeStampsByLabel: bad attr key ${attrKey}`);
20254
+ const rows = this.db.prepare(
20255
+ `SELECT id, created_at, created_at_seq, json_type(attrs_json, '$.${attrKey}') IS NOT NULL AS has
20256
+ FROM nodes WHERE label = ? AND valid_to IS NULL`
20257
+ ).all(label);
20258
+ return rows.map((r) => ({
20259
+ id: r.id,
20260
+ createdAt: r.created_at,
20261
+ ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
20262
+ has: r.has === 1
20263
+ }));
20264
+ }
20251
20265
  findNodeAttrsByLabel(label) {
20252
20266
  const rows = this.stmts.attrsByLabel.all(label);
20253
20267
  return rows.map((r) => ({
@@ -25290,8 +25304,8 @@ function evaluateFed(store, d) {
25290
25304
  if (!d.fed) return { count: 0, total: 0, fraction: 0 };
25291
25305
  const column = COLUMN_INPUTS[d.fed.attr];
25292
25306
  const fed = d.fed;
25293
- const has = (n) => column ? column(n) : n.attrs[fed.attr] !== void 0;
25294
- const nodes = fed.labels.flatMap((label) => store.findNodeAttrsByLabel(label));
25307
+ const nodes = fed.labels.flatMap((label) => store.findNodeStampsByLabel ? store.findNodeStampsByLabel(label, fed.attr) : store.findNodeAttrsByLabel(label));
25308
+ const has = (n) => column ? column(n) : n.has ?? n.attrs?.[fed.attr] !== void 0;
25295
25309
  let window2 = nodes;
25296
25310
  if (fed.recent !== void 0) {
25297
25311
  window2 = [...nodes].sort((a, b) => b.createdAt - a.createdAt).slice(0, fed.recent);
@@ -35151,7 +35165,7 @@ function isDistinctiveIdentifier(name2) {
35151
35165
  }
35152
35166
  function buildDocIndex(store) {
35153
35167
  const byName = /* @__PURE__ */ new Map();
35154
- for (const c of store.findNodesByLabel("Comment")) {
35168
+ for (const c of store.findNodeFieldsByLabel("Comment", ["attachedTo", "kind", "text"])) {
35155
35169
  const name2 = c.attrs["attachedTo"];
35156
35170
  const kind = c.attrs["kind"];
35157
35171
  if (typeof name2 !== "string" || !name2) continue;
@@ -35167,7 +35181,7 @@ function buildSymbolLexicon(store) {
35167
35181
  const docs = buildDocIndex(store);
35168
35182
  const candidates = [];
35169
35183
  for (const [label, kind] of SYMBOL_LABELS2) {
35170
- for (const n of store.findNodesByLabel(label)) {
35184
+ for (const n of store.findNodeFieldsByLabel(label, ["qname"])) {
35171
35185
  const qname = String(n.attrs["qname"] ?? "");
35172
35186
  const doc = (n.description ? docs.get(n.description) : void 0) ?? (qname ? docs.get(qname) : void 0);
35173
35187
  for (const name2 of [n.description, qname]) {
@@ -35222,7 +35236,7 @@ function buildFileLexicon(store) {
35222
35236
  const basenameCount = /* @__PURE__ */ new Map();
35223
35237
  const stemRelPaths = /* @__PURE__ */ new Map();
35224
35238
  const entries = [];
35225
- for (const n of store.findNodesByLabel("File")) {
35239
+ for (const n of store.findNodeFieldsByLabel("File", ["relPath"])) {
35226
35240
  const relPath = typeof n.attrs["relPath"] === "string" && n.attrs["relPath"].length > 0 ? n.attrs["relPath"] : n.description;
35227
35241
  if (!relPath) continue;
35228
35242
  const basename9 = relPath.split("/").pop();
@@ -84052,7 +84066,7 @@ function createWatchBreaker(deps) {
84052
84066
  }
84053
84067
 
84054
84068
  // src/engine.ts
84055
- var DAEMON_VERSION = true ? "2.0.21-dev.4009" : "2.0.0-alpha.0";
84069
+ var DAEMON_VERSION = true ? "2.0.21-dev.4014" : "2.0.0-alpha.0";
84056
84070
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
84057
84071
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
84058
84072
  var GIT_OP_MUTE_MS = 4e3;
@@ -87670,9 +87684,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
87670
87684
  collapsedEdges: 0
87671
87685
  };
87672
87686
  const sharedCauses = /* @__PURE__ */ new Map();
87673
- const causalQueue = store.findNodesByLabel("Problem").filter(
87674
- (n) => n.attrs["source"] !== "cloud" && n.attrs["resolvedAs"] !== PROBLEM_RESOLUTION.FALSE_POSITIVE && !ignored(n.description)
87675
- ).map((n) => n.id);
87687
+ const causalQueue = store.findNodeFieldsByLabel("Problem", ["source", "resolvedAs"]).filter((r) => r.attrs["source"] !== "cloud" && r.attrs["resolvedAs"] !== PROBLEM_RESOLUTION.FALSE_POSITIVE).map((r) => r.id).filter((id) => ignorePatterns.length === 0 || !ignored(store.getNode(id)?.description ?? ""));
87676
87688
  const causalSeen = new Set(causalQueue);
87677
87689
  for (let i2 = 0; i2 < causalQueue.length; i2++) {
87678
87690
  const id = causalQueue[i2];
@@ -87688,7 +87700,20 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
87688
87700
  for (const label of INSTANCE_LABELS) {
87689
87701
  const ref = [];
87690
87702
  includedByLabel.set(label, ref);
87691
- const population = label === "RootCause" && opts.sharedStore ? [...store.findNodesByLabel(label), ...sharedCauses.values()] : store.findNodesByLabel(label);
87703
+ const local = [];
87704
+ for (const row of store.findNodeFieldsByLabel(label, ["source", "privateFinding"])) {
87705
+ if (row.attrs["source"] !== "cloud") {
87706
+ const n = store.getNode(row.id);
87707
+ if (n) local.push(n);
87708
+ continue;
87709
+ }
87710
+ if (seen.has(row.id) || walked.has(row.id)) continue;
87711
+ walked.add(row.id);
87712
+ composition.candidates++;
87713
+ if ((row.attrs["privateFinding"] || label === "Vulnerability") && !opts.project) composition.privateTargetMissing++;
87714
+ else composition.cloudSourced++;
87715
+ }
87716
+ const population = label === "RootCause" && opts.sharedStore ? [...local, ...sharedCauses.values()] : local;
87692
87717
  for (const n of population) {
87693
87718
  if (seen.has(n.id)) continue;
87694
87719
  if (walked.has(n.id)) continue;
@@ -15779,7 +15779,7 @@ var SqliteGraphStore = class {
15779
15779
  if (!stmt) {
15780
15780
  const cols = attrKeys.map((k, i) => `, json_extract(attrs_json, '$.${k}') AS a${i}`).join("");
15781
15781
  stmt = this.db.prepare(
15782
- `SELECT id, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
15782
+ `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
15783
15783
  );
15784
15784
  this.fieldStmts.set(cacheKey, stmt);
15785
15785
  }
@@ -15791,6 +15791,7 @@ var SqliteGraphStore = class {
15791
15791
  });
15792
15792
  return {
15793
15793
  id: r["id"],
15794
+ description: r["description"] ?? "",
15794
15795
  community: r["community"] ?? null,
15795
15796
  extractionConfidence: r["extraction_confidence"],
15796
15797
  attrs
@@ -15923,6 +15924,19 @@ var SqliteGraphStore = class {
15923
15924
  * read attrs and never the vector. `findNodesByLabel` decodes every embedding
15924
15925
  * blob; the liveness watch's coverage pass spent 3.4s of a 4.3s main-thread
15925
15926
  * hold doing exactly that on a 227k-node store (2026-09-24). */
15927
+ findNodeStampsByLabel(label, attrKey) {
15928
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attrKey)) throw new Error(`findNodeStampsByLabel: bad attr key ${attrKey}`);
15929
+ const rows = this.db.prepare(
15930
+ `SELECT id, created_at, created_at_seq, json_type(attrs_json, '$.${attrKey}') IS NOT NULL AS has
15931
+ FROM nodes WHERE label = ? AND valid_to IS NULL`
15932
+ ).all(label);
15933
+ return rows.map((r) => ({
15934
+ id: r.id,
15935
+ createdAt: r.created_at,
15936
+ ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
15937
+ has: r.has === 1
15938
+ }));
15939
+ }
15926
15940
  findNodeAttrsByLabel(label) {
15927
15941
  const rows = this.stmts.attrsByLabel.all(label);
15928
15942
  return rows.map((r) => ({
@@ -17100,7 +17114,7 @@ var DOC_COMMENT_KINDS = /* @__PURE__ */ new Set(["docstring", "explanation"]);
17100
17114
  var MAX_DOC_CHARS = 600;
17101
17115
  function buildDocIndex(store) {
17102
17116
  const byName = /* @__PURE__ */ new Map();
17103
- for (const c of store.findNodesByLabel("Comment")) {
17117
+ for (const c of store.findNodeFieldsByLabel("Comment", ["attachedTo", "kind", "text"])) {
17104
17118
  const name = c.attrs["attachedTo"];
17105
17119
  const kind = c.attrs["kind"];
17106
17120
  if (typeof name !== "string" || !name) continue;
@@ -17116,7 +17130,7 @@ function buildSymbolLexicon(store) {
17116
17130
  const docs = buildDocIndex(store);
17117
17131
  const candidates = [];
17118
17132
  for (const [label, kind] of SYMBOL_LABELS) {
17119
- for (const n of store.findNodesByLabel(label)) {
17133
+ for (const n of store.findNodeFieldsByLabel(label, ["qname"])) {
17120
17134
  const qname = String(n.attrs["qname"] ?? "");
17121
17135
  const doc = (n.description ? docs.get(n.description) : void 0) ?? (qname ? docs.get(qname) : void 0);
17122
17136
  for (const name of [n.description, qname]) {
@@ -17222,7 +17236,7 @@ function buildFileLexicon(store) {
17222
17236
  const basenameCount = /* @__PURE__ */ new Map();
17223
17237
  const stemRelPaths = /* @__PURE__ */ new Map();
17224
17238
  const entries = [];
17225
- for (const n of store.findNodesByLabel("File")) {
17239
+ for (const n of store.findNodeFieldsByLabel("File", ["relPath"])) {
17226
17240
  const relPath = typeof n.attrs["relPath"] === "string" && n.attrs["relPath"].length > 0 ? n.attrs["relPath"] : n.description;
17227
17241
  if (!relPath) continue;
17228
17242
  const basename = relPath.split("/").pop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.21-dev.4009",
3
+ "version": "2.0.21-dev.4014",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -26637,7 +26637,7 @@ var SqliteGraphStore = class {
26637
26637
  if (!stmt) {
26638
26638
  const cols = attrKeys.map((k, i2) => `, json_extract(attrs_json, '$.${k}') AS a${i2}`).join("");
26639
26639
  stmt = this.db.prepare(
26640
- `SELECT id, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
26640
+ `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
26641
26641
  );
26642
26642
  this.fieldStmts.set(cacheKey, stmt);
26643
26643
  }
@@ -26649,6 +26649,7 @@ var SqliteGraphStore = class {
26649
26649
  });
26650
26650
  return {
26651
26651
  id: r["id"],
26652
+ description: r["description"] ?? "",
26652
26653
  community: r["community"] ?? null,
26653
26654
  extractionConfidence: r["extraction_confidence"],
26654
26655
  attrs
@@ -26781,6 +26782,19 @@ var SqliteGraphStore = class {
26781
26782
  * read attrs and never the vector. `findNodesByLabel` decodes every embedding
26782
26783
  * blob; the liveness watch's coverage pass spent 3.4s of a 4.3s main-thread
26783
26784
  * hold doing exactly that on a 227k-node store (2026-09-24). */
26785
+ findNodeStampsByLabel(label, attrKey) {
26786
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attrKey)) throw new Error(`findNodeStampsByLabel: bad attr key ${attrKey}`);
26787
+ const rows = this.db.prepare(
26788
+ `SELECT id, created_at, created_at_seq, json_type(attrs_json, '$.${attrKey}') IS NOT NULL AS has
26789
+ FROM nodes WHERE label = ? AND valid_to IS NULL`
26790
+ ).all(label);
26791
+ return rows.map((r) => ({
26792
+ id: r.id,
26793
+ createdAt: r.created_at,
26794
+ ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
26795
+ has: r.has === 1
26796
+ }));
26797
+ }
26784
26798
  findNodeAttrsByLabel(label) {
26785
26799
  const rows = this.stmts.attrsByLabel.all(label);
26786
26800
  return rows.map((r) => ({
@@ -28834,8 +28848,8 @@ function evaluateFed(store2, d) {
28834
28848
  if (!d.fed) return { count: 0, total: 0, fraction: 0 };
28835
28849
  const column = COLUMN_INPUTS[d.fed.attr];
28836
28850
  const fed = d.fed;
28837
- const has = (n) => column ? column(n) : n.attrs[fed.attr] !== void 0;
28838
- const nodes = fed.labels.flatMap((label) => store2.findNodeAttrsByLabel(label));
28851
+ const nodes = fed.labels.flatMap((label) => store2.findNodeStampsByLabel ? store2.findNodeStampsByLabel(label, fed.attr) : store2.findNodeAttrsByLabel(label));
28852
+ const has = (n) => column ? column(n) : n.has ?? n.attrs?.[fed.attr] !== void 0;
28839
28853
  let window2 = nodes;
28840
28854
  if (fed.recent !== void 0) {
28841
28855
  window2 = [...nodes].sort((a, b) => b.createdAt - a.createdAt).slice(0, fed.recent);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errata-minimap",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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"
@@ -386,6 +386,11 @@ function isTagMode(value: unknown): value is TagMode {
386
386
  return TAG_MODES.includes(value as TagMode)
387
387
  }
388
388
 
389
+ /** A book's hover group: its spine rows and its card light together. */
390
+ function scopeOf(book: { key: string }): string {
391
+ return `book:${book.key}`.slice(0, 64)
392
+ }
393
+
389
394
  function isPaneView(value: unknown): value is PaneView {
390
395
  return PANE_VIEWS.includes(value as PaneView)
391
396
  }
@@ -657,28 +662,35 @@ export const register: Register = on => {
657
662
  return (
658
663
  <Box key="library" position="relative" width={width} height={libraryRows}>
659
664
  <Raster key="map" columns={width} rows={libraryRows} cells={cells} />
665
+ {/* Every book's hit-area first… */}
666
+ {spots.flatMap(s => {
667
+ const scope = scopeOf(s.book)
668
+ const pin = () => {
669
+ motion.focus = s.book.problem?.id ?? s.book.memberIds[0]
670
+ $.ui.invalidate('ui.render')
671
+ }
672
+ return [...Array(s.height).keys()].map(r => (
673
+ <Box key={`${scope}:${r}`} position="absolute" top={s.top + r} left={s.x} hover={{ scope }}>
674
+ <Button key={`${scope}:press:${r}`} plain onPress={pin}>
675
+ {[...Array(s.width).keys()].map(dx => {
676
+ const c = cell(s.x + dx, s.top + r)
677
+ return <Text color={colourOf(c.fg)} backgroundColor={colourOf(c.bg)}>{c.ch}</Text>
678
+ })}
679
+ </Button>
680
+ </Box>
681
+ ))
682
+ })}
683
+ {/* …then every card. Positioned boxes paint in tree order, so a card emitted beside its own
684
+ book was painted over by every book after it. Last in the tree, a card is over all of them;
685
+ the background makes it opaque, or the books behind would show through its blank cells. */}
660
686
  {spots.map(s => {
661
- const scope = `book:${s.book.key}`.slice(0, 64)
687
+ const scope = scopeOf(s.book)
662
688
  const parts = (['cause', 'problem', 'fix'] as const).flatMap(r => (s.book[r] ? [s.book[r]!] : []))
663
689
  const cardHeight = parts.length + 3
664
690
  const cardLeft = s.x + s.width + 1 + cardWidth <= width ? s.x + s.width + 1 : Math.max(0, s.x - cardWidth - 1)
665
691
  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.
692
+ const paper = hex(palette.card)
693
+ return (
682
694
  <Box
683
695
  key={`${scope}:card`}
684
696
  position="absolute"
@@ -686,23 +698,27 @@ export const register: Register = on => {
686
698
  left={cardLeft}
687
699
  width={cardWidth}
688
700
  display="none"
689
- hover={{ scope, display: 'flex' }}
701
+ hover={{ scope, display: 'flex', backgroundColor: paper }}
690
702
  flexDirection="column"
691
703
  borderStyle="round"
692
704
  borderColor={hex(palette.gilt)}
693
705
  >
694
- <Text bold wrap="truncate-end" color={hex(palette.binding.problem)}>
695
- {callNumber(catalog, s.book)} <Text dimColor>{s.book.tech ?? 'general'}</Text>
706
+ <Text bold wrap="truncate-end" color={hex(palette.binding.problem)} backgroundColor={paper}>
707
+ {fitTitle(`${callNumber(catalog, s.book)} ${s.book.tech ?? 'general'}`, cardWidth - 2).padEnd(cardWidth - 2)}
696
708
  </Text>
697
709
  {parts.map(part => (
698
- <Text wrap="truncate-end">
710
+ <Text wrap="truncate-end" backgroundColor={paper}>
699
711
  <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>
712
+ <Text color={hex(part.isDelivered ? palette.cardInk : palette.cardFaint)}>
713
+ {fitTitle(part.name, cardWidth - 4).padEnd(cardWidth - 4)}
714
+ </Text>
701
715
  </Text>
702
716
  ))}
703
- <Text dimColor wrap="truncate-end">click to read it on the card</Text>
704
- </Box>,
705
- ]
717
+ <Text color={hex(palette.cardFaint)} backgroundColor={paper} wrap="truncate-end">
718
+ {'click to read it on the card'.padEnd(cardWidth - 2)}
719
+ </Text>
720
+ </Box>
721
+ )
706
722
  })}
707
723
  </Box>
708
724
  )
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errata-context",
3
- "version": "2.0.21-dev.4009",
3
+ "version": "2.0.21-dev.4014",
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
- ca5151fe29df5ddd759d37705b4d0e186e5eee10d56dc43ff6a2ca550fb2ff46 .codex-plugin/plugin.json
1
+ 3a8ec47210590c6000ed069594b1e6bd6a0c1bf3836374d51933cb7ba6c7b0a7 .codex-plugin/plugin.json
2
2
  a7bebae9fb01c838d6c75fc784129e28be571233fa66ff5ee8039b574f35ccf7 assets/icon.png
3
3
  fc38b64e4df1e0b72c35764bef6c4c05430d97c9009ad0ecc8a9e75b4deab232 configure-local.mjs
4
4
  73dc6028b3b070257c7c31f252d11e55a9e096f4aed0f187f10cb21716e1e6d7 dist/client.js