From 5335f69b43dc56d740e67f19799b79e57f789b19 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 2 Aug 2026 23:02:53 +0000 Subject: [PATCH 1/6] =?UTF-8?q?B134=20=E2=80=94=20motivo=20opcional=20(ten?= =?UTF-8?q?d=C3=AAncia=20+=20n=C3=ADvel);=20remove=20reversalSignals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Make engagement level note optional in schema and UI - Remove reversalSignals field from popover, action snapshot and DB - Add data migration to strip reversalSignals from movimento snapshots - Wizard trend note step no longer requires text; labels show (opcional) Co-authored-by: Francisco Solla --- .../motivo-opcional-tendencia-e-nivel-impl.md | 64 +++++++++++++++++++ .../campanha/actions/municipality.ts | 5 +- .../MunicipalityListLevelControl.tsx | 29 ++------- .../municipality/WizardTrendNoteStep.tsx | 3 +- src/lib/schemas/municipality.ts | 8 +-- ...30000_strip_engagement_reversal_signals.ts | 23 +++++++ src/migrations/index.ts | 6 ++ ...ignMunicipalityEngagementLevel.int.spec.ts | 44 ++++++++++--- ...ementReversalSignalsMigration.unit.spec.ts | 23 +++++++ 9 files changed, 162 insertions(+), 43 deletions(-) create mode 100644 docs/plans/motivo-opcional-tendencia-e-nivel-impl.md create mode 100644 src/migrations/20260802_230000_strip_engagement_reversal_signals.ts create mode 100644 tests/unit/stripEngagementReversalSignalsMigration.unit.spec.ts diff --git a/docs/plans/motivo-opcional-tendencia-e-nivel-impl.md b/docs/plans/motivo-opcional-tendencia-e-nivel-impl.md new file mode 100644 index 00000000..48e5a9db --- /dev/null +++ b/docs/plans/motivo-opcional-tendencia-e-nivel-impl.md @@ -0,0 +1,64 @@ +# Impl: Motivo opcional — tendência (wizard) + nível; remover voltar atrás do DB (B134) + +Status: aprovado +Atualizado em: 2026-08-02 +Issue: #288 +Intenção: docs/plans/motivo-opcional-tendencia-e-nivel.md +Appetite restante: herdado (~1 dia eng) + +## Leitura da intenção + +- **Outcome:** staff grava tendência ou nível sem texto obrigatório; Motivo permanece opcional nos dois ritos; `reversalSignals` some da UI, write path e snapshots persistidos. +- **O que NÃO negociar:** histerese/override/choque triangulado intactos; `leader` lockdown; sem auto-save no nível; motivo vazio grava `null` (não preserva anterior); dados históricos de `reversalSignals` apagados do DB. +- **O que reavaliar:** não há coluna dedicada — `reversalSignals` vive só no JSON `allocation_decision.snapshot`; migration é data-only, sem alterar `AllocationDecision` config. + +## Abordagem recomendada + +```mermaid +flowchart LR + UI[Popover / wizard] --> schema[municipalityEngagementLevelSchema] + schema --> action[setMunicipalityEngagementLevelRecord] + action --> mun[municipality.levelNote nullable] + action --> ad[allocationDecision snapshot sem reversalSignals] + mig[migration SQL] --> ad +``` + +**Opções consideradas:** A remover só da UI/schema | B migration JSON strip + schema/UI/action +**Recomendação:** B — aceite exige apagar do banco, não só esconder. +**Rejeitadas:** A (órfãos no snapshot); tornar `rationale` opcional na collection (escopo desnecessário — string vazia basta quando motivo ausente). + +### Componentes / mudanças + +- **`municipalityEngagementLevelSchema`** (`src/lib/schemas/municipality.ts`): `note` → `trimmedNullableText`; remover `reversalSignals`. +- **`setMunicipalityEngagementLevelRecord`** (`actions/municipality.ts`): snapshot sem `reversalSignals`; `rationale: note ?? ''`. +- **`MunicipalityListLevelControl`**: um campo Motivo (opcional); `canSubmit` só exige movimento + override quando violações. +- **`WizardTrendNoteStep`**: remover `required`; label com “(opcional)”. +- **Migration:** `20260802_*_strip_engagement_reversal_signals` — `UPDATE allocation_decision SET snapshot = snapshot - 'reversalSignals' WHERE …`. +- **Access / Consent:** inalterado (`canManageMunicipalityEngagementLevel`). + +### Dados → forma + +- Tendência: schema já usa `trimmedNullableText` — só UI wizard tinha `required` HTML. +- Nível: `levelNote` nullable no município; `allocationDecision.rationale` string (vazia se sem motivo). + +## Fases verificáveis + +1. **Schema+server** — schema, action, migration, int test com motivo vazio + snapshot sem reversalSignals. +2. **UI** — labels “(opcional)”, remover segundo textarea. +3. **Gates** — `pnpm gate:fast`; push via `pnpm push`. + +## Rabbit holes / Não escopo (engenharia) + +- Alterar copy do glossário `campaignIntelligenceConcepts` (prosa conceitual, não o campo). +- `rationale` required na collection — string vazia, não relaxar schema Payload. + +## Riscos e mitigação + +- **Rationale vazio rejeitado pelo Payload:** int test com `note` omitido; se falhar, usar `''` explicitamente (textarea required aceita vazio). +- **Migration idempotente:** operador JSONB `-` só onde chave existe; log de row count. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto +- [x] Invariantes AGENTS/engineering-standards +- [x] Testes de domínio previstos (int engagement level + migration unit) diff --git a/src/app/(campaign)/campanha/actions/municipality.ts b/src/app/(campaign)/campanha/actions/municipality.ts index 229d1ef8..79e8c596 100644 --- a/src/app/(campaign)/campanha/actions/municipality.ts +++ b/src/app/(campaign)/campanha/actions/municipality.ts @@ -149,7 +149,7 @@ export const setMunicipalityEngagementLevelRecord = async ( actor: CampaignUser, input: MunicipalityEngagementLevelInput, ) => { - const { municipality, level, note, reversalSignals, triangulatedShock, override } = + const { municipality, level, note, triangulatedShock, override } = municipalityEngagementLevelSchema.parse(input) return withPayloadTransaction( @@ -203,13 +203,12 @@ export const setMunicipalityEngagementLevelRecord = async ( municipality, patternId: ENGAGEMENT_LEVEL_PATTERN_ID, outcome: 'movimento', - rationale: note, + rationale: note ?? '', // Only the values the decision was taken on — no document dumps // (the collection caps the serialized snapshot at 16 KB). snapshot: { from, to: level, - reversalSignals, triangulatedShock, violations: violations.map((violation) => violation.id), overridden: violations.length > 0, diff --git a/src/components/campaign/municipality/MunicipalityListLevelControl.tsx b/src/components/campaign/municipality/MunicipalityListLevelControl.tsx index c40cd2de..e1fa9abc 100644 --- a/src/components/campaign/municipality/MunicipalityListLevelControl.tsx +++ b/src/components/campaign/municipality/MunicipalityListLevelControl.tsx @@ -45,9 +45,9 @@ type MunicipalityListLevelControlProps = { /** * E14 — unlike the trend and estimate cells next to it, this one submits - * explicitly: a movement carries a motivo AND the signals that would reverse - * it, and it may need an override. Auto-saving half of that would file an - * incomplete decision under the coordinator's name. + * explicitly: a movement may need an override, and the motivo is optional. + * Auto-saving half of that would file an incomplete decision under the + * coordinator's name. */ export const MunicipalityListLevelControl = ({ municipalityID, @@ -73,7 +73,6 @@ export const MunicipalityListLevelControl = ({ // with it the select — is not mounted. const [draftLevel, setDraftLevel] = useState('') const [note, setNote] = useState('') - const [reversalSignals, setReversalSignals] = useState('') const [triangulatedShock, setTriangulatedShock] = useState(false) const [override, setOverride] = useState(false) const [isPending, setIsPending] = useState(false) @@ -142,7 +141,6 @@ export const MunicipalityListLevelControl = ({ const resetDraft = () => { setDraftLevel(currentLevel ?? '') setNote('') - setReversalSignals('') setTriangulatedShock(false) setOverride(false) setErrorMessage(null) @@ -174,7 +172,6 @@ export const MunicipalityListLevelControl = ({ municipalityId: municipalityID, level: draftLevel, note, - reversalSignals, triangulatedShock, override, }, @@ -205,11 +202,7 @@ export const MunicipalityListLevelControl = ({ } } - const canSubmit = - isMovement && - note.trim().length > 0 && - reversalSignals.trim().length > 0 && - (violations.length === 0 || override) + const canSubmit = isMovement && (violations.length === 0 || override) const fieldId = (suffix: string) => `municipality-list-level-${suffix}-${municipalityID}` const isSheet = variant === 'sheet' const submitButton = ( @@ -283,7 +276,7 @@ export const MunicipalityListLevelControl = ({ - Motivo + Motivo (opcional)