Skip to content

Guard ungrounded answers in sec ask with a grounded flag - #356

Merged
sroussey merged 1 commit into
mainfrom
claude/github-issue-911-32s409
Sep 8, 2026
Merged

Guard ungrounded answers in sec ask with a grounded flag#356
sroussey merged 1 commit into
mainfrom
claude/github-issue-911-32s409

Conversation

@sroussey

@sroussey sroussey commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a grounded boolean field to AskTaskOutput and prevents the model's unsourced answers from being presented to users when no filing text was retrieved. When retrieval fails, users now see a helpful message directing them to build or expand the index instead of confident-sounding fiction.

Changes

  • Added grounded field to AskTaskOutput: A boolean that explicitly signals whether the answer was backed by retrieved filing text, so callers (including --json consumers) don't have to infer it from an empty references array.

  • Introduced ASK_MIN_SCORE constant: States the score floor (0.3) for a chunk to count as a match, rather than inheriting the library default. This makes the grounding decision explicit and decoupled from upstream changes.

  • Added ungroundedAnswer() helper: Returns context-aware guidance when retrieval fails:

    • If the index is empty: directs users to run sec index or sec update documents
    • If chunks exist but none matched: shows the chunk count and suggests rewording or widening scope flags
  • Modified AskTask.execute():

    • Passes minScore: ASK_MIN_SCORE to AiChatWithKbTask
    • Checks if references.length === 0 after retrieval
    • Returns the ungrounded answer message instead of the model's text when nothing was retrieved
    • Sets grounded: false for ungrounded responses, grounded: true otherwise
  • Updated CLI rendering in ask.ts: When grounded is false, prints the guidance message without model attribution and suggests the sec index command as the next step, rather than treating it as a normal answer.

  • Added comprehensive test suite: 140 lines of tests covering the grounding logic, scope flag handling, score floor, and the distinction between empty and unmatched indexes.

Implementation notes

The guard is placed in the task layer rather than the renderer, so both CLI and --json consumers get the same refusal. This prevents a 350M local model (the default path) from confidently answering questions about companies whose filings the database doesn't contain.

https://claude.ai/code/session_01LWp6Z6wvAPDaDCjAFcTSj6

… retrieved

`sec ask` is the command the re-founding is named for, and on an empty or
non-matching index it was neither retrieval-grounded nor about filings.
Reproduced against a freshly `db setup` database: eleven sentences of
confident, unsourced prose putting Apple's FY2023 revenue at ~$383bn,
`references: []`, exit 0. It is the first thing a new user does after
`sec setup`.

The invariant was expressed once, in a sentence in the system prompt. The
default path is a 350M local model — deliberately, so `ask` works with no
API key — and it does not honour that sentence. A model instruction is a
request; this is the guard.

Zero references is now a code outcome in `AskTask.execute`, not the
renderer: a check in the CLI group would leave every `--json` consumer
with the fabrication. The output carries a `grounded` field, so a caller
reads the discriminator instead of inferring that an answer is fiction
from an array's length.

The message names the recovery, and which recovery depends on the state:
`kb.chunkCount()` separates "nothing is indexed" (run `sec index`, or
`sec update documents` if nothing is converted) from "indexed, but nothing
matched", where telling the operator to build the index would be wrong.

The weak-match case comes out of the same mechanism rather than a second
threshold: `minScore` is now stated here (0.3) instead of inherited from
`AiChatWithKbTask`'s default, so a corpus holding only irrelevant chunks
produces no references and takes the same path.

`AskTask` had no test file at all — 164 lines, untested. It has one now,
covering both ungrounded arms, the grounded passthrough, the stated floor,
and every scope flag's contribution to the prompt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LWp6Z6wvAPDaDCjAFcTSj6
@sroussey
sroussey merged commit c9b2c2c into main Sep 8, 2026
1 check passed
@sroussey
sroussey deleted the claude/github-issue-911-32s409 branch September 8, 2026 15:20
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.

2 participants