Task: Refuse a reporting line that would make a loop
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
The server refuses a reporting-line change that would close a cycle, and the refusal names the reason.
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
- A write that would make an account report to itself is refused by the store.
- A write that would close a longer cycle is refused, and the manager that was there stays.
- The refusal names the reason, so the screen can state it.
- A test covers the two-account cycle, the self cycle, and the manager that survives a refused write.
check_component_drift.py --component iamreports no drift.
4. Plan
- Declare
:prevent_cycle: trueon thereports_to_account_idforeign key, the model's own way of asking for the guard. - Regenerate the component, so the accounts insert trigger gains the recursive manager-chain check.
- Test the refusal at the service, where a write goes through the trigger.
5. Measurements
| What | Value | Command | Commit |
|---|---|---|---|
| IAM core suite after this change | 261 cases, 801 assertions | bash build/evidence/run_case.sh ores.iam.core.tests |
(branch tip) |
6. Notes
- The guard is the model's own, not a hand-written check. The codegen
templates already carry a recursive
ancestor_chainblock behind:prevent_cycle:, used by the business-unit and portfolio hierarchies. The account model simply had not asked for it, so the entry is a property and a paragraph rather than new C++. - The check walks the manager chain in SQL. It runs inside the accounts
insert trigger, which fires for every new version, and raises
23514when the chain reaches the row being written. That covers the self cycle and any longer ring, and it holds however the write arrives — the service, the shell, or the Acme publish. - The refused write leaves the old manager in place. The trigger rejects the insert before the version is written, so there is no half-applied change, and the test asserts that.
| 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 |
|---|---|
| #2985 | [iam] Refuse a reporting line that closes a cycle |
10. Review
| Comment summary | File | Decision | Notes |
|---|---|---|---|
11. Result
Done 2026-10-09.
The store refuses a reporting line that would close a cycle:
| Layer | What landed |
|---|---|
| Model | :prevent_cycle: true on reports_to_account_id, with the reason beside it. |
| Store | The regenerated accounts insert trigger walks the manager chain and raises 23514 when it reaches the row being written. |
| Tests | One service case: the two-account cycle is refused, the self cycle is refused, and the manager that was there survives the refused write. |
Verification. check_component_drift.py --component iam reports no drift.
The IAM core suite passes, 261 cases and 801 assertions. The database was
recreated so that the new trigger is the one under test.
One run of the suite failed first, on
an_approved_role_request_is_granted_once in
role_grant_applier_tests.cpp, an untouched test that asserts a fresh account
does not hold whichever role the read returns first; the same suite passed on
the next run. It is order-dependent and unrelated to this change, which touches
the account's reporting line and cannot affect a role grant. Captured at
A role-grant test depends on which
role comes first.