Task: Redesign server-side history-diff architecture
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
Produce a revised design for server-side history diffing that supersedes the currency pilot (PR 1097), reverted 2026-07-09 in commit 43b5c1a16 for being hand-written and non-codegen — not for being architecturally wrong. Answer, in order:
- Server-side field-to-string mapper: codegen'd, simple, plain code. Not hand-written (the pilot's mistake) and not a generic reflection/metaprogramming layer either — the mapper per entity should be ordinary, generated, easy-to-read code (codegen emits the obvious sequence of field renders from the entity's org model), easy to test like any other generated artefact. Bespoke rendering (enum labels, currency amounts, image ids) is just a line of generated code calling a formatting helper, not a special case in a framework.
- Intra-value diff spans, computed once. Extend
ores::diff::domain::diff_entry(or a new type) to carry the changed character/token ranges within old and new values, computed centrally byores::diff::engine::compute(or a sibling) — this part IS fully generic and entity-agnostic, since it operates on already-rendered strings, not domain types. Every frontend (Qt, shell, future Wt/HTTP) renders identical highlighting without its own diff/compare logic. Absorbs the "GitHub-style diff view" capture's requirement for multiline (line-by-line LCS + per-line token diff), not just prefix/suffix highlighting. - One NATS message shape, one consumer code path. The critical
constraint: regardless of how the per-entity mapper is generated,
every entity's history response must share one identical shape
(fields + changes-with-spans), and
ores.shellandores.qtmust each have exactly ONE piece of code that renders it — no per-entity branching, no per-entity dialog/command logic beyond what's already generated boilerplate (wiring, not rendering decisions).HistoryDialogBaseis that one Qt render path; the shell's disabledcurrencies history-diffcommand (task F455B2BE, PR 1105) generalises into the shell's one render path for every entity, not a currency-specific command. - Composite/referenced entities: delegate, don't inline. When
entity A references entity B (e.g. by id/foreign key) and B's own
version changed, decide whether that surfaces in A's history at
all (only if A embeds a snapshot of B, not merely a reference), and
if so, whether A's changes row for that field navigates to/opens
B's own
HistoryDialogBaserather than attempting to render B's diff inline. This is distinct from the existing backlog capture ores.diff: structured topology for nested entities, which is about flattening embedded nested structure (e.g. instrument legs) within one entity's own diff render, not cross-entity delegation to another entity's history.
Output: this story's Architecture and Decisions sections rewritten to match, and the template-rollout task's (7B2998F0) Plan revised to implement the new design rather than the reverted pilot's shape.
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Consolidate history dialogs onto HistoryDialogBase |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-11 |
Acceptance
- Per-entity mapper generation approach decided: codegen'd plain code, not hand-written and not generic reflection/metaprogramming.
- Diff-span extension to ores.diff designed (types + compute contract), covering single-line and multiline/token diffs; this layer confirmed fully generic (operates on rendered strings only).
- One shared history-response shape defined for every entity; one render path designed for ores.qt (HistoryDialogBase) and one for ores.shell (generalising the disabled per-currency command) — no per-entity branching on the consuming side.
- Story's Architecture and Decisions sections updated to match.
- template-rollout task's Plan revised to implement this design.
- Composite/referenced-entity handling decided: when it surfaces in the referencing entity's history at all, and whether it delegates to the referenced entity's own HistoryDialogBase instead of inlining a diff; distinguished explicitly from the structured- topology capture (embedded nesting, not cross-entity reference).
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
Testing requirement for the implementation that follows this design:
ores.diff::engine::compute's diff_span computation must be
exhaustively tested in projects/ores.diff/tests/ (currently 14 tests
across engine_compare_tests.cpp and domain_serialisation_tests.cpp,
covering field-level compute only). New cases needed once spans land:
single-character change, whole-word change, common-prefix-only,
common-suffix-only, no-overlap (entirely different values), multiline
commentary requiring line-by-line LCS, per-line token diff within a
changed multiline block, added field (empty old_value), removed field
(empty new_value), identical values (no spans), empty string vs
non-empty, and unicode/multi-byte content (span offsets must not split
a multi-byte code point). Whichever task implements the ores.diff
extension should treat this list as a acceptance floor, not a ceiling.
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 |
|---|---|
Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
Result
Produced History Diff Architecture, superseding the reverted currency pilot. Key decisions, distilled into the story's Architecture/Decisions:
- One generic NATS history subject (
history.v1.get, entity type + entity id), dispatched server side via a runtime lookup table — reapplying the existing entity-composed-registrars meta-pattern — instead of a typed per-entity subject. No typed domain payload crosses the wire for history. - Per-entity field mapper is codegen'd, plain, generated code from the
outset — rejected both hand-writing (the pilot's mistake) and a
generic
rfl-reflection framework (hard to read/step through). - A generated
entity_type_of()free-function trait is the single source of truth for the dispatch key, kept off the domain struct itself. ores.diff::diff_entrygainsold_spans=/=new_spans(character/token ranges), computed once insideores.diff::engine::compute— this layer stays fully generic since it never sees a domain type.HistoryDialogBaseand its 61+ per-entity derived classes are replaced by one non-templatedHistoryDialogwidget and one shell renderer;Open=/=Revertcarry(entity_type, entity_id, version)only, dispatched through a small codegen'd per-entity action registry (same meta-pattern, client side) that opens the entity's own typed detail dialog — accepting one extra fetch-by-id round trip in exchange for zero per-entity Qt code.- Composite/referenced-entity delegation needs no special code path: opening another entity's history is constructing the same dialog with a different entity id. Distinguished explicitly from the separate, still-open embedded-nesting capture and the still-separate temporal composite entity versioning story.
Filed a follow-up backlog capture generalising the entity_type_of()
- dispatch-table pattern beyond history, scoped out of this document
deliberately.
The template-rollout task's (7B2998F0) Plan is revised separately to implement this design.