@reposkein/mcp 0.8.0 → 0.9.0

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 (45) hide show
  1. package/binary-digests.json +4 -4
  2. package/dist/embed/batch.d.ts +47 -0
  3. package/dist/embed/batch.js +92 -0
  4. package/dist/embed/batch.js.map +1 -0
  5. package/dist/embed/cache.d.ts +22 -48
  6. package/dist/embed/cache.js +44 -164
  7. package/dist/embed/cache.js.map +1 -1
  8. package/dist/embed/corpusVectors.d.ts +20 -0
  9. package/dist/embed/corpusVectors.js +60 -0
  10. package/dist/embed/corpusVectors.js.map +1 -0
  11. package/dist/embed/provider.d.ts +21 -2
  12. package/dist/embed/provider.js.map +1 -1
  13. package/dist/embed/providers/http.d.ts +3 -2
  14. package/dist/embed/providers/http.js +14 -1
  15. package/dist/embed/providers/http.js.map +1 -1
  16. package/dist/embed/providers/voyage.d.ts +3 -2
  17. package/dist/embed/providers/voyage.js +50 -54
  18. package/dist/embed/providers/voyage.js.map +1 -1
  19. package/dist/embed/vectorStore.d.ts +89 -0
  20. package/dist/embed/vectorStore.js +267 -0
  21. package/dist/embed/vectorStore.js.map +1 -0
  22. package/dist/guard/caps.d.ts +3 -0
  23. package/dist/guard/caps.js +4 -1
  24. package/dist/guard/caps.js.map +1 -1
  25. package/dist/search/bm25f.d.ts +17 -1
  26. package/dist/search/bm25f.js +89 -53
  27. package/dist/search/bm25f.js.map +1 -1
  28. package/dist/store/GraphStore.d.ts +6 -1
  29. package/dist/store/JsonlGraphStore.d.ts +47 -10
  30. package/dist/store/JsonlGraphStore.js +129 -50
  31. package/dist/store/JsonlGraphStore.js.map +1 -1
  32. package/dist/store/Neo4jGraphStore.d.ts +11 -0
  33. package/dist/store/Neo4jGraphStore.js +29 -2
  34. package/dist/store/Neo4jGraphStore.js.map +1 -1
  35. package/dist/store/jsonlGraph.d.ts +22 -0
  36. package/dist/store/jsonlGraph.js +75 -18
  37. package/dist/store/jsonlGraph.js.map +1 -1
  38. package/dist/store/jsonlLines.d.ts +21 -0
  39. package/dist/store/jsonlLines.js +64 -0
  40. package/dist/store/jsonlLines.js.map +1 -0
  41. package/dist/tools/readCypher.js +9 -2
  42. package/dist/tools/readCypher.js.map +1 -1
  43. package/dist/tools/semanticFind.js +9 -6
  44. package/dist/tools/semanticFind.js.map +1 -1
  45. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  {
2
- "reposkein-indexer-darwin-arm64": "a9d796f0e331979c68d30087c2aece4fa042c324008736e8f08346747066a638",
3
- "reposkein-indexer-linux-arm64": "b7f57cd831a0b252a225bb535d5ae3b57b0571e43d5593e600e2baf0d7ac477e",
4
- "reposkein-indexer-linux-x64": "68c4ae3371ac35fb24198ea871f13dd8a0ccfed449eec166a4b1e488953eb5b0",
5
- "reposkein-indexer-win32-x64.exe": "b761e7bb308aea16c9e08d4c71cf130c353fe143d4d2c6c71e29e3c13e191119"
2
+ "reposkein-indexer-darwin-arm64": "5b6804c12a676c8028b4cbafd28782eda6d2d277b21da143dea8ea358a700f92",
3
+ "reposkein-indexer-linux-arm64": "96ff841ca329ae49548a009dee316640be81eb8c25caed019414db5717228b91",
4
+ "reposkein-indexer-linux-x64": "de1dc2f52747332fa5da81b2dd75baaa2dac5428dc4d42643e742369f5b2f813",
5
+ "reposkein-indexer-win32-x64.exe": "e4fe5a208ae29cf2ef4101a017883412fa6cb00745e325a146381c92c6d0cb48"
6
6
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The batching seam for embeddings.
3
+ *
4
+ * An EmbeddingProvider can only express ONE request (embedBatch). Everything
5
+ * that decides how much work a request may carry lives here, so no adapter can
6
+ * opt out of the bound — the local HTTP adapter used to post an entire cold
7
+ * corpus in a single request (~5 GB at 50k symbols), because chunking was
8
+ * adapter-local and only the cloud adapter implemented it.
9
+ *
10
+ * Batches are handed to the caller as they complete, never accumulated, so peak
11
+ * memory on the embed path is proportional to one batch rather than the corpus.
12
+ *
13
+ * Config env vars (both optional; may only LOWER a provider's declared limits):
14
+ * REPOSKEIN_EMBED_MAX_BATCH_ITEMS max texts per request
15
+ * REPOSKEIN_EMBED_MAX_BATCH_TOKENS max estimated tokens per request
16
+ */
17
+ import type { BatchLimits, EmbeddingProvider, EmbedKind } from "./provider.js";
18
+ /**
19
+ * Deterministic token estimate: ~4 characters per token, plus a small
20
+ * per-text overhead for the delimiters and special tokens a server adds.
21
+ *
22
+ * This is a memory budget, not a billing figure — it only has to be
23
+ * monotonic in length and never wildly under-count.
24
+ */
25
+ export declare function estimateTokens(text: string): number;
26
+ /**
27
+ * The limits actually applied to a provider: its own, optionally lowered by env.
28
+ *
29
+ * The env can only tighten. A provider's declared limit is a hard fact about
30
+ * its backend (Voyage rejects >1000 inputs); letting an env var exceed it would
31
+ * turn a memory knob into a source of 4xx errors.
32
+ */
33
+ export declare function resolveBatchLimits(provider: EmbeddingProvider, env?: NodeJS.ProcessEnv): BatchLimits;
34
+ /**
35
+ * Embed `texts` in bounded requests, handing each batch's vectors to `onBatch`
36
+ * as it arrives together with the offset into `texts` it starts at.
37
+ *
38
+ * Nothing is accumulated here: the caller decides what to keep. That is what
39
+ * lets embedCorpus persist progress per batch, so an interrupted cold run
40
+ * resumes instead of restarting.
41
+ *
42
+ * A text that exceeds maxTokens on its own is sent alone rather than dropped or
43
+ * split — the server truncates it if it must, and the loop always advances.
44
+ *
45
+ * Throws on the first provider failure; callers fall back to lexical.
46
+ */
47
+ export declare function embedInBatches(provider: EmbeddingProvider, texts: string[], kind: EmbedKind, onBatch: (offset: number, vectors: number[][]) => void | Promise<void>, env?: NodeJS.ProcessEnv): Promise<void>;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The batching seam for embeddings.
3
+ *
4
+ * An EmbeddingProvider can only express ONE request (embedBatch). Everything
5
+ * that decides how much work a request may carry lives here, so no adapter can
6
+ * opt out of the bound — the local HTTP adapter used to post an entire cold
7
+ * corpus in a single request (~5 GB at 50k symbols), because chunking was
8
+ * adapter-local and only the cloud adapter implemented it.
9
+ *
10
+ * Batches are handed to the caller as they complete, never accumulated, so peak
11
+ * memory on the embed path is proportional to one batch rather than the corpus.
12
+ *
13
+ * Config env vars (both optional; may only LOWER a provider's declared limits):
14
+ * REPOSKEIN_EMBED_MAX_BATCH_ITEMS max texts per request
15
+ * REPOSKEIN_EMBED_MAX_BATCH_TOKENS max estimated tokens per request
16
+ */
17
+ /**
18
+ * Deterministic token estimate: ~4 characters per token, plus a small
19
+ * per-text overhead for the delimiters and special tokens a server adds.
20
+ *
21
+ * This is a memory budget, not a billing figure — it only has to be
22
+ * monotonic in length and never wildly under-count.
23
+ */
24
+ export function estimateTokens(text) {
25
+ return Math.ceil(text.length / 4) + 2;
26
+ }
27
+ /** Read a positive-integer env override, or undefined when absent/malformed. */
28
+ function positiveInt(raw) {
29
+ if (raw === undefined)
30
+ return undefined;
31
+ const n = Number(raw);
32
+ if (!Number.isInteger(n) || n <= 0)
33
+ return undefined;
34
+ return n;
35
+ }
36
+ /**
37
+ * The limits actually applied to a provider: its own, optionally lowered by env.
38
+ *
39
+ * The env can only tighten. A provider's declared limit is a hard fact about
40
+ * its backend (Voyage rejects >1000 inputs); letting an env var exceed it would
41
+ * turn a memory knob into a source of 4xx errors.
42
+ */
43
+ export function resolveBatchLimits(provider, env = process.env) {
44
+ const declared = provider.limits();
45
+ const items = positiveInt(env["REPOSKEIN_EMBED_MAX_BATCH_ITEMS"]);
46
+ const tokens = positiveInt(env["REPOSKEIN_EMBED_MAX_BATCH_TOKENS"]);
47
+ return {
48
+ maxItems: items === undefined ? declared.maxItems : Math.min(items, declared.maxItems),
49
+ maxTokens: tokens === undefined ? declared.maxTokens : Math.min(tokens, declared.maxTokens),
50
+ };
51
+ }
52
+ /**
53
+ * Embed `texts` in bounded requests, handing each batch's vectors to `onBatch`
54
+ * as it arrives together with the offset into `texts` it starts at.
55
+ *
56
+ * Nothing is accumulated here: the caller decides what to keep. That is what
57
+ * lets embedCorpus persist progress per batch, so an interrupted cold run
58
+ * resumes instead of restarting.
59
+ *
60
+ * A text that exceeds maxTokens on its own is sent alone rather than dropped or
61
+ * split — the server truncates it if it must, and the loop always advances.
62
+ *
63
+ * Throws on the first provider failure; callers fall back to lexical.
64
+ */
65
+ export async function embedInBatches(provider, texts, kind, onBatch, env = process.env) {
66
+ if (texts.length === 0)
67
+ return;
68
+ const { maxItems, maxTokens } = resolveBatchLimits(provider, env);
69
+ let start = 0;
70
+ while (start < texts.length) {
71
+ let end = start;
72
+ let tokens = 0;
73
+ while (end < texts.length) {
74
+ const next = estimateTokens(texts[end]);
75
+ // Always take at least one text, however oversized, so the loop advances.
76
+ if (end > start && tokens + next > maxTokens)
77
+ break;
78
+ tokens += next;
79
+ end++;
80
+ if (end - start >= maxItems)
81
+ break;
82
+ }
83
+ const batch = texts.slice(start, end);
84
+ const vectors = await provider.embedBatch(batch, kind);
85
+ if (vectors.length !== batch.length) {
86
+ throw new Error(`Embedding provider returned ${vectors.length} vectors for a batch of ${batch.length} texts — count mismatch`);
87
+ }
88
+ await onBatch(start, vectors);
89
+ start = end;
90
+ }
91
+ }
92
+ //# sourceMappingURL=batch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batch.js","sourceRoot":"","sources":["../../src/embed/batch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AACxC,CAAC;AAED,gFAAgF;AAChF,SAAS,WAAW,CAAC,GAAuB;IAC1C,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACrD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,QAA2B,EAC3B,MAAyB,OAAO,CAAC,GAAG;IAEpC,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;IACnC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,kCAAkC,CAAC,CAAC,CAAC;IACpE,OAAO;QACL,QAAQ,EAAE,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC;QACtF,SAAS,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,SAAS,CAAC;KAC5F,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,QAA2B,EAC3B,KAAe,EACf,IAAe,EACf,OAAsE,EACtE,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAC/B,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,kBAAkB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAElE,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,OAAO,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QAC5B,IAAI,GAAG,GAAG,KAAK,CAAC;QAChB,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,OAAO,GAAG,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;YAC1B,MAAM,IAAI,GAAG,cAAc,CAAC,KAAK,CAAC,GAAG,CAAE,CAAC,CAAC;YACzC,0EAA0E;YAC1E,IAAI,GAAG,GAAG,KAAK,IAAI,MAAM,GAAG,IAAI,GAAG,SAAS;gBAAE,MAAM;YACpD,MAAM,IAAI,IAAI,CAAC;YACf,GAAG,EAAE,CAAC;YACN,IAAI,GAAG,GAAG,KAAK,IAAI,QAAQ;gBAAE,MAAM;QACrC,CAAC;QAED,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;QACtC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,UAAU,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACvD,IAAI,OAAO,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,EAAE,CAAC;YACpC,MAAM,IAAI,KAAK,CACb,+BAA+B,OAAO,CAAC,MAAM,2BAA2B,KAAK,CAAC,MAAM,yBAAyB,CAC9G,CAAC;QACJ,CAAC;QACD,MAAM,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC9B,KAAK,GAAG,GAAG,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -1,35 +1,34 @@
1
1
  /**
2
- * Derived embedding cache for semantic_find.
2
+ * How a corpus node becomes a document string, and how that string is keyed.
3
3
  *
4
- * Vectors are stored in `.reposkein/local/embeddings/<providerId>__<modelId>__d<dims>.jsonl`
5
- * — gitignored, never committed, never required.
4
+ * Storage itself lives in vectorStore.ts — fixed-stride float32 beside a JSONL
5
+ * index and the orchestration in corpusVectors.ts. This file is only the
6
+ * text: what we embed, and the hash that decides when to re-embed it.
6
7
  *
7
- * Cache key / invalidation:
8
- * 1. Filename encodes provider + model + dims (switching any → different file → miss).
9
- * 2. Per-row `doc_hash` must match hash of the freshly-built document string
10
- * (changes to qualified_name, signature, semantic_summary, or file_path re-embed).
11
- * Note: CorpusNode does not expose the committed content_hash, so doc_hash is the
12
- * sole per-row invalidation key. It covers all code/summary changes since the doc
13
- * string is built from the same fields (qualified_name + signature + summary + file_path).
14
- *
15
- * Mirrors the atomic-write + best-effort pattern from mcp/src/store/sidecar.ts.
16
- * Any I/O or provider failure in embedCorpus must NOT propagate — callers catch and
17
- * fall back to the lexical result.
8
+ * Invalidation: `doc_hash` must match the hash of a freshly-built document
9
+ * string, so a change to qualified_name, signature, semantic_summary or
10
+ * file_path re-embeds the node. CorpusNode does not expose the committed
11
+ * content_hash, so this is the sole per-row key — which is sufficient, since
12
+ * the document is built from exactly those fields.
18
13
  */
19
- import type { EmbeddingProvider } from "./provider.js";
20
14
  import type { CorpusNode } from "../store/GraphStore.js";
21
- /** One cached record per embedded node. */
22
- export interface EmbedRecord {
23
- id: string;
24
- /** doc_hash: SHA-256 of the embedded document string (invalidation key). */
25
- doc_hash: string;
26
- v: number[];
27
- }
28
15
  /** Build the document string for a corpus node (deterministic, same for all calls).
29
16
  * voyage-code-3 is code-specialized, so including code-ish context plays to its strength.
30
17
  * Document = qualified_name + optional signature + optional summary + file_path.
31
18
  */
32
- export declare function buildDocString(node: CorpusNode): string;
19
+ export declare function buildDocString(node: CorpusNode, budget?: number): string;
20
+ /**
21
+ * Longest document string we will hand a provider.
22
+ *
23
+ * This is a correctness bound, not a preference. The bundled embedding server
24
+ * rejects an over-long input with 413 (EMBED_MAX_INPUT_CHARS, default 8000),
25
+ * and one rejected document fails the whole embed call — so `semantic_find`
26
+ * would fall back to lexical, and keep falling back, because the offending
27
+ * document is never embedded and every later attempt reissues it. Keep this at
28
+ * or below the server's cap.
29
+ */
30
+ export declare const DEFAULT_DOC_CHAR_BUDGET = 8000;
31
+ export declare function docCharBudget(env?: NodeJS.ProcessEnv): number;
33
32
  /** SHA-256 of a string, hex-encoded. */
34
33
  export declare function sha256(s: string): string;
35
34
  /**
@@ -39,28 +38,3 @@ export declare function sha256(s: string): string;
39
38
  * the FS. Replace any run of unsafe characters with "_".
40
39
  */
41
40
  export declare function sanitizeModelId(modelId: string): string;
42
- /** Derive the cache file path for a given repo root + provider. */
43
- export declare function cachePath(repoPath: string, provider: EmbeddingProvider): string;
44
- /**
45
- * Load cache into a Map keyed by node id. Missing file → empty map. Best-effort.
46
- *
47
- * Row validation: any row whose `v` is not an array of finite numbers of the
48
- * correct length (matching provider.dims) is dropped — treated as a miss so
49
- * it will be re-embedded. Pass `expectedDims` to enable dim-length validation;
50
- * omit (undefined) to skip the length check (e.g. when the provider is not known
51
- * at load time — but the embedCorpus function always passes it).
52
- */
53
- export declare function loadCache(path: string, expectedDims?: number): Map<string, EmbedRecord>;
54
- /** Rewrite the cache file (sorted by id). Atomic: write to .tmp then rename. Best-effort. */
55
- export declare function saveCache(path: string, records: Map<string, EmbedRecord>): void;
56
- /**
57
- * Embed the corpus nodes using the provider, leveraging the cache for nodes
58
- * whose doc_hash hasn't changed.
59
- *
60
- * Returns a Map<id, vector> for all nodes in corpus.
61
- * Only embeds nodes whose cache entry is missing or stale.
62
- * Persists updated cache to disk.
63
- *
64
- * Throws on embedding failure — callers must catch and fall back to lexical.
65
- */
66
- export declare function embedCorpus(provider: EmbeddingProvider, repoPath: string, corpus: CorpusNode[]): Promise<Map<string, number[]>>;
@@ -1,36 +1,57 @@
1
1
  /**
2
- * Derived embedding cache for semantic_find.
2
+ * How a corpus node becomes a document string, and how that string is keyed.
3
3
  *
4
- * Vectors are stored in `.reposkein/local/embeddings/<providerId>__<modelId>__d<dims>.jsonl`
5
- * — gitignored, never committed, never required.
4
+ * Storage itself lives in vectorStore.ts — fixed-stride float32 beside a JSONL
5
+ * index and the orchestration in corpusVectors.ts. This file is only the
6
+ * text: what we embed, and the hash that decides when to re-embed it.
6
7
  *
7
- * Cache key / invalidation:
8
- * 1. Filename encodes provider + model + dims (switching any → different file → miss).
9
- * 2. Per-row `doc_hash` must match hash of the freshly-built document string
10
- * (changes to qualified_name, signature, semantic_summary, or file_path re-embed).
11
- * Note: CorpusNode does not expose the committed content_hash, so doc_hash is the
12
- * sole per-row invalidation key. It covers all code/summary changes since the doc
13
- * string is built from the same fields (qualified_name + signature + summary + file_path).
14
- *
15
- * Mirrors the atomic-write + best-effort pattern from mcp/src/store/sidecar.ts.
16
- * Any I/O or provider failure in embedCorpus must NOT propagate — callers catch and
17
- * fall back to the lexical result.
8
+ * Invalidation: `doc_hash` must match the hash of a freshly-built document
9
+ * string, so a change to qualified_name, signature, semantic_summary or
10
+ * file_path re-embeds the node. CorpusNode does not expose the committed
11
+ * content_hash, so this is the sole per-row key — which is sufficient, since
12
+ * the document is built from exactly those fields.
18
13
  */
19
- import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync, mkdirSync } from "node:fs";
20
- import { join, dirname } from "node:path";
21
14
  import { createHash } from "node:crypto";
22
15
  /** Build the document string for a corpus node (deterministic, same for all calls).
23
16
  * voyage-code-3 is code-specialized, so including code-ish context plays to its strength.
24
17
  * Document = qualified_name + optional signature + optional summary + file_path.
25
18
  */
26
- export function buildDocString(node) {
27
- const parts = [node.qualified_name];
19
+ export function buildDocString(node, budget = docCharBudget()) {
20
+ const head = [node.qualified_name];
28
21
  if (node.signature)
29
- parts.push(node.signature);
30
- if (node.summary)
31
- parts.push(node.summary);
32
- parts.push(node.file_path);
33
- return parts.join("\n");
22
+ head.push(node.signature);
23
+ const tail = [node.file_path];
24
+ // Trim the SUMMARY to fit, not the tail. It is the one unbounded field —
25
+ // agents author it — and the identifiers are what a search matches on.
26
+ const overhead = [...head, ...tail].join("\n").length + (node.summary ? 1 : 0);
27
+ let summary = node.summary ?? "";
28
+ if (overhead + summary.length > budget) {
29
+ summary = summary.slice(0, Math.max(0, budget - overhead));
30
+ }
31
+ const parts = [...head];
32
+ if (summary)
33
+ parts.push(summary);
34
+ parts.push(...tail);
35
+ // Last resort, for a node whose own name and path already exceed the budget.
36
+ return parts.join("\n").slice(0, budget);
37
+ }
38
+ /**
39
+ * Longest document string we will hand a provider.
40
+ *
41
+ * This is a correctness bound, not a preference. The bundled embedding server
42
+ * rejects an over-long input with 413 (EMBED_MAX_INPUT_CHARS, default 8000),
43
+ * and one rejected document fails the whole embed call — so `semantic_find`
44
+ * would fall back to lexical, and keep falling back, because the offending
45
+ * document is never embedded and every later attempt reissues it. Keep this at
46
+ * or below the server's cap.
47
+ */
48
+ export const DEFAULT_DOC_CHAR_BUDGET = 8000;
49
+ export function docCharBudget(env = process.env) {
50
+ const raw = env["REPOSKEIN_EMBED_MAX_INPUT_CHARS"];
51
+ if (raw === undefined)
52
+ return DEFAULT_DOC_CHAR_BUDGET;
53
+ const n = Number(raw);
54
+ return Number.isInteger(n) && n > 0 ? n : DEFAULT_DOC_CHAR_BUDGET;
34
55
  }
35
56
  /** SHA-256 of a string, hex-encoded. */
36
57
  export function sha256(s) {
@@ -47,145 +68,4 @@ export function sanitizeModelId(modelId) {
47
68
  // Also collapse multiple consecutive underscores to avoid "___"-confusing names.
48
69
  return modelId.replace(/[/\\:*?"<>|\x00-\x1f]+/g, "_");
49
70
  }
50
- /** Derive the cache file path for a given repo root + provider. */
51
- export function cachePath(repoPath, provider) {
52
- const safeModel = sanitizeModelId(provider.modelId());
53
- const name = `${provider.id()}__${safeModel}__d${provider.dims()}`;
54
- return join(repoPath, ".reposkein", "local", "embeddings", `${name}.jsonl`);
55
- }
56
- /**
57
- * Load cache into a Map keyed by node id. Missing file → empty map. Best-effort.
58
- *
59
- * Row validation: any row whose `v` is not an array of finite numbers of the
60
- * correct length (matching provider.dims) is dropped — treated as a miss so
61
- * it will be re-embedded. Pass `expectedDims` to enable dim-length validation;
62
- * omit (undefined) to skip the length check (e.g. when the provider is not known
63
- * at load time — but the embedCorpus function always passes it).
64
- */
65
- export function loadCache(path, expectedDims) {
66
- const map = new Map();
67
- if (!existsSync(path))
68
- return map;
69
- let text;
70
- try {
71
- text = readFileSync(path, "utf8");
72
- }
73
- catch {
74
- return map;
75
- }
76
- for (const line of text.split("\n")) {
77
- if (line.trim() === "")
78
- continue;
79
- try {
80
- const o = JSON.parse(line);
81
- if (typeof o["id"] === "string" &&
82
- typeof o["doc_hash"] === "string" &&
83
- Array.isArray(o["v"])) {
84
- const v = o["v"];
85
- // Validate: every element must be a finite number
86
- const allFinite = v.every((x) => typeof x === "number" && isFinite(x));
87
- if (!allFinite)
88
- continue; // corrupt row — drop it (will be re-embedded)
89
- // Validate: length must match the expected dims (when known)
90
- if (expectedDims !== undefined && v.length !== expectedDims)
91
- continue;
92
- map.set(o["id"], {
93
- id: o["id"],
94
- doc_hash: o["doc_hash"],
95
- v: v,
96
- });
97
- }
98
- }
99
- catch {
100
- // skip malformed line
101
- }
102
- }
103
- return map;
104
- }
105
- /** Rewrite the cache file (sorted by id). Atomic: write to .tmp then rename. Best-effort. */
106
- export function saveCache(path, records) {
107
- const lines = [...records.keys()].sort().map((id) => {
108
- const r = records.get(id);
109
- return JSON.stringify({ id: r.id, doc_hash: r.doc_hash, v: r.v });
110
- });
111
- const tmp = `${path}.tmp`;
112
- try {
113
- mkdirSync(dirname(path), { recursive: true });
114
- writeFileSync(tmp, lines.length ? lines.join("\n") + "\n" : "");
115
- renameSync(tmp, path);
116
- }
117
- catch {
118
- // best-effort; write failure must not break the tool call
119
- try {
120
- if (existsSync(tmp))
121
- unlinkSync(tmp);
122
- }
123
- catch { /* ignore */ }
124
- }
125
- }
126
- /**
127
- * Embed the corpus nodes using the provider, leveraging the cache for nodes
128
- * whose doc_hash hasn't changed.
129
- *
130
- * Returns a Map<id, vector> for all nodes in corpus.
131
- * Only embeds nodes whose cache entry is missing or stale.
132
- * Persists updated cache to disk.
133
- *
134
- * Throws on embedding failure — callers must catch and fall back to lexical.
135
- */
136
- export async function embedCorpus(provider, repoPath, corpus) {
137
- const path = cachePath(repoPath, provider);
138
- const cache = loadCache(path, provider.dims());
139
- // Compute document strings + hashes for all corpus nodes
140
- const docStrings = new Map();
141
- const docHashes = new Map();
142
- for (const node of corpus) {
143
- const doc = buildDocString(node);
144
- docStrings.set(node.id, doc);
145
- docHashes.set(node.id, sha256(doc));
146
- }
147
- // Identify which nodes need embedding (cache miss or stale doc_hash)
148
- const toEmbed = [];
149
- for (const node of corpus) {
150
- const cached = cache.get(node.id);
151
- const isHit = cached !== undefined && cached.doc_hash === docHashes.get(node.id);
152
- if (!isHit) {
153
- toEmbed.push(node);
154
- }
155
- }
156
- // Embed only misses
157
- if (toEmbed.length > 0) {
158
- const texts = toEmbed.map((n) => docStrings.get(n.id));
159
- const vectors = await provider.embed(texts, "document");
160
- // Guard: provider must return exactly as many vectors as we sent.
161
- // A count mismatch means the provider is broken or mis-aligned — throw so
162
- // the caller's try/catch falls back to lexical. Never write corrupt data.
163
- if (vectors.length !== toEmbed.length) {
164
- throw new Error(`Embedding provider returned ${vectors.length} vectors for ${toEmbed.length} texts — count mismatch; refusing to cache`);
165
- }
166
- const expectedDims = provider.dims();
167
- for (let i = 0; i < toEmbed.length; i++) {
168
- const vec = vectors[i];
169
- if (!Array.isArray(vec) || vec.length !== expectedDims) {
170
- throw new Error(`Embedding provider returned a vector with ${Array.isArray(vec) ? vec.length : "undefined"} dims at index ${i}; expected ${expectedDims} — refusing to cache`);
171
- }
172
- const node = toEmbed[i];
173
- cache.set(node.id, {
174
- id: node.id,
175
- doc_hash: docHashes.get(node.id),
176
- v: vec,
177
- });
178
- }
179
- saveCache(path, cache);
180
- }
181
- // Build result map for all corpus nodes
182
- const result = new Map();
183
- for (const node of corpus) {
184
- const rec = cache.get(node.id);
185
- if (rec) {
186
- result.set(node.id, rec.v);
187
- }
188
- }
189
- return result;
190
- }
191
71
  //# sourceMappingURL=cache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/embed/cache.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/embed/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,IAAgB,EAAE,MAAM,GAAG,aAAa,EAAE;IACvE,MAAM,IAAI,GAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC9C,MAAM,IAAI,GAAa,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAExC,yEAAyE;IACzE,uEAAuE;IACvE,MAAM,QAAQ,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/E,IAAI,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;IACjC,IAAI,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QACvC,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACxB,IAAI,OAAO;QAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjC,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC;IACpB,6EAA6E;IAC7E,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,CAAC;AAE5C,MAAM,UAAU,aAAa,CAAC,MAAyB,OAAO,CAAC,GAAG;IAChE,MAAM,GAAG,GAAG,GAAG,CAAC,iCAAiC,CAAC,CAAC;IACnD,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,uBAAuB,CAAC;IACtD,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IACtB,OAAO,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,uBAAuB,CAAC;AACpE,CAAC;AAED,wCAAwC;AACxC,MAAM,UAAU,MAAM,CAAC,CAAS;IAC9B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,oEAAoE;IACpE,iFAAiF;IACjF,OAAO,OAAO,CAAC,OAAO,CAAC,yBAAyB,EAAE,GAAG,CAAC,CAAC;AACzD,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Corpus vectors for `semantic_find`, behind the vector store.
3
+ *
4
+ * Replaces the JSON cache's "hand me every vector as number[]" shape. Callers
5
+ * get a store they can ask for a ranked top-k; they never hold the corpus.
6
+ */
7
+ import type { EmbeddingProvider } from "./provider.js";
8
+ import { type VectorStore } from "./vectorStore.js";
9
+ import type { CorpusNode } from "../store/GraphStore.js";
10
+ /** Base path (no extension) for a provider's vectors in this repo. */
11
+ export declare function vectorBasePath(repoPath: string, provider: EmbeddingProvider): string;
12
+ /**
13
+ * Ensure every corpus node has a current vector, and return the store.
14
+ *
15
+ * Only misses are embedded, in bounded batches, each flushed as it lands so an
16
+ * interrupted cold run resumes rather than restarting.
17
+ *
18
+ * Throws on embedding failure — callers catch and fall back to lexical.
19
+ */
20
+ export declare function ensureCorpusVectors(provider: EmbeddingProvider, repoPath: string, corpus: CorpusNode[]): Promise<VectorStore>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Corpus vectors for `semantic_find`, behind the vector store.
3
+ *
4
+ * Replaces the JSON cache's "hand me every vector as number[]" shape. Callers
5
+ * get a store they can ask for a ranked top-k; they never hold the corpus.
6
+ */
7
+ import { join } from "node:path";
8
+ import { embedInBatches } from "./batch.js";
9
+ import { buildDocString, sanitizeModelId, sha256 } from "./cache.js";
10
+ import { openVectorStore, removeLegacyJsonCache } from "./vectorStore.js";
11
+ /** Base path (no extension) for a provider's vectors in this repo. */
12
+ export function vectorBasePath(repoPath, provider) {
13
+ const name = `${provider.id()}__${sanitizeModelId(provider.modelId())}__d${provider.dims()}`;
14
+ return join(repoPath, ".reposkein", "local", "embeddings", name);
15
+ }
16
+ /**
17
+ * Ensure every corpus node has a current vector, and return the store.
18
+ *
19
+ * Only misses are embedded, in bounded batches, each flushed as it lands so an
20
+ * interrupted cold run resumes rather than restarting.
21
+ *
22
+ * Throws on embedding failure — callers catch and fall back to lexical.
23
+ */
24
+ export async function ensureCorpusVectors(provider, repoPath, corpus) {
25
+ const base = vectorBasePath(repoPath, provider);
26
+ // The pre-binary cache is superseded, and at 200k nodes it was over 4 GB of
27
+ // dead JSON. Drop it the first time we write the store beside it.
28
+ removeLegacyJsonCache(base);
29
+ const store = openVectorStore(base, provider.dims());
30
+ const docs = new Map();
31
+ const hashes = new Map();
32
+ const misses = [];
33
+ for (const node of corpus) {
34
+ const doc = buildDocString(node);
35
+ docs.set(node.id, doc);
36
+ const hash = sha256(doc);
37
+ hashes.set(node.id, hash);
38
+ if (store.docHash(node.id) !== hash)
39
+ misses.push(node);
40
+ }
41
+ if (misses.length === 0)
42
+ return store;
43
+ const texts = misses.map((n) => docs.get(n.id));
44
+ const expectedDims = provider.dims();
45
+ await embedInBatches(provider, texts, "document", (offset, vectors) => {
46
+ const batch = [];
47
+ for (let i = 0; i < vectors.length; i++) {
48
+ const vec = vectors[i];
49
+ if (!Array.isArray(vec) || vec.length !== expectedDims) {
50
+ throw new Error(`Embedding provider returned a vector with ${Array.isArray(vec) ? vec.length : "undefined"} dims at index ${offset + i}; expected ${expectedDims} — refusing to store`);
51
+ }
52
+ const node = misses[offset + i];
53
+ batch.push({ id: node.id, docHash: hashes.get(node.id), vector: vec });
54
+ }
55
+ store.upsertMany(batch);
56
+ store.flush();
57
+ });
58
+ return store;
59
+ }
60
+ //# sourceMappingURL=corpusVectors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"corpusVectors.js","sourceRoot":"","sources":["../../src/embed/corpusVectors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAoB,MAAM,kBAAkB,CAAC;AAG5F,sEAAsE;AACtE,MAAM,UAAU,cAAc,CAAC,QAAgB,EAAE,QAA2B;IAC1E,MAAM,IAAI,GAAG,GAAG,QAAQ,CAAC,EAAE,EAAE,KAAK,eAAe,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;IAC7F,OAAO,IAAI,CAAC,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,IAAI,CAAC,CAAC;AACnE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,QAA2B,EAC3B,QAAgB,EAChB,MAAoB;IAEpB,MAAM,IAAI,GAAG,cAAc,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;IAChD,4EAA4E;IAC5E,kEAAkE;IAClE,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAE5B,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAErD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,KAAK,MAAM,IAAI,IAAI,MAAM,EAAE,CAAC;QAC1B,MAAM,GAAG,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QACvB,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACzB,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QAC1B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI;YAAE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAEtC,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAE,CAAC,CAAC;IACjD,MAAM,YAAY,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC;IAErC,MAAM,cAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE;QACpE,MAAM,KAAK,GAAG,EAAE,CAAC;QACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACxC,MAAM,GAAG,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;YACvB,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,YAAY,EAAE,CAAC;gBACvD,MAAM,IAAI,KAAK,CACb,6CAA6C,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,kBAAkB,MAAM,GAAG,CAAC,cAAc,YAAY,sBAAsB,CACvK,CAAC;YACJ,CAAC;YACD,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;YACjC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAE,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACxB,KAAK,CAAC,KAAK,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -12,6 +12,19 @@
12
12
  * REPOSKEIN_EMBED_URL base URL for http provider
13
13
  */
14
14
  export type EmbedKind = "document" | "query";
15
+ /**
16
+ * What a single request to this provider may carry.
17
+ *
18
+ * The shared batcher (embed/batch.ts) enforces these; an adapter never chunks
19
+ * for itself. `maxTokens` is measured with estimateTokens, a deterministic
20
+ * length heuristic — it is a memory budget, not a billing figure.
21
+ */
22
+ export interface BatchLimits {
23
+ /** Max texts in one request. */
24
+ maxItems: number;
25
+ /** Max estimated tokens across one request's texts. */
26
+ maxTokens: number;
27
+ }
15
28
  export interface EmbeddingProvider {
16
29
  /** Stable provider id, e.g. "voyage". Forms part of the cache key. */
17
30
  id(): string;
@@ -19,13 +32,19 @@ export interface EmbeddingProvider {
19
32
  modelId(): string;
20
33
  /** Output vector dimensionality, e.g. 1024. Part of the cache key. */
21
34
  dims(): number;
35
+ /** Per-request limits. Callers go through embedInBatches, which enforces them. */
36
+ limits(): BatchLimits;
22
37
  /**
23
- * Embed a batch of texts.
38
+ * Embed ONE request's worth of texts. Implementations must NOT batch
39
+ * internally: the caller has already sized this call against limits().
40
+ * Splitting here would put the memory bound back inside each adapter, which
41
+ * is exactly how the local HTTP adapter came to send whole corpora at once.
42
+ *
24
43
  * kind maps to Voyage's input_type ("document" vs "query") — Voyage uses
25
44
  * asymmetric embeddings so this materially changes the vectors.
26
45
  * Returns one number[] per input, in order.
27
46
  */
28
- embed(texts: string[], kind: EmbedKind): Promise<number[][]>;
47
+ embedBatch(texts: string[], kind: EmbedKind): Promise<number[][]>;
29
48
  }
30
49
  /**
31
50
  * Build a provider from env vars, or null if embeddings are disabled (default).
@@ -1 +1 @@
1
- {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/embed/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAoBH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAyB,OAAO,CAAC,GAAG;IACxE,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,0BAA0B,CAAC,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/E,IAAI,YAAY,KAAK,MAAM,IAAI,YAAY,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAEhE,IAAI,YAAY,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,EAAE,uBAAuB,EAAE,GAAG,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC;QAC1E,OAAO,IAAI,uBAAuB,CAAC,GAAG,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,YAAY,KAAK,MAAM,EAAE,CAAC;QAC5B,MAAM,EAAE,qBAAqB,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;QACtE,OAAO,IAAI,qBAAqB,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,IAAI,KAAK,CACb,sCAAsC,YAAY,kCAAkC,CACrF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/embed/provider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAwCH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,MAAyB,OAAO,CAAC,GAAG;IACxE,MAAM,YAAY,GAAG,CAAC,GAAG,CAAC,0BAA0B,CAAC,IAAI,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC;IAC/E,IAAI,YAAY,KAAK,MAAM,IAAI,YAAY,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAEhE,IAAI,YAAY,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,EAAE,uBAAuB,EAAE,GAAG,MAAM,MAAM,CAAC,uBAAuB,CAAC,CAAC;QAC1E,OAAO,IAAI,uBAAuB,CAAC,GAAG,CAAC,CAAC;IAC1C,CAAC;IAED,IAAI,YAAY,KAAK,MAAM,EAAE,CAAC;QAC5B,MAAM,EAAE,qBAAqB,EAAE,GAAG,MAAM,MAAM,CAAC,qBAAqB,CAAC,CAAC;QACtE,OAAO,IAAI,qBAAqB,CAAC,GAAG,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,IAAI,KAAK,CACb,sCAAsC,YAAY,kCAAkC,CACrF,CAAC;AACJ,CAAC"}