From 0991c04cd723f6162d10c228cf96189f4fc355f3 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Fri, 31 Jul 2026 05:09:36 +0000
Subject: [PATCH 1/6] fix(seed-posts): import dieWithLabel before calling it
(Pass 4 P1)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The P3-H CLI consolidation left seed-posts calling dieWithLabel with no
import — a ReferenceError on the empty-fetch abort path, invisible to
lint/typecheck/knip. Recurrence guard in the same delivery:
scriptCliConventions.unit.spec.ts fails the build when a script calls
dieWithLabel/dieAgent without importing (or defining) it, and bans
re-spelled 'const DAY_MS =' outside src/lib/text.ts (P3-K's fold leaked
one private copy in engagementLevel.ts).
---
scripts/seed-posts.mjs | 2 +-
tests/unit/scriptCliConventions.unit.spec.ts | 63 ++++++++++++++++++++
2 files changed, 64 insertions(+), 1 deletion(-)
create mode 100644 tests/unit/scriptCliConventions.unit.spec.ts
diff --git a/scripts/seed-posts.mjs b/scripts/seed-posts.mjs
index 15c7040d..beb7d38e 100644
--- a/scripts/seed-posts.mjs
+++ b/scripts/seed-posts.mjs
@@ -27,7 +27,7 @@ import { convertHTMLToLexical, editorConfigFactory } from '@payloadcms/richtext-
import { del as blobDel } from '@vercel/blob'
import { JSDOM } from 'jsdom'
import { getPayload } from 'payload'
-import { loadCliEnv } from './lib/cli.mjs'
+import { dieWithLabel, loadCliEnv } from './lib/cli.mjs'
import { assertLocalDatabase } from './assert-local-database.mjs'
diff --git a/tests/unit/scriptCliConventions.unit.spec.ts b/tests/unit/scriptCliConventions.unit.spec.ts
new file mode 100644
index 00000000..58b5e8b8
--- /dev/null
+++ b/tests/unit/scriptCliConventions.unit.spec.ts
@@ -0,0 +1,63 @@
+import { readdirSync, readFileSync } from 'node:fs'
+import { relative, resolve } from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+// Pass 4 (2026-07-31): `scripts/seed-posts.mjs` called `dieWithLabel(…)`
+// without importing it — a ReferenceError on the empty-fetch abort path that
+// lint, typecheck and knip all let through (P3-H half-adopted in the same pass
+// that added the guard). The CLI skeleton is only a skeleton when the symbols
+// it exports are actually imported.
+//
+// `DAY_MS` is single-sourced in `src/lib/text.ts` (P3-K); a private re-spell
+// survived in `engagementLevel.ts` until this pass — ban the declaration form.
+
+const repoRoot = process.cwd()
+const repoPath = (absolute: string) => relative(repoRoot, absolute)
+
+const walkFiles = (root: string, extensions: readonly string[]): string[] =>
+ readdirSync(root, { recursive: true, withFileTypes: true })
+ .filter((entry) => entry.isFile() && extensions.some((ext) => entry.name.endsWith(ext)))
+ .map((entry) => resolve(entry.parentPath, entry.name))
+
+describe('script CLI skeleton (Pass 4)', () => {
+ const scriptFiles = walkFiles(resolve(repoRoot, 'scripts'), ['.mjs'])
+
+ it('imports dieWithLabel / dieAgent wherever they are called', () => {
+ const offenders: string[] = []
+
+ for (const file of scriptFiles) {
+ const source = readFileSync(file, 'utf8')
+ for (const symbol of ['dieWithLabel', 'dieAgent'] as const) {
+ const callsIt = new RegExp(`\\b${symbol}\\s*\\(`).test(source)
+ if (!callsIt) continue
+ const definesIt = new RegExp(`export const ${symbol}\\b`).test(source)
+ const importsIt = new RegExp(`^import\\s*\\{[^}]*\\b${symbol}\\b[^}]*\\}`, 'm').test(source)
+ if (!definesIt && !importsIt) offenders.push(`${repoPath(file)} (${symbol})`)
+ }
+ }
+
+ expect(
+ offenders,
+ 'import the CLI die helper from scripts/lib instead of calling it bare',
+ ).toEqual([])
+ })
+
+ it('keeps DAY_MS single-sourced in src/lib/text.ts', () => {
+ const offenders: string[] = []
+ const declaration = /const DAY_MS\s*=/ // this spec quotes the shape itself
+
+ for (const root of ['src', 'tests', 'scripts'] as const) {
+ for (const file of walkFiles(resolve(repoRoot, root), ['.ts', '.tsx', '.mjs'])) {
+ const path = repoPath(file)
+ if (path === 'src/lib/text.ts') continue
+ if (path === repoPath(import.meta.filename)) continue
+ const lines = readFileSync(file, 'utf8').split('\n')
+ for (const [index, line] of lines.entries()) {
+ if (declaration.test(line)) offenders.push(`${path}:${index + 1}`)
+ }
+ }
+ }
+
+ expect(offenders, 'import DAY_MS from @/lib/text instead of re-spelling it').toEqual([])
+ })
+})
From 9ae062c77c903d794eca7faf471f8f2f640d5f4d Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Fri, 31 Jul 2026 05:09:36 +0000
Subject: [PATCH 2/6] =?UTF-8?q?refactor(lib):=20close=20the=20lib=E2=86=92?=
=?UTF-8?q?utilities=20type-import=20hole=20+=20Local=20API=20omission=20g?=
=?UTF-8?q?uard?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- suggestionCatalog type-imports MunicipalityTerritorialClass from
lib/territorialClassAnchors (the single source) instead of the
utilities re-export — direction inversion introduced in Pass 3.
- ESLint src/lib zone: @/utilities|components|app restriction now rejects
type imports too (allowTypeImports: false); shared types live in lib
contract modules by policy.
- engagementLevel imports DAY_MS from lib/text (fold missed in P3-K).
- searchHomeAdvisors: delete stale 'Intentional admin bypass' comment —
the call is overrideAccess: false under the staff gate.
- New guard localApiOverrideAccessConventions.unit.spec.ts: payload calls
with user must declare overrideAccess (baseline measured: 59 calls, 0
missing), and runtime dynamic import/require of payload/next server
modules is banned in utilities (dodges the server-only sweep).
---
eslint.config.mjs | 9 +-
src/lib/engagementLevel.ts | 4 +-
src/lib/suggestionCatalog.ts | 2 +-
.../homeSearch/searchHomeAdvisors.ts | 3 +-
...lApiOverrideAccessConventions.unit.spec.ts | 86 +++++++++++++++++++
5 files changed, 97 insertions(+), 7 deletions(-)
create mode 100644 tests/unit/localApiOverrideAccessConventions.unit.spec.ts
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 9953f30e..953ca8d4 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -119,7 +119,10 @@ const eslintConfig = [
{
// src/lib is the pure, client-safe layer (docs/ARCHITECTURE.md): it must
// never depend on Payload/Next server code, on utilities/, or on higher
- // layers. Sharing types across the boundary is always fine.
+ // layers. Types shared with utilities live in lib contract modules and
+ // utilities re-export FROM lib — a lib module type-importing utilities
+ // inverts the direction anyway (Pass 4: suggestionCatalog did exactly
+ // that for a type whose source already lived in lib).
files: ['src/lib/**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/no-restricted-imports': [
@@ -129,8 +132,8 @@ const eslintConfig = [
{
group: ['@/utilities/**', '@/components/**', '@/app/**'],
message:
- 'src/lib is the pure layer — it must not depend on utilities/components/app. Move the pure logic into lib/ or this module into utilities/.',
- allowTypeImports: true,
+ 'src/lib is the pure layer — it must not depend on utilities/components/app. Move the pure logic into lib/ or this module into utilities/. Shared types live in lib contract modules.',
+ allowTypeImports: false,
},
{
group: ['payload', 'payload/**', '@payload-config', 'next', 'next/**'],
diff --git a/src/lib/engagementLevel.ts b/src/lib/engagementLevel.ts
index 84fffbc1..cd93387c 100644
--- a/src/lib/engagementLevel.ts
+++ b/src/lib/engagementLevel.ts
@@ -9,6 +9,8 @@
* one object so recalibration is a one-line diff.
*/
+import { DAY_MS } from '@/lib/text'
+
export const engagementLevels = ['n0', 'n1', 'n2', 'n3', 'n4'] as const
/** `allocationDecision.patternId` under which every level movement is filed. */
@@ -94,8 +96,6 @@ export type EngagementLevelMovementInput = {
triangulatedShock: boolean
}
-const DAY_MS = 24 * 60 * 60 * 1000
-
const daysBetween = (from: Date, to: Date): number => (to.getTime() - from.getTime()) / DAY_MS
const isSameCalendarMonth = (left: Date, right: Date): boolean =>
diff --git a/src/lib/suggestionCatalog.ts b/src/lib/suggestionCatalog.ts
index c55dcdc6..e13f87bc 100644
--- a/src/lib/suggestionCatalog.ts
+++ b/src/lib/suggestionCatalog.ts
@@ -10,9 +10,9 @@ import {
} from '@/lib/electionYears'
import type { EngagementLevel } from '@/lib/engagementLevel'
import { engagementLevelRank } from '@/lib/engagementLevel'
+import type { MunicipalityTerritorialClass } from '@/lib/territorialClassAnchors'
import { DAY_MS } from '@/lib/text'
import type { AllocationDecision } from '@/payload-types'
-import type { MunicipalityTerritorialClass } from '@/utilities/municipality/municipalityTerritorialClass'
/**
* E11 "motor de sugestões" — the versioned, curated catalog of data→decision
diff --git a/src/utilities/homeSearch/searchHomeAdvisors.ts b/src/utilities/homeSearch/searchHomeAdvisors.ts
index 6876bb99..c60f9fd5 100644
--- a/src/utilities/homeSearch/searchHomeAdvisors.ts
+++ b/src/utilities/homeSearch/searchHomeAdvisors.ts
@@ -29,7 +29,8 @@ export const searchHomeAdvisors = async (
const normalizedQuery = normalizeHomeSearchName(query)
- // Intentional admin bypass — unrestricted staff gate already ran in the route.
+ // Under access (`overrideAccess: false`): the unrestricted-staff gate ran
+ // above, so the advisor directory is only queried for coordinator/candidate.
const result = await payload.find({
collection: 'campaignUser',
where: { role: { equals: 'advisor' } },
diff --git a/tests/unit/localApiOverrideAccessConventions.unit.spec.ts b/tests/unit/localApiOverrideAccessConventions.unit.spec.ts
new file mode 100644
index 00000000..7adb5c08
--- /dev/null
+++ b/tests/unit/localApiOverrideAccessConventions.unit.spec.ts
@@ -0,0 +1,86 @@
+import { readdirSync, readFileSync } from 'node:fs'
+import { relative, resolve } from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+// Pass 4 (2026-07-31): the engineering standard is "Local API with `user`
+// always with `overrideAccess: false`; bypass admin só com comentário
+// justificando" — but until now nothing failed the build when a call passed
+// `user` and simply omitted `overrideAccess` (Payload then bypasses access
+// silently). Measured baseline at adoption: 59 calls with `user`, 0 missing
+// (the undocumented-bypass ratchet in codebaseConventions covers the `true`
+// side; this guard covers the omission side).
+//
+// Companion rule: the `server-only` sweep in codebaseConventions only sees
+// static imports, so a runtime `await import('payload')` in utilities would
+// dodge the mark. Banned outright — use a static import and the mark.
+
+const repoRoot = process.cwd()
+const repoPath = (absolute: string) => relative(repoRoot, absolute)
+
+const srcFiles = readdirSync(resolve(repoRoot, 'src'), { recursive: true, withFileTypes: true })
+ .filter((entry) => entry.isFile() && /\.(ts|tsx)$/.test(entry.name))
+ .map((entry) => resolve(entry.parentPath, entry.name))
+
+const CALL_PATTERN =
+ /payload\.(find|create|update|delete|count|findByID|findGlobal|updateGlobal)\s*\(\s*\{/g
+
+/** Brace-matches the first `{…}` object literal starting at `openIndex`. */
+const callObjectBody = (source: string, openIndex: number): string => {
+ let depth = 0
+ for (let index = openIndex; index < source.length; index++) {
+ if (source[index] === '{') depth++
+ else if (source[index] === '}') {
+ depth--
+ if (depth === 0) return source.slice(openIndex, index + 1)
+ }
+ }
+ return source.slice(openIndex)
+}
+
+describe('Local API overrideAccess discipline (Pass 4)', () => {
+ it('declares overrideAccess on every payload call that passes user', () => {
+ const offenders: string[] = []
+
+ for (const file of srcFiles) {
+ const source = readFileSync(file, 'utf8')
+ CALL_PATTERN.lastIndex = 0
+ let match: RegExpExecArray | null
+ while ((match = CALL_PATTERN.exec(source)) !== null) {
+ const openIndex = source.indexOf('{', match.index)
+ const body = callObjectBody(source, openIndex)
+ if (!/\buser\s*:/.test(body)) continue
+ if (/overrideAccess\s*:/.test(body)) continue
+ const line = source.slice(0, match.index).split('\n').length
+ offenders.push(`${repoPath(file)}:${line} (payload.${match[1]})`)
+ }
+ }
+
+ expect(
+ offenders,
+ 'Local API with `user` must declare overrideAccess (false under access; true only with the justifying comment)',
+ ).toEqual([])
+ })
+
+ it('bans runtime dynamic import/require of server-bound modules in utilities', () => {
+ const offenders: string[] = []
+ const dynamicServerImport =
+ /\bawait\s+import\(\s*['"](?:payload|@payload-config|next\/(?:cache|server|headers))['"]\s*\)/
+ const requireServerModule = /\brequire\(\s*['"](?:payload|@payload-config)['"]\s*\)/
+
+ for (const file of srcFiles) {
+ const path = repoPath(file)
+ if (!path.startsWith('src/utilities/')) continue
+ const lines = readFileSync(file, 'utf8').split('\n')
+ for (const [index, line] of lines.entries()) {
+ if (dynamicServerImport.test(line) || requireServerModule.test(line)) {
+ offenders.push(`${path}:${index + 1}`)
+ }
+ }
+ }
+
+ expect(
+ offenders,
+ "use a static import plus `import 'server-only'` — dynamic imports dodge the boundary sweep",
+ ).toEqual([])
+ })
+})
From 6f42bc3bf332653168416b608c84f1be6c195860 Mon Sep 17 00:00:00 2001
From: Cursor Agent
Date: Fri, 31 Jul 2026 05:50:41 +0000
Subject: [PATCH 3/6] fix(miss-52): one shared Drawer for the chip-relation
lists too
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The B42 fix reached the municípios mobile cards, but the three
chip-relation surfaces still mounted a Drawer root per OPENED cell on
coarse pointers (lideranças 50 cells, dobradinhas 50, assessores 25):
the exact hundreds-of-hidden-instances class that tripped React #130 in
prod. Each now wraps its table in CampaignListSheetProvider — cell
bodies portal into the single host Drawer, fine-pointer popover path
untouched (behavior delta: after opening N cells sequentially on touch,
N drawer roots -> 1).
---
.../campanha/(app)/dobradinhas/page.tsx | 21 +-
.../campanha/(app)/liderancas/page.tsx | 21 +-
.../campaign/advisor/AdvisorsTable.tsx | 493 +++++++++---------
3 files changed, 276 insertions(+), 259 deletions(-)
diff --git a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx
index 55ade6e5..c8e9a3f9 100644
--- a/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx
+++ b/src/app/(campaign)/campanha/(app)/dobradinhas/page.tsx
@@ -10,6 +10,7 @@ import {
CampaignListResults,
CampaignTransitionAnchor,
} from '@/components/campaign/shared/CampaignListPending'
+import { CampaignListSheetProvider } from '@/components/campaign/shared/CampaignListSheetHost'
import {
CampaignTable,
CampaignTableHead,
@@ -249,14 +250,18 @@ export default async function StateDeputiesPage({ searchParams }: StateDeputiesP
{sortSummary}
- row.id}
- empty={}
- />
+ {/* One shared Drawer for every chip-cell sheet on coarse pointers
+ (miss #52 — never a Drawer root per opened cell). */}
+
+ row.id}
+ empty={}
+ />
+
{rows.length ? (
{sortSummary}
- row.id}
- empty={}
- />
+ {/* One shared Drawer for every chip-cell sheet on coarse pointers
+ (miss #52 — never a Drawer root per opened cell). */}
+
+ row.id}
+ empty={}
+ />
+
{rows.length ? (
-
- {editing ? (
-
- ) : (
-
- )}
-
-
-
- {showEmpty ? (
-
- {!hasQuery ? (
-