brito-cli 0.15.0 → 0.16.1
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/dist/graph/pathfind.js +168 -0
- package/dist/graph/sections.js +5 -2
- package/dist/graph/vaultgraph.js +89 -0
- package/dist/graph/vaultquery.js +103 -0
- package/dist/tools/vault.js +16 -3
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Algoritmos de grafo em memória para o cofre: Dijkstra, A* com landmarks (ALT) e PageRank personalizado.
|
|
3
|
+
* Custo da aresta = custo da relação + penalidade de hub (log do grau): o caminho preferido não passa por "Início" ou "Deus".
|
|
4
|
+
*/
|
|
5
|
+
class Heap {
|
|
6
|
+
k = [];
|
|
7
|
+
v = [];
|
|
8
|
+
get size() { return this.k.length; }
|
|
9
|
+
push(key, val) {
|
|
10
|
+
let i = this.k.length;
|
|
11
|
+
this.k.push(key);
|
|
12
|
+
this.v.push(val);
|
|
13
|
+
while (i > 0) {
|
|
14
|
+
const p = (i - 1) >> 1;
|
|
15
|
+
if (this.k[p] <= this.k[i])
|
|
16
|
+
break;
|
|
17
|
+
this.swap(i, p);
|
|
18
|
+
i = p;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
pop() {
|
|
22
|
+
const top = [this.k[0], this.v[0]];
|
|
23
|
+
const lk = this.k.pop(), lv = this.v.pop();
|
|
24
|
+
if (this.k.length) {
|
|
25
|
+
this.k[0] = lk;
|
|
26
|
+
this.v[0] = lv;
|
|
27
|
+
let i = 0;
|
|
28
|
+
for (;;) {
|
|
29
|
+
const l = 2 * i + 1, r = l + 1;
|
|
30
|
+
let m = i;
|
|
31
|
+
if (l < this.k.length && this.k[l] < this.k[m])
|
|
32
|
+
m = l;
|
|
33
|
+
if (r < this.k.length && this.k[r] < this.k[m])
|
|
34
|
+
m = r;
|
|
35
|
+
if (m === i)
|
|
36
|
+
break;
|
|
37
|
+
this.swap(i, m);
|
|
38
|
+
i = m;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return top;
|
|
42
|
+
}
|
|
43
|
+
swap(a, b) { [this.k[a], this.k[b]] = [this.k[b], this.k[a]]; [this.v[a], this.v[b]] = [this.v[b], this.v[a]]; }
|
|
44
|
+
}
|
|
45
|
+
export class WGraph {
|
|
46
|
+
n;
|
|
47
|
+
adj;
|
|
48
|
+
deg;
|
|
49
|
+
constructor(n, edges, hub = 0.25) {
|
|
50
|
+
this.n = n;
|
|
51
|
+
this.adj = Array.from({ length: n }, () => []);
|
|
52
|
+
this.deg = new Array(n).fill(0);
|
|
53
|
+
for (const [a, b] of edges) {
|
|
54
|
+
this.deg[a]++;
|
|
55
|
+
this.deg[b]++;
|
|
56
|
+
}
|
|
57
|
+
for (const [a, b, base] of edges) {
|
|
58
|
+
const w = base + hub * (Math.log2(1 + this.deg[a]) + Math.log2(1 + this.deg[b]));
|
|
59
|
+
this.adj[a].push({ to: b, w });
|
|
60
|
+
this.adj[b].push({ to: a, w });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
dijkstra(s) {
|
|
64
|
+
const dist = new Float64Array(this.n).fill(Infinity);
|
|
65
|
+
dist[s] = 0;
|
|
66
|
+
const h = new Heap();
|
|
67
|
+
h.push(0, s);
|
|
68
|
+
while (h.size) {
|
|
69
|
+
const [d, u] = h.pop();
|
|
70
|
+
if (d > dist[u])
|
|
71
|
+
continue;
|
|
72
|
+
for (const e of this.adj[u]) {
|
|
73
|
+
const nd = d + e.w;
|
|
74
|
+
if (nd < dist[e.to]) {
|
|
75
|
+
dist[e.to] = nd;
|
|
76
|
+
h.push(nd, e.to);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return dist;
|
|
81
|
+
}
|
|
82
|
+
/** Landmarks por ponto mais distante: distâncias pré-calculadas que dão a heurística admissível do A*. */
|
|
83
|
+
landmarks(k, from = 0) {
|
|
84
|
+
const out = [];
|
|
85
|
+
const mind = new Float64Array(this.n).fill(Infinity);
|
|
86
|
+
let cur = from;
|
|
87
|
+
for (let i = 0; i < k && cur >= 0; i++) {
|
|
88
|
+
const dist = this.dijkstra(cur);
|
|
89
|
+
out.push(dist);
|
|
90
|
+
let best = -1, bd = -1;
|
|
91
|
+
for (let v = 0; v < this.n; v++) {
|
|
92
|
+
mind[v] = Math.min(mind[v], dist[v]);
|
|
93
|
+
if (mind[v] !== Infinity && mind[v] > bd) {
|
|
94
|
+
bd = mind[v];
|
|
95
|
+
best = v;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
cur = best;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
astar(s, t, lm) {
|
|
103
|
+
const hf = (v) => {
|
|
104
|
+
let m = 0;
|
|
105
|
+
for (const d of lm) {
|
|
106
|
+
const a = d[v], b = d[t];
|
|
107
|
+
if (a !== Infinity && b !== Infinity)
|
|
108
|
+
m = Math.max(m, Math.abs(a - b));
|
|
109
|
+
}
|
|
110
|
+
return m;
|
|
111
|
+
};
|
|
112
|
+
const g = new Float64Array(this.n).fill(Infinity);
|
|
113
|
+
const prev = new Int32Array(this.n).fill(-1);
|
|
114
|
+
g[s] = 0;
|
|
115
|
+
const h = new Heap();
|
|
116
|
+
h.push(hf(s), s);
|
|
117
|
+
const done = new Uint8Array(this.n);
|
|
118
|
+
let settled = 0;
|
|
119
|
+
while (h.size) {
|
|
120
|
+
const [, u] = h.pop();
|
|
121
|
+
if (done[u])
|
|
122
|
+
continue;
|
|
123
|
+
done[u] = 1;
|
|
124
|
+
settled++;
|
|
125
|
+
if (u === t) {
|
|
126
|
+
const path = [t];
|
|
127
|
+
while (path[0] !== s)
|
|
128
|
+
path.unshift(prev[path[0]]);
|
|
129
|
+
return { cost: g[t], settled, path };
|
|
130
|
+
}
|
|
131
|
+
for (const e of this.adj[u]) {
|
|
132
|
+
const ng = g[u] + e.w;
|
|
133
|
+
if (ng < g[e.to]) {
|
|
134
|
+
g[e.to] = ng;
|
|
135
|
+
prev[e.to] = u;
|
|
136
|
+
h.push(ng + hf(e.to), e.to);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { cost: Infinity, settled, path: [] };
|
|
141
|
+
}
|
|
142
|
+
/** PageRank personalizado: a massa parte das sementes e segue as arestas na proporção de 1/custo. */
|
|
143
|
+
ppr(seeds, alpha = 0.2, iters = 25) {
|
|
144
|
+
const tot = [...seeds.values()].reduce((a, b) => a + b, 0) || 1;
|
|
145
|
+
const p0 = new Float64Array(this.n);
|
|
146
|
+
for (const [k, v] of seeds)
|
|
147
|
+
p0[k] = v / tot;
|
|
148
|
+
const wsum = this.adj.map((l) => l.reduce((a, e) => a + 1 / e.w, 0));
|
|
149
|
+
let p = Float64Array.from(p0);
|
|
150
|
+
for (let it = 0; it < iters; it++) {
|
|
151
|
+
const q = new Float64Array(this.n);
|
|
152
|
+
for (let u = 0; u < this.n; u++) {
|
|
153
|
+
if (!p[u])
|
|
154
|
+
continue;
|
|
155
|
+
if (!wsum[u]) {
|
|
156
|
+
q[u] += p[u];
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
for (const e of this.adj[u])
|
|
160
|
+
q[e.to] += (1 - alpha) * p[u] * (1 / e.w) / wsum[u];
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < this.n; i++)
|
|
163
|
+
q[i] += alpha * p0[i];
|
|
164
|
+
p = q;
|
|
165
|
+
}
|
|
166
|
+
return p;
|
|
167
|
+
}
|
|
168
|
+
}
|
package/dist/graph/sections.js
CHANGED
|
@@ -6,7 +6,7 @@ import fs from "node:fs";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import crypto from "node:crypto";
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
|
-
export const SECTIONS_VERSION = "
|
|
9
|
+
export const SECTIONS_VERSION = "m2";
|
|
10
10
|
const MD_RE = /\.(md|markdown)$/i;
|
|
11
11
|
/** Formatos que só viram metadado (Doc sem seções): a fonte de verdade é o .md derivado deles. */
|
|
12
12
|
const META_RE = /\.(pdf|epub|docx)$/i;
|
|
@@ -14,7 +14,10 @@ const IGNORE_RE = /(^|\/)(node_modules|dist|build|\.git|\.brito|\.venv|target|ve
|
|
|
14
14
|
const MAX_MD_BYTES = 5_000_000;
|
|
15
15
|
/** Siglas da NAA (e variantes sem acento); só estas valem como livro, para não confundir "Cap 3" com passagem. */
|
|
16
16
|
const LIVROS = new Set(("Gn Êx Ex Lv Nm Dt Js Jz Rt 1Sm 2Sm 1Rs 2Rs 1Cr 2Cr Ed Ne Et Jó Sl Pv Ec Ct Is Jr Lm Ez Dn Os Jl Am Ob Jn Mq Na Hc Sf Ag Zc Ml " +
|
|
17
|
-
"Mt Mc Lc Jo At Rm 1Co 2Co Gl Ef Fp Cl 1Ts 2Ts 1Tm 2Tm Tt Fm Hb Tg 1Pe 2Pe 1Jo 2Jo 3Jo Jd Ap"
|
|
17
|
+
"Mt Mc Lc Jo At Rm 1Co 2Co Gl Ef Fp Cl 1Ts 2Ts 1Tm 2Tm Tt Fm Hb Tg 1Pe 2Pe 1Jo 2Jo 3Jo Jd Ap " +
|
|
18
|
+
// formas longas e variantes usuais em português (as fichas do cofre usam "1Cor", "Hab" etc.)
|
|
19
|
+
"Gên Gen Êxo Exo Lev Núm Num Deut Jos Juí Jz Rute 1Sam 2Sam 1Re 2Re 1Cr 2Cr Esd Est Jó Sal Prov Ecl Cant Isa Jer Lam Eze Ezq Dan Ose Joe Amó Abd Jon Miq Naum Hab Sof Ageu Zac Mal " +
|
|
20
|
+
"Mat Mar Luc Joã Atos Rom 1Cor 2Cor Gál Efé Fil Col 1Tes 2Tes 1Tim 2Tim Tito Filem Heb Tia 1Ped 2Ped 1Joã 2Joã 3Joã Jud Apoc").split(" "));
|
|
18
21
|
/** "Rm 9.13; Rm 3.25-26; Ob 15" → ["Rm 9.13","Rm 3.25","Rm 3.26","Ob 15"]. Intervalo vira um item por versículo (máx. 60). */
|
|
19
22
|
export function parsePassagens(txt) {
|
|
20
23
|
const out = [];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* O cofre como grafo em memória (Doc + Passagem; arestas LIGA e CITA) para pathfinding e expansão por PageRank.
|
|
3
|
+
* O corpo (texto das seções) NÃO fica aqui: só os nós e as arestas. Quem mostra o resultado busca o texto no banco.
|
|
4
|
+
*/
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { WGraph } from "./pathfind.js";
|
|
7
|
+
import { fold } from "./vault.js";
|
|
8
|
+
const cache = new WeakMap();
|
|
9
|
+
export const HUB_DEGREE = 40;
|
|
10
|
+
const pair = (a, b) => (a < b ? `${a}|${b}` : `${b}|${a}`);
|
|
11
|
+
export const label = (v, i) => (i < v.nDocs ? v.paths[i] : `[${v.refs[i - v.nDocs]}]`);
|
|
12
|
+
export const relOf = (v, a, b) => v.rel.get(pair(a, b)) ?? "?";
|
|
13
|
+
export async function vaultGraph(store) {
|
|
14
|
+
const docs = (await store.run("MATCH (d:Doc) RETURN d.path AS p, d.titulo AS t, d.hash AS h ORDER BY p"));
|
|
15
|
+
const liga = (await store.run("MATCH (a:Doc)-[r:LIGA]->(b:Doc) RETURN a.path AS a, b.path AS b, r.tipo AS t"));
|
|
16
|
+
const cita = (await store.run("MATCH (d:Doc)-[:CITA]->(p:Passagem) RETURN d.path AS d, p.ref AS r"));
|
|
17
|
+
const sig = crypto.createHash("sha1").update(docs.map((d) => d.p + d.h).join("\n")).digest("hex") + `:${liga.length}:${cita.length}`;
|
|
18
|
+
const old = cache.get(store);
|
|
19
|
+
if (old && old.sig === sig)
|
|
20
|
+
return old;
|
|
21
|
+
const idx = new Map(docs.map((d, i) => [d.p, i]));
|
|
22
|
+
const refIdx = new Map();
|
|
23
|
+
for (const c of cita)
|
|
24
|
+
if (!refIdx.has(c.r))
|
|
25
|
+
refIdx.set(c.r, docs.length + refIdx.size);
|
|
26
|
+
const edges = new Map();
|
|
27
|
+
const rel = new Map();
|
|
28
|
+
const add = (a, b, cost, t) => {
|
|
29
|
+
const k = pair(a, b);
|
|
30
|
+
const o = edges.get(k);
|
|
31
|
+
if (!o || cost < o[2]) {
|
|
32
|
+
edges.set(k, [a, b, cost]);
|
|
33
|
+
rel.set(k, t);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
for (const l of liga) {
|
|
37
|
+
const a = idx.get(l.a), b = idx.get(l.b);
|
|
38
|
+
if (a !== undefined && b !== undefined && a !== b)
|
|
39
|
+
add(a, b, l.t === "menciona" ? 1.6 : 1.0, l.t);
|
|
40
|
+
}
|
|
41
|
+
for (const c of cita) {
|
|
42
|
+
const a = idx.get(c.d);
|
|
43
|
+
if (a !== undefined)
|
|
44
|
+
add(a, refIdx.get(c.r), 1.2, "cita");
|
|
45
|
+
}
|
|
46
|
+
const g = new WGraph(docs.length + refIdx.size, [...edges.values()]);
|
|
47
|
+
const first = g.deg.findIndex((d) => d > 0);
|
|
48
|
+
const v = {
|
|
49
|
+
g, lm: first < 0 ? [] : g.landmarks(8, first), nDocs: docs.length,
|
|
50
|
+
paths: docs.map((d) => d.p), titles: docs.map((d) => d.t), refs: [...refIdx.keys()], rel, sig,
|
|
51
|
+
};
|
|
52
|
+
cache.set(store, v);
|
|
53
|
+
return v;
|
|
54
|
+
}
|
|
55
|
+
/** Resolve um nome (caminho, título, nome de arquivo ou passagem "Rm 9.13") para nós. Exato primeiro, depois por inclusão. */
|
|
56
|
+
export function resolveEntity(v, q) {
|
|
57
|
+
const f = fold(q).trim();
|
|
58
|
+
if (!f)
|
|
59
|
+
return [];
|
|
60
|
+
const names = (i) => [fold(v.paths[i]), fold(v.paths[i].replace(/\.\w+$/, "")), fold(v.paths[i].split("/").pop().replace(/\.\w+$/, "")), fold(v.titles[i] ?? "")];
|
|
61
|
+
const exact = [];
|
|
62
|
+
for (let i = 0; i < v.nDocs; i++)
|
|
63
|
+
if (names(i).includes(f))
|
|
64
|
+
exact.push(i);
|
|
65
|
+
v.refs.forEach((r, k) => { if (fold(r) === f)
|
|
66
|
+
exact.push(v.nDocs + k); });
|
|
67
|
+
if (exact.length)
|
|
68
|
+
return exact;
|
|
69
|
+
const part = [];
|
|
70
|
+
for (let i = 0; i < v.nDocs; i++)
|
|
71
|
+
if (names(i).some((n) => n.length >= 3 && n.includes(f)))
|
|
72
|
+
part.push(i);
|
|
73
|
+
return part.sort((a, b) => v.paths[a].length - v.paths[b].length).slice(0, 5);
|
|
74
|
+
}
|
|
75
|
+
/** Entidades nomeadas na pergunta: títulos de nota que aparecem inteiros no texto (a mais longa vence a que ela contém). */
|
|
76
|
+
export function entitiesIn(v, query) {
|
|
77
|
+
const clean = (s) => fold(s).replace(/[^\p{L}\p{N}.]+/gu, " ").trim();
|
|
78
|
+
const q = ` ${clean(query)} `;
|
|
79
|
+
const hit = [];
|
|
80
|
+
for (let i = 0; i < v.nDocs; i++) {
|
|
81
|
+
const t = clean(v.titles[i] ?? "");
|
|
82
|
+
if (t.length >= 4 && q.includes(` ${t} `))
|
|
83
|
+
hit.push(i);
|
|
84
|
+
}
|
|
85
|
+
const keep = hit.filter((i) => !hit.some((j) => j !== i && clean(v.titles[j]).includes(clean(v.titles[i])) && clean(v.titles[j]).length > clean(v.titles[i]).length));
|
|
86
|
+
v.refs.forEach((r, k) => { if (q.includes(` ${fold(r)} `))
|
|
87
|
+
keep.push(v.nDocs + k); });
|
|
88
|
+
return keep;
|
|
89
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { vaultSearch } from "./vault.js";
|
|
2
|
+
import { searchSecoes } from "./vault.js";
|
|
3
|
+
import { HUB_DEGREE, entitiesIn, label, relOf, resolveEntity, vaultGraph } from "./vaultgraph.js";
|
|
4
|
+
const isNote = (v, i) => i < v.nDocs && v.paths[i].endsWith(".md") && !/^(Diário de estudo|Modelos)\//.test(v.paths[i]) && !v.paths[i].startsWith("00 ");
|
|
5
|
+
const clip = (s, n = 150) => { const f = s.replace(/\s+/g, " ").trim(); return f.length > n ? f.slice(0, n) + " …" : f; };
|
|
6
|
+
async function intro(store, doc) {
|
|
7
|
+
const rows = await store.run("MATCH (s:Secao {doc:$d}) RETURN s.texto AS x ORDER BY s.ordem LIMIT 6", { d: doc });
|
|
8
|
+
for (const r of rows) {
|
|
9
|
+
// pula seções que são só lista de links ou nota de página: pega a primeira com prosa
|
|
10
|
+
const prose = String(r.x).split("\n").filter((l) => !/^\s*[-*]?\s*(\[\[[^\]]*\]\]\s*)+$/.test(l) && !/^\s*Páginas\s*=/.test(l)).join(" ").replace(/\[\[([^\]|]*)(?:\|[^\]]*)?\]\]/g, "$1");
|
|
11
|
+
if (prose.replace(/\s+/g, " ").trim().length >= 40)
|
|
12
|
+
return clip(prose);
|
|
13
|
+
}
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
/** Por que `r` apareceu: ligação direta com uma semente ou um vizinho em comum com ela. */
|
|
17
|
+
function via(v, r, seeds) {
|
|
18
|
+
const S = new Set(seeds);
|
|
19
|
+
for (const e of v.g.adj[r])
|
|
20
|
+
if (S.has(e.to))
|
|
21
|
+
return `${label(v, e.to)} (${relOf(v, r, e.to)})`;
|
|
22
|
+
const nbS = new Map();
|
|
23
|
+
for (const s of seeds)
|
|
24
|
+
for (const e of v.g.adj[s])
|
|
25
|
+
if (!nbS.has(e.to))
|
|
26
|
+
nbS.set(e.to, s);
|
|
27
|
+
for (const e of v.g.adj[r])
|
|
28
|
+
if (nbS.has(e.to) && v.g.deg[e.to] <= HUB_DEGREE)
|
|
29
|
+
return `${label(v, nbS.get(e.to))} → ${label(v, e.to)} (${relOf(v, r, e.to)})`;
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
export async function vaultSearchGraph(root, store, query, opts = {}) {
|
|
33
|
+
const base = await vaultSearch(root, store, query, opts);
|
|
34
|
+
const v = await vaultGraph(store);
|
|
35
|
+
if (!v.nDocs)
|
|
36
|
+
return base;
|
|
37
|
+
const ents = entitiesIn(v, query);
|
|
38
|
+
const hits = await searchSecoes(store, query, { em: opts.em, limit: 10 });
|
|
39
|
+
const hitIdx = [];
|
|
40
|
+
for (const h of hits) {
|
|
41
|
+
const i = v.paths.indexOf(h.doc);
|
|
42
|
+
if (i >= 0 && !hitIdx.includes(i))
|
|
43
|
+
hitIdx.push(i);
|
|
44
|
+
}
|
|
45
|
+
const join = ents.length >= 2;
|
|
46
|
+
const seeds = join ? ents.slice(0, 4) : [...new Set([...ents, ...hitIdx.slice(0, 3)])];
|
|
47
|
+
if (!seeds.length)
|
|
48
|
+
return base;
|
|
49
|
+
const w = new Map();
|
|
50
|
+
if (join)
|
|
51
|
+
seeds.forEach((s) => w.set(s, 1));
|
|
52
|
+
else {
|
|
53
|
+
ents.forEach((e) => w.set(e, 2));
|
|
54
|
+
hitIdx.slice(0, 3).forEach((d, i) => w.set(d, (w.get(d) ?? 0) + 1 / (1 + i)));
|
|
55
|
+
}
|
|
56
|
+
const p = v.g.ppr(w);
|
|
57
|
+
const skip = new Set([...seeds, ...(join ? [] : hitIdx)]);
|
|
58
|
+
const cand = [];
|
|
59
|
+
for (let i = 0; i < v.nDocs; i++)
|
|
60
|
+
if (p[i] > 0 && !skip.has(i) && isNote(v, i) && v.g.deg[i] <= HUB_DEGREE * 2)
|
|
61
|
+
cand.push(i);
|
|
62
|
+
cand.sort((a, b) => p[b] - p[a]);
|
|
63
|
+
const top = cand.slice(0, opts.limit ?? 5);
|
|
64
|
+
if (!top.length)
|
|
65
|
+
return base;
|
|
66
|
+
const head = join ? `## Notas ligadas a ${seeds.map((s) => label(v, s)).join(" e ")} (junção no grafo)` : "## Relacionadas pelo grafo (não citam as palavras, mas se ligam às notas achadas)";
|
|
67
|
+
const lines = [head];
|
|
68
|
+
for (const r of top)
|
|
69
|
+
lines.push(`- ${v.paths[r]} | ${v.titles[r]}${via(v, r, seeds) ? ` | via ${via(v, r, seeds)}` : ""}`);
|
|
70
|
+
return join ? `${lines.join("\n")}\n\n${base}` : `${base}\n\n${lines.join("\n")}`;
|
|
71
|
+
}
|
|
72
|
+
/** Caminho de menor custo entre duas notas/passagens, com a relação de cada salto e o início do texto de cada nota. */
|
|
73
|
+
export async function vaultPath(store, de, para) {
|
|
74
|
+
const v = await vaultGraph(store);
|
|
75
|
+
const A = resolveEntity(v, de), B = resolveEntity(v, para);
|
|
76
|
+
if (!A.length)
|
|
77
|
+
return `não achei nada para "${de}" (use título, caminho ou passagem como "Rm 9.13")`;
|
|
78
|
+
if (!B.length)
|
|
79
|
+
return `não achei nada para "${para}" (use título, caminho ou passagem como "Rm 9.13")`;
|
|
80
|
+
const a = A[0], b = B[0];
|
|
81
|
+
const notes = [];
|
|
82
|
+
if (A.length > 1)
|
|
83
|
+
notes.push(`"${de}" casa com ${A.length} itens; usei ${label(v, a)} (outros: ${A.slice(1, 4).map((x) => label(v, x)).join(", ")})`);
|
|
84
|
+
if (B.length > 1)
|
|
85
|
+
notes.push(`"${para}" casa com ${B.length} itens; usei ${label(v, b)} (outros: ${B.slice(1, 4).map((x) => label(v, x)).join(", ")})`);
|
|
86
|
+
if (a === b)
|
|
87
|
+
return "origem e destino são o mesmo item";
|
|
88
|
+
const r = v.g.astar(a, b, v.lm);
|
|
89
|
+
if (!r.path.length)
|
|
90
|
+
return `sem caminho entre ${label(v, a)} e ${label(v, b)}`;
|
|
91
|
+
const out = [`Caminho de menor custo (${r.path.length - 1} saltos, custo ${r.cost.toFixed(2)}, ${r.settled} nós visitados):`];
|
|
92
|
+
for (let k = 0; k < r.path.length; k++) {
|
|
93
|
+
const n = r.path[k];
|
|
94
|
+
const text = n < v.nDocs ? await intro(store, v.paths[n]) : "";
|
|
95
|
+
out.push(`${k + 1}. ${label(v, n)}${n < v.nDocs && v.titles[n] ? ` — ${v.titles[n]}` : ""}${text ? `\n ${text}` : ""}`);
|
|
96
|
+
if (k < r.path.length - 1)
|
|
97
|
+
out.push(` ↓ ${relOf(v, n, r.path[k + 1])}`);
|
|
98
|
+
}
|
|
99
|
+
const hubs = r.path.slice(1, -1).filter((n) => v.g.deg[n] > HUB_DEGREE);
|
|
100
|
+
if (hubs.length)
|
|
101
|
+
out.push(`Aviso: o caminho passa por nó muito ligado (${hubs.map((n) => label(v, n)).join(", ")}); a relação pode ser fraca.`);
|
|
102
|
+
return [...notes, ...out].join("\n");
|
|
103
|
+
}
|
package/dist/tools/vault.js
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { defineTool } from "./registry.js";
|
|
3
|
-
import {
|
|
3
|
+
import { vaultPath, vaultSearchGraph } from "../graph/vaultquery.js";
|
|
4
4
|
defineTool({
|
|
5
5
|
name: "vault_search",
|
|
6
6
|
concurrent: true,
|
|
7
7
|
readsGraph: true,
|
|
8
|
-
description: "Busca por palavras num cofre de notas Markdown (sem acento nem caixa, todas as palavras): devolve seções de .md com arquivo e linhas (`arq.md:12-30`), os vizinhos do 1º resultado no grafo ([[links]]) e, se houver índice, trechos de livros/aulas com página. USE ANTES de abrir arquivos ou PDFs; depois leia só a faixa de linhas indicada.",
|
|
8
|
+
description: "Busca por palavras num cofre de notas Markdown (sem acento nem caixa, todas as palavras): devolve seções de .md com arquivo e linhas (`arq.md:12-30`), os vizinhos do 1º resultado no grafo ([[links]]) as notas relacionadas pelo grafo (que não citam as palavras mas se ligam às achadas; com 2 entidades na pergunta, as notas ligadas às duas) e, se houver índice, trechos de livros/aulas com página. USE ANTES de abrir arquivos ou PDFs; depois leia só a faixa de linhas indicada.",
|
|
9
9
|
schema: z.object({
|
|
10
10
|
query: z.string().describe("palavras ou frase"),
|
|
11
11
|
em: z.string().optional().describe("limita a caminhos que contêm este texto (ex.: 'Temas/', 'Frame')"),
|
|
12
12
|
limit: z.number().int().min(1).max(20).optional(),
|
|
13
13
|
}),
|
|
14
14
|
async run({ query, em, limit }, { root, graph }) {
|
|
15
|
-
return
|
|
15
|
+
return vaultSearchGraph(root, graph, query, { em, limit });
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
defineTool({
|
|
19
|
+
name: "vault_path",
|
|
20
|
+
concurrent: true,
|
|
21
|
+
readsGraph: true,
|
|
22
|
+
description: "Como duas notas ou passagens se ligam: caminho de menor custo no grafo do cofre (A*), com a relação de cada salto (autor, fonte, temas, menciona, cita) e o início do texto de cada nota. Evita nós muito ligados. USE para 'como X se relaciona com Y?'. Aceita título, caminho ou passagem ('Rm 9.13').",
|
|
23
|
+
schema: z.object({
|
|
24
|
+
de: z.string().describe("nota ou passagem de origem"),
|
|
25
|
+
para: z.string().describe("nota ou passagem de destino"),
|
|
26
|
+
}),
|
|
27
|
+
async run({ de, para }, { graph }) {
|
|
28
|
+
return vaultPath(graph, de, para);
|
|
16
29
|
},
|
|
17
30
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brito-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "Coding-agent CLI with a code knowledge graph, memory and reviewable skills / CLI de agente de código com grafo de conhecimento, memória e skills revisáveis",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|