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.
- neyra/__init__.py +3 -0
- neyra/anytime.py +50 -0
- neyra/attest.py +501 -0
- neyra/behavior.py +106 -0
- neyra/bisect.py +213 -0
- neyra/check.py +385 -0
- neyra/cli.py +1234 -0
- neyra/clock.py +221 -0
- neyra/conformance.py +319 -0
- neyra/data/neutral.json +26 -0
- neyra/demo.py +83 -0
- neyra/diff.py +244 -0
- neyra/edit.py +509 -0
- neyra/editeval.py +147 -0
- neyra/examine.py +70 -0
- neyra/explain.py +47 -0
- neyra/export.py +112 -0
- neyra/families.py +96 -0
- neyra/gguf_io.py +207 -0
- neyra/hop.py +378 -0
- neyra/hop_gguf.py +152 -0
- neyra/hop_text.py +287 -0
- neyra/identity.py +254 -0
- neyra/interop.py +38 -0
- neyra/kquant.py +122 -0
- neyra/lab.py +528 -0
- neyra/ledger.py +98 -0
- neyra/mcp_server.py +125 -0
- neyra/merge.py +165 -0
- neyra/numerics.py +36 -0
- neyra/patch.py +287 -0
- neyra/probe.py +72 -0
- neyra/protocol.py +420 -0
- neyra/recover.py +195 -0
- neyra/repair.py +211 -0
- neyra/rows.py +412 -0
- neyra/runtime/__init__.py +65 -0
- neyra/runtime/mlx.py +417 -0
- neyra/runtime/torch.py +121 -0
- neyra/stats.py +58 -0
- neyra/store.py +310 -0
- neyra/survival.py +103 -0
- neyra/tasks.py +62 -0
- neyra/units.py +57 -0
- neyra/vectors/PROTOCOL.md +578 -0
- neyra/vectors/cases.json +2760 -0
- neyra/vectors/fixtures/bad-manifest-beats-unknown-extension.neyra +0 -0
- neyra/vectors/fixtures/bad-type.neyra +0 -0
- neyra/vectors/fixtures/base/model-0.safetensors +0 -0
- neyra/vectors/fixtures/base-resharded/model-0.safetensors +0 -0
- neyra/vectors/fixtures/base-resharded/model-1.safetensors +0 -0
- neyra/vectors/fixtures/bind-broken.json +1 -0
- neyra/vectors/fixtures/bind-good.json +1 -0
- neyra/vectors/fixtures/bind-other-base.json +1 -0
- neyra/vectors/fixtures/bind-other-patch-id.json +1 -0
- neyra/vectors/fixtures/bind-other-result.json +1 -0
- neyra/vectors/fixtures/consolidated-name/consolidated.safetensors +0 -0
- neyra/vectors/fixtures/consolidated-name/model-0.safetensors +0 -0
- neyra/vectors/fixtures/dir-named-safetensors/folder.safetensors/.keep +0 -0
- neyra/vectors/fixtures/dir-named-safetensors/model-0.safetensors +0 -0
- neyra/vectors/fixtures/dtype-is-a-list.neyra +0 -0
- neyra/vectors/fixtures/duplicate-name/model-0.safetensors +0 -0
- neyra/vectors/fixtures/duplicate-name/model-1.safetensors +0 -0
- neyra/vectors/fixtures/empty/.keep +0 -0
- neyra/vectors/fixtures/extra-payload-tensor.neyra +0 -0
- neyra/vectors/fixtures/files-broken.json +1 -0
- neyra/vectors/fixtures/files-files.json +1 -0
- neyra/vectors/fixtures/files-ignoring-signature.json +1 -0
- neyra/vectors/fixtures/files-no-files.json +1 -0
- neyra/vectors/fixtures/free-text-meta.neyra +0 -0
- neyra/vectors/fixtures/good.neyra +0 -0
- neyra/vectors/fixtures/header-repeated-field/model-0.safetensors +0 -0
- neyra/vectors/fixtures/header-repeated-key/model-0.safetensors +0 -0
- neyra/vectors/fixtures/hidden-file/.ghost.safetensors +0 -0
- neyra/vectors/fixtures/hidden-file/model-0.safetensors +0 -0
- neyra/vectors/fixtures/huge-header.neyra +0 -0
- neyra/vectors/fixtures/index-duplicate.neyra +0 -0
- neyra/vectors/fixtures/index-out-of-range.neyra +0 -0
- neyra/vectors/fixtures/index-unsorted.neyra +0 -0
- neyra/vectors/fixtures/ledger-bad-time.jsonl +3 -0
- neyra/vectors/fixtures/ledger-body-is-a-list.jsonl +3 -0
- neyra/vectors/fixtures/ledger-body-is-null.jsonl +3 -0
- neyra/vectors/fixtures/ledger-dropped.jsonl +2 -0
- neyra/vectors/fixtures/ledger-edited.jsonl +3 -0
- neyra/vectors/fixtures/ledger-empty.jsonl +0 -0
- neyra/vectors/fixtures/ledger-good.jsonl +3 -0
- neyra/vectors/fixtures/ledger-kind-is-a-number.jsonl +3 -0
- neyra/vectors/fixtures/ledger-no-final-newline.jsonl +3 -0
- neyra/vectors/fixtures/ledger-not-canonical.jsonl +3 -0
- neyra/vectors/fixtures/ledger-not-json.jsonl +3 -0
- neyra/vectors/fixtures/ledger-one-entry.jsonl +1 -0
- neyra/vectors/fixtures/ledger-seq-is-a-string.jsonl +3 -0
- neyra/vectors/fixtures/ledger-seq-is-true.jsonl +3 -0
- neyra/vectors/fixtures/ledger-swapped.jsonl +3 -0
- neyra/vectors/fixtures/ledger-time-fullwidth-digits.jsonl +3 -0
- neyra/vectors/fixtures/ledger-time-in-other-digits.jsonl +3 -0
- neyra/vectors/fixtures/ledger-time-newline.jsonl +3 -0
- neyra/vectors/fixtures/ledger-torn.jsonl +4 -0
- neyra/vectors/fixtures/length-mismatch.neyra +0 -0
- neyra/vectors/fixtures/lowrank-base/model-0.safetensors +0 -0
- neyra/vectors/fixtures/lowrank-base/model-1.safetensors +0 -0
- neyra/vectors/fixtures/lowrank-base/model-2.safetensors +0 -0
- neyra/vectors/fixtures/lowrank-extension-not-required.neyra +0 -0
- neyra/vectors/fixtures/lowrank-factor-rows-wrong.neyra +0 -0
- neyra/vectors/fixtures/lowrank-inner-sizes-differ.neyra +0 -0
- neyra/vectors/fixtures/lowrank-left-digest-wrong.neyra +0 -0
- neyra/vectors/fixtures/lowrank-left-is-float32.neyra +0 -0
- neyra/vectors/fixtures/lowrank-left-missing.neyra +0 -0
- neyra/vectors/fixtures/lowrank-left-without-right.neyra +0 -0
- neyra/vectors/fixtures/lowrank-on-a-vector.neyra +0 -0
- neyra/vectors/fixtures/lowrank-on-an-integer-tensor.neyra +0 -0
- neyra/vectors/fixtures/lowrank-other-extension-too.neyra +0 -0
- neyra/vectors/fixtures/lowrank-rank-zero.neyra +0 -0
- neyra/vectors/fixtures/lowrank-result-digest-wrong.neyra +0 -0
- neyra/vectors/fixtures/lowrank-right-digest-wrong.neyra +0 -0
- neyra/vectors/fixtures/lowrank.neyra +0 -0
- neyra/vectors/fixtures/malformed-digest.neyra +0 -0
- neyra/vectors/fixtures/manifest-is-a-float.neyra +0 -0
- neyra/vectors/fixtures/manifest-is-a-spaced-array.neyra +0 -0
- neyra/vectors/fixtures/manifest-is-a-string.neyra +0 -0
- neyra/vectors/fixtures/manifest-is-an-array.neyra +0 -0
- neyra/vectors/fixtures/metadata-is-a-string.neyra +0 -0
- neyra/vectors/fixtures/metadata-is-an-empty-list.neyra +0 -0
- neyra/vectors/fixtures/metadata-is-null.neyra +0 -0
- neyra/vectors/fixtures/metadata-is-zero.neyra +0 -0
- neyra/vectors/fixtures/missing-result-root.neyra +0 -0
- neyra/vectors/fixtures/name-is-a-number.neyra +0 -0
- neyra/vectors/fixtures/no-manifest.neyra +0 -0
- neyra/vectors/fixtures/not-canonical.neyra +0 -0
- neyra/vectors/fixtures/other/model-0.safetensors +0 -0
- neyra/vectors/fixtures/other-patch.neyra +0 -0
- neyra/vectors/fixtures/overlap.neyra +0 -0
- neyra/vectors/fixtures/patch-header-repeats-payload.neyra +0 -0
- neyra/vectors/fixtures/receipt-bad-predicate.json +1 -0
- neyra/vectors/fixtures/receipt-bad-sig-base64.json +1 -0
- neyra/vectors/fixtures/receipt-bad-signature-then-good.json +1 -0
- neyra/vectors/fixtures/receipt-bad-type.json +1 -0
- neyra/vectors/fixtures/receipt-claim-digest-is-a-number.json +1 -0
- neyra/vectors/fixtures/receipt-claim-digest-without-prefix.json +1 -0
- neyra/vectors/fixtures/receipt-claim-incomplete.json +1 -0
- neyra/vectors/fixtures/receipt-ed25519-key-for-p256-signature.json +1 -0
- neyra/vectors/fixtures/receipt-good-signature-then-bad.json +1 -0
- neyra/vectors/fixtures/receipt-head-mid-ledger.json +1 -0
- neyra/vectors/fixtures/receipt-invalid-utf8.json +1 -0
- neyra/vectors/fixtures/receipt-is-an-array.json +1 -0
- neyra/vectors/fixtures/receipt-ledger-len-one-ok.json +1 -0
- neyra/vectors/fixtures/receipt-ledger-len-string.json +1 -0
- neyra/vectors/fixtures/receipt-ledger-len-true.json +1 -0
- neyra/vectors/fixtures/receipt-no-key-given.json +1 -0
- neyra/vectors/fixtures/receipt-no-ledger-head.json +1 -0
- neyra/vectors/fixtures/receipt-not-canonical.json +1 -0
- neyra/vectors/fixtures/receipt-only-a-bad-signature.json +1 -0
- neyra/vectors/fixtures/receipt-p256-key-for-ed25519-signature.json +1 -0
- neyra/vectors/fixtures/receipt-p256-no-key-given.json +1 -0
- neyra/vectors/fixtures/receipt-p256-tampered-claim.json +1 -0
- neyra/vectors/fixtures/receipt-p256-wrong-curve-key.json +1 -0
- neyra/vectors/fixtures/receipt-p256-wrong-key.json +1 -0
- neyra/vectors/fixtures/receipt-padded-payload-ok.json +1 -0
- neyra/vectors/fixtures/receipt-payload-not-a-string.json +1 -0
- neyra/vectors/fixtures/receipt-payload-type-wrong-and-signatures-missing.json +1 -0
- neyra/vectors/fixtures/receipt-payload-with-stray-bits.json +1 -0
- neyra/vectors/fixtures/receipt-signature-entry-not-an-object.json +1 -0
- neyra/vectors/fixtures/receipt-signature-sig-not-a-string.json +1 -0
- neyra/vectors/fixtures/receipt-signature-with-stray-bits.json +1 -0
- neyra/vectors/fixtures/receipt-signature-without-sig.json +1 -0
- neyra/vectors/fixtures/receipt-signatures-missing.json +1 -0
- neyra/vectors/fixtures/receipt-signatures-not-a-list.json +1 -0
- neyra/vectors/fixtures/receipt-signed-p256.json +1 -0
- neyra/vectors/fixtures/receipt-signed.json +1 -0
- neyra/vectors/fixtures/receipt-subjects-reversed-ok.json +1 -0
- neyra/vectors/fixtures/receipt-tampered-claim.json +1 -0
- neyra/vectors/fixtures/receipt-three-subjects.json +1 -0
- neyra/vectors/fixtures/receipt-two-signatures.json +1 -0
- neyra/vectors/fixtures/receipt-unknown-head.json +1 -0
- neyra/vectors/fixtures/receipt-unsigned-but-key-given.json +1 -0
- neyra/vectors/fixtures/receipt-unsigned.json +1 -0
- neyra/vectors/fixtures/receipt-wrong-digest-algorithm.json +1 -0
- neyra/vectors/fixtures/receipt-wrong-key.json +1 -0
- neyra/vectors/fixtures/receipt-wrong-length.json +1 -0
- neyra/vectors/fixtures/receipt-wrong-subject.json +1 -0
- neyra/vectors/fixtures/repeated-tensor-name.neyra +0 -0
- neyra/vectors/fixtures/requires-not-a-list.neyra +0 -0
- neyra/vectors/fixtures/result-model/.gitignore +1 -0
- neyra/vectors/fixtures/result-model/config.json +1 -0
- neyra/vectors/fixtures/result-model/docs/README.md +1 -0
- neyra/vectors/fixtures/result-model/docs.txt +1 -0
- neyra/vectors/fixtures/result-model/model-0.safetensors +0 -0
- neyra/vectors/fixtures/result-model-resharded/config.json +1 -0
- neyra/vectors/fixtures/result-model-resharded/docs/README.md +1 -0
- neyra/vectors/fixtures/result-model-resharded/docs.txt +1 -0
- neyra/vectors/fixtures/result-model-resharded/model-0.safetensors +0 -0
- neyra/vectors/fixtures/result-model-resharded/model-1.safetensors +0 -0
- neyra/vectors/fixtures/result-model-signed/config.json +1 -0
- neyra/vectors/fixtures/result-model-signed/docs/README.md +1 -0
- neyra/vectors/fixtures/result-model-signed/docs.txt +1 -0
- neyra/vectors/fixtures/result-model-signed/model-0.safetensors +0 -0
- neyra/vectors/fixtures/result-model-signed/model.sig +1 -0
- neyra/vectors/fixtures/result-model-tampered-config/config.json +1 -0
- neyra/vectors/fixtures/result-model-tampered-config/docs/README.md +1 -0
- neyra/vectors/fixtures/result-model-tampered-config/docs.txt +1 -0
- neyra/vectors/fixtures/result-model-tampered-config/model-0.safetensors +0 -0
- neyra/vectors/fixtures/rows/bytes.data +1 -0
- neyra/vectors/fixtures/rows/bytes.json +1 -0
- neyra/vectors/fixtures/rows/flipped-bit.data +0 -0
- neyra/vectors/fixtures/rows/flipped-bit.json +1 -0
- neyra/vectors/fixtures/rows/not-json.data +0 -0
- neyra/vectors/fixtures/rows/not-json.json +1 -0
- neyra/vectors/fixtures/rows/one-row-per-leaf.data +0 -0
- neyra/vectors/fixtures/rows/one-row-per-leaf.json +1 -0
- neyra/vectors/fixtures/rows/other-dtype.data +0 -0
- neyra/vectors/fixtures/rows/other-dtype.json +1 -0
- neyra/vectors/fixtures/rows/other-name.data +0 -0
- neyra/vectors/fixtures/rows/other-name.json +1 -0
- neyra/vectors/fixtures/rows/other-position.data +0 -0
- neyra/vectors/fixtures/rows/other-position.json +1 -0
- neyra/vectors/fixtures/rows/other-root.data +0 -0
- neyra/vectors/fixtures/rows/other-root.json +1 -0
- neyra/vectors/fixtures/rows/partial-last-leaf.data +0 -0
- neyra/vectors/fixtures/rows/partial-last-leaf.json +1 -0
- neyra/vectors/fixtures/rows/path-changed.data +0 -0
- neyra/vectors/fixtures/rows/path-changed.json +1 -0
- neyra/vectors/fixtures/rows/path-short.data +0 -0
- neyra/vectors/fixtures/rows/path-short.json +1 -0
- neyra/vectors/fixtures/rows/range-past-end.data +0 -0
- neyra/vectors/fixtures/rows/range-past-end.json +1 -0
- neyra/vectors/fixtures/rows/ranges-unsorted.data +0 -0
- neyra/vectors/fixtures/rows/ranges-unsorted.json +1 -0
- neyra/vectors/fixtures/rows/scalar.data +0 -0
- neyra/vectors/fixtures/rows/scalar.json +1 -0
- neyra/vectors/fixtures/rows/short-data.data +0 -0
- neyra/vectors/fixtures/rows/short-data.json +1 -0
- neyra/vectors/fixtures/rows/sibling-changed.data +0 -0
- neyra/vectors/fixtures/rows/sibling-changed.json +1 -0
- neyra/vectors/fixtures/rows/three-ranges.data +0 -0
- neyra/vectors/fixtures/rows/three-ranges.json +1 -0
- neyra/vectors/fixtures/rows/tree-long.data +0 -0
- neyra/vectors/fixtures/rows/tree-long.json +1 -0
- neyra/vectors/fixtures/rows/two-leaves.data +0 -0
- neyra/vectors/fixtures/rows/two-leaves.json +1 -0
- neyra/vectors/fixtures/rows/unknown-member.data +0 -0
- neyra/vectors/fixtures/rows/unknown-member.json +1 -0
- neyra/vectors/fixtures/rows/vector-tail.data +0 -0
- neyra/vectors/fixtures/rows/vector-tail.json +1 -0
- neyra/vectors/fixtures/rows-shapes/model-0.safetensors +0 -0
- neyra/vectors/fixtures/shape-has-a-boolean.neyra +0 -0
- neyra/vectors/fixtures/shape-has-a-list.neyra +0 -0
- neyra/vectors/fixtures/shape-is-negative.neyra +0 -0
- neyra/vectors/fixtures/short.neyra +1 -0
- neyra/vectors/fixtures/status-empty-reason.json +1 -0
- neyra/vectors/fixtures/status-for-another-model.json +1 -0
- neyra/vectors/fixtures/status-for-another-statement.json +1 -0
- neyra/vectors/fixtures/status-for-receipt.json +1 -0
- neyra/vectors/fixtures/status-meaning-outside-vocabulary.json +1 -0
- neyra/vectors/fixtures/status-subject-differs.json +1 -0
- neyra/vectors/fixtures/status-supersede-itself.json +1 -0
- neyra/vectors/fixtures/status-supersede-p256.json +1 -0
- neyra/vectors/fixtures/status-supersede-without-replacement.json +1 -0
- neyra/vectors/fixtures/status-target-not-a-digest.json +1 -0
- neyra/vectors/fixtures/status-time-malformed.json +1 -0
- neyra/vectors/fixtures/status-unknown-action.json +1 -0
- neyra/vectors/fixtures/status-unknown-member.json +1 -0
- neyra/vectors/fixtures/status-unsigned.json +1 -0
- neyra/vectors/fixtures/status-withdraw-names-a-replacement.json +1 -0
- neyra/vectors/fixtures/status-withdraw.json +1 -0
- neyra/vectors/fixtures/status-wrong-key.json +1 -0
- neyra/vectors/fixtures/status-wrong-predicate-type.json +1 -0
- neyra/vectors/fixtures/status-x-member-ok.json +1 -0
- neyra/vectors/fixtures/target-receipt.json +1 -0
- neyra/vectors/fixtures/target-transformation.json +1 -0
- neyra/vectors/fixtures/tensor-member-missing.neyra +0 -0
- neyra/vectors/fixtures/trailing-bytes.neyra +0 -0
- neyra/vectors/fixtures/transform-attempts-a-string.json +1 -0
- neyra/vectors/fixtures/transform-attempts-counted-ok.json +1 -0
- neyra/vectors/fixtures/transform-attempts-zero.json +1 -0
- neyra/vectors/fixtures/transform-bad-payload-type.json +1 -0
- neyra/vectors/fixtures/transform-basis-moves-a-field.json +1 -0
- neyra/vectors/fixtures/transform-coverage-missing.json +1 -0
- neyra/vectors/fixtures/transform-declared-but-bytes-read.json +1 -0
- neyra/vectors/fixtures/transform-declared-with-the-basis-of-an-exact-one.json +1 -0
- neyra/vectors/fixtures/transform-declared-without-extent-ok.json +1 -0
- neyra/vectors/fixtures/transform-duplicate-key.json +1 -0
- neyra/vectors/fixtures/transform-evidence-bad-digest.json +1 -0
- neyra/vectors/fixtures/transform-exact-but-partly-read.json +1 -0
- neyra/vectors/fixtures/transform-exact-without-patch-id.json +1 -0
- neyra/vectors/fixtures/transform-extent-member-missing.json +1 -0
- neyra/vectors/fixtures/transform-extent-not-integers.json +1 -0
- neyra/vectors/fixtures/transform-file-digest-uppercase.json +1 -0
- neyra/vectors/fixtures/transform-file-path-escapes.json +1 -0
- neyra/vectors/fixtures/transform-files-repeat-a-path.json +1 -0
- neyra/vectors/fixtures/transform-files-sorted-as-strings.json +1 -0
- neyra/vectors/fixtures/transform-files-without-name.json +1 -0
- neyra/vectors/fixtures/transform-grade-outside-vocabulary.json +1 -0
- neyra/vectors/fixtures/transform-ignore-not-a-list.json +1 -0
- neyra/vectors/fixtures/transform-kind-outside-vocabulary.json +1 -0
- neyra/vectors/fixtures/transform-meaning-outside-vocabulary.json +1 -0
- neyra/vectors/fixtures/transform-member-missing.json +1 -0
- neyra/vectors/fixtures/transform-no-files-ok.json +1 -0
- neyra/vectors/fixtures/transform-no-key-given.json +1 -0
- neyra/vectors/fixtures/transform-not-canonical.json +1 -0
- neyra/vectors/fixtures/transform-not-measured-extra-ok.json +1 -0
- neyra/vectors/fixtures/transform-not-measured-incomplete.json +1 -0
- neyra/vectors/fixtures/transform-observed-without-extent.json +1 -0
- neyra/vectors/fixtures/transform-rederive-empty.json +1 -0
- neyra/vectors/fixtures/transform-root-not-a-digest.json +1 -0
- neyra/vectors/fixtures/transform-sampled-but-fully-read.json +1 -0
- neyra/vectors/fixtures/transform-sampled-ok.json +1 -0
- neyra/vectors/fixtures/transform-sampled-with-the-basis-of-an-exact-one.json +1 -0
- neyra/vectors/fixtures/transform-signed-ed25519.json +1 -0
- neyra/vectors/fixtures/transform-signed-p256.json +1 -0
- neyra/vectors/fixtures/transform-subject-extra.json +1 -0
- neyra/vectors/fixtures/transform-subject-missing-a-file.json +1 -0
- neyra/vectors/fixtures/transform-subject-missing-oms-root.json +1 -0
- neyra/vectors/fixtures/transform-subjects-in-any-order-ok.json +1 -0
- neyra/vectors/fixtures/transform-tampered-after-signing.json +1 -0
- neyra/vectors/fixtures/transform-time-malformed.json +1 -0
- neyra/vectors/fixtures/transform-tool-incomplete.json +1 -0
- neyra/vectors/fixtures/transform-unknown-kind-cannot-be-exact.json +1 -0
- neyra/vectors/fixtures/transform-unknown-member.json +1 -0
- neyra/vectors/fixtures/transform-unsigned-but-key-given.json +1 -0
- neyra/vectors/fixtures/transform-unsigned.json +1 -0
- neyra/vectors/fixtures/transform-weights-subject-under-sha256.json +1 -0
- neyra/vectors/fixtures/transform-wrong-key.json +1 -0
- neyra/vectors/fixtures/transform-wrong-predicate-type.json +1 -0
- neyra/vectors/fixtures/transform-x-member-inside-result-refused.json +1 -0
- neyra/vectors/fixtures/transform-x-member-ok.json +1 -0
- neyra/vectors/fixtures/unknown-extension.neyra +0 -0
- neyra/vectors/fixtures/unknown-member-beats-bad-manifest.neyra +0 -0
- neyra/vectors/fixtures/unknown-member.neyra +0 -0
- neyra/vectors/fixtures/unknown-tensor-member.neyra +0 -0
- neyra/vectors/fixtures/val-wrong-dtype.neyra +0 -0
- neyra/vectors/fixtures/wrong-base-digest.neyra +0 -0
- neyra/vectors/fixtures/wrong-result-digest.neyra +0 -0
- neyra/vectors/fixtures/wrong-result-root.neyra +0 -0
- neyra/vectors/fixtures/wrong-val-digest.neyra +0 -0
- neyra/vectors/fixtures/x-member-ok.neyra +0 -0
- neyra/vectors/fixtures/x-tensor-member-ok.neyra +0 -0
- neyra/vectors/generate.py +908 -0
- neyra/workflows.py +222 -0
- neyra-0.2.1.dist-info/METADATA +219 -0
- neyra-0.2.1.dist-info/RECORD +343 -0
- neyra-0.2.1.dist-info/WHEEL +4 -0
- neyra-0.2.1.dist-info/entry_points.txt +2 -0
- neyra-0.2.1.dist-info/licenses/LICENSE +202 -0
neyra/__init__.py
ADDED
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)
|