diff --git a/CONTEXT.md b/CONTEXT.md index cb87df5f..61c8a2e0 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,25 +1,19 @@ -# CONTEXT - bitvid-perf-agent +# CONTEXT - bitvid-audit-agent ## Goal -Daily, measurable improvement of app responsiveness by identifying and remediating background CPU/network work that degrades UX — while also ensuring user-facing docs (/content) reflect the actual runtime behavior of upload/contribution flows. +Run the daily/weekly static audit scripts (file size, innerHTML, lint) to detect regressions and generate a report. ## Scope - Repo: `PR0M3TH3AN/bitvid` (unstable branch). -- Focus Areas: - - P0: Login/auth, relay initialization, decryption (hashtags, subscriptions, blocks, watch history). - - P1: Initial UI responsiveness (profile cache, relay load). - - P2: User-initiated background features (WebTorrent). - - Docs: `/content` (user-facing docs), ensuring upload/contribution docs match code. +- Focus: Static analysis reports. ## Constraints -- Never invent files, APIs, libraries, or behaviors. -- Prefer minimal incremental changes. -- Keep build/tests green. -- Preserve repo style and architecture. -- Security-critical changes require human signoff. +- Read-only job (do not modify source files). +- Reproducible reports. +- Confidentiality (redact secrets). ## Definition of Done -- Performance issues identified and fixed with small, safe PRs. -- Docs in `/content` matched to code. -- `daily-perf-report` generated. -- Artifacts (`TODO.md`, `DECISIONS.md`, `TEST_LOG.md`) updated. +- Audit scripts run successfully. +- Artifacts (`raw-*.log`, `*-report.json`) generated. +- `summary.md` created. +- `AGENT_TASK_LOG.csv` updated. diff --git a/DECISIONS.md b/DECISIONS.md index b743244a..1a79d846 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1,5 +1,14 @@ # Decisions Log +## 2026-02-12: Audit Run + +### Context +Running the scheduled `audit-agent` task. + +### Decision: Initial Run +- **Action:** Created initial audit report artifacts. +- **Rationale:** No previous reports found, so no delta comparison performed. + ## 2026-02-17: Dependency Audit & Upgrade ### Context diff --git a/TODO.md b/TODO.md index 5a2f77c4..610bd111 100644 --- a/TODO.md +++ b/TODO.md @@ -1,14 +1,16 @@ -# TODO: Dependency Management +# TODO: Audit Run 2026-02-12 -## Priority: High (Blockers) +## Priority: High (Audit) +- [x] Run `scripts/check-file-size.mjs --report`. +- [x] Run `scripts/check-innerhtml.mjs --report`. +- [x] Run `npm run lint`. +- [x] Parse logs into JSON. +- [x] Generate summary report. + +## Priority: Medium (Backlog) - [ ] Fix E2E test environment: `npx playwright install` must be run in the CI/Agent environment before running `npm run test:e2e` for upgrades. This blocked the `esbuild` upgrade. - -## Priority: Medium (Security/Compliance) - [ ] Review `nostr-tools` upgrade (v2.19.4 -> v2.23.0). See `issues/upgrade-nostr-tools.md`. - [ ] Review `tailwindcss` (v4). See `issues/upgrade-tailwindcss.md`. ## Priority: Low (Maintenance) - [ ] Batch upgrade dev dependencies (`stylelint`, `postcss-import`). - -## Priority: High (Performance) -- [x] Bound concurrency in `authService.js` for profile hydration (P0). diff --git a/artifacts/audit/2026-02-12/file-size-report.json b/artifacts/audit/2026-02-12/file-size-report.json new file mode 100644 index 00000000..8e2145cd --- /dev/null +++ b/artifacts/audit/2026-02-12/file-size-report.json @@ -0,0 +1,224 @@ +{ + "grandfathered_files": 43, + "grandfathered_excess_lines": 49463, + "new_oversized_files": 0, + "new_excess_lines": 0, + "grandfathered_list": [ + { + "path": "js/adminListStore.js", + "lines": 1033, + "excess": 33 + }, + { + "path": "js/app/authSessionCoordinator.js", + "lines": 1743, + "excess": 743 + }, + { + "path": "js/app/feedCoordinator.js", + "lines": 1767, + "excess": 767 + }, + { + "path": "js/app/playbackCoordinator.js", + "lines": 1217, + "excess": 217 + }, + { + "path": "js/app.js", + "lines": 5245, + "excess": 4245 + }, + { + "path": "js/channelProfile.js", + "lines": 5530, + "excess": 4530 + }, + { + "path": "js/feedEngine/stages.js", + "lines": 1350, + "excess": 350 + }, + { + "path": "js/historyView.js", + "lines": 2547, + "excess": 1547 + }, + { + "path": "js/index.js", + "lines": 1407, + "excess": 407 + }, + { + "path": "js/nostr/client.js", + "lines": 3990, + "excess": 2990 + }, + { + "path": "js/nostr/managers/SignerManager.js", + "lines": 1106, + "excess": 106 + }, + { + "path": "js/nostr/nip46Client.js", + "lines": 2102, + "excess": 1102 + }, + { + "path": "js/nostr/nip46Connector.js", + "lines": 1094, + "excess": 94 + }, + { + "path": "js/nostr/nip71.js", + "lines": 1531, + "excess": 531 + }, + { + "path": "js/nostr/publishHelpers.js", + "lines": 1415, + "excess": 415 + }, + { + "path": "js/nostr/watchHistory.js", + "lines": 2211, + "excess": 1211 + }, + { + "path": "js/nostrEventSchemas.js", + "lines": 2908, + "excess": 1908 + }, + { + "path": "js/payments/nwcClient.js", + "lines": 1642, + "excess": 642 + }, + { + "path": "js/services/authService.js", + "lines": 1321, + "excess": 321 + }, + { + "path": "js/services/commentThreadService.js", + "lines": 1122, + "excess": 122 + }, + { + "path": "js/services/hashtagPreferencesService.js", + "lines": 1435, + "excess": 435 + }, + { + "path": "js/services/moderationService.js", + "lines": 2513, + "excess": 1513 + }, + { + "path": "js/services/nostrService.js", + "lines": 2227, + "excess": 1227 + }, + { + "path": "js/services/playbackService.js", + "lines": 1289, + "excess": 289 + }, + { + "path": "js/services/r2Service.js", + "lines": 1194, + "excess": 194 + }, + { + "path": "js/state/cache.js", + "lines": 1829, + "excess": 829 + }, + { + "path": "js/subscriptions.js", + "lines": 2374, + "excess": 1374 + }, + { + "path": "js/ui/applicationBootstrap.js", + "lines": 1169, + "excess": 169 + }, + { + "path": "js/ui/components/EditModal.js", + "lines": 1130, + "excess": 130 + }, + { + "path": "js/ui/components/RevertModal.js", + "lines": 1663, + "excess": 663 + }, + { + "path": "js/ui/components/SimilarContentCard.js", + "lines": 1303, + "excess": 303 + }, + { + "path": "js/ui/components/UploadModal.js", + "lines": 1320, + "excess": 320 + }, + { + "path": "js/ui/components/VideoCard.js", + "lines": 3130, + "excess": 2130 + }, + { + "path": "js/ui/components/VideoModal.js", + "lines": 6285, + "excess": 5285 + }, + { + "path": "js/ui/loginModalController.js", + "lines": 2552, + "excess": 1552 + }, + { + "path": "js/ui/moreMenuController.js", + "lines": 1601, + "excess": 601 + }, + { + "path": "js/ui/overlay/popoverEngine.js", + "lines": 1070, + "excess": 70 + }, + { + "path": "js/ui/profileModalController.js", + "lines": 8757, + "excess": 7757 + }, + { + "path": "js/ui/views/VideoListView.js", + "lines": 1392, + "excess": 392 + }, + { + "path": "js/ui/zapController.js", + "lines": 1206, + "excess": 206 + }, + { + "path": "js/userBlocks.js", + "lines": 2305, + "excess": 1305 + }, + { + "path": "js/watchHistoryService.js", + "lines": 1400, + "excess": 400 + }, + { + "path": "js/webtorrent.js", + "lines": 1038, + "excess": 38 + } + ], + "new_list": [] +} diff --git a/artifacts/audit/2026-02-12/innerhtml-report.json b/artifacts/audit/2026-02-12/innerhtml-report.json new file mode 100644 index 00000000..35e96748 --- /dev/null +++ b/artifacts/audit/2026-02-12/innerhtml-report.json @@ -0,0 +1,177 @@ +{ + "total_assignments": 87, + "files_count": 34, + "top_offenders": [ + { + "path": "js/channelProfile.js", + "count": 10, + "is_new": false + }, + { + "path": "js/ui/components/RevertModal.js", + "count": 10, + "is_new": false + }, + { + "path": "js/docsView.js", + "count": 8, + "is_new": false + }, + { + "path": "js/subscriptions.js", + "count": 7, + "is_new": false + }, + { + "path": "js/ui/subscriptionHistoryController.js", + "count": 7, + "is_new": false + }, + { + "path": "js/historyView.js", + "count": 4, + "is_new": false + }, + { + "path": "js/ui/components/DeleteModal.js", + "count": 3, + "is_new": false + }, + { + "path": "js/ui/components/ShareNostrModal.js", + "count": 3, + "is_new": false + }, + { + "path": "js/ui/dm/DMSettingsModalController.js", + "count": 3, + "is_new": false + }, + { + "path": "js/ui/loginModalController.js", + "count": 3, + "is_new": false + }, + { + "path": "js/ui/views/VideoListView.js", + "count": 3, + "is_new": false + }, + { + "path": "js/viewManager.js", + "count": 3, + "is_new": false + }, + { + "path": "js/utils/qrcode.js", + "count": 2, + "is_new": false + }, + { + "path": "js/app/feedCoordinator.js", + "count": 1, + "is_new": false + }, + { + "path": "js/app.js", + "count": 1, + "is_new": false + }, + { + "path": "js/exploreView.js", + "count": 1, + "is_new": false + }, + { + "path": "js/forYouView.js", + "count": 1, + "is_new": false + }, + { + "path": "js/index.js", + "count": 1, + "is_new": false + }, + { + "path": "js/kidsView.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/components/EditModal.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/components/EmbedVideoModal.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/components/EventDetailsModal.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/components/UploadModal.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/components/VideoModal.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/components/nip71FormManager.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/dm/AppShell.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/dm/Composer.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/dm/ConversationList.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/dm/DMRelaySettings.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/dm/MessageThread.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/dm/NotificationCenter.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/moreMenuController.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/profileModalController.js", + "count": 1, + "is_new": false + }, + { + "path": "js/ui/videoListViewController.js", + "count": 1, + "is_new": false + } + ], + "violations": [] +} diff --git a/artifacts/audit/2026-02-12/lint-report.json b/artifacts/audit/2026-02-12/lint-report.json new file mode 100644 index 00000000..98d0b942 --- /dev/null +++ b/artifacts/audit/2026-02-12/lint-report.json @@ -0,0 +1,11 @@ +{ + "total_failures": 0, + "files_with_errors": [], + "skipped_checks": [ + { + "check": "lint:assets", + "reason": "Missing dist/asset-manifest.json.", + "details": "asset reference check (build required)." + } + ] +} diff --git a/artifacts/audit/2026-02-12/summary.md b/artifacts/audit/2026-02-12/summary.md new file mode 100644 index 00000000..ba6be403 --- /dev/null +++ b/artifacts/audit/2026-02-12/summary.md @@ -0,0 +1,40 @@ +Title: Audit Report — 2026-02-12 (unstable) + +**Summary** + +* Date: 2026-02-12 +* Branch: unstable + +**Metrics** + +* Grandfathered oversized files: 43 files (total excess lines: 49463) +* New oversized files: 0 files (total excess lines: 0) +* Total innerHTML assignments: 87 across 34 files + + * Top offenders: + + 1. js/channelProfile.js — 10 + 2. js/ui/components/RevertModal.js — 10 + 3. js/docsView.js — 8 + 4. js/subscriptions.js — 7 + 5. js/ui/subscriptionHistoryController.js — 7 + 6. js/historyView.js — 4 + 7. js/ui/components/DeleteModal.js — 3 + 8. js/ui/components/ShareNostrModal.js — 3 + 9. js/ui/dm/DMSettingsModalController.js — 3 + 10. js/ui/loginModalController.js — 3 + +* Lint failures: 0 +* Skipped checks: 1 + * lint:assets: Missing dist/asset-manifest.json. + +**High-priority items** + +* None. Keep it up! + +**Artifacts** + +* file-size-report.json +* innerhtml-report.json +* lint-report.json +* raw logs diff --git a/docs/agents/AGENT_TASK_LOG.csv b/docs/agents/AGENT_TASK_LOG.csv index 5f9a889e..fd373fc7 100644 --- a/docs/agents/AGENT_TASK_LOG.csv +++ b/docs/agents/AGENT_TASK_LOG.csv @@ -1 +1,2 @@ date,agent_name,prompt_file,status,branch,summary +2026-02-12,audit-agent,bitvid-audit-agent.md,completed,unstable,"Ran audit scripts, created initial reports" diff --git a/scripts/agent/audit-parsers/parse_file_size.py b/scripts/agent/audit-parsers/parse_file_size.py new file mode 100644 index 00000000..7d0c75dd --- /dev/null +++ b/scripts/agent/audit-parsers/parse_file_size.py @@ -0,0 +1,61 @@ +import json +import re +import sys + +def parse_file_size_log(filepath): + """ + Parses check-file-size.mjs raw output. + """ + try: + with open(filepath, 'r') as f: + content = f.read() + except FileNotFoundError: + print(f"Error: File not found: {filepath}", file=sys.stderr) + return {} + + metrics = { + "grandfathered_files": 0, + "grandfathered_excess_lines": 0, + "new_oversized_files": 0, + "new_excess_lines": 0, + "grandfathered_list": [], + "new_list": [], + } + + # Match "grandfathered" lines + # Example: " ⚠ grandfathered: js/adminListStore.js (1033 lines)" + grandfathered_matches = re.findall(r"⚠ grandfathered: (.+?) \((\d+) lines\)", content) + for path, lines in grandfathered_matches: + metrics["grandfathered_files"] += 1 + excess = int(lines) - 1000 # Default threshold + metrics["grandfathered_excess_lines"] += excess + metrics["grandfathered_list"].append({"path": path, "lines": int(lines), "excess": excess}) + + # Match "violations" (new files) + # Example: " ✗ NEW: js/newfile.js (1200 lines, threshold 1000)" + new_matches = re.findall(r"✗ NEW: (.+?) \((\d+) lines, threshold (\d+)\)", content) + for path, lines, threshold in new_matches: + metrics["new_oversized_files"] += 1 + excess = int(lines) - int(threshold) + metrics["new_excess_lines"] += excess + metrics["new_list"].append({"path": path, "lines": int(lines), "excess": excess}) + + # Match "GREW" violations (grandfathered growing) + # Example: " ✗ GREW: js/file.js (1200 lines, was 1100, limit 1150)" + grew_matches = re.findall(r"✗ GREW: (.+?) \((\d+) lines, was (\d+), limit (\d+)\)", content) + for path, lines, was, limit in grew_matches: + metrics["new_oversized_files"] += 1 # Count as a violation + excess = int(lines) - int(limit) # Or relative to original? Let's use diff + metrics["new_excess_lines"] += excess + metrics["new_list"].append({"path": path, "lines": int(lines), "excess": excess, "type": "GREW"}) + + return metrics + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python parse_file_size.py ") + sys.exit(1) + + filepath = sys.argv[1] + metrics = parse_file_size_log(filepath) + print(json.dumps(metrics, indent=2)) diff --git a/scripts/agent/audit-parsers/parse_innerhtml.py b/scripts/agent/audit-parsers/parse_innerhtml.py new file mode 100644 index 00000000..d524fd3f --- /dev/null +++ b/scripts/agent/audit-parsers/parse_innerhtml.py @@ -0,0 +1,57 @@ +import json +import re +import sys + +def parse_innerhtml_log(filepath): + """ + Parses check-innerhtml.mjs raw output. + """ + try: + with open(filepath, 'r') as f: + content = f.read() + except FileNotFoundError: + print(f"Error: File not found: {filepath}", file=sys.stderr) + return {} + + metrics = { + "total_assignments": 0, + "files_count": 0, + "top_offenders": [], + "violations": [] + } + + # Match total summary + # Example: "innerHTML usage: 87 assignments across 34 files" + summary_match = re.search(r"innerHTML usage: (\d+) assignments across (\d+) files", content) + if summary_match: + metrics["total_assignments"] = int(summary_match.group(1)) + metrics["files_count"] = int(summary_match.group(2)) + + # Match individual file usage + # Example: " js/channelProfile.js: 10" or " js/file.js: 5 ← NEW" + usage_matches = re.findall(r"^\s+([^\s:]+): (\d+)( ← NEW)?", content, re.MULTILINE) + for path, count, is_new in usage_matches: + metrics["top_offenders"].append({"path": path, "count": int(count), "is_new": bool(is_new)}) + + # Match violations (enforce mode output) + # Example: " ✗ js/file.js: 5 total (baseline 3, +2 new) at line(s) 10, 20" + violation_matches = re.findall(r"✗ ([^:]+): (\d+) total \(baseline (\d+), \+(\d+) new\) at line\(s\) (.+)", content) + for path, total, baseline, new, lines in violation_matches: + metrics["violations"].append({ + "path": path, + "total": int(total), + "baseline": int(baseline), + "new": int(new), + "lines": lines + }) + + return metrics + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python parse_innerhtml.py ") + sys.exit(1) + + filepath = sys.argv[1] + metrics = parse_innerhtml_log(filepath) + print(json.dumps(metrics, indent=2)) diff --git a/scripts/agent/audit-parsers/parse_lint.py b/scripts/agent/audit-parsers/parse_lint.py new file mode 100644 index 00000000..bcc04fd6 --- /dev/null +++ b/scripts/agent/audit-parsers/parse_lint.py @@ -0,0 +1,58 @@ +import json +import re +import sys + +def parse_lint_log(filepath): + """ + Parses npm run lint raw output. + """ + try: + with open(filepath, 'r') as f: + content = f.read() + except FileNotFoundError: + print(f"Error: File not found: {filepath}", file=sys.stderr) + return {} + + metrics = { + "total_failures": 0, + "files_with_errors": [], + "skipped_checks": [] + } + + # Looking for typical npm error outputs or specific linter errors. + # Since we are running `npm run lint` which chains multiple commands, failure results in `npm ERR!`. + + # Check if the process exited with error (usually indicated by npm ERR!) + if "npm ERR!" in content: + metrics["total_failures"] = 1 # At least one failure + + # Check for specific skipped checks + # Example: "[lint:assets] Missing dist/asset-manifest.json. Skipping asset reference check (build required)." + skipped_matches = re.findall(r"\[(.+?)\] (.+?) Skipping (.+)", content) + for check_name, reason, details in skipped_matches: + metrics["skipped_checks"].append({ + "check": check_name, + "reason": reason.strip(), + "details": details.strip() + }) + + # Check for stylelint errors + # Example: "css/source.css\n 10:5 × Unexpected unknown property "foo" property-no-unknown" + # This is hard to parse generally without JSON output, but we can look for "×" or "error" lines. + + # Count "problems" from eslint/stylelint if standard format + # "3 problems (3 errors, 0 warnings)" + problem_matches = re.findall(r"(\d+) problems? \((\d+) errors?, (\d+) warnings?\)", content) + for problems, errors, warnings in problem_matches: + metrics["total_failures"] += int(errors) + + return metrics + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python parse_lint.py ") + sys.exit(1) + + filepath = sys.argv[1] + metrics = parse_lint_log(filepath) + print(json.dumps(metrics, indent=2)) diff --git a/scripts/agent/audit-reporters/generate_summary.py b/scripts/agent/audit-reporters/generate_summary.py new file mode 100644 index 00000000..da423e3a --- /dev/null +++ b/scripts/agent/audit-reporters/generate_summary.py @@ -0,0 +1,99 @@ +import json +import sys +import datetime + +def load_json(filepath): + try: + with open(filepath, 'r') as f: + return json.load(f) + except FileNotFoundError: + return {} + +def generate_summary(file_size_path, innerhtml_path, lint_path, date_str): + file_size = load_json(file_size_path) + innerhtml = load_json(innerhtml_path) + lint = load_json(lint_path) + + lines = [] + lines.append(f"Title: Audit Report — {date_str} (unstable)") + lines.append("") + lines.append("**Summary**") + lines.append("") + lines.append(f"* Date: {date_str}") + lines.append("* Branch: unstable") + lines.append("") + lines.append("**Metrics**") + lines.append("") + + # File Size + gf_files = file_size.get("grandfathered_files", 0) + gf_excess = file_size.get("grandfathered_excess_lines", 0) + new_files = file_size.get("new_oversized_files", 0) + new_excess = file_size.get("new_excess_lines", 0) + + lines.append(f"* Grandfathered oversized files: {gf_files} files (total excess lines: {gf_excess})") + lines.append(f"* New oversized files: {new_files} files (total excess lines: {new_excess})") + + # InnerHTML + total_assign = innerhtml.get("total_assignments", 0) + files_count = innerhtml.get("files_count", 0) + lines.append(f"* Total innerHTML assignments: {total_assign} across {files_count} files") + lines.append("") + lines.append(" * Top offenders:") + lines.append("") + + top_offenders = innerhtml.get("top_offenders", []) + # Sort by count desc + top_offenders.sort(key=lambda x: x["count"], reverse=True) + + for i, offender in enumerate(top_offenders[:10]): + status = " (NEW)" if offender.get("is_new") else "" + lines.append(f" {i+1}. {offender['path']} — {offender['count']}{status}") + + # Lint + lint_failures = lint.get("total_failures", 0) + lint_skipped = lint.get("skipped_checks", []) + + lines.append("") + lines.append(f"* Lint failures: {lint_failures}") + if lint_skipped: + lines.append(f"* Skipped checks: {len(lint_skipped)}") + for skip in lint_skipped: + lines.append(f" * {skip['check']}: {skip['reason']}") + + lines.append("") + lines.append("**High-priority items**") + lines.append("") + + if new_files > 0: + for f in file_size.get("new_list", []): + lines.append(f"* Remove or trim oversized file `{f['path']}` (excess lines: {f['excess']})") + + violations = innerhtml.get("violations", []) + if violations: + for v in violations: + lines.append(f"* Review `{v['path']}` for new innerHTML usage (+{v['new']})") + + if lint_failures > 0: + lines.append(f"* Fix lint errors (see logs)") + + if not (new_files > 0 or violations or lint_failures > 0): + lines.append("* None. Keep it up!") + + lines.append("") + lines.append("**Artifacts**") + lines.append("") + lines.append("* file-size-report.json") + lines.append("* innerhtml-report.json") + lines.append("* lint-report.json") + lines.append("* raw logs") + + return "\n".join(lines) + +if __name__ == "__main__": + if len(sys.argv) < 5: + print("Usage: python generate_summary.py ") + sys.exit(1) + + summary = generate_summary(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4]) + print(summary)