doc2vec 2.15.1 → 2.17.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.
package/README.md CHANGED
@@ -267,6 +267,7 @@ Configuration is managed through two files:
267
267
  * `qdrant_url`: (Optional) URL of your Qdrant instance. Defaults to `http://localhost:6333`.
268
268
  * `qdrant_port`: (Optional) Port for the Qdrant REST API. Defaults to `443` if `qdrant_url` starts with `https`, otherwise `6333`.
269
269
  * `collection_name`: (Optional) Name of the Qdrant collection to use. Defaults to `<product_name>_<version>` (lowercased, spaces replaced with underscores).
270
+ * `keyword_index`: (Optional) `true` to also store a BM25 keyword vector per chunk, so the MCP server can run hybrid search (`HYBRID_SEARCH=true`). Needs Qdrant 1.18 or later. See [Qdrant keyword index](#qdrant-keyword-index).
270
271
 
271
272
  Optional embedding configuration:
272
273
  * `embedding.provider`: Provider for embeddings (`openai` or `azure`).
@@ -485,6 +486,70 @@ The script will then:
485
486
  7. Cleanup obsolete chunks.
486
487
  8. Output detailed logs.
487
488
 
489
+ ### Evaluating retrieval quality
490
+
491
+ `doc2vec eval` measures how well a SQLite database answers real questions, so a change to the embedding model, chunking or ranking can be judged by numbers. Each case is a question plus the URLs that should come back; the command embeds the question with the same model as the database, runs the same nearest-neighbour query as the MCP server, and reports recall@1/4/10 (share of questions with a relevant chunk in the top k) and MRR (mean reciprocal rank of the first relevant chunk).
492
+
493
+ ```bash
494
+ doc2vec eval cases.jsonl --db ./istio.db --config config.yaml --top-k 10 --out report.json
495
+ ```
496
+
497
+ Cases can be `.jsonl` (one per line), `.json` (an array) or `.yaml`:
498
+
499
+ ```json
500
+ {"id": "mtls-1", "query": "how do I enable strict mTLS for one namespace?", "relevant_urls": ["https://istio.io/latest/docs/tasks/security/authentication/mtls-migration/"], "product_name": "istio", "version": "latest"}
501
+ ```
502
+
503
+ `db`, `product_name` and `version` are optional per case. URLs match ignoring `#fragments`, trailing slashes and case. `--config` reads only the `embedding` section; without it the `OPENAI_*` / `AZURE_OPENAI_*` env vars are used.
504
+
505
+ Search methods can be compared on the same database without re-ingesting it:
506
+
507
+ - `--mode dense` (default): nearest chunks by embedding, the MCP server's query.
508
+ - `--mode bm25`: keyword ranking with SQLite FTS5, built in memory from the stored chunk text.
509
+ - `--mode hybrid`: dense and BM25 lists fused with Reciprocal Rank Fusion.
510
+ - `--per-page`: keep only the best chunk of each page, so the top results cover more pages.
511
+ - `--rerank deepinfra`: rerank the top `--rerank-candidates` chunks (default 40) with `--rerank-model` (default `nvidia/Nemotron-3.5-Rerank-8B-BF16`); needs `DEEPINFRA_API_KEY`.
512
+
513
+ These match the MCP server's retrieval settings (`HYBRID_SEARCH`, `ONE_CHUNK_PER_PAGE`, `RERANKER`; see `mcp/README.md`), so a configuration can be measured before it is deployed.
514
+
515
+ Results of comparing embedding models, search methods and rerankers are in [RETRIEVAL_ROADMAP.md](RETRIEVAL_ROADMAP.md).
516
+
517
+ ## Docker Images and the Browser
518
+
519
+ Website sources are rendered with a real browser (Puppeteer). The `Dockerfile` builds two images so you can choose where that browser runs:
520
+
521
+ | Target | Contents | Use it when |
522
+ |---|---|---|
523
+ | `runtime` | doc2vec only, no browser | You run a browser elsewhere and point doc2vec at it (see below). Recommended for clusters: the browser is the one component that renders untrusted input, and it can be updated on its own schedule. |
524
+ | `runtime-chrome` | doc2vec plus a bundled, version-matched Chrome | You want a single self-contained container. `linux/amd64` only. |
525
+
526
+ ```bash
527
+ docker build --target runtime -t doc2vec .
528
+ docker build --target runtime-chrome -t doc2vec:chrome .
529
+ ```
530
+
531
+ Both images are based on Wolfi (Chainguard) and run as the non-root user `doc2vec` (UID 14000).
532
+
533
+ ### Using a remote browser (`BROWSER_WS_ENDPOINT`)
534
+
535
+ Set `BROWSER_WS_ENDPOINT` to a Chrome DevTools Protocol WebSocket URL and doc2vec connects to that browser instead of launching one. Any CDP-speaking browser works; [browserless](https://github.com/browserless/browserless) is a convenient packaged one:
536
+
537
+ ```bash
538
+ docker run -d --name browser -e TOKEN=secret -e TIMEOUT=0 --shm-size=1g ghcr.io/browserless/chromium
539
+ docker run --rm --link browser \
540
+ -e BROWSER_WS_ENDPOINT='ws://browser:3000?token=secret' \
541
+ -e OPENAI_API_KEY \
542
+ -v "$PWD/config.yaml:/config/config.yaml:ro" \
543
+ doc2vec node dist/doc2vec.js /config/config.yaml
544
+ ```
545
+
546
+ Notes:
547
+
548
+ - A crawl holds a browser for as long as it runs, so disable or raise the browser service's session timeout (browserless: `TIMEOUT=0`).
549
+ - Each connection gets its own browser session; doc2vec closes it when the crawl finishes and every 50 pages.
550
+ - Launch flags such as `--no-sandbox` belong to the process that starts the browser and are not sent to a remote one.
551
+ - With `BROWSER_WS_ENDPOINT` unset, doc2vec launches a local Chrome: `PUPPETEER_EXECUTABLE_PATH` if set, else Puppeteer's downloaded Chrome for Testing, else a system `chromium`.
552
+
488
553
  ## Controller Mode
489
554
 
490
555
  Besides the one-shot sync, doc2vec can run as a **long-lived controller** that schedules sync jobs, records run history and statistics in Postgres, and serves a web UI for monitoring and managing configs:
@@ -591,12 +656,17 @@ spec:
591
656
  - name: OPENAI_API_KEY
592
657
  valueFrom:
593
658
  secretKeyRef: { name: doc2vec-secrets, key: openai-api-key }
659
+ # The browser runs in its own Deployment (e.g. ghcr.io/browserless/chromium
660
+ # with TIMEOUT=0); see "Docker Images and the Browser". Drop this variable
661
+ # and use the `-chrome` image to bundle the browser instead.
662
+ - name: BROWSER_WS_ENDPOINT
663
+ value: ws://browserless:3000?token=$(BROWSERLESS_TOKEN)
594
664
  volumeMounts:
595
665
  - name: configs
596
666
  mountPath: /etc/doc2vec
597
667
  resources:
598
668
  requests: { memory: 1Gi }
599
- limits: { memory: 4Gi } # website sources launch headless Chromium
669
+ limits: { memory: 2Gi }
600
670
  volumes:
601
671
  - name: configs
602
672
  configMap: { name: doc2vec-configs }
@@ -634,6 +704,21 @@ Server-sent events: `GET /api/events` (run and config updates) and `GET /api/run
634
704
  * Uses `@qdrant/js-client-rest`.
635
705
  * Requires `qdrant_url`, `qdrant_port`, `collection_name` and potentially `QDRANT_API_KEY`.
636
706
 
707
+ #### Qdrant point ids
708
+
709
+ Each chunk is stored as one point whose id comes from its page URL and its position on the page. Earlier versions derived the id from the chunk text, so the same text on two pages (a shared "Before you begin" section, for example) became a single point: one page lost the chunk, and both pages looked changed and were re-embedded on every sync.
710
+
711
+ After upgrading, the first sync of each **website** or **S3** source on Qdrant processes every page once (bypassing the lastmod/ETag or timestamp shortcuts) and rewrites only the pages whose chunks were merged; unchanged pages are not re-embedded. Local directories compare every file on each sync and repair themselves on the first run. GitHub issues, Zendesk and code chunks carry the issue, ticket or file in their text, so merges there are rare; any affected item is repaired the next time it changes. SQLite was never affected.
712
+
713
+ #### Qdrant keyword index
714
+
715
+ With `keyword_index: true`, each chunk is stored with two vectors: the embedding (the collection's unnamed vector, as before) and a sparse `bm25` vector that Qdrant computes from the chunk text with its built-in `qdrant/bm25` model (IDF applied at query time). The MCP server queries both when `HYBRID_SEARCH=true`.
716
+
717
+ * **New collections** are created with the `bm25` sparse vector.
718
+ * **Existing collections** get it added on the next sync (Qdrant 1.18+ vector schema update), and every chunk stored before then is backfilled from its stored text. Embeddings are not recomputed. The backfill only reads chunks that lack the vector, so it resumes if interrupted and costs one empty scroll once done.
719
+ * On a Qdrant older than 1.18 the sync logs a warning and stores chunks without the keyword vector.
720
+ * Turning the option off later stops writing the vector; the existing ones stay unused.
721
+
637
722
  ## PDF Processing
638
723
 
639
724
  Doc2Vec includes built-in support for processing PDF files in both local directories and websites. PDF files are automatically detected by their `.pdf` extension and processed using [Mozilla's PDF.js](https://github.com/mozilla/pdf.js) library.
@@ -36,7 +36,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.ContentProcessor = exports.BrowserLaunchError = void 0;
39
+ exports.ContentProcessor = exports.BROWSER_WS_ENDPOINT_ENV = exports.BrowserLaunchError = void 0;
40
+ exports.remoteBrowserEndpoint = remoteBrowserEndpoint;
41
+ exports.openBrowser = openBrowser;
40
42
  exports.resolveBrowserExecutablePath = resolveBrowserExecutablePath;
41
43
  const readability_1 = require("@mozilla/readability");
42
44
  const axios_1 = __importDefault(require("axios"));
@@ -62,6 +64,56 @@ class BrowserLaunchError extends Error {
62
64
  }
63
65
  }
64
66
  exports.BrowserLaunchError = BrowserLaunchError;
67
+ /**
68
+ * Where the browser comes from.
69
+ *
70
+ * Unset (the default): this process launches its own Chromium, which the image
71
+ * has to carry. Set: this process CONNECTS to a browser running elsewhere over
72
+ * the Chrome DevTools Protocol, and the image needs no browser at all. The
73
+ * value is a full CDP WebSocket URL, e.g.
74
+ * ws://browserless.qdrant.svc.cluster.local:3000?token=<token>
75
+ *
76
+ * A remote browser is the one that keeps the app image small and lets the
77
+ * browser be updated independently of the app (a browser image tracks Chrome
78
+ * stable; a distro chromium is frozen at whatever the distro rebuilt last).
79
+ */
80
+ exports.BROWSER_WS_ENDPOINT_ENV = 'BROWSER_WS_ENDPOINT';
81
+ function remoteBrowserEndpoint() {
82
+ const raw = process.env[exports.BROWSER_WS_ENDPOINT_ENV]?.trim();
83
+ return raw ? raw : undefined;
84
+ }
85
+ /**
86
+ * Chromium flags for a browser THIS process launches. They are not sent to a
87
+ * remote browser: launch flags belong to the process that starts the browser.
88
+ */
89
+ const LOCAL_LAUNCH_ARGS = [
90
+ '--no-sandbox',
91
+ '--disable-setuid-sandbox',
92
+ '--disable-dev-shm-usage', // Use /tmp instead of /dev/shm (critical in Docker, default 64MB)
93
+ '--disable-gpu',
94
+ '--disable-extensions',
95
+ ];
96
+ const BROWSER_PROTOCOL_TIMEOUT_MS = 60000;
97
+ /**
98
+ * Open a browser: connect to the remote one named by BROWSER_WS_ENDPOINT, or
99
+ * launch a local one. Callers close it with browser.close() in both cases —
100
+ * a browserless-style service gives each connection its own browser, so
101
+ * closing ends only this caller's session.
102
+ */
103
+ async function openBrowser() {
104
+ const endpoint = remoteBrowserEndpoint();
105
+ if (endpoint) {
106
+ return puppeteer_1.default.connect({
107
+ browserWSEndpoint: endpoint,
108
+ protocolTimeout: BROWSER_PROTOCOL_TIMEOUT_MS,
109
+ });
110
+ }
111
+ return puppeteer_1.default.launch({
112
+ executablePath: await resolveBrowserExecutablePath(),
113
+ args: LOCAL_LAUNCH_ARGS,
114
+ protocolTimeout: BROWSER_PROTOCOL_TIMEOUT_MS,
115
+ });
116
+ }
65
117
  /**
66
118
  * Resolve which browser binary to launch, in order of preference:
67
119
  * 1. PUPPETEER_EXECUTABLE_PATH (explicit override)
@@ -421,21 +473,9 @@ class ContentProcessor {
421
473
  let browser = null;
422
474
  let page = null;
423
475
  const launchBrowser = async () => {
424
- const executablePath = await resolveBrowserExecutablePath();
425
- const launchOptions = {
426
- executablePath,
427
- args: [
428
- '--no-sandbox',
429
- '--disable-setuid-sandbox',
430
- '--disable-dev-shm-usage', // Use /tmp instead of /dev/shm (critical in Docker, default 64MB)
431
- '--disable-gpu',
432
- '--disable-extensions',
433
- ],
434
- protocolTimeout: 60000,
435
- };
436
476
  let b;
437
477
  try {
438
- b = await puppeteer_1.default.launch(launchOptions);
478
+ b = await openBrowser();
439
479
  }
440
480
  catch (firstError) {
441
481
  // One retry after a pause — the failure may be transient (e.g.
@@ -445,7 +485,7 @@ class ContentProcessor {
445
485
  logger.warn(`Browser launch failed (${memoryDiagnostics()}), retrying once in 5s...`, firstError);
446
486
  await new Promise(resolve => setTimeout(resolve, 5000));
447
487
  try {
448
- b = await puppeteer_1.default.launch(launchOptions);
488
+ b = await openBrowser();
449
489
  }
450
490
  catch (secondError) {
451
491
  throw new BrowserLaunchError(`browser failed to launch twice (${memoryDiagnostics()}): ${secondError instanceof Error ? secondError.message : String(secondError)}`);
@@ -962,18 +1002,8 @@ class ContentProcessor {
962
1002
  page = existingPage;
963
1003
  }
964
1004
  else {
965
- // Standalone mode: launch a browser for this single page
966
- browser = await puppeteer_1.default.launch({
967
- executablePath: await resolveBrowserExecutablePath(),
968
- args: [
969
- '--no-sandbox',
970
- '--disable-setuid-sandbox',
971
- '--disable-dev-shm-usage',
972
- '--disable-gpu',
973
- '--disable-extensions',
974
- ],
975
- protocolTimeout: 60000,
976
- });
1005
+ // Standalone mode: open a browser for this single page
1006
+ browser = await openBrowser();
977
1007
  page = await browser.newPage();
978
1008
  }
979
1009
  logger.debug(`Navigating to ${url}`);
package/dist/database.js CHANGED
@@ -82,9 +82,24 @@ class DatabaseManager {
82
82
  const collectionName = params.collection_name || `${config.product_name.toLowerCase().replace(/\s+/g, '_')}_${config.version}`;
83
83
  logger.info(`Connecting to Qdrant at ${qdrantUrl}:${qdrantPort}, collection: ${collectionName}`);
84
84
  const qdrantClient = new js_client_rest_1.QdrantClient({ url: qdrantUrl, apiKey: process.env.QDRANT_API_KEY, port: qdrantPort });
85
- await this.createCollectionQdrant(qdrantClient, collectionName, logger, embeddingDimension);
85
+ await this.createCollectionQdrant(qdrantClient, collectionName, logger, embeddingDimension, !!params.keyword_index);
86
+ const connection = { client: qdrantClient, collectionName, type: 'qdrant' };
87
+ if (params.keyword_index) {
88
+ connection.keywordIndex = await this.ensureKeywordVectorQdrant(qdrantClient, collectionName, logger);
89
+ if (connection.keywordIndex) {
90
+ // Best-effort: a failed backfill must not fail the sync. New
91
+ // chunks still get the vector, and the next sync resumes
92
+ // with the chunks that lack it.
93
+ try {
94
+ await this.backfillKeywordVectorsQdrant(connection, logger);
95
+ }
96
+ catch (error) {
97
+ logger.warn(`Keyword vector backfill of ${collectionName} stopped early; it resumes on the next sync:`, error);
98
+ }
99
+ }
100
+ }
86
101
  logger.info(`Qdrant connection established successfully`);
87
- return { client: qdrantClient, collectionName, type: 'qdrant' };
102
+ return connection;
88
103
  }
89
104
  else {
90
105
  const errMsg = `Unsupported database type: ${dbConfig.type}`;
@@ -92,7 +107,71 @@ class DatabaseManager {
92
107
  throw new Error(errMsg);
93
108
  }
94
109
  }
95
- static async createCollectionQdrant(qdrantClient, collectionName, logger, embeddingDimension) {
110
+ static keywordDocument(text) {
111
+ return { text, model: DatabaseManager.KEYWORD_MODEL };
112
+ }
113
+ /**
114
+ * Adds the keyword vector to a collection that predates it. Needs Qdrant
115
+ * 1.18+ (vector schema updates); on an older server this logs a warning and
116
+ * returns false, and chunks are stored without the keyword vector.
117
+ */
118
+ static async ensureKeywordVectorQdrant(qdrantClient, collectionName, logger) {
119
+ try {
120
+ const info = await qdrantClient.getCollection(collectionName);
121
+ const sparse = info?.config?.params?.sparse_vectors ?? {};
122
+ if (sparse[DatabaseManager.KEYWORD_VECTOR]) {
123
+ return true;
124
+ }
125
+ logger.info(`Adding keyword vector '${DatabaseManager.KEYWORD_VECTOR}' to collection ${collectionName}`);
126
+ await qdrantClient.createVectorName(collectionName, DatabaseManager.KEYWORD_VECTOR, { sparse: { modifier: 'idf' } });
127
+ return true;
128
+ }
129
+ catch (error) {
130
+ logger.warn(`Could not enable the keyword index on ${collectionName} (needs Qdrant 1.18 or later); storing chunks without it:`, error);
131
+ return false;
132
+ }
133
+ }
134
+ /**
135
+ * Gives the keyword vector to every chunk stored before it existed. Only
136
+ * points without it are read, so an interrupted backfill resumes on the
137
+ * next sync and a finished one costs a single empty scroll.
138
+ */
139
+ static async backfillKeywordVectorsQdrant(db, logger) {
140
+ const { client, collectionName } = db;
141
+ const filter = {
142
+ must_not: [
143
+ { has_vector: DatabaseManager.KEYWORD_VECTOR },
144
+ { key: 'is_metadata', match: { value: true } },
145
+ ],
146
+ };
147
+ let updated = 0;
148
+ let offset = undefined;
149
+ do {
150
+ const page = await client.scroll(collectionName, {
151
+ filter,
152
+ limit: 256,
153
+ offset,
154
+ with_payload: ['content'],
155
+ with_vector: false,
156
+ });
157
+ const points = (page?.points ?? [])
158
+ .filter((p) => typeof p.payload?.content === 'string' && p.payload.content.trim().length > 0)
159
+ .map((p) => ({
160
+ id: p.id,
161
+ vector: { [DatabaseManager.KEYWORD_VECTOR]: DatabaseManager.keywordDocument(p.payload.content) },
162
+ }));
163
+ if (points.length > 0) {
164
+ await client.updateVectors(collectionName, { wait: true, points });
165
+ updated += points.length;
166
+ }
167
+ offset = page?.next_page_offset;
168
+ } while (offset !== undefined && offset !== null);
169
+ if (updated > 0) {
170
+ logger.info(`Backfilled the keyword vector for ${updated} chunks in ${collectionName}`);
171
+ }
172
+ return updated;
173
+ }
174
+ static async createCollectionQdrant(qdrantClient, collectionName, logger, embeddingDimension, keywordIndex = false) {
96
175
  try {
97
176
  logger.debug(`Checking if collection ${collectionName} exists`);
98
177
  const collections = await qdrantClient.getCollections();
@@ -107,6 +186,9 @@ class DatabaseManager {
107
186
  size: embeddingDimension,
108
187
  distance: "Cosine",
109
188
  },
189
+ ...(keywordIndex && {
190
+ sparse_vectors: { [DatabaseManager.KEYWORD_VECTOR]: { modifier: 'idf' } },
191
+ }),
110
192
  });
111
193
  logger.info(`Collection ${collectionName} created successfully`);
112
194
  }
@@ -425,23 +507,42 @@ class DatabaseManager {
425
507
  });
426
508
  transaction();
427
509
  }
510
+ /**
511
+ * One point per position on a page. chunk_id is a hash of the chunk text,
512
+ * so using it as the id merged the same text on two pages (or twice on one
513
+ * page) into a single point: one page lost the chunk, and both looked
514
+ * changed on every sync and were re-embedded. Every write path deletes a
515
+ * page's chunks by URL before inserting the new set, so (url, chunk_index)
516
+ * is unique and stable enough. Points written under the old scheme keep
517
+ * their ids until their page changes; deletes go by URL or by the ids read
518
+ * back, so the two schemes coexist.
519
+ */
520
+ static qdrantPointId(chunk) {
521
+ const { url, chunk_index, chunk_id } = chunk.metadata;
522
+ if (url && typeof chunk_index === 'number') {
523
+ return utils_1.Utils.hashToUuid(utils_1.Utils.generateHash(`${url}\u0000${chunk_index}`));
524
+ }
525
+ return utils_1.Utils.isValidUuid(chunk_id) ? chunk_id : utils_1.Utils.hashToUuid(chunk_id);
526
+ }
428
527
  static async storeChunkInQdrant(db, chunk, embedding, chunkHash) {
429
528
  const { client, collectionName } = db;
430
529
  try {
431
530
  let pointId;
432
531
  try {
433
- pointId = chunk.metadata.chunk_id;
434
- if (!utils_1.Utils.isValidUuid(pointId)) {
435
- pointId = utils_1.Utils.hashToUuid(chunk.metadata.chunk_id);
436
- }
532
+ pointId = DatabaseManager.qdrantPointId(chunk);
437
533
  }
438
534
  catch (e) {
439
535
  pointId = crypto_1.default.randomUUID();
440
536
  }
441
537
  const hash = chunkHash || utils_1.Utils.generateHash(chunk.content);
538
+ // With a keyword index the point carries both vectors: the unnamed
539
+ // dense one (named "" in a vector map) and the BM25 one
540
+ const vector = db.keywordIndex
541
+ ? { '': embedding, [DatabaseManager.KEYWORD_VECTOR]: DatabaseManager.keywordDocument(chunk.content) }
542
+ : embedding;
442
543
  const pointItem = {
443
544
  id: pointId,
444
- vector: embedding,
545
+ vector,
445
546
  payload: {
446
547
  content: chunk.content,
447
548
  product_name: chunk.metadata.product_name,
@@ -868,3 +969,8 @@ class DatabaseManager {
868
969
  }
869
970
  exports.DatabaseManager = DatabaseManager;
870
971
  DatabaseManager.columnCache = new WeakMap();
972
+ // Qdrant computes the BM25 keyword vector itself from the chunk text
973
+ // (the `qdrant/bm25` model) and applies IDF at query time (`idf` modifier).
974
+ // The MCP server queries the same vector name with the same model.
975
+ DatabaseManager.KEYWORD_VECTOR = 'bm25';
976
+ DatabaseManager.KEYWORD_MODEL = 'qdrant/bm25';
package/dist/doc2vec.js CHANGED
@@ -641,8 +641,18 @@ class Doc2Vec {
641
641
  // remaining pages to be skipped indefinitely.
642
642
  const syncCompleteKey = `sync_complete:${urlPrefix}`;
643
643
  const syncCompleteValue = await database_1.DatabaseManager.getMetadataValue(dbConnection, syncCompleteKey, undefined, logger);
644
- const forceFullSync = syncCompleteValue !== 'true';
645
- if (forceFullSync) {
644
+ // Qdrant point ids used to come from the chunk text, which merged the
645
+ // same chunk on two pages into one point (see qdrantPointId). One pass
646
+ // over every page rewrites the pages that lost chunks; pages whose
647
+ // chunks are all there are skipped by the hash comparison as usual.
648
+ const pointIdKey = `point_ids_by_position:${urlPrefix}`;
649
+ const repairPointIds = dbConnection.type === 'qdrant'
650
+ && (await database_1.DatabaseManager.getMetadataValue(dbConnection, pointIdKey, undefined, logger)) !== 'true';
651
+ const forceFullSync = syncCompleteValue !== 'true' || repairPointIds;
652
+ if (forceFullSync && syncCompleteValue === 'true') {
653
+ logger.info('Processing all pages once to restore chunks shared between pages (per-page Qdrant point ids); unchanged pages are not re-embedded');
654
+ }
655
+ else if (forceFullSync) {
646
656
  logger.info('Full sync has not yet completed for this source — forcing processing of all pages (bypassing lastmod/ETag skip)');
647
657
  }
648
658
  const crawlResult = await this.contentProcessor.crawlWebsite(config.url, config, async (url, content) => {
@@ -722,6 +732,9 @@ class Doc2Vec {
722
732
  // unset and the next run will force-process all pages again.
723
733
  if (forceFullSync) {
724
734
  await database_1.DatabaseManager.setMetadataValue(dbConnection, syncCompleteKey, 'true', logger, this.embeddingDimension);
735
+ if (repairPointIds) {
736
+ await database_1.DatabaseManager.setMetadataValue(dbConnection, pointIdKey, 'true', logger, this.embeddingDimension);
737
+ }
725
738
  logger.info('Full sync completed successfully — subsequent runs will use normal caching');
726
739
  }
727
740
  let removed = { items: 0, chunks: 0 };
@@ -855,7 +868,15 @@ class Doc2Vec {
855
868
  const lastSyncKey = `s3_last_sync_${bucketKey}`;
856
869
  const fileListKey = `s3_filelist_${bucketKey}`;
857
870
  const lastSyncValue = await database_1.DatabaseManager.getMetadataValue(dbConnection, lastSyncKey, '0', logger);
858
- const lastSyncTimestamp = parseInt(lastSyncValue || '0', 10);
871
+ // One pass over every object to restore chunks that text-derived
872
+ // Qdrant point ids merged across files (see processWebsite)
873
+ const pointIdKey = `point_ids_by_position:s3_${bucketKey}`;
874
+ const repairPointIds = dbConnection.type === 'qdrant'
875
+ && (await database_1.DatabaseManager.getMetadataValue(dbConnection, pointIdKey, undefined, logger)) !== 'true';
876
+ if (repairPointIds && lastSyncValue && lastSyncValue !== '0') {
877
+ logger.info('Processing all objects once to restore chunks shared between files (per-page Qdrant point ids)');
878
+ }
879
+ const lastSyncTimestamp = repairPointIds ? 0 : parseInt(lastSyncValue || '0', 10);
859
880
  const includeExtensions = config.include_extensions || ['.md', '.txt', '.html', '.htm', '.pdf', '.doc', '.docx'];
860
881
  const excludeExtensions = config.exclude_extensions || [];
861
882
  const encoding = config.encoding || 'utf8';
@@ -1034,6 +1055,9 @@ class Doc2Vec {
1034
1055
  const currentKeys = Array.from(processedFiles);
1035
1056
  await database_1.DatabaseManager.setMetadataValue(dbConnection, fileListKey, JSON.stringify(currentKeys), logger, this.embeddingDimension);
1036
1057
  await database_1.DatabaseManager.setMetadataValue(dbConnection, lastSyncKey, `${syncStartTimestamp}`, logger, this.embeddingDimension);
1058
+ if (repairPointIds) {
1059
+ await database_1.DatabaseManager.setMetadataValue(dbConnection, pointIdKey, 'true', logger, this.embeddingDimension);
1060
+ }
1037
1061
  logger.info(`Finished processing S3 bucket: ${config.bucket}`);
1038
1062
  }
1039
1063
  /**
@@ -1910,6 +1934,33 @@ if (require.main === module) {
1910
1934
  .option('--source <product_name>', 'only sync sources with this product_name (repeatable)', (value, previous) => [...previous, value], [])
1911
1935
  .option('--source-index <n>', 'only sync the source at this 0-based position in the config (repeatable, exact)', (value, previous) => [...previous, parseInt(value, 10)], [])
1912
1936
  .action((configPath, options) => runOneShot(configPath, { names: options.source, indices: options.sourceIndex }));
1937
+ program
1938
+ .command('eval <cases>')
1939
+ .description('Measure retrieval quality (recall@k, MRR) of a sqlite-vec database against questions with known relevant URLs')
1940
+ .option('--db <path>', 'sqlite database to query (cases may override it with a "db" field)')
1941
+ .option('--config <path>', 'config file whose embedding section built the database (default: env vars)')
1942
+ .option('--top-k <n>', 'number of chunks retrieved per question', (v) => parseInt(v, 10), 10)
1943
+ .option('--out <path>', 'write the full JSON report here')
1944
+ .option('--mode <mode>', "retrieval: 'dense' (vector, as the MCP server), 'bm25' (keyword) or 'hybrid' (both, fused with RRF)", 'dense')
1945
+ .option('--per-page', 'keep only the best chunk of each page in the results', false)
1946
+ .option('--rerank <provider>', "rerank the top candidates: 'deepinfra' (needs DEEPINFRA_API_KEY)")
1947
+ .option('--rerank-model <model>', 'reranker model (default: nvidia/Nemotron-3.5-Rerank-8B-BF16)')
1948
+ .option('--rerank-candidates <n>', 'how many top chunks the reranker sees (default: 40)', (v) => parseInt(v, 10))
1949
+ .action(async (casesPath, options) => {
1950
+ // Lazy-require so the sync path doesn't load the eval module
1951
+ const { runEvalCommand } = require('./eval');
1952
+ try {
1953
+ await runEvalCommand({
1954
+ casesPath, dbPath: options.db, configPath: options.config, topK: options.topK, outPath: options.out,
1955
+ mode: options.mode, perPage: options.perPage,
1956
+ rerank: options.rerank, rerankModel: options.rerankModel, rerankCandidates: options.rerankCandidates,
1957
+ });
1958
+ }
1959
+ catch (err) {
1960
+ console.error(err instanceof Error ? err.message : err);
1961
+ process.exit(1);
1962
+ }
1963
+ });
1913
1964
  program
1914
1965
  .command('controller [configs...]')
1915
1966
  .description('Run as a long-lived controller: schedule sync jobs per config file, persist runs in Postgres, serve a web UI')
package/dist/eval.js ADDED
@@ -0,0 +1,375 @@
1
+ "use strict";
2
+ // Retrieval evaluation: runs a set of questions with known relevant URLs
3
+ // against a sqlite-vec database and reports recall@k and MRR. It measures
4
+ // the same thing the MCP server serves (nearest chunks by distance), so a
5
+ // change to the embedding model, chunking or ranking can be judged by
6
+ // numbers instead of by eyeballing a few queries.
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.DEFAULT_K_VALUES = exports.DEFAULT_RERANK_CANDIDATES = exports.DEFAULT_RERANK_MODEL = exports.RRF_K = void 0;
42
+ exports.createDeepInfraReranker = createDeepInfraReranker;
43
+ exports.normalizeUrl = normalizeUrl;
44
+ exports.firstRelevantRank = firstRelevantRank;
45
+ exports.computeMetrics = computeMetrics;
46
+ exports.loadEvalCases = loadEvalCases;
47
+ exports.runEval = runEval;
48
+ exports.onePerPage = onePerPage;
49
+ exports.fuseRrf = fuseRrf;
50
+ exports.toFtsQuery = toFtsQuery;
51
+ exports.createSqliteRetriever = createSqliteRetriever;
52
+ exports.createEmbedder = createEmbedder;
53
+ exports.formatReport = formatReport;
54
+ exports.runEvalCommand = runEvalCommand;
55
+ const fs = __importStar(require("fs"));
56
+ const path = __importStar(require("path"));
57
+ const yaml = __importStar(require("js-yaml"));
58
+ const openai_1 = require("openai");
59
+ exports.RRF_K = 60;
60
+ exports.DEFAULT_RERANK_MODEL = 'nvidia/Nemotron-3.5-Rerank-8B-BF16';
61
+ exports.DEFAULT_RERANK_CANDIDATES = 40;
62
+ // Same request the MCP server's reranker sends (mcp/src/server.ts)
63
+ function createDeepInfraReranker(apiKey, model = exports.DEFAULT_RERANK_MODEL) {
64
+ return async (query, documents) => {
65
+ const response = await fetch(`https://api.deepinfra.com/v1/inference/${model}`, {
66
+ method: 'POST',
67
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
68
+ body: JSON.stringify({ queries: [query], documents }),
69
+ });
70
+ if (!response.ok) {
71
+ throw new Error(`reranker returned HTTP ${response.status}: ${(await response.text()).slice(0, 200)}`);
72
+ }
73
+ const body = await response.json();
74
+ if (!Array.isArray(body.scores) || body.scores.length !== documents.length) {
75
+ throw new Error('reranker did not return one score per document');
76
+ }
77
+ return body.scores.map(Number);
78
+ };
79
+ }
80
+ exports.DEFAULT_K_VALUES = [1, 4, 10];
81
+ // Treat https://x/a/, https://x/a and https://x/a#section as the same page:
82
+ // eval sets are written by hand and the stored URLs vary on these points.
83
+ function normalizeUrl(url) {
84
+ let normalized = url.trim();
85
+ const hashIndex = normalized.indexOf('#');
86
+ if (hashIndex !== -1)
87
+ normalized = normalized.substring(0, hashIndex);
88
+ while (normalized.length > 1 && normalized.endsWith('/'))
89
+ normalized = normalized.slice(0, -1);
90
+ return normalized.toLowerCase();
91
+ }
92
+ function firstRelevantRank(retrievedUrls, relevantUrls) {
93
+ const relevant = new Set(relevantUrls.map(normalizeUrl));
94
+ for (let i = 0; i < retrievedUrls.length; i++) {
95
+ const url = retrievedUrls[i];
96
+ if (url && relevant.has(normalizeUrl(url)))
97
+ return i + 1;
98
+ }
99
+ return null;
100
+ }
101
+ function computeMetrics(results, kValues = exports.DEFAULT_K_VALUES) {
102
+ const recall = {};
103
+ for (const k of kValues) {
104
+ const hits = results.filter(r => r.rank !== null && r.rank <= k).length;
105
+ recall[k] = results.length ? hits / results.length : 0;
106
+ }
107
+ const reciprocalSum = results.reduce((sum, r) => sum + (r.rank ? 1 / r.rank : 0), 0);
108
+ return { cases: results.length, recall, mrr: results.length ? reciprocalSum / results.length : 0 };
109
+ }
110
+ // Accepts .jsonl (one case per line), .json (an array) or .yaml/.yml (a list).
111
+ function loadEvalCases(filePath) {
112
+ const raw = fs.readFileSync(filePath, 'utf8');
113
+ const ext = path.extname(filePath).toLowerCase();
114
+ let parsed;
115
+ if (ext === '.jsonl') {
116
+ parsed = raw.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('//')).map(l => JSON.parse(l));
117
+ }
118
+ else if (ext === '.json') {
119
+ parsed = JSON.parse(raw);
120
+ }
121
+ else {
122
+ parsed = yaml.load(raw);
123
+ }
124
+ if (!Array.isArray(parsed)) {
125
+ throw new Error(`Eval file ${filePath} must contain a list of cases`);
126
+ }
127
+ return parsed.map((c, i) => {
128
+ if (!c || typeof c.query !== 'string' || !c.query.trim()) {
129
+ throw new Error(`Eval case #${i + 1} has no query`);
130
+ }
131
+ if (!Array.isArray(c.relevant_urls) || c.relevant_urls.length === 0) {
132
+ throw new Error(`Eval case #${i + 1} ("${c.query}") has no relevant_urls`);
133
+ }
134
+ return { ...c, id: c.id ?? String(i + 1) };
135
+ });
136
+ }
137
+ async function runEval(cases, deps, options) {
138
+ const results = [];
139
+ for (const evalCase of cases) {
140
+ const embedding = await deps.embed(evalCase.query);
141
+ const chunks = await deps.retrieve(embedding, evalCase, options.topK);
142
+ const retrievedUrls = chunks.map(c => c.url ?? '');
143
+ results.push({
144
+ id: evalCase.id ?? '',
145
+ query: evalCase.query,
146
+ rank: firstRelevantRank(retrievedUrls, evalCase.relevant_urls),
147
+ retrieved_urls: retrievedUrls,
148
+ });
149
+ }
150
+ const kValues = (options.kValues ?? exports.DEFAULT_K_VALUES).filter(k => k <= options.topK);
151
+ return {
152
+ model: options.model,
153
+ top_k: options.topK,
154
+ created_at: new Date().toISOString(),
155
+ ...computeMetrics(results, kValues),
156
+ results,
157
+ };
158
+ }
159
+ // Keeps the first (best) chunk of each page, in order.
160
+ function onePerPage(chunks) {
161
+ const seen = new Set();
162
+ return chunks.filter(chunk => {
163
+ const key = normalizeUrl(chunk.url ?? '');
164
+ if (seen.has(key))
165
+ return false;
166
+ seen.add(key);
167
+ return true;
168
+ });
169
+ }
170
+ // Reciprocal Rank Fusion: each list contributes 1 / (RRF_K + rank) per chunk.
171
+ // Rank-based, so dense distances and bm25 scores need no calibration.
172
+ function fuseRrf(lists) {
173
+ const scores = new Map();
174
+ for (const list of lists) {
175
+ list.forEach((chunk, index) => {
176
+ if (chunk.rowid === undefined)
177
+ return;
178
+ const entry = scores.get(chunk.rowid) ?? { chunk, score: 0 };
179
+ entry.score += 1 / (exports.RRF_K + index + 1);
180
+ scores.set(chunk.rowid, entry);
181
+ });
182
+ }
183
+ return [...scores.values()].sort((a, b) => b.score - a.score).map(e => e.chunk);
184
+ }
185
+ // Turns free text into an FTS5 OR-query of quoted terms, so punctuation in a
186
+ // question (colons, quotes, parentheses) cannot break the MATCH syntax.
187
+ function toFtsQuery(text) {
188
+ const terms = text.toLowerCase().match(/[\p{L}\p{N}_]+/gu) ?? [];
189
+ return [...new Set(terms)].map(t => `"${t}"`).join(' OR ');
190
+ }
191
+ // Queries the database the MCP server's sqlite provider reads
192
+ // (mcp/src/server.ts); dense mode runs exactly its query, so the numbers
193
+ // reflect what an agent receives today.
194
+ function createSqliteRetriever(defaultDbPath, options = {}) {
195
+ const Database = require('better-sqlite3');
196
+ const sqliteVec = require('sqlite-vec');
197
+ const mode = options.mode ?? 'dense';
198
+ const connections = new Map();
199
+ const open = (dbPath) => {
200
+ let conn = connections.get(dbPath);
201
+ if (!conn) {
202
+ if (!fs.existsSync(dbPath))
203
+ throw new Error(`Database file not found at ${dbPath}`);
204
+ const db = new Database(dbPath, { readonly: true });
205
+ sqliteVec.load(db);
206
+ conn = { db };
207
+ connections.set(dbPath, conn);
208
+ }
209
+ return conn;
210
+ };
211
+ // The keyword index is built in memory from the stored chunk text, so an
212
+ // existing database can be evaluated without re-ingesting it.
213
+ const ftsFor = (conn) => {
214
+ if (!conn.fts) {
215
+ const fts = new Database(':memory:');
216
+ fts.exec(`CREATE VIRTUAL TABLE chunks USING fts5(content, url UNINDEXED, product_name UNINDEXED, version UNINDEXED)`);
217
+ const insert = fts.prepare('INSERT INTO chunks (rowid, content, url, product_name, version) VALUES (?, ?, ?, ?, ?)');
218
+ const rows = conn.db.prepare('SELECT rowid, content, url, product_name, version FROM vec_items').all();
219
+ fts.transaction(() => {
220
+ for (const r of rows)
221
+ insert.run(BigInt(r.rowid), r.content ?? '', r.url, r.product_name, r.version);
222
+ })();
223
+ conn.fts = fts;
224
+ }
225
+ return conn.fts;
226
+ };
227
+ const dense = (conn, queryEmbedding, evalCase, limit) => {
228
+ let query = `
229
+ SELECT rowid, url, distance, content
230
+ FROM vec_items
231
+ WHERE embedding MATCH @query_embedding`;
232
+ if (evalCase.product_name)
233
+ query += ` AND product_name = @product_name`;
234
+ if (evalCase.version)
235
+ query += ` AND version = @version`;
236
+ query += `
237
+ ORDER BY distance
238
+ LIMIT @top_k;`;
239
+ return conn.db.prepare(query).all({
240
+ query_embedding: new Float32Array(queryEmbedding),
241
+ product_name: evalCase.product_name,
242
+ version: evalCase.version,
243
+ top_k: limit,
244
+ });
245
+ };
246
+ const bm25 = (conn, evalCase, limit) => {
247
+ const match = toFtsQuery(evalCase.query);
248
+ if (!match)
249
+ return [];
250
+ let query = `SELECT rowid, url, content FROM chunks WHERE chunks MATCH @match`;
251
+ if (evalCase.product_name)
252
+ query += ` AND product_name = @product_name`;
253
+ if (evalCase.version)
254
+ query += ` AND version = @version`;
255
+ query += ` ORDER BY bm25(chunks) LIMIT @limit`;
256
+ return ftsFor(conn).prepare(query).all({
257
+ match,
258
+ product_name: evalCase.product_name,
259
+ version: evalCase.version,
260
+ limit,
261
+ });
262
+ };
263
+ return async (queryEmbedding, evalCase, topK) => {
264
+ const dbPath = evalCase.db ?? defaultDbPath;
265
+ if (!dbPath)
266
+ throw new Error(`No database for eval case ${evalCase.id}: pass --db or set "db" on the case`);
267
+ const conn = open(dbPath);
268
+ const rerankCandidates = options.rerankCandidates ?? exports.DEFAULT_RERANK_CANDIDATES;
269
+ const depth = Math.max(topK, options.candidates ?? (mode === 'dense' && !options.perPage ? topK : 50), options.rerank ? rerankCandidates : 0);
270
+ let chunks;
271
+ if (mode === 'bm25') {
272
+ chunks = bm25(conn, evalCase, depth);
273
+ }
274
+ else if (mode === 'hybrid') {
275
+ chunks = fuseRrf([dense(conn, queryEmbedding, evalCase, depth), bm25(conn, evalCase, depth)]);
276
+ }
277
+ else {
278
+ chunks = dense(conn, queryEmbedding, evalCase, depth);
279
+ }
280
+ if (options.rerank && chunks.length > 1) {
281
+ const pool = chunks.slice(0, rerankCandidates);
282
+ const scores = await options.rerank(evalCase.query, pool.map(c => c.content ?? ''));
283
+ const reranked = pool.map((c, i) => ({ c, s: scores[i] })).sort((a, b) => b.s - a.s).map(x => x.c);
284
+ chunks = [...reranked, ...chunks.slice(rerankCandidates)];
285
+ }
286
+ if (options.perPage)
287
+ chunks = onePerPage(chunks);
288
+ return chunks.slice(0, topK);
289
+ };
290
+ }
291
+ // Mirrors the provider selection in Doc2Vec's constructor and the MCP server's
292
+ // env vars, so the query is embedded with the model the database was built with.
293
+ function createEmbedder(embeddingConfig) {
294
+ const provider = embeddingConfig?.provider || process.env.EMBEDDING_PROVIDER || 'openai';
295
+ let client;
296
+ let model;
297
+ if (provider === 'azure') {
298
+ const apiKey = embeddingConfig?.azure?.api_key || process.env.AZURE_OPENAI_KEY;
299
+ const endpoint = embeddingConfig?.azure?.endpoint || process.env.AZURE_OPENAI_ENDPOINT;
300
+ model = embeddingConfig?.azure?.deployment_name || process.env.AZURE_OPENAI_DEPLOYMENT_NAME || 'text-embedding-3-large';
301
+ if (!apiKey || !endpoint)
302
+ throw new Error('Azure OpenAI requires api_key and endpoint to be configured');
303
+ client = new openai_1.AzureOpenAI({
304
+ apiKey,
305
+ endpoint,
306
+ deployment: model,
307
+ apiVersion: embeddingConfig?.azure?.api_version || process.env.AZURE_OPENAI_API_VERSION || '2024-10-21',
308
+ });
309
+ }
310
+ else {
311
+ const apiKey = embeddingConfig?.openai?.api_key || process.env.OPENAI_API_KEY;
312
+ const baseURL = embeddingConfig?.openai?.base_url || process.env.OPENAI_BASE_URL;
313
+ model = embeddingConfig?.openai?.model || process.env.OPENAI_MODEL || 'text-embedding-3-large';
314
+ if (!apiKey)
315
+ throw new Error('OpenAI requires api_key to be configured');
316
+ client = new openai_1.OpenAI({ apiKey, ...(baseURL && { baseURL }) });
317
+ }
318
+ const embed = async (text) => {
319
+ const response = await client.embeddings.create({ model, input: text });
320
+ return response.data[0].embedding;
321
+ };
322
+ return { embed, model };
323
+ }
324
+ function formatReport(report) {
325
+ const lines = [];
326
+ lines.push(`Model: ${report.model} Cases: ${report.cases} top_k: ${report.top_k}`);
327
+ const recallParts = Object.entries(report.recall).map(([k, v]) => `recall@${k} ${(v * 100).toFixed(1)}%`);
328
+ lines.push(`${recallParts.join(' ')} MRR ${report.mrr.toFixed(3)}`);
329
+ const misses = report.results.filter(r => r.rank === null);
330
+ if (misses.length) {
331
+ lines.push('');
332
+ lines.push(`Misses (${misses.length}):`);
333
+ for (const m of misses)
334
+ lines.push(` [${m.id}] ${m.query}`);
335
+ }
336
+ return lines.join('\n');
337
+ }
338
+ async function runEvalCommand(options) {
339
+ const mode = options.mode ?? 'dense';
340
+ if (!['dense', 'bm25', 'hybrid'].includes(mode)) {
341
+ throw new Error(`Unknown mode '${mode}': use dense, bm25 or hybrid`);
342
+ }
343
+ let rerank;
344
+ if (options.rerank) {
345
+ if (options.rerank !== 'deepinfra')
346
+ throw new Error(`Unknown reranker '${options.rerank}': use deepinfra`);
347
+ const apiKey = process.env.DEEPINFRA_API_KEY;
348
+ if (!apiKey)
349
+ throw new Error('--rerank deepinfra requires the DEEPINFRA_API_KEY environment variable');
350
+ rerank = createDeepInfraReranker(apiKey, options.rerankModel);
351
+ }
352
+ let embeddingConfig;
353
+ if (options.configPath) {
354
+ const config = yaml.load(fs.readFileSync(options.configPath, 'utf8'));
355
+ embeddingConfig = config?.embedding;
356
+ }
357
+ // Keyword-only retrieval never looks at the query vector, so skip the API call
358
+ const { embed, model } = mode === 'bm25'
359
+ ? { embed: async () => [], model: 'none (bm25)' }
360
+ : createEmbedder(embeddingConfig);
361
+ const cases = loadEvalCases(options.casesPath);
362
+ const retrieve = createSqliteRetriever(options.dbPath, {
363
+ mode, perPage: options.perPage, rerank, rerankCandidates: options.rerankCandidates,
364
+ });
365
+ const label = `${model}, ${mode}` +
366
+ `${rerank ? `, reranked by ${options.rerankModel ?? exports.DEFAULT_RERANK_MODEL} over ${options.rerankCandidates ?? exports.DEFAULT_RERANK_CANDIDATES}` : ''}` +
367
+ `${options.perPage ? ', one chunk per page' : ''}`;
368
+ const report = await runEval(cases, { embed, retrieve }, { topK: options.topK, model: label });
369
+ console.log(formatReport(report));
370
+ if (options.outPath) {
371
+ fs.writeFileSync(options.outPath, JSON.stringify(report, null, 2));
372
+ console.log(`\nReport written to ${options.outPath}`);
373
+ }
374
+ return report;
375
+ }
@@ -52,7 +52,7 @@ ${e.slice(l+1)}`:c+=e.slice(i),c.slice(1)}function p7(e){let t="",n=0;for(let r=
52
52
  `:" ";r+=`---${p}${d}
53
53
  `}else r+=Ba(n,0,l.contents,{block:!0,compact:!0})+`
54
54
  `;i=l.explicitEnd||l.contents!==null&&b7(l.contents),i&&(r+=`...
55
- `)}return r}var w7={...AO,schema:YW,skipInvalid:!1,noRefs:!1,flowLevel:-1,transform:()=>{}};function O7(e,t={}){const n={...w7,...t},r=HV(e,n.schema,{noRefs:n.noRefs,skipInvalid:n.skipInvalid});return n.flowLevel>=0&&qV(r,(i,a)=>{if(!(a.depth<n.flowLevel))return i.style.flow=!0,tI}),n.transform(r),S7(r,{...W1(n,Object.keys(AO)),schema:n.schema})}const HM=[{key:"product_name",label:"Product name",type:"string",required:!0,placeholder:"istio",help:"Stored in chunk metadata; used to identify the source"},{key:"version",label:"Version",type:"string",required:!0,placeholder:"latest",help:"Required for all types except code (defaults to branch)"},{key:"max_size",label:"Max page size (bytes)",type:"number",placeholder:"1048576",help:"Pages/files larger than this are skipped"}],Fp={website:{label:"Website",fields:[{key:"url",label:"Base URL",type:"string",required:!0,placeholder:"https://docs.example.com/"},{key:"sitemap_url",label:"Sitemap URL",type:"string",placeholder:"https://docs.example.com/sitemap.xml",help:"Discovers pages not linked in navigation; lastmod dates enable cheap change detection"},{key:"markdown_store",label:"Store markdown in Postgres",type:"boolean",help:"Requires the top-level markdown store to be configured"}]},github:{label:"GitHub issues",fields:[{key:"repo",label:"Repository",type:"string",required:!0,placeholder:"owner/repo"},{key:"start_date",label:"Start date",type:"string",placeholder:"2025-01-01",help:"Only issues updated after this date (first run)"}]},local_directory:{label:"Local directory",fields:[{key:"path",label:"Directory path",type:"string",required:!0,placeholder:"/data/docs"},{key:"include_extensions",label:"Include extensions",type:"string_list",placeholder:".md, .txt, .pdf"},{key:"exclude_extensions",label:"Exclude extensions",type:"string_list",placeholder:".log, .tmp"},{key:"recursive",label:"Recurse into subdirectories",type:"boolean"},{key:"encoding",label:"File encoding",type:"string",placeholder:"utf8"},{key:"url_rewrite_prefix",label:"URL rewrite prefix",type:"string",placeholder:"https://mydomain.com",help:"Rewrites file:// URLs in chunk metadata"}]},zendesk:{label:"Zendesk",fields:[{key:"zendesk_subdomain",label:"Subdomain",type:"string",required:!0,placeholder:"mycompany",help:"mycompany.zendesk.com"},{key:"email",label:"User email",type:"string",required:!0,placeholder:"agent@company.com"},{key:"api_token",label:"API token",type:"string",required:!0,secret:!0,placeholder:"${ZENDESK_API_TOKEN}"},{key:"fetch_tickets",label:"Fetch tickets",type:"boolean"},{key:"fetch_articles",label:"Fetch help center articles",type:"boolean"},{key:"start_date",label:"Start date",type:"string",placeholder:"2025-01-01"},{key:"ticket_status",label:"Ticket statuses",type:"string_list",placeholder:"new, open, pending, hold, solved"},{key:"ticket_priority",label:"Ticket priorities",type:"string_list",placeholder:"urgent, high"},{key:"excluded_organizations",label:"Excluded organizations",type:"string_list"},{key:"include_internal_comments",label:"Include internal comments",type:"boolean"}]},code:{label:"Code",fields:[{key:"source",label:"Code source",type:"select",required:!0,options:["github","local_directory"]},{key:"repo",label:"Repository",type:"string",placeholder:"owner/repo",help:"When source is github"},{key:"branch",label:"Branch",type:"string",placeholder:"main",help:"When source is github"},{key:"path",label:"Directory path",type:"string",placeholder:"/data/src",help:"When source is local_directory"},{key:"include_extensions",label:"Include extensions",type:"string_list",placeholder:".ts, .go, .py"},{key:"exclude_extensions",label:"Exclude extensions",type:"string_list"},{key:"exclude_paths",label:"Exclude paths",type:"string_list",placeholder:"vendor/**, **/*_test.go",help:"Glob patterns relative to the source root"},{key:"recursive",label:"Recurse into subdirectories",type:"boolean"},{key:"chunk_size",label:"Chunk size (tokens)",type:"number",placeholder:"1500"},{key:"url_rewrite_prefix",label:"URL rewrite prefix",type:"string"}]},s3:{label:"S3 bucket",fields:[{key:"bucket",label:"Bucket",type:"string",required:!0,placeholder:"my-docs-bucket"},{key:"prefix",label:"Key prefix",type:"string",placeholder:"docs/"},{key:"region",label:"Region",type:"string",placeholder:"us-east-1"},{key:"endpoint",label:"Custom endpoint",type:"string",placeholder:"https://minio.local:9000",help:"For S3-compatible services (MinIO, LocalStack)"},{key:"include_extensions",label:"Include extensions",type:"string_list",placeholder:".md, .txt, .pdf"},{key:"exclude_extensions",label:"Exclude extensions",type:"string_list"},{key:"encoding",label:"Text encoding",type:"string",placeholder:"utf8"},{key:"url_rewrite_prefix",label:"URL rewrite prefix",type:"string",help:"Rewrites s3:// URLs in chunk metadata"}]}},vx={sqlite:{label:"SQLite (sqlite-vec)",fields:[{key:"db_path",label:"Database file",type:"string",placeholder:"./my-product.db"}]},qdrant:{label:"Qdrant",fields:[{key:"qdrant_url",label:"Qdrant URL",type:"string",placeholder:"http://localhost"},{key:"qdrant_port",label:"Port",type:"number",placeholder:"6333"},{key:"collection_name",label:"Collection",type:"string",placeholder:"my-product"}]}},FM={openai:{label:"OpenAI (or compatible)",fields:[{key:"api_key",label:"API key",type:"string",secret:!0,placeholder:"${OPENAI_API_KEY}",help:"Falls back to the OPENAI_API_KEY env var"},{key:"model",label:"Model",type:"string",placeholder:"text-embedding-3-large"},{key:"base_url",label:"Base URL",type:"string",placeholder:"http://localhost:11434/v1",help:"For Ollama or other OpenAI-compatible endpoints"}]},azure:{label:"Azure OpenAI",fields:[{key:"api_key",label:"API key",type:"string",secret:!0,placeholder:"${AZURE_OPENAI_KEY}"},{key:"endpoint",label:"Endpoint",type:"string",placeholder:"https://myresource.openai.azure.com"},{key:"deployment_name",label:"Deployment name",type:"string",placeholder:"text-embedding-3-large"},{key:"api_version",label:"API version",type:"string",placeholder:"2024-10-21"}]}},A7=[{key:"connection_string",label:"Connection string",type:"string",secret:!0,placeholder:"postgres://user:${PG_PASSWORD}@host:5432/db",help:"Either this, or the discrete fields below"},{key:"host",label:"Host",type:"string"},{key:"port",label:"Port",type:"number",placeholder:"5432"},{key:"database",label:"Database",type:"string"},{key:"user",label:"User",type:"string"},{key:"password",label:"Password",type:"string",secret:!0,placeholder:"${PG_PASSWORD}"},{key:"table_name",label:"Table name",type:"string",placeholder:"markdown_pages"}];function qp(e){const t={type:e,product_name:"",version:"latest",max_size:1048576};return e==="code"&&delete t.version,t.database_config={type:"sqlite",params:{db_path:""}},t}function k7(e,t){return t.reduce((n,r)=>n?.[r],e)}function sI(e,t,n){if(t.length===0)return n;const[r,...i]=t,a=Array.isArray(e)?[...e??[]]:{...e??{}};return i.length===0?n===void 0||n===""||Array.isArray(n)&&n.length===0?delete a[r]:a[r]=n:a[r]=sI(a[r],i,n),a}const $o="w-full rounded-md border border-edge bg-page px-3 py-1.5 text-sm outline-none focus:border-accent placeholder:text-ink-muted/60";function vu({field:e,value:t,onChange:n}){const[r,i]=O.useState(null);return e.type==="boolean"?T.jsxs("label",{className:"flex items-center gap-2 py-1.5 text-sm text-ink-secondary",children:[T.jsx("input",{type:"checkbox",checked:!!t,onChange:a=>n(a.target.checked||void 0),className:"h-4 w-4 accent-(--accent)"}),e.label,e.help&&T.jsxs("span",{className:"text-xs text-ink-muted",children:["— ",e.help]})]}):T.jsxs("label",{className:"block",children:[T.jsxs("span",{className:"mb-1 flex items-baseline gap-2 text-xs font-medium text-ink-secondary",children:[e.label,e.required&&T.jsx("span",{className:"text-critical",children:"*"}),e.secret&&T.jsxs("span",{className:"text-ink-muted",children:["use a ",T.jsx("code",{children:"${ENV_VAR}"})," placeholder"]})]}),e.type==="select"?T.jsxs("select",{value:t??"",onChange:a=>n(a.target.value||void 0),className:$o,children:[T.jsx("option",{value:"",children:"—"}),e.options.map(a=>T.jsx("option",{value:a,children:a},a))]}):e.type==="string_list"?T.jsx("input",{value:r??(Array.isArray(t)?t.join(", "):""),placeholder:e.placeholder,onChange:a=>i(a.target.value),onBlur:()=>{r!==null&&(n(r.split(",").map(a=>a.trim()).filter(Boolean)),i(null))},className:$o}):T.jsx("input",{type:"text",inputMode:e.type==="number"?"numeric":void 0,value:t??"",placeholder:e.placeholder,onChange:a=>{const l=a.target.value;e.type==="number"?n(l===""?void 0:Number(l)):n(l)},className:$o}),e.help&&T.jsx("span",{className:"mt-1 block text-xs text-ink-muted",children:e.help})]})}function yx({fields:e,doc:t,basePath:n,onSet:r}){return T.jsx("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:e.map(i=>T.jsx("div",{className:i.type==="boolean"?"sm:col-span-2":"",children:T.jsx(vu,{field:i,value:k7(t,[...n,i.key]),onChange:a=>r([...n,i.key],a)})},i.key))})}function Wp({title:e,subtitle:t,action:n,children:r}){return T.jsxs("section",{className:"rounded-lg border border-edge bg-surface p-4",children:[T.jsxs("div",{className:"mb-3 flex items-center gap-2",children:[T.jsx("h3",{className:"text-sm font-semibold",children:e}),t&&T.jsx("span",{className:"text-xs text-ink-muted",children:t}),T.jsx("div",{className:"ml-auto",children:n})]}),r]})}function uI(e){const t=io(),n=O.useMemo(()=>{if(e.mode==="edit"&&e.config)try{const A=zV(e.config.content);return A&&typeof A=="object"?{doc:A,parseError:null}:{doc:null,parseError:"config is not a YAML mapping"}}catch(A){return{doc:null,parseError:A instanceof Error?A.message:String(A)}}return{doc:{name:"",schedule:"",sources:[qp("website")]},parseError:null}},[e.mode,e.config]),[r,i]=O.useState(n.doc),[a,l]=O.useState(""),[s,c]=O.useState(null),f=Wh({mutationFn:async()=>{const A=O7(r,{lineWidth:120,noRefs:!0}),E=await Ct.validateConfig(A);if(!E.valid)throw new fh(400,E.error??"invalid config");if(e.mode==="create"){const k=a.trim()||`${(r.name||"config").toString().replace(/[^A-Za-z0-9._-]+/g,"-")}.yaml`;return Ct.createConfig(k,A)}return Ct.updateConfig(e.config.id,A,e.config.content_hash)},onSuccess:A=>{c(null),t.invalidateQueries({queryKey:["configs"]}),e.onSaved?.(A)},onError:A=>c(A.message)});if(n.parseError||!r)return T.jsxs("p",{className:"rounded-md border border-warning/50 bg-surface px-3 py-2 text-sm text-ink-secondary",children:["⚠ This config can't be edited as a form (",n.parseError,") — use the YAML view."]});const d=(A,E)=>i(k=>sI(k,A,E)),p=Array.isArray(r.sources)?r.sources:[],g=[];p.forEach((A,E)=>{const k=Fp[A?.type];for(const M of[...HM,...k?.fields??[]]){if(!M.required||A?.type==="code"&&M.key==="version")continue;const D=A?.[M.key];(D===void 0||D==="")&&g.push(`source ${E+1}: ${M.label}`)}});const v=r.embedding!==void 0,y=r.markdown_store!==void 0,x=f.error instanceof fh&&f.error.status===409,w=r.schedule??"";return T.jsxs("div",{className:"space-y-4",children:[x&&T.jsx("div",{className:"rounded-md border border-warning/50 bg-surface px-3 py-2 text-sm text-ink-secondary",children:"⚠ The file changed on disk since you loaded it. Reload the page and reapply your edits."}),s&&!x&&T.jsx("div",{className:"rounded-md border border-critical/50 bg-surface px-3 py-2 text-sm text-critical",children:s}),T.jsx(Wp,{title:"General",children:T.jsxs("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:[e.mode==="create"&&T.jsxs("label",{className:"block",children:[T.jsx("span",{className:"mb-1 block text-xs font-medium text-ink-secondary",children:"Filename"}),T.jsx("input",{value:a,onChange:A=>l(A.target.value),placeholder:`${(r.name||"my-config").toString().replace(/[^A-Za-z0-9._-]+/g,"-")}.yaml`,className:`${$o} font-mono`})]}),T.jsx(vu,{field:{key:"name",label:"Display name",type:"string",placeholder:"product-docs"},value:r.name,onChange:A=>d(["name"],A)}),T.jsxs("div",{children:[T.jsx(vu,{field:{key:"schedule",label:"Schedule (cron)",type:"string",placeholder:"0 2 * * *",help:"Leave empty for manual runs only"},value:w,onChange:A=>d(["schedule"],A)}),w&&T.jsx("p",{className:"mt-1 text-xs text-accent",children:gO(w)})]})]})}),p.map((A,E)=>{const k=Fp[A?.type]??Fp.website,M=A?.database_config?.type??"sqlite",D=vx[M]??vx.sqlite;return T.jsx(Wp,{title:`Source ${E+1}`,subtitle:A?.product_name||void 0,action:T.jsx("button",{onClick:()=>d(["sources"],p.filter((P,N)=>N!==E)),className:"text-xs text-ink-muted transition hover:text-critical",children:"Remove"}),children:T.jsxs("div",{className:"space-y-4",children:[T.jsxs("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:[T.jsxs("label",{className:"block",children:[T.jsx("span",{className:"mb-1 block text-xs font-medium text-ink-secondary",children:"Type"}),T.jsx("select",{value:A?.type??"website",onChange:P=>{const N={...qp(P.target.value),product_name:A?.product_name??"",...A?.version!==void 0&&P.target.value!=="code"&&{version:A.version},...A?.max_size!==void 0&&{max_size:A.max_size},database_config:A?.database_config??qp(P.target.value).database_config};d(["sources",E],N)},className:$o,children:Object.entries(Fp).map(([P,N])=>T.jsx("option",{value:P,children:N.label},P))})]}),HM.map(P=>T.jsx(vu,{field:A?.type==="code"&&P.key==="version"?{...P,required:!1,help:"Defaults to the branch name"}:P,value:A?.[P.key],onChange:N=>d(["sources",E,P.key],N)},P.key))]}),T.jsxs("div",{children:[T.jsxs("p",{className:"mb-2 text-xs font-semibold uppercase tracking-wide text-ink-muted",children:[k.label," options"]}),T.jsx(yx,{fields:k.fields,doc:r,basePath:["sources",E],onSet:d})]}),T.jsxs("div",{children:[T.jsx("p",{className:"mb-2 text-xs font-semibold uppercase tracking-wide text-ink-muted",children:"Vector database"}),T.jsxs("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:[T.jsxs("label",{className:"block",children:[T.jsx("span",{className:"mb-1 block text-xs font-medium text-ink-secondary",children:"Type"}),T.jsx("select",{value:M,onChange:P=>d(["sources",E,"database_config"],{type:P.target.value,params:{}}),className:$o,children:Object.entries(vx).map(([P,N])=>T.jsx("option",{value:P,children:N.label},P))})]}),D.fields.map(P=>T.jsx(vu,{field:P,value:A?.database_config?.params?.[P.key],onChange:N=>d(["sources",E,"database_config","params",P.key],N)},P.key))]})]})]})},E)}),T.jsx("button",{onClick:()=>d(["sources"],[...p,qp("website")]),className:"w-full rounded-lg border border-dashed border-edge py-2.5 text-sm font-medium text-ink-muted transition hover:border-accent hover:text-accent",children:"+ Add source"}),T.jsx(Wp,{title:"Embedding",subtitle:"optional — defaults to OpenAI text-embedding-3-large",action:T.jsxs("label",{className:"flex items-center gap-2 text-xs text-ink-secondary",children:[T.jsx("input",{type:"checkbox",checked:v,onChange:A=>d(["embedding"],A.target.checked?{provider:"openai"}:void 0),className:"h-4 w-4 accent-(--accent)"}),"customize"]}),children:v?T.jsxs("div",{className:"space-y-3",children:[T.jsxs("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:[T.jsxs("label",{className:"block",children:[T.jsx("span",{className:"mb-1 block text-xs font-medium text-ink-secondary",children:"Provider"}),T.jsx("select",{value:r.embedding?.provider??"openai",onChange:A=>d(["embedding"],{provider:A.target.value}),className:$o,children:Object.entries(FM).map(([A,E])=>T.jsx("option",{value:A,children:E.label},A))})]}),T.jsx(vu,{field:{key:"dimension",label:"Dimension",type:"number",placeholder:"3072"},value:r.embedding?.dimension,onChange:A=>d(["embedding","dimension"],A)})]}),T.jsx(yx,{fields:FM[r.embedding?.provider??"openai"].fields,doc:r,basePath:["embedding",r.embedding?.provider??"openai"],onSet:d})]}):T.jsx("p",{className:"text-xs text-ink-muted",children:"Using defaults (OPENAI_API_KEY env var, text-embedding-3-large, 3072 dimensions)."})}),T.jsx(Wp,{title:"Markdown store",subtitle:"optional — store crawled markdown in Postgres",action:T.jsxs("label",{className:"flex items-center gap-2 text-xs text-ink-secondary",children:[T.jsx("input",{type:"checkbox",checked:y,onChange:A=>d(["markdown_store"],A.target.checked?{}:void 0),className:"h-4 w-4 accent-(--accent)"}),"enable"]}),children:y?T.jsx(yx,{fields:A7,doc:r,basePath:["markdown_store"],onSet:d}):T.jsx("p",{className:"text-xs text-ink-muted",children:"Disabled."})}),T.jsxs("div",{className:"flex items-center gap-3",children:[T.jsx("button",{onClick:()=>f.mutate(),disabled:f.isPending||p.length===0||g.length>0,className:"rounded-md bg-accent px-4 py-2 text-sm font-medium text-white transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40",children:f.isPending?"Saving…":e.mode==="create"?"Create config":"Save changes"}),e.onCancel&&T.jsx("button",{onClick:e.onCancel,className:"text-sm text-ink-muted hover:text-ink-secondary",children:"Cancel"}),g.length>0&&T.jsxs("span",{className:"text-xs text-warning",children:["missing: ",g.slice(0,3).join(", "),g.length>3?"…":""]}),p.length===0&&T.jsx("span",{className:"text-xs text-warning",children:"add at least one source"}),e.mode==="edit"&&T.jsx("span",{className:"ml-auto text-xs text-ink-muted",children:"Saving rewrites the YAML file (comments are not preserved)."})]})]})}function cI({config:e,variant:t="subtle"}){const n=io(),[r,i]=O.useState(!1),[a,l]=O.useState(new Set),[s,c]=O.useState(""),f=Wh({mutationFn:k=>Ct.triggerRun(e.id,k,e.content_hash),onSuccess:()=>{i(!1),l(new Set),c("")},onSettled:()=>n.invalidateQueries({queryKey:["configs"]})}),d=O.useMemo(()=>e.source_summary.map((k,M)=>({...k,index:M})),[e.source_summary]),p=O.useMemo(()=>{const k=s.trim().toLowerCase();return k?d.filter(M=>M.product_name.toLowerCase().includes(k)||M.type.toLowerCase().includes(k)):d},[d,s]),g=e.busy||!!e.parse_error||f.isPending,v=k=>{k.preventDefault(),k.stopPropagation()},y=k=>{l(M=>{const D=new Set(M);return D.has(k)?D.delete(k):D.add(k),D})},x=p.length>0&&p.every(k=>a.has(k.index)),w=()=>{l(k=>{const M=new Set(k);return x?p.forEach(D=>M.delete(D.index)):p.forEach(D=>M.add(D.index)),M})},A=t==="primary"?"bg-accent text-white transition hover:opacity-90 px-3.5 py-1.5 text-sm font-medium":"border border-edge bg-surface text-ink-secondary transition hover:border-accent hover:text-accent px-3 py-1 text-xs font-medium",E=t==="primary"?"bg-accent text-white transition hover:opacity-90 border-l border-white/25 px-2 py-1.5 text-sm":"border border-l-0 border-edge bg-surface text-ink-secondary transition hover:border-accent hover:text-accent px-1.5 py-1 text-xs";return T.jsxs("div",{className:"relative inline-flex items-center gap-2",onClick:v,children:[T.jsxs("span",{className:"inline-flex",children:[T.jsx("button",{onClick:k=>{v(k),f.mutate(void 0)},disabled:g,className:`${d.length>1?"rounded-l-md":"rounded-md"} ${A} disabled:cursor-not-allowed disabled:opacity-40`,children:e.busy?"Running…":"Run now"}),d.length>1&&T.jsx("button",{onClick:k=>{v(k),i(M=>!M)},disabled:g,"aria-label":"Choose sources to run",title:"Choose sources to run",className:`rounded-r-md ${E} disabled:cursor-not-allowed disabled:opacity-40`,children:"▾"})]}),f.error&&T.jsx("span",{className:"text-xs text-critical",children:f.error.message}),r&&T.jsxs(T.Fragment,{children:[T.jsx("div",{className:"fixed inset-0 z-10",onClick:k=>{v(k),i(!1)}}),T.jsxs("div",{className:"absolute right-0 top-full z-20 mt-1.5 w-80 rounded-md border border-edge bg-surface shadow-lg",children:[T.jsxs("div",{className:"flex items-center justify-between border-b border-edge px-3 py-2",children:[T.jsxs("span",{className:"text-xs font-semibold uppercase tracking-wide text-ink-muted",children:["Run sources",a.size>0&&` (${a.size}/${d.length})`]}),T.jsxs("button",{onClick:k=>{v(k),w()},className:"text-xs text-accent hover:underline",children:[x?"Clear":"Select all",s.trim()&&" shown"]})]}),d.length>8&&T.jsx("div",{className:"border-b border-edge px-3 py-2",children:T.jsx("input",{type:"text",value:s,onChange:k=>c(k.target.value),onClick:k=>k.stopPropagation(),placeholder:"Filter sources…",className:"w-full rounded-md border border-edge bg-page px-2 py-1 text-xs text-ink-secondary placeholder:text-ink-muted focus:border-accent focus:outline-none"})}),T.jsxs("ul",{className:"max-h-64 overflow-y-auto py-1",children:[p.map(k=>T.jsx("li",{children:T.jsxs("label",{className:"flex cursor-pointer items-center gap-2 px-3 py-1.5 text-sm text-ink-secondary hover:bg-edge/20",onClick:M=>M.stopPropagation(),children:[T.jsx("input",{type:"checkbox",checked:a.has(k.index),onChange:()=>y(k.index),className:"accent-accent"}),T.jsx("span",{className:"truncate",children:k.product_name}),T.jsxs("span",{className:"ml-auto shrink-0 text-xs text-ink-muted",children:[k.type,k.version?` · ${k.version}`:""]})]})},k.index)),p.length===0&&T.jsxs("li",{className:"px-3 py-2 text-xs text-ink-muted",children:["No sources match “",s,"”."]})]}),T.jsx("div",{className:"border-t border-edge px-3 py-2",children:T.jsxs("button",{onClick:k=>{v(k),f.mutate([...a])},disabled:a.size===0||g,className:"w-full rounded-md bg-accent px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40",children:["Run ",a.size>0?`${a.size} selected`:"selected"," source",a.size===1?"":"s"]})})]})]})]})}const C7={website:"🌐",github:"🐙",zendesk:"🎫",local_directory:"📁",code:"⌨️",s3:"🪣"};function fI({sources:e,max:t}){const[n,r]=O.useState(!1);if(e.length===0)return T.jsx("span",{className:"text-xs text-ink-muted",children:"no sources"});const i=n||t===void 0?e.length:t,a=e.slice(0,i),l=e.length-a.length;return T.jsxs("div",{className:"flex flex-wrap items-center gap-1.5",children:[a.map((s,c)=>T.jsxs("span",{title:`${s.type}${s.version?` · ${s.version}`:""}`,className:"inline-flex items-center gap-1 rounded-full border border-edge bg-page px-2 py-0.5 text-xs text-ink-secondary",children:[T.jsx("span",{"aria-hidden":!0,className:"text-[10px]",children:C7[s.type]??"•"}),s.product_name,T.jsx("span",{className:"text-ink-muted",children:s.type})]},`${s.product_name}-${s.type}-${c}`)),l>0&&T.jsxs("button",{onClick:s=>{s.preventDefault(),r(!0)},className:"rounded-full border border-edge px-2 py-0.5 text-xs text-ink-muted transition hover:border-accent hover:text-accent",children:["+",l," more"]})]})}function E7(){const e=Mv(),{data:t,isLoading:n,error:r}=Wi({queryKey:["configs"],queryFn:Ct.configs}),{data:i}=Wi({queryKey:["health"],queryFn:Ct.health}),[a,l]=O.useState(!1),s=t?.filter(c=>c.last_run?.status==="running"||c.last_run?.status==="queued")??[];return n?T.jsx("p",{className:"text-ink-muted",children:"Loading…"}):r?T.jsxs("p",{className:"text-critical",children:["Failed to load configs: ",String(r.message)]}):T.jsxs("div",{className:"space-y-6",children:[s.length>0&&T.jsx("div",{className:"rounded-lg border border-accent/30 bg-surface px-4 py-3",children:T.jsxs("p",{className:"text-sm text-ink-secondary",children:[T.jsxs("span",{className:"font-medium text-accent",children:["● ",s.length," config",s.length>1?"s":""," syncing now"]})," — ",s.map((c,f)=>T.jsxs("span",{children:[f>0&&", ",T.jsx(yi,{to:`/runs/${c.last_run.id}`,className:"text-accent hover:underline",children:c.name})]},c.id))]})}),T.jsxs("div",{className:"flex items-center justify-between",children:[T.jsx("h1",{className:"text-xl font-semibold tracking-tight",children:"Configurations"}),i?.mode==="rw"&&T.jsx("button",{onClick:()=>l(!0),className:"rounded-md bg-accent px-3.5 py-1.5 text-sm font-medium text-white transition hover:opacity-90",children:"New config"})]}),a&&T.jsxs("div",{className:"rounded-lg border border-edge bg-page p-4",children:[T.jsx("h2",{className:"mb-4 text-sm font-semibold",children:"Create a new config"}),T.jsx(uI,{mode:"create",onSaved:c=>{l(!1),e(`/configs/${c.id}`)},onCancel:()=>l(!1)})]}),t&&t.length===0&&T.jsxs("p",{className:"rounded-lg border border-edge bg-surface px-4 py-8 text-center text-sm text-ink-muted",children:["No configs loaded. Pass config files to ",T.jsx("code",{className:"text-ink-secondary",children:"doc2vec controller"}),i?.mode==="rw"&&" or create one with the button above","."]}),T.jsx("div",{className:"grid gap-4",children:t?.map(c=>T.jsx(yi,{to:`/configs/${c.id}`,className:"block rounded-lg border border-edge bg-surface p-4 transition hover:border-accent/50",children:T.jsxs("div",{className:"flex flex-col gap-3 md:grid md:grid-cols-[minmax(9rem,1fr)_12rem_15rem_8rem_auto] md:items-start md:gap-x-6",children:[T.jsxs("div",{className:"min-w-0",children:[T.jsxs("div",{className:"flex items-center gap-2",children:[T.jsx("h2",{className:"truncate font-semibold",children:c.name}),c.parse_error&&T.jsx("span",{className:"shrink-0 rounded-full border border-critical/40 px-2 py-0.5 text-xs text-critical",title:c.parse_error,children:"invalid"})]}),T.jsx("div",{className:"mt-1.5",children:T.jsx(fI,{sources:c.source_summary,max:4})})]}),T.jsxs("div",{className:"text-sm",children:[T.jsx("p",{className:"text-xs uppercase tracking-wide text-ink-muted",children:"Schedule"}),T.jsx("p",{className:"mt-0.5 text-ink-secondary",title:c.schedule??void 0,children:gO(c.schedule)})]}),T.jsxs("div",{className:"text-sm",children:[T.jsx("p",{className:"text-xs uppercase tracking-wide text-ink-muted",children:"Last run"}),T.jsx("div",{className:"mt-0.5 flex flex-wrap items-center gap-2",children:c.last_run?T.jsxs(T.Fragment,{children:[T.jsx(_v,{status:c.last_run.status}),T.jsxs("span",{className:"text-xs text-ink-muted",title:Fa(c.last_run.queued_at),children:[H1(c.last_run.finished_at??c.last_run.queued_at),c.last_run.finished_at&&c.last_run.started_at&&` · ${mO(c.last_run.started_at,c.last_run.finished_at)}`]})]}):T.jsx("span",{className:"text-xs text-ink-muted",children:"never"})})]}),T.jsxs("div",{className:"text-sm",children:[T.jsx("p",{className:"text-xs uppercase tracking-wide text-ink-muted",children:"Next run"}),T.jsx("p",{className:"mt-0.5 text-ink-secondary",title:c.next_run?Fa(c.next_run):void 0,children:c.next_run?H1(c.next_run):"—"})]}),T.jsx("div",{className:"md:self-center",children:T.jsx(cI,{config:c})})]})},c.id))})]})}function aS(){return aS=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)({}).hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},aS.apply(null,arguments)}function T7(e,t){if(e==null)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.indexOf(r)!==-1)continue;n[r]=e[r]}return n}let lS=[],hI=[];(()=>{let e="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(t=>t?parseInt(t,36):1);for(let t=0,n=0;t<e.length;t++)(t%2?hI:lS).push(n=n+e[t])})();function M7(e){if(e<768)return!1;for(let t=0,n=lS.length;;){let r=t+n>>1;if(e<lS[r])n=r;else if(e>=hI[r])t=r+1;else return!0;if(t==n)return!1}}function qM(e){return e>=127462&&e<=127487}const WM=8205;function P7(e,t,n=!0,r=!0){return(n?dI:D7)(e,t,r)}function dI(e,t,n){if(t==e.length)return t;t&&pI(e.charCodeAt(t))&&mI(e.charCodeAt(t-1))&&t--;let r=bx(e,t);for(t+=VM(r);t<e.length;){let i=bx(e,t);if(r==WM||i==WM||n&&M7(i))t+=VM(i),r=i;else if(qM(i)){let a=0,l=t-2;for(;l>=0&&qM(bx(e,l));)a++,l-=2;if(a%2==0)break;t+=2}else break}return t}function D7(e,t,n){for(;t>1;){let r=dI(e,t-2,n);if(r<t)return r;t--}return 0}function bx(e,t){let n=e.charCodeAt(t);if(!mI(n)||t+1==e.length)return n;let r=e.charCodeAt(t+1);return pI(r)?(n-55296<<10)+(r-56320)+65536:n}function pI(e){return e>=56320&&e<57344}function mI(e){return e>=55296&&e<56320}function VM(e){return e<65536?1:2}let Je=class gI{lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid position ${t} in document of length ${this.length}`);return this.lineInner(t,!1,1,0)}line(t){if(t<1||t>this.lines)throw new RangeError(`Invalid line number ${t} in ${this.lines}-line document`);return this.lineInner(t,!0,1,0)}replace(t,n,r){[t,n]=Ku(this,t,n);let i=[];return this.decompose(0,t,i,2),r.length&&r.decompose(0,r.length,i,3),this.decompose(n,this.length,i,1),Hi.from(i,this.length-(n-t)+r.length)}append(t){return this.replace(this.length,this.length,t)}slice(t,n=this.length){[t,n]=Ku(this,t,n);let r=[];return this.decompose(t,n,r,0),Hi.from(r,n-t)}eq(t){if(t==this)return!0;if(t.length!=this.length||t.lines!=this.lines)return!1;let n=this.scanIdentical(t,1),r=this.length-this.scanIdentical(t,-1),i=new Gf(this),a=new Gf(t);for(let l=n,s=n;;){if(i.next(l),a.next(l),l=0,i.lineBreak!=a.lineBreak||i.done!=a.done||i.value!=a.value)return!1;if(s+=i.value.length,i.done||s>=r)return!0}}iter(t=1){return new Gf(this,t)}iterRange(t,n=this.length){return new vI(this,t,n)}iterLines(t,n){let r;if(t==null)r=this.iter();else{n==null&&(n=this.lines+1);let i=this.line(t).from;r=this.iterRange(i,Math.max(i,n==this.lines+1?this.length:n<=1?0:this.line(n-1).to))}return new yI(r)}toString(){return this.sliceString(0)}toJSON(){let t=[];return this.flatten(t),t}constructor(){}static of(t){if(t.length==0)throw new RangeError("A document must have at least one line");return t.length==1&&!t[0]?gI.empty:t.length<=32?new It(t):Hi.from(It.split(t,[]))}};class It extends Je{constructor(t,n=_7(t)){super(),this.text=t,this.length=n}get lines(){return this.text.length}get children(){return null}lineInner(t,n,r,i){for(let a=0;;a++){let l=this.text[a],s=i+l.length;if((n?r:s)>=t)return new N7(i,s,r,l);i=s+1,r++}}decompose(t,n,r,i){let a=t<=0&&n>=this.length?this:new It(KM(this.text,t,n),Math.min(n,this.length)-Math.max(0,t));if(i&1){let l=r.pop(),s=Im(a.text,l.text.slice(),0,a.length);if(s.length<=32)r.push(new It(s,l.length+a.length));else{let c=s.length>>1;r.push(new It(s.slice(0,c)),new It(s.slice(c)))}}else r.push(a)}replace(t,n,r){if(!(r instanceof It))return super.replace(t,n,r);[t,n]=Ku(this,t,n);let i=Im(this.text,Im(r.text,KM(this.text,0,t)),n),a=this.length+r.length-(n-t);return i.length<=32?new It(i,a):Hi.from(It.split(i,[]),a)}sliceString(t,n=this.length,r=`
55
+ `)}return r}var w7={...AO,schema:YW,skipInvalid:!1,noRefs:!1,flowLevel:-1,transform:()=>{}};function O7(e,t={}){const n={...w7,...t},r=HV(e,n.schema,{noRefs:n.noRefs,skipInvalid:n.skipInvalid});return n.flowLevel>=0&&qV(r,(i,a)=>{if(!(a.depth<n.flowLevel))return i.style.flow=!0,tI}),n.transform(r),S7(r,{...W1(n,Object.keys(AO)),schema:n.schema})}const HM=[{key:"product_name",label:"Product name",type:"string",required:!0,placeholder:"istio",help:"Stored in chunk metadata; used to identify the source"},{key:"version",label:"Version",type:"string",required:!0,placeholder:"latest",help:"Required for all types except code (defaults to branch)"},{key:"max_size",label:"Max page size (bytes)",type:"number",placeholder:"1048576",help:"Pages/files larger than this are skipped"}],Fp={website:{label:"Website",fields:[{key:"url",label:"Base URL",type:"string",required:!0,placeholder:"https://docs.example.com/"},{key:"sitemap_url",label:"Sitemap URL",type:"string",placeholder:"https://docs.example.com/sitemap.xml",help:"Discovers pages not linked in navigation; lastmod dates enable cheap change detection"},{key:"markdown_store",label:"Store markdown in Postgres",type:"boolean",help:"Requires the top-level markdown store to be configured"}]},github:{label:"GitHub issues",fields:[{key:"repo",label:"Repository",type:"string",required:!0,placeholder:"owner/repo"},{key:"start_date",label:"Start date",type:"string",placeholder:"2025-01-01",help:"Only issues updated after this date (first run)"}]},local_directory:{label:"Local directory",fields:[{key:"path",label:"Directory path",type:"string",required:!0,placeholder:"/data/docs"},{key:"include_extensions",label:"Include extensions",type:"string_list",placeholder:".md, .txt, .pdf"},{key:"exclude_extensions",label:"Exclude extensions",type:"string_list",placeholder:".log, .tmp"},{key:"recursive",label:"Recurse into subdirectories",type:"boolean"},{key:"encoding",label:"File encoding",type:"string",placeholder:"utf8"},{key:"url_rewrite_prefix",label:"URL rewrite prefix",type:"string",placeholder:"https://mydomain.com",help:"Rewrites file:// URLs in chunk metadata"}]},zendesk:{label:"Zendesk",fields:[{key:"zendesk_subdomain",label:"Subdomain",type:"string",required:!0,placeholder:"mycompany",help:"mycompany.zendesk.com"},{key:"email",label:"User email",type:"string",required:!0,placeholder:"agent@company.com"},{key:"api_token",label:"API token",type:"string",required:!0,secret:!0,placeholder:"${ZENDESK_API_TOKEN}"},{key:"fetch_tickets",label:"Fetch tickets",type:"boolean"},{key:"fetch_articles",label:"Fetch help center articles",type:"boolean"},{key:"start_date",label:"Start date",type:"string",placeholder:"2025-01-01"},{key:"ticket_status",label:"Ticket statuses",type:"string_list",placeholder:"new, open, pending, hold, solved"},{key:"ticket_priority",label:"Ticket priorities",type:"string_list",placeholder:"urgent, high"},{key:"excluded_organizations",label:"Excluded organizations",type:"string_list"},{key:"include_internal_comments",label:"Include internal comments",type:"boolean"}]},code:{label:"Code",fields:[{key:"source",label:"Code source",type:"select",required:!0,options:["github","local_directory"]},{key:"repo",label:"Repository",type:"string",placeholder:"owner/repo",help:"When source is github"},{key:"branch",label:"Branch",type:"string",placeholder:"main",help:"When source is github"},{key:"path",label:"Directory path",type:"string",placeholder:"/data/src",help:"When source is local_directory"},{key:"include_extensions",label:"Include extensions",type:"string_list",placeholder:".ts, .go, .py"},{key:"exclude_extensions",label:"Exclude extensions",type:"string_list"},{key:"exclude_paths",label:"Exclude paths",type:"string_list",placeholder:"vendor/**, **/*_test.go",help:"Glob patterns relative to the source root"},{key:"recursive",label:"Recurse into subdirectories",type:"boolean"},{key:"chunk_size",label:"Chunk size (tokens)",type:"number",placeholder:"1500"},{key:"url_rewrite_prefix",label:"URL rewrite prefix",type:"string"}]},s3:{label:"S3 bucket",fields:[{key:"bucket",label:"Bucket",type:"string",required:!0,placeholder:"my-docs-bucket"},{key:"prefix",label:"Key prefix",type:"string",placeholder:"docs/"},{key:"region",label:"Region",type:"string",placeholder:"us-east-1"},{key:"endpoint",label:"Custom endpoint",type:"string",placeholder:"https://minio.local:9000",help:"For S3-compatible services (MinIO, LocalStack)"},{key:"include_extensions",label:"Include extensions",type:"string_list",placeholder:".md, .txt, .pdf"},{key:"exclude_extensions",label:"Exclude extensions",type:"string_list"},{key:"encoding",label:"Text encoding",type:"string",placeholder:"utf8"},{key:"url_rewrite_prefix",label:"URL rewrite prefix",type:"string",help:"Rewrites s3:// URLs in chunk metadata"}]}},vx={sqlite:{label:"SQLite (sqlite-vec)",fields:[{key:"db_path",label:"Database file",type:"string",placeholder:"./my-product.db"}]},qdrant:{label:"Qdrant",fields:[{key:"qdrant_url",label:"Qdrant URL",type:"string",placeholder:"http://localhost"},{key:"qdrant_port",label:"Port",type:"number",placeholder:"6333"},{key:"collection_name",label:"Collection",type:"string",placeholder:"my-product"},{key:"keyword_index",label:"Keyword index (BM25)",type:"boolean",help:"Stores a BM25 keyword vector per chunk so the MCP server can run hybrid search (HYBRID_SEARCH=true). Needs Qdrant 1.18 or later; existing chunks are backfilled on the next sync"}]}},FM={openai:{label:"OpenAI (or compatible)",fields:[{key:"api_key",label:"API key",type:"string",secret:!0,placeholder:"${OPENAI_API_KEY}",help:"Falls back to the OPENAI_API_KEY env var"},{key:"model",label:"Model",type:"string",placeholder:"text-embedding-3-large"},{key:"base_url",label:"Base URL",type:"string",placeholder:"http://localhost:11434/v1",help:"For Ollama or other OpenAI-compatible endpoints"}]},azure:{label:"Azure OpenAI",fields:[{key:"api_key",label:"API key",type:"string",secret:!0,placeholder:"${AZURE_OPENAI_KEY}"},{key:"endpoint",label:"Endpoint",type:"string",placeholder:"https://myresource.openai.azure.com"},{key:"deployment_name",label:"Deployment name",type:"string",placeholder:"text-embedding-3-large"},{key:"api_version",label:"API version",type:"string",placeholder:"2024-10-21"}]}},A7=[{key:"connection_string",label:"Connection string",type:"string",secret:!0,placeholder:"postgres://user:${PG_PASSWORD}@host:5432/db",help:"Either this, or the discrete fields below"},{key:"host",label:"Host",type:"string"},{key:"port",label:"Port",type:"number",placeholder:"5432"},{key:"database",label:"Database",type:"string"},{key:"user",label:"User",type:"string"},{key:"password",label:"Password",type:"string",secret:!0,placeholder:"${PG_PASSWORD}"},{key:"table_name",label:"Table name",type:"string",placeholder:"markdown_pages"}];function qp(e){const t={type:e,product_name:"",version:"latest",max_size:1048576};return e==="code"&&delete t.version,t.database_config={type:"sqlite",params:{db_path:""}},t}function k7(e,t){return t.reduce((n,r)=>n?.[r],e)}function sI(e,t,n){if(t.length===0)return n;const[r,...i]=t,a=Array.isArray(e)?[...e??[]]:{...e??{}};return i.length===0?n===void 0||n===""||Array.isArray(n)&&n.length===0?delete a[r]:a[r]=n:a[r]=sI(a[r],i,n),a}const $o="w-full rounded-md border border-edge bg-page px-3 py-1.5 text-sm outline-none focus:border-accent placeholder:text-ink-muted/60";function vu({field:e,value:t,onChange:n}){const[r,i]=O.useState(null);return e.type==="boolean"?T.jsxs("label",{className:"flex items-center gap-2 py-1.5 text-sm text-ink-secondary",children:[T.jsx("input",{type:"checkbox",checked:!!t,onChange:a=>n(a.target.checked||void 0),className:"h-4 w-4 accent-(--accent)"}),e.label,e.help&&T.jsxs("span",{className:"text-xs text-ink-muted",children:["— ",e.help]})]}):T.jsxs("label",{className:"block",children:[T.jsxs("span",{className:"mb-1 flex items-baseline gap-2 text-xs font-medium text-ink-secondary",children:[e.label,e.required&&T.jsx("span",{className:"text-critical",children:"*"}),e.secret&&T.jsxs("span",{className:"text-ink-muted",children:["use a ",T.jsx("code",{children:"${ENV_VAR}"})," placeholder"]})]}),e.type==="select"?T.jsxs("select",{value:t??"",onChange:a=>n(a.target.value||void 0),className:$o,children:[T.jsx("option",{value:"",children:"—"}),e.options.map(a=>T.jsx("option",{value:a,children:a},a))]}):e.type==="string_list"?T.jsx("input",{value:r??(Array.isArray(t)?t.join(", "):""),placeholder:e.placeholder,onChange:a=>i(a.target.value),onBlur:()=>{r!==null&&(n(r.split(",").map(a=>a.trim()).filter(Boolean)),i(null))},className:$o}):T.jsx("input",{type:"text",inputMode:e.type==="number"?"numeric":void 0,value:t??"",placeholder:e.placeholder,onChange:a=>{const l=a.target.value;e.type==="number"?n(l===""?void 0:Number(l)):n(l)},className:$o}),e.help&&T.jsx("span",{className:"mt-1 block text-xs text-ink-muted",children:e.help})]})}function yx({fields:e,doc:t,basePath:n,onSet:r}){return T.jsx("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:e.map(i=>T.jsx("div",{className:i.type==="boolean"?"sm:col-span-2":"",children:T.jsx(vu,{field:i,value:k7(t,[...n,i.key]),onChange:a=>r([...n,i.key],a)})},i.key))})}function Wp({title:e,subtitle:t,action:n,children:r}){return T.jsxs("section",{className:"rounded-lg border border-edge bg-surface p-4",children:[T.jsxs("div",{className:"mb-3 flex items-center gap-2",children:[T.jsx("h3",{className:"text-sm font-semibold",children:e}),t&&T.jsx("span",{className:"text-xs text-ink-muted",children:t}),T.jsx("div",{className:"ml-auto",children:n})]}),r]})}function uI(e){const t=io(),n=O.useMemo(()=>{if(e.mode==="edit"&&e.config)try{const A=zV(e.config.content);return A&&typeof A=="object"?{doc:A,parseError:null}:{doc:null,parseError:"config is not a YAML mapping"}}catch(A){return{doc:null,parseError:A instanceof Error?A.message:String(A)}}return{doc:{name:"",schedule:"",sources:[qp("website")]},parseError:null}},[e.mode,e.config]),[r,i]=O.useState(n.doc),[a,l]=O.useState(""),[s,c]=O.useState(null),f=Wh({mutationFn:async()=>{const A=O7(r,{lineWidth:120,noRefs:!0}),E=await Ct.validateConfig(A);if(!E.valid)throw new fh(400,E.error??"invalid config");if(e.mode==="create"){const k=a.trim()||`${(r.name||"config").toString().replace(/[^A-Za-z0-9._-]+/g,"-")}.yaml`;return Ct.createConfig(k,A)}return Ct.updateConfig(e.config.id,A,e.config.content_hash)},onSuccess:A=>{c(null),t.invalidateQueries({queryKey:["configs"]}),e.onSaved?.(A)},onError:A=>c(A.message)});if(n.parseError||!r)return T.jsxs("p",{className:"rounded-md border border-warning/50 bg-surface px-3 py-2 text-sm text-ink-secondary",children:["⚠ This config can't be edited as a form (",n.parseError,") — use the YAML view."]});const d=(A,E)=>i(k=>sI(k,A,E)),p=Array.isArray(r.sources)?r.sources:[],g=[];p.forEach((A,E)=>{const k=Fp[A?.type];for(const M of[...HM,...k?.fields??[]]){if(!M.required||A?.type==="code"&&M.key==="version")continue;const D=A?.[M.key];(D===void 0||D==="")&&g.push(`source ${E+1}: ${M.label}`)}});const v=r.embedding!==void 0,y=r.markdown_store!==void 0,x=f.error instanceof fh&&f.error.status===409,w=r.schedule??"";return T.jsxs("div",{className:"space-y-4",children:[x&&T.jsx("div",{className:"rounded-md border border-warning/50 bg-surface px-3 py-2 text-sm text-ink-secondary",children:"⚠ The file changed on disk since you loaded it. Reload the page and reapply your edits."}),s&&!x&&T.jsx("div",{className:"rounded-md border border-critical/50 bg-surface px-3 py-2 text-sm text-critical",children:s}),T.jsx(Wp,{title:"General",children:T.jsxs("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:[e.mode==="create"&&T.jsxs("label",{className:"block",children:[T.jsx("span",{className:"mb-1 block text-xs font-medium text-ink-secondary",children:"Filename"}),T.jsx("input",{value:a,onChange:A=>l(A.target.value),placeholder:`${(r.name||"my-config").toString().replace(/[^A-Za-z0-9._-]+/g,"-")}.yaml`,className:`${$o} font-mono`})]}),T.jsx(vu,{field:{key:"name",label:"Display name",type:"string",placeholder:"product-docs"},value:r.name,onChange:A=>d(["name"],A)}),T.jsxs("div",{children:[T.jsx(vu,{field:{key:"schedule",label:"Schedule (cron)",type:"string",placeholder:"0 2 * * *",help:"Leave empty for manual runs only"},value:w,onChange:A=>d(["schedule"],A)}),w&&T.jsx("p",{className:"mt-1 text-xs text-accent",children:gO(w)})]})]})}),p.map((A,E)=>{const k=Fp[A?.type]??Fp.website,M=A?.database_config?.type??"sqlite",D=vx[M]??vx.sqlite;return T.jsx(Wp,{title:`Source ${E+1}`,subtitle:A?.product_name||void 0,action:T.jsx("button",{onClick:()=>d(["sources"],p.filter((P,N)=>N!==E)),className:"text-xs text-ink-muted transition hover:text-critical",children:"Remove"}),children:T.jsxs("div",{className:"space-y-4",children:[T.jsxs("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:[T.jsxs("label",{className:"block",children:[T.jsx("span",{className:"mb-1 block text-xs font-medium text-ink-secondary",children:"Type"}),T.jsx("select",{value:A?.type??"website",onChange:P=>{const N={...qp(P.target.value),product_name:A?.product_name??"",...A?.version!==void 0&&P.target.value!=="code"&&{version:A.version},...A?.max_size!==void 0&&{max_size:A.max_size},database_config:A?.database_config??qp(P.target.value).database_config};d(["sources",E],N)},className:$o,children:Object.entries(Fp).map(([P,N])=>T.jsx("option",{value:P,children:N.label},P))})]}),HM.map(P=>T.jsx(vu,{field:A?.type==="code"&&P.key==="version"?{...P,required:!1,help:"Defaults to the branch name"}:P,value:A?.[P.key],onChange:N=>d(["sources",E,P.key],N)},P.key))]}),T.jsxs("div",{children:[T.jsxs("p",{className:"mb-2 text-xs font-semibold uppercase tracking-wide text-ink-muted",children:[k.label," options"]}),T.jsx(yx,{fields:k.fields,doc:r,basePath:["sources",E],onSet:d})]}),T.jsxs("div",{children:[T.jsx("p",{className:"mb-2 text-xs font-semibold uppercase tracking-wide text-ink-muted",children:"Vector database"}),T.jsxs("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:[T.jsxs("label",{className:"block",children:[T.jsx("span",{className:"mb-1 block text-xs font-medium text-ink-secondary",children:"Type"}),T.jsx("select",{value:M,onChange:P=>d(["sources",E,"database_config"],{type:P.target.value,params:{}}),className:$o,children:Object.entries(vx).map(([P,N])=>T.jsx("option",{value:P,children:N.label},P))})]}),D.fields.map(P=>T.jsx(vu,{field:P,value:A?.database_config?.params?.[P.key],onChange:N=>d(["sources",E,"database_config","params",P.key],N)},P.key))]})]})]})},E)}),T.jsx("button",{onClick:()=>d(["sources"],[...p,qp("website")]),className:"w-full rounded-lg border border-dashed border-edge py-2.5 text-sm font-medium text-ink-muted transition hover:border-accent hover:text-accent",children:"+ Add source"}),T.jsx(Wp,{title:"Embedding",subtitle:"optional — defaults to OpenAI text-embedding-3-large",action:T.jsxs("label",{className:"flex items-center gap-2 text-xs text-ink-secondary",children:[T.jsx("input",{type:"checkbox",checked:v,onChange:A=>d(["embedding"],A.target.checked?{provider:"openai"}:void 0),className:"h-4 w-4 accent-(--accent)"}),"customize"]}),children:v?T.jsxs("div",{className:"space-y-3",children:[T.jsxs("div",{className:"grid gap-x-4 gap-y-3 sm:grid-cols-2",children:[T.jsxs("label",{className:"block",children:[T.jsx("span",{className:"mb-1 block text-xs font-medium text-ink-secondary",children:"Provider"}),T.jsx("select",{value:r.embedding?.provider??"openai",onChange:A=>d(["embedding"],{provider:A.target.value}),className:$o,children:Object.entries(FM).map(([A,E])=>T.jsx("option",{value:A,children:E.label},A))})]}),T.jsx(vu,{field:{key:"dimension",label:"Dimension",type:"number",placeholder:"3072"},value:r.embedding?.dimension,onChange:A=>d(["embedding","dimension"],A)})]}),T.jsx(yx,{fields:FM[r.embedding?.provider??"openai"].fields,doc:r,basePath:["embedding",r.embedding?.provider??"openai"],onSet:d})]}):T.jsx("p",{className:"text-xs text-ink-muted",children:"Using defaults (OPENAI_API_KEY env var, text-embedding-3-large, 3072 dimensions)."})}),T.jsx(Wp,{title:"Markdown store",subtitle:"optional — store crawled markdown in Postgres",action:T.jsxs("label",{className:"flex items-center gap-2 text-xs text-ink-secondary",children:[T.jsx("input",{type:"checkbox",checked:y,onChange:A=>d(["markdown_store"],A.target.checked?{}:void 0),className:"h-4 w-4 accent-(--accent)"}),"enable"]}),children:y?T.jsx(yx,{fields:A7,doc:r,basePath:["markdown_store"],onSet:d}):T.jsx("p",{className:"text-xs text-ink-muted",children:"Disabled."})}),T.jsxs("div",{className:"flex items-center gap-3",children:[T.jsx("button",{onClick:()=>f.mutate(),disabled:f.isPending||p.length===0||g.length>0,className:"rounded-md bg-accent px-4 py-2 text-sm font-medium text-white transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40",children:f.isPending?"Saving…":e.mode==="create"?"Create config":"Save changes"}),e.onCancel&&T.jsx("button",{onClick:e.onCancel,className:"text-sm text-ink-muted hover:text-ink-secondary",children:"Cancel"}),g.length>0&&T.jsxs("span",{className:"text-xs text-warning",children:["missing: ",g.slice(0,3).join(", "),g.length>3?"…":""]}),p.length===0&&T.jsx("span",{className:"text-xs text-warning",children:"add at least one source"}),e.mode==="edit"&&T.jsx("span",{className:"ml-auto text-xs text-ink-muted",children:"Saving rewrites the YAML file (comments are not preserved)."})]})]})}function cI({config:e,variant:t="subtle"}){const n=io(),[r,i]=O.useState(!1),[a,l]=O.useState(new Set),[s,c]=O.useState(""),f=Wh({mutationFn:k=>Ct.triggerRun(e.id,k,e.content_hash),onSuccess:()=>{i(!1),l(new Set),c("")},onSettled:()=>n.invalidateQueries({queryKey:["configs"]})}),d=O.useMemo(()=>e.source_summary.map((k,M)=>({...k,index:M})),[e.source_summary]),p=O.useMemo(()=>{const k=s.trim().toLowerCase();return k?d.filter(M=>M.product_name.toLowerCase().includes(k)||M.type.toLowerCase().includes(k)):d},[d,s]),g=e.busy||!!e.parse_error||f.isPending,v=k=>{k.preventDefault(),k.stopPropagation()},y=k=>{l(M=>{const D=new Set(M);return D.has(k)?D.delete(k):D.add(k),D})},x=p.length>0&&p.every(k=>a.has(k.index)),w=()=>{l(k=>{const M=new Set(k);return x?p.forEach(D=>M.delete(D.index)):p.forEach(D=>M.add(D.index)),M})},A=t==="primary"?"bg-accent text-white transition hover:opacity-90 px-3.5 py-1.5 text-sm font-medium":"border border-edge bg-surface text-ink-secondary transition hover:border-accent hover:text-accent px-3 py-1 text-xs font-medium",E=t==="primary"?"bg-accent text-white transition hover:opacity-90 border-l border-white/25 px-2 py-1.5 text-sm":"border border-l-0 border-edge bg-surface text-ink-secondary transition hover:border-accent hover:text-accent px-1.5 py-1 text-xs";return T.jsxs("div",{className:"relative inline-flex items-center gap-2",onClick:v,children:[T.jsxs("span",{className:"inline-flex",children:[T.jsx("button",{onClick:k=>{v(k),f.mutate(void 0)},disabled:g,className:`${d.length>1?"rounded-l-md":"rounded-md"} ${A} disabled:cursor-not-allowed disabled:opacity-40`,children:e.busy?"Running…":"Run now"}),d.length>1&&T.jsx("button",{onClick:k=>{v(k),i(M=>!M)},disabled:g,"aria-label":"Choose sources to run",title:"Choose sources to run",className:`rounded-r-md ${E} disabled:cursor-not-allowed disabled:opacity-40`,children:"▾"})]}),f.error&&T.jsx("span",{className:"text-xs text-critical",children:f.error.message}),r&&T.jsxs(T.Fragment,{children:[T.jsx("div",{className:"fixed inset-0 z-10",onClick:k=>{v(k),i(!1)}}),T.jsxs("div",{className:"absolute right-0 top-full z-20 mt-1.5 w-80 rounded-md border border-edge bg-surface shadow-lg",children:[T.jsxs("div",{className:"flex items-center justify-between border-b border-edge px-3 py-2",children:[T.jsxs("span",{className:"text-xs font-semibold uppercase tracking-wide text-ink-muted",children:["Run sources",a.size>0&&` (${a.size}/${d.length})`]}),T.jsxs("button",{onClick:k=>{v(k),w()},className:"text-xs text-accent hover:underline",children:[x?"Clear":"Select all",s.trim()&&" shown"]})]}),d.length>8&&T.jsx("div",{className:"border-b border-edge px-3 py-2",children:T.jsx("input",{type:"text",value:s,onChange:k=>c(k.target.value),onClick:k=>k.stopPropagation(),placeholder:"Filter sources…",className:"w-full rounded-md border border-edge bg-page px-2 py-1 text-xs text-ink-secondary placeholder:text-ink-muted focus:border-accent focus:outline-none"})}),T.jsxs("ul",{className:"max-h-64 overflow-y-auto py-1",children:[p.map(k=>T.jsx("li",{children:T.jsxs("label",{className:"flex cursor-pointer items-center gap-2 px-3 py-1.5 text-sm text-ink-secondary hover:bg-edge/20",onClick:M=>M.stopPropagation(),children:[T.jsx("input",{type:"checkbox",checked:a.has(k.index),onChange:()=>y(k.index),className:"accent-accent"}),T.jsx("span",{className:"truncate",children:k.product_name}),T.jsxs("span",{className:"ml-auto shrink-0 text-xs text-ink-muted",children:[k.type,k.version?` · ${k.version}`:""]})]})},k.index)),p.length===0&&T.jsxs("li",{className:"px-3 py-2 text-xs text-ink-muted",children:["No sources match “",s,"”."]})]}),T.jsx("div",{className:"border-t border-edge px-3 py-2",children:T.jsxs("button",{onClick:k=>{v(k),f.mutate([...a])},disabled:a.size===0||g,className:"w-full rounded-md bg-accent px-3 py-1.5 text-xs font-medium text-white transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40",children:["Run ",a.size>0?`${a.size} selected`:"selected"," source",a.size===1?"":"s"]})})]})]})]})}const C7={website:"🌐",github:"🐙",zendesk:"🎫",local_directory:"📁",code:"⌨️",s3:"🪣"};function fI({sources:e,max:t}){const[n,r]=O.useState(!1);if(e.length===0)return T.jsx("span",{className:"text-xs text-ink-muted",children:"no sources"});const i=n||t===void 0?e.length:t,a=e.slice(0,i),l=e.length-a.length;return T.jsxs("div",{className:"flex flex-wrap items-center gap-1.5",children:[a.map((s,c)=>T.jsxs("span",{title:`${s.type}${s.version?` · ${s.version}`:""}`,className:"inline-flex items-center gap-1 rounded-full border border-edge bg-page px-2 py-0.5 text-xs text-ink-secondary",children:[T.jsx("span",{"aria-hidden":!0,className:"text-[10px]",children:C7[s.type]??"•"}),s.product_name,T.jsx("span",{className:"text-ink-muted",children:s.type})]},`${s.product_name}-${s.type}-${c}`)),l>0&&T.jsxs("button",{onClick:s=>{s.preventDefault(),r(!0)},className:"rounded-full border border-edge px-2 py-0.5 text-xs text-ink-muted transition hover:border-accent hover:text-accent",children:["+",l," more"]})]})}function E7(){const e=Mv(),{data:t,isLoading:n,error:r}=Wi({queryKey:["configs"],queryFn:Ct.configs}),{data:i}=Wi({queryKey:["health"],queryFn:Ct.health}),[a,l]=O.useState(!1),s=t?.filter(c=>c.last_run?.status==="running"||c.last_run?.status==="queued")??[];return n?T.jsx("p",{className:"text-ink-muted",children:"Loading…"}):r?T.jsxs("p",{className:"text-critical",children:["Failed to load configs: ",String(r.message)]}):T.jsxs("div",{className:"space-y-6",children:[s.length>0&&T.jsx("div",{className:"rounded-lg border border-accent/30 bg-surface px-4 py-3",children:T.jsxs("p",{className:"text-sm text-ink-secondary",children:[T.jsxs("span",{className:"font-medium text-accent",children:["● ",s.length," config",s.length>1?"s":""," syncing now"]})," — ",s.map((c,f)=>T.jsxs("span",{children:[f>0&&", ",T.jsx(yi,{to:`/runs/${c.last_run.id}`,className:"text-accent hover:underline",children:c.name})]},c.id))]})}),T.jsxs("div",{className:"flex items-center justify-between",children:[T.jsx("h1",{className:"text-xl font-semibold tracking-tight",children:"Configurations"}),i?.mode==="rw"&&T.jsx("button",{onClick:()=>l(!0),className:"rounded-md bg-accent px-3.5 py-1.5 text-sm font-medium text-white transition hover:opacity-90",children:"New config"})]}),a&&T.jsxs("div",{className:"rounded-lg border border-edge bg-page p-4",children:[T.jsx("h2",{className:"mb-4 text-sm font-semibold",children:"Create a new config"}),T.jsx(uI,{mode:"create",onSaved:c=>{l(!1),e(`/configs/${c.id}`)},onCancel:()=>l(!1)})]}),t&&t.length===0&&T.jsxs("p",{className:"rounded-lg border border-edge bg-surface px-4 py-8 text-center text-sm text-ink-muted",children:["No configs loaded. Pass config files to ",T.jsx("code",{className:"text-ink-secondary",children:"doc2vec controller"}),i?.mode==="rw"&&" or create one with the button above","."]}),T.jsx("div",{className:"grid gap-4",children:t?.map(c=>T.jsx(yi,{to:`/configs/${c.id}`,className:"block rounded-lg border border-edge bg-surface p-4 transition hover:border-accent/50",children:T.jsxs("div",{className:"flex flex-col gap-3 md:grid md:grid-cols-[minmax(9rem,1fr)_12rem_15rem_8rem_auto] md:items-start md:gap-x-6",children:[T.jsxs("div",{className:"min-w-0",children:[T.jsxs("div",{className:"flex items-center gap-2",children:[T.jsx("h2",{className:"truncate font-semibold",children:c.name}),c.parse_error&&T.jsx("span",{className:"shrink-0 rounded-full border border-critical/40 px-2 py-0.5 text-xs text-critical",title:c.parse_error,children:"invalid"})]}),T.jsx("div",{className:"mt-1.5",children:T.jsx(fI,{sources:c.source_summary,max:4})})]}),T.jsxs("div",{className:"text-sm",children:[T.jsx("p",{className:"text-xs uppercase tracking-wide text-ink-muted",children:"Schedule"}),T.jsx("p",{className:"mt-0.5 text-ink-secondary",title:c.schedule??void 0,children:gO(c.schedule)})]}),T.jsxs("div",{className:"text-sm",children:[T.jsx("p",{className:"text-xs uppercase tracking-wide text-ink-muted",children:"Last run"}),T.jsx("div",{className:"mt-0.5 flex flex-wrap items-center gap-2",children:c.last_run?T.jsxs(T.Fragment,{children:[T.jsx(_v,{status:c.last_run.status}),T.jsxs("span",{className:"text-xs text-ink-muted",title:Fa(c.last_run.queued_at),children:[H1(c.last_run.finished_at??c.last_run.queued_at),c.last_run.finished_at&&c.last_run.started_at&&` · ${mO(c.last_run.started_at,c.last_run.finished_at)}`]})]}):T.jsx("span",{className:"text-xs text-ink-muted",children:"never"})})]}),T.jsxs("div",{className:"text-sm",children:[T.jsx("p",{className:"text-xs uppercase tracking-wide text-ink-muted",children:"Next run"}),T.jsx("p",{className:"mt-0.5 text-ink-secondary",title:c.next_run?Fa(c.next_run):void 0,children:c.next_run?H1(c.next_run):"—"})]}),T.jsx("div",{className:"md:self-center",children:T.jsx(cI,{config:c})})]})},c.id))})]})}function aS(){return aS=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)({}).hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},aS.apply(null,arguments)}function T7(e,t){if(e==null)return{};var n={};for(var r in e)if({}.hasOwnProperty.call(e,r)){if(t.indexOf(r)!==-1)continue;n[r]=e[r]}return n}let lS=[],hI=[];(()=>{let e="lc,34,7n,7,7b,19,,,,2,,2,,,20,b,1c,l,g,,2t,7,2,6,2,2,,4,z,,u,r,2j,b,1m,9,9,,o,4,,9,,3,,5,17,3,3b,f,,w,1j,,,,4,8,4,,3,7,a,2,t,,1m,,,,2,4,8,,9,,a,2,q,,2,2,1l,,4,2,4,2,2,3,3,,u,2,3,,b,2,1l,,4,5,,2,4,,k,2,m,6,,,1m,,,2,,4,8,,7,3,a,2,u,,1n,,,,c,,9,,14,,3,,1l,3,5,3,,4,7,2,b,2,t,,1m,,2,,2,,3,,5,2,7,2,b,2,s,2,1l,2,,,2,4,8,,9,,a,2,t,,20,,4,,2,3,,,8,,29,,2,7,c,8,2q,,2,9,b,6,22,2,r,,,,,,1j,e,,5,,2,5,b,,10,9,,2u,4,,6,,2,2,2,p,2,4,3,g,4,d,,2,2,6,,f,,jj,3,qa,3,t,3,t,2,u,2,1s,2,,7,8,,2,b,9,,19,3,3b,2,y,,3a,3,4,2,9,,6,3,63,2,2,,1m,,,7,,,,,2,8,6,a,2,,1c,h,1r,4,1c,7,,,5,,14,9,c,2,w,4,2,2,,3,1k,,,2,3,,,3,1m,8,2,2,48,3,,d,,7,4,,6,,3,2,5i,1m,,5,ek,,5f,x,2da,3,3x,,2o,w,fe,6,2x,2,n9w,4,,a,w,2,28,2,7k,,3,,4,,p,2,5,,47,2,q,i,d,,12,8,p,b,1a,3,1c,,2,4,2,2,13,,1v,6,2,2,2,2,c,,8,,1b,,1f,,,3,2,2,5,2,,,16,2,8,,6m,,2,,4,,fn4,,kh,g,g,g,a6,2,gt,,6a,,45,5,1ae,3,,2,5,4,14,3,4,,4l,2,fx,4,ar,2,49,b,4w,,1i,f,1k,3,1d,4,2,2,1x,3,10,5,,8,1q,,c,2,1g,9,a,4,2,,2n,3,2,,,2,6,,4g,,3,8,l,2,1l,2,,,,,m,,e,7,3,5,5f,8,2,3,,,n,,29,,2,6,,,2,,,2,,2,6j,,2,4,6,2,,2,r,2,2d,8,2,,,2,2y,,,,2,6,,,2t,3,2,4,,5,77,9,,2,6t,,a,2,,,4,,40,4,2,2,4,,w,a,14,6,2,4,8,,9,6,2,3,1a,d,,2,ba,7,,6,,,2a,m,2,7,,2,,2,3e,6,3,,,2,,7,,,20,2,3,,,,9n,2,f0b,5,1n,7,t4,,1r,4,29,,f5k,2,43q,,,3,4,5,8,8,2,7,u,4,44,3,1iz,1j,4,1e,8,,e,,m,5,,f,11s,7,,h,2,7,,2,,5,79,7,c5,4,15s,7,31,7,240,5,gx7k,2o,3k,6o".split(",").map(t=>t?parseInt(t,36):1);for(let t=0,n=0;t<e.length;t++)(t%2?hI:lS).push(n=n+e[t])})();function M7(e){if(e<768)return!1;for(let t=0,n=lS.length;;){let r=t+n>>1;if(e<lS[r])n=r;else if(e>=hI[r])t=r+1;else return!0;if(t==n)return!1}}function qM(e){return e>=127462&&e<=127487}const WM=8205;function P7(e,t,n=!0,r=!0){return(n?dI:D7)(e,t,r)}function dI(e,t,n){if(t==e.length)return t;t&&pI(e.charCodeAt(t))&&mI(e.charCodeAt(t-1))&&t--;let r=bx(e,t);for(t+=VM(r);t<e.length;){let i=bx(e,t);if(r==WM||i==WM||n&&M7(i))t+=VM(i),r=i;else if(qM(i)){let a=0,l=t-2;for(;l>=0&&qM(bx(e,l));)a++,l-=2;if(a%2==0)break;t+=2}else break}return t}function D7(e,t,n){for(;t>1;){let r=dI(e,t-2,n);if(r<t)return r;t--}return 0}function bx(e,t){let n=e.charCodeAt(t);if(!mI(n)||t+1==e.length)return n;let r=e.charCodeAt(t+1);return pI(r)?(n-55296<<10)+(r-56320)+65536:n}function pI(e){return e>=56320&&e<57344}function mI(e){return e>=55296&&e<56320}function VM(e){return e<65536?1:2}let Je=class gI{lineAt(t){if(t<0||t>this.length)throw new RangeError(`Invalid position ${t} in document of length ${this.length}`);return this.lineInner(t,!1,1,0)}line(t){if(t<1||t>this.lines)throw new RangeError(`Invalid line number ${t} in ${this.lines}-line document`);return this.lineInner(t,!0,1,0)}replace(t,n,r){[t,n]=Ku(this,t,n);let i=[];return this.decompose(0,t,i,2),r.length&&r.decompose(0,r.length,i,3),this.decompose(n,this.length,i,1),Hi.from(i,this.length-(n-t)+r.length)}append(t){return this.replace(this.length,this.length,t)}slice(t,n=this.length){[t,n]=Ku(this,t,n);let r=[];return this.decompose(t,n,r,0),Hi.from(r,n-t)}eq(t){if(t==this)return!0;if(t.length!=this.length||t.lines!=this.lines)return!1;let n=this.scanIdentical(t,1),r=this.length-this.scanIdentical(t,-1),i=new Gf(this),a=new Gf(t);for(let l=n,s=n;;){if(i.next(l),a.next(l),l=0,i.lineBreak!=a.lineBreak||i.done!=a.done||i.value!=a.value)return!1;if(s+=i.value.length,i.done||s>=r)return!0}}iter(t=1){return new Gf(this,t)}iterRange(t,n=this.length){return new vI(this,t,n)}iterLines(t,n){let r;if(t==null)r=this.iter();else{n==null&&(n=this.lines+1);let i=this.line(t).from;r=this.iterRange(i,Math.max(i,n==this.lines+1?this.length:n<=1?0:this.line(n-1).to))}return new yI(r)}toString(){return this.sliceString(0)}toJSON(){let t=[];return this.flatten(t),t}constructor(){}static of(t){if(t.length==0)throw new RangeError("A document must have at least one line");return t.length==1&&!t[0]?gI.empty:t.length<=32?new It(t):Hi.from(It.split(t,[]))}};class It extends Je{constructor(t,n=_7(t)){super(),this.text=t,this.length=n}get lines(){return this.text.length}get children(){return null}lineInner(t,n,r,i){for(let a=0;;a++){let l=this.text[a],s=i+l.length;if((n?r:s)>=t)return new N7(i,s,r,l);i=s+1,r++}}decompose(t,n,r,i){let a=t<=0&&n>=this.length?this:new It(KM(this.text,t,n),Math.min(n,this.length)-Math.max(0,t));if(i&1){let l=r.pop(),s=Im(a.text,l.text.slice(),0,a.length);if(s.length<=32)r.push(new It(s,l.length+a.length));else{let c=s.length>>1;r.push(new It(s.slice(0,c)),new It(s.slice(c)))}}else r.push(a)}replace(t,n,r){if(!(r instanceof It))return super.replace(t,n,r);[t,n]=Ku(this,t,n);let i=Im(this.text,Im(r.text,KM(this.text,0,t)),n),a=this.length+r.length-(n-t);return i.length<=32?new It(i,a):Hi.from(It.split(i,[]),a)}sliceString(t,n=this.length,r=`
56
56
  `){[t,n]=Ku(this,t,n);let i="";for(let a=0,l=0;a<=n&&l<this.text.length;l++){let s=this.text[l],c=a+s.length;a>t&&l&&(i+=r),t<c&&n>a&&(i+=s.slice(Math.max(0,t-a),n-a)),a=c+1}return i}flatten(t){for(let n of this.text)t.push(n)}scanIdentical(){return 0}static split(t,n){let r=[],i=-1;for(let a of t)r.push(a),i+=a.length+1,r.length==32&&(n.push(new It(r,i)),r=[],i=-1);return i>-1&&n.push(new It(r,i)),n}}class Hi extends Je{constructor(t,n){super(),this.children=t,this.length=n,this.lines=0;for(let r of t)this.lines+=r.lines}lineInner(t,n,r,i){for(let a=0;;a++){let l=this.children[a],s=i+l.length,c=r+l.lines-1;if((n?c:s)>=t)return l.lineInner(t,n,r,i);i=s+1,r=c+1}}decompose(t,n,r,i){for(let a=0,l=0;l<=n&&a<this.children.length;a++){let s=this.children[a],c=l+s.length;if(t<=c&&n>=l){let f=i&((l<=t?1:0)|(c>=n?2:0));l>=t&&c<=n&&!f?r.push(s):s.decompose(t-l,n-l,r,f)}l=c+1}}replace(t,n,r){if([t,n]=Ku(this,t,n),r.lines<this.lines)for(let i=0,a=0;i<this.children.length;i++){let l=this.children[i],s=a+l.length;if(t>=a&&n<=s){let c=l.replace(t-a,n-a,r),f=this.lines-l.lines+c.lines;if(c.lines<f>>4&&c.lines>f>>6){let d=this.children.slice();return d[i]=c,new Hi(d,this.length-(n-t)+r.length)}return super.replace(a,s,c)}a=s+1}return super.replace(t,n,r)}sliceString(t,n=this.length,r=`
57
57
  `){[t,n]=Ku(this,t,n);let i="";for(let a=0,l=0;a<this.children.length&&l<=n;a++){let s=this.children[a],c=l+s.length;l>t&&a&&(i+=r),t<c&&n>l&&(i+=s.sliceString(t-l,n-l,r)),l=c+1}return i}flatten(t){for(let n of this.children)n.flatten(t)}scanIdentical(t,n){if(!(t instanceof Hi))return 0;let r=0,[i,a,l,s]=n>0?[0,0,this.children.length,t.children.length]:[this.children.length-1,t.children.length-1,-1,-1];for(;;i+=n,a+=n){if(i==l||a==s)return r;let c=this.children[i],f=t.children[a];if(c!=f)return r+c.scanIdentical(f,n);r+=c.length+1}}static from(t,n=t.reduce((r,i)=>r+i.length+1,-1)){let r=0;for(let v of t)r+=v.lines;if(r<32){let v=[];for(let y of t)y.flatten(v);return new It(v,n)}let i=Math.max(32,r>>5),a=i<<1,l=i>>1,s=[],c=0,f=-1,d=[];function p(v){let y;if(v.lines>a&&v instanceof Hi)for(let x of v.children)p(x);else v.lines>l&&(c>l||!c)?(g(),s.push(v)):v instanceof It&&c&&(y=d[d.length-1])instanceof It&&v.lines+y.lines<=32?(c+=v.lines,f+=v.length+1,d[d.length-1]=new It(y.text.concat(v.text),y.length+1+v.length)):(c+v.lines>i&&g(),c+=v.lines,f+=v.length+1,d.push(v))}function g(){c!=0&&(s.push(d.length==1?d[0]:Hi.from(d,f)),f=-1,c=d.length=0)}for(let v of t)p(v);return g(),s.length==1?s[0]:new Hi(s,n)}}Je.empty=new It([""],0);function _7(e){let t=-1;for(let n of e)t+=n.length+1;return t}function Im(e,t,n=0,r=1e9){for(let i=0,a=0,l=!0;a<e.length&&i<=r;a++){let s=e[a],c=i+s.length;c>=n&&(c>r&&(s=s.slice(0,r-i)),i<n&&(s=s.slice(n-i)),l?(t[t.length-1]+=s,l=!1):t.push(s)),i=c+1}return t}function KM(e,t,n){return Im(e,[""],t,n)}class Gf{constructor(t,n=1){this.dir=n,this.done=!1,this.lineBreak=!1,this.value="",this.nodes=[t],this.offsets=[n>0?1:(t instanceof It?t.text.length:t.children.length)<<1]}nextInner(t,n){for(this.done=this.lineBreak=!1;;){let r=this.nodes.length-1,i=this.nodes[r],a=this.offsets[r],l=a>>1,s=i instanceof It?i.text.length:i.children.length;if(l==(n>0?s:0)){if(r==0)return this.done=!0,this.value="",this;n>0&&this.offsets[r-1]++,this.nodes.pop(),this.offsets.pop()}else if((a&1)==(n>0?0:1)){if(this.offsets[r]+=n,t==0)return this.lineBreak=!0,this.value=`
58
58
  `,this;t--}else if(i instanceof It){let c=i.text[l+(n<0?-1:0)];if(this.offsets[r]+=n,c.length>Math.max(0,t))return this.value=t==0?c:n>0?c.slice(t):c.slice(0,c.length-t),this;t-=c.length}else{let c=i.children[l+(n<0?-1:0)];t>c.length?(t-=c.length,this.offsets[r]+=n):(n<0&&this.offsets[r]--,this.nodes.push(c),this.offsets.push(n>0?1:(c instanceof It?c.text.length:c.children.length)<<1))}}}next(t=0){return t<0&&(this.nextInner(-t,-this.dir),t=this.value.length),this.nextInner(t,this.dir)}}class vI{constructor(t,n,r){this.value="",this.done=!1,this.cursor=new Gf(t,n>r?-1:1),this.pos=n>r?t.length:0,this.from=Math.min(n,r),this.to=Math.max(n,r)}nextInner(t,n){if(n<0?this.pos<=this.from:this.pos>=this.to)return this.value="",this.done=!0,this;t+=Math.max(0,n<0?this.pos-this.to:this.from-this.pos);let r=n<0?this.pos-this.from:this.to-this.pos;t>r&&(t=r),r-=t;let{value:i}=this.cursor.next(t);return this.pos+=(i.length+t)*n,this.value=i.length<=r?i:n<0?i.slice(i.length-r):i.slice(0,r),this.done=!this.value,this}next(t=0){return t<0?t=Math.max(t,this.from-this.pos):t>0&&(t=Math.min(t,this.to-this.pos)),this.nextInner(t,this.cursor.dir)}get lineBreak(){return this.cursor.lineBreak&&this.value!=""}}class yI{constructor(t){this.inner=t,this.afterBreak=!0,this.value="",this.done=!1}next(t=0){let{done:n,lineBreak:r,value:i}=this.inner.next(t);return n&&this.afterBreak?(this.value="",this.afterBreak=!1):n?(this.done=!0,this.value=""):r?this.afterBreak?this.value="":(this.afterBreak=!0,this.next()):(this.value=i,this.afterBreak=!1),this}get lineBreak(){return!1}}typeof Symbol<"u"&&(Je.prototype[Symbol.iterator]=function(){return this.iter()},Gf.prototype[Symbol.iterator]=vI.prototype[Symbol.iterator]=yI.prototype[Symbol.iterator]=function(){return this});let N7=class{constructor(t,n,r,i){this.from=t,this.to=n,this.number=r,this.text=i}get length(){return this.to-this.from}};function Ku(e,t,n){return t=Math.max(0,Math.min(e.length,t)),[t,Math.max(t,Math.min(e.length,n))]}function on(e,t,n=!0,r=!0){return P7(e,t,n,r)}function R7(e){return e>=56320&&e<57344}function j7(e){return e>=55296&&e<56320}function ar(e,t){let n=e.charCodeAt(t);if(!j7(n)||t+1==e.length)return n;let r=e.charCodeAt(t+1);return R7(r)?(n-55296<<10)+(r-56320)+65536:n}function EO(e){return e<=65535?String.fromCharCode(e):(e-=65536,String.fromCharCode((e>>10)+55296,(e&1023)+56320))}function Fi(e){return e<65536?1:2}const oS=/\r\n?|\n/;var Kn=(function(e){return e[e.Simple=0]="Simple",e[e.TrackDel=1]="TrackDel",e[e.TrackBefore=2]="TrackBefore",e[e.TrackAfter=3]="TrackAfter",e})(Kn||(Kn={}));class Zi{constructor(t){this.sections=t}get length(){let t=0;for(let n=0;n<this.sections.length;n+=2)t+=this.sections[n];return t}get newLength(){let t=0;for(let n=0;n<this.sections.length;n+=2){let r=this.sections[n+1];t+=r<0?this.sections[n]:r}return t}get empty(){return this.sections.length==0||this.sections.length==2&&this.sections[1]<0}iterGaps(t){for(let n=0,r=0,i=0;n<this.sections.length;){let a=this.sections[n++],l=this.sections[n++];l<0?(t(r,i,a),i+=a):i+=l,r+=a}}iterChangedRanges(t,n=!1){sS(this,t,n)}get invertedDesc(){let t=[];for(let n=0;n<this.sections.length;){let r=this.sections[n++],i=this.sections[n++];i<0?t.push(r,i):t.push(i,r)}return new Zi(t)}composeDesc(t){return this.empty?t:t.empty?this:bI(this,t)}mapDesc(t,n=!1){return t.empty?this:uS(this,t,n)}mapPos(t,n=-1,r=Kn.Simple){let i=0,a=0;for(let l=0;l<this.sections.length;){let s=this.sections[l++],c=this.sections[l++],f=i+s;if(c<0){if(f>t)return a+(t-i);a+=s}else{if(r!=Kn.Simple&&f>=t&&(r==Kn.TrackDel&&i<t&&f>t||r==Kn.TrackBefore&&i<t||r==Kn.TrackAfter&&f>t))return null;if(f>t||f==t&&n<0&&!s)return t==i||n<0?a:a+c;a+=c}i=f}if(t>i)throw new RangeError(`Position ${t} is out of range for changeset of length ${i}`);return a}touchesRange(t,n=t){for(let r=0,i=0;r<this.sections.length&&i<=n;){let a=this.sections[r++],l=this.sections[r++],s=i+a;if(l>=0&&i<=n&&s>=t)return i<t&&s>n?"cover":!0;i=s}return!1}toString(){let t="";for(let n=0;n<this.sections.length;){let r=this.sections[n++],i=this.sections[n++];t+=(t?" ":"")+r+(i>=0?":"+i:"")}return t}toJSON(){return this.sections}static fromJSON(t){if(!Array.isArray(t)||t.length%2||t.some(n=>typeof n!="number"))throw new RangeError("Invalid JSON representation of ChangeDesc");return new Zi(t)}static create(t){return new Zi(t)}}class Yt extends Zi{constructor(t,n){super(t),this.inserted=n}apply(t){if(this.length!=t.length)throw new RangeError("Applying change set to a document with the wrong length");return sS(this,(n,r,i,a,l)=>t=t.replace(i,i+(r-n),l),!1),t}mapDesc(t,n=!1){return uS(this,t,n,!0)}invert(t){let n=this.sections.slice(),r=[];for(let i=0,a=0;i<n.length;i+=2){let l=n[i],s=n[i+1];if(s>=0){n[i]=s,n[i+1]=l;let c=i>>1;for(;r.length<c;)r.push(Je.empty);r.push(l?t.slice(a,a+l):Je.empty)}a+=l}return new Yt(n,r)}compose(t){return this.empty?t:t.empty?this:bI(this,t,!0)}map(t,n=!1){return t.empty?this:uS(this,t,n,!0)}iterChanges(t,n=!1){sS(this,t,n)}get desc(){return Zi.create(this.sections)}filter(t){let n=[],r=[],i=[],a=new gh(this);e:for(let l=0,s=0;;){let c=l==t.length?1e9:t[l++];for(;s<c||s==c&&a.len==0;){if(a.done)break e;let d=Math.min(a.len,c-s);Tn(i,d,-1);let p=a.ins==-1?-1:a.off==0?a.ins:0;Tn(n,d,p),p>0&&Ul(r,n,a.text),a.forward(d),s+=d}let f=t[l++];for(;s<f;){if(a.done)break e;let d=Math.min(a.len,f-s);Tn(n,d,-1),Tn(i,d,a.ins==-1?-1:a.off==0?a.ins:0),a.forward(d),s+=d}}return{changes:new Yt(n,r),filtered:Zi.create(i)}}toJSON(){let t=[];for(let n=0;n<this.sections.length;n+=2){let r=this.sections[n],i=this.sections[n+1];i<0?t.push(r):i==0?t.push([r]):t.push([r].concat(this.inserted[n>>1].toJSON()))}return t}static of(t,n,r){let i=[],a=[],l=0,s=null;function c(d=!1){if(!d&&!i.length)return;l<n&&Tn(i,n-l,-1);let p=new Yt(i,a);s=s?s.compose(p.map(s)):p,i=[],a=[],l=0}function f(d){if(Array.isArray(d))for(let p of d)f(p);else if(d instanceof Yt){if(d.length!=n)throw new RangeError(`Mismatched change set length (got ${d.length}, expected ${n})`);c(),s=s?s.compose(d.map(s)):d}else{let{from:p,to:g=p,insert:v}=d;if(p>g||p<0||g>n)throw new RangeError(`Invalid change range ${p} to ${g} (in doc of length ${n})`);let y=v?typeof v=="string"?Je.of(v.split(r||oS)):v:Je.empty,x=y.length;if(p==g&&x==0)return;p<l&&c(),p>l&&Tn(i,p-l,-1),Tn(i,g-p,x),Ul(a,i,y),l=g}}return f(t),c(!s),s}static empty(t){return new Yt(t?[t,-1]:[],[])}static fromJSON(t){if(!Array.isArray(t))throw new RangeError("Invalid JSON representation of ChangeSet");let n=[],r=[];for(let i=0;i<t.length;i++){let a=t[i];if(typeof a=="number")n.push(a,-1);else{if(!Array.isArray(a)||typeof a[0]!="number"||a.some((l,s)=>s&&typeof l!="string"))throw new RangeError("Invalid JSON representation of ChangeSet");if(a.length==1)n.push(a[0],0);else{for(;r.length<i;)r.push(Je.empty);r[i]=Je.of(a.slice(1)),n.push(a[0],r[i].length)}}}return new Yt(n,r)}static createSet(t,n){return new Yt(t,n)}}function Tn(e,t,n,r=!1){if(t==0&&n<=0)return;let i=e.length-2;i>=0&&n<=0&&n==e[i+1]?e[i]+=t:i>=0&&t==0&&e[i]==0?e[i+1]+=n:r?(e[i]+=t,e[i+1]+=n):e.push(t,n)}function Ul(e,t,n){if(n.length==0)return;let r=t.length-2>>1;if(r<e.length)e[e.length-1]=e[e.length-1].append(n);else{for(;e.length<r;)e.push(Je.empty);e.push(n)}}function sS(e,t,n){let r=e.inserted;for(let i=0,a=0,l=0;l<e.sections.length;){let s=e.sections[l++],c=e.sections[l++];if(c<0)i+=s,a+=s;else{let f=i,d=a,p=Je.empty;for(;f+=s,d+=c,c&&r&&(p=p.append(r[l-2>>1])),!(n||l==e.sections.length||e.sections[l+1]<0);)s=e.sections[l++],c=e.sections[l++];t(i,f,a,d,p),i=f,a=d}}}function uS(e,t,n,r=!1){let i=[],a=r?[]:null,l=new gh(e),s=new gh(t);for(let c=-1;;){if(l.done&&s.len||s.done&&l.len)throw new Error("Mismatched change set lengths");if(l.ins==-1&&s.ins==-1){let f=Math.min(l.len,s.len);Tn(i,f,-1),l.forward(f),s.forward(f)}else if(s.ins>=0&&(l.ins<0||c==l.i||l.off==0&&(s.len<l.len||s.len==l.len&&!n))){let f=s.len;for(Tn(i,s.ins,-1);f;){let d=Math.min(l.len,f);l.ins>=0&&c<l.i&&l.len<=d&&(Tn(i,0,l.ins),a&&Ul(a,i,l.text),c=l.i),l.forward(d),f-=d}s.next()}else if(l.ins>=0){let f=0,d=l.len;for(;d;)if(s.ins==-1){let p=Math.min(d,s.len);f+=p,d-=p,s.forward(p)}else if(s.ins==0&&s.len<d)d-=s.len,s.next();else break;Tn(i,f,c<l.i?l.ins:0),a&&c<l.i&&Ul(a,i,l.text),c=l.i,l.forward(l.len-d)}else{if(l.done&&s.done)return a?Yt.createSet(i,a):Zi.create(i);throw new Error("Mismatched change set lengths")}}}function bI(e,t,n=!1){let r=[],i=n?[]:null,a=new gh(e),l=new gh(t);for(let s=!1;;){if(a.done&&l.done)return i?Yt.createSet(r,i):Zi.create(r);if(a.ins==0)Tn(r,a.len,0,s),a.next();else if(l.len==0&&!l.done)Tn(r,0,l.ins,s),i&&Ul(i,r,l.text),l.next();else{if(a.done||l.done)throw new Error("Mismatched change set lengths");{let c=Math.min(a.len2,l.len),f=r.length;if(a.ins==-1){let d=l.ins==-1?-1:l.off?0:l.ins;Tn(r,c,d,s),i&&d&&Ul(i,r,l.text)}else l.ins==-1?(Tn(r,a.off?0:a.len,c,s),i&&Ul(i,r,a.textBit(c))):(Tn(r,a.off?0:a.len,l.off?0:l.ins,s),i&&!l.off&&Ul(i,r,l.text));s=(a.ins>c||l.ins>=0&&l.len>c)&&(s||r.length>f),a.forward2(c),l.forward(c)}}}}class gh{constructor(t){this.set=t,this.i=0,this.next()}next(){let{sections:t}=this.set;this.i<t.length?(this.len=t[this.i++],this.ins=t[this.i++]):(this.len=0,this.ins=-2),this.off=0}get done(){return this.ins==-2}get len2(){return this.ins<0?this.len:this.ins}get text(){let{inserted:t}=this.set,n=this.i-2>>1;return n>=t.length?Je.empty:t[n]}textBit(t){let{inserted:n}=this.set,r=this.i-2>>1;return r>=n.length&&!t?Je.empty:n[r].slice(this.off,t==null?void 0:this.off+t)}forward(t){t==this.len?this.next():(this.len-=t,this.off+=t)}forward2(t){this.ins==-1?this.forward(t):t==this.ins?this.next():(this.ins-=t,this.off+=t)}}class Il{constructor(t,n,r,i){this.from=t,this.to=n,this.flags=r,this.goalColumn=i}get anchor(){return this.flags&32?this.to:this.from}get head(){return this.flags&32?this.from:this.to}get empty(){return this.from==this.to}get assoc(){return this.flags&8?-1:this.flags&16?1:0}get undirectional(){return(this.flags&64)>0}get bidiLevel(){let t=this.flags&7;return t==7?null:t}map(t,n=-1){let r,i;return this.empty?r=i=t.mapPos(this.from,n):(r=t.mapPos(this.from,1),i=t.mapPos(this.to,-1)),r==this.from&&i==this.to?this:new Il(r,i,this.flags,this.goalColumn)}extend(t,n=t,r=0){if(t<=this.anchor&&n>=this.anchor)return te.range(t,n,void 0,void 0,r);let i=Math.abs(t-this.anchor)>Math.abs(n-this.anchor)?t:n;return te.range(this.anchor,i,void 0,void 0,r)}eq(t,n=!1){return this.anchor==t.anchor&&this.head==t.head&&this.goalColumn==t.goalColumn&&(!n||!this.empty||this.assoc==t.assoc)}toJSON(){return{anchor:this.anchor,head:this.head}}static fromJSON(t){if(!t||typeof t.anchor!="number"||typeof t.head!="number")throw new RangeError("Invalid JSON representation for SelectionRange");return te.range(t.anchor,t.head)}static create(t,n,r,i){return new Il(t,n,r,i)}}class te{constructor(t,n){this.ranges=t,this.mainIndex=n}map(t,n=-1){return t.empty?this:te.create(this.ranges.map(r=>r.map(t,n)),this.mainIndex)}eq(t,n=!1){if(this.ranges.length!=t.ranges.length||this.mainIndex!=t.mainIndex)return!1;for(let r=0;r<this.ranges.length;r++)if(!this.ranges[r].eq(t.ranges[r],n))return!1;return!0}get main(){return this.ranges[this.mainIndex]}asSingle(){return this.ranges.length==1?this:new te([this.main],0)}addRange(t,n=!0){return te.create([t].concat(this.ranges),n?0:this.mainIndex+1)}replaceRange(t,n=this.mainIndex){let r=this.ranges.slice();return r[n]=t,te.create(r,this.mainIndex)}toJSON(){return{ranges:this.ranges.map(t=>t.toJSON()),main:this.mainIndex}}static fromJSON(t){if(!t||!Array.isArray(t.ranges)||typeof t.main!="number"||t.main>=t.ranges.length)throw new RangeError("Invalid JSON representation for EditorSelection");return new te(t.ranges.map(n=>Il.fromJSON(n)),t.main)}static single(t,n=t){return new te([te.range(t,n)],0)}static create(t,n=0){if(t.length==0)throw new RangeError("A selection needs at least one range");for(let r=0,i=0;i<t.length;i++){let a=t[i];if(a.empty?a.from<=r:a.from<r)return te.normalized(t.slice(),n);r=a.to}return new te(t,n)}static cursor(t,n=0,r,i){return Il.create(t,t,(n==0?0:n<0?8:16)|(r==null?7:Math.min(6,r)),i)}static range(t,n,r,i,a){let l=i==null?7:Math.min(6,i);return!a&&t!=n&&(a=n<t?1:-1),a&&(l|=a<0?8:16),n<t?Il.create(n,t,l|32,r):Il.create(t,n,l,r)}static undirectionalRange(t,n){return Il.create(t,n,64,void 0)}static normalized(t,n=0){let r=t[n];t.sort((i,a)=>i.from-a.from),n=t.indexOf(r);for(let i=1;i<t.length;i++){let a=t[i],l=t[i-1];if(a.empty?a.from<=l.to:a.from<l.to){let s=l.from,c=Math.max(a.to,l.to);i<=n&&n--,t.splice(--i,2,a.anchor>a.head?te.range(c,s):te.range(s,c))}}return new te(t,n)}}function xI(e,t){for(let n of e.ranges)if(n.to>t)throw new RangeError("Selection points outside of document")}let TO=0;class me{constructor(t,n,r,i,a){this.combine=t,this.compareInput=n,this.compare=r,this.isStatic=i,this.id=TO++,this.default=t([]),this.extensions=typeof a=="function"?a(this):a}get reader(){return this}static define(t={}){return new me(t.combine||(n=>n),t.compareInput||((n,r)=>n===r),t.compare||(t.combine?(n,r)=>n===r:MO),!!t.static,t.enables)}of(t){return new Bm([],this,0,t)}compute(t,n){if(this.isStatic)throw new Error("Can't compute a static facet");return new Bm(t,this,1,n)}computeN(t,n){if(this.isStatic)throw new Error("Can't compute a static facet");return new Bm(t,this,2,n)}from(t,n){return n||(n=r=>r),this.compute([t],r=>n(r.field(t)))}}function MO(e,t){return e==t||e.length==t.length&&e.every((n,r)=>n===t[r])}class Bm{constructor(t,n,r,i){this.dependencies=t,this.facet=n,this.type=r,this.value=i,this.id=TO++}dynamicSlot(t){var n;let r=this.value,i=this.facet.compareInput,a=this.id,l=t[a]>>1,s=this.type==2,c=!1,f=!1,d=[];for(let p of this.dependencies)p=="doc"?c=!0:p=="selection"?f=!0:(((n=t[p.id])!==null&&n!==void 0?n:1)&1)==0&&d.push(t[p.id]);return{create(p){return p.values[l]=r(p),1},update(p,g){if(c&&g.docChanged||f&&(g.docChanged||g.selection)||cS(p,d)){let v=r(p);if(s?!QM(v,p.values[l],i):!i(v,p.values[l]))return p.values[l]=v,1}return 0},reconfigure:(p,g)=>{let v,y=g.config.address[a];if(y!=null){let x=lg(g,y);if(this.dependencies.every(w=>w instanceof me?g.facet(w)===p.facet(w):w instanceof jn?g.field(w,!1)==p.field(w,!1):!0)||(s?QM(v=r(p),x,i):i(v=r(p),x)))return p.values[l]=x,0}else v=r(p);return p.values[l]=v,1}}}get extension(){return this}}function QM(e,t,n){if(e.length!=t.length)return!1;for(let r=0;r<e.length;r++)if(!n(e[r],t[r]))return!1;return!0}function cS(e,t){let n=!1;for(let r of t)Zf(e,r)&1&&(n=!0);return n}function L7(e,t,n){let r=n.map(c=>e[c.id]),i=n.map(c=>c.type),a=r.filter(c=>!(c&1)),l=e[t.id]>>1;function s(c){let f=[];for(let d=0;d<r.length;d++){let p=lg(c,r[d]);if(i[d]==2)for(let g of p)f.push(g);else f.push(p)}return t.combine(f)}return{create(c){for(let f of r)Zf(c,f);return c.values[l]=s(c),1},update(c,f){if(!cS(c,a))return 0;let d=s(c);return t.compare(d,c.values[l])?0:(c.values[l]=d,1)},reconfigure(c,f){let d=cS(c,r),p=f.config.facets[t.id],g=f.facet(t);if(p&&!d&&MO(n,p))return c.values[l]=g,0;let v=s(c);return t.compare(v,g)?(c.values[l]=g,0):(c.values[l]=v,1)}}}const Vp=me.define({static:!0});class jn{constructor(t,n,r,i,a){this.id=t,this.createF=n,this.updateF=r,this.compareF=i,this.spec=a,this.provides=void 0}static define(t){let n=new jn(TO++,t.create,t.update,t.compare||((r,i)=>r===i),t);return t.provide&&(n.provides=t.provide(n)),n}create(t){let n=t.facet(Vp).find(r=>r.field==this);return(n?.create||this.createF)(t)}slot(t){let n=t[this.id]>>1;return{create:r=>(r.values[n]=this.create(r),1),update:(r,i)=>{let a=r.values[n],l=this.updateF(a,i);return this.compareF(a,l)?0:(r.values[n]=l,1)},reconfigure:(r,i)=>{let a=r.facet(Vp),l=i.facet(Vp),s;return(s=a.find(c=>c.field==this))&&s!=l.find(c=>c.field==this)?(r.values[n]=s.create(r),1):i.config.address[this.id]!=null?(r.values[n]=i.field(this),0):(r.values[n]=this.create(r),1)}}}init(t){return[this,Vp.of({field:this,create:t})]}get extension(){return this}}const Lo={lowest:4,low:3,default:2,high:1,highest:0};function kf(e){return t=>new SI(t,e)}const ys={highest:kf(Lo.highest),high:kf(Lo.high),default:kf(Lo.default),low:kf(Lo.low),lowest:kf(Lo.lowest)};class SI{constructor(t,n){this.inner=t,this.prec=n}get extension(){return this}}class Lv{of(t){return new fS(this,t)}reconfigure(t){return Lv.reconfigure.of({compartment:this,extension:t})}get(t){return t.config.compartments.get(this)}}class fS{constructor(t,n){this.compartment=t,this.inner=n}get extension(){return this}}class ag{constructor(t,n,r,i,a,l){for(this.base=t,this.compartments=n,this.dynamicSlots=r,this.address=i,this.staticValues=a,this.facets=l,this.statusTemplate=[];this.statusTemplate.length<r.length;)this.statusTemplate.push(0)}staticFacet(t){let n=this.address[t.id];return n==null?t.default:this.staticValues[n>>1]}static resolve(t,n,r){let i=[],a=Object.create(null),l=new Map;for(let g of I7(t,n,l))g instanceof jn?i.push(g):(a[g.facet.id]||(a[g.facet.id]=[])).push(g);let s=Object.create(null),c=[],f=[];for(let g of i)s[g.id]=f.length<<1,f.push(v=>g.slot(v));let d=r?.config.facets;for(let g in a){let v=a[g],y=v[0].facet,x=d&&d[g]||[];if(v.every(w=>w.type==0))if(s[y.id]=c.length<<1|1,MO(x,v))c.push(r.facet(y));else{let w=y.combine(v.map(A=>A.value));c.push(r&&y.compare(w,r.facet(y))?r.facet(y):w)}else{for(let w of v)w.type==0?(s[w.id]=c.length<<1|1,c.push(w.value)):(s[w.id]=f.length<<1,f.push(A=>w.dynamicSlot(A)));s[y.id]=f.length<<1,f.push(w=>L7(w,y,v))}}let p=f.map(g=>g(s));return new ag(t,l,p,s,c,a)}}function I7(e,t,n){let r=[[],[],[],[],[]],i=new Map;function a(l,s){let c=i.get(l);if(c!=null){if(c<=s)return;let f=r[c].indexOf(l);f>-1&&r[c].splice(f,1),l instanceof fS&&n.delete(l.compartment)}if(i.set(l,s),Array.isArray(l))for(let f of l)a(f,s);else if(l instanceof fS){if(n.has(l.compartment))throw new RangeError("Duplicate use of compartment in extensions");let f=t.get(l.compartment)||l.inner;n.set(l.compartment,f),a(f,s)}else if(l instanceof SI)a(l.inner,l.prec);else if(l instanceof jn)r[s].push(l),l.provides&&a(l.provides,s);else if(l instanceof Bm)r[s].push(l),l.facet.extensions&&a(l.facet.extensions,Lo.default);else{let f=l.extension;if(!f)throw new Error(`Unrecognized extension value in extension set (${l}).`);if(f==l)throw new Error(`Unrecognized extension value in extension set (${l}). This sometimes happens because multiple instances of @codemirror/state are loaded, breaking instanceof checks.`);a(f,s)}}return a(e,Lo.default),r.reduce((l,s)=>l.concat(s))}function Zf(e,t){if(t&1)return 2;let n=t>>1,r=e.status[n];if(r==4)throw new Error("Cyclic dependency between fields and/or facets");if(r&2)return r;e.status[n]=4;let i=e.computeSlot(e,e.config.dynamicSlots[n]);return e.status[n]=2|i}function lg(e,t){return t&1?e.config.staticValues[t>>1]:e.values[t>>1]}const wI=me.define(),hS=me.define({combine:e=>e.some(t=>t),static:!0}),OI=me.define({combine:e=>e.length?e[0]:void 0,static:!0}),AI=me.define(),kI=me.define(),CI=me.define(),EI=me.define({combine:e=>e.length?e[0]:!1});class oa{constructor(t,n){this.type=t,this.value=n}static define(){return new B7}}class B7{of(t){return new oa(this,t)}}class z7{constructor(t){this.map=t}of(t){return new _e(this,t)}}class _e{constructor(t,n){this.type=t,this.value=n}map(t){let n=this.type.map(this.value,t);return n===void 0?void 0:n==this.value?this:new _e(this.type,n)}is(t){return this.type==t}static define(t={}){return new z7(t.map||(n=>n))}static mapEffects(t,n){if(!t.length)return t;let r=[];for(let i of t){let a=i.map(n);a&&r.push(a)}return r}}_e.reconfigure=_e.define();_e.appendConfig=_e.define();class Zt{constructor(t,n,r,i,a,l){this.startState=t,this.changes=n,this.selection=r,this.effects=i,this.annotations=a,this.scrollIntoView=l,this._doc=null,this._state=null,r&&xI(r,n.newLength),a.some(s=>s.type==Zt.time)||(this.annotations=a.concat(Zt.time.of(Date.now())))}static create(t,n,r,i,a,l){return new Zt(t,n,r,i,a,l)}get newDoc(){return this._doc||(this._doc=this.changes.apply(this.startState.doc))}get newSelection(){return this.selection||this.startState.selection.map(this.changes)}get state(){return this._state||this.startState.applyTransaction(this),this._state}annotation(t){for(let n of this.annotations)if(n.type==t)return n.value}get docChanged(){return!this.changes.empty}get reconfigured(){return this.startState.config!=this.state.config}isUserEvent(t){let n=this.annotation(Zt.userEvent);return!!(n&&(n==t||n.length>t.length&&n.slice(0,t.length)==t&&n[t.length]=="."))}}Zt.time=oa.define();Zt.userEvent=oa.define();Zt.addToHistory=oa.define();Zt.remote=oa.define();function $7(e,t){let n=[];for(let r=0,i=0;;){let a,l;if(r<e.length&&(i==t.length||t[i]>=e[r]))a=e[r++],l=e[r++];else if(i<t.length)a=t[i++],l=t[i++];else return n;!n.length||n[n.length-1]<a?n.push(a,l):n[n.length-1]<l&&(n[n.length-1]=l)}}function TI(e,t,n){var r;let i,a,l;return n?(i=t.changes,a=Yt.empty(t.changes.length),l=e.changes.compose(t.changes)):(i=t.changes.map(e.changes),a=e.changes.mapDesc(t.changes,!0),l=e.changes.compose(i)),{changes:l,selection:t.selection?t.selection.map(a):(r=e.selection)===null||r===void 0?void 0:r.map(i),effects:_e.mapEffects(e.effects,i).concat(_e.mapEffects(t.effects,a)),annotations:e.annotations.length?e.annotations.concat(t.annotations):t.annotations,scrollIntoView:e.scrollIntoView||t.scrollIntoView}}function dS(e,t,n){let r=t.selection,i=Nu(t.annotations);return t.userEvent&&(i=i.concat(Zt.userEvent.of(t.userEvent))),{changes:t.changes instanceof Yt?t.changes:Yt.of(t.changes||[],n,e.facet(OI)),selection:r&&(r instanceof te?r:te.single(r.anchor,r.head)),effects:Nu(t.effects),annotations:i,scrollIntoView:!!t.scrollIntoView}}function MI(e,t,n){let r=dS(e,t.length?t[0]:{},e.doc.length);t.length&&t[0].filter===!1&&(n=!1);for(let a=1;a<t.length;a++){t[a].filter===!1&&(n=!1);let l=!!t[a].sequential;r=TI(r,dS(e,t[a],l?r.changes.newLength:e.doc.length),l)}let i=Zt.create(e,r.changes,r.selection,r.effects,r.annotations,r.scrollIntoView);return H7(n?U7(i):i)}function U7(e){let t=e.startState,n=!0;for(let i of t.facet(AI)){let a=i(e);if(a===!1){n=!1;break}Array.isArray(a)&&(n=n===!0?a:$7(n,a))}if(n!==!0){let i,a;if(n===!1)a=e.changes.invertedDesc,i=Yt.empty(t.doc.length);else{let l=e.changes.filter(n);i=l.changes,a=l.filtered.mapDesc(l.changes).invertedDesc}e=Zt.create(t,i,e.selection&&e.selection.map(a),_e.mapEffects(e.effects,a),e.annotations,e.scrollIntoView)}let r=t.facet(kI);for(let i=r.length-1;i>=0;i--){let a=r[i](e);a instanceof Zt?e=a:Array.isArray(a)&&a.length==1&&a[0]instanceof Zt?e=a[0]:e=MI(t,Nu(a),!1)}return e}function H7(e){let t=e.startState,n=t.facet(CI),r=e;for(let i=n.length-1;i>=0;i--){let a=n[i](e);a&&Object.keys(a).length&&(r=TI(r,dS(t,a,e.changes.newLength),!0))}return r==e?e:Zt.create(t,e.changes,e.selection,r.effects,r.annotations,r.scrollIntoView)}const F7=[];function Nu(e){return e==null?F7:Array.isArray(e)?e:[e]}var Ot=(function(e){return e[e.Word=0]="Word",e[e.Space=1]="Space",e[e.Other=2]="Other",e})(Ot||(Ot={}));const q7=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/;let pS;try{pS=new RegExp("[\\p{Alphabetic}\\p{Number}_]","u")}catch{}function W7(e){if(pS)return pS.test(e);for(let t=0;t<e.length;t++){let n=e[t];if(/\w/.test(n)||n>"€"&&(n.toUpperCase()!=n.toLowerCase()||q7.test(n)))return!0}return!1}function V7(e){return t=>{if(!/\S/.test(t))return Ot.Space;if(W7(t))return Ot.Word;for(let n=0;n<e.length;n++)if(t.indexOf(e[n])>-1)return Ot.Word;return Ot.Other}}class Fe{constructor(t,n,r,i,a,l){this.config=t,this.doc=n,this.selection=r,this.values=i,this.status=t.statusTemplate.slice(),this.computeSlot=a,l&&(l._state=this);for(let s=0;s<this.config.dynamicSlots.length;s++)Zf(this,s<<1);this.computeSlot=null}field(t,n=!0){let r=this.config.address[t.id];if(r==null){if(n)throw new RangeError("Field is not present in this state");return}return Zf(this,r),lg(this,r)}update(...t){return MI(this,t,!0)}applyTransaction(t){let n=this.config,{base:r,compartments:i}=n;for(let s of t.effects)s.is(Lv.reconfigure)?(n&&(i=new Map,n.compartments.forEach((c,f)=>i.set(f,c)),n=null),i.set(s.value.compartment,s.value.extension)):s.is(_e.reconfigure)?(n=null,r=s.value):s.is(_e.appendConfig)&&(n=null,r=Nu(r).concat(s.value));let a;n?a=t.startState.values.slice():(n=ag.resolve(r,i,this),a=new Fe(n,this.doc,this.selection,n.dynamicSlots.map(()=>null),(c,f)=>f.reconfigure(c,this),null).values);let l=t.startState.facet(hS)?t.newSelection:t.newSelection.asSingle();new Fe(n,t.newDoc,l,a,(s,c)=>c.update(s,t),t)}replaceSelection(t){return typeof t=="string"&&(t=this.toText(t)),this.changeByRange(n=>({changes:{from:n.from,to:n.to,insert:t},range:te.cursor(n.from+t.length)}))}changeByRange(t){let n=this.selection,r=t(n.ranges[0]),i=this.changes(r.changes),a=[r.range],l=Nu(r.effects);for(let s=1;s<n.ranges.length;s++){let c=t(n.ranges[s]),f=this.changes(c.changes),d=f.map(i);for(let g=0;g<s;g++)a[g]=a[g].map(d);let p=i.mapDesc(f,!0);a.push(c.range.map(p)),i=i.compose(d),l=_e.mapEffects(l,d).concat(_e.mapEffects(Nu(c.effects),p))}return{changes:i,selection:te.create(a,n.mainIndex),effects:l}}changes(t=[]){return t instanceof Yt?t:Yt.of(t,this.doc.length,this.facet(Fe.lineSeparator))}toText(t){return Je.of(t.split(this.facet(Fe.lineSeparator)||oS))}sliceDoc(t=0,n=this.doc.length){return this.doc.sliceString(t,n,this.lineBreak)}facet(t){let n=this.config.address[t.id];return n==null?t.default:(Zf(this,n),lg(this,n))}toJSON(t){let n={doc:this.sliceDoc(),selection:this.selection.toJSON()};if(t)for(let r in t){let i=t[r];i instanceof jn&&this.config.address[i.id]!=null&&(n[r]=i.spec.toJSON(this.field(t[r]),this))}return n}static fromJSON(t,n={},r){if(!t||typeof t.doc!="string")throw new RangeError("Invalid JSON representation for EditorState");let i=[];if(r){for(let a in r)if(Object.prototype.hasOwnProperty.call(t,a)){let l=r[a],s=t[a];i.push(l.init(c=>l.spec.fromJSON(s,c)))}}return Fe.create({doc:t.doc,selection:te.fromJSON(t.selection),extensions:n.extensions?i.concat([n.extensions]):i})}static create(t={}){let n=ag.resolve(t.extensions||[],new Map),r=t.doc instanceof Je?t.doc:Je.of((t.doc||"").split(n.staticFacet(Fe.lineSeparator)||oS)),i=t.selection?t.selection instanceof te?t.selection:te.single(t.selection.anchor,t.selection.head):te.single(0);return xI(i,r.length),n.staticFacet(hS)||(i=i.asSingle()),new Fe(n,r,i,n.dynamicSlots.map(()=>null),(a,l)=>l.create(a),null)}get tabSize(){return this.facet(Fe.tabSize)}get lineBreak(){return this.facet(Fe.lineSeparator)||`
@@ -5,7 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>doc2vec</title>
7
7
  <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ctext y='0.9em' font-size='90'%3E%F0%9F%93%9A%3C/text%3E%3C/svg%3E" />
8
- <script type="module" crossorigin src="/assets/index-C0Yy-BZB.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-HvEXxtAr.js"></script>
9
9
  <link rel="stylesheet" crossorigin href="/assets/index-Cf1xdzH_.css">
10
10
  </head>
11
11
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doc2vec",
3
- "version": "2.15.1",
3
+ "version": "2.17.0",
4
4
  "type": "commonjs",
5
5
  "engines": {
6
6
  "node": ">=22"