Changeset 59342ce in flexoentity


Ignore:
Timestamp:
10/19/25 11:42:00 (3 months ago)
Author:
Enrico Schwass <ennoausberlin@…>
Branches:
master
Children:
0b4a5e6
Parents:
0036877
Message:

all tests green

Files:
3 added
3 edited

Legend:

Unmodified
Added
Removed
  • flexoentity/__init__.py

    r0036877 r59342ce  
    66- FlexoEntity: lifecycle-tracked base class for all Flex-O domain objects
    77"""
     8from importlib.metadata import version, PackageNotFoundError
     9from .id_factory import FlexOID, canonical_seed
     10from .flexo_entity import FlexoEntity, EntityType, EntityState
    811
    9 from .id_factory import FlexOID, canonical_seed
    10 from .flexo_entity import FlexoEntity
     12__all__ = [
     13    "FlexOID",
     14    "canonical_seed",
     15    "FlexoEntity",
     16    "EntityType",
     17    "EntityState",
     18]
    1119
    12 __all__ = ["FlexOID", "canonical_seed", "FlexoEntity"]
    13 __version__ = "1.0.0"
     20# Optional: keep dynamic version retrieval synced with pyproject.toml
     21try:
     22    __version__ = version("flexoentity")
     23except PackageNotFoundError:
     24    __version__ = "0.0.0-dev"
  • flexoentity/flexo_entity.py

    r0036877 r59342ce  
    99from typing import Optional
    1010from abc import ABC
    11 
    12 from id_factory import FlexOID
    13 
     11import hashlib
     12
     13
     14from flexoentity.id_factory import FlexOID
     15from flexoentity import canonical_seed
     16 
    1417
    1518# ──────────────────────────────────────────────────────────────────────────────
     
    5659            EntityState.APPROVED: "A",
    5760            EntityState.APPROVED_AND_SIGNED: "S",
     61            EntityState.PUBLISHED: "P",
    5862            EntityState.OBSOLETE: "O",
     63
    5964        }
    6065        return mapping[self]
     
    6974            "A": cls.APPROVED,
    7075            "S": cls.APPROVED_AND_SIGNED,
     76            "P": cls.PUBLISHED,
    7177            "O": cls.OBSOLETE,
    7278        }
     
    7783
    7884    def __str__(self):
    79         return self.value
     85        return self.name
    8086
    8187
     
    139145        return cls.from_dict(data)
    140146
     147    @staticmethod
     148    def should_version(state) -> bool:
     149        """
     150        Determine if a given lifecycle state should trigger a version increment.
     151
     152        Entities typically version when they move into more stable or
     153        externally visible states, such as APPROVED, SIGNED, or PUBLISHED.
     154        """
     155
     156        return state in (
     157            EntityState.APPROVED,
     158            EntityState.APPROVED_AND_SIGNED,
     159            EntityState.PUBLISHED,
     160        )
     161   
    141162    # ───────────────────────────────────────────────────────────────
    142163    def _update_fingerprint(self) -> bool:
    143164        """Recalculate fingerprint and return True if content changed."""
    144165        # extract version from current flexo_id
    145         new_oid = FlexOID.generate(self.domain, self.etype, self.text_seed, self.flexo_id.version)
     166        new_oid = FlexOID.generate(self.domain, self.etype.short(), self.state.short(), self.text_seed, self.flexo_id.version)
    146167        if new_oid.signature != self.flexo_id.signature:
    147168            self.flexo_id = new_oid
     
    157178
    158179        # Check if version should bump
    159         if FlexOID.should_version(self.etype, target_state):
    160             if self._update_fingerprint():
    161                 self.flexo_id = FlexOID.next_version(self.flexo_id)
     180        if self.should_version(target_state):
     181            self._update_fingerprint()
     182            self.flexo_id = FlexOID.next_version(self.flexo_id)
    162183
    163184        self.state = target_state
     
    184205        """
    185206        Move from DRAFT to APPROVED state.
    186         Draft entities receive a new permanent FlexOID.
     207        Draft entities receive a new permanent FlexOID with incremented version.
    187208        """
    188209        if self.state == EntityState.DRAFT:
    189             # Generate a brand new permanent ID
     210            new_version = self.flexo_id.version + 1
    190211            new_fid = FlexOID.generate(
    191212                self.domain,
    192                 self.etype,
     213                self.etype.short(),
     214                EntityState.APPROVED.short(),
    193215                self.text_seed,
    194                 draft=False
     216                version=new_version
    195217            )
    196218            self.previous_id = self.flexo_id  # optional: keep audit trail
    197219            self.flexo_id = new_fid
    198 
    199220            self.state = EntityState.APPROVED
    200221            self.updated_at = datetime.utcnow()
     
    203224
    204225    def sign(self):
    205         # FIXME: We need to define clear conditions, when resigning is neccessary and allowed
    206         #        if self.state == EntityState.APPROVED:
    207         #            self._transition(EntityState.APPROVED_AND_SIGNED)
    208         #            self.bump_version()
    209226        self._transition(EntityState.APPROVED_AND_SIGNED)
    210         self.bump_version()
    211227
    212228    def publish(self):
     
    220236    def clone_new_base(self):
    221237        """Start new lineage when obsolete."""
    222         self.flexo_id = FlexOID.clone_new_base(self.domain, self.etype, self.text_seed)
     238        self.flexo_id = FlexOID.clone_new_base(
     239            self.domain,
     240            self.etype.short(),
     241            self.state.short(),
     242            self.text_seed,
     243        )
    223244        self.state = EntityState.DRAFT
    224245        self.updated_at = datetime.utcnow()
     
    235256            self.updated_at = datetime.utcnow()
    236257
     258
     259    # ───────────────────────────────────────────────────────────────
     260    # Integrity verification
     261    # ───────────────────────────────────────────────────────────────
     262
    237263    @staticmethod
    238264    def verify_integrity(entity) -> bool:
    239265        """
    240         Verify if the stored fingerprint matches recalculated fingerprint.
    241         Returns True if intact, False if tampered or corrupted.
    242         """
    243         recalculated = FlexOID.generate(entity.domain, entity.etype, entity.text_seed, entity.version)
    244         return recalculated.signature == entity.flexo_id.signature
     266        Verify *state-aware* integrity.
     267
     268        This method validates that the entity's stored digital signature
     269        matches a freshly recalculated one, based on the combination of:
     270
     271            text_seed + current lifecycle state (one-letter code)
     272
     273        Returns
     274        -------
     275        bool
     276            True if the entity's *state and content* are unchanged,
     277            False if either was altered or corrupted.
     278        """
     279        seed = canonical_seed(f"{entity.text_seed}:{entity.state.short()}")
     280        recalculated_sig = hashlib.blake2s(
     281            seed.encode("utf-8"), digest_size=8
     282        ).hexdigest().upper()
     283
     284        return recalculated_sig == entity.flexo_id.signature
     285
     286    @staticmethod
     287    def verify_content_integrity(entity) -> bool:
     288        """
     289        Verify *content-only* integrity (ignores lifecycle state).
     290
     291        This method checks whether the stored entity's signature matches
     292        a fresh hash of its text seed alone. It does not include the
     293        lifecycle state in the fingerprint.
     294
     295        Returns
     296        -------
     297        bool
     298        True if the text content has not been altered,
     299        False if it differs from the original content.
     300        """
     301        seed = canonical_seed(entity.text_seed)
     302        recalculated_sig = hashlib.blake2s(
     303            seed.encode("utf-8"), digest_size=8
     304        ).hexdigest().upper()
     305        return recalculated_sig == entity.flexo_id.signature
    245306
    246307    def allowed_transitions(self) -> list[str]:
  • flexoentity/id_factory.py

    r0036877 r59342ce  
    2828    """
    2929    if isinstance(obj, str):
    30         text = " ".join(obj.lower().split())
     30        text = " ".join(obj.split())
    3131        return text
    3232    if isinstance(obj, dict):
     
    9191    # ──────────────────────────────────────────────────────────────────────────
    9292    @staticmethod
    93     def generate(domain: str, etype: str, estate: str, text: str, version: int = 1):
     93    def generate(domain: str, etype: str, estate: str, text: str,
     94                 version: int = 1, enforce_unique = True):
    9495        """
    9596        Generate a new, versioned, and state-aware Flex-O ID.
     
    138139
    139140        base_hash = FlexOID._blake_hash(seed)
    140         unique_hash = FlexOID._ensure_unique(base_hash, version)
     141        unique_hash = FlexOID._ensure_unique(base_hash, version)if enforce_unique else base_hash
    141142
    142143        ver_part = f"{version:03d}{estate}"
Note: See TracChangeset for help on using the changeset viewer.