Skill: wangjipeng977/text-to-sql (version 2.0.3)

--- name: text-to-sql description: > Use when (1) user describes what data they want in plain English and asks for the corresponding SQL query. (2) user says "write SQL for this", "convert to query", "how do I select", or "give me the SQL". (3) user provides a database schema or table descriptions and asks a question answerable by SQL. license: MIT metadata: version: "1.0.1" category: data author: wangjipeng sources: - https://github.com/MiniMax-AI/skills --- # Text to SQL Use when (1) user describes what data they want in plain English and asks for the corresponding SQL query. (2) user says "write SQL for this", "convert to query", "how do I select", or "give me the SQL". (3) user provides a database schema or table descriptions and asks a question answerable by SQL. ## Core Position This skill solves the specific problem of: *non-technical users who know what data they want cannot translate their intent into SQL — they need a bridge from natural language to query.* This skill IS NOT: - A SQL execution environment — it writes queries, does not run them - A schema design tool — it works with existing schema the user provides - A data analysis tool — it produces SQL, not results or insights This skill IS activated ONLY when: natural language description + database schema + SQL request are all present. ## Modes ### `/text-to-sql` **Default mode.** Converts natural language into a syntactically correct SQL query. When to use: User describes data needs and provides schema — wants the query. ### `/text-to-sql/explain` Outputs the SQL query with inline comments explaining each clause. When to use: User wants to understand the query while seeing it, for learning purposes. ### `/text-to-sql/alternatives` Provides 2-3 alternative query approaches (different JOINs, subqueries vs CTEs, etc.). When to use: User is learning SQL or wants to compare query strategies. ## Execution Steps ### Step 1 — Confirm Schema 1. Receive natural language request and detect if schema is present 2. Schema may be provided as: - Table/column names explicitly in the request - A CREATE TABLE statement - A DESCRIBE output - Column names from a previous query 3. If schema is NOT provided, ask the user for it before proceeding — do not guess table or column names 4. Build a schema map: `table_name → {column: type}` ### Step 2 — Translate Intent to SQL Clauses Map natural language intent to SQL components: | Natural Language | SQL Clause | |---|---| | "all", "every", "complete list" | `SELECT *` or `SELECT all columns` | | "only", "just", "specifically" | `SELECT [specific columns]` | | "where [condition]" | `WHERE` clause | | "sorted by", "in order of" | `ORDER BY` | | "grouped by", "each [X]" | `GROUP BY` | | "top N", "first N", "N most" | `LIMIT N` + `ORDER BY` | | "not", "exclude", "without" | `WHERE NOT` or `!=` / `<>` | | "both X and Y", "along with" | `AND` in WHERE, or JOIN | | "either X or Y", "or" | `OR` in WHERE | | "between X and Y" | `BETWEEN` | | "like", "containing", "includes" | `LIKE '%value%'` | | "before", "after", "earlier than" | `WHERE date_column < 'date'` | | "latest", "most recent", "newest" | `ORDER BY date DESC LIMIT 1` | | "count of", "how many" | `COUNT(*)` aggregate | | "total of", "sum of" | `SUM(column)` | | "average of" | `AVG(column)` | ### Step 3 — Handle Joins and Relationships If the request involves multiple tables: 1. Identify which tables contain the needed columns 2. Determine the join key (foreign key relationship) 3. Select join type: `INNER JOIN` (default), `LEFT JOIN` (if some side may be empty), `RIGHT JOIN` (rare) 4. Write join on the correct key pair If schema doesn't include relationship info, ask user to clarify which column links the tables. ### Step 4 — Generate and Validate ```sql SELECT o.order_id, o.created_at, c.customer_name, SUM(o.total_amount) AS total_revenue FROM orders o INNER JOIN customers c ON o.customer_id = c.id WHERE o.created_at >= '2024-01-01' GROUP BY o.order_id, o.created_at, c.customer_name ORDER BY total_revenue DESC LIMIT 10; ``` Check: - All columns referenced exist in the schema - All table aliases are defined - JOIN conditions are valid (same type, correct keys) - No ambiguous column references (all tables have aliases) - Aggregate queries have appropriate GROUP BY ## Mandatory Rules ### Do not - Do not invent table names or column names not in the provided schema - Do not use SQL keywords as column names without backtick quoting where needed - Do not write `SELECT *` in production queries — list specific columns - Do not assume which table a column belongs to — qualify all column references ### Do - Ask for schema information before writing the query if it wasn't provided - Qualify all column references with table aliases (e.g., `o.order_id`) - Use backtick or quoted identifiers if column names are SQL reserved words - Provide both the query and a one-line plain English translation of what it does ## Quality Bar **A good output:** - Query is syntactically correct for the stated dialect (PostgreSQL, MySQL, SQLite, etc.) - All column and table names match the provided schema exactly - JOIN conditions are valid and use the correct key types - The query actually answers the stated question **A bad output:** - References a column not in the schema - `SELECT *` without justification in a query that should return specific columns - Missing `GROUP BY` for an aggregate query - JOIN on mismatched types (string ID to integer ID) ## Good vs. Bad Examples | Scenario | Bad Output | Good Output | |---|---|---| | Schema: `users(id, name)`, `orders(user_id, total)` | `SELECT * FROM orders` | `SELECT u.name, SUM(o.total) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name` | | "top 10 customers" | No `LIMIT 10` or `ORDER BY` | `ORDER BY total DESC LIMIT 10` | | No schema provided | Writes a query with invented columns | "Could you share the table schema (column names and types)?" | | "show me revenue by month" | `SELECT revenue` without `GROUP BY` | `SELECT DATE_TRUNC('month', date), SUM(revenue) FROM orders GROUP BY 1` | ## References - `references/` — SQL dialect cheat sheet (PostgreSQL, MySQL, SQLite), JOIN types and when to use each, common intent-to-clause mapping

path: CHANGELOG.md
content: # Changelog
## [1.0.1] - 2026-05-18

### Minor update

- **Previous:** 1.0
- **Changed:** Updated skill content and quality



## [1.0] - 2026-05-18

### Added

- Initial release


path: CONTRIBUTING.md
content: # Contributing to `User Describes Data`

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>/User Describes Data.git
cd User Describes Data

# 3. Install dependencies (if any)
pip install -r scripts/requirements.txt

# 4. Run the self-audit to verify quality
python scripts/audit_skill.py .
```

## 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 .

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

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

# 6. Open a Pull Request on GitHub
#    - Title: feat(text-to-sql): 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; reference docs in English
- **No secrets**: Never commit API keys, tokens, or credentials

## Quality Checklist

Before opening a PR, verify:

- [ ] `audit_skill.py` exits with code 0 or 2
- [ ] `validate_skills.py` exits with code 0
- [ ] README.md and README_zh.md are both present
- [ ] CONTRIBUTING.md is present
- [ ] .gitignore is present
- [ ] No hardcoded secrets anywhere in the codebase

## 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, etc.)

## License

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

path: README.md
content: # Text To Sql

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

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

> Converts natural language descriptions into syntactically correct SQL queries

## What Problem This Solves

Non-technical users know what data they want ("show me revenue by month for the top 10 customers") but can't write SQL. This skill bridges the gap — takes a schema + natural language request and produces a syntactically correct query with table aliases, proper JOINs, and GROUP BY.

**When triggered:** Database schema + natural language question + write SQL intent.

## Features

- **Schema-aware translation** — asks for table/column schema before writing queries (never invents column names)
- **Complete SQL coverage** — SELECT, WHERE, GROUP BY, ORDER BY, LIMIT, JOIN (INNER, LEFT, RIGHT), aggregate functions
- **Plain English explanation** — outputs both the query AND a one-line description of what it does
- **Multiple approach options** — `/alternatives` mode shows different JOIN strategies or subquery vs CTE approaches

## Quick Start

```bash
# Via ClawHub
clawhub install text-to-sql

# Or manually
cp -r text-to-sql ~/.openclaw/skills/
```

### Usage

```
/text-to-sql
```

Provide schema (table names + columns) and describe what data you want in English.

```
/text-to-sql/explain
```

Outputs query with inline comments explaining each clause — for learning purposes.

```
/text-to-sql/alternatives
```

Shows 2-3 different query approaches for the same question.

## Modes

| Mode | Description |
|------|-------------|
| `/text-to-sql` | Converts natural language to SQL query |
| `/text-to-sql/explain` | Query with inline comments explaining each clause |
| `/text-to-sql/alternatives` | 2-3 alternative query strategies |

## Examples

| Request | Query |
|---------|-------|
| Schema: `users(id, name)`, `orders(user_id, total)` | `SELECT u.name, SUM(o.total) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name` |
| "top 10 customers" | `ORDER BY total DESC LIMIT 10` added |
| "revenue by month" | `SELECT DATE_TRUNC('month', date), SUM(revenue) FROM orders GROUP BY 1` |
| No schema provided | Asks for schema first — doesn't guess column names |

## Directory Structure

```
text-to-sql/
├── SKILL.md
├── LICENSE
├── README.md
├── README_zh.md
├── CONTRIBUTING.md
├── .gitignore
├── references/       # SQL dialect cheat sheet, JOIN guide, intent mapping
└── tests/
```

## License

MIT License — see [LICENSE](LICENSE).

path: README_zh.md
content: # Text To Sql

[English](./README.md)

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

> 将自然语言描述转换为语法正确的 SQL 查询

## 解决什么问题

非技术用户知道想要什么数据（"显示前 10 客户每月收入"）但不会写 SQL。这个技能弥合这个差距——接收 schema + 自然语言请求，生成带表别名、正确 JOIN 和 GROUP BY 的语法正确的查询。

**触发条件：** 数据库 schema + 自然语言问题 + 写 SQL 意图。

## 功能特性

- **Schema 感知翻译** — 写查询前先询问表/列 schema（绝不虚构列名）
- **完整 SQL 覆盖** — SELECT、WHERE、GROUP BY、ORDER BY、LIMIT、JOIN（INNER、LEFT、RIGHT）、聚合函数
- **通俗英文解释** — 同时输出查询和一行描述其作用
- **多方法选项** — `/alternatives` 模式展示不同 JOIN 策略或子查询 vs CTE 方法

## 快速开始

```bash
# 通过 ClawHub 安装
clawhub install text-to-sql

# 或手动复制
cp -r text-to-sql ~/.openclaw/skills/
```

### 使用方法

```
/text-to-sql
```

提供 schema（表名 + 列）并用英文描述想要什么数据。

```
/text-to-sql/explain
```

输出带内联注释的查询，解释每个子句——用于学习。

```
/text-to-sql/alternatives
```

展示同一问题的 2-3 种不同查询方法。

## 工作模式

| 模式 | 说明 |
|------|------|
| `/text-to-sql` | 将自然语言转换为 SQL 查询 |
| `/text-to-sql/explain` | 带内联注释的查询，解释每个子句 |
| `/text-to-sql/alternatives` | 2-3 种替代查询策略 |

## 示例

| 请求 | 查询 |
|---------|-------|
| Schema: `users(id, name)`，`orders(user_id, total)` | `SELECT u.name, SUM(o.total) FROM users u JOIN orders o ON u.id = o.user_id GROUP BY u.name` |
| "前 10 客户" | 添加 `ORDER BY total DESC LIMIT 10` |
| "按月收入" | `SELECT DATE_TRUNC('month', date), SUM(revenue) FROM orders GROUP BY 1` |
| 未提供 schema | 先询问 schema——不猜测列名 |

## 目录结构

```
text-to-sql/
├── SKILL.md
├── LICENSE
├── README.md
├── README_zh.md
├── CONTRIBUTING.md
├── .gitignore
├── references/       # SQL 方言速查表、JOIN 指南、意图映射
└── tests/
```

## 许可证

MIT 许可证 — 详见 [LICENSE](LICENSE)。

path: references/index.md
content: # text-to-sql — References

Detailed documents for `user-describes-data` skill.

TODO: Add reference files here as needed.


path: tests/test_skill.py
content: #!/usr/bin/env python3
"""
Auto-generated test suite for skill: user-describes-data
Run with: python tests/test_skill.py
"""

import sys, os, re, yaml

def _p(name, passed, msg=''):
    emoji = "\u2705" if passed else "\u274c"
    result = "PASS" if passed else "FAIL"
    print(f"  [{emoji}] {result} -- {name}{msg}")
    return passed

def _skill_md():
    return os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'SKILL.md')

def _skill_dir():
    return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

def _frontmatter():
    with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
        text = f.read()
    end = text.find('\n---', 3)
    if end == -1:
        return None
    try:
        return yaml.safe_load(text[3:end]) or {}
    except Exception:
        return None

def test_frontmatter_delimiters():
        with open(_skill_md(), "r", encoding="utf-8", errors="ignore") as f:
            text = f.read()
        return _p("frontmatter_delimiters", text.startswith("---"))

def test_frontmatter_name():
        fm = _frontmatter()
        if fm is None:
            return _p('frontmatter_name', False, ' -- no frontmatter')
        name = fm.get('name', '').strip()
        skill_dir = os.path.basename(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
        return _p('frontmatter_name', name == skill_dir,
                 f" (got '{name}', expected '{skill_dir}')")

def test_frontmatter_name_is_kebab_case():
        fm = _frontmatter()
        if fm is None:
            return _p('name_is_kebab_case', False)
        name = fm.get('name', '')
        is_kebab = bool(re.match(r'^[a-z0-9]+(-[a-z0-9]+)*$', name))
        return _p('name_is_kebab_case', is_kebab, f" (got '{name}')")

def test_frontmatter_description():
        fm = _frontmatter()
        if fm is None:
            return _p('frontmatter_description', False)
        return _p('frontmatter_description', bool(fm.get('description', '').strip()))

def test_description_has_trigger_conditions():
        fm = _frontmatter()
        if fm is None:
            return _p('description_is_trigger', False)
        desc = fm.get('description', '')
        has_trigger = ('use when' in desc.lower() or
                      re.search(r'\([0-9]+\)', desc) or
                      ('when the user' in desc.lower()))
        return _p('description_is_trigger', has_trigger,
                  ' (must be trigger condition, not capability statement)')

def test_description_length():
        fm = _frontmatter()
        if fm is None:
            return _p('description_length', False)
        desc = fm.get('description', '')
        length = len(desc.strip())
        ok = 50 <= length <= 500
        hint = 'optimal' if ok else f'{length} chars'
        return _p('description_length', ok, f' ({hint} -- optimal: 50-500)')

def test_frontmatter_license():
        fm = _frontmatter()
        if fm is None:
            return _p('frontmatter_license', False)
        lic = fm.get('license', '').strip().upper()
        return _p('frontmatter_license', lic == 'MIT' or 'MIT' in lic)

def test_frontmatter_metadata():
        fm = _frontmatter()
        if fm is None:
            return _p('frontmatter_metadata', False)
        meta = fm.get('metadata', {}) or {}
        has_version = bool(str(meta.get('version', '')).strip())
        has_category = bool(str(meta.get('category', '')).strip())
        return _p('frontmatter_metadata', has_version and has_category,
                  f' (version={has_version}, category={has_category})')

def test_has_modes():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        return _p('has_modes', '## Modes' in text or '## Mode' in text or '## Core Position' in text)

def test_modes_are_distinct():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        mode_blocks = re.findall(r'^#{1,3} [^#\n].+$', text, re.MULTILINE)
        distinct = len(mode_blocks)
        return _p('modes_distinct', distinct >= 2,
                  f' ({distinct} distinct sections -- need 2+ for multi-mode)')

def test_has_do_not():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        return _p('has_do_not', 'Do not' in text or 'do-not' in text or 'Must not' in text)

def test_do_not_section_has_content():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        do_not_match = re.search(r'(?i)##?\s*(do not|mandatory rules)[:\n](.+?)(?=##|\Z)', text, re.DOTALL)
        if not do_not_match:
            return _p('do_not_has_rules', False, ' (no Do not section found)')
        do_not_text = do_not_match.group(1)
        rules = re.findall(r'^\s*[-*]\s+\w', do_not_text, re.MULTILINE)
        return _p('do_not_has_rules', len(rules) >= 2,
                  f' ({len(rules)} rules -- need at least 2)')

def test_execution_steps_are_numbered():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        has_numbered = bool(re.search(r'(?:^|\n)\d+\.?\s+\w', text, re.MULTILINE))
        return _p('steps_numbered', has_numbered,
                  ' (execution steps must be numbered, not bullets or prose)')

def test_has_quality_bar():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        return _p('has_quality_bar', 'Quality Bar' in text or 'quality bar' in text)

def test_quality_bar_has_criteria():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        qb_match = re.search(r'(?i)##?\s*quality bar[:\n](.+?)(?=##|\Z)', text, re.DOTALL)
        if not qb_match:
            return _p('quality_bar_has_criteria', False, ' (no Quality Bar section)')
        qb_text = qb_match.group(1).lower()
        concrete_markers = ['must have', 'must be', 'must not', 'a good output', 'a bad output',
                            'is present', 'is valid', 'returns', 'provides', 'has a']
        has_concrete = any(m in qb_text for m in concrete_markers)
        return _p('quality_bar_has_criteria', has_concrete,
                  ' (Quality Bar must have concrete observable criteria)')

def test_has_good_bad_examples():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            text = f.read()
        return _p('has_good_bad_examples', 'Good' in text and 'Bad' in text)

def test_no_secrets():
        SECRET_PATTERNS = [
            r'sk-[A-Za-z0-9]{20,}',
            r'sk-[A-Za-z0-9][A-Za-z0-9-]{19,}',
            r'AKIA[A-Z0-9]{16}',
            r'ghp_[A-Za-z0-9]{36}',
            r"(?i)api[_-]?key\s*[=:]\s*['\"]?[A-Za-z0-9_-]{20,}",
        ]
        skill_path = os.path.dirname(os.path.abspath(__file__))
        passed = True
        for root, dirs, files in os.walk(skill_path):
            dirs[:] = [d for d in dirs if not d.startswith('.') and 'installed_skills' not in d]
            for fname in files:
                if fname.startswith('.'):
                    continue
                fpath = os.path.join(root, fname)
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                        content = f.read()
                except Exception:
                    continue
                for pat in SECRET_PATTERNS:
                    if re.search(pat, content):
                        rel = os.path.relpath(fpath, skill_path)
                        print(f'  \u274c FAIL -- secret found: {rel}')
                        passed = False
                        break
        return _p('no_secrets', passed)

def test_readme_zh_exists():
        return _p('readme_zh_exists', os.path.isfile(os.path.join(_skill_dir(), 'README_zh.md')))

def test_contributing_exists():
        return _p('contributing_exists', os.path.isfile(os.path.join(_skill_dir(), 'CONTRIBUTING.md')))

def test_gitignore_exists():
        return _p('gitignore_exists', os.path.isfile(os.path.join(_skill_dir(), '.gitignore')))

def test_tests_dir_not_empty():
        tests_dir = os.path.join(_skill_dir(), 'tests')
        if not os.path.isdir(tests_dir):
            return _p('tests_dir_not_empty', False, ' (tests/ dir missing)')
        has_tests = any(f.startswith('test_') and f.endswith('.py')
                        for f in os.listdir(tests_dir) if not f.startswith('.'))
        return _p('tests_dir_not_empty', has_tests)

def test_license_badge_in_readme():
        readme = os.path.join(_skill_dir(), 'README.md')
        if not os.path.isfile(readme):
            return _p('license_badge_in_readme', False, ' (README.md missing)')
        with open(readme, 'r', encoding='utf-8', errors='ignore') as f:
            content = f.read().lower()
        has_badge = 'license' in content and ('mit' in content or 'apache' in content or 'gpl' in content)
        return _p('license_badge_in_readme', has_badge)

def test_scripts_shebangs():
        scripts_dir = os.path.join(_skill_dir(), 'scripts')
        if not os.path.isdir(scripts_dir):
            return _p('scripts_shebangs_skipped', True, ' (no scripts/ dir)')
        passed = True
        for fname in os.listdir(scripts_dir):
            if fname.startswith('.'):
                continue
            if fname.endswith(('.py', '.sh')):
                fpath = os.path.join(scripts_dir, fname)
                try:
                    with open(fpath, 'r', encoding='utf-8', errors='ignore') as f:
                        first = f.read(10)
                    if not first.startswith('#!'):
                        print(f'  \u274c FAIL -- missing shebang: scripts/{fname}')
                        passed = False
                except Exception:
                    pass
        return _p('scripts_have_shebangs', passed)

def test_skill_md_size():
        with open(_skill_md(), 'r', encoding='utf-8', errors='ignore') as f:
            content = f.read()
        line_count = content.count('\n') + 1
        if line_count <= 400:
            hint = '\u2705 good'
        elif line_count <= 800:
            hint = '\u26a0\ufe0f consider extracting to references/'
        else:
            hint = '\u274c too large'
        return _p('skill_md_size', line_count <= 800,
                  f' ({line_count} lines -- {hint})')

def _main():
    print()
    print("=" * 60)
    print(f"  🧪 TEST SUITE -- skill-factory")
    print("=" * 60)
    print()
    tests = [
        test_frontmatter_delimiters,
        test_frontmatter_name,
        test_frontmatter_name_is_kebab_case,
        test_frontmatter_description,
        test_description_has_trigger_conditions,
        test_description_length,
        test_frontmatter_license,
        test_frontmatter_metadata,
        test_has_modes,
        test_modes_are_distinct,
        test_has_do_not,
        test_do_not_section_has_content,
        test_execution_steps_are_numbered,
        test_has_quality_bar,
        test_quality_bar_has_criteria,
        test_has_good_bad_examples,
        test_no_secrets,
        test_readme_zh_exists,
        test_contributing_exists,
        test_gitignore_exists,
        test_tests_dir_not_empty,
        test_license_badge_in_readme,
        test_scripts_shebangs,
        test_skill_md_size,
    ]
    results = []
    for t in tests:
        try:
            results.append(t())
        except Exception as e:
            print(f"  \u274c FAIL -- {t.__name__} raised {e}")
            results.append(False)
    passed = sum(results)
    total = len(results)
    print()
    print("=" * 60)
    print(f"  \U0001f4ca RESULTS: {passed}/{total} passed")
    print("=" * 60)
    if passed == total:
        print("  \u2705 ALL TESTS PASSED")
    else:
        print(f'  \u274c {total - passed} test(s) FAILED -- fix before submission')
    print()
    return 0 if passed == total else 1

if __name__ == '__main__':
    sys.exit(_main())


def _main():
    _run_unit_tests()
    return 0


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