primarysource 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.
- primarysource/__init__.py +5 -0
- primarysource/bbf/__init__.py +56 -0
- primarysource/bbf/_xml.py +63 -0
- primarysource/bbf/catalogue.json +777 -0
- primarysource/bbf/catalogue.py +137 -0
- primarysource/bbf/cli.py +79 -0
- primarysource/bbf/datamodel.py +281 -0
- primarysource/bbf/importer.py +151 -0
- primarysource/bbf/library.py +380 -0
- primarysource/bbf/server.py +72 -0
- primarysource/budget.py +63 -0
- primarysource/c2sp/__init__.py +53 -0
- primarysource/c2sp/_fetch.py +5 -0
- primarysource/c2sp/catalogue.json +231 -0
- primarysource/c2sp/catalogue.py +95 -0
- primarysource/c2sp/cli.py +62 -0
- primarysource/c2sp/importer.py +90 -0
- primarysource/c2sp/library.py +309 -0
- primarysource/c2sp/markdown.py +31 -0
- primarysource/c2sp/server.py +68 -0
- primarysource/cavp/__init__.py +42 -0
- primarysource/cavp/cli.py +84 -0
- primarysource/cavp/data/vectors.toml +47 -0
- primarysource/cavp/importer.py +156 -0
- primarysource/cavp/model.py +279 -0
- primarysource/cavp/parse.py +269 -0
- primarysource/cavp/paths.py +17 -0
- primarysource/cavp/server.py +67 -0
- primarysource/cavp/tools.py +169 -0
- primarysource/cli.py +107 -0
- primarysource/cursor.py +82 -0
- primarysource/cve/__init__.py +5 -0
- primarysource/cve/__main__.py +6 -0
- primarysource/cve/cli.py +144 -0
- primarysource/cve/collection.py +20 -0
- primarysource/cve/config.py +58 -0
- primarysource/cve/errors.py +16 -0
- primarysource/cve/kernel.py +232 -0
- primarysource/cve/mcp.py +139 -0
- primarysource/cve/parse.py +318 -0
- primarysource/cve/py.typed +0 -0
- primarysource/cve/service.py +760 -0
- primarysource/cve/store.py +214 -0
- primarysource/cve/sync.py +438 -0
- primarysource/cve/views.py +304 -0
- primarysource/debpolicy/__init__.py +16 -0
- primarysource/debpolicy/catalogue.py +263 -0
- primarysource/debpolicy/cli.py +12 -0
- primarysource/debpolicy/server.py +11 -0
- primarysource/draft/__init__.py +11 -0
- primarysource/draft/catalogue.py +117 -0
- primarysource/draft/cli.py +149 -0
- primarysource/draft/collection.py +21 -0
- primarysource/draft/datatracker.py +158 -0
- primarysource/draft/importer.py +163 -0
- primarysource/draft/licence.py +89 -0
- primarysource/draft/names.py +64 -0
- primarysource/draft/net.py +133 -0
- primarysource/draft/segment.py +192 -0
- primarysource/draft/server.py +91 -0
- primarysource/draft/status.py +146 -0
- primarysource/draft/store.py +199 -0
- primarysource/draft/tracked.json +104 -0
- primarysource/draft/views.py +430 -0
- primarysource/draft/xmlmeta.py +74 -0
- primarysource/ecma/__init__.py +52 -0
- primarysource/ecma/_pdf.py +143 -0
- primarysource/ecma/catalogue.json +22 -0
- primarysource/ecma/catalogue.py +100 -0
- primarysource/ecma/cli.py +67 -0
- primarysource/ecma/identify.py +25 -0
- primarysource/ecma/importer.py +108 -0
- primarysource/ecma/library.py +240 -0
- primarysource/ecma/server.py +67 -0
- primarysource/errors.py +29 -0
- primarysource/fetch.py +360 -0
- primarysource/fsutil.py +150 -0
- primarysource/gitdocs/__init__.py +15 -0
- primarysource/gitdocs/catalogue.py +116 -0
- primarysource/gitdocs/cli.py +12 -0
- primarysource/gitdocs/server.py +8 -0
- primarysource/home.py +65 -0
- primarysource/iana/__init__.py +32 -0
- primarysource/iana/catalogue.json +244 -0
- primarysource/iana/catalogue.py +69 -0
- primarysource/iana/cli.py +69 -0
- primarysource/iana/importer.py +157 -0
- primarysource/iana/model.py +325 -0
- primarysource/iana/reader.py +315 -0
- primarysource/iana/server.py +67 -0
- primarysource/iana/store.py +83 -0
- primarysource/iana/xmlsafe.py +47 -0
- primarysource/identify.py +186 -0
- primarysource/ieee/__init__.py +58 -0
- primarysource/ieee/_html.py +49 -0
- primarysource/ieee/catalogue.json +74 -0
- primarysource/ieee/catalogue.py +123 -0
- primarysource/ieee/cli.py +79 -0
- primarysource/ieee/identify.py +24 -0
- primarysource/ieee/ieee802.py +47 -0
- primarysource/ieee/importer.py +305 -0
- primarysource/ieee/posix.py +233 -0
- primarysource/ieee/server.py +79 -0
- primarysource/index.py +446 -0
- primarysource/iso/__init__.py +51 -0
- primarysource/iso/cli.py +98 -0
- primarysource/iso/designation.py +95 -0
- primarysource/iso/identify.py +100 -0
- primarysource/iso/importer.py +224 -0
- primarysource/iso/library.py +249 -0
- primarysource/iso/server.py +69 -0
- primarysource/itu/__init__.py +41 -0
- primarysource/itu/__main__.py +8 -0
- primarysource/itu/asn1mod.py +478 -0
- primarysource/itu/catalog.py +472 -0
- primarysource/itu/cli.py +445 -0
- primarysource/itu/collection.py +20 -0
- primarysource/itu/corpus.py +118 -0
- primarysource/itu/errata.py +16 -0
- primarysource/itu/formal.py +565 -0
- primarysource/itu/identify.py +26 -0
- primarysource/itu/index.py +490 -0
- primarysource/itu/mcp.py +474 -0
- primarysource/itu/py.typed +0 -0
- primarysource/itu/sources.py +600 -0
- primarysource/itu/text.py +609 -0
- primarysource/kconfig/__init__.py +16 -0
- primarysource/kconfig/catalogue.py +98 -0
- primarysource/kconfig/cli.py +12 -0
- primarysource/kconfig/server.py +8 -0
- primarysource/model.py +63 -0
- primarysource/nist/__init__.py +5 -0
- primarysource/nist/collection.py +46 -0
- primarysource/nist/data/acquired-nist.json +1657 -0
- primarysource/nist/data/nist-catalog.json +12090 -0
- primarysource/nist/data/sources.toml +1996 -0
- primarysource/nist/identify.py +41 -0
- primarysource/nist/ingest/__init__.py +1 -0
- primarysource/nist/ingest/acquire_nist.py +128 -0
- primarysource/nist/ingest/controls.py +82 -0
- primarysource/nist/ingest/discover_nist.py +218 -0
- primarysource/nist/ingest/extract.py +48 -0
- primarysource/nist/ingest/fetch.py +211 -0
- primarysource/nist/ingest/index.py +14 -0
- primarysource/nist/ingest/oscal.py +480 -0
- primarysource/nist/ingest/prose_layouts.py +236 -0
- primarysource/nist/ingest/sources.py +94 -0
- primarysource/nist/ingest/status.py +255 -0
- primarysource/nist/models.py +7 -0
- primarysource/nist/paths.py +33 -0
- primarysource/nist/server/__init__.py +3 -0
- primarysource/nist/server/corpus.py +38 -0
- primarysource/nist/server/mcp.py +19 -0
- primarysource/nist/server/tools.py +35 -0
- primarysource/nist/spec.py +38 -0
- primarysource/pdfcorpus/__init__.py +16 -0
- primarysource/pdfcorpus/corpus.py +159 -0
- primarysource/pdfcorpus/fts.py +175 -0
- primarysource/pdfcorpus/index.py +70 -0
- primarysource/pdfcorpus/models.py +30 -0
- primarysource/pdfcorpus/server.py +96 -0
- primarysource/pdfcorpus/spec.py +37 -0
- primarysource/pdfcorpus/split.py +391 -0
- primarysource/pdfcorpus/tools.py +244 -0
- primarysource/pep/__init__.py +15 -0
- primarysource/pep/catalogue.py +191 -0
- primarysource/pep/cli.py +12 -0
- primarysource/pep/server.py +8 -0
- primarysource/pkgdocs/__init__.py +19 -0
- primarysource/pkgdocs/fetch.py +356 -0
- primarysource/pkgdocs/index.py +89 -0
- primarysource/pkgdocs/model.py +76 -0
- primarysource/pkgdocs/readers/__init__.py +1 -0
- primarysource/pkgdocs/readers/_common.py +134 -0
- primarysource/pkgdocs/readers/asciidoc.py +160 -0
- primarysource/pkgdocs/readers/latex.py +315 -0
- primarysource/pkgdocs/readers/markdown.py +173 -0
- primarysource/pkgdocs/readers/pod.py +132 -0
- primarysource/pkgdocs/readers/rst.py +239 -0
- primarysource/pkgdocs/readers/scdoc.py +108 -0
- primarysource/pkgdocs/runtime.py +552 -0
- primarysource/pkgdocs/serving.py +158 -0
- primarysource/pkgdocs/store.py +167 -0
- primarysource/pms/__init__.py +16 -0
- primarysource/pms/catalogue.py +88 -0
- primarysource/pms/cli.py +12 -0
- primarysource/pms/server.py +8 -0
- primarysource/py.typed +0 -0
- primarysource/readers/__init__.py +10 -0
- primarysource/readers/bookmarks.py +192 -0
- primarysource/readers/html.py +1312 -0
- primarysource/readers/markdown.py +189 -0
- primarysource/readers/pdf.py +187 -0
- primarysource/readers/xml.py +230 -0
- primarysource/refs.py +87 -0
- primarysource/registry.py +161 -0
- primarysource/render.py +49 -0
- primarysource/rfc/__init__.py +3 -0
- primarysource/rfc/__main__.py +6 -0
- primarysource/rfc/cite.py +204 -0
- primarysource/rfc/cli.py +903 -0
- primarysource/rfc/collection.py +19 -0
- primarysource/rfc/config.py +138 -0
- primarysource/rfc/errata.py +203 -0
- primarysource/rfc/fsutil.py +82 -0
- primarysource/rfc/graph.py +213 -0
- primarysource/rfc/index/__init__.py +1 -0
- primarysource/rfc/index/chunker.py +281 -0
- primarysource/rfc/index/embed.py +166 -0
- primarysource/rfc/index/hybrid.py +305 -0
- primarysource/rfc/index/lexical.py +223 -0
- primarysource/rfc/index/rfcxml.py +673 -0
- primarysource/rfc/index/search.py +508 -0
- primarysource/rfc/index/txtseg.py +585 -0
- primarysource/rfc/library.py +294 -0
- primarysource/rfc/mcp/__init__.py +1 -0
- primarysource/rfc/mcp/server.py +381 -0
- primarysource/rfc/packs/__init__.py +1 -0
- primarysource/rfc/packs/builder.py +228 -0
- primarysource/rfc/packs/collections/all-current-txt.json +10 -0
- primarysource/rfc/packs/collections/essentials.json +19 -0
- primarysource/rfc/packs/collections/fyi-current.json +10 -0
- primarysource/rfc/packs/collections/standards-current.json +10 -0
- primarysource/rfc/packs/collections/year-2025.json +10 -0
- primarysource/rfc/packs/collections/year-2026.json +10 -0
- primarysource/rfc/packs/collections.py +163 -0
- primarysource/rfc/packs/docsetexport.py +163 -0
- primarysource/rfc/packs/epub.py +159 -0
- primarysource/rfc/packs/export.py +306 -0
- primarysource/rfc/packs/htmlexport.py +82 -0
- primarysource/rfc/packs/serve.py +143 -0
- primarysource/rfc/packs/zimexport.py +171 -0
- primarysource/rfc/query.py +466 -0
- primarysource/rfc/subjects.py +457 -0
- primarysource/rfc/sync/__init__.py +1 -0
- primarysource/rfc/sync/downloader.py +132 -0
- primarysource/rfc/sync/manifest.py +162 -0
- primarysource/rfc/sync/scanner.py +49 -0
- primarysource/rfc/sync/state.py +464 -0
- primarysource/rfc/views.py +1067 -0
- primarysource/rpmdocs/__init__.py +15 -0
- primarysource/rpmdocs/catalogue.py +117 -0
- primarysource/rpmdocs/cli.py +12 -0
- primarysource/rpmdocs/server.py +8 -0
- primarysource/scaffold.py +223 -0
- primarysource/serve.py +75 -0
- primarysource/settings.py +152 -0
- primarysource/usb/__init__.py +5 -0
- primarysource/usb/collection.py +37 -0
- primarysource/usb/data/acquired.json +2025 -0
- primarysource/usb/data/sources-usb.toml +852 -0
- primarysource/usb/data/sources.toml +10 -0
- primarysource/usb/data/usb-catalog.json +1663 -0
- primarysource/usb/data/usb-classification.json +1986 -0
- primarysource/usb/data/usb-corpus-build.json +608 -0
- primarysource/usb/data/usb-pdf-choice.json +839 -0
- primarysource/usb/data/usb-split-measure.json +1803 -0
- primarysource/usb/ingest/__init__.py +1 -0
- primarysource/usb/ingest/acquire.py +130 -0
- primarysource/usb/ingest/discover_usb.py +88 -0
- primarysource/usb/ingest/extract.py +48 -0
- primarysource/usb/ingest/fetch.py +197 -0
- primarysource/usb/ingest/index.py +14 -0
- primarysource/usb/ingest/sources.py +85 -0
- primarysource/usb/models.py +7 -0
- primarysource/usb/paths.py +33 -0
- primarysource/usb/server/__init__.py +3 -0
- primarysource/usb/server/corpus.py +38 -0
- primarysource/usb/server/mcp.py +19 -0
- primarysource/usb/server/tools.py +35 -0
- primarysource/usb/spec.py +20 -0
- primarysource/usbids/__init__.py +56 -0
- primarysource/usbids/cli.py +65 -0
- primarysource/usbids/importer.py +272 -0
- primarysource/usbids/parse.py +283 -0
- primarysource/usbids/server.py +55 -0
- primarysource/usbids/store.py +80 -0
- primarysource/usbids/tools.py +174 -0
- primarysource/zim/__init__.py +5 -0
- primarysource/zim/__main__.py +8 -0
- primarysource/zim/_libzim.py +37 -0
- primarysource/zim/catalog.py +594 -0
- primarysource/zim/cli.py +555 -0
- primarysource/zim/collection.py +41 -0
- primarysource/zim/errors.py +100 -0
- primarysource/zim/groups.py +30 -0
- primarysource/zim/library.py +668 -0
- primarysource/zim/manage.py +636 -0
- primarysource/zim/markdown.py +1247 -0
- primarysource/zim/mcp.py +201 -0
- primarysource/zim/purl.py +130 -0
- primarysource/zim/py.typed +0 -0
- primarysource/zim/refs.py +90 -0
- primarysource/zim/service.py +1182 -0
- primarysource/zim/snapshot.py +197 -0
- primarysource/zim/sync.py +269 -0
- primarysource/zim/views.py +230 -0
- primarysource-0.1.0.dist-info/METADATA +118 -0
- primarysource-0.1.0.dist-info/RECORD +303 -0
- primarysource-0.1.0.dist-info/WHEEL +4 -0
- primarysource-0.1.0.dist-info/entry_points.txt +2 -0
- primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
- primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""Full-text retrieval over section chunks (FTS5/BM25) + table of contents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import sqlite3
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from ..sync.state import file_path, mirror_of
|
|
10
|
+
from .chunker import sha256_text, split_sections
|
|
11
|
+
|
|
12
|
+
# Reference/boilerplate sections match every vague query and pollute result
|
|
13
|
+
# lists: shared discount pattern used by both rankings (never drops rows).
|
|
14
|
+
NOISE_TITLE_RE = re.compile(
|
|
15
|
+
r"\b(references?|acknowledg\w*|authors?|contributors?|participants|"
|
|
16
|
+
r"editors'? address\w*|table of contents|change log|copyright)\b", re.I)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _doc_sections(conn: sqlite3.Connection, doc_id: str, text: str,
|
|
20
|
+
xml_path: Path | None, stats: dict) -> list:
|
|
21
|
+
"""Sections for one document: RFCXML structure when the doc has XML
|
|
22
|
+
(exact boundaries, aligned back onto txt lines), else the ToC-driven
|
|
23
|
+
txt segmenter (txtseg); the legacy heuristic only if both fail. Also
|
|
24
|
+
refreshes the doc's `blocks`/`xrefs`/`seg_quality` rows."""
|
|
25
|
+
from . import rfcxml, txtseg
|
|
26
|
+
for t in ("blocks", "xrefs", "seg_quality"):
|
|
27
|
+
conn.execute(f"DELETE FROM {t} WHERE doc_id=?", (doc_id,))
|
|
28
|
+
|
|
29
|
+
def quality(method: str, q=None, notes: str = ""):
|
|
30
|
+
conn.execute(
|
|
31
|
+
"INSERT INTO seg_quality(doc_id, method, toc_entries, matched, "
|
|
32
|
+
"extra, headings, score, notes) VALUES(?,?,?,?,?,?,?,?)",
|
|
33
|
+
(doc_id, method, q.toc_entries if q else 0,
|
|
34
|
+
q.matched if q else 0, q.extra if q else 0,
|
|
35
|
+
q.headings if q else 0, q.score if q else None,
|
|
36
|
+
notes or ("; ".join(q.notes) if q else "")))
|
|
37
|
+
|
|
38
|
+
px = lines = None
|
|
39
|
+
if xml_path and Path(xml_path).exists():
|
|
40
|
+
try:
|
|
41
|
+
px = rfcxml.parse(Path(xml_path).read_text(
|
|
42
|
+
encoding="utf-8", errors="replace"), doc_id)
|
|
43
|
+
lines = rfcxml.align(px, text)
|
|
44
|
+
stats["xml_docs"] += 1
|
|
45
|
+
quality("rfcxml")
|
|
46
|
+
except Exception as e: # noqa: BLE001 - fall back, never block a build
|
|
47
|
+
stats["xml_failed"] += 1
|
|
48
|
+
stats.setdefault("xml_errors", []).append(
|
|
49
|
+
f"{doc_id}: {type(e).__name__}: {e}"[:160])
|
|
50
|
+
px = None
|
|
51
|
+
if px is None:
|
|
52
|
+
try:
|
|
53
|
+
px, q = txtseg.segment(text, doc_id)
|
|
54
|
+
lines = text.lstrip("").splitlines()
|
|
55
|
+
stats["txtseg_docs"] += 1
|
|
56
|
+
quality(q.method, q)
|
|
57
|
+
except Exception as e: # noqa: BLE001
|
|
58
|
+
stats.setdefault("txtseg_errors", []).append(
|
|
59
|
+
f"{doc_id}: {type(e).__name__}: {e}"[:160])
|
|
60
|
+
quality("legacy", notes=f"txtseg failed: {type(e).__name__}")
|
|
61
|
+
return split_sections(text)
|
|
62
|
+
conn.executemany(
|
|
63
|
+
"INSERT OR REPLACE INTO blocks(doc_id, pn, ord, kind, sec_num, "
|
|
64
|
+
"sec_title, sec_anchor, start_line, end_line, text, bcp14, lang, "
|
|
65
|
+
"exact) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)", rfcxml.blocks_rows(px))
|
|
66
|
+
conn.executemany(
|
|
67
|
+
"INSERT OR REPLACE INTO xrefs(doc_id, src_pn, src_sec, dst_doc, "
|
|
68
|
+
"dst_sec, ref_anchor, normative) VALUES(?,?,?,?,?,?,?)",
|
|
69
|
+
rfcxml.xref_rows(px))
|
|
70
|
+
return rfcxml.xml_sections(px, lines)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _src_sha(txt_sha: str, xml_sha: str | None) -> str:
|
|
74
|
+
"""Source hash a document's chunks are built from: txt + xml + the
|
|
75
|
+
parser that read them, so a parser fix re-chunks exactly its docs."""
|
|
76
|
+
from .rfcxml import PARSER_VERSION
|
|
77
|
+
from .txtseg import VERSION as TXTSEG_VERSION
|
|
78
|
+
if xml_sha:
|
|
79
|
+
return sha256_text(f"{txt_sha}:{xml_sha}:rfcxml-{PARSER_VERSION}")
|
|
80
|
+
return sha256_text(f"{txt_sha}:txtseg-{TXTSEG_VERSION}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _index_one(conn: sqlite3.Connection, doc_id: str, path: Path,
|
|
84
|
+
src_sha: str, xml_path: Path | None, stats: dict,
|
|
85
|
+
fts_incremental: bool) -> None:
|
|
86
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
87
|
+
# content-addressed id carryover: a re-chunk keeps the old id for
|
|
88
|
+
# every section whose body hash is unchanged, so its embedding
|
|
89
|
+
# stays valid (embed_state is keyed by chunk_id + content sha);
|
|
90
|
+
# only genuinely new sections take fresh ids.
|
|
91
|
+
old_ids: dict[str, list[int]] = {}
|
|
92
|
+
for o in conn.execute("SELECT id, sha256 FROM chunks WHERE doc_id=?",
|
|
93
|
+
(doc_id,)).fetchall():
|
|
94
|
+
old_ids.setdefault(o["sha256"], []).append(o["id"])
|
|
95
|
+
next_id = (conn.execute("SELECT MAX(id) FROM chunks")
|
|
96
|
+
.fetchone()[0] or 0) + 1
|
|
97
|
+
taken: set[int] = set()
|
|
98
|
+
plan: list[tuple[int, object, str]] = []
|
|
99
|
+
for s in _doc_sections(conn, doc_id, text, xml_path, stats):
|
|
100
|
+
sh = sha256_text(s.text)
|
|
101
|
+
sid = next((i for i in old_ids.get(sh, ()) if i not in taken), None)
|
|
102
|
+
if sid is None:
|
|
103
|
+
sid = next_id
|
|
104
|
+
next_id += 1
|
|
105
|
+
taken.add(sid)
|
|
106
|
+
plan.append((sid, s, sh))
|
|
107
|
+
stale = [i for ids in old_ids.values() for i in ids if i not in taken]
|
|
108
|
+
conn.executemany("DELETE FROM embed_state WHERE chunk_id=?",
|
|
109
|
+
[(i,) for i in stale])
|
|
110
|
+
if fts_incremental: # external-content FTS needs the old values to delete
|
|
111
|
+
conn.execute(
|
|
112
|
+
"INSERT INTO chunks_fts(chunks_fts, rowid, sec, body) "
|
|
113
|
+
"SELECT 'delete', id, sec, body FROM chunks WHERE doc_id=?",
|
|
114
|
+
(doc_id,))
|
|
115
|
+
conn.execute("DELETE FROM chunks WHERE doc_id=?", (doc_id,))
|
|
116
|
+
for sid, s, sh in plan:
|
|
117
|
+
conn.execute(
|
|
118
|
+
"INSERT INTO chunks(id, doc_id, sec_num, sec_title, "
|
|
119
|
+
"start_line, end_line, body, sha256) VALUES(?,?,?,?,?,?,?,?)",
|
|
120
|
+
(sid, doc_id, s.sec_num, s.sec_title, s.start_line,
|
|
121
|
+
s.end_line, s.text, sh))
|
|
122
|
+
if fts_incremental:
|
|
123
|
+
conn.execute(
|
|
124
|
+
"INSERT INTO chunks_fts(rowid, sec, body) "
|
|
125
|
+
"SELECT id, sec, body FROM chunks WHERE doc_id=?", (doc_id,))
|
|
126
|
+
from .lexical import index_identifiers
|
|
127
|
+
reused = {i for ids in old_ids.values() for i in ids}
|
|
128
|
+
index_identifiers(conn, [sid for sid, _s, _h in plan if sid not in reused],
|
|
129
|
+
drop_ids=stale)
|
|
130
|
+
conn.execute(
|
|
131
|
+
"INSERT INTO chunk_src(doc_id, sha256) VALUES(?,?) "
|
|
132
|
+
"ON CONFLICT(doc_id) DO UPDATE SET sha256=excluded.sha256",
|
|
133
|
+
(doc_id, src_sha))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def index_doc(conn: sqlite3.Connection, doc_id: str) -> bool:
|
|
137
|
+
"""(Re)index ONE document whose txt (and xml, if any) is on disk, keeping
|
|
138
|
+
the FTS index in sync incrementally: the on-demand path, cheap enough
|
|
139
|
+
to run inside a tool call (well under a second for most RFCs). Returns
|
|
140
|
+
False when the document has no local txt."""
|
|
141
|
+
rows = conn.execute(
|
|
142
|
+
"""SELECT t.path, t.sha256, x.path AS xml_path, x.sha256 AS xml_sha
|
|
143
|
+
FROM files t LEFT JOIN files x
|
|
144
|
+
ON x.doc_id = t.doc_id AND x.fmt = 'xml'
|
|
145
|
+
WHERE t.fmt = 'txt' AND t.doc_id = ?""", (doc_id,)).fetchall()
|
|
146
|
+
if not rows:
|
|
147
|
+
return False
|
|
148
|
+
r = rows[0]
|
|
149
|
+
mirror = mirror_of(conn)
|
|
150
|
+
path = file_path(conn, r["path"], mirror)
|
|
151
|
+
if not path.exists():
|
|
152
|
+
return False
|
|
153
|
+
xml_path = (file_path(conn, r["xml_path"], mirror) if r["xml_path"]
|
|
154
|
+
else None)
|
|
155
|
+
stats = {"xml_docs": 0, "xml_failed": 0, "txtseg_docs": 0}
|
|
156
|
+
# Take the write lock BEFORE _index_one reads MAX(id) and the old
|
|
157
|
+
# chunks. sqlite3 otherwise opens the transaction at the first INSERT,
|
|
158
|
+
# so two writers (threads of the shared server, or two processes)
|
|
159
|
+
# indexing different documents at once would read the same MAX(id),
|
|
160
|
+
# pick the same new chunk ids, and the second would fail on
|
|
161
|
+
# chunks.id. A waiting writer blocks here for up to busy_timeout.
|
|
162
|
+
if not conn.in_transaction:
|
|
163
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
164
|
+
try:
|
|
165
|
+
_index_one(conn, doc_id, path, _src_sha(r["sha256"], r["xml_sha"]),
|
|
166
|
+
xml_path, stats, fts_incremental=True)
|
|
167
|
+
conn.commit()
|
|
168
|
+
except BaseException:
|
|
169
|
+
conn.rollback()
|
|
170
|
+
raise
|
|
171
|
+
return True
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def build_chunks(conn: sqlite3.Connection, mirror_root: Path,
|
|
175
|
+
progress=None) -> dict:
|
|
176
|
+
"""(Re)chunk every locally-held .txt, incrementally by source hash.
|
|
177
|
+
|
|
178
|
+
Documents that also have RFCXML (RFC 8650+) take section boundaries
|
|
179
|
+
from the XML; the source hash then covers txt + xml + parser version, so
|
|
180
|
+
an XML download or a parser fix re-chunks exactly the affected docs.
|
|
181
|
+
"""
|
|
182
|
+
rows = conn.execute(
|
|
183
|
+
"""SELECT t.doc_id, t.path, t.sha256, x.path AS xml_path,
|
|
184
|
+
x.sha256 AS xml_sha
|
|
185
|
+
FROM files t LEFT JOIN files x
|
|
186
|
+
ON x.doc_id = t.doc_id AND x.fmt = 'xml'
|
|
187
|
+
WHERE t.fmt = 'txt'""").fetchall()
|
|
188
|
+
stats = {"scanned": len(rows), "rebuilt": 0, "unchanged": 0, "missing": 0,
|
|
189
|
+
"xml_docs": 0, "xml_failed": 0, "txtseg_docs": 0}
|
|
190
|
+
mirror = mirror_of(conn)
|
|
191
|
+
for r in rows:
|
|
192
|
+
path = file_path(conn, r["path"], mirror)
|
|
193
|
+
xml_path = (file_path(conn, r["xml_path"], mirror)
|
|
194
|
+
if r["xml_path"] else None)
|
|
195
|
+
if not path.exists():
|
|
196
|
+
stats["missing"] += 1
|
|
197
|
+
continue
|
|
198
|
+
src_sha = _src_sha(r["sha256"], r["xml_sha"] if r["xml_path"]
|
|
199
|
+
else None)
|
|
200
|
+
prev = conn.execute("SELECT sha256 FROM chunk_src WHERE doc_id=?",
|
|
201
|
+
(r["doc_id"],)).fetchone()
|
|
202
|
+
if prev and prev["sha256"] == src_sha:
|
|
203
|
+
stats["unchanged"] += 1
|
|
204
|
+
continue
|
|
205
|
+
_index_one(conn, r["doc_id"], path, src_sha, xml_path, stats,
|
|
206
|
+
fts_incremental=False)
|
|
207
|
+
stats["rebuilt"] += 1
|
|
208
|
+
if progress and stats["rebuilt"] % 250 == 0:
|
|
209
|
+
progress(stats["rebuilt"], stats["scanned"])
|
|
210
|
+
if stats["rebuilt"] and stats["rebuilt"] % 1000 == 0:
|
|
211
|
+
conn.commit()
|
|
212
|
+
conn.execute("INSERT INTO chunks_fts(chunks_fts) VALUES('rebuild')")
|
|
213
|
+
from .lexical import backfill_identifiers, mine_acronyms
|
|
214
|
+
stats["idents_backfilled"] = backfill_identifiers(conn)
|
|
215
|
+
if stats["rebuilt"] or not conn.execute(
|
|
216
|
+
"SELECT 1 FROM acronyms LIMIT 1").fetchone():
|
|
217
|
+
stats["acronyms"] = mine_acronyms(conn)
|
|
218
|
+
conn.commit()
|
|
219
|
+
stats["chunks"] = conn.execute("SELECT COUNT(*) c FROM chunks").fetchone()["c"]
|
|
220
|
+
return stats
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _fts_query(query: str, or_mode: bool = False,
|
|
224
|
+
hybrid_bm25: bool = False,
|
|
225
|
+
extra_phrases: list[str] | None = None) -> str:
|
|
226
|
+
"""Pass through expert FTS syntax; fall back to quoted-word AND/OR.
|
|
227
|
+
|
|
228
|
+
Pure-numeric tokens are dropped from the default query: the tokenizer
|
|
229
|
+
splits decimals, so "TLS 1.3" would otherwise match every section
|
|
230
|
+
number containing 1.3 (e.g. §4.1.3) in any document. Versioned protocol
|
|
231
|
+
names are still covered by the doc-title ranking list and by embeddings.
|
|
232
|
+
|
|
233
|
+
or_mode ranks by number of matched (IDF-weighted) terms instead of
|
|
234
|
+
requiring all of them — the AND conjunct starves recall on real
|
|
235
|
+
questions ("NTP timestamp era rollover" AND-matches 1 chunk in 300k).
|
|
236
|
+
|
|
237
|
+
hybrid_bm25 (for RRF candidates) is a two-level OR: bare terms, plus
|
|
238
|
+
the full AND restricted to {sec body} — a chunk whose SECTION TITLE and
|
|
239
|
+
ABSTRACT together carry every term is the canonical passage, and that
|
|
240
|
+
pattern survives porter stemming noise ("era"->"er") and documents that
|
|
241
|
+
merely mention one hot keyword a lot. Porter folds "Reno NewReno" into
|
|
242
|
+
one token; dedup terms for that.
|
|
243
|
+
"""
|
|
244
|
+
q = query.strip()
|
|
245
|
+
if re.search(r'"|AND|OR|NEAR|\*|-|\{|\}', q):
|
|
246
|
+
return q
|
|
247
|
+
terms, seen = [], set()
|
|
248
|
+
for t in re.findall(r"[A-Za-z0-9.\-]+", q):
|
|
249
|
+
if re.search(r"[A-Za-z]", t) and t not in seen:
|
|
250
|
+
seen.add(t)
|
|
251
|
+
terms.append(t)
|
|
252
|
+
if not terms:
|
|
253
|
+
terms = re.findall(r"[A-Za-z0-9.\-]+", q)
|
|
254
|
+
if not terms:
|
|
255
|
+
return '""'
|
|
256
|
+
if hybrid_bm25:
|
|
257
|
+
# two-level OR: bare terms for recall; a NEAR proximity group so a
|
|
258
|
+
# chunk carrying ALL terms in one passage outscores term scattered
|
|
259
|
+
# hits. ({col} (...) grouping is not valid FTS5 grammar.)
|
|
260
|
+
parts = [f'"{t}"' for t in terms]
|
|
261
|
+
if 2 <= len(terms) <= 5:
|
|
262
|
+
parts.append("NEAR(" + " ".join(f'"{t}"' for t in terms)
|
|
263
|
+
+ f", {40 * len(terms)})")
|
|
264
|
+
# acronym expansions: "NTP" also matches "network time protocol"
|
|
265
|
+
parts += [f'"{p}"' for p in (extra_phrases or [])]
|
|
266
|
+
return " OR ".join(parts)
|
|
267
|
+
joiner = " OR " if or_mode else " "
|
|
268
|
+
return joiner.join(f'"{t}"' for t in terms)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def search_chunks(conn: sqlite3.Connection, query: str, limit: int = 10,
|
|
272
|
+
include_obsolete: bool = False,
|
|
273
|
+
or_mode: bool = False,
|
|
274
|
+
hybrid_bm25: bool = False,
|
|
275
|
+
extra_phrases: list[str] | None = None) -> list[dict]:
|
|
276
|
+
sql = """
|
|
277
|
+
SELECT c.id AS chunk_id, c.doc_id, c.sec_num, c.sec_title,
|
|
278
|
+
c.start_line, c.end_line, d.title, d.is_current,
|
|
279
|
+
d.current_status, d.pub_date,
|
|
280
|
+
bm25(chunks_fts, 3.0, 1.0) AS rank,
|
|
281
|
+
snippet(chunks_fts, 1, '>>>', '<<<', ' … ', 16) AS snippet
|
|
282
|
+
FROM chunks_fts
|
|
283
|
+
JOIN chunks c ON c.id = chunks_fts.rowid
|
|
284
|
+
JOIN docs d ON d.doc_id = c.doc_id
|
|
285
|
+
WHERE chunks_fts MATCH ?"""
|
|
286
|
+
params: list = [_fts_query(query, or_mode=or_mode,
|
|
287
|
+
hybrid_bm25=hybrid_bm25,
|
|
288
|
+
extra_phrases=extra_phrases)]
|
|
289
|
+
if not include_obsolete:
|
|
290
|
+
sql += " AND d.is_current = 1"
|
|
291
|
+
sql += " ORDER BY rank LIMIT ?"
|
|
292
|
+
params.append(limit)
|
|
293
|
+
try:
|
|
294
|
+
rows = conn.execute(sql, params).fetchall()
|
|
295
|
+
except sqlite3.OperationalError:
|
|
296
|
+
terms = [t for t in re.findall(r"[A-Za-z0-9.\-]+", query)[:8]
|
|
297
|
+
if re.search(r"[A-Za-z]", t)] or \
|
|
298
|
+
re.findall(r"[A-Za-z0-9.\-]+", query)[:8]
|
|
299
|
+
params[0] = " ".join(f'"{t}"' for t in terms) or '""'
|
|
300
|
+
rows = conn.execute(sql, params).fetchall()
|
|
301
|
+
return [dict(r) for r in rows]
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# --------------------------------------------------------------------------
|
|
305
|
+
# section addressing: let a caller ask for "3.4" / "Appendix C" instead of
|
|
306
|
+
# doing line arithmetic against a .txt file it should not be reading.
|
|
307
|
+
|
|
308
|
+
_PART_RE = re.compile(r"\s*\(part \d+/\d+\)$")
|
|
309
|
+
_APX_LETTER = re.compile(r"^([A-Z])(?:\.\d+)*$")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def norm_sec(sec: str) -> str:
|
|
313
|
+
"""'Section 3.4' | '§3.4' | '3.4.' | 'Appendix C' | 'C' -> the canonical
|
|
314
|
+
sec_num as stored in chunks ('3.4' | 'Appendix C' | '' for unnumbered).
|
|
315
|
+
|
|
316
|
+
A bare letter becomes 'Appendix X': the chunker only stores bare letters
|
|
317
|
+
for appendix sub-headings, so 'C' means Appendix C, never section C.
|
|
318
|
+
"""
|
|
319
|
+
s = (sec or "").strip().rstrip(".:")
|
|
320
|
+
s = re.sub(r"^(see\s+)?(section|sect|sec|clause|paragraph)\s+", "", s,
|
|
321
|
+
flags=re.I)
|
|
322
|
+
s = s.lstrip("§ ").strip()
|
|
323
|
+
if not s:
|
|
324
|
+
return ""
|
|
325
|
+
m = re.fullmatch(r"(?:appendix|appx)\s*:?\s*([A-Z][a-z]?(?:\.\d+)?)", s,
|
|
326
|
+
re.I)
|
|
327
|
+
if m:
|
|
328
|
+
lab = m.group(1)[0].upper() + m.group(1)[1:]
|
|
329
|
+
# sub-appendices are stored bare ('Appendix B.1' -> 'B.1')
|
|
330
|
+
return lab if "." in lab else f"Appendix {lab}"
|
|
331
|
+
if re.fullmatch(r"\d{1,3}(?:\.\d{1,3}){0,3}", s):
|
|
332
|
+
return s
|
|
333
|
+
m = re.fullmatch(r"([A-Z][a-z]?)((?:\.\d+)+)", s)
|
|
334
|
+
if m: # 'A.1' stays 'A.1' as stored
|
|
335
|
+
return m.group(1) + m.group(2)
|
|
336
|
+
if re.fullmatch(r"[A-Z][a-z]?", s):
|
|
337
|
+
return f"Appendix {s[0].upper()}"
|
|
338
|
+
return s
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def url_frag(sec: str) -> str:
|
|
342
|
+
"""rfc-editor.org html anchor for a section number ('' -> no fragment)."""
|
|
343
|
+
if not sec:
|
|
344
|
+
return ""
|
|
345
|
+
if sec.startswith("Appendix"):
|
|
346
|
+
return "appendix-" + sec.split(None, 1)[1]
|
|
347
|
+
if _APX_LETTER.match(sec):
|
|
348
|
+
return "appendix-" + sec
|
|
349
|
+
return "section-" + sec
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def section_rows(conn: sqlite3.Connection, doc_id: str, sec: str,
|
|
353
|
+
deep: bool = True) -> list[dict]:
|
|
354
|
+
"""Chunk rows for a section, all parts in order. deep=True also returns
|
|
355
|
+
sub-sections: the chunker stores a parent's own text and its children as
|
|
356
|
+
sibling rows, so the parent row alone is not the whole section."""
|
|
357
|
+
want = norm_sec(sec)
|
|
358
|
+
rows = conn.execute(
|
|
359
|
+
"""SELECT id, sec_num, sec_title, start_line, end_line, sha256
|
|
360
|
+
FROM chunks WHERE doc_id = ? ORDER BY start_line, id""",
|
|
361
|
+
(doc_id,)).fetchall()
|
|
362
|
+
out: list[dict] = []
|
|
363
|
+
for r in rows:
|
|
364
|
+
sn = r["sec_num"]
|
|
365
|
+
if sn == want:
|
|
366
|
+
out.append(dict(r))
|
|
367
|
+
elif deep and want and sn.startswith(want + "."):
|
|
368
|
+
out.append(dict(r))
|
|
369
|
+
elif deep and want and want.startswith("Appendix ") and \
|
|
370
|
+
_APX_LETTER.match(sn or "") and \
|
|
371
|
+
sn[0] == want.split(None, 1)[1][0]:
|
|
372
|
+
out.append(dict(r)) # 'Appendix C' owns its 'C.1' sub-headings
|
|
373
|
+
return out
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def get_toc(conn: sqlite3.Connection, doc_id: str) -> list[dict]:
|
|
377
|
+
rows = conn.execute(
|
|
378
|
+
"""SELECT sec_num, sec_title, start_line, end_line,
|
|
379
|
+
substr(replace(body, char(10), ' '), 1, 90) AS preview
|
|
380
|
+
FROM chunks WHERE doc_id = ?
|
|
381
|
+
ORDER BY start_line, id""", (doc_id,)).fetchall()
|
|
382
|
+
return [dict(r) for r in rows]
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def resolve_span(conn: sqlite3.Connection, doc_id: str, sec: str) -> dict:
|
|
386
|
+
"""Resolve a section reference to a line span, or explain why it failed.
|
|
387
|
+
|
|
388
|
+
ok=False carries 'suggestions' (nearest section numbers) plus the real
|
|
389
|
+
outline: the usual cause is a number borrowed from another document or
|
|
390
|
+
dropped by a rename, and guessing is worse than showing the outline.
|
|
391
|
+
"""
|
|
392
|
+
import difflib
|
|
393
|
+
want = norm_sec(sec)
|
|
394
|
+
rows = section_rows(conn, doc_id, want)
|
|
395
|
+
if rows:
|
|
396
|
+
title = _PART_RE.sub("", rows[0]["sec_title"] or "")
|
|
397
|
+
start = min(r["start_line"] for r in rows)
|
|
398
|
+
end = max(r["end_line"] for r in rows)
|
|
399
|
+
# RFCXML heading row: the section's own title and heading line even
|
|
400
|
+
# when it has no text of its own (rows[0] is then its first child).
|
|
401
|
+
head = conn.execute(
|
|
402
|
+
"SELECT sec_title, start_line, end_line FROM blocks WHERE "
|
|
403
|
+
"doc_id=? AND kind='heading' AND sec_num=?", (doc_id, want)
|
|
404
|
+
).fetchone() if want else None
|
|
405
|
+
if head:
|
|
406
|
+
title = head["sec_title"]
|
|
407
|
+
start = min(start, head["start_line"])
|
|
408
|
+
end = max(end, head["end_line"])
|
|
409
|
+
return {"ok": True, "sec_num": want, "sec_title": title,
|
|
410
|
+
"start_line": start, "end_line": end,
|
|
411
|
+
"subsections": sorted({r["sec_num"] for r in rows
|
|
412
|
+
if r["sec_num"] != want}),
|
|
413
|
+
"url_frag": url_frag(want), "parts": len(rows)}
|
|
414
|
+
known = conn.execute(
|
|
415
|
+
"SELECT DISTINCT sec_num, sec_title FROM chunks WHERE doc_id=?",
|
|
416
|
+
(doc_id,)).fetchall()
|
|
417
|
+
if not known:
|
|
418
|
+
return {"ok": False, "sec_num": want, "indexed": False,
|
|
419
|
+
"reason": f"{doc_id} is not section-indexed "
|
|
420
|
+
"(run `rfclib index build`)"}
|
|
421
|
+
names = [k["sec_num"] for k in known if k["sec_num"]]
|
|
422
|
+
near = difflib.get_close_matches(want, names, n=4, cutoff=0.5)
|
|
423
|
+
if not near: # numbers wrong but the title right is common
|
|
424
|
+
low = (sec or "").strip().lower()
|
|
425
|
+
near = [k["sec_num"] for k in known
|
|
426
|
+
if low and low in (k["sec_title"] or "").lower()][:4]
|
|
427
|
+
return {"ok": False, "sec_num": want, "indexed": True,
|
|
428
|
+
"reason": f"no section {want or '(unnumbered)'} in {doc_id}",
|
|
429
|
+
"suggestions": near,
|
|
430
|
+
"outline": [f"{k['sec_num']} {k['sec_title']}".strip()
|
|
431
|
+
for k in known if k["sec_num"]][:40]}
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def toc(conn: sqlite3.Connection, doc_id: str, depth: int | None = None,
|
|
435
|
+
collapse_parts: bool = True, contains: str | None = None,
|
|
436
|
+
preview: bool = False) -> list[dict]:
|
|
437
|
+
"""Section outline with one row per real section (chunk parts merged), so
|
|
438
|
+
a 70-section RFC costs 70 rows instead of 120 `(part 3/21)` artifacts.
|
|
439
|
+
|
|
440
|
+
depth: keep sections with at most this many dot-levels ('7.2' -> 2).
|
|
441
|
+
contains: substring filter on section number or title.
|
|
442
|
+
"""
|
|
443
|
+
rows = conn.execute(
|
|
444
|
+
"""SELECT sec_num, sec_title, start_line, end_line,
|
|
445
|
+
substr(replace(body, char(10), ' '), 1, 90) AS preview
|
|
446
|
+
FROM chunks WHERE doc_id = ? ORDER BY start_line, id""",
|
|
447
|
+
(doc_id,)).fetchall()
|
|
448
|
+
merged: dict[str, dict] = {}
|
|
449
|
+
order: list[str] = []
|
|
450
|
+
for r in rows:
|
|
451
|
+
title = _PART_RE.sub("", r["sec_title"] or "")
|
|
452
|
+
key = f"{r['sec_num']}\x00{title}"
|
|
453
|
+
m = merged.get(key)
|
|
454
|
+
if m is None:
|
|
455
|
+
m = merged[key] = {"sec_num": r["sec_num"], "title": title,
|
|
456
|
+
"start_line": r["start_line"],
|
|
457
|
+
"end_line": r["end_line"], "parts": 1,
|
|
458
|
+
"preview": r["preview"]}
|
|
459
|
+
order.append(key)
|
|
460
|
+
elif collapse_parts:
|
|
461
|
+
m["start_line"] = min(m["start_line"], r["start_line"])
|
|
462
|
+
m["end_line"] = max(m["end_line"], r["end_line"])
|
|
463
|
+
m["parts"] += 1
|
|
464
|
+
else:
|
|
465
|
+
key2 = f"{key}\x01{len(order)}"
|
|
466
|
+
merged[key2] = dict(m, parts=1, preview=r["preview"])
|
|
467
|
+
order.append(key2)
|
|
468
|
+
# RFCXML docs: a section with no text of its own ('1. Introduction'
|
|
469
|
+
# holding only 1.1-1.4) has no chunk; its heading row fills the gap
|
|
470
|
+
# (parts=0: read it with section='1' to get the subsections).
|
|
471
|
+
have = {m["sec_num"] for m in merged.values()}
|
|
472
|
+
for h in conn.execute(
|
|
473
|
+
"SELECT sec_num, sec_title, start_line, end_line FROM blocks "
|
|
474
|
+
"WHERE doc_id=? AND kind='heading' AND sec_num<>''", (doc_id,)):
|
|
475
|
+
if h["sec_num"] in have:
|
|
476
|
+
continue
|
|
477
|
+
key = f"{h['sec_num']}\x00{h['sec_title']}"
|
|
478
|
+
merged[key] = {"sec_num": h["sec_num"], "title": h["sec_title"],
|
|
479
|
+
"start_line": h["start_line"],
|
|
480
|
+
"end_line": h["end_line"], "parts": 0,
|
|
481
|
+
"preview": ""}
|
|
482
|
+
order.append(key)
|
|
483
|
+
order.sort(key=lambda k: merged[k]["start_line"])
|
|
484
|
+
out = []
|
|
485
|
+
for k in order:
|
|
486
|
+
s = merged[k]
|
|
487
|
+
if depth is not None and s["sec_num"].count(".") + 1 > depth:
|
|
488
|
+
continue
|
|
489
|
+
if contains:
|
|
490
|
+
needle = contains.lower()
|
|
491
|
+
if needle not in s["sec_num"].lower() and \
|
|
492
|
+
needle not in (s["title"] or "").lower():
|
|
493
|
+
continue
|
|
494
|
+
if not preview:
|
|
495
|
+
s.pop("preview", None)
|
|
496
|
+
out.append(s)
|
|
497
|
+
return out
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def index_stats(conn: sqlite3.Connection) -> dict:
|
|
501
|
+
return {
|
|
502
|
+
"chunked_docs": conn.execute(
|
|
503
|
+
"SELECT COUNT(*) c FROM chunk_src").fetchone()["c"],
|
|
504
|
+
"chunks": conn.execute(
|
|
505
|
+
"SELECT COUNT(*) c FROM chunks").fetchone()["c"],
|
|
506
|
+
"embedded_chunks": conn.execute(
|
|
507
|
+
"SELECT COUNT(*) c FROM embed_state").fetchone()["c"],
|
|
508
|
+
}
|