From f682cc596575d5947221235cb3601bfd615ad691 Mon Sep 17 00:00:00 2001 From: WhereIs38 Date: Tue, 7 Jul 2026 19:00:55 +0800 Subject: [PATCH] rename contrib/multilingual to contrib/batch_scan and update README usage Signed-off-by: WhereIs38 --- README.md | 27 +++++++ contrib/batch_scan/.env.example | 24 ++++++ .../CONTRIBUTING.md | 32 ++++---- .../{multilingual => batch_scan}/__init__.py | 0 .../annotation.py | 0 .../{multilingual => batch_scan}/api_pool.py | 0 .../batch_scan.py | 6 +- .../{multilingual => batch_scan}/detection.py | 0 .../{multilingual => batch_scan}/discovery.py | 0 .../docs/DESIGN.md | 4 +- .../docs/README.md | 76 +++++++++---------- .../docs/REVIEW_RESPONSE.md | 0 .../docs/archive/ARCHITECTURE_DEEP_DIVE.md | 2 +- .../docs/archive/DESIGN_HISTORY.md | 6 +- .../docs/archive/FLOW_DIAGRAM.md | 2 +- .../docs/archive/FUTURE_WORK.md | 0 .../docs/archive/PITFALLS.md | 6 +- .../{multilingual => batch_scan}/gap_fill.py | 0 .../{multilingual => batch_scan}/reports.py | 2 +- .../{multilingual => batch_scan}/runner.py | 0 .../tests/conftest.py | 4 +- .../tests/docs/BUGS_FOUND.md | 0 .../tests/docs/TEST_DESIGN.md | 2 +- .../tests/docs/TEST_GUIDE.md | 16 ++-- .../tests/test_monkeypatch_fragility.py | 8 +- .../tests/test_monkeypatch_invasiveness.py | 8 +- .../tests/test_pool_wiring.py | 8 +- .../tests/tests-pro/__init__.py | 2 +- .../tests/tests-pro/mutation_max.py | 76 +++++++++---------- .../tests/tests-pro/random_numbered.py | 2 +- .../tests/tests-pro/test_annotation.py | 2 +- .../tests/tests-pro/test_api_pool.py | 2 +- .../tests/tests-pro/test_gap_fill.py | 2 +- .../tests/tests-pro/test_runner_patches.py | 50 ++++++------ contrib/multilingual/.env.example | 27 ------- 35 files changed, 210 insertions(+), 186 deletions(-) create mode 100644 contrib/batch_scan/.env.example rename contrib/{multilingual => batch_scan}/CONTRIBUTING.md (81%) rename contrib/{multilingual => batch_scan}/__init__.py (100%) rename contrib/{multilingual => batch_scan}/annotation.py (100%) rename contrib/{multilingual => batch_scan}/api_pool.py (100%) rename contrib/{multilingual => batch_scan}/batch_scan.py (98%) rename contrib/{multilingual => batch_scan}/detection.py (100%) rename contrib/{multilingual => batch_scan}/discovery.py (100%) rename contrib/{multilingual => batch_scan}/docs/DESIGN.md (99%) rename contrib/{multilingual => batch_scan}/docs/README.md (82%) rename contrib/{multilingual => batch_scan}/docs/REVIEW_RESPONSE.md (100%) rename contrib/{multilingual => batch_scan}/docs/archive/ARCHITECTURE_DEEP_DIVE.md (99%) rename contrib/{multilingual => batch_scan}/docs/archive/DESIGN_HISTORY.md (98%) rename contrib/{multilingual => batch_scan}/docs/archive/FLOW_DIAGRAM.md (99%) rename contrib/{multilingual => batch_scan}/docs/archive/FUTURE_WORK.md (100%) rename contrib/{multilingual => batch_scan}/docs/archive/PITFALLS.md (97%) rename contrib/{multilingual => batch_scan}/gap_fill.py (100%) rename contrib/{multilingual => batch_scan}/reports.py (99%) rename contrib/{multilingual => batch_scan}/runner.py (100%) rename contrib/{multilingual => batch_scan}/tests/conftest.py (87%) rename contrib/{multilingual => batch_scan}/tests/docs/BUGS_FOUND.md (100%) rename contrib/{multilingual => batch_scan}/tests/docs/TEST_DESIGN.md (99%) rename contrib/{multilingual => batch_scan}/tests/docs/TEST_GUIDE.md (94%) rename contrib/{multilingual => batch_scan}/tests/test_monkeypatch_fragility.py (98%) rename contrib/{multilingual => batch_scan}/tests/test_monkeypatch_invasiveness.py (98%) rename contrib/{multilingual => batch_scan}/tests/test_pool_wiring.py (93%) rename contrib/{multilingual => batch_scan}/tests/tests-pro/__init__.py (88%) rename contrib/{multilingual => batch_scan}/tests/tests-pro/mutation_max.py (91%) rename contrib/{multilingual => batch_scan}/tests/tests-pro/random_numbered.py (97%) rename contrib/{multilingual => batch_scan}/tests/tests-pro/test_annotation.py (98%) rename contrib/{multilingual => batch_scan}/tests/tests-pro/test_api_pool.py (99%) rename contrib/{multilingual => batch_scan}/tests/tests-pro/test_gap_fill.py (99%) rename contrib/{multilingual => batch_scan}/tests/tests-pro/test_runner_patches.py (94%) delete mode 100644 contrib/multilingual/.env.example diff --git a/README.md b/README.md index 4a09b50b..dc79e5ae 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,33 @@ skillspector scan ./my-skill/ --format markdown --output report.md skillspector scan ./my-skill/ --format sarif --output report.sarif ``` +### Batch Scanning + +Scan entire directories of skills in parallel from `contrib/batch_scan/`: + +```bash +python -m contrib.batch_scan.batch_scan ./my-skills/ --no-llm +python -m contrib.batch_scan.batch_scan ./my-skills/ --workers 20 -f json -o report.json +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 20 +``` + +Supports multilingual detection (zh/ja/ko) and terminal/JSON/Markdown output. + +For LLM scans with higher concurrency, configure multiple API keys following +[`.env.example`](contrib/batch_scan/.env.example) — the pool improves throughput +and resilience, provided the keys don't share an account-level rate limit. + +See the [contrib guide](contrib/batch_scan/docs/) for details. + +> **Note on LLM support:** The default configuration targets DeepSeek as the +> cheapest public option. DeepSeek-Chat is +> [expected to sunset](https://api-docs.deepseek.com/), and the contributor +> does not have hardware to test against local models. The batch scanner was +> originally tested with OpenAI-compatible endpoints — DeepSeek's lack of +> structured-output support required manual JSON-parsing patches. If you can +> contribute a more universal backend (Ollama, vLLM, or a different provider), +> PRs are very welcome. + ### Suppressing False Positives (baseline) Suppress known/accepted findings so the risk score reflects only un-triaged diff --git a/contrib/batch_scan/.env.example b/contrib/batch_scan/.env.example new file mode 100644 index 00000000..7817a71d --- /dev/null +++ b/contrib/batch_scan/.env.example @@ -0,0 +1,24 @@ +# SkillSpector Batch Scanner — DO NOT COMMIT +# +# Copy to the repository root as .env: +# cp contrib/batch_scan/.env.example .env +# +# ============================================================================= +# Multi-key pool (recommended for batch scans) +# ============================================================================= +# +# Format: key|base_url|model, separated by semicolons. +# Add as many keys as you want — the pool distributes requests across them. +# ⚠️ Only helps if keys don't share an account-level rate limit. +# +SKILLSPECTOR_API_KEYS="sk-or-xxx1|https://api.deepseek.com|deepseek-chat;sk-or-xxx2|https://api.deepseek.com|deepseek-chat;sk-or-xxx3|https://api.openai.com/v1|gpt-5.4" + +# Force OpenAI-compatible provider mode +SKILLSPECTOR_PROVIDER=openai + +# Single-key fallback (ignored when SKILLSPECTOR_API_KEYS is set) +OPENAI_API_KEY=sk-or-xxxxxxxxxxxxxxxxxxxxxxxx +OPENAI_BASE_URL=https://api.deepseek.com + +SKILLSPECTOR_MODEL=deepseek-chat +SKILLSPECTOR_LOG_LEVEL=WARNING diff --git a/contrib/multilingual/CONTRIBUTING.md b/contrib/batch_scan/CONTRIBUTING.md similarity index 81% rename from contrib/multilingual/CONTRIBUTING.md rename to contrib/batch_scan/CONTRIBUTING.md index 99f6e131..ea14f016 100644 --- a/contrib/multilingual/CONTRIBUTING.md +++ b/contrib/batch_scan/CONTRIBUTING.md @@ -10,12 +10,12 @@ python3 -m venv .venv source .venv/bin/activate pip install -e . -cp contrib/multilingual/.env.example .env # edit with your API keys +cp contrib/batch_scan/.env.example .env # edit with your API keys ``` Verify everything works: ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 8 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 ``` --- @@ -23,7 +23,7 @@ python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --worker ## Project Map ``` -contrib/multilingual/ +contrib/batch_scan/ ├── batch_scan.py # CLI entry + ThreadPoolExecutor (start here) ├── runner.py # graph.invoke() wrapper + 7 patches + pool wiring (core) ├── gap_fill.py # GapFillAnalyzer — LLM pass for 8 uncovered rules @@ -63,30 +63,30 @@ contrib/multilingual/ ```bash # All 164 tests -python contrib/multilingual/tests/tests-pro/random_numbered.py # 120 unit (seed=42) -python contrib/multilingual/tests/test_pool_wiring.py # 4 smoke checks -python contrib/multilingual/tests/test_monkeypatch_invasiveness.py # 14 thematic -python contrib/multilingual/tests/test_monkeypatch_fragility.py # 26 thematic +python contrib/batch_scan/tests/tests-pro/random_numbered.py # 120 unit (seed=42) +python contrib/batch_scan/tests/test_pool_wiring.py # 4 smoke checks +python contrib/batch_scan/tests/test_monkeypatch_invasiveness.py # 14 thematic +python contrib/batch_scan/tests/test_monkeypatch_fragility.py # 26 thematic # Review-themed only python -m unittest \ - contrib.multilingual.tests.test_monkeypatch_invasiveness \ - contrib.multilingual.tests.test_monkeypatch_fragility -v -python contrib/multilingual/tests/test_pool_wiring.py + contrib.batch_scan.tests.test_monkeypatch_invasiveness \ + contrib.batch_scan.tests.test_monkeypatch_fragility -v +python contrib/batch_scan/tests/test_pool_wiring.py # Mutation test -python contrib/multilingual/tests/tests-pro/mutation_max.py +python contrib/batch_scan/tests/tests-pro/mutation_max.py # End-to-end (fixture suite) -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 8 -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 8 --no-llm +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 --no-llm ``` **Three commands catch most regressions:** ```bash -python contrib/multilingual/tests/tests-pro/random_numbered.py -python contrib/multilingual/tests/test_pool_wiring.py -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 8 +python contrib/batch_scan/tests/tests-pro/random_numbered.py +python contrib/batch_scan/tests/test_pool_wiring.py +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 ``` --- diff --git a/contrib/multilingual/__init__.py b/contrib/batch_scan/__init__.py similarity index 100% rename from contrib/multilingual/__init__.py rename to contrib/batch_scan/__init__.py diff --git a/contrib/multilingual/annotation.py b/contrib/batch_scan/annotation.py similarity index 100% rename from contrib/multilingual/annotation.py rename to contrib/batch_scan/annotation.py diff --git a/contrib/multilingual/api_pool.py b/contrib/batch_scan/api_pool.py similarity index 100% rename from contrib/multilingual/api_pool.py rename to contrib/batch_scan/api_pool.py diff --git a/contrib/multilingual/batch_scan.py b/contrib/batch_scan/batch_scan.py similarity index 98% rename from contrib/multilingual/batch_scan.py rename to contrib/batch_scan/batch_scan.py index a75aa06a..d68cacea 100644 --- a/contrib/multilingual/batch_scan.py +++ b/contrib/batch_scan/batch_scan.py @@ -40,9 +40,9 @@ Usage:: - python -m contrib.multilingual.batch_scan ./skills/ --no-llm - python -m contrib.multilingual.batch_scan ./skills/ -f json -o report.json - python -m contrib.multilingual.batch_scan ./skills/ --lang zh --workers 8 + python -m contrib.batch_scan.batch_scan ./skills/ --no-llm + python -m contrib.batch_scan.batch_scan ./skills/ -f json -o report.json + python -m contrib.batch_scan.batch_scan ./skills/ --lang zh --workers 8 """ from __future__ import annotations diff --git a/contrib/multilingual/detection.py b/contrib/batch_scan/detection.py similarity index 100% rename from contrib/multilingual/detection.py rename to contrib/batch_scan/detection.py diff --git a/contrib/multilingual/discovery.py b/contrib/batch_scan/discovery.py similarity index 100% rename from contrib/multilingual/discovery.py rename to contrib/batch_scan/discovery.py diff --git a/contrib/multilingual/docs/DESIGN.md b/contrib/batch_scan/docs/DESIGN.md similarity index 99% rename from contrib/multilingual/docs/DESIGN.md rename to contrib/batch_scan/docs/DESIGN.md index 4f330095..bb44ca0b 100644 --- a/contrib/multilingual/docs/DESIGN.md +++ b/contrib/batch_scan/docs/DESIGN.md @@ -8,7 +8,7 @@ ``` CLI - │ python -m contrib.multilingual.batch_scan ./tests/fixtures/ --workers 7 + │ python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 7 │ ▼ batch_scan.py :: main() @@ -187,7 +187,7 @@ HTTP-level timeouts (Patch 6) prevent most hangs from reaching the 90s ceiling. ## File layout ``` -contrib/multilingual/ +contrib/batch_scan/ ├── __init__.py # package init + dotenv preload ├── batch_scan.py # CLI + ThreadPoolExecutor ├── runner.py # graph wrapper + setup_deepseek_compat() diff --git a/contrib/multilingual/docs/README.md b/contrib/batch_scan/docs/README.md similarity index 82% rename from contrib/multilingual/docs/README.md rename to contrib/batch_scan/docs/README.md index fa2bdf4a..87c5dc57 100644 --- a/contrib/multilingual/docs/README.md +++ b/contrib/batch_scan/docs/README.md @@ -15,7 +15,7 @@ Zero changes to upstream `src/skillspector/`. ## What it does ``` -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 7 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 7 ``` 1. Finds all `SKILL.md`-containing directories under the input root @@ -38,7 +38,7 @@ source .venv/bin/activate pip install -e . # Copy and edit the environment template -cp contrib/multilingual/.env.example .env +cp contrib/batch_scan/.env.example .env ``` The `.env` file needs these keys (see `.env.example` for the full template): @@ -60,19 +60,19 @@ The `.env` file needs these keys (see `.env.example` for the full template): ### Static-only (fast, no API keys needed) ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --no-llm +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-llm ``` ### Full LLM scan ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 7 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 7 ``` ### Test with built-in fixtures ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 8 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 ``` 23 skills designed to exercise every detection rule. @@ -197,7 +197,7 @@ static rules, LLM finds 2–8 additional issues per skill. skillspector scan ./tests/fixtures/malicious_skill/ -f json -o upstream.json # Batch — scan all skills -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f json -o batch.json +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o batch.json ``` Key differences in batch output: @@ -211,49 +211,49 @@ Key differences in batch output: ### Scan (LLM mode) ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 7 # default -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 1 # sequential, easy to read -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 20 # high throughput +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 7 # default +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 1 # sequential, easy to read +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 20 # high throughput ``` ### Scan (static-only, no API keys) ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --no-llm -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --no-require-llm --no-llm # skip LLM even for non-English +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-llm +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-require-llm --no-llm # skip LLM even for non-English ``` ### Output formats ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal # default (Rich) -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f json -o report.json -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f markdown -o report.md +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal # default (Rich) +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f markdown -o report.md ``` ### Fixture test (built-in 23 skills) ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 8 -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 8 --no-llm -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f json -o report.json --workers 8 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 --no-llm +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json --workers 8 ``` ### Language override ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --lang auto --workers 4 # detect (default) -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --lang zh -f terminal --workers 4 -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --lang ja -f terminal --workers 4 -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --lang ko -f terminal --workers 4 -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --lang en -f terminal --workers 4 # skip gap-fill +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang auto --workers 4 # detect (default) +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang zh -f terminal --workers 4 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang ja -f terminal --workers 4 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang ko -f terminal --workers 4 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --lang en -f terminal --workers 4 # skip gap-fill ``` ### Debugging ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --workers 1 -V # single worker + verbose -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --workers 4 -V +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 1 -V # single worker + verbose +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 4 -V skillspector scan ./tests/fixtures/malicious_skill/ --no-llm # verify upstream works ``` @@ -261,13 +261,13 @@ skillspector scan ./tests/fixtures/malicious_skill/ --no-llm # ```bash skillspector scan ./tests/fixtures/malicious_skill/ -f json -o upstream.json -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f json -o batch.json --workers 4 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o batch.json --workers 4 ``` ### CI ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f json -o report.json --workers 8 +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json --workers 8 if [ $? -eq 0 ]; then echo "All clean"; fi ``` @@ -294,7 +294,7 @@ if [ $? -eq 0 ]; then echo "All clean"; fi ```bash # Single worker + verbose output — easiest to read -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --workers 1 -V +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 1 -V # Verify upstream still works skillspector scan ./tests/fixtures/malicious_skill/ --no-llm @@ -304,7 +304,7 @@ skillspector scan ./tests/fixtures/malicious_skill/ --no-llm ```bash # Static-only + skip LLM requirement even for non-English skills -python -m contrib.multilingual.batch_scan ./tests/fixtures/ --no-require-llm --no-llm +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --no-require-llm --no-llm ``` ## Exit codes @@ -318,7 +318,7 @@ python -m contrib.multilingual.batch_scan ./tests/fixtures/ --no-require-llm --n CI usage: ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f json -o report.json +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f json -o report.json if [ $? -eq 0 ]; then echo "All clean" fi @@ -354,30 +354,30 @@ See `DESIGN.md` for architecture details and `docs/archive/FUTURE_WORK.md` for s # === All 164 tests === # Unit tests — random order (seed=42, 120 tests) -python contrib/multilingual/tests/tests-pro/random_numbered.py +python contrib/batch_scan/tests/tests-pro/random_numbered.py # Pool wiring smoke test (4 checks) -python contrib/multilingual/tests/test_pool_wiring.py +python contrib/batch_scan/tests/test_pool_wiring.py # Monkey-patch invasiveness (14 tests) -python contrib/multilingual/tests/test_monkeypatch_invasiveness.py +python contrib/batch_scan/tests/test_monkeypatch_invasiveness.py # Monkey-patch fragility (26 tests) -python contrib/multilingual/tests/test_monkeypatch_fragility.py +python contrib/batch_scan/tests/test_monkeypatch_fragility.py # === Convenience === # All review-themed tests in one command python -m unittest \ - contrib.multilingual.tests.test_monkeypatch_invasiveness \ - contrib.multilingual.tests.test_monkeypatch_fragility -v -python contrib/multilingual/tests/test_pool_wiring.py + contrib.batch_scan.tests.test_monkeypatch_invasiveness \ + contrib.batch_scan.tests.test_monkeypatch_fragility -v +python contrib/batch_scan/tests/test_pool_wiring.py # Mutation test — 30 injected bugs across 4 risk areas -python contrib/multilingual/tests/tests-pro/mutation_max.py +python contrib/batch_scan/tests/tests-pro/mutation_max.py # Sequential pytest (if pytest installed) -pytest contrib/multilingual/tests/tests-pro/ -v +pytest contrib/batch_scan/tests/tests-pro/ -v ``` ## For PR Reviewers diff --git a/contrib/multilingual/docs/REVIEW_RESPONSE.md b/contrib/batch_scan/docs/REVIEW_RESPONSE.md similarity index 100% rename from contrib/multilingual/docs/REVIEW_RESPONSE.md rename to contrib/batch_scan/docs/REVIEW_RESPONSE.md diff --git a/contrib/multilingual/docs/archive/ARCHITECTURE_DEEP_DIVE.md b/contrib/batch_scan/docs/archive/ARCHITECTURE_DEEP_DIVE.md similarity index 99% rename from contrib/multilingual/docs/archive/ARCHITECTURE_DEEP_DIVE.md rename to contrib/batch_scan/docs/archive/ARCHITECTURE_DEEP_DIVE.md index c5f17e27..81b3af13 100644 --- a/contrib/multilingual/docs/archive/ARCHITECTURE_DEEP_DIVE.md +++ b/contrib/batch_scan/docs/archive/ARCHITECTURE_DEEP_DIVE.md @@ -260,7 +260,7 @@ SKILLSPECTOR_PROVIDER env var The contrib layer sits entirely outside upstream. It imports upstream classes as parents and wraps upstream functions: ``` -contrib/multilingual/ +contrib/batch_scan/ ├── batch_scan.py ← CLI + ThreadPoolExecutor ├── runner.py ← graph.invoke() wrapper + 7 safety patches ├── gap_fill.py ← GapFillAnalyzer(LLMAnalyzerBase) diff --git a/contrib/multilingual/docs/archive/DESIGN_HISTORY.md b/contrib/batch_scan/docs/archive/DESIGN_HISTORY.md similarity index 98% rename from contrib/multilingual/docs/archive/DESIGN_HISTORY.md rename to contrib/batch_scan/docs/archive/DESIGN_HISTORY.md index cc9e2d98..84b39c36 100644 --- a/contrib/multilingual/docs/archive/DESIGN_HISTORY.md +++ b/contrib/batch_scan/docs/archive/DESIGN_HISTORY.md @@ -14,7 +14,7 @@ 1. Zero changes to `src/skillspector/` 2. Subclass and wrap, don't rewrite 3. Output comparable with standard single-skill scan -4. All extensions in `contrib/multilingual/` +4. All extensions in `contrib/batch_scan/` --- @@ -23,7 +23,7 @@ ### Four-layer model ``` -CLI layer python -m contrib.multilingual.batch_scan +CLI layer python -m contrib.batch_scan.batch_scan Scheduling layer ThreadPoolExecutor(max_workers=N) API Pool layer ApiKeyPool (multi-key scheduler) Graph layer graph.invoke() per skill (upstream, untouched) @@ -96,7 +96,7 @@ Chose stdlib `unicodedata` over ML-based detectors (e.g., `langdetect`, `fasttex ### Files created (9 source + tests + docs) ``` -contrib/multilingual/ +contrib/batch_scan/ ├── __init__.py # Package init + dotenv pre-loading ├── discovery.py # Recursive SKILL.md finder ├── detection.py # Unicode script-ratio detection diff --git a/contrib/multilingual/docs/archive/FLOW_DIAGRAM.md b/contrib/batch_scan/docs/archive/FLOW_DIAGRAM.md similarity index 99% rename from contrib/multilingual/docs/archive/FLOW_DIAGRAM.md rename to contrib/batch_scan/docs/archive/FLOW_DIAGRAM.md index 356b5490..29ccb7de 100644 --- a/contrib/multilingual/docs/archive/FLOW_DIAGRAM.md +++ b/contrib/batch_scan/docs/archive/FLOW_DIAGRAM.md @@ -4,7 +4,7 @@ ``` CLI - │ python -m contrib.multilingual.batch_scan ./tests/fixtures/ --workers 4 [--no-llm] + │ python -m contrib.batch_scan.batch_scan ./tests/fixtures/ --workers 4 [--no-llm] │ ▼ ┌──────────────────────────────────────────────────────────────────────┐ diff --git a/contrib/multilingual/docs/archive/FUTURE_WORK.md b/contrib/batch_scan/docs/archive/FUTURE_WORK.md similarity index 100% rename from contrib/multilingual/docs/archive/FUTURE_WORK.md rename to contrib/batch_scan/docs/archive/FUTURE_WORK.md diff --git a/contrib/multilingual/docs/archive/PITFALLS.md b/contrib/batch_scan/docs/archive/PITFALLS.md similarity index 97% rename from contrib/multilingual/docs/archive/PITFALLS.md rename to contrib/batch_scan/docs/archive/PITFALLS.md index d08d5de3..20ad24b2 100644 --- a/contrib/multilingual/docs/archive/PITFALLS.md +++ b/contrib/batch_scan/docs/archive/PITFALLS.md @@ -183,9 +183,9 @@ declaring a change complete. ### The fixture suite is your safety net ```bash -python -m contrib.multilingual.batch_scan ./tests/fixtures/ -f terminal --workers 8 -cd contrib/multilingual/tests/tests-pro && python random_numbered.py -python contrib/multilingual/tests/tests-pro/mutation_max.py +python -m contrib.batch_scan.batch_scan ./tests/fixtures/ -f terminal --workers 8 +cd contrib/batch_scan/tests/tests-pro && python random_numbered.py +python contrib/batch_scan/tests/tests-pro/mutation_max.py ``` Three commands catch most regressions: batch scan → unit tests → mutation tests. diff --git a/contrib/multilingual/gap_fill.py b/contrib/batch_scan/gap_fill.py similarity index 100% rename from contrib/multilingual/gap_fill.py rename to contrib/batch_scan/gap_fill.py diff --git a/contrib/multilingual/reports.py b/contrib/batch_scan/reports.py similarity index 99% rename from contrib/multilingual/reports.py rename to contrib/batch_scan/reports.py index f7b8bbab..2eb23190 100644 --- a/contrib/multilingual/reports.py +++ b/contrib/batch_scan/reports.py @@ -17,7 +17,7 @@ All three formatters accept the same ``list[dict]`` result list and produce a string. The entry shape is defined by -:func:`~contrib.multilingual.runner.entry_from_result`. +:func:`~contrib.batch_scan.runner.entry_from_result`. """ from __future__ import annotations diff --git a/contrib/multilingual/runner.py b/contrib/batch_scan/runner.py similarity index 100% rename from contrib/multilingual/runner.py rename to contrib/batch_scan/runner.py diff --git a/contrib/multilingual/tests/conftest.py b/contrib/batch_scan/tests/conftest.py similarity index 87% rename from contrib/multilingual/tests/conftest.py rename to contrib/batch_scan/tests/conftest.py index bb37b2d1..a40b7c36 100644 --- a/contrib/multilingual/tests/conftest.py +++ b/contrib/batch_scan/tests/conftest.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Pytest configuration for contrib.multilingual tests.""" +"""Pytest configuration for contrib.batch_scan tests.""" from __future__ import annotations @@ -21,7 +21,7 @@ def pytest_configure(config: pytest.Config) -> None: - """Register custom markers for the contrib.multilingual test suite.""" + """Register custom markers for the contrib.batch_scan test suite.""" config.addinivalue_line( "markers", "slow: tests that take longer than 5 seconds (e.g. subprocess isolation)", diff --git a/contrib/multilingual/tests/docs/BUGS_FOUND.md b/contrib/batch_scan/tests/docs/BUGS_FOUND.md similarity index 100% rename from contrib/multilingual/tests/docs/BUGS_FOUND.md rename to contrib/batch_scan/tests/docs/BUGS_FOUND.md diff --git a/contrib/multilingual/tests/docs/TEST_DESIGN.md b/contrib/batch_scan/tests/docs/TEST_DESIGN.md similarity index 99% rename from contrib/multilingual/tests/docs/TEST_DESIGN.md rename to contrib/batch_scan/tests/docs/TEST_DESIGN.md index 782a9d36..372c0b06 100644 --- a/contrib/multilingual/tests/docs/TEST_DESIGN.md +++ b/contrib/batch_scan/tests/docs/TEST_DESIGN.md @@ -1,4 +1,4 @@ -# Test Design Document — contrib/multilingual +# Test Design Document — contrib/batch_scan > **WHY & HOW.** The design rationale behind every test suite — how each > answers a specific concern from the PR #100 review. For coverage maps diff --git a/contrib/multilingual/tests/docs/TEST_GUIDE.md b/contrib/batch_scan/tests/docs/TEST_GUIDE.md similarity index 94% rename from contrib/multilingual/tests/docs/TEST_GUIDE.md rename to contrib/batch_scan/tests/docs/TEST_GUIDE.md index 24409588..d884777d 100644 --- a/contrib/multilingual/tests/docs/TEST_GUIDE.md +++ b/contrib/batch_scan/tests/docs/TEST_GUIDE.md @@ -1,4 +1,4 @@ -# Test Guide — contrib/multilingual +# Test Guide — contrib/batch_scan > **WHAT & WHERE.** Coverage map and quick reference. For design rationale > — why each suite exists and how it was designed — see `TEST_DESIGN.md`. @@ -10,16 +10,16 @@ ```bash # All 164 tests -python contrib/multilingual/tests/tests-pro/random_numbered.py # 120 unit (seed=42) -python contrib/multilingual/tests/test_pool_wiring.py # 4 smoke checks -python contrib/multilingual/tests/test_monkeypatch_invasiveness.py # 14 thematic -python contrib/multilingual/tests/test_monkeypatch_fragility.py # 26 thematic +python contrib/batch_scan/tests/tests-pro/random_numbered.py # 120 unit (seed=42) +python contrib/batch_scan/tests/test_pool_wiring.py # 4 smoke checks +python contrib/batch_scan/tests/test_monkeypatch_invasiveness.py # 14 thematic +python contrib/batch_scan/tests/test_monkeypatch_fragility.py # 26 thematic # Review-themed only (44 total) python -m unittest \ - contrib.multilingual.tests.test_monkeypatch_invasiveness \ - contrib.multilingual.tests.test_monkeypatch_fragility -v -python contrib/multilingual/tests/test_pool_wiring.py + contrib.batch_scan.tests.test_monkeypatch_invasiveness \ + contrib.batch_scan.tests.test_monkeypatch_fragility -v +python contrib/batch_scan/tests/test_pool_wiring.py ``` --- diff --git a/contrib/multilingual/tests/test_monkeypatch_fragility.py b/contrib/batch_scan/tests/test_monkeypatch_fragility.py similarity index 98% rename from contrib/multilingual/tests/test_monkeypatch_fragility.py rename to contrib/batch_scan/tests/test_monkeypatch_fragility.py index fc6b17c0..26b55e8b 100644 --- a/contrib/multilingual/tests/test_monkeypatch_fragility.py +++ b/contrib/batch_scan/tests/test_monkeypatch_fragility.py @@ -52,7 +52,7 @@ ) from skillspector.nodes.meta_analyzer import LLMMetaAnalyzer, MetaAnalyzerResult -from contrib.multilingual.runner import ( +from contrib.batch_scan.runner import ( _check_signature, _original_asyncio_run, _original_base_init, @@ -73,7 +73,7 @@ def _force_restore() -> None: """Safety-net: restore all patches regardless of depth counter.""" - import contrib.multilingual.runner as _runner + import contrib.batch_scan.runner as _runner while _runner._patches_depth > 0: _runner._restore_patches() @@ -194,7 +194,7 @@ def test_guard_after_context_cycle_still_passes(self) -> None: def test_guard_after_setup_and_manual_restore_still_passes(self) -> None: """Guard should pass after setup_deepseek_compat() + manual restore.""" - from contrib.multilingual.runner import setup_deepseek_compat + from contrib.batch_scan.runner import setup_deepseek_compat setup_deepseek_compat() _force_restore() try: @@ -516,7 +516,7 @@ def test_original_base_init_is_true_upstream(self) -> None: ) def test_original_chatopenai_init_is_not_none(self) -> None: - from contrib.multilingual.runner import _original_chatopenai_init + from contrib.batch_scan.runner import _original_chatopenai_init self.assertIsNotNone( _original_chatopenai_init, "_original_chatopenai_init must be captured at import time", diff --git a/contrib/multilingual/tests/test_monkeypatch_invasiveness.py b/contrib/batch_scan/tests/test_monkeypatch_invasiveness.py similarity index 98% rename from contrib/multilingual/tests/test_monkeypatch_invasiveness.py rename to contrib/batch_scan/tests/test_monkeypatch_invasiveness.py index a01bbc68..9d461727 100644 --- a/contrib/multilingual/tests/test_monkeypatch_invasiveness.py +++ b/contrib/batch_scan/tests/test_monkeypatch_invasiveness.py @@ -71,7 +71,7 @@ def _safe_chatopenai_init(self, **kwargs): from skillspector.llm_analyzer_base import LLMAnalyzerBase -from contrib.multilingual.runner import ( +from contrib.batch_scan.runner import ( _apply_patches, _original_asyncio_run, _original_base_build_prompt, @@ -116,7 +116,7 @@ def _force_restore() -> None: Call in tearDown / tearDownClass to prevent test-order leakage when random-order runners (random_numbered.py) shuffle test classes. """ - import contrib.multilingual.runner as _runner + import contrib.batch_scan.runner as _runner while _runner._patches_depth > 0: _runner._restore_patches() @@ -127,7 +127,7 @@ def _force_restore() -> None: class TestImportNoSideEffect(unittest.TestCase): - """Prove that ``import contrib.multilingual.runner`` does NOT apply patches. + """Prove that ``import contrib.batch_scan.runner`` does NOT apply patches. Reviewer concern: "Import-time global monkey-patching is invasive." Resolution: patches fire only via explicit ``deepseek_compat()`` or @@ -147,7 +147,7 @@ def test_import_runner_leaves_original_init_untouched(self): sys.executable, "-X", "utf8", "-c", "from skillspector.llm_analyzer_base import LLMAnalyzerBase; " "orig = LLMAnalyzerBase.__init__; " - "import contrib.multilingual.runner; " + "import contrib.batch_scan.runner; " "assert LLMAnalyzerBase.__init__ is orig, 'Import applied patches!'", ], capture_output=True, text=True, timeout=30, diff --git a/contrib/multilingual/tests/test_pool_wiring.py b/contrib/batch_scan/tests/test_pool_wiring.py similarity index 93% rename from contrib/multilingual/tests/test_pool_wiring.py rename to contrib/batch_scan/tests/test_pool_wiring.py index bdc3dd4c..1e07df95 100644 --- a/contrib/multilingual/tests/test_pool_wiring.py +++ b/contrib/batch_scan/tests/test_pool_wiring.py @@ -33,7 +33,7 @@ if sys.platform == "win32": sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore[attr-defined] -# Ensure project root is on sys.path (test lives under contrib/multilingual/tests/) +# Ensure project root is on sys.path (test lives under contrib/batch_scan/tests/) _project_root = Path(__file__).resolve().parents[3] if str(_project_root) not in sys.path: sys.path.insert(0, str(_project_root)) @@ -46,13 +46,13 @@ ) # -- Build pool ------------------------------------------------------------ -from contrib.multilingual.api_pool import create_api_key_pool_from_env +from contrib.batch_scan.api_pool import create_api_key_pool_from_env pool = create_api_key_pool_from_env() assert pool is not None, "2 keys should produce a pool" print(f"✅ Pool created: {pool.keys_configured} keys") # -- Scoped patches + pool wiring ----------------------------------------- -from contrib.multilingual.runner import set_api_pool, deepseek_compat +from contrib.batch_scan.runner import set_api_pool, deepseek_compat with deepseek_compat(): set_api_pool(pool) @@ -72,7 +72,7 @@ print(f"✅ LLMAnalyzerBase._llm → {type(analyzer._llm).__name__} (graph path)") # Path 3: gap-fill pass - from contrib.multilingual.gap_fill import GapFillAnalyzer + from contrib.batch_scan.gap_fill import GapFillAnalyzer gf = GapFillAnalyzer(language="zh", api_pool=pool) assert type(gf.chat_model).__name__ == "PooledChatModel" print(f"✅ GapFillAnalyzer → {type(gf.chat_model).__name__} (gap-fill path)") diff --git a/contrib/multilingual/tests/tests-pro/__init__.py b/contrib/batch_scan/tests/tests-pro/__init__.py similarity index 88% rename from contrib/multilingual/tests/tests-pro/__init__.py rename to contrib/batch_scan/tests/tests-pro/__init__.py index c4f95128..7e3adab0 100644 --- a/contrib/multilingual/tests/tests-pro/__init__.py +++ b/contrib/batch_scan/tests/tests-pro/__init__.py @@ -13,6 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Unit tests for contrib.multilingual — API pool, gap-fill, runner patches, annotation.""" +"""Unit tests for contrib.batch_scan — API pool, gap-fill, runner patches, annotation.""" from __future__ import annotations diff --git a/contrib/multilingual/tests/tests-pro/mutation_max.py b/contrib/batch_scan/tests/tests-pro/mutation_max.py similarity index 91% rename from contrib/multilingual/tests/tests-pro/mutation_max.py rename to contrib/batch_scan/tests/tests-pro/mutation_max.py index d35d17ab..e846df46 100644 --- a/contrib/multilingual/tests/tests-pro/mutation_max.py +++ b/contrib/batch_scan/tests/tests-pro/mutation_max.py @@ -43,7 +43,7 @@ def mutate(label: str, module: str, target: str, broken_fn, test_specs: list[tup try: for test_mod, test_cls in test_specs: suite = unittest.TestLoader().loadTestsFromName( - f"contrib.multilingual.tests.tests-pro.{test_mod}.{test_cls}" + f"contrib.batch_scan.tests.tests-pro.{test_mod}.{test_cls}" ) r = unittest.TextTestRunner(verbosity=0).run(suite) caught = not r.wasSuccessful() @@ -57,7 +57,7 @@ def mutate(label: str, module: str, target: str, broken_fn, test_specs: list[tup # ═══════════════════════════════════════════════════════════════════════ # Mutation 1a: acquire forgets to increment active_requests -import contrib.multilingual.api_pool as _ap +import contrib.batch_scan.api_pool as _ap _orig_acquire = _ap.ApiKeyPool.acquire @@ -82,7 +82,7 @@ def _broken_acquire_no_increment(self, timeout=None): _ap.ApiKeyPool.acquire = _broken_acquire_no_increment -mutate("acquire forgets active_requests++", "contrib.multilingual.api_pool", +mutate("acquire forgets active_requests++", "contrib.batch_scan.api_pool", "ApiKeyPool.acquire", _broken_acquire_no_increment, [("test_api_pool", "TestAcquireRelease")]) _ap.ApiKeyPool.acquire = _orig_acquire @@ -107,7 +107,7 @@ def _broken_release_no_decrement(self, key, *, success=True): _ap.ApiKeyPool.release = _broken_release_no_decrement -mutate("release forgets active_requests--", "contrib.multilingual.api_pool", +mutate("release forgets active_requests--", "contrib.batch_scan.api_pool", "ApiKeyPool.release", _broken_release_no_decrement, [("test_api_pool", "TestAcquireRelease"), ("test_api_pool", "TestResourceLeakRecovery")]) @@ -143,7 +143,7 @@ def _broken_acquire_no_load_balance(self, timeout=None): _ap.ApiKeyPool.acquire = _broken_acquire_no_load_balance -mutate("least-loaded scheduling broken", "contrib.multilingual.api_pool", +mutate("least-loaded scheduling broken", "contrib.batch_scan.api_pool", "ApiKeyPool.acquire", _broken_acquire_no_load_balance, [("test_api_pool", "TestEdgeCases")]) # test_released_slot_returns_least_loaded_key _ap.ApiKeyPool.acquire = _orig_acquire2 @@ -169,7 +169,7 @@ def _broken_try_acquire(self): _ap.ApiKeyPool.try_acquire = _broken_try_acquire -mutate("try_acquire recovery broken", "contrib.multilingual.api_pool", +mutate("try_acquire recovery broken", "contrib.batch_scan.api_pool", "ApiKeyPool.try_acquire", _broken_try_acquire, [("test_api_pool", "TestRecoveredKeyScheduling")]) _ap.ApiKeyPool.try_acquire = _orig_try_acquire @@ -197,7 +197,7 @@ def _broken_release_fixed_backoff(self, key, *, success=True): _ap.ApiKeyPool.release = _broken_release_fixed_backoff -mutate("backoff always 5s", "contrib.multilingual.api_pool", +mutate("backoff always 5s", "contrib.batch_scan.api_pool", "ApiKeyPool.release", _broken_release_fixed_backoff, [("test_api_pool", "TestRateLimitBackoff")]) _ap.ApiKeyPool.release = _orig_release2 @@ -211,7 +211,7 @@ def _broken_recover(self, now): _ap.ApiKeyPool._recover_expired_keys = _broken_recover -mutate("recovery never runs", "contrib.multilingual.api_pool", +mutate("recovery never runs", "contrib.batch_scan.api_pool", "ApiKeyPool._recover_expired_keys", _broken_recover, [("test_api_pool", "TestRateLimitBackoff")]) # TestRecoveredKeyScheduling hangs: acquire() blocks forever w/o recovery _ap.ApiKeyPool._recover_expired_keys = _orig_recover @@ -221,7 +221,7 @@ def _broken_recover(self, now): # ═══════════════════════════════════════════════════════════════════════ # Mutation 3a: Patch 1 broken — doesn't set response_schema=None -import contrib.multilingual.runner as _runner +import contrib.batch_scan.runner as _runner _orig_patched_init = _runner._patched_base_init @@ -266,7 +266,7 @@ def _broken_apply_no_patch1(): _runner._apply_patches = _broken_apply_no_patch1 -mutate("Patch 1 not applied", "contrib.multilingual.runner", +mutate("Patch 1 not applied", "contrib.batch_scan.runner", "_apply_patches", _broken_apply_no_patch1, [("test_runner_patches", "TestContextManagerApplyRestore")]) _runner._apply_patches = _orig_apply @@ -281,7 +281,7 @@ def _broken_co_init(self, **kwargs): _runner._patched_chatopenai_init = _broken_co_init -mutate("Patch 6 no timeout", "contrib.multilingual.runner", +mutate("Patch 6 no timeout", "contrib.batch_scan.runner", "_patched_chatopenai_init", _broken_co_init, [("test_runner_patches", "TestPatch6ChatOpenAITimeout")]) _runner._patched_chatopenai_init = _orig_patched_co @@ -290,7 +290,7 @@ def _broken_co_init(self, **kwargs): # Area 4: GapFillAnalyzer.parse_response # ═══════════════════════════════════════════════════════════════════════ -import contrib.multilingual.gap_fill as _gf +import contrib.batch_scan.gap_fill as _gf # Mutation 4a: confidence filter broken — threshold 0.7 → 0.0 _orig_parse = _gf.GapFillAnalyzer.parse_response @@ -324,7 +324,7 @@ def _broken_parse_no_filter(self, response, batch): # Apply directly to class since mutation test targets the class method _gf.GapFillAnalyzer.parse_response = _broken_parse_no_filter -mutate("confidence filter removed", "contrib.multilingual.gap_fill", +mutate("confidence filter removed", "contrib.batch_scan.gap_fill", "GapFillAnalyzer.parse_response", _broken_parse_no_filter, [("test_gap_fill", "TestParseResponseFiltering")]) _gf.GapFillAnalyzer.parse_response = _orig_parse @@ -351,7 +351,7 @@ def _broken_parse_no_fence_strip(self, response, batch): _gf.GapFillAnalyzer.parse_response = _broken_parse_no_fence_strip -mutate("fence stripping broken", "contrib.multilingual.gap_fill", +mutate("fence stripping broken", "contrib.batch_scan.gap_fill", "GapFillAnalyzer.parse_response", _broken_parse_no_fence_strip, [("test_gap_fill", "TestParseResponseMarkdownFences")]) _gf.GapFillAnalyzer.parse_response = _orig_parse2 @@ -369,7 +369,7 @@ def _broken_patched_parse(self, response, batch): _runner._patched_base_parse = _broken_patched_parse _runner.LLMAnalyzerBase.parse_response = _broken_patched_parse -mutate("Patch 2 parse always empty", "contrib.multilingual.runner", +mutate("Patch 2 parse always empty", "contrib.batch_scan.runner", "_patched_base_parse", _broken_patched_parse, [("test_runner_patches", "TestContextManagerApplyRestore")]) _runner._patched_base_parse = _orig_patched_parse @@ -399,7 +399,7 @@ def _broken_meta_parse(self, response, batch): _runner._patched_meta_parse = _broken_meta_parse _runner.LLMMetaAnalyzer.parse_response = _broken_meta_parse -mutate("Patch 3 sanitize broken", "contrib.multilingual.runner", +mutate("Patch 3 sanitize broken", "contrib.batch_scan.runner", "_patched_meta_parse", _broken_meta_parse, [("test_runner_patches", "TestSanitizeMetaFinding")]) _runner._patched_meta_parse = _orig_meta_parse @@ -415,7 +415,7 @@ def _broken_base_build(self, batch, **kwargs): _runner._patched_base_build_prompt = _broken_base_build _runner.LLMAnalyzerBase.build_prompt = _broken_base_build -mutate("Patch 4 JSON prompt missing", "contrib.multilingual.runner", +mutate("Patch 4 JSON prompt missing", "contrib.batch_scan.runner", "_patched_base_build_prompt", _broken_base_build, [("test_runner_patches", "TestContextManagerApplyRestore")]) _runner._patched_base_build_prompt = _orig_base_build @@ -430,7 +430,7 @@ def _broken_meta_build(self, batch, **kwargs): _runner._patched_meta_build_prompt = _broken_meta_build _runner.LLMMetaAnalyzer.build_prompt = _broken_meta_build -mutate("Patch 5 JSON meta prompt missing", "contrib.multilingual.runner", +mutate("Patch 5 JSON meta prompt missing", "contrib.batch_scan.runner", "_patched_meta_build_prompt", _broken_meta_build, [("test_runner_patches", "TestContextManagerApplyRestore")]) _runner._patched_meta_build_prompt = _orig_meta_build @@ -445,7 +445,7 @@ def _broken_asyncio_run(main, *, debug=None, loop_factory=None): _runner._patched_asyncio_run = _broken_asyncio_run -mutate("Patch 7 asyncio not patched", "contrib.multilingual.runner", +mutate("Patch 7 asyncio not patched", "contrib.batch_scan.runner", "_patched_asyncio_run", _broken_asyncio_run, [("test_runner_patches", "TestPatch7AsyncioQuietLoop")]) _runner._patched_asyncio_run = _orig_patched_asyncio @@ -481,7 +481,7 @@ def _broken_parse_no_rule_filter(self, response, batch): _gf.GapFillAnalyzer.parse_response = _broken_parse_no_rule_filter -mutate("rule_id filter removed", "contrib.multilingual.gap_fill", +mutate("rule_id filter removed", "contrib.batch_scan.gap_fill", "GapFillAnalyzer.parse_response", _broken_parse_no_rule_filter, [("test_gap_fill", "TestParseResponseFiltering")]) _gf.GapFillAnalyzer.parse_response = _orig_parse3 @@ -507,7 +507,7 @@ def _broken_parse_no_json_catch(self, response, batch): _gf.GapFillAnalyzer.parse_response = _broken_parse_no_json_catch -mutate("JSON decode error not caught", "contrib.multilingual.gap_fill", +mutate("JSON decode error not caught", "contrib.batch_scan.gap_fill", "GapFillAnalyzer.parse_response", _broken_parse_no_json_catch, [("test_gap_fill", "TestParseResponseInvalidInput")]) _gf.GapFillAnalyzer.parse_response = _orig_parse4 @@ -536,7 +536,7 @@ def _broken_parse_no_pydantic_catch(self, response, batch): _gf.GapFillAnalyzer.parse_response = _broken_parse_no_pydantic_catch -mutate("Pydantic validation error not caught", "contrib.multilingual.gap_fill", +mutate("Pydantic validation error not caught", "contrib.batch_scan.gap_fill", "GapFillAnalyzer.parse_response", _broken_parse_no_pydantic_catch, [("test_gap_fill", "TestParseResponseInvalidInput")]) _gf.GapFillAnalyzer.parse_response = _orig_parse5 @@ -554,7 +554,7 @@ def _broken_next_avail(self, now): _ap.ApiKeyPool._next_available_in = _broken_next_avail # Note: this mutation can't be directly tested without a rate-limited+full pool scenario # which is Q16's blind spot. Test validates the function exists but not this branch. -mutate("_next_available_in always None", "contrib.multilingual.api_pool", +mutate("_next_available_in always None", "contrib.batch_scan.api_pool", "ApiKeyPool._next_available_in", _broken_next_avail, []) # No matching test — documented as Q16/Q17 blind spot _ap.ApiKeyPool._next_available_in = _orig_next_avail @@ -579,7 +579,7 @@ def _broken_restore(): _runner._restore_patches = _broken_restore -mutate("_restore_patches skips Patch 6+7", "contrib.multilingual.runner", +mutate("_restore_patches skips Patch 6+7", "contrib.batch_scan.runner", "_restore_patches", _broken_restore, [("test_runner_patches", "TestContextManagerApplyRestore")]) _runner._restore_patches = _orig_restore @@ -593,7 +593,7 @@ def _broken_verify(): _runner._verify_patch_targets = _broken_verify -mutate("_verify_patch_targets no-op", "contrib.multilingual.runner", +mutate("_verify_patch_targets no-op", "contrib.batch_scan.runner", "_verify_patch_targets", _broken_verify, []) # Q13: no test asserts guard actually ran — documented blind spot _runner._verify_patch_targets = _orig_verify @@ -607,7 +607,7 @@ def _broken_check(func, expected, label, num): _runner._check_signature = _broken_check -mutate("_check_signature no-op", "contrib.multilingual.runner", +mutate("_check_signature no-op", "contrib.batch_scan.runner", "_check_signature", _broken_check, []) # No test directly calls _check_signature — documented _runner._check_signature = _orig_check @@ -623,7 +623,7 @@ def _broken_set_api(pool): import skillspector.llm_utils as _u def _bad_wrapper(model=None): if _runner._api_pool: - from contrib.multilingual.api_pool import PooledChatModel + from contrib.batch_scan.api_pool import PooledChatModel return PooledChatModel(_runner._api_pool) # BUG: fallback calls patched version instead of original return _u.get_chat_model(model) @@ -631,13 +631,13 @@ def _bad_wrapper(model=None): _runner.set_api_pool = _broken_set_api -mutate("set_api_pool broken fallback", "contrib.multilingual.runner", +mutate("set_api_pool broken fallback", "contrib.batch_scan.runner", "set_api_pool", _broken_set_api, [("test_runner_patches", "TestSetApiPoolRestore")]) _runner.set_api_pool = _orig_set_api # Mutation 5f: annotate_findings broken — always returns incompatible -import contrib.multilingual.annotation as _ann +import contrib.batch_scan.annotation as _ann _orig_annotate = _ann.annotate_findings @@ -651,7 +651,7 @@ def _broken_annotate(issues, detected_language): _ann.annotate_findings = _broken_annotate -mutate("annotate_findings always incompatible", "contrib.multilingual.annotation", +mutate("annotate_findings always incompatible", "contrib.batch_scan.annotation", "annotate_findings", _broken_annotate, [("test_annotation", "TestAnnotateFindings")]) _ann.annotate_findings = _orig_annotate @@ -665,7 +665,7 @@ def _broken_is_compat(rule_id, detected_language): _ann.is_language_compatible = _broken_is_compat -mutate("is_language_compatible always True", "contrib.multilingual.annotation", +mutate("is_language_compatible always True", "contrib.batch_scan.annotation", "is_language_compatible", _broken_is_compat, [("test_annotation", "TestAnnotateFindings")]) _ann.is_language_compatible = _orig_is_compat @@ -683,7 +683,7 @@ def _broken_build_prompt(self, batch, **kwargs): _gf.GapFillAnalyzer.build_prompt = _broken_build_prompt -mutate("build_prompt missing file content", "contrib.multilingual.gap_fill", +mutate("build_prompt missing file content", "contrib.batch_scan.gap_fill", "GapFillAnalyzer.build_prompt", _broken_build_prompt, [("test_gap_fill", "TestBuildPrompt")]) _gf.GapFillAnalyzer.build_prompt = _orig_build @@ -697,7 +697,7 @@ def _broken_get_batches(self, file_paths, file_cache, findings=None): _gf.GapFillAnalyzer.get_batches = _broken_get_batches -mutate("get_batches always empty", "contrib.multilingual.gap_fill", +mutate("get_batches always empty", "contrib.batch_scan.gap_fill", "GapFillAnalyzer.get_batches", _broken_get_batches, [("test_gap_fill", "TestGetBatchesAndCollectFindings")]) _gf.GapFillAnalyzer.get_batches = _orig_batches @@ -711,7 +711,7 @@ def _broken_collect_findings(self, batch_results): _gf.GapFillAnalyzer.collect_findings = _broken_collect_findings -mutate("collect_findings always empty", "contrib.multilingual.gap_fill", +mutate("collect_findings always empty", "contrib.batch_scan.gap_fill", "GapFillAnalyzer.collect_findings", _broken_collect_findings, [("test_gap_fill", "TestGetBatchesAndCollectFindings")]) _gf.GapFillAnalyzer.collect_findings = _orig_collect @@ -725,7 +725,7 @@ def _broken_run_gap_fill(file_cache, language, model=None, api_pool=None): _gf.run_gap_fill = _broken_run_gap_fill -mutate("run_gap_fill always empty", "contrib.multilingual.gap_fill", +mutate("run_gap_fill always empty", "contrib.batch_scan.gap_fill", "run_gap_fill", _broken_run_gap_fill, [("test_gap_fill", "TestRunGapFill")]) _gf.run_gap_fill = _orig_run_gf @@ -739,7 +739,7 @@ def _broken_is_rl(exc): _ap.PooledChatModel._is_rate_limit = staticmethod(_broken_is_rl) -mutate("_is_rate_limit always False", "contrib.multilingual.api_pool", +mutate("_is_rate_limit always False", "contrib.batch_scan.api_pool", "PooledChatModel._is_rate_limit", staticmethod(_broken_is_rl), [("test_api_pool", "TestIsRateLimit")]) _ap.PooledChatModel._is_rate_limit = _orig_is_rl @@ -753,7 +753,7 @@ def _broken_create_pool(max_concurrent_per_key=5): _ap.create_api_key_pool_from_env = _broken_create_pool -mutate("create_api_key_pool_from_env always None", "contrib.multilingual.api_pool", +mutate("create_api_key_pool_from_env always None", "contrib.batch_scan.api_pool", "create_api_key_pool_from_env", _broken_create_pool, [("test_api_pool", "TestCreateApiKeyPoolFromEnv")]) _ap.create_api_key_pool_from_env = _orig_create_pool @@ -774,7 +774,7 @@ def _broken_ds_compat(): _runner.deepseek_compat = _broken_ds_compat -mutate("deepseek_compat no restore on exception", "contrib.multilingual.runner", +mutate("deepseek_compat no restore on exception", "contrib.batch_scan.runner", "deepseek_compat", _broken_ds_compat, [("test_runner_patches", "TestContextManagerApplyRestore")]) _runner.deepseek_compat = _orig_ds_compat diff --git a/contrib/multilingual/tests/tests-pro/random_numbered.py b/contrib/batch_scan/tests/tests-pro/random_numbered.py similarity index 97% rename from contrib/multilingual/tests/tests-pro/random_numbered.py rename to contrib/batch_scan/tests/tests-pro/random_numbered.py index 11dbe9f7..a1760593 100644 --- a/contrib/multilingual/tests/tests-pro/random_numbered.py +++ b/contrib/batch_scan/tests/tests-pro/random_numbered.py @@ -43,7 +43,7 @@ def flatten(suite): ]: flatten( loader.loadTestsFromName( - f"contrib.multilingual.tests.tests-pro.{mod}" + f"contrib.batch_scan.tests.tests-pro.{mod}" ) ) diff --git a/contrib/multilingual/tests/tests-pro/test_annotation.py b/contrib/batch_scan/tests/tests-pro/test_annotation.py similarity index 98% rename from contrib/multilingual/tests/tests-pro/test_annotation.py rename to contrib/batch_scan/tests/tests-pro/test_annotation.py index c38e364c..3a74ef32 100644 --- a/contrib/multilingual/tests/tests-pro/test_annotation.py +++ b/contrib/batch_scan/tests/tests-pro/test_annotation.py @@ -30,7 +30,7 @@ from skillspector.models import Finding -from contrib.multilingual.annotation import annotate_findings, is_language_compatible +from contrib.batch_scan.annotation import annotate_findings, is_language_compatible def _make_finding(rule_id: str = "P1", file: str = "test.md") -> dict: diff --git a/contrib/multilingual/tests/tests-pro/test_api_pool.py b/contrib/batch_scan/tests/tests-pro/test_api_pool.py similarity index 99% rename from contrib/multilingual/tests/tests-pro/test_api_pool.py rename to contrib/batch_scan/tests/tests-pro/test_api_pool.py index de761ddf..208f42d4 100644 --- a/contrib/multilingual/tests/tests-pro/test_api_pool.py +++ b/contrib/batch_scan/tests/tests-pro/test_api_pool.py @@ -33,7 +33,7 @@ if str(_project_root) not in sys.path: sys.path.insert(0, str(_project_root)) -from contrib.multilingual.api_pool import ( +from contrib.batch_scan.api_pool import ( ApiKey, ApiKeyPool, PooledChatModel, diff --git a/contrib/multilingual/tests/tests-pro/test_gap_fill.py b/contrib/batch_scan/tests/tests-pro/test_gap_fill.py similarity index 99% rename from contrib/multilingual/tests/tests-pro/test_gap_fill.py rename to contrib/batch_scan/tests/tests-pro/test_gap_fill.py index 07d32272..3b36bbb8 100644 --- a/contrib/multilingual/tests/tests-pro/test_gap_fill.py +++ b/contrib/batch_scan/tests/tests-pro/test_gap_fill.py @@ -33,7 +33,7 @@ from skillspector.llm_analyzer_base import Batch from skillspector.models import Finding -from contrib.multilingual.gap_fill import ( +from contrib.batch_scan.gap_fill import ( GapFillAnalyzer, GapFillFinding, GapFillResult, diff --git a/contrib/multilingual/tests/tests-pro/test_runner_patches.py b/contrib/batch_scan/tests/tests-pro/test_runner_patches.py similarity index 94% rename from contrib/multilingual/tests/tests-pro/test_runner_patches.py rename to contrib/batch_scan/tests/tests-pro/test_runner_patches.py index 042945bc..af3b5712 100644 --- a/contrib/multilingual/tests/tests-pro/test_runner_patches.py +++ b/contrib/batch_scan/tests/tests-pro/test_runner_patches.py @@ -65,7 +65,7 @@ def _safe_chatopenai_init(self, **kwargs): from skillspector.llm_analyzer_base import LLMAnalyzerBase from skillspector.nodes.meta_analyzer import LLMMetaAnalyzer -from contrib.multilingual.runner import ( +from contrib.batch_scan.runner import ( _original_asyncio_run, _original_base_init, _original_base_parse, @@ -243,7 +243,7 @@ def tearDownClass(cls): """Restore global state mutated by setup_deepseek_compat(). Calls _restore_patches until depth reaches 0 (setup may be called multiple times across test methods).""" - import contrib.multilingual.runner as _runner + import contrib.batch_scan.runner as _runner while _runner._patches_depth > 0: _runner._restore_patches() @@ -278,7 +278,7 @@ class TestSetupContextInteraction(unittest.TestCase): @classmethod def tearDownClass(cls): - import contrib.multilingual.runner as _runner + import contrib.batch_scan.runner as _runner while _runner._patches_depth > 0: _runner._restore_patches() @@ -288,7 +288,7 @@ def test_context_manager_after_setup_does_not_restore_on_exit(self): with deepseek_compat(): self.assertIsNot(LLMAnalyzerBase.__init__, _original_base_init) self.assertIsNot(LLMAnalyzerBase.__init__, _original_base_init) - from contrib.multilingual.runner import _restore_patches + from contrib.batch_scan.runner import _restore_patches _restore_patches() self.assertIs(LLMAnalyzerBase.__init__, _original_base_init) @@ -311,7 +311,7 @@ def test_importing_runner_does_not_apply_patches(self): sys.executable, "-X", "utf8", "-c", "from skillspector.llm_analyzer_base import LLMAnalyzerBase; " "orig = LLMAnalyzerBase.__init__; " - "import contrib.multilingual.runner; " + "import contrib.batch_scan.runner; " "assert LLMAnalyzerBase.__init__ is orig, 'Import applied patches!'", ], capture_output=True, text=True, timeout=30, @@ -330,7 +330,7 @@ class TestPatch2OriginalCapture(unittest.TestCase): def test_original_chatopenai_init_is_captured_at_import_time(self): """Verify P2 fix: _original_chatopenai_init is not None after import.""" - from contrib.multilingual.runner import _original_chatopenai_init + from contrib.batch_scan.runner import _original_chatopenai_init self.assertIsNotNone( _original_chatopenai_init, "_original_chatopenai_init should be captured at module-load time", @@ -341,21 +341,21 @@ class TestCheckSignature(unittest.TestCase): """_check_signature() — previously untested.""" def test_check_signature_passes_when_all_params_present(self): - from contrib.multilingual.runner import _check_signature + from contrib.batch_scan.runner import _check_signature def _sample(self, a, b, c): pass # Should not raise _check_signature(_sample, ["self", "a", "b", "c"], "test_func", 99) def test_check_signature_raises_when_param_missing(self): - from contrib.multilingual.runner import _check_signature + from contrib.batch_scan.runner import _check_signature def _sample(self, a, b): pass with self.assertRaises(RuntimeError): _check_signature(_sample, ["self", "a", "b", "c"], "test_func", 99) def test_check_signature_raises_when_param_becomes_keyword_only(self): - from contrib.multilingual.runner import _check_signature + from contrib.batch_scan.runner import _check_signature def _sample(self, *, a, b, c): pass with self.assertRaises(RuntimeError): @@ -367,7 +367,7 @@ class TestVerifyPatchTargets(unittest.TestCase): def test_guard_passes_against_current_upstream_version(self): """Entering context manager must not raise.""" - from contrib.multilingual.runner import _verify_patch_targets, _apply_patches + from contrib.batch_scan.runner import _verify_patch_targets, _apply_patches try: _verify_patch_targets() except RuntimeError as e: @@ -441,7 +441,7 @@ def test_asyncio_run_is_replaced_inside_context(self): def test_quiet_loop_handler_suppresses_event_loop_closed_error(self): """#C8: Verify _patched_asyncio_run installs quiet handler via loop_factory.""" - from contrib.multilingual.runner import _patched_asyncio_run, _original_asyncio_run + from contrib.batch_scan.runner import _patched_asyncio_run, _original_asyncio_run # Create a loop via _patched_asyncio_run — it calls _make_quiet_loop internally loop = None def _capture_loop(): @@ -575,7 +575,7 @@ def test_set_api_pool_none_restores_original_get_chat_model(self): original = _llm_utils.get_chat_model # Act — wire pool - from contrib.multilingual.api_pool import create_api_key_pool_from_env + from contrib.batch_scan.api_pool import create_api_key_pool_from_env pool = create_api_key_pool_from_env() set_api_pool(pool) self.assertIsNot(_llm_utils.get_chat_model, original) @@ -595,14 +595,14 @@ class TestScanState(unittest.TestCase): """scan_state() — pure function, previously zero coverage.""" def test_scan_state_returns_correct_keys_with_llm_enabled(self): - from contrib.multilingual.runner import scan_state + from contrib.batch_scan.runner import scan_state state = scan_state(Path("/tmp/test_skill"), use_llm=True) self.assertEqual(state["input_path"], str(Path("/tmp/test_skill"))) self.assertEqual(state["output_format"], "json") self.assertTrue(state["use_llm"]) def test_scan_state_returns_correct_keys_with_llm_disabled(self): - from contrib.multilingual.runner import scan_state + from contrib.batch_scan.runner import scan_state state = scan_state(Path("/tmp/test_skill"), use_llm=False) self.assertFalse(state["use_llm"]) @@ -611,13 +611,13 @@ class TestRelName(unittest.TestCase): """_rel_name() — pure function, previously zero coverage.""" def test_rel_name_returns_relative_path_when_skill_is_under_root(self): - from contrib.multilingual.runner import _rel_name + from contrib.batch_scan.runner import _rel_name result = _rel_name(Path("/root/sub/skill"), Path("/root")) self.assertIn("sub", result) self.assertIn("skill", result) def test_rel_name_falls_back_to_skill_name_when_unrelated_paths(self): - from contrib.multilingual.runner import _rel_name + from contrib.batch_scan.runner import _rel_name result = _rel_name(Path("/other/skill"), Path("/root")) self.assertEqual(result, "skill") @@ -630,7 +630,7 @@ def setUp(self): self.root = Path("/tmp") def test_entry_from_minimal_result_has_all_required_keys(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result result = {"findings": []} entry = entry_from_result(result, self.skill_dir, self.root) self.assertIn("skill", entry) @@ -641,20 +641,20 @@ def test_entry_from_minimal_result_has_all_required_keys(self): self.assertIn("enhancements", entry) def test_entry_defaults_risk_to_low_zero_when_not_provided(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result entry = entry_from_result({}, self.skill_dir, self.root) self.assertEqual(entry["risk_assessment"]["score"], 0) self.assertEqual(entry["risk_assessment"]["severity"], "LOW") def test_entry_preserves_explicit_risk_score_and_severity(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result result = {"risk_score": 85, "risk_severity": "HIGH", "findings": []} entry = entry_from_result(result, self.skill_dir, self.root) self.assertEqual(entry["risk_assessment"]["score"], 85) self.assertEqual(entry["risk_assessment"]["severity"], "HIGH") def test_entry_marks_gap_fill_applied_in_enhancements(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result entry = entry_from_result( {"findings": []}, self.skill_dir, self.root, detected_language="zh", gap_fill_applied=True, gap_fill_findings=3, @@ -663,32 +663,32 @@ def test_entry_marks_gap_fill_applied_in_enhancements(self): self.assertEqual(entry["enhancements"]["gap_fill_findings"], 3) def test_entry_counts_english_keyword_rules_skipped_for_non_english(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result entry = entry_from_result( {"findings": []}, self.skill_dir, self.root, detected_language="zh", ) self.assertGreater(entry["enhancements"]["english_keyword_rules_skipped"], 0) def test_entry_zero_english_keyword_rules_skipped_for_english(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result entry = entry_from_result( {"findings": []}, self.skill_dir, self.root, detected_language="en", ) self.assertEqual(entry["enhancements"]["english_keyword_rules_skipped"], 0) def test_entry_uses_manifest_name_when_available(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result result = {"manifest": {"name": "my-skill"}, "findings": []} entry = entry_from_result(result, self.skill_dir, self.root) self.assertEqual(entry["skill"]["name"], "my-skill") def test_entry_falls_back_to_directory_name_when_no_manifest(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result entry = entry_from_result({"findings": []}, self.skill_dir, self.root) self.assertEqual(entry["skill"]["name"], "test_skill") def test_entry_handles_value_error_on_relative_to_for_different_drives(self): - from contrib.multilingual.runner import entry_from_result + from contrib.batch_scan.runner import entry_from_result # On Windows, relative_to raises ValueError for different drives try: entry = entry_from_result({"findings": []}, Path("D:/skill"), Path("C:/root")) diff --git a/contrib/multilingual/.env.example b/contrib/multilingual/.env.example deleted file mode 100644 index 85a8213d..00000000 --- a/contrib/multilingual/.env.example +++ /dev/null @@ -1,27 +0,0 @@ -# SkillSpector Contrib Batch Scanner — Environment Configuration -# -# Copy to the repository root as .env: -# cp contrib/multilingual/.env.example .env -# -# The scanner also respects the upstream .env.example keys -# (OPENAI_API_KEY, SKILLSPECTOR_PROVIDER, SKILLSPECTOR_MODEL). - -# Provider configuration -SKILLSPECTOR_PROVIDER=openai -SKILLSPECTOR_MODEL=deepseek-v4-flash - -# Single-key mode (standard OpenAI-compatible) -OPENAI_API_KEY=sk-or-xxxxxxxxxxxxxxxxxxxxxxxx -OPENAI_BASE_URL=https://api.deepseek.com/v1 - -# Multi-key pool (recommended for batch scans). -# Pipe-delimited: key|base_url|model. Separate entries with newlines -# or semicolons. Supports up to 10 keys. Leave unset to use -# single-key mode above. -# SKILLSPECTOR_API_KEYS=" -# sk-or-xxx1|https://api.deepseek.com/v1|deepseek-v4-flash -# sk-or-xxx2|https://api.deepseek.com/v1|deepseek-v4-flash -# " - -# Logging (DEBUG | INFO | WARNING | ERROR) -SKILLSPECTOR_LOG_LEVEL=WARNING