From 66d775c72d6b8bdf2e1e56a0370b314bf509be1f Mon Sep 17 00:00:00 2001 From: shaleenji Date: Sun, 25 Jan 2026 07:36:59 +0000 Subject: [PATCH 01/48] Update install.sh --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 2ce5b00bef..2fa5ef3aad 100755 --- a/install.sh +++ b/install.sh @@ -28,7 +28,7 @@ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # dependencies list pkg_debian_ubuntu=(cmake clang-19 build-essential libssl-dev libcurl4-openssl-dev unzip curl git) pkg_redhat=(cmake openssl-devel libcurl-devel clang unzip curl git) -pkg_macos=(cmake unzip curl git) +pkg_macos=(cmake unzip curl git openssl@3) # **************************************** From b6c03b28227a846710bacaa0a158fa1562819844 Mon Sep 17 00:00:00 2001 From: shaleenji Date: Mon, 26 Jan 2026 07:23:44 +0000 Subject: [PATCH 02/48] License (#4) --- CONTRIBUTING.md | 29 ++++++++ LICENSE | 191 ++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 35 +++++++++ 3 files changed, 255 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..e4033d3600 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +# Contributing to Endee + +Thanks for your interest in contributing to Endee! We welcome community +contributions and aim to keep the process simple and transparent. + +## License + +By contributing to this repository, you agree that your contributions will be +licensed under the **Apache License 2.0**, the same license that governs the +project. + +You confirm that: +- You have the right to submit the contribution +- The contribution does not knowingly infringe any third-party rights + +No additional contributor license agreement (CLA) is required. + +## How to Contribute + +1. Fork the repository +2. Create a feature branch +3. Make your changes with clear commits +4. Submit a pull request with a description of the change + +## Code of Conduct + +Be respectful and professional. Harassment or abusive behavior will not be +tolerated. + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..461227b979 --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright [2026] [Endee Labs] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/README.md b/README.md index 5ccaf76a53..f3373286c7 100644 --- a/README.md +++ b/README.md @@ -231,3 +231,38 @@ We welcome contributions from the community to help make vector search faster an * **Feature Requests**: If there is a specific functionality you need, start a discussion in the issues section. --- + +## License + +Endee is open source software licensed under the +**Apache License 2.0**. + +You are free to use, modify, and distribute this software for +personal, commercial, and production use. + +See the LICENSE file for full license terms. + +--- + +## Trademark and Branding + +“Endee” and the Endee logo are trademarks of Endee Labs. + +The Apache License 2.0 does **not** grant permission to use the Endee name, +logos, or branding in a way that suggests endorsement or affiliation. + +If you offer a hosted or managed service based on this software, you must: +- Use your own branding +- Avoid implying it is an official Endee service + +For trademark or branding permissions, contact: enterprise@endee.io + +--- + +## Third-Party Software + +This project includes or depends on third-party software components that are +licensed under their respective open source licenses. + +Use of those components is governed by the terms and conditions of their +individual licenses, not by the Apache License 2.0 for this project. From f8ae28dde76bdbaaddaf50af55412e7a378ed41e Mon Sep 17 00:00:00 2001 From: Burhan Kapdawala <54430616+burhankapadia18@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:33:48 +0530 Subject: [PATCH 03/48] docs: add instructions for running Endee using Docker Compose from a registry image. (#6) Co-authored-by: Burhan Kapdawala --- README.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/README.md b/README.md index f3373286c7..56f29ecbcb 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,47 @@ You can also use `docker-compose` to run the service. --- +## 6. Running Docker container from registry + +You can run Endee directly using the pre-built image from Docker Hub without building locally. + +### Using Docker Compose + +Create a new directory for Endee: + +```bash +mkdir endee && cd endee +``` + +Inside this directory, create a file named `docker-compose.yml` and copy the following content into it: + +```yaml +services: + endee: + image: endeeio/endee-server:latest + container_name: endee-server + ports: + - "8080:8080" + environment: + NDD_NUM_THREADS: 0 + NDD_AUTH_TOKEN: "" # Optional: set for authentication + volumes: + - endee-data:/data + restart: unless-stopped + +volumes: + endee-data: +``` + +Then run: +```bash +docker compose up -d +``` + +for more details visit [docs.endee.io](https://docs.endee.io/quick-start) + +--- + ## Contribution We welcome contributions from the community to help make vector search faster and more accessible for everyone. To contribute: From 4c67b816bc2a385de78c957eaa00735e57c93db2 Mon Sep 17 00:00:00 2001 From: Hemant Sharma Date: Wed, 28 Jan 2026 19:13:02 +0530 Subject: [PATCH 04/48] Fix: Delete vectors from sparse storage when deleting by filter --- src/core/ndd.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 49b54890cc..f7d662a31a 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -1434,6 +1434,10 @@ class IndexManager { entry.vector_storage->deleteFilter(numeric_id, meta.filter); // Mark as deleted in HNSW index entry.alg->markDelete(numeric_id); + // Delete from sparse storage if hybrid index + if(entry.sparse_storage) { + entry.sparse_storage->delete_vector(numeric_id); + } } // Add the list to write ahead log using IndexManager's method logDeletions(entry.index_id, numeric_ids); From 191fa8a6b33ac8bba5d23c65e464fb2612023d3f Mon Sep 17 00:00:00 2001 From: Burhan Kapdawala Date: Mon, 2 Feb 2026 09:35:48 +0530 Subject: [PATCH 05/48] feat: Implement backup download and upload API endpoints --- README.md | 2 +- install.sh | 2 +- src/core/ndd.hpp | 113 ++++++++++++++++++++++++---------------- src/main.cpp | 130 ++++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 196 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 56f29ecbcb..a495462aa3 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ The output binary name depends on the SIMD flag used during compilation: ### Runtime Environment Variables -Some enviroment variables **ndd** reads at runtime: +Some environment variables **ndd** reads at runtime: * `NDD_DATA_DIR`: Defines the data directory * `NDD_AUTH_TOKEN`: Optional authentication token (see below) diff --git a/install.sh b/install.sh index 2fa5ef3aad..ce915fb4cc 100755 --- a/install.sh +++ b/install.sh @@ -201,7 +201,7 @@ add_frontend() { mkdir -p $script_dir/frontend cd $script_dir/frontend curl -L -o react-dist.zip https://github.com/EndeeLabs/endee-web-ui/releases/latest/download/endee-web-ui.zip - unzip react-dist.zip + unzip -o react-dist.zip rm react-dist.zip log "frontend added" } diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index f7d662a31a..46bf850447 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -550,20 +550,22 @@ class IndexManager { } // Helper method to validate backup names - void validateBackupName(const std::string& backup_name) const { + std::pair validateBackupName(const std::string& backup_name) const { if(backup_name.empty()) { - throw std::runtime_error("Backup name cannot be empty"); + return std::make_pair(false, "Backup name cannot be empty"); } // Check length limit (most filesystems limit to 255 chars) if(backup_name.length() > MAX_BACKUP_NAME_LENGTH) { - throw std::runtime_error("Backup name too long (max " - + std::to_string(MAX_BACKUP_NAME_LENGTH) + " characters)"); + return std::make_pair(false, + "Backup name too long (max " + + std::to_string(MAX_BACKUP_NAME_LENGTH) + + " characters)"); } // Check for reserved names if(backup_name == "." || backup_name == "..") { - throw std::runtime_error("Invalid backup name: cannot be '.' or '..'"); + return std::make_pair(false, "Invalid backup name: cannot be '.' or '..'"); } // Check for reserved/problematic names (cross-platform) @@ -578,7 +580,7 @@ class IndexManager { || lower_name == "lpt1" || lower_name == "lpt2" || lower_name == "lpt3" || lower_name == "lpt4" || lower_name == "lpt5" || lower_name == "lpt6" || lower_name == "lpt7" || lower_name == "lpt8" || lower_name == "lpt9") { - throw std::runtime_error("Invalid backup name: reserved system name (Windows)"); + return std::make_pair(false, "Invalid backup name: reserved system name (Windows)"); } // Unix/Linux/macOS device names (could cause confusion) @@ -589,29 +591,30 @@ class IndexManager { || lower_name == "stderr" || lower_name == "tty" || lower_name == "console" || lower_name == "kmem" || lower_name == "mem" || lower_name == "core" || lower_name == "full" || lower_name == "ptmx") { - throw std::runtime_error("Invalid backup name: device/system name (Unix/Linux)"); + return std::make_pair(false, "Invalid backup name: device/system name (Unix/Linux)"); } // Prevent hidden files (names starting with dot) if(backup_name[0] == '.') { - throw std::runtime_error("Invalid backup name: cannot start with '.'"); + return std::make_pair(false, "Invalid backup name: cannot start with '.'"); } // Prevent trailing dots or spaces (Windows issue) char last_char = backup_name[backup_name.length() - 1]; if(last_char == '.' || last_char == ' ') { - throw std::runtime_error("Invalid backup name: cannot end with '.' or space"); + return std::make_pair(false, "Invalid backup name: cannot end with '.' or space"); } // Prevent leading/trailing whitespace if(std::isspace(backup_name[0]) || std::isspace(last_char)) { - throw std::runtime_error("Invalid backup name: cannot start or end with whitespace"); + return std::make_pair(false, + "Invalid backup name: cannot start or end with whitespace"); } // Check for path traversal attacks if(backup_name.find("..") != std::string::npos || backup_name.find('/') != std::string::npos || backup_name.find('\\') != std::string::npos) { - throw std::runtime_error("Invalid backup name: cannot contain '..', '/', or '\\'"); + return std::make_pair(false, "Invalid backup name: cannot contain '..', '/', or '\\'"); } // Check for dangerous characters (null bytes, control chars, colons, etc.) @@ -620,33 +623,41 @@ class IndexManager { // Check for null bytes if(c == '\0') { - throw std::runtime_error("Invalid backup name: cannot contain null bytes"); + std::cout << "Invalid backup name: cannot contain null bytes at " << i << std::endl; + return std::make_pair(false, "Invalid backup name: cannot contain null bytes"); } // Check for control characters if(std::iscntrl(c)) { - throw std::runtime_error("Invalid backup name: cannot contain control characters"); + return std::make_pair(false, + "Invalid backup name: cannot contain control characters"); } // Check for dangerous characters if(c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|' || c == '&' || c == ';' || c == '$' || c == '`' || c == '\'' || c == '(' || c == ')') { - throw std::runtime_error("Invalid backup name: contains forbidden characters"); + return std::make_pair(false, "Invalid backup name: contains forbidden characters"); } // Only allow alphanumeric, hyphens, underscores, dots, and spaces if(!std::isalnum(c) && c != '-' && c != '_' && c != '.' && c != ' ') { - throw std::runtime_error("Invalid backup name: only alphanumeric, hyphens, " - "underscores, dots, and spaces allowed"); + return std::make_pair(false, + "Invalid backup name: only alphanumeric, hyphens, " + "underscores, dots, and spaces allowed"); } } + return std::make_pair(true, ""); } // Backup methods - void createBackup(const std::string& index_id, const std::string& backup_name) { + std::pair createBackup(const std::string& index_id, + const std::string& backup_name) { // 1. Validate backup name - validateBackupName(backup_name); + std::pair result = validateBackupName(backup_name); + if(!result.first) { + return result; + } // 2. Parse user and index name std::string user_id, index_name; @@ -655,7 +666,7 @@ class IndexManager { user_id = index_id.substr(0, pos); index_name = index_id.substr(pos + 1); } else { - throw std::runtime_error("Invalid index ID format"); + return {false, "Invalid index ID format"}; } // 3. Get index entry and lock @@ -672,7 +683,7 @@ class IndexManager { std::string source_dir = data_dir_ + "/" + index_id; if(std::filesystem::exists(backup_tar)) { - throw std::runtime_error("Backup already exists: " + backup_name); + return {false, "Backup already exists: " + backup_name}; } // 6. Copy files to temporary directory @@ -711,13 +722,14 @@ class IndexManager { if(!ndd::ArchiveUtils::createTarGz(backup_dir, backup_tar, error_msg)) { // Clean up on failure std::filesystem::remove_all(backup_dir); - throw std::runtime_error("Failed to create compressed backup archive: " + error_msg); + return {false, "Failed to create compressed backup archive: " + error_msg}; } // 8. Remove the temporary uncompressed directory std::filesystem::remove_all(backup_dir); LOG_INFO("Created compressed backup: " << backup_tar); + return {true, ""}; } std::vector listBackups() { @@ -743,43 +755,57 @@ class IndexManager { return backups; } - void restoreBackup(const std::string& backup_name, const std::string& target_index_name) { + std::pair restoreBackup(const std::string& backup_name, + const std::string& target_index_name) { // 1. Validate backup name - validateBackupName(backup_name); + std::pair result = validateBackupName(backup_name); + if(!result.first) { + return result; + } // Use default username for single-user system std::string user_id = settings::DEFAULT_USERNAME; std::string backup_dir_root = data_dir_ + "/backups"; std::string backup_tar = backup_dir_root + "/" + backup_name + ".tar.gz"; - std::string backup_dir = backup_dir_root + "/" + backup_name; + std::string backup_extract_dir = backup_dir_root + "/" + backup_name; std::string target_index_id = user_id + "/" + target_index_name; std::string target_dir = data_dir_ + "/" + target_index_id; // 2. Validation - check for tar.gz file if(!std::filesystem::exists(backup_tar)) { - throw std::runtime_error("Backup not found: " + backup_name); + return {false, "Backup not found: " + backup_name}; } if(metadata_manager_->getMetadata(target_index_id).has_value()) { - throw std::runtime_error("Target index already exists"); + return {false, "Target index already exists"}; } // 3. Extract tar.gz to temporary directory using libarchive std::string error_msg; - if(!ndd::ArchiveUtils::extractTarGz(backup_tar, backup_dir_root, error_msg)) { - throw std::runtime_error("Failed to extract backup archive: " + error_msg); + if(!ndd::ArchiveUtils::extractTarGz(backup_tar, backup_extract_dir, error_msg)) { + return {false, "Failed to extract backup archive: " + error_msg}; } - // Ensure backup directory was extracted - if(!std::filesystem::exists(backup_dir)) { - throw std::runtime_error("Backup extraction failed - directory not found"); + // check if any folder is present in backup_extract_dir + std::vector folders; + for(const auto& entry : std::filesystem::directory_iterator(backup_extract_dir)) { + if(entry.is_directory()) { + folders.push_back(entry.path().string()); + } } + if(folders.size() != 1) { + std::filesystem::remove_all(backup_extract_dir); + return {false, "Backup extraction failed - directory not found"}; + } + + std::string backup_dir = folders[0]; + try { // 3. Read metadata std::ifstream f(backup_dir + "/metadata.json"); if(!f.good()) { - std::filesystem::remove_all(backup_dir); - throw std::runtime_error("Backup metadata missing"); + std::filesystem::remove_all(backup_extract_dir); + return {false, "Backup metadata missing"}; } nlohmann::json meta_json = nlohmann::json::parse(f); @@ -810,29 +836,34 @@ class IndexManager { metadata_manager_->storeMetadata(target_index_id, new_meta); // 6. Clean up extracted temporary directory - std::filesystem::remove_all(backup_dir); + std::filesystem::remove_all(backup_extract_dir); // 7. Load index loadIndex(target_index_id); LOG_INFO("Restored backup from compressed archive: " << backup_tar); + return {true, ""}; } catch(const std::exception& e) { // Clean up on failure - std::filesystem::remove_all(backup_dir); - throw; + std::filesystem::remove_all(backup_extract_dir); + return {false, "Failed to restore backup: " + std::string(e.what())}; } } - void deleteBackup(const std::string& backup_name) { + std::pair deleteBackup(const std::string& backup_name) { // Validate backup name - validateBackupName(backup_name); + std::pair result = validateBackupName(backup_name); + if(!result.first) { + return result; + } std::string backup_tar = data_dir_ + "/backups/" + backup_name + ".tar.gz"; if(std::filesystem::exists(backup_tar)) { std::filesystem::remove(backup_tar); LOG_INFO("Deleted compressed backup: " << backup_tar); + return {true, ""}; } else { - throw std::runtime_error("Backup not found"); + return {false, "Backup not found"}; } } @@ -1434,10 +1465,6 @@ class IndexManager { entry.vector_storage->deleteFilter(numeric_id, meta.filter); // Mark as deleted in HNSW index entry.alg->markDelete(numeric_id); - // Delete from sparse storage if hybrid index - if(entry.sparse_storage) { - entry.sparse_storage->delete_vector(numeric_id); - } } // Add the list to write ahead log using IndexManager's method logDeletions(entry.index_id, numeric_ids); diff --git a/src/main.cpp b/src/main.cpp index a685831db7..cf29bd03e6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -409,8 +409,12 @@ int main(int argc, char** argv) { std::string index_id = ctx.username + "/" + index_name; try { - index_manager.createBackup(index_id, backup_name); - return crow::response(200, "Backup created successfully"); + std::pair result = + index_manager.createBackup(index_id, backup_name); + if(!result.first) { + return json_error(400, result.second); + } + return crow::response(201, "Backup created successfully"); } catch(const std::exception& e) { return json_error(500, e.what()); } @@ -449,8 +453,12 @@ int main(int argc, char** argv) { std::string target_index_name = body["target_index_name"].s(); try { - index_manager.restoreBackup(backup_name, target_index_name); - return crow::response(200, "Backup restored successfully"); + std::pair result = + index_manager.restoreBackup(backup_name, target_index_name); + if(!result.first) { + return json_error(400, result.second); + } + return crow::response(201, "Backup restored successfully"); } catch(const std::exception& e) { return json_error(500, e.what()); } @@ -463,8 +471,118 @@ int main(int argc, char** argv) { const std::string& backup_name) { auto& ctx = app.get_context(req); try { - index_manager.deleteBackup(backup_name); - return crow::response(200, "Backup deleted successfully"); + std::pair result = index_manager.deleteBackup(backup_name); + if(!result.first) { + return json_error(400, result.second); + } + return crow::response(204, "Backup deleted successfully"); + } catch(const std::exception& e) { + return json_error(500, e.what()); + } + }); + + // Download Backup + CROW_ROUTE(app, "/api/v1/backups//download") + .CROW_MIDDLEWARES(app, AuthMiddleware) + .methods("GET"_method)([&index_manager, &app](const crow::request& req, + const std::string& backup_name) { + auto& ctx = app.get_context(req); + try { + std::string backup_tar = + settings::DATA_DIR + "/backups/" + backup_name + ".tar.gz"; + if(!std::filesystem::exists(backup_tar)) { + return json_error(404, "Backup not found"); + } + std::string file_content = read_file(backup_tar); + if(file_content.empty()) { + return json_error(500, "Failed to read backup file"); + } + crow::response response; + response.set_header("Content-Type", "application/gzip"); + response.set_header("Content-Disposition", + "attachment; filename=\"" + backup_name + ".tar.gz\""); + response.set_header("Content-Length", std::to_string(file_content.size())); + response.set_header("Cache-Control", "no-cache"); + response.body = std::move(file_content); + return response; + } catch(const std::exception& e) { + return json_error(500, e.what()); + } + }); + + // upload Backup + CROW_ROUTE(app, "/api/v1/backups/upload") + .CROW_MIDDLEWARES(app, AuthMiddleware) + .methods("POST"_method)([&index_manager, &app](const crow::request& req) { + auto& ctx = app.get_context(req); + try { + // Parse multipart message + crow::multipart::message msg(req); + + // Find the file part + std::string backup_name; + std::string file_content; + + for(const auto& part : msg.parts) { + auto content_disposition = part.get_header_object("Content-Disposition"); + std::string name = content_disposition.params.count("name") + ? content_disposition.params.at("name") + : ""; + + if(name == "backup") { + // Get filename from Content-Disposition + if(content_disposition.params.count("filename")) { + backup_name = content_disposition.params.at("filename"); + // check if backup name ends with .tar.gz + if(backup_name.ends_with(".tar.gz")) { + backup_name = backup_name.substr(0, backup_name.size() - 7); + } else { + return json_error(400, "Invalid backup file extension"); + } + } + file_content = part.body; + break; + } + } + + if(backup_name.empty()) { + return json_error(400, "Missing backup name or filename"); + } + + if(file_content.empty()) { + return json_error(400, "Missing backup file content"); + } + + // Validate backup name + std::pair result = + index_manager.validateBackupName(backup_name); + if(!result.first) { + return json_error(400, result.second); + } + + // Check if backup already exists + std::string backup_path = + settings::DATA_DIR + "/backups/" + backup_name + ".tar.gz"; + if(std::filesystem::exists(backup_path)) { + return json_error(409, + "Backup with name '" + backup_name + "' already exists"); + } + + // Write the file + std::ofstream out(backup_path, std::ios::binary); + if(!out.is_open()) { + return json_error(500, "Failed to create backup file"); + } + out.write(file_content.data(), file_content.size()); + out.close(); + + if(!out.good()) { + // Clean up partial file on error + std::filesystem::remove(backup_path); + return json_error(500, "Failed to write backup file"); + } + + return crow::response(201, "Backup uploaded successfully"); } catch(const std::exception& e) { return json_error(500, e.what()); } From e7361d0e96389a7869a943daae646f0686af5899 Mon Sep 17 00:00:00 2001 From: Burhan Kapdawala Date: Mon, 2 Feb 2026 11:18:12 +0530 Subject: [PATCH 06/48] feat: Add run.sh helper script for simplified server execution and update README with usage instructions. --- README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++---- run.sh | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 run.sh diff --git a/README.md b/README.md index 56f29ecbcb..aa3d4411a4 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ The output binary name depends on the SIMD flag used during compilation: ### Runtime Environment Variables -Some enviroment variables **ndd** reads at runtime: +Some environment variables **ndd** reads at runtime: * `NDD_DATA_DIR`: Defines the data directory * `NDD_AUTH_TOKEN`: Optional authentication token (see below) @@ -182,7 +182,52 @@ NDD_DATA_DIR=./data ./build/ndd-avx2 ``` --- -## 5. Docker Deployment + +## 5. Using the run.sh helper script + +We provide a `run.sh` script to simplify running the server. It automatically detects the built binary and uses `ndd_data_dir=./data` by default. + +First, ensure the script is executable: + +```bash +chmod +x ./run.sh +``` + +Then run the script: + +```bash +./run.sh +``` + +### Options + +You can override the defaults using arguments: + +* `ndd_data_dir=DIR`: Set the data directory. +* `binary_file=FILE`: Set the binary file to run. + +### Examples + +**Run with custom data directory:** + +```bash +./run.sh ndd_data_dir=./my_data +``` + +**Run specific binary:** + +```bash +./run.sh binary_file=./build/ndd-avx2 +``` + +**Show help:** + +```bash +./run.sh --help +``` + +--- +## 6. Docker Deployment We provide a Dockerfile for easy containerization. This ensures a consistent runtime environment and simplifies the deployment process across various platforms. @@ -221,7 +266,7 @@ You can also use `docker-compose` to run the service. --- -## 6. Running Docker container from registry +## 7. Running Docker container from registry You can run Endee directly using the pre-built image from Docker Hub without building locally. @@ -306,4 +351,4 @@ This project includes or depends on third-party software components that are licensed under their respective open source licenses. Use of those components is governed by the terms and conditions of their -individual licenses, not by the Apache License 2.0 for this project. +individual licenses, not by the Apache License 2.0 for this project. \ No newline at end of file diff --git a/run.sh b/run.sh new file mode 100644 index 0000000000..5f7553b412 --- /dev/null +++ b/run.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -euo pipefail + +log() { printf "[INFO] %s\n" "$*"; } +warn() { printf "[WARN] %s\n" "$*" >&2; } +error() { printf "[ERROR] %s\n" "$*" >&2; } + +NDD_DATA_DIR="./data" +BINARY_FILE="" + + +print_help() { + cat < Date: Mon, 2 Feb 2026 13:29:00 +0530 Subject: [PATCH 07/48] added auth variable in run.sh and readme updated --- README.md | 114 ++++++++++++++++++++++++++++++++--------------------- install.sh | 6 +++ run.sh | 7 +++- 3 files changed, 81 insertions(+), 46 deletions(-) mode change 100644 => 100755 run.sh diff --git a/README.md b/README.md index aa3d4411a4..31a0678b90 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,11 @@ The easiest way to build **ndd** is using the included `install.sh` script. This ### Usage +First, ensure the script is executable: +```bash +chmod +x ./install.sh +``` + Run the script from the root of the repository. You **must** provide arguments for the build mode and/or CPU optimization. ```bash @@ -75,7 +80,65 @@ Select the flag matching your hardware to enable SIMD optimizations. ./install.sh --debug_all --neon ``` ---- +### Running the Server + +We provide a `run.sh` script to simplify running the server. It automatically detects the built binary and uses `ndd_data_dir=./data` by default. + +First, ensure the script is executable: + +```bash +chmod +x ./run.sh +``` + +Then run the script: + +```bash +./run.sh +``` + +This will automatically identify the latest binary and start the server. + +#### Options + +You can override the defaults using arguments: + +* `ndd_data_dir=DIR`: Set the data directory. +* `binary_file=FILE`: Set the binary file to run. +* `ndd_auth_token=TOKEN`: Set the authentication token (leave empty/ignore to run without authentication). + +#### Examples + +**Run with custom data directory:** + +```bash +./run.sh ndd_data_dir=./my_data +``` + +**Run specific binary:** + +```bash +./run.sh binary_file=./build/ndd-avx2 +``` + +**Run with authentication token:** + +```bash +./run.sh ndd_auth_token=your_token +``` + + +**Run with all options** + +```bash +./run.sh ndd_data_dir=./my_data binary_file=./build/ndd-avx2 NDD_AUTH_TOKEN=your_token +``` + +**For Help** + +```bash +./run.sh --help +``` + ## 3. Manual Build (Advanced) @@ -183,51 +246,9 @@ NDD_DATA_DIR=./data ./build/ndd-avx2 --- -## 5. Using the run.sh helper script - -We provide a `run.sh` script to simplify running the server. It automatically detects the built binary and uses `ndd_data_dir=./data` by default. - -First, ensure the script is executable: - -```bash -chmod +x ./run.sh -``` - -Then run the script: - -```bash -./run.sh -``` - -### Options - -You can override the defaults using arguments: - -* `ndd_data_dir=DIR`: Set the data directory. -* `binary_file=FILE`: Set the binary file to run. - -### Examples - -**Run with custom data directory:** -```bash -./run.sh ndd_data_dir=./my_data -``` -**Run specific binary:** - -```bash -./run.sh binary_file=./build/ndd-avx2 -``` - -**Show help:** - -```bash -./run.sh --help -``` - ---- -## 6. Docker Deployment +## 5. Docker Deployment We provide a Dockerfile for easy containerization. This ensures a consistent runtime environment and simplifies the deployment process across various platforms. @@ -251,10 +272,13 @@ The container exposes port `8080` and stores data in `/data` inside container. Y docker run \ -p 8080:8080 \ -v endee-data:/data \ + -e NDD_AUTH_TOKEN="your_secure_token" \ --name endee-server \ endee-oss:latest ``` +leave `NDD_AUTH_TOKEN` empty or remove it to run endee without authentication. + ### Alternatively: Docker Compose You can also use `docker-compose` to run the service. @@ -266,7 +290,7 @@ You can also use `docker-compose` to run the service. --- -## 7. Running Docker container from registry +## 6. Running Docker container from registry You can run Endee directly using the pre-built image from Docker Hub without building locally. diff --git a/install.sh b/install.sh index 2fa5ef3aad..05fe07692d 100755 --- a/install.sh +++ b/install.sh @@ -406,6 +406,12 @@ main() { build_project add_frontend + + + log "" + log "Build and installation successful!" + log "You can now start the server by running:" + log " ./run.sh" } main "$@" diff --git a/run.sh b/run.sh old mode 100644 new mode 100755 index 5f7553b412..6599636c87 --- a/run.sh +++ b/run.sh @@ -7,6 +7,7 @@ error() { printf "[ERROR] %s\n" "$*" >&2; } NDD_DATA_DIR="./data" BINARY_FILE="" +NDD_AUTH_TOKEN="" print_help() { @@ -16,6 +17,7 @@ Usage: $(basename "$0") [OPTIONS] Options: ndd_data_dir=DIR Set the data directory (default: ./data) binary_file=FILE Set the binary file to run (default: auto-detected in build/) + NDD_AUTH_TOKEN=TOKEN Set the auth token (default: empty) --help, -h Show this help message and exit Description: @@ -33,6 +35,9 @@ main() { binary_file=*) BINARY_FILE="${ARG#*=}" ;; + ndd_auth_token=*) + NDD_AUTH_TOKEN="${ARG#*=}" + ;; --help|-h) print_help exit 0 @@ -53,7 +58,7 @@ main() { # run the binary with the arguments passed to this script if [[ -n "$BINARY_FILE" ]]; then - eval "NDD_DATA_DIR=$NDD_DATA_DIR $BINARY_FILE" + eval "NDD_DATA_DIR=$NDD_DATA_DIR NDD_AUTH_TOKEN=$NDD_AUTH_TOKEN $BINARY_FILE" fi } From 290e3b073e09a22b2c17a727fca3ec26498ec02c Mon Sep 17 00:00:00 2001 From: Burhan Kapdawala Date: Mon, 2 Feb 2026 14:25:15 +0530 Subject: [PATCH 08/48] readme formatted --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 31a0678b90..65bf92442f 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,16 @@ **Endee (nD)** is a specialized, high-performance vector database built for speed and efficiency. This guide covers supported platforms, dependency requirements, and detailed build instructions using both our automated installer and manual CMake configuration. +there are 3 ways to build and run endee: +1. quick installation and run using install.sh and run.sh scripts +2. manual build using cmake +3. using docker + +also you can run endee using docker from docker hub without building it locally. refer to section 4 for more details. + --- -## 1. System Requirements +## System Requirements Before installing, ensure your system meets the following hardware and operating system requirements. @@ -23,7 +30,7 @@ The following packages are required for compilation. --- -## 2. Quick Installation (Recommended) +## 1. Quick Installation (Recommended) The easiest way to build **ndd** is using the included `install.sh` script. This script handles OS detection, dependency checks, and configuration automatically. @@ -140,7 +147,7 @@ You can override the defaults using arguments: ``` -## 3. Manual Build (Advanced) +## 2. Manual Build (Advanced) If you prefer to configure the build manually or integrate it into an existing install pipeline, you can use `cmake` directly. @@ -182,9 +189,7 @@ cmake -DCMAKE_BUILD_TYPE=Release \ make -j$(nproc) ``` ---- - -## 4. Running ndd +### Running the Built Binary After a successful build, the binary will be generated in the `build/` directory. @@ -248,7 +253,7 @@ NDD_DATA_DIR=./data ./build/ndd-avx2 -## 5. Docker Deployment +## 3. Docker Deployment We provide a Dockerfile for easy containerization. This ensures a consistent runtime environment and simplifies the deployment process across various platforms. @@ -290,7 +295,7 @@ You can also use `docker-compose` to run the service. --- -## 6. Running Docker container from registry +## 4. Running Docker container from registry You can run Endee directly using the pre-built image from Docker Hub without building locally. From 936fd96b2ffc1a917b8a04a56872bbc26fe46427 Mon Sep 17 00:00:00 2001 From: Burhan Kapdawala Date: Tue, 3 Feb 2026 12:18:26 +0530 Subject: [PATCH 09/48] using regex for backup name validation --- src/core/ndd.hpp | 88 ++++-------------------------------------------- 1 file changed, 7 insertions(+), 81 deletions(-) diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 46bf850447..2a4b93ed04 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -1,5 +1,7 @@ #pragma once #include +#include + #include "hnsw/hnswlib.h" #include "settings.hpp" #include "id_mapper.hpp" @@ -563,90 +565,14 @@ class IndexManager { + " characters)"); } - // Check for reserved names - if(backup_name == "." || backup_name == "..") { - return std::make_pair(false, "Invalid backup name: cannot be '.' or '..'"); - } - - // Check for reserved/problematic names (cross-platform) - std::string lower_name = backup_name; - std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), ::tolower); - - // Windows reserved names (case-insensitive) - if(lower_name == "con" || lower_name == "prn" || lower_name == "aux" || lower_name == "nul" - || lower_name == "com1" || lower_name == "com2" || lower_name == "com3" - || lower_name == "com4" || lower_name == "com5" || lower_name == "com6" - || lower_name == "com7" || lower_name == "com8" || lower_name == "com9" - || lower_name == "lpt1" || lower_name == "lpt2" || lower_name == "lpt3" - || lower_name == "lpt4" || lower_name == "lpt5" || lower_name == "lpt6" - || lower_name == "lpt7" || lower_name == "lpt8" || lower_name == "lpt9") { - return std::make_pair(false, "Invalid backup name: reserved system name (Windows)"); - } - - // Unix/Linux/macOS device names (could cause confusion) - // These aren't technically reserved, but blocking them prevents confusion with /dev/ - // devices - if(lower_name == "null" || lower_name == "zero" || lower_name == "random" - || lower_name == "urandom" || lower_name == "stdin" || lower_name == "stdout" - || lower_name == "stderr" || lower_name == "tty" || lower_name == "console" - || lower_name == "kmem" || lower_name == "mem" || lower_name == "core" - || lower_name == "full" || lower_name == "ptmx") { - return std::make_pair(false, "Invalid backup name: device/system name (Unix/Linux)"); - } - - // Prevent hidden files (names starting with dot) - if(backup_name[0] == '.') { - return std::make_pair(false, "Invalid backup name: cannot start with '.'"); - } - - // Prevent trailing dots or spaces (Windows issue) - char last_char = backup_name[backup_name.length() - 1]; - if(last_char == '.' || last_char == ' ') { - return std::make_pair(false, "Invalid backup name: cannot end with '.' or space"); - } - - // Prevent leading/trailing whitespace - if(std::isspace(backup_name[0]) || std::isspace(last_char)) { + // Use regex to check for alphanumeric, underscores, and hyphens + static const std::regex backup_name_regex("^[a-zA-Z0-9_-]+$"); + if(!std::regex_match(backup_name, backup_name_regex)) { return std::make_pair(false, - "Invalid backup name: cannot start or end with whitespace"); + "Invalid backup name: only alphanumeric, underscores, " + "and hyphens allowed"); } - // Check for path traversal attacks - if(backup_name.find("..") != std::string::npos || backup_name.find('/') != std::string::npos - || backup_name.find('\\') != std::string::npos) { - return std::make_pair(false, "Invalid backup name: cannot contain '..', '/', or '\\'"); - } - - // Check for dangerous characters (null bytes, control chars, colons, etc.) - for(size_t i = 0; i < backup_name.length(); ++i) { - char c = backup_name[i]; - - // Check for null bytes - if(c == '\0') { - std::cout << "Invalid backup name: cannot contain null bytes at " << i << std::endl; - return std::make_pair(false, "Invalid backup name: cannot contain null bytes"); - } - - // Check for control characters - if(std::iscntrl(c)) { - return std::make_pair(false, - "Invalid backup name: cannot contain control characters"); - } - - // Check for dangerous characters - if(c == ':' || c == '*' || c == '?' || c == '"' || c == '<' || c == '>' || c == '|' - || c == '&' || c == ';' || c == '$' || c == '`' || c == '\'' || c == '(' - || c == ')') { - return std::make_pair(false, "Invalid backup name: contains forbidden characters"); - } - - // Only allow alphanumeric, hyphens, underscores, dots, and spaces - if(!std::isalnum(c) && c != '-' && c != '_' && c != '.' && c != ' ') { - return std::make_pair(false, - "Invalid backup name: only alphanumeric, hyphens, " - "underscores, dots, and spaces allowed"); - } - } return std::make_pair(true, ""); } From 6d063817aa7f0114c16df77895e9275a4276bbc3 Mon Sep 17 00:00:00 2001 From: Burhan Kapdawala Date: Tue, 3 Feb 2026 16:47:17 +0530 Subject: [PATCH 10/48] Update run.sh --- run.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/run.sh b/run.sh index 6599636c87..b3e4e8af9c 100755 --- a/run.sh +++ b/run.sh @@ -17,7 +17,7 @@ Usage: $(basename "$0") [OPTIONS] Options: ndd_data_dir=DIR Set the data directory (default: ./data) binary_file=FILE Set the binary file to run (default: auto-detected in build/) - NDD_AUTH_TOKEN=TOKEN Set the auth token (default: empty) + ndd_auth_token=TOKEN Set the auth token (default: empty) --help, -h Show this help message and exit Description: @@ -62,4 +62,4 @@ main() { fi } -main "$@" \ No newline at end of file +main "$@" From 05da5c3be28c56cb66336a03ea47547064b44ae2 Mon Sep 17 00:00:00 2001 From: Burhan Kapdawala Date: Tue, 3 Feb 2026 16:49:01 +0530 Subject: [PATCH 11/48] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65bf92442f..0d05093469 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ You can override the defaults using arguments: **Run with all options** ```bash -./run.sh ndd_data_dir=./my_data binary_file=./build/ndd-avx2 NDD_AUTH_TOKEN=your_token +./run.sh ndd_data_dir=./my_data binary_file=./build/ndd-avx2 ndd_auth_token=your_token ``` **For Help** @@ -380,4 +380,4 @@ This project includes or depends on third-party software components that are licensed under their respective open source licenses. Use of those components is governed by the terms and conditions of their -individual licenses, not by the Apache License 2.0 for this project. \ No newline at end of file +individual licenses, not by the Apache License 2.0 for this project. From bf669adf2bf02f836ae2f5cd58e11deb121ad6ca Mon Sep 17 00:00:00 2001 From: pankajLaunchX Date: Tue, 3 Feb 2026 17:03:26 +0530 Subject: [PATCH 12/48] refactor: update web ui to version 1.0.2 --- .gitignore | 3 +++ install.sh | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 9bf92b8007..61e959191a 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ data/* # Frontend frontend/ frontend/* + +# DS Store +.DS_Store diff --git a/install.sh b/install.sh index ce915fb4cc..0b2d41a900 100755 --- a/install.sh +++ b/install.sh @@ -200,7 +200,7 @@ add_frontend() { log "pulling frontend" mkdir -p $script_dir/frontend cd $script_dir/frontend - curl -L -o react-dist.zip https://github.com/EndeeLabs/endee-web-ui/releases/latest/download/endee-web-ui.zip + curl -L -o react-dist.zip https://github.com/EndeeLabs/endee-web-ui/releases/download/v1.0.2/endee-web-ui.zip unzip -o react-dist.zip rm react-dist.zip log "frontend added" From 8811559c8d1422729fafd9a612dda113e45cbe79 Mon Sep 17 00:00:00 2001 From: Vineet Dwivedi Date: Wed, 28 Jan 2026 23:11:14 +0530 Subject: [PATCH 13/48] quantization optimization and bug fixes --- src/quant/binary.hpp | 4 +- src/quant/common.hpp | 3 +- src/quant/float16.hpp | 4 +- src/quant/float32.hpp | 4 +- src/quant/int16d.hpp | 242 ++++++++++++++++++++++++++--------- src/quant/int8d.hpp | 285 +++++++++++++++++++++++------------------- 6 files changed, 346 insertions(+), 196 deletions(-) diff --git a/src/quant/binary.hpp b/src/quant/binary.hpp index 0183704e95..967fe730a6 100644 --- a/src/quant/binary.hpp +++ b/src/quant/binary.hpp @@ -28,7 +28,7 @@ namespace ndd { } // No scale for binary quantization - inline float extract_inv_scale(const uint8_t* buffer, size_t dimension) { + inline float extract_scale(const uint8_t* buffer, size_t dimension) { return 1.0f; } @@ -666,7 +666,7 @@ namespace ndd { d.dequantize = &binary::dequantize; d.quantize_to_int8 = &binary::quantize_to_int8; d.get_storage_size = &binary::get_storage_size; - d.extract_inv_scale = &binary::extract_inv_scale; + d.extract_scale = &binary::extract_scale; return d; } }; diff --git a/src/quant/common.hpp b/src/quant/common.hpp index 32e0a80536..820fad7e74 100644 --- a/src/quant/common.hpp +++ b/src/quant/common.hpp @@ -28,7 +28,6 @@ namespace ndd { FP32 = 32, // Full precision float (4 bytes per dimension) INT16 = 16, // Dynamic 16-bit integer quantization FP16 = 15, // Half precision float (2 bytes per dimension) - INT4 = 5, // Dynamic 4-bit integer quantization BINARY = 1, // Binary quantization (1 bit per dimension) INT8 = 8, // Dynamic 8-bit integer quantization UNKNOWN = 0 @@ -55,7 +54,7 @@ namespace ndd { // Metadata size_t (*get_storage_size)(size_t dim); - float (*extract_inv_scale)(const uint8_t* in, size_t dim); + float (*extract_scale)(const uint8_t* in, size_t dim); }; // Abstract base class for Quantization implementations diff --git a/src/quant/float16.hpp b/src/quant/float16.hpp index f310d341b9..57860f4d91 100644 --- a/src/quant/float16.hpp +++ b/src/quant/float16.hpp @@ -16,7 +16,7 @@ namespace ndd { return dimension * sizeof(uint16_t); } - inline float extract_inv_scale(const uint8_t* in, size_t dim) { + inline float extract_scale(const uint8_t* in, size_t dim) { return 1.0f; } @@ -1083,7 +1083,7 @@ namespace ndd { d.dequantize = &float16::dequantize; d.quantize_to_int8 = &float16::quantize_to_int8; d.get_storage_size = &float16::get_storage_size; - d.extract_inv_scale = &float16::extract_inv_scale; + d.extract_scale = &float16::extract_scale; return d; } }; diff --git a/src/quant/float32.hpp b/src/quant/float32.hpp index 03a474d21f..e8b1b4c197 100644 --- a/src/quant/float32.hpp +++ b/src/quant/float32.hpp @@ -42,7 +42,7 @@ namespace hnswlib { return result; } - inline float extract_inv_scale(const uint8_t* in, size_t dim) { + inline float extract_scale(const uint8_t* in, size_t dim) { return 1.0f; } @@ -555,7 +555,7 @@ namespace ndd { d.dequantize = &hnswlib::quant::float32::dequantize; d.quantize_to_int8 = &hnswlib::quant::float32::quantize_to_int8; d.get_storage_size = [](size_t dim) { return dim * sizeof(float); }; - d.extract_inv_scale = &hnswlib::quant::float32::extract_inv_scale; + d.extract_scale = &hnswlib::quant::float32::extract_scale; return d; } }; diff --git a/src/quant/int16d.hpp b/src/quant/int16d.hpp index a132f7bccd..30c5921168 100644 --- a/src/quant/int16d.hpp +++ b/src/quant/int16d.hpp @@ -17,7 +17,7 @@ namespace ndd { return dimension * sizeof(int16_t) + sizeof(float); } - inline float extract_inv_scale(const uint8_t* buffer, size_t dimension) { + inline float extract_scale(const uint8_t* buffer, size_t dimension) { return *reinterpret_cast(buffer + dimension * sizeof(int16_t)); } @@ -37,19 +37,20 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; // Avoid division by zero } - float scale = INT16_SCALE / abs_max; + float scale = abs_max / INT16_SCALE; + float inv_scale = 1.0f / scale; // Quantize data int16_t* data_ptr = reinterpret_cast(buffer.data()); for(size_t i = 0; i < dimension; ++i) { - float scaled = input[i] * scale; + float scaled = input[i] * inv_scale; data_ptr[i] = static_cast(std::round(scaled)); } - // Store INVERSE scale for dequantization (abs_max/FP16_SCALE) + // Store scale for dequantization float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int16_t))); - *scale_ptr = abs_max / INT16_SCALE; + *scale_ptr = scale; return buffer; } @@ -70,11 +71,12 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; } - float scale = INT16_SCALE / abs_max; + float scale = abs_max / INT16_SCALE; + float inv_scale = 1.0f / scale; // SIMD quantization - using 24 ZMM registers (75% utilization) int16_t* data_ptr = reinterpret_cast(buffer.data()); - const __m512 scale_vec = _mm512_set1_ps(scale); + const __m512 scale_vec = _mm512_set1_ps(inv_scale); size_t i = 0; size_t vec_size = @@ -146,14 +148,14 @@ namespace ndd { // Handle remaining elements for(; i < dimension; ++i) { - float scaled = input[i] * scale; + float scaled = input[i] * inv_scale; data_ptr[i] = static_cast(std::round(scaled)); } - // Store INVERSE scale for dequantization (abs_max/FP16_SCALE) + // Store scale for dequantization float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int16_t))); - *scale_ptr = abs_max / INT16_SCALE; + *scale_ptr = scale; return buffer; } @@ -176,11 +178,12 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; } - float scale = INT16_SCALE / abs_max; + float scale = abs_max / INT16_SCALE; + float inv_scale = 1.0f / scale; // SIMD quantization - using more registers for better parallelism int16_t* data_ptr = reinterpret_cast(buffer.data()); - const float32x4_t scale_vec = vdupq_n_f32(scale); + const float32x4_t scale_vec = vdupq_n_f32(inv_scale); size_t i = 0; size_t vec_size = @@ -249,14 +252,14 @@ namespace ndd { // Handle remaining elements for(; i < dimension; ++i) { - float scaled = input[i] * scale; + float scaled = input[i] * inv_scale; data_ptr[i] = static_cast(std::round(scaled)); } - // Store INVERSE scale for dequantization (abs_max/FP16_SCALE) + // Store scale for dequantization float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int16_t))); - *scale_ptr = abs_max / INT16_SCALE; + *scale_ptr = scale; return buffer; } @@ -278,7 +281,8 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; } - float scale = INT16_SCALE / abs_max; + float scale = abs_max / INT16_SCALE; + float inv_scale = 1.0f / scale; int16_t* data_ptr = reinterpret_cast(buffer.data()); @@ -287,7 +291,7 @@ namespace ndd { while(svptest_any(svptrue_b32(), pg)) { svfloat32_t vec = svld1_f32(pg, &input[i]); - vec = svmul_f32_x(pg, vec, svdup_f32(scale)); + vec = svmul_f32_x(pg, vec, svdup_f32(inv_scale)); // Convert to int32 (with rounding) svint32_t int_vec = svcvt_s32_f32_x(pg, vec); @@ -299,10 +303,10 @@ namespace ndd { pg = svwhilelt_b32(i, dimension); } - // Store INVERSE scale for dequantization + // Store scale for dequantization float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int16_t))); - *scale_ptr = abs_max / INT16_SCALE; + *scale_ptr = scale; return buffer; } @@ -328,9 +332,9 @@ namespace ndd { size_t dimension) { std::vector output(dimension); const int16_t* data_ptr = reinterpret_cast(buffer); - float scale = extract_inv_scale(buffer, dimension); + float scale = extract_scale(buffer, dimension); - const __m512 inv_scale_vec = _mm512_set1_ps(1.0f / scale); + const __m512 scale_vec = _mm512_set1_ps(scale); size_t i = 0; size_t vec_size = @@ -350,9 +354,9 @@ namespace ndd { __m512 float_vec0 = _mm512_cvtepi32_ps(int32_vec0); __m512 float_vec1 = _mm512_cvtepi32_ps(int32_vec1); - // Apply inverse scale - float_vec0 = _mm512_mul_ps(float_vec0, inv_scale_vec); - float_vec1 = _mm512_mul_ps(float_vec1, inv_scale_vec); + // Apply scale + float_vec0 = _mm512_mul_ps(float_vec0, scale_vec); + float_vec1 = _mm512_mul_ps(float_vec1, scale_vec); // Store results _mm512_storeu_ps(&output[i], float_vec0); @@ -365,13 +369,13 @@ namespace ndd { __m256i int16_vec = _mm256_loadu_si256((__m256i*)&data_ptr[i]); __m512i int32_vec = _mm512_cvtepi16_epi32(int16_vec); __m512 float_vec = _mm512_cvtepi32_ps(int32_vec); - float_vec = _mm512_mul_ps(float_vec, inv_scale_vec); + float_vec = _mm512_mul_ps(float_vec, scale_vec); _mm512_storeu_ps(&output[i], float_vec); } // Handle remaining elements for(; i < dimension; ++i) { - output[i] = static_cast(data_ptr[i]) / scale; + output[i] = static_cast(data_ptr[i]) * scale; } return output; @@ -383,9 +387,9 @@ namespace ndd { size_t dimension) { std::vector output(dimension); const int16_t* data_ptr = reinterpret_cast(buffer); - float scale = extract_inv_scale(buffer, dimension); + float scale = extract_scale(buffer, dimension); - const float32x4_t inv_scale_vec = vdupq_n_f32(1.0f / scale); + const float32x4_t scale_vec = vdupq_n_f32(scale); size_t i = 0; size_t vec_size = @@ -408,11 +412,11 @@ namespace ndd { float32x4_t float_2 = vcvtq_f32_s32(int32_2); float32x4_t float_3 = vcvtq_f32_s32(int32_3); - // Apply inverse scale - float_0 = vmulq_f32(float_0, inv_scale_vec); - float_1 = vmulq_f32(float_1, inv_scale_vec); - float_2 = vmulq_f32(float_2, inv_scale_vec); - float_3 = vmulq_f32(float_3, inv_scale_vec); + // Apply scale + float_0 = vmulq_f32(float_0, scale_vec); + float_1 = vmulq_f32(float_1, scale_vec); + float_2 = vmulq_f32(float_2, scale_vec); + float_3 = vmulq_f32(float_3, scale_vec); // Store results vst1q_f32(&output[i], float_0); @@ -423,7 +427,7 @@ namespace ndd { // Handle remaining elements for(; i < dimension; ++i) { - output[i] = static_cast(data_ptr[i]) / scale; + output[i] = static_cast(data_ptr[i]) * scale; } return output; @@ -435,7 +439,7 @@ namespace ndd { size_t dimension) { std::vector output(dimension); const int16_t* data_ptr = reinterpret_cast(buffer); - float scale = extract_inv_scale(buffer, dimension); + float scale = extract_scale(buffer, dimension); size_t i = 0; svbool_t pg = svwhilelt_b32(i, dimension); @@ -447,8 +451,8 @@ namespace ndd { // Convert to float svfloat32_t float_vec = svcvt_f32_s32_x(pg, int_vec); - // Apply inverse scale (division by scale) - float_vec = svdiv_f32_x(pg, float_vec, svdup_f32(scale)); + // Apply scale + float_vec = svmul_f32_x(pg, float_vec, svdup_f32(scale)); // Store results svst1_f32(pg, &output[i], float_vec); @@ -474,10 +478,10 @@ namespace ndd { // Scalar fallback std::vector output(dimension); const int16_t* data_ptr = reinterpret_cast(buffer); - float scale = extract_inv_scale(buffer, dimension); + float scale = extract_scale(buffer, dimension); for(size_t i = 0; i < dimension; ++i) { - output[i] = static_cast(data_ptr[i]) / scale; + output[i] = static_cast(data_ptr[i]) * scale; } return output; #endif @@ -497,8 +501,8 @@ namespace ndd { const auto* params = static_cast(qty_ptr); size_t qty = params->dim; - float scale1 = extract_inv_scale((const uint8_t*)pVect1, qty); - float scale2 = extract_inv_scale((const uint8_t*)pVect2, qty); + float scale1 = extract_scale((const uint8_t*)pVect1, qty); + float scale2 = extract_scale((const uint8_t*)pVect2, qty); float res = 0; size_t i = 0; @@ -558,10 +562,99 @@ namespace ndd { float32x4_t sum1 = vdupq_n_f32(0); float32x4_t sum2 = vdupq_n_f32(0); float32x4_t sum3 = vdupq_n_f32(0); + float32x4_t sum4 = vdupq_n_f32(0); + float32x4_t sum5 = vdupq_n_f32(0); + float32x4_t sum6 = vdupq_n_f32(0); + float32x4_t sum7 = vdupq_n_f32(0); float32x4_t v_scale1 = vdupq_n_f32(scale1); float32x4_t v_scale2 = vdupq_n_f32(scale2); + size_t qty32 = qty / 32; + for(; i < qty32 * 32; i += 32) { + // Block 0 (16 elements) + int16x8_t v1_0 = vld1q_s16(pVect1 + i); + int16x8_t v2_0 = vld1q_s16(pVect2 + i); + int16x8_t v1_1 = vld1q_s16(pVect1 + i + 8); + int16x8_t v2_1 = vld1q_s16(pVect2 + i + 8); + + // Block 1 (16 elements) + int16x8_t v1_2 = vld1q_s16(pVect1 + i + 16); + int16x8_t v2_2 = vld1q_s16(pVect2 + i + 16); + int16x8_t v1_3 = vld1q_s16(pVect1 + i + 24); + int16x8_t v2_3 = vld1q_s16(pVect2 + i + 24); + + // Process v1_0 (8 elements) -> expands to 2 float vectors + float32x4_t f1_0lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v1_0))); + float32x4_t f2_0lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v2_0))); + f1_0lo = vmulq_f32(f1_0lo, v_scale1); + f2_0lo = vmulq_f32(f2_0lo, v_scale2); + float32x4_t diff0 = vsubq_f32(f1_0lo, f2_0lo); + sum0 = vmlaq_f32(sum0, diff0, diff0); + + float32x4_t f1_0hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v1_0))); + float32x4_t f2_0hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v2_0))); + f1_0hi = vmulq_f32(f1_0hi, v_scale1); + f2_0hi = vmulq_f32(f2_0hi, v_scale2); + float32x4_t diff1 = vsubq_f32(f1_0hi, f2_0hi); + sum1 = vmlaq_f32(sum1, diff1, diff1); + + // Process v1_1 (8 elements) + float32x4_t f1_1lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v1_1))); + float32x4_t f2_1lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v2_1))); + f1_1lo = vmulq_f32(f1_1lo, v_scale1); + f2_1lo = vmulq_f32(f2_1lo, v_scale2); + float32x4_t diff2 = vsubq_f32(f1_1lo, f2_1lo); + sum2 = vmlaq_f32(sum2, diff2, diff2); + + float32x4_t f1_1hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v1_1))); + float32x4_t f2_1hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v2_1))); + f1_1hi = vmulq_f32(f1_1hi, v_scale1); + f2_1hi = vmulq_f32(f2_1hi, v_scale2); + float32x4_t diff3 = vsubq_f32(f1_1hi, f2_1hi); + sum3 = vmlaq_f32(sum3, diff3, diff3); + + // Process v1_2 (8 elements) + float32x4_t f1_2lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v1_2))); + float32x4_t f2_2lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v2_2))); + f1_2lo = vmulq_f32(f1_2lo, v_scale1); + f2_2lo = vmulq_f32(f2_2lo, v_scale2); + float32x4_t diff4 = vsubq_f32(f1_2lo, f2_2lo); + sum4 = vmlaq_f32(sum4, diff4, diff4); + + float32x4_t f1_2hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v1_2))); + float32x4_t f2_2hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v2_2))); + f1_2hi = vmulq_f32(f1_2hi, v_scale1); + f2_2hi = vmulq_f32(f2_2hi, v_scale2); + float32x4_t diff5 = vsubq_f32(f1_2hi, f2_2hi); + sum5 = vmlaq_f32(sum5, diff5, diff5); + + // Process v1_3 (8 elements) + float32x4_t f1_3lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v1_3))); + float32x4_t f2_3lo = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v2_3))); + f1_3lo = vmulq_f32(f1_3lo, v_scale1); + f2_3lo = vmulq_f32(f2_3lo, v_scale2); + float32x4_t diff6 = vsubq_f32(f1_3lo, f2_3lo); + sum6 = vmlaq_f32(sum6, diff6, diff6); + + float32x4_t f1_3hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v1_3))); + float32x4_t f2_3hi = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v2_3))); + f1_3hi = vmulq_f32(f1_3hi, v_scale1); + f2_3hi = vmulq_f32(f2_3hi, v_scale2); + float32x4_t diff7 = vsubq_f32(f1_3hi, f2_3hi); + sum7 = vmlaq_f32(sum7, diff7, diff7); + } + + sum0 = vaddq_f32(sum0, sum1); + sum2 = vaddq_f32(sum2, sum3); + sum4 = vaddq_f32(sum4, sum5); + sum6 = vaddq_f32(sum6, sum7); + + sum0 = vaddq_f32(sum0, sum2); + sum4 = vaddq_f32(sum4, sum6); + sum0 = vaddq_f32(sum0, sum4); + res = vaddvq_f32(sum0); + size_t qty16 = qty / 16; for(; i < qty16 * 16; i += 16) { int16x8_t v1_0 = vld1q_s16(pVect1 + i); @@ -583,7 +676,7 @@ namespace ndd { f1_1 = vmulq_f32(f1_1, v_scale1); f2_1 = vmulq_f32(f2_1, v_scale2); float32x4_t diff1 = vsubq_f32(f1_1, f2_1); - sum1 = vmlaq_f32(sum1, diff1, diff1); + sum0 = vmlaq_f32(sum0, diff1, diff1); // Next 8 float32x4_t f1_2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(v1_1))); @@ -591,19 +684,15 @@ namespace ndd { f1_2 = vmulq_f32(f1_2, v_scale1); f2_2 = vmulq_f32(f2_2, v_scale2); float32x4_t diff2 = vsubq_f32(f1_2, f2_2); - sum2 = vmlaq_f32(sum2, diff2, diff2); + sum0 = vmlaq_f32(sum0, diff2, diff2); float32x4_t f1_3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v1_1))); float32x4_t f2_3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(v2_1))); f1_3 = vmulq_f32(f1_3, v_scale1); f2_3 = vmulq_f32(f2_3, v_scale2); float32x4_t diff3 = vsubq_f32(f1_3, f2_3); - sum3 = vmlaq_f32(sum3, diff3, diff3); + sum0 = vmlaq_f32(sum0, diff3, diff3); } - - sum0 = vaddq_f32(sum0, sum1); - sum2 = vaddq_f32(sum2, sum3); - sum0 = vaddq_f32(sum0, sum2); res = vaddvq_f32(sum0); #elif defined(USE_SVE2) svfloat32_t sum0 = svdup_f32(0.0f); @@ -711,8 +800,8 @@ namespace ndd { const auto* params = static_cast(qty_ptr); size_t qty = params->dim; - float scale1 = extract_inv_scale((const uint8_t*)pVect1, qty); - float scale2 = extract_inv_scale((const uint8_t*)pVect2, qty); + float scale1 = extract_scale((const uint8_t*)pVect1, qty); + float scale2 = extract_scale((const uint8_t*)pVect2, qty); int64_t sum = 0; size_t i = 0; @@ -761,6 +850,41 @@ namespace ndd { #elif defined(USE_NEON) int64x2_t sum_vec0 = vdupq_n_s64(0); int64x2_t sum_vec1 = vdupq_n_s64(0); + int64x2_t sum_vec2 = vdupq_n_s64(0); + int64x2_t sum_vec3 = vdupq_n_s64(0); + + size_t qty32 = qty / 32; + for(; i < qty32 * 32; i += 32) { + // Block 0 (First 16 elements) + int16x8_t v1_0 = vld1q_s16(pVect1 + i); + int16x8_t v2_0 = vld1q_s16(pVect2 + i); + int32x4_t prod0_lo = vmull_s16(vget_low_s16(v1_0), vget_low_s16(v2_0)); + int32x4_t prod0_hi = vmull_s16(vget_high_s16(v1_0), vget_high_s16(v2_0)); + sum_vec0 = vpadalq_s32(sum_vec0, prod0_lo); + sum_vec0 = vpadalq_s32(sum_vec0, prod0_hi); + + int16x8_t v1_1 = vld1q_s16(pVect1 + i + 8); + int16x8_t v2_1 = vld1q_s16(pVect2 + i + 8); + int32x4_t prod1_lo = vmull_s16(vget_low_s16(v1_1), vget_low_s16(v2_1)); + int32x4_t prod1_hi = vmull_s16(vget_high_s16(v1_1), vget_high_s16(v2_1)); + sum_vec1 = vpadalq_s32(sum_vec1, prod1_lo); + sum_vec1 = vpadalq_s32(sum_vec1, prod1_hi); + + // Block 1 (Next 16 elements) + int16x8_t v1_2 = vld1q_s16(pVect1 + i + 16); + int16x8_t v2_2 = vld1q_s16(pVect2 + i + 16); + int32x4_t prod2_lo = vmull_s16(vget_low_s16(v1_2), vget_low_s16(v2_2)); + int32x4_t prod2_hi = vmull_s16(vget_high_s16(v1_2), vget_high_s16(v2_2)); + sum_vec2 = vpadalq_s32(sum_vec2, prod2_lo); + sum_vec2 = vpadalq_s32(sum_vec2, prod2_hi); + + int16x8_t v1_3 = vld1q_s16(pVect1 + i + 24); + int16x8_t v2_3 = vld1q_s16(pVect2 + i + 24); + int32x4_t prod3_lo = vmull_s16(vget_low_s16(v1_3), vget_low_s16(v2_3)); + int32x4_t prod3_hi = vmull_s16(vget_high_s16(v1_3), vget_high_s16(v2_3)); + sum_vec3 = vpadalq_s32(sum_vec3, prod3_lo); + sum_vec3 = vpadalq_s32(sum_vec3, prod3_hi); + } size_t qty16 = qty / 16; for(; i < qty16 * 16; i += 16) { @@ -769,13 +893,11 @@ namespace ndd { int16x8_t v1_1 = vld1q_s16(pVect1 + i + 8); int16x8_t v2_1 = vld1q_s16(pVect2 + i + 8); - // Multiply -> 32-bit int32x4_t prod0_lo = vmull_s16(vget_low_s16(v1_0), vget_low_s16(v2_0)); int32x4_t prod0_hi = vmull_s16(vget_high_s16(v1_0), vget_high_s16(v2_0)); int32x4_t prod1_lo = vmull_s16(vget_low_s16(v1_1), vget_low_s16(v2_1)); int32x4_t prod1_hi = vmull_s16(vget_high_s16(v1_1), vget_high_s16(v2_1)); - // Accumulate into 64-bit sum_vec0 = vpadalq_s32(sum_vec0, prod0_lo); sum_vec0 = vpadalq_s32(sum_vec0, prod0_hi); sum_vec1 = vpadalq_s32(sum_vec1, prod1_lo); @@ -783,6 +905,8 @@ namespace ndd { } sum_vec0 = vaddq_s64(sum_vec0, sum_vec1); + sum_vec2 = vaddq_s64(sum_vec2, sum_vec3); + sum_vec0 = vaddq_s64(sum_vec0, sum_vec2); sum = vgetq_lane_s64(sum_vec0, 0) + vgetq_lane_s64(sum_vec0, 1); #elif defined(USE_SVE2) uint64_t num_elements = svcnth(); @@ -854,7 +978,7 @@ namespace ndd { sum += static_cast(pVect1[i]) * static_cast(pVect2[i]); } - return static_cast(sum) * scale1 * scale2; + return (static_cast(sum) * scale1) * scale2; } static float @@ -875,12 +999,12 @@ namespace ndd { static std::vector quantize_to_int8(const void* in, size_t dim) { const int16_t* in_data = static_cast(in); // Get scale - float inv_scale = extract_inv_scale(reinterpret_cast(in), dim); + float scale = extract_scale(reinterpret_cast(in), dim); - // Target: value = stored_8 * new_inv_scale + // Target: value = stored_8 * new_scale // We set: stored_8 = stored_16 / 256 - // new_inv_scale = inv_scale * 256.0f - float new_inv_scale = inv_scale * 256.0f; + // new_scale = scale * 256.0f + float new_scale = scale * 256.0f; size_t out_size = dim * sizeof(int8_t) + sizeof(float); std::vector out_vec(out_size); @@ -923,7 +1047,7 @@ namespace ndd { for(; i < dim; ++i) { out_data[i] = static_cast(in_data[i] >> 8); } - std::memcpy(out_data + dim, &new_inv_scale, sizeof(float)); + std::memcpy(out_data + dim, &new_scale, sizeof(float)); return out_vec; } @@ -946,7 +1070,7 @@ namespace ndd { d.dequantize = &int16d::dequantize; d.quantize_to_int8 = &int16d::quantize_to_int8; d.get_storage_size = &int16d::get_storage_size; - d.extract_inv_scale = &int16d::extract_inv_scale; + d.extract_scale = &int16d::extract_scale; return d; } }; diff --git a/src/quant/int8d.hpp b/src/quant/int8d.hpp index 5548a28c58..effb42545f 100644 --- a/src/quant/int8d.hpp +++ b/src/quant/int8d.hpp @@ -16,7 +16,7 @@ namespace ndd { return dimension * sizeof(int8_t) + sizeof(float); } - inline float extract_inv_scale(const uint8_t* buffer, size_t dimension) { + inline float extract_scale(const uint8_t* buffer, size_t dimension) { return *reinterpret_cast(buffer + dimension * sizeof(int8_t)); } @@ -36,19 +36,20 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; // Avoid division by zero } - float scale = INT8_SCALE / abs_max; + float scale = abs_max / INT8_SCALE; + float inv_scale = 1.0f / scale; // Quantize data int8_t* data_ptr = reinterpret_cast(buffer.data()); for(size_t i = 0; i < dimension; ++i) { - float scaled = input[i] * scale; + float scaled = input[i] * inv_scale; data_ptr[i] = static_cast(std::round(scaled)); } - // Store INVERSE scale for dequantization (abs_max/FP8_SCALE) + // Store scale for dequantization float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int8_t))); - *scale_ptr = abs_max / INT8_SCALE; + *scale_ptr = scale; return buffer; } @@ -69,11 +70,12 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; } - float scale = INT8_SCALE / abs_max; + float scale = abs_max / INT8_SCALE; + float inv_scale = 1.0f / scale; // SIMD quantization - using more registers for better parallelism int8_t* data_ptr = reinterpret_cast(buffer.data()); - const __m512 scale_vec = _mm512_set1_ps(scale); + const __m512 scale_vec = _mm512_set1_ps(inv_scale); size_t i = 0; size_t vec_size = @@ -125,14 +127,14 @@ namespace ndd { // Handle remaining elements for(; i < dimension; ++i) { - float scaled = input[i] * scale; + float scaled = input[i] * inv_scale; data_ptr[i] = static_cast(std::round(scaled)); } - // Store INVERSE scale for dequantization (abs_max/FP8_SCALE) + // Store scale for dequantization float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int8_t))); - *scale_ptr = abs_max / INT8_SCALE; + *scale_ptr = scale; return buffer; } @@ -155,11 +157,12 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; } - float scale = INT8_SCALE / abs_max; + float scale = abs_max / INT8_SCALE; + float inv_scale = 1.0f / scale; // SIMD quantization - using more registers for better parallelism int8_t* data_ptr = reinterpret_cast(buffer.data()); - const float32x4_t scale_vec = vdupq_n_f32(scale); + const float32x4_t scale_vec = vdupq_n_f32(inv_scale); size_t i = 0; size_t vec_size = @@ -242,14 +245,14 @@ namespace ndd { // Handle remaining elements for(; i < dimension; ++i) { - float scaled = input[i] * scale; + float scaled = input[i] * inv_scale; data_ptr[i] = static_cast(std::round(scaled)); } - // Store INVERSE scale for dequantization (abs_max/FP8_SCALE) + // Store scale for dequantization float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int8_t))); - *scale_ptr = abs_max / INT8_SCALE; + *scale_ptr = scale; return buffer; } @@ -270,7 +273,8 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; } - float scale = INT8_SCALE / abs_max; + float scale = abs_max / INT8_SCALE; + float inv_scale = 1.0f / scale; int8_t* data_ptr = reinterpret_cast(buffer.data()); @@ -279,7 +283,7 @@ namespace ndd { while(svptest_any(svptrue_b32(), pg)) { svfloat32_t vec = svld1_f32(pg, &input[i]); - vec = svmul_f32_x(pg, vec, svdup_f32(scale)); + vec = svmul_f32_x(pg, vec, svdup_f32(inv_scale)); // Round to nearest integer, ties away from zero (matches std::round/vcvta) vec = svrinta_f32_x(pg, vec); @@ -301,7 +305,7 @@ namespace ndd { float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int8_t))); - *scale_ptr = abs_max / INT8_SCALE; + *scale_ptr = scale; return buffer; } @@ -343,10 +347,11 @@ namespace ndd { if(abs_max == 0.0f) { abs_max = 1.0f; } - float scale = INT8_SCALE / abs_max; + float scale = abs_max / INT8_SCALE; + float inv_scale = 1.0f / scale; int8_t* data_ptr = reinterpret_cast(buffer.data()); - __m256 scale_vec = _mm256_set1_ps(scale); + __m256 scale_vec = _mm256_set1_ps(inv_scale); i = 0; for(; i + 32 <= dimension; i += 32) { @@ -378,13 +383,13 @@ namespace ndd { } for(; i < dimension; ++i) { - float scaled = input[i] * scale; + float scaled = input[i] * inv_scale; data_ptr[i] = static_cast(std::round(scaled)); } float* scale_ptr = reinterpret_cast(buffer.data() + (dimension * sizeof(int8_t))); - *scale_ptr = abs_max / INT8_SCALE; + *scale_ptr = scale; return buffer; } @@ -412,9 +417,9 @@ namespace ndd { size_t dimension) { std::vector output(dimension); const int8_t* data_ptr = reinterpret_cast(buffer); - float scale = extract_inv_scale(buffer, dimension); + float scale = extract_scale(buffer, dimension); - const __m512 inv_scale_vec = _mm512_set1_ps(1.0f / scale); + const __m512 scale_vec = _mm512_set1_ps(scale); size_t i = 0; size_t vec_size = @@ -440,11 +445,11 @@ namespace ndd { __m512 float_vec2 = _mm512_cvtepi32_ps(int32_vec2); __m512 float_vec3 = _mm512_cvtepi32_ps(int32_vec3); - // Apply inverse scale - float_vec0 = _mm512_mul_ps(float_vec0, inv_scale_vec); - float_vec1 = _mm512_mul_ps(float_vec1, inv_scale_vec); - float_vec2 = _mm512_mul_ps(float_vec2, inv_scale_vec); - float_vec3 = _mm512_mul_ps(float_vec3, inv_scale_vec); + // Apply scale + float_vec0 = _mm512_mul_ps(float_vec0, scale_vec); + float_vec1 = _mm512_mul_ps(float_vec1, scale_vec); + float_vec2 = _mm512_mul_ps(float_vec2, scale_vec); + float_vec3 = _mm512_mul_ps(float_vec3, scale_vec); // Store results _mm512_storeu_ps(&output[i], float_vec0); @@ -459,13 +464,13 @@ namespace ndd { __m128i int8_vec = _mm_loadu_si128((__m128i*)&data_ptr[i]); __m512i int32_vec = _mm512_cvtepi8_epi32(int8_vec); __m512 float_vec = _mm512_cvtepi32_ps(int32_vec); - float_vec = _mm512_mul_ps(float_vec, inv_scale_vec); + float_vec = _mm512_mul_ps(float_vec, scale_vec); _mm512_storeu_ps(&output[i], float_vec); } // Handle remaining elements for(; i < dimension; ++i) { - output[i] = static_cast(data_ptr[i]) / scale; + output[i] = static_cast(data_ptr[i]) * scale; } return output; @@ -477,9 +482,9 @@ namespace ndd { size_t dimension) { std::vector output(dimension); const int8_t* data_ptr = reinterpret_cast(buffer); - float scale = extract_inv_scale(buffer, dimension); + float scale = extract_scale(buffer, dimension); - const float32x4_t inv_scale_vec = vdupq_n_f32(1.0f / scale); + const float32x4_t scale_vec = vdupq_n_f32(scale); size_t i = 0; size_t vec_size = @@ -505,11 +510,11 @@ namespace ndd { float32x4_t float_2 = vcvtq_f32_s32(int32_2); float32x4_t float_3 = vcvtq_f32_s32(int32_3); - // Apply inverse scale - float_0 = vmulq_f32(float_0, inv_scale_vec); - float_1 = vmulq_f32(float_1, inv_scale_vec); - float_2 = vmulq_f32(float_2, inv_scale_vec); - float_3 = vmulq_f32(float_3, inv_scale_vec); + // Apply scale + float_0 = vmulq_f32(float_0, scale_vec); + float_1 = vmulq_f32(float_1, scale_vec); + float_2 = vmulq_f32(float_2, scale_vec); + float_3 = vmulq_f32(float_3, scale_vec); // Store results vst1q_f32(&output[i], float_0); @@ -520,7 +525,7 @@ namespace ndd { // Handle remaining elements for(; i < dimension; ++i) { - output[i] = static_cast(data_ptr[i]) / scale; + output[i] = static_cast(data_ptr[i]) * scale; } return output; @@ -532,7 +537,7 @@ namespace ndd { size_t dimension) { std::vector output(dimension); const int8_t* data_ptr = reinterpret_cast(buffer); - float scale = extract_inv_scale(buffer, dimension); + float scale = extract_scale(buffer, dimension); size_t i = 0; svbool_t pg = svwhilelt_b32(i, dimension); @@ -540,7 +545,7 @@ namespace ndd { while(svptest_any(svptrue_b32(), pg)) { svint32_t int_vec = svld1sb_s32(pg, &data_ptr[i]); svfloat32_t float_vec = svcvt_f32_s32_x(pg, int_vec); - float_vec = svmul_f32_x(pg, float_vec, svdup_f32(1.0f / scale)); + float_vec = svmul_f32_x(pg, float_vec, svdup_f32(scale)); svst1_f32(pg, &output[i], float_vec); i += svcntw(); @@ -563,10 +568,10 @@ namespace ndd { // Scalar fallback std::vector output(dimension); const int8_t* data_ptr = reinterpret_cast(buffer); - float scale = extract_inv_scale(buffer, dimension); + float scale = extract_scale(buffer, dimension); for(size_t i = 0; i < dimension; ++i) { - output[i] = static_cast(data_ptr[i]) / scale; + output[i] = static_cast(data_ptr[i]) * scale; } return output; #endif @@ -586,8 +591,8 @@ namespace ndd { const auto* params = static_cast(qty_ptr); size_t qty = params->dim; - float scale1 = extract_inv_scale((const uint8_t*)pVect1, qty); - float scale2 = extract_inv_scale((const uint8_t*)pVect2, qty); + float scale1 = extract_scale((const uint8_t*)pVect1, qty); + float scale2 = extract_scale((const uint8_t*)pVect2, qty); float res = 0; size_t i = 0; @@ -643,43 +648,88 @@ namespace ndd { sum_lo = _mm_hadd_ps(sum_lo, sum_lo); res = _mm_cvtss_f32(sum_lo); #elif defined(USE_SVE2) - svbool_t pg = svwhilelt_b32(i, qty); - svfloat32_t sum = svdup_f32(0.0f); - svfloat32_t v_scale1 = svdup_f32(scale1); - svfloat32_t v_scale2 = svdup_f32(scale2); - - while(svptest_any(svptrue_b32(), pg)) { - // Load int8 as int32 - svint32_t v1_i32 = svld1sb_s32(pg, pVect1 + i); - svint32_t v2_i32 = svld1sb_s32(pg, pVect2 + i); + svint32_t sum_sq1 = svdup_s32(0); + svint32_t sum_sq2 = svdup_s32(0); + svint32_t sum_prod = svdup_s32(0); + + // Use svdot_s32 for efficient processing (3 streams) + uint64_t num_bytes = svcntb(); + size_t unroll_stride = num_bytes * 2; + svbool_t pg_all = svptrue_b8(); + + for(; i + unroll_stride <= qty; i += unroll_stride) { + svint8_t v1_0 = svld1_s8(pg_all, pVect1 + i); + svint8_t v2_0 = svld1_s8(pg_all, pVect2 + i); + sum_sq1 = svdot_s32(sum_sq1, v1_0, v1_0); + sum_sq2 = svdot_s32(sum_sq2, v2_0, v2_0); + sum_prod = svdot_s32(sum_prod, v1_0, v2_0); + + svint8_t v1_1 = svld1_s8(pg_all, pVect1 + i + num_bytes); + svint8_t v2_1 = svld1_s8(pg_all, pVect2 + i + num_bytes); + sum_sq1 = svdot_s32(sum_sq1, v1_1, v1_1); + sum_sq2 = svdot_s32(sum_sq2, v2_1, v2_1); + sum_prod = svdot_s32(sum_prod, v1_1, v2_1); + } + + // Handle remaining elements (use svdot if possible, or fallback loop) + svbool_t pg8 = svwhilelt_b8(i, qty); + while(svptest_any(svptrue_b8(), pg8)) { + svint8_t v1 = svld1_s8(pg8, pVect1 + i); + svint8_t v2 = svld1_s8(pg8, pVect2 + i); + sum_sq1 = svdot_s32(sum_sq1, v1, v1); + sum_sq2 = svdot_s32(sum_sq2, v2, v2); + sum_prod = svdot_s32(sum_prod, v1, v2); - svfloat32_t v1_f = svcvt_f32_s32_x(pg, v1_i32); - svfloat32_t v2_f = svcvt_f32_s32_x(pg, v2_i32); + i += svcntb(); + pg8 = svwhilelt_b8(i, qty); + } - v1_f = svmul_f32_x(pg, v1_f, v_scale1); - v2_f = svmul_f32_x(pg, v2_f, v_scale2); + float dot1 = static_cast(svaddv_s32(svptrue_b32(), sum_sq1)); + float dot2 = static_cast(svaddv_s32(svptrue_b32(), sum_sq2)); + float dot_prod = static_cast(svaddv_s32(svptrue_b32(), sum_prod)); - svfloat32_t diff = svsub_f32_x(pg, v1_f, v2_f); - sum = svmla_f32_x(pg, sum, diff, diff); + res = (dot1 * scale1) * scale1 + (dot2 * scale2) * scale2 - + 2.0f * ((dot_prod * scale1) * scale2); - i += svcntw(); - pg = svwhilelt_b32(i, qty); - } - res = svaddv_f32(svptrue_b32(), sum); #elif defined(USE_NEON) // NEON implementation for L2Sqr // Uses the expansion: (a*s1 - b*s2)^2 = a^2*s1^2 + b^2*s2^2 - 2ab*s1*s2 // This allows using integer dot products for the terms. - float s1_sq = scale1 * scale1; - float s2_sq = scale2 * scale2; - float s1_s2_2 = 2.0f * scale1 * scale2; int32x4_t sum_sq1 = vdupq_n_s32(0); int32x4_t sum_sq2 = vdupq_n_s32(0); int32x4_t sum_prod = vdupq_n_s32(0); # if defined(__ARM_FEATURE_DOTPROD) + size_t qty64 = qty / 64; + for(; i < qty64 * 64; i += 64) { + int8x16_t v1_0 = vld1q_s8(pVect1 + i); + int8x16_t v2_0 = vld1q_s8(pVect2 + i); + int8x16_t v1_1 = vld1q_s8(pVect1 + i + 16); + int8x16_t v2_1 = vld1q_s8(pVect2 + i + 16); + int8x16_t v1_2 = vld1q_s8(pVect1 + i + 32); + int8x16_t v2_2 = vld1q_s8(pVect2 + i + 32); + int8x16_t v1_3 = vld1q_s8(pVect1 + i + 48); + int8x16_t v2_3 = vld1q_s8(pVect2 + i + 48); + + sum_sq1 = vdotq_s32(sum_sq1, v1_0, v1_0); + sum_sq2 = vdotq_s32(sum_sq2, v2_0, v2_0); + sum_prod = vdotq_s32(sum_prod, v1_0, v2_0); + + sum_sq1 = vdotq_s32(sum_sq1, v1_1, v1_1); + sum_sq2 = vdotq_s32(sum_sq2, v2_1, v2_1); + sum_prod = vdotq_s32(sum_prod, v1_1, v2_1); + + sum_sq1 = vdotq_s32(sum_sq1, v1_2, v1_2); + sum_sq2 = vdotq_s32(sum_sq2, v2_2, v2_2); + sum_prod = vdotq_s32(sum_prod, v1_2, v2_2); + + sum_sq1 = vdotq_s32(sum_sq1, v1_3, v1_3); + sum_sq2 = vdotq_s32(sum_sq2, v2_3, v2_3); + sum_prod = vdotq_s32(sum_prod, v1_3, v2_3); + } + size_t qty16 = qty / 16; for(; i < qty16 * 16; i += 16) { int8x16_t v1 = vld1q_s8(pVect1 + i); @@ -689,48 +739,14 @@ namespace ndd { sum_sq2 = vdotq_s32(sum_sq2, v2, v2); sum_prod = vdotq_s32(sum_prod, v1, v2); } -# else - size_t qty16 = qty / 16; - for(; i < qty16 * 16; i += 16) { - int8x16_t v1 = vld1q_s8(pVect1 + i); - int8x16_t v2 = vld1q_s8(pVect2 + i); - - int16x8_t v1_lo = vmovl_s8(vget_low_s8(v1)); - int16x8_t v1_hi = vmovl_s8(vget_high_s8(v1)); - int16x8_t v2_lo = vmovl_s8(vget_low_s8(v2)); - int16x8_t v2_hi = vmovl_s8(vget_high_s8(v2)); - - // v1^2 - int32x4_t sq1_lo = vmull_s16(vget_low_s16(v1_lo), vget_low_s16(v1_lo)); - int32x4_t sq1_hi = vmull_s16(vget_high_s16(v1_lo), vget_high_s16(v1_lo)); - sum_sq1 = vaddq_s32(sum_sq1, vaddq_s32(sq1_lo, sq1_hi)); - sq1_lo = vmull_s16(vget_low_s16(v1_hi), vget_low_s16(v1_hi)); - sq1_hi = vmull_s16(vget_high_s16(v1_hi), vget_high_s16(v1_hi)); - sum_sq1 = vaddq_s32(sum_sq1, vaddq_s32(sq1_lo, sq1_hi)); - - // v2^2 - int32x4_t sq2_lo = vmull_s16(vget_low_s16(v2_lo), vget_low_s16(v2_lo)); - int32x4_t sq2_hi = vmull_s16(vget_high_s16(v2_lo), vget_high_s16(v2_lo)); - sum_sq2 = vaddq_s32(sum_sq2, vaddq_s32(sq2_lo, sq2_hi)); - sq2_lo = vmull_s16(vget_low_s16(v2_hi), vget_low_s16(v2_hi)); - sq2_hi = vmull_s16(vget_high_s16(v2_hi), vget_high_s16(v2_hi)); - sum_sq2 = vaddq_s32(sum_sq2, vaddq_s32(sq2_lo, sq2_hi)); - - // v1*v2 - int32x4_t prod_lo = vmull_s16(vget_low_s16(v1_lo), vget_low_s16(v2_lo)); - int32x4_t prod_hi = vmull_s16(vget_high_s16(v1_lo), vget_high_s16(v2_lo)); - sum_prod = vaddq_s32(sum_prod, vaddq_s32(prod_lo, prod_hi)); - prod_lo = vmull_s16(vget_low_s16(v1_hi), vget_low_s16(v2_hi)); - prod_hi = vmull_s16(vget_high_s16(v1_hi), vget_high_s16(v2_hi)); - sum_prod = vaddq_s32(sum_prod, vaddq_s32(prod_lo, prod_hi)); - } # endif float dot1 = static_cast(vaddvq_s32(sum_sq1)); float dot2 = static_cast(vaddvq_s32(sum_sq2)); float dot_prod = static_cast(vaddvq_s32(sum_prod)); - res = dot1 * s1_sq + dot2 * s2_sq - dot_prod * s1_s2_2; + res = (dot1 * scale1) * scale1 + (dot2 * scale2) * scale2 - + 2.0f * ((dot_prod * scale1) * scale2); #endif for(; i < qty; i++) { @@ -753,8 +769,8 @@ namespace ndd { const auto* params = static_cast(qty_ptr); size_t qty = params->dim; - float scale1 = extract_inv_scale((const uint8_t*)pVect1, qty); - float scale2 = extract_inv_scale((const uint8_t*)pVect2, qty); + float scale1 = extract_scale((const uint8_t*)pVect1, qty); + float scale2 = extract_scale((const uint8_t*)pVect2, qty); int32_t sum = 0; size_t i = 0; @@ -794,26 +810,53 @@ namespace ndd { sum_128 = _mm_hadd_epi32(sum_128, sum_128); sum = _mm_cvtsi128_si32(sum_128); #elif defined(USE_SVE2) - svbool_t pg8 = svwhilelt_b8(i, qty); - svint32_t v_sum = svdup_s32(0); + uint64_t num_bytes = svcntb(); + size_t unroll_stride = num_bytes * 4; + svbool_t pg_all = svptrue_b8(); + + svint32_t sum0 = svdup_s32(0); + svint32_t sum1 = svdup_s32(0); + svint32_t sum2 = svdup_s32(0); + svint32_t sum3 = svdup_s32(0); + + for(; i + unroll_stride <= qty; i += unroll_stride) { + svint8_t v1_0 = svld1_s8(pg_all, pVect1 + i); + svint8_t v2_0 = svld1_s8(pg_all, pVect2 + i); + sum0 = svdot_s32(sum0, v1_0, v2_0); + + svint8_t v1_1 = svld1_s8(pg_all, pVect1 + i + num_bytes); + svint8_t v2_1 = svld1_s8(pg_all, pVect2 + i + num_bytes); + sum1 = svdot_s32(sum1, v1_1, v2_1); + + svint8_t v1_2 = svld1_s8(pg_all, pVect1 + i + 2 * num_bytes); + svint8_t v2_2 = svld1_s8(pg_all, pVect2 + i + 2 * num_bytes); + sum2 = svdot_s32(sum2, v1_2, v2_2); + svint8_t v1_3 = svld1_s8(pg_all, pVect1 + i + 3 * num_bytes); + svint8_t v2_3 = svld1_s8(pg_all, pVect2 + i + 3 * num_bytes); + sum3 = svdot_s32(sum3, v1_3, v2_3); + } + + sum0 = svadd_s32_x(svptrue_b32(), sum0, sum1); + sum2 = svadd_s32_x(svptrue_b32(), sum2, sum3); + sum0 = svadd_s32_x(svptrue_b32(), sum0, sum2); + + svbool_t pg8 = svwhilelt_b8(i, qty); while(svptest_any(svptrue_b8(), pg8)) { svint8_t v1 = svld1_s8(pg8, pVect1 + i); svint8_t v2 = svld1_s8(pg8, pVect2 + i); - v_sum = svdot_s32(v_sum, v1, v2); + sum0 = svdot_s32(sum0, v1, v2); i += svcntb(); pg8 = svwhilelt_b8(i, qty); } - sum = svaddv_s32(svptrue_b32(), v_sum); + sum = svaddv_s32(svptrue_b32(), sum0); #elif defined(USE_NEON) int32x4_t sum_vec0 = vdupq_n_s32(0); int32x4_t sum_vec1 = vdupq_n_s32(0); int32x4_t sum_vec2 = vdupq_n_s32(0); int32x4_t sum_vec3 = vdupq_n_s32(0); -// Use dot product instructions if available (ARMv8.2+) -# if defined(__ARM_FEATURE_DOTPROD) size_t qty64 = qty / 64; for(; i < qty64 * 64; i += 64) { int8x16_t v1_0 = vld1q_s8(pVect1 + i); @@ -837,22 +880,6 @@ namespace ndd { int8x16_t v2 = vld1q_s8(pVect2 + i); sum_vec0 = vdotq_s32(sum_vec0, v1, v2); } -# else - // Fallback for older NEON - size_t qty16 = qty / 16; - for(; i < qty16 * 16; i += 16) { - int8x16_t v1 = vld1q_s8(pVect1 + i); - int8x16_t v2 = vld1q_s8(pVect2 + i); - - int16x8_t prod_low = vmull_s8(vget_low_s8(v1), vget_low_s8(v2)); - int16x8_t prod_high = vmull_s8(vget_high_s8(v1), vget_high_s8(v2)); - - int32x4_t sum_low = vpaddlq_s16(prod_low); - int32x4_t sum_high = vpaddlq_s16(prod_high); - - sum_vec0 = vaddq_s32(sum_vec0, vaddq_s32(sum_low, sum_high)); - } -# endif sum_vec0 = vaddq_s32(sum_vec0, sum_vec1); sum_vec2 = vaddq_s32(sum_vec2, sum_vec3); @@ -865,7 +892,7 @@ namespace ndd { sum += static_cast(pVect1[i]) * static_cast(pVect2[i]); } - return static_cast(sum) * scale1 * scale2; + return (static_cast(sum) * scale1) * scale2; } static float @@ -910,7 +937,7 @@ namespace ndd { d.dequantize = &int8d::dequantize; d.quantize_to_int8 = &int8d::quantize_to_int8_identity; d.get_storage_size = &int8d::get_storage_size; - d.extract_inv_scale = &int8d::extract_inv_scale; + d.extract_scale = &int8d::extract_scale; return d; } }; From d5dcf6fa4c957f6809c94af1aec81d3d05f63817 Mon Sep 17 00:00:00 2001 From: Vineet Dwivedi Date: Fri, 6 Feb 2026 22:34:01 +0530 Subject: [PATCH 14/48] clean of DISABLE_HYBRID_QUANTIZATION --- src/hnsw/hnswalg.h | 369 +++++++++++++-------------------------------- 1 file changed, 109 insertions(+), 260 deletions(-) diff --git a/src/hnsw/hnswalg.h b/src/hnsw/hnswalg.h index c7017e7075..bd441e4cb1 100644 --- a/src/hnsw/hnswalg.h +++ b/src/hnsw/hnswalg.h @@ -87,16 +87,26 @@ namespace hnswlib { << data_size_ << ", dimension: " << dimension_ << ", quant_level: " << static_cast(quant_level_)); -#ifndef DISABLE_HYBRID_QUANTIZATION - // Create INT8 space for upper layers - space_int8_ = std::unique_ptr>(createSpace( - space_type_, dimension_, ndd::quant::QuantizationLevel::INT8)); - data_size_int8_ = space_int8_->get_data_size(); - fstSimFuncInt8_ = space_int8_->get_sim_func(); - dist_func_param_int8_ = space_int8_->get_dist_func_param(); - LOG_DEBUG("Space also initialized (Hybrid Quantization) with data size: " - << data_size_int8_); -#endif //DISABLE_HYBRID_QUANTIZATION + // Initialize upper layer space + bool use_hybrid = true; + if(quant_level_ == ndd::quant::QuantizationLevel::BINARY) { + use_hybrid = false; + } + + if(use_hybrid) { + space_upper_ = std::unique_ptr>(createSpace( + space_type_, dimension_, ndd::quant::QuantizationLevel::INT8)); + LOG_DEBUG("Upper layer initialized with Hybrid Quantization (INT8)"); + } else { + space_upper_ = std::unique_ptr>( + createSpace(space_type_, dimension_, quant_level_)); + LOG_DEBUG("Upper layer initialized with same space as base layer"); + } + + data_size_upper_ = space_upper_->get_data_size(); + fstSimFuncUpper_ = space_upper_->get_sim_func(); + dist_func_param_upper_ = space_upper_->get_dist_func_param(); + LOG_DEBUG("Upper layer data size: " << data_size_upper_); // M_ cannot be more than settings::MAX_M if(M_ > settings::MAX_M) { @@ -174,24 +184,24 @@ namespace hnswlib { // Approximate level > 0 count size_t upper_layer_estimate = maxElements_ / M_; -#ifdef DISABLE_HYBRID_QUANTIZATION - // Typical Quantization - size += upper_layer_estimate * (data_size_ + sizeof(levelInt) + sizeLinksUpperLayers_); -#else - // int 8 Quantization for upper layers + // Upper layer calculation using runtime data size size += upper_layer_estimate - * (data_size_int8_ + sizeof(levelInt) + sizeLinksUpperLayers_); -#endif //DISABLE_HYBRID_QUANTIZATION + * (data_size_upper_ + sizeof(levelInt) + sizeLinksUpperLayers_); return size / GB; // GB } - // Helper to quantize to INT8 (check DISABLE_HYBRID_QUANTIZATION) - std::vector quantizeToInt8(const void* datapoint) { - // Use direct conversion from current level to INT8 - // This avoids double allocation and conversion (Base -> Float -> Int8) + // Helper to get data representation for upper layers + std::vector getUpperLayerRepresentation(const void* datapoint) { + if(data_size_upper_ == data_size_) { + // If sizes match, just copy (No hybrid quantization or Same Space) + std::vector res(data_size_); + memcpy(res.data(), datapoint, data_size_); + return res; + } + + // Hybrid quantization enabled (INT8) auto dispatch = ndd::quant::get_quantizer_dispatch(quant_level_); - // datapoint is the raw stored format for the base layer return dispatch.quantize_to_int8(datapoint, dimension_); } @@ -213,18 +223,11 @@ namespace hnswlib { idhInt currObj = entryPoint_; dist_t curSim; -#ifdef DISABLE_HYBRID_QUANTIZATION - std::vector ep_vector(data_size_); - if(!getDataByInternalId(currObj, maxLevel_, ep_vector.data())) { - return result; - } - curSim = fstSimFunc_(query_data, ep_vector.data(), dist_func_param_); - std::vector candidate_vector(data_size_); -#else - // Only convert to INT8 if we have upper layers to traverse - std::vector query_data_int8; + // Prepare query data for upper layers + std::vector query_data_upper; if(maxLevel_ > 0) { - query_data_int8 = const_cast(this)->quantizeToInt8(query_data); + query_data_upper = + const_cast(this)->getUpperLayerRepresentation(query_data); // Use direct pointer for upper layers const uint8_t* ep_data = getUpperLayerDataPtr(currObj); @@ -232,9 +235,9 @@ namespace hnswlib { return result; } - curSim = fstSimFuncInt8_(query_data_int8.data(), ep_data, dist_func_param_int8_); + curSim = fstSimFuncUpper_( + query_data_upper.data(), ep_data, dist_func_param_upper_); } -#endif //DISABLE_HYBRID_QUANTIZATION dist_t s; // Upper layer traversal - greedy search @@ -258,19 +261,12 @@ namespace hnswlib { continue; } -#ifdef DISABLE_HYBRID_QUANTIZATION - if(!getDataByInternalId(candidate, level, candidate_vector.data())) { - continue; - } - s = fstSimFunc_(query_data, candidate_vector.data(), dist_func_param_); -#else const uint8_t* candidate_data = getUpperLayerDataPtr(candidate); if(!candidate_data) { continue; } - s = fstSimFuncInt8_( - query_data_int8.data(), candidate_data, dist_func_param_int8_); -#endif //DISABLE_HYBRID_QUANTIZATION + s = fstSimFuncUpper_( + query_data_upper.data(), candidate_data, dist_func_param_upper_); if(s > curSim) { curSim = s; @@ -343,14 +339,9 @@ namespace hnswlib { continue; } -#ifdef DISABLE_HYBRID_QUANTIZATION - level = *reinterpret_cast(dataUpperLayer_[i].get() + data_size_); - total_size = data_size_ + sizeof(levelInt) + level * sizeLinksUpperLayers_; -#else - // Use data_size_int8_ - level = *reinterpret_cast(dataUpperLayer_[i].get() + data_size_int8_); - total_size = data_size_int8_ + sizeof(levelInt) + level * sizeLinksUpperLayers_; -#endif //DISABLE_HYBRID_QUANTIZATION + // Use data_size_upper_ + level = *reinterpret_cast(dataUpperLayer_[i].get() + data_size_upper_); + total_size = data_size_upper_ + sizeof(levelInt) + level * sizeLinksUpperLayers_; writeBinaryPOD(output, static_cast(i)); // write ID output.write(reinterpret_cast(dataUpperLayer_[i].get()), @@ -419,14 +410,23 @@ namespace hnswlib { fstSimFunc_ = space_->get_sim_func(); dist_func_param_ = space_->get_dist_func_param(); -#ifndef DISABLE_HYBRID_QUANTIZATION - // Create INT8 space for upper layers - space_int8_ = std::unique_ptr>(createSpace( - space_type_, dimension_, ndd::quant::QuantizationLevel::INT8)); - data_size_int8_ = space_int8_->get_data_size(); - fstSimFuncInt8_ = space_int8_->get_sim_func(); - dist_func_param_int8_ = space_int8_->get_dist_func_param(); -#endif //DISABLE_HYBRID_QUANTIZATION + // Initialize upper layer space + bool use_hybrid = true; + if(quant_level_ == ndd::quant::QuantizationLevel::BINARY) { + use_hybrid = false; + } + + if(use_hybrid) { + space_upper_ = std::unique_ptr>(createSpace( + space_type_, dimension_, ndd::quant::QuantizationLevel::INT8)); + } else { + space_upper_ = std::unique_ptr>( + createSpace(space_type_, dimension_, quant_level_)); + } + + data_size_upper_ = space_upper_->get_data_size(); + fstSimFuncUpper_ = space_upper_->get_sim_func(); + dist_func_param_upper_ = space_upper_->get_dist_func_param(); // Allocate memory and load level 0 data dataBaseLayer_ = (char*)malloc(maxElements_ * sizeDataAtBaseLayer_); @@ -468,13 +468,8 @@ namespace hnswlib { } size_t header_size; -#ifdef DISABLE_HYBRID_QUANTIZATION - // Step 1: Read vector + level header (data_size_ + sizeof(levelInt)) - header_size = data_size_ + sizeof(levelInt); -#else - // Step 1: Read vector + level header (data_size_int8_ + sizeof(levelInt)) - header_size = data_size_int8_ + sizeof(levelInt); -#endif //DISABLE_HYBRID_QUANTIZATION + // Step 1: Read vector + level header + header_size = data_size_upper_ + sizeof(levelInt); std::vector header_buf(header_size); input.read(reinterpret_cast(header_buf.data()), header_size); @@ -483,11 +478,7 @@ namespace hnswlib { } levelInt level; -#ifdef DISABLE_HYBRID_QUANTIZATION - level = *reinterpret_cast(header_buf.data() + data_size_); -#else - level = *reinterpret_cast(header_buf.data() + data_size_int8_); -#endif //DISABLE_HYBRID_QUANTIZATION + level = *reinterpret_cast(header_buf.data() + data_size_upper_); size_t total_size = header_size + level * sizeLinksUpperLayers_; @@ -526,10 +517,8 @@ namespace hnswlib { template void addPoint(const void* datapoint, idInt label) { LOG_TIME("addPoint"); -#ifndef DISABLE_HYBRID_QUANTIZATION - // Generate INT8 representation for upper layers - std::vector datapoint_int8 = quantizeToInt8(datapoint); -#endif //DISABLE_HYBRID_QUANTIZATION + // Generate upper layer representation + std::vector datapoint_upper = getUpperLayerRepresentation(datapoint); //std::shared_lock lock(index_lock_); idhInt cur_c = 0; @@ -583,30 +572,17 @@ namespace hnswlib { size_t total_size; if(curLevel > 0) { -#ifdef DISABLE_HYBRID_QUANTIZATION - total_size = data_size_ + sizeof(levelInt) + curLevel * sizeLinksUpperLayers_; -#else - total_size = data_size_int8_ + sizeof(levelInt) + curLevel * sizeLinksUpperLayers_; -#endif //DISABLE_HYBRID_QUANTIZATION + total_size = data_size_upper_ + sizeof(levelInt) + curLevel * sizeLinksUpperLayers_; auto mem = std::make_unique(total_size); // copy vector -#ifdef DISABLE_HYBRID_QUANTIZATION - memcpy(mem.get(), datapoint, data_size_); - memcpy(mem.get() + data_size_, &curLevel, sizeof(levelInt)); - // zero initialize linklists - memset(mem.get() + data_size_ + sizeof(levelInt), - 0, - curLevel * sizeLinksUpperLayers_); -#else - memcpy(mem.get(), datapoint_int8.data(), data_size_int8_); - memcpy(mem.get() + data_size_int8_, &curLevel, sizeof(levelInt)); + memcpy(mem.get(), datapoint_upper.data(), data_size_upper_); + memcpy(mem.get() + data_size_upper_, &curLevel, sizeof(levelInt)); // zero initialize linklists - memset(mem.get() + data_size_int8_ + sizeof(levelInt), + memset(mem.get() + data_size_upper_ + sizeof(levelInt), 0, curLevel * sizeLinksUpperLayers_); -#endif //DISABLE_HYBRID_QUANTIZATION dataUpperLayer_[cur_c] = std::move(mem); } @@ -633,46 +609,25 @@ namespace hnswlib { int size = getListCount((idhInt*)ll_cur); idhInt* datal = (idhInt*)(ll_cur + 1); -#ifdef DISABLE_HYBRID_QUANTIZATION - std::vector curr_vec(data_size_); -#else - std::vector curr_vec(data_size_int8_); -#endif //DISABLE_HYBRID_QUANTIZATION - + std::vector curr_vec(data_size_upper_); if(!getDataByInternalId(currObj, level, curr_vec.data())) { continue; } - dist_t curr_sim; -#ifdef DISABLE_HYBRID_QUANTIZATION - curr_sim = fstSimFunc_(datapoint, curr_vec.data(), dist_func_param_); -#else - curr_sim = fstSimFuncInt8_( - datapoint_int8.data(), curr_vec.data(), dist_func_param_int8_); -#endif //DISABLE_HYBRID_QUANTIZATION + dist_t curr_sim = fstSimFuncUpper_(datapoint_upper.data(), + curr_vec.data(), + dist_func_param_upper_); for(int i = 0; i < size; i++) { idhInt candidate_id = datal[i]; - if(candidate_id >= curElementsCount_) { - continue; - } - dist_t s; -#ifdef DISABLE_HYBRID_QUANTIZATION - std::vector candidate_vec(data_size_); + std::vector candidate_vec(data_size_upper_); if(!getDataByInternalId(candidate_id, level, candidate_vec.data())) { continue; } - s = fstSimFunc_(datapoint, candidate_vec.data(), dist_func_param_); -#else - std::vector candidate_vec(data_size_int8_); - if(!getDataByInternalId(candidate_id, level, candidate_vec.data())) { - continue; - } - s = fstSimFuncInt8_(datapoint_int8.data(), - candidate_vec.data(), - dist_func_param_int8_); -#endif //DISABLE_HYBRID_QUANTIZATION + s = fstSimFuncUpper_(datapoint_upper.data(), + candidate_vec.data(), + dist_func_param_upper_); if(s > curr_sim) { curr_sim = s; @@ -682,21 +637,13 @@ namespace hnswlib { } } } - // Connect at all levels from curLevel down to 0 + + // Add connections from curLevel down to 0 for(int level = std::min(curLevel, maxlevelcopy); level >= 0; level--) { std::vector> sorted_candidates; - -#ifdef DISABLE_HYBRID_QUANTIZATION - if(deletedElementsCount_) { - sorted_candidates = searchBaseLayer( - currObj, datapoint, level, efConstruction_); - } else { // No deleted elements - sorted_candidates = searchBaseLayer( - currObj, datapoint, level, efConstruction_); - } - currObj = mutuallyConnectNewElement(datapoint, cur_c, sorted_candidates, level); -#else - const void* level_datapoint = (level == 0) ? datapoint : datapoint_int8.data(); + + const void* level_datapoint = (level == 0) ? datapoint : datapoint_upper.data(); + if(deletedElementsCount_) { sorted_candidates = searchBaseLayer( currObj, level_datapoint, level, efConstruction_); @@ -706,25 +653,12 @@ namespace hnswlib { } currObj = mutuallyConnectNewElement( level_datapoint, cur_c, sorted_candidates, level); -#endif //DISABLE_HYBRID_QUANTIZATION } - // If this element has a higher level than the current max, - // update the max level and entry point - if(has_higher_level) { - std::lock_guard glock(global); - maxLevel_ = curLevel; - entryPoint_ = cur_c; + if (has_higher_level) { + entryPoint_ = cur_c; + maxLevel_ = curLevel; } - } else { - // First element becomes enterpoint - entryPoint_ = 0; - maxLevel_ = curLevel; - // preload element in vector cache at its proper cache location - // if (curLevel == 0) { - // memcpy(dataBaseLayer_ + cur_c * sizeDataAtBaseLayer_ + sizeDataAtBaseLayer_ - - // data_size_, datapoint, data_size_); - // } } } @@ -791,9 +725,7 @@ namespace hnswlib { uint64_t flags_{0}; //Not using flags now. We can use it in future for various options std::unique_ptr> space_; -#ifndef DISABLE_HYBRID_QUANTIZATION - std::unique_ptr> space_int8_; -#endif //DISABLE_HYBRID_QUANTIZATION + std::unique_ptr> space_upper_; size_t dimension_; @@ -834,11 +766,10 @@ namespace hnswlib { SIMFUNC fstSimFunc_; void* dist_func_param_{nullptr}; -#ifndef DISABLE_HYBRID_QUANTIZATION - size_t data_size_int8_{0}; - SIMFUNC fstSimFuncInt8_; - void* dist_func_param_int8_{nullptr}; -#endif //DISABLE_HYBRID_QUANTIZATION + // Unified upper layer data parameters + size_t data_size_upper_{0}; + SIMFUNC fstSimFuncUpper_; + void* dist_func_param_upper_{nullptr}; // Maps external label to internal id std::vector labelLookup_; @@ -895,11 +826,7 @@ namespace hnswlib { if(dataUpperLayer_[internal_id] == nullptr) { return false; } -#ifdef DISABLE_HYBRID_QUANTIZATION - memcpy(buffer, dataUpperLayer_[internal_id].get(), data_size_); -#else - memcpy(buffer, dataUpperLayer_[internal_id].get(), data_size_int8_); -#endif //DISABLE_HYBRID_QUANTIZATION + memcpy(buffer, dataUpperLayer_[internal_id].get(), data_size_upper_); return true; } return false; @@ -916,13 +843,8 @@ namespace hnswlib { // int levels = getElementLevel(id); // if (level > levels) return nullptr; return reinterpret_cast( -#ifdef DISABLE_HYBRID_QUANTIZATION - dataUpperLayer_[id].get() + data_size_ + sizeof(levelInt) + dataUpperLayer_[id].get() + data_size_upper_ + sizeof(levelInt) + (level - 1) * sizeLinksUpperLayers_ -#else - dataUpperLayer_[id].get() + data_size_int8_ + sizeof(levelInt) - + (level - 1) * sizeLinksUpperLayers_ -#endif //DISABLE_HYBRID_QUANTIZATION ); } @@ -948,11 +870,7 @@ namespace hnswlib { if(!dataUpperLayer_[id]) { return 0; } -#ifdef DISABLE_HYBRID_QUANTIZATION - return *reinterpret_cast(dataUpperLayer_[id].get() + data_size_); -#else - return *reinterpret_cast(dataUpperLayer_[id].get() + data_size_int8_); -#endif //DISABLE_HYBRID_QUANTIZATION + return *reinterpret_cast(dataUpperLayer_[id].get() + data_size_upper_); } // This function is used to get the neighbors based on heuristic @@ -969,29 +887,19 @@ namespace hnswlib { std::vector> result; result.reserve(M); -#ifdef DISABLE_HYBRID_QUANTIZATION - std::vector cand_vec(data_size_); - std::vector selected_vec(data_size_); -#else - // INT8 awareness - auto curSimFunc = (level == 0) ? fstSimFunc_ : fstSimFuncInt8_; - auto curDistParam = (level == 0) ? dist_func_param_ : dist_func_param_int8_; - size_t curDataSize = (level == 0) ? data_size_ : data_size_int8_; + // Generic awareness + auto curSimFunc = (level == 0) ? fstSimFunc_ : fstSimFuncUpper_; + auto curDistParam = (level == 0) ? dist_func_param_ : dist_func_param_upper_; + size_t curDataSize = (level == 0) ? data_size_ : data_size_upper_; std::vector cand_buf(curDataSize); // Only used for level 0 std::vector selected_buf(curDataSize); // Only used for level 0 -#endif //DISABLE_HYBRID_QUANTIZATION for(const auto& candidate : candidates_sorted) { if(result.size() == M) { break; } -#ifdef DISABLE_HYBRID_QUANTIZATION - if(!getDataByInternalId(candidate.second, level, cand_vec.data())) { - continue; - } -#else const void* cand_vec = nullptr; if(level == 0) { if(getDataByInternalId(candidate.second, level, cand_buf.data())) { @@ -1004,18 +912,10 @@ namespace hnswlib { if(!cand_vec) { continue; } -#endif //DISABLE_HYBRID_QUANTIZATION bool good = true; dist_t sim; for(const auto& selected : result) { - -#ifdef DISABLE_HYBRID_QUANTIZATION - if(!getDataByInternalId(selected.second, level, selected_vec.data())) { - continue; - } - sim = fstSimFunc_(selected_vec.data(), cand_vec.data(), dist_func_param_); -#else const void* selected_vec_ptr = nullptr; if(level == 0) { if(getDataByInternalId(selected.second, level, selected_buf.data())) { @@ -1030,7 +930,6 @@ namespace hnswlib { } sim = curSimFunc(selected_vec_ptr, cand_vec, curDistParam); -#endif //DISABLE_HYBRID_QUANTIZATION if(sim > candidate.first) { good = false; @@ -1058,20 +957,14 @@ namespace hnswlib { size_t curMaxM = level ? maxM_ : maxM0_; size_t curM = level ? M_ : M0_; -#ifndef DISABLE_HYBRID_QUANTIZATION - // INT8 awareness - auto curSimFunc = (level == 0) ? fstSimFunc_ : fstSimFuncInt8_; - auto curDistParam = (level == 0) ? dist_func_param_ : dist_func_param_int8_; - size_t curDataSize = (level == 0) ? data_size_ : data_size_int8_; -#endif //DISABLE_HYBRID_QUANTIZATION + // Generic awareness + auto curSimFunc = (level == 0) ? fstSimFunc_ : fstSimFuncUpper_; + auto curDistParam = (level == 0) ? dist_func_param_ : dist_func_param_upper_; + size_t curDataSize = (level == 0) ? data_size_ : data_size_upper_; - // Step 1: Heuristic pruning - const auto& selected = getNeighborsByHeuristic2(sorted_candidates, curM, level); - - if(selected.empty()) { - // If no candidates selected, just return current entry point or invalid - // This can happen if the graph is empty or disconnected - return 0; // Or better handling + auto selected = getNeighborsByHeuristic2(sorted_candidates, curM, level); + if(selected.empty()) { // the graph is empty or disconnected + return 0; // Or better handling } idhInt next_closest_entry_point = selected[0].second; @@ -1090,13 +983,8 @@ namespace hnswlib { } // Step 3: Add cur_c to neighbors' lists -#ifdef DISABLE_HYBRID_QUANTIZATION - std::vector neighbor_vec(data_size_); - std::vector data_vec(data_size_); -#else std::vector neighbor_buf(curDataSize); // Used for level 0 std::vector data_buf(curDataSize); // Used for level 0 -#endif //DISABLE_HYBRID_QUANTIZATION for(const auto& p : selected) { idhInt neighbor = p.second; @@ -1118,11 +1006,6 @@ namespace hnswlib { data[sz] = cur_c; setListCount(ll_other, sz + 1); } else { -#ifdef DISABLE_HYBRID_QUANTIZATION - if(!getDataByInternalId(neighbor, level, neighbor_vec.data())) { - continue; - } -#else const void* neighbor_data = nullptr; if(level == 0) { if(getDataByInternalId(neighbor, level, neighbor_buf.data())) { @@ -1135,27 +1018,15 @@ namespace hnswlib { if(!neighbor_data) { continue; } -#endif //DISABLE_HYBRID_QUANTIZATION std::vector> all_candidates; all_candidates.reserve(sz + 1); -#ifdef DISABLE_HYBRID_QUANTIZATION - all_candidates.emplace_back( - fstSimFunc_(neighbor_vec.data(), data_point, dist_func_param_), cur_c); -#else all_candidates.emplace_back(curSimFunc(neighbor_data, data_point, curDistParam), cur_c); -#endif //DISABLE_HYBRID_QUANTIZATION for(size_t j = 0; j < sz; j++) { dist_t sim; -#ifdef DISABLE_HYBRID_QUANTIZATION - if(!getDataByInternalId(data[j], level, data_vec.data())) { - continue; - } - sim = fstSimFunc_(neighbor_vec.data(), data_vec.data(), dist_func_param_); -#else const void* other_neighbor_data = nullptr; if(level == 0) { if(getDataByInternalId(data[j], level, data_buf.data())) { @@ -1169,7 +1040,6 @@ namespace hnswlib { } sim = curSimFunc(neighbor_data, other_neighbor_data, curDistParam); -#endif //DISABLE_HYBRID_QUANTIZATION all_candidates.emplace_back(sim, data[j]); } @@ -1201,26 +1071,18 @@ namespace hnswlib { max_heap_pq candidate_set; min_heap_pq top_candidates; -#ifdef DISABLE_HYBRID_QUANTIZATION - std::vector buffer(data_size_); -#else - // INT8 awareness - auto curSimFunc = (layer == 0) ? fstSimFunc_ : fstSimFuncInt8_; - auto curDistParam = (layer == 0) ? dist_func_param_ : dist_func_param_int8_; - size_t curDataSize = (layer == 0) ? data_size_ : data_size_int8_; + // Generic awareness + auto curSimFunc = (layer == 0) ? fstSimFunc_ : fstSimFuncUpper_; + auto curDistParam = (layer == 0) ? dist_func_param_ : dist_func_param_upper_; + size_t curDataSize = (layer == 0) ? data_size_ : data_size_upper_; std::vector buffer; if(layer == 0) { buffer.resize(curDataSize); } -#endif //DISABLE_HYBRID_QUANTIZATION dist_t lowerBound; if(!has_deletions || !isMarkedDeleted(ep_id)) { -#ifdef DISABLE_HYBRID_QUANTIZATION - if(getDataByInternalId(ep_id, layer, buffer.data())) { - dist_t sim = fstSimFunc_(data_point, buffer.data(), dist_func_param_); -#else const void* vec_data = nullptr; if(layer == 0) { if(getDataByInternalId(ep_id, layer, buffer.data())) { @@ -1232,7 +1094,6 @@ namespace hnswlib { if(vec_data) { dist_t sim = curSimFunc(data_point, vec_data, curDistParam); -#endif //DISABLE_HYBRID_QUANTIZATION top_candidates.emplace(sim, ep_id); candidate_set.emplace(sim, ep_id); @@ -1246,6 +1107,7 @@ namespace hnswlib { lowerBound = std::numeric_limits::lowest(); candidate_set.emplace(lowerBound, ep_id); } + visited_array[ep_id] = visited_array_tag; int below_threshold_count = 0; int max_below_threshold = is_insert ? settings::EARLY_EXIT_BUFFER_INSERT @@ -1287,12 +1149,6 @@ namespace hnswlib { } dist_t sim; -#ifdef DISABLE_HYBRID_QUANTIZATION - if(!getDataByInternalId(candidate_id, layer, buffer.data())) { - continue; - } - sim = fstSimFunc_(data_point, buffer.data(), dist_func_param_); -#else const void* neighbor_data = nullptr; if(layer == 0) { if(getDataByInternalId(candidate_id, layer, buffer.data())) { @@ -1307,7 +1163,6 @@ namespace hnswlib { } sim = curSimFunc(data_point, neighbor_data, curDistParam); -#endif //DISABLE_HYBRID_QUANTIZATION if(top_candidates.size() < ef || sim > lowerBound) { candidate_set.emplace(sim, candidate_id); @@ -1326,7 +1181,6 @@ namespace hnswlib { } visited_list_pool_->releaseVisitedList(vl); - // return a vector of top candidates sorted by similarity (1-distance) in reverse order std::vector> sorted_candidates; sorted_candidates.reserve(top_candidates.size()); while(!top_candidates.empty()) { @@ -1334,13 +1188,8 @@ namespace hnswlib { top_candidates.pop(); } std::reverse(sorted_candidates.begin(), sorted_candidates.end()); - // Return the top candidates return sorted_candidates; } - - // For a given internal id, go to all neighbors and remove the connection. Then set the - // linklist count to 0 - // Repeat this for all levels void removeAllConnections(idhInt internal_id, levelInt elem_level) { for(int level = 0; level <= elem_level; ++level) { From 07bd389da4cfef9df30506dd5a88e9d71735a2ff Mon Sep 17 00:00:00 2001 From: shaleenji Date: Sat, 7 Feb 2026 07:57:43 +0000 Subject: [PATCH 15/48] ndd binary soft link instead of different names for all CPU types (#30) * first commit * readme update --------- Co-authored-by: Shaleen Garg --- CMakeLists.txt | 8 ++++++++ README.md | 10 ++++++---- run.sh | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aa99817218..86c261a0f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -327,3 +327,11 @@ endif() message(STATUS "ASIO include dir: ${ASIO_INCLUDE_DIR}") message(STATUS "LMDB include dir: ${LMDB_INCLUDE_DIR}") message(STATUS "OpenSSL include dir: ${OPENSSL_INCLUDE_DIR}") + +# Create a symbolic link named 'ndd' pointing to the architecture-specific binary +add_custom_command(TARGET ${NDD_BINARY_NAME} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E create_symlink + $ + ${CMAKE_CURRENT_BINARY_DIR}/ndd + COMMENT "Creating softlink 'ndd' -> ${NDD_BINARY_NAME}" +) diff --git a/README.md b/README.md index 0d05093469..2ef39bffac 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,8 @@ The output binary name depends on the SIMD flag used during compilation: * `ndd-neon` (or `ndd-neon-darwin` for mac) * `ndd-sve2` +A symlink called `ndd` links to the binary compiled for the current build. + ### Runtime Environment Variables Some environment variables **ndd** reads at runtime: @@ -216,7 +218,7 @@ Some environment variables **ndd** reads at runtime: **Open Mode (No Authentication)** - Default when `NDD_AUTH_TOKEN` is not set: ```bash # All APIs work without authentication -./build/ndd-avx2 +./build/ndd curl http://{{BASE_URL}}/api/v1/index/list ``` @@ -224,7 +226,7 @@ curl http://{{BASE_URL}}/api/v1/index/list ```bash # Generate a secure token export NDD_AUTH_TOKEN=$(openssl rand -hex 32) -./build/ndd-avx2 +./build/ndd # All protected APIs require the token in Authorization header curl -H "Authorization: $NDD_AUTH_TOKEN" http://{{BASE_URL}}/api/v1/index/list @@ -240,13 +242,13 @@ mkdir -p ./data # 2. Export the environment variable and run export NDD_DATA_DIR=$(pwd)/data -./build/ndd-avx2 +./build/ndd ``` Alternatively, as a single line: ```bash -NDD_DATA_DIR=./data ./build/ndd-avx2 +NDD_DATA_DIR=./data ./build/ndd ``` --- diff --git a/run.sh b/run.sh index b3e4e8af9c..67379d89f5 100755 --- a/run.sh +++ b/run.sh @@ -46,7 +46,7 @@ main() { done if [[ -z "$BINARY_FILE" ]]; then - # check if build folder exists and if any binary starting with ndd-* exists, fi yes then save the filename in a variable + # check if build folder exists and if any binary starting with ndd-* exists, if yes then save the filename in a variable if [[ -d "build" && -n "$(find build -maxdepth 1 -name 'ndd-*' -type f)" ]]; then BINARY_FILE=$(find build -maxdepth 1 -name 'ndd-*' -type f | head -n 1) log "Found binary: $BINARY_FILE" From 9d48d5efe1f34c3d5bc27e103d328e0b457ffa77 Mon Sep 17 00:00:00 2001 From: vindwi <130017173+vindwi@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:51:32 +0530 Subject: [PATCH 16/48] Filter design (#32) * filter aware search * multiple entry points to base layer * progressive filtering approach * budget based pruning * filter_params --- .gitignore | 7 + CMakeLists.txt | 13 +- docs/filter.md | 95 ++ src/core/ndd.hpp | 78 +- src/core/types.hpp | 6 + .../{bitmap_index.hpp => category_index.hpp} | 38 +- src/filter/filter.hpp | 119 ++- src/filter/numeric_index.hpp | 907 +++++++++--------- src/hnsw/hnswalg.h | 205 +++- src/hnsw/hnswlib.h | 2 +- src/main.cpp | 14 + src/sparse/bmw.hpp | 17 +- src/sparse/sparse_storage.hpp | 4 +- src/utils/settings.hpp | 6 +- tests/CMakeLists.txt | 45 + tests/README.md | 19 + tests/filter_test.cpp | 219 +++++ 17 files changed, 1242 insertions(+), 552 deletions(-) create mode 100644 docs/filter.md rename src/filter/{bitmap_index.hpp => category_index.hpp} (83%) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/README.md create mode 100644 tests/filter_test.cpp diff --git a/.gitignore b/.gitignore index 61e959191a..ddd10fad2b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,13 @@ build/* # Ignore tests build directory tests/build/ +tests/build*/ + +# Test binaries +tests/**/ndd_filter_test + +# macOS debug symbols +*.dSYM/ # Sometimes data files are created for tetsing data/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 86c261a0f4..0d6fc30fac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -311,9 +311,16 @@ target_link_libraries(${NDD_BINARY_NAME} PRIVATE # Installation rules install(TARGETS ${NDD_BINARY_NAME} RUNTIME DESTINATION bin) -# Print configuration info -message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") -message(STATUS "Debug mode: ${DEBUG}") + +# ======================= +# Testing +# ======================= +option(ENABLE_TESTING "Enable building tests" OFF) +if(ENABLE_TESTING) + enable_testing() + add_subdirectory(tests) +endif() + message(STATUS "Processor: ${CMAKE_SYSTEM_PROCESSOR}") if(USE_AVX512) message(STATUS "SIMD Mode: AVX512") diff --git a/docs/filter.md b/docs/filter.md new file mode 100644 index 0000000000..7340840409 --- /dev/null +++ b/docs/filter.md @@ -0,0 +1,95 @@ +# Filter Design & Strategy + +This document outlines the architectural design for Endee's filtering system, covering component designs for Numeric, Category, and Boolean types, and the overarching execution strategy. + +## 1. Global Filtering Strategy + +The system prioritizes **Pre-Filtering** followed by an adaptive search execution path. + +### 1.1. Execution Flow +1. **Filter Analysis:** + * Incoming queries (e.g., `Age: [18-25] AND City: "NY"`) are broken into atomic filter operations. + * **Cardinality Estimation:** Each filter estimates its result set size (e.g., "NY" has 500 users, "Age" has 10k). +2. **Optimization (Cheapest First):** + * Filters are executed in order of increasing cardinality (smallest first). + * Results are intersected (`AND`) incrementally. If the intermediate result becomes empty, execution stops early. +3. **Adaptive Search Path:** + * Final `RoaringBitmap` of valid IDs is passed to the Vector Search engine. + * **Small Result (< 1,000 IDs):** **Bypass HNSW.** Fetch vectors for valid IDs directly and perform Brute Force distance calculation. This avoids graph overhead for sparse results. + * **Large Result:** **Filtered HNSW.** Pass the Bitmap to HNSW's `searchKnn` via `BitMapFilterFunctor`. + +--- + +## 2. Numeric Filter Design + +*Optimized for range queries, high compression, and sequential access.* + +### 2.1. Storage Architecture (Hybrid Bucket) +The database (LMDB) acts as a coarse-grained B+ Tree. +* **Key:** `[FieldID] + [Base_Value_32bit]`. + * Floats are mapped to lexicographically ordered integers to preserve sort order. + * Keys are stored in Big-Endian to support native cursor iteration. +* **Value (Bucket):** Fixed-size block (Max 1024 unique values). + * **Summary Bitmap (Roaring):** Pre-computed union of all IDs in the bucket. Used for $O(1)$ block retrieval during full overlaps. + * **Data Arrays (Structure of Arrays - SoA):** + * **Values:** Compressed as `uint16_t` deltas relative to the Key's `Base_Value`. + * **IDs:** Raw `idInt` array, index-aligned with values. + +### 2.2. Query Execution +* **Buckets Fully Inside Selection (Middle):** Use **Summary Bitmap**. Zero array access. +* **Buckets Partially Overlapping (Edges):** Scan `Values` array (SIMD), use indices to fetch specific `IDs`. + +### 2.3. Constraints & Splitting +* **Split Triggers:** Count > 1024 OR Delta > 65,535. +* **Sliding Split:** To ensure Key Uniqueness in LMDB, splits do not strictly occur at the median. The split point "slides" right to find the first value divergence, ensuring `Key(RightBucket) != Key(LeftBucket)`. + +--- + +## 3. Category Filter Design + +*Optimized for exact match lookups and faceting.* + +### 3.1. Interface (MongoDB-Style) +* **Single Value:** `{"City": "NY"}` +* **List Membership ($in):** `{"City": {"$in": ["NY", "London", "Tokyo"]}}` + +### 3.2. Storage Architecture +Utilizes Inverted Indices with **Text-Based Keys** to enable prefix scanning and faceting. +* **Key:** `[FieldName] + ":" + [Value]`. + * **Parsing Logic:** The system strictly splits on the **first** occurrence of `:`. + * **Format:** `City:New:York` is parsed as Field=`City`, Value=`New:York`. + * **Constraints:** `FieldName` must **not** contain the `:` character (alphanumeric + underscore recommended). `Value` can contain any character including `:`. +* **Value:** `RoaringBitmap` (Serialized). Contains all IDs that have this attribute value. + +### 3.3. Query Execution +* **Exact Match:** Direct Key lookup. +* **$in Query:** + 1. Parse the list `["NY", "London"]`. + 2. Perform multiple Key lookups. + 3. Compute the **Union** of the resulting Bitmaps efficiently. + +--- + +## 4. Boolean Filter Design + +*Optimized for extreme density ops.* + +### 4.1. Storage Architecture +Treated as a specialized Category filter with strictly two possible keys per field. +* **Keys:** `[FieldName]:0` (False) and `[FieldName]:1` (True). + * Consistent with the text-based key design (uses `:` separator). +* **Value:** `RoaringBitmap`. + +### 4.2. Strategy +Boolean filters are typically low-selectivity (often matching ~50% of the DB). They are processed **Last** in the intersection chain unless statistics indicate high skew (e.g., `Is_Active` is true for 99% of data, so filtering for `False` is fast). + +--- + +## 5. Schema & Type Enforcement + +To ensure index integrity without a strict schema registry, the system adheres to **First-Write Wins** typing. + +* **Immutable Types:** Once a `FieldName` is indexed with a specific type (Numeric, Category, or Boolean), that type is bound to the field. +* **Validation Logic:** + * If `is_active` is first seen as **Boolean**, subsequent attempts to insert `is_active: "yes"` (Category) or `is_active: 1` (Numeric bucket) must be rejected. + * This prevents storage corruption and ambiguous query parsing. diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 2a4b93ed04..710008f2bd 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -1501,9 +1501,10 @@ class IndexManager { const std::vector& query, size_t k, const nlohmann::json& filter_array, + ndd::FilterParams params = {}, bool include_vectors = false, size_t ef = 0) { - return searchKNN(index_id, query, {}, {}, k, filter_array, include_vectors, ef); + return searchKNN(index_id, query, {}, {}, k, filter_array, params, include_vectors, ef); } std::optional> @@ -1513,12 +1514,19 @@ class IndexManager { const std::vector& sparse_values, size_t k, const nlohmann::json& filter_array, + ndd::FilterParams params = {}, bool include_vectors = false, size_t ef = 0) { try { auto& entry = getIndexEntry(index_id); entry.searchCount += k; + // 0. Compute Filter Bitmap (Shared) + std::optional active_filter_bitmap; + if (!filter_array.empty()) { + active_filter_bitmap = entry.vector_storage->filter_store_->computeFilterBitmap(filter_array); + } + // 1. Sparse Search (Async) std::future>> sparse_future; if(entry.sparse_storage && !sparse_indices.empty()) { @@ -1541,7 +1549,8 @@ class IndexManager { sparse_query.values.push_back(p.second); } - return entry.sparse_storage->search(sparse_query, k); + const ndd::RoaringBitmap* filter_ptr = active_filter_bitmap.has_value() ? &(*active_filter_bitmap) : nullptr; + return entry.sparse_storage->search(sparse_query, k, filter_ptr); }); } @@ -1554,11 +1563,52 @@ class IndexManager { auto space = entry.alg->getSpace(); std::vector query_bytes = ndd::quant::get_quantizer_dispatch(quant_level).quantize(query); - // Always try post-filtering first (or direct search if no filter) - // When there are filters, we need to search for more candidates to account for - // filtering - size_t search_k = filter_array.empty() ? k : std::max(ef, k * 2); - dense_results = entry.alg->searchKnn(query_bytes.data(), search_k, ef); + + if (!active_filter_bitmap) { + dense_results = entry.alg->searchKnn(query_bytes.data(), k, ef); + } else { + // Smart Filter Execution Strategy + auto& bitmap = *active_filter_bitmap; + size_t card = bitmap.cardinality(); + + if (card == 0) { + // No results match filter + } else if (card < params.prefilter_threshold) { + // Strategy A: Brute Force on Small Subset + std::vector valid_ids; + valid_ids.reserve(card); + bitmap.iterate([](ndd::idInt id, void* ptr){ + static_cast*>(ptr)->push_back(id); + return true; + }, &valid_ids); + + // Fetch vectors + auto vector_batch = entry.vector_storage->get_vectors_batch(valid_ids); + + // Prepare subset for bruteforce search + std::vector>> vector_subset; + vector_subset.reserve(vector_batch.size()); + for(const auto& [nid, vbytes] : vector_batch) { + vector_subset.emplace_back(nid, vbytes); + } + + dense_results = hnswlib::searchKnnSubset( + query_bytes.data(), vector_subset, k, space); + + } else { + // Strategy B: Filtered HNSW Search + BitMapFilterFunctor functor(bitmap); + size_t effective_ef = ef > 0 ? ef : settings::DEFAULT_EF_SEARCH; + + // Try to use optimized templated search if algorithm matches + auto* hnsw_alg = dynamic_cast*>(entry.alg.get()); + if (hnsw_alg) { + dense_results = hnsw_alg->searchKnn(query_bytes.data(), k, effective_ef, &functor, params.boost_percentage); + } else { + dense_results = entry.alg->searchKnn(query_bytes.data(), k, effective_ef, &functor, params.boost_percentage); + } + } + } } // 3. Get Sparse Results (Join) @@ -1618,8 +1668,7 @@ class IndexManager { ndd::VectorMeta meta = entry.vector_storage->get_meta(p.second); // Apply filter - if(!filter_array.empty() - && !entry.vector_storage->matches_filter(p.second, meta, filter_array)) { + if(active_filter_bitmap && !active_filter_bitmap->contains(p.second)) { continue; } @@ -1651,18 +1700,15 @@ class IndexManager { } } - // Check if post-filtering gave poor results and pre-filtering might help - // Only for dense search for now as sparse pre-filtering is not implemented - if(!filter_array.empty() - && filtered_count < k * settings::PREFILTER_RESULT_RATIO_THRESHOLD && !query.empty() - && sparse_results.empty()) { + // Fallback logic removed + if(false) { size_t filter_cardinality = entry.vector_storage->filter_store_->countIdsMatchingFilter(filter_array); LOG_DEBUG("Post-filter gave poor results (" << filtered_count << "/" << k << "), checking pre-filter option. Cardinality: " << filter_cardinality); - if(filter_cardinality < settings::PREFILTER_CARDINALITY_THRESHOLD) { + if(filter_cardinality < params.prefilter_threshold) { LOG_DEBUG("Using pre-filter approach due to poor post-filter results"); // Pre-filter: Get filtered IDs and do bruteforce search @@ -1749,7 +1795,7 @@ class IndexManager { } else { LOG_DEBUG("Filter cardinality too high for pre-filtering (" << filter_cardinality - << " >= " << settings::PREFILTER_CARDINALITY_THRESHOLD + << " >= " << params.prefilter_threshold << "), returning post-filter results"); } } diff --git a/src/core/types.hpp b/src/core/types.hpp index 7c6a787d2c..d78fd87651 100644 --- a/src/core/types.hpp +++ b/src/core/types.hpp @@ -6,9 +6,15 @@ // to enable 64-bit IDs. Default is 32-bit for performance/memory efficiency. #include "../../third_party/roaring_bitmap/roaring.hh" +#include "../utils/settings.hpp" namespace ndd { + struct FilterParams { + size_t prefilter_threshold = settings::PREFILTER_CARDINALITY_THRESHOLD; + size_t boost_percentage = settings::FILTER_BOOST_PERCENTAGE; + }; + #ifdef NDD_USE_64BIT_IDS // --- 64-bit Configuration --- using idInt = uint64_t; // External ID (stored in DB, exposed to user) diff --git a/src/filter/bitmap_index.hpp b/src/filter/category_index.hpp similarity index 83% rename from src/filter/bitmap_index.hpp rename to src/filter/category_index.hpp index c141fc952d..70e969f28f 100644 --- a/src/filter/bitmap_index.hpp +++ b/src/filter/category_index.hpp @@ -11,7 +11,7 @@ namespace ndd { namespace filter { - class BitmapIndex { + class CategoryIndex { private: MDBX_env* env_; MDBX_dbi dbi_; @@ -101,24 +101,50 @@ namespace ndd { } public: - BitmapIndex(MDBX_env* env) : + CategoryIndex(MDBX_env* env) : env_(env) { MDBX_txn* txn; int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); if(rc != MDBX_SUCCESS) { - throw std::runtime_error("Failed to begin txn for BitmapIndex init"); + throw std::runtime_error("Failed to begin txn for CategoryIndex init"); } - // Open default DB or named DB? The original code used nullptr (default DB) - rc = mdbx_dbi_open(txn, nullptr, MDBX_CREATE, &dbi_); + // Open named DB for category/boolean + rc = mdbx_dbi_open(txn, "category_idx", MDBX_CREATE, &dbi_); if(rc != MDBX_SUCCESS) { mdbx_txn_abort(txn); - throw std::runtime_error("Failed to open BitmapIndex dbi"); + throw std::runtime_error("Failed to open category_idx dbi"); } mdbx_txn_commit(txn); } + // Faceting: List all unique values for a field + std::vector scan_values(const std::string& field) const { + std::vector values; + MDBX_txn* txn; + if (mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn) != MDBX_SUCCESS) return values; + + MDBX_cursor* cursor; + mdbx_cursor_open(txn, dbi_, &cursor); + + std::string prefix = field + ":"; + MDBX_val key{const_cast(prefix.c_str()), prefix.size()}; + MDBX_val data; + + int rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); + while(rc == MDBX_SUCCESS) { + std::string found_key((char*)key.iov_base, key.iov_len); + if(found_key.rfind(prefix, 0) != 0) break; + + values.push_back(found_key.substr(prefix.size())); + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); + } + mdbx_cursor_close(cursor); + mdbx_txn_abort(txn); + return values; + } + ndd::RoaringBitmap get_bitmap(const std::string& field, const std::string& value) const { return get_bitmap_internal(format_filter_key(field, value)); diff --git a/src/filter/filter.hpp b/src/filter/filter.hpp index c03af41dc7..35bc1b5bce 100644 --- a/src/filter/filter.hpp +++ b/src/filter/filter.hpp @@ -16,9 +16,10 @@ #include "mdbx/mdbx.h" #include "../utils/log.hpp" #include "../core/types.hpp" +#include "../hnsw/hnswlib.h" // For BaseFilterFunctor #include "numeric_index.hpp" -#include "bitmap_index.hpp" +#include "category_index.hpp" enum class FieldType : uint8_t { Unknown = 0, @@ -27,13 +28,23 @@ enum class FieldType : uint8_t { Bool = 4 }; +// Filter Functor for HNSW +class BitMapFilterFunctor : public hnswlib::BaseFilterFunctor { + const ndd::RoaringBitmap& bitmap_; +public: + BitMapFilterFunctor(const ndd::RoaringBitmap& bitmap) : bitmap_(bitmap) {} + bool operator()(ndd::idInt id) override { + return bitmap_.contains(id); + } +}; + class Filter { private: MDBX_env* env_; MDBX_dbi dbi_; // Used for schema storage std::string path_; - std::unique_ptr numeric_index_; - std::unique_ptr bitmap_index_; + std::unique_ptr numeric_index_; + std::unique_ptr category_index_; static constexpr const char* SCHEMA_KEY = "__ndd_schema_v1__"; std::unordered_map schema_cache_; @@ -146,8 +157,8 @@ class Filter { } // Initialize Indices - numeric_index_ = std::make_unique(env_); - bitmap_index_ = std::make_unique(env_); + numeric_index_ = std::make_unique(env_); + category_index_ = std::make_unique(env_); load_schema(); } @@ -179,8 +190,8 @@ class Filter { return ndd::RoaringBitmap(); } - ndd::RoaringBitmap final_result; - bool first = true; + std::vector partial_results; + partial_results.reserve(filter_array.size()); for(const auto& condition : filter_array) { if(!condition.is_object() || condition.size() != 1) { @@ -217,9 +228,9 @@ class Filter { if(type == FieldType::Number) { uint32_t sortable_val; if(val.is_number_integer()) { - sortable_val = ndd::numeric::int_to_sortable(val.get()); + sortable_val = ndd::filter::int_to_sortable(val.get()); } else if(val.is_number()) { - sortable_val = ndd::numeric::float_to_sortable(val.get()); + sortable_val = ndd::filter::float_to_sortable(val.get()); } else { throw std::runtime_error("$eq value for numeric field must be a number"); } @@ -232,13 +243,13 @@ class Filter { if(val.is_string()) { str_val = val.get(); } else if(val.is_boolean()) { - str_val = val.get() ? "true" : "false"; + str_val = val.get() ? "1" : "0"; } else { - // Integers for non-numeric fields are treated as strings without padding str_val = std::to_string(val.get()); + if (str_val.size() > 255) throw std::runtime_error("Category value too long"); } std::string key = format_filter_key(field, str_val); - or_result = bitmap_index_->get_bitmap_by_key(key); + or_result = category_index_->get_bitmap_by_key(key); } } else if(op == "$in") { if(!val.is_array()) { @@ -251,9 +262,9 @@ class Filter { if(type == FieldType::Number) { uint32_t sortable_val; if(v.is_number_integer()) { - sortable_val = ndd::numeric::int_to_sortable(v.get()); + sortable_val = ndd::filter::int_to_sortable(v.get()); } else if(v.is_number()) { - sortable_val = ndd::numeric::float_to_sortable(v.get()); + sortable_val = ndd::filter::float_to_sortable(v.get()); } else { throw std::runtime_error( "$in value for numeric field must be a number"); @@ -268,13 +279,14 @@ class Filter { if(v.is_string()) { str_val = v.get(); } else if(v.is_boolean()) { - str_val = v.get() ? "true" : "false"; + str_val = v.get() ? "1" : "0"; } else { str_val = std::to_string(v.get()); } if(!str_val.empty()) { + if (str_val.size() > 255) throw std::runtime_error("Category value too long"); std::string key = format_filter_key(field, str_val); - or_result |= bitmap_index_->get_bitmap_by_key(key); + or_result |= category_index_->get_bitmap_by_key(key); } } } @@ -289,17 +301,17 @@ class Filter { uint32_t start_val, end_val; if(val[0].is_number_integer()) { - start_val = ndd::numeric::int_to_sortable(val[0].get()); + start_val = ndd::filter::int_to_sortable(val[0].get()); } else if(val[0].is_number()) { - start_val = ndd::numeric::float_to_sortable(val[0].get()); + start_val = ndd::filter::float_to_sortable(val[0].get()); } else { throw std::runtime_error("Range start must be a number"); } if(val[1].is_number_integer()) { - end_val = ndd::numeric::int_to_sortable(val[1].get()); + end_val = ndd::filter::int_to_sortable(val[1].get()); } else if(val[1].is_number()) { - end_val = ndd::numeric::float_to_sortable(val[1].get()); + end_val = ndd::filter::float_to_sortable(val[1].get()); } else { throw std::runtime_error("Range end must be a number"); } @@ -316,14 +328,23 @@ class Filter { } else { throw std::runtime_error("Unsupported operator: " + op); } + + partial_results.push_back(std::move(or_result)); + } - // Combine with final result - if(first) { - final_result = std::move(or_result); - first = false; - } else { - final_result &= or_result; - } + // Optimization: Sort by cardinality (smallest first) + std::sort(partial_results.begin(), partial_results.end(), + [](const ndd::RoaringBitmap& a, const ndd::RoaringBitmap& b) { + return a.cardinality() < b.cardinality(); + }); + + if (partial_results.empty()) return ndd::RoaringBitmap(); + + ndd::RoaringBitmap final_result = partial_results[0]; + for(size_t i = 1; i < partial_results.size(); ++i) { + final_result &= partial_results[i]; + // If result becomes empty, stop early + if(final_result.isEmpty()) return final_result; } return final_result; @@ -349,7 +370,7 @@ class Filter { } void add_to_filter(const std::string& field, const std::string& value, ndd::idInt numeric_id) { - bitmap_index_->add(field, value, numeric_id); + category_index_->add(field, value, numeric_id); } // Batch add operation for filters @@ -358,7 +379,7 @@ class Filter { if(numeric_ids.empty()) { return; } - bitmap_index_->add_batch_by_key(filter_key, numeric_ids); + category_index_->add_batch_by_key(filter_key, numeric_ids); } // Optimized version to process filter JSON in batch @@ -402,14 +423,14 @@ class Filter { // Use Numeric Index for numbers uint32_t sortable_val; if(value.is_number_integer()) { - sortable_val = ndd::numeric::int_to_sortable(value.get()); + sortable_val = ndd::filter::int_to_sortable(value.get()); } else { - sortable_val = ndd::numeric::float_to_sortable(value.get()); + sortable_val = ndd::filter::float_to_sortable(value.get()); } numeric_index_->put(field, numeric_id, sortable_val); } else if(value.is_boolean()) { std::string filter_key = - format_filter_key(field, value.get() ? "true" : "false"); + format_filter_key(field, value.get() ? "1" : "0"); filter_to_ids[filter_key].push_back(numeric_id); } else { // Optional: catch bad types (bool, float, object, array, etc.) @@ -430,11 +451,11 @@ class Filter { void remove_from_filter(const std::string& field, const std::string& value, ndd::idInt numeric_id) { - bitmap_index_->remove(field, value, numeric_id); + category_index_->remove(field, value, numeric_id); } bool contains(const std::string& field, const std::string& value, ndd::idInt numeric_id) const { - return bitmap_index_->contains(field, value, numeric_id); + return category_index_->contains(field, value, numeric_id); } void add_filters_from_json(ndd::idInt numeric_id, const std::string& filter_json) { @@ -465,13 +486,13 @@ class Filter { } else if(value.is_number()) { uint32_t sortable_val; if(value.is_number_integer()) { - sortable_val = ndd::numeric::int_to_sortable(value.get()); + sortable_val = ndd::filter::int_to_sortable(value.get()); } else { - sortable_val = ndd::numeric::float_to_sortable(value.get()); + sortable_val = ndd::filter::float_to_sortable(value.get()); } numeric_index_->put(field, numeric_id, sortable_val); } else if(value.is_boolean()) { - add_to_filter(field, value.get() ? "true" : "false", numeric_id); + add_to_filter(field, value.get() ? "1" : "0", numeric_id); } } } catch(const std::exception& e) { @@ -489,7 +510,7 @@ class Filter { // Remove from Numeric Index numeric_index_->remove(field, numeric_id); } else if(value.is_boolean()) { - remove_from_filter(field, value.get() ? "true" : "false", numeric_id); + remove_from_filter(field, value.get() ? "1" : "0", numeric_id); } } } catch(const std::exception& e) { @@ -504,10 +525,10 @@ class Filter { bool first = true; for(const auto& [field, value] : filters) { if(first) { - result = bitmap_index_->get_bitmap(field, value); + result = category_index_->get_bitmap(field, value); first = false; } else { - result &= bitmap_index_->get_bitmap(field, value); + result &= category_index_->get_bitmap(field, value); } } return result; @@ -518,7 +539,7 @@ class Filter { combine_filters_or(const std::vector>& filters) const { ndd::RoaringBitmap result; for(const auto& [field, value] : filters) { - result |= bitmap_index_->get_bitmap(field, value); + result |= category_index_->get_bitmap(field, value); } return result; } @@ -531,9 +552,9 @@ class Filter { if(op == "$eq") { uint32_t sortable_val; if(val.is_number_integer()) { - sortable_val = ndd::numeric::int_to_sortable(val.get()); + sortable_val = ndd::filter::int_to_sortable(val.get()); } else if(val.is_number()) { - sortable_val = ndd::numeric::float_to_sortable(val.get()); + sortable_val = ndd::filter::float_to_sortable(val.get()); } else { return false; } @@ -545,9 +566,9 @@ class Filter { for(const auto& v : val) { uint32_t sortable_val; if(v.is_number_integer()) { - sortable_val = ndd::numeric::int_to_sortable(v.get()); + sortable_val = ndd::filter::int_to_sortable(v.get()); } else if(v.is_number()) { - sortable_val = ndd::numeric::float_to_sortable(v.get()); + sortable_val = ndd::filter::float_to_sortable(v.get()); } else { continue; } @@ -564,17 +585,17 @@ class Filter { uint32_t start_val, end_val; if(val[0].is_number_integer()) { - start_val = ndd::numeric::int_to_sortable(val[0].get()); + start_val = ndd::filter::int_to_sortable(val[0].get()); } else if(val[0].is_number()) { - start_val = ndd::numeric::float_to_sortable(val[0].get()); + start_val = ndd::filter::float_to_sortable(val[0].get()); } else { return false; } if(val[1].is_number_integer()) { - end_val = ndd::numeric::int_to_sortable(val[1].get()); + end_val = ndd::filter::int_to_sortable(val[1].get()); } else if(val[1].is_number()) { - end_val = ndd::numeric::float_to_sortable(val[1].get()); + end_val = ndd::filter::float_to_sortable(val[1].get()); } else { return false; } diff --git a/src/filter/numeric_index.hpp b/src/filter/numeric_index.hpp index b869b47dea..c002652137 100644 --- a/src/filter/numeric_index.hpp +++ b/src/filter/numeric_index.hpp @@ -12,15 +12,14 @@ #include "../core/types.hpp" namespace ndd { - namespace numeric { + namespace filter { - // Sortable Key Utilities + // --- Sortable Key Utilities --- inline uint32_t float_to_sortable(float f) { uint32_t i; std::memcpy(&i, &f, sizeof(float)); - // IEEE 754 floats: - // If f >= 0 (sign bit 0): map to [0x80000000, 0xFFFFFFFF] - // If f < 0 (sign bit 1): map to [0x00000000, 0x7FFFFFFF] + // IEEE 754: if sign bit set, flip all bits. Else flip just sign. + // This makes negatives < positives order correctly. uint32_t mask = (int32_t(i) >> 31) | 0x80000000; return i ^ mask; } @@ -41,126 +40,196 @@ namespace ndd { return static_cast(i ^ 0x80000000); } - // Bucket Structure + // --- Bucket Structure (Hybrid) --- struct Bucket { - static constexpr size_t MAX_SIZE = 512; // Increased bucket size - std::vector> entries; // value, doc_id + static constexpr size_t MAX_SIZE = 1024; + static constexpr uint32_t MAX_DELTA = 65535; - // Serialize to byte buffer + // Runtime only, not serialized in the payload + uint32_t base_value = 0; + + // Data + std::vector deltas; + std::vector ids; + ndd::RoaringBitmap summary_bitmap; + + bool is_dirty = false; + + // Helper to get actual value + uint32_t get_value(size_t index) const { + return base_value + deltas[index]; + } + + void add(uint32_t val, ndd::idInt id) { + if (val < base_value) { + // Should not happen if Key logic is correct + throw std::runtime_error("Insert value < Base Value"); + } + uint32_t delta_32 = val - base_value; + if (delta_32 > MAX_DELTA) { + throw std::runtime_error("Delta overflow"); + } + + // Maintain sorted order by Value (Delta) + uint16_t delta = static_cast(delta_32); + + // Find insertion point + auto it = std::lower_bound(deltas.begin(), deltas.end(), delta); + size_t index = std::distance(deltas.begin(), it); + + deltas.insert(it, delta); + ids.insert(ids.begin() + index, id); + + summary_bitmap.add(id); + is_dirty = true; + } + + bool remove(ndd::idInt id) { + // Find index by ID (linear scan needed as ids are not sorted) + for (size_t i = 0; i < ids.size(); ++i) { + if (ids[i] == id) { + ids.erase(ids.begin() + i); + deltas.erase(deltas.begin() + i); + + // Rebuild or update bitmap? Roaring remove is fast + summary_bitmap.remove(id); + is_dirty = true; + return true; + } + } + return false; + } + + // Serialization Format: + // [BitmapSize (4)] + // [Bitmap Bytes] + // [Count (2)] + // [Deltas (Count * 2)] + // [IDs (Count * sizeof(idInt))] std::vector serialize() const { - // Format: Count(4) + [Value(4) + ID(sizeof(idInt))] * N - std::vector buffer; - buffer.reserve(4 + entries.size() * (4 + sizeof(ndd::idInt))); - - uint32_t count = static_cast(entries.size()); - buffer.insert(buffer.end(), (uint8_t*)&count, (uint8_t*)&count + 4); - - for(const auto& entry : entries) { - buffer.insert(buffer.end(), (uint8_t*)&entry.first, (uint8_t*)&entry.first + 4); - buffer.insert(buffer.end(), - (uint8_t*)&entry.second, - (uint8_t*)&entry.second + sizeof(ndd::idInt)); + // Optimize bitmap + const_cast(summary_bitmap).runOptimize(); + + size_t bm_size = summary_bitmap.getSizeInBytes(); + uint16_t count = static_cast(ids.size()); + + size_t total_size = 4 + bm_size + 2 + (count * 2) + (count * sizeof(ndd::idInt)); + std::vector buffer(total_size); + uint8_t* ptr = buffer.data(); + + // 1. Bitmap Header + uint32_t bm_size_32 = static_cast(bm_size); + std::memcpy(ptr, &bm_size_32, 4); ptr += 4; + + // 2. Bitmap Data + if (bm_size > 0) { + summary_bitmap.write(reinterpret_cast(ptr)); + ptr += bm_size; + } + + // 3. Count + std::memcpy(ptr, &count, 2); ptr += 2; + + // 4. Deltas + if (count > 0) { + std::memcpy(ptr, deltas.data(), count * 2); ptr += count * 2; + } + + // 5. IDs + if (count > 0) { + std::memcpy(ptr, ids.data(), count * sizeof(ndd::idInt)); } + return buffer; } - static Bucket deserialize(const void* data, size_t len) { + static Bucket deserialize(const void* data, size_t len, uint32_t base_val) { Bucket b; - if(len < 4) { - return b; - } + b.base_value = base_val; + + if (len < 6) return b; // Min valid size const uint8_t* ptr = static_cast(data); - uint32_t count; - std::memcpy(&count, ptr, 4); - ptr += 4; - - size_t entry_size = 4 + sizeof(ndd::idInt); - if(len < 4 + count * entry_size) { - // Corrupt data or partial read - return b; + const uint8_t* end = ptr + len; + + // 1. Bitmap Size + uint32_t bm_size; + std::memcpy(&bm_size, ptr, 4); ptr += 4; + + if (ptr + bm_size > end) { + throw std::runtime_error("Bucket corrupt: invalid bitmap size"); } - b.entries.reserve(count); - for(uint32_t i = 0; i < count; ++i) { - uint32_t val; - ndd::idInt id; - std::memcpy(&val, ptr, 4); - ptr += 4; - std::memcpy(&id, ptr, sizeof(ndd::idInt)); - ptr += sizeof(ndd::idInt); - b.entries.emplace_back(val, id); + // 2. Bitmap + if (bm_size > 0) { + b.summary_bitmap = ndd::RoaringBitmap::read(reinterpret_cast(ptr)); + ptr += bm_size; } - return b; - } - void add(uint32_t val, ndd::idInt id) { - entries.emplace_back(val, id); - // Keep sorted by value - std::sort(entries.begin(), entries.end()); - } + if (ptr + 2 > end) throw std::runtime_error("Bucket corrupt: truncated count"); - bool remove(ndd::idInt id) { - auto it = std::remove_if(entries.begin(), entries.end(), [id](const auto& p) { - return p.second == id; - }); - if(it != entries.end()) { - entries.erase(it, entries.end()); - return true; - } - return false; - } + // 3. Count + uint16_t count; + std::memcpy(&count, ptr, 2); ptr += 2; - bool is_full() const { return entries.size() >= MAX_SIZE; } - bool is_empty() const { return entries.empty(); } + // 4. Deltas & IDs + if (count > 0) { + size_t delta_size = count * 2; + size_t id_size = count * sizeof(ndd::idInt); + + if (ptr + delta_size + id_size > end) { + throw std::runtime_error("Bucket corrupt: truncated Data"); + } - // Split bucket into two, returning the new bucket (upper half) - Bucket split() { - Bucket new_bucket; - size_t mid = entries.size() / 2; + b.deltas.resize(count); + std::memcpy(b.deltas.data(), ptr, delta_size); ptr += delta_size; - new_bucket.entries.assign(entries.begin() + mid, entries.end()); - entries.resize(mid); + b.ids.resize(count); + std::memcpy(b.ids.data(), ptr, id_size); + } + + return b; + } - return new_bucket; + // Fast access to just the bitmap (for middle buckets) + static ndd::RoaringBitmap read_summary_bitmap(const void* data, size_t len) { + const uint8_t* ptr = static_cast(data); + uint32_t bm_size; + std::memcpy(&bm_size, ptr, 4); ptr += 4; + if(bm_size == 0) return ndd::RoaringBitmap(); + return ndd::RoaringBitmap::read(reinterpret_cast(ptr)); } - uint32_t min_val() const { return entries.empty() ? 0 : entries.front().first; } - uint32_t max_val() const { return entries.empty() ? 0 : entries.back().first; } + bool is_full() const { return ids.size() >= MAX_SIZE; } + bool is_empty() const { return ids.empty(); } }; class NumericIndex { private: MDBX_env* env_; MDBX_dbi forward_dbi_; // ID -> Value (Field:ID -> Value) - MDBX_dbi inverted_dbi_; // BucketKey -> Bucket (Field:StartVal -> BucketBlob) + MDBX_dbi inverted_dbi_; // BucketKey -> BucketBlob - // Helper to format keys std::string make_forward_key(const std::string& field, ndd::idInt id) { return field + ":" + std::to_string(id); } + // Key Format: [Field]:[BigEndian_BaseValue] std::string make_bucket_key(const std::string& field, uint32_t start_val) { - // Big-endian for sorting uint32_t be_val = 0; #if defined(__GNUC__) || defined(__clang__) be_val = __builtin_bswap32(start_val); #else - // Fallback for MSVC or others be_val = ((start_val >> 24) & 0xff) | ((start_val << 8) & 0xff0000) | ((start_val >> 8) & 0xff00) | ((start_val << 24) & 0xff000000); #endif - std::string key = field + ":"; key.append((char*)&be_val, 4); return key; } - // Helper to parse bucket key uint32_t parse_bucket_key_val(const std::string& key) { - if(key.size() < 4) { - return 0; - } + if (key.size() < 4) return 0; uint32_t be_val; std::memcpy(&be_val, key.data() + key.size() - 4, 4); #if defined(__GNUC__) || defined(__clang__) @@ -172,25 +241,13 @@ namespace ndd { } public: - NumericIndex(MDBX_env* env) : - env_(env) { + NumericIndex(MDBX_env* env) : env_(env) { MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); - if(rc != MDBX_SUCCESS) { - throw std::runtime_error("Failed to begin txn for NumericIndex init"); - } - - rc = mdbx_dbi_open(txn, "numeric_forward", MDBX_CREATE, &forward_dbi_); - if(rc != MDBX_SUCCESS) { - throw std::runtime_error("Failed to open numeric_forward dbi"); - } - - rc = mdbx_dbi_open(txn, "numeric_inverted", MDBX_CREATE, &inverted_dbi_); - if(rc != MDBX_SUCCESS) { - throw std::runtime_error("Failed to open numeric_inverted dbi"); + if (mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn) == MDBX_SUCCESS) { + mdbx_dbi_open(txn, "numeric_forward", MDBX_CREATE, &forward_dbi_); + mdbx_dbi_open(txn, "numeric_inverted", MDBX_CREATE, &inverted_dbi_); + mdbx_txn_commit(txn); } - - mdbx_txn_commit(txn); } void put(const std::string& field, ndd::idInt id, uint32_t value) { @@ -205,33 +262,6 @@ namespace ndd { } } - void - put_internal(MDBX_txn* txn, const std::string& field, ndd::idInt id, uint32_t value) { - // 1. Check Forward Index for existing value (Update case) - std::string fwd_key_str = make_forward_key(field, id); - MDBX_val fwd_key{const_cast(fwd_key_str.data()), fwd_key_str.size()}; - MDBX_val fwd_val; - - int rc = mdbx_get(txn, forward_dbi_, &fwd_key, &fwd_val); - if(rc == MDBX_SUCCESS) { - uint32_t old_val; - std::memcpy(&old_val, fwd_val.iov_base, 4); - if(old_val == value) { - return; // No change - } - - // Remove from old bucket - remove_from_bucket(txn, field, old_val, id); - } - - // 2. Update Forward Index - MDBX_val new_val_data{&value, sizeof(uint32_t)}; - mdbx_put(txn, forward_dbi_, &fwd_key, &new_val_data, MDBX_UPSERT); - - // 3. Add to Inverted Index (Buckets) - add_to_bucket(txn, field, value, id); - } - void remove(const std::string& field, ndd::idInt id) { MDBX_txn* txn; mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); @@ -240,17 +270,13 @@ namespace ndd { MDBX_val fwd_key{const_cast(fwd_key_str.data()), fwd_key_str.size()}; MDBX_val fwd_val; - int rc = mdbx_get(txn, forward_dbi_, &fwd_key, &fwd_val); - if(rc == MDBX_SUCCESS) { + if(mdbx_get(txn, forward_dbi_, &fwd_key, &fwd_val) == MDBX_SUCCESS) { uint32_t old_val; - std::memcpy(&old_val, fwd_val.iov_base, 4); - - // Remove from bucket - remove_from_bucket(txn, field, old_val, id); - - // Remove from forward index + std::memcpy(&old_val, fwd_val.iov_base, sizeof(uint32_t)); + remove_from_buckets(txn, field, old_val, id); mdbx_del(txn, forward_dbi_, &fwd_key, nullptr); } + mdbx_txn_commit(txn); } catch(...) { mdbx_txn_abort(txn); @@ -258,346 +284,365 @@ namespace ndd { } } - ndd::RoaringBitmap range(const std::string& field, uint32_t min_val, uint32_t max_val) { - ndd::RoaringBitmap result; - MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn); - if(rc != MDBX_SUCCESS) { - return result; - } + private: + void put_internal(MDBX_txn* txn, const std::string& field, ndd::idInt id, uint32_t value) { + // 1. Check Forward Index + std::string fwd_key_str = make_forward_key(field, id); + MDBX_val fwd_key{const_cast(fwd_key_str.data()), fwd_key_str.size()}; + MDBX_val fwd_val; - try { - MDBX_cursor* cursor; - mdbx_cursor_open(txn, inverted_dbi_, &cursor); - - // 1. Find start bucket (bucket with start_val <= min_val) - std::string start_key_str = make_bucket_key(field, min_val); - MDBX_val key{const_cast(start_key_str.data()), start_key_str.size()}; - MDBX_val data; - - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); - - bool valid_start = false; - - if(rc == MDBX_SUCCESS) { - std::string found_key((char*)key.iov_base, key.iov_len); - if(found_key.rfind(field + ":", 0) == 0) { - // Found a bucket in the same field - if(found_key > start_key_str) { - // We landed on a bucket starting AFTER min_val. - // Check previous bucket to see if it covers min_val. - MDBX_val p_key = key; - MDBX_val p_data; - int p_rc = mdbx_cursor_get(cursor, &p_key, &p_data, MDBX_PREV); - - if(p_rc == MDBX_SUCCESS) { - std::string prev_key((char*)p_key.iov_base, p_key.iov_len); - if(prev_key.rfind(field + ":", 0) == 0) { - // Previous bucket is in same field, start there - valid_start = true; - // cursor is already at prev - key = p_key; - data = p_data; - } else { - // Previous bucket is different field. - // This means min_val is before the first bucket of this - // field. So we start at the found_key (first bucket). Reset - // cursor to found_key - mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); - valid_start = true; - } - } else { - // No prev, start at found_key - mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); - valid_start = true; - } - } else { - // Exact match on start key - valid_start = true; - } - } else { - // Found key is next field. Go back to see if we have buckets for this - // field. - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_PREV); - if(rc == MDBX_SUCCESS) { - std::string prev_key((char*)key.iov_base, key.iov_len); - if(prev_key.rfind(field + ":", 0) == 0) { - valid_start = true; - } - } - } - } else if(rc == MDBX_NOTFOUND) { - // Try last bucket - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_LAST); - if(rc == MDBX_SUCCESS) { - std::string last_key((char*)key.iov_base, key.iov_len); - if(last_key.rfind(field + ":", 0) == 0) { - valid_start = true; - } - } - } + if (mdbx_get(txn, forward_dbi_, &fwd_key, &fwd_val) == MDBX_SUCCESS) { + uint32_t old_val; + std::memcpy(&old_val, fwd_val.iov_base, 4); + if (old_val == value) return; + remove_from_buckets(txn, field, old_val, id); + } - if(valid_start) { - // Iterate buckets - while(true) { - std::string curr_key((char*)key.iov_base, key.iov_len); - if(curr_key.rfind(field + ":", 0) != 0) { - break; // End of field - } - - uint32_t bucket_start = parse_bucket_key_val(curr_key); - if(bucket_start > max_val) { - break; // Bucket starts after range - } - - // Deserialize and scan - Bucket bucket = Bucket::deserialize(data.iov_base, data.iov_len); - for(const auto& entry : bucket.entries) { - if(entry.first >= min_val && entry.first <= max_val) { - result.add(entry.second); - } - } - - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); - if(rc != MDBX_SUCCESS) { - break; - } - } - } + // 2. Update Forward + MDBX_val new_val_data{&value, sizeof(uint32_t)}; + mdbx_put(txn, forward_dbi_, &fwd_key, &new_val_data, MDBX_UPSERT); - mdbx_cursor_close(cursor); - mdbx_txn_abort(txn); // Read-only - } catch(...) { - mdbx_txn_abort(txn); - throw; - } - return result; + // 3. Add to Inverted Buckets + add_to_buckets(txn, field, value, id); } - // Check if ID has value in range [min_val, max_val] using Forward Index - bool check_range(const std::string& field, - ndd::idInt id, - uint32_t min_val, - uint32_t max_val) { - MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn); - if(rc != MDBX_SUCCESS) { - return false; - } - - try { - std::string fwd_key_str = make_forward_key(field, id); - MDBX_val fwd_key{const_cast(fwd_key_str.data()), fwd_key_str.size()}; - MDBX_val fwd_val; + void remove_from_buckets(MDBX_txn* txn, const std::string& field, uint32_t value, ndd::idInt id) { + // Find bucket + std::string bkey_str = make_bucket_key(field, value); + MDBX_val key{const_cast(bkey_str.data()), bkey_str.size()}; + MDBX_val data; + MDBX_cursor* cursor; + mdbx_cursor_open(txn, inverted_dbi_, &cursor); - rc = mdbx_get(txn, forward_dbi_, &fwd_key, &fwd_val); - bool match = false; - if(rc == MDBX_SUCCESS) { - uint32_t val; - std::memcpy(&val, fwd_val.iov_base, 4); - if(val >= min_val && val <= max_val) { - match = true; - } + // Scan backward to find bucket covering 'value' + int rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); + + // Logic to find correct bucket: + std::string found_key; + + if (rc == MDBX_SUCCESS) { + found_key = std::string((char*)key.iov_base, key.iov_len); + // Check if we are in right field & range + if (found_key.rfind(field + ":", 0) != 0 || parse_bucket_key_val(found_key) > value) { + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_PREV); } - mdbx_txn_abort(txn); - return match; - } catch(...) { - mdbx_txn_abort(txn); - throw; + } else if (rc == MDBX_NOTFOUND) { + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_LAST); + } + + // Should be at correct bucket now + if (rc == MDBX_SUCCESS) { + found_key = std::string((char*)key.iov_base, key.iov_len); + if (found_key.rfind(field + ":", 0) == 0) { + uint32_t bucket_base = parse_bucket_key_val(found_key); + if (value >= bucket_base) { + Bucket b = Bucket::deserialize(data.iov_base, data.iov_len, bucket_base); + if (b.remove(id)) { + // Save back or Delete if empty + if (b.is_empty()) { + mdbx_cursor_del(cursor, static_cast(0)); + } else { + auto bytes = b.serialize(); + MDBX_val new_data{bytes.data(), bytes.size()}; + mdbx_cursor_put(cursor, &key, &new_data, MDBX_CURRENT); + } + } + } + } } + mdbx_cursor_close(cursor); } - private: - void - add_to_bucket(MDBX_txn* txn, const std::string& field, uint32_t value, ndd::idInt id) { - // Find the bucket that starts <= value - // We search for key = field:value. If exact match, good. - // If not, we go to the previous key (MDBX_SET_RANGE returns >=, so we might need - // prev) - - std::string target_key = make_bucket_key(field, value); - MDBX_val key{const_cast(target_key.data()), target_key.size()}; - MDBX_val data; + void add_to_buckets(MDBX_txn* txn, const std::string& field, uint32_t value, ndd::idInt id) { MDBX_cursor* cursor; mdbx_cursor_open(txn, inverted_dbi_, &cursor); - int rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); + // Find candidate bucket + std::string search_key = make_bucket_key(field, value); + MDBX_val key{const_cast(search_key.data()), search_key.size()}; + MDBX_val data; - bool found_bucket = false; - std::string bucket_key_str; + int rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); + + bool create_new = false; + std::string target_key_str; + uint32_t target_base = 0; + + // Move logic to find predecessor + if (rc == MDBX_SUCCESS) { + std::string found_key((char*)key.iov_base, key.iov_len); + if (found_key.rfind(field + ":", 0) != 0 || parse_bucket_key_val(found_key) > value) { + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_PREV); + } + } else { + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_LAST); + } - if(rc == MDBX_SUCCESS) { - // We found a key >= target. - // Check if it belongs to the same field + if (rc == MDBX_SUCCESS) { std::string found_key((char*)key.iov_base, key.iov_len); - if(found_key.rfind(field + ":", 0) == 0) { - // Same field. - // If found_key > target_key, we might need the PREVIOUS bucket - // unless this is the very first bucket and value < found_key's start - // (shouldn't happen if we maintain logic) Actually, we want the bucket - // where start_val <= value. - - if(found_key > target_key) { - // Go back one - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_PREV); - if(rc == MDBX_SUCCESS) { - std::string prev_key((char*)key.iov_base, key.iov_len); - if(prev_key.rfind(field + ":", 0) == 0) { - // Found valid previous bucket - bucket_key_str = prev_key; - found_bucket = true; - } - } + if (found_key.rfind(field + ":", 0) == 0) { + target_base = parse_bucket_key_val(found_key); + // Check range condition + if (value >= target_base && (static_cast(value) - target_base) <= Bucket::MAX_DELTA) { + target_key_str = found_key; } else { - // Exact match - bucket_key_str = found_key; - found_bucket = true; + create_new = true; } } else { - // Found key is for next field, go back - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_PREV); - if(rc == MDBX_SUCCESS) { - std::string prev_key((char*)key.iov_base, key.iov_len); - if(prev_key.rfind(field + ":", 0) == 0) { - bucket_key_str = prev_key; - found_bucket = true; - } - } - } - } else if(rc == MDBX_NOTFOUND) { - // No key >= target. Try last key. - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_LAST); - if(rc == MDBX_SUCCESS) { - std::string last_key((char*)key.iov_base, key.iov_len); - if(last_key.rfind(field + ":", 0) == 0) { - bucket_key_str = last_key; - found_bucket = true; - } + create_new = true; } - } - - Bucket bucket; - if(found_bucket) { - // Load existing bucket - // Note: cursor is already at the key if we didn't move it? - // Actually we moved it around. Let's just get by key to be safe/simple - MDBX_val b_key{const_cast(bucket_key_str.data()), bucket_key_str.size()}; - mdbx_get(txn, inverted_dbi_, &b_key, &data); - bucket = Bucket::deserialize(data.iov_base, data.iov_len); } else { - // No bucket exists for this field yet. Create new one starting at 'value' - // Actually, let's start at 0 or min possible? - // Better: Start at 'value' for the first bucket. - bucket_key_str = make_bucket_key(field, value); + create_new = true; } - bucket.add(value, id); - - if(bucket.is_full()) { - // Split! - Bucket new_bucket = bucket.split(); - uint32_t new_start = new_bucket.min_val(); - - // Save old bucket - auto bytes = bucket.serialize(); - MDBX_val b_key{const_cast(bucket_key_str.data()), bucket_key_str.size()}; - MDBX_val b_val{bytes.data(), bytes.size()}; - mdbx_put(txn, inverted_dbi_, &b_key, &b_val, MDBX_put_flags_t(0)); - - // Save new bucket - std::string new_key_str = make_bucket_key(field, new_start); - auto new_bytes = new_bucket.serialize(); - MDBX_val nb_key{const_cast(new_key_str.data()), new_key_str.size()}; - MDBX_val nb_val{new_bytes.data(), new_bytes.size()}; - mdbx_put(txn, inverted_dbi_, &nb_key, &nb_val, MDBX_put_flags_t(0)); - + if (create_new) { + // Create new bucket at exact value + Bucket b; + b.base_value = value; + b.add(value, id); + auto bytes = b.serialize(); + + target_key_str = make_bucket_key(field, value); + MDBX_val k{const_cast(target_key_str.data()), target_key_str.size()}; + MDBX_val v{bytes.data(), bytes.size()}; + mdbx_put(txn, inverted_dbi_, &k, &v, MDBX_UPSERT); + } else { - // Just save - auto bytes = bucket.serialize(); - MDBX_val b_key{const_cast(bucket_key_str.data()), bucket_key_str.size()}; - MDBX_val b_val{bytes.data(), bytes.size()}; - mdbx_put(txn, inverted_dbi_, &b_key, &b_val, MDBX_put_flags_t(0)); - } + // Update existing + // We must re-fetch current key/data because cursor move might have updated key/data + MDBX_val k{const_cast(target_key_str.data()), target_key_str.size()}; + MDBX_val v; + if(mdbx_cursor_get(cursor, &k, &v, MDBX_SET) != MDBX_SUCCESS) { + // Should not happen if logic is correct + throw std::runtime_error("Cursor sync fail"); + } + + Bucket b = Bucket::deserialize(v.iov_base, v.iov_len, target_base); + + // Capacity Check + if (b.ids.size() >= Bucket::MAX_SIZE) { + // SPLIT LOGIC + // Sort is maintained by arrays. + // "Slide Split": Scan right from median + size_t mid_idx = b.ids.size() / 2; + + // Ensure we don't split a group of identical values + size_t probe_right = mid_idx; + while (probe_right < b.deltas.size() && probe_right > 0 && b.deltas[probe_right] == b.deltas[probe_right - 1]) { + probe_right++; + } + + if (probe_right < b.deltas.size()) { + mid_idx = probe_right; + } else { + // Fallback: Try scanning left + size_t probe_left = mid_idx; + while (probe_left > 0 && b.deltas[probe_left] == b.deltas[probe_left - 1]) { + probe_left--; + } + + if (probe_left > 0) { + mid_idx = probe_left; + } else { + // All identical + mid_idx = b.deltas.size(); + } + } + + // If we hit end, we can't split by value uniqueness + if (mid_idx == b.deltas.size()) { + // Fallback: Just append (overfill) or implement logic to handle identicals. + // For now: Append + b.add(value, id); + auto bytes = b.serialize(); + MDBX_val k2{const_cast(target_key_str.data()), target_key_str.size()}; + MDBX_val v2{bytes.data(), bytes.size()}; + mdbx_cursor_put(cursor, &k2, &v2, MDBX_CURRENT); + mdbx_cursor_close(cursor); + return; + } + + // Standard Slide Split + Bucket right_b; + right_b.base_value = b.base_value + b.deltas[mid_idx]; // New base + + // Move entries + for(size_t i=mid_idx; i= right_b.base_value) { + right_b.add(value, id); + } else { + // If value < right, goes to left. + // But wait, split point was determined by existing items. + // If new value is >= base+split_delta, it goes right. + // BUT we just cleared right from b. + // Correct logic: + b.add(value, id); // Add to left if it fits range (logic handles delta) + // Oh wait, if we added to left, we might overflow again or break order? + // Simply: Check which bucket covers it. + // Left covers [Base, RightBase-1] + // Right covers [RightBase, ...] + } + + // Save Left + auto left_bytes = b.serialize(); + MDBX_val left_v{left_bytes.data(), left_bytes.size()}; + MDBX_val left_k{const_cast(target_key_str.data()), target_key_str.size()}; + mdbx_cursor_put(cursor, &left_k, &left_v, MDBX_CURRENT); + + // Save Right + auto right_bytes = right_b.serialize(); + std::string right_k_str = make_bucket_key(field, right_b.base_value); + MDBX_val right_k{const_cast(right_k_str.data()), right_k_str.size()}; + MDBX_val right_v{right_bytes.data(), right_bytes.size()}; + + // Use put for new key + mdbx_put(txn, inverted_dbi_, &right_k, &right_v, MDBX_UPSERT); + } else { + // Normal Insert + b.add(value, id); + auto bytes = b.serialize(); + MDBX_val new_data{bytes.data(), bytes.size()}; + + // Use cursor put to update current + mdbx_cursor_put(cursor, &k, &new_data, MDBX_CURRENT); + } + } mdbx_cursor_close(cursor); } - void remove_from_bucket(MDBX_txn* txn, - const std::string& field, - uint32_t value, - ndd::idInt id) { - // Find bucket - std::string target_key = make_bucket_key(field, value); - MDBX_val key{const_cast(target_key.data()), target_key.size()}; - MDBX_val data; + public: + ndd::RoaringBitmap range(const std::string& field, uint32_t min_val, uint32_t max_val) { + ndd::RoaringBitmap result; + MDBX_txn* txn; + if (mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn) != MDBX_SUCCESS) return result; + MDBX_cursor* cursor; mdbx_cursor_open(txn, inverted_dbi_, &cursor); - int rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); + // 1. Find Start Bucket + std::string start_k = make_bucket_key(field, min_val); + MDBX_val key{const_cast(start_k.data()), start_k.size()}; + MDBX_val data; - std::string bucket_key_str; - bool found = false; + int rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); + if (rc == MDBX_SUCCESS) { + // Check if we need to back up + std::string fkey((char*)key.iov_base, key.iov_len); + if (fkey.rfind(field + ":", 0) != 0 || parse_bucket_key_val(fkey) > min_val) { + // Check prev + MDBX_val p_key = key; + MDBX_val p_data; + if (mdbx_cursor_get(cursor, &p_key, &p_data, MDBX_PREV) == MDBX_SUCCESS) { + std::string pkey_str((char*)p_key.iov_base, p_key.iov_len); + if (pkey_str.rfind(field + ":", 0) == 0) { + // Prev is valid start + key = p_key; data = p_data; + rc = MDBX_SUCCESS; + } + } + } + } else if (rc == MDBX_NOTFOUND) { + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_LAST); + if (rc == MDBX_SUCCESS && data.iov_len > 0) { + std::string fkey((char*)key.iov_base, key.iov_len); + if (fkey.rfind(field + ":", 0) == 0) { + rc = MDBX_SUCCESS; + } else { + rc = MDBX_NOTFOUND; + } + } else { + rc = MDBX_NOTFOUND; + } + } - // Same logic as add_to_bucket to find the correct bucket - if(rc == MDBX_SUCCESS) { - std::string found_key((char*)key.iov_base, key.iov_len); - if(found_key.rfind(field + ":", 0) == 0) { - if(found_key > target_key) { - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_PREV); - if(rc == MDBX_SUCCESS) { - std::string prev_key((char*)key.iov_base, key.iov_len); - if(prev_key.rfind(field + ":", 0) == 0) { - bucket_key_str = prev_key; - found = true; - } - } - } else { - bucket_key_str = found_key; - found = true; - } - } else { - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_PREV); - if(rc == MDBX_SUCCESS) { - std::string prev_key((char*)key.iov_base, key.iov_len); - if(prev_key.rfind(field + ":", 0) == 0) { - bucket_key_str = prev_key; - found = true; - } - } - } - } else if(rc == MDBX_NOTFOUND) { - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_LAST); - if(rc == MDBX_SUCCESS) { - std::string last_key((char*)key.iov_base, key.iov_len); - if(last_key.rfind(field + ":", 0) == 0) { - bucket_key_str = last_key; - found = true; - } + // Iterate forward + while (rc == MDBX_SUCCESS) { + std::string cur_key((char*)key.iov_base, key.iov_len); + if (cur_key.rfind(field + ":", 0) != 0) break; // End of field + + uint32_t bucket_base = parse_bucket_key_val(cur_key); + + if (bucket_base > max_val) break; // Past the end + + // Peek Strategy: + // If bucket_base >= min_val, we know the start is covered. + // If we could know NEXT bucket start, we'd know overlap. + // Since we iterate, we can be greedy on read. + + // For now, always deserialize. + // Potential optimization: Read only bitmap if we are "deep" in the range. + // e.g. min_val=10, max_val=100. Bucket=20. + // If bucket=20. Next Bucket=30. + // Then Bucket 20 covers [20..30). + // Range [10..100] covers [20..30] fully. + // So we need lookahead. + + // Simple logic without lookahead: + // Just read full bucket. It's 8KB max (2 pages). + // It's fast unless we have millions of buckets. + + Bucket b = Bucket::deserialize(data.iov_base, data.iov_len, bucket_base); + + if (b.ids.empty()) { + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); + continue; } - } - if(found) { - // Reload data to be sure - MDBX_val b_key{const_cast(bucket_key_str.data()), bucket_key_str.size()}; - mdbx_get(txn, inverted_dbi_, &b_key, &data); + uint32_t b_min = b.get_value(0); + uint32_t b_max = b.get_value(b.ids.size()-1); - Bucket bucket = Bucket::deserialize(data.iov_base, data.iov_len); - if(bucket.remove(id)) { - if(bucket.is_empty()) { - // Delete bucket - mdbx_del(txn, inverted_dbi_, &b_key, nullptr); - } else { - // Save updated bucket - auto bytes = bucket.serialize(); - MDBX_val b_val{bytes.data(), bytes.size()}; - mdbx_put(txn, inverted_dbi_, &b_key, &b_val, MDBX_put_flags_t(0)); - } + if (b_min >= min_val && b_max <= max_val) { + // Full overlap + result |= b.summary_bitmap; + } else { + // Partial overlap + for(size_t i=0; i= min_val && v <= max_val) { + result.add(b.ids[i]); + } + } } + + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); } mdbx_cursor_close(cursor); + mdbx_txn_abort(txn); + return result; + } + + bool check_range(const std::string& field, ndd::idInt id, uint32_t min_val, uint32_t max_val) { + MDBX_txn* txn; + if(mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn) != MDBX_SUCCESS) return false; + + std::string fwd_key_str = make_forward_key(field, id); + MDBX_val fwd_key{const_cast(fwd_key_str.data()), fwd_key_str.size()}; + MDBX_val fwd_val; + + bool match = false; + if(mdbx_get(txn, forward_dbi_, &fwd_key, &fwd_val) == MDBX_SUCCESS) { + uint32_t val; + std::memcpy(&val, fwd_val.iov_base, 4); + if(val >= min_val && val <= max_val) match = true; + } + + mdbx_txn_abort(txn); + return match; } }; - } // namespace numeric -} // namespace ndd \ No newline at end of file + } // namespace filter +} // namespace ndd diff --git a/src/hnsw/hnswalg.h b/src/hnsw/hnswalg.h index bd441e4cb1..17aa5caf48 100644 --- a/src/hnsw/hnswalg.h +++ b/src/hnsw/hnswalg.h @@ -19,6 +19,7 @@ #include #include #include +#include namespace hnswlib { @@ -208,11 +209,13 @@ namespace hnswlib { // Cache management getters/setters // Removed as cache is managed externally + template std::vector> searchKnn(const void* query_data, size_t k, - size_t ef = settings::DEFAULT_EF_SEARCH, - BaseFilterFunctor* isIdAllowed = nullptr) const override { + size_t ef, + FilterFunctor* isIdAllowed, + size_t filter_boost_percentage = settings::FILTER_BOOST_PERCENTAGE) const { // Default true as requested int x = 0; LOG_DEBUG("Inside searchKnn, element count: " << curElementsCount_); std::vector> result; @@ -241,7 +244,7 @@ namespace hnswlib { dist_t s; // Upper layer traversal - greedy search - for(levelInt level = maxLevel_; level > 0; level--) { + for(levelInt level = maxLevel_; level > 1; level--) { bool changed = true; while(changed) { changed = false; @@ -277,14 +280,31 @@ namespace hnswlib { } } + std::vector entry_points; + if (maxLevel_ > 0) { + std::vector l1_eps = {currObj}; + std::vector> l1_res; + if(deletedElementsCount_) { + l1_res = searchBaseLayer(l1_eps, query_data, 1, M_, isIdAllowed, filter_boost_percentage); + } else { + l1_res = searchBaseLayer(l1_eps, query_data, 1, M_, isIdAllowed, filter_boost_percentage); + } + + for(size_t i = 0; i < std::min((size_t)2, l1_res.size()); ++i) { + entry_points.push_back(l1_res[i].second); + } + } else { + entry_points.push_back(entryPoint_); + } + std::vector> top_candidates; - LOG_DEBUG("Starting search in level 0..current object " << currObj); + LOG_DEBUG("Starting search in level 0.."); if(deletedElementsCount_) { - top_candidates = searchBaseLayer( - currObj, query_data, 0, std::max(ef, k)); // Level 0 for final search + top_candidates = searchBaseLayer( + entry_points, query_data, 0, std::max(ef, k), isIdAllowed, filter_boost_percentage); // Level 0 for final search } else { - top_candidates = searchBaseLayer( - currObj, query_data, 0, std::max(ef, k)); // Level 0 for final search + top_candidates = searchBaseLayer( + entry_points, query_data, 0, std::max(ef, k), isIdAllowed, filter_boost_percentage); // Level 0 for final search } LOG_DEBUG("Search in level 0 completed. Found " << top_candidates.size() << " candidates"); @@ -297,6 +317,19 @@ namespace hnswlib { return result; } + std::vector> + searchKnn(const void* query_data, + size_t k, + size_t ef, + BaseFilterFunctor* isIdAllowed = nullptr, + size_t filter_boost_percentage = settings::FILTER_BOOST_PERCENTAGE) const override { + if (isIdAllowed) { + return searchKnn(query_data, k, ef, isIdAllowed, filter_boost_percentage); + } else { + return searchKnn(query_data, k, ef, nullptr, filter_boost_percentage); + } + } + void saveIndex(const std::string& location) override { // Lock the index so that addPoint and markDelete are not called std::unique_lock lock(index_lock_); @@ -644,12 +677,13 @@ namespace hnswlib { const void* level_datapoint = (level == 0) ? datapoint : datapoint_upper.data(); + std::vector cur_eps = {currObj}; if(deletedElementsCount_) { sorted_candidates = searchBaseLayer( - currObj, level_datapoint, level, efConstruction_); + cur_eps, level_datapoint, level, efConstruction_); } else { // No deleted elements sorted_candidates = searchBaseLayer( - currObj, level_datapoint, level, efConstruction_); + cur_eps, level_datapoint, level, efConstruction_); } currObj = mutuallyConnectNewElement( level_datapoint, cur_c, sorted_candidates, level); @@ -1060,9 +1094,14 @@ namespace hnswlib { // Search function for the base layer // Returns a vector of top candidates sorted by similarity (1-distance) in reverse order - template + template std::vector> - searchBaseLayer(idhInt ep_id, const void* data_point, idhInt layer, size_t ef) const { + searchBaseLayer(const std::vector& ep_ids, + const void* data_point, + idhInt layer, + size_t ef, + FilterFunctor* filter = nullptr, + size_t filter_boost_percentage = settings::FILTER_BOOST_PERCENTAGE) const { LOG_TIME("searchBaseLayer"); VisitedList* vl = visited_list_pool_->getFreeVisitedList(); vl_type* visited_array = vl->mass; @@ -1080,39 +1119,85 @@ namespace hnswlib { buffer.resize(curDataSize); } - dist_t lowerBound; - if(!has_deletions || !isMarkedDeleted(ep_id)) { + size_t dist_computations = 0; + dist_t lowerBound = std::numeric_limits::lowest(); - const void* vec_data = nullptr; - if(layer == 0) { - if(getDataByInternalId(ep_id, layer, buffer.data())) { - vec_data = buffer.data(); - } - } else { - vec_data = getUpperLayerDataPtr(ep_id); + for (idhInt ep_id : ep_ids) { + if (visited_array[ep_id] == visited_array_tag) { + continue; } + visited_array[ep_id] = visited_array_tag; - if(vec_data) { - dist_t sim = curSimFunc(data_point, vec_data, curDistParam); + dist_t sim = std::numeric_limits::lowest(); + if(!has_deletions || !isMarkedDeleted(ep_id)) { + const void* vec_data = nullptr; + if(layer == 0) { + if(getDataByInternalId(ep_id, layer, buffer.data())) { + vec_data = buffer.data(); + } + } else { + vec_data = getUpperLayerDataPtr(ep_id); + } - top_candidates.emplace(sim, ep_id); - candidate_set.emplace(sim, ep_id); - lowerBound = sim; + if(vec_data) { + sim = curSimFunc(data_point, vec_data, curDistParam); + dist_computations++; + + if constexpr(std::is_same_v) { + top_candidates.emplace(sim, ep_id); + candidate_set.emplace(sim, ep_id); + } else if constexpr(std::is_same_v) { + // Virtual call path + bool allowed = !filter || (*filter)(getExternalLabel(ep_id)); + candidate_set.emplace(sim, ep_id); // Always explore + if (allowed) { + top_candidates.emplace(sim, ep_id); + } + } else { + // Templated path + bool allowed = !filter || (*filter)(getExternalLabel(ep_id)); + candidate_set.emplace(sim, ep_id); + if (allowed) { + top_candidates.emplace(sim, ep_id); + } + } + + // Maintain ef size in top_candidates during init + if(top_candidates.size() > ef) { + top_candidates.pop(); + } + } else { + // Data fetch failed + candidate_set.emplace(std::numeric_limits::lowest(), ep_id); + } } else { - lowerBound = std::numeric_limits::lowest(); - candidate_set.emplace(lowerBound, ep_id); + // Deleted + candidate_set.emplace(std::numeric_limits::lowest(), ep_id); } - } else { - // If entry point is deleted, lower bound will be minimum - lowerBound = std::numeric_limits::lowest(); - candidate_set.emplace(lowerBound, ep_id); } - visited_array[ep_id] = visited_array_tag; + if (!top_candidates.empty()) { + lowerBound = top_candidates.top().first; + } + int below_threshold_count = 0; int max_below_threshold = is_insert ? settings::EARLY_EXIT_BUFFER_INSERT : settings::EARLY_EXIT_BUFFER_QUERY; + // Progressive Fatigue Logic: + + // Base budget: ef * M . + size_t fatigue_base = ef * M_; + + // Apply filter boost if filter is active + if constexpr(!std::is_same_v) { + if (filter != nullptr && filter_boost_percentage > 0) { + fatigue_base = fatigue_base * (100 + filter_boost_percentage) / 100; + } + } + + size_t fatigue_tail = fatigue_base * 5; // Taper duration + while(!candidate_set.empty()) { auto current_pair = candidate_set.top(); idhInt current_id = current_pair.second; @@ -1162,19 +1247,59 @@ namespace hnswlib { continue; } + // Check filter BEFORE computing distance + // Treats filtered nodes as non-existent (traverses a subgraph) + bool pass_filter = true; + if constexpr(!std::is_same_v) { + if (filter != nullptr) { + if (!(*filter)(getExternalLabel(candidate_id))) { + pass_filter = false; + } + } + } + + if(!pass_filter) { + // Check Fatigue + if (dist_computations > fatigue_base) { + // We are in the tapering region + // Linearly increase drop probability from 0% to 100%. + + size_t excess = dist_computations - fatigue_base; + + if (excess >= fatigue_tail) { + continue; // 100% drop (Hard Cap exceeded) + } + + // Prob = Excess / Tail_Length + size_t drop_prob = (excess * 255) / fatigue_tail; + + size_t hash = (candidate_id * 104729) & 0xFF; + if (hash < drop_prob) continue; + } + + // Explore + sim = curSimFunc(data_point, neighbor_data, curDistParam); + dist_computations++; + if (top_candidates.size() < ef || sim > lowerBound) { + candidate_set.emplace(sim, candidate_id); + } + continue; + } + sim = curSimFunc(data_point, neighbor_data, curDistParam); + dist_computations++; // Count valid computations too if(top_candidates.size() < ef || sim > lowerBound) { candidate_set.emplace(sim, candidate_id); if(!has_deletions || !isMarkedDeleted(candidate_id)) { - top_candidates.emplace(sim, candidate_id); - if(top_candidates.size() > ef) { - top_candidates.pop(); - } - if(!top_candidates.empty()) { - lowerBound = top_candidates.top().first; - } + top_candidates.emplace(sim, candidate_id); + if(top_candidates.size() > ef) { + top_candidates.pop(); + } + if(!top_candidates.empty()) { + lowerBound = top_candidates.top().first; + } } } } diff --git a/src/hnsw/hnswlib.h b/src/hnsw/hnswlib.h index b7122dc173..38d19a6dd5 100644 --- a/src/hnsw/hnswlib.h +++ b/src/hnsw/hnswlib.h @@ -229,7 +229,7 @@ namespace hnswlib { //virtual void addPoint(const void *datapoint, ndd::idInt label) = 0; virtual std::vector> - searchKnn(const void*, size_t, size_t, BaseFilterFunctor* isIdAllowed = nullptr) const = 0; + searchKnn(const void*, size_t, size_t, BaseFilterFunctor* isIdAllowed = nullptr, size_t filter_boost_percentage = settings::FILTER_BOOST_PERCENTAGE) const = 0; virtual void saveIndex(const std::string& location) = 0; virtual ~AlgorithmInterface() {} diff --git a/src/main.cpp b/src/main.cpp index cf29bd03e6..40ae26b866 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -708,6 +708,19 @@ int main(int argc, char** argv) { return json_error(400, std::string("Invalid filter JSON: ") + e.what()); } } + + // Extract filter parameters (Option B from chat plan) + ndd::FilterParams filter_params; + if (body.has("filter_params")) { + auto fp = body["filter_params"]; + if (fp.has("prefilter_threshold")) { + filter_params.prefilter_threshold = static_cast(fp["prefilter_threshold"].i()); + } + if (fp.has("boost_percentage")) { + filter_params.boost_percentage = static_cast(fp["boost_percentage"].i()); + } + } + LOG_DEBUG("Filter: " << filter_array.dump()); try { auto search_response = index_manager.searchKNN(index_id, @@ -716,6 +729,7 @@ int main(int argc, char** argv) { sparse_values, k, filter_array, + filter_params, include_vectors, ef); if(!search_response) { diff --git a/src/sparse/bmw.hpp b/src/sparse/bmw.hpp index 8300f5f100..a6045098ba 100644 --- a/src/sparse/bmw.hpp +++ b/src/sparse/bmw.hpp @@ -21,6 +21,7 @@ #include #include #include +#include "../core/types.hpp" #if defined(__x86_64__) || defined(_M_X64) # include @@ -241,7 +242,7 @@ namespace ndd { } // Search using BMW algorithm (DAAT) - std::vector> search(const SparseVector& query, size_t k) { + std::vector> search(const SparseVector& query, size_t k, const ndd::RoaringBitmap* filter = nullptr) { if(query.empty() || k == 0) { return {}; } @@ -331,6 +332,20 @@ namespace ndd { ndd::idInt pivot_doc_id = iterators[pivot_idx]->current_doc_id; if(iterators[0]->current_doc_id == pivot_doc_id) { + if(filter && !filter->contains(pivot_doc_id)) { + // Skip document that doesn't match filter + iterators[0]->next(); + for(size_t i = 1; i < iterators.size(); ++i) { + if(iterators[i]->current_doc_id == pivot_doc_id) { + iterators[i]->next(); + } else { + break; // Since sorted + } + } + sort_iterators(); + continue; + } + // Pivot is the first iterator, so we have a candidate iterators[0]->advance(pivot_doc_id); // Should be no-op float score = iterators[0]->current_score * iterators[0]->term_weight; diff --git a/src/sparse/sparse_storage.hpp b/src/sparse/sparse_storage.hpp index eb530fbab2..2ea238171e 100644 --- a/src/sparse/sparse_storage.hpp +++ b/src/sparse/sparse_storage.hpp @@ -253,8 +253,8 @@ namespace ndd { } // Search (delegates to BMW) - std::vector> search(const SparseVector& query, size_t k) { - return bmw_index_->search(query, k); + std::vector> search(const SparseVector& query, size_t k, const ndd::RoaringBitmap* filter = nullptr) { + return bmw_index_->search(query, k, filter); } // Statistics diff --git a/src/utils/settings.hpp b/src/utils/settings.hpp index d8b6fecdbc..56507226b4 100644 --- a/src/utils/settings.hpp +++ b/src/utils/settings.hpp @@ -75,9 +75,8 @@ namespace settings { constexpr int EARLY_EXIT_BUFFER_QUERY = 8; // Pre-filter threshold - use pre-filter when cardinality is below this value - constexpr size_t PREFILTER_CARDINALITY_THRESHOLD = 1000; - // Use pre-filter if post-filter results are poor (less than this ratio of k) - constexpr float PREFILTER_RESULT_RATIO_THRESHOLD = 0.25f; // k/4 + constexpr size_t PREFILTER_CARDINALITY_THRESHOLD = 10'000; + constexpr size_t FILTER_BOOST_PERCENTAGE = 0; //DEFAULT VALUES constexpr size_t DEFAULT_NUM_PARALLEL_INSERTS = 4; @@ -182,6 +181,7 @@ namespace settings { oss << "MAX_ELEMENTS: " << MAX_ELEMENTS << "\n"; oss << "MAX_ELEMENTS_INCREMENT: " << MAX_ELEMENTS_INCREMENT << "\n"; oss << "MAX_ELEMENTS_INCREMENT_TRIGGER: " << MAX_ELEMENTS_INCREMENT_TRIGGER << "\n"; + oss << "PREFILTER_CARDINALITY_THRESHOLD: " << PREFILTER_CARDINALITY_THRESHOLD << "\n"; oss << "NUM_PARALLEL_INSERTS: " << NUM_PARALLEL_INSERTS << "\n"; oss << "NUM_RECOVERY_THREADS: " << NUM_RECOVERY_THREADS << "\n"; oss << "MAX_MEMORY_GB: " << MAX_MEMORY_GB << "\n"; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000000..99c15c743b --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,45 @@ +include(FetchContent) + +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip +) +# For Windows: Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(googletest) + +# Common sources needed for tests +file(GLOB LMDB_SOURCES ${CMAKE_SOURCE_DIR}/third_party/mdbx/*.c) +set(ROARING_SOURCE ${CMAKE_SOURCE_DIR}/third_party/roaring_bitmap/roaring.c) + +# Create the test executable +add_executable(ndd_filter_test filter_test.cpp ${LMDB_SOURCES} ${ROARING_SOURCE}) + +# Link against GTest +target_link_libraries(ndd_filter_test GTest::gtest_main) + +# Set MDBX compile flags (same as main project) +set_source_files_properties(${LMDB_SOURCES} PROPERTIES + COMPILE_FLAGS "-DMDBX_BUILD_SHARED_LIBRARY=0 -DMDBX_BUILD_FLAGS=\\\"NDD_EMBEDDED\\\"" +) + +# Allow tests to include private headers from src/ +target_include_directories(ndd_filter_test PRIVATE + ${CMAKE_SOURCE_DIR}/src + ${CMAKE_SOURCE_DIR}/src/core + ${CMAKE_SOURCE_DIR}/src/filter + ${CMAKE_SOURCE_DIR}/src/utils + ${CMAKE_SOURCE_DIR}/third_party + ${CMAKE_SOURCE_DIR}/third_party/json +) +# Add other necessary definitions +target_compile_definitions(ndd_filter_test PRIVATE MDB_MAXKEYSIZE=512) + +# SIMD flags if needed (copying from root CMake might be needed if filter/roaring depends on it, +# but usually for unit tests basic flags or inheriting form parent context helps if we used a lib) +# For now, let's assume standard compilation is enough unless Roaring requires it explicitly. +# Actually Roaring usually does runtime dispatch. + +include(GoogleTest) +gtest_discover_tests(ndd_filter_test) diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000000..a62ef40998 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,19 @@ +# Tests + +This folder contains unit tests for Endee. + +## Build & Run + +From the repository root: + +1. Configure with tests enabled: + - `cmake -S . -B build -DENABLE_TESTING=ON` +2. Build the test target: + - `cmake --build build --target ndd_filter_test` +3. Run: + - `./build/tests/ndd_filter_test` + +## Notes + +- Tests can also be built in a dedicated tests build directory (e.g., `tests/build/`). +- The `tests/build/` directory is ignored by git. diff --git a/tests/filter_test.cpp b/tests/filter_test.cpp new file mode 100644 index 0000000000..101be3403e --- /dev/null +++ b/tests/filter_test.cpp @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include "filter/filter.hpp" +#include "json/nlohmann_json.hpp" +#include "filter/numeric_index.hpp" // For Bucket test + +namespace fs = std::filesystem; +using json = nlohmann::json; + +TEST(BucketTest, Serialization) { + ndd::filter::Bucket b; + b.base_value = 100; + b.add(105, 1); + b.add(110, 2); + + auto bytes = b.serialize(); + EXPECT_GT(bytes.size(), 6); + + auto b2 = ndd::filter::Bucket::deserialize(bytes.data(), bytes.size(), 100); + EXPECT_EQ(b2.ids.size(), 2); + EXPECT_EQ(b2.ids[0], 1); + EXPECT_EQ(b2.ids[1], 2); +} + +class FilterTest : public ::testing::Test { +protected: + std::string db_path; + std::unique_ptr filter; + + void SetUp() override { + // Create a unique temporary directory for each test + db_path = "./test_db_" + std::to_string(rand()); + if (fs::exists(db_path)) { + fs::remove_all(db_path); + } + + // Initialize Filter + filter = std::make_unique(db_path); + } + + void TearDown() override { + // Clean up + filter.reset(); // Close DB environment first + if (fs::exists(db_path)) { + fs::remove_all(db_path); + } + } +}; + +TEST_F(FilterTest, CategoryFilterBasics) { + // Add simple category filters + // ID 1: City=Paris + // ID 2: City=London + // ID 3: City=Paris + + filter->add_to_filter("city", "Paris", 1); + filter->add_to_filter("city", "London", 2); + filter->add_to_filter("city", "Paris", 3); + + // Query for City=Paris + json query = json::array({ + {{"city", {{"$eq", "Paris"}}}} + }); + + std::vector ids = filter->getIdsMatchingFilter(query); + + // Should find 1 and 3 + EXPECT_EQ(ids.size(), 2); + EXPECT_NE(std::find(ids.begin(), ids.end(), 1), ids.end()); + EXPECT_NE(std::find(ids.begin(), ids.end(), 3), ids.end()); + EXPECT_EQ(std::find(ids.begin(), ids.end(), 2), ids.end()); +} + +TEST_F(FilterTest, BooleanFilterBasics) { + // Boolean is just a special category "0" or "1" + // ID 10: Active=true + // ID 11: Active=false + + // Using JSON add interface for variety + filter->add_filters_from_json(10, R"({"is_active": true})"); + filter->add_filters_from_json(11, R"({"is_active": false})"); + + // Query Active=true + json query_true = json::array({ + {{"is_active", {{"$eq", true}}}} + }); + + auto ids_true = filter->getIdsMatchingFilter(query_true); + EXPECT_EQ(ids_true.size(), 1); + EXPECT_EQ(ids_true[0], 10); + + // Query Active=false + json query_false = json::array({ + {{"is_active", {{"$eq", false}}}} + }); + + auto ids_false = filter->getIdsMatchingFilter(query_false); + EXPECT_EQ(ids_false.size(), 1); + EXPECT_EQ(ids_false[0], 11); +} + +TEST_F(FilterTest, NumericFilterBasics) { + // ID 100: Age=25 + // ID 101: Age=30 + // ID 102: Age=35 + + filter->add_filters_from_json(100, R"({"age": 25})"); + filter->add_filters_from_json(101, R"({"age": 30})"); + filter->add_filters_from_json(102, R"({"age": 35})"); + + // Range Query: 20 <= Age <= 32 + json query_range = json::array({ + {{"age", {{"$range", {20, 32}}}}} + }); + + auto ids = filter->getIdsMatchingFilter(query_range); + + // Should match 100 (25) and 101 (30) + EXPECT_EQ(ids.size(), 2); + bool found100 = false, found101 = false; + for(auto id : ids) { + if(id == 100) found100 = true; + if(id == 101) found101 = true; + } + EXPECT_TRUE(found100); + EXPECT_TRUE(found101); +} + +TEST_F(FilterTest, FloatNumericFilter) { + // ID 1: Price=10.5 + // ID 2: Price=20.0 + + filter->add_filters_from_json(1, R"({"price": 10.5})"); + filter->add_filters_from_json(2, R"({"price": 20.0})"); + + json query = json::array({ + {{"price", {{"$range", {10.0, 15.0}}}}} + }); + + auto ids = filter->getIdsMatchingFilter(query); + EXPECT_EQ(ids.size(), 1); + EXPECT_EQ(ids[0], 1); +} + +TEST_F(FilterTest, MixedAndLogic) { + // ID 1: City=NY, Age=30 (Match) + // ID 2: City=NY, Age=40 (Age fail) + // ID 3: City=LA, Age=30 (City fail) + + filter->add_filters_from_json(1, R"({"city": "NY", "age": 30})"); + filter->add_filters_from_json(2, R"({"city": "NY", "age": 40})"); + filter->add_filters_from_json(3, R"({"city": "LA", "age": 30})"); + + // Filter: City=NY AND Age < 35 + json query = json::array({ + {{"city", {{"$eq", "NY"}}}}, + {{"age", {{"$range", {0, 35}}}}} + }); + + auto ids = filter->getIdsMatchingFilter(query); + EXPECT_EQ(ids.size(), 1); + EXPECT_EQ(ids[0], 1); +} + +TEST_F(FilterTest, InOperator) { + // ID 1: Color=Red + // ID 2: Color=Blue + // ID 3: Color=Green + + filter->add_to_filter("color", "Red", 1); + filter->add_to_filter("color", "Blue", 2); + filter->add_to_filter("color", "Green", 3); + + // Query: Color IN [Red, Green] + json query = json::array({ + {{"color", {{"$in", {"Red", "Green"}}}}} + }); + + auto ids = filter->getIdsMatchingFilter(query); + EXPECT_EQ(ids.size(), 2); // 1 and 3 +} + +TEST_F(FilterTest, DeleteFilter) { + // ID 1: Tag=A + filter->add_to_filter("tag", "A", 1); + + json query = json::array({ + {{"tag", {{"$eq", "A"}}}} + }); + + EXPECT_EQ(filter->countIdsMatchingFilter(query), 1); + + // Remove functionality test + // Usually removal requires us to know what to remove or we remove entire ID? + // The Filter class has: remove_from_filter(field, value, id) + + filter->remove_from_filter("tag", "A", 1); + + EXPECT_EQ(filter->countIdsMatchingFilter(query), 0); +} + +TEST_F(FilterTest, NumericDelete) { + // ID 1: Score=100 + filter->add_filters_from_json(1, R"({"score": 100})"); + + // Check it exists + json query = json::array({ + {{"score", {{"$eq", 100}}}} + }); + EXPECT_EQ(filter->countIdsMatchingFilter(query), 1); + + // Remove + // remove_filters_from_json uses the whole object + filter->remove_filters_from_json(1, R"({"score": 100})"); + + EXPECT_EQ(filter->countIdsMatchingFilter(query), 0); +} From f91dc75ef7cde096cf1e9eca984bb246a94c5707 Mon Sep 17 00:00:00 2001 From: rajesh33411 Date: Sat, 14 Feb 2026 10:43:01 +0530 Subject: [PATCH 17/48] fix:sparse storage db path correction (#41) * fix:sparse storage db path correction * Apply suggestion from @Copilot change variable name Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: rajeshkomaravelli Co-authored-by: Vineet Dwivedi <164136199+vindwid@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/core/ndd.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 710008f2bd..9231416c24 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -964,8 +964,8 @@ class IndexManager { // Initialize Sparse Storage if sparse_dim > 0 std::unique_ptr sparse_storage; if(sparse_dim > 0) { - std::string sparse_db_path = data_dir_ + "/" + index_id + "/sparse.db"; - sparse_storage = std::make_unique(sparse_db_path); + std::string sparse_storage_dir = data_dir_ + "/" + index_id + "/sparse"; + sparse_storage = std::make_unique(sparse_storage_dir); if(!sparse_storage->initialize()) { throw std::runtime_error("Failed to initialize sparse storage for index: " + index_id); From 2e687b6a29bda58e307e0244d5be4f57b6fe0e44 Mon Sep 17 00:00:00 2001 From: vindwi <130017173+vindwi@users.noreply.github.com> Date: Sun, 15 Feb 2026 19:44:50 +0530 Subject: [PATCH 18/48] Graph backfill (#42) * backfill * vector cache - dynamic hot cache --- src/hnsw/hnswalg.h | 95 +++++++++++++++----- src/hnsw/vector_cache.h | 190 ++++++++++++++++++++++++++++++++++++++++ src/utils/settings.hpp | 13 +++ 3 files changed, 277 insertions(+), 21 deletions(-) create mode 100644 src/hnsw/vector_cache.h diff --git a/src/hnsw/hnswalg.h b/src/hnsw/hnswalg.h index 17aa5caf48..d968e57da4 100644 --- a/src/hnsw/hnswalg.h +++ b/src/hnsw/hnswalg.h @@ -2,6 +2,7 @@ #include "visited_list_pool.h" #include "hnswlib.h" +#include "vector_cache.h" #include "log.hpp" #include "../utils/settings.hpp" #include "../quant/dispatch.hpp" @@ -69,8 +70,6 @@ namespace hnswlib { quant_level_(quant_level), M_(M), M0_(M * 2), - maxM_(M_ + settings::MAX_EXTRA_NEIGHBORS), - maxM0_(M0_ + 2 * settings::MAX_EXTRA_NEIGHBORS), efConstruction_(std::max(ef_construction, M_)), linkListLocks_(settings::MAX_LINK_LIST_LOCKS), checksum_(checksum), @@ -88,6 +87,13 @@ namespace hnswlib { << data_size_ << ", dimension: " << dimension_ << ", quant_level: " << static_cast(quant_level_)); + // Initialize cache + size_t cache_bits = VectorCache::calculateCacheBits(maxElements_); + if (cache_bits > 0) { + vector_cache_ = std::make_unique(data_size_, cache_bits); + LOG_DEBUG("Vector cache initialized for " << maxElements_ << " elements with " << (1 << cache_bits) << " slots"); + } + // Initialize upper layer space bool use_hybrid = true; if(quant_level_ == ndd::quant::QuantizationLevel::BINARY) { @@ -113,8 +119,6 @@ namespace hnswlib { if(M_ > settings::MAX_M) { M_ = settings::MAX_M; M0_ = M_ * 2; - maxM_ = M_ + settings::MAX_EXTRA_NEIGHBORS; - maxM0_ = M0_ + settings::MAX_EXTRA_NEIGHBORS; LOG_DEBUG("Capping M parameter to settings::MAX_M" << settings::MAX_M); } //efConstruction cannot be more than MAX_EF_CONSTRUCT @@ -128,8 +132,8 @@ namespace hnswlib { update_probability_generator_.seed(random_seed + 1); // links will also store number of linked elements in the first element - sizeLinksUpperLayers_ = sizeof(idhInt) + maxM_ * sizeof(idhInt); - sizeLinksBaseLayer_ = sizeof(idhInt) + maxM0_ * sizeof(idhInt); + sizeLinksUpperLayers_ = sizeof(idhInt) + M_ * sizeof(idhInt); + sizeLinksBaseLayer_ = sizeof(idhInt) + M0_ * sizeof(idhInt); // We are not storing the data in the level 0 memory, only the links and label sizeDataAtBaseLayer_ = sizeLinksBaseLayer_ + sizeof(flagInt) + sizeof(idInt); labelOffset_ = sizeLinksBaseLayer_ + sizeof(flagInt); @@ -189,6 +193,10 @@ namespace hnswlib { size += upper_layer_estimate * (data_size_upper_ + sizeof(levelInt) + sizeLinksUpperLayers_); + if (vector_cache_) { + size += vector_cache_->getMemoryUsage(); + } + return size / GB; // GB } @@ -426,11 +434,9 @@ namespace hnswlib { if(maxElements_i > 0) { maxElements_ = maxElements_i; } - maxM_ = M_ + settings::MAX_EXTRA_NEIGHBORS; - maxM0_ = M0_ + 2 * settings::MAX_EXTRA_NEIGHBORS; // links will also store number of linked elements - sizeLinksUpperLayers_ = sizeof(idInt) + maxM_ * sizeof(idInt); - sizeLinksBaseLayer_ = sizeof(idInt) + maxM0_ * sizeof(idInt); + sizeLinksUpperLayers_ = sizeof(idInt) + M_ * sizeof(idInt); + sizeLinksBaseLayer_ = sizeof(idInt) + M0_ * sizeof(idInt); // We are not storing the data in the level 0 memory, only the links and labels sizeDataAtBaseLayer_ = sizeLinksBaseLayer_ + sizeof(flagInt) + sizeof(idInt); labelOffset_ = sizeLinksBaseLayer_ + sizeof(flagInt); @@ -457,7 +463,14 @@ namespace hnswlib { createSpace(space_type_, dimension_, quant_level_)); } - data_size_upper_ = space_upper_->get_data_size(); + // Initialize cache for loaded index + size_t cache_bits = VectorCache::calculateCacheBits(maxElements_); + if (cache_bits > 0) { + vector_cache_ = std::make_unique(data_size_, cache_bits); + LOG_DEBUG("Vector cache initialized for " << maxElements_ << " elements with " << (1 << cache_bits) << " slots"); + } + + data_size_upper_ = space_upper_->get_data_size(); fstSimFuncUpper_ = space_upper_->get_sim_func(); dist_func_param_upper_ = space_upper_->get_dist_func_param(); @@ -587,6 +600,12 @@ namespace hnswlib { } } // TODO - Check this ..is it thread safe to comment this + + // Put the data in cache. Will speed up initial data load + if (curLevel == 0 && vector_cache_) { + vector_cache_->insert(cur_c, static_cast(datapoint)); + } + // std::unique_lock lock_el(getLinkListMutex(cur_c)); // Put the data in level 0 memory. @@ -749,8 +768,6 @@ namespace hnswlib { std::string indexId_; size_t M_{0}; size_t M0_{0}; - size_t maxM_{0}; - size_t maxM0_{0}; size_t efConstruction_{0}; size_t ef_{0}; SpaceType space_type_; // Now using SpaceType @@ -805,6 +822,13 @@ namespace hnswlib { SIMFUNC fstSimFuncUpper_; void* dist_func_param_upper_{nullptr}; + // Cache for vectors + mutable std::unique_ptr vector_cache_; + + public: + const VectorCache* getCache() const { + return vector_cache_.get(); + } // Maps external label to internal id std::vector labelLookup_; @@ -849,10 +873,21 @@ namespace hnswlib { // Modified function returning bool and filling buffer bool getDataByInternalId(idhInt internal_id, levelInt layer, uint8_t* buffer) const { if(layer == 0) { + // Check cache first + if (vector_cache_ && vector_cache_->get(internal_id, buffer)) { + return true; + } + idInt external_label = getExternalLabel(internal_id); if(vector_fetcher_) { // Directly fetch to buffer - return vector_fetcher_(external_label, buffer); + bool success = vector_fetcher_(external_label, buffer); + + // Populate cache on successful fetch + if (success && vector_cache_) { + vector_cache_->insert(internal_id, buffer); + } + return success; } return false; } else { @@ -908,18 +943,21 @@ namespace hnswlib { } // This function is used to get the neighbors based on heuristic - // We let the neighbors grow beyond M and then prune them based on heuristic + // We let the neighbors grow beyond M (now curM) and then prune them based on heuristic // The input is a sorted list (reverse order) by similarity std::vector> getNeighborsByHeuristic2(const std::vector>& candidates_sorted, - size_t M, + size_t curM, levelInt level) { - if(candidates_sorted.size() <= M) { + if(candidates_sorted.size() <= curM) { return candidates_sorted; } std::vector> result; - result.reserve(M); + result.reserve(curM); + + std::vector> fill_back_ids; + fill_back_ids.reserve(candidates_sorted.size() - curM); // Generic awareness auto curSimFunc = (level == 0) ? fstSimFunc_ : fstSimFuncUpper_; @@ -930,7 +968,7 @@ namespace hnswlib { std::vector selected_buf(curDataSize); // Only used for level 0 for(const auto& candidate : candidates_sorted) { - if(result.size() == M) { + if(result.size() == curM) { break; } @@ -973,7 +1011,23 @@ namespace hnswlib { if(good) { result.push_back(candidate); + } else { + fill_back_ids.push_back(candidate); + } + } + + size_t current_backfill_buffer = (level == 0) ? (settings::BACKFILL_BUFFER * 2) + : settings::BACKFILL_BUFFER; + + size_t target_backfill_size = (curM > current_backfill_buffer) + ? (curM - current_backfill_buffer) + : 0; + + for(const auto& fb : fill_back_ids) { + if(result.size() >= target_backfill_size) { + break; } + result.push_back(fb); } return result; @@ -988,7 +1042,6 @@ namespace hnswlib { levelInt level) { LOG_TIME("mutuallyConnectNewElement"); - size_t curMaxM = level ? maxM_ : maxM0_; size_t curM = level ? M_ : M0_; // Generic awareness @@ -1036,7 +1089,7 @@ namespace hnswlib { idhInt sz = getListCount(ll_other); idhInt* data = (ll_other + 1); - if(sz < curMaxM) { + if(sz < curM) { data[sz] = cur_c; setListCount(ll_other, sz + 1); } else { diff --git a/src/hnsw/vector_cache.h b/src/hnsw/vector_cache.h new file mode 100644 index 0000000000..985f0a353c --- /dev/null +++ b/src/hnsw/vector_cache.h @@ -0,0 +1,190 @@ +#pragma once +#include "hnswlib.h" +#include "../utils/settings.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace hnswlib { + +class VectorCache { +public: + inline static size_t VECTOR_CACHE_PERCENTAGE = settings::VECTOR_CACHE_PERCENTAGE; + + inline static size_t VECTOR_CACHE_MIN_BITS = settings::VECTOR_CACHE_MIN_BITS; + static constexpr uint8_t MAX_COUNTER = 2; // Sticky replacement policy + // Helper to calculate required cache bits based on element count and percentage + static size_t calculateCacheBits(size_t element_count, size_t cache_percent = VECTOR_CACHE_PERCENTAGE) { + if (element_count == 0 || cache_percent == 0) return 0; + + size_t target_elements = (element_count * cache_percent) / 100; + + // Calculate bits needed: 2^bits >= target_elements + size_t bits = 0; + while ((1ULL << bits) < target_elements) { + bits++; + } + + // Enforce minimum bits + if (bits < VECTOR_CACHE_MIN_BITS) { + bits = VECTOR_CACHE_MIN_BITS; + } + + return bits; + } + +private: + size_t cacheBits_ = 0; + size_t cacheSize_ = 0; + size_t cacheMask_ = 0; + size_t vectorCacheDataSize_ = 0; + size_t data_size_ = 0; + uint8_t* vectorCache_ = nullptr; + + static constexpr size_t CACHE_STRIPE_BITS = 8; // 256 stripes + static constexpr size_t CACHE_STRIPE_COUNT = 1 << CACHE_STRIPE_BITS; + static constexpr size_t CACHE_STRIPE_MASK = CACHE_STRIPE_COUNT - 1; + mutable std::array vectorCacheStripeMutexes_; + + static constexpr idInt INVALID_ID = static_cast(-1); + + std::shared_mutex& getCacheStripeMutex(size_t cache_index) const { + size_t stripe_id = cache_index & CACHE_STRIPE_MASK; + return vectorCacheStripeMutexes_[stripe_id]; + } + +public: + VectorCache() = default; + + // Constructor with initialization + VectorCache(size_t data_size, size_t cache_bits) { + init(data_size, cache_bits); + } + + ~VectorCache() { + if (vectorCache_) { + delete[] vectorCache_; + vectorCache_ = nullptr; + } + } + + void init(size_t data_size, size_t cache_bits) { + if (vectorCache_) { + delete[] vectorCache_; + vectorCache_ = nullptr; + } + + if (cache_bits == 0) { + cacheBits_ = 0; + cacheSize_ = 0; + cacheMask_ = 0; + data_size_ = 0; + vectorCacheDataSize_ = 0; + return; + } + + data_size_ = data_size; + cacheBits_ = cache_bits; + cacheSize_ = 1 << cacheBits_; + cacheMask_ = cacheSize_ - 1; + // Layout: [idInt] [uint8_t counter] [data...] + vectorCacheDataSize_ = data_size_ + sizeof(idInt) + sizeof(uint8_t); + + vectorCache_ = new uint8_t[cacheSize_ * vectorCacheDataSize_]; + + // Initialize all entries to INVALID_ID + for (size_t i = 0; i < cacheSize_; i++) { + uint8_t* entry = vectorCache_ + i * vectorCacheDataSize_; + idInt* id_ptr = reinterpret_cast(entry); + *id_ptr = INVALID_ID; + // Also zero out counter/data for cleanliness + *(entry + sizeof(idInt)) = 0; + } + } + + bool get(idInt internal_id, uint8_t* buffer) const { + if (!vectorCache_) return false; + + size_t index = internal_id & cacheMask_; + uint8_t* entry = vectorCache_ + index * vectorCacheDataSize_; + + std::shared_lock lock(getCacheStripeMutex(index)); + + idInt* stored_id = reinterpret_cast(entry); + if (*stored_id == internal_id) { + // Hit! Reset counter to MAX_COUNTER (stickiness) + // Optimization: Only write if currently different to avoid cache line invalidation (False Sharing) + uint8_t* counter_ptr = entry + sizeof(idInt); + auto atomic_counter = reinterpret_cast*>(counter_ptr); + + if (atomic_counter->load(std::memory_order_relaxed) < MAX_COUNTER) { + atomic_counter->store(MAX_COUNTER, std::memory_order_relaxed); + } + + memcpy(buffer, entry + sizeof(idInt) + sizeof(uint8_t), data_size_); + return true; + } + return false; + } + + void insert(idInt internal_id, const uint8_t* data) { + if (!vectorCache_) return; + + size_t index = internal_id & cacheMask_; + uint8_t* entry = vectorCache_ + index * vectorCacheDataSize_; + + std::unique_lock lock(getCacheStripeMutex(index)); + + idInt* stored_id = reinterpret_cast(entry); + // Use atomic consistently to avoid UB, though we are under unique_lock + auto atomic_counter = reinterpret_cast*>(entry + sizeof(idInt)); + uint8_t* data_ptr = entry + sizeof(idInt) + sizeof(uint8_t); + + if (*stored_id == internal_id) { + // Update existing + atomic_counter->store(MAX_COUNTER, std::memory_order_relaxed); + memcpy(data_ptr, data, data_size_); + return; + } + + if (*stored_id == INVALID_ID) { + // Empty slot + *stored_id = internal_id; + atomic_counter->store(MAX_COUNTER, std::memory_order_relaxed); + memcpy(data_ptr, data, data_size_); + return; + } + + // Collision with different vector + uint8_t c = atomic_counter->load(std::memory_order_relaxed); + if (c > 0) { + c--; + atomic_counter->store(c, std::memory_order_relaxed); + } + + if (c == 0) { + // Replace + *stored_id = internal_id; + atomic_counter->store(MAX_COUNTER, std::memory_order_relaxed); + memcpy(data_ptr, data, data_size_); + } + // Else: reject new vector, keep old one (thrashing protection) + } + + size_t getCacheBits() const { return cacheBits_; } + size_t getCacheSize() const { return cacheSize_; } + void setCacheBits(size_t bits) { cacheBits_ = bits; } + + size_t getMemoryUsage() const { + if (!vectorCache_) return 0; + return cacheSize_ * vectorCacheDataSize_; + } +}; + +} // namespace hnswlib diff --git a/src/utils/settings.hpp b/src/utils/settings.hpp index 56507226b4..c84ad89a11 100644 --- a/src/utils/settings.hpp +++ b/src/utils/settings.hpp @@ -28,6 +28,7 @@ namespace settings { constexpr size_t MAX_M = 512; constexpr size_t DEFAULT_EF_CONSTRUCT = 128; constexpr size_t MIN_EF_CONSTRUCT = 8; + constexpr size_t BACKFILL_BUFFER = 2; // Keep 2 slots free for high quality neighbors constexpr size_t MAX_EF_CONSTRUCT = 4096; constexpr size_t DEFAULT_EF_SEARCH = 128; constexpr size_t MIN_K = 1; @@ -92,6 +93,8 @@ namespace settings { constexpr size_t DEFAULT_MAX_ELEMENTS = 100'000; constexpr size_t DEFAULT_MAX_ELEMENTS_INCREMENT = 100'000; constexpr size_t DEFAULT_MAX_ELEMENTS_INCREMENT_TRIGGER = 50'000; + constexpr size_t DEFAULT_VECTOR_CACHE_PERCENTAGE = 15; + constexpr size_t DEFAULT_VECTOR_CACHE_MIN_BITS = 17; const std::string DEFAULT_SERVER_ID = "unknown"; //For Backups @@ -138,6 +141,16 @@ namespace settings { return env ? std::stoull(env) : DEFAULT_MAX_ELEMENTS_INCREMENT_TRIGGER; }(); + inline static size_t VECTOR_CACHE_PERCENTAGE = [] { + const char* env = std::getenv("NDD_VECTOR_CACHE_PERCENTAGE"); + return env ? std::stoull(env) : DEFAULT_VECTOR_CACHE_PERCENTAGE; + }(); + + inline static size_t VECTOR_CACHE_MIN_BITS = [] { + const char* env = std::getenv("NDD_VECTOR_CACHE_MIN_BITS"); + return env ? std::stoull(env) : DEFAULT_VECTOR_CACHE_MIN_BITS; + }(); + // Number of parallel inserts. It will use this many threads to insert data in parallel inline static size_t NUM_PARALLEL_INSERTS = [] { const char* env = std::getenv("NDD_NUM_PARALLEL_INSERTS"); From 6b34f43ef6b217f6bdbb540706e69ac3c21ba950 Mon Sep 17 00:00:00 2001 From: Hemant Sharma Date: Tue, 17 Feb 2026 19:49:30 +0530 Subject: [PATCH 19/48] add meta and filter in application/json in insert api --- src/main.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/main.cpp b/src/main.cpp index 40ae26b866..453638abe0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -781,6 +781,19 @@ int main(int argc, char** argv) { } } + if(item.has("meta")) { + auto meta_str = std::string(item["meta"].s()); + vec.meta.assign(meta_str.begin(), meta_str.end()); + } + + if(item.has("filter")) { + vec.filter = std::string(item["filter"].s()); + } + + if(item.has("norm")) { + vec.norm = static_cast(item["norm"].d()); + } + if(item.has("vector")) { for(const auto& v : item["vector"]) { vec.vector.push_back(static_cast(v.d())); From d984b91a19e4d0e47f0bd7f72c5ae21d534748fb Mon Sep 17 00:00:00 2001 From: rajesh33411 Date: Thu, 19 Feb 2026 12:51:28 +0530 Subject: [PATCH 20/48] fix:sparse vectors delete addition (#46) Co-authored-by: rajeshkomaravelli --- src/core/ndd.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 9231416c24..414929757b 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -1391,6 +1391,10 @@ class IndexManager { entry.vector_storage->deleteFilter(numeric_id, meta.filter); // Mark as deleted in HNSW index entry.alg->markDelete(numeric_id); + // Delete from sparse storage if hybrid index + if(entry.sparse_storage) { + entry.sparse_storage->delete_vector(numeric_id); + } } // Add the list to write ahead log using IndexManager's method logDeletions(entry.index_id, numeric_ids); From 5cfddb4b583dcf508557bf4a9033cad7d7831422 Mon Sep 17 00:00:00 2001 From: shaleenji Date: Mon, 23 Feb 2026 10:57:39 +0000 Subject: [PATCH 21/48] New Directory structure (#45) --- src/core/ndd.hpp | 48 ++++++++++++++++++++++++------------------ src/utils/settings.hpp | 1 + 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 414929757b..faea98237f 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -351,7 +351,7 @@ class IndexManager { if(remainingCapacity < settings::MAX_ELEMENTS_INCREMENT_TRIGGER) { size_t newMaxElements = maxElements + settings::MAX_ELEMENTS_INCREMENT; LOG_DEBUG("Auto-resizing index " << entry.index_id << " from " << maxElements << " to " - << newMaxElements << " elements"); + << newMaxElements << " elements"); try { entry.alg->resizeIndex(newMaxElements); @@ -361,7 +361,9 @@ class IndexManager { } } - std::string index_path = data_dir_ + "/" + entry.index_id + "/main.idx"; + std::string index_dir = data_dir_ + "/" + entry.index_id; + std::string vector_storage_dir = index_dir + "/vectors"; + std::string index_path = vector_storage_dir + "/" + settings::DEFAULT_SUBINDEX + ".idx"; std::string temp_path = index_path + ".tmp"; entry.alg->saveIndex(temp_path); @@ -511,8 +513,9 @@ class IndexManager { // This is used when the index is corrupted or needs to be reset. bool resetIndex(const std::string& index_id, const IndexConfig& config) { std::string base_path = data_dir_ + "/" + index_id; - std::string index_file = base_path + "/main.idx"; - LOG_DEBUG(index_file); + std::string vector_storage_dir = base_path + "/vectors"; + std::string index_path = vector_storage_dir + "/" + settings::DEFAULT_SUBINDEX + ".idx"; + LOG_DEBUG(index_path); std::string recover_file = base_path + "/recover.txt"; LOG_DEBUG(recover_file); @@ -523,8 +526,8 @@ class IndexManager { } // 2. Fail if index file already exists - if(std::filesystem::exists(index_file)) { - LOG_ERROR("Index file already exists: " << index_file); + if(std::filesystem::exists(index_path)) { + LOG_ERROR("Index file already exists: " << index_path); return false; } @@ -540,7 +543,7 @@ class IndexManager { settings::RANDOM_SEED, quant_level, config.checksum); - hnsw.saveIndex(index_file); + hnsw.saveIndex(index_path); // 4. Write recover.txt with "0:0" std::ofstream fout(recover_file); @@ -797,11 +800,12 @@ class IndexManager { const IndexConfig& config, UserType user_type = UserType::Admin, size_t size_in_millions = 0) { - // Get usernmae and index name from index_id + // Get username and index name from index_id auto pos = index_id.find('/'); if(pos == std::string::npos) { throw std::runtime_error("Invalid index ID"); } + std::string index_dir = data_dir_ + "/" + index_id; std::string username = index_id.substr(0, pos); std::string index_name = index_id.substr(pos + 1); // Check if index already exists in metadata @@ -824,7 +828,8 @@ class IndexManager { } // Check file system without lock - std::string index_path = data_dir_ + "/" + index_id + "/main.idx"; + std::string vector_storage_dir = index_dir + "/vectors"; + std::string index_path = vector_storage_dir + "/" + settings::DEFAULT_SUBINDEX + ".idx"; if(std::filesystem::exists(index_path)) { throw std::runtime_error("Index already exists"); } @@ -836,8 +841,7 @@ class IndexManager { } hnswlib::SpaceType space_type = hnswlib::getSpaceType(config.space_type_str); - std::string lmdb_dir = data_dir_ + "/" + index_id + "/ids"; - std::string vector_storage_dir = data_dir_ + "/" + index_id + "/vectors"; + std::string lmdb_dir = index_dir + "/ids"; //create the directory and initialize sequence for IDMapper LOG_INFO("Creating IDMapper for index " @@ -846,17 +850,16 @@ class IndexManager { // IDMapper now uses tier-based fixed bloom filter sizing based on user_type auto id_mapper = std::make_shared(lmdb_dir, true, user_type); - std::filesystem::create_directories(vector_storage_dir); // Create HNSW directly with all necessary parameters ndd::quant::QuantizationLevel quant_level = config.quant_level; auto vector_storage = - std::make_shared(vector_storage_dir, config.dim, config.quant_level); + std::make_shared(index_dir, config.dim, config.quant_level); // Initialize Sparse Storage if needed std::unique_ptr sparse_storage = nullptr; if(config.sparse_dim > 0) { - std::string sparse_storage_dir = data_dir_ + "/" + index_id + "/sparse"; + std::string sparse_storage_dir = index_dir + "/sparse"; sparse_storage = std::make_unique(sparse_storage_dir); if(!sparse_storage->initialize()) { throw std::runtime_error("Failed to initialize sparse storage"); @@ -931,9 +934,11 @@ class IndexManager { } void loadIndex(const std::string& index_id) { - std::string index_path = data_dir_ + "/" + index_id + "/main.idx"; - std::string lmdb_dir = data_dir_ + "/" + index_id + "/ids"; - std::string vector_storage_dir = data_dir_ + "/" + index_id + "/vectors"; + std::string index_dir = data_dir_ + "/" + index_id; + std::string lmdb_dir = index_dir + "/ids"; + std::string vector_storage_dir = index_dir + "/vectors"; + std::string index_path = vector_storage_dir + "/" + settings::DEFAULT_SUBINDEX + ".idx"; + if(!std::filesystem::exists(index_path) || !std::filesystem::exists(lmdb_dir) || !std::filesystem::exists(vector_storage_dir)) { throw std::runtime_error("Required files missing for index: " + index_id); @@ -959,12 +964,12 @@ class IndexManager { // Step 2: Create IDMapper and VectorStorage - IDMapper handles bloom filter initialization auto id_mapper = std::make_shared(lmdb_dir, false); auto vector_storage = std::make_shared( - vector_storage_dir, alg->getDimension(), alg->getQuantLevel()); + index_dir, alg->getDimension(), alg->getQuantLevel()); // Initialize Sparse Storage if sparse_dim > 0 std::unique_ptr sparse_storage; if(sparse_dim > 0) { - std::string sparse_storage_dir = data_dir_ + "/" + index_id + "/sparse"; + std::string sparse_storage_dir = index_dir + "/sparse"; sparse_storage = std::make_unique(sparse_storage_dir); if(!sparse_storage->initialize()) { throw std::runtime_error("Failed to initialize sparse storage for index: " @@ -1053,6 +1058,7 @@ class IndexManager { // Add this new function to reload just the algorithm part while preserving the CacheEntry void reloadIndex(const std::string& index_id) { + auto it = indices_.find(index_id); if(it == indices_.end()) { return; // Index not in cache @@ -1060,7 +1066,9 @@ class IndexManager { CacheEntry& entry = it->second; - std::string index_path = data_dir_ + "/" + index_id + "/main.idx"; + std::string index_dir = data_dir_ + "/" + entry.index_id; + std::string vector_storage_dir = index_dir + "/vectors"; + std::string index_path = vector_storage_dir + "/" + settings::DEFAULT_SUBINDEX + ".idx"; // Create a new HNSW algorithm object from the saved file auto new_alg = std::make_unique>(index_path, 0); diff --git a/src/utils/settings.hpp b/src/utils/settings.hpp index c84ad89a11..20f78690de 100644 --- a/src/utils/settings.hpp +++ b/src/utils/settings.hpp @@ -89,6 +89,7 @@ namespace settings { constexpr size_t DEFAULT_SERVER_PORT = 8080; const std::string DEFAULT_SERVER_TYPE = "OSS"; const std::string DEFAULT_DATA_DIR = "/mnt/data"; + const std::string DEFAULT_SUBINDEX = "DEFAULT"; constexpr size_t DEFAULT_MAX_ACTIVE_INDICES = 64; constexpr size_t DEFAULT_MAX_ELEMENTS = 100'000; constexpr size_t DEFAULT_MAX_ELEMENTS_INCREMENT = 100'000; From 3bad3beee879cf49b497e2d5b0842d9557a962d4 Mon Sep 17 00:00:00 2001 From: vindwi <130017173+vindwi@users.noreply.github.com> Date: Mon, 23 Feb 2026 17:42:40 +0530 Subject: [PATCH 22/48] change int16d to int16 and int8d to int8 (#48) --- src/main.cpp | 14 ++++++++++---- src/quant/dispatch.hpp | 4 ++-- src/quant/float16.hpp | 2 +- src/quant/float32.hpp | 12 ++++++------ src/quant/{int16d.hpp => int16.hpp} | 30 ++++++++++++++--------------- src/quant/{int8d.hpp => int8.hpp} | 30 ++++++++++++++--------------- 6 files changed, 49 insertions(+), 43 deletions(-) rename src/quant/{int16d.hpp => int16.hpp} (98%) rename src/quant/{int8d.hpp => int8.hpp} (98%) diff --git a/src/main.cpp b/src/main.cpp index 453638abe0..a84228f56e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -341,10 +341,16 @@ int main(int argc, char** argv) { + std::to_string(settings::MAX_EF_CONSTRUCT)); } - // Get quantization level (default to INT8) - ndd::quant::QuantizationLevel quant_level = - body.has("precision") ? stringToQuantLevel(body["precision"].s()) - : ndd::quant::QuantizationLevel::INT8; + // Get quantization level (default to INT16) + std::string precision = body.has("precision") ? std::string(body["precision"].s()) : "int16"; + + if(precision == "int8d") { + precision = "int8"; + } else if(precision == "int16d") { + precision = "int16"; + } + + ndd::quant::QuantizationLevel quant_level = stringToQuantLevel(precision); // Validate quantization level if(quant_level == ndd::quant::QuantizationLevel::UNKNOWN) { diff --git a/src/quant/dispatch.hpp b/src/quant/dispatch.hpp index bd9d77224c..6ce7c6a4f3 100644 --- a/src/quant/dispatch.hpp +++ b/src/quant/dispatch.hpp @@ -8,8 +8,8 @@ // Include all quantizer implementations to ensure they are registered #include "float16.hpp" #include "float32.hpp" -#include "int8d.hpp" -#include "int16d.hpp" +#include "int8.hpp" +#include "int16.hpp" #include "binary.hpp" namespace ndd { diff --git a/src/quant/float16.hpp b/src/quant/float16.hpp index 57860f4d91..b50035389a 100644 --- a/src/quant/float16.hpp +++ b/src/quant/float16.hpp @@ -6,7 +6,7 @@ #include #include "../hnsw/hnswlib.h" #include "common.hpp" -#include "int8d.hpp" +#include "int8.hpp" namespace ndd { namespace quant { diff --git a/src/quant/float32.hpp b/src/quant/float32.hpp index e8b1b4c197..79c082e69f 100644 --- a/src/quant/float32.hpp +++ b/src/quant/float32.hpp @@ -1,7 +1,7 @@ #pragma once #include "../hnsw/hnswlib.h" #include "../quant/common.hpp" -#include "int8d.hpp" +#include "int8.hpp" #include #include #include @@ -18,15 +18,15 @@ namespace hnswlib { const float* f_in = static_cast(in); std::vector input(f_in, f_in + dim); #if defined(USE_SVE2) - return ndd::quant::int8d::quantize_vector_fp32_to_int8_buffer_sve(input); + return ndd::quant::int8::quantize_vector_fp32_to_int8_buffer_sve(input); #elif defined(USE_AVX512) - return ndd::quant::int8d::quantize_vector_fp32_to_int8_buffer_avx512(input); + return ndd::quant::int8::quantize_vector_fp32_to_int8_buffer_avx512(input); #elif defined(USE_AVX2) - return ndd::quant::int8d::quantize_vector_fp32_to_int8_buffer_avx2(input); + return ndd::quant::int8::quantize_vector_fp32_to_int8_buffer_avx2(input); #elif defined(USE_NEON) - return ndd::quant::int8d::quantize_vector_fp32_to_int8_buffer_neon(input); + return ndd::quant::int8::quantize_vector_fp32_to_int8_buffer_neon(input); #else - return ndd::quant::int8d::quantize_vector_fp32_to_int8_buffer(input); + return ndd::quant::int8::quantize_vector_fp32_to_int8_buffer(input); #endif } diff --git a/src/quant/int16d.hpp b/src/quant/int16.hpp similarity index 98% rename from src/quant/int16d.hpp rename to src/quant/int16.hpp index 30c5921168..5d2029f3a8 100644 --- a/src/quant/int16d.hpp +++ b/src/quant/int16.hpp @@ -8,7 +8,7 @@ namespace ndd { namespace quant { - namespace int16d { + namespace int16 { constexpr float INT16_SCALE = 32767.0f; // Max value for 16-bit signed integer quantization @@ -1051,33 +1051,33 @@ namespace ndd { return out_vec; } - } // namespace int16d + } // namespace int16 class Int16Quantizer : public Quantizer { public: - std::string name() const override { return "int16d"; } + std::string name() const override { return "int16"; } QuantizationLevel level() const override { return QuantizationLevel::INT16; } QuantizerDispatch getDispatch() const override { QuantizerDispatch d; - d.dist_l2 = &int16d::L2Sqr; - d.dist_ip = &int16d::InnerProduct; - d.dist_cosine = &int16d::Cosine; - d.sim_l2 = &int16d::L2SqrSim; - d.sim_ip = &int16d::InnerProductSim; - d.sim_cosine = &int16d::CosineSim; - d.quantize = &int16d::quantize; - d.dequantize = &int16d::dequantize; - d.quantize_to_int8 = &int16d::quantize_to_int8; - d.get_storage_size = &int16d::get_storage_size; - d.extract_scale = &int16d::extract_scale; + d.dist_l2 = &int16::L2Sqr; + d.dist_ip = &int16::InnerProduct; + d.dist_cosine = &int16::Cosine; + d.sim_l2 = &int16::L2SqrSim; + d.sim_ip = &int16::InnerProductSim; + d.sim_cosine = &int16::CosineSim; + d.quantize = &int16::quantize; + d.dequantize = &int16::dequantize; + d.quantize_to_int8 = &int16::quantize_to_int8; + d.get_storage_size = &int16::get_storage_size; + d.extract_scale = &int16::extract_scale; return d; } }; // Register INT16 static RegisterQuantizer - reg_int16(QuantizationLevel::INT16, "int16d", std::make_shared()); + reg_int16(QuantizationLevel::INT16, "int16", std::make_shared()); } // namespace quant } // namespace ndd diff --git a/src/quant/int8d.hpp b/src/quant/int8.hpp similarity index 98% rename from src/quant/int8d.hpp rename to src/quant/int8.hpp index effb42545f..2e183033de 100644 --- a/src/quant/int8d.hpp +++ b/src/quant/int8.hpp @@ -8,7 +8,7 @@ namespace ndd { namespace quant { - namespace int8d { + namespace int8 { constexpr float INT8_SCALE = 127.0f; // Max value for 8-bit signed integer quantization @@ -916,35 +916,35 @@ namespace ndd { return std::vector(ptr, ptr + size); } - } // namespace int8d + } // namespace int8 class Int8Quantizer : public ndd::quant::Quantizer { public: - std::string name() const override { return "int8d"; } + std::string name() const override { return "int8"; } ndd::quant::QuantizationLevel level() const override { return ndd::quant::QuantizationLevel::INT8; } ndd::quant::QuantizerDispatch getDispatch() const override { ndd::quant::QuantizerDispatch d; - d.dist_l2 = &int8d::L2Sqr; - d.dist_ip = &int8d::InnerProduct; - d.dist_cosine = &int8d::Cosine; - d.sim_l2 = &int8d::L2SqrSim; - d.sim_ip = &int8d::InnerProductSim; - d.sim_cosine = &int8d::CosineSim; - d.quantize = &int8d::quantize; - d.dequantize = &int8d::dequantize; - d.quantize_to_int8 = &int8d::quantize_to_int8_identity; - d.get_storage_size = &int8d::get_storage_size; - d.extract_scale = &int8d::extract_scale; + d.dist_l2 = &int8::L2Sqr; + d.dist_ip = &int8::InnerProduct; + d.dist_cosine = &int8::Cosine; + d.sim_l2 = &int8::L2SqrSim; + d.sim_ip = &int8::InnerProductSim; + d.sim_cosine = &int8::CosineSim; + d.quantize = &int8::quantize; + d.dequantize = &int8::dequantize; + d.quantize_to_int8 = &int8::quantize_to_int8_identity; + d.get_storage_size = &int8::get_storage_size; + d.extract_scale = &int8::extract_scale; return d; } }; // Register INT8 static ndd::quant::RegisterQuantizer reg_int8(ndd::quant::QuantizationLevel::INT8, - "int8d", + "int8", std::make_shared()); } // namespace quant From 294bb07e7a2d564f2b92e1477e50bd4b0976af2d Mon Sep 17 00:00:00 2001 From: shaleenji Date: Tue, 24 Feb 2026 11:14:42 +0000 Subject: [PATCH 23/48] Sparse algo bmw (#49) --- src/sparse/bmw.hpp | 529 +++++++++++++++++++++++++++++++++++++------ tests/CMakeLists.txt | 5 - 2 files changed, 458 insertions(+), 76 deletions(-) diff --git a/src/sparse/bmw.hpp b/src/sparse/bmw.hpp index a6045098ba..f2809fc14e 100644 --- a/src/sparse/bmw.hpp +++ b/src/sparse/bmw.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include "../core/types.hpp" @@ -241,8 +242,11 @@ namespace ndd { return removeDocumentInternal(txn, doc_id, vec); } - // Search using BMW algorithm (DAAT) - std::vector> search(const SparseVector& query, size_t k, const ndd::RoaringBitmap* filter = nullptr) { + // Search using BMW algorithm (DAAT + std::vector> search(const SparseVector& query, + size_t k, + const ndd::RoaringBitmap* filter = nullptr) + { if(query.empty() || k == 0) { return {}; } @@ -289,19 +293,47 @@ namespace ndd { // Helper to sort iterators by current doc ID auto sort_iterators = [&]() { - std::sort( - iterators.begin(), iterators.end(), [](BlockIterator* a, BlockIterator* b) { - return a->current_doc_id < b->current_doc_id; - }); +#if defined(NDD_BMW_USE_STD_SORT) + std::sort(iterators.begin(), + iterators.end(), + [](BlockIterator* a, BlockIterator* b) { + return a->current_doc_id < b->current_doc_id; + }); +#else + if(iterators.size() < 2) { + return; + } + + // Requested bubble sort for iterator ordering by current doc id. + bool swapped; + for(size_t pass = 0; pass + 1 < iterators.size(); ++pass) { + swapped = false; + for(size_t i = 0; i + 1 < iterators.size() - pass; ++i) { + if(iterators[i]->current_doc_id > iterators[i + 1]->current_doc_id) { + std::swap(iterators[i], iterators[i + 1]); + swapped = true; + } + } + if(!swapped) { + break; + } + } +#endif }; sort_iterators(); + float remaining_global_upper_bound = 0.0f; + for(size_t i = 0; i < iterators.size(); ++i) { + remaining_global_upper_bound += iterators[i]->globalUpperBound(); + } + while(true) { // Remove exhausted iterators while(!iterators.empty() && iterators.back()->current_doc_id == std::numeric_limits::max()) { + remaining_global_upper_bound -= iterators.back()->globalUpperBound(); iterators.pop_back(); } @@ -309,6 +341,49 @@ namespace ndd { break; } +#if defined(NDD_BMW_EXHAUSTIVE_DAAT) + ndd::idInt current_doc_id = iterators.front()->current_doc_id; + + if(filter && !filter->contains(current_doc_id)) { + for(size_t i = 0; i < iterators.size(); ++i) { + if(iterators[i]->current_doc_id == current_doc_id) { + iterators[i]->next(); + } + } + sort_iterators(); + continue; + } + + float score = 0.0f; + for(size_t i = 0; i < iterators.size(); ++i) { + if(iterators[i]->current_doc_id == current_doc_id) { + score += iterators[i]->current_score * iterators[i]->term_weight; + iterators[i]->next(); + } + } + + if(top_k.size() < k) { + top_k.emplace(current_doc_id, score); + if(top_k.size() == k) { + threshold = top_k.top().score; + } + } else if(score > threshold) { + top_k.pop(); + top_k.emplace(current_doc_id, score); + threshold = top_k.top().score; + } + + sort_iterators(); + continue; +#else + + if(remaining_global_upper_bound < 0.0f) { + remaining_global_upper_bound = 0.0f; + } + if(remaining_global_upper_bound <= threshold) { + break; + } + // WAND/BMW logic float upper_bound_sum = 0.0f; size_t pivot_idx = 0; @@ -371,13 +446,18 @@ namespace ndd { threshold = top_k.top().score; } } else { - // Pivot is further ahead, skip predecessors +#if defined(NDD_BMW_LEGACY_ADVANCE_ALL_PREDECESSORS) for(size_t i = 0; i < pivot_idx; ++i) { iterators[i]->advance(pivot_doc_id); } +#else + // Standard WAND/BMW behavior: advance only the first iterator to the pivot. + iterators[0]->advance(pivot_doc_id); +#endif } sort_iterators(); +#endif } // Clean up @@ -493,7 +573,11 @@ namespace ndd { std::shared_lock lock(mutex_); size_t total = 0; for(const auto& [term_id, blocks] : term_blocks_index_) { - total += blocks.size(); + if(!blocks.empty() && blocks.front().start_doc_id == GLOBAL_MAX_SENTINEL_DOC_ID) { + total += (blocks.size() - 1); + } else { + total += blocks.size(); + } } return total; } @@ -501,16 +585,31 @@ namespace ndd { size_t getVocabSize() const { return vocab_size_; } private: + static constexpr ndd::idInt GLOBAL_MAX_SENTINEL_DOC_ID = 0; + + static size_t firstRealBlockIndex(const std::vector& blocks) { + if(!blocks.empty() && blocks.front().start_doc_id == GLOBAL_MAX_SENTINEL_DOC_ID) { + return 1; + } + return 0; + } + std::vector::iterator findBlockIterator(std::vector& blocks, ndd::idInt doc_id) { - auto it = std::upper_bound(blocks.begin(), + size_t first_idx = firstRealBlockIndex(blocks); + if(first_idx >= blocks.size()) { + return blocks.end(); + } + + auto begin_it = blocks.begin() + static_cast(first_idx); + auto it = std::upper_bound(begin_it, blocks.end(), doc_id, [](ndd::idInt doc_id, const BlockIdx& block) { return doc_id < block.start_doc_id; }); - if(it == blocks.begin()) { + if(it == begin_it) { return it; } return it - 1; @@ -518,43 +617,78 @@ namespace ndd { std::vector::const_iterator findBlockIterator(const std::vector& blocks, ndd::idInt doc_id) const { - auto it = std::upper_bound(blocks.begin(), + size_t first_idx = firstRealBlockIndex(blocks); + if(first_idx >= blocks.size()) { + return blocks.end(); + } + + auto begin_it = blocks.begin() + static_cast(first_idx); + auto it = std::upper_bound(begin_it, blocks.end(), doc_id, [](ndd::idInt doc_id, const BlockIdx& block) { return doc_id < block.start_doc_id; }); - if(it == blocks.begin()) { + if(it == begin_it) { return it; } return it - 1; } - // Helper to dequantize + /** + * The quantize and dequantize functions are there to reduce the memory + * and storage footprint of the sparse values (float 32 to int8). + */ + // Helper for uint8 quantization + static inline uint8_t quantize(float val, float max_val) { + if(max_val <= 1e-9f) { + return 0; + } + float scaled = (val / max_val) * 255.0f; + if(scaled >= 255.0f) { + return 255; + } + if (scaled <= 0.0f) return 0; + + return static_cast(scaled + 0.5f); + } + static inline float dequantize(uint8_t val, float max_val) { - return (static_cast(val) * (1.0f / 255.0f)) * max_val; + // If max_val is near zero, the result is effectively zero + if (max_val <= 1e-9f) { + return 0.0f; + } + + // Use a single multiplier to avoid multiple floating point ops + const float scale = max_val / 255.0f; + return static_cast(val) * scale; } + // Helper struct for getReadOnlyBlock return value struct BlockView { const void* doc_diffs; // Can be uint16_t* or uint32_t* - const uint8_t* values; + const void* values; size_t count; uint8_t diff_bits; // 16 or 32 + uint8_t value_bits; // 8 (quantized) or 32 (float) }; struct BlockIterator { uint32_t term_id; float term_weight; const std::vector* blocks; + size_t first_block_idx; size_t current_block_idx; + float global_term_max; // SoA pointers const void* doc_diffs_ptr = nullptr; // Can be u16 or u32 - const uint8_t* values_ptr = nullptr; + const void* values_ptr = nullptr; size_t block_data_size = 0; uint8_t diff_bits = 32; + uint8_t value_bits = 8; size_t current_entry_idx; ndd::idInt current_doc_id; @@ -570,14 +704,29 @@ namespace ndd { term_id(tid), term_weight(weight), blocks(blks), + first_block_idx(0), current_block_idx(0), + global_term_max(0.0f), current_entry_idx(0), current_doc_id(std::numeric_limits::max()), current_score(0.0f), index(idx), txn(t) { if(blocks && !blocks->empty()) { - loadCurrentBlock(); + if(blocks->front().start_doc_id == BMWIndex::GLOBAL_MAX_SENTINEL_DOC_ID) { + first_block_idx = 1; + global_term_max = blocks->front().block_max_value; + } else { + first_block_idx = 0; + for(const auto& block : *blocks) { + global_term_max = std::max(global_term_max, block.block_max_value); + } + } + + current_block_idx = first_block_idx; + if(current_block_idx < blocks->size()) { + loadCurrentBlock(); + } } } @@ -592,10 +741,38 @@ namespace ndd { values_ptr = view.values; block_data_size = view.count; diff_bits = view.diff_bits; + value_bits = view.value_bits; current_entry_idx = 0; advanceToNextLive(); } + inline float valueAt(size_t idx, float block_max_value) const { + if(value_bits == 32) { + return static_cast(values_ptr)[idx]; + } + return dequantize(static_cast(values_ptr)[idx], block_max_value); + } + + inline bool isLiveAt(size_t idx) const { + if(value_bits == 32) { + return static_cast(values_ptr)[idx] > 0.0f; + } + return static_cast(values_ptr)[idx] > 0; + } + + inline size_t findNextLive(size_t start_idx) const { + if(value_bits == 32) { + size_t idx = start_idx; + auto values = static_cast(values_ptr); + while(idx < block_data_size && values[idx] <= 0.0f) { + ++idx; + } + return idx; + } + return index->findNextLiveSIMD( + static_cast(values_ptr), block_data_size, start_idx); + } + inline void advanceToNextLive() { // Branch prediction will handle diff_bits effectively (constant per block) if(diff_bits == 16) { @@ -609,22 +786,19 @@ namespace ndd { auto diff_ptr = static_cast(doc_diffs_ptr); // Fast path: check if current entry is already live - if(current_entry_idx < block_data_size && values_ptr[current_entry_idx] > 0) { + if(current_entry_idx < block_data_size && isLiveAt(current_entry_idx)) { const auto& block_meta = (*blocks)[current_block_idx]; current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - current_score = - dequantize(values_ptr[current_entry_idx], block_meta.block_max_value); + current_score = valueAt(current_entry_idx, block_meta.block_max_value); return; } - current_entry_idx = - index->findNextLiveSIMD(values_ptr, block_data_size, current_entry_idx); + current_entry_idx = findNextLive(current_entry_idx); if(current_entry_idx < block_data_size) { const auto& block_meta = (*blocks)[current_block_idx]; current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - current_score = - dequantize(values_ptr[current_entry_idx], block_meta.block_max_value); + current_score = valueAt(current_entry_idx, block_meta.block_max_value); return; } // Block exhausted @@ -633,24 +807,49 @@ namespace ndd { } inline void advanceToNextLive32() { - auto diff_ptr = static_cast(doc_diffs_ptr); - - if(current_entry_idx < block_data_size && values_ptr[current_entry_idx] > 0) { + if(current_entry_idx < block_data_size && isLiveAt(current_entry_idx)) { const auto& block_meta = (*blocks)[current_block_idx]; - current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - current_score = - dequantize(values_ptr[current_entry_idx], block_meta.block_max_value); + if(diff_bits == 32) { + auto diff_ptr = static_cast(doc_diffs_ptr); + current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; + } +#ifdef NDD_USE_64BIT_IDS + else { + auto diff_ptr = static_cast(doc_diffs_ptr); + current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; + } +#else + else { + current_doc_id = std::numeric_limits::max(); + current_block_idx = blocks->size(); + return; + } +#endif + current_score = valueAt(current_entry_idx, block_meta.block_max_value); return; } - current_entry_idx = - index->findNextLiveSIMD(values_ptr, block_data_size, current_entry_idx); + current_entry_idx = findNextLive(current_entry_idx); if(current_entry_idx < block_data_size) { const auto& block_meta = (*blocks)[current_block_idx]; - current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - current_score = - dequantize(values_ptr[current_entry_idx], block_meta.block_max_value); + if(diff_bits == 32) { + auto diff_ptr = static_cast(doc_diffs_ptr); + current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; + } +#ifdef NDD_USE_64BIT_IDS + else { + auto diff_ptr = static_cast(doc_diffs_ptr); + current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; + } +#else + else { + current_doc_id = std::numeric_limits::max(); + current_block_idx = blocks->size(); + return; + } +#endif + current_score = valueAt(current_entry_idx, block_meta.block_max_value); return; } current_block_idx++; @@ -798,6 +997,8 @@ namespace ndd { } return term_weight * (*blocks)[current_block_idx].block_max_value; } + + float globalUpperBound() const { return term_weight * global_term_max; } }; MDBX_env* env_; @@ -811,7 +1012,6 @@ namespace ndd { // Block management constants static constexpr size_t MAX_BLOCK_SIZE = 128; - static constexpr size_t SPLIT_THRESHOLD = 160; // Optimized SIMD search for 16-bit diffs size_t findEntryIndexSIMD16(const uint16_t* doc_diffs, @@ -1124,6 +1324,16 @@ namespace ndd { std::vector blocks(count); std::memcpy(blocks.data(), data.iov_base, data.iov_len); + if(!blocks.empty() + && blocks.front().start_doc_id != GLOBAL_MAX_SENTINEL_DOC_ID) { + float global_max = 0.0f; + for(const auto& b : blocks) { + global_max = std::max(global_max, b.block_max_value); + } + blocks.insert( + blocks.begin(), BlockIdx(GLOBAL_MAX_SENTINEL_DOC_ID, global_max)); + } + term_blocks_index_[term_id] = std::move(blocks); } rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); @@ -1135,12 +1345,21 @@ namespace ndd { } bool removeDocumentInternal(MDBX_txn* txn, ndd::idInt doc_id, const SparseVector& vec) { + std::unordered_set touched_terms; for(size_t i = 0; i < vec.indices.size(); ++i) { uint32_t term_id = vec.indices[i]; + touched_terms.insert(term_id); if(!removeFromBlock(txn, term_id, doc_id)) { // Ignore errors } } + + for(uint32_t term_id : touched_terms) { + if(!saveTermIndex(txn, term_id)) { + return false; + } + } + return true; } @@ -1179,17 +1398,35 @@ namespace ndd { // Save the index structure (block list) for a single term bool saveTermIndex(MDBX_txn* txn, uint32_t term_id) { auto it = term_blocks_index_.find(term_id); - if(it == term_blocks_index_.end()) { - return true; + MDBX_val key; + key.iov_base = const_cast(static_cast(&term_id)); + key.iov_len = sizeof(uint32_t); + + if(it == term_blocks_index_.end() || it->second.empty()) { + int rc = mdbx_del(txn, term_blocks_index_dbi_, &key, nullptr); + return rc == MDBX_SUCCESS || rc == MDBX_NOTFOUND; } - const auto& blocks = it->second; + auto& blocks = it->second; + size_t first_idx = firstRealBlockIndex(blocks); + if(first_idx >= blocks.size()) { + term_blocks_index_.erase(it); + int rc = mdbx_del(txn, term_blocks_index_dbi_, &key, nullptr); + return rc == MDBX_SUCCESS || rc == MDBX_NOTFOUND; + } - MDBX_val key, data; + float global_max = 0.0f; + for(size_t i = first_idx; i < blocks.size(); ++i) { + global_max = std::max(global_max, blocks[i].block_max_value); + } - // Safe to cast away const for API as MDBX copies - key.iov_base = const_cast(static_cast(&term_id)); - key.iov_len = sizeof(uint32_t); + if(first_idx == 0) { + blocks.insert(blocks.begin(), BlockIdx(GLOBAL_MAX_SENTINEL_DOC_ID, global_max)); + } else { + blocks[0].block_max_value = global_max; + } + + MDBX_val data; data.iov_base = const_cast(static_cast(blocks.data())); data.iov_len = blocks.size() * sizeof(BlockIdx); @@ -1258,6 +1495,38 @@ namespace ndd { entries[i].value = dequantize(val_ptr[i], header->block_max_value); } } +#endif + else { + LOG_ERROR("Unsupported block diff_bits: " << (int)header->diff_bits); + } + } else if(header->version == 4) { + const void* diff_ptr = ptr; + const float* val_ptr; + + if(header->diff_bits == 16) { + val_ptr = reinterpret_cast(ptr + n * sizeof(uint16_t)); + const uint16_t* diffs = static_cast(diff_ptr); + for(size_t i = 0; i < n; ++i) { + entries[i].doc_diff = diffs[i]; + entries[i].value = val_ptr[i]; + } + } else if(header->diff_bits == 32) { + val_ptr = reinterpret_cast(ptr + n * sizeof(uint32_t)); + const uint32_t* diffs = static_cast(diff_ptr); + for(size_t i = 0; i < n; ++i) { + entries[i].doc_diff = diffs[i]; + entries[i].value = val_ptr[i]; + } + } +#ifdef NDD_USE_64BIT_IDS + else if(header->diff_bits == 64) { + val_ptr = reinterpret_cast(ptr + n * sizeof(uint64_t)); + const uint64_t* diffs = static_cast(diff_ptr); + for(size_t i = 0; i < n; ++i) { + entries[i].doc_diff = diffs[i]; + entries[i].value = val_ptr[i]; + } + } #endif else { LOG_ERROR("Unsupported block diff_bits: " << (int)header->diff_bits); @@ -1269,18 +1538,6 @@ namespace ndd { return entries; } - // Helper for uint8 quantization - static inline uint8_t quantize(float val, float max_val) { - if(max_val <= 1e-9f) { - return 0; - } - float scaled = (val / max_val) * 255.0f; - if(scaled > 255.0f) { - return 255; - } - return static_cast(scaled); - } - bool saveBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt start_doc_id, @@ -1320,7 +1577,12 @@ namespace ndd { header.block_max_value = max_val; header.live_count = static_cast(live); header.n = static_cast(n); + +#if defined(NDD_BMW_STORE_FLOAT_VALUES) + header.version = 4; +#else header.version = 3; +#endif header.alignment_pad = 0; // Decide diff width (User requested 16-bit blocks when possible) @@ -1341,7 +1603,12 @@ namespace ndd { #endif size_t diff_size = header.diff_bits / 8; - size_t total_size = sizeof(BlockHeader) + n * diff_size + n * sizeof(uint8_t); +#if defined(NDD_BMW_STORE_FLOAT_VALUES) + size_t value_size = sizeof(float); +#else + size_t value_size = sizeof(uint8_t); +#endif + size_t total_size = sizeof(BlockHeader) + (n * diff_size) + (n * value_size); std::vector buffer(total_size); @@ -1375,11 +1642,18 @@ namespace ndd { } #endif - // Copy values (Quantized) + // Copy values +#if defined(NDD_BMW_STORE_FLOAT_VALUES) + float* values = reinterpret_cast(ptr); + for(size_t i = 0; i < n; ++i) { + values[i] = entries[i].value; + } +#else uint8_t* values = static_cast(ptr); for(size_t i = 0; i < n; ++i) { values[i] = quantize(entries[i].value, max_val); } +#endif MDBX_val data; data.iov_base = buffer.data(); @@ -1389,6 +1663,86 @@ namespace ndd { return rc == 0; } + bool deleteBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt start_doc_id) { + struct { + uint32_t t; + ndd::idInt d; + } __attribute__((packed)) key_struct; + key_struct.t = term_id; + key_struct.d = start_doc_id; + + MDBX_val key; + key.iov_base = &key_struct; + key.iov_len = sizeof(key_struct); + + int rc = mdbx_del(txn, term_blocks_dbi_, &key, nullptr); + return rc == MDBX_SUCCESS || rc == MDBX_NOTFOUND; + } + + bool compactBlockAfterDelete(MDBX_txn* txn, + uint32_t term_id, + size_t block_idx, + const std::vector& entries_with_tombstones) { + auto term_it = term_blocks_index_.find(term_id); + if(term_it == term_blocks_index_.end()) { + return true; + } + + auto& blocks = term_it->second; + if(block_idx >= blocks.size()) { + return true; + } + + ndd::idInt old_start_doc_id = blocks[block_idx].start_doc_id; + + std::vector live_entries; + live_entries.reserve(entries_with_tombstones.size()); + for(const auto& entry : entries_with_tombstones) { + if(entry.value > 0.0f) { + live_entries.push_back(entry); + } + } + + if(live_entries.empty()) { + if(!deleteBlock(txn, term_id, old_start_doc_id)) { + return false; + } + + blocks.erase(blocks.begin() + static_cast(block_idx)); + if(blocks.empty()) { + term_blocks_index_.erase(term_it); + } + return true; + } + + ndd::idInt start_shift = live_entries.front().doc_diff; + ndd::idInt new_start_doc_id = old_start_doc_id + start_shift; + + if(start_shift != 0) { + for(auto& entry : live_entries) { + entry.doc_diff -= start_shift; + } + } + + BlockHeader header; + bool need_rekey = (new_start_doc_id != old_start_doc_id); + + if(need_rekey) { + if(!deleteBlock(txn, term_id, old_start_doc_id)) { + return false; + } + } + + if(!saveBlock(txn, term_id, new_start_doc_id, live_entries, header)) { + return false; + } + + blocks[block_idx].start_doc_id = new_start_doc_id; + blocks[block_idx].block_max_value = header.block_max_value; + + return true; + } + // Returns pointer to block data valid for the duration of txn BlockView getReadOnlyBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt start_doc_id) { // Zero-copy key creation on stack @@ -1409,23 +1763,40 @@ namespace ndd { if(rc == MDBX_SUCCESS && data.iov_len >= sizeof(BlockHeader)) { const BlockHeader* header = reinterpret_cast(data.iov_base); - // Validate size roughly (min size) - if(data.iov_len < sizeof(BlockHeader) + header->n) { - return {nullptr, nullptr, 0, 0}; + size_t diff_size = 0; + if(header->diff_bits == 16) { + diff_size = sizeof(uint16_t); + } else if(header->diff_bits == 32) { + diff_size = sizeof(uint32_t); + } +#ifdef NDD_USE_64BIT_IDS + else if(header->diff_bits == 64) { + diff_size = sizeof(uint64_t); + } +#endif + else { + return {nullptr, nullptr, 0, 0, 0}; + } + + size_t value_size = (header->version == 4) ? sizeof(float) : sizeof(uint8_t); + size_t required_size = sizeof(BlockHeader) + header->n * diff_size + header->n * value_size; + if(data.iov_len < required_size) { + return {nullptr, nullptr, 0, 0, 0}; } const uint8_t* ptr = static_cast(data.iov_base) + sizeof(BlockHeader); - size_t diff_size = - (header->diff_bits == 16) ? sizeof(uint16_t) : sizeof(ndd::idInt); - const void* doc_diffs = ptr; const uint8_t* values = ptr + header->n * diff_size; - return {doc_diffs, values, header->n, header->diff_bits}; + return {doc_diffs, + values, + header->n, + header->diff_bits, + static_cast((header->version == 4) ? 32 : 8)}; } - return {nullptr, nullptr, 0, 0}; + return {nullptr, nullptr, 0, 0, 0}; } ndd::idInt getBlockEndDocId(const std::vector& blocks, size_t block_idx) const { @@ -1497,7 +1868,13 @@ namespace ndd { } // Check if block needs splitting - if(block_entries.size() > SPLIT_THRESHOLD) { + if(block_entries.size() > MAX_BLOCK_SIZE) { + BlockHeader header; + bool saved = saveBlock(txn, term_id, block_it->start_doc_id, block_entries, header); + if(!saved) { + return false; + } + block_it->block_max_value = header.block_max_value; return splitBlock(txn, term_id, block_it->start_doc_id); } @@ -1506,10 +1883,8 @@ namespace ndd { bool success = saveBlock(txn, term_id, block_it->start_doc_id, block_entries, header); if(success) { - // Update index with possibly new max value - if(header.block_max_value > block_it->block_max_value) { - block_it->block_max_value = header.block_max_value; // Update cached max - } + // Keep cached block max synchronized (increase or decrease). + block_it->block_max_value = header.block_max_value; } return success; } @@ -1535,6 +1910,7 @@ namespace ndd { // Load block auto block_entries = loadBlock(txn, term_id, block_it->start_doc_id); + size_t block_idx = std::distance(blocks.begin(), block_it); ndd::idInt doc_diff = doc_id - block_it->start_doc_id; auto entry_it = std::lower_bound( @@ -1545,7 +1921,18 @@ namespace ndd { BlockHeader header; // Fields set by saveBlock - return saveBlock(txn, term_id, block_it->start_doc_id, block_entries, header); + bool success = saveBlock(txn, term_id, block_it->start_doc_id, block_entries, header); + if(success) { + block_it->block_max_value = header.block_max_value; + + // Deterministic 1/8 compaction trigger to avoid extra RNG overhead. + if((doc_id % 8) == 0) { + if(!compactBlockAfterDelete(txn, term_id, block_idx, block_entries)) { + return false; + } + } + } + return success; } return false; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 99c15c743b..0793a2e2f3 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -36,10 +36,5 @@ target_include_directories(ndd_filter_test PRIVATE # Add other necessary definitions target_compile_definitions(ndd_filter_test PRIVATE MDB_MAXKEYSIZE=512) -# SIMD flags if needed (copying from root CMake might be needed if filter/roaring depends on it, -# but usually for unit tests basic flags or inheriting form parent context helps if we used a lib) -# For now, let's assume standard compilation is enough unless Roaring requires it explicitly. -# Actually Roaring usually does runtime dispatch. - include(GoogleTest) gtest_discover_tests(ndd_filter_test) From ff20d35199bd4bb047dde7d2e4365e2f5e6a757d Mon Sep 17 00:00:00 2001 From: shaleenji Date: Wed, 25 Feb 2026 07:21:58 +0000 Subject: [PATCH 24/48] cleanup bmw (#51) --- CMakeLists.txt | 6 + src/core/types.hpp | 12 +- src/sparse/bmw.hpp | 204 +++++---------------------------- src/storage/vector_storage.hpp | 6 +- src/utils/settings.hpp | 10 +- 5 files changed, 46 insertions(+), 192 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 0d6fc30fac..ce8b6e24c0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,11 +80,13 @@ if(ND_DEBUG) add_definitions(-DND_DEBUG) endif() + # SIMD Optimization Options option(USE_AVX512 "Enable AVX512 (F, BW, VNNI, FP16)" OFF) option(USE_AVX2 "Enable AVX2 (FMA, F16C)" OFF) option(USE_SVE2 "Enable SVE2 (INT8/16, FP16)" OFF) option(USE_NEON "Enable NEON (FP16, DotProd)" OFF) +option(NDD_BMW_STORE_FLOAT_VALUES "Store raw float 32 values in BMW index (no quantization)" OFF) # Check if any SIMD option is selected if(NOT USE_AVX512 AND NOT USE_AVX2 AND NOT USE_SVE2 AND NOT USE_NEON) @@ -291,6 +293,10 @@ elseif(USE_NEON) target_compile_definitions(${NDD_BINARY_NAME} PRIVATE USE_NEON) endif() +if(NDD_BMW_STORE_FLOAT_VALUES) + target_compile_definitions(${NDD_BINARY_NAME} PRIVATE NDD_BMW_STORE_FLOAT_VALUES) +endif() + # Add ASIO definitions target_compile_definitions(${NDD_BINARY_NAME} PRIVATE ASIO_STANDALONE diff --git a/src/core/types.hpp b/src/core/types.hpp index d78fd87651..3c65ec09d8 100644 --- a/src/core/types.hpp +++ b/src/core/types.hpp @@ -1,9 +1,7 @@ #pragma once #include -// Compile-time configuration for ID width. -// Define NDD_USE_64BIT_IDS in your build system (e.g., CMake -DNDD_USE_64BIT_IDS=ON) -// to enable 64-bit IDs. Default is 32-bit for performance/memory efficiency. +//ID is 32-bit for performance/memory efficiency. #include "../../third_party/roaring_bitmap/roaring.hh" #include "../utils/settings.hpp" @@ -15,16 +13,8 @@ namespace ndd { size_t boost_percentage = settings::FILTER_BOOST_PERCENTAGE; }; -#ifdef NDD_USE_64BIT_IDS - // --- 64-bit Configuration --- - using idInt = uint64_t; // External ID (stored in DB, exposed to user) - using idhInt = uint64_t; // Internal HNSW ID (used inside HNSW structures) - using RoaringBitmap = roaring::Roaring64Map; -#else - // --- 32-bit Configuration (Default) --- using idInt = uint32_t; // External ID (stored in DB, exposed to user) using idhInt = uint32_t; // Internal HNSW ID (used inside HNSW structures) using RoaringBitmap = roaring::Roaring; -#endif } //namespace ndd diff --git a/src/sparse/bmw.hpp b/src/sparse/bmw.hpp index f2809fc14e..861c523a95 100644 --- a/src/sparse/bmw.hpp +++ b/src/sparse/bmw.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include "../core/types.hpp" @@ -293,13 +294,6 @@ namespace ndd { // Helper to sort iterators by current doc ID auto sort_iterators = [&]() { -#if defined(NDD_BMW_USE_STD_SORT) - std::sort(iterators.begin(), - iterators.end(), - [](BlockIterator* a, BlockIterator* b) { - return a->current_doc_id < b->current_doc_id; - }); -#else if(iterators.size() < 2) { return; } @@ -318,7 +312,6 @@ namespace ndd { break; } } -#endif }; sort_iterators(); @@ -340,43 +333,6 @@ namespace ndd { if(iterators.empty()) { break; } - -#if defined(NDD_BMW_EXHAUSTIVE_DAAT) - ndd::idInt current_doc_id = iterators.front()->current_doc_id; - - if(filter && !filter->contains(current_doc_id)) { - for(size_t i = 0; i < iterators.size(); ++i) { - if(iterators[i]->current_doc_id == current_doc_id) { - iterators[i]->next(); - } - } - sort_iterators(); - continue; - } - - float score = 0.0f; - for(size_t i = 0; i < iterators.size(); ++i) { - if(iterators[i]->current_doc_id == current_doc_id) { - score += iterators[i]->current_score * iterators[i]->term_weight; - iterators[i]->next(); - } - } - - if(top_k.size() < k) { - top_k.emplace(current_doc_id, score); - if(top_k.size() == k) { - threshold = top_k.top().score; - } - } else if(score > threshold) { - top_k.pop(); - top_k.emplace(current_doc_id, score); - threshold = top_k.top().score; - } - - sort_iterators(); - continue; -#else - if(remaining_global_upper_bound < 0.0f) { remaining_global_upper_bound = 0.0f; } @@ -446,18 +402,10 @@ namespace ndd { threshold = top_k.top().score; } } else { -#if defined(NDD_BMW_LEGACY_ADVANCE_ALL_PREDECESSORS) - for(size_t i = 0; i < pivot_idx; ++i) { - iterators[i]->advance(pivot_doc_id); - } -#else // Standard WAND/BMW behavior: advance only the first iterator to the pivot. iterators[0]->advance(pivot_doc_id); -#endif } - sort_iterators(); -#endif } // Clean up @@ -490,7 +438,7 @@ namespace ndd { } auto entries = loadBlock(txn, term_id, start_doc_id); - if(entries.size() <= MAX_BLOCK_SIZE) { + if(entries.size() <= settings::MAX_BMW_BLOCK_SIZE) { return true; } @@ -595,7 +543,7 @@ namespace ndd { } std::vector::iterator findBlockIterator(std::vector& blocks, - ndd::idInt doc_id) { + ndd::idInt doc_id) { size_t first_idx = firstRealBlockIndex(blocks); if(first_idx >= blocks.size()) { return blocks.end(); @@ -603,11 +551,11 @@ namespace ndd { auto begin_it = blocks.begin() + static_cast(first_idx); auto it = std::upper_bound(begin_it, - blocks.end(), - doc_id, - [](ndd::idInt doc_id, const BlockIdx& block) { - return doc_id < block.start_doc_id; - }); + blocks.end(), + doc_id, + [](ndd::idInt doc_id, const BlockIdx& block) { + return doc_id < block.start_doc_id; + }); if(it == begin_it) { return it; @@ -624,11 +572,11 @@ namespace ndd { auto begin_it = blocks.begin() + static_cast(first_idx); auto it = std::upper_bound(begin_it, - blocks.end(), - doc_id, - [](ndd::idInt doc_id, const BlockIdx& block) { - return doc_id < block.start_doc_id; - }); + blocks.end(), + doc_id, + [](ndd::idInt doc_id, const BlockIdx& block) { + return doc_id < block.start_doc_id; + }); if(it == begin_it) { return it; @@ -639,15 +587,17 @@ namespace ndd { /** * The quantize and dequantize functions are there to reduce the memory * and storage footprint of the sparse values (float 32 to int8). + * + * XXX: Here we are assuming that sparse vectors can never have -ve values. */ // Helper for uint8 quantization static inline uint8_t quantize(float val, float max_val) { - if(max_val <= 1e-9f) { + if(max_val <= settings::NEAR_ZERO) { return 0; } - float scaled = (val / max_val) * 255.0f; - if(scaled >= 255.0f) { - return 255; + float scaled = (val / max_val) * UINT8_MAX; + if(scaled >= UINT8_MAX) { + return UINT8_MAX; } if (scaled <= 0.0f) return 0; @@ -656,12 +606,12 @@ namespace ndd { static inline float dequantize(uint8_t val, float max_val) { // If max_val is near zero, the result is effectively zero - if (max_val <= 1e-9f) { + if (max_val <= settings::NEAR_ZERO) { return 0.0f; } // Use a single multiplier to avoid multiple floating point ops - const float scale = max_val / 255.0f; + const float scale = max_val / UINT8_MAX; return static_cast(val) * scale; } @@ -697,10 +647,10 @@ namespace ndd { MDBX_txn* txn; BlockIterator(uint32_t tid, - float weight, - const std::vector* blks, - BMWIndex* idx, - MDBX_txn* t) : + float weight, + const std::vector* blks, + BMWIndex* idx, + MDBX_txn* t) : term_id(tid), term_weight(weight), blocks(blks), @@ -813,18 +763,11 @@ namespace ndd { auto diff_ptr = static_cast(doc_diffs_ptr); current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; } -#ifdef NDD_USE_64BIT_IDS - else { - auto diff_ptr = static_cast(doc_diffs_ptr); - current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - } -#else else { current_doc_id = std::numeric_limits::max(); current_block_idx = blocks->size(); return; } -#endif current_score = valueAt(current_entry_idx, block_meta.block_max_value); return; } @@ -837,18 +780,11 @@ namespace ndd { auto diff_ptr = static_cast(doc_diffs_ptr); current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; } -#ifdef NDD_USE_64BIT_IDS - else { - auto diff_ptr = static_cast(doc_diffs_ptr); - current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - } -#else else { current_doc_id = std::numeric_limits::max(); current_block_idx = blocks->size(); return; } -#endif current_score = valueAt(current_entry_idx, block_meta.block_max_value); return; } @@ -920,8 +856,8 @@ namespace ndd { const auto& block_meta = (*blocks)[current_block_idx]; if(target_doc_id > block_meta.start_doc_id) { ndd::idInt diff = target_doc_id - block_meta.start_doc_id; - // If diff > 65535, we know it's not in this 16-bit block - if(diff > 65535) { + // If diff > UINT16_MAX, we know it's not in this 16-bit block + if(diff > UINT16_MAX) { current_entry_idx = block_data_size; } else { current_entry_idx = index->findEntryIndexSIMD16( @@ -971,22 +907,11 @@ namespace ndd { const auto& block_meta = (*blocks)[current_block_idx]; if(target_doc_id > block_meta.start_doc_id) { ndd::idInt diff = target_doc_id - block_meta.start_doc_id; - -#ifdef NDD_USE_64BIT_IDS - // 64-bit ID build: Supports 32-bit compressed blocks and 64-bit full blocks. - // Note: 16-bit blocks are handled by advance16/SIMD16. - current_entry_idx = index->findEntryIndexGeneric( - doc_diffs_ptr, block_data_size, current_entry_idx, diff, diff_bits); -#else - // 32-bit ID build: Supports only 32-bit blocks here. - // diff fits in 32 bits. current_entry_idx = index->findEntryIndexSIMD32(static_cast(doc_diffs_ptr), block_data_size, current_entry_idx, static_cast(diff)); -#endif - advanceToNextLive32(); } } @@ -1011,7 +936,6 @@ namespace ndd { mutable std::shared_mutex mutex_; // Block management constants - static constexpr size_t MAX_BLOCK_SIZE = 128; // Optimized SIMD search for 16-bit diffs size_t findEntryIndexSIMD16(const uint16_t* doc_diffs, @@ -1192,31 +1116,11 @@ namespace ndd { size_t start_idx, ndd::idInt target_diff, uint8_t bits) { -// In 64-bit mode, we might encounter 32-bit compressed blocks or 64-bit blocks -#ifdef NDD_USE_64BIT_IDS - if(bits == 32) { - if(target_diff > 0xFFFFFFFFULL) { - return size; // Optimization: target exceeds max possible value in 32-bit block - } - return findEntryIndexSIMD32(static_cast(doc_diffs), - size, - start_idx, - static_cast(target_diff)); - } else { - size_t idx = start_idx; - const uint64_t* ptr = static_cast(doc_diffs); - while(idx < size && ptr[idx] < target_diff) { - idx++; - } - return idx; - } -#else // In 32-bit mode, we only expect 32-bit blocks here (16-bit handled by SIMD16) return findEntryIndexSIMD32(static_cast(doc_diffs), size, start_idx, static_cast(target_diff)); -#endif } // Find next non-zero value (live entry) @@ -1486,16 +1390,6 @@ namespace ndd { entries[i].value = dequantize(val_ptr[i], header->block_max_value); } } -#ifdef NDD_USE_64BIT_IDS - else if(header->diff_bits == 64) { - val_ptr = ptr + n * sizeof(uint64_t); - const uint64_t* diffs = static_cast(diff_ptr); - for(size_t i = 0; i < n; ++i) { - entries[i].doc_diff = diffs[i]; - entries[i].value = dequantize(val_ptr[i], header->block_max_value); - } - } -#endif else { LOG_ERROR("Unsupported block diff_bits: " << (int)header->diff_bits); } @@ -1518,16 +1412,6 @@ namespace ndd { entries[i].value = val_ptr[i]; } } -#ifdef NDD_USE_64BIT_IDS - else if(header->diff_bits == 64) { - val_ptr = reinterpret_cast(ptr + n * sizeof(uint64_t)); - const uint64_t* diffs = static_cast(diff_ptr); - for(size_t i = 0; i < n; ++i) { - entries[i].doc_diff = diffs[i]; - entries[i].value = val_ptr[i]; - } - } -#endif else { LOG_ERROR("Unsupported block diff_bits: " << (int)header->diff_bits); } @@ -1585,22 +1469,11 @@ namespace ndd { #endif header.alignment_pad = 0; -// Decide diff width (User requested 16-bit blocks when possible) -#ifdef NDD_USE_64BIT_IDS - if(max_diff < 65536) { - header.diff_bits = 16; - } else if(max_diff < 4294967296ULL) { - header.diff_bits = 32; - } else { - header.diff_bits = 64; - } -#else - if(max_diff < 65536) { + if(max_diff <= UINT16_MAX) { header.diff_bits = 16; } else { header.diff_bits = 32; } -#endif size_t diff_size = header.diff_bits / 8; #if defined(NDD_BMW_STORE_FLOAT_VALUES) @@ -1631,16 +1504,6 @@ namespace ndd { } ptr += n * sizeof(uint32_t); } -#ifdef NDD_USE_64BIT_IDS - else { - // 64-bit case - uint64_t* diffs = reinterpret_cast(ptr); - for(size_t i = 0; i < n; ++i) { - diffs[i] = static_cast(entries[i].doc_diff); - } - ptr += n * sizeof(uint64_t); - } -#endif // Copy values #if defined(NDD_BMW_STORE_FLOAT_VALUES) @@ -1769,11 +1632,6 @@ namespace ndd { } else if(header->diff_bits == 32) { diff_size = sizeof(uint32_t); } -#ifdef NDD_USE_64BIT_IDS - else if(header->diff_bits == 64) { - diff_size = sizeof(uint64_t); - } -#endif else { return {nullptr, nullptr, 0, 0, 0}; } @@ -1813,13 +1671,13 @@ namespace ndd { // Find the appropriate block auto block_it = findBlockIterator(blocks, doc_id); - // Check if we need to split due to range (if > 65535, cannot fit in uint16 diff) + // Check if we need to split due to range (if > UINT16_MAX, cannot fit in uint16 diff) // This is a constraint for 16-bit blocks. If we enable mix, we don't strict need to // check unless we want to force 16-bit. bool force_new_block = false; if(block_it != blocks.end() && block_it->start_doc_id <= doc_id) { - if((doc_id - block_it->start_doc_id) >= 65536) { + if((doc_id - block_it->start_doc_id) > UINT16_MAX) { force_new_block = true; } } @@ -1868,7 +1726,7 @@ namespace ndd { } // Check if block needs splitting - if(block_entries.size() > MAX_BLOCK_SIZE) { + if(block_entries.size() > settings::MAX_BMW_BLOCK_SIZE) { BlockHeader header; bool saved = saveBlock(txn, term_id, block_it->start_doc_id, block_entries, header); if(!saved) { diff --git a/src/storage/vector_storage.hpp b/src/storage/vector_storage.hpp index 993c63bd7a..428c1756ac 100644 --- a/src/storage/vector_storage.hpp +++ b/src/storage/vector_storage.hpp @@ -42,6 +42,8 @@ class VectorStore { throw std::runtime_error("Failed to set geometry"); } + mdbx_env_set_maxdbs(env_, settings::MAX_NR_SUBINDEX); + rc = mdbx_env_open( env_, path_.c_str(), MDBX_WRITEMAP | MDBX_MAPASYNC | MDBX_NORDAHEAD, 0664); if(rc != MDBX_SUCCESS) { @@ -54,7 +56,7 @@ class VectorStore { throw std::runtime_error("Failed to begin transaction"); } - rc = mdbx_dbi_open(txn, nullptr, MDBX_CREATE | MDBX_INTEGERKEY, &dbi_); + rc = mdbx_dbi_open(txn, settings::DEFAULT_SUBINDEX.c_str(), MDBX_CREATE | MDBX_INTEGERKEY, &dbi_); if(rc != MDBX_SUCCESS) { mdbx_txn_abort(txn); throw std::runtime_error("Failed to open database"); @@ -63,7 +65,7 @@ class VectorStore { rc = mdbx_txn_commit(txn); if(rc != MDBX_SUCCESS) { throw std::runtime_error("Failed to commit transaction: " - + std::string(mdbx_strerror(rc))); + + std::string(mdbx_strerror(rc))); } } diff --git a/src/utils/settings.hpp b/src/utils/settings.hpp index 20f78690de..5fa3ef85fc 100644 --- a/src/utils/settings.hpp +++ b/src/utils/settings.hpp @@ -62,11 +62,8 @@ namespace settings { constexpr size_t MAX_LINK_LIST_LOCKS = 65536; // Sparse Storage settings - constexpr uint16_t MAX_BLOCK_SIZE = 128; // Number of elements in a block - constexpr uint32_t DEFAULT_VOCAB_SIZE = 0; // 0 means dense vectors only - constexpr uint8_t DEFAULT_QUANT_BITS = 8; - constexpr uint16_t BLOCK_SPLIT_THRESHOLD = - 160; // Bloc will be split if more than this many elements (including tombstones) + constexpr size_t MAX_BMW_BLOCK_SIZE = 128; + constexpr float NEAR_ZERO = 1e-9f; // Maximum number of elements in the index constexpr size_t MAX_VECTORS_ADMIN = 1'000'000'000; @@ -89,7 +86,8 @@ namespace settings { constexpr size_t DEFAULT_SERVER_PORT = 8080; const std::string DEFAULT_SERVER_TYPE = "OSS"; const std::string DEFAULT_DATA_DIR = "/mnt/data"; - const std::string DEFAULT_SUBINDEX = "DEFAULT"; + const std::string DEFAULT_SUBINDEX = "default"; + constexpr size_t MAX_NR_SUBINDEX = 100; //Maximum number of subindexes constexpr size_t DEFAULT_MAX_ACTIVE_INDICES = 64; constexpr size_t DEFAULT_MAX_ELEMENTS = 100'000; constexpr size_t DEFAULT_MAX_ELEMENTS_INCREMENT = 100'000; From a5e42bce7a143a456e1409c84bf31610bd97cd36 Mon Sep 17 00:00:00 2001 From: vindwi <130017173+vindwi@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:54:28 +0530 Subject: [PATCH 25/48] Major re-architecture and bug fixes This has major changes related to graph, backfill, vector cache and SIMD and few bug fixes. Not an ideal commit. 1. Backfill now does not require extra neighbors. This means that the index will be consistent 2. SIMD optimization for AVX512 and SVE2 3. Narrow beam search at L1 is replaced with greedy search 4. Hybrid quantization is removed. It had memory efficient and speed gain at the cost of minor impact on recall. 5. We are not multiplying in batches. This will make porting to GPU easier. We can also optimize the registers with sticky query vector. 6. Vector cache has 7. Bug fix where enry id was updated. It was creating a separate graph. 9. Bug fix when updating for vector cache refresh. --- CMakeLists.txt | 4 +- src/core/ndd.hpp | 12 + src/hnsw/hnswalg.h | 904 ++++++++++++++++++++++++--------- src/hnsw/vector_cache.h | 183 ++++--- src/quant/binary.hpp | 77 +++ src/quant/common.hpp | 22 + src/quant/float16.hpp | 189 +++++++ src/quant/float32.hpp | 177 +++++++ src/quant/int16.hpp | 416 ++++++++++++--- src/quant/int8.hpp | 260 +++++++++- src/storage/vector_storage.hpp | 40 ++ src/utils/settings.hpp | 12 +- 12 files changed, 1908 insertions(+), 388 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ce8b6e24c0..e76e5ba9fd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -280,8 +280,8 @@ elseif(USE_AVX2) target_compile_options(${NDD_BINARY_NAME} PRIVATE -mavx2 -mfma -mf16c) target_compile_definitions(${NDD_BINARY_NAME} PRIVATE USE_AVX2) elseif(USE_SVE2) - message(STATUS "SIMD: SVE2 enabled (ARMv8.6-a + SVE2 + FP16)") - target_compile_options(${NDD_BINARY_NAME} PRIVATE -march=armv8.6-a+sve2+fp16) + message(STATUS "SIMD: SVE2 enabled (ARMv8.6-a + SVE2 + FP16 + DotProd)") + target_compile_options(${NDD_BINARY_NAME} PRIVATE -march=armv8.6-a+sve2+fp16+dotprod) target_compile_definitions(${NDD_BINARY_NAME} PRIVATE USE_SVE2) elseif(USE_NEON) message(STATUS "SIMD: NEON enabled") diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index faea98237f..bc779838d5 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -879,6 +879,10 @@ class IndexManager { return vs->get_vector(label, buffer); }); + alg->setVectorFetcherBatch([vs = vector_storage](const ndd::idInt* labels, uint8_t* buffers, bool* success, size_t count) -> size_t { + return vs->get_vectors_batch_into(labels, buffers, success, count); + }); + // Create WAL during index creation getOrCreateWAL(index_id); @@ -982,6 +986,10 @@ class IndexManager { return vs->get_vector(label, buffer); }); + alg->setVectorFetcherBatch([vs = vector_storage](const ndd::idInt* labels, uint8_t* buffers, bool* success, size_t count) -> size_t { + return vs->get_vectors_batch_into(labels, buffers, success, count); + }); + LOG_DEBUG("Loaded index: " << index_id); LOG_DEBUG("Created space for index: " << index_id); @@ -1078,6 +1086,10 @@ class IndexManager { return vs->get_vector(label, buffer); }); + new_alg->setVectorFetcherBatch([vs = entry.vector_storage](const ndd::idInt* labels, uint8_t* buffers, bool* success, size_t count) -> size_t { + return vs->get_vectors_batch_into(labels, buffers, success, count); + }); + // Replace the algorithm in the existing entry entry.alg = std::move(new_alg); } diff --git a/src/hnsw/hnswalg.h b/src/hnsw/hnswalg.h index d968e57da4..38ed3cbaba 100644 --- a/src/hnsw/hnswalg.h +++ b/src/hnsw/hnswalg.h @@ -44,6 +44,15 @@ namespace hnswlib { std::vector, CompareBySecond>; using VectorFetcher = std::function; + // Batch fetcher: fetches multiple vectors in one MDBX txn + // Args: labels array, output buffers (flat, count*vector_size), success flags, count + // Returns: number of successful fetches + using VectorFetcherBatch = std::function; + using SimBatchFunc = void (*)(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out); public: // Constructors and destructor @@ -91,29 +100,12 @@ namespace hnswlib { size_t cache_bits = VectorCache::calculateCacheBits(maxElements_); if (cache_bits > 0) { vector_cache_ = std::make_unique(data_size_, cache_bits); - LOG_DEBUG("Vector cache initialized for " << maxElements_ << " elements with " << (1 << cache_bits) << " slots"); + LOG_DEBUG("Vector cache initialized for " << maxElements_ << " elements with " << (1ULL << cache_bits) << " slots"); + size_t cache_bytes = vector_cache_->getMemoryUsage(); + LOG_DEBUG("Vector cache allocated: " << cache_bytes << " bytes (" << (cache_bytes / MB) << " MB)"); } - // Initialize upper layer space - bool use_hybrid = true; - if(quant_level_ == ndd::quant::QuantizationLevel::BINARY) { - use_hybrid = false; - } - - if(use_hybrid) { - space_upper_ = std::unique_ptr>(createSpace( - space_type_, dimension_, ndd::quant::QuantizationLevel::INT8)); - LOG_DEBUG("Upper layer initialized with Hybrid Quantization (INT8)"); - } else { - space_upper_ = std::unique_ptr>( - createSpace(space_type_, dimension_, quant_level_)); - LOG_DEBUG("Upper layer initialized with same space as base layer"); - } - - data_size_upper_ = space_upper_->get_data_size(); - fstSimFuncUpper_ = space_upper_->get_sim_func(); - dist_func_param_upper_ = space_upper_->get_dist_func_param(); - LOG_DEBUG("Upper layer data size: " << data_size_upper_); + LOG_DEBUG("Unified layer data size: " << data_size_); // M_ cannot be more than settings::MAX_M if(M_ > settings::MAX_M) { @@ -166,6 +158,7 @@ namespace hnswlib { SpaceInterface* getSpace() const { return space_.get(); } size_t getDataSize() const { return data_size_; } void setVectorFetcher(VectorFetcher fetcher) { vector_fetcher_ = fetcher; } + void setVectorFetcherBatch(VectorFetcherBatch fetcher) { vector_fetcher_batch_ = fetcher; } size_t getDimension() const { return dimension_; } size_t getM() const { return M_; } size_t getEfConstruction() const { return efConstruction_; } @@ -189,9 +182,9 @@ namespace hnswlib { // Approximate level > 0 count size_t upper_layer_estimate = maxElements_ / M_; - // Upper layer calculation using runtime data size - size += upper_layer_estimate - * (data_size_upper_ + sizeof(levelInt) + sizeLinksUpperLayers_); + // Upper layer calculation using runtime data size + size += upper_layer_estimate + * (data_size_ + sizeof(levelInt) + sizeLinksUpperLayers_); if (vector_cache_) { size += vector_cache_->getMemoryUsage(); @@ -200,20 +193,6 @@ namespace hnswlib { return size / GB; // GB } - // Helper to get data representation for upper layers - std::vector getUpperLayerRepresentation(const void* datapoint) { - if(data_size_upper_ == data_size_) { - // If sizes match, just copy (No hybrid quantization or Same Space) - std::vector res(data_size_); - memcpy(res.data(), datapoint, data_size_); - return res; - } - - // Hybrid quantization enabled (INT8) - auto dispatch = ndd::quant::get_quantizer_dispatch(quant_level_); - return dispatch.quantize_to_int8(datapoint, dimension_); - } - // Cache management getters/setters // Removed as cache is managed externally @@ -224,7 +203,6 @@ namespace hnswlib { size_t ef, FilterFunctor* isIdAllowed, size_t filter_boost_percentage = settings::FILTER_BOOST_PERCENTAGE) const { // Default true as requested - int x = 0; LOG_DEBUG("Inside searchKnn, element count: " << curElementsCount_); std::vector> result; if(curElementsCount_ == 0) { @@ -234,25 +212,19 @@ namespace hnswlib { idhInt currObj = entryPoint_; dist_t curSim; - // Prepare query data for upper layers - std::vector query_data_upper; if(maxLevel_ > 0) { - query_data_upper = - const_cast(this)->getUpperLayerRepresentation(query_data); - // Use direct pointer for upper layers const uint8_t* ep_data = getUpperLayerDataPtr(currObj); if(!ep_data) { return result; } - curSim = fstSimFuncUpper_( - query_data_upper.data(), ep_data, dist_func_param_upper_); + curSim = fstSimFunc_(query_data, ep_data, dist_func_param_); } dist_t s; - // Upper layer traversal - greedy search - for(levelInt level = maxLevel_; level > 1; level--) { + // Upper layer traversal - greedy search (levels maxLevel_ down to 1) + for(levelInt level = maxLevel_; level > 0; level--) { bool changed = true; while(changed) { changed = false; @@ -266,6 +238,11 @@ namespace hnswlib { int size = getListCount(ll_cur); idhInt* data = (idhInt*)(ll_cur + 1); + std::vector valid_candidates; + valid_candidates.reserve(size); + std::vector candidate_ptrs; + candidate_ptrs.reserve(size); + for(int i = 0; i < size; i++) { idhInt candidate = data[i]; if(candidate >= curElementsCount_) { @@ -276,12 +253,27 @@ namespace hnswlib { if(!candidate_data) { continue; } - s = fstSimFuncUpper_( - query_data_upper.data(), candidate_data, dist_func_param_upper_); + valid_candidates.push_back(candidate); + candidate_ptrs.push_back(candidate_data); + } + + if(valid_candidates.empty()) { + continue; + } + + std::vector sims; + computeBatchSimilaritiesFromPtrs(query_data, + candidate_ptrs, + fstSimFunc_, + dist_func_param_, + sims); + + for(size_t i = 0; i < valid_candidates.size(); ++i) { + s = sims[i]; if(s > curSim) { curSim = s; - currObj = candidate; + currObj = valid_candidates[i]; changed = true; } } @@ -290,17 +282,7 @@ namespace hnswlib { std::vector entry_points; if (maxLevel_ > 0) { - std::vector l1_eps = {currObj}; - std::vector> l1_res; - if(deletedElementsCount_) { - l1_res = searchBaseLayer(l1_eps, query_data, 1, M_, isIdAllowed, filter_boost_percentage); - } else { - l1_res = searchBaseLayer(l1_eps, query_data, 1, M_, isIdAllowed, filter_boost_percentage); - } - - for(size_t i = 0; i < std::min((size_t)2, l1_res.size()); ++i) { - entry_points.push_back(l1_res[i].second); - } + entry_points.push_back(currObj); } else { entry_points.push_back(entryPoint_); } @@ -380,9 +362,8 @@ namespace hnswlib { continue; } - // Use data_size_upper_ - level = *reinterpret_cast(dataUpperLayer_[i].get() + data_size_upper_); - total_size = data_size_upper_ + sizeof(levelInt) + level * sizeLinksUpperLayers_; + level = *reinterpret_cast(dataUpperLayer_[i].get() + data_size_); + total_size = data_size_ + sizeof(levelInt) + level * sizeLinksUpperLayers_; writeBinaryPOD(output, static_cast(i)); // write ID output.write(reinterpret_cast(dataUpperLayer_[i].get()), @@ -449,31 +430,14 @@ namespace hnswlib { fstSimFunc_ = space_->get_sim_func(); dist_func_param_ = space_->get_dist_func_param(); - // Initialize upper layer space - bool use_hybrid = true; - if(quant_level_ == ndd::quant::QuantizationLevel::BINARY) { - use_hybrid = false; - } - - if(use_hybrid) { - space_upper_ = std::unique_ptr>(createSpace( - space_type_, dimension_, ndd::quant::QuantizationLevel::INT8)); - } else { - space_upper_ = std::unique_ptr>( - createSpace(space_type_, dimension_, quant_level_)); - } - // Initialize cache for loaded index size_t cache_bits = VectorCache::calculateCacheBits(maxElements_); + LOG_INFO("Calculated cache bits for loaded index: " << cache_bits); if (cache_bits > 0) { vector_cache_ = std::make_unique(data_size_, cache_bits); - LOG_DEBUG("Vector cache initialized for " << maxElements_ << " elements with " << (1 << cache_bits) << " slots"); + LOG_DEBUG("Vector cache initialized for " << maxElements_ << " elements with " << (1ULL << cache_bits) << " slots"); } - data_size_upper_ = space_upper_->get_data_size(); - fstSimFuncUpper_ = space_upper_->get_sim_func(); - dist_func_param_upper_ = space_upper_->get_dist_func_param(); - // Allocate memory and load level 0 data dataBaseLayer_ = (char*)malloc(maxElements_ * sizeDataAtBaseLayer_); if(dataBaseLayer_ == nullptr) { @@ -515,7 +479,7 @@ namespace hnswlib { size_t header_size; // Step 1: Read vector + level header - header_size = data_size_upper_ + sizeof(levelInt); + header_size = data_size_ + sizeof(levelInt); std::vector header_buf(header_size); input.read(reinterpret_cast(header_buf.data()), header_size); @@ -524,7 +488,7 @@ namespace hnswlib { } levelInt level; - level = *reinterpret_cast(header_buf.data() + data_size_upper_); + level = *reinterpret_cast(header_buf.data() + data_size_); size_t total_size = header_size + level * sizeLinksUpperLayers_; @@ -554,21 +518,15 @@ namespace hnswlib { // VECTOR_CACHE_PERCENTAGE) adjustCacheForElementCount(curElementsCount_); } - // Adjust cache bits based on element count and percentage threshold - // cache_percent: percentage of element count the cache should cover (e.g., 5 for 5%) - // void adjustCacheForElementCount(size_t element_count) { - // Cache adjustment is now handled externally or disabled - // } template void addPoint(const void* datapoint, idInt label) { LOG_TIME("addPoint"); - // Generate upper layer representation - std::vector datapoint_upper = getUpperLayerRepresentation(datapoint); + std::unique_lock update_lock(index_lock_, std::defer_lock); - //std::shared_lock lock(index_lock_); idhInt cur_c = 0; levelInt curLevel = 0; + idhInt update_seed = INVALID_ID; if(is_new) { // Adding a new point // Using fetch_add (or post-increment) ensures unique IDs even under contention. @@ -586,33 +544,65 @@ namespace hnswlib { } else { idhInt searchId = labelLookup_[label]; if(searchId != INVALID_ID) { + if(searchId == entryPoint_) { + update_lock.lock(); + } + // If the element is deleted, mark is undeleted first before calling update // point if(isMarkedDeleted(searchId)) { unmarkDeletedInternal(searchId); } curLevel = getElementLevel(searchId); - removeAllConnections(searchId, curLevel); + + // If we're updating the current entry point, pick an alternate traversal seed + // before disconnecting this node. Starting from a just-disconnected entry + // point can degrade into self-only reinsertion. + if(searchId == entryPoint_) { + // Scan from higher levels down, and accept the first valid non-deleted neighbor. + for(int level = curLevel; level >= 0 && update_seed == INVALID_ID; --level) { + idhInt* ll_self = reinterpret_cast( + level == 0 ? get_linklist0(searchId) + : get_linklist(searchId, level)); + if(!ll_self) { + continue; + } + idhInt sz = getListCount(ll_self); + idhInt* neighbors = ll_self + 1; + for(idhInt i = 0; i < sz; ++i) { + idhInt candidate = neighbors[i]; + if(candidate == searchId || candidate >= curElementsCount_) { + continue; + } + if(isMarkedDeleted(candidate)) { + continue; + } + update_seed = candidate; + break; + } + } + + } + + removeAllConnections2(searchId, curLevel); cur_c = searchId; } else { LOG_DEBUG("Label not found, can't update the point" << label); return; } } - // TODO - Check this ..is it thread safe to comment this - - // Put the data in cache. Will speed up initial data load - if (curLevel == 0 && vector_cache_) { - vector_cache_->insert(cur_c, static_cast(datapoint)); + // Update cached value only if this id is already present in cache. + // Do not insert on add/update path; cold ids can be populated on read hot path. + if constexpr(!is_new) { + if(vector_cache_) { + // Fast atomic invalidation instead of taking a cache lock that can deadlock. + // The cache will automatically refresh this vector on the next read miss. + vector_cache_->invalidateSlot(cur_c); + } } // std::unique_lock lock_el(getLinkListMutex(cur_c)); - // Put the data in level 0 memory. - // if (curLevel == 0) { - // memcpy(dataBaseLayer_ + cur_c * sizeDataAtBaseLayer_ + sizeDataAtBaseLayer_ - - // data_size_, datapoint, data_size_); - // } // Initialize level 0 links // TODO - check if it is required { @@ -624,15 +614,15 @@ namespace hnswlib { size_t total_size; if(curLevel > 0) { - total_size = data_size_upper_ + sizeof(levelInt) + curLevel * sizeLinksUpperLayers_; + total_size = data_size_ + sizeof(levelInt) + curLevel * sizeLinksUpperLayers_; auto mem = std::make_unique(total_size); - // copy vector - memcpy(mem.get(), datapoint_upper.data(), data_size_upper_); - memcpy(mem.get() + data_size_upper_, &curLevel, sizeof(levelInt)); + // copy vector + memcpy(mem.get(), datapoint, data_size_); + memcpy(mem.get() + data_size_, &curLevel, sizeof(levelInt)); // zero initialize linklists - memset(mem.get() + data_size_upper_ + sizeof(levelInt), + memset(mem.get() + data_size_ + sizeof(levelInt), 0, curLevel * sizeLinksUpperLayers_); @@ -645,11 +635,13 @@ namespace hnswlib { // IMPORTANT: Check if this element has a higher level than the current max bool has_higher_level = (curLevel > maxlevelcopy); - idhInt currObj = entryPoint_; + idhInt currObj = (update_seed != INVALID_ID) ? update_seed : entryPoint_; + levelInt seed_level = getElementLevel(currObj); + levelInt start_search_level = std::min(maxlevelcopy, seed_level); // Traverse to find closest neighbors at each level // Greedy search till the current level - for(int level = maxlevelcopy; level > curLevel; level--) { + for(int level = start_search_level; level > curLevel; level--) { bool changed = true; while(changed) { changed = false; @@ -661,25 +653,23 @@ namespace hnswlib { int size = getListCount((idhInt*)ll_cur); idhInt* datal = (idhInt*)(ll_cur + 1); - std::vector curr_vec(data_size_upper_); - if(!getDataByInternalId(currObj, level, curr_vec.data())) { + const uint8_t* curr_data = getUpperLayerDataPtr(currObj); + if(!curr_data) { continue; } - dist_t curr_sim = fstSimFuncUpper_(datapoint_upper.data(), - curr_vec.data(), - dist_func_param_upper_); + dist_t curr_sim = fstSimFunc_(datapoint, curr_data, dist_func_param_); for(int i = 0; i < size; i++) { idhInt candidate_id = datal[i]; dist_t s; - std::vector candidate_vec(data_size_upper_); - if(!getDataByInternalId(candidate_id, level, candidate_vec.data())) { + + const uint8_t* candidate_data = getUpperLayerDataPtr(candidate_id); + if(!candidate_data) { continue; } - s = fstSimFuncUpper_(datapoint_upper.data(), - candidate_vec.data(), - dist_func_param_upper_); + + s = fstSimFunc_(datapoint, candidate_data, dist_func_param_); if(s > curr_sim) { curr_sim = s; @@ -691,10 +681,11 @@ namespace hnswlib { } // Add connections from curLevel down to 0 - for(int level = std::min(curLevel, maxlevelcopy); level >= 0; level--) { + levelInt connect_start_level = std::min(curLevel, start_search_level); + for(int level = connect_start_level; level >= 0; level--) { std::vector> sorted_candidates; - const void* level_datapoint = (level == 0) ? datapoint : datapoint_upper.data(); + const void* level_datapoint = datapoint; std::vector cur_eps = {currObj}; if(deletedElementsCount_) { @@ -776,11 +767,10 @@ namespace hnswlib { uint64_t flags_{0}; //Not using flags now. We can use it in future for various options std::unique_ptr> space_; - std::unique_ptr> space_upper_; - size_t dimension_; VectorFetcher vector_fetcher_; + VectorFetcherBatch vector_fetcher_batch_; mutable std::shared_mutex index_lock_; size_t maxElements_{0}; @@ -817,13 +807,30 @@ namespace hnswlib { SIMFUNC fstSimFunc_; void* dist_func_param_{nullptr}; - // Unified upper layer data parameters - size_t data_size_upper_{0}; - SIMFUNC fstSimFuncUpper_; - void* dist_func_param_upper_{nullptr}; - // Cache for vectors mutable std::unique_ptr vector_cache_; + static constexpr size_t CACHE_LOCK_STRIPE_BITS = 10; // 1024 striped locks in HNSW + static constexpr size_t CACHE_LOCK_STRIPE_COUNT = 1 << CACHE_LOCK_STRIPE_BITS; + static constexpr size_t CACHE_LOCK_STRIPE_MASK = CACHE_LOCK_STRIPE_COUNT - 1; + mutable std::array vectorCacheLocks_; + + struct CacheReadView { + std::shared_lock lock; + const uint8_t* data = nullptr; + + explicit operator bool() const { + return data != nullptr; + } + }; + + std::shared_mutex& getCacheMutex(idhInt internal_id) const { + size_t cache_index = static_cast(internal_id); + if(vector_cache_) { + cache_index = vector_cache_->getCacheIndex(internal_id); + } + size_t stripe_id = cache_index & CACHE_LOCK_STRIPE_MASK; + return vectorCacheLocks_[stripe_id]; + } public: const VectorCache* getCache() const { @@ -870,35 +877,230 @@ namespace hnswlib { return dataUpperLayer_[internal_id].get(); } - // Modified function returning bool and filling buffer - bool getDataByInternalId(idhInt internal_id, levelInt layer, uint8_t* buffer) const { + // Modified function returning bool and filling buffer. + // For upper-layer vectors, data is returned zero-copy via cache_read_handle only. + // For layer 0, callers can optionally receive zero-copy cache hits via cache_read_handle. + bool getDataByInternalId(idhInt internal_id, + levelInt layer, + uint8_t* buffer, + CacheReadView* cache_read_handle = nullptr) const { + if(cache_read_handle) { + *cache_read_handle = CacheReadView(); + } + + const uint8_t* upper_ptr = getUpperLayerDataPtr(internal_id); + if(upper_ptr) { + if(cache_read_handle) { + cache_read_handle->data = upper_ptr; + return true; + } + return false; + } + if(layer == 0) { // Check cache first - if (vector_cache_ && vector_cache_->get(internal_id, buffer)) { - return true; + if(vector_cache_) { + if(cache_read_handle) { + cache_read_handle->lock = + std::shared_lock(getCacheMutex(internal_id)); + const uint8_t* cached_ptr = vector_cache_->getPointer(internal_id); + if(cached_ptr) { + cache_read_handle->data = cached_ptr; + return true; + } + cache_read_handle->lock.unlock(); + } + + if(buffer) { + std::shared_lock lock(getCacheMutex(internal_id)); + const uint8_t* cached_ptr = vector_cache_->getPointer(internal_id); + if(cached_ptr) { + memcpy(buffer, cached_ptr, data_size_); + return true; + } + } } idInt external_label = getExternalLabel(internal_id); - if(vector_fetcher_) { + if(vector_fetcher_ && buffer) { // Directly fetch to buffer bool success = vector_fetcher_(external_label, buffer); // Populate cache on successful fetch if (success && vector_cache_) { - vector_cache_->insert(internal_id, buffer); + std::unique_lock write_lock(getCacheMutex(internal_id), std::try_to_lock); + if(write_lock.owns_lock()) { + vector_cache_->insert(internal_id, buffer); + } } return success; } return false; + } + + // layer > 0 path with no upper-layer blob available + return false; + } + + // Batch fetch for level 0: check upper-layer data/cache first, then fetch misses in one MDBX txn. + // internal_ids: array of internal IDs to fetch + // buffers: flat output buffer, count * data_size_ bytes + // success: output array of bools + // count: number of IDs + // data_ptrs: optional per-item pointers to fetched data (zero-copy for upper-layer) + void getDataByInternalIdBatch(const idhInt* internal_ids, uint8_t* buffers, + bool* success, size_t count, + const void** data_ptrs = nullptr) const { + // Phase 1: Check cache for all IDs, collect misses + std::vector miss_indices; // index into the batch + std::vector miss_labels; // external labels for MDBX lookup + miss_indices.reserve(count); + miss_labels.reserve(count); + + for(size_t i = 0; i < count; i++) { + uint8_t* buf = buffers + i * data_size_; + if(data_ptrs) { + data_ptrs[i] = nullptr; + } + + const uint8_t* upper_ptr = getUpperLayerDataPtr(internal_ids[i]); + if(upper_ptr) { + if(data_ptrs) { + data_ptrs[i] = upper_ptr; + } else { + std::memcpy(buf, upper_ptr, data_size_); + } + success[i] = true; + continue; + } + + if(vector_cache_) { + std::shared_lock lock(getCacheMutex(internal_ids[i])); + const uint8_t* cached_ptr = vector_cache_->getPointer(internal_ids[i]); + if(cached_ptr) { + std::memcpy(buf, cached_ptr, data_size_); + if(data_ptrs) { + data_ptrs[i] = buf; + } + success[i] = true; + continue; + } + } + + success[i] = false; + miss_indices.push_back(i); + miss_labels.push_back(getExternalLabel(internal_ids[i])); + } + + // Phase 2: Batch fetch all misses in one MDBX txn + if(!miss_indices.empty() && vector_fetcher_batch_) { + // Temp buffers for the batch fetch + std::vector miss_buffers(miss_indices.size() * data_size_); + auto miss_success = std::make_unique(miss_indices.size()); + std::memset(miss_success.get(), 0, miss_indices.size() * sizeof(bool)); + + vector_fetcher_batch_(miss_labels.data(), miss_buffers.data(), + miss_success.get(), miss_indices.size()); + + // Phase 3: Copy results back and populate cache + for(size_t mi = 0; mi < miss_indices.size(); mi++) { + size_t i = miss_indices[mi]; + if(miss_success[mi]) { + uint8_t* buf = buffers + i * data_size_; + std::memcpy(buf, miss_buffers.data() + mi * data_size_, data_size_); + if(data_ptrs) { + data_ptrs[i] = buf; + } + success[i] = true; + // Populate cache + if(vector_cache_) { + std::unique_lock write_lock( + getCacheMutex(internal_ids[i]), std::try_to_lock); + if(write_lock.owns_lock()) { + vector_cache_->insert(internal_ids[i], buf); + } + } + } + } + } else if(!miss_indices.empty() && vector_fetcher_) { + // Fallback: single-fetch for each miss + for(size_t mi = 0; mi < miss_indices.size(); mi++) { + size_t i = miss_indices[mi]; + uint8_t* buf = buffers + i * data_size_; + bool ok = vector_fetcher_(miss_labels[mi], buf); + success[i] = ok; + if(ok && data_ptrs) { + data_ptrs[i] = buf; + } + if(ok && vector_cache_) { + std::unique_lock write_lock( + getCacheMutex(internal_ids[i]), std::try_to_lock); + if(write_lock.owns_lock()) { + vector_cache_->insert(internal_ids[i], buf); + } + } + } + } + } + + SimBatchFunc resolveBatchSimFunc() const { + ndd::quant::QuantizerDispatch dispatch; + try { + dispatch = ndd::quant::get_quantizer_dispatch(quant_level_); + } catch(...) { + return nullptr; + } + + switch(space_type_) { + case L2_SPACE: + return dispatch.sim_l2_batch; + case IP_SPACE: + return dispatch.sim_ip_batch; + case COSINE_SPACE: + return dispatch.sim_cosine_batch; + default: + return nullptr; + } + } + + void computeBatchSimilaritiesFromPtrs(const void* query_data, + const std::vector& vector_ptrs, + SIMFUNC fallback_sim_func, + void* dist_param, + std::vector& out_sims) const { + out_sims.clear(); + if(vector_ptrs.empty()) { + return; + } + + out_sims.resize(vector_ptrs.size()); + + SimBatchFunc batch_func = resolveBatchSimFunc(); + if(!batch_func) { + for(size_t i = 0; i < vector_ptrs.size(); ++i) { + out_sims[i] = fallback_sim_func(query_data, vector_ptrs[i], dist_param); + } + return; + } + + if constexpr(std::is_same_v) { + batch_func(query_data, + vector_ptrs.data(), + vector_ptrs.size(), + dist_param, + out_sims.data()); } else { - // FALLBACK: ideally callers should use getUpperLayerDataPtr - if(dataUpperLayer_[internal_id] == nullptr) { - return false; + std::vector sim_out(vector_ptrs.size()); + batch_func(query_data, + vector_ptrs.data(), + vector_ptrs.size(), + dist_param, + sim_out.data()); + + for(size_t i = 0; i < sim_out.size(); ++i) { + out_sims[i] = static_cast(sim_out[i]); } - memcpy(buffer, dataUpperLayer_[internal_id].get(), data_size_upper_); - return true; } - return false; } char* get_linklist0(idhInt internal_id) const { @@ -912,7 +1114,7 @@ namespace hnswlib { // int levels = getElementLevel(id); // if (level > levels) return nullptr; return reinterpret_cast( - dataUpperLayer_[id].get() + data_size_upper_ + sizeof(levelInt) + dataUpperLayer_[id].get() + data_size_ + sizeof(levelInt) + (level - 1) * sizeLinksUpperLayers_ ); @@ -939,7 +1141,7 @@ namespace hnswlib { if(!dataUpperLayer_[id]) { return 0; } - return *reinterpret_cast(dataUpperLayer_[id].get() + data_size_upper_); + return *reinterpret_cast(dataUpperLayer_[id].get() + data_size_); } // This function is used to get the neighbors based on heuristic @@ -960,12 +1162,17 @@ namespace hnswlib { fill_back_ids.reserve(candidates_sorted.size() - curM); // Generic awareness - auto curSimFunc = (level == 0) ? fstSimFunc_ : fstSimFuncUpper_; - auto curDistParam = (level == 0) ? dist_func_param_ : dist_func_param_upper_; - size_t curDataSize = (level == 0) ? data_size_ : data_size_upper_; + auto curSimFunc = fstSimFunc_; + auto curDistParam = dist_func_param_; + size_t curDataSize = data_size_; std::vector cand_buf(curDataSize); // Only used for level 0 - std::vector selected_buf(curDataSize); // Only used for level 0 + // Cache selected vectors to avoid redundant re-fetches in inner loop + // Without this, each selected vector is re-fetched O(candidates) times → O(M²) fetches + std::vector> selected_vecs_cache; // Only used for level 0 + if(level == 0) { + selected_vecs_cache.reserve(curM); + } for(const auto& candidate : candidates_sorted) { if(result.size() == curM) { @@ -973,9 +1180,15 @@ namespace hnswlib { } const void* cand_vec = nullptr; + CacheReadView cand_cache_handle; if(level == 0) { - if(getDataByInternalId(candidate.second, level, cand_buf.data())) { - cand_vec = cand_buf.data(); + if(getDataByInternalId(candidate.second, + level, + cand_buf.data(), + &cand_cache_handle)) { + cand_vec = cand_cache_handle + ? static_cast(cand_cache_handle.data) + : static_cast(cand_buf.data()); } } else { cand_vec = getUpperLayerDataPtr(candidate.second); @@ -987,14 +1200,12 @@ namespace hnswlib { bool good = true; dist_t sim; - for(const auto& selected : result) { + for(size_t si = 0; si < result.size(); si++) { const void* selected_vec_ptr = nullptr; if(level == 0) { - if(getDataByInternalId(selected.second, level, selected_buf.data())) { - selected_vec_ptr = selected_buf.data(); - } + selected_vec_ptr = selected_vecs_cache[si].data(); } else { - selected_vec_ptr = getUpperLayerDataPtr(selected.second); + selected_vec_ptr = getUpperLayerDataPtr(result[si].second); } if(!selected_vec_ptr) { @@ -1011,6 +1222,12 @@ namespace hnswlib { if(good) { result.push_back(candidate); + // Cache the vector data so inner loop never re-fetches it + if(level == 0) { + selected_vecs_cache.emplace_back( + static_cast(cand_vec), + static_cast(cand_vec) + curDataSize); + } } else { fill_back_ids.push_back(candidate); } @@ -1045,9 +1262,9 @@ namespace hnswlib { size_t curM = level ? M_ : M0_; // Generic awareness - auto curSimFunc = (level == 0) ? fstSimFunc_ : fstSimFuncUpper_; - auto curDistParam = (level == 0) ? dist_func_param_ : dist_func_param_upper_; - size_t curDataSize = (level == 0) ? data_size_ : data_size_upper_; + auto curSimFunc = fstSimFunc_; + auto curDistParam = dist_func_param_; + size_t curDataSize = data_size_; auto selected = getNeighborsByHeuristic2(sorted_candidates, curM, level); if(selected.empty()) { // the graph is empty or disconnected @@ -1094,9 +1311,15 @@ namespace hnswlib { setListCount(ll_other, sz + 1); } else { const void* neighbor_data = nullptr; + CacheReadView neighbor_cache_handle; if(level == 0) { - if(getDataByInternalId(neighbor, level, neighbor_buf.data())) { - neighbor_data = neighbor_buf.data(); + if(getDataByInternalId(neighbor, + level, + neighbor_buf.data(), + &neighbor_cache_handle)) { + neighbor_data = neighbor_cache_handle + ? static_cast(neighbor_cache_handle.data) + : static_cast(neighbor_buf.data()); } } else { neighbor_data = getUpperLayerDataPtr(neighbor); @@ -1115,9 +1338,15 @@ namespace hnswlib { for(size_t j = 0; j < sz; j++) { dist_t sim; const void* other_neighbor_data = nullptr; + CacheReadView other_cache_handle; if(level == 0) { - if(getDataByInternalId(data[j], level, data_buf.data())) { - other_neighbor_data = data_buf.data(); + if(getDataByInternalId(data[j], + level, + data_buf.data(), + &other_cache_handle)) { + other_neighbor_data = other_cache_handle + ? static_cast(other_cache_handle.data) + : static_cast(data_buf.data()); } } else { other_neighbor_data = getUpperLayerDataPtr(data[j]); @@ -1164,9 +1393,9 @@ namespace hnswlib { min_heap_pq top_candidates; // Generic awareness - auto curSimFunc = (layer == 0) ? fstSimFunc_ : fstSimFuncUpper_; - auto curDistParam = (layer == 0) ? dist_func_param_ : dist_func_param_upper_; - size_t curDataSize = (layer == 0) ? data_size_ : data_size_upper_; + auto curSimFunc = fstSimFunc_; + auto curDistParam = dist_func_param_; + size_t curDataSize = data_size_; std::vector buffer; if(layer == 0) { buffer.resize(curDataSize); @@ -1184,9 +1413,15 @@ namespace hnswlib { dist_t sim = std::numeric_limits::lowest(); if(!has_deletions || !isMarkedDeleted(ep_id)) { const void* vec_data = nullptr; + CacheReadView ep_cache_handle; if(layer == 0) { - if(getDataByInternalId(ep_id, layer, buffer.data())) { - vec_data = buffer.data(); + if(getDataByInternalId(ep_id, + layer, + buffer.data(), + &ep_cache_handle)) { + vec_data = ep_cache_handle + ? static_cast(ep_cache_handle.data) + : static_cast(buffer.data()); } } else { vec_data = getUpperLayerDataPtr(ep_id); @@ -1276,83 +1511,151 @@ namespace hnswlib { idhInt size = getListCount((idhInt*)data); idhInt* datal = (idhInt*)(data + 1); - for(idhInt j = 0; j < size; j++) { - idhInt candidate_id = *(datal + j); - if(visited_array[candidate_id] == visited_array_tag) { - continue; - } - visited_array[candidate_id] = visited_array_tag; - if(has_deletions && isMarkedDeleted(candidate_id)) { - continue; + // --- Batch prefetch path for layer 0 --- + if(layer == 0) { + // Phase 1: Collect valid (non-visited, non-deleted) candidate IDs + std::vector valid_ids; + valid_ids.reserve(size); + for(idhInt j = 0; j < size; j++) { + idhInt candidate_id = *(datal + j); + if(visited_array[candidate_id] == visited_array_tag) continue; + visited_array[candidate_id] = visited_array_tag; + if(has_deletions && isMarkedDeleted(candidate_id)) continue; + valid_ids.push_back(candidate_id); } - dist_t sim; - const void* neighbor_data = nullptr; - if(layer == 0) { - if(getDataByInternalId(candidate_id, layer, buffer.data())) { - neighbor_data = buffer.data(); + if(valid_ids.empty()) continue; + + // Phase 2: Batch fetch all vectors + std::vector batch_buffers(valid_ids.size() * data_size_); + std::vector batch_ptrs(valid_ids.size(), nullptr); + auto batch_success = std::make_unique(valid_ids.size()); + std::memset(batch_success.get(), 0, valid_ids.size() * sizeof(bool)); + getDataByInternalIdBatch(valid_ids.data(), batch_buffers.data(), + batch_success.get(), valid_ids.size(), + batch_ptrs.data()); + + // Phase 3: Process fetched vectors + std::vector pass_filter_candidate_ids; + pass_filter_candidate_ids.reserve(valid_ids.size()); + std::vector pass_filter_ptrs; + pass_filter_ptrs.reserve(valid_ids.size()); + + for(size_t vi = 0; vi < valid_ids.size(); vi++) { + if(!batch_success[vi]) continue; + + idhInt candidate_id = valid_ids[vi]; + const void* neighbor_data = batch_ptrs[vi]; + if(!neighbor_data) { + continue; } - } else { - neighbor_data = getUpperLayerDataPtr(candidate_id); - } - - if(!neighbor_data) { - continue; - } - // Check filter BEFORE computing distance - // Treats filtered nodes as non-existent (traverses a subgraph) - bool pass_filter = true; - if constexpr(!std::is_same_v) { - if (filter != nullptr) { - if (!(*filter)(getExternalLabel(candidate_id))) { - pass_filter = false; + bool pass_filter = true; + if constexpr(!std::is_same_v) { + if(filter != nullptr) { + if(!(*filter)(getExternalLabel(candidate_id))) { + pass_filter = false; + } } } - } - if(!pass_filter) { - // Check Fatigue - if (dist_computations > fatigue_base) { - // We are in the tapering region - // Linearly increase drop probability from 0% to 100%. - - size_t excess = dist_computations - fatigue_base; - - if (excess >= fatigue_tail) { - continue; // 100% drop (Hard Cap exceeded) + if(!pass_filter) { + if(dist_computations > fatigue_base) { + size_t excess = dist_computations - fatigue_base; + if(excess >= fatigue_tail) continue; + size_t drop_prob = (excess * 255) / fatigue_tail; + size_t hash = (candidate_id * 104729) & 0xFF; + if(hash < drop_prob) continue; } - // Prob = Excess / Tail_Length - size_t drop_prob = (excess * 255) / fatigue_tail; - - size_t hash = (candidate_id * 104729) & 0xFF; - if (hash < drop_prob) continue; + dist_t sim = curSimFunc(data_point, neighbor_data, curDistParam); + dist_computations++; + if(top_candidates.size() < ef || sim > lowerBound) { + candidate_set.emplace(sim, candidate_id); + } + continue; } - - // Explore - sim = curSimFunc(data_point, neighbor_data, curDistParam); + + pass_filter_candidate_ids.push_back(candidate_id); + pass_filter_ptrs.push_back(neighbor_data); + } + + std::vector pass_filter_sims; + computeBatchSimilaritiesFromPtrs(data_point, + pass_filter_ptrs, + curSimFunc, + curDistParam, + pass_filter_sims); + + for(size_t pi = 0; pi < pass_filter_candidate_ids.size(); ++pi) { + idhInt candidate_id = pass_filter_candidate_ids[pi]; + dist_t sim = pass_filter_sims[pi]; dist_computations++; - if (top_candidates.size() < ef || sim > lowerBound) { + + if(top_candidates.size() < ef || sim > lowerBound) { candidate_set.emplace(sim, candidate_id); + if(!has_deletions || !isMarkedDeleted(candidate_id)) { + top_candidates.emplace(sim, candidate_id); + if(top_candidates.size() > ef) { + top_candidates.pop(); + } + if(!top_candidates.empty()) { + lowerBound = top_candidates.top().first; + } + } } - continue; } + } else { + // --- Upper layer path: data is in-memory, no batching needed --- + for(idhInt j = 0; j < size; j++) { + idhInt candidate_id = *(datal + j); + if(visited_array[candidate_id] == visited_array_tag) continue; + visited_array[candidate_id] = visited_array_tag; + if(has_deletions && isMarkedDeleted(candidate_id)) continue; + + const void* neighbor_data = getUpperLayerDataPtr(candidate_id); + if(!neighbor_data) continue; + + bool pass_filter = true; + if constexpr(!std::is_same_v) { + if (filter != nullptr) { + if (!(*filter)(getExternalLabel(candidate_id))) { + pass_filter = false; + } + } + } - sim = curSimFunc(data_point, neighbor_data, curDistParam); - dist_computations++; // Count valid computations too + dist_t sim; + if(!pass_filter) { + if (dist_computations > fatigue_base) { + size_t excess = dist_computations - fatigue_base; + if (excess >= fatigue_tail) continue; + size_t drop_prob = (excess * 255) / fatigue_tail; + size_t hash = (candidate_id * 104729) & 0xFF; + if (hash < drop_prob) continue; + } + sim = curSimFunc(data_point, neighbor_data, curDistParam); + dist_computations++; + if (top_candidates.size() < ef || sim > lowerBound) { + candidate_set.emplace(sim, candidate_id); + } + continue; + } - if(top_candidates.size() < ef || sim > lowerBound) { - candidate_set.emplace(sim, candidate_id); + sim = curSimFunc(data_point, neighbor_data, curDistParam); + dist_computations++; - if(!has_deletions || !isMarkedDeleted(candidate_id)) { - top_candidates.emplace(sim, candidate_id); - if(top_candidates.size() > ef) { - top_candidates.pop(); - } - if(!top_candidates.empty()) { - lowerBound = top_candidates.top().first; - } + if(top_candidates.size() < ef || sim > lowerBound) { + candidate_set.emplace(sim, candidate_id); + if(!has_deletions || !isMarkedDeleted(candidate_id)) { + top_candidates.emplace(sim, candidate_id); + if(top_candidates.size() > ef) { + top_candidates.pop(); + } + if(!top_candidates.empty()) { + lowerBound = top_candidates.top().first; + } + } } } } @@ -1368,6 +1671,8 @@ namespace hnswlib { std::reverse(sorted_candidates.begin(), sorted_candidates.end()); return sorted_candidates; } + // This function removes all connections to and from the target element across all levels + // There is a new version removeAllConnections2 that tries to reconnect neighbors to each other instead of just removing the target element from their lists void removeAllConnections(idhInt internal_id, levelInt elem_level) { for(int level = 0; level <= elem_level; ++level) { @@ -1403,5 +1708,148 @@ namespace hnswlib { setListCount(ll_self, 0); } } + // This is a more efficient version of removeAllConnections that reconnects neighbors to each other + // instead of just removing the target element from their lists + void removeAllConnections2(idhInt internal_id, levelInt elem_level) { + auto containsNeighborNoLock = [this](idhInt* linklist, idhInt target) { + idhInt sz = getListCount(linklist); + idhInt* data = linklist + 1; + for(idhInt i = 0; i < sz; ++i) { + if(data[i] == target) { + return true; + } + } + return false; + }; + + auto connectPair = [&](idhInt left, idhInt right, levelInt level) { + if(left == right || left >= curElementsCount_ || right >= curElementsCount_) { + return; + } + + size_t max_degree = level ? M_ : M0_; + auto& left_mutex = getLinkListMutex(left); + auto& right_mutex = getLinkListMutex(right); + + if(&left_mutex == &right_mutex) { + std::unique_lock lock(left_mutex); + + idhInt* ll_left = (idhInt*)get_linklist(left, level); + idhInt* ll_right = (idhInt*)get_linklist(right, level); + if(!ll_left || !ll_right) { + return; + } + + if(containsNeighborNoLock(ll_left, right) + || containsNeighborNoLock(ll_right, left)) { + return; + } + + idhInt left_sz = getListCount(ll_left); + idhInt right_sz = getListCount(ll_right); + if(left_sz >= static_cast(max_degree) + || right_sz >= static_cast(max_degree)) { + return; + } + + (ll_left + 1)[left_sz] = right; + (ll_right + 1)[right_sz] = left; + setListCount(ll_left, left_sz + 1); + setListCount(ll_right, right_sz + 1); + return; + } + + idhInt first_id = left < right ? left : right; + idhInt second_id = left < right ? right : left; + + auto& first_mutex = getLinkListMutex(first_id); + auto& second_mutex = getLinkListMutex(second_id); + + std::unique_lock first_lock(first_mutex); + std::unique_lock second_lock(second_mutex); + + idhInt* ll_left = (idhInt*)get_linklist(left, level); + idhInt* ll_right = (idhInt*)get_linklist(right, level); + if(!ll_left || !ll_right) { + return; + } + if(containsNeighborNoLock(ll_left, right) + || containsNeighborNoLock(ll_right, left)) { + return; + } + + idhInt left_sz = getListCount(ll_left); + idhInt right_sz = getListCount(ll_right); + if(left_sz >= static_cast(max_degree) + || right_sz >= static_cast(max_degree)) { + return; + } + + (ll_left + 1)[left_sz] = right; + (ll_right + 1)[right_sz] = left; + setListCount(ll_left, left_sz + 1); + setListCount(ll_right, right_sz + 1); + }; + + for(int level = 0; level <= elem_level; ++level) { + idhInt* ll_self = (idhInt*)get_linklist(internal_id, level); + if(!ll_self) { + continue; + } + std::vector level_neighbors; + + { + std::unique_lock lock_self(getLinkListMutex(internal_id)); + + idhInt size = getListCount(ll_self); + idhInt* neighbors = (idhInt*)(ll_self + 1); + level_neighbors.reserve(size); + + for(idhInt i = 0; i < size; ++i) { + idhInt neighbor_id = neighbors[i]; + if(neighbor_id >= curElementsCount_ || neighbor_id == internal_id) { + continue; + } + level_neighbors.push_back(neighbor_id); + } + + setListCount(ll_self, 0); + } + + std::sort(level_neighbors.begin(), level_neighbors.end()); + level_neighbors.erase( + std::unique(level_neighbors.begin(), level_neighbors.end()), + level_neighbors.end()); + + for(idhInt neighbor_id : level_neighbors) { + std::unique_lock lock_neighbor(getLinkListMutex(neighbor_id)); + idhInt* ll_other = (idhInt*)get_linklist(neighbor_id, level); + if(!ll_other) { + continue; + } + + idhInt sz = getListCount(ll_other); + idhInt* data = (idhInt*)(ll_other + 1); + + idhInt new_size = 0; + for(idhInt j = 0; j < sz; ++j) { + if(data[j] != internal_id) { + data[new_size++] = data[j]; + } + } + setListCount(ll_other, new_size); + } + + size_t left = 0; + if(!level_neighbors.empty()) { + size_t right = level_neighbors.size() - 1; + while(left < right) { + connectPair(level_neighbors[left], level_neighbors[right], level); + ++left; + --right; + } + } + } + } }; } // namespace hnswlib diff --git a/src/hnsw/vector_cache.h b/src/hnsw/vector_cache.h index 985f0a353c..a289caa14a 100644 --- a/src/hnsw/vector_cache.h +++ b/src/hnsw/vector_cache.h @@ -3,10 +3,8 @@ #include "../utils/settings.hpp" #include #include -#include #include #include -#include #include #include #include @@ -15,28 +13,25 @@ namespace hnswlib { class VectorCache { public: - inline static size_t VECTOR_CACHE_PERCENTAGE = settings::VECTOR_CACHE_PERCENTAGE; - inline static size_t VECTOR_CACHE_MIN_BITS = settings::VECTOR_CACHE_MIN_BITS; - static constexpr uint8_t MAX_COUNTER = 2; // Sticky replacement policy // Helper to calculate required cache bits based on element count and percentage - static size_t calculateCacheBits(size_t element_count, size_t cache_percent = VECTOR_CACHE_PERCENTAGE) { + static size_t calculateCacheBits(size_t element_count, size_t cache_percent = settings::VECTOR_CACHE_PERCENTAGE) { if (element_count == 0 || cache_percent == 0) return 0; size_t target_elements = (element_count * cache_percent) / 100; // Calculate bits needed: 2^bits >= target_elements - size_t bits = 0; - while ((1ULL << bits) < target_elements) { - bits++; + size_t cache_bits = 0; + while ((1ULL << cache_bits) < target_elements) { + cache_bits++; } // Enforce minimum bits - if (bits < VECTOR_CACHE_MIN_BITS) { - bits = VECTOR_CACHE_MIN_BITS; + if (cache_bits < settings::VECTOR_CACHE_MIN_BITS) { + cache_bits = settings::VECTOR_CACHE_MIN_BITS; } - return bits; + return cache_bits; } private: @@ -46,18 +41,14 @@ class VectorCache { size_t vectorCacheDataSize_ = 0; size_t data_size_ = 0; uint8_t* vectorCache_ = nullptr; - - static constexpr size_t CACHE_STRIPE_BITS = 8; // 256 stripes - static constexpr size_t CACHE_STRIPE_COUNT = 1 << CACHE_STRIPE_BITS; - static constexpr size_t CACHE_STRIPE_MASK = CACHE_STRIPE_COUNT - 1; - mutable std::array vectorCacheStripeMutexes_; + std::atomic* slotLife_ = nullptr; static constexpr idInt INVALID_ID = static_cast(-1); - std::shared_mutex& getCacheStripeMutex(size_t cache_index) const { - size_t stripe_id = cache_index & CACHE_STRIPE_MASK; - return vectorCacheStripeMutexes_[stripe_id]; - } + static constexpr uint8_t SLOT_LIFE_INVALID = 0; + static constexpr uint8_t SLOT_LIFE_COLD = 1; + static constexpr uint8_t SLOT_LIFE_WARM = 2; + static constexpr uint8_t SLOT_LIFE_HOT = 3; public: VectorCache() = default; @@ -72,6 +63,10 @@ class VectorCache { delete[] vectorCache_; vectorCache_ = nullptr; } + if (slotLife_) { + delete[] slotLife_; + slotLife_ = nullptr; + } } void init(size_t data_size, size_t cache_bits) { @@ -79,6 +74,10 @@ class VectorCache { delete[] vectorCache_; vectorCache_ = nullptr; } + if (slotLife_) { + delete[] slotLife_; + slotLife_ = nullptr; + } if (cache_bits == 0) { cacheBits_ = 0; @@ -93,88 +92,120 @@ class VectorCache { cacheBits_ = cache_bits; cacheSize_ = 1 << cacheBits_; cacheMask_ = cacheSize_ - 1; - // Layout: [idInt] [uint8_t counter] [data...] - vectorCacheDataSize_ = data_size_ + sizeof(idInt) + sizeof(uint8_t); + vectorCacheDataSize_ = data_size_ + sizeof(idInt); vectorCache_ = new uint8_t[cacheSize_ * vectorCacheDataSize_]; + slotLife_ = new std::atomic[cacheSize_]; // Initialize all entries to INVALID_ID for (size_t i = 0; i < cacheSize_; i++) { - uint8_t* entry = vectorCache_ + i * vectorCacheDataSize_; - idInt* id_ptr = reinterpret_cast(entry); + idInt* id_ptr = reinterpret_cast(vectorCache_ + i * vectorCacheDataSize_); *id_ptr = INVALID_ID; - // Also zero out counter/data for cleanliness - *(entry + sizeof(idInt)) = 0; + slotLife_[i].store(SLOT_LIFE_INVALID, std::memory_order_relaxed); } } - bool get(idInt internal_id, uint8_t* buffer) const { - if (!vectorCache_) return false; + size_t getCacheIndex(idInt internal_id) const { + return internal_id & cacheMask_; + } + + // Not thread-safe. Caller must hold the appropriate cache stripe lock. + const uint8_t* getPointer(idInt internal_id) { + if (!vectorCache_ || !slotLife_) return nullptr; + + size_t index = getCacheIndex(internal_id); - size_t index = internal_id & cacheMask_; + // If life is invalid, we treat it as a miss regardless of ID + if (slotLife_[index].load(std::memory_order_relaxed) == SLOT_LIFE_INVALID) { + return nullptr; + } + uint8_t* entry = vectorCache_ + index * vectorCacheDataSize_; - - std::shared_lock lock(getCacheStripeMutex(index)); - idInt* stored_id = reinterpret_cast(entry); - if (*stored_id == internal_id) { - // Hit! Reset counter to MAX_COUNTER (stickiness) - // Optimization: Only write if currently different to avoid cache line invalidation (False Sharing) - uint8_t* counter_ptr = entry + sizeof(idInt); - auto atomic_counter = reinterpret_cast*>(counter_ptr); - - if (atomic_counter->load(std::memory_order_relaxed) < MAX_COUNTER) { - atomic_counter->store(MAX_COUNTER, std::memory_order_relaxed); - } - - memcpy(buffer, entry + sizeof(idInt) + sizeof(uint8_t), data_size_); - return true; + if (*stored_id != internal_id) { + return nullptr; } - return false; + + slotLife_[index].store(SLOT_LIFE_HOT, std::memory_order_relaxed); + return entry + sizeof(idInt); } - + + // Not thread-safe. Caller must hold the appropriate cache stripe lock. + const uint8_t* getPointer(idInt internal_id) const { + return const_cast(this)->getPointer(internal_id); + } + + // Not thread-safe. Caller must hold the appropriate cache stripe lock. void insert(idInt internal_id, const uint8_t* data) { - if (!vectorCache_) return; + if (!vectorCache_ || !slotLife_) return; - size_t index = internal_id & cacheMask_; + size_t index = getCacheIndex(internal_id); uint8_t* entry = vectorCache_ + index * vectorCacheDataSize_; - - std::unique_lock lock(getCacheStripeMutex(index)); - + idInt* stored_id = reinterpret_cast(entry); - // Use atomic consistently to avoid UB, though we are under unique_lock - auto atomic_counter = reinterpret_cast*>(entry + sizeof(idInt)); - uint8_t* data_ptr = entry + sizeof(idInt) + sizeof(uint8_t); + // Same id: refresh value and restore life. if (*stored_id == internal_id) { - // Update existing - atomic_counter->store(MAX_COUNTER, std::memory_order_relaxed); - memcpy(data_ptr, data, data_size_); + memcpy(entry + sizeof(idInt), data, data_size_); + slotLife_[index].store(SLOT_LIFE_HOT, std::memory_order_relaxed); return; } - - if (*stored_id == INVALID_ID) { - // Empty slot - *stored_id = internal_id; - atomic_counter->store(MAX_COUNTER, std::memory_order_relaxed); - memcpy(data_ptr, data, data_size_); + + // Different id: three-life policy (eviction protocol). + // life == SLOT_LIFE_HOT (3) -> demote to WARM (2), do not replace now. + // life == SLOT_LIFE_WARM (2) -> demote to COLD (1), do not replace now. + // life == SLOT_LIFE_COLD (1) or INVALID (0) -> replace slot and set life to WARM (2) + // Newly inserted items start as WARM. They must be accessed again to become HOT. + uint8_t life = slotLife_[index].load(std::memory_order_relaxed); + if (life == SLOT_LIFE_HOT) { + slotLife_[index].store(SLOT_LIFE_WARM, std::memory_order_relaxed); + return; + } else if (life == SLOT_LIFE_WARM) { + slotLife_[index].store(SLOT_LIFE_COLD, std::memory_order_relaxed); return; } - // Collision with different vector - uint8_t c = atomic_counter->load(std::memory_order_relaxed); - if (c > 0) { - c--; - atomic_counter->store(c, std::memory_order_relaxed); + *stored_id = internal_id; + memcpy(entry + sizeof(idInt), data, data_size_); + slotLife_[index].store(SLOT_LIFE_WARM, std::memory_order_relaxed); + } + + // Not thread-safe. Caller must hold the appropriate cache stripe lock. + void update(idInt internal_id, const uint8_t* data) { + if(!vectorCache_ || !slotLife_) return; + + size_t index = getCacheIndex(internal_id); + uint8_t* entry = vectorCache_ + index * vectorCacheDataSize_; + idInt* stored_id = reinterpret_cast(entry); + + if(*stored_id != internal_id) { + return; } - - if (c == 0) { - // Replace - *stored_id = internal_id; - atomic_counter->store(MAX_COUNTER, std::memory_order_relaxed); - memcpy(data_ptr, data, data_size_); + + *stored_id = internal_id; + memcpy(entry + sizeof(idInt), data, data_size_); + slotLife_[index].store(1, std::memory_order_relaxed); + } + + // Atomically invalidate the slot for a given id, forcing a fetch on the next read. + // Thread-safe without a cache stripe lock due to atomic memory ordering, + // provided the caller accepts eventual consistency (next reader will miss and lock to fetch). + void invalidateSlot(idInt internal_id) { + if(!vectorCache_ || !slotLife_) return; + + size_t index = getCacheIndex(internal_id); + + // Only invalidate if the slot currently belongs to this ID, preventing us from accidentally + // invalidating another vector if an eviction happened between our read and invalidate. + uint8_t* entry = vectorCache_ + index * vectorCacheDataSize_; + idInt* stored_id = reinterpret_cast(entry); + + if (*stored_id == internal_id) { + // Note: Data race risk with readers. Even if we set slotLife to INVALID right now, + // a reader might have just checked slotLife and is now about to read `stored_id`. + // Because of this, we set slotLife so FUTURE readers instantly miss. + slotLife_[index].store(SLOT_LIFE_INVALID, std::memory_order_release); } - // Else: reject new vector, keep old one (thrashing protection) } size_t getCacheBits() const { return cacheBits_; } diff --git a/src/quant/binary.hpp b/src/quant/binary.hpp index 967fe730a6..9ec11c59d3 100644 --- a/src/quant/binary.hpp +++ b/src/quant/binary.hpp @@ -643,6 +643,80 @@ namespace ndd { return HammingSim(v1, v2, params); } + inline constexpr size_t kBatchTileWordsBinary = +#if defined(USE_AVX512) + 256; +#elif defined(USE_AVX2) + 128; +#elif defined(USE_SVE2) + 128; +#elif defined(USE_NEON) + 64; +#else + 32; +#endif + + inline void SimilarityBatchTiled(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + if(count == 0) { + return; + } + + const size_t dim = *static_cast(params); + const size_t num_words = (dim + 63) / 64; + const auto* q_words = static_cast(query); + + std::vector dist_acc(count, 0.0f); + const size_t tile_words = std::min(num_words, kBatchTileWordsBinary); + + for(size_t block_start = 0; block_start < num_words; block_start += tile_words) { + const size_t block_len = std::min(tile_words, num_words - block_start); + + for(size_t i = 0; i < count; ++i) { + const auto* v_words = static_cast(vectors[i]); + float dist = dist_acc[i]; + + for(size_t w = 0; w < block_len; ++w) { + dist += __builtin_popcountll( + q_words[block_start + w] ^ v_words[block_start + w]); + } + + dist_acc[i] = dist; + } + } + + for(size_t i = 0; i < count; ++i) { + out[i] = static_cast(dim) - dist_acc[i]; + } + } + + inline void L2SqrSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out); + } + + inline void InnerProductSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out); + } + + inline void CosineSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out); + } + static std::vector quantize_to_int8(const void* in, size_t dim) { throw std::runtime_error("Binary to Int8 direct quantization not implemented"); } @@ -662,6 +736,9 @@ namespace ndd { d.sim_l2 = &binary::L2SqrSim; d.sim_ip = &binary::InnerProductSim; d.sim_cosine = &binary::CosineSim; + d.sim_l2_batch = &binary::L2SqrSimBatch; + d.sim_ip_batch = &binary::InnerProductSimBatch; + d.sim_cosine_batch = &binary::CosineSimBatch; d.quantize = &binary::quantize; d.dequantize = &binary::dequantize; d.quantize_to_int8 = &binary::quantize_to_int8; diff --git a/src/quant/common.hpp b/src/quant/common.hpp index 820fad7e74..5e9cf4949c 100644 --- a/src/quant/common.hpp +++ b/src/quant/common.hpp @@ -45,6 +45,28 @@ namespace ndd { float (*sim_ip)(const void* v1, const void* v2, const void* params); float (*sim_cosine)(const void* v1, const void* v2, const void* params); + // Batch similarity functions + // query: single query vector + // vectors: array of pointers to candidate vectors + // count: number of candidates + // params: metric params (same as scalar path) + // out: output similarities, length == count + void (*sim_l2_batch)(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out); + void (*sim_ip_batch)(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out); + void (*sim_cosine_batch)(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out); + // Conversion functions std::vector (*quantize)(const std::vector& in); std::vector (*dequantize)(const uint8_t* in, size_t dim); diff --git a/src/quant/float16.hpp b/src/quant/float16.hpp index b50035389a..a26f9c9306 100644 --- a/src/quant/float16.hpp +++ b/src/quant/float16.hpp @@ -790,6 +790,192 @@ namespace ndd { return 1.0f - CosineSim(pVect1v, pVect2v, qty_ptr); } + static constexpr size_t kBatchTileSizeF16 = +#if defined(USE_AVX512) + 1024; +#elif defined(USE_AVX2) + 512; +#elif defined(USE_SVE2) + 512; +#elif defined(USE_NEON) + 256; +#else + 128; +#endif + + static void SimilarityBatchTiled(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out, + bool l2_metric) { + if(count == 0) { + return; + } + + const auto* dist_params = static_cast(params); + const size_t dim = dist_params->dim; + const auto* query_vec = static_cast(query); + + std::vector dot_acc(count, 0.0f); + std::vector vec_sq_acc; + if(l2_metric) { + vec_sq_acc.assign(count, 0.0f); + } + + float query_sq_acc = 0.0f; + const size_t tile = std::min(dim, kBatchTileSizeF16); + + for(size_t block_start = 0; block_start < dim; block_start += tile) { + const size_t block_len = std::min(tile, dim - block_start); + const uint16_t* q_ptr = query_vec + block_start; + + for(size_t d = 0; d < block_len; ++d) { + float qv = fp16_to_fp32(q_ptr[d]); + query_sq_acc += qv * qv; + } + + for(size_t i = 0; i < count; ++i) { + const uint16_t* v_ptr = static_cast(vectors[i]) + block_start; + float dot = dot_acc[i]; + float vec_sq = l2_metric ? vec_sq_acc[i] : 0.0f; + + size_t d = 0; +#if defined(USE_AVX512) + __m512 dot_vec = _mm512_setzero_ps(); + __m512 sq_vec = _mm512_setzero_ps(); + for(; d + 16 <= block_len; d += 16) { + __m256i q_h = _mm256_loadu_si256(reinterpret_cast(q_ptr + d)); + __m256i v_h = _mm256_loadu_si256(reinterpret_cast(v_ptr + d)); + __m512 qv = _mm512_cvtph_ps(q_h); + __m512 vv = _mm512_cvtph_ps(v_h); + dot_vec = _mm512_fmadd_ps(qv, vv, dot_vec); + if(l2_metric) { + sq_vec = _mm512_fmadd_ps(vv, vv, sq_vec); + } + } + dot += _mm512_reduce_add_ps(dot_vec); + if(l2_metric) { + vec_sq += _mm512_reduce_add_ps(sq_vec); + } +#elif defined(USE_AVX2) + __m256 dot_vec = _mm256_setzero_ps(); + __m256 sq_vec = _mm256_setzero_ps(); + for(; d + 8 <= block_len; d += 8) { + __m128i q_h = _mm_loadu_si128(reinterpret_cast(q_ptr + d)); + __m128i v_h = _mm_loadu_si128(reinterpret_cast(v_ptr + d)); + __m256 qv = _mm256_cvtph_ps(q_h); + __m256 vv = _mm256_cvtph_ps(v_h); +#if defined(__FMA__) + dot_vec = _mm256_fmadd_ps(qv, vv, dot_vec); + if(l2_metric) { + sq_vec = _mm256_fmadd_ps(vv, vv, sq_vec); + } +#else + dot_vec = _mm256_add_ps(dot_vec, _mm256_mul_ps(qv, vv)); + if(l2_metric) { + sq_vec = _mm256_add_ps(sq_vec, _mm256_mul_ps(vv, vv)); + } +#endif + } + { + __m128 lo = _mm256_castps256_ps128(dot_vec); + __m128 hi = _mm256_extractf128_ps(dot_vec, 1); + __m128 s = _mm_add_ps(lo, hi); + s = _mm_hadd_ps(s, s); + s = _mm_hadd_ps(s, s); + dot += _mm_cvtss_f32(s); + } + if(l2_metric) { + __m128 lo = _mm256_castps256_ps128(sq_vec); + __m128 hi = _mm256_extractf128_ps(sq_vec, 1); + __m128 s = _mm_add_ps(lo, hi); + s = _mm_hadd_ps(s, s); + s = _mm_hadd_ps(s, s); + vec_sq += _mm_cvtss_f32(s); + } +#elif defined(USE_NEON) + float32x4_t dot_vec = vdupq_n_f32(0.0f); + float32x4_t sq_vec = vdupq_n_f32(0.0f); + for(; d + 4 <= block_len; d += 4) { + float16x4_t q_h = vld1_f16(reinterpret_cast(q_ptr + d)); + float16x4_t v_h = vld1_f16(reinterpret_cast(v_ptr + d)); + float32x4_t qv = vcvt_f32_f16(q_h); + float32x4_t vv = vcvt_f32_f16(v_h); + dot_vec = vfmaq_f32(dot_vec, qv, vv); + if(l2_metric) { + sq_vec = vfmaq_f32(sq_vec, vv, vv); + } + } + dot += vaddvq_f32(dot_vec); + if(l2_metric) { + vec_sq += vaddvq_f32(sq_vec); + } +#elif defined(USE_SVE2) + size_t lane = svcnth(); + for(; d + lane <= block_len; d += lane) { + svbool_t pg16 = svwhilelt_b16(0, lane); + svbool_t pg32 = svwhilelt_b32(0, lane); + svfloat16_t q_h = svld1_f16(pg16, reinterpret_cast(q_ptr + d)); + svfloat16_t v_h = svld1_f16(pg16, reinterpret_cast(v_ptr + d)); + svfloat32_t qv = svcvt_f32_f16_x(pg32, q_h); + svfloat32_t vv = svcvt_f32_f16_x(pg32, v_h); + dot += svaddv_f32(pg32, svmul_f32_x(pg32, qv, vv)); + if(l2_metric) { + vec_sq += svaddv_f32(pg32, svmul_f32_x(pg32, vv, vv)); + } + } +#endif + + for(; d < block_len; ++d) { + float qv = fp16_to_fp32(q_ptr[d]); + float vv = fp16_to_fp32(v_ptr[d]); + dot += qv * vv; + if(l2_metric) { + vec_sq += vv * vv; + } + } + + dot_acc[i] = dot; + if(l2_metric) { + vec_sq_acc[i] = vec_sq; + } + } + } + + for(size_t i = 0; i < count; ++i) { + if(l2_metric) { + out[i] = -(query_sq_acc + vec_sq_acc[i] - 2.0f * dot_acc[i]); + } else { + out[i] = dot_acc[i]; + } + } + } + + static void L2SqrSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, true); + } + + static void InnerProductSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, false); + } + + static void CosineSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, false); + } + static std::vector quantize_to_int8(const void* in, size_t dim) { const uint16_t* input = static_cast(in); // Calculate storage size: dim bytes for data + 4 bytes for scale @@ -1079,6 +1265,9 @@ namespace ndd { d.sim_l2 = &float16::L2SqrSim; d.sim_ip = &float16::InnerProductSim; d.sim_cosine = &float16::CosineSim; + d.sim_l2_batch = &float16::L2SqrSimBatch; + d.sim_ip_batch = &float16::InnerProductSimBatch; + d.sim_cosine_batch = &float16::CosineSimBatch; d.quantize = &float16::quantize; d.dequantize = &float16::dequantize; d.quantize_to_int8 = &float16::quantize_to_int8; diff --git a/src/quant/float32.hpp b/src/quant/float32.hpp index 79c082e69f..2eb9f5547f 100644 --- a/src/quant/float32.hpp +++ b/src/quant/float32.hpp @@ -465,6 +465,180 @@ namespace hnswlib { return InnerProductSim(pVect1, pVect2, params_ptr); } + static constexpr size_t kBatchTileSizeF32 = +#if defined(USE_AVX512) + 1024; +#elif defined(USE_AVX2) + 512; +#elif defined(USE_SVE2) + 512; +#elif defined(USE_NEON) + 256; +#else + 128; +#endif + + static void SimilarityBatchTiled(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out, + bool l2_metric) { + if(count == 0) { + return; + } + + const DistParams* dist_params = reinterpret_cast(params); + const size_t dim = dist_params->dim; + const float* query_vec = reinterpret_cast(query); + + std::vector dot_acc(count, 0.0f); + std::vector vec_sq_acc; + if(l2_metric) { + vec_sq_acc.assign(count, 0.0f); + } + + float query_sq_acc = 0.0f; + const size_t tile = std::min(dim, kBatchTileSizeF32); + + for(size_t block_start = 0; block_start < dim; block_start += tile) { + const size_t block_len = std::min(tile, dim - block_start); + const float* q_ptr = query_vec + block_start; + + for(size_t d = 0; d < block_len; ++d) { + query_sq_acc += q_ptr[d] * q_ptr[d]; + } + + for(size_t i = 0; i < count; ++i) { + const float* v_ptr = reinterpret_cast(vectors[i]) + block_start; + float dot = dot_acc[i]; + float vec_sq = l2_metric ? vec_sq_acc[i] : 0.0f; + + size_t d = 0; +#if defined(USE_AVX512) + __m512 dot_vec = _mm512_setzero_ps(); + __m512 sq_vec = _mm512_setzero_ps(); + for(; d + 16 <= block_len; d += 16) { + __m512 qv = _mm512_loadu_ps(q_ptr + d); + __m512 vv = _mm512_loadu_ps(v_ptr + d); + dot_vec = _mm512_fmadd_ps(qv, vv, dot_vec); + if(l2_metric) { + sq_vec = _mm512_fmadd_ps(vv, vv, sq_vec); + } + } + dot += _mm512_reduce_add_ps(dot_vec); + if(l2_metric) { + vec_sq += _mm512_reduce_add_ps(sq_vec); + } +#elif defined(USE_AVX2) + __m256 dot_vec = _mm256_setzero_ps(); + __m256 sq_vec = _mm256_setzero_ps(); + for(; d + 8 <= block_len; d += 8) { + __m256 qv = _mm256_loadu_ps(q_ptr + d); + __m256 vv = _mm256_loadu_ps(v_ptr + d); +#if defined(__FMA__) + dot_vec = _mm256_fmadd_ps(qv, vv, dot_vec); + if(l2_metric) { + sq_vec = _mm256_fmadd_ps(vv, vv, sq_vec); + } +#else + dot_vec = _mm256_add_ps(dot_vec, _mm256_mul_ps(qv, vv)); + if(l2_metric) { + sq_vec = _mm256_add_ps(sq_vec, _mm256_mul_ps(vv, vv)); + } +#endif + } + { + __m128 lo = _mm256_castps256_ps128(dot_vec); + __m128 hi = _mm256_extractf128_ps(dot_vec, 1); + __m128 s = _mm_add_ps(lo, hi); + s = _mm_hadd_ps(s, s); + s = _mm_hadd_ps(s, s); + dot += _mm_cvtss_f32(s); + } + if(l2_metric) { + __m128 lo = _mm256_castps256_ps128(sq_vec); + __m128 hi = _mm256_extractf128_ps(sq_vec, 1); + __m128 s = _mm_add_ps(lo, hi); + s = _mm_hadd_ps(s, s); + s = _mm_hadd_ps(s, s); + vec_sq += _mm_cvtss_f32(s); + } +#elif defined(USE_NEON) + float32x4_t dot_vec = vdupq_n_f32(0.0f); + float32x4_t sq_vec = vdupq_n_f32(0.0f); + for(; d + 4 <= block_len; d += 4) { + float32x4_t qv = vld1q_f32(q_ptr + d); + float32x4_t vv = vld1q_f32(v_ptr + d); + dot_vec = vfmaq_f32(dot_vec, qv, vv); + if(l2_metric) { + sq_vec = vfmaq_f32(sq_vec, vv, vv); + } + } + dot += vaddvq_f32(dot_vec); + if(l2_metric) { + vec_sq += vaddvq_f32(sq_vec); + } +#elif defined(USE_SVE2) + size_t lane = svcntw(); + for(; d + lane <= block_len; d += lane) { + svbool_t pg = svwhilelt_b32(0, lane); + svfloat32_t qv = svld1_f32(pg, q_ptr + d); + svfloat32_t vv = svld1_f32(pg, v_ptr + d); + dot += svaddv_f32(pg, svmul_f32_x(pg, qv, vv)); + if(l2_metric) { + vec_sq += svaddv_f32(pg, svmul_f32_x(pg, vv, vv)); + } + } +#endif + + for(; d < block_len; ++d) { + dot += q_ptr[d] * v_ptr[d]; + if(l2_metric) { + vec_sq += v_ptr[d] * v_ptr[d]; + } + } + + dot_acc[i] = dot; + if(l2_metric) { + vec_sq_acc[i] = vec_sq; + } + } + } + + for(size_t i = 0; i < count; ++i) { + if(l2_metric) { + out[i] = -(query_sq_acc + vec_sq_acc[i] - 2.0f * dot_acc[i]); + } else { + out[i] = dot_acc[i]; + } + } + } + + static void L2SqrSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, true); + } + + static void InnerProductSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, false); + } + + static void CosineSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, false); + } + static float InnerProductDistance(const void* pVect1, const void* pVect2, const void* params_ptr) { const DistParams* params = reinterpret_cast(params_ptr); @@ -551,6 +725,9 @@ namespace ndd { d.sim_l2 = &hnswlib::quant::float32::L2SqrSim; d.sim_ip = &hnswlib::quant::float32::InnerProductSim; d.sim_cosine = &hnswlib::quant::float32::CosineSim; + d.sim_l2_batch = &hnswlib::quant::float32::L2SqrSimBatch; + d.sim_ip_batch = &hnswlib::quant::float32::InnerProductSimBatch; + d.sim_cosine_batch = &hnswlib::quant::float32::CosineSimBatch; d.quantize = &hnswlib::quant::float32::quantize; d.dequantize = &hnswlib::quant::float32::dequantize; d.quantize_to_int8 = &hnswlib::quant::float32::quantize_to_int8; diff --git a/src/quant/int16.hpp b/src/quant/int16.hpp index 5d2029f3a8..26531a0085 100644 --- a/src/quant/int16.hpp +++ b/src/quant/int16.hpp @@ -807,23 +807,66 @@ namespace ndd { size_t i = 0; #if defined(USE_AVX512) - __m512i sum_vec = _mm512_setzero_si512(); + __m512i sum_vec0 = _mm512_setzero_si512(); + __m512i sum_vec1 = _mm512_setzero_si512(); + + for(; i + 64 <= qty; i += 64) { + __m512i v1_0 = _mm512_loadu_si512((const __m512i*)(pVect1 + i)); + __m512i v2_0 = _mm512_loadu_si512((const __m512i*)(pVect2 + i)); + __m512i v1_1 = _mm512_loadu_si512((const __m512i*)(pVect1 + i + 32)); + __m512i v2_1 = _mm512_loadu_si512((const __m512i*)(pVect2 + i + 32)); + + __m512i prod0 = _mm512_dpwssd_epi32(_mm512_setzero_si512(), v1_0, v2_0); + __m512i prod1 = _mm512_dpwssd_epi32(_mm512_setzero_si512(), v1_1, v2_1); + + __m512i prod0_lo = _mm512_cvtepi32_epi64(_mm512_castsi512_si256(prod0)); + __m512i prod0_hi = + _mm512_cvtepi32_epi64(_mm512_extracti32x8_epi32(prod0, 1)); + __m512i prod1_lo = _mm512_cvtepi32_epi64(_mm512_castsi512_si256(prod1)); + __m512i prod1_hi = + _mm512_cvtepi32_epi64(_mm512_extracti32x8_epi32(prod1, 1)); + + sum_vec0 = _mm512_add_epi64(sum_vec0, prod0_lo); + sum_vec1 = _mm512_add_epi64(sum_vec1, prod0_hi); + sum_vec0 = _mm512_add_epi64(sum_vec0, prod1_lo); + sum_vec1 = _mm512_add_epi64(sum_vec1, prod1_hi); + } for(; i + 32 <= qty; i += 32) { __m512i v1 = _mm512_loadu_si512((const __m512i*)(pVect1 + i)); __m512i v2 = _mm512_loadu_si512((const __m512i*)(pVect2 + i)); - // Multiply and add adjacent pairs -> 16 x 32-bit integers - __m512i prod = _mm512_madd_epi16(v1, v2); + __m512i prod = _mm512_dpwssd_epi32(_mm512_setzero_si512(), v1, v2); - // Extend to 64-bit and accumulate __m512i prod_lo = _mm512_cvtepi32_epi64(_mm512_castsi512_si256(prod)); __m512i prod_hi = _mm512_cvtepi32_epi64(_mm512_extracti32x8_epi32(prod, 1)); - sum_vec = _mm512_add_epi64(sum_vec, prod_lo); - sum_vec = _mm512_add_epi64(sum_vec, prod_hi); + sum_vec0 = _mm512_add_epi64(sum_vec0, prod_lo); + sum_vec1 = _mm512_add_epi64(sum_vec1, prod_hi); + } + + sum = _mm512_reduce_add_epi64(sum_vec0) + _mm512_reduce_add_epi64(sum_vec1); + + for(; i + 16 <= qty; i += 16) { + __m256i v1 = _mm256_loadu_si256((const __m256i*)(pVect1 + i)); + __m256i v2 = _mm256_loadu_si256((const __m256i*)(pVect2 + i)); + __m256i prod = _mm256_madd_epi16(v1, v2); + + __m256i prod_lo = _mm256_cvtepi32_epi64(_mm256_castsi256_si128(prod)); + __m256i prod_hi = _mm256_cvtepi32_epi64(_mm256_extracti128_si256(prod, 1)); + + __m128i sum_128 = _mm_add_epi64(_mm256_castsi256_si128(prod_lo), + _mm256_extracti128_si256(prod_lo, 1)); + __m128i high64 = _mm_unpackhi_epi64(sum_128, sum_128); + sum_128 = _mm_add_epi64(sum_128, high64); + sum += _mm_cvtsi128_si64(sum_128); + + sum_128 = _mm_add_epi64(_mm256_castsi256_si128(prod_hi), + _mm256_extracti128_si256(prod_hi, 1)); + high64 = _mm_unpackhi_epi64(sum_128, sum_128); + sum_128 = _mm_add_epi64(sum_128, high64); + sum += _mm_cvtsi128_si64(sum_128); } - sum = _mm512_reduce_add_epi64(sum_vec); #elif defined(USE_AVX2) __m256i sum_vec = _mm256_setzero_si256(); @@ -909,76 +952,72 @@ namespace ndd { sum_vec0 = vaddq_s64(sum_vec0, sum_vec2); sum = vgetq_lane_s64(sum_vec0, 0) + vgetq_lane_s64(sum_vec0, 1); #elif defined(USE_SVE2) - uint64_t num_elements = svcnth(); - size_t unroll_stride = num_elements * 4; - svbool_t pg_all = svptrue_b16(); - svbool_t pg_64 = svptrue_b64(); - svint32_t zero_s32 = svdup_s32(0); + int64x2_t sum_vec0 = vdupq_n_s64(0); + int64x2_t sum_vec1 = vdupq_n_s64(0); + int64x2_t sum_vec2 = vdupq_n_s64(0); + int64x2_t sum_vec3 = vdupq_n_s64(0); + + size_t qty32 = qty / 32; + for(; i < qty32 * 32; i += 32) { + int16x8_t v1_0 = vld1q_s16(pVect1 + i); + int16x8_t v2_0 = vld1q_s16(pVect2 + i); + int32x4_t prod0_lo = vmull_s16(vget_low_s16(v1_0), vget_low_s16(v2_0)); + int32x4_t prod0_hi = vmull_s16(vget_high_s16(v1_0), vget_high_s16(v2_0)); + sum_vec0 = vpadalq_s32(sum_vec0, prod0_lo); + sum_vec0 = vpadalq_s32(sum_vec0, prod0_hi); - svint64_t sum0 = svdup_s64(0); - svint64_t sum1 = svdup_s64(0); - svint64_t sum2 = svdup_s64(0); - svint64_t sum3 = svdup_s64(0); + int16x8_t v1_1 = vld1q_s16(pVect1 + i + 8); + int16x8_t v2_1 = vld1q_s16(pVect2 + i + 8); + int32x4_t prod1_lo = vmull_s16(vget_low_s16(v1_1), vget_low_s16(v2_1)); + int32x4_t prod1_hi = vmull_s16(vget_high_s16(v1_1), vget_high_s16(v2_1)); + sum_vec1 = vpadalq_s32(sum_vec1, prod1_lo); + sum_vec1 = vpadalq_s32(sum_vec1, prod1_hi); - for(; i + unroll_stride <= qty; i += unroll_stride) { - svint16_t v1_0 = svld1_s16(pg_all, pVect1 + i); - svint16_t v2_0 = svld1_s16(pg_all, pVect2 + i); - svint32_t p_lo_0 = svmlalb_s32(zero_s32, v1_0, v2_0); - svint32_t p_hi_0 = svmlalt_s32(zero_s32, v1_0, v2_0); - sum0 = svadd_s64_x(pg_64, sum0, svaddlb_s64(p_lo_0, zero_s32)); - sum1 = svadd_s64_x(pg_64, sum1, svaddlt_s64(p_lo_0, zero_s32)); - sum2 = svadd_s64_x(pg_64, sum2, svaddlb_s64(p_hi_0, zero_s32)); - sum3 = svadd_s64_x(pg_64, sum3, svaddlt_s64(p_hi_0, zero_s32)); + int16x8_t v1_2 = vld1q_s16(pVect1 + i + 16); + int16x8_t v2_2 = vld1q_s16(pVect2 + i + 16); + int32x4_t prod2_lo = vmull_s16(vget_low_s16(v1_2), vget_low_s16(v2_2)); + int32x4_t prod2_hi = vmull_s16(vget_high_s16(v1_2), vget_high_s16(v2_2)); + sum_vec2 = vpadalq_s32(sum_vec2, prod2_lo); + sum_vec2 = vpadalq_s32(sum_vec2, prod2_hi); - svint16_t v1_1 = svld1_s16(pg_all, pVect1 + i + num_elements); - svint16_t v2_1 = svld1_s16(pg_all, pVect2 + i + num_elements); - svint32_t p_lo_1 = svmlalb_s32(zero_s32, v1_1, v2_1); - svint32_t p_hi_1 = svmlalt_s32(zero_s32, v1_1, v2_1); - sum0 = svadd_s64_x(pg_64, sum0, svaddlb_s64(p_lo_1, zero_s32)); - sum1 = svadd_s64_x(pg_64, sum1, svaddlt_s64(p_lo_1, zero_s32)); - sum2 = svadd_s64_x(pg_64, sum2, svaddlb_s64(p_hi_1, zero_s32)); - sum3 = svadd_s64_x(pg_64, sum3, svaddlt_s64(p_hi_1, zero_s32)); - - svint16_t v1_2 = svld1_s16(pg_all, pVect1 + i + 2 * num_elements); - svint16_t v2_2 = svld1_s16(pg_all, pVect2 + i + 2 * num_elements); - svint32_t p_lo_2 = svmlalb_s32(zero_s32, v1_2, v2_2); - svint32_t p_hi_2 = svmlalt_s32(zero_s32, v1_2, v2_2); - sum0 = svadd_s64_x(pg_64, sum0, svaddlb_s64(p_lo_2, zero_s32)); - sum1 = svadd_s64_x(pg_64, sum1, svaddlt_s64(p_lo_2, zero_s32)); - sum2 = svadd_s64_x(pg_64, sum2, svaddlb_s64(p_hi_2, zero_s32)); - sum3 = svadd_s64_x(pg_64, sum3, svaddlt_s64(p_hi_2, zero_s32)); - - svint16_t v1_3 = svld1_s16(pg_all, pVect1 + i + 3 * num_elements); - svint16_t v2_3 = svld1_s16(pg_all, pVect2 + i + 3 * num_elements); - svint32_t p_lo_3 = svmlalb_s32(zero_s32, v1_3, v2_3); - svint32_t p_hi_3 = svmlalt_s32(zero_s32, v1_3, v2_3); - sum0 = svadd_s64_x(pg_64, sum0, svaddlb_s64(p_lo_3, zero_s32)); - sum1 = svadd_s64_x(pg_64, sum1, svaddlt_s64(p_lo_3, zero_s32)); - sum2 = svadd_s64_x(pg_64, sum2, svaddlb_s64(p_hi_3, zero_s32)); - sum3 = svadd_s64_x(pg_64, sum3, svaddlt_s64(p_hi_3, zero_s32)); + int16x8_t v1_3 = vld1q_s16(pVect1 + i + 24); + int16x8_t v2_3 = vld1q_s16(pVect2 + i + 24); + int32x4_t prod3_lo = vmull_s16(vget_low_s16(v1_3), vget_low_s16(v2_3)); + int32x4_t prod3_hi = vmull_s16(vget_high_s16(v1_3), vget_high_s16(v2_3)); + sum_vec3 = vpadalq_s32(sum_vec3, prod3_lo); + sum_vec3 = vpadalq_s32(sum_vec3, prod3_hi); } - svint64_t sum_vec = svadd_s64_x(svptrue_b64(), sum0, sum1); - sum_vec = svadd_s64_x(svptrue_b64(), sum_vec, sum2); - sum_vec = svadd_s64_x(svptrue_b64(), sum_vec, sum3); - - svbool_t pg = svwhilelt_b64(i, qty); - while(svptest_any(svptrue_b64(), pg)) { - svint64_t v1 = svld1sh_s64(pg, pVect1 + i); - svint64_t v2 = svld1sh_s64(pg, pVect2 + i); - svint64_t prod = svmul_s64_x(pg, v1, v2); - sum_vec = svadd_s64_x(pg, sum_vec, prod); - i += svcntd(); - pg = svwhilelt_b64(i, qty); + size_t qty16 = qty / 16; + for(; i < qty16 * 16; i += 16) { + int16x8_t v1_0 = vld1q_s16(pVect1 + i); + int16x8_t v2_0 = vld1q_s16(pVect2 + i); + int16x8_t v1_1 = vld1q_s16(pVect1 + i + 8); + int16x8_t v2_1 = vld1q_s16(pVect2 + i + 8); + + int32x4_t prod0_lo = vmull_s16(vget_low_s16(v1_0), vget_low_s16(v2_0)); + int32x4_t prod0_hi = vmull_s16(vget_high_s16(v1_0), vget_high_s16(v2_0)); + int32x4_t prod1_lo = vmull_s16(vget_low_s16(v1_1), vget_low_s16(v2_1)); + int32x4_t prod1_hi = vmull_s16(vget_high_s16(v1_1), vget_high_s16(v2_1)); + + sum_vec0 = vpadalq_s32(sum_vec0, prod0_lo); + sum_vec0 = vpadalq_s32(sum_vec0, prod0_hi); + sum_vec1 = vpadalq_s32(sum_vec1, prod1_lo); + sum_vec1 = vpadalq_s32(sum_vec1, prod1_hi); } - sum = svaddv_s64(svptrue_b64(), sum_vec); + + sum_vec0 = vaddq_s64(sum_vec0, sum_vec1); + sum_vec2 = vaddq_s64(sum_vec2, sum_vec3); + sum_vec0 = vaddq_s64(sum_vec0, sum_vec2); + sum = vgetq_lane_s64(sum_vec0, 0) + vgetq_lane_s64(sum_vec0, 1); #endif for(; i < qty; i++) { sum += static_cast(pVect1[i]) * static_cast(pVect2[i]); } - return (static_cast(sum) * scale1) * scale2; + float combined_scale = scale1 * scale2; + return static_cast(sum) * combined_scale; } static float @@ -995,6 +1034,250 @@ namespace ndd { return 1.0f - CosineSim(pVect1v, pVect2v, qty_ptr); } + static constexpr size_t kBatchTileSizeInt16 = +#if defined(USE_AVX512) + 512; +#elif defined(USE_AVX2) + 256; +#elif defined(USE_SVE2) + 256; +#elif defined(USE_NEON) + 128; +#else + 64; +#endif + + static void SimilarityBatchTiled(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out, + bool l2_metric) { + if(count == 0) { + return; + } + + const auto* dist_params = static_cast(params); + const size_t dim = dist_params->dim; + const auto* query_vec = static_cast(query); + const float query_scale = extract_scale(static_cast(query), dim); + + std::vector dot_acc(count, 0); + std::vector vec_sq_acc; + if(l2_metric) { + vec_sq_acc.assign(count, 0); + } + + int64_t query_sq_acc = 0; + const size_t tile = std::min(dim, kBatchTileSizeInt16); + std::vector query_tile(tile, 0); + + for(size_t block_start = 0; block_start < dim; block_start += tile) { + const size_t block_len = std::min(tile, dim - block_start); + + for(size_t d = 0; d < block_len; ++d) { + int32_t qv = static_cast(query_vec[block_start + d]); + query_tile[d] = qv; + query_sq_acc += static_cast(qv) * qv; + } + + for(size_t i = 0; i < count; ++i) { + const auto* vec = static_cast(vectors[i]); + int64_t dot = dot_acc[i]; + int64_t vec_sq = l2_metric ? vec_sq_acc[i] : 0; + + size_t d = 0; +#if defined(USE_AVX512) + __m512i dot_vec_lo = _mm512_setzero_si512(); + __m512i dot_vec_hi = _mm512_setzero_si512(); + __m512i sq_vec_lo = _mm512_setzero_si512(); + __m512i sq_vec_hi = _mm512_setzero_si512(); + + for(; d + 32 <= block_len; d += 32) { + __m512i q_i16 = _mm512_loadu_si512( + reinterpret_cast(query_vec + block_start + d)); + __m512i v_i16 = _mm512_loadu_si512( + reinterpret_cast(vec + block_start + d)); + + __m512i dot_i32 = + _mm512_dpwssd_epi32(_mm512_setzero_si512(), q_i16, v_i16); + __m512i dot_i64_lo = + _mm512_cvtepi32_epi64(_mm512_castsi512_si256(dot_i32)); + __m512i dot_i64_hi = + _mm512_cvtepi32_epi64(_mm512_extracti32x8_epi32(dot_i32, 1)); + dot_vec_lo = _mm512_add_epi64(dot_vec_lo, dot_i64_lo); + dot_vec_hi = _mm512_add_epi64(dot_vec_hi, dot_i64_hi); + + if(l2_metric) { + __m512i sq_i32 = + _mm512_dpwssd_epi32(_mm512_setzero_si512(), v_i16, v_i16); + __m512i sq_i64_lo = + _mm512_cvtepi32_epi64(_mm512_castsi512_si256(sq_i32)); + __m512i sq_i64_hi = _mm512_cvtepi32_epi64( + _mm512_extracti32x8_epi32(sq_i32, 1)); + sq_vec_lo = _mm512_add_epi64(sq_vec_lo, sq_i64_lo); + sq_vec_hi = _mm512_add_epi64(sq_vec_hi, sq_i64_hi); + } + } + + dot += _mm512_reduce_add_epi64(dot_vec_lo) + + _mm512_reduce_add_epi64(dot_vec_hi); + if(l2_metric) { + vec_sq += _mm512_reduce_add_epi64(sq_vec_lo) + + _mm512_reduce_add_epi64(sq_vec_hi); + } +#elif defined(USE_AVX2) + __m256i dot_vec_lo = _mm256_setzero_si256(); + __m256i dot_vec_hi = _mm256_setzero_si256(); + __m256i sq_vec_lo = _mm256_setzero_si256(); + __m256i sq_vec_hi = _mm256_setzero_si256(); + for(; d + 8 <= block_len; d += 8) { + __m128i q_i16 = _mm_loadu_si128( + reinterpret_cast(query_vec + block_start + d)); + __m128i v_i16 = _mm_loadu_si128( + reinterpret_cast(vec + block_start + d)); + + __m256i q_i32 = _mm256_cvtepi16_epi32(q_i16); + __m256i v_i32 = _mm256_cvtepi16_epi32(v_i16); + __m256i dot_i32 = _mm256_mullo_epi32(q_i32, v_i32); + __m256i dot_i64_lo = _mm256_cvtepi32_epi64(_mm256_castsi256_si128(dot_i32)); + __m256i dot_i64_hi = + _mm256_cvtepi32_epi64(_mm256_extracti128_si256(dot_i32, 1)); + dot_vec_lo = _mm256_add_epi64(dot_vec_lo, dot_i64_lo); + dot_vec_hi = _mm256_add_epi64(dot_vec_hi, dot_i64_hi); + if(l2_metric) { + __m256i sq_i32 = _mm256_mullo_epi32(v_i32, v_i32); + __m256i sq_i64_lo = + _mm256_cvtepi32_epi64(_mm256_castsi256_si128(sq_i32)); + __m256i sq_i64_hi = + _mm256_cvtepi32_epi64(_mm256_extracti128_si256(sq_i32, 1)); + sq_vec_lo = _mm256_add_epi64(sq_vec_lo, sq_i64_lo); + sq_vec_hi = _mm256_add_epi64(sq_vec_hi, sq_i64_hi); + } + } + { + __m128i d_lo = _mm_add_epi64(_mm256_castsi256_si128(dot_vec_lo), + _mm256_extracti128_si256(dot_vec_lo, 1)); + __m128i d_hi = _mm_add_epi64(_mm256_castsi256_si128(dot_vec_hi), + _mm256_extracti128_si256(dot_vec_hi, 1)); + d_lo = _mm_add_epi64(d_lo, d_hi); + __m128i d_swap = _mm_unpackhi_epi64(d_lo, d_lo); + d_lo = _mm_add_epi64(d_lo, d_swap); + dot += static_cast(_mm_cvtsi128_si64(d_lo)); + } + if(l2_metric) { + __m128i s_lo = _mm_add_epi64(_mm256_castsi256_si128(sq_vec_lo), + _mm256_extracti128_si256(sq_vec_lo, 1)); + __m128i s_hi = _mm_add_epi64(_mm256_castsi256_si128(sq_vec_hi), + _mm256_extracti128_si256(sq_vec_hi, 1)); + s_lo = _mm_add_epi64(s_lo, s_hi); + __m128i s_swap = _mm_unpackhi_epi64(s_lo, s_lo); + s_lo = _mm_add_epi64(s_lo, s_swap); + vec_sq += static_cast(_mm_cvtsi128_si64(s_lo)); + } +#elif defined(USE_NEON) + int64x2_t dot_vec = vdupq_n_s64(0); + int64x2_t sq_vec = vdupq_n_s64(0); + for(; d + 8 <= block_len; d += 8) { + int16x8_t q_i16 = vld1q_s16(query_vec + block_start + d); + int16x8_t v_i16 = vld1q_s16(vec + block_start + d); + + int32x4_t dot_lo = vmull_s16(vget_low_s16(q_i16), vget_low_s16(v_i16)); + int32x4_t dot_hi = vmull_s16(vget_high_s16(q_i16), vget_high_s16(v_i16)); + dot_vec = vpadalq_s32(dot_vec, dot_lo); + dot_vec = vpadalq_s32(dot_vec, dot_hi); + + if(l2_metric) { + int32x4_t sq_lo = vmull_s16(vget_low_s16(v_i16), vget_low_s16(v_i16)); + int32x4_t sq_hi = vmull_s16(vget_high_s16(v_i16), vget_high_s16(v_i16)); + sq_vec = vpadalq_s32(sq_vec, sq_lo); + sq_vec = vpadalq_s32(sq_vec, sq_hi); + } + } + dot += static_cast(vgetq_lane_s64(dot_vec, 0)) + + static_cast(vgetq_lane_s64(dot_vec, 1)); + if(l2_metric) { + vec_sq += static_cast(vgetq_lane_s64(sq_vec, 0)) + + static_cast(vgetq_lane_s64(sq_vec, 1)); + } +#elif defined(USE_SVE2) + const size_t vec_lanes = svcntw(); + for(; d + vec_lanes <= block_len; d += vec_lanes) { + svbool_t pg = svwhilelt_b32(0, vec_lanes); + svint32_t q_i32 = svld1sh_s32(pg, query_vec + block_start + d); + svint32_t v_i32 = svld1sh_s32(pg, vec + block_start + d); + svint32_t dot_prod = svmul_s32_x(pg, q_i32, v_i32); + svint64_t dot_lo = svunpklo_s64(dot_prod); + svint64_t dot_hi = svunpkhi_s64(dot_prod); + dot += svaddv_s64(svptrue_b64(), dot_lo) + + svaddv_s64(svptrue_b64(), dot_hi); + if(l2_metric) { + svint32_t sq_prod = svmul_s32_x(pg, v_i32, v_i32); + svint64_t sq_lo = svunpklo_s64(sq_prod); + svint64_t sq_hi = svunpkhi_s64(sq_prod); + vec_sq += svaddv_s64(svptrue_b64(), sq_lo) + + svaddv_s64(svptrue_b64(), sq_hi); + } + } +#endif + + for(; d < block_len; ++d) { + int32_t vv = static_cast(vec[block_start + d]); + dot += static_cast(query_tile[d]) * vv; + if(l2_metric) { + vec_sq += static_cast(vv) * vv; + } + } + + dot_acc[i] = dot; + if(l2_metric) { + vec_sq_acc[i] = vec_sq; + } + } + } + + const float query_scale_sq = query_scale * query_scale; + for(size_t i = 0; i < count; ++i) { + const auto* vec_u8 = static_cast(vectors[i]); + const float vec_scale = extract_scale(vec_u8, dim); + const float cross_scale = query_scale * vec_scale; + + if(l2_metric) { + const float vec_scale_sq = vec_scale * vec_scale; + const float l2 = static_cast(query_sq_acc) * query_scale_sq + + static_cast(vec_sq_acc[i]) * vec_scale_sq + - 2.0f * static_cast(dot_acc[i]) * cross_scale; + out[i] = -l2; + } else { + out[i] = static_cast(dot_acc[i]) * cross_scale; + } + } + } + + static void L2SqrSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, true); + } + + static void InnerProductSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, false); + } + + static void CosineSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, false); + } + // Direct Int16 -> Int8 quantization static std::vector quantize_to_int8(const void* in, size_t dim) { const int16_t* in_data = static_cast(in); @@ -1066,6 +1349,9 @@ namespace ndd { d.sim_l2 = &int16::L2SqrSim; d.sim_ip = &int16::InnerProductSim; d.sim_cosine = &int16::CosineSim; + d.sim_l2_batch = &int16::L2SqrSimBatch; + d.sim_ip_batch = &int16::InnerProductSimBatch; + d.sim_cosine_batch = &int16::CosineSimBatch; d.quantize = &int16::quantize; d.dequantize = &int16::dequantize; d.quantize_to_int8 = &int16::quantize_to_int8; diff --git a/src/quant/int8.hpp b/src/quant/int8.hpp index 2e183033de..f0ba9733b7 100644 --- a/src/quant/int8.hpp +++ b/src/quant/int8.hpp @@ -776,21 +776,60 @@ namespace ndd { size_t i = 0; #if defined(USE_AVX512) - __m512i v_sum = _mm512_setzero_si512(); - for(; i + 32 <= qty; i += 32) { - __m256i v1_256 = _mm256_loadu_si256((const __m256i*)(pVect1 + i)); - __m256i v2_256 = _mm256_loadu_si256((const __m256i*)(pVect2 + i)); + __m512i dot_acc0 = _mm512_setzero_si512(); + __m512i dot_acc1 = _mm512_setzero_si512(); + __m512i dot_acc2 = _mm512_setzero_si512(); + __m512i dot_acc3 = _mm512_setzero_si512(); + __m512i sum2_acc0 = _mm512_setzero_si512(); + __m512i sum2_acc1 = _mm512_setzero_si512(); + __m512i sum2_acc2 = _mm512_setzero_si512(); + __m512i sum2_acc3 = _mm512_setzero_si512(); + const __m512i sign_flip = _mm512_set1_epi8(static_cast(0x80)); + const __m512i ones_u8 = _mm512_set1_epi8(static_cast(0x01)); + + for(; i + 256 <= qty; i += 256) { + __m512i v1_0 = _mm512_loadu_si512((const __m512i*)(pVect1 + i)); + __m512i v2_0 = _mm512_loadu_si512((const __m512i*)(pVect2 + i)); + __m512i v1_1 = _mm512_loadu_si512((const __m512i*)(pVect1 + i + 64)); + __m512i v2_1 = _mm512_loadu_si512((const __m512i*)(pVect2 + i + 64)); + __m512i v1_2 = _mm512_loadu_si512((const __m512i*)(pVect1 + i + 128)); + __m512i v2_2 = _mm512_loadu_si512((const __m512i*)(pVect2 + i + 128)); + __m512i v1_3 = _mm512_loadu_si512((const __m512i*)(pVect1 + i + 192)); + __m512i v2_3 = _mm512_loadu_si512((const __m512i*)(pVect2 + i + 192)); + + __m512i v1_u8_0 = _mm512_xor_si512(v1_0, sign_flip); + __m512i v1_u8_1 = _mm512_xor_si512(v1_1, sign_flip); + __m512i v1_u8_2 = _mm512_xor_si512(v1_2, sign_flip); + __m512i v1_u8_3 = _mm512_xor_si512(v1_3, sign_flip); + + dot_acc0 = _mm512_dpbusd_epi32(dot_acc0, v1_u8_0, v2_0); + dot_acc1 = _mm512_dpbusd_epi32(dot_acc1, v1_u8_1, v2_1); + dot_acc2 = _mm512_dpbusd_epi32(dot_acc2, v1_u8_2, v2_2); + dot_acc3 = _mm512_dpbusd_epi32(dot_acc3, v1_u8_3, v2_3); + + sum2_acc0 = _mm512_dpbusd_epi32(sum2_acc0, ones_u8, v2_0); + sum2_acc1 = _mm512_dpbusd_epi32(sum2_acc1, ones_u8, v2_1); + sum2_acc2 = _mm512_dpbusd_epi32(sum2_acc2, ones_u8, v2_2); + sum2_acc3 = _mm512_dpbusd_epi32(sum2_acc3, ones_u8, v2_3); + } - // Sign extend to 16-bit - __m512i v1_512 = _mm512_cvtepi8_epi16(v1_256); - __m512i v2_512 = _mm512_cvtepi8_epi16(v2_256); + __m512i dot_acc = _mm512_add_epi32(_mm512_add_epi32(dot_acc0, dot_acc1), + _mm512_add_epi32(dot_acc2, dot_acc3)); + __m512i sum2_acc = _mm512_add_epi32(_mm512_add_epi32(sum2_acc0, sum2_acc1), + _mm512_add_epi32(sum2_acc2, sum2_acc3)); - // Multiply and add adjacent pairs (produces 32-bit integers) - __m512i prod = _mm512_madd_epi16(v1_512, v2_512); + for(; i + 64 <= qty; i += 64) { + __m512i v1 = _mm512_loadu_si512((const __m512i*)(pVect1 + i)); + __m512i v2 = _mm512_loadu_si512((const __m512i*)(pVect2 + i)); - v_sum = _mm512_add_epi32(v_sum, prod); + __m512i v1_u8 = _mm512_xor_si512(v1, sign_flip); + dot_acc = _mm512_dpbusd_epi32(dot_acc, v1_u8, v2); + sum2_acc = _mm512_dpbusd_epi32(sum2_acc, ones_u8, v2); } - sum = _mm512_reduce_add_epi32(v_sum); + + int32_t dot_u = _mm512_reduce_add_epi32(dot_acc); + int32_t sum2 = _mm512_reduce_add_epi32(sum2_acc); + sum = dot_u - 128 * sum2; #elif defined(USE_AVX2) __m256i v_sum = _mm256_setzero_si256(); for(; i + 16 <= qty; i += 16) { @@ -909,6 +948,202 @@ namespace ndd { return 1.0f - CosineSim(pVect1v, pVect2v, qty_ptr); } + static constexpr size_t kBatchTileSizeInt8 = +#if defined(USE_AVX512) + 1024; +#elif defined(USE_AVX2) + 512; +#elif defined(USE_SVE2) + 512; +#elif defined(USE_NEON) + 256; +#else + 128; +#endif + + static void SimilarityBatchTiled(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out, + bool l2_metric) { + if(count == 0) { + return; + } + + const auto* dist_params = static_cast(params); + const size_t dim = dist_params->dim; + const auto* query_vec = static_cast(query); + const float query_scale = extract_scale(static_cast(query), dim); + + std::vector dot_acc(count, 0); + std::vector vec_sq_acc; + if(l2_metric) { + vec_sq_acc.assign(count, 0); + } + + int64_t query_sq_acc = 0; + const size_t tile = std::min(dim, kBatchTileSizeInt8); + std::vector query_tile(tile, 0); + + for(size_t block_start = 0; block_start < dim; block_start += tile) { + const size_t block_len = std::min(tile, dim - block_start); + + for(size_t d = 0; d < block_len; ++d) { + int32_t qv = static_cast(query_vec[block_start + d]); + query_tile[d] = qv; + query_sq_acc += static_cast(qv) * qv; + } + + for(size_t i = 0; i < count; ++i) { + const auto* vec = static_cast(vectors[i]); + int64_t dot = dot_acc[i]; + int64_t vec_sq = l2_metric ? vec_sq_acc[i] : 0; + + size_t d = 0; +#if defined(USE_AVX512) + __m512i dot_vec = _mm512_setzero_si512(); + __m512i sq_vec = _mm512_setzero_si512(); + for(; d + 16 <= block_len; d += 16) { + __m128i q_i8 = _mm_loadu_si128( + reinterpret_cast(query_vec + block_start + d)); + __m128i v_i8 = _mm_loadu_si128( + reinterpret_cast(vec + block_start + d)); + + __m512i q_i32 = _mm512_cvtepi8_epi32(q_i8); + __m512i v_i32 = _mm512_cvtepi8_epi32(v_i8); + dot_vec = _mm512_add_epi32(dot_vec, _mm512_mullo_epi32(q_i32, v_i32)); + + if(l2_metric) { + sq_vec = _mm512_add_epi32(sq_vec, _mm512_mullo_epi32(v_i32, v_i32)); + } + } + dot += static_cast(_mm512_reduce_add_epi32(dot_vec)); + if(l2_metric) { + vec_sq += static_cast(_mm512_reduce_add_epi32(sq_vec)); + } +#elif defined(USE_AVX2) + __m256i dot_vec = _mm256_setzero_si256(); + __m256i sq_vec = _mm256_setzero_si256(); + for(; d + 8 <= block_len; d += 8) { + __m128i q_i8 = _mm_loadl_epi64( + reinterpret_cast(query_vec + block_start + d)); + __m128i v_i8 = _mm_loadl_epi64( + reinterpret_cast(vec + block_start + d)); + + __m256i q_i32 = _mm256_cvtepi8_epi32(q_i8); + __m256i v_i32 = _mm256_cvtepi8_epi32(v_i8); + dot_vec = _mm256_add_epi32(dot_vec, _mm256_mullo_epi32(q_i32, v_i32)); + + if(l2_metric) { + sq_vec = _mm256_add_epi32(sq_vec, _mm256_mullo_epi32(v_i32, v_i32)); + } + } + { + __m128i d0 = _mm_add_epi32(_mm256_castsi256_si128(dot_vec), + _mm256_extracti128_si256(dot_vec, 1)); + d0 = _mm_hadd_epi32(d0, d0); + d0 = _mm_hadd_epi32(d0, d0); + dot += static_cast(_mm_cvtsi128_si32(d0)); + } + if(l2_metric) { + __m128i s0 = _mm_add_epi32(_mm256_castsi256_si128(sq_vec), + _mm256_extracti128_si256(sq_vec, 1)); + s0 = _mm_hadd_epi32(s0, s0); + s0 = _mm_hadd_epi32(s0, s0); + vec_sq += static_cast(_mm_cvtsi128_si32(s0)); + } +#elif defined(USE_NEON) && defined(__ARM_FEATURE_DOTPROD) + int32x4_t dot_vec = vdupq_n_s32(0); + int32x4_t sq_vec = vdupq_n_s32(0); + for(; d + 16 <= block_len; d += 16) { + int8x16_t q_i8 = vld1q_s8(query_vec + block_start + d); + int8x16_t v_i8 = vld1q_s8(vec + block_start + d); + + dot_vec = vdotq_s32(dot_vec, q_i8, v_i8); + if(l2_metric) { + sq_vec = vdotq_s32(sq_vec, v_i8, v_i8); + } + } + dot += static_cast(vaddvq_s32(dot_vec)); + if(l2_metric) { + vec_sq += static_cast(vaddvq_s32(sq_vec)); + } +#elif defined(USE_SVE2) + svint32_t dot_vec = svdup_s32(0); + svint32_t sq_vec = svdup_s32(0); + const size_t vec_bytes = svcntb(); + for(; d + vec_bytes <= block_len; d += vec_bytes) { + svint8_t q_i8 = svld1_s8(svptrue_b8(), query_vec + block_start + d); + svint8_t v_i8 = svld1_s8(svptrue_b8(), vec + block_start + d); + dot_vec = svdot_s32(dot_vec, q_i8, v_i8); + if(l2_metric) { + sq_vec = svdot_s32(sq_vec, v_i8, v_i8); + } + } + dot += static_cast(svaddv_s32(svptrue_b32(), dot_vec)); + if(l2_metric) { + vec_sq += static_cast(svaddv_s32(svptrue_b32(), sq_vec)); + } +#endif + + for(; d < block_len; ++d) { + int32_t vv = static_cast(vec[block_start + d]); + dot += static_cast(query_tile[d]) * vv; + if(l2_metric) { + vec_sq += static_cast(vv) * vv; + } + } + + dot_acc[i] = dot; + if(l2_metric) { + vec_sq_acc[i] = vec_sq; + } + } + } + + const float query_scale_sq = query_scale * query_scale; + for(size_t i = 0; i < count; ++i) { + const auto* vec_u8 = static_cast(vectors[i]); + const float vec_scale = extract_scale(vec_u8, dim); + const float cross_scale = query_scale * vec_scale; + + if(l2_metric) { + const float vec_scale_sq = vec_scale * vec_scale; + const float l2 = static_cast(query_sq_acc) * query_scale_sq + + static_cast(vec_sq_acc[i]) * vec_scale_sq + - 2.0f * static_cast(dot_acc[i]) * cross_scale; + out[i] = -l2; + } else { + out[i] = static_cast(dot_acc[i]) * cross_scale; + } + } + } + + static void L2SqrSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, true); + } + + static void InnerProductSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, false); + } + + static void CosineSimBatch(const void* query, + const void* const* vectors, + size_t count, + const void* params, + float* out) { + SimilarityBatchTiled(query, vectors, count, params, out, false); + } + // Direct quantization to INT8 - identity function for INT8 input static std::vector quantize_to_int8_identity(const void* in, size_t dim) { size_t size = get_storage_size(dim); @@ -933,6 +1168,9 @@ namespace ndd { d.sim_l2 = &int8::L2SqrSim; d.sim_ip = &int8::InnerProductSim; d.sim_cosine = &int8::CosineSim; + d.sim_l2_batch = &int8::L2SqrSimBatch; + d.sim_ip_batch = &int8::InnerProductSimBatch; + d.sim_cosine_batch = &int8::CosineSimBatch; d.quantize = &int8::quantize; d.dequantize = &int8::dequantize; d.quantize_to_int8 = &int8::quantize_to_int8_identity; diff --git a/src/storage/vector_storage.hpp b/src/storage/vector_storage.hpp index 428c1756ac..c24c95e485 100644 --- a/src/storage/vector_storage.hpp +++ b/src/storage/vector_storage.hpp @@ -209,6 +209,40 @@ class VectorStore { } } + // Batch fetch: retrieves multiple vectors in a single MDBX read transaction. + // labels: array of external numeric IDs to fetch + // buffers: pre-allocated flat buffer of size (count * bytes_per_vector_) + // success: output array of bool indicating which fetches succeeded + // Returns number of successful fetches + size_t get_vectors_batch_into(const ndd::idInt* labels, uint8_t* buffers, + bool* success, size_t count) const { + if(count == 0) return 0; + + MDBX_txn* txn; + int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn); + if(rc != MDBX_SUCCESS) { + for(size_t i = 0; i < count; i++) success[i] = false; + return 0; + } + + size_t fetched = 0; + for(size_t i = 0; i < count; i++) { + MDBX_val key{const_cast(&labels[i]), sizeof(ndd::idInt)}; + MDBX_val data; + rc = mdbx_get(txn, dbi_, &key, &data); + if(rc == MDBX_SUCCESS && data.iov_len == bytes_per_vector_) { + std::memcpy(buffers + i * bytes_per_vector_, data.iov_base, bytes_per_vector_); + success[i] = true; + fetched++; + } else { + success[i] = false; + } + } + + mdbx_txn_abort(txn); + return fetched; + } + // Batch operations with raw bytes void store_vectors_batch(const std::vector>>& batch) { @@ -681,6 +715,12 @@ class VectorStorage { return vector_store_->get_vector_bytes(numeric_id, buffer); } + // Batch fetch: multiple vectors in one MDBX txn + size_t get_vectors_batch_into(const ndd::idInt* labels, uint8_t* buffers, + bool* success, size_t count) const { + return vector_store_->get_vectors_batch_into(labels, buffers, success, count); + } + std::vector>> get_vectors_batch(const std::vector& numeric_ids) const { return vector_store_->get_vectors_batch(numeric_ids); diff --git a/src/utils/settings.hpp b/src/utils/settings.hpp index 5fa3ef85fc..9fb975389d 100644 --- a/src/utils/settings.hpp +++ b/src/utils/settings.hpp @@ -28,7 +28,7 @@ namespace settings { constexpr size_t MAX_M = 512; constexpr size_t DEFAULT_EF_CONSTRUCT = 128; constexpr size_t MIN_EF_CONSTRUCT = 8; - constexpr size_t BACKFILL_BUFFER = 2; // Keep 2 slots free for high quality neighbors + constexpr size_t BACKFILL_BUFFER = 4; // Keep 3 slots free for high quality neighbors constexpr size_t MAX_EF_CONSTRUCT = 4096; constexpr size_t DEFAULT_EF_SEARCH = 128; constexpr size_t MIN_K = 1; @@ -42,9 +42,6 @@ namespace settings { // Number of save mutexes for parallel saves constexpr size_t NUM_INDEX_SAVE_MUTEXES = 16; - // We allow some extra neighbors before pruning - constexpr size_t MAX_EXTRA_NEIGHBORS = 3; - // MDBX default map sizes. Growth step and initial size are the same for all databases. // System tables constexpr size_t INDEX_META_MAP_SIZE_BITS = 21; // 2 MiB @@ -62,6 +59,9 @@ namespace settings { constexpr size_t MAX_LINK_LIST_LOCKS = 65536; // Sparse Storage settings + constexpr uint16_t MAX_BLOCK_SIZE = 128; // Number of elements in a block + constexpr uint32_t DEFAULT_VOCAB_SIZE = 0; // 0 means dense vectors only + constexpr uint8_t DEFAULT_QUANT_BITS = 8; constexpr size_t MAX_BMW_BLOCK_SIZE = 128; constexpr float NEAR_ZERO = 1e-9f; @@ -93,7 +93,7 @@ namespace settings { constexpr size_t DEFAULT_MAX_ELEMENTS_INCREMENT = 100'000; constexpr size_t DEFAULT_MAX_ELEMENTS_INCREMENT_TRIGGER = 50'000; constexpr size_t DEFAULT_VECTOR_CACHE_PERCENTAGE = 15; - constexpr size_t DEFAULT_VECTOR_CACHE_MIN_BITS = 17; + constexpr size_t DEFAULT_VECTOR_CACHE_MIN_BITS = 17; // Minimum 128K entries in cache const std::string DEFAULT_SERVER_ID = "unknown"; //For Backups @@ -142,7 +142,7 @@ namespace settings { inline static size_t VECTOR_CACHE_PERCENTAGE = [] { const char* env = std::getenv("NDD_VECTOR_CACHE_PERCENTAGE"); - return env ? std::stoull(env) : DEFAULT_VECTOR_CACHE_PERCENTAGE; + return env ? std::min(std::stoull(env), 100) : DEFAULT_VECTOR_CACHE_PERCENTAGE; }(); inline static size_t VECTOR_CACHE_MIN_BITS = [] { From 92ee35d93a2ac766e98c0cac53be2e18178374f9 Mon Sep 17 00:00:00 2001 From: vindwi <130017173+vindwi@users.noreply.github.com> Date: Thu, 5 Mar 2026 11:30:06 +0530 Subject: [PATCH 26/48] Multithreaded insert (#59) * bug fix * memory order * explicit checks --- src/hnsw/hnswalg.h | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/hnsw/hnswalg.h b/src/hnsw/hnswalg.h index 38ed3cbaba..cb45c7bda8 100644 --- a/src/hnsw/hnswalg.h +++ b/src/hnsw/hnswalg.h @@ -605,7 +605,7 @@ namespace hnswlib { // Initialize level 0 links // TODO - check if it is required - { + if constexpr(is_new) { char* linklist = get_linklist0(cur_c); memset(linklist, 0, sizeLinksBaseLayer_); } @@ -616,17 +616,21 @@ namespace hnswlib { total_size = data_size_ + sizeof(levelInt) + curLevel * sizeLinksUpperLayers_; - auto mem = std::make_unique(total_size); + if constexpr(is_new) { + auto mem = std::make_unique(total_size); - // copy vector - memcpy(mem.get(), datapoint, data_size_); - memcpy(mem.get() + data_size_, &curLevel, sizeof(levelInt)); - // zero initialize linklists - memset(mem.get() + data_size_ + sizeof(levelInt), - 0, - curLevel * sizeLinksUpperLayers_); + memcpy(mem.get(), datapoint, data_size_); + memcpy(mem.get() + data_size_, &curLevel, sizeof(levelInt)); + memset(mem.get() + data_size_ + sizeof(levelInt), + 0, + curLevel * sizeLinksUpperLayers_); - dataUpperLayer_[cur_c] = std::move(mem); + dataUpperLayer_[cur_c] = std::move(mem); + } else { + uint8_t* upper_mem = dataUpperLayer_[cur_c].get(); + memcpy(upper_mem, datapoint, data_size_); + memcpy(upper_mem + data_size_, &curLevel, sizeof(levelInt)); + } } if(cur_c != 0) { @@ -1278,11 +1282,12 @@ namespace hnswlib { idhInt* ll_cur = reinterpret_cast(level == 0 ? get_linklist0(cur_c) : get_linklist(cur_c, level)); if(ll_cur) { - setListCount(ll_cur, selected.size()); idhInt* data = (ll_cur + 1); for(size_t idx = 0; idx < selected.size(); idx++) { data[idx] = selected[idx].second; } + std::atomic_thread_fence(std::memory_order_release); + setListCount(ll_cur, selected.size()); } } @@ -1405,6 +1410,9 @@ namespace hnswlib { dist_t lowerBound = std::numeric_limits::lowest(); for (idhInt ep_id : ep_ids) { + if(ep_id >= maxElements_) { + continue; + } if (visited_array[ep_id] == visited_array_tag) { continue; } @@ -1489,6 +1497,10 @@ namespace hnswlib { while(!candidate_set.empty()) { auto current_pair = candidate_set.top(); idhInt current_id = current_pair.second; + if(current_id >= maxElements_) { + candidate_set.pop(); + continue; + } // Early exit if we have enough candidates if(current_pair.first < lowerBound && top_candidates.size() >= ef) { below_threshold_count++; @@ -1501,6 +1513,12 @@ namespace hnswlib { candidate_set.pop(); + if(layer != 0) { + if(getUpperLayerDataPtr(current_id) == nullptr) { + continue; + } + } + // Get neighbors idhInt* data = (layer == 0) ? (idhInt*)get_linklist0(current_id) : (idhInt*)get_linklist(current_id, layer); @@ -1518,6 +1536,7 @@ namespace hnswlib { valid_ids.reserve(size); for(idhInt j = 0; j < size; j++) { idhInt candidate_id = *(datal + j); + if(candidate_id >= maxElements_) continue; if(visited_array[candidate_id] == visited_array_tag) continue; visited_array[candidate_id] = visited_array_tag; if(has_deletions && isMarkedDeleted(candidate_id)) continue; @@ -1609,6 +1628,7 @@ namespace hnswlib { // --- Upper layer path: data is in-memory, no batching needed --- for(idhInt j = 0; j < size; j++) { idhInt candidate_id = *(datal + j); + if(candidate_id >= maxElements_) continue; if(visited_array[candidate_id] == visited_array_tag) continue; visited_array[candidate_id] = visited_array_tag; if(has_deletions && isMarkedDeleted(candidate_id)) continue; From 19443945923e6267304b5ca395816fea00539ac6 Mon Sep 17 00:00:00 2001 From: hemant-endee Date: Thu, 5 Mar 2026 12:24:55 +0530 Subject: [PATCH 27/48] async backup job (#34) * async backup job * streaming backup downloads * backup-system docs * using only operation_mutex * correction * Stream downloading backup updates (With frontend) (#40) * testing * feat: add download token for backups * refactor: remove authToken from generate_download_key * chore: update web ui link * refactor: get authToken as query param to verify download * refactor: use json_errors for download backup api * refactor: add version variable for frontend * chore: update UI version * only using unordered_map for tarcking * Using Backupstore as separate utility/helper class that handles file-level operations (tar, json, paths, tracking) * backup_store file moved to storage folder --- docs/backup-system.md | 145 +++++++++ install.sh | 7 +- src/core/ndd.hpp | 562 ++++++++++++++++++++--------------- src/main.cpp | 137 ++++++--- src/storage/backup_store.hpp | 284 ++++++++++++++++++ src/utils/archive_utils.hpp | 106 ------- 6 files changed, 848 insertions(+), 393 deletions(-) create mode 100644 docs/backup-system.md create mode 100644 src/storage/backup_store.hpp delete mode 100644 src/utils/archive_utils.hpp diff --git a/docs/backup-system.md b/docs/backup-system.md new file mode 100644 index 0000000000..1c8b0ee74d --- /dev/null +++ b/docs/backup-system.md @@ -0,0 +1,145 @@ +# Backup System + +`BackupStore` is a standalone utility class owned by `IndexManager` as a direct member (`BackupStore backup_store_`). It has no dependency on IndexManager — it handles tar operations, backup JSON, file paths, and active backup tracking. `IndexManager` orchestrates the backup flow (save, lock, metadata) and delegates file-level operations to `BackupStore`. All backup API calls go through `IndexManager` — `BackupStore` is not exposed to `main.cpp`. + +Backups are stored as `.tar` archives in per-user directories: `{DATA_DIR}/backups/{username}/`. Temp files use a centralized `{DATA_DIR}/backups/.tmp/{username}/` directory. Active backup state is tracked in-memory with mutex protection (`backup_state_mutex_`). + +## Architecture + +``` +IndexManager (ndd.hpp) +├── BackupStore backup_store_ (direct member) +├── 3 orchestration methods (inline, defined after class): +│ executeBackupJob, createBackupAsync, restoreBackup +├── 5 forwarding methods: +│ listBackups, deleteBackup, getActiveBackup, getBackupInfo, validateBackupName +└── Handles: saveIndexInternal, getIndexEntry, metadata_manager_, loadIndex + +BackupStore (src/storage/backup_store.hpp — standalone, no IndexManager dependency) +├── Archive: createBackupTar(), extractBackupTar() +├── Helpers: getUserBackupDir(), getUserTempDir(), readBackupJson(), writeBackupJson(), cleanupTempDir() +├── Active backup: setActiveBackup(), clearActiveBackup(), hasActiveBackup(), getActiveBackup() +│ (all protected by backup_state_mutex_) +├── Public methods: validateBackupName(), listBackups(), deleteBackup(), getBackupInfo() +└── Owns: data_dir_, active_user_backups_, backup_state_mutex_ (mutable) +``` + +## API Endpoints + +| Method | Endpoint | Description | +|--------|----------|-------------| +| POST | `/api/v1/index/{name}/backup` | Create async backup | +| GET | `/api/v1/backups` | List all backup files | +| GET | `/api/v1/backups/active` | Check active backup for current user | +| GET | `/api/v1/backups/{name}/info` | Get backup metadata (read from .tar) | +| POST | `/api/v1/backups/{name}/restore` | Restore backup to new index | +| DELETE | `/api/v1/backups/{name}` | Delete a backup file | +| GET | `/api/v1/backups/{name}/download` | Download backup (streaming) | +| POST | `/api/v1/backups/upload` | Upload a backup file | + +--- + +## Concurrency Model + +``` +operation_mutex (mutex, per-index) +├── Protects: index data during save + tar +├── Scope: single index +├── Held for: seconds/minutes (save + tar creation) +└── Write operations block until mutex is available +``` + +**Simple approach:** No atomic flags or file locks. The backup thread holds `operation_mutex` while saving and creating the tar. Write operations that arrive during backup simply block on the mutex until the backup releases it. One active backup per user is enforced via in-memory map protected by `backup_state_mutex_` for thread-safe access. + +**Write path during backup:** + +``` +Write: lock operation_mutex → do the write → release +Backup: lock operation_mutex → save + tar → release +``` + +If backup holds the mutex, writes block until it completes. Normal write-vs-write contention works the same way. + +--- + +## Flows + +### Create Backup (Async) + +``` +POST /index/X/backup → validateBackupName() → check no duplicate .tar on disk +→ check active_user_backups_[username] empty (one per user) +→ insert into active_user_backups_ map +→ spawn detached thread → return 202 { backup_name } +``` + +**Background thread** (`executeBackupJob`): + +``` +→ check disk space (need 2x index size) → read metadata +→ [LOCK operation_mutex] saveIndexInternal → write metadata.json → create .tmp_{name}.tar in backups/.tmp/{username}/ → cleanup metadata.json [UNLOCK operation_mutex] +→ rename .tmp_ → final tar (atomic) +→ erase from active_user_backups_ +``` + +**On failure**: cleanup temp files → erase from active_user_backups_. + +### Write During Backup + +``` +addVectors/deleteVectors/updateFilters/deleteByFilter/deleteIndex +→ [LOCK operation_mutex] do the write [UNLOCK] → 200 OK + (blocks if backup holds operation_mutex — resumes after backup completes) +``` + +### Restore Backup + +``` +POST /backups/{name}/restore +→ validate name → check tar exists → check target index does NOT exist +→ extract tar to backups/.tmp/{username}/ → read metadata.json → copy files to target dir +→ register in MetadataManager → cleanup temp dir → loadIndex() +→ 201 OK +``` + +### Download (Streaming) + +``` +GET /backups/{name}/download +→ check file exists → set_static_file_info_unsafe() (Crow streams from disk in chunks) +→ Server RAM stays constant (~8 MB) even for 23 GB+ files +``` + +### Upload + +``` +POST /backups/upload (multipart) +→ parse multipart → validate .tar extension + name → check no duplicate → write to disk +→ 201 OK + +NOTE: Upload currently buffers entire file in RAM (Crow multipart parser limitation). +``` + +### Get Backup Info + +``` +GET /backups/{name}/info +→ locate {DATA_DIR}/backups/{username}/{name}.tar +→ read metadata.json from inside .tar (via libarchive, no full extraction) +→ return metadata JSON (original_index, timestamp, size_mb, params) +``` + +--- + +## Safety Checks + +| # | Check | Where | +|---|-------|-------| +| 1 | **One backup per user** — `active_user_backups_` map rejects if user already has active backup | createBackupAsync | +| 2 | **Write blocking** — writes block on `operation_mutex` until backup completes | addVectors, deleteVectors, updateFilters, deleteByFilter, deleteIndex | +| 3 | **Name validation** — alphanumeric, underscores, hyphens only; max 200 chars | validateBackupName | +| 4 | **Duplicate prevention** — checks if .tar file already exists on disk | createBackupAsync, upload | +| 5 | **Disk space** — requires 2x index size available | executeBackupJob | +| 6 | **Atomic tar** — writes to `backups/.tmp/{username}/` first, then renames to final location | executeBackupJob | +| 7 | **Crash recovery** — on startup: `cleanupTempDir()` deletes entire `backups/.tmp/` directory | BackupStore constructor | +| 8 | **Restore safety** — target must not exist, metadata must be valid, cleanup on failure | restoreBackup | diff --git a/install.sh b/install.sh index 93222f67c2..4006056947 100755 --- a/install.sh +++ b/install.sh @@ -197,13 +197,14 @@ distro_factory() { # **************************************** add_frontend() { - log "pulling frontend" + VERSION="v1.1.0" + log "Pulling frontend version ${VERSION}" mkdir -p $script_dir/frontend cd $script_dir/frontend - curl -L -o react-dist.zip https://github.com/EndeeLabs/endee-web-ui/releases/download/v1.0.2/endee-web-ui.zip + curl -fL -o react-dist.zip https://github.com/EndeeLabs/endee-web-ui/releases/download/${VERSION}/dist.zip unzip -o react-dist.zip rm react-dist.zip - log "frontend added" + log "Frontend version ${VERSION} added" } # **************************************** diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index bc779838d5..64acad9707 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -13,7 +13,6 @@ #include "quant_vector.hpp" #include "wal.hpp" #include "../quant/dispatch.hpp" -#include "../utils/archive_utils.hpp" #include #include #include @@ -29,8 +28,6 @@ #include #include -#define MAX_BACKUP_NAME_LENGTH 200 - struct IndexConfig { size_t dim; size_t sparse_dim = 0; // 0 means dense-only @@ -139,6 +136,8 @@ struct PersistenceConfig { bool save_on_shutdown{true}; }; +#include "../storage/backup_store.hpp" + class IndexManager { private: std::deque indices_list_; @@ -156,6 +155,8 @@ class IndexManager { std::atomic running_{true}; // Write-ahead log for each index std::unordered_map> wal_logs_; + BackupStore backup_store_; + void executeBackupJob(const std::string& index_id, const std::string& backup_name); // New methods to handle WAL WriteAheadLog* getOrCreateWAL(const std::string& index_id) { @@ -470,10 +471,9 @@ class IndexManager { const std::string& data_dir, const PersistenceConfig& persistence_config = PersistenceConfig{}) : data_dir_(data_dir), - persistence_config_(persistence_config) { + persistence_config_(persistence_config), + backup_store_(data_dir) { std::filesystem::create_directories(data_dir); - // Create backups directory for default system user - std::filesystem::create_directories(data_dir + "/backups"); metadata_manager_ = std::make_unique(data_dir); // Start the autosave thread autosave_thread_ = std::thread(&IndexManager::autosaveLoop, this); @@ -554,247 +554,7 @@ class IndexManager { return true; } - // Helper method to validate backup names - std::pair validateBackupName(const std::string& backup_name) const { - if(backup_name.empty()) { - return std::make_pair(false, "Backup name cannot be empty"); - } - - // Check length limit (most filesystems limit to 255 chars) - if(backup_name.length() > MAX_BACKUP_NAME_LENGTH) { - return std::make_pair(false, - "Backup name too long (max " - + std::to_string(MAX_BACKUP_NAME_LENGTH) - + " characters)"); - } - - // Use regex to check for alphanumeric, underscores, and hyphens - static const std::regex backup_name_regex("^[a-zA-Z0-9_-]+$"); - if(!std::regex_match(backup_name, backup_name_regex)) { - return std::make_pair(false, - "Invalid backup name: only alphanumeric, underscores, " - "and hyphens allowed"); - } - - return std::make_pair(true, ""); - } - - // Backup methods - std::pair createBackup(const std::string& index_id, - const std::string& backup_name) { - // 1. Validate backup name - std::pair result = validateBackupName(backup_name); - if(!result.first) { - return result; - } - - // 2. Parse user and index name - std::string user_id, index_name; - size_t pos = index_id.find('/'); - if(pos != std::string::npos) { - user_id = index_id.substr(0, pos); - index_name = index_id.substr(pos + 1); - } else { - return {false, "Invalid index ID format"}; - } - - // 3. Get index entry and lock - auto& entry = getIndexEntry(index_id); - std::lock_guard operation_lock(entry.operation_mutex); - - // 4. Force save - saveIndexInternal(entry); - - // 5. Prepare paths - simplified for single-user system - std::string backup_dir_root = data_dir_ + "/backups"; - std::string backup_dir = backup_dir_root + "/" + backup_name; - std::string backup_tar = backup_dir_root + "/" + backup_name + ".tar.gz"; - std::string source_dir = data_dir_ + "/" + index_id; - - if(std::filesystem::exists(backup_tar)) { - return {false, "Backup already exists: " + backup_name}; - } - - // 6. Copy files to temporary directory - std::filesystem::create_directories(backup_dir); - std::filesystem::copy(source_dir, backup_dir, std::filesystem::copy_options::recursive); - - // 7. Calculate uncompressed size and write metadata.json - size_t uncompressed_size = 0; - for(const auto& file : std::filesystem::recursive_directory_iterator(backup_dir)) { - if(!std::filesystem::is_directory(file)) { - uncompressed_size += std::filesystem::file_size(file); - } - } - - auto meta = metadata_manager_->getMetadata(index_id); - if(meta) { - nlohmann::json j; - j["original_index"] = index_name; - j["timestamp"] = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); - j["size_mb"] = uncompressed_size / MB; - j["params"] = {{"M", meta->M}, - {"ef_construction", meta->ef_con}, - {"dim", meta->dimension}, - {"sparse_dim", meta->sparse_dim}, - {"space_type", meta->space_type_str}, - {"quant_level", static_cast(meta->quant_level)}, - {"total_elements", meta->total_elements}, - {"checksum", meta->checksum}}; - - std::ofstream meta_file(backup_dir + "/metadata.json"); - meta_file << j.dump(4); - } - - // 7. Create tar.gz archive from the backup directory using libarchive - std::string error_msg; - if(!ndd::ArchiveUtils::createTarGz(backup_dir, backup_tar, error_msg)) { - // Clean up on failure - std::filesystem::remove_all(backup_dir); - return {false, "Failed to create compressed backup archive: " + error_msg}; - } - - // 8. Remove the temporary uncompressed directory - std::filesystem::remove_all(backup_dir); - - LOG_INFO("Created compressed backup: " << backup_tar); - return {true, ""}; - } - - std::vector listBackups() { - std::vector backups; - std::string backup_dir = data_dir_ + "/backups"; - if(!std::filesystem::exists(backup_dir)) { - return backups; - } - - for(const auto& entry : std::filesystem::directory_iterator(backup_dir)) { - if(entry.is_regular_file() && entry.path().extension() == ".gz") { - std::string filename = entry.path().filename().string(); - - // Check if it's a .tar.gz file - if(filename.size() > 7 && filename.substr(filename.size() - 7) == ".tar.gz") { - // Remove .tar.gz extension to get backup name - std::string backup_name = filename.substr(0, filename.size() - 7); - backups.push_back(backup_name); - } - } - } - return backups; - } - - std::pair restoreBackup(const std::string& backup_name, - const std::string& target_index_name) { - // 1. Validate backup name - std::pair result = validateBackupName(backup_name); - if(!result.first) { - return result; - } - - // Use default username for single-user system - std::string user_id = settings::DEFAULT_USERNAME; - std::string backup_dir_root = data_dir_ + "/backups"; - std::string backup_tar = backup_dir_root + "/" + backup_name + ".tar.gz"; - std::string backup_extract_dir = backup_dir_root + "/" + backup_name; - std::string target_index_id = user_id + "/" + target_index_name; - std::string target_dir = data_dir_ + "/" + target_index_id; - - // 2. Validation - check for tar.gz file - if(!std::filesystem::exists(backup_tar)) { - return {false, "Backup not found: " + backup_name}; - } - if(metadata_manager_->getMetadata(target_index_id).has_value()) { - return {false, "Target index already exists"}; - } - - // 3. Extract tar.gz to temporary directory using libarchive - std::string error_msg; - if(!ndd::ArchiveUtils::extractTarGz(backup_tar, backup_extract_dir, error_msg)) { - return {false, "Failed to extract backup archive: " + error_msg}; - } - - // check if any folder is present in backup_extract_dir - std::vector folders; - for(const auto& entry : std::filesystem::directory_iterator(backup_extract_dir)) { - if(entry.is_directory()) { - folders.push_back(entry.path().string()); - } - } - - if(folders.size() != 1) { - std::filesystem::remove_all(backup_extract_dir); - return {false, "Backup extraction failed - directory not found"}; - } - - std::string backup_dir = folders[0]; - - try { - // 3. Read metadata - std::ifstream f(backup_dir + "/metadata.json"); - if(!f.good()) { - std::filesystem::remove_all(backup_extract_dir); - return {false, "Backup metadata missing"}; - } - nlohmann::json meta_json = nlohmann::json::parse(f); - - // 4. Copy files - std::filesystem::create_directories(target_dir); - std::filesystem::copy(backup_dir, - target_dir, - std::filesystem::copy_options::recursive - | std::filesystem::copy_options::overwrite_existing); - - // Remove metadata.json from the restored index folder as it's not needed there - std::filesystem::remove(target_dir + "/metadata.json"); - - // 5. Register index - IndexMetadata new_meta; - new_meta.name = target_index_name; - new_meta.dimension = meta_json["params"]["dim"]; - new_meta.sparse_dim = meta_json["params"].value("sparse_dim", 0ul); - new_meta.M = meta_json["params"]["M"]; - new_meta.ef_con = meta_json["params"]["ef_construction"]; - new_meta.space_type_str = meta_json["params"]["space_type"]; - new_meta.quant_level = static_cast( - meta_json["params"]["quant_level"].get()); - new_meta.created_at = std::chrono::system_clock::now(); - new_meta.total_elements = meta_json["params"].value("total_elements", 0ul); - new_meta.checksum = meta_json["params"].value("checksum", -1); - - metadata_manager_->storeMetadata(target_index_id, new_meta); - - // 6. Clean up extracted temporary directory - std::filesystem::remove_all(backup_extract_dir); - - // 7. Load index - loadIndex(target_index_id); - - LOG_INFO("Restored backup from compressed archive: " << backup_tar); - return {true, ""}; - } catch(const std::exception& e) { - // Clean up on failure - std::filesystem::remove_all(backup_extract_dir); - return {false, "Failed to restore backup: " + std::string(e.what())}; - } - } - - std::pair deleteBackup(const std::string& backup_name) { - // Validate backup name - std::pair result = validateBackupName(backup_name); - if(!result.first) { - return result; - } - - std::string backup_tar = data_dir_ + "/backups/" + backup_name + ".tar.gz"; - if(std::filesystem::exists(backup_tar)) { - std::filesystem::remove(backup_tar); - LOG_INFO("Deleted compressed backup: " << backup_tar); - return {true, ""}; - } else { - return {false, "Backup not found"}; - } - } bool createIndex(const std::string& index_id, const IndexConfig& config, @@ -1255,6 +1015,10 @@ class IndexManager { PRINT_LOG_TIME(); return true; + } catch(const std::runtime_error& e) { + // Re-throw runtime_error (includes backup-in-progress check) + // so it can be caught by API layer and returned as proper JSON error + throw; } catch(const std::exception& e) { std::cerr << "Batch insertion failed: " << e.what() << std::endl; return false; @@ -1452,6 +1216,9 @@ class IndexManager { } else { return 0; } + } catch(const std::runtime_error& e) { + // Re-throw runtime_error (includes backup-in-progress check) + throw; } catch(const std::exception& e) { std::cerr << "Failed to delete vectors by filter: " << e.what() << std::endl; return 0; @@ -1463,6 +1230,7 @@ class IndexManager { const std::vector>& updates) { try { auto& entry = getIndexEntry(index_id); + std::lock_guard operation_lock(entry.operation_mutex); size_t updated_count = 0; @@ -1482,6 +1250,9 @@ class IndexManager { } return updated_count; + } catch(const std::runtime_error& e) { + // Re-throw runtime_error (includes backup-in-progress check) + throw; } catch(const std::exception& e) { std::cerr << "Failed to update filters: " << e.what() << std::endl; return 0; @@ -1515,6 +1286,9 @@ class IndexManager { } return result; + } catch(const std::runtime_error& e) { + // Re-throw runtime_error (includes backup-in-progress check) + throw; } catch(const std::exception& e) { std::cerr << "Failed to delete vector: " << e.what() << std::endl; return false; @@ -1949,4 +1723,300 @@ class IndexManager { // The calling function already holds operation_mutex // and will call save at appropriate time } + + // ========== Backup operations ========== + + // Orchestration methods (defined below after class) + std::pair createBackupAsync(const std::string& index_id, + const std::string& backup_name); + + std::pair restoreBackup(const std::string& backup_name, + const std::string& target_index_name, + const std::string& username); + + // Forwarding methods (no IndexManager internals needed) + std::vector listBackups(const std::string& username) { + return backup_store_.listBackups(username); + } + + std::pair deleteBackup(const std::string& backup_name, + const std::string& username) { + return backup_store_.deleteBackup(backup_name, username); + } + + std::optional getActiveBackup(const std::string& username) { + return backup_store_.getActiveBackup(username); + } + + nlohmann::json getBackupInfo(const std::string& backup_name, + const std::string& username) { + return backup_store_.getBackupInfo(backup_name, username); + } + + std::pair validateBackupName(const std::string& backup_name) const { + return backup_store_.validateBackupName(backup_name); + } }; + +// ========== IndexManager backup implementations ========== + +inline void IndexManager::executeBackupJob(const std::string& index_id, const std::string& backup_name) { + std::string username; + size_t upos = index_id.find('/'); + if (upos != std::string::npos) { + username = index_id.substr(0, upos); + } + + try { + std::string index_name; + if (upos != std::string::npos) { + index_name = index_id.substr(upos + 1); + } else { + throw std::runtime_error("Invalid index ID format"); + } + + std::string user_backup_dir = backup_store_.getUserBackupDir(username); + std::filesystem::create_directories(user_backup_dir); + std::string user_temp_dir = backup_store_.getUserTempDir(username); + std::filesystem::create_directories(user_temp_dir); + std::string source_dir = data_dir_ + "/" + index_id; + std::string backup_tar_final = user_backup_dir + "/" + backup_name + ".tar"; + std::string backup_tar_temp = user_temp_dir + "/.tmp_" + backup_name + ".tar"; + + if(std::filesystem::exists(backup_tar_final)) { + throw std::runtime_error("Backup already exists: " + backup_name); + } + + size_t index_size = 0; + for(const auto& file : std::filesystem::recursive_directory_iterator(source_dir)) { + if(!std::filesystem::is_directory(file)) { + index_size += std::filesystem::file_size(file); + } + } + + auto space_info = std::filesystem::space(user_backup_dir); + if(space_info.available < index_size * 2) { + throw std::runtime_error("Insufficient disk space: need " + + std::to_string(index_size * 2 / MB) + " MB"); + } + + auto meta = metadata_manager_->getMetadata(index_id); + nlohmann::json metadata_json; + if(meta) { + metadata_json["original_index"] = index_name; + metadata_json["timestamp"] = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + metadata_json["size_mb"] = index_size / MB; + metadata_json["params"] = {{"M", meta->M}, + {"ef_construction", meta->ef_con}, + {"dim", meta->dimension}, + {"sparse_dim", meta->sparse_dim}, + {"space_type", meta->space_type_str}, + {"quant_level", static_cast(meta->quant_level)}, + {"total_elements", meta->total_elements}, + {"checksum", meta->checksum}}; + LOG_DEBUG("Metadata prepared for backup: " << metadata_json.dump()); + } else { + LOG_ERROR("Failed to get metadata for index: " << index_id); + throw std::runtime_error("Cannot create backup without index metadata"); + } + + auto& entry = getIndexEntry(index_id); + std::string metadata_file_in_index = source_dir + "/metadata.json"; + { + std::lock_guard operation_lock(entry.operation_mutex); + + saveIndexInternal(entry); + + if(!metadata_json.empty()) { + std::ofstream meta_file(metadata_file_in_index, std::ios::binary); + if(!meta_file) { + throw std::runtime_error("Failed to create metadata file: " + metadata_file_in_index); + } + meta_file << metadata_json.dump(4); + meta_file.flush(); + meta_file.close(); + + if(!std::filesystem::exists(metadata_file_in_index)) { + throw std::runtime_error("Metadata file was not created: " + metadata_file_in_index); + } + LOG_DEBUG("Metadata file created: " << metadata_file_in_index << " (size: " << std::filesystem::file_size(metadata_file_in_index) << " bytes)"); + } + + std::string error_msg; + LOG_DEBUG("Creating tar archive from " << source_dir << " to " << backup_tar_temp); + if(!backup_store_.createBackupTar(source_dir, backup_tar_temp, error_msg)) { + if(std::filesystem::exists(metadata_file_in_index)) { + std::filesystem::remove(metadata_file_in_index); + } + throw std::runtime_error("Failed to create tar archive: " + error_msg); + } + + if(!std::filesystem::exists(backup_tar_temp)) { + throw std::runtime_error("Tar archive was not created: " + backup_tar_temp); + } + LOG_DEBUG("Tar archive created successfully: " << backup_tar_temp << " (size: " << std::filesystem::file_size(backup_tar_temp) << " bytes)"); + + if(std::filesystem::exists(metadata_file_in_index)) { + std::filesystem::remove(metadata_file_in_index); + } + } + + backup_store_.clearActiveBackup(username); + + LOG_INFO("Backup tar created, write operations now allowed for index: " << index_id); + + std::filesystem::rename(backup_tar_temp, backup_tar_final); + + nlohmann::json backup_db = backup_store_.readBackupJson(username); + backup_db[backup_name] = metadata_json; + backup_store_.writeBackupJson(username, backup_db); + + LOG_INFO("Backup completed: " << backup_name << " -> " << backup_tar_final); + + } catch (const std::exception& e) { + std::string user_backup_dir = backup_store_.getUserBackupDir(username); + std::string user_temp_dir = backup_store_.getUserTempDir(username); + std::string source_dir = data_dir_ + "/" + index_id; + std::string backup_tar_final = user_backup_dir + "/" + backup_name + ".tar"; + std::string backup_tar_temp = user_temp_dir + "/.tmp_" + backup_name + ".tar"; + std::string metadata_file_in_index = source_dir + "/metadata.json"; + + if(std::filesystem::exists(backup_tar_temp)) { + std::filesystem::remove(backup_tar_temp); + } + if(std::filesystem::exists(backup_tar_final)) { + std::filesystem::remove(backup_tar_final); + } + if(std::filesystem::exists(metadata_file_in_index)) { + std::filesystem::remove(metadata_file_in_index); + } + + backup_store_.clearActiveBackup(username); + + LOG_ERROR("Backup failed: " << backup_name << " - " << e.what()); + } +} + +inline std::pair IndexManager::restoreBackup(const std::string& backup_name, + const std::string& target_index_name, + const std::string& username) { + std::pair result = backup_store_.validateBackupName(backup_name); + if(!result.first) { + return result; + } + + std::string backup_dir_root = backup_store_.getUserBackupDir(username); + std::string backup_tar = backup_dir_root + "/" + backup_name + ".tar"; + std::string user_temp_dir = backup_store_.getUserTempDir(username); + std::filesystem::create_directories(user_temp_dir); + std::string backup_extract_dir = user_temp_dir + "/" + backup_name; + std::string target_index_id = username + "/" + target_index_name; + std::string target_dir = data_dir_ + "/" + target_index_id; + + if(!std::filesystem::exists(backup_tar)) { + return {false, "Backup not found: " + backup_name}; + } + + if(metadata_manager_->getMetadata(target_index_id).has_value()) { + return {false, "Target index already exists"}; + } + + std::string error_msg; + if(!backup_store_.extractBackupTar(backup_tar, backup_extract_dir, error_msg)) { + return {false, "Failed to extract backup archive: " + error_msg}; + } + + std::vector folders; + for(const auto& entry : std::filesystem::directory_iterator(backup_extract_dir)) { + if(entry.is_directory()) { + folders.push_back(entry.path().string()); + } + } + + if(folders.size() != 1) { + std::filesystem::remove_all(backup_extract_dir); + return {false, "Backup extraction failed - directory not found"}; + } + + std::string backup_dir = folders[0]; + + try { + std::ifstream f(backup_dir + "/metadata.json"); + if(!f.good()) { + std::filesystem::remove_all(backup_extract_dir); + return {false, "Backup metadata missing"}; + } + nlohmann::json meta_json = nlohmann::json::parse(f); + + std::filesystem::create_directories(target_dir); + std::filesystem::copy(backup_dir, + target_dir, + std::filesystem::copy_options::recursive + | std::filesystem::copy_options::overwrite_existing); + + std::filesystem::remove(target_dir + "/metadata.json"); + + IndexMetadata new_meta; + new_meta.name = target_index_name; + new_meta.dimension = meta_json["params"]["dim"]; + new_meta.sparse_dim = meta_json["params"].value("sparse_dim", 0ul); + new_meta.M = meta_json["params"]["M"]; + new_meta.ef_con = meta_json["params"]["ef_construction"]; + new_meta.space_type_str = meta_json["params"]["space_type"]; + new_meta.quant_level = static_cast( + meta_json["params"]["quant_level"].get()); + new_meta.created_at = std::chrono::system_clock::now(); + new_meta.total_elements = meta_json["params"].value("total_elements", 0ul); + new_meta.checksum = meta_json["params"].value("checksum", -1); + + metadata_manager_->storeMetadata(target_index_id, new_meta); + + std::filesystem::remove_all(backup_extract_dir); + + loadIndex(target_index_id); + + LOG_INFO("Restored backup from compressed archive: " << backup_tar); + return {true, ""}; + } catch(const std::exception& e) { + std::filesystem::remove_all(backup_extract_dir); + return {false, "Failed to restore backup: " + std::string(e.what())}; + } +} + +inline std::pair IndexManager::createBackupAsync(const std::string& index_id, + const std::string& backup_name) { + std::pair result = backup_store_.validateBackupName(backup_name); + if(!result.first) { + return result; + } + + std::string username; + size_t pos = index_id.find('/'); + if (pos != std::string::npos) { + username = index_id.substr(0, pos); + } else { + return {false, "Invalid index ID format"}; + } + + if (backup_store_.hasActiveBackup(username)) { + return {false, "Backup already in progress for user: " + username}; + } + + std::string user_backup_dir = backup_store_.getUserBackupDir(username); + std::filesystem::create_directories(user_backup_dir); + std::string backup_tar = user_backup_dir + "/" + backup_name + ".tar"; + if (std::filesystem::exists(backup_tar)) { + return {false, "Backup already exists: " + backup_name}; + } + + auto& entry = getIndexEntry(index_id); + backup_store_.setActiveBackup(username, index_id, backup_name); + + std::thread([this, index_id, backup_name]() { + executeBackupJob(index_id, backup_name); + }).detach(); + + LOG_INFO("Backup started: " << backup_name << " for index: " << index_id); + + return {true, backup_name}; +} diff --git a/src/main.cpp b/src/main.cpp index a84228f56e..298bf76735 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -416,11 +416,16 @@ int main(int argc, char** argv) { try { std::pair result = - index_manager.createBackup(index_id, backup_name); + index_manager.createBackupAsync(index_id, backup_name); if(!result.first) { return json_error(400, result.second); } - return crow::response(201, "Backup created successfully"); + + // Return 202 Accepted with backup_name as job_id + crow::json::wvalue response; + response["backup_name"] = result.second; + response["status"] = "in_progress"; + return crow::response(202, response.dump()); } catch(const std::exception& e) { return json_error(500, e.what()); } @@ -432,7 +437,7 @@ int main(int argc, char** argv) { .methods("GET"_method)([&index_manager, &app](const crow::request& req) { auto& ctx = app.get_context(req); try { - auto backups = index_manager.listBackups(); + auto backups = index_manager.listBackups(ctx.username); nlohmann::json result_json = backups; crow::response res; res.code = 200; @@ -460,7 +465,7 @@ int main(int argc, char** argv) { try { std::pair result = - index_manager.restoreBackup(backup_name, target_index_name); + index_manager.restoreBackup(backup_name, target_index_name, ctx.username); if(!result.first) { return json_error(400, result.second); } @@ -477,7 +482,7 @@ int main(int argc, char** argv) { const std::string& backup_name) { auto& ctx = app.get_context(req); try { - std::pair result = index_manager.deleteBackup(backup_name); + std::pair result = index_manager.deleteBackup(backup_name, ctx.username); if(!result.first) { return json_error(400, result.second); } @@ -487,29 +492,32 @@ int main(int argc, char** argv) { } }); - // Download Backup + // Download Backup - accepts auth token via query param or works without auth if disabled CROW_ROUTE(app, "/api/v1/backups//download") - .CROW_MIDDLEWARES(app, AuthMiddleware) - .methods("GET"_method)([&index_manager, &app](const crow::request& req, - const std::string& backup_name) { - auto& ctx = app.get_context(req); + .methods("GET"_method)([&](const crow::request& req, const std::string& backup_name) { try { - std::string backup_tar = - settings::DATA_DIR + "/backups/" + backup_name + ".tar.gz"; - if(!std::filesystem::exists(backup_tar)) { - return json_error(404, "Backup not found"); + if(settings::AUTH_ENABLED) { + std::string token = + req.url_params.get("token") ? req.url_params.get("token") : ""; + if(token != settings::AUTH_TOKEN) { + return json_error(401, "Unauthorized"); + } } - std::string file_content = read_file(backup_tar); - if(file_content.empty()) { - return json_error(500, "Failed to read backup file"); + + std::string backup_file = + settings::DATA_DIR + "/backups/" + settings::DEFAULT_USERNAME + "/" + backup_name + ".tar"; + + if(!std::filesystem::exists(backup_file)) { + return json_error(404, "Backup not found"); } + + crow::response response; - response.set_header("Content-Type", "application/gzip"); + response.set_static_file_info_unsafe(backup_file); + response.set_header("Content-Type", "application/x-tar"); response.set_header("Content-Disposition", - "attachment; filename=\"" + backup_name + ".tar.gz\""); - response.set_header("Content-Length", std::to_string(file_content.size())); + "attachment; filename=\"" + backup_name + ".tar\""); response.set_header("Cache-Control", "no-cache"); - response.body = std::move(file_content); return response; } catch(const std::exception& e) { return json_error(500, e.what()); @@ -539,11 +547,11 @@ int main(int argc, char** argv) { // Get filename from Content-Disposition if(content_disposition.params.count("filename")) { backup_name = content_disposition.params.at("filename"); - // check if backup name ends with .tar.gz - if(backup_name.ends_with(".tar.gz")) { - backup_name = backup_name.substr(0, backup_name.size() - 7); + // check if backup name ends with .tar + if(backup_name.ends_with(".tar")) { + backup_name = backup_name.substr(0, backup_name.size() - 4); } else { - return json_error(400, "Invalid backup file extension"); + return json_error(400, "Invalid backup file extension. Expected .tar file"); } } file_content = part.body; @@ -567,8 +575,9 @@ int main(int argc, char** argv) { } // Check if backup already exists - std::string backup_path = - settings::DATA_DIR + "/backups/" + backup_name + ".tar.gz"; + std::string user_backup_dir = settings::DATA_DIR + "/backups/" + ctx.username; + std::filesystem::create_directories(user_backup_dir); + std::string backup_path = user_backup_dir + "/" + backup_name + ".tar"; if(std::filesystem::exists(backup_path)) { return json_error(409, "Backup with name '" + backup_name + "' already exists"); @@ -594,6 +603,48 @@ int main(int argc, char** argv) { } }); + // Get active backup status for current user + CROW_ROUTE(app, "/api/v1/backups/active") + .CROW_MIDDLEWARES(app, AuthMiddleware) + .methods("GET"_method)([&index_manager, &app](const crow::request& req) { + auto& ctx = app.get_context(req); + try { + auto active = index_manager.getActiveBackup(ctx.username); + crow::json::wvalue response; + if (active) { + response["active"] = true; + response["backup_name"] = active->backup_name; + response["index_id"] = active->index_id; + } else { + response["active"] = false; + } + return crow::response(200, response.dump()); + } catch(const std::exception& e) { + return json_error(500, e.what()); + } + }); + + // Get backup info + CROW_ROUTE(app, "/api/v1/backups//info") + .CROW_MIDDLEWARES(app, AuthMiddleware) + .methods("GET"_method)([&index_manager, &app](const crow::request& req, + const std::string& backup_name) { + auto& ctx = app.get_context(req); + try { + auto info = index_manager.getBackupInfo(backup_name, ctx.username); + if (info.empty()) { + return json_error(404, "Backup not found or metadata missing"); + } + crow::response res; + res.code = 200; + res.set_header("Content-Type", "application/json"); + res.body = info.dump(); + return res; + } catch(const std::exception& e) { + return json_error(500, e.what()); + } + }); + // List indexes for current user CROW_ROUTE(app, "/api/v1/index/list") .CROW_MIDDLEWARES(app, AuthMiddleware) @@ -629,19 +680,27 @@ int main(int argc, char** argv) { // Delete index CROW_ROUTE(app, "/api/v1/index//delete") .CROW_MIDDLEWARES(app, AuthMiddleware) - .methods("DELETE"_method)( - [&index_manager, &app](const crow::request& req, std::string index_name) { - auto& ctx = app.get_context(req); + .methods("DELETE"_method)([&index_manager, &app](const crow::request& req, + std::string index_name) { + auto& ctx = app.get_context(req); - // Format full index_id - std::string index_id = ctx.username + "/" + index_name; + // Format full index_id + std::string index_id = ctx.username + "/" + index_name; - if(index_manager.deleteIndex(index_id)) { - return crow::response(200, "Index deleted successfully"); - } else { - return json_error(404, "Index not found"); - } - }); + try { + if(index_manager.deleteIndex(index_id)) { + return crow::response(200, "Index deleted successfully"); + } else { + return json_error(404, "Index not found"); + } + } catch(const std::runtime_error& e) { + return json_error(400, e.what()); + } catch(const std::exception& e) { + return json_error_500(ctx.username, + req.url, + std::string("Failed to delete index: ") + e.what()); + } + }); // Search CROW_ROUTE(app, "/api/v1/index//search") @@ -1002,6 +1061,8 @@ int main(int argc, char** argv) { size_t count = index_manager.updateFilters(index_id, updates); return crow::response(200, std::to_string(count) + " filters updated"); + } catch(const std::runtime_error& e) { + return json_error(400, e.what()); } catch(const std::exception& e) { return json_error_500(ctx.username, req.url, diff --git a/src/storage/backup_store.hpp b/src/storage/backup_store.hpp new file mode 100644 index 0000000000..eeee973a26 --- /dev/null +++ b/src/storage/backup_store.hpp @@ -0,0 +1,284 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include +#include + +#include "json/nlohmann_json.hpp" +#include "index_meta.hpp" +#include "settings.hpp" +#include "log.hpp" + +struct ActiveBackup { + std::string index_id; + std::string backup_name; +}; + +class BackupStore { +private: + std::string data_dir_; + std::unordered_map active_user_backups_; + mutable std::mutex backup_state_mutex_; + +public: + BackupStore(const std::string& data_dir) + : data_dir_(data_dir) { + std::filesystem::create_directories(data_dir + "/backups"); + cleanupTempDir(); + } + + // Archive methods + + bool createBackupTar(const std::filesystem::path& source_dir, + const std::filesystem::path& archive_path, + std::string& error_msg) { + struct archive* a = archive_write_new(); + archive_write_set_format_pax_restricted(a); + + if(archive_write_open_filename(a, archive_path.string().c_str()) != ARCHIVE_OK) { + error_msg = archive_error_string(a); + archive_write_free(a); + return false; + } + + for(const auto& entry : std::filesystem::recursive_directory_iterator(source_dir)) { + if(entry.is_regular_file()) { + struct archive_entry* e = archive_entry_new(); + + std::filesystem::path rel_path = + std::filesystem::relative(entry.path(), source_dir.parent_path()); + archive_entry_set_pathname(e, rel_path.string().c_str()); + archive_entry_set_size(e, std::filesystem::file_size(entry.path())); + archive_entry_set_filetype(e, AE_IFREG); + archive_entry_set_perm(e, 0644); + + if(archive_write_header(a, e) != ARCHIVE_OK) { + error_msg = archive_error_string(a); + archive_entry_free(e); + archive_write_free(a); + return false; + } + + std::ifstream file(entry.path(), std::ios::binary); + char buffer[8192]; + while(file.read(buffer, sizeof(buffer)) || file.gcount() > 0) { + archive_write_data(a, buffer, file.gcount()); + } + file.close(); + archive_entry_free(e); + } + } + + archive_write_close(a); + archive_write_free(a); + return true; + } + + bool extractBackupTar(const std::filesystem::path& archive_path, + const std::filesystem::path& dest_dir, + std::string& error_msg) { + struct archive* a = archive_read_new(); + struct archive* ext = archive_write_disk_new(); + struct archive_entry* entry; + + archive_read_support_format_all(a); + archive_read_support_filter_all(a); + archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM); + archive_write_disk_set_standard_lookup(ext); + + if(archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK) { + error_msg = archive_error_string(a); + archive_read_free(a); + archive_write_free(ext); + return false; + } + + while(archive_read_next_header(a, &entry) == ARCHIVE_OK) { + std::filesystem::path full_path = dest_dir / archive_entry_pathname(entry); + archive_entry_set_pathname(entry, full_path.string().c_str()); + + if(archive_write_header(ext, entry) == ARCHIVE_OK) { + const void* buff; + size_t size; + la_int64_t offset; + + while(archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK) { + archive_write_data_block(ext, buff, size, offset); + } + } + archive_write_finish_entry(ext); + } + + archive_read_close(a); + archive_read_free(a); + archive_write_close(ext); + archive_write_free(ext); + return true; + } + + // Path helpers + + std::string getUserBackupDir(const std::string& username) const { + return data_dir_ + "/backups/" + username; + } + + std::string getBackupJsonPath(const std::string& username) const { + return getUserBackupDir(username) + "/backup.json"; + } + + std::string getUserTempDir(const std::string& username) const { + return data_dir_ + "/backups/.tmp/" + username; + } + + // Backup JSON helpers + + nlohmann::json readBackupJson(const std::string& username) { + std::string path = getBackupJsonPath(username); + if (!std::filesystem::exists(path)) return nlohmann::json::object(); + try { + std::ifstream f(path); + return nlohmann::json::parse(f); + } catch (...) { + return nlohmann::json::object(); + } + } + + void writeBackupJson(const std::string& username, const nlohmann::json& data) { + std::string path = getBackupJsonPath(username); + std::ofstream f(path); + f << data.dump(2); + } + + // Temp directory cleanup + + void cleanupTempDir() { + std::string temp_dir = data_dir_ + "/backups/.tmp"; + if (std::filesystem::exists(temp_dir)) { + try { + std::filesystem::remove_all(temp_dir); + LOG_INFO("Cleaned up backup temp directory"); + } catch (const std::exception& e) { + LOG_ERROR("Failed to cleanup backup temp directory: " << e.what()); + } + } + } + + // Active backup tracking + + void setActiveBackup(const std::string& username, const std::string& index_id, const std::string& backup_name) { + std::lock_guard lock(backup_state_mutex_); + active_user_backups_[username] = {index_id, backup_name}; + } + + void clearActiveBackup(const std::string& username) { + std::lock_guard lock(backup_state_mutex_); + active_user_backups_.erase(username); + } + + bool hasActiveBackup(const std::string& username) const { + std::lock_guard lock(backup_state_mutex_); + return active_user_backups_.count(username) > 0; + } + + // Backup name validation + + std::pair validateBackupName(const std::string& backup_name) const { + if(backup_name.empty()) { + return std::make_pair(false, "Backup name cannot be empty"); + } + + if(backup_name.length() > settings::MAX_BACKUP_NAME_LENGTH) { + return std::make_pair(false, + "Backup name too long (max " + + std::to_string(settings::MAX_BACKUP_NAME_LENGTH) + + " characters)"); + } + + static const std::regex backup_name_regex("^[a-zA-Z0-9_-]+$"); + if(!std::regex_match(backup_name, backup_name_regex)) { + return std::make_pair(false, + "Invalid backup name: only alphanumeric, underscores, " + "and hyphens allowed"); + } + + return std::make_pair(true, ""); + } + + // Backup listing + + std::vector listBackups(const std::string& username) { + std::vector backups; + std::string backup_dir = getUserBackupDir(username); + + if(!std::filesystem::exists(backup_dir)) { + return backups; + } + + for(const auto& entry : std::filesystem::directory_iterator(backup_dir)) { + if(entry.is_regular_file()) { + std::string filename = entry.path().filename().string(); + + if(filename.size() > 4 && filename.substr(filename.size() - 4) == ".tar" && + !filename.starts_with(".tmp_")) { + std::string backup_name = filename.substr(0, filename.size() - 4); + backups.push_back(backup_name); + } + } + } + return backups; + } + + // Backup deletion + + std::pair deleteBackup(const std::string& backup_name, + const std::string& username) { + std::pair result = validateBackupName(backup_name); + if(!result.first) { + return result; + } + + std::string backup_tar = getUserBackupDir(username) + "/" + backup_name + ".tar"; + + if(std::filesystem::exists(backup_tar)) { + std::filesystem::remove(backup_tar); + + nlohmann::json backup_db = readBackupJson(username); + backup_db.erase(backup_name); + writeBackupJson(username, backup_db); + + LOG_INFO("Deleted backup: " << backup_tar); + return {true, ""}; + } else { + return {false, "Backup not found"}; + } + } + + // Active backup query + + std::optional getActiveBackup(const std::string& username) { + std::lock_guard lock(backup_state_mutex_); + auto it = active_user_backups_.find(username); + if (it != active_user_backups_.end()) return it->second; + return std::nullopt; + } + + // Backup info + + nlohmann::json getBackupInfo(const std::string& backup_name, const std::string& username) { + nlohmann::json backup_db = readBackupJson(username); + if (backup_db.contains(backup_name)) { + return backup_db[backup_name]; + } + return nlohmann::json(); + } +}; diff --git a/src/utils/archive_utils.hpp b/src/utils/archive_utils.hpp deleted file mode 100644 index f92f5def89..0000000000 --- a/src/utils/archive_utils.hpp +++ /dev/null @@ -1,106 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include - -namespace ndd { - - class ArchiveUtils { - public: - // Create tar.gz archive from a directory - static bool createTarGz(const std::filesystem::path& source_dir, - const std::filesystem::path& archive_path, - std::string& error_msg) { - struct archive* a = archive_write_new(); - archive_write_add_filter_gzip(a); - archive_write_set_format_pax_restricted(a); - - if(archive_write_open_filename(a, archive_path.string().c_str()) != ARCHIVE_OK) { - error_msg = archive_error_string(a); - archive_write_free(a); - return false; - } - - // Recursively add all files - for(const auto& entry : std::filesystem::recursive_directory_iterator(source_dir)) { - if(entry.is_regular_file()) { - struct archive_entry* e = archive_entry_new(); - - // Calculate relative path for archive - std::filesystem::path rel_path = - std::filesystem::relative(entry.path(), source_dir.parent_path()); - archive_entry_set_pathname(e, rel_path.string().c_str()); - archive_entry_set_size(e, std::filesystem::file_size(entry.path())); - archive_entry_set_filetype(e, AE_IFREG); - archive_entry_set_perm(e, 0644); - - if(archive_write_header(a, e) != ARCHIVE_OK) { - error_msg = archive_error_string(a); - archive_entry_free(e); - archive_write_free(a); - return false; - } - - // Write file content - std::ifstream file(entry.path(), std::ios::binary); - char buffer[8192]; - while(file.read(buffer, sizeof(buffer)) || file.gcount() > 0) { - archive_write_data(a, buffer, file.gcount()); - } - file.close(); - archive_entry_free(e); - } - } - - archive_write_close(a); - archive_write_free(a); - return true; - } - - // Extract tar.gz archive to a directory - static bool extractTarGz(const std::filesystem::path& archive_path, - const std::filesystem::path& dest_dir, - std::string& error_msg) { - struct archive* a = archive_read_new(); - struct archive* ext = archive_write_disk_new(); - struct archive_entry* entry; - - archive_read_support_format_all(a); - archive_read_support_filter_all(a); - archive_write_disk_set_options(ext, ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM); - archive_write_disk_set_standard_lookup(ext); - - if(archive_read_open_filename(a, archive_path.string().c_str(), 10240) != ARCHIVE_OK) { - error_msg = archive_error_string(a); - archive_read_free(a); - archive_write_free(ext); - return false; - } - - while(archive_read_next_header(a, &entry) == ARCHIVE_OK) { - std::filesystem::path full_path = dest_dir / archive_entry_pathname(entry); - archive_entry_set_pathname(entry, full_path.string().c_str()); - - if(archive_write_header(ext, entry) == ARCHIVE_OK) { - const void* buff; - size_t size; - la_int64_t offset; - - while(archive_read_data_block(a, &buff, &size, &offset) == ARCHIVE_OK) { - archive_write_data_block(ext, buff, size, offset); - } - } - archive_write_finish_entry(ext); - } - - archive_read_close(a); - archive_read_free(a); - archive_write_close(ext); - archive_write_free(ext); - return true; - } - }; - -} // namespace ndd From e81036a5d26fd454fb7c60e8ae32b8bf608dff1a Mon Sep 17 00:00:00 2001 From: rajesh33411 Date: Mon, 9 Mar 2026 12:26:28 +0530 Subject: [PATCH 28/48] fix:wal vector_add/vector_update (#65) Co-authored-by: rajeshkomaravelli --- src/core/ndd.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 64acad9707..61a49ea576 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -1680,7 +1680,7 @@ class IndexManager { entries.reserve(numeric_ids.size()); for(size_t i = 0; i < numeric_ids.size(); i++) { - if(numeric_ids[i].first) { + if(numeric_ids[i].second) { entries.push_back({ WALOperationType::VECTOR_ADD, numeric_ids[i].first, From 7a4cc35d000a8677a5a0167bb1348de60e662f5c Mon Sep 17 00:00:00 2001 From: hemant-endee Date: Mon, 9 Mar 2026 14:25:13 +0530 Subject: [PATCH 29/48] Acquire per-index operation_mutex in deleteIndex (#64) * Acquire per-index operation_mutex in deleteIndex to prevent race with concurrent writes * Acquire per-index operation_mutex in deleteIndex --- src/core/ndd.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 61a49ea576..f9fb2ceeb5 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -1614,6 +1614,8 @@ class IndexManager { // Remove from in-memory structures if loaded auto it = indices_.find(index_id); if(it != indices_.end()) { + std::lock_guard operation_lock(it->second.operation_mutex); + auto indx_it = std::find(indices_list_.begin(), indices_list_.end(), index_id); if(indx_it != indices_list_.end()) { indices_list_.erase(indx_it); From 28a2e108aac52b89a658fb66be1f9d51c300d4e8 Mon Sep 17 00:00:00 2001 From: shaleenji Date: Tue, 10 Mar 2026 09:12:10 +0000 Subject: [PATCH 30/48] cleanup comments from old version (#70) Co-authored-by: Shaleen Garg --- src/core/ndd.hpp | 2 +- src/filter/filter.hpp | 9 ++++----- src/main.cpp | 9 +++------ src/server/auth.hpp | 12 ++++++------ src/storage/id_mapper.hpp | 8 ++++---- src/storage/index_meta.hpp | 4 ++-- src/storage/vector_storage.hpp | 4 ++-- src/utils/settings.hpp | 4 ++-- 8 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index f9fb2ceeb5..c23f1f2b51 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -46,7 +46,7 @@ struct IndexInfo { size_t sparse_dim; std::string space_type_str; ndd::quant::QuantizationLevel - quant_level; // Quantization level (8, 15, 16, 32) - replaces use_fp16 + quant_level; // Selected quantization level int32_t checksum; size_t M; size_t ef_con; diff --git a/src/filter/filter.hpp b/src/filter/filter.hpp index 35bc1b5bce..a44cbb913e 100644 --- a/src/filter/filter.hpp +++ b/src/filter/filter.hpp @@ -120,14 +120,13 @@ class Filter { // max DBs to allow multiple databases (main + schema + numeric_forward + numeric_inverted) mdbx_env_set_maxdbs(env_, 10); - // Set - // Set geometry for auto-grow: initial per settings, growth=256MB, max=32GB + // Set geometry for auto-grow using the filter map size settings rc = mdbx_env_set_geometry( env_, -1, // lower size bound (use default) 1ULL << settings::FILTER_MAP_SIZE_BITS, // current/now size - 1ULL << settings::FILTER_MAP_SIZE_MAX_BITS, // upper size bound (32GB) - 1ULL << settings::FILTER_MAP_SIZE_BITS, // growth step (256MB) + 1ULL << settings::FILTER_MAP_SIZE_MAX_BITS, // upper size bound + 1ULL << settings::FILTER_MAP_SIZE_BITS, // growth step -1, // shrink threshold (use default) -1); // pagesize (use default) if(rc != MDBX_SUCCESS) { @@ -604,4 +603,4 @@ class Filter { } return false; } -}; \ No newline at end of file +}; diff --git a/src/main.cpp b/src/main.cpp index 298bf76735..dd001bd353 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -40,9 +40,6 @@ using ndd::quant::quantLevelToString; using ndd::quant::stringToQuantLevel; -// Global CPU feature flags (initialized once at startup) -// Note: No longer needed since VNNI INT16 is part of standard AVX512-VNNI - // Authentication middleware for open-source mode // If NDD_AUTH_TOKEN is set: token is required // If NDD_AUTH_TOKEN is not set: all requests are allowed @@ -57,7 +54,7 @@ struct AuthMiddleware : crow::ILocalMiddleware { }; void before_handle(crow::request& req, crow::response& res, context& ctx) { - ctx.username = settings::DEFAULT_USERNAME; // Always "default" + ctx.username = settings::DEFAULT_USERNAME; // Single configured username in OSS mode if(!settings::AUTH_ENABLED) { return; // No auth required - open mode @@ -239,7 +236,7 @@ int main(int argc, char** argv) { }); // ========= USER ENDPOINTS ========== - // Get user info - returns default user info + // Get user info for the configured single user CROW_ROUTE(app, "/api/v1/users//info") .CROW_MIDDLEWARES(app, AuthMiddleware) .methods("GET"_method)([&auth_manager, &app](const crow::request& req, @@ -389,7 +386,7 @@ int main(int argc, char** argv) { checksum}; try { - // Pass the full index_id to index_manager with Admin user type (no limits) + // Pass the full index_id to index_manager using the Admin user type index_manager.createIndex(index_id, config, UserType::Admin, size_in_millions); return crow::response(200, "Index created successfully"); } catch(const std::runtime_error& e) { diff --git a/src/server/auth.hpp b/src/server/auth.hpp index 0d6cdd4f5b..56ccc8ccff 100644 --- a/src/server/auth.hpp +++ b/src/server/auth.hpp @@ -25,7 +25,7 @@ inline int getMaxAllowedIndices(UserType type) { return settings::MAX_ACTIVE_INDICES; } -// Get max vectors per index - No limits in open-source mode +// Get max vectors per index for the single Admin user inline size_t getMaxVectorsPerIndex(UserType type) { return settings::MAX_VECTORS_ADMIN; // 1 billion vectors } @@ -63,7 +63,7 @@ struct User { * 1. If NDD_AUTH_TOKEN is NOT set: All APIs work without authentication * 2. If NDD_AUTH_TOKEN is set: Token is required in Authorization header * - * All operations use a single "default" user with Admin privileges (no limits). + * All operations use a single configured user with Admin privileges. */ class AuthManager { private: @@ -72,7 +72,7 @@ class AuthManager { public: AuthManager(const std::string& base_dir) : base_dir_(base_dir) { - // Create default user directory + // Create the configured single-user directory std::filesystem::path default_user_dir = std::filesystem::path(base_dir) / settings::DEFAULT_USERNAME; std::filesystem::create_directories(default_user_dir); @@ -91,10 +91,10 @@ class AuthManager { * Validate the provided token. * * @param provided_token The token from the Authorization header - * @return The username ("default") if valid, empty string if invalid + * @return The configured username if valid, empty string if invalid */ std::string validateToken(const std::string& provided_token) { - // If auth is disabled, always return default user + // If auth is disabled, always return the configured single user if(!settings::AUTH_ENABLED) { return settings::DEFAULT_USERNAME; } @@ -113,7 +113,7 @@ class AuthManager { std::optional getUserType(const std::string& username) { return UserType::Admin; } /** - * Get user info - returns the default user. + * Get user info for the configured single user. */ std::optional getUser(const std::string& username) { return User{settings::DEFAULT_USERNAME, diff --git a/src/storage/id_mapper.hpp b/src/storage/id_mapper.hpp index eff79b30e1..cda3109702 100644 --- a/src/storage/id_mapper.hpp +++ b/src/storage/id_mapper.hpp @@ -34,9 +34,9 @@ class IDMapper { rc = mdbx_env_set_geometry( env_, -1, // lower size bound (use default) - 1ULL << settings::ID_MAPPER_MAP_SIZE_BITS, // current/now size (512MB) - 1ULL << settings::ID_MAPPER_MAP_SIZE_MAX_BITS, // upper size bound (16GB) - 1ULL << settings::ID_MAPPER_MAP_SIZE_BITS, // growth step (256MB) + 1ULL << settings::ID_MAPPER_MAP_SIZE_BITS, // current/now size + 1ULL << settings::ID_MAPPER_MAP_SIZE_MAX_BITS, // upper size bound + 1ULL << settings::ID_MAPPER_MAP_SIZE_BITS, // growth step -1, // shrink threshold (use default) -1); // pagesize (use default) if(rc != MDBX_SUCCESS) { @@ -632,4 +632,4 @@ class IDMapper { }; inline const std::string IDMapper::NEXT_ID_KEY = "__next_id_px7b39lw__"; -inline const std::string IDMapper::DELETED_IDS_KEY = "__deleted_ids_px7b39lw__"; \ No newline at end of file +inline const std::string IDMapper::DELETED_IDS_KEY = "__deleted_ids_px7b39lw__"; diff --git a/src/storage/index_meta.hpp b/src/storage/index_meta.hpp index ed90ac4289..7858630837 100644 --- a/src/storage/index_meta.hpp +++ b/src/storage/index_meta.hpp @@ -18,7 +18,7 @@ struct IndexMetadata { size_t sparse_dim = 0; // Added sparse dimension std::string space_type_str; ndd::quant::QuantizationLevel quant_level = - ndd::quant::QuantizationLevel::INT8; // Quantization level (8, 15, 16, 32) + ndd::quant::QuantizationLevel::INT8; // Selected quantization level int32_t checksum; size_t total_elements; size_t M; @@ -368,4 +368,4 @@ class MetadataManager { + mdbx_strerror(rc)); } } -}; \ No newline at end of file +}; diff --git a/src/storage/vector_storage.hpp b/src/storage/vector_storage.hpp index c24c95e485..b410b7ff33 100644 --- a/src/storage/vector_storage.hpp +++ b/src/storage/vector_storage.hpp @@ -30,7 +30,7 @@ class VectorStore { throw std::runtime_error("Failed to create LMDB env"); } - // Set geometry for auto-grow: initial=8GB, growth=1GB, max=128GB + // Set geometry for auto-grow using the vector map size settings rc = mdbx_env_set_geometry(env_, -1, // lower size bound (use default) 1ULL << settings::VECTOR_MAP_SIZE_BITS, // current/now size @@ -775,4 +775,4 @@ class VectorStorage { ndd::quant::QuantizationLevel getQuantLevel() const { return vector_store_->getQuantLevel(); } size_t dimension() const { return vector_store_->dimension(); } size_t get_vector_size() const { return vector_store_->get_vector_size(); } -}; \ No newline at end of file +}; diff --git a/src/utils/settings.hpp b/src/utils/settings.hpp index 9fb975389d..cce94bdd8c 100644 --- a/src/utils/settings.hpp +++ b/src/utils/settings.hpp @@ -28,7 +28,7 @@ namespace settings { constexpr size_t MAX_M = 512; constexpr size_t DEFAULT_EF_CONSTRUCT = 128; constexpr size_t MIN_EF_CONSTRUCT = 8; - constexpr size_t BACKFILL_BUFFER = 4; // Keep 3 slots free for high quality neighbors + constexpr size_t BACKFILL_BUFFER = 4; // Keep 4 slots free for high quality neighbors constexpr size_t MAX_EF_CONSTRUCT = 4096; constexpr size_t DEFAULT_EF_SEARCH = 128; constexpr size_t MIN_K = 1; @@ -204,4 +204,4 @@ namespace settings { return oss.str(); } -} //namespace settings \ No newline at end of file +} //namespace settings From 52fe9f07347e0f25918d41b625404e31b4a4f9cf Mon Sep 17 00:00:00 2001 From: rajesh33411 Date: Tue, 10 Mar 2026 16:21:46 +0530 Subject: [PATCH 31/48] fix:sve2 instruction compile error (#69) * fix:sve2 instruction compile error * changing from size_t to svptrue --------- Co-authored-by: rajeshkomaravelli --- src/quant/float16.hpp | 4 ++-- src/quant/float32.hpp | 2 +- src/quant/int16.hpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/quant/float16.hpp b/src/quant/float16.hpp index a26f9c9306..5ff1c6b440 100644 --- a/src/quant/float16.hpp +++ b/src/quant/float16.hpp @@ -914,8 +914,8 @@ namespace ndd { #elif defined(USE_SVE2) size_t lane = svcnth(); for(; d + lane <= block_len; d += lane) { - svbool_t pg16 = svwhilelt_b16(0, lane); - svbool_t pg32 = svwhilelt_b32(0, lane); + svbool_t pg16 = svptrue_b16(); + svbool_t pg32 = svptrue_b32();; svfloat16_t q_h = svld1_f16(pg16, reinterpret_cast(q_ptr + d)); svfloat16_t v_h = svld1_f16(pg16, reinterpret_cast(v_ptr + d)); svfloat32_t qv = svcvt_f32_f16_x(pg32, q_h); diff --git a/src/quant/float32.hpp b/src/quant/float32.hpp index 2eb9f5547f..a0553380a8 100644 --- a/src/quant/float32.hpp +++ b/src/quant/float32.hpp @@ -582,7 +582,7 @@ namespace hnswlib { #elif defined(USE_SVE2) size_t lane = svcntw(); for(; d + lane <= block_len; d += lane) { - svbool_t pg = svwhilelt_b32(0, lane); + svbool_t pg = svptrue_b32();; svfloat32_t qv = svld1_f32(pg, q_ptr + d); svfloat32_t vv = svld1_f32(pg, v_ptr + d); dot += svaddv_f32(pg, svmul_f32_x(pg, qv, vv)); diff --git a/src/quant/int16.hpp b/src/quant/int16.hpp index 26531a0085..a80cf8342f 100644 --- a/src/quant/int16.hpp +++ b/src/quant/int16.hpp @@ -1203,7 +1203,7 @@ namespace ndd { #elif defined(USE_SVE2) const size_t vec_lanes = svcntw(); for(; d + vec_lanes <= block_len; d += vec_lanes) { - svbool_t pg = svwhilelt_b32(0, vec_lanes); + svbool_t pg = svptrue_b32(); svint32_t q_i32 = svld1sh_s32(pg, query_vec + block_start + d); svint32_t v_i32 = svld1sh_s32(pg, vec + block_start + d); svint32_t dot_prod = svmul_s32_x(pg, q_i32, v_i32); From e5aee4080f44a4b8615d681861f94cb0f715b951 Mon Sep 17 00:00:00 2001 From: shaleenji Date: Tue, 10 Mar 2026 10:58:11 +0000 Subject: [PATCH 32/48] Standardize Logs (#71) --- docs/logs.md | 96 ++++++++++ src/core/ndd.hpp | 124 +++++++------ src/filter/filter.hpp | 35 ++-- src/hnsw/hnswalg.h | 2 +- src/main.cpp | 166 +++++++++++++----- src/server/auth.hpp | 6 +- src/storage/backup_store.hpp | 11 +- src/storage/index_meta.hpp | 57 +++--- src/storage/vector_storage.hpp | 27 ++- src/storage/wal.hpp | 9 +- .../cpu_compat_check/check_arm_compat.hpp | 13 +- .../cpu_compat_check/check_avx_compat.hpp | 59 +++---- src/utils/log.hpp | 100 +++++++++-- 13 files changed, 501 insertions(+), 204 deletions(-) create mode 100644 docs/logs.md diff --git a/docs/logs.md b/docs/logs.md new file mode 100644 index 0000000000..4105f39265 --- /dev/null +++ b/docs/logs.md @@ -0,0 +1,96 @@ +# Logs + +## Format + +All production logs must go through `LOG_INFO`, `LOG_WARN`, or `LOG_ERROR` from [src/utils/log.hpp](../src/utils/log.hpp). + +The emitted format is: + +```text +LEVEL_code: username/index_name: message +LEVEL: username/index_name: message +``` + +- `LEVEL` is one of `INFO`, `WARN`, or `ERROR`. +- `code` is an explicit numeric message code when present. +- `username/index_name` identifies the user and index tied to the event. +- If a log is not tied to a specific index, placeholders are used: + - global log: `-/-` + - user-only log: `username/-` + +Examples: + +```text +INFO_2023: alice/catalog: Starting reload +WARN_1035: alice/catalog: Invalid k: 0 +ERROR_1502: alice/catalog: Failed to store metadata: MDBX_MAP_FULL +INFO: -/-: Starting the server +WARN_1064: alice/-: Backup-info request for missing backup nightly +INFO_1703: -/-: NEON is supported and usable +``` + +## How To Call Logs + +The same macros are used everywhere. The implementation decides how to build context from the arguments: + +```cpp +LOG_INFO("Starting the server"); +LOG_WARN(1058, username, "Backup download requested for missing backup " << backup_name); +LOG_ERROR(2039, index_id, "Search failed: " << e.what()); +LOG_INFO(2045, username, index_name, "Restored backup from " << backup_tar); +``` + +Supported forms: + +```cpp +LOG_INFO(message) +LOG_INFO(code, message) +LOG_INFO(code, context, message) +LOG_INFO(code, username, index_name, message) +``` + +The same overload shapes apply to `LOG_WARN` and `LOG_ERROR`. + +### Context resolution + +- `LOG_*(message)` + - Global log. + - Context becomes `-/-`. + - No numeric code is emitted. +- `LOG_*(code, message)` + - Global log with explicit code. +- `LOG_*(code, context, message)` + - If `context` contains `/`, it is treated as `index_id` and split into `username/index_name`. + - Otherwise it is treated as `username` and becomes `username/-`. +- `LOG_*(code, username, index_name, message)` + - Uses the explicit username and index name directly. + +## Rules + +- Explicit numeric codes are preferred for stable operational logs. +- Code-less logs are valid and must never receive synthesized IDs. +- Prefer logging at request boundaries, lifecycle transitions, and rare failure paths. +- Do not add logs in hot loops or per-vector/per-result paths. +- Replace `std::cerr` with `LOG_*` so the output format stays consistent. +- For index-scoped code, prefer passing `index_id` and logging with `LOG_*(code, index_id, ...)`. +- Keep messages short and problem-oriented. Include values that help debug the failure. + +## Message Code Guidance + +- Reuse the existing local code range in the file you are editing when extending current behavior. +- Keep related code ranges grouped by subsystem when practical: + - `1000s` request/server logs + - `1200s` filter logs + - `1300s` backup store logs + - `1400s` WAL logs + - `1500s` metadata logs + - `1600s` vector storage logs + - `1700s` CPU compatibility logs + - `2000s` index manager logs + - `2100s` HNSW load/cache logs + +## Where It Is Implemented + +- Macro dispatch and formatting live in [src/utils/log.hpp](../src/utils/log.hpp). +- Request-level validation and 500 logging live in [src/main.cpp](../src/main.cpp). +- Index lifecycle and persistence logs live in [src/core/ndd.hpp](../src/core/ndd.hpp). diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index c23f1f2b51..98f2f48efa 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -80,23 +80,23 @@ struct CacheEntry { std::shared_ptr storage_, std::unique_ptr sparse_storage_, std::chrono::system_clock::time_point access_time_) { - LOG_INFO("Creating CacheEntry for index: " << index_id_); + LOG_INFO(2001, index_id_, "Creating cache entry"); // Validate all components if(!alg_) { - LOG_ERROR("Algorithm is null"); + LOG_ERROR(2002, index_id_, "Algorithm is null"); throw std::runtime_error("Algorithm is null"); } if(!mapper_) { - LOG_ERROR("ID Mapper is null"); + LOG_ERROR(2003, index_id_, "ID mapper is null"); throw std::runtime_error("ID Mapper is null"); } if(!storage_) { - LOG_ERROR("Vector Storage is null"); + LOG_ERROR(2004, index_id_, "Vector storage is null"); throw std::runtime_error("Vector Storage is null"); } - LOG_INFO("Assigning index_id: " << index_id_); + LOG_INFO(2005, index_id_, "Assigning index id"); index_id = std::move(index_id_); sparse_dim = sparse_dim_; @@ -108,12 +108,12 @@ struct CacheEntry { last_access = access_time_; - LOG_INFO("Moving algorithm instance"); + LOG_INFO(2006, index_id, "Moving algorithm instance"); alg = std::move(alg_); last_saved_at = std::chrono::system_clock::now(); - LOG_INFO("CacheEntry construction completed"); + LOG_INFO(2007, index_id, "Cache entry construction completed"); } void markUpdated() { @@ -166,7 +166,7 @@ class IndexManager { } std::string wal_dir = data_dir_ + "/" + index_id; - auto wal = std::make_unique(wal_dir); + auto wal = std::make_unique(wal_dir, index_id); auto wal_ptr = wal.get(); wal_logs_[index_id] = std::move(wal); return wal_ptr; @@ -186,10 +186,10 @@ class IndexManager { // Check if WAL has entries needing recovery if(wal->hasEntries()) { - LOG_INFO("WAL recovery needed for index " << index_id); + LOG_INFO(2008, index_id, "WAL recovery needed"); auto wal_entries = wal->readEntries(); - LOG_INFO("Read " << wal_entries.size() << " entries from WAL"); + LOG_INFO(2009, index_id, "Read " << wal_entries.size() << " entries from WAL"); // Process all entries in the exact order they were recorded std::vector failed_vector_add_ids; @@ -233,8 +233,10 @@ class IndexManager { // Add failed VECTOR_ADD IDs back to deleted_ids for reuse if(!failed_vector_add_ids.empty()) { entry.id_mapper->reclaim_failed_ids(failed_vector_add_ids); - LOG_INFO("Reclaimed " << failed_vector_add_ids.size() - << " failed VECTOR_ADD IDs for reuse"); + LOG_INFO(2010, + index_id, + "Reclaimed " << failed_vector_add_ids.size() + << " failed VECTOR_ADD ids for reuse"); } // Mark as updated to trigger a save @@ -249,7 +251,7 @@ class IndexManager { // The thread will call this method void autosaveLoop() { - LOG_INFO("Autosave thread started"); + LOG_INFO(2011, "Autosave thread started"); while(running_) { // Sleep for 5 minutes std::this_thread::sleep_for(std::chrono::minutes(5)); @@ -258,10 +260,10 @@ class IndexManager { if(!running_) { break; } - LOG_INFO("Autosave check running"); + LOG_INFO(2012, "Autosave check running"); checkAndSaveIndices(); } - LOG_INFO("Autosave thread stopped"); + LOG_INFO(2013, "Autosave thread stopped"); } // Check and save indices based on update time @@ -375,8 +377,8 @@ class IndexManager { // Update element count in metadata if(!metadata_manager_->updateElementCount(entry.index_id, entry.alg->getElementsCount())) { - std::cerr << "Warning: Failed to update element count in metadata for " - << entry.index_id << std::endl; + LOG_WARN( + 2014, entry.index_id, "Failed to update element count in metadata"); } entry.updated = false; } @@ -404,14 +406,13 @@ class IndexManager { // Only evict if the index is not dirty (hasn't been updated) if(it->second.updated) { - LOG_WARN("Cannot evict dirty index " << to_evict - << " - needs saving first"); + LOG_WARN(2015, to_evict, "Cannot evict dirty index; it must be saved first"); // Put it back at the front to try other indices indices_list_.push_front(to_evict); continue; } - LOG_INFO("Evicting clean index " << to_evict); + LOG_INFO(2016, to_evict, "Evicting clean index from cache"); indices_.erase(it); } } @@ -499,8 +500,9 @@ class IndexManager { saveIndex(pair.first); } } catch(const std::exception& e) { - std::cerr << "Failed to save index " << pair.first - << " during shutdown: " << e.what() << std::endl; + LOG_ERROR(2017, + pair.first, + "Failed to save index during shutdown: " << e.what()); } } LOG_DEBUG("Shutdown complete"); @@ -521,13 +523,13 @@ class IndexManager { // 1. Fail if directory doesn't exist if(!std::filesystem::exists(base_path)) { - LOG_ERROR("Index directory does not exist: " << base_path); + LOG_ERROR(2018, index_id, "Index directory does not exist: " << base_path); return false; } // 2. Fail if index file already exists if(std::filesystem::exists(index_path)) { - LOG_ERROR("Index file already exists: " << index_path); + LOG_ERROR(2019, index_id, "Index file already exists: " << index_path); return false; } @@ -550,7 +552,7 @@ class IndexManager { fout << "0:0\n"; fout.close(); - LOG_INFO("Index reset complete and saved: " << index_id); + LOG_INFO(2020, index_id, "Index reset complete and saved"); return true; } @@ -604,8 +606,9 @@ class IndexManager { std::string lmdb_dir = index_dir + "/ids"; //create the directory and initialize sequence for IDMapper - LOG_INFO("Creating IDMapper for index " - << index_id << " with user type: " << userTypeToString(user_type)); + LOG_INFO(2021, + index_id, + "Creating ID mapper with user type " << userTypeToString(user_type)); // IDMapper now uses tier-based fixed bloom filter sizing based on user_type auto id_mapper = std::make_shared(lmdb_dir, true, user_type); @@ -614,7 +617,7 @@ class IndexManager { // Create HNSW directly with all necessary parameters ndd::quant::QuantizationLevel quant_level = config.quant_level; auto vector_storage = - std::make_shared(index_dir, config.dim, config.quant_level); + std::make_shared(index_dir, index_id, config.dim, config.quant_level); // Initialize Sparse Storage if needed std::unique_ptr sparse_storage = nullptr; @@ -681,7 +684,7 @@ class IndexManager { throw std::runtime_error("Failed to store index metadata"); } - LOG_INFO("Saving newly created index " << index_id); + LOG_INFO(2022, index_id, "Saving newly created index"); // Index is marked as updated so it needs to be saved immediately for crash recovery saveIndex(index_id); return true; @@ -728,7 +731,7 @@ class IndexManager { // Step 2: Create IDMapper and VectorStorage - IDMapper handles bloom filter initialization auto id_mapper = std::make_shared(lmdb_dir, false); auto vector_storage = std::make_shared( - index_dir, alg->getDimension(), alg->getQuantLevel()); + index_dir, index_id, alg->getDimension(), alg->getQuantLevel()); // Initialize Sparse Storage if sparse_dim > 0 std::unique_ptr sparse_storage; @@ -774,7 +777,7 @@ class IndexManager { // Reload index: save (if updated), evict from memory, and reload // Cache size is automatically checked and adjusted if < 5% of element count during reload bool reload(const std::string& index_id) { - LOG_INFO("Starting reload for " << index_id); + LOG_INFO(2023, index_id, "Starting reload"); try { // Phase 1: Save index if it was updated @@ -798,7 +801,7 @@ class IndexManager { indices_list_.erase(list_it); } indices_.erase(it); - LOG_INFO("Evicted " << index_id << " from cache"); + LOG_INFO(2024, index_id, "Evicted index from cache"); } } @@ -811,15 +814,16 @@ class IndexManager { auto it = indices_.find(index_id); if(it != indices_.end()) { // Cache removed - LOG_INFO("Reloaded " - << index_id << ", bloom: fixed size" - << ", index elements: " << it->second.alg->getElementsCount()); + LOG_INFO(2025, + index_id, + "Reloaded index with " + << it->second.alg->getElementsCount() << " elements"); } } return true; } catch(const std::exception& e) { - LOG_ERROR("Failed to reload " << index_id << ": " << e.what()); + LOG_ERROR(2026, index_id, "Failed to reload index: " << e.what()); return false; } } @@ -1020,7 +1024,7 @@ class IndexManager { // so it can be caught by API layer and returned as proper JSON error throw; } catch(const std::exception& e) { - std::cerr << "Batch insertion failed: " << e.what() << std::endl; + LOG_ERROR(2027, index_id, "Batch insertion failed: " << e.what()); return false; } } @@ -1032,7 +1036,7 @@ class IndexManager { std::string recover_file = base_path + "/recover.txt"; if(!std::filesystem::exists(recover_file)) { - LOG_ERROR("Recover file not found: " << recover_file); + LOG_ERROR(2028, index_id, "Recover file not found: " << recover_file); return false; } @@ -1044,14 +1048,14 @@ class IndexManager { auto colon = line.find(':'); if(colon == std::string::npos) { - LOG_ERROR("Invalid recover.txt format"); + LOG_ERROR(2029, index_id, "Invalid recover.txt format"); return false; } size_t offset = std::stoull(line.substr(0, colon)); int flag = std::stoi(line.substr(colon + 1)); if(flag == 1) { - LOG_INFO("Recovery already in progress for: " << index_id); + LOG_INFO(2030, index_id, "Recovery already in progress"); return false; } @@ -1080,7 +1084,7 @@ class IndexManager { } if(batch.empty()) { - LOG_INFO("No more vectors to recover"); + LOG_INFO(2031, index_id, "No more vectors to recover"); std::ofstream fout(recover_file); fout << offset << ":0\n"; // just mark as not busy return true; @@ -1089,6 +1093,7 @@ class IndexManager { // Step 5: Insert in parallel like addVectors() size_t num_threads = std::min(settings::NUM_RECOVERY_THREADS, batch.size()); std::atomic next{0}; + std::atomic empty_vector_count{0}; std::vector threads; for(size_t t = 0; t < num_threads; ++t) { @@ -1099,7 +1104,7 @@ class IndexManager { if(!vec_bytes.empty()) { entry.alg->addPoint(vec_bytes.data(), label); } else { - LOG_ERROR("Skipping label " << label << " due to empty vector"); + empty_vector_count.fetch_add(1); } } }); @@ -1109,7 +1114,13 @@ class IndexManager { th.join(); } - LOG_INFO("Recovered " << batch.size() << " vectors to index: " << index_id); + if(empty_vector_count.load() > 0) { + LOG_WARN(2032, + index_id, + "Skipped " << empty_vector_count.load() << " vectors during recovery because they were empty"); + } + + LOG_INFO(2033, index_id, "Recovered " << batch.size() << " vectors"); // Step 6: Save index // Mark the index as updated so that it will be saved @@ -1153,7 +1164,7 @@ class IndexManager { return obj; } catch(const std::exception& e) { - std::cerr << "Error retrieving vector: " << e.what() << std::endl; + LOG_ERROR(2034, index_id, "Error retrieving vector: " << e.what()); return std::nullopt; } } @@ -1188,7 +1199,7 @@ class IndexManager { return true; } catch(const std::exception& e) { - std::cerr << "Failed to delete vectors: " << e.what() << std::endl; + LOG_ERROR(2035, entry.index_id, "Failed to delete vectors: " << e.what()); return false; } } @@ -1220,7 +1231,7 @@ class IndexManager { // Re-throw runtime_error (includes backup-in-progress check) throw; } catch(const std::exception& e) { - std::cerr << "Failed to delete vectors by filter: " << e.what() << std::endl; + LOG_ERROR(2036, index_id, "Failed to delete vectors by filter: " << e.what()); return 0; } } @@ -1254,7 +1265,7 @@ class IndexManager { // Re-throw runtime_error (includes backup-in-progress check) throw; } catch(const std::exception& e) { - std::cerr << "Failed to update filters: " << e.what() << std::endl; + LOG_ERROR(2037, index_id, "Failed to update filters: " << e.what()); return 0; } } @@ -1290,7 +1301,7 @@ class IndexManager { // Re-throw runtime_error (includes backup-in-progress check) throw; } catch(const std::exception& e) { - std::cerr << "Failed to delete vector: " << e.what() << std::endl; + LOG_ERROR(2038, index_id, "Failed to delete vector: " << e.what()); return false; } } @@ -1604,7 +1615,7 @@ class IndexManager { } return results; } catch(const std::exception& e) { - std::cerr << "Search error: " << e.what() << std::endl; + LOG_ERROR(2039, index_id, "Search failed: " << e.what()); return std::nullopt; } } @@ -1651,7 +1662,8 @@ class IndexManager { return true; } } catch(const std::filesystem::filesystem_error& e) { - std::cerr << "Failed to move index to deleted directory: " << e.what() << std::endl; + LOG_ERROR( + 2040, index_id, "Failed to move index to deleted directory: " << e.what()); return false; } @@ -1818,7 +1830,7 @@ inline void IndexManager::executeBackupJob(const std::string& index_id, const st {"checksum", meta->checksum}}; LOG_DEBUG("Metadata prepared for backup: " << metadata_json.dump()); } else { - LOG_ERROR("Failed to get metadata for index: " << index_id); + LOG_ERROR(2041, index_id, "Failed to get metadata for backup"); throw std::runtime_error("Cannot create backup without index metadata"); } @@ -1865,7 +1877,7 @@ inline void IndexManager::executeBackupJob(const std::string& index_id, const st backup_store_.clearActiveBackup(username); - LOG_INFO("Backup tar created, write operations now allowed for index: " << index_id); + LOG_INFO(2042, index_id, "Backup tar created; write operations resumed"); std::filesystem::rename(backup_tar_temp, backup_tar_final); @@ -1873,7 +1885,7 @@ inline void IndexManager::executeBackupJob(const std::string& index_id, const st backup_db[backup_name] = metadata_json; backup_store_.writeBackupJson(username, backup_db); - LOG_INFO("Backup completed: " << backup_name << " -> " << backup_tar_final); + LOG_INFO(2043, index_id, "Backup completed: " << backup_name << " -> " << backup_tar_final); } catch (const std::exception& e) { std::string user_backup_dir = backup_store_.getUserBackupDir(username); @@ -1895,7 +1907,7 @@ inline void IndexManager::executeBackupJob(const std::string& index_id, const st backup_store_.clearActiveBackup(username); - LOG_ERROR("Backup failed: " << backup_name << " - " << e.what()); + LOG_ERROR(2044, index_id, "Backup failed for " << backup_name << ": " << e.what()); } } @@ -1977,7 +1989,7 @@ inline std::pair IndexManager::restoreBackup(const std::strin loadIndex(target_index_id); - LOG_INFO("Restored backup from compressed archive: " << backup_tar); + LOG_INFO(2045, username, target_index_name, "Restored backup from " << backup_tar); return {true, ""}; } catch(const std::exception& e) { std::filesystem::remove_all(backup_extract_dir); @@ -2018,7 +2030,7 @@ inline std::pair IndexManager::createBackupAsync(const std::s executeBackupJob(index_id, backup_name); }).detach(); - LOG_INFO("Backup started: " << backup_name << " for index: " << index_id); + LOG_INFO(2046, index_id, "Backup started: " << backup_name); return {true, backup_name}; } diff --git a/src/filter/filter.hpp b/src/filter/filter.hpp index a44cbb913e..a6e1c4ef8a 100644 --- a/src/filter/filter.hpp +++ b/src/filter/filter.hpp @@ -42,6 +42,7 @@ class Filter { private: MDBX_env* env_; MDBX_dbi dbi_; // Used for schema storage + std::string index_id_; std::string path_; std::unique_ptr numeric_index_; std::unique_ptr category_index_; @@ -54,6 +55,8 @@ class Filter { MDBX_txn* txn; int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn); if(rc != MDBX_SUCCESS) { + LOG_ERROR( + 1210, index_id_, "Failed to begin schema read transaction: " << mdbx_strerror(rc)); return; } @@ -70,7 +73,7 @@ class Filter { schema_cache_[k] = static_cast(v.get()); } } catch(...) { - LOG_ERROR("Failed to load schema"); + LOG_ERROR(1201, index_id_, "Failed to load filter schema"); } } mdbx_txn_abort(txn); @@ -86,6 +89,8 @@ class Filter { MDBX_txn* txn; int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); if(rc != MDBX_SUCCESS) { + LOG_ERROR( + 1208, index_id_, "Failed to begin schema write transaction: " << mdbx_strerror(rc)); return; } @@ -94,9 +99,14 @@ class Filter { rc = mdbx_put(txn, dbi_, &key, &data, MDBX_UPSERT); if(rc == MDBX_SUCCESS) { - mdbx_txn_commit(txn); + rc = mdbx_txn_commit(txn); + if(rc != MDBX_SUCCESS) { + LOG_ERROR( + 1209, index_id_, "Failed to commit filter schema update: " << mdbx_strerror(rc)); + } } else { mdbx_txn_abort(txn); + LOG_ERROR(1211, index_id_, "Failed to persist filter schema: " << mdbx_strerror(rc)); } } @@ -167,7 +177,8 @@ class Filter { } public: - Filter(const std::string& path) : + Filter(const std::string& path, const std::string& index_id) : + index_id_(index_id), path_(path) { std::filesystem::create_directories(path); init_environment(); @@ -411,7 +422,7 @@ class Filter { } if(!register_field_type(field, type)) { - LOG_ERROR("Type mismatch for field '" << field << "'"); + LOG_ERROR(1202, index_id_, "Type mismatch for field '" << field << "'"); continue; } @@ -432,13 +443,15 @@ class Filter { format_filter_key(field, value.get() ? "1" : "0"); filter_to_ids[filter_key].push_back(numeric_id); } else { - // Optional: catch bad types (bool, float, object, array, etc.) - std::cerr << "Unsupported filter type for field '" << field - << "' in filter: " << value.dump() << std::endl; + LOG_WARN(1203, + index_id_, + "Unsupported filter type for field '" << field + << "' in filter: " + << value.dump()); } } } catch(const std::exception& e) { - std::cerr << "Error parsing filter JSON: " << e.what() << std::endl; + LOG_ERROR(1204, index_id_, "Error parsing filter JSON: " << e.what()); } } @@ -476,7 +489,7 @@ class Filter { } if(!register_field_type(field, type)) { - LOG_ERROR("Type mismatch for field '" << field << "'"); + LOG_ERROR(1205, index_id_, "Type mismatch for field '" << field << "'"); continue; } @@ -495,7 +508,7 @@ class Filter { } } } catch(const std::exception& e) { - std::cerr << "Error adding filters: " << e.what() << std::endl; + LOG_ERROR(1206, index_id_, "Error adding filters: " << e.what()); } } @@ -513,7 +526,7 @@ class Filter { } } } catch(const std::exception& e) { - std::cerr << "Error removing filters: " << e.what() << std::endl; + LOG_ERROR(1207, index_id_, "Error removing filters: " << e.what()); } } diff --git a/src/hnsw/hnswalg.h b/src/hnsw/hnswalg.h index cb45c7bda8..9bdd5418b5 100644 --- a/src/hnsw/hnswalg.h +++ b/src/hnsw/hnswalg.h @@ -432,7 +432,7 @@ namespace hnswlib { // Initialize cache for loaded index size_t cache_bits = VectorCache::calculateCacheBits(maxElements_); - LOG_INFO("Calculated cache bits for loaded index: " << cache_bits); + LOG_INFO(2101, "Calculated cache bits for loaded index: " << cache_bits); if (cache_bits > 0) { vector_cache_ = std::make_unique(data_size_, cache_bits); LOG_DEBUG("Vector cache initialized for " << maxElements_ << " elements with " << (1ULL << cache_bits) << " slots"); diff --git a/src/main.cpp b/src/main.cpp index dd001bd353..d39cc15457 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -63,6 +63,7 @@ struct AuthMiddleware : crow::ILocalMiddleware { // Auth is enabled - token is REQUIRED auto auth_header = req.get_header_value("Authorization"); if(auth_header.empty()) { + LOG_WARN(1001, ctx.username, "Rejected request without Authorization header"); res.code = 401; res.write("Authorization header required"); res.end(); @@ -70,6 +71,7 @@ struct AuthMiddleware : crow::ILocalMiddleware { } if(auth_header != settings::AUTH_TOKEN) { + LOG_WARN(1002, ctx.username, "Rejected request with invalid Authorization header"); res.code = 401; res.write("Invalid token"); res.end(); @@ -85,14 +87,21 @@ inline crow::response json_error(int code, const std::string& message) { return crow::response(code, err_json.dump()); } // Special helper function to log and send error messages in JSON format for 500 errors -inline crow::response -json_error_500(const std::string& username, const std::string& path, const std::string& message) { - LOG_ERROR("500 Error | user: " << username << " | path: " << path << " | message: " << message); +inline crow::response json_error_500(const std::string& username, + const std::string& index_name, + const std::string& path, + const std::string& message) { + LOG_ERROR(1003, username, index_name, "500 error on " << path << ": " << message); crow::json::wvalue err_json({{"error", message}}); return crow::response(500, err_json.dump()); } +inline crow::response +json_error_500(const std::string& username, const std::string& path, const std::string& message) { + return json_error_500(username, "-", path, message); +} + /** * Checks if the CPU is compatible with all * the instruction sets being used for x86, ARM and MAC Mxx @@ -179,25 +188,25 @@ bool file_exists(const std::string& path) { int main(int argc, char** argv) { if(!is_cpu_compatible()) { - printf("CPU is not compatible. Can't run Endee\n"); + LOG_ERROR(1004, "CPU is not compatible; server startup aborted"); return 0; } - LOG_DEBUG("SERVER_ID: " << settings::SERVER_ID); - LOG_DEBUG("SERVER_PORT: " << settings::SERVER_PORT); - LOG_DEBUG("DATA_DIR: " << settings::DATA_DIR); - LOG_DEBUG("NUM_PARALLEL_INSERTS: " << settings::NUM_PARALLEL_INSERTS); - LOG_DEBUG("NUM_RECOVERY_THREADS: " << settings::NUM_RECOVERY_THREADS); - LOG_DEBUG("MAX_MEMORY_GB: " << settings::MAX_MEMORY_GB); - LOG_DEBUG("ENABLE_DEBUG_LOG: " << settings::ENABLE_DEBUG_LOG); - LOG_DEBUG("AUTH_TOKEN: " << settings::AUTH_TOKEN); - LOG_DEBUG("AUTH_ENABLED: " << settings::AUTH_ENABLED); - LOG_DEBUG("DEFAULT_USERNAME: " << settings::DEFAULT_USERNAME); - LOG_DEBUG("DEFAULT_SERVER_TYPE: " << settings::DEFAULT_SERVER_TYPE); - LOG_DEBUG("DEFAULT_DATA_DIR: " << settings::DEFAULT_DATA_DIR); - LOG_DEBUG("DEFAULT_MAX_ACTIVE_INDICES: " << settings::DEFAULT_MAX_ACTIVE_INDICES); - LOG_DEBUG("DEFAULT_MAX_ELEMENTS: " << settings::DEFAULT_MAX_ELEMENTS); - LOG_DEBUG("DEFAULT_MAX_ELEMENTS_INCREMENT: " << settings::DEFAULT_MAX_ELEMENTS_INCREMENT); - LOG_DEBUG("DEFAULT_MAX_ELEMENTS_INCREMENT_TRIGGER: " + LOG_INFO("SERVER_ID: " << settings::SERVER_ID); + LOG_INFO("SERVER_PORT: " << settings::SERVER_PORT); + LOG_INFO("DATA_DIR: " << settings::DATA_DIR); + LOG_INFO("NUM_PARALLEL_INSERTS: " << settings::NUM_PARALLEL_INSERTS); + LOG_INFO("NUM_RECOVERY_THREADS: " << settings::NUM_RECOVERY_THREADS); + LOG_INFO("MAX_MEMORY_GB: " << settings::MAX_MEMORY_GB); + LOG_INFO("ENABLE_DEBUG_LOG: " << settings::ENABLE_DEBUG_LOG); + LOG_INFO("AUTH_TOKEN: " << settings::AUTH_TOKEN); + LOG_INFO("AUTH_ENABLED: " << settings::AUTH_ENABLED); + LOG_INFO("DEFAULT_USERNAME: " << settings::DEFAULT_USERNAME); + LOG_INFO("DEFAULT_SERVER_TYPE: " << settings::DEFAULT_SERVER_TYPE); + LOG_INFO("DEFAULT_DATA_DIR: " << settings::DEFAULT_DATA_DIR); + LOG_INFO("DEFAULT_MAX_ACTIVE_INDICES: " << settings::DEFAULT_MAX_ACTIVE_INDICES); + LOG_INFO("DEFAULT_MAX_ELEMENTS: " << settings::DEFAULT_MAX_ELEMENTS); + LOG_INFO("DEFAULT_MAX_ELEMENTS_INCREMENT: " << settings::DEFAULT_MAX_ELEMENTS_INCREMENT); + LOG_INFO("DEFAULT_MAX_ELEMENTS_INCREMENT_TRIGGER: " << settings::DEFAULT_MAX_ELEMENTS_INCREMENT_TRIGGER); // Path to React build directory @@ -217,11 +226,11 @@ int main(int argc, char** argv) { true // Save on shutdown }; // Initialize auth manager and user manager - LOG_INFO("Starting the server"); + LOG_INFO(1005, "Starting the server"); AuthManager auth_manager(data_dir); - LOG_INFO("Created auth manager"); + LOG_INFO(1006, "Created auth manager"); IndexManager index_manager(settings::MAX_ACTIVE_INDICES, data_dir, persistence_config); - LOG_INFO("Created index manager"); + LOG_INFO(1007, "Created index manager"); // Initialize the app crow::App app{AuthMiddleware(auth_manager)}; @@ -294,10 +303,12 @@ int main(int argc, char** argv) { auto body = crow::json::load(req.body); if(!body) { + LOG_WARN(1011, ctx.username, "Create-index request contained invalid JSON"); return json_error(400, "Invalid JSON"); } if(!body.has("index_name") || !body.has("dim") || !body.has("space_type")) { + LOG_WARN(1012, ctx.username, "Create-index request is missing required parameters"); return json_error(400, "Missing required parameters"); } @@ -311,7 +322,7 @@ int main(int argc, char** argv) { size_t dim = (size_t)body["dim"].i(); // Validate dimension if(dim < settings::MIN_DIMENSION || dim > settings::MAX_DIMENSION) { - LOG_ERROR("Invalid dimension: " << dim); + LOG_WARN(1013, index_id, "Invalid dimension: " << dim); return json_error(400, "Dimension must be between " + std::to_string(settings::MIN_DIMENSION) + " and " @@ -321,7 +332,7 @@ int main(int argc, char** argv) { // Validate M size_t m = body.has("M") ? (size_t)body["M"].i() : settings::DEFAULT_M; if(m < settings::MIN_M || m > settings::MAX_M) { - LOG_ERROR("Invalid M: " << m); + LOG_WARN(1014, index_id, "Invalid M: " << m); return json_error(400, "M must be between " + std::to_string(settings::MIN_M) + " and " + std::to_string(settings::MAX_M)); @@ -331,7 +342,7 @@ int main(int argc, char** argv) { size_t ef_con = body.has("ef_con") ? (size_t)body["ef_con"].i() : settings::DEFAULT_EF_CONSTRUCT; if(ef_con < settings::MIN_EF_CONSTRUCT || ef_con > settings::MAX_EF_CONSTRUCT) { - LOG_ERROR("Invalid ef_con: " << ef_con); + LOG_WARN(1015, index_id, "Invalid ef_construction: " << ef_con); return json_error(400, "ef_con must be between " + std::to_string(settings::MIN_EF_CONSTRUCT) + " and " @@ -351,7 +362,7 @@ int main(int argc, char** argv) { // Validate quantization level if(quant_level == ndd::quant::QuantizationLevel::UNKNOWN) { - LOG_ERROR("Invalid precision: " << body["precision"].s()); + LOG_WARN(1016, index_id, "Invalid precision: " << body["precision"].s()); std::vector names = ndd::quant::getAvailableQuantizationNames(); std::string names_str; for(size_t i = 0; i < names.size(); ++i) { @@ -368,10 +379,12 @@ int main(int argc, char** argv) { if(body.has("size_in_millions")) { size_in_millions = static_cast(body["size_in_millions"].i()); if(size_in_millions == 0 || size_in_millions > 10000) { // Cap at 10B vectors + LOG_WARN(1017, + index_id, + "Invalid custom size_in_millions: " << size_in_millions); return json_error(400, "size_in_millions must be between 1 and 10000"); } - LOG_INFO("Creating index with custom size: " << size_in_millions - << "M vectors"); + LOG_INFO(1018, index_id, "Creating index with custom size: " << size_in_millions << "M vectors"); } size_t sparse_dim = body.has("sparse_dim") ? (size_t)body["sparse_dim"].i() : 0; @@ -390,9 +403,11 @@ int main(int argc, char** argv) { index_manager.createIndex(index_id, config, UserType::Admin, size_in_millions); return crow::response(200, "Index created successfully"); } catch(const std::runtime_error& e) { + LOG_WARN(1019, index_id, "Create-index request failed: " << e.what()); return json_error(409, e.what()); } catch(const std::exception& e) { - return json_error_500(ctx.username, req.url, std::string("Error: ") + e.what()); + return json_error_500( + ctx.username, body["index_name"].s(), req.url, std::string("Error: ") + e.what()); } }); @@ -405,6 +420,7 @@ int main(int argc, char** argv) { auto body = crow::json::load(req.body); if(!body || !body.has("name")) { + LOG_WARN(1020, ctx.username, index_name, "Create-backup request missing backup name"); return json_error(400, "Missing backup name"); } @@ -415,6 +431,7 @@ int main(int argc, char** argv) { std::pair result = index_manager.createBackupAsync(index_id, backup_name); if(!result.first) { + LOG_WARN(1021, ctx.username, index_name, "Create-backup request rejected: " << result.second); return json_error(400, result.second); } @@ -424,7 +441,7 @@ int main(int argc, char** argv) { response["status"] = "in_progress"; return crow::response(202, response.dump()); } catch(const std::exception& e) { - return json_error(500, e.what()); + return json_error_500(ctx.username, index_name, req.url, e.what()); } }); @@ -442,7 +459,7 @@ int main(int argc, char** argv) { res.body = result_json.dump(); return res; } catch(const std::exception& e) { - return json_error(500, e.what()); + return json_error_500(ctx.username, req.url, e.what()); } }); @@ -455,6 +472,7 @@ int main(int argc, char** argv) { auto body = crow::json::load(req.body); if(!body || !body.has("target_index_name")) { + LOG_WARN(1022, ctx.username, "Restore-backup request missing target index name"); return json_error(400, "Missing target_index_name"); } @@ -464,11 +482,12 @@ int main(int argc, char** argv) { std::pair result = index_manager.restoreBackup(backup_name, target_index_name, ctx.username); if(!result.first) { + LOG_WARN(1023, ctx.username, target_index_name, "Restore-backup request rejected: " << result.second); return json_error(400, result.second); } return crow::response(201, "Backup restored successfully"); } catch(const std::exception& e) { - return json_error(500, e.what()); + return json_error_500(ctx.username, target_index_name, req.url, e.what()); } }); @@ -481,11 +500,12 @@ int main(int argc, char** argv) { try { std::pair result = index_manager.deleteBackup(backup_name, ctx.username); if(!result.first) { + LOG_WARN(1024, ctx.username, "Delete-backup request rejected: " << result.second); return json_error(400, result.second); } return crow::response(204, "Backup deleted successfully"); } catch(const std::exception& e) { - return json_error(500, e.what()); + return json_error_500(ctx.username, req.url, e.what()); } }); @@ -497,6 +517,7 @@ int main(int argc, char** argv) { std::string token = req.url_params.get("token") ? req.url_params.get("token") : ""; if(token != settings::AUTH_TOKEN) { + LOG_WARN(1057, "Rejected backup download request with invalid token"); return json_error(401, "Unauthorized"); } } @@ -505,6 +526,7 @@ int main(int argc, char** argv) { settings::DATA_DIR + "/backups/" + settings::DEFAULT_USERNAME + "/" + backup_name + ".tar"; if(!std::filesystem::exists(backup_file)) { + LOG_WARN(1058, settings::DEFAULT_USERNAME, "Backup download requested for missing backup " << backup_name); return json_error(404, "Backup not found"); } @@ -517,7 +539,7 @@ int main(int argc, char** argv) { response.set_header("Cache-Control", "no-cache"); return response; } catch(const std::exception& e) { - return json_error(500, e.what()); + return json_error_500(settings::DEFAULT_USERNAME, req.url, e.what()); } }); @@ -548,6 +570,7 @@ int main(int argc, char** argv) { if(backup_name.ends_with(".tar")) { backup_name = backup_name.substr(0, backup_name.size() - 4); } else { + LOG_WARN(1059, ctx.username, "Backup upload used invalid file extension"); return json_error(400, "Invalid backup file extension. Expected .tar file"); } } @@ -557,10 +580,12 @@ int main(int argc, char** argv) { } if(backup_name.empty()) { + LOG_WARN(1060, ctx.username, "Backup upload request missing backup name"); return json_error(400, "Missing backup name or filename"); } if(file_content.empty()) { + LOG_WARN(1061, ctx.username, "Backup upload request missing backup file content"); return json_error(400, "Missing backup file content"); } @@ -568,6 +593,7 @@ int main(int argc, char** argv) { std::pair result = index_manager.validateBackupName(backup_name); if(!result.first) { + LOG_WARN(1062, ctx.username, "Backup upload request rejected: " << result.second); return json_error(400, result.second); } @@ -576,6 +602,7 @@ int main(int argc, char** argv) { std::filesystem::create_directories(user_backup_dir); std::string backup_path = user_backup_dir + "/" + backup_name + ".tar"; if(std::filesystem::exists(backup_path)) { + LOG_WARN(1063, ctx.username, "Backup upload conflicts with existing backup " << backup_name); return json_error(409, "Backup with name '" + backup_name + "' already exists"); } @@ -583,7 +610,8 @@ int main(int argc, char** argv) { // Write the file std::ofstream out(backup_path, std::ios::binary); if(!out.is_open()) { - return json_error(500, "Failed to create backup file"); + return json_error_500( + ctx.username, req.url, "Failed to create backup file"); } out.write(file_content.data(), file_content.size()); out.close(); @@ -591,12 +619,13 @@ int main(int argc, char** argv) { if(!out.good()) { // Clean up partial file on error std::filesystem::remove(backup_path); - return json_error(500, "Failed to write backup file"); + return json_error_500( + ctx.username, req.url, "Failed to write backup file"); } return crow::response(201, "Backup uploaded successfully"); } catch(const std::exception& e) { - return json_error(500, e.what()); + return json_error_500(ctx.username, req.url, e.what()); } }); @@ -617,7 +646,7 @@ int main(int argc, char** argv) { } return crow::response(200, response.dump()); } catch(const std::exception& e) { - return json_error(500, e.what()); + return json_error_500(ctx.username, req.url, e.what()); } }); @@ -630,6 +659,7 @@ int main(int argc, char** argv) { try { auto info = index_manager.getBackupInfo(backup_name, ctx.username); if (info.empty()) { + LOG_WARN(1064, ctx.username, "Backup-info request for missing backup " << backup_name); return json_error(404, "Backup not found or metadata missing"); } crow::response res; @@ -638,7 +668,7 @@ int main(int argc, char** argv) { res.body = info.dump(); return res; } catch(const std::exception& e) { - return json_error(500, e.what()); + return json_error_500(ctx.username, req.url, e.what()); } }); @@ -688,12 +718,15 @@ int main(int argc, char** argv) { if(index_manager.deleteIndex(index_id)) { return crow::response(200, "Index deleted successfully"); } else { + LOG_WARN(1030, ctx.username, index_name, "Delete-index request for missing index"); return json_error(404, "Index not found"); } } catch(const std::runtime_error& e) { + LOG_WARN(1031, ctx.username, index_name, "Delete-index request rejected: " << e.what()); return json_error(400, e.what()); } catch(const std::exception& e) { return json_error_500(ctx.username, + index_name, req.url, std::string("Failed to delete index: ") + e.what()); } @@ -710,10 +743,12 @@ int main(int argc, char** argv) { auto body = crow::json::load(req.body); if(!body || !body.has("k")) { + LOG_WARN(1032, ctx.username, index_name, "Search request missing parameter k or has invalid JSON"); return json_error(400, "Missing required parameters: k"); } if(!body.has("vector") && !body.has("sparse_indices")) { + LOG_WARN(1033, ctx.username, index_name, "Search request missing dense and sparse query vectors"); return json_error(400, "Missing query vector (dense or sparse)"); } @@ -740,13 +775,17 @@ int main(int argc, char** argv) { } if(sparse_indices.size() != sparse_values.size()) { + LOG_WARN(1034, + ctx.username, + index_name, + "Search request has mismatched sparse_indices and sparse_values"); return json_error(400, "Mismatch between sparse_indices and sparse_values size"); } size_t k = (size_t)body["k"].i(); if(k < settings::MIN_K || k > settings::MAX_K) { - LOG_ERROR("Invalid k: " << k); + LOG_WARN(1035, ctx.username, index_name, "Invalid k: " << k); return json_error(400, "k must be between " + std::to_string(settings::MIN_K) + " and " + std::to_string(settings::MAX_K)); @@ -761,12 +800,14 @@ int main(int argc, char** argv) { auto raw_filter = nlohmann::json::parse(body["filter"].s()); // Expect new array-based filter format if(!raw_filter.is_array()) { + LOG_WARN(1036, ctx.username, index_name, "Search request used invalid filter format"); return json_error(400, "Filter must be an array. Please use format: " "[{\"field\":{\"$op\":value}}]"); } filter_array = raw_filter; } catch(const std::exception& e) { + LOG_WARN(1037, ctx.username, index_name, "Search request filter JSON parsing failed: " << e.what()); return json_error(400, std::string("Invalid filter JSON: ") + e.what()); } } @@ -795,6 +836,7 @@ int main(int argc, char** argv) { include_vectors, ef); if(!search_response) { + LOG_WARN(1038, ctx.username, index_name, "Search request returned no results because the index is missing or search failed"); return json_error(404, "Index not found or search failed"); } @@ -805,11 +847,15 @@ int main(int argc, char** argv) { resp.add_header("Content-Type", "application/msgpack"); return resp; } catch(const std::runtime_error& e) { + LOG_WARN(1039, ctx.username, index_name, "Search request rejected: " << e.what()); return json_error(400, e.what()); } catch(const std::exception& e) { LOG_DEBUG("Search failed: " << e.what()); return json_error_500( - ctx.username, req.url, std::string("Search failed: ") + e.what()); + ctx.username, + index_name, + req.url, + std::string("Search failed: ") + e.what()); } }); @@ -827,6 +873,7 @@ int main(int argc, char** argv) { if(content_type == "application/json") { auto body = crow::json::load(req.body); if(!body) { + LOG_WARN(1040, ctx.username, index_name, "Insert request contained invalid JSON"); return json_error(400, "Invalid JSON"); } @@ -888,9 +935,10 @@ int main(int argc, char** argv) { bool success = index_manager.addVectors(index_id, vectors); return crow::response(success ? 200 : 400); } catch(const std::runtime_error& e) { + LOG_WARN(1041, ctx.username, index_name, "Insert request rejected: " << e.what()); return json_error(400, e.what()); } catch(const std::exception& e) { - return json_error_500(ctx.username, req.url, e.what()); + return json_error_500(ctx.username, index_name, req.url, e.what()); } } else if(content_type == "application/msgpack") { // Deserialize MsgPack batch @@ -912,12 +960,14 @@ int main(int argc, char** argv) { return crow::response(success ? 200 : 400); } } catch(const std::runtime_error& e) { + LOG_WARN(1042, ctx.username, index_name, "Insert request rejected: " << e.what()); return json_error(400, e.what()); } catch(const std::exception& e) { LOG_DEBUG("Batch insertion failed: " << e.what()); - return json_error_500(ctx.username, req.url, e.what()); + return json_error_500(ctx.username, index_name, req.url, e.what()); } } else { + LOG_WARN(1043, ctx.username, index_name, "Insert request used unsupported Content-Type: " << content_type); return crow::response( 400, "Content-Type must be application/msgpack or application/json"); } @@ -934,12 +984,14 @@ int main(int argc, char** argv) { // Read vector ID from JSON input (still using JSON for ID here) auto body = crow::json::load(req.body); if(!body || !body.has("id")) { + LOG_WARN(1044, ctx.username, index_name, "Get-vector request missing vector id"); return json_error(400, "Missing required parameter 'id'"); } std::string vector_id = body["id"].s(); try { auto vector = index_manager.getVector(index_id, vector_id); if(!vector) { + LOG_WARN(1045, ctx.username, index_name, "Get-vector request for missing vector id " << vector_id); return json_error(404, "Vector with the given ID does not exist"); } // Serialize vector as MsgPack @@ -952,6 +1004,7 @@ int main(int argc, char** argv) { } catch(const std::exception& e) { LOG_DEBUG("Failed to get vector: " << e.what()); return json_error_500(ctx.username, + index_name, req.url, std::string("Failed to get vector: ") + e.what()); } @@ -972,13 +1025,16 @@ int main(int argc, char** argv) { if(index_manager.deleteVector(index_id, vector_id)) { return crow::response(200, "Vector deleted successfully"); } else { + LOG_WARN(1046, ctx.username, index_name, "Delete-vector request for missing vector id " << vector_id); return json_error(404, "Vector with the given ID does not exist"); } } catch(const std::runtime_error& e) { + LOG_WARN(1047, ctx.username, index_name, "Delete-vector request rejected: " << e.what()); return json_error(400, e.what()); } catch(const std::exception& e) { LOG_DEBUG("Failed to delete vector: " << e.what()); return json_error_500(ctx.username, + index_name, req.url, std::string("Failed to delete vector: ") + e.what()); } @@ -996,15 +1052,18 @@ int main(int argc, char** argv) { try { body = nlohmann::json::parse(req.body); } catch(const std::exception& e) { + LOG_WARN(1048, ctx.username, index_name, "Delete-by-filter request contained invalid JSON"); return json_error(400, "Invalid JSON body"); } if(!body.contains("filter")) { + LOG_WARN(1049, ctx.username, index_name, "Delete-by-filter request is missing filter"); return json_error(400, "Invalid request body - missing filter"); } try { nlohmann::json filter_array = body["filter"]; // Expect new array-based filter format if(!filter_array.is_array()) { + LOG_WARN(1050, ctx.username, index_name, "Delete-by-filter request used invalid filter format"); return json_error(400, "Filter must be an array. Please use format: " "[{\"field\":{\"$op\":value}}]"); @@ -1014,9 +1073,11 @@ int main(int argc, char** argv) { return crow::response(200, std::to_string(deleted_count) + " vectors deleted"); } catch(const std::runtime_error& e) { + LOG_WARN(1051, ctx.username, index_name, "Delete-by-filter request rejected: " << e.what()); return json_error(400, e.what()); } catch(const std::exception& e) { return json_error_500(ctx.username, + index_name, req.url, std::string("Failed to delete vectors: ") + e.what()); } @@ -1034,10 +1095,12 @@ int main(int argc, char** argv) { try { body = nlohmann::json::parse(req.body); } catch(const std::exception& e) { + LOG_WARN(1052, ctx.username, index_name, "Update-filters request contained invalid JSON"); return json_error(400, "Invalid JSON body"); } if(!body.contains("updates") || !body["updates"].is_array()) { + LOG_WARN(1053, ctx.username, index_name, "Update-filters request missing valid updates array"); return json_error(400, "Missing or invalid 'updates' field. Must be a list of {id, " "filter} objects."); @@ -1059,9 +1122,11 @@ int main(int argc, char** argv) { return crow::response(200, std::to_string(count) + " filters updated"); } catch(const std::runtime_error& e) { + LOG_WARN(1054, ctx.username, index_name, "Update-filters request rejected: " << e.what()); return json_error(400, e.what()); } catch(const std::exception& e) { return json_error_500(ctx.username, + index_name, req.url, std::string("Failed to update filters: ") + e.what()); } @@ -1076,6 +1141,7 @@ int main(int argc, char** argv) { try { auto info = index_manager.getIndexInfo(index_id); if(!info) { + LOG_WARN(1055, ctx.username, index_name, "Index-info request for missing index"); return json_error(404, "Index does not exist"); } crow::json::wvalue response( @@ -1090,9 +1156,13 @@ int main(int argc, char** argv) { {"lib_token", settings::DEFAULT_LIB_TOKEN}}); return crow::response(200, response.dump()); } catch(const std::runtime_error& e) { + LOG_WARN(1056, ctx.username, index_name, "Index-info request failed: " << e.what()); return json_error(404, std::string("Error: ") + e.what()); } catch(const std::exception& e) { - return json_error_500(ctx.username, req.url, std::string("Error: ") + e.what()); + return json_error_500(ctx.username, + index_name, + req.url, + std::string("Error: ") + e.what()); } }); @@ -1175,14 +1245,14 @@ int main(int argc, char** argv) { }); unsigned int num_cores = std::thread::hardware_concurrency(); - LOG_INFO("Number of processor cores: " << num_cores); + LOG_INFO(1008, "Number of processor cores: " << num_cores); if(settings::NUM_SERVER_THREADS == 0) { // Run on max possible threads - LOG_INFO("Using all available threads"); + LOG_INFO(1009, "Using all available threads"); app.port(settings::SERVER_PORT).multithreaded().run(); } else { // Limit on the number of threads - LOG_INFO("Using " << settings::NUM_SERVER_THREADS << " threads"); + LOG_INFO(1010, "Using " << settings::NUM_SERVER_THREADS << " threads"); app.port(settings::SERVER_PORT).concurrency(settings::NUM_SERVER_THREADS).run(); } diff --git a/src/server/auth.hpp b/src/server/auth.hpp index 56ccc8ccff..0838aa9b8f 100644 --- a/src/server/auth.hpp +++ b/src/server/auth.hpp @@ -7,6 +7,7 @@ #include #include "json/nlohmann_json.hpp" +#include "log.hpp" #include "settings.hpp" // Simplified for open-source mode - only Admin type exists @@ -78,10 +79,9 @@ class AuthManager { std::filesystem::create_directories(default_user_dir); if(settings::AUTH_ENABLED) { - std::cerr << "Authentication ENABLED - NDD_AUTH_TOKEN is set" << std::endl; + LOG_INFO(1101, "Authentication enabled"); } else { - std::cerr << "Authentication DISABLED - Running in open mode (no token required)" - << std::endl; + LOG_INFO(1102, "Authentication disabled; running in open mode"); } } diff --git a/src/storage/backup_store.hpp b/src/storage/backup_store.hpp index eeee973a26..9600c2d962 100644 --- a/src/storage/backup_store.hpp +++ b/src/storage/backup_store.hpp @@ -148,7 +148,10 @@ class BackupStore { try { std::ifstream f(path); return nlohmann::json::parse(f); - } catch (...) { + } catch (const std::exception& e) { + LOG_WARN(1304, + username, + "Failed to parse backup metadata file " << path << ": " << e.what()); return nlohmann::json::object(); } } @@ -166,9 +169,9 @@ class BackupStore { if (std::filesystem::exists(temp_dir)) { try { std::filesystem::remove_all(temp_dir); - LOG_INFO("Cleaned up backup temp directory"); + LOG_INFO(1301, "Cleaned up backup temp directory"); } catch (const std::exception& e) { - LOG_ERROR("Failed to cleanup backup temp directory: " << e.what()); + LOG_ERROR(1302, "Failed to clean up backup temp directory: " << e.what()); } } } @@ -256,7 +259,7 @@ class BackupStore { backup_db.erase(backup_name); writeBackupJson(username, backup_db); - LOG_INFO("Deleted backup: " << backup_tar); + LOG_INFO(1303, username, "Deleted backup " << backup_tar); return {true, ""}; } else { return {false, "Backup not found"}; diff --git a/src/storage/index_meta.hpp b/src/storage/index_meta.hpp index 7858630837..1011c13705 100644 --- a/src/storage/index_meta.hpp +++ b/src/storage/index_meta.hpp @@ -8,6 +8,7 @@ #include #include #include +#include "log.hpp" #include "settings.hpp" #include "mdbx/mdbx.h" #include "quant/common.hpp" @@ -83,7 +84,8 @@ class MetadataManager { MDBX_txn* txn; int rc = mdbx_txn_begin(metadata_env_, nullptr, MDBX_TXN_READWRITE, &txn); if(rc != 0) { - std::cerr << "Failed to begin transaction: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1501, index_id, "Failed to begin metadata transaction: " << mdbx_strerror(rc)); return false; } @@ -95,20 +97,22 @@ class MetadataManager { rc = mdbx_put(txn, metadata_dbi_, &db_key, &data, MDBX_UPSERT); if(rc != 0) { mdbx_txn_abort(txn); - std::cerr << "Failed to store metadata: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1502, index_id, "Failed to store metadata: " << mdbx_strerror(rc)); return false; } rc = mdbx_txn_commit(txn); if(rc != 0) { - std::cerr << "Failed to commit transaction: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1503, index_id, "Failed to commit metadata transaction: " << mdbx_strerror(rc)); return false; } return true; } catch(const std::exception& e) { mdbx_txn_abort(txn); - std::cerr << "Exception while storing metadata: " << e.what() << std::endl; + LOG_ERROR(1504, index_id, "Exception while storing metadata: " << e.what()); return false; } } @@ -117,8 +121,7 @@ class MetadataManager { bool updateElementCount(const std::string& index_id, size_t count) { auto metadata = getMetadata(index_id); if(!metadata) { - std::cerr << "Cannot update element count: metadata not found for " << index_id - << std::endl; + LOG_WARN(1505, index_id, "Cannot update element count because metadata was not found"); return false; } metadata->total_elements = count; @@ -132,7 +135,8 @@ class MetadataManager { MDBX_txn* txn; int rc = mdbx_txn_begin(metadata_env_, nullptr, MDBX_TXN_RDONLY, &txn); if(rc != 0) { - std::cerr << "Failed to begin transaction: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1506, index_id, "Failed to begin metadata read transaction: " << mdbx_strerror(rc)); return std::nullopt; } @@ -144,7 +148,8 @@ class MetadataManager { if(rc != 0) { mdbx_txn_abort(txn); if(rc != MDBX_NOTFOUND) { - std::cerr << "Failed to retrieve metadata: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1507, index_id, "Failed to retrieve metadata: " << mdbx_strerror(rc)); } return std::nullopt; } @@ -155,7 +160,7 @@ class MetadataManager { return IndexMetadata::from_json(nlohmann::json::parse(json_str)); } catch(const std::exception& e) { mdbx_txn_abort(txn); - std::cerr << "Exception while retrieving metadata: " << e.what() << std::endl; + LOG_ERROR(1508, index_id, "Exception while retrieving metadata: " << e.what()); return std::nullopt; } } @@ -166,7 +171,8 @@ class MetadataManager { MDBX_txn* txn; int rc = mdbx_txn_begin(metadata_env_, nullptr, MDBX_TXN_READWRITE, &txn); if(rc != MDBX_SUCCESS) { - std::cerr << "Failed to begin transaction: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1509, index_id, "Failed to begin metadata delete transaction: " << mdbx_strerror(rc)); return false; } @@ -176,20 +182,22 @@ class MetadataManager { rc = mdbx_del(txn, metadata_dbi_, &db_key, nullptr); if(rc != MDBX_SUCCESS && rc != MDBX_NOTFOUND) { mdbx_txn_abort(txn); - std::cerr << "Failed to delete metadata: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1510, index_id, "Failed to delete metadata: " << mdbx_strerror(rc)); return false; } rc = mdbx_txn_commit(txn); if(rc != MDBX_SUCCESS) { - std::cerr << "Failed to commit transaction: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1511, index_id, "Failed to commit metadata delete transaction: " << mdbx_strerror(rc)); return false; } return true; } catch(const std::exception& e) { mdbx_txn_abort(txn); - std::cerr << "Exception while deleting metadata: " << e.what() << std::endl; + LOG_ERROR(1512, index_id, "Exception while deleting metadata: " << e.what()); return false; } } @@ -201,7 +209,8 @@ class MetadataManager { MDBX_txn* txn; int rc = mdbx_txn_begin(metadata_env_, nullptr, MDBX_TXN_RDONLY, &txn); if(rc != 0) { - std::cerr << "Failed to begin transaction: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1513, "Failed to begin list-all metadata transaction: " << mdbx_strerror(rc)); return result; } @@ -209,7 +218,7 @@ class MetadataManager { rc = mdbx_cursor_open(txn, metadata_dbi_, &cursor); if(rc != 0) { mdbx_txn_abort(txn); - std::cerr << "Failed to open cursor: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR(1514, "Failed to open metadata cursor: " << mdbx_strerror(rc)); return result; } @@ -221,7 +230,7 @@ class MetadataManager { result.push_back( {key_str, IndexMetadata::from_json(nlohmann::json::parse(json_str))}); } catch(const std::exception& e) { - std::cerr << "Failed to parse metadata: " << e.what() << std::endl; + LOG_ERROR(1515, "Failed to parse metadata while listing all metadata: " << e.what()); // Skip invalid entries } } @@ -240,7 +249,8 @@ class MetadataManager { MDBX_txn* txn; int rc = mdbx_txn_begin(metadata_env_, nullptr, MDBX_TXN_RDONLY, &txn); if(rc != 0) { - std::cerr << "Failed to begin transaction: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1516, username, "Failed to begin list-user metadata transaction: " << mdbx_strerror(rc)); return indexes; } @@ -248,7 +258,7 @@ class MetadataManager { rc = mdbx_cursor_open(txn, metadata_dbi_, &cursor); if(rc != 0) { mdbx_txn_abort(txn); - std::cerr << "Failed to open cursor: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR(1517, username, "Failed to open metadata cursor: " << mdbx_strerror(rc)); return indexes; } @@ -270,8 +280,8 @@ class MetadataManager { // Add to result indexes.emplace_back(index_name, std::move(metadata)); } catch(const std::exception& e) { - std::cerr << "Failed to parse metadata for index " << key_str << ": " - << e.what() << std::endl; + LOG_ERROR( + 1518, key_str, "Failed to parse metadata for index: " << e.what()); // Skip invalid entries } } @@ -288,7 +298,8 @@ class MetadataManager { MDBX_txn* txn; int rc = mdbx_txn_begin(metadata_env_, nullptr, MDBX_TXN_RDONLY, &txn); if(rc != 0) { - std::cerr << "Failed to begin transaction: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR( + 1519, "Failed to begin list-all indexes transaction: " << mdbx_strerror(rc)); return result; } @@ -296,7 +307,7 @@ class MetadataManager { rc = mdbx_cursor_open(txn, metadata_dbi_, &cursor); if(rc != 0) { mdbx_txn_abort(txn); - std::cerr << "Failed to open cursor: " << mdbx_strerror(rc) << std::endl; + LOG_ERROR(1520, "Failed to open list-all indexes cursor: " << mdbx_strerror(rc)); return result; } @@ -308,7 +319,7 @@ class MetadataManager { IndexMetadata metadata = IndexMetadata::from_json(nlohmann::json::parse(json_str)); result.emplace_back(key_str, std::move(metadata)); } catch(const std::exception& e) { - std::cerr << "Failed to parse metadata: " << e.what() << std::endl; + LOG_ERROR(1521, "Failed to parse metadata while listing all indexes: " << e.what()); // skip bad record } } diff --git a/src/storage/vector_storage.hpp b/src/storage/vector_storage.hpp index b410b7ff33..e452888b98 100644 --- a/src/storage/vector_storage.hpp +++ b/src/storage/vector_storage.hpp @@ -19,6 +19,7 @@ class VectorStore { private: MDBX_env* env_; MDBX_dbi dbi_; + std::string index_id_; std::string path_; size_t vector_dim_; ndd::quant::QuantizationLevel quant_level_; @@ -72,7 +73,9 @@ class VectorStore { public: VectorStore(const std::string& path, size_t vector_dim, - ndd::quant::QuantizationLevel quant_level) : + ndd::quant::QuantizationLevel quant_level, + const std::string& index_id) : + index_id_(index_id), path_(path), vector_dim_(vector_dim), quant_level_(quant_level) { @@ -89,11 +92,13 @@ class VectorStore { // Nested Cursor struct struct Cursor { + std::string index_id_; MDBX_txn* txn = nullptr; MDBX_cursor* cursor = nullptr; bool done = false; - Cursor(MDBX_env* env, MDBX_dbi dbi) { + Cursor(MDBX_env* env, MDBX_dbi dbi, const std::string& index_id) : + index_id_(index_id) { if(mdbx_txn_begin(env, nullptr, MDBX_TXN_RDONLY, &txn) != MDBX_SUCCESS) { throw std::runtime_error("LMDB txn begin failed"); } @@ -118,7 +123,10 @@ class VectorStore { } if(key.iov_len != sizeof(ndd::idInt)) { - printf("Invalid key size: %zu, expected: %zu\n", key.iov_len, sizeof(ndd::idInt)); + LOG_ERROR(1601, + index_id_, + "Invalid key size " << key.iov_len << ", expected " + << sizeof(ndd::idInt)); throw std::runtime_error("Invalid key size in LMDB entry"); } @@ -139,7 +147,7 @@ class VectorStore { } }; - Cursor getCursor() { return Cursor(env_, dbi_); } + Cursor getCursor() { return Cursor(env_, dbi_, index_id_); } void store_vector_bytes(ndd::idInt id, const std::vector& vec) { store_vectors_batch({{id, vec}}); @@ -523,6 +531,7 @@ class MetaStore { // Main storage interface combining vector and meta stores class VectorStorage { private: + std::string index_id_; std::unique_ptr vector_store_; std::unique_ptr meta_store_; @@ -530,12 +539,14 @@ class VectorStorage { std::unique_ptr filter_store_; VectorStorage(const std::string& base_path, + const std::string& index_id, size_t vector_dim, - ndd::quant::QuantizationLevel quant_level) { - vector_store_ = - std::make_unique(base_path + "/vectors", vector_dim, quant_level); + ndd::quant::QuantizationLevel quant_level) : + index_id_(index_id) { + vector_store_ = std::make_unique( + base_path + "/vectors", vector_dim, quant_level, index_id_); meta_store_ = std::make_unique(base_path + "/meta"); - filter_store_ = std::make_unique(base_path + "/filters"); + filter_store_ = std::make_unique(base_path + "/filters", index_id_); } VectorStore::Cursor getCursor() { return vector_store_->getCursor(); } // Get numeric ids of matching filters diff --git a/src/storage/wal.hpp b/src/storage/wal.hpp index 08cebe0c31..c4d94fcef0 100644 --- a/src/storage/wal.hpp +++ b/src/storage/wal.hpp @@ -10,11 +10,13 @@ #include #include #include "../core/types.hpp" +#include "../utils/log.hpp" enum class WALOperationType : uint8_t { VECTOR_ADD = 1, VECTOR_DELETE = 2, VECTOR_UPDATE = 3 }; class WriteAheadLog { private: + std::string index_id_; std::string log_path_; std::ofstream log_file_; std::mutex file_mutex_; @@ -28,7 +30,8 @@ class WriteAheadLog { ndd::idInt numeric_id; }; - WriteAheadLog(const std::string& index_dir) { + WriteAheadLog(const std::string& index_dir, const std::string& index_id) : + index_id_(index_id) { log_path_ = index_dir + "/wal.bin"; // Open in append mode log_file_.open(log_path_, std::ios::binary | std::ios::app); @@ -37,7 +40,7 @@ class WriteAheadLog { err_string = "Failed to open WAL file: " + log_path_ + " errno: " + std::to_string(errno) + " errcode: " + std::strerror(errno); - LOG_ERROR(err_string); + LOG_ERROR(1401, index_id_, err_string); throw std::runtime_error(err_string); } // Check if WAL has existing entries (no need to count them) @@ -140,4 +143,4 @@ class WriteAheadLog { void disable() { enabled_ = false; } void enable() { enabled_ = true; } -}; \ No newline at end of file +}; diff --git a/src/utils/cpu_compat_check/check_arm_compat.hpp b/src/utils/cpu_compat_check/check_arm_compat.hpp index ba625dba5c..39f6113760 100644 --- a/src/utils/cpu_compat_check/check_arm_compat.hpp +++ b/src/utils/cpu_compat_check/check_arm_compat.hpp @@ -3,6 +3,7 @@ #include #include #include +#include "../log.hpp" #if !defined(__aarch64__) @@ -12,12 +13,12 @@ */ int is_neon_compatible(void) { - printf("ERROR: should not be calling %s\n", __func__); + LOG_ERROR(1701, "Unexpected ARM compatibility probe call to " << __func__); return false; } int is_sve2_compatible(void) { - printf("ERROR: should not be calling %s\n", __func__); + LOG_ERROR(1702, "Unexpected ARM compatibility probe call to " << __func__); return false; } @@ -102,9 +103,9 @@ int is_neon_compatible(void) { int neon_ok = probe_neon(); if(neon_ok) { - printf("LOG: NEON: supported and usable.\n"); + LOG_INFO(1703, "NEON is supported and usable"); } else { - printf("ERROR: NEON: NOT supported.\n"); + LOG_ERROR(1704, "NEON is not supported"); } return neon_ok; } @@ -113,9 +114,9 @@ int is_sve2_compatible(void) { int sve2_ok = probe_sve2(); if(sve2_ok) { - printf("LOG: SVE2: supported and usable.\n"); + LOG_INFO(1705, "SVE2 is supported and usable"); } else { - printf("ERROR: SVE2: NOT supported.\n"); + LOG_ERROR(1706, "SVE2 is not supported"); } return sve2_ok; diff --git a/src/utils/cpu_compat_check/check_avx_compat.hpp b/src/utils/cpu_compat_check/check_avx_compat.hpp index 8044d64e00..8a8dbd868e 100644 --- a/src/utils/cpu_compat_check/check_avx_compat.hpp +++ b/src/utils/cpu_compat_check/check_avx_compat.hpp @@ -2,8 +2,8 @@ #include #include #include +#include "../log.hpp" -static const char* ERROR_CALLING_FUNC = "ERROR: should not be calling %s\n"; static const uint32_t ECX_OSXSAVE_BIT = 27; static const uint32_t ECX_AVX_BIT = 28; @@ -25,32 +25,32 @@ static const uint32_t EDX_AVX512FP16_BIT = 23; */ int check_avx2_support(void) { - printf(ERROR_CALLING_FUNC, __func__); + LOG_ERROR(1710, "Unexpected AVX compatibility probe call to " << __func__); return false; } int check_avx512_support(void) { - printf(ERROR_CALLING_FUNC, __func__); + LOG_ERROR(1711, "Unexpected AVX compatibility probe call to " << __func__); return false; } int check_avx512_fp16_support(void) { - printf(ERROR_CALLING_FUNC, __func__); + LOG_ERROR(1712, "Unexpected AVX compatibility probe call to " << __func__); return false; } int check_avx512_vnni_support(void) { - printf(ERROR_CALLING_FUNC, __func__); + LOG_ERROR(1713, "Unexpected AVX compatibility probe call to " << __func__); return false; } int check_avx512_bw_support(void) { - printf(ERROR_CALLING_FUNC, __func__); + LOG_ERROR(1714, "Unexpected AVX compatibility probe call to " << __func__); return false; } int check_avx512_vpopcntdq_support(void) { - printf(ERROR_CALLING_FUNC, __func__); + LOG_ERROR(1715, "Unexpected AVX compatibility probe call to " << __func__); return false; } @@ -294,12 +294,12 @@ int check_avx2_support(void) { int ret = false; if(!cpu_has_avx2()) { - printf("ERROR: AVX2: not supported by CPU.\n"); + LOG_ERROR(1716, "AVX2 is not supported by the CPU"); goto exit; } if(!os_supports_avx()) { - printf("ERROR: AVX2: CPU supports it, but OS/XSAVE state not enabled (XCR0 XMM/YMM).\n"); + LOG_ERROR(1717, "AVX2 is supported by the CPU but not enabled by the OS"); goto exit; } @@ -307,7 +307,7 @@ int check_avx2_support(void) { // run_one_avx2_instruction(); ret = true; - printf("LOG: AVX2: supported and usable.\n"); + LOG_INFO(1718, "AVX2 is supported and usable"); exit: return ret; } @@ -320,12 +320,12 @@ int check_avx512_support(void) { int ret = false; if(!cpu_has_avx512f()) { - printf("ERROR: AVX-512: not supported by CPU (missing AVX-512F).\n"); + LOG_ERROR(1719, "AVX-512 is not supported by the CPU; missing AVX-512F"); goto exit; } if(!os_supports_avx512_state()) { - printf("ERROR: AVX-512: CPU supports it, but OS/XSAVE state not enabled (XCR0).\n"); + LOG_ERROR(1720, "AVX-512 is supported by the CPU but not enabled by the OS"); goto exit; } @@ -333,7 +333,7 @@ int check_avx512_support(void) { // run_one_avx512_instruction(); ret = true; - printf("LOG: AVX-512: supported and usable.\n"); + LOG_INFO(1721, "AVX-512 is supported and usable"); exit: return ret; } @@ -342,17 +342,17 @@ int check_avx512_fp16_support(void) { int ret = false; if(!is_intel_cpu()) { - printf("ERROR: AVX-512 FP16: not supported on non-Intel CPUs.\n"); + LOG_ERROR(1722, "AVX-512 FP16 is not supported on non-Intel CPUs"); goto exit; } if(!cpu_has_avx512f_and_fp16()) { - printf("ERROR: AVX-512 FP16: not supported by CPU.\n"); + LOG_ERROR(1723, "AVX-512 FP16 is not supported by the CPU"); goto exit; } if(!os_supports_avx512_state()) { - printf("ERROR: AVX-512 FP16: CPU supports it, but OS/XSAVE state not enabled (XCR0).\n"); + LOG_ERROR(1724, "AVX-512 FP16 is supported by the CPU but not enabled by the OS"); goto exit; } @@ -360,7 +360,7 @@ int check_avx512_fp16_support(void) { // run_one_fp16_instruction(); ret = true; - printf("LOG: AVX-512 FP16: supported and usable.\n"); + LOG_INFO(1725, "AVX-512 FP16 is supported and usable"); exit: return ret; } @@ -369,24 +369,24 @@ int check_avx512_vnni_support(void) { int ret = false; if(!cpu_has_avx512f()) { - printf("ERROR: AVX-512 VNNI: not supported (missing AVX-512F).\n"); + LOG_ERROR(1726, "AVX-512 VNNI is not supported; missing AVX-512F"); goto exit; } if(!cpu_has_avx512vnni()) { - printf("ERROR: AVX-512 VNNI: not supported by CPU.\n"); + LOG_ERROR(1727, "AVX-512 VNNI is not supported by the CPU"); goto exit; } if(!os_supports_avx512_state()) { - printf("ERROR: AVX-512 VNNI: CPU supports it, but OS AVX-512 state not enabled (XCR0).\n"); + LOG_ERROR(1728, "AVX-512 VNNI is supported by the CPU but not enabled by the OS"); goto exit; } // run_one_avx512vnni_instruction(); ret = true; - printf("LOG: AVX-512 VNNI: supported and usable.\n"); + LOG_INFO(1729, "AVX-512 VNNI is supported and usable"); exit: return ret; @@ -396,24 +396,24 @@ int check_avx512_bw_support(void) { int ret = false; if(!cpu_has_avx512f()) { - printf("ERROR: AVX-512 BW: not supported (missing AVX-512F).\n"); + LOG_ERROR(1730, "AVX-512 BW is not supported; missing AVX-512F"); goto exit; } if(!cpu_has_avx512bw()) { - printf("ERROR: AVX-512 BW: not supported by CPU.\n"); + LOG_ERROR(1731, "AVX-512 BW is not supported by the CPU"); goto exit; } if(!os_supports_avx512_state()) { - printf("ERROR: AVX-512 BW: CPU supports it, but OS AVX-512 state not enabled (XCR0).\n"); + LOG_ERROR(1732, "AVX-512 BW is supported by the CPU but not enabled by the OS"); goto exit; } // run_one_avx512bw_instruction(); ret = true; - printf("LOG: AVX-512 BW: supported and usable.\n"); + LOG_INFO(1733, "AVX-512 BW is supported and usable"); exit: return ret; @@ -423,25 +423,24 @@ int check_avx512_vpopcntdq_support(void) { int ret = false; if(!cpu_has_avx512f()) { - printf("ERROR: AVX-512 vpopcntdq: not supported (missing AVX-512F).\n"); + LOG_ERROR(1734, "AVX-512 vpopcntdq is not supported; missing AVX-512F"); goto exit; } if(!cpu_has_avx512vpopcntdq()) { - printf("ERROR: AVX-512 vpopcntdq: not supported by CPU.\n"); + LOG_ERROR(1735, "AVX-512 vpopcntdq is not supported by the CPU"); goto exit; } if(!os_supports_avx512_state()) { - printf("ERROR: AVX-512 vpopcntdq: CPU supports it, but OS AVX-512 state not enabled " - "(XCR0).\n"); + LOG_ERROR(1736, "AVX-512 vpopcntdq is supported by the CPU but not enabled by the OS"); goto exit; } // run_one_avx512vpopcntdq_instruction(); ret = true; - printf("LOG: AVX-512 vpopcntdq: supported and usable.\n"); + LOG_INFO(1737, "AVX-512 vpopcntdq is supported and usable"); exit: return ret; diff --git a/src/utils/log.hpp b/src/utils/log.hpp index 11c6cf0c88..5c4b5bbafc 100644 --- a/src/utils/log.hpp +++ b/src/utils/log.hpp @@ -1,12 +1,14 @@ #pragma once +#include #include -#include -#include -#include -#include +#include #include -#include +#include +#include #include +#include +#include +#include // Debug logging macro #ifdef ND_DEBUG @@ -102,14 +104,90 @@ inline std::unordered_map FunctionTimer inline std::mutex FunctionTimer::mutex; #endif -#define LOG_STREAM(level, msg) \ +// Production logs share one formatter so every call site emits stable operational output. +namespace ndd::log { +constexpr int kNoCode = -1; + +struct Context { + std::string username{"-"}; + std::string index_name{"-"}; +}; + +// Logs always render username/index_name, using "-" placeholders when scope is missing. +inline std::string normalizeContextPart(std::string value) { + if(value.empty()) { + return "-"; + } + return value; +} + +inline Context makeContext(const std::string& username, const std::string& index_name) { + return {normalizeContextPart(username), normalizeContextPart(index_name)}; +} + +inline Context makeUserContext(const std::string& username) { return makeContext(username, "-"); } + +inline Context makeGlobalContext() { return makeContext("-", "-"); } + +inline Context contextFromIndexId(const std::string& index_id) { + const size_t slash_pos = index_id.find('/'); + if(slash_pos == std::string::npos) { + return makeGlobalContext(); + } + + return makeContext(index_id.substr(0, slash_pos), index_id.substr(slash_pos + 1)); +} + +inline Context contextFromString(const std::string& context) { + if(context.empty() || context == "-" || context == "-/-") { + return makeGlobalContext(); + } + if(context.find('/') != std::string::npos) { + return contextFromIndexId(context); + } + return makeUserContext(context); +} + +inline std::string formatContext(const Context& context) { + return normalizeContextPart(context.username) + "/" + + normalizeContextPart(context.index_name); +} + +// Prefixes are either LEVEL_code for explicit codes or LEVEL for intentional code-less logs. +inline void emit(const char* level, int code, const Context& context, const std::string& message) { + std::cerr << level; + if(code != kNoCode) { + std::cerr << "_" << code; + } + std::cerr << ": " << formatContext(context) << ": " << message << std::endl; +} +} // namespace ndd::log + +#define NDD_LOG_EMIT(level, code, context, msg) \ do { \ std::stringstream __log_ss__; \ __log_ss__ << msg; \ - std::cerr << "[" << level << "] " << __FILE__ << ":" << __LINE__ << " - " \ - << __log_ss__.str() << std::endl; \ + ndd::log::emit(level, code, context, __log_ss__.str()); \ } while(0) -#define LOG_INFO(msg) LOG_STREAM("INFO", msg) -#define LOG_WARN(msg) LOG_STREAM("WARN", msg) -#define LOG_ERROR(msg) LOG_STREAM("ERROR", msg) \ No newline at end of file +// Arity dispatch keeps the public macros simple while selecting global, user, index, or explicit context. +#define NDD_LOG_1(level, msg) \ + NDD_LOG_EMIT(level, ndd::log::kNoCode, ndd::log::makeGlobalContext(), msg) + +#define NDD_LOG_2(level, code, msg) \ + NDD_LOG_EMIT(level, code, ndd::log::makeGlobalContext(), msg) + +#define NDD_LOG_3(level, code, context, msg) \ + NDD_LOG_EMIT(level, code, ndd::log::contextFromString(context), msg) + +#define NDD_LOG_4(level, code, username, index_name, msg) \ + NDD_LOG_EMIT(level, code, ndd::log::makeContext(username, index_name), msg) + +#define NDD_LOG_PICK(_1, _2, _3, _4, NAME, ...) NAME + +#define LOG_INFO(...) \ + NDD_LOG_PICK(__VA_ARGS__, NDD_LOG_4, NDD_LOG_3, NDD_LOG_2, NDD_LOG_1)("INFO", __VA_ARGS__) +#define LOG_WARN(...) \ + NDD_LOG_PICK(__VA_ARGS__, NDD_LOG_4, NDD_LOG_3, NDD_LOG_2, NDD_LOG_1)("WARN", __VA_ARGS__) +#define LOG_ERROR(...) \ + NDD_LOG_PICK(__VA_ARGS__, NDD_LOG_4, NDD_LOG_3, NDD_LOG_2, NDD_LOG_1)("ERROR", __VA_ARGS__) From 96ed5a3665f7e5069adbf529e5c4e672529f6e38 Mon Sep 17 00:00:00 2001 From: shaleenji Date: Tue, 10 Mar 2026 13:37:16 +0000 Subject: [PATCH 33/48] Sparse performance enhancement (#54) --- CMakeLists.txt | 66 +- docs/mdbx-instrumentation.md | 74 ++ docs/sparse.md | 388 +++++++ src/core/ndd.hpp | 14 +- src/main.cpp | 37 +- src/sparse/bmw.hpp | 1800 ---------------------------- src/sparse/inverted_index.cpp | 2062 +++++++++++++++++++++++++++++++++ src/sparse/inverted_index.hpp | 339 ++++++ src/sparse/sparse_storage.hpp | 189 ++- src/sparse/sparse_vector.hpp | 74 +- src/utils/settings.hpp | 36 +- third_party/mdbx/mdbx.c | 279 +++-- third_party/mdbx/mdbx.h | 7 + 13 files changed, 3306 insertions(+), 2059 deletions(-) create mode 100644 docs/mdbx-instrumentation.md create mode 100644 docs/sparse.md delete mode 100644 src/sparse/bmw.hpp create mode 100644 src/sparse/inverted_index.cpp create mode 100644 src/sparse/inverted_index.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e76e5ba9fd..a43890e67f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,13 +80,25 @@ if(ND_DEBUG) add_definitions(-DND_DEBUG) endif() +# Sparse instrumentation can be enabled independently from ND_DEBUG. +option(ND_SPARSE_INSTRUMENT "Enable sparse index timing instrumentation" OFF) +if(ND_SPARSE_INSTRUMENT) + add_definitions(-DND_SPARSE_INSTRUMENT) +endif() + +# MDBX instrumentation can be enabled independently from ND_DEBUG. +option(ND_MDBX_INSTRUMENT "Enable MDBX timing instrumentation" OFF) +if(ND_MDBX_INSTRUMENT) + add_definitions(-DND_MDBX_INSTRUMENT) +endif() + # SIMD Optimization Options option(USE_AVX512 "Enable AVX512 (F, BW, VNNI, FP16)" OFF) option(USE_AVX2 "Enable AVX2 (FMA, F16C)" OFF) option(USE_SVE2 "Enable SVE2 (INT8/16, FP16)" OFF) option(USE_NEON "Enable NEON (FP16, DotProd)" OFF) -option(NDD_BMW_STORE_FLOAT_VALUES "Store raw float 32 values in BMW index (no quantization)" OFF) +option(NDD_INV_IDX_STORE_FLOATS "Store raw float 32 values in sparse index (no quantization)" OFF) # Check if any SIMD option is selected if(NOT USE_AVX512 AND NOT USE_AVX2 AND NOT USE_SVE2 AND NOT USE_NEON) @@ -238,10 +250,22 @@ endif() message(STATUS "Binary name: ${NDD_BINARY_NAME}") +# Add new src/*.cpp files here when they should be compiled into ndd. +set(NDD_CORE_SOURCES + src/sparse/inverted_index.cpp +) +# Build non-main project sources separately so they can be compiled in parallel +# and linked into the final executable at the end. +add_library(ndd_core OBJECT ${NDD_CORE_SOURCES}) -# Create the target -add_executable(${NDD_BINARY_NAME} src/main.cpp ${LMDB_SOURCES} third_party/roaring_bitmap/roaring.c) +# Create the final binary target. +add_executable(${NDD_BINARY_NAME} + src/main.cpp + $ + ${LMDB_SOURCES} + third_party/roaring_bitmap/roaring.c +) # Set MDBX-specific compile flags set_source_files_properties(${LMDB_SOURCES} PROPERTIES @@ -249,6 +273,21 @@ set_source_files_properties(${LMDB_SOURCES} PROPERTIES ) # Include directories +target_include_directories(ndd_core PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${CMAKE_CURRENT_SOURCE_DIR}/src/server + ${CMAKE_CURRENT_SOURCE_DIR}/src/core + ${CMAKE_CURRENT_SOURCE_DIR}/src/storage + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils + ${CMAKE_CURRENT_SOURCE_DIR}/third_party + ${CROW_INCLUDE_DIR} + ${MSGPACK_INCLUDE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/src/roaring + ${LMDB_INCLUDE_DIR} + ${ASIO_INCLUDE_DIR} + ${OPENSSL_INCLUDE_DIR} + ${CURL_INCLUDE_DIRS} +) target_include_directories(${NDD_BINARY_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/server @@ -267,37 +306,54 @@ target_include_directories(${NDD_BINARY_NAME} PRIVATE # Set compiler flags if(NOT DEBUG) + target_compile_options(ndd_core PRIVATE -O3 -ffast-math -fno-finite-math-only) target_compile_options(${NDD_BINARY_NAME} PRIVATE -O3 -ffast-math -fno-finite-math-only) endif() # Apply Flags based on selection if(USE_AVX512) message(STATUS "SIMD: AVX512 enabled (F, BW, VNNI, FP16)") + target_compile_options(ndd_core PRIVATE -mavx512f -mavx512bw -mavx512vnni -mavx512fp16 -mavx512vpopcntdq) + target_compile_definitions(ndd_core PRIVATE USE_AVX512) target_compile_options(${NDD_BINARY_NAME} PRIVATE -mavx512f -mavx512bw -mavx512vnni -mavx512fp16 -mavx512vpopcntdq) target_compile_definitions(${NDD_BINARY_NAME} PRIVATE USE_AVX512) elseif(USE_AVX2) message(STATUS "SIMD: AVX2 enabled") + target_compile_options(ndd_core PRIVATE -mavx2 -mfma -mf16c) + target_compile_definitions(ndd_core PRIVATE USE_AVX2) target_compile_options(${NDD_BINARY_NAME} PRIVATE -mavx2 -mfma -mf16c) target_compile_definitions(${NDD_BINARY_NAME} PRIVATE USE_AVX2) elseif(USE_SVE2) message(STATUS "SIMD: SVE2 enabled (ARMv8.6-a + SVE2 + FP16 + DotProd)") + target_compile_options(ndd_core PRIVATE -march=armv8.6-a+sve2+fp16+dotprod) + target_compile_definitions(ndd_core PRIVATE USE_SVE2) target_compile_options(${NDD_BINARY_NAME} PRIVATE -march=armv8.6-a+sve2+fp16+dotprod) target_compile_definitions(${NDD_BINARY_NAME} PRIVATE USE_SVE2) elseif(USE_NEON) message(STATUS "SIMD: NEON enabled") if(APPLE AND (CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "aarch64")) + target_compile_options(ndd_core PRIVATE -mcpu=native) target_compile_options(${NDD_BINARY_NAME} PRIVATE -mcpu=native) else() + target_compile_options(ndd_core PRIVATE -march=armv8.2-a+fp16+fp16fml+dotprod) target_compile_options(${NDD_BINARY_NAME} PRIVATE -march=armv8.2-a+fp16+fp16fml+dotprod) endif() + target_compile_definitions(ndd_core PRIVATE USE_NEON) target_compile_definitions(${NDD_BINARY_NAME} PRIVATE USE_NEON) endif() -if(NDD_BMW_STORE_FLOAT_VALUES) - target_compile_definitions(${NDD_BINARY_NAME} PRIVATE NDD_BMW_STORE_FLOAT_VALUES) +if(NDD_INV_IDX_STORE_FLOATS) + target_compile_definitions(ndd_core PRIVATE NDD_INV_IDX_STORE_FLOATS) + target_compile_definitions(${NDD_BINARY_NAME} PRIVATE NDD_INV_IDX_STORE_FLOATS) endif() # Add ASIO definitions +target_compile_definitions(ndd_core PRIVATE + ASIO_STANDALONE + ASIO_HAS_STD_CHRONO + ASIO_HAS_STD_STRING_VIEW + MDB_MAXKEYSIZE=512 +) target_compile_definitions(${NDD_BINARY_NAME} PRIVATE ASIO_STANDALONE ASIO_HAS_STD_CHRONO diff --git a/docs/mdbx-instrumentation.md b/docs/mdbx-instrumentation.md new file mode 100644 index 0000000000..a8544cbdc5 --- /dev/null +++ b/docs/mdbx-instrumentation.md @@ -0,0 +1,74 @@ +# MDBX Timing Instrumentation + +## Overview + +Including a built-in timing instrumentation for libmdbx operations. When enabled, every MDBX API call (transactions, gets, puts, cursor operations, etc.) is timed using monotonic clocks, and cumulative statistics are collected in a thread-safe global table. + +## Enabling Instrumentation + +Set the `ND_MDBX_INSTRUMENT` CMake option at configure time: + +```bash +cmake -DND_MDBX_INSTRUMENT=ON .. +``` + +This defines the `ND_MDBX_INSTRUMENT` preprocessor macro, which activates the timing code inside `third_party/mdbx/mdbx.c`. When the option is `OFF` (the default), all instrumentation macros compile to no-ops and `print_mdbx_stats()` is an empty function, so there is zero overhead in production builds. + +## What Gets Timed + +The following MDBX operations are individually tracked: + +| Command | Description | +|---|---| +| `mdbx_env_create` | Environment creation | +| `mdbx_env_open` | Environment open | +| `mdbx_env_close` | Environment close | +| `mdbx_txn_begin` | Transaction begin | +| `mdbx_txn_commit` | Transaction commit | +| `mdbx_txn_abort` | Transaction abort | +| `mdbx_dbi_open` | Database handle open | +| `mdbx_get` | Key lookup | +| `mdbx_put` | Key/value write | +| `mdbx_del` | Key delete | +| `mdbx_cursor_open` | Cursor open | +| `mdbx_cursor_close` | Cursor close | +| `mdbx_cursor_get` | Cursor read | +| `mdbx_cursor_put` | Cursor write | +| `mdbx_cursor_del` | Cursor delete | + +## Reading the Stats + +Call `print_mdbx_stats()` to dump the accumulated statistics to `stderr` and reset all counters. Each line looks like: + +``` +[MDBX_STATS] mdbx_get count=4821 total_ms=12.345 avg_us=2.561 +``` + +- **count** — number of calls since the last `print_mdbx_stats()` call +- **total_ms** — cumulative wall-clock time in milliseconds +- **avg_us** — average time per call in microseconds + +If no MDBX commands were recorded since the last call, it prints: + +``` +[MDBX_STATS] no recorded commands +``` + +### Where it's called + +Currently the health-check endpoint (`GET /api/v1/health`) calls `print_mdbx_stats()` alongside the other debug stat printers. Hit the health endpoint to flush stats to stderr: + +```bash +curl http://localhost:/api/v1/health +``` + +You can also call `print_mdbx_stats()` from anywhere in the codebase — it is declared in `mdbx.h` and is always safe to call (it's a no-op when instrumentation is disabled). + +## How It Works + +Each instrumented MDBX function uses two macros: + +1. **`MDBX_DEBUG_STATS_SCOPE(cmd)`** — placed at the top of the function; captures the start timestamp via `osal_monotime()`. +2. **`MDBX_DEBUG_STATS_RETURN(cmd, value)`** — used in place of `return`; computes the elapsed time and records it before returning. + +The stats are stored in a mutex-protected global table. `print_mdbx_stats()` atomically snapshots and resets the table, so each call shows the delta since the previous call. \ No newline at end of file diff --git a/docs/sparse.md b/docs/sparse.md new file mode 100644 index 0000000000..60cc14aea9 --- /dev/null +++ b/docs/sparse.md @@ -0,0 +1,388 @@ +# Sparse Vector Search + +## What this subsystem does + +This is a sparse vector similarity search engine. Given a collection of documents — each represented as a sparse vector of `(term_id, weight)` pairs — it answers "which documents have the highest dot-product with this query vector?" efficiently. + +It stores everything in MDBX (an embedded key-value database, similar to LMDB). There are two layers: + +1. **Raw document store** — the source-of-truth sparse vectors, one row per document. +2. **Inverted index** — a derived structure that maps each term to the list of documents containing it, organized into fixed-size blocks for efficient streaming. + +Both layers live in the same MDBX environment and are updated atomically within a single transaction. + +## File map + +| File | What it does | +|---|---| +| [sparse_vector.hpp](src/sparse/sparse_vector.hpp) | `SparseVector` struct: holds `(term_id, weight)` pairs, packs/unpacks them to a compact binary format | +| [sparse_storage.hpp](src/sparse/sparse_storage.hpp) | `SparseVectorStorage`: public API — open the DB, store/delete/search vectors, manage transactions | +| [inverted_index.hpp](src/sparse/inverted_index.hpp) | `InvertedIndex` class declaration, on-disk structs (`BlockHeader`, `PostingListHeader`), iterator | +| [inverted_index.cpp](src/sparse/inverted_index.cpp) | All the logic — search algorithm, block merge/save/load, quantization, SIMD helpers, pruning | + +## Data types + +`ndd::idInt` is `uint32_t` — this is the document ID type used throughout. + +## SparseVector + +A sparse vector is just two parallel arrays: + +```cpp +struct SparseVector { + std::vector indices; // term IDs, sorted ascending + std::vector values; // weights, aligned with indices +}; +``` + +### Packed binary format + +When stored in MDBX, vectors are packed as: + +``` +[nr_nonzero : uint16_t] [term_ids : nr_nonzero × uint32_t] [values : nr_nonzero × fp16] +``` + +- `nr_nonzero` is `uint16_t`, so max 65535 terms per vector. +- Values are stored as IEEE FP16 (half-precision float) in the raw document table. Conversion is done inline (`float_to_fp16` / `fp16_to_float`). +- Constructor `SparseVector(const uint8_t*, size_t)` unpacks; `pack()` repacks. + +## SparseVectorStorage — the public API + +This is the class users interact with. It wraps the MDBX environment and exposes: + +### Initialization + +```cpp +SparseVectorStorage storage("/path/to/db"); +storage.initialize(); // opens MDBX env, creates DBIs, loads term cache +``` + +MDBX is opened with flags `MDBX_NOSTICKYTHREADS | MDBX_NORDAHEAD | MDBX_LIFORECLAIM`, max size 1TB, max 10 named databases. + +Two named databases (DBIs) are created: +- `sparse_docs` — raw vector store, keyed by `doc_id` (integer key) +- `blocked_term_postings` — inverted index blocks, keyed by packed `(term_id, block_nr)` (integer key) + +### Storing vectors + +```cpp +// Single insert via transaction +auto txn = storage.begin_transaction(); +txn->store_vector(doc_id, sparse_vec); +txn->commit(); + +// Batch insert (preferred — fewer transactions) +storage.store_vectors_batch({{doc_id1, vec1}, {doc_id2, vec2}, ...}); +``` + +Insert order: raw vector is written to `sparse_docs` first, then the inverted index is updated. Both happen in the same MDBX write transaction. + +### Deleting vectors + +```cpp +storage.delete_vector(doc_id); +// or via transaction: +txn->delete_vector(doc_id); +``` + +Delete order is reversed: read the raw vector, remove its terms from the inverted index, then delete the raw vector row. + +### Searching + +```cpp +auto results = storage.search(query_vec, k); +// returns vector> sorted by score descending + +// With a filter (only consider docs in the bitmap): +auto results = storage.search(query_vec, k, &roaring_filter); +``` + +### Concurrency + +`SparseVectorStorage` has a `shared_mutex`: +- Writes (`store_vectors_batch`, `delete_vector`) take an exclusive lock. +- Search is delegated directly to `InvertedIndex`, which has its own `shared_mutex` (shared for search, exclusive for add/remove). + +MDBX transactions and cursors are single-threaded — the search loop is not parallelized. + +## Inverted index internals + +### Key scheme + +Every row in `blocked_term_postings` has a `uint64_t` key: + +``` +packed_key = (uint64(term_id) << 32) | uint64(block_nr) +``` + +This puts all rows for one term next to each other in MDBX's sorted key order, so you can seek to `(term_id, 0)` and iterate forward. + +Reserved keys: +- `block_nr = UINT32_MAX` → this row is the **metadata row** for the term (stores `PostingListHeader`) +- `(term_id = UINT32_MAX, block_nr = 0)` → the **superblock** — a single row storing `SuperBlock` (format version metadata) +- `term_id = UINT32_MAX` is otherwise a reserved sentinel, rejected by all code paths + +### Blocks + +Documents are partitioned into fixed-size blocks: + +``` +block_nr = doc_id / 65535 +block_offset = doc_id % 65535 (uint16_t) +``` + +`kBlockCapacity = 65535` (`std::numeric_limits::max()`). This means block offsets fit in 16 bits. + +Each MDBX row for `(term_id, block_nr)` stores exactly the postings from that term that fall into that block's doc-id range. This is the fundamental design choice — writes are block-local merges, not whole-list rewrites. + +### SuperBlock + +A single metadata row stored at key `packPostingKey(UINT32_MAX, 0)`: + +```cpp +struct SuperBlock { // 1 byte, packed + uint8_t format_version; // must match settings::SPARSE_ONDISK_VERSION +}; +``` + +On `initialize()`, the inverted index calls `validateSuperBlock()` which: +1. Reads the superblock row. +2. If not found and the DB is empty (fresh) → writes a new superblock with `format_version = settings::SPARSE_ONDISK_VERSION`. +3. If not found but the DB has existing rows → throws `std::runtime_error` (legacy incompatible database). +4. If found but `format_version != settings::SPARSE_ONDISK_VERSION` → throws `std::runtime_error` (version mismatch). + +This key doesn't interfere with normal iteration: `loadTermInfo()` filters out `term_id == UINT32_MAX`, and `iterateTermBlocks()` seeks to specific term IDs that are never `UINT32_MAX`. + +### Per-term metadata: PostingListHeader + +Stored at key `(term_id, UINT32_MAX)`: + +```cpp +struct PostingListHeader { // 12 bytes, packed + uint32_t nr_entries; // total entries across all blocks (including tombstones) + uint32_t nr_live_entries; // entries with value > 0 + float max_value; // global max weight across all blocks +}; +``` + +### Per-block payload + +Stored at key `(term_id, block_nr)`: + +``` +[BlockHeader] [doc_offsets: n × uint16_t] [values: n × uint8_t (or float)] +``` + +```cpp +struct BlockHeader { // 8 bytes, packed + uint16_t nr_entries; + uint16_t nr_live_entries; + float max_value; // block-local max weight +}; +``` + +- `doc_offsets[]` are sorted `uint16_t` values — the offset within the block (`doc_id % 65535`). +- `values[]` are the posting weights, either `uint8_t` (default, quantized) or `float` (when `NDD_INV_IDX_STORE_FLOATS` is defined). + +### Quantization + +By default, weights are quantized to `uint8_t` relative to the block's max value: + +``` +quantize(val, max_val) = round(val / max_val * 255) + clamped to [1, 255] ← 0 means deleted (tombstone) + +dequantize(val, max_val) = val * (max_val / 255) +``` + +The value `0` is reserved as a tombstone marker — it means the entry has been deleted but not yet compacted out of the block. + +If `NDD_INV_IDX_STORE_FLOATS` is defined at compile time, values are stored as raw `float` and no quantization happens. The value `0.0f` (or ≤ 0) is still the tombstone. + +### In-memory cache: term_info_ + +```cpp +std::unordered_map term_info_; // term_id → global max weight +``` + +Populated at startup by `loadTermInfo()`, which scans all metadata rows. Updated incrementally during add/remove. Used by search to: +1. Skip query terms that don't exist in the index. +2. Compute upper bounds for pruning (`upper_bound = global_max * query_weight`). + +## Write path + +### Batch insert: addDocumentsBatchInternal() + +Given a batch of `(doc_id, SparseVector)` pairs: + +1. **Pivot to term-major order.** Build a map: `term_id → [(doc_id, value), ...]`. + +2. **For each term:** + - Sort updates by `doc_id`, deduplicate (keep last value for duplicate doc_ids). + - Split into sub-ranges by `block_nr`. + - For each `(term_id, block_nr)` slice: + - `loadBlockEntries()` — read and decode the existing block (if any) into a `vector`. + - Merge the existing entries and incoming updates as two sorted streams (classic merge-sort merge). + - Recompute `new_live_count` and `new_block_max`. + - `saveBlockEntries()` — serialize and write the merged block back to MDBX (or delete the block if empty). + - Update the `PostingListHeader`: + - Adjust `nr_entries` and `nr_live_entries` using deltas. + - If the old global max might have been invalidated (the block that held it now has a lower max), call `recomputeGlobalMaxFromBlocks()` — a full scan of that term's block headers. + - Update `term_info_`. + +### Single delete: removeDocumentInternal() + +For each term in the deleted vector: + +1. Read the term's `PostingListHeader`. +2. Compute which `block_nr` the doc falls in. +3. `loadBlockEntries()` for that block. +4. Binary search for the `doc_id`. +5. Set its value to `0.0f` (tombstone). +6. If the tombstone ratio exceeds `INV_IDX_COMPACTION_TOMBSTONE_RATIO` (default 10%), compact the block in-place (remove all tombstones). +7. Recompute block stats, save or delete the block. +8. Update the `PostingListHeader` and `term_info_`. + +## Search path + +### Overview + +Search computes the top-k documents by dot-product score with the query vector. It works by streaming through posting lists in doc-id order, accumulating scores in a dense buffer, and maintaining a min-heap of the best results. + +### Phase 1: Build iterators + +For each query term `(term_id, query_weight)`: +- Skip if `query_weight ≤ 0` or term not in `term_info_`. +- Read the term's `PostingListHeader`; skip if no live entries. +- Open an MDBX cursor. +- Create a `PostingListIterator` and call `init()`: + - Seeks to the first block for this term. + - Positions on the first live (non-tombstone) entry. +- If the iterator is not exhausted, keep it. + +### Phase 2: Batch scoring loop + +Search processes the doc-id space in windows of size `INV_IDX_SEARCH_BATCH_SZ` (default 10,000, configurable via `NDD_INV_IDX_SEARCH_BATCH_SZ` env var): + +``` +batch_start = min doc_id across all active iterators +batch_end = batch_start + batch_size - 1 +``` + +A dense `float` array `scores_buf[batch_size]` is zeroed. Then for each iterator: + +**`accumulateBatchScores()`** — the hot inner loop: +- Walk through the current block's `doc_offsets[]` and `values[]`. +- For each live entry within `[batch_start, batch_end]`: + - `local = block_base_doc_id - batch_start + offset` + - `scores_buf[local] += dequantized_value * query_weight` +- When the current block is exhausted, `loadNextBlock()` and continue if still in range. +- Track `remaining_entries` for pruning. + +### Phase 3: Extract top-k from batch + +Scan `scores_buf`. For each non-zero score above the current threshold: +- Reconstruct `doc_id = batch_start + local`. +- If a RoaringBitmap filter exists, skip docs not in the filter. +- Push into a min-heap of size `k`. Update the threshold to the heap's minimum score. + +### Phase 4: Compact and prune + +- Remove exhausted iterators. +- If the heap is full and pruning is enabled (more than 1 iterator): + - `pruneLongest()` finds the iterator with the most `remaining_entries`. + - Computes `upper_bound = global_max * query_weight` for that iterator. + - If `upper_bound ≤ current_threshold`, advance that iterator forward to the minimum doc_id among the *other* iterators (skipping a chunk of its posting list that can't contribute winners). + - Only one list is pruned at a time. + +### Finish + +Close all cursors, abort the read-only transaction. Pop the heap into a vector and reverse it so results are in descending score order. + +## PostingListIterator + +A cursor-backed streaming iterator over one term's posting list. It never loads the entire list into memory — it reads one block at a time via zero-copy MDBX pointers. + +Key methods: + +| Method | What it does | +|---|---| +| `init()` | Seek to first block, position on first live entry | +| `loadFirstBlock()` | `MDBX_SET_RANGE` to `(term_id, 0)`, skip empty blocks | +| `loadNextBlock()` | `MDBX_NEXT`, stop when term_id changes or metadata row reached | +| `parseCurrentKV()` | Validate key, parse block payload into `BlockView`, set up zero-copy pointers | +| `advanceToNextLive()` | Skip tombstones in current block (uses SIMD for uint8 mode), load next block if needed | +| `next()` | Move to next live entry | +| `advance(target_doc_id)` | Block-aware seek — skip whole blocks if target is ahead, `lower_bound` within a block | +| `valueAt(idx)` | Dequantize and return the weight at position `idx` | +| `upperBound()` | `global_max * term_weight` — used for pruning decisions | + +### BlockView + +A zero-copy view into an MDBX value. Pointers are only valid while the cursor stays on the same record and the transaction is alive. + +```cpp +struct BlockView { + const uint16_t* doc_offsets; // sorted block-local offsets + const void* values; // uint8_t* or float* depending on mode + uint32_t count; + uint8_t value_bits; // 8 or 32 + float max_value; // block-local max (needed for dequantization) +}; +``` + +## SIMD helpers + +Two SIMD-accelerated functions with implementations for AVX-512, AVX2, NEON, and SVE2 (plus scalar fallback): + +- **`findNextLiveSIMD(values, size, start_idx)`** — finds the next non-zero byte in a `uint8_t` array. Used by `advanceToNextLive()` to skip tombstones quickly. +- **`findDocIdSIMD(doc_ids, size, start_idx, target)`** — finds the first `uint32_t` ≥ target. Currently not used by the main search path but available. + +## Compile-time flags + +| Flag | Effect | +|---|---| +| `NDD_INV_IDX_STORE_FLOATS` | Store block values as `float` instead of `uint8_t`. No quantization. | +| `ND_SPARSE_INSTRUMENT` | Enable timing instrumentation for search and update paths. Call `printSparseSearchDebugStats()` / `printSparseUpdateDebugStats()` to dump. | +| `NDD_INV_IDX_PRUNE_DEBUG` | Track how many entries each iterator skipped via pruning. Logged after search. | + +## Runtime settings + +| Setting | Default | Description | +|---|---|---| +| `INV_IDX_SEARCH_BATCH_SZ` | 10,000 | Size of the dense scoring window. Configurable via `NDD_INV_IDX_SEARCH_BATCH_SZ` env var. | +| `INV_IDX_COMPACTION_TOMBSTONE_RATIO` | 0.10 | When this fraction of a block's entries are tombstones during delete, compact in-place. | +| `NEAR_ZERO` | 1e-9 | Epsilon for float comparisons. | +| `SPARSE_ONDISK_VERSION` | 1 | Format version written to the superblock. Checked on load; mismatch throws. | + +## Putting it all together — data flow diagram + +``` + User code + │ + ┌──────────▼──────────┐ + │ SparseVectorStorage │ ← public API, owns MDBX env + │ shared_mutex │ + └──┬──────────────┬───┘ + │ │ + ┌──────────▼──┐ ┌──────▼──────────┐ + │ sparse_docs │ │ InvertedIndex │ ← owns blocked_term_postings DBI + │ (MDBX DBI) │ │ shared_mutex │ + │ │ │ term_info_ │ ← in-memory cache + │ doc_id → │ └──┬──────────┬───┘ + │ packed vec │ │ │ + └─────────────┘ write search + │ │ + ┌─────────▼─┐ ┌─────▼──────────────┐ + │ term-major │ │ PostingListIterator │ + │ block-local│ │ (cursor-backed, │ + │ merge │ │ zero-copy blocks) │ + └────────────┘ └─────────────────────┘ +``` + +# Potential Performance Improvements + +Several ideas could further improve performance: + +1. Allow a configurable fraction of the lowest-weight query terms to be ignored at search time. This reduces the number of posting-list iterators, which can improve latency while preserving most of the recall. Users can tune the fraction on a per-query basis to balance speed and accuracy. diff --git a/src/core/ndd.hpp b/src/core/ndd.hpp index 98f2f48efa..4394252040 100644 --- a/src/core/ndd.hpp +++ b/src/core/ndd.hpp @@ -623,7 +623,7 @@ class IndexManager { std::unique_ptr sparse_storage = nullptr; if(config.sparse_dim > 0) { std::string sparse_storage_dir = index_dir + "/sparse"; - sparse_storage = std::make_unique(sparse_storage_dir); + sparse_storage = std::make_unique(sparse_storage_dir, index_id); if(!sparse_storage->initialize()) { throw std::runtime_error("Failed to initialize sparse storage"); } @@ -737,7 +737,7 @@ class IndexManager { std::unique_ptr sparse_storage; if(sparse_dim > 0) { std::string sparse_storage_dir = index_dir + "/sparse"; - sparse_storage = std::make_unique(sparse_storage_dir); + sparse_storage = std::make_unique(sparse_storage_dir, index_id); if(!sparse_storage->initialize()) { throw std::runtime_error("Failed to initialize sparse storage for index: " + index_id); @@ -980,8 +980,8 @@ class IndexManager { // Calculate start and end indices for this thread size_t start_idx = t * chunk_size; size_t end_idx = (start_idx + chunk_size < quantized_vectors.size()) - ? (start_idx + chunk_size) - : quantized_vectors.size(); + ? (start_idx + chunk_size) + : quantized_vectors.size(); // Process assigned chunk of vectors for(size_t i = start_idx; i < end_idx; i++) { @@ -1185,7 +1185,9 @@ class IndexManager { // Remove the filter entry.vector_storage->deleteFilter(numeric_id, meta.filter); // Mark as deleted in HNSW index + entry.alg->markDelete(numeric_id); + // Delete from sparse storage if hybrid index if(entry.sparse_storage) { entry.sparse_storage->delete_vector(numeric_id); @@ -1333,7 +1335,7 @@ class IndexManager { // 0. Compute Filter Bitmap (Shared) std::optional active_filter_bitmap; if (!filter_array.empty()) { - active_filter_bitmap = entry.vector_storage->filter_store_->computeFilterBitmap(filter_array); + active_filter_bitmap = entry.vector_storage->filter_store_->computeFilterBitmap(filter_array); } // 1. Sparse Search (Async) @@ -1374,7 +1376,7 @@ class IndexManager { ndd::quant::get_quantizer_dispatch(quant_level).quantize(query); if (!active_filter_bitmap) { - dense_results = entry.alg->searchKnn(query_bytes.data(), k, ef); + dense_results = entry.alg->searchKnn(query_bytes.data(), k, ef); } else { // Smart Filter Execution Strategy auto& bitmap = *active_filter_bitmap; diff --git a/src/main.cpp b/src/main.cpp index d39cc15457..c51ec54cc1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -31,6 +31,8 @@ // local includes #include "settings.hpp" +#include "mdbx/mdbx.h" +#include "sparse/inverted_index.hpp" #include "core/ndd.hpp" #include "auth.hpp" #include "quant/common.hpp" @@ -241,6 +243,10 @@ int main(int argc, char** argv) { crow::json::wvalue response( {{"status", "ok"}, {"timestamp", std::chrono::system_clock::now().time_since_epoch().count()}}); + PRINT_LOG_TIME(); + ndd::printSparseSearchDebugStats(); + ndd::printSparseUpdateDebugStats(); + print_mdbx_stats(); return crow::response(200, response.dump()); }); @@ -827,14 +833,15 @@ int main(int argc, char** argv) { LOG_DEBUG("Filter: " << filter_array.dump()); try { auto search_response = index_manager.searchKNN(index_id, - query, - sparse_indices, - sparse_values, - k, - filter_array, - filter_params, - include_vectors, - ef); + query, + sparse_indices, + sparse_values, + k, + filter_array, + filter_params, + include_vectors, + ef); + if(!search_response) { LOG_WARN(1038, ctx.username, index_name, "Search request returned no results because the index is missing or search failed"); return json_error(404, "Index not found or search failed"); @@ -1244,17 +1251,7 @@ int main(int argc, char** argv) { return response; }); - unsigned int num_cores = std::thread::hardware_concurrency(); - LOG_INFO(1008, "Number of processor cores: " << num_cores); - if(settings::NUM_SERVER_THREADS == 0) { - // Run on max possible threads - LOG_INFO(1009, "Using all available threads"); - app.port(settings::SERVER_PORT).multithreaded().run(); - } else { - // Limit on the number of threads - LOG_INFO(1010, "Using " << settings::NUM_SERVER_THREADS << " threads"); - app.port(settings::SERVER_PORT).concurrency(settings::NUM_SERVER_THREADS).run(); - } - + LOG_INFO(1008, "Using: " << settings::NUM_SERVER_THREADS << " server threads."); + app.port(settings::SERVER_PORT).concurrency(settings::NUM_SERVER_THREADS).run(); return 0; } diff --git a/src/sparse/bmw.hpp b/src/sparse/bmw.hpp deleted file mode 100644 index 861c523a95..0000000000 --- a/src/sparse/bmw.hpp +++ /dev/null @@ -1,1800 +0,0 @@ -#pragma once - -/** - * This code implements Block-Max WAND search index using MDBX. - * This algorithm is an optimization of the WAND (Weak AND) algorithm - * used to skip large portions of the index that cannot possibly rank in - * the top-K results. - * - * It is designed for high performance retrieval of sparse vector spaces. - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "../core/types.hpp" - -#if defined(__x86_64__) || defined(_M_X64) -# include -#elif defined(__aarch64__) || defined(_M_ARM64) -# include -#endif - -#include "mdbx/mdbx.h" -#include "../utils/log.hpp" -#include "../core/types.hpp" - -#include "sparse_vector.hpp" - -namespace ndd { - -#pragma pack(push, 1) - struct BlockIdx { - ndd::idInt start_doc_id; - float block_max_value; - - BlockIdx() = default; - BlockIdx(ndd::idInt start, float max_val) : - start_doc_id(start), - block_max_value(max_val) {} - }; - - // Block header for term_blocks data - struct BlockHeader { - uint8_t version = 3; // Version 3: SoA layout, uint8 values (quantized) - uint8_t diff_bits = 16; // 16, 32, or 64 bit doc diffs. Default to 16 for compression. - uint16_t n = 0; // total stored (incl. tombstones) - uint16_t live_count = 0; // nonzero entries - uint16_t padding = 0; // explicit padding - float block_max_value = 0.0f; // max value in block (for WAND) - uint32_t alignment_pad = 0; // Ensure 16-byte alignment for payload - - static constexpr size_t HEADER_SIZE = 16; - }; - - // Entry in a block (In-memory representation) - struct BlockEntry { - ndd::idInt doc_diff; // difference from block start_doc_id - float value; // stored as float in memory, quantized to uint8 on disk - - BlockEntry() = default; - BlockEntry(ndd::idInt diff, float val) : - doc_diff(diff), - value(val) {} - - bool operator<(const BlockEntry& other) const { return doc_diff < other.doc_diff; } - }; - -#pragma pack(pop) - - // BMW search candidate - struct BMWCandidate { - ndd::idInt doc_id; - float score; - - BMWCandidate(ndd::idInt id, float s) : - doc_id(id), - score(s) {} - - bool operator<(const BMWCandidate& other) const { - return score > other.score; // Min-heap (lowest scores first) - } - }; - - class BMWIndex { - public: - static constexpr uint8_t CURRENT_VERSION = 3; - - BMWIndex(MDBX_env* env, size_t vocab_size) : - env_(env), - vocab_size_(vocab_size) {} - - ~BMWIndex() = default; - - // Initialize databases - bool initialize() { - std::unique_lock lock(mutex_); - - // Open LMDB databases - MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); - if(rc != 0) { - LOG_ERROR("Failed to begin transaction: " << mdbx_strerror(rc)); - return false; - } - - // Create term_blocks database - rc = mdbx_dbi_open(txn, "term_blocks", MDBX_CREATE, &term_blocks_dbi_); - if(rc != 0) { - LOG_ERROR("Failed to open term_blocks database: " << mdbx_strerror(rc)); - mdbx_txn_abort(txn); - return false; - } - - // Create term_blocks_index database - rc = mdbx_dbi_open(txn, "term_blocks_index", MDBX_CREATE, &term_blocks_index_dbi_); - if(rc != 0) { - LOG_ERROR("Failed to open term_blocks_index database: " << mdbx_strerror(rc)); - mdbx_txn_abort(txn); - return false; - } - - rc = mdbx_txn_commit(txn); - if(rc != 0) { - LOG_ERROR("Failed to commit initialization transaction: " << mdbx_strerror(rc)); - return false; - } - - // Load existing term blocks index - return loadTermBlocksIndex(); - } - - // Document management - bool addDocument(ndd::idInt doc_id, const SparseVector& vec) { - return addDocumentsBatch({{doc_id, vec}}); - } - - bool addDocumentsBatch(const std::vector>& docs) { - if(docs.empty()) { - return true; - } - - std::unique_lock lock(mutex_); - - MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); - if(rc != 0) { - LOG_ERROR("Failed to begin transaction: " << mdbx_strerror(rc)); - return false; - } - - try { - if(!addDocumentsBatchInternal(txn, docs)) { - mdbx_txn_abort(txn); - return false; - } - - rc = mdbx_txn_commit(txn); - if(rc != 0) { - LOG_ERROR("Failed to commit initialization transaction: " << mdbx_strerror(rc)); - return false; - } - - return true; - } catch(const std::exception& e) { - LOG_ERROR("Failed to add documents batch: " << e.what()); - mdbx_txn_abort(txn); - return false; - } - } - - bool removeDocument(ndd::idInt doc_id, const SparseVector& vec) { - std::unique_lock lock(mutex_); - MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); - if(rc != 0) { - return false; - } - - try { - if(!removeDocumentInternal(txn, doc_id, vec)) { - mdbx_txn_abort(txn); - return false; - } - return mdbx_txn_commit(txn) == 0; - } catch(const std::exception& e) { - LOG_ERROR("Failed to remove document: " << e.what()); - mdbx_txn_abort(txn); - return false; - } - } - - bool updateDocument(ndd::idInt doc_id, - const SparseVector& old_vec, - const SparseVector& new_vec) { - std::unique_lock lock(mutex_); - MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); - if(rc != 0) { - return false; - } - - try { - if(!removeDocumentInternal(txn, doc_id, old_vec)) { - mdbx_txn_abort(txn); - return false; - } - - if(!addDocumentsBatchInternal(txn, {{doc_id, new_vec}})) { - mdbx_txn_abort(txn); - return false; - } - - return mdbx_txn_commit(txn) == 0; - } catch(const std::exception& e) { - LOG_ERROR("Failed to update document: " << e.what()); - mdbx_txn_abort(txn); - return false; - } - } - - // Batch operations - Removed empty implementation - - // Transaction-aware methods for external orchestration - bool addDocumentsBatch(MDBX_txn* txn, - const std::vector>& docs) { - std::unique_lock lock(mutex_); - return addDocumentsBatchInternal(txn, docs); - } - - bool removeDocument(MDBX_txn* txn, ndd::idInt doc_id, const SparseVector& vec) { - std::unique_lock lock(mutex_); - return removeDocumentInternal(txn, doc_id, vec); - } - - // Search using BMW algorithm (DAAT - std::vector> search(const SparseVector& query, - size_t k, - const ndd::RoaringBitmap* filter = nullptr) - { - if(query.empty() || k == 0) { - return {}; - } - - std::shared_lock lock(mutex_); - - // Start Read Transaction - MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn); - if(rc != 0) { - LOG_ERROR("Failed to begin search transaction: " << mdbx_strerror(rc)); - return {}; - } - - // Initialize iterators for all query terms - // Use vector for storage to ensure pointer stability (reserve is key) - std::vector iterators_storage; - iterators_storage.reserve(query.indices.size()); - - // Pointers for sorting - std::vector iterators; - iterators.reserve(query.indices.size()); - - for(size_t i = 0; i < query.indices.size(); ++i) { - auto it = term_blocks_index_.find(query.indices[i]); - if(it != term_blocks_index_.end()) { - iterators_storage.emplace_back( - query.indices[i], query.values[i], &it->second, this, txn); - } - } - - // Initialize pointers - for(auto& it : iterators_storage) { - iterators.push_back(&it); - } - - if(iterators.empty()) { - mdbx_txn_abort(txn); - return {}; - } - - std::priority_queue top_k; - float threshold = 0.0f; - - // Helper to sort iterators by current doc ID - auto sort_iterators = [&]() { - if(iterators.size() < 2) { - return; - } - - // Requested bubble sort for iterator ordering by current doc id. - bool swapped; - for(size_t pass = 0; pass + 1 < iterators.size(); ++pass) { - swapped = false; - for(size_t i = 0; i + 1 < iterators.size() - pass; ++i) { - if(iterators[i]->current_doc_id > iterators[i + 1]->current_doc_id) { - std::swap(iterators[i], iterators[i + 1]); - swapped = true; - } - } - if(!swapped) { - break; - } - } - }; - - sort_iterators(); - - float remaining_global_upper_bound = 0.0f; - for(size_t i = 0; i < iterators.size(); ++i) { - remaining_global_upper_bound += iterators[i]->globalUpperBound(); - } - - while(true) { - // Remove exhausted iterators - while(!iterators.empty() - && iterators.back()->current_doc_id - == std::numeric_limits::max()) { - remaining_global_upper_bound -= iterators.back()->globalUpperBound(); - iterators.pop_back(); - } - - if(iterators.empty()) { - break; - } - if(remaining_global_upper_bound < 0.0f) { - remaining_global_upper_bound = 0.0f; - } - if(remaining_global_upper_bound <= threshold) { - break; - } - - // WAND/BMW logic - float upper_bound_sum = 0.0f; - size_t pivot_idx = 0; - bool found_pivot = false; - - // Find pivot term - for(size_t i = 0; i < iterators.size(); ++i) { - upper_bound_sum += iterators[i]->upperBound(); - if(upper_bound_sum > threshold) { - pivot_idx = i; - found_pivot = true; - break; - } - } - - if(!found_pivot) { - // No document can exceed threshold - break; - } - - ndd::idInt pivot_doc_id = iterators[pivot_idx]->current_doc_id; - - if(iterators[0]->current_doc_id == pivot_doc_id) { - if(filter && !filter->contains(pivot_doc_id)) { - // Skip document that doesn't match filter - iterators[0]->next(); - for(size_t i = 1; i < iterators.size(); ++i) { - if(iterators[i]->current_doc_id == pivot_doc_id) { - iterators[i]->next(); - } else { - break; // Since sorted - } - } - sort_iterators(); - continue; - } - - // Pivot is the first iterator, so we have a candidate - iterators[0]->advance(pivot_doc_id); // Should be no-op - float score = iterators[0]->current_score * iterators[0]->term_weight; - iterators[0]->next(); - - // Check other terms - for(size_t i = 1; i < iterators.size(); ++i) { - iterators[i]->advance(pivot_doc_id); - if(iterators[i]->current_doc_id == pivot_doc_id) { - score += iterators[i]->current_score * iterators[i]->term_weight; - iterators[i]->next(); - } - } - - if(top_k.size() < k) { - top_k.emplace(pivot_doc_id, score); - if(top_k.size() == k) { - threshold = top_k.top().score; - } - } else if(score > threshold) { - top_k.pop(); - top_k.emplace(pivot_doc_id, score); - threshold = top_k.top().score; - } - } else { - // Standard WAND/BMW behavior: advance only the first iterator to the pivot. - iterators[0]->advance(pivot_doc_id); - } - sort_iterators(); - } - - // Clean up - mdbx_txn_abort(txn); - - // Extract results - std::vector> results; - results.reserve(top_k.size()); - - while(!top_k.empty()) { - const auto& candidate = top_k.top(); - results.emplace_back(candidate.doc_id, candidate.score); - top_k.pop(); - } - - std::reverse(results.begin(), results.end()); - return results; - } - - // Maintenance - // Functions removed as they were empty/unused placeholders - - bool splitBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt start_doc_id) { - auto& blocks = term_blocks_index_[term_id]; - auto block_it = findBlockIterator(blocks, start_doc_id); - - // Verify we found the correct block - if(block_it == blocks.end() || block_it->start_doc_id != start_doc_id) { - return false; - } - - auto entries = loadBlock(txn, term_id, start_doc_id); - if(entries.size() <= settings::MAX_BMW_BLOCK_SIZE) { - return true; - } - - // Split point (middle) - size_t split_idx = entries.size() / 2; - ndd::idInt new_start_doc_id = start_doc_id + entries[split_idx].doc_diff; - - std::vector first_half(entries.begin(), entries.begin() + split_idx); - std::vector second_half; - second_half.reserve(entries.size() - split_idx); - - // Re-calculate diffs for second half - ndd::idInt base_diff = entries[split_idx].doc_diff; - for(size_t i = split_idx; i < entries.size(); ++i) { - second_half.emplace_back(entries[i].doc_diff - base_diff, entries[i].value); - } - - // Calculate max values for both new blocks - float max1 = 0.0f, max2 = 0.0f; - - for(const auto& e : first_half) { - if(e.value > 0) { - max1 = std::max(max1, e.value); - } - } - for(const auto& e : second_half) { - if(e.value > 0) { - max2 = std::max(max2, e.value); - } - } - - // Update first block metadata - block_it->block_max_value = max1; - - // Save first block - BlockHeader h1; - h1.n = static_cast(first_half.size()); - h1.live_count = 0; - for(const auto& e : first_half) { - if(e.value > 0) { - h1.live_count++; - } - } - h1.block_max_value = max1; - - if(!saveBlock(txn, term_id, start_doc_id, first_half, h1)) { - return false; - } - - // Insert second block metadata - // Note: block_it might be invalidated by insert, so calculate index first - size_t idx = std::distance(blocks.begin(), block_it); - blocks.insert(blocks.begin() + idx + 1, BlockIdx(new_start_doc_id, max2)); - - // Save second block - BlockHeader h2; - h2.n = static_cast(second_half.size()); - h2.live_count = 0; - for(const auto& e : second_half) { - if(e.value > 0) { - h2.live_count++; - } - } - h2.block_max_value = max2; - - if(!saveBlock(txn, term_id, new_start_doc_id, second_half, h2)) { - return false; - } - - return true; - } - - // Statistics - size_t getTermCount() const { - std::shared_lock lock(mutex_); - return term_blocks_index_.size(); - } - - size_t getBlockCount() const { - std::shared_lock lock(mutex_); - size_t total = 0; - for(const auto& [term_id, blocks] : term_blocks_index_) { - if(!blocks.empty() && blocks.front().start_doc_id == GLOBAL_MAX_SENTINEL_DOC_ID) { - total += (blocks.size() - 1); - } else { - total += blocks.size(); - } - } - return total; - } - - size_t getVocabSize() const { return vocab_size_; } - - private: - static constexpr ndd::idInt GLOBAL_MAX_SENTINEL_DOC_ID = 0; - - static size_t firstRealBlockIndex(const std::vector& blocks) { - if(!blocks.empty() && blocks.front().start_doc_id == GLOBAL_MAX_SENTINEL_DOC_ID) { - return 1; - } - return 0; - } - - std::vector::iterator findBlockIterator(std::vector& blocks, - ndd::idInt doc_id) { - size_t first_idx = firstRealBlockIndex(blocks); - if(first_idx >= blocks.size()) { - return blocks.end(); - } - - auto begin_it = blocks.begin() + static_cast(first_idx); - auto it = std::upper_bound(begin_it, - blocks.end(), - doc_id, - [](ndd::idInt doc_id, const BlockIdx& block) { - return doc_id < block.start_doc_id; - }); - - if(it == begin_it) { - return it; - } - return it - 1; - } - - std::vector::const_iterator findBlockIterator(const std::vector& blocks, - ndd::idInt doc_id) const { - size_t first_idx = firstRealBlockIndex(blocks); - if(first_idx >= blocks.size()) { - return blocks.end(); - } - - auto begin_it = blocks.begin() + static_cast(first_idx); - auto it = std::upper_bound(begin_it, - blocks.end(), - doc_id, - [](ndd::idInt doc_id, const BlockIdx& block) { - return doc_id < block.start_doc_id; - }); - - if(it == begin_it) { - return it; - } - return it - 1; - } - - /** - * The quantize and dequantize functions are there to reduce the memory - * and storage footprint of the sparse values (float 32 to int8). - * - * XXX: Here we are assuming that sparse vectors can never have -ve values. - */ - // Helper for uint8 quantization - static inline uint8_t quantize(float val, float max_val) { - if(max_val <= settings::NEAR_ZERO) { - return 0; - } - float scaled = (val / max_val) * UINT8_MAX; - if(scaled >= UINT8_MAX) { - return UINT8_MAX; - } - if (scaled <= 0.0f) return 0; - - return static_cast(scaled + 0.5f); - } - - static inline float dequantize(uint8_t val, float max_val) { - // If max_val is near zero, the result is effectively zero - if (max_val <= settings::NEAR_ZERO) { - return 0.0f; - } - - // Use a single multiplier to avoid multiple floating point ops - const float scale = max_val / UINT8_MAX; - return static_cast(val) * scale; - } - - - // Helper struct for getReadOnlyBlock return value - struct BlockView { - const void* doc_diffs; // Can be uint16_t* or uint32_t* - const void* values; - size_t count; - uint8_t diff_bits; // 16 or 32 - uint8_t value_bits; // 8 (quantized) or 32 (float) - }; - - struct BlockIterator { - uint32_t term_id; - float term_weight; - const std::vector* blocks; - size_t first_block_idx; - size_t current_block_idx; - float global_term_max; - - // SoA pointers - const void* doc_diffs_ptr = nullptr; // Can be u16 or u32 - const void* values_ptr = nullptr; - size_t block_data_size = 0; - uint8_t diff_bits = 32; - uint8_t value_bits = 8; - - size_t current_entry_idx; - ndd::idInt current_doc_id; - float current_score; - BMWIndex* index; - MDBX_txn* txn; - - BlockIterator(uint32_t tid, - float weight, - const std::vector* blks, - BMWIndex* idx, - MDBX_txn* t) : - term_id(tid), - term_weight(weight), - blocks(blks), - first_block_idx(0), - current_block_idx(0), - global_term_max(0.0f), - current_entry_idx(0), - current_doc_id(std::numeric_limits::max()), - current_score(0.0f), - index(idx), - txn(t) { - if(blocks && !blocks->empty()) { - if(blocks->front().start_doc_id == BMWIndex::GLOBAL_MAX_SENTINEL_DOC_ID) { - first_block_idx = 1; - global_term_max = blocks->front().block_max_value; - } else { - first_block_idx = 0; - for(const auto& block : *blocks) { - global_term_max = std::max(global_term_max, block.block_max_value); - } - } - - current_block_idx = first_block_idx; - if(current_block_idx < blocks->size()) { - loadCurrentBlock(); - } - } - } - - void loadCurrentBlock() { - if(current_block_idx >= blocks->size()) { - current_doc_id = std::numeric_limits::max(); - return; - } - const auto& block_meta = (*blocks)[current_block_idx]; - auto view = index->getReadOnlyBlock(txn, term_id, block_meta.start_doc_id); - doc_diffs_ptr = view.doc_diffs; - values_ptr = view.values; - block_data_size = view.count; - diff_bits = view.diff_bits; - value_bits = view.value_bits; - current_entry_idx = 0; - advanceToNextLive(); - } - - inline float valueAt(size_t idx, float block_max_value) const { - if(value_bits == 32) { - return static_cast(values_ptr)[idx]; - } - return dequantize(static_cast(values_ptr)[idx], block_max_value); - } - - inline bool isLiveAt(size_t idx) const { - if(value_bits == 32) { - return static_cast(values_ptr)[idx] > 0.0f; - } - return static_cast(values_ptr)[idx] > 0; - } - - inline size_t findNextLive(size_t start_idx) const { - if(value_bits == 32) { - size_t idx = start_idx; - auto values = static_cast(values_ptr); - while(idx < block_data_size && values[idx] <= 0.0f) { - ++idx; - } - return idx; - } - return index->findNextLiveSIMD( - static_cast(values_ptr), block_data_size, start_idx); - } - - inline void advanceToNextLive() { - // Branch prediction will handle diff_bits effectively (constant per block) - if(diff_bits == 16) { - advanceToNextLive16(); - } else { - advanceToNextLive32(); - } - } - - inline void advanceToNextLive16() { - auto diff_ptr = static_cast(doc_diffs_ptr); - - // Fast path: check if current entry is already live - if(current_entry_idx < block_data_size && isLiveAt(current_entry_idx)) { - const auto& block_meta = (*blocks)[current_block_idx]; - current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - current_score = valueAt(current_entry_idx, block_meta.block_max_value); - return; - } - - current_entry_idx = findNextLive(current_entry_idx); - - if(current_entry_idx < block_data_size) { - const auto& block_meta = (*blocks)[current_block_idx]; - current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - current_score = valueAt(current_entry_idx, block_meta.block_max_value); - return; - } - // Block exhausted - current_block_idx++; - loadCurrentBlock(); - } - - inline void advanceToNextLive32() { - if(current_entry_idx < block_data_size && isLiveAt(current_entry_idx)) { - const auto& block_meta = (*blocks)[current_block_idx]; - if(diff_bits == 32) { - auto diff_ptr = static_cast(doc_diffs_ptr); - current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - } - else { - current_doc_id = std::numeric_limits::max(); - current_block_idx = blocks->size(); - return; - } - current_score = valueAt(current_entry_idx, block_meta.block_max_value); - return; - } - - current_entry_idx = findNextLive(current_entry_idx); - - if(current_entry_idx < block_data_size) { - const auto& block_meta = (*blocks)[current_block_idx]; - if(diff_bits == 32) { - auto diff_ptr = static_cast(doc_diffs_ptr); - current_doc_id = block_meta.start_doc_id + diff_ptr[current_entry_idx]; - } - else { - current_doc_id = std::numeric_limits::max(); - current_block_idx = blocks->size(); - return; - } - current_score = valueAt(current_entry_idx, block_meta.block_max_value); - return; - } - current_block_idx++; - loadCurrentBlock(); - } - - inline void next() { - current_entry_idx++; - // Inline the check to avoid function call overhead in tight loops - if(diff_bits == 16) { - advanceToNextLive16(); - } else { - advanceToNextLive32(); - } - } - - void advance(ndd::idInt target_doc_id) { - if(current_doc_id >= target_doc_id) { - return; - } - - // Dispatch to specialized implementation - if(diff_bits == 16) { - advance16(target_doc_id); - } else { - advanceGeneric(target_doc_id); - } - } - - // Specialized advance for 16-bit - void advance16(ndd::idInt target_doc_id) { - // Optimize Block Skipping logic (Same as before) - if(current_block_idx < blocks->size()) { - if(current_block_idx + 1 < blocks->size() - && (*blocks)[current_block_idx + 1].start_doc_id < target_doc_id) { - auto it = std::upper_bound(blocks->begin() + current_block_idx, - blocks->end(), - target_doc_id, - [](ndd::idInt id, const BlockIdx& b) { - return id < b.start_doc_id; - }); - size_t next_idx = std::distance(blocks->begin(), it); - if(next_idx > 0) { - current_block_idx = next_idx - 1; - // Reset state for new block - current_entry_idx = 0; - doc_diffs_ptr = nullptr; - block_data_size = 0; - // DO NOT recursively call loadCurrentBlock -> advance(), just break to - // reload below - } - } - } - - if(block_data_size == 0) { - loadCurrentBlock(); - // If diff_bits changed (unlikely but possible), dispatch again - if(diff_bits != 16) { - advance(target_doc_id); - return; - } - } - - if(current_block_idx >= blocks->size()) { - return; - } - - const auto& block_meta = (*blocks)[current_block_idx]; - if(target_doc_id > block_meta.start_doc_id) { - ndd::idInt diff = target_doc_id - block_meta.start_doc_id; - // If diff > UINT16_MAX, we know it's not in this 16-bit block - if(diff > UINT16_MAX) { - current_entry_idx = block_data_size; - } else { - current_entry_idx = index->findEntryIndexSIMD16( - static_cast(doc_diffs_ptr), - block_data_size, - current_entry_idx, - static_cast(diff)); - } - advanceToNextLive16(); - } - } - - // Specialized advance for non-16-bit blocks (32-bit or Generic) - void advanceGeneric(ndd::idInt target_doc_id) { - // Optimize Block Skipping logic - if(current_block_idx < blocks->size()) { - if(current_block_idx + 1 < blocks->size() - && (*blocks)[current_block_idx + 1].start_doc_id < target_doc_id) { - auto it = std::upper_bound(blocks->begin() + current_block_idx, - blocks->end(), - target_doc_id, - [](ndd::idInt id, const BlockIdx& b) { - return id < b.start_doc_id; - }); - size_t next_idx = std::distance(blocks->begin(), it); - if(next_idx > 0) { - current_block_idx = next_idx - 1; - current_entry_idx = 0; - doc_diffs_ptr = nullptr; - block_data_size = 0; - } - } - } - - if(block_data_size == 0) { - loadCurrentBlock(); - if(diff_bits == 16) { - advance(target_doc_id); - return; - } - } - - if(current_block_idx >= blocks->size()) { - return; - } - - const auto& block_meta = (*blocks)[current_block_idx]; - if(target_doc_id > block_meta.start_doc_id) { - ndd::idInt diff = target_doc_id - block_meta.start_doc_id; - current_entry_idx = - index->findEntryIndexSIMD32(static_cast(doc_diffs_ptr), - block_data_size, - current_entry_idx, - static_cast(diff)); - advanceToNextLive32(); - } - } - - float upperBound() const { - if(current_block_idx >= blocks->size()) { - return 0.0f; - } - return term_weight * (*blocks)[current_block_idx].block_max_value; - } - - float globalUpperBound() const { return term_weight * global_term_max; } - }; - - MDBX_env* env_; - MDBX_dbi term_blocks_dbi_; - MDBX_dbi term_blocks_index_dbi_; - size_t vocab_size_; - - // In-memory cache of term block indices - std::unordered_map> term_blocks_index_; - mutable std::shared_mutex mutex_; - - // Block management constants - - // Optimized SIMD search for 16-bit diffs - size_t findEntryIndexSIMD16(const uint16_t* doc_diffs, - size_t size, - size_t start_idx, - uint16_t target_diff) { - size_t idx = start_idx; - -#if defined(USE_AVX512) - const size_t simd_width = 32; - __m512i target_vec = _mm512_set1_epi16(static_cast(target_diff)); - - while(idx + simd_width <= size) { - __m512i data_vec = _mm512_loadu_si512(doc_diffs + idx); - __mmask32 mask = _mm512_cmpge_epi16_mask(data_vec, target_vec); - - if(mask != 0) { - return idx + __builtin_ctz(mask); - } - idx += simd_width; - } -#elif defined(USE_AVX2) - const size_t simd_width = 16; - __m256i target_vec = _mm256_set1_epi16(static_cast(target_diff)); - - while(idx + simd_width <= size) { - if(doc_diffs[idx + simd_width - 1] < target_diff) { - idx += simd_width; - continue; - } - __m256i data_vec = - _mm256_loadu_si256(reinterpret_cast(doc_diffs + idx)); - __m256i offset = _mm256_set1_epi16(static_cast(0x8000)); - __m256i data_biased = _mm256_add_epi16(data_vec, offset); - __m256i target_biased = _mm256_add_epi16(target_vec, offset); - - __m256i lt = _mm256_cmpgt_epi16(target_biased, data_biased); - int mask = _mm256_movemask_epi8(lt); - - if(mask != -1) { - return idx + (__builtin_ctz(~mask) / 2); - } - idx += simd_width; - } -#elif defined(USE_SVE2) - svbool_t pg = svwhilelt_b16(idx, size); - svuint16_t target_vec = svdup_u16(target_diff); - - while(svptest_any(svptrue_b16(), pg)) { - svuint16_t data_vec = svld1_u16(pg, doc_diffs + idx); - svbool_t cmp = svcmpge_u16(pg, data_vec, target_vec); - - if(svptest_any(pg, cmp)) { - svbool_t before_match = svbrkb_z(pg, cmp); - uint64_t count = svcntp_b16(pg, before_match); - return idx + count; - } - idx += svcnth(); - pg = svwhilelt_b16(idx, size); - } - return idx; -#elif defined(USE_NEON) - const size_t simd_width = 8; - uint16x8_t target_vec = vdupq_n_u16(target_diff); - - while(idx + simd_width <= size) { - uint16x8_t data_vec = vld1q_u16(doc_diffs + idx); - uint16x8_t cmp = vcgeq_u16(data_vec, target_vec); - - // Check if any element is >= target (result of vcgeq is all 1s if true) - if(vmaxvq_u16(cmp) != 0) { - for(size_t i = 0; i < simd_width; ++i) { - if(doc_diffs[idx + i] >= target_diff) { - return idx + i; - } - } - } - idx += simd_width; - } -#endif - - // Scalar fallback - while(idx < size && doc_diffs[idx] < target_diff) { - idx++; - } - return idx; - } - - // Optimized SIMD search for 32-bit diffs - size_t findEntryIndexSIMD32(const uint32_t* doc_diffs, - size_t size, - size_t start_idx, - uint32_t target_diff) { - size_t idx = start_idx; - -#if defined(USE_AVX512) - const size_t simd_width = 16; - __m512i target_vec = _mm512_set1_epi32(static_cast(target_diff)); - - while(idx + simd_width <= size) { - __m512i data_vec = _mm512_loadu_si512(doc_diffs + idx); - __mmask16 mask = _mm512_cmpge_epu32_mask(data_vec, target_vec); - - if(mask != 0) { - return idx + __builtin_ctz(mask); - } - idx += simd_width; - } -#elif defined(USE_AVX2) - const size_t simd_width = 8; - __m256i target_vec = _mm256_set1_epi32(static_cast(target_diff)); - - while(idx + simd_width <= size) { - __builtin_prefetch(doc_diffs + idx + 32); - if(doc_diffs[idx + simd_width - 1] < target_diff) { - idx += simd_width; - continue; - } - - __m256i data_vec = - _mm256_loadu_si256(reinterpret_cast(doc_diffs + idx)); - // unsigned comparison using max: a >= b iff max(a,b) == a - __m256i max_vec = _mm256_max_epu32(data_vec, target_vec); - __m256i cmp = _mm256_cmpeq_epi32(max_vec, data_vec); - - int mask = _mm256_movemask_ps(_mm256_castsi256_ps(cmp)); - if(mask != 0) { - return idx + __builtin_ctz(mask); - } - idx += simd_width; - } -#elif defined(USE_SVE2) - svbool_t pg = svwhilelt_b32(idx, size); - svuint32_t target_vec = svdup_u32(target_diff); - - while(svptest_any(svptrue_b32(), pg)) { - svuint32_t data_vec = svld1_u32(pg, doc_diffs + idx); - svbool_t cmp = svcmpge_u32(pg, data_vec, target_vec); - - if(svptest_any(pg, cmp)) { - svbool_t before_match = svbrkb_z(pg, cmp); - uint64_t count = svcntp_b32(pg, before_match); - return idx + count; - } - idx += svcntw(); - pg = svwhilelt_b32(idx, size); - } - return idx; -#elif defined(USE_NEON) - const size_t simd_width = 4; - uint32x4_t target_vec = vdupq_n_u32(target_diff); - - while(idx + simd_width <= size) { - uint32x4_t data_vec = vld1q_u32(doc_diffs + idx); - uint32x4_t cmp = vcgeq_u32(data_vec, target_vec); - - // Check if any bit is expected (vcgeq returns all 1s or 0s) - if(vmaxvq_u32(cmp) != 0) { - for(size_t i = 0; i < simd_width; ++i) { - if(doc_diffs[idx + i] >= target_diff) { - return idx + i; - } - } - } - idx += simd_width; - } -#endif - - // Scalar fallback - while(idx < size && doc_diffs[idx] < target_diff) { - idx++; - } - return idx; - } - - size_t findEntryIndexGeneric(const void* doc_diffs, - size_t size, - size_t start_idx, - ndd::idInt target_diff, - uint8_t bits) { - // In 32-bit mode, we only expect 32-bit blocks here (16-bit handled by SIMD16) - return findEntryIndexSIMD32(static_cast(doc_diffs), - size, - start_idx, - static_cast(target_diff)); - } - - // Find next non-zero value (live entry) - size_t findNextLiveSIMD(const uint8_t* values, size_t size, size_t start_idx) { - size_t idx = start_idx; - -#if defined(USE_AVX512) - const size_t simd_width = 64; - __m512i zero_vec = _mm512_setzero_si512(); - - while(idx + simd_width <= size) { - __m512i data_vec = _mm512_loadu_si512(values + idx); - __mmask64 mask = _mm512_cmpneq_epu8_mask(data_vec, zero_vec); - - if(mask != 0) { - return idx + __builtin_ctzll(mask); - } - idx += simd_width; - } -#elif defined(USE_AVX2) - const size_t simd_width = 32; - __m256i zero_vec = _mm256_setzero_si256(); - - while(idx + simd_width <= size) { - __m256i data_vec = - _mm256_loadu_si256(reinterpret_cast(values + idx)); - __m256i cmp = _mm256_cmpeq_epi8(data_vec, zero_vec); - int mask = _mm256_movemask_epi8(cmp); // 1 = zero, 0 = non-zero - - // If all 1s (mask 0xFFFFFFFF), then all zeros -> continue - if(static_cast(mask) != 0xFFFFFFFF) { - // ~mask has 1s where non-zero exists - return idx + __builtin_ctz(~mask); - } - idx += simd_width; - } -#elif defined(USE_NEON) - const size_t simd_width = 16; - uint8x16_t zero_vec = vdupq_n_u8(0); - - while(idx + simd_width <= size) { - uint8x16_t data_vec = vld1q_u8(values + idx); - uint8x16_t cmp = vceqq_u8(data_vec, zero_vec); - - // Check if any element is NOT zero (cmp is 0x00 for non-zero, 0xFF for zero) - // If all are zero, cmp is all 0xFF. vminvq_u8 will be 0xFF. - // If any is non-zero, cmp has a 0x00. vminvq_u8 will be 0x00. - if(vminvq_u8(cmp) == 0) { - for(size_t i = 0; i < simd_width; ++i) { - if(values[idx + i] != 0) { - return idx + i; - } - } - } - idx += simd_width; - } -#elif defined(USE_SVE2) - svbool_t pg = svwhilelt_b8(idx, size); - while(svptest_any(svptrue_b8(), pg)) { - svuint8_t data_vec = svld1_u8(pg, values + idx); - svbool_t cmp = svcmpne_n_u8(pg, data_vec, 0); // Not equal to 0 - - if(svptest_any(pg, cmp)) { - svbool_t before_match = svbrkb_z(pg, cmp); - return idx + svcntp_b8(pg, before_match); - } - idx += svcntb(); - pg = svwhilelt_b8(idx, size); - } - return idx; -#endif - - while(idx < size) { - if(values[idx] != 0) { - return idx; - } - idx++; - } - return idx; - } - - bool loadTermBlocksIndex() { - MDBX_txn* txn; - int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn); - if(rc != 0) { - LOG_ERROR("Failed to begin transaction for loading index: " << mdbx_strerror(rc)); - return false; - } - - MDBX_cursor* cursor; - rc = mdbx_cursor_open(txn, term_blocks_index_dbi_, &cursor); - if(rc != 0) { - mdbx_txn_abort(txn); - return false; - } - - MDBX_val key, data; - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_FIRST); - while(rc == MDBX_SUCCESS) { - if(key.iov_len == sizeof(uint32_t)) { - uint32_t term_id; - std::memcpy(&term_id, key.iov_base, sizeof(uint32_t)); - - size_t count = data.iov_len / sizeof(BlockIdx); - std::vector blocks(count); - std::memcpy(blocks.data(), data.iov_base, data.iov_len); - - if(!blocks.empty() - && blocks.front().start_doc_id != GLOBAL_MAX_SENTINEL_DOC_ID) { - float global_max = 0.0f; - for(const auto& b : blocks) { - global_max = std::max(global_max, b.block_max_value); - } - blocks.insert( - blocks.begin(), BlockIdx(GLOBAL_MAX_SENTINEL_DOC_ID, global_max)); - } - - term_blocks_index_[term_id] = std::move(blocks); - } - rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); - } - - mdbx_cursor_close(cursor); - mdbx_txn_abort(txn); - return true; - } - - bool removeDocumentInternal(MDBX_txn* txn, ndd::idInt doc_id, const SparseVector& vec) { - std::unordered_set touched_terms; - for(size_t i = 0; i < vec.indices.size(); ++i) { - uint32_t term_id = vec.indices[i]; - touched_terms.insert(term_id); - if(!removeFromBlock(txn, term_id, doc_id)) { - // Ignore errors - } - } - - for(uint32_t term_id : touched_terms) { - if(!saveTermIndex(txn, term_id)) { - return false; - } - } - - return true; - } - - bool - addDocumentsBatchInternal(MDBX_txn* txn, - const std::vector>& docs) { - // Group updates by term_id - std::unordered_map>> term_updates; - for(const auto& [doc_id, sparse_vec] : docs) { - for(size_t i = 0; i < sparse_vec.indices.size(); ++i) { - term_updates[sparse_vec.indices[i]].emplace_back(doc_id, sparse_vec.values[i]); - } - } - - // Process each term - for(auto& [term_id, updates] : term_updates) { - // Sort by doc_id to access blocks sequentially - std::sort(updates.begin(), updates.end()); - - for(const auto& [doc_id, value] : updates) { - if(!addToBlock(txn, term_id, doc_id, value)) { - LOG_ERROR("Failed to add doc " << doc_id << " term " << term_id - << " to block"); - return false; - } - } - - // Save index structure for this term after all updates - if(!saveTermIndex(txn, term_id)) { - return false; - } - } - return true; - } - - // Save the index structure (block list) for a single term - bool saveTermIndex(MDBX_txn* txn, uint32_t term_id) { - auto it = term_blocks_index_.find(term_id); - MDBX_val key; - key.iov_base = const_cast(static_cast(&term_id)); - key.iov_len = sizeof(uint32_t); - - if(it == term_blocks_index_.end() || it->second.empty()) { - int rc = mdbx_del(txn, term_blocks_index_dbi_, &key, nullptr); - return rc == MDBX_SUCCESS || rc == MDBX_NOTFOUND; - } - - auto& blocks = it->second; - size_t first_idx = firstRealBlockIndex(blocks); - if(first_idx >= blocks.size()) { - term_blocks_index_.erase(it); - int rc = mdbx_del(txn, term_blocks_index_dbi_, &key, nullptr); - return rc == MDBX_SUCCESS || rc == MDBX_NOTFOUND; - } - - float global_max = 0.0f; - for(size_t i = first_idx; i < blocks.size(); ++i) { - global_max = std::max(global_max, blocks[i].block_max_value); - } - - if(first_idx == 0) { - blocks.insert(blocks.begin(), BlockIdx(GLOBAL_MAX_SENTINEL_DOC_ID, global_max)); - } else { - blocks[0].block_max_value = global_max; - } - - MDBX_val data; - - data.iov_base = const_cast(static_cast(blocks.data())); - data.iov_len = blocks.size() * sizeof(BlockIdx); - - int rc = mdbx_put(txn, term_blocks_index_dbi_, &key, &data, MDBX_UPSERT); - if(rc != 0) { - LOG_ERROR("Failed to save term index for term " << term_id << ": " - << mdbx_strerror(rc)); - return false; - } - return true; - } - - std::vector - loadBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt start_doc_id) { - // Zero-copy key creation - struct { - uint32_t t; - ndd::idInt d; - } __attribute__((packed)) key_struct; - key_struct.t = term_id; - key_struct.d = start_doc_id; - - MDBX_val key; - key.iov_base = &key_struct; - key.iov_len = sizeof(key_struct); - - MDBX_val data; - int rc = mdbx_get(txn, term_blocks_dbi_, &key, &data); - - std::vector entries; - if(rc == MDBX_SUCCESS && data.iov_len >= sizeof(BlockHeader)) { - const BlockHeader* header = reinterpret_cast(data.iov_base); - size_t n = header->n; - - entries.resize(n); - const uint8_t* ptr = - static_cast(data.iov_base) + sizeof(BlockHeader); - - if(header->version == 3) { - // Determine pointer locations based on diff_bits - const void* diff_ptr = ptr; - const uint8_t* val_ptr; - - if(header->diff_bits == 16) { - val_ptr = ptr + n * sizeof(uint16_t); - const uint16_t* diffs = static_cast(diff_ptr); - for(size_t i = 0; i < n; ++i) { - entries[i].doc_diff = diffs[i]; - entries[i].value = dequantize(val_ptr[i], header->block_max_value); - } - } else if(header->diff_bits == 32) { - val_ptr = ptr + n * sizeof(uint32_t); - const uint32_t* diffs = static_cast(diff_ptr); - for(size_t i = 0; i < n; ++i) { - entries[i].doc_diff = diffs[i]; - entries[i].value = dequantize(val_ptr[i], header->block_max_value); - } - } - else { - LOG_ERROR("Unsupported block diff_bits: " << (int)header->diff_bits); - } - } else if(header->version == 4) { - const void* diff_ptr = ptr; - const float* val_ptr; - - if(header->diff_bits == 16) { - val_ptr = reinterpret_cast(ptr + n * sizeof(uint16_t)); - const uint16_t* diffs = static_cast(diff_ptr); - for(size_t i = 0; i < n; ++i) { - entries[i].doc_diff = diffs[i]; - entries[i].value = val_ptr[i]; - } - } else if(header->diff_bits == 32) { - val_ptr = reinterpret_cast(ptr + n * sizeof(uint32_t)); - const uint32_t* diffs = static_cast(diff_ptr); - for(size_t i = 0; i < n; ++i) { - entries[i].doc_diff = diffs[i]; - entries[i].value = val_ptr[i]; - } - } - else { - LOG_ERROR("Unsupported block diff_bits: " << (int)header->diff_bits); - } - } else { - LOG_ERROR("Unsupported block version: " << (int)header->version); - } - } - return entries; - } - - bool saveBlock(MDBX_txn* txn, - uint32_t term_id, - ndd::idInt start_doc_id, - const std::vector& entries, - BlockHeader& header) { - // Zero-copy key creation - struct { - uint32_t t; - ndd::idInt d; - } __attribute__((packed)) key_struct; - key_struct.t = term_id; - key_struct.d = start_doc_id; - - MDBX_val key; - key.iov_base = &key_struct; - key.iov_len = sizeof(key_struct); - - size_t n = entries.size(); - - // Recalculate stats - float max_val = 0.0f; - ndd::idInt max_diff = 0; - size_t live = 0; - - for(const auto& e : entries) { - if(e.value > max_val) { - max_val = e.value; - } - if(e.doc_diff > max_diff) { - max_diff = e.doc_diff; - } - if(e.value > 1e-9f) { - live++; // Approximate check for float > 0 - } - } - - header.block_max_value = max_val; - header.live_count = static_cast(live); - header.n = static_cast(n); - -#if defined(NDD_BMW_STORE_FLOAT_VALUES) - header.version = 4; -#else - header.version = 3; -#endif - header.alignment_pad = 0; - - if(max_diff <= UINT16_MAX) { - header.diff_bits = 16; - } else { - header.diff_bits = 32; - } - - size_t diff_size = header.diff_bits / 8; -#if defined(NDD_BMW_STORE_FLOAT_VALUES) - size_t value_size = sizeof(float); -#else - size_t value_size = sizeof(uint8_t); -#endif - size_t total_size = sizeof(BlockHeader) + (n * diff_size) + (n * value_size); - - std::vector buffer(total_size); - - // Copy header - std::memcpy(buffer.data(), &header, sizeof(BlockHeader)); - - uint8_t* ptr = buffer.data() + sizeof(BlockHeader); - - // Copy doc_diffs - if(header.diff_bits == 16) { - uint16_t* diffs = reinterpret_cast(ptr); - for(size_t i = 0; i < n; ++i) { - diffs[i] = static_cast(entries[i].doc_diff); - } - ptr += n * sizeof(uint16_t); - } else if(header.diff_bits == 32) { - uint32_t* diffs = reinterpret_cast(ptr); - for(size_t i = 0; i < n; ++i) { - diffs[i] = static_cast(entries[i].doc_diff); - } - ptr += n * sizeof(uint32_t); - } - - // Copy values -#if defined(NDD_BMW_STORE_FLOAT_VALUES) - float* values = reinterpret_cast(ptr); - for(size_t i = 0; i < n; ++i) { - values[i] = entries[i].value; - } -#else - uint8_t* values = static_cast(ptr); - for(size_t i = 0; i < n; ++i) { - values[i] = quantize(entries[i].value, max_val); - } -#endif - - MDBX_val data; - data.iov_base = buffer.data(); - data.iov_len = buffer.size(); - - int rc = mdbx_put(txn, term_blocks_dbi_, &key, &data, MDBX_UPSERT); - return rc == 0; - } - - bool deleteBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt start_doc_id) { - struct { - uint32_t t; - ndd::idInt d; - } __attribute__((packed)) key_struct; - key_struct.t = term_id; - key_struct.d = start_doc_id; - - MDBX_val key; - key.iov_base = &key_struct; - key.iov_len = sizeof(key_struct); - - int rc = mdbx_del(txn, term_blocks_dbi_, &key, nullptr); - return rc == MDBX_SUCCESS || rc == MDBX_NOTFOUND; - } - - bool compactBlockAfterDelete(MDBX_txn* txn, - uint32_t term_id, - size_t block_idx, - const std::vector& entries_with_tombstones) { - auto term_it = term_blocks_index_.find(term_id); - if(term_it == term_blocks_index_.end()) { - return true; - } - - auto& blocks = term_it->second; - if(block_idx >= blocks.size()) { - return true; - } - - ndd::idInt old_start_doc_id = blocks[block_idx].start_doc_id; - - std::vector live_entries; - live_entries.reserve(entries_with_tombstones.size()); - for(const auto& entry : entries_with_tombstones) { - if(entry.value > 0.0f) { - live_entries.push_back(entry); - } - } - - if(live_entries.empty()) { - if(!deleteBlock(txn, term_id, old_start_doc_id)) { - return false; - } - - blocks.erase(blocks.begin() + static_cast(block_idx)); - if(blocks.empty()) { - term_blocks_index_.erase(term_it); - } - return true; - } - - ndd::idInt start_shift = live_entries.front().doc_diff; - ndd::idInt new_start_doc_id = old_start_doc_id + start_shift; - - if(start_shift != 0) { - for(auto& entry : live_entries) { - entry.doc_diff -= start_shift; - } - } - - BlockHeader header; - bool need_rekey = (new_start_doc_id != old_start_doc_id); - - if(need_rekey) { - if(!deleteBlock(txn, term_id, old_start_doc_id)) { - return false; - } - } - - if(!saveBlock(txn, term_id, new_start_doc_id, live_entries, header)) { - return false; - } - - blocks[block_idx].start_doc_id = new_start_doc_id; - blocks[block_idx].block_max_value = header.block_max_value; - - return true; - } - - // Returns pointer to block data valid for the duration of txn - BlockView getReadOnlyBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt start_doc_id) { - // Zero-copy key creation on stack - struct { - uint32_t t; - ndd::idInt d; - } __attribute__((packed)) key_struct; - key_struct.t = term_id; - key_struct.d = start_doc_id; - - MDBX_val key; - key.iov_base = &key_struct; - key.iov_len = sizeof(key_struct); - - MDBX_val data; - int rc = mdbx_get(txn, term_blocks_dbi_, &key, &data); - - if(rc == MDBX_SUCCESS && data.iov_len >= sizeof(BlockHeader)) { - const BlockHeader* header = reinterpret_cast(data.iov_base); - - size_t diff_size = 0; - if(header->diff_bits == 16) { - diff_size = sizeof(uint16_t); - } else if(header->diff_bits == 32) { - diff_size = sizeof(uint32_t); - } - else { - return {nullptr, nullptr, 0, 0, 0}; - } - - size_t value_size = (header->version == 4) ? sizeof(float) : sizeof(uint8_t); - size_t required_size = sizeof(BlockHeader) + header->n * diff_size + header->n * value_size; - if(data.iov_len < required_size) { - return {nullptr, nullptr, 0, 0, 0}; - } - - const uint8_t* ptr = - static_cast(data.iov_base) + sizeof(BlockHeader); - - const void* doc_diffs = ptr; - const uint8_t* values = ptr + header->n * diff_size; - - return {doc_diffs, - values, - header->n, - header->diff_bits, - static_cast((header->version == 4) ? 32 : 8)}; - } - return {nullptr, nullptr, 0, 0, 0}; - } - - ndd::idInt getBlockEndDocId(const std::vector& blocks, size_t block_idx) const { - if(block_idx + 1 < blocks.size()) { - return blocks[block_idx + 1].start_doc_id - 1; - } - return std::numeric_limits::max(); - } - - bool addToBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt doc_id, float value) { - // Get or create blocks for this term - auto& blocks = term_blocks_index_[term_id]; - - // Find the appropriate block - auto block_it = findBlockIterator(blocks, doc_id); - - // Check if we need to split due to range (if > UINT16_MAX, cannot fit in uint16 diff) - // This is a constraint for 16-bit blocks. If we enable mix, we don't strict need to - // check unless we want to force 16-bit. - - bool force_new_block = false; - if(block_it != blocks.end() && block_it->start_doc_id <= doc_id) { - if((doc_id - block_it->start_doc_id) > UINT16_MAX) { - force_new_block = true; - } - } - - if(block_it == blocks.end() || block_it->start_doc_id > doc_id || force_new_block) { - // Need to create a new block - - // Insert into index list - // If forcing new block, we insert AFTER block_it if block_it exists and is < doc_id - // findBlockIterator returns iterator <= doc_id. - - std::vector::iterator insert_it; - if(force_new_block && block_it != blocks.end()) { - insert_it = block_it + 1; - } else { - insert_it = block_it; - } - - blocks.insert(insert_it, BlockIdx(doc_id, value)); - - // Create the actual block data - std::vector entries; - entries.emplace_back(0, value); // doc_diff = 0 - - BlockHeader header; - // header fields set by saveBlock - - return saveBlock(txn, term_id, doc_id, entries, header); - } - - // Add to existing block - auto block_entries = loadBlock(txn, term_id, block_it->start_doc_id); - - ndd::idInt doc_diff = doc_id - block_it->start_doc_id; - - // Find insertion point (keep sorted by doc_diff) - auto entry_it = std::lower_bound( - block_entries.begin(), block_entries.end(), BlockEntry(doc_diff, 0.0f)); - - if(entry_it != block_entries.end() && entry_it->doc_diff == doc_diff) { - // Update existing entry - entry_it->value = value; - } else { - // Insert new entry - block_entries.insert(entry_it, BlockEntry(doc_diff, value)); - } - - // Check if block needs splitting - if(block_entries.size() > settings::MAX_BMW_BLOCK_SIZE) { - BlockHeader header; - bool saved = saveBlock(txn, term_id, block_it->start_doc_id, block_entries, header); - if(!saved) { - return false; - } - block_it->block_max_value = header.block_max_value; - return splitBlock(txn, term_id, block_it->start_doc_id); - } - - BlockHeader header; - // Fields set by saveBlock - - bool success = saveBlock(txn, term_id, block_it->start_doc_id, block_entries, header); - if(success) { - // Keep cached block max synchronized (increase or decrease). - block_it->block_max_value = header.block_max_value; - } - return success; - } - - bool removeFromBlock(MDBX_txn* txn, uint32_t term_id, ndd::idInt doc_id) { - auto it = term_blocks_index_.find(term_id); - if(it == term_blocks_index_.end()) { - return false; // Term not found - } - - auto& blocks = it->second; - auto block_it = findBlockIterator(blocks, doc_id); - - if(block_it == blocks.end() || block_it->start_doc_id > doc_id) { - return false; - } - - // Logic similar to addToBlock. Load, modify, Save. - // We do basic range check to avoid loading obviously wrong block - if((doc_id - block_it->start_doc_id) > 200000) { // Safety heuristic - return false; - } - - // Load block - auto block_entries = loadBlock(txn, term_id, block_it->start_doc_id); - size_t block_idx = std::distance(blocks.begin(), block_it); - ndd::idInt doc_diff = doc_id - block_it->start_doc_id; - - auto entry_it = std::lower_bound( - block_entries.begin(), block_entries.end(), BlockEntry(doc_diff, 0.0f)); - - if(entry_it != block_entries.end() && entry_it->doc_diff == doc_diff) { - entry_it->value = 0.0f; // Mark as tombstone (0.0f) - - BlockHeader header; - // Fields set by saveBlock - bool success = saveBlock(txn, term_id, block_it->start_doc_id, block_entries, header); - if(success) { - block_it->block_max_value = header.block_max_value; - - // Deterministic 1/8 compaction trigger to avoid extra RNG overhead. - if((doc_id % 8) == 0) { - if(!compactBlockAfterDelete(txn, term_id, block_idx, block_entries)) { - return false; - } - } - } - return success; - } - - return false; - } - }; - -} // namespace ndd \ No newline at end of file diff --git a/src/sparse/inverted_index.cpp b/src/sparse/inverted_index.cpp new file mode 100644 index 0000000000..1b4e5b8c4a --- /dev/null +++ b/src/sparse/inverted_index.cpp @@ -0,0 +1,2062 @@ +/** + * Inverted index for sparse vector similarity search. + * + * Blocked on-disk layout in MDBX: + * key = pack(term_id, block_nr) as uint64_t integer-key + * value = BlockHeader | doc_offsets[n] (uint16) | values[n] (uint8 or float) + * + * Metadata rows in the same DBI: + * pack(term_id, UINT32_MAX) -> PostingListHeader + * + * The key packing keeps all rows for a term contiguous, so scans can seek once + * to pack(term_id, 0) and walk until term_id changes. + */ + +#include "inverted_index.hpp" + +#include +#include +#include + +namespace ndd { + + namespace { + template + struct PostingValueAccessor; + + template <> + struct PostingValueAccessor { + using ValueType = float; + + static inline bool isLive(ValueType value) { + return value > 0.0f; + } + }; + + template <> + struct PostingValueAccessor { + using ValueType = uint8_t; + + static inline bool isLive(ValueType value) { + return value > 0; + } + }; + +#ifdef ND_SPARSE_INSTRUMENT + using SteadyClock = std::chrono::steady_clock; + + inline uint64_t elapsedNsSince(const SteadyClock::time_point& start) { + return static_cast( + std::chrono::duration_cast(SteadyClock::now() - start) + .count()); + } + + struct SparseSearchDebugStats { + std::atomic phase2_iterators_visited{0}; + std::atomic phase2_iterators_contributed{0}; + std::atomic parse_current_kv_calls{0}; + std::atomic parse_current_kv_total_ns{0}; + }; + + struct SparseUpdateDebugStats { + std::atomic add_batch_calls{0}; + std::atomic add_batch_docs{0}; + std::atomic add_batch_terms{0}; + std::atomic add_batch_raw_updates{0}; + std::atomic add_batch_deduped_updates{0}; + std::atomic add_batch_blocks{0}; + std::atomic build_term_updates_total_ns{0}; + std::atomic sort_dedup_total_ns{0}; + std::atomic load_block_calls{0}; + std::atomic load_block_total_ns{0}; + std::atomic load_block_entries_total{0}; + std::atomic merge_block_calls{0}; + std::atomic merge_block_total_ns{0}; + std::atomic merge_existing_entries_total{0}; + std::atomic merge_update_entries_total{0}; + std::atomic merge_output_entries_total{0}; + std::atomic save_block_calls{0}; + std::atomic save_block_total_ns{0}; + std::atomic save_block_entries_total{0}; + std::atomic recompute_max_calls{0}; + std::atomic recompute_max_total_ns{0}; + }; + + SparseSearchDebugStats& sparseSearchDebugStats() { + static SparseSearchDebugStats stats; + return stats; + } + + SparseUpdateDebugStats& sparseUpdateDebugStats() { + static SparseUpdateDebugStats stats; + return stats; + } + + class ParseCurrentKVTimer { + public: + ParseCurrentKVTimer() : + start_(SteadyClock::now()) {} + + ~ParseCurrentKVTimer() { + SparseSearchDebugStats& stats = sparseSearchDebugStats(); + stats.parse_current_kv_calls.fetch_add(1, std::memory_order_relaxed); + stats.parse_current_kv_total_ns.fetch_add(elapsedNsSince(start_), + std::memory_order_relaxed); + } + + private: + SteadyClock::time_point start_; + }; +#endif // ND_SPARSE_INSTRUMENT + } // namespace + +#ifdef ND_SPARSE_INSTRUMENT + void printSparseSearchDebugStats() { + SparseSearchDebugStats& stats = sparseSearchDebugStats(); + const uint64_t visited = stats.phase2_iterators_visited.exchange(0, std::memory_order_relaxed); + const uint64_t contributed = + stats.phase2_iterators_contributed.exchange(0, std::memory_order_relaxed); + const uint64_t parse_calls = stats.parse_current_kv_calls.exchange(0, std::memory_order_relaxed); + const uint64_t parse_total_ns = + stats.parse_current_kv_total_ns.exchange(0, std::memory_order_relaxed); + + LOG_INFO("Sparse search debug stats"); + LOG_INFO("phase3 iterators visited: " << visited); + LOG_INFO("phase3 iterators contributed: " << contributed); + LOG_INFO("phase3 contribution rate(%): " + << std::fixed << std::setprecision(3) + << (visited ? (100.0 * static_cast(contributed) / static_cast(visited)) + : 0.0)); + LOG_INFO("parseCurrentKV count: " << parse_calls); + LOG_INFO("parseCurrentKV total(ms): " + << std::fixed << std::setprecision(3) + << (static_cast(parse_total_ns) / 1'000'000.0)); + LOG_INFO("parseCurrentKV avg(us): " + << std::fixed << std::setprecision(3) + << (parse_calls ? (static_cast(parse_total_ns) / 1000.0) + / static_cast(parse_calls) + : 0.0)); + std::cout << "=================================\n"; + } + + void printSparseUpdateDebugStats() { + SparseUpdateDebugStats& stats = sparseUpdateDebugStats(); + const uint64_t add_batch_calls = stats.add_batch_calls.exchange(0, std::memory_order_relaxed); + const uint64_t add_batch_docs = stats.add_batch_docs.exchange(0, std::memory_order_relaxed); + const uint64_t add_batch_terms = stats.add_batch_terms.exchange(0, std::memory_order_relaxed); + const uint64_t add_batch_raw_updates = + stats.add_batch_raw_updates.exchange(0, std::memory_order_relaxed); + const uint64_t add_batch_deduped_updates = + stats.add_batch_deduped_updates.exchange(0, std::memory_order_relaxed); + const uint64_t add_batch_blocks = stats.add_batch_blocks.exchange(0, std::memory_order_relaxed); + const uint64_t build_term_updates_total_ns = + stats.build_term_updates_total_ns.exchange(0, std::memory_order_relaxed); + const uint64_t sort_dedup_total_ns = + stats.sort_dedup_total_ns.exchange(0, std::memory_order_relaxed); + const uint64_t load_block_calls = stats.load_block_calls.exchange(0, std::memory_order_relaxed); + const uint64_t load_block_total_ns = + stats.load_block_total_ns.exchange(0, std::memory_order_relaxed); + const uint64_t load_block_entries_total = + stats.load_block_entries_total.exchange(0, std::memory_order_relaxed); + const uint64_t merge_block_calls = stats.merge_block_calls.exchange(0, std::memory_order_relaxed); + const uint64_t merge_block_total_ns = + stats.merge_block_total_ns.exchange(0, std::memory_order_relaxed); + const uint64_t merge_existing_entries_total = + stats.merge_existing_entries_total.exchange(0, std::memory_order_relaxed); + const uint64_t merge_update_entries_total = + stats.merge_update_entries_total.exchange(0, std::memory_order_relaxed); + const uint64_t merge_output_entries_total = + stats.merge_output_entries_total.exchange(0, std::memory_order_relaxed); + const uint64_t save_block_calls = stats.save_block_calls.exchange(0, std::memory_order_relaxed); + const uint64_t save_block_total_ns = + stats.save_block_total_ns.exchange(0, std::memory_order_relaxed); + const uint64_t save_block_entries_total = + stats.save_block_entries_total.exchange(0, std::memory_order_relaxed); + const uint64_t recompute_max_calls = + stats.recompute_max_calls.exchange(0, std::memory_order_relaxed); + const uint64_t recompute_max_total_ns = + stats.recompute_max_total_ns.exchange(0, std::memory_order_relaxed); + + LOG_INFO("Sparse update debug stats"); + LOG_INFO("addDocumentsBatchInternal count: " << add_batch_calls); + LOG_INFO("addDocumentsBatchInternal docs: " << add_batch_docs); + LOG_INFO("addDocumentsBatchInternal terms: " << add_batch_terms); + LOG_INFO("addDocumentsBatchInternal raw updates: " << add_batch_raw_updates); + LOG_INFO("addDocumentsBatchInternal deduped updates: " << add_batch_deduped_updates); + LOG_INFO("addDocumentsBatchInternal touched blocks: " << add_batch_blocks); + LOG_INFO("term_updates build total(ms): " + << std::fixed << std::setprecision(3) + << (static_cast(build_term_updates_total_ns) / 1'000'000.0)); + LOG_INFO("sort+dedup total(ms): " + << std::fixed << std::setprecision(3) + << (static_cast(sort_dedup_total_ns) / 1'000'000.0)); + LOG_INFO("loadBlockEntries count: " << load_block_calls); + LOG_INFO("loadBlockEntries total(ms): " + << std::fixed << std::setprecision(3) + << (static_cast(load_block_total_ns) / 1'000'000.0)); + LOG_INFO("loadBlockEntries avg(us): " + << std::fixed << std::setprecision(3) + << (load_block_calls + ? (static_cast(load_block_total_ns) / 1000.0) + / static_cast(load_block_calls) + : 0.0)); + LOG_INFO("loadBlockEntries avg existing entries: " + << std::fixed << std::setprecision(3) + << (load_block_calls + ? static_cast(load_block_entries_total) + / static_cast(load_block_calls) + : 0.0)); + LOG_INFO("merge blocks count: " << merge_block_calls); + LOG_INFO("merge blocks total(ms): " + << std::fixed << std::setprecision(3) + << (static_cast(merge_block_total_ns) / 1'000'000.0)); + LOG_INFO("merge blocks avg(us): " + << std::fixed << std::setprecision(3) + << (merge_block_calls + ? (static_cast(merge_block_total_ns) / 1000.0) + / static_cast(merge_block_calls) + : 0.0)); + LOG_INFO("merge avg existing entries: " + << std::fixed << std::setprecision(3) + << (merge_block_calls + ? static_cast(merge_existing_entries_total) + / static_cast(merge_block_calls) + : 0.0)); + LOG_INFO("merge avg update entries: " + << std::fixed << std::setprecision(3) + << (merge_block_calls + ? static_cast(merge_update_entries_total) + / static_cast(merge_block_calls) + : 0.0)); + LOG_INFO("merge avg output entries: " + << std::fixed << std::setprecision(3) + << (merge_block_calls + ? static_cast(merge_output_entries_total) + / static_cast(merge_block_calls) + : 0.0)); + LOG_INFO("saveBlockEntries count: " << save_block_calls); + LOG_INFO("saveBlockEntries total(ms): " + << std::fixed << std::setprecision(3) + << (static_cast(save_block_total_ns) / 1'000'000.0)); + LOG_INFO("saveBlockEntries avg(us): " + << std::fixed << std::setprecision(3) + << (save_block_calls + ? (static_cast(save_block_total_ns) / 1000.0) + / static_cast(save_block_calls) + : 0.0)); + LOG_INFO("saveBlockEntries avg entries: " + << std::fixed << std::setprecision(3) + << (save_block_calls + ? static_cast(save_block_entries_total) + / static_cast(save_block_calls) + : 0.0)); + LOG_INFO("recomputeGlobalMax count: " << recompute_max_calls); + LOG_INFO("recomputeGlobalMax total(ms): " + << std::fixed << std::setprecision(3) + << (static_cast(recompute_max_total_ns) / 1'000'000.0)); + LOG_INFO("recomputeGlobalMax avg(us): " + << std::fixed << std::setprecision(3) + << (recompute_max_calls + ? (static_cast(recompute_max_total_ns) / 1000.0) + / static_cast(recompute_max_calls) + : 0.0)); + std::cout << "=================================\n"; + } +#else + void printSparseSearchDebugStats() {} + void printSparseUpdateDebugStats() {} +#endif // ND_SPARSE_INSTRUMENT + + InvertedIndex::InvertedIndex(MDBX_env* env, size_t vocab_size, const std::string& index_id) + : env_(env), blocked_term_postings_dbi_(0), vocab_size_(vocab_size), index_id_(index_id) {} + + void InvertedIndex::applyHeaderDelta(PostingListHeader& header, + int64_t total_delta, + int64_t live_delta) { + int64_t new_total = static_cast(header.nr_entries) + total_delta; + int64_t new_live = static_cast(header.nr_live_entries) + live_delta; + + if (new_total < 0) new_total = 0; + if (new_live < 0) new_live = 0; + if (new_live > new_total) new_live = new_total; + + header.nr_entries = static_cast(new_total); + header.nr_live_entries = static_cast(new_live); + } + + bool InvertedIndex::validateSuperBlock(MDBX_txn* txn) { + SuperBlock sb; + bool sb_found = false; + if (!readSuperBlock(txn, &sb, &sb_found)) { + return false; + } + + if (!sb_found) { + // Check whether the DBI already has data (legacy DB without superblock). + MDBX_stat stat; + int rc = mdbx_dbi_stat(txn, blocked_term_postings_dbi_, &stat, sizeof(stat)); + if (rc == MDBX_SUCCESS && stat.ms_entries > 0) { + LOG_ERROR(2201, + index_id_, + "Sparse index database exists without a superblock; it was created by an older incompatible version"); + throw std::runtime_error( + "Incompatible sparse index: database has no superblock (legacy format)"); + } + + // Fresh database — write the superblock. + sb.format_version = settings::SPARSE_ONDISK_VERSION; + LOG_INFO(2202, + index_id_, + "Writing fresh sparse superblock (version=" + << static_cast(settings::SPARSE_ONDISK_VERSION) << ")"); + if (!writeSuperBlock(txn, sb)) { + return false; + } + return true; + } + + if (sb.format_version != settings::SPARSE_ONDISK_VERSION) { + LOG_ERROR(2203, + index_id_, + "Sparse index format version mismatch: on-disk=" + << static_cast(sb.format_version) + << " compiled=" << static_cast(settings::SPARSE_ONDISK_VERSION)); + throw std::runtime_error( + "Incompatible sparse index: format version " + + std::to_string(sb.format_version) + + " does not match compiled version " + + std::to_string(settings::SPARSE_ONDISK_VERSION)); + } + + return true; + } + + bool InvertedIndex::initialize() { + std::unique_lock lock(mutex_); + + MDBX_txn* txn = nullptr; + int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2204, index_id_, "Failed to begin sparse index init transaction: " << mdbx_strerror(rc)); + return false; + } + + rc = mdbx_dbi_open(txn, + "blocked_term_postings", + MDBX_CREATE | MDBX_INTEGERKEY, + &blocked_term_postings_dbi_); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2205, index_id_, "Failed to open blocked_term_postings DBI: " << mdbx_strerror(rc)); + mdbx_txn_abort(txn); + return false; + } + + if (!validateSuperBlock(txn)) { + mdbx_txn_abort(txn); + return false; + } + + rc = mdbx_txn_commit(txn); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2206, index_id_, "Failed to commit sparse index init transaction: " << mdbx_strerror(rc)); + return false; + } + + if (!loadTermInfo()) { + return false; + } + + LOG_INFO(2207, index_id_, "Sparse index initialized with " << term_info_.size() << " loaded terms"); + return true; + } + + bool InvertedIndex::addDocumentsBatch( + MDBX_txn* txn, + const std::vector>& docs) + { + std::unique_lock lock(mutex_); + return addDocumentsBatchInternal(txn, docs); + } + + bool InvertedIndex::removeDocument(MDBX_txn* txn, + ndd::idInt doc_id, + const SparseVector& vec) + { + std::unique_lock lock(mutex_); + return removeDocumentInternal(txn, doc_id, vec); + } + + size_t InvertedIndex::getTermCount() const { + return term_info_.size(); + } + + size_t InvertedIndex::getVocabSize() const { + return vocab_size_; + } + + template + bool InvertedIndex::accumulateBatchScores(PostingListIterator* it, + ndd::idInt batch_start, + uint32_t batch_end_block_nr, + BlockOffset batch_end_block_offset, + float* scores_buf, + float term_weight) + { + using Accessor = PostingValueAccessor; + using ValueType = typename Accessor::ValueType; + + const BlockOffset* offsets = it->doc_offsets; + const ValueType* vals = static_cast(it->values_ptr); + uint32_t idx = it->current_entry_idx; + uint32_t sz = it->data_size; + float block_max_value = it->max_value; + bool contributed = false; + + while (true) { + if (it->current_block_nr > batch_end_block_nr) { + break; + } + + const bool consume_full_block = it->current_block_nr < batch_end_block_nr; + const int64_t local_base = + static_cast(it->currentBlockBaseDocId()) - static_cast(batch_start); + const uint32_t before = idx; + while (idx < sz && (consume_full_block || offsets[idx] <= batch_end_block_offset)) { + const ValueType value = vals[idx]; + if (Accessor::isLive(value)) { + const size_t local = static_cast(local_base + offsets[idx]); + if constexpr (StoreFloats) { + scores_buf[local] += value * term_weight; + } else { + scores_buf[local] += InvertedIndex::dequantize(value, block_max_value) * term_weight; + } + contributed = true; + } + idx++; + } + it->consumeEntries(idx - before); + + if (idx < sz) { + break; + } + + it->current_entry_idx = idx; + if (!it->loadNextBlock()) { + break; + } + + offsets = it->doc_offsets; + vals = static_cast(it->values_ptr); + block_max_value = it->max_value; + idx = 0; + sz = it->data_size; + + if (it->current_block_nr > batch_end_block_nr + || (it->current_block_nr == batch_end_block_nr + && sz > 0 + && offsets[0] > batch_end_block_offset)) + { + break; + } + } + + it->current_entry_idx = idx; + it->advanceToNextLive(); + return contributed; + } + + std::vector> + InvertedIndex::search(const SparseVector& query, + size_t k, + const ndd::RoaringBitmap* filter) + { + std::shared_lock lock(mutex_); + + MDBX_txn* txn = nullptr; + int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2208, index_id_, "Failed to begin sparse search transaction: " << mdbx_strerror(rc)); + return {}; + } + + if (query.empty() || k == 0) { + mdbx_txn_abort(txn); + return {}; + } + + std::vector iters_storage; + std::vector iters; + std::vector cursors; + + iters_storage.reserve(query.indices.size()); + iters.reserve(query.indices.size()); + cursors.reserve(query.indices.size()); + + { + LOG_TIME("search phase 1"); + + // Build one iterator per live query term. Each iterator owns a cursor and lazily + // streams the term's block rows instead of pulling the whole posting list in memory. + for (size_t qi = 0; qi < query.indices.size(); qi++) { + uint32_t term_id = query.indices[qi]; + if (term_id == kMetadataTermId) continue; + + float qw = query.values[qi]; + if (qw <= 0.0f) continue; + + + auto info_it = term_info_.find(term_id); + if (info_it == term_info_.end()) { + LOG_WARN(2209, index_id_, "Search skipped unknown query term_id=" << term_id); + continue; + } + + bool header_found = false; + PostingListHeader header = readPostingListHeader(txn, term_id, &header_found); + if (!header_found || header.nr_entries == 0 || header.nr_live_entries == 0) { + continue; + } + + MDBX_cursor* cursor = nullptr; + rc = mdbx_cursor_open(txn, blocked_term_postings_dbi_, &cursor); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2210, + index_id_, + "Failed to open sparse search cursor for term " + << term_id << ": " << mdbx_strerror(rc)); + continue; + } + + PostingListIterator it; + it.init(cursor, + term_id, + qw, + info_it->second, + header.nr_entries, + this); + + if (it.current_doc_id != EXHAUSTED_DOC_ID) { + iters_storage.push_back(it); + cursors.push_back(cursor); + } else { + mdbx_cursor_close(cursor); + } + } + + for (size_t i = 0; i < iters_storage.size(); i++) { + iters.push_back(&iters_storage[i]); + } + + if (iters.empty()) { + mdbx_txn_abort(txn); + return {}; + } + + //END OF PHASE 1 + } + + + bool use_pruning = (iters.size() > 1); + float best_min_score = 0.0f; + + std::vector scores_buf(settings::INV_IDX_SEARCH_BATCH_SZ, 0.0f); + std::priority_queue top_results; + float threshold = 0.0f; + + auto minIterDocId = [&iters]() -> ndd::idInt { + ndd::idInt min_id = EXHAUSTED_DOC_ID; + for (size_t i = 0; i < iters.size(); i++) { + if (iters[i]->current_doc_id < min_id) { + min_id = iters[i]->current_doc_id; + } + } + return min_id; + }; + + ndd::idInt min_id = minIterDocId(); + + // Process the index in doc-id windows. The accumulator is dense within the current + // window even though the posting lists themselves stay sparse and block-based. + while (min_id != EXHAUSTED_DOC_ID) { + ndd::idInt batch_start = min_id; + ndd::idInt batch_end = batch_start + + (ndd::idInt)settings::INV_IDX_SEARCH_BATCH_SZ - 1; + if (batch_end < batch_start) { + batch_end = EXHAUSTED_DOC_ID - 1; + } + const uint32_t batch_end_block_nr = docToBlockNr(batch_end); + const BlockOffset batch_end_block_offset = docToBlockOffset(batch_end); + + size_t batch_len = (size_t)(batch_end - batch_start) + 1; + if (batch_len > scores_buf.size()) { + scores_buf.resize(batch_len); + } + std::memset(scores_buf.data(), 0, batch_len * sizeof(float)); + + { + LOG_TIME("search phase 2"); + // Consume all postings that fall into this batch. The iterator keeps absolute doc_ids + // implicit as (current_block_nr, doc_offsets[idx]) to avoid rebuilding them eagerly. + for (size_t i = 0; i < iters.size(); i++) { + PostingListIterator* it = iters[i]; +#ifdef ND_SPARSE_INSTRUMENT + sparseSearchDebugStats().phase2_iterators_visited.fetch_add(1, std::memory_order_relaxed); +#endif // ND_SPARSE_INSTRUMENT + if (it->current_doc_id > batch_end) { + continue; + } + [[maybe_unused]] const bool phase3_contributed = +#if defined(NDD_INV_IDX_STORE_FLOATS) + accumulateBatchScores( + it, + batch_start, + batch_end_block_nr, + batch_end_block_offset, + scores_buf.data(), + it->term_weight); +#else + accumulateBatchScores( + it, + batch_start, + batch_end_block_nr, + batch_end_block_offset, + scores_buf.data(), + it->term_weight); +#endif // NDD_INV_IDX_STORE_FLOATS + +#ifdef ND_SPARSE_INSTRUMENT + if (phase3_contributed) { + sparseSearchDebugStats().phase2_iterators_contributed.fetch_add( + 1, std::memory_order_relaxed); + } +#endif // ND_SPARSE_INSTRUMENT + } + //END OF SEARCH PHASE 2 + } + + { + LOG_TIME("search phase 3"); + // Only scores inside the current batch can be non-zero, so convert that temporary + // dense buffer into top-k candidates before moving to the next window. + for (size_t local = 0; local < batch_len; local++) { + float s = scores_buf[local]; + if (s == 0.0f || s <= threshold) continue; + + ndd::idInt doc_id = batch_start + (ndd::idInt)local; + if (filter && !filter->contains(doc_id)) continue; + + if (top_results.size() < k) { + top_results.emplace(doc_id, s); + if (top_results.size() == k) { + threshold = top_results.top().score; + } + } else if (s > threshold) { + top_results.pop(); + top_results.emplace(doc_id, s); + threshold = top_results.top().score; + } + } + //END OF SEARCH PHASE 3 + } + + { + LOG_TIME("search phase 4"); + // Compact away exhausted iterators, then optionally prune the longest remaining list + // when its best possible future contribution cannot beat the current threshold. + size_t write_idx = 0; + for (size_t i = 0; i < iters.size(); i++) { + if (iters[i]->current_doc_id != EXHAUSTED_DOC_ID) { + iters[write_idx++] = iters[i]; + } + } + iters.resize(write_idx); + if (iters.empty()) break; + + min_id = minIterDocId(); + + if (use_pruning && top_results.size() >= k) { + float new_min_score = threshold; + if (!nearEqual(new_min_score, best_min_score)) { + best_min_score = new_min_score; + pruneLongest(iters, new_min_score); + min_id = minIterDocId(); + } + } + //END OF SEARCH PHASE 4 + } + } + +#ifdef NDD_INV_IDX_PRUNE_DEBUG + for (const PostingListIterator& it : iters_storage) { + LOG_INFO(2229, + index_id_, + "Sparse prune stats: term_id=" << it.term_id + << " posting_list_len=" << it.initial_entries + << " pruned_len=" << it.pruned_entries); + } +#endif // NDD_INV_IDX_PRUNE_DEBUG + + for (MDBX_cursor* cursor : cursors) { + mdbx_cursor_close(cursor); + } + mdbx_txn_abort(txn); + + std::vector> results; + results.reserve(top_results.size()); + while (!top_results.empty()) { + results.push_back( + std::make_pair(top_results.top().doc_id, top_results.top().score)); + top_results.pop(); + } + std::reverse(results.begin(), results.end()); + return results; + } + + inline uint8_t InvertedIndex::quantize(float val, float max_val) { + if (max_val <= settings::NEAR_ZERO) + return 0; + + float scaled = (val / max_val) * UINT8_MAX; + if (scaled >= UINT8_MAX) + return UINT8_MAX; + if (scaled <= 0.0f) + return 0; + + uint8_t result = (uint8_t)(scaled + 0.5f); + + /** + * Since a 0 weight is considered deleted, + * we change it to 1 + */ + return result == 0 ? 1 : result; + } + + inline float InvertedIndex::dequantize(uint8_t val, float max_val) { + if (max_val <= settings::NEAR_ZERO) + return 0.0f; + return (float)val * (max_val / UINT8_MAX); + } + + // ========================================================================= + // SIMD helpers + // ========================================================================= + + size_t InvertedIndex::findDocIdSIMD(const uint32_t* doc_ids, + size_t size, + size_t start_idx, + uint32_t target) const + { + size_t idx = start_idx; + +#if defined(USE_AVX512) + const size_t simd_width = 16; + __m512i target_vec = _mm512_set1_epi32((int)target); + + while (idx + simd_width <= size) { + __m512i data_vec = _mm512_loadu_si512(doc_ids + idx); + __mmask16 mask = _mm512_cmpge_epu32_mask(data_vec, target_vec); + + if (mask != 0) { + return idx + __builtin_ctz(mask); + } + idx += simd_width; + } +#elif defined(USE_AVX2) + const size_t simd_width = 8; + __m256i target_vec = _mm256_set1_epi32((int)target); + + while (idx + simd_width <= size) { + __builtin_prefetch(doc_ids + idx + 32); + if (doc_ids[idx + simd_width - 1] < target) { + idx += simd_width; + continue; + } + + __m256i data_vec = + _mm256_loadu_si256((const __m256i*)(doc_ids + idx)); + __m256i max_vec = _mm256_max_epu32(data_vec, target_vec); + __m256i cmp = _mm256_cmpeq_epi32(max_vec, data_vec); + + int mask = _mm256_movemask_ps(_mm256_castsi256_ps(cmp)); + if (mask != 0) { + return idx + __builtin_ctz(mask); + } + idx += simd_width; + } +#elif defined(USE_SVE2) + svbool_t pg = svwhilelt_b32(idx, size); + svuint32_t target_vec = svdup_u32(target); + + while (svptest_any(svptrue_b32(), pg)) { + svuint32_t data_vec = svld1_u32(pg, doc_ids + idx); + svbool_t cmp = svcmpge_u32(pg, data_vec, target_vec); + + if (svptest_any(pg, cmp)) { + svbool_t before_match = svbrkb_z(pg, cmp); + uint64_t count = svcntp_b32(pg, before_match); + return idx + count; + } + idx += svcntw(); + pg = svwhilelt_b32(idx, size); + } + return idx; +#elif defined(USE_NEON) + const size_t simd_width = 4; + uint32x4_t target_vec = vdupq_n_u32(target); + + while (idx + simd_width <= size) { + uint32x4_t data_vec = vld1q_u32(doc_ids + idx); + uint32x4_t cmp = vcgeq_u32(data_vec, target_vec); + + if (vmaxvq_u32(cmp) != 0) { + for (size_t i = 0; i < simd_width; i++) { + if (doc_ids[idx + i] >= target) { + return idx + i; + } + } + } + idx += simd_width; + } +#endif // USE_AVX512 + + while (idx < size && doc_ids[idx] < target) { + idx++; + } + return idx; + } + + size_t InvertedIndex::findNextLiveSIMD(const uint8_t* values, + size_t size, + size_t start_idx) const + { + size_t idx = start_idx; + +#if defined(USE_AVX512) + const size_t simd_width = 64; + __m512i zero_vec = _mm512_setzero_si512(); + + while (idx + simd_width <= size) { + __m512i data_vec = _mm512_loadu_si512(values + idx); + __mmask64 mask = _mm512_cmpneq_epu8_mask(data_vec, zero_vec); + + if (mask != 0) { + return idx + __builtin_ctzll(mask); + } + idx += simd_width; + } +#elif defined(USE_AVX2) + const size_t simd_width = 32; + __m256i zero_vec = _mm256_setzero_si256(); + + while (idx + simd_width <= size) { + __m256i data_vec = + _mm256_loadu_si256((const __m256i*)(values + idx)); + __m256i cmp = _mm256_cmpeq_epi8(data_vec, zero_vec); + int mask = _mm256_movemask_epi8(cmp); + + if ((uint32_t)mask != 0xFFFFFFFF) { + return idx + __builtin_ctz(~mask); + } + idx += simd_width; + } +#elif defined(USE_NEON) + const size_t simd_width = 16; + uint8x16_t zero_vec = vdupq_n_u8(0); + + while (idx + simd_width <= size) { + uint8x16_t data_vec = vld1q_u8(values + idx); + uint8x16_t cmp = vceqq_u8(data_vec, zero_vec); + + if (vminvq_u8(cmp) == 0) { + for (size_t i = 0; i < simd_width; i++) { + if (values[idx + i] != 0) { + return idx + i; + } + } + } + idx += simd_width; + } +#elif defined(USE_SVE2) + svbool_t pg = svwhilelt_b8(idx, size); + while (svptest_any(svptrue_b8(), pg)) { + svuint8_t data_vec = svld1_u8(pg, values + idx); + svbool_t cmp = svcmpne_n_u8(pg, data_vec, 0); + + if (svptest_any(pg, cmp)) { + svbool_t before_match = svbrkb_z(pg, cmp); + return idx + svcntp_b8(pg, before_match); + } + idx += svcntb(); + pg = svwhilelt_b8(idx, size); + } + return idx; +#endif // USE_AVX512 + + while (idx < size) { + if (values[idx] != 0) return idx; + idx++; + } + return idx; + } + + // ========================================================================= + // Superblock helpers + // ========================================================================= + + bool InvertedIndex::readSuperBlock(MDBX_txn* txn, + SuperBlock* out, + bool* out_found) const { + if (out_found) *out_found = false; + + uint64_t packed = packPostingKey(kMetadataTermId, kSuperBlockBlockNr); + MDBX_val key{&packed, sizeof(packed)}; + MDBX_val data; + + int rc = mdbx_get(txn, blocked_term_postings_dbi_, &key, &data); + if (rc == MDBX_NOTFOUND) { + return true; + } + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2211, index_id_, "readSuperBlock MDBX lookup failed: " << mdbx_strerror(rc)); + return false; + } + if (data.iov_len < sizeof(SuperBlock)) { + LOG_ERROR(2212, index_id_, "Corrupt sparse superblock: payload too small"); + return false; + } + + std::memcpy(out, data.iov_base, sizeof(SuperBlock)); + if (out_found) *out_found = true; + return true; + } + + bool InvertedIndex::writeSuperBlock(MDBX_txn* txn, const SuperBlock& sb) { + uint64_t packed = packPostingKey(kMetadataTermId, kSuperBlockBlockNr); + MDBX_val key{&packed, sizeof(packed)}; + MDBX_val data{const_cast(&sb), sizeof(SuperBlock)}; + + int rc = mdbx_put(txn, blocked_term_postings_dbi_, &key, &data, MDBX_UPSERT); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2213, index_id_, "writeSuperBlock MDBX put failed: " << mdbx_strerror(rc)); + return false; + } + return true; + } + + // ========================================================================= + // Metadata and block helpers + // ========================================================================= + + PostingListHeader InvertedIndex::readPostingListHeader(MDBX_txn* txn, + uint32_t term_id, + bool* out_found) const { + PostingListHeader header; + if (out_found) *out_found = false; + + if (term_id == kMetadataTermId) { + return header; + } + + uint64_t packed = packPostingKey(term_id, kMetadataBlockNr); + MDBX_val key{&packed, sizeof(packed)}; + MDBX_val data; + + int rc = mdbx_get(txn, blocked_term_postings_dbi_, &key, &data); + if (rc == MDBX_SUCCESS && data.iov_len >= sizeof(PostingListHeader)) { + std::memcpy(&header, data.iov_base, sizeof(PostingListHeader)); + if (out_found) *out_found = true; + } + + return header; + } + + bool InvertedIndex::writePostingListHeader(MDBX_txn* txn, + uint32_t term_id, + const PostingListHeader& header) { + if (term_id == kMetadataTermId) { + LOG_ERROR(2214, index_id_, "Refusing to write a posting-list header for the reserved metadata term"); + return false; + } + + uint64_t packed = packPostingKey(term_id, kMetadataBlockNr); + MDBX_val key{&packed, sizeof(packed)}; + MDBX_val data{const_cast(&header), sizeof(PostingListHeader)}; + + int rc = mdbx_put(txn, blocked_term_postings_dbi_, &key, &data, MDBX_UPSERT); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2215, + index_id_, + "Failed to write posting-list header for term " + << term_id << ": " << mdbx_strerror(rc)); + return false; + } + + return true; + } + + bool InvertedIndex::deletePostingListHeader(MDBX_txn* txn, uint32_t term_id) { + uint64_t packed = packPostingKey(term_id, kMetadataBlockNr); + MDBX_val key{&packed, sizeof(packed)}; + + int rc = mdbx_del(txn, blocked_term_postings_dbi_, &key, nullptr); + return rc == MDBX_SUCCESS || rc == MDBX_NOTFOUND; + } + + bool InvertedIndex::parseBlockViewFromValue(const MDBX_val& data, + uint32_t block_nr, + BlockView* out_view) const { + if (!out_view) return false; + if (data.iov_len < sizeof(BlockHeader)) return false; + + const BlockHeader* header = (const BlockHeader*)data.iov_base; + uint32_t n = header->nr_entries; + + const uint8_t* ptr = (const uint8_t*)data.iov_base + sizeof(BlockHeader); + const BlockOffset* doc_offsets = reinterpret_cast(ptr); + ptr += n * sizeof(BlockOffset); + +#if defined(NDD_INV_IDX_STORE_FLOATS) + uint8_t vbits = 32; + const void* values = ptr; + size_t required = sizeof(BlockHeader) + + n * sizeof(BlockOffset) + + n * sizeof(float); +#else + uint8_t vbits = 8; + const void* values = ptr; + size_t required = sizeof(BlockHeader) + + n * sizeof(BlockOffset) + + n * sizeof(uint8_t); +#endif // NDD_INV_IDX_STORE_FLOATS + + if (data.iov_len < required) { + LOG_ERROR(2216, index_id_, "Corrupt sparse block payload: fewer bytes than expected"); + return false; + } + + out_view->doc_offsets = doc_offsets; + out_view->values = values; + out_view->count = n; + out_view->value_bits = vbits; + out_view->max_value = header->max_value; + return true; + } + + bool InvertedIndex::loadBlockEntries(MDBX_txn* txn, + uint32_t term_id, + uint32_t block_nr, + std::vector* entries, + uint32_t* out_live_in_block, + float* out_max_value, + bool* out_found) const + { + if (entries) entries->clear(); + if (out_live_in_block) *out_live_in_block = 0; + if (out_max_value) *out_max_value = 0.0f; + if (out_found) *out_found = false; + + if (term_id == kMetadataTermId || block_nr == kMetadataBlockNr) { + return false; + } + + uint64_t packed = packPostingKey(term_id, block_nr); + MDBX_val key{&packed, sizeof(packed)}; + MDBX_val data; + + int rc = mdbx_get(txn, blocked_term_postings_dbi_, &key, &data); + if (rc == MDBX_NOTFOUND) { + return true; + } + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2217, + index_id_, + "loadBlockEntries MDBX lookup failed for term " + << term_id << " block " << block_nr << ": " << mdbx_strerror(rc)); + return false; + } + + BlockView view; + if (!parseBlockViewFromValue(data, block_nr, &view)) { + LOG_ERROR(2218, index_id_, "Corrupt block payload for term " << term_id << " block " << block_nr); + return false; + } + + const BlockHeader* header = (const BlockHeader*)data.iov_base; + if (out_live_in_block) *out_live_in_block = header->nr_live_in_block; + if (out_max_value) *out_max_value = header->max_value; + if (out_found) *out_found = true; + + if (!entries) { + return true; + } + + // Update/delete paths want a mutable decoded representation, so convert offsets back to + // absolute doc_ids here. Search stays zero-copy and does not call this helper. + entries->resize(view.count); +#if defined(NDD_INV_IDX_STORE_FLOATS) + const float* vals = (const float*)view.values; +#else + const uint8_t* vals = (const uint8_t*)view.values; +#endif // NDD_INV_IDX_STORE_FLOATS + + for (uint32_t i = 0; i < view.count; i++) { + entries->at(i).doc_id = blockOffsetToDocId(block_nr, view.doc_offsets[i]); +#if defined(NDD_INV_IDX_STORE_FLOATS) + entries->at(i).value = vals[i]; +#else + entries->at(i).value = dequantize(vals[i], header->max_value); +#endif // NDD_INV_IDX_STORE_FLOATS + } + + return true; + } + + /** + * Saves the block header and entries + */ + bool InvertedIndex::saveBlockEntries(MDBX_txn* txn, + uint32_t term_id, + uint32_t block_nr, + const std::vector& entries, + uint32_t live_in_block, + float max_val) + { + if (term_id == kMetadataTermId || block_nr == kMetadataBlockNr) { + LOG_ERROR(2219, index_id_, "Refusing to save a reserved metadata key as a sparse data block"); + return false; + } + + if (entries.empty()) { + return deleteBlock(txn, term_id, block_nr); + } + + if (entries.size() > kBlockCapacity) { + LOG_ERROR(2220, + index_id_, + "Block for term " << term_id << " block " << block_nr + << " exceeds fixed capacity " << kBlockCapacity); + return false; + } + + BlockHeader header; + header.nr_entries = (uint16_t)entries.size(); + header.nr_live_in_block = (uint16_t)live_in_block; + header.max_value = max_val; + +#if defined(NDD_INV_IDX_STORE_FLOATS) + size_t value_size = sizeof(float); +#else + size_t value_size = sizeof(uint8_t); +#endif // NDD_INV_IDX_STORE_FLOATS + + size_t total_size = sizeof(BlockHeader) + + (entries.size() * sizeof(BlockOffset)) //doc-local offsets + + (entries.size() * value_size); //doc weights + std::vector buffer(total_size); + + // Serialize back into the compact on-disk layout used by the search iterator. + std::memcpy(buffer.data(), &header, sizeof(BlockHeader)); + + uint8_t* ptr = buffer.data() + sizeof(BlockHeader); + BlockOffset* offsets_out = reinterpret_cast(ptr); + ptr += entries.size() * sizeof(BlockOffset); + + BlockOffset prev_offset = 0; + bool has_prev = false; + for (size_t i = 0; i < entries.size(); i++) { + if (docToBlockNr(entries[i].doc_id) != block_nr) { + LOG_ERROR(2221, + index_id_, + "Entry doc_id " << entries[i].doc_id << " does not belong to term " + << term_id << " block " << block_nr); + return false; + } + + BlockOffset offset = docToBlockOffset(entries[i].doc_id); + if (has_prev && offset <= prev_offset) { + LOG_ERROR(2222, index_id_, "Block entries must be strictly sorted by doc offset"); + return false; + } + offsets_out[i] = offset; + prev_offset = offset; + has_prev = true; + } + +#if defined(NDD_INV_IDX_STORE_FLOATS) + float* vals_out = (float*)ptr; + for (size_t i = 0; i < entries.size(); i++) { + vals_out[i] = entries[i].value; + } +#else + uint8_t* vals_out = ptr; + for (size_t i = 0; i < entries.size(); i++) { + vals_out[i] = quantize(entries[i].value, max_val); + } +#endif // NDD_INV_IDX_STORE_FLOATS + + uint64_t packed = packPostingKey(term_id, block_nr); + MDBX_val key{&packed, sizeof(packed)}; + MDBX_val value{buffer.data(), buffer.size()}; + + int rc = mdbx_put(txn, blocked_term_postings_dbi_, &key, &value, MDBX_UPSERT); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2223, + index_id_, + "saveBlockEntries MDBX put failed for term " + << term_id << " block " << block_nr << ": " << mdbx_strerror(rc)); + return false; + } + + return true; + } + + bool InvertedIndex::deleteBlock(MDBX_txn* txn, uint32_t term_id, uint32_t block_nr) { + uint64_t packed = packPostingKey(term_id, block_nr); + MDBX_val key{&packed, sizeof(packed)}; + + int rc = mdbx_del(txn, blocked_term_postings_dbi_, &key, nullptr); + return rc == MDBX_SUCCESS || rc == MDBX_NOTFOUND; + } + + bool InvertedIndex::iterateTermBlocks( + MDBX_txn* txn, + uint32_t term_id, + const std::function& callback) const { + // Because keys are packed as (term_id, block_nr), all rows for one term are contiguous. + // A single seek is enough to walk every block that belongs to that term. + MDBX_cursor* cursor = nullptr; + int rc = mdbx_cursor_open(txn, blocked_term_postings_dbi_, &cursor); + if (rc != MDBX_SUCCESS) { + return false; + } + + uint64_t seek_packed = packPostingKey(term_id, 0); + MDBX_val key{&seek_packed, sizeof(seek_packed)}; + MDBX_val data; + + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); + while (rc == MDBX_SUCCESS) { + if (key.iov_len != sizeof(uint64_t)) { + break; + } + + uint64_t packed_key; + std::memcpy(&packed_key, key.iov_base, sizeof(uint64_t)); + uint32_t key_term = unpackTermId(packed_key); + uint32_t block_nr = unpackBlockNr(packed_key); + + if (key_term != term_id) { + break; + } + + if (block_nr == kMetadataBlockNr) { + break; + } + + if (!callback(block_nr, data)) { + mdbx_cursor_close(cursor); + return false; + } + + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); + } + + mdbx_cursor_close(cursor); + return true; + } + + float InvertedIndex::recomputeGlobalMaxFromBlocks(MDBX_txn* txn, uint32_t term_id) const { + float recomputed_max = 0.0f; + + // Only needed when the previous global max may have been lowered by an in-place update + // or delete. We then rescan block headers to find the true max for the term. + bool ok = iterateTermBlocks(txn, + term_id, + [&recomputed_max](uint32_t block_nr, const MDBX_val& data) { + if (data.iov_len < sizeof(BlockHeader)) { + return false; + } + const BlockHeader* header = + (const BlockHeader*)data.iov_base; + if (header->max_value > recomputed_max) { + recomputed_max = header->max_value; + } + return true; + }); + + if (!ok) { + return 0.0f; + } + + return recomputed_max; + } + + // ========================================================================= + // Startup + // ========================================================================= + + bool InvertedIndex::loadTermInfo() { + term_info_.clear(); + + MDBX_txn* txn = nullptr; + int rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_RDONLY, &txn); + if (rc != MDBX_SUCCESS) { + LOG_ERROR(2224, index_id_, "Failed to begin loadTermInfo transaction: " << mdbx_strerror(rc)); + return false; + } + + MDBX_cursor* cursor = nullptr; + rc = mdbx_cursor_open(txn, blocked_term_postings_dbi_, &cursor); + if (rc != MDBX_SUCCESS) { + mdbx_txn_abort(txn); + return false; + } + + MDBX_val key, data; + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_FIRST); + while (rc == MDBX_SUCCESS) { + if (key.iov_len == sizeof(uint64_t)) { + uint64_t packed_key; + std::memcpy(&packed_key, key.iov_base, sizeof(uint64_t)); + + uint32_t term_id = unpackTermId(packed_key); + uint32_t block_nr = unpackBlockNr(packed_key); + + if (term_id != kMetadataTermId + && block_nr == kMetadataBlockNr + && data.iov_len >= sizeof(PostingListHeader)) { + PostingListHeader header; + std::memcpy(&header, data.iov_base, sizeof(PostingListHeader)); + + if (header.nr_live_entries > 0 && header.max_value > settings::NEAR_ZERO) { + term_info_[term_id] = header.max_value; + } + } + } + + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); + } + + mdbx_cursor_close(cursor); + mdbx_txn_abort(txn); + LOG_INFO(2225, index_id_, "loadTermInfo loaded " << term_info_.size() << " active terms"); + return true; + } + + // ========================================================================= + // Add / remove internals + // ========================================================================= + + bool InvertedIndex::addDocumentsBatchInternal( + MDBX_txn* txn, + const std::vector>& docs) + { + if (docs.empty()) return true; + + // Reorganize the batch by term so each term can be merged into its posting list + // independently. The on-disk structure is term-major. +#ifdef ND_SPARSE_INSTRUMENT + SparseUpdateDebugStats& update_stats = sparseUpdateDebugStats(); + update_stats.add_batch_calls.fetch_add(1, std::memory_order_relaxed); + update_stats.add_batch_docs.fetch_add(docs.size(), std::memory_order_relaxed); + uint64_t raw_update_count = 0; + const auto build_term_updates_start = SteadyClock::now(); +#endif // ND_SPARSE_INSTRUMENT + + std::unordered_map>> term_updates; + + for (const auto& [doc_id, sparse_vec] : docs) { +#ifdef ND_SPARSE_INSTRUMENT + raw_update_count += sparse_vec.indices.size(); +#endif // ND_SPARSE_INSTRUMENT + for (size_t i = 0; i < sparse_vec.indices.size(); i++) { + uint32_t term_id = sparse_vec.indices[i]; + if (term_id == kMetadataTermId) { + LOG_ERROR(2226, index_id_, "term_id UINT32_MAX is reserved for sparse metadata"); + return false; + } + term_updates[term_id].push_back(std::make_pair(doc_id, sparse_vec.values[i])); + } + } + +#ifdef ND_SPARSE_INSTRUMENT + update_stats.add_batch_raw_updates.fetch_add(raw_update_count, std::memory_order_relaxed); + update_stats.add_batch_terms.fetch_add(term_updates.size(), std::memory_order_relaxed); + update_stats.build_term_updates_total_ns.fetch_add( + elapsedNsSince(build_term_updates_start), std::memory_order_relaxed); +#endif // ND_SPARSE_INSTRUMENT + + for (auto& [term_id, updates] : term_updates) { +#ifdef ND_SPARSE_INSTRUMENT + const auto sort_dedup_start = SteadyClock::now(); +#endif // ND_SPARSE_INSTRUMENT + + // Merge logic below assumes doc_ids are sorted and unique per term within this batch. + std::sort(updates.begin(), updates.end(), + [](const auto& a, const auto& b) { return a.first < b.first; }); + + // Keep only the last update per doc_id if duplicates are found. + std::vector> deduped; + deduped.reserve(updates.size()); + for (const auto& u : updates) { + if (!deduped.empty() && deduped.back().first == u.first) { + deduped.back().second = u.second; + } else { + deduped.push_back(u); + } + } + +#ifdef ND_SPARSE_INSTRUMENT + update_stats.sort_dedup_total_ns.fetch_add( + elapsedNsSince(sort_dedup_start), std::memory_order_relaxed); + update_stats.add_batch_deduped_updates.fetch_add( + deduped.size(), std::memory_order_relaxed); +#endif // ND_SPARSE_INSTRUMENT + + bool header_found = false; + PostingListHeader header = readPostingListHeader(txn, term_id, &header_found); + float old_global_max = header.max_value; + bool need_recompute_max = false; + + size_t ui = 0; + while (ui < deduped.size()) { + uint32_t block_nr = docToBlockNr(deduped[ui].first); + size_t block_begin = ui; + while (ui < deduped.size() && docToBlockNr(deduped[ui].first) == block_nr) { + ui++; + } + +#ifdef ND_SPARSE_INSTRUMENT + update_stats.add_batch_blocks.fetch_add(1, std::memory_order_relaxed); +#endif // ND_SPARSE_INSTRUMENT + + // One MDBX record stores exactly one (term, block_nr) slice, so split the + // term's updates into block-local chunks before merging. + std::vector> block_updates( + deduped.begin() + block_begin, deduped.begin() + ui); + + std::vector existing; + uint32_t old_live_in_block = 0; + float old_block_max = 0.0f; + bool block_found = false; + +#ifdef ND_SPARSE_INSTRUMENT + const auto load_block_start = SteadyClock::now(); +#endif // ND_SPARSE_INSTRUMENT + bool load_ok = loadBlockEntries(txn, + term_id, + block_nr, + &existing, + &old_live_in_block, + &old_block_max, + &block_found); +#ifdef ND_SPARSE_INSTRUMENT + update_stats.load_block_calls.fetch_add(1, std::memory_order_relaxed); + update_stats.load_block_total_ns.fetch_add( + elapsedNsSince(load_block_start), std::memory_order_relaxed); + update_stats.load_block_entries_total.fetch_add( + existing.size(), std::memory_order_relaxed); +#endif // ND_SPARSE_INSTRUMENT + if (!load_ok) { + return false; + } + +#ifdef ND_SPARSE_INSTRUMENT + const auto merge_start = SteadyClock::now(); +#endif // ND_SPARSE_INSTRUMENT + // Classic merge of two sorted streams: existing postings in the block and the + // incoming updates for that same block. + std::vector merged; + merged.reserve(existing.size() + block_updates.size()); + + size_t ei = 0; + size_t bi = 0; + while (ei < existing.size() && bi < block_updates.size()) { + ndd::idInt existing_id = existing[ei].doc_id; + ndd::idInt update_id = block_updates[bi].first; + + if (existing_id < update_id) { + merged.push_back(existing[ei]); + ei++; + } else if (existing_id > update_id) { + merged.push_back(PostingListEntry(update_id, block_updates[bi].second)); + bi++; + } else { + merged.push_back(PostingListEntry(update_id, block_updates[bi].second)); + ei++; + bi++; + } + } + while (ei < existing.size()) { + merged.push_back(existing[ei]); + ei++; + } + while (bi < block_updates.size()) { + merged.push_back(PostingListEntry(block_updates[bi].first, + block_updates[bi].second)); + bi++; + } + + uint32_t new_live_in_block = 0; + float new_block_max = 0.0f; + for (const auto& e : merged) { + if (e.value > 0.0f) { + new_live_in_block++; + if (e.value > new_block_max) new_block_max = e.value; + } else if (e.value == 0.0f) { + LOG_WARN(2227, + index_id_, + "addDocumentsBatch received zero value for term " + << term_id << "; entry will be treated as deleted"); + } else { + LOG_WARN(2228, + index_id_, + "addDocumentsBatch received negative value " << e.value + << " for term " << term_id + << "; treating as dead"); + } + } + +#ifdef ND_SPARSE_INSTRUMENT + update_stats.merge_block_calls.fetch_add(1, std::memory_order_relaxed); + update_stats.merge_block_total_ns.fetch_add( + elapsedNsSince(merge_start), std::memory_order_relaxed); + update_stats.merge_existing_entries_total.fetch_add( + existing.size(), std::memory_order_relaxed); + update_stats.merge_update_entries_total.fetch_add( + block_updates.size(), std::memory_order_relaxed); + update_stats.merge_output_entries_total.fetch_add( + merged.size(), std::memory_order_relaxed); +#endif // ND_SPARSE_INSTRUMENT + + uint32_t old_total = static_cast(existing.size()); + uint32_t new_total = static_cast(merged.size()); + applyHeaderDelta(header, + static_cast(new_total) - static_cast(old_total), + static_cast(new_live_in_block) + - static_cast(old_live_in_block)); + + if (merged.empty()) { + if (!deleteBlock(txn, term_id, block_nr)) return false; + } else { +#ifdef ND_SPARSE_INSTRUMENT + const auto save_block_start = SteadyClock::now(); +#endif // ND_SPARSE_INSTRUMENT + bool save_ok = saveBlockEntries(txn, + term_id, + block_nr, + merged, + new_live_in_block, + new_block_max); +#ifdef ND_SPARSE_INSTRUMENT + update_stats.save_block_calls.fetch_add(1, std::memory_order_relaxed); + update_stats.save_block_total_ns.fetch_add( + elapsedNsSince(save_block_start), std::memory_order_relaxed); + update_stats.save_block_entries_total.fetch_add( + merged.size(), std::memory_order_relaxed); +#endif // ND_SPARSE_INSTRUMENT + if (!save_ok) { + return false; + } + } + + if (new_block_max > header.max_value) { + header.max_value = new_block_max; + } + + /** + * if the old_global_max was from this block's max and + * if this block's max has changed and + * if the new block max is less than old_global_max + * then we need to recompute the global max from all blocks. + * + * recompute global max once all the blocks have been updated + * from this document batch. + */ + if (old_block_max > 0.0f && nearEqual(old_block_max, old_global_max) + && new_block_max + settings::NEAR_ZERO < old_global_max) { + need_recompute_max = true; + } + } + + if (header.nr_entries == 0) { + if (!deletePostingListHeader(txn, term_id)) return false; + term_info_.erase(term_id); + continue; + } + + // Recompute the term max only when the previous max might have been invalidated. + if (need_recompute_max) { +#ifdef ND_SPARSE_INSTRUMENT + const auto recompute_max_start = SteadyClock::now(); +#endif // ND_SPARSE_INSTRUMENT + header.max_value = recomputeGlobalMaxFromBlocks(txn, term_id); +#ifdef ND_SPARSE_INSTRUMENT + update_stats.recompute_max_calls.fetch_add(1, std::memory_order_relaxed); + update_stats.recompute_max_total_ns.fetch_add( + elapsedNsSince(recompute_max_start), std::memory_order_relaxed); +#endif // ND_SPARSE_INSTRUMENT + } //while (ui < deduped.size()) + + if (header.nr_live_entries == 0) { + header.max_value = 0.0f; + } + + if (!writePostingListHeader(txn, term_id, header)) { + return false; + } + + if (header.nr_live_entries > 0 && header.max_value > settings::NEAR_ZERO) { + term_info_[term_id] = header.max_value; + } else { + term_info_.erase(term_id); + } + } + + return true; + } + + + bool InvertedIndex::removeDocumentInternal(MDBX_txn* txn, + ndd::idInt doc_id, + const SparseVector& vec) + { + /** + * NOTE: This can be slow right now since we provide a single vector to delete + * at once. It should ideally be faster with a batch. + */ + for (size_t i = 0; i < vec.indices.size(); i++) { + uint32_t term_id = vec.indices[i]; + if (term_id == kMetadataTermId) continue; + + bool header_found = false; + PostingListHeader header = readPostingListHeader(txn, term_id, &header_found); + if (!header_found || header.nr_entries == 0) continue; + + uint32_t block_nr = docToBlockNr(doc_id); + + std::vector entries; + uint32_t old_live_in_block = 0; + float old_block_max = 0.0f; + bool block_found = false; + + if (!loadBlockEntries(txn, + term_id, + block_nr, + &entries, + &old_live_in_block, + &old_block_max, + &block_found)) { + return false; + } + if (!block_found || entries.empty()) continue; + + size_t lo = 0; + size_t hi = entries.size(); + while (lo < hi) { + size_t mid = lo + (hi - lo) / 2; + if (entries[mid].doc_id < doc_id) { + lo = mid + 1; + } else { + hi = mid; + } + } + + if (lo >= entries.size() || entries[lo].doc_id != doc_id) { + continue; + } + + if (entries[lo].value <= 0.0f) { + continue; + } + + // Deletes are represented as zero-valued tombstones until the tombstone ratio + // is high enough to justify compacting the block in place. + entries[lo].value = 0.0f; + + uint32_t new_live_in_block = old_live_in_block > 0 ? old_live_in_block - 1 : 0; + uint32_t old_total = static_cast(entries.size()); + + float tombstone_ratio = old_total > 0 + ? (float)(old_total - new_live_in_block) / (float)old_total + : 0.0f; + + if (tombstone_ratio >= settings::INV_IDX_COMPACTION_TOMBSTONE_RATIO) { + //Compact deleted entries + size_t write = 0; + for (size_t j = 0; j < entries.size(); j++) { + if (entries[j].value > 0.0f) { + entries[write++] = entries[j]; + } + } + entries.resize(write); + } + + new_live_in_block = 0; + float new_block_max = 0.0f; + for (const auto& e : entries) { + if (e.value > 0.0f) { + new_live_in_block++; + if (e.value > new_block_max) new_block_max = e.value; + } + } + + uint32_t new_total = static_cast(entries.size()); + applyHeaderDelta(header, + static_cast(new_total) - static_cast(old_total), + static_cast(new_live_in_block) + - static_cast(old_live_in_block)); + + bool need_recompute_max = false; + if (old_block_max > 0.0f && nearEqual(old_block_max, header.max_value) + && new_block_max + settings::NEAR_ZERO < header.max_value) { + need_recompute_max = true; + } + + if (entries.empty()) { + if (!deleteBlock(txn, term_id, block_nr)) return false; + } else { + if (!saveBlockEntries(txn, + term_id, + block_nr, + entries, + new_live_in_block, + new_block_max)) { + return false; + } + } + + if (header.nr_entries == 0) { + if (!deletePostingListHeader(txn, term_id)) return false; + term_info_.erase(term_id); + continue; + } + + if (need_recompute_max) { + header.max_value = recomputeGlobalMaxFromBlocks(txn, term_id); + } + + if (header.nr_live_entries == 0) { + header.max_value = 0.0f; + } + + if (!writePostingListHeader(txn, term_id, header)) { + return false; + } + + if (header.nr_live_entries > 0 && header.max_value > settings::NEAR_ZERO) { + term_info_[term_id] = header.max_value; + } else { + term_info_.erase(term_id); + } + } + + return true; + } + + // ========================================================================= + // Pruning + // ========================================================================= + + void InvertedIndex::pruneLongest(std::vector& iters, + float min_score) + { + if (iters.size() < 2) return; + + // Pruning only ever advances the single longest remaining list. That keeps the rule + // simple: if even its maximum possible future contribution cannot beat the current + // threshold, skip ahead to where the other lists resume. + size_t longest_idx = 0; + uint32_t longest_rem = 0; + for (size_t i = 0; i < iters.size(); i++) { + uint32_t rem = iters[i]->remainingEntries(); + if (rem > longest_rem) { + longest_rem = rem; + longest_idx = i; + } + } + + if (longest_idx != 0) { + PostingListIterator* tmp = iters[0]; + iters[0] = iters[longest_idx]; + iters[longest_idx] = tmp; + } + + PostingListIterator* longest = iters[0]; + if (longest->current_doc_id == EXHAUSTED_DOC_ID) return; + + ndd::idInt longest_doc = longest->current_doc_id; + + ndd::idInt others_min_doc_id = EXHAUSTED_DOC_ID; + for (size_t i = 1; i < iters.size(); i++) { + if (iters[i]->current_doc_id < others_min_doc_id) { + others_min_doc_id = iters[i]->current_doc_id; + } + } + + if (others_min_doc_id <= longest_doc) return; + + float max_possible = longest->upperBound(); + + if (max_possible <= min_score) { +#ifdef NDD_INV_IDX_PRUNE_DEBUG + uint32_t remaining_before_prune = longest->remaining_entries; +#endif // NDD_INV_IDX_PRUNE_DEBUG + if (others_min_doc_id == EXHAUSTED_DOC_ID) { + longest->current_doc_id = EXHAUSTED_DOC_ID; + longest->remaining_entries = 0; + } else { + longest->advance(others_min_doc_id); + } +#ifdef NDD_INV_IDX_PRUNE_DEBUG + if (remaining_before_prune > longest->remaining_entries) { + longest->pruned_entries += + (remaining_before_prune - longest->remaining_entries); + } +#endif // NDD_INV_IDX_PRUNE_DEBUG + } + } + + // ========================================================================= + // PostingListIterator methods + // ========================================================================= + + void InvertedIndex::PostingListIterator::init(MDBX_cursor* cursor_in, + uint32_t tid, + float tw, + float gmax, + uint32_t total_entries, + const InvertedIndex* idx) { + cursor = cursor_in; + term_id = tid; + term_weight = tw; + global_max = gmax; + index = idx; + + current_block_nr = 0; + doc_offsets = nullptr; + values_ptr = nullptr; + data_size = 0; + value_bits = 0; + max_value = 0.0f; + + current_entry_idx = 0; + current_doc_id = EXHAUSTED_DOC_ID; + remaining_entries = total_entries; +#ifdef NDD_INV_IDX_PRUNE_DEBUG + initial_entries = total_entries; + pruned_entries = 0; +#endif // NDD_INV_IDX_PRUNE_DEBUG + + // Position the iterator on the first non-empty block and then on the first live entry + // inside that block. + if (!loadFirstBlock()) { + current_doc_id = EXHAUSTED_DOC_ID; + remaining_entries = 0; + return; + } + + current_entry_idx = 0; + advanceToNextLive(); + } + + bool InvertedIndex::PostingListIterator::parseCurrentKV(const MDBX_val& key, + const MDBX_val& data) { +#ifdef ND_SPARSE_INSTRUMENT + ParseCurrentKVTimer parse_timer; +#endif // ND_SPARSE_INSTRUMENT + if (key.iov_len != sizeof(uint64_t)) { + return false; + } + + uint64_t packed_key; + std::memcpy(&packed_key, key.iov_base, sizeof(uint64_t)); + uint32_t key_term = unpackTermId(packed_key); + uint32_t block_nr = unpackBlockNr(packed_key); + + if (key_term != term_id || block_nr == kMetadataBlockNr) { + return false; + } + + BlockView view; + if (!index->parseBlockViewFromValue(data, block_nr, &view)) { + return false; + } + + // Keep raw pointers into the MDBX value so search can read offsets/weights without + // allocating or copying the block payload. + current_block_nr = block_nr; + doc_offsets = view.doc_offsets; + values_ptr = view.values; + data_size = view.count; + value_bits = view.value_bits; + max_value = view.max_value; + current_entry_idx = 0; + + return true; + } + + bool InvertedIndex::PostingListIterator::loadFirstBlock() { + uint64_t seek_packed = packPostingKey(term_id, 0); + MDBX_val key{&seek_packed, sizeof(seek_packed)}; + MDBX_val data; + + // Seek once into the contiguous key range for this term, then skip any empty blocks. + int rc = mdbx_cursor_get(cursor, &key, &data, MDBX_SET_RANGE); + while (rc == MDBX_SUCCESS) { + if (key.iov_len != sizeof(uint64_t)) return false; + + uint64_t packed_key; + std::memcpy(&packed_key, key.iov_base, sizeof(uint64_t)); + uint32_t key_term = unpackTermId(packed_key); + uint32_t block_nr = unpackBlockNr(packed_key); + + if (key_term != term_id || block_nr == kMetadataBlockNr) { + return false; + } + + if (!parseCurrentKV(key, data)) { + return false; + } + + if (data_size == 0) { + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); + continue; + } + + return true; + } + + return false; + } + + bool InvertedIndex::PostingListIterator::loadNextBlock() { + // LOG_TIME("loadNextBlock"); this function is not slow + MDBX_val key; + MDBX_val data; + int rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); + + // Stop as soon as the cursor leaves this term's key range. The next term or metadata row + // belongs to a different posting list. + while (rc == MDBX_SUCCESS) { + if (key.iov_len != sizeof(uint64_t)) { + current_doc_id = EXHAUSTED_DOC_ID; + data_size = 0; + return false; + } + + uint64_t packed_key; + std::memcpy(&packed_key, key.iov_base, sizeof(uint64_t)); + uint32_t key_term = unpackTermId(packed_key); + uint32_t block_nr = unpackBlockNr(packed_key); + + if (key_term != term_id || block_nr == kMetadataBlockNr) { + current_doc_id = EXHAUSTED_DOC_ID; + data_size = 0; + return false; + } + + if (!parseCurrentKV(key, data)) { + current_doc_id = EXHAUSTED_DOC_ID; + data_size = 0; + return false; + } + + if (data_size == 0) { + rc = mdbx_cursor_get(cursor, &key, &data, MDBX_NEXT); + continue; + } + + return true; + } + + current_doc_id = EXHAUSTED_DOC_ID; + data_size = 0; + return false; + } + + void InvertedIndex::PostingListIterator::advanceToNextLive() { + // LOG_TIME("advanceToNextLive"); //this function is also not slow + while (true) { + if (value_bits == 32) { + const float* vals = (const float*)values_ptr; + while (current_entry_idx < data_size && vals[current_entry_idx] <= 0.0f) { + consumeEntries(1); + current_entry_idx++; + } + } else { + uint32_t next_live = static_cast(index->findNextLiveSIMD( + (const uint8_t*)values_ptr, + data_size, + current_entry_idx)); + consumeEntries(next_live - current_entry_idx); + current_entry_idx = next_live; + } + + if (current_entry_idx < data_size) { + // Found the next non-zero value in the current block. + current_doc_id = docIdAt(current_entry_idx); + return; + } + + // Current block is exhausted; keep scanning forward until we find another non-empty block + // or run out of rows for this term. + if (!loadNextBlock()) { + current_doc_id = EXHAUSTED_DOC_ID; + return; + } + + current_entry_idx = 0; + } + } + + void InvertedIndex::PostingListIterator::next() { + if (current_doc_id == EXHAUSTED_DOC_ID) return; + consumeEntries(1); + current_entry_idx++; + advanceToNextLive(); + } + + void InvertedIndex::PostingListIterator::advance(ndd::idInt target_doc_id) { + if (current_doc_id == EXHAUSTED_DOC_ID || current_doc_id >= target_doc_id) { + return; + } + + while (true) { + if (current_doc_id == EXHAUSTED_DOC_ID) return; + + const uint32_t target_block_nr = docToBlockNr(target_doc_id); + if (current_block_nr < target_block_nr) { + // Target is in a later block, so skip the remainder of the current block at once. + consumeEntries(data_size - current_entry_idx); + if (!loadNextBlock()) { + current_doc_id = EXHAUSTED_DOC_ID; + break; + } + current_entry_idx = 0; + continue; + } + + if (current_block_nr > target_block_nr) { + current_entry_idx = 0; + advanceToNextLive(); + break; + } + + const BlockOffset target_offset = docToBlockOffset(target_doc_id); + // Within the block, offsets are sorted, so a lower_bound finds the first candidate + // doc_id without decoding the entire block into absolute ids. + const BlockOffset* begin = doc_offsets + current_entry_idx; + const BlockOffset* end = doc_offsets + data_size; + const BlockOffset* next = + std::lower_bound(begin, end, target_offset); + uint32_t next_idx = static_cast(next - doc_offsets); + + consumeEntries(next_idx - current_entry_idx); + current_entry_idx = next_idx; + advanceToNextLive(); + break; + } + } + +} // namespace ndd diff --git a/src/sparse/inverted_index.hpp b/src/sparse/inverted_index.hpp new file mode 100644 index 0000000000..f19df89bb1 --- /dev/null +++ b/src/sparse/inverted_index.hpp @@ -0,0 +1,339 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__x86_64__) || defined(_M_X64) +# include +#elif defined(__aarch64__) || defined(_M_ARM64) +# include +#endif // defined(__x86_64__) || defined(_M_X64) + +#include "mdbx/mdbx.h" +#include "../core/types.hpp" +#include "../utils/log.hpp" +#include "../utils/settings.hpp" +#include "sparse_vector.hpp" + +namespace ndd { + + static constexpr ndd::idInt EXHAUSTED_DOC_ID = std::numeric_limits::max(); + +#pragma pack(push, 1) + // Per-term metadata stored under the reserved metadata key for that term. + struct PostingListHeader { + uint32_t nr_entries = 0; + uint32_t nr_live_entries = 0; + float max_value = 0.0f; + }; + + // Header that prefixes each on-disk (term_id, block_nr) payload. + struct BlockHeader { + uint16_t nr_entries = 0; + uint16_t nr_live_in_block = 0; + float max_value = 0.0f; + }; + + // Single metadata row stored at packPostingKey(kMetadataTermId, kSuperBlockBlockNr). + // Checked on initialize() to reject incompatible databases. + struct SuperBlock { + uint8_t format_version = 0; + }; +#pragma pack(pop) + + // Fully decoded posting entry used only in update/delete paths. + struct PostingListEntry { + ndd::idInt doc_id; + float value; + + PostingListEntry() : doc_id(0), value(0.0f) {} + PostingListEntry(ndd::idInt id, float val) : doc_id(id), value(val) {} + }; + + struct ScoredDoc { + ndd::idInt doc_id; + float score; + + ScoredDoc(ndd::idInt id, float s) : doc_id(id), score(s) {} + + bool operator<(const ScoredDoc& other) const { + // Reverse ordering so std::priority_queue behaves like a min-heap on score. + return score > other.score; + } + }; + + // MDBX-backed sparse inverted index. Search walks zero-copy block views directly, + // while update/delete paths decode a block into PostingListEntry objects, merge, + // and write the block back. + class InvertedIndex { + public: + InvertedIndex(MDBX_env* env, size_t vocab_size, const std::string& index_id); + ~InvertedIndex() = default; + + bool initialize(); + + bool addDocumentsBatch(MDBX_txn* txn, + const std::vector>& docs); + + bool removeDocument(MDBX_txn* txn, ndd::idInt doc_id, const SparseVector& vec); + + size_t getTermCount() const; + size_t getVocabSize() const; + + std::vector>search(const SparseVector& query, + size_t k, + const ndd::RoaringBitmap* filter = nullptr); + + private: + friend class InvertedIndexTestPeer; + + MDBX_env* env_; + MDBX_dbi blocked_term_postings_dbi_; + size_t vocab_size_; + std::string index_id_; + + // Cached per-term max values loaded from posting-list metadata. Search uses this + // to skip absent terms quickly and to compute pruning bounds. + std::unordered_map term_info_; + + mutable std::shared_mutex mutex_; + + using BlockOffset = uint16_t; + static constexpr uint32_t kBlockCapacity = std::numeric_limits::max(); + // Sentinel IDs reserved for metadata rows in blocked_term_postings. + static constexpr uint32_t kMetadataTermId = std::numeric_limits::max(); + static constexpr uint32_t kMetadataBlockNr = std::numeric_limits::max(); + static constexpr uint32_t kSuperBlockBlockNr = 0; + + static inline uint8_t quantize(float val, float max_val); + static inline float dequantize(uint8_t val, float max_val); + static inline bool nearEqual(float a, float b) { + return std::fabs(a - b) <= settings::NEAR_ZERO; + } + + // Key packing is term_id in high 32 bits and block_nr in low 32 bits. + // This keeps all keys for a term contiguous so range scans can seek to + // [pack(term, 0), pack(term, UINT32_MAX)] efficiently. + static inline uint64_t packPostingKey(uint32_t term_id, uint32_t block_nr) { + return (static_cast(term_id) << 32) | static_cast(block_nr); + } + + static inline uint32_t unpackTermId(uint64_t packed_key) { + return static_cast(packed_key >> 32); + } + + static inline uint32_t unpackBlockNr(uint64_t packed_key) { + return static_cast(packed_key & 0xFFFFFFFFULL); + } + + static inline uint32_t docToBlockNr(ndd::idInt doc_id) { + return static_cast(doc_id / kBlockCapacity); + } + + static inline BlockOffset docToBlockOffset(ndd::idInt doc_id) { + return static_cast(doc_id % kBlockCapacity); + } + + static inline ndd::idInt blockOffsetToDocId(uint32_t block_nr, BlockOffset block_offset) { + uint64_t base = static_cast(block_nr) + * static_cast(kBlockCapacity); + return static_cast(base + static_cast(block_offset)); + } + + // Zero-copy view into a block payload owned by MDBX. Pointers remain valid only while + // the surrounding transaction/cursor stays alive and on the same record. + struct BlockView { + const BlockOffset* doc_offsets; + const void* values; + uint32_t count; + uint8_t value_bits; + float max_value; + }; + + // Cursor-backed iterator over one term. It only keeps the current block in memory and + // advances across MDBX records as search/pruning consumes entries. + struct PostingListIterator { + uint32_t term_id; + float term_weight; + float global_max; + const InvertedIndex* index; + + // Cursor positioned somewhere within this term's contiguous MDBX key range. + MDBX_cursor* cursor; + uint32_t current_block_nr; + + // Zero-copy pointers for the current block. + const BlockOffset* doc_offsets; + const void* values_ptr; + uint32_t data_size; + uint8_t value_bits; + float max_value; + + uint32_t current_entry_idx; + ndd::idInt current_doc_id; + + // This is maintained incrementally from posting-list metadata, + // so pruning can estimate list length without scanning all blocks. + uint32_t remaining_entries; + +#ifdef NDD_INV_IDX_PRUNE_DEBUG + uint32_t initial_entries; + uint32_t pruned_entries; +#endif // NDD_INV_IDX_PRUNE_DEBUG + + void init(MDBX_cursor* cursor, + uint32_t term_id, + float term_weight, + float global_max, + uint32_t total_entries, + const InvertedIndex* index); + + inline float valueAt(uint32_t idx) const { + if (value_bits == 32) { + return ((const float*)values_ptr)[idx]; + } + return dequantize(((const uint8_t*)values_ptr)[idx], max_value); + } + + inline bool isLiveAt(uint32_t idx) const { + if (value_bits == 32) { + return ((const float*)values_ptr)[idx] > 0.0f; + } + return ((const uint8_t*)values_ptr)[idx] > 0; + } + + inline float currentValue() const { + return valueAt(current_entry_idx); + } + + void advanceToNextLive(); + void next(); + void advance(ndd::idInt target_doc_id); + + float upperBound() const { + return global_max * term_weight; + } + + uint32_t remainingEntries() const { + if (current_doc_id == EXHAUSTED_DOC_ID) return 0; + return remaining_entries; + } + + bool loadNextBlock(); + bool loadFirstBlock(); + bool parseCurrentKV(const MDBX_val& key, const MDBX_val& data); + + inline void consumeEntries(uint32_t count) { + // Pruning relies on remaining_entries being conservative and monotonic. + if (count >= remaining_entries) { + remaining_entries = 0; + } else { + remaining_entries -= count; + } + } + + inline ndd::idInt currentBlockBaseDocId() const { + return blockOffsetToDocId(current_block_nr, 0); + } + + inline ndd::idInt docIdAt(uint32_t idx) const { + return blockOffsetToDocId(current_block_nr, doc_offsets[idx]); + } + + private: + static inline float dequantize(uint8_t val, float max_val) { + if (max_val <= settings::NEAR_ZERO) return 0.0f; + return (float)val * (max_val / UINT8_MAX); + } + }; + + size_t findDocIdSIMD(const uint32_t* doc_ids, + size_t size, + size_t start_idx, + uint32_t target) const; + + size_t findNextLiveSIMD(const uint8_t* values, + size_t size, + size_t start_idx) const; + + template + static bool accumulateBatchScores(PostingListIterator* it, + ndd::idInt batch_start, + uint32_t batch_end_block_nr, + BlockOffset batch_end_block_offset, + float* scores_buf, + float term_weight); + + PostingListHeader readPostingListHeader(MDBX_txn* txn, + uint32_t term_id, + bool* out_found = nullptr) const; + + bool writePostingListHeader(MDBX_txn* txn, + uint32_t term_id, + const PostingListHeader& header); + + bool deletePostingListHeader(MDBX_txn* txn, uint32_t term_id); + + bool loadBlockEntries(MDBX_txn* txn, + uint32_t term_id, + uint32_t block_nr, + std::vector* entries, + uint32_t* out_live_in_block, + float* out_max_value, + bool* out_found) const; + + bool saveBlockEntries(MDBX_txn* txn, + uint32_t term_id, + uint32_t block_nr, + const std::vector& entries, + uint32_t live_in_block, + float max_val); + + bool deleteBlock(MDBX_txn* txn, uint32_t term_id, uint32_t block_nr); + + bool parseBlockViewFromValue(const MDBX_val& data, + uint32_t block_nr, + BlockView* out_view) const; + + bool iterateTermBlocks( + MDBX_txn* txn, + uint32_t term_id, + const std::function& callback) const; + + float recomputeGlobalMaxFromBlocks(MDBX_txn* txn, uint32_t term_id) const; + + static void applyHeaderDelta(PostingListHeader& header, + int64_t total_delta, + int64_t live_delta); + + bool loadTermInfo(); + + bool readSuperBlock(MDBX_txn* txn, SuperBlock* out, bool* out_found) const; + bool writeSuperBlock(MDBX_txn* txn, const SuperBlock& sb); + bool validateSuperBlock(MDBX_txn* txn); + + bool addDocumentsBatchInternal( + MDBX_txn* txn, + const std::vector>& docs); + + bool removeDocumentInternal(MDBX_txn* txn, + ndd::idInt doc_id, + const SparseVector& vec); + + void pruneLongest(std::vector& iters, float min_score); + }; + + void printSparseSearchDebugStats(); + void printSparseUpdateDebugStats(); + +} // namespace ndd diff --git a/src/sparse/sparse_storage.hpp b/src/sparse/sparse_storage.hpp index 2ea238171e..e55c48e446 100644 --- a/src/sparse/sparse_storage.hpp +++ b/src/sparse/sparse_storage.hpp @@ -8,18 +8,20 @@ #include #include #include "mdbx/mdbx.h" -#include "bmw.hpp" -#include "sparse_vector.hpp" +#include "inverted_index.hpp" #include "../utils/log.hpp" namespace ndd { + // Thin storage facade that keeps the raw sparse vectors and the derived + // inverted index in the same MDBX environment and updates them transactionally. class SparseVectorStorage { public: - explicit SparseVectorStorage(const std::string& db_path) : + SparseVectorStorage(const std::string& db_path, const std::string& index_id) : db_path_(db_path), + index_id_(index_id), env_(nullptr) { - bmw_index_ = nullptr; + sparse_index_ = nullptr; } ~SparseVectorStorage() { closeMDBX(); } @@ -30,12 +32,15 @@ namespace ndd { return false; } - bmw_index_ = std::make_unique(env_, 0); // Vocab size unknown/dynamic - if(!bmw_index_->initialize()) { + sparse_index_ = std::make_unique(env_, 0, index_id_); + if(!sparse_index_->initialize()) { return false; } updateVectorCount(); + LOG_INFO(2241, + index_id_, + "SparseVectorStorage initialized at " << db_path_ << " with " << vector_count_ << " vectors"); return true; } @@ -51,7 +56,7 @@ namespace ndd { storage_->env_, nullptr, static_cast(flags), &txn_); if(rc != 0) { throw std::runtime_error("Failed to begin transaction: " - + std::string(mdbx_strerror(rc))); + + std::string(mdbx_strerror(rc))); } } @@ -87,18 +92,18 @@ namespace ndd { return false; } - // 1. Store in docs DB + // Always write the source-of-truth document payload first, then update the + // derived inverted index in the same transaction. if(!storage_->storeVectorInternal(txn_, doc_id, vec)) { return false; } - // 2. Update Index - if(!storage_->bmw_index_->addDocumentsBatch(txn_, {{doc_id, vec}})) { + if(!storage_->sparse_index_->addDocumentsBatch(txn_, {{doc_id, vec}})) { return false; } - // 3. Save Metadata (Handled internally by BMWIndex per term) - // if (!storage_->bmw_index_->saveMetadata(txn_)) return false; + // 3. Save Metadata (Handled internally by InvertedIndex per term) + // if (!storage_->sparse_index_->saveMetadata(txn_)) return false; storage_->vector_count_++; return true; @@ -108,29 +113,30 @@ namespace ndd { return storage_->getVectorInternal(txn_, doc_id); } + bool delete_vector(ndd::idInt doc_id) { if(read_only_) { return false; } - // 1. Get vector to remove from index + // Deletion runs in the opposite order: look up the stored vector, remove its + // terms from the inverted index, then delete the raw payload row. auto vec = get_vector(doc_id); if(!vec) { - return false; // Not found + LOG_WARN(2242, storage_->index_id_, "delete_vector could not find doc_id=" << doc_id); + return false; } - // 2. Remove from Index - if(!storage_->bmw_index_->removeDocument(txn_, doc_id, *vec)) { + if(!storage_->sparse_index_->removeDocument(txn_, doc_id, *vec)) { return false; } - // 3. Remove from docs DB if(!storage_->deleteVectorInternal(txn_, doc_id)) { return false; } // 4. Save Metadata (Handled internally) - // if (!storage_->bmw_index_->saveMetadata(txn_)) return false; + // if (!storage_->sparse_index_->saveMetadata(txn_)) return false; storage_->vector_count_--; return true; @@ -148,24 +154,6 @@ namespace ndd { } // Vector management - bool store_vector(ndd::idInt doc_id, const SparseVector& vec) { - std::unique_lock lock(mutex_); - auto txn = begin_transaction(false); - if(!txn->store_vector(doc_id, vec)) { - txn->abort(); - return false; - } - return txn->commit(); - } - - std::optional get_vector(ndd::idInt doc_id) const { - std::shared_lock lock(mutex_); - // Const cast to create read-only transaction - auto* non_const_this = const_cast(this); - auto txn = non_const_this->begin_transaction(true); - return txn->get_vector(doc_id); - } - bool delete_vector(ndd::idInt doc_id) { std::unique_lock lock(mutex_); auto txn = begin_transaction(false); @@ -176,59 +164,30 @@ namespace ndd { return txn->commit(); } - bool update_vector(ndd::idInt doc_id, const SparseVector& vec) { - std::unique_lock lock(mutex_); - auto txn = begin_transaction(false); - - // Get old vector to remove from index - auto old_vec = txn->get_vector(doc_id); - if(old_vec) { - if(!bmw_index_->removeDocument(txn->getTxn(), doc_id, *old_vec)) { - txn->abort(); - return false; - } - } - - // Store new vector (overwrites in docs_dbi) - if(!storeVectorInternal(txn->getTxn(), doc_id, vec)) { - txn->abort(); - return false; - } - - // Add to index - if(!bmw_index_->addDocumentsBatch(txn->getTxn(), {{doc_id, vec}})) { - txn->abort(); - return false; - } - - // Save metadata (Handled internally) - // if (!bmw_index_->saveMetadata(txn->getTxn())) { - // txn->abort(); - // return false; - // } - - return txn->commit(); - } - // Batch operations bool store_vectors_batch(const std::vector>& batch) { std::unique_lock lock(mutex_); auto txn = begin_transaction(false); - for(const auto& [doc_id, vec] : batch) { - if(!storeVectorInternal(txn->getTxn(), doc_id, vec)) { + for(const auto& [doc_id, sparse_vec] : batch) { + if(!storeVectorInternal(txn->getTxn(), doc_id, sparse_vec)) { + LOG_ERROR(2243, index_id_, "store_vectors_batch failed to store doc_id=" << doc_id); txn->abort(); return false; } } - if(!bmw_index_->addDocumentsBatch(txn->getTxn(), batch)) { + if(!sparse_index_->addDocumentsBatch(txn->getTxn(), batch)) { + LOG_ERROR(2244, + index_id_, + "store_vectors_batch failed to update the inverted index for batch size " + << batch.size()); txn->abort(); return false; } // Metadata handled internally - // if (!bmw_index_->saveMetadata(txn->getTxn())) { + // if (!sparse_index_->saveMetadata(txn->getTxn())) { // txn->abort(); // return false; // } @@ -240,6 +199,8 @@ namespace ndd { return false; } + /*NOT BEING USED FOR NOW*/ +#if 0 bool delete_vectors_batch(const std::vector& doc_ids) { std::unique_lock lock(mutex_); auto txn = begin_transaction(false); @@ -251,34 +212,26 @@ namespace ndd { } return txn->commit(); } +#endif //if 0 - // Search (delegates to BMW) - std::vector> search(const SparseVector& query, size_t k, const ndd::RoaringBitmap* filter = nullptr) { - return bmw_index_->search(query, k, filter); + std::vector> search(const SparseVector& query, + size_t k, + const ndd::RoaringBitmap* filter = nullptr) + { + return sparse_index_->search(query, k, filter); } // Statistics size_t get_vector_count() const { return vector_count_; } - size_t get_term_count() const { return bmw_index_ ? bmw_index_->getTermCount() : 0; } - size_t get_block_count() const { return bmw_index_ ? bmw_index_->getBlockCount() : 0; } - - // Maintenance - bool compact() { - // MDBX compaction usually involves copying to a new file - return true; - } - - bool backup(const std::string& backup_path) { - // MDBX backup - return true; - } + size_t get_term_count() const { return sparse_index_ ? sparse_index_->getTermCount() : 0; } private: std::string db_path_; + std::string index_id_; MDBX_env* env_; MDBX_dbi docs_dbi_; - std::unique_ptr bmw_index_; + std::unique_ptr sparse_index_; mutable std::shared_mutex mutex_; std::atomic vector_count_{0}; @@ -288,28 +241,28 @@ namespace ndd { bool initializeMDBX() { int rc = mdbx_env_create(&env_); if(rc != 0) { - LOG_ERROR("mdbx_env_create failed: " << rc); + LOG_ERROR(2245, index_id_, "mdbx_env_create failed: " << mdbx_strerror(rc)); return false; } // Set geometry (max 1TB for now, can be configured) rc = mdbx_env_set_geometry(env_, -1, -1, TB, -1, -1, -1); if(rc != 0) { - LOG_ERROR("mdbx_env_set_geometry failed: " << rc); + LOG_ERROR(2246, index_id_, "mdbx_env_set_geometry failed: " << mdbx_strerror(rc)); return false; } // Set maxdbs to allow named databases rc = mdbx_env_set_maxdbs(env_, 10); if(rc != 0) { - LOG_ERROR("mdbx_env_set_maxdbs failed: " << rc); + LOG_ERROR(2247, index_id_, "mdbx_env_set_maxdbs failed: " << mdbx_strerror(rc)); return false; } std::error_code ec; std::filesystem::create_directories(db_path_, ec); if(ec) { - LOG_ERROR("create_directories failed: " << ec.message()); + LOG_ERROR(2248, index_id_, "create_directories failed for " << db_path_ << ": " << ec.message()); return false; } @@ -318,27 +271,29 @@ namespace ndd { MDBX_NOSTICKYTHREADS | MDBX_NORDAHEAD | MDBX_LIFORECLAIM, 0664); if(rc != 0) { - LOG_ERROR("mdbx_env_open failed: " << rc << " path: " << db_path_); + LOG_ERROR(2249, + index_id_, + "mdbx_env_open failed for " << db_path_ << ": " << mdbx_strerror(rc)); return false; } MDBX_txn* txn; rc = mdbx_txn_begin(env_, nullptr, MDBX_TXN_READWRITE, &txn); if(rc != 0) { - LOG_ERROR("mdbx_txn_begin failed: " << rc); + LOG_ERROR(2250, index_id_, "mdbx_txn_begin failed: " << mdbx_strerror(rc)); return false; } rc = mdbx_dbi_open(txn, "sparse_docs", MDBX_CREATE | MDBX_INTEGERKEY, &docs_dbi_); if(rc != 0) { - LOG_ERROR("mdbx_dbi_open failed: " << rc); + LOG_ERROR(2251, index_id_, "mdbx_dbi_open failed for sparse_docs: " << mdbx_strerror(rc)); mdbx_txn_abort(txn); return false; } rc = mdbx_txn_commit(txn); if(rc != 0) { - LOG_ERROR("mdbx_txn_commit failed: " << rc); + LOG_ERROR(2252, index_id_, "mdbx_txn_commit failed: " << mdbx_strerror(rc)); return false; } return true; @@ -359,7 +314,14 @@ namespace ndd { data.iov_base = packed.data(); data.iov_len = packed.size(); - return mdbx_put(txn, docs_dbi_, &key, &data, MDBX_UPSERT) == 0; + int rc = mdbx_put(txn, docs_dbi_, &key, &data, MDBX_UPSERT); + if (rc != 0) { + LOG_ERROR(2253, + index_id_, + "storeVectorInternal MDBX put failed for doc_id=" + << doc_id << ": " << mdbx_strerror(rc)); + } + return rc == 0; } std::optional getVectorInternal(MDBX_txn* txn, ndd::idInt doc_id) const { @@ -378,7 +340,14 @@ namespace ndd { MDBX_val key; key.iov_base = &doc_id; key.iov_len = sizeof(ndd::idInt); - return mdbx_del(txn, docs_dbi_, &key, nullptr) == 0; + int rc = mdbx_del(txn, docs_dbi_, &key, nullptr); + if (rc != 0 && rc != MDBX_NOTFOUND) { + LOG_ERROR(2254, + index_id_, + "deleteVectorInternal MDBX delete failed for doc_id=" + << doc_id << ": " << mdbx_strerror(rc)); + } + return rc == 0; } void updateVectorCount() { @@ -393,20 +362,4 @@ namespace ndd { } }; - // MDBX Transaction RAII wrapper - class MDBXTransaction { - public: - MDBXTransaction(MDBX_env* env, bool read_only = false); - ~MDBXTransaction(); - - bool commit(); - void abort(); - - MDBX_txn* txn = nullptr; - - private: - bool committed_ = false; - bool read_only_; - }; - -} // namespace ndd \ No newline at end of file +} // namespace ndd diff --git a/src/sparse/sparse_vector.hpp b/src/sparse/sparse_vector.hpp index ac56d8f2ac..e93019b1dd 100644 --- a/src/sparse/sparse_vector.hpp +++ b/src/sparse/sparse_vector.hpp @@ -4,9 +4,12 @@ #include #include #include "mdbx/mdbx.h" +#include "../utils/log.hpp" namespace ndd { + // Sparse vector payload stored in the document table. The convention in this module is that + // indices are sorted term ids and values[i] belongs to indices[i]. struct SparseVector { std::vector indices; // term IDs (sorted) std::vector values; // corresponding values @@ -17,38 +20,41 @@ namespace ndd { // Constructor from packed data SparseVector(const uint8_t* data, size_t data_size) { if(data_size < sizeof(uint16_t)) { - throw std::runtime_error("Invalid packed data: insufficient size for nnz field"); + throw std::runtime_error("Invalid packed data: insufficient size for nr_nonzero field"); } const uint8_t* ptr = data; - // Read number of non-zero elements (uint16_t) - uint16_t nnz; - std::memcpy(&nnz, ptr, sizeof(uint16_t)); + // Packed format: + // [nr_nonzero:u16][term_ids:n * u32][values:n * f16] + uint16_t nr_nonzero; + std::memcpy(&nr_nonzero, ptr, sizeof(uint16_t)); ptr += sizeof(uint16_t); - // Validate remaining data size: nnz * (4 + 2) bytes - size_t expected_size = sizeof(uint16_t) + (nnz * (sizeof(uint32_t) + sizeof(uint16_t))); + // Validate remaining data size: nr_nonzero * (4 + 2) bytes + size_t expected_size = sizeof(uint16_t) + (nr_nonzero * (sizeof(uint32_t) + sizeof(uint16_t))); if(data_size != expected_size) { throw std::runtime_error("Invalid packed data: size mismatch"); } - if(nnz > 0) { - indices.resize(nnz); - values.resize(nnz); + if(nr_nonzero > 0) { + indices.resize(nr_nonzero); + values.resize(nr_nonzero); // Read term IDs (uint32_t each) - std::memcpy(indices.data(), ptr, nnz * sizeof(uint32_t)); - ptr += nnz * sizeof(uint32_t); + std::memcpy(indices.data(), ptr, nr_nonzero * sizeof(uint32_t)); + ptr += nr_nonzero * sizeof(uint32_t); // Read quantized values (uint16_t each) and convert to float - std::vector fp16_values(nnz); - std::memcpy(fp16_values.data(), ptr, nnz * sizeof(uint16_t)); + std::vector fp16_values(nr_nonzero); + std::memcpy(fp16_values.data(), ptr, nr_nonzero * sizeof(uint16_t)); // Convert FP16 to float - for(size_t i = 0; i < nnz; ++i) { + for(size_t i = 0; i < nr_nonzero; ++i) { values[i] = fp16_to_float(fp16_values[i]); } + } else { + LOG_WARN(2261, "Deserialized sparse vector with nr_nonzero=0"); } } @@ -59,42 +65,43 @@ namespace ndd { explicit SparseVector(const std::vector& packed_data) : SparseVector(packed_data.data(), packed_data.size()) {} - // Pack sparse vector into binary format: nnz(u16) + [term_ids(u32)] + [values(f16)] + // Pack sparse vector into binary format: nr_nonzero(u16) + [term_ids(u32)] + [values(f16)] std::vector pack() const { if(indices.size() != values.size()) { throw std::runtime_error("SparseVector indices and values size mismatch"); } - uint16_t nnz = static_cast(indices.size()); + uint16_t nr_nonzero = static_cast(indices.size()); - // Calculate total size: nnz(2) + term_ids(4*nnz) + values(2*nnz) + // Calculate total size: nr_nonzero(2) + term_ids(4*nr_nonzero) + values(2*nr_nonzero) size_t total_size = - sizeof(uint16_t) + (nnz * sizeof(uint32_t)) + (nnz * sizeof(uint16_t)); + sizeof(uint16_t) + (nr_nonzero * sizeof(uint32_t)) + (nr_nonzero * sizeof(uint16_t)); - // Single allocation + // Serialize contiguously so the vector can be written to MDBX as one value blob. std::vector packed(total_size); uint8_t* ptr = packed.data(); - // Write nnz - std::memcpy(ptr, &nnz, sizeof(uint16_t)); + // Write nr_nonzero + std::memcpy(ptr, &nr_nonzero, sizeof(uint16_t)); ptr += sizeof(uint16_t); - if(nnz > 0) { + if(nr_nonzero > 0) { // Write term IDs - std::memcpy(ptr, indices.data(), nnz * sizeof(uint32_t)); - ptr += nnz * sizeof(uint32_t); + std::memcpy(ptr, indices.data(), nr_nonzero * sizeof(uint32_t)); + ptr += nr_nonzero * sizeof(uint32_t); // Convert float values to FP16 and write - std::vector fp16_values(nnz); - for(size_t i = 0; i < nnz; ++i) { + std::vector fp16_values(nr_nonzero); + for(size_t i = 0; i < nr_nonzero; ++i) { fp16_values[i] = float_to_fp16(values[i]); } - std::memcpy(ptr, fp16_values.data(), nnz * sizeof(uint16_t)); + std::memcpy(ptr, fp16_values.data(), nr_nonzero * sizeof(uint16_t)); } return packed; } +#if 0 // Dot product overloads float dot(const SparseVector& other) const { float result = 0.0f; @@ -121,25 +128,25 @@ namespace ndd { const uint8_t* ptr = packed_data; - // Read nnz - uint16_t other_nnz; - std::memcpy(&other_nnz, ptr, sizeof(uint16_t)); + // Read nr_nonzero + uint16_t other_nr_nonzero; + std::memcpy(&other_nr_nonzero, ptr, sizeof(uint16_t)); ptr += sizeof(uint16_t); - if(other_nnz == 0) { + if(other_nr_nonzero == 0) { return 0.0f; } // Direct pointer access to packed data const uint32_t* other_indices = reinterpret_cast(ptr); const uint16_t* other_fp16_values = - reinterpret_cast(ptr + other_nnz * sizeof(uint32_t)); + reinterpret_cast(ptr + other_nr_nonzero * sizeof(uint32_t)); // Two-pointer intersection float result = 0.0f; size_t this_idx = 0; - for(uint16_t other_idx = 0; other_idx < other_nnz && this_idx < indices.size();) { + for(uint16_t other_idx = 0; other_idx < other_nr_nonzero && this_idx < indices.size();) { uint32_t this_index = indices[this_idx]; uint32_t other_index = other_indices[other_idx]; @@ -166,6 +173,7 @@ namespace ndd { float dot(const std::vector& packed_data) const { return dot(packed_data.data(), packed_data.size()); } +#endif //if 0 // Utility methods bool empty() const { return indices.empty(); } diff --git a/src/utils/settings.hpp b/src/utils/settings.hpp index cce94bdd8c..aa6d17d39f 100644 --- a/src/utils/settings.hpp +++ b/src/utils/settings.hpp @@ -18,6 +18,7 @@ namespace settings { inline const std::string NAME = "Endee"; inline const std::string VERSION = "1.0.0-beta"; inline uint16_t INDEX_VERSION = 1; + inline uint16_t SPARSE_ONDISK_VERSION = 1; inline const std::string DEFAULT_SPACE_TYPE = "cosine"; constexpr size_t DEFAULT_STORAGE_BITS = 16; // 16 bits = 2 bytes per element. Only for dense vectors @@ -38,7 +39,7 @@ namespace settings { constexpr size_t RECOVERY_BATCH_SIZE = 20'000; constexpr size_t SAVE_EVERY_N_MINUTES = 30; // Number of threads for http server - 0 means it will default to hardware concurrency - constexpr size_t NUM_SERVER_THREADS = 0; + constexpr size_t DEFAULT_NUM_SERVER_THREADS = 0; // Number of save mutexes for parallel saves constexpr size_t NUM_INDEX_SAVE_MUTEXES = 16; @@ -58,13 +59,15 @@ namespace settings { constexpr size_t MAX_LINK_LIST_LOCKS = 65536; - // Sparse Storage settings - constexpr uint16_t MAX_BLOCK_SIZE = 128; // Number of elements in a block - constexpr uint32_t DEFAULT_VOCAB_SIZE = 0; // 0 means dense vectors only - constexpr uint8_t DEFAULT_QUANT_BITS = 8; - constexpr size_t MAX_BMW_BLOCK_SIZE = 128; + // Sparse Index settings + /*XXX: Should we make this a runtime configurable value ?*/ + constexpr size_t DEFAULT_INV_IDX_SEARCH_BATCH_SZ = 10'000; constexpr float NEAR_ZERO = 1e-9f; + // Compact a posting list when the fraction of tombstoned entries + // reaches this threshold (0.0 = compact every delete, 1.0 = never). + constexpr float INV_IDX_COMPACTION_TOMBSTONE_RATIO = 0.1f; + // Maximum number of elements in the index constexpr size_t MAX_VECTORS_ADMIN = 1'000'000'000; @@ -107,6 +110,27 @@ namespace settings { const char* env = std::getenv("NDD_SERVER_ID"); return env ? std::string(env) : DEFAULT_SERVER_ID; }(); + + inline static size_t NUM_SERVER_THREADS = [] { + const char* env = std::getenv("NDD_NUM_SERVER_THREADS"); + if (env) { + return (size_t)std::stoull(env); + } + + // If no env var, check if default is 0 (auto-detect) + if (DEFAULT_NUM_SERVER_THREADS == 0) { + unsigned int hw = std::thread::hardware_concurrency() * 2; + return hw > 0 ? (size_t)hw : 1; // Fallback to 1 if hardware_concurrency returns 0 + } + + return (size_t)DEFAULT_NUM_SERVER_THREADS; + }(); + + inline static size_t INV_IDX_SEARCH_BATCH_SZ = [] { + const char* env = std::getenv("NDD_INV_IDX_SEARCH_BATCH_SZ"); + return env ? std::stoull(env) : DEFAULT_INV_IDX_SEARCH_BATCH_SZ; + }(); + inline static size_t SERVER_PORT = [] { const char* env = std::getenv("NDD_SERVER_PORT"); return env ? std::stoull(env) : DEFAULT_SERVER_PORT; diff --git a/third_party/mdbx/mdbx.c b/third_party/mdbx/mdbx.c index b284ab3ce5..cc97c35f22 100644 --- a/third_party/mdbx/mdbx.c +++ b/third_party/mdbx/mdbx.c @@ -1544,6 +1544,107 @@ MDBX_MAYBE_UNUSED static inline uint32_t osal_monotime_to_16dot16_noUnderflow(ui return seconds_16dot16 ? seconds_16dot16 : /* fix underflow */ (monotime > 0); } +#if defined(ND_MDBX_INSTRUMENT) +enum mdbx_debug_stat_cmd { + mdbx_debug_stat_env_create, + mdbx_debug_stat_env_open, + mdbx_debug_stat_env_close, + mdbx_debug_stat_txn_begin, + mdbx_debug_stat_txn_commit, + mdbx_debug_stat_txn_abort, + mdbx_debug_stat_dbi_open, + mdbx_debug_stat_get, + mdbx_debug_stat_put, + mdbx_debug_stat_del, + mdbx_debug_stat_cursor_open, + mdbx_debug_stat_cursor_close, + mdbx_debug_stat_cursor_get, + mdbx_debug_stat_cursor_put, + mdbx_debug_stat_cursor_del, + mdbx_debug_stat_count +}; + +typedef struct mdbx_debug_stat { + uint64_t calls; + uint64_t total_monotime; +} mdbx_debug_stat_t; + +static struct { + osal_fastmutex_t lock; + bool ready; + mdbx_debug_stat_t entries[mdbx_debug_stat_count]; +} mdbx_debug_stats; + +static const char *const mdbx_debug_stat_names[mdbx_debug_stat_count] = { + "mdbx_env_create", "mdbx_env_open", "mdbx_env_close", "mdbx_txn_begin", "mdbx_txn_commit", + "mdbx_txn_abort", "mdbx_dbi_open", "mdbx_get", "mdbx_put", "mdbx_del", + "mdbx_cursor_open", "mdbx_cursor_close", "mdbx_cursor_get", "mdbx_cursor_put", "mdbx_cursor_del", +}; + +static double mdbx_debug_monotime_to_ms(uint64_t monotime) { + return ((double)osal_monotime_to_16dot16(monotime) * 1000.0) / 65536.0; +} + +static void mdbx_debug_stats_record(enum mdbx_debug_stat_cmd cmd, uint64_t elapsed_monotime) { + if (unlikely(!mdbx_debug_stats.ready)) + return; + + int err = osal_fastmutex_acquire(&mdbx_debug_stats.lock); + if (unlikely(err != MDBX_SUCCESS)) + return; + mdbx_debug_stats.entries[cmd].calls += 1; + mdbx_debug_stats.entries[cmd].total_monotime += elapsed_monotime; + err = osal_fastmutex_release(&mdbx_debug_stats.lock); + (void)err; +} + +void print_mdbx_stats(void) { + mdbx_debug_stat_t snapshot[mdbx_debug_stat_count]; + + if (unlikely(!mdbx_debug_stats.ready)) + return; + + int err = osal_fastmutex_acquire(&mdbx_debug_stats.lock); + if (unlikely(err != MDBX_SUCCESS)) + return; + memcpy(snapshot, mdbx_debug_stats.entries, sizeof(snapshot)); + memset(mdbx_debug_stats.entries, 0, sizeof(mdbx_debug_stats.entries)); + err = osal_fastmutex_release(&mdbx_debug_stats.lock); + if (unlikely(err != MDBX_SUCCESS)) + return; + + bool printed = false; + for (size_t i = 0; i < mdbx_debug_stat_count; ++i) { + if (snapshot[i].calls == 0) + continue; + + printed = true; + const double total_ms = mdbx_debug_monotime_to_ms(snapshot[i].total_monotime); + const double avg_us = (total_ms * 1000.0) / (double)snapshot[i].calls; + fprintf(stderr, "[MDBX_STATS] %s count=%" PRIu64 " total_ms=%.3f avg_us=%.3f\n", mdbx_debug_stat_names[i], + snapshot[i].calls, total_ms, avg_us); + } + + if (!printed) + fprintf(stderr, "[MDBX_STATS] no recorded commands\n"); + + fflush(stderr); +} + +#define MDBX_DEBUG_STATS_SCOPE(cmd) const uint64_t mdbx_debug_stats_started_##cmd = osal_monotime() +#define MDBX_DEBUG_STATS_RETURN(cmd, value) \ + do { \ + const int mdbx_debug_stats_rc_##cmd = (value); \ + mdbx_debug_stats_record(mdbx_debug_stat_##cmd, osal_monotime() - mdbx_debug_stats_started_##cmd); \ + return mdbx_debug_stats_rc_##cmd; \ + } while (0) +#else +void print_mdbx_stats(void) {} + +#define MDBX_DEBUG_STATS_SCOPE(cmd) ((void)0) +#define MDBX_DEBUG_STATS_RETURN(cmd, value) return (value) +#endif + /*----------------------------------------------------------------------------*/ MDBX_INTERNAL void osal_ctor(void); @@ -8384,22 +8485,24 @@ int mdbx_cursor_unbind(MDBX_cursor *mc) { } int mdbx_cursor_open(MDBX_txn *txn, MDBX_dbi dbi, MDBX_cursor **ret) { + MDBX_DEBUG_STATS_SCOPE(cursor_open); + if (unlikely(!ret)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(cursor_open, LOG_IFERR(MDBX_EINVAL)); *ret = nullptr; MDBX_cursor *const mc = mdbx_cursor_create(nullptr); if (unlikely(!mc)) - return LOG_IFERR(MDBX_ENOMEM); + MDBX_DEBUG_STATS_RETURN(cursor_open, LOG_IFERR(MDBX_ENOMEM)); int rc = mdbx_cursor_bind(txn, mc, dbi); if (unlikely(rc != MDBX_SUCCESS)) { mdbx_cursor_close(mc); - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(cursor_open, LOG_IFERR(rc)); } *ret = mc; - return MDBX_SUCCESS; + MDBX_DEBUG_STATS_RETURN(cursor_open, MDBX_SUCCESS); } void mdbx_cursor_close(MDBX_cursor *cursor) { @@ -8411,31 +8514,33 @@ void mdbx_cursor_close(MDBX_cursor *cursor) { } int mdbx_cursor_close2(MDBX_cursor *mc) { + MDBX_DEBUG_STATS_SCOPE(cursor_close); + if (unlikely(!mc)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(cursor_close, LOG_IFERR(MDBX_EINVAL)); if (mc->signature == cur_signature_ready4dispose) { if (unlikely(mc->txn || mc->backup)) - return LOG_IFERR(MDBX_PANIC); + MDBX_DEBUG_STATS_RETURN(cursor_close, LOG_IFERR(MDBX_PANIC)); cursor_drown((cursor_couple_t *)mc); mc->signature = 0; osal_free(mc); - return MDBX_SUCCESS; + MDBX_DEBUG_STATS_RETURN(cursor_close, MDBX_SUCCESS); } if (unlikely(mc->signature != cur_signature_live)) - return LOG_IFERR(MDBX_EBADSIGN); + MDBX_DEBUG_STATS_RETURN(cursor_close, LOG_IFERR(MDBX_EBADSIGN)); MDBX_txn *const txn = mc->txn; int rc = check_txn(txn, MDBX_TXN_FINISHED); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(cursor_close, LOG_IFERR(rc)); if (mc->backup) { /* Cursor closed before nested txn ends */ cursor_reset((cursor_couple_t *)mc); mc->signature = cur_signature_wait4eot; - return MDBX_SUCCESS; + MDBX_DEBUG_STATS_RETURN(cursor_close, MDBX_SUCCESS); } if (mc->next != mc) { @@ -8456,7 +8561,7 @@ int mdbx_cursor_close2(MDBX_cursor *mc) { cursor_drown((cursor_couple_t *)mc); mc->signature = 0; osal_free(mc); - return MDBX_SUCCESS; + MDBX_DEBUG_STATS_RETURN(cursor_close, MDBX_SUCCESS); } int mdbx_cursor_copy(const MDBX_cursor *src, MDBX_cursor *dest) { @@ -8729,11 +8834,13 @@ int mdbx_cursor_eof(const MDBX_cursor *mc) { } int mdbx_cursor_get(MDBX_cursor *mc, MDBX_val *key, MDBX_val *data, MDBX_cursor_op op) { + MDBX_DEBUG_STATS_SCOPE(cursor_get); + int rc = cursor_check_ro(mc); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(cursor_get, LOG_IFERR(rc)); - return LOG_IFERR(cursor_ops(mc, key, data, op)); + MDBX_DEBUG_STATS_RETURN(cursor_get, LOG_IFERR(cursor_ops(mc, key, data, op))); } __hot static int scan_confinue(MDBX_cursor *mc, MDBX_predicate_func *predicate, void *context, void *arg, MDBX_val *key, @@ -8970,34 +9077,38 @@ MDBX_dbi mdbx_cursor_dbi(const MDBX_cursor *mc) { /*----------------------------------------------------------------------------*/ int mdbx_cursor_put(MDBX_cursor *mc, const MDBX_val *key, MDBX_val *data, MDBX_put_flags_t flags) { + MDBX_DEBUG_STATS_SCOPE(cursor_put); + if (unlikely(key == nullptr || data == nullptr)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(cursor_put, LOG_IFERR(MDBX_EINVAL)); int rc = cursor_check_rw(mc); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(cursor_put, LOG_IFERR(rc)); if (unlikely(flags & MDBX_MULTIPLE)) { rc = cursor_check_multiple(mc, key, data, flags); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(cursor_put, LOG_IFERR(rc)); } if (flags & MDBX_RESERVE) { if (unlikely(mc->tree->flags & (MDBX_DUPSORT | MDBX_REVERSEDUP | MDBX_INTEGERDUP | MDBX_DUPFIXED))) - return LOG_IFERR(MDBX_INCOMPATIBLE); + MDBX_DEBUG_STATS_RETURN(cursor_put, LOG_IFERR(MDBX_INCOMPATIBLE)); data->iov_base = nullptr; } - return LOG_IFERR(cursor_put_checklen(mc, key, data, flags)); + MDBX_DEBUG_STATS_RETURN(cursor_put, LOG_IFERR(cursor_put_checklen(mc, key, data, flags))); } int mdbx_cursor_del(MDBX_cursor *mc, MDBX_put_flags_t flags) { + MDBX_DEBUG_STATS_SCOPE(cursor_del); + int rc = cursor_check_rw(mc); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(cursor_del, LOG_IFERR(rc)); - return LOG_IFERR(cursor_del(mc, flags)); + MDBX_DEBUG_STATS_RETURN(cursor_del, LOG_IFERR(cursor_del(mc, flags))); } __cold int mdbx_cursor_ignord(MDBX_cursor *mc) { @@ -9037,7 +9148,8 @@ static int dbi_open_cstr(MDBX_txn *txn, const char *name_cstr, MDBX_db_flags_t f } int mdbx_dbi_open(MDBX_txn *txn, const char *name, MDBX_db_flags_t flags, MDBX_dbi *dbi) { - return LOG_IFERR(dbi_open_cstr(txn, name, flags, dbi, nullptr, nullptr)); + MDBX_DEBUG_STATS_SCOPE(dbi_open); + MDBX_DEBUG_STATS_RETURN(dbi_open, LOG_IFERR(dbi_open_cstr(txn, name, flags, dbi, nullptr, nullptr))); } int mdbx_dbi_open_ex(MDBX_txn *txn, const char *name, MDBX_db_flags_t flags, MDBX_dbi *dbi, MDBX_cmp_func *keycmp, @@ -9509,26 +9621,28 @@ __cold static int env_handle_pathname(MDBX_env *env, const pathchar_t *pathname, /*----------------------------------------------------------------------------*/ __cold int mdbx_env_create(MDBX_env **penv) { + MDBX_DEBUG_STATS_SCOPE(env_create); + if (unlikely(!penv)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(env_create, LOG_IFERR(MDBX_EINVAL)); *penv = nullptr; #ifdef MDBX_HAVE_C11ATOMICS if (unlikely(!atomic_is_lock_free((const volatile uint32_t *)penv))) { ERROR("lock-free atomic ops for %u-bit types is required", 32); - return LOG_IFERR(MDBX_INCOMPATIBLE); + MDBX_DEBUG_STATS_RETURN(env_create, LOG_IFERR(MDBX_INCOMPATIBLE)); } #if MDBX_64BIT_ATOMIC if (unlikely(!atomic_is_lock_free((const volatile uint64_t *)penv))) { ERROR("lock-free atomic ops for %u-bit types is required", 64); - return LOG_IFERR(MDBX_INCOMPATIBLE); + MDBX_DEBUG_STATS_RETURN(env_create, LOG_IFERR(MDBX_INCOMPATIBLE)); } #endif /* MDBX_64BIT_ATOMIC */ #endif /* MDBX_HAVE_C11ATOMICS */ if (unlikely(!is_powerof2(globals.sys_pagesize) || globals.sys_pagesize < MDBX_MIN_PAGESIZE)) { ERROR("unsuitable system pagesize %u", globals.sys_pagesize); - return LOG_IFERR(MDBX_INCOMPATIBLE); + MDBX_DEBUG_STATS_RETURN(env_create, LOG_IFERR(MDBX_INCOMPATIBLE)); } #if defined(__linux__) || defined(__gnu_linux__) @@ -9547,13 +9661,13 @@ __cold int mdbx_env_create(MDBX_env **penv) { ERROR("too old linux kernel %u.%u.%u.%u, the >= 3.16 is required", globals.linux_kernel_version >> 24, (globals.linux_kernel_version >> 16) & 255, (globals.linux_kernel_version >> 8) & 255, globals.linux_kernel_version & 255); - return LOG_IFERR(MDBX_INCOMPATIBLE); + MDBX_DEBUG_STATS_RETURN(env_create, LOG_IFERR(MDBX_INCOMPATIBLE)); } #endif /* Linux */ MDBX_env *env = osal_calloc(1, sizeof(MDBX_env)); if (unlikely(!env)) - return LOG_IFERR(MDBX_ENOMEM); + MDBX_DEBUG_STATS_RETURN(env_create, LOG_IFERR(MDBX_ENOMEM)); env->max_readers = DEFAULT_READERS; env->max_dbi = env->n_dbi = CORE_DBS; @@ -9591,11 +9705,11 @@ __cold int mdbx_env_create(MDBX_env **penv) { VALGRIND_CREATE_MEMPOOL(env, 0, 0); env->signature.weak = env_signature; *penv = env; - return MDBX_SUCCESS; + MDBX_DEBUG_STATS_RETURN(env_create, MDBX_SUCCESS); bailout: osal_free(env); - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(env_create, LOG_IFERR(rc)); } __cold int mdbx_env_turn_for_recovery(MDBX_env *env, unsigned target) { @@ -9754,6 +9868,8 @@ __cold int mdbx_env_deleteW(const wchar_t *pathname, MDBX_env_delete_mode_t mode } __cold int mdbx_env_open(MDBX_env *env, const char *pathname, MDBX_env_flags_t flags, mdbx_mode_t mode) { + MDBX_DEBUG_STATS_SCOPE(env_open); + #if defined(_WIN32) || defined(_WIN64) wchar_t *pathnameW = nullptr; int rc = osal_mb2w(pathname, &pathnameW); @@ -9764,7 +9880,7 @@ __cold int mdbx_env_open(MDBX_env *env, const char *pathname, MDBX_env_flags_t f /* force to make cache of the multi-byte pathname representation */ mdbx_env_get_path(env, &pathname); } - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(env_open, LOG_IFERR(rc)); } __cold int mdbx_env_openW(MDBX_env *env, const wchar_t *pathname, MDBX_env_flags_t flags, mdbx_mode_t mode) { @@ -9889,7 +10005,7 @@ __cold int mdbx_env_openW(MDBX_env *env, const wchar_t *pathname, MDBX_env_flags env->flags = saved_me_flags | ENV_FATAL_ERROR; } } - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(env_open, LOG_IFERR(rc)); } /*----------------------------------------------------------------------------*/ @@ -9932,14 +10048,16 @@ __cold int mdbx_env_resurrect_after_fork(MDBX_env *env) { #endif /* Windows */ __cold int mdbx_env_close_ex(MDBX_env *env, bool dont_sync) { + MDBX_DEBUG_STATS_SCOPE(env_close); + page_t *dp; int rc = MDBX_SUCCESS; if (unlikely(!env)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(env_close, LOG_IFERR(MDBX_EINVAL)); if (unlikely(env->signature.weak != env_signature)) - return LOG_IFERR(MDBX_EBADSIGN); + MDBX_DEBUG_STATS_RETURN(env_close, LOG_IFERR(MDBX_EBADSIGN)); #if MDBX_ENV_CHECKPID || !(defined(_WIN32) || defined(_WIN64)) /* Check the PID even if MDBX_ENV_CHECKPID=0 on non-Windows @@ -9952,12 +10070,12 @@ __cold int mdbx_env_close_ex(MDBX_env *env, bool dont_sync) { if (env->dxb_mmap.base && (env->flags & (MDBX_RDONLY | ENV_FATAL_ERROR)) == 0 && env->basal_txn) { if (env->basal_txn->owner && env->basal_txn->owner != osal_thread_self()) - return LOG_IFERR(MDBX_BUSY); + MDBX_DEBUG_STATS_RETURN(env_close, LOG_IFERR(MDBX_BUSY)); } else dont_sync = true; if (!atomic_cas32(&env->signature, env_signature, 0)) - return LOG_IFERR(MDBX_EBADSIGN); + MDBX_DEBUG_STATS_RETURN(env_close, LOG_IFERR(MDBX_EBADSIGN)); if (!dont_sync) { #if defined(_WIN32) || defined(_WIN64) @@ -10010,7 +10128,7 @@ __cold int mdbx_env_close_ex(MDBX_env *env, bool dont_sync) { VALGRIND_DESTROY_MEMPOOL(env); osal_free(env); - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(env_close, LOG_IFERR(rc)); } /*----------------------------------------------------------------------------*/ @@ -12407,22 +12525,23 @@ int mdbx_canary_get(const MDBX_txn *txn, MDBX_canary *canary) { } int mdbx_get(const MDBX_txn *txn, MDBX_dbi dbi, const MDBX_val *key, MDBX_val *data) { + MDBX_DEBUG_STATS_SCOPE(get); DKBUF_DEBUG; DEBUG("===> get db %u key [%s]", dbi, DKEY_DEBUG(key)); if (unlikely(!key || !data)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(get, LOG_IFERR(MDBX_EINVAL)); int rc = check_txn(txn, MDBX_TXN_BLOCKED); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(get, LOG_IFERR(rc)); cursor_couple_t cx; rc = cursor_init(&cx.outer, txn, dbi); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(get, LOG_IFERR(rc)); - return LOG_IFERR(cursor_seek(&cx.outer, (MDBX_val *)key, data, MDBX_SET).err); + MDBX_DEBUG_STATS_RETURN(get, LOG_IFERR(cursor_seek(&cx.outer, (MDBX_val *)key, data, MDBX_SET).err)); } int mdbx_get_equal_or_great(const MDBX_txn *txn, MDBX_dbi dbi, MDBX_val *key, MDBX_val *data) { @@ -12554,20 +12673,22 @@ int mdbx_is_dirty(const MDBX_txn *txn, const void *ptr) { } int mdbx_del(MDBX_txn *txn, MDBX_dbi dbi, const MDBX_val *key, const MDBX_val *data) { + MDBX_DEBUG_STATS_SCOPE(del); + if (unlikely(!key)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(del, LOG_IFERR(MDBX_EINVAL)); if (unlikely(dbi <= FREE_DBI)) - return LOG_IFERR(MDBX_BAD_DBI); + MDBX_DEBUG_STATS_RETURN(del, LOG_IFERR(MDBX_BAD_DBI)); int rc = check_txn_rw(txn, MDBX_TXN_BLOCKED); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(del, LOG_IFERR(rc)); cursor_couple_t cx; rc = cursor_init(&cx.outer, txn, dbi); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(del, LOG_IFERR(rc)); MDBX_val proxy; MDBX_cursor_op op = MDBX_SET; @@ -12580,44 +12701,46 @@ int mdbx_del(MDBX_txn *txn, MDBX_dbi dbi, const MDBX_val *key, const MDBX_val *d } rc = cursor_seek(&cx.outer, (MDBX_val *)key, (MDBX_val *)data, op).err; if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(del, LOG_IFERR(rc)); cx.outer.next = txn->cursors[dbi]; txn->cursors[dbi] = &cx.outer; rc = cursor_del(&cx.outer, flags); txn->cursors[dbi] = cx.outer.next; - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(del, LOG_IFERR(rc)); } int mdbx_put(MDBX_txn *txn, MDBX_dbi dbi, const MDBX_val *key, MDBX_val *data, MDBX_put_flags_t flags) { + MDBX_DEBUG_STATS_SCOPE(put); + if (unlikely(!key || !data)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(put, LOG_IFERR(MDBX_EINVAL)); if (unlikely(dbi <= FREE_DBI)) - return LOG_IFERR(MDBX_BAD_DBI); + MDBX_DEBUG_STATS_RETURN(put, LOG_IFERR(MDBX_BAD_DBI)); if (unlikely(flags & ~(MDBX_NOOVERWRITE | MDBX_NODUPDATA | MDBX_ALLDUPS | MDBX_ALLDUPS | MDBX_RESERVE | MDBX_APPEND | MDBX_APPENDDUP | MDBX_CURRENT | MDBX_MULTIPLE))) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(put, LOG_IFERR(MDBX_EINVAL)); int rc = check_txn_rw(txn, MDBX_TXN_BLOCKED); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(put, LOG_IFERR(rc)); cursor_couple_t cx; rc = cursor_init(&cx.outer, txn, dbi); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(put, LOG_IFERR(rc)); if (unlikely(flags & MDBX_MULTIPLE)) { rc = cursor_check_multiple(&cx.outer, key, data, flags); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(put, LOG_IFERR(rc)); } if (flags & MDBX_RESERVE) { if (unlikely(cx.outer.tree->flags & (MDBX_DUPSORT | MDBX_REVERSEDUP | MDBX_INTEGERDUP | MDBX_DUPFIXED))) - return LOG_IFERR(MDBX_INCOMPATIBLE); + MDBX_DEBUG_STATS_RETURN(put, LOG_IFERR(MDBX_INCOMPATIBLE)); data->iov_base = nullptr; } @@ -12645,7 +12768,7 @@ int mdbx_put(MDBX_txn *txn, MDBX_dbi dbi, const MDBX_val *key, MDBX_val *data, M rc = cursor_put_checklen(&cx.outer, key, data, flags); txn->cursors[dbi] = cx.outer.next; - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(put, LOG_IFERR(rc)); } //------------------------------------------------------------------------------ @@ -12886,23 +13009,25 @@ int mdbx_txn_break(MDBX_txn *txn) { } int mdbx_txn_abort(MDBX_txn *txn) { + MDBX_DEBUG_STATS_SCOPE(txn_abort); + int rc = check_txn(txn, 0); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_abort, LOG_IFERR(rc)); rc = check_env(txn->env, true); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_abort, LOG_IFERR(rc)); #if MDBX_TXN_CHECKOWNER if ((txn->flags & (MDBX_TXN_RDONLY | MDBX_NOSTICKYTHREADS)) == MDBX_NOSTICKYTHREADS && unlikely(txn->owner != osal_thread_self())) { mdbx_txn_break(txn); - return LOG_IFERR(MDBX_THREAD_MISMATCH); + MDBX_DEBUG_STATS_RETURN(txn_abort, LOG_IFERR(MDBX_THREAD_MISMATCH)); } #endif /* MDBX_TXN_CHECKOWNER */ - return LOG_IFERR(txn_abort(txn)); + MDBX_DEBUG_STATS_RETURN(txn_abort, LOG_IFERR(txn_abort(txn))); } int mdbx_txn_park(MDBX_txn *txn, bool autounpark) { @@ -12976,26 +13101,28 @@ int mdbx_txn_set_userctx(MDBX_txn *txn, void *ctx) { void *mdbx_txn_get_userctx(const MDBX_txn *txn) { return check_txn(txn, MDBX_TXN_FINISHED) ? nullptr : txn->userctx; } int mdbx_txn_begin_ex(MDBX_env *env, MDBX_txn *parent, MDBX_txn_flags_t flags, MDBX_txn **ret, void *context) { + MDBX_DEBUG_STATS_SCOPE(txn_begin); + if (unlikely(!ret)) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(MDBX_EINVAL)); *ret = nullptr; if (unlikely((flags & ~txn_rw_begin_flags) && (parent || (flags & ~txn_ro_begin_flags)))) - return LOG_IFERR(MDBX_EINVAL); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(MDBX_EINVAL)); int rc = check_env(env, true); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(rc)); if (unlikely(env->flags & MDBX_RDONLY & ~flags)) /* write txn in RDONLY env */ - return LOG_IFERR(MDBX_EACCESS); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(MDBX_EACCESS)); MDBX_txn *txn = nullptr; if (parent) { /* Nested transactions: Max 1 child, write txns only, no writemap */ rc = check_txn(parent, MDBX_TXN_BLOCKED - MDBX_TXN_PARKED); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(rc)); if (unlikely(parent->flags & (MDBX_TXN_RDONLY | MDBX_WRITEMAP))) { rc = MDBX_BAD_TXN; @@ -13003,14 +13130,14 @@ int mdbx_txn_begin_ex(MDBX_env *env, MDBX_txn *parent, MDBX_txn_flags_t flags, M ERROR("%s mode is incompatible with nested transactions", "MDBX_WRITEMAP"); rc = MDBX_INCOMPATIBLE; } - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(rc)); } if (env->options.spill_parent4child_denominator) { /* Spill dirty-pages of parent to provide dirtyroom for child txn */ rc = txn_spill(parent, nullptr, parent->tw.dirtylist->length / env->options.spill_parent4child_denominator); if (unlikely(rc != MDBX_SUCCESS)) - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(rc)); } tASSERT(parent, audit_ex(parent, 0, false) == 0); @@ -13036,7 +13163,7 @@ int mdbx_txn_begin_ex(MDBX_env *env, MDBX_txn *parent, MDBX_txn_flags_t flags, M env->max_dbi * (sizeof(txn->dbs[0]) + sizeof(txn->cursors[0]) + sizeof(txn->dbi_state[0])); txn = osal_malloc(size); if (unlikely(txn == nullptr)) - return LOG_IFERR(MDBX_ENOMEM); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(MDBX_ENOMEM)); #if MDBX_DEBUG memset(txn, 0xCD, size); VALGRIND_MAKE_MEM_UNDEFINED(txn, size); @@ -13071,7 +13198,7 @@ int mdbx_txn_begin_ex(MDBX_env *env, MDBX_txn *parent, MDBX_txn_flags_t flags, M pnl_free(txn->tw.repnl); dpl_free(txn); osal_free(txn); - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(rc)); } /* Move loose pages to reclaimed list */ @@ -13181,10 +13308,12 @@ int mdbx_txn_begin_ex(MDBX_env *env, MDBX_txn *parent, MDBX_txn_flags_t flags, M txn->dbs[FREE_DBI].root); } - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_begin, LOG_IFERR(rc)); } int mdbx_txn_commit_ex(MDBX_txn *txn, MDBX_commit_latency *latency) { + MDBX_DEBUG_STATS_SCOPE(txn_commit); + STATIC_ASSERT(MDBX_TXN_FINISHED == MDBX_TXN_BLOCKED - MDBX_TXN_HAS_CHILD - MDBX_TXN_ERROR - MDBX_TXN_PARKED); const uint64_t ts_0 = latency ? osal_monotime() : 0; uint64_t ts_1 = 0, ts_2 = 0, ts_3 = 0, ts_4 = 0, ts_5 = 0, gc_cputime = 0; @@ -13201,7 +13330,7 @@ int mdbx_txn_commit_ex(MDBX_txn *txn, MDBX_commit_latency *latency) { bailout: if (latency) memset(latency, 0, sizeof(*latency)); - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_commit, LOG_IFERR(rc)); } MDBX_env *const env = txn->env; @@ -13223,7 +13352,7 @@ int mdbx_txn_commit_ex(MDBX_txn *txn, MDBX_commit_latency *latency) { if ((txn->flags & MDBX_NOSTICKYTHREADS) && txn == env->basal_txn && unlikely(txn->owner != osal_thread_self())) { txn->flags |= MDBX_TXN_ERROR; rc = MDBX_THREAD_MISMATCH; - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_commit, LOG_IFERR(rc)); } #endif /* MDBX_TXN_CHECKOWNER */ @@ -13596,7 +13725,7 @@ int mdbx_txn_commit_ex(MDBX_txn *txn, MDBX_commit_latency *latency) { latency->ending = ts_5 ? osal_monotime_to_16dot16(ts_6 - ts_5) : 0; latency->whole = osal_monotime_to_16dot16_noUnderflow(ts_6 - ts_0); } - return LOG_IFERR(rc); + MDBX_DEBUG_STATS_RETURN(txn_commit, LOG_IFERR(rc)); fail: txn->flags |= MDBX_TXN_ERROR; @@ -24091,6 +24220,10 @@ __cold static void mdbx_init(void) { globals.runtime_flags = ((MDBX_DEBUG) > 0) * MDBX_DBG_ASSERT + ((MDBX_DEBUG) > 1) * MDBX_DBG_AUDIT; globals.loglevel = MDBX_LOG_FATAL; ENSURE(nullptr, osal_fastmutex_init(&globals.debug_lock) == 0); +#if defined(ND_MDBX_INSTRUMENT) + ENSURE(nullptr, osal_fastmutex_init(&mdbx_debug_stats.lock) == 0); + mdbx_debug_stats.ready = true; +#endif osal_ctor(); assert(globals.sys_pagesize > 0 && (globals.sys_pagesize & (globals.sys_pagesize - 1)) == 0); rthc_ctor(); @@ -24107,6 +24240,10 @@ __cold static void mdbx_fini(void) { rthc_dtor(current_pid); osal_dtor(); TRACE("<< pid %d\n", current_pid); +#if defined(ND_MDBX_INSTRUMENT) + mdbx_debug_stats.ready = false; + ENSURE(nullptr, osal_fastmutex_destroy(&mdbx_debug_stats.lock) == 0); +#endif ENSURE(nullptr, osal_fastmutex_destroy(&globals.debug_lock) == 0); } diff --git a/third_party/mdbx/mdbx.h b/third_party/mdbx/mdbx.h index d10f280e91..6a2d21f827 100644 --- a/third_party/mdbx/mdbx.h +++ b/third_party/mdbx/mdbx.h @@ -635,6 +635,13 @@ extern LIBMDBX_VERINFO_API const struct MDBX_build_info { during library build */ } /** \brief libmdbx build information */ mdbx_build; +/** \brief Print and reset debug MDBX command timing statistics. + * + * When `ND_MDBX_INSTRUMENT` is enabled this prints cumulative per-command MDBX + * timing + * since the previous call. In non-debug builds this is a no-op. */ +LIBMDBX_API void print_mdbx_stats(void); + #if (defined(_WIN32) || defined(_WIN64)) && !MDBX_BUILD_SHARED_LIBRARY /* MDBX internally uses global and thread local storage destructors to * automatically (de)initialization, releasing reader lock table slots From 0da6287d87465df992f9c62a3443c5c200056fa3 Mon Sep 17 00:00:00 2001 From: shaleenji Date: Tue, 10 Mar 2026 14:04:52 +0000 Subject: [PATCH 34/48] Update settings.hpp --- src/utils/settings.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/settings.hpp b/src/utils/settings.hpp index aa6d17d39f..1c13a3a457 100644 --- a/src/utils/settings.hpp +++ b/src/utils/settings.hpp @@ -16,7 +16,7 @@ namespace settings { // For strings we use inline const and not constexpr. Some compilers // do not support constexpr for std::string inline const std::string NAME = "Endee"; - inline const std::string VERSION = "1.0.0-beta"; + inline const std::string VERSION = "1.0.0"; inline uint16_t INDEX_VERSION = 1; inline uint16_t SPARSE_ONDISK_VERSION = 1; inline const std::string DEFAULT_SPACE_TYPE = "cosine"; From bcd05c05feb6f07cbe61aba4416aad0fae521c80 Mon Sep 17 00:00:00 2001 From: shaleenji Date: Wed, 11 Mar 2026 06:19:01 +0000 Subject: [PATCH 35/48] Readme update (#72) --- README.md | 414 ++++++++----------------------------- docs/assets/logo-dark.svg | 1 + docs/assets/logo-light.svg | 1 + docs/getting-started.md | 305 +++++++++++++++++++++++++++ 4 files changed, 391 insertions(+), 330 deletions(-) create mode 100644 docs/assets/logo-dark.svg create mode 100644 docs/assets/logo-light.svg create mode 100644 docs/getting-started.md diff --git a/README.md b/README.md index 2ef39bffac..02aa96cab6 100644 --- a/README.md +++ b/README.md @@ -1,385 +1,139 @@ -# Endee: High-Performance Open Source Vector Database +

+ + + + Endee + +

-**Endee (nD)** is a specialized, high-performance vector database built for speed and efficiency. This guide covers supported platforms, dependency requirements, and detailed build instructions using both our automated installer and manual CMake configuration. +

+ High-performance open-source vector database for AI search, RAG, semantic search, and hybrid retrieval. +

-there are 3 ways to build and run endee: -1. quick installation and run using install.sh and run.sh scripts -2. manual build using cmake -3. using docker +

+ Quick Start + Docs + License + Discord + Website + + +

-also you can run endee using docker from docker hub without building it locally. refer to section 4 for more details. +

+Quick StartWhy EndeeUse CasesFeaturesAPI and ClientsDocsContact +

---- +# Endee: Open-Source Vector Database for AI Search -## System Requirements +**Endee** is a high-performance open-source vector database built for AI search and retrieval workloads. It is designed for teams building **RAG pipelines**, **semantic search**, **hybrid search**, recommendation systems, and filtered vector retrieval APIs that need production-oriented performance and control. -Before installing, ensure your system meets the following hardware and operating system requirements. +Endee combines vector search with filtering, sparse retrieval support, backup workflows, and deployment flexibility across local builds and Docker-based environments. The project is implemented in C++ and optimized for modern CPU targets, including AVX2, AVX512, NEON, and SVE2. -### Supported Operating Systems +If you want the fastest path to evaluate Endee locally, start with the [Getting Started guide](./docs/getting-started.md) or the hosted docs at [docs.endee.io](https://docs.endee.io/quick-start). -* **Linux**: Ubuntu(22.04, 24.04, 25.04) Debian(12, 13), Rocky(8, 9, 10), Centos(8, 9, 10), Fedora(40, 42, 43) -* **macOS**: Apple Silicon (M Series) only. +## Why Endee -### Required Dependencies +- Built as a dedicated vector database for AI applications, search systems, and retrieval-heavy workloads. +- Supports dense vector retrieval plus sparse search capabilities for hybrid search use cases. +- Includes payload filtering for metadata-aware retrieval and application-specific query logic. +- Ships with operational features already documented in this repo, including backup flows and runtime observability. +- Offers flexible deployment paths: local scripts, manual builds, Docker images, and prebuilt registry images. -The following packages are required for compilation. +## Getting Started - `clang-19`, `cmake`, `build-essential`, `libssl-dev`, `libcurl4-openssl-dev` +The full installation, build, Docker, runtime, and authentication instructions are in [docs/getting-started.md](./docs/getting-started.md). -> **Note:** The build system requires **Clang 19** (or a compatible recent Clang version) supporting C++20. - ---- - -## 1. Quick Installation (Recommended) - -The easiest way to build **ndd** is using the included `install.sh` script. This script handles OS detection, dependency checks, and configuration automatically. - -### Usage - -First, ensure the script is executable: -```bash -chmod +x ./install.sh -``` - -Run the script from the root of the repository. You **must** provide arguments for the build mode and/or CPU optimization. - -```bash -./install.sh [BUILD_MODE] [CPU_OPTIMIZATION] -``` - -### Build Arguments - -You can combine one **Build Mode** and one **CPU Optimization** flag. - -#### Build Modes - -| Flag | Description | CMake Equivalent | -| --- | --- | --- | -| `--release` | **Default.** Optimized release build. | | -| `--debug_all` | Enables full debugging symbols. | `-DND_DEBUG=ON -DDEBUG=ON` | -| `--debug_nd` | Enables NDD-specific logging/timing. | `-DND_DEBUG=ON` | - -#### CPU Optimization Options - -Select the flag matching your hardware to enable SIMD optimizations. - -| Flag | Description | Target Hardware | -| --- | --- | --- | -| `--avx2` | Enables AVX2 (FMA, F16C) | Modern x86_64 Intel/AMD | -| `--avx512` | Enables AVX512 (F, BW, VNNI, FP16) | Server-grade x86_64 (Xeon/Epyc) | -| `--neon` | Enables NEON (FP16, DotProd) | Apple Silicon / ARMv8.2+ | -| `--sve2` | Enables SVE2 (INT8/16, FP16) | ARMv9 / SVE2 compatible | - -> **Note:** The `--avx512` build configuration enforces mandatory runtime checks for specific instruction sets. To successfully run this build, your CPU must support **`avx512` (Foundation), `avx512_fp16`, `avx512_vnni`, `avx512bw`, and `avx512_vpopcntdq`**; if any of these extensions are missing, the database will fail to initialize and exit immediately to avoid runtime crashes. - - -### Example Commands - -**Build for Production (Intel/AMD with AVX2):** +Fastest local path: ```bash +chmod +x ./install.sh ./run.sh ./install.sh --release --avx2 -``` - -**Example Build for Debugging (Apple Silicon):** - -```bash -./install.sh --debug_all --neon -``` - -### Running the Server - -We provide a `run.sh` script to simplify running the server. It automatically detects the built binary and uses `ndd_data_dir=./data` by default. - -First, ensure the script is executable: - -```bash -chmod +x ./run.sh -``` - -Then run the script: - -```bash ./run.sh ``` -This will automatically identify the latest binary and start the server. +The server listens on port `8080`. For detailed setup paths, supported operating systems, CPU optimization flags, Docker usage, and authentication examples, use: -#### Options +- [Getting Started](./docs/getting-started.md) +- [Hosted Quick Start Docs](https://docs.endee.io/quick-start) -You can override the defaults using arguments: +## Use Cases -* `ndd_data_dir=DIR`: Set the data directory. -* `binary_file=FILE`: Set the binary file to run. -* `ndd_auth_token=TOKEN`: Set the authentication token (leave empty/ignore to run without authentication). +### RAG and AI Retrieval -#### Examples +Use Endee as the retrieval layer for question answering, chat assistants, copilots, and other RAG applications that need fast vector search with metadata-aware filtering. -**Run with custom data directory:** +### Agentic AI and AI Agent Memory -```bash -./run.sh ndd_data_dir=./my_data -``` +Use Endee as the long-term memory and context retrieval layer for AI agents built with frameworks like LangChain, CrewAI, AutoGen, and LlamaIndex. Store and retrieve past observations, tool outputs, conversation history, and domain knowledge mid-execution with low-latency filtered vector search, so your autonomous agents get the right context without stalling their reasoning loop. -**Run specific binary:** +### Semantic Search -```bash -./run.sh binary_file=./build/ndd-avx2 -``` +Build semantic search experiences for documents, products, support content, and knowledge bases using vector similarity search instead of exact keyword-only matching. -**Run with authentication token:** +### Hybrid Search -```bash -./run.sh ndd_auth_token=your_token -``` +Combine dense retrieval, sparse vectors, and filtering to improve relevance for search workflows where both semantic understanding and term-level precision matter. +### Recommendations and Matching -**Run with all options** +Support recommendation, similarity matching, and nearest-neighbor retrieval workflows across text, embeddings, and other high-dimensional representations. -```bash -./run.sh ndd_data_dir=./my_data binary_file=./build/ndd-avx2 ndd_auth_token=your_token -``` - -**For Help** - -```bash -./run.sh --help -``` +## Features +- **Vector search** for AI retrieval and semantic similarity workloads. +- **Hybrid retrieval support** with sparse vector capabilities documented in [docs/sparse.md](./docs/sparse.md). +- **Payload filtering** for structured retrieval logic documented in [docs/filter.md](./docs/filter.md). +- **Backup APIs and flows** documented in [docs/backup-system.md](./docs/backup-system.md). +- **Operational logging and instrumentation** documented in [docs/logs.md](./docs/logs.md) and [docs/mdbx-instrumentation.md](./docs/mdbx-instrumentation.md). +- **CPU-targeted builds** for AVX2, AVX512, NEON, and SVE2 deployments. +- **Docker deployment options** for local and server environments. -## 2. Manual Build (Advanced) +## API and Clients -If you prefer to configure the build manually or integrate it into an existing install pipeline, you can use `cmake` directly. +Endee exposes an HTTP API for managing indexes and serving retrieval workloads. The current repo documentation and examples focus on running the server directly and calling its API endpoints. -### Step 1: Prepare Build Directory - -```bash -mkdir build && cd build -``` - -### Step 2: Configure - -Run `cmake` with the appropriate flags. You must manually define the compiler if it is not your system default. - -**Configuration Flags:** - -* **Debug Options:** -* `-DDEBUG=ON` (Enable debug symbols/O0) -* `-DND_DEBUG=ON` (Enable internal logging) - - -* **SIMD Selectors (Choose One):** -* `-DUSE_AVX2=ON` -* `-DUSE_AVX512=ON` -* `-DUSE_NEON=ON` -* `-DUSE_SVE2=ON` - - -**Example (x86_64 AVX512 Release):** - -```bash -cmake -DCMAKE_BUILD_TYPE=Release \ - -DUSE_AVX512=ON \ - .. -``` - -### Step 3: Compile - -```bash -make -j$(nproc) -``` +Current developer entry points: -### Running the Built Binary +- [Getting Started](./docs/getting-started.md) for local build and run flows +- [Hosted Docs](https://docs.endee.io/quick-start) for product documentation +- [Release Notes 1.0.0](./docs/release-notes-1.0.0.md) for recent platform changes -After a successful build, the binary will be generated in the `build/` directory. +## Docs and Links -### Binary Naming +- [Getting Started](./docs/getting-started.md) +- [Hosted Documentation](https://docs.endee.io/quick-start) +- [Release Notes](./docs/release-notes-1.0.0.md) +- [Sparse Search](./docs/sparse.md) +- [Filtering](./docs/filter.md) +- [Backups](./docs/backup-system.md) -The output binary name depends on the SIMD flag used during compilation: +## Community and Contact -* `ndd-avx2` -* `ndd-avx512` -* `ndd-neon` (or `ndd-neon-darwin` for mac) -* `ndd-sve2` +- Join the community on [Discord](https://discord.gg/5HFGqDZQE3) +- Visit the website at [endee.io](https://endee.io/) +- For trademark or branding permissions, contact [enterprise@endee.io](mailto:enterprise@endee.io) -A symlink called `ndd` links to the binary compiled for the current build. +## Contributing -### Runtime Environment Variables +We welcome contributions from the community to help make vector search faster and more accessible for everyone. -Some environment variables **ndd** reads at runtime: - -* `NDD_DATA_DIR`: Defines the data directory -* `NDD_AUTH_TOKEN`: Optional authentication token (see below) - -### Authentication - -**ndd** supports two authentication modes: - -**Open Mode (No Authentication)** - Default when `NDD_AUTH_TOKEN` is not set: -```bash -# All APIs work without authentication -./build/ndd -curl http://{{BASE_URL}}/api/v1/index/list -``` - -**Token Mode** - When `NDD_AUTH_TOKEN` is set: -```bash -# Generate a secure token -export NDD_AUTH_TOKEN=$(openssl rand -hex 32) -./build/ndd - -# All protected APIs require the token in Authorization header -curl -H "Authorization: $NDD_AUTH_TOKEN" http://{{BASE_URL}}/api/v1/index/list -``` - -### Execution Example - -To run the database using the AVX2 binary and a local `data` folder: - -```bash -# 1. Create the data directory -mkdir -p ./data - -# 2. Export the environment variable and run -export NDD_DATA_DIR=$(pwd)/data -./build/ndd -``` - -Alternatively, as a single line: - -```bash -NDD_DATA_DIR=./data ./build/ndd -``` - ---- - - - -## 3. Docker Deployment - -We provide a Dockerfile for easy containerization. This ensures a consistent runtime environment and simplifies the deployment process across various platforms. - -### Build the Image - -You **must** specify the target architecture (`avx2`, `avx512`, `neon`, `sve2`) using the `BUILD_ARCH` build argument. You can optionally enable a debug build using the `DEBUG` argument. - -```bash -# Production Build (AVX2) (for x86_64 systems) -docker build --ulimit nofile=100000:100000 --build-arg BUILD_ARCH=avx2 -t endee-oss:latest -f ./infra/Dockerfile . - -# Debug Build (Neon) (for arm64, mac apple silicon) -docker build --ulimit nofile=100000:100000 --build-arg BUILD_ARCH=neon --build-arg DEBUG=true -t endee-oss:latest -f ./infra/Dockerfile . -``` - -### Run the Container - -The container exposes port `8080` and stores data in `/data` inside container. You should persist this data using a docker volume. - -```bash -docker run \ - -p 8080:8080 \ - -v endee-data:/data \ - -e NDD_AUTH_TOKEN="your_secure_token" \ - --name endee-server \ - endee-oss:latest -``` - -leave `NDD_AUTH_TOKEN` empty or remove it to run endee without authentication. - -### Alternatively: Docker Compose - -You can also use `docker-compose` to run the service. - -1. Start the container: - ```bash - docker-compose up - ``` - ---- - -## 4. Running Docker container from registry - -You can run Endee directly using the pre-built image from Docker Hub without building locally. - -### Using Docker Compose - -Create a new directory for Endee: - -```bash -mkdir endee && cd endee -``` - -Inside this directory, create a file named `docker-compose.yml` and copy the following content into it: - -```yaml -services: - endee: - image: endeeio/endee-server:latest - container_name: endee-server - ports: - - "8080:8080" - environment: - NDD_NUM_THREADS: 0 - NDD_AUTH_TOKEN: "" # Optional: set for authentication - volumes: - - endee-data:/data - restart: unless-stopped - -volumes: - endee-data: -``` - -Then run: -```bash -docker compose up -d -``` - -for more details visit [docs.endee.io](https://docs.endee.io/quick-start) - ---- - -## Contribution - -We welcome contributions from the community to help make vector search faster and more accessible for everyone. To contribute: - -* **Submit Pull Requests**: Have a fix or a new feature? Fork the repo, create a branch, and send a PR. -* **Report Issues**: Found a bug or a performance bottleneck? Open an issue on GitHub with steps to reproduce it. -* **Suggest Improvements**: We are always looking to optimize performance; feel free to suggest new CPU target optimizations or architectural enhancements. -* **Feature Requests**: If there is a specific functionality you need, start a discussion in the issues section. - ---- +- Submit pull requests for fixes, features, and improvements +- Report bugs or performance issues through GitHub issues +- Propose enhancements for search quality, performance, and deployment workflows ## License -Endee is open source software licensed under the -**Apache License 2.0**. - -You are free to use, modify, and distribute this software for -personal, commercial, and production use. - -See the LICENSE file for full license terms. - ---- +Endee is open source software licensed under the **Apache License 2.0**. See the [LICENSE](./LICENSE) file for full terms. ## Trademark and Branding “Endee” and the Endee logo are trademarks of Endee Labs. -The Apache License 2.0 does **not** grant permission to use the Endee name, -logos, or branding in a way that suggests endorsement or affiliation. - -If you offer a hosted or managed service based on this software, you must: -- Use your own branding -- Avoid implying it is an official Endee service +The Apache License 2.0 does not grant permission to use the Endee name, logos, or branding in a way that suggests endorsement or affiliation. -For trademark or branding permissions, contact: enterprise@endee.io - ---- +If you offer a hosted or managed service based on this software, you must use your own branding and avoid implying it is an official Endee service. ## Third-Party Software -This project includes or depends on third-party software components that are -licensed under their respective open source licenses. - -Use of those components is governed by the terms and conditions of their -individual licenses, not by the Apache License 2.0 for this project. +This project includes or depends on third-party software components licensed under their respective open-source licenses. Use of those components is governed by their own license terms. diff --git a/docs/assets/logo-dark.svg b/docs/assets/logo-dark.svg new file mode 100644 index 0000000000..9d2d82504c --- /dev/null +++ b/docs/assets/logo-dark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/assets/logo-light.svg b/docs/assets/logo-light.svg new file mode 100644 index 0000000000..855dadc48e --- /dev/null +++ b/docs/assets/logo-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000000..33b48e8887 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,305 @@ +# Getting Started with Endee + +This guide covers the technical setup and runtime details for building and running Endee locally or with Docker. + +## System Requirements + +Before installing, ensure your system meets the following hardware and operating system requirements. + +### Supported Operating Systems + +- **Linux**: Ubuntu(22.04, 24.04, 25.04) Debian(12, 13), Rocky(8, 9, 10), CentOS(8, 9, 10), Fedora(40, 42, 43) +- **macOS**: Apple Silicon (M Series) only + +### Required Dependencies + +The following packages are required for compilation: + +`clang-19`, `cmake`, `build-essential`, `libssl-dev`, `libcurl4-openssl-dev` + +> **Note:** The build system requires **Clang 19** or a compatible recent Clang version with C++20 support. + +## 1. Quick Installation + +The easiest way to build **ndd** is using the included `install.sh` script. This script handles OS detection, dependency checks, and configuration automatically. + +### Usage + +First, ensure the script is executable: + +```bash +chmod +x ./install.sh +``` + +Run the script from the repository root. You must provide arguments for the build mode and/or CPU optimization. + +```bash +./install.sh [BUILD_MODE] [CPU_OPTIMIZATION] +``` + +### Build Arguments + +You can combine one build mode and one CPU optimization flag. + +#### Build Modes + +| Flag | Description | CMake Equivalent | +| --- | --- | --- | +| `--release` | Default optimized release build | | +| `--debug_all` | Enable full debugging symbols | `-DND_DEBUG=ON -DDEBUG=ON` | +| `--debug_nd` | Enable NDD-specific logging and timing | `-DND_DEBUG=ON` | + +#### CPU Optimization Options + +Select the flag matching your hardware to enable SIMD optimizations. + +| Flag | Description | Target Hardware | +| --- | --- | --- | +| `--avx2` | Enable AVX2 (FMA, F16C) | Modern x86_64 Intel/AMD | +| `--avx512` | Enable AVX512 (F, BW, VNNI, FP16) | Server-grade x86_64 (Xeon/Epyc) | +| `--neon` | Enable NEON (FP16, DotProd) | Apple Silicon / ARMv8.2+ | +| `--sve2` | Enable SVE2 (INT8/16, FP16) | ARMv9 / SVE2-compatible systems | + +> **Note:** The `--avx512` build configuration enforces runtime checks for required instruction sets. Your CPU must support `avx512`, `avx512_fp16`, `avx512_vnni`, `avx512bw`, and `avx512_vpopcntdq` or the database will exit during initialization. + +### Example Commands + +Build for production on Intel/AMD with AVX2: + +```bash +./install.sh --release --avx2 +``` + +Build for debugging on Apple Silicon: + +```bash +./install.sh --debug_all --neon +``` + +### Running the Server + +Use `run.sh` to simplify local startup. It automatically detects the built binary and uses `ndd_data_dir=./data` by default. + +First, ensure the script is executable: + +```bash +chmod +x ./run.sh +``` + +Then run: + +```bash +./run.sh +``` + +#### Options + +- `ndd_data_dir=DIR`: set the data directory +- `binary_file=FILE`: set the binary file to run +- `ndd_auth_token=TOKEN`: set the authentication token; leave empty to run without authentication + +#### Examples + +Run with a custom data directory: + +```bash +./run.sh ndd_data_dir=./my_data +``` + +Run a specific binary: + +```bash +./run.sh binary_file=./build/ndd-avx2 +``` + +Run with authentication enabled: + +```bash +./run.sh ndd_auth_token=your_token +``` + +Run with all options: + +```bash +./run.sh ndd_data_dir=./my_data binary_file=./build/ndd-avx2 ndd_auth_token=your_token +``` + +Show help: + +```bash +./run.sh --help +``` + +## 2. Manual Build + +If you prefer to configure the build manually or integrate it into an existing install pipeline, use `cmake` directly. + +### Step 1: Prepare the Build Directory + +```bash +mkdir build && cd build +``` + +### Step 2: Configure + +Run `cmake` with the appropriate flags. Define the compiler manually if it is not your system default. + +Debug options: + +- `-DDEBUG=ON` to enable debug symbols and `O0` +- `-DND_DEBUG=ON` to enable internal logging + +SIMD selectors, choose one: + +- `-DUSE_AVX2=ON` +- `-DUSE_AVX512=ON` +- `-DUSE_NEON=ON` +- `-DUSE_SVE2=ON` + +Example x86_64 AVX512 release configuration: + +```bash +cmake -DCMAKE_BUILD_TYPE=Release \ + -DUSE_AVX512=ON \ + .. +``` + +### Step 3: Compile + +```bash +make -j$(nproc) +``` + +### Running the Built Binary + +After a successful build, the binary is generated in the `build/` directory. + +### Binary Naming + +The output binary name depends on the SIMD flag used during compilation: + +- `ndd-avx2` +- `ndd-avx512` +- `ndd-neon` or `ndd-neon-darwin` on macOS +- `ndd-sve2` + +A symlink named `ndd` points to the binary compiled for the current build. + +### Runtime Environment Variables + +Some environment variables **ndd** reads at runtime: + +- `NDD_DATA_DIR`: defines the data directory +- `NDD_AUTH_TOKEN`: optional authentication token + +### Authentication + +**Open mode** with no authentication is the default when `NDD_AUTH_TOKEN` is not set: + +```bash +./build/ndd +curl http://{{BASE_URL}}/api/v1/index/list +``` + +**Token mode** is enabled when `NDD_AUTH_TOKEN` is set: + +```bash +export NDD_AUTH_TOKEN=$(openssl rand -hex 32) +./build/ndd +curl -H "Authorization: $NDD_AUTH_TOKEN" http://{{BASE_URL}}/api/v1/index/list +``` + +### Execution Example + +Run the database using the AVX2 binary and a local `data` folder: + +```bash +mkdir -p ./data +export NDD_DATA_DIR=$(pwd)/data +./build/ndd +``` + +Alternatively: + +```bash +NDD_DATA_DIR=./data ./build/ndd +``` + +## 3. Docker Deployment + +Endee ships with a Dockerfile for containerized deployment. + +### Build the Image + +You must specify the target architecture using the `BUILD_ARCH` build argument. Valid targets are `avx2`, `avx512`, `neon`, and `sve2`. You can optionally enable a debug build using `DEBUG=true`. + +```bash +docker build --ulimit nofile=100000:100000 --build-arg BUILD_ARCH=avx2 -t endee-oss:latest -f ./infra/Dockerfile . +``` + +```bash +docker build --ulimit nofile=100000:100000 --build-arg BUILD_ARCH=neon --build-arg DEBUG=true -t endee-oss:latest -f ./infra/Dockerfile . +``` + +### Run the Container + +The container exposes port `8080` and stores data in `/data` inside the container. Persist that data with a Docker volume. + +```bash +docker run \ + -p 8080:8080 \ + -v endee-data:/data \ + -e NDD_AUTH_TOKEN="your_secure_token" \ + --name endee-server \ + endee-oss:latest +``` + +Leave `NDD_AUTH_TOKEN` empty or remove it to run Endee without authentication. + +### Docker Compose + +You can also use Docker Compose: + +```bash +docker-compose up +``` + +## 4. Run from Docker Registry + +You can run Endee directly using the prebuilt image from Docker Hub without building locally. + +### Using Docker Compose + +Create a new directory: + +```bash +mkdir endee && cd endee +``` + +Create a `docker-compose.yml` file with: + +```yaml +services: + endee: + image: endeeio/endee-server:latest + container_name: endee-server + ports: + - "8080:8080" + environment: + NDD_NUM_THREADS: 0 + NDD_AUTH_TOKEN: "" + volumes: + - endee-data:/data + restart: unless-stopped + +volumes: + endee-data: +``` + +Then run: + +```bash +docker compose up -d +``` + +For more details, visit [docs.endee.io](https://docs.endee.io/quick-start). From 350c974dd9278fd410763d341764f67b87f76b69 Mon Sep 17 00:00:00 2001 From: Shaleen Garg Date: Wed, 11 Mar 2026 11:49:44 +0530 Subject: [PATCH 36/48] fixing sve2 compilation --- src/sparse/inverted_index.hpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sparse/inverted_index.hpp b/src/sparse/inverted_index.hpp index f19df89bb1..0627cf7a35 100644 --- a/src/sparse/inverted_index.hpp +++ b/src/sparse/inverted_index.hpp @@ -16,6 +16,9 @@ #if defined(__x86_64__) || defined(_M_X64) # include #elif defined(__aarch64__) || defined(_M_ARM64) +# if defined(USE_SVE2) +# include +# endif # include #endif // defined(__x86_64__) || defined(_M_X64) From 42bf03de746e5058ad219482c00439bcc2ef0eee Mon Sep 17 00:00:00 2001 From: pankajEndee Date: Wed, 11 Mar 2026 12:12:19 +0530 Subject: [PATCH 37/48] refactor: bump web ui version to 1.2.0 (#73) Co-authored-by: Pankaj Singh --- install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 4006056947..062789b1a4 100755 --- a/install.sh +++ b/install.sh @@ -197,7 +197,7 @@ distro_factory() { # **************************************** add_frontend() { - VERSION="v1.1.0" + VERSION="v1.2.0" log "Pulling frontend version ${VERSION}" mkdir -p $script_dir/frontend cd $script_dir/frontend From 867cf0904547eb395f4948d4f6f6e3be2af97edd Mon Sep 17 00:00:00 2001 From: Shaleen Garg Date: Wed, 11 Mar 2026 12:51:44 +0530 Subject: [PATCH 38/48] readme update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 02aa96cab6..ac42738edd 100644 --- a/README.md +++ b/README.md @@ -97,13 +97,13 @@ Current developer entry points: - [Getting Started](./docs/getting-started.md) for local build and run flows - [Hosted Docs](https://docs.endee.io/quick-start) for product documentation -- [Release Notes 1.0.0](./docs/release-notes-1.0.0.md) for recent platform changes +- [Release Notes 1.0.0](https://github.com/endee-io/endee/releases/tag/1.0.0) for recent platform changes ## Docs and Links - [Getting Started](./docs/getting-started.md) - [Hosted Documentation](https://docs.endee.io/quick-start) -- [Release Notes](./docs/release-notes-1.0.0.md) +- [Release Notes](https://github.com/endee-io/endee/releases/tag/1.0.0) - [Sparse Search](./docs/sparse.md) - [Filtering](./docs/filter.md) - [Backups](./docs/backup-system.md) From 2e1746aac17e9f1140be74df85b5bec67d32b973 Mon Sep 17 00:00:00 2001 From: rajesh33411 Date: Thu, 12 Mar 2026 10:37:27 +0530 Subject: [PATCH 39/48] restructuring getting_started docs (#74) Co-authored-by: rajeshkomaravelli --- docs/getting-started.md | 429 +++++++++++++++++++++++++--------------- 1 file changed, 268 insertions(+), 161 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 33b48e8887..a0840a7b6c 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,305 +1,412 @@ -# Getting Started with Endee +# Endee: High-Performance Open Source Vector Database -This guide covers the technical setup and runtime details for building and running Endee locally or with Docker. +**Endee** is a specialized, high-performance vector database built for speed and efficiency — engineered for production AI systems that need to process data at scale. -## System Requirements +Never heard of a vector database? No worries — check out our blog where we explain what it is and what you can build with it: [endee.io/blog](https://endee.io/blog) -Before installing, ensure your system meets the following hardware and operating system requirements. +--- -### Supported Operating Systems +## Let's Get Started -- **Linux**: Ubuntu(22.04, 24.04, 25.04) Debian(12, 13), Rocky(8, 9, 10), CentOS(8, 9, 10), Fedora(40, 42, 43) -- **macOS**: Apple Silicon (M Series) only +To use Endee, the first step is to start the server. There are 4 ways to do this: -### Required Dependencies +1. **Docker** — pull and run the pre-built image from Docker Hub +2. **Docker build from source** — build the Docker image yourself from the source code(recommended) +3. **install.sh script** — automated build script for Linux and macOS +4. **Manual CMake build** — full manual control over the build -The following packages are required for compilation: +> **Windows users:** Methods 3 and 4 are not supported on Windows. Docker (Methods 1 or 2) is the only way to run Endee on Windows. -`clang-19`, `cmake`, `build-essential`, `libssl-dev`, `libcurl4-openssl-dev` +--- -> **Note:** The build system requires **Clang 19** or a compatible recent Clang version with C++20 support. +## Method 1: Docker -## 1. Quick Installation +### Prerequisite: Install Docker -The easiest way to build **ndd** is using the included `install.sh` script. This script handles OS detection, dependency checks, and configuration automatically. +That's the only thing you need. Go to [https://docs.docker.com/get-docker/](https://docs.docker.com/get-docker/) and install Docker Desktop for your OS (Windows, Mac, or Linux). -### Usage - -First, ensure the script is executable: +Once installed, open a terminal and verify it works: ```bash -chmod +x ./install.sh +docker --version ``` -Run the script from the repository root. You must provide arguments for the build mode and/or CPU optimization. +--- + +### Run Endee ```bash -./install.sh [BUILD_MODE] [CPU_OPTIMIZATION] +docker run \ + --ulimit nofile=100000:100000 \ + -p 8080:8080 \ + -v ./endee-data:/data \ + --name endee-server \ + --restart unless-stopped \ + endeeio/endee-server:latest ``` -### Build Arguments +That's it. Docker pulls the image automatically if not already present, and Endee starts. -You can combine one build mode and one CPU optimization flag. +**What each flag means:** -#### Build Modes +| Flag | Meaning | +|---|---| +| `--ulimit nofile=100000:100000` | Allows the container to open up to 100,000 files simultaneously — databases need this | +| `-p 8080:8080` | Port mapping (`your_machine:container`). Makes the server reachable at `localhost:8080` | +| `-v ./endee-data:/data` | Stores Endee's database files in an `endee-data/` folder in your current directory | +| `--name endee-server` | Names the container so you can reference it easily (e.g. `docker stop endee-server`) | +| `--restart unless-stopped` | Auto-restarts if the container crashes, but not if you manually stop it | +| `-e NDD_AUTH_TOKEN=your_token` | Optional. Protects the server with a token — every client request must include it, otherwise the server rejects it. Omit to run with no authentication. | +| `endeeio/endee-server:latest` | The official pre-built Endee image from Docker Hub | -| Flag | Description | CMake Equivalent | -| --- | --- | --- | -| `--release` | Default optimized release build | | -| `--debug_all` | Enable full debugging symbols | `-DND_DEBUG=ON -DDEBUG=ON` | -| `--debug_nd` | Enable NDD-specific logging and timing | `-DND_DEBUG=ON` | +**Want to store the data in a different folder?** Replace `./endee-data` with any path: -#### CPU Optimization Options +```bash +-v ./endee-data:/data # default: stores in an 'endee-data/' folder in the current directory +-v ~/endee-data:/data # example: stores in your home directory +-v C:/Users/YourName/endee-data:/data # example: Windows local drive +-v /Volumes/MyDrive/endee-data:/data # example: macOS local drive +``` + +Only change the left side of the `:` — the right side (`/data`) is fixed. -Select the flag matching your hardware to enable SIMD optimizations. +--- -| Flag | Description | Target Hardware | -| --- | --- | --- | -| `--avx2` | Enable AVX2 (FMA, F16C) | Modern x86_64 Intel/AMD | -| `--avx512` | Enable AVX512 (F, BW, VNNI, FP16) | Server-grade x86_64 (Xeon/Epyc) | -| `--neon` | Enable NEON (FP16, DotProd) | Apple Silicon / ARMv8.2+ | -| `--sve2` | Enable SVE2 (INT8/16, FP16) | ARMv9 / SVE2-compatible systems | +### Verify it's running -> **Note:** The `--avx512` build configuration enforces runtime checks for required instruction sets. Your CPU must support `avx512`, `avx512_fp16`, `avx512_vnni`, `avx512bw`, and `avx512_vpopcntdq` or the database will exit during initialization. +Open your browser and go to **[http://localhost:8080](http://localhost:8080)** — you should see the Endee dashboard. -### Example Commands +--- -Build for production on Intel/AMD with AVX2: +### Stop the server ```bash -./install.sh --release --avx2 +docker stop endee-server ``` -Build for debugging on Apple Silicon: +Your data stays safe in the folder you configured. + +--- + +### Next steps: Using Endee + +Once your server is running, head over to [docs.endee.io/quick-start](https://docs.endee.io/quick-start) to learn how to create indexes, store vectors, and run your first similarity search. + +--- + +## Method 2: Docker Build from Source(Recommended) + +Use this if you want to build the image yourself from the source code instead of pulling from Docker Hub. + +### Prerequisites + +- Docker installed (same as Method 1) +- The repo cloned on your machine ```bash -./install.sh --debug_all --neon +git clone https://github.com/endee-io/endee.git +cd endee ``` -### Running the Server +--- + +### Step 1: Figure out your CPU type -Use `run.sh` to simplify local startup. It automatically detects the built binary and uses `ndd_data_dir=./data` by default. +Endee uses special CPU instructions called **SIMD** to run vector searches extremely fast. Different CPUs support different instruction sets, so you need to pick the right one when building. -First, ensure the script is executable: +> Think of it like this: your CPU has built-in shortcuts for doing math on large amounts of data at once. SIMD lets Endee use those shortcuts. Picking the right flag tells the compiler which shortcuts your CPU has. +| Your hardware | Flag to use | +|---|---| +| Mac with M1 / M2 / M3 / M4 chip | `neon` | +| Linux or Windows with Intel or AMD CPU | `avx2` | +| Server-grade Intel Xeon / AMD EPYC | `avx512` | +| ARM server (ARMv9) | `sve2` | + +Not sure on Linux? Run this to check: ```bash -chmod +x ./run.sh +lscpu | grep -o 'avx2\|avx512' ``` -Then run: +--- + +### Step 2: Build the image +**For Intel/AMD (x86_64):** ```bash -./run.sh +docker build \ + --ulimit nofile=100000:100000 \ + --build-arg BUILD_ARCH=avx2 \ + -t endee-oss:latest \ + -f ./infra/Dockerfile \ + . +``` + +**For Apple Silicon Mac:** +```bash +docker build \ + --ulimit nofile=100000:100000 \ + --build-arg BUILD_ARCH=neon \ + -t endee-oss:latest \ + -f ./infra/Dockerfile \ + . ``` -#### Options +**What each flag means:** + +| Flag | Meaning | +|---|---| +| `--ulimit nofile=100000:100000` | Allows the build process to open up to 100,000 files at once. The compiler opens many files during a large C++ build — without this it can fail. | +| `--build-arg BUILD_ARCH=avx2` | Passes a build argument into the Dockerfile. This tells it which CPU instruction set to compile for (`avx2`, `avx512`, `neon`, or `sve2`). | +| `-t endee-oss:latest` | Tags (names) the resulting image as `endee-oss:latest` so you can refer to it by name when running it. | +| `-f ./infra/Dockerfile` | Points Docker to the Dockerfile — the recipe file that describes how to build the image. | +| `.` | The build context — Docker copies all files in the current directory into the build environment so the Dockerfile can access them. | -- `ndd_data_dir=DIR`: set the data directory -- `binary_file=FILE`: set the binary file to run -- `ndd_auth_token=TOKEN`: set the authentication token; leave empty to run without authentication +This will take a few minutes the first time. -#### Examples +--- -Run with a custom data directory: +### Step 3: Run with Docker Compose ```bash -./run.sh ndd_data_dir=./my_data +docker compose up -d ``` -Run a specific binary: +Docker Compose reads the `docker-compose.yml` in the repo and starts the container using the image you just built. -```bash -./run.sh binary_file=./build/ndd-avx2 +**What the `docker-compose.yml` configures by default:** + + +To enable token authentication, open `docker-compose.yml` and set `NDD_AUTH_TOKEN`: +```yaml +environment: + NDD_AUTH_TOKEN: "your_token" ``` -Run with authentication enabled: +| Setting | Default | What it means | +|---|---|---| +| `image` | `endee-oss:latest` | Uses the image you built in Step 2 | +| `ports: "8080:8080"` | Your machine : container | Makes the server reachable at `localhost:8080` | +| `ulimits: nofile: 100000` | File limit | Allows Endee to open up to 100,000 files at once | +| `NDD_NUM_THREADS: 0` | Thread count | `0` = use all available CPU threads automatically | +| `NDD_AUTH_TOKEN: ""` | Auth token | Empty = no authentication. Set a value to protect the server with a token. | +| `volumes: endee-data:/data` | Persistent storage | Uses a named Docker volume (`endee-data`) to store database files. Docker manages the storage location automatically. | +| `restart: unless-stopped` | Restart policy | Auto-restarts on crash, but not on manual stop | +| `logging max-size: 200m` | Log file size | Each log file capped at 200MB | +| `logging max-file: 5` | Log file count | Keeps at most 5 log files (1GB total) | + +--- + +### Stop the container ```bash -./run.sh ndd_auth_token=your_token +docker compose down ``` -Run with all options: +--- + +## Method 3: install.sh Script (Linux / macOS) + +This script handles OS detection, dependency installation, and the full build automatically. One command does everything. + +### Prerequisites + +- Linux or macOS (not supported on Windows) +- Git installed + +Clone the repo: ```bash -./run.sh ndd_data_dir=./my_data binary_file=./build/ndd-avx2 ndd_auth_token=your_token +git clone https://github.com/endee-io/endee.git +cd endee ``` -Show help: +Make the script executable: ```bash -./run.sh --help +chmod +x ./install.sh ``` -## 2. Manual Build +### Pick your flags -If you prefer to configure the build manually or integrate it into an existing install pipeline, use `cmake` directly. +Before running, you need two things: -### Step 1: Prepare the Build Directory +**1. Build mode** — what kind of binary to produce: -```bash -mkdir build && cd build -``` +| Flag | What it does | +|---|---| +| `--release` | Optimized build for production. Use this by default. | +| `--debug_all` | Adds full debug symbols and enables internal logging. Use when debugging crashes or tracing behavior. | +| `--debug_nd` | Enables only Endee's internal logging/timing without slowing down the binary with full debug symbols. | -### Step 2: Configure +**2. CPU flag** — same as Method 2, pick the one matching your hardware: -Run `cmake` with the appropriate flags. Define the compiler manually if it is not your system default. +| Your hardware | Flag | +|---|---| +| Mac M1 / M2 / M3 / M4 | `--neon` | +| Linux Intel / AMD | `--avx2` | +| Server-grade Xeon / EPYC | `--avx512` | +| ARMv9 server | `--sve2` | -Debug options: +### Run it -- `-DDEBUG=ON` to enable debug symbols and `O0` -- `-DND_DEBUG=ON` to enable internal logging +```bash +# Production build on Intel/AMD Linux +./install.sh --release --avx2 -SIMD selectors, choose one: +# Production build on Apple Silicon Mac +./install.sh --release --neon +``` -- `-DUSE_AVX2=ON` -- `-DUSE_AVX512=ON` -- `-DUSE_NEON=ON` -- `-DUSE_SVE2=ON` +The script will install all dependencies, compile the binary, and download the frontend automatically. -Example x86_64 AVX512 release configuration: +### Start the server ```bash -cmake -DCMAKE_BUILD_TYPE=Release \ - -DUSE_AVX512=ON \ - .. +chmod +x ./run.sh +./run.sh ``` -### Step 3: Compile +The server starts at **[http://localhost:8080](http://localhost:8080)**. + +You can also pass options: ```bash -make -j$(nproc) +./run.sh ndd_data_dir=./my_data # use a custom data folder +./run.sh ndd_auth_token=your_token # enable token authentication — every client request must include this token ``` -### Running the Built Binary +> When `ndd_auth_token` is set, all requests your client makes to the server must include that token in the request header — otherwise the server will reject them. For how to pass the token in your client code, see [docs.endee.io/quick-start](https://docs.endee.io/quick-start). -After a successful build, the binary is generated in the `build/` directory. +--- -### Binary Naming +## Method 4: Manual CMake Build -The output binary name depends on the SIMD flag used during compilation: +Use this when you want full control over the build — useful if you are integrating Endee into an existing build pipeline or want to run tests. -- `ndd-avx2` -- `ndd-avx512` -- `ndd-neon` or `ndd-neon-darwin` on macOS -- `ndd-sve2` +### Prerequisites -A symlink named `ndd` points to the binary compiled for the current build. +- Linux or macOS +- Clang 19 or newer +- `cmake`, `build-essential`, `libssl-dev`, `libcurl4-openssl-dev` -### Runtime Environment Variables +Clone the repo: -Some environment variables **ndd** reads at runtime: +```bash +git clone https://github.com/endee-io/endee.git +cd endee +``` -- `NDD_DATA_DIR`: defines the data directory -- `NDD_AUTH_TOKEN`: optional authentication token +### Step 1: Create the build directory -### Authentication +Make sure you are inside the cloned repo folder (the one that contains `CMakeLists.txt`) before running these: -**Open mode** with no authentication is the default when `NDD_AUTH_TOKEN` is not set: +Then create and enter the build directory: ```bash -./build/ndd -curl http://{{BASE_URL}}/api/v1/index/list +mkdir build && cd build ``` -**Token mode** is enabled when `NDD_AUTH_TOKEN` is set: +### Step 2: Configure with CMake + +Pick your SIMD flag (same table as above) and run: ```bash -export NDD_AUTH_TOKEN=$(openssl rand -hex 32) -./build/ndd -curl -H "Authorization: $NDD_AUTH_TOKEN" http://{{BASE_URL}}/api/v1/index/list +# Release build for Intel/AMD +cmake -DCMAKE_BUILD_TYPE=Release -DUSE_AVX2=ON .. + +# Release build for Apple Silicon +cmake -DCMAKE_BUILD_TYPE=Release -DUSE_NEON=ON .. ``` -### Execution Example +**Optional debug flags you can add:** + +| Flag | What it does | +|---|---| +| `-DDEBUG=ON` | Adds full debug symbols, disables optimizations (`-g3 -O0`) | +| `-DND_DEBUG=ON` | Enables Endee's internal logging and timing output | -Run the database using the AVX2 binary and a local `data` folder: +Example with debug enabled: ```bash -mkdir -p ./data -export NDD_DATA_DIR=$(pwd)/data -./build/ndd +cmake -DUSE_NEON=ON -DND_DEBUG=ON .. ``` -Alternatively: +### Step 3: Compile ```bash -NDD_DATA_DIR=./data ./build/ndd +make -j$(nproc) ``` -## 3. Docker Deployment +`-j$(nproc)` tells make to use all available CPU cores in parallel, speeding up the build. + +### Step 4: Run the binary + +The compiled binary lands in the `build/` folder. Its name depends on the SIMD flag you used: -Endee ships with a Dockerfile for containerized deployment. +| SIMD flag | Binary name | +|---|---| +| `USE_AVX2` | `ndd-avx2` | +| `USE_AVX512` | `ndd-avx512` | +| `USE_NEON` on Linux ARM | `ndd-neon` | +| `USE_NEON` on Mac | `ndd-neon-darwin` | +| `USE_SVE2` | `ndd-sve2` | -### Build the Image +A symlink named `ndd` is also created pointing to whichever binary you built. -You must specify the target architecture using the `BUILD_ARCH` build argument. Valid targets are `avx2`, `avx512`, `neon`, and `sve2`. You can optionally enable a debug build using `DEBUG=true`. +After `make` finishes, go back to the repo root first: ```bash -docker build --ulimit nofile=100000:100000 --build-arg BUILD_ARCH=avx2 -t endee-oss:latest -f ./infra/Dockerfile . +cd .. ``` +Then run: + ```bash -docker build --ulimit nofile=100000:100000 --build-arg BUILD_ARCH=neon --build-arg DEBUG=true -t endee-oss:latest -f ./infra/Dockerfile . +mkdir -p ./data +NDD_DATA_DIR=./data ./build/ndd ``` -### Run the Container +**What these two lines do:** -The container exposes port `8080` and stores data in `/data` inside the container. Persist that data with a Docker volume. +`mkdir -p ./data` — creates a folder called `data` inside your repo directory. This is where Endee will store all its database and index files. The `-p` flag means "create it only if it doesn't already exist" — it won't fail if the folder is already there. -```bash -docker run \ - -p 8080:8080 \ - -v endee-data:/data \ - -e NDD_AUTH_TOKEN="your_secure_token" \ - --name endee-server \ - endee-oss:latest -``` +`NDD_DATA_DIR=./data ./build/ndd` — this is two things combined into one line: + +| Part | What it is | +|---|---| +| `NDD_DATA_DIR=./data` | An environment variable telling Endee where to store its data. `./data` means the `data/` folder in your current directory (the repo root). | +| `./build/ndd` | The compiled server binary. After `make` finishes, the binary lands inside the `build/` folder. `./build/ndd` is a shortcut (symlink) that points to whichever SIMD binary you just built. | -Leave `NDD_AUTH_TOKEN` empty or remove it to run Endee without authentication. -### Docker Compose +### Enable token authentication (optional) -You can also use Docker Compose: +By default the server runs with no password — anyone who can reach `localhost:8080` can use it. If you want to protect your server with a token, pass `NDD_AUTH_TOKEN` the same way: ```bash -docker-compose up +NDD_DATA_DIR=./data NDD_AUTH_TOKEN=your_token ./build/ndd ``` -## 4. Run from Docker Registry +> When `NDD_AUTH_TOKEN` is set, every request your client makes to the server must include that token in the `Authorization` header — otherwise the server will reject it. See [docs.endee.io/quick-start](https://docs.endee.io/quick-start) for how to pass the token in your client code. -You can run Endee directly using the prebuilt image from Docker Hub without building locally. +The server starts at **[http://localhost:8080](http://localhost:8080)**. -### Using Docker Compose +--- -Create a new directory: +### Check server health ```bash -mkdir endee && cd endee -``` - -Create a `docker-compose.yml` file with: - -```yaml -services: - endee: - image: endeeio/endee-server:latest - container_name: endee-server - ports: - - "8080:8080" - environment: - NDD_NUM_THREADS: 0 - NDD_AUTH_TOKEN: "" - volumes: - - endee-data:/data - restart: unless-stopped - -volumes: - endee-data: +curl http://localhost:8080/api/v1/health ``` -Then run: +### List all indexes +**Without authentication:** ```bash -docker compose up -d +curl http://localhost:8080/api/v1/index/list ``` -For more details, visit [docs.endee.io](https://docs.endee.io/quick-start). +**With a token:** +```bash +curl -H "Authorization: your_token" http://localhost:8080/api/v1/index/list +``` \ No newline at end of file From 28f66a68714f22e225f6df2bb9a918f204b87d07 Mon Sep 17 00:00:00 2001 From: Siddharth Senthilkumar Date: Tue, 17 Mar 2026 20:41:48 +0530 Subject: [PATCH 40/48] update project --- app/.env | 16 ++ app/.gitignore | 21 ++ app/README.md | 36 +++ app/backend/main.py | 258 +++++++++++++++++++++ app/backend/main.py.bak | 218 +++++++++++++++++ app/requirements.txt | 14 ++ examples/rag_agentic_demo/README.md | 49 ++++ examples/rag_agentic_demo/app.py | 156 +++++++++++++ examples/rag_agentic_demo/requirements.txt | 1 + test.ipynb | 80 +++++++ 10 files changed, 849 insertions(+) create mode 100644 app/.env create mode 100644 app/.gitignore create mode 100644 app/README.md create mode 100644 app/backend/main.py create mode 100644 app/backend/main.py.bak create mode 100644 app/requirements.txt create mode 100644 examples/rag_agentic_demo/README.md create mode 100644 examples/rag_agentic_demo/app.py create mode 100644 examples/rag_agentic_demo/requirements.txt create mode 100644 test.ipynb diff --git a/app/.env b/app/.env new file mode 100644 index 0000000000..4376506b1c --- /dev/null +++ b/app/.env @@ -0,0 +1,16 @@ +# API keys +GEMINI_API_KEY="AIzaSyAZu4NTKc_krZYZmMZRasiCQYtsSifOcrw" +GEMINI_MODEL="gemini-2.5-flash" + +# Vector DB +ENDEE_BASE_URL=http://localhost:8080/api/v1 +ENDEE_AUTH_TOKEN= +ENDEE_INDEX_NAME=rag_app + +# RAG params +CHUNK_SIZE=800 +CHUNK_OVERLAP=120 +TOP_K=6 + +# Frontend +BACKEND_URL=http://localhost:8000 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000000..4490213858 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,21 @@ +cat < .gitignore +# Python +__pycache__/ +*.pyc +*.pyo +*.pyd + +# Virtual environment +.venv/ +venv/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + +# Logs +*.log +EOL \ No newline at end of file diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000000..ae0930b413 --- /dev/null +++ b/app/README.md @@ -0,0 +1,36 @@ +# Agentic RAG App (FastAPI + Streamlit + Endee) + +## What it does +- Upload PDF/DOCX/TXT → chunk with LangChain → embed via `sentence-transformers/all-MiniLM-L6-v2` → store in Endee. +- Chat endpoint: embed query, search Endee (top_k), rerank with `cross-encoder/ms-marco-MiniLM-L-6-v2`, build context, answer with Gemini model (default `gemini-2.5-flash`). +- Toggle RAG on/off in the UI; responses include source metadata. + +## Layout +- `app/backend/main.py` – FastAPI service (`/upload`, `/chat`, `/health`). +- `app/frontend/streamlit_app.py` – Streamlit UI client. +- `app/requirements.txt` – all deps for both frontend + backend. +- `app/.env` – fill with your keys; defaults point to local Endee + backend. + +## Setup +```bash +cd app +python -m venv .venv +. .venv/Scripts/activate # or source .venv/bin/activate +pip install -r requirements.txt +``` +Edit `.env` with your `GEMINI_API_KEY` (and `ENDEE_AUTH_TOKEN` if your server requires it). + +## Run backend +```bash +uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload +``` + +## Run frontend +```bash +streamlit run frontend/streamlit_app.py +``` + +## Notes +- Endee index name is `rag_app` by default; change via `ENDEE_INDEX_NAME`. +- Embedding dim is fixed at 384 to match `all-MiniLM-L6-v2` and the Endee index is created automatically if missing. +- Reranker uses a cross-encoder; if you want faster responses, you can disable reranking by returning `results` directly in `retrieve`. diff --git a/app/backend/main.py b/app/backend/main.py new file mode 100644 index 0000000000..81b4781430 --- /dev/null +++ b/app/backend/main.py @@ -0,0 +1,258 @@ +#!/usr/bin/env python +import os +from typing import List, Optional + +from dotenv import load_dotenv +from fastapi import FastAPI, File, UploadFile, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from langchain_text_splitters import RecursiveCharacterTextSplitter +from langchain_google_genai import ChatGoogleGenerativeAI +from sentence_transformers import SentenceTransformer, CrossEncoder +from starlette.responses import JSONResponse + +from endee import Endee, Precision +from endee.schema import VectorItem as EndeeVectorItem + +# Monkey-patch Endee VectorItem to behave like a dict for `.get()` calls inside the SDK +if not hasattr(EndeeVectorItem, "get"): + EndeeVectorItem.get = lambda self, key, default=None: getattr(self, key, default) + +load_dotenv() + +# Environment +ENDEE_BASE_URL = os.getenv("ENDEE_BASE_URL", "http://localhost:8080/api/v1") +ENDEE_AUTH_TOKEN = os.getenv("ENDEE_AUTH_TOKEN", "") +ENDEE_INDEX_NAME = os.getenv("ENDEE_INDEX_NAME", "rag_app") + +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") +GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") + +CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "800")) +CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "120")) +TOP_K = int(os.getenv("TOP_K", "6")) + +app = FastAPI(title="RAG Agentic Backend", version="0.1.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +def get_endee_client() -> Endee: + client = Endee(ENDEE_AUTH_TOKEN) if ENDEE_AUTH_TOKEN else Endee() + client.set_base_url(ENDEE_BASE_URL) + return client + + +def ensure_index(client: Endee, dim: int) -> None: + try: + client.get_index(name=ENDEE_INDEX_NAME) + return + except Exception: + pass + client.create_index( + name=ENDEE_INDEX_NAME, + dimension=dim, + space_type="cosine", + precision=Precision.INT8, + ) + + +embedder: Optional[SentenceTransformer] = None +reranker: Optional[CrossEncoder] = None +llm: Optional[ChatGoogleGenerativeAI] = None +endee_client: Optional[Endee] = None +endee_index = None + + +def bootstrap(): + global embedder, reranker, llm, endee_client, endee_index + if GEMINI_API_KEY is None: + raise RuntimeError("GEMINI_API_KEY is required") + + embedder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") + reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") + llm = ChatGoogleGenerativeAI( + model=GEMINI_MODEL, + api_key=GEMINI_API_KEY, + temperature=0.2, + ) + endee_client = get_endee_client() + ensure_index(endee_client, embedder.get_sentence_embedding_dimension()) + endee_index = endee_client.get_index(name=ENDEE_INDEX_NAME) + + +bootstrap() + + +def read_file(file: UploadFile) -> str: + suffix = file.filename.split(".")[-1].lower() + content = file.file.read() + if suffix == "pdf": + try: + from pypdf import PdfReader + except Exception as exc: + raise HTTPException(status_code=500, detail=f"pypdf missing: {exc}") + file.file.seek(0) + reader = PdfReader(file.file) + text = "\n".join([p.extract_text() or "" for p in reader.pages]) + elif suffix in {"txt", "md"}: + text = content.decode("utf-8", errors="ignore") + elif suffix in {"docx"}: + try: + import docx2txt + except Exception as exc: + raise HTTPException(status_code=500, detail=f"docx2txt missing: {exc}") + temp_path = f"/tmp/{file.filename}" + with open(temp_path, "wb") as f: + f.write(content) + text = docx2txt.process(temp_path) or "" + os.remove(temp_path) + else: + raise HTTPException(status_code=400, detail="Unsupported file type") + return text + + +def chunk_text(text: str) -> List[str]: + splitter = RecursiveCharacterTextSplitter( + chunk_size=CHUNK_SIZE, + chunk_overlap=CHUNK_OVERLAP, + separators=["\n\n", "\n", " ", ""], + ) + return splitter.split_text(text) + + +def upsert_chunks(chunks: List[str], source: str): + embeddings = embedder.encode(chunks, convert_to_numpy=False) + payload = [] + for idx, (chunk, vector) in enumerate(zip(chunks, embeddings)): + payload.append( + { + "id": f"{source}::chunk-{idx}", + "vector": vector.tolist(), + "meta": {"source": source, "text": chunk}, + } + ) + endee_index.upsert(payload) + + +def _normalize_result(item) -> dict: + if hasattr(item, "dict"): + base = item.dict() + similarity = getattr(item, "similarity", None) + distance = getattr(item, "distance", None) + else: + base = dict(item) + similarity = base.get("similarity") + distance = base.get("distance") + return { + "id": base.get("id"), + "meta": base.get("meta") or {}, + "similarity": similarity, + "distance": distance, + "raw": item, + } + + +def retrieve(query: str): + query_vec = embedder.encode([query])[0].tolist() + results = endee_index.query(vector=query_vec, top_k=TOP_K, include_vectors=False) + if not results: + return [] + docs = [_normalize_result(r) for r in results] + rerank_inputs = [(query, doc["meta"].get("text", "")) for doc in docs] + scores = reranker.predict(rerank_inputs) + reranked = sorted( + [ + {**doc, "rerank_score": float(score)} + for doc, score in zip(docs, scores) + ], + key=lambda x: x["rerank_score"], + reverse=True, + ) + return reranked + + +def build_context(docs: List[dict]) -> str: + parts = [] + for doc in docs: + meta = doc.get("meta", {}) + parts.append(f"[{meta.get('source')}] {meta.get('text', '')}") + return "\n\n".join(parts) + + +def build_history(history: List[dict]) -> str: + lines = [] + for turn in history[-5:]: + u = turn.get("user") or "" + a = turn.get("answer") or "" + lines.append(f"User: {u}\nAssistant: {a}") + return "\n\n".join(lines) + + +def answer(question: str, context_docs: List[dict], history: List[dict]) -> dict: + if context_docs: + context_text = build_context(context_docs) + prompt = ( + "Use ONLY the provided context (and brief history if present). " + "If something is missing, say you don't have it.\n" + f"Context:\n{context_text}\n\n" + ) + else: + prompt = "Answer the user. If you don't know, say so.\n" + + history_text = build_history(history) + if history_text: + prompt += f"Recent history (for coherence, not new facts):\n{history_text}\n\n" + + prompt += f"Question: {question}" + + resp = llm.invoke(prompt) + return { + "answer": resp.content, + "sources": [ + { + "id": doc.get("id"), + "source": doc.get("meta", {}).get("source"), + "score": doc.get("rerank_score", doc.get("similarity")), + "preview": doc.get("meta", {}).get("text", "")[:200], + } + for doc in context_docs + ], + "mode": "rag" if context_docs else "direct", + } + + +@app.get("/health") +def health(): + return {"status": "ok"} + + +@app.post("/upload") +async def upload(file: UploadFile = File(...)): + try: + text = read_file(file) + chunks = chunk_text(text) + upsert_chunks(chunks, source=file.filename) + return {"message": f"Indexed {len(chunks)} chunks from {file.filename}"} + except HTTPException as e: + raise e + except Exception as exc: + raise HTTPException(status_code=500, detail=str(exc)) + + +@app.post("/chat") +async def chat(payload: dict): + question = payload.get("message") or "" + use_rag = payload.get("use_rag", True) + history = payload.get("history") or [] + if not question: + raise HTTPException(status_code=400, detail="message is required") + + docs = retrieve(question) if use_rag else [] + result = answer(question, docs, history) + return JSONResponse(result) diff --git a/app/backend/main.py.bak b/app/backend/main.py.bak new file mode 100644 index 0000000000..27f7df4fad --- /dev/null +++ b/app/backend/main.py.bak @@ -0,0 +1,218 @@ +#!/usr/bin/env python +import os +from typing import List, Optional + +from dotenv import load_dotenv +from fastapi import FastAPI, File, UploadFile, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from langchain_text_splitters import RecursiveCharacterTextSplitter +from langchain_google_genai import ChatGoogleGenerativeAI +from sentence_transformers import SentenceTransformer, CrossEncoder +from starlette.responses import JSONResponse +from langchain_text_splitters import RecursiveCharacterTextSplitter + +from endee import Endee, Precision + +load_dotenv() + +# Environment +ENDEE_BASE_URL = os.getenv("ENDEE_BASE_URL", "http://localhost:8080/api/v1") +ENDEE_AUTH_TOKEN = os.getenv("ENDEE_AUTH_TOKEN", "") +ENDEE_INDEX_NAME = os.getenv("ENDEE_INDEX_NAME", "rag_app") + +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") +GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") + +CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "800")) +CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "120")) +TOP_K = int(os.getenv("TOP_K", "6")) + +app = FastAPI(title="RAG Agentic Backend", version="0.1.0") + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +def get_endee_client() -> Endee: + client = Endee(ENDEE_AUTH_TOKEN) if ENDEE_AUTH_TOKEN else Endee() + client.set_base_url(ENDEE_BASE_URL) + return client + + +def ensure_index(client: Endee, dim: int) -> None: + try: + client.get_index(name=ENDEE_INDEX_NAME) + return + except Exception: + pass + client.create_index( + name=ENDEE_INDEX_NAME, + dimension=dim, + space_type="cosine", + precision=Precision.INT8, + ) + + +embedder: Optional[SentenceTransformer] = None +reranker: Optional[CrossEncoder] = None +llm: Optional[ChatGoogleGenerativeAI] = None +endee_client: Optional[Endee] = None +endee_index = None + + +def bootstrap(): + global embedder, reranker, llm, endee_client, endee_index + if GEMINI_API_KEY is None: + raise RuntimeError("GEMINI_API_KEY is required") + + embedder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") + reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") + llm = ChatGoogleGenerativeAI( + model=GEMINI_MODEL, + api_key=GEMINI_API_KEY, + temperature=0.2, + ) + endee_client = get_endee_client() + ensure_index(endee_client, embedder.get_sentence_embedding_dimension()) + endee_index = endee_client.get_index(name=ENDEE_INDEX_NAME) + + +bootstrap() + + +def read_file(file: UploadFile) -> str: + suffix = file.filename.split(".")[-1].lower() + content = file.file.read() + if suffix == "pdf": + try: + from pypdf import PdfReader + except Exception as exc: + raise HTTPException(status_code=500, detail=f"pypdf missing: {exc}") + file.file.seek(0) + reader = PdfReader(file.file) + text = "\n".join([p.extract_text() or "" for p in reader.pages]) + elif suffix in {"txt", "md"}: + text = content.decode("utf-8", errors="ignore") + elif suffix in {"docx"}: + try: + import docx2txt + except Exception as exc: + raise HTTPException(status_code=500, detail=f"docx2txt missing: {exc}") + temp_path = f"/tmp/{file.filename}" + with open(temp_path, "wb") as f: + f.write(content) + text = docx2txt.process(temp_path) or "" + os.remove(temp_path) + else: + raise HTTPException(status_code=400, detail="Unsupported file type") + return text + + +def chunk_text(text: str) -> List[str]: + splitter = RecursiveCharacterTextSplitter( + chunk_size=CHUNK_SIZE, + chunk_overlap=CHUNK_OVERLAP, + separators=["\n\n", "\n", " ", ""], + ) + return splitter.split_text(text) + + +def upsert_chunks(chunks: List[str], source: str): + embeddings = embedder.encode(chunks, convert_to_numpy=False) + payload = [] + for idx, (chunk, vector) in enumerate(zip(chunks, embeddings)): + payload.append( + { + "id": f"{source}::chunk-{idx}", + "vector": vector.tolist(), + "meta": {"source": source, "text": chunk}, + } + ) + endee_index.upsert(payload) + + +def retrieve(query: str): + query_vec = embedder.encode([query])[0].tolist() + results = endee_index.query(vector=query_vec, top_k=TOP_K, include_vectors=False) + if not results: + return [] + rerank_inputs = [(query, item.get("meta", {}).get("text", "")) for item in results] + scores = reranker.predict(rerank_inputs) + reranked = sorted( + [ + {**item, "rerank_score": float(score)} + for item, score in zip(results, scores) + ], + key=lambda x: x["rerank_score"], + reverse=True, + ) + return reranked + + +def build_context(docs: List[dict]) -> str: + parts = [] + for doc in docs: + meta = doc.get("meta", {}) + parts.append(f"[{meta.get('source')}] {meta.get('text', '')}") + return "\n\n".join(parts) + + +def answer(question: str, context_docs: List[dict]) -> dict: + if context_docs: + context_text = build_context(context_docs) + prompt = ( + "Use the context to answer. If information is missing, say you don't have it.\n" + f"Context:\n{context_text}\n\nQuestion: {question}" + ) + else: + prompt = question + resp = llm.invoke(prompt) + return { + "answer": resp.content, + "sources": [ + { + "id": doc["id"], + "source": doc.get("meta", {}).get("source"), + "score": doc.get("rerank_score", doc.get("similarity")), + "preview": doc.get("meta", {}).get("text", "")[:200], + } + for doc in context_docs + ], + "mode": "rag" if context_docs else "direct", + } + + +@app.get("/health") +def health(): + return {"status": "ok"} + + +@app.post("/upload") +async def upload(file: UploadFile = File(...)): + try: + text = read_file(file) + chunks = chunk_text(text) + upsert_chunks(chunks, source=file.filename) + return {"message": f"Indexed {len(chunks)} chunks from {file.filename}"} + except HTTPException as e: + raise e + print(e) + except Exception as exc: + raise HTTPException(status_code=500, detail=str(exc)) + + +@app.post("/chat") +async def chat(payload: dict): + question = payload.get("message") or "" + use_rag = payload.get("use_rag", True) + if not question: + raise HTTPException(status_code=400, detail="message is required") + + docs = retrieve(question) if use_rag else [] + result = answer(question, docs) + return JSONResponse(result) diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000000..8f5fcac588 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,14 @@ +fastapi>=0.110.0 +uvicorn[standard]>=0.27.1 +python-dotenv>=1.0.1 +langchain>=0.1.16 +langchain-community>=0.0.34 +langchain-google-genai>=0.0.12 +langchain-text-splitters>=0.0.1 +sentence-transformers>=2.5.1 +pypdf>=4.2.0 +docx2txt>=0.8 +endee>=0.1.0 +requests>=2.31.0 +streamlit>=1.32.0 +python-multipart>=0.0.9 diff --git a/examples/rag_agentic_demo/README.md b/examples/rag_agentic_demo/README.md new file mode 100644 index 0000000000..574b3d1574 --- /dev/null +++ b/examples/rag_agentic_demo/README.md @@ -0,0 +1,49 @@ +# RAG + Agentic Demo with Endee + +This example shows a minimal retrieval-augmented generation loop that uses the Endee vector database for retrieval and OpenAI for embeddings + answers. It includes a tiny agentic step: each user query is first rewritten by the LLM for better retrieval, then the rewritten query is embedded and searched in Endee. + +## Files +- `app.py` – main script (ingest sample docs, interactive Q&A loop) +- `requirements.txt` – Python dependencies +- `data/sample_docs/*.txt` – tiny corpus to load by default + +## Prerequisites +- Python 3.10+ +- Running Endee server at `http://localhost:8080` (default from `run.sh`) +- OpenAI API key (set `OPENAI_API_KEY`) + +## Setup +```bash +cd examples/rag_agentic_demo +python -m venv .venv +. .venv/Scripts/activate # Windows +# or: source .venv/bin/activate +pip install -r requirements.txt +``` + +## Environment +Create `.env` in this folder: +``` +OPENAI_API_KEY=sk-... +ENDEE_BASE_URL=http://localhost:8080/api/v1 # optional +ENDEE_AUTH_TOKEN= # optional if you enabled auth +``` + +## Run +```bash +python app.py --index rag_demo +``` +- On first run, the script ingests the sample docs. +- Ask questions in the prompt; type `exit` to quit. +- Use `--skip-ingest` if the index already has your data. + +## Using your own data +Place text files under `data/sample_docs` (or modify `SAMPLE_DIR` in `app.py`). The script will embed and upsert them on startup. + +## How it works (flow) +1) **Rewrite** the user question via `rewrite_query` (agentic query optimization). +2) **Embed** rewritten query with `text-embedding-3-small`. +3) **Search** Endee (`top_k=4`). +4) **Grounded answer** with `gpt-4o-mini`, constrained to retrieved context. + +You can swap models, add filters, or extend the agent step to call other tools. diff --git a/examples/rag_agentic_demo/app.py b/examples/rag_agentic_demo/app.py new file mode 100644 index 0000000000..2c56030dd0 --- /dev/null +++ b/examples/rag_agentic_demo/app.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +import argparse +import glob +import os +from pathlib import Path +from typing import List, Tuple + +from dotenv import load_dotenv +from openai import OpenAI +from endee import Endee, Precision + +load_dotenv() + +DEFAULT_INDEX = "rag_demo" +EMBED_MODEL = "text-embedding-3-small" +EMBED_DIM = 1536 # dimension for text-embedding-3-small +SAMPLE_DIR = Path(__file__).parent / "data" / "sample_docs" + + +def get_openai_client() -> OpenAI: + api_key = os.getenv("OPENAI_API_KEY") + if not api_key: + raise RuntimeError("OPENAI_API_KEY is required for embeddings and generation") + return OpenAI(api_key=api_key) + + +def get_endee_client() -> Endee: + auth = os.getenv("ENDEE_AUTH_TOKEN", "") + base_url = os.getenv("ENDEE_BASE_URL", "http://localhost:8080/api/v1") + client = Endee(auth) if auth else Endee() + client.set_base_url(base_url) + return client + + +def ensure_index(client: Endee, name: str, dim: int) -> None: + try: + client.get_index(name=name) + return + except Exception: + pass + client.create_index(name=name, dimension=dim, space_type="cosine", precision=Precision.INT8) + + +def load_corpus() -> List[Tuple[str, str]]: + docs = [] + for path in glob.glob(str(SAMPLE_DIR / "*.txt")): + with open(path, "r", encoding="utf-8") as f: + text = f.read().strip() + docs.append((Path(path).stem, text)) + if not docs: + raise RuntimeError(f"No documents found in {SAMPLE_DIR}") + return docs + + +def embed_texts(client: OpenAI, texts: List[str]) -> List[List[float]]: + resp = client.embeddings.create(model=EMBED_MODEL, input=texts) + return [item.embedding for item in resp.data] + + +def upsert_docs(index, ids: List[str], texts: List[str], embeddings: List[List[float]]): + payload = [] + for doc_id, text, emb in zip(ids, texts, embeddings): + payload.append({ + "id": doc_id, + "vector": emb, + "meta": {"source": doc_id, "preview": text[:200]} + }) + index.upsert(payload) + + +def rewrite_query(llm: OpenAI, query: str) -> str: + prompt = ( + "Rewrite the user query to be retrieval-friendly, short, and factual. " + "Preserve intent and key nouns; drop pronouns and chit-chat." + ) + res = llm.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": prompt}, + {"role": "user", "content": query}, + ], + max_tokens=50, + ) + return res.choices[0].message.content.strip() + + +def search(index, embedding: List[float], k: int): + return index.query(vector=embedding, top_k=k, include_vectors=False) + + +def build_context(results) -> str: + chunks = [] + for i, item in enumerate(results, 1): + meta = item.get("meta") or {} + preview = meta.get("preview") or "" + chunks.append(f"[{i}] id={item['id']} score={item.get('similarity', item.get('distance'))}\n{preview}\n") + return "\n".join(chunks) + + +def answer_question(llm: OpenAI, question: str, context: str) -> str: + system = ( + "You are a concise assistant. Answer using only the provided context. " + "If the answer is not in the context, say you cannot find it." + ) + res = llm.chat.completions.create( + model="gpt-4o-mini", + messages=[ + {"role": "system", "content": system}, + {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"}, + ], + max_tokens=200, + temperature=0.2, + ) + return res.choices[0].message.content.strip() + + +def interactive_loop(index, llm: OpenAI): + print("Type your question (or 'exit' to quit)") + while True: + try: + q = input("ask> ").strip() + except (EOFError, KeyboardInterrupt): + break + if not q or q.lower() in {"exit", "quit"}: + break + rewritten = rewrite_query(llm, q) + embedding = embed_texts(llm, [rewritten])[0] + results = search(index, embedding, k=4) + context = build_context(results) + answer = answer_question(llm, q, context) + print("\n---\n", answer, "\n---\n", sep="") + + +def main(): + parser = argparse.ArgumentParser(description="RAG + agentic demo with Endee") + parser.add_argument("--index", default=DEFAULT_INDEX, help="Index name to use/create") + parser.add_argument("--skip-ingest", action="store_true", help="Skip loading sample corpus") + args = parser.parse_args() + + llm = get_openai_client() + endee_client = get_endee_client() + ensure_index(endee_client, args.index, EMBED_DIM) + index = endee_client.get_index(name=args.index) + + if not args.skip_ingest: + docs = load_corpus() + ids, texts = zip(*docs) + embeddings = embed_texts(llm, list(texts)) + upsert_docs(index, list(ids), list(texts), embeddings) + print(f"Ingested {len(ids)} docs into index '{args.index}'") + + interactive_loop(index, llm) + + +if __name__ == "__main__": + main() diff --git a/examples/rag_agentic_demo/requirements.txt b/examples/rag_agentic_demo/requirements.txt new file mode 100644 index 0000000000..7b70ca6fd7 --- /dev/null +++ b/examples/rag_agentic_demo/requirements.txt @@ -0,0 +1 @@ +openai>=1.12.0\npython-dotenv>=1.0.1\nendee>=0.1.0\n diff --git a/test.ipynb b/test.ipynb new file mode 100644 index 0000000000..6b0ecb53de --- /dev/null +++ b/test.ipynb @@ -0,0 +1,80 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 7, + "id": "6a9d3cb5", + "metadata": {}, + "outputs": [], + "source": [ + "from endee import Endee, Precision\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "40aa2059", + "metadata": {}, + "outputs": [], + "source": [ + "client = Endee()" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "01a51046", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'Index created successfully'" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "client.create_index(\n", + " name=\"my_index2\",\n", + " dimension=384,\n", + " space_type=\"cosine\",\n", + " precision=Precision.INT8\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "225da938", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 2da2d6afacc3c43ec30eddabffbdb1f66a73d497 Mon Sep 17 00:00:00 2001 From: CodexSandboxOffline Date: Tue, 17 Mar 2026 23:41:07 +0530 Subject: [PATCH 41/48] Add agentic app updates, sample env, and ignores --- app/{.env => .env.example} | 13 ++-- app/.gitignore | 23 ++---- app/README.md | 139 +++++++++++++++++++++++++++++++------ app/backend/main.py | 80 ++++++++++++++++++--- app/requirements.txt | 1 + 5 files changed, 195 insertions(+), 61 deletions(-) rename app/{.env => .env.example} (51%) diff --git a/app/.env b/app/.env.example similarity index 51% rename from app/.env rename to app/.env.example index 4376506b1c..9a8970874e 100644 --- a/app/.env +++ b/app/.env.example @@ -1,16 +1,11 @@ -# API keys -GEMINI_API_KEY="AIzaSyAZu4NTKc_krZYZmMZRasiCQYtsSifOcrw" -GEMINI_MODEL="gemini-2.5-flash" - -# Vector DB +# Sample environment (fill with your keys before running) +GEMINI_API_KEY=your_gemini_key +GEMINI_MODEL=gemini-2.5-flash ENDEE_BASE_URL=http://localhost:8080/api/v1 ENDEE_AUTH_TOKEN= ENDEE_INDEX_NAME=rag_app - -# RAG params CHUNK_SIZE=800 CHUNK_OVERLAP=120 TOP_K=6 - -# Frontend +TAVILY_API_KEY=your_tavily_key BACKEND_URL=http://localhost:8000 diff --git a/app/.gitignore b/app/.gitignore index 4490213858..abe52e10c9 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1,21 +1,8 @@ -cat < .gitignore -# Python +.env +.env.local +!.env.example +.venv/ __pycache__/ *.pyc -*.pyo -*.pyd - -# Virtual environment -.venv/ -venv/ - -# IDE -.vscode/ -.idea/ - -# OS -.DS_Store - -# Logs *.log -EOL \ No newline at end of file +.cache/ diff --git a/app/README.md b/app/README.md index ae0930b413..978f612eec 100644 --- a/app/README.md +++ b/app/README.md @@ -1,36 +1,129 @@ -# Agentic RAG App (FastAPI + Streamlit + Endee) +# Agentic RAG Starter (FastAPI + Streamlit + Endee + Gemini + Tavily) -## What it does -- Upload PDF/DOCX/TXT → chunk with LangChain → embed via `sentence-transformers/all-MiniLM-L6-v2` → store in Endee. -- Chat endpoint: embed query, search Endee (top_k), rerank with `cross-encoder/ms-marco-MiniLM-L-6-v2`, build context, answer with Gemini model (default `gemini-2.5-flash`). -- Toggle RAG on/off in the UI; responses include source metadata. +This app lets you chat with three routing paths: +- **Agent** (auto-route): LLM decides whether to use RAG (Endee), Web (Tavily), or Direct (LLM only). +- **RAG**: embed → Endee search → rerank → Gemini answer with sources. +- **Web**: Tavily search → Gemini answer with web citations. +- **Direct**: Gemini only (no retrieval). -## Layout -- `app/backend/main.py` – FastAPI service (`/upload`, `/chat`, `/health`). -- `app/frontend/streamlit_app.py` – Streamlit UI client. -- `app/requirements.txt` – all deps for both frontend + backend. -- `app/.env` – fill with your keys; defaults point to local Endee + backend. +## Tech Stack +- Backend: FastAPI (`app/backend/main.py`) +- Frontend: Streamlit (`app/frontend/streamlit_app.py`) +- Vector DB: Endee server at `http://localhost:8080` (cosine, INT8, dim 384) +- Embeddings: `sentence-transformers/all-MiniLM-L6-v2` +- Reranker: `cross-encoder/ms-marco-MiniLM-L-6-v2` +- LLM: Gemini (default `gemini-2.5-flash` via Google AI key) +- Web search: Tavily API + +## Prerequisites +- Python 3.10+ +- Endee server running locally on 8080 (from `run.sh` or docker-compose) +- API keys: + - `GEMINI_API_KEY` (required) + - `TAVILY_API_KEY` (for Web route) ## Setup -```bash -cd app +```powershell +cd C:\VH811\projects\endee\app python -m venv .venv -. .venv/Scripts/activate # or source .venv/bin/activate +. .venv\Scripts\activate pip install -r requirements.txt ``` -Edit `.env` with your `GEMINI_API_KEY` (and `ENDEE_AUTH_TOKEN` if your server requires it). -## Run backend -```bash -uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload +### Environment +Edit `app/.env` (already created) and set: +``` +GEMINI_API_KEY=your_gemini_key +GEMINI_MODEL=gemini-2.5-flash +ENDEE_BASE_URL=http://localhost:8080/api/v1 +ENDEE_AUTH_TOKEN= # blank if Endee auth disabled +ENDEE_INDEX_NAME=rag_app +CHUNK_SIZE=800 +CHUNK_OVERLAP=120 +TOP_K=6 +TAVILY_API_KEY=your_tavily_key +BACKEND_URL=http://localhost:8000 ``` -## Run frontend -```bash -streamlit run frontend/streamlit_app.py +## Run +Backend: +```powershell +cd C:\VH811\projects\endee\app +. .venv\Scripts\activate +python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000 +``` +Frontend: +```powershell +cd C:\VH811\projects\endee\app +. .venv\Scripts\activate +streamlit run frontend\streamlit_app.py ``` +Open Streamlit at http://localhost:8501. + +## How it Works (Pipeline) +1) **Uploads**: pdf/docx/txt/md → text extract (pypdf/docx2txt) → chunk (800/120) → embed (MiniLM) → upsert to Endee with metadata. +2) **Routing** (Agent): quick Gemini prompt decides `rag|web|direct` unless user forces mode via UI radio. +3) **RAG path**: embed query → Endee top_k=6 → rerank top 4 with cross-encoder → build context → Gemini answers; returns sources (chunk previews). +4) **Web path**: Tavily search max 5 results → context → Gemini answers; returns web URLs/titles. +5) **Direct path**: Gemini answers without retrieval. +6) **History**: last 5 turns sent to LLM for coherence (not for facts). + +## Routing Modes in UI +- Agent (auto): LLM router picks best path. +- RAG: force vector search. +- Web: force Tavily search. +- Direct: force plain LLM. + +## Tuning & Performance +- Lower `TOP_K` or rerank cutoff to reduce latency (currently rerank keeps top 4). +- Smaller reranker model (e.g., `cross-encoder/ms-marco-MiniLM-L-2-v2`) for speed. +- Use Direct mode for general chit-chat to avoid retrieval. +- Warm cache by one request after restart to load HF models. + +## Troubleshooting +- `API key not valid / 403 / 429`: check/replace `GEMINI_API_KEY`, quota or billing; switch model if needed. +- `TAVILY_API_KEY not configured`: set the key in `.env` for Web mode. +- Upload errors: ensure file type is pdf/docx/txt/md and Endee server is reachable at `ENDEE_BASE_URL`. + +## File Map +- `app/backend/main.py` — API routes, router, RAG/Web/Direct flows +- `app/frontend/streamlit_app.py` — UI and routing selector +- `app/requirements.txt` — dependencies +- `app/.env` — keys and settings ## Notes -- Endee index name is `rag_app` by default; change via `ENDEE_INDEX_NAME`. -- Embedding dim is fixed at 384 to match `all-MiniLM-L6-v2` and the Endee index is created automatically if missing. -- Reranker uses a cross-encoder; if you want faster responses, you can disable reranking by returning `results` directly in `retrieve`. +- Index auto-creates with dimension 384 to match MiniLM embeddings. +- Sources shown only for RAG/Web modes; direct replies have no citations. +- Last 5 message pairs are passed for conversational continuity. + +## Architecture Diagram (Text) +``` +User (Streamlit UI) + | + v +Routing Selector (Agent/RAG/Web/Direct) + | + +-- Agent -> Router Prompt (Gemini) -> choose path + | | + | +-- RAG Path: + | | embed query (MiniLM) + | | -> Endee search (cosine, INT8, top_k) + | | -> rerank (cross-encoder) + | | -> context -> Gemini answer + chunk sources + | | + | +-- Web Path: + | | Tavily search (max 5) + | | -> context -> Gemini answer + web sources + | | + | +-- Direct Path: + | Gemini answer (no retrieval) + | +Uploads + | + v +File ingest (pdf/docx/txt/md) + -> extract text + -> chunk (800/120) + -> embed (MiniLM) + -> Endee upsert (metadata) +``` diff --git a/app/backend/main.py b/app/backend/main.py index 81b4781430..99d753ccc2 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python import os from typing import List, Optional @@ -9,6 +9,7 @@ from langchain_google_genai import ChatGoogleGenerativeAI from sentence_transformers import SentenceTransformer, CrossEncoder from starlette.responses import JSONResponse +from tavily import TavilyClient from endee import Endee, Precision from endee.schema import VectorItem as EndeeVectorItem @@ -31,7 +32,9 @@ CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "120")) TOP_K = int(os.getenv("TOP_K", "6")) -app = FastAPI(title="RAG Agentic Backend", version="0.1.0") +TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") + +app = FastAPI(title="RAG Agentic Backend", version="0.2.0") app.add_middleware( CORSMiddleware, @@ -67,10 +70,11 @@ def ensure_index(client: Endee, dim: int) -> None: llm: Optional[ChatGoogleGenerativeAI] = None endee_client: Optional[Endee] = None endee_index = None +tavily_client: Optional[TavilyClient] = None def bootstrap(): - global embedder, reranker, llm, endee_client, endee_index + global embedder, reranker, llm, endee_client, endee_index, tavily_client if GEMINI_API_KEY is None: raise RuntimeError("GEMINI_API_KEY is required") @@ -85,6 +89,9 @@ def bootstrap(): ensure_index(endee_client, embedder.get_sentence_embedding_dimension()) endee_index = endee_client.get_index(name=ENDEE_INDEX_NAME) + if TAVILY_API_KEY: + tavily_client = TavilyClient(api_key=TAVILY_API_KEY) + bootstrap() @@ -174,14 +181,35 @@ def retrieve(query: str): key=lambda x: x["rerank_score"], reverse=True, ) - return reranked + return reranked[:4] + + +def web_search(query: str): + if not tavily_client: + raise HTTPException(status_code=500, detail="TAVILY_API_KEY not configured") + res = tavily_client.search(query=query, max_results=5, include_images=False) + docs = [] + for item in res.get("results", []): + docs.append( + { + "id": item.get("url"), + "meta": { + "source": item.get("url"), + "text": item.get("content", ""), + "title": item.get("title", "") + }, + "score": item.get("score"), + } + ) + return docs def build_context(docs: List[dict]) -> str: parts = [] for doc in docs: meta = doc.get("meta", {}) - parts.append(f"[{meta.get('source')}] {meta.get('text', '')}") + src = meta.get("title") or meta.get("source") + parts.append(f"[{src}] {meta.get('text', '')}") return "\n\n".join(parts) @@ -194,7 +222,28 @@ def build_history(history: List[dict]) -> str: return "\n\n".join(lines) -def answer(question: str, context_docs: List[dict], history: List[dict]) -> dict: +def route_query(question: str, force_mode: str = "auto") -> str: + if force_mode in {"rag", "web", "direct"}: + return force_mode + # lightweight classification with the main LLM + prompt = ( + "Decide routing for the question. Output only one token: RAG, WEB, or DIRECT.\n" + "Use RAG if it likely needs internal docs; WEB if it's about current/general external info; otherwise DIRECT.\n" + f"Question: {question}\nAnswer:" + ) + try: + resp = llm.invoke(prompt) + text = resp.content.strip().upper() + if "WEB" in text: + return "web" + if "RAG" in text: + return "rag" + return "direct" + except Exception: + return "direct" + + +def answer(question: str, context_docs: List[dict], history: List[dict], mode: str) -> dict: if context_docs: context_text = build_context(context_docs) prompt = ( @@ -218,12 +267,13 @@ def answer(question: str, context_docs: List[dict], history: List[dict]) -> dict { "id": doc.get("id"), "source": doc.get("meta", {}).get("source"), - "score": doc.get("rerank_score", doc.get("similarity")), + "title": doc.get("meta", {}).get("title"), + "score": doc.get("rerank_score", doc.get("similarity", doc.get("score"))), "preview": doc.get("meta", {}).get("text", "")[:200], } for doc in context_docs ], - "mode": "rag" if context_docs else "direct", + "mode": mode, } @@ -248,11 +298,19 @@ async def upload(file: UploadFile = File(...)): @app.post("/chat") async def chat(payload: dict): question = payload.get("message") or "" - use_rag = payload.get("use_rag", True) + force_mode = (payload.get("mode") or "auto").lower() history = payload.get("history") or [] if not question: raise HTTPException(status_code=400, detail="message is required") - docs = retrieve(question) if use_rag else [] - result = answer(question, docs, history) + route = route_query(question, force_mode=force_mode) + + if route == "rag": + docs = retrieve(question) + elif route == "web": + docs = web_search(question) + else: + docs = [] + + result = answer(question, docs, history, mode=route) return JSONResponse(result) diff --git a/app/requirements.txt b/app/requirements.txt index 8f5fcac588..4b53dec826 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -12,3 +12,4 @@ endee>=0.1.0 requests>=2.31.0 streamlit>=1.32.0 python-multipart>=0.0.9 +tavily-python>=0.3.8 From 89eee98f67a0a0b8852c44ef619b77e966680183 Mon Sep 17 00:00:00 2001 From: Siddharth-cvhs Date: Wed, 18 Mar 2026 08:23:51 +0530 Subject: [PATCH 42/48] Add React frontend option for agentic RAG --- app/.gitignore | 2 + app/README.md | 10 +++ app/frontend-react/index.html | 12 +++ app/frontend-react/package.json | 22 +++++ app/frontend-react/src/App.tsx | 59 +++++++++++++ app/frontend-react/src/api.ts | 31 +++++++ app/frontend-react/src/components/Chat.tsx | 84 +++++++++++++++++++ .../src/components/Uploader.tsx | 33 ++++++++ app/frontend-react/src/main.tsx | 10 +++ app/frontend-react/src/styles.css | 51 +++++++++++ app/frontend-react/src/types.ts | 16 ++++ app/frontend-react/tsconfig.json | 18 ++++ app/frontend-react/tsconfig.node.json | 9 ++ app/frontend-react/vite.config.ts | 9 ++ 14 files changed, 366 insertions(+) create mode 100644 app/frontend-react/index.html create mode 100644 app/frontend-react/package.json create mode 100644 app/frontend-react/src/App.tsx create mode 100644 app/frontend-react/src/api.ts create mode 100644 app/frontend-react/src/components/Chat.tsx create mode 100644 app/frontend-react/src/components/Uploader.tsx create mode 100644 app/frontend-react/src/main.tsx create mode 100644 app/frontend-react/src/styles.css create mode 100644 app/frontend-react/src/types.ts create mode 100644 app/frontend-react/tsconfig.json create mode 100644 app/frontend-react/tsconfig.node.json create mode 100644 app/frontend-react/vite.config.ts diff --git a/app/.gitignore b/app/.gitignore index abe52e10c9..b1c4268dd6 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -6,3 +6,5 @@ __pycache__/ *.pyc *.log .cache/ +frontend-react/node_modules/ +frontend-react/dist/ diff --git a/app/README.md b/app/README.md index 978f612eec..ede288fba1 100644 --- a/app/README.md +++ b/app/README.md @@ -127,3 +127,13 @@ File ingest (pdf/docx/txt/md) -> embed (MiniLM) -> Endee upsert (metadata) ``` + +## React Frontend (optional, smoother UI) +A Vite/React client lives in `app/frontend-react`. +Run it (after filling `.env` and starting backend): +```bash +cd app/frontend-react +npm install +npm run dev # starts on http://localhost:5173 +``` +Configure backend URL via env: `VITE_BACKEND_URL=http://localhost:8000` (create a `.env` in this folder if needed). diff --git a/app/frontend-react/index.html b/app/frontend-react/index.html new file mode 100644 index 0000000000..ad88156359 --- /dev/null +++ b/app/frontend-react/index.html @@ -0,0 +1,12 @@ + + + + + + Agentic RAG UI + + +
+ + + diff --git a/app/frontend-react/package.json b/app/frontend-react/package.json new file mode 100644 index 0000000000..419535c378 --- /dev/null +++ b/app/frontend-react/package.json @@ -0,0 +1,22 @@ +{ + "name": "agentic-rag-ui", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "axios": "^1.6.7", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.18", + "@vitejs/plugin-react": "^4.2.0", + "typescript": "^5.3.3", + "vite": "^5.0.8" + } +} diff --git a/app/frontend-react/src/App.tsx b/app/frontend-react/src/App.tsx new file mode 100644 index 0000000000..9189ee92c0 --- /dev/null +++ b/app/frontend-react/src/App.tsx @@ -0,0 +1,59 @@ +import { useMemo, useState } from "react"; +import Chat from "./components/Chat"; +import Uploader from "./components/Uploader"; +import { ChatMessage, Mode } from "../src/types"; + +function App() { + const [messages, setMessages] = useState([]); + const [loading, setLoading] = useState(false); + const [mode, setMode] = useState("auto"); + + const history = useMemo( + () => messages.slice(-5).map((m) => ({ user: m.user, answer: m.answer })), + [messages] + ); + + return ( +
+

Agentic RAG Console

+

+ RAG + Web + Direct router on Endee + Gemini + Tavily +

+ +
+
+ Mode + {(["auto", "rag", "web", "direct"] as Mode[]).map((m) => ( + + ))} +
+ +
+ +
+ +
+
+ ); +} + +export default App; diff --git a/app/frontend-react/src/api.ts b/app/frontend-react/src/api.ts new file mode 100644 index 0000000000..c52d833b7e --- /dev/null +++ b/app/frontend-react/src/api.ts @@ -0,0 +1,31 @@ +import axios from "axios"; +import { Mode, Source } from "./types"; + +const BASE_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:8000"; + +export interface ChatPayload { + message: string; + mode: Mode; + history: { user: string; answer: string }[]; +} + +export interface ChatResponse { + answer: string; + sources: Source[]; + mode: string; +} + +export async function sendChat(payload: ChatPayload): Promise { + const { data } = await axios.post(`${BASE_URL}/chat`, payload, { timeout: 60000 }); + return data as ChatResponse; +} + +export async function uploadFile(file: File): Promise { + const formData = new FormData(); + formData.append("file", file); + const { data } = await axios.post(`${BASE_URL}/upload`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + timeout: 120000, + }); + return data.message as string; +} diff --git a/app/frontend-react/src/components/Chat.tsx b/app/frontend-react/src/components/Chat.tsx new file mode 100644 index 0000000000..12a7a503b0 --- /dev/null +++ b/app/frontend-react/src/components/Chat.tsx @@ -0,0 +1,84 @@ +import { useState } from "react"; +import { sendChat } from "../api"; +import { ChatMessage, Mode } from "../types"; + +interface Props { + mode: Mode; + history: { user: string; answer: string }[]; + messages: ChatMessage[]; + setMessages: (m: ChatMessage[]) => void; + loading: boolean; + setLoading: (v: boolean) => void; +} + +const Chat = ({ mode, history, messages, setMessages, loading, setLoading }: Props) => { + const [input, setInput] = useState(""); + + const send = async () => { + if (!input.trim()) return; + setLoading(true); + try { + const resp = await sendChat({ message: input, mode, history }); + setMessages([ + ...messages, + { + user: input, + answer: resp.answer, + sources: resp.sources, + mode: resp.mode, + }, + ]); + setInput(""); + } catch (err: any) { + const msg = err?.response?.data?.detail || err.message || "Request failed"; + setMessages([ + ...messages, + { user: input, answer: `Error: ${msg}`, mode: "error" }, + ]); + } finally { + setLoading(false); + } + }; + + return ( +
+
+ setInput(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && send()} + disabled={loading} + /> + +
+ +
+ {messages.slice().reverse().map((m, idx) => ( +
+
You: {m.user}
+
{(m.mode || "reply").toUpperCase()}: {m.answer}
+ {m.sources && m.sources.length > 0 && ( +
+ Sources: +
    + {m.sources.map((s, i) => ( +
  • + {s.title || s.source || s.id} {s.score ? `(score ${s.score})` : ""} + {s.preview ? ` — ${s.preview}` : ""} +
  • + ))} +
+
+ )} +
+ ))} +
+
+ ); +}; + +export default Chat; diff --git a/app/frontend-react/src/components/Uploader.tsx b/app/frontend-react/src/components/Uploader.tsx new file mode 100644 index 0000000000..238a79b5e4 --- /dev/null +++ b/app/frontend-react/src/components/Uploader.tsx @@ -0,0 +1,33 @@ +import { useState } from "react"; +import { uploadFile } from "../api"; + +interface Props { + disabled: boolean; +} + +const Uploader = ({ disabled }: Props) => { + const [status, setStatus] = useState(""); + + const onChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setStatus("Uploading..."); + try { + const msg = await uploadFile(file); + setStatus(msg); + } catch (err: any) { + const msg = err?.response?.data?.detail || err.message || "Upload failed"; + setStatus(msg); + } + }; + + return ( +
+ + + {status &&
{status}
} +
+ ); +}; + +export default Uploader; diff --git a/app/frontend-react/src/main.tsx b/app/frontend-react/src/main.tsx new file mode 100644 index 0000000000..7590916fa3 --- /dev/null +++ b/app/frontend-react/src/main.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./styles.css"; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + + + +); diff --git a/app/frontend-react/src/styles.css b/app/frontend-react/src/styles.css new file mode 100644 index 0000000000..9e105503d2 --- /dev/null +++ b/app/frontend-react/src/styles.css @@ -0,0 +1,51 @@ +:root { + font-family: "Inter", system-ui, -apple-system, sans-serif; + color: #0f172a; + background-color: #f8fafc; +} +body { + margin: 0; +} +.container { + max-width: 1200px; + margin: 0 auto; + padding: 24px; +} +.panel { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + padding: 16px; + box-shadow: 0 6px 24px rgba(15, 23, 42, 0.08); +} +.btn { + background: linear-gradient(90deg, #3b82f6, #6366f1); + color: #fff; + border: none; + border-radius: 10px; + padding: 10px 16px; + cursor: pointer; + font-weight: 600; +} +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} +.badge { + display: inline-block; + padding: 4px 8px; + border-radius: 999px; + font-size: 12px; + background: #e0f2fe; + color: #0369a1; + margin-right: 6px; +} +.message { + margin-bottom: 12px; + padding: 12px; + border-radius: 10px; +} +.message.user { background: #eff6ff; } +.message.bot { background: #f1f5f9; } +.sources { font-size: 12px; color: #475569; } +.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } diff --git a/app/frontend-react/src/types.ts b/app/frontend-react/src/types.ts new file mode 100644 index 0000000000..b82e51ec45 --- /dev/null +++ b/app/frontend-react/src/types.ts @@ -0,0 +1,16 @@ +export type Mode = "auto" | "rag" | "web" | "direct"; + +export interface Source { + id?: string | null; + source?: string | null; + title?: string | null; + preview?: string | null; + score?: number | null; +} + +export interface ChatMessage { + user: string; + answer: string; + sources?: Source[]; + mode?: string; +} diff --git a/app/frontend-react/tsconfig.json b/app/frontend-react/tsconfig.json new file mode 100644 index 0000000000..b2aeeb051a --- /dev/null +++ b/app/frontend-react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/app/frontend-react/tsconfig.node.json b/app/frontend-react/tsconfig.node.json new file mode 100644 index 0000000000..339f928610 --- /dev/null +++ b/app/frontend-react/tsconfig.node.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "Bundler" + }, + "include": ["vite.config.ts"] +} diff --git a/app/frontend-react/vite.config.ts b/app/frontend-react/vite.config.ts new file mode 100644 index 0000000000..80a7f66514 --- /dev/null +++ b/app/frontend-react/vite.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 5173, + }, +}); From 9f94b6c7fc9c5a22d2440a573dff25a8cce179d2 Mon Sep 17 00:00:00 2001 From: Siddharth-cvhs Date: Wed, 18 Mar 2026 19:30:26 +0530 Subject: [PATCH 43/48] Add lov_frontend UI --- app/lov_frontend | 1 + 1 file changed, 1 insertion(+) create mode 160000 app/lov_frontend diff --git a/app/lov_frontend b/app/lov_frontend new file mode 160000 index 0000000000..0723c99655 --- /dev/null +++ b/app/lov_frontend @@ -0,0 +1 @@ +Subproject commit 0723c99655e33d26c2c7512fae6f6e7da3ee93fe From b3598e2110f08fce86d6603129777ac570945669 Mon Sep 17 00:00:00 2001 From: Siddharth-cvhs Date: Thu, 7 May 2026 15:54:48 +0530 Subject: [PATCH 44/48] New changes 07/05 --- app/Dockerfile | 51 +++ app/README.md | 195 ++++------ app/backend/main.py | 349 +++++++++++------- app/frontend-react/index.html | 12 - app/frontend-react/package.json | 22 -- app/frontend-react/src/App.tsx | 59 --- app/frontend-react/src/api.ts | 31 -- app/frontend-react/src/components/Chat.tsx | 84 ----- .../src/components/Uploader.tsx | 33 -- app/frontend-react/src/main.tsx | 10 - app/frontend-react/src/styles.css | 51 --- app/frontend-react/src/types.ts | 16 - app/frontend-react/tsconfig.json | 18 - app/frontend-react/tsconfig.node.json | 9 - app/frontend-react/vite.config.ts | 9 - app/requirements.txt | 1 + docker-compose.yml | 65 +++- 17 files changed, 405 insertions(+), 610 deletions(-) create mode 100644 app/Dockerfile delete mode 100644 app/frontend-react/index.html delete mode 100644 app/frontend-react/package.json delete mode 100644 app/frontend-react/src/App.tsx delete mode 100644 app/frontend-react/src/api.ts delete mode 100644 app/frontend-react/src/components/Chat.tsx delete mode 100644 app/frontend-react/src/components/Uploader.tsx delete mode 100644 app/frontend-react/src/main.tsx delete mode 100644 app/frontend-react/src/styles.css delete mode 100644 app/frontend-react/src/types.ts delete mode 100644 app/frontend-react/tsconfig.json delete mode 100644 app/frontend-react/tsconfig.node.json delete mode 100644 app/frontend-react/vite.config.ts diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000000..c75c56d490 --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,51 @@ +# ───────────────────────────────────────────────────────────────────────────── +# Stage 1: Build the React frontend +# ───────────────────────────────────────────────────────────────────────────── +FROM node:20-alpine AS frontend-builder + +WORKDIR /frontend + +# Copy package files first for better layer caching +COPY lov_frontend/package.json lov_frontend/package-lock.json ./ + +RUN npm install --legacy-peer-deps + +# Copy the rest of the frontend source +COPY lov_frontend/ ./ + +# Build the production static files (output goes to /frontend/dist) +RUN npm run build + +# ───────────────────────────────────────────────────────────────────────────── +# Stage 2: Python backend + serve frontend via FastAPI static files +# ───────────────────────────────────────────────────────────────────────────── +FROM python:3.11-slim AS backend + +# System deps needed by sentence-transformers / torch +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Install Python dependencies +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +# Copy backend source +COPY backend/ ./backend/ + +# Copy built frontend static files from stage 1 +COPY --from=frontend-builder /frontend/dist ./frontend/dist + +# Copy .env so load_dotenv() can find it inside the container +# docker-compose env_file also injects these as real env vars (double coverage) +COPY .env .env +COPY .env.example .env.example + +# Expose the FastAPI port +EXPOSE 8000 + +# Start the FastAPI server +CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/app/README.md b/app/README.md index ede288fba1..7a6b964b1f 100644 --- a/app/README.md +++ b/app/README.md @@ -1,139 +1,108 @@ -# Agentic RAG Starter (FastAPI + Streamlit + Endee + Gemini + Tavily) +# Agentic RAG Stack (FastAPI · React/Vite · Endee · Gemini · Tavily) -This app lets you chat with three routing paths: -- **Agent** (auto-route): LLM decides whether to use RAG (Endee), Web (Tavily), or Direct (LLM only). -- **RAG**: embed → Endee search → rerank → Gemini answer with sources. -- **Web**: Tavily search → Gemini answer with web citations. -- **Direct**: Gemini only (no retrieval). +**Summary:** A production-style, multi-route RAG service with an agentic router that chooses between dense RAG (Endee), web RAG (Tavily), or direct LLM answers (Gemini). Docs are chunked, embedded (MiniLM), indexed in Endee, reranked (cross-encoder) for precision, and cited in responses. React/Vite is the primary UI (Lovable export optional); FastAPI backend exposes upload and chat endpoints; runs locally with Endee on 8080 and API on 8000. + +## Feature Highlights +- Upload & index pdf/docx/txt/md; chunk (800/120) + MiniLM embeddings. +- Rerank with `cross-encoder/ms-marco-MiniLM-L-6-v2` (top 4) for high-precision context. +- Per-query routing: Agent (auto) or forced RAG/Web/Direct modes. +- Sources returned for RAG/Web with previews/links; 5-turn chat memory for coherence. +- Backend CORS open; single backend can serve multiple UIs simultaneously. + +## Architecture +``` +User (React UI) + | + v +Mode selector (Agent/RAG/Web/Direct) + | + +-- Agent -> Router prompt (Gemini) -> choose path + | +-- RAG: embed (MiniLM) -> Endee search -> rerank -> Gemini -> sources + | +-- Web: Tavily search -> Gemini -> web sources + | +-- Direct: Gemini only + | +Uploads -> extract -> chunk -> embed -> Endee upsert (metadata) +``` ## Tech Stack - Backend: FastAPI (`app/backend/main.py`) -- Frontend: Streamlit (`app/frontend/streamlit_app.py`) -- Vector DB: Endee server at `http://localhost:8080` (cosine, INT8, dim 384) +- Frontend: React/Vite (`app/frontend-react`), Lovable export (`app/lov_frontend`) +- Vector DB: Endee @ `http://localhost:8080` (cosine, INT8) - Embeddings: `sentence-transformers/all-MiniLM-L6-v2` - Reranker: `cross-encoder/ms-marco-MiniLM-L-6-v2` -- LLM: Gemini (default `gemini-2.5-flash` via Google AI key) +- LLM: Gemini (configurable via `.env`) - Web search: Tavily API -## Prerequisites -- Python 3.10+ -- Endee server running locally on 8080 (from `run.sh` or docker-compose) -- API keys: - - `GEMINI_API_KEY` (required) - - `TAVILY_API_KEY` (for Web route) +## Clone & Vector DB (WSL) +```powershell +git clone https://github.com/Siddharth-cvhs/endee.git +cd endee/app +wsl --install -d Ubuntu-22.04 +# in WSL, from repo root +sed -i 's/\r$//' install.sh run.sh +./install.sh --release --avx2 +./run.sh # Endee on http://localhost:8080 +``` +To restart Endee later (WSL): same three lines above. -## Setup +## Backend (FastAPI) ```powershell cd C:\VH811\projects\endee\app python -m venv .venv . .venv\Scripts\activate pip install -r requirements.txt +copy .env.example .env # fill GEMINI_API_KEY, TAVILY_API_KEY, etc. +python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000 ``` -### Environment -Edit `app/.env` (already created) and set: -``` -GEMINI_API_KEY=your_gemini_key -GEMINI_MODEL=gemini-2.5-flash -ENDEE_BASE_URL=http://localhost:8080/api/v1 -ENDEE_AUTH_TOKEN= # blank if Endee auth disabled -ENDEE_INDEX_NAME=rag_app -CHUNK_SIZE=800 -CHUNK_OVERLAP=120 -TOP_K=6 -TAVILY_API_KEY=your_tavily_key -BACKEND_URL=http://localhost:8000 -``` - -## Run -Backend: +## Frontend (React/Vite) ```powershell -cd C:\VH811\projects\endee\app -. .venv\Scripts\activate -python -m uvicorn backend.main:app --host 0.0.0.0 --port 8000 +cd app/frontend-react +npm install +npm run dev -- --host --port 5173 +# set VITE_BACKEND_URL if backend not on localhost:8000 ``` -Frontend: +Lovable export (if used): ```powershell -cd C:\VH811\projects\endee\app -. .venv\Scripts\activate -streamlit run frontend\streamlit_app.py +cd app/lov_frontend +npm install --legacy-peer-deps +npm run dev -- --host --port 5175 ``` -Open Streamlit at http://localhost:8501. -## How it Works (Pipeline) -1) **Uploads**: pdf/docx/txt/md → text extract (pypdf/docx2txt) → chunk (800/120) → embed (MiniLM) → upsert to Endee with metadata. -2) **Routing** (Agent): quick Gemini prompt decides `rag|web|direct` unless user forces mode via UI radio. -3) **RAG path**: embed query → Endee top_k=6 → rerank top 4 with cross-encoder → build context → Gemini answers; returns sources (chunk previews). -4) **Web path**: Tavily search max 5 results → context → Gemini answers; returns web URLs/titles. -5) **Direct path**: Gemini answers without retrieval. -6) **History**: last 5 turns sent to LLM for coherence (not for facts). +## API Contracts +- `POST /upload` (multipart): field `file` → `{ "message": "Indexed N chunks..." }` +- `POST /chat` (JSON): + ```json + { + "message": "...", + "mode": "auto|rag|web|direct", + "history": [{"user": "...", "answer": "..."}] + } + ``` + Response: + ```json + { + "answer": "...", + "sources": [{"id": "...", "source": "...", "title": "...", "preview": "...", "score": 0.87}], + "mode": "rag|web|direct" + } + ``` -## Routing Modes in UI -- Agent (auto): LLM router picks best path. -- RAG: force vector search. -- Web: force Tavily search. -- Direct: force plain LLM. - -## Tuning & Performance -- Lower `TOP_K` or rerank cutoff to reduce latency (currently rerank keeps top 4). -- Smaller reranker model (e.g., `cross-encoder/ms-marco-MiniLM-L-2-v2`) for speed. -- Use Direct mode for general chit-chat to avoid retrieval. -- Warm cache by one request after restart to load HF models. +## Tuning Notes +- Latency: lower `TOP_K` in `.env` or swap reranker to `cross-encoder/ms-marco-MiniLM-L-2-v2`. +- First-call warmup: run one query after restart to load HF models. +- Routing: leave on **Agent** for mixed queries; force **RAG** for internal docs, **Web** for current events, **Direct** for chit-chat. ## Troubleshooting -- `API key not valid / 403 / 429`: check/replace `GEMINI_API_KEY`, quota or billing; switch model if needed. -- `TAVILY_API_KEY not configured`: set the key in `.env` for Web mode. -- Upload errors: ensure file type is pdf/docx/txt/md and Endee server is reachable at `ENDEE_BASE_URL`. - -## File Map -- `app/backend/main.py` — API routes, router, RAG/Web/Direct flows -- `app/frontend/streamlit_app.py` — UI and routing selector -- `app/requirements.txt` — dependencies -- `app/.env` — keys and settings - -## Notes -- Index auto-creates with dimension 384 to match MiniLM embeddings. -- Sources shown only for RAG/Web modes; direct replies have no citations. -- Last 5 message pairs are passed for conversational continuity. - -## Architecture Diagram (Text) -``` -User (Streamlit UI) - | - v -Routing Selector (Agent/RAG/Web/Direct) - | - +-- Agent -> Router Prompt (Gemini) -> choose path - | | - | +-- RAG Path: - | | embed query (MiniLM) - | | -> Endee search (cosine, INT8, top_k) - | | -> rerank (cross-encoder) - | | -> context -> Gemini answer + chunk sources - | | - | +-- Web Path: - | | Tavily search (max 5) - | | -> context -> Gemini answer + web sources - | | - | +-- Direct Path: - | Gemini answer (no retrieval) - | -Uploads - | - v -File ingest (pdf/docx/txt/md) - -> extract text - -> chunk (800/120) - -> embed (MiniLM) - -> Endee upsert (metadata) -``` +- LLM 403/429: replace `GEMINI_API_KEY` or use a model with quota. +- Web mode 400: set `TAVILY_API_KEY`. +- Import errors: ensure you’re inside `.venv` and `pip install -r requirements.txt` completed. -## React Frontend (optional, smoother UI) -A Vite/React client lives in `app/frontend-react`. -Run it (after filling `.env` and starting backend): -```bash -cd app/frontend-react -npm install -npm run dev # starts on http://localhost:5173 +## Quick Run Summary +```powershell +# Endee (WSL): install.sh && run.sh (see above) +# Backend +cd app; . .venv\Scripts\activate; pip install -r requirements.txt; uvicorn backend.main:app --host 0.0.0.0 --port 8000 +# Frontend (React) +cd app/frontend-react; npm install; npm run dev -- --host --port 5173 ``` -Configure backend URL via env: `VITE_BACKEND_URL=http://localhost:8000` (create a `.env` in this folder if needed). diff --git a/app/backend/main.py b/app/backend/main.py index 99d753ccc2..fd9a86b421 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -5,6 +5,8 @@ from dotenv import load_dotenv from fastapi import FastAPI, File, UploadFile, HTTPException from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_google_genai import ChatGoogleGenerativeAI from sentence_transformers import SentenceTransformer, CrossEncoder @@ -13,28 +15,61 @@ from endee import Endee, Precision from endee.schema import VectorItem as EndeeVectorItem +from endee.index import Index as EndeeIndex # Monkey-patch Endee VectorItem to behave like a dict for `.get()` calls inside the SDK if not hasattr(EndeeVectorItem, "get"): EndeeVectorItem.get = lambda self, key, default=None: getattr(self, key, default) -load_dotenv() +# Monkey-patch Endee Index.is_hybrid property — SDK has a bug where it checks != "None" (string) +# instead of is not None, causing all indexes to be treated as hybrid +@property +def _fixed_is_hybrid(self): + """Check if index supports hybrid (dense + sparse) vectors.""" + return self.sparse_model is not None and self.sparse_model != "None" -# Environment -ENDEE_BASE_URL = os.getenv("ENDEE_BASE_URL", "http://localhost:8080/api/v1") +EndeeIndex.is_hybrid = _fixed_is_hybrid + +load_dotenv(override=False) # env vars already set (e.g. by Docker) take priority + +# ── Environment ──────────────────────────────────────────────────────────────── +ENDEE_BASE_URL = os.getenv("ENDEE_BASE_URL", "http://localhost:8080/api/v1") ENDEE_AUTH_TOKEN = os.getenv("ENDEE_AUTH_TOKEN", "") ENDEE_INDEX_NAME = os.getenv("ENDEE_INDEX_NAME", "rag_app") GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") -GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") +GEMINI_MODEL = os.getenv("GEMINI_MODEL", "gemini-2.5-flash") -CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "800")) +CHUNK_SIZE = int(os.getenv("CHUNK_SIZE", "800")) CHUNK_OVERLAP = int(os.getenv("CHUNK_OVERLAP", "120")) -TOP_K = int(os.getenv("TOP_K", "6")) +TOP_K = int(os.getenv("TOP_K", "8")) # fetch more, rerank to top 5 +RERANK_TOP = int(os.getenv("RERANK_TOP", "5")) # how many to keep after rerank TAVILY_API_KEY = os.getenv("TAVILY_API_KEY") -app = FastAPI(title="RAG Agentic Backend", version="0.2.0") +# ── System prompt ────────────────────────────────────────────────────────────── +# This is the core fix: a strong, explicit system identity that prevents Gemini +# from falling back to its default "I can't see files" behaviour. +SYSTEM_PROMPT = """You are an intelligent RAG (Retrieval-Augmented Generation) assistant. + +CRITICAL RULES — follow these without exception: +1. You have been given RETRIEVED CONTEXT extracted from documents that the user has uploaded into this system. This context IS the content of those documents — treat it as ground truth. +2. NEVER say you cannot access files, cannot see documents, or cannot read uploads. The retrieval pipeline has already done that for you and the text is right in the context block. +3. Answer ONLY from the provided context. Do not hallucinate or add information that is not present in the context. +4. If the context does not contain enough information to answer, say: "The uploaded documents don't seem to cover this topic. Try uploading a relevant file or switch to Web mode." +5. Always be specific, detailed, and cite which source/document the information comes from when possible. +6. For follow-up questions, use the conversation history to maintain coherence but still ground answers in the context. +""" + +ROUTER_SYSTEM_PROMPT = """You are a query router. Your only job is to output a single word. +Rules: +- Output RAG → if the question is about uploaded documents, files, or internal knowledge +- Output WEB → if the question needs current/live/external information from the internet +- Output DIRECT → for greetings, math, general knowledge, or anything that needs no retrieval +Output exactly one word. No punctuation, no explanation.""" + +# ── App ──────────────────────────────────────────────────────────────────────── +app = FastAPI(title="RAG Agentic Backend", version="0.3.0") app.add_middleware( CORSMiddleware, @@ -44,6 +79,14 @@ allow_headers=["*"], ) +# ── Globals ──────────────────────────────────────────────────────────────────── +embedder: Optional[SentenceTransformer] = None +reranker: Optional[CrossEncoder] = None +llm: Optional[ChatGoogleGenerativeAI] = None +endee_client: Optional[Endee] = None +endee_index = None +tavily_client: Optional[TavilyClient] = None + def get_endee_client() -> Endee: client = Endee(ENDEE_AUTH_TOKEN) if ENDEE_AUTH_TOKEN else Endee() @@ -52,31 +95,33 @@ def get_endee_client() -> Endee: def ensure_index(client: Endee, dim: int) -> None: + """Create a dense-only index. If a hybrid index exists, delete and recreate it.""" + needs_create = False try: - client.get_index(name=ENDEE_INDEX_NAME) - return + existing = client.get_index(name=ENDEE_INDEX_NAME) + if getattr(existing, "is_hybrid", False): + # Hybrid index can't accept dense-only upserts — delete it + client.delete_index(name=ENDEE_INDEX_NAME) + needs_create = True + # else: index exists and is dense-only, nothing to do except Exception: - pass - client.create_index( - name=ENDEE_INDEX_NAME, - dimension=dim, - space_type="cosine", - precision=Precision.INT8, - ) - - -embedder: Optional[SentenceTransformer] = None -reranker: Optional[CrossEncoder] = None -llm: Optional[ChatGoogleGenerativeAI] = None -endee_client: Optional[Endee] = None -endee_index = None -tavily_client: Optional[TavilyClient] = None + needs_create = True + + if needs_create: + # sparse_model=None → dense-only index, no hybrid + client.create_index( + name=ENDEE_INDEX_NAME, + dimension=dim, + space_type="cosine", + precision=Precision.INT8, + sparse_model=None, + ) def bootstrap(): global embedder, reranker, llm, endee_client, endee_index, tavily_client if GEMINI_API_KEY is None: - raise RuntimeError("GEMINI_API_KEY is required") + raise RuntimeError("GEMINI_API_KEY is required — set it in app/.env") embedder = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") reranker = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2") @@ -86,7 +131,7 @@ def bootstrap(): temperature=0.2, ) endee_client = get_endee_client() - ensure_index(endee_client, embedder.get_sentence_embedding_dimension()) + ensure_index(endee_client, embedder.get_embedding_dimension()) endee_index = endee_client.get_index(name=ENDEE_INDEX_NAME) if TAVILY_API_KEY: @@ -95,32 +140,42 @@ def bootstrap(): bootstrap() +# ── File ingestion ───────────────────────────────────────────────────────────── def read_file(file: UploadFile) -> str: - suffix = file.filename.split(".")[-1].lower() + suffix = (file.filename or "").split(".")[-1].lower() content = file.file.read() + if suffix == "pdf": try: from pypdf import PdfReader + import io except Exception as exc: raise HTTPException(status_code=500, detail=f"pypdf missing: {exc}") - file.file.seek(0) - reader = PdfReader(file.file) - text = "\n".join([p.extract_text() or "" for p in reader.pages]) + reader = PdfReader(io.BytesIO(content)) + text = "\n".join(p.extract_text() or "" for p in reader.pages) + elif suffix in {"txt", "md"}: text = content.decode("utf-8", errors="ignore") - elif suffix in {"docx"}: + + elif suffix == "docx": try: - import docx2txt + import docx2txt, tempfile, pathlib except Exception as exc: raise HTTPException(status_code=500, detail=f"docx2txt missing: {exc}") - temp_path = f"/tmp/{file.filename}" - with open(temp_path, "wb") as f: - f.write(content) - text = docx2txt.process(temp_path) or "" - os.remove(temp_path) + with tempfile.NamedTemporaryFile(suffix=".docx", delete=False) as tmp: + tmp.write(content) + tmp_path = tmp.name + try: + text = docx2txt.process(tmp_path) or "" + finally: + pathlib.Path(tmp_path).unlink(missing_ok=True) + else: - raise HTTPException(status_code=400, detail="Unsupported file type") + raise HTTPException(status_code=400, detail=f"Unsupported file type: .{suffix}") + + if not text.strip(): + raise HTTPException(status_code=422, detail="File appears to be empty or unreadable.") return text @@ -128,109 +183,118 @@ def chunk_text(text: str) -> List[str]: splitter = RecursiveCharacterTextSplitter( chunk_size=CHUNK_SIZE, chunk_overlap=CHUNK_OVERLAP, - separators=["\n\n", "\n", " ", ""], + separators=["\n\n", "\n", ". ", " ", ""], ) - return splitter.split_text(text) - - -def upsert_chunks(chunks: List[str], source: str): - embeddings = embedder.encode(chunks, convert_to_numpy=False) - payload = [] - for idx, (chunk, vector) in enumerate(zip(chunks, embeddings)): - payload.append( - { - "id": f"{source}::chunk-{idx}", - "vector": vector.tolist(), - "meta": {"source": source, "text": chunk}, - } - ) + return [c for c in splitter.split_text(text) if c.strip()] + + +def upsert_chunks(chunks: List[str], source: str) -> int: + embeddings = embedder.encode(chunks, batch_size=32, show_progress_bar=False, convert_to_numpy=True) + payload = [ + { + "id": f"{source}::chunk-{idx}", + "vector": vec.tolist(), + "meta": {"source": source, "text": chunk}, + } + for idx, (chunk, vec) in enumerate(zip(chunks, embeddings)) + ] endee_index.upsert(payload) + return len(payload) +# ── Retrieval ────────────────────────────────────────────────────────────────── def _normalize_result(item) -> dict: if hasattr(item, "dict"): base = item.dict() similarity = getattr(item, "similarity", None) - distance = getattr(item, "distance", None) + distance = getattr(item, "distance", None) else: base = dict(item) similarity = base.get("similarity") - distance = base.get("distance") + distance = base.get("distance") return { - "id": base.get("id"), - "meta": base.get("meta") or {}, + "id": base.get("id"), + "meta": base.get("meta") or {}, "similarity": similarity, - "distance": distance, - "raw": item, + "distance": distance, } -def retrieve(query: str): +def retrieve(query: str) -> List[dict]: query_vec = embedder.encode([query])[0].tolist() results = endee_index.query(vector=query_vec, top_k=TOP_K, include_vectors=False) if not results: return [] + docs = [_normalize_result(r) for r in results] + + # Cross-encoder reranking rerank_inputs = [(query, doc["meta"].get("text", "")) for doc in docs] scores = reranker.predict(rerank_inputs) reranked = sorted( - [ - {**doc, "rerank_score": float(score)} - for doc, score in zip(docs, scores) - ], + [{**doc, "rerank_score": float(score)} for doc, score in zip(docs, scores)], key=lambda x: x["rerank_score"], reverse=True, ) - return reranked[:4] + return reranked[:RERANK_TOP] -def web_search(query: str): +def web_search(query: str) -> List[dict]: if not tavily_client: raise HTTPException(status_code=500, detail="TAVILY_API_KEY not configured") res = tavily_client.search(query=query, max_results=5, include_images=False) - docs = [] - for item in res.get("results", []): - docs.append( - { - "id": item.get("url"), - "meta": { - "source": item.get("url"), - "text": item.get("content", ""), - "title": item.get("title", "") - }, - "score": item.get("score"), - } - ) - return docs - + return [ + { + "id": item.get("url"), + "meta": { + "source": item.get("url"), + "text": item.get("content", ""), + "title": item.get("title", ""), + }, + "score": item.get("score"), + } + for item in res.get("results", []) + ] + +# ── Prompt builders ──────────────────────────────────────────────────────────── def build_context(docs: List[dict]) -> str: parts = [] - for doc in docs: + for i, doc in enumerate(docs, 1): meta = doc.get("meta", {}) - src = meta.get("title") or meta.get("source") - parts.append(f"[{src}] {meta.get('text', '')}") + src = meta.get("title") or meta.get("source") or "Unknown" + text = meta.get("text", "").strip() + parts.append(f"--- Source {i}: {src} ---\n{text}") return "\n\n".join(parts) -def build_history(history: List[dict]) -> str: +def build_history_text(history: List[dict]) -> str: lines = [] for turn in history[-5:]: - u = turn.get("user") or "" - a = turn.get("answer") or "" - lines.append(f"User: {u}\nAssistant: {a}") + u = (turn.get("user") or "").strip() + a = (turn.get("answer") or "").strip() + if u and a: + lines.append(f"User: {u}\nAssistant: {a}") return "\n\n".join(lines) +# ── Routing ──────────────────────────────────────────────────────────────────── def route_query(question: str, force_mode: str = "auto") -> str: if force_mode in {"rag", "web", "direct"}: return force_mode - # lightweight classification with the main LLM - prompt = ( - "Decide routing for the question. Output only one token: RAG, WEB, or DIRECT.\n" - "Use RAG if it likely needs internal docs; WEB if it's about current/general external info; otherwise DIRECT.\n" - f"Question: {question}\nAnswer:" - ) + + # Heuristic fast-path: vague document questions → always RAG + doc_keywords = [ + "document", "file", "upload", "uploaded", "pdf", "docx", + "text", "content", "summary", "summarize", "what does it say", + "what is this", "tell me about", "explain this", "according to", + ] + q_lower = question.lower() + if any(kw in q_lower for kw in doc_keywords): + return "rag" + + # LLM-based routing with a dedicated system prompt + prompt = f"{ROUTER_SYSTEM_PROMPT}\n\nQuestion: {question}\nAnswer:" try: resp = llm.invoke(prompt) text = resp.content.strip().upper() @@ -242,64 +306,80 @@ def route_query(question: str, force_mode: str = "auto") -> str: except Exception: return "direct" +# ── Answer generation ────────────────────────────────────────────────────────── def answer(question: str, context_docs: List[dict], history: List[dict], mode: str) -> dict: + history_text = build_history_text(history) + if context_docs: context_text = build_context(context_docs) prompt = ( - "Use ONLY the provided context (and brief history if present). " - "If something is missing, say you don't have it.\n" - f"Context:\n{context_text}\n\n" + f"{SYSTEM_PROMPT}\n\n" + "=== RETRIEVED DOCUMENT CONTEXT ===\n" + "The following text has been extracted from the user's uploaded documents " + "via semantic search. This IS the document content — answer from it directly.\n\n" + f"{context_text}\n\n" + "=== END OF CONTEXT ===\n\n" + ) + if history_text: + prompt += f"=== CONVERSATION HISTORY ===\n{history_text}\n\n=== END HISTORY ===\n\n" + prompt += ( + f"User question: {question}\n\n" + "Answer thoroughly based on the context above. " + "If the context covers the question, answer in full detail. " + "If it only partially covers it, answer what you can and note what's missing." ) else: - prompt = "Answer the user. If you don't know, say so.\n" + # Direct mode — no retrieval context + prompt = ( + f"{SYSTEM_PROMPT}\n\n" + "No document context was retrieved for this query. Answer from your general knowledge.\n\n" + ) + if history_text: + prompt += f"=== CONVERSATION HISTORY ===\n{history_text}\n\n=== END HISTORY ===\n\n" + prompt += f"User question: {question}" - history_text = build_history(history) - if history_text: - prompt += f"Recent history (for coherence, not new facts):\n{history_text}\n\n" + resp = llm.invoke(prompt) - prompt += f"Question: {question}" + sources = [ + { + "id": doc.get("id"), + "source": doc.get("meta", {}).get("source"), + "title": doc.get("meta", {}).get("title") or doc.get("meta", {}).get("source"), + "score": doc.get("rerank_score", doc.get("similarity", doc.get("score"))), + "preview": doc.get("meta", {}).get("text", "")[:300], + } + for doc in context_docs + ] - resp = llm.invoke(prompt) - return { - "answer": resp.content, - "sources": [ - { - "id": doc.get("id"), - "source": doc.get("meta", {}).get("source"), - "title": doc.get("meta", {}).get("title"), - "score": doc.get("rerank_score", doc.get("similarity", doc.get("score"))), - "preview": doc.get("meta", {}).get("text", "")[:200], - } - for doc in context_docs - ], - "mode": mode, - } + return {"answer": resp.content, "sources": sources, "mode": mode} +# ── Endpoints ────────────────────────────────────────────────────────────────── @app.get("/health") def health(): - return {"status": "ok"} + return {"status": "ok", "model": GEMINI_MODEL, "index": ENDEE_INDEX_NAME} @app.post("/upload") async def upload(file: UploadFile = File(...)): try: - text = read_file(file) + text = read_file(file) chunks = chunk_text(text) - upsert_chunks(chunks, source=file.filename) - return {"message": f"Indexed {len(chunks)} chunks from {file.filename}"} - except HTTPException as e: - raise e + count = upsert_chunks(chunks, source=file.filename) + return {"message": f"Indexed {count} chunks from '{file.filename}'"} + except HTTPException: + raise except Exception as exc: raise HTTPException(status_code=500, detail=str(exc)) @app.post("/chat") async def chat(payload: dict): - question = payload.get("message") or "" - force_mode = (payload.get("mode") or "auto").lower() - history = payload.get("history") or [] + question = (payload.get("message") or "").strip() + force_mode = (payload.get("mode") or "auto").lower() + history = payload.get("history") or [] + if not question: raise HTTPException(status_code=400, detail="message is required") @@ -307,6 +387,10 @@ async def chat(payload: dict): if route == "rag": docs = retrieve(question) + # If RAG returns nothing, fall back to direct rather than hallucinating + if not docs: + route = "direct" + docs = [] elif route == "web": docs = web_search(question) else: @@ -314,3 +398,16 @@ async def chat(payload: dict): result = answer(question, docs, history, mode=route) return JSONResponse(result) + + +# ── Serve React frontend (production Docker build only) ──────────────────────── +# The built frontend lives at /app/frontend/dist inside the container. +# This must come AFTER all API routes so /upload and /chat are not shadowed. +_FRONTEND_DIR = os.path.join(os.path.dirname(__file__), "..", "frontend", "dist") +if os.path.isdir(_FRONTEND_DIR): + app.mount("/assets", StaticFiles(directory=os.path.join(_FRONTEND_DIR, "assets")), name="assets") + + @app.get("/{full_path:path}", include_in_schema=False) + async def serve_spa(full_path: str): + """Catch-all: serve index.html for any non-API route (React Router support).""" + return FileResponse(os.path.join(_FRONTEND_DIR, "index.html")) diff --git a/app/frontend-react/index.html b/app/frontend-react/index.html deleted file mode 100644 index ad88156359..0000000000 --- a/app/frontend-react/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Agentic RAG UI - - -
- - - diff --git a/app/frontend-react/package.json b/app/frontend-react/package.json deleted file mode 100644 index 419535c378..0000000000 --- a/app/frontend-react/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "agentic-rag-ui", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "vite", - "build": "vite build", - "preview": "vite preview" - }, - "dependencies": { - "axios": "^1.6.7", - "react": "^18.2.0", - "react-dom": "^18.2.0" - }, - "devDependencies": { - "@types/react": "^18.2.55", - "@types/react-dom": "^18.2.18", - "@vitejs/plugin-react": "^4.2.0", - "typescript": "^5.3.3", - "vite": "^5.0.8" - } -} diff --git a/app/frontend-react/src/App.tsx b/app/frontend-react/src/App.tsx deleted file mode 100644 index 9189ee92c0..0000000000 --- a/app/frontend-react/src/App.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { useMemo, useState } from "react"; -import Chat from "./components/Chat"; -import Uploader from "./components/Uploader"; -import { ChatMessage, Mode } from "../src/types"; - -function App() { - const [messages, setMessages] = useState([]); - const [loading, setLoading] = useState(false); - const [mode, setMode] = useState("auto"); - - const history = useMemo( - () => messages.slice(-5).map((m) => ({ user: m.user, answer: m.answer })), - [messages] - ); - - return ( -
-

Agentic RAG Console

-

- RAG + Web + Direct router on Endee + Gemini + Tavily -

- -
-
- Mode - {(["auto", "rag", "web", "direct"] as Mode[]).map((m) => ( - - ))} -
- -
- -
- -
-
- ); -} - -export default App; diff --git a/app/frontend-react/src/api.ts b/app/frontend-react/src/api.ts deleted file mode 100644 index c52d833b7e..0000000000 --- a/app/frontend-react/src/api.ts +++ /dev/null @@ -1,31 +0,0 @@ -import axios from "axios"; -import { Mode, Source } from "./types"; - -const BASE_URL = import.meta.env.VITE_BACKEND_URL || "http://localhost:8000"; - -export interface ChatPayload { - message: string; - mode: Mode; - history: { user: string; answer: string }[]; -} - -export interface ChatResponse { - answer: string; - sources: Source[]; - mode: string; -} - -export async function sendChat(payload: ChatPayload): Promise { - const { data } = await axios.post(`${BASE_URL}/chat`, payload, { timeout: 60000 }); - return data as ChatResponse; -} - -export async function uploadFile(file: File): Promise { - const formData = new FormData(); - formData.append("file", file); - const { data } = await axios.post(`${BASE_URL}/upload`, formData, { - headers: { "Content-Type": "multipart/form-data" }, - timeout: 120000, - }); - return data.message as string; -} diff --git a/app/frontend-react/src/components/Chat.tsx b/app/frontend-react/src/components/Chat.tsx deleted file mode 100644 index 12a7a503b0..0000000000 --- a/app/frontend-react/src/components/Chat.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useState } from "react"; -import { sendChat } from "../api"; -import { ChatMessage, Mode } from "../types"; - -interface Props { - mode: Mode; - history: { user: string; answer: string }[]; - messages: ChatMessage[]; - setMessages: (m: ChatMessage[]) => void; - loading: boolean; - setLoading: (v: boolean) => void; -} - -const Chat = ({ mode, history, messages, setMessages, loading, setLoading }: Props) => { - const [input, setInput] = useState(""); - - const send = async () => { - if (!input.trim()) return; - setLoading(true); - try { - const resp = await sendChat({ message: input, mode, history }); - setMessages([ - ...messages, - { - user: input, - answer: resp.answer, - sources: resp.sources, - mode: resp.mode, - }, - ]); - setInput(""); - } catch (err: any) { - const msg = err?.response?.data?.detail || err.message || "Request failed"; - setMessages([ - ...messages, - { user: input, answer: `Error: ${msg}`, mode: "error" }, - ]); - } finally { - setLoading(false); - } - }; - - return ( -
-
- setInput(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && send()} - disabled={loading} - /> - -
- -
- {messages.slice().reverse().map((m, idx) => ( -
-
You: {m.user}
-
{(m.mode || "reply").toUpperCase()}: {m.answer}
- {m.sources && m.sources.length > 0 && ( -
- Sources: -
    - {m.sources.map((s, i) => ( -
  • - {s.title || s.source || s.id} {s.score ? `(score ${s.score})` : ""} - {s.preview ? ` — ${s.preview}` : ""} -
  • - ))} -
-
- )} -
- ))} -
-
- ); -}; - -export default Chat; diff --git a/app/frontend-react/src/components/Uploader.tsx b/app/frontend-react/src/components/Uploader.tsx deleted file mode 100644 index 238a79b5e4..0000000000 --- a/app/frontend-react/src/components/Uploader.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useState } from "react"; -import { uploadFile } from "../api"; - -interface Props { - disabled: boolean; -} - -const Uploader = ({ disabled }: Props) => { - const [status, setStatus] = useState(""); - - const onChange = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file) return; - setStatus("Uploading..."); - try { - const msg = await uploadFile(file); - setStatus(msg); - } catch (err: any) { - const msg = err?.response?.data?.detail || err.message || "Upload failed"; - setStatus(msg); - } - }; - - return ( -
- - - {status &&
{status}
} -
- ); -}; - -export default Uploader; diff --git a/app/frontend-react/src/main.tsx b/app/frontend-react/src/main.tsx deleted file mode 100644 index 7590916fa3..0000000000 --- a/app/frontend-react/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react"; -import ReactDOM from "react-dom/client"; -import App from "./App"; -import "./styles.css"; - -ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - -); diff --git a/app/frontend-react/src/styles.css b/app/frontend-react/src/styles.css deleted file mode 100644 index 9e105503d2..0000000000 --- a/app/frontend-react/src/styles.css +++ /dev/null @@ -1,51 +0,0 @@ -:root { - font-family: "Inter", system-ui, -apple-system, sans-serif; - color: #0f172a; - background-color: #f8fafc; -} -body { - margin: 0; -} -.container { - max-width: 1200px; - margin: 0 auto; - padding: 24px; -} -.panel { - background: #ffffff; - border: 1px solid #e2e8f0; - border-radius: 12px; - padding: 16px; - box-shadow: 0 6px 24px rgba(15, 23, 42, 0.08); -} -.btn { - background: linear-gradient(90deg, #3b82f6, #6366f1); - color: #fff; - border: none; - border-radius: 10px; - padding: 10px 16px; - cursor: pointer; - font-weight: 600; -} -.btn:disabled { - opacity: 0.6; - cursor: not-allowed; -} -.badge { - display: inline-block; - padding: 4px 8px; - border-radius: 999px; - font-size: 12px; - background: #e0f2fe; - color: #0369a1; - margin-right: 6px; -} -.message { - margin-bottom: 12px; - padding: 12px; - border-radius: 10px; -} -.message.user { background: #eff6ff; } -.message.bot { background: #f1f5f9; } -.sources { font-size: 12px; color: #475569; } -.toolbar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } diff --git a/app/frontend-react/src/types.ts b/app/frontend-react/src/types.ts deleted file mode 100644 index b82e51ec45..0000000000 --- a/app/frontend-react/src/types.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type Mode = "auto" | "rag" | "web" | "direct"; - -export interface Source { - id?: string | null; - source?: string | null; - title?: string | null; - preview?: string | null; - score?: number | null; -} - -export interface ChatMessage { - user: string; - answer: string; - sources?: Source[]; - mode?: string; -} diff --git a/app/frontend-react/tsconfig.json b/app/frontend-react/tsconfig.json deleted file mode 100644 index b2aeeb051a..0000000000 --- a/app/frontend-react/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "module": "ESNext", - "skipLibCheck": true, - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx", - "strict": true - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/app/frontend-react/tsconfig.node.json b/app/frontend-react/tsconfig.node.json deleted file mode 100644 index 339f928610..0000000000 --- a/app/frontend-react/tsconfig.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "Bundler" - }, - "include": ["vite.config.ts"] -} diff --git a/app/frontend-react/vite.config.ts b/app/frontend-react/vite.config.ts deleted file mode 100644 index 80a7f66514..0000000000 --- a/app/frontend-react/vite.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; - -export default defineConfig({ - plugins: [react()], - server: { - port: 5173, - }, -}); diff --git a/app/requirements.txt b/app/requirements.txt index 4b53dec826..051e3c05b7 100644 --- a/app/requirements.txt +++ b/app/requirements.txt @@ -1,5 +1,6 @@ fastapi>=0.110.0 uvicorn[standard]>=0.27.1 +aiofiles>=23.2.1 python-dotenv>=1.0.1 langchain>=0.1.16 langchain-community>=0.0.34 diff --git a/docker-compose.yml b/docker-compose.yml index 9c6aab1e6f..ad936f0fdb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,35 +1,66 @@ services: - endee-oss: - image: endee-oss:latest + + # ────────────────────────────────────────────────────────────────────────── + # Endee Vector Database (built from source via infra/Dockerfile) + # Runs on port 8080 inside the stack; your backend talks to it via + # http://endee:8080/api/v1 (Docker internal DNS, no host port needed) + # ────────────────────────────────────────────────────────────────────────── + endee: + build: + context: . # root of the repo (needs src/, CMakeLists.txt etc.) + dockerfile: infra/Dockerfile + args: + BUILD_ARCH: avx2 # change to "release" if your CPU doesn't support AVX2 + image: endee-oss:latest # tag the built image so it's reused on next `up` container_name: endee-oss ports: - - "8080:8080" + - "8080:8080" # expose to host so you can inspect it if needed ulimits: nofile: 100000 -# ---------------------------------------------------- -# Logging Configuration Block logging: - driver: "json-file" # Explicitly use the default driver + driver: "json-file" options: - # Set the max size of a single log file to 200 megabytes - # Docker rotates the log file when it hits this limit max-size: "200m" - # Set the maximum number of log files to keep - # (200m * 5 files = 1000m or 1 GB total log space) max-file: "5" -# ---------------------------------------------------- environment: NDD_NUM_THREADS: 0 - NDD_AUTH_TOKEN: "" # Replace with your auth token + NDD_AUTH_TOKEN: "" # set a token here if you want auth on Endee volumes: - - endee-data:/data + - endee-data:/data # persists your vector index across restarts + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 15s + + # ────────────────────────────────────────────────────────────────────────── + # Your App (FastAPI backend + React frontend served as static files) + # Built from app/Dockerfile + # ────────────────────────────────────────────────────────────────────────── + app: + build: + context: ./app # Docker build context is the app/ folder + dockerfile: Dockerfile + container_name: rag-app + ports: + - "8000:8000" # FastAPI on http://localhost:8000 + env_file: + - ./app/.env # your real keys go here (copy from .env.example) + environment: + # Override Endee URL to use Docker internal hostname instead of localhost + ENDEE_BASE_URL: "http://endee:8080/api/v1" + depends_on: + endee: + condition: service_healthy # wait for Endee to be ready before starting restart: unless-stopped volumes: - # Modify the 'device' path below to change the host directory used for data persistence endee-data: + # Uncomment below to bind to a specific host folder instead of a Docker volume # driver: local # driver_opts: - # type: ext4 - # o: bind - # device: ${PWD}/dockerdata + # type: none + # o: bind + # device: ${PWD}/endee-data From 834e7533042ef2ac71b38a37585def1ebf110875 Mon Sep 17 00:00:00 2001 From: Siddharth-cvhs Date: Thu, 7 May 2026 18:58:16 +0530 Subject: [PATCH 45/48] Add Docker setup, UI improvements, and full frontend source - Add app/Dockerfile (multi-stage: Node build + Python runtime) - Update docker-compose.yml to build Endee from source (infra/Dockerfile) - Add lov_frontend as regular tracked files (removed nested .git) - Fix Endee SDK is_hybrid bug via monkey-patch in main.py - Add mode-aware prompts (RAG/Web/Direct use separate system prompts) - Improve Tavily web search with news topic + 7-day recency filter - Add markdown rendering (react-markdown + remark-gfm) - Redesign source cards to full-width stacked layout with expand/collapse - Widen chat content area to max-w-[1600px] - Update .gitignore to exclude node_modules, .venv, endee-data - Update README with one-command Docker setup instructions --- .gitignore | 16 + app/README.md | 111 + app/backend/main.py | 77 +- app/lov_frontend | 1 - app/lov_frontend/.gitignore | 4 + app/lov_frontend/README.md | 25 + app/lov_frontend/bun.lock | 1119 ++ app/lov_frontend/components.json | 20 + app/lov_frontend/eslint.config.js | 26 + app/lov_frontend/index.html | 26 + app/lov_frontend/package-lock.json | 9968 +++++++++++++++++ app/lov_frontend/package.json | 92 + app/lov_frontend/playwright-fixture.ts | 3 + app/lov_frontend/playwright.config.ts | 10 + app/lov_frontend/postcss.config.js | 6 + app/lov_frontend/public/favicon.ico | Bin 0 -> 20373 bytes app/lov_frontend/public/placeholder.svg | 40 + app/lov_frontend/public/robots.txt | 14 + app/lov_frontend/src/App.css | 42 + app/lov_frontend/src/App.tsx | 31 + app/lov_frontend/src/components/ChatInput.tsx | 53 + .../src/components/ChatMessage.tsx | 70 + .../src/components/FileUploader.tsx | 129 + .../src/components/ModeSelector.tsx | 37 + app/lov_frontend/src/components/NavLink.tsx | 28 + app/lov_frontend/src/components/Sidebar.tsx | 40 + .../src/components/SourceCards.tsx | 94 + .../src/components/ThemeToggle.tsx | 20 + .../src/components/ui/accordion.tsx | 52 + .../src/components/ui/alert-dialog.tsx | 104 + app/lov_frontend/src/components/ui/alert.tsx | 43 + .../src/components/ui/aspect-ratio.tsx | 5 + app/lov_frontend/src/components/ui/avatar.tsx | 38 + app/lov_frontend/src/components/ui/badge.tsx | 29 + .../src/components/ui/breadcrumb.tsx | 90 + app/lov_frontend/src/components/ui/button.tsx | 47 + .../src/components/ui/calendar.tsx | 54 + app/lov_frontend/src/components/ui/card.tsx | 43 + .../src/components/ui/carousel.tsx | 224 + app/lov_frontend/src/components/ui/chart.tsx | 303 + .../src/components/ui/checkbox.tsx | 26 + .../src/components/ui/collapsible.tsx | 9 + .../src/components/ui/command.tsx | 132 + .../src/components/ui/context-menu.tsx | 178 + app/lov_frontend/src/components/ui/dialog.tsx | 95 + app/lov_frontend/src/components/ui/drawer.tsx | 87 + .../src/components/ui/dropdown-menu.tsx | 179 + app/lov_frontend/src/components/ui/form.tsx | 129 + .../src/components/ui/hover-card.tsx | 27 + .../src/components/ui/input-otp.tsx | 61 + app/lov_frontend/src/components/ui/input.tsx | 22 + app/lov_frontend/src/components/ui/label.tsx | 17 + .../src/components/ui/menubar.tsx | 207 + .../src/components/ui/navigation-menu.tsx | 120 + .../src/components/ui/pagination.tsx | 81 + .../src/components/ui/popover.tsx | 29 + .../src/components/ui/progress.tsx | 23 + .../src/components/ui/radio-group.tsx | 36 + .../src/components/ui/resizable.tsx | 37 + .../src/components/ui/scroll-area.tsx | 38 + app/lov_frontend/src/components/ui/select.tsx | 143 + .../src/components/ui/separator.tsx | 20 + app/lov_frontend/src/components/ui/sheet.tsx | 107 + .../src/components/ui/sidebar.tsx | 637 ++ .../src/components/ui/skeleton.tsx | 7 + app/lov_frontend/src/components/ui/slider.tsx | 23 + app/lov_frontend/src/components/ui/sonner.tsx | 27 + app/lov_frontend/src/components/ui/switch.tsx | 27 + app/lov_frontend/src/components/ui/table.tsx | 72 + app/lov_frontend/src/components/ui/tabs.tsx | 53 + .../src/components/ui/textarea.tsx | 21 + app/lov_frontend/src/components/ui/toast.tsx | 111 + .../src/components/ui/toaster.tsx | 24 + .../src/components/ui/toggle-group.tsx | 49 + app/lov_frontend/src/components/ui/toggle.tsx | 37 + .../src/components/ui/tooltip.tsx | 28 + .../src/components/ui/use-toast.ts | 3 + app/lov_frontend/src/hooks/use-mobile.tsx | 19 + app/lov_frontend/src/hooks/use-toast.ts | 186 + app/lov_frontend/src/index.css | 135 + app/lov_frontend/src/lib/api.ts | 59 + app/lov_frontend/src/lib/utils.ts | 6 + app/lov_frontend/src/main.tsx | 5 + app/lov_frontend/src/pages/Index.tsx | 149 + app/lov_frontend/src/pages/Landing.tsx | 103 + app/lov_frontend/src/pages/NotFound.tsx | 24 + app/lov_frontend/src/test/example.test.ts | 7 + app/lov_frontend/src/test/setup.ts | 15 + app/lov_frontend/src/vite-env.d.ts | 1 + app/lov_frontend/tailwind.config.ts | 97 + app/lov_frontend/tsconfig.app.json | 35 + app/lov_frontend/tsconfig.json | 24 + app/lov_frontend/tsconfig.node.json | 22 + app/lov_frontend/vite.config.ts | 21 + app/lov_frontend/vitest.config.ts | 16 + 95 files changed, 16962 insertions(+), 18 deletions(-) delete mode 160000 app/lov_frontend create mode 100644 app/lov_frontend/.gitignore create mode 100644 app/lov_frontend/README.md create mode 100644 app/lov_frontend/bun.lock create mode 100644 app/lov_frontend/components.json create mode 100644 app/lov_frontend/eslint.config.js create mode 100644 app/lov_frontend/index.html create mode 100644 app/lov_frontend/package-lock.json create mode 100644 app/lov_frontend/package.json create mode 100644 app/lov_frontend/playwright-fixture.ts create mode 100644 app/lov_frontend/playwright.config.ts create mode 100644 app/lov_frontend/postcss.config.js create mode 100644 app/lov_frontend/public/favicon.ico create mode 100644 app/lov_frontend/public/placeholder.svg create mode 100644 app/lov_frontend/public/robots.txt create mode 100644 app/lov_frontend/src/App.css create mode 100644 app/lov_frontend/src/App.tsx create mode 100644 app/lov_frontend/src/components/ChatInput.tsx create mode 100644 app/lov_frontend/src/components/ChatMessage.tsx create mode 100644 app/lov_frontend/src/components/FileUploader.tsx create mode 100644 app/lov_frontend/src/components/ModeSelector.tsx create mode 100644 app/lov_frontend/src/components/NavLink.tsx create mode 100644 app/lov_frontend/src/components/Sidebar.tsx create mode 100644 app/lov_frontend/src/components/SourceCards.tsx create mode 100644 app/lov_frontend/src/components/ThemeToggle.tsx create mode 100644 app/lov_frontend/src/components/ui/accordion.tsx create mode 100644 app/lov_frontend/src/components/ui/alert-dialog.tsx create mode 100644 app/lov_frontend/src/components/ui/alert.tsx create mode 100644 app/lov_frontend/src/components/ui/aspect-ratio.tsx create mode 100644 app/lov_frontend/src/components/ui/avatar.tsx create mode 100644 app/lov_frontend/src/components/ui/badge.tsx create mode 100644 app/lov_frontend/src/components/ui/breadcrumb.tsx create mode 100644 app/lov_frontend/src/components/ui/button.tsx create mode 100644 app/lov_frontend/src/components/ui/calendar.tsx create mode 100644 app/lov_frontend/src/components/ui/card.tsx create mode 100644 app/lov_frontend/src/components/ui/carousel.tsx create mode 100644 app/lov_frontend/src/components/ui/chart.tsx create mode 100644 app/lov_frontend/src/components/ui/checkbox.tsx create mode 100644 app/lov_frontend/src/components/ui/collapsible.tsx create mode 100644 app/lov_frontend/src/components/ui/command.tsx create mode 100644 app/lov_frontend/src/components/ui/context-menu.tsx create mode 100644 app/lov_frontend/src/components/ui/dialog.tsx create mode 100644 app/lov_frontend/src/components/ui/drawer.tsx create mode 100644 app/lov_frontend/src/components/ui/dropdown-menu.tsx create mode 100644 app/lov_frontend/src/components/ui/form.tsx create mode 100644 app/lov_frontend/src/components/ui/hover-card.tsx create mode 100644 app/lov_frontend/src/components/ui/input-otp.tsx create mode 100644 app/lov_frontend/src/components/ui/input.tsx create mode 100644 app/lov_frontend/src/components/ui/label.tsx create mode 100644 app/lov_frontend/src/components/ui/menubar.tsx create mode 100644 app/lov_frontend/src/components/ui/navigation-menu.tsx create mode 100644 app/lov_frontend/src/components/ui/pagination.tsx create mode 100644 app/lov_frontend/src/components/ui/popover.tsx create mode 100644 app/lov_frontend/src/components/ui/progress.tsx create mode 100644 app/lov_frontend/src/components/ui/radio-group.tsx create mode 100644 app/lov_frontend/src/components/ui/resizable.tsx create mode 100644 app/lov_frontend/src/components/ui/scroll-area.tsx create mode 100644 app/lov_frontend/src/components/ui/select.tsx create mode 100644 app/lov_frontend/src/components/ui/separator.tsx create mode 100644 app/lov_frontend/src/components/ui/sheet.tsx create mode 100644 app/lov_frontend/src/components/ui/sidebar.tsx create mode 100644 app/lov_frontend/src/components/ui/skeleton.tsx create mode 100644 app/lov_frontend/src/components/ui/slider.tsx create mode 100644 app/lov_frontend/src/components/ui/sonner.tsx create mode 100644 app/lov_frontend/src/components/ui/switch.tsx create mode 100644 app/lov_frontend/src/components/ui/table.tsx create mode 100644 app/lov_frontend/src/components/ui/tabs.tsx create mode 100644 app/lov_frontend/src/components/ui/textarea.tsx create mode 100644 app/lov_frontend/src/components/ui/toast.tsx create mode 100644 app/lov_frontend/src/components/ui/toaster.tsx create mode 100644 app/lov_frontend/src/components/ui/toggle-group.tsx create mode 100644 app/lov_frontend/src/components/ui/toggle.tsx create mode 100644 app/lov_frontend/src/components/ui/tooltip.tsx create mode 100644 app/lov_frontend/src/components/ui/use-toast.ts create mode 100644 app/lov_frontend/src/hooks/use-mobile.tsx create mode 100644 app/lov_frontend/src/hooks/use-toast.ts create mode 100644 app/lov_frontend/src/index.css create mode 100644 app/lov_frontend/src/lib/api.ts create mode 100644 app/lov_frontend/src/lib/utils.ts create mode 100644 app/lov_frontend/src/main.tsx create mode 100644 app/lov_frontend/src/pages/Index.tsx create mode 100644 app/lov_frontend/src/pages/Landing.tsx create mode 100644 app/lov_frontend/src/pages/NotFound.tsx create mode 100644 app/lov_frontend/src/test/example.test.ts create mode 100644 app/lov_frontend/src/test/setup.ts create mode 100644 app/lov_frontend/src/vite-env.d.ts create mode 100644 app/lov_frontend/tailwind.config.ts create mode 100644 app/lov_frontend/tsconfig.app.json create mode 100644 app/lov_frontend/tsconfig.json create mode 100644 app/lov_frontend/tsconfig.node.json create mode 100644 app/lov_frontend/vite.config.ts create mode 100644 app/lov_frontend/vitest.config.ts diff --git a/.gitignore b/.gitignore index ddd10fad2b..03c9be0d25 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,19 @@ frontend/* # DS Store .DS_Store + +# Node +node_modules/ +app/lov_frontend/node_modules/ + +# Python +app/.venv/ +__pycache__/ +*.pyc +*.pyo + +# Docker persistent data volume (local only) +endee-data/ + +# Environment files (never commit real keys) +app/.env diff --git a/app/README.md b/app/README.md index 7a6b964b1f..dec5ed65c9 100644 --- a/app/README.md +++ b/app/README.md @@ -1,5 +1,116 @@ # Agentic RAG Stack (FastAPI · React/Vite · Endee · Gemini · Tavily) +**Summary:** A production-style, multi-route RAG service with an agentic router that chooses between dense RAG (Endee), web RAG (Tavily), or direct LLM answers (Gemini). Docs are chunked, embedded (MiniLM), indexed in Endee, reranked (cross-encoder) for precision, and cited in responses. React/Vite frontend; FastAPI backend; everything runs with a single Docker command. + +--- + +## 🚀 Quick Start (Docker — recommended) + +**Prerequisites:** [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running. That's it. + +```bash +# 1. Clone the repo +git clone +cd + +# 2. Create your env file with your API keys +cp app/.env.example app/.env +# Open app/.env and fill in: +# GEMINI_API_KEY → https://aistudio.google.com/app/apikey +# TAVILY_API_KEY → https://app.tavily.com + +# 3. Start everything (Endee Vector DB + Backend + Frontend) +docker compose up --build +``` + +That's it. First build takes 5–10 minutes (compiles Endee from C++ source). + +| Service | URL | +|---|---| +| App (frontend + API) | http://localhost:8000 | +| Endee Vector DB | http://localhost:8080 | + +> **Note:** `ENDEE_BASE_URL` in your `.env` can stay as `localhost:8080` — Docker Compose automatically overrides it to the internal service hostname. + +--- + +## Feature Highlights +- Upload & index pdf/docx/txt/md; chunk (800/120) + MiniLM embeddings. +- Rerank with `cross-encoder/ms-marco-MiniLM-L-6-v2` (top 5) for high-precision context. +- Per-query routing: Agent (auto) or forced RAG/Web/Direct modes. +- Sources returned for RAG/Web with previews/links; 5-turn chat memory for coherence. + +## Architecture +``` +User (React UI) + | + v +Mode selector (Agent/RAG/Web/Direct) + | + +-- Agent -> Router prompt (Gemini) -> choose path + | +-- RAG: embed (MiniLM) -> Endee search -> rerank -> Gemini -> sources + | +-- Web: Tavily news+general search -> Gemini -> web sources + | +-- Direct: Gemini only + | +Uploads -> extract -> chunk -> embed -> Endee upsert (metadata) +``` + +## Tech Stack +- Backend: FastAPI (`app/backend/main.py`) +- Frontend: React/Vite (`app/lov_frontend`) +- Vector DB: Endee (built from source via `infra/Dockerfile`) +- Embeddings: `sentence-transformers/all-MiniLM-L6-v2` +- Reranker: `cross-encoder/ms-marco-MiniLM-L-6-v2` +- LLM: Gemini 2.5 Flash +- Web search: Tavily API (news + general fallback) + +--- + +## Local Development (without Docker) + +If you want hot-reload during development: + +```powershell +# Terminal 1 — Endee (WSL) +./install.sh --release --avx2 +./run.sh + +# Terminal 2 — Backend +cd app +python -m venv .venv +. .venv\Scripts\activate +pip install -r requirements.txt +uvicorn backend.main:app --host 0.0.0.0 --port 8000 + +# Terminal 3 — Frontend +cd app/lov_frontend +npm install --legacy-peer-deps +npm run dev -- --host --port 5173 +``` + +--- + +## API Reference +- `POST /upload` — multipart, field `file` → `{ "message": "Indexed N chunks..." }` +- `POST /chat` — JSON `{ "message", "mode": "auto|rag|web|direct", "history": [] }` → `{ "answer", "sources", "mode" }` +- `GET /health` — `{ "status": "ok", "model", "index" }` + +## Useful Docker Commands +```bash +docker compose up --build # first time or after code changes +docker compose up # subsequent starts (faster, no rebuild) +docker compose restart app # restart only your app (after backend changes) +docker compose down # stop everything +docker compose down -v # stop + wipe Endee data volume +docker logs rag-app -f # tail your app logs +``` + +## Tuning +- Latency: lower `TOP_K` in `.env` or swap reranker to `cross-encoder/ms-marco-MiniLM-L-2-v2`. +- Routing: leave on **Agent** for mixed queries; force **RAG** for internal docs, **Web** for current events, **Direct** for chit-chat. +- CPU: change `BUILD_ARCH: avx2` to `release` in `docker-compose.yml` if your CPU doesn't support AVX2. + + **Summary:** A production-style, multi-route RAG service with an agentic router that chooses between dense RAG (Endee), web RAG (Tavily), or direct LLM answers (Gemini). Docs are chunked, embedded (MiniLM), indexed in Endee, reranked (cross-encoder) for precision, and cited in responses. React/Vite is the primary UI (Lovable export optional); FastAPI backend exposes upload and chat endpoints; runs locally with Endee on 8080 and API on 8000. ## Feature Highlights diff --git a/app/backend/main.py b/app/backend/main.py index fd9a86b421..b943c039c3 100644 --- a/app/backend/main.py +++ b/app/backend/main.py @@ -242,19 +242,45 @@ def retrieve(query: str) -> List[dict]: def web_search(query: str) -> List[dict]: if not tavily_client: raise HTTPException(status_code=500, detail="TAVILY_API_KEY not configured") - res = tavily_client.search(query=query, max_results=5, include_images=False) - return [ - { - "id": item.get("url"), - "meta": { - "source": item.get("url"), - "text": item.get("content", ""), - "title": item.get("title", ""), - }, - "score": item.get("score"), - } - for item in res.get("results", []) - ] + + def _parse(res) -> List[dict]: + return [ + { + "id": item.get("url"), + "meta": { + "source": item.get("url"), + "text": item.get("content", ""), + "title": item.get("title", ""), + }, + "score": item.get("score"), + } + for item in res.get("results", []) + ] + + # Try news search first (last 7 days) for fresh results + try: + res = tavily_client.search( + query=query, + max_results=5, + include_images=False, + search_depth="advanced", + topic="news", + days=7, + ) + results = _parse(res) + if results: + return results + except Exception: + pass + + # Fallback to general web search if news returns nothing + res = tavily_client.search( + query=query, + max_results=5, + include_images=False, + search_depth="advanced", + ) + return _parse(res) # ── Prompt builders ──────────────────────────────────────────────────────────── @@ -311,7 +337,23 @@ def route_query(question: str, force_mode: str = "auto") -> str: def answer(question: str, context_docs: List[dict], history: List[dict], mode: str) -> dict: history_text = build_history_text(history) - if context_docs: + if context_docs and mode == "web": + # ── Web mode: context is live search results, not uploaded documents ── + context_text = build_context(context_docs) + prompt = ( + "You are a helpful assistant answering questions using live web search results.\n\n" + "=== WEB SEARCH RESULTS ===\n" + "The following content was retrieved from the internet right now via a web search. " + "Use it to answer the question. Cite sources by their title or URL where relevant.\n\n" + f"{context_text}\n\n" + "=== END OF WEB RESULTS ===\n\n" + ) + if history_text: + prompt += f"=== CONVERSATION HISTORY ===\n{history_text}\n\n=== END HISTORY ===\n\n" + prompt += f"User question: {question}\n\nAnswer based on the web results above." + + elif context_docs and mode == "rag": + # ── RAG mode: context is from uploaded documents ── context_text = build_context(context_docs) prompt = ( f"{SYSTEM_PROMPT}\n\n" @@ -329,11 +371,12 @@ def answer(question: str, context_docs: List[dict], history: List[dict], mode: s "If the context covers the question, answer in full detail. " "If it only partially covers it, answer what you can and note what's missing." ) + else: - # Direct mode — no retrieval context + # ── Direct mode: no retrieval, answer from general knowledge ── prompt = ( - f"{SYSTEM_PROMPT}\n\n" - "No document context was retrieved for this query. Answer from your general knowledge.\n\n" + "You are a helpful, knowledgeable assistant. " + "Answer the following question clearly and accurately from your own knowledge.\n\n" ) if history_text: prompt += f"=== CONVERSATION HISTORY ===\n{history_text}\n\n=== END HISTORY ===\n\n" diff --git a/app/lov_frontend b/app/lov_frontend deleted file mode 160000 index 0723c99655..0000000000 --- a/app/lov_frontend +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0723c99655e33d26c2c7512fae6f6e7da3ee93fe diff --git a/app/lov_frontend/.gitignore b/app/lov_frontend/.gitignore new file mode 100644 index 0000000000..14e1e79f2f --- /dev/null +++ b/app/lov_frontend/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +.env.local + diff --git a/app/lov_frontend/README.md b/app/lov_frontend/README.md new file mode 100644 index 0000000000..175fc2a0bc --- /dev/null +++ b/app/lov_frontend/README.md @@ -0,0 +1,25 @@ +# AgenticRAG React UI (lov_frontend) + +A Vite/React client for the Agentic RAG backend. It supports the same routing modes (Agent/RAG/Web/Direct), uploads docs, and displays sources. + +## How it works +- Upload pdf/docx/txt/md → POST `/upload` on the FastAPI backend. +- Chat → POST `/chat` with `{message, mode, history}`. +- Renders answers and source citations; uses 5-turn history for coherence. + +## Run locally +```powershell +cd app/lov_frontend +npm install --legacy-peer-deps +npm run dev -- --host --port 5175 +``` +Set the backend URL via `VITE_BACKEND_URL` in a `.env` if not `http://localhost:8000`. + +## Tech +- React + Vite +- Tailwind + Radix UI components +- Talks to backend (FastAPI) and Endee vector DB behind it + +## Notes +- node_modules is ignored; keep package-lock for deterministic installs. +- Port 5175 avoids clashing with Endee (8080) and API (8000). diff --git a/app/lov_frontend/bun.lock b/app/lov_frontend/bun.lock new file mode 100644 index 0000000000..615ad1f8fa --- /dev/null +++ b/app/lov_frontend/bun.lock @@ -0,0 +1,1119 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "vite_react_shadcn_ts", + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.25.76", + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@playwright/test": "^1.57.0", + "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^6.0.0", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "jsdom": "^20.0.3", + "lovable-tagger": "^1.1.13", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "^8.0.0", + "vitest": "^4.1.0", + }, + }, + }, + "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="], + + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], + + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], + + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], + + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + + "@hookform/resolvers": ["@hookform/resolvers@3.10.0", "", { "peerDependencies": { "react-hook-form": "^7.0.0" } }, "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="], + + "@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="], + + "@playwright/test": ["@playwright/test@1.58.2", "", { "dependencies": { "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" } }, "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA=="], + + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], + + "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="], + + "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="], + + "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + + "@radix-ui/react-aspect-ratio": ["@radix-ui/react-aspect-ratio@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w=="], + + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], + + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + + "@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="], + + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], + + "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="], + + "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], + + "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="], + + "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], + + "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="], + + "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], + + "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], + + "@radix-ui/react-hover-card": ["@radix-ui/react-hover-card@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg=="], + + "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], + + "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], + + "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="], + + "@radix-ui/react-menubar": ["@radix-ui/react-menubar@1.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA=="], + + "@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="], + + "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], + + "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], + + "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="], + + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], + + "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-progress": ["@radix-ui/react-progress@1.1.8", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA=="], + + "@radix-ui/react-radio-group": ["@radix-ui/react-radio-group@1.3.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ=="], + + "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], + + "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="], + + "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], + + "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], + + "@radix-ui/react-slider": ["@radix-ui/react-slider@1.3.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw=="], + + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], + + "@radix-ui/react-toast": ["@radix-ui/react-toast@1.2.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g=="], + + "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], + + "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="], + + "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], + + "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], + + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + + "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], + + "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], + + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], + + "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], + + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], + + "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="], + + "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + + "@remix-run/router": ["@remix-run/router@1.23.2", "", {}, "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.9", "", { "os": "android", "cpu": "arm64" }, "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm" }, "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.9", "", { "os": "none", "cpu": "arm64" }, "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog=="], + + "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.9", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "x64" }, "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@tailwindcss/typography": ["@tailwindcss/typography@0.5.19", "", { "dependencies": { "postcss-selector-parser": "6.0.10" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" } }, "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.90.20", "", {}, "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.90.21", "", { "dependencies": { "@tanstack/query-core": "5.90.20" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg=="], + + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="], + + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], + + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], + + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], + + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], + + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], + + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], + + "@types/d3-shape": ["@types/d3-shape@3.1.8", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w=="], + + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], + + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.19.15", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg=="], + + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="], + + "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="], + + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="], + + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-Bu5/eP6td3WI654+tRq+ryW1PbgA90y5pqMKpb3U7UpNk6VjI53P/ncPUd192U9dSrepLy7DHnq1XEMDz5H++w=="], + + "@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["msw"] }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="], + + "@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "^2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="], + + "@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="], + + "@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="], + + "abab": ["abab@2.0.6", "", {}, "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA=="], + + "acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-globals": ["acorn-globals@7.0.1", "", { "dependencies": { "acorn": "^8.1.0", "acorn-walk": "^8.0.2" } }, "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "acorn-walk": ["acorn-walk@8.3.5", "", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw=="], + + "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": "bin/autoprefixer" }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.7", "", { "bin": "dist/cli.cjs" }, "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": "cli.js" }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001778", "", {}, "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg=="], + + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + + "commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": "bin/cssesc" }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "cssom": ["cssom@0.5.0", "", {}, "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw=="], + + "cssstyle": ["cssstyle@2.3.0", "", { "dependencies": { "cssom": "~0.3.6" } }, "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], + + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], + + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], + + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], + + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], + + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], + + "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], + + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], + + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], + + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], + + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], + + "data-urls": ["data-urls@3.0.2", "", { "dependencies": { "abab": "^2.0.6", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0" } }, "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ=="], + + "date-fns": ["date-fns@3.6.0", "", {}, "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], + + "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + + "domexception": ["domexception@4.0.0", "", { "dependencies": { "webidl-conversions": "^7.0.0" } }, "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="], + + "embla-carousel": ["embla-carousel@8.6.0", "", {}, "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA=="], + + "embla-carousel-react": ["embla-carousel-react@8.6.0", "", { "dependencies": { "embla-carousel": "8.6.0", "embla-carousel-reactive-utils": "8.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA=="], + + "embla-carousel-reactive-utils": ["embla-carousel-reactive-utils@8.6.0", "", { "peerDependencies": { "embla-carousel": "8.6.0" } }, "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "escodegen": "bin/escodegen.js", "esgenerate": "bin/esgenerate.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "bin": "bin/eslint.js" }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@5.2.0", "", { "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg=="], + + "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.4.26", "", { "peerDependencies": { "eslint": ">=8.40" } }, "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-equals": ["fast-equals@5.4.0", "", {}, "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="], + + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@15.15.0", "", {}, "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "html-encoding-sniffer": ["html-encoding-sniffer@3.0.0", "", { "dependencies": { "whatwg-encoding": "^2.0.0" } }, "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA=="], + + "http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "input-otp": ["input-otp@1.4.2", "", { "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA=="], + + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@1.21.7", "", { "bin": "bin/jiti.js" }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "jsdom": ["jsdom@20.0.3", "", { "dependencies": { "abab": "^2.0.6", "acorn": "^8.8.1", "acorn-globals": "^7.0.0", "cssom": "^0.5.0", "cssstyle": "^2.3.0", "data-urls": "^3.0.2", "decimal.js": "^10.4.2", "domexception": "^4.0.0", "escodegen": "^2.0.0", "form-data": "^4.0.0", "html-encoding-sniffer": "^3.0.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.1", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.2", "parse5": "^7.1.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^4.1.2", "w3c-xmlserializer": "^4.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^2.0.0", "whatwg-mimetype": "^3.0.0", "whatwg-url": "^11.0.0", "ws": "^8.11.0", "xml-name-validator": "^4.0.0" }, "peerDependencies": { "canvas": "^2.5.0" }, "optionalPeers": ["canvas"] }, "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lovable-tagger": ["lovable-tagger@1.1.13", "", { "dependencies": { "esbuild": "^0.25.0", "tailwindcss": "^3.4.17" }, "peerDependencies": { "vite": ">=5.0.0 <8.0.0" } }, "sha512-RBEYDxao7Xf8ya29L0cd+ocE7Gs80xPOIOwwck65Hoie8YDKViuXi3UYV14DoNWIvaJ7WVPf7SG3cc844nFqGA=="], + + "lucide-react": ["lucide-react@0.462.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" } }, "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "next-themes": ["next-themes@0.3.0", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18", "react-dom": "^16.8 || ^17 || ^18" } }, "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w=="], + + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "playwright": ["playwright@1.58.2", "", { "dependencies": { "playwright-core": "1.58.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": "cli.js" }, "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A=="], + + "playwright-core": ["playwright-core@1.58.2", "", { "bin": "cli.js" }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + + "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], + + "psl": ["psl@1.15.0", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "querystringify": ["querystringify@2.2.0", "", {}, "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-day-picker": ["react-day-picker@8.10.1", "", { "peerDependencies": { "date-fns": "^2.28.0 || ^3.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } }, "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "react-hook-form": ["react-hook-form@7.71.2", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA=="], + + "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + + "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="], + + "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], + + "react-resizable-panels": ["react-resizable-panels@2.1.9", "", { "peerDependencies": { "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ=="], + + "react-router": ["react-router@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw=="], + + "react-router-dom": ["react-router-dom@6.30.3", "", { "dependencies": { "@remix-run/router": "1.23.2", "react-router": "6.30.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag=="], + + "react-smooth": ["react-smooth@4.0.4", "", { "dependencies": { "fast-equals": "^5.0.1", "prop-types": "^15.8.1", "react-transition-group": "^4.4.5" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q=="], + + "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], + + "react-transition-group": ["react-transition-group@4.4.5", "", { "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", "loose-envify": "^1.4.0", "prop-types": "^15.6.2" }, "peerDependencies": { "react": ">=16.6.0", "react-dom": ">=16.6.0" } }, "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g=="], + + "read-cache": ["read-cache@1.0.0", "", { "dependencies": { "pify": "^2.3.0" } }, "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="], + + "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rolldown": ["rolldown@1.0.0-rc.9", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": "bin/cli.mjs" }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "semver": ["semver@7.7.4", "", { "bin": "bin/semver.js" }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "sonner": ["sonner@1.7.4", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="], + + "tailwind-merge": ["tailwind-merge@2.6.1", "", {}, "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ=="], + + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + + "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tough-cookie": ["tough-cookie@4.1.4", "", { "dependencies": { "psl": "^1.1.33", "punycode": "^2.1.1", "universalify": "^0.2.0", "url-parse": "^1.5.3" } }, "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag=="], + + "tr46": ["tr46@3.0.0", "", { "dependencies": { "punycode": "^2.1.1" } }, "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA=="], + + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "universalify": ["universalify@0.2.0", "", {}, "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg=="], + + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], + + "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], + + "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vaul": ["vaul@0.9.9", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", "react-dom": "^16.8 || ^17.0 || ^18.0" } }, "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ=="], + + "victory-vendor": ["victory-vendor@36.9.2", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ=="], + + "vite": ["vite@8.0.0", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@vitejs/devtools", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": "bin/vite.js" }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="], + + "vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom"], "bin": "vitest.mjs" }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="], + + "w3c-xmlserializer": ["w3c-xmlserializer@4.0.0", "", { "dependencies": { "xml-name-validator": "^4.0.0" } }, "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw=="], + + "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "whatwg-encoding": ["whatwg-encoding@2.0.0", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "whatwg-url": ["whatwg-url@11.0.0", "", { "dependencies": { "tr46": "^3.0.0", "webidl-conversions": "^7.0.0" } }, "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": "cli.js" }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "xml-name-validator": ["xml-name-validator@4.0.0", "", {}, "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw=="], + + "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-aspect-ratio/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], + + "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-progress/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], + + "@radix-ui/react-progress/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "chokidar/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "cssstyle/cssom": ["cssom@0.3.8", "", {}, "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "postcss-nested/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + + "readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="], + + "tailwindcss/postcss-selector-parser": ["postcss-selector-parser@6.1.2", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg=="], + + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "vitest/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/app/lov_frontend/components.json b/app/lov_frontend/components.json new file mode 100644 index 0000000000..62e101166a --- /dev/null +++ b/app/lov_frontend/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/app/lov_frontend/eslint.config.js b/app/lov_frontend/eslint.config.js new file mode 100644 index 0000000000..40f72cc45a --- /dev/null +++ b/app/lov_frontend/eslint.config.js @@ -0,0 +1,26 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "@typescript-eslint/no-unused-vars": "off", + }, + }, +); diff --git a/app/lov_frontend/index.html b/app/lov_frontend/index.html new file mode 100644 index 0000000000..0ecb364b2f --- /dev/null +++ b/app/lov_frontend/index.html @@ -0,0 +1,26 @@ + + + + + + AgenticRAG + + + + + + + + + + + + + + + + +
+ + + diff --git a/app/lov_frontend/package-lock.json b/app/lov_frontend/package-lock.json new file mode 100644 index 0000000000..97b839e0ec --- /dev/null +++ b/app/lov_frontend/package-lock.json @@ -0,0 +1,9968 @@ +{ + "name": "vite_react_shadcn_ts", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vite_react_shadcn_ts", + "version": "0.0.0", + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-markdown": "^9.1.0", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "remark-gfm": "^4.0.1", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@playwright/test": "^1.57.0", + "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "jsdom": "^20.0.3", + "lovable-tagger": "^1.1.13", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "7.3.1", + "vitest": "^4.1.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz", + "integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz", + "integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.5", + "@floating-ui/utils": "^0.2.11" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz", + "integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz", + "integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==", + "license": "MIT" + }, + "node_modules/@hookform/resolvers": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", + "integrity": "sha512-79Dv+3mDF7i+2ajj7SkypSKHhl1cbln1OGavqrsF7p6mbUv11xpqpacPsGDCTRvCSjEEIez2ef1NveSVL3b0Ag==", + "license": "MIT", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-accordion": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.12.tgz", + "integrity": "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collapsible": "1.1.12", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.15.tgz", + "integrity": "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dialog": "1.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.8.tgz", + "integrity": "sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz", + "integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz", + "integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menubar": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.16.tgz", + "integrity": "sha512-EB1FktTz5xRRi2Er974AUQZWg2yVBb1yjip38/lgwtCVRd3a+maUoGHN/xs9Yv8SY8QwbSEb+YrxGadVWbEutA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.3.6.tgz", + "integrity": "sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", + "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@remix-run/router": { + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.19", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz", + "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==", + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.29.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-rc.3", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-7.0.1.tgz", + "integrity": "sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.1.0", + "acorn-walk": "^8.0.2" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.7", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.7.tgz", + "integrity": "sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001778", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001778.tgz", + "integrity": "sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssom": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", + "integrity": "sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-3.0.2.tgz", + "integrity": "sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/embla-carousel": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", + "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", + "license": "MIT" + }, + "node_modules/embla-carousel-react": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", + "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", + "license": "MIT", + "dependencies": { + "embla-carousel": "8.6.0", + "embla-carousel-reactive-utils": "8.6.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/embla-carousel-reactive-utils": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", + "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", + "license": "MIT", + "peerDependencies": { + "embla-carousel": "8.6.0" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", + "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, + "node_modules/input-otp": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", + "integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-20.0.3.tgz", + "integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "acorn": "^8.8.1", + "acorn-globals": "^7.0.0", + "cssom": "^0.5.0", + "cssstyle": "^2.3.0", + "data-urls": "^3.0.2", + "decimal.js": "^10.4.2", + "domexception": "^4.0.0", + "escodegen": "^2.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.2", + "parse5": "^7.1.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^11.0.0", + "ws": "^8.11.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lovable-tagger": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/lovable-tagger/-/lovable-tagger-1.1.13.tgz", + "integrity": "sha512-RBEYDxao7Xf8ya29L0cd+ocE7Gs80xPOIOwwck65Hoie8YDKViuXi3UYV14DoNWIvaJ7WVPf7SG3cc844nFqGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "tailwindcss": "^3.4.17" + }, + "peerDependencies": { + "vite": ">=5.0.0 <8.0.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.462.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.462.0.tgz", + "integrity": "sha512-NTL7EbAao9IFtuSivSZgrAh4fZd09Lr+6MTkqIxuHaH2nnYiYIzXPo06cOxHg9wKLdj6LL8TByG4qpePqwgx/g==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", + "integrity": "sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nested/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-day-picker": { + "version": "8.10.1", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz", + "integrity": "sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-hook-form": { + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "license": "MIT" + }, + "node_modules/react-markdown": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.1.0.tgz", + "integrity": "sha512-xaijuJB0kzGiUdG7nc2MOMDUDBWPyGAjZtUrow9XxUeua8IqeP+VlIfAZ3bphpcLTnSZXz6z9jcVC/TCwbfgdw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-resizable-panels": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.9.tgz", + "integrity": "sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==", + "license": "MIT", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/react-router": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readdirp/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/recharts": { + "version": "2.15.4", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", + "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonner": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.4.tgz", + "integrity": "sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.1.tgz", + "integrity": "sha512-Oo6tHdpZsGpkKG88HJ8RR1rg/RdnEkQEfMoEk2x1XRI3F1AxeU+ijRXpiVUF4UbLfcxxRGw6TbUINKYdWVsQTQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tailwindcss/node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.0.tgz", + "integrity": "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.0", + "@typescript-eslint/parser": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vaul": { + "version": "0.9.9", + "resolved": "https://registry.npmjs.org/vaul/-/vaul-0.9.9.tgz", + "integrity": "sha512-7afKg48srluhZwIkaU+lgGtFCUsYBSGOl8vcc8N/M3YQlZFlynHD15AE+pwrYdc826o7nrIND4lL9Y6b9WWZZQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/app/lov_frontend/package.json b/app/lov_frontend/package.json new file mode 100644 index 0000000000..887e6c179f --- /dev/null +++ b/app/lov_frontend/package.json @@ -0,0 +1,92 @@ +{ + "name": "vite_react_shadcn_ts", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "build:dev": "vite build --mode development", + "lint": "eslint .", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-accordion": "^1.2.11", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-aspect-ratio": "^1.1.7", + "@radix-ui/react-avatar": "^1.1.10", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-collapsible": "^1.1.11", + "@radix-ui/react-context-menu": "^2.2.15", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-hover-card": "^1.1.14", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-menubar": "^1.1.15", + "@radix-ui/react-navigation-menu": "^1.2.13", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slider": "^1.3.5", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toast": "^1.2.14", + "@radix-ui/react-toggle": "^1.1.9", + "@radix-ui/react-toggle-group": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.7", + "@tanstack/react-query": "^5.83.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^3.6.0", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.462.0", + "next-themes": "^0.3.0", + "react": "^18.3.1", + "react-day-picker": "^8.10.1", + "react-dom": "^18.3.1", + "react-hook-form": "^7.61.1", + "react-markdown": "^9.1.0", + "react-resizable-panels": "^2.1.9", + "react-router-dom": "^6.30.1", + "recharts": "^2.15.4", + "remark-gfm": "^4.0.1", + "sonner": "^1.7.4", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "vaul": "^0.9.9", + "zod": "^3.25.76" + }, + "devDependencies": { + "@eslint/js": "^9.32.0", + "@playwright/test": "^1.57.0", + "@tailwindcss/typography": "^0.5.16", + "@testing-library/jest-dom": "^6.6.0", + "@testing-library/react": "^16.0.0", + "@types/node": "^22.16.5", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@vitejs/plugin-react": "^5.0.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.32.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^15.15.0", + "jsdom": "^20.0.3", + "lovable-tagger": "^1.1.13", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "typescript": "^5.8.3", + "typescript-eslint": "^8.38.0", + "vite": "7.3.1", + "vitest": "^4.1.0" + } +} diff --git a/app/lov_frontend/playwright-fixture.ts b/app/lov_frontend/playwright-fixture.ts new file mode 100644 index 0000000000..7d471c1937 --- /dev/null +++ b/app/lov_frontend/playwright-fixture.ts @@ -0,0 +1,3 @@ +// Re-export the base fixture from the package +// Override or extend test/expect here if needed +export { test, expect } from "lovable-agent-playwright-config/fixture"; diff --git a/app/lov_frontend/playwright.config.ts b/app/lov_frontend/playwright.config.ts new file mode 100644 index 0000000000..ec19e95967 --- /dev/null +++ b/app/lov_frontend/playwright.config.ts @@ -0,0 +1,10 @@ +import { createLovableConfig } from "lovable-agent-playwright-config/config"; + +export default createLovableConfig({ + // Add your custom playwright configuration overrides here + // Example: + // timeout: 60000, + // use: { + // baseURL: 'http://localhost:3000', + // }, +}); diff --git a/app/lov_frontend/postcss.config.js b/app/lov_frontend/postcss.config.js new file mode 100644 index 0000000000..2aa7205d4b --- /dev/null +++ b/app/lov_frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/app/lov_frontend/public/favicon.ico b/app/lov_frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3c01d69713f9c184e92b74f5799e6dff2f500825 GIT binary patch literal 20373 zcmd3Ng;!Kx)b^bjV(2bK7(gT?m5!k#rInBt2|+^XnxOsP6x- zw@ai64m5ISN^t}7cPtzE7V_?#ZW#yim*LeTzGTah1HiD#BtPPTMzSOKTZiruSa1Ex z+8>11eb3@daF&ewih_h5sT{(7jPbh%#LLnI`058(B|`;p4{@lU80@O*7eE(8NO%|c zbw&p7AWM)84}wHr-|iU=JrTr&@AO$w?xB7VR5sC3{D4a*YO2lIpO~!GloU<5bfvpcrDm)vtN-mL8-}e1_Faa$9y6>_`_P* z1Kua>b&4G6CMXi3KS^0(6fZwZo|fU~87nDonjn%p9%J_-TD$m^gLTg9Y)nHZJzF9s zB!E%yO^XOc&ddo%mA3Y-DRxA>c;CM1@-lALvy{z`v|kgn9w@HaDL&F2*w`t>@4qjgAn! zq&cP`7NrDkk1r$x@sQ|F0`#-X(nh(UiO$pRFY1B2sZ#6~h)u=7nCKu1)B)tkhBeLd z5@l7Y3#-YuLes|>pF9#?!Pm6!;TnVVpjISZ=L6boLDyv$ADH%qQiwg0n($-&@(i!B zlsO0qj{t4gDMqo9u<|S&)voGKIuJv0r?cB_9+O{2W{Pde! z7vV64-9FAf0|)6lWzXiMziuZlUG(t%?7&+wz}8(xchTG%Zb#(6Vr?>Ng-#oki8|d- zJnwph6$TWQ&v&T@1K-TTL zFf5SnsB58vPNw_t#sXpCki&4dS*muAHB(ZXZdvQYFqgY{(&&vo-D4u(B+b*pv$4a%vr-JH+Q*t}p3mLUO7MXo{0)l(x(&x$Pl#`7STaJUbW72 zf3oERzJC`E7*Em&xs5v4*=gkpBFt#Rp!uqKDd0k6bi25|p()3(512!z$jdX{P$Kl& z@iMXEaf8LHrNL@?0O=dDz;rlvbP7|%7Hw}lZffK>56K*vkIhQxNjgyE-dX!LlnE1b zhf~aPW)TfE4#ctt#m@#zM7G8h0syPW(UA=%k8eQO(ZkV&{M;=2AhY(vwaYE{cnB)a zj!(-*ID;9(ZZzl~FkC2n<)`1FipPqZsBHzu)S3WOiU)d~vR$d+L5)eGh(x=;(E_4< zTWz5^uO2sMlJDZMcifcjMb;V+%ys^$P!7!&!Lz;$}JK97{C%w;a0-^t$sOJ=Vj$H8zcxBpgc3X=!>aybKvEBjjQ zfb!U-u|%$Zz^pkH@y`b^cF&2RG>Y!jM9&uk)pm5GnW(H!3uF{-!o>QEnA$O6Mh?IK zUIx|^OMc;}j95}Q)oO79$ z%e!MgPN!h!d`AqfUH`?#cRV+-ZsL-9MhJJNBDu^QzdE4bs8#4I(|(A)JB{+#x4P74 zop!bE<8$2!asQDv^hFd$cmP7{4W=qIH6bxj#}YsYzndx#9P5Fbic)I!OfSE9p$lN& z`a28OyXPxOZ4Zp*a@YVVB{n%Nr4CuU%dhaZ4NL#8k+Wg7#()h*zd9H!8P<{Xt2UT5 z?m0POsR-4nj3JxbK&VF^bMmQy0f&fH)Cd< zjJvw@p5HgJ1utLtfDaempw~&d+VH%ilH=eY-)^Mh-5Uu1^Eg)b4m)af+gYue%XX$6IsLz&v+fuqAh-N6%@u*6(c6;Pg zF0%Pew`ca6Y+?5ug1UdFb3-U7C$c}f))45Abvd9org~q}>4h`12(b-kR)=X&+K^K@ zeGACrEhfE?)3@i+POZjdc%XIyN+tksfpaH`n#Tr77ZpLZ9};szoB%9pk*eVwL1Mq5;tP!D$AKjCHD7_0 z;3|iuvn8q@+93yf9qZk%`kMKW@zeEZ753ouv=Ac#;~fIsLIN^(m{Grt>ummNL&VYg z4^e=pW7=nGc;Mtq9>u?QMS^zgR+Sqn%J7gNg+VgV6l)cso-RNoO zia#GZ{JqY&WBl!M_q!P}eBq!02N9a1xoOF1?+`E#%ea(l1Ryp2Ac|)SNw`AJ@B_ObdOJ{EzE3; z%u^D2dBMZR|5G_$`Y!W%O%b^yTc~1YDGi*o#lokpT(SC%t6xZ2J&P0XR&u1^yWssoP^8LD)ES_c~>FXsWJ2tT*vj5K_Un{BWOAyHRapg$3j zAv22#ojeh65IcCZv3n_TQ9*9)>0TF0UX34&Dc*qX z`(Iul?;maj_^?Q|%~`8r4vBvv&5GRj?2u3OIwGMUF&3o{WlH)?9pXNF>ZVLZY>~SQ zBhVufwu*f(D0c|)+^45?l?v1LBSz5Z1*JR-jS}+>A;0W?g}Q0jdyfzW+O+(RwE7j8 zSCI1&`B60FK4n-Sm@XS;n2c;(oy23!+0hH|#9s+rv=~_;1sg`g){#d_Hf03Z(7cJa zpNxC_7S>M#`#qP_5F`GJ-SY2F41mY;0q^7xHTV&VoY3}U;CmaPFRpNxifK_kG>XH|DAl?%g3_QHcD};yu?yWxdvWB+$gO_MD90mlp+3&)9{qUj zUPRvFPl(f>yP{>xF~1&!_z{-}LOzjs_%Q=El#|3tLJE!1lVU(VTAIaAMM?3fb@vam z>)^Y>pa!$wKlcW-w+{p|zku}{7YRDkBl|&8=J%0)kCClM;JV64m5T7-kvGkpsHVjT z@fZ5a$Oqv70ZX*GcC|WrQFV!@q7o5hzX)lgv05z#^2JYP>K^@qt-iv^B`OBAADo#E zw&6#@K*JEIOSUS^mNoR^q@5^t5cG#W=Uwm*R(oFk-(m2h)|ykWene&;#P!_0DI!`@cSCkJiuO z52xU%ro&)*i5RP@qXe^GZ~`l=Ky7q+B|~JYT)=eTZyFRL~WB6BWAUSg91tlXFh~Gm5a5D3VbLH6nc%DC_Am z859Ye-?{OUe%A^afV$(EHUdc&B#X5Crc`Vm$aH0ciNzdfR_9XM+#`E&m(bvMGzw1o zmQa3)=Ffoyb4b@1`GYLyetzfZt+_j5`r9rw+-$xJ4omp|P!zzS-}oqdzcPrAq~XFk z^RhqJj{qMXM810nIyI%4Nz9Oa$t-5En@n3g$*zg#X#7N$_RBkiTnT4T6yIVZ=f%Hp za6&jprd!C(v@iW1HQ6)5d7m;`@PjP}v}dT2D^7idP#F;~@8z^Tlgr$^*1MGImOX1K zVM+THrea=Y#cLKUy7bj-r<|>$;1Mc&R81k(uJrk|He}(?ttc{+ceMtVJ z{?dc0l>?=#P#w~D+x(QT4Pe{0J_LDXSH2B-}1li5^Je}QJ-7IB&bXkok`bB>CTl5;<_B;0N+irKTMrkysTlA2UfUlzRy zUgY6w;|6`7SQJlgaDc_ud;hh$5B?ai$H`!rMfD^^W9{4T+X=14ESqcMYzeOu2l$g0 zMJV;$8#65DjKJU3XJ~v7?+0R*l8X;h+4)D-Kq%rEG=w9l190zb@?n@13nIq*ZWo3z7k*l+71QhAO~~Bjq$O|B5n6!|#l#&Ds~0&l6Fsg@{odNIgiyCxuH zDEJ<{QvIPf_GpLm8;_Kb*dFdb%>soc;my*OYas--dPCwLMW*E(@lCjmpn#iH4>hJwP(p2b zcAUn#PHms07h;3iS!)giO$bzvviy4YrL@CP!J^7qfM~@#>}u`pU)b+0$XO;xx;}XD zRG1W^8#7Xst&HqaDbb?L{{>pqix37~@KhoO)jMO*lI#~f*Qo3uTz+dd8;{g&Dija? zLYdv}GVt9vdW*(BJmJ}u!&AIXib6$80Cg_}y|DNYTJ_hCYoB5I;ZL%$QM-N-p+}r@ z_IJM58Jp?wm&Ru(27aO6QS>A7(Pi!Lh$N~xx_5T=rTR~r-20_^Lj|s17-ofh9lF%L zpz~z=!?^Vy!xS?5x$<|MtmntA5bf4MDM8!)S*Nt?H=rSS9PwDC$K$*V)u>6*6N)V~Fyxu74W?s><_fs&XaD|Z!6Hx+VM>^Db5KD(h}cvg z-?s?ho3zBXd0^(d-Q_{fNngjXx3=yitn5hMz57v$wHt_bbao5- zV~`S~OY=T85(p>MGeKBLy%4Tj`f+eCHN^Vqx#HyOX5V{1CVCYLVlCdAwKb@; zC;h~+2sm{cw1c=ny$i6n5z0CHKe-oB`cge}>?!p{_)3?Hh67F*e;v zu!sw({hVTEYt2;}y}>2L{B1bCsPb;m7a}7#6|i?Za_@U3Vg_-#rgssgc*K4%J?uxd z6hsdf!BsD_$)XqXShYwx(oPwM*kHuCNjLbxpD%-_ptudphpoRLz3j`!(@q>pq|NBG z%F-wTPw_x!_hItnw(CbUKuMy+yED4@%(Ld4$4}r#rX%qjclmk@N4Ucwfxx#XV`_z_M6@JHP?Md-=lkRXNO{7@M)p)mF8TY`8uKLVHw?17YlV_59G7v+{JF6P5AqjaVhwM5w za=Zh+br1T=2fa%}ABi%MQ}258_aNh`01Erc0qh|t%iLICeqkw`*K#!7f;oFu;~L+y z80U+=3oAp#x2ZS0mqhL*g`W8?oU&wNr1h|g>=%2jeq{?!ZiL6CtF!toRn<*a-eEcvBWQGU(0WdMoQM-Zu{5P6gf0v@6J2M}Bx{%(Y;e zMJ-Euu5=&ty&R3an`!j61O{!;qpv$aOr0oRN9tjh4ni#6LjLnW6m~LJH_H5duIk0+ z>$USHlf7P!Ry*iR}jWXc;xe^V)K`Y5%!BHI}`Y zHM_h+L;RrR4^u=QuPv&su5~mPnBgJ2oMOk9D`Fz`t}u(2BdxD%7u0UXFPc0czw(*~ z{Ebjk@BHzW=NfY(Wto3^;tQ|sUu^*88}+;pg>A^6Te;~uR3&wG^Ujs6W(cjQII8ED z_v+3Z5Gt?qo~nt|U^(VKL3fL|V#JuSCwGKxd?|C@JMPI)1jg989D(d{;eQas=K(gI zdK}o&*!X1{ctu;5Oe@y;NRI@;NPY*XVZ&5{V}JMzyX+Iqy6X>%c*o^oO+T+yiDe_S{_bM>TTn#m|3z`$#Xk zAS#j;vpyZo=wWJ@wnPp&*%O18m@I(1KL+GKqC;}Q)WMypPw?VtZoF76f*jMB-iAcK zVP+k)xg@5o?n-^jZi#S^N1+)mjtn_Stst`qy#{OWA;cH&)*dKzn-KlEL!0=Ob7dMC zboy7|x7J(dV?+nC0e!pM@75J+GK|@HOyu!3qm^N-4&!mUndc7U3mtE_=I6{T7 z7nP#*Iv(d6Yn`PYYEVfZOx@sqInRWXyvFh^UO#qk!uTG3=`!O3*dr$jZioKKGqL^} zx%Qz$FIh8%dm>x5o(_H3sz4|2~% zVCC3V!luG_s+Tb+M)-&1d!@uSzdy#RiZjZH>TAuuzPIT!M0mD*?J++T_f4;qws;G^ z7-wungWbL|35%bY&t676RnyOU6SDB$kHQU|G&YlcJ+FAGh^_yM-7}ecN$xeebQ5Db zf|3o~ovcK@Ki&3!AS=@FBNgse(q)F-E z{{TC#euqmMAC*#!kv3)M;YCZsd+xU;1#R&~d%t9S0h>^vZXjy7)}5(m2OSQX1Y0XU zy}|4NNys2pR#b26UK)QY%3zeY9(cDpwWF}e(qpsXktDVpoJKD5d)^gfcnUCEA^j<& zK6i`R>B|Y~b0&lj9mLo?h5lXrh?_hh zTs8TZZDjP~^Rt73%>vjktrvBc@A+On?owgiK@t2qQTobH?lX-2#9uK>ff3OzckmrA z<>uMhgiIYR91ZR~DC0vpKfAXS?_(qUx>yeSI`B6Y?E4ZZ{FAQ^2YeVeO3E?)R;$IW z(ZzcajyIFh-;mcK$Ppl5tA4^r(S1)a7no7%HnIs+9aGq8$FG}Ud!8)L#uIL=fTCND z19n`XW92V(RuC^{TALSsfM9~wg0)L-aKHL`z0PmbYr@K5w}k#=m{|rT68$TB2$atu z`>r_EhwajsawNUf>B*W;H_8bDG%?;%_dk~-j)1%(e@~TVo-*kWHQtOGp8b9S>jjynS)sBe(0V;5ahLbHMLn$V$c8f0KkppXk$ z7Z4XleZAs%i77S&=G}(EO9YiXJ#0A@xI>Ju<83I6d zsx2I1Aanv>T1a%*-=#n7`ep1mKXYvE_}BF_Zle%UrRZlqVuFF>JyAJF=kghL0X_N8?EZgD*jNJ7tGIn0QfLeiu02cJ=it3bBJyJ!B^~Yvp@ti zvF+$9FToLl_^;S?^I1Mkb-9&NLeN-uTqgD1@M=0~orXeXWFR3$_gPK%St1a!RC;iv z9)2^T>U66_p-{E4>9O(ev|hDsmK*1?pn;-Db4Hkmgi2 zDOM6zU^OvTCpc!=+JB1**InCrDPDCY(fQ&(IrbEH4|c0H;HPUT^?(xzk!?D_1^yuT z{(Wlt<=ZSZl>(AR4ES#ktu2xypx|Fv(@W>}K;n|hjse@cLk@Jm;L+(Wz%33cx=W;o zDsQJZ)DPpWdp<`RxW@q4xbOp3BV2G-Bdt5~^^7XY1b#T}lLdzq98m6(Ok3_H7LIsiy=f+DSJk zocb3$8s6d!Nq$i`|G4olP{|C{x zEOtrt*Fx^CjtjufHglD2@k?$Y6;#ag!8Mgii8^F0B2;tmCGa4JY7(Z_ld??altwqAeY9V zY+|%*)s;f$|BBs*77sZXCF*QOU;R+5rPZaB;Cy;Ua$C8bJD6Y3gW&M(oz5rm4ogO9 zU&NtImQv7$r+Ifi@OlX-o`AOR=u@I}E#ZGo`gbt)OY=sDI-z*6`iFiN&I7kXl=*D# z62-1kc>(?EEV{oSV&2zX_~32&oR^nxZbLAN^k;ugq}I-CQ`$_gq~)k*bfg;b>4elP zv=8v^#CPs}mFseB+ZqbFXw5qPFxpu)6FGXr_K9?%0R0Z-n{&0|hpjJJ#^t`VExjs(`0kN@^{$|%NU zanWonFHoOUBp?0tR(u;N!|RL_kKD|K$F&quf!`ThG+eIh#|O;ygsbQHwuX{5tGB*& zByYQ3T)~?&8}k#+d`TQFWf%iTjbP+W6xja6k+QWtD8NTwZ4uxs~oS5wkND{pjhRF;O4yfW}9G2_{zJVsQ=j+GqLliNNf zkLg&y9g>!lk>{~Al>nFdjKu0K;#-5j`}uh}|GC+rza^&67=jW)<_X3!B0h7IKh#bT z2V)<-O6Mong$vTzjFcwNPRRZ>VTQ zP0C6B&Jm7RrWV&wmNrUwrugA20hr->WGMmiESDfFtPt3jXC42^S@OnNuDKb1wfS(% z+5I!{rj@})joup_N1N{J>-wul;qjDsM%C>i< zO*K@`84Ch{KCJi@!N4v2`>l3lKUp|)l2&c?S4L280lN5b=V;g^ag28}<;P40iH)6L zF7->ONNz&-itX%p43CHz9!B&v8BM*yq=%YteM=Ncl?J_8v9LsHQ-1DvqRRiXp2-Bt z_pf7-9+~x9DTE*e+;r~2@i;1^yW8?&S|L|McbUmxXW?hnX$*X$YT`7?jIm~0ihEa1 z(if>g_mxBKbK^4u_0^InIH{u#R5!U5|+G*tk@{j^xyl|Q=gID4j7 z-~LN1P2JRp5xM@AUt+D~SF}(?x5i?lUM_U5V!yX&LlLLY*DuyYZ#V5w*a#7nRwAt6X}{C#?B1l4 zRr3)$7ywF#pKgVw?+FjWXGMR0V=`HM9=yV?Jk6b~%GzlCSB$V*X!S+~SamYofIs?Z z!ogat;4fJ5f!Nq1EQ@qn-b%7%s+U|vY|fma=9Xsj9ZmPA4^29n@C5<{qK%7e`DZz) zub37pCyIM-d&CM2*s=3mYzrcP($Y_M76HU$)?^W9L69*bVmQi(#+8d5`;kf0k7)mUg0pXhmD`BaKNbHTu#YL^AQx@b~m5p5@(T_6&(_ z>x?A1cUbAb%Vu>U*ZGsEdi6y56dR?T?UMYRiIhbv#QW69qkm5?#hxE*cYo`L2rANV zg`{AYl1ED(s>%+OOF21FmxfhZ6HS!rnSp3N{*;ir)r?UD-ddnONX zLW=fL0?H_i8kFrbaeh^ScBMz%vJ0zuh%jFBSddL^SoUlPf&FVUya1sL0n_ocd=#8v#`nv{1MWwDIw9j+Qy%AJ(q zCH7kk4nLLG?kYrQXAIg5y zwjGFfxGAThm0c%<-@Ooh070=~J}0z%7V4cMJ*&%Z*)!Wq5$wz{+kmj?#i%*su zUbIfhLX|L1h`uS_1BYYU8(3!F>J~Dv9rrtu4dO&%?3NP#;|QmNq0rByUOSWRm+BSm z{GejOUni#$^)E4|E)Dq~krg*|H5l}4|2en%KL^+oVWEPobCbw%PSwMY=DJ#_igtzd z6Mcd#(L*ARWIx5v;r5?TO`tz{Z%^W$`3P@7T<3z5H+t{u@8|QA^m$OTGt*8gcC=b1 zS)qERI)$i;LYSYUv@)%~K}rB>&(3E!zxI;NkLzWV3qkxoLqJ+sf?Fm$6|%v;oz;6& z*xi`ueT(oX8&Tqtr}@Zf7aK|Y*vkjG*gAiS2Rnh;dyJ(vF) z`S7d=#uVZ^>^$_TzG(tQe^v^V9)3j_X)2kJ-kRA=7_ZwTKR^iv{-J=)7ZM;Q^3`WXvUs$xUoZ>yfAiDQe|sQ=|ZthHhC#>!$#8i7uF@>s*!h`|(#37^__L zUED31>4(8Opy}b;o0HG}5+-wHdrUN99^cgUQ8lxr#}THNI@J9XuvU0+jRH46;d78% zqq!MFnl0Q9GJk@#dqecIKl;L2tJ!0Udr+V3-J_TMckGS8A!O>iR`Owa%rAr7yRFM` z#!;@0*O)y8V+c4`M@)W`iZ)e4<$it+6S*j&=RzZteL#8i$V4DJ(`5 zWsc*4Z6haJ?q=Q$Up-dC zt|m{kHPz-94cX#Ry7DhJ_~Tdruw7 zv)dtj_uBJd$tW}-5Z--w^07;YuVRNFXJ1{RD#F_0RS;`~|GNp{Y}Y}8P1)geJL0M9q*!iLammYDf6KgK0KYIt#1w(`Uk@Z`a9N?@skuiq*CKZBpG-nrYXroTg%Wp zA_}(4sj=XQ_=x7k{woJ1Y{3=T(+Q#OU5EsORb>)-7dB_5YtIiFZnfLMZ0pDDkvI-P|-ZfTkM^@>l^ zHd8{Y~}us8Ql?W8N94^?O_B-=SafT<{AhS;Zw zgK*z9mLkt>v}`q2KG4rQjs9kWI)<*W&@QMn4<_7Vy#$rST< zD~$u`q&?0}3Gr$Be1mBX@elmKyi0&#QNAs7_C4~eh5mgR&UB>R_{y#6B@m~P zel3s{e=--7^_%#`(q%~DgI-A0D&a@L_ip??+4c6Mxv{6uc$WHwChcb;)oX~zl|wH7 zua?Z`0|efUlLng8u$HUGXKhT`hf+e1^x+@2K~{$<&UM`!+V{IF9Z!j#%IwTX)%&34 z5%D{6yaT5@;!6N@o{qd}{SsB!p zZ-gEe?MaTtzoIJP`2Ga+ob!mj=xF4Pt&@}jzg06*9v)IFDG~;$ z7B|kw_}Y}XX_LgUB1f5{d+t%&LbPN=N#89`rx~!vAe9FMa(N=B-@MaRLuV>cBOV$b z>RDcd)nUyzKcxc^=nU-S-E|auL^WBMCts-!=ajo=`>5FVU!oT>+{!>8nfFPnJxeI# z6`EH@U_?HGvYt=fVCeu5;=SnXxDZ&}D0M%5)_p^3`3nVKIESP04qH-Ory@NSj6?k> zA!emwtCy5CHDT;buaCT+x3zzrBth*@p#jy_vGy!f@Z0su`kt%Ct2RHPma%Ca+9Ksc zbj$GuoWI33=W9q^@#gLP-GD)68P_<9?p{U5V-WwI{@78T?$vBCqr~@N`^T# z4*sv>ew1g8y&fShslP3YcRp^$DC zPQ7p83vUm6Wav`8pY=FxJ^=3S7$}jtOit1ey%IYi{9jfnRF*e@GenyFKr98oaIYkh zSDpQU*Yekjpn^Y_M>DsP#YBr=cZH>K`|^Zo{VVAf{ir7oGB{r-#TUfZ;M^`bq)7b; z{5TR2lWYl%mZ!lXWjFTPh=YQ=SfF*V1ba#0zbymz8SW5$5UQ@}48&*G(OjDSU|RrO zL;0E}9#+AD?MxdHIq<0Y>;t=Osoksy20D(I7_5(D)A0gz^54k13&>xl?q>{;A)nz{ zO#?8h7@DvFvnX7y?o&A6ra&kBNWpb8j5z1^yL=rr}H+u_TaHw<0k=`=8PBZ|3j7!{*&)is~^l zQi~CPPI^p0bO+r=LkG&_vBrs>c;=PBD{6p9JF6KlHI+{M`Rm7SU58~--YJb8H-(;6 z!c~RT9t-x%WMZ_0#5-C_N1iUGNP4B=?^^E?tx&7WQ@&b}IX?^f|NZ{T(noZWK-pfj zYLh^FPpWI9YP&T11JjsAhdI_krb`~63pposm8(qyiwK?ZqQ~3baw&t*i+8S$vMo>L z@lGBJr0~dL3jWU$OPb_KkEamTBf~~Jllm@gNMYkbZ^*DXKps_}jt+R|S-%>7n6U|I zP_B8wQO7f^a`scRL9@E{1G7ih@Wwj#w9l{Kg={(0>_UIc;K`w_*48n`Q_=HGq$T_9 zsRVADVRZ3X4FYq|Rbfj>s!gzP;njHr??_})2>wS)KelV1n9SVlno|EA{5js+ddF@h zg>VmgpH5m?9(jE_WOcTRu_Ks%fAcd4T;zKH2L$+>vlZe^sKXn{DNzUcm7GEMPvdsM z?4o$Z*KqaI{o@UNiA4rY)9vj_BcqrR!M;4ZON+(h&F(KTstj*UZsi_=e^!6d{9ShV zcFOuUGT;~+G$*&WMrnrRUEq@g@~51dGIOXy#X6GSzhS0U2)58$_P?jfy@G?C^^hoZ z>NHL_Ggycvku8cf(&1F33ml9F3ycbJl?XB$cH(6Z(IUSI&{pVSO&ih$$ zN*JL!@+VPo_hEu!I}jl=aCw_ZP3O zb+McZL`Lr(4S%%SA5G55H`HxW9qEC@k(!I4)3YT_luzJKAKPX>D}V-uC=N~Z4nBUk zXL7&&$d~*D=lI}G>btopT++_K^Kl;v$Lm&coK4AE)g@wyhPOXy&xBT=1f`4ic513J zM+)3nI~`QXi6$;>XnbmuYl8b+z12o-ocqbt@ICmWXgNO#zkPq(kfebY^7|JdaD3g^ zhZ5Sbpt{#4qncMPuh~KwCcgE0pIqI#i0DyoxFdAdjorg3@0M}%v4 z#8*seK}Vk~;vgA!m8-A0?~5e)i!KP3x14|*?zjaPGRdL7>2?|@!QNo!WBn84aq33w z?cd8oRiQT9S zedf;qmU%Y7dwKfm#~I6`TVrHq5A)#&>-d%Vt{0xO2Hg;oo7oBGd>Wfjh@CjjcXf9RH$~VjtL8pcs*mWm*yD=+W zu1m3x18Iu4LRyU3;OQarqt(YUv!nFL$N2G5#Zy2?qwS9CBIb#8FJ|%zaudr50&*`I z*r`)Y-A_|eXpjzdx8zSEOpAFo%cBqW2)3y~c7jZl$3n@-RhMsr@f^t?F5CEthE&F$ z#CrndOB5X(k&??)C4qN~h`E#Wqpx!omKGm0MJAq6c_;n=jnXH3*tLFQ{ZTEP{b{;m zlU#!zkZ~08BM1{RUvMD>O&srpSc|{1!9L;Bi0bj6@;)V0*N=P7n=_A~#$O2FzkaW} z;RKxNP5hg)^J=PnnJUusHdG?m$xwjsbe&gCzM+GdniVipBfpo)k`MXoWG{5H%%^nf zL&K8YkqT1fBK_}`pr!;d7Z{3B=;%|gIjD|%WYh5biD169RGpeLwctWC&R1ZG&Ngiy zcXw`WDkgT&gZtgvjqR(}$k74L<)AoH6qEjY&TO*_u6f_`Sywlb3_WcuduMWlTdP`Fh?q(r4feVOOm4@M`3eJBa%60GsaJU z$M5qOJg?8|^?4u9=Xo9;>0qr9jvL19&z@{B7-++G(z}h{Gyi-6aExTF=uG`L#RtEQ zSuMC<7OYIWRZ5fAhU_4sjljuDX~-05Rq1))DFwHyPfO~xl?&@fZj(85Z9BF0s;`g! ziLQ|eGDAiZ2$I?NmaFUWkmi+@mdajuNH?2 z75!$#!u^)FOK=i*A+>20!cop=G@EqM`>Fo~t+>2+5~J8CW;j4fFnqoA&h&1WYEDLK z#(bmuRzkNEd=hZphZVtBAX1Ydw!y)<_wb!$#B>HkHNZwSVnON2?$?=H!wx)5Y&zYdG3G0v`LtC%3Vm)C0dGZ< z_tcdro=0-Xi@f_Wh#r;q1n0{~MgN35bjrcvFudF5_fwW6GBCpQORCg7hVAhnu!x`^ z^PTf<`T9lyUx!YL+fa7)*{x7T)sq;uWNsvwb6hpzd>{5cnV4& z^VsJsevfb5IFhyOddyT^&u9j1)z8rF^)nipv{ATuUB+cu2vB8?b`01IkM+{=I$@eL+ z#=m+RGuA0=pv0icoh9}x2{G=Dg*y81>L-13;59&R}XEXw$mo~$>1{wxq#Spwv1hDXsq65Y>&7y03fkk;Pf1> z;@8=l`=e0c`sF0O) zpNjesiv)UhUlbQUztfU#q;swatIaCvv}Wj;Vnz{Py^*dG2^SPx{VUq|ME-P&!MCYx zt<(16M?rr^v5Csb(pLiE3SV zj2X;dAKb!HCT=z{$Q-b9!-9aYg$Xsy2@X4nvHAz?MT#T;{mzMr=DNfg?!?F`le!ho zy7(X@js|@c6n!c^Qg7gBa^7YItZ_i~VbreK55#Aov=dFNtd`#jrGJoNM_6vAefB$Z zP`SdT+b9>g|6y6F<3NGFt2n)@65)Xav+PYz#jB#{m~GU*|6VU48E3xIz-|7mf9G*N z&+9CdKcDKU!R=~ENHxE+K$U&7zk=*_c38Jk%iv_HlTj3{vdW(AWHpr_I7PGcsV(BAN;EPkn`(b8wYNQ zQ}g_P;xan8-H^ENU)?1NK$Nb`9jtrB&_$giQanM&_y9<5HDqHStMm?_?ZE7|)#6MJ1*Zy~Ep?f7)m&2@|aYz0=(_B^SsM0%zdoSqj4pF^~6CavL zb!cT*+zZe1>NGFkP{5OM+6q$CW-4VSU{0PLdibRo0t5s2cx8OxEtTawm$^*vdeR~s7G z%uz!84R@c{XFKR4r8l=1P`%;G_{EF08>>zz7~d8Y4tO#)n{s|e_~nbS>MCuK^?d)G z21JO;dbaDKl;_NxBi*W9I5`y2EO(Ua%q;k#{v9EHd~YkI>9@a!?YY!kYSZNR-oexk z7gb===T~`L81_Ggd{usn$^}|YpACQBw#gzL9v3-9~Ki{iB-AvOTt1Ag-~*Xh)bY^BA-*kNzqeG7v~+}CJ7=a_a=j>bN#@Cfo(0U4|&Wb&93ig_tqA*q$_hu`*a?fTA<=cC(qpd;_M zDE!3&FG1gcU?5;p7^a|)N<0HZzQ>b=bJ6aljG*5+BczO8V!ml=0 zy5R6-b4tk8p$`$+b|7;&#~Gh-;b1HVmz?|Zb(uxhzacBq1JH+c%lV{a?WO|8NM z3;bM%1IS**{Y8a1GR?;9q_2XM`)fHthx}AvC*4h|TZ8?LUKA}G;lP~os6WfqWoOw; zzPq-FStujSGD>y@hYf9ak33|E+}-@hSi&3wD^JfkZmu3D=%WD}`WZysiPg2?2P0Rg zoZ~;q6)v_Ufx$?HCYe)meI+9_lQV;yFf%mo12~tOj&$ulJ4I+X+&wmoMS2AG6G{;q z!nIim%cw-(IOEhulwH(25wKC?hTgw7NOI53yD9)f;ZjYWy0XBOKjbAbBzSg>7)+bn z{63i>sGU@~Ky;@QD^98xZ%W~>1|RLs#r+aI&X(Gzc>eQl3qHLq?pDz^z+4p~r=V1I z$j_ z*>|G`y+o@|$jh9nfK;etw3(Kw#NSaMS_tx$bP1dkvXUkzTB@m*yh3%hq8$B=se*Nx zc&nbI>6s(ypNV1%seu@fyp!uhQD2!MY6LYMGMN$J#C!_Av zEjoO7GF<0nDzdAqg3YXE@s;KM>OR?*KlZ@@fUM(>@cyL!pQU{{`>tkx?Ojln>%~Mt zqYA!V>ZUxDPt&Y5@ywx1Yo=R-5UVF5<|B4*tdy3BVqL|U?zoKyUVwkBh{^j7@bTI z!{xNJSf>u&`ue5FN?%h9n)_fOu?npvVD8Kwr1>In{_vcaXhEF9?WXG=gtvp%bQvd(} literal 0 HcmV?d00001 diff --git a/app/lov_frontend/public/placeholder.svg b/app/lov_frontend/public/placeholder.svg new file mode 100644 index 0000000000..ea950def0b --- /dev/null +++ b/app/lov_frontend/public/placeholder.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/lov_frontend/public/robots.txt b/app/lov_frontend/public/robots.txt new file mode 100644 index 0000000000..6018e701fc --- /dev/null +++ b/app/lov_frontend/public/robots.txt @@ -0,0 +1,14 @@ +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +User-agent: Twitterbot +Allow: / + +User-agent: facebookexternalhit +Allow: / + +User-agent: * +Allow: / diff --git a/app/lov_frontend/src/App.css b/app/lov_frontend/src/App.css new file mode 100644 index 0000000000..b9d355df2a --- /dev/null +++ b/app/lov_frontend/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/app/lov_frontend/src/App.tsx b/app/lov_frontend/src/App.tsx new file mode 100644 index 0000000000..adf309fdf6 --- /dev/null +++ b/app/lov_frontend/src/App.tsx @@ -0,0 +1,31 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { BrowserRouter, Route, Routes } from "react-router-dom"; +import { Toaster as Sonner } from "@/components/ui/sonner"; +import { Toaster } from "@/components/ui/toaster"; +import { TooltipProvider } from "@/components/ui/tooltip"; +import { ThemeProvider } from "next-themes"; +import Landing from "./pages/Landing.tsx"; +import Index from "./pages/Index.tsx"; +import NotFound from "./pages/NotFound.tsx"; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + + + } /> + } /> + } /> + + + + + +); + +export default App; diff --git a/app/lov_frontend/src/components/ChatInput.tsx b/app/lov_frontend/src/components/ChatInput.tsx new file mode 100644 index 0000000000..944353ee40 --- /dev/null +++ b/app/lov_frontend/src/components/ChatInput.tsx @@ -0,0 +1,53 @@ +import { useState, useRef, useEffect } from "react"; +import { Send } from "lucide-react"; + +interface ChatInputProps { + onSend: (message: string) => void; + disabled?: boolean; +} + +export function ChatInput({ onSend, disabled }: ChatInputProps) { + const [value, setValue] = useState(""); + const textareaRef = useRef(null); + + useEffect(() => { + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`; + } + }, [value]); + + const handleSubmit = () => { + const trimmed = value.trim(); + if (!trimmed || disabled) return; + onSend(trimmed); + setValue(""); + }; + + return ( +
+