Task: Implement A write checks the version of another party's row
Table of Contents
This page documents a task in the A write checks the version of another party's row story. It captures the goal, current status, acceptance, and any notes or results.
1. Goal
Scope the current-row reads of a generated refdata service's write to the acting party, so the claim is checked against the row the write replaces.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent story | A write checks the version of another party's row |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-10-26 |
3. Acceptance
4. Plan
Diagnosis: prepare_change in the service template reads read_one(repo_, ctx_, key_from(out)), keyed by the model's key (id). Under row-level security a parent party sees its subsidiaries, so the read returns several rows and current.front() is any of them. stamp() then sets party_id from the context, so the write lands on the acting party's row, which the check never looked at.
- Add
of_acting_party()besidestamp()inhandler_helpers.hpp: keep the rows whoseparty_idis the context's party; a type with noparty_id, or a context with no party, keeps every row. - Use it in the template's entity half: the claim check in
prepare_change, the read-back of a put and a put many, andchanged_columns. The junction half is left alone, because thereparty_idnames the association's target. - Tangle the templates, regenerate the services, build, run the service suites.
- Prove it live: the convention edit from the Acme login party lands, and a stale version is a conflict.
5. Measurements
Every number this task claims about a tree, with the command that produced it
and the commit it was measured at. A number without both is not a measurement,
because a clean-up record describes a tree and trunk moves under a branch: a
consumer count, a gate's component count and a suite's size all change with no
code changing, and the reader of a bare number cannot tell which tree it came
from. Where the raw output matters, put it in an evidence/ file beside the
task and name that file in the Command column.
| What | Value | Command | Commit |
|---|---|---|---|
| Service files regenerated | 428 | git show --stat 8dbcc2f063 |
8dbcc2f063 |
| Handler helper tests | 18 cases, 43 assertions pass | ores.service.tests "[handler_helpers]" |
8dbcc2f063 |
| Live write from the Acme login party | refused on its term; saved as version 4; stale version a conflict; revert saved | the probe in the session scratchpad, against Acme | 8dbcc2f063 |
| Other parties' rows of that id | unchanged | a read of the live rows after the probe | 8dbcc2f063 |
6. Notes
- Root cause.
prepare_changeread the current row by the model's key, which for a convention is its id. Under row-level security a parent party's session sees its subsidiaries' rows of that id, socurrent.front()was any party's row, and the claim was checked against it.stamp()then set the acting party, so the write landed on a row the check never read. - Fix.
of_acting_party()besidestamp()keeps the context party's rows; a type with noparty_id, or a context with no party, keeps every row, and a row with no party is kept. The template's entity half uses it; the junction half does not, because thereparty_idnames the target. - Left. A delete and an unclaimed write read by key too; see the capture Scope a delete and an unclaimed write to the acting party.
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 |
|---|---|
9. Review
| # | Comment summary | File | Decision | Notes |
|---|---|---|---|---|
| 1 | A nil or empty party_id counts as another party, so a write may miss a shared row | handler_helpers.hpp | Accept | The compute tables hold an id once per tenant with a nullable party, so a strict filter breaks their amends. The rule now keeps only the acting party's rows when one exists, and otherwise keeps the rows as read. Tests cover nullopt, nil and empty. |
| 2 | Link the delete and unclaimed write capture from the story | story.org | Accept | The story links the capture by its id. |
| 3 | The filter runs after the read, which still loads every visible row | handler_helpers.hpp | Defer | The capture records that the read itself must be scoped by party. |
| 4 | The fx option ATM fix and the feed_seed.hpp entry are unrelated | — | Keep | Separate commits; both were needed for the gate on main. |
| 5 | Confirm the two ctest failures also fail on main | — | Accept | The gate on the rebased branch fails the same two suites: the vendor catalogue count (lseg 14, demo 71, expects 86) and the tick_clock timing test. The branch changes only the generated services in those components, and neither test reaches them. |
10. Result
- From the Acme login party, a convention edit lands against the version it read, and a stale version is still a conflict; other parties' rows of the same id are untouched.
- A put's reply carries the acting party's row.
- The build passes; the handler helper suite passes, with four new cases for
of_acting_party().