diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 766108e65..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*' - -permissions: - contents: write - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - - - name: Run discovery tests - run: | - for test in tests/scripts/test-discovery-*.cjs; do - echo "=== Running $test ===" - node --test "$test" - done - - - name: Run migration tests - run: | - for test in tests/scripts/test-migration-*.sh; do - echo "=== Running $test ===" - /bin/bash "$test" - done - - release: - needs: test - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Extract release notes from tag annotation - id: notes - run: | - TAG_MESSAGE=$(git tag -l --format='%(contents)' "$GITHUB_REF_NAME") - { - echo 'BODY<> "$GITHUB_OUTPUT" - - - name: Create GitHub release - uses: softprops/action-gh-release@v2 - with: - body: ${{ steps.notes.outputs.BODY }} diff --git a/.tick/tasks.jsonl b/.tick/tasks.jsonl index 79e436d62..7583e4889 100644 --- a/.tick/tasks.jsonl +++ b/.tick/tasks.jsonl @@ -1,4 +1,4 @@ -{"id":"tick-cbbd13","title":"Knowledge Base","status":"in_progress","priority":2,"refs":["knowledge-base"],"transitions":[{"from":"open","to":"in_progress","at":"2026-04-10T21:02:52Z","auto":true}],"created":"2026-04-07T19:01:36Z","updated":"2026-04-10T21:02:52Z"} +{"id":"tick-cbbd13","title":"Knowledge Base","status":"done","priority":2,"refs":["knowledge-base"],"transitions":[{"from":"open","to":"in_progress","at":"2026-04-10T21:02:52Z","auto":true},{"from":"in_progress","to":"done","at":"2026-04-20T19:05:06Z","auto":true}],"created":"2026-04-07T19:01:36Z","updated":"2026-04-20T19:05:06Z","closed":"2026-04-20T19:05:06Z"} {"id":"tick-16f12a","title":"Phase 1: Build Pipeline + Store Fundamentals","status":"done","priority":2,"refs":["knowledge-base-1"],"transitions":[{"from":"open","to":"in_progress","at":"2026-04-10T21:02:52Z","auto":true},{"from":"in_progress","to":"done","at":"2026-04-10T21:15:09Z","auto":true}],"parent":"tick-cbbd13","created":"2026-04-07T19:01:42Z","updated":"2026-04-10T21:15:09Z","closed":"2026-04-10T21:15:09Z"} {"id":"tick-0fe119","title":"Project Scaffolding + esbuild Validation","status":"done","priority":2,"type":"task","refs":["knowledge-base-1-1"],"description":"Problem: The knowledge base CLI requires a build pipeline. The project currently has no npm dependencies or build tooling. esbuild bundling with Orama + MsgPack is the highest implementation risk per the design doc — if it doesn't work, the entire architecture needs rethinking. This must be validated first.\n\nSolution: Create project scaffolding (package.json, directory structure) and validate that esbuild bundles Orama and MsgPack into a single CJS file that runs correctly.\n\nOutcome: A working build pipeline producing skills/workflow-knowledge/scripts/knowledge.cjs from src/knowledge/index.js. Design doc estimates ~110-120KB minified; threshold set at 150KB to allow headroom for application code while catching unexpected bloat.\n\nDo:\n- Create package.json at project root with devDependencies: @orama/orama, @msgpack/msgpack, esbuild. Pin exact versions to avoid API drift (design doc: verify exact API shape against targeted Orama version at implementation time).\n- Add to .gitignore: node_modules/, .workflows/.knowledge/store.msp\n- Create directory structure:\n - src/knowledge/ (source files — NOT copied by AGNTC, lives outside skills/)\n - build/ (build config — NOT copied by AGNTC)\n - skills/workflow-knowledge/scripts/ (bundled output destination — IS copied by AGNTC)\n - skills/workflow-knowledge/chunking/ (phase configs directory, populated in Phase 2)\n- Create build/knowledge.build.js — esbuild config using: --bundle --platform=node --format=cjs --minify. Entry: src/knowledge/index.js. Output: skills/workflow-knowledge/scripts/knowledge.cjs\n- Create src/knowledge/index.js as the entry point that will grow into the full knowledge CLI. Structure it as a module with exports (not a throwaway script). For now it should import @orama/orama (create, insert, search) and @msgpack/msgpack (encode, decode), perform a trivial round-trip with each library, and exit cleanly. Both imports must be actively used — esbuild tree-shaking will remove unused imports. Subsequent tasks in this phase will replace the trivial operations with real store, provider, and persistence code.\n- Add a build script to package.json: node build/knowledge.build.js\n- The bundled knowledge.cjs MUST be committed to the repo (not gitignored). AGNTC installs from git tags with no build step — the bundle must be present in the tree at tag time. During development, rebuild and commit the bundle when source changes.\n- Verify __dirname in bundled output resolves to the script directory. esbuild preserves __dirname as-is in --platform=node --format=cjs output (verified in design doc). The knowledge CLI uses this at runtime to find chunking configs via path.join(__dirname, '..', 'chunking').\n- Plain JS, not TypeScript — matches the existing manifest.cjs convention.\n- Minimum Node 18 required by Orama. Node 20+ recommended (Node 18 fetch emits ExperimentalWarning to stderr — cosmetic, not functional).\n\nAcceptance Criteria:\n- npm install \u0026\u0026 npm run build succeeds without errors\n- Bundle exists at skills/workflow-knowledge/scripts/knowledge.cjs and is committed to git\n- Bundle size is under 150KB (design doc estimates ~110-120KB minified — 150KB provides ~25% headroom for growth across Phase 1 tasks while catching unexpected bloat)\n- node skills/workflow-knowledge/scripts/knowledge.cjs runs and exits cleanly\n- __dirname in the bundle resolves to skills/workflow-knowledge/scripts/, not src/knowledge/\n- node_modules/ and .workflows/.knowledge/store.msp are in .gitignore (but knowledge.cjs is NOT)\n- src/knowledge/index.js is structured as a module with exports, not a disposable script\n\nTests:\n- Test file: tests/scripts/test-knowledge-build.sh (shell test following existing migration test conventions — PASS/FAIL counters, assert_eq function, summary line)\n- it builds without errors (npm run build exits 0)\n- it produces a bundle under 150KB (stat the output file)\n- it runs and exits with code 0 (node knowledge.cjs)\n- __dirname resolves to the script directory, not the source directory (run the bundle, capture stdout, assert path contains skills/workflow-knowledge/scripts)\n\nEdge Cases:\n- esbuild tree-shaking removes imports that appear unused — the minimal index.js must actively use both libraries in its trivial operations\n- If bundle exceeds 150KB at Task 1-1 (trivial code), investigate — libraries alone are ~90KB per design doc, so Task 1-1's minimal bundle should be ~90-100KB. Significant deviation means esbuild is including more than expected.\n- Both @orama/orama and @msgpack/msgpack are zero-dependency pure JS — no native modules, no node-gyp. If npm install fails, it is not a dependency issue.\n- NOTE: later Phase 1 tasks (1-3, 1-4, 1-5) add real store/persistence/locking code and the bundle will grow. If growth pushes past 150KB during Phase 1, re-evaluate the threshold — do not silently raise it without investigating what's being included.\n\nSpec Reference: knowledge-base/design.md — Build \u0026 Distribution section (bundle estimated at ~110-120KB minified, line 208 and 446) and Runtime config file discovery subsection (line 461)","transitions":[{"from":"open","to":"in_progress","at":"2026-04-10T21:02:52Z","auto":false},{"from":"in_progress","to":"done","at":"2026-04-10T21:06:00Z","auto":false}],"parent":"tick-16f12a","created":"2026-04-07T19:02:32Z","updated":"2026-04-10T21:06:00Z","closed":"2026-04-10T21:06:00Z"} {"id":"tick-cca624","title":"Embedding Provider Interface + StubProvider","status":"done","priority":2,"type":"task","refs":["knowledge-base-1-2"],"description":"Problem: The knowledge base needs an embedding provider to convert text into vectors for semantic search. All subsequent tasks in this phase (store, persistence, integration test) depend on having a provider interface to program against. The real OpenAI provider comes in Phase 4, but testing needs a deterministic provider now that produces consistent vectors without API calls.\n\nSolution: Define the KnowledgeProvider interface and implement a StubProvider that generates deterministic vectors by hashing input text.\n\nOutcome: A provider interface that the store, CLI, and all tests program against, plus a StubProvider that enables the entire test suite without external dependencies.\n\nTERMINOLOGY DISAMBIGUATION (important — the design doc uses two similar terms):\n- StubProvider (this task) — a TEST provider that returns deterministic fake vectors via hash. ALWAYS returns vectors. Configured via provider: stub in config. Used for development and testing only. NOT a production mode.\n- Stub mode / keyword-only mode (design doc lines 322-331) — a PRODUCTION degraded mode triggered when NO provider is configured or no API key is available. Stores documents WITHOUT vectors (omits the vector field). Uses BM25 fulltext search only. Triggered by the ABSENCE of a provider, not by provider: stub.\nThese are DIFFERENT concepts that share a similar name. StubProvider is a first-class provider implementation for testing. Stub mode is the absence of any provider in production. Do not conflate them.\n\nDo:\n- Create src/knowledge/embeddings.js exporting the interface contract and StubProvider implementation.\n- Define KnowledgeProvider interface with four methods/accessors:\n - embed(text) -\u003e vector (array of floats, length matching dimensions())\n - embedBatch(texts[]) -\u003e vectors[] (array of vectors, one per input)\n - dimensions() -\u003e number (the vector dimensionality this provider produces)\n - model() -\u003e string (a stable identifier for this provider's model, used by Task 3-3 to write into metadata.json and by Task 3-4 / Task 4-6 for mismatch detection). Every concrete provider must return a stable, non-empty string.\n- Implement StubProvider:\n - Constructor accepts optional dimensions parameter. Implementer picks a sensible default (e.g., 128 for smaller/faster tests, or 1536 to match production). Downstream tasks must not assume a specific default.\n - embed(text): hash the input string deterministically to produce a float vector of length dimensions(). Does not need to be cryptographically strong — consistency and differentiation matter, not quality.\n - embedBatch(texts): maps over embed() for each input. No batching optimisation needed — this is for testing.\n - dimensions(): returns the configured dimension count.\n - model(): returns a stable identifier string for the stub provider (e.g., \"stub\"). This value is written to metadata.json by Task 3-3 and compared on mismatch checks — it must never be null, undefined, or empty.\n - CRITICAL: never return null for vectors. Orama crashes when a vector field is null. Return a real vector array from embed/embedBatch. Production keyword-only mode (no provider configured at all) is handled at the calling layer by omitting the vector field — that is NOT StubProvider's responsibility. StubProvider is a real provider that always returns vectors.\n - Same input text MUST always produce the same vector (deterministic).\n - Different input text MUST produce different vectors (distinguishable).\n- The provider dimensions and the Orama schema dimensions must match. Task 1-3 (store creation) will read dimensions from the provider. Production uses whatever the OpenAI provider (Phase 4) exposes — typically 1536 for text-embedding-3-small.\n- Rebuild after implementation: npm run build to verify embeddings.js bundles into knowledge.cjs correctly.\n\nAcceptance Criteria:\n- embed('hello') returns an array of floats with length equal to dimensions()\n- embed('hello') called twice returns identical vectors\n- embed('hello') and embed('world') return different vectors\n- embedBatch(['a', 'b', 'c']) returns exactly 3 vectors, each with correct dimensions\n- dimensions() returns the configured value\n- model() returns a stable, non-empty string\n- embed() never returns null or undefined — always a real array\n- Module exports both the StubProvider class and a clear description of the interface contract (JSDoc or comments documenting what providers must implement, including the model() requirement)\n- JSDoc/comments explicitly note that StubProvider is for testing, and that production keyword-only mode is a separate concept (no provider configured, handled by the caller)\n- npm run build succeeds with the new module included\n\nTests:\n- Test file: tests/scripts/test-knowledge-embeddings.cjs (Node test using node:test module and assert, following existing discovery test conventions — describe/it blocks, beforeEach/afterEach if needed)\n- it returns a vector of correct length for a single string\n- it returns identical vectors for identical input (determinism)\n- it returns different vectors for different input (differentiation)\n- it handles embedBatch with multiple inputs correctly\n- it respects custom dimensions parameter\n- it returns a stable non-empty model() identifier\n- it never returns null (explicit assertion)\n- it handles empty string input without error\n- it handles very long string input without error\n\nEdge Cases:\n- Empty string input — must still return a valid vector, not crash\n- Very long input — must produce a vector of correct length regardless of input size\n- Unicode input — must handle non-ASCII characters\n- embedBatch with empty array — should return empty array\n- embedBatch with single item — should return array of one vector, identical to calling embed() directly\n\nSpec Reference: knowledge-base/design.md — Embedding Provider section (KnowledgeProvider interface, embed/embedBatch/dimensions methods), Testing Strategy section (StubProvider description, mock provider usage), Knowledge Base is Required Infrastructure section (lines 322-331, stub mode / keyword-only as the distinct production concept), Provider mismatch detection finding #10 (line 982, requires provider/model/dimensions stored in metadata.json — every provider must expose all three)","transitions":[{"from":"open","to":"in_progress","at":"2026-04-10T21:06:10Z","auto":false},{"from":"in_progress","to":"done","at":"2026-04-10T21:07:23Z","auto":false}],"parent":"tick-16f12a","created":"2026-04-07T19:19:51Z","updated":"2026-04-10T21:07:23Z","closed":"2026-04-10T21:07:23Z"} @@ -32,6 +32,6 @@ {"id":"tick-2e511f","title":"Phase 7: Setup Wizard","status":"done","priority":2,"refs":["knowledge-base-7"],"transitions":[{"from":"open","to":"in_progress","at":"2026-04-19T16:28:36Z","auto":true},{"from":"in_progress","to":"done","at":"2026-04-19T16:44:49Z","auto":true}],"parent":"tick-cbbd13","created":"2026-04-09T19:04:09Z","updated":"2026-04-19T16:44:49Z","closed":"2026-04-19T16:44:49Z"} {"id":"tick-78c1c7","title":"Setup Wizard: System Config + Project Init + Stub Mode","status":"done","priority":2,"type":"task","refs":["knowledge-base-7-1"],"description":"Problem: Users need a single entry point to configure the knowledge base for the first time. This involves system-level configuration (API keys, provider choice) that lives in the user's home directory and project-level initialisation that lives in .workflows/. Both must be handled in one flow so the user doesn't need to know the internal structure.\n\nSolution: Implement the knowledge setup command as an interactive wizard using Node's readline interface, handling system config, project init, and stub mode in one guided flow.\n\nOutcome: A user can run one command and have a fully configured knowledge base ready for use, with clear prompts at each step and graceful handling of partial or existing configuration.\n\nDo:\n- Implement the setup command handler in the CLI (invoked by Task 3-1 dispatch).\n- Invocation: knowledge setup (no arguments)\n- HUMAN-ONLY — uses interactive prompts throughout via Node readline interface (process.stdin/process.stdout). Claude cannot handle interactive terminals — this is the natural protection against accidental invocation (design doc line 632).\n\nSTEP 1: SYSTEM CONFIG (~/.config/workflows/config.json):\n- Check if system config already exists.\n - If it exists: read it, display current settings, ask if the user wants to reconfigure or skip. If skip, proceed to Step 2.\n - If it does not exist: create ~/.config/workflows/ directory if needed.\n- Prompt for provider:\n Which embedding provider? (openai / skip)\n - openai: proceed with OpenAI configuration\n - skip: stub mode (keyword-only search, no embeddings). Write config with no provider field. Display clear message about limitations. Proceed to Step 2.\n- If openai selected:\n - Prompt for model (default: text-embedding-3-small):\n Embedding model [text-embedding-3-small]:\n - Prompt for dimensions (default: 1536):\n Vector dimensions [1536]:\n - Prompt for API key env var name (default: OPENAI_API_KEY):\n API key environment variable [OPENAI_API_KEY]:\n - Check if the env var is set in the current environment:\n - If set: validate with a test embed call — embed a short test string and verify a vector is returned. If validation fails, display the error and ask the user to fix it or continue anyway.\n - If not set: warn the user and explain they need to set this env var in their shell profile before using the knowledge base. Do not block — they may be setting it up for later.\n- Write system config to ~/.config/workflows/config.json:\n { knowledge: { provider, model, dimensions, api_key_env, similarity_threshold: 0.8, decay_months: 6 } }\n- For stub mode (skip): write minimal config:\n { knowledge: { similarity_threshold: 0.8, decay_months: 6 } }\n\nSTEP 2: PROJECT INIT (.workflows/.knowledge/):\n- Check if project knowledge base directory already exists with config.json and store.msp.\n - If fully initialised: display status and ask to skip or reinitialise. If skip, proceed to Step 3.\n - If partially initialised (directory exists but missing files): complete the missing pieces.\n - If not initialised: create from scratch.\n- Create .workflows/.knowledge/ directory.\n- Write .workflows/.knowledge/config.json — project-level config. By default this is empty (inherits everything from system config):\n { knowledge: {} }\n The user can customise project-level overrides later by editing this file.\n- Create an empty Orama store and save it to .workflows/.knowledge/store.msp using createStore + saveStore from Phase 1. Use the dimensions from the resolved config (system + project merge).\n- Write .workflows/.knowledge/metadata.json with the provider/model/dimensions from the resolved config (or empty provider fields for stub mode).\n\nREADLINE INTERFACE:\n- Use Node built-in readline module (require('readline')).\n- Create interface with input: process.stdin, output: process.stdout.\n- Use rl.question() for prompts with defaults shown in brackets.\n- Close the interface when done.\n- If stdin is not a TTY (piped input, non-interactive environment): detect with process.stdin.isTTY and abort with message: knowledge setup requires an interactive terminal. Run it directly, not through Claude.\n\nAcceptance Criteria:\n- knowledge setup prompts for provider, model, dimensions, API key env var\n- System config is written to ~/.config/workflows/config.json\n- Project knowledge base is initialised at .workflows/.knowledge/ with config.json, store.msp, metadata.json\n- Test embed call validates the API key when configured\n- Stub mode writes config without provider, displays limitation message\n- Existing system config is detected and can be skipped or reconfigured\n- Existing project init is detected and can be skipped\n- Non-interactive terminal is detected and aborts with clear message\n- npm run build succeeds with the setup command included\n\nTests:\n- No automated tests for the interactive flow — it is inherently human-driven.\n- Unit tests for the non-interactive parts (tests/scripts/test-knowledge-config.cjs — extend from Task 3-1):\n - it creates a valid system config object from provider choices\n - it creates a valid stub-mode config object\n - it detects existing system config correctly\n - it detects existing project init correctly\n - it creates the project directory structure correctly\n\nEdge Cases:\n- ~/.config/workflows/ directory does not exist — create it (mkdir -p equivalent with { recursive: true })\n- .workflows/ directory does not exist — this means no workflow project exists. Display error: No .workflows/ directory found. Initialise a workflow project first.\n- System config file exists but is invalid JSON — display error, offer to overwrite\n- API key env var is set but the test embed call fails (wrong key, network error) — display error, offer to continue setup anyway (user may fix the key later)\n- User presses Ctrl+C during setup — readline handles SIGINT, process exits cleanly\n- Previous setup was stub mode, user re-runs with openai — detect existing store has no vectors, suggest knowledge rebuild after setup completes\n\nSpec Reference: knowledge-base/design.md — knowledge setup command (lines 626-633, interactive wizard flow, skip logic, stub mode), System config creation flow review finding #18 (line 1008), Configuration Hierarchy section (lines 258-301, system and project config schemas)","transitions":[{"from":"open","to":"in_progress","at":"2026-04-19T16:28:36Z","auto":false},{"from":"in_progress","to":"done","at":"2026-04-19T16:36:59Z","auto":false}],"parent":"tick-2e511f","created":"2026-04-09T19:05:02Z","updated":"2026-04-19T16:36:59Z","closed":"2026-04-19T16:36:59Z"} {"id":"tick-b4348b","title":"Setup Wizard: Initial Indexing + Idempotency + CLI Integration","status":"done","priority":2,"type":"task","refs":["knowledge-base-7-2"],"description":"Problem: After system config and project init (Task 7-1), the knowledge base is empty. Existing projects may have dozens of completed artifacts that need to be indexed. The setup wizard must automatically index everything as its final step, and re-running setup must be safe — skipping already-completed steps without duplicating work.\n\nSolution: Wire up the existing bulk index logic (Task 4-4) as the final step of the setup wizard, ensure the full setup flow is idempotent, and register setup in the CLI dispatch.\n\nOutcome: Running knowledge setup on a project with existing completed work populates the knowledge base immediately. Re-running setup on an already-configured project skips to indexing (or exits if everything is done).\n\nDo:\n\nINITIAL INDEXING (Step 3 of the setup wizard):\n- After system config (Step 1) and project init (Step 2) complete (or are skipped), run initial indexing.\n- This delegates to the existing knowledge index (no args) bulk index logic from Task 4-4.\n- The bulk index discovers all completed artifacts via manifest, diffs against the store (which is empty for first setup), and indexes everything.\n- Display progress as files are indexed (same output as the bulk index command).\n- Display a clear completion summary covering how many files and chunks were indexed.\n- If the store already has chunks (re-run scenario): the bulk index logic naturally skips already-indexed items and only processes missing ones. No special handling needed.\n- If indexing fails for some files: the retry/pending queue mechanism from Task 4-4 handles it. Inform the user that some files could not be indexed and were added to the pending queue for automatic retry on next use.\n\nIDEMPOTENCY:\n- The full setup flow must be safe to re-run at any point:\n - System config exists → display current settings, offer to skip or reconfigure\n - Project init exists → display status, offer to skip or reinitialise\n - Store has chunks → bulk index skips already-indexed, only processes new/missing\n - Everything is current → display a clear \"already set up\" message and exit.\n- This covers the design doc requirement (line 631): skips steps that are already done.\n- Also covers the interrupted-setup scenario (design doc finding #5, line 965): if setup was interrupted mid-indexing, re-running picks up where it left off via the pending queue.\n\nCLI INTEGRATION:\n- Register the setup command in the CLI dispatch from Task 3-1. Task 3-1 lists setup alongside other Phase 4+ commands with a \"not yet implemented\" placeholder — replace that placeholder with a real handler that calls the wizard flow from Task 7-1 followed by the initial indexing from this task.\n\nSTUB-TO-FULL UPGRADE PATH:\n- If the user previously ran setup in stub mode and now re-runs with an OpenAI provider:\n - System config is updated with the new provider settings.\n - The existing store has no vectors (keyword-only mode).\n - Inform the user that the previous setup was keyword-only and they should run knowledge rebuild to re-index with embeddings for full hybrid search.\n - Do NOT automatically rebuild — it is destructive and may take time. The user should run it explicitly.\n- This matches the design doc (line 329): upgrading from stub to full requires knowledge rebuild.\n\nAcceptance Criteria:\n- Setup wizard indexes all existing completed artifacts as its final step\n- Progress and summary output matches the bulk index command\n- Failed indexing files are logged to pending queue with a note to the user\n- Re-running setup on a fully configured project exits cleanly with an already-set-up message\n- Re-running setup after interrupted indexing picks up where it left off\n- Setup command registered in CLI dispatch, replacing the Task 3-1 not-yet-implemented placeholder\n- Stub-to-full upgrade detected and user informed about knowledge rebuild\n- npm run build succeeds\n\nTests:\n- Unit tests for idempotency logic (tests/scripts/test-knowledge-config.cjs — extend):\n - it detects fully configured state (system + project + indexed)\n - it detects partially configured state (system exists, project missing)\n - it detects empty store needing initial indexing\n- CLI dispatch test (tests/scripts/test-knowledge-cli.sh — extend):\n - it routes to setup command without error (non-interactive detection will abort, which is the expected behaviour in test)\n- Manual validation:\n - Run setup on a project with existing completed artifacts -\u003e verify all are indexed\n - Run setup again -\u003e verify it skips everything and exits cleanly\n - Run setup after interruption -\u003e verify pending queue items are processed\n\nEdge Cases:\n- Project with no completed work units — initial indexing indexes 0 files, which is correct. The knowledge base is ready but empty.\n- Project with hundreds of completed artifacts — initial indexing may take time (embedding API calls). Display progress so the user knows it is working.\n- Setup interrupted with Ctrl+C during indexing — indexed files are saved (each save is atomic), unindexed files go to pending queue. Re-run picks up.\n- System config points to a provider but the API key env var is not set — initial indexing will fail (provider cannot embed). The retry/pending queue catches this. Setup still completes — the project is initialised, indexing is pending.\n- Multiple projects sharing the same system config — this is normal. System config has provider/key, each project has its own .workflows/.knowledge/. Setup only modifies the current project.\n\nSpec Reference: knowledge-base/design.md — knowledge setup command (lines 626-633, initial indexing as final step, skip logic), Init/resumability finding #5 (line 965, interrupted setup, pending queue picks up), Upgrading from stub to full (line 329, requires rebuild)","transitions":[{"from":"open","to":"in_progress","at":"2026-04-19T16:37:13Z","auto":false},{"from":"in_progress","to":"done","at":"2026-04-19T16:44:49Z","auto":false}],"parent":"tick-2e511f","created":"2026-04-09T19:05:40Z","updated":"2026-04-19T16:44:49Z","closed":"2026-04-19T16:44:49Z"} -{"id":"tick-74a599","title":"Phase 8: Release Process","status":"open","priority":2,"refs":["knowledge-base-8"],"parent":"tick-cbbd13","created":"2026-04-09T19:05:44Z","updated":"2026-04-09T19:05:44Z"} -{"id":"tick-7f4725","title":"Release Script Build Integration","status":"open","priority":2,"type":"task","refs":["knowledge-base-8-1"],"description":"Problem: The knowledge CLI bundle (knowledge.cjs) must be committed to the repo before tagging because AGNTC installs from git tags with no build step. During Phases 1-7 development, the bundle was built and committed manually. For production releases, the build must be formalised into the release pipeline to prevent shipping stale bundles.\n\nSolution: Integrate the esbuild build step into the local release script so the bundle is always fresh when a release tag is created.\n\nOutcome: The release process automatically builds knowledge.cjs from source before tagging, ensuring every tagged release has an up-to-date bundle.\n\nDo:\n- Read the existing release script at the project root (./release) before editing. Study:\n 1. Where perform_release runs the dirty-tree gate (git status --porcelain check that aborts on any uncommitted changes)\n 2. Where perform_release currently stages/commits (note: this is gated inside an `if [[ \"$VERSION_STRATEGY\" != \"none\" ]]` block — the default \"none\" strategy currently performs no commit at all, only tag + push)\n 3. Where the annotated tag is created and pushed\n- The build step must sit AFTER the existing dirty-tree check and BEFORE the annotated tag is created. Placing it before the dirty-tree check would cause the gate to abort on a bundle rebuild; placing it after the tag would ship a stale bundle in the tagged commit.\n- The build step sequence:\n 1. Run `npm install` (ensure dev dependencies are available)\n 2. Run `npm run build` (esbuild bundles src/knowledge/ into skills/workflow-knowledge/scripts/knowledge.cjs)\n 3. Check whether the bundle changed (e.g., git diff on the bundle path)\n 4. If it changed: stage and commit the updated bundle. IMPORTANT: this commit must occur in ALL version strategies, including `VERSION_STRATEGY=\"none\"` — the existing `none` branch performs no commits, so this task is introducing a new commit point into a previously commit-free path. Do not hide the bundle commit inside the existing version-bump commit block; it is a distinct concern and must run regardless of version strategy.\n 5. If the bundle did not change: no commit needed, proceed.\n 6. Proceed with the existing tag + push flow.\n- Build failure (non-zero exit from `npm run build`) must abort the release — do not tag with a stale or missing bundle. Clean exit with a clear error.\n- The bundle commit is the only permitted mutation after the dirty-tree check passes. The dirty-tree check still protects against unrelated uncommitted user changes — the user's working tree must be clean entering the release, and the bundle commit is the only thing the release script itself adds on top.\n- DECISION POINT: the design doc (line 52) flags a choice between local build and CI build. This task implements the local-in-release-script path. Reasoning: the bundle must be committed BEFORE tagging (AGNTC installs from tags), and GitHub Actions runs AFTER tag push — too late to commit. Local build keeps the flow simple (build → commit → tag → push → CI validates). CI validates the committed bundle in Task 8-2 but does not rebuild it.\n- package.json already exists from Phase 1 (Task 1-1) with dev deps and build script. No changes needed to package.json.\n- node_modules/ is already gitignored from Phase 1. No changes needed.\n- The design doc also mentions (line 53) that GitHub releases may no longer be needed since AGNTC uses tags, not releases. Consider simplifying if trivial; defer if not. This is a note, not a requirement.\n\nAcceptance Criteria:\n- Release script runs `npm install` + `npm run build` between the dirty-tree check and the annotated tag\n- Updated bundle is committed before the tag is created\n- Bundle commit occurs in all version strategies, including `VERSION_STRATEGY=\"none\"` (previously commit-free)\n- Build failure aborts the release with a clear error message\n- Existing dirty-tree check still rejects unrelated uncommitted user changes\n- Existing release flow (version handling, tag, push) is preserved after the build step\n- Tagged releases contain the up-to-date knowledge.cjs bundle\n- node_modules/ remains gitignored\n\nTests:\n- Test file: tests/scripts/test-release-build.sh (shell test)\n- it runs the build step before tagging\n- it commits the updated bundle when the diff shows changes (in both default and `none` version strategies)\n- it skips the commit when the bundle is unchanged\n- it aborts release on build failure\n- it still rejects dirty working tree on entry\n- Manual validation:\n - Run release with modified source -\u003e verify bundle is rebuilt and committed before tag\n - Run release with no source changes -\u003e verify no unnecessary commit\n - Run release with `VERSION_STRATEGY=\"none\"` and modified source -\u003e verify bundle commit still happens\n\nEdge Cases:\n- First release after Phase 1 — bundle already exists and is committed. Build step rebuilds it (may or may not change depending on esbuild determinism). If unchanged, no commit.\n- npm install fails (network error, registry down) — abort release with clear error\n- Bundle size grows significantly in later phases — Task 1-1 sets a size threshold. If build produces a bundle over that, Task 1-1's test-knowledge-build.sh fails, which blocks CI in Task 8-2, which blocks the release.\n- Git working directory has uncommitted changes at script start — the existing dirty-tree gate aborts before any build step runs. User must commit or stash first.\n- Uncommitted bundle diff at script start — same as above, aborts on the dirty-tree gate. User handles manually.\n\nSpec Reference: knowledge-base/design.md — Release Process section (lines 45-54, build before tagging, bundle committed, local vs CI decision, GitHub releases simplification consideration), Build and Distribution section (lines 448-457, bundle committed to repo, release workflow change required). Script file to modify: ./release (perform_release function — dirty-tree check, VERSION_STRATEGY branch, tag creation).","parent":"tick-74a599","created":"2026-04-09T19:06:33Z","updated":"2026-04-10T20:47:18Z"} -{"id":"tick-de9b1a","title":"CI Test Pipeline Update","status":"open","priority":2,"type":"task","refs":["knowledge-base-8-2"],"description":"Problem: The GitHub Actions release workflow runs tests on tag push but currently only runs migration tests and discovery tests. The knowledge base adds new test files (Node .cjs and shell .sh) that must be included in the CI pipeline to prevent regressions from reaching tagged releases. Additionally, test-workflow-manifest.sh (which will gain manifest resolve tests from Task 3-2) is NOT currently run in CI since it doesn't match the existing glob patterns.\n\nSolution: Update the GitHub Actions release workflow to include all knowledge base test files alongside the existing test suite, explicitly include test-workflow-manifest.sh, and ensure the Node version is 18+ as required by Orama.\n\nOutcome: Every tagged release runs the full test suite including all knowledge base tests and the manifest resolve test, catching regressions before the release is published.\n\nDo:\n- Read the existing GitHub Actions workflow at .github/workflows/release.yml to understand the current test structure. The current pipeline (from the explore agent findings):\n - Run discovery tests: for test in tests/scripts/test-discovery-*.cjs; do node --test test; done\n - Run migration tests: for test in tests/scripts/test-migration-*.sh; do /bin/bash test; done\n\n- Verify and update the Node version. Design doc line 453 requires Node 18 minimum (Orama requirement), Node 20+ recommended. Check the workflow's node-version setting. If it uses an older version or is unspecified, update it to 20.\n\n- Add npm install step BEFORE the test runs. Unit tests (test-knowledge-embeddings.cjs, test-knowledge-store.cjs, test-knowledge-chunker.cjs, test-knowledge-config.cjs) import from src/knowledge/ source files, which require @orama/orama and @msgpack/msgpack from node_modules. npm install provides these dev dependencies.\n\n- DO NOT add npm run build in CI. The bundle is committed before tagging (per Task 8-1's local build decision). CI validates the committed bundle — it does not rebuild. Rebuilding in CI could produce a different bundle if esbuild has any non-determinism, which would defeat the purpose of committing the bundle before tagging. The integration test (Task 1-5) imports from the committed bundle directly.\n\n- Add knowledge base test runs in the same pattern as existing tests:\n\n Knowledge Node tests:\n for test in tests/scripts/test-knowledge-*.cjs; do\n node --test \"$test\"\n done\n This glob covers all Node test files created across Phases 1-4 (embeddings, store, chunker, config, integration, retry, openai).\n\n Knowledge shell tests:\n for test in tests/scripts/test-knowledge-*.sh; do\n /bin/bash \"$test\"\n done\n This glob covers all shell test files created across Phases 1-4 (build, cli).\n\n- EXPLICITLY include test-workflow-manifest.sh. This file exists (pre-knowledge-base) and will gain manifest resolve tests from Task 3-2. It does NOT match test-discovery-*.cjs or test-migration-*.sh globs, so it is currently NOT run in CI. Add it as a direct invocation:\n /bin/bash tests/scripts/test-workflow-manifest.sh\n This is a pre-existing CI gap that Phase 8 is closing.\n\n- The test pipeline ordering should be:\n 1. checkout (existing)\n 2. setup-node with node-version: 20 (verify/update)\n 3. npm install (new — for unit test dev deps)\n 4. Run migration tests (existing)\n 5. Run discovery tests (existing)\n 6. Run test-workflow-manifest.sh (new — explicit)\n 7. Run knowledge Node tests (new)\n 8. Run knowledge shell tests (new)\n\n- The OpenAI integration test (test-knowledge-openai-integration.cjs) is opt-in — it only runs when OPENAI_API_KEY env var is present. In CI, this env var is NOT set by default (no API keys in CI). The test skips itself. If the repo owner wants to run it in CI, they can add the secret to GitHub Actions — but this is optional, not required.\n\nAcceptance Criteria:\n- GitHub Actions workflow uses Node 20+ (verified or updated)\n- GitHub Actions workflow runs npm install before tests (for unit test dev deps)\n- GitHub Actions workflow does NOT run npm run build (bundle is already committed per Task 8-1)\n- GitHub Actions workflow runs all test-knowledge-*.cjs tests\n- GitHub Actions workflow runs all test-knowledge-*.sh tests\n- GitHub Actions workflow explicitly runs tests/scripts/test-workflow-manifest.sh\n- All tests pass in CI (the integration test skips without API key)\n- Existing migration and discovery tests continue to run\n- Test failure blocks the release (non-zero exit fails the workflow)\n\nTests:\n- No separate test for this task — the CI pipeline IS the test. Validation:\n - Push a tag and verify the GitHub Actions workflow runs all knowledge tests + test-workflow-manifest.sh\n - Verify the integration test is skipped (no OPENAI_API_KEY in CI)\n - Verify a test failure would block the release\n\nEdge Cases:\n- npm install in CI takes time — consider caching node_modules across runs (GitHub Actions cache action). Not required but improves CI speed.\n- Knowledge shell tests may depend on node being in PATH to run the bundle — verify the CI environment has node available for shell test subprocesses.\n- Test file ordering — tests should be independent and not depend on execution order. If any knowledge test depends on state from another test, that is a bug in the test.\n- Committed bundle mismatch — if the committed bundle is stale (developer forgot to rebuild before tagging), CI tests against it will either pass (if the bundle is still functionally correct) or fail (if tests assume newer behavior). This is a content issue, not a CI configuration issue. Task 8-1's build-before-tag flow prevents this.\n\nSpec Reference: knowledge-base/design.md — Testing Strategy section (lines 463-475, test location conventions, CLI command tests against built bundle), Build and Distribution section (lines 448-457, bundle committed to repo, release workflow needs build step — implemented in Task 8-1), Node version requirement (line 453)","parent":"tick-74a599","created":"2026-04-09T19:08:28Z","updated":"2026-04-10T15:29:37Z"} +{"id":"tick-74a599","title":"Phase 8: Release Process","status":"done","priority":2,"refs":["knowledge-base-8"],"transitions":[{"from":"open","to":"in_progress","at":"2026-04-20T18:52:42Z","auto":true},{"from":"in_progress","to":"done","at":"2026-04-20T19:05:06Z","auto":true}],"parent":"tick-cbbd13","created":"2026-04-09T19:05:44Z","updated":"2026-04-20T19:05:06Z","closed":"2026-04-20T19:05:06Z"} +{"id":"tick-7f4725","title":"Release Script Build Integration","status":"done","priority":2,"type":"task","refs":["knowledge-base-8-1"],"description":"Problem: The knowledge CLI bundle (knowledge.cjs) must be committed to the repo before tagging because AGNTC installs from git tags with no build step. During Phases 1-7 development, the bundle was built and committed manually. For production releases, the build must be formalised into the release pipeline to prevent shipping stale bundles.\n\nSolution: Integrate the esbuild build step into the local release script so the bundle is always fresh when a release tag is created.\n\nOutcome: The release process automatically builds knowledge.cjs from source before tagging, ensuring every tagged release has an up-to-date bundle.\n\nDo:\n- Read the existing release script at the project root (./release) before editing. Study:\n 1. Where perform_release runs the dirty-tree gate (git status --porcelain check that aborts on any uncommitted changes)\n 2. Where perform_release currently stages/commits (note: this is gated inside an `if [[ \"$VERSION_STRATEGY\" != \"none\" ]]` block — the default \"none\" strategy currently performs no commit at all, only tag + push)\n 3. Where the annotated tag is created and pushed\n- The build step must sit AFTER the existing dirty-tree check and BEFORE the annotated tag is created. Placing it before the dirty-tree check would cause the gate to abort on a bundle rebuild; placing it after the tag would ship a stale bundle in the tagged commit.\n- The build step sequence:\n 1. Run `npm install` (ensure dev dependencies are available)\n 2. Run `npm run build` (esbuild bundles src/knowledge/ into skills/workflow-knowledge/scripts/knowledge.cjs)\n 3. Check whether the bundle changed (e.g., git diff on the bundle path)\n 4. If it changed: stage and commit the updated bundle. IMPORTANT: this commit must occur in ALL version strategies, including `VERSION_STRATEGY=\"none\"` — the existing `none` branch performs no commits, so this task is introducing a new commit point into a previously commit-free path. Do not hide the bundle commit inside the existing version-bump commit block; it is a distinct concern and must run regardless of version strategy.\n 5. If the bundle did not change: no commit needed, proceed.\n 6. Proceed with the existing tag + push flow.\n- Build failure (non-zero exit from `npm run build`) must abort the release — do not tag with a stale or missing bundle. Clean exit with a clear error.\n- The bundle commit is the only permitted mutation after the dirty-tree check passes. The dirty-tree check still protects against unrelated uncommitted user changes — the user's working tree must be clean entering the release, and the bundle commit is the only thing the release script itself adds on top.\n- DECISION POINT: the design doc (line 52) flags a choice between local build and CI build. This task implements the local-in-release-script path. Reasoning: the bundle must be committed BEFORE tagging (AGNTC installs from tags), and GitHub Actions runs AFTER tag push — too late to commit. Local build keeps the flow simple (build → commit → tag → push → CI validates). CI validates the committed bundle in Task 8-2 but does not rebuild it.\n- package.json already exists from Phase 1 (Task 1-1) with dev deps and build script. No changes needed to package.json.\n- node_modules/ is already gitignored from Phase 1. No changes needed.\n- The design doc also mentions (line 53) that GitHub releases may no longer be needed since AGNTC uses tags, not releases. Consider simplifying if trivial; defer if not. This is a note, not a requirement.\n\nAcceptance Criteria:\n- Release script runs `npm install` + `npm run build` between the dirty-tree check and the annotated tag\n- Updated bundle is committed before the tag is created\n- Bundle commit occurs in all version strategies, including `VERSION_STRATEGY=\"none\"` (previously commit-free)\n- Build failure aborts the release with a clear error message\n- Existing dirty-tree check still rejects unrelated uncommitted user changes\n- Existing release flow (version handling, tag, push) is preserved after the build step\n- Tagged releases contain the up-to-date knowledge.cjs bundle\n- node_modules/ remains gitignored\n\nTests:\n- Test file: tests/scripts/test-release-build.sh (shell test)\n- it runs the build step before tagging\n- it commits the updated bundle when the diff shows changes (in both default and `none` version strategies)\n- it skips the commit when the bundle is unchanged\n- it aborts release on build failure\n- it still rejects dirty working tree on entry\n- Manual validation:\n - Run release with modified source -\u003e verify bundle is rebuilt and committed before tag\n - Run release with no source changes -\u003e verify no unnecessary commit\n - Run release with `VERSION_STRATEGY=\"none\"` and modified source -\u003e verify bundle commit still happens\n\nEdge Cases:\n- First release after Phase 1 — bundle already exists and is committed. Build step rebuilds it (may or may not change depending on esbuild determinism). If unchanged, no commit.\n- npm install fails (network error, registry down) — abort release with clear error\n- Bundle size grows significantly in later phases — Task 1-1 sets a size threshold. If build produces a bundle over that, Task 1-1's test-knowledge-build.sh fails, which blocks CI in Task 8-2, which blocks the release.\n- Git working directory has uncommitted changes at script start — the existing dirty-tree gate aborts before any build step runs. User must commit or stash first.\n- Uncommitted bundle diff at script start — same as above, aborts on the dirty-tree gate. User handles manually.\n\nSpec Reference: knowledge-base/design.md — Release Process section (lines 45-54, build before tagging, bundle committed, local vs CI decision, GitHub releases simplification consideration), Build and Distribution section (lines 448-457, bundle committed to repo, release workflow change required). Script file to modify: ./release (perform_release function — dirty-tree check, VERSION_STRATEGY branch, tag creation).","transitions":[{"from":"open","to":"in_progress","at":"2026-04-20T18:52:42Z","auto":false},{"from":"in_progress","to":"done","at":"2026-04-20T18:59:48Z","auto":false}],"parent":"tick-74a599","created":"2026-04-09T19:06:33Z","updated":"2026-04-20T18:59:48Z","closed":"2026-04-20T18:59:48Z"} +{"id":"tick-de9b1a","title":"CI Test Pipeline Update","status":"done","priority":2,"type":"task","refs":["knowledge-base-8-2"],"description":"Problem: The GitHub Actions release workflow runs tests on tag push but currently only runs migration tests and discovery tests. The knowledge base adds new test files (Node .cjs and shell .sh) that must be included in the CI pipeline to prevent regressions from reaching tagged releases. Additionally, test-workflow-manifest.sh (which will gain manifest resolve tests from Task 3-2) is NOT currently run in CI since it doesn't match the existing glob patterns.\n\nSolution: Update the GitHub Actions release workflow to include all knowledge base test files alongside the existing test suite, explicitly include test-workflow-manifest.sh, and ensure the Node version is 18+ as required by Orama.\n\nOutcome: Every tagged release runs the full test suite including all knowledge base tests and the manifest resolve test, catching regressions before the release is published.\n\nDo:\n- Read the existing GitHub Actions workflow at .github/workflows/release.yml to understand the current test structure. The current pipeline (from the explore agent findings):\n - Run discovery tests: for test in tests/scripts/test-discovery-*.cjs; do node --test test; done\n - Run migration tests: for test in tests/scripts/test-migration-*.sh; do /bin/bash test; done\n\n- Verify and update the Node version. Design doc line 453 requires Node 18 minimum (Orama requirement), Node 20+ recommended. Check the workflow's node-version setting. If it uses an older version or is unspecified, update it to 20.\n\n- Add npm install step BEFORE the test runs. Unit tests (test-knowledge-embeddings.cjs, test-knowledge-store.cjs, test-knowledge-chunker.cjs, test-knowledge-config.cjs) import from src/knowledge/ source files, which require @orama/orama and @msgpack/msgpack from node_modules. npm install provides these dev dependencies.\n\n- DO NOT add npm run build in CI. The bundle is committed before tagging (per Task 8-1's local build decision). CI validates the committed bundle — it does not rebuild. Rebuilding in CI could produce a different bundle if esbuild has any non-determinism, which would defeat the purpose of committing the bundle before tagging. The integration test (Task 1-5) imports from the committed bundle directly.\n\n- Add knowledge base test runs in the same pattern as existing tests:\n\n Knowledge Node tests:\n for test in tests/scripts/test-knowledge-*.cjs; do\n node --test \"$test\"\n done\n This glob covers all Node test files created across Phases 1-4 (embeddings, store, chunker, config, integration, retry, openai).\n\n Knowledge shell tests:\n for test in tests/scripts/test-knowledge-*.sh; do\n /bin/bash \"$test\"\n done\n This glob covers all shell test files created across Phases 1-4 (build, cli).\n\n- EXPLICITLY include test-workflow-manifest.sh. This file exists (pre-knowledge-base) and will gain manifest resolve tests from Task 3-2. It does NOT match test-discovery-*.cjs or test-migration-*.sh globs, so it is currently NOT run in CI. Add it as a direct invocation:\n /bin/bash tests/scripts/test-workflow-manifest.sh\n This is a pre-existing CI gap that Phase 8 is closing.\n\n- The test pipeline ordering should be:\n 1. checkout (existing)\n 2. setup-node with node-version: 20 (verify/update)\n 3. npm install (new — for unit test dev deps)\n 4. Run migration tests (existing)\n 5. Run discovery tests (existing)\n 6. Run test-workflow-manifest.sh (new — explicit)\n 7. Run knowledge Node tests (new)\n 8. Run knowledge shell tests (new)\n\n- The OpenAI integration test (test-knowledge-openai-integration.cjs) is opt-in — it only runs when OPENAI_API_KEY env var is present. In CI, this env var is NOT set by default (no API keys in CI). The test skips itself. If the repo owner wants to run it in CI, they can add the secret to GitHub Actions — but this is optional, not required.\n\nAcceptance Criteria:\n- GitHub Actions workflow uses Node 20+ (verified or updated)\n- GitHub Actions workflow runs npm install before tests (for unit test dev deps)\n- GitHub Actions workflow does NOT run npm run build (bundle is already committed per Task 8-1)\n- GitHub Actions workflow runs all test-knowledge-*.cjs tests\n- GitHub Actions workflow runs all test-knowledge-*.sh tests\n- GitHub Actions workflow explicitly runs tests/scripts/test-workflow-manifest.sh\n- All tests pass in CI (the integration test skips without API key)\n- Existing migration and discovery tests continue to run\n- Test failure blocks the release (non-zero exit fails the workflow)\n\nTests:\n- No separate test for this task — the CI pipeline IS the test. Validation:\n - Push a tag and verify the GitHub Actions workflow runs all knowledge tests + test-workflow-manifest.sh\n - Verify the integration test is skipped (no OPENAI_API_KEY in CI)\n - Verify a test failure would block the release\n\nEdge Cases:\n- npm install in CI takes time — consider caching node_modules across runs (GitHub Actions cache action). Not required but improves CI speed.\n- Knowledge shell tests may depend on node being in PATH to run the bundle — verify the CI environment has node available for shell test subprocesses.\n- Test file ordering — tests should be independent and not depend on execution order. If any knowledge test depends on state from another test, that is a bug in the test.\n- Committed bundle mismatch — if the committed bundle is stale (developer forgot to rebuild before tagging), CI tests against it will either pass (if the bundle is still functionally correct) or fail (if tests assume newer behavior). This is a content issue, not a CI configuration issue. Task 8-1's build-before-tag flow prevents this.\n\nSpec Reference: knowledge-base/design.md — Testing Strategy section (lines 463-475, test location conventions, CLI command tests against built bundle), Build and Distribution section (lines 448-457, bundle committed to repo, release workflow needs build step — implemented in Task 8-1), Node version requirement (line 453)","transitions":[{"from":"open","to":"in_progress","at":"2026-04-20T19:00:03Z","auto":false},{"from":"in_progress","to":"done","at":"2026-04-20T19:05:06Z","auto":false}],"parent":"tick-74a599","created":"2026-04-09T19:08:28Z","updated":"2026-04-20T19:05:06Z","closed":"2026-04-20T19:05:06Z"} diff --git a/README.md b/README.md index a9d3af9f4..0108620b9 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,24 @@ npx agntc remove leeovery/agentic-workflows ### Requirements - Node.js 18+ +- (Optional) OpenAI API key — enables semantic search across your workflow history. See [Knowledge Base](#knowledge-base) below; stub mode is available if you'd rather skip embeddings. + +### Knowledge Base + +Workflows record what's been researched, decided, and built into a per-project knowledge base. Subsequent phases query this base to surface prior context — e.g. discussion checks if a similar topic was already decided, planning checks how comparable specs were structured, review checks for cross-work-unit consistency. + +**First run** — before any workflow command can execute, the system checks that the knowledge base is initialised. New installs and existing projects upgrading to this version will be prompted to run setup once: + +```bash +node .claude/skills/workflow-knowledge/scripts/knowledge.cjs setup +``` + +The wizard is interactive and walks through: +- **Embedding provider**: choose `openai` (semantic + keyword search) or `skip` (keyword-only). +- **OpenAI API key** (only if you chose openai): provided via `$OPENAI_API_KEY` in your shell, or stored at `~/.config/workflows/credentials.json` (mode 0600). The wizard validates the key with a test embed before committing config. +- **Project init**: creates `.workflows/.knowledge/` with the store, metadata, and config. + +**Stub mode** — if you don't want to use an embedding API, choose `skip` at the provider prompt. Search falls back to BM25 keyword matching only. You can switch to OpenAI later by re-running `knowledge setup`. ### Your First Workflow diff --git a/build/knowledge.build.js b/build/knowledge.build.js index 9eb309169..c062d60d7 100644 --- a/build/knowledge.build.js +++ b/build/knowledge.build.js @@ -25,6 +25,12 @@ esbuild format: 'cjs', target: 'node18', minify: true, + // Force ESM resolution for dependencies even though we emit CJS output. + // Orama and @msgpack/msgpack both ship ESM dists with sideEffects: false; + // ESM resolution lets esbuild tree-shake in a way CJS barrel files block. + // Net effect measured: ~22 KB shaved off the bundle with no behaviour change. + conditions: ['node', 'import'], + mainFields: ['module', 'main'], logLevel: 'info', }) .catch((err) => { diff --git a/knowledge-base/deferred-issues.md b/knowledge-base/deferred-issues.md index d07ab4d22..39bffbebb 100644 --- a/knowledge-base/deferred-issues.md +++ b/knowledge-base/deferred-issues.md @@ -6,118 +6,121 @@ Format: `### N. Title — Severity` Each entry records: where, what, why it was deferred, and a mitigation idea. Circle back when working on the owning area. +Items below marked **RESOLVED** were addressed during the pre-merge cleanup pass on `feat/knowledge-base-phase-8` (commits after 2026-04-20). Each entry still records the original problem for provenance. + --- ## Phase 4 (CLI Complete) — Deferred -### 1. Index/rebuild TOCTOU on embedding dimensions — Critical-rare +### 1. Index/rebuild TOCTOU on embedding dimensions — Critical-rare — **RESOLVED** **Location:** `src/knowledge/index.js` `indexSingleFile` lines ~400–450. **Description:** Embeddings are computed using `effectiveProvider.dimensions()` BEFORE acquiring the lock. Inside the lock the store is reloaded but provider-state is not re-validated. If a concurrent `rebuild` recreates the store with different dimensions between embed and insert, `insertDocument` will fail or corrupt the vector index. **Why deferred:** Requires concurrent rebuild during indexing — extremely rare in single-developer use. Phase 5+ skill orchestration will serialize. **Mitigation:** Re-validate provider state inside the lock after store reload; abort if schema mismatch. -### 2. Pending queue unbounded growth on persistent failures — Medium +### 2. Pending queue unbounded growth on persistent failures — Medium — **RESOLVED** **Location:** `src/knowledge/index.js` `processPendingQueue` catch block. **Description:** Items that fail catch-up retry stay in the queue forever. No max-retry counter, no eviction. If failure is permanent (renamed work unit, malformed file), each bulk run wastes 3 OpenAI calls per item indefinitely. **Mitigation:** Add `attempts` counter to pending entry; evict at 10 with stderr warning. -### 3. Rebuild has no rollback — Medium +### 3. Rebuild has no rollback — Medium — **RESOLVED** **Location:** `src/knowledge/index.js` `cmdRebuild`. **Description:** Deletes `store.msp` and `metadata.json` BEFORE running bulk index. If bulk index throws (network down, OpenAI outage), left with no store, no metadata. **Mitigation:** Move old files to `.bak` suffix; restore on bulk-index failure. -### 4. `getWorkUnitMeta` / `discoverArtifacts` / `runManifest` swallow all errors — Medium +### 4. `getWorkUnitMeta` / `discoverArtifacts` / `runManifest` swallow all errors — Medium — **RESOLVED** -**Location:** Multiple helpers in `src/knowledge/index.js`. +**Location:** Multiple helpers in `src/knowledge/index.js`; convention in `skills/workflow-manifest/scripts/manifest.cjs`. **Description:** Catch-alls return null/empty on manifest CLI failure. Hides broken MANIFEST_JS path, corrupt manifest JSON, etc. Compact and status consistency checks silently skip work units; bulk index reports "0 files" on broken manifest. **Mitigation:** Distinguish exit-code-1 (key not found) from other errors; surface unexpected errors to stderr at minimum. +**Resolution:** `manifest.cjs` `die()` now takes an optional exit code — 2 for "expected miss" (not-found paths, missing work units, missing values), 1 for real errors (corrupt JSON, validation failures, bad args). Knowledge-base helpers classify via `err.status === 2` on execFileSync — stable and not reliant on stderr-text regex. 10 die() sites updated; 4 tests updated to assert the new codes; one new test covers the exit-code contract directly. -### 5. `MANIFEST_JS` fallback resolves silently to non-existent path — Medium +### 5. `MANIFEST_JS` fallback resolves silently to non-existent path — Medium — **RESOLVED** **Location:** `src/knowledge/index.js` MANIFEST_JS constant (~line 26). **Description:** If neither candidate path exists, fallback resolves to a path that doesn't exist. `execFileSync` throws ENOENT, caught silently in `discoverArtifacts`, returning empty array. Bulk index becomes a silent no-op. **Mitigation:** Throw at module load if MANIFEST_JS doesn't resolve to an existing file. -### 6. Status shells manifest CLI per spec topic — Low (perf) +### 6. Status shells manifest CLI per spec topic — Low (perf) — **RESOLVED** **Location:** `src/knowledge/index.js` `cmdStatus` superseded-spec consistency check. **Description:** N node processes spawned for N spec topics. Status slow on repos with many specs (~5s for 50 topics). **Mitigation:** Cache full manifest once per status invocation; read spec statuses from the cached object. -### 7. `--work-unit` filter vs boost semantics — Low (UX) +### 7. `--work-unit` filter vs boost semantics — Low (UX) — **RESOLVED** -**Location:** `src/knowledge/index.js` `cmdQuery`. -**Description:** `--work-unit` is a re-rank proximity boost, not a hard filter. Inconsistent with `--phase`/`--work-type`/`--topic` which are filters. Usage text was updated to clarify, but inconsistency remains. -**Mitigation:** Phase 5+: introduce separate `--boost-work-unit` and let `--work-unit` filter; or add `--scope work-unit:foo` syntax. +**Location:** `src/knowledge/index.js` `cmdQuery`, `cmdRemove`. +**Description:** `--work-unit` was a re-rank proximity boost on `query` but a hard filter on `remove` — same flag name, opposite semantics. Inconsistent with `--phase`/`--work-type`/`--topic` (all filters). Docs alone weren't enough; the flag spelling itself invited misuse. +**Resolution:** Fully orthogonal CLI — every `--` flag is a hard filter on every command that accepts it (`--work-unit`, `--work-type`, `--phase`, `--topic`). Re-ranking happens exclusively through `--boost: `, which is repeatable, composable across dimensions, and validated against a fixed set of fields (`work-unit`, `work-type`, `phase`, `topic`, `confidence`). `+0.1` per match, additive. Unknown field or missing value → fail-fast error. Skill templates can now compose multi-dimensional bias (e.g. `--boost:work-unit auth-flow --boost:phase research`) that wasn't expressible before. -### 8. Migration `report_update` called unconditionally — Low +### 8. Migration `report_update` called unconditionally — Low — **WITHDRAWN** **Location:** `skills/workflow-migrate/scripts/migrations/036-completed-at.sh` (and pattern from 035). **Description:** `report_update` is called even when 0 work units were modified. Inflates orchestrator counter; may trigger false "review changes" prompt. -**Mitigation:** Cross-migration cleanup — have node script exit 2 when nothing modified; bash dispatches `report_update` vs `report_skip` on exit code. +**Why withdrawn:** Migrations are point-in-time snapshots. Once a migration id is recorded in `.workflows/.state/migrations`, it never re-runs — editing the bash wrapper helps only users who haven't yet run it. The counter is a one-time UX nit during a single migration pass; the fix-vs-snapshot-principle tradeoff isn't worth it. -### 9. `withRetry` swallows programming errors like network errors — Low +### 9. `withRetry` swallows programming errors like network errors — Low — **RESOLVED** **Location:** `src/knowledge/index.js` `withRetry`. **Description:** A `TypeError` from a typo is retried 3× with 7s of sleep before rethrow. Wastes time during development. **Mitigation:** Discriminate error types — don't retry `TypeError`/`ReferenceError`/`SyntaxError`. -### 10. `indexSingleFile` stack trace lost in pending queue — Low +### 10. `indexSingleFile` stack trace lost in pending queue — Low — **RESOLVED** **Location:** `src/knowledge/index.js` `cmdIndexBulk` catch block. **Description:** `addToPendingQueue(item.file, err.message)` saves only the message, not the stack. Debugging relies on stderr which users may not capture. **Mitigation:** Accept and write a bounded stack snippet; or always `console.error(err.stack)` before queueing. -### 11. KEYWORD_ONLY_DIMENSIONS = 1536 silent provider lock-in — Medium (UX) +### 11. KEYWORD_ONLY_DIMENSIONS = 1536 silent provider lock-in — Medium (UX) — **RESOLVED** **Location:** `src/knowledge/index.js` `cmdIndex` / case 4 of `resolveProviderState`. **Description:** User first indexes without provider (keyword-only, schema dims=1536). Later configures OpenAI (also 1536 dims). Subsequent `knowledge index` calls silently stay keyword-only. Only `knowledge status` warns. User must know to run `rebuild`. **Mitigation:** Print upgrade note on `cmdIndex` when entering case 4 with a provider now configured. -### 12. OpenAIProvider mutates `res.data` in place — Low (style) +### 12. OpenAIProvider mutates `res.data` in place — Low (style) — **RESOLVED** **Location:** `src/knowledge/providers/openai.js` `embedBatch`. **Description:** `.sort()` mutates. Response used only locally so no observable effect. **Mitigation:** Use `[...res.data].sort(...)`. -### 13. OpenAIProvider `embed()` assumes non-empty `res.data[0]` — Low +### 13. OpenAIProvider `embed()` assumes non-empty `res.data[0]` — Low — **RESOLVED** **Location:** `src/knowledge/providers/openai.js` ~line 45. **Description:** If OpenAI returns `{ data: [] }` throws non-descriptive TypeError. Not observed in practice (OpenAI returns 400 for empty inputs). **Mitigation:** Guard and throw a provider-specific error. -### 14. Project config cannot unset system config field — Low +### 14. Project config cannot unset system config field — Low — **RESOLVED** **Location:** `src/knowledge/config.js` `loadConfig` merge loop. **Description:** `Object.assign`-style merge only copies defined values; setting project `model: undefined` cannot unset a system `model: "x"` default. **Mitigation:** Treat explicit `null` as unset sentinel in the merge. -### 15. `searchHybrid` similarity threshold may drop strong text-only matches — Low +### 15. `searchHybrid` similarity threshold may drop strong text-only matches — Low — **WITHDRAWN** **Location:** `src/knowledge/store.js` `searchHybrid`. -**Description:** Orama applies `similarity` as a filter on hybrid results; zero vector matches can mask strong BM25 matches. -**Mitigation:** Phase 5+ retrieval tuning — fall back to text-only if hybrid returns 0. +**Description:** Theoretical concern that Orama applies `similarity` as a filter on hybrid results; zero vector matches could mask strong BM25 matches. +**Why withdrawn:** Empirical probing of Orama's hybrid mode shows the concern doesn't manifest. With a flat/poor vector and `similarity: 0.99`, hybrid still returns BM25-driven hits (text matches come through regardless of similarity post-filter). The only way to get zero hybrid hits is when the term itself doesn't match — at which point a text-only fallback also returns zero. A defensive fulltext fallback was briefly added in commit `33303da1` then removed once probing confirmed it was unreachable. Orama's hybrid implementation already does the right thing. -### 16. `cmdRebuild` stdin left in flowing mode — Low +### 16. `cmdRebuild` stdin left in flowing mode — Low — **RESOLVED** **Location:** `src/knowledge/index.js` `cmdRebuild`. **Description:** After `process.stdin.resume()`, stdin stays flowing. Irrelevant for CLI but leaks if called as library. **Mitigation:** `process.stdin.pause()` after reading the line. -### 17. Bulk discovery misses work units not in project manifest — Medium +### 17. Bulk discovery misses work units not in project manifest — **WITHDRAWN** **Location:** `src/knowledge/index.js` `discoverArtifacts` → `manifest list`. -**Description:** `manifest list` reads from the project manifest, not the filesystem. Work units created before the project manifest system (legacy) are invisible to bulk index and status unindexed-artifact detection. Real-data testing on Tick showed 9 work units on disk but only 1 registered in project manifest → only 2 files indexed. -**Mitigation:** Fall back to filesystem scan (like `manifest list` already does when project manifest has no `work_units` key) or add a "register all" migration. +**Original claim:** `manifest list` reads from the project manifest, not the filesystem. Legacy work units invisible to bulk index. +**Why withdrawn:** The Tick-project data that motivated this entry (9 dirs on disk, 1 registered) was the result of a one-off project-manifest corruption bug — not a systemic code issue. Migration 031 already populates the registry from the filesystem; once it has run, the registry is authoritative and reading it directly is correct. Work units are created via `manifest init`, which registers them atomically. A work unit that exists on disk but not in the registry is either mid-migration or the registry has been corrupted externally — neither is something `manifest list` should paper over. --- ## Phase 5 (Skill Integration) — Deferred -### 18. Knowledge removal has no automatic retry on failure — Medium +### 18. Knowledge removal has no automatic retry on failure — Medium — **RESOLVED** **Location:** `skills/workflow-start/references/manage-work-unit.md` (cancellation), `skills/workflow-specification-process/references/spec-completion.md` (supersession), `skills/workflow-specification-process/references/promote-to-cross-cutting.md` (promotion). **Description:** When `knowledge remove` fails (store locked, CLI error), the skill displays a warning and tells the user to retry manually. Unlike indexing failures — which have a pending queue for automatic catch-up on the next `index` call — removal failures have no retry mechanism. Stale chunks from cancelled/superseded/promoted work persist in the knowledge base until the user manually runs `knowledge remove`. diff --git a/knowledge-base/design.md b/knowledge-base/design.md index a4fc770ee..07dbc18ca 100644 --- a/knowledge-base/design.md +++ b/knowledge-base/design.md @@ -205,7 +205,7 @@ search(db, { | MsgPack only | ~30 MB | ~240 MB | 1,010 ms | 1,085 ms | | MsgPack + gzip | ~19 MB | ~154 MB | 10,829 ms | 2,079 ms | -**MsgPack without gzip is the clear winner**: 3x faster serialize, 2x faster deserialize than JSON. Comparable size to JSON+gzip while being 19x faster to serialize. Both `@orama/orama` and `@msgpack/msgpack` are zero-dependency pure JS — the two libraries bundle to **90KB minified** (verified). With application code, the total `knowledge.cjs` is estimated at ~110-120KB minified. +**MsgPack without gzip is the clear winner**: 3x faster serialize, 2x faster deserialize than JSON. Comparable size to JSON+gzip while being 19x faster to serialize. Both `@orama/orama` and `@msgpack/msgpack` are zero-dependency pure JS — the two libraries bundle to **90KB minified** (verified). The shipped `knowledge.cjs` bundle is ~155 KB — well under the 200 KB ceiling. The original ~110-120 KB estimate did not account for the ESM-resolution wrapper esbuild adds to handle Orama's CJS/ESM dual export and for Orama's own minor version drift; the budget is the ceiling, not the estimate. **Performance at realistic scale** (with memory decay/compaction — see below): - Active working set: 500-2,000 chunks (specs forever + recent research/discussion) diff --git a/knowledge-base/planning.md b/knowledge-base/planning.md index f49dfcdd9..ad241a9bb 100644 --- a/knowledge-base/planning.md +++ b/knowledge-base/planning.md @@ -2,6 +2,8 @@ Specification: [design.md](design.md) +> Convention: the `[ ]` checkboxes throughout this plan and the per-phase task files (`phase-{N}-tasks.md`) are author-time planning artefacts that capture the **planned scope** of each phase. They are not progress trackers and intentionally remain unchecked even after implementation lands — completion is recorded in git history (merged phase PRs) rather than by mutating these files. + ## Phases ### Phase 1: Build Pipeline + Store Fundamentals diff --git a/knowledge-base/post-audit-followups.md b/knowledge-base/post-audit-followups.md new file mode 100644 index 000000000..02128ab89 --- /dev/null +++ b/knowledge-base/post-audit-followups.md @@ -0,0 +1,366 @@ +# Post-Audit Follow-ups + +Items surfaced by the six-agent re-audit on `feat/knowledge-base-phase-8` that were **not** resolved during the cleanup pass. Each entry has full context so a fresh agent can pick it up without backreading the conversation. + +The agent picking these up should: +- Read `CLAUDE.md` first (especially "Conditional Routing" and "Skill File Structure" sections — both have load-bearing rules for the markdown items below). +- Read `knowledge-base/deferred-issues.md` for the closed audit ledger (do not reopen items there; they're decided). +- Run the test suite (CLI, store, integration, smoke) before and after each fix. +- Each fix gets its own commit. Commit messages reference the item number from this file. + +--- + +## Branch context + +`feat/knowledge-base-phase-8` is the head of a 4-PR stack (5 → 6 → 7 → 8). Phases 1–4 are merged to main. Phases 5–8 stack open against each other; none merge to main until go-live. The branch had a 26-commit cleanup pass following an initial multi-agent audit. A second six-agent re-audit ran after the cleanup. This file logs what the re-audit surfaced that hasn't been fixed. + +--- + +## Pre-merge musts (the four "tightly scoped" items I batched and then failed to land) + +### #1 — `view-completed.md` reactivation flow has a double-nested bold + missing announcement on edge case (✅ commit `a8152e1d`) + +**File:** `skills/workflow-start/references/view-completed.md:115-137` + +**Context.** When a user reactivates a completed work unit (chooses `r`/`reactivate` in the manage menu), the file flips status to in-progress, then branches on whether the prior status was `cancelled` or `completed`. The completed branch was added during Minor #6 of the cleanup pass (commit `87109d98`) to clear a stale `completed_at` field. Two issues: + +1. **Double-nested bold conditional.** Structure today: + - Outer (line 95): `#### If user chose r/reactivate` (H4) + - First-level bold (line 115): `**If selected.status was completed:**` + - Second-level bold (line 125): `**If the output is true:**` ← double-nest + + `CLAUDE.md` line 622-629 + 639 forbids this. The canonical fix per CLAUDE.md is to flatten by combining conditions into a single bold conditional. The example in CLAUDE.md (lines 624-628) literally shows: + + ``` + **If work_type is not set and other discussions exist:** + ... + **If work_type is not set and no discussions remain:** + ... + ``` + +2. **Announcement only renders on the `true` path.** The `"{name} reactivated."` block is indented inside `**If the output is true:**` (line 131-135). If `manifest exists completed_at` returns `false`, control falls from line 124 → line 137 (`→ Return to caller.`) without rendering the announcement. The cancelled branch (line 103) renders unconditionally. + + When `completed_at` is absent in real life: + - Pre-migration-036 projects (migration not yet run). + - Completed WU with no artifact files (migrations 036/037 skip backfill — `latestMtime === 0`). + - Hand-edited manifests. + +**Fix shape (per CLAUDE.md canonical pattern):** + +The H4 `#### If user chose r/reactivate` stays. Underneath, three peer bold conditionals (matching the three real cases): + +- `**If selected.status was cancelled:**` — existing, unchanged +- `**If selected.status was completed and completed_at is set:**` — delete + announce + return +- `**If selected.status was completed and completed_at is not set:**` — announce + return + +The `manifest exists completed_at` probe sits **before** the bold branches (shared setup, like the canonical example's "1. Shared setup steps..." at CLAUDE.md line 611). It's harmless to run on the cancelled path (output ignored). Lead-in narrative ("Completed work units retain their chunks") moves into each completed-X bold branch where it's relevant. + +**Severity.** Convention violation, not a runtime bug. The flow works on the common path. Edge case (completed-without-completed_at) loses the announcement but the user sees the WU reappear in the in-progress list. + +**Why I failed to land this.** I iterated edits while reasoning instead of writing the full target block first and applying once. Each correction introduced new drift. Multiple attempts violated the convention I was trying to fix. + +**Acceptance criteria for the fix:** +- Zero double-nested bolds. +- Announcement renders on every reactivation path. +- Pattern matches the CLAUDE.md example at lines 622-629 (compound-condition bold siblings). +- All other tests still pass. + +--- + +### #2 — Review process signpost still invites proactive querying (✅ commit `40ce4038`) + +**File:** `skills/workflow-review-process/SKILL.md:254-257` + +**Context.** During Important #11 of the cleanup pass, I rewrote the "Knowledge Usage" step signposts in `workflow-planning-process/SKILL.md` and `workflow-implementation-process/SKILL.md` because they invited proactive querying while the loaded reference (`knowledge-usage.md` Section E) explicitly forbade it for those phases. I missed `workflow-review-process/SKILL.md` — its signpost still reads: + +``` +> Loading the usage guide for the knowledge base so +> proactive querying is available while verifying decisions. +``` + +But `knowledge-usage.md:89` Section E says: + +> *"Review — query only for cross-work-unit consistency checks ('does this mirror how similar decisions were made elsewhere?'). Consistency with the current spec is already in scope — no KB needed for that."* + +**Fix shape.** Rewrite the signpost to match the planning/implementation pattern — frame as "rules for narrow use", not "green light to query". Reference the planning + implementation rewrites (commit `00419cd2`) as the pattern. + +Suggested wording: + +``` +> Loading the usage guide for the knowledge base. Review against the +> current spec is in scope without the KB — the guide documents the +> narrow case where a cross-work-unit consistency check is warranted. +``` + +(Or whatever wording the agent considers cleaner; structural goal is "frames the reference as 'rules for when use is warranted', not 'use freely'".) + +**Severity.** Cosmetic semantic drift. The reference still reaches the model with the correct guidance; the signpost just contradicts it. Worst case: model queries slightly more than the design wants. + +**Acceptance criteria:** +- Signpost matches the spirit of the planning/implementation rewrites. +- Inline nudge later in the same skill (line ~283 area) is already correct — leave alone unless drift is found there too. + +--- + +### #3 — `cmdIndexBulk` catch dumps stack for UserError (✅ commit `e7b9d018`) + +**File:** `src/knowledge/index.js:755-763` + +**Context.** During Important #7 of the cleanup pass (commit `646245b8`), I introduced a `UserError` class with three contracts: +1. `withRetry` skips it (no retry). +2. `main().catch` prints `Error: ` alone, no stack. +3. Thrown only at user-visible validation sites. + +The third contract holds at `main().catch` but `cmdIndexBulk`'s per-file catch block writes `err.stack` unconditionally on every failure: + +```js +} catch (err) { + // All retries exhausted — add to pending queue. Write the stack to + // stderr so debugging does not depend on users capturing it later. + await addToPendingQueue(item.file, err.message); + process.stderr.write( + `Failed to index ${item.file} after 3 attempts: ${err.message}. Added to pending queue.\n` + ); + if (err.stack) process.stderr.write(err.stack + '\n'); +} +``` + +When `indexSingleFile` throws a `UserError` (chunking-config-not-found, empty-file refusal — both convert via the UserError rollout), the bulk loop dumps the full Node stack frames during a bulk run. Noisy output for what's a clean validation error. + +**Fix shape.** Gate the stack write on `!(err instanceof UserError)`: + +```js +if (err.stack && !(err instanceof UserError)) process.stderr.write(err.stack + '\n'); +``` + +**Severity.** Cosmetic. Information is correct, just noisy. + +**Acceptance criteria:** +- Stack still prints for genuine internal errors (TypeError, etc.). +- UserError instances print only the message line. +- A focused test could be added: index a directory containing a malformed file, assert no stack frames in stderr — but probably overkill for this small fix. + +--- + +### #4 — `workflow-start/SKILL.md` Step 0.2 has paraphrased signpost (✅ commit `374ac15d`) + +**File:** `skills/workflow-start/SKILL.md:67-69` + +**Context.** During Minor #4 of the cleanup pass (commit `8c6fd446`), I added the canonical "already invoked" gate to Step 0.2. While doing so I left the existing two-line signpost intact: + +``` +> Running migrations to keep workflow files in sync. +> This ensures everything is up to date before we proceed. +``` + +The other 9 entry-point sites (`start-bugfix`, `start-cross-cutting`, `start-epic`, `start-feature`, `start-quickfix`, `continue-bugfix`, `continue-cross-cutting`, `continue-epic`, `continue-feature`, `continue-quickfix`) all use a 1-line canonical signpost: + +``` +> Running migrations to keep workflow files in sync. +``` + +**Fix shape.** Drop the second line so workflow-start matches the canonical 1-line shape. + +**Severity.** Pure consistency drift. No behavioural impact. + +**Acceptance criteria:** +- All 10 entry-point sites have byte-identical Step 0.2 signpost text. + +--- + +## Should-fixes (real correctness/UX issues, not introduced by the cleanup) + +### #5 — OpenAI 401/403 errors are retried (✅ commit `6cd6ccba`) + +**File:** `src/knowledge/providers/openai.js:136-148`, interaction with `src/knowledge/index.js:211-240` + +**Context.** `_fetch` throws a plain `Error` for HTTP 401/403 (auth failures). `withRetry`'s class-based bypass list includes `UserError`/`TypeError`/`ReferenceError`/`SyntaxError`/`RangeError` but does not distinguish HTTP error types. So an invalid/expired API key burns the full backoff (1s + 2s + 4s ≈ 7s) on every embed call before surfacing. + +429 (rate limit) **should** retry. 401/403 (auth) **shouldn't** — keys don't fix themselves between retries. + +**Fix shape (two plausible options):** + +1. **Promote 401/403 to UserError in `_fetch`.** They're user-config problems (bad key). The `withRetry` short-circuit + clean `main().catch` rendering then handle them correctly. Cleanest. + +2. **Add a marker class** like `AuthError extends Error` in `providers/openai.js`, add it to `withRetry`'s skip list. More targeted but adds a class for one case. + +Option 1 is consistent with how UserError is used elsewhere (validation failures, provider mismatch). The error message "OpenAI returned 401: invalid API key — check `~/.config/workflows/credentials.json` or `OPENAI_API_KEY` env var" is exactly the kind of actionable user-config message UserError exists for. + +**Severity.** UX — wastes 7s on every bad-key call. Real but not blocking. + +**Acceptance criteria:** +- Bad-key invocation surfaces the error in <100ms (no retry budget burned). +- 429s still retry as today. +- Test in `test-knowledge-openai.cjs` that mocks a 401 response, asserts `withRetry` calls the function exactly once. + +--- + +### #6 — Setup writes openai config before validating the key (✅ commit `27a1b9f7`) + +**Files:** `src/knowledge/setup.js:374-407` (`runSystemConfigStep`), `:543-552` (`runProjectInitStep`) + +**Context.** `runSystemConfigStep` writes `provider:'openai', model, dimensions` to disk **before** `ensureOpenAIKey` validates. `ensureOpenAIKey` returns successfully even when the env-var key fails validation (line 408 returns without aborting). `runProjectInitStep` then writes `metadata.json` with `provider:'openai'` (line 545: `provider || null` evaluates `cfg.provider`, which is `'openai'`). + +On the very next `knowledge index`, `resolveProviderState` (`index.js:343-388`) sees `metaProvider === 'openai'` but `provider === null` (resolveProvider returns null when api key is missing), and throws Case 3: + +> *"Provider/model changed since last index. Run `knowledge rebuild` to reindex."* + +The provider hasn't changed — the key is just bad. The user sees a misleading recovery hint. + +**Fix shape.** Re-order setup so the key is validated **before** any config write. If validation fails, abort cleanly with an actionable error ("API key invalid — fix and re-run setup"). Do not write `provider:'openai'` to disk on a bad key. + +Note: `validateApiKey` already exists and does a real test embed call. The bug is purely ordering. + +**Severity.** Real UX bug. User goes through setup, gets through wizard, runs `index`, sees confusing "rebuild" advice that doesn't match their actual problem. + +**Acceptance criteria:** +- Setup with a bad key aborts before writing system config. +- Setup with a good key works as before. +- Test in `test-knowledge-config.cjs` or a new setup-specific test exercises the validation-before-write ordering. + +--- + +### #7 — Setup partial-state recovery gap (✅ commit `3ecd1f1e`) + +**File:** `src/knowledge/setup.js:536-552` + +**Context.** If `metadata.json` exists but `store.msp` does not (rare — partial cleanup, interrupted prior init, manual deletion), `detected.fullyInitialised === false` and `detected.metadataExists === true`. The conditional at line 543 (`!detected.metadataExists || detected.fullyInitialised`) is **false**, so metadata is **not** rewritten, but the store **is** re-created at line 536. Result: a fresh empty store paired with metadata still pointing at the prior run's provider/model/dimensions. First index after that path errors with the same misleading "Provider/model changed" message as #6. + +**Fix shape.** Tighten the condition so metadata is rewritten whenever the store is being (re)created. Or detect partial-state and warn the user to run `rebuild` instead of trying to recover via setup. + +**Severity.** Edge case but real. Same misleading error surface as #6. + +**Acceptance criteria:** +- Partial-state (metadata-without-store) is detected and either recovered cleanly or surfaced with an actionable error. +- Test that simulates the state. + +--- + +## Defer (real but non-blocking; log as deferred-issue or fix later) + +### #8 — Whole-store enumeration capped at 100k (✅ commit `8d7432fb`) + +**Files:** `src/knowledge/index.js:1379, 1865`; `src/knowledge/store.js:128, 167, 187` + +**Context.** `cmdStatus` and `cmdCompact` rely on `searchFulltext(db, { term: '', limit: 100000 })` returning **every** chunk. Beyond 100k chunks the cap silently truncates: status shows wrong totals, compact misses expired chunks. Per-topic `findInternalIdsByIdentity`/`removeByFilter` share the same cap — a topic with >100k chunks would leak chunks across re-indexes. + +**Realistic at the project level for very long-lived bases.** Per-topic, only on pathological input. + +**Fix shape options:** +1. Iterate via paged search until exhausted. Cleanest but more code. +2. Raise the cap to 1M with a comment explaining the assumption. +3. Add a status warning when chunk count approaches the cap. + +**Severity.** Latent. Will bite some user, eventually. Defer to a deferred-issue entry; revisit when project sizes warrant. + +--- + +### #9 — Empty-term `searchFulltext` is undocumented Orama behaviour with no test (✅ commit `8d7432fb`) + +**Files:** `src/knowledge/store.js:122, 165, 187`; `src/knowledge/index.js:635, 1379, 1865` + +**Context.** Six call sites depend on Orama returning **all** matching docs for `term: ''`: +- `findInternalIdsByIdentity` +- `removeByFilter` +- `countByFilter` (added during Critical #2's `--dry-run` fix) +- `isIndexed` +- `cmdStatus` +- `cmdCompact` + +No store/integration test pins down this contract. An Orama version bump that changed empty-term semantics would silently break all six paths. + +**Fix shape.** Add a single store-level test: insert N docs, run `searchFulltext(db, { term: '', limit: 1000 })`, assert returns N. Pins the contract; if Orama changes behaviour, this test fails first instead of production breaking silently. + +**Severity.** Latent test-coverage gap. Cheap fix. + +--- + +### #10 — Test 81 Part B (pending-removal queue drain) has theatre risk on the negative path (✅ commit `a8924796`) + +**File:** `tests/scripts/test-knowledge-cli.sh:1638-1676` (Test 81) + +**Context.** Test 81 was rewritten during Critical #1 to be a real regression guard for the `pending_removals` whitelist fix. Part A (the "queue survives an index write" assertion) is genuine. Part B's "queue drained after remove" assertion only passes because `performRemoval` deliberately omits the registry-existence check that `cmdRemove` enforces (per Important #9 design). The drain path silently succeeds against a non-existent `stale-wu`, deletes the queue entry. Test never proves a **real** failure (lock timeout, store I/O error) increments `attempts` or eventually evicts after `REMOVAL_MAX_ATTEMPTS`. + +**Fix shape.** Add a test that simulates real removal failure (e.g. mock the store path to point at a read-only file, or stub `performRemoval` to throw N times), asserts attempts increment, asserts eviction at `REMOVAL_MAX_ATTEMPTS`. The current Test 81 Part B should be renamed to "queue drains on no-op success" so its purpose is clear. + +**Severity.** Coverage gap on the eviction branch. Code path works; we just don't test it. + +--- + +### #11 — README has zero mention of the knowledge base (✅ commit `ab4c60df`) + +**File:** `README.md` (238 lines) + +**Context.** The user-facing entry point never mentions: +- The knowledge base feature. +- The OpenAI API key requirement. +- The hard-stop on first run after upgrade (existing users hit `Knowledge Base Not Ready` on next workflow invocation per `knowledge-check.md:39-65`). +- `knowledge setup`. + +Per `knowledge-base/design.md:316-333`, the KB is **required infrastructure**. A user installing via `npx agntc add leeovery/agentic-workflows` will hit the hard stop with no warning in Getting Started. + +**Fix shape.** +- `README.md:48-58` (Requirements section) should mention Node ≥ 18, optional OpenAI API key. +- A "Knowledge Base" section explaining what it is, the first-run setup, the stub-mode option for keyword-only. + +**Severity.** Documentation gap. Real impact on first-time users post-merge. + +--- + +### #12 — Manual `knowledge remove --work-unit ` errors with no escape hint (✅ commit `ba515a75`) + +**File:** `src/knowledge/index.js:1740-1751` + +**Context.** Important #9 added registry validation to `cmdRemove`. After absorption (`absorb-into-epic.md`), the WU's registry entry is deleted **after** the remove call. If chunks linger and the user later tries `knowledge remove --work-unit ` manually, they hit `UserError: Work unit "..." not found in project manifest` with no actionable path. The error suggests `knowledge status`, not `knowledge compact` (which is the actual escape hatch via `processPendingRemovals`). + +**Fix shape.** Update the error message in `cmdRemove`'s `catch (err) { if (err.status === 2) throw new UserError(...) }` block to mention `knowledge compact` as an option for orphaned WUs. + +Or: when registry says "not found" but chunks exist for that WU in the store, route into a different message path that tells the user how to clean up. + +**Severity.** UX — narrow scenario, but user lands in a dead-end with no clear next step. + +--- + +## Minor / polish (skip-by-default unless paired with a related fix) + +### #13 — `contextual-query.md:3` header stale (✅ commit `9b0ba424`) + +Says "loaded at phase start in research, discussion, and investigation processing skills" — but is also loaded by scoping (per Minor #9 of the cleanup). Update header to include scoping. + +### #14 — Phase task lists all-unchecked (✅ commit `31c7b688`) + +`knowledge-base/phase-1-tasks.md` through `phase-8-tasks.md` (76 total checkboxes, 0 checked) despite branch being phase-8 with all features implemented. Either update post-implementation or document the convention as "checkboxes are author-time artefacts, not progress trackers". + +### #15 — Setup integer parser is lenient (✅ commit `c556adfe`) + +`src/knowledge/setup.js:366-370` — `parseInt('1536abc', 10)` returns `1536`; `Number.isInteger` then passes. Setup happily stores partly-valid input as the dimensions field. Add `/^\d+$/` regex check before `parseInt`. + +### #16 — `cmdCompact` rejects `decay_months: null` (✅ commit `842db966`) + +`src/knowledge/index.js:1842-1852`. Only `false` or non-negative integer accepted. A user hand-editing config and writing `null` intuitively (to disable) gets an error. Treat `null` as equivalent to `false`. + +### #17 — `cmdStatus` orphan check is cwd-sensitive (✅ commit `246b24e1`) + +`src/knowledge/index.js:1485`. `fs.existsSync(path.resolve(c.source_file))` against the relative path stored at index time. Status from a different cwd reports every chunk as orphaned. Resolve relative to the project root, not `process.cwd()`. + +### #18 — Test convention drift (✅ commit `750d468a`) + +Migration tests 001-028 use `set -eo pipefail`; 029-037 use `set -euo pipefail`. Pick one (likely the stricter `-u`) and apply uniformly. Or add a comment explaining the intent. + +### #19 — Bundle ~30% over design estimate (✅ commit `8e8e687e`) + +`knowledge-base/design.md:208` estimates ~110-120 KB minified. Current bundle is ~156 KB (well under the 200 KB ceiling). Update the design estimate to reflect current reality, or note that ESM-resolution + Orama version drift accounts for the increase. + +--- + +## How to use this file + +1. Pick an item (start with the pre-merge musts unless you have a reason). +2. Read the cited files end-to-end. Do not edit until you've understood the surrounding context. +3. Read CLAUDE.md sections relevant to the item type (markdown convention items: "Conditional Routing" + "Skill File Structure"; code items: no specific section, but project conventions in CLAUDE.md still apply). +4. Write the full target shape mentally, then apply as a single edit. Iterating edits while reasoning is the failure mode that left these items unfinished. +5. Run the test suite before and after. +6. One commit per item. Reference the item number from this file in the commit message. +7. Update this file: change `## #N — ...` to `## #N — ... (✅ commit ``)` so the next agent knows what's done. + +When all pre-merge musts (#1–#4) are landed, the branch is ready to merge into the stack. diff --git a/release b/release index 15c9bf89e..84d49a1b5 100755 --- a/release +++ b/release @@ -257,6 +257,33 @@ perform_release() { exit 1 fi + # Rebuild the knowledge CLI bundle so every tagged release ships a fresh + # skills/workflow-knowledge/scripts/knowledge.cjs. AGNTC installs from tags + # with no build step, so the bundle must be committed before the tag. + # Use `npm ci` (not `npm install`) so the lockfile can never drift mid-release. + # `npm install` would mutate package-lock.json on any resolvable version change, + # bypassing the dirty-tree gate and leaving uncommitted state after the tag. + echo "Installing build dependencies..." + if ! npm ci; then + echo "Error: npm ci failed. Aborting release." >&2 + exit 1 + fi + + echo "Building knowledge bundle..." + if ! npm run build; then + echo "Error: npm run build failed. Refusing to tag with a stale or missing bundle." >&2 + exit 1 + fi + + local bundle_path="skills/workflow-knowledge/scripts/knowledge.cjs" + if ! git diff --quiet -- "$bundle_path"; then + echo "Knowledge bundle changed — committing." + git add "$bundle_path" + git commit -m "chore(release): rebuild knowledge bundle for v${new_version}" + else + echo "Knowledge bundle unchanged — no commit needed." + fi + # Generate commit message local commit_message if $skip_ai; then diff --git a/skills/continue-bugfix/SKILL.md b/skills/continue-bugfix/SKILL.md index 237dd8f7e..9ed9b0a71 100644 --- a/skills/continue-bugfix/SKILL.md +++ b/skills/continue-bugfix/SKILL.md @@ -242,4 +242,6 @@ Skills receive positional arguments: `$0` = work_type (`bugfix`), `$1` = work_un If the user chose to revisit a completed phase in Step 5, use that phase instead of `next_phase`. -Invoke the skill. This is terminal — do not return to the backbone. +Invoke the skill. + +**STOP.** Do not proceed — terminal condition. diff --git a/skills/continue-cross-cutting/SKILL.md b/skills/continue-cross-cutting/SKILL.md index 1c0612578..2449ce575 100644 --- a/skills/continue-cross-cutting/SKILL.md +++ b/skills/continue-cross-cutting/SKILL.md @@ -240,4 +240,6 @@ Skills receive positional arguments: `$0` = work_type (`cross-cutting`), `$1` = If the user chose to revisit a completed phase in Step 5, use that phase instead of `next_phase`. -Invoke the skill. This is terminal — do not return to the backbone. +Invoke the skill. + +**STOP.** Do not proceed — terminal condition. diff --git a/skills/continue-feature/SKILL.md b/skills/continue-feature/SKILL.md index c16e559e9..edb052e20 100644 --- a/skills/continue-feature/SKILL.md +++ b/skills/continue-feature/SKILL.md @@ -243,4 +243,6 @@ Skills receive positional arguments: `$0` = work_type (`feature`), `$1` = work_u If the user chose to revisit a completed phase in Step 5, use that phase instead of `next_phase`. -Invoke the skill. This is terminal — do not return to the backbone. +Invoke the skill. + +**STOP.** Do not proceed — terminal condition. diff --git a/skills/continue-quickfix/SKILL.md b/skills/continue-quickfix/SKILL.md index 514cbe65b..9f719765a 100644 --- a/skills/continue-quickfix/SKILL.md +++ b/skills/continue-quickfix/SKILL.md @@ -240,4 +240,6 @@ Skills receive positional arguments: `$0` = work_type (`quick-fix`), `$1` = work If the user chose to revisit a completed phase in Step 5, use that phase instead of `next_phase`. -Invoke the skill. This is terminal — do not return to the backbone. +Invoke the skill. + +**STOP.** Do not proceed — terminal condition. diff --git a/skills/workflow-discussion-entry/references/display-options.md b/skills/workflow-discussion-entry/references/display-options.md index d684480f5..16599ee92 100644 --- a/skills/workflow-discussion-entry/references/display-options.md +++ b/skills/workflow-discussion-entry/references/display-options.md @@ -132,7 +132,9 @@ Set source="fresh". #### If user chose `back` -Re-invoke the caller's entry-point skill to return to its menu. Invoke `/continue-epic {work_unit}`. This is terminal — the invoked skill takes over. +Re-invoke the caller's entry-point skill to return to its menu. Invoke `/continue-epic {work_unit}`. + +**STOP.** Do not proceed — terminal condition. #### If user chose `refresh` diff --git a/skills/workflow-implementation-process/SKILL.md b/skills/workflow-implementation-process/SKILL.md index 16cc9b179..25a12b85a 100644 --- a/skills/workflow-implementation-process/SKILL.md +++ b/skills/workflow-implementation-process/SKILL.md @@ -206,8 +206,10 @@ Load **[linter-setup.md](references/linter-setup.md)** and follow its instructio > *Output the next fenced block as markdown (not a code block):* ``` -> Loading the usage guide for the knowledge base so -> proactive querying is available while tasks execute. +> Loading the usage guide for the knowledge base. Implementation reads +> the code as the source of truth for *what* exists — the guide +> documents the rare cases where the KB is useful for the *why* +> behind an existing pattern. ``` Load **[knowledge-usage.md](../workflow-knowledge/references/knowledge-usage.md)** and follow its instructions as written. diff --git a/skills/workflow-knowledge/SKILL.md b/skills/workflow-knowledge/SKILL.md index 27df96692..6df60f026 100644 --- a/skills/workflow-knowledge/SKILL.md +++ b/skills/workflow-knowledge/SKILL.md @@ -33,6 +33,8 @@ node .claude/skills/workflow-knowledge/scripts/knowledge.cjs [args] Every skill that calls this must declare `Bash(node .claude/skills/workflow-knowledge/scripts/knowledge.cjs)` in its `allowed-tools` frontmatter. +To list commands and options, use `--help` / `-h` / `help` — writes usage to stdout, exits 0. Invoking the CLI with no arguments writes usage to stderr and exits 1. + --- ## `query` — search the knowledge base @@ -55,17 +57,18 @@ Multiple positional arguments run separate searches in one invocation, merge the | Flag | Behaviour | |------|-----------| -| `--work-type ` | Filter results to a work type. Comma-separated list accepted (e.g., `--work-type cross-cutting` or `--work-type epic,feature`). Hard filter — non-matching chunks are excluded | +| `--work-unit ` | Filter to one or more work units. Comma-separated list accepted. Hard filter — non-matching chunks excluded | +| `--work-type ` | Filter results to a work type. Comma-separated list accepted (e.g., `--work-type cross-cutting` or `--work-type epic,feature`). Hard filter | | `--phase ` | Filter to one or more phases. Same comma-separated syntax. Hard filter | | `--topic ` | Filter to one or more topics. Same comma-separated syntax. Hard filter | -| `--work-unit ` | **Re-ranking hint, NOT a filter.** Boosts chunks from this work unit in post-processing. Cross-work-unit results still appear, just ranked lower. Use it to say "I'm currently working in `auth-flow`, prefer its context" — not to exclude other work | +| `--boost: ` | **Re-ranking hint, NOT a filter.** Boosts chunks where `` equals `` by `+0.1` per match, additive. Repeatable. Valid fields: `work-unit`, `work-type`, `phase`, `topic`, `confidence`. Use it to say "I'm currently working in `auth-flow`, prefer its context" via `--boost:work-unit auth-flow` — results from other work units still appear, just ranked lower | | `--limit ` | Cap result count after merge + re-rank. Default 10 | ### Search modes Two modes, auto-selected based on project config: -- **Hybrid** (default when an embedding provider is configured): keyword + vector search combined, results re-ranked by work-unit proximity, confidence tier, and recency. +- **Hybrid** (default when an embedding provider is configured): keyword + vector search combined, results re-ranked by any `--boost:` directives you pass, plus always-on confidence-tier and recency signals. - **Keyword-only** (when no provider is configured): full-text search only. Still useful — you lose semantic expansion but exact-term queries work. The output prepends a note: `[keyword-only mode — configure embedding provider for semantic search]`. This is a supported degraded mode, not a broken state. ### Query construction @@ -130,8 +133,8 @@ Don't read source files for every result. Most queries produce a couple of chunk - **Do not dump large result sets speculatively.** `--limit 50` with a vague query produces noise. Prefer a focused query with the default limit. - **Do not use topic slugs as search terms.** `"auth-flow"` is a weak semantic signal. Describe the thing, don't name it. - **Do not query during the specification phase.** Spec turns discussion decisions into a golden document. Cross-cutting concerns merge at planning time via an explicit cross-cutting query, not during spec authoring. Querying mid-spec pulls the spec away from its own source material. -- **Do not prepend metadata to the query string.** The CLI already filters by `work_type`, `phase`, `topic`, `work_unit` via flags. `"auth-flow specification UUID identity"` is worse than `"UUID identity"` with `--phase specification`. -- **Do not treat `--work-unit` as a filter.** It re-ranks. If you want to exclude other work units, you probably don't — cross-work-unit context is the point of the knowledge base. +- **Do not prepend metadata to the query string.** The CLI already filters by `work-unit`, `work-type`, `phase`, `topic` via flags. `"auth-flow specification UUID identity"` is worse than `"UUID identity"` with `--phase specification`. +- **Reach for `--boost:` before `--work-unit`.** Filtering by work unit excludes cross-work-unit context — usually the opposite of what you want. `--boost:work-unit ` nudges results toward your current work unit while keeping prior work from other units in the pool. Stack multiple boosts (`--boost:work-unit X --boost:phase specification`) when your query wants multi-dimensional preference, not exclusion. --- diff --git a/skills/workflow-knowledge/references/contextual-query.md b/skills/workflow-knowledge/references/contextual-query.md index 3389a9e71..9f7c76463 100644 --- a/skills/workflow-knowledge/references/contextual-query.md +++ b/skills/workflow-knowledge/references/contextual-query.md @@ -1,12 +1,12 @@ # Contextual Query -*Reference for **[workflow-knowledge](../SKILL.md)** — loaded at phase start in research, discussion, and investigation processing skills.* +*Reference for **[workflow-knowledge](../SKILL.md)** — loaded at phase start in research, discussion, investigation, and scoping processing skills.* --- -At the beginning of these phases, a single targeted query against the knowledge base catches prior work that might otherwise surface as a correction ten minutes into the session. One query, one interpretation step — if nothing comes back, proceed as normal. +At the beginning of these phases, a focused query against the knowledge base catches prior work that might otherwise surface as a correction ten minutes into the session. One invocation, one interpretation step — if nothing comes back, proceed as normal. -This is **not** a speculative dump. It is a focused check using the best context currently available. +This is **not** a speculative dump. It is a focused check using the best context currently available. When the starting context offers multiple distinct angles (e.g. investigation has both symptoms and a subsystem name), batch them in a single invocation rather than running them serially. ## A. Construct the query @@ -18,10 +18,18 @@ If the only context available is a topic name, construct the best descriptive qu ## B. Run the query -Invoke the CLI with the constructed query (or queries). Use `--work-unit {work_unit}` to bias results toward the current work unit without filtering out cross-work-unit context. Do not use hard filters unless you have a specific reason — this is meant to surface prior work broadly. +Invoke the CLI with the constructed query (or queries). Use `--boost:work-unit {work_unit}` to bias results toward the current work unit without filtering out cross-work-unit context. Do not use hard filters (`--work-unit`, `--phase`, `--topic`, `--work-type`) unless you have a specific reason — this is meant to surface prior work broadly. + +Single framing: + +``` +node .claude/skills/workflow-knowledge/scripts/knowledge.cjs query "" --boost:work-unit {work_unit} +``` + +Multiple framings (batch — one invocation, one merged result set): ``` -node .claude/skills/workflow-knowledge/scripts/knowledge.cjs query "" --work-unit {work_unit} +node .claude/skills/workflow-knowledge/scripts/knowledge.cjs query "" "" "" --boost:work-unit {work_unit} ``` #### If the command exits with a non-zero code diff --git a/skills/workflow-knowledge/references/knowledge-usage.md b/skills/workflow-knowledge/references/knowledge-usage.md index ede87d3c1..04723d74c 100644 --- a/skills/workflow-knowledge/references/knowledge-usage.md +++ b/skills/workflow-knowledge/references/knowledge-usage.md @@ -25,7 +25,7 @@ Multiple queries from different angles are expected and encouraged. One query fo ## B. How to construct queries -Use **natural language** describing what you're looking for — not topic slugs, which are weak semantic signal. Filter with `--work-type`, `--phase`, `--topic`; bias toward the current work unit with `--work-unit` (a re-rank hint, not a filter). For multiple angles in one invocation, pass multiple positional terms (batch query). +Use **natural language** describing what you're looking for — not topic slugs, which are weak semantic signal. Filter with `--work-unit`, `--work-type`, `--phase`, `--topic` (hard filters — non-matching chunks excluded). Bias results with `--boost: ` (re-rank hint; repeatable; valid fields: `work-unit`, `work-type`, `phase`, `topic`, `confidence`). For multiple angles in one invocation, pass multiple positional terms (batch query). See **[SKILL.md](../SKILL.md)** — query construction examples and the full flag table. diff --git a/skills/workflow-knowledge/scripts/knowledge.cjs b/skills/workflow-knowledge/scripts/knowledge.cjs index 3820f6b31..ee9456e54 100644 --- a/skills/workflow-knowledge/scripts/knowledge.cjs +++ b/skills/workflow-knowledge/scripts/knowledge.cjs @@ -1,48 +1,48 @@ -"use strict";var b=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var lt=b(K=>{"use strict";Object.defineProperty(K,"__esModule",{value:!0});K.SUPPORTED_LANGUAGES=K.SPLITTERS=K.STEMMERS=void 0;K.getLocale=hc;K.STEMMERS={arabic:"ar",armenian:"am",bulgarian:"bg",czech:"cz",danish:"dk",dutch:"nl",english:"en",finnish:"fi",french:"fr",german:"de",greek:"gr",hungarian:"hu",indian:"in",indonesian:"id",irish:"ie",italian:"it",lithuanian:"lt",nepali:"np",norwegian:"no",portuguese:"pt",romanian:"ro",russian:"ru",serbian:"rs",slovenian:"ru",spanish:"es",swedish:"se",tamil:"ta",turkish:"tr",ukrainian:"uk",sanskrit:"sk"};K.SPLITTERS={dutch:/[^A-Za-zàèéìòóù0-9_'-]+/gim,english:/[^A-Za-zàèéìòóù0-9_'-]+/gim,french:/[^a-z0-9äâàéèëêïîöôùüûœç-]+/gim,italian:/[^A-Za-zàèéìòóù0-9_'-]+/gim,norwegian:/[^a-z0-9_æøåÆØÅäÄöÖüÜ]+/gim,portuguese:/[^a-z0-9à-úÀ-Ú]/gim,russian:/[^a-z0-9а-яА-ЯёЁ]+/gim,spanish:/[^a-z0-9A-Zá-úÁ-ÚñÑüÜ]+/gim,swedish:/[^a-z0-9_åÅäÄöÖüÜ-]+/gim,german:/[^a-z0-9A-ZäöüÄÖÜß]+/gim,finnish:/[^a-z0-9äöÄÖ]+/gim,danish:/[^a-z0-9æøåÆØÅ]+/gim,hungarian:/[^a-z0-9áéíóöőúüűÁÉÍÓÖŐÚÜŰ]+/gim,romanian:/[^a-z0-9ăâîșțĂÂÎȘȚ]+/gim,serbian:/[^a-z0-9čćžšđČĆŽŠĐ]+/gim,turkish:/[^a-z0-9çÇğĞıİöÖşŞüÜ]+/gim,lithuanian:/[^a-z0-9ąčęėįšųūžĄČĘĖĮŠŲŪŽ]+/gim,arabic:/[^a-z0-9أ-ي]+/gim,nepali:/[^a-z0-9अ-ह]+/gim,irish:/[^a-z0-9áéíóúÁÉÍÓÚ]+/gim,indian:/[^a-z0-9अ-ह]+/gim,armenian:/[^a-z0-9ա-ֆ]+/gim,greek:/[^a-z0-9α-ωά-ώ]+/gim,indonesian:/[^a-z0-9]+/gim,ukrainian:/[^a-z0-9а-яА-ЯіїєІЇЄ]+/gim,slovenian:/[^a-z0-9螚ȎŠ]+/gim,bulgarian:/[^a-z0-9а-яА-Я]+/gim,tamil:/[^a-z0-9அ-ஹ]+/gim,sanskrit:/[^a-z0-9A-Zāīūṛḷṃṁḥśṣṭḍṇṅñḻḹṝ]+/gim,czech:/[^A-Z0-9a-zěščřžýáíéúůóťďĚŠČŘŽÝÁÍÉÓÚŮŤĎ-]+/gim};K.SUPPORTED_LANGUAGES=Object.keys(K.STEMMERS);function hc(t){return t!==void 0&&K.SUPPORTED_LANGUAGES.includes(t)?K.STEMMERS[t]:void 0}});var R=b(O=>{"use strict";Object.defineProperty(O,"__esModule",{value:!0});O.MAX_ARGUMENT_FOR_STACK=O.isServer=void 0;O.safeArrayPush=mc;O.sprintf=wc;O.formatBytes=_c;O.isInsideWebWorker=Kr;O.isInsideNode=Hr;O.getNanosecondTimeViaPerformance=gn;O.formatNanoseconds=Sc;O.getNanosecondsTime=bc;O.uniqueId=Ic;O.getOwnProperty=xc;O.getTokenFrequency=Ec;O.insertSortedValue=Ac;O.sortTokenScorePredicate=Gr;O.intersect=vc;O.getDocumentProperties=Yr;O.getNested=Tc;O.flattenObject=Jr;O.convertDistanceToMeters=Mc;O.removeVectorsFromHits=Oc;O.isPromise=Pc;O.isAsyncFunction=Xr;O.setIntersection=kc;O.setUnion=Uc;O.setDifference=Rc;O.sleep=Lc;var pc=j(),gc=Date.now().toString().slice(5),yc=0,$r=1024,qr=BigInt(1e3),zr=BigInt(1e6),Vr=BigInt(1e9);O.isServer=typeof window>"u";O.MAX_ARGUMENT_FOR_STACK=65535;function mc(t,e){if(e.length\d+)\$)?(?-?\d*\.?\d*)(?[dfs])/g,function(...n){let r=n[n.length-1],{width:s,type:i,position:o}=r,c=o?e[Number.parseInt(o)-1]:e.shift(),u=s===""?0:Number.parseInt(s);switch(i){case"d":return c.toString().padStart(u,"0");case"f":{let a=c,[l,d]=s.split(".").map(f=>Number.parseFloat(f));return typeof d=="number"&&d>=0&&(a=a.toFixed(d)),typeof l=="number"&&l>=0?a.toString().padStart(u,"0"):a.toString()}case"s":return u<0?c.toString().padEnd(-u," "):c.toString().padStart(u," ");default:return c}})}function _c(t,e=2){if(t===0)return"0 Bytes";let n=e<0?0:e,r=["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"],s=Math.floor(Math.log(t)/Math.log($r));return`${parseFloat((t/Math.pow($r,s)).toFixed(n))} ${r[s]}`}function Kr(){return typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope}function Hr(){return typeof process<"u"&&process.release&&process.release.name==="node"}function gn(){return BigInt(Math.floor(performance.now()*1e6))}function Sc(t){return typeof t=="number"&&(t=BigInt(t)),t>>1,n(e,t[i])<0?s=i:r=i+1;return t.splice(r,0,e),t}function Gr(t,e){return e[1]===t[1]?t[0]-e[0]:e[1]-t[1]}function vc(t){if(t.length===0)return[];if(t.length===1)return t[0];for(let n=1;n{let r=e.get(n);return r!==void 0&&e.set(n,0),r===t.length})}function Yr(t,e){let n={},r=e.length;for(let s=0;s({...n,document:{...n.document,...e.reduce((r,s)=>{let i=s.split("."),o=i.pop(),c=r;for(let u of i)c[u]=c[u]??{},c=c[u];return c[o]=null,r},n.document)}}))}function Pc(t){return!!t&&(typeof t=="object"||typeof t=="function")&&typeof t.then=="function"}function Xr(t){return Array.isArray(t)?t.some(e=>Xr(e)):t?.constructor?.name==="AsyncFunction"}var Wr="intersection"in new Set;function kc(...t){if(t.length===0)return new Set;if(t.length===1)return t[0];if(t.length===2){let r=t[0],s=t[1];if(Wr)return r.intersection(s);let i=new Set,o=r.size0&&t<1/0)===!1)throw typeof t!="number"&&typeof t!="bigint"?TypeError("sleep: ms must be a number"):RangeError("sleep: ms must be a number that is greater than 0 but less than Infinity");Atomics.wait(e,0,0,Number(t))}else{if((t>0&&t<1/0)===!1)throw typeof t!="number"&&typeof t!="bigint"?TypeError("sleep: ms must be a number"):RangeError("sleep: ms must be a number that is greater than 0 but less than Infinity");let n=Date.now()+Number(t);for(;n>Date.now(););}}});var j=b(yn=>{"use strict";Object.defineProperty(yn,"__esModule",{value:!0});yn.createError=$c;var jc=lt(),Cc=R(),Fc=jc.SUPPORTED_LANGUAGES.join(` - - `),Bc={NO_LANGUAGE_WITH_CUSTOM_TOKENIZER:"Do not pass the language option to create when using a custom tokenizer.",LANGUAGE_NOT_SUPPORTED:`Language "%s" is not supported. +"use strict";var Zt=Object.defineProperty;var Ro=Object.getOwnPropertyDescriptor;var Lo=Object.getOwnPropertyNames;var Co=Object.prototype.hasOwnProperty;var k=(t,e)=>()=>(t&&(e=t(t=0)),e);var xe=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports),ne=(t,e)=>{for(var n in e)Zt(t,n,{get:e[n],enumerable:!0})},$o=(t,e,n,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Lo(e))!Co.call(t,s)&&s!==n&&Zt(t,s,{get:()=>e[s],enumerable:!(r=Ro(e,s))||r.enumerable});return t};var br=t=>$o(Zt({},"__esModule",{value:!0}),t);function kr(t){return t!==void 0&&Oe.includes(t)?Ar[t]:void 0}var Ar,Er,Oe,ot=k(()=>{Ar={arabic:"ar",armenian:"am",bulgarian:"bg",czech:"cz",danish:"dk",dutch:"nl",english:"en",finnish:"fi",french:"fr",german:"de",greek:"gr",hungarian:"hu",indian:"in",indonesian:"id",irish:"ie",italian:"it",lithuanian:"lt",nepali:"np",norwegian:"no",portuguese:"pt",romanian:"ro",russian:"ru",serbian:"rs",slovenian:"ru",spanish:"es",swedish:"se",tamil:"ta",turkish:"tr",ukrainian:"uk",sanskrit:"sk"},Er={dutch:/[^A-Za-zàèéìòóù0-9_'-]+/gim,english:/[^A-Za-zàèéìòóù0-9_'-]+/gim,french:/[^a-z0-9äâàéèëêïîöôùüûœç-]+/gim,italian:/[^A-Za-zàèéìòóù0-9_'-]+/gim,norwegian:/[^a-z0-9_æøåÆØÅäÄöÖüÜ]+/gim,portuguese:/[^a-z0-9à-úÀ-Ú]/gim,russian:/[^a-z0-9а-яА-ЯёЁ]+/gim,spanish:/[^a-z0-9A-Zá-úÁ-ÚñÑüÜ]+/gim,swedish:/[^a-z0-9_åÅäÄöÖüÜ-]+/gim,german:/[^a-z0-9A-ZäöüÄÖÜß]+/gim,finnish:/[^a-z0-9äöÄÖ]+/gim,danish:/[^a-z0-9æøåÆØÅ]+/gim,hungarian:/[^a-z0-9áéíóöőúüűÁÉÍÓÖŐÚÜŰ]+/gim,romanian:/[^a-z0-9ăâîșțĂÂÎȘȚ]+/gim,serbian:/[^a-z0-9čćžšđČĆŽŠĐ]+/gim,turkish:/[^a-z0-9çÇğĞıİöÖşŞüÜ]+/gim,lithuanian:/[^a-z0-9ąčęėįšųūžĄČĘĖĮŠŲŪŽ]+/gim,arabic:/[^a-z0-9أ-ي]+/gim,nepali:/[^a-z0-9अ-ह]+/gim,irish:/[^a-z0-9áéíóúÁÉÍÓÚ]+/gim,indian:/[^a-z0-9अ-ह]+/gim,armenian:/[^a-z0-9ա-ֆ]+/gim,greek:/[^a-z0-9α-ωά-ώ]+/gim,indonesian:/[^a-z0-9]+/gim,ukrainian:/[^a-z0-9а-яА-ЯіїєІЇЄ]+/gim,slovenian:/[^a-z0-9螚ȎŠ]+/gim,bulgarian:/[^a-z0-9а-яА-Я]+/gim,tamil:/[^a-z0-9அ-ஹ]+/gim,sanskrit:/[^a-z0-9A-Zāīūṛḷṃṁḥśṣṭḍṇṅñḻḹṝ]+/gim,czech:/[^A-Z0-9a-zěščřžýáíéúůóťďĚŠČŘŽÝÁÍÉÓÚŮŤĎ-]+/gim},Oe=Object.keys(Ar)});function Se(t,e){if(e.length\d+)\$)?(?-?\d*\.?\d*)(?[dfs])/g,function(...n){let r=n[n.length-1],{width:s,type:i,position:o}=r,c=o?e[Number.parseInt(o)-1]:e.shift(),a=s===""?0:Number.parseInt(s);switch(i){case"d":return c.toString().padStart(a,"0");case"f":{let l=c,[u,d]=s.split(".").map(f=>Number.parseFloat(f));return typeof d=="number"&&d>=0&&(l=l.toFixed(d)),typeof u=="number"&&u>=0?l.toString().padStart(a,"0"):l.toString()}case"s":return a<0?c.toString().padEnd(-a," "):c.toString().padStart(a," ");default:return c}})}function Or(t,e=2){if(t===0)return"0 Bytes";let n=e<0?0:e,r=["Bytes","KB","MB","GB","TB","PB","EB","ZB","YB"],s=Math.floor(Math.log(t)/Math.log(vr));return`${parseFloat((t/Math.pow(vr,s)).toFixed(n))} ${r[s]}`}function zo(){return typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope}function Vo(){return typeof process<"u"&&process.release&&process.release.name==="node"}function Mr(){return BigInt(Math.floor(performance.now()*1e6))}function oe(t){return typeof t=="number"&&(t=BigInt(t)),t{let r=e.get(n);return r!==void 0&&e.set(n,0),r===t.length})}function Pe(t,e){let n={},r=e.length;for(let s=0;s({...n,document:{...n.document,...e.reduce((r,s)=>{let i=s.split("."),o=i.pop(),c=r;for(let a of i)c[a]=c[a]??{},c=c[a];return c[o]=null,r},n.document)}}))}function b(t){return Array.isArray(t)?t.some(e=>b(e)):t?.constructor?.name==="AsyncFunction"}function Le(...t){if(t.length===0)return new Set;if(t.length===1)return t[0];if(t.length===2){let r=t[0],s=t[1];if(Nr)return r.intersection(s);let i=new Set,o=r.size0&&t<1/0)===!1)throw typeof t!="number"&&typeof t!="bigint"?TypeError("sleep: ms must be a number"):RangeError("sleep: ms must be a number that is greater than 0 but less than Infinity");Atomics.wait(e,0,0,Number(t))}else{if((t>0&&t<1/0)===!1)throw typeof t!="number"&&typeof t!="bigint"?TypeError("sleep: ms must be a number"):RangeError("sleep: ms must be a number that is greater than 0 but less than Infinity");let n=Date.now()+Number(t);for(;n>Date.now(););}}var Bo,Fo,vr,Tr,_r,Dr,Qt,Wo,Nr,jo,O=k(()=>{R();Bo=Date.now().toString().slice(5),Fo=0,vr=1024,Tr=BigInt(1e3),_r=BigInt(1e6),Dr=BigInt(1e9),Qt=65535;Wo={cm:.01,m:1,km:1e3,ft:.3048,yd:.9144,mi:1609.344};Nr="intersection"in new Set;jo="union"in new Set});function A(t,...e){let n=new Error(Ur(Ko[t]??`Unsupported Orama Error code: ${t}`,...e));return n.code=t,"captureStackTrace"in Error.prototype&&Error.captureStackTrace(n),n}var qo,Ko,R=k(()=>{ot();O();qo=Oe.join(` + - `),Ko={NO_LANGUAGE_WITH_CUSTOM_TOKENIZER:"Do not pass the language option to create when using a custom tokenizer.",LANGUAGE_NOT_SUPPORTED:`Language "%s" is not supported. Supported languages are: - - ${Fc}`,INVALID_STEMMER_FUNCTION_TYPE:"config.stemmer property must be a function.",MISSING_STEMMER:'As of version 1.0.0 @orama/orama does not ship non English stemmers by default. To solve this, please explicitly import and specify the "%s" stemmer from the package @orama/stemmers. See https://docs.orama.com/docs/orama-js/text-analysis/stemming for more information.',CUSTOM_STOP_WORDS_MUST_BE_FUNCTION_OR_ARRAY:"Custom stop words array must only contain strings.",UNSUPPORTED_COMPONENT:'Unsupported component "%s".',COMPONENT_MUST_BE_FUNCTION:'The component "%s" must be a function.',COMPONENT_MUST_BE_FUNCTION_OR_ARRAY_FUNCTIONS:'The component "%s" must be a function or an array of functions.',INVALID_SCHEMA_TYPE:'Unsupported schema type "%s" at "%s". Expected "string", "boolean" or "number" or array of them.',DOCUMENT_ID_MUST_BE_STRING:'Document id must be of type "string". Got "%s" instead.',DOCUMENT_ALREADY_EXISTS:'A document with id "%s" already exists.',DOCUMENT_DOES_NOT_EXIST:'A document with id "%s" does not exists.',MISSING_DOCUMENT_PROPERTY:'Missing searchable property "%s".',INVALID_DOCUMENT_PROPERTY:'Invalid document property "%s": expected "%s", got "%s"',UNKNOWN_INDEX:'Invalid property name "%s". Expected a wildcard string ("*") or array containing one of the following properties: %s',INVALID_BOOST_VALUE:"Boost value must be a number greater than, or less than 0.",INVALID_FILTER_OPERATION:"You can only use one operation per filter, you requested %d.",SCHEMA_VALIDATION_FAILURE:'Cannot insert document due schema validation failure on "%s" property.',INVALID_SORT_SCHEMA_TYPE:'Unsupported sort schema type "%s" at "%s". Expected "string" or "number".',CANNOT_SORT_BY_ARRAY:'Cannot configure sort for "%s" because it is an array (%s).',UNABLE_TO_SORT_ON_UNKNOWN_FIELD:'Unable to sort on unknown field "%s". Allowed fields: %s',SORT_DISABLED:"Sort is disabled. Please read the documentation at https://docs.orama.com/docs/orama-js for more information.",UNKNOWN_GROUP_BY_PROPERTY:'Unknown groupBy property "%s".',INVALID_GROUP_BY_PROPERTY:'Invalid groupBy property "%s". Allowed types: "%s", but given "%s".',UNKNOWN_FILTER_PROPERTY:'Unknown filter property "%s".',UNKNOWN_VECTOR_PROPERTY:'Unknown vector property "%s". Make sure the property exists in the schema and is configured as a vector.',INVALID_VECTOR_SIZE:'Vector size must be a number greater than 0. Got "%s" instead.',INVALID_VECTOR_VALUE:'Vector value must be a number greater than 0. Got "%s" instead.',INVALID_INPUT_VECTOR:`Property "%s" was declared as a %s-dimensional vector, but got a %s-dimensional vector instead. + - ${qo}`,INVALID_STEMMER_FUNCTION_TYPE:"config.stemmer property must be a function.",MISSING_STEMMER:'As of version 1.0.0 @orama/orama does not ship non English stemmers by default. To solve this, please explicitly import and specify the "%s" stemmer from the package @orama/stemmers. See https://docs.orama.com/docs/orama-js/text-analysis/stemming for more information.',CUSTOM_STOP_WORDS_MUST_BE_FUNCTION_OR_ARRAY:"Custom stop words array must only contain strings.",UNSUPPORTED_COMPONENT:'Unsupported component "%s".',COMPONENT_MUST_BE_FUNCTION:'The component "%s" must be a function.',COMPONENT_MUST_BE_FUNCTION_OR_ARRAY_FUNCTIONS:'The component "%s" must be a function or an array of functions.',INVALID_SCHEMA_TYPE:'Unsupported schema type "%s" at "%s". Expected "string", "boolean" or "number" or array of them.',DOCUMENT_ID_MUST_BE_STRING:'Document id must be of type "string". Got "%s" instead.',DOCUMENT_ALREADY_EXISTS:'A document with id "%s" already exists.',DOCUMENT_DOES_NOT_EXIST:'A document with id "%s" does not exists.',MISSING_DOCUMENT_PROPERTY:'Missing searchable property "%s".',INVALID_DOCUMENT_PROPERTY:'Invalid document property "%s": expected "%s", got "%s"',UNKNOWN_INDEX:'Invalid property name "%s". Expected a wildcard string ("*") or array containing one of the following properties: %s',INVALID_BOOST_VALUE:"Boost value must be a number greater than, or less than 0.",INVALID_FILTER_OPERATION:"You can only use one operation per filter, you requested %d.",SCHEMA_VALIDATION_FAILURE:'Cannot insert document due schema validation failure on "%s" property.',INVALID_SORT_SCHEMA_TYPE:'Unsupported sort schema type "%s" at "%s". Expected "string" or "number".',CANNOT_SORT_BY_ARRAY:'Cannot configure sort for "%s" because it is an array (%s).',UNABLE_TO_SORT_ON_UNKNOWN_FIELD:'Unable to sort on unknown field "%s". Allowed fields: %s',SORT_DISABLED:"Sort is disabled. Please read the documentation at https://docs.orama.com/docs/orama-js for more information.",UNKNOWN_GROUP_BY_PROPERTY:'Unknown groupBy property "%s".',INVALID_GROUP_BY_PROPERTY:'Invalid groupBy property "%s". Allowed types: "%s", but given "%s".',UNKNOWN_FILTER_PROPERTY:'Unknown filter property "%s".',UNKNOWN_VECTOR_PROPERTY:'Unknown vector property "%s". Make sure the property exists in the schema and is configured as a vector.',INVALID_VECTOR_SIZE:'Vector size must be a number greater than 0. Got "%s" instead.',INVALID_VECTOR_VALUE:'Vector value must be a number greater than 0. Got "%s" instead.',INVALID_INPUT_VECTOR:`Property "%s" was declared as a %s-dimensional vector, but got a %s-dimensional vector instead. Input vectors must be of the size declared in the schema, as calculating similarity between vectors of different sizes can lead to unexpected results.`,WRONG_SEARCH_PROPERTY_TYPE:'Property "%s" is not searchable. Only "string" properties are searchable.',FACET_NOT_SUPPORTED:`Facet doens't support the type "%s".`,INVALID_DISTANCE_SUFFIX:'Invalid distance suffix "%s". Valid suffixes are: cm, m, km, mi, yd, ft.',INVALID_SEARCH_MODE:'Invalid search mode "%s". Valid modes are: "fulltext", "vector", "hybrid".',MISSING_VECTOR_AND_SECURE_PROXY:"No vector was provided and no secure proxy was configured. Please provide a vector or configure an Orama Secure Proxy to perform hybrid search.",MISSING_TERM:'"term" is a required parameter when performing hybrid search. Please provide a search term.',INVALID_VECTOR_INPUT:'Invalid "vector" property. Expected an object with "value" and "property" properties, but got "%s" instead.',PLUGIN_CRASHED:"A plugin crashed during initialization. Please check the error message for more information:",PLUGIN_SECURE_PROXY_NOT_FOUND:`Could not find '@orama/secure-proxy-plugin' installed in your Orama instance. Please install it before proceeding with creating an answer session. Read more at https://docs.orama.com/docs/orama-js/plugins/plugin-secure-proxy#plugin-secure-proxy `,PLUGIN_SECURE_PROXY_MISSING_CHAT_MODEL:`Could not find a chat model defined in the secure proxy plugin configuration. Please provide a chat model before proceeding with creating an answer session. Read more at https://docs.orama.com/docs/orama-js/plugins/plugin-secure-proxy#plugin-secure-proxy -`,ANSWER_SESSION_LAST_MESSAGE_IS_NOT_ASSISTANT:"The last message in the session is not an assistant message. Cannot regenerate non-assistant messages.",PLUGIN_COMPONENT_CONFLICT:'The component "%s" is already defined. The plugin "%s" is trying to redefine it.'};function $c(t,...e){let n=new Error((0,Cc.sprintf)(Bc[t]??`Unsupported Orama Error code: ${t}`,...e));return n.code=t,"captureStackTrace"in Error.prototype&&Error.captureStackTrace(n),n}});var $e=b(H=>{"use strict";Object.defineProperty(H,"__esModule",{value:!0});H.getDocumentProperties=void 0;H.formatElapsedTime=zc;H.getDocumentIndexId=Vc;H.validateSchema=Qr;H.isGeoPointType=Hc;H.isVectorType=es;H.isArrayType=ts;H.getInnerType=ns;H.getVectorSize=rs;var dt=j(),Zr=R(),qc=R();Object.defineProperty(H,"getDocumentProperties",{enumerable:!0,get:function(){return qc.getDocumentProperties}});function zc(t){return{raw:Number(t),formatted:(0,Zr.formatNanoseconds)(t)}}function Vc(t){if(t.id){if(typeof t.id!="string")throw(0,dt.createError)("DOCUMENT_ID_MUST_BE_STRING",typeof t.id);return t.id}return(0,Zr.uniqueId)()}function Qr(t,e){for(let[n,r]of Object.entries(e)){let s=t[n];if(!(typeof s>"u")&&!(r==="geopoint"&&typeof s=="object"&&typeof s.lon=="number"&&typeof s.lat=="number")&&!(r==="enum"&&(typeof s=="string"||typeof s=="number"))){if(r==="enum[]"&&Array.isArray(s)){let i=s.length;for(let o=0;o{"use strict";Object.defineProperty(Ie,"__esModule",{value:!0});Ie.createInternalDocumentIDStore=Gc;Ie.save=ss;Ie.load=is;Ie.getInternalDocumentId=os;Ie.getDocumentIdFromInternalId=Yc;function Gc(){return{idToInternalId:new Map,internalIdToId:[],save:ss,load:is}}function ss(t){return{internalIdToId:t.internalIdToId}}function is(t,e){let{internalIdToId:n}=e;t.internalDocumentIDStore.idToInternalId.clear(),t.internalDocumentIDStore.internalIdToId=[];let r=n.length;for(let s=0;st.internalIdToId.length?os(t,e.toString()):e}function Yc(t,e){if(t.internalIdToId.length{"use strict";Object.defineProperty(G,"__esModule",{value:!0});G.create=cs;G.get=us;G.getMultiple=as;G.getAll=ls;G.store=ds;G.remove=fs;G.count=hs;G.load=ps;G.save=gs;G.createDocumentsStore=Jc;var mn=V();function cs(t,e){return{sharedInternalDocumentStore:e,docs:{},count:0}}function us(t,e){let n=(0,mn.getInternalDocumentId)(t.sharedInternalDocumentStore,e);return t.docs[n]}function as(t,e){let n=e.length,r=Array.from({length:n});for(let s=0;s"u"?!1:(delete t.docs[n],t.count--,!0)}function hs(t){return t.count}function ps(t,e){let n=e;return{docs:n.docs,count:n.count,sharedInternalDocumentStore:t}}function gs(t){return{docs:t.docs,count:t.count}}function Jc(){return{create:cs,get:us,getMultiple:as,getAll:ls,store:ds,remove:fs,count:hs,load:ps,save:gs}}});var ys=b(qe=>{"use strict";Object.defineProperty(qe,"__esModule",{value:!0});qe.AVAILABLE_PLUGIN_HOOKS=void 0;qe.getAllPluginsByHook=Zc;var Xc=j();qe.AVAILABLE_PLUGIN_HOOKS=["beforeInsert","afterInsert","beforeRemove","afterRemove","beforeUpdate","afterUpdate","beforeUpsert","afterUpsert","beforeSearch","afterSearch","beforeInsertMultiple","afterInsertMultiple","beforeRemoveMultiple","afterRemoveMultiple","beforeUpdateMultiple","afterUpdateMultiple","beforeUpsertMultiple","afterUpsertMultiple","beforeLoad","afterLoad","afterCreate"];function Zc(t,e){let n=[],r=t.plugins?.length;if(!r)return n;for(let s=0;s{"use strict";Object.defineProperty(W,"__esModule",{value:!0});W.SINGLE_OR_ARRAY_COMPONENTS=W.FUNCTION_COMPONENTS=W.OBJECT_COMPONENTS=void 0;W.runSingleHook=Qc;W.runMultipleHook=eu;W.runAfterSearch=tu;W.runBeforeSearch=nu;W.runAfterCreate=ru;var ze=R();W.OBJECT_COMPONENTS=["tokenizer","index","documentsStore","sorter","pinning"];W.FUNCTION_COMPONENTS=["validateSchema","getDocumentIndexId","getDocumentProperties","formatElapsedTime"];W.SINGLE_OR_ARRAY_COMPONENTS=[];function Qc(t,e,n,r){if(t.some(ze.isAsyncFunction))return(async()=>{for(let i of t)await i(e,n,r)})();for(let i of t)i(e,n,r)}function eu(t,e,n){if(t.some(ze.isAsyncFunction))return(async()=>{for(let s of t)await s(e,n)})();for(let s of t)s(e,n)}function tu(t,e,n,r,s){if(t.some(ze.isAsyncFunction))return(async()=>{for(let o of t)await o(e,n,r,s)})();for(let o of t)o(e,n,r,s)}function nu(t,e,n,r){if(t.some(ze.isAsyncFunction))return(async()=>{for(let i of t)await i(e,n,r)})();for(let i of t)i(e,n,r)}function ru(t,e){if(t.some(ze.isAsyncFunction))return(async()=>{for(let r of t)await r(e)})();for(let r of t)r(e)}});var ms=b(Oe=>{"use strict";Object.defineProperty(Oe,"__esModule",{value:!0});Oe.AVLTree=Oe.AVLNode=void 0;var ae=class t{k;v;l=null;r=null;h=1;constructor(e,n){this.k=e,this.v=new Set(n)}updateHeight(){this.h=Math.max(t.getHeight(this.l),t.getHeight(this.r))+1}static getHeight(e){return e?e.h:0}getBalanceFactor(){return t.getHeight(this.l)-t.getHeight(this.r)}rotateLeft(){let e=this.r;return this.r=e.l,e.l=this,this.updateHeight(),e.updateHeight(),e}rotateRight(){let e=this.l;return this.l=e.r,e.r=this,this.updateHeight(),e.updateHeight(),e}toJSON(){return{k:this.k,v:Array.from(this.v),l:this.l?this.l.toJSON():null,r:this.r?this.r.toJSON():null,h:this.h}}static fromJSON(e){let n=new t(e.k,e.v);return n.l=e.l?t.fromJSON(e.l):null,n.r=e.r?t.fromJSON(e.r):null,n.h=e.h,n}};Oe.AVLNode=ae;var _n=class t{root=null;insertCount=0;constructor(e,n){e!==void 0&&n!==void 0&&(this.root=new ae(e,n))}insert(e,n,r=1e3){this.root=this.insertNode(this.root,e,n,r)}insertMultiple(e,n,r=1e3){for(let s of n)this.insert(e,s,r)}rebalance(){this.root&&(this.root=this.rebalanceNode(this.root))}toJSON(){return{root:this.root?this.root.toJSON():null,insertCount:this.insertCount}}static fromJSON(e){let n=new t;return n.root=e.root?ae.fromJSON(e.root):null,n.insertCount=e.insertCount||0,n}insertNode(e,n,r,s){if(e===null)return new ae(n,[r]);let i=[],o=e,c=null;for(;o!==null;)if(i.push({parent:c,node:o}),no.k)if(o.r===null){o.r=new ae(n,[r]),i.push({parent:o,node:o.r});break}else c=o,o=o.r;else return o.v.add(r),e;let u=!1;this.insertCount++%s===0&&(u=!0);for(let a=i.length-1;a>=0;a--){let{parent:l,node:d}=i[a];if(d.updateHeight(),u){let f=this.rebalanceNode(d);l?l.l===d?l.l=f:l.r===d&&(l.r=f):e=f}}return e}rebalanceNode(e){let n=e.getBalanceFactor();if(n>1){if(e.l&&e.l.getBalanceFactor()>=0)return e.rotateRight();if(e.l)return e.l=e.l.rotateLeft(),e.rotateRight()}if(n<-1){if(e.r&&e.r.getBalanceFactor()<=0)return e.rotateLeft();if(e.r)return e.r=e.r.rotateRight(),e.rotateLeft()}return e}find(e){let n=this.findNodeByKey(e);return n?n.v:null}contains(e){return this.find(e)!==null}getSize(){let e=0,n=[],r=this.root;for(;r||n.length>0;){for(;r;)n.push(r),r=r.l;r=n.pop(),e++,r=r.r}return e}isBalanced(){if(!this.root)return!0;let e=[this.root];for(;e.length>0;){let n=e.pop(),r=n.getBalanceFactor();if(Math.abs(r)>1)return!1;n.l&&e.push(n.l),n.r&&e.push(n.r)}return!0}remove(e){this.root=this.removeNode(this.root,e)}removeDocument(e,n){let r=this.findNodeByKey(e);r&&(r.v.size===1?this.root=this.removeNode(this.root,e):r.v=new Set([...r.v.values()].filter(s=>s!==n)))}findNodeByKey(e){let n=this.root;for(;n;)if(en.k)n=n.r;else return n;return null}removeNode(e,n){if(e===null)return null;let r=[],s=e;for(;s!==null&&s.k!==n;)r.push(s),n=0;i--){let o=r[i];o.updateHeight();let c=this.rebalanceNode(o);if(i>0){let u=r[i-1];u.l===o?u.l=c:u.r===o&&(u.r=c)}else e=c}return e}rangeSearch(e,n){let r=new Set,s=[],i=this.root;for(;i||s.length>0;){for(;i;)s.push(i),i=i.l;if(i=s.pop(),i.k>=e&&i.k<=n)for(let o of i.v)r.add(o);if(i.k>n)break;i=i.r}return r}greaterThan(e,n=!1){let r=new Set,s=[],i=this.root;for(;i||s.length>0;){for(;i;)s.push(i),i=i.r;if(i=s.pop(),n&&i.k>=e||!n&&i.k>e)for(let o of i.v)r.add(o);else if(i.k<=e)break;i=i.l}return r}lessThan(e,n=!1){let r=new Set,s=[],i=this.root;for(;i||s.length>0;){for(;i;)s.push(i),i=i.l;if(i=s.pop(),n&&i.k<=e||!n&&i.ke)break;i=i.r}return r}};Oe.AVLTree=_n});var ws=b(ft=>{"use strict";Object.defineProperty(ft,"__esModule",{value:!0});ft.FlatTree=void 0;var Sn=class t{numberToDocumentId;constructor(){this.numberToDocumentId=new Map}insert(e,n){this.numberToDocumentId.has(e)?this.numberToDocumentId.get(e).add(n):this.numberToDocumentId.set(e,new Set([n]))}find(e){let n=this.numberToDocumentId.get(e);return n?Array.from(n):null}remove(e){this.numberToDocumentId.delete(e)}removeDocument(e,n){let r=this.numberToDocumentId.get(n);r&&(r.delete(e),r.size===0&&this.numberToDocumentId.delete(n))}contains(e){return this.numberToDocumentId.has(e)}getSize(){let e=0;for(let n of this.numberToDocumentId.values())e+=n.size;return e}filter(e){let n=Object.keys(e);if(n.length!==1)throw new Error("Invalid operation");let r=n[0];switch(r){case"eq":{let s=e[r],i=this.numberToDocumentId.get(s);return i?Array.from(i):[]}case"in":{let s=e[r],i=new Set;for(let o of s){let c=this.numberToDocumentId.get(o);if(c)for(let u of c)i.add(u)}return Array.from(i)}case"nin":{let s=new Set(e[r]),i=new Set;for(let[o,c]of this.numberToDocumentId.entries())if(!s.has(o))for(let u of c)i.add(u);return Array.from(i)}default:throw new Error("Invalid operation")}}filterArr(e){let n=Object.keys(e);if(n.length!==1)throw new Error("Invalid operation");let r=n[0];switch(r){case"containsAll":{let i=e[r].map(c=>this.numberToDocumentId.get(c)??new Set);if(i.length===0)return[];let o=i.reduce((c,u)=>new Set([...c].filter(a=>u.has(a))));return Array.from(o)}case"containsAny":{let i=e[r].map(c=>this.numberToDocumentId.get(c)??new Set);if(i.length===0)return[];let o=i.reduce((c,u)=>new Set([...c,...u]));return Array.from(o)}default:throw new Error("Invalid operation")}}static fromJSON(e){if(!e.numberToDocumentId)throw new Error("Invalid Flat Tree JSON");let n=new t;for(let[r,s]of e.numberToDocumentId)n.numberToDocumentId.set(r,new Set(s));return n}toJSON(){return{numberToDocumentId:Array.from(this.numberToDocumentId.entries()).map(([e,n])=>[e,Array.from(n)])}}};ft.FlatTree=Sn});var bn=b(Ve=>{"use strict";Object.defineProperty(Ve,"__esModule",{value:!0});Ve.boundedLevenshtein=su;Ve.syncBoundedLevenshtein=iu;Ve.levenshtein=ou;function _s(t,e,n){if(n<0)return-1;if(t===e)return 0;let r=t.length,s=e.length;if(r===0)return s<=n?s:-1;if(s===0)return r<=n?r:-1;let i=Math.abs(r-s);if(t.startsWith(e))return i<=n?i:-1;if(e.startsWith(t))return 0;if(i>n)return-1;let o=[];for(let c=0;c<=r;c++){o[c]=[c];for(let u=1;u<=s;u++)o[c][u]=c===0?u:0}for(let c=1;c<=r;c++){let u=1/0;for(let a=1;a<=s;a++)t[c-1]===e[a-1]?o[c][a]=o[c-1][a-1]:o[c][a]=Math.min(o[c-1][a]+1,o[c][a-1]+1,o[c-1][a-1]+1),u=Math.min(u,o[c][a]);if(u>n)return-1}return o[r][s]<=n?o[r][s]:-1}function su(t,e,n){let r=_s(t,e,n);return{distance:r,isBounded:r>=0}}function iu(t,e,n){let r=_s(t,e,n);return{distance:r,isBounded:r>=0}}function ou(t,e){if(!t.length)return e.length;if(!e.length)return t.length;let n=t;t.length>e.length&&(t=e,e=n);let r=Array.from({length:t.length+1},(i,o)=>o),s=0;for(let i=1;i<=e.length;i++){let o=i;for(let c=1;c<=t.length;c++)e[i-1]===t[c-1]?s=r[c-1]:s=Math.min(r[c-1]+1,Math.min(o+1,r[c]+1)),r[c-1]=o,o=s;r[t.length]=o}return r[t.length]}});var bs=b(Pe=>{"use strict";Object.defineProperty(Pe,"__esModule",{value:!0});Pe.RadixTree=Pe.RadixNode=void 0;var Ss=bn(),In=R(),We=class t{k;s;c=new Map;d=new Set;e;w="";constructor(e,n,r){this.k=e,this.s=n,this.e=r}updateParent(e){this.w=e.w+this.s}addDocument(e){this.d.add(e)}removeDocument(e){return this.d.delete(e)}findAllWords(e,n,r,s){let i=[this];for(;i.length>0;){let o=i.pop();if(o.e){let{w:c,d:u}=o;if(r&&c!==n)continue;if((0,In.getOwnProperty)(e,c)!==null)if(s)if(Math.abs(n.length-c.length)<=s&&(0,Ss.syncBoundedLevenshtein)(n,c,s).isBounded)e[c]=[];else continue;else e[c]=[];if((0,In.getOwnProperty)(e,c)!=null&&u.size>0){let a=e[c];for(let l of u)a.includes(l)||a.push(l)}}o.c.size>0&&i.push(...o.c.values())}return e}insert(e,n){let r=this,s=0,i=e.length;for(;s0;){let{node:c,index:u,tolerance:a}=o.pop();if(c.w.startsWith(e)){c.findAllWords(i,e,!1,0);continue}if(a<0)continue;if(c.e){let{w:d,d:f}=c;if(d&&((0,Ss.syncBoundedLevenshtein)(e,d,s).isBounded&&(i[d]=[]),(0,In.getOwnProperty)(i,d)!==void 0&&f.size>0)){let p=new Set(i[d]);for(let g of f)p.add(g);i[d]=Array.from(p)}}if(u>=e.length)continue;let l=e[u];if(c.c.has(l)){let d=c.c.get(l);o.push({node:d,index:u+1,tolerance:a})}o.push({node:c,index:u+1,tolerance:a-1});for(let[d,f]of c.c)o.push({node:f,index:u,tolerance:a-1}),d!==l&&o.push({node:f,index:u+1,tolerance:a-1})}}find(e){let{term:n,exact:r,tolerance:s}=e;if(s&&!r){let i={};return this._findLevenshtein(n,0,s,s,i),i}else{let i=this,o=0,c=n.length;for(;o0&&n.c.size===0&&!n.e&&n.d.size===0;){let{parent:i,character:o}=s.pop();i.c.delete(o),n=i}return!0}removeDocumentByWord(e,n,r=!0){if(!e)return!0;let s=this,i=e.length;for(let o=0;o[e,n.toJSON()])}}static fromJSON(e){let n=new t(e.k,e.s,e.e);return n.w=e.w,n.d=new Set(e.d),n.c=new Map(e?.c?.map(([r,s])=>[r,t.fromJSON(s)])||[]),n}};Pe.RadixNode=We;var xn=class t extends We{constructor(){super("","",!1)}static fromJSON(e){let n=new t;return n.w=e.w,n.s=e.s,n.e=e.e,n.k=e.k,n.d=new Set(e.d),n.c=new Map(e?.c?.map(([r,s])=>[r,We.fromJSON(s)])||[]),n}toJSON(){return super.toJSON()}};Pe.RadixTree=xn});var Is=b(pt=>{"use strict";Object.defineProperty(pt,"__esModule",{value:!0});pt.BKDTree=void 0;var cu=2,uu=6371e3,ht=class t{point;docIDs;left;right;parent;constructor(e,n){this.point=e,this.docIDs=new Set(n),this.left=null,this.right=null,this.parent=null}toJSON(){return{point:this.point,docIDs:Array.from(this.docIDs),left:this.left?this.left.toJSON():null,right:this.right?this.right.toJSON():null}}static fromJSON(e,n=null){let r=new t(e.point,e.docIDs);return r.parent=n,e.left&&(r.left=t.fromJSON(e.left,r)),e.right&&(r.right=t.fromJSON(e.right,r)),r}},En=class t{root;nodeMap;constructor(){this.root=null,this.nodeMap=new Map}getPointKey(e){return`${e.lon},${e.lat}`}insert(e,n){let r=this.getPointKey(e),s=this.nodeMap.get(r);if(s){n.forEach(u=>s.docIDs.add(u));return}let i=new ht(e,n);if(this.nodeMap.set(r,i),this.root==null){this.root=i;return}let o=this.root,c=0;for(;;){if(c%cu===0)if(e.lon0;){let{node:a,depth:l}=c.pop();if(a==null)continue;let d=o(e,a.point);(r?d<=n:d>n)&&u.push({point:a.point,docIDs:Array.from(a.docIDs)}),a.left!=null&&c.push({node:a.left,depth:l+1}),a.right!=null&&c.push({node:a.right,depth:l+1})}return s&&u.sort((a,l)=>{let d=o(e,a.point),f=o(e,l.point);return s.toLowerCase()==="asc"?d-f:f-d}),u}searchByPolygon(e,n=!0,r=null,s=!1){let i=[{node:this.root,depth:0}],o=[];for(;i.length>0;){let{node:u,depth:a}=i.pop();if(u==null)continue;u.left!=null&&i.push({node:u.left,depth:a+1}),u.right!=null&&i.push({node:u.right,depth:a+1});let l=t.isPointInPolygon(e,u.point);(l&&n||!l&&!n)&&o.push({point:u.point,docIDs:Array.from(u.docIDs)})}let c=t.calculatePolygonCentroid(e);if(r){let u=s?t.vincentyDistance:t.haversineDistance;o.sort((a,l)=>{let d=u(c,a.point),f=u(c,l.point);return r.toLowerCase()==="asc"?d-f:f-d})}return o}toJSON(){return{root:this.root?this.root.toJSON():null}}static fromJSON(e){let n=new t;return e.root&&(n.root=ht.fromJSON(e.root),n.buildNodeMap(n.root)),n}buildNodeMap(e){if(e==null)return;let n=this.getPointKey(e.point);this.nodeMap.set(n,e),e.left&&this.buildNodeMap(e.left),e.right&&this.buildNodeMap(e.right)}static calculatePolygonCentroid(e){let n=0,r=0,s=0,i=e.length;for(let c=0,u=i-1;ci!=f>i&&s<(d-a)*(i-l)/(f-l)+a&&(r=!r)}return r}static haversineDistance(e,n){let r=Math.PI/180,s=e.lat*r,i=n.lat*r,o=(n.lat-e.lat)*r,c=(n.lon-e.lon)*r,u=Math.sin(o/2)*Math.sin(o/2)+Math.cos(s)*Math.cos(i)*Math.sin(c/2)*Math.sin(c/2),a=2*Math.atan2(Math.sqrt(u),Math.sqrt(1-u));return uu*a}static vincentyDistance(e,n){let s=.0033528106647474805,i=(1-s)*6378137,o=Math.PI/180,c=e.lat*o,u=n.lat*o,a=(n.lon-e.lon)*o,l=Math.atan((1-s)*Math.tan(c)),d=Math.atan((1-s)*Math.tan(u)),f=Math.sin(l),p=Math.cos(l),g=Math.sin(d),y=Math.cos(d),h=a,m,w=1e3,S,_,I,v,D,A;do{let Me=Math.sin(h),Be=Math.cos(h);if(S=Math.sqrt(y*Me*(y*Me)+(p*g-f*y*Be)*(p*g-f*y*Be)),S===0)return 0;_=f*g+p*y*Be,I=Math.atan2(S,_),v=p*y*Me/S,D=1-v*v,A=_-2*f*g/D,isNaN(A)&&(A=0);let pn=s/16*D*(4+s*(4-3*D));m=h,h=a+(1-pn)*s*v*(I+pn*S*(A+pn*_*(-1+2*A*A)))}while(Math.abs(h-m)>1e-12&&--w>0);if(w===0)return NaN;let k=D*(6378137*6378137-i*i)/(i*i),De=1+k/16384*(4096+k*(-768+k*(320-175*k))),X=k/1024*(256+k*(-128+k*(74-47*k))),hn=X*S*(A+X/4*(_*(-1+2*A*A)-X/6*A*(-3+4*S*S)*(-3+4*A*A)));return i*De*(I-hn)}};pt.BKDTree=En});var xs=b(gt=>{"use strict";Object.defineProperty(gt,"__esModule",{value:!0});gt.BoolNode=void 0;var An=class t{true;false;constructor(){this.true=new Set,this.false=new Set}insert(e,n){n?this.true.add(e):this.false.add(e)}delete(e,n){n?this.true.delete(e):this.false.delete(e)}getSize(){return this.true.size+this.false.size}toJSON(){return{true:Array.from(this.true),false:Array.from(this.false)}}static fromJSON(e){let n=new t;return n.true=new Set(e.true),n.false=new Set(e.false),n}};gt.BoolNode=An});var Es=b(yt=>{"use strict";Object.defineProperty(yt,"__esModule",{value:!0});yt.prioritizeTokenScores=lu;yt.BM25=du;var au=j();function lu(t,e,n=0,r){if(e===0)throw(0,au.createError)("INVALID_BOOST_VALUE");let s=new Map,i=t.length;for(let y=0;yh[1]-y[1]);if(n===1||n===0&&r===1)return c;let u=c.length,a=[];for(let y of s.entries())a.push([y[0],y[1][0],y[1][1]]);let l=a.sort((y,h)=>y[2]>h[2]?-1:y[2]h[1]?-1:y[1]"u"){if(n===0)return[];d=0}let f=l.length,p=new Array(f);for(let y=0;y{"use strict";Object.defineProperty(le,"__esModule",{value:!0});le.VectorIndex=le.DEFAULT_SIMILARITY=void 0;le.getMagnitude=Tn;le.findSimilarVectors=As;le.DEFAULT_SIMILARITY=.8;var vn=class t{size;vectors=new Map;constructor(e){this.size=e}add(e,n){n instanceof Float32Array||(n=new Float32Array(n));let r=Tn(n,this.size);this.vectors.set(e,[r,n])}remove(e){this.vectors.delete(e)}find(e,n,r){return e instanceof Float32Array||(e=new Float32Array(e)),As(e,r,this.vectors,this.size,n)}toJSON(){let e=[];for(let[n,[r,s]]of this.vectors)e.push([n,[r,Array.from(s)]]);return{size:this.size,vectors:e}}static fromJSON(e){let n=e,r=new t(n.size);for(let[s,[i,o]]of n.vectors)r.vectors.set(s,[i,new Float32Array(o)]);return r}};le.VectorIndex=vn;function Tn(t,e){let n=0;for(let r=0;r=s&&o.push([u,p])}return o}});var mt=b(L=>{"use strict";Object.defineProperty(L,"__esModule",{value:!0});L.insertDocumentScoreParameters=Us;L.insertTokenScoreParameters=Rs;L.removeDocumentScoreParameters=Ls;L.removeTokenScoreParameters=js;L.create=On;L.insert=Cs;L.insertVector=Fs;L.remove=Bs;L.calculateResultScores=Pn;L.search=$s;L.searchByWhereClause=Ke;L.getSearchableProperties=qs;L.getSearchablePropertiesWithTypes=zs;L.load=Vs;L.save=Ws;L.createIndex=pu;L.searchByGeoWhereClause=yu;var ke=j(),Ms=ms(),Os=ws(),Ps=bs(),He=Is(),ks=xs(),ne=R(),fu=Es(),xe=$e(),Mn=V(),Ns=Dn();function Us(t,e,n,r,s){let i=(0,Mn.getInternalDocumentId)(t.sharedInternalDocumentStore,n);t.avgFieldLength[e]=((t.avgFieldLength[e]??0)*(s-1)+r.length)/s,t.fieldLengths[e][i]=r.length,t.frequencies[e][i]={}}function Rs(t,e,n,r,s){let i=0;for(let u of r)u===s&&i++;let o=(0,Mn.getInternalDocumentId)(t.sharedInternalDocumentStore,n),c=i/r.length;t.frequencies[e][o][s]=c,s in t.tokenOccurrences[e]||(t.tokenOccurrences[e][s]=0),t.tokenOccurrences[e][s]=(t.tokenOccurrences[e][s]??0)+1}function Ls(t,e,n,r){let s=(0,Mn.getInternalDocumentId)(t.sharedInternalDocumentStore,n);r>1?t.avgFieldLength[e]=(t.avgFieldLength[e]*r-t.fieldLengths[e][s])/(r-1):t.avgFieldLength[e]=void 0,t.fieldLengths[e][s]=void 0,t.frequencies[e][s]=void 0}function js(t,e,n){t.tokenOccurrences[e][n]--}function On(t,e,n,r,s=""){r||(r={sharedInternalDocumentStore:e,indexes:{},vectorIndexes:{},searchableProperties:[],searchablePropertiesWithTypes:{},frequencies:{},tokenOccurrences:{},avgFieldLength:{},fieldLengths:{}});for(let[i,o]of Object.entries(n)){let c=`${s}${s?".":""}${i}`;if(typeof o=="object"&&!Array.isArray(o)){On(t,e,o,r,c);continue}if((0,xe.isVectorType)(o))r.searchableProperties.push(c),r.searchablePropertiesWithTypes[c]=o,r.vectorIndexes[c]={type:"Vector",node:new Ns.VectorIndex((0,xe.getVectorSize)(o)),isArray:!1};else{let u=/\[/.test(o);switch(o){case"boolean":case"boolean[]":r.indexes[c]={type:"Bool",node:new ks.BoolNode,isArray:u};break;case"number":case"number[]":r.indexes[c]={type:"AVL",node:new Ms.AVLTree(0,[]),isArray:u};break;case"string":case"string[]":r.indexes[c]={type:"Radix",node:new Ps.RadixTree,isArray:u},r.avgFieldLength[c]=0,r.frequencies[c]={},r.tokenOccurrences[c]={},r.fieldLengths[c]={};break;case"enum":case"enum[]":r.indexes[c]={type:"Flat",node:new Os.FlatTree,isArray:u};break;case"geopoint":r.indexes[c]={type:"BKD",node:new He.BKDTree,isArray:u};break;default:throw(0,ke.createError)("INVALID_SCHEMA_TYPE",Array.isArray(o)?"array":o,c)}r.searchableProperties.push(c),r.searchablePropertiesWithTypes[c]=o}}return r}function hu(t,e,n,r,s,i,o,c){return u=>{let{type:a,node:l}=e.indexes[n];switch(a){case"Bool":{l[u?"true":"false"].add(r);break}case"AVL":{let d=c?.avlRebalanceThreshold??1;l.insert(u,r,d);break}case"Radix":{let d=i.tokenize(u,s,n,!1);t.insertDocumentScoreParameters(e,n,r,d,o);for(let f of d)t.insertTokenScoreParameters(e,n,r,d,f),l.insert(f,r);break}case"Flat":{l.insert(u,r);break}case"BKD":{l.insert(u,[r]);break}}}}function Cs(t,e,n,r,s,i,o,c,u,a,l){if((0,xe.isVectorType)(o))return Fs(e,n,i,r,s);let d=hu(t,e,n,s,c,u,a,l);if(!(0,xe.isArrayType)(o))return d(i);let f=i,p=f.length;for(let g=0;g0&&y.set(k,!0);let hn=X.length;for(let at=0;at[S,_]).sort((S,_)=>_[1]-S[1]);if(m.length===0)return[];if(d===1)return m;if(d===0){if(p===1)return m;for(let _ of f)if(!y.get(_))return[];return m.filter(([_])=>{let I=g.get(_);return I?Array.from(I.values()).some(v=>v===p):!1})}let w=m.filter(([S])=>{let _=g.get(S);return _?Array.from(_.values()).some(I=>I===p):!1});if(w.length>0){let S=m.filter(([I])=>!w.some(([v])=>v===I)),_=Math.ceil(S.length*d);return[...w,...S.slice(0,_)]}return m}function Ke(t,e,n,r){if("and"in n&&n.and&&Array.isArray(n.and)){let o=n.and;if(o.length===0)return new Set;let c=o.map(u=>Ke(t,e,u,r));return(0,ne.setIntersection)(...c)}if("or"in n&&n.or&&Array.isArray(n.or)){let o=n.or;return o.length===0?new Set:o.map(u=>Ke(t,e,u,r)).reduce((u,a)=>(0,ne.setUnion)(u,a),new Set)}if("not"in n&&n.not){let o=n.not,c=new Set,u=t.sharedInternalDocumentStore;for(let l=1;l<=u.internalIdToId.length;l++)c.add(l);let a=Ke(t,e,o,r);return(0,ne.setDifference)(c,a)}let s=Object.keys(n),i=s.reduce((o,c)=>({[c]:new Set,...o}),{});for(let o of s){let c=n[o];if(typeof t.indexes[o]>"u")throw(0,ke.createError)("UNKNOWN_FILTER_PROPERTY",o);let{node:u,type:a,isArray:l}=t.indexes[o];if(a==="Bool"){let f=u,p=c?f.true:f.false;i[o]=(0,ne.setUnion)(i[o],p);continue}if(a==="BKD"){let f;if("radius"in c)f="radius";else if("polygon"in c)f="polygon";else throw new Error(`Invalid operation ${c}`);if(f==="radius"){let{value:p,coordinates:g,unit:y="m",inside:h=!0,highPrecision:m=!1}=c[f],w=(0,ne.convertDistanceToMeters)(p,y),S=u.searchByRadius(g,w,h,void 0,m);i[o]=Ts(i[o],S)}else{let{coordinates:p,inside:g=!0,highPrecision:y=!1}=c[f],h=u.searchByPolygon(p,g,void 0,y);i[o]=Ts(i[o],h)}continue}if(a==="Radix"&&(typeof c=="string"||Array.isArray(c))){for(let f of[c].flat()){let p=e.tokenize(f,r,o);for(let g of p){let y=u.find({term:g,exact:!0});i[o]=mu(i[o],y)}}continue}let d=Object.keys(c);if(d.length>1)throw(0,ke.createError)("INVALID_FILTER_OPERATION",d.length);if(a==="Flat"){let f=new Set(l?u.filterArr(c):u.filter(c));i[o]=(0,ne.setUnion)(i[o],f);continue}if(a==="AVL"){let f=d[0],p=c[f],g;switch(f){case"gt":{g=u.greaterThan(p,!1);break}case"gte":{g=u.greaterThan(p,!0);break}case"lt":{g=u.lessThan(p,!1);break}case"lte":{g=u.lessThan(p,!0);break}case"eq":{g=u.find(p)??new Set;break}case"between":{let[y,h]=p;g=u.rangeSearch(y,h);break}default:throw(0,ke.createError)("INVALID_FILTER_OPERATION",f)}i[o]=(0,ne.setUnion)(i[o],g)}}return(0,ne.setIntersection)(...Object.values(i))}function qs(t){return t.searchableProperties}function zs(t){return t.searchablePropertiesWithTypes}function Vs(t,e){let{indexes:n,vectorIndexes:r,searchableProperties:s,searchablePropertiesWithTypes:i,frequencies:o,tokenOccurrences:c,avgFieldLength:u,fieldLengths:a}=e,l={},d={};for(let f of Object.keys(n)){let{node:p,type:g,isArray:y}=n[f];switch(g){case"Radix":l[f]={type:"Radix",node:Ps.RadixTree.fromJSON(p),isArray:y};break;case"Flat":l[f]={type:"Flat",node:Os.FlatTree.fromJSON(p),isArray:y};break;case"AVL":l[f]={type:"AVL",node:Ms.AVLTree.fromJSON(p),isArray:y};break;case"BKD":l[f]={type:"BKD",node:He.BKDTree.fromJSON(p),isArray:y};break;case"Bool":l[f]={type:"Bool",node:ks.BoolNode.fromJSON(p),isArray:y};break;default:l[f]=n[f]}}for(let f of Object.keys(r))d[f]={type:"Vector",isArray:!1,node:Ns.VectorIndex.fromJSON(r[f])};return{sharedInternalDocumentStore:t,indexes:l,vectorIndexes:d,searchableProperties:s,searchablePropertiesWithTypes:i,frequencies:o,tokenOccurrences:c,avgFieldLength:u,fieldLengths:a}}function Ws(t){let{indexes:e,vectorIndexes:n,searchableProperties:r,searchablePropertiesWithTypes:s,frequencies:i,tokenOccurrences:o,avgFieldLength:c,fieldLengths:u}=t,a={};for(let d of Object.keys(n))a[d]=n[d].node.toJSON();let l={};for(let d of Object.keys(e)){let{type:f,node:p,isArray:g}=e[d];f==="Flat"||f==="Radix"||f==="AVL"||f==="BKD"||f==="Bool"?l[d]={type:f,node:p.toJSON(),isArray:g}:(l[d]=e[d],l[d].node=l[d].node.toJSON())}return{indexes:l,vectorIndexes:a,searchableProperties:r,searchablePropertiesWithTypes:s,frequencies:i,tokenOccurrences:o,avgFieldLength:c,fieldLengths:u}}function pu(){return{create:On,insert:Cs,remove:Bs,insertDocumentScoreParameters:Us,insertTokenScoreParameters:Rs,removeDocumentScoreParameters:Ls,removeTokenScoreParameters:js,calculateResultScores:Pn,search:$s,searchByWhereClause:Ke,getSearchableProperties:qs,getSearchablePropertiesWithTypes:zs,load:Vs,save:Ws}}function Ts(t,e){t||(t=new Set);let n=e.length;for(let r=0;ra[1]-u[1]),s}function gu(t,e){let n=Object.keys(t);if(n.length!==1)return{isGeoOnly:!1};let r=n[0],s=t[r];if(typeof e.indexes[r]>"u")return{isGeoOnly:!1};let{type:i}=e.indexes[r];return i==="BKD"&&s&&("radius"in s||"polygon"in s)?{isGeoOnly:!0,geoProperty:r,geoOperation:s}:{isGeoOnly:!1}}function yu(t,e){let n=t,r=gu(e,n);if(!r.isGeoOnly||!r.geoProperty||!r.geoOperation)return null;let{node:s}=n.indexes[r.geoProperty],i=r.geoOperation,o=s,c;if("radius"in i){let{value:u,coordinates:a,unit:l="m",inside:d=!0,highPrecision:f=!1}=i.radius,p=a,g=(0,ne.convertDistanceToMeters)(u,l);return c=o.searchByRadius(p,g,d,"asc",f),Ds(c,p,f)}else if("polygon"in i){let{coordinates:u,inside:a=!0,highPrecision:l=!1}=i.polygon;c=o.searchByPolygon(u,a,"asc",l);let d=He.BKDTree.calculatePolygonCentroid(u);return Ds(c,d,l)}return null}function mu(t,e){t||(t=new Set);let n=Object.keys(e),r=n.length;for(let s=0;s{"use strict";Object.defineProperty(Ge,"__esModule",{value:!0});Ge.load=Gs;Ge.save=Ys;Ge.createSorter=ku;var kn=j(),wu=$e(),wt=V(),_u=R(),Su=lt();function Ks(t,e,n,r,s){let i={language:t.tokenizer.language,sharedInternalDocumentStore:e,enabled:!0,isSorted:!0,sortableProperties:[],sortablePropertiesWithTypes:{},sorts:{}};for(let[o,c]of Object.entries(n)){let u=`${s}${s?".":""}${o}`;if(!r.includes(u)){if(typeof c=="object"&&!Array.isArray(c)){let a=Ks(t,e,c,r,u);(0,_u.safeArrayPush)(i.sortableProperties,a.sortableProperties),i.sorts={...i.sorts,...a.sorts},i.sortablePropertiesWithTypes={...i.sortablePropertiesWithTypes,...a.sortablePropertiesWithTypes};continue}if(!(0,wu.isVectorType)(c))switch(c){case"boolean":case"number":case"string":i.sortableProperties.push(u),i.sortablePropertiesWithTypes[u]=c,i.sorts[u]={docs:new Map,orderedDocsToRemove:new Map,orderedDocs:[],type:c};break;case"geopoint":case"enum":continue;case"enum[]":case"boolean[]":case"number[]":case"string[]":continue;default:throw(0,kn.createError)("INVALID_SORT_SCHEMA_TYPE",Array.isArray(c)?"array":c,u)}}}return i}function bu(t,e,n,r){return r?.enabled!==!1?Ks(t,e,n,(r||{}).unsortableProperties||[],""):{disabled:!0}}function Iu(t,e,n,r){if(!t.enabled)return;t.isSorted=!1;let s=(0,wt.getInternalDocumentId)(t.sharedInternalDocumentStore,n),i=t.sorts[e];i.orderedDocsToRemove.has(s)&&Nn(t,e),i.docs.set(s,i.orderedDocs.length),i.orderedDocs.push([s,r])}function Hs(t){if(t.isSorted||!t.enabled)return;let e=Object.keys(t.sorts);for(let n of e)vu(t,n);t.isSorted=!0}function xu(t,e,n){return e[1].localeCompare(n[1],(0,Su.getLocale)(t))}function Eu(t,e){return t[1]-e[1]}function Au(t,e){return e[1]?-1:1}function vu(t,e){let n=t.sorts[e],r;switch(n.type){case"string":r=xu.bind(null,t.language);break;case"number":r=Eu.bind(null);break;case"boolean":r=Au.bind(null);break}n.orderedDocs.sort(r);let s=n.orderedDocs.length;for(let i=0;i!n.orderedDocsToRemove.has(r[0])),n.orderedDocsToRemove.clear())}function Du(t,e,n){if(!t.enabled)return;let r=t.sorts[e],s=(0,wt.getInternalDocumentId)(t.sharedInternalDocumentStore,n);r.docs.get(s)&&(r.docs.delete(s),r.orderedDocsToRemove.set(s,!0))}function Mu(t,e,n){if(!t.enabled)throw(0,kn.createError)("SORT_DISABLED");let r=n.property,s=n.order==="DESC",i=t.sorts[r];if(!i)throw(0,kn.createError)("UNABLE_TO_SORT_ON_UNKNOWN_FIELD",r,t.sortableProperties.join(", "));return Nn(t,r),Hs(t),e.sort((o,c)=>{let u=i.docs.get((0,wt.getInternalDocumentId)(t.sharedInternalDocumentStore,o[0])),a=i.docs.get((0,wt.getInternalDocumentId)(t.sharedInternalDocumentStore,c[0])),l=typeof u<"u",d=typeof a<"u";return!l&&!d?0:l?d?s?a-u:u-a:-1:1}),e}function Ou(t){return t.enabled?t.sortableProperties:[]}function Pu(t){return t.enabled?t.sortablePropertiesWithTypes:{}}function Gs(t,e){let n=e;if(!n.enabled)return{enabled:!1};let r=Object.keys(n.sorts).reduce((s,i)=>{let{docs:o,orderedDocs:c,type:u}=n.sorts[i];return s[i]={docs:new Map(Object.entries(o).map(([a,l])=>[+a,l])),orderedDocsToRemove:new Map,orderedDocs:c,type:u},s},{});return{sharedInternalDocumentStore:t,language:n.language,sortableProperties:n.sortableProperties,sortablePropertiesWithTypes:n.sortablePropertiesWithTypes,sorts:r,enabled:!0,isSorted:n.isSorted}}function Ys(t){if(!t.enabled)return{enabled:!1};Tu(t),Hs(t);let e=Object.keys(t.sorts).reduce((n,r)=>{let{docs:s,orderedDocs:i,type:o}=t.sorts[r];return n[r]={docs:Object.fromEntries(s.entries()),orderedDocs:i,type:o},n},{});return{language:t.language,sortableProperties:t.sortableProperties,sortablePropertiesWithTypes:t.sortablePropertiesWithTypes,sorts:e,enabled:t.enabled,isSorted:t.isSorted}}function ku(){return{create:bu,insert:Iu,remove:Du,save:Ys,load:Gs,sortBy:Mu,getSortableProperties:Ou,getSortablePropertiesWithTypes:Pu}}});var Xs=b(Rn=>{"use strict";Object.defineProperty(Rn,"__esModule",{value:!0});Rn.replaceDiacritics=Lu;var Js=192,Nu=383,Uu=[65,65,65,65,65,65,65,67,69,69,69,69,73,73,73,73,69,78,79,79,79,79,79,null,79,85,85,85,85,89,80,115,97,97,97,97,97,97,97,99,101,101,101,101,105,105,105,105,101,110,111,111,111,111,111,null,111,117,117,117,117,121,112,121,65,97,65,97,65,97,67,99,67,99,67,99,67,99,68,100,68,100,69,101,69,101,69,101,69,101,69,101,71,103,71,103,71,103,71,103,72,104,72,104,73,105,73,105,73,105,73,105,73,105,73,105,74,106,75,107,107,76,108,76,108,76,108,76,108,76,108,78,110,78,110,78,110,110,78,110,79,111,79,111,79,111,79,111,82,114,82,114,82,114,83,115,83,115,83,115,83,115,84,116,84,116,84,116,85,117,85,117,85,117,85,117,85,117,85,117,87,119,89,121,89,90,122,90,122,90,122,115];function Ru(t){return tNu?t:Uu[t-Js]||t}function Lu(t){let e=[];for(let n=0;n{"use strict";Object.defineProperty(jn,"__esModule",{value:!0});jn.stemmer=$u;var ju={ational:"ate",tional:"tion",enci:"ence",anci:"ance",izer:"ize",bli:"ble",alli:"al",entli:"ent",eli:"e",ousli:"ous",ization:"ize",ation:"ate",ator:"ate",alism:"al",iveness:"ive",fulness:"ful",ousness:"ous",aliti:"al",iviti:"ive",biliti:"ble",logi:"log"},Cu={icate:"ic",ative:"",alize:"al",iciti:"ic",ical:"ic",ful:"",ness:""},Fu="[^aeiou]",St="[aeiouy]",Z=Fu+"[^aeiouy]*",Ye=St+"[aeiou]*",Ln="^("+Z+")?"+Ye+Z,Bu="^("+Z+")?"+Ye+Z+"("+Ye+")?$",_t="^("+Z+")?"+Ye+Z+Ye+Z,Zs="^("+Z+")?"+St;function $u(t){let e,n,r,s,i,o;if(t.length<3)return t;let c=t.substring(0,1);if(c=="y"&&(t=c.toUpperCase()+t.substring(1)),r=/^(.+?)(ss|i)es$/,s=/^(.+?)([^s])s$/,r.test(t)?t=t.replace(r,"$1$2"):s.test(t)&&(t=t.replace(s,"$1$2")),r=/^(.+?)eed$/,s=/^(.+?)(ed|ing)$/,r.test(t)){let u=r.exec(t);r=new RegExp(Ln),r.test(u[1])&&(r=/.$/,t=t.replace(r,""))}else s.test(t)&&(e=s.exec(t)[1],s=new RegExp(Zs),s.test(e)&&(t=e,s=/(at|bl|iz)$/,i=new RegExp("([^aeiouylsz])\\1$"),o=new RegExp("^"+Z+St+"[^aeiouwxy]$"),s.test(t)?t=t+"e":i.test(t)?(r=/.$/,t=t.replace(r,"")):o.test(t)&&(t=t+"e")));if(r=/^(.+?)y$/,r.test(t)&&(e=r.exec(t)?.[1],r=new RegExp(Zs),e&&r.test(e)&&(t=e+"i")),r=/^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/,r.test(t)){let u=r.exec(t);e=u?.[1],n=u?.[2],r=new RegExp(Ln),e&&r.test(e)&&(t=e+ju[n])}if(r=/^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/,r.test(t)){let u=r.exec(t);e=u?.[1],n=u?.[2],r=new RegExp(Ln),e&&r.test(e)&&(t=e+Cu[n])}if(r=/^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/,s=/^(.+?)(s|t)(ion)$/,r.test(t))e=r.exec(t)?.[1],r=new RegExp(_t),e&&r.test(e)&&(t=e);else if(s.test(t)){let u=s.exec(t);e=u?.[1]??""+u?.[2]??"",s=new RegExp(_t),s.test(e)&&(t=e)}return r=/^(.+?)e$/,r.test(t)&&(e=r.exec(t)?.[1],r=new RegExp(_t),s=new RegExp(Bu),i=new RegExp("^"+Z+St+"[^aeiouwxy]$"),e&&(r.test(e)||s.test(e)&&!i.test(e))&&(t=e)),r=/ll$/,s=new RegExp(_t),r.test(t)&&s.test(t)&&(r=/.$/,t=t.replace(r,"")),c=="y"&&(t=c.toLowerCase()+t.substring(1)),t}});var It=b(bt=>{"use strict";Object.defineProperty(bt,"__esModule",{value:!0});bt.normalizeToken=Cn;bt.createTokenizer=Wu;var Ee=j(),qu=Xs(),ti=lt(),zu=Qs();function Cn(t,e,n=!0){let r=`${this.language}:${t}:${e}`;return n&&this.normalizationCache.has(r)?this.normalizationCache.get(r):this.stopWords?.includes(e)?(n&&this.normalizationCache.set(r,""),""):(this.stemmer&&!this.stemmerSkipProperties.has(t)&&(e=this.stemmer(e)),e=(0,qu.replaceDiacritics)(e),n&&this.normalizationCache.set(r,e),e)}function Vu(t){for(;t[t.length-1]==="";)t.pop();for(;t[0]==="";)t.shift();return t}function ei(t,e,n,r=!0){if(e&&e!==this.language)throw(0,Ee.createError)("LANGUAGE_NOT_SUPPORTED",e);if(typeof t!="string")return[t];let s=this.normalizeToken.bind(this,n??""),i;if(n&&this.tokenizeSkipProperties.has(n))i=[s(t,r)];else{let c=ti.SPLITTERS[this.language];i=t.toLowerCase().split(c).map(u=>s(u,r)).filter(Boolean)}let o=Vu(i);return this.allowDuplicates?o:Array.from(new Set(o))}function Wu(t={}){if(!t.language)t.language="english";else if(!ti.SUPPORTED_LANGUAGES.includes(t.language))throw(0,Ee.createError)("LANGUAGE_NOT_SUPPORTED",t.language);let e;if(t.stemming||t.stemmer&&!("stemming"in t))if(t.stemmer){if(typeof t.stemmer!="function")throw(0,Ee.createError)("INVALID_STEMMER_FUNCTION_TYPE");e=t.stemmer}else if(t.language==="english")e=zu.stemmer;else throw(0,Ee.createError)("MISSING_STEMMER",t.language);let n;if(t.stopWords!==!1){if(n=[],Array.isArray(t.stopWords))n=t.stopWords;else if(typeof t.stopWords=="function")n=t.stopWords(n);else if(t.stopWords)throw(0,Ee.createError)("CUSTOM_STOP_WORDS_MUST_BE_FUNCTION_OR_ARRAY");if(!Array.isArray(n))throw(0,Ee.createError)("CUSTOM_STOP_WORDS_MUST_BE_FUNCTION_OR_ARRAY");for(let s of n)if(typeof s!="string")throw(0,Ee.createError)("CUSTOM_STOP_WORDS_MUST_BE_FUNCTION_OR_ARRAY")}let r={tokenize:ei,language:t.language,stemmer:e,stemmerSkipProperties:new Set(t.stemmerSkipProperties?[t.stemmerSkipProperties].flat():[]),tokenizeSkipProperties:new Set(t.tokenizeSkipProperties?[t.tokenizeSkipProperties].flat():[]),stopWords:n,allowDuplicates:!!t.allowDuplicates,normalizeToken:Cn,normalizationCache:new Map};return r.tokenize=ei.bind(r),r.normalizeToken=Cn,r}});var Fn=b(Ne=>{"use strict";Object.defineProperty(Ne,"__esModule",{value:!0});Ne.getMatchingRules=ni;Ne.load=ri;Ne.save=si;Ne.createPinning=ea;function Ku(t){return{sharedInternalDocumentStore:t,rules:new Map}}function Hu(t,e){if(t.rules.has(e.id))throw new Error(`PINNING_RULE_ALREADY_EXISTS: A pinning rule with id "${e.id}" already exists. Use updateRule to modify it.`);t.rules.set(e.id,e)}function Gu(t,e){if(!t.rules.has(e.id))throw new Error(`PINNING_RULE_NOT_FOUND: Cannot update pinning rule with id "${e.id}" because it does not exist. Use addRule to create it.`);t.rules.set(e.id,e)}function Yu(t,e){return t.rules.delete(e)}function Ju(t,e){return t.rules.get(e)}function Xu(t){return Array.from(t.rules.values())}function Zu(t,e){let n=t.toLowerCase().trim(),r=e.pattern.toLowerCase().trim();switch(e.anchoring){case"is":return n===r;case"starts_with":return n.startsWith(r);case"contains":return n.includes(r);default:return!1}}function Qu(t,e){return t?e.conditions.every(n=>Zu(t,n)):!1}function ni(t,e){if(!e)return[];let n=[];for(let r of t.rules.values())Qu(e,r)&&n.push(r);return n}function ri(t,e){let n=e;return{sharedInternalDocumentStore:t,rules:new Map(n?.rules??[])}}function si(t){return{rules:Array.from(t.rules.entries())}}function ea(){return{create:Ku,addRule:Hu,updateRule:Gu,removeRule:Yu,getRule:Ju,getAllRules:Xu,getMatchingRules:ni,load:ri,save:si}}});var ci=b(Bn=>{"use strict";Object.defineProperty(Bn,"__esModule",{value:!0});Bn.create=ua;var xt=$e(),ta=wn(),ii=ys(),Et=te(),na=mt(),ra=V(),sa=Un(),oi=It(),ia=Fn(),At=j(),oa=R();function ca(t){let e={formatElapsedTime:xt.formatElapsedTime,getDocumentIndexId:xt.getDocumentIndexId,getDocumentProperties:xt.getDocumentProperties,validateSchema:xt.validateSchema};for(let n of Et.FUNCTION_COMPONENTS){let r=n;if(t[r]){if(typeof t[r]!="function")throw(0,At.createError)("COMPONENT_MUST_BE_FUNCTION",r)}else t[r]=e[r]}for(let n of Object.keys(t))if(!Et.OBJECT_COMPONENTS.includes(n)&&!Et.FUNCTION_COMPONENTS.includes(n))throw(0,At.createError)("UNSUPPORTED_COMPONENT",n)}function ua({schema:t,sort:e,language:n,components:r,id:s,plugins:i}){r||(r={});for(let w of i??[]){if(!("getComponents"in w)||typeof w.getComponents!="function")continue;let S=w.getComponents(t),_=Object.keys(S);for(let I of _)if(r[I])throw(0,At.createError)("PLUGIN_COMPONENT_CONFLICT",I,w.name);r={...r,...S}}s||(s=(0,oa.uniqueId)());let o=r.tokenizer,c=r.index,u=r.documentsStore,a=r.sorter,l=r.pinning;if(o?o.tokenize?o=o:o=(0,oi.createTokenizer)(o):o=(0,oi.createTokenizer)({language:n??"english"}),r.tokenizer&&n)throw(0,At.createError)("NO_LANGUAGE_WITH_CUSTOM_TOKENIZER");let d=(0,ra.createInternalDocumentIDStore)();c||=(0,na.createIndex)(),a||=(0,sa.createSorter)(),u||=(0,ta.createDocumentsStore)(),l||=(0,ia.createPinning)(),ca(r);let{getDocumentProperties:f,getDocumentIndexId:p,validateSchema:g,formatElapsedTime:y}=r,h={data:{},caches:{},schema:t,tokenizer:o,index:c,sorter:a,documentsStore:u,pinning:l,internalDocumentIDStore:d,getDocumentProperties:f,getDocumentIndexId:p,validateSchema:g,beforeInsert:[],afterInsert:[],beforeRemove:[],afterRemove:[],beforeUpdate:[],afterUpdate:[],beforeUpsert:[],afterUpsert:[],beforeSearch:[],afterSearch:[],beforeInsertMultiple:[],afterInsertMultiple:[],beforeRemoveMultiple:[],afterRemoveMultiple:[],beforeUpdateMultiple:[],afterUpdateMultiple:[],beforeUpsertMultiple:[],afterUpsertMultiple:[],afterCreate:[],formatElapsedTime:y,id:s,plugins:i,version:aa()};h.data={index:h.index.create(h,d,t),docs:h.documentsStore.create(h,d),sorting:h.sorter.create(h,d,t,e),pinning:h.pinning.create(d)};for(let w of ii.AVAILABLE_PLUGIN_HOOKS)h[w]=(h[w]??[]).concat((0,ii.getAllPluginsByHook)(h,w));let m=h.afterCreate;return m&&(0,Et.runAfterCreate)(m,h),h}function aa(){return"{{VERSION}}"}});var $n=b(vt=>{"use strict";Object.defineProperty(vt,"__esModule",{value:!0});vt.getByID=la;vt.count=da;function la(t,e){return t.documentsStore.get(t.data.docs,e)}function da(t){return t.documentsStore.count(t.data.docs)}});var qn=b(U=>{"use strict";var ui=U&&U.__createBinding||(Object.create?(function(t,e,n,r){r===void 0&&(r=n);var s=Object.getOwnPropertyDescriptor(e,n);(!s||("get"in s?!e.__esModule:s.writable||s.configurable))&&(s={enumerable:!0,get:function(){return e[n]}}),Object.defineProperty(t,r,s)}):(function(t,e,n,r){r===void 0&&(r=n),t[r]=e[n]})),fa=U&&U.__setModuleDefault||(Object.create?(function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}):function(t,e){t.default=e}),ha=U&&U.__exportStar||function(t,e){for(var n in t)n!=="default"&&!Object.prototype.hasOwnProperty.call(e,n)&&ui(e,t,n)},Je=U&&U.__importStar||(function(){var t=function(e){return t=Object.getOwnPropertyNames||function(n){var r=[];for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(r[r.length]=s);return r},t(e)};return function(e){if(e&&e.__esModule)return e;var n={};if(e!=null)for(var r=t(e),s=0;s{"use strict";Object.defineProperty(Xe,"__esModule",{value:!0});Xe.insert=Wn;Xe.insertMultiple=Sa;Xe.innerInsertMultiple=ba;var zn=qn(),F=R(),Ue=te(),Re=j(),Vn=V();function Wn(t,e,n,r,s){let i=t.validateSchema(e,t.schema);if(i)throw(0,Re.createError)("SCHEMA_VALIDATION_FAILURE",i);return(0,F.isAsyncFunction)(t.beforeInsert)||(0,F.isAsyncFunction)(t.afterInsert)||(0,F.isAsyncFunction)(t.index.beforeInsert)||(0,F.isAsyncFunction)(t.index.insert)||(0,F.isAsyncFunction)(t.index.afterInsert)?ya(t,e,n,r,s):ma(t,e,n,r,s)}var pa=new Set(["enum","enum[]"]),ga=new Set(["string","number"]);async function ya(t,e,n,r,s){let{index:i,docs:o}=t.data,c=t.getDocumentIndexId(e);if(typeof c!="string")throw(0,Re.createError)("DOCUMENT_ID_MUST_BE_STRING",typeof c);let u=(0,Vn.getInternalDocumentId)(t.internalDocumentIDStore,c);if(r||await(0,Ue.runSingleHook)(t.beforeInsert,t,c,e),!t.documentsStore.store(o,c,u,e))throw(0,Re.createError)("DOCUMENT_ALREADY_EXISTS",c);let a=t.documentsStore.count(o),l=t.index.getSearchableProperties(i),d=t.index.getSearchablePropertiesWithTypes(i),f=t.getDocumentProperties(e,l);for(let[p,g]of Object.entries(f)){if(typeof g>"u")continue;let y=typeof g,h=d[p];ai(y,h,p,g)}return await wa(t,c,l,f,a,n,e,s),r||await(0,Ue.runSingleHook)(t.afterInsert,t,c,e),c}function ma(t,e,n,r,s){let{index:i,docs:o}=t.data,c=t.getDocumentIndexId(e);if(typeof c!="string")throw(0,Re.createError)("DOCUMENT_ID_MUST_BE_STRING",typeof c);let u=(0,Vn.getInternalDocumentId)(t.internalDocumentIDStore,c);if(r||(0,Ue.runSingleHook)(t.beforeInsert,t,c,e),!t.documentsStore.store(o,c,u,e))throw(0,Re.createError)("DOCUMENT_ALREADY_EXISTS",c);let a=t.documentsStore.count(o),l=t.index.getSearchableProperties(i),d=t.index.getSearchablePropertiesWithTypes(i),f=t.getDocumentProperties(e,l);for(let[p,g]of Object.entries(f)){if(typeof g>"u")continue;let y=typeof g,h=d[p];ai(y,h,p,g)}return _a(t,c,l,f,a,n,e,s),r||(0,Ue.runSingleHook)(t.afterInsert,t,c,e),c}function ai(t,e,n,r){if(!((0,zn.isGeoPointType)(e)&&typeof r=="object"&&typeof r.lon=="number"&&typeof r.lat=="number")&&!((0,zn.isVectorType)(e)&&Array.isArray(r))&&!((0,zn.isArrayType)(e)&&Array.isArray(r))&&!(pa.has(e)&&ga.has(t))&&t!==e)throw(0,Re.createError)("INVALID_DOCUMENT_PROPERTY",n,e,t)}async function wa(t,e,n,r,s,i,o,c){for(let l of n){let d=r[l];if(typeof d>"u")continue;let f=t.index.getSearchablePropertiesWithTypes(t.data.index)[l];await t.index.beforeInsert?.(t.data.index,l,e,d,f,i,t.tokenizer,s);let p=t.internalDocumentIDStore.idToInternalId.get(e);await t.index.insert(t.index,t.data.index,l,e,p,d,f,i,t.tokenizer,s,c),await t.index.afterInsert?.(t.data.index,l,e,d,f,i,t.tokenizer,s)}let u=t.sorter.getSortableProperties(t.data.sorting),a=t.getDocumentProperties(o,u);for(let l of u){let d=a[l];if(typeof d>"u")continue;let f=t.sorter.getSortablePropertiesWithTypes(t.data.sorting)[l];t.sorter.insert(t.data.sorting,l,e,d,f,i)}}function _a(t,e,n,r,s,i,o,c){for(let l of n){let d=r[l];if(typeof d>"u")continue;let f=t.index.getSearchablePropertiesWithTypes(t.data.index)[l],p=(0,Vn.getInternalDocumentId)(t.internalDocumentIDStore,e);t.index.beforeInsert?.(t.data.index,l,e,d,f,i,t.tokenizer,s),t.index.insert(t.index,t.data.index,l,e,p,d,f,i,t.tokenizer,s,c),t.index.afterInsert?.(t.data.index,l,e,d,f,i,t.tokenizer,s)}let u=t.sorter.getSortableProperties(t.data.sorting),a=t.getDocumentProperties(o,u);for(let l of u){let d=a[l];if(typeof d>"u")continue;let f=t.sorter.getSortablePropertiesWithTypes(t.data.sorting)[l];t.sorter.insert(t.data.sorting,l,e,d,f,i)}}function Sa(t,e,n,r,s,i){return(0,F.isAsyncFunction)(t.afterInsertMultiple)||(0,F.isAsyncFunction)(t.beforeInsertMultiple)||(0,F.isAsyncFunction)(t.index.beforeInsert)||(0,F.isAsyncFunction)(t.index.insert)||(0,F.isAsyncFunction)(t.index.afterInsert)?li(t,e,n,r,s,i):di(t,e,n,r,s,i)}async function li(t,e,n=1e3,r,s,i=0){let o=[],c=async a=>{let l=Math.min(a+n,e.length),d=e.slice(a,l);for(let f of d){let p={avlRebalanceThreshold:d.length},g=await Wn(t,f,r,s,p);o.push(g)}return l};return await(async()=>{let a=0;for(;a0){let d=Date.now()-l,f=i-d;f>0&&(0,F.sleep)(f)}}})(),s||await(0,Ue.runMultipleHook)(t.afterInsertMultiple,t,e),o}function di(t,e,n=1e3,r,s,i=0){let o=[],c=0;function u(){let l=e.slice(c*n,(c+1)*n);if(l.length===0)return!1;for(let d of l){let f={avlRebalanceThreshold:l.length},p=Wn(t,d,r,s,f);o.push(p)}return c++,!0}function a(){let l=Date.now();for(;u();)if(i>0){let f=Date.now()-l;if(f>=i){let p=i-f%i;p>0&&(0,F.sleep)(p)}}}return a(),s||(0,Ue.runMultipleHook)(t.afterInsertMultiple,t,e),o}function ba(t,e,n,r,s,i){return(0,F.isAsyncFunction)(t.beforeInsert)||(0,F.isAsyncFunction)(t.afterInsert)||(0,F.isAsyncFunction)(t.index.beforeInsert)||(0,F.isAsyncFunction)(t.index.insert)||(0,F.isAsyncFunction)(t.index.afterInsert)?li(t,e,n,r,s,i):di(t,e,n,r,s,i)}});var fi=b(Ae=>{"use strict";Object.defineProperty(Ae,"__esModule",{value:!0});Ae.insertPin=Ia;Ae.updatePin=xa;Ae.deletePin=Ea;Ae.getPin=Aa;Ae.getAllPins=va;function Ia(t,e){t.pinning.addRule(t.data.pinning,e)}function xa(t,e){t.pinning.updateRule(t.data.pinning,e)}function Ea(t,e){return t.pinning.removeRule(t.data.pinning,e)}function Aa(t,e){return t.pinning.getRule(t.data.pinning,e)}function va(t){return t.pinning.getAllRules(t.data.pinning)}});var Hn=b(Dt=>{"use strict";Object.defineProperty(Dt,"__esModule",{value:!0});Dt.remove=Kn;Dt.removeMultiple=Ma;var fe=te(),he=V(),de=R();function Kn(t,e,n,r){return(0,de.isAsyncFunction)(t.index.beforeRemove)||(0,de.isAsyncFunction)(t.index.remove)||(0,de.isAsyncFunction)(t.index.afterRemove)?Ta(t,e,n,r):Da(t,e,n,r)}async function Ta(t,e,n,r){let s=!0,{index:i,docs:o}=t.data,c=t.documentsStore.get(o,e);if(!c)return!1;let u=(0,he.getInternalDocumentId)(t.internalDocumentIDStore,e),a=(0,he.getDocumentIdFromInternalId)(t.internalDocumentIDStore,u),l=t.documentsStore.count(o);r||await(0,fe.runSingleHook)(t.beforeRemove,t,a);let d=t.index.getSearchableProperties(i),f=t.index.getSearchablePropertiesWithTypes(i),p=t.getDocumentProperties(c,d);for(let h of d){let m=p[h];if(typeof m>"u")continue;let w=f[h];await t.index.beforeRemove?.(t.data.index,h,a,m,w,n,t.tokenizer,l),await t.index.remove(t.index,t.data.index,h,e,u,m,w,n,t.tokenizer,l)||(s=!1),await t.index.afterRemove?.(t.data.index,h,a,m,w,n,t.tokenizer,l)}let g=await t.sorter.getSortableProperties(t.data.sorting),y=await t.getDocumentProperties(c,g);for(let h of g)typeof y[h]>"u"||t.sorter.remove(t.data.sorting,h,e);return r||await(0,fe.runSingleHook)(t.afterRemove,t,a),t.documentsStore.remove(t.data.docs,e,u),s}function Da(t,e,n,r){let s=!0,{index:i,docs:o}=t.data,c=t.documentsStore.get(o,e);if(!c)return!1;let u=(0,he.getInternalDocumentId)(t.internalDocumentIDStore,e),a=(0,he.getDocumentIdFromInternalId)(t.internalDocumentIDStore,u),l=t.documentsStore.count(o);r||(0,fe.runSingleHook)(t.beforeRemove,t,a);let d=t.index.getSearchableProperties(i),f=t.index.getSearchablePropertiesWithTypes(i),p=t.getDocumentProperties(c,d);for(let h of d){let m=p[h];if(typeof m>"u")continue;let w=f[h];t.index.beforeRemove?.(t.data.index,h,a,m,w,n,t.tokenizer,l),t.index.remove(t.index,t.data.index,h,e,u,m,w,n,t.tokenizer,l)||(s=!1),t.index.afterRemove?.(t.data.index,h,a,m,w,n,t.tokenizer,l)}let g=t.sorter.getSortableProperties(t.data.sorting),y=t.getDocumentProperties(c,g);for(let h of g)typeof y[h]>"u"||t.sorter.remove(t.data.sorting,h,e);return r||(0,fe.runSingleHook)(t.afterRemove,t,a),t.documentsStore.remove(t.data.docs,e,u),s}function Ma(t,e,n,r,s){return(0,de.isAsyncFunction)(t.index.beforeRemove)||(0,de.isAsyncFunction)(t.index.remove)||(0,de.isAsyncFunction)(t.index.afterRemove)||(0,de.isAsyncFunction)(t.beforeRemoveMultiple)||(0,de.isAsyncFunction)(t.afterRemoveMultiple)?Oa(t,e,n,r,s):Pa(t,e,n,r,s)}async function Oa(t,e,n,r,s){let i=0;n||(n=1e3);let o=s?[]:e.map(c=>(0,he.getDocumentIdFromInternalId)(t.internalDocumentIDStore,(0,he.getInternalDocumentId)(t.internalDocumentIDStore,c)));return s||await(0,fe.runMultipleHook)(t.beforeRemoveMultiple,t,o),await new Promise((c,u)=>{let a=0;async function l(){let d=e.slice(a*n,++a*n);if(!d.length)return c();for(let f of d)try{await Kn(t,f,r,s)&&i++}catch(p){u(p)}setTimeout(l,0)}setTimeout(l,0)}),s||await(0,fe.runMultipleHook)(t.afterRemoveMultiple,t,o),i}function Pa(t,e,n,r,s){let i=0;n||(n=1e3);let o=s?[]:e.map(a=>(0,he.getDocumentIdFromInternalId)(t.internalDocumentIDStore,(0,he.getInternalDocumentId)(t.internalDocumentIDStore,a)));s||(0,fe.runMultipleHook)(t.beforeRemoveMultiple,t,o);let c=0;function u(){let a=e.slice(c*n,++c*n);if(a.length){for(let l of a)Kn(t,l,r,s)&&i++;setTimeout(u,0)}}return u(),s||(0,fe.runMultipleHook)(t.afterRemoveMultiple,t,o),i}});var Gn=b(pe=>{"use strict";Object.defineProperty(pe,"__esModule",{value:!0});pe.MODE_VECTOR_SEARCH=pe.MODE_HYBRID_SEARCH=pe.MODE_FULLTEXT_SEARCH=void 0;pe.MODE_FULLTEXT_SEARCH="fulltext";pe.MODE_HYBRID_SEARCH="hybrid";pe.MODE_VECTOR_SEARCH="vector"});var Mt=b(Yn=>{"use strict";Object.defineProperty(Yn,"__esModule",{value:!0});Yn.getFacets=ja;var ka=j(),Na=R();function Ua(t,e){return t[1]-e[1]}function Ra(t,e){return e[1]-t[1]}function La(t="desc"){return t.toLowerCase()==="asc"?Ua:Ra}function ja(t,e,n){let r={},s=e.map(([a])=>a),i=t.documentsStore.getMultiple(t.data.docs,s),o=Object.keys(n),c=t.index.getSearchablePropertiesWithTypes(t.data.index);for(let a of o){let l;if(c[a]==="number"){let{ranges:d}=n[a],f=d.length,p=Array.from({length:f});for(let g=0;g{for(let s of t){let i=`${s.from}-${s.to}`;n?.has(i)||r>=s.from&&r<=s.to&&(e[i]===void 0?e[i]=1:(e[i]++,n?.add(i)))}}}function pi(t,e,n){let r=e==="boolean"?"false":"";return s=>{let i=s?.toString()??r;n?.has(i)||(t[i]=(t[i]??0)+1,n?.add(i))}}});var Ot=b(Xn=>{"use strict";Object.defineProperty(Xn,"__esModule",{value:!0});Xn.getGroups=Ba;var gi=j(),Jn=R(),Ca=V(),Fa={reducer:(t,e,n,r)=>(e[r]=n,e),getInitialValue:t=>Array.from({length:t})},yi=["string","number","boolean"];function Ba(t,e,n){let r=n.properties,s=r.length,i=t.index.getSearchablePropertiesWithTypes(t.data.index);for(let m=0;m"u")throw(0,gi.createError)("UNKNOWN_GROUP_BY_PROPERTY",w);if(!yi.includes(i[w]))throw(0,gi.createError)("INVALID_GROUP_BY_PROPERTY",w,yi.join(", "),i[w])}let o=e.map(([m])=>(0,Ca.getDocumentIdFromInternalId)(t.internalDocumentIDStore,m)),c=t.documentsStore.getMultiple(t.data.docs,o),u=c.length,a=n.maxResult||Number.MAX_SAFE_INTEGER,l=[],d={};for(let m=0;m"u")continue;let A=typeof D!="boolean"?D:""+D,k=S.perValue[A]??{indexes:[],count:0};k.count>=a||(k.indexes.push(I),k.count++,S.perValue[A]=k,_.add(D))}l.push(Array.from(_)),d[w]=S}let f=mi(l),p=f.length,g=[];for(let m=0;mv-D),_.indexes.length!==0&&g.push(_)}let y=g.length,h=Array.from({length:y});for(let m=0;m({id:o[A],score:e[A][1],document:c[A]})),I=S.reducer.bind(null,w.values),v=S.getInitialValue(w.indexes.length),D=_.reduce(I,v);h[m]={values:w.values,result:D}}return h}function mi(t,e=0){if(e+1===t.length)return t[e].map(i=>[i]);let n=t[e],r=mi(t,e+1),s=[];for(let i of n)for(let o of r){let c=[i];(0,Jn.safeArrayPush)(c,o),s.push(c)}return s}});var Pt=b(Zn=>{"use strict";Object.defineProperty(Zn,"__esModule",{value:!0});Zn.applyPinningRules=za;var $a=V(),qa=Fn();function za(t,e,n,r){let s=(0,qa.getMatchingRules)(e,r);if(s.length===0)return n;let i=s.flatMap(h=>h.consequence.promote);i.sort((h,m)=>h.position-m.position);let o=new Set,c=new Map,u=new Set;for(let h of i){let m=(0,$a.getInternalDocumentId)(t.internalDocumentIDStore,h.doc_id);if(m!==void 0){if(c.has(m)){let w=c.get(m);h.position!o.has(h)),l=1e6,d=[];for(let[h,m]of c.entries())n.find(([S])=>S===h)?d.push([h,l-m]):t.documentsStore.get(t.data.docs,h)&&d.push([h,0]);d.sort((h,m)=>{let w=c.get(h[0])??1/0,S=c.get(m[0])??1/0;return w-S});let f=[],p=new Map;for(let h of d){let m=c.get(h[0]);p.set(m,h)}let g=0,y=0;for(;y=f.length&&f.push(m);return f}});var Qn=b(re=>{"use strict";Object.defineProperty(re,"__esModule",{value:!0});re.defaultBM25Params=void 0;re.innerFullTextSearch=Si;re.fullTextSearch=Qa;var Va=Mt(),Wa=Ot(),wi=te(),Ka=V(),Ha=mt(),Ga=Pt(),Ya=j(),kt=R(),Ja=$n(),_i=Ze();function Si(t,e,n){let{term:r,properties:s}=e,i=t.data.index,o=t.caches.propertiesToSearch;if(!o){let d=t.index.getSearchablePropertiesWithTypes(i);o=t.index.getSearchableProperties(i),o=o.filter(f=>d[f].startsWith("string")),t.caches.propertiesToSearch=o}if(s&&s!=="*"){for(let d of s)if(!o.includes(d))throw(0,Ya.createError)("UNKNOWN_INDEX",d,o.join(", "));o=o.filter(d=>s.includes(d))}let c=Object.keys(e.where??{}).length>0,u;c&&(u=t.index.searchByWhereClause(i,t.tokenizer,e.where,n));let a,l=e.threshold!==void 0&&e.threshold!==null?e.threshold:1;if(r||s){let d=(0,Ja.count)(t);if(a=t.index.search(i,r||"",t.tokenizer,n,o,e.exact||!1,e.tolerance||0,e.boost||{},el(e.relevance),d,u,l),e.exact&&r){let f=r.trim().split(/\s+/);a=a.filter(([p])=>{let g=t.documentsStore.get(t.data.docs,p);if(!g)return!1;for(let y of o){let h=Za(g,y);if(typeof h=="string"&&f.every(w=>new RegExp(`\\b${Xa(w)}\\b`).test(h)))return!0}return!1})}}else if(c){let d=(0,Ha.searchByGeoWhereClause)(i,e.where);d?a=d:a=(u?Array.from(u):[]).map(p=>[+p,0])}else a=Object.keys(t.documentsStore.getAll(t.data.docs)).map(f=>[+f,0]);return a}function Xa(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function Za(t,e){let n=e.split("."),r=t;for(let s of n)if(r&&typeof r=="object"&&s in r)r=r[s];else return;return r}function Qa(t,e,n){let r=(0,kt.getNanosecondsTime)();function s(){let c=Object.keys(t.data.index.vectorIndexes),u=e.facets&&Object.keys(e.facets).length>0,{limit:a=10,offset:l=0,distinctOn:d,includeVectors:f=!1}=e,p=e.preflight===!0,g=Si(t,e,n);if(e.sortBy)if(typeof e.sortBy=="function"){let m=g.map(([_])=>_),S=t.documentsStore.getMultiple(t.data.docs,m).map((_,I)=>[g[I][0],g[I][1],_]);S.sort(e.sortBy),g=S.map(([_,I])=>[_,I])}else g=t.sorter.sortBy(t.data.sorting,g,e.sortBy).map(([m,w])=>[(0,Ka.getInternalDocumentId)(t.internalDocumentIDStore,m),w]);else g=g.sort(kt.sortTokenScorePredicate);g=(0,Ga.applyPinningRules)(t,t.data.pinning,g,e.term);let y;p||(y=d?(0,_i.fetchDocumentsWithDistinct)(t,g,l,a,d):(0,_i.fetchDocuments)(t,g,l,a));let h={elapsed:{formatted:"",raw:0},hits:[],count:g.length};if(typeof y<"u"&&(h.hits=y.filter(Boolean),f||(0,kt.removeVectorsFromHits)(h,c)),u){let m=(0,Va.getFacets)(t,g,e.facets);h.facets=m}return e.groupBy&&(h.groups=(0,Wa.getGroups)(t,g,e.groupBy)),h.elapsed=t.formatElapsedTime((0,kt.getNanosecondsTime)()-r),h}async function i(){t.beforeSearch&&await(0,wi.runBeforeSearch)(t.beforeSearch,t,e,n);let c=s();return t.afterSearch&&await(0,wi.runAfterSearch)(t.afterSearch,t,e,n,c),c}return t.beforeSearch?.length||t.afterSearch?.length?i():s()}re.defaultBM25Params={k:1.2,b:.75,d:.5};function el(t){let e=t??{};return e.k=e.k??re.defaultBM25Params.k,e.b=e.b??re.defaultBM25Params.b,e.d=e.d??re.defaultBM25Params.d,e}});var Lt=b(Rt=>{"use strict";Object.defineProperty(Rt,"__esModule",{value:!0});Rt.innerVectorSearch=Ii;Rt.searchVector=ol;var Nt=R(),tl=Mt(),Ut=j(),nl=Ot(),rl=V(),bi=te(),sl=Dn(),il=Pt();function Ii(t,e,n){let r=e.vector;if(r&&(!("value"in r)||!("property"in r)))throw(0,Ut.createError)("INVALID_VECTOR_INPUT",Object.keys(r).join(", "));let s=t.data.index.vectorIndexes[r.property];if(!s)throw(0,Ut.createError)("UNKNOWN_VECTOR_PROPERTY",r.property);let i=s.node.size;if(r?.value.length!==i)throw r?.property===void 0||r?.value.length===void 0?(0,Ut.createError)("INVALID_INPUT_VECTOR","undefined",i,"undefined"):(0,Ut.createError)("INVALID_INPUT_VECTOR",r.property,i,r.value.length);let o=t.data.index,c;return Object.keys(e.where??{}).length>0&&(c=t.index.searchByWhereClause(o,t.tokenizer,e.where,n)),s.node.find(r.value,e.similarity??sl.DEFAULT_SIMILARITY,c)}function ol(t,e,n="english"){let r=(0,Nt.getNanosecondsTime)();function s(){let c=Ii(t,e,n).sort(Nt.sortTokenScorePredicate);c=(0,il.applyPinningRules)(t,t.data.pinning,c,void 0);let u=[];e.facets&&Object.keys(e.facets).length>0&&(u=(0,tl.getFacets)(t,c,e.facets));let l=e.vector.property,d=e.includeVectors??!1,f=e.limit??10,p=e.offset??0,g=Array.from({length:f});for(let w=0;w{"use strict";Object.defineProperty(Ct,"__esModule",{value:!0});Ct.innerHybridSearch=Ai;Ct.hybridSearch=hl;var jt=R(),cl=Mt(),ul=Ot(),al=Ze(),ll=Qn(),dl=Lt(),xi=te(),fl=Pt();function Ai(t,e,n){let r=pl((0,ll.innerFullTextSearch)(t,e,n)),s=(0,dl.innerVectorSearch)(t,e,n),i=e.hybridWeights;return yl(r,s,e.term??"",i)}function hl(t,e,n){let r=(0,jt.getNanosecondsTime)();function s(){let c=Ai(t,e,n);c=(0,fl.applyPinningRules)(t,t.data.pinning,c,e.term);let u;e.facets&&Object.keys(e.facets).length>0&&(u=(0,cl.getFacets)(t,c,e.facets));let l;e.groupBy&&(l=(0,ul.getGroups)(t,c,e.groupBy));let d=e.offset??0,f=e.limit??10,p=(0,al.fetchDocuments)(t,c,d,f).filter(Boolean),g=(0,jt.getNanosecondsTime)(),y={count:c.length,elapsed:{raw:Number(g-r),formatted:(0,jt.formatNanoseconds)(g-r)},hits:p,...u?{facets:u}:{},...l?{groups:l}:{}};if(!(e.includeVectors??!1)){let m=Object.keys(t.data.index.vectorIndexes);(0,jt.removeVectorsFromHits)(y,m)}return y}async function i(){t.beforeSearch&&await(0,xi.runBeforeSearch)(t.beforeSearch,t,e,n);let c=s();return t.afterSearch&&await(0,xi.runAfterSearch)(t.afterSearch,t,e,n,c),c}return t.beforeSearch?.length||t.afterSearch?.length?i():s()}function er(t){return t[1]}function pl(t){let e=Math.max.apply(Math,t.map(er));return t.map(([n,r])=>[n,r/e])}function Ei(t,e){return t/e}function gl(t,e){return(n,r)=>n*t+r*e}function yl(t,e,n,r){let s=Math.max.apply(Math,t.map(er)),i=Math.max.apply(Math,e.map(er)),o=r&&r.text&&r.vector,{text:c,vector:u}=o?r:ml(n),a=new Map,l=t.length,d=gl(c,u);for(let p=0;pg[1]-p[1])}function ml(t){return{text:.5,vector:.5}}});var Ze=b(Qe=>{"use strict";Object.defineProperty(Qe,"__esModule",{value:!0});Qe.search=xl;Qe.fetchDocumentsWithDistinct=El;Qe.fetchDocuments=Al;var Ti=V(),wl=j(),_l=R(),Ft=Gn(),Sl=Qn(),bl=Lt(),Il=vi();function xl(t,e,n){let r=e.mode??Ft.MODE_FULLTEXT_SEARCH;if(r===Ft.MODE_FULLTEXT_SEARCH)return(0,Sl.fullTextSearch)(t,e,n);if(r===Ft.MODE_VECTOR_SEARCH)return(0,bl.searchVector)(t,e);if(r===Ft.MODE_HYBRID_SEARCH)return(0,Il.hybridSearch)(t,e);throw(0,wl.createError)("INVALID_SEARCH_MODE",r)}function El(t,e,n,r,s){let i=t.data.docs,o=new Map,c=[],u=new Set,a=e.length,l=0;for(let d=0;d"u")continue;let[p,g]=f;if(u.has(p))continue;let y=t.documentsStore.get(i,p),h=(0,_l.getNested)(y,s);if(!(typeof h>"u"||o.has(h))&&(o.set(h,!0),l++,!(l<=n)&&(c.push({id:(0,Ti.getDocumentIdFromInternalId)(t.internalDocumentIDStore,p),score:g,document:y}),u.add(p),l>=n+r)))break}return c}function Al(t,e,n,r){let s=t.data.docs,i=Array.from({length:r}),o=new Set;for(let c=n;c"u")break;let[a,l]=u;if(!o.has(a)){let d=t.documentsStore.get(s,a);i[c]={id:(0,Ti.getDocumentIdFromInternalId)(t.internalDocumentIDStore,a),score:l,document:d},o.add(a)}}return i}});var Di=b(Bt=>{"use strict";Object.defineProperty(Bt,"__esModule",{value:!0});Bt.load=vl;Bt.save=Tl;function vl(t,e){t.internalDocumentIDStore.load(t,e.internalDocumentIDStore),t.data.index=t.index.load(t.internalDocumentIDStore,e.index),t.data.docs=t.documentsStore.load(t.internalDocumentIDStore,e.docs),t.data.sorting=t.sorter.load(t.internalDocumentIDStore,e.sorting),t.data.pinning=t.pinning.load(t.internalDocumentIDStore,e.pinning),t.tokenizer.language=e.language}function Tl(t){return{internalDocumentIDStore:t.internalDocumentIDStore.save(t.internalDocumentIDStore),index:t.index.save(t.data.index),docs:t.documentsStore.save(t.data.docs),sorting:t.sorter.save(t.data.sorting),pinning:t.pinning.save(t.data.pinning),language:t.tokenizer.language}}});var tr=b(zt=>{"use strict";Object.defineProperty(zt,"__esModule",{value:!0});zt.update=Dl;zt.updateMultiple=Pl;var ge=te(),Mi=j(),$t=Tt(),qt=Hn(),C=R();function Dl(t,e,n,r,s){return(0,C.isAsyncFunction)(t.afterInsert)||(0,C.isAsyncFunction)(t.beforeInsert)||(0,C.isAsyncFunction)(t.afterRemove)||(0,C.isAsyncFunction)(t.beforeRemove)||(0,C.isAsyncFunction)(t.beforeUpdate)||(0,C.isAsyncFunction)(t.afterUpdate)?Ml(t,e,n,r,s):Ol(t,e,n,r,s)}async function Ml(t,e,n,r,s){!s&&t.beforeUpdate&&await(0,ge.runSingleHook)(t.beforeUpdate,t,e),await(0,qt.remove)(t,e,r,s);let i=await(0,$t.insert)(t,n,r,s);return!s&&t.afterUpdate&&await(0,ge.runSingleHook)(t.afterUpdate,t,i),i}function Ol(t,e,n,r,s){!s&&t.beforeUpdate&&(0,ge.runSingleHook)(t.beforeUpdate,t,e),(0,qt.remove)(t,e,r,s);let i=(0,$t.insert)(t,n,r,s);return!s&&t.afterUpdate&&(0,ge.runSingleHook)(t.afterUpdate,t,i),i}function Pl(t,e,n,r,s,i){return(0,C.isAsyncFunction)(t.afterInsert)||(0,C.isAsyncFunction)(t.beforeInsert)||(0,C.isAsyncFunction)(t.afterRemove)||(0,C.isAsyncFunction)(t.beforeRemove)||(0,C.isAsyncFunction)(t.beforeUpdate)||(0,C.isAsyncFunction)(t.afterUpdate)||(0,C.isAsyncFunction)(t.beforeUpdateMultiple)||(0,C.isAsyncFunction)(t.afterUpdateMultiple)||(0,C.isAsyncFunction)(t.beforeRemoveMultiple)||(0,C.isAsyncFunction)(t.afterRemoveMultiple)||(0,C.isAsyncFunction)(t.beforeInsertMultiple)||(0,C.isAsyncFunction)(t.afterInsertMultiple)?kl(t,e,n,r,s,i):Nl(t,e,n,r,s,i)}async function kl(t,e,n,r,s,i){i||await(0,ge.runMultipleHook)(t.beforeUpdateMultiple,t,e);let o=n.length;for(let u=0;u{"use strict";Object.defineProperty(Kt,"__esModule",{value:!0});Kt.upsert=Ul;Kt.upsertMultiple=jl;var ye=te(),Le=j(),Vt=Tt(),Wt=tr(),P=R();function Ul(t,e,n,r,s){return(0,P.isAsyncFunction)(t.afterInsert)||(0,P.isAsyncFunction)(t.beforeInsert)||(0,P.isAsyncFunction)(t.afterRemove)||(0,P.isAsyncFunction)(t.beforeRemove)||(0,P.isAsyncFunction)(t.beforeUpdate)||(0,P.isAsyncFunction)(t.afterUpdate)||(0,P.isAsyncFunction)(t.beforeUpsert)||(0,P.isAsyncFunction)(t.afterUpsert)||(0,P.isAsyncFunction)(t.index.beforeInsert)||(0,P.isAsyncFunction)(t.index.insert)||(0,P.isAsyncFunction)(t.index.afterInsert)?Rl(t,e,n,r,s):Ll(t,e,n,r,s)}async function Rl(t,e,n,r,s){let i=t.getDocumentIndexId(e);if(typeof i!="string")throw(0,Le.createError)("DOCUMENT_ID_MUST_BE_STRING",typeof i);!r&&t.beforeUpsert&&await(0,ye.runSingleHook)(t.beforeUpsert,t,i,e);let o=t.documentsStore.get(t.data.docs,i),c;return o?c=await(0,Wt.update)(t,i,e,n,r):c=await(0,Vt.insert)(t,e,n,r,s),!r&&t.afterUpsert&&await(0,ye.runSingleHook)(t.afterUpsert,t,c,e),c}function Ll(t,e,n,r,s){let i=t.getDocumentIndexId(e);if(typeof i!="string")throw(0,Le.createError)("DOCUMENT_ID_MUST_BE_STRING",typeof i);!r&&t.beforeUpsert&&(0,ye.runSingleHook)(t.beforeUpsert,t,i,e);let o=t.documentsStore.get(t.data.docs,i),c;return o?c=(0,Wt.update)(t,i,e,n,r):c=(0,Vt.insert)(t,e,n,r,s),!r&&t.afterUpsert&&(0,ye.runSingleHook)(t.afterUpsert,t,c,e),c}function jl(t,e,n,r,s){return(0,P.isAsyncFunction)(t.afterInsert)||(0,P.isAsyncFunction)(t.beforeInsert)||(0,P.isAsyncFunction)(t.afterRemove)||(0,P.isAsyncFunction)(t.beforeRemove)||(0,P.isAsyncFunction)(t.beforeUpdate)||(0,P.isAsyncFunction)(t.afterUpdate)||(0,P.isAsyncFunction)(t.beforeUpsert)||(0,P.isAsyncFunction)(t.afterUpsert)||(0,P.isAsyncFunction)(t.beforeUpsertMultiple)||(0,P.isAsyncFunction)(t.afterUpsertMultiple)||(0,P.isAsyncFunction)(t.beforeInsertMultiple)||(0,P.isAsyncFunction)(t.afterInsertMultiple)||(0,P.isAsyncFunction)(t.beforeUpdateMultiple)||(0,P.isAsyncFunction)(t.afterUpdateMultiple)||(0,P.isAsyncFunction)(t.beforeRemoveMultiple)||(0,P.isAsyncFunction)(t.afterRemoveMultiple)||(0,P.isAsyncFunction)(t.index.beforeInsert)||(0,P.isAsyncFunction)(t.index.insert)||(0,P.isAsyncFunction)(t.index.afterInsert)?Cl(t,e,n,r,s):Fl(t,e,n,r,s)}async function Cl(t,e,n,r,s){!s&&t.beforeUpsertMultiple&&await(0,ye.runMultipleHook)(t.beforeUpsertMultiple,t,e);let i=e.length;for(let l=0;l0){let l=await(0,Wt.updateMultiple)(t,u,c,n,r,s);a.push(...l)}if(o.length>0){let l=await(0,Vt.innerInsertMultiple)(t,o,n,r,s);a.push(...l)}return!s&&t.afterUpsertMultiple&&await(0,ye.runMultipleHook)(t.afterUpsertMultiple,t,a),a}function Fl(t,e,n,r,s){!s&&t.beforeUpsertMultiple&&(0,ye.runMultipleHook)(t.beforeUpsertMultiple,t,e);let i=e.length;for(let l=0;l0){let l=(0,Wt.updateMultiple)(t,u,c,n,r,s);a.push(...l)}if(o.length>0){let l=(0,Vt.innerInsertMultiple)(t,o,n,r,s);a.push(...l)}return!s&&t.afterUpsertMultiple&&(0,ye.runMultipleHook)(t.afterUpsertMultiple,t,a),a}});var Pi=b(Gt=>{"use strict";Object.defineProperty(Gt,"__esModule",{value:!0});Gt.AnswerSession=void 0;var Ht=j(),Bl=Ze(),$l="orama-secure-proxy",nr=class{db;proxy=null;config;abortController=null;lastInteractionParams=null;chatModel=null;conversationID;messages=[];events;initPromise;state=[];constructor(e,n){this.db=e,this.config=n,this.init(),this.messages=n.initialMessages||[],this.events=n.events||{},this.conversationID=n.conversationID||this.generateRandomID()}async ask(e){await this.initPromise;let n="";for await(let r of await this.askStream(e))n+=r;return n}async askStream(e){return await this.initPromise,this.fetchAnswer(e)}abortAnswer(){this.abortController?.abort(),this.state[this.state.length-1].aborted=!0,this.triggerStateChange()}getMessages(){return this.messages}clearSession(){this.messages=[],this.state=[]}regenerateLast({stream:e=!0}){if(this.state.length===0||this.messages.length===0)throw new Error("No messages to regenerate");if(!(this.messages.at(-1)?.role==="assistant"))throw(0,Ht.createError)("ANSWER_SESSION_LAST_MESSAGE_IS_NOT_ASSISTANT");return this.messages.pop(),this.state.pop(),e?this.askStream(this.lastInteractionParams):this.ask(this.lastInteractionParams)}async*fetchAnswer(e){if(!this.chatModel)throw(0,Ht.createError)("PLUGIN_SECURE_PROXY_MISSING_CHAT_MODEL");this.abortController=new AbortController,this.lastInteractionParams=e;let n=this.generateRandomID();this.messages.push({role:"user",content:e.term??""}),this.state.push({interactionId:n,aborted:!1,loading:!0,query:e.term??"",response:"",sources:null,translatedQuery:null,error:!1,errorMessage:null});let r=this.state.length-1;this.addEmptyAssistantMessage(),this.triggerStateChange();try{let s=await(0,Bl.search)(this.db,e);this.state[r].sources=s,this.triggerStateChange();for await(let i of this.proxy.chatStream({model:this.chatModel,messages:this.messages}))yield i,this.state[r].response+=i,this.messages.findLast(o=>o.role==="assistant").content+=i,this.triggerStateChange()}catch(s){s.name==="AbortError"?this.state[r].aborted=!0:(this.state[r].error=!0,this.state[r].errorMessage=s.toString()),this.triggerStateChange()}return this.state[r].loading=!1,this.triggerStateChange(),this.state[r].response}generateRandomID(e=24){return Array.from({length:e},()=>Math.floor(Math.random()*36).toString(36)).join("")}triggerStateChange(){this.events.onStateChange&&this.events.onStateChange(this.state)}async init(){let e=this;async function n(){return await e.db.plugins.find(i=>i.name===$l)}let r=await n();if(!r)throw(0,Ht.createError)("PLUGIN_SECURE_PROXY_NOT_FOUND");let s=r.extra;if(this.proxy=s.proxy,this.config.systemPrompt&&this.messages.push({role:"system",content:this.config.systemPrompt}),s?.pluginParams?.chat?.model)this.chatModel=s.pluginParams.chat.model;else throw(0,Ht.createError)("PLUGIN_SECURE_PROXY_MISSING_CHAT_MODEL")}addEmptyAssistantMessage(){this.messages.push({role:"assistant",content:""})}};Gt.AnswerSession=nr});var ki=b(Y=>{"use strict";Object.defineProperty(Y,"__esModule",{value:!0});Y.kRemovals=Y.kInsertions=Y.MODE_VECTOR_SEARCH=Y.MODE_HYBRID_SEARCH=Y.MODE_FULLTEXT_SEARCH=void 0;var rr=Gn();Object.defineProperty(Y,"MODE_FULLTEXT_SEARCH",{enumerable:!0,get:function(){return rr.MODE_FULLTEXT_SEARCH}});Object.defineProperty(Y,"MODE_HYBRID_SEARCH",{enumerable:!0,get:function(){return rr.MODE_HYBRID_SEARCH}});Object.defineProperty(Y,"MODE_VECTOR_SEARCH",{enumerable:!0,get:function(){return rr.MODE_VECTOR_SEARCH}});Y.kInsertions=Symbol("orama.insertions");Y.kRemovals=Symbol("orama.removals")});var Ni=b(N=>{"use strict";Object.defineProperty(N,"__esModule",{value:!0});N.normalizeToken=N.setDifference=N.setUnion=N.setIntersection=N.safeArrayPush=N.convertDistanceToMeters=N.uniqueId=N.getNanosecondsTime=N.formatNanoseconds=N.formatBytes=N.boundedLevenshtein=void 0;var ql=bn();Object.defineProperty(N,"boundedLevenshtein",{enumerable:!0,get:function(){return ql.boundedLevenshtein}});var se=R();Object.defineProperty(N,"formatBytes",{enumerable:!0,get:function(){return se.formatBytes}});Object.defineProperty(N,"formatNanoseconds",{enumerable:!0,get:function(){return se.formatNanoseconds}});Object.defineProperty(N,"getNanosecondsTime",{enumerable:!0,get:function(){return se.getNanosecondsTime}});Object.defineProperty(N,"uniqueId",{enumerable:!0,get:function(){return se.uniqueId}});Object.defineProperty(N,"convertDistanceToMeters",{enumerable:!0,get:function(){return se.convertDistanceToMeters}});Object.defineProperty(N,"safeArrayPush",{enumerable:!0,get:function(){return se.safeArrayPush}});Object.defineProperty(N,"setIntersection",{enumerable:!0,get:function(){return se.setIntersection}});Object.defineProperty(N,"setUnion",{enumerable:!0,get:function(){return se.setUnion}});Object.defineProperty(N,"setDifference",{enumerable:!0,get:function(){return se.setDifference}});var zl=It();Object.defineProperty(N,"normalizeToken",{enumerable:!0,get:function(){return zl.normalizeToken}})});var qi=b(x=>{"use strict";var Ui=x&&x.__createBinding||(Object.create?(function(t,e,n,r){r===void 0&&(r=n);var s=Object.getOwnPropertyDescriptor(e,n);(!s||("get"in s?!e.__esModule:s.writable||s.configurable))&&(s={enumerable:!0,get:function(){return e[n]}}),Object.defineProperty(t,r,s)}):(function(t,e,n,r){r===void 0&&(r=n),t[r]=e[n]})),Vl=x&&x.__setModuleDefault||(Object.create?(function(t,e){Object.defineProperty(t,"default",{enumerable:!0,value:e})}):function(t,e){t.default=e}),Wl=x&&x.__exportStar||function(t,e){for(var n in t)n!=="default"&&!Object.prototype.hasOwnProperty.call(e,n)&&Ui(e,t,n)},Ri=x&&x.__importStar||(function(){var t=function(e){return t=Object.getOwnPropertyNames||function(n){var r=[];for(var s in n)Object.prototype.hasOwnProperty.call(n,s)&&(r[r.length]=s);return r},t(e)};return function(e){if(e&&e.__esModule)return e;var n={};if(e!=null)for(var r=t(e),s=0;s{"use strict";Object.defineProperty(ie,"__esModule",{value:!0});ie.utf8Count=Jl;ie.utf8EncodeJs=zi;ie.utf8EncodeTE=Vi;ie.utf8Encode=Ql;ie.utf8DecodeJs=Wi;ie.utf8DecodeTD=Ki;ie.utf8Decode=rd;function Jl(t){let e=t.length,n=0,r=0;for(;r=55296&&s<=56319&&r>6&31|192;else{if(o>=55296&&o<=56319&&i>12&15|224,e[s++]=o>>6&63|128):(e[s++]=o>>18&7|240,e[s++]=o>>12&63|128,e[s++]=o>>6&63|128)}e[s++]=o&63|128}}var Xl=new TextEncoder,Zl=50;function Vi(t,e,n){Xl.encodeInto(t,e.subarray(n))}function Ql(t,e,n){t.length>Zl?Vi(t,e,n):zi(t,e,n)}var ed=4096;function Wi(t,e,n){let r=e,s=r+n,i=[],o="";for(;r65535&&(d-=65536,i.push(d>>>10&1023|55296),d=56320|d&1023),i.push(d)}else i.push(c);i.length>=ed&&(o+=String.fromCharCode(...i),i.length=0)}return i.length>0&&(o+=String.fromCharCode(...i)),o}var td=new TextDecoder,nd=200;function Ki(t,e,n){let r=t.subarray(e,e+n);return td.decode(r)}function rd(t,e,n){return n>nd?Ki(t,e,n):Wi(t,e,n)}});var ir=b(Jt=>{"use strict";Object.defineProperty(Jt,"__esModule",{value:!0});Jt.ExtData=void 0;var sr=class{type;data;constructor(e,n){this.type=e,this.data=n}};Jt.ExtData=sr});var Zt=b(Xt=>{"use strict";Object.defineProperty(Xt,"__esModule",{value:!0});Xt.DecodeError=void 0;var or=class t extends Error{constructor(e){super(e);let n=Object.create(t.prototype);Object.setPrototypeOf(this,n),Object.defineProperty(this,"name",{configurable:!0,enumerable:!1,value:t.name})}};Xt.DecodeError=or});var Qt=b(me=>{"use strict";Object.defineProperty(me,"__esModule",{value:!0});me.UINT32_MAX=void 0;me.setUint64=sd;me.setInt64=id;me.getInt64=od;me.getUint64=cd;me.UINT32_MAX=4294967295;function sd(t,e,n){let r=n/4294967296,s=n;t.setUint32(e,r),t.setUint32(e+4,s)}function id(t,e,n){let r=Math.floor(n/4294967296),s=n;t.setUint32(e,r),t.setUint32(e+4,s)}function od(t,e){let n=t.getInt32(e),r=t.getUint32(e+4);return n*4294967296+r}function cd(t,e){let n=t.getUint32(e),r=t.getUint32(e+4);return n*4294967296+r}});var cr=b(J=>{"use strict";Object.defineProperty(J,"__esModule",{value:!0});J.timestampExtension=J.EXT_TIMESTAMP=void 0;J.encodeTimeSpecToTimestamp=Gi;J.encodeDateToTimeSpec=Yi;J.encodeTimestampExtension=Ji;J.decodeTimestampToTimeSpec=Xi;J.decodeTimestampExtension=Zi;var ud=Zt(),Hi=Qt();J.EXT_TIMESTAMP=-1;var ad=4294967296-1,ld=17179869184-1;function Gi({sec:t,nsec:e}){if(t>=0&&e>=0&&t<=ld)if(e===0&&t<=ad){let n=new Uint8Array(4);return new DataView(n.buffer).setUint32(0,t),n}else{let n=t/4294967296,r=t&4294967295,s=new Uint8Array(8),i=new DataView(s.buffer);return i.setUint32(0,e<<2|n&3),i.setUint32(4,r),s}else{let n=new Uint8Array(12),r=new DataView(n.buffer);return r.setUint32(0,e),(0,Hi.setInt64)(r,4,t),n}}function Yi(t){let e=t.getTime(),n=Math.floor(e/1e3),r=(e-n*1e3)*1e6,s=Math.floor(r/1e9);return{sec:n+s,nsec:r-s*1e9}}function Ji(t){if(t instanceof Date){let e=Yi(t);return Gi(e)}else return null}function Xi(t){let e=new DataView(t.buffer,t.byteOffset,t.byteLength);switch(t.byteLength){case 4:return{sec:e.getUint32(0),nsec:0};case 8:{let n=e.getUint32(0),r=e.getUint32(4),s=(n&3)*4294967296+r,i=n>>>2;return{sec:s,nsec:i}}case 12:{let n=(0,Hi.getInt64)(e,4),r=e.getUint32(0);return{sec:n,nsec:r}}default:throw new ud.DecodeError(`Unrecognized data size for timestamp (expected 4, 8, or 12): ${t.length}`)}}function Zi(t){let e=Xi(t);return new Date(e.sec*1e3+e.nsec/1e6)}J.timestampExtension={type:J.EXT_TIMESTAMP,encode:Ji,decode:Zi}});var nn=b(tn=>{"use strict";Object.defineProperty(tn,"__esModule",{value:!0});tn.ExtensionCodec=void 0;var en=ir(),dd=cr(),ur=class t{static defaultCodec=new t;__brand;builtInEncoders=[];builtInDecoders=[];encoders=[];decoders=[];constructor(){this.register(dd.timestampExtension)}register({type:e,encode:n,decode:r}){if(e>=0)this.encoders[e]=n,this.decoders[e]=r;else{let s=-1-e;this.builtInEncoders[s]=n,this.builtInDecoders[s]=r}}tryToEncode(e,n){for(let r=0;r{"use strict";Object.defineProperty(ar,"__esModule",{value:!0});ar.ensureUint8Array=hd;function fd(t){return t instanceof ArrayBuffer||typeof SharedArrayBuffer<"u"&&t instanceof SharedArrayBuffer}function hd(t){return t instanceof Uint8Array?t:ArrayBuffer.isView(t)?new Uint8Array(t.buffer,t.byteOffset,t.byteLength):fd(t)?new Uint8Array(t):Uint8Array.from(t)}});var fr=b(Q=>{"use strict";Object.defineProperty(Q,"__esModule",{value:!0});Q.Encoder=Q.DEFAULT_INITIAL_BUFFER_SIZE=Q.DEFAULT_MAX_DEPTH=void 0;var Qi=Yt(),pd=nn(),eo=Qt(),gd=lr();Q.DEFAULT_MAX_DEPTH=100;Q.DEFAULT_INITIAL_BUFFER_SIZE=2048;var dr=class t{extensionCodec;context;useBigInt64;maxDepth;initialBufferSize;sortKeys;forceFloat32;ignoreUndefined;forceIntegerToFloat;pos;view;bytes;entered=!1;constructor(e){this.extensionCodec=e?.extensionCodec??pd.ExtensionCodec.defaultCodec,this.context=e?.context,this.useBigInt64=e?.useBigInt64??!1,this.maxDepth=e?.maxDepth??Q.DEFAULT_MAX_DEPTH,this.initialBufferSize=e?.initialBufferSize??Q.DEFAULT_INITIAL_BUFFER_SIZE,this.sortKeys=e?.sortKeys??!1,this.forceFloat32=e?.forceFloat32??!1,this.ignoreUndefined=e?.ignoreUndefined??!1,this.forceIntegerToFloat=e?.forceIntegerToFloat??!1,this.pos=0,this.view=new DataView(new ArrayBuffer(this.initialBufferSize)),this.bytes=new Uint8Array(this.view.buffer)}clone(){return new t({extensionCodec:this.extensionCodec,context:this.context,useBigInt64:this.useBigInt64,maxDepth:this.maxDepth,initialBufferSize:this.initialBufferSize,sortKeys:this.sortKeys,forceFloat32:this.forceFloat32,ignoreUndefined:this.ignoreUndefined,forceIntegerToFloat:this.forceIntegerToFloat})}reinitializeState(){this.pos=0}encodeSharedRef(e){if(this.entered)return this.clone().encodeSharedRef(e);try{return this.entered=!0,this.reinitializeState(),this.doEncode(e,1),this.bytes.subarray(0,this.pos)}finally{this.entered=!1}}encode(e){if(this.entered)return this.clone().encode(e);try{return this.entered=!0,this.reinitializeState(),this.doEncode(e,1),this.bytes.slice(0,this.pos)}finally{this.entered=!1}}doEncode(e,n){if(n>this.maxDepth)throw new Error(`Too deep objects in depth ${n}`);e==null?this.encodeNil():typeof e=="boolean"?this.encodeBoolean(e):typeof e=="number"?this.forceIntegerToFloat?this.encodeNumberAsFloat(e):this.encodeNumber(e):typeof e=="string"?this.encodeString(e):this.useBigInt64&&typeof e=="bigint"?this.encodeBigInt64(e):this.encodeObject(e,n)}ensureBufferSizeToWrite(e){let n=this.pos+e;this.view.byteLength=0?e<128?this.writeU8(e):e<256?(this.writeU8(204),this.writeU8(e)):e<65536?(this.writeU8(205),this.writeU16(e)):e<4294967296?(this.writeU8(206),this.writeU32(e)):this.useBigInt64?this.encodeNumberAsFloat(e):(this.writeU8(207),this.writeU64(e)):e>=-32?this.writeU8(224|e+32):e>=-128?(this.writeU8(208),this.writeI8(e)):e>=-32768?(this.writeU8(209),this.writeI16(e)):e>=-2147483648?(this.writeU8(210),this.writeI32(e)):this.useBigInt64?this.encodeNumberAsFloat(e):(this.writeU8(211),this.writeI64(e)):this.encodeNumberAsFloat(e)}encodeNumberAsFloat(e){this.forceFloat32?(this.writeU8(202),this.writeF32(e)):(this.writeU8(203),this.writeF64(e))}encodeBigInt64(e){e>=BigInt(0)?(this.writeU8(207),this.writeBigUint64(e)):(this.writeU8(211),this.writeBigInt64(e))}writeStringHeader(e){if(e<32)this.writeU8(160+e);else if(e<256)this.writeU8(217),this.writeU8(e);else if(e<65536)this.writeU8(218),this.writeU16(e);else if(e<4294967296)this.writeU8(219),this.writeU32(e);else throw new Error(`Too long string: ${e} bytes in UTF-8`)}encodeString(e){let r=(0,Qi.utf8Count)(e);this.ensureBufferSizeToWrite(5+r),this.writeStringHeader(r),(0,Qi.utf8Encode)(e,this.bytes,this.pos),this.pos+=r}encodeObject(e,n){let r=this.extensionCodec.tryToEncode(e,this.context);if(r!=null)this.encodeExtension(r);else if(Array.isArray(e))this.encodeArray(e,n);else if(ArrayBuffer.isView(e))this.encodeBinary(e);else if(typeof e=="object")this.encodeMap(e,n);else throw new Error(`Unrecognized object: ${Object.prototype.toString.apply(e)}`)}encodeBinary(e){let n=e.byteLength;if(n<256)this.writeU8(196),this.writeU8(n);else if(n<65536)this.writeU8(197),this.writeU16(n);else if(n<4294967296)this.writeU8(198),this.writeU32(n);else throw new Error(`Too large binary: ${n}`);let r=(0,gd.ensureUint8Array)(e);this.writeU8a(r)}encodeArray(e,n){let r=e.length;if(r<16)this.writeU8(144+r);else if(r<65536)this.writeU8(220),this.writeU16(r);else if(r<4294967296)this.writeU8(221),this.writeU32(r);else throw new Error(`Too large array: ${r}`);for(let s of e)this.doEncode(s,n+1)}countWithoutUndefined(e,n){let r=0;for(let s of n)e[s]!==void 0&&r++;return r}encodeMap(e,n){let r=Object.keys(e);this.sortKeys&&r.sort();let s=this.ignoreUndefined?this.countWithoutUndefined(e,r):r.length;if(s<16)this.writeU8(128+s);else if(s<65536)this.writeU8(222),this.writeU16(s);else if(s<4294967296)this.writeU8(223),this.writeU32(s);else throw new Error(`Too large map object: ${s}`);for(let i of r){let o=e[i];this.ignoreUndefined&&o===void 0||(this.encodeString(i),this.doEncode(o,n+1))}}encodeExtension(e){if(typeof e.data=="function"){let r=e.data(this.pos+6),s=r.length;if(s>=4294967296)throw new Error(`Too large extension object: ${s}`);this.writeU8(201),this.writeU32(s),this.writeI8(e.type),this.writeU8a(r);return}let n=e.data.length;if(n===1)this.writeU8(212);else if(n===2)this.writeU8(213);else if(n===4)this.writeU8(214);else if(n===8)this.writeU8(215);else if(n===16)this.writeU8(216);else if(n<256)this.writeU8(199),this.writeU8(n);else if(n<65536)this.writeU8(200),this.writeU16(n);else if(n<4294967296)this.writeU8(201),this.writeU32(n);else throw new Error(`Too large extension object: ${n}`);this.writeI8(e.type),this.writeU8a(e.data)}writeU8(e){this.ensureBufferSizeToWrite(1),this.view.setUint8(this.pos,e),this.pos++}writeU8a(e){let n=e.length;this.ensureBufferSizeToWrite(n),this.bytes.set(e,this.pos),this.pos+=n}writeI8(e){this.ensureBufferSizeToWrite(1),this.view.setInt8(this.pos,e),this.pos++}writeU16(e){this.ensureBufferSizeToWrite(2),this.view.setUint16(this.pos,e),this.pos+=2}writeI16(e){this.ensureBufferSizeToWrite(2),this.view.setInt16(this.pos,e),this.pos+=2}writeU32(e){this.ensureBufferSizeToWrite(4),this.view.setUint32(this.pos,e),this.pos+=4}writeI32(e){this.ensureBufferSizeToWrite(4),this.view.setInt32(this.pos,e),this.pos+=4}writeF32(e){this.ensureBufferSizeToWrite(4),this.view.setFloat32(this.pos,e),this.pos+=4}writeF64(e){this.ensureBufferSizeToWrite(8),this.view.setFloat64(this.pos,e),this.pos+=8}writeU64(e){this.ensureBufferSizeToWrite(8),(0,eo.setUint64)(this.view,this.pos,e),this.pos+=8}writeI64(e){this.ensureBufferSizeToWrite(8),(0,eo.setInt64)(this.view,this.pos,e),this.pos+=8}writeBigUint64(e){this.ensureBufferSizeToWrite(8),this.view.setBigUint64(this.pos,e),this.pos+=8}writeBigInt64(e){this.ensureBufferSizeToWrite(8),this.view.setBigInt64(this.pos,e),this.pos+=8}};Q.Encoder=dr});var to=b(hr=>{"use strict";Object.defineProperty(hr,"__esModule",{value:!0});hr.encode=md;var yd=fr();function md(t,e){return new yd.Encoder(e).encodeSharedRef(t)}});var no=b(pr=>{"use strict";Object.defineProperty(pr,"__esModule",{value:!0});pr.prettyByte=wd;function wd(t){return`${t<0?"-":""}0x${Math.abs(t).toString(16).padStart(2,"0")}`}});var ro=b(rn=>{"use strict";Object.defineProperty(rn,"__esModule",{value:!0});rn.CachedKeyDecoder=void 0;var _d=Yt(),Sd=16,bd=16,gr=class{hit=0;miss=0;caches;maxKeyLength;maxLengthPerKey;constructor(e=Sd,n=bd){this.maxKeyLength=e,this.maxLengthPerKey=n,this.caches=[];for(let r=0;r0&&e<=this.maxKeyLength}find(e,n,r){let s=this.caches[r-1];e:for(let i of s){let o=i.bytes;for(let c=0;c=this.maxLengthPerKey?r[Math.random()*r.length|0]=s:r.push(s)}decode(e,n,r){let s=this.find(e,n,r);if(s!=null)return this.hit++,s;this.miss++;let i=(0,_d.utf8DecodeJs)(e,n,r),o=Uint8Array.prototype.slice.call(e,n,n+r);return this.store(o,i),i}};rn.CachedKeyDecoder=gr});var on=b(sn=>{"use strict";Object.defineProperty(sn,"__esModule",{value:!0});sn.Decoder=void 0;var yr=no(),Id=nn(),ve=Qt(),xd=Yt(),so=lr(),Ed=ro(),oe=Zt(),mr="array",nt="map_key",oo="map_value",Ad=t=>{if(typeof t=="string"||typeof t=="number")return t;throw new oe.DecodeError("The type of key must be string or number but "+typeof t)},wr=class{stack=[];stackHeadPosition=-1;get length(){return this.stackHeadPosition+1}top(){return this.stack[this.stackHeadPosition]}pushArrayState(e){let n=this.getUninitializedStateFromPool();n.type=mr,n.position=0,n.size=e,n.array=new Array(e)}pushMapState(e){let n=this.getUninitializedStateFromPool();n.type=nt,n.readCount=0,n.size=e,n.map={}}getUninitializedStateFromPool(){if(this.stackHeadPosition++,this.stackHeadPosition===this.stack.length){let e={type:void 0,size:0,array:void 0,position:0,readCount:0,map:void 0,key:null};this.stack.push(e)}return this.stack[this.stackHeadPosition]}release(e){if(this.stack[this.stackHeadPosition]!==e)throw new Error("Invalid stack state. Released state is not on top of the stack.");if(e.type===mr){let r=e;r.size=0,r.array=void 0,r.position=0,r.type=void 0}if(e.type===nt||e.type===oo){let r=e;r.size=0,r.map=void 0,r.readCount=0,r.type=void 0}this.stackHeadPosition--}reset(){this.stack.length=0,this.stackHeadPosition=-1}},tt=-1,Sr=new DataView(new ArrayBuffer(0)),vd=new Uint8Array(Sr.buffer);try{Sr.getInt8(0)}catch(t){if(!(t instanceof RangeError))throw new Error("This module is not supported in the current JavaScript engine because DataView does not throw RangeError on out-of-bounds access")}var io=new RangeError("Insufficient data"),Td=new Ed.CachedKeyDecoder,_r=class t{extensionCodec;context;useBigInt64;rawStrings;maxStrLength;maxBinLength;maxArrayLength;maxMapLength;maxExtLength;keyDecoder;mapKeyConverter;totalPos=0;pos=0;view=Sr;bytes=vd;headByte=tt;stack=new wr;entered=!1;constructor(e){this.extensionCodec=e?.extensionCodec??Id.ExtensionCodec.defaultCodec,this.context=e?.context,this.useBigInt64=e?.useBigInt64??!1,this.rawStrings=e?.rawStrings??!1,this.maxStrLength=e?.maxStrLength??ve.UINT32_MAX,this.maxBinLength=e?.maxBinLength??ve.UINT32_MAX,this.maxArrayLength=e?.maxArrayLength??ve.UINT32_MAX,this.maxMapLength=e?.maxMapLength??ve.UINT32_MAX,this.maxExtLength=e?.maxExtLength??ve.UINT32_MAX,this.keyDecoder=e?.keyDecoder!==void 0?e.keyDecoder:Td,this.mapKeyConverter=e?.mapKeyConverter??Ad}clone(){return new t({extensionCodec:this.extensionCodec,context:this.context,useBigInt64:this.useBigInt64,rawStrings:this.rawStrings,maxStrLength:this.maxStrLength,maxBinLength:this.maxBinLength,maxArrayLength:this.maxArrayLength,maxMapLength:this.maxMapLength,maxExtLength:this.maxExtLength,keyDecoder:this.keyDecoder})}reinitializeState(){this.totalPos=0,this.headByte=tt,this.stack.reset()}setBuffer(e){let n=(0,so.ensureUint8Array)(e);this.bytes=n,this.view=new DataView(n.buffer,n.byteOffset,n.byteLength),this.pos=0}appendBuffer(e){if(this.headByte===tt&&!this.hasRemaining(1))this.setBuffer(e);else{let n=this.bytes.subarray(this.pos),r=(0,so.ensureUint8Array)(e),s=new Uint8Array(n.length+r.length);s.set(n),s.set(r,n.length),this.setBuffer(s)}}hasRemaining(e){return this.view.byteLength-this.pos>=e}createExtraByteError(e){let{view:n,pos:r}=this;return new RangeError(`Extra ${n.byteLength-r} of ${n.byteLength} byte(s) found at buffer[${e}]`)}decode(e){if(this.entered)return this.clone().decode(e);try{this.entered=!0,this.reinitializeState(),this.setBuffer(e);let n=this.doDecodeSync();if(this.hasRemaining(1))throw this.createExtraByteError(this.pos);return n}finally{this.entered=!1}}*decodeMulti(e){if(this.entered){yield*this.clone().decodeMulti(e);return}try{for(this.entered=!0,this.reinitializeState(),this.setBuffer(e);this.hasRemaining(1);)yield this.doDecodeSync()}finally{this.entered=!1}}async decodeAsync(e){if(this.entered)return this.clone().decodeAsync(e);try{this.entered=!0;let n=!1,r;for await(let c of e){if(n)throw this.entered=!1,this.createExtraByteError(this.totalPos);this.appendBuffer(c);try{r=this.doDecodeSync(),n=!0}catch(u){if(!(u instanceof RangeError))throw u}this.totalPos+=this.pos}if(n){if(this.hasRemaining(1))throw this.createExtraByteError(this.totalPos);return r}let{headByte:s,pos:i,totalPos:o}=this;throw new RangeError(`Insufficient data in parsing ${(0,yr.prettyByte)(s)} at ${o} (${i} in the current buffer)`)}finally{this.entered=!1}}decodeArrayStream(e){return this.decodeMultiAsync(e,!0)}decodeStream(e){return this.decodeMultiAsync(e,!1)}async*decodeMultiAsync(e,n){if(this.entered){yield*this.clone().decodeMultiAsync(e,n);return}try{this.entered=!0;let r=n,s=-1;for await(let i of e){if(n&&s===0)throw this.createExtraByteError(this.totalPos);this.appendBuffer(i),r&&(s=this.readArraySize(),r=!1,this.complete());try{for(;yield this.doDecodeSync(),--s!==0;);}catch(o){if(!(o instanceof RangeError))throw o}this.totalPos+=this.pos}}finally{this.entered=!1}}doDecodeSync(){e:for(;;){let e=this.readHeadByte(),n;if(e>=224)n=e-256;else if(e<192)if(e<128)n=e;else if(e<144){let s=e-128;if(s!==0){this.pushMapState(s),this.complete();continue e}else n={}}else if(e<160){let s=e-144;if(s!==0){this.pushArrayState(s),this.complete();continue e}else n=[]}else{let s=e-160;n=this.decodeString(s,0)}else if(e===192)n=null;else if(e===194)n=!1;else if(e===195)n=!0;else if(e===202)n=this.readF32();else if(e===203)n=this.readF64();else if(e===204)n=this.readU8();else if(e===205)n=this.readU16();else if(e===206)n=this.readU32();else if(e===207)this.useBigInt64?n=this.readU64AsBigInt():n=this.readU64();else if(e===208)n=this.readI8();else if(e===209)n=this.readI16();else if(e===210)n=this.readI32();else if(e===211)this.useBigInt64?n=this.readI64AsBigInt():n=this.readI64();else if(e===217){let s=this.lookU8();n=this.decodeString(s,1)}else if(e===218){let s=this.lookU16();n=this.decodeString(s,2)}else if(e===219){let s=this.lookU32();n=this.decodeString(s,4)}else if(e===220){let s=this.readU16();if(s!==0){this.pushArrayState(s),this.complete();continue e}else n=[]}else if(e===221){let s=this.readU32();if(s!==0){this.pushArrayState(s),this.complete();continue e}else n=[]}else if(e===222){let s=this.readU16();if(s!==0){this.pushMapState(s),this.complete();continue e}else n={}}else if(e===223){let s=this.readU32();if(s!==0){this.pushMapState(s),this.complete();continue e}else n={}}else if(e===196){let s=this.lookU8();n=this.decodeBinary(s,1)}else if(e===197){let s=this.lookU16();n=this.decodeBinary(s,2)}else if(e===198){let s=this.lookU32();n=this.decodeBinary(s,4)}else if(e===212)n=this.decodeExtension(1,0);else if(e===213)n=this.decodeExtension(2,0);else if(e===214)n=this.decodeExtension(4,0);else if(e===215)n=this.decodeExtension(8,0);else if(e===216)n=this.decodeExtension(16,0);else if(e===199){let s=this.lookU8();n=this.decodeExtension(s,1)}else if(e===200){let s=this.lookU16();n=this.decodeExtension(s,2)}else if(e===201){let s=this.lookU32();n=this.decodeExtension(s,4)}else throw new oe.DecodeError(`Unrecognized type byte: ${(0,yr.prettyByte)(e)}`);this.complete();let r=this.stack;for(;r.length>0;){let s=r.top();if(s.type===mr)if(s.array[s.position]=n,s.position++,s.position===s.size)n=s.array,r.release(s);else continue e;else if(s.type===nt){if(n==="__proto__")throw new oe.DecodeError("The key __proto__ is not allowed");s.key=this.mapKeyConverter(n),s.type=oo;continue e}else if(s.map[s.key]=n,s.readCount++,s.readCount===s.size)n=s.map,r.release(s);else{s.key=null,s.type=nt;continue e}}return n}}readHeadByte(){return this.headByte===tt&&(this.headByte=this.readU8()),this.headByte}complete(){this.headByte=tt}readArraySize(){let e=this.readHeadByte();switch(e){case 220:return this.readU16();case 221:return this.readU32();default:{if(e<160)return e-144;throw new oe.DecodeError(`Unrecognized array type byte: ${(0,yr.prettyByte)(e)}`)}}}pushMapState(e){if(e>this.maxMapLength)throw new oe.DecodeError(`Max length exceeded: map length (${e}) > maxMapLengthLength (${this.maxMapLength})`);this.stack.pushMapState(e)}pushArrayState(e){if(e>this.maxArrayLength)throw new oe.DecodeError(`Max length exceeded: array length (${e}) > maxArrayLength (${this.maxArrayLength})`);this.stack.pushArrayState(e)}decodeString(e,n){return!this.rawStrings||this.stateIsMapKey()?this.decodeUtf8String(e,n):this.decodeBinary(e,n)}decodeUtf8String(e,n){if(e>this.maxStrLength)throw new oe.DecodeError(`Max length exceeded: UTF-8 byte length (${e}) > maxStrLength (${this.maxStrLength})`);if(this.bytes.byteLength0?this.stack.top().type===nt:!1}decodeBinary(e,n){if(e>this.maxBinLength)throw new oe.DecodeError(`Max length exceeded: bin length (${e}) > maxBinLength (${this.maxBinLength})`);if(!this.hasRemaining(e+n))throw io;let r=this.pos+n,s=this.bytes.subarray(r,r+e);return this.pos+=n+e,s}decodeExtension(e,n){if(e>this.maxExtLength)throw new oe.DecodeError(`Max length exceeded: ext length (${e}) > maxExtLength (${this.maxExtLength})`);let r=this.view.getInt8(this.pos+n),s=this.decodeBinary(e,n+1);return this.extensionCodec.decode(s,r,this.context)}lookU8(){return this.view.getUint8(this.pos)}lookU16(){return this.view.getUint16(this.pos)}lookU32(){return this.view.getUint32(this.pos)}readU8(){let e=this.view.getUint8(this.pos);return this.pos++,e}readI8(){let e=this.view.getInt8(this.pos);return this.pos++,e}readU16(){let e=this.view.getUint16(this.pos);return this.pos+=2,e}readI16(){let e=this.view.getInt16(this.pos);return this.pos+=2,e}readU32(){let e=this.view.getUint32(this.pos);return this.pos+=4,e}readI32(){let e=this.view.getInt32(this.pos);return this.pos+=4,e}readU64(){let e=(0,ve.getUint64)(this.view,this.pos);return this.pos+=8,e}readI64(){let e=(0,ve.getInt64)(this.view,this.pos);return this.pos+=8,e}readU64AsBigInt(){let e=this.view.getBigUint64(this.pos);return this.pos+=8,e}readI64AsBigInt(){let e=this.view.getBigInt64(this.pos);return this.pos+=8,e}readF32(){let e=this.view.getFloat32(this.pos);return this.pos+=4,e}readF64(){let e=this.view.getFloat64(this.pos);return this.pos+=8,e}};sn.Decoder=_r});var uo=b(cn=>{"use strict";Object.defineProperty(cn,"__esModule",{value:!0});cn.decode=Dd;cn.decodeMulti=Md;var co=on();function Dd(t,e){return new co.Decoder(e).decode(t)}function Md(t,e){return new co.Decoder(e).decodeMulti(t)}});var fo=b(rt=>{"use strict";Object.defineProperty(rt,"__esModule",{value:!0});rt.isAsyncIterable=ao;rt.asyncIterableFromStream=lo;rt.ensureAsyncIterable=Od;function ao(t){return t[Symbol.asyncIterator]!=null}async function*lo(t){let e=t.getReader();try{for(;;){let{done:n,value:r}=await e.read();if(n)return;yield r}}finally{e.releaseLock()}}function Od(t){return ao(t)?t:lo(t)}});var ho=b(st=>{"use strict";Object.defineProperty(st,"__esModule",{value:!0});st.decodeAsync=Pd;st.decodeArrayStream=kd;st.decodeMultiStream=Nd;var br=on(),Ir=fo();async function Pd(t,e){let n=(0,Ir.ensureAsyncIterable)(t);return new br.Decoder(e).decodeAsync(n)}function kd(t,e){let n=(0,Ir.ensureAsyncIterable)(t);return new br.Decoder(e).decodeArrayStream(n)}function Nd(t,e){let n=(0,Ir.ensureAsyncIterable)(t);return new br.Decoder(e).decodeStream(n)}});var go=b(M=>{"use strict";Object.defineProperty(M,"__esModule",{value:!0});M.decodeTimestampExtension=M.encodeTimestampExtension=M.decodeTimestampToTimeSpec=M.encodeTimeSpecToTimestamp=M.encodeDateToTimeSpec=M.EXT_TIMESTAMP=M.ExtData=M.ExtensionCodec=M.Encoder=M.DecodeError=M.Decoder=M.decodeMultiStream=M.decodeArrayStream=M.decodeAsync=M.decodeMulti=M.decode=M.encode=void 0;var Ud=to();Object.defineProperty(M,"encode",{enumerable:!0,get:function(){return Ud.encode}});var po=uo();Object.defineProperty(M,"decode",{enumerable:!0,get:function(){return po.decode}});Object.defineProperty(M,"decodeMulti",{enumerable:!0,get:function(){return po.decodeMulti}});var xr=ho();Object.defineProperty(M,"decodeAsync",{enumerable:!0,get:function(){return xr.decodeAsync}});Object.defineProperty(M,"decodeArrayStream",{enumerable:!0,get:function(){return xr.decodeArrayStream}});Object.defineProperty(M,"decodeMultiStream",{enumerable:!0,get:function(){return xr.decodeMultiStream}});var Rd=on();Object.defineProperty(M,"Decoder",{enumerable:!0,get:function(){return Rd.Decoder}});var Ld=Zt();Object.defineProperty(M,"DecodeError",{enumerable:!0,get:function(){return Ld.DecodeError}});var jd=fr();Object.defineProperty(M,"Encoder",{enumerable:!0,get:function(){return jd.Encoder}});var Cd=nn();Object.defineProperty(M,"ExtensionCodec",{enumerable:!0,get:function(){return Cd.ExtensionCodec}});var Fd=ir();Object.defineProperty(M,"ExtData",{enumerable:!0,get:function(){return Fd.ExtData}});var je=cr();Object.defineProperty(M,"EXT_TIMESTAMP",{enumerable:!0,get:function(){return je.EXT_TIMESTAMP}});Object.defineProperty(M,"encodeDateToTimeSpec",{enumerable:!0,get:function(){return je.encodeDateToTimeSpec}});Object.defineProperty(M,"encodeTimeSpecToTimestamp",{enumerable:!0,get:function(){return je.encodeTimeSpecToTimestamp}});Object.defineProperty(M,"decodeTimestampToTimeSpec",{enumerable:!0,get:function(){return je.decodeTimestampToTimeSpec}});Object.defineProperty(M,"encodeTimestampExtension",{enumerable:!0,get:function(){return je.encodeTimestampExtension}});Object.defineProperty(M,"decodeTimestampExtension",{enumerable:!0,get:function(){return je.decodeTimestampExtension}})});var vr=b((Xh,So)=>{"use strict";var z=require("fs"),ee=qi(),{encode:Bd,decode:$d}=go(),Er=["id","content","work_unit","work_type","phase","topic","confidence","source_file","timestamp"];function yo(t){if(!Number.isInteger(t)||t<=0)throw new Error(`createStore: dimensions must be a positive integer, got ${t}`);return{id:"string",content:"string",work_unit:"enum",work_type:"enum",phase:"enum",topic:"enum",confidence:"enum",source_file:"string",timestamp:"number",embedding:`vector[${t}]`}}async function qd(t){let e=yo(t);return ee.create({schema:e})}function zd(t){for(let e of Er)if(t[e]===void 0||t[e]===null)throw new Error(`insertDocument: missing required field "${e}"`);if(typeof t.timestamp!="number"||!Number.isFinite(t.timestamp))throw new Error("insertDocument: timestamp must be a finite number (epoch ms)")}async function Vd(t,e){if(e==null||typeof e!="object")throw new Error("insertDocument: doc must be an object");zd(e);let n={};for(let r of Er)n[r]=e[r];if("embedding"in e){if(e.embedding===null)throw new Error("insertDocument: embedding cannot be null (Orama crashes on null vectors). Omit the field for keyword-only mode, or pass a real vector.");if(e.embedding!==void 0){if(!Array.isArray(e.embedding))throw new Error("insertDocument: embedding must be an array of numbers when present");n.embedding=e.embedding}}return ee.insert(t,n)}async function Wd(t,e){if(!e||!e.work_unit||!e.phase||!e.topic)throw new Error("removeByIdentity: work_unit, phase, and topic are all required");return mo(t,{work_unit:{eq:e.work_unit},phase:{eq:e.phase},topic:{eq:e.topic}})}async function mo(t,e){if(!e||Object.keys(e).length===0)throw new Error("removeByFilter: where clause is required");let r=(await ee.search(t,{term:"",where:e,limit:1e5})).hits.map(i=>i.id);return r.length===0?0:await ee.removeMultiple(t,r)}function Ar(t){let e=t.document||{};return{id:e.id,content:e.content,work_unit:e.work_unit,work_type:e.work_type,phase:e.phase,topic:e.topic,confidence:e.confidence,source_file:e.source_file,timestamp:e.timestamp,score:t.score}}async function Kd(t,{term:e="",where:n,limit:r=10}={}){let s={term:e,limit:r};return n&&Object.keys(n).length>0&&(s.where=n),(await ee.search(t,s)).hits.map(Ar)}async function Hd(t,{vector:e,where:n,limit:r=10,similarity:s}={}){if(!Array.isArray(e))throw new Error("searchVector: vector (number[]) is required");let i={mode:"vector",vector:{value:e,property:"embedding"},limit:r};return typeof s=="number"&&(i.similarity=s),n&&Object.keys(n).length>0&&(i.where=n),(await ee.search(t,i)).hits.map(Ar)}async function Gd(t,{term:e,vector:n,where:r,limit:s=10,textWeight:i=.4,vectorWeight:o=.6,similarity:c}={}){if(typeof e!="string")throw new Error("searchHybrid: term (string) is required");if(!Array.isArray(n))throw new Error("searchHybrid: vector (number[]) is required");let u={mode:"hybrid",term:e,vector:{value:n,property:"embedding"},hybridWeights:{text:i,vector:o},limit:s};return typeof c=="number"&&(u.similarity=c),r&&Object.keys(r).length>0&&(u.where=r),(await ee.search(t,u)).hits.map(Ar)}async function Yd(t,e){if(!e)throw new Error("saveStore: storePath is required");let n=ee.save(t),r={v:1,schema:t.schema,raw:n},s=Bd(r),i=e+".tmp";z.writeFileSync(i,s),z.renameSync(i,e)}async function Jd(t){if(!t)throw new Error("loadStore: storePath is required");if(!z.existsSync(t))throw new Error(`loadStore: store file not found at ${t}`);let e;try{e=z.readFileSync(t)}catch(s){throw new Error(`loadStore: failed to read ${t}: ${s.message}`)}if(e.length===0)throw new Error(`loadStore: store file is empty at ${t}`);let n;try{n=$d(e)}catch(s){throw new Error(`loadStore: corrupted store file at ${t}: ${s.message}`)}if(!n||typeof n!="object"||!n.schema||!n.raw)throw new Error(`loadStore: malformed envelope at ${t}`);let r=await ee.create({schema:n.schema});return ee.load(r,n.raw),r}var Xd=3e4,Zd=50,Qd=1e4;function ef(t){try{let e=z.openSync(t,"wx");return z.writeSync(e,String(process.pid)),z.closeSync(e),!0}catch(e){if(e.code!=="EEXIST")throw e;return!1}}function tf(t){return new Promise(e=>setTimeout(e,t))}async function wo(t){let e=Date.now()+Qd;for(;;){if(ef(t))return;try{let n=z.statSync(t);if(Date.now()-n.mtimeMs>Xd){try{z.unlinkSync(t)}catch{}continue}}catch{continue}if(Date.now()>=e)throw new Error(`knowledge store: timed out waiting for lock at ${t}. If no other process is running, delete the file manually.`);await tf(Zd)}}function _o(t){try{z.unlinkSync(t)}catch{}}async function nf(t,e){await wo(t);try{return await e()}finally{_o(t)}}var rf=["provider","model","dimensions","last_indexed","pending"];function sf(t,e){if(!t)throw new Error("writeMetadata: metadataPath is required");if(e==null||typeof e!="object")throw new Error("writeMetadata: data must be an object");let n={provider:e.provider===void 0?null:e.provider,model:e.model===void 0?null:e.model,dimensions:e.dimensions===void 0?null:e.dimensions,last_indexed:e.last_indexed===void 0?null:e.last_indexed,pending:Array.isArray(e.pending)?e.pending:[]},r=t+".tmp";z.writeFileSync(r,JSON.stringify(n,null,2)+` -`,"utf8"),z.renameSync(r,t)}function of(t){if(!t)throw new Error("readMetadata: metadataPath is required");if(!z.existsSync(t))throw new Error(`readMetadata: metadata file not found at ${t}`);let e;try{e=z.readFileSync(t,"utf8")}catch(r){throw new Error(`readMetadata: failed to read ${t}: ${r.message}`)}let n;try{n=JSON.parse(e)}catch(r){throw new Error(`readMetadata: invalid JSON at ${t}: ${r.message}`)}return n}So.exports={SCHEMA_FIELDS:Er,METADATA_FIELDS:rf,buildSchema:yo,createStore:qd,insertDocument:Vd,removeByIdentity:Wd,removeByFilter:mo,searchFulltext:Kd,searchVector:Hd,searchHybrid:Gd,saveStore:Yd,loadStore:Jd,acquireLock:wo,releaseLock:_o,withLock:nf,writeMetadata:sf,readMetadata:of}});var Ao=b((Zh,Eo)=>{"use strict";var cf=/^\s*(```+|~~~+)/,bo=/^---\s*$/;function uf(t,e){if(typeof t!="string")throw new TypeError("chunk: markdown must be a string");if(!e||typeof e!="object")throw new TypeError("chunk: config must be an object");let{primary_level:n=2,fallback_level:r=3,max_lines:s=200,keep_whole_below:i=50,special_sections:o={},strip_frontmatter:c=!0,skip_empty_sections:u=!0}=e,a=t.replace(/\r\n/g,` +`,ANSWER_SESSION_LAST_MESSAGE_IS_NOT_ASSISTANT:"The last message in the session is not an assistant message. Cannot regenerate non-assistant messages.",PLUGIN_COMPONENT_CONFLICT:'The component "%s" is already defined. The plugin "%s" is trying to redefine it.'}});function tn(t){return{raw:Number(t),formatted:oe(t)}}function nn(t){if(t.id){if(typeof t.id!="string")throw A("DOCUMENT_ID_MUST_BE_STRING",typeof t.id);return t.id}return Ie()}function dt(t,e){for(let[n,r]of Object.entries(e)){let s=t[n];if(!(typeof s>"u")&&!(r==="geopoint"&&typeof s=="object"&&typeof s.lon=="number"&&typeof s.lat=="number")&&!(r==="enum"&&(typeof s=="string"||typeof s=="number"))){if(r==="enum[]"&&Array.isArray(s)){let i=s.length;for(let o=0;o{R();O();O();Go={string:!1,number:!1,boolean:!1,enum:!1,geopoint:!1,"string[]":!0,"number[]":!0,"boolean[]":!0,"enum[]":!0},Yo={"string[]":"string","number[]":"number","boolean[]":"boolean","enum[]":"enum"}});var on={};ne(on,{createInternalDocumentIDStore:()=>sn,getDocumentIdFromInternalId:()=>V,getInternalDocumentId:()=>N,load:()=>Lr,save:()=>Rr});function sn(){return{idToInternalId:new Map,internalIdToId:[],save:Rr,load:Lr}}function Rr(t){return{internalIdToId:t.internalIdToId}}function Lr(t,e){let{internalIdToId:n}=e;t.internalDocumentIDStore.idToInternalId.clear(),t.internalDocumentIDStore.internalIdToId=[];let r=n.length;for(let s=0;st.internalIdToId.length?N(t,e.toString()):e}function V(t,e){if(t.internalIdToId.length{});var an={};ne(an,{count:()=>Wr,create:()=>Cr,createDocumentsStore:()=>cn,get:()=>$r,getAll:()=>Fr,getMultiple:()=>Br,load:()=>jr,remove:()=>Vr,save:()=>qr,store:()=>zr});function Cr(t,e){return{sharedInternalDocumentStore:e,docs:{},count:0}}function $r(t,e){let n=N(t.sharedInternalDocumentStore,e);return t.docs[n]}function Br(t,e){let n=e.length,r=Array.from({length:n});for(let s=0;s"u"?!1:(delete t.docs[n],t.count--,!0)}function Wr(t){return t.count}function jr(t,e){let n=e;return{docs:n.docs,count:n.count,sharedInternalDocumentStore:t}}function qr(t){return{docs:t.docs,count:t.count}}function cn(){return{create:Cr,get:$r,getMultiple:Br,getAll:Fr,store:zr,remove:Vr,count:Wr,load:jr,save:qr}}var ln=k(()=>{W()});function Gr(t,e){let n=[],r=t.plugins?.length;if(!r)return n;for(let s=0;s{R();Kr=["beforeInsert","afterInsert","beforeRemove","afterRemove","beforeUpdate","afterUpdate","beforeUpsert","afterUpsert","beforeSearch","afterSearch","beforeInsertMultiple","afterInsertMultiple","beforeRemoveMultiple","afterRemoveMultiple","beforeUpdateMultiple","afterUpdateMultiple","beforeUpsertMultiple","afterUpsertMultiple","beforeLoad","afterLoad","afterCreate"]});function P(t,e,n,r){if(t.some(b))return(async()=>{for(let i of t)await i(e,n,r)})();for(let i of t)i(e,n,r)}function L(t,e,n){if(t.some(b))return(async()=>{for(let s of t)await s(e,n)})();for(let s of t)s(e,n)}function Ae(t,e,n,r,s){if(t.some(b))return(async()=>{for(let o of t)await o(e,n,r,s)})();for(let o of t)o(e,n,r,s)}function Ee(t,e,n,r){if(t.some(b))return(async()=>{for(let i of t)await i(e,n,r)})();for(let i of t)i(e,n,r)}function Jr(t,e){if(t.some(b))return(async()=>{for(let r of t)await r(e)})();for(let r of t)r(e)}var Hr,un,re=k(()=>{O();Hr=["tokenizer","index","documentsStore","sorter","pinning"],un=["validateSchema","getDocumentIndexId","getDocumentProperties","formatElapsedTime"]});var me,$e,Xr=k(()=>{me=class t{k;v;l=null;r=null;h=1;constructor(e,n){this.k=e,this.v=new Set(n)}updateHeight(){this.h=Math.max(t.getHeight(this.l),t.getHeight(this.r))+1}static getHeight(e){return e?e.h:0}getBalanceFactor(){return t.getHeight(this.l)-t.getHeight(this.r)}rotateLeft(){let e=this.r;return this.r=e.l,e.l=this,this.updateHeight(),e.updateHeight(),e}rotateRight(){let e=this.l;return this.l=e.r,e.r=this,this.updateHeight(),e.updateHeight(),e}toJSON(){return{k:this.k,v:Array.from(this.v),l:this.l?this.l.toJSON():null,r:this.r?this.r.toJSON():null,h:this.h}}static fromJSON(e){let n=new t(e.k,e.v);return n.l=e.l?t.fromJSON(e.l):null,n.r=e.r?t.fromJSON(e.r):null,n.h=e.h,n}},$e=class t{root=null;insertCount=0;constructor(e,n){e!==void 0&&n!==void 0&&(this.root=new me(e,n))}insert(e,n,r=1e3){this.root=this.insertNode(this.root,e,n,r)}insertMultiple(e,n,r=1e3){for(let s of n)this.insert(e,s,r)}rebalance(){this.root&&(this.root=this.rebalanceNode(this.root))}toJSON(){return{root:this.root?this.root.toJSON():null,insertCount:this.insertCount}}static fromJSON(e){let n=new t;return n.root=e.root?me.fromJSON(e.root):null,n.insertCount=e.insertCount||0,n}insertNode(e,n,r,s){if(e===null)return new me(n,[r]);let i=[],o=e,c=null;for(;o!==null;)if(i.push({parent:c,node:o}),no.k)if(o.r===null){o.r=new me(n,[r]),i.push({parent:o,node:o.r});break}else c=o,o=o.r;else return o.v.add(r),e;let a=!1;this.insertCount++%s===0&&(a=!0);for(let l=i.length-1;l>=0;l--){let{parent:u,node:d}=i[l];if(d.updateHeight(),a){let f=this.rebalanceNode(d);u?u.l===d?u.l=f:u.r===d&&(u.r=f):e=f}}return e}rebalanceNode(e){let n=e.getBalanceFactor();if(n>1){if(e.l&&e.l.getBalanceFactor()>=0)return e.rotateRight();if(e.l)return e.l=e.l.rotateLeft(),e.rotateRight()}if(n<-1){if(e.r&&e.r.getBalanceFactor()<=0)return e.rotateLeft();if(e.r)return e.r=e.r.rotateRight(),e.rotateLeft()}return e}find(e){let n=this.findNodeByKey(e);return n?n.v:null}contains(e){return this.find(e)!==null}getSize(){let e=0,n=[],r=this.root;for(;r||n.length>0;){for(;r;)n.push(r),r=r.l;r=n.pop(),e++,r=r.r}return e}isBalanced(){if(!this.root)return!0;let e=[this.root];for(;e.length>0;){let n=e.pop(),r=n.getBalanceFactor();if(Math.abs(r)>1)return!1;n.l&&e.push(n.l),n.r&&e.push(n.r)}return!0}remove(e){this.root=this.removeNode(this.root,e)}removeDocument(e,n){let r=this.findNodeByKey(e);r&&(r.v.size===1?this.root=this.removeNode(this.root,e):r.v=new Set([...r.v.values()].filter(s=>s!==n)))}findNodeByKey(e){let n=this.root;for(;n;)if(en.k)n=n.r;else return n;return null}removeNode(e,n){if(e===null)return null;let r=[],s=e;for(;s!==null&&s.k!==n;)r.push(s),n=0;i--){let o=r[i];o.updateHeight();let c=this.rebalanceNode(o);if(i>0){let a=r[i-1];a.l===o?a.l=c:a.r===o&&(a.r=c)}else e=c}return e}rangeSearch(e,n){let r=new Set,s=[],i=this.root;for(;i||s.length>0;){for(;i;)s.push(i),i=i.l;if(i=s.pop(),i.k>=e&&i.k<=n)for(let o of i.v)r.add(o);if(i.k>n)break;i=i.r}return r}greaterThan(e,n=!1){let r=new Set,s=[],i=this.root;for(;i||s.length>0;){for(;i;)s.push(i),i=i.r;if(i=s.pop(),n&&i.k>=e||!n&&i.k>e)for(let o of i.v)r.add(o);else if(i.k<=e)break;i=i.l}return r}lessThan(e,n=!1){let r=new Set,s=[],i=this.root;for(;i||s.length>0;){for(;i;)s.push(i),i=i.l;if(i=s.pop(),n&&i.k<=e||!n&&i.ke)break;i=i.r}return r}}});var Be,Zr=k(()=>{Be=class t{numberToDocumentId;constructor(){this.numberToDocumentId=new Map}insert(e,n){this.numberToDocumentId.has(e)?this.numberToDocumentId.get(e).add(n):this.numberToDocumentId.set(e,new Set([n]))}find(e){let n=this.numberToDocumentId.get(e);return n?Array.from(n):null}remove(e){this.numberToDocumentId.delete(e)}removeDocument(e,n){let r=this.numberToDocumentId.get(n);r&&(r.delete(e),r.size===0&&this.numberToDocumentId.delete(n))}contains(e){return this.numberToDocumentId.has(e)}getSize(){let e=0;for(let n of this.numberToDocumentId.values())e+=n.size;return e}filter(e){let n=Object.keys(e);if(n.length!==1)throw new Error("Invalid operation");let r=n[0];switch(r){case"eq":{let s=e[r],i=this.numberToDocumentId.get(s);return i?Array.from(i):[]}case"in":{let s=e[r],i=new Set;for(let o of s){let c=this.numberToDocumentId.get(o);if(c)for(let a of c)i.add(a)}return Array.from(i)}case"nin":{let s=new Set(e[r]),i=new Set;for(let[o,c]of this.numberToDocumentId.entries())if(!s.has(o))for(let a of c)i.add(a);return Array.from(i)}default:throw new Error("Invalid operation")}}filterArr(e){let n=Object.keys(e);if(n.length!==1)throw new Error("Invalid operation");let r=n[0];switch(r){case"containsAll":{let i=e[r].map(c=>this.numberToDocumentId.get(c)??new Set);if(i.length===0)return[];let o=i.reduce((c,a)=>new Set([...c].filter(l=>a.has(l))));return Array.from(o)}case"containsAny":{let i=e[r].map(c=>this.numberToDocumentId.get(c)??new Set);if(i.length===0)return[];let o=i.reduce((c,a)=>new Set([...c,...a]));return Array.from(o)}default:throw new Error("Invalid operation")}}static fromJSON(e){if(!e.numberToDocumentId)throw new Error("Invalid Flat Tree JSON");let n=new t;for(let[r,s]of e.numberToDocumentId)n.numberToDocumentId.set(r,new Set(s));return n}toJSON(){return{numberToDocumentId:Array.from(this.numberToDocumentId.entries()).map(([e,n])=>[e,Array.from(n)])}}}});function Qr(t,e,n){if(n<0)return-1;if(t===e)return 0;let r=t.length,s=e.length;if(r===0)return s<=n?s:-1;if(s===0)return r<=n?r:-1;let i=Math.abs(r-s);if(t.startsWith(e))return i<=n?i:-1;if(e.startsWith(t))return 0;if(i>n)return-1;let o=[];for(let c=0;c<=r;c++){o[c]=[c];for(let a=1;a<=s;a++)o[c][a]=c===0?a:0}for(let c=1;c<=r;c++){let a=1/0;for(let l=1;l<=s;l++)t[c-1]===e[l-1]?o[c][l]=o[c-1][l-1]:o[c][l]=Math.min(o[c-1][l]+1,o[c][l-1]+1,o[c-1][l-1]+1),a=Math.min(a,o[c][l]);if(a>n)return-1}return o[r][s]<=n?o[r][s]:-1}function es(t,e,n){let r=Qr(t,e,n);return{distance:r,isBounded:r>=0}}function dn(t,e,n){let r=Qr(t,e,n);return{distance:r,isBounded:r>=0}}var fn=k(()=>{});var pt,Fe,ts=k(()=>{fn();O();pt=class t{k;s;c=new Map;d=new Set;e;w="";constructor(e,n,r){this.k=e,this.s=n,this.e=r}updateParent(e){this.w=e.w+this.s}addDocument(e){this.d.add(e)}removeDocument(e){return this.d.delete(e)}findAllWords(e,n,r,s){let i=[this];for(;i.length>0;){let o=i.pop();if(o.e){let{w:c,d:a}=o;if(r&&c!==n)continue;if(ct(e,c)!==null)if(s)if(Math.abs(n.length-c.length)<=s&&dn(n,c,s).isBounded)e[c]=[];else continue;else e[c]=[];if(ct(e,c)!=null&&a.size>0){let l=e[c];for(let u of a)l.includes(u)||l.push(u)}}o.c.size>0&&i.push(...o.c.values())}return e}insert(e,n){let r=this,s=0,i=e.length;for(;s0;){let{node:c,index:a,tolerance:l}=o.pop();if(c.w.startsWith(e)){c.findAllWords(i,e,!1,0);continue}if(l<0)continue;if(c.e){let{w:d,d:f}=c;if(d&&(dn(e,d,s).isBounded&&(i[d]=[]),ct(i,d)!==void 0&&f.size>0)){let h=new Set(i[d]);for(let p of f)h.add(p);i[d]=Array.from(h)}}if(a>=e.length)continue;let u=e[a];if(c.c.has(u)){let d=c.c.get(u);o.push({node:d,index:a+1,tolerance:l})}o.push({node:c,index:a+1,tolerance:l-1});for(let[d,f]of c.c)o.push({node:f,index:a,tolerance:l-1}),d!==u&&o.push({node:f,index:a+1,tolerance:l-1})}}find(e){let{term:n,exact:r,tolerance:s}=e;if(s&&!r){let i={};return this._findLevenshtein(n,0,s,s,i),i}else{let i=this,o=0,c=n.length;for(;o0&&n.c.size===0&&!n.e&&n.d.size===0;){let{parent:i,character:o}=s.pop();i.c.delete(o),n=i}return!0}removeDocumentByWord(e,n,r=!0){if(!e)return!0;let s=this,i=e.length;for(let o=0;o[e,n.toJSON()])}}static fromJSON(e){let n=new t(e.k,e.s,e.e);return n.w=e.w,n.d=new Set(e.d),n.c=new Map(e?.c?.map(([r,s])=>[r,t.fromJSON(s)])||[]),n}},Fe=class t extends pt{constructor(){super("","",!1)}static fromJSON(e){let n=new t;return n.w=e.w,n.s=e.s,n.e=e.e,n.k=e.k,n.d=new Set(e.d),n.c=new Map(e?.c?.map(([r,s])=>[r,pt.fromJSON(s)])||[]),n}toJSON(){return super.toJSON()}}});var mt,ce,ns=k(()=>{mt=class t{point;docIDs;left;right;parent;constructor(e,n){this.point=e,this.docIDs=new Set(n),this.left=null,this.right=null,this.parent=null}toJSON(){return{point:this.point,docIDs:Array.from(this.docIDs),left:this.left?this.left.toJSON():null,right:this.right?this.right.toJSON():null}}static fromJSON(e,n=null){let r=new t(e.point,e.docIDs);return r.parent=n,e.left&&(r.left=t.fromJSON(e.left,r)),e.right&&(r.right=t.fromJSON(e.right,r)),r}},ce=class t{root;nodeMap;constructor(){this.root=null,this.nodeMap=new Map}getPointKey(e){return`${e.lon},${e.lat}`}insert(e,n){let r=this.getPointKey(e),s=this.nodeMap.get(r);if(s){n.forEach(a=>s.docIDs.add(a));return}let i=new mt(e,n);if(this.nodeMap.set(r,i),this.root==null){this.root=i;return}let o=this.root,c=0;for(;;){if(c%2===0)if(e.lon0;){let{node:l,depth:u}=c.pop();if(l==null)continue;let d=o(e,l.point);(r?d<=n:d>n)&&a.push({point:l.point,docIDs:Array.from(l.docIDs)}),l.left!=null&&c.push({node:l.left,depth:u+1}),l.right!=null&&c.push({node:l.right,depth:u+1})}return s&&a.sort((l,u)=>{let d=o(e,l.point),f=o(e,u.point);return s.toLowerCase()==="asc"?d-f:f-d}),a}searchByPolygon(e,n=!0,r=null,s=!1){let i=[{node:this.root,depth:0}],o=[];for(;i.length>0;){let{node:a,depth:l}=i.pop();if(a==null)continue;a.left!=null&&i.push({node:a.left,depth:l+1}),a.right!=null&&i.push({node:a.right,depth:l+1});let u=t.isPointInPolygon(e,a.point);(u&&n||!u&&!n)&&o.push({point:a.point,docIDs:Array.from(a.docIDs)})}let c=t.calculatePolygonCentroid(e);if(r){let a=s?t.vincentyDistance:t.haversineDistance;o.sort((l,u)=>{let d=a(c,l.point),f=a(c,u.point);return r.toLowerCase()==="asc"?d-f:f-d})}return o}toJSON(){return{root:this.root?this.root.toJSON():null}}static fromJSON(e){let n=new t;return e.root&&(n.root=mt.fromJSON(e.root),n.buildNodeMap(n.root)),n}buildNodeMap(e){if(e==null)return;let n=this.getPointKey(e.point);this.nodeMap.set(n,e),e.left&&this.buildNodeMap(e.left),e.right&&this.buildNodeMap(e.right)}static calculatePolygonCentroid(e){let n=0,r=0,s=0,i=e.length;for(let c=0,a=i-1;ci!=f>i&&s<(d-l)*(i-u)/(f-u)+l&&(r=!r)}return r}static haversineDistance(e,n){let r=Math.PI/180,s=e.lat*r,i=n.lat*r,o=(n.lat-e.lat)*r,c=(n.lon-e.lon)*r,a=Math.sin(o/2)*Math.sin(o/2)+Math.cos(s)*Math.cos(i)*Math.sin(c/2)*Math.sin(c/2);return 6371e3*(2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a)))}static vincentyDistance(e,n){let s=.0033528106647474805,i=(1-s)*6378137,o=Math.PI/180,c=e.lat*o,a=n.lat*o,l=(n.lon-e.lon)*o,u=Math.atan((1-s)*Math.tan(c)),d=Math.atan((1-s)*Math.tan(a)),f=Math.sin(u),h=Math.cos(u),p=Math.sin(d),x=Math.cos(d),g=l,y,I=1e3,m,w,S,T,_,D;do{let we=Math.sin(g),Ue=Math.cos(g);if(m=Math.sqrt(x*we*(x*we)+(h*p-f*x*Ue)*(h*p-f*x*Ue)),m===0)return 0;w=f*p+h*x*Ue,S=Math.atan2(m,w),T=h*x*we/m,_=1-T*T,D=w-2*f*p/_,isNaN(D)&&(D=0);let Xt=s/16*_*(4+s*(4-3*_));y=g,g=l+(1-Xt)*s*T*(S+Xt*m*(D+Xt*w*(-1+2*D*D)))}while(Math.abs(g-y)>1e-12&&--I>0);if(I===0)return NaN;let M=_*(6378137*6378137-i*i)/(i*i),te=1+M/16384*(4096+M*(-768+M*(320-175*M))),J=M/1024*(256+M*(-128+M*(74-47*M))),Jt=J*m*(D+J/4*(w*(-1+2*D*D)-J/6*D*(-3+4*m*m)*(-3+4*D*D)));return i*te*(S-Jt)}}});var ze,rs=k(()=>{ze=class t{true;false;constructor(){this.true=new Set,this.false=new Set}insert(e,n){n?this.true.add(e):this.false.add(e)}delete(e,n){n?this.true.delete(e):this.false.delete(e)}getSize(){return this.true.size+this.false.size}toJSON(){return{true:Array.from(this.true),false:Array.from(this.false)}}static fromJSON(e){let n=new t;return n.true=new Set(e.true),n.false=new Set(e.false),n}}});function ss(t,e,n,r,s,{k:i,b:o,d:c}){return Math.log(1+(n-e+.5)/(e+.5))*(c+t*(i+1))/(t+i*(1-o+o*r/s))}var is=k(()=>{R()});function os(t,e){let n=0;for(let r=0;r=s&&o.push([a,h])}return o}var Ve,hn=k(()=>{Ve=class t{size;vectors=new Map;constructor(e){this.size=e}add(e,n){n instanceof Float32Array||(n=new Float32Array(n));let r=os(n,this.size);this.vectors.set(e,[r,n])}remove(e){this.vectors.delete(e)}find(e,n,r){return e instanceof Float32Array||(e=new Float32Array(e)),Ho(e,r,this.vectors,this.size,n)}toJSON(){let e=[];for(let[n,[r,s]]of this.vectors)e.push([n,[r,Array.from(s)]]);return{size:this.size,vectors:e}}static fromJSON(e){let n=e,r=new t(n.size);for(let[s,[i,o]]of n.vectors)r.vectors.set(s,[i,new Float32Array(o)]);return r}}});var wn={};ne(wn,{calculateResultScores:()=>mn,create:()=>pn,createIndex:()=>gn,getSearchableProperties:()=>ws,getSearchablePropertiesWithTypes:()=>xs,insert:()=>ps,insertDocumentScoreParameters:()=>us,insertTokenScoreParameters:()=>ds,insertVector:()=>ms,load:()=>Ss,remove:()=>gs,removeDocumentScoreParameters:()=>fs,removeTokenScoreParameters:()=>hs,save:()=>Is,search:()=>ys,searchByGeoWhereClause:()=>yn,searchByWhereClause:()=>We});function us(t,e,n,r,s){let i=N(t.sharedInternalDocumentStore,n);t.avgFieldLength[e]=((t.avgFieldLength[e]??0)*(s-1)+r.length)/s,t.fieldLengths[e][i]=r.length,t.frequencies[e][i]={}}function ds(t,e,n,r,s){let i=0;for(let a of r)a===s&&i++;let o=N(t.sharedInternalDocumentStore,n),c=i/r.length;t.frequencies[e][o][s]=c,s in t.tokenOccurrences[e]||(t.tokenOccurrences[e][s]=0),t.tokenOccurrences[e][s]=(t.tokenOccurrences[e][s]??0)+1}function fs(t,e,n,r){let s=N(t.sharedInternalDocumentStore,n);r>1?t.avgFieldLength[e]=(t.avgFieldLength[e]*r-t.fieldLengths[e][s])/(r-1):t.avgFieldLength[e]=void 0,t.fieldLengths[e][s]=void 0,t.frequencies[e][s]=void 0}function hs(t,e,n){t.tokenOccurrences[e][n]--}function pn(t,e,n,r,s=""){r||(r={sharedInternalDocumentStore:e,indexes:{},vectorIndexes:{},searchableProperties:[],searchablePropertiesWithTypes:{},frequencies:{},tokenOccurrences:{},avgFieldLength:{},fieldLengths:{}});for(let[i,o]of Object.entries(n)){let c=`${s}${s?".":""}${i}`;if(typeof o=="object"&&!Array.isArray(o)){pn(t,e,o,r,c);continue}if(X(o))r.searchableProperties.push(c),r.searchablePropertiesWithTypes[c]=o,r.vectorIndexes[c]={type:"Vector",node:new Ve(ht(o)),isArray:!1};else{let a=/\[/.test(o);switch(o){case"boolean":case"boolean[]":r.indexes[c]={type:"Bool",node:new ze,isArray:a};break;case"number":case"number[]":r.indexes[c]={type:"AVL",node:new $e(0,[]),isArray:a};break;case"string":case"string[]":r.indexes[c]={type:"Radix",node:new Fe,isArray:a},r.avgFieldLength[c]=0,r.frequencies[c]={},r.tokenOccurrences[c]={},r.fieldLengths[c]={};break;case"enum":case"enum[]":r.indexes[c]={type:"Flat",node:new Be,isArray:a};break;case"geopoint":r.indexes[c]={type:"BKD",node:new ce,isArray:a};break;default:throw A("INVALID_SCHEMA_TYPE",Array.isArray(o)?"array":o,c)}r.searchableProperties.push(c),r.searchablePropertiesWithTypes[c]=o}}return r}function Jo(t,e,n,r,s,i,o,c){return a=>{let{type:l,node:u}=e.indexes[n];switch(l){case"Bool":{u[a?"true":"false"].add(r);break}case"AVL":{let d=c?.avlRebalanceThreshold??1;u.insert(a,r,d);break}case"Radix":{let d=i.tokenize(a,s,n,!1);t.insertDocumentScoreParameters(e,n,r,d,o);for(let f of d)t.insertTokenScoreParameters(e,n,r,d,f),u.insert(f,r);break}case"Flat":{u.insert(a,r);break}case"BKD":{u.insert(a,[r]);break}}}}function ps(t,e,n,r,s,i,o,c,a,l,u){if(X(o))return ms(e,n,i,r,s);let d=Jo(t,e,n,s,c,a,l,u);if(!pe(o))return d(i);let f=i,h=f.length;for(let p=0;p0&&x.set(M,!0);let Jt=J.length;for(let it=0;it[m,w]).sort((m,w)=>w[1]-m[1]);if(y.length===0)return[];if(d===1)return y;if(d===0){if(h===1)return y;for(let w of f)if(!x.get(w))return[];return y.filter(([w])=>{let S=p.get(w);return S?Array.from(S.values()).some(T=>T===h):!1})}let I=y.filter(([m])=>{let w=p.get(m);return w?Array.from(w.values()).some(S=>S===h):!1});if(I.length>0){let m=y.filter(([S])=>!I.some(([T])=>T===S)),w=Math.ceil(m.length*d);return[...I,...m.slice(0,w)]}return y}function We(t,e,n,r){if("and"in n&&n.and&&Array.isArray(n.and)){let o=n.and;if(o.length===0)return new Set;let c=o.map(a=>We(t,e,a,r));return Le(...c)}if("or"in n&&n.or&&Array.isArray(n.or)){let o=n.or;return o.length===0?new Set:o.map(a=>We(t,e,a,r)).reduce((a,l)=>he(a,l),new Set)}if("not"in n&&n.not){let o=n.not,c=new Set,a=t.sharedInternalDocumentStore;for(let u=1;u<=a.internalIdToId.length;u++)c.add(u);let l=We(t,e,o,r);return ut(c,l)}let s=Object.keys(n),i=s.reduce((o,c)=>({[c]:new Set,...o}),{});for(let o of s){let c=n[o];if(typeof t.indexes[o]>"u")throw A("UNKNOWN_FILTER_PROPERTY",o);let{node:a,type:l,isArray:u}=t.indexes[o];if(l==="Bool"){let f=a,h=c?f.true:f.false;i[o]=he(i[o],h);continue}if(l==="BKD"){let f;if("radius"in c)f="radius";else if("polygon"in c)f="polygon";else throw new Error(`Invalid operation ${c}`);if(f==="radius"){let{value:h,coordinates:p,unit:x="m",inside:g=!0,highPrecision:y=!1}=c[f],I=Re(h,x),m=a.searchByRadius(p,I,g,void 0,y);i[o]=as(i[o],m)}else{let{coordinates:h,inside:p=!0,highPrecision:x=!1}=c[f],g=a.searchByPolygon(h,p,void 0,x);i[o]=as(i[o],g)}continue}if(l==="Radix"&&(typeof c=="string"||Array.isArray(c))){for(let f of[c].flat()){let h=e.tokenize(f,r,o);for(let p of h){let x=a.find({term:p,exact:!0});i[o]=Zo(i[o],x)}}continue}let d=Object.keys(c);if(d.length>1)throw A("INVALID_FILTER_OPERATION",d.length);if(l==="Flat"){let f=new Set(u?a.filterArr(c):a.filter(c));i[o]=he(i[o],f);continue}if(l==="AVL"){let f=d[0],h=c[f],p;switch(f){case"gt":{p=a.greaterThan(h,!1);break}case"gte":{p=a.greaterThan(h,!0);break}case"lt":{p=a.lessThan(h,!1);break}case"lte":{p=a.lessThan(h,!0);break}case"eq":{p=a.find(h)??new Set;break}case"between":{let[x,g]=h;p=a.rangeSearch(x,g);break}default:throw A("INVALID_FILTER_OPERATION",f)}i[o]=he(i[o],p)}}return Le(...Object.values(i))}function ws(t){return t.searchableProperties}function xs(t){return t.searchablePropertiesWithTypes}function Ss(t,e){let{indexes:n,vectorIndexes:r,searchableProperties:s,searchablePropertiesWithTypes:i,frequencies:o,tokenOccurrences:c,avgFieldLength:a,fieldLengths:l}=e,u={},d={};for(let f of Object.keys(n)){let{node:h,type:p,isArray:x}=n[f];switch(p){case"Radix":u[f]={type:"Radix",node:Fe.fromJSON(h),isArray:x};break;case"Flat":u[f]={type:"Flat",node:Be.fromJSON(h),isArray:x};break;case"AVL":u[f]={type:"AVL",node:$e.fromJSON(h),isArray:x};break;case"BKD":u[f]={type:"BKD",node:ce.fromJSON(h),isArray:x};break;case"Bool":u[f]={type:"Bool",node:ze.fromJSON(h),isArray:x};break;default:u[f]=n[f]}}for(let f of Object.keys(r))d[f]={type:"Vector",isArray:!1,node:Ve.fromJSON(r[f])};return{sharedInternalDocumentStore:t,indexes:u,vectorIndexes:d,searchableProperties:s,searchablePropertiesWithTypes:i,frequencies:o,tokenOccurrences:c,avgFieldLength:a,fieldLengths:l}}function Is(t){let{indexes:e,vectorIndexes:n,searchableProperties:r,searchablePropertiesWithTypes:s,frequencies:i,tokenOccurrences:o,avgFieldLength:c,fieldLengths:a}=t,l={};for(let d of Object.keys(n))l[d]=n[d].node.toJSON();let u={};for(let d of Object.keys(e)){let{type:f,node:h,isArray:p}=e[d];f==="Flat"||f==="Radix"||f==="AVL"||f==="BKD"||f==="Bool"?u[d]={type:f,node:h.toJSON(),isArray:p}:(u[d]=e[d],u[d].node=u[d].node.toJSON())}return{indexes:u,vectorIndexes:l,searchableProperties:r,searchablePropertiesWithTypes:s,frequencies:i,tokenOccurrences:o,avgFieldLength:c,fieldLengths:a}}function gn(){return{create:pn,insert:ps,remove:gs,insertDocumentScoreParameters:us,insertTokenScoreParameters:ds,removeDocumentScoreParameters:fs,removeTokenScoreParameters:hs,calculateResultScores:mn,search:ys,searchByWhereClause:We,getSearchableProperties:ws,getSearchablePropertiesWithTypes:xs,load:Ss,save:Is}}function as(t,e){t||(t=new Set);let n=e.length;for(let r=0;rl[1]-a[1]),s}function Xo(t,e){let n=Object.keys(t);if(n.length!==1)return{isGeoOnly:!1};let r=n[0],s=t[r];if(typeof e.indexes[r]>"u")return{isGeoOnly:!1};let{type:i}=e.indexes[r];return i==="BKD"&&s&&("radius"in s||"polygon"in s)?{isGeoOnly:!0,geoProperty:r,geoOperation:s}:{isGeoOnly:!1}}function yn(t,e){let n=t,r=Xo(e,n);if(!r.isGeoOnly||!r.geoProperty||!r.geoOperation)return null;let{node:s}=n.indexes[r.geoProperty],i=r.geoOperation,o=s,c;if("radius"in i){let{value:a,coordinates:l,unit:u="m",inside:d=!0,highPrecision:f=!1}=i.radius,h=l,p=Re(a,u);return c=o.searchByRadius(h,p,d,"asc",f),ls(c,h,f)}else if("polygon"in i){let{coordinates:a,inside:l=!0,highPrecision:u=!1}=i.polygon;c=o.searchByPolygon(a,l,"asc",u);let d=ce.calculatePolygonCentroid(a);return ls(c,d,u)}return null}function Zo(t,e){t||(t=new Set);let n=Object.keys(e),r=n.length;for(let s=0;s{R();Xr();Zr();ts();ns();rs();O();is();Ce();W();hn()});var In={};ne(In,{createSorter:()=>Sn,load:()=>Es,save:()=>ks});function bs(t,e,n,r,s){let i={language:t.tokenizer.language,sharedInternalDocumentStore:e,enabled:!0,isSorted:!0,sortableProperties:[],sortablePropertiesWithTypes:{},sorts:{}};for(let[o,c]of Object.entries(n)){let a=`${s}${s?".":""}${o}`;if(!r.includes(a)){if(typeof c=="object"&&!Array.isArray(c)){let l=bs(t,e,c,r,a);Se(i.sortableProperties,l.sortableProperties),i.sorts={...i.sorts,...l.sorts},i.sortablePropertiesWithTypes={...i.sortablePropertiesWithTypes,...l.sortablePropertiesWithTypes};continue}if(!X(c))switch(c){case"boolean":case"number":case"string":i.sortableProperties.push(a),i.sortablePropertiesWithTypes[a]=c,i.sorts[a]={docs:new Map,orderedDocsToRemove:new Map,orderedDocs:[],type:c};break;case"geopoint":case"enum":continue;case"enum[]":case"boolean[]":case"number[]":case"string[]":continue;default:throw A("INVALID_SORT_SCHEMA_TYPE",Array.isArray(c)?"array":c,a)}}}return i}function Qo(t,e,n,r){return r?.enabled!==!1?bs(t,e,n,(r||{}).unsortableProperties||[],""):{disabled:!0}}function ec(t,e,n,r){if(!t.enabled)return;t.isSorted=!1;let s=N(t.sharedInternalDocumentStore,n),i=t.sorts[e];i.orderedDocsToRemove.has(s)&&xn(t,e),i.docs.set(s,i.orderedDocs.length),i.orderedDocs.push([s,r])}function As(t){if(t.isSorted||!t.enabled)return;let e=Object.keys(t.sorts);for(let n of e)sc(t,n);t.isSorted=!0}function tc(t,e,n){return e[1].localeCompare(n[1],kr(t))}function nc(t,e){return t[1]-e[1]}function rc(t,e){return e[1]?-1:1}function sc(t,e){let n=t.sorts[e],r;switch(n.type){case"string":r=tc.bind(null,t.language);break;case"number":r=nc.bind(null);break;case"boolean":r=rc.bind(null);break}n.orderedDocs.sort(r);let s=n.orderedDocs.length;for(let i=0;i!n.orderedDocsToRemove.has(r[0])),n.orderedDocsToRemove.clear())}function oc(t,e,n){if(!t.enabled)return;let r=t.sorts[e],s=N(t.sharedInternalDocumentStore,n);r.docs.get(s)&&(r.docs.delete(s),r.orderedDocsToRemove.set(s,!0))}function cc(t,e,n){if(!t.enabled)throw A("SORT_DISABLED");let r=n.property,s=n.order==="DESC",i=t.sorts[r];if(!i)throw A("UNABLE_TO_SORT_ON_UNKNOWN_FIELD",r,t.sortableProperties.join(", "));return xn(t,r),As(t),e.sort((o,c)=>{let a=i.docs.get(N(t.sharedInternalDocumentStore,o[0])),l=i.docs.get(N(t.sharedInternalDocumentStore,c[0])),u=typeof a<"u",d=typeof l<"u";return!u&&!d?0:u?d?s?l-a:a-l:-1:1}),e}function ac(t){return t.enabled?t.sortableProperties:[]}function lc(t){return t.enabled?t.sortablePropertiesWithTypes:{}}function Es(t,e){let n=e;if(!n.enabled)return{enabled:!1};let r=Object.keys(n.sorts).reduce((s,i)=>{let{docs:o,orderedDocs:c,type:a}=n.sorts[i];return s[i]={docs:new Map(Object.entries(o).map(([l,u])=>[+l,u])),orderedDocsToRemove:new Map,orderedDocs:c,type:a},s},{});return{sharedInternalDocumentStore:t,language:n.language,sortableProperties:n.sortableProperties,sortablePropertiesWithTypes:n.sortablePropertiesWithTypes,sorts:r,enabled:!0,isSorted:n.isSorted}}function ks(t){if(!t.enabled)return{enabled:!1};ic(t),As(t);let e=Object.keys(t.sorts).reduce((n,r)=>{let{docs:s,orderedDocs:i,type:o}=t.sorts[r];return n[r]={docs:Object.fromEntries(s.entries()),orderedDocs:i,type:o},n},{});return{language:t.language,sortableProperties:t.sortableProperties,sortablePropertiesWithTypes:t.sortablePropertiesWithTypes,sorts:e,enabled:t.enabled,isSorted:t.isSorted}}function Sn(){return{create:Qo,insert:ec,remove:oc,save:ks,load:Es,sortBy:cc,getSortableProperties:ac,getSortablePropertiesWithTypes:lc}}var bn=k(()=>{R();Ce();W();O();ot()});function dc(t){return t<192||t>383?t:uc[t-192]||t}function vs(t){let e=[];for(let n=0;n{uc=[65,65,65,65,65,65,65,67,69,69,69,69,73,73,73,73,69,78,79,79,79,79,79,null,79,85,85,85,85,89,80,115,97,97,97,97,97,97,97,99,101,101,101,101,105,105,105,105,101,110,111,111,111,111,111,null,111,117,117,117,117,121,112,121,65,97,65,97,65,97,67,99,67,99,67,99,67,99,68,100,68,100,69,101,69,101,69,101,69,101,69,101,71,103,71,103,71,103,71,103,72,104,72,104,73,105,73,105,73,105,73,105,73,105,73,105,74,106,75,107,107,76,108,76,108,76,108,76,108,76,108,78,110,78,110,78,110,110,78,110,79,111,79,111,79,111,79,111,82,114,82,114,82,114,83,115,83,115,83,115,83,115,84,116,84,116,84,116,85,117,85,117,85,117,85,117,85,117,85,117,87,119,89,121,89,90,122,90,122,90,122,115]});function Ds(t){let e,n,r,s,i,o;if(t.length<3)return t;let c=t.substring(0,1);if(c=="y"&&(t=c.toUpperCase()+t.substring(1)),r=/^(.+?)(ss|i)es$/,s=/^(.+?)([^s])s$/,r.test(t)?t=t.replace(r,"$1$2"):s.test(t)&&(t=t.replace(s,"$1$2")),r=/^(.+?)eed$/,s=/^(.+?)(ed|ing)$/,r.test(t)){let a=r.exec(t);r=new RegExp(An),r.test(a[1])&&(r=/.$/,t=t.replace(r,""))}else s.test(t)&&(e=s.exec(t)[1],s=new RegExp(_s),s.test(e)&&(t=e,s=/(at|bl|iz)$/,i=new RegExp("([^aeiouylsz])\\1$"),o=new RegExp("^"+Z+wt+"[^aeiouwxy]$"),s.test(t)?t=t+"e":i.test(t)?(r=/.$/,t=t.replace(r,"")):o.test(t)&&(t=t+"e")));if(r=/^(.+?)y$/,r.test(t)&&(e=r.exec(t)?.[1],r=new RegExp(_s),e&&r.test(e)&&(t=e+"i")),r=/^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/,r.test(t)){let a=r.exec(t);e=a?.[1],n=a?.[2],r=new RegExp(An),e&&r.test(e)&&(t=e+fc[n])}if(r=/^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/,r.test(t)){let a=r.exec(t);e=a?.[1],n=a?.[2],r=new RegExp(An),e&&r.test(e)&&(t=e+hc[n])}if(r=/^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/,s=/^(.+?)(s|t)(ion)$/,r.test(t))e=r.exec(t)?.[1],r=new RegExp(yt),e&&r.test(e)&&(t=e);else if(s.test(t)){let a=s.exec(t);e=a?.[1]??""+a?.[2]??"",s=new RegExp(yt),s.test(e)&&(t=e)}return r=/^(.+?)e$/,r.test(t)&&(e=r.exec(t)?.[1],r=new RegExp(yt),s=new RegExp(mc),i=new RegExp("^"+Z+wt+"[^aeiouwxy]$"),e&&(r.test(e)||s.test(e)&&!i.test(e))&&(t=e)),r=/ll$/,s=new RegExp(yt),r.test(t)&&s.test(t)&&(r=/.$/,t=t.replace(r,"")),c=="y"&&(t=c.toLowerCase()+t.substring(1)),t}var fc,hc,pc,wt,Z,je,An,mc,yt,_s,Ms=k(()=>{fc={ational:"ate",tional:"tion",enci:"ence",anci:"ance",izer:"ize",bli:"ble",alli:"al",entli:"ent",eli:"e",ousli:"ous",ization:"ize",ation:"ate",ator:"ate",alism:"al",iveness:"ive",fulness:"ful",ousness:"ous",aliti:"al",iviti:"ive",biliti:"ble",logi:"log"},hc={icate:"ic",ative:"",alize:"al",iciti:"ic",ical:"ic",ful:"",ness:""},pc="[^aeiou]",wt="[aeiouy]",Z=pc+"[^aeiouy]*",je=wt+"[aeiou]*",An="^("+Z+")?"+je+Z,mc="^("+Z+")?"+je+Z+"("+je+")?$",yt="^("+Z+")?"+je+Z+je+Z,_s="^("+Z+")?"+wt});var En={};ne(En,{createTokenizer:()=>xt,normalizeToken:()=>qe});function qe(t,e,n=!0){let r=`${this.language}:${t}:${e}`;return n&&this.normalizationCache.has(r)?this.normalizationCache.get(r):this.stopWords?.includes(e)?(n&&this.normalizationCache.set(r,""),""):(this.stemmer&&!this.stemmerSkipProperties.has(t)&&(e=this.stemmer(e)),e=vs(e),n&&this.normalizationCache.set(r,e),e)}function gc(t){for(;t[t.length-1]==="";)t.pop();for(;t[0]==="";)t.shift();return t}function Ns(t,e,n,r=!0){if(e&&e!==this.language)throw A("LANGUAGE_NOT_SUPPORTED",e);if(typeof t!="string")return[t];let s=this.normalizeToken.bind(this,n??""),i;if(n&&this.tokenizeSkipProperties.has(n))i=[s(t,r)];else{let c=Er[this.language];i=t.toLowerCase().split(c).map(a=>s(a,r)).filter(Boolean)}let o=gc(i);return this.allowDuplicates?o:Array.from(new Set(o))}function xt(t={}){if(!t.language)t.language="english";else if(!Oe.includes(t.language))throw A("LANGUAGE_NOT_SUPPORTED",t.language);let e;if(t.stemming||t.stemmer&&!("stemming"in t))if(t.stemmer){if(typeof t.stemmer!="function")throw A("INVALID_STEMMER_FUNCTION_TYPE");e=t.stemmer}else if(t.language==="english")e=Ds;else throw A("MISSING_STEMMER",t.language);let n;if(t.stopWords!==!1){if(n=[],Array.isArray(t.stopWords))n=t.stopWords;else if(typeof t.stopWords=="function")n=t.stopWords(n);else if(t.stopWords)throw A("CUSTOM_STOP_WORDS_MUST_BE_FUNCTION_OR_ARRAY");if(!Array.isArray(n))throw A("CUSTOM_STOP_WORDS_MUST_BE_FUNCTION_OR_ARRAY");for(let s of n)if(typeof s!="string")throw A("CUSTOM_STOP_WORDS_MUST_BE_FUNCTION_OR_ARRAY")}let r={tokenize:Ns,language:t.language,stemmer:e,stemmerSkipProperties:new Set(t.stemmerSkipProperties?[t.stemmerSkipProperties].flat():[]),tokenizeSkipProperties:new Set(t.tokenizeSkipProperties?[t.tokenizeSkipProperties].flat():[]),stopWords:n,allowDuplicates:!!t.allowDuplicates,normalizeToken:qe,normalizationCache:new Map};return r.tokenize=Ns.bind(r),r.normalizeToken=qe,r}var St=k(()=>{R();Ts();ot();Ms()});function yc(t){return{sharedInternalDocumentStore:t,rules:new Map}}function wc(t,e){if(t.rules.has(e.id))throw new Error(`PINNING_RULE_ALREADY_EXISTS: A pinning rule with id "${e.id}" already exists. Use updateRule to modify it.`);t.rules.set(e.id,e)}function xc(t,e){if(!t.rules.has(e.id))throw new Error(`PINNING_RULE_NOT_FOUND: Cannot update pinning rule with id "${e.id}" because it does not exist. Use addRule to create it.`);t.rules.set(e.id,e)}function Sc(t,e){return t.rules.delete(e)}function Ic(t,e){return t.rules.get(e)}function bc(t){return Array.from(t.rules.values())}function Ac(t,e){let n=t.toLowerCase().trim(),r=e.pattern.toLowerCase().trim();switch(e.anchoring){case"is":return n===r;case"starts_with":return n.startsWith(r);case"contains":return n.includes(r);default:return!1}}function Ec(t,e){return t?e.conditions.every(n=>Ac(t,n)):!1}function kn(t,e){if(!e)return[];let n=[];for(let r of t.rules.values())Ec(e,r)&&n.push(r);return n}function kc(t,e){let n=e;return{sharedInternalDocumentStore:t,rules:new Map(n?.rules??[])}}function vc(t){return{rules:Array.from(t.rules.entries())}}function Us(){return{create:yc,addRule:wc,updateRule:xc,removeRule:Sc,getRule:Ic,getAllRules:bc,getMatchingRules:kn,load:kc,save:vc}}var vn=k(()=>{});function Tc(t){let e={formatElapsedTime:tn,getDocumentIndexId:nn,getDocumentProperties:Pe,validateSchema:dt};for(let n of un){let r=n;if(t[r]){if(typeof t[r]!="function")throw A("COMPONENT_MUST_BE_FUNCTION",r)}else t[r]=e[r]}for(let n of Object.keys(t))if(!Hr.includes(n)&&!un.includes(n))throw A("UNSUPPORTED_COMPONENT",n)}function Os({schema:t,sort:e,language:n,components:r,id:s,plugins:i}){r||(r={});for(let I of i??[]){if(!("getComponents"in I)||typeof I.getComponents!="function")continue;let m=I.getComponents(t),w=Object.keys(m);for(let S of w)if(r[S])throw A("PLUGIN_COMPONENT_CONFLICT",S,I.name);r={...r,...m}}s||(s=Ie());let o=r.tokenizer,c=r.index,a=r.documentsStore,l=r.sorter,u=r.pinning;if(o?o.tokenize?o=o:o=xt(o):o=xt({language:n??"english"}),r.tokenizer&&n)throw A("NO_LANGUAGE_WITH_CUSTOM_TOKENIZER");let d=sn();c||=gn(),l||=Sn(),a||=cn(),u||=Us(),Tc(r);let{getDocumentProperties:f,getDocumentIndexId:h,validateSchema:p,formatElapsedTime:x}=r,g={data:{},caches:{},schema:t,tokenizer:o,index:c,sorter:l,documentsStore:a,pinning:u,internalDocumentIDStore:d,getDocumentProperties:f,getDocumentIndexId:h,validateSchema:p,beforeInsert:[],afterInsert:[],beforeRemove:[],afterRemove:[],beforeUpdate:[],afterUpdate:[],beforeUpsert:[],afterUpsert:[],beforeSearch:[],afterSearch:[],beforeInsertMultiple:[],afterInsertMultiple:[],beforeRemoveMultiple:[],afterRemoveMultiple:[],beforeUpdateMultiple:[],afterUpdateMultiple:[],beforeUpsertMultiple:[],afterUpsertMultiple:[],afterCreate:[],formatElapsedTime:x,id:s,plugins:i,version:_c()};g.data={index:g.index.create(g,d,t),docs:g.documentsStore.create(g,d),sorting:g.sorter.create(g,d,t,e),pinning:g.pinning.create(d)};for(let I of Kr)g[I]=(g[I]??[]).concat(Gr(g,I));let y=g.afterCreate;return y&&Jr(y,g),g}function _c(){return"{{VERSION}}"}var Ps=k(()=>{Ce();ln();Yr();re();gt();W();bn();St();vn();R();O()});function Rs(t,e){return t.documentsStore.get(t.data.docs,e)}function It(t){return t.documentsStore.count(t.data.docs)}var Tn=k(()=>{});var _n={};ne(_n,{documentsStore:()=>an,formatElapsedTime:()=>tn,getDocumentIndexId:()=>nn,getDocumentProperties:()=>Pe,getInnerType:()=>ft,getVectorSize:()=>ht,index:()=>wn,internalDocumentIDStore:()=>on,isArrayType:()=>pe,isGeoPointType:()=>rn,isVectorType:()=>X,sorter:()=>In,tokenizer:()=>En,validateSchema:()=>dt});var Dn=k(()=>{Ce();ln();gt();St();bn();W()});function Q(t,e,n,r,s){let i=t.validateSchema(e,t.schema);if(i)throw A("SCHEMA_VALIDATION_FAILURE",i);return b(t.beforeInsert)||b(t.afterInsert)||b(t.index.beforeInsert)||b(t.index.insert)||b(t.index.afterInsert)?Nc(t,e,n,r,s):Uc(t,e,n,r,s)}async function Nc(t,e,n,r,s){let{index:i,docs:o}=t.data,c=t.getDocumentIndexId(e);if(typeof c!="string")throw A("DOCUMENT_ID_MUST_BE_STRING",typeof c);let a=N(t.internalDocumentIDStore,c);if(r||await P(t.beforeInsert,t,c,e),!t.documentsStore.store(o,c,a,e))throw A("DOCUMENT_ALREADY_EXISTS",c);let l=t.documentsStore.count(o),u=t.index.getSearchableProperties(i),d=t.index.getSearchablePropertiesWithTypes(i),f=t.getDocumentProperties(e,u);for(let[h,p]of Object.entries(f)){if(typeof p>"u")continue;let x=typeof p,g=d[h];Ls(x,g,h,p)}return await Oc(t,c,u,f,l,n,e,s),r||await P(t.afterInsert,t,c,e),c}function Uc(t,e,n,r,s){let{index:i,docs:o}=t.data,c=t.getDocumentIndexId(e);if(typeof c!="string")throw A("DOCUMENT_ID_MUST_BE_STRING",typeof c);let a=N(t.internalDocumentIDStore,c);if(r||P(t.beforeInsert,t,c,e),!t.documentsStore.store(o,c,a,e))throw A("DOCUMENT_ALREADY_EXISTS",c);let l=t.documentsStore.count(o),u=t.index.getSearchableProperties(i),d=t.index.getSearchablePropertiesWithTypes(i),f=t.getDocumentProperties(e,u);for(let[h,p]of Object.entries(f)){if(typeof p>"u")continue;let x=typeof p,g=d[h];Ls(x,g,h,p)}return Pc(t,c,u,f,l,n,e,s),r||P(t.afterInsert,t,c,e),c}function Ls(t,e,n,r){if(!(rn(e)&&typeof r=="object"&&typeof r.lon=="number"&&typeof r.lat=="number")&&!(X(e)&&Array.isArray(r))&&!(pe(e)&&Array.isArray(r))&&!(Dc.has(e)&&Mc.has(t))&&t!==e)throw A("INVALID_DOCUMENT_PROPERTY",n,e,t)}async function Oc(t,e,n,r,s,i,o,c){for(let u of n){let d=r[u];if(typeof d>"u")continue;let f=t.index.getSearchablePropertiesWithTypes(t.data.index)[u];await t.index.beforeInsert?.(t.data.index,u,e,d,f,i,t.tokenizer,s);let h=t.internalDocumentIDStore.idToInternalId.get(e);await t.index.insert(t.index,t.data.index,u,e,h,d,f,i,t.tokenizer,s,c),await t.index.afterInsert?.(t.data.index,u,e,d,f,i,t.tokenizer,s)}let a=t.sorter.getSortableProperties(t.data.sorting),l=t.getDocumentProperties(o,a);for(let u of a){let d=l[u];if(typeof d>"u")continue;let f=t.sorter.getSortablePropertiesWithTypes(t.data.sorting)[u];t.sorter.insert(t.data.sorting,u,e,d,f,i)}}function Pc(t,e,n,r,s,i,o,c){for(let u of n){let d=r[u];if(typeof d>"u")continue;let f=t.index.getSearchablePropertiesWithTypes(t.data.index)[u],h=N(t.internalDocumentIDStore,e);t.index.beforeInsert?.(t.data.index,u,e,d,f,i,t.tokenizer,s),t.index.insert(t.index,t.data.index,u,e,h,d,f,i,t.tokenizer,s,c),t.index.afterInsert?.(t.data.index,u,e,d,f,i,t.tokenizer,s)}let a=t.sorter.getSortableProperties(t.data.sorting),l=t.getDocumentProperties(o,a);for(let u of a){let d=l[u];if(typeof d>"u")continue;let f=t.sorter.getSortablePropertiesWithTypes(t.data.sorting)[u];t.sorter.insert(t.data.sorting,u,e,d,f,i)}}function Cs(t,e,n,r,s,i){return b(t.afterInsertMultiple)||b(t.beforeInsertMultiple)||b(t.index.beforeInsert)||b(t.index.insert)||b(t.index.afterInsert)?$s(t,e,n,r,s,i):Bs(t,e,n,r,s,i)}async function $s(t,e,n=1e3,r,s,i=0){let o=[],c=async l=>{let u=Math.min(l+n,e.length),d=e.slice(l,u);for(let f of d){let h={avlRebalanceThreshold:d.length},p=await Q(t,f,r,s,h);o.push(p)}return u};return await(async()=>{let l=0;for(;l0){let d=Date.now()-u,f=i-d;f>0&&en(f)}}})(),s||await L(t.afterInsertMultiple,t,e),o}function Bs(t,e,n=1e3,r,s,i=0){let o=[],c=0;function a(){let u=e.slice(c*n,(c+1)*n);if(u.length===0)return!1;for(let d of u){let f={avlRebalanceThreshold:u.length},h=Q(t,d,r,s,f);o.push(h)}return c++,!0}function l(){let u=Date.now();for(;a();)if(i>0){let f=Date.now()-u;if(f>=i){let h=i-f%i;h>0&&en(h)}}}return l(),s||L(t.afterInsertMultiple,t,e),o}function ke(t,e,n,r,s,i){return b(t.beforeInsert)||b(t.afterInsert)||b(t.index.beforeInsert)||b(t.index.insert)||b(t.index.afterInsert)?$s(t,e,n,r,s,i):Bs(t,e,n,r,s,i)}var Dc,Mc,bt=k(()=>{Dn();O();re();R();W();Dc=new Set(["enum","enum[]"]),Mc=new Set(["string","number"])});function Fs(t,e){t.pinning.addRule(t.data.pinning,e)}function zs(t,e){t.pinning.updateRule(t.data.pinning,e)}function Vs(t,e){return t.pinning.removeRule(t.data.pinning,e)}function Ws(t,e){return t.pinning.getRule(t.data.pinning,e)}function js(t){return t.pinning.getAllRules(t.data.pinning)}var qs=k(()=>{});function ge(t,e,n,r){return b(t.index.beforeRemove)||b(t.index.remove)||b(t.index.afterRemove)?Rc(t,e,n,r):Lc(t,e,n,r)}async function Rc(t,e,n,r){let s=!0,{index:i,docs:o}=t.data,c=t.documentsStore.get(o,e);if(!c)return!1;let a=N(t.internalDocumentIDStore,e),l=V(t.internalDocumentIDStore,a),u=t.documentsStore.count(o);r||await P(t.beforeRemove,t,l);let d=t.index.getSearchableProperties(i),f=t.index.getSearchablePropertiesWithTypes(i),h=t.getDocumentProperties(c,d);for(let g of d){let y=h[g];if(typeof y>"u")continue;let I=f[g];await t.index.beforeRemove?.(t.data.index,g,l,y,I,n,t.tokenizer,u),await t.index.remove(t.index,t.data.index,g,e,a,y,I,n,t.tokenizer,u)||(s=!1),await t.index.afterRemove?.(t.data.index,g,l,y,I,n,t.tokenizer,u)}let p=await t.sorter.getSortableProperties(t.data.sorting),x=await t.getDocumentProperties(c,p);for(let g of p)typeof x[g]>"u"||t.sorter.remove(t.data.sorting,g,e);return r||await P(t.afterRemove,t,l),t.documentsStore.remove(t.data.docs,e,a),s}function Lc(t,e,n,r){let s=!0,{index:i,docs:o}=t.data,c=t.documentsStore.get(o,e);if(!c)return!1;let a=N(t.internalDocumentIDStore,e),l=V(t.internalDocumentIDStore,a),u=t.documentsStore.count(o);r||P(t.beforeRemove,t,l);let d=t.index.getSearchableProperties(i),f=t.index.getSearchablePropertiesWithTypes(i),h=t.getDocumentProperties(c,d);for(let g of d){let y=h[g];if(typeof y>"u")continue;let I=f[g];t.index.beforeRemove?.(t.data.index,g,l,y,I,n,t.tokenizer,u),t.index.remove(t.index,t.data.index,g,e,a,y,I,n,t.tokenizer,u)||(s=!1),t.index.afterRemove?.(t.data.index,g,l,y,I,n,t.tokenizer,u)}let p=t.sorter.getSortableProperties(t.data.sorting),x=t.getDocumentProperties(c,p);for(let g of p)typeof x[g]>"u"||t.sorter.remove(t.data.sorting,g,e);return r||P(t.afterRemove,t,l),t.documentsStore.remove(t.data.docs,e,a),s}function Ke(t,e,n,r,s){return b(t.index.beforeRemove)||b(t.index.remove)||b(t.index.afterRemove)||b(t.beforeRemoveMultiple)||b(t.afterRemoveMultiple)?Cc(t,e,n,r,s):$c(t,e,n,r,s)}async function Cc(t,e,n,r,s){let i=0;n||(n=1e3);let o=s?[]:e.map(c=>V(t.internalDocumentIDStore,N(t.internalDocumentIDStore,c)));return s||await L(t.beforeRemoveMultiple,t,o),await new Promise((c,a)=>{let l=0;async function u(){let d=e.slice(l*n,++l*n);if(!d.length)return c();for(let f of d)try{await ge(t,f,r,s)&&i++}catch(h){a(h)}setTimeout(u,0)}setTimeout(u,0)}),s||await L(t.afterRemoveMultiple,t,o),i}function $c(t,e,n,r,s){let i=0;n||(n=1e3);let o=s?[]:e.map(l=>V(t.internalDocumentIDStore,N(t.internalDocumentIDStore,l)));s||L(t.beforeRemoveMultiple,t,o);let c=0;function a(){let l=e.slice(c*n,++c*n);if(l.length){for(let u of l)ge(t,u,r,s)&&i++;setTimeout(a,0)}}return a(),s||L(t.afterRemoveMultiple,t,o),i}var Mn=k(()=>{re();W();O()});var Ge,At,Et,Nn=k(()=>{Ge="fulltext",At="hybrid",Et="vector"});function Bc(t,e){return t[1]-e[1]}function Fc(t,e){return e[1]-t[1]}function zc(t="desc"){return t.toLowerCase()==="asc"?Bc:Fc}function ve(t,e,n){let r={},s=e.map(([l])=>l),i=t.documentsStore.getMultiple(t.data.docs,s),o=Object.keys(n),c=t.index.getSearchablePropertiesWithTypes(t.data.index);for(let l of o){let u;if(c[l]==="number"){let{ranges:d}=n[l],f=d.length,h=Array.from({length:f});for(let p=0;p{for(let s of t){let i=`${s.from}-${s.to}`;n?.has(i)||r>=s.from&&r<=s.to&&(e[i]===void 0?e[i]=1:(e[i]++,n?.add(i)))}}}function Gs(t,e,n){let r=e==="boolean"?"false":"";return s=>{let i=s?.toString()??r;n?.has(i)||(t[i]=(t[i]??0)+1,n?.add(i))}}var kt=k(()=>{R();O()});function Te(t,e,n){let r=n.properties,s=r.length,i=t.index.getSearchablePropertiesWithTypes(t.data.index);for(let y=0;y"u")throw A("UNKNOWN_GROUP_BY_PROPERTY",I);if(!Ys.includes(i[I]))throw A("INVALID_GROUP_BY_PROPERTY",I,Ys.join(", "),i[I])}let o=e.map(([y])=>V(t.internalDocumentIDStore,y)),c=t.documentsStore.getMultiple(t.data.docs,o),a=c.length,l=n.maxResult||Number.MAX_SAFE_INTEGER,u=[],d={};for(let y=0;y"u")continue;let D=typeof _!="boolean"?_:""+_,M=m.perValue[D]??{indexes:[],count:0};M.count>=l||(M.indexes.push(S),M.count++,m.perValue[D]=M,w.add(_))}u.push(Array.from(w)),d[I]=m}let f=Hs(u),h=f.length,p=[];for(let y=0;yT-_),w.indexes.length!==0&&p.push(w)}let x=p.length,g=Array.from({length:x});for(let y=0;y({id:o[D],score:e[D][1],document:c[D]})),S=m.reducer.bind(null,I.values),T=m.getInitialValue(I.indexes.length),_=w.reduce(S,T);g[y]={values:I.values,result:_}}return g}function Hs(t,e=0){if(e+1===t.length)return t[e].map(i=>[i]);let n=t[e],r=Hs(t,e+1),s=[];for(let i of n)for(let o of r){let c=[i];Se(c,o),s.push(c)}return s}var Vc,Ys,vt=k(()=>{R();O();W();Vc={reducer:(t,e,n,r)=>(e[r]=n,e),getInitialValue:t=>Array.from({length:t})},Ys=["string","number","boolean"]});function _e(t,e,n,r){let s=kn(e,r);if(s.length===0)return n;let i=s.flatMap(g=>g.consequence.promote);i.sort((g,y)=>g.position-y.position);let o=new Set,c=new Map,a=new Set;for(let g of i){let y=N(t.internalDocumentIDStore,g.doc_id);if(y!==void 0){if(c.has(y)){let I=c.get(y);g.position!o.has(g)),u=1e6,d=[];for(let[g,y]of c.entries())n.find(([m])=>m===g)?d.push([g,u-y]):t.documentsStore.get(t.data.docs,g)&&d.push([g,0]);d.sort((g,y)=>{let I=c.get(g[0])??1/0,m=c.get(y[0])??1/0;return I-m});let f=[],h=new Map;for(let g of d){let y=c.get(g[0]);h.set(y,g)}let p=0,x=0;for(;x=f.length&&f.push(y);return f}var Tt=k(()=>{W();vn()});function On(t,e,n){let{term:r,properties:s}=e,i=t.data.index,o=t.caches.propertiesToSearch;if(!o){let d=t.index.getSearchablePropertiesWithTypes(i);o=t.index.getSearchableProperties(i),o=o.filter(f=>d[f].startsWith("string")),t.caches.propertiesToSearch=o}if(s&&s!=="*"){for(let d of s)if(!o.includes(d))throw A("UNKNOWN_INDEX",d,o.join(", "));o=o.filter(d=>s.includes(d))}let c=Object.keys(e.where??{}).length>0,a;c&&(a=t.index.searchByWhereClause(i,t.tokenizer,e.where,n));let l,u=e.threshold!==void 0&&e.threshold!==null?e.threshold:1;if(r||s){let d=It(t);if(l=t.index.search(i,r||"",t.tokenizer,n,o,e.exact||!1,e.tolerance||0,e.boost||{},qc(e.relevance),d,a,u),e.exact&&r){let f=r.trim().split(/\s+/);l=l.filter(([h])=>{let p=t.documentsStore.get(t.data.docs,h);if(!p)return!1;for(let x of o){let g=jc(p,x);if(typeof g=="string"&&f.every(I=>new RegExp(`\\b${Wc(I)}\\b`).test(g)))return!0}return!1})}}else if(c){let d=yn(i,e.where);d?l=d:l=(a?Array.from(a):[]).map(h=>[+h,0])}else l=Object.keys(t.documentsStore.getAll(t.data.docs)).map(f=>[+f,0]);return l}function Wc(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function jc(t,e){let n=e.split("."),r=t;for(let s of n)if(r&&typeof r=="object"&&s in r)r=r[s];else return;return r}function Js(t,e,n){let r=K();function s(){let c=Object.keys(t.data.index.vectorIndexes),a=e.facets&&Object.keys(e.facets).length>0,{limit:l=10,offset:u=0,distinctOn:d,includeVectors:f=!1}=e,h=e.preflight===!0,p=On(t,e,n);if(e.sortBy)if(typeof e.sortBy=="function"){let y=p.map(([w])=>w),m=t.documentsStore.getMultiple(t.data.docs,y).map((w,S)=>[p[S][0],p[S][1],w]);m.sort(e.sortBy),p=m.map(([w,S])=>[w,S])}else p=t.sorter.sortBy(t.data.sorting,p,e.sortBy).map(([y,I])=>[N(t.internalDocumentIDStore,y),I]);else p=p.sort(at);p=_e(t,t.data.pinning,p,e.term);let x;h||(x=d?Xs(t,p,u,l,d):_t(t,p,u,l));let g={elapsed:{formatted:"",raw:0},hits:[],count:p.length};if(typeof x<"u"&&(g.hits=x.filter(Boolean),f||lt(g,c)),a){let y=ve(t,p,e.facets);g.facets=y}return e.groupBy&&(g.groups=Te(t,p,e.groupBy)),g.elapsed=t.formatElapsedTime(K()-r),g}async function i(){t.beforeSearch&&await Ee(t.beforeSearch,t,e,n);let c=s();return t.afterSearch&&await Ae(t.afterSearch,t,e,n,c),c}return t.beforeSearch?.length||t.afterSearch?.length?i():s()}function qc(t){let e=t??{};return e.k=e.k??Un.k,e.b=e.b??Un.b,e.d=e.d??Un.d,e}var Un,Pn=k(()=>{kt();vt();re();W();gt();Tt();R();O();Tn();Ye();Un={k:1.2,b:.75,d:.5}});function Rn(t,e,n){let r=e.vector;if(r&&(!("value"in r)||!("property"in r)))throw A("INVALID_VECTOR_INPUT",Object.keys(r).join(", "));let s=t.data.index.vectorIndexes[r.property];if(!s)throw A("UNKNOWN_VECTOR_PROPERTY",r.property);let i=s.node.size;if(r?.value.length!==i)throw r?.property===void 0||r?.value.length===void 0?A("INVALID_INPUT_VECTOR","undefined",i,"undefined"):A("INVALID_INPUT_VECTOR",r.property,i,r.value.length);let o=t.data.index,c;return Object.keys(e.where??{}).length>0&&(c=t.index.searchByWhereClause(o,t.tokenizer,e.where,n)),s.node.find(r.value,e.similarity??.8,c)}function Dt(t,e,n="english"){let r=K();function s(){let c=Rn(t,e,n).sort(at);c=_e(t,t.data.pinning,c,void 0);let a=[];e.facets&&Object.keys(e.facets).length>0&&(a=ve(t,c,e.facets));let u=e.vector.property,d=e.includeVectors??!1,f=e.limit??10,h=e.offset??0,p=Array.from({length:f});for(let I=0;I{O();kt();R();vt();W();re();hn();Tt()});function Gc(t,e,n){let r=Yc(On(t,e,n)),s=Rn(t,e,n),i=e.hybridWeights;return Jc(r,s,e.term??"",i)}function Qs(t,e,n){let r=K();function s(){let c=Gc(t,e,n);c=_e(t,t.data.pinning,c,e.term);let a;e.facets&&Object.keys(e.facets).length>0&&(a=ve(t,c,e.facets));let u;e.groupBy&&(u=Te(t,c,e.groupBy));let d=e.offset??0,f=e.limit??10,h=_t(t,c,d,f).filter(Boolean),p=K(),x={count:c.length,elapsed:{raw:Number(p-r),formatted:oe(p-r)},hits:h,...a?{facets:a}:{},...u?{groups:u}:{}};if(!(e.includeVectors??!1)){let y=Object.keys(t.data.index.vectorIndexes);lt(x,y)}return x}async function i(){t.beforeSearch&&await Ee(t.beforeSearch,t,e,n);let c=s();return t.afterSearch&&await Ae(t.afterSearch,t,e,n,c),c}return t.beforeSearch?.length||t.afterSearch?.length?i():s()}function Ln(t){return t[1]}function Yc(t){let e=Math.max.apply(Math,t.map(Ln));return t.map(([n,r])=>[n,r/e])}function Zs(t,e){return t/e}function Hc(t,e){return(n,r)=>n*t+r*e}function Jc(t,e,n,r){let s=Math.max.apply(Math,t.map(Ln)),i=Math.max.apply(Math,e.map(Ln)),o=r&&r.text&&r.vector,{text:c,vector:a}=o?r:Xc(n),l=new Map,u=t.length,d=Hc(c,a);for(let h=0;hp[1]-h[1])}function Xc(t){return{text:.5,vector:.5}}var ei=k(()=>{O();kt();vt();Ye();Pn();Mt();re();Tt()});function Nt(t,e,n){let r=e.mode??Ge;if(r===Ge)return Js(t,e,n);if(r===Et)return Dt(t,e);if(r===At)return Qs(t,e);throw A("INVALID_SEARCH_MODE",r)}function Xs(t,e,n,r,s){let i=t.data.docs,o=new Map,c=[],a=new Set,l=e.length,u=0;for(let d=0;d"u")continue;let[h,p]=f;if(a.has(h))continue;let x=t.documentsStore.get(i,h),g=be(x,s);if(!(typeof g>"u"||o.has(g))&&(o.set(g,!0),u++,!(u<=n)&&(c.push({id:V(t.internalDocumentIDStore,h),score:p,document:x}),a.add(h),u>=n+r)))break}return c}function _t(t,e,n,r){let s=t.data.docs,i=Array.from({length:r}),o=new Set;for(let c=n;c"u")break;let[l,u]=a;if(!o.has(l)){let d=t.documentsStore.get(s,l);i[c]={id:V(t.internalDocumentIDStore,l),score:u,document:d},o.add(l)}}return i}var Ye=k(()=>{W();R();O();Nn();Pn();Mt();ei()});function ti(t,e){t.internalDocumentIDStore.load(t,e.internalDocumentIDStore),t.data.index=t.index.load(t.internalDocumentIDStore,e.index),t.data.docs=t.documentsStore.load(t.internalDocumentIDStore,e.docs),t.data.sorting=t.sorter.load(t.internalDocumentIDStore,e.sorting),t.data.pinning=t.pinning.load(t.internalDocumentIDStore,e.pinning),t.tokenizer.language=e.language}function ni(t){return{internalDocumentIDStore:t.internalDocumentIDStore.save(t.internalDocumentIDStore),index:t.index.save(t.data.index),docs:t.documentsStore.save(t.data.docs),sorting:t.sorter.save(t.data.sorting),pinning:t.pinning.save(t.data.pinning),language:t.tokenizer.language}}var ri=k(()=>{});function He(t,e,n,r,s){return b(t.afterInsert)||b(t.beforeInsert)||b(t.afterRemove)||b(t.beforeRemove)||b(t.beforeUpdate)||b(t.afterUpdate)?Zc(t,e,n,r,s):Qc(t,e,n,r,s)}async function Zc(t,e,n,r,s){!s&&t.beforeUpdate&&await P(t.beforeUpdate,t,e),await ge(t,e,r,s);let i=await Q(t,n,r,s);return!s&&t.afterUpdate&&await P(t.afterUpdate,t,i),i}function Qc(t,e,n,r,s){!s&&t.beforeUpdate&&P(t.beforeUpdate,t,e),ge(t,e,r,s);let i=Q(t,n,r,s);return!s&&t.afterUpdate&&P(t.afterUpdate,t,i),i}function Je(t,e,n,r,s,i){return b(t.afterInsert)||b(t.beforeInsert)||b(t.afterRemove)||b(t.beforeRemove)||b(t.beforeUpdate)||b(t.afterUpdate)||b(t.beforeUpdateMultiple)||b(t.afterUpdateMultiple)||b(t.beforeRemoveMultiple)||b(t.afterRemoveMultiple)||b(t.beforeInsertMultiple)||b(t.afterInsertMultiple)?ea(t,e,n,r,s,i):ta(t,e,n,r,s,i)}async function ea(t,e,n,r,s,i){i||await L(t.beforeUpdateMultiple,t,e);let o=n.length;for(let a=0;a{re();R();bt();Mn();O()});function si(t,e,n,r,s){return b(t.afterInsert)||b(t.beforeInsert)||b(t.afterRemove)||b(t.beforeRemove)||b(t.beforeUpdate)||b(t.afterUpdate)||b(t.beforeUpsert)||b(t.afterUpsert)||b(t.index.beforeInsert)||b(t.index.insert)||b(t.index.afterInsert)?na(t,e,n,r,s):ra(t,e,n,r,s)}async function na(t,e,n,r,s){let i=t.getDocumentIndexId(e);if(typeof i!="string")throw A("DOCUMENT_ID_MUST_BE_STRING",typeof i);!r&&t.beforeUpsert&&await P(t.beforeUpsert,t,i,e);let o=t.documentsStore.get(t.data.docs,i),c;return o?c=await He(t,i,e,n,r):c=await Q(t,e,n,r,s),!r&&t.afterUpsert&&await P(t.afterUpsert,t,c,e),c}function ra(t,e,n,r,s){let i=t.getDocumentIndexId(e);if(typeof i!="string")throw A("DOCUMENT_ID_MUST_BE_STRING",typeof i);!r&&t.beforeUpsert&&P(t.beforeUpsert,t,i,e);let o=t.documentsStore.get(t.data.docs,i),c;return o?c=He(t,i,e,n,r):c=Q(t,e,n,r,s),!r&&t.afterUpsert&&P(t.afterUpsert,t,c,e),c}function ii(t,e,n,r,s){return b(t.afterInsert)||b(t.beforeInsert)||b(t.afterRemove)||b(t.beforeRemove)||b(t.beforeUpdate)||b(t.afterUpdate)||b(t.beforeUpsert)||b(t.afterUpsert)||b(t.beforeUpsertMultiple)||b(t.afterUpsertMultiple)||b(t.beforeInsertMultiple)||b(t.afterInsertMultiple)||b(t.beforeUpdateMultiple)||b(t.afterUpdateMultiple)||b(t.beforeRemoveMultiple)||b(t.afterRemoveMultiple)||b(t.index.beforeInsert)||b(t.index.insert)||b(t.index.afterInsert)?sa(t,e,n,r,s):ia(t,e,n,r,s)}async function sa(t,e,n,r,s){!s&&t.beforeUpsertMultiple&&await L(t.beforeUpsertMultiple,t,e);let i=e.length;for(let u=0;u0){let u=await Je(t,a,c,n,r,s);l.push(...u)}if(o.length>0){let u=await ke(t,o,n,r,s);l.push(...u)}return!s&&t.afterUpsertMultiple&&await L(t.afterUpsertMultiple,t,l),l}function ia(t,e,n,r,s){!s&&t.beforeUpsertMultiple&&L(t.beforeUpsertMultiple,t,e);let i=e.length;for(let u=0;u0){let u=Je(t,a,c,n,r,s);l.push(...u)}if(o.length>0){let u=ke(t,o,n,r,s);l.push(...u)}return!s&&t.afterUpsertMultiple&&L(t.afterUpsertMultiple,t,l),l}var oi=k(()=>{re();R();bt();Cn();O()});var oa,Ut,ci=k(()=>{R();Ye();oa="orama-secure-proxy",Ut=class{db;proxy=null;config;abortController=null;lastInteractionParams=null;chatModel=null;conversationID;messages=[];events;initPromise;state=[];constructor(e,n){this.db=e,this.config=n,this.init(),this.messages=n.initialMessages||[],this.events=n.events||{},this.conversationID=n.conversationID||this.generateRandomID()}async ask(e){await this.initPromise;let n="";for await(let r of await this.askStream(e))n+=r;return n}async askStream(e){return await this.initPromise,this.fetchAnswer(e)}abortAnswer(){this.abortController?.abort(),this.state[this.state.length-1].aborted=!0,this.triggerStateChange()}getMessages(){return this.messages}clearSession(){this.messages=[],this.state=[]}regenerateLast({stream:e=!0}){if(this.state.length===0||this.messages.length===0)throw new Error("No messages to regenerate");if(!(this.messages.at(-1)?.role==="assistant"))throw A("ANSWER_SESSION_LAST_MESSAGE_IS_NOT_ASSISTANT");return this.messages.pop(),this.state.pop(),e?this.askStream(this.lastInteractionParams):this.ask(this.lastInteractionParams)}async*fetchAnswer(e){if(!this.chatModel)throw A("PLUGIN_SECURE_PROXY_MISSING_CHAT_MODEL");this.abortController=new AbortController,this.lastInteractionParams=e;let n=this.generateRandomID();this.messages.push({role:"user",content:e.term??""}),this.state.push({interactionId:n,aborted:!1,loading:!0,query:e.term??"",response:"",sources:null,translatedQuery:null,error:!1,errorMessage:null});let r=this.state.length-1;this.addEmptyAssistantMessage(),this.triggerStateChange();try{let s=await Nt(this.db,e);this.state[r].sources=s,this.triggerStateChange();for await(let i of this.proxy.chatStream({model:this.chatModel,messages:this.messages}))yield i,this.state[r].response+=i,this.messages.findLast(o=>o.role==="assistant").content+=i,this.triggerStateChange()}catch(s){s.name==="AbortError"?this.state[r].aborted=!0:(this.state[r].error=!0,this.state[r].errorMessage=s.toString()),this.triggerStateChange()}return this.state[r].loading=!1,this.triggerStateChange(),this.state[r].response}generateRandomID(e=24){return Array.from({length:e},()=>Math.floor(Math.random()*36).toString(36)).join("")}triggerStateChange(){this.events.onStateChange&&this.events.onStateChange(this.state)}async init(){let e=this;async function n(){return await e.db.plugins.find(i=>i.name===oa)}let r=await n();if(!r)throw A("PLUGIN_SECURE_PROXY_NOT_FOUND");let s=r.extra;if(this.proxy=s.proxy,this.config.systemPrompt&&this.messages.push({role:"system",content:this.config.systemPrompt}),s?.pluginParams?.chat?.model)this.chatModel=s.pluginParams.chat.model;else throw A("PLUGIN_SECURE_PROXY_MISSING_CHAT_MODEL")}addEmptyAssistantMessage(){this.messages.push({role:"assistant",content:""})}}});var ca,aa,ai=k(()=>{Nn();ca=Symbol("orama.insertions"),aa=Symbol("orama.removals")});var $n={};ne($n,{boundedLevenshtein:()=>es,convertDistanceToMeters:()=>Re,formatBytes:()=>Or,formatNanoseconds:()=>oe,getNanosecondsTime:()=>K,normalizeToken:()=>qe,safeArrayPush:()=>Se,setDifference:()=>ut,setIntersection:()=>Le,setUnion:()=>he,uniqueId:()=>Ie});var li=k(()=>{fn();O();St()});var ui={};ne(ui,{AnswerSession:()=>Ut,MODE_FULLTEXT_SEARCH:()=>Ge,MODE_HYBRID_SEARCH:()=>At,MODE_VECTOR_SEARCH:()=>Et,components:()=>_n,count:()=>It,create:()=>Os,deletePin:()=>Vs,getAllPins:()=>js,getByID:()=>Rs,getPin:()=>Ws,insert:()=>Q,insertMultiple:()=>Cs,insertPin:()=>Fs,internals:()=>$n,kInsertions:()=>ca,kRemovals:()=>aa,load:()=>ti,remove:()=>ge,removeMultiple:()=>Ke,save:()=>ni,search:()=>Nt,searchVector:()=>Dt,update:()=>He,updateMultiple:()=>Je,updatePin:()=>zs,upsert:()=>si,upsertMultiple:()=>ii});var di=k(()=>{Ps();Tn();bt();qs();Mn();Ye();Mt();ri();Cn();oi();ci();ai();Dn();li()});function fi(t){let e=t.length,n=0,r=0;for(;r=55296&&s<=56319&&r>6&31|192;else{if(o>=55296&&o<=56319&&i>12&15|224,e[s++]=o>>6&63|128):(e[s++]=o>>18&7|240,e[s++]=o>>12&63|128,e[s++]=o>>6&63|128)}e[s++]=o&63|128}}function fa(t,e,n){ua.encodeInto(t,e.subarray(n))}function hi(t,e,n){t.length>da?fa(t,e,n):la(t,e,n)}function Bn(t,e,n){let r=e,s=r+n,i=[],o="";for(;r65535&&(d-=65536,i.push(d>>>10&1023|55296),d=56320|d&1023),i.push(d)}else i.push(c);i.length>=ha&&(o+=String.fromCharCode(...i),i.length=0)}return i.length>0&&(o+=String.fromCharCode(...i)),o}function ga(t,e,n){let r=t.subarray(e,e+n);return pa.decode(r)}function pi(t,e,n){return n>ma?ga(t,e,n):Bn(t,e,n)}var ua,da,ha,pa,ma,Ot=k(()=>{ua=new TextEncoder,da=50;ha=4096;pa=new TextDecoder,ma=200});var se,Fn=k(()=>{se=class{type;data;constructor(e,n){this.type=e,this.data=n}}});var B,Pt=k(()=>{B=class t extends Error{constructor(e){super(e);let n=Object.create(t.prototype);Object.setPrototypeOf(this,n),Object.defineProperty(this,"name",{configurable:!0,enumerable:!1,value:t.name})}}});function mi(t,e,n){let r=n/4294967296,s=n;t.setUint32(e,r),t.setUint32(e+4,s)}function Rt(t,e,n){let r=Math.floor(n/4294967296),s=n;t.setUint32(e,r),t.setUint32(e+4,s)}function Lt(t,e){let n=t.getInt32(e),r=t.getUint32(e+4);return n*4294967296+r}function gi(t,e){let n=t.getUint32(e),r=t.getUint32(e+4);return n*4294967296+r}var Ct=k(()=>{});function Vn({sec:t,nsec:e}){if(t>=0&&e>=0&&t<=wa)if(e===0&&t<=ya){let n=new Uint8Array(4);return new DataView(n.buffer).setUint32(0,t),n}else{let n=t/4294967296,r=t&4294967295,s=new Uint8Array(8),i=new DataView(s.buffer);return i.setUint32(0,e<<2|n&3),i.setUint32(4,r),s}else{let n=new Uint8Array(12),r=new DataView(n.buffer);return r.setUint32(0,e),Rt(r,4,t),n}}function Wn(t){let e=t.getTime(),n=Math.floor(e/1e3),r=(e-n*1e3)*1e6,s=Math.floor(r/1e9);return{sec:n+s,nsec:r-s*1e9}}function jn(t){if(t instanceof Date){let e=Wn(t);return Vn(e)}else return null}function qn(t){let e=new DataView(t.buffer,t.byteOffset,t.byteLength);switch(t.byteLength){case 4:return{sec:e.getUint32(0),nsec:0};case 8:{let n=e.getUint32(0),r=e.getUint32(4),s=(n&3)*4294967296+r,i=n>>>2;return{sec:s,nsec:i}}case 12:{let n=Lt(e,4),r=e.getUint32(0);return{sec:n,nsec:r}}default:throw new B(`Unrecognized data size for timestamp (expected 4, 8, or 12): ${t.length}`)}}function Kn(t){let e=qn(t);return new Date(e.sec*1e3+e.nsec/1e6)}var zn,ya,wa,yi,Gn=k(()=>{Pt();Ct();zn=-1,ya=4294967296-1,wa=17179869184-1;yi={type:zn,encode:jn,decode:Kn}});var ae,$t=k(()=>{Fn();Gn();ae=class t{static defaultCodec=new t;__brand;builtInEncoders=[];builtInDecoders=[];encoders=[];decoders=[];constructor(){this.register(yi)}register({type:e,encode:n,decode:r}){if(e>=0)this.encoders[e]=n,this.decoders[e]=r;else{let s=-1-e;this.builtInEncoders[s]=n,this.builtInDecoders[s]=r}}tryToEncode(e,n){for(let r=0;r{});var Sa,Ia,De,Hn=k(()=>{Ot();$t();Ct();Yn();Sa=100,Ia=2048,De=class t{extensionCodec;context;useBigInt64;maxDepth;initialBufferSize;sortKeys;forceFloat32;ignoreUndefined;forceIntegerToFloat;pos;view;bytes;entered=!1;constructor(e){this.extensionCodec=e?.extensionCodec??ae.defaultCodec,this.context=e?.context,this.useBigInt64=e?.useBigInt64??!1,this.maxDepth=e?.maxDepth??Sa,this.initialBufferSize=e?.initialBufferSize??Ia,this.sortKeys=e?.sortKeys??!1,this.forceFloat32=e?.forceFloat32??!1,this.ignoreUndefined=e?.ignoreUndefined??!1,this.forceIntegerToFloat=e?.forceIntegerToFloat??!1,this.pos=0,this.view=new DataView(new ArrayBuffer(this.initialBufferSize)),this.bytes=new Uint8Array(this.view.buffer)}clone(){return new t({extensionCodec:this.extensionCodec,context:this.context,useBigInt64:this.useBigInt64,maxDepth:this.maxDepth,initialBufferSize:this.initialBufferSize,sortKeys:this.sortKeys,forceFloat32:this.forceFloat32,ignoreUndefined:this.ignoreUndefined,forceIntegerToFloat:this.forceIntegerToFloat})}reinitializeState(){this.pos=0}encodeSharedRef(e){if(this.entered)return this.clone().encodeSharedRef(e);try{return this.entered=!0,this.reinitializeState(),this.doEncode(e,1),this.bytes.subarray(0,this.pos)}finally{this.entered=!1}}encode(e){if(this.entered)return this.clone().encode(e);try{return this.entered=!0,this.reinitializeState(),this.doEncode(e,1),this.bytes.slice(0,this.pos)}finally{this.entered=!1}}doEncode(e,n){if(n>this.maxDepth)throw new Error(`Too deep objects in depth ${n}`);e==null?this.encodeNil():typeof e=="boolean"?this.encodeBoolean(e):typeof e=="number"?this.forceIntegerToFloat?this.encodeNumberAsFloat(e):this.encodeNumber(e):typeof e=="string"?this.encodeString(e):this.useBigInt64&&typeof e=="bigint"?this.encodeBigInt64(e):this.encodeObject(e,n)}ensureBufferSizeToWrite(e){let n=this.pos+e;this.view.byteLength=0?e<128?this.writeU8(e):e<256?(this.writeU8(204),this.writeU8(e)):e<65536?(this.writeU8(205),this.writeU16(e)):e<4294967296?(this.writeU8(206),this.writeU32(e)):this.useBigInt64?this.encodeNumberAsFloat(e):(this.writeU8(207),this.writeU64(e)):e>=-32?this.writeU8(224|e+32):e>=-128?(this.writeU8(208),this.writeI8(e)):e>=-32768?(this.writeU8(209),this.writeI16(e)):e>=-2147483648?(this.writeU8(210),this.writeI32(e)):this.useBigInt64?this.encodeNumberAsFloat(e):(this.writeU8(211),this.writeI64(e)):this.encodeNumberAsFloat(e)}encodeNumberAsFloat(e){this.forceFloat32?(this.writeU8(202),this.writeF32(e)):(this.writeU8(203),this.writeF64(e))}encodeBigInt64(e){e>=BigInt(0)?(this.writeU8(207),this.writeBigUint64(e)):(this.writeU8(211),this.writeBigInt64(e))}writeStringHeader(e){if(e<32)this.writeU8(160+e);else if(e<256)this.writeU8(217),this.writeU8(e);else if(e<65536)this.writeU8(218),this.writeU16(e);else if(e<4294967296)this.writeU8(219),this.writeU32(e);else throw new Error(`Too long string: ${e} bytes in UTF-8`)}encodeString(e){let r=fi(e);this.ensureBufferSizeToWrite(5+r),this.writeStringHeader(r),hi(e,this.bytes,this.pos),this.pos+=r}encodeObject(e,n){let r=this.extensionCodec.tryToEncode(e,this.context);if(r!=null)this.encodeExtension(r);else if(Array.isArray(e))this.encodeArray(e,n);else if(ArrayBuffer.isView(e))this.encodeBinary(e);else if(typeof e=="object")this.encodeMap(e,n);else throw new Error(`Unrecognized object: ${Object.prototype.toString.apply(e)}`)}encodeBinary(e){let n=e.byteLength;if(n<256)this.writeU8(196),this.writeU8(n);else if(n<65536)this.writeU8(197),this.writeU16(n);else if(n<4294967296)this.writeU8(198),this.writeU32(n);else throw new Error(`Too large binary: ${n}`);let r=Xe(e);this.writeU8a(r)}encodeArray(e,n){let r=e.length;if(r<16)this.writeU8(144+r);else if(r<65536)this.writeU8(220),this.writeU16(r);else if(r<4294967296)this.writeU8(221),this.writeU32(r);else throw new Error(`Too large array: ${r}`);for(let s of e)this.doEncode(s,n+1)}countWithoutUndefined(e,n){let r=0;for(let s of n)e[s]!==void 0&&r++;return r}encodeMap(e,n){let r=Object.keys(e);this.sortKeys&&r.sort();let s=this.ignoreUndefined?this.countWithoutUndefined(e,r):r.length;if(s<16)this.writeU8(128+s);else if(s<65536)this.writeU8(222),this.writeU16(s);else if(s<4294967296)this.writeU8(223),this.writeU32(s);else throw new Error(`Too large map object: ${s}`);for(let i of r){let o=e[i];this.ignoreUndefined&&o===void 0||(this.encodeString(i),this.doEncode(o,n+1))}}encodeExtension(e){if(typeof e.data=="function"){let r=e.data(this.pos+6),s=r.length;if(s>=4294967296)throw new Error(`Too large extension object: ${s}`);this.writeU8(201),this.writeU32(s),this.writeI8(e.type),this.writeU8a(r);return}let n=e.data.length;if(n===1)this.writeU8(212);else if(n===2)this.writeU8(213);else if(n===4)this.writeU8(214);else if(n===8)this.writeU8(215);else if(n===16)this.writeU8(216);else if(n<256)this.writeU8(199),this.writeU8(n);else if(n<65536)this.writeU8(200),this.writeU16(n);else if(n<4294967296)this.writeU8(201),this.writeU32(n);else throw new Error(`Too large extension object: ${n}`);this.writeI8(e.type),this.writeU8a(e.data)}writeU8(e){this.ensureBufferSizeToWrite(1),this.view.setUint8(this.pos,e),this.pos++}writeU8a(e){let n=e.length;this.ensureBufferSizeToWrite(n),this.bytes.set(e,this.pos),this.pos+=n}writeI8(e){this.ensureBufferSizeToWrite(1),this.view.setInt8(this.pos,e),this.pos++}writeU16(e){this.ensureBufferSizeToWrite(2),this.view.setUint16(this.pos,e),this.pos+=2}writeI16(e){this.ensureBufferSizeToWrite(2),this.view.setInt16(this.pos,e),this.pos+=2}writeU32(e){this.ensureBufferSizeToWrite(4),this.view.setUint32(this.pos,e),this.pos+=4}writeI32(e){this.ensureBufferSizeToWrite(4),this.view.setInt32(this.pos,e),this.pos+=4}writeF32(e){this.ensureBufferSizeToWrite(4),this.view.setFloat32(this.pos,e),this.pos+=4}writeF64(e){this.ensureBufferSizeToWrite(8),this.view.setFloat64(this.pos,e),this.pos+=8}writeU64(e){this.ensureBufferSizeToWrite(8),mi(this.view,this.pos,e),this.pos+=8}writeI64(e){this.ensureBufferSizeToWrite(8),Rt(this.view,this.pos,e),this.pos+=8}writeBigUint64(e){this.ensureBufferSizeToWrite(8),this.view.setBigUint64(this.pos,e),this.pos+=8}writeBigInt64(e){this.ensureBufferSizeToWrite(8),this.view.setBigInt64(this.pos,e),this.pos+=8}}});function wi(t,e){return new De(e).encodeSharedRef(t)}var xi=k(()=>{Hn()});function Bt(t){return`${t<0?"-":""}0x${Math.abs(t).toString(16).padStart(2,"0")}`}var Si=k(()=>{});var ba,Aa,Ft,Ii=k(()=>{Ot();ba=16,Aa=16,Ft=class{hit=0;miss=0;caches;maxKeyLength;maxLengthPerKey;constructor(e=ba,n=Aa){this.maxKeyLength=e,this.maxLengthPerKey=n,this.caches=[];for(let r=0;r0&&e<=this.maxKeyLength}find(e,n,r){let s=this.caches[r-1];e:for(let i of s){let o=i.bytes;for(let c=0;c=this.maxLengthPerKey?r[Math.random()*r.length|0]=s:r.push(s)}decode(e,n,r){let s=this.find(e,n,r);if(s!=null)return this.hit++,s;this.miss++;let i=Bn(e,n,r),o=Uint8Array.prototype.slice.call(e,n,n+r);return this.store(o,i),i}}});var Jn,et,Ai,Ea,Xn,Qe,Zn,ka,bi,va,G,zt=k(()=>{Si();$t();Ct();Ot();Yn();Ii();Pt();Jn="array",et="map_key",Ai="map_value",Ea=t=>{if(typeof t=="string"||typeof t=="number")return t;throw new B("The type of key must be string or number but "+typeof t)},Xn=class{stack=[];stackHeadPosition=-1;get length(){return this.stackHeadPosition+1}top(){return this.stack[this.stackHeadPosition]}pushArrayState(e){let n=this.getUninitializedStateFromPool();n.type=Jn,n.position=0,n.size=e,n.array=new Array(e)}pushMapState(e){let n=this.getUninitializedStateFromPool();n.type=et,n.readCount=0,n.size=e,n.map={}}getUninitializedStateFromPool(){if(this.stackHeadPosition++,this.stackHeadPosition===this.stack.length){let e={type:void 0,size:0,array:void 0,position:0,readCount:0,map:void 0,key:null};this.stack.push(e)}return this.stack[this.stackHeadPosition]}release(e){if(this.stack[this.stackHeadPosition]!==e)throw new Error("Invalid stack state. Released state is not on top of the stack.");if(e.type===Jn){let r=e;r.size=0,r.array=void 0,r.position=0,r.type=void 0}if(e.type===et||e.type===Ai){let r=e;r.size=0,r.map=void 0,r.readCount=0,r.type=void 0}this.stackHeadPosition--}reset(){this.stack.length=0,this.stackHeadPosition=-1}},Qe=-1,Zn=new DataView(new ArrayBuffer(0)),ka=new Uint8Array(Zn.buffer);try{Zn.getInt8(0)}catch(t){if(!(t instanceof RangeError))throw new Error("This module is not supported in the current JavaScript engine because DataView does not throw RangeError on out-of-bounds access")}bi=new RangeError("Insufficient data"),va=new Ft,G=class t{extensionCodec;context;useBigInt64;rawStrings;maxStrLength;maxBinLength;maxArrayLength;maxMapLength;maxExtLength;keyDecoder;mapKeyConverter;totalPos=0;pos=0;view=Zn;bytes=ka;headByte=Qe;stack=new Xn;entered=!1;constructor(e){this.extensionCodec=e?.extensionCodec??ae.defaultCodec,this.context=e?.context,this.useBigInt64=e?.useBigInt64??!1,this.rawStrings=e?.rawStrings??!1,this.maxStrLength=e?.maxStrLength??4294967295,this.maxBinLength=e?.maxBinLength??4294967295,this.maxArrayLength=e?.maxArrayLength??4294967295,this.maxMapLength=e?.maxMapLength??4294967295,this.maxExtLength=e?.maxExtLength??4294967295,this.keyDecoder=e?.keyDecoder!==void 0?e.keyDecoder:va,this.mapKeyConverter=e?.mapKeyConverter??Ea}clone(){return new t({extensionCodec:this.extensionCodec,context:this.context,useBigInt64:this.useBigInt64,rawStrings:this.rawStrings,maxStrLength:this.maxStrLength,maxBinLength:this.maxBinLength,maxArrayLength:this.maxArrayLength,maxMapLength:this.maxMapLength,maxExtLength:this.maxExtLength,keyDecoder:this.keyDecoder})}reinitializeState(){this.totalPos=0,this.headByte=Qe,this.stack.reset()}setBuffer(e){let n=Xe(e);this.bytes=n,this.view=new DataView(n.buffer,n.byteOffset,n.byteLength),this.pos=0}appendBuffer(e){if(this.headByte===Qe&&!this.hasRemaining(1))this.setBuffer(e);else{let n=this.bytes.subarray(this.pos),r=Xe(e),s=new Uint8Array(n.length+r.length);s.set(n),s.set(r,n.length),this.setBuffer(s)}}hasRemaining(e){return this.view.byteLength-this.pos>=e}createExtraByteError(e){let{view:n,pos:r}=this;return new RangeError(`Extra ${n.byteLength-r} of ${n.byteLength} byte(s) found at buffer[${e}]`)}decode(e){if(this.entered)return this.clone().decode(e);try{this.entered=!0,this.reinitializeState(),this.setBuffer(e);let n=this.doDecodeSync();if(this.hasRemaining(1))throw this.createExtraByteError(this.pos);return n}finally{this.entered=!1}}*decodeMulti(e){if(this.entered){yield*this.clone().decodeMulti(e);return}try{for(this.entered=!0,this.reinitializeState(),this.setBuffer(e);this.hasRemaining(1);)yield this.doDecodeSync()}finally{this.entered=!1}}async decodeAsync(e){if(this.entered)return this.clone().decodeAsync(e);try{this.entered=!0;let n=!1,r;for await(let c of e){if(n)throw this.entered=!1,this.createExtraByteError(this.totalPos);this.appendBuffer(c);try{r=this.doDecodeSync(),n=!0}catch(a){if(!(a instanceof RangeError))throw a}this.totalPos+=this.pos}if(n){if(this.hasRemaining(1))throw this.createExtraByteError(this.totalPos);return r}let{headByte:s,pos:i,totalPos:o}=this;throw new RangeError(`Insufficient data in parsing ${Bt(s)} at ${o} (${i} in the current buffer)`)}finally{this.entered=!1}}decodeArrayStream(e){return this.decodeMultiAsync(e,!0)}decodeStream(e){return this.decodeMultiAsync(e,!1)}async*decodeMultiAsync(e,n){if(this.entered){yield*this.clone().decodeMultiAsync(e,n);return}try{this.entered=!0;let r=n,s=-1;for await(let i of e){if(n&&s===0)throw this.createExtraByteError(this.totalPos);this.appendBuffer(i),r&&(s=this.readArraySize(),r=!1,this.complete());try{for(;yield this.doDecodeSync(),--s!==0;);}catch(o){if(!(o instanceof RangeError))throw o}this.totalPos+=this.pos}}finally{this.entered=!1}}doDecodeSync(){e:for(;;){let e=this.readHeadByte(),n;if(e>=224)n=e-256;else if(e<192)if(e<128)n=e;else if(e<144){let s=e-128;if(s!==0){this.pushMapState(s),this.complete();continue e}else n={}}else if(e<160){let s=e-144;if(s!==0){this.pushArrayState(s),this.complete();continue e}else n=[]}else{let s=e-160;n=this.decodeString(s,0)}else if(e===192)n=null;else if(e===194)n=!1;else if(e===195)n=!0;else if(e===202)n=this.readF32();else if(e===203)n=this.readF64();else if(e===204)n=this.readU8();else if(e===205)n=this.readU16();else if(e===206)n=this.readU32();else if(e===207)this.useBigInt64?n=this.readU64AsBigInt():n=this.readU64();else if(e===208)n=this.readI8();else if(e===209)n=this.readI16();else if(e===210)n=this.readI32();else if(e===211)this.useBigInt64?n=this.readI64AsBigInt():n=this.readI64();else if(e===217){let s=this.lookU8();n=this.decodeString(s,1)}else if(e===218){let s=this.lookU16();n=this.decodeString(s,2)}else if(e===219){let s=this.lookU32();n=this.decodeString(s,4)}else if(e===220){let s=this.readU16();if(s!==0){this.pushArrayState(s),this.complete();continue e}else n=[]}else if(e===221){let s=this.readU32();if(s!==0){this.pushArrayState(s),this.complete();continue e}else n=[]}else if(e===222){let s=this.readU16();if(s!==0){this.pushMapState(s),this.complete();continue e}else n={}}else if(e===223){let s=this.readU32();if(s!==0){this.pushMapState(s),this.complete();continue e}else n={}}else if(e===196){let s=this.lookU8();n=this.decodeBinary(s,1)}else if(e===197){let s=this.lookU16();n=this.decodeBinary(s,2)}else if(e===198){let s=this.lookU32();n=this.decodeBinary(s,4)}else if(e===212)n=this.decodeExtension(1,0);else if(e===213)n=this.decodeExtension(2,0);else if(e===214)n=this.decodeExtension(4,0);else if(e===215)n=this.decodeExtension(8,0);else if(e===216)n=this.decodeExtension(16,0);else if(e===199){let s=this.lookU8();n=this.decodeExtension(s,1)}else if(e===200){let s=this.lookU16();n=this.decodeExtension(s,2)}else if(e===201){let s=this.lookU32();n=this.decodeExtension(s,4)}else throw new B(`Unrecognized type byte: ${Bt(e)}`);this.complete();let r=this.stack;for(;r.length>0;){let s=r.top();if(s.type===Jn)if(s.array[s.position]=n,s.position++,s.position===s.size)n=s.array,r.release(s);else continue e;else if(s.type===et){if(n==="__proto__")throw new B("The key __proto__ is not allowed");s.key=this.mapKeyConverter(n),s.type=Ai;continue e}else if(s.map[s.key]=n,s.readCount++,s.readCount===s.size)n=s.map,r.release(s);else{s.key=null,s.type=et;continue e}}return n}}readHeadByte(){return this.headByte===Qe&&(this.headByte=this.readU8()),this.headByte}complete(){this.headByte=Qe}readArraySize(){let e=this.readHeadByte();switch(e){case 220:return this.readU16();case 221:return this.readU32();default:{if(e<160)return e-144;throw new B(`Unrecognized array type byte: ${Bt(e)}`)}}}pushMapState(e){if(e>this.maxMapLength)throw new B(`Max length exceeded: map length (${e}) > maxMapLengthLength (${this.maxMapLength})`);this.stack.pushMapState(e)}pushArrayState(e){if(e>this.maxArrayLength)throw new B(`Max length exceeded: array length (${e}) > maxArrayLength (${this.maxArrayLength})`);this.stack.pushArrayState(e)}decodeString(e,n){return!this.rawStrings||this.stateIsMapKey()?this.decodeUtf8String(e,n):this.decodeBinary(e,n)}decodeUtf8String(e,n){if(e>this.maxStrLength)throw new B(`Max length exceeded: UTF-8 byte length (${e}) > maxStrLength (${this.maxStrLength})`);if(this.bytes.byteLength0?this.stack.top().type===et:!1}decodeBinary(e,n){if(e>this.maxBinLength)throw new B(`Max length exceeded: bin length (${e}) > maxBinLength (${this.maxBinLength})`);if(!this.hasRemaining(e+n))throw bi;let r=this.pos+n,s=this.bytes.subarray(r,r+e);return this.pos+=n+e,s}decodeExtension(e,n){if(e>this.maxExtLength)throw new B(`Max length exceeded: ext length (${e}) > maxExtLength (${this.maxExtLength})`);let r=this.view.getInt8(this.pos+n),s=this.decodeBinary(e,n+1);return this.extensionCodec.decode(s,r,this.context)}lookU8(){return this.view.getUint8(this.pos)}lookU16(){return this.view.getUint16(this.pos)}lookU32(){return this.view.getUint32(this.pos)}readU8(){let e=this.view.getUint8(this.pos);return this.pos++,e}readI8(){let e=this.view.getInt8(this.pos);return this.pos++,e}readU16(){let e=this.view.getUint16(this.pos);return this.pos+=2,e}readI16(){let e=this.view.getInt16(this.pos);return this.pos+=2,e}readU32(){let e=this.view.getUint32(this.pos);return this.pos+=4,e}readI32(){let e=this.view.getInt32(this.pos);return this.pos+=4,e}readU64(){let e=gi(this.view,this.pos);return this.pos+=8,e}readI64(){let e=Lt(this.view,this.pos);return this.pos+=8,e}readU64AsBigInt(){let e=this.view.getBigUint64(this.pos);return this.pos+=8,e}readI64AsBigInt(){let e=this.view.getBigInt64(this.pos);return this.pos+=8,e}readF32(){let e=this.view.getFloat32(this.pos);return this.pos+=4,e}readF64(){let e=this.view.getFloat64(this.pos);return this.pos+=8,e}}});function Ei(t,e){return new G(e).decode(t)}function ki(t,e){return new G(e).decodeMulti(t)}var vi=k(()=>{zt()});function Ta(t){return t[Symbol.asyncIterator]!=null}async function*_a(t){let e=t.getReader();try{for(;;){let{done:n,value:r}=await e.read();if(n)return;yield r}}finally{e.releaseLock()}}function Vt(t){return Ta(t)?t:_a(t)}var Ti=k(()=>{});async function _i(t,e){let n=Vt(t);return new G(e).decodeAsync(n)}function Di(t,e){let n=Vt(t);return new G(e).decodeArrayStream(n)}function Mi(t,e){let n=Vt(t);return new G(e).decodeStream(n)}var Ni=k(()=>{zt();Ti()});var Ui={};ne(Ui,{DecodeError:()=>B,Decoder:()=>G,EXT_TIMESTAMP:()=>zn,Encoder:()=>De,ExtData:()=>se,ExtensionCodec:()=>ae,decode:()=>Ei,decodeArrayStream:()=>Di,decodeAsync:()=>_i,decodeMulti:()=>ki,decodeMultiStream:()=>Mi,decodeTimestampExtension:()=>Kn,decodeTimestampToTimeSpec:()=>qn,encode:()=>wi,encodeDateToTimeSpec:()=>Wn,encodeTimeSpecToTimestamp:()=>Vn,encodeTimestampExtension:()=>jn});var Oi=k(()=>{xi();vi();Ni();zt();Pt();Hn();$t();Fn();Gn()});var tr=xe((Ah,Bi)=>{"use strict";var F=require("fs"),j=(di(),br(ui)),{encode:Da,decode:Ma}=(Oi(),br(Ui)),er=["id","content","work_unit","work_type","phase","topic","confidence","source_file","timestamp"];function Pi(t){if(!Number.isInteger(t)||t<=0)throw new Error(`createStore: dimensions must be a positive integer, got ${t}`);return{id:"string",content:"string",work_unit:"enum",work_type:"enum",phase:"enum",topic:"enum",confidence:"enum",source_file:"string",timestamp:"number",embedding:`vector[${t}]`}}async function Na(t){let e=Pi(t);return j.create({schema:e})}function Ua(t){for(let e of er)if(t[e]===void 0||t[e]===null)throw new Error(`insertDocument: missing required field "${e}"`);if(typeof t.timestamp!="number"||!Number.isFinite(t.timestamp))throw new Error("insertDocument: timestamp must be a finite number (epoch ms)")}async function Oa(t,e){if(e==null||typeof e!="object")throw new Error("insertDocument: doc must be an object");Ua(e);let n={};for(let r of er)n[r]=e[r];if("embedding"in e){if(e.embedding===null)throw new Error("insertDocument: embedding cannot be null (Orama crashes on null vectors). Omit the field for keyword-only mode, or pass a real vector.");if(e.embedding!==void 0){if(!Array.isArray(e.embedding))throw new Error("insertDocument: embedding must be an array of numbers when present");n.embedding=e.embedding}}return j.insert(t,n)}var Qn=1e3,Ri=1e6;async function Pa(t){let e=[],n=0;for(;;){let r=await j.search(t,{term:"",limit:Qn,offset:n});if(r.hits.length===0||(e.push(...r.hits.map(Wt)),r.hits.lengthi.id);return r.length===0?0:await j.removeMultiple(t,r,r.length)}async function La(t,e){if(!e||Object.keys(e).length===0)throw new Error("countByFilter: where clause is required");return(await j.search(t,{term:"",where:e,limit:Ri})).hits.length}function Wt(t){let e=t.document||{};return{id:e.id,content:e.content,work_unit:e.work_unit,work_type:e.work_type,phase:e.phase,topic:e.topic,confidence:e.confidence,source_file:e.source_file,timestamp:e.timestamp,score:t.score}}async function Ca(t,{term:e="",where:n,limit:r=10}={}){let s={term:e,limit:r};return n&&Object.keys(n).length>0&&(s.where=n),(await j.search(t,s)).hits.map(Wt)}async function $a(t,{vector:e,where:n,limit:r=10,similarity:s}={}){if(!Array.isArray(e))throw new Error("searchVector: vector (number[]) is required");let i={mode:"vector",vector:{value:e,property:"embedding"},limit:r};return typeof s=="number"&&(i.similarity=s),n&&Object.keys(n).length>0&&(i.where=n),(await j.search(t,i)).hits.map(Wt)}async function Ba(t,{term:e,vector:n,where:r,limit:s=10,textWeight:i=.4,vectorWeight:o=.6,similarity:c}={}){if(typeof e!="string")throw new Error("searchHybrid: term (string) is required");if(!Array.isArray(n))throw new Error("searchHybrid: vector (number[]) is required");let a={mode:"hybrid",term:e,vector:{value:n,property:"embedding"},hybridWeights:{text:i,vector:o},limit:s};return typeof c=="number"&&(a.similarity=c),r&&Object.keys(r).length>0&&(a.where=r),(await j.search(t,a)).hits.map(Wt)}async function Fa(t,e){if(!e)throw new Error("saveStore: storePath is required");let n=j.save(t),r={v:1,schema:t.schema,raw:n},s=Da(r),i=e+".tmp";F.writeFileSync(i,s),F.renameSync(i,e)}async function za(t){if(!t)throw new Error("loadStore: storePath is required");if(!F.existsSync(t))throw new Error(`loadStore: store file not found at ${t}`);let e;try{e=F.readFileSync(t)}catch(s){throw new Error(`loadStore: failed to read ${t}: ${s.message}`)}if(e.length===0)throw new Error(`loadStore: store file is empty at ${t}`);let n;try{n=Ma(e)}catch(s){throw new Error(`loadStore: corrupted store file at ${t}: ${s.message}`)}if(!n||typeof n!="object"||!n.schema||!n.raw)throw new Error(`loadStore: malformed envelope at ${t}`);let r=await j.create({schema:n.schema});return j.load(r,n.raw),r}var Va=3e4,Wa=50,ja=3e4;function qa(t){try{let e=F.openSync(t,"wx");return F.writeSync(e,String(process.pid)),F.closeSync(e),!0}catch(e){if(e.code!=="EEXIST")throw e;return!1}}function Ka(t){return new Promise(e=>setTimeout(e,t))}async function Ci(t){let e=Date.now()+ja;for(;;){if(qa(t))return;try{let n=F.statSync(t);if(Date.now()-n.mtimeMs>Va){try{F.unlinkSync(t)}catch{}continue}}catch{continue}if(Date.now()>=e)throw new Error(`knowledge store: timed out waiting for lock at ${t}. If no other process is running, delete the file manually.`);await Ka(Wa)}}function $i(t){try{F.unlinkSync(t)}catch{}}async function Ga(t,e){await Ci(t);try{return await e()}finally{$i(t)}}var Ya=["provider","model","dimensions","last_indexed","pending","pending_removals"];function Ha(t,e){if(!t)throw new Error("writeMetadata: metadataPath is required");if(e==null||typeof e!="object")throw new Error("writeMetadata: data must be an object");let n={provider:e.provider===void 0?null:e.provider,model:e.model===void 0?null:e.model,dimensions:e.dimensions===void 0?null:e.dimensions,last_indexed:e.last_indexed===void 0?null:e.last_indexed,pending:Array.isArray(e.pending)?e.pending:[],pending_removals:Array.isArray(e.pending_removals)?e.pending_removals:[]},r=t+".tmp";F.writeFileSync(r,JSON.stringify(n,null,2)+` +`,"utf8"),F.renameSync(r,t)}function Ja(t){if(!t)throw new Error("readMetadata: metadataPath is required");if(!F.existsSync(t))throw new Error(`readMetadata: metadata file not found at ${t}`);let e;try{e=F.readFileSync(t,"utf8")}catch(r){throw new Error(`readMetadata: failed to read ${t}: ${r.message}`)}let n;try{n=JSON.parse(e)}catch(r){throw new Error(`readMetadata: invalid JSON at ${t}: ${r.message}`)}return n}Bi.exports={SCHEMA_FIELDS:er,METADATA_FIELDS:Ya,buildSchema:Pi,createStore:Na,insertDocument:Oa,removeByIdentity:Ra,removeByFilter:Li,countByFilter:La,searchFulltext:Ca,searchAllFulltext:Pa,searchVector:$a,searchHybrid:Ba,saveStore:Fa,loadStore:za,acquireLock:Ci,releaseLock:$i,withLock:Ga,writeMetadata:Ha,readMetadata:Ja}});var ji=xe((Eh,Wi)=>{"use strict";var Xa=/^\s*(```+|~~~+)/,Fi=/^---\s*$/;function Za(t,e){if(typeof t!="string")throw new TypeError("chunk: markdown must be a string");if(!e||typeof e!="object")throw new TypeError("chunk: config must be an object");let{primary_level:n=2,fallback_level:r=3,max_lines:s=200,keep_whole_below:i=50,special_sections:o={},strip_frontmatter:c=!0,skip_empty_sections:a=!0}=e,l=t.replace(/\r\n/g,` `).replace(/\r/g,` -`),l=c?af(a):a;if(l.trim()==="")return[];let d=l.split(` -`);if(d.length_.level===n),g=f.some(_=>_.level===r),y;if(p)y=n;else if(g)y=r;else return[{content:we(l)}];let h=lf(d,f,y),m=df(h,d,y,o,f),w=[];for(let _ of m)if(_.action!=="skip"){if(_.action==="merge-up"){if(w.length===0)w.push({action:"regular",startLine:_.startLine,endLine:_.endLine,heading:_.heading,headingLine:_.headingLine});else{let I=w[w.length-1];I.endLine=_.endLine}continue}w.push({action:_.action||"regular",startLine:_.startLine,endLine:_.endLine,heading:_.heading,headingLine:_.headingLine})}let S=[];for(let _ of w){let I=we(d.slice(_.startLine,_.endLine+1).join(` -`)),v={heading:_.heading,headingLine:_.headingLine,text:I};if(u&&Io(v))continue;let D=I.split(` -`);if(_.action==="regular"&&D.length>s){let A=ff(v,r);for(let k of A)u&&Io(k)||S.push({content:k.text})}else S.push({content:I})}return S}function we(t){return t.replace(/\s+$/,"")}function af(t){let e=t.split(` -`);if(e.length===0||!bo.test(e[0]||""))return t;for(let n=1;no.level===n).map(o=>o.line),s=[],i=r.length>0?r[0]:t.length;if(i>0){let o=t.slice(0,i),c=e.find(a=>a.level===1&&a.line{if(p.line<=o.startLine||p.line>o.endLine||p.level===1||p.level===n)return!1;let g=r[p.text];return g==="own-chunk"||g==="skip"});if(a.length===0){i.push({action:"regular",startLine:o.startLine,endLine:o.endLine,heading:o.heading,headingLine:o.headingLine});continue}let l=a.map(p=>{let g=o.endLine;for(let y of s)if(!(y.line<=p.line)){if(y.line>o.endLine)break;if(y.level<=p.level){g=y.line-1;break}}return{action:r[p.text],startLine:p.line,endLine:g,heading:p.text,headingLine:e[p.line]}}),d=o.startLine,f=!0;for(let p of l)p.startLine>d&&(i.push({action:"regular",startLine:d,endLine:p.startLine-1,heading:f?o.heading:"",headingLine:f?o.headingLine:""}),f=!1),i.push({action:p.action,startLine:p.startLine,endLine:p.endLine,heading:p.heading,headingLine:p.headingLine}),d=p.endLine+1;d<=o.endLine&&i.push({action:"regular",startLine:d,endLine:o.endLine,heading:"",headingLine:""})}return i}function ff(t,e){let n=t.text.split(` -`),s=xo(n).filter(c=>c.level===e).map(c=>c.line);if(s.length===0)return[t];let i=[],o=s[0];if(o>0){let c=n.slice(0,o),u=we(c.join(` -`));u.trim()!==""&&i.push({heading:t.heading,headingLine:t.headingLine,text:u})}for(let c=0;cw.level===n),p=f.some(w=>w.level===r),x;if(h)x=n;else if(p)x=r;else return[{content:le(u)}];let g=el(d,f,x),y=tl(g,d,x,o,f),I=[];for(let w of y)if(w.action!=="skip"){if(w.action==="merge-up"){if(I.length===0)I.push({action:"regular",startLine:w.startLine,endLine:w.endLine,heading:w.heading,headingLine:w.headingLine});else{let S=I[I.length-1];S.endLine=w.endLine}continue}I.push({action:w.action||"regular",startLine:w.startLine,endLine:w.endLine,heading:w.heading,headingLine:w.headingLine})}let m=[];for(let w of I){let S=le(d.slice(w.startLine,w.endLine+1).join(` +`)),T={heading:w.heading,headingLine:w.headingLine,text:S};if(a&&zi(T))continue;let _=S.split(` +`);if(w.action==="regular"&&_.length>s){let D=nl(T,r);for(let M of D)a&&zi(M)||m.push({content:M.text})}else m.push({content:S})}return m}function le(t){return t.replace(/\s+$/,"")}function Qa(t){let e=t.split(` +`);if(e.length===0||!Fi.test(e[0]||""))return t;for(let n=1;no.level===n).map(o=>o.line),s=[],i=r.length>0?r[0]:t.length;if(i>0){let o=t.slice(0,i),c=e.find(l=>l.level===1&&l.line{if(h.line<=o.startLine||h.line>o.endLine||h.level===1||h.level===n)return!1;let p=r[h.text];return p==="own-chunk"||p==="skip"});if(l.length===0){i.push({action:"regular",startLine:o.startLine,endLine:o.endLine,heading:o.heading,headingLine:o.headingLine});continue}let u=l.map(h=>{let p=o.endLine;for(let x of s)if(!(x.line<=h.line)){if(x.line>o.endLine)break;if(x.level<=h.level){p=x.line-1;break}}return{action:r[h.text],startLine:h.line,endLine:p,heading:h.text,headingLine:e[h.line]}}),d=o.startLine,f=!0;for(let h of u)h.startLine>d&&(i.push({action:"regular",startLine:d,endLine:h.startLine-1,heading:f?o.heading:"",headingLine:f?o.headingLine:""}),f=!1),i.push({action:h.action,startLine:h.startLine,endLine:h.endLine,heading:h.heading,headingLine:h.headingLine}),d=h.endLine+1;d<=o.endLine&&i.push({action:"regular",startLine:d,endLine:o.endLine,heading:"",headingLine:""})}return i}function nl(t,e){let n=t.text.split(` +`),s=Vi(n).filter(c=>c.level===e).map(c=>c.line);if(s.length===0)return[t];let i=[],o=s[0];if(o>0){let c=n.slice(0,o),a=le(c.join(` +`));a.trim()!==""&&i.push({heading:t.heading,headingLine:t.headingLine,text:a})}for(let c=0;c{"use strict";var vo="stub";function hf(t){let e=2166136261;for(let n=0;n>>0}function pf(t){let e=t>>>0;return function(){e=e+1831565813>>>0;let r=e;return r=Math.imul(r^r>>>15,r|1),r^=r+Math.imul(r^r>>>7,r|61),((r^r>>>14)>>>0)/4294967296}}var Tr=class{constructor(e){let n=e&&typeof e.dimensions=="number"?e.dimensions:128;if(!Number.isInteger(n)||n<=0)throw new Error(`StubProvider: dimensions must be a positive integer, got ${n}`);this._dimensions=n}embed(e){let n=typeof e=="string"?e:String(e??""),r=hf(n)||1,s=pf(r),i=new Array(this._dimensions);for(let o=0;o{"use strict";var Do="text-embedding-3-small",Mo="https://api.openai.com/v1/embeddings",Mr=class{constructor(e){if(!e||!e.apiKey)throw new Error("OpenAIProvider: apiKey is required");this._apiKey=e.apiKey,this._model=e.model||Do,this._dimensions=typeof e.dimensions=="number"?e.dimensions:1536}async embed(e){let n=JSON.stringify({model:this._model,input:typeof e=="string"?e:String(e??""),dimensions:this._dimensions});return(await this._fetch(n)).data[0].embedding}async embedBatch(e){if(!Array.isArray(e))throw new Error("OpenAIProvider.embedBatch: texts must be an array");if(e.length===0)return[];if(e.length<=2048){let r=JSON.stringify({model:this._model,input:e,dimensions:this._dimensions});return(await this._fetch(r)).data.sort((o,c)=>o.index-c.index).map(o=>o.embedding)}let n=new Array(e.length);for(let r=0;ru.index-a.index);for(let u=0;u{"use strict";var B=require("fs"),it=require("path"),Po=require("os"),{StubProvider:gf}=Dr(),{OpenAIProvider:yf}=un(),ko={similarity_threshold:.8,decay_months:6},Or=["stub","openai"],No={openai:"OPENAI_API_KEY"};function Uo(){return it.join(Po.homedir(),".config","workflows","config.json")}function Ro(t){return it.join(t||process.cwd(),".workflows",".knowledge","config.json")}function Lo(){return it.join(Po.homedir(),".config","workflows","credentials.json")}function Pr(t){if(!B.existsSync(t))return null;let e;try{e=B.readFileSync(t,"utf8")}catch(r){throw new Error(`Failed to read config file at ${t}: ${r.message}`)}let n;try{n=JSON.parse(e)}catch(r){throw new Error(`Invalid JSON in config file at ${t}: ${r.message}`)}if(n==null||typeof n!="object"||!n.knowledge)throw new Error(`Config file at ${t} is missing the required top-level "knowledge" key. Expected format: { "knowledge": { ... } }`);if(typeof n.knowledge!="object"||Array.isArray(n.knowledge))throw new Error(`Config file at ${t}: the "knowledge" key must be an object.`);return n.knowledge}function kr(t){if(!B.existsSync(t))return null;let e;try{e=B.readFileSync(t,"utf8")}catch(r){throw new Error(`Failed to read credentials file at ${t}: ${r.message}`)}let n;try{n=JSON.parse(e)}catch(r){throw new Error(`Invalid JSON in credentials file at ${t}: ${r.message}`)}if(n==null||typeof n!="object"||!n.credentials||typeof n.credentials!="object"||Array.isArray(n.credentials))throw new Error(`Credentials file at ${t} is missing the required top-level "credentials" object. Expected format: { "credentials": { "": { "api_key": "..." } } }`);return n.credentials}function mf(t,e,n){if(!t)throw new Error("writeCredentials: filePath is required");if(!e||typeof e!="string")throw new Error("writeCredentials: provider name is required");let r={};if(B.existsSync(t))try{r=kr(t)||{}}catch{r={}}let s=Object.assign({},r);n==null?delete s[e]:s[e]=Object.assign({},s[e]||{},{api_key:n});let i={credentials:s},o=it.dirname(t);B.existsSync(o)||B.mkdirSync(o,{recursive:!0});let c=t+".tmp",u=B.openSync(c,"w",384);try{B.writeSync(u,JSON.stringify(i,null,2)+` -`)}finally{B.closeSync(u)}B.chmodSync(c,384),B.renameSync(c,t);try{B.chmodSync(t,384)}catch{}}function jo(t,e){if(!t)return null;let n=No[t];if(n){let i=process.env[n];if(i&&i.trim()!=="")return i}let r=e&&e.credentialsPath||Lo(),s;try{s=kr(r)}catch{return null}if(s&&s[t]&&typeof s[t].api_key=="string"){let i=s[t].api_key.trim();if(i!=="")return i}return null}function wf(t){let e=t&&t.systemPath||Uo(),n=t&&t.projectPath||Ro(),r=Pr(e),s=Pr(n),i=Object.assign({},ko);if(r)for(let o of Object.keys(r))r[o]!==void 0&&(i[o]=r[o]);if(s)for(let o of Object.keys(s))s[o]!==void 0&&(i[o]=s[o]);return i._api_key=jo(i.provider,{credentialsPath:t&&t.credentialsPath}),i}function _f(t){if(!t||typeof t!="object")throw new Error("resolveProvider: config is required");let e=t.provider;if(!e)return null;if(e==="stub"){let n=t.dimensions||void 0;return new gf(n!=null?{dimensions:n}:void 0)}if(!Or.includes(e))throw new Error(`Provider "${e}" is not available. Available providers: ${Or.join(", ")}`);return t._api_key&&e==="openai"?new yf({apiKey:t._api_key,model:t.model||void 0,dimensions:t.dimensions||void 0}):null}function Sf(t,e){if(!t)throw new Error("writeConfigFile: filePath is required");if(e==null||typeof e!="object"||!e.knowledge)throw new Error('writeConfigFile: payload must be an object with a top-level "knowledge" key');let n=it.dirname(t);B.existsSync(n)||B.mkdirSync(n,{recursive:!0});let r=t+".tmp";B.writeFileSync(r,JSON.stringify(e,null,2)+` -`,"utf8"),B.renameSync(r,t)}Co.exports={DEFAULTS:ko,AVAILABLE_PROVIDERS:Or,PROVIDER_ENV_VARS:No,systemConfigPath:Uo,projectConfigPath:Ro,credentialsPath:Lo,readConfigFile:Pr,loadConfig:wf,loadCredentials:kr,writeCredentials:mf,resolveApiKey:jo,resolveProvider:_f,writeConfigFile:Sf}});var tc=b((np,ec)=>{"use strict";var _e=require("fs"),Se=require("path"),bf=require("readline"),$=Nr(),Ur=vr(),{OpenAIProvider:If}=un(),Fo="text-embedding-3-small",Bo=1536,$o=1536;function qo(){process.stdin.isTTY||(process.stderr.write(`knowledge setup requires an interactive terminal. Run it directly, not through Claude or a pipe. -`),process.exit(1))}function zo(){let t=bf.createInterface({input:process.stdin,output:process.stdout});return t.on("SIGINT",()=>{process.stderr.write(` +`).trim()===""}Wi.exports={chunk:Za}});var rr=xe((kh,Ki)=>{"use strict";var qi="stub";function rl(t){let e=2166136261;for(let n=0;n>>0}function sl(t){let e=t>>>0;return function(){e=e+1831565813>>>0;let r=e;return r=Math.imul(r^r>>>15,r|1),r^=r+Math.imul(r^r>>>7,r|61),((r^r>>>14)>>>0)/4294967296}}var nr=class{constructor(e){let n=e&&typeof e.dimensions=="number"?e.dimensions:128;if(!Number.isInteger(n)||n<=0)throw new Error(`StubProvider: dimensions must be a positive integer, got ${n}`);this._dimensions=n}embed(e){let n=typeof e=="string"?e:String(e??""),r=rl(n)||1,s=sl(r),i=new Array(this._dimensions);for(let o=0;o{"use strict";var Gi="text-embedding-3-small",Yi="https://api.openai.com/v1/embeddings",tt=class extends Error{constructor(e){super(e),this.name="AuthError"}},sr=class{constructor(e){if(!e||!e.apiKey)throw new Error("OpenAIProvider: apiKey is required");this._apiKey=e.apiKey,this._model=e.model||Gi,this._dimensions=typeof e.dimensions=="number"?e.dimensions:1536}async embed(e){let n=JSON.stringify({model:this._model,input:typeof e=="string"?e:String(e??""),dimensions:this._dimensions}),r=await this._fetch(n);if(!r.data||r.data.length===0)throw new Error("OpenAI embed returned no data (empty response)");return r.data[0].embedding}async embedBatch(e){if(!Array.isArray(e))throw new Error("OpenAIProvider.embedBatch: texts must be an array");if(e.length===0)return[];if(e.length<=2048){let r=JSON.stringify({model:this._model,input:e,dimensions:this._dimensions}),s=await this._fetch(r);if(!Array.isArray(s.data)||s.data.length!==e.length)throw new Error(`OpenAI embedBatch response length mismatch: requested ${e.length}, received ${s.data?s.data.length:0}`);return[...s.data].sort((o,c)=>o.index-c.index).map(o=>o.embedding)}let n=new Array(e.length);for(let r=0;ra.index-l.index);for(let a=0;a{"use strict";var C=require("fs"),ue=require("path"),Ji=require("os"),{StubProvider:il}=rr(),{OpenAIProvider:ol}=jt(),Xi={similarity_threshold:.8,decay_months:6},ir=["stub","openai"],Zi={openai:"OPENAI_API_KEY"};function Qi(){return ue.join(Ji.homedir(),".config","workflows","config.json")}function eo(t){let e=ue.resolve(t||process.cwd()),n=e;for(;;){if(C.existsSync(ue.join(e,".workflows")))return e;let r=ue.dirname(e);if(r===e)return n;e=r}}function to(t){return ue.join(eo(t),".workflows",".knowledge","config.json")}function no(){return ue.join(Ji.homedir(),".config","workflows","credentials.json")}function or(t){if(!C.existsSync(t))return null;let e;try{e=C.readFileSync(t,"utf8")}catch(r){throw new Error(`Failed to read config file at ${t}: ${r.message}`)}let n;try{n=JSON.parse(e)}catch(r){throw new Error(`Invalid JSON in config file at ${t}: ${r.message}`)}if(n==null||typeof n!="object"||!n.knowledge)throw new Error(`Config file at ${t} is missing the required top-level "knowledge" key. Expected format: { "knowledge": { ... } }`);if(typeof n.knowledge!="object"||Array.isArray(n.knowledge))throw new Error(`Config file at ${t}: the "knowledge" key must be an object.`);return n.knowledge}function cr(t){if(!C.existsSync(t))return null;let e;try{e=C.readFileSync(t,"utf8")}catch(r){throw new Error(`Failed to read credentials file at ${t}: ${r.message}`)}let n;try{n=JSON.parse(e)}catch(r){throw new Error(`Invalid JSON in credentials file at ${t}: ${r.message}`)}if(n==null||typeof n!="object"||!n.credentials||typeof n.credentials!="object"||Array.isArray(n.credentials))throw new Error(`Credentials file at ${t} is missing the required top-level "credentials" object. Expected format: { "credentials": { "": { "api_key": "..." } } }`);return n.credentials}function cl(t,e,n){if(!t)throw new Error("writeCredentials: filePath is required");if(!e||typeof e!="string")throw new Error("writeCredentials: provider name is required");let r={};if(C.existsSync(t))try{r=cr(t)||{}}catch{r={}}let s=Object.assign({},r);n==null?delete s[e]:s[e]=Object.assign({},s[e]||{},{api_key:n});let i={credentials:s},o=ue.dirname(t);C.existsSync(o)||C.mkdirSync(o,{recursive:!0});let c=t+".tmp",a=C.openSync(c,"w",384);try{C.writeSync(a,JSON.stringify(i,null,2)+` +`)}finally{C.closeSync(a)}C.chmodSync(c,384),C.renameSync(c,t);try{C.chmodSync(t,384)}catch{}}function ro(t,e){if(!t)return null;let n=Zi[t];if(n){let i=process.env[n];if(i&&i.trim()!=="")return i}let r=e&&e.credentialsPath||no(),s;try{s=cr(r)}catch{return null}if(s&&s[t]&&typeof s[t].api_key=="string"){let i=s[t].api_key.trim();if(i!=="")return i}return null}function al(t){let e=t&&t.systemPath||Qi(),n=t&&t.projectPath||to(),r=or(e),s=or(n),i=Object.assign({},Xi);if(r)for(let o of Object.keys(r))r[o]!==void 0&&(r[o]===null?delete i[o]:i[o]=r[o]);if(s)for(let o of Object.keys(s))s[o]!==void 0&&(s[o]===null?delete i[o]:i[o]=s[o]);return i._api_key=ro(i.provider,{credentialsPath:t&&t.credentialsPath}),i}function ll(t){if(!t||typeof t!="object")throw new Error("resolveProvider: config is required");let e=t.provider;if(!e)return null;if(e==="stub"){let n=t.dimensions||void 0;return new il(n!=null?{dimensions:n}:void 0)}if(!ir.includes(e))throw new Error(`Provider "${e}" is not available. Available providers: ${ir.join(", ")}`);return t._api_key&&e==="openai"?new ol({apiKey:t._api_key,model:t.model||void 0,dimensions:t.dimensions||void 0}):null}function ul(t,e){if(!t)throw new Error("writeConfigFile: filePath is required");if(e==null||typeof e!="object"||!e.knowledge)throw new Error('writeConfigFile: payload must be an object with a top-level "knowledge" key');let n=ue.dirname(t);C.existsSync(n)||C.mkdirSync(n,{recursive:!0});let r=t+".tmp";C.writeFileSync(r,JSON.stringify(e,null,2)+` +`,"utf8"),C.renameSync(r,t)}so.exports={DEFAULTS:Xi,AVAILABLE_PROVIDERS:ir,PROVIDER_ENV_VARS:Zi,systemConfigPath:Qi,projectConfigPath:to,findProjectRoot:eo,credentialsPath:no,readConfigFile:or,loadConfig:al,loadCredentials:cr,writeCredentials:cl,resolveApiKey:ro,resolveProvider:ll,writeConfigFile:ul}});var Io=xe((_h,So)=>{"use strict";var de=require("fs"),fe=require("path"),dl=require("readline"),$=ar(),lr=tr(),{OpenAIProvider:fl}=jt(),io="text-embedding-3-small",oo=1536,co=1536;function ao(){process.stdin.isTTY||(process.stderr.write(`knowledge setup requires an interactive terminal. Run it directly, not through Claude or a pipe. +`),process.exit(1))}function lo(){let t=dl.createInterface({input:process.stdin,output:process.stdout});return t.on("SIGINT",()=>{process.stderr.write(` Setup cancelled. -`),t.close(),process.exit(130)}),t}function an(t,e,n){let r=n!=null&&n!==""?` [${n}]`:"";return new Promise(s=>{t.question(`${e}${r}: `,i=>{let o=(i||"").trim();s(o===""&&n!==void 0&&n!==null?String(n):o)})})}async function Ce(t,e,n){let r=n?"Y/n":"y/N";return new Promise(s=>{t.question(`${e} (${r}): `,i=>{let o=(i||"").trim().toLowerCase();if(o==="")return s(!!n);s(o==="y"||o==="yes")})})}function Vo(t,e){return new Promise(n=>{let r=process.stdin,s=process.stdout;if(!r.isTTY||typeof r.setRawMode!="function"){t.question(e,a=>n((a||"").trim()));return}s.write(e),t.pause();let i=r.isRaw===!0;r.setRawMode(!0),r.resume(),r.setEncoding("utf8");let o="",c=()=>{r.removeListener("data",u);try{r.setRawMode(i)}catch{}r.pause(),t.resume()},u=a=>{for(let l of a.toString("utf8")){if(l===` -`||l==="\r")return c(),s.write(` -`),n(o.trim());if(l===""){c(),s.write(` -`),process.exit(130);return}if(l==="")return c(),s.write(` -`),n(o.trim());if(l==="\x7F"||l==="\b"){o.length>0&&(o=o.slice(0,-1),s.write("\b \b"));continue}l<" "||(o+=l,s.write("*"))}};r.on("data",u)})}function Wo({model:t,dimensions:e}){return{knowledge:{provider:"openai",model:t,dimensions:e,similarity_threshold:$.DEFAULTS.similarity_threshold,decay_months:$.DEFAULTS.decay_months}}}function Ko(){return{knowledge:{similarity_threshold:$.DEFAULTS.similarity_threshold,decay_months:$.DEFAULTS.decay_months}}}function Ho(){return{knowledge:{}}}function Go(t){if(!_e.existsSync(t))return{exists:!1,valid:!1,knowledge:null};try{let e=_e.readFileSync(t,"utf8"),n=JSON.parse(e);return!n||typeof n!="object"||!n.knowledge||typeof n.knowledge!="object"||Array.isArray(n.knowledge)?{exists:!0,valid:!1,knowledge:null,reason:'missing or invalid "knowledge" key'}:{exists:!0,valid:!0,knowledge:n.knowledge}}catch(e){return{exists:!0,valid:!1,knowledge:null,reason:e.message}}}function Yo(t){let e=Se.join(t,"config.json"),n=Se.join(t,"store.msp"),r=Se.join(t,"metadata.json"),s=_e.existsSync(t),i=_e.existsSync(e),o=_e.existsSync(n),c=_e.existsSync(r);return{dirExists:s,configExists:i,storeExists:o,metadataExists:c,fullyInitialised:i&&o&&c,partiallyInitialised:s&&!(i&&o&&c)}}async function ln({apiKey:t,model:e,dimensions:n}){let s=await new If({apiKey:t,model:e,dimensions:n}).embed("knowledge base setup test");if(!Array.isArray(s)||s.length!==n)throw new Error(`Expected a vector of length ${n}, got ${Array.isArray(s)?s.length:typeof s}`);return!0}function dn(t){let e=t&&t.message||String(t);return/401/.test(e)||/invalid or expired/i.test(e)?{message:"The API key was rejected (HTTP 401).",hint:"Check that the key is active and not revoked. Free-tier keys also need billing enabled for /v1/embeddings. Create a fresh key at https://platform.openai.com/api-keys and try again."}:/403/.test(e)||/permission/i.test(e)?{message:"The API key does not have permission for embeddings (HTTP 403).",hint:"If this is a restricted key, check its allowed endpoints in the OpenAI dashboard. Create a key with Embeddings access enabled."}:/429/.test(e)||/rate limit/i.test(e)?{message:"Rate limit hit during validation (HTTP 429).",hint:"Your account may be out of quota, or the default rate limit is saturated. Wait a moment and retry, or check billing at https://platform.openai.com/account."}:/network error/i.test(e)||/ENOTFOUND/.test(e)||/ECONN/.test(e)||/ETIMEDOUT/.test(e)?{message:"Could not reach OpenAI (network error).",hint:"Check your internet connection, VPN, or corporate proxy. No key was written \u2014 you can re-run `knowledge setup` once the connection is stable."}:/HTTP 5\d\d/.test(e)?{message:"OpenAI returned a server error during validation.",hint:"Transient on their side. Retry in a minute."}:{message:"API key validation failed.",hint:`Error detail: ${e}`}}async function Jo(t){let e=$.systemConfigPath(),n=Go(e);if(n.exists&&n.valid){process.stdout.write(` +`),t.close(),process.exit(130)}),t}function qt(t,e,n){let r=n!=null&&n!==""?` [${n}]`:"";return new Promise(s=>{t.question(`${e}${r}: `,i=>{let o=(i||"").trim();s(o===""&&n!==void 0&&n!==null?String(n):o)})})}async function Me(t,e,n){let r=n?"Y/n":"y/N";return new Promise(s=>{t.question(`${e} (${r}): `,i=>{let o=(i||"").trim().toLowerCase();if(o==="")return s(!!n);s(o==="y"||o==="yes")})})}function uo(t,e){return new Promise(n=>{let r=process.stdin,s=process.stdout;if(!r.isTTY||typeof r.setRawMode!="function"){t.question(e,l=>n((l||"").trim()));return}s.write(e),t.pause();let i=r.isRaw===!0;r.setRawMode(!0),r.resume(),r.setEncoding("utf8");let o="",c=()=>{r.removeListener("data",a);try{r.setRawMode(i)}catch{}r.pause(),t.resume()},a=l=>{for(let u of l.toString("utf8")){if(u===` +`||u==="\r")return c(),s.write(` +`),n(o.trim());if(u===""){c(),s.write(` +`),process.exit(130);return}if(u==="")return c(),s.write(` +`),n(o.trim());if(u==="\x7F"||u==="\b"){o.length>0&&(o=o.slice(0,-1),s.write("\b \b"));continue}u<" "||(o+=u,s.write("*"))}};r.on("data",a)})}function fo({model:t,dimensions:e}){return{knowledge:{provider:"openai",model:t,dimensions:e,similarity_threshold:$.DEFAULTS.similarity_threshold,decay_months:$.DEFAULTS.decay_months}}}function ur(){return{knowledge:{similarity_threshold:$.DEFAULTS.similarity_threshold,decay_months:$.DEFAULTS.decay_months}}}function ho(){return{knowledge:{}}}function po(t){if(!de.existsSync(t))return{exists:!1,valid:!1,knowledge:null};try{let e=de.readFileSync(t,"utf8"),n=JSON.parse(e);return!n||typeof n!="object"||!n.knowledge||typeof n.knowledge!="object"||Array.isArray(n.knowledge)?{exists:!0,valid:!1,knowledge:null,reason:'missing or invalid "knowledge" key'}:{exists:!0,valid:!0,knowledge:n.knowledge}}catch(e){return{exists:!0,valid:!1,knowledge:null,reason:e.message}}}function mo(t){let e=fe.join(t,"config.json"),n=fe.join(t,"store.msp"),r=fe.join(t,"metadata.json"),s=de.existsSync(t),i=de.existsSync(e),o=de.existsSync(n),c=de.existsSync(r);return{dirExists:s,configExists:i,storeExists:o,metadataExists:c,fullyInitialised:i&&o&&c,partiallyInitialised:s&&!(i&&o&&c)}}async function Kt({apiKey:t,model:e,dimensions:n}){let s=await new fl({apiKey:t,model:e,dimensions:n}).embed("knowledge base setup test");if(!Array.isArray(s)||s.length!==n)throw new Error(`Expected a vector of length ${n}, got ${Array.isArray(s)?s.length:typeof s}`);return!0}function Gt(t){let e=t&&t.message||String(t);return/401/.test(e)||/invalid or expired/i.test(e)?{message:"The API key was rejected (HTTP 401).",hint:"Check that the key is active and not revoked. Free-tier keys also need billing enabled for /v1/embeddings. Create a fresh key at https://platform.openai.com/api-keys and try again."}:/403/.test(e)||/permission/i.test(e)?{message:"The API key does not have permission for embeddings (HTTP 403).",hint:"If this is a restricted key, check its allowed endpoints in the OpenAI dashboard. Create a key with Embeddings access enabled."}:/429/.test(e)||/rate limit/i.test(e)?{message:"Rate limit hit during validation (HTTP 429).",hint:"Your account may be out of quota, or the default rate limit is saturated. Wait a moment and retry, or check billing at https://platform.openai.com/account."}:/network error/i.test(e)||/ENOTFOUND/.test(e)||/ECONN/.test(e)||/ETIMEDOUT/.test(e)?{message:"Could not reach OpenAI (network error).",hint:"Check your internet connection, VPN, or corporate proxy. No key was written \u2014 you can re-run `knowledge setup` once the connection is stable."}:/HTTP 5\d\d/.test(e)?{message:"OpenAI returned a server error during validation.",hint:"Transient on their side. Retry in a minute."}:{message:"API key validation failed.",hint:`Error detail: ${e}`}}async function go(t){let e=$.systemConfigPath(),n=po(e);if(n.exists&&n.valid){process.stdout.write(` System config already exists at ${e} `),process.stdout.write(` Current settings: -`);let a=n.knowledge;if(process.stdout.write(` provider: ${a.provider==null?"(none \u2014 stub mode)":a.provider} -`),a.model&&process.stdout.write(` model: ${a.model} -`),a.dimensions&&process.stdout.write(` dimensions: ${a.dimensions} +`);let u=n.knowledge;if(process.stdout.write(` provider: ${u.provider==null?"(none \u2014 stub mode)":u.provider} +`),u.model&&process.stdout.write(` model: ${u.model} +`),u.dimensions&&process.stdout.write(` dimensions: ${u.dimensions} `),process.stdout.write(` -`),!await Ce(t,"Reconfigure system settings?",!1))return process.stdout.write(`Keeping existing system config. -`),{provider:a.provider||null,previouslyStub:!a.provider}}else n.exists&&!n.valid?(process.stdout.write(` +`),!await Me(t,"Reconfigure system settings?",!1))return process.stdout.write(`Keeping existing system config. +`),{provider:u.provider||null,previouslyStub:!u.provider}}else n.exists&&!n.valid?(process.stdout.write(` System config at ${e} is not valid: ${n.reason} -`),await Ce(t,"Overwrite it?",!0)||(process.stdout.write(`Aborting setup so you can fix the file manually. +`),await Me(t,"Overwrite it?",!0)||(process.stdout.write(`Aborting setup so you can fix the file manually. `),process.exit(1))):process.stdout.write(` No system config found at ${e}. Creating a new one. `);let r=n.exists&&n.valid&&!n.knowledge.provider;process.stdout.write(` @@ -50,26 +50,31 @@ Embedding provider: `),process.stdout.write(` openai \u2014 OpenAI embeddings API (requires an API key) `),process.stdout.write(` skip \u2014 Stub mode (keyword-only search, no embeddings) -`);let s;for(;s=(await an(t,"Provider (openai / skip)","openai")).toLowerCase(),!(s==="openai"||s==="skip");)process.stdout.write(`Unknown choice "${s}". Enter "openai" or "skip". -`);if(s==="skip")return $.writeConfigFile(e,Ko()),process.stdout.write(` +`);let s;for(;s=(await qt(t,"Provider (openai / skip)","openai")).toLowerCase(),!(s==="openai"||s==="skip");)process.stdout.write(`Unknown choice "${s}". Enter "openai" or "skip". +`);if(s==="skip")return $.writeConfigFile(e,ur()),process.stdout.write(` Wrote stub-mode system config to ${e} -`),process.stdout.write("Stub mode uses keyword-only (BM25) search. Semantic search is disabled. Run `knowledge setup` again later to configure a provider.\n"),{provider:null,previouslyStub:r};let i=await an(t,"Embedding model",Fo),o=await an(t,"Vector dimensions",String(Bo)),c=parseInt(o,10);(!Number.isInteger(c)||c<=0)&&(process.stderr.write(`Invalid dimensions: "${o}". Must be a positive integer. -`),process.exit(1)),$.writeConfigFile(e,Wo({model:i,dimensions:c})),process.stdout.write(` +`),process.stdout.write("Stub mode uses keyword-only (BM25) search. Semantic search is disabled. Run `knowledge setup` again later to configure a provider.\n"),{provider:null,previouslyStub:r};let i=await qt(t,"Embedding model",io),o=await qt(t,"Vector dimensions",String(oo));/^\d+$/.test(o.trim())||(process.stderr.write(`Invalid dimensions: "${o}". Must be a positive integer. +`),process.exit(1));let c=parseInt(o,10);(!Number.isInteger(c)||c<=0)&&(process.stderr.write(`Invalid dimensions: "${o}". Must be a positive integer. +`),process.exit(1));let a=$.PROVIDER_ENV_VARS.openai;return await yo(t,{envVar:a,model:i,dimensions:c})==="opted-out"?($.writeConfigFile(e,ur()),process.stdout.write(` +Wrote stub-mode system config to ${e} +`),process.stdout.write("Stub mode uses keyword-only (BM25) search. Semantic search is disabled. Re-run `knowledge setup` once you have a working API key.\n"),{provider:null,previouslyStub:r}):($.writeConfigFile(e,fo({model:i,dimensions:c})),process.stdout.write(` Wrote system config to ${e} -`);let u=$.PROVIDER_ENV_VARS.openai;return await Xo(t,{envVar:u,model:i,dimensions:c}),{provider:"openai",previouslyStub:r}}async function Xo(t,{envVar:e,model:n,dimensions:r}){let s=$.credentialsPath(),i=process.env[e];if(i&&i.trim()!==""){process.stdout.write(` +`),{provider:"openai",previouslyStub:r})}async function yo(t,{envVar:e,model:n,dimensions:r}){let s=$.credentialsPath(),i=process.env[e];if(i&&i.trim()!==""){process.stdout.write(` Using API key from $${e} \u2014 validating via a test embed... -`);try{await ln({apiKey:i.trim(),model:n,dimensions:r}),process.stdout.write(`API key works. -`)}catch(c){let{message:u,hint:a}=dn(c);process.stdout.write(`${u} - ${a} -`),process.stdout.write(`The failing key came from $${e}. Fix or unset it in your shell, then re-run \`knowledge setup\`. Setup will continue \u2014 indexing will queue until the key is corrected. -`)}return}let o=$.resolveApiKey("openai",{credentialsPath:s});if(o){process.stdout.write(` +`);try{return await Kt({apiKey:i.trim(),model:n,dimensions:r}),process.stdout.write(`API key works. +`),"validated"}catch(c){let{message:a,hint:l}=Gt(c);process.stderr.write(` +${a} + ${l} +`),process.stderr.write(`The failing key came from $${e}. Fix or unset it in your shell, then re-run \`knowledge setup\`. +`),process.exit(1)}}let o=$.resolveApiKey("openai",{credentialsPath:s});if(o){process.stdout.write(` Found an existing API key in ${s} \u2014 validating via a test embed... -`);try{await ln({apiKey:o,model:n,dimensions:r}),process.stdout.write(`API key works. -`);return}catch(c){let{message:u,hint:a}=dn(c);if(process.stdout.write(`${u} - ${a} -`),!await Ce(t,"Enter a new key to replace it?",!0)){process.stdout.write(`Keeping the existing stored key. Indexing will fail until it is rotated. +`);try{return await Kt({apiKey:o,model:n,dimensions:r}),process.stdout.write(`API key works. +`),"validated"}catch(c){let{message:a,hint:l}=Gt(c);process.stdout.write(`${a} + ${l} +`),await Me(t,"Enter a new key to replace it?",!0)||(process.stderr.write(` +Keeping the existing stored key would leave setup in an inconsistent state. Edit ${s} or re-run \`knowledge setup\` when you have a new key. -`);return}}}await xf(t,{envVar:e,model:n,dimensions:r,credPath:s})}async function xf(t,{envVar:e,model:n,dimensions:r,credPath:s}){for(process.stdout.write(` +`),process.exit(1))}}return await hl(t,{envVar:e,model:n,dimensions:r,credPath:s})}async function hl(t,{envVar:e,model:n,dimensions:r,credPath:s}){for(process.stdout.write(` OpenAI API Key -------------- Semantic search in the knowledge base relies on OpenAI embeddings. @@ -85,42 +90,51 @@ Your key will be stored at: Setting $${e} in your shell takes precedence and overrides the stored key, so you can swap it without editing the file. -`);;){let i=await Vo(t,"API key (input hidden): ");if(i===""){process.stdout.write(`Empty input \u2014 enter the key, or Ctrl-C to abort setup. +`);;){let i=await uo(t,"API key (input hidden): ");if(i===""){process.stdout.write(`Empty input \u2014 enter the key, or Ctrl-C to abort setup. `);continue}process.stdout.write(` Validating via a test embed... -`);try{await ln({apiKey:i,model:n,dimensions:r})}catch(o){let{message:c,hint:u}=dn(o);if(process.stdout.write(`${c} - ${u} +`);try{await Kt({apiKey:i,model:n,dimensions:r})}catch(o){let{message:c,hint:a}=Gt(o);if(process.stdout.write(`${c} + ${a} -`),!await Ce(t,"Try a different key?",!0)){process.stdout.write(`No key stored. Setup continues but indexing will skip until a key is provided. -Set $${e} in your shell or re-run \`knowledge setup\`. -`);return}continue}$.writeCredentials(s,"openai",i),process.stdout.write(`API key works. Stored at ${s} (mode 0600). -`);return}}async function Zo(t){let e=Se.resolve(process.cwd(),".workflows",".knowledge"),n=Se.join(e,"config.json"),r=Se.join(e,"store.msp"),s=Se.join(e,"metadata.json"),i=Yo(e);if(i.fullyInitialised){if(process.stdout.write(` +`),!await Me(t,"Try a different key?",!0))return process.stdout.write(`No key stored. Falling back to stub mode \u2014 semantic search disabled. +Set $${e} in your shell or re-run \`knowledge setup\` once you have a working key. +`),"opted-out";continue}return $.writeCredentials(s,"openai",i),process.stdout.write(`API key works. Stored at ${s} (mode 0600). +`),"validated"}}async function wo(t){let e=fe.resolve($.findProjectRoot(),".workflows",".knowledge"),n=fe.join(e,"config.json"),r=fe.join(e,"store.msp"),s=fe.join(e,"metadata.json"),i=mo(e);if(i.storeExists&&!i.metadataExists&&(process.stderr.write(` +Project knowledge base at ${e} is in an inconsistent state: + store.msp is present but metadata.json is missing. + Setup cannot recover this safely \u2014 run \`knowledge rebuild\` (which + re-creates the store from scratch and writes matching metadata) and + then re-run \`knowledge setup\` if needed. +`),process.exit(1)),i.fullyInitialised){if(process.stdout.write(` Project knowledge base already initialised at ${e} -`),!await Ce(t,"Reinitialise (destroys existing store)?",!1))return process.stdout.write(`Keeping existing project files. +`),!await Me(t,"Reinitialise (destroys existing store)?",!1))return process.stdout.write(`Keeping existing project files. `),{created:!1}}else i.partiallyInitialised?(process.stdout.write(` Project knowledge base partially initialised at ${e} `),process.stdout.write(` Missing files will be created. `)):process.stdout.write(` Initialising project knowledge base at ${e} -`);_e.mkdirSync(e,{recursive:!0}),(!i.configExists||i.fullyInitialised)&&($.writeConfigFile(n,Ho()),process.stdout.write(` config.json written -`));let o=$.loadConfig(),c=o.provider||null,u=Number.isInteger(o.dimensions)&&o.dimensions>0?o.dimensions:$o;if(!i.storeExists||i.fullyInitialised){let a=await Ur.createStore(u);await Ur.saveStore(a,r),process.stdout.write(` store.msp written (${u} dimensions) -`)}return(!i.metadataExists||i.fullyInitialised)&&(Ur.writeMetadata(s,{provider:c||null,model:c&&o.model?o.model:null,dimensions:c?u:null,last_indexed:null,pending:[]}),process.stdout.write(` metadata.json written -`)),{created:!0,provider:c,dimensions:u}}async function Qo(t,e){let n=$.loadConfig(),r=$.resolveProvider(n);process.stdout.write(` +`);de.mkdirSync(e,{recursive:!0}),(!i.configExists||i.fullyInitialised)&&($.writeConfigFile(n,ho()),process.stdout.write(` config.json written +`));let o=$.loadConfig(),c=o.provider||null,a=Number.isInteger(o.dimensions)&&o.dimensions>0?o.dimensions:co,l=!i.storeExists||i.fullyInitialised;if(l){let u=await lr.createStore(a);await lr.saveStore(u,r),process.stdout.write(` store.msp written (${a} dimensions) +`)}return(!i.metadataExists||i.fullyInitialised||l)&&(lr.writeMetadata(s,{provider:c||null,model:c&&o.model?o.model:null,dimensions:c?a:null,last_indexed:null,pending:[]}),process.stdout.write(` metadata.json written +`)),{created:!0,provider:c,dimensions:a}}async function xo(t,e){let n=$.loadConfig(),r=$.resolveProvider(n);process.stdout.write(` Initial indexing `),process.stdout.write(`---------------- `);try{await t(e||{},n,r)}catch(s){process.stderr.write(` Initial indexing hit an error: ${s.message} Project is initialised; run \`knowledge index\` later to retry. -`)}}async function Ef(t,e,n){qo();let r=Se.resolve(process.cwd(),".workflows");_e.existsSync(r)||(process.stderr.write(`No .workflows/ directory found. Initialise a workflow project first. -`),process.exit(1));let s=zo(),i;try{process.stdout.write(` +`)}}async function pl(t,e,n){ao();let r=fe.resolve($.findProjectRoot(),".workflows");de.existsSync(r)||(process.stderr.write(`No .workflows/ directory found. Initialise a workflow project first. +`),process.exit(1));let s=lo(),i;try{process.stdout.write(` Knowledge base setup `),process.stdout.write(`==================== -`),i=await Jo(s),await Zo(s)}finally{s.close()}await Qo(t,n),process.stdout.write(` +`),i=await go(s),await wo(s)}finally{s.close()}await xo(t,n),process.stdout.write(` Setup complete. `),i.provider?i.previouslyStub&&process.stdout.write("\nUpgraded from stub mode to a configured provider. The existing store was indexed in keyword-only mode \u2014 run `knowledge rebuild` to re-index with embeddings for full hybrid search.\n"):process.stdout.write(` Stub mode: no embedding provider configured. The knowledge base will run in keyword-only (BM25) mode. Semantic search is disabled until you configure a provider. -`)}ec.exports={cmdSetup:Ef,requireTTY:qo,createPrompter:zo,ask:an,askYesNo:Ce,askSecret:Vo,buildSystemConfigOpenAI:Wo,buildSystemConfigStub:Ko,buildProjectConfigEmpty:Ho,detectSystemConfig:Go,detectProjectInit:Yo,validateApiKey:ln,describeValidationError:dn,ensureOpenAIKey:Xo,runSystemConfigStep:Jo,runProjectInitStep:Zo,runInitialIndexStep:Qo,KEYWORD_ONLY_DIMENSIONS:$o,OPENAI_DEFAULT_MODEL:Fo,OPENAI_DEFAULT_DIMENSIONS:Bo}});var T=require("fs"),q=require("path"),E=vr(),sc=Ao(),{StubProvider:Af}=Dr(),{OpenAIProvider:vf}=un(),Fe=Nr(),ic=tc(),Lr=["research","discussion","investigation","specification"],Tf=T.existsSync(q.join(__dirname,"..","..","skills","workflow-manifest","scripts","manifest.cjs"))?q.join(__dirname,"..","..","skills","workflow-manifest","scripts","manifest.cjs"):q.join(__dirname,"..","..","workflow-manifest","scripts","manifest.cjs"),ct=[1e3,2e3,4e3],Df=5,jr=1536;function oc(t){let e=[],n={},r=0;for(;r [options] +`)}So.exports={cmdSetup:pl,requireTTY:ao,createPrompter:lo,ask:qt,askYesNo:Me,askSecret:uo,buildSystemConfigOpenAI:fo,buildSystemConfigStub:ur,buildProjectConfigEmpty:ho,detectSystemConfig:po,detectProjectInit:mo,validateApiKey:Kt,describeValidationError:Gt,ensureOpenAIKey:yo,runSystemConfigStep:go,runProjectInitStep:wo,runInitialIndexStep:xo,KEYWORD_ONLY_DIMENSIONS:co,OPENAI_DEFAULT_MODEL:io,OPENAI_DEFAULT_DIMENSIONS:oo}});var E=require("fs"),z=require("path"),v=tr(),Eo=ji(),{StubProvider:ml}=rr(),{OpenAIProvider:gl,AuthError:mr}=jt(),Y=ar(),ko=Io(),gr=["research","discussion","investigation","specification"],yl=(()=>{let t=z.join(__dirname,"..","..","skills","workflow-manifest","scripts","manifest.cjs"),e=z.join(__dirname,"..","..","workflow-manifest","scripts","manifest.cjs");if(E.existsSync(t))return t;if(E.existsSync(e))return e;throw new Error(`Could not locate manifest.cjs. Tried: + ${t} + ${e} +This is an installation problem \u2014 the knowledge CLI cannot work without the manifest CLI.`)})(),rt=[1e3,2e3,4e3],wl=5,hr=10,yr=1536,bo=!1,U=class extends Error{constructor(e){super(e),this.name="UserError"}};function vo(t){let e=[],n={},r=[],s=0;for(;s [options] Commands: index Index a file or all pending artifacts @@ -132,62 +146,93 @@ Commands: rebuild Rebuild the knowledge base from scratch setup Interactive setup wizard -Options: - --work-type Filter by work type - --work-unit Re-rank boost for this work unit (not a filter) - --phase Filter by phase - --topic Filter by topic - --limit Limit number of results - --dry-run Preview without making changes`;function ce(){return q.resolve(process.cwd(),".workflows",".knowledge")}function ue(){return q.join(ce(),"store.msp")}function be(){return q.join(ce(),"metadata.json")}function Te(){return q.join(ce(),".lock")}function Mf(t){return new Promise(e=>setTimeout(e,t))}async function ut(t,e){let n=e&&e.maxAttempts||3,r=e&&e.backoff||ct,s;for(let i=0;i Filter by work type + --work-unit Filter by work unit + --phase Filter by phase + --topic Filter by topic + +Re-ranking (query only, additive; repeat for multiple boosts): + --boost: Boost chunks matching : by +0.1 + Valid fields: work-unit, work-type, phase, + topic, confidence + +Other options: + --limit Limit number of results + --dry-run Preview without making changes + --help, -h Show this usage and exit 0`;function ee(){return z.resolve(Y.findProjectRoot(),".workflows",".knowledge")}function H(){return z.join(ee(),"store.msp")}function q(){return z.join(ee(),"metadata.json")}function ie(){return z.join(ee(),".lock")}function xl(t){return new Promise(e=>setTimeout(e,t))}async function st(t,e){let n=e&&e.maxAttempts||3,r=e&&e.backoff||rt,s;for(let i=0;iFr(s,o,n,r),{maxAttempts:3,backoff:ct});process.stdout.write(`Indexed ${c} chunks from ${s} -`),await lc(n,r,Df)}async function Fr(t,e,n,r){let s=Of(e.workUnit),i=q.join(__dirname,"..","chunking",e.phase+".json");if(!T.existsSync(i))throw new Error(`Chunking config not found: ${i}`);let o=JSON.parse(T.readFileSync(i,"utf8")),c=q.resolve(t),u=T.readFileSync(c,"utf8"),a=sc.chunk(u,o);if(a.length===0)throw new Error(`No chunks produced from ${t}. Refusing to index an empty file \u2014 this would silently wipe any existing indexed chunks for this topic. Use \`knowledge remove\` explicitly if that is what you want.`);let l=ce(),d=ue(),f=be(),p=Te();T.existsSync(l)||T.mkdirSync(l,{recursive:!0});let g,y,h=T.existsSync(d),m=T.existsSync(f);h&&(g=await E.loadStore(d)),m&&(y=E.readMetadata(f),Array.isArray(y.pending)||(y.pending=[]));let w,S;if(y){let A=uc(y,n,r);w=A.mode,S=A.provider}else r?(w="full",S=r):(w="keyword-only",S=null);if(!g){let A=S?S.dimensions():n.dimensions||jr;g=await E.createStore(A)}let _=null;if(w==="full"&&S&&a.length>0){let A=a.map(k=>k.content);_=await S.embedBatch(A)}let I=Date.now(),v=o.confidence||"medium",D=a.map((A,k)=>{let De=String(k+1).padStart(3,"0"),X={id:`${e.workUnit}-${e.phase}-${e.topic}-${De}`,content:A.content,work_unit:e.workUnit,work_type:s,phase:e.phase,topic:e.topic,confidence:v,source_file:t,timestamp:I};return _&&(X.embedding=_[k]),X});return await E.withLock(p,async()=>{h?g=await E.loadStore(d):T.existsSync(d)&&(g=await E.loadStore(d)),await E.removeByIdentity(g,{work_unit:e.workUnit,phase:e.phase,topic:e.topic});for(let k of D)await E.insertDocument(g,k);await E.saveStore(g,d);let A=T.existsSync(f)?E.readMetadata(f):null;if(A)A.last_indexed=new Date().toISOString(),Array.isArray(A.pending)||(A.pending=[]),E.writeMetadata(f,A);else{let k={provider:S?n.provider:null,model:S?S.model():null,dimensions:S?S.dimensions():null,last_indexed:new Date().toISOString(),pending:[]};E.writeMetadata(f,k)}}),D.length}function ot(t){let{execFileSync:e}=require("child_process");return e("node",[Tf,...t],{cwd:process.cwd(),encoding:"utf8",stdio:["pipe","pipe","pipe"]})}async function ac(t,e,n,r){return(await E.searchFulltext(t,{term:"",where:{work_unit:{eq:e},phase:{eq:n},topic:{eq:r}},limit:1})).length>0}function Br(){let t=[],e;try{let n=ot(["list"]);e=JSON.parse(n)}catch{return t}if(!Array.isArray(e)||e.length===0)return t;for(let n of e){let r=n.name;if(r&&n.status!=="cancelled")for(let s of Lr){let i=n.phases&&n.phases[s];if(!(!i||!i.items)){for(let[o,c]of Object.entries(i.items))if(!(!c||c.status!=="completed"))try{let a=ot(["resolve",`${r}.${s}.${o}`]).trim();a&&T.existsSync(q.resolve(a))&&t.push({file:a,workUnit:r,phase:s,topic:o})}catch{}}}}return t}async function fn(t,e,n){let r=Br(),s=ce(),i=ue();T.existsSync(s)||T.mkdirSync(s,{recursive:!0});let o=null;T.existsSync(i)&&(o=await E.loadStore(i));let c=0,u=0,a=0;for(let l of r){if(o&&await ac(o,l.workUnit,l.phase,l.topic)){a++;continue}try{let d={workUnit:l.workUnit,phase:l.phase,topic:l.topic},f=await ut(()=>Fr(l.file,d,e,n),{maxAttempts:3,backoff:ct});process.stdout.write(`Indexing ${l.file}... ${f} chunks -`),c++,u+=f,T.existsSync(i)&&(o=await E.loadStore(i))}catch(d){await kf(l.file,d.message),process.stderr.write(`Failed to index ${l.file} after 3 attempts: ${d.message}. Added to pending queue. -`)}}await lc(e,n,1/0),process.stdout.write(`Indexed ${c} files (${u} chunks). ${a} already indexed. -`)}async function kf(t,e){let n=be(),r=ce(),s=Te();T.existsSync(r)||T.mkdirSync(r,{recursive:!0}),await E.withLock(s,async()=>{let i;T.existsSync(n)?i=E.readMetadata(n):i={provider:null,model:null,dimensions:null,last_indexed:null,pending:[]},Array.isArray(i.pending)||(i.pending=[]);let o=i.pending.findIndex(u=>u.file===t),c={file:t,failed_at:new Date().toISOString(),error:e};o>=0?i.pending[o]=c:i.pending.push(c),E.writeMetadata(n,i)})}async function Rr(t){let e=be(),n=Te();T.existsSync(e)&&await E.withLock(n,async()=>{if(!T.existsSync(e))return;let r=E.readMetadata(e);Array.isArray(r.pending)&&(r.pending=r.pending.filter(s=>s.file!==t),E.writeMetadata(e,r))})}async function lc(t,e,n){let r=be();if(!T.existsSync(r))return;let s=E.readMetadata(r);if(!Array.isArray(s.pending)||s.pending.length===0)return;let i=s.pending.slice(0,n);for(let o of i){let c=q.resolve(o.file);if(!T.existsSync(c)){process.stderr.write(`Pending item ${o.file} no longer exists. Removing from queue. -`),await Rr(o.file);continue}let u;try{u=Cr(o.file)}catch{await Rr(o.file);continue}try{await ut(()=>Fr(o.file,u,t,e),{maxAttempts:3,backoff:ct}),await Rr(o.file)}catch{}}}var Nf={high:4,medium:3,"low-medium":2,low:1};function Uf(t){if(typeof t!="string")return null;let e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(t.trim());if(!e){let n=new Date(t);return isNaN(n.getTime())?null:n}return new Date(parseInt(e[1],10),parseInt(e[2],10)-1,parseInt(e[3],10))}function Rf(t){let e=new Date(t),n=e.getFullYear(),r=String(e.getMonth()+1).padStart(2,"0"),s=String(e.getDate()).padStart(2,"0");return`${n}-${r}-${s}`}function Lf(t,e){if(t.length===0)return t;let n=Math.max(...t.map(i=>i.timestamp||0)),r=Math.min(...t.map(i=>i.timestamp||0)),s=n-r||1;return t.map(i=>{let o=i.score||0;e&&i.work_unit===e&&(o+=.1);let c=Nf[i.confidence]||0;o+=c*.01;let u=((i.timestamp||0)-r)/s;return o+=u*.05,Object.assign({},i,{score:o})}).sort((i,o)=>o.score-i.score)}function jf(t,e,n){let r=t.provider,s=t.model,i=t.dimensions;if(r==null)return n?{mode:"upgrade-available",provider:null}:{mode:"keyword-only",provider:null};if(!n)throw new Error(`Provider/model changed since last index. Run \`knowledge rebuild\` to reindex. + Current config has no provider configured.`)}async function Il(t,e,n,r){if(t.length===0)return Ht(e,n,r);let s=t[0],i=z.resolve(s);E.existsSync(i)||(process.stderr.write(`File not found: ${i} +`),process.exit(1));let o=wr(s),c=await st(()=>Sr(s,o,n,r),{maxAttempts:3,backoff:rt});process.stdout.write(`Indexed ${c} chunks from ${s} +`),await Mo(n,r,wl)}async function Sr(t,e,n,r){let s=Sl(e.workUnit),i=z.join(__dirname,"..","chunking",e.phase+".json");if(!E.existsSync(i))throw new U(`Chunking config not found: ${i}`);let o=JSON.parse(E.readFileSync(i,"utf8")),c=z.resolve(t),a=E.readFileSync(c,"utf8"),l=Eo.chunk(a,o);if(l.length===0)throw new U(`No chunks produced from ${t}. Refusing to index an empty file \u2014 this would silently wipe any existing indexed chunks for this topic. Use \`knowledge remove\` explicitly if that is what you want.`);let u=ee(),d=H(),f=q(),h=ie();E.existsSync(u)||E.mkdirSync(u,{recursive:!0});let p,x,g=E.existsSync(d),y=E.existsSync(f);g&&(p=await v.loadStore(d)),y&&(x=v.readMetadata(f),Array.isArray(x.pending)||(x.pending=[]));let I,m;if(x){let D=xr(x,n,r);I=D.mode,m=D.provider}else r?(I="full",m=r):(I="keyword-only",m=null);if(!p){let D=m?m.dimensions():n.dimensions||yr;p=await v.createStore(D)}let w=null;if(I==="full"&&m&&l.length>0){let D=l.map(M=>M.content);w=await m.embedBatch(D)}let S=Date.now(),T=o.confidence||"medium",_=l.map((D,M)=>{let te=String(M+1).padStart(3,"0"),J={id:`${e.workUnit}-${e.phase}-${e.topic}-${te}`,content:D.content,work_unit:e.workUnit,work_type:s,phase:e.phase,topic:e.topic,confidence:T,source_file:t,timestamp:S};return w&&(J.embedding=w[M]),J});return await v.withLock(h,async()=>{if(g?p=await v.loadStore(d):E.existsSync(d)&&(p=await v.loadStore(d)),I==="full"&&E.existsSync(f)){let M=v.readMetadata(f),te=m.dimensions();if(M.provider&&M.dimensions!==te)throw new Error(`Store schema changed during index (concurrent rebuild). Embeddings produced for dims=${te}, store now has dims=${M.dimensions}. Retrying.`)}await v.removeByIdentity(p,{work_unit:e.workUnit,phase:e.phase,topic:e.topic});for(let M of _)await v.insertDocument(p,M);await v.saveStore(p,d);let D=E.existsSync(f)?v.readMetadata(f):null;if(D)D.last_indexed=new Date().toISOString(),Array.isArray(D.pending)||(D.pending=[]),v.writeMetadata(f,D);else{let M={provider:m?n.provider:null,model:m?m.model():null,dimensions:m?m.dimensions():null,last_indexed:new Date().toISOString(),pending:[]};v.writeMetadata(f,M)}}),_.length}function Ne(t){let{execFileSync:e}=require("child_process");return e("node",[yl,...t],{cwd:Y.findProjectRoot(),encoding:"utf8",stdio:["pipe","pipe","pipe"]})}function nt(t,e){if(e&&e.status===2)return;let n=e&&e.stderr?String(e.stderr).trim():e.message;process.stderr.write(`Warning: manifest CLI failed in ${t}: ${n} +`)}async function _o(t,e,n,r){return(await v.searchFulltext(t,{term:"",where:{work_unit:{eq:e},phase:{eq:n},topic:{eq:r}},limit:1})).length>0}function Ir(){let t=[],e;try{let n=Ne(["list"]);e=JSON.parse(n)}catch(n){return nt("discoverArtifacts:list",n),t}if(!Array.isArray(e)||e.length===0)return t;for(let n of e){let r=n.name;if(r&&n.status!=="cancelled")for(let s of gr){let i=n.phases&&n.phases[s];if(!(!i||!i.items)){for(let[o,c]of Object.entries(i.items))if(!(!c||c.status!=="completed"))try{let l=Ne(["resolve",`${r}.${s}.${o}`]).trim();l&&E.existsSync(z.resolve(l))&&t.push({file:l,workUnit:r,phase:s,topic:o})}catch(a){nt(`discoverArtifacts:resolve(${r}.${s}.${o})`,a)}}}}return t}async function Ht(t,e,n){let r=Ir(),s=ee(),i=H(),o=q();if(E.existsSync(s)||E.mkdirSync(s,{recursive:!0}),E.existsSync(o)){let d=v.readMetadata(o);xr(d,e,n)}let c=null;E.existsSync(i)&&(c=await v.loadStore(i));let a=0,l=0,u=0;for(let d of r){if(c&&await _o(c,d.workUnit,d.phase,d.topic)){u++;continue}try{let f={workUnit:d.workUnit,phase:d.phase,topic:d.topic},h=await st(()=>Sr(d.file,f,e,n),{maxAttempts:3,backoff:rt});process.stdout.write(`Indexing ${d.file}... ${h} chunks +`),a++,l+=h,E.existsSync(i)&&(c=await v.loadStore(i))}catch(f){await Do(d.file,f.message),process.stderr.write(`Failed to index ${d.file} after 3 attempts: ${f.message}. Added to pending queue. +`);let h=f instanceof U||f instanceof mr;f.stack&&!h&&process.stderr.write(f.stack+` +`)}}await Mo(e,n,1/0),process.stdout.write(`Indexed ${a} files (${l} chunks). ${u} already indexed. +`)}async function Do(t,e){let n=q(),r=ee(),s=ie();E.existsSync(r)||E.mkdirSync(r,{recursive:!0}),await v.withLock(s,async()=>{let i;E.existsSync(n)?i=v.readMetadata(n):i={provider:null,model:null,dimensions:null,last_indexed:null,pending:[]},Array.isArray(i.pending)||(i.pending=[]);let o=i.pending.findIndex(a=>a.file===t),c={file:t,failed_at:new Date().toISOString(),error:e};if(o>=0){let a=i.pending[o];i.pending[o]={...c,attempts:(a.attempts||1)+1}}else i.pending.push({...c,attempts:1});v.writeMetadata(n,i)})}async function Yt(t){let e=q(),n=ie();E.existsSync(e)&&await v.withLock(n,async()=>{if(!E.existsSync(e))return;let r=v.readMetadata(e);Array.isArray(r.pending)&&(r.pending=r.pending.filter(s=>s.file!==t),v.writeMetadata(e,r))})}async function Mo(t,e,n){let r=q();if(!E.existsSync(r))return;let s=v.readMetadata(r);if(!Array.isArray(s.pending)||s.pending.length===0)return;let i=s.pending.slice(0,n);for(let o of i){if((o.attempts||0)>=hr){process.stderr.write(`Pending item ${o.file} exceeded ${hr} attempts \u2014 evicting. Last error: ${o.error} +`),await Yt(o.file);continue}let c=z.resolve(o.file);if(!E.existsSync(c)){process.stderr.write(`Pending item ${o.file} no longer exists. Removing from queue. +`),await Yt(o.file);continue}let a;try{a=wr(o.file)}catch{await Yt(o.file);continue}try{await st(()=>Sr(o.file,a,t,e),{maxAttempts:3,backoff:rt}),await Yt(o.file)}catch(l){await Do(o.file,l.message)}}}var pr=10;function ye(t){return`${t.workUnit}|${t.phase||""}|${t.topic||""}`}async function No(t,e){let n=q(),r=ee(),s=ie();E.existsSync(r)||E.mkdirSync(r,{recursive:!0}),await v.withLock(s,async()=>{let i;E.existsSync(n)?i=v.readMetadata(n):i={provider:null,model:null,dimensions:null,last_indexed:null,pending:[],pending_removals:[]},Array.isArray(i.pending_removals)||(i.pending_removals=[]);let o=ye(t),c=i.pending_removals.findIndex(l=>ye(l)===o),a={workUnit:t.workUnit,phase:t.phase||null,topic:t.topic||null,queued_at:new Date().toISOString(),error:e};if(c>=0){let l=i.pending_removals[c];i.pending_removals[c]={...a,attempts:(l.attempts||0)+1}}else i.pending_removals.push({...a,attempts:1});v.writeMetadata(n,i)})}async function Ao(t){let e=q(),n=ie();E.existsSync(e)&&await v.withLock(n,async()=>{if(!E.existsSync(e))return;let r=v.readMetadata(e);if(!Array.isArray(r.pending_removals))return;let s=ye(t);r.pending_removals=r.pending_removals.filter(i=>ye(i)!==s),v.writeMetadata(e,r)})}async function Uo(){let t=q();if(!E.existsSync(t))return;let e=v.readMetadata(t);if(!Array.isArray(e.pending_removals)||e.pending_removals.length===0)return;let n=e.pending_removals.slice();for(let r of n){if((r.attempts||0)>=pr){process.stderr.write(`Pending removal for ${ye(r)} exceeded ${pr} attempts \u2014 evicting. +`),await Ao(r);continue}try{await Oo({workUnit:r.workUnit,phase:r.phase,topic:r.topic}),process.stderr.write(`Drained pending removal for ${ye(r)}. +`),await Ao(r)}catch(s){try{await No({workUnit:r.workUnit,phase:r.phase,topic:r.topic},s.message)}catch{}}}}async function Oo(t){let e=H(),n=ie();if(!E.existsSync(e))return 0;let r=0;return await v.withLock(n,async()=>{let s=await v.loadStore(e),i={work_unit:{eq:t.workUnit}};t.phase&&(i.phase={eq:t.phase}),t.topic&&(i.topic={eq:t.topic}),r=await v.removeByFilter(s,i),await v.saveStore(s,e)}),r}var bl={high:4,medium:3,"low-medium":2,low:1};function Al(t){if(typeof t!="string")return null;let e=/^(\d{4})-(\d{2})-(\d{2})$/.exec(t.trim());if(!e){let n=new Date(t);return isNaN(n.getTime())?null:n}return new Date(parseInt(e[1],10),parseInt(e[2],10)-1,parseInt(e[3],10))}function El(t){let e=new Date(t),n=e.getFullYear(),r=String(e.getMonth()+1).padStart(2,"0"),s=String(e.getDate()).padStart(2,"0");return`${n}-${r}-${s}`}var fr={"work-unit":"work_unit","work-type":"work_type",phase:"phase",topic:"topic",confidence:"confidence"},kl=.1;function vl(t,e){if(t.length===0)return t;let n=Math.max(...t.map(i=>i.timestamp||0)),r=Math.min(...t.map(i=>i.timestamp||0)),s=n-r||1;return t.map(i=>{let o=i.score||0;if(Array.isArray(e))for(let l of e)i[l.field]===l.value&&(o+=kl);let c=bl[i.confidence]||0;o+=c*.01;let a=((i.timestamp||0)-r)/s;return o+=a*.05,Object.assign({},i,{score:o})}).sort((i,o)=>o.score-i.score)}function Tl(t){let e=[];for(let n of t)(!n.field||!fr[n.field])&&(process.stderr.write(`Unknown --boost field: "${n.field}". Valid fields: ${Object.keys(fr).join(", ")} +`),process.exit(1)),(n.value==null||n.value==="")&&(process.stderr.write(`--boost:${n.field} requires a value +`),process.exit(1)),e.push({field:fr[n.field],value:n.value});return e}function _l(t,e,n){let r=t.provider,s=t.model,i=t.dimensions;if(r==null)return n?{mode:"upgrade-available",provider:null}:{mode:"keyword-only",provider:null};if(!n)throw new U(`Provider/model changed since last index. Run \`knowledge rebuild\` to reindex. Store was indexed with: provider=${r}, model=${s} - Current config has no provider configured.`);let o=n.model(),c=n.dimensions();if(r===e.provider&&s===o&&i===c)return{mode:"full",provider:n};throw new Error(`Provider/model changed since last index. Run \`knowledge rebuild\` to reindex. + Current config has no provider configured.`);let o=n.model(),c=n.dimensions();if(r===e.provider&&s===o&&i===c)return{mode:"full",provider:n};throw new U(`Provider/model changed since last index. Run \`knowledge rebuild\` to reindex. Store: provider=${r}, model=${s}, dimensions=${i} - Config: provider=${e.provider}, model=${o}, dimensions=${c}`)}async function Cf(t,e,n,r){t.length===0&&(process.stderr.write(`Usage: knowledge query [...] [--phase ...] [--work-type ...] [--work-unit ...] [--limit N] -`),process.exit(1));let s=t,i=e.limit||10,o=ue(),c=be();if(!T.existsSync(o)){process.stdout.write(`[0 results] -`);return}let u=await E.loadStore(o),a="keyword-only",l=null,d=null;T.existsSync(c)||(process.stderr.write("metadata.json missing but store exists. Run `knowledge rebuild` to fix.\n"),process.exit(1));let f=E.readMetadata(c),p=jf(f,n,r);a=p.mode,l=p.provider,a==="keyword-only"?d="[keyword-only mode \u2014 configure embedding provider for semantic search]":a==="upgrade-available"&&(d="[keyword-only mode but embedding provider configured \u2014 run knowledge rebuild for full hybrid search]");let g={};if(e.phase){let _=e.phase.split(",").map(I=>I.trim());g.phase=_.length===1?{eq:_[0]}:{in:_}}if(e.workType){let _=e.workType.split(",").map(I=>I.trim());g.work_type=_.length===1?{eq:_[0]}:{in:_}}if(e.topic){let _=e.topic.split(",").map(I=>I.trim());g.topic=_.length===1?{eq:_[0]}:{in:_}}let y=n.similarity_threshold||.8,h=Object.keys(g).length>0?g:void 0,m=new Map;for(let _ of s){let I;if(a==="full"&&l){let v=await ut(()=>l.embed(_),{maxAttempts:3,backoff:ct});I=await E.searchHybrid(u,{term:_,vector:v,where:h,limit:i*2,similarity:y})}else I=await E.searchFulltext(u,{term:_,where:h,limit:i*2});for(let v of I){let D=m.get(v.id);(!D||v.score>D.score)&&m.set(v.id,v)}}let w=Lf(Array.from(m.values()),e.workUnit);w.length>i&&(w=w.slice(0,i));let S=[];d&&S.push(d),S.push(`[${w.length} results]`);for(let _ of w){S.push("");let I=Rf(_.timestamp);S.push(`[${_.phase} | ${_.work_unit}/${_.topic} | ${_.confidence} | ${I}]`),S.push(_.content),S.push(`Source: ${_.source_file}`)}process.stdout.write(S.join(` + Config: provider=${e.provider}, model=${o}, dimensions=${c}`)}async function Dl(t,e,n,r){t.length===0&&(process.stderr.write(`Usage: knowledge query [...] [--work-unit ...] [--work-type ...] [--phase ...] [--topic ...] [--boost: ]... [--limit N] +`),process.exit(1));for(let S of t)if(typeof S!="string"||S.trim()==="")throw new U("Empty search term. `knowledge query` requires at least one non-empty positional term. If you intended to list everything indexed, use `knowledge status` instead.");let s=t,i=e.limit||10,o=H(),c=q();if(!E.existsSync(o)){process.stdout.write(`[0 results] +`);return}let a=await v.loadStore(o),l="keyword-only",u=null,d=null;E.existsSync(c)||(process.stderr.write("metadata.json missing but store exists. Run `knowledge rebuild` to fix.\n"),process.exit(1));let f=v.readMetadata(c),h=_l(f,n,r);l=h.mode,u=h.provider,l==="keyword-only"?d="[keyword-only mode \u2014 configure embedding provider for semantic search]":l==="upgrade-available"&&(d="[keyword-only mode but embedding provider configured \u2014 run knowledge rebuild for full hybrid search]");let p={};if(e.phase){let S=e.phase.split(",").map(T=>T.trim());p.phase=S.length===1?{eq:S[0]}:{in:S}}if(e.workType){let S=e.workType.split(",").map(T=>T.trim());p.work_type=S.length===1?{eq:S[0]}:{in:S}}if(e.workUnit){let S=e.workUnit.split(",").map(T=>T.trim());p.work_unit=S.length===1?{eq:S[0]}:{in:S}}if(e.topic){let S=e.topic.split(",").map(T=>T.trim());p.topic=S.length===1?{eq:S[0]}:{in:S}}let x=n.similarity_threshold??.8,g=Object.keys(p).length>0?p:void 0,y=new Map;for(let S of s){let T;if(l==="full"&&u){let _=await st(()=>u.embed(S),{maxAttempts:3,backoff:rt});T=await v.searchHybrid(a,{term:S,vector:_,where:g,limit:i*2,similarity:x})}else T=await v.searchFulltext(a,{term:S,where:g,limit:i*2});for(let _ of T){let D=y.get(_.id);(!D||_.score>D.score)&&y.set(_.id,_)}}let I=Tl(e.boosts),m=vl(Array.from(y.values()),I);m.length>i&&(m=m.slice(0,i));let w=[];d&&w.push(d),w.push(`[${m.length} results]`);for(let S of m){w.push("");let T=El(S.timestamp);w.push(`[${S.phase} | ${S.work_unit}/${S.topic} | ${S.confidence} | ${T}]`),w.push(S.content),w.push(`Source: ${S.source_file}`)}process.stdout.write(w.join(` `)+` -`)}async function Ff(){let t=ce(),e=q.join(t,"config.json"),n=ue();if(!T.existsSync(t)){process.stdout.write(`not-ready -`);return}if(!T.existsSync(e)){process.stdout.write(`not-ready -`);return}if(!T.existsSync(n)){process.stdout.write(`not-ready -`);return}try{await E.loadStore(n)}catch{process.stdout.write(`not-ready +`)}async function Ml(){let t=ee(),e=z.join(t,"config.json"),n=H();if(!E.existsSync(t)){process.stdout.write(`not-ready +`);return}if(!E.existsSync(e)){process.stdout.write(`not-ready +`);return}try{Y.readConfigFile(e)}catch(r){process.stderr.write(`config error: ${r.message} +`),process.stdout.write(`not-ready +`);return}if(!E.existsSync(n)){process.stdout.write(`not-ready +`);return}try{await v.loadStore(n)}catch{process.stdout.write(`not-ready `);return}process.stdout.write(`ready -`)}async function Bf(){let t=ce(),e=ue(),n=be(),r=[];if(r.push("=== Knowledge Base Status ==="),r.push(""),!T.existsSync(e)){r.push("Store: not initialized"),r.push("Run `knowledge index` to build the index."),process.stdout.write(r.join(` +`)}async function Nl(){let t=ee(),e=H(),n=q(),r=[];if(r.push("=== Knowledge Base Status ==="),r.push(""),!E.existsSync(e)){r.push("Store: not initialized"),r.push("Run `knowledge index` to build the index."),process.stdout.write(r.join(` `)+` -`);return}let s=await E.loadStore(e),i=await E.searchFulltext(s,{term:"",limit:1e5});r.push(`Total chunks: ${i.length}`);let o={},c={},u={};for(let h of i)o[h.work_unit]=(o[h.work_unit]||0)+1,c[h.phase]=(c[h.phase]||0)+1,u[h.work_type]=(u[h.work_type]||0)+1;if(Object.keys(o).length>0){r.push(""),r.push("By work unit:");for(let[h,m]of Object.entries(o))r.push(` ${h}: ${m}`)}if(Object.keys(c).length>0){r.push(""),r.push("By phase:");for(let[h,m]of Object.entries(c))r.push(` ${h}: ${m}`)}if(Object.keys(u).length>0){r.push(""),r.push("By work type:");for(let[h,m]of Object.entries(u))r.push(` ${h}: ${m}`)}r.push("");let l=(T.statSync(e).size/1024).toFixed(1);if(r.push(`Store size: ${l} KB`),T.existsSync(n)){let h=E.readMetadata(n);if(r.push(`Last indexed: ${h.last_indexed||"unknown"}`),h.provider?(r.push(`Provider: ${h.provider} (model: ${h.model}, dimensions: ${h.dimensions})`),r.push("Mode: Full (hybrid search)")):(r.push("Provider: none"),r.push("Mode: Keyword-only")),Array.isArray(h.pending)&&h.pending.length>0){r.push(""),r.push(`Pending items: ${h.pending.length}`);for(let w of h.pending)r.push(` ${w.file} \u2014 ${w.error} (${w.failed_at})`)}let m;try{m=Fe.loadConfig()}catch{m=null}if(m){let w=Fe.resolveProvider(m);h.provider&&w&&(h.provider!==m.provider||h.model!==w.model()||h.dimensions!==w.dimensions())&&(r.push(""),r.push("WARNING: Config has changed since last index. Run `knowledge rebuild` to reindex.")),(h.provider===null||h.provider===void 0)&&w&&(r.push(""),r.push("NOTE: Keyword-only mode but embedding provider configured. Run `knowledge rebuild` for full hybrid search."))}}else r.push("Metadata: missing (run `knowledge rebuild` to fix)");let d=[],f=new Set;for(let h of i)f.has(h.source_file)||(f.add(h.source_file),T.existsSync(q.resolve(h.source_file))||d.push(h.source_file));if(d.length>0){r.push(""),r.push(`Orphaned chunks (source deleted): ${d.length} files`);for(let h of d)r.push(` ${h}`)}try{let h=Br(),m=[];for(let w of h)await ac(s,w.workUnit,w.phase,w.topic)||m.push(w.file);if(m.length>0){r.push(""),r.push(`Unindexed completed artifacts: ${m.length}`);for(let w of m)r.push(` ${w}`)}}catch{}let p=[];for(let h of Object.keys(o)){let m=dc(h);m&&m.status==="cancelled"&&p.push(`Cancelled work unit still indexed: ${h}`)}let g=i.filter(h=>h.phase==="specification"),y=new Set(g.map(h=>`${h.work_unit}.specification.${h.topic}`));for(let h of y)try{ot(["get",h,"status"]).trim()==="superseded"&&p.push(`Superseded spec still indexed: ${h}`)}catch{}if(p.length>0){r.push(""),r.push("Consistency warnings:");for(let h of p)r.push(` ${h}`)}process.stdout.write(r.join(` +`);return}let s=await v.loadStore(e),i=await v.searchAllFulltext(s);r.push(`Total chunks: ${i.length}`);let o={},c={},a={};for(let m of i)o[m.work_unit]=(o[m.work_unit]||0)+1,c[m.phase]=(c[m.phase]||0)+1,a[m.work_type]=(a[m.work_type]||0)+1;if(Object.keys(o).length>0){r.push(""),r.push("By work unit:");for(let[m,w]of Object.entries(o))r.push(` ${m}: ${w}`)}if(Object.keys(c).length>0){r.push(""),r.push("By phase:");for(let[m,w]of Object.entries(c))r.push(` ${m}: ${w}`)}if(Object.keys(a).length>0){r.push(""),r.push("By work type:");for(let[m,w]of Object.entries(a))r.push(` ${m}: ${w}`)}r.push("");let u=(E.statSync(e).size/1024).toFixed(1);if(r.push(`Store size: ${u} KB`),E.existsSync(n)){let m=v.readMetadata(n);if(r.push(`Last indexed: ${m.last_indexed||"unknown"}`),m.provider?(r.push(`Provider: ${m.provider} (model: ${m.model}, dimensions: ${m.dimensions})`),r.push("Mode: Full (hybrid search)")):(r.push("Provider: none"),r.push("Mode: Keyword-only")),Array.isArray(m.pending)&&m.pending.length>0){r.push(""),r.push(`Pending items: ${m.pending.length}`);for(let S of m.pending){let T=S.attempts||1;r.push(` ${S.file} \u2014 ${S.error} (attempt ${T}/${hr}, ${S.failed_at})`)}}if(Array.isArray(m.pending_removals)&&m.pending_removals.length>0){r.push(""),r.push(`Pending removals: ${m.pending_removals.length}`);for(let S of m.pending_removals)r.push(` ${ye(S)} \u2014 ${S.error} (attempt ${S.attempts||1}/${pr})`)}let w;try{w=Y.loadConfig()}catch{w=null}if(w){let S=Y.resolveProvider(w);m.provider&&S&&(m.provider!==w.provider||m.model!==S.model()||m.dimensions!==S.dimensions())&&(r.push(""),r.push("WARNING: Config has changed since last index. Run `knowledge rebuild` to reindex.")),(m.provider===null||m.provider===void 0)&&S&&(r.push(""),r.push("NOTE: Keyword-only mode but embedding provider configured. Run `knowledge rebuild` for full hybrid search."))}}else r.push("Metadata: missing (run `knowledge rebuild` to fix)");let d=Y.findProjectRoot(),f=[],h=new Set;for(let m of i)h.has(m.source_file)||(h.add(m.source_file),E.existsSync(z.resolve(d,m.source_file))||f.push(m.source_file));if(f.length>0){r.push(""),r.push(`Orphaned chunks (source deleted): ${f.length} files`);for(let m of f)r.push(` ${m}`)}try{let m=Ir(),w=[];for(let S of m)await _o(s,S.workUnit,S.phase,S.topic)||w.push(S.file);if(w.length>0){r.push(""),r.push(`Unindexed completed artifacts: ${w.length}`);for(let S of w)r.push(` ${S}`)}}catch(m){process.stderr.write(`Warning: unindexed-artifact discovery failed: ${m.message} +`)}let p=[],x=null;try{x=JSON.parse(Ne(["list"]))}catch(m){nt("cmdStatus:list",m)}let g=new Map;if(Array.isArray(x))for(let m of x)m&&m.name&&g.set(m.name,m);for(let m of Object.keys(o)){let w=g.get(m);w&&w.status==="cancelled"&&p.push(`Cancelled work unit still indexed: ${m}`)}let y=i.filter(m=>m.phase==="specification"),I=new Set(y.map(m=>`${m.work_unit}.specification.${m.topic}`));for(let m of I){let[w,,S]=m.split("."),T=g.get(w);if(!T||!T.phases||!T.phases.specification||!T.phases.specification.items)continue;let _=T.phases.specification.items[S];_&&_.status==="superseded"&&p.push(`Superseded spec still indexed: ${m}`)}if(p.length>0){r.push(""),r.push("Consistency warnings:");for(let m of p)r.push(` ${m}`)}process.stdout.write(r.join(` `)+` -`)}async function $f(t,e,n,r){let s=ue(),i=be(),o=Te();process.stderr.write(`Warning: This will delete the existing index and rebuild from scratch. +`)}async function Ul(t,e,n,r){let s=H(),i=q(),o=ie();process.stderr.write(`Warning: This will delete the existing index and rebuild from scratch. This is non-deterministic \u2014 the rebuilt index will differ from the original. -Type 'rebuild' to confirm: `),await qf()!=="rebuild"&&(process.stderr.write(`Aborted. -`),process.exit(1)),Br().length===0&&(process.stderr.write(`No completed artifacts found to index. Aborting rebuild \u2014 the existing index has NOT been modified. +Type 'rebuild' to confirm: `),await Ol()!=="rebuild"&&(process.stderr.write(` +Aborted. +`),process.exit(1)),Ir().length===0&&(process.stderr.write(`No completed artifacts found to index. Aborting rebuild \u2014 the existing index has NOT been modified. (If you believe this is wrong, check that .workflows/ exists and that work units have items with status "completed".) -`),process.exit(1)),await E.withLock(o,async()=>{T.existsSync(s)&&T.unlinkSync(s),T.existsSync(i)&&T.unlinkSync(i);let a=r?r.dimensions():n&&n.dimensions||jr,l=await E.createStore(a);await E.saveStore(l,s),E.writeMetadata(i,{provider:r?n.provider:null,model:r?r.model():null,dimensions:r?r.dimensions():null,last_indexed:new Date().toISOString(),pending:[]})}),process.stdout.write(`Deleted existing index. -`),await fn(e,n,r)}function qf(){return new Promise(t=>{let e="",n=!1,r=()=>{if(n)return;n=!0;let o=e.search(/\r|\n/),c=o===-1?e:e.slice(0,o);t(c.trim())};process.stdin.setEncoding("utf8");let s=o=>{e+=o,/\r|\n/.test(e)&&(process.stdin.removeListener("data",s),process.stdin.removeListener("end",i),r())},i=()=>{process.stdin.removeListener("data",s),r()};process.stdin.on("data",s),process.stdin.once("end",i),process.stdin.resume()})}async function zf(t,e){e.workUnit||(process.stderr.write(`Usage: knowledge remove --work-unit [--phase

] [--topic ] +`),process.exit(1));let l=s+".bak",u=i+".bak";await v.withLock(o,async()=>{E.existsSync(l)&&E.unlinkSync(l),E.existsSync(u)&&E.unlinkSync(u),E.existsSync(s)&&E.renameSync(s,l),E.existsSync(i)&&E.renameSync(i,u);let d=r?r.dimensions():n&&n.dimensions||yr,f=await v.createStore(d);await v.saveStore(f,s),v.writeMetadata(i,{provider:r?n.provider:null,model:r?r.model():null,dimensions:r?r.dimensions():null,last_indexed:new Date().toISOString(),pending:[]})}),process.stdout.write(`Deleted existing index. +`);try{await Ht(e,n,r)}catch(d){try{await v.withLock(o,async()=>{E.existsSync(l)&&(E.existsSync(s)&&E.unlinkSync(s),E.renameSync(l,s)),E.existsSync(u)&&(E.existsSync(i)&&E.unlinkSync(i),E.renameSync(u,i))}),process.stderr.write(`Rebuild failed; restored previous index from backup. +`)}catch(f){process.stderr.write(`Rebuild failed and rollback also failed. Previous index is at: + ${l} + ${u} +Rename them back manually to recover. Rollback error: ${f.message} +`)}throw d}E.existsSync(l)&&E.unlinkSync(l),E.existsSync(u)&&E.unlinkSync(u)}function Ol(){return new Promise(t=>{let e="",n=!1,r=()=>{if(n)return;n=!0;let o=e.search(/\r|\n/),c=o===-1?e:e.slice(0,o);t(c.trim())};process.stdin.setEncoding("utf8");let s=o=>{e+=o,/\r|\n/.test(e)&&(process.stdin.removeListener("data",s),process.stdin.removeListener("end",i),process.stdin.pause(),r())},i=()=>{process.stdin.removeListener("data",s),r()};process.stdin.on("data",s),process.stdin.once("end",i),process.stdin.resume()})}async function Pl(t,e){e.workUnit||(process.stderr.write(`Usage: knowledge remove --work-unit [--phase

] [--topic ] [--dry-run] `),process.exit(1)),e.topic&&!e.phase&&(process.stderr.write(`Error: --topic requires --phase -`),process.exit(1));let n=ue(),r=Te();if(!T.existsSync(n)){let o=rc(e);process.stdout.write(`Removed 0 chunks for ${o} -`);return}let s=0;await E.withLock(r,async()=>{let o=await E.loadStore(n),c={work_unit:{eq:e.workUnit}};e.phase&&(c.phase={eq:e.phase}),e.topic&&(c.topic={eq:e.topic}),s=await E.removeByFilter(o,c),await E.saveStore(o,n)});let i=rc(e);process.stdout.write(`Removed ${s} chunks for ${i} -`)}function rc(t){return t.topic?`${t.workUnit}/${t.phase}/${t.topic}`:t.phase?`${t.workUnit}/${t.phase}`:`${t.workUnit} (all phases)`}function dc(t){try{let e=ot(["get",t,"status"]).trim(),n=null;try{n=ot(["get",t,"completed_at"]).trim(),(n===""||n==="undefined"||n==="null")&&(n=null)}catch{}return{status:e,completed_at:n}}catch{return null}}async function Vf(t,e,n){let r=ue(),s=Te(),i=n&&n.decay_months!==void 0?n.decay_months:Fe.DEFAULTS.decay_months;if(i===!1){process.stdout.write(`Compaction disabled +`),process.exit(1));let n=!1;try{Ne(["project","get",e.workUnit])}catch(i){if(i&&i.status===2){let o=H(),c=0;if(E.existsSync(o)){let a=await v.loadStore(o),l={work_unit:{eq:e.workUnit}};e.phase&&(l.phase={eq:e.phase}),e.topic&&(l.topic={eq:e.topic}),c=await v.countByFilter(a,l)}if(c===0)throw new U(`Work unit "${e.workUnit}" not found in project manifest, and no matching chunks exist in the knowledge base. + Check the name with \`knowledge status\`.`);n=!0,process.stderr.write(`Work unit "${e.workUnit}" is not in the project manifest, but ${c} chunks remain in the store. Removing as an orphan cleanup. +`)}else throw i}let r=H(),s=Rl(e)+(n?" (orphan cleanup)":"");if(e.dryRun){if(!E.existsSync(r)){process.stdout.write(`Would remove 0 chunks for ${s} (store not initialised) +`);return}let i=await v.loadStore(r),o={work_unit:{eq:e.workUnit}};e.phase&&(o.phase={eq:e.phase}),e.topic&&(o.topic={eq:e.topic});let c=await v.countByFilter(i,o);process.stdout.write(`Would remove ${c} chunks for ${s} +`);return}if(await Uo(),!E.existsSync(r)){process.stdout.write(`Removed 0 chunks for ${s} +`);return}try{let i=await Oo(e);process.stdout.write(`Removed ${i} chunks for ${s} +`)}catch(i){await No(e,i.message),process.stderr.write(`Removal of ${s} failed (${i.message}). Queued for automatic retry on next remove/compact. +`),process.exit(1)}}function Rl(t){return t.topic?`${t.workUnit}/${t.phase}/${t.topic}`:t.phase?`${t.workUnit}/${t.phase}`:`${t.workUnit} (all phases)`}function Ll(t){try{let e=Ne(["get",t,"status"]).trim(),n=null;try{n=Ne(["get",t,"completed_at"]).trim(),(n===""||n==="undefined"||n==="null")&&(n=null)}catch(r){nt(`getWorkUnitMeta:get(${t}.completed_at)`,r)}return{status:e,completed_at:n}}catch(e){return nt(`getWorkUnitMeta:get(${t}.status)`,e),null}}async function Cl(t,e,n){await Uo();let r=H(),s=ie(),i=n&&n.decay_months!==void 0?n.decay_months:Y.DEFAULTS.decay_months;if(i===!1||i===null){process.stdout.write(`Compaction disabled `);return}(!Number.isInteger(i)||i<0)&&(process.stderr.write(`Invalid decay_months: ${JSON.stringify(i)}. Expected false or a non-negative integer. -`),process.exit(1));let o=i;if(!T.existsSync(r))return;let c=await E.loadStore(r),u=new Date,a=new Date(u);a.setMonth(a.getMonth()-o);let l=await E.searchFulltext(c,{term:"",limit:1e5});if(l.length===0)return;let d={};for(let h of l)d[h.work_unit]||(d[h.work_unit]=[]),d[h.work_unit].push(h);let f=[],p=[];for(let[h,m]of Object.entries(d)){let w=dc(h);if(!w||w.status!=="completed"||!w.completed_at)continue;let S=Uf(w.completed_at);if(!S||isNaN(S.getTime()))continue;let _=new Date(S);if(_.setMonth(_.getMonth()+o),_>u)continue;let I=m.filter(D=>D.phase!=="specification");if(I.length===0)continue;let v=new Set(I.map(D=>D.phase));f.push({workUnit:h,count:I.length,phases:v});for(let D of I)p.push({work_unit:D.work_unit,phase:D.phase,topic:D.topic})}if(f.length===0)return;let g=f.reduce((h,m)=>h+m.count,0);if(e.dryRun){let h=[];h.push(`[dry-run] Compacted: removed ${g} chunks from ${f.length} work units (completed > ${o} months ago)`);for(let m of f)h.push(` \u2022 ${m.workUnit}: ${m.count} chunks (${Array.from(m.phases).join(", ")})`);process.stdout.write(h.join(` +`),process.exit(1));let o=i;if(!E.existsSync(r))return;let c=await v.loadStore(r),a=new Date,l=new Date(a);l.setMonth(l.getMonth()-o);let u=await v.searchAllFulltext(c);if(u.length===0)return;let d={};for(let g of u)d[g.work_unit]||(d[g.work_unit]=[]),d[g.work_unit].push(g);let f=[],h=[];for(let[g,y]of Object.entries(d)){let I=Ll(g);if(!I||I.status!=="completed"||!I.completed_at)continue;let m=Al(I.completed_at);if(!m||isNaN(m.getTime()))continue;let w=new Date(m);if(w.setMonth(w.getMonth()+o),w>a)continue;let S=y.filter(_=>_.phase!=="specification");if(S.length===0)continue;let T=new Set(S.map(_=>_.phase));f.push({workUnit:g,count:S.length,phases:T});for(let _ of S)h.push({work_unit:_.work_unit,phase:_.phase,topic:_.topic})}if(f.length===0)return;let p=f.reduce((g,y)=>g+y.count,0);if(e.dryRun){let g=[];g.push(`[dry-run] Compacted: removed ${p} chunks from ${f.length} work units (completed > ${o} months ago)`);for(let y of f)g.push(` \u2022 ${y.workUnit}: ${y.count} chunks (${Array.from(y.phases).join(", ")})`);process.stdout.write(g.join(` `)+` -`);return}await E.withLock(s,async()=>{let h=await E.loadStore(r),m=new Set;for(let w of p){let S=`${w.work_unit}|${w.phase}|${w.topic}`;m.has(S)||(m.add(S),await E.removeByIdentity(h,w))}await E.saveStore(h,r)});let y=[];y.push(`Compacted: removed ${g} chunks from ${f.length} work units (completed > ${o} months ago)`);for(let h of f)y.push(` \u2022 ${h.workUnit}: ${h.count} chunks (${Array.from(h.phases).join(", ")})`);process.stdout.write(y.join(` +`);return}await v.withLock(s,async()=>{let g=await v.loadStore(r),y=new Set;for(let I of h){let m=`${I.work_unit}|${I.phase}|${I.topic}`;y.has(m)||(y.add(m),await v.removeByIdentity(g,I))}await v.saveStore(g,r)});let x=[];x.push(`Compacted: removed ${p} chunks from ${f.length} work units (completed > ${o} months ago)`);for(let g of f)x.push(` \u2022 ${g.workUnit}: ${g.count} chunks (${Array.from(g.phases).join(", ")})`);process.stdout.write(x.join(` `)+` -`)}async function fc(){let t=process.argv.slice(2),{positional:e,flags:n}=oc(t),r=e[0],s=e.slice(1),i=cc(n);r||(process.stderr.write(nc+` -`),process.exit(1));let o=null,c=null;switch(["index","query","rebuild","compact"].includes(r)&&(o=Fe.loadConfig(),c=Fe.resolveProvider(o)),r){case"index":await Pf(s,i,o,c);break;case"query":await Cf(s,i,o,c);break;case"check":await Ff(s,i,o,c);break;case"status":await Bf();break;case"remove":await zf(s,i,o,c);break;case"compact":await Vf(s,i,o,c);break;case"rebuild":await $f(s,i,o,c);break;case"setup":await ic.cmdSetup(fn,s,i);break;default:process.stderr.write(`Unknown command "${r}". +`)}async function Po(){let t=process.argv.slice(2);(t.includes("--help")||t.includes("-h")||t[0]==="help")&&(process.stdout.write(dr+` +`),process.exit(0));let{positional:e,flags:n,boosts:r}=vo(t),s=e[0],i=e.slice(1),o=To(n,r);s||(process.stderr.write(dr+` +`),process.exit(1));let c=null,a=null;switch(["index","query","rebuild","compact"].includes(s)&&(c=Y.loadConfig(),a=Y.resolveProvider(c)),s){case"index":await Il(i,o,c,a);break;case"query":await Dl(i,o,c,a);break;case"check":await Ml(i,o,c,a);break;case"status":await Nl();break;case"remove":await Pl(i,o,c,a);break;case"compact":await Cl(i,o,c,a);break;case"rebuild":await Ul(i,o,c,a);break;case"setup":await ko.cmdSetup(Ht,i,o);break;default:process.stderr.write(`Unknown command "${s}". -${nc} -`),process.exit(1)}}module.exports={parseArgs:oc,buildOptions:cc,deriveIdentity:Cr,resolveProviderState:uc,withRetry:ut,main:fc,cmdIndexBulk:fn,StubProvider:Af,OpenAIProvider:vf,store:E,chunker:sc,config:Fe,setup:ic,knowledgeDir:ce,storePath:ue,metadataPath:be,lockFilePath:Te,INDEXED_PHASES:Lr,KEYWORD_ONLY_DIMENSIONS:jr};require.main===module&&fc().catch(t=>{process.stderr.write(String(t&&t.stack?t.stack:t)+` +${dr} +`),process.exit(1)}}module.exports={parseArgs:vo,buildOptions:To,deriveIdentity:wr,resolveProviderState:xr,withRetry:st,UserError:U,AuthError:mr,main:Po,cmdIndexBulk:Ht,StubProvider:ml,OpenAIProvider:gl,store:v,chunker:Eo,config:Y,setup:ko,knowledgeDir:ee,storePath:H,metadataPath:q,lockFilePath:ie,INDEXED_PHASES:gr,KEYWORD_ONLY_DIMENSIONS:yr};require.main===module&&Po().catch(t=>{t instanceof U?process.stderr.write("Error: "+t.message+` +`):process.stderr.write(String(t&&t.stack?t.stack:t)+` `),process.exit(1)}); diff --git a/skills/workflow-manifest/scripts/manifest.cjs b/skills/workflow-manifest/scripts/manifest.cjs index 57f6a16c5..8a9ee17fa 100644 --- a/skills/workflow-manifest/scripts/manifest.cjs +++ b/skills/workflow-manifest/scripts/manifest.cjs @@ -43,9 +43,14 @@ const LOCK_TIMEOUT_MS = 10000; // Helpers // --------------------------------------------------------------------------- -function die(msg) { +// Exit-code convention: +// 1 — unexpected error (corrupt JSON, bad args, I/O, validation failure) +// 2 — expected miss (work unit / path / value not found) — callers that +// do best-effort lookups can distinguish this from real errors +// without pattern-matching the stderr text. +function die(msg, code = 1) { process.stderr.write(`Error: ${msg}\n`); - process.exit(1); + process.exit(code); } function manifestDir(name) { @@ -62,7 +67,7 @@ function lockPath(name) { function readManifest(name) { const p = manifestPath(name); - if (!fs.existsSync(p)) die(`Work unit "${name}" not found`); + if (!fs.existsSync(p)) die(`Work unit "${name}" not found`, 2); return JSON.parse(fs.readFileSync(p, 'utf8')); } @@ -288,7 +293,7 @@ function resolveSegments(phase, topic, fieldSegments) { function requireWorkUnit(workUnit) { if (!fs.existsSync(manifestPath(workUnit))) { - die(`Work unit "${workUnit}" not found`); + die(`Work unit "${workUnit}" not found`, 2); } } @@ -512,7 +517,7 @@ function cmdGet(args) { } const value = getByPath(manifest, proj.fieldSegments); if (value === undefined) { - die(`Path "${proj.fieldSegments.join('.')}" not found in project manifest`); + die(`Path "${proj.fieldSegments.join('.')}" not found in project manifest`, 2); } outputValue(value); return; @@ -530,7 +535,7 @@ function cmdGet(args) { const segments = args[1].split('.'); const value = getByPath(manifest, segments); if (value === undefined) { - die(`Path "${args[1]}" not found in "${workUnit}"`); + die(`Path "${args[1]}" not found in "${workUnit}"`, 2); } outputValue(value); return; @@ -543,7 +548,7 @@ function cmdGet(args) { if (topic === '*') { const results = resolveWildcardTopic(manifest, phase, fieldSegments); if (results.length === 0) { - die(`No items found in phase "${phase}" of "${workUnit}"`); + die(`No items found in phase "${phase}" of "${workUnit}"`, 2); } process.stdout.write(JSON.stringify(results, null, 2) + '\n'); return; @@ -552,7 +557,7 @@ function cmdGet(args) { const segments = resolvePhaseSegments(phase, topic, fieldSegments); const value = getByPath(manifest, segments); if (value === undefined) { - die(`Path "${segments.join('.')}" not found in "${workUnit}"`); + die(`Path "${segments.join('.')}" not found in "${workUnit}"`, 2); } outputValue(value); } @@ -604,7 +609,7 @@ function cmdDelete(args) { withProjectLock(() => { const manifest = readProjectManifest(); if (!deleteByPath(manifest, proj.fieldSegments)) { - die(`Path "${proj.fieldSegments.join('.')}" not found in project manifest`); + die(`Path "${proj.fieldSegments.join('.')}" not found in project manifest`, 2); } writeProjectManifestAtomic(manifest); }); @@ -623,7 +628,7 @@ function cmdDelete(args) { withLock(workUnit, () => { const manifest = readManifest(workUnit); if (!deleteByPath(manifest, segments)) { - die(`Path "${segments.join('.')}" not found in "${workUnit}"`); + die(`Path "${segments.join('.')}" not found in "${workUnit}"`, 2); } writeManifestAtomic(workUnit, manifest); }); @@ -892,10 +897,10 @@ function cmdProject(args) { const name = args[1]; if (!name) die('Usage: project get '); const projPath = path.join(WORKFLOWS_DIR, 'manifest.json'); - if (!fs.existsSync(projPath)) die(`Project manifest not found`); + if (!fs.existsSync(projPath)) die(`Project manifest not found`, 2); const proj = JSON.parse(fs.readFileSync(projPath, 'utf8')); const entry = (proj.work_units || {})[name]; - if (!entry) die(`Work unit "${name}" not found in project manifest`); + if (!entry) die(`Work unit "${name}" not found in project manifest`, 2); process.stdout.write(`work_type: ${entry.work_type}\n`); return; } @@ -921,7 +926,7 @@ function cmdKeyOf(args) { const key = Object.keys(obj).find(k => String(obj[k]) === searchValue); if (key === undefined) { - die(`Value "${searchValue}" not found in "${segments.join('.')}"`); + die(`Value "${searchValue}" not found in "${segments.join('.')}"`, 2); } process.stdout.write(key + '\n'); diff --git a/skills/workflow-migrate/scripts/migrations/037-completed-at-gap.sh b/skills/workflow-migrate/scripts/migrations/037-completed-at-gap.sh index 8a5e114e8..73652ee8c 100644 --- a/skills/workflow-migrate/scripts/migrations/037-completed-at-gap.sh +++ b/skills/workflow-migrate/scripts/migrations/037-completed-at-gap.sh @@ -61,6 +61,7 @@ function toISODate(ms) { return yyyy + '-' + mm + '-' + dd; } +let modified = 0; for (const entry of entries) { if (!entry.isDirectory() || entry.name.startsWith('.')) continue; @@ -84,11 +85,19 @@ for (const entry of entries) { m.completed_at = toISODate(latest); fs.writeFileSync(mPath, JSON.stringify(m, null, 2) + '\n'); + modified++; } -" "$WORKFLOWS_DIR" 2>/dev/null +// Exit 2 = 'no changes' so the bash wrapper reports skip rather than +// inflating the orchestrator's FILES_UPDATED counter when nothing ran. +// Anything else non-zero = unexpected crash — stderr passes through so +// the orchestrator surfaces it. +process.exit(modified > 0 ? 0 : 2); +" "$WORKFLOWS_DIR" && rc=0 || rc=$? -if [ $? -eq 0 ]; then +if [ "$rc" -eq 0 ]; then report_update else + # exit 2 (no changes) or any other non-zero (real crash, stderr was + # already printed above). In both cases don't inflate the counter. report_skip fi diff --git a/skills/workflow-planning-process/SKILL.md b/skills/workflow-planning-process/SKILL.md index 9fa6f68fa..235688174 100644 --- a/skills/workflow-planning-process/SKILL.md +++ b/skills/workflow-planning-process/SKILL.md @@ -215,8 +215,9 @@ Load **[planning-principles.md](references/planning-principles.md)** and follow > *Output the next fenced block as markdown (not a code block):* ``` -> Loading the usage guide for the knowledge base so -> proactive querying is available while planning tasks. +> Loading the usage guide for the knowledge base. Planning operates +> from the spec as the golden source — the guide documents the narrow +> cases where a KB query is warranted, and those where it is not. ``` Load **[knowledge-usage.md](../workflow-knowledge/references/knowledge-usage.md)** and follow its instructions as written. diff --git a/skills/workflow-review-process/SKILL.md b/skills/workflow-review-process/SKILL.md index 44a2ec9b7..b28eeb871 100644 --- a/skills/workflow-review-process/SKILL.md +++ b/skills/workflow-review-process/SKILL.md @@ -252,8 +252,10 @@ Load **[load-project-skills.md](references/load-project-skills.md)** and follow > *Output the next fenced block as markdown (not a code block):* ``` -> Loading the usage guide for the knowledge base so -> proactive querying is available while verifying decisions. +> Loading the usage guide for the knowledge base. Review verifies +> against the current spec — that's in scope without the KB. The guide +> documents the narrow case where a cross-work-unit consistency check +> is warranted. ``` Load **[knowledge-usage.md](../workflow-knowledge/references/knowledge-usage.md)** and follow its instructions as written. diff --git a/skills/workflow-scoping-process/SKILL.md b/skills/workflow-scoping-process/SKILL.md index 246b484fc..1c298ca40 100644 --- a/skills/workflow-scoping-process/SKILL.md +++ b/skills/workflow-scoping-process/SKILL.md @@ -89,11 +89,11 @@ node .claude/skills/workflow-manifest/scripts/manifest.cjs init-phase {work_unit node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit}.scoping.{topic} status completed ``` -→ Proceed to **Step 7**. +→ Proceed to **Step 8**. **Otherwise:** -→ Proceed to **Step 1** (spec exists but plan is incomplete — resume from format selection). +→ Proceed to **Step 6** (spec exists but plan is incomplete — resume from format selection). #### If specification does not exist @@ -145,7 +145,28 @@ Load **[gather-context.md](references/gather-context.md)** and follow its instru --- -## Step 3: Complexity Check +## Step 3: Contextual Query + +> *Output the next fenced block as a code block:* + +``` +── Contextual Query ───────────────────────────── +``` + +> *Output the next fenced block as markdown (not a code block):* + +``` +> Checking the knowledge base for prior discussions, investigations, +> or specs that touch the area being changed. +``` + +Load **[contextual-query.md](../workflow-knowledge/references/contextual-query.md)** and follow its instructions as written. + +→ Proceed to **Step 4**. + +--- + +## Step 4: Complexity Check > *Output the next fenced block as a code block:* @@ -162,11 +183,11 @@ Load **[gather-context.md](references/gather-context.md)** and follow its instru Load **[complexity-check.md](references/complexity-check.md)** and follow its instructions as written. -→ Proceed to **Step 4**. +→ Proceed to **Step 5**. --- -## Step 4: Write Specification +## Step 5: Write Specification > *Output the next fenced block as a code block:* @@ -183,11 +204,11 @@ Load **[complexity-check.md](references/complexity-check.md)** and follow its in Load **[write-specification.md](references/write-specification.md)** and follow its instructions as written. -→ Proceed to **Step 5**. +→ Proceed to **Step 6**. --- -## Step 5: Select Output Format +## Step 6: Select Output Format > *Output the next fenced block as a code block:* @@ -203,11 +224,11 @@ Load **[write-specification.md](references/write-specification.md)** and follow Load **[select-format.md](references/select-format.md)** and follow its instructions as written. -→ Proceed to **Step 6**. +→ Proceed to **Step 7**. --- -## Step 6: Write Tasks +## Step 7: Write Tasks > *Output the next fenced block as a code block:* @@ -224,11 +245,11 @@ Load **[select-format.md](references/select-format.md)** and follow its instruct Load **[write-tasks.md](references/write-tasks.md)** and follow its instructions as written. -→ Proceed to **Step 7**. +→ Proceed to **Step 8**. --- -## Step 7: Conclude Scoping +## Step 8: Conclude Scoping > *Output the next fenced block as a code block:* diff --git a/skills/workflow-scoping-process/references/complexity-check.md b/skills/workflow-scoping-process/references/complexity-check.md index 6c7e7cf6e..7abefa5d6 100644 --- a/skills/workflow-scoping-process/references/complexity-check.md +++ b/skills/workflow-scoping-process/references/complexity-check.md @@ -61,7 +61,9 @@ node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit} work_ Commit: `workflow({work_unit}): promote quick-fix to feature` -Invoke `/workflow-discussion-entry feature {work_unit}`. This is terminal — do not return to the caller. +Invoke `/workflow-discussion-entry feature {work_unit}`. + +**STOP.** Do not proceed — terminal condition. #### If `bugfix` @@ -73,4 +75,6 @@ node .claude/skills/workflow-manifest/scripts/manifest.cjs set {work_unit} work_ Commit: `workflow({work_unit}): promote quick-fix to bugfix` -Invoke `/workflow-investigation-entry bugfix {work_unit}`. This is terminal — do not return to the caller. +Invoke `/workflow-investigation-entry bugfix {work_unit}`. + +**STOP.** Do not proceed — terminal condition. diff --git a/skills/workflow-specification-process/references/promote-to-cross-cutting.md b/skills/workflow-specification-process/references/promote-to-cross-cutting.md index e640d2245..e7bde2a0e 100644 --- a/skills/workflow-specification-process/references/promote-to-cross-cutting.md +++ b/skills/workflow-specification-process/references/promote-to-cross-cutting.md @@ -81,7 +81,7 @@ If either command fails, display the error but do not block — the move is alre ``` ⚑ Knowledge warning {error details} - The discussion is moved. You can run knowledge index/remove manually later. + The discussion is moved. Removals are queued automatically (retry on next `knowledge remove` / `knowledge compact`). Index the moved discussion manually if the automatic background index did not run. ``` → Proceed to **D. Move Specification**. @@ -143,7 +143,7 @@ If the remove command fails, display the error but do not block — the promotio ``` ⚑ Knowledge removal warning {error details} - The spec is promoted. You can run knowledge remove manually later. + The spec is promoted. The removal has been queued and will retry automatically on the next `knowledge remove` or `knowledge compact` call. ``` → Proceed to **F. Commit and Display**. diff --git a/skills/workflow-specification-process/references/spec-completion.md b/skills/workflow-specification-process/references/spec-completion.md index 1de5d59b3..9b05eab91 100644 --- a/skills/workflow-specification-process/references/spec-completion.md +++ b/skills/workflow-specification-process/references/spec-completion.md @@ -174,7 +174,7 @@ If the remove command fails, display the error but do not block — the superses ``` ⚑ Knowledge removal warning {error details} - The spec is superseded. You can run knowledge remove manually later. + The spec is superseded. The removal has been queued and will retry automatically on the next `knowledge remove` or `knowledge compact` call. ``` 3. Inform the user which topics were updated diff --git a/skills/workflow-start/SKILL.md b/skills/workflow-start/SKILL.md index 67db7e081..d5947377f 100644 --- a/skills/workflow-start/SKILL.md +++ b/skills/workflow-start/SKILL.md @@ -60,7 +60,6 @@ Load **[casing-conventions.md](../workflow-shared/references/casing-conventions. ``` > Running migrations to keep workflow files in sync. -> This ensures everything is up to date before we proceed. ``` **Run migrations — this is mandatory. You must complete it before proceeding.** diff --git a/skills/workflow-start/references/absorb-into-epic.md b/skills/workflow-start/references/absorb-into-epic.md index e6c069d1f..352daac4f 100644 --- a/skills/workflow-start/references/absorb-into-epic.md +++ b/skills/workflow-start/references/absorb-into-epic.md @@ -357,7 +357,9 @@ Absorbed into Epic #### If user chose `c`/`continue` -Invoke the `/continue-epic` skill. This is terminal — do not return to the caller. +Invoke the `/continue-epic` skill. + +**STOP.** Do not proceed — terminal condition. #### If user chose `b`/`back` diff --git a/skills/workflow-start/references/manage-work-unit.md b/skills/workflow-start/references/manage-work-unit.md index 5abc1dd16..a4329f66e 100644 --- a/skills/workflow-start/references/manage-work-unit.md +++ b/skills/workflow-start/references/manage-work-unit.md @@ -222,7 +222,9 @@ Load **[reindex-work-unit.md](../../workflow-knowledge/references/reindex-work-u **If user chose `c`/`continue`:** -Invoke the `/continue-epic` skill. This is terminal — do not return to the caller. +Invoke the `/continue-epic` skill. + +**STOP.** Do not proceed — terminal condition. **If user chose `b`/`back`:** @@ -259,7 +261,7 @@ If the remove command fails, display the error but do not block — the cancella ``` ⚑ Knowledge removal warning {error details} - The work unit is cancelled. You can run knowledge remove manually later. + The work unit is cancelled. The removal has been queued and will retry automatically on the next `knowledge remove` or `knowledge compact` call. ``` Commit: `workflow({selected.name}): mark as cancelled` diff --git a/skills/workflow-start/references/view-completed.md b/skills/workflow-start/references/view-completed.md index b9acc9b09..71727fa32 100644 --- a/skills/workflow-start/references/view-completed.md +++ b/skills/workflow-start/references/view-completed.md @@ -100,6 +100,12 @@ Set status back to in-progress: node .claude/skills/workflow-manifest/scripts/manifest.cjs set {selected.name} status in-progress ``` +Capture whether `completed_at` is set (used by the completed branches; harmless on the cancelled path): + +```bash +node .claude/skills/workflow-manifest/scripts/manifest.cjs exists {selected.name} completed_at +``` + **If `selected.status` was `cancelled`:** Cancellation removed the work unit's chunks from the knowledge base. Restore them by loading **[reindex-work-unit.md](../../workflow-knowledge/references/reindex-work-unit.md)** with work_unit = `{selected.name}`. @@ -112,7 +118,23 @@ Cancellation removed the work unit's chunks from the knowledge base. Restore the → Return to caller. -**If `selected.status` was `completed`:** +**If `selected.status` was `completed` and `completed_at` is set:** + +Completed work units retain their chunks — no re-indexing needed. Clear the stale `completed_at`: + +```bash +node .claude/skills/workflow-manifest/scripts/manifest.cjs delete {selected.name} completed_at +``` + +> *Output the next fenced block as a code block:* + +``` +"{selected.name:(titlecase)}" reactivated. +``` + +→ Return to caller. + +**If `selected.status` was `completed` and `completed_at` is not set:** Completed work units retain their chunks — no re-indexing needed. diff --git a/src/knowledge/config.js b/src/knowledge/config.js index 41670462e..8670202be 100644 --- a/src/knowledge/config.js +++ b/src/knowledge/config.js @@ -40,12 +40,32 @@ function systemConfigPath() { } /** - * Resolve the project config path relative to CWD. + * Find the project root by walking up from `startFrom` (default cwd) + * looking for a `.workflows/` directory. This lets KB commands work + * regardless of which subdirectory of the project the user invoked + * them from. Falls back to `startFrom` if no `.workflows/` is found + * — callers (e.g. setup) can then surface their own pre-init error. + * @param {string} [startFrom] + * @returns {string} + */ +function findProjectRoot(startFrom) { + let dir = path.resolve(startFrom || process.cwd()); + const fallback = dir; + while (true) { + if (fs.existsSync(path.join(dir, '.workflows'))) return dir; + const parent = path.dirname(dir); + if (parent === dir) return fallback; + dir = parent; + } +} + +/** + * Resolve the project config path relative to the project root. * @param {string} [cwd] * @returns {string} */ function projectConfigPath(cwd) { - return path.join(cwd || process.cwd(), '.workflows', '.knowledge', 'config.json'); + return path.join(findProjectRoot(cwd), '.workflows', '.knowledge', 'config.json'); } /** @@ -242,16 +262,22 @@ function loadConfig(paths) { const project = readConfigFile(projPath); // Merge: defaults <- system <- project. Shallow merge — all fields are - // scalars, no nested objects to worry about. + // scalars, no nested objects to worry about. `null` is treated as an + // explicit unset sentinel so a project config can clear a system default + // (e.g. "disable the system-configured provider for this project only"). const merged = Object.assign({}, DEFAULTS); if (system) { for (const key of Object.keys(system)) { - if (system[key] !== undefined) merged[key] = system[key]; + if (system[key] === undefined) continue; + if (system[key] === null) delete merged[key]; + else merged[key] = system[key]; } } if (project) { for (const key of Object.keys(project)) { - if (project[key] !== undefined) merged[key] = project[key]; + if (project[key] === undefined) continue; + if (project[key] === null) delete merged[key]; + else merged[key] = project[key]; } } @@ -349,6 +375,7 @@ module.exports = { PROVIDER_ENV_VARS, systemConfigPath, projectConfigPath, + findProjectRoot, credentialsPath, readConfigFile, loadConfig, diff --git a/src/knowledge/index.js b/src/knowledge/index.js index 5db99ddbe..b1269b757 100644 --- a/src/knowledge/index.js +++ b/src/knowledge/index.js @@ -11,7 +11,7 @@ const path = require('path'); const store = require('./store'); const chunker = require('./chunker'); const { StubProvider } = require('./embeddings'); -const { OpenAIProvider } = require('./providers/openai'); +const { OpenAIProvider, AuthError } = require('./providers/openai'); const config = require('./config'); const setup = require('./setup'); @@ -24,32 +24,84 @@ const INDEXED_PHASES = ['research', 'discussion', 'investigation', 'specificatio // Resolve manifest CLI path. In the bundled form, __dirname is // skills/workflow-knowledge/scripts/. In source, __dirname is // src/knowledge/. Both need to resolve to skills/workflow-manifest/scripts/manifest.cjs. -const MANIFEST_JS = fs.existsSync(path.join(__dirname, '..', '..', 'skills', 'workflow-manifest', 'scripts', 'manifest.cjs')) - ? path.join(__dirname, '..', '..', 'skills', 'workflow-manifest', 'scripts', 'manifest.cjs') - : path.join(__dirname, '..', '..', 'workflow-manifest', 'scripts', 'manifest.cjs'); +const MANIFEST_JS = (() => { + const srcCandidate = path.join(__dirname, '..', '..', 'skills', 'workflow-manifest', 'scripts', 'manifest.cjs'); + const bundledCandidate = path.join(__dirname, '..', '..', 'workflow-manifest', 'scripts', 'manifest.cjs'); + if (fs.existsSync(srcCandidate)) return srcCandidate; + if (fs.existsSync(bundledCandidate)) return bundledCandidate; + // Fail loud at load time — a missing manifest CLI would otherwise turn + // every manifest-dependent command into a silent no-op (deferred-issue #5). + throw new Error( + 'Could not locate manifest.cjs. Tried:\n' + + ` ${srcCandidate}\n` + + ` ${bundledCandidate}\n` + + 'This is an installation problem — the knowledge CLI cannot work without the manifest CLI.' + ); +})(); const DEFAULT_RETRY_BACKOFF = [1000, 2000, 4000]; const PENDING_CATCHUP_LIMIT = 5; +const PENDING_MAX_ATTEMPTS = 10; // Default dimensions when creating a store in keyword-only mode. // The store schema requires a dimension parameter, but keyword-only docs // omit the embedding field entirely — this value just satisfies the schema. const KEYWORD_ONLY_DIMENSIONS = 1536; +// Emit the stub-to-full upgrade note at most once per process to avoid +// spamming bulk-index runs that iterate over many files. +let stubUpgradeWarned = false; + +// --------------------------------------------------------------------------- +// UserError — marker class for user-visible validation failures. Thrown at +// input-validation sites (bad path, provider mismatch, missing chunking +// config, etc.) where the error message is actionable advice for the user. +// +// Two behavioural contracts: +// 1. withRetry does not retry UserError (same treatment as programming +// errors — retry would waste backoff budget on a permanent failure). +// 2. The top-level main().catch prints the message alone, no stack trace. +// --------------------------------------------------------------------------- + +class UserError extends Error { + constructor(message) { + super(message); + this.name = 'UserError'; + } +} + // --------------------------------------------------------------------------- // Flag parsing // --------------------------------------------------------------------------- /** - * Parse argv-style args into { positional: string[], flags: object }. - * Handles --flag value and --flag=value forms. + * Parse argv-style args into { positional, flags, boosts }. + * Handles --flag value and --flag=value forms for regular flags. + * + * `--boost: ` is special — repeatable, collected into an + * ordered list. The field name is embedded in the flag name (not the value) + * so skill templates never have to parse or escape a key/value separator. */ function parseArgs(argv) { const positional = []; const flags = {}; + const boosts = []; let i = 0; while (i < argv.length) { const arg = argv[i]; + if (arg.startsWith('--boost:')) { + const field = arg.slice('--boost:'.length); + if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) { + boosts.push({ field, value: argv[i + 1] }); + i += 2; + } else { + // Missing value — leave null so the command handler can error out + // with a clear message at validation time. + boosts.push({ field, value: null }); + i++; + } + continue; + } if (arg.startsWith('--')) { const eqIdx = arg.indexOf('='); if (eqIdx !== -1) { @@ -69,13 +121,16 @@ function parseArgs(argv) { } i++; } - return { positional, flags }; + return { positional, flags, boosts }; } /** * Build an options object from parsed flags for command handlers. + * `--work-unit` is a hard filter on every command that accepts it + * (consistent with --phase, --topic, --work-type). Re-ranking happens + * exclusively through --boost:. */ -function buildOptions(flags) { +function buildOptions(flags, boosts) { return { workType: flags['work-type'] || null, phase: flags['phase'] || null, @@ -83,6 +138,7 @@ function buildOptions(flags) { topic: flags['topic'] || null, limit: flags['limit'] ? parseInt(flags['limit'], 10) : null, dryRun: flags['dry-run'] === true || flags['dry-run'] === 'true', + boosts: boosts || [], }; } @@ -102,20 +158,28 @@ Commands: rebuild Rebuild the knowledge base from scratch setup Interactive setup wizard -Options: - --work-type Filter by work type - --work-unit Re-rank boost for this work unit (not a filter) - --phase Filter by phase - --topic Filter by topic - --limit Limit number of results - --dry-run Preview without making changes`; +Filter options (hard filters — non-matching chunks excluded): + --work-type Filter by work type + --work-unit Filter by work unit + --phase Filter by phase + --topic Filter by topic + +Re-ranking (query only, additive; repeat for multiple boosts): + --boost: Boost chunks matching : by +0.1 + Valid fields: work-unit, work-type, phase, + topic, confidence + +Other options: + --limit Limit number of results + --dry-run Preview without making changes + --help, -h Show this usage and exit 0`; // --------------------------------------------------------------------------- // Path helpers // --------------------------------------------------------------------------- function knowledgeDir() { - return path.resolve(process.cwd(), '.workflows', '.knowledge'); + return path.resolve(config.findProjectRoot(), '.workflows', '.knowledge'); } function storePath() { @@ -152,6 +216,20 @@ async function withRetry(fn, opts) { try { return await fn(); } catch (err) { + // Don't retry programming errors — retrying a TypeError just burns + // 7s of backoff before the stack trace reaches the user. + // Also don't retry UserError or AuthError: validation failures and + // bad/expired API keys will fail identically on every retry. + if ( + err instanceof UserError || + err instanceof AuthError || + err instanceof TypeError || + err instanceof ReferenceError || + err instanceof SyntaxError || + err instanceof RangeError + ) { + throw err; + } lastErr = err; if (attempt < maxAttempts - 1) { const delay = backoff[attempt] || backoff[backoff.length - 1]; @@ -177,7 +255,7 @@ function deriveIdentity(filePath) { // Match .workflows/{work_unit}/{phase}/{rest} const match = /\.workflows\/([^/]+)\/(research|discussion|investigation|specification)\/(.+)$/.exec(norm); if (!match) { - throw new Error( + throw new UserError( `Cannot derive identity from path: ${filePath}\n` + 'Expected path matching: .workflows/{work_unit}/{phase}/...' ); @@ -191,12 +269,12 @@ function deriveIdentity(filePath) { // anything-without-slash, which would otherwise accept `..` or `.` // and escape the .workflows directory when path.resolve() is applied. if (workUnit === '.' || workUnit === '..' || workUnit.startsWith('.')) { - throw new Error(`Invalid work unit name: "${workUnit}"`); + throw new UserError(`Invalid work unit name: "${workUnit}"`); } // Validate indexed phase. if (!INDEXED_PHASES.includes(phase)) { - throw new Error(`File is in phase "${phase}" which is not indexed.`); + throw new UserError(`File is in phase "${phase}" which is not indexed.`); } let topic; @@ -204,7 +282,7 @@ function deriveIdentity(filePath) { // .workflows/{wu}/specification/{topic}/specification.md const specMatch = /^([^/]+)\/specification\.md$/.exec(rest); if (!specMatch) { - throw new Error( + throw new UserError( `Unexpected specification path structure: ${rest}\n` + 'Expected: .workflows/{work_unit}/specification/{topic}/specification.md' ); @@ -214,7 +292,7 @@ function deriveIdentity(filePath) { // .workflows/{wu}/{phase}/{topic}.md — flat file, no subdirectories. const flatMatch = /^([^/]+)\.md$/.exec(rest); if (!flatMatch) { - throw new Error( + throw new UserError( `Unexpected ${phase} path structure: ${rest}\n` + `Expected: .workflows/{work_unit}/${phase}/{topic}.md` ); @@ -224,7 +302,7 @@ function deriveIdentity(filePath) { // .workflows/{wu}/research/{filename}.md — flat file. const resMatch = /^([^/]+)\.md$/.exec(rest); if (!resMatch) { - throw new Error( + throw new UserError( `Unexpected research path structure: ${rest}\n` + 'Expected: .workflows/{work_unit}/research/{filename}.md' ); @@ -233,7 +311,7 @@ function deriveIdentity(filePath) { } if (topic === '.' || topic === '..' || topic.startsWith('.')) { - throw new Error(`Invalid topic name: "${topic}"`); + throw new UserError(`Invalid topic name: "${topic}"`); } return { workUnit, phase, topic }; @@ -243,13 +321,13 @@ function deriveIdentity(filePath) { * Read the work_type from the work unit's manifest.json. */ function readWorkType(workUnit) { - const manifestFile = path.resolve(process.cwd(), '.workflows', workUnit, 'manifest.json'); + const manifestFile = path.resolve(config.findProjectRoot(), '.workflows', workUnit, 'manifest.json'); if (!fs.existsSync(manifestFile)) { - throw new Error(`Work unit manifest not found: ${manifestFile}`); + throw new UserError(`Work unit manifest not found: ${manifestFile}`); } const manifest = JSON.parse(fs.readFileSync(manifestFile, 'utf8')); if (!manifest.work_type) { - throw new Error(`Work unit manifest missing work_type field: ${manifestFile}`); + throw new UserError(`Work unit manifest missing work_type field: ${manifestFile}`); } return manifest.work_type; } @@ -270,7 +348,16 @@ function resolveProviderState(metadata, cfg, provider) { // Case 4: metadata.provider is null (keyword-only store). // Always allowed — index WITHOUT vectors regardless of current config. + // If the user has since configured a provider, warn once so they know + // they're still in keyword-only mode and must `rebuild` to upgrade. if (metaProvider === null || metaProvider === undefined) { + if (provider && !stubUpgradeWarned) { + stubUpgradeWarned = true; + process.stderr.write( + 'Note: store is keyword-only but an embedding provider is now configured. ' + + 'Run `knowledge rebuild` to switch to full hybrid search.\n' + ); + } return { mode: 'keyword-only', provider: null }; } @@ -286,7 +373,7 @@ function resolveProviderState(metadata, cfg, provider) { } // Case 2: mismatch. - throw new Error( + throw new UserError( 'Provider/model changed since last index. Run `knowledge rebuild` to reindex.\n' + ` Store: provider=${metaProvider}, model=${metaModel}, dimensions=${metaDimensions}\n` + ` Config: provider=${cfg.provider}, model=${curModel}, dimensions=${curDimensions}` @@ -294,7 +381,7 @@ function resolveProviderState(metadata, cfg, provider) { } // Case 3: metadata has provider but current config does not. - throw new Error( + throw new UserError( 'Provider/model changed since last index. Run `knowledge rebuild` to reindex.\n' + ` Store was indexed with: provider=${metaProvider}, model=${metaModel}\n` + ' Current config has no provider configured.' @@ -346,7 +433,7 @@ async function indexSingleFile(sourceFile, identity, cfg, provider) { // Load chunking config. const chunkConfigPath = path.join(__dirname, '..', 'chunking', identity.phase + '.json'); if (!fs.existsSync(chunkConfigPath)) { - throw new Error(`Chunking config not found: ${chunkConfigPath}`); + throw new UserError(`Chunking config not found: ${chunkConfigPath}`); } const chunkConfig = JSON.parse(fs.readFileSync(chunkConfigPath, 'utf8')); @@ -356,7 +443,7 @@ async function indexSingleFile(sourceFile, identity, cfg, provider) { const chunks = chunker.chunk(content, chunkConfig); if (chunks.length === 0) { - throw new Error( + throw new UserError( `No chunks produced from ${sourceFile}. Refusing to index an empty file — ` + 'this would silently wipe any existing indexed chunks for this topic. ' + 'Use `knowledge remove` explicitly if that is what you want.' @@ -454,6 +541,23 @@ async function indexSingleFile(sourceFile, identity, cfg, provider) { db = await store.loadStore(sp); } + // Re-validate provider state inside the lock. A concurrent rebuild or + // another indexer could have rewritten the store with different + // dimensions between our embedBatch call (outside the lock) and now + // (deferred-issue #1 TOCTOU). If dimensions diverged, our embeddings + // are the wrong width — abort, and withRetry at the CLI layer will + // re-enter with fresh state. + if (effectiveMode === 'full' && fs.existsSync(mp)) { + const reloadedMeta = store.readMetadata(mp); + const expectedDims = effectiveProvider.dimensions(); + if (reloadedMeta.provider && reloadedMeta.dimensions !== expectedDims) { + throw new Error( + 'Store schema changed during index (concurrent rebuild). ' + + `Embeddings produced for dims=${expectedDims}, store now has dims=${reloadedMeta.dimensions}. Retrying.` + ); + } + } + await store.removeByIdentity(db, { work_unit: identity.workUnit, phase: identity.phase, @@ -502,13 +606,31 @@ async function indexSingleFile(sourceFile, identity, cfg, provider) { */ function runManifest(args) { const { execFileSync } = require('child_process'); + // Spawn with cwd anchored at the project root so the manifest CLI's + // own cwd-relative resolution lands at the right place even when KB + // commands are invoked from a subdirectory. return execFileSync('node', [MANIFEST_JS, ...args], { - cwd: process.cwd(), + cwd: config.findProjectRoot(), encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); } +/** + * Distinguish expected "not found" misses from broken-manifest / corrupt-JSON + * / bad-path errors. Knowledge-base helpers swallow the former (lookups are + * best-effort); the latter must be surfaced or bulk operations become silent + * no-ops (deferred-issue #4). + * + * manifest.cjs uses exit code 2 for expected misses, 1 for real errors — + * stable and unambiguous. execFileSync surfaces the code on err.status. + */ +function reportUnexpectedManifestError(context, err) { + if (err && err.status === 2) return; + const msg = err && err.stderr ? String(err.stderr).trim() : err.message; + process.stderr.write(`Warning: manifest CLI failed in ${context}: ${msg}\n`); +} + /** * Check if chunks exist for the given identity triple. */ @@ -536,7 +658,8 @@ function discoverArtifacts() { try { const raw = runManifest(['list']); workUnits = JSON.parse(raw); - } catch (_) { + } catch (err) { + reportUnexpectedManifestError('discoverArtifacts:list', err); return items; } @@ -561,8 +684,11 @@ function discoverArtifacts() { if (filePath && fs.existsSync(path.resolve(filePath))) { items.push({ file: filePath, workUnit: wuName, phase, topic: topicName }); } - } catch (_) { - // Skip unresolvable items. + } catch (err) { + reportUnexpectedManifestError( + `discoverArtifacts:resolve(${wuName}.${phase}.${topicName})`, + err + ); } } } @@ -576,12 +702,26 @@ async function cmdIndexBulk(options, cfg, provider) { const kDir = knowledgeDir(); const sp = storePath(); + const mp = metadataPath(); // Ensure knowledge directory exists. if (!fs.existsSync(kDir)) { fs.mkdirSync(kDir, { recursive: true }); } + // Preflight: if a prior store exists, validate the current config's + // provider/model/dimensions match what the store was built with. Without + // this check, a config change (e.g. 128-dim stub → 1536-dim OpenAI) that + // triggers bulk index would short-circuit at the isIndexed() step for + // every file and report "Indexed 0 files. N already indexed." as though + // everything were fine — while the stored embeddings are still at the old + // dimensions. resolveProviderState throws a UserError with the "Run + // `knowledge rebuild`" hint, same as `query` surfaces on mismatch. + if (fs.existsSync(mp)) { + const meta = store.readMetadata(mp); + resolveProviderState(meta, cfg, provider); + } + // Load existing store to check what's already indexed. let db = null; if (fs.existsSync(sp)) { @@ -617,11 +757,17 @@ async function cmdIndexBulk(options, cfg, provider) { db = await store.loadStore(sp); } } catch (err) { - // All retries exhausted — add to pending queue. + // All retries exhausted — add to pending queue. Write the stack to + // stderr so debugging does not depend on users capturing it later. + // Skip the stack for UserError and AuthError — both are user-config + // failures (validation / bad API key) where the message line alone + // is the actionable signal. await addToPendingQueue(item.file, err.message); process.stderr.write( `Failed to index ${item.file} after 3 attempts: ${err.message}. Added to pending queue.\n` ); + const isUserFacing = err instanceof UserError || err instanceof AuthError; + if (err.stack && !isUserFacing) process.stderr.write(err.stack + '\n'); } } @@ -664,11 +810,12 @@ async function addToPendingQueue(file, errorMsg) { if (!Array.isArray(metadata.pending)) metadata.pending = []; const existing = metadata.pending.findIndex((p) => p.file === file); - const entry = { file, failed_at: new Date().toISOString(), error: errorMsg }; + const base = { file, failed_at: new Date().toISOString(), error: errorMsg }; if (existing >= 0) { - metadata.pending[existing] = entry; + const prior = metadata.pending[existing]; + metadata.pending[existing] = { ...base, attempts: (prior.attempts || 1) + 1 }; } else { - metadata.pending.push(entry); + metadata.pending.push({ ...base, attempts: 1 }); } store.writeMetadata(mp, metadata); }); @@ -703,6 +850,15 @@ async function processPendingQueue(cfg, provider, limit) { const toProcess = metadata.pending.slice(0, limit); for (const item of toProcess) { + if ((item.attempts || 0) >= PENDING_MAX_ATTEMPTS) { + // Permanent failure — evict so the queue doesn't grow forever. + process.stderr.write( + `Pending item ${item.file} exceeded ${PENDING_MAX_ATTEMPTS} attempts — evicting. Last error: ${item.error}\n` + ); + await removePendingItem(item.file); + continue; + } + const absFile = path.resolve(item.file); if (!fs.existsSync(absFile)) { // File no longer exists — remove from queue. @@ -726,12 +882,141 @@ async function processPendingQueue(cfg, provider, limit) { { maxAttempts: 3, backoff: DEFAULT_RETRY_BACKOFF } ); await removePendingItem(item.file); - } catch (_) { - // Still failing — leave in queue. + } catch (err) { + // Still failing — bump attempts so eviction eventually fires. + await addToPendingQueue(item.file, err.message); } } } +// --------------------------------------------------------------------------- +// Pending removal queue — mirrors the pending-index queue but for failed +// `knowledge remove` calls (lock timeout, store I/O error). Without this, +// a cancelled/superseded/promoted work unit's stale chunks persist in the +// store until the user manually retries. +// --------------------------------------------------------------------------- + +const REMOVAL_MAX_ATTEMPTS = 10; + +function removalKey(r) { + return `${r.workUnit}|${r.phase || ''}|${r.topic || ''}`; +} + +async function addPendingRemoval(opts, errorMsg) { + const mp = metadataPath(); + const kDir = knowledgeDir(); + const lp = lockFilePath(); + if (!fs.existsSync(kDir)) fs.mkdirSync(kDir, { recursive: true }); + + await store.withLock(lp, async () => { + let metadata; + if (fs.existsSync(mp)) { + metadata = store.readMetadata(mp); + } else { + metadata = { + provider: null, + model: null, + dimensions: null, + last_indexed: null, + pending: [], + pending_removals: [], + }; + } + if (!Array.isArray(metadata.pending_removals)) metadata.pending_removals = []; + + const key = removalKey(opts); + const existing = metadata.pending_removals.findIndex((r) => removalKey(r) === key); + const base = { + workUnit: opts.workUnit, + phase: opts.phase || null, + topic: opts.topic || null, + queued_at: new Date().toISOString(), + error: errorMsg, + }; + if (existing >= 0) { + const prior = metadata.pending_removals[existing]; + metadata.pending_removals[existing] = { ...base, attempts: (prior.attempts || 0) + 1 }; + } else { + metadata.pending_removals.push({ ...base, attempts: 1 }); + } + store.writeMetadata(mp, metadata); + }); +} + +async function removePendingRemoval(opts) { + const mp = metadataPath(); + const lp = lockFilePath(); + if (!fs.existsSync(mp)) return; + + await store.withLock(lp, async () => { + if (!fs.existsSync(mp)) return; + const metadata = store.readMetadata(mp); + if (!Array.isArray(metadata.pending_removals)) return; + const key = removalKey(opts); + metadata.pending_removals = metadata.pending_removals.filter((r) => removalKey(r) !== key); + store.writeMetadata(mp, metadata); + }); +} + +// Lock semantics mirror processPendingQueue: never call while holding the +// store lock — each queued retry acquires the lock itself. +async function processPendingRemovals() { + const mp = metadataPath(); + if (!fs.existsSync(mp)) return; + + const metadata = store.readMetadata(mp); + if (!Array.isArray(metadata.pending_removals) || metadata.pending_removals.length === 0) return; + + const toProcess = metadata.pending_removals.slice(); + + for (const item of toProcess) { + if ((item.attempts || 0) >= REMOVAL_MAX_ATTEMPTS) { + process.stderr.write( + `Pending removal for ${removalKey(item)} exceeded ${REMOVAL_MAX_ATTEMPTS} attempts — evicting.\n` + ); + await removePendingRemoval(item); + continue; + } + + try { + await performRemoval({ workUnit: item.workUnit, phase: item.phase, topic: item.topic }); + process.stderr.write(`Drained pending removal for ${removalKey(item)}.\n`); + await removePendingRemoval(item); + } catch (err) { + // Still failing — bump attempts so we eventually evict. + try { + await addPendingRemoval( + { workUnit: item.workUnit, phase: item.phase, topic: item.topic }, + err.message + ); + } catch (_) { + // Metadata write failed — the next invocation will retry. + } + } + } +} + +// Perform the actual remove-by-filter operation under the store lock. +// Extracted from cmdRemove so the pending-removal queue can invoke it +// without re-running the CLI layer (argument parsing, exit codes). +async function performRemoval(opts) { + const sp = storePath(); + const lp = lockFilePath(); + + if (!fs.existsSync(sp)) return 0; + + let removed = 0; + await store.withLock(lp, async () => { + const db = await store.loadStore(sp); + const where = { work_unit: { eq: opts.workUnit } }; + if (opts.phase) where.phase = { eq: opts.phase }; + if (opts.topic) where.topic = { eq: opts.topic }; + removed = await store.removeByFilter(db, where); + await store.saveStore(db, sp); + }); + return removed; +} + // --------------------------------------------------------------------------- // Query command // --------------------------------------------------------------------------- @@ -772,15 +1057,31 @@ function formatDate(ts) { return `${yyyy}-${mm}-${dd}`; } +// CLI boost field → store schema field. Kebab-case on the CLI surface, +// snake_case in the schema. Keeps the CLI consistent with --work-unit / +// --work-type while matching the indexed field names internally. +const BOOST_FIELD_MAP = { + 'work-unit': 'work_unit', + 'work-type': 'work_type', + 'phase': 'phase', + 'topic': 'topic', + 'confidence': 'confidence', +}; +const BOOST_AMOUNT = 0.1; + /** * Application-level re-ranking per design doc lines 566-574. - * Adjusts Orama scores based on work-unit proximity, confidence tier, - * and recency. Returns the array sorted by adjusted score (descending). + * Adjusts Orama scores based on user-specified boosts (+0.1 per match, + * additive across boosts), plus always-on confidence tier and recency + * signals. Returns the array sorted by adjusted score (descending). + * + * @param {Array} results raw result rows + * @param {Array<{field: string, value: string}>} boosts normalised boost + * list — field is already mapped to the store schema name */ -function rerank(results, workUnitHint) { +function rerank(results, boosts) { if (results.length === 0) return results; - // Find the most recent timestamp for normalisation. const maxTs = Math.max(...results.map((r) => r.timestamp || 0)); const minTs = Math.min(...results.map((r) => r.timestamp || 0)); const tsRange = maxTs - minTs || 1; @@ -789,16 +1090,20 @@ function rerank(results, workUnitHint) { .map((r) => { let adjustedScore = r.score || 0; - // Work-unit proximity boost (0.1 when matching). - if (workUnitHint && r.work_unit === workUnitHint) { - adjustedScore += 0.1; + // User-specified boosts — +0.1 per match, additive. + if (Array.isArray(boosts)) { + for (const b of boosts) { + if (r[b.field] === b.value) { + adjustedScore += BOOST_AMOUNT; + } + } } - // Confidence tier boost (0 to 0.04). + // Always-on confidence tier boost (0 to 0.04). const confRank = CONFIDENCE_RANK[r.confidence] || 0; adjustedScore += confRank * 0.01; - // Recency boost (0 to 0.05). + // Always-on recency boost (0 to 0.05). const recency = ((r.timestamp || 0) - minTs) / tsRange; adjustedScore += recency * 0.05; @@ -807,6 +1112,29 @@ function rerank(results, workUnitHint) { .sort((a, b) => b.score - a.score); } +/** + * Validate user-supplied boost directives and map CLI field names to the + * store schema field names. Exits with a clear error on unknown field or + * missing value so skill-template typos don't silently no-op. + */ +function normaliseBoosts(boosts) { + const out = []; + for (const b of boosts) { + if (!b.field || !BOOST_FIELD_MAP[b.field]) { + process.stderr.write( + `Unknown --boost field: "${b.field}". Valid fields: ${Object.keys(BOOST_FIELD_MAP).join(', ')}\n` + ); + process.exit(1); + } + if (b.value == null || b.value === '') { + process.stderr.write(`--boost:${b.field} requires a value\n`); + process.exit(1); + } + out.push({ field: BOOST_FIELD_MAP[b.field], value: b.value }); + } + return out; +} + /** * Resolve provider state for query. Symmetric with index-time resolution * but returns mode information instead of throwing for the upgrade case. @@ -828,7 +1156,7 @@ function resolveQueryMode(metadata, cfg, provider) { // Store has vectors — check provider compatibility. if (!provider) { // Config has no provider but store has vectors — mismatch. - throw new Error( + throw new UserError( 'Provider/model changed since last index. Run `knowledge rebuild` to reindex.\n' + ` Store was indexed with: provider=${metaProvider}, model=${metaModel}\n` + ' Current config has no provider configured.' @@ -843,7 +1171,7 @@ function resolveQueryMode(metadata, cfg, provider) { } // Mismatch. - throw new Error( + throw new UserError( 'Provider/model changed since last index. Run `knowledge rebuild` to reindex.\n' + ` Store: provider=${metaProvider}, model=${metaModel}, dimensions=${metaDimensions}\n` + ` Config: provider=${cfg.provider}, model=${curModel}, dimensions=${curDimensions}` @@ -852,10 +1180,23 @@ function resolveQueryMode(metadata, cfg, provider) { async function cmdQuery(args, options, cfg, provider) { if (args.length === 0) { - process.stderr.write('Usage: knowledge query [...] [--phase ...] [--work-type ...] [--work-unit ...] [--limit N]\n'); + process.stderr.write('Usage: knowledge query [...] [--work-unit ...] [--work-type ...] [--phase ...] [--topic ...] [--boost: ]... [--limit N]\n'); process.exit(1); } + // Reject empty/whitespace-only terms. Orama treats an empty term as + // "match everything" and returns up to `limit` arbitrary chunks — almost + // certainly a caller mistake (fat-finger, variable template that wasn't + // substituted) rather than an intentional "give me anything" request. + for (const t of args) { + if (typeof t !== 'string' || t.trim() === '') { + throw new UserError( + 'Empty search term. `knowledge query` requires at least one non-empty positional term. ' + + 'If you intended to list everything indexed, use `knowledge status` instead.' + ); + } + } + const searchTerms = args; // batch: multiple positional args const limit = options.limit || 10; const sp = storePath(); @@ -888,9 +1229,8 @@ async function cmdQuery(args, options, cfg, provider) { stubNote = '[keyword-only mode but embedding provider configured — run knowledge rebuild for full hybrid search]'; } - // Build where clause from filters. --work-unit is NOT a filter — it's a - // re-rank proximity hint used after search, so other work units can still - // appear in results but rank lower. + // Build where clause from hard filters. Every --flag that names a + // dimension is a filter; re-ranking happens exclusively via --boost:. const where = {}; if (options.phase) { const phases = options.phase.split(',').map((s) => s.trim()); @@ -900,12 +1240,19 @@ async function cmdQuery(args, options, cfg, provider) { const types = options.workType.split(',').map((s) => s.trim()); where.work_type = types.length === 1 ? { eq: types[0] } : { in: types }; } + if (options.workUnit) { + const units = options.workUnit.split(',').map((s) => s.trim()); + where.work_unit = units.length === 1 ? { eq: units[0] } : { in: units }; + } if (options.topic) { const topics = options.topic.split(',').map((s) => s.trim()); where.topic = topics.length === 1 ? { eq: topics[0] } : { in: topics }; } - const similarity = cfg.similarity_threshold || 0.8; + // ?? (not ||) so an explicit `similarity_threshold: 0` — a legitimate + // "accept all vector matches, no filtering" setting — isn't silently + // rewritten to the default. + const similarity = cfg.similarity_threshold ?? 0.8; const whereClause = Object.keys(where).length > 0 ? where : undefined; // Run a search per term and merge. @@ -942,8 +1289,9 @@ async function cmdQuery(args, options, cfg, provider) { } } - // Re-rank merged results. - let results = rerank(Array.from(allResults.values()), options.workUnit); + // Re-rank merged results using --boost: directives. + const normalisedBoosts = normaliseBoosts(options.boosts); + let results = rerank(Array.from(allResults.values()), normalisedBoosts); if (results.length > limit) { results = results.slice(0, limit); @@ -986,6 +1334,18 @@ async function cmdCheck(/* args, options, cfg, provider */) { return; } + // Condition 2b: config.json parses and has the expected shape. + // Without this, a corrupted config would pass `check` and the user + // would only see the JSON parse error later on `index` or `query`, + // with no hint that the root cause is the config file itself. + try { + config.readConfigFile(configFile); + } catch (err) { + process.stderr.write(`config error: ${err.message}\n`); + process.stdout.write('not-ready\n'); + return; + } + // Condition 3: store.msp exists and is loadable. if (!fs.existsSync(sp)) { process.stdout.write('not-ready\n'); @@ -1024,7 +1384,7 @@ async function cmdStatus() { } const db = await store.loadStore(sp); - const allChunks = await store.searchFulltext(db, { term: '', limit: 100000 }); + const allChunks = await store.searchAllFulltext(db); // 1. Index summary. out.push(`Total chunks: ${allChunks.length}`); @@ -1086,7 +1446,17 @@ async function cmdStatus() { out.push(''); out.push(`Pending items: ${metadata.pending.length}`); for (const p of metadata.pending) { - out.push(` ${p.file} — ${p.error} (${p.failed_at})`); + const a = p.attempts || 1; + out.push(` ${p.file} — ${p.error} (attempt ${a}/${PENDING_MAX_ATTEMPTS}, ${p.failed_at})`); + } + } + + // 4b. Pending removals. + if (Array.isArray(metadata.pending_removals) && metadata.pending_removals.length > 0) { + out.push(''); + out.push(`Pending removals: ${metadata.pending_removals.length}`); + for (const r of metadata.pending_removals) { + out.push(` ${removalKey(r)} — ${r.error} (attempt ${r.attempts || 1}/${REMOVAL_MAX_ATTEMPTS})`); } } @@ -1115,12 +1485,16 @@ async function cmdStatus() { } // 7. Orphan detection — source files that no longer exist. + // Resolve relative to the project root (found by walking up from cwd) + // rather than cwd directly, so status invoked from a subdirectory + // does not mark every chunk as orphaned. + const projectRoot = config.findProjectRoot(); const orphans = []; const seenSources = new Set(); for (const c of allChunks) { if (seenSources.has(c.source_file)) continue; seenSources.add(c.source_file); - if (!fs.existsSync(path.resolve(c.source_file))) { + if (!fs.existsSync(path.resolve(projectRoot, c.source_file))) { orphans.push(c.source_file); } } @@ -1147,30 +1521,43 @@ async function cmdStatus() { out.push(` ${f}`); } } - } catch (_) { - // Discovery may fail if no manifest — skip. + } catch (err) { + // Discovery may fail if no manifest — surface so user can tell. + process.stderr.write(`Warning: unindexed-artifact discovery failed: ${err.message}\n`); } - // 9. Manifest-knowledge consistency. + // 9. Manifest-knowledge consistency. Load all manifests once via + // `manifest list` rather than shelling out per spec topic — status was + // O(specs) processes before, which meant ~5s on 50-spec repos. const consistency = []; + let allManifests = null; + try { + allManifests = JSON.parse(runManifest(['list'])); + } catch (err) { + reportUnexpectedManifestError('cmdStatus:list', err); + } + const manifestByName = new Map(); + if (Array.isArray(allManifests)) { + for (const m of allManifests) if (m && m.name) manifestByName.set(m.name, m); + } + for (const wu of Object.keys(byWu)) { - const meta = getWorkUnitMeta(wu); - if (!meta) continue; - if (meta.status === 'cancelled') { + const m = manifestByName.get(wu); + if (!m) continue; + if (m.status === 'cancelled') { consistency.push(`Cancelled work unit still indexed: ${wu}`); } } - // Check for superseded specs. + // Superseded specs: look up each topic in the cached manifest tree. const specChunks = allChunks.filter((c) => c.phase === 'specification'); const specTopics = new Set(specChunks.map((c) => `${c.work_unit}.specification.${c.topic}`)); for (const key of specTopics) { - try { - const status = runManifest(['get', key, 'status']).trim(); - if (status === 'superseded') { - consistency.push(`Superseded spec still indexed: ${key}`); - } - } catch (_) { - // Skip if manifest lookup fails. + const [wuName, , topicName] = key.split('.'); + const m = manifestByName.get(wuName); + if (!m || !m.phases || !m.phases.specification || !m.phases.specification.items) continue; + const topicData = m.phases.specification.items[topicName]; + if (topicData && topicData.status === 'superseded') { + consistency.push(`Superseded spec still indexed: ${key}`); } } if (consistency.length > 0) { @@ -1205,7 +1592,9 @@ async function cmdRebuild(_args, options, cfg, provider) { const input = await readStdinLine(); if (input !== 'rebuild') { - process.stderr.write('Aborted.\n'); + // Leading newline so the message doesn't run into whatever the user + // typed at the prompt line. + process.stderr.write('\nAborted.\n'); process.exit(1); } @@ -1222,14 +1611,24 @@ async function cmdRebuild(_args, options, cfg, provider) { process.exit(1); } - // Acquire lock before deleting files so a concurrent index/remove/ + const spBak = sp + '.bak'; + const mpBak = mp + '.bak'; + + // Acquire lock before mutating files so a concurrent index/remove/ // compact does not race past and resurrect partial state. Then write // an empty placeholder store+metadata inside the same lock so there // is no "uninitialised" window where another process could build a // fresh store racing with our bulk-index. + // + // Use .bak rename rather than delete so a bulk-index failure (network + // outage, provider down, Ctrl-C) can be rolled back — otherwise a + // transient failure leaves the user with no store and no metadata. await store.withLock(lp, async () => { - if (fs.existsSync(sp)) fs.unlinkSync(sp); - if (fs.existsSync(mp)) fs.unlinkSync(mp); + // Clean any leftover .bak from a prior aborted rebuild. + if (fs.existsSync(spBak)) fs.unlinkSync(spBak); + if (fs.existsSync(mpBak)) fs.unlinkSync(mpBak); + if (fs.existsSync(sp)) fs.renameSync(sp, spBak); + if (fs.existsSync(mp)) fs.renameSync(mp, mpBak); // Write a sentinel empty store + keyword-only metadata so cmdCheck // and concurrent invocations see a valid (empty) state. The bulk @@ -1249,8 +1648,40 @@ async function cmdRebuild(_args, options, cfg, provider) { }); process.stdout.write('Deleted existing index.\n'); - // Run bulk index (acquires the lock per-file internally). - await cmdIndexBulk(options, cfg, provider); + try { + // Run bulk index (acquires the lock per-file internally). + await cmdIndexBulk(options, cfg, provider); + } catch (err) { + // Roll back to the pre-rebuild state. Best-effort: if the rollback + // itself fails (disk full, permission change), we surface both errors + // so the user has enough to recover manually. + try { + await store.withLock(lp, async () => { + if (fs.existsSync(spBak)) { + if (fs.existsSync(sp)) fs.unlinkSync(sp); + fs.renameSync(spBak, sp); + } + if (fs.existsSync(mpBak)) { + if (fs.existsSync(mp)) fs.unlinkSync(mp); + fs.renameSync(mpBak, mp); + } + }); + process.stderr.write( + 'Rebuild failed; restored previous index from backup.\n' + ); + } catch (rollbackErr) { + process.stderr.write( + `Rebuild failed and rollback also failed. Previous index is at:\n` + + ` ${spBak}\n ${mpBak}\n` + + `Rename them back manually to recover. Rollback error: ${rollbackErr.message}\n` + ); + } + throw err; + } + + // Bulk index succeeded — discard the backup. + if (fs.existsSync(spBak)) fs.unlinkSync(spBak); + if (fs.existsSync(mpBak)) fs.unlinkSync(mpBak); } /** @@ -1276,6 +1707,9 @@ function readStdinLine() { if (/\r|\n/.test(buf)) { process.stdin.removeListener('data', onData); process.stdin.removeListener('end', onEnd); + // Pause to release the reference — otherwise an unused stdin keeps + // the event loop alive if the CLI is used as a library. + process.stdin.pause(); finish(); } }; @@ -1296,7 +1730,7 @@ function readStdinLine() { async function cmdRemove(_args, options) { if (!options.workUnit) { - process.stderr.write('Usage: knowledge remove --work-unit [--phase

] [--topic ]\n'); + process.stderr.write('Usage: knowledge remove --work-unit [--phase

] [--topic ] [--dry-run]\n'); process.exit(1); } @@ -1305,30 +1739,95 @@ async function cmdRemove(_args, options) { process.exit(1); } - const sp = storePath(); - const lp = lockFilePath(); - - if (!fs.existsSync(sp)) { - const desc = formatRemoveDesc(options); - process.stdout.write(`Removed 0 chunks for ${desc}\n`); - return; + // Validate the work unit exists in the project registry. Without this, + // `remove --work-unit ` silently succeeds with "Removed 0 chunks" + // — a fat-finger is indistinguishable from a real no-op. The registry + // is authoritative (migration 031 backfills it from the filesystem), + // so a miss here means the caller has the wrong name. + // + // This check lives in cmdRemove only, not performRemoval — the pending- + // removal queue's drain path may legitimately target a WU that has since + // been removed from the registry (e.g. after absorption), and the stored + // chunks can still be cleaned up even when the WU entry is gone. + // Registry presence determines whether this is a "named WU" remove (the + // common path) or an "orphan-chunk cleanup" remove (the escape hatch + // when chunks linger after absorption / manual registry mutation). On + // registry-not-found, fall through to a store probe — if the store has + // chunks for this WU we treat it as an orphan cleanup and proceed; if + // not, surface the typo error. + let isOrphanCleanup = false; + try { + runManifest(['project', 'get', options.workUnit]); + } catch (err) { + if (err && err.status === 2) { + const sp = storePath(); + let storeMatch = 0; + if (fs.existsSync(sp)) { + const db = await store.loadStore(sp); + const where = { work_unit: { eq: options.workUnit } }; + if (options.phase) where.phase = { eq: options.phase }; + if (options.topic) where.topic = { eq: options.topic }; + storeMatch = await store.countByFilter(db, where); + } + if (storeMatch === 0) { + throw new UserError( + `Work unit "${options.workUnit}" not found in project manifest, ` + + `and no matching chunks exist in the knowledge base.\n` + + ` Check the name with \`knowledge status\`.` + ); + } + // Stranded chunks. Proceed with removal as an orphan cleanup. + isOrphanCleanup = true; + process.stderr.write( + `Work unit "${options.workUnit}" is not in the project manifest, but ` + + `${storeMatch} chunks remain in the store. Removing as an orphan cleanup.\n` + ); + } else { + // Any other manifest error is unexpected — rethrow so the stack shows. + throw err; + } } - let removed = 0; + const sp = storePath(); + const desc = formatRemoveDesc(options) + (isOrphanCleanup ? ' (orphan cleanup)' : ''); - await store.withLock(lp, async () => { + // --dry-run is observational only: count what would be removed, touch + // nothing on disk. Don't drain the pending-removal queue either — that + // would be a real side effect. + if (options.dryRun) { + if (!fs.existsSync(sp)) { + process.stdout.write(`Would remove 0 chunks for ${desc} (store not initialised)\n`); + return; + } const db = await store.loadStore(sp); - const where = { work_unit: { eq: options.workUnit } }; if (options.phase) where.phase = { eq: options.phase }; if (options.topic) where.topic = { eq: options.topic }; + const count = await store.countByFilter(db, where); + process.stdout.write(`Would remove ${count} chunks for ${desc}\n`); + return; + } - removed = await store.removeByFilter(db, where); - await store.saveStore(db, sp); - }); + // Drain any previously-failed removals first so stale chunks from earlier + // cancellations/supersessions don't linger just because the store was + // briefly locked. + await processPendingRemovals(); - const desc = formatRemoveDesc(options); - process.stdout.write(`Removed ${removed} chunks for ${desc}\n`); + if (!fs.existsSync(sp)) { + process.stdout.write(`Removed 0 chunks for ${desc}\n`); + return; + } + + try { + const removed = await performRemoval(options); + process.stdout.write(`Removed ${removed} chunks for ${desc}\n`); + } catch (err) { + await addPendingRemoval(options, err.message); + process.stderr.write( + `Removal of ${desc} failed (${err.message}). Queued for automatic retry on next remove/compact.\n` + ); + process.exit(1); + } } function formatRemoveDesc(options) { @@ -1354,25 +1853,33 @@ function getWorkUnitMeta(workUnit) { if (completedAt === '' || completedAt === 'undefined' || completedAt === 'null') { completedAt = null; } - } catch (_) { - // completed_at may not exist. + } catch (err) { + // completed_at may not exist — expected. But surface unexpected errors. + reportUnexpectedManifestError(`getWorkUnitMeta:get(${workUnit}.completed_at)`, err); } return { status, completed_at: completedAt }; - } catch (_) { - // Manifest lookup failed (e.g., orphaned work unit). + } catch (err) { + // Work unit missing or manifest broken. "Not found" is expected for + // orphaned work units referenced by stale chunks; anything else is a + // real problem the user should see. + reportUnexpectedManifestError(`getWorkUnitMeta:get(${workUnit}.status)`, err); return null; } } async function cmdCompact(_args, options, cfg) { + // Drain any previously-failed removals first. + await processPendingRemovals(); + const sp = storePath(); const lp = lockFilePath(); - // Check decay config. Accept only: false (disabled) or non-negative integer. - // Reject strings, negatives, NaN, non-integers — these would silently - // produce either no-op (NaN cutoff) or mass deletion (negative cutoff). + // Check decay config. Accept: false or null (both disable), or a + // non-negative integer. Reject strings, negatives, NaN, non-integers + // — these would silently produce either no-op (NaN cutoff) or mass + // deletion (negative cutoff). const rawDecay = cfg && cfg.decay_months !== undefined ? cfg.decay_months : config.DEFAULTS.decay_months; - if (rawDecay === false) { + if (rawDecay === false || rawDecay === null) { process.stdout.write('Compaction disabled\n'); return; } @@ -1394,7 +1901,7 @@ async function cmdCompact(_args, options, cfg) { cutoffDate.setMonth(cutoffDate.getMonth() - decayMonths); // Discover unique work units in the store by searching for all docs. - const allResults = await store.searchFulltext(db, { term: '', limit: 100000 }); + const allResults = await store.searchAllFulltext(db); if (allResults.length === 0) return; // Group by work unit. @@ -1481,10 +1988,20 @@ async function cmdCompact(_args, options, cfg) { async function main() { const rawArgs = process.argv.slice(2); - const { positional, flags } = parseArgs(rawArgs); + + // Informational help: --help / -h / `help` subcommand. Writes USAGE to + // stdout and exits 0 so scripts can probe the CLI without treating + // help as a failure. `knowledge` with no args is still an error — + // the user forgot a command (stderr, exit 1, handled below). + if (rawArgs.includes('--help') || rawArgs.includes('-h') || rawArgs[0] === 'help') { + process.stdout.write(USAGE + '\n'); + process.exit(0); + } + + const { positional, flags, boosts } = parseArgs(rawArgs); const command = positional[0]; const commandArgs = positional.slice(1); - const options = buildOptions(flags); + const options = buildOptions(flags, boosts); if (!command) { process.stderr.write(USAGE + '\n'); @@ -1520,6 +2037,8 @@ module.exports = { deriveIdentity, resolveProviderState, withRetry, + UserError, + AuthError, main, cmdIndexBulk, StubProvider, @@ -1538,7 +2057,13 @@ module.exports = { if (require.main === module) { main().catch((err) => { - process.stderr.write(String(err && err.stack ? err.stack : err) + '\n'); + // UserError: validation failure — show the message alone, no stack. + // Anything else: full stack (likely a bug worth investigating). + if (err instanceof UserError) { + process.stderr.write('Error: ' + err.message + '\n'); + } else { + process.stderr.write(String(err && err.stack ? err.stack : err) + '\n'); + } process.exit(1); }); } diff --git a/src/knowledge/providers/openai.js b/src/knowledge/providers/openai.js index 4236bc17e..a75cd3f54 100644 --- a/src/knowledge/providers/openai.js +++ b/src/knowledge/providers/openai.js @@ -14,6 +14,16 @@ const DEFAULT_DIMENSIONS = 1536; const OPENAI_EMBEDDINGS_URL = 'https://api.openai.com/v1/embeddings'; const MAX_BATCH_SIZE = 2048; +// AuthError — marker class for HTTP 401/403 from the embeddings API. +// Bad/expired keys do not fix themselves between retries, so withRetry +// short-circuits this class instead of burning the backoff budget. +class AuthError extends Error { + constructor(message) { + super(message); + this.name = 'AuthError'; + } +} + class OpenAIProvider { /** * @param {{ apiKey: string, model?: string, dimensions?: number }} options @@ -42,6 +52,9 @@ class OpenAIProvider { }); const res = await this._fetch(body); + if (!res.data || res.data.length === 0) { + throw new Error('OpenAI embed returned no data (empty response)'); + } return res.data[0].embedding; } @@ -60,8 +73,17 @@ class OpenAIProvider { if (texts.length <= MAX_BATCH_SIZE) { const body = JSON.stringify({ model: this._model, input: texts, dimensions: this._dimensions }); const res = await this._fetch(body); + // Validate response length — a short response silently propagates + // undefined embeddings into Orama and degrades chunks to keyword-only + // with no warning. Rare (OpenAI usually 400s on empty input) but + // cheap to guard. + if (!Array.isArray(res.data) || res.data.length !== texts.length) { + throw new Error( + `OpenAI embedBatch response length mismatch: requested ${texts.length}, received ${res.data ? res.data.length : 0}` + ); + } // OpenAI returns data sorted by index — ensure correct order. - const sorted = res.data.sort((a, b) => a.index - b.index); + const sorted = [...res.data].sort((a, b) => a.index - b.index); return sorted.map((d) => d.embedding); } @@ -71,7 +93,12 @@ class OpenAIProvider { const slice = texts.slice(offset, offset + MAX_BATCH_SIZE); const body = JSON.stringify({ model: this._model, input: slice, dimensions: this._dimensions }); const res = await this._fetch(body); - const sorted = res.data.sort((a, b) => a.index - b.index); + if (!Array.isArray(res.data) || res.data.length !== slice.length) { + throw new Error( + `OpenAI embedBatch response length mismatch on chunk offset=${offset}: requested ${slice.length}, received ${res.data ? res.data.length : 0}` + ); + } + const sorted = [...res.data].sort((a, b) => a.index - b.index); for (let i = 0; i < sorted.length; i++) { results[offset + i] = sorted[i].embedding; } @@ -117,8 +144,13 @@ class OpenAIProvider { } if (res.status === 401) { - throw new Error( - 'OpenAI API key is invalid or expired. Check your OPENAI_API_KEY environment variable.' + throw new AuthError( + 'OpenAI API key is invalid or expired. Run `knowledge setup` to fix.' + ); + } + if (res.status === 403) { + throw new AuthError( + `OpenAI API key lacks permission for this request (HTTP 403). Run \`knowledge setup\` to fix. ${detail}` ); } if (res.status === 429) { @@ -144,6 +176,7 @@ class OpenAIProvider { module.exports = { OpenAIProvider, + AuthError, DEFAULT_MODEL, DEFAULT_DIMENSIONS, MAX_BATCH_SIZE, diff --git a/src/knowledge/setup.js b/src/knowledge/setup.js index 011106dda..2f356db91 100644 --- a/src/knowledge/setup.js +++ b/src/knowledge/setup.js @@ -363,21 +363,43 @@ async function runSystemConfigStep(rl) { // openai path. const model = await ask(rl, 'Embedding model', OPENAI_DEFAULT_MODEL); const dimsRaw = await ask(rl, 'Vector dimensions', String(OPENAI_DEFAULT_DIMENSIONS)); + // parseInt is lenient ('1536abc' → 1536); insist on a clean digits-only + // string before parsing so partial-numeric input is rejected up front. + if (!/^\d+$/.test(dimsRaw.trim())) { + process.stderr.write(`Invalid dimensions: "${dimsRaw}". Must be a positive integer.\n`); + process.exit(1); + } const dimensions = parseInt(dimsRaw, 10); if (!Number.isInteger(dimensions) || dimensions <= 0) { process.stderr.write(`Invalid dimensions: "${dimsRaw}". Must be a positive integer.\n`); process.exit(1); } - // Write the non-secret config first so it's on disk even if the key - // step aborts or the user interrupts mid-prompt. - config.writeConfigFile(sysPath, buildSystemConfigOpenAI({ model, dimensions })); - process.stdout.write(`\nWrote system config to ${sysPath}\n`); - - // Resolve the API key: env first, then credentials file, then prompt. + // Resolve and validate the API key BEFORE writing any system config. + // Writing `provider: openai` to disk with no working key leaves the + // system in a state where the next `knowledge index` misreports a + // provider/model change. ensureOpenAIKey aborts on irrecoverable + // failures (bad env var, bad stored key kept) and returns a status + // for the recoverable cases. const envVar = config.PROVIDER_ENV_VARS.openai; - await ensureOpenAIKey(rl, { envVar, model, dimensions }); + const keyStatus = await ensureOpenAIKey(rl, { envVar, model, dimensions }); + + if (keyStatus === 'opted-out') { + // User explicitly chose to skip past the inline prompt without a + // working key. Honour the "skip to proceed without" feature by + // writing stub-mode system config rather than openai-with-no-key. + config.writeConfigFile(sysPath, buildSystemConfigStub()); + process.stdout.write(`\nWrote stub-mode system config to ${sysPath}\n`); + process.stdout.write( + 'Stub mode uses keyword-only (BM25) search. Semantic search is disabled. ' + + 'Re-run `knowledge setup` once you have a working API key.\n' + ); + return { provider: null, previouslyStub }; + } + // keyStatus === 'validated' — safe to commit the openai config. + config.writeConfigFile(sysPath, buildSystemConfigOpenAI({ model, dimensions })); + process.stdout.write(`\nWrote system config to ${sysPath}\n`); return { provider: 'openai', previouslyStub }; } @@ -385,6 +407,14 @@ async function runSystemConfigStep(rl) { * Ensure an OpenAI API key is available for this run and validate it. * Resolution order: process.env → credentials file → inline prompt. * Newly entered keys are written to the credentials file at 0600. + * + * Returns: + * 'validated' — a working key is in place (env, file, or freshly stored) + * 'opted-out' — user explicitly skipped after a failed inline prompt + * + * Aborts (process.exit 1) on irrecoverable failures: bad env-var key, or + * bad stored key that the user opts to keep. Both leave the system in a + * state where openai-mode setup cannot honestly proceed. */ async function ensureOpenAIKey(rl, { envVar, model, dimensions }) { const credPath = config.credentialsPath(); @@ -396,16 +426,16 @@ async function ensureOpenAIKey(rl, { envVar, model, dimensions }) { try { await validateApiKey({ apiKey: fromEnv.trim(), model, dimensions }); process.stdout.write('API key works.\n'); + return 'validated'; } catch (err) { const { message, hint } = describeValidationError(err); - process.stdout.write(`${message}\n ${hint}\n`); - process.stdout.write( + process.stderr.write(`\n${message}\n ${hint}\n`); + process.stderr.write( `The failing key came from $${envVar}. Fix or unset it in your shell, ` + - 'then re-run `knowledge setup`. Setup will continue — indexing will queue until ' + - 'the key is corrected.\n' + 'then re-run `knowledge setup`.\n' ); + process.exit(1); } - return; } // 2. Existing credentials file. Validate; let the user replace it if broken. @@ -415,24 +445,24 @@ async function ensureOpenAIKey(rl, { envVar, model, dimensions }) { try { await validateApiKey({ apiKey: fromFile, model, dimensions }); process.stdout.write('API key works.\n'); - return; + return 'validated'; } catch (err) { const { message, hint } = describeValidationError(err); process.stdout.write(`${message}\n ${hint}\n`); const replace = await askYesNo(rl, 'Enter a new key to replace it?', true); if (!replace) { - process.stdout.write( - 'Keeping the existing stored key. Indexing will fail until it is rotated.\n' + + process.stderr.write( + `\nKeeping the existing stored key would leave setup in an inconsistent state.\n` + `Edit ${credPath} or re-run \`knowledge setup\` when you have a new key.\n` ); - return; + process.exit(1); } // Fall through to prompt path to collect and store a replacement. } } // 3. No valid key anywhere — prompt inline and store. - await promptForKeyAndStore(rl, { envVar, model, dimensions, credPath }); + return await promptForKeyAndStore(rl, { envVar, model, dimensions, credPath }); } /** @@ -475,10 +505,10 @@ async function promptForKeyAndStore(rl, { envVar, model, dimensions, credPath }) const retry = await askYesNo(rl, 'Try a different key?', true); if (!retry) { process.stdout.write( - 'No key stored. Setup continues but indexing will skip until a key is provided.\n' + - `Set $${envVar} in your shell or re-run \`knowledge setup\`.\n` + `No key stored. Falling back to stub mode — semantic search disabled.\n` + + `Set $${envVar} in your shell or re-run \`knowledge setup\` once you have a working key.\n` ); - return; + return 'opted-out'; } continue; } @@ -486,7 +516,7 @@ async function promptForKeyAndStore(rl, { envVar, model, dimensions, credPath }) // Validated — write the credentials file. config.writeCredentials(credPath, 'openai', key); process.stdout.write(`API key works. Stored at ${credPath} (mode 0600).\n`); - return; + return 'validated'; } } @@ -495,13 +525,30 @@ async function promptForKeyAndStore(rl, { envVar, model, dimensions, credPath }) // --------------------------------------------------------------------------- async function runProjectInitStep(rl) { - const projectDir = path.resolve(process.cwd(), '.workflows', '.knowledge'); + const projectDir = path.resolve(config.findProjectRoot(), '.workflows', '.knowledge'); const projectConfigFile = path.join(projectDir, 'config.json'); const storeFile = path.join(projectDir, 'store.msp'); const metadataFile = path.join(projectDir, 'metadata.json'); const detected = detectProjectInit(projectDir); + // Reject the dangerous partial-state where the store has chunks but + // metadata is missing. Writing fresh metadata against an existing + // populated store would create a provider/model/dimensions mismatch + // we cannot detect from the store alone — the next `knowledge index` + // would surface a misleading error or, worse, mix incompatible + // vectors. The escape hatch is `knowledge rebuild`. + if (detected.storeExists && !detected.metadataExists) { + process.stderr.write( + `\nProject knowledge base at ${projectDir} is in an inconsistent state:\n` + + ` store.msp is present but metadata.json is missing.\n` + + ` Setup cannot recover this safely — run \`knowledge rebuild\` (which\n` + + ` re-creates the store from scratch and writes matching metadata) and\n` + + ` then re-run \`knowledge setup\` if needed.\n` + ); + process.exit(1); + } + if (detected.fullyInitialised) { process.stdout.write(`\nProject knowledge base already initialised at ${projectDir}\n`); const reinit = await askYesNo(rl, 'Reinitialise (destroys existing store)?', false); @@ -533,14 +580,18 @@ async function runProjectInitStep(rl) { : KEYWORD_ONLY_DIMENSIONS; // Create empty store and save. - if (!detected.storeExists || detected.fullyInitialised) { + const wroteStore = !detected.storeExists || detected.fullyInitialised; + if (wroteStore) { const db = await store.createStore(dims); await store.saveStore(db, storeFile); process.stdout.write(` store.msp written (${dims} dimensions)\n`); } - // Write initial metadata. - if (!detected.metadataExists || detected.fullyInitialised) { + // Write initial metadata. Also rewrite whenever a new store was just + // created — stale metadata paired with a fresh empty store surfaces + // as a misleading "Provider/model changed — run rebuild" error on + // the next `knowledge index` (the partial-state recovery case). + if (!detected.metadataExists || detected.fullyInitialised || wroteStore) { store.writeMetadata(metadataFile, { provider: provider || null, model: provider && cfg.model ? cfg.model : null, @@ -582,8 +633,8 @@ async function runInitialIndexStep(cmdIndexBulk, options) { async function cmdSetup(cmdIndexBulk, args, options) { requireTTY(); - // Guard: .workflows/ must exist. - const workflowsDir = path.resolve(process.cwd(), '.workflows'); + // Guard: .workflows/ must exist somewhere at or above cwd. + const workflowsDir = path.resolve(config.findProjectRoot(), '.workflows'); if (!fs.existsSync(workflowsDir)) { process.stderr.write( 'No .workflows/ directory found. Initialise a workflow project first.\n' diff --git a/src/knowledge/store.js b/src/knowledge/store.js index 3abdd2931..7fecbf427 100644 --- a/src/knowledge/store.js +++ b/src/knowledge/store.js @@ -112,10 +112,51 @@ async function insertDocument(db, doc) { return orama.insert(db, payload); } +// Page size for whole-store enumeration. Paged iteration replaces a +// previous fixed `limit: 100000` so very large stores are not silently +// truncated. 1000 keeps round-trip overhead negligible for in-process +// Orama while bounding peak memory per page. +const ENUMERATION_PAGE_SIZE = 1000; + +// Limit for filtered single-shot reads. Orama's `where` clause is an +// indexed lookup — the matched subset is small in practice (one identity, +// one phase, one work unit), so a single search with a high limit is the +// right shape. Pagination is reserved for unfiltered whole-store +// enumeration because Orama 3.1.x's offset+where combination drops pages. +// +// 1M is well above any realistic per-identity / per-topic / per-phase +// chunk count (a giant topic might have low thousands; a per-work-unit +// remove on a long-lived project, low tens of thousands). Orama +// pre-allocates an array of size `limit` for results, so this cannot be +// `Number.MAX_SAFE_INTEGER` — it would throw RangeError. +const FILTERED_QUERY_LIMIT = 1_000_000; + +/** + * Enumerate every document in the store, paged via offset+limit until + * exhausted. Used by status, compact, and any caller that needs a + * complete unfiltered view. Filtered enumerations should call Orama's + * `where` directly with an unbounded limit (see findInternalIdsByIdentity). + */ +async function searchAllFulltext(db) { + const all = []; + let offset = 0; + while (true) { + const res = await orama.search(db, { + term: '', + limit: ENUMERATION_PAGE_SIZE, + offset, + }); + if (res.hits.length === 0) break; + all.push(...res.hits.map(normaliseHit)); + if (res.hits.length < ENUMERATION_PAGE_SIZE) break; + offset += ENUMERATION_PAGE_SIZE; + } + return all; +} + /** * Find all internal document IDs whose (work_unit, phase, topic) matches - * the given identity key. Uses Orama's search with `where` filters and - * a large limit so we get every match in one pass. + * the given identity key. Internal IDs are what `removeMultiple` accepts. */ async function findInternalIdsByIdentity(db, { work_unit, phase, topic }) { const res = await orama.search(db, { @@ -125,7 +166,7 @@ async function findInternalIdsByIdentity(db, { work_unit, phase, topic }) { phase: { eq: phase }, topic: { eq: topic }, }, - limit: 100000, + limit: FILTERED_QUERY_LIMIT, }); return res.hits.map((h) => h.id); } @@ -164,14 +205,35 @@ async function removeByFilter(db, where) { const res = await orama.search(db, { term: '', where, - limit: 100000, + limit: FILTERED_QUERY_LIMIT, }); const ids = res.hits.map((h) => h.id); if (ids.length === 0) return 0; - const removed = await orama.removeMultiple(db, ids); + // Orama's sync removeMultiple chains batches via setTimeout but + // returns the result count after only the first batch — pass + // batchSize == ids.length to force a single batch and get an + // accurate total when removing > 1000 chunks. + const removed = await orama.removeMultiple(db, ids, ids.length); return removed; } +/** + * Count chunks matching `where` without deleting. Used by `remove --dry-run`. + * Same query shape as removeByFilter so the count is guaranteed to match + * what a non-dry-run invocation would actually remove. + */ +async function countByFilter(db, where) { + if (!where || Object.keys(where).length === 0) { + throw new Error('countByFilter: where clause is required'); + } + const res = await orama.search(db, { + term: '', + where, + limit: FILTERED_QUERY_LIMIT, + }); + return res.hits.length; +} + function normaliseHit(hit) { const d = hit.document || {}; return { @@ -348,7 +410,7 @@ async function loadStore(storePath) { const LOCK_STALE_MS = 30000; const LOCK_RETRY_MS = 50; -const LOCK_TIMEOUT_MS = 10000; +const LOCK_TIMEOUT_MS = 30000; function tryAcquire(lockPath) { try { @@ -415,7 +477,9 @@ async function withLock(lockPath, fn) { // provides the read/write primitives. // --------------------------------------------------------------------------- -const METADATA_FIELDS = ['provider', 'model', 'dimensions', 'last_indexed', 'pending']; +const METADATA_FIELDS = [ + 'provider', 'model', 'dimensions', 'last_indexed', 'pending', 'pending_removals', +]; function writeMetadata(metadataPath, data) { if (!metadataPath) throw new Error('writeMetadata: metadataPath is required'); @@ -425,12 +489,18 @@ function writeMetadata(metadataPath, data) { // Every call writes the full schema — no partial updates. Missing // fields are normalised to explicit null so keyword-only mode round- // trips as { provider: null, model: null, dimensions: null }. + // + // IMPORTANT: every persisted field must be listed here. A missing field + // silently strips across writes and every downstream feature using that + // field stops working (see deferred-issue #18 pending_removals, which + // shipped broken because this whitelist was not updated). const full = { provider: data.provider === undefined ? null : data.provider, model: data.model === undefined ? null : data.model, dimensions: data.dimensions === undefined ? null : data.dimensions, last_indexed: data.last_indexed === undefined ? null : data.last_indexed, pending: Array.isArray(data.pending) ? data.pending : [], + pending_removals: Array.isArray(data.pending_removals) ? data.pending_removals : [], }; const tmp = metadataPath + '.tmp'; fs.writeFileSync(tmp, JSON.stringify(full, null, 2) + '\n', 'utf8'); @@ -465,7 +535,9 @@ module.exports = { insertDocument, removeByIdentity, removeByFilter, + countByFilter, searchFulltext, + searchAllFulltext, searchVector, searchHybrid, saveStore, diff --git a/tests/scripts/kb-smoke.cjs b/tests/scripts/kb-smoke.cjs new file mode 100644 index 000000000..bb27c66b5 --- /dev/null +++ b/tests/scripts/kb-smoke.cjs @@ -0,0 +1,147 @@ +'use strict'; +// Manual smoke test — exercises every Orama API path through the built +// bundle after the ESM-resolution change. Not wired into the automated +// suite — run ad hoc when verifying bundle-level changes. + +const path = require('path'); +const fs = require('fs'); +const os = require('os'); + +const bundle = require(path.resolve(__dirname, '..', '..', 'skills', 'workflow-knowledge', 'scripts', 'knowledge.cjs')); +const { StubProvider, store } = bundle; +const { + createStore, insertDocument, removeByFilter, + searchFulltext, searchVector, searchHybrid, + saveStore, loadStore, writeMetadata, readMetadata, +} = store; + +const DIMS = 128; +const provider = new StubProvider({ dimensions: DIMS }); + +const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'kb-smoke-')); +const storePath = path.join(dir, 'store.msp'); +const metaPath = path.join(dir, 'metadata.json'); + +let failures = 0; +function check(label, cond) { + if (cond) { + console.log(' PASS:', label); + } else { + console.log(' FAIL:', label); + failures++; + } +} + +async function main() { + console.log('Smoke test: ESM-resolved bundle end-to-end\n'); + + const db = await createStore(DIMS); + check('create store with vector schema', db !== null); + + const workUnits = ['auth-flow', 'data-model', 'billing']; + const phases = ['discussion', 'specification', 'research']; + let inserted = 0; + for (let i = 0; i < 100; i++) { + const wu = workUnits[i % 3]; + const phase = phases[i % 3]; + const embedding = await provider.embed('content ' + i + ' ' + wu + ' ' + phase); + await insertDocument(db, { + id: wu + '-' + phase + '-t' + i + '-001', + content: 'Document ' + i + ' about ' + phase + ' in ' + wu + '. Token refresh and rate limiting.', + work_unit: wu, + work_type: 'feature', + phase: phase, + topic: 't' + i, + confidence: i % 4 === 0 ? 'high' : 'medium', + source_file: '.workflows/' + wu + '/' + phase + '/t' + i + '.md', + timestamp: Date.now() - i * 86400000, + embedding: embedding, + }); + inserted++; + } + check('inserted 100 docs', inserted === 100); + + const ftRes = await searchFulltext(db, { + term: 'refresh', + where: { work_unit: { eq: 'auth-flow' } }, + limit: 50, + }); + check('fulltext search returns results', ftRes.length > 0); + check('fulltext where filter honoured', ftRes.every(function (r) { return r.work_unit === 'auth-flow'; })); + + const qVec = await provider.embed('token refresh design'); + const vecRes = await searchVector(db, { vector: qVec, limit: 10, similarity: 0.1 }); + check('vector search returns results', vecRes.length > 0); + + const hybRes = await searchHybrid(db, { + term: 'rate limiting', + vector: qVec, + limit: 10, + similarity: 0.5, + }); + check('hybrid search returns results', hybRes.length > 0); + + // Hybrid with a flat/poor vector + tight similarity still returns BM25 + // hits — Orama's hybrid mode handles the "vector matches all weak" case + // natively without needing a fulltext fallback. (deferred-issue #15 + // concern was empirically not reproducible.) + const gibberishVec = new Array(DIMS).fill(0.0001); + const hybStrictRes = await searchHybrid(db, { + term: 'refresh', + vector: gibberishVec, + limit: 10, + similarity: 0.99, + }); + check('hybrid surfaces BM25 hits even with weak vector matches', hybStrictRes.length > 0); + + const removed = await removeByFilter(db, { work_unit: { eq: 'billing' } }); + check('removeByFilter returned count > 0', removed > 0); + const afterRemove = await searchFulltext(db, { + term: '', + where: { work_unit: { eq: 'billing' } }, + limit: 100, + }); + check('removeByFilter actually removed docs', afterRemove.length === 0); + + await saveStore(db, storePath); + const stat = fs.statSync(storePath); + check('store saved to disk', stat.size > 0); + console.log(' store size on disk: ' + (stat.size / 1024).toFixed(1) + ' KB'); + + writeMetadata(metaPath, { + provider: 'stub', + model: 'stub', + dimensions: DIMS, + last_indexed: new Date().toISOString(), + pending: [], + }); + const meta = readMetadata(metaPath); + check('metadata round-trip', meta.provider === 'stub' && meta.dimensions === DIMS); + + const db2 = await loadStore(storePath); + const afterLoadFt = await searchFulltext(db2, { term: 'refresh', limit: 100 }); + check('loaded store serves fulltext', afterLoadFt.length > 0); + + // Vector search on the loaded store proves embeddings survived the round-trip + // internally — Orama doesn't echo the embedding field back on hits by design, + // so we can't assert on it directly; correct search results are the real signal. + const afterLoadVec = await searchVector(db2, { vector: qVec, limit: 5, similarity: 0.1 }); + check('loaded store serves vector', afterLoadVec.length > 0); + + const afterLoadHyb = await searchHybrid(db2, { + term: 'rate', + vector: qVec, + limit: 5, + similarity: 0.3, + }); + check('loaded store serves hybrid', afterLoadHyb.length > 0); + + console.log('\n' + (failures === 0 ? 'ALL PASSED' : failures + ' FAILURES')); + fs.rmSync(dir, { recursive: true, force: true }); + process.exit(failures === 0 ? 0 : 1); +} + +main().catch(function (e) { + console.error(e); + process.exit(2); +}); diff --git a/tests/scripts/test-knowledge-build.sh b/tests/scripts/test-knowledge-build.sh index 8c0d5e10f..e4d7033ad 100755 --- a/tests/scripts/test-knowledge-build.sh +++ b/tests/scripts/test-knowledge-build.sh @@ -7,7 +7,9 @@ set -eo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" BUNDLE="$REPO_DIR/skills/workflow-knowledge/scripts/knowledge.cjs" -MAX_BUNDLE_BYTES=153600 # 150 KB +MAX_BUNDLE_BYTES=179200 # 175 KB — current is 154 KB after ESM-resolution build; + # threshold gives ~20 KB headroom for dependency drift. + # Exists to catch regressions, not to hit an absolute target. LOG_DIR="${TMPDIR:-/tmp}" PASS=0 @@ -43,7 +45,7 @@ test_bundle_exists() { "$([ -f "$BUNDLE" ] && echo true || echo false)" } -# --- Test 3: bundle under 150KB --- +# --- Test 3: bundle under threshold --- test_bundle_under_threshold() { local size size=$(/usr/bin/stat -f '%z' "$BUNDLE" 2>/dev/null || stat -c '%s' "$BUNDLE") @@ -53,7 +55,7 @@ test_bundle_under_threshold() { under=false echo " bundle size: $size bytes (threshold: $MAX_BUNDLE_BYTES)" fi - assert_eq "bundle size under 150KB" "true" "$under" + assert_eq "bundle size under threshold" "true" "$under" } # --- Test 4: bundle runs (no-command prints usage, exits 1 — expected CLI behaviour) --- diff --git a/tests/scripts/test-knowledge-cli.sh b/tests/scripts/test-knowledge-cli.sh index 4f176a7b2..0508744fe 100644 --- a/tests/scripts/test-knowledge-cli.sh +++ b/tests/scripts/test-knowledge-cli.sh @@ -24,6 +24,14 @@ assert_eq() { fi } +# Isolate tests from the developer's real system config (~/.config/workflows/). +# Without this, a real OpenAI config leaks into keyword-only tests and breaks +# Test 11 onward. The knowledge CLI resolves the system path via os.homedir(), +# which honours $HOME — that's the only var that matters. +FAKE_HOME=$(mktemp -d) +export HOME="$FAKE_HOME" +trap 'rm -rf "$FAKE_HOME"' EXIT + # Create a temp dir as the project root for each test group. TEST_ROOT="" setup_project() { @@ -323,6 +331,27 @@ cd "$TEST_ROOT" output=$(node "$BUNDLE" index .workflows/auth-flow/planning/auth-flow/planning.md 2>&1 || true) node "$BUNDLE" index .workflows/auth-flow/planning/auth-flow/planning.md 2>/dev/null || exit_code=$? assert_eq "rejects non-indexed phase" "1" "$exit_code" +# User-visible validation error should be a clean message — no stack +# trace noise. UserError class handles this centrally. Note: for +# /planning/ paths the regex rejects first, producing the 'Cannot +# derive identity' message rather than 'not indexed'. +assert_eq "no node stack frames in error" "false" \ + "$(echo "$output" | grep -qE '^ at [A-Za-z_]+ \(' && echo true || echo false)" +assert_eq "error message surfaces" "true" \ + "$(echo "$output" | grep -qE 'Cannot derive identity|not indexed' && echo true || echo false)" +teardown_project + +# --- Test 14b: Non-.workflows path also produces clean user-facing error --- +echo "Test 14b: Non-.workflows path — clean error, no stack" +setup_project +write_stub_config +echo "hello" > "$TEST_ROOT/outside.md" +cd "$TEST_ROOT" +output=$(node "$BUNDLE" index outside.md 2>&1 || true) +assert_eq "no stack frames" "false" \ + "$(echo "$output" | grep -qE '^ at [A-Za-z_]+ \(' && echo true || echo false)" +assert_eq "error message surfaces" "true" \ + "$(echo "$output" | grep -q 'Cannot derive identity' && echo true || echo false)" teardown_project # --- Test 15: metadata.json created with empty pending array on first index --- @@ -447,6 +476,41 @@ assert_eq "query refuses mismatch" "true" "$([ "$exit_code" -ne 0 ] && echo true assert_eq "mentions rebuild" "true" "$(echo "$output" | grep -q 'rebuild' && echo true || echo false)" teardown_project +# --- Test 22b: Bulk index also refuses provider/dimension mismatch --- +# Previously: bulk index with a mismatching config short-circuited at +# isIndexed() for every file, reported 'N already indexed' as if fine, +# and the stored embeddings silently diverged from the configured dims. +# Now: preflight resolveProviderState check fires before the per-file +# loop, same 'Run knowledge rebuild' message as query. +echo "Test 22b: Bulk index refuses provider mismatch" +setup_project +create_work_unit "auth-flow" "feature" "Auth" +write_stub_config +create_discussion_file "auth-flow" "auth-flow" +run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 +# Simulate a provider/dimension change by editing metadata. +node -e " + const fs = require('fs'); + const mp = '$TEST_ROOT/.workflows/.knowledge/metadata.json'; + const m = JSON.parse(fs.readFileSync(mp, 'utf8')); + m.provider = 'openai'; + m.model = 'text-embedding-3-small'; + m.dimensions = 1536; + fs.writeFileSync(mp, JSON.stringify(m, null, 2) + '\n'); +" +cd "$TEST_ROOT" && node "$MANIFEST_JS" set auth-flow.discussion.auth-flow status completed >/dev/null 2>&1 +exit_code=0 +# Bulk-index (no file arg) should now error on the mismatch instead of +# silently reporting 'N already indexed'. +output=$(run_kb index 2>&1) || exit_code=$? +assert_eq "bulk index refuses mismatch" "true" \ + "$([ "$exit_code" -ne 0 ] && echo true || echo false)" +assert_eq "error mentions rebuild" "true" \ + "$(echo "$output" | grep -q 'rebuild' && echo true || echo false)" +assert_eq "bulk did NOT report 'already indexed'" "false" \ + "$(echo "$output" | grep -q 'already indexed' && echo true || echo false)" +teardown_project + # --- Test 23: Stub-to-full upgrade note --- echo "Test 23: Stub-to-full upgrade note" setup_project @@ -460,6 +524,46 @@ output=$(run_kb query "topic" 2>&1) assert_eq "shows upgrade note" "true" "$(echo "$output" | grep -q 'keyword-only mode' && echo true || echo false)" teardown_project +# --- Test 23b: Stub-to-full upgrade note also fires on `index` (not just query) --- +echo "Test 23b: Upgrade note on index" +setup_project +create_work_unit "auth-flow" "feature" "Auth" +write_keyword_config +create_discussion_file "auth-flow" "auth-flow" +run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 +# Upgrade config to have a provider. +write_stub_config +# Re-index: should emit upgrade note on stderr. +output=$(run_kb index .workflows/auth-flow/discussion/auth-flow.md 2>&1) +assert_eq "shows upgrade note on index" "true" "$(echo "$output" | grep -q 'Run .knowledge rebuild.' && echo true || echo false)" +teardown_project + +# --- Test 23c: Empty-string query rejected (no "match everything") --- +# Orama treats empty term as wildcard and returns up to `limit` chunks. +# That's almost always a caller bug (unsubstituted template variable, +# accidental empty positional) — surface it as an error rather than +# returning arbitrary hits. +echo "Test 23c: Empty query term rejected" +setup_project +create_work_unit "auth-flow" "feature" "Auth" +write_stub_config +create_discussion_file "auth-flow" "auth-flow" +run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 +exit_code=0 +output=$(run_kb query "" 2>&1) || exit_code=$? +assert_eq "empty query exits non-zero" "true" "$([ "$exit_code" -ne 0 ] && echo true || echo false)" +assert_eq "error message surfaces" "true" \ + "$(echo "$output" | grep -q 'Empty search term' && echo true || echo false)" +# Whitespace-only term also rejected. +exit_code=0 +output=$(run_kb query " " 2>&1) || exit_code=$? +assert_eq "whitespace-only query exits non-zero" "true" "$([ "$exit_code" -ne 0 ] && echo true || echo false)" +# Any non-empty term still works. +exit_code=0 +run_kb query "content" >/dev/null 2>&1 || exit_code=$? +assert_eq "valid query still succeeds" "0" "$exit_code" +teardown_project + # --- Test 24: Query on empty store returns [0 results] --- echo "Test 24: Empty store" setup_project @@ -586,11 +690,71 @@ Line 48. Line 49. Line 50. Line 51. Line 52. Line 53. MD run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 run_kb index .workflows/data-model/discussion/data-model.md >/dev/null 2>&1 -# Query with --work-unit auth-flow: auth-flow results should appear first. -output=$(run_kb query "token refresh" --work-unit auth-flow --limit 2 2>&1) +# Query with --boost:work-unit auth-flow: auth-flow results should appear first. +output=$(run_kb query "token refresh" --boost:work-unit auth-flow --limit 2 2>&1) # Extract the first provenance line's work_unit. first_wu=$(echo "$output" | grep -m1 '^\[discussion' | sed 's/.*| \([^/]*\)\/.*/\1/') assert_eq "boosted work-unit appears first" "auth-flow" "$first_wu" +# Cross-work-unit context still surfaces — data-model should be present too. +assert_eq "boost keeps cross-work-unit results" "true" \ + "$(echo "$output" | grep -q 'data-model' && echo true || echo false)" +teardown_project + +# --- Test 31b: --work-unit is a hard filter on query (excludes other units) --- +echo "Test 31b: --work-unit filters on query" +setup_project +create_work_unit "auth-flow" "feature" "Auth" +create_work_unit "data-model" "feature" "Data" +write_stub_config +mkdir -p "$TEST_ROOT/.workflows/auth-flow/discussion" +mkdir -p "$TEST_ROOT/.workflows/data-model/discussion" +cat > "$TEST_ROOT/.workflows/auth-flow/discussion/auth-flow.md" <<'MD' +# Auth Discussion +## Token refresh +Token refresh design. Rate limiting. Padding line 1. Padding line 2. Padding line 3. +Padding line 4. Padding line 5. Padding line 6. Padding line 7. Padding line 8. +Padding line 9. Padding line 10. Padding line 11. Padding line 12. Padding line 13. +MD +cat > "$TEST_ROOT/.workflows/data-model/discussion/data-model.md" <<'MD' +# Data Model Discussion +## Token storage +Token refresh storage. Rate limiting. Padding line 1. Padding line 2. Padding line 3. +Padding line 4. Padding line 5. Padding line 6. Padding line 7. Padding line 8. +Padding line 9. Padding line 10. Padding line 11. Padding line 12. Padding line 13. +MD +run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 +run_kb index .workflows/data-model/discussion/data-model.md >/dev/null 2>&1 +output=$(run_kb query "token" --work-unit auth-flow --limit 10 2>&1) +assert_eq "filter includes auth-flow" "true" "$(echo "$output" | grep -q 'auth-flow/' && echo true || echo false)" +assert_eq "filter excludes data-model" "false" "$(echo "$output" | grep -q 'data-model/' && echo true || echo false)" +teardown_project + +# --- Test 31c: unknown boost field errors out --- +echo "Test 31c: Unknown --boost field errors" +setup_project +create_work_unit "auth-flow" "feature" "Auth" +write_stub_config +create_discussion_file "auth-flow" "auth-flow" +run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 +exit_code=0 +output=$(run_kb query "anything" --boost:bogus foo 2>&1) || exit_code=$? +assert_eq "unknown boost field exits non-zero" "true" "$([ "$exit_code" -ne 0 ] && echo true || echo false)" +assert_eq "unknown boost field error mentions valid fields" "true" \ + "$(echo "$output" | grep -q 'work-unit, work-type, phase, topic, confidence' && echo true || echo false)" +teardown_project + +# --- Test 31d: missing --boost value errors out --- +echo "Test 31d: Missing --boost value errors" +setup_project +create_work_unit "auth-flow" "feature" "Auth" +write_stub_config +create_discussion_file "auth-flow" "auth-flow" +run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 +exit_code=0 +output=$(run_kb query "anything" --boost:work-unit 2>&1) || exit_code=$? +assert_eq "missing boost value exits non-zero" "true" "$([ "$exit_code" -ne 0 ] && echo true || echo false)" +assert_eq "missing boost value error explicit" "true" \ + "$(echo "$output" | grep -q 'requires a value' && echo true || echo false)" teardown_project # --- Test 32: Query errors when metadata missing but store exists --- @@ -675,6 +839,54 @@ assert_eq "outputs not-ready" "not-ready" "$(echo "$output" | tr -d '\n')" assert_eq "exits 0" "0" "$exit_code" teardown_project +# --- Test 37b: Check not-ready when config.json is corrupt (invalid JSON) --- +echo "Test 37b: Check not-ready (corrupt config JSON)" +setup_project +# Write invalid JSON to the config — previously this slipped through and +# check reported 'ready'; user only discovered the problem on a later +# index/query with a cryptic JSON parse error. +echo 'not valid json{{' > "$TEST_ROOT/.workflows/.knowledge/config.json" +stdout=$(run_kb check 2>/dev/null) +stderr=$(run_kb check 2>&1 >/dev/null) +assert_eq "outputs not-ready on corrupt config" "not-ready" "$(echo "$stdout" | tr -d '\n')" +assert_eq "writes config diagnostic on stderr" "true" \ + "$(echo "$stderr" | grep -q 'config error' && echo true || echo false)" +teardown_project + +# --- Test 37c: Check not-ready when config lacks 'knowledge' key --- +echo "Test 37c: Check not-ready (config missing knowledge key)" +setup_project +echo '{"other":"stuff"}' > "$TEST_ROOT/.workflows/.knowledge/config.json" +stdout=$(run_kb check 2>/dev/null) +stderr=$(run_kb check 2>&1 >/dev/null) +assert_eq "outputs not-ready on shape mismatch" "not-ready" "$(echo "$stdout" | tr -d '\n')" +assert_eq "diagnostic mentions 'knowledge' key" "true" \ + "$(echo "$stderr" | grep -q 'knowledge' && echo true || echo false)" +teardown_project + +# --- Test 37d: Corrupt config overrides otherwise-ready state --- +# Strongest guard: an otherwise-healthy KB (valid store, valid metadata) +# whose config gets corrupted. Pre-fix, cmdCheck only checked file +# existence for config — so this reported 'ready' and deferred the +# parse error to the next index/query. Now: not-ready. +echo "Test 37d: Check not-ready (corrupt config overrides ready state)" +setup_project +create_work_unit "auth-flow" "feature" "Auth" +write_stub_config +create_discussion_file "auth-flow" "auth-flow" +run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 +# Sanity: with valid config, check reports ready. +baseline=$(run_kb check 2>/dev/null | tr -d '\n') +assert_eq "baseline is ready" "ready" "$baseline" +# Now corrupt the config; store + metadata remain intact. +echo 'not valid json{{' > "$TEST_ROOT/.workflows/.knowledge/config.json" +stdout=$(run_kb check 2>/dev/null) +stderr=$(run_kb check 2>&1 >/dev/null) +assert_eq "flips to not-ready on corrupt config" "not-ready" "$(echo "$stdout" | tr -d '\n')" +assert_eq "stderr explains why" "true" \ + "$(echo "$stderr" | grep -q 'config error' && echo true || echo false)" +teardown_project + # ============================================================================ # REMOVE COMMAND TESTS # ============================================================================ @@ -702,6 +914,31 @@ assert_eq "data-model chunks unaffected" "true" "$(echo "$query_output" | grep - assert_eq "auth-flow chunks gone" "false" "$(echo "$query_output" | grep -q 'auth-flow/auth-flow' && echo true || echo false)" teardown_project +# --- Test 38b: remove --dry-run previews without deleting --- +echo "Test 38b: remove --dry-run is observational" +setup_project +create_work_unit "preview-wu" "feature" "Preview" +write_stub_config +create_discussion_file "preview-wu" "preview-wu" +run_kb index .workflows/preview-wu/discussion/preview-wu.md >/dev/null 2>&1 +# Use a term that appears in the fixture ('content') to count chunks — +# empty-string queries are now a UserError (intentional, to catch +# unsubstituted template variables). +# grep -c exits 1 on zero matches; absorb under set -eo pipefail. +before=$(run_kb query "content" --limit 100 2>&1 | grep -c '^\[discussion' || true) +dry_output=$(run_kb remove --work-unit preview-wu --dry-run 2>&1) +assert_eq "dry-run says 'Would remove'" "true" \ + "$(echo "$dry_output" | grep -q 'Would remove' && echo true || echo false)" +assert_eq "dry-run does NOT say 'Removed'" "false" \ + "$(echo "$dry_output" | grep -qE '^Removed' && echo true || echo false)" +after=$(run_kb query "content" --limit 100 2>&1 | grep -c '^\[discussion' || true) +assert_eq "chunk count unchanged after dry-run" "$before" "$after" +# Sanity: a real remove afterwards DOES delete. +run_kb remove --work-unit preview-wu >/dev/null 2>&1 +after_real=$(run_kb query "content" --limit 100 2>&1 | grep -c '^\[discussion' || true) +assert_eq "real remove actually deletes" "0" "$after_real" +teardown_project + # --- Test 39: Remove chunks for a work unit + phase --- echo "Test 39: Remove chunks for work unit + phase" setup_project @@ -735,15 +972,30 @@ assert_eq "invoicing chunks unaffected" "true" "$(echo "$query_output" | grep -q assert_eq "billing chunks gone" "false" "$(echo "$query_output" | grep -q 'billing' && echo true || echo false)" teardown_project -# --- Test 41: Remove reports 0 when no chunks match --- -echo "Test 41: Remove 0 when no match" +# --- Test 41: Remove rejects unknown work unit (registry lookup) --- +# Previously this silently succeeded with 'Removed 0 chunks' — a fat-fingered +# --work-unit looked identical to a legitimate no-op removal. Now the work +# unit must exist in the project registry (migration 031 keeps the registry +# in sync with the filesystem). +echo "Test 41: Remove rejects unknown work unit" setup_project create_work_unit "auth-flow" "feature" "Auth" write_stub_config create_discussion_file "auth-flow" "auth-flow" run_kb index .workflows/auth-flow/discussion/auth-flow.md >/dev/null 2>&1 -output=$(run_kb remove --work-unit nonexistent 2>&1) -assert_eq "reports 0 removed" "true" "$(echo "$output" | grep -q 'Removed 0 chunks' && echo true || echo false)" +exit_code=0 +output=$(run_kb remove --work-unit nonexistent 2>&1) || exit_code=$? +assert_eq "exits non-zero on unknown wu" "true" "$([ "$exit_code" -ne 0 ] && echo true || echo false)" +assert_eq "error names the wu" "true" \ + "$(echo "$output" | grep -q '"nonexistent"' && echo true || echo false)" +assert_eq "error mentions project manifest" "true" \ + "$(echo "$output" | grep -q 'project manifest' && echo true || echo false)" +# Legitimate removal of an existing wu with 0 matching chunks still succeeds. +exit_code=0 +output=$(run_kb remove --work-unit auth-flow --phase research --topic nothing 2>&1) || exit_code=$? +assert_eq "wu-exists + no-match still succeeds" "0" "$exit_code" +assert_eq "reports 0 removed on real miss" "true" \ + "$(echo "$output" | grep -q 'Removed 0 chunks' && echo true || echo false)" teardown_project # --- Test 42: Remove errors when --topic given without --phase --- @@ -768,10 +1020,12 @@ assert_eq "exits 1" "1" "$exit_code" assert_eq "shows usage" "true" "$(echo "$output" | grep -q 'Usage:' && echo true || echo false)" teardown_project -# --- Test 44: Remove from empty store reports 0 --- +# --- Test 44: Remove from empty/nonexistent store reports 0 (WU exists) --- echo "Test 44: Remove from empty/nonexistent store" setup_project +create_work_unit "auth-flow" "feature" "Auth" write_stub_config +# WU exists in registry but no store yet — should cleanly no-op. output=$(run_kb remove --work-unit auth-flow 2>&1) assert_eq "reports 0 removed" "true" "$(echo "$output" | grep -q 'Removed 0 chunks' && echo true || echo false)" teardown_project @@ -1381,6 +1635,224 @@ status_output=$(run_kb status 2>&1) assert_eq "bulk index skipped cancelled wu" "true" "$(echo "$status_output" | grep -q 'cancelled-wu' && echo false || echo true)" teardown_project +# --- Test 81: pending-removal queue survives writes + drain works --- +echo "Test 81: Pending removal queue" +setup_project +create_work_unit "drop-me" "feature" "Drop" +write_stub_config +create_discussion_file "drop-me" "drop-me" +run_kb index .workflows/drop-me/discussion/drop-me.md >/dev/null 2>&1 +meta="$TEST_ROOT/.workflows/.knowledge/metadata.json" +# Seed a pending removal (simulates a prior failure). +node -e " +const fs=require('fs'); +const m=JSON.parse(fs.readFileSync('$meta','utf8')); +m.pending_removals=[{workUnit:'stale-wu',phase:null,topic:null,queued_at:new Date().toISOString(),error:'seeded',attempts:1}]; +fs.writeFileSync('$meta', JSON.stringify(m)); +" +# Status surfaces pending removal. +status_output=$(run_kb status 2>&1) +assert_eq "status shows pending removal" "true" "$(echo "$status_output" | grep -q 'Pending removals: 1' && echo true || echo false)" +# Part A — a metadata-mutating operation (index) must PRESERVE the queue, not strip it. +# This is the direct regression guard for the writeMetadata-whitelist bug that +# shipped the pending-removal feature broken. Indexing writes metadata; if +# pending_removals is absent from the whitelist, this assertion fails. +create_work_unit "other-wu" "feature" "Other" +create_discussion_file "other-wu" "other-wu" +run_kb index .workflows/other-wu/discussion/other-wu.md >/dev/null 2>&1 +queue_after_index=$(node -e " +const m=JSON.parse(require('fs').readFileSync('$meta','utf8')); +process.stdout.write(String((m.pending_removals||[]).length)); +") +assert_eq "pending_removals survives an index write" "1" "$queue_after_index" +# Part B — a normal remove call drains the queue on the no-op success path +# (stale-wu has no chunks; performRemoval returns 0 chunks, processPendingRemovals +# then removes the entry). Pairs with Parts C and D below which exercise the +# real-failure paths. +run_kb remove --work-unit drop-me >/dev/null 2>&1 +queue_after_remove=$(node -e " +const m=JSON.parse(require('fs').readFileSync('$meta','utf8')); +process.stdout.write(String((m.pending_removals||[]).length)); +") +assert_eq "queue drains on no-op success" "0" "$queue_after_remove" +teardown_project + +# --- Test 81b: pending-removal queue evicts after REMOVAL_MAX_ATTEMPTS --- +# Guards processPendingRemovals' eviction branch — without a real-failure +# test, all observed behaviour was on the no-op success path above. +echo "Test 81b: Pending removal eviction" +setup_project +create_work_unit "drop-me" "feature" "Drop" +write_stub_config +create_discussion_file "drop-me" "drop-me" +run_kb index .workflows/drop-me/discussion/drop-me.md >/dev/null 2>&1 +meta="$TEST_ROOT/.workflows/.knowledge/metadata.json" +# Seed a pending removal already at the eviction threshold. +node -e " +const fs=require('fs'); +const m=JSON.parse(fs.readFileSync('$meta','utf8')); +m.pending_removals=[{workUnit:'capped-wu',phase:null,topic:null,queued_at:new Date().toISOString(),error:'simulated permanent failure',attempts:10}]; +fs.writeFileSync('$meta', JSON.stringify(m)); +" +# Triggering processPendingRemovals (any command that calls it works). +evict_stderr=$(run_kb remove --work-unit drop-me 2>&1 >/dev/null) +queue_after_evict=$(node -e " +const m=JSON.parse(require('fs').readFileSync('$meta','utf8')); +process.stdout.write(String((m.pending_removals||[]).length)); +") +assert_eq "queue empty after eviction at MAX_ATTEMPTS" "0" "$queue_after_evict" +assert_eq "eviction surfaces stderr notice" "true" "$(echo "$evict_stderr" | grep -q 'exceeded 10 attempts.*evicting' && echo true || echo false)" +teardown_project + +# --- Test 81c: pending-removal failure increments attempts --- +# Guards the addPendingRemoval bump in processPendingRemovals' catch branch. +# Simulates a real failure by corrupting store.msp before the drain runs. +echo "Test 81c: Pending removal attempts increment on failure" +setup_project +create_work_unit "drop-me" "feature" "Drop" +write_stub_config +create_discussion_file "drop-me" "drop-me" +run_kb index .workflows/drop-me/discussion/drop-me.md >/dev/null 2>&1 +meta="$TEST_ROOT/.workflows/.knowledge/metadata.json" +store_msp="$TEST_ROOT/.workflows/.knowledge/store.msp" +# Seed a pending removal mid-attempts and corrupt the store so loadStore throws. +node -e " +const fs=require('fs'); +const m=JSON.parse(fs.readFileSync('$meta','utf8')); +m.pending_removals=[{workUnit:'flaky-wu',phase:null,topic:null,queued_at:new Date().toISOString(),error:'prior failure',attempts:5}]; +fs.writeFileSync('$meta', JSON.stringify(m)); +" +# Backup and corrupt the store. +cp "$store_msp" "$store_msp.bak" +printf 'corrupt-msgpack-bytes' > "$store_msp" +# Trigger processPendingRemovals — this will fail and bump attempts. +run_kb compact >/dev/null 2>&1 || true +attempts_after=$(node -e " +const m=JSON.parse(require('fs').readFileSync('$meta','utf8')); +const r=(m.pending_removals||[]).find(r=>r.workUnit==='flaky-wu'); +process.stdout.write(String(r ? r.attempts : 'evicted')); +") +# Restore the store (so teardown_project can run cleanly if needed). +mv "$store_msp.bak" "$store_msp" +assert_eq "attempts incremented from 5 to 6 after real failure" "6" "$attempts_after" +teardown_project + +# --- Test 82: Rebuild cleans up .bak files on success and on leftover --- +echo "Test 82: Rebuild backup handling" +setup_project +create_work_unit "wu-a" "feature" "A" +write_stub_config +create_discussion_file "wu-a" "wu-a" +cd "$TEST_ROOT" && node "$MANIFEST_JS" init-phase wu-a.discussion.wu-a >/dev/null 2>&1 +cd "$TEST_ROOT" && node "$MANIFEST_JS" set wu-a.discussion.wu-a status completed >/dev/null 2>&1 +run_kb index .workflows/wu-a/discussion/wu-a.md >/dev/null 2>&1 +# Simulate a leftover .bak from a prior aborted rebuild. +touch "$TEST_ROOT/.workflows/.knowledge/store.msp.bak" +touch "$TEST_ROOT/.workflows/.knowledge/metadata.json.bak" +echo "rebuild" | run_kb rebuild >/dev/null 2>&1 +assert_eq "leftover .bak cleaned after successful rebuild" "true" \ + "$([ ! -f "$TEST_ROOT/.workflows/.knowledge/store.msp.bak" ] && [ ! -f "$TEST_ROOT/.workflows/.knowledge/metadata.json.bak" ] && echo true || echo false)" +assert_eq "store still present after rebuild" "true" \ + "$([ -f "$TEST_ROOT/.workflows/.knowledge/store.msp" ] && echo true || echo false)" +teardown_project + +# --- Test 84: Stranded-chunks orphan cleanup --- +# When the registry has no entry for a work unit but the store has +# chunks for it (post-absorption / manual mutation), `knowledge remove +# --work-unit ` should clean the chunks rather than dead-end on +# the typo error. +echo "Test 84: Stranded-chunks orphan cleanup" +setup_project +create_work_unit "absorbed-wu" "feature" "Absorbed" +write_stub_config +create_discussion_file "absorbed-wu" "absorbed-wu" +run_kb index .workflows/absorbed-wu/discussion/absorbed-wu.md >/dev/null 2>&1 +# Simulate post-absorption state: registry entry deleted, chunks linger. +project_manifest="$TEST_ROOT/.workflows/manifest.json" +node -e " +const fs=require('fs'); +const m=JSON.parse(fs.readFileSync('$project_manifest','utf8')); +if (m.work_units) delete m.work_units['absorbed-wu']; +delete m['absorbed-wu']; +fs.writeFileSync('$project_manifest', JSON.stringify(m, null, 2)); +" +exit_code=0 +output=$(run_kb remove --work-unit absorbed-wu 2>&1) || exit_code=$? +assert_eq "orphan cleanup succeeds (exit 0)" "0" "$exit_code" +assert_eq "stderr surfaces orphan-cleanup notice" "true" \ + "$(echo "$output" | grep -q 'orphan cleanup' && echo true || echo false)" +assert_eq "removed chunks reported" "true" \ + "$(echo "$output" | grep -qE 'Removed [1-9].* chunks' && echo true || echo false)" +# Subsequent run must surface the typo error (registry empty, store empty). +exit_code=0 +output=$(run_kb remove --work-unit absorbed-wu 2>&1) || exit_code=$? +assert_eq "subsequent typo case rejects" "true" "$([ "$exit_code" -ne 0 ] && echo true || echo false)" +assert_eq "subsequent error mentions no matching chunks" "true" \ + "$(echo "$output" | grep -q 'no matching chunks exist' && echo true || echo false)" +teardown_project + +# --- Test 85: Setup aborts on store-without-metadata partial state --- +# The inverse of #7 — when store.msp exists but metadata.json is missing, +# writing fresh metadata against an unknown store would create a +# provider/dimensions mismatch we cannot detect from the store alone. +# Setup must abort with rebuild advice instead. +echo "Test 85: Setup aborts on store-without-metadata" +setup_project +write_stub_config +# Seed a store but not metadata. +create_work_unit "seed-wu" "feature" "Seed" +create_discussion_file "seed-wu" "seed-wu" +run_kb index .workflows/seed-wu/discussion/seed-wu.md >/dev/null 2>&1 +rm "$TEST_ROOT/.workflows/.knowledge/metadata.json" +exit_code=0 +# Pipe input "n" so reconfigure prompt (if reached) declines — but we expect +# abort before that. setup is a TTY wizard, but here we just need the project +# init step to surface the partial-state guard. Run via a wrapper that fakes +# a TTY by redirecting stdin from /dev/tty is overkill; instead invoke the +# wrapper script that runs runProjectInitStep directly via node -e. +# The setup CLI requires a TTY; invoke runProjectInitStep directly so +# the partial-state guard fires without needing the readline wizard. +output=$(cd "$TEST_ROOT" && node -e " +const setup = require('$BUNDLE').setup; +(async () => { + try { + await setup.runProjectInitStep({ question: () => '', close: () => {} }); + console.log('UNEXPECTED_SUCCESS'); + } catch (e) { + console.log('THREW:', e.message); + } +})(); +" 2>&1) || true +assert_eq "setup partial-state guard surfaces inconsistent-state error" "true" \ + "$(echo "$output" | grep -q 'inconsistent state' && echo true || echo false)" +assert_eq "setup partial-state guard mentions knowledge rebuild" "true" \ + "$(echo "$output" | grep -q 'knowledge rebuild' && echo true || echo false)" +teardown_project + +# --- Test 83: Subdirectory invocation finds project root --- +# Pre-fix, knowledgeDir() / orphan check / manifest reads anchored at +# process.cwd(). Running `knowledge status` from a subdirectory of the +# project marked every chunk as orphaned (and broke other commands). +# After the findProjectRoot walk-up, KB commands work from any +# subdirectory of a project. +echo "Test 83: Subdirectory invocation" +setup_project +create_work_unit "subdir-wu" "feature" "Subdir" +write_stub_config +create_discussion_file "subdir-wu" "subdir-wu" +cd "$TEST_ROOT" && node "$MANIFEST_JS" init-phase subdir-wu.discussion.subdir-wu >/dev/null 2>&1 +run_kb index .workflows/subdir-wu/discussion/subdir-wu.md >/dev/null 2>&1 +# Now invoke status from a deeply nested subdirectory of the project. +mkdir -p "$TEST_ROOT/.workflows/subdir-wu/discussion" +cd "$TEST_ROOT/.workflows/subdir-wu/discussion" +status_from_subdir=$(node "$BUNDLE" status 2>&1) +cd "$TEST_ROOT" +assert_eq "status from subdir reports zero orphans" "true" \ + "$(echo "$status_from_subdir" | grep -q 'Orphaned chunks' && echo false || echo true)" +assert_eq "status from subdir reports the indexed chunks" "true" \ + "$(echo "$status_from_subdir" | grep -qE 'Total chunks: [1-9]' && echo true || echo false)" +teardown_project + # --- Summary --- echo "" echo "Results: $PASS passed, $FAIL failed" diff --git a/tests/scripts/test-knowledge-openai.cjs b/tests/scripts/test-knowledge-openai.cjs index d9e701df8..cd3f2e87d 100644 --- a/tests/scripts/test-knowledge-openai.cjs +++ b/tests/scripts/test-knowledge-openai.cjs @@ -5,6 +5,7 @@ const assert = require('node:assert'); const { OpenAIProvider, + AuthError, DEFAULT_MODEL, DEFAULT_DIMENSIONS, } = require('../../src/knowledge/providers/openai'); @@ -85,13 +86,27 @@ describe('OpenAIProvider embed (mocked)', () => { assert.deepStrictEqual(result, fakeVector); }); - it('throws on 401 with descriptive message', async () => { + it('throws AuthError on 401 with descriptive message and recovery hint', async () => { globalThis.fetch = mockFetchError(401, 'Unauthorized'); const p = new OpenAIProvider({ apiKey: 'sk-bad' }); await assert.rejects( () => p.embed('hello'), - /OpenAI API key is invalid or expired/ + (err) => + err instanceof AuthError && + /invalid or expired/.test(err.message) && + /knowledge setup/.test(err.message) + ); + }); + + it('throws AuthError on 403 with recovery hint', async () => { + globalThis.fetch = mockFetchError(403, 'Forbidden'); + const p = new OpenAIProvider({ apiKey: 'sk-test' }); + + await assert.rejects( + () => p.embed('hello'), + (err) => + err instanceof AuthError && /403/.test(err.message) && /knowledge setup/.test(err.message) ); }); @@ -193,6 +208,31 @@ describe('OpenAIProvider embedBatch (mocked)', () => { const result = await p.embedBatch(['single']); assert.deepStrictEqual(result, [vec]); }); + + it('throws on short response (fewer rows than requested)', async () => { + // API returned 2 rows for a 3-item request. Previously: results[2] + // stayed undefined and propagated silently into the store. + globalThis.fetch = mockFetchSuccess({ + data: [ + { index: 0, embedding: [0.1, 0.2] }, + { index: 1, embedding: [0.3, 0.4] }, + ], + }); + const p = new OpenAIProvider({ apiKey: 'sk-test', dimensions: 2 }); + await assert.rejects( + () => p.embedBatch(['a', 'b', 'c']), + /response length mismatch.*requested 3, received 2/ + ); + }); + + it('throws on missing data array', async () => { + globalThis.fetch = mockFetchSuccess({ data: null }); + const p = new OpenAIProvider({ apiKey: 'sk-test', dimensions: 2 }); + await assert.rejects( + () => p.embedBatch(['a']), + /response length mismatch/ + ); + }); }); // --------------------------------------------------------------------------- diff --git a/tests/scripts/test-knowledge-retry.cjs b/tests/scripts/test-knowledge-retry.cjs index 63a008c66..ac518b55d 100644 --- a/tests/scripts/test-knowledge-retry.cjs +++ b/tests/scripts/test-knowledge-retry.cjs @@ -3,7 +3,7 @@ const { describe, it } = require('node:test'); const assert = require('node:assert'); -const { withRetry } = require('../../src/knowledge/index'); +const { withRetry, UserError, AuthError } = require('../../src/knowledge/index'); describe('withRetry', () => { it('succeeds on first attempt', async () => { @@ -69,4 +69,63 @@ describe('withRetry', () => { assert.strictEqual(result, 42); assert.strictEqual(calls, 1); }); + + // Permanent-failure short-circuit: programming errors and UserError surface + // immediately on the first attempt rather than burning the retry budget. + // Each test asserts exactly one call (no retries) and that the original + // error class survives. + + it('does not retry TypeError', async () => { + let calls = 0; + await assert.rejects( + () => withRetry(async () => { calls++; throw new TypeError('typo'); }, { maxAttempts: 3, backoff: [1, 1, 1] }), + (err) => err instanceof TypeError && /typo/.test(err.message) + ); + assert.strictEqual(calls, 1); + }); + + it('does not retry ReferenceError', async () => { + let calls = 0; + await assert.rejects( + () => withRetry(async () => { calls++; throw new ReferenceError('missing'); }, { maxAttempts: 3, backoff: [1, 1, 1] }), + (err) => err instanceof ReferenceError + ); + assert.strictEqual(calls, 1); + }); + + it('does not retry SyntaxError', async () => { + let calls = 0; + await assert.rejects( + () => withRetry(async () => { calls++; throw new SyntaxError('bad'); }, { maxAttempts: 3, backoff: [1, 1, 1] }), + (err) => err instanceof SyntaxError + ); + assert.strictEqual(calls, 1); + }); + + it('does not retry RangeError', async () => { + let calls = 0; + await assert.rejects( + () => withRetry(async () => { calls++; throw new RangeError('out'); }, { maxAttempts: 3, backoff: [1, 1, 1] }), + (err) => err instanceof RangeError + ); + assert.strictEqual(calls, 1); + }); + + it('does not retry UserError', async () => { + let calls = 0; + await assert.rejects( + () => withRetry(async () => { calls++; throw new UserError('bad input'); }, { maxAttempts: 3, backoff: [1, 1, 1] }), + (err) => err instanceof UserError && /bad input/.test(err.message) + ); + assert.strictEqual(calls, 1); + }); + + it('does not retry AuthError', async () => { + let calls = 0; + await assert.rejects( + () => withRetry(async () => { calls++; throw new AuthError('bad key'); }, { maxAttempts: 3, backoff: [1, 1, 1] }), + (err) => err instanceof AuthError && /bad key/.test(err.message) + ); + assert.strictEqual(calls, 1); + }); }); diff --git a/tests/scripts/test-knowledge-store.cjs b/tests/scripts/test-knowledge-store.cjs index c99d96b88..940e039d4 100644 --- a/tests/scripts/test-knowledge-store.cjs +++ b/tests/scripts/test-knowledge-store.cjs @@ -10,7 +10,10 @@ const { createStore, insertDocument, removeByIdentity, + removeByFilter, + countByFilter, searchFulltext, + searchAllFulltext, searchVector, searchHybrid, saveStore, @@ -337,6 +340,42 @@ describe('knowledge store', () => { }); assert.deepStrictEqual(hits, []); }); + + // Pin two contracts the previous `limit: 100000` cap silently relied on: + // 1. Whole-store enumeration (searchAllFulltext) must paginate so it + // returns every doc regardless of count. + // 2. Filtered single-shot reads (countByFilter, removeByFilter) must + // surface every match, not stop at an internal cap. + // Seeding 2500 docs forces both to handle counts above any single + // Orama batch (and above the previous 100k cap if Orama ever changes + // its internal page defaults). + it('whole-store enumeration and filtered reads return every match (no truncation)', async () => { + const db = await createStore(STUB_DIMS); + const TOTAL = 2500; // > ENUMERATION_PAGE_SIZE (1000) — forces multiple pages + for (let i = 0; i < TOTAL; i++) { + await insertDocument(db, makeDoc({ + id: `page-${i}`, + work_unit: i % 2 === 0 ? 'wu-even' : 'wu-odd', + })); + } + + // 1. Paginated whole-store enumeration returns every document. + const all = await searchAllFulltext(db); + assert.strictEqual(all.length, TOTAL, 'searchAllFulltext returns every document'); + const allIds = new Set(all.map((h) => h.id)); + assert.strictEqual(allIds.size, TOTAL, 'no duplicate IDs across paginated results'); + + // 2. Filtered reads via Orama's where clause return every match. + const evenCount = await countByFilter(db, { work_unit: { eq: 'wu-even' } }); + assert.strictEqual(evenCount, TOTAL / 2); + + // 3. removeByFilter removes every match (including the > 1000 batch case + // that exposed Orama's sync removeMultiple count bug). + const removed = await removeByFilter(db, { work_unit: { eq: 'wu-odd' } }); + assert.strictEqual(removed, TOTAL / 2); + const remaining = await searchAllFulltext(db); + assert.strictEqual(remaining.length, TOTAL / 2); + }); }); // --------------------------------------------------------------------------- diff --git a/tests/scripts/test-migration-001.sh b/tests/scripts/test-migration-001.sh index 56ce2101d..e5287fd88 100755 --- a/tests/scripts/test-migration-001.sh +++ b/tests/scripts/test-migration-001.sh @@ -2,7 +2,7 @@ # Tests for migration 001: discussion-frontmatter # Run: bash tests/scripts/test-migration-001.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-002.sh b/tests/scripts/test-migration-002.sh index e2055c14b..6a646b6c0 100755 --- a/tests/scripts/test-migration-002.sh +++ b/tests/scripts/test-migration-002.sh @@ -2,7 +2,7 @@ # Tests for migration 002: specification-frontmatter # Run: bash tests/scripts/test-migration-002.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-003.sh b/tests/scripts/test-migration-003.sh index afc8beb05..7887598e6 100755 --- a/tests/scripts/test-migration-003.sh +++ b/tests/scripts/test-migration-003.sh @@ -2,7 +2,7 @@ # Tests for migration 003: planning-frontmatter # Run: bash tests/scripts/test-migration-003.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-004.sh b/tests/scripts/test-migration-004.sh index 230d3a18f..142a3d50d 100755 --- a/tests/scripts/test-migration-004.sh +++ b/tests/scripts/test-migration-004.sh @@ -2,7 +2,7 @@ # Tests for migration 004: sources-object-format # Run: bash tests/scripts/test-migration-004.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-005.sh b/tests/scripts/test-migration-005.sh index 34e8015b8..6bed19c1f 100755 --- a/tests/scripts/test-migration-005.sh +++ b/tests/scripts/test-migration-005.sh @@ -2,7 +2,7 @@ # Tests for migration 005: plan-external-deps-frontmatter # Run: bash tests/scripts/test-migration-005.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-006.sh b/tests/scripts/test-migration-006.sh index 3371b0cc3..cf41a5f4a 100755 --- a/tests/scripts/test-migration-006.sh +++ b/tests/scripts/test-migration-006.sh @@ -2,7 +2,7 @@ # Tests for migration 006: directory-restructure # Run: bash tests/scripts/test-migration-006.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-007.sh b/tests/scripts/test-migration-007.sh index 5fd827271..8c1fa6da0 100755 --- a/tests/scripts/test-migration-007.sh +++ b/tests/scripts/test-migration-007.sh @@ -2,7 +2,7 @@ # Tests for migration 007: tasks-subdirectory # Run: bash tests/scripts/test-migration-007.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-008.sh b/tests/scripts/test-migration-008.sh index a860acd5e..72afb417e 100755 --- a/tests/scripts/test-migration-008.sh +++ b/tests/scripts/test-migration-008.sh @@ -2,7 +2,7 @@ # Tests for migration 008: review-directory-structure # Run: bash tests/scripts/test-migration-008.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-009.sh b/tests/scripts/test-migration-009.sh index cfde7398e..1f9183370 100755 --- a/tests/scripts/test-migration-009.sh +++ b/tests/scripts/test-migration-009.sh @@ -2,7 +2,7 @@ # Tests for migration 009: review-per-plan-storage # Run: bash tests/scripts/test-migration-009.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-010.sh b/tests/scripts/test-migration-010.sh index 0e13e982c..b1d3d02ea 100755 --- a/tests/scripts/test-migration-010.sh +++ b/tests/scripts/test-migration-010.sh @@ -2,7 +2,7 @@ # Tests for migration 010: gitignore-sessions # Run: bash tests/scripts/test-migration-010.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-011.sh b/tests/scripts/test-migration-011.sh index 1ba4f987c..6786e4218 100755 --- a/tests/scripts/test-migration-011.sh +++ b/tests/scripts/test-migration-011.sh @@ -2,7 +2,7 @@ # Tests for migration 011: rename-workflow-directory # Run: bash tests/scripts/test-migration-011.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-012.sh b/tests/scripts/test-migration-012.sh index bc296b769..dc9d690e5 100755 --- a/tests/scripts/test-migration-012.sh +++ b/tests/scripts/test-migration-012.sh @@ -2,7 +2,7 @@ # Tests for migration 012: environment-setup-to-state # Run: bash tests/scripts/test-migration-012.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-013.sh b/tests/scripts/test-migration-013.sh index 4a4979c0d..1ed06b90f 100644 --- a/tests/scripts/test-migration-013.sh +++ b/tests/scripts/test-migration-013.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-013.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-014.sh b/tests/scripts/test-migration-014.sh index d677543b0..6dfa46da7 100644 --- a/tests/scripts/test-migration-014.sh +++ b/tests/scripts/test-migration-014.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-014.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-015.sh b/tests/scripts/test-migration-015.sh index 00085e38e..3fedd131e 100644 --- a/tests/scripts/test-migration-015.sh +++ b/tests/scripts/test-migration-015.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-015.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-016.sh b/tests/scripts/test-migration-016.sh index 9807a9d1b..a6c867a97 100755 --- a/tests/scripts/test-migration-016.sh +++ b/tests/scripts/test-migration-016.sh @@ -2,7 +2,7 @@ # Tests for migration 016: work-unit-restructure # Run: bash tests/scripts/test-migration-016.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-017.sh b/tests/scripts/test-migration-017.sh index 9c879dc09..7f39ccf2e 100755 --- a/tests/scripts/test-migration-017.sh +++ b/tests/scripts/test-migration-017.sh @@ -2,7 +2,7 @@ # Tests for migration 017: external-deps-object # Run: bash tests/scripts/test-migration-017.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-018.sh b/tests/scripts/test-migration-018.sh index 53703969f..78eb712ae 100755 --- a/tests/scripts/test-migration-018.sh +++ b/tests/scripts/test-migration-018.sh @@ -2,7 +2,7 @@ # Tests for migration 018: remove-stale-environment-setup # Run: bash tests/scripts/test-migration-018.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-019.sh b/tests/scripts/test-migration-019.sh index 3ae13186c..df35cff70 100755 --- a/tests/scripts/test-migration-019.sh +++ b/tests/scripts/test-migration-019.sh @@ -2,7 +2,7 @@ # Tests for migration 019: status-rename # Run: bash tests/scripts/test-migration-019.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-020.sh b/tests/scripts/test-migration-020.sh index 87d6bf32d..aa4972ad3 100755 --- a/tests/scripts/test-migration-020.sh +++ b/tests/scripts/test-migration-020.sh @@ -2,7 +2,7 @@ # Tests for migration 020: normalise-terminal-status # Run: bash tests/scripts/test-migration-020.sh -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-021.sh b/tests/scripts/test-migration-021.sh index 628a55fe5..9696421f6 100644 --- a/tests/scripts/test-migration-021.sh +++ b/tests/scripts/test-migration-021.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-021.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-022.sh b/tests/scripts/test-migration-022.sh index 564a0241d..3085f2edd 100644 --- a/tests/scripts/test-migration-022.sh +++ b/tests/scripts/test-migration-022.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-022.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-023.sh b/tests/scripts/test-migration-023.sh index 533bb333d..7ab6ca30b 100755 --- a/tests/scripts/test-migration-023.sh +++ b/tests/scripts/test-migration-023.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-023.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-024.sh b/tests/scripts/test-migration-024.sh index a67f423b1..f6b401294 100755 --- a/tests/scripts/test-migration-024.sh +++ b/tests/scripts/test-migration-024.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-024.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-025.sh b/tests/scripts/test-migration-025.sh index 59b206c0c..166cd8feb 100644 --- a/tests/scripts/test-migration-025.sh +++ b/tests/scripts/test-migration-025.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-025.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-026.sh b/tests/scripts/test-migration-026.sh index c7e63cc1a..ae6c97a4d 100755 --- a/tests/scripts/test-migration-026.sh +++ b/tests/scripts/test-migration-026.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-026.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-027.sh b/tests/scripts/test-migration-027.sh index d506a478b..b504552bc 100755 --- a/tests/scripts/test-migration-027.sh +++ b/tests/scripts/test-migration-027.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-027.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-028.sh b/tests/scripts/test-migration-028.sh index 62f612a5f..f861bdb06 100644 --- a/tests/scripts/test-migration-028.sh +++ b/tests/scripts/test-migration-028.sh @@ -5,7 +5,7 @@ # Run: bash tests/scripts/test-migration-028.sh # -set -eo pipefail +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" diff --git a/tests/scripts/test-migration-037.sh b/tests/scripts/test-migration-037.sh index 900cb7634..2629558ae 100644 --- a/tests/scripts/test-migration-037.sh +++ b/tests/scripts/test-migration-037.sh @@ -14,8 +14,8 @@ MIGRATION="$REPO_DIR/skills/workflow-migrate/scripts/migrations/037-completed-at PASS=0 FAIL=0 -report_update() { : ; } -report_skip() { : ; } +report_update() { REPORT_CALLED=update; } +report_skip() { REPORT_CALLED=skip; } assert_eq() { local label="$1" expected="$2" actual="$3" @@ -33,6 +33,7 @@ setup() { TEST_DIR=$(mktemp -d /tmp/migration-037-test.XXXXXX) export PROJECT_DIR="$TEST_DIR" mkdir -p "$TEST_DIR/.workflows" + REPORT_CALLED="" } teardown() { @@ -301,6 +302,65 @@ JSON teardown } +# --- Test 10: Counter accuracy — update path calls report_update --- +test_counter_reports_update_when_modified() { + setup + + mkdir -p "$TEST_DIR/.workflows/needs-backfill/discussion" + cat > "$TEST_DIR/.workflows/needs-backfill/manifest.json" <<'JSON' +{ + "name": "needs-backfill", + "work_type": "feature", + "status": "completed", + "phases": {} +} +JSON + echo "content" > "$TEST_DIR/.workflows/needs-backfill/discussion/topic.md" + touch -t 202501150930 "$TEST_DIR/.workflows/needs-backfill/discussion/topic.md" + + source "$MIGRATION" + + assert_eq "report_update called when modified" "update" "$REPORT_CALLED" + + teardown +} + +# --- Test 11: Counter accuracy — no-op path calls report_skip --- +test_counter_reports_skip_when_nothing_to_do() { + setup + + # Only an already-backfilled WU exists — migration has nothing to do. + mkdir -p "$TEST_DIR/.workflows/already-done/discussion" + cat > "$TEST_DIR/.workflows/already-done/manifest.json" <<'JSON' +{ + "name": "already-done", + "work_type": "feature", + "status": "completed", + "completed_at": "2025-03-01", + "phases": {} +} +JSON + echo "content" > "$TEST_DIR/.workflows/already-done/discussion/topic.md" + + source "$MIGRATION" + + assert_eq "report_skip called when nothing modified" "skip" "$REPORT_CALLED" + + teardown +} + +# --- Test 12: Counter accuracy — empty workflows dir calls report_skip --- +test_counter_reports_skip_on_empty_workflows() { + setup + + # No work units at all. + source "$MIGRATION" + + assert_eq "report_skip on empty workflows" "skip" "$REPORT_CALLED" + + teardown +} + # --- Run all tests --- echo "Running migration 037 tests..." echo "" @@ -314,6 +374,9 @@ test_multiple test_content_preservation test_no_workflows test_empty_work_unit +test_counter_reports_update_when_modified +test_counter_reports_skip_when_nothing_to_do +test_counter_reports_skip_on_empty_workflows echo "" echo "Results: $PASS passed, $FAIL failed" diff --git a/tests/scripts/test-release-build.sh b/tests/scripts/test-release-build.sh new file mode 100755 index 000000000..a3a9d7097 --- /dev/null +++ b/tests/scripts/test-release-build.sh @@ -0,0 +1,296 @@ +#!/bin/bash +# Tests for the release-script build integration (knowledge-base phase 8, task 8-1). +# Run: bash tests/scripts/test-release-build.sh + +set -eo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)" +RELEASE_SCRIPT="$REPO_DIR/release" + +PASS=0 +FAIL=0 + +assert_eq() { + local label="$1" expected="$2" actual="$3" + if [ "$expected" = "$actual" ]; then + PASS=$((PASS + 1)) + else + FAIL=$((FAIL + 1)) + echo "FAIL: $label" + echo " expected: $expected" + echo " actual: $actual" + fi +} + +# Piping `git log ... | grep -q` under `set -o pipefail` causes SIGPIPE (141) +# when grep closes its stdin early. Capture first, then match. +file_contains() { + local pattern="$1" file="$2" + local content + content=$(cat "$file" 2>/dev/null || true) + case "$content" in + *"$pattern"*) echo true ;; + *) echo false ;; + esac +} + +git_log_contains() { + local pattern="$1" + local log + log=$(cd "$REPO" && git log --oneline 2>/dev/null || true) + case "$log" in + *"$pattern"*) echo true ;; + *) echo false ;; + esac +} + +# Create a self-contained throwaway git repo with a committed bundle, +# npm/git stubs, and a function-only copy of the real release script +# (main "$@" stripped so sourcing does not execute a release). +setup() { + TEST_DIR=$(mktemp -d "${TMPDIR:-/tmp}/release-build-test.XXXXXX") + REPO="$TEST_DIR/repo" + STUBS="$TEST_DIR/stubs" + CALLS="$TEST_DIR/calls.log" + RELEASE_FUNCS="$TEST_DIR/release-funcs.sh" + mkdir -p "$REPO" "$STUBS" + + cd "$REPO" + git init -q + git config user.email "test@example.com" + git config user.name "Test" + git config commit.gpgsign false + + mkdir -p skills/workflow-knowledge/scripts + echo "// initial bundle" > skills/workflow-knowledge/scripts/knowledge.cjs + echo '{"name":"stub","version":"0.0.0"}' > package.json + echo "0.0.0" > release.txt + git add . + git commit -q -m "initial" + + # Stub npm: install is a no-op; build optionally fails or mutates the bundle + # based on env vars set by the individual test. + cat > "$STUBS/npm" << 'NPMEOF' +#!/bin/bash +echo "npm $*" >> "$CALLS" +case "$1" in + ci) + if [ "${STUB_NPM_CI_FAIL:-0}" = "1" ]; then + echo "stub: npm ci failed" >&2 + exit 1 + fi + exit 0 + ;; + run) + if [ "$2" = "build" ]; then + if [ "${STUB_NPM_BUILD_FAIL:-0}" = "1" ]; then + echo "stub: npm run build failed" >&2 + exit 1 + fi + if [ "${STUB_NPM_BUILD_MODIFIES:-0}" = "1" ]; then + echo "// rebuilt by stub $(date +%s%N)" > skills/workflow-knowledge/scripts/knowledge.cjs + fi + exit 0 + fi + exit 0 + ;; +esac +exit 0 +NPMEOF + chmod +x "$STUBS/npm" + + # Strip the main invocation so sourcing defines functions without running. + grep -v '^main "\$@"$' "$RELEASE_SCRIPT" > "$RELEASE_FUNCS" + + export PATH="$STUBS:$PATH" + export CALLS +} + +teardown() { + cd "$REPO_DIR" + rm -rf "$TEST_DIR" + unset STUB_NPM_CI_FAIL STUB_NPM_BUILD_FAIL STUB_NPM_BUILD_MODIFIES +} + +# Run perform_release in a subshell with git tag/push stubbed so tests never +# tag or push. git add/commit pass through to the real repo so we can observe +# whether a bundle commit lands. +run_release() { + local version="$1" current="$2" strategy="${3:-none}" + ( + cd "$REPO" + source "$RELEASE_FUNCS" + VERSION_STRATEGY="$strategy" + + update_version_file() { :; } + generate_release_commit_message() { echo "🔖 Release v$1"; } + + git() { + case "$1" in + tag|push) + echo "git $*" >> "$CALLS" + return 0 + ;; + *) + command git "$@" + ;; + esac + } + + perform_release "$version" "$current" "true" + ) > "$TEST_DIR/out.log" 2>&1 +} + +# --- Test 1: happy path — bundle changes, bundle commit lands, tag called --- +test_happy_path_bundle_changes() { + setup + export STUB_NPM_BUILD_MODIFIES=1 + + local rc=0 + run_release "1.0.0" "0.0.0" "none" || rc=$? + + assert_eq "perform_release exits 0 on clean build" "0" "$rc" + assert_eq "npm ci was invoked" "true" "$(file_contains 'npm ci' "$CALLS")" + assert_eq "npm run build was invoked" "true" "$(file_contains 'npm run build' "$CALLS")" + assert_eq "bundle commit was created" "true" "$(git_log_contains 'rebuild knowledge bundle for v1.0.0')" + assert_eq "tag was called after build" "true" "$(file_contains 'git tag' "$CALLS")" + + teardown +} + +# --- Test 2: bundle unchanged — no bundle commit --- +test_bundle_unchanged_skips_commit() { + setup + # STUB_NPM_BUILD_MODIFIES unset → build does not touch the bundle + + local rc=0 + run_release "1.0.0" "0.0.0" "none" || rc=$? + + assert_eq "perform_release exits 0 when bundle unchanged" "0" "$rc" + assert_eq "no bundle commit was created" "false" "$(git_log_contains 'rebuild knowledge bundle')" + assert_eq "tag was still called" "true" "$(file_contains 'git tag' "$CALLS")" + + teardown +} + +# --- Test 3: VERSION_STRATEGY=none still commits bundle when it changes --- +test_version_none_commits_bundle() { + setup + export STUB_NPM_BUILD_MODIFIES=1 + + local rc=0 + run_release "1.2.3" "0.0.0" "none" || rc=$? + + assert_eq "perform_release exits 0 in none strategy" "0" "$rc" + assert_eq "bundle commit occurs even with VERSION_STRATEGY=none" "true" \ + "$(git_log_contains 'rebuild knowledge bundle for v1.2.3')" + + teardown +} + +# --- Test 4: VERSION_STRATEGY=file also commits bundle when it changes --- +test_version_file_commits_bundle() { + setup + export STUB_NPM_BUILD_MODIFIES=1 + + local rc=0 + run_release "1.2.3" "0.0.0" "file" || rc=$? + + assert_eq "perform_release exits 0 in file strategy" "0" "$rc" + assert_eq "bundle commit occurs with VERSION_STRATEGY=file" "true" \ + "$(git_log_contains 'rebuild knowledge bundle for v1.2.3')" + + teardown +} + +# --- Test 5: build failure aborts before tagging --- +test_build_failure_aborts() { + setup + export STUB_NPM_BUILD_FAIL=1 + + local rc=0 + run_release "1.0.0" "0.0.0" "none" || rc=$? + + assert_eq "perform_release exits non-zero on build failure" "true" \ + "$([ "$rc" -ne 0 ] && echo true || echo false)" + assert_eq "tag was NOT called after build failure" "false" "$(file_contains 'git tag' "$CALLS")" + assert_eq "error message mentions build failure" "true" \ + "$(file_contains 'npm run build failed' "$TEST_DIR/out.log")" + + teardown +} + +# --- Test 6: npm ci failure aborts --- +test_ci_failure_aborts() { + setup + export STUB_NPM_CI_FAIL=1 + + local rc=0 + run_release "1.0.0" "0.0.0" "none" || rc=$? + + assert_eq "perform_release exits non-zero on ci failure" "true" \ + "$([ "$rc" -ne 0 ] && echo true || echo false)" + assert_eq "npm run build was NOT invoked after ci failure" "false" \ + "$(file_contains 'npm run build' "$CALLS")" + assert_eq "tag was NOT called after ci failure" "false" "$(file_contains 'git tag' "$CALLS")" + + teardown +} + +# --- Test 7: dirty working tree gate still fires before build --- +test_dirty_tree_gate_fires() { + setup + # Introduce an unrelated uncommitted change + echo "dirty" > "$REPO/README.md" + + local rc=0 + run_release "1.0.0" "0.0.0" "none" || rc=$? + + assert_eq "perform_release exits non-zero on dirty tree" "true" \ + "$([ "$rc" -ne 0 ] && echo true || echo false)" + assert_eq "dirty-tree error message emitted" "true" \ + "$(file_contains 'working directory is dirty' "$TEST_DIR/out.log")" + assert_eq "npm ci was NOT invoked when tree dirty" "false" "$(file_contains 'npm ci' "$CALLS")" + assert_eq "npm run build was NOT invoked when tree dirty" "false" "$(file_contains 'npm run build' "$CALLS")" + + teardown +} + +# --- Test 8: build runs before tag in the call ordering --- +test_build_runs_before_tag() { + setup + export STUB_NPM_BUILD_MODIFIES=1 + + run_release "1.0.0" "0.0.0" "none" + + # Read CALLS log, find line numbers via awk to avoid SIGPIPE from grep -q + local calls_content build_line tag_line + calls_content=$(cat "$CALLS") + build_line=$(echo "$calls_content" | awk '/^npm run build$/ {print NR; exit}') + tag_line=$(echo "$calls_content" | awk '/^git tag/ {print NR; exit}') + + assert_eq "both build and tag recorded" "true" \ + "$([ -n "$build_line" ] && [ -n "$tag_line" ] && echo true || echo false)" + assert_eq "build runs before tag" "true" \ + "$([ -n "$build_line" ] && [ -n "$tag_line" ] && [ "$build_line" -lt "$tag_line" ] && echo true || echo false)" + + teardown +} + +# --- Run all tests --- +echo "Running release-build integration tests..." +echo "" + +test_happy_path_bundle_changes +test_bundle_unchanged_skips_commit +test_version_none_commits_bundle +test_version_file_commits_bundle +test_build_failure_aborts +test_ci_failure_aborts +test_dirty_tree_gate_fires +test_build_runs_before_tag + +echo "" +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] || exit 1 diff --git a/tests/scripts/test-workflow-manifest.sh b/tests/scripts/test-workflow-manifest.sh index aff6fc589..f1fb18ed3 100755 --- a/tests/scripts/test-workflow-manifest.sh +++ b/tests/scripts/test-workflow-manifest.sh @@ -1192,7 +1192,7 @@ run_cli init-phase key-of-miss.planning.key-of-miss >/dev/null 2>&1 run_cli set key-of-miss.planning.key-of-miss task_map.t-1 ext-1 >/dev/null 2>&1 exit_code=$(run_cli_exit_code key-of key-of-miss.planning.key-of-miss task_map ext-notfound) -assert_equals "$exit_code" "1" "key-of errors when value not found" +assert_equals "$exit_code" "2" "key-of value-not-found exits 2 (expected miss)" echo "" @@ -1623,7 +1623,7 @@ echo -e "${YELLOW}Test: resolve errors for non-existent work unit${NC}" setup_fixture exit_code=$(run_cli_exit_code resolve nonexistent.discussion.foo) -assert_equals "$exit_code" "1" "Non-existent work unit exits 1" +assert_equals "$exit_code" "2" "Non-existent work unit exits 2 (expected miss)" echo "" @@ -1710,6 +1710,31 @@ assert_equals "$registered" "first-unit" "Manifest registers the first work unit echo "" +# ---------------------------------------------------------------------------- + +echo -e "${YELLOW}Test: exit codes distinguish expected miss (2) from real error (1)${NC}" +setup_fixture +# Missing work unit → expected miss → exit 2. +exit_code=$(run_cli_exit_code get nonexistent status) +assert_equals "$exit_code" "2" "Missing work unit → exit 2" + +run_cli init real --work-type feature --description "Real" >/dev/null 2>&1 +# Missing path inside existing manifest → expected miss → exit 2. +exit_code=$(run_cli_exit_code get real nonexistent_field) +assert_equals "$exit_code" "2" "Missing path in existing manifest → exit 2" + +# Invalid work_type → validation error → exit 1. +exit_code=$(run_cli_exit_code init bad --work-type bogus --description "x") +assert_equals "$exit_code" "1" "Invalid work-type → exit 1" + +# Corrupt manifest JSON → real error → exit 1. +mkdir -p "$TEST_DIR/.workflows/corrupt-wu" +echo "{not valid json" > "$TEST_DIR/.workflows/corrupt-wu/manifest.json" +exit_code=$(run_cli_exit_code get corrupt-wu status) +assert_equals "$exit_code" "1" "Corrupt work-unit JSON → exit 1" + +echo "" + # ============================================================================ # SUMMARY # ============================================================================