zettelkasten.grounding¶
zettelkasten.grounding ¶
Mechanical grounding checks for extracted notes.
The extraction protocol asks the agent to back load-bearing notes with verbatim
quote notes and to never fabricate a quote (see _EVIDENCE_PROTOCOL in
:mod:zettelkasten.server). That discipline used to be prompt-only. This module
makes it enforceable: given a quote body and the source's own text (the cached
full text plus any saved Zotero highlights), it decides whether the quote is
actually present in the source and, when it can, recovers the page it sits on.
Design (deterministic, no LLM — mirrors the engine/agent split in backfill.py):
- A quote is grounded when its text appears in the source. PDF text extraction is
noisy (line-break hyphenation, ligatures, smart quotes, runaway whitespace), so
matching is done on an aggressively normalized form and tolerates small drift
via a fuzzy fallback above a high floor.
- Matching against a Zotero highlight is preferred: the highlight is curated and
page-labeled, so it yields the page locator the flat full-text cache cannot.
- When a quote cannot be verified the result carries the closest passage actually
found in the source (near_miss) so the caller can hand the agent the real
text to copy instead of its invented version.
normalize_for_match ¶
Collapse a string to a comparison form robust to PDF extraction noise.
Lowercases, removes soft hyphens / zero-width characters, folds common
ligatures and smart quotes to ASCII, de-hyphenates words split across a line
break (exam-\nple -> example), and collapses all whitespace to single
spaces. The result is what every match in this module compares against.
Source code in zettelkasten/grounding.py
strip_reader_note ¶
Return the verbatim portion of a quote body, dropping any reader-note tail.
backfill._quote_body appends \n\n_Reader note: ..._ for the reader's
own comment; only the quoted text before it should be checked against the
source.
Source code in zettelkasten/grounding.py
strip_math_delimiters ¶
Return the inner LaTeX of an equation body, dropping a wrapping delimiter.
Source code in zettelkasten/grounding.py
verify_equation ¶
verify_equation(body: str, *, page: str | int | None = None, fulltext: str | None = None, context: str | None = None) -> dict[str, Any]
Decide whether an equation body is well-formed (render-valid) LaTeX.
Unlike :func:verify_quote this performs NO verbatim source match — it
validates that the LaTeX is structurally sound (the render-validity guard) and
records a best-effort page locator. It never rejects on a locator miss.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
str
|
The equation note body (LaTeX, optionally delimiter-wrapped). |
required |
page
|
str | int | None
|
A claimed page locator (e.g. from a Zotero annotation or the
reader). Echoed back and marked |
None
|
fulltext
|
str | None
|
The source's cached text, used only for the advisory context match (never for validating the math itself). |
None
|
context
|
str | None
|
Optional surrounding caption/label text (e.g. |
None
|
Returns a dict with
valid (bool — render-validity), method
(render-valid | empty | mojibake | unbalanced-braces |
unbalanced-delimiters | unbalanced-environments), reason
(str, empty when valid), page (str | None), and locator
(annotation | context-matched | claimed | none).
Source code in zettelkasten/grounding.py
400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 | |
verify_figure ¶
verify_figure(snapshot: dict[str, Any] | None, *, page: str | int | None = None, fulltext: str | None = None, caption: str | None = None) -> dict[str, Any]
Decide whether a figure note is grounded by a resolvable snapshot.
The visual analogue of :func:verify_equation: it performs NO verbatim text
match. A figure (chart/diagram/plot) is grounded when its snapshot
pointer resolves to a committed image on disk — the visual ground-truth — and
inferred otherwise. Like verify_equation this is ADVISORY: a missing or
unresolvable snapshot lowers the grounding but NEVER rejects the note (unlike
a quote, a figure has no source text to reproduce verbatim), and a locator
miss is never a gate.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
snapshot
|
dict[str, Any] | None
|
The figure's snapshot pointer ( |
required |
page
|
str | int | None
|
A claimed page locator (e.g. from a Zotero annotation or the
reader). Echoed back and marked |
None
|
fulltext
|
str | None
|
The source's cached text, used only for the advisory caption match (never for deciding whether the figure is grounded). |
None
|
caption
|
str | None
|
Optional figure caption/label text (e.g. |
None
|
Returns a dict with
grounded (bool — the snapshot image resolves), method
(snapshot-resolved | snapshot-missing | no-snapshot),
reason (str, empty when grounded), page (str | None), and
locator (context-matched | claimed | none).
Source code in zettelkasten/grounding.py
verify_quote ¶
verify_quote(body: str, fulltext: str | None, annotations: list[dict[str, Any]] | None = None, *, fuzzy_floor: float = DEFAULT_FUZZY_FLOOR) -> dict[str, Any]
Decide whether a quote body is grounded in the source's own text.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
body
|
str
|
The quote note body (the verbatim passage, possibly with a reader note appended). |
required |
fulltext
|
str | None
|
The source's cached/extracted full text, or |
required |
annotations
|
list[dict[str, Any]] | None
|
Saved Zotero highlights (each |
None
|
fuzzy_floor
|
float
|
Minimum similarity for the fuzzy fallback to count as verified. |
DEFAULT_FUZZY_FLOOR
|
Returns a dict with
verified (bool), method (annotation | exact | fuzzy |
none | unavailable), score (0..1), page (str | None,
only from an annotation match), near_miss (str | None, the closest
passage actually found when not verified), and confirmed_source_text
(str | None). The last is the text the verifier actually PROVED is present
in the source (the matched span, under this module's normalization): for an
exact match, the SOURCE tokens at the matched offset (the needle's token
sequence confirmed as a contiguous, word-aligned sublist of the haystack's
tokens — equal to the needle for a genuine verbatim match, derived from the
source for safety); for an annotation match, the OVERLAP that lies inside
the real Zotero highlight (the matched highlight span when the needle's tokens
are a contiguous sublist of the highlight's, else the whole highlight in the
superset case) — a source-derived span, NOT the raw superset body. Containment
is WORD/NUMBER-ALIGNED (token-subsequence), so a character-substring that carves
across word boundaries ("2000 dollars" inside "12000 dollars") is
rejected, never confirmed. Numeric tokens preserve SIGN and UNIT (-5 vs
5, 5% vs 5 are DISTINCT tokens), so a sign/magnitude/unit flip can
never match and the confirmed span is itself sign/unit-preserving — a faithful
quote of a -5 source confirms ...-5..., not ...5.... It is None
for fuzzy (the accepted window may
diverge from the supplied body) and for none / unavailable. Callers
that license downstream text off a quote MUST use this source-derived span,
never the caller-supplied body (which may carry a reader-note tail,
a fuzzy-tolerated drift, or an annotation-superset fabrication).
Source code in zettelkasten/grounding.py
552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 | |