nextStateDeputyIdsAfterMunicipalityMembership was the third byte-for-byte copy of the same single-id delta — nextAdvisorIdsAfterMembership and nextStateDeputyIdsAfterMembership differ from it only in which cap they read — so the algorithm now lives once in nextIdsAfterMembership with the cap as data, and the three named wrappers just bind their own. The three cloned spec files became one describe.each suite: coverage went up rather than down, since every relation now gets the "can go to zero" and no-mutation cases only B37's clone had, plus an explicit assertion that each wrapper refuses at ITS cap with ITS message. That pairing matters because mapCampaignFormActionError matches thrown messages against safeMessages by exact string, and two of those cap messages were bare literals in three src places each (thrown, plus two independent allowlists): rewording any one would have silently collapsed a real refusal into "tente novamente". Both are constants now, next to their caps, following the MUNICIPALITY_STATE_DEPUTIES_CAP_MESSAGE precedent already in those same files. The dobradinha staff message had the same exposure in four places — and actions/stateDeputy.ts already imported the constant while restating it as a literal one line below — so it is STATE_DEPUTY_STAFF_MESSAGE (renamed: it guards create/update too, not just the batch), and the conflict message it shares with the create route is named too. The batch action stopped resolving the dobradinha slug before its loop: the slug exists only to target the revalidate, which the caller already skips when nothing changed, so on a no-op batch that read was fetched and thrown away — the guard setLeadershipMunicipalitiesMembershipRecord already had. Its redundant second dedup is gone (the schema's transform already deduped; the sort stays, for deadlock-free lock ordering), and the bare 435 shared with the advisor batch is MAX_MUNICIPALITIES_PER_BATCH. Two memoization gaps closed, both restoring invariants the codebase had already established elsewhere. An advisor's scoped .filter() returned a fresh array per row, defeating the WeakMap that exists precisely to share derivations across rows, so scopedPortfolioIndex memoizes on (index, addableIds) and every row gets one array. And LeadershipStateDeputyRelationCell called matchesAtWordStart per candidate, paying an NFD pass plus three Unicode regexes on both sides per keystroke over the unpaginated leadership catalog — the cost wordStartFilter's own doc comment warns about; it now normalizes the query once against labels cached on options, like municipalityPortfolio. Rejected with reason: maxItems on the dobradinhas cell. A dobradinha has no product ceiling on municípios (that is why the bound is the catalog), so a "generous 30" would invent a rule that refuses a legitimate statewide dobradinha, and the honest 435 is unreachable when ids are unique. Also dropped the dead addPlaceholder knob no caller ever set. |
||
|---|---|---|
| .agents/skills | ||
| .cursor | ||
| .github/workflows | ||
| .husky | ||
| .impeccable | ||
| .vscode | ||
| .zed | ||
| docker/postgres/init | ||
| docs | ||
| public | ||
| scripts | ||
| src | ||
| tests | ||
| .env.example | ||
| .env.test | ||
| .gitignore | ||
| .npmrc | ||
| .prettierignore | ||
| .prettierrc.json | ||
| .yarnrc | ||
| AGENTS.md | ||
| components.json | ||
| DESIGN.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| eslint-legacy-ignores.mjs | ||
| eslint-local-rules.mjs | ||
| eslint.config.mjs | ||
| knip.json | ||
| next.config.mjs | ||
| package.json | ||
| playwright.config.ts | ||
| pnpm-lock.yaml | ||
| postcss.config.mjs | ||
| PRODUCT.md | ||
| README.md | ||
| skills-lock.json | ||
| tailwind.config.ts | ||
| test.env | ||
| tsconfig.json | ||
| vercel.json | ||
| vitest.config.mts | ||
| vitest.setup.ts | ||
| vitest.unit.config.mts | ||
Teqo
Teqo starts as the official digital platform for deputado Jorge Solla and evolves into a white-label civic engagement platform for politicians in Brazil.
The goal is to help political teams build direct, durable relationships with their base without depending on Big Tech social platforms as the primary channel.
Mission
- Strengthen direct communication between representatives and citizens.
- Reduce platform dependency risk by owning audience and data channels.
- Provide reusable building blocks so each political team can launch quickly.
Product Direction
Phase 1: Jorge Solla Website
Initial delivery focuses on Jorge Solla's public website, editorial CMS, and the internal /campanha tool:
- Public-facing content and updates (news/
post+taglive) - Institutional pages and biography (still pending a
Pagescollection) - Internal campaign operations: electoral nuclei, leaderships, estimates, updates, WhatsApp invites
- Editorial operations through Payload CMS
Phase 2: White-Label Platform
Teqo then becomes a configurable base product for other politicians in Brazil:
- Multi-tenant and reusable architecture
- Brand and content customization per mandate/campaign
- Shared core modules for communication and engagement
- Operational autonomy with self-hosted owned channels
Local Development
Production safety: the production database is a live Neon Postgres holding real citizens' data. Local development and tests run against a local Postgres and must never point at production.
pnpm devrefuses to start against a non-local database, and the test suite refuses any database whose name doesn't end in_test.
First-time setup
- Install dependencies:
pnpm install - Create your env file:
cp .env.example .env.local - Start Docker, then start the local database:
pnpm db:start- This runs Postgres 17 (matching production) and auto-creates a
teqo_testdatabase for the test suite.
- This runs Postgres 17 (matching production) and auto-creates a
- Make sure
DATABASE_URLin.env.localpoints at local:DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo - Load a schema + content into the local database — pick one:
- Mirror production content (recommended):
PROD_DATABASE_URL="<unpooled Neon URL>" pnpm db:pullCopies production's schema and content into local. It only reads production and excludes supporter PII (contact/signature/subscriptiondata). - Empty schema from migrations:
pnpm migrate
- Mirror production content (recommended):
- Start the dev server:
pnpm dev - Open:
http://localhost:3000
Useful scripts: pnpm db:start / pnpm db:stop (local Postgres), pnpm db:pull (refresh local content from prod), pnpm db:seed:posts (import news posts/tags fresh from the live jorgesolla.com.br site into the local db; idempotent by slug, refuses a non-local database), pnpm db:seed:tse (import TSE 2014/2018/2022 Bahia election results into the local db; idempotent per year/office/turn scope, refuses a non-local database), pnpm build:election-aggregates (regenerate the committed TSE aggregate artifact in src/lib/electionAggregates/ from locally seeded data).
Content & cache revalidation
Public news pages (/[type], /[type]/[category], articles, and the home news list) are cached under a shared posts tag. Editing a post or tag in the deployed admin self-revalidates via afterChange hooks. Any write that bypasses the deployed runtime — a direct-DB seed (pnpm db:seed:posts), SQL, Onda 0 migrations, or a restore — does not bust production's cache, so afterwards call the secured endpoint:
# News (default tag: posts)
curl -X POST "https://<prod-domain>/api/revalidate" -H "x-revalidate-secret: $REVALIDATE_SECRET"
# Privacy policy after Onda 0 provision (tag: global_privacy-policy)
curl -X POST "https://<prod-domain>/api/revalidate?tag=global_privacy-policy" -H "x-revalidate-secret: $REVALIDATE_SECRET"
REVALIDATE_SECRET must be set in the Vercel production env (see .env.example). See the "Posts & Tags" section of AGENTS.md for the full model.
Database migrations
Schema is managed by committed Payload migrations (src/migrations/); push is disabled everywhere.
To change the schema:
- Edit the collection/global/field config.
- Generate a migration:
pnpm migrate:create <name> - Review and commit the generated files in
src/migrations/(both.tsand.json, plusindex.ts). - Apply it locally:
pnpm migrate(check status anytime withpnpm migrate:status).
Deploying to production: pnpm build runs payload migrate before building, so every Vercel deploy automatically applies pending migrations to the production database. Do not run migrations against production by hand.
Running tests
Tests use the isolated teqo_test database (config in .env.test). Prepare its schema once with:
DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test pnpm migrate
Then run:
pnpm test— unit + integrationpnpm test:e2e— Playwright (requires the test DB schema and a free port)pnpm test:all— unit + integration + E2E
Quality gates (also enforced by CI on the GitHub mirror): pnpm lint (zero warnings — --max-warnings=0), pnpm typecheck, and pnpm exec knip (dead files/dependencies fail; delete what your change orphaned). Standards: .cursor/rules/engineering-standards.mdc.
Tech Stack
- Payload CMS
- Next.js
- PostgreSQL (via
@payloadcms/db-postgres) - TypeScript
Campaign (/campanha)
Internal campaign tool for electoral nuclei, local leaderships, vote estimates, field updates, and WhatsApp invites. Authenticated separately from /admin via the campaignUser collection (geral / coordenador / lideranca). Operational status and decisions: .cursor/rules/projects/nucleos-eleitorais.mdc. Conventions and deploy checklist: AGENTS.md (“Campaign auth” / “Campaign nuclei MVP”).
Production blocker: do not load real leadership data or enable invites until counsel-approved Consent.key = 'lideranca-autopreenchimento' exists. Absolute invite URLs require NEXT_PUBLIC_SITE_URL as an exact HTTPS DNS origin in production.
Roadmap
Backlog and future plans live in docs/roadmap.md (blockers, public site, /campanha next cycles, admin RBAC, white-label).