Skill: wangjipeng977/skill-net (version 3.1.0)

--- name: skill-net description: > Analyze OpenClaw skill ecosystem — dependencies, orphan detection, ecosystem health score, impact analysis, and skill relationships. Use when the user asks about skill relationships, "what depends on X", "if I delete Y what breaks", ecosystem health, or wants to find skills without trigger conditions (orhpans). license: MIT metadata: version: "3.1" category: skill-development author: wangjipeng --- # OpenClaw Skill Net Analyze, map, and diagnose the OpenClaw skill ecosystem — not a skill creator, a diagnostic lens. --- ## Core Positioning This skill answers: **how does my skill ecosystem actually work?** It scans every SKILL.md, detects dependency relationships, scores ecosystem health, and finds orhpans. --- ## Modes ### Mode 1: Full Analyze (default) Run the complete ecosystem scan and produce a full diagnostic report. **Trigger:** "analyze ecosystem", "full scan", "ecosystem health", "skill health", "技能生态", "生态报告" **Language options (CLI flags):** ```bash python3 scripts/analyze_deps.py # default: ZH then EN python3 scripts/analyze_deps.py --lang=ZH # Chinese only python3 scripts/analyze_deps.py --lang=EN # English only python3 scripts/analyze_deps.py --lang=BOTH # ZH then EN (default) ``` **Output sections:** 1. 🌡️ Ecosystem Health Score (0–100) — 生态健康分 2. 📊 Health Breakdown — 健康度明细 (trigger coverage, metadata, cross-references) 3. 🔵 Core Hubs — 核心枢纽 (referenced by 3+ skills) 4. 🟡 Bridge Connectors — 桥接技能 5. 🟢 Leaf Skills — 叶节点技能 6. ⚪ Isolated Skills — 孤立技能 7. ⚠️ Orphan Skills — 孤儿技能 (have SKILL.md but no trigger conditions) 8. 🗑️ Impact Analysis — 删除影响分析 (who breaks what) 9. 📋 ASCII Ecosystem Map — 技能生态地图 > All sections rendered in the requested language (ZH/EN) with full bilingual labels. ### Mode 2: Query Answer specific questions from cached or fresh data. **Trigger:** "what depends on X", "if I delete Y", "who references Z", "core skills", "most connected skill" **Execution:** Answer from `data/ecosystem.json` or run fresh scan. ### Mode 3: Orphan Scan Find all skills with SKILL.md but missing trigger conditions. **Trigger:** "find orphans", "skills without triggers", "dead skills", "missing triggers" **Output:** List of orphan skills with line count and frontmatter name. ### Mode 4: Compare Compare two skills side-by-side. **Trigger:** "compare X and Y", "X vs Y dependencies", "skill X relationship to Y" **Output:** Shared mentions, relationship type, overlap analysis. --- ## Key Findings From Real Data > The ecosystem reveals structural patterns invisible from casual observation: | Finding | Evidence | |---------|----------| | **True core hub: `review`** | 53 skills reference `/review` — by far the most connected | | **`qa` is a secondary hub** | 9 skills reference `/qa` | | **`/summarize`, `/weather`** | Referenced by 2+ skills each — utility anchors | | **100/123 skills lack triggers** | Many use `/protocol` style instead of "use when" | | **Ecosystem Health: 22.6/100** | Most skills missing metadata and trigger conditions | | **`review` and `qa` are invisible hubs** | They don't use `skill-` prefix — protocol commands | --- ## Execution Steps (Full Analyze) 1. **Scan** — walk `~/.openclaw/skills/` and `~/.openclaw/workspace/skills/`, read every SKILL.md 2. **Extract** — for each skill: - Frontmatter fields (name, version, license, metadata) - Trigger presence (`use when` / `trigger` / `/protocol`) - ALL cross-skill name mentions (full scan, not just known slugs) - Metadata blocks 3. **Build graph** — `mentions` (outgoing) + `referenced_by` (incoming) for each skill 4. **Classify** — Core (≥3 incoming) → Bridge → Leaf → Isolated 5. **Detect orhpans** — has SKILL.md but no trigger phrase detected 6. **Score health** — weighted formula across 4 dimensions 7. **Render** — bilingual ASCII report (ZH/EN) + save `data/ecosystem.json` + `data/report.md` --- ## Ecosystem Health Formula ``` Health Score = ( trigger_coverage × 30% + metadata_complete × 20% + cross_reference × 20% + ecosystem_cohesion × 30% ) ``` Your ecosystem: **22.6/100** — healthy room for improvement. --- ## What the Real Data Reveals **Surprising insight:** The most-connected nodes are protocol commands (`/review`, `/qa`), not `skill-*` named skills. These protocol skills are referenced by code patterns like: ```python # Many skills open with this: # /review — Structured Code Review Protocol # /qa — Quality Assurance Execution Protocol ``` This means traditional dependency detection (looking for `skill-X` mentions) **severely underestimates** real relationships. **True dependency types:** 1. **Named skill mentions** — `skill-factory`, `gupiao`, `bazi` 2. **Protocol command references** — `/review`, `/qa`, `/careful`, `/cso` 3. **CLI tool references** — `clawhub`, `mmx`, `summarize`, `weather` --- ## Do not - Do not modify any skill based on the analysis without explicit user request - Do not publish the ecosystem map without context — it's a diagnostic tool - Do not call skills "broken" just because they lack trigger phrases — many use protocol-style activation - Do not include `.git/`, `.venv/`, `__pycache__/` in scans --- ## Quality Bar The output must: - Scan both `~/.openclaw/skills/` and `~/.openclaw/workspace/skills/` - Correctly identify incoming + outgoing references per skill - Detect orhpans (has SKILL.md, no trigger phrase) - Compute ecosystem health score (0–100) - Complete full scan in < 15 seconds for 120+ skills - Save structured data to `data/ecosystem.json` + `data/report.md` - Support bilingual output (ZH/EN) via `--lang` flag --- ## Good vs Bad Examples **Good:** > "🔵 review (Core Hub, 53 incoming): /review is referenced by 53 skills. If deleted, these skills lose their review protocol: gupiao, proactive-agent, skill-vetter..." **Bad:** > "Here are all skills listed alphabetically" **Good Orphan Report:** > "⚠️ Found 100 orhpans — most are protocol-style skills (review, qa, careful, cso) that use `/command` activation instead of 'use when' phrases. These are not broken, just designed differently." **Good Query:** > "DELETE review → Breaks 53 skills including: gupiao, marketing-*, engineering-*, testing-*, project-*. This is the most critical skill in the ecosystem."

path: CONTRIBUTING.md
content: # Contributing to skill-net

Thank you for your interest in contributing! Please follow these steps to set up your development environment and submit changes.

## Development Setup

```bash
# 1. Fork the repository on GitHub

# 2. Clone your fork
git clone https://github.com/<your-username>/skill-net.git
cd skill-net

# 3. No external dependencies required
#    (analyze_deps.py uses only Python standard library)

# 4. Run the analyzer to verify it works
python3 scripts/analyze_deps.py --lang=EN
```

## Workflow

We use a standard feature branch workflow:

```bash
# 1. Create a new branch from main
git checkout -b feat/<your-feature-name>

# 2. Make your changes
#    - Follow the SKILL.md structure standards
#    - Keep SKILL.md body in English
#    - Do not hardcode API keys or secrets

# 3. Run the audit to check for issues
python scripts/audit_skill.py .   # if you have audit_skill.py available

# 4. Commit your changes
git add .
git commit -m "feat(<skill-id>): add <brief description>"

# 5. Push to your fork
git push origin feat/<your-feature-name>

# 6. Open a Pull Request on GitHub
#    - Title: feat(<skill-id>): add <brief description>
#    - Description: What + Why + Testing
```

## Code Standards

- **SKILL.md**: Follow the YAML frontmatter standard (name, description, license, metadata)
- **Scripts**: Must include shebang, requirements.txt, and graceful error handling
- **Language**: SKILL.md body must be in English; README_zh.md is Chinese translation
- **No secrets**: Never commit API keys, tokens, or credentials
- **Bilingual**: README.md (EN) and README_zh.md (ZH) must be kept in sync

## Quality Checklist

Before opening a PR, verify:

- [ ] `python3 scripts/analyze_deps.py` runs without errors
- [ ] `--lang=ZH` and `--lang=EN` both produce correct output
- [ ] `python3 scripts/analyze_deps.py --json` outputs valid JSON
- [ ] README.md and README_zh.md are both present and consistent
- [ ] CONTRIBUTING.md is present
- [ ] .gitignore is present and excludes `__pycache__/`, `.git/`, `.venv/`
- [ ] No hardcoded secrets anywhere in the codebase

## Key Design Decisions

### Dependency Detection Scope
`skill-net` scans for **three types** of skill references:
1. Named skill mentions — `skill-factory`, `gupiao`
2. Protocol command references — `/review`, `/qa`, `/careful`, `/cso`
3. CLI tool references — `clawhub`, `mmx`, `summarize`

Do not limit detection to only `skill-*` slugs — this severely underestimates real ecosystem relationships.

### Orphan Interpretation
"Orphan" means "has SKILL.md but no `use when` trigger phrase detected."
This is a **design flag, not an error**. Many skills intentionally use `/protocol` activation.
Do not mark orhpans as broken; report them with the protocol-style note.

### Health Score Formula
```
Health = trigger_coverage×30% + metadata_complete×20% + cross_ref×20% + cohesion×30%
```
Do not change this formula without empirical validation against real ecosystem data.

## Reporting Issues

Please report issues via GitHub Issues with:

1. **What you expected to happen**
2. **What actually happened**
3. **Steps to reproduce**
4. **Environment** (OS, Python version, OpenClaw version)

## License

By contributing, you agree that your contributions will be licensed under the MIT License.

path: README.md
content: # skill-net

[中文版](./README_zh.md)

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Version](https://img.shields.io/badge/version-3.1.0-blue)

> Diagnose, map, and score your OpenClaw skill ecosystem — find orphans, trace dependencies, measure health.

## What Problem This Solves

When managing 100+ skills, you need to answer questions like:
- "What actually depends on `/review`?"
- "If I delete `skill-factory`, what breaks?"
- "Which skills have no trigger phrases?"
- "How healthy is my ecosystem overall?"

`skill-net` scans every SKILL.md, builds a dependency graph, and produces actionable diagnostic reports.

**When to trigger:** "analyze ecosystem", "full scan", "ecosystem health", "skill health", "what depends on X", "if I delete Y what breaks", "find orphans", "技能生态"

## Features

- **Ecosystem Health Score** — weighted 0–100 score across 4 dimensions (trigger coverage, metadata completeness, cross-referencing, cohesion)
- **Dependency Graph** — maps all skill references (both `skill-*` mentions and protocol commands like `/review`, `/qa`)
- **Orphan Detection** — finds skills with SKILL.md but no trigger phrase
- **Impact Analysis** — for any skill, shows exactly what breaks if it's deleted, grouped by category
- **Bilingual Reports** — output in English, Chinese, or both via `--lang` flag
- **Structured Output** — saves `ecosystem.json` + `report.md` for further processing

## Quick Start

```bash
# Full bilingual scan (default: ZH then EN)
python3 scripts/analyze_deps.py

# Chinese report only
python3 scripts/analyze_deps.py --lang=ZH

# English report only
python3 scripts/analyze_deps.py --lang=EN

# JSON output (for automation)
python3 scripts/analyze_deps.py --json

# Impact analysis for a specific skill
python3 scripts/analyze_deps.py --impact=review
```

## Output Sections

| Section | Description |
|---------|-------------|
| 🌡️ Health Score | 0–100 ecosystem score |
| 📊 Composition | Core Hubs / Bridges / Leaves / Isolated / Orphans |
| 🗑️ Delete Impact | What breaks if you remove skill X |
| 📋 Ecosystem Map | ASCII tree of skill relationships |

## Ecosystem Health Formula

```
Health Score = (
  trigger_coverage  × 30%  +
  metadata_complete × 20%  +
  cross_reference    × 20%  +
  ecosystem_cohesion × 30%
)
```

## Key Findings (Real Data)

**The most-connected nodes are protocol commands, not `skill-*` named skills:**

| Skill | Type | Referenced By |
|-------|------|---------------|
| `/review` | protocol | 59 skills |
| `/qa` | protocol | 10 skills |
| `/summarize` | CLI | 3 skills |
| `/weather` | protocol | 3 skills |

Traditional dependency detection (looking only for `skill-X` mentions) **severely underestimates** real ecosystem relationships.

## Directory Structure

```
skill-net/
├── SKILL.md              # Entry point + skill definition
├── LICENSE               # MIT License
├── README.md             # This file
├── README_zh.md          # Chinese version
├── CONTRIBUTING.md       # Contribution guide
├── .gitignore
├── scripts/
│   ├── analyze_deps.py   # Main analyzer (self-contained)
│   └── requirements.txt   # (empty — no external deps)
└── data/
    ├── ecosystem.json    # Structured data (generated)
    └── report.md         # Markdown report (generated)
```

## Skill Classification

| Type | Definition |
|------|-----------|
| 🔵 Core Hub | Referenced by 3+ skills (high blast radius) |
| 🟡 Bridge | Both references others and is referenced |
| 🟢 Leaf | References others, nothing references it |
| ⚪ Isolated | Self-contained, no dependencies |
| ⚠️ Orphan | Has SKILL.md, no trigger phrase detected |

## Caveats

- **Orphan ≠ Broken** — many skills use `/protocol` activation instead of `use when` phrases. Orphans are flagged for awareness, not as errors.
- **Impact is directional** — `A mentions B` doesn't mean `B depends on A`. Delete impact shows what *could* break, not what *will*.
- **Does not modify skills** — this is a read-only diagnostic tool.

## Contributing

Contributions welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.

## License

This project is licensed under the MIT License — see [LICENSE](LICENSE) for details.

path: README_zh.md
content: # skill-net

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![版本](https://img.shields.io/badge/version-3.0.0-blue)

> 诊断、映射、打分你的 OpenClaw 技能生态系统 — 发现孤儿技能、追踪依赖关系、测量健康度。

## 解决什么问题

当你管理 100+ 个技能时，会遇到这些问题：
- "`/review` 到底被哪些技能依赖？"
- "如果删除 `skill-factory`，哪些功能会坏掉？"
- "哪些技能没有触发词？"
- "整个生态系统的健康度怎么样？"

`skill-net` 扫描所有 SKILL.md，构建依赖图谱，输出可操作的诊断报告。

**触发条件：** "analyze ecosystem"、"full scan"、"ecosystem health"、"skill health"、"what depends on X"、"if I delete Y what breaks"、"find orphans"、"技能生态"

## 功能特性

- **生态健康分** — 四维度加权评分（触发词覆盖率、Metadata 完整率、跨技能引用、内聚度），满分 100
- **依赖图谱** — 映射所有技能引用（含 `skill-*` 命名引用和 `/review`、`/qa` 等协议命令）
- **孤儿检测** — 发现有 SKILL.md 但无触发词的技能
- **影响分析** — 对任意技能，精确显示删除后哪些功能会受影响，按类别分组
- **双语报告** — 通过 `--lang` 参数输出中文、英文或两者
- **结构化输出** — 保存 `ecosystem.json` + `report.md`，便于后续处理

## 快速开始

```bash
# 完整双语扫描（默认：中→英）
python3 scripts/analyze_deps.py

# 仅输出中文报告
python3 scripts/analyze_deps.py --lang=ZH

# 仅输出英文报告
python3 scripts/analyze_deps.py --lang=EN

# JSON 输出（用于自动化）
python3 scripts/analyze_deps.py --json

# 分析特定技能的影响
python3 scripts/analyze_deps.py --impact=review
```

## 输出板块

| 板块 | 说明 |
|------|------|
| 🌡️ 健康分 | 0–100 生态评分 |
| 📊 生态构成 | 核心枢纽 / 桥接节点 / 叶节点 / 孤立技能 / 孤儿技能 |
| 🗑️ 删除影响 | 删除技能 X 后哪些会坏掉 |
| 📋 生态地图 | ASCII 树状图展示技能关系 |

## 健康分公式

```
健康分 = (
  触发词覆盖率 × 30%  +
  Metadata完整率 × 20%  +
  跨技能引用 × 20%  +
  内聚度 × 30%
)
```

## 关键发现（真实数据）

**连接度最高的节点是协议命令，而非 `skill-*` 命名的技能：**

| 技能 | 类型 | 被引用次数 |
|------|------|-----------|
| `/review` | protocol | 59 个技能 |
| `/qa` | protocol | 10 个技能 |
| `/summarize` | CLI | 3 个技能 |
| `/weather` | protocol | 3 个技能 |

传统的依赖检测（只查找 `skill-X` 引用）会**严重低估**真实的生态关系。

## 目录结构

```
skill-net/
├── SKILL.md              # 技能入口 + 定义
├── LICENSE               # MIT 许可证
├── README.md             # 英文说明
├── README_zh.md          # 本文件
├── CONTRIBUTING.md       # 贡献指南
├── .gitignore
├── scripts/
│   ├── analyze_deps.py   # 主分析器（零依赖，自含）
│   └── requirements.txt   # （空 — 无外部依赖）
└── data/
    ├── ecosystem.json    # 结构化数据（运行后生成）
    └── report.md         # Markdown 报告（运行后生成）
```

## 技能分类

| 类型 | 定义 |
|------|------|
| 🔵 核心枢纽 | 被 3+ 个技能引用（高爆炸半径） |
| 🟡 桥接节点 | 既引用他人也被他人引用 |
| 🟢 叶节点 | 引用他人但无人引用它 |
| ⚪ 孤立技能 | 完全自洽，无依赖关系 |
| ⚠️ 孤儿技能 | 有 SKILL.md 但未检测到触发词 |

## 注意事项

- **孤儿 ≠ 损坏** — 许多技能使用 `/protocol` 激活而非 `use when` 短语。孤儿标记是提示，不是错误。
- **影响是单向的** — `A 引用 B` 不意味着 `B 依赖 A`。删除影响显示的是*可能*坏什么，不是*一定*坏什么。
- **不修改任何技能** — 这是只读诊断工具。

## 如何贡献

欢迎贡献！请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 了解指南。

## 许可证

本项目采用 MIT 许可证 — 详见 [LICENSE](LICENSE)。

path: data/dep_graph.json
content: {
  "agent-browser": {
    "references": [],
    "referenced_by": [],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "agent-browser",
      "installedVersion": "0.2.0",
      "installedAt": 1773573733666
    }
  },
  "gstack-openclaw-investigate": {
    "references": [],
    "referenced_by": [],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "gstack-openclaw-investigate",
      "installedVersion": "1.0.0",
      "installedAt": 1776002281305
    }
  },
  "self-improving-agent": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "careful": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "cso": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "nuwa": {
    "references": [
      "skill-creator"
    ],
    "referenced_by": [],
    "meta": {}
  },
  "openai-whisper": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "summarize": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "find-skills": {
    "references": [],
    "referenced_by": [
      "skill-net"
    ],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "find-skills",
      "installedVersion": "0.1.0",
      "installedAt": 1773572161516
    }
  },
  "openclaw-wendang": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "qa": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "bazi": {
    "references": [],
    "referenced_by": [
      "skill-net"
    ],
    "meta": {}
  },
  "minimax-monitor": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "skill-factory": {
    "references": [
      "clawhub"
    ],
    "referenced_by": [
      "skill-net"
    ],
    "meta": {}
  },
  "ontology": {
    "references": [],
    "referenced_by": [
      "skill-net"
    ],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "ontology",
      "installedVersion": "1.0.4",
      "installedAt": 1773573902046
    }
  },
  "session-continuity": {
    "references": [
      "proactive-agent"
    ],
    "referenced_by": [
      "skill-net"
    ],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "session-continuity",
      "installedVersion": "1.0.0",
      "installedAt": 1778577649451
    }
  },
  "implementation-plan": {
    "references": [],
    "referenced_by": [],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "implementation-plan",
      "installedVersion": "1.0.2",
      "installedAt": 1775957043989
    }
  },
  "gog-v2": {
    "references": [],
    "referenced_by": [],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "gog-v2",
      "installedVersion": "1.0.0",
      "installedAt": 1775927190489
    }
  },
  "review": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "office-hours": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "gupiao": {
    "references": [],
    "referenced_by": [
      "skill-net"
    ],
    "meta": {}
  },
  "proactive-agent": {
    "references": [],
    "referenced_by": [
      "skill-net"
    ],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "proactive-agent",
      "installedVersion": "3.1.0",
      "installedAt": 1776002044866
    }
  },
  "openclaw-weixin": {
    "references": [],
    "referenced_by": [],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "openclaw-weixin",
      "installedVersion": "2.0.1",
      "installedAt": 1777788177556
    }
  },
  "weather": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "zoufeier-perspective": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "superpowers-writing-plans": {
    "references": [],
    "referenced_by": [],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "superpowers-writing-plans",
      "installedVersion": "1.0.1",
      "installedAt": 1776005583796
    }
  },
  "skill-net": {
    "references": [
      "humanizer-zh",
      "proactive-agent",
      "gupiao",
      "skill-factory",
      "session-continuity",
      "ontology",
      "bazi",
      "clawhub",
      "humanizer",
      "find-skills"
    ],
    "referenced_by": [],
    "meta": {}
  },
  "minimax-feishu-tts": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "humanizer": {
    "references": [],
    "referenced_by": [
      "humanizer-zh"
    ],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "humanizer",
      "installedVersion": "1.0.0",
      "installedAt": 1775956803761
    }
  },
  "stock-analysis-agent": {
    "references": [],
    "referenced_by": [],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "stock-analysis-agent",
      "installedVersion": "1.0.0",
      "installedAt": 1775926804036
    }
  },
  "openclaw-git-manager": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "skill-vetter": {
    "references": [],
    "referenced_by": [],
    "meta": {
      "version": 1,
      "registry": "https://clawhub.ai",
      "slug": "skill-vetter",
      "installedVersion": "1.0.0",
      "installedAt": 1773573071598
    }
  },
  "nano-pdf": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-solidity-smart-contract-engineer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "support-legal-compliance-checker": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-seo-specialist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-kuaishou-strategist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "testing-test-results-analyzer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "testing-performance-benchmarker": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-growth-hacker": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "compliance-auditor": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "testing-evidence-collector": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-ai-engineer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "project-manager-senior": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-instagram-curator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-mobile-app-builder": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-app-store-optimizer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "humanizer-zh": {
    "references": [
      "humanizer"
    ],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-security-engineer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "design-ux-researcher": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "project-management-jira-workflow-steward": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-devops-automator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "identity-graph-operator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-backend-architect": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "product-trend-researcher": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "support-infrastructure-maintainer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-carousel-growth-engine": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "blockchain-security-auditor": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-embedded-firmware-engineer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-bilibili-content-strategist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-autonomous-optimization-architect": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "support-finance-tracker": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "specialized-model-qa": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "report-distribution-agent": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "pdf": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "product-behavioral-nudge-engine": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "support-analytics-reporter": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-incident-response-commander": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "specialized-developer-advocate": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "mmx-cli": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-wechat-mini-program-developer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "zk-steward": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "product-feedback-synthesizer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "specialized-cultural-intelligence-strategist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "support-support-responder": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "testing-reality-checker": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "design-inclusive-visuals-specialist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "data-analytics-reporter": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "design-visual-storyteller": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "agents-orchestrator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "data-consolidation-agent": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "agent-xlsx": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-rapid-prototyper": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-threat-detection-engineer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-frontend-developer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "n8n-workflow-creator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "exa-web-search-free": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-wechat-official-account": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "support-executive-summary-generator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "project-management-studio-producer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "design-ux-architect": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "testing-api-tester": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "design-whimsy-injector": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "accounts-payable-agent": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-zhihu-strategist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "design-image-prompt-engineer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "agentic-identity-trust": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-content-creator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-tiktok-strategist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-xiaohongshu-specialist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "testing-accessibility-auditor": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-technical-writer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "testing-tool-evaluator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "superdesign": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "project-management-project-shepherd": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-senior-developer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "powerpoint-pptx": {
    "references": [
      "clawhub"
    ],
    "referenced_by": [],
    "meta": {}
  },
  "project-management-studio-operations": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "product-sprint-prioritizer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-social-media-strategist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "engineering-data-engineer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-baidu-seo-specialist": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-twitter-engager": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-reddit-community-builder": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "sales-data-extraction-agent": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "marketing-china-ecommerce-operator": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "design-brand-guardian": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "word-docx": {
    "references": [
      "clawhub"
    ],
    "referenced_by": [],
    "meta": {}
  },
  "design-ui-designer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "testing-workflow-optimizer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "project-management-experiment-tracker": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "auto-updater": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  },
  "lsp-index-engineer": {
    "references": [],
    "referenced_by": [],
    "meta": {}
  }
}

path: scripts/analyze_deps.py
content: #!/usr/bin/env python3
"""
Skill Net — Deep Dependency & Ecosystem Analyzer v3.0
Full-spectrum analysis with detailed impact analysis and enhanced visualization.
"""
import json, os, re, sys
from collections import defaultdict
from datetime import datetime

# Pre-compiled regex patterns for performance
RE_COMPILED = {
    "frontmatter_key": re.compile(r"^(\w+):\s*(.*)$"),
    "slug_mention": re.compile(r"(?:[`'\"]|/|[\b]){slug}(?:[`'\"]|/|[\b]|<)".replace("{slug}", r"([a-z][a-z0-9_-]{{4,30}})"), re.I),
    "backtick_slug": re.compile(r"`([a-z][a-z0-9_-]{{4,30})`"),
    "trigger": re.compile(r"\b(use when|trigger|activates|use this skill)\b", re.I),
    "trigger_section": re.compile(r"(?:trigger|when to use|use when|activates)[\s:]*([^\n]+(?:\n(?!\n)[^\n]+){{0,5}})", re.I),
    "protocol_style": re.compile(r"^#\s+/[a-z][a-z0-9_-]+\s*[-—]", re.M),
    "meta_block": re.compile(r"metadata:\s*\{{([^}}]+)\}"),
    "meta_kv": re.compile(r'"(\w+)":\s*"?([^"]+)"?'),
}

SKILLS_DIRS = [
    os.path.expanduser("~/.openclaw/workspace/skills"),
    os.path.expanduser("~/.openclaw/skills"),
]

# Functional categories for human-readable impact reports
CATEGORY_TAGS = {
    "engineering": ["engineering", "frontend", "backend", "devops", "security", "firmware", "technical-writer"],
    "marketing": ["marketing", "content", "seo", "social", "reddit", "twitter", "instagram", "tiktok", "bilibili", "xiaohongshu", "kuaishou", "baidu", "zhihu", "wechat-official"],
    "design": ["design", "ui", "ux", "brand", "visual", "image", "whimsy", "inclusive"],
    "testing": ["testing", "test", "benchmark", "evidence", "accessibility", "api"],
    "project-management": ["project", "jira", "studio", "experiment", "shepherd", "sprint", "product"],
    "support": ["support", "finance", "legal", "analytics", "executive-summary", "infrastructure"],
    "data": ["data", "analytics", "automation", "gupiao", "stock", "pdf", "excel"],
    "protocol": ["review", "qa", "careful", "cso", "office-hours"],
    "communication": ["weixin", "feishu", "whatsapp", "tts", "summarize", "whisper"],
    "personal": ["bazi", "productivity", "humanizer", "nuwa", "perspective", "ontology"],
    "meta": ["skill-factory", "skill-net", "skill-vetter", "find-skills", "proactive-agent", "self-improving"],
}

PROTOCOL_SKILLS = {"review", "qa", "careful", "cso", "office-hours", "summarize", "weather"}


def tag_skill(slug):
    """Return the functional category for a skill."""
    slug_lower = slug.lower()
    for cat, keywords in CATEGORY_TAGS.items():
        for kw in keywords:
            if kw in slug_lower:
                return cat
    if slug in PROTOCOL_SKILLS:
        return "protocol"
    return "general"


def parse_frontmatter(content):
    """Parse frontmatter from skill content.
    
    Returns (frontmatter_dict, body_str).
    frontmatter is a dict of key->value from YAML frontmatter.
    body is everything after the closing '---' delimiter.
    """
    frontmatter = {}
    body = content
    if content.startswith("---"):
        parts = content.split("---", 2)
        if len(parts) >= 2:
            for line in parts[1].split("\n"):
                m = RE_COMPILED["frontmatter_key"].match(line.strip())
                if m:
                    frontmatter[m.group(1)] = m.group(2).strip()
            body = parts[2] if len(parts) > 2 else ""
    return frontmatter, body


def scan_all_skills():
    skills = {}
    all_slugs = set()

    # First pass: collect all slugs
    for base_dir in SKILLS_DIRS:
        if not os.path.exists(base_dir):
            continue
        for entry in os.scandir(base_dir):
            if entry.is_dir():
                all_slugs.add(entry.name)

    # Second pass: analyze each skill
    for base_dir in SKILLS_DIRS:
        if not os.path.exists(base_dir):
            continue
        for entry in os.scandir(base_dir):
            if not entry.is_dir():
                continue
            slug = entry.name
            sk_path = os.path.join(entry.path, "SKILL.md")
            if not os.path.exists(sk_path):
                continue

            with open(sk_path, encoding="utf-8", errors="ignore") as f:
                content = f.read()

            frontmatter, body = parse_frontmatter(content)

            # Extract ALL skill name mentions
            all_mentions = set()
            for other in all_slugs:
                if other == slug:
                    continue
                pat = re.compile(
                    r"(?:[`'\"]|/|[\b])" + re.escape(other) + r"(?:[`'\"]|/|[\b]|<)",
                    re.I
                )
                if pat.search(body):
                    all_mentions.add(other)

            # Also find backtick-quoted slugs
            for found in RE_COMPILED["backtick_slug"].findall(body):
                if found != slug and found in all_slugs:
                    all_mentions.add(found)

            # Detect triggers — check both body and frontmatter description
            desc = frontmatter.get("description", "")
            has_triggers = bool(RE_COMPILED["trigger"].search(body) or RE_COMPILED["trigger"].search(desc))
            trigger_section = RE_COMPILED["trigger_section"].search(body) or RE_COMPILED["trigger_section"].search(desc)
            trigger_text = trigger_section.group(1).strip()[:200] if trigger_section else ""

            # Protocol-style detection: starts with # /command or ## /command
            is_protocol = bool(RE_COMPILED["protocol_style"].search(body))

            # Metadata
            meta_block = {}
            meta_m = RE_COMPILED["meta_block"].search(content)
            if meta_m:
                for kv in RE_COMPILED["meta_kv"].findall(meta_m.group(1)):
                    meta_block[kv[0]] = kv[1]

            # ClawHub origin data
            origin_path = os.path.join(entry.path, ".clawhub", "origin.json")
            origin = {}
            if os.path.exists(origin_path):
                try:
                    with open(origin_path) as f:
                        origin = json.load(f)
                except: pass

            desc = frontmatter.get("description", "")

            skills[slug] = {
                "path": sk_path,
                "frontmatter": frontmatter,
                "mentions": all_mentions,
                "referenced_by": set(),
                "has_triggers": has_triggers,
                "is_protocol": is_protocol,
                "trigger_text": trigger_text,
                "meta_block": meta_block,
                "lines": content.count("\n"),
                "origin": origin,
                "category": tag_skill(slug),
                "is_role_based": _is_role_based(desc),
                "has_meaningful_desc": _has_meaningful_desc(desc, content.count("\n")),
            }

    # Resolve references
    for slug, data in skills.items():
        for mentioned in data["mentions"]:
            if mentioned in skills:
                skills[mentioned]["referenced_by"].add(slug)

    return skills


def classify_skills(skills):
    core, bridge, leaf, isolated = [], [], [], []
    for slug, data in skills.items():
        refs = len(data["mentions"])
        refd = len(data.get("referenced_by", set()))
        if refd >= 3:
            core.append((slug, data))
        elif refs > 0 and refd > 0:
            bridge.append((slug, data))
        elif refs > 0:
            leaf.append((slug, data))
        else:
            isolated.append((slug, data))
    return core, bridge, leaf, isolated


def _is_role_based(desc):
    """Role-based agent: description starts with 'You are' and has a role name."""
    stripped = desc.strip()
    return stripped.startswith("You are") and len(stripped) > 20


def _has_meaningful_desc(desc, body_lines):
    """Has substantive content beyond role definition."""
    if not desc or len(desc.strip()) < 15:
        return False
    # Penalize generic placeholder descriptions
    placeholder_phrases = ["to be written", "tbd", "todo", "under development",
                          "not yet implemented", "coming soon"]
    desc_lower = desc.lower()
    if any(p in desc_lower for p in placeholder_phrases):
        return False
    return True


def _has_identity(skill_data):
    """A skill has identity if it has any form of self-definition."""
    if skill_data.get("has_triggers"):
        return True
    if skill_data.get("is_protocol"):
        return True
    desc = skill_data.get("frontmatter", {}).get("description", "")
    if _is_role_based(desc):
        return True
    if _has_meaningful_desc(desc, skill_data.get("lines", 0)):
        return True
    return False


def detect_orphans(skills):
    """Detect skills with no trigger, protocol style, or meaningful identity."""
    return [(s, d["lines"], d["frontmatter"].get("name", s), d["category"])
            for s, d in skills.items()
            if not _has_identity(d)]


def ecosystem_health_score(skills):
    total = len(skills)
    if total == 0:
        return 0
    desc_not_empty = sum(1 for d in skills.values() if d["frontmatter"].get("description", "").strip())
    desc_substantive = sum(1 for d in skills.values()
                           if len(d["frontmatter"].get("description", "").strip()) >= 20)
    body_has_content = sum(1 for d in skills.values() if d.get("lines", 0) >= 20)
    has_identity = sum(1 for d in skills.values() if _has_identity(d))
    score = (
        (desc_not_empty / total) * 20 +
        (desc_substantive / total) * 25 +
        (body_has_content / total) * 15 +
        (has_identity / total) * 40
    )
    return round(score, 1)


def build_impact_report(skills, slug):
    """Build detailed impact report for deleting a specific skill."""
    data = skills.get(slug, {})
    dependents = sorted(data.get("referenced_by", set()))
    if not dependents:
        return None

    # Group dependents by category
    by_category = defaultdict(list)
    for dep in dependents:
        cat = skills[dep]["category"] if dep in skills else "general"
        by_category[cat].append(dep)

    lines = []
    lines.append(f"\n{'='*60}")
    lines.append(f"  🗑️  IMPACT REPORT: DELETE `{slug}`")
    lines.append(f"{'='*60}")
    lines.append(f"  Affects {len(dependents)} skill(s) across {len(by_category)} category(ies)")

    for cat, members in sorted(by_category.items(), key=lambda x: -len(x[1])):
        lines.append(f"\n  [{cat.upper()}] — {len(members)} skill(s)")
        for m in members:
            name = skills[m]["frontmatter"].get("name", m)
            protocol_style = "📋" if skills[m]["is_protocol"] else "🏷️"
            triggers = "✅" if skills[m]["has_triggers"] else "⚠️"
            lines.append(f"    {protocol_style} {triggers} `{m}` ({name})")

            # What specifically would break
            if slug in skills[m]["mentions"]:
                lines.append(f"        → `{m}` explicitly mentions `{slug}`")
            else:
                lines.append(f"        → `{m}` references this skill's protocol/command")

    lines.append(f"\n  {'─'*60}")
    lines.append(f"  TOTAL BLAST RADIUS: {len(dependents)} skill(s)")
    lines.append(f"  {'─'*60}")
    return "\n".join(lines)


def render_markdown_table(skills):
    """Render skills as a Markdown table sorted by impact."""
    core, bridge, leaf, isolated = classify_skills(skills)

    rows = []
    rows.append("# OpenClaw Skill Ecosystem — Markdown Tables\n")

    # Table 1: Core Hubs (sorted by dependents desc)
    rows.append("## 🔵 Core Hub Skills (referenced by 3+)\n")
    rows.append("| Skill | Name | Category | Referenced By | References | Protocol Style |")
    rows.append("|-------|------|----------|---------------|------------|----------------|")
    for slug, data in sorted(core, key=lambda x: -len(x[1].get("referenced_by", []))):
        name = data["frontmatter"].get("name", "")
        cat = data["category"]
        refd = len(data.get("referenced_by", []))
        refs = len(data["mentions"])
        protocol = "✅" if data["is_protocol"] else "—"
        rows.append(f"| `{slug}` | {name} | {cat} | **{refd}** | {refs} | {protocol} |")

    # Table 2: Bridges
    rows.append("\n## 🟡 Bridge Skills (both references and referenced)\n")
    rows.append("| Skill | Name | Category | References | Referenced By |")
    rows.append("|-------|------|----------|------------|---------------|")
    for slug, data in sorted(bridge, key=lambda x: -len(x[1]["mentions"])):
        name = data["frontmatter"].get("name", "")
        cat = data["category"]
        refs = len(data["mentions"])
        refd = len(data.get("referenced_by", []))
        rows.append(f"| `{slug}` | {name} | {cat} | {refs} | {refd} |")

    # Table 3: Top Leaf skills (most references)
    rows.append("\n## 🟢 Top Leaf Skills (most outgoing references)\n")
    rows.append("| Skill | Name | Category | References |")
    rows.append("|-------|------|----------|------------|")
    for slug, data in sorted(leaf, key=lambda x: -len(x[1]["mentions"]))[:15]:
        name = data["frontmatter"].get("name", "")
        cat = data["category"]
        refs = len(data["mentions"])
        rows.append(f"| `{slug}` | {name} | {cat} | {refs} |")

    # Table 4: Orphan skills by category
    orphans = detect_orphans(skills)
    by_cat = defaultdict(list)
    for s, lines_cnt, name, cat in orphans:
        by_cat[cat].append((s, lines_cnt, name))

    rows.append("\n## ⚠️ Orphan Skills (no trigger conditions, by category)\n")
    for cat in sorted(by_cat.keys(), key=lambda c: -len(by_cat[c])):
        members = by_cat[cat]
        rows.append(f"\n### [{cat.upper()}] — {len(members)} orphan(s)\n")
        rows.append("| Skill | Lines | Name |")
        rows.append("|-------|-------|------|")
        for s, lc, name in sorted(members, key=lambda x: -x[1])[:10]:
            rows.append(f"| `{s}` | {lc} | {name} |")

    # Table 5: Full impact analysis (delete X → breaks Y)
    rows.append("\n## 🗑️ Delete Impact Matrix\n")
    rows.append("| If You Delete | Breaks These Skills | Count |")
    rows.append("|---------------|---------------------|-------|")
    impacts = []
    for slug, data in skills.items():
        refd = list(data.get("referenced_by", []))
        if len(refd) >= 2:
            impacts.append((slug, refd, len(refd)))
    for slug, refd, count in sorted(impacts, key=lambda x: -x[2]):
        skill_names = ", ".join(f"`{r}`" for r in refd[:5])
        if count > 5:
            skill_names += f" +{count-5} more"
        rows.append(f"| `{slug}` | {skill_names} | **{count}** |")

    return "\n".join(rows)


def render_full_report(skills):
    core, bridge, leaf, isolated = classify_skills(skills)
    orphans = detect_orphans(skills)
    health = ecosystem_health_score(skills)

    lines = []
    lines.append("=" * 60)
    lines.append("  SKILL NET v3.0 — OpenClaw Ecosystem Analysis")
    lines.append("=" * 60)
    lines.append(f"  {len(skills)} skills scanned | {datetime.now().strftime('%Y-%m-%d %H:%M')}")
    lines.append(f"  🌡️  Ecosystem Health Score: {health}/100")
    lines.append("=" * 60)

    total = len(skills)
    desc_not_empty = sum(1 for d in skills.values() if d["frontmatter"].get("description", "").strip())
    desc_substantive = sum(1 for d in skills.values()
                           if len(d["frontmatter"].get("description", "").strip()) >= 20)
    body_has_content = sum(1 for d in skills.values() if d.get("lines", 0) >= 20)
    has_identity = sum(1 for d in skills.values() if _has_identity(d))
    with_triggers = sum(1 for d in skills.values() if d["has_triggers"])  # keep for other uses
    with_meta = sum(1 for d in skills.values() if d.get("meta_block"))  # keep for other uses

    lines.append(f"\n  📊 Ecosystem Composition:")
    lines.append(f"     🔵 Core hubs:    {len(core)} skill(s)")
    lines.append(f"     🟡 Bridges:     {len(bridge)} skill(s)")
    lines.append(f"     🟢 Leaf:        {len(leaf)} skill(s)")
    lines.append(f"     ⚪ Isolated:    {len(isolated)} skill(s)")
    lines.append(f"     ⚠️  Orphans:     {len(orphans)} skill(s)")

    lines.append(f"\n  📊 Health Breakdown:")
    lines.append(f"     Description present:  {desc_not_empty}/{total} ({100*desc_not_empty//total}%)")
    lines.append(f"     Description quality: {desc_substantive}/{total} ({100*desc_substantive//total}%)")
    lines.append(f"     Body content:        {body_has_content}/{total} ({100*body_has_content//total}%)")
    lines.append(f"     Identity coverage:  {has_identity}/{total} ({100*has_identity//total}%)")

    # Core hubs
    lines.append(f"\n{'🔵 CORE HUB SKILLS (ref by 3+)':=^60}")
    for slug, data in sorted(core, key=lambda x: -len(x[1].get("referenced_by", []))):
        name = data["frontmatter"].get("name", slug)
        refd = len(data.get("referenced_by", []))
        cat = data["category"]
        protocol = "📋" if data["is_protocol"] else "🏷️"
        dependents = sorted(data["referenced_by"])
        lines.append(f"\n  {protocol} 🔵 `{slug}` ({name}) | {cat} | ← {refd}")
        if refd <= 10:
            lines.append(f"      → {', '.join(dependents)}")
        else:
            lines.append(f"      → {', '.join(dependents[:8])} +{refd-8} more")

    # Bridge skills
    lines.append(f"\n{'🟡 BRIDGE SKILLS':=^60}")
    for slug, data in sorted(bridge, key=lambda x: -len(x[1]["mentions"])):
        name = data["frontmatter"].get("name", slug)
        cat = data["category"]
        refs = len(data["mentions"])
        refd = len(data.get("referenced_by", []))
        lines.append(f"  🟡 `{slug}` ({name}) | → {refs} ← {refd} | {cat}")

    # Orphans
    lines.append(f"\n{'⚠️  ORPHAN SKILLS (SKILL.md without trigger conditions)':=^60}")
    lines.append(f"  Total: {len(orphans)} skills\n")
    by_cat = defaultdict(list)
    for s, lc, name, cat in orphans:
        by_cat[cat].append((s, lc, name))
    for cat in sorted(by_cat.keys(), key=lambda c: -len(by_cat[c])):
        members = by_cat[cat]
        extra = f" +{len(members)-5} more" if len(members) > 5 else ""
        lines.append(f"  [{cat.upper()}] {len(members)}: {', '.join(f'`{s}`' for s, _, _ in members[:5])}{extra}")

    # Full impact table
    lines.append(f"\n{'🗑️  DELETE IMPACT TABLE (2+ dependents)':=^60}")
    lines.append(f"  {'Skill':<35} {'Category':<15} {'Count':<6} {'Depends On It'}")
    lines.append(f"  {'─'*60}")
    impacts = [(s, d, sorted(d.get("referenced_by", [])), len(d.get("referenced_by", [])))
               for s, d in skills.items() if len(d.get("referenced_by", [])) >= 2]
    for slug, data, refd, count in sorted(impacts, key=lambda x: -x[3]):
        cat = data["category"]
        lines.append(f"  🗑️  `{slug}`")
        lines.append(f"     {cat:<15} {count:>4} skills: {', '.join(refd[:4])}{'+' if count > 4 else ''}")

    # ASCII map
    lines.append(f"\n{'📋 SKILL ECOSYSTEM MAP':=^60}")
    ranked = sorted(skills.items(), key=lambda x: len(x[1].get("referenced_by", set())), reverse=True)
    shown = 0
    for slug, data in ranked:
        if not data.get("referenced_by") and not data["mentions"]:
            continue
        if shown >= 15:
            break
        refd = len(data.get("referenced_by", set()))
        refs = len(data["mentions"])
        indent = "  " * shown
        if refd >= 3:
            lines.append(f"{indent}📦 {slug} ← {refd}")
        elif refd > 0:
            lines.append(f"{indent}🔗 {slug} → {refs} ← {refd}")
        elif refs > 0:
            lines.append(f"{indent}🍃 {slug} → {refs}")
        shown += 1

    lines.append("\n" + "=" * 60)
    lines.append("  LEGEND")
    lines.append("  🔵 Core = ref by 3+ (high blast radius) | 🟡 Bridge = in+out | 🟢 Leaf = out only | ⚪ Isolated = none")
    lines.append("  📋 = protocol-style skill | 🏷️ = standard trigger-style skill | ⚠️ = orphan (no triggers)")
    lines.append("=" * 60)

    return "\n".join(lines), health, orphans



# ── BILINGUAL LABELS ───────────────────────────────────────────────────────
L = {
    "skills_scanned":       ("skills scanned",               "个技能已扫描"),
    "health_score":         ("Ecosystem Health Score",       "生态健康分"),
    "composition":          ("Ecosystem Composition",         "生态构成"),
    "core_hubs":            ("Core Hub Skills",               "核心枢纽技能"),
    "bridges":              ("Bridge Skills",                "桥接技能"),
    "leaf_skills":          ("Leaf Skills",                   "叶节点技能"),
    "isolated_skills":      ("Isolated Skills",              "孤立技能"),
    "orphan_skills":        ("Orphan Skills",                "孤儿技能"),
    "delete_impact":        ("Delete Impact Analysis",       "删除影响分析"),
    "health_breakdown":     ("Health Breakdown",             "健康度明细"),
    "desc_present":         ("Description Present",          "Description存在"),
    "desc_quality":         ("Description Quality",          "Description质量"),
    "body_content":         ("Body Content",                  "Body内容"),
    "identity_cov":         ("Identity Coverage",             "身份覆盖"),
    "referenced_by":        ("referenced by",                "被引用"),
    "references":           ("references",                   "外联引用"),
    "affected":             ("affected skill(s)",             "受影响技能"),
    "no_triggers":          ("no trigger conditions",         "无触发条件"),
    "no_orphans":           ("No orphans found",             "无孤儿技能"),
    "no_impact":            ("No high-impact skills",        "无高频影响技能"),
    "delete":               ("DELETE",                       "删除"),
    "breaks":               ("breaks",                       "将影响"),
    "across":               ("across",                       "跨"),
    "categories":           ("categories",                   "个功能类别"),
    "legend":               ("LEGEND",                       "图例"),
    "ecosystem_map":        ("Skill Ecosystem Map",         "技能生态地图"),
    "health_note_en":       ("Health: content(60%) + identity(40%)",  ""),
    "health_note_zh":       ("", "健康分：触发词x30% + metadatax20% + 跨引用x20% + 内聚度x30%"),
    "orphan_note2_en":       ("Many use /protocol activation -- not broken, different design",         ""),
    "orphan_note2_zh":      ("", "许多孤儿使用/protocol激活风格 -- 非损坏，仅设计风格不同"),
}

def l(en_key):
    pair = L.get(en_key, (en_key, en_key))
    return pair[0] if lang == "EN" else pair[1]


# ── BILINGUAL REPORT RENDERER ───────────────────────────────────────────────
def le(en_key):  # returns (en, zh) tuple
    return L.get(en_key, (en_key, en_key))

def render_bilingual(skills, lang="EN"):
    def l(en_key):  # returns single string for current lang
        pair = L.get(en_key, (en_key, en_key))
        return pair[0] if lang == "EN" else pair[1]
    core, bridge, leaf, isolated = classify_skills(skills)
    orphans = detect_orphans(skills)
    health = ecosystem_health_score(skills)
    total = len(skills)

    lines = []
    W = 60
    title = "SKILL NET -- Ecosystem Analysis" if lang == "EN" else "SKILL NET -- 技能生态分析"
    lines.append("=" * W)
    lines.append("  " + title)
    lines.append("=" * W)
    lines.append("  " + str(total) + " " + l("skills_scanned") + " | " + datetime.now().strftime("%Y-%m-%d %H:%M"))
    lines.append("  [Health] " + l("health_score") + ": " + str(health) + "/100")
    lines.append("=" * W)

    desc_not_empty = sum(1 for d in skills.values() if d["frontmatter"].get("description", "").strip())
    desc_substantive = sum(1 for d in skills.values()
                           if len(d["frontmatter"].get("description", "").strip()) >= 20)
    body_has_content = sum(1 for d in skills.values() if d.get("lines", 0) >= 20)
    has_identity = sum(1 for d in skills.values() if _has_identity(d))
    eco_nodes = sum(1 for d in skills.values() if d.get("referenced_by"))


    lines.append("")
    lines.append("  [Composition] " + l("composition") + ":")
    lines.append("     [Core] " + l("core_hubs") + ":    " + str(len(core)))
    lines.append("     [Bridge] " + l("bridges") + ":     " + str(len(bridge)))
    lines.append("     [Leaf] " + l("leaf_skills") + ":        " + str(len(leaf)))
    lines.append("     [Isolated] " + l("isolated_skills") + ":    " + str(len(isolated)))
    lines.append("     [Orphan] " + l("orphan_skills") + ":     " + str(len(orphans)))

    lines.append("")
    lines.append("  [Health] " + l("health_breakdown") + ":")
    lines.append("     " + l("desc_present") + ":  " + str(desc_not_empty) + "/" + str(total) + " (" + str(100*desc_not_empty//total) + "%)")
    lines.append("     " + l("desc_quality") + ":  " + str(desc_substantive) + "/" + str(total) + " (" + str(100*desc_substantive//total) + "%)")
    lines.append("     " + l("body_content") + ":   " + str(body_has_content) + "/" + str(total) + " (" + str(100*body_has_content//total) + "%)")
    lines.append("     " + l("identity_cov") + ":   " + str(has_identity) + "/" + str(total) + " (" + str(100*has_identity//total) + "%)")
    lines.append("     " + l("eco_nodes") + ":             " + str(eco_nodes) + "/" + str(total))

    if lang == "EN":
        lines.append("  * Health: content(60%) + identity(40%)")
    else:
        lines.append("  * 健康分：内容完整性(60%) + 身份覆盖(40%)")

    # Core hubs
    hub_title = "[Core] " + l("core_hubs") + " (ref by 3+)"
    lines.append("")
    lines.append(hub_title)
    lines.append("=" * W)
    if core:
        for slug, data in sorted(core, key=lambda x: -len(x[1].get("referenced_by", []))):
            name  = data["frontmatter"].get("name", slug)
            refd  = len(data.get("referenced_by", []))
            cat   = data["category"]
            proto = "[P]" if data["is_protocol"] else "[T]"
            deps  = sorted(data["referenced_by"])
            lines.append("")
            lines.append("  " + proto + " [Core] " + slug + " / " + name)
            lines.append("     " + cat + " | <- " + str(refd) + " " + l("referenced_by"))
            if refd <= 6:
                lines.append("     -> " + ", ".join(deps))
            else:
                lines.append("     -> " + ", ".join(deps[:6]) + " +" + str(refd-6) + " more")
    else:
        lines.append("  -- " + l("no_impact"))

    # Bridges
    bridge_title = "[Bridge] " + l("bridges")
    lines.append("")
    lines.append(bridge_title)
    lines.append("=" * W)
    for slug, data in sorted(bridge, key=lambda x: -len(x[1]["mentions"])):
        name  = data["frontmatter"].get("name", slug)
        cat   = data["category"]
        refs  = len(data["mentions"])
        refd  = len(data.get("referenced_by", []))
        lines.append("  [Bridge] " + slug + " / " + name + " | ->" + str(refs) + " <-" + str(refd) + " | " + cat)

    # Delete impact
    di_title = "[Delete Impact] " + l("delete_impact") + "  (X -> breaks Y)"
    lines.append("")
    lines.append(di_title)
    lines.append("=" * W)
    impacts = [(s, data, sorted(data.get("referenced_by", [])), len(data.get("referenced_by", [])))
               for s, data in skills.items() if len(data.get("referenced_by", [])) >= 2]
    if impacts:
        for slug, data, refd, count in sorted(impacts, key=lambda x: -x[3]):
            cat = data["category"]
            by_cat = defaultdict(list)
            for dep in refd:
                dep_cat = skills[dep]["category"] if dep in skills else "general"
                by_cat[dep_cat].append(dep)
            lines.append("")
            lines.append("  [X] DELETE " + slug + " / " + cat)
            lines.append("     " + l("affected") + ": " + str(count))
            for c, members in sorted(by_cat.items(), key=lambda x: -len(x[1])):
                lines.append("     [" + c.upper() + "] " + str(len(members)) + ": " + ", ".join(members[:5]) + ("+" if len(members)>5 else ""))
    else:
        lines.append("  -- " + l("no_impact"))

    # Orphans by category
    or_title = "[Orphan] " + l("orphan_skills") + "  (" + l("no_triggers") + ")"
    lines.append("")
    lines.append(or_title)
    lines.append("=" * W)
    if lang == "EN":
        lines.append("  * Many use /protocol activation -- not broken, different design")
    else:
        lines.append("  * 许多孤儿使用/protocol激活风格 -- 非损坏，仅设计风格不同")
    lines.append("")
    by_cat = defaultdict(list)
    for s, lc, name, cat in orphans:
        by_cat[cat].append(s)
    for cat in sorted(by_cat.keys(), key=lambda c: -len(by_cat[c])):
        members = by_cat[cat]
        names_short = ", ".join(s for s in members[:5])
        extra = " +" + str(len(members)-5) + " more" if len(members) > 5 else ""
        lines.append("  [" + cat.upper() + "] " + str(len(members)) + ": " + names_short + extra)

    # Ecosystem map
    map_title = "[Ecosystem Map] " + l("ecosystem_map")
    lines.append("")
    lines.append(map_title)
    lines.append("=" * W)
    ranked = sorted(skills.items(), key=lambda x: len(x[1].get("referenced_by", set())), reverse=True)
    shown = 0
    for slug, data in ranked:
        if shown >= 12:
            break
        refd = len(data.get("referenced_by", set()))
        refs = len(data["mentions"])
        if refd == 0 and refs == 0:
            continue
        if refd >= 3:
            lines.append("  [Hub] " + slug + " <- " + str(refd))
        elif refd > 0:
            lines.append("  [Bridge] " + slug + " ->" + str(refs) + " <-" + str(refd))
        else:
            lines.append("  [Leaf] " + slug + " ->" + str(refs))
        shown += 1

    # Legend
    leg_title = "[Legend] " + l("legend")
    lines.append("")
    lines.append(leg_title)
    lines.append("=" * W)
    if lang == "EN":
        lines.append("  [Core] = ref by 3+ (high blast radius)")
        lines.append("  [Bridge] = refs others + is referenced")
        lines.append("  [Leaf] = refs others, nothing refs it")
        lines.append("  [Isolated] = self-contained, no dependencies")
        lines.append("  [Orphan] = has SKILL.md, no triggers detected")
        lines.append("  [P] = protocol-style (/command activation)")
        lines.append("  [T] = trigger-style (use when phrases)")
    else:
        lines.append("  [核心] = 被3个以上技能引用（高爆炸半径）")
        lines.append("  [桥接] = 既引用他人也被他人引用")
        lines.append("  [叶节点] = 引用他人但无人引用它")
        lines.append("  [孤立] = 完全自洽，无依赖关系")
        lines.append("  [孤儿] = 有SKILL.md但未检测到触发词")
        lines.append("  [协议型] = 使用 /command 激活风格")
        lines.append("  [触发词型] = 使用 use when 短语激活")

    lines.append("=" * W)
    return "\n".join(lines)

if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser(description="Skill Net — Ecosystem Analyzer")
    parser.add_argument("--lang", choices=["EN", "ZH", "BOTH"], default="BOTH",
                        help="Language: EN, ZH, or BOTH (default)")
    parser.add_argument("--impact", type=str, default=None,
                        help="Show detailed impact for a specific skill")
    parser.add_argument("--json", action="store_true", help="Output raw JSON")
    args = parser.parse_args()

    skills = scan_all_skills()

    if args.json:
        print(json.dumps({k: {**v, 'referenced_by': list(v.get('referenced_by', set())), 'mentions': list(v['mentions'])} for k, v in skills.items()}, ensure_ascii=False, indent=2))
        sys.exit(0)

    if args.impact:
        report = build_impact_report(skills, args.impact)
        if report:
            print(report)
        else:
            print(f"No impact data for `{args.impact}` (0 or 1 dependent)")
        sys.exit(0)

    if args.lang in ("ZH", "BOTH"):
        print(render_bilingual(skills, "ZH"))
    if args.lang in ("EN", "BOTH"):
        if args.lang == "BOTH":
            print("\n" + "=" * 60 + "\n")
        print(render_bilingual(skills, "EN"))

    # Save
    output_dir = os.path.expanduser("~/.openclaw/workspace/skills/skill-net/data")
    os.makedirs(output_dir, exist_ok=True)


    serializable = {k: {"mentions": list(v["mentions"]), "referenced_by": list(v.get("referenced_by", set())),
                    "has_triggers": v["has_triggers"], "is_protocol": v["is_protocol"],
                    "trigger_text": v["trigger_text"], "meta_block": v["meta_block"],
                    "lines": v["lines"], "frontmatter": v["frontmatter"],
                    "category": v["category"], "health_score": ecosystem_health_score(skills)}
                   for k, v in skills.items()}

    with open(os.path.join(output_dir, "ecosystem.json"), "w") as f:
        json.dump(serializable, f, indent=2, ensure_ascii=False)

    # Markdown tables
    md_table = render_markdown_table(skills)
    with open(os.path.join(output_dir, "report.md"), "w") as f:
        f.write(md_table)

    orphans = detect_orphans(skills)
    print(f"\n💾 ecosystem.json + report.md saved")
    print(f"🌡️  Health: {ecosystem_health_score(skills)}/100 | ⚠️  Orphans: {len(orphans)}")


path: scripts/requirements.txt
content: PyYAML>=6.0

path: tests/test_frontmatter.py
content: #!/usr/bin/env python3
"""
Tests for analyze_deps.py frontmatter parsing.

BUG FIXED (2026-05-15):
- Original code: parts = content[3:].split("---", 2)
  This stripped the first 3 chars before splitting, causing body to always be empty
  when frontmatter exists, because the split point was consumed by the slice.
- Fixed code: parts = content.split("---", 2)
  Now correctly splits on both frontmatter delimiters.
"""
import unittest
import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "scripts"))
import analyze_deps


class TestFrontmatterParsing(unittest.TestCase):
    def test_body_with_use_when_in_body(self):
        """Use when phrase in body should be detected (was broken before fix)."""
        content = """---
name: example-skill
description: "A skill"
---
# Example Skill

Use when: user asks about something specific

Some body content here.
"""
        frontmatter, body = analyze_deps.parse_frontmatter(content)
        self.assertEqual(frontmatter.get("name"), "example-skill")
        self.assertIn("Use when", body)
        self.assertIn("Some body content here.", body)

    def test_body_with_use_when_multiline(self):
        """Multi-line body with Use when in middle."""
        content = """---
name: test
---
# Title

Use when: user says trigger phrase
And also when they say this

## Section
More content.
"""
        _, body = analyze_deps.parse_frontmatter(content)
        self.assertIn("Use when", body)
        self.assertIn("## Section", body)

    def test_no_frontmatter(self):
        """Content without frontmatter should return full content as body."""
        content = "# No Frontmatter\n\nSome text without YAML delimiters."
        _, body = analyze_deps.parse_frontmatter(content)
        self.assertEqual(body, content)

    def test_empty_frontmatter_single_delimiter(self):
        """Single --- without closing delimiter."""
        content = """---
name: single-delimiter
---
# Body
Use when: this is in body
"""
        _, body = analyze_deps.parse_frontmatter(content)
        self.assertIn("Use when", body)

    def test_complex_yaml_value(self):
        """Description with multi-line YAML literal block."""
        content = """---
name: complex
description: >
  This is a multi-line
  description value
---
# Body
Use when: trigger here
"""
        fm, body = analyze_deps.parse_frontmatter(content)
        self.assertEqual(fm.get("name"), "complex")
        self.assertIn("Use when", body)

    def test_frontmatter_with_inline_use_when(self):
        """Use when phrase on same line as frontmatter field."""
        content = """---
name: inline-test
description: "Use when: inline description"
---
# Body
More content.
"""
        fm, body = analyze_deps.parse_frontmatter(content)
        self.assertEqual(fm.get("description"), '"Use when: inline description"')
        self.assertEqual(body.strip(), "# Body\nMore content.")

    def test_frontmatter_key_value_pairs(self):
        """Multiple key-value pairs in frontmatter are parsed correctly."""
        content = """---
name: multi-field
version: 1.0.0
description: Test skill
author: test
---
# Body content
"""
        fm, body = analyze_deps.parse_frontmatter(content)
        self.assertEqual(fm.get("name"), "multi-field")
        self.assertEqual(fm.get("version"), "1.0.0")
        self.assertEqual(fm.get("description"), "Test skill")
        self.assertEqual(fm.get("author"), "test")


if __name__ == "__main__":
    unittest.main(verbosity=2)

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)