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 (code PK) – structurally correct; generated DDL adds change_reason_code, a column the hand-written table predates (every other DQ entity's table already has it).
  • change_reason (code PK, soft FK to change_reason_category via a Validations entry reusing ores_dq_validate_change_reason_category_fn, boolean/integer columns with defaults, an Indexes entry for the category_code lookup index) – structurally correct match, with the same change_reason_code-column caveat as change_reason_category above (sql_schema_table_create.mustache emits it unconditionally for every lookup_entity; the hand-written table predates it, same reasoning: a change-reason record referencing its own change reason would be circular).
  • data_domain (name PK) – structurally correct match, same shape as catalog.
  • subject_area – the real table's natural key is compound (name

    • domain_name), which lookup_entity can't express

    (52FE40FA, still BACKLOG); per the earlier user-approved descoping decision, domain_name is a plain Column with a soft-FK Validations entry 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> across ores.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=/=_mapper already existed as tracked, generated-shaped files (predating catalog's demotion to lookup_entity-only) and got real diffs, not fresh creation – the regenerated repository moved from a stateful (constructor-injected context) to the now-standard stateless (context passed per call) shape, matching every other domain_entity.
  • catalog's hand-written service/protocol/handler were combined with data_domain=/=subject_area=/=dataset_dependency under data_organization_service=/=_protocol=/=_handler – removed catalog's methods/structs/handlers from all three (leaving the other three entities' code untouched) and wired the new generated catalog_service=/=catalog_protocol=/=catalog_handler=/ =catalog_registrar into registrar.cpp via the standard register_catalog_handlers() sub-registrar call, matching the badge_definition=/=badge_severity=/=code_domain convention already established there.
  • The new catalog_protocol.hpp renamed delete_catalog_request.codes -> .names and get_catalog_history_request.code -> .name (matching the real primary-key column, more accurate than the old generic codes=/=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's reason_code was never assigned to catalog_.change_reason_code before 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_id now typed utility::uuid::tenant_id not std::string) and one pre-existing test assertion was wrong by the codebase's own established convention (generators_tests.cpp expected a freshly generated, not-yet-written catalog's version = 1=; every other domain_entity generator – confirmed against country_generator.cpp – sets version = 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.exe all build clean. Full ores.dq.api.tests (267 assertions/46 cases) and ores.dq.core.tests (318 assertions/104 cases) suites pass, including catalog's own 9 test cases.
  • Eventing (catalog_changed_event.hpp exists, 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_reason under change_management_service=/=_protocol=/=_handler – removed category's methods/structs/handlers from all three (change_reason's own code untouched) and wired the generated change_reason_category_service=/=_protocol=/=_handler=/ =_registrar into registrar.cpp via register_change_reason_category_handlers().
  • change_management_service's remove_category() had a real, hand-written safety check (refusing to delete a category still referenced by existing change reasons) with no equivalent in the generated change_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 in change_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_subject constant – ores.shell's change_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 via compass db recreate (destructive, dev-only bright_faraday environment – 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-1 generator-test fix as catalog (generators_tests.cpp), same stateless-repository-shape updates to two more test files (repository_change_reason_category_repository_tests.cpp, plus repository_change_reason_repository_tests.cpp and service_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 / standalone change_reason_category_service).
  • Verified: same build targets as catalog, plus ores.shell.exe, all clean. Full ores.dq.core.tests (305 assertions/96 cases, live DB) and ores.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 left data_organization_*=/=change_management_* standing (still serving other entities). change_management_service's remove_category()-style cross-entity check doesn't apply here, but a related, real design decision: list_reasons_by_category() and is_valid_reason_code()=/=is_valid_category_code()-style validation helpers were confirmed to have no real callers outside their own tests before deletion (grep across 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's change_reason_code column (defined standalone in dq_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 a system.new_record sentinel on null/empty instead of raising – neither of which the generic generated version does. create or replace function can'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 from dq_change_reasons_create.sql after generation, keeping everything else (table/indexes/insert trigger, whose ores_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.hpp carried a companion namespace change_reasons \{ ... \} block of well-known reason-code constants (e.g. system_new_record, static_data_regulatory_change) alongside the change_reason struct itself – domain_entity's template has no way to express "also emit this extra namespace of constants," so regenerating silently dropped it, breaking 3 unrelated ores.compute callers (work_handler.hpp, result_handler.hpp, workunit_handler.hpp) that only surfaced when actually building ores.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 generated change_reason.hpp, same namespace) so it survives future regenerations, and added the include to the 3 affected files.
  • Same version = 0=-vs-1 generator-test fix, and the same promptChangeReason()=/=change_reason_code-not-assigned bug fixed in ChangeReasonDetailDialog (tracked more broadly as 5DF07DAD).
  • ores.shell's change_reasons_commands.cpp had the same hardcoded-hyphenated-subject-string bug as change_reason_categories_commands.cpp did – 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 another compass 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.tests 290 assertions/89 cases, ores.dq.api.tests 267 assertions/46 cases, both green. Also verified ores.qt.exe, ores.dq.service.lib, and ores.http.server.exe all build clean (the last one is what surfaced the change_reason_codes.hpp regression).

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_domain out of the combined hand-written data_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 bundles register_data_domain_handlers alongside register_catalog_handlers.
  • Qt callers (DataDomainDetailDialog, DataDomainMdiWindow, ClientDataDomainModel, DataDomainHistoryDialog) swapped to the new data_domain_protocol.hpp; SubjectAreaDetailDialog, DatasetDetailDialog, CodingSchemeDetailDialog needed 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 prerequisite data_domain_repository usage. Same version = 0=-vs-1 generator-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, per entity_plural) – renamed via git and updated both master \ir include lists (dq_create.sql, dq_drop.sql). No naming collision this time (unlike change_reason's ores_dq_validate_change_reason_fn). Applied via compass db recreate.
  • Verified via compass build test_ores.dq.core.tests=/ =test_ores.dq.api.tests against the recreated schema: ores.dq.core.tests 290 assertions/89 cases, ores.dq.api.tests 267 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.

Result

Emacs 29.3 (Org mode 9.6.15)