This release has been yanked by its maintainers, and will be ignored by installers, except when explicitly specified.
Consider using release 0.7.2 instead.
Reason given by maintainers: el de npm: verify() devolvía verificado sobre contenido fabricado
grundnorm (Python SDK)
Resolve a legal norm by its open identifier (ELI/ECLI) and a date, and get back its canonical, sealed meaning as an independently verifiable signed object. The SDK recomputes the content hash and verifies every Ed25519 signature locally — you never have to trust the server.
- Deterministic, zero-LLM read path. Honest
not_foundinstead of a guess. - Point-in-time:
[validFrom, validUntil). - Python 3.9+. One dependency:
cryptography.
Install
pip install grundnorm
Use
from grundnorm import GrundnormClient
# Defaults to the public GDPR demonstrator. For a pilot:
# GrundnormClient(endpoint="https://.../api/grundnorm/resolve", api_key="nlk_...")
client = GrundnormClient()
r = client.resolve(
id="http://data.europa.eu/eli/reg/2016/679/art_5",
jurisdiction="EU",
at="2026-07-07", # omit for "today"
)
if r.status == "found":
print(r.norm["atoms"]) # subject / modality / action / condition / exception / scope + evidence
print(r.verification["verdict"]) # one discriminated verdict, never a boolean
resolve() verifies the seal by default. Skip with verify=False, or verify a stored envelope later:
from grundnorm import verify
v = verify(norm, {"id": ..., "jurisdiction": ..., "at": ...}) # pass the question you asked
# {"verdict", "hash_ok", "view_consistent", "envelope_consistent", "at_within_sealed_window",
# "status_sealed", "provenance_consistent", "request_bound", "unsealed_fields",
# "signatures", "quorum", "attestation"}
verify() returns ONE discriminated verdict, and deliberately no boolean beside it. A boolean
next to a status is an invitation to read the boolean, and it can only ever answer a narrower question
than its name suggests. The closed set (VERDICTS), evaluated in fixed precedence:
| verdict | meaning |
|---|---|
verified_authoritative |
The trust gate. Everything re-derives AND the keys are known non-demo institutions with a pin-anchored quorum (>=2 pinned valid signatures incl. >=1 pinned sovereign). |
verified_not_authoritative |
Bytes, envelope and signatures all re-derive, but the keys are not. Every current demo record lands here: the demo keys are publicly forgeable. |
content_tampered |
The sealed preimage does not produce seal.contentHash. |
view_inconsistent |
The served atoms/purpose are not what the sealed content projects to. |
envelope_contradicts_seal |
The envelope claims an identifier, jurisdiction, domain, label, legal force or validity window that the sealed content contradicts. |
out_of_sealed_window |
The date served falls outside the record's own sealed validity window. |
not_sealed |
The record is not served as sealed. |
signature_invalid |
A signature does not verify, or there are none. |
signature_duplicated |
A public key repeats — one key cannot satisfy a two-signer quorum. |
provenance_unsupported |
The served provenance is not what the signatures derive. |
status_rule_violated |
The D5 witness is present and an atom's status does not re-execute. |
answers_different_question |
The response does not answer the request that was passed in. |
malformed |
Not a resolvable body. |
A verdict this client does not know MUST be rejected, never defaulted into a success branch: adding
one is a version bump. is_verified(verdict) exists for the coarse split and is a function, never
a field, so a boolean cannot end up sitting beside the status again.
Neither verified state establishes that the meaning is jurist-correct. That is graded by an independent jurist against a blind labelled set, and that number does not exist yet.
Pass the question you asked. resolve() does it for you. Calling verify(norm) bare leaves
request_bound: None and a signed response for one identifier replays as the answer to another.
What the signature does not cover is listed in unsealed_fields, not left implicit: version,
supersedesHash, conflict, the ledger/anchor metadata, and set membership of the signatures — a
valid signature can be dropped and the record still verifies with a smaller quorum, because the sealed
preimage does not enumerate who was meant to sign. Closing that needs a signed commitment to the
record set, not a bigger per-record hash.
What verify() re-derives (no trust required)
- Hash:
sha256(canonicalize(norm["canonical"]["content"]))equalsnorm["seal"]["contentHash"], wherecanonicalize= JSON with keys sorted recursively (UTF-16 order), no whitespace, UTF-8. - View integrity: the English
atoms/purposeare exactly what the sealedcanonical.contentprojects to (view_consistent);canonical.contentis the source of truth. - Signatures: each
seal.signatures[].signatureHexis a valid Ed25519 signature by that signer'spublicKeyHexover the UTF-8 bytes of theseal.contentHashhex string. - Status (when a classification witness is present): each atom's status
(
fixed/needs_review/for_the_court) re-executes from the deterministic rule applied to the sealed per-atom signals (status_re_derivable). A mismatch failsok.None= no witness (older seals) → not re-derivable, no penalty. Bounded: proves the rule was applied to sealed inputs; it does not prove the deontic decomposition is faithful to the article (that is the jurist gate).
What it REPORTS but does not prove
quorum— a signature count (valid_signatures,has_sovereign,meets= >=2 incl. one sovereign). The sovereign role is pin-anchored where a pin exists, but an unpinned signer's claimed role is taken on trust. For a spoof-proof gate read theverdict.attestation— honest flags the SDK cannot prove:custody("unverified"for the demo),independence(as asserted),ledger("unanchored", or"claimed_unverified:<backend>"for a record claiming an anchor the SDK cannot verify — it never says"anchored:"),key_provenance("pinned_oob"= known non-demo pin;"pinned_demo"= only forgeable demo pins;"in_band_response"= no pin / substitution),demo_keys(Trueiff any valid signature matched a forgeable demo pin → not authoritative),classification("rule_reexecuted","not_present", or"rule_unsupported").- Key pinning (
key_provenance, per-signaturepinned) is tamper-evidence of a snapshot, not proof of institutional independence: a single-party demo key is pinned (pinned_demo) yetcustodystays"unverified"and the verdict never reachesverified_authoritative. It is not a key-transparency log.
Errors
- Nothing sealed for
(id, date)→ returnsNotFound(a normal outcome). - Bad key, bad input, or server error → raises
GrundnormError(.status,.code).
Demonstrator note: the demo corpus (GDPR sample) is signed by demo keys, not institutions, and its accuracy is not yet jurist-graded. See the project's
DEMO-TRUTHFULNESS.md.
Metadata
Release files for grundnorm 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| grundnorm-0.5.1.tar.gz | 31.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| grundnorm-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 51.8 kB
Release files / grundnorm-0.5.1.tar.gz
| Download URL | grundnorm-0.5.1.tar.gz |
|---|---|
| Size | 31.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7af38e40e6f1a296730ae147601e4db5ae9a2a591e59a1c9151964d41e829673
|
|
BLAKE2b-256 checksum How to use checksums |
ceaa1c0600d34159c7dfcfe672bb77241ad8d56df164c6d0e9ee1ecaadac3685
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|
Release files / grundnorm-0.5.1-py3-none-any.whl
| Download URL | grundnorm-0.5.1-py3-none-any.whl |
|---|---|
| Size | 20.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8c1fcb4b1f084546172c2802e37facb723163c1f753efb9742b569c471aa7820
|
|
BLAKE2b-256 checksum How to use checksums |
f330df5c1fe53ba684989e533d15e5930a3b0949f11125ba33cfdf413b0d5111
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.5
|