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_linewrites 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 iamreports no drift, and the C++ and BFF tests cover it.
4. Plan
- Declare
iam.v1.ops.set_reporting_line: the account, the manager, the version the caller read, and the reason. - 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.
- Serve it from the account handler behind
iam::accounts:update. - Wire the browser path, and let codegen emit the shell command.
- 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.
wireAccountSchemaandmapAccountbecame 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.