@soulcraft/brainy 4.2.2 β†’ 4.2.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ### [4.2.3](https://github.com/soulcraftlabs/brainy/compare/v4.2.2...v4.2.3) (2025-10-23)
6
+
7
+
8
+ ### πŸ› Bug Fixes
9
+
10
+ * **metadata-index**: fix rebuild stalling after first batch on FileSystemStorage
11
+ - **Critical Fix**: v4.2.2 rebuild stalled after processing first batch (500/1,157 entities)
12
+ - **Root Cause**: `getAllShardedFiles()` was called on EVERY batch, re-reading all 256 shard directories each time
13
+ - **Performance Impact**: Second batch call to `getAllShardedFiles()` took 3+ minutes, appearing to hang
14
+ - **Solution**: Load all entities at once for local storage (FileSystem/Memory/OPFS)
15
+ - FileSystem/Memory/OPFS: Load all nouns/verbs in single batch (no pagination overhead)
16
+ - Cloud (GCS/S3/R2): Keep conservative pagination (25 items/batch for socket safety)
17
+ - **Benefits**:
18
+ - FileSystem: 1,157 entities load in **2-3 seconds** (one `getAllShardedFiles()` call)
19
+ - Cloud: Unchanged behavior (still uses safe batching)
20
+ - Zero config: Auto-detects storage type via `constructor.name`
21
+ - **Technical Details**:
22
+ - Pagination was designed for cloud storage socket exhaustion
23
+ - FileSystem doesn't need pagination - can handle loading thousands of entities at once
24
+ - Eliminates repeated directory scans: 3 batches Γ— 256 dirs β†’ 1 batch Γ— 256 dirs
25
+ - **Workshop Team**: This resolves the v4.2.2 stalling issue - rebuild will now complete in seconds
26
+ - **Files Changed**: `src/utils/metadataIndex.ts` (rebuilt() method with adaptive loading strategy)
27
+
5
28
  ### [4.2.2](https://github.com/soulcraftlabs/brainy/compare/v4.2.1...v4.2.2) (2025-10-23)
6
29
 
7
30
 
@@ -1738,188 +1738,272 @@ export class MetadataIndexManager {
1738
1738
  // Clear all cached sparse indices in UnifiedCache
1739
1739
  // This ensures rebuild starts fresh (v3.44.1)
1740
1740
  this.unifiedCache.clear('metadata');
1741
- // Adaptive batch sizing based on storage adapter (v4.2.2)
1742
- // FileSystem/Memory/OPFS: Large batches (fast local I/O, no socket limits)
1743
- // Cloud (GCS/S3/R2): Small batches (prevent socket exhaustion)
1741
+ // Adaptive rebuild strategy based on storage adapter (v4.2.3)
1742
+ // FileSystem/Memory/OPFS: Load all at once (avoids getAllShardedFiles() overhead on every batch)
1743
+ // Cloud (GCS/S3/R2): Use pagination with small batches (prevent socket exhaustion)
1744
1744
  const storageType = this.storage.constructor.name;
1745
1745
  const isLocalStorage = storageType === 'FileSystemStorage' ||
1746
1746
  storageType === 'MemoryStorage' ||
1747
1747
  storageType === 'OPFSStorage';
1748
- const nounLimit = isLocalStorage ? 500 : 25;
1749
- prodLog.info(`⚑ Using ${isLocalStorage ? 'optimized' : 'conservative'} batch size: ${nounLimit} items/batch`);
1750
- // Rebuild noun metadata indexes using pagination
1751
- let nounOffset = 0;
1752
- let hasMoreNouns = true;
1748
+ let nounLimit;
1753
1749
  let totalNounsProcessed = 0;
1754
- let consecutiveEmptyBatches = 0;
1755
- const MAX_ITERATIONS = 10000; // Safety limit to prevent infinite loops
1756
- let iterations = 0;
1757
- while (hasMoreNouns && iterations < MAX_ITERATIONS) {
1758
- iterations++;
1750
+ if (isLocalStorage) {
1751
+ // Load all nouns at once for local storage
1752
+ // Avoids repeated directory scans in getAllShardedFiles()
1753
+ prodLog.info(`⚑ Using optimized strategy: load all nouns at once (local storage)`);
1759
1754
  const result = await this.storage.getNouns({
1760
- pagination: { offset: nounOffset, limit: nounLimit }
1755
+ pagination: { offset: 0, limit: 1000000 } // Effectively unlimited
1761
1756
  });
1762
- // CRITICAL SAFETY CHECK: Prevent infinite loop on empty results
1763
- if (result.items.length === 0) {
1764
- consecutiveEmptyBatches++;
1765
- if (consecutiveEmptyBatches >= 3) {
1766
- prodLog.warn('⚠️ Breaking metadata rebuild loop: received 3 consecutive empty batches');
1767
- break;
1768
- }
1769
- // If hasMore is true but items are empty, it's likely a bug
1770
- if (result.hasMore) {
1771
- prodLog.warn(`⚠️ Storage returned empty items but hasMore=true at offset ${nounOffset}`);
1772
- hasMoreNouns = false; // Force exit
1773
- break;
1774
- }
1775
- }
1776
- else {
1777
- consecutiveEmptyBatches = 0; // Reset counter on non-empty batch
1778
- }
1779
- // CRITICAL FIX: Use batch metadata reading to prevent socket exhaustion
1757
+ prodLog.info(`πŸ“¦ Loading ${result.items.length} nouns with metadata...`);
1758
+ // Get all metadata in one batch if available
1780
1759
  const nounIds = result.items.map(noun => noun.id);
1781
1760
  let metadataBatch;
1782
1761
  if (this.storage.getMetadataBatch) {
1783
- // Use batch reading if available (prevents socket exhaustion)
1784
- prodLog.info(`πŸ“¦ Processing metadata batch ${Math.floor(totalNounsProcessed / nounLimit) + 1} (${nounIds.length} items)...`);
1785
1762
  metadataBatch = await this.storage.getMetadataBatch(nounIds);
1786
- const successRate = ((metadataBatch.size / nounIds.length) * 100).toFixed(1);
1787
- prodLog.info(`βœ… Batch loaded ${metadataBatch.size}/${nounIds.length} metadata objects (${successRate}% success)`);
1763
+ prodLog.info(`βœ… Loaded ${metadataBatch.size}/${nounIds.length} metadata objects`);
1788
1764
  }
1789
1765
  else {
1790
- // Fallback to individual calls with strict concurrency control
1791
- prodLog.warn(`⚠️ FALLBACK: Storage adapter missing getMetadataBatch - using individual calls with concurrency limit`);
1766
+ // Fallback to individual calls
1792
1767
  metadataBatch = new Map();
1793
- const CONCURRENCY_LIMIT = 3; // Very conservative limit
1794
- for (let i = 0; i < nounIds.length; i += CONCURRENCY_LIMIT) {
1795
- const batch = nounIds.slice(i, i + CONCURRENCY_LIMIT);
1796
- const batchPromises = batch.map(async (id) => {
1797
- try {
1798
- const metadata = await this.storage.getNounMetadata(id);
1799
- return { id, metadata };
1800
- }
1801
- catch (error) {
1802
- prodLog.debug(`Failed to read metadata for ${id}:`, error);
1803
- return { id, metadata: null };
1804
- }
1805
- });
1806
- const batchResults = await Promise.all(batchPromises);
1807
- for (const { id, metadata } of batchResults) {
1808
- if (metadata) {
1768
+ for (const id of nounIds) {
1769
+ try {
1770
+ const metadata = await this.storage.getNounMetadata(id);
1771
+ if (metadata)
1809
1772
  metadataBatch.set(id, metadata);
1810
- }
1811
1773
  }
1812
- // Yield between batches to prevent socket exhaustion
1813
- await this.yieldToEventLoop();
1774
+ catch (error) {
1775
+ prodLog.debug(`Failed to read metadata for ${id}:`, error);
1776
+ }
1814
1777
  }
1815
1778
  }
1816
- // Process the metadata batch
1779
+ // Process all nouns
1817
1780
  for (const noun of result.items) {
1818
1781
  const metadata = metadataBatch.get(noun.id);
1819
1782
  if (metadata) {
1820
- // Skip flush during rebuild for performance
1821
1783
  await this.addToIndex(noun.id, metadata, true);
1822
1784
  }
1823
1785
  }
1824
- // Yield after processing the entire batch
1825
- await this.yieldToEventLoop();
1826
- totalNounsProcessed += result.items.length;
1827
- hasMoreNouns = result.hasMore;
1828
- nounOffset += nounLimit;
1829
- // Progress logging and event loop yield after each batch
1830
- if (totalNounsProcessed % 100 === 0 || !hasMoreNouns) {
1831
- prodLog.debug(`πŸ“Š Indexed ${totalNounsProcessed} nouns...`);
1832
- }
1833
- await this.yieldToEventLoop();
1786
+ totalNounsProcessed = result.items.length;
1787
+ prodLog.info(`βœ… Indexed ${totalNounsProcessed} nouns`);
1834
1788
  }
1835
- // Rebuild verb metadata indexes using pagination
1836
- let verbOffset = 0;
1837
- const verbLimit = isLocalStorage ? 500 : 25; // Same adaptive batch sizing as nouns
1838
- let hasMoreVerbs = true;
1839
- let totalVerbsProcessed = 0;
1840
- let consecutiveEmptyVerbBatches = 0;
1841
- let verbIterations = 0;
1842
- while (hasMoreVerbs && verbIterations < MAX_ITERATIONS) {
1843
- verbIterations++;
1844
- const result = await this.storage.getVerbs({
1845
- pagination: { offset: verbOffset, limit: verbLimit }
1846
- });
1847
- // CRITICAL SAFETY CHECK: Prevent infinite loop on empty results
1848
- if (result.items.length === 0) {
1849
- consecutiveEmptyVerbBatches++;
1850
- if (consecutiveEmptyVerbBatches >= 3) {
1851
- prodLog.warn('⚠️ Breaking verb metadata rebuild loop: received 3 consecutive empty batches');
1852
- break;
1789
+ else {
1790
+ // Cloud storage: use conservative batching
1791
+ nounLimit = 25;
1792
+ prodLog.info(`⚑ Using conservative batch size: ${nounLimit} items/batch (cloud storage)`);
1793
+ let nounOffset = 0;
1794
+ let hasMoreNouns = true;
1795
+ let consecutiveEmptyBatches = 0;
1796
+ const MAX_ITERATIONS = 10000;
1797
+ let iterations = 0;
1798
+ while (hasMoreNouns && iterations < MAX_ITERATIONS) {
1799
+ iterations++;
1800
+ const result = await this.storage.getNouns({
1801
+ pagination: { offset: nounOffset, limit: nounLimit }
1802
+ });
1803
+ // CRITICAL SAFETY CHECK: Prevent infinite loop on empty results
1804
+ if (result.items.length === 0) {
1805
+ consecutiveEmptyBatches++;
1806
+ if (consecutiveEmptyBatches >= 3) {
1807
+ prodLog.warn('⚠️ Breaking metadata rebuild loop: received 3 consecutive empty batches');
1808
+ break;
1809
+ }
1810
+ // If hasMore is true but items are empty, it's likely a bug
1811
+ if (result.hasMore) {
1812
+ prodLog.warn(`⚠️ Storage returned empty items but hasMore=true at offset ${nounOffset}`);
1813
+ hasMoreNouns = false; // Force exit
1814
+ break;
1815
+ }
1816
+ }
1817
+ else {
1818
+ consecutiveEmptyBatches = 0; // Reset counter on non-empty batch
1819
+ }
1820
+ // CRITICAL FIX: Use batch metadata reading to prevent socket exhaustion
1821
+ const nounIds = result.items.map(noun => noun.id);
1822
+ let metadataBatch;
1823
+ if (this.storage.getMetadataBatch) {
1824
+ // Use batch reading if available (prevents socket exhaustion)
1825
+ prodLog.info(`πŸ“¦ Processing metadata batch ${Math.floor(totalNounsProcessed / nounLimit) + 1} (${nounIds.length} items)...`);
1826
+ metadataBatch = await this.storage.getMetadataBatch(nounIds);
1827
+ const successRate = ((metadataBatch.size / nounIds.length) * 100).toFixed(1);
1828
+ prodLog.info(`βœ… Batch loaded ${metadataBatch.size}/${nounIds.length} metadata objects (${successRate}% success)`);
1829
+ }
1830
+ else {
1831
+ // Fallback to individual calls with strict concurrency control
1832
+ prodLog.warn(`⚠️ FALLBACK: Storage adapter missing getMetadataBatch - using individual calls with concurrency limit`);
1833
+ metadataBatch = new Map();
1834
+ const CONCURRENCY_LIMIT = 3; // Very conservative limit
1835
+ for (let i = 0; i < nounIds.length; i += CONCURRENCY_LIMIT) {
1836
+ const batch = nounIds.slice(i, i + CONCURRENCY_LIMIT);
1837
+ const batchPromises = batch.map(async (id) => {
1838
+ try {
1839
+ const metadata = await this.storage.getNounMetadata(id);
1840
+ return { id, metadata };
1841
+ }
1842
+ catch (error) {
1843
+ prodLog.debug(`Failed to read metadata for ${id}:`, error);
1844
+ return { id, metadata: null };
1845
+ }
1846
+ });
1847
+ const batchResults = await Promise.all(batchPromises);
1848
+ for (const { id, metadata } of batchResults) {
1849
+ if (metadata) {
1850
+ metadataBatch.set(id, metadata);
1851
+ }
1852
+ }
1853
+ // Yield between batches to prevent socket exhaustion
1854
+ await this.yieldToEventLoop();
1855
+ }
1856
+ }
1857
+ // Process the metadata batch
1858
+ for (const noun of result.items) {
1859
+ const metadata = metadataBatch.get(noun.id);
1860
+ if (metadata) {
1861
+ // Skip flush during rebuild for performance
1862
+ await this.addToIndex(noun.id, metadata, true);
1863
+ }
1853
1864
  }
1854
- // If hasMore is true but items are empty, it's likely a bug
1855
- if (result.hasMore) {
1856
- prodLog.warn(`⚠️ Storage returned empty verb items but hasMore=true at offset ${verbOffset}`);
1857
- hasMoreVerbs = false; // Force exit
1858
- break;
1865
+ // Yield after processing the entire batch
1866
+ await this.yieldToEventLoop();
1867
+ totalNounsProcessed += result.items.length;
1868
+ hasMoreNouns = result.hasMore;
1869
+ nounOffset += nounLimit;
1870
+ // Progress logging and event loop yield after each batch
1871
+ if (totalNounsProcessed % 100 === 0 || !hasMoreNouns) {
1872
+ prodLog.debug(`πŸ“Š Indexed ${totalNounsProcessed} nouns...`);
1859
1873
  }
1874
+ await this.yieldToEventLoop();
1860
1875
  }
1861
- else {
1862
- consecutiveEmptyVerbBatches = 0; // Reset counter on non-empty batch
1876
+ // Check iteration limits for cloud storage
1877
+ if (iterations >= MAX_ITERATIONS) {
1878
+ prodLog.error(`❌ Metadata noun rebuild hit maximum iteration limit (${MAX_ITERATIONS}). This indicates a bug in storage pagination.`);
1863
1879
  }
1864
- // CRITICAL FIX: Use batch verb metadata reading to prevent socket exhaustion
1880
+ }
1881
+ // Rebuild verb metadata indexes - same strategy as nouns
1882
+ let totalVerbsProcessed = 0;
1883
+ if (isLocalStorage) {
1884
+ // Load all verbs at once for local storage
1885
+ prodLog.info(`⚑ Loading all verbs at once (local storage)`);
1886
+ const result = await this.storage.getVerbs({
1887
+ pagination: { offset: 0, limit: 1000000 } // Effectively unlimited
1888
+ });
1889
+ prodLog.info(`πŸ“¦ Loading ${result.items.length} verbs with metadata...`);
1890
+ // Get all verb metadata at once
1865
1891
  const verbIds = result.items.map(verb => verb.id);
1866
1892
  let verbMetadataBatch;
1867
1893
  if (this.storage.getVerbMetadataBatch) {
1868
- // Use batch reading if available (prevents socket exhaustion)
1869
1894
  verbMetadataBatch = await this.storage.getVerbMetadataBatch(verbIds);
1870
- prodLog.debug(`πŸ“¦ Batch loaded ${verbMetadataBatch.size}/${verbIds.length} verb metadata objects`);
1895
+ prodLog.info(`βœ… Loaded ${verbMetadataBatch.size}/${verbIds.length} verb metadata objects`);
1871
1896
  }
1872
1897
  else {
1873
- // Fallback to individual calls with strict concurrency control
1874
1898
  verbMetadataBatch = new Map();
1875
- const CONCURRENCY_LIMIT = 3; // Very conservative limit to prevent socket exhaustion
1876
- for (let i = 0; i < verbIds.length; i += CONCURRENCY_LIMIT) {
1877
- const batch = verbIds.slice(i, i + CONCURRENCY_LIMIT);
1878
- const batchPromises = batch.map(async (id) => {
1879
- try {
1880
- const metadata = await this.storage.getVerbMetadata(id);
1881
- return { id, metadata };
1882
- }
1883
- catch (error) {
1884
- prodLog.debug(`Failed to read verb metadata for ${id}:`, error);
1885
- return { id, metadata: null };
1886
- }
1887
- });
1888
- const batchResults = await Promise.all(batchPromises);
1889
- for (const { id, metadata } of batchResults) {
1890
- if (metadata) {
1899
+ for (const id of verbIds) {
1900
+ try {
1901
+ const metadata = await this.storage.getVerbMetadata(id);
1902
+ if (metadata)
1891
1903
  verbMetadataBatch.set(id, metadata);
1892
- }
1893
1904
  }
1894
- // Yield between batches to prevent socket exhaustion
1895
- await this.yieldToEventLoop();
1905
+ catch (error) {
1906
+ prodLog.debug(`Failed to read verb metadata for ${id}:`, error);
1907
+ }
1896
1908
  }
1897
1909
  }
1898
- // Process the verb metadata batch
1910
+ // Process all verbs
1899
1911
  for (const verb of result.items) {
1900
1912
  const metadata = verbMetadataBatch.get(verb.id);
1901
1913
  if (metadata) {
1902
- // Skip flush during rebuild for performance
1903
1914
  await this.addToIndex(verb.id, metadata, true);
1904
1915
  }
1905
1916
  }
1906
- // Yield after processing the entire batch
1907
- await this.yieldToEventLoop();
1908
- totalVerbsProcessed += result.items.length;
1909
- hasMoreVerbs = result.hasMore;
1910
- verbOffset += verbLimit;
1911
- // Progress logging and event loop yield after each batch
1912
- if (totalVerbsProcessed % 100 === 0 || !hasMoreVerbs) {
1913
- prodLog.debug(`πŸ”— Indexed ${totalVerbsProcessed} verbs...`);
1914
- }
1915
- await this.yieldToEventLoop();
1916
- }
1917
- // Check if we hit iteration limits
1918
- if (iterations >= MAX_ITERATIONS) {
1919
- prodLog.error(`❌ Metadata noun rebuild hit maximum iteration limit (${MAX_ITERATIONS}). This indicates a bug in storage pagination.`);
1917
+ totalVerbsProcessed = result.items.length;
1918
+ prodLog.info(`βœ… Indexed ${totalVerbsProcessed} verbs`);
1920
1919
  }
1921
- if (verbIterations >= MAX_ITERATIONS) {
1922
- prodLog.error(`❌ Metadata verb rebuild hit maximum iteration limit (${MAX_ITERATIONS}). This indicates a bug in storage pagination.`);
1920
+ else {
1921
+ // Cloud storage: use conservative batching
1922
+ let verbOffset = 0;
1923
+ const verbLimit = 25;
1924
+ let hasMoreVerbs = true;
1925
+ let consecutiveEmptyVerbBatches = 0;
1926
+ let verbIterations = 0;
1927
+ const MAX_ITERATIONS = 10000;
1928
+ while (hasMoreVerbs && verbIterations < MAX_ITERATIONS) {
1929
+ verbIterations++;
1930
+ const result = await this.storage.getVerbs({
1931
+ pagination: { offset: verbOffset, limit: verbLimit }
1932
+ });
1933
+ // CRITICAL SAFETY CHECK: Prevent infinite loop on empty results
1934
+ if (result.items.length === 0) {
1935
+ consecutiveEmptyVerbBatches++;
1936
+ if (consecutiveEmptyVerbBatches >= 3) {
1937
+ prodLog.warn('⚠️ Breaking verb metadata rebuild loop: received 3 consecutive empty batches');
1938
+ break;
1939
+ }
1940
+ // If hasMore is true but items are empty, it's likely a bug
1941
+ if (result.hasMore) {
1942
+ prodLog.warn(`⚠️ Storage returned empty verb items but hasMore=true at offset ${verbOffset}`);
1943
+ hasMoreVerbs = false; // Force exit
1944
+ break;
1945
+ }
1946
+ }
1947
+ else {
1948
+ consecutiveEmptyVerbBatches = 0; // Reset counter on non-empty batch
1949
+ }
1950
+ // CRITICAL FIX: Use batch verb metadata reading to prevent socket exhaustion
1951
+ const verbIds = result.items.map(verb => verb.id);
1952
+ let verbMetadataBatch;
1953
+ if (this.storage.getVerbMetadataBatch) {
1954
+ // Use batch reading if available (prevents socket exhaustion)
1955
+ verbMetadataBatch = await this.storage.getVerbMetadataBatch(verbIds);
1956
+ prodLog.debug(`πŸ“¦ Batch loaded ${verbMetadataBatch.size}/${verbIds.length} verb metadata objects`);
1957
+ }
1958
+ else {
1959
+ // Fallback to individual calls with strict concurrency control
1960
+ verbMetadataBatch = new Map();
1961
+ const CONCURRENCY_LIMIT = 3; // Very conservative limit to prevent socket exhaustion
1962
+ for (let i = 0; i < verbIds.length; i += CONCURRENCY_LIMIT) {
1963
+ const batch = verbIds.slice(i, i + CONCURRENCY_LIMIT);
1964
+ const batchPromises = batch.map(async (id) => {
1965
+ try {
1966
+ const metadata = await this.storage.getVerbMetadata(id);
1967
+ return { id, metadata };
1968
+ }
1969
+ catch (error) {
1970
+ prodLog.debug(`Failed to read verb metadata for ${id}:`, error);
1971
+ return { id, metadata: null };
1972
+ }
1973
+ });
1974
+ const batchResults = await Promise.all(batchPromises);
1975
+ for (const { id, metadata } of batchResults) {
1976
+ if (metadata) {
1977
+ verbMetadataBatch.set(id, metadata);
1978
+ }
1979
+ }
1980
+ // Yield between batches to prevent socket exhaustion
1981
+ await this.yieldToEventLoop();
1982
+ }
1983
+ }
1984
+ // Process the verb metadata batch
1985
+ for (const verb of result.items) {
1986
+ const metadata = verbMetadataBatch.get(verb.id);
1987
+ if (metadata) {
1988
+ // Skip flush during rebuild for performance
1989
+ await this.addToIndex(verb.id, metadata, true);
1990
+ }
1991
+ }
1992
+ // Yield after processing the entire batch
1993
+ await this.yieldToEventLoop();
1994
+ totalVerbsProcessed += result.items.length;
1995
+ hasMoreVerbs = result.hasMore;
1996
+ verbOffset += verbLimit;
1997
+ // Progress logging and event loop yield after each batch
1998
+ if (totalVerbsProcessed % 100 === 0 || !hasMoreVerbs) {
1999
+ prodLog.debug(`πŸ”— Indexed ${totalVerbsProcessed} verbs...`);
2000
+ }
2001
+ await this.yieldToEventLoop();
2002
+ }
2003
+ // Check iteration limits for cloud storage
2004
+ if (verbIterations >= MAX_ITERATIONS) {
2005
+ prodLog.error(`❌ Metadata verb rebuild hit maximum iteration limit (${MAX_ITERATIONS}). This indicates a bug in storage pagination.`);
2006
+ }
1923
2007
  }
1924
2008
  // Flush to storage with final yield
1925
2009
  prodLog.debug('πŸ’Ύ Flushing metadata index to storage...');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soulcraft/brainy",
3
- "version": "4.2.2",
3
+ "version": "4.2.3",
4
4
  "description": "Universal Knowledge Protocolβ„’ - World's first Triple Intelligence database unifying vector, graph, and document search in one API. 31 nouns Γ— 40 verbs for infinite expressiveness.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",