# Table of Contents - [Overview](#org77ab3e0) - [Flex-O ID Structure](#orgf136db1) - [Lifecycle States](#orgea1c2ca) - [Core Classes](#org2b0320a) - [FlexOID](#org39ec589) - [`FlexoEntity`](#orgd84c86d) - [Integrity Verification](#org4c3e14b) - [Real World Example](#orgfb82c02) - [Usage](#orge03e624) - [Serialization Example](#org118d77d) - [Entity Type and State Codes](#org83e21be) - [Design Notes](#orga31954b) - [Dependencies](#org5589bef) - [Integration](#org7b599dd) - [License](#org56e2d0f) # Overview \`flexoentity\` provides the **identity and lifecycle backbone** for all Flex-O components (Flex-O-Grader, Flex-O-Vault, Flex-O-Drill, …). It defines how entities such as questions, media, catalogs, and exams are **identified, versioned, signed, and verified** — all without any external database dependencies. At its heart lie two modules: - `id_factory.py` – robust, cryptographically-verifiable **Flex-O ID generator** - `flexo_entity.py` – abstract **base class for all versioned entities** Together, they form a compact yet powerful foundation for audit-ready, reproducible data structures across offline and air-gapped deployments. - Design Goals
Goal Description
Determinism IDs are derived from canonicalized entity content — identical input always yields identical ID prefix.
Integrity BLAKE2s hashing and digital signatures protect against manual tampering.
Traceability Version numbers (@001A, @002S, …) track entity lifecycle transitions.
Stability Hash prefixes remain constant across state changes; only version and state suffixes evolve.
Auditability Every entity can be serialized, verified, and reconstructed without hidden dependencies.
Simplicity Pure-Python, zero external libraries, self-contained and easy to embed.
# Flex-O ID Structure Each entity carries a unique **Flex-O ID**, generated by `FlexOID.generate()`. AF-I250101-9A4C2D@003S
Segment Example Meaning
Domain AF or PY_LANG Uppercase - Logical scope (e.g. “Air Force”)
Type I Entity type (e.g. ITEM)
Date 250101 UTC creation date (YYMMDD)
Hash 9A4C2D4F6E53 12-digit BLAKE2s digest of canonical content
Version 003 Sequential version counter
State S Lifecycle state (D, A, S, P, O)
# Lifecycle States
State Abbrev. Description
DRAFT D Editable, not yet validated
APPROVED A Reviewed and accepted
APPROVEDANDSIGNED S Cryptographically signed
PUBLISHED P Released to consumers
OBSOLETE O Archived or replaced
Transitions follow a strict progression: DRAFT -> APPROVED -> APPROVED_AND_SIGNED -> PUBLISHED -> OBSOLETE Only DRAFT entities can be deleted - all others got OBSOLETE mark instead # Core Classes ## FlexOID A lightweight immutable class representing the full identity of an entity. **Highlights** - safegenerate(domain, entitytype, estate, text, version=1, repo) -> create a new ID - nextversion(oid) -> increment version safely - clonenewbase(domain, entitytype, estate, text) -> start a new lineage - Deterministic prefix, state-dependent signature ## `FlexoEntity` Abstract base class for all versioned entities (e.g., Question, Exam, Catalog). Implements: - ID lifecycle management (approve(), sign(), publish(), obsolete()) - Serialization (tojson(), fromjson(), todict(), fromdict()) - Integrity verification (verifyintegrity(entity)) - Controlled state transitions with automatic timestamps Subclasses define a single property: @property def text_seed(self) -> str: """Canonical text or core content for hashing.""" # Integrity Verification Each entity can self-verify its integrity: entity = Question.with_domain_id(domain_id="AF", text="What is Ohm’s law?", topic="Electronics") json_str = entity.to_json() reloaded = Question.from_json(json_str) assert FlexoEntity.verify_integrity(reloaded) If the file is tampered with (e.g. “Ohm’s” → “Omm’s”), verification fails: # Real World Example Below you can see the implementation of a dedicated FlexoEntity class, used for Domains. We set an ENTITYTYPE and define the needed fields in the data class. We define how to create a default object, the textseed (it is easy because the domain id is unique and therefore sufficient) and the methods for serialization. from uuid import UUID from dataclasses import dataclass from flexoentity import FlexOID, FlexoEntity, EntityType @dataclass class Domain(FlexoEntity): """ I am a helper class to provide more information than just a domain abbreviation in FlexOID, doing mapping and management """ ENTITY_TYPE = EntityType.DOMAIN fullname: str = "" description: str = "" classification: str = "UNCLASSIFIED" @classmethod def default(cls): """Return the default domain object.""" return cls.with_domain_id(domain_id="GEN_GENERIC", fullname="Generic Domain", classification="UNCLASSIFIED") @property def text_seed(self) -> str: return self.domain_id def to_dict(self): base = super().to_dict() base.update({ "flexo_id": self.flexo_id, "domain_id": self.domain_id, "fullname": self.fullname, "description": self.description, "classification": self.classification, }) return base @classmethod def from_dict(cls, data): # Must have flexo_id if "flexo_id" not in data: raise ValueError("Domain serialization missing 'flexo_id'.") flexo_id = FlexOID(data["flexo_id"]) obj = cls( fullname=data.get("fullname", ""), description=data.get("description", ""), classification=data.get("classification", "UNCLASSIFIED"), flexo_id=flexo_id, _in_factory=True ) # Restore metadata obj.origin = data.get("origin") obj.fingerprint = data.get("fingerprint", "") obj.originator_id = ( UUID(data["originator_id"]) if data.get("originator_id") else None ) obj.owner_id = ( UUID(data["owner_id"]) if data.get("owner_id") else None ) return obj # Usage d = Domain.default() print(d.flexo_id) # GEN_GENERIC-D251124-67C2CAE292CE@001D d.approve() print(d.flexo_id) # GEN_GENERIC-D251124-67C2CAE292CE@001A d.sign() print(d.flexo_id) # GEN_GENERIC-D251124-67C2CAE292CE@001S # Serialization Example { 'flexo_id': FlexOID(GEN_GENERIC-D251124-29CE0F4BE59D@001S), 'fingerprint': '534BD2EC5C5511F1', 'origin': FlexOID(GEN_GENERIC-D251124-67C2CAE292CE@001D), 'originator_id': '00000000-0000-0000-0000-000000000000', 'owner_id': '00000000-0000-0000-0000-000000000000', 'domain_id': 'GEN_GENERIC', 'fullname': 'Generic Domain', 'description': '', 'classification': 'UNCLASSIFIED'} { "flexo_id": "GEN_GENERIC-D251124-29CE0F4BE59D@001S", "fingerprint": "534BD2EC5C5511F1", "origin": "GEN_GENERIC-D251124-67C2CAE292CE@001D", "originator_id": "00000000-0000-0000-0000-000000000000", "owner_id": "00000000-0000-0000-0000-000000000000", "domain_id": "GEN_GENERIC", "fullname": "Generic Domain", "description": "", "classification": "UNCLASSIFIED" } # Entity Type and State Codes
EntityType Code Typical Use
GENERIC G Generic entities that does not fit other types yet or are temporarily only
DOMAIN D Every Domain is of this type
MEDIA M Every media item belongs to this type, e.g. Pictures, Audio, Video
ITEM I An Entity what is usually used in a collection, e.g. Questions in a test
COLLECTION C A collection of items, as an Exam or a catalog
TEXT T A text document
HANDOUT H A published document
OUTPUT O The output of a computation
RECORD R Record type data, as bibliography entries
SESSION S A unique session, e.g. managed by a session manager
USER U User objects
CONFIG F CONFIG files that need to be tracked over time and state
EVENT E Events that have to be tracked over time, as status messages or orders
ATTESTATION X Entities that attest a formal technical (not human) check e.g. Signatures
EntityState Code Meaning
DRAFT D Work in progress
APPROVED A Reviewed
APPROVEDANDSIGNED S Signed version
PUBLISHED P Publicly released
OBSOLETE O Deprecated
# Design Notes - **Hash Stability:** Only domain, entity type, and content text influence the hash. This ensures consistent prefixes across state changes. - **State-Dependent Signatures:** Each lifecycle stage has its own signature seed. Modifying a file without re-signing invalidates integrity. - **Obsolescence Threshold:** Version numbers above 900 trigger warnings; beyond 999 are considered obsolete. - **Clone Lineages:** Cloning an entity resets versioning but preserves metadata lineage. # Dependencies - Python 3.11+ - Standard library only (`hashlib`, `json`, `datetime`, `enum`, `dataclasses`) No external packages. Fully compatible with **Guix**, **air-gapped** deployments, and **reproducible builds**. # Integration \`flexoentity\` is imported by higher-level modules such as: - **Flex-O-Grader** → manages question catalogs and exam bundles - **Flex-O-Vault** → provides persistent media storage with metadata integrity - **Flex-O-Drill** → uses versioned entities for training simulations All share the same identity and versioning logic — ensuring that **what was approved, signed, and published remains provably authentic.** # License MIT License 2025 Part of the **Flex-O family** by Flex-O-Dyne GmbH Designed for reproducible, audit-ready, human-centered software.