neyra 0.2.1__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 (343) hide show
  1. neyra/__init__.py +3 -0
  2. neyra/anytime.py +50 -0
  3. neyra/attest.py +501 -0
  4. neyra/behavior.py +106 -0
  5. neyra/bisect.py +213 -0
  6. neyra/check.py +385 -0
  7. neyra/cli.py +1234 -0
  8. neyra/clock.py +221 -0
  9. neyra/conformance.py +319 -0
  10. neyra/data/neutral.json +26 -0
  11. neyra/demo.py +83 -0
  12. neyra/diff.py +244 -0
  13. neyra/edit.py +509 -0
  14. neyra/editeval.py +147 -0
  15. neyra/examine.py +70 -0
  16. neyra/explain.py +47 -0
  17. neyra/export.py +112 -0
  18. neyra/families.py +96 -0
  19. neyra/gguf_io.py +207 -0
  20. neyra/hop.py +378 -0
  21. neyra/hop_gguf.py +152 -0
  22. neyra/hop_text.py +287 -0
  23. neyra/identity.py +254 -0
  24. neyra/interop.py +38 -0
  25. neyra/kquant.py +122 -0
  26. neyra/lab.py +528 -0
  27. neyra/ledger.py +98 -0
  28. neyra/mcp_server.py +125 -0
  29. neyra/merge.py +165 -0
  30. neyra/numerics.py +36 -0
  31. neyra/patch.py +287 -0
  32. neyra/probe.py +72 -0
  33. neyra/protocol.py +420 -0
  34. neyra/recover.py +195 -0
  35. neyra/repair.py +211 -0
  36. neyra/rows.py +412 -0
  37. neyra/runtime/__init__.py +65 -0
  38. neyra/runtime/mlx.py +417 -0
  39. neyra/runtime/torch.py +121 -0
  40. neyra/stats.py +58 -0
  41. neyra/store.py +310 -0
  42. neyra/survival.py +103 -0
  43. neyra/tasks.py +62 -0
  44. neyra/units.py +57 -0
  45. neyra/vectors/PROTOCOL.md +578 -0
  46. neyra/vectors/cases.json +2760 -0
  47. neyra/vectors/fixtures/bad-manifest-beats-unknown-extension.neyra +0 -0
  48. neyra/vectors/fixtures/bad-type.neyra +0 -0
  49. neyra/vectors/fixtures/base/model-0.safetensors +0 -0
  50. neyra/vectors/fixtures/base-resharded/model-0.safetensors +0 -0
  51. neyra/vectors/fixtures/base-resharded/model-1.safetensors +0 -0
  52. neyra/vectors/fixtures/bind-broken.json +1 -0
  53. neyra/vectors/fixtures/bind-good.json +1 -0
  54. neyra/vectors/fixtures/bind-other-base.json +1 -0
  55. neyra/vectors/fixtures/bind-other-patch-id.json +1 -0
  56. neyra/vectors/fixtures/bind-other-result.json +1 -0
  57. neyra/vectors/fixtures/consolidated-name/consolidated.safetensors +0 -0
  58. neyra/vectors/fixtures/consolidated-name/model-0.safetensors +0 -0
  59. neyra/vectors/fixtures/dir-named-safetensors/folder.safetensors/.keep +0 -0
  60. neyra/vectors/fixtures/dir-named-safetensors/model-0.safetensors +0 -0
  61. neyra/vectors/fixtures/dtype-is-a-list.neyra +0 -0
  62. neyra/vectors/fixtures/duplicate-name/model-0.safetensors +0 -0
  63. neyra/vectors/fixtures/duplicate-name/model-1.safetensors +0 -0
  64. neyra/vectors/fixtures/empty/.keep +0 -0
  65. neyra/vectors/fixtures/extra-payload-tensor.neyra +0 -0
  66. neyra/vectors/fixtures/files-broken.json +1 -0
  67. neyra/vectors/fixtures/files-files.json +1 -0
  68. neyra/vectors/fixtures/files-ignoring-signature.json +1 -0
  69. neyra/vectors/fixtures/files-no-files.json +1 -0
  70. neyra/vectors/fixtures/free-text-meta.neyra +0 -0
  71. neyra/vectors/fixtures/good.neyra +0 -0
  72. neyra/vectors/fixtures/header-repeated-field/model-0.safetensors +0 -0
  73. neyra/vectors/fixtures/header-repeated-key/model-0.safetensors +0 -0
  74. neyra/vectors/fixtures/hidden-file/.ghost.safetensors +0 -0
  75. neyra/vectors/fixtures/hidden-file/model-0.safetensors +0 -0
  76. neyra/vectors/fixtures/huge-header.neyra +0 -0
  77. neyra/vectors/fixtures/index-duplicate.neyra +0 -0
  78. neyra/vectors/fixtures/index-out-of-range.neyra +0 -0
  79. neyra/vectors/fixtures/index-unsorted.neyra +0 -0
  80. neyra/vectors/fixtures/ledger-bad-time.jsonl +3 -0
  81. neyra/vectors/fixtures/ledger-body-is-a-list.jsonl +3 -0
  82. neyra/vectors/fixtures/ledger-body-is-null.jsonl +3 -0
  83. neyra/vectors/fixtures/ledger-dropped.jsonl +2 -0
  84. neyra/vectors/fixtures/ledger-edited.jsonl +3 -0
  85. neyra/vectors/fixtures/ledger-empty.jsonl +0 -0
  86. neyra/vectors/fixtures/ledger-good.jsonl +3 -0
  87. neyra/vectors/fixtures/ledger-kind-is-a-number.jsonl +3 -0
  88. neyra/vectors/fixtures/ledger-no-final-newline.jsonl +3 -0
  89. neyra/vectors/fixtures/ledger-not-canonical.jsonl +3 -0
  90. neyra/vectors/fixtures/ledger-not-json.jsonl +3 -0
  91. neyra/vectors/fixtures/ledger-one-entry.jsonl +1 -0
  92. neyra/vectors/fixtures/ledger-seq-is-a-string.jsonl +3 -0
  93. neyra/vectors/fixtures/ledger-seq-is-true.jsonl +3 -0
  94. neyra/vectors/fixtures/ledger-swapped.jsonl +3 -0
  95. neyra/vectors/fixtures/ledger-time-fullwidth-digits.jsonl +3 -0
  96. neyra/vectors/fixtures/ledger-time-in-other-digits.jsonl +3 -0
  97. neyra/vectors/fixtures/ledger-time-newline.jsonl +3 -0
  98. neyra/vectors/fixtures/ledger-torn.jsonl +4 -0
  99. neyra/vectors/fixtures/length-mismatch.neyra +0 -0
  100. neyra/vectors/fixtures/lowrank-base/model-0.safetensors +0 -0
  101. neyra/vectors/fixtures/lowrank-base/model-1.safetensors +0 -0
  102. neyra/vectors/fixtures/lowrank-base/model-2.safetensors +0 -0
  103. neyra/vectors/fixtures/lowrank-extension-not-required.neyra +0 -0
  104. neyra/vectors/fixtures/lowrank-factor-rows-wrong.neyra +0 -0
  105. neyra/vectors/fixtures/lowrank-inner-sizes-differ.neyra +0 -0
  106. neyra/vectors/fixtures/lowrank-left-digest-wrong.neyra +0 -0
  107. neyra/vectors/fixtures/lowrank-left-is-float32.neyra +0 -0
  108. neyra/vectors/fixtures/lowrank-left-missing.neyra +0 -0
  109. neyra/vectors/fixtures/lowrank-left-without-right.neyra +0 -0
  110. neyra/vectors/fixtures/lowrank-on-a-vector.neyra +0 -0
  111. neyra/vectors/fixtures/lowrank-on-an-integer-tensor.neyra +0 -0
  112. neyra/vectors/fixtures/lowrank-other-extension-too.neyra +0 -0
  113. neyra/vectors/fixtures/lowrank-rank-zero.neyra +0 -0
  114. neyra/vectors/fixtures/lowrank-result-digest-wrong.neyra +0 -0
  115. neyra/vectors/fixtures/lowrank-right-digest-wrong.neyra +0 -0
  116. neyra/vectors/fixtures/lowrank.neyra +0 -0
  117. neyra/vectors/fixtures/malformed-digest.neyra +0 -0
  118. neyra/vectors/fixtures/manifest-is-a-float.neyra +0 -0
  119. neyra/vectors/fixtures/manifest-is-a-spaced-array.neyra +0 -0
  120. neyra/vectors/fixtures/manifest-is-a-string.neyra +0 -0
  121. neyra/vectors/fixtures/manifest-is-an-array.neyra +0 -0
  122. neyra/vectors/fixtures/metadata-is-a-string.neyra +0 -0
  123. neyra/vectors/fixtures/metadata-is-an-empty-list.neyra +0 -0
  124. neyra/vectors/fixtures/metadata-is-null.neyra +0 -0
  125. neyra/vectors/fixtures/metadata-is-zero.neyra +0 -0
  126. neyra/vectors/fixtures/missing-result-root.neyra +0 -0
  127. neyra/vectors/fixtures/name-is-a-number.neyra +0 -0
  128. neyra/vectors/fixtures/no-manifest.neyra +0 -0
  129. neyra/vectors/fixtures/not-canonical.neyra +0 -0
  130. neyra/vectors/fixtures/other/model-0.safetensors +0 -0
  131. neyra/vectors/fixtures/other-patch.neyra +0 -0
  132. neyra/vectors/fixtures/overlap.neyra +0 -0
  133. neyra/vectors/fixtures/patch-header-repeats-payload.neyra +0 -0
  134. neyra/vectors/fixtures/receipt-bad-predicate.json +1 -0
  135. neyra/vectors/fixtures/receipt-bad-sig-base64.json +1 -0
  136. neyra/vectors/fixtures/receipt-bad-signature-then-good.json +1 -0
  137. neyra/vectors/fixtures/receipt-bad-type.json +1 -0
  138. neyra/vectors/fixtures/receipt-claim-digest-is-a-number.json +1 -0
  139. neyra/vectors/fixtures/receipt-claim-digest-without-prefix.json +1 -0
  140. neyra/vectors/fixtures/receipt-claim-incomplete.json +1 -0
  141. neyra/vectors/fixtures/receipt-ed25519-key-for-p256-signature.json +1 -0
  142. neyra/vectors/fixtures/receipt-good-signature-then-bad.json +1 -0
  143. neyra/vectors/fixtures/receipt-head-mid-ledger.json +1 -0
  144. neyra/vectors/fixtures/receipt-invalid-utf8.json +1 -0
  145. neyra/vectors/fixtures/receipt-is-an-array.json +1 -0
  146. neyra/vectors/fixtures/receipt-ledger-len-one-ok.json +1 -0
  147. neyra/vectors/fixtures/receipt-ledger-len-string.json +1 -0
  148. neyra/vectors/fixtures/receipt-ledger-len-true.json +1 -0
  149. neyra/vectors/fixtures/receipt-no-key-given.json +1 -0
  150. neyra/vectors/fixtures/receipt-no-ledger-head.json +1 -0
  151. neyra/vectors/fixtures/receipt-not-canonical.json +1 -0
  152. neyra/vectors/fixtures/receipt-only-a-bad-signature.json +1 -0
  153. neyra/vectors/fixtures/receipt-p256-key-for-ed25519-signature.json +1 -0
  154. neyra/vectors/fixtures/receipt-p256-no-key-given.json +1 -0
  155. neyra/vectors/fixtures/receipt-p256-tampered-claim.json +1 -0
  156. neyra/vectors/fixtures/receipt-p256-wrong-curve-key.json +1 -0
  157. neyra/vectors/fixtures/receipt-p256-wrong-key.json +1 -0
  158. neyra/vectors/fixtures/receipt-padded-payload-ok.json +1 -0
  159. neyra/vectors/fixtures/receipt-payload-not-a-string.json +1 -0
  160. neyra/vectors/fixtures/receipt-payload-type-wrong-and-signatures-missing.json +1 -0
  161. neyra/vectors/fixtures/receipt-payload-with-stray-bits.json +1 -0
  162. neyra/vectors/fixtures/receipt-signature-entry-not-an-object.json +1 -0
  163. neyra/vectors/fixtures/receipt-signature-sig-not-a-string.json +1 -0
  164. neyra/vectors/fixtures/receipt-signature-with-stray-bits.json +1 -0
  165. neyra/vectors/fixtures/receipt-signature-without-sig.json +1 -0
  166. neyra/vectors/fixtures/receipt-signatures-missing.json +1 -0
  167. neyra/vectors/fixtures/receipt-signatures-not-a-list.json +1 -0
  168. neyra/vectors/fixtures/receipt-signed-p256.json +1 -0
  169. neyra/vectors/fixtures/receipt-signed.json +1 -0
  170. neyra/vectors/fixtures/receipt-subjects-reversed-ok.json +1 -0
  171. neyra/vectors/fixtures/receipt-tampered-claim.json +1 -0
  172. neyra/vectors/fixtures/receipt-three-subjects.json +1 -0
  173. neyra/vectors/fixtures/receipt-two-signatures.json +1 -0
  174. neyra/vectors/fixtures/receipt-unknown-head.json +1 -0
  175. neyra/vectors/fixtures/receipt-unsigned-but-key-given.json +1 -0
  176. neyra/vectors/fixtures/receipt-unsigned.json +1 -0
  177. neyra/vectors/fixtures/receipt-wrong-digest-algorithm.json +1 -0
  178. neyra/vectors/fixtures/receipt-wrong-key.json +1 -0
  179. neyra/vectors/fixtures/receipt-wrong-length.json +1 -0
  180. neyra/vectors/fixtures/receipt-wrong-subject.json +1 -0
  181. neyra/vectors/fixtures/repeated-tensor-name.neyra +0 -0
  182. neyra/vectors/fixtures/requires-not-a-list.neyra +0 -0
  183. neyra/vectors/fixtures/result-model/.gitignore +1 -0
  184. neyra/vectors/fixtures/result-model/config.json +1 -0
  185. neyra/vectors/fixtures/result-model/docs/README.md +1 -0
  186. neyra/vectors/fixtures/result-model/docs.txt +1 -0
  187. neyra/vectors/fixtures/result-model/model-0.safetensors +0 -0
  188. neyra/vectors/fixtures/result-model-resharded/config.json +1 -0
  189. neyra/vectors/fixtures/result-model-resharded/docs/README.md +1 -0
  190. neyra/vectors/fixtures/result-model-resharded/docs.txt +1 -0
  191. neyra/vectors/fixtures/result-model-resharded/model-0.safetensors +0 -0
  192. neyra/vectors/fixtures/result-model-resharded/model-1.safetensors +0 -0
  193. neyra/vectors/fixtures/result-model-signed/config.json +1 -0
  194. neyra/vectors/fixtures/result-model-signed/docs/README.md +1 -0
  195. neyra/vectors/fixtures/result-model-signed/docs.txt +1 -0
  196. neyra/vectors/fixtures/result-model-signed/model-0.safetensors +0 -0
  197. neyra/vectors/fixtures/result-model-signed/model.sig +1 -0
  198. neyra/vectors/fixtures/result-model-tampered-config/config.json +1 -0
  199. neyra/vectors/fixtures/result-model-tampered-config/docs/README.md +1 -0
  200. neyra/vectors/fixtures/result-model-tampered-config/docs.txt +1 -0
  201. neyra/vectors/fixtures/result-model-tampered-config/model-0.safetensors +0 -0
  202. neyra/vectors/fixtures/rows/bytes.data +1 -0
  203. neyra/vectors/fixtures/rows/bytes.json +1 -0
  204. neyra/vectors/fixtures/rows/flipped-bit.data +0 -0
  205. neyra/vectors/fixtures/rows/flipped-bit.json +1 -0
  206. neyra/vectors/fixtures/rows/not-json.data +0 -0
  207. neyra/vectors/fixtures/rows/not-json.json +1 -0
  208. neyra/vectors/fixtures/rows/one-row-per-leaf.data +0 -0
  209. neyra/vectors/fixtures/rows/one-row-per-leaf.json +1 -0
  210. neyra/vectors/fixtures/rows/other-dtype.data +0 -0
  211. neyra/vectors/fixtures/rows/other-dtype.json +1 -0
  212. neyra/vectors/fixtures/rows/other-name.data +0 -0
  213. neyra/vectors/fixtures/rows/other-name.json +1 -0
  214. neyra/vectors/fixtures/rows/other-position.data +0 -0
  215. neyra/vectors/fixtures/rows/other-position.json +1 -0
  216. neyra/vectors/fixtures/rows/other-root.data +0 -0
  217. neyra/vectors/fixtures/rows/other-root.json +1 -0
  218. neyra/vectors/fixtures/rows/partial-last-leaf.data +0 -0
  219. neyra/vectors/fixtures/rows/partial-last-leaf.json +1 -0
  220. neyra/vectors/fixtures/rows/path-changed.data +0 -0
  221. neyra/vectors/fixtures/rows/path-changed.json +1 -0
  222. neyra/vectors/fixtures/rows/path-short.data +0 -0
  223. neyra/vectors/fixtures/rows/path-short.json +1 -0
  224. neyra/vectors/fixtures/rows/range-past-end.data +0 -0
  225. neyra/vectors/fixtures/rows/range-past-end.json +1 -0
  226. neyra/vectors/fixtures/rows/ranges-unsorted.data +0 -0
  227. neyra/vectors/fixtures/rows/ranges-unsorted.json +1 -0
  228. neyra/vectors/fixtures/rows/scalar.data +0 -0
  229. neyra/vectors/fixtures/rows/scalar.json +1 -0
  230. neyra/vectors/fixtures/rows/short-data.data +0 -0
  231. neyra/vectors/fixtures/rows/short-data.json +1 -0
  232. neyra/vectors/fixtures/rows/sibling-changed.data +0 -0
  233. neyra/vectors/fixtures/rows/sibling-changed.json +1 -0
  234. neyra/vectors/fixtures/rows/three-ranges.data +0 -0
  235. neyra/vectors/fixtures/rows/three-ranges.json +1 -0
  236. neyra/vectors/fixtures/rows/tree-long.data +0 -0
  237. neyra/vectors/fixtures/rows/tree-long.json +1 -0
  238. neyra/vectors/fixtures/rows/two-leaves.data +0 -0
  239. neyra/vectors/fixtures/rows/two-leaves.json +1 -0
  240. neyra/vectors/fixtures/rows/unknown-member.data +0 -0
  241. neyra/vectors/fixtures/rows/unknown-member.json +1 -0
  242. neyra/vectors/fixtures/rows/vector-tail.data +0 -0
  243. neyra/vectors/fixtures/rows/vector-tail.json +1 -0
  244. neyra/vectors/fixtures/rows-shapes/model-0.safetensors +0 -0
  245. neyra/vectors/fixtures/shape-has-a-boolean.neyra +0 -0
  246. neyra/vectors/fixtures/shape-has-a-list.neyra +0 -0
  247. neyra/vectors/fixtures/shape-is-negative.neyra +0 -0
  248. neyra/vectors/fixtures/short.neyra +1 -0
  249. neyra/vectors/fixtures/status-empty-reason.json +1 -0
  250. neyra/vectors/fixtures/status-for-another-model.json +1 -0
  251. neyra/vectors/fixtures/status-for-another-statement.json +1 -0
  252. neyra/vectors/fixtures/status-for-receipt.json +1 -0
  253. neyra/vectors/fixtures/status-meaning-outside-vocabulary.json +1 -0
  254. neyra/vectors/fixtures/status-subject-differs.json +1 -0
  255. neyra/vectors/fixtures/status-supersede-itself.json +1 -0
  256. neyra/vectors/fixtures/status-supersede-p256.json +1 -0
  257. neyra/vectors/fixtures/status-supersede-without-replacement.json +1 -0
  258. neyra/vectors/fixtures/status-target-not-a-digest.json +1 -0
  259. neyra/vectors/fixtures/status-time-malformed.json +1 -0
  260. neyra/vectors/fixtures/status-unknown-action.json +1 -0
  261. neyra/vectors/fixtures/status-unknown-member.json +1 -0
  262. neyra/vectors/fixtures/status-unsigned.json +1 -0
  263. neyra/vectors/fixtures/status-withdraw-names-a-replacement.json +1 -0
  264. neyra/vectors/fixtures/status-withdraw.json +1 -0
  265. neyra/vectors/fixtures/status-wrong-key.json +1 -0
  266. neyra/vectors/fixtures/status-wrong-predicate-type.json +1 -0
  267. neyra/vectors/fixtures/status-x-member-ok.json +1 -0
  268. neyra/vectors/fixtures/target-receipt.json +1 -0
  269. neyra/vectors/fixtures/target-transformation.json +1 -0
  270. neyra/vectors/fixtures/tensor-member-missing.neyra +0 -0
  271. neyra/vectors/fixtures/trailing-bytes.neyra +0 -0
  272. neyra/vectors/fixtures/transform-attempts-a-string.json +1 -0
  273. neyra/vectors/fixtures/transform-attempts-counted-ok.json +1 -0
  274. neyra/vectors/fixtures/transform-attempts-zero.json +1 -0
  275. neyra/vectors/fixtures/transform-bad-payload-type.json +1 -0
  276. neyra/vectors/fixtures/transform-basis-moves-a-field.json +1 -0
  277. neyra/vectors/fixtures/transform-coverage-missing.json +1 -0
  278. neyra/vectors/fixtures/transform-declared-but-bytes-read.json +1 -0
  279. neyra/vectors/fixtures/transform-declared-with-the-basis-of-an-exact-one.json +1 -0
  280. neyra/vectors/fixtures/transform-declared-without-extent-ok.json +1 -0
  281. neyra/vectors/fixtures/transform-duplicate-key.json +1 -0
  282. neyra/vectors/fixtures/transform-evidence-bad-digest.json +1 -0
  283. neyra/vectors/fixtures/transform-exact-but-partly-read.json +1 -0
  284. neyra/vectors/fixtures/transform-exact-without-patch-id.json +1 -0
  285. neyra/vectors/fixtures/transform-extent-member-missing.json +1 -0
  286. neyra/vectors/fixtures/transform-extent-not-integers.json +1 -0
  287. neyra/vectors/fixtures/transform-file-digest-uppercase.json +1 -0
  288. neyra/vectors/fixtures/transform-file-path-escapes.json +1 -0
  289. neyra/vectors/fixtures/transform-files-repeat-a-path.json +1 -0
  290. neyra/vectors/fixtures/transform-files-sorted-as-strings.json +1 -0
  291. neyra/vectors/fixtures/transform-files-without-name.json +1 -0
  292. neyra/vectors/fixtures/transform-grade-outside-vocabulary.json +1 -0
  293. neyra/vectors/fixtures/transform-ignore-not-a-list.json +1 -0
  294. neyra/vectors/fixtures/transform-kind-outside-vocabulary.json +1 -0
  295. neyra/vectors/fixtures/transform-meaning-outside-vocabulary.json +1 -0
  296. neyra/vectors/fixtures/transform-member-missing.json +1 -0
  297. neyra/vectors/fixtures/transform-no-files-ok.json +1 -0
  298. neyra/vectors/fixtures/transform-no-key-given.json +1 -0
  299. neyra/vectors/fixtures/transform-not-canonical.json +1 -0
  300. neyra/vectors/fixtures/transform-not-measured-extra-ok.json +1 -0
  301. neyra/vectors/fixtures/transform-not-measured-incomplete.json +1 -0
  302. neyra/vectors/fixtures/transform-observed-without-extent.json +1 -0
  303. neyra/vectors/fixtures/transform-rederive-empty.json +1 -0
  304. neyra/vectors/fixtures/transform-root-not-a-digest.json +1 -0
  305. neyra/vectors/fixtures/transform-sampled-but-fully-read.json +1 -0
  306. neyra/vectors/fixtures/transform-sampled-ok.json +1 -0
  307. neyra/vectors/fixtures/transform-sampled-with-the-basis-of-an-exact-one.json +1 -0
  308. neyra/vectors/fixtures/transform-signed-ed25519.json +1 -0
  309. neyra/vectors/fixtures/transform-signed-p256.json +1 -0
  310. neyra/vectors/fixtures/transform-subject-extra.json +1 -0
  311. neyra/vectors/fixtures/transform-subject-missing-a-file.json +1 -0
  312. neyra/vectors/fixtures/transform-subject-missing-oms-root.json +1 -0
  313. neyra/vectors/fixtures/transform-subjects-in-any-order-ok.json +1 -0
  314. neyra/vectors/fixtures/transform-tampered-after-signing.json +1 -0
  315. neyra/vectors/fixtures/transform-time-malformed.json +1 -0
  316. neyra/vectors/fixtures/transform-tool-incomplete.json +1 -0
  317. neyra/vectors/fixtures/transform-unknown-kind-cannot-be-exact.json +1 -0
  318. neyra/vectors/fixtures/transform-unknown-member.json +1 -0
  319. neyra/vectors/fixtures/transform-unsigned-but-key-given.json +1 -0
  320. neyra/vectors/fixtures/transform-unsigned.json +1 -0
  321. neyra/vectors/fixtures/transform-weights-subject-under-sha256.json +1 -0
  322. neyra/vectors/fixtures/transform-wrong-key.json +1 -0
  323. neyra/vectors/fixtures/transform-wrong-predicate-type.json +1 -0
  324. neyra/vectors/fixtures/transform-x-member-inside-result-refused.json +1 -0
  325. neyra/vectors/fixtures/transform-x-member-ok.json +1 -0
  326. neyra/vectors/fixtures/unknown-extension.neyra +0 -0
  327. neyra/vectors/fixtures/unknown-member-beats-bad-manifest.neyra +0 -0
  328. neyra/vectors/fixtures/unknown-member.neyra +0 -0
  329. neyra/vectors/fixtures/unknown-tensor-member.neyra +0 -0
  330. neyra/vectors/fixtures/val-wrong-dtype.neyra +0 -0
  331. neyra/vectors/fixtures/wrong-base-digest.neyra +0 -0
  332. neyra/vectors/fixtures/wrong-result-digest.neyra +0 -0
  333. neyra/vectors/fixtures/wrong-result-root.neyra +0 -0
  334. neyra/vectors/fixtures/wrong-val-digest.neyra +0 -0
  335. neyra/vectors/fixtures/x-member-ok.neyra +0 -0
  336. neyra/vectors/fixtures/x-tensor-member-ok.neyra +0 -0
  337. neyra/vectors/generate.py +908 -0
  338. neyra/workflows.py +222 -0
  339. neyra-0.2.1.dist-info/METADATA +219 -0
  340. neyra-0.2.1.dist-info/RECORD +343 -0
  341. neyra-0.2.1.dist-info/WHEEL +4 -0
  342. neyra-0.2.1.dist-info/entry_points.txt +2 -0
  343. neyra-0.2.1.dist-info/licenses/LICENSE +202 -0
neyra/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Edit local model weights with patches that revert bit for bit."""
2
+
3
+ __version__ = "0.2.1"
neyra/anytime.py ADDED
@@ -0,0 +1,50 @@
1
+ """Tests that stay valid however often you look.
2
+
3
+ A fixed threshold checked after every new item rejects a true null far more often
4
+ than its nominal rate. An e-process does not: by Ville's inequality, the chance it
5
+ ever reaches 1/alpha under the null is at most alpha.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+
12
+ import numpy as np
13
+
14
+
15
+ def betting_process(x: np.ndarray, null: float = 0.5, alpha: float = 0.05, c: float = 0.75) -> np.ndarray:
16
+ """Running e-process for H0: mean of x <= null, with x in [0, 1].
17
+
18
+ The predictable plug-in bet of Waudby-Smith and Ramdas (2023), one sided, so the
19
+ capital only grows on evidence that the mean is above null.
20
+ """
21
+ capital = 1.0
22
+ total, squares = 0.0, 0.0
23
+ mean, spread = 0.5, 0.25
24
+ out = np.empty(len(x))
25
+ for t, value in enumerate(np.asarray(x, dtype=np.float64), start=1):
26
+ rate = math.sqrt(2 * math.log(1 / alpha) / (spread * t * math.log(t + 1)))
27
+ capital *= 1 + min(rate, c / null) * (value - null)
28
+ out[t - 1] = capital
29
+ total += value
30
+ mean = (0.5 + total) / (t + 1)
31
+ squares += (value - mean) ** 2
32
+ spread = (0.25 + squares) / (t + 1)
33
+ return out
34
+
35
+
36
+ def sign_evalue(gained: int, lost: int) -> float:
37
+ """Mixture e-value that gains are likelier than losses, uniform prior on the rate.
38
+
39
+ Under H0 each changed item is a fair coin or worse. Integrating the likelihood ratio
40
+ against a uniform prior over rates above one half gives a value with expectation
41
+ at most 1, for any stopping rule.
42
+ """
43
+ n = gained + lost
44
+ log_beta = math.lgamma(gained + 1) + math.lgamma(lost + 1) - math.lgamma(n + 2)
45
+ tail = sum(math.comb(n + 1, k) for k in range(gained + 1)) / 2 ** (n + 1)
46
+ return math.exp((n + 1) * math.log(2) + log_beta) * tail
47
+
48
+
49
+ def rejects(evalue: float, alpha: float = 0.05, tests: int = 1) -> bool:
50
+ return evalue >= tests / alpha
neyra/attest.py ADDED
@@ -0,0 +1,501 @@
1
+ """Transformation statements: the signed record of what changed between two models.
2
+
3
+ The normative text is Annex C of spec/PROTOCOL.md. The statement is the single source of truth. The
4
+ CycloneDX fragment and the one page summary are derived from it and from nothing else, so a reader who holds
5
+ the statement can regenerate both and compare.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import hashlib
12
+ import json
13
+ import re
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+
17
+ import numpy as np
18
+
19
+ from . import __version__
20
+ from .protocol import (
21
+ STATEMENT_TYPE,
22
+ ProtocolError,
23
+ check_signature,
24
+ decode_envelope,
25
+ digest,
26
+ jcs,
27
+ now,
28
+ root_of,
29
+ tensor_table,
30
+ )
31
+ from .store import Checkpoint
32
+
33
+ PREDICATE_TYPE = "https://neyra.run/transformation/v1"
34
+ STATUS_TYPE = "https://neyra.run/status/v1"
35
+ WEIGHTS_KEY = "neyra_weights_v1"
36
+ KINDS = ("patch", "merge", "adapter", "quantize", "edit", "unknown")
37
+ GRADES = ("exact", "observed", "sampled", "declared")
38
+ MEANINGS = {
39
+ "authored": "made this change",
40
+ "reviewed": "reviewed this change and did not make it",
41
+ "approved-for-deploy": "approves this exact result for deployment, on the strength of the checks listed here only",
42
+ "responsible-for-merge": "is responsible for combining the inputs listed here",
43
+ }
44
+ NOT_MEASURED = ("training compute", "training data", "behaviour")
45
+ OMS_IGNORE = (".git", ".gitattributes", ".github", ".gitignore")
46
+ EXTENT = ("tensors_base", "tensors_result", "changed", "added", "removed", "entries_changed", "entries_total")
47
+ ACTIONS = ("supersede", "withdraw")
48
+
49
+ PREDICATE_MEMBERS = {"type", "kind", "grade", "base", "result", "recipe", "extent", "coverage", "basis", "attempts",
50
+ "rederive", "meaning", "not_measured", "evidence", "time", "tool"}
51
+ STATUS_MEMBERS = {"type", "action", "target", "replacement", "weights_root", "reason", "meaning", "time", "tool"}
52
+ SHA256 = re.compile(r"[0-9a-f]{64}")
53
+ DIGEST = re.compile(r"sha256:[0-9a-f]{64}")
54
+ TIME = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z")
55
+
56
+
57
+ def bad(message: str) -> ProtocolError:
58
+ return ProtocolError("bad_transform", message)
59
+
60
+
61
+ def statement_digest(payload: bytes) -> str:
62
+ return digest("statement", payload)
63
+
64
+
65
+ def path_order(path: str) -> tuple[str, ...]:
66
+ """The order OMS lists files in: component by component, as pathlib compares them."""
67
+ return tuple(path.split("/"))
68
+
69
+
70
+ def file_digests(root: str | Path, ignore: tuple[str, ...] = (), allow_symlinks: bool = False) -> list[dict]:
71
+ """sha256 of every file the way OMS serialises a model directory (method `files`)."""
72
+ root = Path(root)
73
+ skipped = [Path(s).parts for s in (*OMS_IGNORE, *ignore)]
74
+ found = []
75
+ for path in root.rglob("*"):
76
+ rel = path.relative_to(root)
77
+ if any(rel.parts[:len(s)] == s for s in skipped):
78
+ continue
79
+ if path.is_symlink() and not allow_symlinks:
80
+ raise ValueError(f"{rel} is a symlink, which OMS refuses by default")
81
+ if path.is_file():
82
+ h = hashlib.sha256()
83
+ with open(path, "rb") as f:
84
+ while block := f.read(1 << 20):
85
+ h.update(block)
86
+ found.append({"path": rel.as_posix(), "sha256": h.hexdigest()})
87
+ return sorted(found, key=lambda f: path_order(f["path"]))
88
+
89
+
90
+ def oms_root(files: list[dict]) -> str:
91
+ """The subject digest OMS gives a model: sha256 over the file digests in manifest order."""
92
+ h = hashlib.sha256()
93
+ for f in sorted(files, key=lambda f: path_order(f["path"])):
94
+ h.update(bytes.fromhex(f["sha256"]))
95
+ return h.hexdigest()
96
+
97
+
98
+ def oms_files(bundle: dict) -> tuple[list[dict], str]:
99
+ """The files and subject digest an OMS signature declares. The signature itself is OMS's to verify."""
100
+ st = decode_oms(bundle)
101
+ files = [{"path": r["name"], "sha256": r["digest"]} for r in st["predicate"]["resources"]]
102
+ if st["predicate"]["serialization"]["method"] != "files":
103
+ raise ValueError("only OMS serialisation by files can be joined to a Neyra statement")
104
+ return sorted(files, key=lambda f: path_order(f["path"])), st["subject"][0]["digest"]["sha256"]
105
+
106
+
107
+ def decode_oms(bundle: dict) -> dict:
108
+ return json.loads(base64.b64decode(bundle["dsseEnvelope"]["payload"]))
109
+
110
+
111
+ def measure(base: Checkpoint, result: Checkpoint, before: dict, after: dict) -> dict:
112
+ """Count what differs, reading only the tensors whose digests differ."""
113
+ changed = removed = entries = total = 0
114
+ for name, ref in base.tensors.items():
115
+ other = result.tensors.get(name)
116
+ if other is None:
117
+ removed += 1
118
+ elif before[name]["digest"] != after[name]["digest"]:
119
+ changed += 1
120
+ if other.dtype == ref.dtype and other.shape == ref.shape:
121
+ a, b = ref.read(), other.read()
122
+ entries += int(np.count_nonzero(a != b))
123
+ total += a.size
124
+ else:
125
+ entries += other.numel
126
+ total += other.numel
127
+ return {"tensors_base": len(base.tensors), "tensors_result": len(result.tensors), "changed": changed,
128
+ "added": len(set(result.tensors) - set(base.tensors)), "removed": removed,
129
+ "entries_changed": entries, "entries_total": total}
130
+
131
+
132
+ def basis_for(grade: str) -> dict:
133
+ """What a reader can recompute from the models and what rests on the signer's word, fixed by the grade.
134
+
135
+ Written into the statement so that nobody reads an unchecked field as a checked one. A verifier compares it
136
+ with this function, so a signer cannot move a field from one list to the other.
137
+ """
138
+ checkable = ["base.weights_root", "result.weights_root", "result.files", "extent"]
139
+ recomputable = checkable + ["patch_application"] if grade == "exact" else checkable if grade == "observed" else []
140
+ asserted = ["base.files", "kind", "recipe", "meaning", "time", "tool", "evidence", "attempts"]
141
+ return {"recomputable": recomputable, "asserted": asserted + ([] if recomputable else checkable)}
142
+
143
+
144
+ def rederive_steps(kind: str, grade: str, recipe: dict, base_root: str, result_root: str) -> list[str]:
145
+ if grade == "exact":
146
+ return [f"Take a model whose weights root is {base_root}.",
147
+ f"Apply patch {recipe['patch_id']} to it with `neyra apply`.",
148
+ f"The result must have weights root {result_root}.",
149
+ ("Check the whole statement with `neyra verify STATEMENT --key KEY --base BASE --result RESULT "
150
+ "--patch PATCH`.")]
151
+ if grade == "observed":
152
+ return ["Hash both models tensor by tensor with `neyra verify STATEMENT --key KEY --base BASE --result RESULT`.",
153
+ f"The weights roots must be {base_root} and {result_root}, and the counts under extent must match."]
154
+ if grade == "sampled":
155
+ return ["Re-run the same sampled comparison. Counts are lower bounds: spans that were not read may differ."]
156
+ return ["The signer recomputed nothing. Kind and recipe are the signer's statement, not a measurement."]
157
+
158
+
159
+ def transformation(*, kind: str, grade: str, meaning: str, base_root: str, result_root: str, base_files: list[dict],
160
+ result_files: list[dict], result_name: str, recipe: dict, extent: dict | None,
161
+ coverage: dict, attempts: int | None = None, ignore: tuple[str, ...] = (),
162
+ evidence: list[dict] | None = None, time: str | None = None) -> dict:
163
+ """An in-toto statement. The subjects are derived from the predicate, never chosen separately."""
164
+ predicate = {
165
+ "type": "neyra.transformation/1", "kind": kind, "grade": grade,
166
+ "base": {"weights_root": base_root, "files": base_files},
167
+ "result": {"weights_root": result_root, "files": result_files, "name": result_name, "ignore": list(ignore)},
168
+ "recipe": recipe, "extent": extent, "coverage": coverage, "basis": basis_for(grade), "attempts": attempts,
169
+ "rederive": rederive_steps(kind, grade, recipe, base_root, result_root),
170
+ "meaning": meaning, "not_measured": list(NOT_MEASURED), "evidence": evidence or [],
171
+ "time": time or now(), "tool": {"name": "neyra", "version": __version__},
172
+ }
173
+ return {"_type": STATEMENT_TYPE, "subject": subjects_of(predicate), "predicateType": PREDICATE_TYPE,
174
+ "predicate": predicate}
175
+
176
+
177
+ def subjects_of(predicate: dict) -> list[dict]:
178
+ result = predicate["result"]
179
+ subjects = [{"name": "result-weights", "digest": {WEIGHTS_KEY: result["weights_root"][7:]}}]
180
+ subjects += [{"name": f["path"], "digest": {"sha256": f["sha256"]}} for f in result["files"]]
181
+ if result["files"]:
182
+ subjects.append({"name": result["name"], "digest": {"sha256": oms_root(result["files"])}})
183
+ return subjects
184
+
185
+
186
+ def check_members(obj: dict, want: set[str], where: str, extensible: bool = False) -> None:
187
+ extra = {k for k in obj if k.startswith("x-")} if extensible else set()
188
+ if set(obj) - want - extra or want - set(obj):
189
+ raise bad(f"{where} has the wrong members")
190
+
191
+
192
+ def check_files(files, where: str) -> None:
193
+ if not isinstance(files, list):
194
+ raise bad(f"{where}.files is not a list")
195
+ for f in files:
196
+ if not isinstance(f, dict) or set(f) != {"path", "sha256"} or not isinstance(f["path"], str) \
197
+ or not isinstance(f["sha256"], str) or not SHA256.fullmatch(f["sha256"]):
198
+ raise bad(f"{where}.files holds a malformed entry")
199
+ if any(part in ("", ".", "..") for part in f["path"].split("/")) or "\\" in f["path"]:
200
+ raise bad(f"{where}.files holds the path {f['path']!r}")
201
+ keys = [path_order(f["path"]) for f in files]
202
+ if keys != sorted(set(keys)):
203
+ raise bad(f"{where}.files is not sorted without repeats")
204
+
205
+
206
+ def check_predicate(p) -> None:
207
+ if not isinstance(p, dict):
208
+ raise bad("the predicate is not an object")
209
+ check_members(p, PREDICATE_MEMBERS, "predicate", extensible=True)
210
+ if p["type"] != "neyra.transformation/1":
211
+ raise bad("unknown predicate type")
212
+ if p["kind"] not in KINDS or p["grade"] not in GRADES or p["meaning"] not in MEANINGS:
213
+ raise bad("kind, grade or meaning is outside its vocabulary")
214
+ for side in ("base", "result"):
215
+ part = p[side]
216
+ if not isinstance(part, dict):
217
+ raise bad(f"{side} is not an object")
218
+ check_members(part, {"weights_root", "files"} | ({"name", "ignore"} if side == "result" else set()), side)
219
+ if not isinstance(part["weights_root"], str) or not DIGEST.fullmatch(part["weights_root"]):
220
+ raise bad(f"{side}.weights_root is not a digest")
221
+ check_files(part["files"], side)
222
+ if not isinstance(p["result"]["name"], str) or (p["result"]["files"] and not p["result"]["name"]):
223
+ raise bad("result.name must name the model when files are listed")
224
+ skipped = p["result"]["ignore"]
225
+ if not isinstance(skipped, list) or not all(isinstance(s, str) and s for s in skipped):
226
+ raise bad("result.ignore must be a list of paths")
227
+ recipe = p["recipe"]
228
+ if not isinstance(recipe, dict):
229
+ raise bad("recipe is not an object")
230
+ named = isinstance(recipe.get("patch_id"), str) and DIGEST.fullmatch(recipe["patch_id"])
231
+ if p["grade"] == "exact" and p["kind"] in ("patch", "edit", "merge") and not named:
232
+ raise bad("an exact patch, edit or merge names its patch_id in the recipe")
233
+ extent = p["extent"]
234
+ if extent is None:
235
+ if p["grade"] != "declared":
236
+ raise bad("only a declared change may leave extent empty")
237
+ else:
238
+ if not isinstance(extent, dict) or set(extent) != set(EXTENT):
239
+ raise bad("extent has the wrong members")
240
+ if any(not isinstance(v, int) or isinstance(v, bool) or v < 0 for v in extent.values()):
241
+ raise bad("extent holds a value that is not a count")
242
+ cover = p["coverage"]
243
+ if not isinstance(cover, dict) or set(cover) != {"bytes_read", "bytes_total"} or not all(
244
+ isinstance(v, int) and not isinstance(v, bool) and v >= 0 for v in cover.values()):
245
+ raise bad("coverage must count bytes_read and bytes_total")
246
+ read, total = cover["bytes_read"], cover["bytes_total"]
247
+ whole = {"exact": read == total, "observed": read == total, "sampled": read < total, "declared": read == 0}
248
+ if not whole[p["grade"]]:
249
+ raise bad(f"coverage does not fit a {p['grade']} grade")
250
+ if p["basis"] != basis_for(p["grade"]):
251
+ raise bad("basis is not the one this grade fixes")
252
+ tries = p["attempts"]
253
+ if tries is not None and (not isinstance(tries, int) or isinstance(tries, bool) or tries < 1):
254
+ raise bad("attempts is a count of at least 1, or null when nobody counted")
255
+ steps = p["rederive"]
256
+ if not isinstance(steps, list) or not steps or not all(isinstance(s, str) and s for s in steps):
257
+ raise bad("rederive must be a non empty list of steps")
258
+ unmeasured = p["not_measured"]
259
+ if not isinstance(unmeasured, list) or not all(isinstance(s, str) for s in unmeasured) \
260
+ or not set(NOT_MEASURED) <= set(unmeasured):
261
+ raise bad("not_measured must say that " + ", ".join(NOT_MEASURED) + " were not measured")
262
+ ev = p["evidence"]
263
+ if not isinstance(ev, list) or any(
264
+ not isinstance(e, dict) or set(e) != {"role", "digest"} or not isinstance(e["role"], str)
265
+ or not isinstance(e["digest"], str) or not DIGEST.fullmatch(e["digest"]) for e in ev):
266
+ raise bad("evidence is malformed")
267
+ if not isinstance(p["time"], str) or not TIME.fullmatch(p["time"]):
268
+ raise bad("time is not an RFC 3339 UTC string")
269
+ tool = p["tool"]
270
+ if not isinstance(tool, dict) or set(tool) != {"name", "version"} or not all(isinstance(v, str) for v in tool.values()):
271
+ raise bad("tool is malformed")
272
+ if p["kind"] == "unknown" and p["grade"] == "exact":
273
+ raise bad("a change of unknown kind cannot be exact")
274
+
275
+
276
+ @dataclass
277
+ class Opened:
278
+ statement: dict
279
+ signed: bool
280
+ payload: bytes
281
+
282
+
283
+ def open_statement(doc: dict, public: bytes | None, predicate_type: str, check) -> Opened:
284
+ """The shared steps of section 8: envelope, canonical payload, types, then the predicate's own checks."""
285
+ statement, payload = decode_envelope(doc)
286
+ if statement.get("_type") != STATEMENT_TYPE or statement.get("predicateType") != predicate_type:
287
+ raise ProtocolError("bad_type", "wrong statement or predicate type")
288
+ check(statement)
289
+ if public is None:
290
+ return Opened(statement, False, payload)
291
+ check_signature(doc, payload, public)
292
+ return Opened(statement, True, payload)
293
+
294
+
295
+ def check_transformation(statement: dict) -> None:
296
+ check_predicate(statement.get("predicate"))
297
+ want = sorted(jcs_text(s) for s in subjects_of(statement["predicate"]))
298
+ got = statement.get("subject")
299
+ if not isinstance(got, list) or sorted(jcs_text(s) for s in got) != want:
300
+ raise ProtocolError("subject_mismatch", "subjects must be exactly those the predicate implies")
301
+
302
+
303
+ def jcs_text(value) -> str:
304
+ return jcs(value).decode()
305
+
306
+
307
+ def open_transformation(doc: dict, public: bytes | None = None) -> Opened:
308
+ return open_statement(doc, public, PREDICATE_TYPE, check_transformation)
309
+
310
+
311
+ def bind_patch(statement: dict, patch) -> str:
312
+ """The patch the statement names must be the patch given, and agree on both roots."""
313
+ p = statement["predicate"]
314
+ if p["recipe"].get("patch_id") != patch.id:
315
+ return "binding_mismatch"
316
+ if patch.base_root != p["base"]["weights_root"] or patch.result_root != p["result"]["weights_root"]:
317
+ return "binding_mismatch"
318
+ return "ok"
319
+
320
+
321
+ def bind_model(statement: dict, model: str | Path) -> str:
322
+ """Hash a result directory two ways and compare: the weights root, then the OMS file digests."""
323
+ p = statement["predicate"]
324
+ root = root_of(tensor_table(Checkpoint.open(str(model))))
325
+ if root != p["result"]["weights_root"]:
326
+ return "result_mismatch"
327
+ if not p["result"]["files"]:
328
+ return "ok"
329
+ try:
330
+ listed = file_digests(model, tuple(p["result"]["ignore"]))
331
+ except ValueError:
332
+ return "files_mismatch"
333
+ return "ok" if listed == p["result"]["files"] else "files_mismatch"
334
+
335
+
336
+ def status(*, action: str, target: str, weights_root: str, reason: str, meaning: str,
337
+ replacement: str | None = None, time: str | None = None) -> dict:
338
+ predicate = {"type": "neyra.status/1", "action": action, "target": target, "weights_root": weights_root,
339
+ "reason": reason, "meaning": meaning, "time": time or now(),
340
+ "tool": {"name": "neyra", "version": __version__}}
341
+ if replacement is not None:
342
+ predicate["replacement"] = replacement
343
+ return {"_type": STATEMENT_TYPE, "subject": [{"name": "result-weights", "digest": {WEIGHTS_KEY: weights_root[7:]}}],
344
+ "predicateType": STATUS_TYPE, "predicate": predicate}
345
+
346
+
347
+ def check_status(statement: dict) -> None:
348
+ p = statement.get("predicate")
349
+ if not isinstance(p, dict):
350
+ raise ProtocolError("bad_status", "the predicate is not an object")
351
+ if p.get("action") not in ACTIONS:
352
+ raise ProtocolError("bad_status", "action is not supersede or withdraw")
353
+ want = STATUS_MEMBERS - ({"replacement"} if p["action"] == "withdraw" else set())
354
+ if set(p) - want - {k for k in p if k.startswith("x-")} or want - set(p) or p["type"] != "neyra.status/1":
355
+ raise ProtocolError("bad_status", "the predicate has the wrong members")
356
+ for key in ("target", "weights_root", *(["replacement"] if "replacement" in want else [])):
357
+ if not isinstance(p[key], str) or not DIGEST.fullmatch(p[key]):
358
+ raise ProtocolError("bad_status", f"{key} is not a digest")
359
+ if not isinstance(p["reason"], str) or not p["reason"] or p["meaning"] not in MEANINGS:
360
+ raise ProtocolError("bad_status", "reason or meaning is missing")
361
+ if not isinstance(p["time"], str) or not TIME.fullmatch(p["time"]):
362
+ raise ProtocolError("bad_status", "time is not an RFC 3339 UTC string")
363
+ tool = p["tool"]
364
+ if not isinstance(tool, dict) or set(tool) != {"name", "version"} or not all(isinstance(v, str) for v in tool.values()):
365
+ raise ProtocolError("bad_status", "tool is malformed")
366
+ if p["action"] == "supersede" and p["replacement"] == p["target"]:
367
+ raise ProtocolError("bad_status", "a statement cannot supersede itself")
368
+ if statement.get("subject") != [{"name": "result-weights", "digest": {WEIGHTS_KEY: p["weights_root"][7:]}}]:
369
+ raise ProtocolError("subject_mismatch", "the subject must be the weights root the record concerns")
370
+
371
+
372
+ def open_status(doc: dict, public: bytes | None = None) -> Opened:
373
+ return open_statement(doc, public, STATUS_TYPE, check_status)
374
+
375
+
376
+ def weights_root_of(statement: dict) -> str | None:
377
+ """The result weights root a transformation statement or a v1 receipt is about."""
378
+ p = statement.get("predicate", {})
379
+ if statement.get("predicateType") == PREDICATE_TYPE:
380
+ return p["result"]["weights_root"]
381
+ claim = p.get("claim") if isinstance(p, dict) else None
382
+ return claim.get("result_root") if isinstance(claim, dict) else None
383
+
384
+
385
+ def bind_target(record: Opened, target: Opened) -> str:
386
+ """A status record applies to a statement when it names the digest of its payload and the same weights."""
387
+ p = record.statement["predicate"]
388
+ ok = p["target"] == statement_digest(target.payload) and p["weights_root"] == weights_root_of(target.statement)
389
+ return "ok" if ok else "binding_mismatch"
390
+
391
+
392
+ def resolve(target: Opened, records: list[Opened]) -> dict:
393
+ """What the records the caller trusts say about a statement. Nothing is ever edited; a withdrawal wins."""
394
+ mine = [r for r in records if bind_target(r, target) == "ok"]
395
+ withdrawn = [r.statement["predicate"] for r in mine if r.statement["predicate"]["action"] == "withdraw"]
396
+ superseded = [r.statement["predicate"] for r in mine if r.statement["predicate"]["action"] == "supersede"]
397
+ state = "withdrawn" if withdrawn else "superseded" if superseded else "current"
398
+ return {"state": state, "withdrawn": withdrawn, "superseded_by": [p["replacement"] for p in superseded]}
399
+
400
+
401
+ def short(value: str, n: int = 12) -> str:
402
+ return value.split(":", 1)[-1][:n]
403
+
404
+
405
+ def to_cyclonedx(doc: dict) -> dict:
406
+ """A CycloneDX 1.7 BOM with the result as a machine-learning-model component and the base as its ancestor.
407
+
408
+ Properties use the `neyra:` namespace. Hashes appear only on the file components, where they are hashes of
409
+ files. The weights roots are not file hashes and travel as properties.
410
+ """
411
+ statement, _ = decode_envelope(doc)
412
+ p = statement["predicate"]
413
+ base, result = p["base"], p["result"]
414
+
415
+ def files(owner: str, listing: list[dict]) -> list[dict]:
416
+ return [{"type": "file", "bom-ref": f"{owner}:{f['path']}", "name": f["path"],
417
+ "hashes": [{"alg": "SHA-256", "content": f["sha256"]}]} for f in listing]
418
+
419
+ ancestor = {"type": "machine-learning-model", "bom-ref": "base", "name": "base model",
420
+ "properties": [{"name": "neyra:weights-root", "value": base["weights_root"]}]}
421
+ if base["files"]:
422
+ ancestor["components"] = files("base", base["files"])
423
+ pedigree = {"ancestors": [ancestor],
424
+ "notes": f"{p['kind']} ({p['grade']}). Not measured: {', '.join(p['not_measured'])}."}
425
+ if "patch_id" in p["recipe"]:
426
+ pedigree["patches"] = [{"type": "unofficial", "diff": {"url": "urn:neyra:patch:" + p["recipe"]["patch_id"][7:]}}]
427
+ props = [("neyra:kind", p["kind"]), ("neyra:evidence-grade", p["grade"]), ("neyra:meaning", p["meaning"]),
428
+ ("neyra:weights-root", result["weights_root"]), ("neyra:base-weights-root", base["weights_root"])]
429
+ if p["extent"]:
430
+ props += [(f"neyra:extent:{k.replace('_', '-')}", str(v)) for k, v in p["extent"].items()]
431
+ props += [("neyra:coverage:bytes-read", str(p["coverage"]["bytes_read"])),
432
+ ("neyra:coverage:bytes-total", str(p["coverage"]["bytes_total"]))]
433
+ props += [("neyra:attempts", str(p["attempts"]) if p["attempts"] else "not stated")]
434
+ props += [("neyra:recomputable", s) for s in p["basis"]["recomputable"]]
435
+ props += [("neyra:asserted", s) for s in p["basis"]["asserted"]]
436
+ props += [("neyra:not-measured", s) for s in p["not_measured"]]
437
+ props += [("neyra:" + k.replace("_", "-"), str(v)) for k, v in sorted(p["recipe"].items())]
438
+ model = {"type": "machine-learning-model", "bom-ref": "result", "name": result["name"] or "result model",
439
+ "pedigree": pedigree, "properties": [{"name": n, "value": v} for n, v in props],
440
+ "externalReferences": [{"type": "attestation", "url": "change.statement.json",
441
+ "comment": "DSSE envelope holding the signed neyra transformation statement",
442
+ "hashes": [{"alg": "SHA-256", "content": hashlib.sha256(
443
+ base64.b64decode(doc["payload"])).hexdigest()}]}]}
444
+ if result["files"]:
445
+ model["components"] = files("result", result["files"])
446
+ return {"bomFormat": "CycloneDX", "specVersion": "1.7", "version": 1,
447
+ "metadata": {"timestamp": p["time"],
448
+ "tools": {"components": [{"type": "application", "name": p["tool"]["name"],
449
+ "version": p["tool"]["version"]}]}},
450
+ "components": [model]}
451
+
452
+
453
+ def to_page(doc: dict, signed_by: str | None = None) -> str:
454
+ """One page for a person. It leads with what the record cannot tell them."""
455
+ statement, payload = decode_envelope(doc)
456
+ p = statement["predicate"]
457
+ base, result, extent = p["base"], p["result"], p["extent"]
458
+ signer = signed_by or (doc["signatures"][0]["keyid"] if doc.get("signatures") else None)
459
+ lines = ["# Model change statement", "",
460
+ f"Made {p['time']} with {p['tool']['name']} {p['tool']['version']}. "
461
+ + (f"Signed by key {short(signer, 16)}. Meaning of the signature: {p['meaning']}, the signer {MEANINGS[p['meaning']]}."
462
+ if signer else "Not signed: an unauthenticated record."), "",
463
+ "## What changed", "",
464
+ f"- Result: {result['name'] or 'unnamed'}, weights root {result['weights_root']}",
465
+ f"- Base: weights root {base['weights_root']}",
466
+ f"- Kind: {p['kind']}. Evidence: {p['grade']} ({GRADE_TEXT[p['grade']]})."]
467
+ if extent:
468
+ pct = 100 * extent["entries_changed"] / extent["entries_total"] if extent["entries_total"] else 0
469
+ lines.append(f"- Extent: {extent['changed']} of {extent['tensors_base']} tensors changed, {extent['added']} added, "
470
+ f"{extent['removed']} removed. In the changed tensors {extent['entries_changed']:,} of "
471
+ f"{extent['entries_total']:,} entries differ ({pct:.3f}%).")
472
+ else:
473
+ lines.append("- Extent: not measured by the signer.")
474
+ if "patch_id" in p["recipe"]:
475
+ lines.append(f"- Patch: {p['recipe']['patch_id']}")
476
+ if result["files"]:
477
+ lines.append(f"- Result files: {len(result['files'])}, joined to an OpenSSF model signature by sha256 "
478
+ f"(subject digest {oms_root(result['files'])}).")
479
+ lines.append(f"- Reading: {p['coverage']['bytes_read']:,} of {p['coverage']['bytes_total']:,} weight bytes were read.")
480
+ tries = p["attempts"]
481
+ lines.append(f"- Attempts: the signer says {tries}." if tries else
482
+ "- Attempts: not stated. Nothing here shows how many results were tried and put aside.")
483
+ lines += ["", "## What anyone can recompute, and what is only the signer's word", ""]
484
+ lines.append("Recomputable from the models: " + (", ".join(p["basis"]["recomputable"]) or "nothing") + ".")
485
+ lines.append("Only the signer's word: " + ", ".join(p["basis"]["asserted"]) + ".")
486
+ lines += ["", "## How to check it", ""] + [f"{i}. {s}" for i, s in enumerate(p["rederive"], 1)]
487
+ lines += ["", "## What this does not measure", "",
488
+ "- Training compute. No weight comparison can say how much was spent making these weights.",
489
+ "- Training data. Nothing here says what the model was trained or tuned on.",
490
+ "- Behaviour. Which bytes differ does not say what the model now does, or whether it is safe.",
491
+ "", ("This record shows which bytes differ. It is not a compute estimate, a safety evaluation or a "
492
+ "legal determination. A signature shows which key said this, not when, not that the signer is who they say, "
493
+ "and not that anything above is true beyond what the list of recomputable fields lets you check."),
494
+ "", f"Statement digest {statement_digest(payload)}", ""]
495
+ return "\n".join(lines)
496
+
497
+
498
+ GRADE_TEXT = {"exact": "the signer applied the named patch and got this result bit for bit",
499
+ "observed": "the signer hashed both models in full and compared every tensor",
500
+ "sampled": "only parts of the models were read; counts are lower bounds",
501
+ "declared": "the signer computed nothing and this is their word"}
neyra/behavior.py ADDED
@@ -0,0 +1,106 @@
1
+ """Measure how a generation went."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from dataclasses import dataclass
7
+
8
+ THINK_END = "</think>"
9
+ GRAM = 16
10
+ LOOP_TAIL = 512
11
+ SHORT_BUDGET = 4096
12
+ SHORT_LOOP_TAIL = 256
13
+
14
+ try:
15
+ from math_verify import parse as _parse
16
+ from math_verify import verify as _verify
17
+ except ImportError:
18
+ _parse = _verify = None
19
+
20
+
21
+ def repeat_flags(ids: list[int], n: int = GRAM) -> list[bool]:
22
+ """True where the n tokens ending here already appeared earlier in the text."""
23
+ flags = [False] * len(ids)
24
+ seen: set[tuple[int, ...]] = set()
25
+ for i in range(n - 1, len(ids)):
26
+ gram = tuple(ids[i - n + 1:i + 1])
27
+ flags[i] = gram in seen
28
+ seen.add(gram)
29
+ return flags
30
+
31
+
32
+ def loop_tail(ids: list[int], n: int = GRAM) -> int:
33
+ """Tokens at the end of a generation that sit inside one unbroken repeat of earlier text.
34
+
35
+ Validated on 800 published Qwen3.5-0.8B traces at 32k tokens: a tail of 512 or more
36
+ gives a true positive rate of 0.976 and a false positive rate of 0.020. Word level
37
+ duplicate counts flag a third of healthy reasoning, since careful work restates itself.
38
+ """
39
+ run = 0
40
+ for flagged in reversed(repeat_flags(ids, n)):
41
+ if not flagged:
42
+ break
43
+ run += 1
44
+ return run
45
+
46
+
47
+ def loop_onset(ids: list[int], n: int = GRAM) -> int | None:
48
+ """Estimated token where a terminal loop begins, or None when there is no loop."""
49
+ tail = loop_tail(ids, n)
50
+ return len(ids) - tail - n if tail >= SHORT_LOOP_TAIL else None
51
+
52
+
53
+ def is_looping(ids: list[int], budget: int) -> bool:
54
+ return loop_tail(ids) >= (SHORT_LOOP_TAIL if budget <= SHORT_BUDGET else LOOP_TAIL)
55
+
56
+
57
+ def boxed_answer(text: str) -> str | None:
58
+ """Content of the last \\boxed{...}, braces balanced."""
59
+ i = text.rfind("\\boxed{")
60
+ if i < 0:
61
+ return None
62
+ depth, j = 0, i + len("\\boxed")
63
+ for k in range(j, len(text)):
64
+ depth += (text[k] == "{") - (text[k] == "}")
65
+ if depth == 0:
66
+ return text[j + 1:k].strip()
67
+ return None
68
+
69
+
70
+ def normalize_answer(s: str) -> str:
71
+ s = s.replace("\\dfrac", "\\frac").replace("\\tfrac", "\\frac").replace("\\!", "")
72
+ s = s.replace("\\left", "").replace("\\right", "").replace("$", "").replace("\\%", "").replace("%", "")
73
+ s = re.sub(r"\\text\{[^}]*\}", "", s).replace(",", "").replace(" ", "")
74
+ return s.rstrip(".").lower()
75
+
76
+
77
+ def same_answer(got: str, gold: str) -> bool:
78
+ if normalize_answer(got) == normalize_answer(gold):
79
+ return True
80
+ if _verify is None:
81
+ return False
82
+ try:
83
+ return bool(_verify(_parse(f"${gold}$"), _parse(f"${got}$")))
84
+ except Exception: # noqa: BLE001 math-verify raises many types on unparsable LaTeX and timeouts
85
+ return False
86
+
87
+
88
+ @dataclass
89
+ class Outcome:
90
+ closed: bool
91
+ finished: bool
92
+ tail: int
93
+ looping: bool
94
+ answered: bool
95
+ correct: bool | None
96
+
97
+
98
+ def score(text: str, tokens: list[int], hit_limit: bool, budget: int, answer: str | None = None) -> Outcome:
99
+ closed = THINK_END in text
100
+ # A capped trace with an open think block has not answered, whatever it boxed on the way.
101
+ final = text.split(THINK_END)[-1] if closed or not hit_limit else ""
102
+ got = boxed_answer(final)
103
+ correct = None if answer is None else got is not None and same_answer(got, answer)
104
+ tail = loop_tail(tokens)
105
+ return Outcome(closed, not hit_limit, tail, tail >= (SHORT_LOOP_TAIL if budget <= SHORT_BUDGET else LOOP_TAIL),
106
+ got is not None, correct)