ORE Studio Variability Model
Table of Contents
Within MASD, the Variability Metamodel (VMM) is the fourth of the methodology's four metamodels — alongside the Logical Metamodel, the Physical Metamodel, and the Codec Metamodel — and it is the one concerned specifically with non-structural variability: the dimension of a code generator's behaviour along which a fixed logical model can still be projected in more than one way, because some aspect of the projection was left as a choice for the model's author rather than fixed by the model's shape. Where the Physical Metamodel defines the full space of which artefacts could exist — the complete TS→Part→Facet→Archetype geometry, fixed for the product — the VMM decides, for a given entity, which of those possible artefacts actually do exist (an archetype can be switched off entirely) and, for each one that does, how it is rendered. Both are non-structural: neither changes the entity's logical shape, only whether and how that fixed shape is projected. This document describes how that abstract metamodel is instantiated concretely in ORE Studio's own code generator — which features exist, what each one does, and how the two responsibilities the VMM assigns to any of its instantiations (enabling parts of the physical space, and configuring how an enabled part is projected) are actually discharged here. For the underlying, product-agnostic concepts the instantiation below draws on — feature, feature bundle, profile, binding point, configuration scope — see Variability, which stays deliberately silent on anything specific to this product. For the templates that variability acts upon, see Facet and the per-TS documents (C++ Technical Space, SQL Technical Space, Other Technical Spaces).
Non-structural variability in ORE Studio
This document is the catalogue: the concrete answer to which features ORE Studio's code generator actually recognises, what each one does, and how the two responsibilities Variability assigns to any VMM instantiation — activation and projection configuration — are discharged here specifically. For the abstract distinction between non-structural variability and structure, and for the mechanical test that tells a genuine feature apart from a value the generator merely computes, see Variability § "Distinguishing a feature from a computed predicate"; that distinction is the single most important discipline this document follows, and is not restated here.
ORE Studio discharges the VMM's two responsibilities as follows:
- 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 the ores physical-space graph (root → technical space → facet → archetype).
- 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 features on the
model — the
ores.sql.schema,ores.cpp.qtand licence features below.
Both responsibilities are authored the same way, mechanically: as
key-value pairs in an org-mode property drawer — the :PROPERTIES:
... :END: block org-mode allows immediately under any heading, holding
metadata that is distinct from, and never rendered as part of, that
heading's own prose body. ORE Studio's entity model files use this
mechanism pervasively, and everywhere this document shows a
:some_property: value line, it is this mechanism it is pointing at.
A model file's top-level heading carries the property drawer that
activation reads — its ores.* bindings, introduced below — and each
facet-specific section of the same file, its SQL heading, its Qt
heading, carries its own property drawer of the projection-configuration
features that facet's own templates read. org_loader.py parses these
drawers directly when it loads a model; a feature's authored value lives
nowhere else in the file, and nowhere else in the toolchain.
Telling variability from structure
Applying the abstract test to a concrete generator requires knowing
where to look, and it is worth being precise about that: the operative
source is projects/ores.codegen/src/codegen/core.py, org_loader.py,
and the library/templates/*.mustache templates themselves. What
actually distinguishes a feature from a computed predicate, though, is
not which file happens to consult its value — it is the direction the
value travels. A feature's value flows from the model's author to the
generator unchanged, a plain read-through with no computation in
between; a computed predicate's value flows the other way, derived by
the generator from other facts the entity already carries, with no
human choice involved at all. That distinction does not always show up
as a Python accessor a reader can grep for, which is itself worth
knowing before hunting for one: several features are never touched by
name in core.py or org_loader.py, and are read instead purely by
Mustache's own dotted-path section lookup, direct off the parsed
model — domain_entity.qt.has_change_reason_cache, for instance,
resolves entirely inside the template, with no corresponding
Python-side .get() call to find. A computed predicate, by contrast,
always shows up as a derivation in the source: table['has_tenant_in_pk']
is set from has_tenant_id together with a check on system_scope,
and field['is_uuid'] is set from the column's own declared type — in
both cases the generator is computing something from facts the entity
already has, not consulting a choice its author made. Predicates of
this kind are catalogued separately, under Excluded: structural predicates, precisely so a reader
is never tempted to read one of them as configuration.
The pair :has_tenant_id: and :has_workspace_id: (documented on
Keys and columns) is the case where this test is easiest to apply
wrongly, and working through it in full is instructive precisely
because of that. Both properties look exactly like computed predicates
at first glance: setting either one causes a real tenant_id=/
=workspace_id column to appear in the generated table, and a reader
who equates "produces a column" with "is structural" will file them
under Excluded without checking further. That equation is exactly the
mistake the abstract test exists to prevent — structure is measured by
where a value came from, not by what it goes on to produce. Read
against the generator's actual source, both properties are read
straight off the model's frontmatter, with nothing more than a default
applied (bool(table.get('has_tenant_id', True))); no other fact about
the entity feeds into that value. They are therefore genuine, authored
features by the test's own definition, however structural their
consequence looks, and are documented on Keys and columns rather than
under Excluded. The instructive contrast is has_tenant_in_pk, which
looks similar on the surface but fails the test in exactly the way
has_tenant_id passes it: its value is computed from has_tenant_id
together with other already-known facts, never authored directly, and
it is correctly excluded as a result.
Activation: an authored feature at facet granularity
An entity's ores.* property-drawer bindings are a genuinely authored
feature, no different in kind from anything the projection-configuration
catalogue below defines — an entity's author declares which technical spaces, facets, or
individual archetypes apply to it. Everything about how that
declaration is then resolved at build time — CLI address syntax, the
target-set/supported-set intersection, model-type filtering, and which
facets exist under which technical space — is physical-space addressing
mechanism, not itself an authored per-entity choice, and is documented
in Applied MASD § "Address resolution: the physical-space graph and
element-scope binding".
The 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
— see Applied MASD § "Address" for what an address is and how it addresses
a point in physical space:
| 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 \(\to S_e =\) all C++ except Qt — mirrors
Dogen's property resolution exactly.
Entities with no ores.* properties: \(S_e\) is every archetype the
model-type filter admits — see Applied MASD for how a CLI invocation's
target set is resolved against \(S_e\) to decide what actually generates
in a given run.
Projection configuration: how an existing artefact family is shaped
Once a facet is activated, its projection is tuned by features —
authored configuration points 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, corresponding one-to-one with the facet whose rendering it
tunes — the ores.sql.schema bundle below configures exactly what the
ores.sql.schema facet produces, and likewise for ores.cpp.qt. The
licence bundle is the one exception, and deliberately so: licence
decoration is not tied to any single facet's own semantics, so it
stays a cross-facet bundle rather than living inside one.
Every feature below passes Variability § "Distinguishing a feature from a computed predicate" — each is read, never computed, in the generator (§ "Telling variability from structure" above names which files), and none of them adds or removes a logical attribute on its own account. Where a feature 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: ores.sql.schema (SQL projection configuration)
Authored on the model's sql section; all read through in
codegen/core.py=/=org_loader.py (never computed). These tune the
generated DDL, triggers and rules for a fixed entity. Full narrative,
worked examples, and the tenant-scoping-is-a-policy note: Codegen entity
meta-model — SQL § "Projection configuration (variability)" — this
table is the cross-facet index only.
| Feature | Type | Default |
|---|---|---|
has_parent_id |
bool | false |
system_scope |
bool | false |
nullable_tenant_id |
bool | false |
extra_checks |
collection<string> | ∅ |
extra_delete_sets |
collection<string> | ∅ |
fk_copy_validations |
collection<struct> | ∅ |
text_code_validations |
collection<struct> | ∅ |
party_id_from_book_id |
struct | absent |
party_id_from_session |
bool | false |
Bundle: ores.cpp.qt (Qt projection configuration)
Authored on the model's qt section, read through in codegen/core.py
and, for several knobs, directly by Mustache's dotted-path section lookup
in the library/templates/cpp_qt_*.mustache templates (no explicit Python
.get() call at all — see § "Telling variability from structure" above).
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.) Full narrative, the silent-omission failure mode, and a
worked example: Codegen entity meta-model — C++ Qt — this table is the
cross-facet index only.
| Feature | Type | Default |
|---|---|---|
has_pagination |
bool | false |
has_uuid_primary_key |
bool | false |
has_change_reason_cache |
bool | false |
has_explorer_api |
bool | false |
parent_entity_singular |
string | (unset) |
has_csv_xml_io |
bool | false |
has_export_macro |
bool | false |
has_version_navigation |
bool | false |
has_readonly_paginated_list |
bool | false |
has_parent_scoped_list |
bool | false |
parent_key_field=/=parent_key_param |
string | required if has_parent_scoped_list |
window_title=/=icon=/=settings_group=/=collection_name=/=key_field=/=columns |
string/collection | derived |
| naming/binding knobs | string | derived |
Scaffold-only features (not generation-time features)
projects/ores.codegen/src/doc_generate.py's compass add entity_org
--shape=/–entity-<knob>= flags use a feature vocabulary
(ENTITY_ORG_KNOBS) that only partly matches the feature catalogue above.
Checked against core.py=/=org_loader.py=/=library/templates/*.mustache:
has_parent_scopingis read nowhere outsidelibrary/templates/doc_entity_org.org.mustacheitself — it only decides which example boilerplate the scaffold pre-populates into a freshly created model file, and has no effect at generation time on an entity once authored. It is not a feature in the sense this document defines.has_foreign_keysandhas_insert_trigger_validationsas literal boolean feature names are likewise not read as such by the generator:foreign_keysandinsert_triggerare structural sections/lists derived from what the model author actually writes under theForeign keys=/=Insert triggerheadings, not flags a generated entity toggles independently of that content. The scaffold flags of the same name only control which example heading/section the scaffold writes into the new file. (has_parent_idwas originally listed here too; it is not scaffold-only — it is read generically, like everyFlagsdrawer property, and is catalogued as a real feature above.)
Not yet reconciled into a single vocabulary; tracked by Entity classification and drift baseline across all components.
Excluded: structural predicates
The following are the values templates branch on that are not
variability. Every one is computed by codegen/core.py from other,
already-known facts about the entity — reflecting the structure of
the types the facets will generate, not a choice about how to project
them — so each 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. has_tenant_id,
has_workspace_id, and has_display_order are genuine authored
features — all read verbatim from frontmatter with no derivation from
other facts — and so do not belong on this list;
has_tenant_id=/=has_workspace_id are documented on Keys and
columns. See Variability § "Distinguishing a feature from a computed
predicate" for the test applied here. has_image_id and
has_coding_scheme are split-personality:
for domain_entity=/=table models (the row below) they are genuinely
computed in core.py; for lookup_entity models the same-named
frontmatter key is instead read verbatim in org_loader.py
(_LOOKUP_ENTITY_BOOL_SCALARS) — a feature for that metatype, not a
predicate. Which one applies depends on the model's metatype.
| Group | Predicates | Computed from |
|---|---|---|
| Presence gates | has_tenant_in_pk, 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, has_qt |
"does the entity have attribute / key X" — has_qt is truthiness of whether a ** Qt heading was authored at all, not a settable value |
Presence gates (domain_entity=/=table only) |
has_image_id, has_coding_scheme, has_nullable_coding_scheme |
computed in core.py from other fields (image_id, coding_scheme) — for these two model types only |
| 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 |
Profiles: naming a recurring configuration point
Variability defines a profile as "a bundle of configuration points
that can be bound to logical elements… named after the ability they
confer," composing Base → Component → Element scope. This entity's own
ores.* facet address instantiates this at the activation half of the
VMM. Profiles instantiate the other half: named bundles of feature
values — the ores.sql.schema=/=ores.cpp.qt features above plus Keys
and columns's has_tenant_id=/=has_workspace_id — bound at element
scope when authoring a new entity.
This supersedes projects/ores.codegen/src/doc_generate.py's
ENTITY_ORG_SHAPE_PRESETS (the compass add entity_org --shape
presets: simple-text-key, fk-scoped, hierarchical-composite,
richest, timeseries). That scheme sampled 5 points from the feature
space and named each after whichever reference entity happened to sit
there — the samples were never mutually exclusive (a self-referencing
hierarchy and CSV/XML export + version navigation can genuinely
co-occur; the old scheme just never sampled that combination) and two
of the five features it toggled (has_parent_scoping, boolean-shaped
has_insert_trigger_validations) turned out not to be real
generation-time features at all (§ "Scaffold-only features" above).
The axes
Rather than named archetypes, a profile is a point along independent axes — pick one value per axis, freely, with no exclusivity between axes. Each axis has a small, closed vocabulary:
| Axis | Values | Composed from |
|---|---|---|
| Tenancy scope | global / tenant / tenant-workspace | has_tenant_id, has_workspace_id (Keys and columns) |
| SQL tenant-scoping policy | standard / system-scoped / nullable-tenant | system_scope, nullable_tenant_id (ores.sql.schema; already an enumerated feature per the note above, not two independent booleans) |
| Key shape | text-natural-key / uuid-surrogate-key | has_uuid_primary_key (ores.cpp.qt) |
| Qt richness tier | none / basic / standard / rich | no ** Qt heading authored at all, or one present plus an increasing set of has_change_reason_cache=/=has_pagination=/=has_csv_xml_io=/=has_version_navigation=/=has_explorer_api=/=has_export_macro (ores.cpp.qt) |
| List mode | full-crud / readonly-paginated | has_readonly_paginated_list (ores.cpp.qt) |
| Parent relationship | none / explorer-child / parent-scoped-list | parent_entity_singular=+=has_explorer_api, or has_parent_scoped_list=+=parent_key_field=+=parent_key_param (ores.cpp.qt; the two are mutually exclusive with each other but each independent of every other axis here — has_parent_scoped_list is only meaningful paired with readonly-paginated above, per C++ Qt) |
Full effect of every feature referenced above: the ores.sql.schema=/=ores.cpp.qt
bundle tables and their linked segment pages (Codegen entity meta-model
— SQL, Codegen entity meta-model — C++ Qt) — this section names axis
values, it does not redefine what they do. The axes are a conceptual
map of the space for a human reader; the concrete profile docs below
express their assignments as literal feature names, not axis labels —
no translation layer between what a profile says and what a parser
would read.
Profiles
Each profile is its own document, anchored to a real commissioned entity: Variability Profiles is the routing index. The catalogue is deliberately not exhaustive — a profile is added only once a genuinely new combination recurs across more than one entity; a profile catalogue with one member per row is just a list of entities wearing a costume.
Appendix
Reference material: not part of the argument above, consulted as needed.
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 catalogued above 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. Dogen's generic masd.cpp.hash.enabled) |
An authored feature on the model: a ores.sql.schema / ores.cpp.qt / licence feature |
| Feature bundle | Cohesive group of features | licence / ores.sql.schema / ores.cpp.qt bundles |
| Profile | Named bundle of feature values + activation | Split: activation is an ores.* facet address; feature values are authored per element |
| Base profile | Product-wide defaults | A technical-space or root address (ores.cpp, ores) (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; authored ores.sql.schema=/=ores.cpp.qt features for projection configuration |
| VMM | Meta-model of the full configuration space | ores.* physical-space graph (activation) + the feature catalogue (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; features are flat, read at render time with no scoped resolution chain. 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 — address resolution (CLI syntax, target/supported-set intersection, the technical-space-to-facet map) and 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.