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

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.
@@ -15255,6 +15255,36 @@ var init_history_evidence = __esm({
15255
15255
  });
15256
15256
 
15257
15257
  // ../../packages/shared/src/history-dependencies.ts
15258
+ function historyDependencyIdentity(raw) {
15259
+ const compact = compactDependencyIdentity(raw);
15260
+ if (compact !== void 0) return compact;
15261
+ const parsed = HistoryDependencyContextSchema.safeParse(raw);
15262
+ if (!parsed.success || !parsed.data.complete || parsed.data.conflict) return null;
15263
+ if (parsed.data.dependencies.some((d) => !exactHistoryDependencyVersion(d))) return null;
15264
+ return digest(parsed.data.dependencies.map((d) => [d.packageId, d.manifestId, d.side, d.declaredRange, d.resolvedVersion]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))));
15265
+ }
15266
+ function exactHistoryDependencyVersion(d) {
15267
+ return d.resolvedVersion ?? (/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(d.declaredRange ?? "") ? d.declaredRange : null);
15268
+ }
15269
+ function compactDependencyIdentity(raw) {
15270
+ if (!raw || typeof raw !== "object" || raw.compact !== true) return void 0;
15271
+ const identity = raw.identity;
15272
+ return typeof identity === "string" ? identity : null;
15273
+ }
15274
+ function compactHistoryQuality(quality) {
15275
+ const out = { ...quality };
15276
+ const context = quality["dependencyContext"];
15277
+ if (context !== void 0 && compactDependencyIdentity(context) === void 0) {
15278
+ const compact = { schemaVersion: "history-deps-1", compact: true, identity: historyDependencyIdentity(context) };
15279
+ out["dependencyContext"] = compact;
15280
+ }
15281
+ const evidence = quality["reconciliationEvidence"];
15282
+ if (evidence && typeof evidence === "object" && !Array.isArray(evidence)) {
15283
+ const build = evidence["build"];
15284
+ out["reconciliationEvidence"] = build === void 0 ? {} : { build };
15285
+ }
15286
+ return out;
15287
+ }
15258
15288
  var version2, HistoryDependencySchema, HistoryDependencyContextSchema;
15259
15289
  var init_history_dependencies = __esm({
15260
15290
  "../../packages/shared/src/history-dependencies.ts"() {
@@ -16444,6 +16474,40 @@ function installCloudAttrCompaction(db) {
16444
16474
  });
16445
16475
  }
16446
16476
 
16477
+ // ../../packages/local-graph/src/history-attr-slim.ts
16478
+ init_src();
16479
+ var HISTORY_ATTR_SLIM_KEY = "history-attr-slim-1";
16480
+ var SLIM_BATCH = 500;
16481
+ function installHistoryAttrSlim(db) {
16482
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(HISTORY_ATTR_SLIM_KEY)) return;
16483
+ const select = db.prepare(`SELECT rowid AS rid, json_extract(attrs_json, '$.historyQuality') AS q FROM nodes
16484
+ WHERE rowid > ? AND (json_type(attrs_json, '$.historyQuality.dependencyContext.dependencies') IS NOT NULL
16485
+ OR json_type(attrs_json, '$.historyQuality.reconciliationEvidence') = 'object')
16486
+ ORDER BY rowid LIMIT ${SLIM_BATCH}`);
16487
+ const write = db.prepare(`UPDATE nodes SET attrs_json = json_set(attrs_json, '$.historyQuality', json(?)) WHERE rowid = ?`);
16488
+ let after = 0;
16489
+ for (; ; ) {
16490
+ const rows = select.all(after);
16491
+ if (rows.length === 0) break;
16492
+ db.transaction(() => {
16493
+ for (const { rid, q } of rows) {
16494
+ if (!q) continue;
16495
+ let quality;
16496
+ try {
16497
+ quality = JSON.parse(q);
16498
+ } catch {
16499
+ continue;
16500
+ }
16501
+ if (!quality || typeof quality !== "object" || Array.isArray(quality)) continue;
16502
+ const slim = JSON.stringify(compactHistoryQuality(quality));
16503
+ if (slim !== q) write.run(slim, rid);
16504
+ }
16505
+ });
16506
+ after = rows[rows.length - 1].rid;
16507
+ }
16508
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
16509
+ }
16510
+
16447
16511
  // ../../packages/math/src/constants.ts
16448
16512
  var NEMORI_DEDUP_COSINE = 0.88;
16449
16513
 
@@ -16918,6 +16982,7 @@ var SqliteGraphStore = class {
16918
16982
  installHistoryCacheFloor(this.db);
16919
16983
  installEditPlaceholderRetirement(this.db);
16920
16984
  installCloudAttrCompaction(this.db);
16985
+ installHistoryAttrSlim(this.db);
16921
16986
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
16922
16987
  this.db.transaction(() => {
16923
16988
  this.repairSessionAliases();
package/errata.mjs CHANGED
@@ -15874,6 +15874,8 @@ function selectHistoryDependencies(context, input) {
15874
15874
  return { schemaVersion: "history-deps-1", complete: context.complete, dependencies, statedVersions, conflict };
15875
15875
  }
15876
15876
  function historyDependencyIdentity(raw2) {
15877
+ const compact = compactDependencyIdentity(raw2);
15878
+ if (compact !== void 0) return compact;
15877
15879
  const parsed = HistoryDependencyContextSchema.safeParse(raw2);
15878
15880
  if (!parsed.success || !parsed.data.complete || parsed.data.conflict) return null;
15879
15881
  if (parsed.data.dependencies.some((d) => !exactHistoryDependencyVersion(d))) return null;
@@ -15971,6 +15973,25 @@ function resolveHistoricalNpmVersion(name2, manifest, declared, locks) {
15971
15973
  }
15972
15974
  return null;
15973
15975
  }
15976
+ function compactDependencyIdentity(raw2) {
15977
+ if (!raw2 || typeof raw2 !== "object" || raw2.compact !== true) return void 0;
15978
+ const identity = raw2.identity;
15979
+ return typeof identity === "string" ? identity : null;
15980
+ }
15981
+ function compactHistoryQuality(quality) {
15982
+ const out2 = { ...quality };
15983
+ const context = quality["dependencyContext"];
15984
+ if (context !== void 0 && compactDependencyIdentity(context) === void 0) {
15985
+ const compact = { schemaVersion: "history-deps-1", compact: true, identity: historyDependencyIdentity(context) };
15986
+ out2["dependencyContext"] = compact;
15987
+ }
15988
+ const evidence = quality["reconciliationEvidence"];
15989
+ if (evidence && typeof evidence === "object" && !Array.isArray(evidence)) {
15990
+ const build = evidence["build"];
15991
+ out2["reconciliationEvidence"] = build === void 0 ? {} : { build };
15992
+ }
15993
+ return out2;
15994
+ }
15974
15995
  var version2, HistoryDependencySchema, HistoryDependencyContextSchema, HISTORY_RECONCILE_BUILD, historyManifestId, pnpmIndexes, historyDependencyPackageId;
15975
15996
  var init_history_dependencies = __esm({
15976
15997
  "../../packages/shared/src/history-dependencies.ts"() {
@@ -17307,6 +17328,10 @@ function unpackHistoryAttrs(attrs = {}) {
17307
17328
  function localCloudAttrs(attrs = {}) {
17308
17329
  const out2 = unpackHistoryAttrs(attrs);
17309
17330
  for (const key of CLOUD_ATTR_DUPLICATE_KEYS) delete out2[key];
17331
+ const quality = out2["historyQuality"];
17332
+ if (quality && typeof quality === "object" && !Array.isArray(quality)) {
17333
+ out2["historyQuality"] = compactHistoryQuality(quality);
17334
+ }
17310
17335
  return out2;
17311
17336
  }
17312
17337
  function historyReadPolicy(node2, options = {}) {
@@ -17626,6 +17651,7 @@ __export(src_exports, {
17626
17651
  canonicalizeToken: () => canonicalizeToken,
17627
17652
  checkHistoryCandidate: () => checkHistoryCandidate,
17628
17653
  claimStructure: () => claimStructure,
17654
+ compactHistoryQuality: () => compactHistoryQuality,
17629
17655
  conceptBag: () => conceptBag,
17630
17656
  conceptTokens: () => conceptTokens,
17631
17657
  depLanguage: () => depLanguage,
@@ -18417,6 +18443,46 @@ var init_cloud_attr_compaction = __esm({
18417
18443
  }
18418
18444
  });
18419
18445
 
18446
+ // ../../packages/local-graph/src/history-attr-slim.ts
18447
+ function installHistoryAttrSlim(db) {
18448
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(HISTORY_ATTR_SLIM_KEY)) return;
18449
+ const select = db.prepare(`SELECT rowid AS rid, json_extract(attrs_json, '$.historyQuality') AS q FROM nodes
18450
+ WHERE rowid > ? AND (json_type(attrs_json, '$.historyQuality.dependencyContext.dependencies') IS NOT NULL
18451
+ OR json_type(attrs_json, '$.historyQuality.reconciliationEvidence') = 'object')
18452
+ ORDER BY rowid LIMIT ${SLIM_BATCH}`);
18453
+ const write2 = db.prepare(`UPDATE nodes SET attrs_json = json_set(attrs_json, '$.historyQuality', json(?)) WHERE rowid = ?`);
18454
+ let after = 0;
18455
+ for (; ; ) {
18456
+ const rows = select.all(after);
18457
+ if (rows.length === 0) break;
18458
+ db.transaction(() => {
18459
+ for (const { rid, q } of rows) {
18460
+ if (!q) continue;
18461
+ let quality;
18462
+ try {
18463
+ quality = JSON.parse(q);
18464
+ } catch {
18465
+ continue;
18466
+ }
18467
+ if (!quality || typeof quality !== "object" || Array.isArray(quality)) continue;
18468
+ const slim = JSON.stringify(compactHistoryQuality(quality));
18469
+ if (slim !== q) write2.run(slim, rid);
18470
+ }
18471
+ });
18472
+ after = rows[rows.length - 1].rid;
18473
+ }
18474
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
18475
+ }
18476
+ var HISTORY_ATTR_SLIM_KEY, SLIM_BATCH;
18477
+ var init_history_attr_slim = __esm({
18478
+ "../../packages/local-graph/src/history-attr-slim.ts"() {
18479
+ "use strict";
18480
+ init_src();
18481
+ HISTORY_ATTR_SLIM_KEY = "history-attr-slim-1";
18482
+ SLIM_BATCH = 500;
18483
+ }
18484
+ });
18485
+
18420
18486
  // ../../packages/math/src/constants.ts
18421
18487
  var SURPRISE_MOMENTUM, SURPRISE_LEARNING_RATE, SURPRISE_LEARNING_RATE_SCALING, TIER_HALFLIFE_MULTIPLIER, HIT_RATE_SCALING_ALPHA, BASE_HALF_LIFE_SEQ, MOMENTUM_GATE_FLOOR, SURPRISE_RECENCY_LAMBDA_SEQ, NEMORI_SOFT_WRITE_MIN_COSINE, NEMORI_SOFT_WRITE_MAX_COSINE, NEMORI_DEDUP_COSINE, NEMORI_SOFT_ALPHA_MIN, NEMORI_SOFT_ALPHA_MAX, NEMORI_HIGH_SURPRISE_THRESHOLD, NEMORI_PREDICTION_FLOOR, REVIEW_SURPRISE_THRESHOLD, REVIEW_HITS_THRESHOLD;
18422
18488
  var init_constants = __esm({
@@ -19360,7 +19426,7 @@ function graphStoreNeedsPreparation(path2) {
19360
19426
  for (const match2 of (SCHEMA_SQL + TEMPORAL_INDEX_SQL).matchAll(/CREATE INDEX IF NOT EXISTS (\w+)/g)) {
19361
19427
  if (!indexes.has(match2[1])) return true;
19362
19428
  }
19363
- return [SESSION_REPAIR_KEY, HISTORY_CACHE_FLOOR_KEY, EDIT_PLACEHOLDER_FLOOR_KEY, CLOUD_ATTR_COMPACTION_KEY].some((key) => !db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key));
19429
+ return [SESSION_REPAIR_KEY, HISTORY_CACHE_FLOOR_KEY, EDIT_PLACEHOLDER_FLOOR_KEY, CLOUD_ATTR_COMPACTION_KEY, HISTORY_ATTR_SLIM_KEY].some((key) => !db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key));
19364
19430
  } catch {
19365
19431
  return true;
19366
19432
  } finally {
@@ -19437,6 +19503,7 @@ var init_store = __esm({
19437
19503
  init_session_evidence();
19438
19504
  init_edit_placeholder_retirement();
19439
19505
  init_cloud_attr_compaction();
19506
+ init_history_attr_slim();
19440
19507
  init_src3();
19441
19508
  PAGE_RANK_SCALE_KEY = "pageRankScale.p95.v1";
19442
19509
  TEXT_SEARCH_COLUMNS = `id, label, description, page_rank,
@@ -19648,6 +19715,7 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
19648
19715
  installHistoryCacheFloor(this.db);
19649
19716
  installEditPlaceholderRetirement(this.db);
19650
19717
  installCloudAttrCompaction(this.db);
19718
+ installHistoryAttrSlim(this.db);
19651
19719
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
19652
19720
  this.db.transaction(() => {
19653
19721
  this.repairSessionAliases();
@@ -21069,8 +21137,10 @@ function conditionOf(node2) {
21069
21137
  function clearAnsweredRevisits(store, ts) {
21070
21138
  const report = { cleared: 0, standing: 0 };
21071
21139
  for (const label of SEMANTIC_NODE_LABELS) {
21072
- for (const node2 of store.findNodesByLabel(label)) {
21073
- if (node2.attrs["revisit"] !== true) continue;
21140
+ for (const row of store.findNodeFieldsByLabel(label, ["revisit"])) {
21141
+ if (row.attrs["revisit"] !== 1 && row.attrs["revisit"] !== true) continue;
21142
+ const node2 = store.getNode(row.id);
21143
+ if (!node2 || node2.attrs["revisit"] !== true) continue;
21074
21144
  const condition = conditionOf(node2);
21075
21145
  if (condition === void 0 || !isRevisitAnswered(store, node2, condition)) {
21076
21146
  report.standing++;
@@ -22787,7 +22857,7 @@ function priorsForFile(store, relPath) {
22787
22857
  }
22788
22858
  }
22789
22859
  if (!file2) return null;
22790
- const openProblems2 = [];
22860
+ const openProblems = [];
22791
22861
  const constraints = [];
22792
22862
  const resolvedProblems = [];
22793
22863
  const seenProblem = /* @__PURE__ */ new Set();
@@ -22803,7 +22873,7 @@ function priorsForFile(store, relPath) {
22803
22873
  if (seenProblem.has(n.id)) continue;
22804
22874
  seenProblem.add(n.id);
22805
22875
  if (!n.attrs["resolvedAt"]) {
22806
- (isConstraintProblem(n) ? constraints : openProblems2).push(n);
22876
+ (isConstraintProblem(n) ? constraints : openProblems).push(n);
22807
22877
  } else if (n.attrs["resolvedAs"] == null && n.attrs["mergedInto"] === void 0 && e.attrs?.["anchorProvenance"] !== "legacy") {
22808
22878
  resolvedProblems.push(n);
22809
22879
  }
@@ -22812,7 +22882,7 @@ function priorsForFile(store, relPath) {
22812
22882
  }
22813
22883
  }
22814
22884
  const solutionsByProblem = /* @__PURE__ */ new Map();
22815
- for (const p of [...openProblems2, ...constraints, ...resolvedProblems]) {
22885
+ for (const p of [...openProblems, ...constraints, ...resolvedProblems]) {
22816
22886
  for (const e of store.outEdges(p.id, ["SOLVED_BY", "CAUSED_BY", "INSTANCE_OF"])) {
22817
22887
  const n = store.getNode(e.to);
22818
22888
  if (n && CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) related.set(n.id, n);
@@ -22823,17 +22893,17 @@ function priorsForFile(store, relPath) {
22823
22893
  }
22824
22894
  }
22825
22895
  }
22826
- if (openProblems2.length === 0 && constraints.length === 0 && resolvedProblems.length === 0 && related.size === 0)
22896
+ if (openProblems.length === 0 && constraints.length === 0 && resolvedProblems.length === 0 && related.size === 0)
22827
22897
  return null;
22828
22898
  const recentFirst = (a, b) => (b.lastUpdatedAt ?? 0) - (a.lastUpdatedAt ?? 0);
22829
- openProblems2.sort(recentFirst);
22899
+ openProblems.sort(recentFirst);
22830
22900
  constraints.sort(recentFirst);
22831
22901
  resolvedProblems.sort(
22832
22902
  (a, b) => Number(b.attrs["resolvedAt"] ?? 0) - Number(a.attrs["resolvedAt"] ?? 0)
22833
22903
  );
22834
22904
  return {
22835
22905
  file: file2,
22836
- openProblems: openProblems2,
22906
+ openProblems,
22837
22907
  constraints,
22838
22908
  resolvedProblems,
22839
22909
  related: [...related.values()],
@@ -25573,7 +25643,7 @@ function wmCalibrationCells(store) {
25573
25643
  reinforcedNodes: 0
25574
25644
  };
25575
25645
  for (const label of WM_CALIBRATION_LABELS) {
25576
- for (const n of store.findNodesByLabel(label)) {
25646
+ for (const n of store.findNodeFieldsByLabel(label, WM_CALIBRATION_KEYS)) {
25577
25647
  const shown = num(n.attrs["shownCount"]);
25578
25648
  const evicted = num(n.attrs["evictedCount"]);
25579
25649
  if (shown > 0) {
@@ -25595,7 +25665,7 @@ function wmCalibrationCells(store) {
25595
25665
  }
25596
25666
  return cells;
25597
25667
  }
25598
- var WM_CALIBRATION_LABELS, num;
25668
+ var WM_CALIBRATION_LABELS, num, WM_CALIBRATION_KEYS;
25599
25669
  var init_wm_calibration = __esm({
25600
25670
  "../../packages/local-graph/src/wm-calibration.ts"() {
25601
25671
  "use strict";
@@ -25608,6 +25678,13 @@ var init_wm_calibration = __esm({
25608
25678
  "AntiPattern"
25609
25679
  ];
25610
25680
  num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
25681
+ WM_CALIBRATION_KEYS = [
25682
+ "shownCount",
25683
+ "evictedCount",
25684
+ "reinforceShownCount",
25685
+ "reinforceEvictedCount",
25686
+ "reinforceUnshownCount"
25687
+ ];
25611
25688
  }
25612
25689
  });
25613
25690
 
@@ -31410,8 +31487,8 @@ async function ingestErrorSignature(opts, sig, commandSig) {
31410
31487
  const candidateEmbedding = await embedTextWithModelOrHash(
31411
31488
  candidateDescription
31412
31489
  );
31413
- const openProblems2 = opts.store.findNodesByLabel("Problem");
31414
- const openMatch = openProblems2.find(
31490
+ const openProblems = opts.store.findNodesByLabel("Problem");
31491
+ const openMatch = openProblems.find(
31415
31492
  (p) => p.attrs["errorSignatureDigest"] === sig.digest
31416
31493
  );
31417
31494
  const top = rankOpenProblemNeighbors(opts.store, candidateEmbedding, 5);
@@ -33526,15 +33603,17 @@ function readContextCard(root, hash2) {
33526
33603
  return null;
33527
33604
  }
33528
33605
  }
33529
- function openProblems(store, now = Date.now()) {
33606
+ function openProblemCount(store, now = Date.now()) {
33530
33607
  const hit = openProblemsCache.get(store);
33531
- if (hit && now - hit.at < OPEN_PROBLEMS_TTL_MS) return hit.nodes;
33532
- const nodes = [...store.findNodesByLabel("Problem"), ...store.findNodesByLabel("Vulnerability")].filter((p) => live(p) && !p.attrs["resolvedAt"] && p.attrs["mergedInto"] === void 0 && !isConstraintProblem(p));
33533
- openProblemsCache.set(store, { at: now, nodes });
33534
- return nodes;
33535
- }
33536
- function openProblemCount(store) {
33537
- return openProblems(store).length;
33608
+ if (hit && now - hit.at < OPEN_PROBLEMS_TTL_MS) return hit.count;
33609
+ let count = 0;
33610
+ for (const label of ["Problem", "Vulnerability"]) {
33611
+ for (const p of store.findNodesByLabel(label)) {
33612
+ if (live(p) && !p.attrs["resolvedAt"] && p.attrs["mergedInto"] === void 0 && !isConstraintProblem(p)) count++;
33613
+ }
33614
+ }
33615
+ openProblemsCache.set(store, { at: now, count });
33616
+ return count;
33538
33617
  }
33539
33618
  var CONTEXT_CARD_FILE, OPEN_PROBLEMS_TTL_MS, openProblemsCache;
33540
33619
  var init_recall_signal = __esm({
@@ -83973,7 +84052,7 @@ function createWatchBreaker(deps) {
83973
84052
  }
83974
84053
 
83975
84054
  // src/engine.ts
83976
- var DAEMON_VERSION = true ? "2.0.21-dev.4001" : "2.0.0-alpha.0";
84055
+ var DAEMON_VERSION = true ? "2.0.21-dev.4009" : "2.0.0-alpha.0";
83977
84056
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
83978
84057
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
83979
84058
  var GIT_OP_MUTE_MS = 4e3;
@@ -89143,12 +89222,12 @@ async function syncHistoryRevisionPage(opts) {
89143
89222
  store.mergeNode({ ...node2, attrs: {
89144
89223
  ...node2.attrs,
89145
89224
  depVersions: dependencies.success ? historyDependencyVersions(dependencies.data) : null,
89146
- historyQuality: {
89225
+ historyQuality: compactHistoryQuality({
89147
89226
  ...occurrence.quality,
89148
89227
  evaluatedAtCommit: atCommit,
89149
89228
  applicability: receipt.verdict,
89150
89229
  reconciliationEvidence: receipt.evidence
89151
- }
89230
+ })
89152
89231
  } });
89153
89232
  changed++;
89154
89233
  }
@@ -356,6 +356,21 @@ function sha256(input) {
356
356
  function sha256Short(input, n = 8) {
357
357
  return sha256(input).slice(0, n);
358
358
  }
359
+ function canonicalJSON(value) {
360
+ if (value === null || typeof value !== "object") {
361
+ return JSON.stringify(value);
362
+ }
363
+ if (Array.isArray(value)) {
364
+ return "[" + value.map(canonicalJSON).join(",") + "]";
365
+ }
366
+ const keys = Object.keys(value).sort();
367
+ return "{" + keys.map(
368
+ (k) => JSON.stringify(k) + ":" + canonicalJSON(value[k])
369
+ ).join(",") + "}";
370
+ }
371
+ function digest(value) {
372
+ return sha256(canonicalJSON(value));
373
+ }
359
374
 
360
375
  // ../../packages/shared/src/nlp/taxonomy.ts
361
376
  var REGISTRY = [
@@ -14307,6 +14322,36 @@ var HistoryDependencyContextSchema = external_exports.object({
14307
14322
  statedVersions: external_exports.array(external_exports.object({ packageId: external_exports.string().max(300), version: external_exports.string().max(80) })).max(64).optional(),
14308
14323
  conflict: external_exports.boolean().optional()
14309
14324
  });
14325
+ function historyDependencyIdentity(raw) {
14326
+ const compact = compactDependencyIdentity(raw);
14327
+ if (compact !== void 0) return compact;
14328
+ const parsed = HistoryDependencyContextSchema.safeParse(raw);
14329
+ if (!parsed.success || !parsed.data.complete || parsed.data.conflict) return null;
14330
+ if (parsed.data.dependencies.some((d) => !exactHistoryDependencyVersion(d))) return null;
14331
+ return digest(parsed.data.dependencies.map((d) => [d.packageId, d.manifestId, d.side, d.declaredRange, d.resolvedVersion]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))));
14332
+ }
14333
+ function exactHistoryDependencyVersion(d) {
14334
+ return d.resolvedVersion ?? (/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(d.declaredRange ?? "") ? d.declaredRange : null);
14335
+ }
14336
+ function compactDependencyIdentity(raw) {
14337
+ if (!raw || typeof raw !== "object" || raw.compact !== true) return void 0;
14338
+ const identity = raw.identity;
14339
+ return typeof identity === "string" ? identity : null;
14340
+ }
14341
+ function compactHistoryQuality(quality) {
14342
+ const out = { ...quality };
14343
+ const context = quality["dependencyContext"];
14344
+ if (context !== void 0 && compactDependencyIdentity(context) === void 0) {
14345
+ const compact = { schemaVersion: "history-deps-1", compact: true, identity: historyDependencyIdentity(context) };
14346
+ out["dependencyContext"] = compact;
14347
+ }
14348
+ const evidence = quality["reconciliationEvidence"];
14349
+ if (evidence && typeof evidence === "object" && !Array.isArray(evidence)) {
14350
+ const build = evidence["build"];
14351
+ out["reconciliationEvidence"] = build === void 0 ? {} : { build };
14352
+ }
14353
+ return out;
14354
+ }
14310
14355
 
14311
14356
  // ../../packages/shared/src/edge-rules.ts
14312
14357
  var EDGE_RULES = {
@@ -15015,6 +15060,39 @@ function installCloudAttrCompaction(db) {
15015
15060
  });
15016
15061
  }
15017
15062
 
15063
+ // ../../packages/local-graph/src/history-attr-slim.ts
15064
+ var HISTORY_ATTR_SLIM_KEY = "history-attr-slim-1";
15065
+ var SLIM_BATCH = 500;
15066
+ function installHistoryAttrSlim(db) {
15067
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(HISTORY_ATTR_SLIM_KEY)) return;
15068
+ const select = db.prepare(`SELECT rowid AS rid, json_extract(attrs_json, '$.historyQuality') AS q FROM nodes
15069
+ WHERE rowid > ? AND (json_type(attrs_json, '$.historyQuality.dependencyContext.dependencies') IS NOT NULL
15070
+ OR json_type(attrs_json, '$.historyQuality.reconciliationEvidence') = 'object')
15071
+ ORDER BY rowid LIMIT ${SLIM_BATCH}`);
15072
+ const write = db.prepare(`UPDATE nodes SET attrs_json = json_set(attrs_json, '$.historyQuality', json(?)) WHERE rowid = ?`);
15073
+ let after = 0;
15074
+ for (; ; ) {
15075
+ const rows = select.all(after);
15076
+ if (rows.length === 0) break;
15077
+ db.transaction(() => {
15078
+ for (const { rid, q } of rows) {
15079
+ if (!q) continue;
15080
+ let quality;
15081
+ try {
15082
+ quality = JSON.parse(q);
15083
+ } catch {
15084
+ continue;
15085
+ }
15086
+ if (!quality || typeof quality !== "object" || Array.isArray(quality)) continue;
15087
+ const slim = JSON.stringify(compactHistoryQuality(quality));
15088
+ if (slim !== q) write.run(slim, rid);
15089
+ }
15090
+ });
15091
+ after = rows[rows.length - 1].rid;
15092
+ }
15093
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
15094
+ }
15095
+
15018
15096
  // ../../packages/math/src/relevance.ts
15019
15097
  function pageRankScale(ranks) {
15020
15098
  const positive = [...ranks].filter((r) => Number.isFinite(r) && r > 0).sort((a, b) => a - b);
@@ -15312,6 +15390,7 @@ var SqliteGraphStore = class {
15312
15390
  installHistoryCacheFloor(this.db);
15313
15391
  installEditPlaceholderRetirement(this.db);
15314
15392
  installCloudAttrCompaction(this.db);
15393
+ installHistoryAttrSlim(this.db);
15315
15394
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
15316
15395
  this.db.transaction(() => {
15317
15396
  this.repairSessionAliases();
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.4009",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -15101,6 +15101,8 @@ var init_history_evidence = __esm({
15101
15101
 
15102
15102
  // ../../packages/shared/src/history-dependencies.ts
15103
15103
  function historyDependencyIdentity(raw) {
15104
+ const compact = compactDependencyIdentity(raw);
15105
+ if (compact !== void 0) return compact;
15104
15106
  const parsed = HistoryDependencyContextSchema.safeParse(raw);
15105
15107
  if (!parsed.success || !parsed.data.complete || parsed.data.conflict) return null;
15106
15108
  if (parsed.data.dependencies.some((d) => !exactHistoryDependencyVersion(d))) return null;
@@ -15109,6 +15111,25 @@ function historyDependencyIdentity(raw) {
15109
15111
  function exactHistoryDependencyVersion(d) {
15110
15112
  return d.resolvedVersion ?? (/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(d.declaredRange ?? "") ? d.declaredRange : null);
15111
15113
  }
15114
+ function compactDependencyIdentity(raw) {
15115
+ if (!raw || typeof raw !== "object" || raw.compact !== true) return void 0;
15116
+ const identity = raw.identity;
15117
+ return typeof identity === "string" ? identity : null;
15118
+ }
15119
+ function compactHistoryQuality(quality) {
15120
+ const out2 = { ...quality };
15121
+ const context = quality["dependencyContext"];
15122
+ if (context !== void 0 && compactDependencyIdentity(context) === void 0) {
15123
+ const compact = { schemaVersion: "history-deps-1", compact: true, identity: historyDependencyIdentity(context) };
15124
+ out2["dependencyContext"] = compact;
15125
+ }
15126
+ const evidence = quality["reconciliationEvidence"];
15127
+ if (evidence && typeof evidence === "object" && !Array.isArray(evidence)) {
15128
+ const build = evidence["build"];
15129
+ out2["reconciliationEvidence"] = build === void 0 ? {} : { build };
15130
+ }
15131
+ return out2;
15132
+ }
15112
15133
  var version2, HistoryDependencySchema, HistoryDependencyContextSchema, HISTORY_RECONCILE_BUILD;
15113
15134
  var init_history_dependencies = __esm({
15114
15135
  "../../packages/shared/src/history-dependencies.ts"() {
@@ -25460,6 +25481,40 @@ function installCloudAttrCompaction(db) {
25460
25481
  });
25461
25482
  }
25462
25483
 
25484
+ // ../../packages/local-graph/src/history-attr-slim.ts
25485
+ init_src();
25486
+ var HISTORY_ATTR_SLIM_KEY = "history-attr-slim-1";
25487
+ var SLIM_BATCH = 500;
25488
+ function installHistoryAttrSlim(db) {
25489
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(HISTORY_ATTR_SLIM_KEY)) return;
25490
+ const select = db.prepare(`SELECT rowid AS rid, json_extract(attrs_json, '$.historyQuality') AS q FROM nodes
25491
+ WHERE rowid > ? AND (json_type(attrs_json, '$.historyQuality.dependencyContext.dependencies') IS NOT NULL
25492
+ OR json_type(attrs_json, '$.historyQuality.reconciliationEvidence') = 'object')
25493
+ ORDER BY rowid LIMIT ${SLIM_BATCH}`);
25494
+ const write = db.prepare(`UPDATE nodes SET attrs_json = json_set(attrs_json, '$.historyQuality', json(?)) WHERE rowid = ?`);
25495
+ let after = 0;
25496
+ for (; ; ) {
25497
+ const rows = select.all(after);
25498
+ if (rows.length === 0) break;
25499
+ db.transaction(() => {
25500
+ for (const { rid, q } of rows) {
25501
+ if (!q) continue;
25502
+ let quality;
25503
+ try {
25504
+ quality = JSON.parse(q);
25505
+ } catch {
25506
+ continue;
25507
+ }
25508
+ if (!quality || typeof quality !== "object" || Array.isArray(quality)) continue;
25509
+ const slim = JSON.stringify(compactHistoryQuality(quality));
25510
+ if (slim !== q) write.run(slim, rid);
25511
+ }
25512
+ });
25513
+ after = rows[rows.length - 1].rid;
25514
+ }
25515
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
25516
+ }
25517
+
25463
25518
  // ../../packages/math/src/pagerank.ts
25464
25519
  function pagerank(input) {
25465
25520
  const damping = input.damping ?? 0.85;
@@ -26193,6 +26248,7 @@ var SqliteGraphStore = class {
26193
26248
  installHistoryCacheFloor(this.db);
26194
26249
  installEditPlaceholderRetirement(this.db);
26195
26250
  installCloudAttrCompaction(this.db);
26251
+ installHistoryAttrSlim(this.db);
26196
26252
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
26197
26253
  this.db.transaction(() => {
26198
26254
  this.repairSessionAliases();
@@ -27321,8 +27377,10 @@ function conditionOf(node) {
27321
27377
  function clearAnsweredRevisits(store2, ts) {
27322
27378
  const report = { cleared: 0, standing: 0 };
27323
27379
  for (const label of SEMANTIC_NODE_LABELS) {
27324
- for (const node of store2.findNodesByLabel(label)) {
27325
- if (node.attrs["revisit"] !== true) continue;
27380
+ for (const row of store2.findNodeFieldsByLabel(label, ["revisit"])) {
27381
+ if (row.attrs["revisit"] !== 1 && row.attrs["revisit"] !== true) continue;
27382
+ const node = store2.getNode(row.id);
27383
+ if (!node || node.attrs["revisit"] !== true) continue;
27326
27384
  const condition = conditionOf(node);
27327
27385
  if (condition === void 0 || !isRevisitAnswered(store2, node, condition)) {
27328
27386
  report.standing++;
@@ -28840,6 +28898,13 @@ var WM_CALIBRATION_LABELS = [
28840
28898
  "AntiPattern"
28841
28899
  ];
28842
28900
  var num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
28901
+ var WM_CALIBRATION_KEYS = [
28902
+ "shownCount",
28903
+ "evictedCount",
28904
+ "reinforceShownCount",
28905
+ "reinforceEvictedCount",
28906
+ "reinforceUnshownCount"
28907
+ ];
28843
28908
  function wmCalibrationCells(store2) {
28844
28909
  const cells = {
28845
28910
  shownNodes: 0,
@@ -28852,7 +28917,7 @@ function wmCalibrationCells(store2) {
28852
28917
  reinforcedNodes: 0
28853
28918
  };
28854
28919
  for (const label of WM_CALIBRATION_LABELS) {
28855
- for (const n of store2.findNodesByLabel(label)) {
28920
+ for (const n of store2.findNodeFieldsByLabel(label, WM_CALIBRATION_KEYS)) {
28856
28921
  const shown = num(n.attrs["shownCount"]);
28857
28922
  const evicted = num(n.attrs["evictedCount"]);
28858
28923
  if (shown > 0) {
@@ -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.4009",
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
+ ca5151fe29df5ddd759d37705b4d0e186e5eee10d56dc43ff6a2ca550fb2ff46 .codex-plugin/plugin.json
2
2
  a7bebae9fb01c838d6c75fc784129e28be571233fa66ff5ee8039b574f35ccf7 assets/icon.png
3
3
  fc38b64e4df1e0b72c35764bef6c4c05430d97c9009ad0ecc8a9e75b4deab232 configure-local.mjs
4
4
  73dc6028b3b070257c7c31f252d11e55a9e096f4aed0f187f10cb21716e1e6d7 dist/client.js