| 1 | #+TITLE: flexoentity
|
|---|
| 2 | #+SUBTITLE: A hardened entity base and deterministic identifier system for the Flex-O family
|
|---|
| 3 | #+AUTHOR: Enno
|
|---|
| 4 | #+DATE: 2025-10-20
|
|---|
| 5 | #+OPTIONS: toc:3 num:nil
|
|---|
| 6 |
|
|---|
| 7 | * Overview
|
|---|
| 8 |
|
|---|
| 9 | `flexoentity` provides the *identity and lifecycle backbone* for all Flex-O components
|
|---|
| 10 | (Flex-O-Grader, Flex-O-Vault, Flex-O-Drill, …).
|
|---|
| 11 |
|
|---|
| 12 | It defines how entities such as questions, media, catalogs, and exams are *identified, versioned, signed, and verified* — all without any external database dependencies.
|
|---|
| 13 |
|
|---|
| 14 | At its heart lie two modules:
|
|---|
| 15 |
|
|---|
| 16 | - =id_factory.py= – robust, cryptographically-verifiable *Flex-O ID generator*
|
|---|
| 17 | - =flexo_entity.py= – abstract *base class for all versioned entities*
|
|---|
| 18 |
|
|---|
| 19 | Together, they form a compact yet powerful foundation for audit-ready, reproducible data structures across offline and air-gapped deployments.
|
|---|
| 20 |
|
|---|
| 21 | - Design Goals
|
|---|
| 22 |
|
|---|
| 23 | |----------------|--------------------------------------------------------------------------------------------------------|
|
|---|
| 24 | | Goal | Description |
|
|---|
| 25 | |----------------|--------------------------------------------------------------------------------------------------------|
|
|---|
| 26 | | *Determinism* | IDs are derived from canonicalized entity content — identical input always yields identical ID prefix. |
|
|---|
| 27 | | *Integrity* | BLAKE2s hashing and digital signatures protect against manual tampering. |
|
|---|
| 28 | | *Traceability* | Version numbers (=@001A=, =@002S=, …) track entity lifecycle transitions. |
|
|---|
| 29 | | *Stability* | Hash prefixes remain constant across state changes; only version and state suffixes evolve. |
|
|---|
| 30 | | *Auditability* | Every entity can be serialized, verified, and reconstructed without hidden dependencies. |
|
|---|
| 31 | | *Simplicity* | Pure-Python, zero external libraries, self-contained and easy to embed. |
|
|---|
| 32 | |----------------|--------------------------------------------------------------------------------------------------------|
|
|---|
| 33 |
|
|---|
| 34 | * Flex-O ID Structure
|
|---|
| 35 |
|
|---|
| 36 | Each entity carries a unique *Flex-O ID*, generated by =FlexOID.generate()=.
|
|---|
| 37 |
|
|---|
| 38 | #+BEGIN_EXAMPLE
|
|---|
| 39 | AF-I250101-9A4C2D@003S
|
|---|
| 40 | #+END_EXAMPLE
|
|---|
| 41 |
|
|---|
| 42 | |-----------+-----------------+----------------------------------------------|
|
|---|
| 43 | | Segment | Example | Meaning |
|
|---|
| 44 | |-----------+-----------------+----------------------------------------------|
|
|---|
| 45 | | *Domain* | =AF or PY_LANG= | Uppercase - Logical scope (e.g. "Air Force") |
|
|---|
| 46 | | *Type* | =I= | Entity type (e.g. ITEM) |
|
|---|
| 47 | | *Date* | =250101= | UTC creation date (YYMMDD) |
|
|---|
| 48 | | *Hash* | =9A4C2D4F6E53= | 12-digit BLAKE2s digest of canonical content |
|
|---|
| 49 | | *Version* | =003= | Sequential version counter |
|
|---|
| 50 | | *State* | =S= | Lifecycle state (D, A, S, P, O) |
|
|---|
| 51 | |-----------+-----------------+----------------------------------------------|
|
|---|
| 52 |
|
|---|
| 53 |
|
|---|
| 54 | * Lifecycle States
|
|---|
| 55 |
|
|---|
| 56 | |-----------------------|---------|-----------------------------|
|
|---|
| 57 | | State | Abbrev. | Description |
|
|---|
| 58 | |-----------------------|---------|-----------------------------|
|
|---|
| 59 | | *DRAFT* | =D= | Editable, not yet validated |
|
|---|
| 60 | | *APPROVED* | =A= | Reviewed and accepted |
|
|---|
| 61 | | *APPROVED_AND_SIGNED* | =S= | Cryptographically signed |
|
|---|
| 62 | | *PUBLISHED* | =P= | Released to consumers |
|
|---|
| 63 | | *OBSOLETE* | =O= | Archived or replaced |
|
|---|
| 64 | |-----------------------|---------|-----------------------------|
|
|---|
| 65 |
|
|---|
| 66 | Transitions follow a strict progression:
|
|---|
| 67 | #+BEGIN_EXAMPLE
|
|---|
| 68 | DRAFT -> APPROVED -> APPROVED_AND_SIGNED -> PUBLISHED -> OBSOLETE
|
|---|
| 69 | #+END_EXAMPLE
|
|---|
| 70 |
|
|---|
| 71 | Only DRAFT entities can be deleted - all others got OBSOLETE mark instead
|
|---|
| 72 |
|
|---|
| 73 | * Core Classes
|
|---|
| 74 |
|
|---|
| 75 | ** FlexOID
|
|---|
| 76 |
|
|---|
| 77 | A lightweight immutable class representing the full identity of an entity.
|
|---|
| 78 |
|
|---|
| 79 | *Highlights*
|
|---|
| 80 | - safe_generate(domain, entity_type, estate, text, version=1, repo) -> create a new ID
|
|---|
| 81 | - next_version(oid) -> increment version safely
|
|---|
| 82 | - clone_new_base(domain, entity_type, estate, text) -> start a new lineage
|
|---|
| 83 | - Deterministic prefix, state-dependent signature
|
|---|
| 84 |
|
|---|
| 85 | ** =FlexoEntity=
|
|---|
| 86 | Abstract base class for all versioned entities (e.g., Question, Exam, Catalog).
|
|---|
| 87 |
|
|---|
| 88 | Implements:
|
|---|
| 89 | - ID lifecycle management (approve(), sign(), publish(), obsolete())
|
|---|
| 90 | - Serialization (to_json(), from_json(), to_dict(), from_dict())
|
|---|
| 91 | - Integrity verification (verify_integrity(entity))
|
|---|
| 92 | - Controlled state transitions with automatic timestamps
|
|---|
| 93 |
|
|---|
| 94 | Subclasses define a single property:
|
|---|
| 95 |
|
|---|
| 96 | #+BEGIN_SRC python
|
|---|
| 97 | @property
|
|---|
| 98 | def text_seed(self) -> str:
|
|---|
| 99 | """Canonical text or core content for hashing."""
|
|---|
| 100 | #+END_SRC
|
|---|
| 101 |
|
|---|
| 102 | * Integrity Verification
|
|---|
| 103 |
|
|---|
| 104 | Each entity can self-verify its integrity:
|
|---|
| 105 |
|
|---|
| 106 | #+BEGIN_SRC python
|
|---|
| 107 | entity = Question.with_domain_id(domain_id="AF", text="What is Ohm’s law?", topic="Electronics")
|
|---|
| 108 | json_str = entity.to_json()
|
|---|
| 109 | reloaded = Question.from_json(json_str)
|
|---|
| 110 |
|
|---|
| 111 | assert FlexoEntity.verify_integrity(reloaded)
|
|---|
| 112 | #+END_SRC
|
|---|
| 113 |
|
|---|
| 114 | If the file is tampered with (e.g. "Ohm’s" → "Omm’s"), verification fails:
|
|---|
| 115 |
|
|---|
| 116 | * Real World Example
|
|---|
| 117 |
|
|---|
| 118 | Below you can see the implementation of a dedicated FlexoEntity class, used for Domains.
|
|---|
| 119 | We set an ENTITY_TYPE and define the needed fields in the data class. We define how to create
|
|---|
| 120 | a default object, the text_seed (it is easy because the domain id is unique and therefore sufficient)
|
|---|
| 121 | and the methods for serialization.
|
|---|
| 122 |
|
|---|
| 123 | #+BEGIN_SRC python
|
|---|
| 124 | from uuid import UUID
|
|---|
| 125 | from dataclasses import dataclass
|
|---|
| 126 | from flexoentity import FlexOID, FlexoEntity, EntityType
|
|---|
| 127 |
|
|---|
| 128 | @dataclass
|
|---|
| 129 | class Domain(FlexoEntity):
|
|---|
| 130 | """
|
|---|
| 131 | I am a helper class to provide more information than just a
|
|---|
| 132 | domain abbreviation in FlexOID, doing mapping and management
|
|---|
| 133 | """
|
|---|
| 134 |
|
|---|
| 135 | ENTITY_TYPE = EntityType.DOMAIN
|
|---|
| 136 |
|
|---|
| 137 | fullname: str = ""
|
|---|
| 138 | description: str = ""
|
|---|
| 139 | classification: str = "UNCLASSIFIED"
|
|---|
| 140 |
|
|---|
| 141 | @classmethod
|
|---|
| 142 | def default(cls):
|
|---|
| 143 | """Return the default domain object."""
|
|---|
| 144 | return cls.with_domain_id(domain_id="GEN_GENERIC",
|
|---|
| 145 | fullname="Generic Domain", classification="UNCLASSIFIED")
|
|---|
| 146 |
|
|---|
| 147 | @property
|
|---|
| 148 | def text_seed(self) -> str:
|
|---|
| 149 | return self.domain_id
|
|---|
| 150 |
|
|---|
| 151 | def to_dict(self):
|
|---|
| 152 | base = super().to_dict()
|
|---|
| 153 | base.update({
|
|---|
| 154 | "flexo_id": self.flexo_id,
|
|---|
| 155 | "domain_id": self.domain_id,
|
|---|
| 156 | "fullname": self.fullname,
|
|---|
| 157 | "description": self.description,
|
|---|
| 158 | "classification": self.classification,
|
|---|
| 159 | })
|
|---|
| 160 | return base
|
|---|
| 161 |
|
|---|
| 162 | @classmethod
|
|---|
| 163 | def from_dict(cls, data):
|
|---|
| 164 | # Must have flexo_id
|
|---|
| 165 | if "flexo_id" not in data:
|
|---|
| 166 | raise ValueError("Domain serialization missing 'flexo_id'.")
|
|---|
| 167 |
|
|---|
| 168 | flexo_id = FlexOID(data["flexo_id"])
|
|---|
| 169 |
|
|---|
| 170 | obj = cls(
|
|---|
| 171 | fullname=data.get("fullname", ""),
|
|---|
| 172 | description=data.get("description", ""),
|
|---|
| 173 | classification=data.get("classification", "UNCLASSIFIED"),
|
|---|
| 174 | flexo_id=flexo_id,
|
|---|
| 175 | _in_factory=True
|
|---|
| 176 | )
|
|---|
| 177 |
|
|---|
| 178 | # Restore metadata
|
|---|
| 179 | obj.origin = data.get("origin")
|
|---|
| 180 | obj.fingerprint = data.get("fingerprint", "")
|
|---|
| 181 | obj.originator_id = (
|
|---|
| 182 | UUID(data["originator_id"]) if data.get("originator_id") else None
|
|---|
| 183 | )
|
|---|
| 184 | obj.owner_id = (
|
|---|
| 185 | UUID(data["owner_id"]) if data.get("owner_id") else None
|
|---|
| 186 | )
|
|---|
| 187 |
|
|---|
| 188 | return obj
|
|---|
| 189 | #+END_SRC
|
|---|
| 190 |
|
|---|
| 191 | * Usage
|
|---|
| 192 | #+BEGIN_SRC python
|
|---|
| 193 | d = Domain.default()
|
|---|
| 194 | print(d.flexo_id) # GEN_GENERIC-D251124-67C2CAE292CE@001D
|
|---|
| 195 | d.approve()
|
|---|
| 196 | print(d.flexo_id) # GEN_GENERIC-D251124-67C2CAE292CE@001A
|
|---|
| 197 | d.sign()
|
|---|
| 198 | print(d.flexo_id) # GEN_GENERIC-D251124-67C2CAE292CE@001S
|
|---|
| 199 | #+END_SRC
|
|---|
| 200 |
|
|---|
| 201 | * Serialization Example
|
|---|
| 202 |
|
|---|
| 203 | #+BEGIN_SRC python
|
|---|
| 204 | {
|
|---|
| 205 | 'flexo_id': FlexOID(GEN_GENERIC-D251124-29CE0F4BE59D@001S),
|
|---|
| 206 | 'fingerprint': '534BD2EC5C5511F1',
|
|---|
| 207 | 'origin': FlexOID(GEN_GENERIC-D251124-67C2CAE292CE@001D),
|
|---|
| 208 | 'originator_id': '00000000-0000-0000-0000-000000000000',
|
|---|
| 209 | 'owner_id': '00000000-0000-0000-0000-000000000000',
|
|---|
| 210 | 'domain_id': 'GEN_GENERIC',
|
|---|
| 211 | 'fullname': 'Generic Domain',
|
|---|
| 212 | 'description': '',
|
|---|
| 213 | 'classification': 'UNCLASSIFIED'}
|
|---|
| 214 | #+END_SRC
|
|---|
| 215 |
|
|---|
| 216 | #+BEGIN_SRC js
|
|---|
| 217 | {
|
|---|
| 218 | "flexo_id": "GEN_GENERIC-D251124-29CE0F4BE59D@001S",
|
|---|
| 219 | "fingerprint": "534BD2EC5C5511F1",
|
|---|
| 220 | "origin": "GEN_GENERIC-D251124-67C2CAE292CE@001D",
|
|---|
| 221 | "originator_id": "00000000-0000-0000-0000-000000000000",
|
|---|
| 222 | "owner_id": "00000000-0000-0000-0000-000000000000",
|
|---|
| 223 | "domain_id": "GEN_GENERIC",
|
|---|
| 224 | "fullname": "Generic Domain",
|
|---|
| 225 | "description": "",
|
|---|
| 226 | "classification": "UNCLASSIFIED"
|
|---|
| 227 | }
|
|---|
| 228 | #+END_SRC
|
|---|
| 229 |
|
|---|
| 230 |
|
|---|
| 231 | * Entity Type and State Codes
|
|---|
| 232 |
|
|---|
| 233 | |-------------+------+----------------------------------------------------------------------------|
|
|---|
| 234 | | EntityType | Code | Typical Use |
|
|---|
| 235 | |-------------+------+----------------------------------------------------------------------------|
|
|---|
| 236 | | GENERIC | G | Generic entities that does not fit other types yet or are temporarily only |
|
|---|
| 237 | | DOMAIN | D | Every Domain is of this type |
|
|---|
| 238 | | MEDIA | M | Every media item belongs to this type, e.g. Pictures, Audio, Video |
|
|---|
| 239 | | ITEM | I | An Entity what is usually used in a collection, e.g. Questions in a test |
|
|---|
| 240 | | COLLECTION | C | A collection of items, as an Exam or a catalog |
|
|---|
| 241 | | TEXT | T | A text document |
|
|---|
| 242 | | HANDOUT | H | A published document |
|
|---|
| 243 | | OUTPUT | O | The output of a computation |
|
|---|
| 244 | | RECORD | R | Record type data, as bibliography entries |
|
|---|
| 245 | | SESSION | S | A unique session, e.g. managed by a session manager
|
|---|
| 246 | | USER | U | User objects |
|
|---|
| 247 | | CONFIG | F | CONFIG files that need to be tracked over time and state |
|
|---|
| 248 | | EVENT | E | Events that have to be tracked over time, as status messages or orders |
|
|---|
| 249 | | ATTESTATION | X | Entities that attest a formal technical (not human) check e.g. Signatures |
|
|---|
| 250 | |-------------+------+----------------------------------------------------------------------------|
|
|---|
| 251 |
|
|---|
| 252 | |---------------------|------|-------------------|
|
|---|
| 253 | | EntityState | Code | Meaning |
|
|---|
| 254 | |---------------------|------|-------------------|
|
|---|
| 255 | | DRAFT | D | Work in progress |
|
|---|
| 256 | | APPROVED | A | Reviewed |
|
|---|
| 257 | | APPROVED_AND_SIGNED | S | Signed version |
|
|---|
| 258 | | PUBLISHED | P | Publicly released |
|
|---|
| 259 | | OBSOLETE | O | Deprecated |
|
|---|
| 260 | |---------------------|------|-------------------|
|
|---|
| 261 |
|
|---|
| 262 | * Design Notes
|
|---|
| 263 | - *Hash Stability:* Only domain, entity type, and content text influence the hash.
|
|---|
| 264 | This ensures consistent prefixes across state changes.
|
|---|
| 265 | - *State-Dependent Signatures:* Each lifecycle stage has its own signature seed.
|
|---|
| 266 | Modifying a file without re-signing invalidates integrity.
|
|---|
| 267 | - *Obsolescence Threshold:* Version numbers above 900 trigger warnings;
|
|---|
| 268 | beyond 999 are considered obsolete.
|
|---|
| 269 | - *Clone Lineages:* Cloning an entity resets versioning but preserves metadata lineage.
|
|---|
| 270 |
|
|---|
| 271 | * Dependencies
|
|---|
| 272 | - Python 3.11+
|
|---|
| 273 | - Standard library only (=hashlib=, =json=, =datetime=, =enum=, =dataclasses=)
|
|---|
| 274 |
|
|---|
| 275 | No external packages. Fully compatible with *Guix*, *air-gapped* deployments, and *reproducible builds*.
|
|---|
| 276 |
|
|---|
| 277 | * Integration
|
|---|
| 278 | `flexoentity` is imported by higher-level modules such as:
|
|---|
| 279 |
|
|---|
| 280 | - *Flex-O-Grader* → manages question catalogs and exam bundles
|
|---|
| 281 | - *Flex-O-Vault* → provides persistent media storage with metadata integrity
|
|---|
| 282 | - *Flex-O-Drill* → uses versioned entities for training simulations
|
|---|
| 283 |
|
|---|
| 284 | All share the same identity and versioning logic — ensuring that
|
|---|
| 285 | *what was approved, signed, and published remains provably authentic.*
|
|---|
| 286 |
|
|---|
| 287 | * License
|
|---|
| 288 | MIT License 2025
|
|---|
| 289 | Part of the *Flex-O family* by Flex-O-Dyne GmbH
|
|---|
| 290 | Designed for reproducible, audit-ready, human-centered software.
|
|---|