Index: org/FlexoEntity.org
===================================================================
--- org/FlexoEntity.org	(revision 37b5d111a63a7caf8477361289173c4798e334cb)
+++ 	(revision )
@@ -1,225 +1,0 @@
-* Tech-Talk - Hashes / IDs / Lebenszyklen / FlexoEntity
-:PROPERTIES:
-:CUSTOM_ID: hashes-ids-lifecycle
-:END:
-
-** Was ist ein Hash?
-
-Ein Hash ist ein hoffentlich eindeutiger "Fingerabdruck" für Daten, der
-sich aus eben diesen Daten über den Algorithmus einer Hash-Funktion errechnet.
-
-Die Eingabedaten bezeichnet man als Seed, das Ergebnis als Digest.
-
-Man gibt die Saat (das Futter) an die Hashfunktion zum Zerkauen/Zerkleinern (Hashen),
-das wird verdaut, nebenbei noch gewürzt (Salt) und kommt als Digest wieder heraus.
-Die Länge der Ausgabe ist fest.
-
-** Ein wichtiger Punkt
-
-Wichtig ist auch zu verstehen, dass Hashes sogenannte "One-Way-Funktionen" sind.
-Das heißt, man kann das Hashing nicht einfach umkehren, um das ursprüngliche Objekt wiederherzustellen.
-Ein Hash ist also eine Einbahnstraße: Er dient nur dazu, die Daten zu repräsentieren,
-aber man kann nicht vom Hash auf die originalen Daten zurückrechnen.
-Aus Kacke kann man kein Essen zaubern.
-
-** Was ist kein Hash?
-
-Die ISBN identifiziert ein Buch eindeutig. Sie ist nicht das Buch selbst, sondern eher ein
-Identifier (eine geordnete Nummer mit Prüfziffer). Sie ist aber kein Hash, weil sie nicht
-aus dem Inhalt des Buches berechnet wird. Ähnliches gilt für die IBAN
-
-|-------------------------+---------------------------+-------------------------------------|
-| Merkmal                 | Hash                      | Identifier (z. B. ISBN, IBAN, UUID) |
-|-------------------------+---------------------------+-------------------------------------|
-| Abhängig vom Inhalt     | ja                        | nein                                |
-| Ziel                    | Integrität / Vergleich    | Identität / Referenz                |
-| Länge                   | fix z. B. 32 Hex-Zeichen  | frei definierbar                    |
-| Zufällig oder berechnet | deterministisch berechnet | oft generiert oder vergeben         |
-|-------------------------+---------------------------+-------------------------------------|
-
-** Warum verwenden wir Hashes?
-
-Hashes sind super praktisch, weil sie uns eine Menge Zeit und Rechenleistung sparen.
-Anstatt zwei große Dateien oder ganze Bücher direkt miteinander zu vergleichen,
-vergleichen wir einfach ihre Hashes. Wenn die Hashes gleich sind, wissen wir,
-dass die Daten sehr wahrscheinlich identisch sind. Wenn sie unterschiedlich sind,
-hat sich etwas geändert. Das spart enorm viel Aufwand.
-
-** Beispiele für den Einsatz von Hashes
-
-- **Passwort-Hashes**: Wenn du ein Passwort eingibst, wird oft nur der Hash gespeichert, nicht das
-   eigentliche Passwort. So bleibt es sicher, selbst wenn jemand die Datenbank stiehlt.
-
-   Beispiel aus der /etc/shadow-Datei
-
-root:$6$tsicHaoV3Q$YtAbiIvrHGXFtAJYz9tcEYWHXiGVQ40sJAgzPAbc57lIq9jH8eYjWXwctSW6YQnrMznRFcm6yXLnnY9mHhso20
-enno:$6$sdygzfEgx0$YpaZJMQdkZgxGPclphz6RojqNG.PSNEq1oIHRP4kvZRN2iuS5MQrxt0nCkrYQIcpDGyohrb1o0S/GkWrFriWL1
-
-Der Hash ist länger als das Passwort. Das dient der Sicherheit, weil man zwar nicht zurückrechnen,
-wohl aber vorwärtsrechnen kann. Hash-Funktionen sind deterministisch.
-Bei Linux-Passworteinträgen hat man hier $6$ den Hashalgorithmus kodiert, im daraufolgendem $.....$ das Salz
-und im Rest den eigentlichen Hash kodiert
-
-- **Git-Commits**: In Versionskontrollsystemen wie Git werden Hashes benutzt, um jeden Schwung an
-  Änderungen mit einer eindeutigen Kennung zu versehen
-
-- **URL-Shortener: hash("https://example.com") → 8a3f12
-  
-** FlexOID
-
-Die FlexOID ist eine Mischform. Sie selbst ist ein Identifier, der als Bestandteil aber einen Hash
-enthält, um möglichst eindeutig, aber nicht zu lang zu sein.
-
-AF-Q251022-70F759@001D
-│  │ │       │     │ │
-│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
-│  │ │       │     └──── Version
-│  │ │       └────────── Hash (3 bytes, 6 Stellen)
-│  │ └────────────────── Date (YYMMDD)
-│  └──────────────────── Entity type (Question)
-└─────────────────────── Domain (e.g. Air force)
-
-In der ersten Variante der FlexOID habe ich mich mit einem 6-stelligen Hash zufrieden gegeben,
-weil das einfach lesbarer ist. Warum reicht das nicht?
-
-** Das Geburtstags-Paradoxon
-
-Der obige 3-Byte Hash liefert mir etwas über 16 Millionen unterschiedlich Varianten.
-Das klingt viel. Ist es aber nicht. Wieviele Schüler müssen nacheinander die Klasse
-betreten bis sich zwei finden, die mit mehr als 50 Prozent Wahrscheinlichkeit am gleichen
-Tag Geburtstag (also ein identisches Merkmal) haben?
-
-** Lösung
-
-Ab 23 Schülern beträgt die Wahrscheinlichkeit 50 % (näherungsweise Wurzel 365 Tagen)
-
-Bei 47 Schülern beträgt die Wahrscheinlichkeit bereits 95 Prozent
-
-Unsere 3 Bytes ergeben zwar 16.000.000 mögliche Varianten - im Gegenzug zu den 365 Tagen im Jahr
-aus dem Geburtstagsbeispiel - aber da die Wahrscheinlichkeit zur Kollision hier bei
-etwa Wurzel 16 Mio. liegt, bekommt man bereits bei 4000 Neuzugängen die ersten
-Übereinstimmungen im Hash.
-
-Der Hash ist also nicht gut genug, weil man sehr schnell und sehr häufig, diese
-Übereinstimmungen feststellen und behandeln müsste, wenn man weiterhin eindeutige
-Zuordnungen treffen will.
-
-Wenn man die Ausgabe der Hashfunktion auf 6 Bytes erweitert, kommen die ersten Kollisionen
-erst bei etwa 20 Mio erzeugten Hashes (Fingerabdrücken) und die kann man dann ohne
-Einbußen gesondert behandeln (Salzen), weil es so selten passiert.
-
-Übrigens, wenn man beim Menschen den Daumenabdruck nimmt und sich dabei auf 12 Merkmale
-beschränkt und sehr lockere Toleranzen ansetzt (was man in der Praxis nicht macht),
-hat man bereits nach 14000 Menschen eine Übereinstimmung. Bei 24 Merkmalen und sehr
-lockeren Toleranzen, hat man bei etwa nach 9 Mio. Menschen eine ungefähre Übereinstimmung.
-Da muss die Polizei schon sehr schlampig arbeiten, damit man fälschlicherweise beschuldigt wird.
-Die Zahlen in der Realität sind sogar noch deutlich höher.
-
-** FlexoEntity
-
-Nun haben wir gesehen, dass wir mit der FlexOID (mit 6-Byte Hash) sehr viele unterschiedliche
-Dinge eindeutig bestimmen können. Da unsere FlexOID erstmal nur eine Zeichenfolge ist,
-brauchen wir etwas das damit umgehen kann und was dafür verantwortlich ist. Das ist die FlexoEntity.
-
-Sie beinhaltet zusätzlich eine Signatur und ein Origin-Feld, wo festgehalten wird, woher diese
-Entität stammt (beispielsweise aus einer Hashkollision oder einer Änderung an den weiteren Daten)
-
-Jede Klasse, die von FlexoEntity erbt, muss zwingend die Method "text_seed" implementieren, mit der
-der Algorithmus einer Hash-Funktion gefüttert wird, aus der dann der 6-Byte Hash herauspurzelt.
-Hashfunktionen sind z.B. MD5, SHA1, SHA256 oder wie von mir genutzt Blake2s.
-Die Mathematik dahinter ist recht aufwändig, aber wer sich mal einlesen möchte
-
-- Hashing in Smalltalk: Theory and Practice von Andres Valloud
-
-Damit die Hash-Funktion genug Eingabedaten pro Entity hat, muss man sich überlegen, welche Merkmale
-einer Entität man durch "text_seed" übermittelt.
-
-** text_seed
-
-Man kann beliebige Klassen von FlexoEntity ableiten und erhält ohne Aufwand die Funktionalität
-zur eindeutigen Identifizierung und zur Lebenszyklus-Verwaltung
-
-
-Das ist das Beispiel einer OptionQuestion, also einer Testfrage, wo mögliche Antworten enthalten sind
-
-    @property
-    def text_seed(self) -> str:
-        """Include answer options (and points) for deterministic ID generation."""
-        base = super().text_seed
-        if not self.options:
-            return base
-
-        joined = "|".join(
-            f"{opt.text.strip()}:{opt.points}"
-            for opt in sorted(self.options, key=lambda o: o.text.strip().lower())
-        )
-        return f"{base}::{joined}"
-
-** Lebenszyklus
-
-Der Lebenszyklus einer Entität folgt dieser Reihenfolge und ist nicht umkehrbar
-
-- Entwurf (DRAFT)
-- Genehmigt (APPROVED)
-- Unterschrieben (SIGNED)
-- Veröffentlicht (PUBLISHED)
-- Veraltet (OBSOLETE)
-
-Eine Entität, die bereits die Stufe Veröffentlicht erreicht hat, kann nicht in die Stufe
-(nur) Unterschrieben zurückkehren. Daher ist auch die Lebenszyklusstufe in der ID kodiert
-(letztes Symbol der FlexOID)
-
-Beispiele für Entitäten:
-
-- Testfrage
-- Fragenkatalog
-- Einstufungstest
-- Zertifikat
-
-Ein veröffentlichter Einstufungstest kann nur Fragen beinhalten, die ihrerseits die Stufe Veröffentlicht
-erreicht haben. Ein Zertifikat kann nur ausgestellt werden, wenn der passende Einstufungstest die Stufe
-Veröffentlicht hat. Das origin-Feld des Zertifikats sollte sinnvollerweise die ID des Tests enthalten.
-
-** Erhöhung der Versionsnummer oder neue ID
-
-Sobald - aus Gründen - eine neue ID vergeben werden muss, wird ggf. die Ursprungs-ID
-im Feld origin der neuen FlexoEntity gespeichert. So ist immer ein Suchen im Stammbaum möglich.
-
-AF-Q251022-70F759@001D
-│  │ │       │     │ │
-│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
-│  │ │       │     └──── Version
-│  │ │       └────────── Hash (3 bytes, 6 Stellen)
-│  │ └────────────────── Date (YYMMDD)
-│  └──────────────────── Entity type (Question)
-└─────────────────────── Domain (e.g. Air force)
-
-
-Eine einfache Erhöhung der Versionsnummer (am Ende der ID) ist unter Umständen auch ausreichend,
-damit nicht zu häufig komplett neue IDs erzeugt werden müssen. Der Entscheidungsbaum dazu ist
-für das Projekt FlexoGrader recht umfangreich und soll hier nicht weiter besprochen werden.
-Da es aber im Hintergrund passiert und vom Endanwender nicht bemerkt wird, behindert der
-Mechanismus die Nutzung des FlexoGrader nicht. 
-
-** Reifegrad
-
-Das Konzept und Design hat die Alpha-Phase verlassen, der Code ist aber noch deutlich Beta.
-RC1 etwa Anfang Dezember (nach meinem Urlaub). Änderungen an der FlexOID sind nicht mehr zu erwarten, beim API der FlexoEntity schon eher.
-
-Das Perfekte ist der Feind des Guten, aber ...
-
-Als Gegenbeispiel für unausgereiftes Design ist Pythons eingebaute datetime Bibliothek.
-Da könnte man ein Buch drüber schreiben. Halb Drama, halb Komödie.
-
-- dateutil, arrow, pendulum, maya, moment, delorean, pytz, zoneinfo + numpy/pandas
-  
-** Lizenz
-
-Da ich die FlexoEntity-Bibliothek in meiner Freizeit entworfen und programmiert habe,
-bin ich alleiniger Urheber. Als Lizenz habe ich MIT gewählt, somit kann - unter Nennung
-des Urhebers - jeder damit machen, was er möchte
-
-Der FlexoGrader ist ein Bundeswehrprojekt, welches ich im Dienst programmiere und somit
-gehen die Nutzungsrechte uneingeschränkt auf den Dienstherren über.
-
-Das betrifft auch etwaige Folgeprojekte wie FlexoVault und FlexoDrill
Index: org/FlexoEntityTalk.org
===================================================================
--- org/FlexoEntityTalk.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ org/FlexoEntityTalk.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,224 @@
+* Tech-Talk - Hashes / IDs / Lebenszyklen / FlexoEntity
+:PROPERTIES:
+:CUSTOM_ID: hashes-ids-lifecycle
+:END:
+
+** Was ist ein Hash?
+
+Ein Hash ist ein hoffentlich eindeutiger "Fingerabdruck" für Daten, der
+sich aus eben diesen Daten über den Algorithmus einer Hash-Funktion errechnet.
+
+Die Eingabedaten bezeichnet man als Seed, das Ergebnis als Digest.
+
+Man gibt die Saat (das Futter) an die Hashfunktion zum Zerkauen/Zerkleinern (Hashen),
+das wird verdaut, nebenbei noch gewürzt (Salt) und kommt als Digest wieder heraus.
+Die Länge der Ausgabe ist fest.
+
+** Ein wichtiger Punkt
+
+Wichtig ist auch zu verstehen, dass Hashes sogenannte "One-Way-Funktionen" sind.
+Das heißt, man kann das Hashing nicht einfach umkehren, um das ursprüngliche Objekt wiederherzustellen.
+Ein Hash ist also eine Einbahnstraße: Er dient nur dazu, die Daten zu repräsentieren,
+aber man kann nicht vom Hash auf die originalen Daten zurückrechnen.
+Aus Kacke kann man kein Essen zaubern.
+
+** Was ist kein Hash?
+
+Die ISBN identifiziert ein Buch eindeutig. Sie ist nicht das Buch selbst, sondern eher ein
+Identifier (eine geordnete Nummer mit Prüfziffer). Sie ist aber kein Hash, weil sie nicht
+aus dem Inhalt des Buches berechnet wird. Ähnliches gilt für die IBAN
+
+|-------------------------+---------------------------+-------------------------------------|
+| Merkmal                 | Hash                      | Identifier (z. B. ISBN, IBAN, UUID) |
+|-------------------------+---------------------------+-------------------------------------|
+| Abhängig vom Inhalt     | ja                        | nein                                |
+| Ziel                    | Integrität / Vergleich    | Identität / Referenz                |
+| Länge                   | fix z. B. 32 Hex-Zeichen  | frei definierbar                    |
+| Zufällig oder berechnet | deterministisch berechnet | oft generiert oder vergeben         |
+|-------------------------+---------------------------+-------------------------------------|
+
+** Warum verwenden wir Hashes?
+
+Hashes sind super praktisch, weil sie uns eine Menge Zeit und Rechenleistung sparen.
+Anstatt zwei große Dateien oder ganze Bücher direkt miteinander zu vergleichen,
+vergleichen wir einfach ihre Hashes. Wenn die Hashes gleich sind, wissen wir,
+dass die Daten sehr wahrscheinlich identisch sind. Wenn sie unterschiedlich sind,
+hat sich etwas geändert. Das spart enorm viel Aufwand.
+
+** Beispiele für den Einsatz von Hashes
+
+- **Passwort-Hashes**: Wenn du ein Passwort eingibst, wird oft nur der Hash gespeichert, nicht das
+   eigentliche Passwort. So bleibt es sicher, selbst wenn jemand die Datenbank stiehlt.
+
+   Beispiel aus der /etc/shadow-Datei
+
+root:$6$tsicHaoV3Q$YtAbiIvrHGXFtAJYz9tcEYWHXiGVQ40sJAgzPAbc57lIq9jH8eYjWXwctSW6YQnrMznRFcm6yXLnnY9mHhso20
+enno:$6$sdygzfEgx0$YpaZJMQdkZgxGPclphz6RojqNG.PSNEq1oIHRP4kvZRN2iuS5MQrxt0nCkrYQIcpDGyohrb1o0S/GkWrFriWL1
+
+Der Hash ist länger als das Passwort. Das dient der Sicherheit, weil man zwar nicht zurückrechnen,
+wohl aber vorwärtsrechnen kann. Hash-Funktionen sind deterministisch.
+Bei Linux-Passworteinträgen hat man hier $6$ den Hashalgorithmus kodiert, im daraufolgendem $.....$ das Salz
+und im Rest den eigentlichen Hash kodiert
+
+- **Git-Commits**: In Versionskontrollsystemen wie Git werden Hashes benutzt, um jeden Schwung an
+  Änderungen mit einer eindeutigen Kennung zu versehen
+
+- **URL-Shortener: hash("https://example.com") → 8a3f12
+  
+** FlexOID
+
+Die FlexOID ist eine Mischform. Sie selbst ist ein Identifier, der als Bestandteil aber einen Hash
+enthält, um möglichst eindeutig, aber nicht zu lang zu sein.
+
+AF-I251022-70F759@001D
+│  │ │       │     │ │
+│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
+│  │ │       │     └──── Version
+│  │ │       └────────── Hash (6 bytes, 12 Stellen)
+│  │ └────────────────── Date (YYMMDD)
+│  └──────────────────── Entity type (ITEM)
+└─────────────────────── Domain (e.g. Air force)
+
+In der ersten Variante der FlexOID habe ich mich mit einem 6-stelligen Hash zufrieden gegeben,
+weil das einfach lesbarer ist. Warum reicht das nicht?
+
+** Das Geburtstags-Paradoxon
+
+Der obige 3-Byte Hash liefert mir etwas über 16 Millionen unterschiedlich Varianten.
+Das klingt viel. Ist es aber nicht. Wieviele Schüler müssen nacheinander die Klasse
+betreten bis sich zwei finden, die mit mehr als 50 Prozent Wahrscheinlichkeit am gleichen
+Tag Geburtstag (also ein identisches Merkmal) haben?
+
+** Lösung
+
+Ab 23 Schülern beträgt die Wahrscheinlichkeit 50 % (näherungsweise Wurzel 365 Tagen)
+
+Bei 47 Schülern beträgt die Wahrscheinlichkeit bereits 95 Prozent
+
+Unsere 3 Bytes ergeben zwar 16.000.000 mögliche Varianten - im Gegenzug zu den 365 Tagen im Jahr
+aus dem Geburtstagsbeispiel - aber da die Wahrscheinlichkeit zur Kollision hier bei
+etwa Wurzel 16 Mio. liegt, bekommt man bereits bei 4000 Neuzugängen die ersten
+Übereinstimmungen im Hash.
+
+Der Hash ist also nicht gut genug, weil man sehr schnell und sehr häufig, diese
+Übereinstimmungen feststellen und behandeln müsste, wenn man weiterhin eindeutige
+Zuordnungen treffen will.
+
+Wenn man die Ausgabe der Hashfunktion auf 6 Bytes erweitert, kommen die ersten Kollisionen
+erst bei etwa 20 Mio erzeugten Hashes (Fingerabdrücken) und die kann man dann ohne
+Einbußen gesondert behandeln (Salzen), weil es so selten passiert.
+
+Übrigens, wenn man beim Menschen den Daumenabdruck nimmt und sich dabei auf 12 Merkmale
+beschränkt und sehr lockere Toleranzen ansetzt (was man in der Praxis nicht macht),
+hat man bereits nach 14000 Menschen eine Übereinstimmung. Bei 24 Merkmalen und sehr
+lockeren Toleranzen, hat man bei etwa nach 9 Mio. Menschen eine ungefähre Übereinstimmung.
+Da muss die Polizei schon sehr schlampig arbeiten, damit man fälschlicherweise beschuldigt wird.
+Die Zahlen in der Realität sind sogar noch deutlich höher.
+
+** FlexoEntity
+
+Nun haben wir gesehen, dass wir mit der FlexOID (mit 6-Byte Hash) sehr viele unterschiedliche
+Dinge eindeutig bestimmen können. Da unsere FlexOID erstmal nur eine Zeichenfolge ist,
+brauchen wir etwas das damit umgehen kann und was dafür verantwortlich ist. Das ist die FlexoEntity.
+
+Sie beinhaltet zusätzlich ein Origin-Feld, wo festgehalten wird, woher diese
+Entität stammt (beispielsweise aus einer Hashkollision oder einer Änderung an den weiteren Daten)
+
+Jede Klasse, die von FlexoEntity erbt, muss zwingend die Methode "text_seed" implementieren,
+mit der der Algorithmus einer Hash-Funktion gefüttert wird, aus der dann der 6-Byte Hash herauspurzelt.
+Hashfunktionen sind z.B. MD5, SHA1, SHA256 oder wie von mir genutzt: Blake2s.
+Die Mathematik dahinter ist recht aufwändig, aber wer sich mal einlesen möchte
+
+- Hashing in Smalltalk: Theory and Practice von Andres Valloud
+
+Damit die Hash-Funktion genug Eingabedaten pro Entity hat, muss man sich überlegen, welche Merkmale
+einer Entität man durch "text_seed" übermittelt.
+
+** text_seed
+
+Man kann beliebige Klassen von FlexoEntity ableiten und erhält ohne Aufwand die Funktionalität
+zur eindeutigen Identifizierung und zur Lebenszyklus-Verwaltung
+
+Das ist das Beispiel einer ChoiceQuestion, also einer Testfrage, wo mögliche Antworten enthalten sind
+
+    @property
+    def text_seed(self) -> str:
+        """Include answer options (and points) for deterministic ID generation."""
+        base = super().text_seed
+        if not self.options:
+            return base
+
+        joined = "|".join(
+            f"{opt.text.strip()}:{opt.points}"
+            for opt in sorted(self.options, key=lambda o: o.text.strip().lower())
+        )
+        return f"{base}::{joined}"
+
+** Lebenszyklus
+
+Der Lebenszyklus einer Entität folgt dieser Reihenfolge und ist nicht umkehrbar
+
+- Entwurf (DRAFT)
+- Genehmigt (APPROVED)
+- Unterschrieben (APPROVED_AND_SIGNED)
+- Veröffentlicht (PUBLISHED)
+- Veraltet (OBSOLETE)
+
+Eine Entität, die bereits die Stufe Veröffentlicht erreicht hat, kann nicht in die Stufe
+(nur) Unterschrieben zurückkehren. Daher ist auch die Lebenszyklusstufe in der ID kodiert
+(letztes Symbol der FlexOID)
+
+Beispiele für Entitäten:
+
+- Testfrage
+- Fragenkatalog
+- Einstufungstest
+- Zertifikat
+
+Ein veröffentlichter Einstufungstest kann nur Fragen beinhalten, die ihrerseits die Stufe Veröffentlicht
+erreicht haben. Ein Zertifikat kann nur ausgestellt werden, wenn der passende Einstufungstest die Stufe
+Veröffentlicht hat. Das origin-Feld des Zertifikats sollte sinnvollerweise die ID des Tests enthalten.
+
+** Erhöhung der Versionsnummer oder neue ID
+
+Sobald - aus Gründen - eine neue ID vergeben werden muss, wird ggf. die Ursprungs-ID
+im Feld origin der neuen FlexoEntity gespeichert. So ist immer ein Suchen im Stammbaum möglich.
+
+AF-Q251022-70F759@001D
+│  │ │       │     │ │
+│  │ │       │     │ └── State (Draft, Approved, Signed, Published, Obsolete)
+│  │ │       │     └──── Version
+│  │ │       └────────── Hash (3 bytes, 6 Stellen)
+│  │ └────────────────── Date (YYMMDD)
+│  └──────────────────── Entity type (Question)
+└─────────────────────── Domain (e.g. Air force)
+
+
+Eine einfache Erhöhung der Versionsnummer (am Ende der ID) ist unter Umständen auch ausreichend,
+damit nicht zu häufig komplett neue IDs erzeugt werden müssen. Der Entscheidungsbaum dazu ist
+für das Projekt FlexoGrader recht umfangreich und soll hier nicht weiter besprochen werden.
+Da es aber im Hintergrund passiert und vom Endanwender nicht bemerkt wird, behindert der
+Mechanismus die Nutzung des FlexoGrader nicht. 
+
+** Reifegrad
+
+Das Konzept und Design hat die Alpha-Phase verlassen, der Code ist aber noch deutlich Beta.
+RC1 etwa Anfang Dezember (nach meinem Urlaub). Änderungen an der FlexOID sind nicht mehr zu erwarten, beim API der FlexoEntity schon eher.
+
+Das Perfekte ist der Feind des Guten, aber ...
+
+Als Gegenbeispiel für unausgereiftes Design ist Pythons eingebaute datetime Bibliothek.
+Da könnte man ein Buch drüber schreiben. Halb Drama, halb Komödie.
+
+- dateutil, arrow, pendulum, maya, moment, delorean, pytz, zoneinfo + numpy/pandas
+  
+** Lizenz
+
+Da ich die FlexoEntity-Bibliothek in meiner Freizeit entworfen und programmiert habe,
+bin ich alleiniger Urheber. Als Lizenz habe ich MIT gewählt, somit kann - unter Nennung
+des Urhebers - jeder damit machen, was er möchte
+
+Der FlexoGrader ist ein Bundeswehrprojekt, welches ich im Dienst programmiere und somit
+gehen die Nutzungsrechte uneingeschränkt auf den Dienstherren über.
+
+Das betrifft auch etwaige Folgeprojekte wie FlexoVault und FlexoDrill
Index: org/README.org
===================================================================
--- org/README.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ org/README.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,291 @@
+#+TITLE: flexoentity
+#+SUBTITLE: A hardened entity base and deterministic identifier system for the Flex-O family
+#+AUTHOR: Enno
+#+DATE: 2025-10-20
+#+OPTIONS: toc:3 num:nil
+
+* 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()=.
+
+#+BEGIN_EXAMPLE
+AF-I250101-9A4C2D@003S
+#+END_EXAMPLE
+
+|-----------+-----------------+----------------------------------------------|
+| 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       |
+| *APPROVED_AND_SIGNED* | =S=     | Cryptographically signed    |
+| *PUBLISHED*           | =P=     | Released to consumers       |
+| *OBSOLETE*            | =O=     | Archived or replaced        |
+|-----------------------|---------|-----------------------------|
+
+Transitions follow a strict progression:
+#+BEGIN_EXAMPLE
+DRAFT -> APPROVED -> APPROVED_AND_SIGNED -> PUBLISHED -> OBSOLETE
+#+END_EXAMPLE
+
+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*
+- safe_generate(domain, entity_type, estate, text, version=1, repo) -> create a new ID
+- next_version(oid) -> increment version safely
+- clone_new_base(domain, entity_type, 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 (to_json(), from_json(), to_dict(), from_dict())
+- Integrity verification (verify_integrity(entity))
+- Controlled state transitions with automatic timestamps
+
+Subclasses define a single property:
+
+#+BEGIN_SRC python
+@property
+def text_seed(self) -> str:
+    """Canonical text or core content for hashing."""
+#+END_SRC
+
+* Integrity Verification
+
+Each entity can self-verify its integrity:
+
+#+BEGIN_SRC python
+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)
+#+END_SRC
+
+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 ENTITY_TYPE and define the needed fields in the data class. We define how to create
+a default object, the text_seed (it is easy because the domain id is unique and therefore sufficient)
+and the methods for serialization.
+
+#+BEGIN_SRC python
+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
+#+END_SRC
+        
+* Usage
+#+BEGIN_SRC python
+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
+#+END_SRC
+
+* Serialization Example
+
+#+BEGIN_SRC python 
+{
+    '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'}
+#+END_SRC
+
+#+BEGIN_SRC js
+
+{
+        "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          |
+| APPROVED_AND_SIGNED | 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.
Index: org/certificates.org
===================================================================
--- org/certificates.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
+++ org/certificates.org	(revision d7499cad8c700086cf682c7c11caf3a253c4fcc6)
@@ -0,0 +1,210 @@
+* Flex-O Cross-Platform Signing System
+
+This document defines how Flex-O performs cryptographic signing across Linux, Windows, and macOS using a single interoperable workflow.
+
+** Requirements
+
+- PKCS#7 / CMS signatures
+- RSA-4096
+- DER output format (Windows/macOS native)
+- No Python crypto libraries
+- OS-native signing tooling
+
+* 1. Key and Certificate Creation (Universal)
+
+** 1.1 Generate a private key (recommended: RSA-4096)
+
+#+begin_src bash
+openssl genrsa -out mykey.pem 4096
+#+end_src
+
+** 1.2 Generate an X.509 certificate
+
+#+begin_src bash
+openssl req -new -x509 -key mykey.pem -out mycert.pem -days 3650 -sha256
+#+end_src
+
+Result:
+- mykey.pem → private key
+- mycert.pem → certificate (public key)
+
+* 2. Import Certificates into Each Platform
+
+** 2.1 Linux
+
+No import required. Use PEM files directly.
+
+** 2.2 Windows (PKCS#12 required)
+
+Create PKCS#12 bundle:
+
+#+begin_src bash
+openssl pkcs12 -export -out flexo.pfx -inkey mykey.pem -in mycert.pem
+#+end_src
+
+Import into user certificate store:
+
+#+begin_src bash
+certutil -user -p PASSWORD -importpfx flexo.pfx
+#+end_src
+
+** 2.3 macOS (PKCS#12)
+
+#+begin_src bash
+openssl pkcs12 -export -out flexo.p12 -inkey mykey.pem -in mycert.pem
+#+end_src
+
+Import:
+
+#+begin_src bash
+security import flexo.p12 -k ~/Library/Keychains/login.keychain-db
+#+end_src
+
+
+* 3. Cross-Platform Signing Commands
+
+** 3.1 Linux (OpenSSL CMS / DER)
+
+#+begin_src bash
+openssl cms -sign \
+    -binary \
+    -in data.txt \
+    -signer mycert.pem \
+    -inkey mykey.pem \
+    -outform DER \
+    -out signature.p7s
+#+end_src
+
+** 3.2 Windows (certutil)
+
+#+begin_src bash
+certutil -sign data.txt signature.p7s
+#+end_src
+
+** 3.3 macOS (`security cms`)
+
+#+begin_src bash
+security cms -S \
+    -N "Common Name of Cert" \
+    -i data.txt \
+    -o signature.p7s
+#+end_src
+
+All three platforms produce binary DER PKCS#7 signatures.
+
+* 4. Cross-Platform Verification
+
+** 4.1 Linux (OpenSSL)
+
+#+begin_src bash
+openssl cms -verify \
+    -in signature.p7s \
+    -inform DER \
+    -content data.txt \
+    -CAfile mycert.pem \
+    -purpose any \
+    -out /dev/null
+#+end_src
+
+** 4.2 Windows
+
+#+begin_src bash
+certutil -verify signature.p7s data.txt
+#+end_src
+
+** 4.3 macOS
+
+#+begin_src bash
+security cms -D -i signature.p7s > verified.txt
+#+end_src
+
+* 5. Flex-O Signing Specification
+
+** 5.1 Key Requirements
+- RSA-4096
+- X.509 certificate
+- Valid for ≥ 10 years
+
+** 5.2 Signature Format Requirements
+- PKCS#7/CMS
+- Binary DER form
+- Signing certificate must be embedded
+
+** 5.3 Verification Requirements
+- Must work with OpenSSL CMS
+- No dependency on OS certificate stores
+- Must accept DER PKCS#7 signatures
+
+** 5.4 Flex-O Signature Entity Schema
+Required fields:
+- signed_entity: FlexOID
+- signer_id: UUID
+- signature_data: base64(PKCS7 DER blob)
+- signature_type: "PKCS7-DER"
+- certificate_thumbprint: SHA-1 thumbprint
+- comment: optional
+
+** 5.5 Security Assumptions
+- Flex-O never stores private keys
+- OS handles private key protection
+- Only public certificates embedded in signatures
+
+* 6. Refer to certificates 
+
+** Linux
+
+#+BEGIN_SRC python
+cert_ref = CertificateReference(
+    platform="LINUX",
+    identifier="/etc/flexo/certs/mycert.pem",
+    private_key_path="$HOME/.flexo/mykey.pem",
+    public_cert_path="/etc/flexo/certs/mycert.pem",
+)
+backend = create_backend(cert_ref)
+signature_bytes = backend.sign(b"hello world")
+#+END_SRC
+
+
+** Windows
+
+#+BEGIN_SRC python
+cert_ref = CertificateReference(
+    platform="WINDOWS",
+    identifier="E1A2B3C4D5F6A7B8C9D0E1F2A3B4C5D6E7F8A9B",   # Thumbprint
+)
+
+backend = create_backend(cert_ref)
+signature_bytes = backend.sign(b"hello world")
+#+END_SRC
+
+** MacOS
+
+#+BEGIN_SRC python
+cert_ref = CertificateReference(
+    platform="MACOS",
+    identifier="FlexOSigner", # Common Name (CN)
+    public_cert_path="/Users/enno/certs/FlexOSigner.pem"
+)
+
+backend = create_backend(cert_ref)
+signature_bytes = backend.sign(b"hello world")
+#+END_SRC
+
+* 8. Flex-O Signature Entity
+
+#+begin_src python
+@dataclass
+class Signature(FlexoEntity):
+    ENTITY_TYPE = EntityType.OUTPUT
+
+    signed_entity: Optional[FlexOID] = None
+    signer_id: Optional[UUID] = None
+    signature_data: str = ""          # Base64 of PKCS#7 DER
+    signature_type: str = "PKCS7-DER"
+    certificate_thumbprint: str = ""
+    comment: Optional[str] = None
+
+    @property
+    def text_seed(self) -> str:
+        return f"{self.signed_entity}:{self.signer_id}:{self.certificate_thumbprint}"
+#+end_src
