@appchy/jarvis 0.1.37 → 0.1.39

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.
Files changed (108) hide show
  1. package/bin/config-change.mjs +1 -1
  2. package/bin/jarvis.mjs +8 -12
  3. package/bin/pre-tool-use.mjs +2 -2
  4. package/bin/session-start.mjs +1 -1
  5. package/bin/stop.mjs +1 -1
  6. package/bin/user-prompt-submit.mjs +1 -1
  7. package/dist/bin.js +10168 -37341
  8. package/dist/bin.js.map +1 -1
  9. package/dist/data/backends.mjs +469 -0
  10. package/dist/data/chunk-7REP35VA.mjs +462 -0
  11. package/dist/data/chunk-AKQQC5IT.mjs +1224 -0
  12. package/dist/data/chunk-AYOJSS2F.mjs +72 -0
  13. package/dist/data/chunk-RRJ6KKYL.mjs +42 -0
  14. package/dist/data/chunk-YWSWQEJN.mjs +138 -0
  15. package/dist/data/embedders.mjs +80 -0
  16. package/dist/data/finders.mjs +35 -0
  17. package/dist/data/index.mjs +137 -0
  18. package/dist/data/linkers.mjs +83 -0
  19. package/dist/data/mcp.mjs +2624 -0
  20. package/dist/data/persistences.mjs +8 -0
  21. package/dist/data/rerankers.mjs +52 -0
  22. package/dist/data/stores.mjs +38 -0
  23. package/dist/hooks/config-change.js +7 -29
  24. package/dist/hooks/config-change.js.map +1 -1
  25. package/dist/hooks/pre-tool-use.js +129 -32
  26. package/dist/hooks/pre-tool-use.js.map +1 -1
  27. package/dist/hooks/session-start.js +72 -37
  28. package/dist/hooks/session-start.js.map +1 -1
  29. package/dist/hooks/stop.js +140 -32
  30. package/dist/hooks/stop.js.map +1 -1
  31. package/dist/hooks/user-prompt-submit.js +7 -29
  32. package/dist/hooks/user-prompt-submit.js.map +1 -1
  33. package/harness/assets/architecture-template.md +53 -0
  34. package/harness/assets/design-template.md +89 -0
  35. package/harness/assets/domains/architecture.md +43 -0
  36. package/harness/assets/domains/commercial.md +42 -0
  37. package/harness/assets/domains/design.md +42 -0
  38. package/harness/assets/domains/legal.md +43 -0
  39. package/harness/assets/domains/operations.md +39 -0
  40. package/harness/assets/domains/product.md +43 -0
  41. package/harness/assets/domains/quality.md +43 -0
  42. package/harness/assets/domains/security.md +44 -0
  43. package/harness/assets/domains/support.md +45 -0
  44. package/harness/assets/epic-template.md +85 -0
  45. package/harness/assets/feature-template.md +86 -0
  46. package/harness/assets/handoff-template.md +26 -0
  47. package/harness/assets/task-template.md +38 -0
  48. package/harness/assets/version-template.md +43 -0
  49. package/harness/harness/__init__.py +7 -0
  50. package/harness/harness/align.py +582 -0
  51. package/harness/harness/architecture.py +254 -0
  52. package/harness/harness/autonomy.py +374 -0
  53. package/harness/harness/branches.py +408 -0
  54. package/harness/harness/config.py +1482 -0
  55. package/harness/harness/coverage.py +199 -0
  56. package/harness/harness/epic.py +220 -0
  57. package/harness/harness/events.py +153 -0
  58. package/harness/harness/extend.py +99 -0
  59. package/harness/harness/frontmatter.py +218 -0
  60. package/harness/harness/gate.py +591 -0
  61. package/harness/harness/generate.py +267 -0
  62. package/harness/harness/git.py +775 -0
  63. package/harness/harness/ids.py +140 -0
  64. package/harness/harness/kickoff.py +231 -0
  65. package/harness/harness/lint.py +505 -0
  66. package/harness/harness/model.py +364 -0
  67. package/harness/harness/peers.py +187 -0
  68. package/harness/harness/product.py +29 -0
  69. package/harness/harness/registry.py +382 -0
  70. package/harness/harness/report.py +227 -0
  71. package/harness/harness/safety.py +387 -0
  72. package/harness/harness/scaffold.py +129 -0
  73. package/harness/harness/shard.py +63 -0
  74. package/harness/harness/shift.py +348 -0
  75. package/harness/harness/task.py +507 -0
  76. package/harness/harness/tree.py +258 -0
  77. package/harness/harness/version.py +305 -0
  78. package/harness/harness/wrap.py +217 -0
  79. package/harness/hooks/guard.py +259 -0
  80. package/harness/presets/appchy/PRESET.md +717 -0
  81. package/harness/presets/appchy/references/artifacts.md +539 -0
  82. package/harness/presets/appchy/references/graph.md +154 -0
  83. package/harness/presets/appchy/references/operations.md +444 -0
  84. package/harness/presets/appchy/references/research.md +216 -0
  85. package/harness/schema/work.config.schema.json +401 -0
  86. package/harness/test_work.py +5002 -0
  87. package/harness/work.py +534 -0
  88. package/package.json +34 -40
  89. package/bin/config-change.dev.mjs +0 -17
  90. package/bin/jarvis-dev.mjs +0 -30
  91. package/bin/pre-tool-use.dev.mjs +0 -23
  92. package/bin/session-start.dev.mjs +0 -17
  93. package/bin/stop.dev.mjs +0 -17
  94. package/bin/user-prompt-submit.dev.mjs +0 -17
  95. package/dev/bin.js +0 -38821
  96. package/dev/bin.js.map +0 -1
  97. package/dev/env.json +0 -1
  98. package/dev/hooks/config-change.js +0 -110
  99. package/dev/hooks/config-change.js.map +0 -1
  100. package/dev/hooks/pre-tool-use.js +0 -120
  101. package/dev/hooks/pre-tool-use.js.map +0 -1
  102. package/dev/hooks/session-start.js +0 -115
  103. package/dev/hooks/session-start.js.map +0 -1
  104. package/dev/hooks/stop.js +0 -112
  105. package/dev/hooks/stop.js.map +0 -1
  106. package/dev/hooks/user-prompt-submit.js +0 -111
  107. package/dev/hooks/user-prompt-submit.js.map +0 -1
  108. package/dist/env.json +0 -1
@@ -0,0 +1,462 @@
1
+ // ../../packages/data/src/embeddings.ts
2
+ async function buildEmbeddings(config, graph) {
3
+ const cfg = config.search;
4
+ if (!cfg) return void 0;
5
+ const embedder = cfg.embedder.resolve ? await cfg.embedder.resolve() : cfg.embedder;
6
+ const storeImpl = cfg.store;
7
+ const nodes = cfg.types ? cfg.types.flatMap((t) => graph.nodes(t)) : graph.nodes();
8
+ const ctx = { repoRoot: config.repoRoot };
9
+ const cache = await readCache(config, ctx, embedder.name);
10
+ const items = [];
11
+ const toEmbed = [];
12
+ for (const node of nodes) {
13
+ const text = searchText(node);
14
+ const hash = hashText(text);
15
+ const cached = cache.entries[node.id];
16
+ if (cached && cached.hash === hash) items.push({ id: node.id, vector: cached.vector });
17
+ else toEmbed.push({ id: node.id, text, hash });
18
+ }
19
+ const batchSize = Math.max(1, embedder.batchSize ?? DEFAULT_BATCH_SIZE);
20
+ const batches = [];
21
+ for (let i = 0; i < toEmbed.length; i += batchSize) batches.push(toEmbed.slice(i, i + batchSize));
22
+ let done = 0;
23
+ await pool(batches, Math.max(1, embedder.concurrency ?? 1), async (batch) => {
24
+ const out = await embedder.embed(batch.map((b) => b.text));
25
+ batch.forEach((b, j) => {
26
+ const vector = out[j] ?? [];
27
+ items.push({ id: b.id, vector });
28
+ cache.entries[b.id] = { hash: b.hash, vector };
29
+ });
30
+ done += 1;
31
+ process.stderr.write(
32
+ `[data] embedding ${done}/${batches.length} batch(es)${process.stderr.isTTY ? "\r" : "\n"}`
33
+ );
34
+ });
35
+ if (batches.length > 0) process.stderr.write(`[data] embedded ${toEmbed.length} node(s)
36
+ `);
37
+ const live = new Set(nodes.map((n) => n.id));
38
+ let pruned = 0;
39
+ for (const id of Object.keys(cache.entries)) {
40
+ if (!live.has(id)) {
41
+ delete cache.entries[id];
42
+ pruned += 1;
43
+ }
44
+ }
45
+ if (toEmbed.length > 0 || pruned > 0) await writeCache(config, ctx, cache);
46
+ await storeImpl.add(items);
47
+ return toRuntime(cfg, embedder, storeImpl, items);
48
+ }
49
+ var DEFAULT_BATCH_SIZE = 64;
50
+ async function pool(items, limit, work) {
51
+ let next = 0;
52
+ const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
53
+ for (let i = next++; i < items.length; i = next++) {
54
+ const item = items[i];
55
+ if (item !== void 0) await work(item);
56
+ }
57
+ });
58
+ await Promise.all(runners);
59
+ }
60
+ async function loadEmbeddings(config, graph) {
61
+ const cfg = config.search;
62
+ if (!cfg) return void 0;
63
+ const embedder = cfg.embedder.resolve ? await cfg.embedder.resolve() : cfg.embedder;
64
+ await probeEmbedder(embedder);
65
+ const storeImpl = cfg.store;
66
+ const nodes = cfg.types ? cfg.types.flatMap((t) => graph.nodes(t)) : graph.nodes();
67
+ const cache = await readCache(config, { repoRoot: config.repoRoot }, embedder.name);
68
+ const items = [];
69
+ for (const node of nodes) {
70
+ const cached = cache.entries[node.id];
71
+ if (cached) items.push({ id: node.id, vector: cached.vector });
72
+ }
73
+ if (nodes.length > 0 && items.length === 0) {
74
+ process.stderr.write(
75
+ `[data:warn] no cached vectors for ${nodes.length} node(s) under embedder ${embedder.name} \u2014 dense search is empty (stale cache or a different build-time embedder). Run \`jarvis build graph\`; lexical (BM25) still applies.
76
+ `
77
+ );
78
+ }
79
+ await storeImpl.add(items);
80
+ return toRuntime(cfg, embedder, storeImpl, items);
81
+ }
82
+ function toRuntime(cfg, embedder, storeImpl, items) {
83
+ return {
84
+ embedder,
85
+ store: storeImpl,
86
+ vectorById: new Map(items.map((it) => [it.id, it.vector])),
87
+ hybrid: cfg.hybrid !== false,
88
+ ...cfg.reranker ? { reranker: cfg.reranker } : {},
89
+ ...cfg.reranker?.limit !== void 0 ? { rerankLimit: cfg.reranker.limit } : {},
90
+ ...cfg.expand ? { expand: true } : {},
91
+ ...cfg.types ? { types: cfg.types } : {},
92
+ ...cfg.confidence ? { confidence: cfg.confidence } : {}
93
+ };
94
+ }
95
+ async function probeEmbedder(embedder) {
96
+ try {
97
+ await embedder.embed(["probe"]);
98
+ } catch (err) {
99
+ throw new Error(
100
+ `embedder ${embedder.name} unavailable \u2014 ${err instanceof Error ? err.message : String(err)}`
101
+ );
102
+ }
103
+ }
104
+ function searchText(node) {
105
+ const label = node.metadata.label ?? node.metadata.title;
106
+ const parts = [node.id, typeof label === "string" ? label : "", node.type];
107
+ const body = node.metadata.embedText ?? node.metadata.excerpt;
108
+ if (typeof body === "string") parts.push(body);
109
+ return parts.filter(Boolean).join(" \xB7 ");
110
+ }
111
+ var parsedCache = /* @__PURE__ */ new Map();
112
+ var cacheKey = (ctx, model) => `${ctx.repoRoot}::${model}`;
113
+ async function readCache(config, ctx, model) {
114
+ const held = parsedCache.get(cacheKey(ctx, model));
115
+ if (held) return held;
116
+ const fresh = await loadCache(config, ctx, model);
117
+ parsedCache.set(cacheKey(ctx, model), fresh);
118
+ return fresh;
119
+ }
120
+ async function loadCache(config, ctx, model) {
121
+ try {
122
+ const raw = await config.persistence.load(ctx, "embeddings");
123
+ if (raw) {
124
+ const parsed = JSON.parse(raw);
125
+ if (parsed.model === model && parsed.entries) return parsed;
126
+ }
127
+ } catch {
128
+ }
129
+ return { model, entries: {} };
130
+ }
131
+ async function writeCache(config, ctx, cache) {
132
+ parsedCache.set(cacheKey(ctx, cache.model), cache);
133
+ await config.persistence.save(ctx, "embeddings", `${JSON.stringify(cache)}
134
+ `);
135
+ }
136
+ function hashText(text) {
137
+ let h = 5381;
138
+ for (let i = 0; i < text.length; i++) h = (h << 5) + h + text.charCodeAt(i) | 0;
139
+ return (h >>> 0).toString(36);
140
+ }
141
+ function buildSearchRuntime(config, graph) {
142
+ return runSearchRuntime(config, graph, "building", buildEmbeddings);
143
+ }
144
+ function loadSearchRuntime(config, graph) {
145
+ return runSearchRuntime(config, graph, "loading", loadEmbeddings);
146
+ }
147
+ async function runSearchRuntime(config, graph, verb, make) {
148
+ const cfg = config.search;
149
+ if (!cfg) return void 0;
150
+ try {
151
+ process.stderr.write(`[data] ${verb} embeddings (${cfg.embedder.name})\u2026
152
+ `);
153
+ const runtime = await make(config, graph);
154
+ process.stderr.write(
155
+ `[data] semantic search ready (hybrid, ${verb === "loading" ? "loaded" : "built"})
156
+ `
157
+ );
158
+ return runtime;
159
+ } catch (err) {
160
+ process.stderr.write(
161
+ `[data:warn] semantic search unavailable \u2014 falling back to lexical (BM25) search. ${err instanceof Error ? err.message : String(err)}
162
+ `
163
+ );
164
+ return void 0;
165
+ }
166
+ }
167
+
168
+ // ../../packages/data/src/tokenize.ts
169
+ var STOPWORDS = /* @__PURE__ */ new Set([
170
+ "a",
171
+ "an",
172
+ "and",
173
+ "are",
174
+ "as",
175
+ "at",
176
+ "be",
177
+ "but",
178
+ "by",
179
+ "can",
180
+ "do",
181
+ "does",
182
+ "for",
183
+ "from",
184
+ "how",
185
+ "i",
186
+ "if",
187
+ "in",
188
+ "into",
189
+ "is",
190
+ "it",
191
+ "its",
192
+ "of",
193
+ "on",
194
+ "or",
195
+ "that",
196
+ "the",
197
+ "their",
198
+ "then",
199
+ "there",
200
+ "this",
201
+ "to",
202
+ "was",
203
+ "we",
204
+ "what",
205
+ "when",
206
+ "where",
207
+ "which",
208
+ "who",
209
+ "why",
210
+ "will",
211
+ "with",
212
+ "you",
213
+ "your"
214
+ ]);
215
+ function tokenize(text) {
216
+ const out = [];
217
+ for (const raw of text.split(/[\s_\-/.:]+/)) {
218
+ if (!raw) continue;
219
+ const parts = splitIdentifier(raw);
220
+ for (const p of parts) out.push(p.toLowerCase());
221
+ if (parts.length > 1) out.push(raw.toLowerCase());
222
+ }
223
+ return out;
224
+ }
225
+ function splitIdentifier(s) {
226
+ return s.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/([a-zA-Z])([0-9])/g, "$1 $2").replace(/([0-9])([a-zA-Z])/g, "$1 $2").split(/\s+/).filter(Boolean);
227
+ }
228
+
229
+ // ../../packages/data/src/search.ts
230
+ var SEED_COUNT = 10;
231
+ var FRONTIER_CAP = 50;
232
+ var RERANK_LIMIT = 50;
233
+ async function rankNodes(req) {
234
+ const { candidates, query, graph, runtime } = req;
235
+ const lexical = lexicalRank(candidates, query);
236
+ if (!runtime) return lexical;
237
+ const dense = await denseRank(runtime, candidates, query, graph);
238
+ let ranked = fuseRankings(lexical, dense, runtime.hybrid, candidates);
239
+ if ((req.expand ?? runtime.expand) && dense.length > 0) {
240
+ ranked = expandFrontier(ranked, graph);
241
+ }
242
+ if (req.deep && !isSingleTerm(query) && runtime.reranker && ranked.length > 0) {
243
+ ranked = await rerankHead(runtime.reranker, query, ranked, runtime.rerankLimit ?? RERANK_LIMIT);
244
+ }
245
+ return ranked;
246
+ }
247
+ function fuseRankings(lexical, dense, hybrid, candidates) {
248
+ if (dense.length === 0) return lexical;
249
+ if (!hybrid) return dense;
250
+ const fused = rrf({
251
+ lists: [lexical.map((r) => r.node.id), dense.map((r) => r.node.id)],
252
+ weights: [1, DENSE_FUSION_WEIGHT]
253
+ });
254
+ const byId = new Map(candidates.map((n) => [n.id, n]));
255
+ const lexicalById = new Map(lexical.map((r) => [r.node.id, r.score]));
256
+ const denseById = new Map(dense.map((r) => [r.node.id, r.score]));
257
+ const ranked = [];
258
+ for (const [id, score] of fused) {
259
+ const node = byId.get(id);
260
+ if (!node) continue;
261
+ const lex = lexicalById.get(id);
262
+ const den = denseById.get(id);
263
+ ranked.push({
264
+ node,
265
+ score,
266
+ ...lex !== void 0 ? { lexical: lex } : {},
267
+ ...den !== void 0 ? { dense: den } : {}
268
+ });
269
+ }
270
+ const pinned = ranked.filter((r) => (r.lexical ?? 0) >= IDENTIFIER_PIN_FLOOR).sort((a, b) => (b.lexical ?? 0) - (a.lexical ?? 0) || tieByNodeId(a, b));
271
+ const rest = ranked.filter((r) => (r.lexical ?? 0) < IDENTIFIER_PIN_FLOOR).sort((a, b) => b.score - a.score || tieByNodeId(a, b));
272
+ return [...pinned, ...rest];
273
+ }
274
+ function expandFrontier(ranked, graph) {
275
+ const present = new Set(ranked.map((r) => r.node.id));
276
+ const seeds = ranked.slice(0, SEED_COUNT);
277
+ const floor = seeds.length > 0 ? seeds[seeds.length - 1]?.score ?? 0 : 0;
278
+ const added = /* @__PURE__ */ new Map();
279
+ const communitiesDone = /* @__PURE__ */ new Set();
280
+ for (const seed of seeds) {
281
+ for (const nb of graph.neighbors(seed.node.id)) {
282
+ if (!present.has(nb.id) && !added.has(nb.id)) added.set(nb.id, nb);
283
+ }
284
+ const community = seed.node.metadata.community;
285
+ if (typeof community === "string" && community && !communitiesDone.has(community)) {
286
+ communitiesDone.add(community);
287
+ for (const m of graph.nodes()) {
288
+ if (m.metadata.community === community && !present.has(m.id) && !added.has(m.id))
289
+ added.set(m.id, m);
290
+ }
291
+ }
292
+ if (added.size >= FRONTIER_CAP) break;
293
+ }
294
+ const frontier = [...added.values()].slice(0, FRONTIER_CAP).map((node) => ({ node, score: floor * 0.5 }));
295
+ return [...ranked, ...frontier];
296
+ }
297
+ async function rerankHead(reranker, query, ranked, limit) {
298
+ const head = ranked.slice(0, limit);
299
+ const tail = ranked.slice(limit);
300
+ const hits = head.map((r) => ({
301
+ id: r.node.id,
302
+ text: searchText(r.node),
303
+ score: r.score
304
+ }));
305
+ let reordered;
306
+ try {
307
+ reordered = await reranker.rerank(query, hits, { topN: head.length });
308
+ } catch {
309
+ return ranked;
310
+ }
311
+ const byId = new Map(head.map((r) => [r.node.id, r]));
312
+ const out = [];
313
+ const seen = /* @__PURE__ */ new Set();
314
+ for (const hit of reordered) {
315
+ const prior = byId.get(hit.id);
316
+ if (prior) {
317
+ out.push({ ...prior, score: hit.score ?? 0 });
318
+ seen.add(hit.id);
319
+ }
320
+ }
321
+ for (const r of head) if (!seen.has(r.node.id)) out.push(r);
322
+ return [...out, ...tail];
323
+ }
324
+ var BM25_K1 = 1.2;
325
+ var BM25_B = 0.25;
326
+ var EXACT_ID_BOOST = 1e3;
327
+ var SYMBOL_HIT_BOOST = 100;
328
+ var IDENTIFIER_PIN_FLOOR = SYMBOL_HIT_BOOST;
329
+ var DENSE_FUSION_WEIGHT = 3;
330
+ function lexicalRank(candidates, query) {
331
+ const raw = query.trim().toLowerCase();
332
+ if (!raw) return [];
333
+ const rawTerms = raw.split(/\s+/).filter(Boolean);
334
+ const single = rawTerms.length === 1;
335
+ let queryTerms = [...new Set(tokenize(query))];
336
+ if (!single) {
337
+ const stripped = queryTerms.filter((t) => !STOPWORDS.has(t));
338
+ if (stripped.length > 0) queryTerms = stripped;
339
+ }
340
+ const docs = candidates.map((node) => ({ node, tokens: tokenize(haystack(node)) }));
341
+ const total = docs.length;
342
+ const df = /* @__PURE__ */ new Map();
343
+ let lengthSum = 0;
344
+ for (const d of docs) {
345
+ lengthSum += d.tokens.length;
346
+ for (const t of new Set(d.tokens)) df.set(t, (df.get(t) ?? 0) + 1);
347
+ }
348
+ const avgdl = total > 0 ? lengthSum / total : 0;
349
+ const ranked = [];
350
+ for (const d of docs) {
351
+ const tf = /* @__PURE__ */ new Map();
352
+ for (const t of d.tokens) tf.set(t, (tf.get(t) ?? 0) + 1);
353
+ let score = 0;
354
+ for (const term of queryTerms) {
355
+ const f = tf.get(term);
356
+ if (!f) continue;
357
+ const n = df.get(term) ?? 0;
358
+ const idf = Math.log(1 + (total - n + 0.5) / (n + 0.5));
359
+ const norm = avgdl > 0 ? 1 - BM25_B + BM25_B * (d.tokens.length / avgdl) : 1;
360
+ score += idf * (f * (BM25_K1 + 1) / (f + BM25_K1 * norm));
361
+ }
362
+ score += identifierBoost(d.node, raw, rawTerms[0] ?? "", single);
363
+ if (score > 0) ranked.push({ node: d.node, score, lexical: score });
364
+ }
365
+ ranked.sort((a, b) => b.score - a.score || tieByNodeId(a, b));
366
+ return ranked;
367
+ }
368
+ function tieByNodeId(a, b) {
369
+ return a.node.id < b.node.id ? -1 : a.node.id > b.node.id ? 1 : 0;
370
+ }
371
+ function isSingleTerm(query) {
372
+ return query.trim().split(/\s+/).filter(Boolean).length === 1;
373
+ }
374
+ function diversify(ranked, key) {
375
+ const byKey = /* @__PURE__ */ new Map();
376
+ const order = [];
377
+ for (const r of ranked) {
378
+ const k = key(r.node);
379
+ const kept = byKey.get(k);
380
+ if (kept === void 0) {
381
+ byKey.set(k, { ...r });
382
+ order.push(k);
383
+ } else {
384
+ kept.collapsed = (kept.collapsed ?? 0) + 1;
385
+ }
386
+ }
387
+ const out = [];
388
+ for (const k of order) {
389
+ const r = byKey.get(k);
390
+ if (r) out.push(r);
391
+ }
392
+ return out;
393
+ }
394
+ var CONFIDENT_LEXICAL = 1;
395
+ var CONFIDENT_DENSE = 0.6;
396
+ function isConfident(r, confidence) {
397
+ const lexicalFloor = confidence?.lexical ?? CONFIDENT_LEXICAL;
398
+ const denseFloor = confidence?.dense ?? CONFIDENT_DENSE;
399
+ return (r.lexical ?? 0) >= lexicalFloor || (r.dense ?? 0) >= denseFloor;
400
+ }
401
+ function haystack(node) {
402
+ const label = node.metadata.label ?? node.metadata.title;
403
+ const parts = [node.id, typeof label === "string" ? label : "", node.type];
404
+ const community = node.metadata.community;
405
+ if (typeof community === "string") parts.push(community);
406
+ const excerpt = node.metadata.excerpt;
407
+ if (typeof excerpt === "string") parts.push(excerpt);
408
+ return parts.filter(Boolean).join(" ");
409
+ }
410
+ function identifierBoost(node, raw, term, single) {
411
+ const id = node.id.toLowerCase();
412
+ let boost = 0;
413
+ if (id === raw) boost += EXACT_ID_BOOST;
414
+ if (single && id !== raw) {
415
+ const label = node.metadata.label ?? node.metadata.title;
416
+ const labelText = typeof label === "string" ? label.toLowerCase() : "";
417
+ if (id.includes(term) || labelText.includes(term)) boost += SYMBOL_HIT_BOOST;
418
+ }
419
+ return boost;
420
+ }
421
+ async function denseRank(runtime, candidates, query, graph) {
422
+ const queryVector = await embedQuery(runtime.embedder, query);
423
+ const filterIds = new Set(candidates.map((n) => n.id));
424
+ const hits = await runtime.store.query(queryVector, { topK: 50, filterIds });
425
+ const ranked = [];
426
+ for (const hit of hits) {
427
+ const node = graph.node(hit.id);
428
+ if (node) ranked.push({ node, score: hit.score, dense: hit.score });
429
+ }
430
+ return ranked;
431
+ }
432
+ async function embedQuery(embedder, text) {
433
+ if (embedder.embedQuery) return embedder.embedQuery(text);
434
+ const [vector] = await embedder.embed([text]);
435
+ return vector ?? [];
436
+ }
437
+ function rrf(req) {
438
+ const { lists, k = 60, weights } = req;
439
+ const scores = /* @__PURE__ */ new Map();
440
+ lists.forEach((list, i) => {
441
+ const w = weights?.[i] ?? 1;
442
+ list.forEach((id, rank) => {
443
+ scores.set(id, (scores.get(id) ?? 0) + w / (k + rank + 1));
444
+ });
445
+ });
446
+ return scores;
447
+ }
448
+
449
+ export {
450
+ buildEmbeddings,
451
+ loadEmbeddings,
452
+ searchText,
453
+ buildSearchRuntime,
454
+ loadSearchRuntime,
455
+ rankNodes,
456
+ lexicalRank,
457
+ isSingleTerm,
458
+ diversify,
459
+ isConfident,
460
+ embedQuery,
461
+ rrf
462
+ };