Task: Put the numbers-changing engine run settings through sign-off

Table of Contents

This page documents a task in the Make every configuration screen follow one standard story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

Every configuration whose change alters the numbers a run computes goes through the same proposal and sign-off lifecycle, whichever kind it is.

2. Status

Field Value
State DONE
Parent story Make every configuration screen follow one standard
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-11-08

3. Acceptance

  • A write to a group that changes the numbers produces a proposal; a write to an operational group lands directly with a reason and a history entry.
  • The engine run settings kind names its approval parts, and the inbox holds them.
  • The person who proposed a change cannot sign it off.
  • A run records the version of the engine run settings it used.

4. Plan

4.1. The model

  1. The inbox gains approval_kind_part: a kind names the parts its requests always need, seeded with fixings (answer order 1) and building (order 2) for reporting.engine_settings_change. execution names no part, because a write to it changes no numbers.
  2. The reporting service gains engine_settings_change: one held value per setting of a proposed group, with the request, the configuration, the group, the setting, the value as the screen reads it, the version the person read, and whether the apply wrote it.
  3. report_instance gains engine_settings_id and engine_settings_version, stamped when the trigger admits the run from the definition's binding.

4.2. The service

  1. engine_settings_proposal_service answers whether a group needs approval (the group changes numbers and a report binds the configuration), raises the proposal with the part of the group it touches, reads the lines beside the values in force, checks them against the version in force, and applies an approved request in one transaction.
  2. engine_settings_apply_workflow is registered beside the book and counterparty workflows: recheck, apply, tell. The kind names it, so the inbox starts it on the approval.
  3. A proposal operation model renders the raise and the two reads to C++ and TypeScript; the handler serves them and opens a request only to its proposer, a part decider, or a reader of the tenant's requests.

4.3. The screen

  1. The BFF group save asks whether the group changes numbers and whether a report binds the configuration. If both hold it raises a proposal and answers "this is a proposal" with the approval; otherwise it writes the group as before. A failed reach read proposes rather than writes, because the governed case is the one that must not be got wrong.
  2. The record page draws Propose a change there, and both Save and Revert show the approval the write became. The inbox draws the kind with the shared delta.

4.4. Findings: where the standard and the machinery disagree**

  • The standard states the lifecycle per section, and the engine run settings state whether a setting changes the numbers per setting (engine_settings_group_settings.changes_numbers). No section-level declaration exists, so a group's status is derived: a group changes the numbers when any of its settings does. That matches the standard for fixings and execution, but the seed declares building:observation_model and building:lazy_market_building as not changing the numbers, so a section declaration and the settings' own declaration disagree about part of building. The group-level reading (building is numbers-changing) is the one the standard names, and it is the one used.
  • The standard says a kind declares which approval parts sign it. The approval machinery had no such table: a request's parts were the parts an approval policy named for a changed column, so a kind whose parts are not columns had nowhere to state them. The kind-part table closes that gap.
  • The standard says a numbers-changing write is a proposal on a configuration a report binds. The parts were seeded from the parts of the thing (the group), so a proposal takes the part of its group and not every part of the kind.
  • The standard's rule 6 (the proposer cannot sign) is the schema's: the decision trigger refuses it whatever part the proposer holds.
  • inbox_approval_decide_fn lets one person approve a request once whatever part, so a proposal touching two number-changing groups needs two people. The standard does not say this; the machinery does.
  • The market data service's approval grants lived only in the generated grants script and not in the registry it comes from; regenerating dropped them. Both are in the registry now.

5. Measurements

What Value Command Commit
Groups the data states, with their settings 3 groups, 15 settings compass sql -- -c "select count(*) from ores_reporting_engine_settings_groups_tbl" a434c73876
Parts the engine run settings kind names 2 (fixings, building) compass sql -- -c "select count(*) from ores_inbox_approval_kind_parts_tbl where valid_to = ores_utility_infinity_timestamp_fn()" a434c73876
Proposed lines the live run wrote 30 applied of 126 compass sql -- -c "select applied, count(*) from ores_reporting_engine_settings_changes_tbl group by applied" a434c73876
Reporting, inbox, analytics core suites 4 suites, 0 failures =compass test run – -R "ores.reporting.core.tests ores.inbox.core.tests ores.reporting.service.tests ores.analytics.core.tests"= a434c73876
BFF engine settings suite 24 tests, 0 failures npx vitest run packages/bff/src/engine-settings.test.ts a434c73876

The numbers above are from this environment's database after the live drive, so the counts of proposals and parts grow with every further run.

6. Notes

7. Test Scenarios

Manual QA scenarios (scaffolded via compass add test_scenario) 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
     

8. PRs

PR Title
#4011 [reporting] Put the numbers-changing engine run settings through sign-off

9. Review

# Comment summary File Decision Notes
1 The base version came from the client engine_settings_proposal.cpp Fixed The raise reads the version in force and the values in force in its transaction and stamps the version it read. A client version that does not match is refused as stale, in the screen's words, before anything is held.
2 The reads sat outside the unit of work engine_settings_proposal.cpp Fixed The configuration, the values in force, the version in force and the proposal's lines are one unit of work. The kind's parts are read before it, because a junction read inside the transaction sees the transaction's tenant and not the system tenant's catalogue.
3 A whole out of range was read as a transient failure engine_settings_proposal.cpp Fixed as_whole parses with from_chars and throws invalid_argument for text that is not a whole and for a value too large to hold, so the apply refuses it for good rather than retrying it forever.
7 The stale refusal had no test engine_settings_proposal_tests.cpp Fixed Three cases: a raise against a moved version and one ahead of it, both refused with the screen's words and nothing held; the apply against a moved base version, which the existing moved-group case already covered; and a value too large to hold, refused at the raise and at the apply.
4 The three per-group branches repeated each other, and each re-read the row with a hard-coded page engine_settings_proposal.cpp Fixed One table lists a group once: its code, a reader of the row in force and a writer of the next version. The three generic functions became a lookup in that table, each use reads the row once, and the page is one constant instead of three literals. The read is one row (`0, 1`), not a page of a thousand.
5 The parts seed and the settings seed could drift engine_settings_group_parts_tests.cpp Fixed A case in the reporting suite reads both seeds through the repositories and the lifecycle the service uses, and fails if a numbers-changing group is not a part of the kind or a part is not a numbers-changing group. It lives in the database suite rather than a seed parse because it must check the data the service meets, including what the triggers and the system-tenant lookups make of the seed.

10. Result

The engine run settings now follow the standard's numbers-changing lifecycle, on the existing approval machinery.

The model: ores_inbox_approval_kind_parts_tbl lets a kind of request name the parts its requests need, seeded for reporting.engine_settings_change with fixings and building. ores_reporting_engine_settings_changes_tbl holds a proposal's values until its request is decided. ores_reporting_report_instances_tbl gained engine_settings_id and engine_settings_version, so a run keeps what it ran with.

The protocol: reporting.v1.ops.raise_engine_settings_change, get_engine_settings_changes and recheck_engine_settings_changes, plus the engine_settings_apply_workflow the kind names. A raise names the part of the group it writes, and refuses a draft, an operational group, a value the setting does not accept, and a proposal that changes nothing. The apply re-checks the version in force, writes the group in one transaction through the part's own service, marks each line applied once, and names the request it applied in the group's history entry.

The screen: the group save answers {document, proposal}, with the approval when the write became one; the record page says Propose a change and shows the approval, and the inbox draws the kind with the shared delta.

The tests: the reporting core suite drives the lifecycle against a database (proposal holds its values, draft and operational groups refused, the kind's parts, the proposer's own decision refused, the apply, the moved-group refusal, and the line applied once); the BFF suite covers the proposal branch, the direct-write branch and the response shape.

The live run, through the web BFF: Nadia proposed turning today's fixings on (HTTP 202 with the approval, the value in force unchanged), the proposal was in her own requests and in Adrian's queue, Nadia's own approval was refused with "You asked for this request, so someone else decides it", Adrian approved, the workflow applied it (fixings version 3 with change_commentary = "Applied approval request ..."), and a run of the Exposure report recorded =engine_settings_version = 6, the version in force.

Gaps that remain, named rather than hidden:

  • The reach read drives the choice between proposing and writing. A configuration that no report binds is a draft and changes in place, as the standard says; the engine settings list does not yet draw a draft mark.
  • The inbox queue is oldest first and pages at fifty, so an old queue buries a new proposal at the end. The request is in the queue; the screen offers no filter for kind.
  • building:observation_model and building:lazy_market_building declare that they change no numbers while their section is numbers-changing. The section decides the lifecycle; the per-setting flag still drives the warning on the row, so a person may see "does not change the numbers" inside a section whose write is proposed.
  • The step handler reads its command as JSON explicitly, because the shared decode helper reads with a process-wide default codec another client can change. The same latent fault sits in every other step handler.

Emacs 29.3 (Org mode 9.6.15)