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