Task: Codegen per-entity history field mapper
Table of Contents
This page documents a task in the Consolidate history dialogs onto HistoryDialogBase story. It captures the goal, current status, acceptance, and any notes or results.
Goal
Add a new presentation codegen facet: render_{entity}_fields(), a
plain, generated (not hand-written, not runtime-reflective) per-entity
function converting a domain value to an ordered
ores::diff::domain::field_value list, per
History Diff Architecture layer 1.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Consolidate history dialogs onto HistoryDialogBase |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing — task closed. |
| Last touched | 2026-07-12 |
Acceptance
- New
presentationfacet (ores.cpp.presentation, both the literate doc group and the physical-space address node) declares/implementsrender_{entity}_fields(const domain::{entity}&)per entity. - Generated for the same 27
ores.refdataentities asentity_type_of()(task 352B308F) — the established clean baseline for this story; the same 7 drifted entities stay excluded. - One
field_valuepush per field: primary key, natural keys, columns, audit columns,recorded_at. Labels are mechanical title-case, not hand-curated. - Handles every
cpp_typeactually present across the 27 entities:std::string,std::optional<std::string>,int,std::optional<int>,double,std::optional<double>,bool,std::optional<bool>,boost::uuids::uuid,std::optional<boost::uuids::uuid>, plusrecorded_at's fixedstd::chrono::system_clock::time_pointvia the existingores::platform::time::datetime::to_iso8601_utc(). - Local build clean (full repo,
-j3); full ctest suite green (69/69 suites, 215s).
Plan
- Investigated whether the existing per-column type flags
(
is_uuid=/=is_nullable_string=/=is_already_optional=/ =is_plain_string, used by the repository/SQL facets) could be reused directly for the mapper. They could not — see Notes for why; this cost a full round of regenerate → compile-fail → diagnose → fix. - Added a new, independent set of
render_is_*column flags incore.py, derived directly from each column's rawcpp_typestring (the same ground truth the domain class template itself emits verbatim) —render_is_string,render_is_optional_string,render_is_bool,render_is_optional_bool,render_is_int,render_is_optional_int,render_is_double,render_is_optional_double,render_is_uuid,render_is_optional_uuid,render_is_timestamp,render_is_optional_timestamp. Also addedrender_label(mechanical title-case) tocolumns,natural_keys, andprimary_key— none had a label before (the closest existing thing, the QtDetail Fieldstable's curatedlabelcolumn, is about widget wiring and isn't guaranteed present for every entity). - Added the
ores.cpp.presentationfacet: two archetypes (cpp_history_field_mapper.hpp/cpp.mustache, tangled fromores.cpp.presentation.history_field_mapper_header/impl.org), a facet-group literate doc (cpp_presentation.org), afacet_catalogue.orgentry, and the separate physical-space address node (ores.cpp.presentation.org) the--addressresolution mechanism requires (distinct from the literate group doc — see Notes). - Proved the facet on
currencyalone first viacodegen generate --model ... --profile ores.cpp.presentation(note:--profile <name>only works when<name>is the realores.cpp.<facet>address, not a bare facet-catalogue heading — see Notes). - First attempt used the wrong flags (repository/SQL layer's
nullable-promotion flags) and produced code that failed to compile
against
book(uuid primary key rendered as a plain string; nullable text columns with an explicit non-optionalcpp_typeoverride called.value_or()on a plainstd::string). Root- caused (see Notes) and replaced with therender_is_*flags above. - Regenerated all 27 clean entities via
codegen regenerate --component refdata-cpp --profile ores.cpp.presentation; removed the 7 known-drifted entities' output to stay consistent with the established baseline. - Added
ores.diff.libtoores.refdata.core's link libraries (public, sincefield_value.hppis now in a public header). - Added
projects/ores.refdata/core/tests/presentation_currency_history_field_mapper_tests.cppcovering field count/uniqueness, string/int rendering, optional- uuid (both empty and populated), andrecorded_atformatting. - Verified: full local build clean, full ctest suite green (69/69).
Notes
Why the repository/SQL layer's type flags don't apply to the domain
class. is_uuid=/=is_nullable_string=/=is_already_optional=/
=is_plain_string encode a nullable-to-std::optional-promotion
decision that only the repository entity and SQL facets apply. The
domain class template (cpp_domain_type_class.hpp.mustache) does
not apply that promotion — it emits \{\{\{cpp_type\}\}\} verbatim.
A column can have :nullable: true in its model but an explicit
:cpp_type: std::string override, and the domain struct gets a plain
std::string field, not std::optional<std::string> — exactly what
happened with book's description=/=gl_account_ref=/=cost_center,
and what made the first implementation attempt fail to compile. The
render_is_* flags fix this by deriving straight from the raw
cpp_type string — the same source of truth the domain class template
itself uses — rather than from the derived nullable-promotion flags.
Two separate facet-registration files are needed for a new facet:
the literate group doc (e.g. cpp_presentation.org, human-facing,
indexes archetypes) is not the same thing generator.py's
--address resolution reads. That reads a physical-space address
graph built from files matching ores.*.org in the templates
directory (ores.cpp.presentation.org here), each declaring
#+type: facet, #+model_types:, and #+facet_group:. Forgetting
this file produces "unknown address" errors that look like a typo but
aren't. --profile <name> only works as a bare facet name for the
small set of legacy curated profiles (domain, sql, all-cpp,
…); any other name is treated as a literal address and must be the
full ores.cpp.<facet> form.
Regenerating a new facet is comparatively low-risk for drift (no prior generated files exist to diff against — every output is a new file), unlike modifying an existing facet's template. The risk here is instead per-entity field-type coverage gaps, not silent drift.
Scope not covered by this facet (deferred, matching precedent from entity_type_of and the redesign task):
has_identity_group=/=has_audit_groupentities (none of the 27 covered entities use them).- Enum columns (
is_enum; none present in the 27 covered entities). - The 7 entities excluded from
entity_type_of()for pre-existing model drift (business_unit,contact_type,currency_market_tier,monetary_nature,party,party_id_scheme,rounding_type) —partyin particular is a known gap the generic-history-subject task inherits. - Junction models (not in the
domain=/=presentationfacets'model_typesscope at all).
Test Scenarios
Manual QA scenarios (scaffolded via compass add test_scenario, run
through the QA Validation Runner panel) that verify this task. Link
new ones here as they're created; the scenario doc itself links back
via its "Verifies task" field.
| Scenario | State | Notes |
|---|---|---|
PRs
| PR | Title |
|---|---|
| #1513 | [refdata,codegen] Add history field mapper facet |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | CI drift check failed: cpp_group.org facet inventory not regenerated after adding the presentation facet | cpp_group.org | Fixed | Ran regenerate_facet_inventories.py; 1-line addition. |
| 2 | has_uuid_columns include-guard derived from nullable-promotion flags could drift from render_is_uuid's raw-cpp_type dispatch — same bug class this PR fixed for strings, just latent (no covered entity hits it yet) | core.py, cpp_history_field_mapper.cpp.mustache | Fixed | Added a dedicated has_render_uuid_columns flag from the same render_is_uuid/render_is_optional_uuid ground truth; switched the mapper's include guard to it. 8 entities' generated .cpp lost the now-unneeded include. |
Result
Added the ores.cpp.presentation codegen facet: render_{entity}_fields()
per entity, generated for all 27 clean ores.refdata entities (same
baseline as entity_type_of()). First implementation attempt reused
the repository/SQL layer's nullable-promotion type flags and produced
code that failed to compile against real entities (book's uuid
primary key, and nullable-but-explicitly-non-optional string columns)
— root-caused to the domain class template using raw cpp_type
verbatim rather than a promoted type, and fixed with a new,
independent render_is_* flag set derived from that same raw
cpp_type ground truth. Added ores.diff.lib as a public dependency
of ores.refdata.core. New test coverage in
presentation_currency_history_field_mapper_tests.cpp. Full local
build clean; full ctest suite green (69/69 suites, 215s).
Distilled into the story's Decisions: field-mapper rendering logic
must derive from raw cpp_type, never from the repository/SQL
layer's nullable-promotion flags — the two facets diverge whenever a
column has an explicit cpp_type override.