Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable user-facing changes to this project will be documented in this file.

## Unreleased

- The Submit Contribution page now shows a compact "Before you submit" guidance card that adapts to the selected type: a personal weekly slot counter for Projects, quality bars for Projects and Intelligent Contracts, a milestone eligibility check, one-click switching between related types, and review/appeal rules tucked behind a collapsed accordion (also shown on the Projects type page) (eddbe897)

- Finishing the Creator or Builder journey now actually grants the role: since late June the final "Become a Creator" / "Claim Builder Role" step failed for every new member with a generic error, and completion errors now show their real reason instead of a dead-end "try again" (9d546e70)

- Validators can link Telegram support groups to their validator: generate a one-time code on the new Telegram Support page, paste it in a Telegram group with the Deckard support bot, and the group is bound to the validator (multiple groups supported, codes expire in 48 hours and can be revoked) (0cd7e5f)
Expand Down
15 changes: 15 additions & 0 deletions backend/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ backend/
- ContributionType - Categories with slug field, M2M `accepted_evidence_url_types`, optional global lifetime `max_submissions`, and optional `max_submissions_per_user_per_week`. The weekly limit uses Monday-Sunday UTC `SubmittedContribution.created_at` bounds and counts every state; edits/appeals reuse the same row and do not consume another slot. If an editable submission changes contribution type, the target type's capacity is checked in the submission's original creation week. `requires_ai_review` gates unengaged pending submissions from tier-1 stewards, while `escalation_threshold_points` converts tier-1 accepts whose contribution-date multiplied points meet the threshold into proposals. Migration 0084 backfills Builder-category types once; new Builder types default to `True` / `400` for values not supplied explicitly, and later explicit admin/model updates remain unchanged.
- AIReviewFeedback - Per-reviewer, per-AI-proposal benchmark feedback with an immutable `(proposal_source, proposal_source_id)` binding, timestamp metadata in `proposal_ref`, verdict, optional corrected decision/rubric ranges, typed anchored error claims, and a best-effort commit SHA pinned on first save. Records are unique by `(submitted_contribution, reviewer, proposal_source, proposal_source_id)` and never alter submission review state.
- SubmissionStateTransition - Append-only lifecycle log for submissions (migration 0079). One row per event (`submitted`/`review`/`bulk_reject`/`gate_reject`/`edited`/`canceled`/`appeal`/`evidence_added`/`escalated`/`admin`) with from_state/to_state/actor. Written by every path that changes `state` or clears `reviewed_by`/`reviewed_at` (creation via post_save signal; the rest inline at each call site, incl. the Tier-1 gate command and admin `save_model`). Never mutate or delete rows; read-only in admin. Rationale: row state is overwritten in place and re-open paths destroy review fields, so this log is the only durable decision/lifecycle history. Bulk reject also writes a per-submission decision SubmissionNote (`data.action='reject'`, `data.bulk=true`) so bulk decisions appear in the CRM timeline and note-based metrics like single rejects. The dead `resubmitted_more_info` filter (relied on `reviewed_at` surviving edits, impossible since 2026-06-22) was removed from both filtersets and the steward search grammar.
- SubmissionMoreInfoResponse - Immutable submitter response paired one-to-one with a structured request-more-info `SubmissionNote`, plus a nullable request-note FK for legacy `staff_reply` requests. It snapshots the request text/author/time, responder, and capped 1,000-character response so every request cycle remains auditable after the submission returns to pending. Submitter and steward serializers expose it as `more_info_requests[].response`; legacy requests expose `response: null` until actually answered and never receive fabricated history.
- Human review hierarchy - `Steward.tier` is reviewer (1), top-level (2), or apex (3); steward superusers act as tier 3. Tier 2+ gets the full permission matrix and unrestricted submission visibility. `SubmittedContribution.escalated_at` marks an active threshold proposal and is exposed only through the steward serializer. Tier-1 AI-gated visibility accepts durable AI proposal notes/rows, any active proposal, appeals, and transition-backed more-info resubmissions. Search supports `is_escalated`; the portal grammar maps `is:escalated` / `not:escalated`.
- Reviewer reward economy - Direct tier-1 decisions on threshold-enabled types earn 10 points for reviewing another user's submission; dedup uses the exact notes key `Review decision reward for submission {submission.id} [{action}]`, while bulk rejects, escalated accepts, and tier 2+ decisions are excluded. Every escalation records a human `ReviewProposal`, so a different finalizer rewards the escalator using rubric agreement for Builder Projects or binary action agreement for standard flows. All human-proposal rewards are then reduced proportionally when final points differ from proposed points.
- **Projects/Milestones split**: `contributions/project_milestones.py`
Expand Down Expand Up @@ -478,10 +479,24 @@ DELETE /api/v1/contributions/{id}/ (requires auth)

# Submissions (submitter-side)
GET /api/v1/submissions/my/ (requires auth, paginated user submissions)
GET /api/v1/submissions/{id}/ (requires auth, owner-only source/detail lookup)
PUT /api/v1/submissions/{id}/ (requires auth, owner-only editable submission update)
PATCH /api/v1/submissions/{id}/ (requires auth, owner-only editable submission update)
GET /api/v1/submissions/accepted-projects/ (requires auth, the user's highlighted Projects contributions milestones can link to; optional ?submission=UUID includes that pending milestone's grandfathered current link; includes next_milestone_version and github_url)
POST /api/v1/submissions/{id}/appeal/ (requires auth, owner-only, one per submission)
POST /api/v1/submissions/{id}/add-evidence/ (requires auth, owner-only)

# More-info resubmission contract
# A PUT/PATCH while state=more_info_needed must include this write-only object:
# "more_info_response": {"request_id": 123, "message": "What changed"}
# request_id is null only for the current legacy staff_reply fallback. The
# server locks the row, revalidates the resulting snapshot with current type,
# mission, role/social/Discord, capacity and evidence rules, writes the paired
# response, clears review fields, and transitions the same row to pending in
# one transaction. Blank, stale, mismatched, duplicate and pending-state
# responses are rejected. Read payloads expose the result under
# more_info_requests[].response as {id, message, user, user_name, created_at}.

# Contribution Types
GET /api/v1/contribution-types/ (requires auth)
GET /api/v1/contribution-types/{id}/ (requires auth)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('contributions', '0084_enable_builder_review_hierarchy'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='SubmissionMoreInfoResponse',
fields=[
(
'id',
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name='ID',
),
),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('request_message', models.TextField()),
('requested_at', models.DateTimeField(blank=True, null=True)),
('message', models.TextField(max_length=1000)),
(
'request_note',
models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='submitter_response',
to='contributions.submissionnote',
),
),
(
'requested_by',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='more_info_requests_answered',
to=settings.AUTH_USER_MODEL,
),
),
(
'responder',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='submission_more_info_responses',
to=settings.AUTH_USER_MODEL,
),
),
(
'submitted_contribution',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='more_info_responses',
to='contributions.submittedcontribution',
),
),
],
options={
'ordering': ['-created_at', '-id'],
'indexes': [
models.Index(
fields=['submitted_contribution', 'created_at'],
name='sub_info_resp_created_idx',
),
],
},
),
]
52 changes: 52 additions & 0 deletions backend/contributions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,58 @@ def __str__(self):
return f"{prefix}{self.user} on {self.submitted_contribution} at {self.created_at}"


class SubmissionMoreInfoResponse(BaseModel):
"""A submitter's durable response to a steward more-information request.

New responses point at the structured ``SubmissionNote`` that recorded the
request. ``request_note`` remains nullable so legacy submissions whose
request only survives in ``staff_reply`` can still be answered; the request
snapshot fields preserve that context after ``staff_reply`` is cleared.
"""

submitted_contribution = models.ForeignKey(
SubmittedContribution,
on_delete=models.CASCADE,
related_name='more_info_responses',
)
request_note = models.OneToOneField(
SubmissionNote,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='submitter_response',
)
request_message = models.TextField()
requested_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='more_info_requests_answered',
)
requested_at = models.DateTimeField(null=True, blank=True)
responder = models.ForeignKey(
settings.AUTH_USER_MODEL,
null=True,
blank=True,
on_delete=models.SET_NULL,
related_name='submission_more_info_responses',
)
message = models.TextField(max_length=1000)

class Meta:
ordering = ['-created_at', '-id']
indexes = [
models.Index(
fields=['submitted_contribution', 'created_at'],
name='sub_info_resp_created_idx',
),
]

def __str__(self):
return f"Response on {self.submitted_contribution_id} at {self.created_at}"


class SubmissionStateTransition(BaseModel):
"""
Append-only log of submission lifecycle events.
Expand Down
Loading
Loading