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 iam reports no drift.

4. Plan

  1. Declare :prevent_cycle: true on the reports_to_account_id foreign key, the model's own way of asking for the guard.
  2. Regenerate the component, so the accounts insert trigger gains the recursive manager-chain check.
  3. 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_chain block 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 23514 when 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.

Emacs 29.3 (Org mode 9.6.15)