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.get NATS subject exists, replacing typed per-entity history subjects going forward.
  • A server-side dispatch_registry routes requests to a caller-registered per-entity history_provider by entity_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).

Emacs 29.3 (Org mode 9.6.15)