httk.store.id_ledger¶
A signed, append-only id ledger mapping stable source keys to entry ids.
The ledger is an allocator: it maps a stable, opaque source key to a
recommended entry id (httk.core.entry_ids) and hands the same id back for
that key forever, so a database rebuilt from the same sources keeps its ids and
content changes become revisions rather than fresh entries.
The on-disk container is a single stdlib-sqlite3 database file (not one of
httk-store’s own store engines). Its meta table records the container format
and the id series; its records table holds the ordered ledger records, each a
{key, family, id} assignment or a {key, alias_of} alias, either optionally
carrying supersedes (see below), keyed by a 1-based append sequence; and its
segments table records, one row per append, the contiguous range of records
that append added together with the canonical seal-body signature over exactly
that segment (httk.core.project.sealing: kind="httk-idledger-segment",
its subject carrying the format version, this ledger’s uuid, the id series,
the full per-family id bases at that point, and the segment’s number and record
range). The uuid binds every segment to this ledger, so a segment cannot be
grafted from another ledger that happens to share a series and bases. Each close
that appends signs only the segment it added, so old segments are never rewritten.
Signatures attest to the bytes, never to their meaning, so IdLedger.open()
validates the container’s structure and every entry invariant itself. A segment
signature is an audit record, not a build gate: it is logged (naming the
signers) and inspected manually alongside git history, never demanded. The
integrity self-check is always on — an INVALID signature (content that no longer
matches its own segment: tamper, corruption, a hand-edit) always raises. Trust
enforcement is opt-in: with trusted_keys every segment’s signer must be one
of them; without them a valid signature is accepted and the signers merely noted.
The segments partition the records exactly, so a middle-record deletion,
renumbering, or any record/segment range mismatch is caught in-file by the
partition check even without verification. What no in-file check can attest is
the tip: removing the newest segment(s) together with exactly the records they
cover leaves a state indistinguishable from an older valid ledger (as with any
whole-file rollback), so that class of loss is witnessed by git history alone.
The recovery for a corrupted ledger is to restore it from git; the errors say so.
No record is ever edited or removed. An entry for a source that later disappears
simply persists and keeps its number. A key is re-bound only by supersession:
appending a fresh record that carries supersedes=<the id the key resolved to>.
Supersession exists to track source-data regrouping — when the store’s
content deduplication splits one shared-content group into several, or merges
several into one — and is driven deliberately by build scripts. It is never a
recovery path for a lost or corrupted ledger; that is restore-from-git. The
newest record for a key wins; a superseded id stays reserved (counters only
grow) and stays resolvable through whatever key still points at it, becoming a
harmless orphan when none does.
Exceptions¶
An id ledger cannot be opened, verified, locked, or extended. |
Classes¶
A signed, append-only allocator of stable entry ids for source keys. |
Functions¶
|
Check a ledger source key, warning for deviations but never rejecting. |
Module Contents¶
- exception httk.store.id_ledger.IdLedgerError¶
Bases:
RuntimeErrorAn id ledger cannot be opened, verified, locked, or extended.
This covers both the cannot proceed cases — a held lock, a signature or invariant that does not verify — and the API-misuse cases — assigning an aliased key without superseding, or aliasing a key to an id absent from the ledger.
- httk.store.id_ledger.check_ledger_key(key)¶
Check a ledger source key, warning for deviations but never rejecting.
Ledger keys are opaque to the allocator: the standardized grammar is a convention enforced only by the workflow helpers, so this mirrors
httk.core.entry_ids.check_entry_idin stance but, unlike it, never raises — it only warns on leading/trailing whitespace or non-URL-safe characters.
- class httk.store.id_ledger.IdLedger(path, *, bases, series, ledger_uuid, keys, records, live, persisted, segments)¶
A signed, append-only allocator of stable entry ids for source keys.
Open one with
create()oropen()and use it as a context manager; the enclosingwithholds an exclusive lock and, on exit, appends and signs one new segment only when something was assigned or aliased. Callers use those constructors rather than the initializer, which binds an already-validated state to its file and lock.- Parameters:
path (pathlib.Path) – The ledger sqlite database path.
bases (dict[str, str]) – The per-family id bases, keyed by family name.
series (str) – The id series every minted id carries.
ledger_uuid (str) – This ledger’s identity, stamped into every segment subject.
keys (collections.abc.Sequence[tuple[str, bytes]]) – The signing keys used to sign the next segment on close, each a
(role, seed)pair.records (list[dict[str, str]]) – The ordered ledger records, each a
{key, ...}mapping.live (dict[str, dict[str, str]]) – The newest record per key, the key’s live binding.
persisted (int) – How many records are already stored on disk.
segments (int) – How many segments are already stored on disk.
- classmethod create(path, *, bases, series, keys)¶
Create and sign a fresh, empty ledger, then hold it open and locked.
The fresh database carries segment 1: an empty segment (no records) whose signed subject stamps the initial bases, so the ledger is signed from birth.
- Parameters:
path (str | os.PathLike[str]) – Where to write the ledger database; it must not exist.
bases (collections.abc.Mapping[str, str]) – The explicit per-family id bases, e.g.
{"structures": "anyt.am.structure"}.series (str) – The id series token every minted id carries.
keys (collections.abc.Sequence[tuple[str, bytes]]) – The signing keys, each a
(role, seed)pair.
- Returns:
The open, locked ledger.
- Raises:
IdLedgerError – If the ledger already exists or the lock is held.
ValueError – If a base or the series is malformed.
- Return type:
- classmethod open(path, *, keys=(), trusted_keys=(), verify=True, bases=None, series=None)¶
Open an existing ledger, verifying its signatures and invariants.
With verify, every segment’s signature is checked: an INVALID signature (content that no longer matches its own segment) always raises, while a valid one is treated as an audit record — trusted_keys demand a trusted signer on every segment, and without them the valid signers are logged. The structure and every entry invariant are validated regardless (segment ranges must partition the records exactly, bases must grow monotonically, ids must conform, supersession must be sound), because a signature attests only to the bytes, not to their meaning.
When series is given it must equal the stored series. When bases is given it is reconciled as a SUPERSET of the stored map: every stored family must appear in it with the same base (removing, renaming, or re-basing a stored family is an error), while families present only in the expectation are ADDED and stamped into the next segment’s subject at close (so a build that assigns nothing but grows the scheme still writes on close). The merged map is revalidated for id shape and base uniqueness.
- Parameters:
path (str | os.PathLike[str]) – The ledger database to open.
keys (collections.abc.Sequence[tuple[str, bytes]]) – The signing keys used to sign the next segment on close.
trusted_keys (collections.abc.Sequence[str]) – Trust anchors as
ed25519:keys orsha256:fingerprints; when given, every segment’s signer must be one of them.verify (bool) – Whether to verify the segment signatures.
bases (collections.abc.Mapping[str, str] | None) – The per-family bases the caller expects, asserted when given.
series (str | None) – The id series the caller expects, asserted when given.
- Returns:
The open, locked ledger.
- Raises:
IdLedgerError – If the ledger is missing, the lock is held, verification fails, an invariant is violated, or the expected bases/series disagree.
- Return type:
- assign(key, family, *, supersede=False)¶
Return the id for a source key, minting one on first sight.
The call is idempotent: a key already assigned in this family returns its existing id. A key assigned in another family is an error, and an aliased key is an error unless supersede re-binds it.
With
supersede=Trueon a currently-aliased key, a fresh assignment is appended carryingsupersedes=<the alias target>and a newly minted id — the split half of source regrouping.supersede=Trueon an already-assigned key is an error: an assignment is never re-bound to another assignment.- Parameters:
- Returns:
The assigned entry id.
- Raises:
IdLedgerError – If the key is aliased and supersede is false, its family disagrees, the family has no base, or supersede is passed outside its one transition (a new key, or an already-assigned key).
- Return type:
- alias(key, existing_id, *, supersede=False)¶
Record a source key pointing at an id already in the ledger.
Aliases exist because the store deduplicates content-identical rows family-wide: several keys can reach one row, which must keep one id. The call is idempotent for an identical
(key, existing_id)pair; a conflicting re-alias, or targeting an id absent from the ledger, is an error, and so is aliasing an already-assigned key unless supersede re-binds it.With
supersede=Trueon a currently-assigned key, an alias record is appended carryingsupersedes=<its old id>— the merge half of source regrouping. The old id stays reserved and stays resolvable while any other key points at it, otherwise becoming a harmless orphan.- Parameters:
- Raises:
IdLedgerError – On a conflict, an unknown target, an assigned key when supersede is false, or supersede passed for a new key.
- lookup(key)¶
Return the id a source key resolves to, following an alias.
Resolution is to the key’s newest record, so a superseded binding is never returned for the key that moved on from it.
- close()¶
Append and sign a new segment when the ledger changed, then unlock.
A ledger untouched since it was opened is left byte-identical: no connection is opened and nothing is written, so an idempotent rebuild produces no git churn. A dirty close with no signing key available raises from the sealing layer before anything is written; a failed close leaves the ledger unclosed and its lock held, so a retried close reattempts the write instead of silently skipping it (the manual remedy for an abandoned session is to delete the lock).