Skip to content

feat(recall): a recalled document segment names its source file and version (#713, slice 2) - #899

Merged
github-actions[bot] merged 2 commits into
mainfrom
fix/713-recall-document-source
Nov 6, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
fix/713-recall-document-source

Conversation

@cdeust

@cdeust cdeust commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Summary

Refs #713 (slice 2 of 4). A recall hit written by ingest_document now names the document it came from: document_sources lists the source file, the content version and the adapter kind.

Root cause. ingest_document already tagged every summary/section memory with doc-ingest:<kind>:<source>@<version> (document_provenance_dedup_tag). But recall returned only the raw tag list, so a caller had to parse tags to trace a segment back to a file version. No code read that tag back.

Change.

  • core/document_model.parse_document_provenance_dedup_tag inverts the tag. The kind never holds :. The version is a hex digest, so the last @ starts it. A source path may hold either character (C:\docs\team@corp\guide.docx round-trips). Any other or malformed tag gives None.
  • core/document_reference.annotate_document_sources (pure) adds document_sources to each hit that has such a tag. A non-document hit gets no key. The value is always a list, so a hit whose tags name two versions shows both instead of an arbitrary pick.
  • recall_helpers.annotate_source_attribution, the read-side provenance surfacing recall already calls, now also calls it. I deliberately put the call outside recall._handler_impl: touching that function would have put all of it in scope for the ADR-1122 mutation gate. outputSchema documents the field.
  • In the same function, mem.get("content", "") or "" becomes mem.get("content"). recall_confabulation_risk already normalizes content or "". The redundant defaults produced 4 equivalent mutants once the function was in scope.

Remaining slices (listed on the issue): trace a derived claim or wiki synthesis to its source segments, with missing references made explicit; and when a source is corrected or deleted, report the affected references without inventing new locations.

Type of change

  • New feature (non-breaking change which adds functionality)

Test plan

  • tests_py/handlers/test_recall_document_sources.py (new, 5 tests):
    • end to end on SQLite: ingest_document then recall, and the hit carries {source, version, source_kind}. It fails without the change (KeyError: 'document_sources').
    • tag round trip with : and @ in the path; 8 malformed or foreign tags give None; no key on a non-document hit; two versions both listed.
  • pytest tests_py/handlers tests_py/core tests_py/server: 5549 passed, 242 skipped, 1 failed. The failure is test_wiki_coverage_dashboard.py::test_unreadable_page_is_skipped_not_fatal, and it fails the same way on origin/main in this container: the container runs as root, so chmod does not make the page unreadable.
  • Mutation gate (ADR-1122), run locally with scripts/ci_mutation_diff.py's plan and run_job: document_model.py rc 0, document_reference.py rc 0, recall_helpers.py rc 0. Before the extra malformed-tag cases, 3 survivors in the validity check; before the content simplification, 4 equivalent survivors.
  • ruff check and ruff format --check (ruff 0.17.3) pass on mcp_server, tests_py/handlers and tests_py/core. python scripts/check_craftsmanship.py: OK.

Audit notes

  • No ranking change: the field is added after ordering and before budgeting. No retrieval benchmark is affected.
  • Not verified here: the PostgreSQL-gated tests (no PostgreSQL in this session), the FlashRank reranker (the model download is refused by the proxy) and pyright (not run).

Coding-standards compliance

  • Layer dependency direction preserved (core only imports core; handler imports core).
  • No new file over 300 lines and no new function over 40 lines. check_craftsmanship.py OK.
  • No new numeric constant.

Breaking changes

None: the field is additive and absent on non-document hits.

Reviewer checklist

  • CHANGELOG.md updated (Unreleased / Added).

Generated by Claude Code

…ersion (#713, slice 2)

ingest_document tags every summary/section memory with
doc-ingest:<kind>:<source>@<version>, but recall returned only the raw tags,
so a caller had to parse them to trace a segment back to a file version.
parse_document_provenance_dedup_tag inverts the tag (a source path may hold
':' and '@'), and annotate_source_attribution now adds document_sources to
every hit written by ingest_document. Non-document hits get no key.

Refs #713

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018kyZYt2w35ZfYtNeiy6Ey9
@cdeust
cdeust marked this pull request as ready for review November 6, 2026 14:46
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T14:47:49.643388Z 269d23c Draft marked ready
🔒 Security Review ✅ Completed 2026-11-06T14:53:55.093084Z 269d23c Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

Claude finished @cdeust's task in 2m 57s —— View job


PR #899 review (1 of 3, ADR-1119)

No Blockers. I posted 3 Notes:

  • a doc-ingest: tag from remember can produce a document_sources entry;
  • the memory_id and exact-lookup paths do not add the field;
  • the comment at recall.py:656-660 is stale, and the type hint on recall_confabulation_risk should be str | None.

Verdict: approve

@claude

claude Bot commented Nov 6, 2026

Copy link
Copy Markdown

Review 1 of 3 (ADR-1119): PR #899 at 269d23c

I ran all five reviews (code quality, performance, test coverage, documentation accuracy, security) and checked their findings against the code. I found no Blockers.

  • Parser: parse_document_provenance_dedup_tag is correct for every tag ingest_document can write. _resolve_format only returns docx or confluence-export, so the kind has no :. content_version is sha256(...).hexdigest()[:16], so the version has no @.
  • Layers: core/document_reference imports only core, and the handler imports core.
  • Ranking: none. The annotation runs after ordering and the max_results cut.
  • content change: passing mem.get("content") without a default is safe at runtime, because recall_confabulation_risk normalizes it with content or "".

Notes, no action before merge

  • The doc-ingest: prefix is not reserved, so remember(tags=["doc-ingest:docx:/x.docx@abcd"]) gets a document_sources entry. The schema says the field is "Present only on a segment written by ingest_document" (recall.py:105). Nothing consumes the field, so the risk is low. A fix is to also require mem["source"] == "ingest_document:<path>" (redacted, matching how ingest_document_writers.py:113 compares it), or to reword the schema text.
  • document_sources is only added on query recall. The memory_id fetch and bounded_exact paths (recall.py:537-570) do not add it, so following up on a truncated document hit by id loses the source and version. source_attribution already has the same gap.
  • The comment at recall.py:656-660 still says annotate_source_attribution writes two keys per result; it now writes up to three. Separately, recall_confabulation_risk(content: str, ...) now receives None, so its hint should be str | None to keep pyright clean.

Verdict: approve

cdeust commented Nov 6, 2026

Copy link
Copy Markdown
Owner Author

The review approved this PR with no Blocker, so I am not pushing for its three Notes (ADR-1119: only a Blocker justifies a push). Here is what happens to each one:

  • doc-ingest: tag set by remember. The tag is not reserved, so a manual remember with such a tag gets a document_sources entry. Nothing consumes the field yet. I will handle this in the next slice of feat: preserve source segments and document versions through memory and claim retrieval #713, the one that traces a claim back to its segments and will consume the field. There, the annotation will also require source == "ingest_document:<path>".
  • memory_id and bounded_exact paths. These paths do not add the field. source_attribution already has the same gap. This goes into the same slice, so that all three recall paths get the field together.
  • Stale comment at recall.py:656-660 and str | None hint on recall_confabulation_risk. These are wording and type-hint fixes with no effect at runtime. mem.get returns Any, so pyright reports nothing. They will go into the next code push on this area.

Generated by Claude Code

@github-actions
github-actions Bot merged commit c4c434e into main Nov 6, 2026
20 of 28 checks passed
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