Skip to content

Atomic, idempotent proposeContribution (owner = origin controller) - #77

Draft
phoebus-84 wants to merge 2 commits into
feat/atomic-accept-contributionfrom
feat/atomic-propose-contribution
Draft

phoebus-84 wants to merge 2 commits into
feat/atomic-accept-contributionfrom
feat/atomic-propose-contribution

Conversation

@phoebus-84

Copy link
Copy Markdown
Collaborator

Workstream: #74 (non-closing reference). Stacked on #76 → #75 → #73 → #72 → #71. Draft: nothing to merge or deploy.

Why

The GUI's propose_contribution.tsx (and the SDK's proposeContribution) create a proposal in four non-transactional requests: process, proposal, three intents, three links. This causes three problems:

  • Partial failure leaves orphans. Unfinished intents naming the owner keep inviting them into the proposer's process (slice-3 §3.2), with no proposal referencing them.
  • A retry makes a second proposal.
  • The owner was primaryAccountable. When that isn't the controller, nobody can accept: the named party lacks control and the controller isn't named.

What

proposeContribution(resourceForkedId, resourceOriginId, name, note) → { proposal process citeIntent acceptIntent modifyIntent created }, in one transaction, through the existing Process/Intent/Proposal/ProposedIntent domain functions and their policy.

  • owner = the origin's controller, a Person or an Organization. With an Organization, its active admins can accept through the existing event rules.
  • proposer = the signer, who must control the fork (resource.update on it). Otherwise anyone could submit someone else's work, and acceptance would append it to the origin's relations.
  • Idempotent: a pending proposal by the same signer for the same fork → origin and owner is returned with created: false. After a rejection or acceptance, a new one can be made.
  • Intent quantities follow each resource's on-hand quantity and unit, which is what acceptContribution (Atomic, idempotent acceptContribution for contribution proposals #76) needs.
  • Errors: fork = origin, unknown resource, or an origin without a controller → BAD_USER_INPUT. A fork the signer doesn't control → FORBIDDEN. Any later failure rolls everything back.
  • Locks: fork and origin resource rows FOR UPDATE, ascending id, first (same order as Atomic, idempotent acceptContribution for contribution proposals #76), which serializes concurrent proposals for the same pair.

Design: docs/atomic-propose-contribution.md.

Verification (local sandbox)

  • test/authz/propose_contribution.test.exs: 8 tests.
    • The owner is the controller even when primaryAccountable is someone else, end to end through acceptContribution.
    • A retry returns the same proposal and writes nothing; after a rejection a new one is allowed.
    • Someone else's fork → FORBIDDEN, nothing written.
    • Bad input cases.
    • An organization owner: a stranger is denied, the org admin accepts.
    • Quantities and units.
    • Rollback when the proposal fails after the process and intents were written.
  • test/authz/propose_contribution_concurrency.test.exs: 4 concurrent identical calls on separate connections → one proposal, every caller gets it, created is true exactly once.
  • These run together with Atomic, idempotent acceptContribution for contribution proposals #76's 17 tests: 26/26.
  • Full suite, seed 590095: 898 tests, 3 failures:
    • classifications/1, a known failure;
    • claimable_agents defaults to empty, from the local stack config;
    • one DB.Page cursor test. It is pre-existing and now explained: ID.gen/0 is a ULID with millisecond time + 10 random bytes, so people created in the same millisecond are not ordered, and the page test's tight insert loop assumes they are. This also explains the note on Atomic, idempotent acceptContribution for contribution proposals #76.

Not in this PR

  • SDK and GUI switch to it in separate stacked PRs.
  • The old multi-request sequence keeps working.
  • Existing orphan intents are not cleaned up.
  • Accepting requires the owner to have custody of the origin. This is the existing accept event rule; with an Organization owner, custody must be the Organization's.

🤖 Generated with Claude Code

proposeContribution(resourceForkedId, resourceOriginId, name, note)
creates the signer's process, the cite/accept/modify intents, the proposal
and its three links in one transaction, through the existing domain
functions and policy. The owner is the origin's controller (Person or
Organization), not its primaryAccountable; the signer must control the
fork. An existing pending proposal by the same signer for the same fork
and origin is returned (created: false). Intent quantities follow each
resource's on-hand quantity and unit, as acceptance requires.

Refs #74

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@phoebus-84

Copy link
Copy Markdown
Collaborator Author

Merged the #76 update (a31d216: acceptance finishes the intents) into this branch (1fcc687) rather than rebasing, to keep the published history. Targeted 28/28; real signed propose → accept re-run green. A proposal accepted this way is no longer pending, so the same fork → origin can be proposed again.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant