refactor(lib): close the lib→utilities type-import hole + Local API omission guard

- 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).
This commit is contained in:
Cursor Agent 2026-07-31 05:09:36 +00:00 committed by Francisco Ignácio Fontoura Solla
parent 0991c04cd7
commit 9ae062c77c
5 changed files with 97 additions and 7 deletions

View file

@ -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/**'],

View file

@ -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 =>

View file

@ -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

View file

@ -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' } },

View file

@ -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([])
})
})