Repository navigation
Rewrite search-by-meaning around vec0; enforce the distcheck platform mirror - #1009
Merged
Merged
Conversation
The guide taught semantic search only as a path-table subquery, which costs one embed round trip per matched file per query on top of a walk that reads every file's content. Since #956 the ddl batch can declare a vec0 virtual table beside the row table and fill it from a trigger, so a query embeds the question once and sqlite-vec scans in process. The index shape is now the primary recipe: model-width probe, the [[table]] config, the MATCH/k query joined back on rowid, what the two triggers guarantee, and what a model-id edit costs. The path-table shape stays as the zero-setup option with the trade stated explicitly, and the "Top-k is LIMIT k" tip is folded into it -- it existed to explain why MATCH ... AND k = did not apply, which stops being true once the primary example uses vec0. Two corrections to the template sketched in #960, both found by running it: vec0 defaults to L2, so the declaration needs distance_metric=cosine to agree with vec_distance_cosine; and embed(NULL) is NULL, which vec0 rejects outright and which fails the whole table load, so the insert trigger needs a WHEN new.text IS NOT NULL guard. Closes #1003
`internals/distcheck`'s `Platform` dataclass is a hand-maintained subset of `packages/ts/src/platforms.ts`, asserted by a comment and checked by nothing. It drifted both ways: `exe` sits on the Python side justified as mirroring the TypeScript source of truth, which has never carried the field, while `triple` and `libc` exist only on the TypeScript side. The same unchecked-mirror mechanism is behind #972 and #996. These tests specify a `dirsql-checks platforms-mirror` gate that holds the two files to the subset framing: the shared fields agree row for row, every published target has a row on both sides, and a Python field with no TypeScript counterpart is drift. The last integration case runs it over the repo's own two files, which is where it earns its keep. Red: `checks.platforms_mirror` does not exist and the CLI has no such subcommand. Refs #1004
`dirsql-checks platforms-mirror` reads both platform tables as source text -- the Python one through `ast`, the TypeScript one through a comment-stripping scan into `json` -- and holds them to the subset framing the Python docstring already claimed: `SHARED` names every Python field and the TypeScript property it comes from, both tables must carry the same targets, and a Python field outside `SHARED` is drift by construction. `triple` and `libc` staying TypeScript-only is the subset working, not a gap. Neither side is imported. internals/checks depends on neither internals/distcheck nor a node toolchain, and a check that executes the file it audits can be fooled by the file. A table it cannot parse fails the check rather than passing on an empty read -- a mirror check that silently stops comparing passes forever. With that in place `Platform.exe` goes: nothing outside its own tests read it, and the comment keeping it alive cited a correspondence packages/ts has never had. The "Keep in lockstep" comment it hid behind is replaced by one that states the subset and names the check enforcing it. Runs in its own workflow rather than as a job in internals-distcheck-ci.yml: that lane's path filter does not watch packages/ts, which is exactly where a half-added target starts. Closes #1004
CI found 33 surviving mutants the local run had not: the gate scopes to the diff, and at the time it ran the new modules were still untracked, so it mutated nothing in them. Run it after committing, not before. Most survivors were unkillable by any assertion rather than merely untested. Comparing a parsed identifier against a module-level string literal admits an `==` -> `is` mutant that CPython's interning makes genuinely equivalent, so the name comparisons become dict and set lookups instead: classes and module bindings are collected by name and looked up, and a row's callee is tested for membership in ROW_CALLS. `_assigned_names` also drops the `len(targets) == 1` guard and its `targets[0]`, whose `[-1]` mutant was equivalent under that very guard -- and it now reads `A = PLATFORMS = (...)` correctly as a side effect. The hand-rolled TypeScript scanner is gone, and with it seventeen survivors in its index arithmetic and bounds checks. Comments and string literals are now one regex alternation, and `json`'s own `raw_decode` finds the end of the array, so nothing counts brackets. The three bespoke "unterminated X" errors collapse into one message that names every shape it cannot read. `decide.typescript_value` keys its per-field derivation off a DERIVE table rather than comparing the field name, and `gate.run` drops the keyword-only marker, which cosmic-ray mutates to the positional-only one -- accepted by the interpreter and unobservable to every caller. New assertions cover what remains: the positional-arity guard from both sides, a field whose value sorts above platforms.ts as well as below, a class and a table bound under names sorting either side of the real ones, and the string unwrapping pinned to an exact value. Refs #1004
…plate #1010 landed while this branch was open and put the vec0 template on its own page, search-indexes.md, which frames search-by-meaning.md as the no-config guide. Git merges the two cleanly because neither touched the other's file, but the result would have taught the same recipe twice, with different details. So the division of labour follows the page that landed: the index recipe lives once, on search-indexes.md, and search-by-meaning.md keeps the path-table shape and links out. What #1003 asked for survives that split. The two shapes and their trade are stated up front as a table -- setup, freshness, per-query cost, top-k form -- along with the caveat that an index only pays off when the table outlives the query, since a one-shot run against an ephemeral index re-embeds the corpus before answering. The "Top-k is LIMIT k" tip is retired: it existed to explain why MATCH ... AND k = did not apply, which is now a row in that table and one clause in the walk-through. Two fixes to the template on search-indexes.md, both found by running it: vec0 defaults to L2, which is not the metric the rest of dirsql's semantic search uses -- vec_distance_cosine() backs both the plugin one-liner and search-by-meaning. The two agree only for vectors of equal length, so documents that embed to different magnitudes rank differently: over three notes, L2 returned a, c, b where cosine returned a, b, c. The declaration now carries distance_metric=cosine. embed(NULL) is NULL and vec0 rejects a NULL vector, so one row with missing text failed the whole table load rather than its own insert. The insert trigger now carries WHEN new.body IS NOT NULL; the row still lands, only its vector is skipped. Closes #1003
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent changes, on one branch because the session was scoped to both issues.
Closes #1003
Closes #1004
1. Docs: the two semantic-search shapes, and two fixes to the
vec0template (#1003)#1010 merged while this branch was open, landing #960's templates on a new page,
docs/howto/search-indexes.md, which framessearch-by-meaning.mdas the no-config guide. Git merges the two cleanly — neither touched the other's file — but the result would have taught the samevec0recipe twice with different details. So this follows the page that landed rather than competing with it: the index recipe lives once, onsearch-indexes.md, andsearch-by-meaning.mdkeeps the path-table shape and links out.What #1003 asked for survives that split:
--persist, ordirsql server): a one-shotdirsql queryagainst an ephemeral index rebuilds the table, and therefore re-embeds the corpus, before it answers, which is strictly more work than the subquery.::: tip Top-k is LIMIT kblock is retired. It existed to explain whyMATCH … AND k =did not apply; that is now a row in the trade table and one clause in the walk-through.search-by-meaningaround avec0index instead of per-rowembed()#1003 item 5) is onsearch-indexes.md, where theddlbatch is, and linked from the trade section.Two fixes to the template
search-indexes.mdshipsBoth found by running it, not by reading it:
distance_metric=cosinewas missing.vec0defaults to L2, which is not the metric the rest of dirsql's semantic search uses —vec_distance_cosine()backs both the plugin's one-liner andsearch-by-meaning. The two agree only for vectors of equal length, so documents embedding to different magnitudes rank differently. Demonstrated against real sqlite-vec with three notes whose vectors are[1,1],[5,5],[2,1]and the query[1,1]:bandcswap. (My first pass claimed this with a unit-vector fixture, where the two metrics cannot diverge — that evidence didn't support the claim, so this is the run that does.)The insert trigger needed
WHEN new.body IS NOT NULL.embed(NULL)isNULL, andvec0rejects a NULL vector outright, so one row with missing text fails the whole table load rather than its own insert:With the guard the row still lands in
notes; only its vector is skipped. FTS5 needs no equivalent — it indexes a NULL happily.Also worth noting for anyone reading #960: its sketch's
embed(:q, '')would fail — the worker treats a two-arg call's second argument as a real model id (StaticModel.from_pretrained('')). The page that landed already spells the id out in both places, which is right.Manual verification
HuggingFace is blocked by the sandbox proxy (
CONNECT→ 403), so the model itself could not be downloaded. Everything dirsql does was exercised for real against a locally built CLI (cargo build -p dirsql --features cli) and realsqlite-vec0.1.9 from PyPI, with a stubembed()worker speaking the genuine[[dirsql.function]]worker protocol. Only model inference was substituted; function dispatch, trigger-time invocation, JSON-text vector binding,vec0KNN, and the persist sweep are all the real code paths.The corrected
[[table]]block was extracted verbatim fromsearch-indexes.md(onlyfloat[512]→float[2]for the stub's width) and run:Also confirmed earlier in the session: delete propagation across a
--persistrestart ([{"notes":2,"vecs":2}]after removing a file — theAFTER DELETEtrigger fires through the incremental sweep), and that a wrongfloat[N]fails withDimension mismatch … Expected 7 dimensions but received 8.pnpm buildindocs/exits clean (VitePress fails on broken links).One thing I could not verify: the real vector width of
minishlab/potion-retrieval-32M. HF is unreachable from the sandbox and neither the repo nor the model2vec README records it.search-indexes.mdalready handles this correctly — it makes the probe a required step and treats512as a placeholder — so nothing here depends on the number being right, but it is worth a maintainer eye.2. Checks: enforce the
platforms.py↔platforms.tsmirror (#1004)#1004 is a discussion, not a work item, and asked which framing wins before anything is deleted. The maintainer chose the subset framing, enforced now — so
exegoes and the mechanism behind it gets a gate, rather than tidying the symptom and leaving the drift possible.Its claims check out:
Platform.exeis read only by its own three test assertions, andgit log --all -S'exe' -- packages/ts/src/platforms.tsis empty — the TypeScript side has never carried the field, so the comment justifying it has been false since it was written.The check
dirsql-checks platforms-mirrorreads both tables as source text — the Python one throughast, the TypeScript one stripped of comments and handed tojson's own decoder. Neither is imported:internals/checksdepends on neitherinternals/distchecknor a node toolchain, and a check that executes the file it audits can be fooled by the file. A table it cannot parse fails rather than passing on an empty read, because a mirror check that silently stops comparing passes forever.decide.py'sSHAREDis the invariant as data — every Python field and the TypeScript property it is sourced from (slug←libNameminus the@dirsql/lib-prefix, which is whatlibrarySlug()returns). From that one table it derives four failures:SHAREDexecasePlatform(...)line to pastetripleandlibcstaying TypeScript-only is the subset working as intended — the framing the Python docstring already claimed and thePLATFORMScomment contradicted. That comment now states the subset and names the check enforcing it.Wired as its own workflow (
platforms-mirror-check.yml) rather than a job ininternals-distcheck-ci.yml: that lane's path filter does not watchpackages/ts, which is exactly where the half-added-target case starts.Red/green
The removal of
exeis exempt (AGENTS.md), but the gate is new behavior:d617620pushed the integration + e2e tests alone. Run 32441254650 failed on exactly one job,internals/checks integration tests, withModuleNotFoundError: No module named 'checks.platforms_mirror'. Every other job passed.c9a387fadded the check and deletedexe.6a12c1e. See below.The mutation round
CI found 33 surviving mutants that the local gate had not, because the gate scopes to the diff and the new modules were still untracked when it ran — it mutated nothing in them. (Lesson: run that gate after committing.)
Most were unkillable by any assertion rather than merely untested, so the fix was structural, not an exemption:
==→ismutant that CPython's interning makes genuinely equivalent. Those name comparisons are now dict and set lookups: classes and module bindings are collected by name and looked up, and a row's callee is tested for membership inROW_CALLS._assigned_namesdrops thelen(targets) == 1guard and itstargets[0], whose[-1]mutant was equivalent under that very guard — and it now readsA = PLATFORMS = (...)correctly as a side effect.json'sraw_decodefinds the end of the array, so nothing counts brackets.typescript_valuekeys its per-field derivation off aDERIVEtable instead of comparing the field name, andgate.rundrops the keyword-only marker*,— cosmic-ray mutates it to the positional-only/, which the interpreter accepts and no caller can observe.New assertions cover the rest: the positional-arity guard from both sides, a field whose value sorts above
platforms.tsas well as below, a class and a table bound under names sorting either side of the real ones, and the string unwrapping pinned to an exact value. 224 mutants, no survivors locally with the files committed (versus 140 in the run that missed them). Zero exemptions — the repo's count stays at zero.Manual verification
Re-adding
exe: bool:Simulating a half-added
win32-arm64target inplatforms.tsonly:Both scratch edits were reverted.
Gates run locally
dirsql-checks preflight --conventions internals-checks-ci.yml --conventions internals-distcheck-ci.yml— 0 failing pairs across both roots:declared-deps,colocated-test,unit-lint,integration-lint,unit-coverage(100%),e2e-verify;mutationre-run separately post-commit at 224 mutants, no survivors. Full suites: 461 passed (internals/checks), 65 passed / 2 skipped (internals/distcheck).E2E Verification
packages/pythonchangepackages/tschangepackages/rustchangepackages/python/e2e-attestations/<branch>.json— N/A,packages/pythonuntouchedpackages/ts/e2e-attestations/<branch>.json— N/A,packages/tsuntoucheduvx testing-conventions e2e attest 'uv run python -m pytest tests/e2e -x -q'ininternals/checks(its owne2e-verifygate is scoped tointernals/checks/src, which this PR changes)internals/checks/e2e-attestations/claude-issues-1003-1004-kfn8ug.json(abdcae0). The docs half isN/A - docs only.Changelog / Migrations
packages/orplugins/changed.docs/,internals/checks/,internals/distcheck/and.github/workflows/are all outsidechanged_packages.Platform.exeis repo-only, never published, and nothing outside its own tests read it, so no public API or runtime behavior changes.PARITY.mdis untouched: no SDK public API changed, and no parity drift is introduced or restored.