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,309 @@
1
+ """Read, outline, search and cite the installed C2SP specifications. Local files only; nothing here fetches.
2
+
3
+ A ref is `c2sp:static-ct-api@09f3e51§Log Entries¶2`, with the `c2sp:` prefix and the `@<commit prefix>` both
4
+ optional (no `@` means the newest import). `§` names a section by its heading text, or by its GitHub-style slug
5
+ (`log-entries`) when the heading contains `:`, `#`, `¶` or `§`, or when two headings read alike. `¶n` is the nth
6
+ block of that section's span (see `markdown.py`). A ref with no section is the whole document.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import difflib
12
+ import json
13
+ import re
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ from primarysource import budget, cursor
18
+ from primarysource.c2sp import markdown
19
+ from primarysource.c2sp._fetch import sha256
20
+ from primarysource.c2sp.catalogue import DOC_ID
21
+ from primarysource.c2sp.catalogue import catalogue as packaged_catalogue
22
+ from primarysource.errors import LibError
23
+ from primarysource.home import collection_home
24
+ from primarysource.model import span_hash
25
+ from primarysource.refs import Ref, RefGrammar
26
+ from primarysource.render import render
27
+
28
+ PREFIX = "c2sp:"
29
+ IMPORT = "primarysource c2sp import"
30
+ _COMMIT_PREFIX = re.compile(r"[0-9a-f]{7,40}")
31
+ _UNSAFE_IN_REF = re.compile(r"[:#¶§]")
32
+ _HEADING_LINE = re.compile(r"#{1,6}(?: |$)")
33
+ _TERM = re.compile(r"\w+", re.UNICODE)
34
+ MAX_QUERY_TERMS = 16
35
+
36
+
37
+ def grammar() -> RefGrammar:
38
+ from primarysource.c2sp import C2SP_DOC, C2SP_SECTION
39
+
40
+ return RefGrammar(C2SP_DOC, C2SP_SECTION)
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class Loaded:
45
+ """One installed specification, parsed."""
46
+
47
+ id: str
48
+ commit: str
49
+ title: str
50
+ document: markdown.MarkdownDocument
51
+
52
+ @property
53
+ def short(self) -> str:
54
+ return self.commit[:7]
55
+
56
+
57
+ def default_home() -> Path:
58
+ return collection_home("c2sp")
59
+
60
+
61
+ def installs(home: Path) -> list[tuple[str, dict[str, object]]]:
62
+ """Every install under `home` as (commit, record), newest import first. A directory without a readable
63
+ `install.json` is not an install."""
64
+ found: list[tuple[str, dict[str, object]]] = []
65
+ if home.is_dir():
66
+ for child in home.iterdir():
67
+ record = child / "install.json"
68
+ if child.is_dir() and re.fullmatch(r"[0-9a-f]{40}", child.name) and record.is_file():
69
+ try:
70
+ loaded = json.loads(record.read_text(encoding="utf-8"))
71
+ except (ValueError, OSError):
72
+ continue
73
+ if isinstance(loaded, dict) and loaded.get("commit") == child.name:
74
+ found.append((child.name, loaded))
75
+ found.sort(key=lambda item: str(item[1].get("imported", "")), reverse=True)
76
+ return found
77
+
78
+
79
+ def _split_doc(token: str) -> tuple[str, str | None]:
80
+ doc, _, version = token.partition("@")
81
+ if not DOC_ID.fullmatch(doc):
82
+ raise LibError(f"not a C2SP specification id: {doc!r}", "c2sp_docs lists the specification ids")
83
+ if version and not _COMMIT_PREFIX.fullmatch(version):
84
+ raise LibError(f"not a commit prefix: {version!r}", "use at least 7 lowercase hex digits of the commit")
85
+ return doc, version or None
86
+
87
+
88
+ def load(token: str, home: Path | None = None) -> Loaded:
89
+ """Load `doc[@commit-prefix]`, verifying the file against the install record's sha256."""
90
+ home = home or default_home()
91
+ doc_id, version = _split_doc(token)
92
+ available = installs(home)
93
+ if not available:
94
+ raise LibError("no C2SP specifications are imported", "c2sp_docs lists the catalogue", IMPORT)
95
+ for commit, record in available:
96
+ if version and not commit.startswith(version):
97
+ continue
98
+ files = record.get("files")
99
+ pin = files.get(doc_id) if isinstance(files, dict) else None
100
+ if not isinstance(pin, str):
101
+ continue
102
+ data = (home / commit / f"{doc_id}.md").read_bytes() if (home / commit / f"{doc_id}.md").is_file() else None
103
+ if data is None:
104
+ continue
105
+ if sha256(data) != pin:
106
+ raise LibError(f"{doc_id}@{commit[:7]} on disk does not match its recorded sha256", "", IMPORT)
107
+ parsed = markdown.parse(data)
108
+ return Loaded(doc_id, commit, parsed.title or doc_id, parsed)
109
+ have = ", ".join(f"{c[:7]}" for c, _ in available)
110
+ what = f"{doc_id}@{version}" if version else doc_id
111
+ raise LibError(f"{what} is not imported (imported commits: {have})", "c2sp_docs lists what is imported", IMPORT)
112
+
113
+
114
+ def parse_ref(text: str) -> Ref:
115
+ """Parse a c2sp ref, accepting the optional `c2sp:` prefix."""
116
+ body = text.strip()
117
+ if body.lower().startswith(PREFIX):
118
+ body = body[len(PREFIX) :]
119
+ body, doc_para = cursor.split_doc_para(body)
120
+ ref = grammar().parse(body)
121
+ if doc_para is not None:
122
+ ref = Ref(ref.doc, None, doc_para, None, None)
123
+ if ref.lines is not None:
124
+ raise LibError(f"not a c2sp ref: {text!r}", "c2sp refs address headings and blocks, not line ranges")
125
+ if ref.anchor is not None:
126
+ raise LibError(f"not a c2sp ref: {text!r}", "c2sp has no #anchor; name the heading after §")
127
+ if ref.doc is None:
128
+ raise LibError(f"not a c2sp ref: {text!r}", "start with the specification id, e.g. static-ct-api")
129
+ return Ref(ref.doc, ref.section.strip() if ref.section else None, ref.para, None, None)
130
+
131
+
132
+ def find_heading(doc: Loaded, name: str) -> tuple[markdown.Heading, str]:
133
+ """The heading `name` names, and a warning ("" if none). Exact title, then slug, then case-folded title."""
134
+ headings = doc.document.headings
135
+ wanted = name.strip()
136
+ for match in (
137
+ [h for h in headings if h.title == wanted],
138
+ [h for h in headings if h.slug == wanted.lower()],
139
+ [h for h in headings if h.title.casefold() == wanted.casefold()],
140
+ ):
141
+ if match:
142
+ warning = ""
143
+ if len(match) > 1:
144
+ slugs = ", ".join(h.slug for h in match)
145
+ warning = f"{len(match)} headings are titled {wanted!r}; this is the first. Use a slug: {slugs}"
146
+ return match[0], warning
147
+ close = difflib.get_close_matches(wanted, [h.title for h in headings], n=3, cutoff=0.5)
148
+ hint = f"did you mean: {', '.join(close)}" if close else f"c2sp_outline('{doc.id}') lists the headings"
149
+ raise LibError(f"no section {wanted!r} in {doc.id}@{doc.short}", hint)
150
+
151
+
152
+ def section_ref(doc: Loaded, heading: markdown.Heading) -> str:
153
+ """The canonical ref of a heading: its title, or its slug if the title cannot appear in a ref."""
154
+ same = [h for h in doc.document.headings if h.title == heading.title]
155
+ use_slug = _UNSAFE_IN_REF.search(heading.title) is not None or len(same) > 1
156
+ return f"{doc.id}@{doc.short}§{heading.slug if use_slug else heading.title}"
157
+
158
+
159
+ def _cite_text(doc: Loaded, heading: markdown.Heading | None, para: int | str | None) -> str:
160
+ where = f", §{heading.title}" if heading else ""
161
+ if para:
162
+ where += f" ¶{para}"
163
+ return (
164
+ f"C2SP, {doc.id} ({doc.title}), commit {doc.short}{where}. CC BY 4.0. "
165
+ f"https://github.com/C2SP/C2SP/blob/{doc.commit}/{doc.id}.md"
166
+ )
167
+
168
+
169
+ def _doc(ref: Ref) -> str:
170
+ """The document of a parsed ref; `parse_ref` has already refused a ref without one."""
171
+ if ref.doc is None:
172
+ raise LibError("the ref names no document", "start with the document id")
173
+ return ref.doc
174
+
175
+
176
+ def _select(ref: Ref, home: Path | None) -> tuple[Loaded, markdown.Heading | None, tuple[str, ...], str]:
177
+ doc = load(_doc(ref), home)
178
+ if ref.section is None:
179
+ return doc, None, doc.document.units(), ""
180
+ heading, warning = find_heading(doc, ref.section)
181
+ return doc, heading, heading.units, warning
182
+
183
+
184
+ def read(ref_text: str, max_tokens: int = 2000, home: Path | None = None) -> str:
185
+ """The text of a ref, cut at block boundaries within `max_tokens`. A `¶n` ref starts at block n and runs to the
186
+ end of its scope; the `next:` footer is the ref that reads the next page."""
187
+ ref = parse_ref(ref_text)
188
+ doc, heading, units, warning = _select(ref, home)
189
+ if not units:
190
+ raise LibError(f"{ref_text}: that section has no text of its own", "read its subsections, see c2sp_outline")
191
+ first = cursor.first_unit(ref.para, len(units), "blocks", ref_text)
192
+ chosen = budget.page(units, start=first, max_tokens=max_tokens)
193
+ body = "\n\n".join(chosen.units)
194
+ scope = section_ref(doc, heading) if heading else f"{doc.id}@{doc.short}"
195
+ footer = {
196
+ "cite": _cite_text(doc, heading, cursor.para_label(ref.para, chosen)),
197
+ "unit": "block",
198
+ "span": span_hash(body),
199
+ "next": cursor.read_next(scope, chosen),
200
+ "warning": warning,
201
+ }
202
+ return render(body, footer)
203
+
204
+
205
+ def outline(token: str, home: Path | None = None) -> str:
206
+ """The headings of one specification, indented by level, each with the ref that reads it."""
207
+ doc = load(token, home)
208
+ lines = [f"{doc.title} [{doc.id}@{doc.short}]"]
209
+ for heading in doc.document.headings:
210
+ indent = " " * (heading.level - 1)
211
+ lines.append(f"{indent}{heading.title} {section_ref(doc, heading)} ({len(heading.units)} blocks)")
212
+ return "\n".join(lines)
213
+
214
+
215
+ def _terms(query: str) -> list[str]:
216
+ terms = [t for t in _TERM.findall(query.casefold()) if len(t) <= 64]
217
+ if not terms:
218
+ raise LibError("the query has no searchable words", "use words or identifiers, e.g. 'checkpoint origin'")
219
+ return terms[:MAX_QUERY_TERMS]
220
+
221
+
222
+ def _snippet(text: str, terms: list[str]) -> str:
223
+ flat = " ".join(text.split())
224
+ low = flat.casefold()
225
+ at = min((i for i in (low.find(t) for t in terms) if i >= 0), default=0)
226
+ begin = max(0, at - 60)
227
+ return ("..." if begin else "") + flat[begin : begin + 160] + ("..." if begin + 160 < len(flat) else "")
228
+
229
+
230
+ def search(
231
+ query: str,
232
+ doc: str | None = None,
233
+ limit: int = 10,
234
+ start: int = 0,
235
+ home: Path | None = None,
236
+ ) -> str:
237
+ """Blocks containing every query word (else at least half of them, flagged `relaxed`), best first: one line
238
+ per hit, `<ref>¶n: <snippet>`. Ranked by term frequency with a bonus for a match in the heading."""
239
+ terms = _terms(query)
240
+ home = home or default_home()
241
+ available = installs(home)
242
+ if not available:
243
+ raise LibError("no C2SP specifications are imported", "c2sp_docs lists the catalogue", IMPORT)
244
+ ids: list[str] = []
245
+ if doc:
246
+ ids = [doc]
247
+ else:
248
+ record_files = available[0][1].get("files")
249
+ ids = sorted(record_files) if isinstance(record_files, dict) else []
250
+ scored: list[tuple[float, int, str, str]] = []
251
+ relaxed = False
252
+ for need_all in (True, False):
253
+ for order, token in enumerate(ids):
254
+ loaded = load(token, home)
255
+ for heading in loaded.document.headings:
256
+ title = heading.title.casefold()
257
+ for index, block in enumerate(heading.units, start=1):
258
+ if _HEADING_LINE.match(block):
259
+ break # what follows belongs to a subsection, which is searched under its own heading
260
+ words = _TERM.findall(block.casefold())
261
+ hits = [words.count(t) for t in terms]
262
+ present = sum(1 for h in hits if h)
263
+ if present == 0 or (need_all and present < len(terms)) or present * 2 < len(terms):
264
+ continue
265
+ score = sum(hits) + 3 * present + sum(2 for t in terms if t in title)
266
+ ref = f"{section_ref(loaded, heading)}¶{index}"
267
+ scored.append((-score, order, ref, _snippet(block, terms)))
268
+ if scored:
269
+ relaxed = not need_all
270
+ break
271
+ if not scored:
272
+ return render("no matches", {})
273
+ scored.sort()
274
+ shown, nxt = cursor.window(scored, start, limit)
275
+ body = "\n".join(f"{ref}: {snip}" for _, _, ref, snip in shown)
276
+ return render(body, {"next": nxt, "relaxed": "only some query words matched" if relaxed else ""})
277
+
278
+
279
+ def cite(ref_text: str, home: Path | None = None) -> str:
280
+ """The citation string for a ref, with the span hash of the exact text cited."""
281
+ ref = parse_ref(ref_text)
282
+ doc, heading, units, warning = _select(ref, home)
283
+ if ref.para is not None:
284
+ if ref.para > len(units):
285
+ raise LibError(f"{ref_text}: the span has {len(units)} blocks", f"use ¶1 to ¶{len(units)}")
286
+ units = (units[ref.para - 1],)
287
+ footer = {
288
+ "cite": _cite_text(doc, heading, ref.para),
289
+ "unit": "block",
290
+ "span": span_hash("\n\n".join(units)),
291
+ "warning": warning,
292
+ }
293
+ return render(f"{doc.id}@{doc.short}" + (f"§{heading.title}" if heading else ""), footer)
294
+
295
+
296
+ def docs(home: Path | None = None) -> str:
297
+ """The catalogue with what is imported: one line per specification."""
298
+ home = home or default_home()
299
+ cat = packaged_catalogue()
300
+ present: set[str] = set()
301
+ commits = installs(home)
302
+ for _, record in commits:
303
+ files = record.get("files")
304
+ if isinstance(files, dict):
305
+ present.update(files)
306
+ lines = [f"commit {cat.commit[:7]} ({cat.terms.licence}, placement {cat.terms.placement})"]
307
+ for spec in cat.specs:
308
+ lines.append(f"{'imported' if spec.id in present else 'not imported':<13} {spec.id} {spec.title}")
309
+ return "\n".join(lines)
@@ -0,0 +1,31 @@
1
+ """The c2sp collection's Markdown reader: a re-export of the core's `primarysource.readers.markdown`.
2
+
3
+ The reader moved to the core (`cl.reader_markdown`); this module keeps `primarysource.c2sp.markdown` importable so
4
+ the importer, the library and their tests are unchanged.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from primarysource.readers.markdown import (
10
+ MAX_BYTES,
11
+ MAX_LINE,
12
+ MAX_SECTIONS,
13
+ Heading,
14
+ MarkdownDocument,
15
+ decode,
16
+ parse,
17
+ plain,
18
+ slugify,
19
+ )
20
+
21
+ __all__ = [
22
+ "MAX_BYTES",
23
+ "MAX_LINE",
24
+ "MAX_SECTIONS",
25
+ "Heading",
26
+ "MarkdownDocument",
27
+ "decode",
28
+ "parse",
29
+ "plain",
30
+ "slugify",
31
+ ]
@@ -0,0 +1,68 @@
1
+ """MCP server for the c2sp collection: `primarysource mcp --collection c2sp`.
2
+
3
+ Imported only when served, so `mcp` (the `[mcp]` extra) is needed only then. Tools carry the `c2sp_` prefix,
4
+ answer in text with the fixed footer, and turn a `LibError` into an MCP tool error naming the remedy. Every tool
5
+ reads local files only; none writes or fetches.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from mcp.server.mcpserver import MCPServer
13
+ from mcp.types import ToolAnnotations
14
+
15
+ from primarysource import __version__
16
+ from primarysource.c2sp import library
17
+ from primarysource.scaffold import add_tools
18
+
19
+ READ_ONLY = ToolAnnotations(read_only_hint=True, idempotent_hint=True, destructive_hint=False, open_world_hint=False)
20
+
21
+ INSTRUCTIONS = (
22
+ "C2SP specifications (CC BY 4.0) at a pinned commit: static-ct-api, tlog-tiles, tlog-checkpoint, signed-note, "
23
+ "and the rest of the C2SP repository's top-level documents. Loop: c2sp_search('<words>') -> c2sp_read('<ref>'). "
24
+ "Refs: 'static-ct-api§Log Entries' (a heading by its text or slug), '...¶2' (its 2nd block), "
25
+ "'static-ct-api@09f3e51§...' to pin a commit. c2sp_outline('<doc>') lists headings as refs; c2sp_cite('<ref>') "
26
+ "gives the citation and the hash of the cited text. If nothing is imported, the error says how."
27
+ )
28
+
29
+
30
+ def c2sp_read(ref: str, max_tokens: int = 2000) -> str:
31
+ """Read C2SP text by ref, cut at block boundaries within max_tokens. A 'next: <ref>' footer line is the ref
32
+ that reads the next page: pass it back as ref.
33
+
34
+ ref: 'static-ct-api' (whole document) | 'static-ct-api§Log Entries' (a section, subsections included) |
35
+ '...§Log Entries¶2' (from its 2nd block) | 'static-ct-api@09f3e51§...' (pinned commit). A 'c2sp:' prefix is
36
+ allowed. c2sp_cite gives exactly one block."""
37
+ return library.read(ref, max_tokens=max_tokens)
38
+
39
+
40
+ def c2sp_search(query: str, doc: str | None = None, limit: int = 10, start: int = 0) -> str:
41
+ """Find blocks containing the query words (all of them, else at least half, flagged 'relaxed'). One line per
42
+ hit: '<ref>¶n: <snippet>'. doc restricts to one specification. A 'next: start=N' footer line means call again
43
+ with start=N (a 0-based offset)."""
44
+ return library.search(query, doc=doc, limit=limit, start=start)
45
+
46
+
47
+ def c2sp_outline(doc: str) -> str:
48
+ """List a specification's headings, indented by level, each with the ref that reads it. doc: 'tlog-tiles'."""
49
+ return library.outline(doc)
50
+
51
+
52
+ def c2sp_cite(ref: str) -> str:
53
+ """The citation string for a ref, with the sha256 prefix of the exact text cited (cite: and span: lines)."""
54
+ return library.cite(ref)
55
+
56
+
57
+ def c2sp_docs() -> str:
58
+ """List the catalogued specifications and whether each is imported."""
59
+ return library.docs()
60
+
61
+
62
+ def _build() -> Any:
63
+ built = MCPServer("primarysource-c2sp", version=__version__, instructions=INSTRUCTIONS)
64
+ add_tools(built, (c2sp_read, c2sp_search, c2sp_outline, c2sp_cite, c2sp_docs), annotations=READ_ONLY)
65
+ return built
66
+
67
+
68
+ server = _build()
@@ -0,0 +1,42 @@
1
+ """NIST cryptographic test vectors as a primarysource collection: CAVP `.rsp` files and ACVP JSON vector sets.
2
+
3
+ Vectors are not documents: the unit is a case, and the ref names set, group and case
4
+ (`nist:cavp/ecdsa/SigVer.rsp#[P-256,SHA-256]/3`), so this is its own collection rather than part of `nist`.
5
+ Importing this package needs only the standard library; `mcp` is imported only by `server.py` when served.
6
+ `docs/cavp.md` is the user-facing description.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from primarysource.cavp.model import format_ref, parse_ref
12
+ from primarysource.errors import LibError
13
+ from primarysource.model import Unit
14
+ from primarysource.refs import Ref
15
+ from primarysource.registry import Collection
16
+
17
+
18
+ class CavpRefs:
19
+ """The collection's `RefParser`, over `model.parse_ref`: the set id is `doc`, the group `section` and the case
20
+ `anchor` (`nist:cavp/ecdsa/SigVer.rsp#[P-256,SHA-256]/3`)."""
21
+
22
+ def parse(self, text: str) -> Ref:
23
+ ref = parse_ref(text)
24
+ return Ref(ref.set_id, ref.group, None, ref.case)
25
+
26
+ def format(self, ref: Ref) -> str:
27
+ if ref.doc is None or ref.para is not None or ref.lines is not None:
28
+ raise LibError(f"not a vector ref: {ref!r}", "a vector ref is a set, optionally a group and a case")
29
+ return format_ref(ref.doc, ref.section, ref.anchor)
30
+
31
+
32
+ COLLECTION = Collection(
33
+ name="cavp",
34
+ title="NIST cryptographic test vectors (CAVP .rsp, ACVP JSON), pinned by sha256",
35
+ prefix="cavp_",
36
+ unit=Unit.BLOCK,
37
+ grammar=CavpRefs(),
38
+ server="primarysource.cavp.server:cavp_server",
39
+ cli="primarysource.cavp.cli:main",
40
+ )
41
+
42
+ __all__ = ["COLLECTION"]
@@ -0,0 +1,84 @@
1
+ """`primarysource cavp ...`: the collection's own commands.
2
+
3
+ primarysource cavp fetch [id ...] download the pinned sources in vectors.toml, import them
4
+ primarysource cavp import <id> <file> [<expected-file>] import files you already have, under the same pins
5
+ primarysource cavp pin <file> print the sha256 to pin
6
+ primarysource cavp sets [query]
7
+ primarysource cavp groups <set-ref>
8
+ primarysource cavp read <ref> [--max-tokens N]
9
+ primarysource cavp find <set-ref> <field> <value> [--group G]
10
+ primarysource cavp cite <ref>
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import sys
17
+ from collections.abc import Sequence
18
+ from pathlib import Path
19
+
20
+ from primarysource.cavp import importer, tools
21
+ from primarysource.cavp.model import sha256_bytes
22
+ from primarysource.cavp.paths import open_store
23
+ from primarysource.errors import LibError
24
+
25
+
26
+ def _parser() -> argparse.ArgumentParser:
27
+ parser = argparse.ArgumentParser(prog="primarysource cavp", description="NIST CAVP/ACVP test vectors")
28
+ sub = parser.add_subparsers(dest="command", required=True)
29
+ sub.add_parser("fetch", help="download and import the pinned sources").add_argument("ids", nargs="*")
30
+ imp = sub.add_parser("import", help="import local files under a vectors.toml pin")
31
+ imp.add_argument("id")
32
+ imp.add_argument("file", type=Path)
33
+ imp.add_argument("expected", type=Path, nargs="?")
34
+ sub.add_parser("pin", help="print a file's sha256").add_argument("file", type=Path)
35
+ sub.add_parser("sets").add_argument("query", nargs="?")
36
+ sub.add_parser("groups").add_argument("ref")
37
+ read = sub.add_parser("read")
38
+ read.add_argument("ref")
39
+ read.add_argument("--max-tokens", type=int, default=2000)
40
+ find = sub.add_parser("find")
41
+ find.add_argument("ref")
42
+ find.add_argument("field")
43
+ find.add_argument("value")
44
+ find.add_argument("--group")
45
+ sub.add_parser("cite").add_argument("ref")
46
+ return parser
47
+
48
+
49
+ def _say(text: str) -> int:
50
+ sys.stdout.write(text if text.endswith("\n") else text + "\n")
51
+ return 0
52
+
53
+
54
+ def main(argv: Sequence[str]) -> int:
55
+ args = _parser().parse_args(list(argv))
56
+ store = open_store()
57
+ try:
58
+ if args.command == "pin":
59
+ return _say(sha256_bytes(args.file.read_bytes()))
60
+ if args.command in ("fetch", "import"):
61
+ known = {s.id: s for s in importer.load_sources()}
62
+ wanted = args.ids if args.command == "fetch" else [args.id]
63
+ unknown = [i for i in wanted if i not in known]
64
+ if unknown:
65
+ raise LibError(f"not in vectors.toml: {', '.join(unknown)}", f"known: {', '.join(known)}")
66
+ for source in (known[i] for i in (wanted or list(known))):
67
+ if args.command == "fetch":
68
+ entry = importer.fetch(store, source)
69
+ else:
70
+ entry = importer.import_files(store, source, args.file, args.expected)
71
+ print(f"{entry.id}: {entry.groups} groups, {entry.cases} cases, sha256 {entry.sha256}")
72
+ return 0
73
+ if args.command == "sets":
74
+ return _say(tools.sets(store, args.query))
75
+ if args.command == "groups":
76
+ return _say(tools.groups(store, args.ref))
77
+ if args.command == "read":
78
+ return _say(tools.read(store, args.ref, args.max_tokens))
79
+ if args.command == "find":
80
+ return _say(tools.find(store, args.ref, args.field, args.value, args.group))
81
+ return _say(tools.cite(store, args.ref))
82
+ except LibError as exc:
83
+ print(exc.as_text(), file=sys.stderr)
84
+ return 1
@@ -0,0 +1,47 @@
1
+ # Vector sets `primarysource cavp fetch` may download. Each entry pins the sha256 of the artifact it downloads (the
2
+ # zip, or the ACVP prompt.json), measured 2026-11-13. No vector data is stored in this repository.
3
+ #
4
+ # CAVP zips are the ones CSRC lists on its "Digital Signatures" CAVP page for FIPS 186-4; the RSA zip is named
5
+ # 186-3 upstream and holds the FIPS 186-4 RSA SigVer response files. ACVP files are pinned to a commit of
6
+ # usnistgov/ACVP-Server (master at 2026-08-12), never to a branch.
7
+
8
+ [[vector]]
9
+ id = "cavp/ecdsa/SigVer.rsp"
10
+ kind = "cavp"
11
+ algorithm = "ECDSA"
12
+ url = "https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/dss/186-4ecdsatestvectors.zip"
13
+ sha256 = "fe47cc92b4cee418236125c9ffbcd9bb01c8c34e74a4ba195d954bcb72824752"
14
+ member = "186-4ecdsatestvectors/SigVer.rsp"
15
+
16
+ [[vector]]
17
+ id = "cavp/rsa/SigVer15_186-3.rsp"
18
+ kind = "cavp"
19
+ algorithm = "RSA"
20
+ url = "https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/dss/186-3rsatestvectors.zip"
21
+ sha256 = "8405aeb3572a4f98ed4b1a3ccb3f2f49e725462dd28ec4759d6a15d88855d19c"
22
+ member = "SigVer15_186-3.rsp"
23
+
24
+ [[vector]]
25
+ id = "cavp/rsa/SigVerPSS_186-3.rsp"
26
+ kind = "cavp"
27
+ algorithm = "RSA"
28
+ url = "https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/dss/186-3rsatestvectors.zip"
29
+ sha256 = "8405aeb3572a4f98ed4b1a3ccb3f2f49e725462dd28ec4759d6a15d88855d19c"
30
+ member = "SigVerPSS_186-3.rsp"
31
+
32
+ [[vector]]
33
+ id = "cavp/rsa/SigVer931_186-3.rsp"
34
+ kind = "cavp"
35
+ algorithm = "RSA"
36
+ url = "https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Algorithm-Validation-Program/documents/dss/186-3rsatestvectors.zip"
37
+ sha256 = "8405aeb3572a4f98ed4b1a3ccb3f2f49e725462dd28ec4759d6a15d88855d19c"
38
+ member = "SigVer931_186-3.rsp"
39
+
40
+ [[vector]]
41
+ id = "acvp/ECDSA-SigVer-1.0"
42
+ kind = "acvp"
43
+ algorithm = "ECDSA"
44
+ url = "https://raw.githubusercontent.com/usnistgov/ACVP-Server/975de31eb83d87039ec88934fdc47d8c312b892d/gen-val/json-files/ECDSA-SigVer-1.0/prompt.json"
45
+ sha256 = "eb41243f151f14ecb8e906d5d20a2809262fa9d8442a48cb4299ced5e6f30f8b"
46
+ expected_url = "https://raw.githubusercontent.com/usnistgov/ACVP-Server/975de31eb83d87039ec88934fdc47d8c312b892d/gen-val/json-files/ECDSA-SigVer-1.0/expectedResults.json"
47
+ expected_sha256 = "8a2b5e18f08cfa4846dcbc64c5f38bde94127c9549feeb42130acc4fcfa5f97e"