From 2861f9d1346b76bf5d838bef4823674b9ad4311e Mon Sep 17 00:00:00 2001 From: Ali Al Dallal Date: Tue, 18 Aug 2026 15:48:03 -0400 Subject: [PATCH] ci: sonarjs gates -- cognitive complexity at 15, duplication rules at error (goal 0109 phase 2b) eslint-plugin-sonarjs@4.2.0, thresholds from a full measurement run over src/: no-duplicated-branches and no-identical-functions land clean repo-wide and gate at error; cognitive-complexity gates at 15 with the 11 legacy sites over threshold (max 32) grandfathered by named disable-with-reason comments -- new code gates immediately, the burn-down list IS the greppable disables ("goal 0109 burn-down"). no-duplicate-string stays OFF per Sonar's own default posture (i18n keys/testids are the documented noise class). Completes goal 0109's acceptance; the goal file carries the full record. Claude-Session: https://claude.ai/code/session_01FW5GkkAG8du7tNdYLk2zSd Co-Authored-By: Claude Fable 5 --- frontend/eslint.config.js | 11 ++ frontend/package-lock.json | 121 ++++++++++++++++++ frontend/package.json | 1 + frontend/src/app/WorkTabShell.tsx | 1 + frontend/src/app/pageMeta.tsx | 1 + frontend/src/atlas/atlasBuildBoardNodes.ts | 1 + frontend/src/atlas/atlasOverlapResolution.ts | 1 + frontend/src/atlas/useAtlasCreation.ts | 1 + frontend/src/atlas/useAtlasKeyboardNav.ts | 1 + frontend/src/composition/CanvasNodeView.tsx | 1 + frontend/src/composition/NodeConfigFields.tsx | 1 + frontend/src/composition/TriggerRowLabel.tsx | 1 + .../src/composition/WorkflowRunsPanel.tsx | 1 + frontend/src/configure/openapiSynth.ts | 1 + 14 files changed, 144 insertions(+) diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index e5babd68..a4f9edb8 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -3,6 +3,7 @@ import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' import i18next from 'eslint-plugin-i18next' +import sonarjs from 'eslint-plugin-sonarjs' import tseslint from 'typescript-eslint' export default tseslint.config( @@ -17,9 +18,19 @@ export default tseslint.config( plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh, + sonarjs, }, rules: { ...reactHooks.configs.recommended.rules, + // Sonar-class quality gates (goal 0109 phase 2b), the TS + // counterparts of .golangci.yml's dupl/gocognit: thresholds and + // enables set from a full measurement run over src/, never + // aspiration (hit lists in the goal file). no-duplicate-string + // stays OFF -- Sonar's own default posture; i18n keys and + // testids are the documented noise class. + 'sonarjs/cognitive-complexity': ['error', 15], + 'sonarjs/no-duplicated-branches': 'error', + 'sonarjs/no-identical-functions': 'error', // eslint-plugin-react-hooks 6.x/7.x folded the React Compiler's // lint rules into `recommended` (goal: dependency majors sweep, // eslint 10 + react-hooks 7.1.1). `set-state-in-effect` flags diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f56d425c..10bf78aa 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -49,6 +49,7 @@ "eslint-plugin-i18next": "^6.1.5", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", + "eslint-plugin-sonarjs": "^4.2.0", "globals": "^17.11.0", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", @@ -2403,6 +2404,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3549,6 +3563,31 @@ "eslint": "^9 || ^10" } }, + "node_modules/eslint-plugin-sonarjs": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-sonarjs/-/eslint-plugin-sonarjs-4.2.0.tgz", + "integrity": "sha512-bqADfuNtTL7VK6RU29eoiFTtaaBKIpVPuX3bOl+rBpWSBa0zIBVZlqZNZQjfP6s4iXkAJokv5IsD8OsACkwApg==", + "dev": true, + "license": "LGPL-3.0-only", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "builtin-modules": "^3.3.0", + "bytes": "^3.1.2", + "functional-red-black-tree": "^1.0.1", + "globals": "^17.7.0", + "jsx-ast-utils-x": "^0.1.0", + "lodash.merge": "^4.6.2", + "minimatch": "^10.2.5", + "scslre": "^0.3.0", + "semver": "^7.8.5", + "ts-api-utils": "^2.5.0", + "typescript": ">=5 <6.1.0", + "yaml": "^2.9.0" + }, + "peerDependencies": { + "eslint": "^8.0.0 || ^9.0.0 || ^10.0.0" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", @@ -3957,6 +3996,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true, + "license": "MIT" + }, "node_modules/genson-js": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/genson-js/-/genson-js-0.0.8.tgz", @@ -4524,6 +4570,16 @@ "node": ">=6" } }, + "node_modules/jsx-ast-utils-x": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/jsx-ast-utils-x/-/jsx-ast-utils-x-0.1.0.tgz", + "integrity": "sha512-eQQBjBnsVtGacsG9uJNB8qOr3yA8rga4wAaGG1qRcBzSIvfhERLrWxMAM1hp5fcS6Abo8M4+bUBTekYR0qTPQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/katex": { "version": "0.16.47", "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", @@ -4889,6 +4945,13 @@ "integrity": "sha512-3/Qptq2vr7WeJbB4KHUSKlq8Pl7ASXi3UG6CMbBm8WRtXi8+GHm7mKaU3urfpSEzWe2wCIChs6/sdocUsTKJiA==", "license": "MIT" }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -5755,6 +5818,33 @@ "redux": "^5.0.0" } }, + "node_modules/refa": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/refa/-/refa-0.12.1.tgz", + "integrity": "sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/regexp-ast-analysis": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/regexp-ast-analysis/-/regexp-ast-analysis-0.7.1.tgz", + "integrity": "sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.1" + }, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -5918,6 +6008,21 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/scslre": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/scslre/-/scslre-0.3.0.tgz", + "integrity": "sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.8.0", + "refa": "^0.12.0", + "regexp-ast-analysis": "^0.7.0" + }, + "engines": { + "node": "^14.0.0 || >=16.0.0" + } + }, "node_modules/semver": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", @@ -6734,6 +6839,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a4600d45..0422ccb9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,6 +54,7 @@ "eslint-plugin-i18next": "^6.1.5", "eslint-plugin-react-hooks": "^7.1.1", "eslint-plugin-react-refresh": "^0.5.4", + "eslint-plugin-sonarjs": "^4.2.0", "globals": "^17.11.0", "typescript": "^6.0.3", "typescript-eslint": "^8.67.0", diff --git a/frontend/src/app/WorkTabShell.tsx b/frontend/src/app/WorkTabShell.tsx index 2d2b55d2..756a5393 100644 --- a/frontend/src/app/WorkTabShell.tsx +++ b/frontend/src/app/WorkTabShell.tsx @@ -122,6 +122,7 @@ export function WorkTabShell({ pageLabel, pageIcon, titlebarSlot, children }: { const workflowLabel = (id: string) => workflows?.find((w) => w.ID === id)?.Label const requestLabel = (id: string) => requests?.find((r) => r.ID === id)?.Label + // eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) const renderTab = (tab: WorkTab) => { switch (tab.kind) { case 'workflow-edit': diff --git a/frontend/src/app/pageMeta.tsx b/frontend/src/app/pageMeta.tsx index 5440648a..b8532f41 100644 --- a/frontend/src/app/pageMeta.tsx +++ b/frontend/src/app/pageMeta.tsx @@ -29,6 +29,7 @@ export function pageLabelFor(view: View, capabilities: { ID: string; Label: stri // that section (CAPABILITY_ICON), so the band's first tab matches the // selected nav item's silhouette. Settings/placeholder views have no // capability icon; the tab simply shows no glyph there. +// eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) export function pageIconFor(view: View): ReactNode { const key = view.kind === 'home' ? 'capability-home' : view.kind === 'composition' ? 'capability-composition' diff --git a/frontend/src/atlas/atlasBuildBoardNodes.ts b/frontend/src/atlas/atlasBuildBoardNodes.ts index 32a7adeb..c061bb5e 100644 --- a/frontend/src/atlas/atlasBuildBoardNodes.ts +++ b/frontend/src/atlas/atlasBuildBoardNodes.ts @@ -20,6 +20,7 @@ export type BoardCardRFNode = AtlasNoteCardRFNode | AtlasGroupRFNode | AtlasRegi // non-draggable preview nodes anchored inside it (parentId + // extent:'parent') -- one nesting level deep, regardless of board // mode; a childless card renders as a flippable note (AtlasNoteCardNode). +// eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) export function buildBoardCardNodes({ cards, allCards, kinds, links, linkKinds, isFree, readOnly, boardWidth, freeMoves, arteries, pulsedID, hintedID, hoveredFrameID, isSoleSelected, onOpenOverlay, handleDrill, diff --git a/frontend/src/atlas/atlasOverlapResolution.ts b/frontend/src/atlas/atlasOverlapResolution.ts index d7730b60..04a455da 100644 --- a/frontend/src/atlas/atlasOverlapResolution.ts +++ b/frontend/src/atlas/atlasOverlapResolution.ts @@ -32,6 +32,7 @@ const MAX_PASSES = 24 // yesterday-clear layout can overlap today with nobody having moved a // card -- those collisions auto-resolve. A leaf-on-leaf overlap is // hand placement and is left exactly where the user put it. +// eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) export function resolveFreeOverlaps(boxes: OverlapBox[]): OverlapMove[] { const work = boxes.map((b) => ({ ...b })) // Stable processing order: by id, so the same board always nudges diff --git a/frontend/src/atlas/useAtlasCreation.ts b/frontend/src/atlas/useAtlasCreation.ts index 8ff67d78..d68e98ca 100644 --- a/frontend/src/atlas/useAtlasCreation.ts +++ b/frontend/src/atlas/useAtlasCreation.ts @@ -194,6 +194,7 @@ export function useAtlasCreation({ parentID, allCards, notes, readOnly, screenTo // identity changing when the popover opens/closes is bounded and // never feeds the sticky-node data path that caused the React #185 // loop this file's other comments describe. + // eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) const submitPopover = useCallback((kindID: string, title: string) => { const pending = popover if (!pending) return diff --git a/frontend/src/atlas/useAtlasKeyboardNav.ts b/frontend/src/atlas/useAtlasKeyboardNav.ts index 2d6ed2ad..0ef94851 100644 --- a/frontend/src/atlas/useAtlasKeyboardNav.ts +++ b/frontend/src/atlas/useAtlasKeyboardNav.ts @@ -77,6 +77,7 @@ export function useAtlasKeyboardNav({ if (e.key in ARROW_KEYS && pendingNudgeRef.current.size > 0) flushNudge() } + // eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) const onKeyDown = (e: KeyboardEvent) => { if (isEditableTarget(e.target)) return if (!isFocusInsideBoard(wrapperRef)) return diff --git a/frontend/src/composition/CanvasNodeView.tsx b/frontend/src/composition/CanvasNodeView.tsx index 0e3fc6d2..8c68d31d 100644 --- a/frontend/src/composition/CanvasNodeView.tsx +++ b/frontend/src/composition/CanvasNodeView.tsx @@ -44,6 +44,7 @@ function runStatusLabelFor(t: (key: string) => string): Record) { const { t } = useTranslation('composition') const RUN_STATUS_LABEL = runStatusLabelFor(t) diff --git a/frontend/src/composition/NodeConfigFields.tsx b/frontend/src/composition/NodeConfigFields.tsx index 584e338c..9493ea5b 100644 --- a/frontend/src/composition/NodeConfigFields.tsx +++ b/frontend/src/composition/NodeConfigFields.tsx @@ -215,6 +215,7 @@ export function NodeConfigFields({ node, workflowId, attrs, nodeType, sameKindNo // decisions 4-5), not a raw number field a user has to already // know. .filter((field) => !(node.data.nodeTypeID === 'decision-outcome' && field.Key === 'version')) + // eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) .map((field) => ( {field.Label} diff --git a/frontend/src/composition/TriggerRowLabel.tsx b/frontend/src/composition/TriggerRowLabel.tsx index 0b9c6f58..6f2ed508 100644 --- a/frontend/src/composition/TriggerRowLabel.tsx +++ b/frontend/src/composition/TriggerRowLabel.tsx @@ -35,6 +35,7 @@ interface TriggerRowLabelProps { // node's real type and config instead of a second, independent guess at // the same thing. Shared by WorkflowsTable.tsx and CompositionView.tsx's // InventoryList rows (docs/goals/0007, the former WorkflowsCards.tsx). +// eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) export function TriggerRowLabel({ workflow, armed, publishing, onPublish, onHotkeyChanged }: TriggerRowLabelProps) { const { t } = useTranslation('composition') const rootNode = findRootNode(workflow.Nodes, workflow.Edges) diff --git a/frontend/src/composition/WorkflowRunsPanel.tsx b/frontend/src/composition/WorkflowRunsPanel.tsx index 32e3476b..5154e8f4 100644 --- a/frontend/src/composition/WorkflowRunsPanel.tsx +++ b/frontend/src/composition/WorkflowRunsPanel.tsx @@ -366,6 +366,7 @@ function WorkflowRunsPanel({ workflowId, attrs, initialRunId, onInitialRunConsum {detail.error && {detail.error}} + {/* eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) */} {detail.pending && (() => { // A breakpoint or step-mode park is a DEBUG park // (docs/adr/0031) -- distinct icon/wording/controls, never diff --git a/frontend/src/configure/openapiSynth.ts b/frontend/src/configure/openapiSynth.ts index 0b382c20..453dc18b 100644 --- a/frontend/src/configure/openapiSynth.ts +++ b/frontend/src/configure/openapiSynth.ts @@ -239,6 +239,7 @@ function fieldFromSchema(name: string, schema: JSONSchemaLike, placement: Manual // non-JSON media types are silently skipped rather than guessed at. // JSON only (no YAML) -- this runs in the browser with no YAML parser // adopted for it; paste JSON, or keep editing via the raw-text mode. +// eslint-disable-next-line sonarjs/cognitive-complexity -- legacy complexity grandfathered at gate adoption; pay down when touched (goal 0109 burn-down) export function parseOpenAPIToOperations(t: (key: string, opts?: Record) => string, specText: string): { operations: ManualOperation[]; errors: string[] } { let doc: { paths?: Record> } try {