ORE Studio Variability Model

Table of Contents

This document describes how MASD variability is instantiated in ORE Studio's code generator. For the underlying concepts — features, profiles, configuration scopes, and the Variability Metamodel (VMM) — see Variability. For the templates that variability acts on, see Facet and the per-TS documents (C++ Technical Space, SQL Technical Space, Other Technical Spaces).

What variability is — and is not

MASD reserves variability for non-structural variability: configuration that does not change the object graph, only how a fixed graph is projected into physical artefacts. Structural differences — different entities, attributes, keys, types — are the job of the logical model, not the variability model. Keeping the two apart is the single most important discipline in this document; conflating them is what makes a variability model rot into a second, shadow copy of the entity model.

The VMM has two responsibilities, and this document is organised around them:

  1. Physical-space activation — enabling and disabling regions of the physical space: which facets (and therefore which templates) are generated for a given model. ORE Studio realises this with facet_catalogue.org.
  2. Projection configuration — tuning how an enabled projection renders a fixed entity: naming, extensions, and feature toggles. ORE Studio realises this with a catalogue of authored knobs on the model — the sql.*, qt.* and licence features below.

An earlier version of this document covered only responsibility (1) and concluded that ORE Studio "does not represent features." That was wrong: the features are responsibility (2) — the authored knobs — and they had simply not been catalogued. The bulk of this document is that catalogue.

The test: authored knob vs computed predicate

Templates branch on a large number of mustache section tags. Only some of them are variability. The mechanical test — verifiable by reading src/generator.py — is:

  • A feature (variability) is a knob the model file authors and the generator reads through (section.get('flag')). Nobody computes it; a human sets it to tune the projection.
  • A structural predicate (not variability) is a value the generator computes from the object graph (entity['has_tenant_id'] = bool(...), field['is_uuid'] = type = 'uuid'=). It is a consequence of the structure, not a choice about projection. These belong to the logical model and are listed under Excluded: structural predicates so they are never mistaken for configuration.

Part 1 — Physical-space activation (facet_catalogue.org)

ORE Studio does not maintain a formal VMM data structure for activation. Instead, projects/ores.codegen/library/facet_catalogue.org serves as the Profile Metamodel (PMM): a flat catalogue mapping every profile name to the Mustache templates it activates and the model types those templates apply to. generator.py reads it directly via _load_profiles_from_org().

The org source format for a leaf profile is:

** <profile-name> :facet:
:PROPERTIES:
:description: ...
:model_types: domain_entity schema
:END:

| Template | Output |
|----------+--------|
| <name>.mustache | projects/ores.{component}/include/... |

Composite aliases (all, all-cpp, non-temporal) use :includes: instead of a templates table and are stereotyped :facet_group: or :component_archetype:.

There is no feature graph and no inheritance chain in the activation layer. The simplicity is deliberate: ORE Studio's physical space is small enough that a flat catalogue is maintainable and legible for both humans and LLMs.

Model types — the scope filter

In MASD's VMM, feature values are set at product / component / element scope. In ORE Studio the primary scope filter for activation is the model type — a tag that classifies each model file and controls which profile templates can apply to it. A profile's model_types list acts as the filter: only a model whose type appears in that list is a valid target for the profile.

Model type is derived from the filename suffix (JSON route) or the #+type: frontmatter field (org-mode route):

Model type Filename suffix Org frontmatter type What it represents
domain_entity *_domain_entity.json ores.codegen.entity Bi-temporal C++ entity with full SQL + C++ stack
schema *_entity.json ores.codegen.entity SQL-only schema entity (legacy JSON route)
table *_table.json ores.codegen.table SQL refdata table (current standard)
junction *_junction.json ores.codegen.junction SQL junction (many-to-many) table
component ores.codegen.component Component scaffold (CMakeLists, stubs, export macros)
field_group ores.codegen.field_group Nested value struct (no DB mapping)
enum ores.codegen.lookup_entity C++ enum class with string conversion
service_registry ores.codegen.service_registry DB service registration (users, grants, accounts)
data PlantUML ER diagrams

This filter prevents, for instance, the component scaffold profile from inadvertently running against entity models, and the domain profile from running against SQL-only table models.

Profile catalogue

All profiles defined in facet_catalogue.org as of this writing:

Profile Model types Templates Description
domain schema, domain_entity 7 Domain struct (header-only), JSON I/O, libfort table adapter
generator schema, domain_entity 2 Test data generator (sample-value builder)
non-temporal-domain domain_entity 3 Non-temporal domain class + JSON I/O
repository schema, domain_entity 6 ORM entity, mapper, CRUD repository (temporal)
non-temporal-repository domain_entity 6 Read-only repository layer (non-temporal)
service schema, domain_entity 2 NATS entity service class
protocol domain_entity, schema 1 NATS messaging protocol types (header-only)
nats-eventing domain_entity, schema 1 NATS domain changed-event struct
nats-handler domain_entity, schema 1 NATS message handler class (header-only)
qt domain_entity 12 Qt MDI window, dialogs, controller, client model, .ui forms
sql domain_entity, junction, schema, table 7 SQL DDL: create, drop, notify trigger, junction, artefact
non-temporal-sql domain_entity 1 SQL schema for non-temporal (simple) tables
enum enum 1 C++ enum class with to_string / from_string
field-group field_group 1 Plain C++ value struct header
component component 10 Component scaffold: CMakeLists, export macros, stubs, tests
component-api component 10 API sub-project scaffold
component-core component 10 Core sub-project scaffold
component-service component 19 Service sub-project: executable entry point, full service wiring
service-registry service_registry 5 DB service registration: users, grants, IAM accounts, roles
plantuml schema, data 1 PlantUML ER diagram
all domain_entity 0 Composite: all facets for a bi-temporal domain entity
all-cpp schema, domain_entity 0 Composite: all C++ facets (domain + repo + service + protocol + generator)
non-temporal domain_entity 0 Composite: all artefacts for non-temporal entities

Profiles with 0 templates are composite aliases — they exist as named shortcuts for CLI use. The generator resolves them by running the constituent profiles in sequence. The alias itself carries no templates; the resolution logic is in src/generator.py.

Profile activation

Profiles are activated explicitly at invocation time via the CLI:

./projects/ores.codegen/codegen.sh generate \
  --component refdata \
  --profile domain

The generator filters the profile's template list by the current model file's model type, then renders each matching template against the model's data. No profile is activated implicitly — every invocation names the profile explicitly.

Component scope is handled by src/codegen/manifest.py: the manifest maps component names to their model discovery roots (models_dir for JSON models; modeling_dir for org-mode model files). It does not assign default profiles. The profile is always specified at the point of invocation — by the developer at the command line or by the skill or compass recipe driving the regeneration.

Product scope — a MASD concept meaning a profile applies across the whole product — is approximated by the composite alias profiles (all, all-cpp, non-temporal) and by skills that bundle common multi-profile invocations into a single command.

Element scope — individual entities can enable or disable specific technical spaces, facets, or archetypes via ores.* properties in their :PROPERTIES: drawer. See Element-scope activation — the supported/target set model for the full address hierarchy, specificity resolution rules, and examples.

Element-scope activation — the supported/target set model

Physical space is a set of sets: technical-space → facet → archetype. Two named sets govern what generates for each entity in each run:

Supported set (S_e)

The supported set S_e is what entity e is capable of generating. It is declared in the entity's :PROPERTIES: drawer via ores.* properties, following Dogen's convention (:masd.cpp.enabled:, :masd.csharp.enabled:):

:PROPERTIES:
:ID: ...
:ores.cpp.enabled: true
:ores.cpp.qt.enabled: false
:END:

The address hierarchy is ores.{technical_space}[.{facet}[.{archetype}]].enabled:

Address Scope Effect on S_e
:ores.sql.enabled: false technical space removes all SQL facets
:ores.cpp.enabled: false technical space removes all C++ facets
:ores.cpp.qt.enabled: false facet removes only qt
:ores.cpp.domain.class_header.enabled: false archetype removes one archetype

Specificity resolution: more-specific addresses override less-specific ones (depth-first). :ores.cpp.enabled: true + :ores.cpp.qt.enabled: false → S_e = all C++ except Qt — mirrors Dogen's property resolution exactly.

Entities with no ores.* properties: S_e = all archetypes matching the model-types filter (backward-compatible; existing models need no change).

Target set (T) and generation set

The target set T is what to generate in this particular run. Default: T = S_e. The optional CLI --address argument overrides T:

# T = S_e — generate everything the entity supports
./projects/ores.codegen/codegen.sh generate --component refdata

# T = all sql archetypes
./projects/ores.codegen/codegen.sh generate --component refdata --address sql

# T = all cpp.qt archetypes
./projects/ores.codegen/codegen.sh generate --component refdata --address cpp.qt

--profile is removed. The entity's ores.* binding IS the profile definition; --address is a generation-time filter, never an expansion.

What generates for entity e = T ∩ S_e.

  • T ∩ S_e = ∅: warning "<entity>: nothing to generate for address <addr>="; run continues.
  • --address value unknown (not in facet catalogue): error.

Technical-space to facet map

The TS→facet map lives in a * Technical spaces section of facet_catalogue.org:

Technical space Member facets
sql sql, non-temporal-sql
cpp domain, generator, repository, service, protocol, nats-eventing, nats-handler, qt, non-temporal-domain, non-temporal-repository

All supported/target set logic lives in codegen/physical_space.py — isolated from dispatch code and covered by a dedicated test suite (tests/test_physical_space.py). This is deliberately complex logic; it must not be diffused across multiple modules.

Part 2 — Projection configuration (the feature catalogue)

This is the part the previous document omitted. Once a facet is activated, its projection is tuned by features — authored knobs the model file sets and the generator reads through. They are grouped into feature bundles, one per facet (plus a cross-facet licence bundle). Each bundle is a cohesive "variability entity": a named set of features that configure one projection.

The features below are exactly those that pass the authored-knob test — every one is read (never computed) in src/generator.py, and none of them adds or removes a logical attribute. Where a knob has a structural consequence (e.g. tenant-scoping changes a column's nullability) it is flagged; those are candidates to migrate into the logical model proper.

Bundle: licence (cross-facet decoration)

Pure projection decoration: the licence header prepended to each generated artefact. One feature per language technical space. Changing it cannot change the object graph.

Feature Type Binds to Effect
cpp_license template/string C++ facets Licence comment block at the top of generated C++
sql_license template/string SQL facets Licence comment block at the top of generated SQL
cmake_license template/string Build facet Licence comment block at the top of generated CMake

Bundle: masd.sql (SQL projection configuration)

Authored on the model's sql section; all read through in generator.py (never computed). These tune the generated DDL, triggers and rules for a fixed entity.

Feature Type Default Effect
system_scope bool false Table is system-tenant-scoped: tenant_id defaults to the system tenant and the insert trigger forces it. Structural consequence: changes the tenant_id column default.
nullable_tenant_id bool false tenant_id is nullable (system records carry NULL); selects the nullable-tenant validation and index variants. Structural consequence: changes tenant_id nullability.
security_definer bool false Emit SECURITY DEFINER set search_path on the trigger function.
extra_checks collection<string> Additional CHECK constraints to emit on the table.
extra_delete_sets collection<string> Extra SET clauses in the soft-delete rule.
soft_fk_validations collection<struct> Soft foreign-key validations to emit in the insert trigger.
fk_copy_validations collection<struct> FK validations that also copy/denormalise fields from the referenced row.
text_code_validations collection<struct> FK validations against a text lookup column.
party_id_from_book_id struct absent Derive and validate party_id / portfolio_id from book_id in the trigger.
party_id_from_session bool false Set party_id from the session GUC app.current_party_id.
tablename string derived Override the generated table name.

Tenant-scoping is a policy, not three booleans. system_scope and nullable_tenant_id are mutually exclusive with the default (standard-tenant) mode; together they form a single enumerated feature — tenant-scoping policy ∈ {standard, system, nullable}. They are the clearest example of structural-leaning configuration: authored as knobs, but with consequences on the schema shape. If they migrate anywhere, it is into the logical model as a tenant-scoping attribute.

Bundle: masd.qt (Qt projection configuration)

Authored on the model's qt section, read through in generator.py. These configure the Qt projection — window chrome, presentation and binding — without touching the domain object graph. (The many computed qt.* values — detail_fields, required_fields, has_badge_columns, domain_class, metadata_start_row, the is_* widget predicates — are not features; see Excluded.)

Feature Type Default Effect
has_pagination bool false Emit pagination controls in the list view
has_uuid_primary_key bool (set) Client treats the PK as a uuid for id handling
has_change_reason_cache bool false Controller constructor accepts a ChangeReasonCache* parameter; each detail-dialog creation site (add, edit, open-version, revert) calls setChangeReasonCache before wiring the dialog. Set to true for entities whose detail dialogs require a reason for every mutation.
window_title string derived MDI sub-window title
icon string derived Window / menu icon resource
settings_group string derived QSettings group for persisted view state
collection_name string derived Display name of the entity collection
key_field string derived Field used as the user-facing key in the GUI
columns collection derived Which columns the Qt views surface
naming/binding knobs string derived item_var, domain_include, protocol_include and the request-class names that bind the projection to the domain and protocol types

Excluded: structural predicates

The following are the values templates branch on that are not variability. Every one is computed by generator.py from the object graph, so it is a structural fact, not a configuration choice. They are listed here so they are never re-imported into the variability model; their home is the logical model.

Group Predicates Computed from
Presence gates has_tenant_id, has_workspace_id, has_tenant_in_pk, has_image_id, has_coding_scheme, has_nullable_coding_scheme, has_display_order, has_multiple_natural_keys, has_uuid_columns, has_badge_columns, has_description_column, has_combo_fields, has_text_edit_fields, has_uuid_detail_fields, has_uuid_left_or_right "does the entity have attribute / key X"
Type predicates is_uuid, is_text, primary_key.is_uuid/is_text, left.is_uuid, right.is_uuid attribute SQL / C++ type
Nullability predicates nullable, is_optional(_string/_uuid/_timestamp), is_nullable_int/_string, is_already_optional, is_tristate attribute nullability
Widget predicates is_line_edit, is_text_edit, is_spin_box, is_check_box, is_dynamic_combo, is_static_combo, is_simple, is_fk, is_key, is_unique field.type
Scope predicates system_tenant_validation, use_system_tenant, use_no_tenant tenant scope of the entity
Derived projection values detail_fields, required_fields, required_dynamic_combo_fields, domain_class, key_widget, metadata_start_row columns / fields of the entity

Model files — the logical element

Each model file is ORE Studio's equivalent of a MASD logical element — the entity that projects into physical artefacts via the generator. Two authoring routes exist:

  • JSON route (legacy): *_table.json, *_domain_entity.json, etc. in projects/ores.codegen/models/<component>/. The model file is a data bag consumed directly by the Mustache renderer.
  • Org-mode route (current, preferred): *.org files in projects/<component>/modeling/ declaring #+type: ores.codegen.* in their frontmatter. The generator discovers these via the component manifest. This route keeps the entity definition, its prose documentation, and its org-roam cross-references in a single file — aligned with the MASD literate approach.

The feature bundles of Part 2 are authored on the model file — in the sql / qt sections (JSON route) or the corresponding org headings (org-mode route). They travel with the element, which is why their natural scope is the element.

Mapping from MASD VMM to ORE Studio

MASD concept MASD definition ORE Studio equivalent
Feature Single configurable property (e.g. masd.cpp.hash.enabled) An authored knob on the model: a sql.* / qt.* / licence feature (Part 2)
Feature bundle Cohesive group of features licence / masd.sql / masd.qt bundles (Part 2)
Profile Named bundle of feature values + activation Split: activation is a facet_catalogue.org entry (Part 1); feature values are authored per element (Part 2)
Base profile Product-wide defaults all / all-cpp / non-temporal composite aliases (activation only)
Component profile Per-component overrides Component manifest (manifest.py) + explicit CLI invocation
Element profile Per-element overrides Individual model file: ores.* property-drawer bindings for activation (Part 1 element scope); authored sql.*=/=qt.* features for projection configuration (Part 2)
VMM Meta-model of the full configuration space facet_catalogue.org (activation) + the feature catalogue in Part 2 (configuration)
Configuration scope Product / Component / Element Activation: explicit per invocation. Configuration: features are authored at element scope

The key difference from full MASD variability remains: ORE Studio has no intermediate feature graph with inheritance and resolution. Activation selects templates directly (Part 1); features are flat knobs read at render time (Part 2). This keeps the system simple to maintain and to hand to an LLM, at the cost of the fine-grained, scoped per-feature resolution that a full VMM enables.

See also

Emacs 29.1 (Org mode 9.6.6)