Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0f8b318afa |
@@ -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:
|
||||
[<proposer.gitea_login>]`, 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
|
||||
|
||||
@@ -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: <session email>`, `proposed_at: <today>` — auto.
|
||||
- `owners: []` — empty; the claim flow (§13.1) fills this.
|
||||
- `owners: [<proposer.gitea_login>]` — 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
|
||||
|
||||
|
||||
+5
-1
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rfc-app-frontend",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -159,6 +159,7 @@ export default function App() {
|
||||
</div>
|
||||
{proposeOpen && viewer && (
|
||||
<ProposeModal
|
||||
viewer={viewer}
|
||||
onClose={() => setProposeOpen(false)}
|
||||
onSubmitted={({ pr_number }) => {
|
||||
setProposeOpen(false)
|
||||
|
||||
@@ -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 }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{viewer && (
|
||||
<p className="field-help" style={{ marginTop: 14, marginBottom: 0 }}>
|
||||
Owner: <strong>{viewer.display_name || viewer.gitea_login}</strong> — you'll be the first owner of this super-draft. Additional owners can claim later (§13.1).
|
||||
</p>
|
||||
)}
|
||||
|
||||
{error && <p className="field-error">{error}</p>}
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
|
||||
Reference in New Issue
Block a user