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.
Files changed (45) hide show
  1. weave_compose-0.3.0/LICENSE +21 -0
  2. weave_compose-0.3.0/PKG-INFO +126 -0
  3. weave_compose-0.3.0/README.md +94 -0
  4. weave_compose-0.3.0/pyproject.toml +61 -0
  5. weave_compose-0.3.0/setup.cfg +4 -0
  6. weave_compose-0.3.0/tests/test_adapters.py +163 -0
  7. weave_compose-0.3.0/tests/test_adapters_codex.py +63 -0
  8. weave_compose-0.3.0/tests/test_adapters_windsurf.py +65 -0
  9. weave_compose-0.3.0/tests/test_cli.py +105 -0
  10. weave_compose-0.3.0/tests/test_composer.py +97 -0
  11. weave_compose-0.3.0/tests/test_config.py +120 -0
  12. weave_compose-0.3.0/tests/test_detector.py +62 -0
  13. weave_compose-0.3.0/tests/test_e2e.py +169 -0
  14. weave_compose-0.3.0/tests/test_registry.py +108 -0
  15. weave_compose-0.3.0/tests/test_schema.py +95 -0
  16. weave_compose-0.3.0/tests/test_selector.py +132 -0
  17. weave_compose-0.3.0/weave/__init__.py +1 -0
  18. weave_compose-0.3.0/weave/cli/__init__.py +1 -0
  19. weave_compose-0.3.0/weave/cli/config.py +182 -0
  20. weave_compose-0.3.0/weave/cli/config_schema.py +68 -0
  21. weave_compose-0.3.0/weave/cli/main.py +181 -0
  22. weave_compose-0.3.0/weave/cli/query_command.py +65 -0
  23. weave_compose-0.3.0/weave/cli/run_command.py +128 -0
  24. weave_compose-0.3.0/weave/core/__init__.py +1 -0
  25. weave_compose-0.3.0/weave/core/adapters/__init__.py +1 -0
  26. weave_compose-0.3.0/weave/core/adapters/base.py +165 -0
  27. weave_compose-0.3.0/weave/core/adapters/claude_code.py +158 -0
  28. weave_compose-0.3.0/weave/core/adapters/codex.py +185 -0
  29. weave_compose-0.3.0/weave/core/adapters/cursor.py +186 -0
  30. weave_compose-0.3.0/weave/core/adapters/gemini_cli.py +1 -0
  31. weave_compose-0.3.0/weave/core/adapters/windsurf.py +108 -0
  32. weave_compose-0.3.0/weave/core/composer.py +120 -0
  33. weave_compose-0.3.0/weave/core/detector.py +63 -0
  34. weave_compose-0.3.0/weave/core/embedder.py +99 -0
  35. weave_compose-0.3.0/weave/core/registry.py +152 -0
  36. weave_compose-0.3.0/weave/core/schema.py +111 -0
  37. weave_compose-0.3.0/weave/core/selector.py +138 -0
  38. weave_compose-0.3.0/weave/server/__init__.py +1 -0
  39. weave_compose-0.3.0/weave/server/app.py +1 -0
  40. weave_compose-0.3.0/weave_compose.egg-info/PKG-INFO +126 -0
  41. weave_compose-0.3.0/weave_compose.egg-info/SOURCES.txt +43 -0
  42. weave_compose-0.3.0/weave_compose.egg-info/dependency_links.txt +1 -0
  43. weave_compose-0.3.0/weave_compose.egg-info/entry_points.txt +2 -0
  44. weave_compose-0.3.0/weave_compose.egg-info/requires.txt +10 -0
  45. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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