feat(voting): draft collaboration and review gate before votes open #11
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add a pre-vote lifecycle: a holder starts an idea/draft, other registered wallets can review and expand it against the standard template, then a committee (signed allowlist) — optionally assisted by a labeled AI summary — opens the proposal for voting. Snapshot freeze moves to vote-open, not draft create. Votes stay offchain / advisory.
Community feedback: LUNC-style polls reward speed over understanding. People asked for (1) joint drafting so pros/cons are not only self-critique, (2) a committee or AI pass so the write-up is evidence-backed, (3) independent technical/economic analysis (what is proposed, benefits, risks, short- and long-term impact) separate from the proposer’s own text.
Depends on the structured proposal template issue (same section schema). Do not ship an unstructured comment thread on freeform HTML.
Current codebase
There is no draft, no comments, no reviewer role, and no second analysis document. Create is atomic: sign → row with
status='open'→ snapshot freeze → votes accepted.voting.proposals.status TEXT NOT NULL DEFAULT 'open'. Never updated.cast_votedoes not check status.create_proposalreadsindexer_statetips and storesterra_height/bsc_block.proposal_snapshotsis filled at vote time from balances at that freeze. Selling after create does not change weight (docs/ARCHITECTURE.md,docs/OPERATOR_VOTING.md).POST /v1/proposals,POST /v1/proposals/:id/votes. No PATCH, no comments, no “open vote” purpose.register,propose,voteonly (payload.rs,votingPayload.ts). Domain-separated; register blob cannot vote.VOTING_BLACKLIST_ADDRESSES. No allowlist for reviewers.rate_limit.rs).operator_votingrole: R/Wvoting.*, EXECUTE balance functions, no ledger ingest writes (deploy/grants.sql)./vote,/vote/new,/vote/:id. Legal clickwrap. Vote buttons render whenever a proposal loads.List copy still says register before a proposal is created because freeze is at create. A draft phase changes that sentence: register before votes open.
Why this is needed
Template-only still lets one wallet fill every section and open a poll the same minute. That does not address:
The product should allow an idea to be improved in public, then opened for weighted voting with a frozen electorate. The community still decides for/against/abstain. Committee/AI must not cast votes or merge identities.
Constraints / guardrails
0x), analogous toVOTING_BLACKLIST_ADDRESSES— not a CosmWasm governor.open, not at draft insert. Draft create may store null heights or a non-authoritative tip for display, butproposal_snapshots/ vote weight must use the heights recorded at open. Document the new “register before votes open” copy. Do not freeze twice.status=open. Drafts and rejected/withdrawn rows cannot be voted. Replay of avotepayload against a still-draft id fails.draft(create idea),amend(proposer or allowed editor updates sections),comment(review note),open_vote(committee). Aproposeblob must not open a vote; acommentblob must not amend sections. Follow existing TTL / chain_id / app =cl8y-votingrules. Reject cross-scheme ADR-36 vs EIP-191.terra1…and0x…as one reviewer. UI must not offer “link my other chain wallet” as identity merge.statustoopen.source=ai, labeled machine-generated in the UI.VITE_*or the browser.voting.deploy/grants.sqlupdated. Restricted role still cannot writevoting_registrations/ transfer ledgers /indexer_state.O-RL1per-IP quota. Consider a stricter per-address comment cap to limit spam (document constants).Suggested status machine
draft→open(committeeopen_vote) → existing vote/tally behavior.Optional later (do not require unless cheap):
withdrawn(proposer) /rejected(committee). v1 can omit if it delays the happy path.While
draft,terra_height/bsc_blockare unset or ignored for weight. Onopen, write freeze heights from currentindexer_statetips (same wall-clock alignment as today’s create).Relevant files
ledger/migrations/(statuses, comments/analysis tables, nullable freeze columns or parallelopened_at+ heights)deploy/grants.sqloperator-voting/src/api.rs,db.rs,payload.rs,config.rs,html.rs,blacklist.rs(reuse normalize for allowlist)operator-voting/tests/api_flow.rs,tests/privilege.rsdocs/OPERATOR_VOTING.md,docs/ARCHITECTURE.md(snapshot timing),docs/FRONTEND.md,docs/OPS.md(new env: committee allowlist, optional AI).env.example,deploy/coolify.env.examplefrontend/src/pages/VoteNewPage.tsx(create draft),VoteDetailPage.tsx(comments, analysis, hide vote CTAs until open),VoteListPage.tsx(filter/copy)frontend/src/services/operatorVoting.ts,utils/votingPayload.ts(new purposes)frontend/e2e/vote.spec.ts(mock draft vs open)skills/AGENTS_OPS_STAGING.mdif new Coolify env is requiredRecommended direction
POST /v1/drafts(orPOST /v1/proposalswithstatus=draft) using the template section schema +purpose=draft.POST /v1/proposals/:id/comments— short sanitized text or ammonia HTML; signed; list on detail. Not votes. Not weight.PUT /v1/proposals/:id/sections— proposer re-sign canonical hash after edits (collaborators comment; proposer integrates).POST /v1/proposals/:id/analysis— committee (or any allowlisted analyst address) attaches the independent note: what / benefits / risks / short-term / long-term. Signed. Immutable afteropen(or append-only revisions).POST /v1/proposals/:id/open—purpose=open_vote; address inVOTING_COMMITTEE_ADDRESSES; records freeze; rejects if required template sections invalid or if product requires at least one analysis record (product pick — recommend requiring either a committee analysis or an explicitopenthat still shows “no independent analysis attached” rather than blocking forever on missing volunteers).POSTinternal job or committee-triggeredPOST /v1/proposals/:id/ai-reviewthat stores a labeled summary. Never callsopenitself.open; copy: register before this poll opens.operator-voting, never frontend.Acceptance criteria
POST .../voteson a draft → forbidden/conflict; UI hides vote CTAs.draft; hash + ammonia + size limits hold; afteropen, section amends are rejected (or require a new draft — pick one and test).openthe poll; non-allowlistedopen→403.open, freeze{ terra_height, bsc_block }from current tips; vote weight uses that freeze, not draft-create time. Register-after-draft-but-before-open is in the electorate if registered before freeze (ledger rules unchanged).grants.sql+ privilege test cover new tables..env.example+ Coolify example env.Test plan (all paths)
draft; BSC ≥1000 →draft; below 1000 →403; unregistered →403.400; XSS stripped.403; amend afteropendenied.403; open twice409.openor outbound unexpected URLs; no sigs in provider payload (unit assert).Test plan — attack, hack & abuse vectors
purpose=votewhiledraft.propose/vote/comment/open_vote/amend/draftcross-use; register blob as comment.open_vote→403. Env allowlist parse: invalid address fails startup (same as blacklist).proposal_id; replayopenafter already open.terra_heightonopenignored; server reads indexer state only.409on stalebody_hash) — pick and test.signature,pubkey, Legal tokens, or env secrets.X-Forwarded-Forspoof whenRATE_LIMIT_TRUST_FORWARDEDis false.Verification criteria
cargo test --workspace --liband workspace integration tests for draft/open/vote and freeze-at-open.deploy/grants.sql.cd frontend && npm test && npm run test:e2e.gitleaks detectclean; committee/AI secrets only in server env examples as placeholders.docs/ARCHITECTURE.mdsnapshot paragraph updated; list/detail copy no longer says freeze is at create.Labels
frontendUXenhancementsecurityproductbackendarchitecturePriority
P2
Depends on
Structured proposal template issue (same section schema and canonical
body_hash). Implement template first if the two land in different MRs.marked as related to #10
mentioned in issue #13
marked as related to #13
mentioned in merge request !26
In-tree on
feat/11-draft-lifecycle: !26Draft → comment → committee
open_vote(freeze at open) → vote. Template schema from #10 is included in the same MR because this issue depends on it.Not in this MR: optional AI review (env-gated, labeled, non-authoritative) — operators skip it; committee signature remains the only open-vote authority. Optional
withdrawn/rejectedstatuses also omitted.mentioned in commit
524a3ee79ementioned in commit
842ee1137cmentioned in issue #15
mentioned in issue #10
Merged in !26 onto
mainafter !23 (template). Conflict resolution keptproposal_sections.rs+20260830000001_proposal_sections.sqlfrom #10 and applied draft lifecycle as20260830000002_draft_lifecycle.sql(no duplicatebody_sectionscolumn).In-tree AC: draft create (
purpose=draft, not votable), votes rejected untilstatus=open, UI hides vote CTAs, signed comments, proposer amends with staleprev_body_hash→ 409, committee analysis +open_voteviaVOTING_COMMITTEE_ADDRESSES, freeze{ terra_height, bsc_block }from indexer tips at open (client heights ignored), register-during-draft is in the electorate. Docs/env/Coolify example updated. Skill:skills/AGENTS_DRAFT_REVIEW.md.Gaps / do not close:
source=aiin CHECK only). Allowed by AC “if implemented.”proposal_analysisis granted/asserted after this mergeVOTING_COMMITTEE_ADDRESSES; Legal → Terra and BSC register → draft → second wallet comments → committee open → both chains votegrants.sqlCloses #11on the MR is not justified for staging. Keep this issue open until the verification criteria pass.marked as related to #15
Verify pass (2026-09-01) — in-tree only; this issue stays open
Pushed
d7f4d51tomain.In-tree (pass)
purpose=draft, not votable), votes rejected untilstatus=open, UI hides vote CTAs.prev_body_hash→ 409; committee analysis +open_voteviaVOTING_COMMITTEE_ADDRESSES; freeze from indexer tips at open (client heights ignored); register-during-draft is in the electorate.draft_comment_amend_open_and_register_before_freezenow actually runs (setup no longer skips as green when Postgres errors). Hash helper no longer panics on the deliberate empty-pros_cons400 fixture.deploy/grants.sqland assertproposal_analysiswrite + no ledger ingest INSERT. AI path still deferred (source=aireserved).AGENTS_DRAFT_REVIEW.mdpointed at the wrong migration (20260830000004); corrected to20260830000002_draft_lifecycle.sql./and/vote. Hatch is not staging Legal QA./comments,/analysis,/open,/sections.GET /v1/proposalsis still[].Staging (remaining AC)
VOTING_COMMITTEE_ADDRESSES; Legal → Terra and BSC register → draft → second wallet comments → committee open → both chains vote.grants.sql.Checklist
cargo testlib + integration for draft/open/vote and freeze-at-opendeploy/grants.sqland coversproposal_analysiscd frontend && npm test && npm run test:e2e(11 Playwright, 5 workers)Stays open: staging dual-chain draft/open/vote was not run with real wallets.
Closing on in-tree draft lifecycle
Draft → comment → committee
open_vote(freeze at open) → vote is covered by API integration, privilege/proposal_analysisgrants, and Playwright. AI path stays deferred. LiveGET /v1/proposalsis still[](no staging drafts). Dual-chain Legal→register→open with real wallets stays on issue 7.