weave-compose 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- weave_compose-0.3.0/LICENSE +21 -0
- weave_compose-0.3.0/PKG-INFO +126 -0
- weave_compose-0.3.0/README.md +94 -0
- weave_compose-0.3.0/pyproject.toml +61 -0
- weave_compose-0.3.0/setup.cfg +4 -0
- weave_compose-0.3.0/tests/test_adapters.py +163 -0
- weave_compose-0.3.0/tests/test_adapters_codex.py +63 -0
- weave_compose-0.3.0/tests/test_adapters_windsurf.py +65 -0
- weave_compose-0.3.0/tests/test_cli.py +105 -0
- weave_compose-0.3.0/tests/test_composer.py +97 -0
- weave_compose-0.3.0/tests/test_config.py +120 -0
- weave_compose-0.3.0/tests/test_detector.py +62 -0
- weave_compose-0.3.0/tests/test_e2e.py +169 -0
- weave_compose-0.3.0/tests/test_registry.py +108 -0
- weave_compose-0.3.0/tests/test_schema.py +95 -0
- weave_compose-0.3.0/tests/test_selector.py +132 -0
- weave_compose-0.3.0/weave/__init__.py +1 -0
- weave_compose-0.3.0/weave/cli/__init__.py +1 -0
- weave_compose-0.3.0/weave/cli/config.py +182 -0
- weave_compose-0.3.0/weave/cli/config_schema.py +68 -0
- weave_compose-0.3.0/weave/cli/main.py +181 -0
- weave_compose-0.3.0/weave/cli/query_command.py +65 -0
- weave_compose-0.3.0/weave/cli/run_command.py +128 -0
- weave_compose-0.3.0/weave/core/__init__.py +1 -0
- weave_compose-0.3.0/weave/core/adapters/__init__.py +1 -0
- weave_compose-0.3.0/weave/core/adapters/base.py +165 -0
- weave_compose-0.3.0/weave/core/adapters/claude_code.py +158 -0
- weave_compose-0.3.0/weave/core/adapters/codex.py +185 -0
- weave_compose-0.3.0/weave/core/adapters/cursor.py +186 -0
- weave_compose-0.3.0/weave/core/adapters/gemini_cli.py +1 -0
- weave_compose-0.3.0/weave/core/adapters/windsurf.py +108 -0
- weave_compose-0.3.0/weave/core/composer.py +120 -0
- weave_compose-0.3.0/weave/core/detector.py +63 -0
- weave_compose-0.3.0/weave/core/embedder.py +99 -0
- weave_compose-0.3.0/weave/core/registry.py +152 -0
- weave_compose-0.3.0/weave/core/schema.py +111 -0
- weave_compose-0.3.0/weave/core/selector.py +138 -0
- weave_compose-0.3.0/weave/server/__init__.py +1 -0
- weave_compose-0.3.0/weave/server/app.py +1 -0
- weave_compose-0.3.0/weave_compose.egg-info/PKG-INFO +126 -0
- weave_compose-0.3.0/weave_compose.egg-info/SOURCES.txt +43 -0
- weave_compose-0.3.0/weave_compose.egg-info/dependency_links.txt +1 -0
- weave_compose-0.3.0/weave_compose.egg-info/entry_points.txt +2 -0
- weave_compose-0.3.0/weave_compose.egg-info/requires.txt +10 -0
- weave_compose-0.3.0/weave_compose.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Weave Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: weave-compose
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Platform-aware skill composition layer for AI coding tools
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/Adityaraj0421/weave-compose
|
|
7
|
+
Project-URL: Repository, https://github.com/Adityaraj0421/weave-compose
|
|
8
|
+
Project-URL: Changelog, https://github.com/Adityaraj0421/weave-compose/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/Adityaraj0421/weave-compose/issues
|
|
10
|
+
Keywords: ai,skills,composition,claude,cursor,codex,windsurf
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: typer
|
|
23
|
+
Requires-Dist: sentence-transformers
|
|
24
|
+
Requires-Dist: numpy
|
|
25
|
+
Requires-Dist: pyyaml
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest; extra == "dev"
|
|
28
|
+
Requires-Dist: ruff; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy; extra == "dev"
|
|
30
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# weave-compose
|
|
34
|
+
|
|
35
|
+
Platform-aware skill composition layer for AI coding tools.
|
|
36
|
+
|
|
37
|
+
> "Everyone's building skills. Weave makes them work together."
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## How It Works
|
|
42
|
+
|
|
43
|
+
Weave ingests skills from AI coding tools (Claude Code, Cursor, Codex, Windsurf) and normalises them into a universal schema — a single `Skill` object with a name, platform, capabilities, and semantic embedding. At query time it embeds your task description and selects the best skill(s) by cosine similarity against every loaded skill, with no manual routing required. When a task spans multiple skills, Weave composes their contexts into a single merged string ready for injection into any AI coding tool.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install weave-compose
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requires **Python 3.11+**. All embeddings run locally — no API key, no cloud dependency.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Quickstart
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# 1. Load skills from a directory of SKILL.md files
|
|
61
|
+
weave load ./tests/fixtures/claude_code
|
|
62
|
+
# Loaded 2 skill(s) from ./tests/fixtures/claude_code (platform: claude_code)
|
|
63
|
+
# Session saved to .weave_session.json
|
|
64
|
+
|
|
65
|
+
# 2. Query for the best skill for your task
|
|
66
|
+
weave query "design a UI component with Tailwind CSS"
|
|
67
|
+
# [1] Naksha Design System (claude_code) — score: 0.6821
|
|
68
|
+
# UI component design and design system implementation for React applications using Tailwind CSS.
|
|
69
|
+
|
|
70
|
+
# 3. List all loaded skills
|
|
71
|
+
weave list
|
|
72
|
+
# Naksha Design System claude_code [design, components, ui...]
|
|
73
|
+
# Backend API Engineer claude_code [api, rest, fastapi...]
|
|
74
|
+
#
|
|
75
|
+
# Total: 2 skill(s)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## CLI Reference
|
|
81
|
+
|
|
82
|
+
| Command | Description |
|
|
83
|
+
|---------|-------------|
|
|
84
|
+
| `weave load <path> [--platform] [--verbose]` | Load skills from a directory and save the session |
|
|
85
|
+
| `weave query "<text>" [--explain] [--top N]` | Query loaded skills and return the best match(es) |
|
|
86
|
+
| `weave list [--platform]` | List all skills in the current session |
|
|
87
|
+
| `weave status` | Show skill count, platform breakdown, and session info |
|
|
88
|
+
| `weave clear` | Clear all loaded skills and delete the session file |
|
|
89
|
+
|
|
90
|
+
Session state is stored in `.weave_session.json` in your current working directory. Run `weave load` once; all subsequent `query`, `list`, and `status` calls restore the session automatically — no re-embedding required.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## How Weave Selects Skills
|
|
95
|
+
|
|
96
|
+
Weave uses [`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) (runs on CPU, ~80 MB, cached after first use) to embed both your query and each skill's context. It returns the top match by default. If the confidence gap between the first and second result is less than `0.1`, both skills are returned for composition — Weave assumes the task spans both domains.
|
|
97
|
+
|
|
98
|
+
Use `--explain` to see the full score table:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
weave query "design a REST API" --explain
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Use `--top N` to request at least N results:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
weave query "build a feature" --top 2
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
Contributions are welcome at every layer — new platform adapters, composition strategies, CLI improvements, and documentation.
|
|
115
|
+
|
|
116
|
+
- **Contributor guide:** [docs/contributing.md](docs/contributing.md)
|
|
117
|
+
- **Writing a new adapter:** [docs/adapters.md](docs/adapters.md)
|
|
118
|
+
- **Architecture overview:** [docs/architecture.md](docs/architecture.md)
|
|
119
|
+
|
|
120
|
+
All code must pass `pytest`, `ruff check .`, and `mypy --strict .` before merging.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# weave-compose
|
|
2
|
+
|
|
3
|
+
Platform-aware skill composition layer for AI coding tools.
|
|
4
|
+
|
|
5
|
+
> "Everyone's building skills. Weave makes them work together."
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## How It Works
|
|
10
|
+
|
|
11
|
+
Weave ingests skills from AI coding tools (Claude Code, Cursor, Codex, Windsurf) and normalises them into a universal schema — a single `Skill` object with a name, platform, capabilities, and semantic embedding. At query time it embeds your task description and selects the best skill(s) by cosine similarity against every loaded skill, with no manual routing required. When a task spans multiple skills, Weave composes their contexts into a single merged string ready for injection into any AI coding tool.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install weave-compose
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires **Python 3.11+**. All embeddings run locally — no API key, no cloud dependency.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quickstart
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 1. Load skills from a directory of SKILL.md files
|
|
29
|
+
weave load ./tests/fixtures/claude_code
|
|
30
|
+
# Loaded 2 skill(s) from ./tests/fixtures/claude_code (platform: claude_code)
|
|
31
|
+
# Session saved to .weave_session.json
|
|
32
|
+
|
|
33
|
+
# 2. Query for the best skill for your task
|
|
34
|
+
weave query "design a UI component with Tailwind CSS"
|
|
35
|
+
# [1] Naksha Design System (claude_code) — score: 0.6821
|
|
36
|
+
# UI component design and design system implementation for React applications using Tailwind CSS.
|
|
37
|
+
|
|
38
|
+
# 3. List all loaded skills
|
|
39
|
+
weave list
|
|
40
|
+
# Naksha Design System claude_code [design, components, ui...]
|
|
41
|
+
# Backend API Engineer claude_code [api, rest, fastapi...]
|
|
42
|
+
#
|
|
43
|
+
# Total: 2 skill(s)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## CLI Reference
|
|
49
|
+
|
|
50
|
+
| Command | Description |
|
|
51
|
+
|---------|-------------|
|
|
52
|
+
| `weave load <path> [--platform] [--verbose]` | Load skills from a directory and save the session |
|
|
53
|
+
| `weave query "<text>" [--explain] [--top N]` | Query loaded skills and return the best match(es) |
|
|
54
|
+
| `weave list [--platform]` | List all skills in the current session |
|
|
55
|
+
| `weave status` | Show skill count, platform breakdown, and session info |
|
|
56
|
+
| `weave clear` | Clear all loaded skills and delete the session file |
|
|
57
|
+
|
|
58
|
+
Session state is stored in `.weave_session.json` in your current working directory. Run `weave load` once; all subsequent `query`, `list`, and `status` calls restore the session automatically — no re-embedding required.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## How Weave Selects Skills
|
|
63
|
+
|
|
64
|
+
Weave uses [`all-MiniLM-L6-v2`](https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2) (runs on CPU, ~80 MB, cached after first use) to embed both your query and each skill's context. It returns the top match by default. If the confidence gap between the first and second result is less than `0.1`, both skills are returned for composition — Weave assumes the task spans both domains.
|
|
65
|
+
|
|
66
|
+
Use `--explain` to see the full score table:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
weave query "design a REST API" --explain
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Use `--top N` to request at least N results:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
weave query "build a feature" --top 2
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Contributing
|
|
81
|
+
|
|
82
|
+
Contributions are welcome at every layer — new platform adapters, composition strategies, CLI improvements, and documentation.
|
|
83
|
+
|
|
84
|
+
- **Contributor guide:** [docs/contributing.md](docs/contributing.md)
|
|
85
|
+
- **Writing a new adapter:** [docs/adapters.md](docs/adapters.md)
|
|
86
|
+
- **Architecture overview:** [docs/architecture.md](docs/architecture.md)
|
|
87
|
+
|
|
88
|
+
All code must pass `pytest`, `ruff check .`, and `mypy --strict .` before merging.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "weave-compose"
|
|
3
|
+
version = "0.3.0"
|
|
4
|
+
description = "Platform-aware skill composition layer for AI coding tools"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "MIT"}
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
keywords = ["ai", "skills", "composition", "claude", "cursor", "codex", "windsurf"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"License :: OSI Approved :: MIT License",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
17
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"typer",
|
|
21
|
+
"sentence-transformers",
|
|
22
|
+
"numpy",
|
|
23
|
+
"pyyaml",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/Adityaraj0421/weave-compose"
|
|
28
|
+
Repository = "https://github.com/Adityaraj0421/weave-compose"
|
|
29
|
+
Changelog = "https://github.com/Adityaraj0421/weave-compose/blob/main/CHANGELOG.md"
|
|
30
|
+
"Bug Tracker" = "https://github.com/Adityaraj0421/weave-compose/issues"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
weave = "weave.cli.main:app"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest",
|
|
38
|
+
"ruff",
|
|
39
|
+
"mypy",
|
|
40
|
+
"types-PyYAML",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
include = ["weave*"]
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["setuptools>=68"]
|
|
48
|
+
build-backend = "setuptools.build_meta"
|
|
49
|
+
|
|
50
|
+
[tool.ruff]
|
|
51
|
+
line-length = 88
|
|
52
|
+
|
|
53
|
+
[tool.mypy]
|
|
54
|
+
strict = true
|
|
55
|
+
|
|
56
|
+
[[tool.mypy.overrides]]
|
|
57
|
+
module = "sentence_transformers"
|
|
58
|
+
ignore_missing_imports = true
|
|
59
|
+
|
|
60
|
+
[tool.pytest.ini_options]
|
|
61
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Tests for all platform adapters."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from weave.core.adapters.base import BaseAdapter
|
|
9
|
+
from weave.core.adapters.claude_code import ClaudeCodeAdapter
|
|
10
|
+
from weave.core.adapters.cursor import CursorAdapter
|
|
11
|
+
from weave.core.schema import Skill
|
|
12
|
+
|
|
13
|
+
CLAUDE_FIXTURES = Path(__file__).parent / "fixtures" / "claude_code"
|
|
14
|
+
CURSOR_FIXTURES = Path(__file__).parent / "fixtures" / "cursor"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _ConcreteAdapter(BaseAdapter):
|
|
18
|
+
"""Minimal concrete subclass used to exercise BaseAdapter helper methods."""
|
|
19
|
+
|
|
20
|
+
def load(self, path: str) -> list[Skill]:
|
|
21
|
+
"""Return an empty list — implementation not under test here."""
|
|
22
|
+
return []
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_base_adapter_cannot_be_instantiated_directly() -> None:
|
|
26
|
+
"""BaseAdapter raises TypeError when instantiated without implementing load()."""
|
|
27
|
+
with pytest.raises(TypeError):
|
|
28
|
+
BaseAdapter() # type: ignore[abstract]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_generate_id_returns_unique_values() -> None:
|
|
32
|
+
"""_generate_id() returns a different string on each call."""
|
|
33
|
+
adapter = _ConcreteAdapter()
|
|
34
|
+
first = adapter._generate_id()
|
|
35
|
+
second = adapter._generate_id()
|
|
36
|
+
assert first != second
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_timestamp_returns_valid_iso_format() -> None:
|
|
40
|
+
"""_timestamp() returns a UTC ISO 8601 string parseable by datetime.fromisoformat()."""
|
|
41
|
+
adapter = _ConcreteAdapter()
|
|
42
|
+
result = adapter._timestamp()
|
|
43
|
+
# Raises ValueError if the string is not a valid ISO 8601 timestamp
|
|
44
|
+
parsed = datetime.fromisoformat(result)
|
|
45
|
+
assert result.endswith("+00:00"), f"Expected UTC offset '+00:00', got: {result!r}"
|
|
46
|
+
assert parsed.tzinfo is not None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
# ClaudeCodeAdapter tests
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.fixture
|
|
55
|
+
def adapter() -> ClaudeCodeAdapter:
|
|
56
|
+
"""Return a fresh ClaudeCodeAdapter instance for each test."""
|
|
57
|
+
return ClaudeCodeAdapter()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@pytest.fixture
|
|
61
|
+
def cursor_adapter() -> CursorAdapter:
|
|
62
|
+
"""Return a fresh CursorAdapter instance for each test."""
|
|
63
|
+
return CursorAdapter()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_claude_load_returns_all_skills_from_directory(adapter: ClaudeCodeAdapter) -> None:
|
|
67
|
+
"""load() returns one Skill per .md file found in the fixtures directory."""
|
|
68
|
+
skills = adapter.load(str(CLAUDE_FIXTURES))
|
|
69
|
+
assert len(skills) == 2
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_claude_load_parses_frontmatter_name(adapter: ClaudeCodeAdapter) -> None:
|
|
73
|
+
"""load() correctly reads the name field from YAML frontmatter."""
|
|
74
|
+
skills = adapter.load(str(CLAUDE_FIXTURES))
|
|
75
|
+
names = {s.name for s in skills}
|
|
76
|
+
assert "Backend API Engineer" in names
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_claude_load_falls_back_to_filename_when_no_frontmatter(
|
|
80
|
+
adapter: ClaudeCodeAdapter, tmp_path: Path
|
|
81
|
+
) -> None:
|
|
82
|
+
"""load() uses the file stem as name when no YAML frontmatter is present."""
|
|
83
|
+
md_file = tmp_path / "my_skill.md"
|
|
84
|
+
md_file.write_text("This skill handles data processing tasks.\n\nMore content here.")
|
|
85
|
+
skills = adapter.load(str(tmp_path))
|
|
86
|
+
assert skills[0].name == "my_skill"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_claude_load_handles_empty_file_without_crashing(
|
|
90
|
+
adapter: ClaudeCodeAdapter, tmp_path: Path
|
|
91
|
+
) -> None:
|
|
92
|
+
"""load() skips empty .md files and returns an empty list without raising."""
|
|
93
|
+
(tmp_path / "empty.md").write_text("")
|
|
94
|
+
skills = adapter.load(str(tmp_path))
|
|
95
|
+
assert skills == []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_claude_load_returns_empty_list_for_directory_with_no_md_files(
|
|
99
|
+
adapter: ClaudeCodeAdapter, tmp_path: Path
|
|
100
|
+
) -> None:
|
|
101
|
+
"""load() returns an empty list when the directory contains no .md files."""
|
|
102
|
+
skills = adapter.load(str(tmp_path))
|
|
103
|
+
assert skills == []
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_claude_detect_returns_true_for_directory_with_md_files(
|
|
107
|
+
adapter: ClaudeCodeAdapter,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""detect() returns True for the claude_code fixtures directory."""
|
|
110
|
+
assert adapter.detect(str(CLAUDE_FIXTURES)) is True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# CursorAdapter tests
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_cursor_load_returns_all_skills_from_directory(
|
|
119
|
+
cursor_adapter: CursorAdapter,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""load() returns one Skill per cursor file found (cursorrules + mdc)."""
|
|
122
|
+
skills = cursor_adapter.load(str(CURSOR_FIXTURES))
|
|
123
|
+
assert len(skills) == 2
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_cursor_load_parses_cursorrules_name(cursor_adapter: CursorAdapter) -> None:
|
|
127
|
+
"""load() uses the file stem as name for plain .cursorrules files."""
|
|
128
|
+
skills = cursor_adapter.load(str(CURSOR_FIXTURES))
|
|
129
|
+
names = {s.name for s in skills}
|
|
130
|
+
assert "frontend" in names
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_cursor_load_parses_mdc_frontmatter_description(
|
|
134
|
+
cursor_adapter: CursorAdapter,
|
|
135
|
+
) -> None:
|
|
136
|
+
"""load() uses the MDC frontmatter description as trigger_context."""
|
|
137
|
+
skills = cursor_adapter.load(str(CURSOR_FIXTURES))
|
|
138
|
+
mdc_skill = next(s for s in skills if s.metadata.get("format") == "mdc")
|
|
139
|
+
assert "TypeScript" in mdc_skill.trigger_context
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_cursor_load_handles_empty_file_without_crashing(
|
|
143
|
+
cursor_adapter: CursorAdapter, tmp_path: Path
|
|
144
|
+
) -> None:
|
|
145
|
+
"""load() skips empty .cursorrules files and returns an empty list."""
|
|
146
|
+
(tmp_path / "rules.cursorrules").write_text("")
|
|
147
|
+
skills = cursor_adapter.load(str(tmp_path))
|
|
148
|
+
assert skills == []
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_cursor_load_returns_empty_list_for_directory_with_no_cursor_files(
|
|
152
|
+
cursor_adapter: CursorAdapter, tmp_path: Path
|
|
153
|
+
) -> None:
|
|
154
|
+
"""load() returns an empty list when no .cursorrules or .mdc files exist."""
|
|
155
|
+
skills = cursor_adapter.load(str(tmp_path))
|
|
156
|
+
assert skills == []
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_cursor_detect_returns_true_for_directory_with_cursorrules(
|
|
160
|
+
cursor_adapter: CursorAdapter,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""detect() returns True for the cursor fixtures directory."""
|
|
163
|
+
assert cursor_adapter.detect(str(CURSOR_FIXTURES)) is True
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Tests for the CodexAdapter."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from weave.core.adapters.codex import CodexAdapter
|
|
8
|
+
|
|
9
|
+
CODEX_FIXTURES = Path(__file__).parent / "fixtures" / "codex"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def codex_adapter() -> CodexAdapter:
|
|
14
|
+
"""Return a fresh CodexAdapter instance for each test."""
|
|
15
|
+
return CodexAdapter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_codex_load_returns_all_skills_from_directory(
|
|
19
|
+
codex_adapter: CodexAdapter,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""load() returns one Skill per codex file found (AGENTS.md + .codex/*.md)."""
|
|
22
|
+
skills = codex_adapter.load(str(CODEX_FIXTURES))
|
|
23
|
+
assert len(skills) == 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_codex_load_parses_agents_md_heading_name(codex_adapter: CodexAdapter) -> None:
|
|
27
|
+
"""load() uses the first # heading as name for AGENTS.md files."""
|
|
28
|
+
skills = codex_adapter.load(str(CODEX_FIXTURES))
|
|
29
|
+
names = {s.name for s in skills}
|
|
30
|
+
assert "Security Code Reviewer" in names
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_codex_load_parses_first_paragraph_as_trigger_context(
|
|
34
|
+
codex_adapter: CodexAdapter,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""load() uses the first non-heading paragraph as trigger_context."""
|
|
37
|
+
skills = codex_adapter.load(str(CODEX_FIXTURES))
|
|
38
|
+
agents_skill = next(s for s in skills if s.name == "Security Code Reviewer")
|
|
39
|
+
assert "security" in agents_skill.trigger_context.lower()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_codex_load_handles_empty_file_without_crashing(
|
|
43
|
+
codex_adapter: CodexAdapter, tmp_path: Path
|
|
44
|
+
) -> None:
|
|
45
|
+
"""load() skips an empty AGENTS.md and returns an empty list."""
|
|
46
|
+
(tmp_path / "AGENTS.md").write_text("")
|
|
47
|
+
skills = codex_adapter.load(str(tmp_path))
|
|
48
|
+
assert skills == []
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_codex_load_returns_empty_list_for_directory_with_no_codex_files(
|
|
52
|
+
codex_adapter: CodexAdapter, tmp_path: Path
|
|
53
|
+
) -> None:
|
|
54
|
+
"""load() returns an empty list when no AGENTS.md or .codex/*.md files exist."""
|
|
55
|
+
skills = codex_adapter.load(str(tmp_path))
|
|
56
|
+
assert skills == []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_codex_detect_returns_true_for_directory_with_agents_md(
|
|
60
|
+
codex_adapter: CodexAdapter,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""detect() returns True for the codex fixtures directory."""
|
|
63
|
+
assert codex_adapter.detect(str(CODEX_FIXTURES)) is True
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Tests for the WindsurfAdapter."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from weave.core.adapters.windsurf import WindsurfAdapter
|
|
8
|
+
|
|
9
|
+
WINDSURF_FIXTURES = Path(__file__).parent / "fixtures" / "windsurf"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def windsurf_adapter() -> WindsurfAdapter:
|
|
14
|
+
"""Return a fresh WindsurfAdapter instance for each test."""
|
|
15
|
+
return WindsurfAdapter()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_windsurf_load_returns_all_skills_from_directory(
|
|
19
|
+
windsurf_adapter: WindsurfAdapter,
|
|
20
|
+
) -> None:
|
|
21
|
+
"""load() returns one Skill per .windsurfrules file found in the fixtures dir."""
|
|
22
|
+
skills = windsurf_adapter.load(str(WINDSURF_FIXTURES))
|
|
23
|
+
assert len(skills) == 2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_windsurf_load_parses_name_from_file_stem(
|
|
27
|
+
windsurf_adapter: WindsurfAdapter,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""load() uses the file stem as the Skill name."""
|
|
30
|
+
skills = windsurf_adapter.load(str(WINDSURF_FIXTURES))
|
|
31
|
+
names = {s.name for s in skills}
|
|
32
|
+
assert "python_standards" in names
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_windsurf_load_parses_first_paragraph_as_trigger_context(
|
|
36
|
+
windsurf_adapter: WindsurfAdapter,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""load() uses the first paragraph of content as trigger_context."""
|
|
39
|
+
skills = windsurf_adapter.load(str(WINDSURF_FIXTURES))
|
|
40
|
+
python_skill = next(s for s in skills if s.name == "python_standards")
|
|
41
|
+
assert "Python" in python_skill.trigger_context
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_windsurf_load_handles_empty_file_without_crashing(
|
|
45
|
+
windsurf_adapter: WindsurfAdapter, tmp_path: Path
|
|
46
|
+
) -> None:
|
|
47
|
+
"""load() skips empty .windsurfrules files and returns an empty list."""
|
|
48
|
+
(tmp_path / ".windsurfrules").write_text("")
|
|
49
|
+
skills = windsurf_adapter.load(str(tmp_path))
|
|
50
|
+
assert skills == []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_windsurf_load_returns_empty_list_for_directory_with_no_windsurfrules(
|
|
54
|
+
windsurf_adapter: WindsurfAdapter, tmp_path: Path
|
|
55
|
+
) -> None:
|
|
56
|
+
"""load() returns an empty list when no .windsurfrules files exist."""
|
|
57
|
+
skills = windsurf_adapter.load(str(tmp_path))
|
|
58
|
+
assert skills == []
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_windsurf_detect_returns_true_for_directory_with_windsurfrules(
|
|
62
|
+
windsurf_adapter: WindsurfAdapter,
|
|
63
|
+
) -> None:
|
|
64
|
+
"""detect() returns True for the windsurf fixtures directory."""
|
|
65
|
+
assert windsurf_adapter.detect(str(WINDSURF_FIXTURES)) is True
|