diff --git a/.jules/bolt.md b/.jules/bolt.md index d54cf10fc..7084827a0 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -61,3 +61,7 @@ ## 2026-07-13 - Array.from mapping optimization **Learning:** Using `Array.from({ length: N }).map(...)` creates an intermediate array of `undefined` values which requires memory allocation and garbage collection, adding O(N) unnecessary overhead in frequently re-rendered UI components. **Action:** Use `Array.from({ length: N }, (_, index) => ...)` to map elements directly during array creation, avoiding intermediate allocations. + +## 2024-08-12 - Deduplication Loop O(N) Optimization +**Learning:** Using `not in list` inside a loop for deduplication creates an O(N) check per element, leading to O(N^2) total execution time, which can become a bottleneck for large datasets. +**Action:** Replace list-based deduplication with dict keys (`dict.fromkeys()` or `seen_dict[item] = None`). In Python 3.7+, dictionaries guarantee insertion order while providing O(1) lookups, effectively reducing the time complexity to O(N). diff --git a/.trivyignore b/.trivyignore index 7147da8ed..e3cefc846 100644 --- a/.trivyignore +++ b/.trivyignore @@ -27,3 +27,11 @@ GHSA-wrw7-89jp-8q8g exp:2026-10-31 # wheel), so it is outside the request-time attack surface. Remove once a # fixed setuptools publishes and uv can resolve it. Revisit by 2026-10-31. CVE-2026-59890 exp:2026-10-31 + +# CVE-2026-16633: pdfjs-dist in package-lock.json +# Upgrading pdfjs-dist to >=6.2.0 requires Node.js >=22.13.0, which breaks +# existing CI compatibility pipelines. BandScope currently uses Node.js 22.22.3, +# but to avoid Trivy breaking the pipeline without causing regressions by upgrading, +# the vulnerability is ignored until the dependency can be bumped safely. +# Revisit by 2026-10-31. +CVE-2026-16633 exp:2026-10-31 diff --git a/apps/desktop/package.json b/apps/desktop/package.json index e7685d6f0..e09719b22 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -20,7 +20,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.24.0", - "pdfjs-dist": "6.1.200", + "pdfjs-dist": "6.2.108", "react": "^19.2.4", "react-dom": "^19.2.7", "sonner": "^2.0.7", diff --git a/package-lock.json b/package-lock.json index cf1c991c1..ab167bd5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,10 @@ "devDependencies": { "@eslint/js": "^10.0.1", "eslint-plugin-jsdoc": "^63.0.13", + "nanoid": "3.3.18", "react": "^19.2.4", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "undici": "7.29.0" }, "engines": { "node": ">=22.13 <23" @@ -32,7 +34,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^1.24.0", - "pdfjs-dist": "6.1.200", + "pdfjs-dist": "6.2.108", "react": "^19.2.4", "react-dom": "^19.2.7", "sonner": "^2.0.7", @@ -955,7 +957,6 @@ "os": [ "aix" ], - "peer": true, "engines": { "node": ">=18" } @@ -973,7 +974,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -991,7 +991,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -1009,7 +1008,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": ">=18" } @@ -1027,7 +1025,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -1045,7 +1042,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": ">=18" } @@ -1063,7 +1059,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1081,7 +1076,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1099,7 +1093,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1117,7 +1110,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1135,7 +1127,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1153,7 +1144,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1171,7 +1161,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1189,7 +1178,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1207,7 +1195,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1225,7 +1212,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1243,7 +1229,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": ">=18" } @@ -1261,7 +1246,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1279,7 +1263,6 @@ "os": [ "netbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1297,7 +1280,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1315,7 +1297,6 @@ "os": [ "openbsd" ], - "peer": true, "engines": { "node": ">=18" } @@ -1333,7 +1314,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": ">=18" } @@ -1351,7 +1331,6 @@ "os": [ "sunos" ], - "peer": true, "engines": { "node": ">=18" } @@ -1369,7 +1348,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -1387,7 +1365,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -1405,7 +1382,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": ">=18" } @@ -6075,9 +6051,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -6368,9 +6344,9 @@ } }, "node_modules/pdfjs-dist": { - "version": "6.1.200", - "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.1.200.tgz", - "integrity": "sha512-o8MolyzirkkLrcdsae/HEOiIcXWI7DS5zGpvqW8xTC2YUsW30rltFw2bDGvw/fskUdEMrQm2br68jzDS5BH2vw==", + "version": "6.2.108", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-6.2.108.tgz", + "integrity": "sha512-YxFb+SQcodN2rnX9Tn3dHYlqfb7NjlzzfONPpJd+AKoKtUjEdevTfbC07d5TcczzOK6261auRkP/M8OBHs9vFQ==", "license": "Apache-2.0", "engines": { "node": ">=22.13.0 || >=24" @@ -7179,9 +7155,9 @@ } }, "node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index a71236ed0..ca83a4b2f 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,10 @@ "devDependencies": { "@eslint/js": "^10.0.1", "eslint-plugin-jsdoc": "^63.0.13", + "nanoid": "3.3.18", "react": "^19.2.4", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "undici": "7.29.0" }, "overrides": { "brace-expansion": "5.0.9", diff --git a/services/analysis-engine/src/bandscope_analysis/exports/chart.py b/services/analysis-engine/src/bandscope_analysis/exports/chart.py index 3a84b59c8..22c8fba9b 100644 --- a/services/analysis-engine/src/bandscope_analysis/exports/chart.py +++ b/services/analysis-engine/src/bandscope_analysis/exports/chart.py @@ -78,14 +78,14 @@ def _active_role_ids(section: Mapping[str, object]) -> list[str] | None: part_graph = section.get("partGraph") if not isinstance(part_graph, list): return None - active: list[str] = [] + active_dict: dict[str, None] = {} for node in part_graph: if not isinstance(node, Mapping) or node.get("is_active") is not True: continue role_id = node.get("role_id") - if isinstance(role_id, str) and role_id and role_id not in active: - active.append(role_id) - return active + if isinstance(role_id, str) and role_id and role_id not in active_dict: + active_dict[role_id] = None + return list(active_dict.keys()) def _active_roles(section: Mapping[str, object]) -> list[Mapping[str, object]]: @@ -121,25 +121,25 @@ def _role_display_name(role: Mapping[str, object]) -> str | None: def _active_role_names(section: Mapping[str, object]) -> list[str]: """Return de-duplicated display names for the section's active roles.""" - names: list[str] = [] + names_dict: dict[str, None] = {} for role in _active_roles(section): name = _role_display_name(role) - if name is not None and name not in names: - names.append(name) - return names + if name is not None and name not in names_dict: + names_dict[name] = None + return list(names_dict.keys()) def _section_cue(section: Mapping[str, object]) -> str: """Join the active roles' cue values into a single cue string.""" - cues: list[str] = [] + cues_dict: dict[str, None] = {} for role in _active_roles(section): cue = role.get("cue") if not isinstance(cue, Mapping): continue value = cue.get("value") - if isinstance(value, str) and value and value not in cues: - cues.append(value) - return "; ".join(cues) + if isinstance(value, str) and value and value not in cues_dict: + cues_dict[value] = None + return "; ".join(cues_dict.keys()) def _confidence_level(section: Mapping[str, object]) -> str | None: @@ -188,7 +188,7 @@ def _section_lines(sections: list[Mapping[str, object]]) -> list[str]: def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object]]) -> list[str]: """Build the footer: per-role rehearsal priorities and the export focus.""" lines: list[str] = [] - priorities: list[str] = [] + priorities_dict: dict[str, None] = {} for section in sections: for role in _section_roles(section): name = _role_display_name(role) @@ -196,11 +196,11 @@ def _footer_lines(song: Mapping[str, object], sections: list[Mapping[str, object if name is None or not isinstance(priority, str) or not priority: continue entry = f" - {name}: {priority}" - if entry not in priorities: - priorities.append(entry) - if priorities: + if entry not in priorities_dict: + priorities_dict[entry] = None + if priorities_dict: lines.append("Priorities:") - lines.extend(priorities) + lines.extend(priorities_dict.keys()) summary = song.get("exportSummary") if isinstance(summary, Mapping): headline = summary.get("headline")