# 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.