@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.
- package/consolidate-worker.mjs +35 -0
- package/errata.mjs +55 -24
- package/generalize-worker.mjs +35 -0
- package/package.json +1 -1
- package/pass-worker.mjs +53 -22
- package/resources/claude-mods/errata-minimap/.claude-plugin/plugin.json +1 -1
- package/resources/claude-mods/errata-minimap/hooks/bookcase.ts +37 -8
- package/resources/claude-mods/errata-minimap/hooks/chains.ts +29 -18
- package/resources/claude-mods/errata-minimap/hooks/register.tsx +85 -6
- package/resources/context-plugin/.codex-plugin/plugin.json +1 -1
- package/resources/context-plugin/SHA256SUMS +1 -1
package/consolidate-worker.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
24656
|
-
|
|
24657
|
-
|
|
24658
|
-
|
|
24659
|
-
if (
|
|
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 :
|
|
24710
|
-
|
|
24711
|
-
|
|
24712
|
-
|
|
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.
|
|
32157
|
-
if (
|
|
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
|
|
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.
|
|
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;
|
package/generalize-worker.mjs
CHANGED
|
@@ -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
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.
|
|
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.
|
|
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
|
-
|
|
28385
|
-
|
|
28386
|
-
|
|
28387
|
-
|
|
28388
|
-
if (
|
|
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 :
|
|
28439
|
-
|
|
28440
|
-
|
|
28441
|
-
|
|
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.
|
|
28920
|
-
if (
|
|
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
|
|
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
|
+
"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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
281
|
-
|
|
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
|
-
|
|
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
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
{
|
|
738
|
+
{showLegend && (
|
|
660
739
|
<Text wrap="truncate-end">
|
|
661
740
|
{legend.map(b => (
|
|
662
741
|
<Text>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
988e2d2c901e9c5271b40684d0e30351ea8bae6c46ebb98c5067f6fb2944f1ab .codex-plugin/plugin.json
|
|
2
2
|
a7bebae9fb01c838d6c75fc784129e28be571233fa66ff5ee8039b574f35ccf7 assets/icon.png
|
|
3
3
|
fc38b64e4df1e0b72c35764bef6c4c05430d97c9009ad0ecc8a9e75b4deab232 configure-local.mjs
|
|
4
4
|
73dc6028b3b070257c7c31f252d11e55a9e096f4aed0f187f10cb21716e1e6d7 dist/client.js
|