Search: hybrid keyword + semantic retrieval #59

Open
opened 2026-09-24 15:53:00 +00:00 by kayg · 21 comments
Owner

Owner decision S3 (DESIGN §32): hybrid search. Keep Tantivy BM25 and add local semantic embeddings for notes, log entries, tasks, bookmarks/clips and extracted file text, using a small English embedding model through ort (ONNX Runtime; pick a permissively licensed model, record it in a model manifest with hash/license). Vectors in sqlite-vec (derived, rebuildable), chunked by paragraph/heading with overlap. Fuse with Reciprocal Rank Fusion so exact keyword and title matches still win. Embedding runs as bounded background jobs; query-time embedding must stay within the ⌘K budget (cache query embeddings; skip semantic for 1–2 character queries). Report recall examples ("flat search in berlin" finds "apartment hunting") and latency numbers. This index is also the retrieval layer for Ask (#next issue).

Context for the owning job

  • Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§15, §18 budgets, §31, §32) first. Prior art: calternal.js docs/search.md (read-only at /home/kayg/Developer/calternal.js) — its palette UX, > command mode, ranking weights, a11y combobox pattern and recents carry over.
  • Existing code: crates/calternal-search (Tantivy index, watcher + reconcile, providers via calternal-plugin fan-out), the ⌘K registry in apps/web.
  • Owner rules: search must be ultra fast (⌘K results < 50 ms p95 at 100k items; first keystroke to first results < 16 ms for client providers); file over app (indexes are derived and rebuildable); performance first but never at the cost of finesse; calternal.js design system (Claude reviews screenshots; floating window over a dimmed + blurred background, spring motion, reduced-motion respected); never ship sample data; atomic commits (commit every 30–45 min); adversarial testing after API work.
  • Comment on this issue when you start, on findings, when blocked, and when finished. Never close it.
Owner decision S3 (DESIGN §32): hybrid search. Keep Tantivy BM25 and add **local semantic embeddings** for notes, log entries, tasks, bookmarks/clips and extracted file text, using a small English embedding model through `ort` (ONNX Runtime; pick a permissively licensed model, record it in a model manifest with hash/license). Vectors in sqlite-vec (derived, rebuildable), chunked by paragraph/heading with overlap. Fuse with Reciprocal Rank Fusion so exact keyword and title matches still win. Embedding runs as bounded background jobs; query-time embedding must stay within the ⌘K budget (cache query embeddings; skip semantic for 1–2 character queries). Report recall examples ("flat search in berlin" finds "apartment hunting") and latency numbers. This index is also the retrieval layer for Ask (#next issue). ## Context for the owning job - Repo: kayg/calternal (~/Developer/calternal). Read CLAUDE.md, CONTEXT.md and docs/DESIGN.md (§15, §18 budgets, §31, §32) first. Prior art: calternal.js `docs/search.md` (read-only at /home/kayg/Developer/calternal.js) — its palette UX, `>` command mode, ranking weights, a11y combobox pattern and recents carry over. - Existing code: `crates/calternal-search` (Tantivy index, watcher + reconcile, providers via calternal-plugin fan-out), the ⌘K registry in apps/web. - Owner rules: search must be **ultra fast** (⌘K results < 50 ms p95 at 100k items; first keystroke to first results < 16 ms for client providers); file over app (indexes are derived and rebuildable); performance first but never at the cost of finesse; calternal.js design system (Claude reviews screenshots; floating window over a dimmed + blurred background, spring motion, reduced-motion respected); never ship sample data; atomic commits (commit every 30–45 min); adversarial testing after API work. - Comment on this issue when you start, on findings, when blocked, and when finished. Never close it.
Author
Owner

Starting search-hybrid work on branch job/search-hybrid at base 55a2752a4c. I confirmed issue #59 has no prior comments. I am reading the search design and existing search/server/API seams before implementation.

Starting search-hybrid work on branch job/search-hybrid at base 55a2752a4c4c7a087fce55868871a8576fea31a7. I confirmed issue #59 has no prior comments. I am reading the search design and existing search/server/API seams before implementation.
Author
Owner

Finding: the current server already exposes the permission-filtered GET /api/v1/search provider fan-out, and SearchHit/SearchResponse can carry fused results without a contract shape change. I verified sentence-transformers/all-MiniLM-L6-v2 is an English ONNX-capable Apache-2.0 model. I am pinning repository revision b9db1e8a0d3a51769172ba8546f282a73f066e47; the upstream ONNX SHA-256 is 6fd5d72fe4589f189f8ebc006442dbb529bb7ce38f8082112682524616046452 (90,405,214 bytes), and tokenizer.json SHA-256 is be50c3628f2bf5bb5e3a7f17b1f74611b2561a3a27eeab05e5aa30f411572037 (466,247 bytes). I will keep inference local and fall back to Tantivy if model setup fails.

Finding: the current server already exposes the permission-filtered GET /api/v1/search provider fan-out, and SearchHit/SearchResponse can carry fused results without a contract shape change. I verified sentence-transformers/all-MiniLM-L6-v2 is an English ONNX-capable Apache-2.0 model. I am pinning repository revision b9db1e8a0d3a51769172ba8546f282a73f066e47; the upstream ONNX SHA-256 is 6fd5d72fe4589f189f8ebc006442dbb529bb7ce38f8082112682524616046452 (90,405,214 bytes), and tokenizer.json SHA-256 is be50c3628f2bf5bb5e3a7f17b1f74611b2561a3a27eeab05e5aa30f411572037 (466,247 bytes). I will keep inference local and fall back to Tantivy if model setup fails.
Author
Owner

Finding: cargo check -p calternal-embed --lib fails while compiling the workspace-pinned sqlite-vec 0.1.10-alpha.4. Its packaged sqlite-vec.c includes sqlite-vec-diskann.c when SQLITE_VEC_ENABLE_DISKANN defaults to 1, but that file is absent from the published crate. I am checking a compatible published version or another supported registration path; no implementation change has been committed yet.

Finding: `cargo check -p calternal-embed --lib` fails while compiling the workspace-pinned sqlite-vec 0.1.10-alpha.4. Its packaged sqlite-vec.c includes `sqlite-vec-diskann.c` when SQLITE_VEC_ENABLE_DISKANN defaults to 1, but that file is absent from the published crate. I am checking a compatible published version or another supported registration path; no implementation change has been committed yet.
Author
Owner

Resolution for the sqlite-vec build failure: cargo info sqlite-vec@0.1.9 verified the last stable published version (MIT/Apache-2.0). Its packaged C source does not include the missing DiskANN file, and cargo check -p calternal-embed --lib succeeds with 0.1.9. The embedding crate pins 0.1.9 directly and the lockfile now records it; the workspace-wide alpha pin remains unchanged.

Resolution for the sqlite-vec build failure: `cargo info sqlite-vec@0.1.9` verified the last stable published version (MIT/Apache-2.0). Its packaged C source does not include the missing DiskANN file, and `cargo check -p calternal-embed --lib` succeeds with 0.1.9. The embedding crate pins 0.1.9 directly and the lockfile now records it; the workspace-wide alpha pin remains unchanged.
Author
Owner

Finding resolution: the first calternal-embed test showed sqlite-vec rejects auxiliary-column filters inside a KNN query, so owner filtering could not run. I changed owner_id to a sqlite-vec partition key. The regression test stores identical vectors for Alice and Bob and confirms an Alice-root query only returns Alice's row. I also capped file reads through the Root-opened handle at 1 MiB plus one byte, so a file that grows after its metadata check cannot cause an unbounded read.

Verified with CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 cargo test -p calternal-embed -p calternal-fs: 7 embed tests, 16 filesystem unit tests, 34 filesystem storage tests, and both doc-test suites passed.

Committed as f49ee68 (Add local embedding index with pinned model assets).

Finding resolution: the first `calternal-embed` test showed sqlite-vec rejects auxiliary-column filters inside a KNN query, so owner filtering could not run. I changed `owner_id` to a sqlite-vec partition key. The regression test stores identical vectors for Alice and Bob and confirms an Alice-root query only returns Alice's row. I also capped file reads through the Root-opened handle at 1 MiB plus one byte, so a file that grows after its metadata check cannot cause an unbounded read. Verified with `CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 cargo test -p calternal-embed -p calternal-fs`: 7 embed tests, 16 filesystem unit tests, 34 filesystem storage tests, and both doc-test suites passed. Committed as `f49ee68` (`Add local embedding index with pinned model assets`).
Author
Owner

Finding: semantic change events for directory moves and deletes identify the directory root, while my first indexer pass treated that path like one file. That could leave vectors for moved/deleted descendants searchable until reconciliation. The worker now scans an upserted subtree and removes deleted rows by exact path boundary; a regression test confirms removing Archive preserves Archive2. Query inference also keeps its single semaphore permit in the blocking task, so an API deadline cannot release capacity while ONNX work continues. Keyword and semantic indexing now call the same text-file policy.

Verified: SCCACHE_DISABLE=1 OPENSSL_NO_VENDOR=1 CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 cargo test -p calternal-embed passed 8 tests, with the opt-in 100k benchmark ignored. cargo test -p calternal-search passed 2 unit and 7 indexer tests, with its opt-in 100k keyword benchmark ignored.

Finding: semantic change events for directory moves and deletes identify the directory root, while my first indexer pass treated that path like one file. That could leave vectors for moved/deleted descendants searchable until reconciliation. The worker now scans an upserted subtree and removes deleted rows by exact path boundary; a regression test confirms removing `Archive` preserves `Archive2`. Query inference also keeps its single semaphore permit in the blocking task, so an API deadline cannot release capacity while ONNX work continues. Keyword and semantic indexing now call the same text-file policy. Verified: `SCCACHE_DISABLE=1 OPENSSL_NO_VENDOR=1 CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 cargo test -p calternal-embed` passed 8 tests, with the opt-in 100k benchmark ignored. `cargo test -p calternal-search` passed 2 unit and 7 indexer tests, with its opt-in 100k keyword benchmark ignored.
Author
Owner

Finding: the 1 MiB per-file cap did not bound repeated chunk context. A file with a very long title or heading could copy that text into every chunk and multiply memory use and stored title bytes. Chunk titles/headings and stored semantic titles are now capped at 256 Unicode characters; a regression test exercises a long frontmatter title and heading.

Verified with SCCACHE_DISABLE=1 OPENSSL_NO_VENDOR=1 CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 cargo test -p calternal-embed: 9 passed, 1 opt-in 100k benchmark ignored.

Finding: the 1 MiB per-file cap did not bound repeated chunk context. A file with a very long title or heading could copy that text into every chunk and multiply memory use and stored title bytes. Chunk titles/headings and stored semantic titles are now capped at 256 Unicode characters; a regression test exercises a long frontmatter title and heading. Verified with `SCCACHE_DISABLE=1 OPENSSL_NO_VENDOR=1 CARGO_PROFILE_DEV_DEBUG=line-tables-only CARGO_INCREMENTAL=0 cargo test -p calternal-embed`: 9 passed, 1 opt-in 100k benchmark ignored.
Author
Owner

Finding: the first 100k-vector benchmark missed the 50 ms target in both test and release profiles. The release measurement was inference p95 261.316 ms, exact sqlite-vec p95 734.859 ms, total p95 930.368 ms (100 uncached queries). This indicates the exact scan is too expensive at this size, and model inference also needs lower query latency. I am adding a local locality-sensitive candidate index with sqlite-vec exact reranking, then will measure again.

Finding: the first 100k-vector benchmark missed the 50 ms target in both test and release profiles. The release measurement was inference p95 261.316 ms, exact sqlite-vec p95 734.859 ms, total p95 930.368 ms (100 uncached queries). This indicates the exact scan is too expensive at this size, and model inference also needs lower query latency. I am adding a local locality-sensitive candidate index with sqlite-vec exact reranking, then will measure again.
Author
Owner

Finding from the 100k release benchmark: sqlite-vec 0.1.9 accepts rowid IN (...) as a KNN filter but still reads each vector chunk while computing KNN. With two 20-bit LSH tables and 4,096 candidates, 100 uncached queries measured inference p95 325.095 ms, sqlite-vec p95 623.252 ms, total p95 932.023 ms. I changed retrieval to use the LSH index for candidate IDs and sqlite-vec rowid point reads for exact cosine reranking. The embed crate tests pass (9 passed, 1 ignored); I am rerunning the release benchmark against that path.

Finding from the 100k release benchmark: sqlite-vec 0.1.9 accepts `rowid IN (...)` as a KNN filter but still reads each vector chunk while computing KNN. With two 20-bit LSH tables and 4,096 candidates, 100 uncached queries measured inference p95 325.095 ms, sqlite-vec p95 623.252 ms, total p95 932.023 ms. I changed retrieval to use the LSH index for candidate IDs and sqlite-vec rowid point reads for exact cosine reranking. The embed crate tests pass (9 passed, 1 ignored); I am rerunning the release benchmark against that path.
Author
Owner

Second performance finding: after changing KNN to LSH candidate selection plus sqlite-vec rowid point reads, the 100k release benchmark still did not finish its 100 samples in 96 seconds. /proc/<test-pid>/io showed rchar=128506160314 bytes at that point, so the candidate query is still causing excessive logical reads. I stopped the run and am inspecting SQLite's query plan before trusting any latency measurement.

Second performance finding: after changing KNN to LSH candidate selection plus sqlite-vec rowid point reads, the 100k release benchmark still did not finish its 100 samples in 96 seconds. `/proc/<test-pid>/io` showed `rchar=128506160314` bytes at that point, so the candidate query is still causing excessive logical reads. I stopped the run and am inspecting SQLite's query plan before trusting any latency measurement.
Author
Owner

The pinned AVX2 quantized model plus non-parallel tokenization reduced the 100k benchmark from 930.368 ms total p95 to 72.478 ms: inference p95 65.861 ms and candidate lookup/rerank p95 9.957 ms (100 uncached queries). The 128-candidate global cap also keeps vector reads bounded across multiple Homes. The benchmark still misses the 50 ms target; I am tuning ONNX thread count and will report the final measurement.

The pinned AVX2 quantized model plus non-parallel tokenization reduced the 100k benchmark from 930.368 ms total p95 to 72.478 ms: inference p95 65.861 ms and candidate lookup/rerank p95 9.957 ms (100 uncached queries). The 128-candidate global cap also keeps vector reads bounded across multiple Homes. The benchmark still misses the 50 ms target; I am tuning ONNX thread count and will report the final measurement.
Author
Owner

Latency target now passes on the release 100k-vector benchmark after using the architecture-appropriate quantized graph, avoiding single-query tokenization's Rayon setup, and setting ORT intra-op threads to 2. Across 100 uncached queries: inference p95 36.382 ms, LSH + exact sqlite-vec point-read reranking p95 5.204 ms, total p95 40.261 ms. The benchmark's <50 ms assertion passed.

Latency target now passes on the release 100k-vector benchmark after using the architecture-appropriate quantized graph, avoiding single-query tokenization's Rayon setup, and setting ORT intra-op threads to 2. Across 100 uncached queries: inference p95 36.382 ms, LSH + exact sqlite-vec point-read reranking p95 5.204 ms, total p95 40.261 ms. The benchmark's `<50 ms` assertion passed.
Author
Owner

The adversarial API run had 0 hostile-input acceptance and 0 search-storm 5xx, but it found a recall defect: after the model loaded and the note fixture was created, "flat search in berlin" did not return "Apartment hunting" within 120 seconds. The current two-table 20-bit radius-1 LSH candidate probe is too narrow for this paraphrase. I am widening it and will rerun the adversarial recall probe before accepting the search gate.

The adversarial API run had 0 hostile-input acceptance and 0 search-storm 5xx, but it found a recall defect: after the model loaded and the note fixture was created, `"flat search in berlin"` did not return `"Apartment hunting"` within 120 seconds. The current two-table 20-bit radius-1 LSH candidate probe is too narrow for this paraphrase. I am widening it and will rerun the adversarial recall probe before accepting the search gate.
Author
Owner

Correction to my prior recall finding: the exact pinned-model pair test now reports minimum LSH distance 2 and query_vectors returns the Apartment hunting document. The API probe's fixture path is Home-relative (Notes/...), while Search hit IDs include the Home prefix (users/<id>/Notes/...); the probe compared them for exact equality and therefore reported a false negative. I am correcting the probe to compare the full path suffix and will rerun it against the real server.

Correction to my prior recall finding: the exact pinned-model pair test now reports minimum LSH distance 2 and `query_vectors` returns the Apartment hunting document. The API probe's fixture path is Home-relative (`Notes/...`), while Search hit IDs include the Home prefix (`users/<id>/Notes/...`); the probe compared them for exact equality and therefore reported a false negative. I am correcting the probe to compare the full path suffix and will rerun it against the real server.
Author
Owner

The 100k semantic latency test currently misses the <50 ms p95 target under shared build/server load: 57.002 ms total (50.377 ms inference, 10.584 ms sqlite-vec). I am checking ONNX thread settings and will report the final measured result.

The 100k semantic latency test currently misses the <50 ms p95 target under shared build/server load: 57.002 ms total (50.377 ms inference, 10.584 ms sqlite-vec). I am checking ONNX thread settings and will report the final measured result.
Author
Owner

Review of the expanded Hamming-shell retrieval found a candidate starvation case: the per-shell row limit was 4 * remaining, while documents already found in earlier shells could still occupy up to three other table rows in a later shell. With one slot remaining, four rows can therefore all be repeats while a new result follows them. I added a regression fixture for prior-shell exclusions and am changing each shell query to exclude prior IDs and limit distinct new documents.

Review of the expanded Hamming-shell retrieval found a candidate starvation case: the per-shell row limit was `4 * remaining`, while documents already found in earlier shells could still occupy up to three other table rows in a later shell. With one slot remaining, four rows can therefore all be repeats while a new result follows them. I added a regression fixture for prior-shell exclusions and am changing each shell query to exclude prior IDs and limit distinct new documents.
Author
Owner

Finished on branch job/search-hybrid at dc98fe4d5f0cda28f18adbeca6f3045c9f87ec2f.

Implemented a local pinned ONNX embedding crate, semantic index with bounded LSH candidate retrieval and exact reranking, semantic/keyword search fusion, file-index hooks and server registration. The pinned model regression passed: cross-vocabulary minimum LSH distance: 2 and test store::tests::local_model_retrieves_apartment_hunting_for_flat_search_in_berlin ... ok. The adversarial run ended with ==== FINDINGS 0 and printed semantic recall: "flat search in berlin" -> "Apartment hunting".

Gate output:

  • cargo fmt --check: exit 0, no output.
  • cargo clippy --all-targets -- -D warnings: Finished dev profile [unoptimized + debuginfo] target(s) in 20.79s (exit 0).
  • Workspace cargo test: Finished test profile [unoptimized + debuginfo] target(s) in 39.75s; all test results were ok, 0 failures (exit 0).
  • bash packages/api-client/check-generated.sh: ✨ openapi-typescript 7.13.0 and 🚀 ../../contracts/openapi.json → src/generated.ts [1.1s] (exit 0).
  • bun run --cwd apps/web check: svelte-check found 0 errors and 0 warnings.
  • bun run --cwd apps/web test: Test Files 3 passed (3) and Tests 9 passed (9).
  • bash tests/adversarial/run.sh: ==== FINDINGS 0.
  • Required 100k latency benchmark did not meet <50 ms p95 under the shared host load. The latest unpinned output was 100k semantic query latency: inference p50=105354 us p95=188202 us; sqlite-vec p50=8667 us p95=22041 us; total p50=113572 us p95=201733 us (100 uncached queries). A run pinned to the two least busy CPUs measured total p95 213.991018ms. At the same time, other worktrees were running server attack tests and builds. The earlier 40.261 ms sample was before widening the LSH index, so it is not claimed as final-config evidence.
  • cargo clean: Removed 22090 files, 10.7GiB total.

Known gap: verify the 100k end-to-end latency target on an uncontended host. The current measurement fails the target under observed shared CPU contention.

Finished on branch `job/search-hybrid` at `dc98fe4d5f0cda28f18adbeca6f3045c9f87ec2f`. Implemented a local pinned ONNX embedding crate, semantic index with bounded LSH candidate retrieval and exact reranking, semantic/keyword search fusion, file-index hooks and server registration. The pinned model regression passed: `cross-vocabulary minimum LSH distance: 2` and `test store::tests::local_model_retrieves_apartment_hunting_for_flat_search_in_berlin ... ok`. The adversarial run ended with `==== FINDINGS 0` and printed `semantic recall: "flat search in berlin" -> "Apartment hunting"`. Gate output: - `cargo fmt --check`: exit 0, no output. - `cargo clippy --all-targets -- -D warnings`: `Finished `dev` profile [unoptimized + debuginfo] target(s) in 20.79s` (exit 0). - Workspace `cargo test`: `Finished `test` profile [unoptimized + debuginfo] target(s) in 39.75s`; all test results were `ok`, 0 failures (exit 0). - `bash packages/api-client/check-generated.sh`: `✨ openapi-typescript 7.13.0` and `🚀 ../../contracts/openapi.json → src/generated.ts [1.1s]` (exit 0). - `bun run --cwd apps/web check`: `svelte-check found 0 errors and 0 warnings`. - `bun run --cwd apps/web test`: `Test Files 3 passed (3)` and `Tests 9 passed (9)`. - `bash tests/adversarial/run.sh`: `==== FINDINGS 0`. - Required 100k latency benchmark did not meet `<50 ms p95` under the shared host load. The latest unpinned output was `100k semantic query latency: inference p50=105354 us p95=188202 us; sqlite-vec p50=8667 us p95=22041 us; total p50=113572 us p95=201733 us (100 uncached queries)`. A run pinned to the two least busy CPUs measured total p95 `213.991018ms`. At the same time, other worktrees were running server attack tests and builds. The earlier 40.261 ms sample was before widening the LSH index, so it is not claimed as final-config evidence. - `cargo clean`: `Removed 22090 files, 10.7GiB total`. Known gap: verify the 100k end-to-end latency target on an uncontended host. The current measurement fails the target under observed shared CPU contention.
Author
Owner

Semantic recall evidence from #303's adversarial round (2026-09-28): the probe created Notes/20260928-apartment-hunting-cb30e5ce.md, then queried renting a home close to public transport for 120 seconds. The local model did not return that Note. The run had sustained CPU load and media worker backlog, so this result needs a quieter reproduction before attributing it to retrieval quality.

Semantic recall evidence from #303's adversarial round (2026-09-28): the probe created `Notes/20260928-apartment-hunting-cb30e5ce.md`, then queried `renting a home close to public transport` for 120 seconds. The local model did not return that Note. The run had sustained CPU load and media worker backlog, so this result needs a quieter reproduction before attributing it to retrieval quality.
Author
Owner

During the single real-server adversarial round for #291, the semantic recall probe reported: the local model did not return 'Notes/20260928-apartment-hunting-966896d3.md' within 120s. The probe did not determine whether this was a model retrieval miss or an indexing/freshness delay. This is evidence from a shared-host run and was not repeated.

During the single real-server adversarial round for #291, the semantic recall probe reported: `the local model did not return 'Notes/20260928-apartment-hunting-966896d3.md' within 120s`. The probe did not determine whether this was a model retrieval miss or an indexing/freshness delay. This is evidence from a shared-host run and was not repeated.
Author
Owner

The one-time adversarial run on job/touch-369 did not return the semantic recall fixture Notes/20260929-apartment-hunting-ea920a65.md within the probe's 120-second limit. The server was under sustained Search/API load on a shared host during the run. No Search implementation or expectation was changed here.

The one-time adversarial run on `job/touch-369` did not return the semantic recall fixture `Notes/20260929-apartment-hunting-ea920a65.md` within the probe's 120-second limit. The server was under sustained Search/API load on a shared host during the run. No Search implementation or expectation was changed here.
Author
Owner

Hygiene review: the latest real-server run did not return the semantic recall fixture within 120 seconds, and the final 100k end-to-end latency sample was 213.99 ms p95. Keeping #59 open for a controlled retrieval and latency check.

Hygiene review: the latest real-server run did not return the semantic recall fixture within 120 seconds, and the final 100k end-to-end latency sample was 213.99 ms p95. Keeping #59 open for a controlled retrieval and latency check.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#59
No description provided.