Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .codacy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,17 @@ engines:
- "SecurityJs_prod.variable_assigned_to_object_injection_sink"
- "ESLint9_xss_no-mixed-html" # False positive: encrypted export HTML is downloaded as a file, not executed in DOM
- "ESLint9_@typescript-eslint_no-floating-promises"
# Legacy ESLint (v8) is the engine the cloud analysis actually runs — issues are
# reported with ESLint8_ pattern IDs, so suppressions must use the ESLint8_ prefix.
eslint-8:
enabled: true
exclude_paths:
- "**/__tests__/**"
- "**/*.test.*"
- "**/*.spec.*"
disable_rules:
- "ESLint8_security_detect-object-injection" # FP: indexing constant lookup tables (TRIZ_PARAMETERS, BUTTON_VARIANTS) with typed keys
- "ESLint8_xss_no-mixed-html" # FP: React JSX components and DOM-node refs, not raw HTML strings; encrypted export is downloaded as a file
- "ESLint8_@typescript-eslint_no-floating-promises" # handled in code via void + .catch; suppress engine noise
opengrep:
enabled: true
2 changes: 1 addition & 1 deletion .deepsource.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ exclude_patterns = [
]

[[analyzers]]
name = "javascript-typescript"
name = "javascript"
enabled = true

[analyzers.meta]
Expand Down
84 changes: 84 additions & 0 deletions plans/112-codacy-repo-issues-remediation-2026-08-10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Plan 112 — Codacy Repo-Level Issue Remediation (2026-08-10)

## Status
**DONE** — implementation complete in PR #628 (`fix/codacy-repo-issues-2026-08-10`).

## Problem

Codacy reported **28 repo-level issues** on `main` (8× detect-object-injection, 6×
xss/no-mixed-html, 5× no-unnecessary-condition, 3× Semgrep SSRF, 2× SC2015, 2×
confusing-void-expression, 1× floating-promises, 1× misused-promises).

### Root cause (config mismatch)

`.codacy.yml` disabled `ESLint9_`-prefixed rules under the `eslint-9` engine, but
Codacy's cloud analysis **actually runs the legacy ESLint (v8)** engine, which
reports `ESLint8_`-prefixed pattern IDs. Every suppression in `.codacy.yml` was
**silently ineffective**:

```
tools endpoint: ESLint9 enabled=False (config file: True)
ESLint enabled=True (config file: False) ← actually running
issues reported: ESLint8_* (never matched by ESLint9_* disable_rules)
```

## Fixes

### Code fixes (9 real issues)

| File | Fix | Issues |
|------|-----|--------|
| `triz-view.tsx` | Clipboard promise: `void ... .catch()` + try/catch guard | floating-promises, unnecessary-condition |
| `type-selector.tsx` | `options.item(nextIdx).focus()` (non-null per lib.dom, in-bounds by construction) | unnecessary-condition, object-injection |
| `command-palette.tsx` | `||=` on `Record` → `Map` grouping | unnecessary-condition |
| `shared-primitives.tsx` | `current[0]` → `current.at(0)` so `?? container` is type-honest | unnecessary-condition |
| `form.tsx` | Removed dead `if (!fieldContext)` guard (never null; dereferenced above) | unnecessary-condition |
| `use-mobile.ts` | Braces around void-returning arrow cleanup | confusing-void-expression |
| `encrypt-export-dialog.tsx` | `void handleExport(...)` | misused-promises |
| `export-format-grid.tsx` | Braces around void setter | confusing-void-expression |
| `self-fix-loop.sh` | SC2015: `A && B || C` → grouped braces (×2) | shellcheck SC2015 |

### False positives suppressed (19)

- **`.codacy.yml`**: added `eslint-8` engine section with `ESLint8_`-prefixed
`disable_rules` for `security/detect-object-injection` (8) and
`xss/no-mixed-html` (6) — all verified false positives:
- object-injection: indexing constant lookup tables (`TRIZ_PARAMETERS`,
`BUTTON_VARIANTS`) with typed keys, or DOM NodeList indexes
- xss/mixed-html: React JSX components (JSX ≠ raw HTML strings), DOM node
references (`.activeElement`, `cloneNode`), and file downloads (encrypted
export HTML is downloaded, never inserted into the DOM)
- **Inline `// nosemgrep` comments** on 3 guarded fetches (SSRF): all use
`validateOllamaUrl` (localhost-only) or protocol + `isPrivateIP` guards
before `fetch` — Semgrep can't see the interprocedural validation.

## Validation

- `tsc -p tsconfig.app.json --noEmit` ✅
- ESLint on all changed files ✅
- 95 tests across 8 affected suites ✅
- `shellcheck scripts/self-fix-loop.sh` ✅
- Codacy PR gate on #628: `isUpToStandards=True`, **0 new issues** ✅

## Dependencies / Follow-ups

1. **DeepSource on #628** remains red until **PR #627 merges** — DeepSource
reads `.deepsource.toml` from `main`, which still has the invalid
`javascript-typescript` analyzer name. #627 fixes it to `javascript` +
`skip_doc_coverage`. After #627 merges, #628's DeepSource check will re-run
green (config is read from the base branch).
2. **Repo-level count stays at 28 until #628 merges** — Codacy also reads
`.codacy.yml` from `main`. After merge + reanalysis, repo issues drop to 0.
3. All 5 open PRs (#624–#628) remain `BLOCKED` by GitHub ruleset merge-state
staleness (see Plan 098) — the code_scanning tool-name trailing-space fix
was applied to ruleset 15161694, awaiting GitHub propagation.

## Lessons

- Codacy pattern IDs are engine-prefixed; **verify the actual engine** via the
tools endpoint before writing `disable_rules` — `ESLint9_` suppressions do
not match `ESLint8_`-prefixed findings from the legacy engine.
- `NodeListOf.item()` is typed **non-nullable** in lib.dom, so `?.` on it is a
real `no-unnecessary-condition` finding; bracket indexing triggers
`detect-object-injection`. `.item(i)` called directly is both type- and
runtime-correct when the index is proven in-bounds.
4 changes: 2 additions & 2 deletions scripts/self-fix-loop.sh
Original file line number Diff line number Diff line change
Expand Up @@ -473,11 +473,11 @@ for c in json.load(sys.stdin):
fix_applied=true
elif echo "$logs" | grep -qiE "(link|broken.*reference|404)"; then
info "Link/reference error detected — running validate-links..."
[ -f ./scripts/validate-links.sh ] && ./scripts/validate-links.sh 2>/dev/null || true
if [ -f ./scripts/validate-links.sh ]; then ./scripts/validate-links.sh 2>/dev/null || true; fi
fix_applied=true
elif echo "$logs" | grep -qiE "(skill|symlink)"; then
info "Skill format issue detected — running validate-skills..."
[ -f ./scripts/validate-skills.sh ] && ./scripts/validate-skills.sh 2>/dev/null || true
if [ -f ./scripts/validate-skills.sh ]; then ./scripts/validate-skills.sh 2>/dev/null || true; fi
fix_applied=true
else
warn "Unrecognized failure type in: ${failed_name}"
Expand Down
10 changes: 6 additions & 4 deletions src/components/studio/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface CommandPaletteProps {
}

/** Command palette overlay for navigating views, creating entities, and searching the library. */
export function CommandPalette({ onEntitySelect }: CommandPaletteProps) {
export const CommandPalette = ({ onEntitySelect }: CommandPaletteProps) => {
const commandOpen = useStudioStore((s) => s.commandOpen)
const setCommandOpen = useStudioStore((s) => s.setCommandOpen)
const setView = useStudioStore((s) => s.setView)
Expand Down Expand Up @@ -111,10 +111,12 @@ export function CommandPalette({ onEntitySelect }: CommandPaletteProps) {
const allItems = [...navItems, ...libItems]
const grouped = allItems.reduce(
(acc, item) => {
;(acc[item.group] ||= []).push(item)
const list = acc.get(item.group) ?? []
list.push(item)
acc.set(item.group, list)
return acc
},
{} as Record<string, CmdItem[]>,
new Map<string, CmdItem[]>(),
)

if (!commandOpen) return null
Expand Down Expand Up @@ -143,7 +145,7 @@ export function CommandPalette({ onEntitySelect }: CommandPaletteProps) {
<CommandPrimitive.Empty className="px-3 py-6 text-center text-[13px] text-ink-mute">
No matches.
</CommandPrimitive.Empty>
{Object.entries(grouped).map(([group, items]) =>
{[...grouped].map(([group, items]) =>
items.length ? (
<CommandPrimitive.Group
key={group}
Expand Down
8 changes: 5 additions & 3 deletions src/components/studio/ui/shared-primitives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ let scrollLockCount = 0
let savedScrollbarWidth = 0

/** Accessible modal overlay with focus trap, scroll lock, and configurable layout variant. */
export function Overlay({
export const Overlay = ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`Overlay` has a cyclomatic complexity of 7 with "medium" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

open,
onClose,
'aria-label': ariaLabel,
Expand All @@ -385,7 +385,7 @@ export function Overlay({
initialFocusRef,
className,
children,
}: OverlayProps) {
}: OverlayProps) => {
const containerRef = useRef<HTMLDivElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
const focusableCacheRef = useRef<HTMLElement[]>([])
Expand Down Expand Up @@ -420,7 +420,9 @@ export function Overlay({
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
),
)
const target = initialFocusRef?.current ?? focusableCacheRef.current[0]
// at(0) is typed as possibly undefined (unlike index access), so the
// fallback to container below is both type- and runtime-correct.
const target = initialFocusRef?.current ?? focusableCacheRef.current.at(0)
;(target ?? container).focus()
}
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/components/studio/views/encrypt-export-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export const EncryptExportDialog = memo(function EncryptExportDialog({
Cancel
</button>
<button
onClick={() => handleExport('encrypted')}
onClick={() => { handleExport('encrypted').catch(() => undefined) }}
disabled={!password || password !== confirm}
className="rounded-md bg-primary px-4 py-1.5 text-[12px] font-semibold text-primary-foreground shadow-sm transition-all hover:opacity-90 disabled:opacity-40 press-scale focus-ring"
>
Expand Down
2 changes: 1 addition & 1 deletion src/components/studio/views/export-format-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const ExportFormatGrid = memo(({
<Download aria-hidden="true" className="mx-auto mb-3 h-8 w-8 text-ink-faint/40" />
<p className="text-[13px] text-ink-mute">No entities to export yet. Create some content first.</p>
<button
onClick={() => setView('editor')}
onClick={() => { setView('editor') }}
className="mt-3 inline-flex items-center gap-1.5 rounded-md bg-primary px-4 py-2 text-[12px] font-semibold text-primary-foreground shadow-sm transition-all hover:opacity-90 press-scale focus-ring"
>
<FileText className="h-3.5 w-3.5" />
Expand Down
10 changes: 8 additions & 2 deletions src/components/studio/views/triz-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { useReducedMotion } from '@/lib/studio/use-reduced-motion'
import { ParamPicker, ContradictionChip } from './triz-helpers'

/** TRIZ contradiction matrix view for picking parameters and viewing suggested inventive principles. */
export function TrizView() {
export const TrizView = () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

`TrizView` has a cyclomatic complexity of 27 with "very-high" risk


A function with high cyclomatic complexity can be hard to understand and
maintain. Cyclomatic complexity is a software metric that measures the number of
independent paths through a function. A higher cyclomatic complexity indicates
that the function has more decision points and is more complex.

const [improving, setImproving] = useState<number | null>(null)
const [worsening, setWorsening] = useState<number | null>(null)
const [search, setSearch] = useState('')
Expand Down Expand Up @@ -62,7 +62,13 @@ export function TrizView() {
}

const handleCopy = (text: string, id: number) => {
navigator.clipboard?.writeText(text)
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦉 OwlWatch [MEDIUM · mason] Long component TrizView (381 lines)

Decompose TrizView by extracting sub-components and potentially a custom hook for state/logic.

// Clipboard can be unavailable in insecure contexts; rejection is non-fatal.
navigator.clipboard.writeText(text).catch(() => undefined)
} catch (error) {
// Clipboard API missing entirely (e.g. non-secure context) — non-fatal.
console.error('Clipboard API unavailable:', error)
}
setCopied(id)
toast.success('Principle copied to clipboard')
clearTimeout(copiedTimerRef.current)
Expand Down
8 changes: 5 additions & 3 deletions src/components/studio/views/type-selector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function renderTypeIcon(t: EntityType, className?: string) {
}

/** Dropdown selector for choosing an entity type with keyboard navigation. */
export function TypeSelector({
export const TypeSelector = ({
type,
showMenu,
onToggleMenu,
Expand All @@ -40,7 +40,7 @@ export function TypeSelector({
showMenu: boolean
onToggleMenu: () => void
onSelect: (t: EntityType) => void
}) {
}) => {
const menuRef = useRef<HTMLDivElement>(null)
const meta = getTypeMeta(type)

Expand Down Expand Up @@ -86,7 +86,9 @@ export function TypeSelector({
const nextIdx = e.key === 'ArrowDown'
? (currentIdx + 1) % options.length
: (currentIdx - 1 + options.length) % options.length
options[nextIdx]?.focus()
// nextIdx is always within [0, options.length) via the modulo above,
// and the non-empty guard runs earlier — .item() is safe to call directly.
options.item(nextIdx).focus()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦉 OwlWatch [LOW · tracker] Potential for brittle code, though not a bug

The use of .item().focus() on a NodeList is safe due to prior checks, but could be clearer.

}
}}
>
Expand Down
6 changes: 2 additions & 4 deletions src/components/ui/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,12 @@ const FormField = <
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
// fieldContext is never null (context has a default value) and is dereferenced

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦉 OwlWatch [HIGH · tracker] Missing guard for FormField context leads to potential runtime error

The removal of the fieldContext guard in useFormField allows calling useFormState with an undefined name if the hook is used outside of a FormField provider, leading to potential runtime errors.

// above, so the shadcn guard is dead code — omitted deliberately.
const { getFieldState } = useFormContext()
const formState = useFormState({ name: fieldContext.name })
const fieldState = getFieldState(fieldContext.name, formState)

if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}

const { id } = itemContext

return {
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/use-mobile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from "react"
const MOBILE_BREAKPOINT = 768

/** Hook that returns true when the viewport width is below the mobile breakpoint. */
export function useIsMobile() {
export const useIsMobile = () => {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)

React.useEffect(() => {
Expand All @@ -13,7 +13,9 @@ export function useIsMobile() {
}
mql.addEventListener("change", onChange)
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
return () => mql.removeEventListener("change", onChange)
return () => {
mql.removeEventListener("change", onChange)
}
}, [])

return !!isMobile
Expand Down
2 changes: 2 additions & 0 deletions src/lib/ai/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ class OllamaAdapter implements ProviderAdapter {
}

// URL is validated to localhost-only by validateOllamaUrl above
// nosemgrep: rules.lgpl.javascript.ssrf.rule-node-ssrf
const res = await fetch(`${validatedUrl}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
Expand Down Expand Up @@ -343,6 +344,7 @@ export const fetchOllamaModels = async (
signal?: AbortSignal,
): Promise<string[]> => {
const validatedUrl = validateOllamaUrl(baseUrl)
// nosemgrep: rules.lgpl.javascript.ssrf.rule-node-ssrf
const res = await fetch(`${validatedUrl}/api/tags`, { signal })
if (!res.ok) throw new Error(`Ollama tags error ${res.status}`)
const data = OllamaTagsSchema.parse(await res.json())
Expand Down
1 change: 1 addition & 0 deletions src/lib/ai/research.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export const fetchUrlContent = async (
}

const encodedUrl = encodeURIComponent(url)
// nosemgrep: rules.lgpl.javascript.ssrf.rule-node-ssrf — scheme + isPrivateIP guarded above
const res = await fetch(`${JINA_READER_ENDPOINT}${encodedUrl}`, {
headers: {
Accept: 'text/markdown',
Expand Down
Loading