reference-audit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. reference_audit/__init__.py +8 -0
  2. reference_audit/alignmentcheck.py +136 -0
  3. reference_audit/bookcheck.py +98 -0
  4. reference_audit/cache/__init__.py +1 -0
  5. reference_audit/cache/db.py +108 -0
  6. reference_audit/cache/store.py +128 -0
  7. reference_audit/cli.py +190 -0
  8. reference_audit/config.py +179 -0
  9. reference_audit/fieldcheck.py +599 -0
  10. reference_audit/inputs.py +96 -0
  11. reference_audit/llm/__init__.py +6 -0
  12. reference_audit/llm/client.py +85 -0
  13. reference_audit/llm/prompts.py +206 -0
  14. reference_audit/llm/schemas.py +38 -0
  15. reference_audit/matching/__init__.py +6 -0
  16. reference_audit/matching/adjudicate.py +63 -0
  17. reference_audit/matching/features.py +185 -0
  18. reference_audit/matching/names.py +238 -0
  19. reference_audit/matching/pool.py +344 -0
  20. reference_audit/matching/sameobject.py +150 -0
  21. reference_audit/matching/scoring.py +75 -0
  22. reference_audit/matching/verdict.py +56 -0
  23. reference_audit/matching/webcheck.py +189 -0
  24. reference_audit/models.py +412 -0
  25. reference_audit/parsing/__init__.py +1 -0
  26. reference_audit/parsing/bib.py +392 -0
  27. reference_audit/parsing/context.py +55 -0
  28. reference_audit/parsing/entities.py +22 -0
  29. reference_audit/parsing/identifiers.py +165 -0
  30. reference_audit/parsing/latex.py +83 -0
  31. reference_audit/parsing/tex.py +159 -0
  32. reference_audit/pdf/__init__.py +16 -0
  33. reference_audit/pdf/embedded.py +117 -0
  34. reference_audit/pdf/grobid.py +234 -0
  35. reference_audit/pdf/tei.py +470 -0
  36. reference_audit/pipeline.py +1692 -0
  37. reference_audit/report.py +330 -0
  38. reference_audit/sources/__init__.py +6 -0
  39. reference_audit/sources/arxiv.py +81 -0
  40. reference_audit/sources/base.py +54 -0
  41. reference_audit/sources/clickhouse.py +575 -0
  42. reference_audit/sources/crossref.py +90 -0
  43. reference_audit/sources/dblp.py +140 -0
  44. reference_audit/sources/google_books.py +101 -0
  45. reference_audit/sources/http.py +260 -0
  46. reference_audit/sources/normalize.py +464 -0
  47. reference_audit/sources/openalex.py +93 -0
  48. reference_audit/sources/openlibrary.py +158 -0
  49. reference_audit/sources/publisher.py +186 -0
  50. reference_audit/sources/registry.py +117 -0
  51. reference_audit/sources/render.py +115 -0
  52. reference_audit/sources/semantic_scholar.py +68 -0
  53. reference_audit/sources/titlewords.py +31 -0
  54. reference_audit/sources/web.py +256 -0
  55. reference_audit/versioning.py +125 -0
  56. reference_audit-0.1.0.dist-info/METADATA +715 -0
  57. reference_audit-0.1.0.dist-info/RECORD +60 -0
  58. reference_audit-0.1.0.dist-info/WHEEL +4 -0
  59. reference_audit-0.1.0.dist-info/entry_points.txt +2 -0
  60. reference_audit-0.1.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,179 @@
1
+ """Runtime configuration for the reference auditor.
2
+
3
+ All settings load from the environment / `.env` (pydantic-settings). Secrets live only in
4
+ `.env` (git-ignored); never hard-code keys. The LLM model defaults to `gpt-6-luna` per
5
+ the README but is configurable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Literal
12
+
13
+ from pydantic import AliasChoices, Field
14
+ from pydantic_settings import BaseSettings, SettingsConfigDict
15
+
16
+
17
+ class AuditConfig(BaseSettings):
18
+ """Central config: LLM, data-source credentials, matching thresholds, cache."""
19
+
20
+ model_config = SettingsConfigDict(
21
+ env_file=".env",
22
+ env_file_encoding="utf-8",
23
+ extra="ignore",
24
+ protected_namespaces=(),
25
+ )
26
+
27
+ # --- LLM (OpenAI SDK) ---
28
+ model: str = "gpt-6-luna"
29
+ # None ⇒ omit `temperature` and use the model default. gpt-6-luna rejects any value but its default
30
+ # (1), so a fixed 0.0 would fail every call; set LLM_TEMPERATURE=0 for models that accept it.
31
+ llm_temperature: float | None = None
32
+ openai_api_key: str | None = Field(default=None, alias="OPENAI_API_KEY")
33
+ openai_base_url: str | None = Field(default=None, alias="OPENAI_BASE_URL")
34
+
35
+ # --- Data-source credentials (all already present in .env) ---
36
+ crossref_mailto: str = Field(
37
+ default="reference-audit@example.org", alias="CROSSREF_MAILTO"
38
+ )
39
+ s2_api_key: str | None = Field(default=None, alias="S2_API_KEY")
40
+ ncbi_api_key: str | None = Field(default=None, alias="NCBI_API_KEY")
41
+ nasa_ads_api_key: str | None = Field(default=None, alias="NASA_ADS_API_KEY")
42
+ core_api_key: str | None = Field(default=None, alias="CORE_API_KEY")
43
+ unpaywall_email: str | None = Field(
44
+ default=None, alias="PAPER_SEARCH_MCP_UNPAYWALL_EMAIL"
45
+ )
46
+ openlibrary_email: str | None = Field(default=None, alias="OPENLIBRARY_EMAIL")
47
+ google_books_api_key: str | None = Field(default=None, alias="GOOGLE_BOOKS_API_KEY")
48
+
49
+ # --- Source backend: where Semantic Scholar, OpenAlex and DBLP are read from ---
50
+ # "api": their public APIs (rate-limited; needs no setup). "clickhouse": a local ClickHouse mirror
51
+ # of the three databases (no rate limits; must carry the full-text title indexes, see README).
52
+ # Crossref, arXiv, Open Library, Google Books and the web/publisher fetches are API-only.
53
+ source_backend: Literal["api", "clickhouse"] = Field(
54
+ default="api", validation_alias=AliasChoices("SOURCE_BACKEND", "source_backend")
55
+ )
56
+ clickhouse_host: str = Field(default="127.0.0.1", alias="CLICKHOUSE_HOST")
57
+ clickhouse_port: int = Field(default=8123, alias="CLICKHOUSE_PORT")
58
+ clickhouse_user: str = Field(default="default", alias="CLICKHOUSE_USER")
59
+ clickhouse_password: str = Field(
60
+ default="",
61
+ validation_alias=AliasChoices("CLICKHOUSE_DEFAULT_USER_PASSWORD", "CLICKHOUSE_PASSWORD"),
62
+ )
63
+ clickhouse_s2_db: str = Field(
64
+ default="s2ag", validation_alias=AliasChoices("CLICKHOUSE_S2_DB", "CLICKHOUSE_DB")
65
+ )
66
+ clickhouse_openalex_db: str = Field(default="openalex", alias="CLICKHOUSE_OPENALEX_DB")
67
+ clickhouse_dblp_db: str = Field(default="kb", alias="CLICKHOUSE_DBLP_DB")
68
+ clickhouse_concurrency: int = 8 # queries in flight against the local server
69
+ clickhouse_query_timeout: float = 60.0 # seconds; a slower query is an error, never "not found"
70
+
71
+ # --- Matching thresholds (calibrated against the pilot; see plan risk #3) ---
72
+ title_accept: float = 0.92 # auto_accept title floor (entry has an identifier)
73
+ title_backfill: float = 0.95 # stricter title floor when entry has NO identifier
74
+ author_accept: float = 0.80 # auto_accept author-overlap floor
75
+ composite_reject: float = 0.40 # auto_reject ceiling
76
+ prefix_trap_tail_jaccard: float = 0.34 # V3: tail-token Jaccard below this ⇒ distinct
77
+ author_set_distinct_jaccard: float = 0.60 # V4: author-set Jaccard below this ⇒ distinct
78
+
79
+ # --- LLM adjudication ---
80
+ use_llm: bool = True # CLI --no-llm sets this False (deterministic CI)
81
+ llm_concurrency: int = 8
82
+ llm_max_candidates: int = 8 # cap CAN_CORRESPOND calls per entry (cost control)
83
+
84
+ # --- Step 3: field correctness ---
85
+ check_fields: bool = True # verify each field of an exactly-one match is correct
86
+ # A cited author list that names only some of the work's authors without saying so ('and
87
+ # others'): `ignore` it, `warn` (an `uncertain` author finding) or report an `error`. Many
88
+ # bibliographies shorten long lists; HALLMARK counts an unmarked omission as a hallucination.
89
+ # CLI --partial-authors.
90
+ partial_authors: Literal["ignore", "warn", "error"] = Field(
91
+ default="warn", alias="PARTIAL_AUTHORS"
92
+ )
93
+
94
+ # --- Citation alignment (advisory; opt-in) ---
95
+ # Compare each citing context against the cited work's abstract. Off by default: it needs the LLM
96
+ # and an abstract, and adds a per-citation cost. CLI --check-citations enables it.
97
+ check_alignment: bool = False
98
+
99
+ # --- Web artifacts (URL-only @misc): HTML-metadata check before the LLM fallback ---
100
+ web_title_accept: float = 0.85 # page meta-title vs cited-title floor for a deterministic confirm
101
+
102
+ # --- Web artifacts: headless rendering of client-side-rendered (single-page-app) pages ---
103
+ web_render_enabled: bool = True # render SPA shells in a headless browser before judging
104
+ web_render_browser_path: str | None = Field(default=None, alias="WEB_RENDER_BROWSER_PATH")
105
+ web_render_timeout: float = 30.0 # seconds before a render is treated as a (retryable) error
106
+ web_render_virtual_time_ms: int = 15000 # JS virtual-time budget given to the headless browser
107
+
108
+ # --- PDF input (GROBID) ---
109
+ # The service is NOT managed by this package. Point this at a running instance; a PDF input with
110
+ # no reachable GROBID is a reported failure, never a silently empty reference list.
111
+ # podman run -d --replace --name grobid -p 8070:8070 docker.io/grobid/grobid:0.9.1-crf
112
+ # Intentionally NOT declared with alias="GROBID_URL": a non-aliased field already reads its
113
+ # upper-cased env var, whereas an aliased one silently ignores by-name construction here
114
+ # (`populate_by_name` is not set), which would make AuditConfig(grobid_url=...) a no-op in tests.
115
+ grobid_url: str = "http://localhost:8070"
116
+ # A PDF that embeds the BibTeX records of its cited works (`pdf.embedded`, e.g. one made with the
117
+ # AI4X template) is audited from them, without GROBID. CLI --ignore-embedded turns this off.
118
+ use_embedded_references: bool = True
119
+
120
+ # --- Cache / pipeline ---
121
+ cache_path: Path | None = None # default: <bib_dir>/.reference_audit/cache.db
122
+ # Bump when thresholds/prompts/rules change, or when a new verdict-producing path lands.
123
+ # 0.16: PDF input via GROBID — TEI entry-type inference changes source routing, and an entry with
124
+ # neither a title nor an identifier is now reported unresolved instead of being searched on
125
+ # (a title-less query could previously score an arbitrary paper as a match).
126
+ # 0.17: default LLM model gpt-5.4-mini → gpt-6-luna, and temperature is no longer pinned to 0.0
127
+ # (model default unless LLM_TEMPERATURE is set) — adjudication behavior changes.
128
+ # 0.18: DBLP is queried through its SPARQL endpoint (QLever word search, shortest title first)
129
+ # instead of the search API, which now serves automated clients a bot-challenge page. DBLP's
130
+ # candidates and their ranking change, and DBLP no longer errors on every query, so entries
131
+ # that were left unresolved can now reach a verdict.
132
+ # 0.19: a second source backend — Semantic Scholar, OpenAlex and DBLP read from a local ClickHouse
133
+ # mirror (full-text title index, all title words required, shortest title first). The
134
+ # verdict cache is now also keyed by backend.
135
+ # 0.20: the local ClickHouse title search retries with each word left out when no all-words hit is
136
+ # a near-exact title, and OpenAlex rows gain their best open-access location as a version
137
+ # link (candidates change). A cited DOI the matched work does not carry is checked and
138
+ # reported as a `doi` field finding; an unresolved entry records why; a `none` on a local
139
+ # snapshot that may predate the work carries a coverage caveat.
140
+ # 0.21: HTML character references (`d&apos;Amore`, `&amp;`) are decoded in .bib fields and in
141
+ # author/title normalization, so author checks and title scores change for such entries.
142
+ # 0.22: the HALLMARK run's work list. Field checks compare against the version the entry cites
143
+ # (the pooled record keeps its member records); a pooled venue is never a preprint
144
+ # server while a member names the venue; pooling no longer fuses a paper with a journal
145
+ # extension that has other authors; the field-check LLM sees the matched work, not the
146
+ # entry, as context; authors are checked person by person against every source, as an
147
+ # `author` field finding (with the partial-list option); the cited DOI is checked with the
148
+ # doi.org Handle API, which answers 404 for an unregistered prefix.
149
+ # 0.23: regressions the 0.22 run exposed. A field's canonical value is the one most sources of the
150
+ # cited version agree on (an OpenAlex title defect won on source rank); a pooled record's
151
+ # author list comes from the most reliable source, not the citation-richest (S2's defective
152
+ # list made the LLM reject a real paper); members are deduplicated; given-name forms
153
+ # ('Tim' / 'Timothy') and dropped middle initials match; the field-check prompt calls a
154
+ # different journal or conference series an error (the LLM hedged on ACL vs ICLR).
155
+ # 0.24: bibtexparser 2. Field values are read by `parsing.bib` itself and LaTeX is decoded with
156
+ # pylatexenc (`parsing.latex`), so some values change: `\url{x}` is `x` (v1: `r̆lx`),
157
+ # `\textit{X}` is `X`, `\href{u}{X}` is `X <u>`. An undefined `@string`, a duplicate field or
158
+ # an undecodable value now makes only that entry unparsed (v1 failed the whole file on an
159
+ # undefined `@string` and kept the first of duplicate fields); commented twins are read
160
+ # from the uncommented text.
161
+ # 0.25: HTML character references in source values are decoded (`SourceRecord`), as they already
162
+ # were in `.bib` values: a Crossref title "Allocation &amp; Regularization" no longer folds
163
+ # to the word "amp", so it is settled by rule instead of going to the field-check LLM, and
164
+ # the canonical value is reported decoded.
165
+ # 0.26: a cited OpenAlex Work id is looked up on its own even when the entry also carries a DOI or
166
+ # arXiv id (OpenAlex was queried by those instead, so the Work id was never checked), and
167
+ # can then be pinned as the match; a cited DOI that is another Work is reported by the
168
+ # cited-DOI check. A cited Work id that resolves to a different work, to nothing, or could
169
+ # not be looked up is now reported as an issue, whatever the verdict.
170
+ pipeline_version: str = "0.26"
171
+
172
+ def llm_enabled(self) -> bool:
173
+ return self.use_llm and bool(self.openai_api_key)
174
+
175
+ def resolved_mailto(self) -> str:
176
+ """Crossref polite-pool contact; fall back to the Unpaywall email if set."""
177
+ if self.crossref_mailto and self.crossref_mailto != "reference-audit@example.org":
178
+ return self.crossref_mailto
179
+ return self.unpaywall_email or self.crossref_mailto