package main import ( "os" "path/filepath" "regexp" "strings" ) type frontendViolation struct { file string line int rule string snippet string } // Future enhancement (FUTURE-WO-012-ENFORCE): Add a lint rule that requires // every string literal in a JSX text node to be wrapped in t(). This was // intentionally omitted from WO-012 to avoid breaking all in-flight WOs that // have untranslated strings. The scaffolding (i18next init, en.json catalog, // t() + useTranslation exports) was shipped in WO-012; strict enforcement is // deferred to a follow-up work order. // Files/dirs where fetch() and manual clients are allowed. // Each entry has a documented reason — add new entries only with justification. var allowedFrontendFiles = map[string]bool{ "lib/transport.ts": true, // Global transport setup "lib/api/transport.ts": true, // Orchestrator global transport setup "lib/token-refresh.ts": true, // Auth bootstrap client (avoids interceptor loop) "lib/sync/sync-engine.ts": true, // Jotai sync — lives outside React, can't use hooks "components/media-library/media-upload-area.tsx": true, // Multipart file upload via FormData "components/html-editor/preview-iframe.tsx": true, // Block HTML preview (returns HTML, not proto) "lib/editor-bridge/iframe-script.ts": true, // Editor iframe preview "player/human-proof/index.ts": true, // Standalone player bundle — no React, fetches public REST endpoint // createClient exceptions — each has a technical reason why useQuery/useMutation can't be used: // use-entity-search fans one debounced query out to 10 RPCs across 8 services // via useQueries (single combined loading state + result blending for the // command palette); generated per-RPC hooks can't express this without 10 // independent hook instances and manual aggregation. "hooks/use-entity-search.ts": true, "routes/admin/menus.tsx": true, // Imperative async callback in useCallback "components/data-platform/tables/creation-wizard.tsx": true, // useMutation has serialization bug with oneof fields // SiteAgentService.SendMessage is a Connect server-streaming RPC consumed // with `for await` — generated Connect Query hooks are unary-only and // cannot express a streaming turn. "components/site-agent/use-site-agent-stream.ts": true, // InfrastructureService.RenewCertificate is a Connect server-streaming RPC; // Connect Query omits server-streaming methods, so the networking terminal // must consume the generated client's AsyncIterable directly. "routes/dashboard/admin/infrastructure/$nodeId/networking.tsx": true, // Registry browse/detail call the ORCHESTRATOR's public Connect endpoints // (PluginRegistryService/PluginReviewService) on a different origin. The // CMS transport and generated hooks only cover the CMS's own proto surface, // so these are plain JSON POSTs to the registry URL from GetAuthConfig // (same reason routes/admin/plugins.tsx carried before the extraction). "components/plugins/browse-registry-tab.tsx": true, "components/plugins/registry-detail.tsx": true, } // Plugin web files that are allowed to call plugin-owned REST handlers because // there is no ConnectRPC surface for them yet. Keep this list narrow. var allowedPluginRESTFiles = map[string]bool{ // Suffix-matched (see pluginRESTFileAllowed), so these cover calcomblock both // as the standalone plugin repo (plugins/calcomblock/web/...) and the legacy // bundled path (backend/internal/plugins/calcomblock/web/...). "plugins/calcomblock/web/settings.tsx": true, // Plugin settings/test endpoints are mounted via HTTPHandler; no generated hooks exist yet "plugins/calcomblock/web/editor.tsx": true, // Block editor reads plugin /settings + /event-types via HTTPHandler routes "plugins/judgefestblock/web/settings.tsx": true, // Same HTTPHandler REST shape as calcomblock (settings/test endpoints) "plugins/judgefestblock/web/editor.tsx": true, // Block editor reads plugin /events-picker via HTTPHandler routes } type nonProtoFetchAllowance struct { file string target string reason string } // allowedNonProtoFetches is intentionally scoped by both source file and exact // fetch target. A broad path prefix would let unrelated API calls bypass the // generated-hook rule. Add an entry only after confirming that no generated // ConnectRPC hook can carry the endpoint's transport contract. var allowedNonProtoFetches = []nonProtoFetchAllowance{ {file: "lib/table-file-transport.ts", target: "/api/table-file-uploads/${sessionId}", reason: "bounded resumable binary chunks with durable offsets; controls use generated RPC hooks"}, {file: "lib/table-file-transport.ts", target: "/api/table-files/${attachmentId}/content?version=${versionId}", reason: "authenticated exact-version binary download; access is rechecked at open"}, {file: "lib/table-file-transport.ts", target: "/api/table-files/${attachmentId}/preview?version=${versionId}", reason: "bounded safe binary/text preview; no raw document embedding"}, { file: "components/admin/backups/restore-new-instance-dialog.tsx", target: "/api/push/upload?account_id=${encodeURIComponent(accountId)}", reason: "multipart backup ZIP upload; ConnectRPC messages cannot carry FormData", }, { file: "components/helpdesk/helpers.ts", target: "/api/helpdesk/upload", reason: "multipart helpdesk attachment upload; ConnectRPC messages cannot carry FormData", }, { file: "components/ai-agents/chat-page.tsx", target: "/api/ai/chat/stream", reason: "SSE response stream; generated Connect Query hooks are unary", }, { file: "components/ai-agents/chat-widget.tsx", target: "/api/ai/chat/stream", reason: "SSE response stream; generated Connect Query hooks are unary", }, { file: "components/bug-report/bug-report-dialog.tsx", target: "/api/support/bug-reports", reason: "multipart bug report forwarded by the authenticated CMS support proxy", }, { file: "components/support/help-widget.tsx", target: "/api/support/tickets/${ticketId}/messages/${messageId}/attachments", reason: "multipart attachment forwarded by the authenticated CMS support proxy", }, { file: "lib/support/availability.ts", target: "/api/support/tickets", reason: "CMS support availability probe targets the authenticated orchestrator proxy, not a CMS RPC", }, { file: "routes/admin/authorize-device.tsx", target: "/api/mcp/device/status", reason: "browser leg of the MCP device authorization protocol, outside the admin RPC surface", }, { file: "routes/admin/authorize-device.tsx", target: "/api/mcp/device/authorize", reason: "browser leg of the MCP device authorization protocol, outside the admin RPC surface", }, { file: "routes/admin/authorize-device.tsx", target: "/api/mcp/device/deny", reason: "browser leg of the MCP device authorization protocol, outside the admin RPC surface", }, { file: "routes/admin/plugins.tsx", target: "/api/plugins/upload?token=${encodeURIComponent(token)}", reason: "multipart BNP upload authorized by a one-time token minted through PluginsService", }, } var ( // Anti-pattern: importing axios reAxios = regexp.MustCompile(`(?:import|require).*['"]axios['"]`) // Anti-pattern: useQueryClient (should use refetch() instead) reUseQueryClient = regexp.MustCompile(`useQueryClient\s*\(`) // Anti-pattern in plugins: QueryClientProvider is already mounted by core reQueryClientProvider = regexp.MustCompile(`\bQueryClientProvider\b`) // Anti-pattern: createClient from connectrpc (should use generated hooks) reCreateClient = regexp.MustCompile(`createClient\s*\(`) // Anti-pattern: fetch() to /api/ or proto paths (should use ConnectRPC). // The literal first argument is parsed separately for narrow REST allowances. reFetchAPI = regexp.MustCompile(`fetch\s*\(\s*['\x60"/]`) // Anti-pattern: createConnectTransport (only allowed in transport.ts) reCreateTransport = regexp.MustCompile(`createConnectTransport\s*\(`) // Anti-pattern: new XMLHttpRequest reXHR = regexp.MustCompile(`new\s+XMLHttpRequest`) // Anti-pattern: importing from @connectrpc/connect-web directly to create clients reConnectWebImport = regexp.MustCompile(`from\s+['"]@connectrpc/connect-web['"]`) // Anti-pattern: browser confirm() instead of the shared confirm dialog reWindowConfirm = regexp.MustCompile(`\bwindow\.confirm\s*\(`) reBareConfirm = regexp.MustCompile(`(?:^|[^\w.])confirm\s*\(`) // Anti-pattern: raw