[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M751TX511EDMDTGC2Q2T23GH":3,"skill-same:01M751TX511EDMDTGC2Q2T23GH":49},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":26,"commitSha":27,"skillDirectory":28,"forksCount":29,"repositoryHtmlUrl":30,"repositoryHomepageUrl":31,"ownerHtmlUrl":32,"ownerBlogUrl":31,"ownerDescription":33,"updatedAt":34,"install":35,"resources":48},"01M751TX511EDMDTGC2Q2T23GH","spark-jobs",null,"Writes and tunes PySpark jobs - join strategy and broadcast size limits, shuffle-partition sizing, skew diagnosis and salting, UDF avoidance, caching, and output file layout - with concrete size and skew thresholds. Use when someone asks \"why is my Spark job slow\", \"should I broadcast this join\", \"one task takes forever while the rest finish\", \"my job OOMs during a join\", or is writing a new PySpark ETL job. Do NOT use for Kafka topic, consumer-group, or streaming-pipeline design - use kafka-pipelines instead; do NOT use for single-machine dataframe work that fits in memory - use pandas-expert instead.","SkillMedev\u002Fskills","SkillMedev","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F298078254?v=4","01M74ZSGTQ918CB0VXW4EG3RMX","SkillMe","01M74ZSGVBT497NA95CBT8JTY4",25,"MIT","https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fspark-jobs\u002FSKILL.md",[],[],"2026-11-11 18:22:23",false,"---\nname: spark-jobs\ndescription: Writes and tunes PySpark jobs - join strategy and broadcast size limits, shuffle-partition sizing, skew diagnosis and salting, UDF avoidance, caching, and output file layout - with concrete size and skew thresholds. Use when someone asks \"why is my Spark job slow\", \"should I broadcast this join\", \"one task takes forever while the rest finish\", \"my job OOMs during a join\", or is writing a new PySpark ETL job. Do NOT use for Kafka topic, consumer-group, or streaming-pipeline design - use kafka-pipelines instead; do NOT use for single-machine dataframe work that fits in memory - use pandas-expert instead.\nmetadata:\n  title: \"Spark & PySpark\"\n---\n\n# Spark & PySpark\n\nAlmost every slow Spark job is slow for one of three reasons: an avoidable shuffle, a skewed key, or rows leaking through Python one at a time. The costly mistake is tuning cluster size before reading the plan - paying for more executors to run the same wasteful DAG faster.\n\n## Operating procedure\n\n### Step 1: Gather inputs\n\n1. The job code and, for a slow job, the Spark UI view of the slow stage (task duration distribution, shuffle read\u002Fwrite sizes) and `df.explain(True)` output.\n2. Input data size and format, and the sizes of both sides of every join.\n3. Cluster shape: executor count, cores, and memory.\n4. Whether the job is new authoring or a tuning pass - for tuning, diagnose from the plan before touching code.\n\n### Step 2: Start from a sane session\n\n```python\nfrom pyspark.sql import SparkSession\nfrom pyspark.sql import functions as F\n\nspark = (\n    SparkSession.builder\n    .appName(\"etl\")\n    .config(\"spark.sql.shuffle.partitions\", \"200\")\n    .config(\"spark.sql.adaptive.enabled\", \"true\")\n    .getOrCreate()\n)\n```\n\nEnable Adaptive Query Execution (AQE); it coalesces shuffle partitions and switches join strategies at runtime. Then size `spark.sql.shuffle.partitions` deliberately: target 100-200 MB per shuffle partition, so partitions ≈ shuffle data size ÷ 128 MB, and at least 2-3x the total executor cores so no core idles.\n\n### Step 3: Stay in the DataFrame API and out of Python\n\nDataFrame operations go through Catalyst and Tungsten, giving query optimization and off-heap memory. Drop to RDDs only when no DataFrame primitive exists. Python UDFs serialize each row to the Python worker and back, breaking codegen. Order of preference:\n\n1. Built-in `pyspark.sql.functions` (e.g. `F.regexp_extract`, `F.when`).\n2. Pandas (vectorized) UDFs when Python is unavoidable.\n3. Scalar Python UDFs only as a last resort.\n\n```python\nfrom pyspark.sql.functions import pandas_udf\n\n@pandas_udf(\"double\")\ndef normalize(s):\n    return (s - s.mean()) \u002F s.std()\n```\n\n### Step 4: Pick the join strategy by size\n\n- Broadcast the smaller side when it fits in memory. Spark auto-broadcasts below `spark.sql.autoBroadcastJoinThreshold` (default 10 MB); explicitly broadcast tables up to a few hundred MB when executor memory allows, and treat ~1 GB as the practical ceiling - broadcast copies the table to every executor and OOMs the job when misjudged.\n\n```python\nfrom pyspark.sql.functions import broadcast\nresult = large.join(broadcast(small), \"key\")\n```\n\n- Filter and project columns before joining to shrink the shuffle; every dropped column and row is shuffle bytes saved.\n- Two large tables → accept the sort-merge join, but check for skew first (Step 5).\n\n### Step 5: Diagnose and fix skew\n\nSkew symptoms: the stage is done except for one or two tasks; max task duration exceeds ~3-5x the median; one shuffle-read partition is far larger than the rest in the Spark UI. AQE's skew-join handling splits a partition when it is both over 5x the median partition size and over 256 MB (the defaults) - verify it fired in the plan. When AQE cannot help (e.g. skewed aggregation), salt the hot keys: append a random suffix 0..N to the key on the big side, explode the small side across all N suffixes, join, then strip the salt. Choose N ≈ the skew factor (a key 20x the median gets N=20).\n\n### Step 6: Control partitioning and output layout\n\n- `repartition(n, col)` does a full shuffle to balance data; use before wide writes.\n- `coalesce(n)` reduces partitions without a shuffle; use to avoid tiny output files.\n- Target output files of 128 MB-1 GB; thousands of small files punish every downstream reader.\n- Partition output only by low-cardinality columns (date, region - dozens to hundreds of values, never IDs):\n\n```python\ndf.write.partitionBy(\"dt\").mode(\"overwrite\").parquet(\"\u002Fdata\u002Fout\")\n```\n\n### Step 7: Cache only what is reused\n\nCache only when a DataFrame is used multiple times in the DAG, materialize it, and unpersist when done:\n\n```python\ndf.cache()\ndf.count()  # materialize\n# ... reuse df ...\ndf.unpersist()\n```\n\nCaching a once-used DataFrame wastes memory and can evict data that mattered.\n\n### Step 8: Verify with the plan\n\nRe-run `df.explain(True)` and confirm the intended strategy appears (`BroadcastHashJoin`, pushed filters, no `Exchange` where one was eliminated). Read columnar formats (Parquet, ORC) so predicate pushdown works, never `collect()` large data to the driver, and prefer `F.col` references over string columns when chaining.\n\n## Worked example: bad vs good join\n\n**Bad** - a 2 TB events table joined to a 200 MB dimension, full width, Python UDF for parsing:\n\n```python\nresult = (events.join(dims, \"store_id\")          # sort-merge: shuffles all 2 TB\n    .withColumn(\"region\", parse_region_udf(\"meta\"))  # row-at-a-time Python\n    .filter(F.col(\"dt\") == \"2024-06-01\"))            # filter AFTER the join\n```\n\nThe plan shows an `Exchange` on both sides (2 TB shuffled), and the UDF blocks codegen.\n\n**Good** - filter and project first, broadcast the dimension, use a built-in:\n\n```python\nresult = (events\n    .filter(F.col(\"dt\") == \"2024-06-01\")              # prune to ~30 GB first\n    .select(\"store_id\", \"amount\", \"meta\")\n    .join(broadcast(dims.select(\"store_id\", \"region_code\")), \"store_id\")\n    .withColumn(\"region\", F.regexp_extract(\"meta\", r\"region=(\\w+)\", 1)))\n```\n\nThe plan now shows `BroadcastHashJoin` with no large-side `Exchange`: ~30 GB scanned, zero shuffle of the big table, no Python boundary.\n\n## Deliverable\n\nProduce the revised job code plus a tuning note stating: the join strategy per join with the size evidence, the shuffle-partition setting with its arithmetic, any skew found (max-vs-median task ratio) and the fix applied, caching decisions, output file-size expectations, and the before\u002Fafter `explain` deltas for a tuning pass.\n\n## Do NOT\n\n- Do NOT broadcast a table you have not sized - a misjudged broadcast OOMs every executor at once.\n- Do NOT write a Python UDF before checking `pyspark.sql.functions`; nearly all string, date, and conditional logic has a built-in.\n- Do NOT `collect()` or `toPandas()` a large DataFrame; it funnels the cluster's data through one driver.\n- Do NOT partition output by a high-cardinality column - millions of directories, one row each.\n- Do NOT throw executors at a job whose slow stage is one skewed task; more cores cannot split one partition.\n- Do NOT cache by reflex; cache only DataFrames reused downstream, and unpersist them.\n\n## Quality bar\n\nA finished job passes only when: every join names its strategy and the size evidence for it; `spark.sql.shuffle.partitions` is derived from data size, not left at a habit value; the Spark UI shows no task above ~3x the median duration in shuffle stages (or the skew is explained and fixed); no scalar Python UDF survives where a built-in exists; output files land in the 128 MB-1 GB band; and the final `explain` confirms the intended plan.\n","# Spark & PySpark\n\nAlmost every slow Spark job is slow for one of three reasons: an avoidable shuffle, a skewed key, or rows leaking through Python one at a time. The costly mistake is tuning cluster size before reading the plan - paying for more executors to run the same wasteful DAG faster.\n\n## Operating procedure\n\n### Step 1: Gather inputs\n\n1. The job code and, for a slow job, the Spark UI view of the slow stage (task duration distribution, shuffle read\u002Fwrite sizes) and `df.explain(True)` output.\n2. Input data size and format, and the sizes of both sides of every join.\n3. Cluster shape: executor count, cores, and memory.\n4. Whether the job is new authoring or a tuning pass - for tuning, diagnose from the plan before touching code.\n\n### Step 2: Start from a sane session\n\n```python\nfrom pyspark.sql import SparkSession\nfrom pyspark.sql import functions as F\n\nspark = (\n    SparkSession.builder\n    .appName(\"etl\")\n    .config(\"spark.sql.shuffle.partitions\", \"200\")\n    .config(\"spark.sql.adaptive.enabled\", \"true\")\n    .getOrCreate()\n)\n```\n\nEnable Adaptive Query Execution (AQE); it coalesces shuffle partitions and switches join strategies at runtime. Then size `spark.sql.shuffle.partitions` deliberately: target 100-200 MB per shuffle partition, so partitions ≈ shuffle data size ÷ 128 MB, and at least 2-3x the total executor cores so no core idles.\n\n### Step 3: Stay in the DataFrame API and out of Python\n\nDataFrame operations go through Catalyst and Tungsten, giving query optimization and off-heap memory. Drop to RDDs only when no DataFrame primitive exists. Python UDFs serialize each row to the Python worker and back, breaking codegen. Order of preference:\n\n1. Built-in `pyspark.sql.functions` (e.g. `F.regexp_extract`, `F.when`).\n2. Pandas (vectorized) UDFs when Python is unavoidable.\n3. Scalar Python UDFs only as a last resort.\n\n```python\nfrom pyspark.sql.functions import pandas_udf\n\n@pandas_udf(\"double\")\ndef normalize(s):\n    return (s - s.mean()) \u002F s.std()\n```\n\n### Step 4: Pick the join strategy by size\n\n- Broadcast the smaller side when it fits in memory. Spark auto-broadcasts below `spark.sql.autoBroadcastJoinThreshold` (default 10 MB); explicitly broadcast tables up to a few hundred MB when executor memory allows, and treat ~1 GB as the practical ceiling - broadcast copies the table to every executor and OOMs the job when misjudged.\n\n```python\nfrom pyspark.sql.functions import broadcast\nresult = large.join(broadcast(small), \"key\")\n```\n\n- Filter and project columns before joining to shrink the shuffle; every dropped column and row is shuffle bytes saved.\n- Two large tables → accept the sort-merge join, but check for skew first (Step 5).\n\n### Step 5: Diagnose and fix skew\n\nSkew symptoms: the stage is done except for one or two tasks; max task duration exceeds ~3-5x the median; one shuffle-read partition is far larger than the rest in the Spark UI. AQE's skew-join handling splits a partition when it is both over 5x the median partition size and over 256 MB (the defaults) - verify it fired in the plan. When AQE cannot help (e.g. skewed aggregation), salt the hot keys: append a random suffix 0..N to the key on the big side, explode the small side across all N suffixes, join, then strip the salt. Choose N ≈ the skew factor (a key 20x the median gets N=20).\n\n### Step 6: Control partitioning and output layout\n\n- `repartition(n, col)` does a full shuffle to balance data; use before wide writes.\n- `coalesce(n)` reduces partitions without a shuffle; use to avoid tiny output files.\n- Target output files of 128 MB-1 GB; thousands of small files punish every downstream reader.\n- Partition output only by low-cardinality columns (date, region - dozens to hundreds of values, never IDs):\n\n```python\ndf.write.partitionBy(\"dt\").mode(\"overwrite\").parquet(\"\u002Fdata\u002Fout\")\n```\n\n### Step 7: Cache only what is reused\n\nCache only when a DataFrame is used multiple times in the DAG, materialize it, and unpersist when done:\n\n```python\ndf.cache()\ndf.count()  # materialize\n# ... reuse df ...\ndf.unpersist()\n```\n\nCaching a once-used DataFrame wastes memory and can evict data that mattered.\n\n### Step 8: Verify with the plan\n\nRe-run `df.explain(True)` and confirm the intended strategy appears (`BroadcastHashJoin`, pushed filters, no `Exchange` where one was eliminated). Read columnar formats (Parquet, ORC) so predicate pushdown works, never `collect()` large data to the driver, and prefer `F.col` references over string columns when chaining.\n\n## Worked example: bad vs good join\n\n**Bad** - a 2 TB events table joined to a 200 MB dimension, full width, Python UDF for parsing:\n\n```python\nresult = (events.join(dims, \"store_id\")          # sort-merge: shuffles all 2 TB\n    .withColumn(\"region\", parse_region_udf(\"meta\"))  # row-at-a-time Python\n    .filter(F.col(\"dt\") == \"2024-06-01\"))            # filter AFTER the join\n```\n\nThe plan shows an `Exchange` on both sides (2 TB shuffled), and the UDF blocks codegen.\n\n**Good** - filter and project first, broadcast the dimension, use a built-in:\n\n```python\nresult = (events\n    .filter(F.col(\"dt\") == \"2024-06-01\")              # prune to ~30 GB first\n    .select(\"store_id\", \"amount\", \"meta\")\n    .join(broadcast(dims.select(\"store_id\", \"region_code\")), \"store_id\")\n    .withColumn(\"region\", F.regexp_extract(\"meta\", r\"region=(\\w+)\", 1)))\n```\n\nThe plan now shows `BroadcastHashJoin` with no large-side `Exchange`: ~30 GB scanned, zero shuffle of the big table, no Python boundary.\n\n## Deliverable\n\nProduce the revised job code plus a tuning note stating: the join strategy per join with the size evidence, the shuffle-partition setting with its arithmetic, any skew found (max-vs-median task ratio) and the fix applied, caching decisions, output file-size expectations, and the before\u002Fafter `explain` deltas for a tuning pass.\n\n## Do NOT\n\n- Do NOT broadcast a table you have not sized - a misjudged broadcast OOMs every executor at once.\n- Do NOT write a Python UDF before checking `pyspark.sql.functions`; nearly all string, date, and conditional logic has a built-in.\n- Do NOT `collect()` or `toPandas()` a large DataFrame; it funnels the cluster's data through one driver.\n- Do NOT partition output by a high-cardinality column - millions of directories, one row each.\n- Do NOT throw executors at a job whose slow stage is one skewed task; more cores cannot split one partition.\n- Do NOT cache by reflex; cache only DataFrames reused downstream, and unpersist them.\n\n## Quality bar\n\nA finished job passes only when: every join names its strategy and the size evidence for it; `spark.sql.shuffle.partitions` is derived from data size, not left at a habit value; the Spark UI shows no task above ~3x the median duration in shuffle stages (or the skew is explained and fixed); no scalar Python UDF survives where a built-in exists; output files land in the 128 MB-1 GB band; and the final `explain` confirms the intended plan.\n",{"name":5,"metadata":24,"description":7},{"title":25},"Spark & PySpark","main","97a13607d669157a4a16d5ba79b77f208319f7b9","skills\u002Fspark-jobs",3,"https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills","https:\u002F\u002Fskillme.dev","https:\u002F\u002Fgithub.com\u002FSkillMedev","MCP-native catalog of Claude Agent Skills. Browse, recommend, install, and auto-apply skills in chat. Open source, every skill free and clonable on GitHub.","2026-10-14 03:19:31",{"source":8,"skillName":5,"commands":36,"prompt":46,"zipAvailable":47},[37,40,43],{"tool":38,"command":39},"npx","npx skills add SkillMedev\u002Fskills --skill spark-jobs",{"tool":41,"command":42},"bunx","bunx skills add SkillMedev\u002Fskills --skill spark-jobs",{"tool":44,"command":45},"pnpm","pnpm dlx skills add SkillMedev\u002Fskills --skill spark-jobs","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fspark-jobs\u002FSKILL.md",true,[],[50,58,65,72,79,86],{"skillNo":51,"name":52,"nameZh":6,"description":53,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":54,"categories":55,"keywords":56,"publishedAt":57},"01M75271B256DF3FX2VG5VRYEE","zone-2-cardio-plan","Use when someone asks to build a steady-state cardio plan, train an aerobic base or Zone 2, structure easy or endurance weekly cardio, train by heart-rate zones, or improve resting heart rate, VO2, or endurance. Builds a heart-rate-zone aerobic training block with session structure, modality choice, and progression. General fitness guidance, not medical advice. Do NOT use to design a strength or lifting program (use Strength Training Plan), high-intensity interval conditioning, HIIT, or circuits (use Conditioning & HIIT Program), or a mobility\u002Fflexibility routine (use Mobility Routine).","https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fzone-2-cardio-plan\u002FSKILL.md",[],[],"2026-11-11 18:27:18",{"skillNo":59,"name":60,"nameZh":6,"description":61,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":62,"categories":63,"keywords":64,"publishedAt":57},"01M7526VP1QBBKYBE17S1WR86Y","youtube-script-writer","Writes retention-engineered long-form YouTube scripts on a hook\u002Fbody\u002Fpayoff architecture - a first-30-seconds hook that survives the retention cliff, a beat-sheet body with mini-hooks and bridges, timed pattern interrupts, and a payoff that over-delivers - with B-roll and editing cues marked inline. Use when someone says \"script my next video\", \"my retention graph dies at the start\", \"tighten this draft\", or \"outline a video on X\". Do NOT use for the 3-second opener of TikTok, Reels, or Shorts - use video-hook-writer instead; do NOT use for spoken voiceover synced to on-screen tutorial actions - use narration-script instead; do NOT use for a sales demo built on a buyer's discovery notes - use demo-script instead; do NOT use to design the thumbnail and title - use thumbnail-concept instead.","https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fyoutube-script-writer\u002FSKILL.md",[],[],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":71,"publishedAt":57},"01M7526P2T0WKMHDXS4Q58237T","win-back-campaign","Designs segmented win-back email sequences for dormant users - dormancy tiers, a three-beat sequence with a single proportional incentive, and a suppression rule - and measures success by re-activation, not opens. Use when someone says \"our users went quiet\", \"design a win-back campaign\", \"how do I re-engage lapsed customers\", \"should we email people who haven't logged in for months\", or lifecycle messaging has stopped landing with a lapsed segment. Do NOT use for recovering an unfinished checkout - use abandoned-cart-sequence instead. Do NOT use for diagnosing and fixing why active customers cancel - use churn-reduction instead. Do NOT use for building always-on onboarding or nurture flows - use email-drip-builder instead.","https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fwin-back-campaign\u002FSKILL.md",[],[],{"skillNo":73,"name":74,"nameZh":6,"description":75,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":76,"categories":77,"keywords":78,"publishedAt":57},"01M7526HF2RCDJ50P93WRZRHVV","wikipedia-style","Writes neutral, verifiable, encyclopedic prose following Wikipedia's core content policies and manual of style - NPOV, inline citations, notability-backed claims, and a strict ban on promotional language. Use when someone asks \"write this like a Wikipedia article\", \"draft a Wikipedia page for this company or person\", \"make this neutral and encyclopedic\", or \"why does my article draft keep getting rejected\". Do NOT use for persuasive or opinion writing - use op-ed-writer instead; for thesis-driven academic papers, use academic-essay; for verifying claims in existing text, use fact-checker.","https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fwikipedia-style\u002FSKILL.md",[],[],{"skillNo":80,"name":81,"nameZh":6,"description":82,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":83,"categories":84,"keywords":85,"publishedAt":57},"01M7526D43CTGRKGKNQ2GB0KVX","whitepapers","Structures and writes authoritative B2B whitepapers - a standalone executive summary, a data-sized problem statement, methodology, evidence-driven findings, recommendations, and a soft call to action - flagging every claim that needs a real citation. Use when a marketer asks \"write a whitepaper on this topic\", \"turn our research into a lead-gen asset\", \"make this credible enough for executives\", or a draft reads like a product pitch instead of analysis. Do NOT use for customer-story-driven proof - use case-study-builder instead; for policy audiences, use policy-brief.","https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fwhitepapers\u002FSKILL.md",[],[],{"skillNo":87,"name":88,"nameZh":6,"description":89,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":92,"publishedAt":57},"01M75268423Y3K50SY5WC2NX2G","white-space-analysis","Finds unmet market gaps by ranking jobs-to-be-done on importance versus satisfaction, overlaying them against current solutions and workarounds, and validating that each gap is real, winnable, and worth winning. Use when someone asks \"where are the gaps in this market\", \"what's underserved here\", \"where should we play next\", or is hunting for product, feature, or market-entry opportunities. Do NOT use for profiling competitors and building battle cards - use competitive-intelligence instead; for putting a dollar figure on an identified opportunity - use market-sizing instead; for extracting jobs-to-be-done from raw customer interviews - use jtbd-extractor instead.","https:\u002F\u002Fgithub.com\u002FSkillMedev\u002Fskills\u002Fblob\u002Fmain\u002Fskills\u002Fwhite-space-analysis\u002FSKILL.md",[],[]]