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:
- 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. - 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. --addressvalue 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. inprojects/ores.codegen/models/<component>/. The model file is a data bag consumed directly by the Mustache renderer. - Org-mode route (current, preferred):
*.orgfiles inprojects/<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
- Variability — the MASD variability concept: features, profiles, VMM.
- Facet — the MASD facet concept the profiles activate.
- Applied MASD — the applied overview, including the full ORE Studio facet catalogue.
- ORE Studio Technical Spaces — TS/Part/Facet/Archetype index.
- C++ Technical Space — C++ profile-to-facet mapping.
- SQL Technical Space — SQL profile-to-facet mapping.
- ores.codegen — the code generator implementing this model.