From 69eaa24de2e998c045ba2b33b6391d44d639da70 Mon Sep 17 00:00:00 2001 From: Kailas Mahavarkar <66670953+KailasMahavarkar@users.noreply.github.com> Date: Mon, 15 Jun 2026 21:55:49 +0530 Subject: [PATCH] feat(optimizer): add optimizer plugin + skill - teaches runtime analysis + algorithm derivation, catalog confirms technique, websearch the impl --- .../runtime-context/hyperstack.bootstrap.md | 2 + scripts/audit/sources.ts | 1 + skills/INDEX.md | 1 + skills/hyperstack/SKILL.md | 2 + skills/optimizer/SKILL.md | 97 +++++++++ src/index.ts | 2 + src/plugins/optimizer/data.ts | 198 ++++++++++++++++++ src/plugins/optimizer/index.ts | 20 ++ src/plugins/optimizer/tools/get-technique.ts | 33 +++ src/plugins/optimizer/tools/list-classes.ts | 20 ++ .../optimizer/tools/list-techniques.ts | 23 ++ src/plugins/optimizer/tools/match-problem.ts | 35 ++++ src/plugins/optimizer/tools/search.ts | 25 +++ tests/plugin-registry-behaviour.test.ts | 4 +- 14 files changed, 461 insertions(+), 2 deletions(-) create mode 100644 skills/optimizer/SKILL.md create mode 100644 src/plugins/optimizer/data.ts create mode 100644 src/plugins/optimizer/index.ts create mode 100644 src/plugins/optimizer/tools/get-technique.ts create mode 100644 src/plugins/optimizer/tools/list-classes.ts create mode 100644 src/plugins/optimizer/tools/list-techniques.ts create mode 100644 src/plugins/optimizer/tools/match-problem.ts create mode 100644 src/plugins/optimizer/tools/search.ts diff --git a/generated/runtime-context/hyperstack.bootstrap.md b/generated/runtime-context/hyperstack.bootstrap.md index 0a4716b..87137d0 100644 --- a/generated/runtime-context/hyperstack.bootstrap.md +++ b/generated/runtime-context/hyperstack.bootstrap.md @@ -48,6 +48,7 @@ Hyperstack is a **Three-Layer Ecosystem**: - `echo_*` -> `echo_get_recipe`, `echo_get_middleware`, `echo_decision_matrix` - `golang_*` -> `golang_get_practice`, `golang_get_pattern`, `golang_get_antipatterns` - `rust_*` -> `rust_get_practice`, `rust_cheatsheet`, `rust_search_docs` +- `optimizer_*` -> `optimizer_match_problem`, `optimizer_get_technique`, `optimizer_list_classes` ## Workflow Skills - `hyperstack:blueprint`: Before any feature build - MCP survey, design gate, negative doubt @@ -70,6 +71,7 @@ Hyperstack is a **Three-Layer Ecosystem**: - `hyperstack:security-review`: OWASP audits, API and infrastructure security - `hyperstack:readme-writer`: Evidence-based documentation - `hyperstack:codemode`: Understanding an unfamiliar codebase before reviewing or changing it - 7-phase context load +- `hyperstack:optimizer`: Algorithmic optimization - match the problem to the right DSA technique, suggest the complexity win (evidence-gated, not premature) ## Internal Roles - Roles are internal and auto-called. Users do not invoke them directly. diff --git a/scripts/audit/sources.ts b/scripts/audit/sources.ts index 5ae4ec2..33dd6ae 100644 --- a/scripts/audit/sources.ts +++ b/scripts/audit/sources.ts @@ -40,5 +40,6 @@ export const SOURCES: PluginSource[] = [ { plugin: "rust", editorial: true, skip: false, skills: [], packages: [] }, { plugin: "ui-ux", editorial: true, skip: false, skills: [], packages: [] }, { plugin: "designer", editorial: true, skip: false, skills: ["designer"], packages: [] }, + { plugin: "optimizer", editorial: true, skip: false, skills: ["optimizer"], packages: [] }, { plugin: "hyperstack", editorial: false, skip: true, skills: [], packages: [] }, ]; diff --git a/skills/INDEX.md b/skills/INDEX.md index 8ea97ed..f4974a5 100644 --- a/skills/INDEX.md +++ b/skills/INDEX.md @@ -22,6 +22,7 @@ Categories: | `deliver` | Use after all implementation tasks are complete. Runs final verification, confirms the branch is clean, detects the work | | `engineering-discipline` | Apply senior-level software engineering discipline including design patterns, SOLID principles, architectural reasoning, | | `forge-plan` | Use after blueprint design approval to produce a task-by-task implementation plan grounded in MCP-verified API calls. No | +| `optimizer` | Teaches runtime analysis - deriving Big-O straight from code - and how to derive a better algorithm by removing redundan | | `parallel-dispatch` | Use when facing 2+ independent tasks that can be investigated or executed without shared state or sequential dependencie | | `run-plan` | Use when you have an existing plan, spec, or task list to execute. Validates the plan for gaps and MCP accuracy before a | | `ship-gate` | Use before claiming any work is complete, fixed, or passing. Run the verification command and show output before making | diff --git a/skills/hyperstack/SKILL.md b/skills/hyperstack/SKILL.md index feb8bd3..adf9f19 100644 --- a/skills/hyperstack/SKILL.md +++ b/skills/hyperstack/SKILL.md @@ -97,6 +97,7 @@ Call these BEFORE writing any code for these stacks. **Memory is not acceptable. | `echo_*` | Echo (Go HTTP) | `echo_get_recipe`, `echo_get_middleware`, `echo_decision_matrix` | | `golang_*` | Go best practices | `golang_get_practice`, `golang_get_pattern`, `golang_get_antipatterns` | | `rust_*` | Rust practices | `rust_get_practice`, `rust_cheatsheet`, `rust_search_docs` | +| `optimizer_*` | Algorithm/DSA selection (the menu, not the impl) | `optimizer_match_problem`, `optimizer_get_technique`, `optimizer_list_classes` | ### MCP Degraded Mode @@ -157,6 +158,7 @@ This is non-negotiable. Silent skill invocations are invisible to the user and c | `hyperstack:security-review` | OWASP audits, API and infrastructure security | | `hyperstack:readme-writer` | Evidence-based documentation | | `hyperstack:codemode` | Understanding an unfamiliar codebase before reviewing or changing it - 7-phase context load | +| `hyperstack:optimizer` | Algorithmic optimization - match the problem to the right DSA technique, suggest the complexity win (evidence-gated, not premature) | ### Workflow Chain diff --git a/skills/optimizer/SKILL.md b/skills/optimizer/SKILL.md new file mode 100644 index 0000000..be61f2f --- /dev/null +++ b/skills/optimizer/SKILL.md @@ -0,0 +1,97 @@ +--- +name: optimizer +category: core +description: Teaches runtime analysis - deriving Big-O straight from code - and how to derive a better algorithm by removing redundant work, then confirms the technique via the optimizer_* catalog and web-searches the implementation. Suggests the complexity win as Big-O before -> after. Evidence-gated, not premature - stays quiet when the naive solution is correctly the lazy-right answer. Use when asked to optimize, "make it faster", "better algorithm", "what's the complexity", or when a hot path / large-n / nested-loop / known-slow pattern shows up in review. +--- + +# Optimizer - Algorithmic Lens + +Hyperstack already makes code current, correct, designed, and minimal. This is the missing axis: **the right algorithm and data structure for the problem, proven in complexity terms.** A lens, not a gate. + +It does not hand you a list of algorithms to copy. It teaches two transferable skills - **(1) derive the runtime, (2) derive a better algorithm by removing redundant work** - and uses the `optimizer_*` catalog only to confirm the named technique and point you at the authoritative implementation. The thinking is the skill; the catalog is the lookup. + +## 1. Runtime analysis (derive it, never guess) + +Read the Big-O off the code. Cost model: + +| Code shape | Complexity | +|---|---| +| sequential statements | add, dominant term wins | +| single loop to n | O(n) | +| two nested loops to n (all pairs, all subarrays) | O(n^2) | +| loop that halves/doubles the range each step | O(log n) | +| loop to n with an O(log n) op inside (binary search, heap push, set in a balanced tree) | O(n log n) | +| binary recursion on n/2 + linear merge: `T(n)=2T(n/2)+O(n)` | O(n log n) | +| linear recursion: `T(n)=T(n-1)+O(1)` | O(n) | +| branching recursion ×2, depth n: `T(n)=2T(n-1)+O(1)` | O(2^n) (naive Fibonacci, all subsets) | +| all permutations | O(n!) | + +Rules: +- **Drop constants and lower-order terms.** `O(n + n log n) = O(n log n)`. `O(2n) = O(n)`. +- **Recursion -> recurrence -> Master Theorem.** `T(n)=aT(n/b)+f(n)`: compare `f(n)` to `n^(log_b a)`. Or draw the recursion tree and sum the levels. +- **Amortized != worst-case-per-op.** Dynamic-array push is O(1) amortized, not O(n). Union-find op is ~O(alpha(n)). A loop where an inner pointer only ever *advances* (two-pointer, sliding window) is O(n) total - not O(n^2) - because the inner index moves at most n times across the whole run. +- **Space:** count auxiliary allocation - recursion stack depth (`O(h)`), aux arrays, a memo/DP table (`O(states)`). In-place = `O(1)` aux. + +**Then find the bottleneck:** the single highest-order term IS what to attack. Usually a loop or recursion doing repeated work. Everything below it is noise until that term is lowered. + +## 2. The improvement playbook (derive the better algorithm) + +You do not pick an algorithm from a list - you *remove redundant work*. The one question: + +> **What is this code computing repeatedly that it could compute once?** + +Map the redundancy to its fix - that *names the technique class*; then confirm + fetch the impl from the catalog. + +| Redundant work (the smell) | The idea (remove the repetition) | Typical lift | +|---|---|---| +| re-searching a collection every step | hash for O(1) lookup, or sort once + binary search | O(n^2) -> O(n) / O(n log n) | +| re-summing / re-aggregating a range per query | precompute prefix sums | O(n)/query -> O(1) | +| recomputing overlapping subproblems | memoize / tabulate (DP) | exponential -> polynomial | +| re-finding the min/max repeatedly | heap, or monotonic stack/deque | O(n^2) -> O(n log n) / O(n) | +| comparing all pairs in ordered/sorted data | two pointers / sliding window | O(n^2) -> O(n) | +| repeated connectivity / grouping queries | union-find | O(V+E)/query -> ~O(1) | +| exhaustive search with structure | prune (backtracking), greedy if an exchange argument holds, or DP if optimal substructure holds | n! -> tractable | +| repeated shortest-path / level queries | BFS (unweighted) / Dijkstra (weighted) | re-scan -> O(V+E) / O(E log V) | + +### The derivation loop + +1. **Derive** the current complexity (section 1). +2. **Locate** the dominant cost - the loop/recursion that produces the top term. +3. **Name the redundancy** - what work repeats across its iterations? +4. **Map** the redundancy to its fix (table above) -> this yields the technique *class*. +5. **Confirm + fetch:** `optimizer_match_problem` / `optimizer_get_technique` to confirm the named technique and get the web-search query; **web-search the implementation** (off-by-ones and edge cases are where memory fails) and hand to the language plugin (`golang_*`, `rust_*`, `react_*`) for idiomatic code. +6. **Re-derive** the new complexity to *prove* the win. Present Big-O before -> after. + +## When NOT to apply (the restraint gate) + +Gated by Coding Law 0 / YAGNI. **Do not optimize what does not need it.** A naive loop over 10 items is correctly the lazy-right answer. Skip when input is provably small, the path is cold, or the code is correct and clear. Premature optimization is its own slop - the opposite of this skill. When you skip, say why (e.g. "O(n^2) but n<=50 on a cold path - leaving it"). + +## MCP tools (the lookup, after you have reasoned) + +| Tool | Purpose | +|---|---| +| `optimizer_match_problem` | problem -> candidate classes + techniques | +| `optimizer_list_classes` | the taxonomy | +| `optimizer_list_techniques` | the menu, optionally per class | +| `optimizer_get_technique` | complexity + naive-smell + web-search query for the impl | +| `optimizer_search` | free-text over the catalog | + +## Position in the harness + +| Connection | Wiring | +|---|---| +| Gated by | Coding Law 0 / YAGNI | +| Deepens | `engineering-discipline` Step 8 (negative doubt: "try a better alternative") | +| Adds a dimension to | `code-review` - "right algorithm + complexity?" | +| Hands off to | `golang_*` / `rust_*` / `react_*` for idiomatic implementation | +| Verifies via | web search (catalog ships no code - algorithms are stable, specifics get checked) | + +## Red flags - STOP + +| Thought | Reality | +|---|---| +| "It's roughly O(n) I think" | Derive it. Count the loops, write the recurrence. Guessed Big-O is how slow code ships. | +| "I'll write the algorithm from memory" | Reason to the *class* yourself; web-search the *implementation*. Edge cases are where memory fails. | +| "Faster is always better, optimize it" | No. Premature optimization is slop. Gate on scale + hot path. | +| "It's O(n^2) but the input is tiny" | Then leave it, and say so. Correct + clear beats clever for n=10. | +| "I'll claim it's faster" | Prove it: Big-O before -> after, both derived. | diff --git a/src/index.ts b/src/index.ts index eb53e7c..b0648fc 100755 --- a/src/index.ts +++ b/src/index.ts @@ -15,6 +15,7 @@ import { uiUxPlugin } from "./plugins/ui-ux/index.js"; import { designerPlugin } from "./plugins/designer/index.js"; import { shadcnPlugin } from "./plugins/shadcn/index.js"; import { hyperstackPlugin } from "./plugins/hyperstack/index.js"; +import { optimizerPlugin } from "./plugins/optimizer/index.js"; import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; @@ -42,6 +43,7 @@ export const allPlugins = [ designerPlugin, shadcnPlugin, hyperstackPlugin, + optimizerPlugin, ]; loadPlugins(server, allPlugins); diff --git a/src/plugins/optimizer/data.ts b/src/plugins/optimizer/data.ts new file mode 100644 index 0000000..bc2a6de --- /dev/null +++ b/src/plugins/optimizer/data.ts @@ -0,0 +1,198 @@ +// Optimizer plugin - the DSA/algorithm MENU, not the implementations. +// +// Models already know how to write BFS. This catalog exists to make the agent +// RECALL the right technique for a problem and VERIFY its specifics via web +// search, instead of shipping a naive O(n^2) when an O(n) technique exists. +// It carries only stable metadata: name, complexity, the signal that flags the +// problem class, the naive smell it replaces, and a web-search hint for the +// authoritative implementation. No code lives here on purpose. + +export const ALGO_CLASSES = [ + "arrays-strings", "hashing", "sorting", "binary-search", "linked-list", + "stack-queue", "trees", "heap-pq", "graphs", "dp", "greedy", + "backtracking", "bit", "math", "intervals", "tries", "strings-advanced", + "ds-design", +] as const; +export type AlgoClass = (typeof ALGO_CLASSES)[number]; + +export interface Technique { + name: string; + class: AlgoClass; + time: string; + space: string; + when: string; // the signal that this technique fits the problem + replaces: string; // the naive smell it lifts (the optimizer's teeth) + websearch: string; // query hint for the authoritative impl + edge cases +} + +export interface ProblemSignal { + class: AlgoClass; + signals: string[]; // lowercase keywords/phrases that flag this class + candidates: string[]; // technique names to consider +} + +export const TECHNIQUES: Technique[] = [ + // ARRAYS / STRINGS + { name: "two-pointer-opposite", class: "arrays-strings", time: "O(n)", space: "O(1)", when: "sorted array, find pair/triplet to target, reverse, palindrome check", replaces: "nested-loop O(n^2) pair search", websearch: "two pointer technique opposite ends" }, + { name: "two-pointer-same-direction", class: "arrays-strings", time: "O(n)", space: "O(1)", when: "in-place filter/partition, remove duplicates from sorted", replaces: "extra array O(n) or O(n^2) shifting", websearch: "fast slow two pointer remove duplicates in place" }, + { name: "sliding-window-fixed", class: "arrays-strings", time: "O(n)", space: "O(1)", when: "max/sum/avg over fixed-size-k subarray", replaces: "recompute each window O(n*k)", websearch: "fixed size sliding window" }, + { name: "sliding-window-variable", class: "arrays-strings", time: "O(n)", space: "O(k)", when: "longest/shortest subarray or substring under a constraint", replaces: "nested-loop O(n^2) substring scan", websearch: "variable size sliding window longest substring" }, + { name: "prefix-sum", class: "arrays-strings", time: "O(n) build, O(1) query", space: "O(n)", when: "many range-sum / range-aggregate queries on static data", replaces: "recompute range each query O(n) per query", websearch: "prefix sum array range query" }, + { name: "kadane", class: "arrays-strings", time: "O(n)", space: "O(1)", when: "maximum subarray sum", replaces: "O(n^2) all-subarrays scan", websearch: "Kadane algorithm maximum subarray" }, + { name: "dutch-national-flag", class: "arrays-strings", time: "O(n)", space: "O(1)", when: "3-way partition / sort 0s-1s-2s in one pass", replaces: "full sort O(n log n) for only 3 categories", websearch: "Dutch national flag three way partition" }, + + // HASHING + { name: "hash-set-dedup", class: "hashing", time: "O(n)", space: "O(n)", when: "membership / dedup / seen-before on unsorted data", replaces: "nested-loop O(n^2) dedup", websearch: "hash set deduplication pattern" }, + { name: "hash-map-frequency", class: "hashing", time: "O(n)", space: "O(n)", when: "counting, anagrams, group-by, first-unique", replaces: "repeated linear scans O(n^2)", websearch: "frequency counter hashmap" }, + { name: "two-sum-hashmap", class: "hashing", time: "O(n)", space: "O(n)", when: "find pair summing to target in UNSORTED data", replaces: "nested-loop O(n^2)", websearch: "two sum hashmap one pass" }, + + // SORTING + { name: "introsort", class: "sorting", time: "O(n log n)", space: "O(log n)", when: "general-purpose comparison sort (most stdlib sorts)", replaces: "bubble/insertion/selection O(n^2)", websearch: "introsort quicksort heapsort hybrid" }, + { name: "merge-sort", class: "sorting", time: "O(n log n)", space: "O(n)", when: "stable sort needed, linked lists, external/streamed sort", replaces: "O(n^2) sorts; unstable quicksort when stability matters", websearch: "merge sort stable" }, + { name: "counting-sort", class: "sorting", time: "O(n+k)", space: "O(k)", when: "integer keys in a small known range k", replaces: "comparison sort O(n log n) when range is small", websearch: "counting sort" }, + { name: "radix-sort", class: "sorting", time: "O(d*(n+k))", space: "O(n+k)", when: "fixed-width integer or string keys", replaces: "comparison sort O(n log n)", websearch: "radix sort LSD" }, + { name: "bucket-sort", class: "sorting", time: "O(n) avg", space: "O(n)", when: "uniformly distributed floats in a range", replaces: "comparison sort O(n log n)", websearch: "bucket sort uniform distribution" }, + + // BINARY SEARCH + { name: "binary-search", class: "binary-search", time: "O(log n)", space: "O(1)", when: "search in a sorted collection", replaces: "linear scan O(n)", websearch: "binary search" }, + { name: "lower-upper-bound", class: "binary-search", time: "O(log n)", space: "O(1)", when: "first/last position, insertion point, count of value", replaces: "linear scan O(n)", websearch: "lower bound upper bound binary search" }, + { name: "binary-search-on-answer", class: "binary-search", time: "O(n log(range))", space: "O(1)", when: "minimize-the-max / maximize-the-min with a monotone feasibility check", replaces: "brute force over the answer space", websearch: "binary search on answer parametric search" }, + { name: "search-rotated", class: "binary-search", time: "O(log n)", space: "O(1)", when: "search in a rotated sorted array", replaces: "linear scan O(n)", websearch: "search in rotated sorted array" }, + + // LINKED LIST + { name: "fast-slow-pointer", class: "linked-list", time: "O(n)", space: "O(1)", when: "cycle detection, find middle, nth-from-end", replaces: "two passes or extra storage O(n)", websearch: "Floyd cycle detection fast slow pointer" }, + { name: "linked-list-reversal", class: "linked-list", time: "O(n)", space: "O(1)", when: "reverse a list or sublist in place", replaces: "copy to array O(n) space", websearch: "reverse linked list in place" }, + + // STACK / QUEUE + { name: "monotonic-stack", class: "stack-queue", time: "O(n)", space: "O(n)", when: "next/previous greater-or-smaller element, largest rectangle in histogram", replaces: "nested-loop O(n^2)", websearch: "monotonic stack next greater element" }, + { name: "monotonic-deque", class: "stack-queue", time: "O(n)", space: "O(k)", when: "sliding window maximum/minimum", replaces: "recompute each window O(n*k) or heap O(n log k)", websearch: "monotonic deque sliding window maximum" }, + { name: "min-max-stack", class: "stack-queue", time: "O(1) per op", space: "O(n)", when: "track running min/max alongside a stack", replaces: "recompute min O(n)", websearch: "min stack constant time minimum" }, + + // TREES + { name: "tree-dfs", class: "trees", time: "O(n)", space: "O(h)", when: "traversal (in/pre/post-order), path sums, height, validate", replaces: "ad-hoc recursion without structure", websearch: "binary tree DFS recursive iterative" }, + { name: "tree-bfs-level", class: "trees", time: "O(n)", space: "O(w)", when: "level-order, min-depth, right-side view, shortest path in a tree", replaces: "DFS then post-process levels", websearch: "binary tree level order BFS queue" }, + { name: "bst-operations", class: "trees", time: "O(h)", space: "O(h)", when: "ordered insert/search/delete, range queries on ordered data", replaces: "linear scan O(n) of an unsorted structure", websearch: "binary search tree operations balanced" }, + { name: "lowest-common-ancestor", class: "trees", time: "O(n) / O(log n) with prep", space: "O(h)", when: "LCA of two nodes, distance between nodes", replaces: "naive root-to-node path compare", websearch: "lowest common ancestor binary tree binary lifting" }, + + // HEAP / PRIORITY QUEUE + { name: "heap-top-k", class: "heap-pq", time: "O(n log k)", space: "O(k)", when: "k largest/smallest, k-th element", replaces: "full sort O(n log n)", websearch: "top k elements min heap" }, + { name: "merge-k-sorted", class: "heap-pq", time: "O(n log k)", space: "O(k)", when: "merge k sorted lists/streams", replaces: "concatenate then sort O(n log n)", websearch: "merge k sorted lists heap" }, + { name: "two-heaps-median", class: "heap-pq", time: "O(log n) insert, O(1) query", space: "O(n)", when: "running median of a stream", replaces: "sort on every query O(n log n)", websearch: "find median from data stream two heaps" }, + + // GRAPHS + { name: "graph-bfs", class: "graphs", time: "O(V+E)", space: "O(V)", when: "shortest path in UNWEIGHTED graph, levels, nearest", replaces: "DFS then take min, or repeated scans", websearch: "graph BFS shortest path unweighted" }, + { name: "graph-dfs", class: "graphs", time: "O(V+E)", space: "O(V)", when: "connectivity, cycle detection, path existence, flood fill", replaces: "ad-hoc recursion", websearch: "graph DFS cycle detection" }, + { name: "topological-sort", class: "graphs", time: "O(V+E)", space: "O(V)", when: "dependency / build order on a DAG, course schedule", replaces: "guessing order or repeated passes", websearch: "topological sort Kahn algorithm" }, + { name: "dijkstra", class: "graphs", time: "O(E log V)", space: "O(V)", when: "shortest path with NON-NEGATIVE weights", replaces: "Bellman-Ford O(VE) when no negatives", websearch: "Dijkstra shortest path priority queue" }, + { name: "bellman-ford", class: "graphs", time: "O(VE)", space: "O(V)", when: "shortest path with NEGATIVE edges, negative-cycle detection", replaces: "Dijkstra (which is wrong with negatives)", websearch: "Bellman-Ford negative weight edges" }, + { name: "floyd-warshall", class: "graphs", time: "O(V^3)", space: "O(V^2)", when: "ALL-PAIRS shortest path on a small/dense graph", replaces: "running Dijkstra from every node", websearch: "Floyd Warshall all pairs shortest path" }, + { name: "union-find", class: "graphs", time: "~O(alpha(n)) per op", space: "O(n)", when: "dynamic connectivity, cycle in undirected, grouping, Kruskal", replaces: "BFS/DFS per connectivity query O(V+E)", websearch: "union find disjoint set union path compression rank" }, + { name: "mst-kruskal-prim", class: "graphs", time: "O(E log V)", space: "O(V)", when: "minimum spanning tree (Kruskal sparse, Prim dense)", replaces: "brute force spanning trees", websearch: "minimum spanning tree Kruskal Prim" }, + { name: "tarjan-scc", class: "graphs", time: "O(V+E)", space: "O(V)", when: "strongly connected components, 2-SAT, condensation", replaces: "naive reachability O(V*(V+E))", websearch: "Tarjan strongly connected components" }, + + // DYNAMIC PROGRAMMING + { name: "dp-memoization", class: "dp", time: "O(states * transition)", space: "O(states)", when: "overlapping subproblems, optimal substructure, top-down", replaces: "exponential plain recursion (e.g. recursive Fibonacci)", websearch: "memoization top down dynamic programming" }, + { name: "dp-tabulation", class: "dp", time: "O(states * transition)", space: "O(states), often O(width)", when: "bottom-up DP, when iteration order is clear and space can be rolled", replaces: "exponential recursion; deep recursion stack", websearch: "tabulation bottom up dynamic programming space optimization" }, + { name: "knapsack-01", class: "dp", time: "O(n*W)", space: "O(W)", when: "pick subset under a capacity/weight budget", replaces: "exponential 2^n subset enumeration", websearch: "0/1 knapsack dynamic programming" }, + { name: "lis", class: "dp", time: "O(n log n)", space: "O(n)", when: "longest increasing subsequence and variants", replaces: "O(n^2) DP or O(2^n) brute force", websearch: "longest increasing subsequence patience sorting binary search" }, + { name: "lcs-edit-distance", class: "dp", time: "O(n*m)", space: "O(min(n,m))", when: "sequence alignment, diff, edit distance, similarity", replaces: "exponential recursion", websearch: "edit distance longest common subsequence DP" }, + { name: "coin-change", class: "dp", time: "O(n*amount)", space: "O(amount)", when: "min coins / number of ways to make a sum", replaces: "exponential recursion", websearch: "coin change dynamic programming" }, + { name: "bitmask-dp", class: "dp", time: "O(2^n * n)", space: "O(2^n)", when: "small-n (<=20) subset states: TSP, assignment, set cover", replaces: "brute force n! permutations", websearch: "bitmask DP traveling salesman assignment" }, + { name: "interval-dp", class: "dp", time: "O(n^3)", space: "O(n^2)", when: "matrix-chain, burst balloons, optimal merge over intervals", replaces: "exponential recursion", websearch: "interval DP matrix chain multiplication" }, + + // GREEDY + { name: "interval-scheduling", class: "greedy", time: "O(n log n)", space: "O(1)", when: "max non-overlapping intervals (sort by earliest finish)", replaces: "brute force / DP when greedy is provably optimal", websearch: "interval scheduling greedy earliest finish time" }, + { name: "activity-exchange-argument", class: "greedy", time: "O(n log n)", space: "O(1)", when: "selection/ordering where a local optimal choice is provably global", replaces: "DP/brute force when an exchange argument holds", websearch: "greedy exchange argument activity selection" }, + { name: "huffman-coding", class: "greedy", time: "O(n log n)", space: "O(n)", when: "optimal prefix codes, minimum-cost merge of items", replaces: "fixed-length encoding / brute force merge order", websearch: "Huffman coding greedy priority queue" }, + + // BACKTRACKING + { name: "backtracking-enumerate", class: "backtracking", time: "O(2^n) / O(n!)", space: "O(n)", when: "all subsets, combinations, permutations", replaces: "ad-hoc nested loops that do not generalize", websearch: "backtracking subsets combinations permutations" }, + { name: "backtracking-prune", class: "backtracking", time: "exponential with pruning", space: "O(n)", when: "constraint search: N-queens, sudoku, word search", replaces: "brute force over all configurations", websearch: "backtracking constraint pruning N queens sudoku" }, + + // BIT MANIPULATION + { name: "bit-tricks", class: "bit", time: "O(1)", space: "O(1)", when: "flags/sets, parity, power-of-two test, lowest set bit, count bits", replaces: "loops and arithmetic for set membership", websearch: "bit manipulation tricks cheat sheet" }, + { name: "xor-find-unique", class: "bit", time: "O(n)", space: "O(1)", when: "find the single/missing number among pairs", replaces: "hashing O(n) space", websearch: "XOR find single number missing number" }, + { name: "subset-enumeration-bitmask", class: "bit", time: "O(2^n)", space: "O(1)", when: "iterate all subsets of a small set", replaces: "recursive subset generation", websearch: "iterate all subsets bitmask submask enumeration" }, + + // MATH / NUMBER THEORY + { name: "euclid-gcd", class: "math", time: "O(log min(a,b))", space: "O(1)", when: "gcd/lcm, simplify fractions, periodicity", replaces: "naive factor loop", websearch: "Euclidean algorithm GCD" }, + { name: "sieve-of-eratosthenes", class: "math", time: "O(n log log n)", space: "O(n)", when: "all primes up to n, smallest prime factor", replaces: "trial division per number O(n*sqrt(n))", websearch: "Sieve of Eratosthenes primes" }, + { name: "modular-exponentiation", class: "math", time: "O(log e)", space: "O(1)", when: "a^e mod m for large e (hashing, crypto, combinatorics mod p)", replaces: "naive O(e) multiply loop", websearch: "modular exponentiation fast power binary exponentiation" }, + + // INTERVALS + { name: "merge-intervals", class: "intervals", time: "O(n log n)", space: "O(n)", when: "merge overlapping intervals, insert interval", replaces: "nested-loop O(n^2) overlap checks", websearch: "merge intervals sort by start" }, + { name: "sweep-line", class: "intervals", time: "O(n log n)", space: "O(n)", when: "max concurrent intervals, meeting rooms, skyline, segment overlaps", replaces: "brute force check at every point", websearch: "sweep line algorithm events intervals" }, + + // TRIES + { name: "trie-prefix", class: "tries", time: "O(L) per op", space: "O(alphabet * total length)", when: "prefix search, autocomplete, dictionary of words, word break", replaces: "scanning all words per query, repeated string compares", websearch: "trie prefix tree autocomplete" }, + { name: "xor-trie", class: "tries", time: "O(L) per op", space: "O(N * L)", when: "maximum XOR pair, XOR range queries", replaces: "O(n^2) pair comparison", websearch: "XOR trie maximum xor of two numbers" }, + + // ADVANCED STRINGS + { name: "kmp", class: "strings-advanced", time: "O(n+m)", space: "O(m)", when: "single-pattern substring search, prefix-function uses", replaces: "naive substring search O(n*m)", websearch: "KMP string matching prefix function" }, + { name: "rabin-karp", class: "strings-advanced", time: "O(n+m) avg", space: "O(1)", when: "substring search, multiple patterns, rolling hash dedup of substrings", replaces: "naive O(n*m)", websearch: "Rabin-Karp rolling hash substring" }, + { name: "z-algorithm", class: "strings-advanced", time: "O(n)", space: "O(n)", when: "pattern matching, string periodicity, distinct substrings", replaces: "naive O(n*m)", websearch: "Z algorithm string matching" }, + + // DATA-STRUCTURE DESIGN + { name: "lru-cache", class: "ds-design", time: "O(1) get/put", space: "O(capacity)", when: "bounded cache with least-recently-used eviction", replaces: "linear scan to find eviction victim O(n)", websearch: "LRU cache hashmap doubly linked list ordered map" }, + { name: "fenwick-bit", class: "ds-design", time: "O(log n) per op", space: "O(n)", when: "prefix sums / point updates that interleave", replaces: "recompute prefix O(n) per update", websearch: "Fenwick tree binary indexed tree" }, + { name: "segment-tree", class: "ds-design", time: "O(log n) per op", space: "O(n)", when: "range query + range update (sum/min/max/gcd), lazy propagation", replaces: "recompute range O(n) per query", websearch: "segment tree lazy propagation range query" }, + { name: "sparse-table", class: "ds-design", time: "O(1) query, O(n log n) build", space: "O(n log n)", when: "STATIC idempotent range query (min/max/gcd), RMQ", replaces: "O(n) per query or O(log n) segment tree when data is static", websearch: "sparse table range minimum query" }, +]; + +export const PROBLEM_SIGNALS: ProblemSignal[] = [ + { class: "arrays-strings", signals: ["sorted array", "pair sum", "two sum sorted", "subarray", "substring", "palindrome", "reverse in place", "max subarray", "contiguous", "window", "range sum"], candidates: ["two-pointer-opposite", "two-pointer-same-direction", "sliding-window-fixed", "sliding-window-variable", "prefix-sum", "kadane"] }, + { class: "hashing", signals: ["duplicate", "dedup", "seen before", "count occurrences", "frequency", "anagram", "group by", "unsorted pair", "two sum"], candidates: ["hash-set-dedup", "hash-map-frequency", "two-sum-hashmap"] }, + { class: "sorting", signals: ["sort", "order by", "small integer range", "rank", "kth after sort"], candidates: ["introsort", "merge-sort", "counting-sort", "radix-sort", "bucket-sort"] }, + { class: "binary-search", signals: ["sorted search", "find position", "insertion point", "minimize the maximum", "maximize the minimum", "rotated array", "feasible threshold", "monotonic"], candidates: ["binary-search", "lower-upper-bound", "binary-search-on-answer", "search-rotated"] }, + { class: "linked-list", signals: ["linked list", "cycle", "middle node", "nth from end", "reverse list"], candidates: ["fast-slow-pointer", "linked-list-reversal"] }, + { class: "stack-queue", signals: ["next greater", "previous smaller", "histogram", "valid parentheses", "sliding window max", "monotonic"], candidates: ["monotonic-stack", "monotonic-deque", "min-max-stack"] }, + { class: "trees", signals: ["tree", "binary tree", "traversal", "level order", "ancestor", "subtree", "bst", "in-order"], candidates: ["tree-dfs", "tree-bfs-level", "bst-operations", "lowest-common-ancestor"] }, + { class: "heap-pq", signals: ["k largest", "k smallest", "top k", "kth", "merge sorted lists", "running median", "priority", "schedule by priority"], candidates: ["heap-top-k", "merge-k-sorted", "two-heaps-median"] }, + { class: "graphs", signals: ["graph", "grid path", "shortest path", "connected components", "dependency", "course schedule", "weighted edges", "negative weight", "all pairs", "spanning tree", "island", "flood fill"], candidates: ["graph-bfs", "graph-dfs", "topological-sort", "dijkstra", "bellman-ford", "floyd-warshall", "union-find", "mst-kruskal-prim", "tarjan-scc"] }, + { class: "dp", signals: ["overlapping subproblems", "min cost", "max value", "number of ways", "longest", "edit distance", "knapsack", "subsequence", "optimal substructure", "exponential recursion"], candidates: ["dp-memoization", "dp-tabulation", "knapsack-01", "lis", "lcs-edit-distance", "coin-change", "bitmask-dp", "interval-dp"] }, + { class: "greedy", signals: ["intervals non-overlapping", "earliest finish", "minimum number of", "select maximum", "merge cost", "prefix code"], candidates: ["interval-scheduling", "activity-exchange-argument", "huffman-coding"] }, + { class: "backtracking", signals: ["all combinations", "all permutations", "all subsets", "generate all", "n-queens", "sudoku", "word search", "constraint"], candidates: ["backtracking-enumerate", "backtracking-prune"] }, + { class: "bit", signals: ["bitmask", "xor", "single number", "missing number", "power of two", "set bits", "parity"], candidates: ["bit-tricks", "xor-find-unique", "subset-enumeration-bitmask"] }, + { class: "math", signals: ["gcd", "lcm", "prime", "sieve", "modulo", "power mod", "combinatorics"], candidates: ["euclid-gcd", "sieve-of-eratosthenes", "modular-exponentiation"] }, + { class: "intervals", signals: ["intervals", "overlap", "meeting rooms", "merge ranges", "skyline", "max concurrent"], candidates: ["merge-intervals", "sweep-line"] }, + { class: "tries", signals: ["prefix", "autocomplete", "dictionary of words", "word break", "maximum xor pair"], candidates: ["trie-prefix", "xor-trie"] }, + { class: "strings-advanced", signals: ["substring search", "pattern matching", "string periodicity", "find pattern in text", "rolling hash"], candidates: ["kmp", "rabin-karp", "z-algorithm"] }, + { class: "ds-design", signals: ["design a cache", "lru", "range query with updates", "range update", "prefix sum with updates", "rmq"], candidates: ["lru-cache", "fenwick-bit", "segment-tree", "sparse-table"] }, +]; + +export function getTechnique(name: string): Technique | undefined { + return TECHNIQUES.find((t) => t.name.toLowerCase() === name.toLowerCase()); +} + +export function techniquesByClass(cls: AlgoClass): Technique[] { + return TECHNIQUES.filter((t) => t.class === cls); +} + +export interface ProblemMatch { + class: AlgoClass; + matchedSignals: string[]; + candidates: string[]; +} + +export function matchProblem(text: string): ProblemMatch[] { + const hay = text.toLowerCase(); + const matches: ProblemMatch[] = []; + for (const ps of PROBLEM_SIGNALS) { + const hit = ps.signals.filter((s) => hay.includes(s)); + if (hit.length > 0) { + matches.push({ class: ps.class, matchedSignals: hit, candidates: ps.candidates }); + } + } + return matches.sort((a, b) => b.matchedSignals.length - a.matchedSignals.length); +} + +export function searchTechniques(query: string): Technique[] { + const q = query.toLowerCase(); + return TECHNIQUES.filter( + (t) => + t.name.toLowerCase().includes(q) || + t.class.includes(q) || + t.when.toLowerCase().includes(q) || + t.replaces.toLowerCase().includes(q), + ); +} diff --git a/src/plugins/optimizer/index.ts b/src/plugins/optimizer/index.ts new file mode 100644 index 0000000..ca81bdb --- /dev/null +++ b/src/plugins/optimizer/index.ts @@ -0,0 +1,20 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { Plugin } from "../../registry.js"; +import { register as matchProblem } from "./tools/match-problem.js"; +import { register as getTechnique } from "./tools/get-technique.js"; +import { register as listTechniques } from "./tools/list-techniques.js"; +import { register as listClasses } from "./tools/list-classes.js"; +import { register as search } from "./tools/search.js"; + +function register(server: McpServer): void { + matchProblem(server); + getTechnique(server); + listTechniques(server); + listClasses(server); + search(server); +} + +export const optimizerPlugin: Plugin = { + name: "optimizer", + register, +}; diff --git a/src/plugins/optimizer/tools/get-technique.ts b/src/plugins/optimizer/tools/get-technique.ts new file mode 100644 index 0000000..470f91b --- /dev/null +++ b/src/plugins/optimizer/tools/get-technique.ts @@ -0,0 +1,33 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { getTechnique, TECHNIQUES } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "optimizer_get_technique", + "Get a technique's complexity, when it fits, the naive smell it replaces, and the web-search query to fetch its authoritative implementation. The catalog ships no code by design - algorithms are stable; verify the impl and exact complexity against a real source, not memory.", + { + name: z.string().describe("Technique name from the catalog, e.g. 'union-find', 'sliding-window-variable', 'dijkstra', 'lru-cache'."), + }, + async ({ name }) => { + const t = getTechnique(name); + if (!t) { + return { + content: [{ type: "text" as const, text: `Technique "${name}" not found.\n\nAvailable: ${TECHNIQUES.map((x) => x.name).join(", ")}` }], + isError: true, + }; + } + let text = `# ${t.name} [${t.class}]\n\n`; + text += `- **Time:** ${t.time}\n`; + text += `- **Space:** ${t.space}\n`; + text += `- **When it fits:** ${t.when}\n`; + text += `- **Replaces (the naive smell):** ${t.replaces}\n\n`; + text += `## Get the implementation\n\n`; + text += `Web search: \`${t.websearch}\`\n\n`; + text += `Confirm the exact complexity and edge cases against an authoritative source - do not trust memory for the specifics. Then hand to the language plugin (\`golang_*\`, \`rust_*\`, \`react_*\`, ...) for idiomatic code in the target stack.\n\n`; + text += `## Suggest with evidence\n\n`; + text += `Frame the improvement as Big-O before -> after, e.g. "this is O(n^2); '${t.name}' makes it ${t.time}". If the current code is already correct and the input is small, say so and leave it - efficiency is gated by evidence, not reflex.\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/optimizer/tools/list-classes.ts b/src/plugins/optimizer/tools/list-classes.ts new file mode 100644 index 0000000..1323d08 --- /dev/null +++ b/src/plugins/optimizer/tools/list-classes.ts @@ -0,0 +1,20 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ALGO_CLASSES, techniquesByClass } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "optimizer_list_classes", + "List the algorithmic problem classes in the catalog with how many techniques each holds. The taxonomy to orient on before matching a problem.", + {}, + async () => { + let text = `# Algorithmic classes (${ALGO_CLASSES.length})\n\n`; + text += `| class | techniques |\n|---|---|\n`; + for (const cls of ALGO_CLASSES) { + const names = techniquesByClass(cls).map((t) => t.name); + text += `| ${cls} | ${names.join(", ")} |\n`; + } + text += `\nUse \`optimizer_match_problem(description)\` to map a problem to candidates, or \`optimizer_list_techniques(class)\` to drill in.\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/optimizer/tools/list-techniques.ts b/src/plugins/optimizer/tools/list-techniques.ts new file mode 100644 index 0000000..528d6a6 --- /dev/null +++ b/src/plugins/optimizer/tools/list-techniques.ts @@ -0,0 +1,23 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { ALGO_CLASSES, TECHNIQUES, techniquesByClass, type AlgoClass } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "optimizer_list_techniques", + "List the technique menu, optionally filtered by class. Each row is name + complexity + when to reach for it. No implementations - this is the recall aid.", + { + class: z.enum(ALGO_CLASSES).optional().describe("Optional class filter, e.g. 'graphs', 'dp', 'binary-search'. Omit for the full menu."), + }, + async ({ class: cls }) => { + const list = cls ? techniquesByClass(cls as AlgoClass) : TECHNIQUES; + let text = cls ? `# Techniques: ${cls}\n\n` : `# All techniques (${TECHNIQUES.length})\n\n`; + text += `| technique | class | time | space | when |\n|---|---|---|---|---|\n`; + for (const t of list) { + text += `| \`${t.name}\` | ${t.class} | ${t.time} | ${t.space} | ${t.when} |\n`; + } + text += `\nUse \`optimizer_get_technique(name)\` for the web-search query to fetch an implementation.\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/optimizer/tools/match-problem.ts b/src/plugins/optimizer/tools/match-problem.ts new file mode 100644 index 0000000..77eecd1 --- /dev/null +++ b/src/plugins/optimizer/tools/match-problem.ts @@ -0,0 +1,35 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { matchProblem, getTechnique } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "optimizer_match_problem", + "Match a problem description (or what the code does) to likely algorithmic classes and the candidate techniques to consider. Returns the menu to scan; the implementation is not shipped on purpose - web search the technique you pick.", + { + description: z + .string() + .describe("The problem statement or what the code does, in terms of data shape + operation. e.g. 'find a pair in an unsorted array summing to target', 'shortest path in a weighted graph', 'longest substring without repeating chars'."), + }, + async ({ description }) => { + const matches = matchProblem(description); + if (matches.length === 0) { + return { + content: [{ type: "text" as const, text: "No strong class match. Re-describe by data shape + operation (search / sort / path / count / range / subsequence), or call `optimizer_list_classes` to scan the full menu." }], + }; + } + let text = `# Candidate techniques\n\nMatched ${matches.length} class(es). Scan the menu, then apply restraint: only swap in a faster technique if scale and a hot path actually warrant it (YAGNI - a naive loop over 10 items is fine). For the one you choose, **web search its implementation** - do not write a non-trivial algorithm from memory.\n\n`; + for (const m of matches) { + text += `## ${m.class} (matched: ${m.matchedSignals.join(", ")})\n\n`; + text += `| technique | time | space | replaces |\n|---|---|---|---|\n`; + for (const name of m.candidates) { + const t = getTechnique(name); + if (t) text += `| \`${t.name}\` | ${t.time} | ${t.space} | ${t.replaces} |\n`; + } + text += `\n`; + } + text += `Next: \`optimizer_get_technique(name)\` for the one you pick - it gives the web-search query for the authoritative impl.\n`; + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/src/plugins/optimizer/tools/search.ts b/src/plugins/optimizer/tools/search.ts new file mode 100644 index 0000000..60deb1f --- /dev/null +++ b/src/plugins/optimizer/tools/search.ts @@ -0,0 +1,25 @@ +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { searchTechniques } from "../data.js"; + +export function register(server: McpServer): void { + server.tool( + "optimizer_search", + "Free-text search across the technique catalog (name, class, when-to-use, and the naive smell it replaces).", + { + query: z.string().describe("e.g. 'shortest path', 'dedup', 'range query', 'kth largest', 'overlapping intervals'."), + }, + async ({ query }) => { + const hits = searchTechniques(query); + if (hits.length === 0) { + return { content: [{ type: "text" as const, text: `No technique matched "${query}". Try \`optimizer_list_classes\` or \`optimizer_match_problem\`.` }] }; + } + let text = `# Search: "${query}" (${hits.length})\n\n`; + text += `| technique | class | time | when | replaces |\n|---|---|---|---|---|\n`; + for (const t of hits) { + text += `| \`${t.name}\` | ${t.class} | ${t.time} | ${t.when} | ${t.replaces} |\n`; + } + return { content: [{ type: "text" as const, text }] }; + }, + ); +} diff --git a/tests/plugin-registry-behaviour.test.ts b/tests/plugin-registry-behaviour.test.ts index da7f6ef..79d25f1 100644 --- a/tests/plugin-registry-behaviour.test.ts +++ b/tests/plugin-registry-behaviour.test.ts @@ -17,10 +17,10 @@ function getRegisteredTools() { return tools; } -test("all 12 plugins register at least one tool", () => { +test("all 13 plugins register at least one tool", () => { const tools = getRegisteredTools(); const pluginPrefixes = new Set(tools.map((t) => t.name.split("_")[0])); - expect(pluginPrefixes.size).toBe(12); + expect(pluginPrefixes.size).toBe(13); }); test("every registered tool has a non-empty name and description", () => {