{"id":"dca6050b-4cb8-43d4-8362-1432f903fe09","entityType":"agent","slug":"clawhub-athola-nm-pensive-performance-review","name":"performance-review","canonicalUrl":"https://www.xpersona.co/agent/clawhub-athola-nm-pensive-performance-review","canonicalPath":"/agent/clawhub-athola-nm-pensive-performance-review","generatedAt":"2026-10-11T21:00:00.305Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-11T17:49:39.875Z","emptyReason":null},"description":"Detects time and space complexity hotspots via AST scan Skill: performance-review Owner: athola Summary: Detects time and space complexity hotspots via AST scan Tags: latest:1.9.19 Version history: v1.9.19 | 2026-08-26T13:19:06.422Z | user Release v1.9.19 v1.9.17 | 2026-07-30T05:39:18.228Z | user Release v1.9.17 v1.9.16 | 2026-07-14T19:56:01.066Z | user Release v1.9.16 v1.9.14 | 2026-06-30T18:04:19.616Z | user Release v1.9.14 v1.9.13 | 2026-06-27T16:22:19.651Z | user Rele","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1K downloads reported by the source. Last updated 10/11/2026.","installCommand":"clawhub skill install s17emme0e2m3cpf7k2jvp3a84984b8z9:nm-pensive-performance-review","sourceUrl":"https://clawhub.ai/athola/nm-pensive-performance-review","homepage":"https://clawhub.ai/athola/skills/nm-pensive-performance-review","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/athola/nm-pensive-performance-review","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/athola/skills/nm-pensive-performance-review","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":60,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Detects time and space complexity hotspots via AST scan Skill: performance-review Owner: athola Summary: Detects time and space complexity hotspots via AST scan"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-11T17:49:39.875Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T17:49:39.875Z","emptyReason":null},"stars":null,"forks":null,"downloads":1015,"packageName":null,"latestVersion":"1.9.19","tractionLabel":"1K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-11T17:49:39.809Z","emptyReason":null},"lastUpdatedAt":"2026-10-11T17:49:39.875Z","lastCrawledAt":"2026-10-11T17:49:39.809Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-12T17:49:39.809Z","lastVerifiedAt":null,"highlights":[{"version":"1.9.19","createdAt":"2026-08-26T13:19:06.422Z","changelog":"Release v1.9.19","fileCount":7,"zipByteSize":14569},{"version":"1.9.17","createdAt":"2026-07-30T05:39:18.228Z","changelog":"Release v1.9.17","fileCount":7,"zipByteSize":14627},{"version":"1.9.16","createdAt":"2026-07-14T19:56:01.066Z","changelog":"Release v1.9.16","fileCount":7,"zipByteSize":14415},{"version":"1.9.14","createdAt":"2026-06-30T18:04:19.616Z","changelog":"Release v1.9.14","fileCount":7,"zipByteSize":14430},{"version":"1.9.13","createdAt":"2026-06-27T16:22:19.651Z","changelog":"Release v1.9.13","fileCount":7,"zipByteSize":14543},{"version":"1.9.12","createdAt":"2026-06-19T03:17:29.995Z","changelog":"Release v1.9.12","fileCount":7,"zipByteSize":14394},{"version":"1.0.0","createdAt":"2026-06-18T15:15:09.129Z","changelog":"- Initial public release of the performance-review skill. - Detects time and space complexity hotspots via static AST scans. - Supports a tiered analysis approach: - Tier 1: Runs Python stdlib AST checks (always enabled). - Tier 2: Adds multi-language coverage via gauntlet’s tree-sitter if available. - Tier 3: Upgrades severity based on call chain analysis with gauntlet’s code graph if present. - Outputs ranked findings (HIGH, MEDIUM, LOW, CRITICAL) as ReviewFinding objects, suitable for integration with other pensive review tools. - Includes detailed documentation for usage, tier functionality, dependencies, and practical workflow.","fileCount":7,"zipByteSize":14576}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17emme0e2m3cpf7k2jvp3a84984b8z9:nm-pensive-performance-review","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-11T21:00:00.301Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-athola-nm-pensive-performance-review/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"high","updatedAt":"2026-10-11T17:49:39.875Z","emptyReason":null},"readme":"Skill: performance-review\n\nOwner: athola\n\nSummary: Detects time and space complexity hotspots via AST scan\n\nTags: latest:1.9.19\n\nVersion history:\n\nv1.9.19 | 2026-08-26T13:19:06.422Z | user\n\nRelease v1.9.19\n\nv1.9.17 | 2026-07-30T05:39:18.228Z | user\n\nRelease v1.9.17\n\nv1.9.16 | 2026-07-14T19:56:01.066Z | user\n\nRelease v1.9.16\n\nv1.9.14 | 2026-06-30T18:04:19.616Z | user\n\nRelease v1.9.14\n\nv1.9.13 | 2026-06-27T16:22:19.651Z | user\n\nRelease v1.9.13\n\nv1.9.12 | 2026-06-19T03:17:29.995Z | user\n\nRelease v1.9.12\n\nv1.0.0 | 2026-06-18T15:15:09.129Z | auto\n\n- Initial public release of the performance-review skill.\n- Detects time and space complexity hotspots via static AST scans.\n- Supports a tiered analysis approach:\n  - Tier 1: Runs Python stdlib AST checks (always enabled).\n  - Tier 2: Adds multi-language coverage via gauntlet’s tree-sitter if available.\n  - Tier 3: Upgrades severity based on call chain analysis with gauntlet’s code graph if present.\n- Outputs ranked findings (HIGH, MEDIUM, LOW, CRITICAL) as ReviewFinding objects, suitable for integration with other pensive review tools.\n- Includes detailed documentation for usage, tier functionality, dependencies, and practical workflow.\n\nArchive index:\n\nArchive v1.9.19: 7 files, 14569 bytes\n\nFiles: modules/gauntlet-integration.md (4563b), modules/kuva-visualization.md (3728b), modules/space-complexity.md (3864b), modules/time-complexity.md (4172b), skill-card.md (2411b), SKILL.md (10017b), _meta.json (149b)\n\nFile v1.9.19:SKILL.md\n\n---\nname: performance-review\ndescription: Detects time and space complexity hotspots via AST scan\nversion: 1.9.8\ntriggers:\n  - performance\n  - complexity\n  - algorithms\n  - ast\n  - static-analysis\n  - code feels slow\n  - before performance-sensitive merges\n  - or to find O(n²) regressions\nmetadata: {\"openclaw\": {\"homepage\": \"https://github.com/athola/claude-night-market/tree/master/plugins/pensive\", \"emoji\": \"\\ud83e\\udd9e\", \"requires\": {\"config\": [\"night-market.pensive:shared\"]}}}\nsource: claude-night-market\nsource_plugin: pensive\n---\n\n> **Night Market Skill** — ported from [claude-night-market/pensive](https://github.com/athola/claude-night-market/tree/master/plugins/pensive). For the full experience with agents, hooks, and commands, install the Claude Code plugin.\n\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [When to Use](#when-to-use)\n- [When NOT to Use](#when-not-to-use)\n- [Required TodoWrite Items](#required-todowrite-items)\n- [Workflow](#workflow)\n- [Tiered Analysis](#tiered-analysis)\n- [Output Format](#output-format)\n- [Cross-Plugin Dependencies](#cross-plugin-dependencies)\n- [Supporting Modules](#supporting-modules)\n\n# Performance Review\n\nStatic-analysis review of time and space complexity hotspots.\n\nThe skill runs in three escalating tiers. Tier 1 uses Python's\nstdlib `ast` and always runs. Tier 2 uses gauntlet's tree-sitter\nparser to extend detection across languages when gauntlet is\ninstalled. Tier 3 uses the gauntlet code graph to upgrade\nseverity when hotspots reach other hotspots transitively. If\ngauntlet is missing, Tiers 2 and 3 no-op and Tier 1 still\nproduces useful findings on Python source.\n\n## Quick Start\n\n```bash\n/performance-review                  # scan changed files\n/performance-review path/to/file.py  # scan one file\n/performance-review --tier 1         # force Tier 1 only\n```\n\nProgrammatic use:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nskill = PerformanceReviewSkill()\nresult = skill.analyze(context, \"src/module.py\")\nfor f in result.issues:\n    print(f\"[{f.severity}] {f.file}:{f.line} {f.message}\")\n```\n\n## When to Use\n\n- Pre-merge review of code that runs on user-scaled inputs.\n- Triage of a function that \"feels slow\" before reaching for a\n  profiler.\n- Audit a refactor for newly introduced O(n²) patterns.\n- Guardrail for AI-generated code where nested-loop hot spots\n  are common.\n\n## When NOT to Use\n\n- The target needs **runtime** measurement (memory profile, CPU\n  time on real data). Use `Skill(parseltongue:python-performance)`\n  instead: that skill drives `cProfile`, `py-spy`, and benchmarks.\n- General refactoring guidance not focused on hotspots: use\n  `Skill(pensive:code-refinement)` whose `algorithm-efficiency`\n  module covers broader optimization patterns. This skill\n  detects; that skill teaches.\n- Architecture-level performance (sharding, caching layers,\n  queue placement): use `Skill(pensive:architecture-review)`.\n\n## Required TodoWrite Items\n\n1. `perf-review:context-established`\n2. `perf-review:scan-complete`\n3. `perf-review:findings-categorized`\n4. `perf-review:integration-checked`\n5. `perf-review:report-generated`\n\n## Workflow\n\n### Step 1: Context (`perf-review:context-established`)\n\n- Identify target files. If invoked with no argument, use\n  `git diff --name-only`. If invoked with a path, scope to that.\n- Note language(s) involved. Tier 1 covers Python; non-Python\n  files need gauntlet for Tier 2 coverage.\n\n### Step 2: Tier 1 AST scan (`perf-review:scan-complete`)\n\nLoad `modules/time-complexity.md` for the time-side patterns and\n`modules/space-complexity.md` for space-side. Each module\ndocuments the AST shape of every detector.\n\nFor each Python target file, call:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nresult = PerformanceReviewSkill().analyze(context, path)\n```\n\nThe visitor walks the AST once and emits `ReviewFinding` records.\n\n### Step 3: Categorize and rank (`perf-review:findings-categorized`)\n\nGroup findings by severity:\n\n- **HIGH**: O(n²) or worse on input-sized iterables (T1, T2).\n- **MEDIUM**: Unbounded allocation or per-iteration overhead\n  (T3, T4, S1, S3).\n- **LOW**: Style-level inefficiencies (T5, T6, S2).\n- **CRITICAL**: Reserved for Tier-3 transitive upgrades.\n\nWithin a severity, sort by file then line. Suppress findings\nthe user has explicitly marked acceptable (TODO/comment\nmarkers) at module-load time of the target.\n\n### Step 4: Tier 2/3 enrichment (`perf-review:integration-checked`)\n\nLoad `modules/gauntlet-integration.md` for the contract.\n\nIf gauntlet is installed, run Tier 2 on non-Python files that\nwere skipped at Step 2. If a `.gauntlet/graph.db` exists in the\nworking tree, run Tier 3 to upgrade severities based on\ntransitive hotspot reachability.\n\nIf gauntlet is missing, this step is a no-op and the report\nnotes \"Tier 2/3 not available: install gauntlet for\nmulti-language and call-chain coverage.\"\n\n### Step 5: Report (`perf-review:report-generated`)\n\nEmit a markdown report:\n\n```\n## Performance Review: <target>\n\n### HIGH (<count>)\n- src/foo.py:42: Nested loop over the same iterable 'items'.\n  Suggestion: sort + two pointers, or hash-set membership.\n\n### MEDIUM (<count>)\n- ...\n\n### LOW (<count>)\n- ...\n\nTier coverage: 1 (always) | 2 (gauntlet ✓/✗) | 3 (graph ✓/✗)\n```\n\nThe report is informational. Apply fixes via\n`Skill(pensive:code-refinement)` or hand-merge.\n\n## Tiered Analysis\n\n| Tier | Source | When it runs | What it covers |\n|------|--------|--------------|----------------|\n| 1 | stdlib `ast` | Always (Python source only) | T1-T6, S1-S3 |\n| 2 | `gauntlet.treesitter_parser` | When gauntlet importable | Same patterns adapted to JS/TS, Go, Rust, Java, C/C++ |\n| 3 | `gauntlet.graph.GraphStore` | When `.gauntlet/graph.db` exists | Severity upgrade via transitive call chains |\n\n## Output Format\n\nFindings use the shared `ReviewFinding` dataclass from\n`pensive.skills.base`:\n\n```python\nReviewFinding(\n    file=\"src/module.py\",\n    line=42,\n    severity=\"HIGH\",          # LOW | MEDIUM | HIGH | CRITICAL\n    category=\"time\",          # time | space\n    message=\"Nested loop over the same iterable 'items'.\",\n    suggestion=\"Sort + two pointers, or hash-set membership.\",\n    code_snippet=\"\",\n)\n```\n\nThis shape matches every other pensive review skill, so the\nfindings can flow into `Skill(pensive:unified-review)` without\ntranslation.\n\n## Cross-Plugin Dependencies\n\n| Dependency | Required? | Effect when missing |\n|------------|-----------|---------------------|\n| `gauntlet.treesitter_parser` | Optional | Tier 2 returns []; Python coverage unchanged |\n| `gauntlet.graph.GraphStore` | Optional | Tier 3 returns []; severities are not upgraded |\n\nThe optional-import contract follows the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32` and\n`plugins/gauntlet/hooks/pr_blast_radius.py:52-56`: try-import\nto module-level sentinels, then early-return on `None` inside\neach tier helper. See `modules/gauntlet-integration.md` for the\nexact code shape.\n\n## Supporting Modules\n\n- `modules/time-complexity.md`: T1-T6 detector patterns and AST\n  shapes.\n- `modules/space-complexity.md`: S1-S3 detector patterns.\n- `modules/gauntlet-integration.md`: Tier 2/3 contract,\n  fallback semantics, examples.\n- `modules/kuva-visualization.md`: Rendering benchmark data as\n  charts with kuva (criterion, pytest-benchmark, ad-hoc tables).\n  Covers when chart evidence satisfies proof-of-work requirements.\n\n## Verification\n\nA perf-review finding is only useful if the caller can confirm it\nis real. Use this checklist before treating any finding as worth\nfixing:\n\n1. **Reproduce under a profiler.** Run `cProfile`, `py-spy`, or the\n   language-specific equivalent on the hotspot. The findings\n   pinpoint AST shapes; the profiler validates the runtime impact.\n2. **Re-run the failing benchmark.** If `benches/` exists, the\n   hotspot should show up in numbers, not just AST scans.\n3. **Compare numbers before and after the proposed fix.** The fix\n   is wrong if numbers do not move. Capture both timings as\n   evidence references like `[E1]` (before) and `[E2]` (after).\n   When 3+ data points exist, render a kuva chart and attach it\n   to the PR — see `modules/kuva-visualization.md`.\n4. **Sample two or three reported hotspots manually.** Findings can\n   be true at the AST level and false at the call-graph level\n   when callers short-circuit. Manual sampling catches that.\n\nThe `Skill(imbue:proof-of-work)` discipline applies: claims like\n\"the hotspot is fixed\" require evidence, not assertion.\n\n## Testing\n\nA test file already lives at\n`plugins/pensive/tests/skills/test_performance_review.py` covering\nthe AST-shape detectors. Two rules for changes here:\n\n- **Add a new detector with a test.** Any new T-* or S-* pattern\n  added to the modules ships with a test that has the smallest\n  AST sample exercising it.\n- **Add a regression test for any false positive removed.** When\n  the skill stops firing on a shape that used to look hot, the\n  reason should appear as a test case so the regression is\n  discoverable later.\n\nThe Iron Law applies: a new detector without a failing test first\nis a request to skip TDD on a code-analysis component, which is\nexactly the place where TDD pays off most.\n\n## Exit Criteria\n\n- [ ] A perf-review report file exists for the requested target.\n- [ ] Every finding carries a severity label and a concrete\n      suggestion the caller can act on.\n- [ ] Time-complexity (T1-T6) and space-complexity (S1-S3)\n      detectors have been run; tier coverage is reported.\n- [ ] Tier 2 (gauntlet treesitter) and Tier 3 (graph store)\n      contracts honor the optional-import sentinel: missing\n      modules return `[]` rather than raising.\n- [ ] Each new detector ships with a smallest-AST test that\n      fails before the detector exists; each removed false\n      positive ships with a regression test.\n- [ ] Findings flow into `Skill(pensive:unified-review)` without\n      translation when invoked from the unified entry point.\n\nFile v1.9.19:_meta.json\n\n{\n  \"ownerId\": \"kn7d107jg9jv602h9ytsegydq184a42s\",\n  \"slug\": \"nm-pensive-performance-review\",\n  \"version\": \"1.9.19\",\n  \"publishedAt\": 1787750346422\n}\n\nFile v1.9.19:modules/gauntlet-integration.md\n\n---\nmodule: gauntlet-integration\ndescription: Tier 2/3 contract via gauntlet tree-sitter and graph\nparent_skill: performance-review\ncategory: integration\ntags:\n- gauntlet\n- tree-sitter\n- graph\n- optional-dependency\n---\n\n# Gauntlet Integration\n\nPerformance review is a Tier-1 skill out of the box. Tiers 2 and\n3 enrich the analysis when gauntlet is installed.\n\n## Optional-import contract\n\nAt module load time, `performance_review.py` runs two\ntry-imports to module-level sentinels:\n\n```python\ntry:\n    from gauntlet.treesitter_parser import parse_file as _gt_parse\nexcept (ImportError, ModuleNotFoundError):\n    _gt_parse = None\n\ntry:\n    from gauntlet.graph import GraphStore as _GraphStore\nexcept (ImportError, ModuleNotFoundError):\n    _GraphStore = None\n```\n\nThe dual-exception catch matches the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32`. It survives the\ncase where the import fails for a reason other than the module\nbeing absent (e.g., a transitive ImportError deep inside\ngauntlet's own stack).\n\nEach tier helper checks its sentinel and early-returns:\n\n```python\ndef _tier2_findings(self, context, file_path):\n    if _gt_parse is None:\n        return []\n    ...\n\ndef _tier3_findings(self, context, existing, file_path):\n    if _GraphStore is None:\n        return []\n    ...\n```\n\nThis is the same pattern proven in\n`plugins/pensive/hooks/pr_blast_radius.py:52-56`, where\ngauntlet's blast-radius graph is consulted only when the\nplugin is installed.\n\n## Tier 2: Tree-sitter coverage\n\nWhen `_gt_parse` is set, `_tier2_findings` invokes\n`parse_file(path)` and receives `(nodes, edges)` describing the\ntarget file's AST in gauntlet's neutral graph format.\n\nLanguages currently parsed: Python, JavaScript, TypeScript, Go,\nRust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Scala (per\ngauntlet's `_EXT_TO_LANG` map).\n\nThe patterns translated to Tier 2 are the language-agnostic\nones:\n\n- T1 (nested loop over same iterable): present in every\n  imperative language.\n- T2 (membership in list): adapts to language idioms (e.g.,\n  `Array.includes` in JS, `slices.Contains` in Go).\n- S1 (append in nested loops): `arr.push(...)` in JS,\n  `append(slice, ...)` in Go.\n\nPatterns that do NOT translate (skipped at Tier 2):\n\n- T3 (`re.compile` in a loop): Python-specific call shape.\n- T6 (list comprehension passed to a reducer): Python-specific\n  syntax.\n- T4 (string `+=`): many languages have language-level string\n  builders that handle this; the cost model differs.\n\n## Tier 3: Transitive call analysis\n\nWhen both `_GraphStore` is set AND a `.gauntlet/graph.db` file\nexists in the working tree, `_tier3_findings` opens the graph\nand queries `impact_radius()` for each existing finding's\nfunction.\n\nIf a function reachable from a Tier-1/2 hotspot is itself a\nhotspot, the original finding's severity is upgraded one step:\n\n| Original | Upgraded |\n|----------|----------|\n| LOW      | MEDIUM   |\n| MEDIUM   | HIGH     |\n| HIGH     | CRITICAL |\n\nThis catches cases where the surface code looks fine but the\nhelper it calls is the actual bottleneck.\n\nThe graph file is built by gauntlet's own command:\n\n```bash\n/gauntlet-graph build .\n```\n\nWhen the graph does not exist, Tier 3 returns []. Building the\ngraph is a one-time cost; it speeds up every subsequent review.\n\n## Failure modes and fallbacks\n\n| Condition | Tier 2 | Tier 3 | User-visible effect |\n|-----------|--------|--------|---------------------|\n| gauntlet not installed | sentinel None, no-op | sentinel None, no-op | Tier 1 only; report notes the gap |\n| gauntlet installed, no graph.db | parses non-Python files | no-op (no DB) | Multi-language coverage but no transitive upgrades |\n| Both installed | full enrichment | severity upgrades active | Maximum coverage |\n\nIn every case, Tier 1 still runs. The skill never fails because\ngauntlet is missing. This is a deliberate choice: pensive must\nnot require an optional plugin to deliver core value.\n\n## Verification\n\nThe fallback contract is exercised by three tests in\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_tier2_returns_empty_when_gauntlet_missing`: stubs\n  `_gt_parse` to None and asserts `_tier2_findings` returns `[]`.\n- `test_tier3_returns_empty_when_graphstore_missing`: same for\n  `_tier3_findings`.\n- `test_full_analyze_with_gauntlet_blocked_returns_tier1_only`:\n  stubs both sentinels and asserts the full `analyze()` still\n  produces Tier-1 findings (T1 fires on a nested-loop snippet).\n\nRun them with:\n\n```bash\ncd plugins/pensive\nuv run pytest tests/skills/test_performance_review.py -v --no-cov\n```\n\nFile v1.9.19:modules/kuva-visualization.md\n\n---\nmodule: kuva-visualization\ncategory: output\ndependencies: [Bash, Read]\nestimated_tokens: 350\n---\n\n# Visualizing Performance Findings with kuva\n\n**When a performance review produces before/after benchmark data,\nrender it as a chart.** Text comparisons like \"380ms → 60ms\" are\ncorrect but hard to scan across multiple hotspots. A scatter or\nbar chart makes regressions and wins immediately visible.\n\n[kuva](https://github.com/Psy-Fer/kuva) is a Rust scientific\nplotting library (and CLI binary) that renders directly from TSV/CSV\ninput to SVG, PNG, or the terminal. Install once; pipe benchmark\ndata in without modifying project source.\n\n## Install\n\n```bash\ncargo install kuva --features cli\n```\n\n## Rendering a before/after benchmark comparison\n\n### criterion (Rust)\n\ncriterion writes per-benchmark timing samples to\n`target/criterion/<name>/new/estimates.json`. Extract the mean and\npipe to kuva:\n\n```bash\n# Collect before/after means for all criterion benchmarks\npython3 - <<'EOF'\nimport json, pathlib, sys\n\nrows = [\"benchmark\\tstage\\tns\"]\nfor est in pathlib.Path(\"target/criterion\").rglob(\"estimates.json\"):\n    bench = est.parts[-3]\n    data = json.loads(est.read_text())\n    mean_ns = data[\"mean\"][\"point_estimate\"]\n    # Distinguish before/after by tag; adjust to your workflow.\n    rows.append(f\"{bench}\\tafter\\t{mean_ns:.1f}\")\n\nprint(\"\\n\".join(rows))\nEOF | kuva bar /dev/stdin --x benchmark --y ns --color-by stage \\\n      --title \"Before vs After\" --terminal\n```\n\nFor a paired comparison where you have both runs saved:\n\n```bash\n# before.tsv and after.tsv each contain: benchmark<TAB>ns\nkuva scatter before.tsv after.tsv \\\n    --x ns --y ns --color-by stage \\\n    --title \"Hotspot timing (lower is better)\" \\\n    -o perf-comparison.svg\n```\n\n### pytest-benchmark (Python)\n\n```bash\npytest --benchmark-json=bench.json tests/\n\n# Convert to TSV\npython3 -c \"\nimport json, sys\nd = json.load(open('bench.json'))\nprint('name\\tns')\nfor b in d['benchmarks']:\n    print(b['name'] + '\\t' + str(b['stats']['mean'] * 1e9))\n\" | kuva bar /dev/stdin --x name --y ns \\\n      --title \"Benchmark means (ns)\" -o bench.svg\n```\n\n### Ad-hoc timing table\n\nIf you are capturing timings manually (e.g., from production traces\nas in the mlock war story):\n\n```tsv\nstage\tp50_ms\tp99_ms\nbefore_mlock\t180\t380\nafter_mlock\t35\t60\n```\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms \\\n    --title \"p99 barge-in latency (ms)\" -o latency.svg\n```\n\n## Terminal output (no file required)\n\nFor quick CI feedback without writing an SVG artifact, add\n`--terminal` to any kuva command. The chart renders as Unicode\nblock characters directly in the shell, visible in CI logs.\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms --terminal\n```\n\n## When to attach a chart as evidence\n\nThe `Skill(imbue:proof-of-work)` discipline requires evidence\nreferences `[E1]`/`[E2]` for before/after claims. A kuva-rendered\nSVG in the PR description or comments is a valid `[E2]` when it\nshows the post-fix benchmark result alongside the pre-fix baseline.\n\nMinimum evidence bar:\n\n| Claim | Required chart type |\n|-------|---------------------|\n| \"Latency improved by X\" | Bar or scatter with before/after |\n| \"Throughput doubled\" | Line or bar over input size range |\n| \"Memory usage flat\" | Line over time or input size |\n| \"O(n log n) vs O(n²)\" | Log-log scatter showing slope change |\n\n## When NOT to use kuva\n\n- The project already has matplotlib/plotly in its dev dependencies;\n  consistency matters more than zero-dep.\n- The hotspot is trivial (single function, clear before/after number\n  in a two-column table). Charts are for 3+ data points.\n- CI environment has no Rust toolchain and adding one is not\n  worth it; fall back to a numeric table in the PR comment.\n\nFile v1.9.19:modules/space-complexity.md\n\n---\nmodule: space-complexity\ndescription: AST patterns for space-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- space-complexity\n- memory\n- ast\n- python\n---\n\n# Space Complexity Detectors\n\nThree AST patterns that signal likely space-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## S1: Unbounded `.append()` inside nested loops (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is `ast.Attribute` named\n`append`, found while the loop stack has depth >= 2.\n\n**Why it matters**: A single-loop accumulator is bounded by the\ninput size, which is usually fine. A nested-loop accumulator\ngrows multiplicatively (n×m or n²) and is the typical \"result\nexplosion\" pattern that drives memory exhaustion.\n\n**Note**: The detector deliberately does not flag single-loop\nappends. They are common, expected, and rarely a hotspot. If\nsingle-loop accumulation becomes a problem, that is a runtime\nprofiling concern handled by\n`Skill(parseltongue:python-performance)`.\n\n**Fix**: If the consumer can iterate, yield instead of\nmaterialize:\n\n```python\ndef all_pairs(xs):\n    for x in xs:\n        for y in xs:\n            yield (x, y)  # streaming, O(1) space\n```\n\nWhen the full list is genuinely needed, document the size\nbound:\n\n```python\n# Bounded: |xs| <= 100, so output <= 10000 pairs.\nout = [(x, y) for x in xs for y in xs]\n```\n\n## S2: List wrapping a generator inside a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, where the first arg is\nitself an `ast.Call` to `list`, `dict`, `tuple`, or `set` with\nan `ast.GeneratorExp` as its first argument.\n\n**Why it matters**: `max(list(g))` allocates the full list, then\nwalks it. The wrapper is redundant: reducers accept generators\ndirectly.\n\n**Fix**:\n\n```python\n# Before\nreturn max(list(x * 2 for x in xs))\n\n# After\nreturn max(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` the wrapper is sometimes intentional (to\nforce evaluation), but it's still cheaper to let `sorted` /\n`set` consume the generator directly.\n\n## S3: Per-iteration allocation inside a loop (MEDIUM)\n\n**AST shape**: `ast.Call` inside a loop body where either:\n\n- The `func` is an `ast.Attribute` with name `copy`, or\n- The `func` is an `ast.Name` of `dict`, `list`, or `tuple`\n  with a non-comprehension first argument (the comprehension\n  case is a builder, not a copy).\n\n**Why it matters**: `base.copy()` per iteration allocates a new\ncontainer N times. If only one or two fields change per\niteration, a single allocation outside the loop with selective\nmutation costs less.\n\n**Fix**: Hoist when possible.\n\n```python\n# Before\nfor x in items:\n    snapshot = base.copy()\n    snapshot[\"key\"] = x\n    out.append(snapshot)\n\n# After (when downstream tolerates shared dict identity):\nshared = {**base}\nfor x in items:\n    shared[\"key\"] = x\n    out.append(dict(shared))  # explicit copy at the boundary\n```\n\nWhen the snapshots must be independent, keep `.copy()` but\nmove it outside the loop if possible, or use\n`copy.deepcopy` once and patch.\n\n## What is NOT in this module\n\n- **S4 (closure capture)** was scoped in the plan but deferred:\n  reliable detection requires control-flow analysis beyond\n  single-pass AST. Revisit when gauntlet's graph integration\n  matures.\n- **Numerical-stability concerns** (precision loss, overflow):\n  use `Skill(pensive:math-review)`.\n- **String-builder patterns**: covered by T4 in\n  `time-complexity.md` since the dominant cost is time\n  (quadratic concat), not space.\n\n## Test references\n\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_s1_unbounded_append_in_loop`\n- `test_s2_list_wrapping_generator_in_reducer`\n- `test_s3_copy_inside_loop`\n\nEach test feeds a synthetic snippet through the visitor and\nasserts the expected severity and line.\n\nFile v1.9.19:modules/time-complexity.md\n\n---\nmodule: time-complexity\ndescription: AST patterns for time-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- time-complexity\n- ast\n- python\n---\n\n# Time Complexity Detectors\n\nSix AST patterns that signal likely time-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## T1: Nested loop over the same iterable (HIGH)\n\n**AST shape**: `ast.For` whose `iter` is `ast.Name`, where the\nsame `Name.id` already appears in an enclosing `ast.For`'s iter\non the loop stack.\n\n**Why it matters**: `for x in items: for y in items: ...` is\nO(n²) and rarely intentional. When `items` is large, this\nbecomes the hot spot.\n\n**Fix**:\n\n- If pairwise comparison is needed, sort once and use two\n  pointers (O(n log n)).\n- If membership is needed, build a set once outside the outer\n  loop.\n- If the nested work is independent, consider\n  `itertools.product` for clarity (same complexity but signals\n  intent).\n\n## T2: List `in` lookup inside a loop (HIGH)\n\n**AST shape**: `ast.Compare` with `ast.In` op, right-hand\noperand `ast.Name`, found while the loop stack is non-empty.\n\n**Why it matters**: `if x in ys` is O(n) when `ys` is a list,\nmaking the enclosing loop O(n²). Static analysis can't prove\nthe variable's type, so the detector flags every `in <Name>`\ninside a loop with a conditional suggestion.\n\n**Fix**: If `ys` is a list and won't mutate during the loop:\n\n```python\nys_set = set(ys)\nfor x in xs:\n    if x in ys_set:  # O(1) per lookup\n        ...\n```\n\n## T3: `re.compile()` inside a loop body (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is the attribute access\n`re.compile`, found while the loop stack is non-empty.\n\n**Why it matters**: Python's regex engine caches compiled\npatterns internally, but the cache is bounded and not\nguaranteed for every pattern. Hoisting the compile is cheap\nand explicit.\n\n**Fix**:\n\n```python\n_PAT = re.compile(r\"\\d+\")\n\ndef matches(items):\n    return [s for s in items if _PAT.search(s)]\n```\n\n## T4: String `+=` accumulator in a loop (MEDIUM)\n\n**AST shape**: `ast.AugAssign` with `ast.Add` op, target an\n`ast.Name` previously bound to a string literal in the same\nfunction, occurring inside a loop.\n\n**Why it matters**: Each `+=` allocates a new string and copies\nthe prefix. For long iterations this becomes O(n²) on total\nsize.\n\n**Fix**:\n\n```python\nparts = []\nfor r in rows:\n    parts.append(\",\".join(r) + \"\\n\")\nreturn \"\".join(parts)\n```\n\n`io.StringIO` is also acceptable.\n\n## T5: Recursive function without memoization (LOW)\n\n**AST shape**: `ast.FunctionDef` (or `AsyncFunctionDef`) whose\nbody contains `ast.Call` to the function's own name, with no\n`@functools.cache`, `@functools.lru_cache`, or `@cache`\ndecorator on the def.\n\n**Why it matters**: Naive recursion (e.g., textbook\n`fib(n) = fib(n-1) + fib(n-2)`) has exponential repeat work.\nMemoization makes the same recurrence linear.\n\n**Fix**:\n\n```python\nfrom functools import lru_cache\n\n@lru_cache(maxsize=None)\ndef fib(n):\n    if n < 2:\n        return n\n    return fib(n - 1) + fib(n - 2)\n```\n\nIf the recursion is intentionally non-memoized (e.g., side\neffects on each call), suppress with a comment marker:\n\n```python\n# perf-review: intentional, side-effects on each call\ndef walk(node):\n    ...\n```\n\n## T6: List comprehension passed to a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, with first arg\n`ast.ListComp`.\n\n**Why it matters**: The list materializes the entire result in\nmemory, then the reducer walks it. A generator expression skips\nthe intermediate.\n\n**Fix**: Drop the brackets.\n\n```python\n# Before\nreturn sum([x * 2 for x in xs])\n\n# After\nreturn sum(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` / `frozenset` the materialization is\nunavoidable, so the detector still flags them but a fix is\noptional and may be cosmetic.\n\n## Test references\n\nTests for each detector are at\n`plugins/pensive/tests/skills/test_performance_review.py`. Each\ndetector is paired with at least one BDD-style scenario test\n(`test_t1_*`, `test_t2_*`, ...). New detectors should ship with\na failing test first per the Iron Law.\n\nFile v1.9.19:skill-card.md\n\n## Description:\n\nDetects time and space complexity hotspots via AST scan.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[athola](https://clawhub.ai/user/athola)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and engineers use this skill before performance-sensitive changes to scan changed or specified source files for likely time and space complexity hotspots and receive categorized findings with suggested fixes.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Static complexity findings can be misleading if treated as proof of runtime impact.\n\nMitigation: Review generated findings before acting and confirm important hotspots with profiling or benchmark evidence.\n\nRisk: The optional kuva charting workflow asks users to install an external CLI.\n\nMitigation: Pin or separately vet the kuva CLI before installing it in development or CI environments.\n\nRisk: The skill inspects changed or specified source files and may surface sensitive code context in reports.\n\nMitigation: Invoke it deliberately on intended targets and handle generated reports according to the repository's normal code-review controls.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/athola/skills/nm-pensive-performance-review)\n- [OpenClaw homepage](https://github.com/athola/claude-night-market/tree/master/plugins/pensive)\n- [Time Complexity Detectors](modules/time-complexity.md)\n- [Space Complexity Detectors](modules/space-complexity.md)\n- [Gauntlet Integration](modules/gauntlet-integration.md)\n- [Kuva Visualization](modules/kuva-visualization.md)\n- [kuva plotting library](https://github.com/Psy-Fer/kuva)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Code, Shell commands, Guidance]\n\n**Output Format:** [Markdown report with categorized findings, code snippets, and inline shell or Python commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Findings include severity, file, line, category, message, suggestion, and tier coverage.]\n\n## Skill Version(s):\n\n1.9.19 (source: ClawHub release evidence; artifact frontmatter reports 1.9.8)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.9.17: 7 files, 14627 bytes\n\nFiles: modules/gauntlet-integration.md (4563b), modules/kuva-visualization.md (3728b), modules/space-complexity.md (3864b), modules/time-complexity.md (4172b), skill-card.md (2512b), SKILL.md (10017b), _meta.json (149b)\n\nFile v1.9.17:SKILL.md\n\n---\nname: performance-review\ndescription: Detects time and space complexity hotspots via AST scan\nversion: 1.9.8\ntriggers:\n  - performance\n  - complexity\n  - algorithms\n  - ast\n  - static-analysis\n  - code feels slow\n  - before performance-sensitive merges\n  - or to find O(n²) regressions\nmetadata: {\"openclaw\": {\"homepage\": \"https://github.com/athola/claude-night-market/tree/master/plugins/pensive\", \"emoji\": \"\\ud83e\\udd9e\", \"requires\": {\"config\": [\"night-market.pensive:shared\"]}}}\nsource: claude-night-market\nsource_plugin: pensive\n---\n\n> **Night Market Skill** — ported from [claude-night-market/pensive](https://github.com/athola/claude-night-market/tree/master/plugins/pensive). For the full experience with agents, hooks, and commands, install the Claude Code plugin.\n\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [When to Use](#when-to-use)\n- [When NOT to Use](#when-not-to-use)\n- [Required TodoWrite Items](#required-todowrite-items)\n- [Workflow](#workflow)\n- [Tiered Analysis](#tiered-analysis)\n- [Output Format](#output-format)\n- [Cross-Plugin Dependencies](#cross-plugin-dependencies)\n- [Supporting Modules](#supporting-modules)\n\n# Performance Review\n\nStatic-analysis review of time and space complexity hotspots.\n\nThe skill runs in three escalating tiers. Tier 1 uses Python's\nstdlib `ast` and always runs. Tier 2 uses gauntlet's tree-sitter\nparser to extend detection across languages when gauntlet is\ninstalled. Tier 3 uses the gauntlet code graph to upgrade\nseverity when hotspots reach other hotspots transitively. If\ngauntlet is missing, Tiers 2 and 3 no-op and Tier 1 still\nproduces useful findings on Python source.\n\n## Quick Start\n\n```bash\n/performance-review                  # scan changed files\n/performance-review path/to/file.py  # scan one file\n/performance-review --tier 1         # force Tier 1 only\n```\n\nProgrammatic use:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nskill = PerformanceReviewSkill()\nresult = skill.analyze(context, \"src/module.py\")\nfor f in result.issues:\n    print(f\"[{f.severity}] {f.file}:{f.line} {f.message}\")\n```\n\n## When to Use\n\n- Pre-merge review of code that runs on user-scaled inputs.\n- Triage of a function that \"feels slow\" before reaching for a\n  profiler.\n- Audit a refactor for newly introduced O(n²) patterns.\n- Guardrail for AI-generated code where nested-loop hot spots\n  are common.\n\n## When NOT to Use\n\n- The target needs **runtime** measurement (memory profile, CPU\n  time on real data). Use `Skill(parseltongue:python-performance)`\n  instead: that skill drives `cProfile`, `py-spy`, and benchmarks.\n- General refactoring guidance not focused on hotspots: use\n  `Skill(pensive:code-refinement)` whose `algorithm-efficiency`\n  module covers broader optimization patterns. This skill\n  detects; that skill teaches.\n- Architecture-level performance (sharding, caching layers,\n  queue placement): use `Skill(pensive:architecture-review)`.\n\n## Required TodoWrite Items\n\n1. `perf-review:context-established`\n2. `perf-review:scan-complete`\n3. `perf-review:findings-categorized`\n4. `perf-review:integration-checked`\n5. `perf-review:report-generated`\n\n## Workflow\n\n### Step 1: Context (`perf-review:context-established`)\n\n- Identify target files. If invoked with no argument, use\n  `git diff --name-only`. If invoked with a path, scope to that.\n- Note language(s) involved. Tier 1 covers Python; non-Python\n  files need gauntlet for Tier 2 coverage.\n\n### Step 2: Tier 1 AST scan (`perf-review:scan-complete`)\n\nLoad `modules/time-complexity.md` for the time-side patterns and\n`modules/space-complexity.md` for space-side. Each module\ndocuments the AST shape of every detector.\n\nFor each Python target file, call:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nresult = PerformanceReviewSkill().analyze(context, path)\n```\n\nThe visitor walks the AST once and emits `ReviewFinding` records.\n\n### Step 3: Categorize and rank (`perf-review:findings-categorized`)\n\nGroup findings by severity:\n\n- **HIGH**: O(n²) or worse on input-sized iterables (T1, T2).\n- **MEDIUM**: Unbounded allocation or per-iteration overhead\n  (T3, T4, S1, S3).\n- **LOW**: Style-level inefficiencies (T5, T6, S2).\n- **CRITICAL**: Reserved for Tier-3 transitive upgrades.\n\nWithin a severity, sort by file then line. Suppress findings\nthe user has explicitly marked acceptable (TODO/comment\nmarkers) at module-load time of the target.\n\n### Step 4: Tier 2/3 enrichment (`perf-review:integration-checked`)\n\nLoad `modules/gauntlet-integration.md` for the contract.\n\nIf gauntlet is installed, run Tier 2 on non-Python files that\nwere skipped at Step 2. If a `.gauntlet/graph.db` exists in the\nworking tree, run Tier 3 to upgrade severities based on\ntransitive hotspot reachability.\n\nIf gauntlet is missing, this step is a no-op and the report\nnotes \"Tier 2/3 not available: install gauntlet for\nmulti-language and call-chain coverage.\"\n\n### Step 5: Report (`perf-review:report-generated`)\n\nEmit a markdown report:\n\n```\n## Performance Review: <target>\n\n### HIGH (<count>)\n- src/foo.py:42: Nested loop over the same iterable 'items'.\n  Suggestion: sort + two pointers, or hash-set membership.\n\n### MEDIUM (<count>)\n- ...\n\n### LOW (<count>)\n- ...\n\nTier coverage: 1 (always) | 2 (gauntlet ✓/✗) | 3 (graph ✓/✗)\n```\n\nThe report is informational. Apply fixes via\n`Skill(pensive:code-refinement)` or hand-merge.\n\n## Tiered Analysis\n\n| Tier | Source | When it runs | What it covers |\n|------|--------|--------------|----------------|\n| 1 | stdlib `ast` | Always (Python source only) | T1-T6, S1-S3 |\n| 2 | `gauntlet.treesitter_parser` | When gauntlet importable | Same patterns adapted to JS/TS, Go, Rust, Java, C/C++ |\n| 3 | `gauntlet.graph.GraphStore` | When `.gauntlet/graph.db` exists | Severity upgrade via transitive call chains |\n\n## Output Format\n\nFindings use the shared `ReviewFinding` dataclass from\n`pensive.skills.base`:\n\n```python\nReviewFinding(\n    file=\"src/module.py\",\n    line=42,\n    severity=\"HIGH\",          # LOW | MEDIUM | HIGH | CRITICAL\n    category=\"time\",          # time | space\n    message=\"Nested loop over the same iterable 'items'.\",\n    suggestion=\"Sort + two pointers, or hash-set membership.\",\n    code_snippet=\"\",\n)\n```\n\nThis shape matches every other pensive review skill, so the\nfindings can flow into `Skill(pensive:unified-review)` without\ntranslation.\n\n## Cross-Plugin Dependencies\n\n| Dependency | Required? | Effect when missing |\n|------------|-----------|---------------------|\n| `gauntlet.treesitter_parser` | Optional | Tier 2 returns []; Python coverage unchanged |\n| `gauntlet.graph.GraphStore` | Optional | Tier 3 returns []; severities are not upgraded |\n\nThe optional-import contract follows the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32` and\n`plugins/gauntlet/hooks/pr_blast_radius.py:52-56`: try-import\nto module-level sentinels, then early-return on `None` inside\neach tier helper. See `modules/gauntlet-integration.md` for the\nexact code shape.\n\n## Supporting Modules\n\n- `modules/time-complexity.md`: T1-T6 detector patterns and AST\n  shapes.\n- `modules/space-complexity.md`: S1-S3 detector patterns.\n- `modules/gauntlet-integration.md`: Tier 2/3 contract,\n  fallback semantics, examples.\n- `modules/kuva-visualization.md`: Rendering benchmark data as\n  charts with kuva (criterion, pytest-benchmark, ad-hoc tables).\n  Covers when chart evidence satisfies proof-of-work requirements.\n\n## Verification\n\nA perf-review finding is only useful if the caller can confirm it\nis real. Use this checklist before treating any finding as worth\nfixing:\n\n1. **Reproduce under a profiler.** Run `cProfile`, `py-spy`, or the\n   language-specific equivalent on the hotspot. The findings\n   pinpoint AST shapes; the profiler validates the runtime impact.\n2. **Re-run the failing benchmark.** If `benches/` exists, the\n   hotspot should show up in numbers, not just AST scans.\n3. **Compare numbers before and after the proposed fix.** The fix\n   is wrong if numbers do not move. Capture both timings as\n   evidence references like `[E1]` (before) and `[E2]` (after).\n   When 3+ data points exist, render a kuva chart and attach it\n   to the PR — see `modules/kuva-visualization.md`.\n4. **Sample two or three reported hotspots manually.** Findings can\n   be true at the AST level and false at the call-graph level\n   when callers short-circuit. Manual sampling catches that.\n\nThe `Skill(imbue:proof-of-work)` discipline applies: claims like\n\"the hotspot is fixed\" require evidence, not assertion.\n\n## Testing\n\nA test file already lives at\n`plugins/pensive/tests/skills/test_performance_review.py` covering\nthe AST-shape detectors. Two rules for changes here:\n\n- **Add a new detector with a test.** Any new T-* or S-* pattern\n  added to the modules ships with a test that has the smallest\n  AST sample exercising it.\n- **Add a regression test for any false positive removed.** When\n  the skill stops firing on a shape that used to look hot, the\n  reason should appear as a test case so the regression is\n  discoverable later.\n\nThe Iron Law applies: a new detector without a failing test first\nis a request to skip TDD on a code-analysis component, which is\nexactly the place where TDD pays off most.\n\n## Exit Criteria\n\n- [ ] A perf-review report file exists for the requested target.\n- [ ] Every finding carries a severity label and a concrete\n      suggestion the caller can act on.\n- [ ] Time-complexity (T1-T6) and space-complexity (S1-S3)\n      detectors have been run; tier coverage is reported.\n- [ ] Tier 2 (gauntlet treesitter) and Tier 3 (graph store)\n      contracts honor the optional-import sentinel: missing\n      modules return `[]` rather than raising.\n- [ ] Each new detector ships with a smallest-AST test that\n      fails before the detector exists; each removed false\n      positive ships with a regression test.\n- [ ] Findings flow into `Skill(pensive:unified-review)` without\n      translation when invoked from the unified entry point.\n\nFile v1.9.17:_meta.json\n\n{\n  \"ownerId\": \"kn7d107jg9jv602h9ytsegydq184a42s\",\n  \"slug\": \"nm-pensive-performance-review\",\n  \"version\": \"1.9.17\",\n  \"publishedAt\": 1785389958228\n}\n\nFile v1.9.17:modules/gauntlet-integration.md\n\n---\nmodule: gauntlet-integration\ndescription: Tier 2/3 contract via gauntlet tree-sitter and graph\nparent_skill: performance-review\ncategory: integration\ntags:\n- gauntlet\n- tree-sitter\n- graph\n- optional-dependency\n---\n\n# Gauntlet Integration\n\nPerformance review is a Tier-1 skill out of the box. Tiers 2 and\n3 enrich the analysis when gauntlet is installed.\n\n## Optional-import contract\n\nAt module load time, `performance_review.py` runs two\ntry-imports to module-level sentinels:\n\n```python\ntry:\n    from gauntlet.treesitter_parser import parse_file as _gt_parse\nexcept (ImportError, ModuleNotFoundError):\n    _gt_parse = None\n\ntry:\n    from gauntlet.graph import GraphStore as _GraphStore\nexcept (ImportError, ModuleNotFoundError):\n    _GraphStore = None\n```\n\nThe dual-exception catch matches the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32`. It survives the\ncase where the import fails for a reason other than the module\nbeing absent (e.g., a transitive ImportError deep inside\ngauntlet's own stack).\n\nEach tier helper checks its sentinel and early-returns:\n\n```python\ndef _tier2_findings(self, context, file_path):\n    if _gt_parse is None:\n        return []\n    ...\n\ndef _tier3_findings(self, context, existing, file_path):\n    if _GraphStore is None:\n        return []\n    ...\n```\n\nThis is the same pattern proven in\n`plugins/pensive/hooks/pr_blast_radius.py:52-56`, where\ngauntlet's blast-radius graph is consulted only when the\nplugin is installed.\n\n## Tier 2: Tree-sitter coverage\n\nWhen `_gt_parse` is set, `_tier2_findings` invokes\n`parse_file(path)` and receives `(nodes, edges)` describing the\ntarget file's AST in gauntlet's neutral graph format.\n\nLanguages currently parsed: Python, JavaScript, TypeScript, Go,\nRust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Scala (per\ngauntlet's `_EXT_TO_LANG` map).\n\nThe patterns translated to Tier 2 are the language-agnostic\nones:\n\n- T1 (nested loop over same iterable): present in every\n  imperative language.\n- T2 (membership in list): adapts to language idioms (e.g.,\n  `Array.includes` in JS, `slices.Contains` in Go).\n- S1 (append in nested loops): `arr.push(...)` in JS,\n  `append(slice, ...)` in Go.\n\nPatterns that do NOT translate (skipped at Tier 2):\n\n- T3 (`re.compile` in a loop): Python-specific call shape.\n- T6 (list comprehension passed to a reducer): Python-specific\n  syntax.\n- T4 (string `+=`): many languages have language-level string\n  builders that handle this; the cost model differs.\n\n## Tier 3: Transitive call analysis\n\nWhen both `_GraphStore` is set AND a `.gauntlet/graph.db` file\nexists in the working tree, `_tier3_findings` opens the graph\nand queries `impact_radius()` for each existing finding's\nfunction.\n\nIf a function reachable from a Tier-1/2 hotspot is itself a\nhotspot, the original finding's severity is upgraded one step:\n\n| Original | Upgraded |\n|----------|----------|\n| LOW      | MEDIUM   |\n| MEDIUM   | HIGH     |\n| HIGH     | CRITICAL |\n\nThis catches cases where the surface code looks fine but the\nhelper it calls is the actual bottleneck.\n\nThe graph file is built by gauntlet's own command:\n\n```bash\n/gauntlet-graph build .\n```\n\nWhen the graph does not exist, Tier 3 returns []. Building the\ngraph is a one-time cost; it speeds up every subsequent review.\n\n## Failure modes and fallbacks\n\n| Condition | Tier 2 | Tier 3 | User-visible effect |\n|-----------|--------|--------|---------------------|\n| gauntlet not installed | sentinel None, no-op | sentinel None, no-op | Tier 1 only; report notes the gap |\n| gauntlet installed, no graph.db | parses non-Python files | no-op (no DB) | Multi-language coverage but no transitive upgrades |\n| Both installed | full enrichment | severity upgrades active | Maximum coverage |\n\nIn every case, Tier 1 still runs. The skill never fails because\ngauntlet is missing. This is a deliberate choice: pensive must\nnot require an optional plugin to deliver core value.\n\n## Verification\n\nThe fallback contract is exercised by three tests in\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_tier2_returns_empty_when_gauntlet_missing`: stubs\n  `_gt_parse` to None and asserts `_tier2_findings` returns `[]`.\n- `test_tier3_returns_empty_when_graphstore_missing`: same for\n  `_tier3_findings`.\n- `test_full_analyze_with_gauntlet_blocked_returns_tier1_only`:\n  stubs both sentinels and asserts the full `analyze()` still\n  produces Tier-1 findings (T1 fires on a nested-loop snippet).\n\nRun them with:\n\n```bash\ncd plugins/pensive\nuv run pytest tests/skills/test_performance_review.py -v --no-cov\n```\n\nFile v1.9.17:modules/kuva-visualization.md\n\n---\nmodule: kuva-visualization\ncategory: output\ndependencies: [Bash, Read]\nestimated_tokens: 350\n---\n\n# Visualizing Performance Findings with kuva\n\n**When a performance review produces before/after benchmark data,\nrender it as a chart.** Text comparisons like \"380ms → 60ms\" are\ncorrect but hard to scan across multiple hotspots. A scatter or\nbar chart makes regressions and wins immediately visible.\n\n[kuva](https://github.com/Psy-Fer/kuva) is a Rust scientific\nplotting library (and CLI binary) that renders directly from TSV/CSV\ninput to SVG, PNG, or the terminal. Install once; pipe benchmark\ndata in without modifying project source.\n\n## Install\n\n```bash\ncargo install kuva --features cli\n```\n\n## Rendering a before/after benchmark comparison\n\n### criterion (Rust)\n\ncriterion writes per-benchmark timing samples to\n`target/criterion/<name>/new/estimates.json`. Extract the mean and\npipe to kuva:\n\n```bash\n# Collect before/after means for all criterion benchmarks\npython3 - <<'EOF'\nimport json, pathlib, sys\n\nrows = [\"benchmark\\tstage\\tns\"]\nfor est in pathlib.Path(\"target/criterion\").rglob(\"estimates.json\"):\n    bench = est.parts[-3]\n    data = json.loads(est.read_text())\n    mean_ns = data[\"mean\"][\"point_estimate\"]\n    # Distinguish before/after by tag; adjust to your workflow.\n    rows.append(f\"{bench}\\tafter\\t{mean_ns:.1f}\")\n\nprint(\"\\n\".join(rows))\nEOF | kuva bar /dev/stdin --x benchmark --y ns --color-by stage \\\n      --title \"Before vs After\" --terminal\n```\n\nFor a paired comparison where you have both runs saved:\n\n```bash\n# before.tsv and after.tsv each contain: benchmark<TAB>ns\nkuva scatter before.tsv after.tsv \\\n    --x ns --y ns --color-by stage \\\n    --title \"Hotspot timing (lower is better)\" \\\n    -o perf-comparison.svg\n```\n\n### pytest-benchmark (Python)\n\n```bash\npytest --benchmark-json=bench.json tests/\n\n# Convert to TSV\npython3 -c \"\nimport json, sys\nd = json.load(open('bench.json'))\nprint('name\\tns')\nfor b in d['benchmarks']:\n    print(b['name'] + '\\t' + str(b['stats']['mean'] * 1e9))\n\" | kuva bar /dev/stdin --x name --y ns \\\n      --title \"Benchmark means (ns)\" -o bench.svg\n```\n\n### Ad-hoc timing table\n\nIf you are capturing timings manually (e.g., from production traces\nas in the mlock war story):\n\n```tsv\nstage\tp50_ms\tp99_ms\nbefore_mlock\t180\t380\nafter_mlock\t35\t60\n```\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms \\\n    --title \"p99 barge-in latency (ms)\" -o latency.svg\n```\n\n## Terminal output (no file required)\n\nFor quick CI feedback without writing an SVG artifact, add\n`--terminal` to any kuva command. The chart renders as Unicode\nblock characters directly in the shell, visible in CI logs.\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms --terminal\n```\n\n## When to attach a chart as evidence\n\nThe `Skill(imbue:proof-of-work)` discipline requires evidence\nreferences `[E1]`/`[E2]` for before/after claims. A kuva-rendered\nSVG in the PR description or comments is a valid `[E2]` when it\nshows the post-fix benchmark result alongside the pre-fix baseline.\n\nMinimum evidence bar:\n\n| Claim | Required chart type |\n|-------|---------------------|\n| \"Latency improved by X\" | Bar or scatter with before/after |\n| \"Throughput doubled\" | Line or bar over input size range |\n| \"Memory usage flat\" | Line over time or input size |\n| \"O(n log n) vs O(n²)\" | Log-log scatter showing slope change |\n\n## When NOT to use kuva\n\n- The project already has matplotlib/plotly in its dev dependencies;\n  consistency matters more than zero-dep.\n- The hotspot is trivial (single function, clear before/after number\n  in a two-column table). Charts are for 3+ data points.\n- CI environment has no Rust toolchain and adding one is not\n  worth it; fall back to a numeric table in the PR comment.\n\nFile v1.9.17:modules/space-complexity.md\n\n---\nmodule: space-complexity\ndescription: AST patterns for space-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- space-complexity\n- memory\n- ast\n- python\n---\n\n# Space Complexity Detectors\n\nThree AST patterns that signal likely space-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## S1: Unbounded `.append()` inside nested loops (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is `ast.Attribute` named\n`append`, found while the loop stack has depth >= 2.\n\n**Why it matters**: A single-loop accumulator is bounded by the\ninput size, which is usually fine. A nested-loop accumulator\ngrows multiplicatively (n×m or n²) and is the typical \"result\nexplosion\" pattern that drives memory exhaustion.\n\n**Note**: The detector deliberately does not flag single-loop\nappends. They are common, expected, and rarely a hotspot. If\nsingle-loop accumulation becomes a problem, that is a runtime\nprofiling concern handled by\n`Skill(parseltongue:python-performance)`.\n\n**Fix**: If the consumer can iterate, yield instead of\nmaterialize:\n\n```python\ndef all_pairs(xs):\n    for x in xs:\n        for y in xs:\n            yield (x, y)  # streaming, O(1) space\n```\n\nWhen the full list is genuinely needed, document the size\nbound:\n\n```python\n# Bounded: |xs| <= 100, so output <= 10000 pairs.\nout = [(x, y) for x in xs for y in xs]\n```\n\n## S2: List wrapping a generator inside a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, where the first arg is\nitself an `ast.Call` to `list`, `dict`, `tuple`, or `set` with\nan `ast.GeneratorExp` as its first argument.\n\n**Why it matters**: `max(list(g))` allocates the full list, then\nwalks it. The wrapper is redundant: reducers accept generators\ndirectly.\n\n**Fix**:\n\n```python\n# Before\nreturn max(list(x * 2 for x in xs))\n\n# After\nreturn max(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` the wrapper is sometimes intentional (to\nforce evaluation), but it's still cheaper to let `sorted` /\n`set` consume the generator directly.\n\n## S3: Per-iteration allocation inside a loop (MEDIUM)\n\n**AST shape**: `ast.Call` inside a loop body where either:\n\n- The `func` is an `ast.Attribute` with name `copy`, or\n- The `func` is an `ast.Name` of `dict`, `list`, or `tuple`\n  with a non-comprehension first argument (the comprehension\n  case is a builder, not a copy).\n\n**Why it matters**: `base.copy()` per iteration allocates a new\ncontainer N times. If only one or two fields change per\niteration, a single allocation outside the loop with selective\nmutation costs less.\n\n**Fix**: Hoist when possible.\n\n```python\n# Before\nfor x in items:\n    snapshot = base.copy()\n    snapshot[\"key\"] = x\n    out.append(snapshot)\n\n# After (when downstream tolerates shared dict identity):\nshared = {**base}\nfor x in items:\n    shared[\"key\"] = x\n    out.append(dict(shared))  # explicit copy at the boundary\n```\n\nWhen the snapshots must be independent, keep `.copy()` but\nmove it outside the loop if possible, or use\n`copy.deepcopy` once and patch.\n\n## What is NOT in this module\n\n- **S4 (closure capture)** was scoped in the plan but deferred:\n  reliable detection requires control-flow analysis beyond\n  single-pass AST. Revisit when gauntlet's graph integration\n  matures.\n- **Numerical-stability concerns** (precision loss, overflow):\n  use `Skill(pensive:math-review)`.\n- **String-builder patterns**: covered by T4 in\n  `time-complexity.md` since the dominant cost is time\n  (quadratic concat), not space.\n\n## Test references\n\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_s1_unbounded_append_in_loop`\n- `test_s2_list_wrapping_generator_in_reducer`\n- `test_s3_copy_inside_loop`\n\nEach test feeds a synthetic snippet through the visitor and\nasserts the expected severity and line.\n\nFile v1.9.17:modules/time-complexity.md\n\n---\nmodule: time-complexity\ndescription: AST patterns for time-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- time-complexity\n- ast\n- python\n---\n\n# Time Complexity Detectors\n\nSix AST patterns that signal likely time-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## T1: Nested loop over the same iterable (HIGH)\n\n**AST shape**: `ast.For` whose `iter` is `ast.Name`, where the\nsame `Name.id` already appears in an enclosing `ast.For`'s iter\non the loop stack.\n\n**Why it matters**: `for x in items: for y in items: ...` is\nO(n²) and rarely intentional. When `items` is large, this\nbecomes the hot spot.\n\n**Fix**:\n\n- If pairwise comparison is needed, sort once and use two\n  pointers (O(n log n)).\n- If membership is needed, build a set once outside the outer\n  loop.\n- If the nested work is independent, consider\n  `itertools.product` for clarity (same complexity but signals\n  intent).\n\n## T2: List `in` lookup inside a loop (HIGH)\n\n**AST shape**: `ast.Compare` with `ast.In` op, right-hand\noperand `ast.Name`, found while the loop stack is non-empty.\n\n**Why it matters**: `if x in ys` is O(n) when `ys` is a list,\nmaking the enclosing loop O(n²). Static analysis can't prove\nthe variable's type, so the detector flags every `in <Name>`\ninside a loop with a conditional suggestion.\n\n**Fix**: If `ys` is a list and won't mutate during the loop:\n\n```python\nys_set = set(ys)\nfor x in xs:\n    if x in ys_set:  # O(1) per lookup\n        ...\n```\n\n## T3: `re.compile()` inside a loop body (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is the attribute access\n`re.compile`, found while the loop stack is non-empty.\n\n**Why it matters**: Python's regex engine caches compiled\npatterns internally, but the cache is bounded and not\nguaranteed for every pattern. Hoisting the compile is cheap\nand explicit.\n\n**Fix**:\n\n```python\n_PAT = re.compile(r\"\\d+\")\n\ndef matches(items):\n    return [s for s in items if _PAT.search(s)]\n```\n\n## T4: String `+=` accumulator in a loop (MEDIUM)\n\n**AST shape**: `ast.AugAssign` with `ast.Add` op, target an\n`ast.Name` previously bound to a string literal in the same\nfunction, occurring inside a loop.\n\n**Why it matters**: Each `+=` allocates a new string and copies\nthe prefix. For long iterations this becomes O(n²) on total\nsize.\n\n**Fix**:\n\n```python\nparts = []\nfor r in rows:\n    parts.append(\",\".join(r) + \"\\n\")\nreturn \"\".join(parts)\n```\n\n`io.StringIO` is also acceptable.\n\n## T5: Recursive function without memoization (LOW)\n\n**AST shape**: `ast.FunctionDef` (or `AsyncFunctionDef`) whose\nbody contains `ast.Call` to the function's own name, with no\n`@functools.cache`, `@functools.lru_cache`, or `@cache`\ndecorator on the def.\n\n**Why it matters**: Naive recursion (e.g., textbook\n`fib(n) = fib(n-1) + fib(n-2)`) has exponential repeat work.\nMemoization makes the same recurrence linear.\n\n**Fix**:\n\n```python\nfrom functools import lru_cache\n\n@lru_cache(maxsize=None)\ndef fib(n):\n    if n < 2:\n        return n\n    return fib(n - 1) + fib(n - 2)\n```\n\nIf the recursion is intentionally non-memoized (e.g., side\neffects on each call), suppress with a comment marker:\n\n```python\n# perf-review: intentional, side-effects on each call\ndef walk(node):\n    ...\n```\n\n## T6: List comprehension passed to a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, with first arg\n`ast.ListComp`.\n\n**Why it matters**: The list materializes the entire result in\nmemory, then the reducer walks it. A generator expression skips\nthe intermediate.\n\n**Fix**: Drop the brackets.\n\n```python\n# Before\nreturn sum([x * 2 for x in xs])\n\n# After\nreturn sum(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` / `frozenset` the materialization is\nunavoidable, so the detector still flags them but a fix is\noptional and may be cosmetic.\n\n## Test references\n\nTests for each detector are at\n`plugins/pensive/tests/skills/test_performance_review.py`. Each\ndetector is paired with at least one BDD-style scenario test\n(`test_t1_*`, `test_t2_*`, ...). New detectors should ship with\na failing test first per the Iron Law.\n\nFile v1.9.17:skill-card.md\n\n## Description: <br>\nDetects time and space complexity hotspots via AST scan. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[athola](https://clawhub.ai/user/athola) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineers use this skill before performance-sensitive merges or during triage to identify likely time and space complexity hotspots in code. It produces severity-ranked findings and suggestions that should be confirmed with profiling or benchmarks before changes are treated as proven fixes. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Static analysis findings can be false positives or may not represent a real runtime bottleneck. <br>\nMitigation: Confirm important findings with profiling, benchmarks, and manual sampling before treating a proposed fix as proven. <br>\nRisk: Optional gauntlet and kuva integrations broaden the tool surface used during analysis and reporting. <br>\nMitigation: Review and install those separate tools only when multi-language, call-graph, or charting support is needed. <br>\nRisk: The skill reads target code supplied for review. <br>\nMitigation: Use it only on code the agent is authorized to inspect and follow local handling rules for sensitive repositories. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/athola/skills/nm-pensive-performance-review) <br>\n- [Pensive homepage from ClawHub metadata](https://github.com/athola/claude-night-market/tree/master/plugins/pensive) <br>\n- [kuva plotting library](https://github.com/Psy-Fer/kuva) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [text, markdown, code, shell commands, guidance] <br>\n**Output Format:** [Markdown report with severity-ranked findings, concrete suggestions, and optional code or shell snippets for verification and charting.] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Reports Tier 1 Python AST coverage by default; optional gauntlet support can add multi-language and call-graph enrichment when available.] <br>\n\n## Skill Version(s): <br>\n1.9.17 (source: server release metadata; artifact frontmatter reports 1.9.8) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.9.16: 7 files, 14415 bytes\n\nFiles: modules/gauntlet-integration.md (4563b), modules/kuva-visualization.md (3728b), modules/space-complexity.md (3864b), modules/time-complexity.md (4172b), skill-card.md (2095b), SKILL.md (10017b), _meta.json (149b)\n\nFile v1.9.16:SKILL.md\n\n---\nname: performance-review\ndescription: Detects time and space complexity hotspots via AST scan\nversion: 1.9.8\ntriggers:\n  - performance\n  - complexity\n  - algorithms\n  - ast\n  - static-analysis\n  - code feels slow\n  - before performance-sensitive merges\n  - or to find O(n²) regressions\nmetadata: {\"openclaw\": {\"homepage\": \"https://github.com/athola/claude-night-market/tree/master/plugins/pensive\", \"emoji\": \"\\ud83e\\udd9e\", \"requires\": {\"config\": [\"night-market.pensive:shared\"]}}}\nsource: claude-night-market\nsource_plugin: pensive\n---\n\n> **Night Market Skill** — ported from [claude-night-market/pensive](https://github.com/athola/claude-night-market/tree/master/plugins/pensive). For the full experience with agents, hooks, and commands, install the Claude Code plugin.\n\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [When to Use](#when-to-use)\n- [When NOT to Use](#when-not-to-use)\n- [Required TodoWrite Items](#required-todowrite-items)\n- [Workflow](#workflow)\n- [Tiered Analysis](#tiered-analysis)\n- [Output Format](#output-format)\n- [Cross-Plugin Dependencies](#cross-plugin-dependencies)\n- [Supporting Modules](#supporting-modules)\n\n# Performance Review\n\nStatic-analysis review of time and space complexity hotspots.\n\nThe skill runs in three escalating tiers. Tier 1 uses Python's\nstdlib `ast` and always runs. Tier 2 uses gauntlet's tree-sitter\nparser to extend detection across languages when gauntlet is\ninstalled. Tier 3 uses the gauntlet code graph to upgrade\nseverity when hotspots reach other hotspots transitively. If\ngauntlet is missing, Tiers 2 and 3 no-op and Tier 1 still\nproduces useful findings on Python source.\n\n## Quick Start\n\n```bash\n/performance-review                  # scan changed files\n/performance-review path/to/file.py  # scan one file\n/performance-review --tier 1         # force Tier 1 only\n```\n\nProgrammatic use:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nskill = PerformanceReviewSkill()\nresult = skill.analyze(context, \"src/module.py\")\nfor f in result.issues:\n    print(f\"[{f.severity}] {f.file}:{f.line} {f.message}\")\n```\n\n## When to Use\n\n- Pre-merge review of code that runs on user-scaled inputs.\n- Triage of a function that \"feels slow\" before reaching for a\n  profiler.\n- Audit a refactor for newly introduced O(n²) patterns.\n- Guardrail for AI-generated code where nested-loop hot spots\n  are common.\n\n## When NOT to Use\n\n- The target needs **runtime** measurement (memory profile, CPU\n  time on real data). Use `Skill(parseltongue:python-performance)`\n  instead: that skill drives `cProfile`, `py-spy`, and benchmarks.\n- General refactoring guidance not focused on hotspots: use\n  `Skill(pensive:code-refinement)` whose `algorithm-efficiency`\n  module covers broader optimization patterns. This skill\n  detects; that skill teaches.\n- Architecture-level performance (sharding, caching layers,\n  queue placement): use `Skill(pensive:architecture-review)`.\n\n## Required TodoWrite Items\n\n1. `perf-review:context-established`\n2. `perf-review:scan-complete`\n3. `perf-review:findings-categorized`\n4. `perf-review:integration-checked`\n5. `perf-review:report-generated`\n\n## Workflow\n\n### Step 1: Context (`perf-review:context-established`)\n\n- Identify target files. If invoked with no argument, use\n  `git diff --name-only`. If invoked with a path, scope to that.\n- Note language(s) involved. Tier 1 covers Python; non-Python\n  files need gauntlet for Tier 2 coverage.\n\n### Step 2: Tier 1 AST scan (`perf-review:scan-complete`)\n\nLoad `modules/time-complexity.md` for the time-side patterns and\n`modules/space-complexity.md` for space-side. Each module\ndocuments the AST shape of every detector.\n\nFor each Python target file, call:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nresult = PerformanceReviewSkill().analyze(context, path)\n```\n\nThe visitor walks the AST once and emits `ReviewFinding` records.\n\n### Step 3: Categorize and rank (`perf-review:findings-categorized`)\n\nGroup findings by severity:\n\n- **HIGH**: O(n²) or worse on input-sized iterables (T1, T2).\n- **MEDIUM**: Unbounded allocation or per-iteration overhead\n  (T3, T4, S1, S3).\n- **LOW**: Style-level inefficiencies (T5, T6, S2).\n- **CRITICAL**: Reserved for Tier-3 transitive upgrades.\n\nWithin a severity, sort by file then line. Suppress findings\nthe user has explicitly marked acceptable (TODO/comment\nmarkers) at module-load time of the target.\n\n### Step 4: Tier 2/3 enrichment (`perf-review:integration-checked`)\n\nLoad `modules/gauntlet-integration.md` for the contract.\n\nIf gauntlet is installed, run Tier 2 on non-Python files that\nwere skipped at Step 2. If a `.gauntlet/graph.db` exists in the\nworking tree, run Tier 3 to upgrade severities based on\ntransitive hotspot reachability.\n\nIf gauntlet is missing, this step is a no-op and the report\nnotes \"Tier 2/3 not available: install gauntlet for\nmulti-language and call-chain coverage.\"\n\n### Step 5: Report (`perf-review:report-generated`)\n\nEmit a markdown report:\n\n```\n## Performance Review: <target>\n\n### HIGH (<count>)\n- src/foo.py:42: Nested loop over the same iterable 'items'.\n  Suggestion: sort + two pointers, or hash-set membership.\n\n### MEDIUM (<count>)\n- ...\n\n### LOW (<count>)\n- ...\n\nTier coverage: 1 (always) | 2 (gauntlet ✓/✗) | 3 (graph ✓/✗)\n```\n\nThe report is informational. Apply fixes via\n`Skill(pensive:code-refinement)` or hand-merge.\n\n## Tiered Analysis\n\n| Tier | Source | When it runs | What it covers |\n|------|--------|--------------|----------------|\n| 1 | stdlib `ast` | Always (Python source only) | T1-T6, S1-S3 |\n| 2 | `gauntlet.treesitter_parser` | When gauntlet importable | Same patterns adapted to JS/TS, Go, Rust, Java, C/C++ |\n| 3 | `gauntlet.graph.GraphStore` | When `.gauntlet/graph.db` exists | Severity upgrade via transitive call chains |\n\n## Output Format\n\nFindings use the shared `ReviewFinding` dataclass from\n`pensive.skills.base`:\n\n```python\nReviewFinding(\n    file=\"src/module.py\",\n    line=42,\n    severity=\"HIGH\",          # LOW | MEDIUM | HIGH | CRITICAL\n    category=\"time\",          # time | space\n    message=\"Nested loop over the same iterable 'items'.\",\n    suggestion=\"Sort + two pointers, or hash-set membership.\",\n    code_snippet=\"\",\n)\n```\n\nThis shape matches every other pensive review skill, so the\nfindings can flow into `Skill(pensive:unified-review)` without\ntranslation.\n\n## Cross-Plugin Dependencies\n\n| Dependency | Required? | Effect when missing |\n|------------|-----------|---------------------|\n| `gauntlet.treesitter_parser` | Optional | Tier 2 returns []; Python coverage unchanged |\n| `gauntlet.graph.GraphStore` | Optional | Tier 3 returns []; severities are not upgraded |\n\nThe optional-import contract follows the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32` and\n`plugins/gauntlet/hooks/pr_blast_radius.py:52-56`: try-import\nto module-level sentinels, then early-return on `None` inside\neach tier helper. See `modules/gauntlet-integration.md` for the\nexact code shape.\n\n## Supporting Modules\n\n- `modules/time-complexity.md`: T1-T6 detector patterns and AST\n  shapes.\n- `modules/space-complexity.md`: S1-S3 detector patterns.\n- `modules/gauntlet-integration.md`: Tier 2/3 contract,\n  fallback semantics, examples.\n- `modules/kuva-visualization.md`: Rendering benchmark data as\n  charts with kuva (criterion, pytest-benchmark, ad-hoc tables).\n  Covers when chart evidence satisfies proof-of-work requirements.\n\n## Verification\n\nA perf-review finding is only useful if the caller can confirm it\nis real. Use this checklist before treating any finding as worth\nfixing:\n\n1. **Reproduce under a profiler.** Run `cProfile`, `py-spy`, or the\n   language-specific equivalent on the hotspot. The findings\n   pinpoint AST shapes; the profiler validates the runtime impact.\n2. **Re-run the failing benchmark.** If `benches/` exists, the\n   hotspot should show up in numbers, not just AST scans.\n3. **Compare numbers before and after the proposed fix.** The fix\n   is wrong if numbers do not move. Capture both timings as\n   evidence references like `[E1]` (before) and `[E2]` (after).\n   When 3+ data points exist, render a kuva chart and attach it\n   to the PR — see `modules/kuva-visualization.md`.\n4. **Sample two or three reported hotspots manually.** Findings can\n   be true at the AST level and false at the call-graph level\n   when callers short-circuit. Manual sampling catches that.\n\nThe `Skill(imbue:proof-of-work)` discipline applies: claims like\n\"the hotspot is fixed\" require evidence, not assertion.\n\n## Testing\n\nA test file already lives at\n`plugins/pensive/tests/skills/test_performance_review.py` covering\nthe AST-shape detectors. Two rules for changes here:\n\n- **Add a new detector with a test.** Any new T-* or S-* pattern\n  added to the modules ships with a test that has the smallest\n  AST sample exercising it.\n- **Add a regression test for any false positive removed.** When\n  the skill stops firing on a shape that used to look hot, the\n  reason should appear as a test case so the regression is\n  discoverable later.\n\nThe Iron Law applies: a new detector without a failing test first\nis a request to skip TDD on a code-analysis component, which is\nexactly the place where TDD pays off most.\n\n## Exit Criteria\n\n- [ ] A perf-review report file exists for the requested target.\n- [ ] Every finding carries a severity label and a concrete\n      suggestion the caller can act on.\n- [ ] Time-complexity (T1-T6) and space-complexity (S1-S3)\n      detectors have been run; tier coverage is reported.\n- [ ] Tier 2 (gauntlet treesitter) and Tier 3 (graph store)\n      contracts honor the optional-import sentinel: missing\n      modules return `[]` rather than raising.\n- [ ] Each new detector ships with a smallest-AST test that\n      fails before the detector exists; each removed false\n      positive ships with a regression test.\n- [ ] Findings flow into `Skill(pensive:unified-review)` without\n      translation when invoked from the unified entry point.\n\nFile v1.9.16:_meta.json\n\n{\n  \"ownerId\": \"kn7d107jg9jv602h9ytsegydq184a42s\",\n  \"slug\": \"nm-pensive-performance-review\",\n  \"version\": \"1.9.16\",\n  \"publishedAt\": 1784058961066\n}\n\nFile v1.9.16:modules/gauntlet-integration.md\n\n---\nmodule: gauntlet-integration\ndescription: Tier 2/3 contract via gauntlet tree-sitter and graph\nparent_skill: performance-review\ncategory: integration\ntags:\n- gauntlet\n- tree-sitter\n- graph\n- optional-dependency\n---\n\n# Gauntlet Integration\n\nPerformance review is a Tier-1 skill out of the box. Tiers 2 and\n3 enrich the analysis when gauntlet is installed.\n\n## Optional-import contract\n\nAt module load time, `performance_review.py` runs two\ntry-imports to module-level sentinels:\n\n```python\ntry:\n    from gauntlet.treesitter_parser import parse_file as _gt_parse\nexcept (ImportError, ModuleNotFoundError):\n    _gt_parse = None\n\ntry:\n    from gauntlet.graph import GraphStore as _GraphStore\nexcept (ImportError, ModuleNotFoundError):\n    _GraphStore = None\n```\n\nThe dual-exception catch matches the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32`. It survives the\ncase where the import fails for a reason other than the module\nbeing absent (e.g., a transitive ImportError deep inside\ngauntlet's own stack).\n\nEach tier helper checks its sentinel and early-returns:\n\n```python\ndef _tier2_findings(self, context, file_path):\n    if _gt_parse is None:\n        return []\n    ...\n\ndef _tier3_findings(self, context, existing, file_path):\n    if _GraphStore is None:\n        return []\n    ...\n```\n\nThis is the same pattern proven in\n`plugins/pensive/hooks/pr_blast_radius.py:52-56`, where\ngauntlet's blast-radius graph is consulted only when the\nplugin is installed.\n\n## Tier 2: Tree-sitter coverage\n\nWhen `_gt_parse` is set, `_tier2_findings` invokes\n`parse_file(path)` and receives `(nodes, edges)` describing the\ntarget file's AST in gauntlet's neutral graph format.\n\nLanguages currently parsed: Python, JavaScript, TypeScript, Go,\nRust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Scala (per\ngauntlet's `_EXT_TO_LANG` map).\n\nThe patterns translated to Tier 2 are the language-agnostic\nones:\n\n- T1 (nested loop over same iterable): present in every\n  imperative language.\n- T2 (membership in list): adapts to language idioms (e.g.,\n  `Array.includes` in JS, `slices.Contains` in Go).\n- S1 (append in nested loops): `arr.push(...)` in JS,\n  `append(slice, ...)` in Go.\n\nPatterns that do NOT translate (skipped at Tier 2):\n\n- T3 (`re.compile` in a loop): Python-specific call shape.\n- T6 (list comprehension passed to a reducer): Python-specific\n  syntax.\n- T4 (string `+=`): many languages have language-level string\n  builders that handle this; the cost model differs.\n\n## Tier 3: Transitive call analysis\n\nWhen both `_GraphStore` is set AND a `.gauntlet/graph.db` file\nexists in the working tree, `_tier3_findings` opens the graph\nand queries `impact_radius()` for each existing finding's\nfunction.\n\nIf a function reachable from a Tier-1/2 hotspot is itself a\nhotspot, the original finding's severity is upgraded one step:\n\n| Original | Upgraded |\n|----------|----------|\n| LOW      | MEDIUM   |\n| MEDIUM   | HIGH     |\n| HIGH     | CRITICAL |\n\nThis catches cases where the surface code looks fine but the\nhelper it calls is the actual bottleneck.\n\nThe graph file is built by gauntlet's own command:\n\n```bash\n/gauntlet-graph build .\n```\n\nWhen the graph does not exist, Tier 3 returns []. Building the\ngraph is a one-time cost; it speeds up every subsequent review.\n\n## Failure modes and fallbacks\n\n| Condition | Tier 2 | Tier 3 | User-visible effect |\n|-----------|--------|--------|---------------------|\n| gauntlet not installed | sentinel None, no-op | sentinel None, no-op | Tier 1 only; report notes the gap |\n| gauntlet installed, no graph.db | parses non-Python files | no-op (no DB) | Multi-language coverage but no transitive upgrades |\n| Both installed | full enrichment | severity upgrades active | Maximum coverage |\n\nIn every case, Tier 1 still runs. The skill never fails because\ngauntlet is missing. This is a deliberate choice: pensive must\nnot require an optional plugin to deliver core value.\n\n## Verification\n\nThe fallback contract is exercised by three tests in\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_tier2_returns_empty_when_gauntlet_missing`: stubs\n  `_gt_parse` to None and asserts `_tier2_findings` returns `[]`.\n- `test_tier3_returns_empty_when_graphstore_missing`: same for\n  `_tier3_findings`.\n- `test_full_analyze_with_gauntlet_blocked_returns_tier1_only`:\n  stubs both sentinels and asserts the full `analyze()` still\n  produces Tier-1 findings (T1 fires on a nested-loop snippet).\n\nRun them with:\n\n```bash\ncd plugins/pensive\nuv run pytest tests/skills/test_performance_review.py -v --no-cov\n```\n\nFile v1.9.16:modules/kuva-visualization.md\n\n---\nmodule: kuva-visualization\ncategory: output\ndependencies: [Bash, Read]\nestimated_tokens: 350\n---\n\n# Visualizing Performance Findings with kuva\n\n**When a performance review produces before/after benchmark data,\nrender it as a chart.** Text comparisons like \"380ms → 60ms\" are\ncorrect but hard to scan across multiple hotspots. A scatter or\nbar chart makes regressions and wins immediately visible.\n\n[kuva](https://github.com/Psy-Fer/kuva) is a Rust scientific\nplotting library (and CLI binary) that renders directly from TSV/CSV\ninput to SVG, PNG, or the terminal. Install once; pipe benchmark\ndata in without modifying project source.\n\n## Install\n\n```bash\ncargo install kuva --features cli\n```\n\n## Rendering a before/after benchmark comparison\n\n### criterion (Rust)\n\ncriterion writes per-benchmark timing samples to\n`target/criterion/<name>/new/estimates.json`. Extract the mean and\npipe to kuva:\n\n```bash\n# Collect before/after means for all criterion benchmarks\npython3 - <<'EOF'\nimport json, pathlib, sys\n\nrows = [\"benchmark\\tstage\\tns\"]\nfor est in pathlib.Path(\"target/criterion\").rglob(\"estimates.json\"):\n    bench = est.parts[-3]\n    data = json.loads(est.read_text())\n    mean_ns = data[\"mean\"][\"point_estimate\"]\n    # Distinguish before/after by tag; adjust to your workflow.\n    rows.append(f\"{bench}\\tafter\\t{mean_ns:.1f}\")\n\nprint(\"\\n\".join(rows))\nEOF | kuva bar /dev/stdin --x benchmark --y ns --color-by stage \\\n      --title \"Before vs After\" --terminal\n```\n\nFor a paired comparison where you have both runs saved:\n\n```bash\n# before.tsv and after.tsv each contain: benchmark<TAB>ns\nkuva scatter before.tsv after.tsv \\\n    --x ns --y ns --color-by stage \\\n    --title \"Hotspot timing (lower is better)\" \\\n    -o perf-comparison.svg\n```\n\n### pytest-benchmark (Python)\n\n```bash\npytest --benchmark-json=bench.json tests/\n\n# Convert to TSV\npython3 -c \"\nimport json, sys\nd = json.load(open('bench.json'))\nprint('name\\tns')\nfor b in d['benchmarks']:\n    print(b['name'] + '\\t' + str(b['stats']['mean'] * 1e9))\n\" | kuva bar /dev/stdin --x name --y ns \\\n      --title \"Benchmark means (ns)\" -o bench.svg\n```\n\n### Ad-hoc timing table\n\nIf you are capturing timings manually (e.g., from production traces\nas in the mlock war story):\n\n```tsv\nstage\tp50_ms\tp99_ms\nbefore_mlock\t180\t380\nafter_mlock\t35\t60\n```\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms \\\n    --title \"p99 barge-in latency (ms)\" -o latency.svg\n```\n\n## Terminal output (no file required)\n\nFor quick CI feedback without writing an SVG artifact, add\n`--terminal` to any kuva command. The chart renders as Unicode\nblock characters directly in the shell, visible in CI logs.\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms --terminal\n```\n\n## When to attach a chart as evidence\n\nThe `Skill(imbue:proof-of-work)` discipline requires evidence\nreferences `[E1]`/`[E2]` for before/after claims. A kuva-rendered\nSVG in the PR description or comments is a valid `[E2]` when it\nshows the post-fix benchmark result alongside the pre-fix baseline.\n\nMinimum evidence bar:\n\n| Claim | Required chart type |\n|-------|---------------------|\n| \"Latency improved by X\" | Bar or scatter with before/after |\n| \"Throughput doubled\" | Line or bar over input size range |\n| \"Memory usage flat\" | Line over time or input size |\n| \"O(n log n) vs O(n²)\" | Log-log scatter showing slope change |\n\n## When NOT to use kuva\n\n- The project already has matplotlib/plotly in its dev dependencies;\n  consistency matters more than zero-dep.\n- The hotspot is trivial (single function, clear before/after number\n  in a two-column table). Charts are for 3+ data points.\n- CI environment has no Rust toolchain and adding one is not\n  worth it; fall back to a numeric table in the PR comment.\n\nFile v1.9.16:modules/space-complexity.md\n\n---\nmodule: space-complexity\ndescription: AST patterns for space-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- space-complexity\n- memory\n- ast\n- python\n---\n\n# Space Complexity Detectors\n\nThree AST patterns that signal likely space-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## S1: Unbounded `.append()` inside nested loops (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is `ast.Attribute` named\n`append`, found while the loop stack has depth >= 2.\n\n**Why it matters**: A single-loop accumulator is bounded by the\ninput size, which is usually fine. A nested-loop accumulator\ngrows multiplicatively (n×m or n²) and is the typical \"result\nexplosion\" pattern that drives memory exhaustion.\n\n**Note**: The detector deliberately does not flag single-loop\nappends. They are common, expected, and rarely a hotspot. If\nsingle-loop accumulation becomes a problem, that is a runtime\nprofiling concern handled by\n`Skill(parseltongue:python-performance)`.\n\n**Fix**: If the consumer can iterate, yield instead of\nmaterialize:\n\n```python\ndef all_pairs(xs):\n    for x in xs:\n        for y in xs:\n            yield (x, y)  # streaming, O(1) space\n```\n\nWhen the full list is genuinely needed, document the size\nbound:\n\n```python\n# Bounded: |xs| <= 100, so output <= 10000 pairs.\nout = [(x, y) for x in xs for y in xs]\n```\n\n## S2: List wrapping a generator inside a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, where the first arg is\nitself an `ast.Call` to `list`, `dict`, `tuple`, or `set` with\nan `ast.GeneratorExp` as its first argument.\n\n**Why it matters**: `max(list(g))` allocates the full list, then\nwalks it. The wrapper is redundant: reducers accept generators\ndirectly.\n\n**Fix**:\n\n```python\n# Before\nreturn max(list(x * 2 for x in xs))\n\n# After\nreturn max(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` the wrapper is sometimes intentional (to\nforce evaluation), but it's still cheaper to let `sorted` /\n`set` consume the generator directly.\n\n## S3: Per-iteration allocation inside a loop (MEDIUM)\n\n**AST shape**: `ast.Call` inside a loop body where either:\n\n- The `func` is an `ast.Attribute` with name `copy`, or\n- The `func` is an `ast.Name` of `dict`, `list`, or `tuple`\n  with a non-comprehension first argument (the comprehension\n  case is a builder, not a copy).\n\n**Why it matters**: `base.copy()` per iteration allocates a new\ncontainer N times. If only one or two fields change per\niteration, a single allocation outside the loop with selective\nmutation costs less.\n\n**Fix**: Hoist when possible.\n\n```python\n# Before\nfor x in items:\n    snapshot = base.copy()\n    snapshot[\"key\"] = x\n    out.append(snapshot)\n\n# After (when downstream tolerates shared dict identity):\nshared = {**base}\nfor x in items:\n    shared[\"key\"] = x\n    out.append(dict(shared))  # explicit copy at the boundary\n```\n\nWhen the snapshots must be independent, keep `.copy()` but\nmove it outside the loop if possible, or use\n`copy.deepcopy` once and patch.\n\n## What is NOT in this module\n\n- **S4 (closure capture)** was scoped in the plan but deferred:\n  reliable detection requires control-flow analysis beyond\n  single-pass AST. Revisit when gauntlet's graph integration\n  matures.\n- **Numerical-stability concerns** (precision loss, overflow):\n  use `Skill(pensive:math-review)`.\n- **String-builder patterns**: covered by T4 in\n  `time-complexity.md` since the dominant cost is time\n  (quadratic concat), not space.\n\n## Test references\n\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_s1_unbounded_append_in_loop`\n- `test_s2_list_wrapping_generator_in_reducer`\n- `test_s3_copy_inside_loop`\n\nEach test feeds a synthetic snippet through the visitor and\nasserts the expected severity and line.\n\nFile v1.9.16:modules/time-complexity.md\n\n---\nmodule: time-complexity\ndescription: AST patterns for time-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- time-complexity\n- ast\n- python\n---\n\n# Time Complexity Detectors\n\nSix AST patterns that signal likely time-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## T1: Nested loop over the same iterable (HIGH)\n\n**AST shape**: `ast.For` whose `iter` is `ast.Name`, where the\nsame `Name.id` already appears in an enclosing `ast.For`'s iter\non the loop stack.\n\n**Why it matters**: `for x in items: for y in items: ...` is\nO(n²) and rarely intentional. When `items` is large, this\nbecomes the hot spot.\n\n**Fix**:\n\n- If pairwise comparison is needed, sort once and use two\n  pointers (O(n log n)).\n- If membership is needed, build a set once outside the outer\n  loop.\n- If the nested work is independent, consider\n  `itertools.product` for clarity (same complexity but signals\n  intent).\n\n## T2: List `in` lookup inside a loop (HIGH)\n\n**AST shape**: `ast.Compare` with `ast.In` op, right-hand\noperand `ast.Name`, found while the loop stack is non-empty.\n\n**Why it matters**: `if x in ys` is O(n) when `ys` is a list,\nmaking the enclosing loop O(n²). Static analysis can't prove\nthe variable's type, so the detector flags every `in <Name>`\ninside a loop with a conditional suggestion.\n\n**Fix**: If `ys` is a list and won't mutate during the loop:\n\n```python\nys_set = set(ys)\nfor x in xs:\n    if x in ys_set:  # O(1) per lookup\n        ...\n```\n\n## T3: `re.compile()` inside a loop body (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is the attribute access\n`re.compile`, found while the loop stack is non-empty.\n\n**Why it matters**: Python's regex engine caches compiled\npatterns internally, but the cache is bounded and not\nguaranteed for every pattern. Hoisting the compile is cheap\nand explicit.\n\n**Fix**:\n\n```python\n_PAT = re.compile(r\"\\d+\")\n\ndef matches(items):\n    return [s for s in items if _PAT.search(s)]\n```\n\n## T4: String `+=` accumulator in a loop (MEDIUM)\n\n**AST shape**: `ast.AugAssign` with `ast.Add` op, target an\n`ast.Name` previously bound to a string literal in the same\nfunction, occurring inside a loop.\n\n**Why it matters**: Each `+=` allocates a new string and copies\nthe prefix. For long iterations this becomes O(n²) on total\nsize.\n\n**Fix**:\n\n```python\nparts = []\nfor r in rows:\n    parts.append(\",\".join(r) + \"\\n\")\nreturn \"\".join(parts)\n```\n\n`io.StringIO` is also acceptable.\n\n## T5: Recursive function without memoization (LOW)\n\n**AST shape**: `ast.FunctionDef` (or `AsyncFunctionDef`) whose\nbody contains `ast.Call` to the function's own name, with no\n`@functools.cache`, `@functools.lru_cache`, or `@cache`\ndecorator on the def.\n\n**Why it matters**: Naive recursion (e.g., textbook\n`fib(n) = fib(n-1) + fib(n-2)`) has exponential repeat work.\nMemoization makes the same recurrence linear.\n\n**Fix**:\n\n```python\nfrom functools import lru_cache\n\n@lru_cache(maxsize=None)\ndef fib(n):\n    if n < 2:\n        return n\n    return fib(n - 1) + fib(n - 2)\n```\n\nIf the recursion is intentionally non-memoized (e.g., side\neffects on each call), suppress with a comment marker:\n\n```python\n# perf-review: intentional, side-effects on each call\ndef walk(node):\n    ...\n```\n\n## T6: List comprehension passed to a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, with first arg\n`ast.ListComp`.\n\n**Why it matters**: The list materializes the entire result in\nmemory, then the reducer walks it. A generator expression skips\nthe intermediate.\n\n**Fix**: Drop the brackets.\n\n```python\n# Before\nreturn sum([x * 2 for x in xs])\n\n# After\nreturn sum(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` / `frozenset` the materialization is\nunavoidable, so the detector still flags them but a fix is\noptional and may be cosmetic.\n\n## Test references\n\nTests for each detector are at\n`plugins/pensive/tests/skills/test_performance_review.py`. Each\ndetector is paired with at least one BDD-style scenario test\n(`test_t1_*`, `test_t2_*`, ...). New detectors should ship with\na failing test first per the Iron Law.\n\nFile v1.9.16:skill-card.md\n\n## Description: <br>\nDetects likely time and space complexity hotspots in project code using static analysis with optional enrichment. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[athola](https://clawhub.ai/user/athola) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineers use this skill before merges or during performance triage to scan target code paths for likely time and space complexity hotspots and receive ranked findings with suggested validation steps. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Static-analysis findings may be false positives or may not reflect real runtime impact. <br>\nMitigation: Confirm important findings with profiling, benchmark reruns, or manual review before applying or claiming a performance fix. <br>\nRisk: Optional gauntlet and kuva integrations may read additional project data or require separate tool installation. <br>\nMitigation: Review optional integrations and tool installs separately in environments with network, package, or source-code access restrictions. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/athola/skills/nm-pensive-performance-review) <br>\n- [Project homepage](https://github.com/athola/claude-night-market/tree/master/plugins/pensive) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [markdown, code, shell commands, guidance] <br>\n**Output Format:** [Markdown report with inline code and shell command snippets] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Findings are informational and should be validated with profiling or benchmarks before fixes are treated as proven.] <br>\n\n## Skill Version(s): <br>\n1.9.16 (source: server release metadata) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.9.14: 7 files, 14430 bytes\n\nFiles: modules/gauntlet-integration.md (4563b), modules/kuva-visualization.md (3728b), modules/space-complexity.md (3864b), modules/time-complexity.md (4172b), skill-card.md (2125b), SKILL.md (10017b), _meta.json (149b)\n\nFile v1.9.14:SKILL.md\n\n---\nname: performance-review\ndescription: Detects time and space complexity hotspots via AST scan\nversion: 1.9.8\ntriggers:\n  - performance\n  - complexity\n  - algorithms\n  - ast\n  - static-analysis\n  - code feels slow\n  - before performance-sensitive merges\n  - or to find O(n²) regressions\nmetadata: {\"openclaw\": {\"homepage\": \"https://github.com/athola/claude-night-market/tree/master/plugins/pensive\", \"emoji\": \"\\ud83e\\udd9e\", \"requires\": {\"config\": [\"night-market.pensive:shared\"]}}}\nsource: claude-night-market\nsource_plugin: pensive\n---\n\n> **Night Market Skill** — ported from [claude-night-market/pensive](https://github.com/athola/claude-night-market/tree/master/plugins/pensive). For the full experience with agents, hooks, and commands, install the Claude Code plugin.\n\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [When to Use](#when-to-use)\n- [When NOT to Use](#when-not-to-use)\n- [Required TodoWrite Items](#required-todowrite-items)\n- [Workflow](#workflow)\n- [Tiered Analysis](#tiered-analysis)\n- [Output Format](#output-format)\n- [Cross-Plugin Dependencies](#cross-plugin-dependencies)\n- [Supporting Modules](#supporting-modules)\n\n# Performance Review\n\nStatic-analysis review of time and space complexity hotspots.\n\nThe skill runs in three escalating tiers. Tier 1 uses Python's\nstdlib `ast` and always runs. Tier 2 uses gauntlet's tree-sitter\nparser to extend detection across languages when gauntlet is\ninstalled. Tier 3 uses the gauntlet code graph to upgrade\nseverity when hotspots reach other hotspots transitively. If\ngauntlet is missing, Tiers 2 and 3 no-op and Tier 1 still\nproduces useful findings on Python source.\n\n## Quick Start\n\n```bash\n/performance-review                  # scan changed files\n/performance-review path/to/file.py  # scan one file\n/performance-review --tier 1         # force Tier 1 only\n```\n\nProgrammatic use:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nskill = PerformanceReviewSkill()\nresult = skill.analyze(context, \"src/module.py\")\nfor f in result.issues:\n    print(f\"[{f.severity}] {f.file}:{f.line} {f.message}\")\n```\n\n## When to Use\n\n- Pre-merge review of code that runs on user-scaled inputs.\n- Triage of a function that \"feels slow\" before reaching for a\n  profiler.\n- Audit a refactor for newly introduced O(n²) patterns.\n- Guardrail for AI-generated code where nested-loop hot spots\n  are common.\n\n## When NOT to Use\n\n- The target needs **runtime** measurement (memory profile, CPU\n  time on real data). Use `Skill(parseltongue:python-performance)`\n  instead: that skill drives `cProfile`, `py-spy`, and benchmarks.\n- General refactoring guidance not focused on hotspots: use\n  `Skill(pensive:code-refinement)` whose `algorithm-efficiency`\n  module covers broader optimization patterns. This skill\n  detects; that skill teaches.\n- Architecture-level performance (sharding, caching layers,\n  queue placement): use `Skill(pensive:architecture-review)`.\n\n## Required TodoWrite Items\n\n1. `perf-review:context-established`\n2. `perf-review:scan-complete`\n3. `perf-review:findings-categorized`\n4. `perf-review:integration-checked`\n5. `perf-review:report-generated`\n\n## Workflow\n\n### Step 1: Context (`perf-review:context-established`)\n\n- Identify target files. If invoked with no argument, use\n  `git diff --name-only`. If invoked with a path, scope to that.\n- Note language(s) involved. Tier 1 covers Python; non-Python\n  files need gauntlet for Tier 2 coverage.\n\n### Step 2: Tier 1 AST scan (`perf-review:scan-complete`)\n\nLoad `modules/time-complexity.md` for the time-side patterns and\n`modules/space-complexity.md` for space-side. Each module\ndocuments the AST shape of every detector.\n\nFor each Python target file, call:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nresult = PerformanceReviewSkill().analyze(context, path)\n```\n\nThe visitor walks the AST once and emits `ReviewFinding` records.\n\n### Step 3: Categorize and rank (`perf-review:findings-categorized`)\n\nGroup findings by severity:\n\n- **HIGH**: O(n²) or worse on input-sized iterables (T1, T2).\n- **MEDIUM**: Unbounded allocation or per-iteration overhead\n  (T3, T4, S1, S3).\n- **LOW**: Style-level inefficiencies (T5, T6, S2).\n- **CRITICAL**: Reserved for Tier-3 transitive upgrades.\n\nWithin a severity, sort by file then line. Suppress findings\nthe user has explicitly marked acceptable (TODO/comment\nmarkers) at module-load time of the target.\n\n### Step 4: Tier 2/3 enrichment (`perf-review:integration-checked`)\n\nLoad `modules/gauntlet-integration.md` for the contract.\n\nIf gauntlet is installed, run Tier 2 on non-Python files that\nwere skipped at Step 2. If a `.gauntlet/graph.db` exists in the\nworking tree, run Tier 3 to upgrade severities based on\ntransitive hotspot reachability.\n\nIf gauntlet is missing, this step is a no-op and the report\nnotes \"Tier 2/3 not available: install gauntlet for\nmulti-language and call-chain coverage.\"\n\n### Step 5: Report (`perf-review:report-generated`)\n\nEmit a markdown report:\n\n```\n## Performance Review: <target>\n\n### HIGH (<count>)\n- src/foo.py:42: Nested loop over the same iterable 'items'.\n  Suggestion: sort + two pointers, or hash-set membership.\n\n### MEDIUM (<count>)\n- ...\n\n### LOW (<count>)\n- ...\n\nTier coverage: 1 (always) | 2 (gauntlet ✓/✗) | 3 (graph ✓/✗)\n```\n\nThe report is informational. Apply fixes via\n`Skill(pensive:code-refinement)` or hand-merge.\n\n## Tiered Analysis\n\n| Tier | Source | When it runs | What it covers |\n|------|--------|--------------|----------------|\n| 1 | stdlib `ast` | Always (Python source only) | T1-T6, S1-S3 |\n| 2 | `gauntlet.treesitter_parser` | When gauntlet importable | Same patterns adapted to JS/TS, Go, Rust, Java, C/C++ |\n| 3 | `gauntlet.graph.GraphStore` | When `.gauntlet/graph.db` exists | Severity upgrade via transitive call chains |\n\n## Output Format\n\nFindings use the shared `ReviewFinding` dataclass from\n`pensive.skills.base`:\n\n```python\nReviewFinding(\n    file=\"src/module.py\",\n    line=42,\n    severity=\"HIGH\",          # LOW | MEDIUM | HIGH | CRITICAL\n    category=\"time\",          # time | space\n    message=\"Nested loop over the same iterable 'items'.\",\n    suggestion=\"Sort + two pointers, or hash-set membership.\",\n    code_snippet=\"\",\n)\n```\n\nThis shape matches every other pensive review skill, so the\nfindings can flow into `Skill(pensive:unified-review)` without\ntranslation.\n\n## Cross-Plugin Dependencies\n\n| Dependency | Required? | Effect when missing |\n|------------|-----------|---------------------|\n| `gauntlet.treesitter_parser` | Optional | Tier 2 returns []; Python coverage unchanged |\n| `gauntlet.graph.GraphStore` | Optional | Tier 3 returns []; severities are not upgraded |\n\nThe optional-import contract follows the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32` and\n`plugins/gauntlet/hooks/pr_blast_radius.py:52-56`: try-import\nto module-level sentinels, then early-return on `None` inside\neach tier helper. See `modules/gauntlet-integration.md` for the\nexact code shape.\n\n## Supporting Modules\n\n- `modules/time-complexity.md`: T1-T6 detector patterns and AST\n  shapes.\n- `modules/space-complexity.md`: S1-S3 detector patterns.\n- `modules/gauntlet-integration.md`: Tier 2/3 contract,\n  fallback semantics, examples.\n- `modules/kuva-visualization.md`: Rendering benchmark data as\n  charts with kuva (criterion, pytest-benchmark, ad-hoc tables).\n  Covers when chart evidence satisfies proof-of-work requirements.\n\n## Verification\n\nA perf-review finding is only useful if the caller can confirm it\nis real. Use this checklist before treating any finding as worth\nfixing:\n\n1. **Reproduce under a profiler.** Run `cProfile`, `py-spy`, or the\n   language-specific equivalent on the hotspot. The findings\n   pinpoint AST shapes; the profiler validates the runtime impact.\n2. **Re-run the failing benchmark.** If `benches/` exists, the\n   hotspot should show up in numbers, not just AST scans.\n3. **Compare numbers before and after the proposed fix.** The fix\n   is wrong if numbers do not move. Capture both timings as\n   evidence references like `[E1]` (before) and `[E2]` (after).\n   When 3+ data points exist, render a kuva chart and attach it\n   to the PR — see `modules/kuva-visualization.md`.\n4. **Sample two or three reported hotspots manually.** Findings can\n   be true at the AST level and false at the call-graph level\n   when callers short-circuit. Manual sampling catches that.\n\nThe `Skill(imbue:proof-of-work)` discipline applies: claims like\n\"the hotspot is fixed\" require evidence, not assertion.\n\n## Testing\n\nA test file already lives at\n`plugins/pensive/tests/skills/test_performance_review.py` covering\nthe AST-shape detectors. Two rules for changes here:\n\n- **Add a new detector with a test.** Any new T-* or S-* pattern\n  added to the modules ships with a test that has the smallest\n  AST sample exercising it.\n- **Add a regression test for any false positive removed.** When\n  the skill stops firing on a shape that used to look hot, the\n  reason should appear as a test case so the regression is\n  discoverable later.\n\nThe Iron Law applies: a new detector without a failing test first\nis a request to skip TDD on a code-analysis component, which is\nexactly the place where TDD pays off most.\n\n## Exit Criteria\n\n- [ ] A perf-review report file exists for the requested target.\n- [ ] Every finding carries a severity label and a concrete\n      suggestion the caller can act on.\n- [ ] Time-complexity (T1-T6) and space-complexity (S1-S3)\n      detectors have been run; tier coverage is reported.\n- [ ] Tier 2 (gauntlet treesitter) and Tier 3 (graph store)\n      contracts honor the optional-import sentinel: missing\n      modules return `[]` rather than raising.\n- [ ] Each new detector ships with a smallest-AST test that\n      fails before the detector exists; each removed false\n      positive ships with a regression test.\n- [ ] Findings flow into `Skill(pensive:unified-review)` without\n      translation when invoked from the unified entry point.\n\nFile v1.9.14:_meta.json\n\n{\n  \"ownerId\": \"kn7d107jg9jv602h9ytsegydq184a42s\",\n  \"slug\": \"nm-pensive-performance-review\",\n  \"version\": \"1.9.14\",\n  \"publishedAt\": 1782842659616\n}\n\nFile v1.9.14:modules/gauntlet-integration.md\n\n---\nmodule: gauntlet-integration\ndescription: Tier 2/3 contract via gauntlet tree-sitter and graph\nparent_skill: performance-review\ncategory: integration\ntags:\n- gauntlet\n- tree-sitter\n- graph\n- optional-dependency\n---\n\n# Gauntlet Integration\n\nPerformance review is a Tier-1 skill out of the box. Tiers 2 and\n3 enrich the analysis when gauntlet is installed.\n\n## Optional-import contract\n\nAt module load time, `performance_review.py` runs two\ntry-imports to module-level sentinels:\n\n```python\ntry:\n    from gauntlet.treesitter_parser import parse_file as _gt_parse\nexcept (ImportError, ModuleNotFoundError):\n    _gt_parse = None\n\ntry:\n    from gauntlet.graph import GraphStore as _GraphStore\nexcept (ImportError, ModuleNotFoundError):\n    _GraphStore = None\n```\n\nThe dual-exception catch matches the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32`. It survives the\ncase where the import fails for a reason other than the module\nbeing absent (e.g., a transitive ImportError deep inside\ngauntlet's own stack).\n\nEach tier helper checks its sentinel and early-returns:\n\n```python\ndef _tier2_findings(self, context, file_path):\n    if _gt_parse is None:\n        return []\n    ...\n\ndef _tier3_findings(self, context, existing, file_path):\n    if _GraphStore is None:\n        return []\n    ...\n```\n\nThis is the same pattern proven in\n`plugins/pensive/hooks/pr_blast_radius.py:52-56`, where\ngauntlet's blast-radius graph is consulted only when the\nplugin is installed.\n\n## Tier 2: Tree-sitter coverage\n\nWhen `_gt_parse` is set, `_tier2_findings` invokes\n`parse_file(path)` and receives `(nodes, edges)` describing the\ntarget file's AST in gauntlet's neutral graph format.\n\nLanguages currently parsed: Python, JavaScript, TypeScript, Go,\nRust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Scala (per\ngauntlet's `_EXT_TO_LANG` map).\n\nThe patterns translated to Tier 2 are the language-agnostic\nones:\n\n- T1 (nested loop over same iterable): present in every\n  imperative language.\n- T2 (membership in list): adapts to language idioms (e.g.,\n  `Array.includes` in JS, `slices.Contains` in Go).\n- S1 (append in nested loops): `arr.push(...)` in JS,\n  `append(slice, ...)` in Go.\n\nPatterns that do NOT translate (skipped at Tier 2):\n\n- T3 (`re.compile` in a loop): Python-specific call shape.\n- T6 (list comprehension passed to a reducer): Python-specific\n  syntax.\n- T4 (string `+=`): many languages have language-level string\n  builders that handle this; the cost model differs.\n\n## Tier 3: Transitive call analysis\n\nWhen both `_GraphStore` is set AND a `.gauntlet/graph.db` file\nexists in the working tree, `_tier3_findings` opens the graph\nand queries `impact_radius()` for each existing finding's\nfunction.\n\nIf a function reachable from a Tier-1/2 hotspot is itself a\nhotspot, the original finding's severity is upgraded one step:\n\n| Original | Upgraded |\n|----------|----------|\n| LOW      | MEDIUM   |\n| MEDIUM   | HIGH     |\n| HIGH     | CRITICAL |\n\nThis catches cases where the surface code looks fine but the\nhelper it calls is the actual bottleneck.\n\nThe graph file is built by gauntlet's own command:\n\n```bash\n/gauntlet-graph build .\n```\n\nWhen the graph does not exist, Tier 3 returns []. Building the\ngraph is a one-time cost; it speeds up every subsequent review.\n\n## Failure modes and fallbacks\n\n| Condition | Tier 2 | Tier 3 | User-visible effect |\n|-----------|--------|--------|---------------------|\n| gauntlet not installed | sentinel None, no-op | sentinel None, no-op | Tier 1 only; report notes the gap |\n| gauntlet installed, no graph.db | parses non-Python files | no-op (no DB) | Multi-language coverage but no transitive upgrades |\n| Both installed | full enrichment | severity upgrades active | Maximum coverage |\n\nIn every case, Tier 1 still runs. The skill never fails because\ngauntlet is missing. This is a deliberate choice: pensive must\nnot require an optional plugin to deliver core value.\n\n## Verification\n\nThe fallback contract is exercised by three tests in\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_tier2_returns_empty_when_gauntlet_missing`: stubs\n  `_gt_parse` to None and asserts `_tier2_findings` returns `[]`.\n- `test_tier3_returns_empty_when_graphstore_missing`: same for\n  `_tier3_findings`.\n- `test_full_analyze_with_gauntlet_blocked_returns_tier1_only`:\n  stubs both sentinels and asserts the full `analyze()` still\n  produces Tier-1 findings (T1 fires on a nested-loop snippet).\n\nRun them with:\n\n```bash\ncd plugins/pensive\nuv run pytest tests/skills/test_performance_review.py -v --no-cov\n```\n\nFile v1.9.14:modules/kuva-visualization.md\n\n---\nmodule: kuva-visualization\ncategory: output\ndependencies: [Bash, Read]\nestimated_tokens: 350\n---\n\n# Visualizing Performance Findings with kuva\n\n**When a performance review produces before/after benchmark data,\nrender it as a chart.** Text comparisons like \"380ms → 60ms\" are\ncorrect but hard to scan across multiple hotspots. A scatter or\nbar chart makes regressions and wins immediately visible.\n\n[kuva](https://github.com/Psy-Fer/kuva) is a Rust scientific\nplotting library (and CLI binary) that renders directly from TSV/CSV\ninput to SVG, PNG, or the terminal. Install once; pipe benchmark\ndata in without modifying project source.\n\n## Install\n\n```bash\ncargo install kuva --features cli\n```\n\n## Rendering a before/after benchmark comparison\n\n### criterion (Rust)\n\ncriterion writes per-benchmark timing samples to\n`target/criterion/<name>/new/estimates.json`. Extract the mean and\npipe to kuva:\n\n```bash\n# Collect before/after means for all criterion benchmarks\npython3 - <<'EOF'\nimport json, pathlib, sys\n\nrows = [\"benchmark\\tstage\\tns\"]\nfor est in pathlib.Path(\"target/criterion\").rglob(\"estimates.json\"):\n    bench = est.parts[-3]\n    data = json.loads(est.read_text())\n    mean_ns = data[\"mean\"][\"point_estimate\"]\n    # Distinguish before/after by tag; adjust to your workflow.\n    rows.append(f\"{bench}\\tafter\\t{mean_ns:.1f}\")\n\nprint(\"\\n\".join(rows))\nEOF | kuva bar /dev/stdin --x benchmark --y ns --color-by stage \\\n      --title \"Before vs After\" --terminal\n```\n\nFor a paired comparison where you have both runs saved:\n\n```bash\n# before.tsv and after.tsv each contain: benchmark<TAB>ns\nkuva scatter before.tsv after.tsv \\\n    --x ns --y ns --color-by stage \\\n    --title \"Hotspot timing (lower is better)\" \\\n    -o perf-comparison.svg\n```\n\n### pytest-benchmark (Python)\n\n```bash\npytest --benchmark-json=bench.json tests/\n\n# Convert to TSV\npython3 -c \"\nimport json, sys\nd = json.load(open('bench.json'))\nprint('name\\tns')\nfor b in d['benchmarks']:\n    print(b['name'] + '\\t' + str(b['stats']['mean'] * 1e9))\n\" | kuva bar /dev/stdin --x name --y ns \\\n      --title \"Benchmark means (ns)\" -o bench.svg\n```\n\n### Ad-hoc timing table\n\nIf you are capturing timings manually (e.g., from production traces\nas in the mlock war story):\n\n```tsv\nstage\tp50_ms\tp99_ms\nbefore_mlock\t180\t380\nafter_mlock\t35\t60\n```\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms \\\n    --title \"p99 barge-in latency (ms)\" -o latency.svg\n```\n\n## Terminal output (no file required)\n\nFor quick CI feedback without writing an SVG artifact, add\n`--terminal` to any kuva command. The chart renders as Unicode\nblock characters directly in the shell, visible in CI logs.\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms --terminal\n```\n\n## When to attach a chart as evidence\n\nThe `Skill(imbue:proof-of-work)` discipline requires evidence\nreferences `[E1]`/`[E2]` for before/after claims. A kuva-rendered\nSVG in the PR description or comments is a valid `[E2]` when it\nshows the post-fix benchmark result alongside the pre-fix baseline.\n\nMinimum evidence bar:\n\n| Claim | Required chart type |\n|-------|---------------------|\n| \"Latency improved by X\" | Bar or scatter with before/after |\n| \"Throughput doubled\" | Line or bar over input size range |\n| \"Memory usage flat\" | Line over time or input size |\n| \"O(n log n) vs O(n²)\" | Log-log scatter showing slope change |\n\n## When NOT to use kuva\n\n- The project already has matplotlib/plotly in its dev dependencies;\n  consistency matters more than zero-dep.\n- The hotspot is trivial (single function, clear before/after number\n  in a two-column table). Charts are for 3+ data points.\n- CI environment has no Rust toolchain and adding one is not\n  worth it; fall back to a numeric table in the PR comment.\n\nFile v1.9.14:modules/space-complexity.md\n\n---\nmodule: space-complexity\ndescription: AST patterns for space-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- space-complexity\n- memory\n- ast\n- python\n---\n\n# Space Complexity Detectors\n\nThree AST patterns that signal likely space-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## S1: Unbounded `.append()` inside nested loops (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is `ast.Attribute` named\n`append`, found while the loop stack has depth >= 2.\n\n**Why it matters**: A single-loop accumulator is bounded by the\ninput size, which is usually fine. A nested-loop accumulator\ngrows multiplicatively (n×m or n²) and is the typical \"result\nexplosion\" pattern that drives memory exhaustion.\n\n**Note**: The detector deliberately does not flag single-loop\nappends. They are common, expected, and rarely a hotspot. If\nsingle-loop accumulation becomes a problem, that is a runtime\nprofiling concern handled by\n`Skill(parseltongue:python-performance)`.\n\n**Fix**: If the consumer can iterate, yield instead of\nmaterialize:\n\n```python\ndef all_pairs(xs):\n    for x in xs:\n        for y in xs:\n            yield (x, y)  # streaming, O(1) space\n```\n\nWhen the full list is genuinely needed, document the size\nbound:\n\n```python\n# Bounded: |xs| <= 100, so output <= 10000 pairs.\nout = [(x, y) for x in xs for y in xs]\n```\n\n## S2: List wrapping a generator inside a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, where the first arg is\nitself an `ast.Call` to `list`, `dict`, `tuple`, or `set` with\nan `ast.GeneratorExp` as its first argument.\n\n**Why it matters**: `max(list(g))` allocates the full list, then\nwalks it. The wrapper is redundant: reducers accept generators\ndirectly.\n\n**Fix**:\n\n```python\n# Before\nreturn max(list(x * 2 for x in xs))\n\n# After\nreturn max(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` the wrapper is sometimes intentional (to\nforce evaluation), but it's still cheaper to let `sorted` /\n`set` consume the generator directly.\n\n## S3: Per-iteration allocation inside a loop (MEDIUM)\n\n**AST shape**: `ast.Call` inside a loop body where either:\n\n- The `func` is an `ast.Attribute` with name `copy`, or\n- The `func` is an `ast.Name` of `dict`, `list`, or `tuple`\n  with a non-comprehension first argument (the comprehension\n  case is a builder, not a copy).\n\n**Why it matters**: `base.copy()` per iteration allocates a new\ncontainer N times. If only one or two fields change per\niteration, a single allocation outside the loop with selective\nmutation costs less.\n\n**Fix**: Hoist when possible.\n\n```python\n# Before\nfor x in items:\n    snapshot = base.copy()\n    snapshot[\"key\"] = x\n    out.append(snapshot)\n\n# After (when downstream tolerates shared dict identity):\nshared = {**base}\nfor x in items:\n    shared[\"key\"] = x\n    out.append(dict(shared))  # explicit copy at the boundary\n```\n\nWhen the snapshots must be independent, keep `.copy()` but\nmove it outside the loop if possible, or use\n`copy.deepcopy` once and patch.\n\n## What is NOT in this module\n\n- **S4 (closure capture)** was scoped in the plan but deferred:\n  reliable detection requires control-flow analysis beyond\n  single-pass AST. Revisit when gauntlet's graph integration\n  matures.\n- **Numerical-stability concerns** (precision loss, overflow):\n  use `Skill(pensive:math-review)`.\n- **String-builder patterns**: covered by T4 in\n  `time-complexity.md` since the dominant cost is time\n  (quadratic concat), not space.\n\n## Test references\n\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_s1_unbounded_append_in_loop`\n- `test_s2_list_wrapping_generator_in_reducer`\n- `test_s3_copy_inside_loop`\n\nEach test feeds a synthetic snippet through the visitor and\nasserts the expected severity and line.\n\nFile v1.9.14:modules/time-complexity.md\n\n---\nmodule: time-complexity\ndescription: AST patterns for time-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- time-complexity\n- ast\n- python\n---\n\n# Time Complexity Detectors\n\nSix AST patterns that signal likely time-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## T1: Nested loop over the same iterable (HIGH)\n\n**AST shape**: `ast.For` whose `iter` is `ast.Name`, where the\nsame `Name.id` already appears in an enclosing `ast.For`'s iter\non the loop stack.\n\n**Why it matters**: `for x in items: for y in items: ...` is\nO(n²) and rarely intentional. When `items` is large, this\nbecomes the hot spot.\n\n**Fix**:\n\n- If pairwise comparison is needed, sort once and use two\n  pointers (O(n log n)).\n- If membership is needed, build a set once outside the outer\n  loop.\n- If the nested work is independent, consider\n  `itertools.product` for clarity (same complexity but signals\n  intent).\n\n## T2: List `in` lookup inside a loop (HIGH)\n\n**AST shape**: `ast.Compare` with `ast.In` op, right-hand\noperand `ast.Name`, found while the loop stack is non-empty.\n\n**Why it matters**: `if x in ys` is O(n) when `ys` is a list,\nmaking the enclosing loop O(n²). Static analysis can't prove\nthe variable's type, so the detector flags every `in <Name>`\ninside a loop with a conditional suggestion.\n\n**Fix**: If `ys` is a list and won't mutate during the loop:\n\n```python\nys_set = set(ys)\nfor x in xs:\n    if x in ys_set:  # O(1) per lookup\n        ...\n```\n\n## T3: `re.compile()` inside a loop body (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is the attribute access\n`re.compile`, found while the loop stack is non-empty.\n\n**Why it matters**: Python's regex engine caches compiled\npatterns internally, but the cache is bounded and not\nguaranteed for every pattern. Hoisting the compile is cheap\nand explicit.\n\n**Fix**:\n\n```python\n_PAT = re.compile(r\"\\d+\")\n\ndef matches(items):\n    return [s for s in items if _PAT.search(s)]\n```\n\n## T4: String `+=` accumulator in a loop (MEDIUM)\n\n**AST shape**: `ast.AugAssign` with `ast.Add` op, target an\n`ast.Name` previously bound to a string literal in the same\nfunction, occurring inside a loop.\n\n**Why it matters**: Each `+=` allocates a new string and copies\nthe prefix. For long iterations this becomes O(n²) on total\nsize.\n\n**Fix**:\n\n```python\nparts = []\nfor r in rows:\n    parts.append(\",\".join(r) + \"\\n\")\nreturn \"\".join(parts)\n```\n\n`io.StringIO` is also acceptable.\n\n## T5: Recursive function without memoization (LOW)\n\n**AST shape**: `ast.FunctionDef` (or `AsyncFunctionDef`) whose\nbody contains `ast.Call` to the function's own name, with no\n`@functools.cache`, `@functools.lru_cache`, or `@cache`\ndecorator on the def.\n\n**Why it matters**: Naive recursion (e.g., textbook\n`fib(n) = fib(n-1) + fib(n-2)`) has exponential repeat work.\nMemoization makes the same recurrence linear.\n\n**Fix**:\n\n```python\nfrom functools import lru_cache\n\n@lru_cache(maxsize=None)\ndef fib(n):\n    if n < 2:\n        return n\n    return fib(n - 1) + fib(n - 2)\n```\n\nIf the recursion is intentionally non-memoized (e.g., side\neffects on each call), suppress with a comment marker:\n\n```python\n# perf-review: intentional, side-effects on each call\ndef walk(node):\n    ...\n```\n\n## T6: List comprehension passed to a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, with first arg\n`ast.ListComp`.\n\n**Why it matters**: The list materializes the entire result in\nmemory, then the reducer walks it. A generator expression skips\nthe intermediate.\n\n**Fix**: Drop the brackets.\n\n```python\n# Before\nreturn sum([x * 2 for x in xs])\n\n# After\nreturn sum(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` / `frozenset` the materialization is\nunavoidable, so the detector still flags them but a fix is\noptional and may be cosmetic.\n\n## Test references\n\nTests for each detector are at\n`plugins/pensive/tests/skills/test_performance_review.py`. Each\ndetector is paired with at least one BDD-style scenario test\n(`test_t1_*`, `test_t2_*`, ...). New detectors should ship with\na failing test first per the Iron Law.\n\nFile v1.9.14:skill-card.md\n\n## Description: <br>\nDetects time and space complexity hotspots in selected source files using static analysis. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[athola](https://clawhub.ai/user/athola) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineering reviewers use this skill before performance-sensitive merges, refactors, or AI-generated code reviews to identify likely time and space complexity hotspots and prioritize follow-up profiling. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The skill inspects selected source files and may propose optional install, graph-build, benchmark, or test commands. <br>\nMitigation: Review target scope and commands before allowing execution, especially in private repositories. <br>\nRisk: Static-analysis findings can identify AST-level hotspots that may not be material in runtime behavior. <br>\nMitigation: Confirm important findings with profiling, benchmarks, or manual review before relying on them. <br>\n\n\n## Reference(s): <br>\n- [ClawHub skill page](https://clawhub.ai/athola/skills/nm-pensive-performance-review) <br>\n- [Package metadata homepage](https://github.com/athola/claude-night-market/tree/master/plugins/pensive) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [markdown, guidance, shell commands] <br>\n**Output Format:** [Markdown report with severity-grouped findings, suggestions, tier coverage, and optional command snippets.] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Findings are informational static-analysis results and should be confirmed with profiling or benchmarks before changes are treated as performance fixes.] <br>\n\n## Skill Version(s): <br>\n1.9.14 (source: server-resolved release evidence) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.9.13: 7 files, 14543 bytes\n\nFiles: modules/gauntlet-integration.md (4563b), modules/kuva-visualization.md (3728b), modules/space-complexity.md (3864b), modules/time-complexity.md (4172b), skill-card.md (2458b), SKILL.md (10017b), _meta.json (149b)\n\nFile v1.9.13:SKILL.md\n\n---\nname: performance-review\ndescription: Detects time and space complexity hotspots via AST scan\nversion: 1.9.8\ntriggers:\n  - performance\n  - complexity\n  - algorithms\n  - ast\n  - static-analysis\n  - code feels slow\n  - before performance-sensitive merges\n  - or to find O(n²) regressions\nmetadata: {\"openclaw\": {\"homepage\": \"https://github.com/athola/claude-night-market/tree/master/plugins/pensive\", \"emoji\": \"\\ud83e\\udd9e\", \"requires\": {\"config\": [\"night-market.pensive:shared\"]}}}\nsource: claude-night-market\nsource_plugin: pensive\n---\n\n> **Night Market Skill** — ported from [claude-night-market/pensive](https://github.com/athola/claude-night-market/tree/master/plugins/pensive). For the full experience with agents, hooks, and commands, install the Claude Code plugin.\n\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [When to Use](#when-to-use)\n- [When NOT to Use](#when-not-to-use)\n- [Required TodoWrite Items](#required-todowrite-items)\n- [Workflow](#workflow)\n- [Tiered Analysis](#tiered-analysis)\n- [Output Format](#output-format)\n- [Cross-Plugin Dependencies](#cross-plugin-dependencies)\n- [Supporting Modules](#supporting-modules)\n\n# Performance Review\n\nStatic-analysis review of time and space complexity hotspots.\n\nThe skill runs in three escalating tiers. Tier 1 uses Python's\nstdlib `ast` and always runs. Tier 2 uses gauntlet's tree-sitter\nparser to extend detection across languages when gauntlet is\ninstalled. Tier 3 uses the gauntlet code graph to upgrade\nseverity when hotspots reach other hotspots transitively. If\ngauntlet is missing, Tiers 2 and 3 no-op and Tier 1 still\nproduces useful findings on Python source.\n\n## Quick Start\n\n```bash\n/performance-review                  # scan changed files\n/performance-review path/to/file.py  # scan one file\n/performance-review --tier 1         # force Tier 1 only\n```\n\nProgrammatic use:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nskill = PerformanceReviewSkill()\nresult = skill.analyze(context, \"src/module.py\")\nfor f in result.issues:\n    print(f\"[{f.severity}] {f.file}:{f.line} {f.message}\")\n```\n\n## When to Use\n\n- Pre-merge review of code that runs on user-scaled inputs.\n- Triage of a function that \"feels slow\" before reaching for a\n  profiler.\n- Audit a refactor for newly introduced O(n²) patterns.\n- Guardrail for AI-generated code where nested-loop hot spots\n  are common.\n\n## When NOT to Use\n\n- The target needs **runtime** measurement (memory profile, CPU\n  time on real data). Use `Skill(parseltongue:python-performance)`\n  instead: that skill drives `cProfile`, `py-spy`, and benchmarks.\n- General refactoring guidance not focused on hotspots: use\n  `Skill(pensive:code-refinement)` whose `algorithm-efficiency`\n  module covers broader optimization patterns. This skill\n  detects; that skill teaches.\n- Architecture-level performance (sharding, caching layers,\n  queue placement): use `Skill(pensive:architecture-review)`.\n\n## Required TodoWrite Items\n\n1. `perf-review:context-established`\n2. `perf-review:scan-complete`\n3. `perf-review:findings-categorized`\n4. `perf-review:integration-checked`\n5. `perf-review:report-generated`\n\n## Workflow\n\n### Step 1: Context (`perf-review:context-established`)\n\n- Identify target files. If invoked with no argument, use\n  `git diff --name-only`. If invoked with a path, scope to that.\n- Note language(s) involved. Tier 1 covers Python; non-Python\n  files need gauntlet for Tier 2 coverage.\n\n### Step 2: Tier 1 AST scan (`perf-review:scan-complete`)\n\nLoad `modules/time-complexity.md` for the time-side patterns and\n`modules/space-complexity.md` for space-side. Each module\ndocuments the AST shape of every detector.\n\nFor each Python target file, call:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nresult = PerformanceReviewSkill().analyze(context, path)\n```\n\nThe visitor walks the AST once and emits `ReviewFinding` records.\n\n### Step 3: Categorize and rank (`perf-review:findings-categorized`)\n\nGroup findings by severity:\n\n- **HIGH**: O(n²) or worse on input-sized iterables (T1, T2).\n- **MEDIUM**: Unbounded allocation or per-iteration overhead\n  (T3, T4, S1, S3).\n- **LOW**: Style-level inefficiencies (T5, T6, S2).\n- **CRITICAL**: Reserved for Tier-3 transitive upgrades.\n\nWithin a severity, sort by file then line. Suppress findings\nthe user has explicitly marked acceptable (TODO/comment\nmarkers) at module-load time of the target.\n\n### Step 4: Tier 2/3 enrichment (`perf-review:integration-checked`)\n\nLoad `modules/gauntlet-integration.md` for the contract.\n\nIf gauntlet is installed, run Tier 2 on non-Python files that\nwere skipped at Step 2. If a `.gauntlet/graph.db` exists in the\nworking tree, run Tier 3 to upgrade severities based on\ntransitive hotspot reachability.\n\nIf gauntlet is missing, this step is a no-op and the report\nnotes \"Tier 2/3 not available: install gauntlet for\nmulti-language and call-chain coverage.\"\n\n### Step 5: Report (`perf-review:report-generated`)\n\nEmit a markdown report:\n\n```\n## Performance Review: <target>\n\n### HIGH (<count>)\n- src/foo.py:42: Nested loop over the same iterable 'items'.\n  Suggestion: sort + two pointers, or hash-set membership.\n\n### MEDIUM (<count>)\n- ...\n\n### LOW (<count>)\n- ...\n\nTier coverage: 1 (always) | 2 (gauntlet ✓/✗) | 3 (graph ✓/✗)\n```\n\nThe report is informational. Apply fixes via\n`Skill(pensive:code-refinement)` or hand-merge.\n\n## Tiered Analysis\n\n| Tier | Source | When it runs | What it covers |\n|------|--------|--------------|----------------|\n| 1 | stdlib `ast` | Always (Python source only) | T1-T6, S1-S3 |\n| 2 | `gauntlet.treesitter_parser` | When gauntlet importable | Same patterns adapted to JS/TS, Go, Rust, Java, C/C++ |\n| 3 | `gauntlet.graph.GraphStore` | When `.gauntlet/graph.db` exists | Severity upgrade via transitive call chains |\n\n## Output Format\n\nFindings use the shared `ReviewFinding` dataclass from\n`pensive.skills.base`:\n\n```python\nReviewFinding(\n    file=\"src/module.py\",\n    line=42,\n    severity=\"HIGH\",          # LOW | MEDIUM | HIGH | CRITICAL\n    category=\"time\",          # time | space\n    message=\"Nested loop over the same iterable 'items'.\",\n    suggestion=\"Sort + two pointers, or hash-set membership.\",\n    code_snippet=\"\",\n)\n```\n\nThis shape matches every other pensive review skill, so the\nfindings can flow into `Skill(pensive:unified-review)` without\ntranslation.\n\n## Cross-Plugin Dependencies\n\n| Dependency | Required? | Effect when missing |\n|------------|-----------|---------------------|\n| `gauntlet.treesitter_parser` | Optional | Tier 2 returns []; Python coverage unchanged |\n| `gauntlet.graph.GraphStore` | Optional | Tier 3 returns []; severities are not upgraded |\n\nThe optional-import contract follows the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32` and\n`plugins/gauntlet/hooks/pr_blast_radius.py:52-56`: try-import\nto module-level sentinels, then early-return on `None` inside\neach tier helper. See `modules/gauntlet-integration.md` for the\nexact code shape.\n\n## Supporting Modules\n\n- `modules/time-complexity.md`: T1-T6 detector patterns and AST\n  shapes.\n- `modules/space-complexity.md`: S1-S3 detector patterns.\n- `modules/gauntlet-integration.md`: Tier 2/3 contract,\n  fallback semantics, examples.\n- `modules/kuva-visualization.md`: Rendering benchmark data as\n  charts with kuva (criterion, pytest-benchmark, ad-hoc tables).\n  Covers when chart evidence satisfies proof-of-work requirements.\n\n## Verification\n\nA perf-review finding is only useful if the caller can confirm it\nis real. Use this checklist before treating any finding as worth\nfixing:\n\n1. **Reproduce under a profiler.** Run `cProfile`, `py-spy`, or the\n   language-specific equivalent on the hotspot. The findings\n   pinpoint AST shapes; the profiler validates the runtime impact.\n2. **Re-run the failing benchmark.** If `benches/` exists, the\n   hotspot should show up in numbers, not just AST scans.\n3. **Compare numbers before and after the proposed fix.** The fix\n   is wrong if numbers do not move. Capture both timings as\n   evidence references like `[E1]` (before) and `[E2]` (after).\n   When 3+ data points exist, render a kuva chart and attach it\n   to the PR — see `modules/kuva-visualization.md`.\n4. **Sample two or three reported hotspots manually.** Findings can\n   be true at the AST level and false at the call-graph level\n   when callers short-circuit. Manual sampling catches that.\n\nThe `Skill(imbue:proof-of-work)` discipline applies: claims like\n\"the hotspot is fixed\" require evidence, not assertion.\n\n## Testing\n\nA test file already lives at\n`plugins/pensive/tests/skills/test_performance_review.py` covering\nthe AST-shape detectors. Two rules for changes here:\n\n- **Add a new detector with a test.** Any new T-* or S-* pattern\n  added to the modules ships with a test that has the smallest\n  AST sample exercising it.\n- **Add a regression test for any false positive removed.** When\n  the skill stops firing on a shape that used to look hot, the\n  reason should appear as a test case so the regression is\n  discoverable later.\n\nThe Iron Law applies: a new detector without a failing test first\nis a request to skip TDD on a code-analysis component, which is\nexactly the place where TDD pays off most.\n\n## Exit Criteria\n\n- [ ] A perf-review report file exists for the requested target.\n- [ ] Every finding carries a severity label and a concrete\n      suggestion the caller can act on.\n- [ ] Time-complexity (T1-T6) and space-complexity (S1-S3)\n      detectors have been run; tier coverage is reported.\n- [ ] Tier 2 (gauntlet treesitter) and Tier 3 (graph store)\n      contracts honor the optional-import sentinel: missing\n      modules return `[]` rather than raising.\n- [ ] Each new detector ships with a smallest-AST test that\n      fails before the detector exists; each removed false\n      positive ships with a regression test.\n- [ ] Findings flow into `Skill(pensive:unified-review)` without\n      translation when invoked from the unified entry point.\n\nFile v1.9.13:_meta.json\n\n{\n  \"ownerId\": \"kn7d107jg9jv602h9ytsegydq184a42s\",\n  \"slug\": \"nm-pensive-performance-review\",\n  \"version\": \"1.9.13\",\n  \"publishedAt\": 1782577339651\n}\n\nFile v1.9.13:modules/gauntlet-integration.md\n\n---\nmodule: gauntlet-integration\ndescription: Tier 2/3 contract via gauntlet tree-sitter and graph\nparent_skill: performance-review\ncategory: integration\ntags:\n- gauntlet\n- tree-sitter\n- graph\n- optional-dependency\n---\n\n# Gauntlet Integration\n\nPerformance review is a Tier-1 skill out of the box. Tiers 2 and\n3 enrich the analysis when gauntlet is installed.\n\n## Optional-import contract\n\nAt module load time, `performance_review.py` runs two\ntry-imports to module-level sentinels:\n\n```python\ntry:\n    from gauntlet.treesitter_parser import parse_file as _gt_parse\nexcept (ImportError, ModuleNotFoundError):\n    _gt_parse = None\n\ntry:\n    from gauntlet.graph import GraphStore as _GraphStore\nexcept (ImportError, ModuleNotFoundError):\n    _GraphStore = None\n```\n\nThe dual-exception catch matches the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32`. It survives the\ncase where the import fails for a reason other than the module\nbeing absent (e.g., a transitive ImportError deep inside\ngauntlet's own stack).\n\nEach tier helper checks its sentinel and early-returns:\n\n```python\ndef _tier2_findings(self, context, file_path):\n    if _gt_parse is None:\n        return []\n    ...\n\ndef _tier3_findings(self, context, existing, file_path):\n    if _GraphStore is None:\n        return []\n    ...\n```\n\nThis is the same pattern proven in\n`plugins/pensive/hooks/pr_blast_radius.py:52-56`, where\ngauntlet's blast-radius graph is consulted only when the\nplugin is installed.\n\n## Tier 2: Tree-sitter coverage\n\nWhen `_gt_parse` is set, `_tier2_findings` invokes\n`parse_file(path)` and receives `(nodes, edges)` describing the\ntarget file's AST in gauntlet's neutral graph format.\n\nLanguages currently parsed: Python, JavaScript, TypeScript, Go,\nRust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Scala (per\ngauntlet's `_EXT_TO_LANG` map).\n\nThe patterns translated to Tier 2 are the language-agnostic\nones:\n\n- T1 (nested loop over same iterable): present in every\n  imperative language.\n- T2 (membership in list): adapts to language idioms (e.g.,\n  `Array.includes` in JS, `slices.Contains` in Go).\n- S1 (append in nested loops): `arr.push(...)` in JS,\n  `append(slice, ...)` in Go.\n\nPatterns that do NOT translate (skipped at Tier 2):\n\n- T3 (`re.compile` in a loop): Python-specific call shape.\n- T6 (list comprehension passed to a reducer): Python-specific\n  syntax.\n- T4 (string `+=`): many languages have language-level string\n  builders that handle this; the cost model differs.\n\n## Tier 3: Transitive call analysis\n\nWhen both `_GraphStore` is set AND a `.gauntlet/graph.db` file\nexists in the working tree, `_tier3_findings` opens the graph\nand queries `impact_radius()` for each existing finding's\nfunction.\n\nIf a function reachable from a Tier-1/2 hotspot is itself a\nhotspot, the original finding's severity is upgraded one step:\n\n| Original | Upgraded |\n|----------|----------|\n| LOW      | MEDIUM   |\n| MEDIUM   | HIGH     |\n| HIGH     | CRITICAL |\n\nThis catches cases where the surface code looks fine but the\nhelper it calls is the actual bottleneck.\n\nThe graph file is built by gauntlet's own command:\n\n```bash\n/gauntlet-graph build .\n```\n\nWhen the graph does not exist, Tier 3 returns []. Building the\ngraph is a one-time cost; it speeds up every subsequent review.\n\n## Failure modes and fallbacks\n\n| Condition | Tier 2 | Tier 3 | User-visible effect |\n|-----------|--------|--------|---------------------|\n| gauntlet not installed | sentinel None, no-op | sentinel None, no-op | Tier 1 only; report notes the gap |\n| gauntlet installed, no graph.db | parses non-Python files | no-op (no DB) | Multi-language coverage but no transitive upgrades |\n| Both installed | full enrichment | severity upgrades active | Maximum coverage |\n\nIn every case, Tier 1 still runs. The skill never fails because\ngauntlet is missing. This is a deliberate choice: pensive must\nnot require an optional plugin to deliver core value.\n\n## Verification\n\nThe fallback contract is exercised by three tests in\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_tier2_returns_empty_when_gauntlet_missing`: stubs\n  `_gt_parse` to None and asserts `_tier2_findings` returns `[]`.\n- `test_tier3_returns_empty_when_graphstore_missing`: same for\n  `_tier3_findings`.\n- `test_full_analyze_with_gauntlet_blocked_returns_tier1_only`:\n  stubs both sentinels and asserts the full `analyze()` still\n  produces Tier-1 findings (T1 fires on a nested-loop snippet).\n\nRun them with:\n\n```bash\ncd plugins/pensive\nuv run pytest tests/skills/test_performance_review.py -v --no-cov\n```\n\nFile v1.9.13:modules/kuva-visualization.md\n\n---\nmodule: kuva-visualization\ncategory: output\ndependencies: [Bash, Read]\nestimated_tokens: 350\n---\n\n# Visualizing Performance Findings with kuva\n\n**When a performance review produces before/after benchmark data,\nrender it as a chart.** Text comparisons like \"380ms → 60ms\" are\ncorrect but hard to scan across multiple hotspots. A scatter or\nbar chart makes regressions and wins immediately visible.\n\n[kuva](https://github.com/Psy-Fer/kuva) is a Rust scientific\nplotting library (and CLI binary) that renders directly from TSV/CSV\ninput to SVG, PNG, or the terminal. Install once; pipe benchmark\ndata in without modifying project source.\n\n## Install\n\n```bash\ncargo install kuva --features cli\n```\n\n## Rendering a before/after benchmark comparison\n\n### criterion (Rust)\n\ncriterion writes per-benchmark timing samples to\n`target/criterion/<name>/new/estimates.json`. Extract the mean and\npipe to kuva:\n\n```bash\n# Collect before/after means for all criterion benchmarks\npython3 - <<'EOF'\nimport json, pathlib, sys\n\nrows = [\"benchmark\\tstage\\tns\"]\nfor est in pathlib.Path(\"target/criterion\").rglob(\"estimates.json\"):\n    bench = est.parts[-3]\n    data = json.loads(est.read_text())\n    mean_ns = data[\"mean\"][\"point_estimate\"]\n    # Distinguish before/after by tag; adjust to your workflow.\n    rows.append(f\"{bench}\\tafter\\t{mean_ns:.1f}\")\n\nprint(\"\\n\".join(rows))\nEOF | kuva bar /dev/stdin --x benchmark --y ns --color-by stage \\\n      --title \"Before vs After\" --terminal\n```\n\nFor a paired comparison where you have both runs saved:\n\n```bash\n# before.tsv and after.tsv each contain: benchmark<TAB>ns\nkuva scatter before.tsv after.tsv \\\n    --x ns --y ns --color-by stage \\\n    --title \"Hotspot timing (lower is better)\" \\\n    -o perf-comparison.svg\n```\n\n### pytest-benchmark (Python)\n\n```bash\npytest --benchmark-json=bench.json tests/\n\n# Convert to TSV\npython3 -c \"\nimport json, sys\nd = json.load(open('bench.json'))\nprint('name\\tns')\nfor b in d['benchmarks']:\n    print(b['name'] + '\\t' + str(b['stats']['mean'] * 1e9))\n\" | kuva bar /dev/stdin --x name --y ns \\\n      --title \"Benchmark means (ns)\" -o bench.svg\n```\n\n### Ad-hoc timing table\n\nIf you are capturing timings manually (e.g., from production traces\nas in the mlock war story):\n\n```tsv\nstage\tp50_ms\tp99_ms\nbefore_mlock\t180\t380\nafter_mlock\t35\t60\n```\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms \\\n    --title \"p99 barge-in latency (ms)\" -o latency.svg\n```\n\n## Terminal output (no file required)\n\nFor quick CI feedback without writing an SVG artifact, add\n`--terminal` to any kuva command. The chart renders as Unicode\nblock characters directly in the shell, visible in CI logs.\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms --terminal\n```\n\n## When to attach a chart as evidence\n\nThe `Skill(imbue:proof-of-work)` discipline requires evidence\nreferences `[E1]`/`[E2]` for before/after claims. A kuva-rendered\nSVG in the PR description or comments is a valid `[E2]` when it\nshows the post-fix benchmark result alongside the pre-fix baseline.\n\nMinimum evidence bar:\n\n| Claim | Required chart type |\n|-------|---------------------|\n| \"Latency improved by X\" | Bar or scatter with before/after |\n| \"Throughput doubled\" | Line or bar over input size range |\n| \"Memory usage flat\" | Line over time or input size |\n| \"O(n log n) vs O(n²)\" | Log-log scatter showing slope change |\n\n## When NOT to use kuva\n\n- The project already has matplotlib/plotly in its dev dependencies;\n  consistency matters more than zero-dep.\n- The hotspot is trivial (single function, clear before/after number\n  in a two-column table). Charts are for 3+ data points.\n- CI environment has no Rust toolchain and adding one is not\n  worth it; fall back to a numeric table in the PR comment.\n\nFile v1.9.13:modules/space-complexity.md\n\n---\nmodule: space-complexity\ndescription: AST patterns for space-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- space-complexity\n- memory\n- ast\n- python\n---\n\n# Space Complexity Detectors\n\nThree AST patterns that signal likely space-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## S1: Unbounded `.append()` inside nested loops (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is `ast.Attribute` named\n`append`, found while the loop stack has depth >= 2.\n\n**Why it matters**: A single-loop accumulator is bounded by the\ninput size, which is usually fine. A nested-loop accumulator\ngrows multiplicatively (n×m or n²) and is the typical \"result\nexplosion\" pattern that drives memory exhaustion.\n\n**Note**: The detector deliberately does not flag single-loop\nappends. They are common, expected, and rarely a hotspot. If\nsingle-loop accumulation becomes a problem, that is a runtime\nprofiling concern handled by\n`Skill(parseltongue:python-performance)`.\n\n**Fix**: If the consumer can iterate, yield instead of\nmaterialize:\n\n```python\ndef all_pairs(xs):\n    for x in xs:\n        for y in xs:\n            yield (x, y)  # streaming, O(1) space\n```\n\nWhen the full list is genuinely needed, document the size\nbound:\n\n```python\n# Bounded: |xs| <= 100, so output <= 10000 pairs.\nout = [(x, y) for x in xs for y in xs]\n```\n\n## S2: List wrapping a generator inside a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, where the first arg is\nitself an `ast.Call` to `list`, `dict`, `tuple`, or `set` with\nan `ast.GeneratorExp` as its first argument.\n\n**Why it matters**: `max(list(g))` allocates the full list, then\nwalks it. The wrapper is redundant: reducers accept generators\ndirectly.\n\n**Fix**:\n\n```python\n# Before\nreturn max(list(x * 2 for x in xs))\n\n# After\nreturn max(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` the wrapper is sometimes intentional (to\nforce evaluation), but it's still cheaper to let `sorted` /\n`set` consume the generator directly.\n\n## S3: Per-iteration allocation inside a loop (MEDIUM)\n\n**AST shape**: `ast.Call` inside a loop body where either:\n\n- The `func` is an `ast.Attribute` with name `copy`, or\n- The `func` is an `ast.Name` of `dict`, `list`, or `tuple`\n  with a non-comprehension first argument (the comprehension\n  case is a builder, not a copy).\n\n**Why it matters**: `base.copy()` per iteration allocates a new\ncontainer N times. If only one or two fields change per\niteration, a single allocation outside the loop with selective\nmutation costs less.\n\n**Fix**: Hoist when possible.\n\n```python\n# Before\nfor x in items:\n    snapshot = base.copy()\n    snapshot[\"key\"] = x\n    out.append(snapshot)\n\n# After (when downstream tolerates shared dict identity):\nshared = {**base}\nfor x in items:\n    shared[\"key\"] = x\n    out.append(dict(shared))  # explicit copy at the boundary\n```\n\nWhen the snapshots must be independent, keep `.copy()` but\nmove it outside the loop if possible, or use\n`copy.deepcopy` once and patch.\n\n## What is NOT in this module\n\n- **S4 (closure capture)** was scoped in the plan but deferred:\n  reliable detection requires control-flow analysis beyond\n  single-pass AST. Revisit when gauntlet's graph integration\n  matures.\n- **Numerical-stability concerns** (precision loss, overflow):\n  use `Skill(pensive:math-review)`.\n- **String-builder patterns**: covered by T4 in\n  `time-complexity.md` since the dominant cost is time\n  (quadratic concat), not space.\n\n## Test references\n\n`plugins/pensive/tests/skills/test_performance_review.py`:\n\n- `test_s1_unbounded_append_in_loop`\n- `test_s2_list_wrapping_generator_in_reducer`\n- `test_s3_copy_inside_loop`\n\nEach test feeds a synthetic snippet through the visitor and\nasserts the expected severity and line.\n\nFile v1.9.13:modules/time-complexity.md\n\n---\nmodule: time-complexity\ndescription: AST patterns for time-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- time-complexity\n- ast\n- python\n---\n\n# Time Complexity Detectors\n\nSix AST patterns that signal likely time-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## T1: Nested loop over the same iterable (HIGH)\n\n**AST shape**: `ast.For` whose `iter` is `ast.Name`, where the\nsame `Name.id` already appears in an enclosing `ast.For`'s iter\non the loop stack.\n\n**Why it matters**: `for x in items: for y in items: ...` is\nO(n²) and rarely intentional. When `items` is large, this\nbecomes the hot spot.\n\n**Fix**:\n\n- If pairwise comparison is needed, sort once and use two\n  pointers (O(n log n)).\n- If membership is needed, build a set once outside the outer\n  loop.\n- If the nested work is independent, consider\n  `itertools.product` for clarity (same complexity but signals\n  intent).\n\n## T2: List `in` lookup inside a loop (HIGH)\n\n**AST shape**: `ast.Compare` with `ast.In` op, right-hand\noperand `ast.Name`, found while the loop stack is non-empty.\n\n**Why it matters**: `if x in ys` is O(n) when `ys` is a list,\nmaking the enclosing loop O(n²). Static analysis can't prove\nthe variable's type, so the detector flags every `in <Name>`\ninside a loop with a conditional suggestion.\n\n**Fix**: If `ys` is a list and won't mutate during the loop:\n\n```python\nys_set = set(ys)\nfor x in xs:\n    if x in ys_set:  # O(1) per lookup\n        ...\n```\n\n## T3: `re.compile()` inside a loop body (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is the attribute access\n`re.compile`, found while the loop stack is non-empty.\n\n**Why it matters**: Python's regex engine caches compiled\npatterns internally, but the cache is bounded and not\nguaranteed for every pattern. Hoisting the compile is cheap\nand explicit.\n\n**Fix**:\n\n```python\n_PAT = re.compile(r\"\\d+\")\n\ndef matches(items):\n    return [s for s in items if _PAT.search(s)]\n```\n\n## T4: String `+=` accumulator in a loop (MEDIUM)\n\n**AST shape**: `ast.AugAssign` with `ast.Add` op, target an\n`ast.Name` previously bound to a string literal in the same\nfunction, occurring inside a loop.\n\n**Why it matters**: Each `+=` allocates a new string and copies\nthe prefix. For long iterations this becomes O(n²) on total\nsize.\n\n**Fix**:\n\n```python\nparts = []\nfor r in rows:\n    parts.append(\",\".join(r) + \"\\n\")\nreturn \"\".join(parts)\n```\n\n`io.StringIO` is also acceptable.\n\n## T5: Recursive function without memoization (LOW)\n\n**AST shape**: `ast.FunctionDef` (or `AsyncFunctionDef`) whose\nbody contains `ast.Call` to the function's own name, with no\n`@functools.cache`, `@func\n\nArchive v1.9.12: 7 files, 14394 bytes\n\nFiles: modules/gauntlet-integration.md (4563b), modules/kuva-visualization.md (3728b), modules/space-complexity.md (3864b), modules/time-complexity.md (4172b), skill-card.md (1967b), SKILL.md (10017b), _meta.json (149b)\n\nArchive v1.0.0: 7 files, 14576 bytes\n\nFiles: modules/gauntlet-integration.md (4563b), modules/kuva-visualization.md (3728b), modules/space-complexity.md (3864b), modules/time-complexity.md (4172b), skill-card.md (2426b), SKILL.md (10017b), _meta.json (148b)","readmeExcerpt":"Skill: performance-review Owner: athola Summary: Detects time and space complexity hotspots via AST scan Tags: latest:1.9.19 Version history: v1.9.19 | 2026-08-26T13:19:06.422Z | user Release v1.9.19 v1.9.17 | 2026-07-30T05:39:18.228Z | user Release v1.9.17 v1.9.16 | 2026-07-14T19:56:01.066Z | user Release v1.9.16 v1.9.14 | 2026-06-30T18:04:19.616Z | user Release v1.9.14 v1.9.13 | 2026-06-27T16:22:19.651Z | user Rele","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"/performance-review                  # scan changed files\n/performance-review path/to/file.py  # scan one file\n/performance-review --tier 1         # force Tier 1 only"},{"language":"python","snippet":"from pensive.skills.performance_review import PerformanceReviewSkill\nskill = PerformanceReviewSkill()\nresult = skill.analyze(context, \"src/module.py\")\nfor f in result.issues:\n    print(f\"[{f.severity}] {f.file}:{f.line} {f.message}\")"},{"language":"python","snippet":"from pensive.skills.performance_review import PerformanceReviewSkill\nresult = PerformanceReviewSkill().analyze(context, path)"},{"language":"text","snippet":"## Performance Review: <target>\n\n### HIGH (<count>)\n- src/foo.py:42: Nested loop over the same iterable 'items'.\n  Suggestion: sort + two pointers, or hash-set membership.\n\n### MEDIUM (<count>)\n- ...\n\n### LOW (<count>)\n- ...\n\nTier coverage: 1 (always) | 2 (gauntlet ✓/✗) | 3 (graph ✓/✗)"},{"language":"python","snippet":"ReviewFinding(\n    file=\"src/module.py\",\n    line=42,\n    severity=\"HIGH\",          # LOW | MEDIUM | HIGH | CRITICAL\n    category=\"time\",          # time | space\n    message=\"Nested loop over the same iterable 'items'.\",\n    suggestion=\"Sort + two pointers, or hash-set membership.\",\n    code_snippet=\"\",\n)"},{"language":"python","snippet":"try:\n    from gauntlet.treesitter_parser import parse_file as _gt_parse\nexcept (ImportError, ModuleNotFoundError):\n    _gt_parse = None\n\ntry:\n    from gauntlet.graph import GraphStore as _GraphStore\nexcept (ImportError, ModuleNotFoundError):\n    _GraphStore = None"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: performance-review\ndescription: Detects time and space complexity hotspots via AST scan\nversion: 1.9.8\ntriggers:\n  - performance\n  - complexity\n  - algorithms\n  - ast\n  - static-analysis\n  - code feels slow\n  - before performance-sensitive merges\n  - or to find O(n²) regressions\nmetadata: {\"openclaw\": {\"homepage\": \"https://github.com/athola/claude-night-market/tree/master/plugins/pensive\", \"emoji\": \"\\ud83e\\udd9e\", \"requires\": {\"config\": [\"night-market.pensive:shared\"]}}}\nsource: claude-night-market\nsource_plugin: pensive\n---\n\n> **Night Market Skill** — ported from [claude-night-market/pensive](https://github.com/athola/claude-night-market/tree/master/plugins/pensive). For the full experience with agents, hooks, and commands, install the Claude Code plugin.\n\n\n## Table of Contents\n\n- [Quick Start](#quick-start)\n- [When to Use](#when-to-use)\n- [When NOT to Use](#when-not-to-use)\n- [Required TodoWrite Items](#required-todowrite-items)\n- [Workflow](#workflow)\n- [Tiered Analysis](#tiered-analysis)\n- [Output Format](#output-format)\n- [Cross-Plugin Dependencies](#cross-plugin-dependencies)\n- [Supporting Modules](#supporting-modules)\n\n# Performance Review\n\nStatic-analysis review of time and space complexity hotspots.\n\nThe skill runs in three escalating tiers. Tier 1 uses Python's\nstdlib `ast` and always runs. Tier 2 uses gauntlet's tree-sitter\nparser to extend detection across languages when gauntlet is\ninstalled. Tier 3 uses the gauntlet code graph to upgrade\nseverity when hotspots reach other hotspots transitively. If\ngauntlet is missing, Tiers 2 and 3 no-op and Tier 1 still\nproduces useful findings on Python source.\n\n## Quick Start\n\n```bash\n/performance-review                  # scan changed files\n/performance-review path/to/file.py  # scan one file\n/performance-review --tier 1         # force Tier 1 only\n```\n\nProgrammatic use:\n\n```python\nfrom pensive.skills.performance_review import PerformanceReviewSkill\nskill = PerformanceReviewSkill()\nresult = skill.analyze(context, \"src/module.py\")\nfor f in result.issues:\n    print(f\"[{f.severity}] {f.file}:{f.line} {f.message}\")\n```\n\n## When to Use\n\n- Pre-merge review of code that runs on user-scaled inputs.\n- Triage of a function that \"feels slow\" before reaching for a\n  profiler.\n- Audit a refactor for newly introduced O(n²) patterns.\n- Guardrail for AI-generated code where nested-loop hot spots\n  are common.\n\n## When NOT to Use\n\n- The target needs **runtime** measurement (memory profile, CPU\n  time on real data). Use `Skill(parseltongue:python-performance)`\n  instead: that skill drives `cProfile`, `py-spy`, and benchmarks.\n- General refactoring guidance not focused on hotspots: use\n  `Skill(pensive:code-refinement)` whose `algorithm-efficiency`\n  module covers broader optimization patterns. This skill\n  detects; that skill teaches.\n- Architecture-level performance (sharding, caching layers,\n  queue placement): use `Skill(pensive:architecture-review)`.\n\n## Required TodoWrite Items\n\n1. `perf-review:con"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7d107jg9jv602h9ytsegydq184a42s\",\n  \"slug\": \"nm-pensive-performance-review\",\n  \"version\": \"1.9.19\",\n  \"publishedAt\": 1787750346422\n}"},{"path":"modules/gauntlet-integration.md","content":"---\nmodule: gauntlet-integration\ndescription: Tier 2/3 contract via gauntlet tree-sitter and graph\nparent_skill: performance-review\ncategory: integration\ntags:\n- gauntlet\n- tree-sitter\n- graph\n- optional-dependency\n---\n\n# Gauntlet Integration\n\nPerformance review is a Tier-1 skill out of the box. Tiers 2 and\n3 enrich the analysis when gauntlet is installed.\n\n## Optional-import contract\n\nAt module load time, `performance_review.py` runs two\ntry-imports to module-level sentinels:\n\n```python\ntry:\n    from gauntlet.treesitter_parser import parse_file as _gt_parse\nexcept (ImportError, ModuleNotFoundError):\n    _gt_parse = None\n\ntry:\n    from gauntlet.graph import GraphStore as _GraphStore\nexcept (ImportError, ModuleNotFoundError):\n    _GraphStore = None\n```\n\nThe dual-exception catch matches the precedent in\n`plugins/leyline/src/leyline/tokens.py:25-32`. It survives the\ncase where the import fails for a reason other than the module\nbeing absent (e.g., a transitive ImportError deep inside\ngauntlet's own stack).\n\nEach tier helper checks its sentinel and early-returns:\n\n```python\ndef _tier2_findings(self, context, file_path):\n    if _gt_parse is None:\n        return []\n    ...\n\ndef _tier3_findings(self, context, existing, file_path):\n    if _GraphStore is None:\n        return []\n    ...\n```\n\nThis is the same pattern proven in\n`plugins/pensive/hooks/pr_blast_radius.py:52-56`, where\ngauntlet's blast-radius graph is consulted only when the\nplugin is installed.\n\n## Tier 2: Tree-sitter coverage\n\nWhen `_gt_parse` is set, `_tier2_findings` invokes\n`parse_file(path)` and receives `(nodes, edges)` describing the\ntarget file's AST in gauntlet's neutral graph format.\n\nLanguages currently parsed: Python, JavaScript, TypeScript, Go,\nRust, Java, C, C++, C#, Ruby, PHP, Kotlin, Swift, Scala (per\ngauntlet's `_EXT_TO_LANG` map).\n\nThe patterns translated to Tier 2 are the language-agnostic\nones:\n\n- T1 (nested loop over same iterable): present in every\n  imperative language.\n- T2 (membership in list): adapts to language idioms (e.g.,\n  `Array.includes` in JS, `slices.Contains` in Go).\n- S1 (append in nested loops): `arr.push(...)` in JS,\n  `append(slice, ...)` in Go.\n\nPatterns that do NOT translate (skipped at Tier 2):\n\n- T3 (`re.compile` in a loop): Python-specific call shape.\n- T6 (list comprehension passed to a reducer): Python-specific\n  syntax.\n- T4 (string `+=`): many languages have language-level string\n  builders that handle this; the cost model differs.\n\n## Tier 3: Transitive call analysis\n\nWhen both `_GraphStore` is set AND a `.gauntlet/graph.db` file\nexists in the working tree, `_tier3_findings` opens the graph\nand queries `impact_radius()` for each existing finding's\nfunction.\n\nIf a function reachable from a Tier-1/2 hotspot is itself a\nhotspot, the original finding's severity is upgraded one step:\n\n| Original | Upgraded |\n|----------|----------|\n| LOW      | MEDIUM   |\n| MEDIUM   | HIGH     |\n| HIGH     | CRITICAL |\n\nThis catches cases where the surface code look"},{"path":"modules/kuva-visualization.md","content":"---\nmodule: kuva-visualization\ncategory: output\ndependencies: [Bash, Read]\nestimated_tokens: 350\n---\n\n# Visualizing Performance Findings with kuva\n\n**When a performance review produces before/after benchmark data,\nrender it as a chart.** Text comparisons like \"380ms → 60ms\" are\ncorrect but hard to scan across multiple hotspots. A scatter or\nbar chart makes regressions and wins immediately visible.\n\n[kuva](https://github.com/Psy-Fer/kuva) is a Rust scientific\nplotting library (and CLI binary) that renders directly from TSV/CSV\ninput to SVG, PNG, or the terminal. Install once; pipe benchmark\ndata in without modifying project source.\n\n## Install\n\n```bash\ncargo install kuva --features cli\n```\n\n## Rendering a before/after benchmark comparison\n\n### criterion (Rust)\n\ncriterion writes per-benchmark timing samples to\n`target/criterion/<name>/new/estimates.json`. Extract the mean and\npipe to kuva:\n\n```bash\n# Collect before/after means for all criterion benchmarks\npython3 - <<'EOF'\nimport json, pathlib, sys\n\nrows = [\"benchmark\\tstage\\tns\"]\nfor est in pathlib.Path(\"target/criterion\").rglob(\"estimates.json\"):\n    bench = est.parts[-3]\n    data = json.loads(est.read_text())\n    mean_ns = data[\"mean\"][\"point_estimate\"]\n    # Distinguish before/after by tag; adjust to your workflow.\n    rows.append(f\"{bench}\\tafter\\t{mean_ns:.1f}\")\n\nprint(\"\\n\".join(rows))\nEOF | kuva bar /dev/stdin --x benchmark --y ns --color-by stage \\\n      --title \"Before vs After\" --terminal\n```\n\nFor a paired comparison where you have both runs saved:\n\n```bash\n# before.tsv and after.tsv each contain: benchmark<TAB>ns\nkuva scatter before.tsv after.tsv \\\n    --x ns --y ns --color-by stage \\\n    --title \"Hotspot timing (lower is better)\" \\\n    -o perf-comparison.svg\n```\n\n### pytest-benchmark (Python)\n\n```bash\npytest --benchmark-json=bench.json tests/\n\n# Convert to TSV\npython3 -c \"\nimport json, sys\nd = json.load(open('bench.json'))\nprint('name\\tns')\nfor b in d['benchmarks']:\n    print(b['name'] + '\\t' + str(b['stats']['mean'] * 1e9))\n\" | kuva bar /dev/stdin --x name --y ns \\\n      --title \"Benchmark means (ns)\" -o bench.svg\n```\n\n### Ad-hoc timing table\n\nIf you are capturing timings manually (e.g., from production traces\nas in the mlock war story):\n\n```tsv\nstage\tp50_ms\tp99_ms\nbefore_mlock\t180\t380\nafter_mlock\t35\t60\n```\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms \\\n    --title \"p99 barge-in latency (ms)\" -o latency.svg\n```\n\n## Terminal output (no file required)\n\nFor quick CI feedback without writing an SVG artifact, add\n`--terminal` to any kuva command. The chart renders as Unicode\nblock characters directly in the shell, visible in CI logs.\n\n```bash\nkuva bar timings.tsv --x stage --y p99_ms --terminal\n```\n\n## When to attach a chart as evidence\n\nThe `Skill(imbue:proof-of-work)` discipline requires evidence\nreferences `[E1]`/`[E2]` for before/after claims. A kuva-rendered\nSVG in the PR description or comments is a valid `[E2]` when it\nshows the post-fix benchmark result alongside the pre-fix b"},{"path":"modules/space-complexity.md","content":"---\nmodule: space-complexity\ndescription: AST patterns for space-complexity hotspot detection\nparent_skill: performance-review\ncategory: code-quality\ntags:\n- space-complexity\n- memory\n- ast\n- python\n---\n\n# Space Complexity Detectors\n\nThree AST patterns that signal likely space-complexity hotspots.\nEach detector cites the AST node it matches, the heuristic, and\na concrete fix.\n\n## S1: Unbounded `.append()` inside nested loops (MEDIUM)\n\n**AST shape**: `ast.Call` whose `func` is `ast.Attribute` named\n`append`, found while the loop stack has depth >= 2.\n\n**Why it matters**: A single-loop accumulator is bounded by the\ninput size, which is usually fine. A nested-loop accumulator\ngrows multiplicatively (n×m or n²) and is the typical \"result\nexplosion\" pattern that drives memory exhaustion.\n\n**Note**: The detector deliberately does not flag single-loop\nappends. They are common, expected, and rarely a hotspot. If\nsingle-loop accumulation becomes a problem, that is a runtime\nprofiling concern handled by\n`Skill(parseltongue:python-performance)`.\n\n**Fix**: If the consumer can iterate, yield instead of\nmaterialize:\n\n```python\ndef all_pairs(xs):\n    for x in xs:\n        for y in xs:\n            yield (x, y)  # streaming, O(1) space\n```\n\nWhen the full list is genuinely needed, document the size\nbound:\n\n```python\n# Bounded: |xs| <= 100, so output <= 10000 pairs.\nout = [(x, y) for x in xs for y in xs]\n```\n\n## S2: List wrapping a generator inside a reducer (LOW)\n\n**AST shape**: `ast.Call` to one of `sum`, `max`, `min`, `any`,\n`all`, `sorted`, `set`, `frozenset`, where the first arg is\nitself an `ast.Call` to `list`, `dict`, `tuple`, or `set` with\nan `ast.GeneratorExp` as its first argument.\n\n**Why it matters**: `max(list(g))` allocates the full list, then\nwalks it. The wrapper is redundant: reducers accept generators\ndirectly.\n\n**Fix**:\n\n```python\n# Before\nreturn max(list(x * 2 for x in xs))\n\n# After\nreturn max(x * 2 for x in xs)\n```\n\nFor `sorted` / `set` the wrapper is sometimes intentional (to\nforce evaluation), but it's still cheaper to let `sorted` /\n`set` consume the generator directly.\n\n## S3: Per-iteration allocation inside a loop (MEDIUM)\n\n**AST shape**: `ast.Call` inside a loop body where either:\n\n- The `func` is an `ast.Attribute` with name `copy`, or\n- The `func` is an `ast.Name` of `dict`, `list`, or `tuple`\n  with a non-comprehension first argument (the comprehension\n  case is a builder, not a copy).\n\n**Why it matters**: `base.copy()` per iteration allocates a new\ncontainer N times. If only one or two fields change per\niteration, a single allocation outside the loop with selective\nmutation costs less.\n\n**Fix**: Hoist when possible.\n\n```python\n# Before\nfor x in items:\n    snapshot = base.copy()\n    snapshot[\"key\"] = x\n    out.append(snapshot)\n\n# After (when downstream tolerates shared dict identity):\nshared = {**base}\nfor x in items:\n    shared[\"key\"] = x\n    out.append(dict(shared))  # explicit copy at the boundary\n```\n\nWhen the snapshots must be inde"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":"Detects time and space complexity hotspots via AST scan Skill: performance-review Owner: athola Summary: Detects time and space complexity hotspots via AST scan Tags: latest:1.9.19 Version history: v1.9.19 | 2026-08-26T13:19:06.422Z | user Release v1.9.19 v1.9.17 | 2026-07-30T05:39:18.228Z | user Release v1.9.17 v1.9.16 | 2026-07-14T19:56:01.066Z | user Release v1.9.16 v1.9.14 | 2026-06-30T18:04:19.616Z | user Release v1.9.14 v1.9.13 | 2026-06-27T16:22:19.651Z | user Rele","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1733,"uniquenessScore":51,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-11T17:49:39.875Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-11T17:49:39.875Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-11T21:00:00.305Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}