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.
Files changed (303) hide show
  1. primarysource/__init__.py +5 -0
  2. primarysource/bbf/__init__.py +56 -0
  3. primarysource/bbf/_xml.py +63 -0
  4. primarysource/bbf/catalogue.json +777 -0
  5. primarysource/bbf/catalogue.py +137 -0
  6. primarysource/bbf/cli.py +79 -0
  7. primarysource/bbf/datamodel.py +281 -0
  8. primarysource/bbf/importer.py +151 -0
  9. primarysource/bbf/library.py +380 -0
  10. primarysource/bbf/server.py +72 -0
  11. primarysource/budget.py +63 -0
  12. primarysource/c2sp/__init__.py +53 -0
  13. primarysource/c2sp/_fetch.py +5 -0
  14. primarysource/c2sp/catalogue.json +231 -0
  15. primarysource/c2sp/catalogue.py +95 -0
  16. primarysource/c2sp/cli.py +62 -0
  17. primarysource/c2sp/importer.py +90 -0
  18. primarysource/c2sp/library.py +309 -0
  19. primarysource/c2sp/markdown.py +31 -0
  20. primarysource/c2sp/server.py +68 -0
  21. primarysource/cavp/__init__.py +42 -0
  22. primarysource/cavp/cli.py +84 -0
  23. primarysource/cavp/data/vectors.toml +47 -0
  24. primarysource/cavp/importer.py +156 -0
  25. primarysource/cavp/model.py +279 -0
  26. primarysource/cavp/parse.py +269 -0
  27. primarysource/cavp/paths.py +17 -0
  28. primarysource/cavp/server.py +67 -0
  29. primarysource/cavp/tools.py +169 -0
  30. primarysource/cli.py +107 -0
  31. primarysource/cursor.py +82 -0
  32. primarysource/cve/__init__.py +5 -0
  33. primarysource/cve/__main__.py +6 -0
  34. primarysource/cve/cli.py +144 -0
  35. primarysource/cve/collection.py +20 -0
  36. primarysource/cve/config.py +58 -0
  37. primarysource/cve/errors.py +16 -0
  38. primarysource/cve/kernel.py +232 -0
  39. primarysource/cve/mcp.py +139 -0
  40. primarysource/cve/parse.py +318 -0
  41. primarysource/cve/py.typed +0 -0
  42. primarysource/cve/service.py +760 -0
  43. primarysource/cve/store.py +214 -0
  44. primarysource/cve/sync.py +438 -0
  45. primarysource/cve/views.py +304 -0
  46. primarysource/debpolicy/__init__.py +16 -0
  47. primarysource/debpolicy/catalogue.py +263 -0
  48. primarysource/debpolicy/cli.py +12 -0
  49. primarysource/debpolicy/server.py +11 -0
  50. primarysource/draft/__init__.py +11 -0
  51. primarysource/draft/catalogue.py +117 -0
  52. primarysource/draft/cli.py +149 -0
  53. primarysource/draft/collection.py +21 -0
  54. primarysource/draft/datatracker.py +158 -0
  55. primarysource/draft/importer.py +163 -0
  56. primarysource/draft/licence.py +89 -0
  57. primarysource/draft/names.py +64 -0
  58. primarysource/draft/net.py +133 -0
  59. primarysource/draft/segment.py +192 -0
  60. primarysource/draft/server.py +91 -0
  61. primarysource/draft/status.py +146 -0
  62. primarysource/draft/store.py +199 -0
  63. primarysource/draft/tracked.json +104 -0
  64. primarysource/draft/views.py +430 -0
  65. primarysource/draft/xmlmeta.py +74 -0
  66. primarysource/ecma/__init__.py +52 -0
  67. primarysource/ecma/_pdf.py +143 -0
  68. primarysource/ecma/catalogue.json +22 -0
  69. primarysource/ecma/catalogue.py +100 -0
  70. primarysource/ecma/cli.py +67 -0
  71. primarysource/ecma/identify.py +25 -0
  72. primarysource/ecma/importer.py +108 -0
  73. primarysource/ecma/library.py +240 -0
  74. primarysource/ecma/server.py +67 -0
  75. primarysource/errors.py +29 -0
  76. primarysource/fetch.py +360 -0
  77. primarysource/fsutil.py +150 -0
  78. primarysource/gitdocs/__init__.py +15 -0
  79. primarysource/gitdocs/catalogue.py +116 -0
  80. primarysource/gitdocs/cli.py +12 -0
  81. primarysource/gitdocs/server.py +8 -0
  82. primarysource/home.py +65 -0
  83. primarysource/iana/__init__.py +32 -0
  84. primarysource/iana/catalogue.json +244 -0
  85. primarysource/iana/catalogue.py +69 -0
  86. primarysource/iana/cli.py +69 -0
  87. primarysource/iana/importer.py +157 -0
  88. primarysource/iana/model.py +325 -0
  89. primarysource/iana/reader.py +315 -0
  90. primarysource/iana/server.py +67 -0
  91. primarysource/iana/store.py +83 -0
  92. primarysource/iana/xmlsafe.py +47 -0
  93. primarysource/identify.py +186 -0
  94. primarysource/ieee/__init__.py +58 -0
  95. primarysource/ieee/_html.py +49 -0
  96. primarysource/ieee/catalogue.json +74 -0
  97. primarysource/ieee/catalogue.py +123 -0
  98. primarysource/ieee/cli.py +79 -0
  99. primarysource/ieee/identify.py +24 -0
  100. primarysource/ieee/ieee802.py +47 -0
  101. primarysource/ieee/importer.py +305 -0
  102. primarysource/ieee/posix.py +233 -0
  103. primarysource/ieee/server.py +79 -0
  104. primarysource/index.py +446 -0
  105. primarysource/iso/__init__.py +51 -0
  106. primarysource/iso/cli.py +98 -0
  107. primarysource/iso/designation.py +95 -0
  108. primarysource/iso/identify.py +100 -0
  109. primarysource/iso/importer.py +224 -0
  110. primarysource/iso/library.py +249 -0
  111. primarysource/iso/server.py +69 -0
  112. primarysource/itu/__init__.py +41 -0
  113. primarysource/itu/__main__.py +8 -0
  114. primarysource/itu/asn1mod.py +478 -0
  115. primarysource/itu/catalog.py +472 -0
  116. primarysource/itu/cli.py +445 -0
  117. primarysource/itu/collection.py +20 -0
  118. primarysource/itu/corpus.py +118 -0
  119. primarysource/itu/errata.py +16 -0
  120. primarysource/itu/formal.py +565 -0
  121. primarysource/itu/identify.py +26 -0
  122. primarysource/itu/index.py +490 -0
  123. primarysource/itu/mcp.py +474 -0
  124. primarysource/itu/py.typed +0 -0
  125. primarysource/itu/sources.py +600 -0
  126. primarysource/itu/text.py +609 -0
  127. primarysource/kconfig/__init__.py +16 -0
  128. primarysource/kconfig/catalogue.py +98 -0
  129. primarysource/kconfig/cli.py +12 -0
  130. primarysource/kconfig/server.py +8 -0
  131. primarysource/model.py +63 -0
  132. primarysource/nist/__init__.py +5 -0
  133. primarysource/nist/collection.py +46 -0
  134. primarysource/nist/data/acquired-nist.json +1657 -0
  135. primarysource/nist/data/nist-catalog.json +12090 -0
  136. primarysource/nist/data/sources.toml +1996 -0
  137. primarysource/nist/identify.py +41 -0
  138. primarysource/nist/ingest/__init__.py +1 -0
  139. primarysource/nist/ingest/acquire_nist.py +128 -0
  140. primarysource/nist/ingest/controls.py +82 -0
  141. primarysource/nist/ingest/discover_nist.py +218 -0
  142. primarysource/nist/ingest/extract.py +48 -0
  143. primarysource/nist/ingest/fetch.py +211 -0
  144. primarysource/nist/ingest/index.py +14 -0
  145. primarysource/nist/ingest/oscal.py +480 -0
  146. primarysource/nist/ingest/prose_layouts.py +236 -0
  147. primarysource/nist/ingest/sources.py +94 -0
  148. primarysource/nist/ingest/status.py +255 -0
  149. primarysource/nist/models.py +7 -0
  150. primarysource/nist/paths.py +33 -0
  151. primarysource/nist/server/__init__.py +3 -0
  152. primarysource/nist/server/corpus.py +38 -0
  153. primarysource/nist/server/mcp.py +19 -0
  154. primarysource/nist/server/tools.py +35 -0
  155. primarysource/nist/spec.py +38 -0
  156. primarysource/pdfcorpus/__init__.py +16 -0
  157. primarysource/pdfcorpus/corpus.py +159 -0
  158. primarysource/pdfcorpus/fts.py +175 -0
  159. primarysource/pdfcorpus/index.py +70 -0
  160. primarysource/pdfcorpus/models.py +30 -0
  161. primarysource/pdfcorpus/server.py +96 -0
  162. primarysource/pdfcorpus/spec.py +37 -0
  163. primarysource/pdfcorpus/split.py +391 -0
  164. primarysource/pdfcorpus/tools.py +244 -0
  165. primarysource/pep/__init__.py +15 -0
  166. primarysource/pep/catalogue.py +191 -0
  167. primarysource/pep/cli.py +12 -0
  168. primarysource/pep/server.py +8 -0
  169. primarysource/pkgdocs/__init__.py +19 -0
  170. primarysource/pkgdocs/fetch.py +356 -0
  171. primarysource/pkgdocs/index.py +89 -0
  172. primarysource/pkgdocs/model.py +76 -0
  173. primarysource/pkgdocs/readers/__init__.py +1 -0
  174. primarysource/pkgdocs/readers/_common.py +134 -0
  175. primarysource/pkgdocs/readers/asciidoc.py +160 -0
  176. primarysource/pkgdocs/readers/latex.py +315 -0
  177. primarysource/pkgdocs/readers/markdown.py +173 -0
  178. primarysource/pkgdocs/readers/pod.py +132 -0
  179. primarysource/pkgdocs/readers/rst.py +239 -0
  180. primarysource/pkgdocs/readers/scdoc.py +108 -0
  181. primarysource/pkgdocs/runtime.py +552 -0
  182. primarysource/pkgdocs/serving.py +158 -0
  183. primarysource/pkgdocs/store.py +167 -0
  184. primarysource/pms/__init__.py +16 -0
  185. primarysource/pms/catalogue.py +88 -0
  186. primarysource/pms/cli.py +12 -0
  187. primarysource/pms/server.py +8 -0
  188. primarysource/py.typed +0 -0
  189. primarysource/readers/__init__.py +10 -0
  190. primarysource/readers/bookmarks.py +192 -0
  191. primarysource/readers/html.py +1312 -0
  192. primarysource/readers/markdown.py +189 -0
  193. primarysource/readers/pdf.py +187 -0
  194. primarysource/readers/xml.py +230 -0
  195. primarysource/refs.py +87 -0
  196. primarysource/registry.py +161 -0
  197. primarysource/render.py +49 -0
  198. primarysource/rfc/__init__.py +3 -0
  199. primarysource/rfc/__main__.py +6 -0
  200. primarysource/rfc/cite.py +204 -0
  201. primarysource/rfc/cli.py +903 -0
  202. primarysource/rfc/collection.py +19 -0
  203. primarysource/rfc/config.py +138 -0
  204. primarysource/rfc/errata.py +203 -0
  205. primarysource/rfc/fsutil.py +82 -0
  206. primarysource/rfc/graph.py +213 -0
  207. primarysource/rfc/index/__init__.py +1 -0
  208. primarysource/rfc/index/chunker.py +281 -0
  209. primarysource/rfc/index/embed.py +166 -0
  210. primarysource/rfc/index/hybrid.py +305 -0
  211. primarysource/rfc/index/lexical.py +223 -0
  212. primarysource/rfc/index/rfcxml.py +673 -0
  213. primarysource/rfc/index/search.py +508 -0
  214. primarysource/rfc/index/txtseg.py +585 -0
  215. primarysource/rfc/library.py +294 -0
  216. primarysource/rfc/mcp/__init__.py +1 -0
  217. primarysource/rfc/mcp/server.py +381 -0
  218. primarysource/rfc/packs/__init__.py +1 -0
  219. primarysource/rfc/packs/builder.py +228 -0
  220. primarysource/rfc/packs/collections/all-current-txt.json +10 -0
  221. primarysource/rfc/packs/collections/essentials.json +19 -0
  222. primarysource/rfc/packs/collections/fyi-current.json +10 -0
  223. primarysource/rfc/packs/collections/standards-current.json +10 -0
  224. primarysource/rfc/packs/collections/year-2025.json +10 -0
  225. primarysource/rfc/packs/collections/year-2026.json +10 -0
  226. primarysource/rfc/packs/collections.py +163 -0
  227. primarysource/rfc/packs/docsetexport.py +163 -0
  228. primarysource/rfc/packs/epub.py +159 -0
  229. primarysource/rfc/packs/export.py +306 -0
  230. primarysource/rfc/packs/htmlexport.py +82 -0
  231. primarysource/rfc/packs/serve.py +143 -0
  232. primarysource/rfc/packs/zimexport.py +171 -0
  233. primarysource/rfc/query.py +466 -0
  234. primarysource/rfc/subjects.py +457 -0
  235. primarysource/rfc/sync/__init__.py +1 -0
  236. primarysource/rfc/sync/downloader.py +132 -0
  237. primarysource/rfc/sync/manifest.py +162 -0
  238. primarysource/rfc/sync/scanner.py +49 -0
  239. primarysource/rfc/sync/state.py +464 -0
  240. primarysource/rfc/views.py +1067 -0
  241. primarysource/rpmdocs/__init__.py +15 -0
  242. primarysource/rpmdocs/catalogue.py +117 -0
  243. primarysource/rpmdocs/cli.py +12 -0
  244. primarysource/rpmdocs/server.py +8 -0
  245. primarysource/scaffold.py +223 -0
  246. primarysource/serve.py +75 -0
  247. primarysource/settings.py +152 -0
  248. primarysource/usb/__init__.py +5 -0
  249. primarysource/usb/collection.py +37 -0
  250. primarysource/usb/data/acquired.json +2025 -0
  251. primarysource/usb/data/sources-usb.toml +852 -0
  252. primarysource/usb/data/sources.toml +10 -0
  253. primarysource/usb/data/usb-catalog.json +1663 -0
  254. primarysource/usb/data/usb-classification.json +1986 -0
  255. primarysource/usb/data/usb-corpus-build.json +608 -0
  256. primarysource/usb/data/usb-pdf-choice.json +839 -0
  257. primarysource/usb/data/usb-split-measure.json +1803 -0
  258. primarysource/usb/ingest/__init__.py +1 -0
  259. primarysource/usb/ingest/acquire.py +130 -0
  260. primarysource/usb/ingest/discover_usb.py +88 -0
  261. primarysource/usb/ingest/extract.py +48 -0
  262. primarysource/usb/ingest/fetch.py +197 -0
  263. primarysource/usb/ingest/index.py +14 -0
  264. primarysource/usb/ingest/sources.py +85 -0
  265. primarysource/usb/models.py +7 -0
  266. primarysource/usb/paths.py +33 -0
  267. primarysource/usb/server/__init__.py +3 -0
  268. primarysource/usb/server/corpus.py +38 -0
  269. primarysource/usb/server/mcp.py +19 -0
  270. primarysource/usb/server/tools.py +35 -0
  271. primarysource/usb/spec.py +20 -0
  272. primarysource/usbids/__init__.py +56 -0
  273. primarysource/usbids/cli.py +65 -0
  274. primarysource/usbids/importer.py +272 -0
  275. primarysource/usbids/parse.py +283 -0
  276. primarysource/usbids/server.py +55 -0
  277. primarysource/usbids/store.py +80 -0
  278. primarysource/usbids/tools.py +174 -0
  279. primarysource/zim/__init__.py +5 -0
  280. primarysource/zim/__main__.py +8 -0
  281. primarysource/zim/_libzim.py +37 -0
  282. primarysource/zim/catalog.py +594 -0
  283. primarysource/zim/cli.py +555 -0
  284. primarysource/zim/collection.py +41 -0
  285. primarysource/zim/errors.py +100 -0
  286. primarysource/zim/groups.py +30 -0
  287. primarysource/zim/library.py +668 -0
  288. primarysource/zim/manage.py +636 -0
  289. primarysource/zim/markdown.py +1247 -0
  290. primarysource/zim/mcp.py +201 -0
  291. primarysource/zim/purl.py +130 -0
  292. primarysource/zim/py.typed +0 -0
  293. primarysource/zim/refs.py +90 -0
  294. primarysource/zim/service.py +1182 -0
  295. primarysource/zim/snapshot.py +197 -0
  296. primarysource/zim/sync.py +269 -0
  297. primarysource/zim/views.py +230 -0
  298. primarysource-0.1.0.dist-info/METADATA +118 -0
  299. primarysource-0.1.0.dist-info/RECORD +303 -0
  300. primarysource-0.1.0.dist-info/WHEEL +4 -0
  301. primarysource-0.1.0.dist-info/entry_points.txt +2 -0
  302. primarysource-0.1.0.dist-info/licenses/LICENSE +203 -0
  303. primarysource-0.1.0.dist-info/licenses/NOTICE +51 -0
@@ -0,0 +1,230 @@
1
+ """The text of a result: `service`'s payload as `render()` text with the core's fixed footer (PLAN.md §5.9).
2
+
3
+ `service` answers in dictionaries, which the CLI's `--json` prints as they are. The MCP tools and the CLI's human
4
+ output show the same text instead: the passage or the rows first, then footer lines in the core's fixed order
5
+ (`cite`, `unit`, `span`, `next`, `relaxed`, `confidence`, `warning`). What a JSON result carried as fields is a
6
+ footer line where it is a fact about the whole result, and part of a row where it belongs to one hit.
7
+
8
+ `next:` is exactly what to pass back (PLAN.md §5.11): a ref for a read, `start=<n>` for the same list tool.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any
14
+
15
+ from primarysource.render import next_start, render
16
+
17
+
18
+ def _age(fresh: dict[str, Any]) -> str:
19
+ if fresh.get("age_days") is None:
20
+ return "undated"
21
+ return f"{fresh['date']}, {fresh['age_days']} days old" + (", STALE" if fresh.get("stale") else "")
22
+
23
+
24
+ def _stale(fresh: dict[str, Any], what: str = "the ZIM") -> str:
25
+ """The warning for a ZIM past `library.STALE_DAYS`: a snapshot, so version details may have moved on."""
26
+ if fresh.get("stale"):
27
+ return f"{what} is a snapshot from {fresh['date']}, {fresh['age_days']} days old; check version details"
28
+ return ""
29
+
30
+
31
+ def _warn(*parts: str) -> str:
32
+ """One footer line: the non-empty parts, joined (a footer value is one line)."""
33
+ return "; ".join(" ".join(part.split()) for part in parts if part)
34
+
35
+
36
+ def _clamped(payload: dict[str, Any]) -> str:
37
+ clamped = payload.get("clamped")
38
+ if not clamped:
39
+ return ""
40
+ return "limits were lowered: " + ", ".join(f"{name} to {value}" for name, value in clamped.items())
41
+
42
+
43
+ def _row(row: dict[str, Any]) -> str:
44
+ """One library as a line: name, title, then what choosing one needs."""
45
+ bits = [row["search"], f"{row['articles']} articles", f"{row['size_bytes'] / 1e6:.1f} MB", _age(row["freshness"])]
46
+ if row.get("version") is not None and not isinstance(row["version"], dict):
47
+ bits.append(f"version {row['version']}")
48
+ label = f"{row['family']} [{', '.join(row['versions'])}]" if row.get("versions") else row["name"]
49
+ line = f"{label} {row['title']} [{'; '.join(bits)}]"
50
+ extra = []
51
+ if row.get("other_editions"):
52
+ extra.append(f"other editions: {', '.join(row['other_editions'])}")
53
+ if row.get("provenance") and isinstance(row["provenance"], str):
54
+ extra.append(f"provenance: {row['provenance']}")
55
+ if row.get("match"):
56
+ extra.append(f"match: {row['match']}")
57
+ if row.get("description"):
58
+ extra.append(f"description: {row['description']}")
59
+ if row.get("creator"):
60
+ extra.append(f"creator: {row['creator']}")
61
+ if row.get("tags"):
62
+ extra.append(f"tags: {', '.join(row['tags'])}")
63
+ if "license" in row:
64
+ extra.append(f"licence: {row['license'] or 'not stated in the ZIM'}")
65
+ if isinstance(row.get("provenance"), dict):
66
+ record = row["provenance"]
67
+ extra.append(
68
+ f"provenance: {record['state']}" + (f", from {record['source']}" if record.get("source") else "")
69
+ )
70
+ return "\n".join([line, *(f" {item}" for item in extra)])
71
+
72
+
73
+ def libraries(payload: dict[str, Any]) -> str:
74
+ rows = payload["libraries"]
75
+ if not rows:
76
+ body = f"no installed library matches {payload.get('query', '')!r}"
77
+ else:
78
+ body = "\n".join(_row(row) for row in rows)
79
+ problems = payload.get("problems") or []
80
+ warning = _warn(
81
+ f"{len(problems)} ZIM file(s) could not be read: " + "; ".join(f"{p['path']}: {p['error']}" for p in problems)
82
+ if problems
83
+ else "",
84
+ _clamped(payload),
85
+ )
86
+ head = f"{payload['total']} installed ({payload['directory']})"
87
+ return render(f"{head}\n{body}", {"next": next_start(payload.get("next")), "warning": warning})
88
+
89
+
90
+ def resolve(payload: dict[str, Any]) -> str:
91
+ version = payload["version"]
92
+ lines = [_row({**payload, "version": None})]
93
+ if "upstream" in version:
94
+ others = f"; also installed: {', '.join(payload['other_versions'])}" if payload.get("other_versions") else ""
95
+ lines.append(f" upstream version: {version['upstream']} ({version['selected']}){others}")
96
+ else:
97
+ lines.append(f" version: {version['note']}")
98
+ return render("\n".join(lines), {"warning": _stale(payload["freshness"])})
99
+
100
+
101
+ def _hit(hit: dict[str, Any]) -> str:
102
+ tags = [hit["match"]] + ([f"{hit['blocks']} blocks"] if "blocks" in hit else [])
103
+ if hit.get("language"):
104
+ tags.append(f"language {hit['language']}")
105
+ lines = [f"{hit['ref']} {hit['title']} [{'; '.join(tags)}]"]
106
+ if hit.get("excerpt"):
107
+ lines.append(f" {' '.join(hit['excerpt'].split())}")
108
+ if hit.get("note"):
109
+ lines.append(f" {hit['note']}")
110
+ if hit.get("section"):
111
+ section = hit["section"]
112
+ lines.append(f" best section: {section['ref']} {section['heading']} [{section['blocks']} blocks]")
113
+ return "\n".join(lines)
114
+
115
+
116
+ def _obtainable(payload: dict[str, Any]) -> list[str]:
117
+ """What the offline catalogue snapshot says could be installed, as lines a model relays to the user."""
118
+ lines = []
119
+ if payload.get("obtainable"):
120
+ age = payload.get("snapshot_age_days")
121
+ lines.append(f"not installed, but in the catalogue snapshot ({age} days old); give the user the command:")
122
+ lines += [f" {row['name']} {row['title']} -> {row['install']}" for row in payload["obtainable"]]
123
+ elif payload.get("user_action"):
124
+ lines.append(f"{payload['user_action']}")
125
+ return lines
126
+
127
+
128
+ def search(payload: dict[str, Any]) -> str:
129
+ hits = payload["hits"]
130
+ searched = ", ".join(
131
+ f"{name} (ZIM edition {payload['editions'].get(name) or 'undated'})" for name in payload["libraries_searched"]
132
+ )
133
+ lines = [f"searched: {searched}"]
134
+ lines += [_hit(hit) for hit in hits] if hits else [payload.get("note", "no entry matched")]
135
+ if hits and payload.get("note"):
136
+ lines.append(payload["note"])
137
+ for key in ("title_only", "ranking"):
138
+ if payload.get(key):
139
+ lines.append(f"({payload[key]})")
140
+ lines += _obtainable(payload)
141
+ partial = [hit for hit in hits if hit["match"] == "some terms"]
142
+ relaxed = f"{len(partial)} of {len(hits)} hit(s) matched only some of the words" if partial else ""
143
+ low = "low: no hit matched all the words, so the topic may not be covered; do not answer from these hits"
144
+ footer = {
145
+ "next": next_start(payload.get("next")),
146
+ "relaxed": relaxed,
147
+ "confidence": low if payload.get("confidence") == "low" and hits else "",
148
+ "warning": _clamped(payload),
149
+ }
150
+ return render("\n".join(lines), footer)
151
+
152
+
153
+ def read(payload: dict[str, Any]) -> str:
154
+ blocks = payload["blocks"]
155
+ left = f"; {payload['remaining_tokens']} tokens left" if payload.get("remaining_tokens") else ""
156
+ title = payload.get("page_title") or payload["title"]
157
+ head = f"{title} [¶{blocks['from']}-{blocks['to']} of {blocks['of']}{left}]"
158
+ if payload.get("redirected_from"):
159
+ head += f" (redirected from {payload['redirected_from']})"
160
+ warning = _warn(
161
+ _stale(payload["freshness"]),
162
+ payload.get("oversize", ""),
163
+ payload.get("converted_truncated", ""),
164
+ payload.get("images", ""),
165
+ _clamped(payload),
166
+ )
167
+ footer = {"cite": payload["cite"], "unit": "block", "next": payload.get("next") or "", "warning": warning}
168
+ return render(f"{head}\n\n{payload['content']}", footer)
169
+
170
+
171
+ def outline(payload: dict[str, Any]) -> str:
172
+ footer = {"next": next_start(payload.get("next")), "warning": _clamped(payload)}
173
+ if "outline" in payload:
174
+ rows = payload["outline"]
175
+ lines = [f"{payload['ref']} {payload['title']} [{payload['total']} rows]"]
176
+ for row in rows:
177
+ indent = " " * (int(row.get("level") or 4) - 1)
178
+ anchor = f" #{row['anchor']}" if row.get("anchor") else ""
179
+ kind = " (definition)" if row.get("kind") else ""
180
+ lines.append(f"{indent}{row['text']}{anchor}{kind}")
181
+ if not rows:
182
+ lines.append("no headings or definitions")
183
+ return render("\n".join(lines), footer)
184
+ footer["warning"] = _warn(_stale(payload["freshness"]), footer["warning"])
185
+ lines = [f"{row['ref']} {row['title']}" for row in payload["entries"]]
186
+ if not lines:
187
+ lines = [f"no entries in {payload['library']} match"]
188
+ return render("\n".join(lines), footer)
189
+
190
+
191
+ def links(payload: dict[str, Any]) -> str:
192
+ lines = [f"{payload['ref']} [{payload['total']} links]"]
193
+ for row in payload["links"]:
194
+ target = row.get("url") or row["ref"] + ("" if row.get("in_zim") else " (not in this ZIM)")
195
+ lines.append(f"{row['text']} -> {target}")
196
+ if not payload["links"]:
197
+ lines.append("no links")
198
+ return render("\n".join(lines), {"next": next_start(payload.get("next")), "warning": _clamped(payload)})
199
+
200
+
201
+ def cite(payload: dict[str, Any]) -> str:
202
+ lines = [payload["cite"], f"licence: {payload['license']}"]
203
+ if payload.get("creator"):
204
+ lines.append(f"creator: {payload['creator']}")
205
+ record = payload["provenance"]
206
+ lines.append(f"ZIM {payload['uuid']}; provenance: {record['state']}")
207
+ footer = {
208
+ "cite": payload["cite"],
209
+ "unit": "block" if "span" in payload else "",
210
+ "span": payload.get("span", ""),
211
+ "warning": _stale(payload["freshness"]),
212
+ }
213
+ return render("\n".join(lines), footer)
214
+
215
+
216
+ def available(payload: dict[str, Any]) -> str:
217
+ snap = payload["snapshot"]
218
+ lines = []
219
+ for row in payload["available"]:
220
+ state = f"INSTALLED {row['installed_edition']}" if row["installed"] else "not installed"
221
+ lines.append(
222
+ f"{row['name']} {row['title']} [{row['date']}; ~{row['approx_mb']} MB; {row['articles']} articles; "
223
+ f"{state}]\n install: {row['install']}"
224
+ )
225
+ if not payload["available"]:
226
+ lines.append(f"nothing in the snapshot matches {payload['query']!r}")
227
+ lines.append(
228
+ f"({payload['matches']} matches; snapshot of {snap['fetched_at']}, {snap['age_days']} days old, offline)"
229
+ )
230
+ return render("\n".join(lines), {"next": next_start(payload.get("next"))})
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.5
2
+ Name: primarysource
3
+ Version: 0.1.0
4
+ Summary: Citable primary-source libraries for agents: RFCs, NIST, USB-IF, ITU-T, Kiwix and CVE collections behind one core (ref grammar, token-budgeted paging, full-text index, CLI, MCP server).
5
+ Author: Justin Capella
6
+ License-Expression: Apache-2.0
7
+ License-File: LICENSE
8
+ License-File: NOTICE
9
+ Keywords: citation,documents,full-text-search,mcp,model-context-protocol,standards
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Text Processing :: Indexing
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.12
22
+ Provides-Extra: bbf
23
+ Requires-Dist: pypdf>=5; extra == 'bbf'
24
+ Provides-Extra: c2sp
25
+ Provides-Extra: cavp
26
+ Provides-Extra: cve
27
+ Provides-Extra: debpolicy
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.20; extra == 'dev'
30
+ Requires-Dist: packaging>=24; extra == 'dev'
31
+ Requires-Dist: pytest>=8; extra == 'dev'
32
+ Requires-Dist: ruff>=0.15; extra == 'dev'
33
+ Provides-Extra: draft
34
+ Provides-Extra: ecma
35
+ Requires-Dist: pypdf>=5; extra == 'ecma'
36
+ Provides-Extra: gitdocs
37
+ Provides-Extra: iana
38
+ Provides-Extra: ieee
39
+ Requires-Dist: pypdf>=5; extra == 'ieee'
40
+ Provides-Extra: iso
41
+ Requires-Dist: pypdf>=5; extra == 'iso'
42
+ Provides-Extra: itu
43
+ Requires-Dist: pypdf>=5; extra == 'itu'
44
+ Provides-Extra: kconfig
45
+ Provides-Extra: mcp
46
+ Requires-Dist: mcp>=2; extra == 'mcp'
47
+ Provides-Extra: nist
48
+ Provides-Extra: pep
49
+ Provides-Extra: pms
50
+ Provides-Extra: rfc
51
+ Requires-Dist: httpx>=0.27; extra == 'rfc'
52
+ Requires-Dist: rich>=13; extra == 'rfc'
53
+ Requires-Dist: typer>=0.12; extra == 'rfc'
54
+ Provides-Extra: rfc-semantic
55
+ Requires-Dist: httpx>=0.27; extra == 'rfc-semantic'
56
+ Requires-Dist: model2vec>=0.3; extra == 'rfc-semantic'
57
+ Requires-Dist: rich>=13; extra == 'rfc-semantic'
58
+ Requires-Dist: sqlite-vec; extra == 'rfc-semantic'
59
+ Requires-Dist: typer>=0.12; extra == 'rfc-semantic'
60
+ Provides-Extra: rfc-zim
61
+ Requires-Dist: httpx>=0.27; extra == 'rfc-zim'
62
+ Requires-Dist: libzim>=3.7; extra == 'rfc-zim'
63
+ Requires-Dist: rich>=13; extra == 'rfc-zim'
64
+ Requires-Dist: typer>=0.12; extra == 'rfc-zim'
65
+ Provides-Extra: rpmdocs
66
+ Provides-Extra: usb
67
+ Provides-Extra: usbids
68
+ Provides-Extra: zim
69
+ Requires-Dist: libzim>=4.1; (sys_platform != 'win32' or platform_machine != 'ARM64') and extra == 'zim'
70
+ Description-Content-Type: text/markdown
71
+
72
+ # primarysource
73
+
74
+ A primary source is the text itself: answer from it, and say exactly where it is.
75
+
76
+ One package, many collections. The core is a sectioned-document model, a ref grammar, token-budgeted paging, a fixed result shape, readers, a full-text index, a CLI and an MCP server. Each collection is a subpackage behind an extra: `rfc` (the RFC series, moved in from rfclib), `nist`, `usb`, `itu`, `zim`, `cve`, and more. `PLAN.md` has the design and `ROADMAP.md` the open work.
77
+
78
+ ```
79
+ pip install "primarysource[rfc,mcp]"
80
+ primarysource collections
81
+ primarysource mcp --collection rfc
82
+ ```
83
+
84
+ Pre-alpha. It ships no document text: you fetch the sources yourself.
85
+
86
+ ## What works today
87
+
88
+ ```python
89
+ from primarysource.budget import page
90
+ from primarysource.model import Document, Section, Unit, span_hash
91
+ from primarysource.refs import RefGrammar
92
+ from primarysource.render import render
93
+
94
+ grammar = RefGrammar(r"(?:RFC|BCP|STD)\d+")
95
+ ref = grammar.parse("RFC9000§9.6¶2") # Ref(doc='RFC9000', section='9.6', para=2)
96
+
97
+ document = Document("demo", "Demo", Unit.SENTENCE, (Section("1", "Scope", ("First.", "Second.")),))
98
+ section = document.section("1")
99
+ result = page(section.units, start=1, max_tokens=2000)
100
+ print(render(" ".join(result.units), {"cite": "Demo §1", "unit": document.unit, "span": span_hash("First.")}))
101
+ ```
102
+
103
+ | Module | What it does |
104
+ |---|---|
105
+ | `primarysource.model` | `Document`, `Section`, the declared `Unit` that `¶n` counts, `span_hash` |
106
+ | `primarysource.refs` | `RefGrammar`: `<doc>§<section>[#anchor][¶n]` and `<doc>:<first>-<last>`, for a library's own document-id pattern |
107
+ | `primarysource.budget` | `tokens`, `page`: whole units up to a token budget, always making progress, with `next_start` |
108
+ | `primarysource.render` | body plus `cite:`, `unit:`, `span:`, `next:`, `relaxed:`, `warning:` footer lines in a fixed order |
109
+ | `primarysource.errors` | `LibError(message, hint)`: an error that names its remedy |
110
+ | `primarysource.home` | `resolve_home(app)`: `$<APP>_HOME` or the per-user data directory; `atomic_write` |
111
+
112
+ ## What it does not do yet
113
+
114
+ It reads no file format, builds no index, has no command and serves nothing. No library depends on it.
115
+
116
+ ## Tests
117
+
118
+ `python -m pytest -q` from this directory. All tests use invented text.