@inerrata-corporation/errata 2.0.21-dev.3996 → 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.
@@ -16969,6 +16969,12 @@ var SqliteGraphStore = class {
16969
16969
  setPageRank: this.db.prepare("UPDATE nodes SET page_rank = :v WHERE id = :id"),
16970
16970
  setLandmark: this.db.prepare("UPDATE nodes SET is_landmark = :v WHERE id = :id"),
16971
16971
  setCommunity: this.db.prepare("UPDATE nodes SET community = :v WHERE id = :id"),
16972
+ setLocalCommunity: this.db.prepare(`
16973
+ UPDATE nodes SET
16974
+ attrs_json = CASE WHEN :v IS NULL THEN json_remove(attrs_json, '$.localCommunity')
16975
+ ELSE json_set(attrs_json, '$.localCommunity', :v) END,
16976
+ community = CASE WHEN :col = 1 THEN :v ELSE community END
16977
+ WHERE id = :id`),
16972
16978
  updateNode: this.db.prepare(`
16973
16979
  UPDATE nodes SET
16974
16980
  description = COALESCE(:description, description),
@@ -17289,6 +17295,35 @@ var SqliteGraphStore = class {
17289
17295
  setLandmark(id, isLandmark) {
17290
17296
  this.stmts.setLandmark.run({ id, v: isLandmark ? 1 : 0 });
17291
17297
  }
17298
+ setLocalCommunity(id, community, column) {
17299
+ this.stmts.setLocalCommunity.run({ id, v: community, col: column ? 1 : 0 });
17300
+ }
17301
+ fieldStmts = /* @__PURE__ */ new Map();
17302
+ findNodeFieldsByLabel(label, attrKeys) {
17303
+ for (const k of attrKeys) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Error(`findNodeFieldsByLabel: bad attr key ${k}`);
17304
+ const cacheKey = attrKeys.join(",");
17305
+ let stmt = this.fieldStmts.get(cacheKey);
17306
+ if (!stmt) {
17307
+ const cols = attrKeys.map((k, i) => `, json_extract(attrs_json, '$.${k}') AS a${i}`).join("");
17308
+ stmt = this.db.prepare(
17309
+ `SELECT id, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
17310
+ );
17311
+ this.fieldStmts.set(cacheKey, stmt);
17312
+ }
17313
+ return stmt.all(label).map((r) => {
17314
+ const attrs = {};
17315
+ attrKeys.forEach((k, i) => {
17316
+ const v = r[`a${i}`];
17317
+ if (v !== null && v !== void 0) attrs[k] = v;
17318
+ });
17319
+ return {
17320
+ id: r["id"],
17321
+ community: r["community"] ?? null,
17322
+ extractionConfidence: r["extraction_confidence"],
17323
+ attrs
17324
+ };
17325
+ });
17326
+ }
17292
17327
  setCommunity(id, community) {
17293
17328
  this.stmts.setCommunity.run({ id, v: community });
17294
17329
  }
package/errata.mjs CHANGED
@@ -19699,6 +19699,12 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
19699
19699
  setPageRank: this.db.prepare("UPDATE nodes SET page_rank = :v WHERE id = :id"),
19700
19700
  setLandmark: this.db.prepare("UPDATE nodes SET is_landmark = :v WHERE id = :id"),
19701
19701
  setCommunity: this.db.prepare("UPDATE nodes SET community = :v WHERE id = :id"),
19702
+ setLocalCommunity: this.db.prepare(`
19703
+ UPDATE nodes SET
19704
+ attrs_json = CASE WHEN :v IS NULL THEN json_remove(attrs_json, '$.localCommunity')
19705
+ ELSE json_set(attrs_json, '$.localCommunity', :v) END,
19706
+ community = CASE WHEN :col = 1 THEN :v ELSE community END
19707
+ WHERE id = :id`),
19702
19708
  updateNode: this.db.prepare(`
19703
19709
  UPDATE nodes SET
19704
19710
  description = COALESCE(:description, description),
@@ -20019,6 +20025,35 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
20019
20025
  setLandmark(id, isLandmark) {
20020
20026
  this.stmts.setLandmark.run({ id, v: isLandmark ? 1 : 0 });
20021
20027
  }
20028
+ setLocalCommunity(id, community, column) {
20029
+ this.stmts.setLocalCommunity.run({ id, v: community, col: column ? 1 : 0 });
20030
+ }
20031
+ fieldStmts = /* @__PURE__ */ new Map();
20032
+ findNodeFieldsByLabel(label, attrKeys) {
20033
+ for (const k of attrKeys) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Error(`findNodeFieldsByLabel: bad attr key ${k}`);
20034
+ const cacheKey2 = attrKeys.join(",");
20035
+ let stmt = this.fieldStmts.get(cacheKey2);
20036
+ if (!stmt) {
20037
+ const cols = attrKeys.map((k, i2) => `, json_extract(attrs_json, '$.${k}') AS a${i2}`).join("");
20038
+ stmt = this.db.prepare(
20039
+ `SELECT id, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
20040
+ );
20041
+ this.fieldStmts.set(cacheKey2, stmt);
20042
+ }
20043
+ return stmt.all(label).map((r) => {
20044
+ const attrs = {};
20045
+ attrKeys.forEach((k, i2) => {
20046
+ const v = r[`a${i2}`];
20047
+ if (v !== null && v !== void 0) attrs[k] = v;
20048
+ });
20049
+ return {
20050
+ id: r["id"],
20051
+ community: r["community"] ?? null,
20052
+ extractionConfidence: r["extraction_confidence"],
20053
+ attrs
20054
+ };
20055
+ });
20056
+ }
20022
20057
  setCommunity(id, community) {
20023
20058
  this.stmts.setCommunity.run({ id, v: community });
20024
20059
  }
@@ -24640,25 +24675,23 @@ function detectLocalCommunities(store, opts = {}) {
24640
24675
  const nodes = /* @__PURE__ */ new Map();
24641
24676
  const unowned = [];
24642
24677
  for (const label of JOINT_COMMUNITY_LABELS) {
24643
- for (const n of store.findNodesByLabel(label)) {
24678
+ for (const n of store.findNodeFieldsByLabel(label, PASS_ATTRS)) {
24644
24679
  if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) unowned.push(n);
24645
- else nodes.set(n.id, n);
24680
+ else nodes.set(n.id, { ...n, label });
24646
24681
  }
24647
24682
  }
24648
24683
  const ids = new Set(nodes.keys());
24649
24684
  report.candidates = ids.size;
24650
24685
  for (const label of SEMANTIC_NODE_LABELS) {
24651
- if (!JOINT_COMMUNITY_LABELS.includes(label)) unowned.push(...store.findNodesByLabel(label));
24686
+ if (!JOINT_COMMUNITY_LABELS.includes(label)) unowned.push(...store.findNodeFieldsByLabel(label, PASS_ATTRS));
24652
24687
  }
24653
24688
  store.transaction(() => {
24654
24689
  for (const n of unowned) {
24655
- if (n.attrs["localCommunity"] !== void 0) {
24656
- const { localCommunity: _gone, ...rest2 } = n.attrs;
24657
- store.updateNode(n.id, { attrs: rest2 });
24658
- }
24659
- if (!n.community || n.attrs["source"] === "cloud") continue;
24660
- store.setCommunity(n.id, null);
24661
- report.cleared++;
24690
+ const pulled = n.attrs["source"] === "cloud";
24691
+ const clearColumn = !!n.community && !pulled;
24692
+ if (n.attrs["localCommunity"] === void 0 && !clearColumn) continue;
24693
+ store.setLocalCommunity(n.id, null, clearColumn);
24694
+ if (clearColumn) report.cleared++;
24662
24695
  }
24663
24696
  });
24664
24697
  if (ids.size === 0) return report;
@@ -24706,14 +24739,10 @@ function detectLocalCommunities(store, opts = {}) {
24706
24739
  const pulled = n.attrs["source"] === "cloud";
24707
24740
  if (pulled) hasCloud = true;
24708
24741
  else hasLocal = true;
24709
- const want = qualifies ? cid : void 0;
24710
- if (n.attrs["localCommunity"] !== want) {
24711
- const nextAttrs = { ...n.attrs };
24712
- if (want) nextAttrs["localCommunity"] = want;
24713
- else delete nextAttrs["localCommunity"];
24714
- store.updateNode(id, { attrs: nextAttrs });
24715
- }
24716
- if (!pulled && (n.community ?? null) !== (want ?? null)) store.setCommunity(id, want ?? null);
24742
+ const want = qualifies ? cid : null;
24743
+ const attrChanged = (n.attrs["localCommunity"] ?? null) !== want;
24744
+ const columnChanged = !pulled && (n.community ?? null) !== want;
24745
+ if (attrChanged || columnChanged) store.setLocalCommunity(id, want, !pulled);
24717
24746
  }
24718
24747
  if (qualifies) {
24719
24748
  report.communities++;
@@ -24797,12 +24826,13 @@ function communityInductionRequests(store, opts = {}) {
24797
24826
  };
24798
24827
  }).filter((r) => r.members.length >= MIN_INDUCTION_MEMBERS).slice(0, maxCommunities);
24799
24828
  }
24800
- var JOINT_COMMUNITY_LABELS, JOINT_COMMUNITY_EDGES, DOMAIN_EDGES, DOMAIN_INHERIT_EDGES, MIN_INDUCTION_MEMBERS;
24829
+ var PASS_ATTRS, JOINT_COMMUNITY_LABELS, JOINT_COMMUNITY_EDGES, DOMAIN_EDGES, DOMAIN_INHERIT_EDGES, MIN_INDUCTION_MEMBERS;
24801
24830
  var init_community2 = __esm({
24802
24831
  "../../packages/local-graph/src/community.ts"() {
24803
24832
  "use strict";
24804
24833
  init_src2();
24805
24834
  init_src3();
24835
+ PASS_ATTRS = ["resolvedAs", "source", "localCommunity"];
24806
24836
  JOINT_COMMUNITY_LABELS = [
24807
24837
  "Problem",
24808
24838
  "Vulnerability",
@@ -32153,12 +32183,12 @@ function promoteMotifs(store, t) {
32153
32183
  const report = { communities: 0, candidates: 0, incoherent: 0, promoted: 0, techniques: 0, antipatterns: 0, retracted: 0, retired: 0 };
32154
32184
  const byCommunity = /* @__PURE__ */ new Map();
32155
32185
  for (const label of MEMBER_LABELS) {
32156
- for (const n of store.findNodesByLabel(label)) {
32157
- if (n.label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
32186
+ for (const n of store.findNodeFieldsByLabel(label, ["localCommunity", "resolvedAs"])) {
32187
+ if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
32158
32188
  const c = n.attrs["localCommunity"];
32159
32189
  if (typeof c !== "string") continue;
32160
32190
  const g = byCommunity.get(c) ?? [];
32161
- g.push(n);
32191
+ g.push({ id: n.id, label, extractionConfidence: n.extractionConfidence });
32162
32192
  byCommunity.set(c, g);
32163
32193
  }
32164
32194
  }
@@ -32211,7 +32241,8 @@ function promoteMotifs(store, t) {
32211
32241
  report.candidates++;
32212
32242
  kept.add(motifId);
32213
32243
  const kind = classifyMotif(store, instances, d.promoted);
32214
- const exemplar = exemplarOf(members);
32244
+ const exemplarRef = exemplarOf(members);
32245
+ const exemplar = exemplarRef ? store.getNode(exemplarRef.id) ?? void 0 : void 0;
32215
32246
  const motif = {
32216
32247
  id: motifId,
32217
32248
  label: kind,
@@ -83942,7 +83973,7 @@ function createWatchBreaker(deps) {
83942
83973
  }
83943
83974
 
83944
83975
  // src/engine.ts
83945
- var DAEMON_VERSION = true ? "2.0.21-dev.3996" : "2.0.0-alpha.0";
83976
+ var DAEMON_VERSION = true ? "2.0.21-dev.4004" : "2.0.0-alpha.0";
83946
83977
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
83947
83978
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
83948
83979
  var GIT_OP_MUTE_MS = 4e3;
@@ -15363,6 +15363,12 @@ var SqliteGraphStore = class {
15363
15363
  setPageRank: this.db.prepare("UPDATE nodes SET page_rank = :v WHERE id = :id"),
15364
15364
  setLandmark: this.db.prepare("UPDATE nodes SET is_landmark = :v WHERE id = :id"),
15365
15365
  setCommunity: this.db.prepare("UPDATE nodes SET community = :v WHERE id = :id"),
15366
+ setLocalCommunity: this.db.prepare(`
15367
+ UPDATE nodes SET
15368
+ attrs_json = CASE WHEN :v IS NULL THEN json_remove(attrs_json, '$.localCommunity')
15369
+ ELSE json_set(attrs_json, '$.localCommunity', :v) END,
15370
+ community = CASE WHEN :col = 1 THEN :v ELSE community END
15371
+ WHERE id = :id`),
15366
15372
  updateNode: this.db.prepare(`
15367
15373
  UPDATE nodes SET
15368
15374
  description = COALESCE(:description, description),
@@ -15683,6 +15689,35 @@ var SqliteGraphStore = class {
15683
15689
  setLandmark(id, isLandmark) {
15684
15690
  this.stmts.setLandmark.run({ id, v: isLandmark ? 1 : 0 });
15685
15691
  }
15692
+ setLocalCommunity(id, community, column) {
15693
+ this.stmts.setLocalCommunity.run({ id, v: community, col: column ? 1 : 0 });
15694
+ }
15695
+ fieldStmts = /* @__PURE__ */ new Map();
15696
+ findNodeFieldsByLabel(label, attrKeys) {
15697
+ for (const k of attrKeys) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Error(`findNodeFieldsByLabel: bad attr key ${k}`);
15698
+ const cacheKey = attrKeys.join(",");
15699
+ let stmt = this.fieldStmts.get(cacheKey);
15700
+ if (!stmt) {
15701
+ const cols = attrKeys.map((k, i) => `, json_extract(attrs_json, '$.${k}') AS a${i}`).join("");
15702
+ stmt = this.db.prepare(
15703
+ `SELECT id, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
15704
+ );
15705
+ this.fieldStmts.set(cacheKey, stmt);
15706
+ }
15707
+ return stmt.all(label).map((r) => {
15708
+ const attrs = {};
15709
+ attrKeys.forEach((k, i) => {
15710
+ const v = r[`a${i}`];
15711
+ if (v !== null && v !== void 0) attrs[k] = v;
15712
+ });
15713
+ return {
15714
+ id: r["id"],
15715
+ community: r["community"] ?? null,
15716
+ extractionConfidence: r["extraction_confidence"],
15717
+ attrs
15718
+ };
15719
+ });
15720
+ }
15686
15721
  setCommunity(id, community) {
15687
15722
  this.stmts.setCommunity.run({ id, v: community });
15688
15723
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.21-dev.3996",
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": {
package/pass-worker.mjs CHANGED
@@ -26244,6 +26244,12 @@ var SqliteGraphStore = class {
26244
26244
  setPageRank: this.db.prepare("UPDATE nodes SET page_rank = :v WHERE id = :id"),
26245
26245
  setLandmark: this.db.prepare("UPDATE nodes SET is_landmark = :v WHERE id = :id"),
26246
26246
  setCommunity: this.db.prepare("UPDATE nodes SET community = :v WHERE id = :id"),
26247
+ setLocalCommunity: this.db.prepare(`
26248
+ UPDATE nodes SET
26249
+ attrs_json = CASE WHEN :v IS NULL THEN json_remove(attrs_json, '$.localCommunity')
26250
+ ELSE json_set(attrs_json, '$.localCommunity', :v) END,
26251
+ community = CASE WHEN :col = 1 THEN :v ELSE community END
26252
+ WHERE id = :id`),
26247
26253
  updateNode: this.db.prepare(`
26248
26254
  UPDATE nodes SET
26249
26255
  description = COALESCE(:description, description),
@@ -26564,6 +26570,35 @@ var SqliteGraphStore = class {
26564
26570
  setLandmark(id, isLandmark) {
26565
26571
  this.stmts.setLandmark.run({ id, v: isLandmark ? 1 : 0 });
26566
26572
  }
26573
+ setLocalCommunity(id, community, column) {
26574
+ this.stmts.setLocalCommunity.run({ id, v: community, col: column ? 1 : 0 });
26575
+ }
26576
+ fieldStmts = /* @__PURE__ */ new Map();
26577
+ findNodeFieldsByLabel(label, attrKeys) {
26578
+ for (const k of attrKeys) if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(k)) throw new Error(`findNodeFieldsByLabel: bad attr key ${k}`);
26579
+ const cacheKey = attrKeys.join(",");
26580
+ let stmt = this.fieldStmts.get(cacheKey);
26581
+ if (!stmt) {
26582
+ const cols = attrKeys.map((k, i2) => `, json_extract(attrs_json, '$.${k}') AS a${i2}`).join("");
26583
+ stmt = this.db.prepare(
26584
+ `SELECT id, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL`
26585
+ );
26586
+ this.fieldStmts.set(cacheKey, stmt);
26587
+ }
26588
+ return stmt.all(label).map((r) => {
26589
+ const attrs = {};
26590
+ attrKeys.forEach((k, i2) => {
26591
+ const v = r[`a${i2}`];
26592
+ if (v !== null && v !== void 0) attrs[k] = v;
26593
+ });
26594
+ return {
26595
+ id: r["id"],
26596
+ community: r["community"] ?? null,
26597
+ extractionConfidence: r["extraction_confidence"],
26598
+ attrs
26599
+ };
26600
+ });
26601
+ }
26567
26602
  setCommunity(id, community) {
26568
26603
  this.stmts.setCommunity.run({ id, v: community });
26569
26604
  }
@@ -28322,6 +28357,7 @@ var DRIFT_VALUES = new Set(Object.values(DRIFT_KIND));
28322
28357
 
28323
28358
  // ../../packages/local-graph/src/community.ts
28324
28359
  init_src2();
28360
+ var PASS_ATTRS = ["resolvedAs", "source", "localCommunity"];
28325
28361
  var JOINT_COMMUNITY_LABELS = [
28326
28362
  "Problem",
28327
28363
  "Vulnerability",
@@ -28369,25 +28405,23 @@ function detectLocalCommunities(store2, opts = {}) {
28369
28405
  const nodes = /* @__PURE__ */ new Map();
28370
28406
  const unowned = [];
28371
28407
  for (const label of JOINT_COMMUNITY_LABELS) {
28372
- for (const n of store2.findNodesByLabel(label)) {
28408
+ for (const n of store2.findNodeFieldsByLabel(label, PASS_ATTRS)) {
28373
28409
  if (n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) unowned.push(n);
28374
- else nodes.set(n.id, n);
28410
+ else nodes.set(n.id, { ...n, label });
28375
28411
  }
28376
28412
  }
28377
28413
  const ids = new Set(nodes.keys());
28378
28414
  report.candidates = ids.size;
28379
28415
  for (const label of SEMANTIC_NODE_LABELS) {
28380
- if (!JOINT_COMMUNITY_LABELS.includes(label)) unowned.push(...store2.findNodesByLabel(label));
28416
+ if (!JOINT_COMMUNITY_LABELS.includes(label)) unowned.push(...store2.findNodeFieldsByLabel(label, PASS_ATTRS));
28381
28417
  }
28382
28418
  store2.transaction(() => {
28383
28419
  for (const n of unowned) {
28384
- if (n.attrs["localCommunity"] !== void 0) {
28385
- const { localCommunity: _gone, ...rest } = n.attrs;
28386
- store2.updateNode(n.id, { attrs: rest });
28387
- }
28388
- if (!n.community || n.attrs["source"] === "cloud") continue;
28389
- store2.setCommunity(n.id, null);
28390
- report.cleared++;
28420
+ const pulled = n.attrs["source"] === "cloud";
28421
+ const clearColumn = !!n.community && !pulled;
28422
+ if (n.attrs["localCommunity"] === void 0 && !clearColumn) continue;
28423
+ store2.setLocalCommunity(n.id, null, clearColumn);
28424
+ if (clearColumn) report.cleared++;
28391
28425
  }
28392
28426
  });
28393
28427
  if (ids.size === 0) return report;
@@ -28435,14 +28469,10 @@ function detectLocalCommunities(store2, opts = {}) {
28435
28469
  const pulled = n.attrs["source"] === "cloud";
28436
28470
  if (pulled) hasCloud = true;
28437
28471
  else hasLocal = true;
28438
- const want = qualifies ? cid : void 0;
28439
- if (n.attrs["localCommunity"] !== want) {
28440
- const nextAttrs = { ...n.attrs };
28441
- if (want) nextAttrs["localCommunity"] = want;
28442
- else delete nextAttrs["localCommunity"];
28443
- store2.updateNode(id, { attrs: nextAttrs });
28444
- }
28445
- if (!pulled && (n.community ?? null) !== (want ?? null)) store2.setCommunity(id, want ?? null);
28472
+ const want = qualifies ? cid : null;
28473
+ const attrChanged = (n.attrs["localCommunity"] ?? null) !== want;
28474
+ const columnChanged = !pulled && (n.community ?? null) !== want;
28475
+ if (attrChanged || columnChanged) store2.setLocalCommunity(id, want, !pulled);
28446
28476
  }
28447
28477
  if (qualifies) {
28448
28478
  report.communities++;
@@ -28916,12 +28946,12 @@ function promoteMotifs(store2, t) {
28916
28946
  const report = { communities: 0, candidates: 0, incoherent: 0, promoted: 0, techniques: 0, antipatterns: 0, retracted: 0, retired: 0 };
28917
28947
  const byCommunity = /* @__PURE__ */ new Map();
28918
28948
  for (const label of MEMBER_LABELS) {
28919
- for (const n of store2.findNodesByLabel(label)) {
28920
- if (n.label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
28949
+ for (const n of store2.findNodeFieldsByLabel(label, ["localCommunity", "resolvedAs"])) {
28950
+ if (label === "Problem" && n.attrs["resolvedAs"] === PROBLEM_RESOLUTION.FALSE_POSITIVE) continue;
28921
28951
  const c = n.attrs["localCommunity"];
28922
28952
  if (typeof c !== "string") continue;
28923
28953
  const g = byCommunity.get(c) ?? [];
28924
- g.push(n);
28954
+ g.push({ id: n.id, label, extractionConfidence: n.extractionConfidence });
28925
28955
  byCommunity.set(c, g);
28926
28956
  }
28927
28957
  }
@@ -28974,7 +29004,8 @@ function promoteMotifs(store2, t) {
28974
29004
  report.candidates++;
28975
29005
  kept.add(motifId);
28976
29006
  const kind = classifyMotif(store2, instances, d.promoted);
28977
- const exemplar = exemplarOf(members);
29007
+ const exemplarRef = exemplarOf(members);
29008
+ const exemplar = exemplarRef ? store2.getNode(exemplarRef.id) ?? void 0 : void 0;
28978
29009
  const motif = {
28979
29010
  id: motifId,
28980
29011
  label: kind,
@@ -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.3996",
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
- 5e52c2053e6264cb739f4f92c174af82a478044f66ebd9e80e0fbb1a36cb1446 .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