From: Thomas Walker Lynch Date: Thu, 8 Jan 2026 15:37:58 +0000 (+0000) Subject: annotating Python objects with property values passes a simple experiment test X-Git-Url: https://git.reasoningtechnology.com/style/theme_dark_gold.js?a=commitdiff_plain;h=7dadbdd586af408e29e0e801ea78d28c5b8e4000;p=Epimetheus%2F.git annotating Python objects with property values passes a simple experiment test --- diff --git a/README.md b/README.md deleted file mode 100644 index 7104a94..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Epimetheus - diff --git a/README.org b/README.org new file mode 100644 index 0000000..07975ca --- /dev/null +++ b/README.org @@ -0,0 +1,28 @@ +* Epimetheus +/The God of Afterthought/ + +Epimetheus is a Python library for **Higher Order Analysis** and **Semantic Annotation**. + +Unlike Prometheus (Forethought), who defines what an object /is/ before it is born (classic class instantiation), Epimetheus allows you to attach meaning, identity, and semantic types to objects /after/ they exist. + +It is a mechanism for "Late Binding" of ontology. + +** Concept +Most type systems are embedded in the object's definition (the Class). Epimetheus treats the object as a point in space and allows an external observer (the library) to weave a "constellation" of properties and relationships around it. + +This enables: +- **Semantic Typing:** Tagging objects with properties ($color$, $spin$, $is_valid$) without altering the object's code. +- **Higher Order Analysis:** defining relationships between objects (constellations) rather than just properties of objects. +- **Non-Invasive Instrumentation:** Annotating third-party objects or C-extensions that cannot be subclassed. + +** Etymology +Named after the Greek Titan *Epimetheus* (Afterthought). While his brother Prometheus is associated with architectural planning, Epimetheus is the patron of patching, extending, and adapting the world as it is found. + +** Status +- **Author:** Thomas Walker Lynch +- **Version:** Pre-Alpha + +#+BEGIN_QUOTE +"Zero length is a second order concept." +-- T.W. Lynch, *TTCA* +#+END_QUOTE diff --git a/developer/__init__.py b/developer/__init__.py new file mode 100644 index 0000000..45fa30d --- /dev/null +++ b/developer/__init__.py @@ -0,0 +1,2 @@ +# developer/authored/__init__.py +from .PropertyManager import PropertyManager diff --git a/developer/authored/Epimetheus/ObjectRegistry.py b/developer/authored/Epimetheus/ObjectRegistry.py new file mode 100644 index 0000000..9d30fd9 --- /dev/null +++ b/developer/authored/Epimetheus/ObjectRegistry.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- + +""" +ObjectRegistry + +Maps Python runtime objects to ProcessLocalId. + +Strategies: + 1. Weak Identity (Preferred): For objects that support weakrefs (instances ,classes). + - ID lifetime is bound to object lifetime. + - Auto-cleaned on GC. + + 2. Value Identity (Fallback): For immutable primitives (int ,str ,tuple). + - ID is bound to the *value* (hash/equality). + - Stored strongly (since values like 42 or "red" are conceptually eternal). +""" + +from __future__ import annotations + +import weakref +from typing import Any ,Callable ,Dict ,Optional + +from .ProcessLocalId import ProcessLocalIdGenerator ,ProcessLocalId + + +class ObjectRegistry: + def __init__(self ,id_gen: ProcessLocalIdGenerator): + self._id_gen = id_gen + + # Strategy 1: Entities (Weakref-able) + self._obj_to_id_wkd: "weakref.WeakKeyDictionary[Any ,ProcessLocalId]" = weakref.WeakKeyDictionary() + self._id_to_obj_ref: Dict[ProcessLocalId ,weakref.ref] = {} + + # Strategy 2: Values (Hashable ,not weakref-able) + self._value_to_id: Dict[Any ,ProcessLocalId] = {} + + self._finalizers: Dict[ProcessLocalId ,Callable[[ProcessLocalId] ,None]] = {} + self._global_finalizer: Optional[Callable[[ProcessLocalId] ,None]] = None + + def register_finalizer(self ,fn: Callable[[ProcessLocalId] ,None]): + """ + Registers a finalizer callback invoked when any registered *weak-refable* object is GC'd. + """ + self._global_finalizer = fn + + def _on_collect(self ,obj_id: ProcessLocalId): + # Only called for Strategy 1 objects + ref = self._id_to_obj_ref.pop(obj_id ,None) + if(ref is not None): + # The WeakKeyDictionary auto-cleans the forward mapping , + # but we double check or clean any edge cases if needed. + pass + + fn = self._global_finalizer + if(fn is not None): + fn(obj_id) + + def get_id(self ,obj: Any) -> ProcessLocalId: + """ + Returns the ProcessLocalId for `obj` ,registering it if needed. + """ + # 1. Try WeakRef Strategy (Entities) + try: + existing = self._obj_to_id_wkd.get(obj) + if(existing is not None): + return existing + except TypeError: + # obj is not weakref-able (e.g. int ,str ,tuple ,or list). + # Fall through to Strategy 2. + pass + else: + # It IS weakref-able ,but wasn't in the dictionary. Register it. + obj_id = self._id_gen.next_id() + self._obj_to_id_wkd[obj] = obj_id + # Create reverse lookup with callback + self._id_to_obj_ref[obj_id] = weakref.ref(obj ,lambda _ref ,oid=obj_id: self._on_collect(oid)) + return obj_id + + # 2. Try Value Strategy (Primitives) + # Note: Mutable non-weakrefables (like standard lists) will fail here because they are unhashable. + try: + existing = self._value_to_id.get(obj) + if(existing is not None): + return existing + + # Register new value + obj_id = self._id_gen.next_id() + self._value_to_id[obj] = obj_id + return obj_id + except TypeError: + # It is neither weakref-able NOR hashable (e.g. standard list ,dict). + raise TypeError( + f"ObjectRegistry: cannot track object of type {type(obj)!r}. " + "It is neither weakref-able (Entity) nor hashable (Value)." + ) + + def try_get_object(self ,obj_id: ProcessLocalId) -> Optional[Any]: + """ + Best-effort: returns the live object/value. + """ + # Check Entities + ref = self._id_to_obj_ref.get(obj_id) + if(ref is not None): + return ref() + + # Check Values + # For now ,we assume values identify themselves. + for val ,pid in self._value_to_id.items(): + if(pid == obj_id): + return val + + return None diff --git a/developer/authored/Epimetheus/ProcessLocalId.py b/developer/authored/Epimetheus/ProcessLocalId.py new file mode 100644 index 0000000..b57c47c --- /dev/null +++ b/developer/authored/Epimetheus/ProcessLocalId.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- + + +""" +ProcessLocalId + +A process-local identifier used as an internal key. + +Design constraint: + - NOT intended to be serialized or persisted. + - `repr()` intentionally does not reveal the numeric token, to discourage logging/persistence. +""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True ,slots=True) +class ProcessLocalId: + _n: int + + def __repr__(self) -> str: + return "" + + def __str__(self) -> str: + return "" + + def as_int_UNSAFE(self) -> int: + """ + Returns the raw integer token. + + UNSAFE because: + - tokens are process-local + - do not write these into files/databases/logs as stable identifiers + """ + return self._n + + +class ProcessLocalIdGenerator: + """ + Monotonic generator; ids are never recycled. + """ + def __init__(self ,start: int = 1): + if start < 1: raise ValueError("start must be >= 1") + self._next_n: int = start + + def next_id(self) -> ProcessLocalId: + n = self._next_n + self._next_n += 1 + return ProcessLocalId(n) diff --git a/developer/authored/Epimetheus/Property.py b/developer/authored/Epimetheus/Property.py new file mode 100644 index 0000000..5b33226 --- /dev/null +++ b/developer/authored/Epimetheus/Property.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- + + +""" +Property + +A Property is itself an entity (it has an Identity id) so that: + - properties can have properties + - properties can be members of semantic sets +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Optional ,Tuple + +from .ProcessLocalId import ProcessLocalId + + +@dataclass(frozen=True ,slots=True) +class Property: + id: ProcessLocalId + name_path: Tuple[str ,...] + doc: str = "" + + def __repr__(self) -> str: + # name_path is safe to reveal; id token is not. + return f"" diff --git a/developer/authored/Epimetheus/PropertyManager.py b/developer/authored/Epimetheus/PropertyManager.py new file mode 100644 index 0000000..36e2b9e --- /dev/null +++ b/developer/authored/Epimetheus/PropertyManager.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- + + +""" +PropertyManager + +Core RT property system. + +Key decisions vs the earlier `property_manager.py`: + - do NOT key by `object_path()` strings (avoids collisions) fileciteturn3file4 + - runtime objects are keyed by weak identity (ProcessLocalId assigned by ObjectRegistry) + - properties are first-class entities (Property has an id), so properties can have properties + +This remains process-local and in-memory. +""" + +from __future__ import annotations + +from typing import Any ,Dict ,Iterable ,List ,Optional ,Tuple ,Union + +from .ProcessLocalId import ProcessLocalIdGenerator ,ProcessLocalId +from .ObjectRegistry import ObjectRegistry +from .PropertyStore import PropertyStore +from .Property import Property +from .SemanticSets import SemanticSet ,SemanticSetStore +from .Syntax import SyntaxInstance + + +NamePathLike = Union[str ,List[str] ,Tuple[str ,...]] + + +class PropertyManager: + def __init__(self): + self._id_gen = ProcessLocalIdGenerator() + self._obj_reg = ObjectRegistry(self._id_gen) + self._store = PropertyStore() + self._sets = SemanticSetStore() + + # Declare-by-name registry + self._name_path_to_property: Dict[Tuple[str ,...],Property] = {} + self._property_id_to_property: Dict[ProcessLocalId ,Property] = {} + + self._name_path_to_set: Dict[Tuple[str ,...],SemanticSet] = {} + self._set_id_to_set: Dict[ProcessLocalId ,SemanticSet] = {} + + # Optional syntax instances (if user chooses to model them) + self._syntax_id_to_instance: Dict[ProcessLocalId ,SyntaxInstance] = {} + + # Finalization cleanup + self._obj_reg.register_finalizer(self._on_subject_finalized) + + def _on_subject_finalized(self ,subject_id: ProcessLocalId): + self._store.remove_subject(subject_id) + self._sets.remove_subject(subject_id) + + def _normalize_name_path(self ,name_path: NamePathLike) -> Tuple[str ,...]: + if isinstance(name_path ,tuple): return name_path + if isinstance(name_path ,list): return tuple(name_path) + if isinstance(name_path ,str): return tuple(name_path.split(".")) + raise TypeError("name_path must be str ,list[str] ,or tuple[str ,...]") + + # ------------------------- + # Identity acquisition + # ------------------------- + def id_of_py_object(self ,obj: Any) -> ProcessLocalId: + return self._obj_reg.get_id(obj) + + def create_syntax_identity(self ,syntax: SyntaxInstance) -> ProcessLocalId: + sid = self._id_gen.next_id() + self._syntax_id_to_instance[sid] = syntax + return sid + + def try_get_syntax(self ,syntax_id: ProcessLocalId) -> Optional[SyntaxInstance]: + return self._syntax_id_to_instance.get(syntax_id) + + # ------------------------- + # Property declaration + # ------------------------- + def declare_property(self ,name_path: NamePathLike ,doc: str = "") -> ProcessLocalId: + np = self._normalize_name_path(name_path) + existing = self._name_path_to_property.get(np) + if existing is not None: return existing.id + pid = self._id_gen.next_id() + p = Property(pid ,np ,doc) + self._name_path_to_property[np] = p + self._property_id_to_property[pid] = p + return pid + + def property_id(self ,name_path: NamePathLike) -> ProcessLocalId: + np = self._normalize_name_path(name_path) + p = self._name_path_to_property.get(np) + if p is None: raise KeyError(f"Property not declared: {np!r}") + return p.id + + def try_get_property(self ,prop_id: ProcessLocalId) -> Optional[Property]: + return self._property_id_to_property.get(prop_id) + + # ------------------------- + # Semantic sets + # ------------------------- + def declare_set(self ,name_path: NamePathLike ,doc: str = "") -> ProcessLocalId: + np = self._normalize_name_path(name_path) + existing = self._name_path_to_set.get(np) + if existing is not None: return existing.id + sid = self._id_gen.next_id() + s = SemanticSet(sid ,np ,doc) + self._name_path_to_set[np] = s + self._set_id_to_set[sid] = s + return sid + + def add_to_set(self ,subject: Any ,set_id: ProcessLocalId): + subject_id = self._coerce_subject_id(subject) + self._sets.add_member(set_id ,subject_id) + + def is_in_set(self ,subject: Any ,set_id: ProcessLocalId) -> bool: + subject_id = self._coerce_subject_id(subject) + return self._sets.has_member(set_id ,subject_id) + + def members(self ,set_id: ProcessLocalId) -> List[ProcessLocalId]: + return list(self._sets.members(set_id)) + + # ------------------------- + # Set/get properties + # ------------------------- + def set(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike] ,value: Any): + subject_id = self._coerce_subject_id(subject) + prop_id = self._coerce_property_id(prop) + self._store.set(subject_id ,prop_id ,value) + + def get(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike] ,default: Any = None) -> Any: + subject_id = self._coerce_subject_id(subject) + prop_id = self._coerce_property_id(prop) + return self._store.get(subject_id ,prop_id ,default) + + def has(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike]) -> bool: + subject_id = self._coerce_subject_id(subject) + prop_id = self._coerce_property_id(prop) + return self._store.has(subject_id ,prop_id) + + def subjects_with(self ,prop: Union[ProcessLocalId ,NamePathLike]) -> List[ProcessLocalId]: + prop_id = self._coerce_property_id(prop) + return list(self._store.subjects_with(prop_id)) + + # ------------------------- + # Coercions + # ------------------------- + def _coerce_subject_id(self ,subject: Any) -> ProcessLocalId: + if isinstance(subject ,ProcessLocalId): return subject + # For Python runtime objects, we require weakref-able instances. + return self._obj_reg.get_id(subject) + + def _coerce_property_id(self ,prop: Union[ProcessLocalId ,NamePathLike]) -> ProcessLocalId: + if isinstance(prop ,ProcessLocalId): return prop + return self.property_id(prop) diff --git a/developer/authored/Epimetheus/PropertyStore.py b/developer/authored/Epimetheus/PropertyStore.py new file mode 100644 index 0000000..bc8f3f3 --- /dev/null +++ b/developer/authored/Epimetheus/PropertyStore.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- + + +""" +PropertyStore + +Stores property values and maintains reverse lookups. + +This is intentionally process-local and in-memory. +""" + +from __future__ import annotations + +from typing import Any ,Dict ,Optional ,Set ,Tuple + +from .ProcessLocalId import ProcessLocalId + + +class PropertyStore: + def __init__(self): + # (subject_id ,property_id) -> value + self._values: Dict[Tuple[ProcessLocalId ,ProcessLocalId] ,Any] = {} + + # subject_id -> set(property_id) + self._subject_to_props: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {} + + # property_id -> set(subject_id) + self._prop_to_subjects: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {} + + def set(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId ,value: Any): + key = (subject_id ,prop_id) + self._values[key] = value + self._subject_to_props.setdefault(subject_id ,set()).add(prop_id) + self._prop_to_subjects.setdefault(prop_id ,set()).add(subject_id) + + def get(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId ,default: Any = None) -> Any: + return self._values.get((subject_id ,prop_id) ,default) + + def has(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId) -> bool: + return (subject_id ,prop_id) in self._values + + def subjects_with(self ,prop_id: ProcessLocalId) -> Set[ProcessLocalId]: + return set(self._prop_to_subjects.get(prop_id ,set())) + + def props_of(self ,subject_id: ProcessLocalId) -> Set[ProcessLocalId]: + return set(self._subject_to_props.get(subject_id ,set())) + + def remove_subject(self ,subject_id: ProcessLocalId): + """ + Remove all stored properties for a subject (used on finalization). + """ + prop_ids = self._subject_to_props.pop(subject_id ,set()) + for prop_id in prop_ids: + self._values.pop((subject_id ,prop_id) ,None) + s = self._prop_to_subjects.get(prop_id) + if s is not None: + s.discard(subject_id) + if not s: self._prop_to_subjects.pop(prop_id ,None) diff --git a/developer/authored/Epimetheus/SemanticSets.py b/developer/authored/Epimetheus/SemanticSets.py new file mode 100644 index 0000000..f0fdf48 --- /dev/null +++ b/developer/authored/Epimetheus/SemanticSets.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- + + +""" +SemanticSets + +Membership sets over identities. Used for semantic typing. + +Design: + - set_id identifies the set + - members are subject ids + - reverse index for cleanup +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Dict ,Optional ,Set + +from .ProcessLocalId import ProcessLocalId + + +@dataclass(frozen=True ,slots=True) +class SemanticSet: + id: ProcessLocalId + name_path: tuple[str ,...] + doc: str = "" + + def __repr__(self) -> str: + return f"" + + +class SemanticSetStore: + def __init__(self): + self._members: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {} + self._subject_to_sets: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {} + + def add_member(self ,set_id: ProcessLocalId ,subject_id: ProcessLocalId): + self._members.setdefault(set_id ,set()).add(subject_id) + self._subject_to_sets.setdefault(subject_id ,set()).add(set_id) + + def has_member(self ,set_id: ProcessLocalId ,subject_id: ProcessLocalId) -> bool: + return subject_id in self._members.get(set_id ,set()) + + def members(self ,set_id: ProcessLocalId) -> Set[ProcessLocalId]: + return set(self._members.get(set_id ,set())) + + def remove_subject(self ,subject_id: ProcessLocalId): + set_ids = self._subject_to_sets.pop(subject_id ,set()) + for set_id in set_ids: + m = self._members.get(set_id) + if m is not None: + m.discard(subject_id) + if not m: self._members.pop(set_id ,None) diff --git a/developer/authored/Epimetheus/Syntax.py b/developer/authored/Epimetheus/Syntax.py new file mode 100644 index 0000000..4fcfa7c --- /dev/null +++ b/developer/authored/Epimetheus/Syntax.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- + + +""" +Syntax + +RT syntax identity instances. + +We treat "syntax" as AST-level objects: + - kind: official-ish AST node kind name (e.g., "ast.FunctionDef") + - location: file + span + - scope: enclosing syntax identity id (optional) + - parts: mapping of part-name to literal or referenced syntax identity id(s) + +This module does NOT traverse Python programs. It only defines the data model. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any ,Dict ,Optional ,Tuple ,Union ,List + +from .ProcessLocalId import ProcessLocalId + + +@dataclass(frozen=True ,slots=True) +class SourceSpan: + file_path: str + lineno: int + col: int + end_lineno: int + end_col: int + + +SyntaxPartValue = Union[ + None + ,bool + ,int + ,float + ,str + ,ProcessLocalId + ,List["SyntaxPartValue"] + ,Dict[str ,"SyntaxPartValue"] +] + + +@dataclass(frozen=True ,slots=True) +class SyntaxInstance: + """ + A single syntax node instance. + + NOTE: many syntax nodes have no identifier-name. Name-like things (identifiers) + appear as child nodes or literals inside `parts`. + """ + kind: str + span: SourceSpan + scope_id: Optional[ProcessLocalId] = None + parts: Dict[str ,SyntaxPartValue] = None diff --git a/developer/authored/Epimetheus/__init__.py b/developer/authored/Epimetheus/__init__.py new file mode 100644 index 0000000..ce046ae --- /dev/null +++ b/developer/authored/Epimetheus/__init__.py @@ -0,0 +1 @@ +from .PropertyManager import PropertyManager diff --git a/developer/authored/Identity.py b/developer/authored/Identity.py deleted file mode 100644 index 236ef8a..0000000 --- a/developer/authored/Identity.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - - -""" -Identity - -An abstract identity used as the subject key for property attachment. - -Kinds (strings) determine storage and resolution behavior. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any ,Optional ,Tuple - -from .ProcessLocalId import ProcessLocalId - - -IDENTITY_KIND_PY_OBJECT = "py_object" -IDENTITY_KIND_SYNTAX = "syntax" -IDENTITY_KIND_PROPERTY = "property" -IDENTITY_KIND_SET = "semantic_set" - - -@dataclass(frozen=True ,slots=True) -class Identity: - """ - `id` is always a ProcessLocalId. - - `kind` partitions lookup behavior. - - `payload` is kind-specific metadata (kept small; do not put giant graphs here). - """ - id: ProcessLocalId - kind: str - payload: Any = None - - def __repr__(self) -> str: - # Do not reveal id token. - return f"" diff --git a/developer/authored/ObjectRegistry.py b/developer/authored/ObjectRegistry.py deleted file mode 100644 index b3e4cab..0000000 --- a/developer/authored/ObjectRegistry.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - - -""" -ObjectRegistry - -Maps Python runtime objects to ProcessLocalId using weak identity. - -Constraints: - - Only weakref-able Python objects can be registered. - - This is intentional: RT properties attach to identity-bearing runtime instances, - not to value-like primitives (ints/strings/lists/dicts). -""" - -from __future__ import annotations - -import weakref -from typing import Any ,Callable ,Dict ,Optional - -from .ProcessLocalId import ProcessLocalIdGenerator ,ProcessLocalId - - -class ObjectRegistry: - def __init__(self ,id_gen: ProcessLocalIdGenerator): - self._id_gen = id_gen - self._obj_to_id_wkd: "weakref.WeakKeyDictionary[Any ,ProcessLocalId]" = weakref.WeakKeyDictionary() - self._id_to_obj_ref: Dict[ProcessLocalId ,weakref.ref] = {} - self._finalizers: Dict[ProcessLocalId ,Callable[[ProcessLocalId],None]] = {} - - def register_finalizer(self ,fn: Callable[[ProcessLocalId],None]): - """ - Registers a finalizer callback invoked when any registered object is GC'd. - """ - self._global_finalizer = fn - - def _on_collect(self ,obj_id: ProcessLocalId): - self._obj_to_id_wkd.pop(self._id_to_obj_ref[obj_id]() ,None) - self._id_to_obj_ref.pop(obj_id ,None) - fn = getattr(self ,"_global_finalizer" ,None) - if fn is not None: fn(obj_id) - - def get_id(self ,obj: Any) -> ProcessLocalId: - """ - Returns the ProcessLocalId for `obj`, registering it if needed. - - Raises TypeError if `obj` is not weakref-able. - """ - try: - existing = self._obj_to_id_wkd.get(obj) - except TypeError: - raise TypeError("ObjectRegistry: object is not weakref-able; RT properties do not attach to value-like primitives.") - if existing is not None: return existing - - obj_id = self._id_gen.next_id() - try: - self._obj_to_id_wkd[obj] = obj_id - except TypeError: - raise TypeError("ObjectRegistry: object is not weakref-able; RT properties do not attach to value-like primitives.") - self._id_to_obj_ref[obj_id] = weakref.ref(obj ,lambda _ref ,oid=obj_id: self._on_collect(oid)) - return obj_id - - def try_get_object(self ,obj_id: ProcessLocalId) -> Optional[Any]: - """ - Best-effort: returns the live object, or None if it has been collected or never registered. - """ - ref = self._id_to_obj_ref.get(obj_id) - if ref is None: return None - return ref() diff --git a/developer/authored/ProcessLocalId.py b/developer/authored/ProcessLocalId.py deleted file mode 100644 index b57c47c..0000000 --- a/developer/authored/ProcessLocalId.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - - -""" -ProcessLocalId - -A process-local identifier used as an internal key. - -Design constraint: - - NOT intended to be serialized or persisted. - - `repr()` intentionally does not reveal the numeric token, to discourage logging/persistence. -""" - -from __future__ import annotations - -from dataclasses import dataclass - - -@dataclass(frozen=True ,slots=True) -class ProcessLocalId: - _n: int - - def __repr__(self) -> str: - return "" - - def __str__(self) -> str: - return "" - - def as_int_UNSAFE(self) -> int: - """ - Returns the raw integer token. - - UNSAFE because: - - tokens are process-local - - do not write these into files/databases/logs as stable identifiers - """ - return self._n - - -class ProcessLocalIdGenerator: - """ - Monotonic generator; ids are never recycled. - """ - def __init__(self ,start: int = 1): - if start < 1: raise ValueError("start must be >= 1") - self._next_n: int = start - - def next_id(self) -> ProcessLocalId: - n = self._next_n - self._next_n += 1 - return ProcessLocalId(n) diff --git a/developer/authored/Property.py b/developer/authored/Property.py deleted file mode 100644 index 5b33226..0000000 --- a/developer/authored/Property.py +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - - -""" -Property - -A Property is itself an entity (it has an Identity id) so that: - - properties can have properties - - properties can be members of semantic sets -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Optional ,Tuple - -from .ProcessLocalId import ProcessLocalId - - -@dataclass(frozen=True ,slots=True) -class Property: - id: ProcessLocalId - name_path: Tuple[str ,...] - doc: str = "" - - def __repr__(self) -> str: - # name_path is safe to reveal; id token is not. - return f"" diff --git a/developer/authored/PropertyManager.py b/developer/authored/PropertyManager.py deleted file mode 100644 index 36e2b9e..0000000 --- a/developer/authored/PropertyManager.py +++ /dev/null @@ -1,155 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - - -""" -PropertyManager - -Core RT property system. - -Key decisions vs the earlier `property_manager.py`: - - do NOT key by `object_path()` strings (avoids collisions) fileciteturn3file4 - - runtime objects are keyed by weak identity (ProcessLocalId assigned by ObjectRegistry) - - properties are first-class entities (Property has an id), so properties can have properties - -This remains process-local and in-memory. -""" - -from __future__ import annotations - -from typing import Any ,Dict ,Iterable ,List ,Optional ,Tuple ,Union - -from .ProcessLocalId import ProcessLocalIdGenerator ,ProcessLocalId -from .ObjectRegistry import ObjectRegistry -from .PropertyStore import PropertyStore -from .Property import Property -from .SemanticSets import SemanticSet ,SemanticSetStore -from .Syntax import SyntaxInstance - - -NamePathLike = Union[str ,List[str] ,Tuple[str ,...]] - - -class PropertyManager: - def __init__(self): - self._id_gen = ProcessLocalIdGenerator() - self._obj_reg = ObjectRegistry(self._id_gen) - self._store = PropertyStore() - self._sets = SemanticSetStore() - - # Declare-by-name registry - self._name_path_to_property: Dict[Tuple[str ,...],Property] = {} - self._property_id_to_property: Dict[ProcessLocalId ,Property] = {} - - self._name_path_to_set: Dict[Tuple[str ,...],SemanticSet] = {} - self._set_id_to_set: Dict[ProcessLocalId ,SemanticSet] = {} - - # Optional syntax instances (if user chooses to model them) - self._syntax_id_to_instance: Dict[ProcessLocalId ,SyntaxInstance] = {} - - # Finalization cleanup - self._obj_reg.register_finalizer(self._on_subject_finalized) - - def _on_subject_finalized(self ,subject_id: ProcessLocalId): - self._store.remove_subject(subject_id) - self._sets.remove_subject(subject_id) - - def _normalize_name_path(self ,name_path: NamePathLike) -> Tuple[str ,...]: - if isinstance(name_path ,tuple): return name_path - if isinstance(name_path ,list): return tuple(name_path) - if isinstance(name_path ,str): return tuple(name_path.split(".")) - raise TypeError("name_path must be str ,list[str] ,or tuple[str ,...]") - - # ------------------------- - # Identity acquisition - # ------------------------- - def id_of_py_object(self ,obj: Any) -> ProcessLocalId: - return self._obj_reg.get_id(obj) - - def create_syntax_identity(self ,syntax: SyntaxInstance) -> ProcessLocalId: - sid = self._id_gen.next_id() - self._syntax_id_to_instance[sid] = syntax - return sid - - def try_get_syntax(self ,syntax_id: ProcessLocalId) -> Optional[SyntaxInstance]: - return self._syntax_id_to_instance.get(syntax_id) - - # ------------------------- - # Property declaration - # ------------------------- - def declare_property(self ,name_path: NamePathLike ,doc: str = "") -> ProcessLocalId: - np = self._normalize_name_path(name_path) - existing = self._name_path_to_property.get(np) - if existing is not None: return existing.id - pid = self._id_gen.next_id() - p = Property(pid ,np ,doc) - self._name_path_to_property[np] = p - self._property_id_to_property[pid] = p - return pid - - def property_id(self ,name_path: NamePathLike) -> ProcessLocalId: - np = self._normalize_name_path(name_path) - p = self._name_path_to_property.get(np) - if p is None: raise KeyError(f"Property not declared: {np!r}") - return p.id - - def try_get_property(self ,prop_id: ProcessLocalId) -> Optional[Property]: - return self._property_id_to_property.get(prop_id) - - # ------------------------- - # Semantic sets - # ------------------------- - def declare_set(self ,name_path: NamePathLike ,doc: str = "") -> ProcessLocalId: - np = self._normalize_name_path(name_path) - existing = self._name_path_to_set.get(np) - if existing is not None: return existing.id - sid = self._id_gen.next_id() - s = SemanticSet(sid ,np ,doc) - self._name_path_to_set[np] = s - self._set_id_to_set[sid] = s - return sid - - def add_to_set(self ,subject: Any ,set_id: ProcessLocalId): - subject_id = self._coerce_subject_id(subject) - self._sets.add_member(set_id ,subject_id) - - def is_in_set(self ,subject: Any ,set_id: ProcessLocalId) -> bool: - subject_id = self._coerce_subject_id(subject) - return self._sets.has_member(set_id ,subject_id) - - def members(self ,set_id: ProcessLocalId) -> List[ProcessLocalId]: - return list(self._sets.members(set_id)) - - # ------------------------- - # Set/get properties - # ------------------------- - def set(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike] ,value: Any): - subject_id = self._coerce_subject_id(subject) - prop_id = self._coerce_property_id(prop) - self._store.set(subject_id ,prop_id ,value) - - def get(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike] ,default: Any = None) -> Any: - subject_id = self._coerce_subject_id(subject) - prop_id = self._coerce_property_id(prop) - return self._store.get(subject_id ,prop_id ,default) - - def has(self ,subject: Any ,prop: Union[ProcessLocalId ,NamePathLike]) -> bool: - subject_id = self._coerce_subject_id(subject) - prop_id = self._coerce_property_id(prop) - return self._store.has(subject_id ,prop_id) - - def subjects_with(self ,prop: Union[ProcessLocalId ,NamePathLike]) -> List[ProcessLocalId]: - prop_id = self._coerce_property_id(prop) - return list(self._store.subjects_with(prop_id)) - - # ------------------------- - # Coercions - # ------------------------- - def _coerce_subject_id(self ,subject: Any) -> ProcessLocalId: - if isinstance(subject ,ProcessLocalId): return subject - # For Python runtime objects, we require weakref-able instances. - return self._obj_reg.get_id(subject) - - def _coerce_property_id(self ,prop: Union[ProcessLocalId ,NamePathLike]) -> ProcessLocalId: - if isinstance(prop ,ProcessLocalId): return prop - return self.property_id(prop) diff --git a/developer/authored/PropertyStore.py b/developer/authored/PropertyStore.py deleted file mode 100644 index bc8f3f3..0000000 --- a/developer/authored/PropertyStore.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - - -""" -PropertyStore - -Stores property values and maintains reverse lookups. - -This is intentionally process-local and in-memory. -""" - -from __future__ import annotations - -from typing import Any ,Dict ,Optional ,Set ,Tuple - -from .ProcessLocalId import ProcessLocalId - - -class PropertyStore: - def __init__(self): - # (subject_id ,property_id) -> value - self._values: Dict[Tuple[ProcessLocalId ,ProcessLocalId] ,Any] = {} - - # subject_id -> set(property_id) - self._subject_to_props: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {} - - # property_id -> set(subject_id) - self._prop_to_subjects: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {} - - def set(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId ,value: Any): - key = (subject_id ,prop_id) - self._values[key] = value - self._subject_to_props.setdefault(subject_id ,set()).add(prop_id) - self._prop_to_subjects.setdefault(prop_id ,set()).add(subject_id) - - def get(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId ,default: Any = None) -> Any: - return self._values.get((subject_id ,prop_id) ,default) - - def has(self ,subject_id: ProcessLocalId ,prop_id: ProcessLocalId) -> bool: - return (subject_id ,prop_id) in self._values - - def subjects_with(self ,prop_id: ProcessLocalId) -> Set[ProcessLocalId]: - return set(self._prop_to_subjects.get(prop_id ,set())) - - def props_of(self ,subject_id: ProcessLocalId) -> Set[ProcessLocalId]: - return set(self._subject_to_props.get(subject_id ,set())) - - def remove_subject(self ,subject_id: ProcessLocalId): - """ - Remove all stored properties for a subject (used on finalization). - """ - prop_ids = self._subject_to_props.pop(subject_id ,set()) - for prop_id in prop_ids: - self._values.pop((subject_id ,prop_id) ,None) - s = self._prop_to_subjects.get(prop_id) - if s is not None: - s.discard(subject_id) - if not s: self._prop_to_subjects.pop(prop_id ,None) diff --git a/developer/authored/SemanticSets.py b/developer/authored/SemanticSets.py deleted file mode 100644 index f0fdf48..0000000 --- a/developer/authored/SemanticSets.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - - -""" -SemanticSets - -Membership sets over identities. Used for semantic typing. - -Design: - - set_id identifies the set - - members are subject ids - - reverse index for cleanup -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Dict ,Optional ,Set - -from .ProcessLocalId import ProcessLocalId - - -@dataclass(frozen=True ,slots=True) -class SemanticSet: - id: ProcessLocalId - name_path: tuple[str ,...] - doc: str = "" - - def __repr__(self) -> str: - return f"" - - -class SemanticSetStore: - def __init__(self): - self._members: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {} - self._subject_to_sets: Dict[ProcessLocalId ,Set[ProcessLocalId]] = {} - - def add_member(self ,set_id: ProcessLocalId ,subject_id: ProcessLocalId): - self._members.setdefault(set_id ,set()).add(subject_id) - self._subject_to_sets.setdefault(subject_id ,set()).add(set_id) - - def has_member(self ,set_id: ProcessLocalId ,subject_id: ProcessLocalId) -> bool: - return subject_id in self._members.get(set_id ,set()) - - def members(self ,set_id: ProcessLocalId) -> Set[ProcessLocalId]: - return set(self._members.get(set_id ,set())) - - def remove_subject(self ,subject_id: ProcessLocalId): - set_ids = self._subject_to_sets.pop(subject_id ,set()) - for set_id in set_ids: - m = self._members.get(set_id) - if m is not None: - m.discard(subject_id) - if not m: self._members.pop(set_id ,None) diff --git a/developer/authored/Syntax.py b/developer/authored/Syntax.py deleted file mode 100644 index 4fcfa7c..0000000 --- a/developer/authored/Syntax.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - - -""" -Syntax - -RT syntax identity instances. - -We treat "syntax" as AST-level objects: - - kind: official-ish AST node kind name (e.g., "ast.FunctionDef") - - location: file + span - - scope: enclosing syntax identity id (optional) - - parts: mapping of part-name to literal or referenced syntax identity id(s) - -This module does NOT traverse Python programs. It only defines the data model. -""" - -from __future__ import annotations - -from dataclasses import dataclass -from typing import Any ,Dict ,Optional ,Tuple ,Union ,List - -from .ProcessLocalId import ProcessLocalId - - -@dataclass(frozen=True ,slots=True) -class SourceSpan: - file_path: str - lineno: int - col: int - end_lineno: int - end_col: int - - -SyntaxPartValue = Union[ - None - ,bool - ,int - ,float - ,str - ,ProcessLocalId - ,List["SyntaxPartValue"] - ,Dict[str ,"SyntaxPartValue"] -] - - -@dataclass(frozen=True ,slots=True) -class SyntaxInstance: - """ - A single syntax node instance. - - NOTE: many syntax nodes have no identifier-name. Name-like things (identifiers) - appear as child nodes or literals inside `parts`. - """ - kind: str - span: SourceSpan - scope_id: Optional[ProcessLocalId] = None - parts: Dict[str ,SyntaxPartValue] = None diff --git a/developer/authored/__init__.py b/developer/authored/__init__.py deleted file mode 100644 index 2b53823..0000000 --- a/developer/authored/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env python3 -# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- - -""" -rt_property_manager - -Process-local property attachment with: - - weak identity for Python runtime instances - - explicit identities for syntax instances and properties - -Notes: - - ProcessLocalId values are not meant to be serialized or persisted. -""" - -from .PropertyManager import PropertyManager diff --git a/developer/experiment/test_Epimetheus.py b/developer/experiment/test_Epimetheus.py new file mode 100755 index 0000000..7024e9d --- /dev/null +++ b/developer/experiment/test_Epimetheus.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# -*- mode: python; coding: utf-8; python-indent-offset: 2 -*- + +import sys +# CHANGED: Import directly from the package 'Epimetheus' +from Epimetheus import PropertyManager + +def test_everything() -> int: + """ + The Work Function. + Returns 0 on success ,non-zero on failure. + """ + try: + pm = PropertyManager() + + # 1. Declare Properties (The Schema) + prop_color = pm.declare_property("color") + prop_meaning = pm.declare_property("meaning") + + # 2. Annotate an Entity (Instance) + class Robot: pass + r1 = Robot() + + pm.set(r1 ,"color" ,"silver") + print(f"Robot Color: {pm.get(r1 ,'color')}") + # Expect: silver + + # 3. Annotate an Entity (Class) + pm.set(Robot ,"meaning" ,"A machine") + print(f"Robot Class Meaning: {pm.get(Robot ,'meaning')}") + # Expect: A machine + + # 4. Annotate a Value (Primitive) + pm.set(42 ,"meaning" ,"The Answer") + print(f"42 Meaning: {pm.get(42 ,'meaning')}") + # Expect: The Answer + + # Annotating a Tuple + my_key = (1 ,2) + pm.set(my_key ,"color" ,"invisible") + print(f"Tuple Color: {pm.get((1 ,2) ,'color')}") + # Expect: invisible + + print("\n--- Success ---") + return 0 + + except Exception as e: + print(f"Test Failed: {e}") + return 1 + + +def CLI(args_seq) -> int: + """ + The CLI Entry Point. + """ + return test_everything() + + +if(__name__ == "__main__"): + sys.exit(CLI(sys.argv)) diff --git a/developer/tool/env b/developer/tool/env index 0b993ad..610170c 100644 --- a/developer/tool/env +++ b/developer/tool/env @@ -1,3 +1,6 @@ #!/usr/bin/env bash script_afp=$(realpath "${BASH_SOURCE[0]}") +export PYTHONPATH="$REPO_HOME/developer/authored:$PYTHONPATH" + + diff --git a/nohup.out b/nohup.out deleted file mode 100644 index e69de29..0000000 diff --git a/temp.sh b/temp.sh deleted file mode 100644 index c166d29..0000000 --- a/temp.sh +++ /dev/null @@ -1,11 +0,0 @@ -2025-11-25 09:33:05 Z [subu:developer] Thomas_developer@StanleyPark -§/home/Thomas/subu_data/developer/subu_data/Harmony§ -> find . -type l -exec ls -l {} \; -lrwxrwxrwx 1 Thomas_developer Thomas_developer 35 Nov 25 09:08 ./tool/sync -> ../tool_shared/authored/sync/CLI.py -lrwxrwxrwx 1 Thomas_developer Thomas_developer 3 May 19 2025 ./shared/third_party/Python/lib64 -> lib -lrwxrwxrwx 1 Thomas_developer Thomas_developer 16 May 19 2025 ./shared/third_party/Python/bin/python3 -> /usr/bin/python3 -lrwxrwxrwx 1 Thomas_developer Thomas_developer 7 May 19 2025 ./shared/third_party/Python/bin/python -> python3 -lrwxrwxrwx 1 Thomas_developer Thomas_developer 7 May 19 2025 ./shared/third_party/Python/bin/python3.11 -> python3 -lrwxrwxrwx 1 Thomas_developer Thomas_developer 15 Nov 24 15:19 ./shared/authored/git-empty-dir/source_sync -> ../source_sync/ -lrwxrwxrwx 1 Thomas_developer Thomas_developer 25 Nov 24 15:21 ./shared/authored/git-empty-dir/Harmony.py -> ../source_sync/Harmony.py -lrwxrwxrwx 1 Thomas_developer Thomas_developer 37 Nov 24 15:22 ./shared/authored/git-empty-dir/load_command_module.py -> ../source_sync/load_command_module.py