Documents the fuzzy-search-by-default change shipped across tasks 1-10:
appends ADR-019 (fuzzy default, hand-rolled zero-dependency scorer, why
fuse.js/fastest-levenshtein were rejected), describes the src/search/
pure computation unit in ARCHITECTURE.md, updates README.md's tool
table/count (18 -> 19) and adds a Search behaviour section, and updates
TODO.md with the completed milestone, two accepted limitations, and
three tracked follow-ups. No source or test files were changed;
bun test (349 pass) and bun run typecheck remain unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The test queried 'budget', which only one corpus document matches, so it
returned a single-element array with nothing to reorder — it would pass
even with the tie-break removed. Switched to 'muller', where docs 1 and 5
tie at the same score and ordering is decided solely by the ascending-id
tie-break.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The test used query 'certificat', which is a literal substring of the
fixture's 'certificate' comment text. Exact mode is substring containment
by design, so the query matched and the toHaveLength(0) assertion could
never pass. Replaced with an insertion typo, which cannot be a substring
of the shorter original.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
mapWithConcurrency's callback return was inferred as a union of two object
literal types rather than one type with deep: TDeep | undefined, so the
'entry is Pair' predicate was rejected: a type predicate's type must be
assignable to its parameter's type. Added an explicit EnrichedCandidate
annotation on the call. Tests passed but tsc did not, so this was only
visible via typecheck.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
The guard tests used 3-char query tokens whose field tokens were literal
continuations ('car'/'carpet'), so the startsWith rule returned 0.9 before
the shared-prefix rule was reached. With MIN_SHARED_PREFIX=4 the rule is
unreachable for any 3-char token, making the assertion impossible to
satisfy. Swapped in cases where neither token is a true prefix of the
other, which actually exercise the guard.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Hand-computing the plan's own test expectations against its own formula
showed four assertions could not pass:
- Body-only matches maxed at 1.0*1/3 = 0.333, below minScore 0.35, so no
low-weight field could ever return a result. The gate now uses the raw
field score; the weighted score only orders.
- The phrase bonus was dead code whenever every token matched, since the
base already saturated at 1.0. Base is now scaled by 0.85.
- Natural-language queries tanked the coverage multiplier; query tokens
are now stopword-filtered.
- 'decide' vs 'decision' is 4 edits apart and unmatchable; scoreToken
gains a shared-prefix rule.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Adds an 11-task TDD implementation plan covering the pure search unit
(tokenize, score, rank, snippet, pipeline), per-service integration for
meetings, wikis, work packages and activities, a recall acceptance
suite, and documentation.
Corrects two errors found while writing the plan:
- The spec's behaviour table named an openproject_search_work_packages
tool that does not exist. searchWorkPackages is a service helper that
only tests call, so ranked work package search would have been
unreachable by any LLM. The plan now registers the tool (18 -> 19) and
leaves openproject_list_work_packages as a pure server-side filter.
- Plan Task 6 originally identified matching agenda items by substring
searching the meeting-level snippet, which cannot work because
snippets are trimmed and ellipsized. Agenda items are now ranked
directly.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Specifies replacing exact substring matching with token-aware fuzzy
matching as the default across all search tools, plus relevance ranking
and wider content coverage (wiki body text, work package descriptions
and comments, meeting participants and project names).
Key decisions:
- Hand-rolled zero-dependency scorer in src/search/; fuse.js rejected
because its Bitap implementation caps patterns at 32 chars and targets
short fields, not the long-text matching required here.
- matchMode: 'fuzzy' | 'exact' escape hatch, defaulting to fuzzy, with
exact mode preserving current behaviour byte-for-byte.
- Two-phase pipeline: cheap shallow rank, deep-enrich top 25 with
bounded concurrency, recency backfill for deep-only matches.
Also records two known limitations left out of scope: the two-phase
recall ceiling and the wiki cache's ID 1..50 probe window.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
- Add Section 3 in docs/ARCHITECTURE.md detailing multi-user concurrency and RequestContext
- Add ADR-012 in docs/DECISIONS.md establishing RequestContext pattern over global state
- Update AGENTS.md mandating stateless services and context propagation
- Update docs/TODO.md with src/context.ts implementation task
- Specify OPENPROJECT_READ_ONLY and --read-only in docs/ARCHITECTURE.md
- Add ADR-011 in docs/DECISIONS.md for tool manifest filtering and execution guards
- Update docs/TODO.md with read-only implementation tasks
- Update README.md, AGENTS.md, and .env.example with read-only configuration reference
- Track completed work, upcoming Phase 1 tasks, and backlog
- Establish strict agent policy to never proceed automatically without checking in
- Link docs/TODO.md in AGENTS.md and README.md
- Update AGENTS.md, CLAUDE.md, and GEMINI.md with Bun workflow rules and bun:test
- Update docs/ARCHITECTURE.md with Bun runtime layer and lockfile
- Add ADR-010 in docs/DECISIONS.md superseding ADR-001 with Bun adoption
- Update README.md with Bun prerequisites, installation, and run instructions