Task: Commission catalog/change_reason/change_reason_category/data_domain onto standard codegen
Table of Contents
This page documents a task in the Commission ores.qt.dq — full-stack codegen for the DQ component story. It captures the goal, current status, acceptance, and any notes or results.
Goal
Commission four of the five no-model DQ entities named in the parent story's original scope onto the standard codegen stack: catalog, change_reason, change_reason_category, data_domain. subject_area split out to a new Sprint 24 task/story (blocked on compound natural-key support in the lookup_entity/table SQL templates, 52FE40FA) — see Commission remaining DQ entities.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Commission ores.qt.dq — full-stack codegen for the DQ component |
| Now | catalog, change_reason_category, change_reason, and data_domain fully commissioned end to end (real C++ + SQL regeneration, hand-written code retired, build+tests green via compass build test_ against a recreated dev DB). |
| Waiting on | Nothing. |
| Next | subject_area carried forward to a Sprint 24 follow-on task. |
| Last touched | 2026-07-21 |
Acceptance
- Each of the four entities has a codegen .org model under projects/ores.dq/modeling/; generated core/repository/service/handler/protocol code replaces any hand-written equivalent; build and tests green.
Plan
(Implementation strategy. Written when work starts; key decisions
are distilled into the parent story's * Decisions at close, but the
plan itself stays — it is the historical record of what we did.)
Notes
Checked the real SQL for all five entities before authoring the first
model: none of them has a UUID surrogate primary key — every one is
keyed by (tenant_id, <natural key column(s)>, valid_from, valid_to)
directly (name for catalog/data_domain, code for
change_reason/change_reason_category, (name, domain_name) for
subject_area). That is the LookupEntity metatype's shape, not
domain_entity's — despite the story calling them "simple", they are
#+type: ores.codegen.lookup_entity, not entity_org. The --shape
scaffolding from task EE237306 only covers domain_entity; a first
compass add entity_org --shape simple-text-key attempt for
catalog was discarded once this was discovered.
subject_area's compound natural key (name + domain_name) also
doesn't fit doc_lookup_entity.org.mustache's current single-column
Primary key section, and has_coding_scheme=/=has_image_id=/
=has_artefact_insert_fn are hardcoded false in that template with
no CLI flag at all. Filed F71E530F to fix both before resuming here.
Discovered a second, separate bug while verifying catalog.org: no
lookup_entity model in the repo could regenerate SQL or C++ at all
(core.py mapped its #+type: to a model_type no archetype matched).
Filed and fixed DDBD522D (SQL
only – C++ needs a bigger fix, tracked separately as
464B5EC6, migrating
lookup_entity into domain_entity rather than patching a duplicate C++
pipeline). catalog.org now verified: compass codegen generate
--model ... --address ores.sql.schema produces DDL structurally
matching projects/ores.sql/create/dq/dq_catalog_create.sql (only
cosmetic differences – comment formatting, NEW/OLD case – plus an
auto-generated validate_catalog_fn the hand-written original
predates). C++ generation and swapping the hand-written table over to
generated SQL are deferred to 464B5EC6/a later step, not blocking
authoring the remaining four entities' models.
Remaining four entities authored and each SQL-verified the same way
(compass codegen generate --model ... --address ores.sql.schema,
diffed against the real hand-written table, generated file reverted
afterwards – only the .org model is committed):
change_reason_category(codePK) – structurally correct; generated DDL addschange_reason_code, a column the hand-written table predates (every other DQ entity's table already has it).change_reason(codePK, soft FK tochange_reason_categoryvia aValidationsentry reusingores_dq_validate_change_reason_category_fn, boolean/integer columns with defaults, anIndexesentry for thecategory_codelookup index) – structurally correct match, with the samechange_reason_code-column caveat aschange_reason_categoryabove (sql_schema_table_create.mustacheemits it unconditionally for everylookup_entity; the hand-written table predates it, same reasoning: a change-reason record referencing its own change reason would be circular).data_domain(namePK) – structurally correct match, same shape ascatalog.subject_area– the real table's natural key is compound (namedomain_name), whichlookup_entitycan't express
(52FE40FA, still BACKLOG); per the earlier user-approved descoping decision,
domain_nameis a plainColumnwith a soft-FKValidationsentry rather than part of the primary key. Generated DDL is therefore intentionally weaker than the hand-written table (PK/exclusion/uniqueness cover(tenant_id, name, ...)only, not the pair) – do not regenerate/swap this table over the hand-written one until 52FE40FA lands.
All five models' SQL generation is verified; none of the five hand-
written tables have actually been swapped over yet (deliberately –
see per-entity notes above and 464B5EC6 below). C++ generation
(core/repository/service/handler/protocol) remains blocked on
464B5EC6: attempting it directly on lookup_entity's own pipeline
during DDBD522D surfaced that the shared C++ templates only render
\{\{#domain_entity\}\} sections, and lookup_entity's org
format/loader carries none of the C++-facing column metadata
(cpp_type, derived uuid/int/enum flags, doxygen formatting) that
pipeline needs – porting that in place would build a second,
permanently-duplicate pipeline rather than fixing the real
architectural overlap. 464B5EC6 migrates lookup_entity into
domain_entity instead, which this task's remaining acceptance
(generated core/repository/service/handler/protocol replacing the
hand-written equivalents, build and tests green) depends on.
464B5EC6 merged: all five
.org models here are now #+type: ores.codegen.entity
(domain_entity), not lookup_entity (the five
ores.dq.<entity>_lookup_entity.org files were deleted and replaced
by ores.dq.<entity>.org, same :ID: each, preserving org-roam
identity). Re-verified SQL generation against the real hand-written
tables through the new pipeline (structurally correct, same shape as
before). C++ generation is now unblocked – next step is regenerating
it for real and doing the actual swap-over described in this task's
Acceptance.
catalog fully commissioned (picked as the pilot – simplest of the five, per user decision to do one end to end before the rest):
- Regenerated for real via
compass codegen generate --model ores.dq.catalog.org --address <facet>acrossores.cpp.domain,ores.cpp.generator,ores.cpp.repository,ores.cpp.service,ores.cpp.protocol,ores.cpp.nats-handler,ores.cpp.nats-sub-registrar.catalog_repository=/=_entity=/=_mapperalready existed as tracked, generated-shaped files (predating catalog's demotion tolookup_entity-only) and got real diffs, not fresh creation – the regenerated repository moved from a stateful (constructor-injectedcontext) to the now-standard stateless (contextpassed per call) shape, matching every otherdomain_entity. - catalog's hand-written service/protocol/handler were combined with
data_domain=/=subject_area=/=dataset_dependencyunderdata_organization_service=/=_protocol=/=_handler– removed catalog's methods/structs/handlers from all three (leaving the other three entities' code untouched) and wired the new generatedcatalog_service=/=catalog_protocol=/=catalog_handler=/ =catalog_registrarintoregistrar.cppvia the standardregister_catalog_handlers()sub-registrar call, matching thebadge_definition=/=badge_severity=/=code_domainconvention already established there. - The new
catalog_protocol.hpprenameddelete_catalog_request.codes->.namesandget_catalog_history_request.code->.name(matching the real primary-key column, more accurate than the old genericcodes=/=code) – updated the 5 Qt caller files that used the old names/included the old combined protocol header (CatalogDetailDialog,CatalogMdiWindow,CatalogHistoryDialog,ClientCatalogModel,DatasetDetailDialog– the last one uses both catalog's and data_domain's protocols, needed both includes). - Found and fixed a real, pre-existing bug while wiring
CatalogDetailDialog:promptChangeReason()was called but its result'sreason_codewas never assigned tocatalog_.change_reason_codebefore saving – would have made every catalog save fail the insert trigger's change-reason validation once this entity was actually live on the generated stack. Fixed (catalog_.change_reason_code = crSel->reason_code;), mirroring the story's own note about similar hand-written change-reason gaps found during the badge migration. - Two hand-written test files needed updating for the new stateless
repository shape (
repo.write(x)->repo.write(ctx, x), etc.,tenant_idnow typedutility::uuid::tenant_idnotstd::string) and one pre-existing test assertion was wrong by the codebase's own established convention (generators_tests.cppexpected a freshly generated, not-yet-written catalog'sversion =1=; every other domain_entity generator – confirmed againstcountry_generator.cpp– setsversion = 0, incremented to 1 only by the insert trigger on first write; fixed the assertion, not the generator). - Verified:
ores.dq.api.lib,ores.dq.core.lib,ores.qt.data_management.lib,ores.qt.exe,ores.dq.service.lib,ores.http.server.exeall build clean. Fullores.dq.api.tests(267 assertions/46 cases) andores.dq.core.tests(318 assertions/104 cases) suites pass, including catalog's own 9 test cases. - Eventing (
catalog_changed_event.hppexists, tracked, but unwired – matching the story's own "not eventing" note about the badge migration) and NATS event-relay wiring are known, separately-tracked gaps (see the post-generation checklist), not attempted here – out of this task's acceptance (core/repository/service/handler/protocol, not eventing).
change_reason_category commissioned the same way as catalog, with one genuine difference in outcome:
- Regenerated for real across the same seven facets. Its hand-written
service/protocol/handler were combined with
change_reasonunderchange_management_service=/=_protocol=/=_handler– removed category's methods/structs/handlers from all three (change_reason's own code untouched) and wired the generatedchange_reason_category_service=/=_protocol=/=_handler=/ =_registrarintoregistrar.cppviaregister_change_reason_category_handlers(). change_management_service'sremove_category()had a real, hand-written safety check (refusing to delete a category still referenced by existing change reasons) with no equivalent in the generatedchange_reason_category_service– codegen has no way to know about this cross-entity business rule. Removed along with the rest of category management; noted here as a known behaviour loss, not silently dropped.is_valid_category_code()was also removed (dead code – only ever called from tests, not from any real validation path; the DB-level soft-FK check inchange_reason's own insert trigger is what actually enforces category existence on save).- The NATS subject strings changed shape: the old hand-written
protocol used hyphens (
dq.v1.change-reason-categories.list), the generated one uses underscores (dq.v1.change_reason_categories.list, matching the entity/table naming convention). Found one real caller that hardcoded the raw hyphenated strings instead of referencing the protocol's::nats_subjectconstant –ores.shell'schange_reason_categories_commands.cpp– fixed to use the new subjects and switched its include to the new protocol header. - Unlike catalog (whose hand-written SQL table already had every
column the generated template expects), change_reason_category's
hand-written table genuinely lacked
change_reason_code– exactly the gap flagged during PR #1635's review. Regenerated and swapped the SQL for real this time (unlike catalog, where the SQL swap was deferred): diffed structurally correct against the old table, then actually applied it viacompass db recreate(destructive, dev-onlybright_faradayenvironment – confirmed with the user before running). This is what surfaced a live-DB test failure ("column change_reason_code… does not exist") that catalog's own verification never hit, since catalog's SQL was never touched. - Same
version =0=-vs-1generator-test fix as catalog (generators_tests.cpp), same stateless-repository-shape updates to two more test files (repository_change_reason_category_repository_tests.cpp, plusrepository_change_reason_repository_tests.cppandservice_change_management_service_tests.cpp, both of which construct a prerequisite category row for their own change_reason tests and needed to switch from the old service/repository call to the new stateless repository / standalonechange_reason_category_service). - Verified: same build targets as catalog, plus
ores.shell.exe, all clean. Fullores.dq.core.tests(305 assertions/96 cases, live DB) andores.dq.api.tests(267 assertions/46 cases) green after the DB recreate.
change_reason commissioned the same way, with two genuine differences worth recording:
- Its hand-written service/protocol/handler (
change_management_*) was ENTIRELY about change_reason at this point – category management had already moved out in the previous PR. After removing change_reason's own methods/structs/handlers, the triplet (change_management_service.hpp/.cpp,change_management_handler.hpp,change_management_protocol.hpp) plus its dedicated test file (service_change_management_service_tests.cpp) were left completely empty of real content, so they were deleted outright rather than left as hollow shells – unlike catalog/category, which leftdata_organization_*=/=change_management_*standing (still serving other entities).change_management_service'sremove_category()-style cross-entity check doesn't apply here, but a related, real design decision:list_reasons_by_category()andis_valid_reason_code()=/=is_valid_category_code()-style validation helpers were confirmed to have no real callers outside their own tests before deletion (grepacross the whole repo), so nothing silently lost this time. - Hit a genuine SQL naming collision, not seen with catalog/category:
change_reason.org's own generated
ores_dq_validate_change_reason_fn(the standard "validate my own primary key exists" function every domain_entity gets) collides in NAME with a deliberately hand-tuned function of the same name already used by every OTHER entity'schange_reason_codecolumn (defined standalone indq_change_reason_functions_create.sql). The hand-written one has real, different, better semantics for this specific case: validates against system tenant's change reasons (shared governance data across all tenants, not the calling tenant's own), and returns asystem.new_recordsentinel on null/empty instead of raising – neither of which the generic generated version does.create or replace functioncan't even apply here (Postgres rejects changing a parameter name), so this wasn't a silent-shadowing risk, it was a hard error at DB-recreate time. Fixed by stripping the generated duplicate function definition fromdq_change_reasons_create.sqlafter generation, keeping everything else (table/indexes/insert trigger, whoseores_dq_validate_change_reason_fn(NEW.tenant_id, NEW.change_reason_code)call resolves to the existing hand-tuned function unchanged, since parameter names don't affect positional calls). - Also found and fixed a second, unrelated regression during the full
build sweep: the old hand-written
change_reason.hppcarried a companionnamespace change_reasons \{ ... \}block of well-known reason-code constants (e.g.system_new_record,static_data_regulatory_change) alongside thechange_reasonstruct itself – domain_entity's template has no way to express "also emit this extra namespace of constants," so regenerating silently dropped it, breaking 3 unrelatedores.computecallers (work_handler.hpp,result_handler.hpp,workunit_handler.hpp) that only surfaced when actually buildingores.http.server.exe(never previously built in this session). Moved the block into its own permanent, hand-maintained header (change_reason_codes.hpp, alongside the generatedchange_reason.hpp, same namespace) so it survives future regenerations, and added the include to the 3 affected files. - Same
version =0=-vs-1generator-test fix, and the samepromptChangeReason()=/=change_reason_code-not-assigned bug fixed inChangeReasonDetailDialog(tracked more broadly as 5DF07DAD). ores.shell'schange_reasons_commands.cpphad the same hardcoded-hyphenated-subject-string bug aschange_reason_categories_commands.cppdid – fixed the same way.- SQL swapped for real (change_reason's hand-written table also
lacked
change_reason_code, same gap as catalog/category), applied via anothercompass db recreate. - Verified via
compass build test_ores.dq.core.tests=/ =test_ores.dq.api.tests(the correct, ctest-backed way to both build and actually run the suites – not by invoking the test binaries directly):ores.dq.core.tests290 assertions/89 cases,ores.dq.api.tests267 assertions/46 cases, both green. Also verifiedores.qt.exe,ores.dq.service.lib, andores.http.server.exeall build clean (the last one is what surfaced thechange_reason_codes.hppregression).
Stopping here again per the same scope agreement – data_domain and subject_area remain.
data_domain
- Regenerated the full stack (domain, generator, entity/mapper, stateless repository, service, protocol, NATS handler/registrar).
- Decomposed
data_domainout of the combined hand-writtendata_organization_service=/=_protocol=/=_handler(which also covers methodology/subject_area/nature_dimension/origin_dimension/ treatment_dimension, so kept as a shell for those); registrar now bundlesregister_data_domain_handlersalongsideregister_catalog_handlers. - Qt callers (
DataDomainDetailDialog,DataDomainMdiWindow,ClientDataDomainModel,DataDomainHistoryDialog) swapped to the newdata_domain_protocol.hpp;SubjectAreaDetailDialog,DatasetDetailDialog,CodingSchemeDetailDialogneeded both old and new protocol includes since they also touch other still-bespoke entities. No field renames needed – old/new protocol field names matched. - Test files converted to stateless repository shape, including
repository_subject_area_repository_tests.cpp's own prerequisitedata_domain_repositoryusage. Sameversion =0=-vs-1generator-test fix as the other entities. - SQL swapped for real: hand-written files were singular-named
(
dq_data_domain_create.sql=/=_drop.sql) while codegen generates plural (dq_data_domains_create.sql=/=_drop.sql, perentity_plural) – renamed via git and updated both master\irinclude lists (dq_create.sql,dq_drop.sql). No naming collision this time (unlike change_reason'sores_dq_validate_change_reason_fn). Applied viacompass db recreate. - Verified via
compass build test_ores.dq.core.tests=/ =test_ores.dq.api.testsagainst the recreated schema:ores.dq.core.tests290 assertions/89 cases,ores.dq.api.tests267 assertions/46 cases, both green.
Stopping here again per the same scope agreement – subject_area remains.
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 |
|---|---|
| #1654 | [dq] Commission data_domain onto standard codegen stack |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | task_fix-lookup-entity-model-type-mapping.org reverted from DONE to a blank BACKLOG stub – branch not rebased onto main after PR #1634 merged | doc/agile/versions/v0/sprint_23/commission-dq-component/task_fix-lookup-entity-model-type-mapping.org | Accepted | Root cause: an earlier rebase-conflict resolution used –theirs, which in a rebase means "the commit being replayed" (this branch's stale stub), not main's content – inverted from what was intended. Restored from origin/main verbatim. |
| 2 | change_reason bullet omits the change_reason_code-column caveat that change_reason_category's bullet states, even though the same template gap applies to both | doc/agile/versions/v0/sprint_23/commission-dq-component/task_commission-dq-reference-entities.org, PR description | Accepted | Added the same caveat to the change_reason bullet and to the PR description's Summary/Test plan. |
| 3 | catalog_table.cpp dropped the "Version" column present in every sibling generated *_table.cpp | ores.dq.catalog.org, catalog_table.cpp | Accepted | Missing `version | Version` row in the org model's Table display section; added and regenerated. |
| 4 | catalog.hpp drops the explicit #include <chrono> every sibling generated domain header keeps alongside tenant_id.hpp | ores.dq.catalog.org, catalog.hpp | Accepted | Missing from the org model's Domain includes block; added and regenerated. |
| 5 | Stale doxygen comment in data_organization_service.hpp still lists "Managing catalogs" after catalog moved to its own generated service | data_organization_service.hpp | Accepted | Updated the class-level comment. |
| 6 | The promptChangeReason()/change_reason_code-not-assigned bug fixed in CatalogDetailDialog also exists in ~9 other Qt dialogs | (out of scope for this PR) | Accepted, filed | Filed 5DF07DAD as a tracked follow-up rather than fixing here. |