Skip to content

Use an options object for searchByIndex and the custom-index search() contract - #2187

Open
maurice-harper wants to merge 2 commits into
mainfrom
fix/issue-2165-search-options
Open

Use an options object for searchByIndex and the custom-index search() contract#2187
maurice-harper wants to merge 2 commits into
mainfrom
fix/issue-2165-search-options

Conversation

@maurice-harper

@maurice-harper maurice-harper commented Aug 17, 2026

Copy link
Copy Markdown

Closes #2165

Summary

searchByIndex had eight positional parameters, five optional, so callers padded with undefined or inherited unintended defaults. The same shape reached HierarchicalNavigableSmallWorld.search(), the contract an external index implements. Both now name the optional tail. No behaviour change.

Important changes

  1. searchByIndex takes a named options tail(condition, txn, reverse, Table, { allowFullScan, filtered, context, minResults }), defaulting to {}. All four call sites updated.
    • Why: Optional positionals were why callers padded.
    • Support: New searchByIndexOptions.test.js.
  2. customIndex.search() takes { filter, minResults }context stays second positional; benchmarks/hnsw-search.js updated too.
    • Why: A future cursor or deadline becomes a field, not a break.
    • Support: Existing offset+limit and filtered-traversal tests.
  3. The documented contract matches the coderesources/DESIGN.md said search(cond, ctx, filter); now search(cond, ctx, { filter }).
    • Why: A stale doc ships the old contract to external implementers.
    • Support: No *.md states the positional form now.

Where to focus

  • resources/Table.ts:4965allowFullScan was a bare false; destructured, false yields undefined, so a missed conversion fails open — permitting a full scan.

Risks and boundaries

  • Risk: Source-breaking for a custom index outside this repo — intended while unreleased.
  • Risk: A missed positional call in plain JS fails silently, not at build time.
  • Boundary: The three call sites that never threaded context still do not.
  • Boundary: No cursor field, typed interface, or shim, per team-lead decisions.
  • Boundary: Top-level DESIGN.md states no signature, so it did not change.
  • Boundary: Sandbox limits exclude test:unit:main and schemaMigrationFragility.test.js.

Validation

  • Only resources/DESIGN.md changed since head 44711b8, where these ran: build, typecheck, lint, prettier clean; resources suite 1534 → 1536 passing, identical 12 pre-existing failures.
  • Reverting resources/search.ts fails the new test and makes tsc reject Table.ts.
Dispatch evidence and stage history

Current head ab88257 · size class small (114 changed production lines) · visible body 300 words (guardrail 150-300).

Evidence:

Prior heads:

  • 44711b8 — small, 299 words, 2 important change(s), 2026-08-17T02:24:49Z

…ject (#2165)

searchByIndex had grown to eight positional parameters, five optional, so
callers padded with undefined or stopped early and inherited defaults they
never chose. The same shape reached HierarchicalNavigableSmallWorld.search(),
which is the contract an index implemented outside this repo has to satisfy —
every future capability there would be another positional argument and a
breaking change.

The four required leading parameters stay positional on both; the optional tail
becomes a named options object:

  searchByIndex(condition, txn, reverse, Table, { allowFullScan, filtered, context, minResults })
  customIndex.search(condition, context, { filter, minResults })

No behaviour change. The plain-JS benchmark caller is updated too, since the
TypeScript build cannot flag it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the searchByIndex and HNSW search methods to accept optional trailing parameters as a named options object instead of positional arguments, updating all call sites and adding a unit test to verify this behavior. The reviewer identified three locations in resources/Table.ts and resources/search.ts where the context parameter was omitted from the new options object, which would bypass the active transaction and potentially cause transaction isolation issues.

Comment thread resources/Table.ts
Comment thread resources/search.ts
Comment thread resources/search.ts
@maurice-harper

maurice-harper commented Aug 17, 2026

Copy link
Copy Markdown
Author
Dispatch durable state — QA PASS at ab88257 (machine handoff; the human summary is in the Dispatch status comment)

Behaviours exercised:

  • resources/DESIGN.md's 'How is a filter applied during a vector search?' row now reads HierarchicalNavigableSmallWorld.search(cond, ctx, { filter }), matching the real signature at head
  • HierarchicalNavigableSmallWorld.search's third parameter is a destructured { filter, minResults } = {} object (unchanged from the prior verified head 44711b8) — predicate-aware traversal, minResults widening, filterExpansion visit budget, and threshold comparators all still pass
  • searchByIndex options-object contract (allowFullScan/filtered/context/minResults) still behaves correctly: rejects an unindexed attribute when allowFullScan:false is passed in the options object, and full-scans permissively when the options object is omitted

Break attempts:

  • Diffed 44711b8..ab88257: exactly one file changed (resources/DESIGN.md), one line. Confirmed resources/search.ts, resources/Table.ts, and resources/indexes/HierarchicalNavigableSmallWorld.ts are byte-identical between the prior FAIL head and this head (empty git diff --stat on those three files), so the prior run's manual revert evidence against those exact files fully carries over — this run does not need to re-derive it
  • Grepped every *.md in the repo for customIndex.search(, searchByIndex(, and HierarchicalNavigableSmallWorld.search( looking for any remaining statement of the OLD positional (cond, ctx, filter) or (cond, ctx, filter, minResults) contract: only one hit repo-wide, resources/DESIGN.md:96, and it now reads the options-object form. DESIGN.md (root) mentions searchByIndex generically (isIndexing flag, ef/limit plumbing) but states no positional-argument contract, so nothing stale there
  • Re-ran the exact HierarchicalNavigableSmallWorld.search signature at head: search({...cond}, context, { filter, minResults } = {}) — confirms the doc row's { filter } phrasing is accurate (minResults also travels through the same third-arg object, doc row only needed to name the one it was describing)
  • Ran qa-revert-verify.sh 2165 2187 again: reproduced the identical documented infra limitation (status=tests-fail-before-revert, MODULE_NOT_FOUND on dist/utility/environment/environmentManager.js — the script's disposable worktree never runs npm run build). Not a new limitation; same one recorded at 44711b8, and it says nothing about this PR since it fails before any revert step runs
  • npm run build (tsc) clean at head; npx mocha unitTests/resources/vectorIndex.test.js unitTests/resources/searchByIndexOptions.test.js -> 94 passing, 0 failing, identical count to the prior verified head

Limitations:

  • qa-revert-verify.sh's disposable worktree symlinks node_modules but never runs npm run build, and this repo's tests load dist/ not source, so it fails with MODULE_NOT_FOUND before any revert step (status=tests-fail-before-revert) — same generic-script/repo-build-step mismatch documented at the prior head 44711b8, reproduced identically here. Since the production files this PR could regress (resources/search.ts, resources/Table.ts, resources/indexes/HierarchicalNavigableSmallWorld.ts) are byte-identical to that prior verified head, that head's manual revert evidence (options-object callers fail closed/throw when reverted to the old positional contract) fully carries over rather than needing to be redone this pass.
  • Did not re-run the full matched base-vs-head unitTests/resources/** suite comparison (1534/1536 passing, 12 identical pre-existing failures on both sides) or unitTests/sqlEngine/**; scope for this re-check was bounded to confirming the single-line doc fix plus a build + focused-test spot-check, per the docs-only delta between 44711b8 and ab88257. That broader comparison is unaffected by a resources/DESIGN.md-only change and stands as recorded on the prior QA artifact.

tests: npm run build (tsc --project tsconfig.build.json) clean at head ab88257. npx mocha unitTests/resources/vectorIndex.test.js unitTests/resources/searchByIndexOptions.test.js -> 94 passing, 0 failing (matches prior verified head's count exactly). Full base-vs-head unitTests/resources/** matched comparison (1534/1536 passing, 12 identical pre-existing failures both sides) and unitTests/sqlEngine/** (103 passing) carry over unchanged from the prior QA run since none of the files they exercise changed in this delta. · revert: qa-revert-verify.sh 2165 2187 hit the same documented infra limitation as the prior head (tests-fail-before-revert: disposable worktree never runs npm run build, MODULE_NOT_FOUND on dist/). The prior QA run's manual revert (reverted resources/search.ts, resources/Table.ts, resources/indexes/HierarchicalNavigableSmallWorld.ts to base 11a1c48, rebuilt, confirmed searchByIndexOptions.test.js and 8 vectorIndex.test.js predicate-aware cases fail with clear signals tied to the reverted contract) fully carries over: those three files are byte-identical between 44711b8 and this head ab88257 (confirmed via empty git diff --stat). · lint: npx prettier --check resources/DESIGN.md -> clean, no drift. oxlint does not apply (markdown file, not JS/TS).
Mechanical checks: Only changed surface is one Markdown table cell in resources/DESIGN.md (a backtick-quoted code span inside a GFM table row). Verified it renders as valid GFM (pipe-escaping unaffected, backticks balanced) and that prettier --check accepts the file as correctly formatted. No regex, HTML/JSX/XML, JSON/YAML/TOML, CSS selector, URL, or shell/heredoc strings were touched by this delta.

Acceptance criterion Verdict Evidence
AC1: searchByIndex leading params stay positional, optional tail becomes a named options object with allowFullScan/filtered/context/minResults PASS Unchanged since 44711b8 (file byte-identical); unitTests/resources/searchByIndexOptions.test.js both cases pass at ab88257
AC2: relationship-recursive searches (search.ts) and Table.ts:4960 relationship call preserve existing defaults, no positional-undefined padding PASS Unchanged since 44711b8 (file byte-identical); prior run's manual revert of this exact file breaks searchByIndexOptions.test.js as designed
AC3: customIndex.search() keeps context positional, filter/minResults via third options object PASS Unchanged since 44711b8 (file byte-identical); vectorIndex.test.js call sites pass at ab88257
AC4: HierarchicalNavigableSmallWorld.search's options object preserves predicate-aware filtering, minResults widening, per-query ef, threshold comparators PASS 94 passing across vectorIndex.test.js + searchByIndexOptions.test.js at ab88257, including the #1241 predicate-aware-traversal and minResults/ef-widening describe blocks
AC5: custom-index search contract documentation and direct test calls updated to options-object shape, no speculative cursor/interface/shim added PASS resources/DESIGN.md:96 now reads HierarchicalNavigableSmallWorld.search(cond, ctx, { filter }), matching the real signature; repo-wide grep of *.md finds no other stale positional-contract statement; no cursor field/typed interface/shim present (unchanged production diff)
AC6: npm run build, npm run test:unit:resources, and existing HNSW/vector-index tests pass without new dependencies PASS build clean at ab88257; focused HNSW/vector-index suite 94 passing; matched base-vs-head unitTests/resources/** comparison from the prior run (unaffected by this docs-only delta) showed no new failures; no package.json changes in this delta

Commands:

  • ~/.dispatch-dev-team/scripts/sprint.sh spec-path 2165
  • ~/.dispatch-dev-team/scripts/sprint.sh branch-of 2165
  • gh pr list --head fix/issue-2165-search-options --json number,headRefOid,isDraft
  • git fetch origin fix/issue-2165-search-options
  • git diff 44711b8ef..ab882571e (confirms exactly one line changed, resources/DESIGN.md)
  • git diff 44711b8ef..ab882571e --stat -- resources/search.ts resources/Table.ts resources/indexes/HierarchicalNavigableSmallWorld.ts (empty -> byte-identical)
  • git checkout ab882571e
  • grep -rn 'customIndex\.search(' / 'searchByIndex(' / 'HierarchicalNavigableSmallWorld\.search(' --include=*.md .
  • sed -n '971,1000p' resources/indexes/HierarchicalNavigableSmallWorld.ts (confirm real signature)
  • npm ci --no-audit --no-fund
  • npm run build
  • npx mocha unitTests/resources/vectorIndex.test.js unitTests/resources/searchByIndexOptions.test.js (94 passing)
  • npx prettier --check resources/DESIGN.md
  • ~/.dispatch-dev-team/scripts/qa-revert-verify.sh 2165 2187 (reproduced documented infra limitation, tests-fail-before-revert)

Dismissed concerns:

  • qa-revert-verify.sh reports tests-fail-before-revert instead of exercising an actual revert — Justification: pre-existing - Identical script/repo-build-step mismatch documented at the prior QA head 44711b8 (disposable worktree never runs npm run build, tests load dist/ not source). Not new to this delta, and the production files it would revert are byte-identical to that prior head, so that head's manual revert evidence stands.
  • unitTests/resources/schemaMigrationFragility.test.js SIGABRTs mocha (exit 134) when the full resources/** suite is run — Justification: pre-existing - Reproduced identically on base commit 11a1c48 in the prior QA run, before this PR's changes existed; this docs-only delta does not touch that file or anything it depends on.
  • 12 pre-existing failures remain in unitTests/resources/** at head — Justification: pre-existing - Same 12 test names fail on base commit 11a1c48 per the prior QA run's matched comparison; this delta only touches resources/DESIGN.md and cannot affect them.

Provenance: stage qa-check, head ab882571e4b050f7afda417773cd97a9d632a6a3, recorded 2026-08-17T03:57:52Z.

Prior heads:

  • 44711b8 — FAIL, 1 blocking finding(s), 2026-08-17T03:45:54Z

@maurice-harper

maurice-harper commented Aug 17, 2026

Copy link
Copy Markdown
Author

Dispatch status — head ab88257

Signal Source State
CI GitHub checks passing
QA Dispatch qa-check stage PASS (evidence)
Independent review Dispatch reviewer stage — Gemini REST API (degraded) (gemini-flash-latest) RECOMMEND_MERGE — low risk, 2 lens(es), degraded evidence (review)
Other review gemini-code-assist[bot] — automatic GitHub app commented (open)
Unresolved blocking findings QA + independent review 0

Checked at this head

  • QA exercised resources/DESIGN.md's 'How is a filter applied during a vector search?' row now reads…, HierarchicalNavigableSmallWorld.search's third parameter is a destructured { filter,…, searchByIndex options-object contract (allowFullScan/filtered/context/minResults) still…; tried to break it with Diffed 44711b8..ab88257, Grepped every *.md in the repo for customIndex.search(, searchByIndex(, and…, Re-ran the exact HierarchicalNavigableSmallWorld.search signature at head, and 2 more.
  • Independent review inspected searchByIndex options object refactoring, HierarchicalNavigableSmallWorld search signature and options handling, relationship recursive search call sites, vector index unit tests and benchmarks.

Recommended human action: Review and merge if you agree.

Basis: QA PASS at ab88257 (Dispatch qa-check stage); independent review RECOMMEND_MERGE at ab88257 (Dispatch reviewer stage, Gemini REST API (degraded) gemini-flash-latest); CI passing; gemini-code-assist[bot] commented.

Prior heads and machine state
  • 44711b8 — CI failing, QA FAIL, review missing, 2026-08-17T03:46:11Z

…hape (#2165)

resources/DESIGN.md still described HierarchicalNavigableSmallWorld.search
as (cond, ctx, filter). The third argument is now an options object.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@maurice-harper

Copy link
Copy Markdown
Author
Dispatch durable state — independent review at ab88257 (machine handoff; the human summary is in the Dispatch status comment)

Independent review: no requested changes at ab88257.
Inspected searchByIndex options object refactoring, HierarchicalNavigableSmallWorld search signature and options handling, relationship recursive search call sites, vector index unit tests and benchmarks.
Source: Dispatch reviewer stage — Gemini REST API (degraded) (gemini-flash-latest), not the automatic Gemini Code Assist app.
Degraded source: the REST fallback has no repository tool access. It read the staged prompt and the fallback diff only, so this result does not represent repository-level inspection.
Semantic risk: low (size class small); no sensitive surface matched.
Review lenses assigned: correctness, test-integrity.
Outside-family review: yes (author claude, reviewer gemini).
Review legs:

  • gemini-api: completed in degraded mode (degraded) — api/gemini-flash-latest (family gemini); lenses correctness, test-integrity; degraded: Gemini CLI exited 55; REST fallback has no repository tool access; attempts 2; 4s

@maurice-harper

Copy link
Copy Markdown
Author

Follow-up filed for the one deliberately-declined review theme: #2188 tracks threading context through the relationship-recursive searchByIndex calls and the Table.ts relationship resolver. Pre-existing behavior (those call sites never passed context on the old positional signature); kept out of this PR to preserve its no-behavior-change scope. Analysis lives in the resolved Code Assist threads.

@cb1kenobi

Copy link
Copy Markdown
Member

Reviewed ab882571 — no issues found. This PR looks good, nice job!


Generated by Barber AI

Comment thread resources/search.ts
Comment on lines +246 to +259
{
allowFullScan,
filtered,
context,
minResults,
}: {
allowFullScan?: boolean;
filtered?: any;
context?: any;
// How many rows the query will ultimately consume (offset + limit), when it is bounded. An
// approximate index returns a fixed-size candidate list, so without this a query asking for more
// rows than that list holds silently gets a short result set. Only custom indexes read it.
minResults?: number;
} = {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: a stale positional call silently permits a full scan — the one hazard the PR flags has no guard

allowFullScan is tested with a strict === false (lines 446 and 448), so destructuring a non-object 5th argument yields undefined and neither guard fires. I confirmed this at head rather than reasoning about it — searchByIndex({ attribute: 'unindexedName', value: 'x' }, undefined, false, T, false), the exact pre-PR call shape from Table.ts on main, returns the unindexed row instead of throwing the 404 "is not indexed" ClientError.

tsc rejects that in .ts (verified: npm run typecheck is clean), so nothing is broken today. But it does not cover the repo's plain-JS callers under unitTests/ and benchmarks/, and the PR body accepts "a missed positional call in plain JS fails silently, not at build time" as residual risk. Since the typed-index-interface mitigation was scoped out by the team-lead decision, a runtime guard is the only remaining one — and it is what makes the hazard this PR's new test file describes in prose actually assertable.

Suggested fix — name the parameter, reject a non-object, then destructure (structural, so not a one-click suggestion):

	Table: any,
	options: {
		allowFullScan?: boolean;
		filtered?: any;
		context?: any;
		minResults?: number;
	} = {}
): AsyncIterable<Id | { key: Id; value: any }> {
	if (typeof options !== 'object' || options === null)
		throw new TypeError('searchByIndex: the 5th argument is an options object (#2165), not a positional value');
	const { allowFullScan, filtered, context, minResults } = options;

That also lets searchByIndexOptions.test.js add the case its header comment already describes: assert the stale positional form throws instead of quietly full-scanning.


Generated by Barber AI

* @param distance
* @param comparator
* @param context
* @param options

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: options now names two different things in search()

This new @param options documents the third parameter, but the body already uses options for something unrelated — const options = context.transaction (line 1038), the nested RocksDB transaction handed to getEntryPoint(options) and searchLayer(..., options, ...).

Nothing breaks today, because the third parameter is destructured inline and never bound to a name. The cost is on the next edit: giving that parameter a name — which is exactly what adding a runtime type guard would require — collides with the local.

Renaming the local to txnOptions (3 uses) frees the name and makes the JSDoc unambiguous.


Generated by Barber AI

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated gate — not yet queued for human review.

This PR's AI review found issues, and the PR description reports no cross-model reviews.
Per team policy, a substantive PR with AI-review findings is queued for human review only after at least 2 cross-model reviews have been run, their findings addressed, and the coverage reported in the PR description (## Review coverage naming each model — see harper-engineering-guidelines).
The findings below count as one of the two: address them, run a second outside-model review, update the description, and the gate lifts automatically on the next pass.


TL;DR

This refactors growing optional positional parameters into named options objects.
The approach addresses the root cause, preserves in-repo behavior, and adds negligible hot-path overhead.
One external-contract migration concern remains.
Independent review and tests were unavailable because this read-only checkout has no installed dependencies; syntax and whitespace checks passed.

verdict: CHANGES
merge: rebase
Human-Review-Need: 4 @ ab88257

Change tour

  • resources/search.ts:143-148, resources/search.ts:241-259 — query execution and searchByIndex now use named options with permissive omission defaults.
  • resources/search.ts:281-309, resources/Table.ts:4960-4966 — recursive relationship searches preserve allowFullScan and join-filter propagation.
  • resources/search.ts:509-536, resources/indexes/HierarchicalNavigableSmallWorld.ts:960-1009 — the custom-index boundary and HNSW implementation adopt { filter, minResults }; traversal behavior remains unchanged at resources/indexes/HierarchicalNavigableSmallWorld.ts:1058-1095.
  • benchmarks/hnsw-search.js:226, resources/DESIGN.md:96 — benchmark and design documentation reflect the new shape.
  • unitTests/resources/searchByIndexOptions.test.js:35-52, unitTests/resources/vectorIndex.test.js:809-813, unitTests/resources/vectorIndex.test.js:1955-2045 — coverage checks omitted defaults, allowFullScan, filtering, and minimum-result behavior.

Findings

major — resources/search.ts:516 — the unversioned argument-shape change breaks existing out-of-repo custom indexes and legacy direct HNSW callers

Verification

  • Passed node --check on every changed TypeScript/JavaScript file.
  • Passed git diff --check origin/main...HEAD.
  • Unit tests and lint were not run: dependencies are absent and the sandbox prevented installation.
  • Cross-model review setup failed before sharing the diff because its artifact directory is read-only.

Review coverage

lens outcome
gemini pruned — pruned (policy minimal)
cursor-grok pruned — pruned (policy minimal)
cursor-composer pruned — pruned (policy minimal)
codex ok — graded leg — produced review.md + comments.json
domain pruned — pruned (policy minimal)

Pre-push review of fix/issue-2165-search-options (ab88257) vs origin/main by codex.
Review emphasis: Dispatch-configured.

— codex review, submitted by the dispatch review gate

Comment thread resources/search.ts
return entry;
});
const loaded = index.customIndex
.search(searchCondition, context, { filter: recordFilter, minResults })

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a breaking change to the external custom-index contract. An existing search(condition, context, filter, minResults) implementation now receives an options object as filter; invoking that truthy value fails with TypeError. Conversely, a legacy direct HNSW caller silently loses its filter because resources/indexes/HierarchicalNavigableSmallWorld.ts:995-1009 destructures the function as an object with no filter field. Please add a version/feature gate or transitional normalization for the old signature, or confirm and document that every out-of-repo implementation and caller will be updated atomically.

— KrAIs (Codex)

@kriszyp kriszyp added this to the v5.3 milestone Aug 17, 2026
@kriszyp
kriszyp dismissed their stale review August 18, 2026 20:49

cross-model coverage reported — released to human review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use an options object for searchByIndex and the custom-index search() contract

3 participants