Task: Generic history.v1.get NATS subject and server-side dispatch registry
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
(Describe what user-visible-or-internal change this task produces.)
Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | Consolidate history dialogs onto HistoryDialogBase |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-07-13 |
Acceptance
- One shared
history.v1.getNATS subject exists, replacing typed per-entity history subjects going forward. - A server-side
dispatch_registryroutes requests to a caller-registered per-entity history_provider byentity_type. - At least one entity (currency) has a real, working provider proving the full path: repository read -> field mapper -> diff -> generic dispatch -> NATS reply.
- Old per-entity history subjects/handlers are removed only once every consuming frontend has migrated — deliberately not required for this task's completion; tracked as follow-on story tasks.
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
Design decision (caller_context — REVERSED, see below): PR #1523
review flagged that ores.history had no way to carry tenant/auth
scope into a history_provider, and that wiring a real provider would
make history.v1.get an unauthenticated, tenant-unscoped subject — a
regression from the per-entity handlers it replaces. Considered
threading a full ores::database::context through (rejected at the
time: breaks ores.history's deliberate dependency-free-leaf design,
pulling in ores.database for every consumer), and spiking one
entity end-to-end with a hardcoded/system-tenant context (rejected:
would have to be re-done once real scoping lands). Chose instead: an
opaque caller_context string threaded through history_provider
and dispatch(), produced per-request by a caller-supplied
caller_context_resolver.
Design decision (dependency-free leaf, revisited and reversed): after
PR #1531 review flagged that the currency pilot's resolver only
packed tenant_id (dropping party/roles/workspace) — see Review #13
— the underlying premise was re-examined rather than patched again.
The "keep ores.history dependency-free" rule was modeled on
ores.diff's field_value=/=diff_result, which genuinely are
embedded in client-decoded wire types. But dispatch_registry,
history_handler and registrar are server-only NATS glue that no
client ever touches, and every service that would register a
history_provider already links ores.database=/=ores.security to do
anything else. The rule bought nothing there — it just forced an
opaque-string workaround for a dependency that was never actually
avoided. Reversed it: those three headers now depend on
ores.database=/=ores.security directly; history_provider takes a
real database::context; history_handler calls
make_request_context() itself, exactly like every other handler in
the codebase. caller_context_resolver and the opaque-string
pack/unpack machinery (including a short-lived
ores.refdata.core/messaging/history_caller_context.hpp helper) are
deleted entirely. history_protocol.hpp (wire types) and
version_builder.hpp remain the true dependency-free leaf, since
those genuinely are client-visible.
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 |
|---|---|
| #1537 | [agile] Close generic-history-subject task |
| #1531 | [refdata] Wire the currency history provider onto history.v1.get |
| #1529 | [history] Thread caller_context and add build_entity_history_versions |
| #1523 | [history] Add history_handler and single-subject registrar |
| #1521 | [history] Add ores.history component with dispatch registry |
Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | dispatch() doesn't guard against a provider throwing | dispatch_registry.cpp | Accepted | Wrapped provider call in try/catch, mapped std::exception to failure response; added throwing-provider test |
| 2 | dispatch_registry not thread-safe (cf. event_channel_registry mutex) | dispatch_registry.hpp | Declined | Scaffolding increment; registration happens at startup before dispatch, no concurrent access yet. Revisit when real providers are wired |
| 3 | dispatch_registry copyable/movable by default | dispatch_registry.hpp | Declined | Minor; not an issue at current scope, no single-owned-instance requirement yet |
| 4 | get_entity_history_response field order differs from workflow_query_protocol.hpp sibling | history_protocol.hpp | Declined | Cosmetic, not house-style-mandated |
| 5 | decode failure leaves the caller hanging (no error_reply) | history_handler.hpp | Accepted | Added error_reply(bad_request) on the decode-failure path, matching every other handler in the codebase |
| 6 | dispatch_registry parameter/member could be const& (only const dispatch() is called) | history_handler.hpp, registrar.hpp/.cpp | Accepted | Tightened to const service::dispatch_registry& throughout |
| 7 | no auth/tenant-scoping on the generic handler; history_provider has no context parameter | history_handler.hpp, dispatch_registry.hpp | Accepted (resolved later) | Deferred at the time (no real provider yet); resolved when the ores.history dependency-free-leaf decision was revisited — see #13 |
| 8 | no unit tests for history_handler/registrar | history_handler.hpp, registrar.cpp | Declined | Matches existing convention — no other component's NATS handler/registrar pairs are unit-tested directly (need a live client); covered by dispatch_registry's own tests plus future integration tests |
| 9 | registrar is a free function returning one subscription, unlike the class + vector<subscription> shape used elsewhere | registrar.hpp/.cpp | Declined | Deliberate: there is exactly one shared subject, not N per-entity ones |
| 10 | messaging namespace reopened twice in the PlantUML diagram | ores.history.puml | Accepted | Merged into a single namespace block |
| 11 | caller_context_resolver collapses token_expired into unauthorized, breaking ores.nats's re-auth/session-expired UX once a real JWT resolver is wired | history_handler.hpp | Accepted | Changed resolver to return std::expected<std::string, error_code> instead of std::optional<std::string>; history() now propagates the resolver's actual error_code via error_reply |
| 12 | request decoded before caller_context resolved, opposite order to every other handler (auth first) | history_handler.hpp | Accepted | Reordered: resolve caller_context first, then decode, matching currency_handler and the rest of the codebase |
| 13 | resolve_context only packs tenant_id, discarding the party/roles/workspace that make_request_context already resolved | registrar.cpp | Accepted (root-caused and fixed) | First patched with a JSON pack/unpack round-trip in ores.refdata.core (history_caller_context.hpp/.cpp) — then the root cause was revisited: the underlying opaque-caller_context design existed only because ores.history was kept a dependency-free leaf, an ores.diff-modeled decision that never actually applied to the messaging/service layer (server-only glue, no client ever touches it, every composing service already depends on ores.database/ores.security). Reverted that: history_provider/dispatch() now take a real database::context; history_handler calls make_request_context() itself exactly like every other handler; caller_context_resolver and the JSON pack/unpack helper are deleted entirely. history_protocol.hpp and version_builder.hpp remain the true dependency-free leaf |
Result
Added the ores.history component: the one shared history.v1.get
NATS request/response pair (get_entity_history_request=/
=get_entity_history_response, keyed by entity_type rather than a
typed per-entity subject), a server-side dispatch_registry that
looks up a caller-registered history provider by entity_type
(reapplying the entity-composed-registrars meta-pattern as a runtime
lookup table), and build_entity_history_versions() — the shared
rendering/diffing glue every per-entity provider needs, built on
ores.diff::engine::compute. history_handler=/=registrar resolve
each request via make_request_context(), exactly like every other
NATS handler in the codebase — see the Notes design-decision entry on
why ores.history's messaging/service layer ended up depending on
ores.database=/=ores.security directly rather than staying a
dependency-free leaf (only history_protocol.hpp and
version_builder.hpp remained leaf).
Wired the first real provider — currency, in
ores.refdata.core's registrar — as a pilot proving the whole path
end to end: repository read -> render_currency_fields() ->
build_entity_history_versions() -> generic dispatch -> NATS reply.
The old per-entity get_currency_history subject/handler is
untouched and still live; no frontend has switched over to
history.v1.get yet. That migration (starting with
ores.shell, per
Generic unified-diff history renderer in ores.shell), and wiring
the remaining 26 ores.refdata entities' providers, continue as
their own tasks on this story.
Shipped across PRs #1521, #1523, #1529, #1531. Full local build/ctest
green throughout (final round: 70-71/71 passing, the one intermittent
failure traced to local DB schema drift unrelated to this branch,
resolved by compass db recreate).