๐ Developer guide¶
Material for working on this repository itself, as opposed to using it to draft content: test running, the full source layout, and known gaps. See README.md for the user-facing Quickstart/Configuration/ Architecture docs and DOCKER.md for running a container; DOCKER-DEVELOPER.md here is this repo's own record of how the two images are built and verified.
๐งญ Table of contents¶
- Running tests
- Benchmarking the parser
- Writing a script that drives the enrichment layer
- Repository layout
- Figures and copyright
- Citation provenance
- Open questions and unbuilt features
๐งช Running tests¶
1 2 3 4 5 6 7 8 9 10 | |
tests/ covers both the corpus layer and chitragupta/enrich/*. The
enrich group's dependencies (docling, chromadb, bertopic,
sentence-transformers) are mocked via sys.modules for fast,
deterministic unit tests, so the dev-deps group alone is not
enough: the enrich group
(bash scripts/install_full_pipeline.sh python-deps) must already be
installed too, since
tests/test_bib_reader.py needs bibtexparser and the chitragupta/enrich/ test
modules need docling/chromadb/bertopic/sentence-transformers.
One module is the deliberate exception.
tests/test_enrich_real_libraries.py drives the real chromadb through
build_index()/search(), and asks the real
sentence_transformers/bertopic classes whether they still accept the
keywords the fakes accept. The fakes are faithful enough that the
expensive failure is the day they quietly stop being; that module is what
notices. It does not download an embedding model (its own docstring says
why), so it stays as fast and as offline as the rest.
A handful of tests run real dependencies end to end rather than mocking
them, and skip automatically when the dependency is absent:
tests/test_feature_workflows.py and the TestRenderReal/
TestExtractTextReal classes elsewhere probe for the
pdftotext, pandoc and lualatex binaries on PATH (and
test_feature_workflows.py for pdflatex, to build a fixture PDF);
tests/test_enrich_real_libraries.py probes for an importable library
instead, which is the same idiom against a different kind of absence.
โก Benchmarking the parser¶
bench/ measures what a full docling parse of the bib corpus costs on
a given machine, and is deliberately kept out of tests/: it takes a couple of
hours, needs real PDFs and a GPU, and answers a "how long / what's the
bottleneck" question rather than a pass/fail one. It is excluded from the
release zip for the same reason tests/ is.
- docs/PERFORMANCE.md: what each setting costs,
organised by setting. Ships in the release archive, unlike
bench/ - docs/PARALLELISM.md: parallel parse design (architecture, components, and the roadmap)
- bench/README.md covers how to run it, and what each switch measures
- bench/RESULTS.md
is the dated measurement record, newest last, with raw per-run data
in
bench/results/. Read its "Which sections are current" table first: several early conclusions were overturned by later runs and are kept, marked, rather than deleted - bench/PARALLELISM-PLAN.md records what is still unknown, and what to measure before changing it
What the measurements established, and what the code relies on:
- A
doclingparse is CPU-bound. A GPU alone buys little; turning OCR off ([parser].ocr = false, the default) buys more, because OCR competes for the same CPU the worker pool needs. - The pool gives each worker its own GPU rather than letting every
worker resolve to
cuda:0. worker_ceiling()clamps the pool size;bench/RESULTS.mdrecords what that clamp costs on the multi-GPU machine.- About 1% of documents come back with a different quotable passage on a re-parse, even with the same configuration. The artifact-by-artifact contract is in docs/ARCHITECTURE.md.
Read bench/RESULTS.md's "Which sections are current" table before
quoting a number from it: several early conclusions were overturned by
later runs and are kept, marked, rather than deleted. bench/ exists so
that the next conclusion is measured too.
๐ง Writing a script that drives the enrichment layer¶
chitragupta.enrich.docling_parse.parse_corpus and python -m
chitragupta.corpus sync both use
a worker pool when [parser].workers is above 1, and every start method
they can pick (forkserver or spawn; see [parser].start_method)
re-imports the calling program's __main__ in each worker. Any script of
your own that calls them must guard its top level:
1 2 | |
Without it, every worker re-runs the script on startup and the pool dies
with BrokenProcessPool. chitragupta/enrich/__main__.py and chitragupta/sync.py
are both guarded already; this only bites ad-hoc scripts, and it bites
immediately rather than subtly.
๐ Repository layout¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
๐ Figures and copyright¶
A source paper's figures are a reading aid, never draft content.
AGENTS.md states the rule and the one exception (the
verbatim digest); python -m chitragupta.draft figures <citekey> is the
supported way to look at them. Code here must not insert a source
image into content/drafts/.
๐ Citation provenance¶
python -m chitragupta.review provenance content/drafts/<slug>.md reports, for
every citation in a draft, what in the cited source supports it and
where, ordered worst match first. It writes
content/review/<the draft's path minus its suffix>.provenance.md plus
.tex/.pdf renders beside it. The report mirrors the draft's own place
under content/drafts/, the same rule rendered/ and dossiers/ follow
(config.mirrored_dir), and chitragupta/review/__init__.py owns that
contract for every review-layer command.
Run it directly rather than wrapping it in an enrichment stage: that
would have the enrichment layer importing the review layer, and would
make an advisory report wait on sync's write lock.
Advisory, not a gate, deliberately: matching is lexical, so it
cannot tell "the source doesn't say this" from "the source says it in
words I didn't recognise". citation_gate blocks because it checks
something exact (ledger membership); this reports because it doesn't.
Passage quality depends on what has been parsed. With the Docling stage
run, content/docling/<citekey>.passages.json supplies reading-ordered
paragraphs and the report quotes them. Without it, pdftotext output is
used and the report gives a page number without quoting: on a
two-column paper that text splices two columns onto every line, so any
excerpt would be a collage of two arguments.
Full design rationale, including the measurements behind those choices: docs/CITATION-PROVENANCE.md.
โ Open questions and unbuilt features¶
Running this pipeline on a schedule is supported: a rotating
logs/pipeline.log, shared by the
corpus and enrichment layers (see chitragupta/logging_setup.py), a pages/s
throughput figure, exit codes an unattended caller can branch
on, and worked cron and systemd units in
docs/CLI.md, including the
absolute-interpreter-path detail that cron's minimal environment
requires.
One blocker is left, and it is not a coding task. With no continuous
auto-export, bibliography.bib is a manual, point-in-time snapshot: a
scheduled sync re-reads whatever was last exported, so it keeps the
corpus consistent with the bib file but cannot keep the bib file
consistent with your reference manager. A schedule watching only its
mtime does nothing until a human re-exports. Closing that properly means
either a Zotero auto-export plugin (outside this repo) or accepting that
the export stays a deliberate human step.
๐ซ content/topics.json has no drafting consumer¶
chitragupta/enrich/topic_model.py writes it. Only corpus discover
(chitragupta/discover/_data.py, for its c-TF-IDF terms) and sync's residue
report read it; no genre skill does. survey-writer groups themes by
judgement and says so explicitly ("With a small corpus there's no BERTopic
step"). That is defensible today: clustering is whole-corpus, so assignments
are not stable between runs, and on a small corpus every document legitimately
lands in the outlier topic. If it is ever wired in, survey-writer's "Cluster
by judgment" step is the seam, gated on the file existing and on there being
non--1 assignments, the same shape the existing skills use to gate on
content/chroma/.