diff --git a/docs/02 - produto/feedback/feedback-reputacao.md b/docs/02 - produto/feedback/feedback-reputacao.md index 7bb1c939..288b4e21 100644 --- a/docs/02 - produto/feedback/feedback-reputacao.md +++ b/docs/02 - produto/feedback/feedback-reputacao.md @@ -102,15 +102,36 @@ genérico e destruiria a comparabilidade entre perfis. |---|---|---| | `COMUNICA_IMPEDIMENTOS` | 🗣️ Avisa antes de travar | Comunicou bloqueio ou atraso com antecedência útil | | `ENTREGA_O_COMBINADO` | 📦 Entrega o que assume | Cumpriu o escopo da stack que assumiu | +| `ENTREGA_NO_PRAZO` | ⏱️ Entrega no prazo | Cumpriu o prazo acordado, não apenas o escopo | | `APOIA_A_EQUIPE` | 🤝 Puxa a equipe junto | Ajudou colega, revisou trabalho de outro, destravou alguém | | `PERGUNTA_ANTES_DE_DECIDIR` | 🧭 Pergunta antes de decidir | Não inventou regra de negócio por conta própria | -| `RECEBE_REVISAO_BEM` | 🔄 Recebe revisão bem | Absorveu crítica técnica sem travar o fluxo do time | -| `AUTONOMIA_NA_STACK` | 🛠️ Resolve sem ser conduzido | Avançou na própria stack sem precisar ser conduzido | +| `RECEBE_REVISAO` | 🔄 Recebe revisão sem travar o time | Absorveu crítica técnica e seguiu, sem parar o fluxo | +| `ADAPTA_A_MUDANCA` | 🛠️ Se ajusta quando o plano muda | Seguiu produzindo quando escopo ou prazo mudaram | **Regra dos rótulos:** todo selo começa com verbo na terceira pessoa e descreve ação. É proibido rótulo com superlativo (*"Mestre em..."*), com traço inato (*"...nato"*) ou que implique escala (*"nível avançado em..."*). Superlativo pressupõe hierarquia, e hierarquia é ranking. + +#### Eixos avaliados e não incluídos — 19/08/2026 + +Registrado para não serem repropostos sem contexto. + +| Proposta | Por que ficou de fora | +|---|---| +| **Criatividade** | Não testemunhável — como se comprova? Julgamento de criatividade carrega viés documentado: a mesma ideia é lida como criativa ou estranha conforme quem propôs | +| **Disciplina** | Sinônimo de comprometimento no uso corrente, já coberto por *Entrega o que assume*. Carrega julgamento moral | +| **Trabalha bem em equipe** | Já coberto por *Puxa a equipe junto*, que diz a mesma coisa como ação observada em vez de qualidade da pessoa | +| **Comprometimento** | Idem — *Entrega o que assume* é a versão testemunhável | +| **Domínio da stack** | "Domínio" implica escala, e escala é nível. O portfólio já exibe as stacks assumidas por projeto — isso é registro factual, não precisa de atestado | +| **Aprende rápido** | Único eixo comparativo proposto: rápido em relação a quem? Fere o princípio 6 com mais força que qualquer outro, porque quem está começando *está aprendendo por definição*. Há ainda o efeito perverso de que "aprende rápido" costuma significar "já sabia" | +| **Encara stack que não conhecia** | Versão comportamental do anterior, e viável. Ficou de fora para não passar de sete eixos — com limite de 3, cada eixo a mais dilui a escolha. Primeiro candidato a entrar se algum eixo se mostrar morto pela métrica da seção 17 | +| **Autonomia na stack** *(existia na v2)* | Substituído por *Se ajusta quando o plano muda*. A frase original — "Resolve sem ser conduzido" — definia a pessoa pela ausência de ajuda e colidia com *Pergunta antes de decidir*, podendo ensinar a quem chega que pedir ajuda custa um selo | + +> 💡 **Critério que guiou a lista:** adjetivo é opinião, comportamento é +> testemunho. Como aval profissional, *"Entrega o que assume · 5 pessoas · +> 3 projetos"* vale mais que *"é comprometida"* — todo perfil se diz +> comprometido, e recrutador desconta adjetivo. ### 5.2 Como se avalia diff --git a/prisma/migrations/20260823193828_add_feedback_corroboracoes/migration.sql b/prisma/migrations/20260823193828_add_feedback_corroboracoes/migration.sql new file mode 100644 index 00000000..cd5e0047 --- /dev/null +++ b/prisma/migrations/20260823193828_add_feedback_corroboracoes/migration.sql @@ -0,0 +1,35 @@ +-- CreateEnum +CREATE TYPE "FeedbackAttrEnum" AS ENUM ('COMUNICA_IMPEDIMENTOS', 'ENTREGA_O_COMBINADO', 'APOIA_A_EQUIPE', 'PERGUNTA_ANTES_DE_DECIDIR', 'RECEBE_REVISAO_BEM', 'AUTONOMIA_NA_STACK'); + +-- AlterTable +ALTER TABLE "Feedback" ADD COLUMN "hiddenByAdmin" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "hiddenReason" TEXT, +ADD COLUMN "publicationAllowed" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "publishedByReceiver" BOOLEAN NOT NULL DEFAULT false, +ALTER COLUMN "rating" DROP NOT NULL, +ALTER COLUMN "anonymous" SET DEFAULT true; + +-- CreateTable +CREATE TABLE "FeedbackAttribute" ( + "id" TEXT NOT NULL, + "feedbackId" TEXT NOT NULL, + "attribute" "FeedbackAttrEnum" NOT NULL, + + CONSTRAINT "FeedbackAttribute_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "FeedbackAttribute_attribute_idx" ON "FeedbackAttribute"("attribute"); + +-- CreateIndex +CREATE UNIQUE INDEX "FeedbackAttribute_feedbackId_attribute_key" ON "FeedbackAttribute"("feedbackId", "attribute"); + +-- CreateIndex +CREATE INDEX "Feedback_toUserId_hiddenByAdmin_idx" ON "Feedback"("toUserId", "hiddenByAdmin"); + +-- CreateIndex +CREATE UNIQUE INDEX "Feedback_projectId_fromUserId_toUserId_key" ON "Feedback"("projectId", "fromUserId", "toUserId"); + +-- AddForeignKey +ALTER TABLE "FeedbackAttribute" ADD CONSTRAINT "FeedbackAttribute_feedbackId_fkey" FOREIGN KEY ("feedbackId") REFERENCES "Feedback"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/prisma/migrations/20260823202155_ajusta_eixos_de_corroboracao/migration.sql b/prisma/migrations/20260823202155_ajusta_eixos_de_corroboracao/migration.sql new file mode 100644 index 00000000..37f39d2c --- /dev/null +++ b/prisma/migrations/20260823202155_ajusta_eixos_de_corroboracao/migration.sql @@ -0,0 +1,9 @@ +-- AlterEnum +BEGIN; +CREATE TYPE "FeedbackAttrEnum_new" AS ENUM ('COMUNICA_IMPEDIMENTOS', 'ENTREGA_O_COMBINADO', 'ENTREGA_NO_PRAZO', 'APOIA_A_EQUIPE', 'PERGUNTA_ANTES_DE_DECIDIR', 'RECEBE_REVISAO', 'ADAPTA_A_MUDANCA'); +ALTER TABLE "FeedbackAttribute" ALTER COLUMN "attribute" TYPE "FeedbackAttrEnum_new" USING ("attribute"::text::"FeedbackAttrEnum_new"); +ALTER TYPE "FeedbackAttrEnum" RENAME TO "FeedbackAttrEnum_old"; +ALTER TYPE "FeedbackAttrEnum_new" RENAME TO "FeedbackAttrEnum"; +DROP TYPE "public"."FeedbackAttrEnum_old"; +COMMIT; + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 98899162..b8823c50 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -152,16 +152,67 @@ model Feedback { projectId String fromUserId String toUserId String - rating Int - comment String? - anonymous Boolean - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt stackTakenId String? - fromUser User @relation("FeedbackGiver", fields: [fromUserId], references: [id]) - project Project @relation(fields: [projectId], references: [id]) - stackTaken StackTaken? @relation(fields: [stackTakenId], references: [id]) - toUser User @relation("FeedbackReceiver", fields: [toUserId], references: [id]) + + // Camada B — texto de desenvolvimento. Vive na área privada do avaliado. + // Só é publicado se o avaliador permitir na origem (publicationAllowed) E o + // avaliado decidir publicar (publishedByReceiver). As duas condições valem. + comment String? + publicationAllowed Boolean @default(false) + publishedByReceiver Boolean @default(false) + + // Moderação — ver seção 16 do documento de produto. + hiddenByAdmin Boolean @default(false) + hiddenReason String? + + // Camada A — corroborações por eixo comportamental. + attributes FeedbackAttribute[] + + // Legado. `rating` deixa de ser exibido e agregado publicamente; permanece + // como sinal operacional interno consultável por ADMIN. `anonymous` perde + // função de produto — o anonimato público passou a ser regra fixa. + // Código novo não deve ler nenhum dos dois. Ver seção 14. + rating Int? + anonymous Boolean @default(true) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + fromUser User @relation("FeedbackGiver", fields: [fromUserId], references: [id]) + project Project @relation(fields: [projectId], references: [id]) + stackTaken StackTaken? @relation(fields: [stackTakenId], references: [id]) + toUser User @relation("FeedbackReceiver", fields: [toUserId], references: [id]) + + // Uma avaliação por par de pessoas em cada projeto. + @@unique([projectId, fromUserId, toUserId]) + // Suporta a agregação do portfólio público. + @@index([toUserId, hiddenByAdmin]) +} + +// Tabela relacional em vez de array de enum: só ela impede, no banco, que o +// mesmo eixo seja contado duas vezes na mesma avaliação. Como o valor público +// da corroboração é a contagem, dado duplicado corrompe o que a funcionalidade +// entrega. Decidido na spike de 19/08/2026. +model FeedbackAttribute { + id String @id @default(cuid()) + feedbackId String + attribute FeedbackAttrEnum + feedback Feedback @relation(fields: [feedbackId], references: [id], onDelete: Cascade) + + @@unique([feedbackId, attribute]) + @@index([attribute]) +} + +// Lista fechada por decisão de produto: eixo livre viraria elogio genérico e +// destruiria a comparabilidade entre perfis. Ver seção 5.1. +enum FeedbackAttrEnum { + COMUNICA_IMPEDIMENTOS + ENTREGA_O_COMBINADO + ENTREGA_NO_PRAZO + APOIA_A_EQUIPE + PERGUNTA_ANTES_DE_DECIDIR + RECEBE_REVISAO + ADAPTA_A_MUDANCA } model Invite { diff --git a/src/components/Portfolio/sections/PortfolioFeedbackSection.tsx b/src/components/Portfolio/sections/PortfolioFeedbackSection.tsx index 963fcdf2..9c3b4936 100644 --- a/src/components/Portfolio/sections/PortfolioFeedbackSection.tsx +++ b/src/components/Portfolio/sections/PortfolioFeedbackSection.tsx @@ -1,4 +1,4 @@ -import { Star } from 'lucide-react'; +import { MessageSquareQuote } from 'lucide-react'; import type { PortfolioPublicResponse } from '@/types/portfolio.types'; import { Section } from '@/components/ui/section'; @@ -6,42 +6,43 @@ interface PortfolioFeedbackSectionProps { data: PortfolioPublicResponse; } +/** + * Camada B do sistema de feedback: texto de desenvolvimento publicado pelo + * próprio avaliado. + * + * As estrelas foram removidas por decisão de produto (seção 5.4 — antipadrões + * visuais proibidos): nota e escala transformam a página em ranking, e o + * documento é explícito em que a camada pública não comunica quantidade nem + * teto. Aqui o texto vale por si. + * + * A autoria não é exibida: o anonimato público deixou de ser escolha do + * avaliador e passou a ser regra fixa (seção 7). + */ export function PortfolioFeedbackSection({ data, }: PortfolioFeedbackSectionProps) { - if (!data.feedback || data.feedback.length === 0) return null; + const comentarios = (data.feedback ?? []).filter((fb) => fb.comment); + + if (comentarios.length === 0) return null; return ( -
} title="Feedback recebido"> +
} + title="Feedback recebido" + >
- {data.feedback.map((fb) => ( -
( +
-
-
- {Array.from({ length: 5 }).map((_, i) => ( - - ))} -
- - {fb.givenBy ?? 'Avaliação anônima'} · {fb.projectName} - -
- {fb.comment && ( -

- "{fb.comment}" -

- )} -
+
+ "{fb.comment}" +
+
+ {fb.projectName} +
+ ))}
diff --git a/src/lib/portfolio.service.ts b/src/lib/portfolio.service.ts index f2a50525..494a5559 100644 --- a/src/lib/portfolio.service.ts +++ b/src/lib/portfolio.service.ts @@ -135,9 +135,11 @@ export async function getPublicPortfolio( if (user.showProjects) response.projects = Array.from(projectMap.values()); if (user.showCertificates) response.certificates = user.certificates; if (user.showFeedback) { + // `rating` deixou de ser exposto publicamente — seção 14 do documento de + // produto. A camada pública passa a ser as corroborações por eixo; o texto + // é complemento, nunca evidência quantificada. response.feedback = user.feedbacksReceived.map((fb) => ({ id: fb.id, - rating: fb.rating, comment: fb.comment, projectName: fb.project.name, givenBy: fb.anonymous ? null : fb.fromUser.name, @@ -303,9 +305,6 @@ export async function listPortfolioSummary( skill: { select: { id: true, name: true, iconUrl: true } }, }, }, - feedbacksReceived: { - select: { rating: true }, - }, }, }); @@ -315,12 +314,9 @@ export async function listPortfolioSummary( return { items: page.map((u) => { - const avgFeedback = - u.feedbacksReceived.length > 0 - ? u.feedbacksReceived.reduce((sum, fb) => sum + fb.rating, 0) / - u.feedbacksReceived.length - : null; - + // A média pública de ratings foi revogada na v2 do documento de produto: + // duas médias lado a lado são um ranking, independentemente do rótulo. + // Ver seções 3 (princípio 2) e 14. return { username: u.userName!, name: u.name, @@ -329,7 +325,6 @@ export async function listPortfolioSummary( role: u.role, github: u.showGithub ? u.github : null, linkedin: u.showLinkedin ? u.linkedin : null, - feedback: u.showFeedback ? avgFeedback : null, skills: u.skills.map((us) => us.skill), }; }), diff --git a/src/types/portfolio.types.ts b/src/types/portfolio.types.ts index 61a633aa..99b06b73 100644 --- a/src/types/portfolio.types.ts +++ b/src/types/portfolio.types.ts @@ -59,10 +59,11 @@ export interface PortfolioCertificate { export interface PortfolioFeedback { id: string; - rating: number; comment: string | null; projectName: string; - givenBy: string | null; // null quando anonymous = true + /** Sempre null na camada pública: o anonimato deixou de ser escolha do + * avaliador e passou a ser regra fixa. Ver seção 7 do documento de produto. */ + givenBy: string | null; } export interface PortfolioSummaryItem { @@ -74,7 +75,6 @@ export interface PortfolioSummaryItem { // Campos do summary (opcionais — só presentes no /summary) github?: string | null; linkedin?: string | null; - feedback?: number | null; skills: Array<{ id: string; name: string; iconUrl: string | null }>; }