# Structive Design Philosophy and Principles [中文](DESIGN.zh-CN.md) ## 1. Purpose Structive exists to give ordinary C++ object types a machine-readable structural layer without forcing those types into a framework-specific storage model. The library should make it possible to ask questions such as: - Which members are part of the public structural model? - What is the stable runtime key of a property? - Which properties are externally readable or writable? - Which properties participate in persistence? - Which metadata belongs to presentation, validation or another extension? - Which properties share a synchronization domain? - How can generic runtime code read or write a property safely with respect to declared capabilities? It should answer those questions while leaving the underlying type recognizably C++. ## 2. Primary principle: enhance, do not replace Structive is not a replacement object model. It is a structural layer attached to an existing C++ type. The preferred shape is: ```cpp struct Device : structive::Property_Object { double temperature; std::string name; }; ``` not: ```cpp struct Device { Framework_Property temperature; Framework_Property name; }; ``` This principle protects several properties of normal C++ code: 1. Members remain real members. 2. Member pointers remain meaningful identities. 3. Raw access remains possible where ownership rules permit it. 4. Unregistered members can remain implementation details. 5. Generic Structive metadata can evolve independently from storage representation. The cost of this principle is intentional: Structive cannot intercept raw member access. Managed behavior only applies when callers use the managed path. ## 3. Two layers, not one Structive separates type-level description from instance-level management. ### 3.1 Type layer `Type_Descriptor` produces an `Object_Schema` describing: - registered properties; - declared property keys; - accessors; - Attributes; - constraints; - object-level inheritable defaults; - a default synchronization plan. This is the structural definition of the type. ### 3.2 Instance layer `Property_Object` provides: - resolved lock-slot topology; - per-instance mutex storage; - managed typed reads and writes; - capability views; - static and runtime multi-property guards; - traversal over managed values; - type-erased runtime access through `Property_Object_Base`. This is instance behavior, not schema identity. ### 3.3 Design rule Do not move instance state into the schema, and do not make schema metadata depend on one particular instance-management policy. A future user may want to describe a large number of plain objects without paying per-object synchronization cost. The architecture should continue to leave that possibility open. ## 4. Registration is explicit Structive does not assume every C++ member belongs to the structural model. ```cpp struct Device : structive::Property_Object { int id; double temperature; mutable int internal_cache; }; ``` If only `id` and `temperature` are registered, `internal_cache` is invisible to Structive. This is deliberate. Structural exposure is an API decision and should not be inferred from physical layout. ### Rule **The schema is the public structural contract; the struct layout is not automatically the schema.** ## 5. Prefer compile-time identity inside C++ Business C++ code should normally identify member-backed properties by member pointer: ```cpp device.read<&Device::temperature>(); device.write<&Device::temperature>(30.0); device.schema().property<&Device::temperature>(); ``` This gives the compiler the strongest relationship between the object type and the selected field. Numeric indexes are useful for generic compile-time traversal. String keys are useful at runtime boundaries. ### Identity hierarchy ```text business C++ code → member pointer compile-time generic code → property index runtime/adapters → declared string key ``` Do not unnecessarily convert compile-time code to strings merely because a key exists. ## 6. Keys are structural protocol identifiers Every `Property_Descriptor` requires a non-empty `key` Attribute. Keys must be unique inside one schema. A key is not merely a UI label. It is the runtime identity used by schema lookup, dynamic lock selection and runtime access. Changing a key may therefore change an external protocol or persistence contract even when the C++ member name stays unchanged. ### Rule **Treat property keys as protocol-level names. Rename them deliberately.** ## 7. One Attribute system Structive has one Attribute protocol. It should not grow parallel metadata channels such as “hint”, “annotation”, “UI metadata” and “serializer metadata” with separate storage machinery. An Attribute may declare: - `attribute_category` for category-based lookup; - `single_valued` when only one Attribute of that category may appear in a declaration; - `inheritable` when it may participate in object defaults; - any payload required by the category owner. Core and extensions use this same mechanism. ### Rule **Add semantics by adding an Attribute category and an interpreter, not by adding a second metadata framework.** ## 8. Category ownership Core should interpret only categories that belong to Core. For example, Core understands access and persistence capability because those affect Core-managed views. It does not need to understand `presentation::label`. Presentation owns presentation semantics. A future JSON extension should own JSON-specific semantics. An RPC extension should own RPC-specific semantics. The Core still stores and traverses all Attributes uniformly. ### Dependency rule ```text extension implementation ↓ Structive Property Core ``` Never reverse this dependency merely to make an extension convenient. ## 9. Defaults are inheritance, not hidden mutation `defaults(...)` supports inheritable Attribute categories. Property declarations remain able to override those defaults. Current Core inheritable categories include external access, persistence access and sensitivity. This mechanism should be used for stable object-wide policy defaults, not as a general mechanism for implicit behavior. ### Rule **Defaults should reduce repetition without making a property’s effective policy impossible to determine from schema rules.** ## 10. Managed access and raw access are distinct contracts Raw access: ```cpp device.temperature = 30.0; ``` Managed access: ```cpp device.write<&Device::temperature>(30.0); ``` The raw path is normal C++. It does not acquire Structive locks or enforce Structive capability views. The managed path uses the descriptor and synchronization topology. This duality is intentional rather than accidental. ### Rule Do not pretend that inheriting `Property_Object` turns public C++ members into encapsulated properties. If a subsystem requires managed synchronization, its coding rules must require the managed path. ## 11. Capabilities are views, not copies of the object model The same property may have different visibility under different managed modes: ```text internal external persistence ``` Core derives those capabilities from Attributes and the accessor’s actual read/write abilities. An external view should not become a second schema. A persistence view should not become a second schema. They are projections over one schema. ### Rule **One property definition, multiple capability projections.** ## 12. Validation is metadata plus an explicit operation Structive constraints describe candidate validity, but `write()` does not automatically run them. This separation is essential because these are different concerns: ```text single-property constraint cross-property invariant locking transaction boundary rollback strategy error reporting side effects ``` A generic `write()` cannot correctly guess all of them. ### Example ```cpp auto guard = device.lock_unique<&Device::min_speed, &Device::max_speed>(); auto old_min = guard.get<&Device::min_speed>(); auto old_max = guard.get<&Device::max_speed>(); guard.set<&Device::min_speed>(candidate_min); guard.set<&Device::max_speed>(candidate_max); if (candidate_min > candidate_max) { guard.set<&Device::min_speed>(old_min); guard.set<&Device::max_speed>(old_max); } ``` The business operation owns the invariant and rollback semantics. ### Rule **Do not turn `write()` into a hidden transaction engine.** ## 13. Synchronization is synchronization only A synchronization plan maps properties to lock slots. It supports independent, shared, unsynchronized and grouped configurations. The purpose of a group is to define a consistency domain: properties in the group share a mutex slot. Multi-property lock operations deduplicate slots and acquire them in stable slot order. ### Synchronization does not mean - validation; - transaction; - rollback; - event emission; - dirty tracking; - persistence commit. Those may be built above Structive, but should not be silently coupled to locking. ## 14. Computed properties must have explicit consistency boundaries `computed_property` reads through a synchronized view. The view only permits access to properties in the same resolved lock slot. Therefore a computed property that depends on `min_speed` and `max_speed` should share their synchronization group: ```cpp synchronization( sync_all_independent, sync_group("speed", "min_speed", "max_speed", "speed_span") ) ``` This makes consistency explicit rather than relying on a getter that casually reads unrelated fields. ### Trusted accessors `trusted_computed_property` and `trusted_accessor_property` invoke trusted member functions on the object. They bypass synchronized-view dependency checking by design. ### Rule **Use synchronized computed properties by default. Use trusted accessors only when the accessor itself owns or guarantees the required synchronization semantics.** ## 15. Runtime access is a boundary feature `Property_Object_Base` intentionally provides a type-erased runtime interface using: - string key; - `Managed_Access_Mode`; - `std::type_info`; - explicit result codes. This is appropriate for adapters that do not know the concrete type at compile time. It is not a reason to make normal typed C++ code dynamic. ### Rule **Keep compile-time code compile-time; cross dynamic boundaries only where the application actually has a dynamic boundary.** ## 16. Runtime errors and compile-time errors have different jobs Structive uses compile-time rejection where the selection is statically known: - missing member registration; - duplicate keys; - duplicate member storage identity; - invalid Attribute category duplication; - non-inheritable Attributes inside `defaults(...)`; - impossible access capability for an accessor. Runtime failures are reserved for runtime inputs and runtime configuration: - unknown dynamic key; - inaccessible dynamic property; - invalid synchronization plan key; - duplicate runtime synchronization configuration; - runtime type mismatch. ### Rule **Do not defer a statically knowable schema error to runtime. Do not force dynamic adapter input into compile-time machinery.** ## 17. Synchronization policy should remain replaceable `Property_Object` supports a lock policy, with `Shared_Mutex_Policy` as the default and `No_Lock_Policy` available. This is an important architectural seam. Core semantics should not become inseparable from one specific mutex type or one global scheduler. `No_Lock_Policy` removes actual mutual exclusion; it does not magically make concurrent access safe. ## 18. Extension design rules A good Structive extension should: 1. Define categories that belong to its own domain. 2. Reuse the Core Attribute protocol. 3. Interpret only the categories it owns or explicitly depends on. 4. Depend on Core, never require Core to depend on it. 5. Keep domain-specific fallback behavior inside the extension. 6. Prefer reading the existing schema over duplicating a parallel descriptor tree. 7. Avoid changing Core behavior merely because an adapter needs a convenience rule. ## 19. Non-goals Structive Core should not become all of the following at once: - an ORM; - a JSON library; - a GUI framework; - an RPC framework; - a signal/slot system; - a transaction engine; - a general-purpose reflection replacement for every language feature. Structive should provide a strong structural contract that those systems can consume. ## 20. Evolution rules When changing the library, apply these questions in order: 1. Does this belong to the structural model, instance management, or an extension? 2. Is this new concept really an Attribute category rather than a new metadata channel? 3. Can the error be detected at compile time? 4. Does the change preserve native C++ member semantics? 5. Does it preserve the distinction between raw and managed access? 6. Is synchronization being mixed with validation, transaction or events? 7. Does Core now know something that should belong to an extension? 8. Does the API still make member pointers the natural typed identity? 9. Does a new convenience API duplicate an existing semantic path? 10. Does the change preserve existing function-signature meaning rather than silently redefining it? If a feature fails these questions, reconsider its layer before implementing it. ## 21. Architectural summary Structive should remain a small number of strong concepts rather than a large number of magical conveniences: ```text ordinary C++ type + explicit schema + one Attribute model + explicit capabilities + explicit validation + explicit synchronization + optional managed instance behavior + extension-owned interpretation ``` The library is strongest when generic systems can understand a type without taking ownership of that type.