Skip to content

fix!: actionable fql errors and anchor-seeded recall - #218

Merged
RonsenbergVI merged 10 commits into
mainfrom
chore/improve-query-parser
Aug 22, 2026
Merged

fix!: actionable fql errors and anchor-seeded recall#218
RonsenbergVI merged 10 commits into
mainfrom
chore/improve-query-parser

Conversation

@RonsenbergVI

@RonsenbergVI RonsenbergVI commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Pull Request

Type of change

  • Bug fix
  • New feature
  • Refactor / internal cleanup
  • Docs
  • Other:

Description of the changes

A recall is now seeded by a term, an anchor or a vector instead of requiring a text term, and every FQL rejection carries a repair instruction rather than a report. The shapes that used to be answered wrongly (a repeated modifier, a second line, an empty quoted value) are now errors. With support for upper case, a warning is issued for an upper case letter in a keyword.

Motivation for the changes

An agent can only fix a query the error tells it how to fix, and it can only notice a wrong answer if it gets one: Expected colon, but found "" names an empty literal the caller never wrote, and depth:2 depth:5 silently ran a differently-scoped query. Closes #160recall topic:billing, the SDK's own documented seed, was unreachable, so callers padded it with a keyword they did not mean.

Breaking change?

  • No
  • Yes — details below
If yes, fill this in

What breaks: four query shapes that returned 200 now return 400, three that
returned 400 now return 200, and several error strings changed wording. The FQL
surface is public API, so this is a breaking release.

Before → after:

// before — answered, wrongly
recall x depth:2 depth:5   200, silently ran depth:5
recall x\nbridge           200, silently a two-term recall spanning lines
remember@1 ''              200, stored an unretrievable empty fact
recall x topic:''          200, stored an anchor nobody can name again

// after
recall x depth:2 depth:5   400  duplicate depth clause: depth may be given only once — drop one
recall x\nbridge           400  unexpected "bridge" after end of query (one command per instruction)
remember@1 ''              400  a remembered fact must not be empty
recall x topic:''          400  an anchor value must not be empty

// before — rejected, wrongly
recall topic:billing       400  expected a word or quoted phrase, but found "topic"
recall vec:$v              400  same
recall x Depth:2           400  mis-cased keyword

// after
recall topic:billing       200
recall vec:$v              200
recall x Depth:2           200, read as depth:2

// same rejection, new wording
recall x top               was: Expected colon, but found ""
                           now: "top" is a keyword and starts no clause here: write
                                top:<value> if a clause was meant, or quote it ('top')
                                to search for the word
recall x depth:9           was: depth:9 exceeds max 2
                           now: depth:9 out of range (0-2)
recall x vec:v             was: Error while parsing vector ref field "v"
                           now: expected param field operator $, but found "v"

Migration: drop the repeat from any query giving top:/depth:/since:/ until:/vec: twice (repeated topic:/entity: is unaffected — anchors are a list by design); send one command per request, a trailing newline is still fine; and update anything matching on the old error strings (exceeds max, Error while parsing, Expected colon, but found ""). Positions and the parse error at column N: prefix are unchanged, and the newly-accepted shapes need nothing — recall("zzznomatch", vector=...) still works, it just no longer needs the placeholder keyword.

How it was tested

Against a locally-built server on tests/fraise.config.toml (the config compose mounts), each suite on a fresh instance since they claim overlapping graphs:

make lint                                                    # clean
go test ./...                                                # 19 packages, 0 failures
uv run --package fraise-sdk pytest sdk/python/src/tests       # 93 passed
uv run --package tests pytest tests/e2e                       # 341 passed, 1 skipped
uv run --package fraise-sdk pytest tests/integration/python    # 73 passed

The skip is test_vector_search_with_real_embeddings — needs transformers,
and was skipped before this change too.

tests/e2e/parser_test.py is the new adversarial surface: 208 cases over raw
HTTP, all passing. It pins the error a caller can repair rather than the one the
parser happens to emit, and pins that queries which only look dangerous
(specials inside quotes, reserved words in value position) still succeed.

Checklist

  • Tests pass locally
  • Added/updated tests for the change (or explained why not)
  • Docs updated if behavior changed

Notes for reviewers

Two calls worth a second opinion:

  • A mis-cased keyword glued to a : is now that clause. Reading DEPTH:5 as the depth clause is what lets depth:2 DEPTH:5 be a duplicate error rather than a casing one — blaming the casing names the shallower problem, and an agent that lower-cased it would get a silently rescoped query back. The cost: recall x Depth:2 now parses, relaxing "keywords are matched exactly" and changing six pinned cases. Away from a colon casing still matters.
  • Anchor-only recall parses but returns zero hits. gatherSeeds guards on len(keywords) > 0, so with no term and no vector there are no seeds. Making anchors actually seed retrieval is a graph-layer change with scoring consequences, so this stops at making the question askable.

Also: sdk/python/pyproject.toml gains readme = "README.md" — unrelated to the grammar, but it is why the PyPI page has no description. Happy to split it out.

@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.93048% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 87%. Comparing base (6f81991) to head (ddb5697).

Files with missing lines Patch % Lines
internal/query/parser/parser.go 98% 2 Missing ⚠️
Additional details and impacted files
@@         Coverage Diff          @@
##           main   #218    +/-   ##
====================================
+ Coverage    86%    87%    +1%     
====================================
  Files        56     56            
  Lines      3461   3567   +106     
====================================
+ Hits       2980   3107   +127     
+ Misses      403    389    -14     
+ Partials     78     71     -7     
Flag Coverage Δ
go 87% <98%> (+1%) ⬆️
python 86% <100%> (+<1%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
internal/query/lexer/lexer.go 100% <100%> (+3%) ⬆️
internal/query/lexer/token.go 100% <100%> (ø)
internal/query/query.go 94% <100%> (ø)
sdk/python/src/fraise_sdk/query.py 96% <100%> (+<1%) ⬆️
internal/query/parser/parser.go 98% <98%> (+7%) ⬆️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@RonsenbergVI
RonsenbergVI merged commit 31af72a into main Aug 22, 2026
38 checks passed
@RonsenbergVI
RonsenbergVI deleted the chore/improve-query-parser branch August 22, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: SDK builds anchored-only recalls that the server grammar rejects

1 participant