Skip to content

fix(agent): read the sqlite kind vocabulary from the provider, not source text - #986

Merged
cevheri merged 2 commits into
libredb:mainfrom
sloemo01:fix/sqlite-kind-guard-reads-the-provider
Sep 19, 2026
Merged

cevheri merged 2 commits into
libredb:mainfrom
sloemo01:fix/sqlite-kind-guard-reads-the-provider

Conversation

@sloemo01

Copy link
Copy Markdown
Contributor

Closes #981

What was wrong

The guard read SQLITE_OBJECT_KINDS out of sqlite.ts with /\{ id: "(\w+)"/g, which matches a single-line entry only. Measured against the same declaration, using the same pattern:

declaration the scrape sees the guard
today, unchanged 4 ids passes
a fifth kind wrapped 4 ids passes
the same fifth kind on one line 5 ids fails on fifth_kind

A fifth kind written across two lines is invisible to the scrape, and COMPOSED_KIND_WORDS never gains it either. Both sides shrink together, and the guard passes over the exact defect it exists to catch.

What changed

tests/unit/lib/agent/context-snapshot.test.ts

The guard asks the constructed provider instead of the file's text:

const provider = await createDatabaseProvider({
  id: "kind-vocabulary-guard",
  name: "kind-vocabulary-guard",
  type: "sqlite",
  database: ":memory:",
  createdAt: new Date(0),
} satisfies DatabaseConnection);
const declaredIds = (provider.getCapabilities().objectKinds ?? []).map((kind) => kind.id);

Capabilities are type-driven, so the provider is never connected. The connection carries database only because SQLiteProvider validates its config in the constructor and wants a file path; :memory: satisfies that without naming one.

Two tests, because the failure has two halves:

  • the identity guard itself, now provider-sourced and therefore indifferent to layout
  • a kind declared but left unmapped is caught, exercised through the same comparison. That is the case that used to pass.

src/lib/db/providers/sql/sqlite.ts

The comment that kept the entries on one line is rewritten. It still carries what it did (why SOURCE_SQL is one constant, and the #789 Phase 2 fact about the definition text) and now states what protects the vocabulary rather than what constrains the formatting.

Testing

bun run format       exit 0
bun run lint         exit 0 (228 warnings, 0 errors)
bun run typecheck    exit 0
bun tests/run-tests.ts tests/unit/lib/agent/context-snapshot.test.ts
                     147 pass, 0 fail

bun run test over the whole tree is red here, on files this change does not touch. I compared those files against a clean checkout of main before opening this, and the counts are identical on both trees:

file clean main with this change
tests/integration/db/sqlite-provider.test.ts 75 fail 75 fail
tests/isolated/object-source-declarations.test.ts 3 fail 3 fail
tests/unit/launcher-utils.test.ts 3 fail 3 fail
tests/unit/test-runner-cli.test.ts 2 fail 2 fail
tests/unit/db/sqlite-driver.test.ts 1 fail 1 fail
tests/isolated/monaco-language-ids.test.ts 1 fail 1 fail

tests/isolated/object-edit-declarations.test.ts (4), tests/isolated/agent-investigation-e2e.test.ts (2), tests/security/agent-statement-boundary.test.ts (1) and tests/integration/db/mongodb-provider.test.ts are also red on this machine. I did not compare those four against a clean tree, so I am not claiming they are unrelated, only that I did not introduce anything the six above did not already show. CI is the authority on them.

Coverage should be unchanged: the only src/ edit is a comment, and the new executable lines are in the test file.

…urce text

The guard that catches a SQLite object kind declared but left unmapped read
`SQLITE_OBJECT_KINDS` out of `sqlite.ts` with a `{ id: "..."` scrape that matches
a single-line entry only. A fifth kind written across two lines was therefore
absent from the guard's population AND from `COMPOSED_KIND_WORDS`, both sides
shrank together, and the guard passed over a kind nothing maps.

Measured against the same declaration, using the same pattern:

  today, unchanged                   scrape sees 4  -> guard passes
  a FIFTH kind wrapped               scrape sees 4  -> guard passes
  the same FIFTH kind on one line    scrape sees 5  -> guard fails on it

The guard now asks the constructed provider for `getCapabilities().objectKinds`,
the durable fix the array's own comment named. Capabilities are type-driven and
read off a provider that is never connected, so no socket is opened.

An entry that wraps can no longer hide a kind, which is what the new test pins,
alongside the unmapped-kind failure itself. The one-line-entries constraint and
its rationale leave the comment in `sqlite.ts`, since nothing reads that array as
text any more.
@cevheri cevheri added documentation Improvements or additions to documentation enhancement New feature or request labels Sep 19, 2026

@cevheri cevheri 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.

Thanks for this, and for the measurement table in the description. The core fix is right, and I verified it rather than took it on trust: with a fifth kind wrapped across two lines in SQLITE_OBJECT_KINDS, the provider-sourced guard fails where the old scrape passed (146 pass / 1 fail, and the failing test is the guard). The comment rewrite in sqlite.ts is accurate too, and the connectionless claim holds since SQLiteProvider loads its driver in connect(), not the constructor.

One change before merge, on the second test.

I ran that same mutation against it, and a kind declared but left unmapped is caught, and the declaration's formatting cannot hide it passes while the exact defect is present. Its population is Object.keys(composedMap("sqlite")) plus a literal "fifth_kind", so it never reaches the declaration or the provider, and what it ends up asserting is that filter filters. unmappedKindIds has no other caller either, so the docblock calling it "the guard's comparison" does not quite hold.

Could you replace it with a control plus a mutant over the real inputs, and drop the helper?

test("a fifth declared kind that nothing maps fails the guard, however its entry is laid out", async () => {
  const composed = composedMap("sqlite");
  const declaredIds = await declaredSqliteKindIds();

  // Control: the real declaration agrees, so the mutant below is the only difference.
  expect(composed).toEqual(Object.fromEntries(declaredIds.map((id) => [id, id])));
  // Mutant: one more declared kind, and the SAME comparison must stop holding.
  expect(composed).not.toEqual(Object.fromEntries([...declaredIds, "fifth_kind"].map((id) => [id, id])));
});

One small thing alongside it: the describe docblock still says "Source-level, because the agent side must not import a provider module", which no longer describes the sqlite arm now that it goes through the factory.

Everything else looks good to me, happy to approve once that lands.

@cevheri cevheri added the loop:needs-info Maintainer-loop task blocked on human-reviewed clarification label Sep 19, 2026
@sloemo01

Copy link
Copy Markdown
Contributor Author

Thanks for re-running the mutation. Both of those are right: that test's population never reached the declaration or the provider, so it was checking the filter, not the guard. Replacing it with your control plus the mutant over declaredSqliteKindIds() and the real composed map, dropping unmappedKindIds with it, and scoping the describe docblock's source-level remark to the postgres arm, which is the only arm still reading text. Pushing the commit now.

…nd guard

Review feedback: the previous test's population was built from composedMap
plus a literal, so it never reached the declaration or the provider and
only asserted the filter. Replaced with the control plus mutant over
declaredSqliteKindIds() and the real composed map, so the comparison
itself is what stops holding when a fifth kind appears. unmappedKindIds
is dropped with it, and the describe docblock no longer claims a
source-level read for the sqlite arm.
@sloemo01
sloemo01 requested a review from cevheri September 19, 2026 13:58
@codecov

codecov Bot commented Sep 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cevheri cevheri 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.

Both changes landed as described, and I re-ran the mutation against this head rather than reading the diff: with a fifth_kind entry wrapped across two lines in SQLITE_OBJECT_KINDS, the file goes from 147 pass to 145 pass / 2 fail, and the two failures are the identity guard and the new control-plus-mutant test. Clean head is 147 pass. The helper is gone and the describe docblock now scopes the source-level remark to the postgres arm.

Thanks for the quick turnaround.

@cevheri
cevheri merged commit ed69904 into libredb:main Sep 19, 2026
24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation enhancement New feature or request loop:needs-info Maintainer-loop task blocked on human-reviewed clarification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SQLite kind-vocabulary guard scrapes source text, so a kind can be declared and unmapped

2 participants