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:

  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 the ores physical-space graph (root → technical space → facet → archetype).
  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 features on the model — the ores.sql.schema, ores.cpp.qt and 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_scoping is read nowhere outside library/templates/doc_entity_org.org.mustache itself — 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_keys and has_insert_trigger_validations as literal boolean feature names are likewise not read as such by the generator: foreign_keys and insert_trigger are structural sections/lists derived from what the model author actually writes under the Foreign keys=/=Insert trigger headings, 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_id was originally listed here too; it is not scaffold-only — it is read generically, like every Flags drawer 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. 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 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

Emacs 29.3 (Org mode 9.6.15)