Slice 5: graduation per §13
The §13.3 transactional sequence flips a super-draft to active — five steps with paired undoes, an in-process orchestrator fed by an asyncio.Queue, the §17 SSE endpoint streaming step transitions to the dialog. Each step is a new bot primitive that logs an `actions` row, bracketed by `graduate_start` / `graduate_complete` for the linkable audit sequence. Rollback runs the undoes in reverse from the last completed step; merge_pr has no undo by design per §13.5. The §9.8 precondition gate is enforced server-side at the top of POST /graduate so the §13.3 rollback complexity does not grow. The §13.4 chat migration is a database semantic no-op — the (slug, branch_name='main') threads keep their identity, only the interpretation changes. The §9.8 pre-graduation history surfaces via a new _is_meta_target(rfc, branch) dispatch helper and lands as pre_graduation_history on /main. §13.1 claim flow landed alongside since it's the prerequisite for non-admin graduation — bot.open_claim_pr plus broadening api_prs._require_pr to accept meta_claim. 45/45 tests green; ten new integration tests cover the validator, the §9.8 precondition refusal, happy path with audit verification, mid-sequence rollback at steps 2 and 3, concurrent refusal, chat-survives-without-data-movement, pre-graduation history, and the §13.1 claim PR cycle. SPEC.md §19.1 rewritten for Slice 6 (notifications); §19.2 grew four candidates surfaced during the slice. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,8 @@ import ChatPanel from './ChatPanel.jsx'
|
||||
import ChangePanel from './ChangePanel.jsx'
|
||||
import DiffView from './DiffView.jsx'
|
||||
import PRModal from './PRModal.jsx'
|
||||
import GraduateDialog from './GraduateDialog.jsx'
|
||||
import { claimOwnership } from '../api'
|
||||
|
||||
const MANUAL_IDLE_MS = 5 * 60 * 1000 // §8.6 idle window; exact value is impl detail.
|
||||
const MANUAL_DEBOUNCE_MS = 800
|
||||
@@ -108,6 +110,8 @@ export default function RFCView({ viewer }) {
|
||||
// metadata pane, and the start-contributing dispatch target.
|
||||
const isSuperDraft = entry?.state === 'super-draft'
|
||||
const [showMetadataPane, setShowMetadataPane] = useState(false)
|
||||
const [showGraduateDialog, setShowGraduateDialog] = useState(false)
|
||||
const [claimError, setClaimError] = useState(null)
|
||||
|
||||
// Load main view + branch view whenever slug/branch changes.
|
||||
useEffect(() => {
|
||||
@@ -538,8 +542,42 @@ export default function RFCView({ viewer }) {
|
||||
Metadata
|
||||
</button>
|
||||
)}
|
||||
{isSuperDraft && viewer && entry?.owners && !entry.owners.includes(viewer.gitea_login) && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-link"
|
||||
onClick={async () => {
|
||||
setClaimError(null)
|
||||
try {
|
||||
const result = await claimOwnership(slug)
|
||||
if (result?.noop) return
|
||||
if (result?.pr_number) {
|
||||
navigate(`/rfc/${slug}/pr/${result.pr_number}`)
|
||||
}
|
||||
} catch (err) {
|
||||
setClaimError(err.message)
|
||||
}
|
||||
}}
|
||||
title="§13.1 — open a claim PR adding you to the entry's owners"
|
||||
>
|
||||
Claim ownership
|
||||
</button>
|
||||
)}
|
||||
{isSuperDraft && viewer && (viewer.role === 'owner' || viewer.role === 'admin' || (entry?.owners || []).includes(viewer.gitea_login)) && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary btn-graduate"
|
||||
onClick={() => setShowGraduateDialog(true)}
|
||||
title="§13 — graduate this super-draft to a per-RFC repo"
|
||||
>
|
||||
Graduate to RFC repo
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{claimError && (
|
||||
<div className="rfc-error-banner">Claim failed: {claimError}</div>
|
||||
)}
|
||||
|
||||
{/* Two columns: editor + chat */}
|
||||
<div className="rfc-body">
|
||||
@@ -693,6 +731,20 @@ export default function RFCView({ viewer }) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showGraduateDialog && (
|
||||
<GraduateDialog
|
||||
slug={slug}
|
||||
entry={entry}
|
||||
onClose={() => setShowGraduateDialog(false)}
|
||||
onCompleted={() => {
|
||||
setShowGraduateDialog(false)
|
||||
// The catalog row and the RFC view now reflect `active`.
|
||||
getRFC(slug).then(setEntry).catch(() => {})
|
||||
getRFCMain(slug).then(setMainView).catch(() => {})
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showMetadataPane && (
|
||||
<MetadataPaneModal
|
||||
slug={slug}
|
||||
@@ -776,6 +828,7 @@ function BranchDropdown({ current, mainView, isSuperDraft, onPick }) {
|
||||
// RFCs it is literally `main` per §8.1.
|
||||
const mainLabel = isSuperDraft ? 'canonical body' : 'main'
|
||||
const items = [{ name: 'main', label: mainLabel }, ...(mainView?.branches || [])]
|
||||
const preGrad = mainView?.pre_graduation_history || []
|
||||
const currentLabel = current === 'main' ? mainLabel : current
|
||||
return (
|
||||
<div className="branch-dropdown">
|
||||
@@ -804,6 +857,28 @@ function BranchDropdown({ current, mainView, isSuperDraft, onPick }) {
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
{preGrad.length > 0 && (
|
||||
<>
|
||||
<div className="branch-dropdown-section">
|
||||
Pre-graduation history ({preGrad.length})
|
||||
</div>
|
||||
{preGrad.map(b => (
|
||||
<button
|
||||
key={b.branch_name}
|
||||
type="button"
|
||||
className={`branch-dropdown-item pre-graduation ${b.branch_name === current ? 'active' : ''}`}
|
||||
onClick={() => { setOpen(false); onPick(b.branch_name) }}
|
||||
title="§9.8: meta-repo edit branch from before graduation — read-only"
|
||||
>
|
||||
<span className="branch-name">{b.branch_name}</span>
|
||||
<span className="branch-meta">
|
||||
{b.thread_count} thread{b.thread_count === 1 ? '' : 's'}
|
||||
{b.change_count ? ` · ${b.change_count} change${b.change_count === 1 ? '' : 's'}` : ''}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user