Index: tests/conftest.py
===================================================================
--- tests/conftest.py	(revision ca3927462b83d9ae55fd59d487c07e1a3f0f994c)
+++ tests/conftest.py	(revision 8aa20c772729e2042bc331b986df06947d18fcdd)
@@ -1,64 +1,93 @@
-# tests/conftest.py
-
+# tests/stubs/single_choice_question.py
 import pytest
-import json
+from datetime import datetime
+from dataclasses import dataclass, field
+from typing import List
 from flexoentity import FlexoEntity, EntityType, EntityState, Domain
 
-import pytest
-import json
-from flexoentity import EntityType, EntityState, Domain
-from builder.questions import RadioQuestion, AnswerOption  # adjust path if different
-from builder.media_items import NullMediaItem  # adjust import if needed
+@pytest.fixture
+def fixed_datetime(monkeypatch):
+    class FixedDate(datetime):
+        @classmethod
+        def now(cls, tz=None):
+            return datetime(2025, 11, 1, tzinfo=tz)
+    monkeypatch.setattr("flexoentity.id_factory.datetime", FixedDate)
+    return FixedDate
 
 
-@pytest.fixture(scope="session")
-def domain():
-    """Provide a reusable domain for all entity tests."""
-    return Domain(
-        domain="SIG",
-        etype=EntityType.DOMAIN,
-        state=EntityState.DRAFT,
-        fullname="Signal Corps",
-        description="Questions related to communications and signaling systems.",
-        classification="RESTRICTED",
-        owner="test-suite"
-    )
+@dataclass
+class AnswerOption:
+    id: str
+    text: str
+    points: float = 0.0
+
+    def to_dict(self):
+        return {"id": self.id, "text": self.text, "points": self.points}
+
+    @classmethod
+    def from_dict(cls, data):
+        return cls(
+            id=data.get("id", ""),
+            text=data.get("text", ""),
+            points=data.get("points", 0.0)
+        )
 
 
-@pytest.fixture
-def radio_question(domain):
-    """Return a simple RadioQuestion entity for testing FlexoEntity logic."""
-    q = RadioQuestion(
-        domain=domain,
-        etype=EntityType.QUESTION,
-        state=EntityState.DRAFT,
-        text="Which frequency band is used for shortwave communication?",
-        options=[
-            AnswerOption(id="opt1", text="HF (3–30 MHz)", points=1),
-            AnswerOption(id="opt2", text="VHF (30–300 MHz)", points=0),
-            AnswerOption(id="opt3", text="UHF (300–3000 MHz)", points=0),
-        ]
-    )
-    return q
+@dataclass
+class SingleChoiceQuestion(FlexoEntity):
+    """A minimal stub to test FlexoEntity integration."""
+    text: str = ""
+    options: List[AnswerOption] = field(default_factory=list)
 
 
-@pytest.fixture
-def serialized_question(radio_question):
-    """Provide the serialized JSON form for roundtrip tests."""
-    return radio_question.to_json()
+    @classmethod
+    def default(cls):
+        return cls(domain=Domain(domain="GEN",
+                                 entity_type=EntityType.DOMAIN,
+                                 state=EntityState.DRAFT),
+                   state=EntityState.DRAFT, entity_type=EntityType.ITEM)
 
+    def to_dict(self):
+        base = super().to_dict()
+        base.update({
+            "text": self.text,
+            "options": [opt.to_dict() for opt in self.options],
+        })
+        return base
+
+    @property
+    def text_seed(self) -> str:
+        """Include answer options (and points) for deterministic ID generation."""
+
+        joined = "|".join(
+            f"{opt.text.strip()}:{opt.points}"
+            for opt in sorted(self.options, key=lambda o: o.text.strip().lower())
+        )
+        return f"{self.text}{joined}"
+
+    @classmethod
+    def from_dict(cls, data):
+        obj = cls(
+            text=data.get("text", ""),
+            options=[AnswerOption.from_dict(o) for o in data.get("options", [])],
+        )
+        # restore FlexoEntity core fields
+        obj.domain = data.get("domain")
+        obj.entity_type = EntityType[data.get("etype")] if "etype" in data else EntityType.ITEM
+        obj.state = EntityState[data.get("state")] if "state" in data else EntityState.DRAFT
+        if "flexo_id" in data:
+            from flexoentity import FlexOID
+            obj.flexo_id = FlexOID.parsed(data["flexo_id"])
+        return obj
 
 @pytest.fixture
-def deserialized_question(serialized_question):
-    """Recreate a question from JSON for consistency tests."""
-    return RadioQuestion.from_json(serialized_question)
-
+def domain():
+    return Domain.default()
 
 @pytest.fixture
-def null_media():
-    """Provide a default NullMediaItem instance for media tests."""
-    return NullMediaItem(
-        domain=domain,
-        etype=EntityType.MEDIA,
-        state=EntityState.DRAFT
-    )
+def sample_question():
+    return SingleChoiceQuestion(domain=Domain.default(),
+                               text="What is 2 + 2?",
+                               options=[],
+                               entity_type=EntityType.ITEM,
+                               state=EntityState.DRAFT)
Index: tests/test_flexoid.py
===================================================================
--- tests/test_flexoid.py	(revision 8aa20c772729e2042bc331b986df06947d18fcdd)
+++ tests/test_flexoid.py	(revision 8aa20c772729e2042bc331b986df06947d18fcdd)
@@ -0,0 +1,148 @@
+#!/usr/bin/env python3
+
+# tests/test_flexoid.py
+"""
+Test suite for id_factory.FlexOID.
+
+I verify that FlexOID behaves deterministically, validates itself strictly,
+and evolves correctly through version and state transitions.
+"""
+
+import re
+from datetime import datetime, date
+import pytest
+from logging import Logger
+from flexoentity import FlexOID, canonical_seed
+
+
+logger = Logger(__file__)
+
+# ──────────────────────────────────────────────
+# Basic construction and validation
+# ──────────────────────────────────────────────
+
+def test_valid_flexoid_parsing():
+    fid = FlexOID("GEN-I251101-ABCDEF123456@001D")
+    assert isinstance(fid, str)
+    assert fid.domain == "GEN"
+    assert fid.entity_type == "I"
+    assert fid.date_str == "251101"
+    assert fid.version == 1
+    assert fid.state_code == "D"
+    assert fid.prefix.endswith("ABCDEF123456")
+    assert str(fid).endswith("@001D")
+
+
+def test_invalid_flexoid_raises():
+    with pytest.raises(ValueError):
+        FlexOID("INVALIDFORMAT")
+    with pytest.raises(ValueError):
+        FlexOID("GEN-I251101-ABCDEF@1D")  # bad version width
+
+
+def test_regex_is_strict():
+    pat = FlexOID.OID_PATTERN
+    assert re.match(pat, "GEN-I251101-123456ABCDEF@001A")
+    assert not re.match(pat, "gen-item251101-123456@001A")  # lowercase not allowed
+    assert not re.match(pat, "GEN-I251101-123456@01A")   # wrong digit count
+
+
+# ──────────────────────────────────────────────
+# Generation and deterministic hashing
+# ──────────────────────────────────────────────
+
+def test_generate_and_hash_stability(fixed_datetime):
+    # Fix the date so test is stable
+    fid1 = FlexOID.generate("GEN", "I", "D", "test content")
+    fid2 = FlexOID.generate("GEN", "I", "D", "test content")
+    assert fid1 == fid2  # deterministic
+    assert fid1.hash_part == fid2.hash_part
+    assert fid1.domain == "GEN"
+    assert fid1.entity_type == "I"
+
+
+def test_safe_generate_collision(monkeypatch):
+    # Fake repo that always returns a conflicting item with different seed
+    class DummyRepo(dict):
+        def get(self, key): return True
+
+    repo = DummyRepo()
+    fid = FlexOID.safe_generate("GEN", "I", "D", "abc", repo=repo)
+    assert isinstance(fid, FlexOID)
+    assert fid.state_code == "D"
+
+
+# ──────────────────────────────────────────────
+# State and version transitions
+# ──────────────────────────────────────────────
+
+def test_with_state_creates_new_instance():
+    fid = FlexOID("GEN-I251101-ABCDEF123456@001D")
+    fid2 = fid.with_state("A")
+    assert fid != fid2
+    assert fid.version == fid2.version
+    assert fid2.state_code == "A"
+    assert str(fid2).endswith("@001A")
+
+
+def test_next_version_increments_properly():
+    fid = FlexOID("GEN-I251101-ABCDEF123456@001A")
+    fid2 = FlexOID.next_version(fid)
+    assert fid2.version == 2
+    assert fid2.state_code == fid.state_code
+    assert fid.prefix == fid2.prefix
+
+
+def test_from_oid_and_version_sets_exact_version():
+    fid = FlexOID("GEN-I251101-ABCDEF123456@001A")
+    fid2 = FlexOID.from_oid_and_version(fid, 10)
+    assert fid2.version == 10
+    assert fid2.state_code == "A"
+
+
+def test_clone_new_base_starts_at_one(fixed_datetime):
+    fid = FlexOID.clone_new_base("GEN", "I", "D", "clone text")
+    assert fid.version == 1
+    assert fid.state_code == "D"
+
+
+# ──────────────────────────────────────────────
+# Canonical seed behavior
+# ──────────────────────────────────────────────
+
+def test_canonical_seed_for_strings_and_dicts():
+    s1 = "  Hello   world "
+    s2 = "Hello world"
+    assert canonical_seed(s1) == canonical_seed(s2)
+
+    d1 = {"b": 1, "a": 2}
+    d2 = {"a": 2, "b": 1}
+    assert canonical_seed(d1) == canonical_seed(d2)
+
+    class Dummy:
+        def __init__(self):
+            self.x = 1
+            self.y = 2
+    obj = Dummy()
+    assert isinstance(canonical_seed(obj), str)
+
+
+# ──────────────────────────────────────────────
+# Parsed structure and representations
+# ──────────────────────────────────────────────
+
+def test_parsed_returns_expected_dict():
+    fid = FlexOID("GEN-I251101-ABCDEF123456@007S")
+    data = fid.parsed()
+    assert data["domain"] == "GEN"
+    assert data["entity_type"] == "I"
+    assert data["version"] == 7
+    assert data["state"] == "S"
+    assert isinstance(data["date"], date)
+
+
+def test_repr_is_readable():
+    fid = FlexOID("GEN-I251101-ABCDEF123456@001D")
+    s = repr(fid)
+    assert "FlexOID" in s
+    assert "GEN-I" in s
Index: tests/test_id_lifecycle.py
===================================================================
--- tests/test_id_lifecycle.py	(revision ca3927462b83d9ae55fd59d487c07e1a3f0f994c)
+++ tests/test_id_lifecycle.py	(revision 8aa20c772729e2042bc331b986df06947d18fcdd)
@@ -1,12 +1,12 @@
 import pytest
-from flexoentity import FlexOID, FlexoEntity, EntityType, EntityState
+from flexoentity import FlexOID, FlexoEntity, EntityState
 
 
 # ──────────────────────────────────────────────────────────────────────────────
-# Tests adapted to use real RadioQuestion fixture instead of DummyEntity
+# Tests adapted to use real SingleChoiceQuestion fixture instead of DummyEntity
 # ──────────────────────────────────────────────────────────────────────────────
 
-def test_initial_state(radio_question):
-    q = radio_question
+def test_initial_state(sample_question):
+    q = sample_question
     assert q.state == EntityState.DRAFT
     assert q.flexo_id.version == 1
@@ -14,13 +14,13 @@
 
 
-def test_approval_bumps_version(radio_question):
-    q = radio_question
+def test_approval_does_not_bump_version(sample_question):
+    q = sample_question
     q.approve()
     assert q.state == EntityState.APPROVED
-    assert q.flexo_id.version == 2
+    assert q.flexo_id.version == 1
 
 
-def test_signing_bumps_version(radio_question):
-    q = radio_question
+def test_signing_bumps_version(sample_question):
+    q = sample_question
     q.approve()
     v_before = str(q.flexo_id)
@@ -30,6 +30,6 @@
 
 
-def test_publish_bumps_version(radio_question):
-    q = radio_question
+def test_publish_bumps_version(sample_question):
+    q = sample_question
     q.approve()
     q.sign()
@@ -40,13 +40,13 @@
 
 
-def test_modify_content_changes_fingerprint(radio_question):
-    q = radio_question
-    q.text = "Rephrased content"  # simulate text change
+def test_modify_content_changes_fingerprint(sample_question):
+    q = sample_question
+    q.text += "Rephrased content"  # simulate text change
     changed = q._update_fingerprint()
     assert changed
 
 
-def test_no_version_bump_on_draft_edits(radio_question):
-    q = radio_question
+def test_no_version_bump_on_draft_edits(sample_question):
+    q = sample_question
     q.text = "Minor draft edit"
     q._update_fingerprint()
@@ -54,6 +54,6 @@
 
 
-def test_version_bump_after_edit_and_sign(radio_question):
-    q = radio_question
+def test_version_bump_after_edit_and_sign(sample_question):
+    q = sample_question
     q.approve()
     v1 = str(q.flexo_id)
@@ -63,6 +63,6 @@
 
 
-def test_integrity_check_passes_and_fails(radio_question):
-    q = radio_question
+def test_integrity_check_passes_and_fails(sample_question):
+    q = sample_question
     q.approve()
     assert FlexoEntity.verify_integrity(q)
@@ -73,6 +73,6 @@
 
 
-def test_obsolete_state(radio_question):
-    q = radio_question
+def test_obsolete_state(sample_question):
+    q = sample_question
     q.approve()
     q.sign()
@@ -82,6 +82,6 @@
 
 
-def test_clone_new_base_resets_lineage(radio_question):
-    q = radio_question
+def test_clone_new_base_resets_lineage(sample_question):
+    q = sample_question
     q.approve()
     q.sign()
@@ -94,6 +94,6 @@
     assert q.flexo_id.version == 1
 
-def test_clone_new_base_sets_origin(radio_question):
-    q = radio_question
+def test_clone_new_base_sets_origin(sample_question):
+    q = sample_question
     q.approve()
     q.sign()
@@ -107,8 +107,8 @@
     assert q.flexo_id != old_id
 
-def test_mass_version_increments_until_obsolete(radio_question):
-    q = radio_question
+def test_mass_version_increments_until_obsolete(sample_question):
+    q = sample_question
     q.approve()
-    for _ in range(FlexOID.MAX_VERSION - 2):
+    for _ in range(FlexOID.MAX_VERSION - 1):
         q.bump_version()
     with pytest.raises(RuntimeError, match="mark obsolete"):
Index: tests/test_id_stress.py
===================================================================
--- tests/test_id_stress.py	(revision ca3927462b83d9ae55fd59d487c07e1a3f0f994c)
+++ tests/test_id_stress.py	(revision 8aa20c772729e2042bc331b986df06947d18fcdd)
@@ -4,9 +4,11 @@
 """
 
+import copy
+import logging
+import random
+
 import pytest
-import random
-import logging
+
 from flexoentity import FlexOID, EntityType, EntityState
-from builder.questions import RadioQuestion, AnswerOption
 
 logger = logging.getLogger(__name__)
@@ -18,5 +20,5 @@
     via salt + date adjustment.
     """
-    etype = EntityType.QUESTION
+    entity_type = EntityType.ITEM
     estate = EntityState.DRAFT
     seeds = [f"question {i}" for i in range(100000)]
@@ -31,5 +33,5 @@
     ids = []
     for seed in seeds:
-        oid = FlexOID.safe_generate(domain.domain, etype, estate, seed, repo=repo)
+        oid = FlexOID.safe_generate(domain.domain, entity_type.value, estate.value, seed, repo=repo)
         assert isinstance(oid, FlexOID)
         ids.append(str(oid))
@@ -46,5 +48,5 @@
 
     # Sanity check: IDs should look canonical
-    assert all(id_str.startswith("SIG-") for id_str in ids)
+    assert all(id_str.startswith("GEN") for id_str in ids)
     assert all("@" in id_str for id_str in ids)
 
@@ -54,33 +56,33 @@
     (No runtime disambiguation; IDs are deterministic by design.)
     """
-    etype = EntityType.QUESTION
+    entity_type = EntityType.ITEM
     estate = EntityState.DRAFT
     text = "identical question text"
 
-    id1 = FlexOID.generate(domain.domain, etype, estate, text)
-    id2 = FlexOID.generate(domain.domain, etype, estate, text)
+    id1 = FlexOID.generate(domain.domain, entity_type.value, estate.value, text)
+    id2 = FlexOID.generate(domain.domain, entity_type.value, estate.value, text)
     # IDs must be identical because generation is deterministic
     assert id1 == id2
 
 
-def test_id_reproducibility_across_runs(domain):
-    """
-    The same seed on a new process (fresh _seen_hashes)
-    should yield the same base ID (without suffix).
-    """
-    etype = EntityType.CATALOG
-    estate = EntityState.DRAFT
-    seed = "reproducibility test seed"
+# def test_id_reproducibility_across_runs(domain):
+#     """
+#     The same seed on a new process (fresh _seen_hashes)
+#     should yield the same base ID (without suffix).
+#     """
+#     entity_type = EntityType.CATALOG
+#     estate = EntityState.DRAFT
+#     seed = "reproducibility test seed"
 
-    id1 = FlexOID.generate(domain.domain, etype, estate, seed)
-    FlexOID._seen_hashes.clear()
-    id2 = FlexOID.generate(domain.domain, etype, estate, seed)
+#     id1 = FlexOID.generate(domain.domain, entity_type.value, estate.value, seed)
+#     FlexOID._seen_hashes.clear()
+#     id2 = FlexOID.generate(domain.domain, entity_type.value, estate.value, seed)
 
-    assert id1 == id2
+#     assert id1 == id2
 
 
-def test_version_ceiling_enforcement(radio_question):
+def test_version_ceiling_enforcement(sample_question):
     """Simulate approaching @999 to trigger obsolescence guard."""
-    q = radio_question
+    q = sample_question
     q.approve()
 
@@ -97,26 +99,16 @@
 
 
-def test_massive_lifecycle_simulation(domain):
+def test_massive_lifecycle_simulation(sample_question):
     """
-    Generate 100 random RadioQuestions, simulate multiple edits and state transitions,
+    Generate 100 random SingleChoiceQuestions, simulate multiple edits and state transitions,
     ensure all final IDs and fingerprints are unique and valid.
     """
     entities = [
-        RadioQuestion(
-            domain=domain,
-            etype=EntityType.QUESTION,
-            state=EntityState.DRAFT,
-            text=f"random question {i}",
-            options=[
-                AnswerOption(id="opt4", text="HF (3–30 MHz)", points=1),
-                AnswerOption(id="opt5", text="VHF (30–300 MHz)", points=0),
-            ],
-        )
-        for i in range(100)
+        copy.deepcopy(sample_question) for _ in range(100)
     ]
 
-    for e in entities:
+    for i, e in enumerate(entities):
         # random edit
-        e.text += " updated"
+        e.text += f" updated #{i}"
         e._update_fingerprint()
 
Index: tests/test_persistance_integrity.py
===================================================================
--- tests/test_persistance_integrity.py	(revision ca3927462b83d9ae55fd59d487c07e1a3f0f994c)
+++ tests/test_persistance_integrity.py	(revision 8aa20c772729e2042bc331b986df06947d18fcdd)
@@ -6,13 +6,12 @@
 import pytest
 
-from builder.questions import RadioQuestion, AnswerOption
 from flexoentity import EntityState, EntityType, Domain
 
 @pytest.fixture
 def approved_question():
-    """Provide a fully approved and published RadioQuestion for persistence tests."""
-    q = RadioQuestion(
-        domain=Domain(domain="GEN", etype=EntityType.DOMAIN, state=EntityState.DRAFT),
-        etype=None,  # RadioQuestion sets this internally to EntityType.QUESTION
+    """Provide a fully approved and published SingleChoiceQuestion for persistence tests."""
+    q = SingleChoiceQuestion(
+        domain=Domain(domain="GEN", entity_type=EntityType.DOMAIN, state=EntityState.DRAFT),
+        entity_type=None,  # SingleChoiceQuestion sets this internally to EntityType.ITEM
         state=EntityState.DRAFT,
         text="What is Ohm’s law?",
@@ -36,10 +35,10 @@
     json_str = approved_question.to_json()
     print("JSON", json_str)
-    loaded = RadioQuestion.from_json(json_str)
+    loaded = SingleChoiceQuestion.from_json(json_str)
 
     print("Approved", approved_question.text_seed)
     print("Loaded", loaded.text_seed)
     # Fingerprint and state should match — integrity must pass
-    assert RadioQuestion.verify_integrity(loaded)
+    assert SingleChoiceQuestion.verify_integrity(loaded)
 
     # Metadata should be preserved exactly
@@ -56,6 +55,6 @@
     tampered_json = json.dumps(tampered)
 
-    loaded = RadioQuestion.from_json(tampered_json)
-    assert not RadioQuestion.verify_integrity(loaded)
+    loaded = SingleChoiceQuestion.from_json(tampered_json)
+    assert not SingleChoiceQuestion.verify_integrity(loaded)
 
 @pytest.mark.skip(reason="FlexOIDs regenerated on import; corruption detection not yet applicable")
@@ -70,4 +69,4 @@
     file.write_text(corrupted)
 
-    loaded = RadioQuestion.from_json(file.read_text())
-    assert not RadioQuestion.verify_integrity(loaded)
+    loaded = SingleChoiceQuestion.from_json(file.read_text())
+    assert not SingleChoiceQuestion.verify_integrity(loaded)
