Skip to main content
Yanked

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_found instead 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)

  1. Hash: sha256(canonicalize(norm["canonical"]["content"])) equals norm["seal"]["contentHash"], where canonicalize = JSON with keys sorted recursively (UTF-16 order), no whitespace, UTF-8.
  2. View integrity: the English atoms/purpose are exactly what the sealed canonical.content projects to (view_consistent); canonical.content is the source of truth.
  3. Signatures: each seal.signatures[].signatureHex is a valid Ed25519 signature by that signer's publicKeyHex over the UTF-8 bytes of the seal.contentHash hex string.
  4. 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 fails ok. 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 the verdict.
  • 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 (True iff any valid signature matched a forgeable demo pin → not authoritative), classification ("rule_reexecuted", "not_present", or "rule_unsupported").
  • Key pinning (key_provenance, per-signature pinned) is tamper-evidence of a snapshot, not proof of institutional independence: a single-party demo key is pinned (pinned_demo) yet custody stays "unverified" and the verdict never reaches verified_authoritative. It is not a key-transparency log.

Errors

  • Nothing sealed for (id, date) → returns NotFound (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.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for grundnorm 0.5.0
File Size Uploaded
grundnorm-0.5.0.tar.gz 30.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for grundnorm 0.5.0
File Interpreter ABI Platform
grundnorm-0.5.0-py3-none-any.whl Python 3 none any Details

Total release size: 51.1 kB

Release files / grundnorm-0.5.0.tar.gz

Download URL grundnorm-0.5.0.tar.gz
Size 30.9 kB
Tags Source
SHA-256 checksum
How to use checksums
ef9bfeec7215c7abf82efc523328f7fc5c1a9db6ec8a9bac24d047bf46c7b3aa
BLAKE2b-256 checksum
How to use checksums
303b5c9ad81eca4b42740fca7d9d5597107608dac369d4e62e517c78f208d001
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.0-py3-none-any.whl

Download URL grundnorm-0.5.0-py3-none-any.whl
Size 20.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
80d6a1862c1c0b935487c7e2ddda9728cdd63effba397d3a6637c33f1b51c2fd
BLAKE2b-256 checksum
How to use checksums
222e47f72d0a74f190c8d902adbf1b73b80f4fd29766e4bb0ac7e762bcc197cf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.5

Release history Release notifications | RSS feed

0.7.2

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

This release

0.5.0 This release

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page