Task: Give the reporting line its own write

Table of Contents

This page documents a task in the Implement the membership journeys in ores.web story. It captures the goal, current status, acceptance, and any notes or results.

1. Goal

Changing who a person reports to writes that field alone, records why, and refuses a write against a stale account version.

2. Status

Field Value
State DONE
Parent story Implement the membership journeys in ores.web
Now Nothing.
Waiting on Nothing.
Next Nothing.
Last touched 2026-10-09

3. Acceptance

  • iam.v1.ops.set_reporting_line writes the reporting line alone, leaving every other field as the account had it.
  • The request names the manager (empty clears the line), the version the caller read, and the reason.
  • A stated version that no longer matches is refused as conflict, and the line does not move.
  • A manager that does not exist, a self reference, and a line that would close a cycle are all refused, each naming the reason.
  • The write needs iam::accounts:update, and a caller without it is refused before the service is reached.
  • The browser reaches the write over PUT /api/accounts/:id/reporting-line, and the shell reaches the same subject.
  • check_component_drift.py --component iam reports no drift, and the C++ and BFF tests cover it.

4. Plan

  1. Declare iam.v1.ops.set_reporting_line: the account, the manager, the version the caller read, and the reason.
  2. Write it in the service: read the account, refuse a stale version, refuse a self reference, set the one field, and answer the account as written.
  3. Serve it from the account handler behind iam::accounts:update.
  4. Wire the browser path, and let codegen emit the shell command.
  5. Test what the narrow write promises: one field written, the reason recorded, a stale version refused, a self reference refused.

5. Measurements

What Value Command Commit
IAM core suite after this change 264 cases, 821 assertions bash build/evidence/run_case.sh ores.iam.core.tests (branch tip)
BFF membership cases 8 npm test --workspace @ores/bff -- membership (branch tip)

6. Notes

  • The version is text, not a number. The first draft carried std::optional<int>. The shell facet refused it — a message field with an optional scalar has no shell token form — and a sentinel integer would not work either, because a fresh account's version is 0. Text with empty meaning "not stated" is what every other optional value on this wire already does. The gap in the shell generator is captured at An optional scalar in a message has no shell token form.
  • The write answers the account as written. The store stamps the new version, so the service re-reads after the write rather than answering the copy it read. The screen then redraws against the version it must state next time.
  • The cycle guard and the self check overlap, and both stay. The SQL guard catches every cycle including a self reference, but it arrives as a constraint violation; the service checks the self reference itself so it can answer with a result the screen can draw.
  • wireAccountSchema and mapAccount became exports. The reply carries an account, and the alternative was restating sixteen fields and the nil-to-null rule in the membership seam.
What Value Command Commit
       

7. Notes

8. 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
     

9. PRs

PR Title
#2987 [iam] Give the reporting line its own write

10. Review

Comment summary File Decision Notes
       

11. Result

Done 2026-10-09.

The reporting line has its own write, so a screen can change one field without sending the rest of the account back:

Layer What landed
Message iam.v1.ops.set_reporting_line: the account, the manager (empty clears), the version the caller read, and the reason.
Service set_reporting_line reads the account, refuses a stale version as conflict, refuses a self reference, writes the one field, and answers the account as written.
Handler Served behind iam::accounts:update, so a caller without it is refused before the service is reached.
Clients setReportingLine in the wire seam, PUT /api/accounts/:id/reporting-line, api.setReportingLine(), and the generated shell command account_operations set-reporting-line.
Tests Three C++ cases and two BFF cases.

Verification. check_component_drift.py --component iam reports no drift and clang-format is clean. The IAM core suite passes, 264 cases and 821 assertions (three new). The BFF membership tests pass, 8 cases. The three TypeScript packages typecheck.

What the task taught. The first draft carried std::optional<int> for the version and the shell facet refused the whole component: an optional scalar in a message has no shell token form. The field is text with empty meaning "not stated", which is the wire's existing idiom for an optional value; the generator gap is captured rather than papered over.

Emacs 29.3 (Org mode 9.6.15)