diff --git a/CHANGELOG.md b/CHANGELOG.md index cab6c43..eff4eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,51 @@ skip versions are the composition of each intervening adjacent release's steps in order — no A-to-B path is pre-computed beyond that. +## 0.4.0 — 2026-05-27 + +**Minor — no operator action required beyond rebuild + restart.** The +proposer of a new RFC is now the implicit first owner of its super- +draft entry, set automatically at propose time from the session user. +The §13.1 claim flow remains available for *additional* owners. No +schema changes, no env-var changes, no API-shape changes; only newly +proposed RFCs receive the auto-owner — existing super-drafts whose +`owners:` is empty are unaffected and can still be claimed via §13.1 +as before. This is a §19.3 rule-2 spec correction: `SPEC.md` §9.1, +§9.2, and §13.1 are updated to reflect the new shape. + +### Changed + +- **`POST /api/rfcs/propose`** (`backend/app/api.py`) now sets + `Entry.owners = [user.gitea_login]` when constructing the new + super-draft entry, instead of `owners=[]`. The session's + `gitea_login` is the canonical source; the endpoint never accepted + an owner field from the request payload and still doesn't. +- **`SPEC.md` §9.1** narrowed: the "no proposed-owner or working- + group fields" sentence becomes a proposer-owner-auto / working- + group-deferred split, with a §19.3 rule-2 note. +- **`SPEC.md` §9.2** frontmatter shape: `owners: []` → `owners: + []`, with a §19.3 rule-2 note. +- **`SPEC.md` §13.1** reframed: claim flow is now a graduation-time + broadening for additional owners, not a precondition for the + proposer's own RFC. The §13.1 / §13.2 / §13.3 graduation pipeline + itself is unchanged — the "at least one owner" precondition for + graduation still holds and is now satisfied by default. + +### Upgrade steps (from 0.3.0) + +1. The deployment **MUST** rebuild and restart per the routine + deploy steps. No `.env` changes, no schema/migration changes, no + API-shape changes. +2. Operators **SHOULD** note that newly proposed RFCs after the + upgrade carry the proposer in `owners:` automatically. Existing + super-drafts with empty `owners:` are not migrated; they remain + claimable via the §13.1 flow exactly as before. No deployment- + side data action is required. +3. Deployments **MAY** communicate the UX shift to active proposers + (the "Claim ownership" affordance no longer applies to your own + newly proposed RFC), but the affordance simply hides on RFCs the + viewer already owns, so no operator-side action is required. + ## 0.3.0 — 2026-05-27 **Minor — operator action required if a deployment wants to enable the diff --git a/SPEC.md b/SPEC.md index d69022e..ba3f9e6 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1120,12 +1120,20 @@ The modal collects four fields: type their own. No proposer name or email — the logged-in identity is canonical and -need not be retyped. No proposed-owner or working-group fields — -ownership flows through the post-merge claim flow (§13.1), and +need not be retyped. No proposer-owner field either: the proposer is +implicitly the first owner of their own RFC, set automatically at +submit-time from the session user (see §9.2); the post-merge claim +flow (§13.1) remains for *other* contributors to add themselves as +owners on an RFC they didn't propose. No working-group field — arbiters are admin work, not the proposer's call. AI's drafting role is intentionally narrow: tag suggestions only. The proposer is making a specific claim about a specific word, and having AI propose -the claim for them would undercut the gesture. +the claim for them would undercut the gesture. (§19.3 rule-2 +correction: 0.4.0 narrowed this paragraph; the prior version asserted +"no proposed-owner field" without qualification, but running code +revealed that forcing a separate claim-flow gesture for the +proposer's own RFC was UX friction with no benefit — the proposer +already self-identified by proposing.) Primary action: **"Open proposal PR"** — naming the actual Git artifact produced, consistent with §10.1's *Open PR* and §13's @@ -1141,7 +1149,13 @@ is populated from the modal and session: - `state: super-draft`, `id: null`, `repo: null`, `graduated_at: null`, `graduated_by: null` — fixed at creation. - `proposed_by: `, `proposed_at: ` — auto. -- `owners: []` — empty; the claim flow (§13.1) fills this. +- `owners: []` — the proposer is the first + owner at propose time, set automatically from the session user. + The §13.1 claim flow remains available for *additional* owners on + the same entry. (§19.3 rule-2 correction: 0.4.0 changed this from + `owners: []`; the prior shape required a separate claim-flow + gesture for the proposer's own RFC, which running code surfaced + as needless UX friction.) - `arbiters: []` — empty; arbiters are admin work, not the proposer's call. @@ -1718,11 +1732,20 @@ PR do not block graduation; they remain on the meta repo subject to ### 13.1 Claim ownership (prerequisite) -If a super-draft has no owner, any signed-in contributor can click -"Claim ownership," which opens a PR against the meta repo adding their -username to the `owners:` field of the entry. Owners and admins can -merge. (A self-merge window for un-acted claims is not enabled in v1; -configurable later if needed.) Multiple claims simply append. +The proposer is already the first owner of any super-draft they +proposed (per §9.2; auto-set at propose time). The claim flow exists +for *additional* contributors to become owners on an existing super- +draft — typically a draft whose proposer has stepped away, or a draft +graduating with a working group. Any signed-in contributor can click +"Claim ownership," which opens a PR against the meta repo adding +their username to the `owners:` field of the entry. Owners and admins +can merge. (A self-merge window for un-acted claims is not enabled in +v1; configurable later if needed.) Multiple claims simply append. +(§19.3 rule-2 correction: 0.4.0 narrowed this section's framing; the +prior version implied owners always started empty and the claim flow +was a hard prerequisite to graduation, but with the proposer now +auto-set as first owner, the claim flow is a graduation-time +broadening rather than a precondition for the proposer's own RFC.) ### 13.2 The Graduate dialog diff --git a/VERSION b/VERSION index 0d91a54..1d0ba9e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.0 +0.4.0 diff --git a/backend/app/api.py b/backend/app/api.py index e0ab320..b00bd32 100644 --- a/backend/app/api.py +++ b/backend/app/api.py @@ -298,7 +298,11 @@ def make_router( proposed_at=entry_mod.today(), graduated_at=None, graduated_by=None, - owners=[], + # §9.2: the proposer is the implicit first owner at propose time. + # The §13.1 claim flow exists for *other* contributors to become + # owners on an RFC they didn't propose; the proposer never needs + # to claim their own RFC. + owners=[user.gitea_login], arbiters=[], tags=[t.strip() for t in payload.tags if t.strip()], body=payload.pitch.strip() + "\n", diff --git a/backend/tests/test_propose_vertical.py b/backend/tests/test_propose_vertical.py index ec7b9ff..4722bdc 100644 --- a/backend/tests/test_propose_vertical.py +++ b/backend/tests/test_propose_vertical.py @@ -496,6 +496,11 @@ def test_propose_to_super_draft_vertical(app_with_fake_gitea): proposal = r.json() assert proposal["entry"]["title"] == "Open Human Model" assert proposal["entry"]["state"] == "super-draft" + # §9.2: the proposer is the implicit first owner at propose time. + # The owners field is a single-element list containing exactly the + # session user's gitea_login — no request-supplied owner field + # exists or is honored. + assert proposal["entry"]["owners"] == ["alice"] assert proposal["affordances"]["merge"] is True # Owner merges. The catalog picks up the new super-draft. @@ -516,6 +521,10 @@ def test_propose_to_super_draft_vertical(app_with_fake_gitea): view = r.json() assert view["state"] == "super-draft" assert "shared definition" in view["body"] + # §9.2: the auto-set proposer-owner survives the meta-repo round-trip + # — it's in the file's frontmatter on main after merge, not just + # in the pending-PR view above. + assert view["owners"] == ["alice"] # The pending-ideas list no longer carries the merged proposal. r = client.get("/api/proposals") @@ -568,6 +577,37 @@ def test_anonymous_cannot_propose(app_with_fake_gitea): assert r.status_code == 401 +def test_proposer_is_auto_owner_request_payload_ignored(app_with_fake_gitea): + """§9.2: the owners field on the new entry is always exactly + `[session.gitea_login]`. The propose endpoint never accepts an owner + from the client; a request payload that smuggles one in is ignored + by the Pydantic body model and the auto-set value lands instead. + """ + from fastapi.testclient import TestClient + app, _fake = app_with_fake_gitea + with TestClient(app) as client: + provision_user_row(user_id=11, login="alice", role="contributor") + sign_in_as(client, user_id=11, gitea_login="alice", display_name="Alice", role="contributor") + # Extra unknown fields like `owners` are dropped by the + # ProposeBody model; the session user is the only source of truth. + r = client.post("/api/rfcs/propose", json={ + "title": "Spoof attempt", + "slug": "spoof-attempt", + "pitch": "p", + "tags": [], + "owners": ["mallory", "eve"], + "proposed_by": "mallory@test", + }) + assert r.status_code == 200, r.text + pr_number = r.json()["pr_number"] + r = client.get(f"/api/proposals/{pr_number}") + assert r.status_code == 200, r.text + entry = r.json()["entry"] + assert entry["owners"] == ["alice"] + # proposed_by also comes from the session, never the body. + assert entry["proposed_by"] in ("alice@test", "alice") + + def test_withdraw_by_proposer_works(app_with_fake_gitea): from fastapi.testclient import TestClient app, _fake = app_with_fake_gitea diff --git a/frontend/package.json b/frontend/package.json index 40f0b02..e7d105d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "rfc-app-frontend", "private": true, - "version": "0.3.0", + "version": "0.4.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 26f0bf6..c55af28 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -159,6 +159,7 @@ export default function App() { {proposeOpen && viewer && ( setProposeOpen(false)} onSubmitted={({ pr_number }) => { setProposeOpen(false) diff --git a/frontend/src/components/ProposeModal.jsx b/frontend/src/components/ProposeModal.jsx index 9a59486..3e54f1b 100644 --- a/frontend/src/components/ProposeModal.jsx +++ b/frontend/src/components/ProposeModal.jsx @@ -20,7 +20,7 @@ function slugify(title) { .replace(/^-+|-+$/g, '') } -export default function ProposeModal({ onClose, onSubmitted }) { +export default function ProposeModal({ viewer, onClose, onSubmitted }) { const [title, setTitle] = useState('') const [slug, setSlug] = useState('') const [slugEdited, setSlugEdited] = useState(false) @@ -131,6 +131,12 @@ export default function ProposeModal({ onClose, onSubmitted }) { )} + {viewer && ( +

+ Owner: {viewer.display_name || viewer.gitea_login} — you'll be the first owner of this super-draft. Additional owners can claim later (§13.1). +

+ )} + {error &&

{error}

}