daimyo 1.0.0__py3-none-any.whl

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 (49) hide show
  1. daimyo/__init__.py +11 -0
  2. daimyo/__main__.py +205 -0
  3. daimyo/application/__init__.py +1 -0
  4. daimyo/application/filtering/__init__.py +5 -0
  5. daimyo/application/filtering/category_filter.py +81 -0
  6. daimyo/application/formatters/__init__.py +8 -0
  7. daimyo/application/formatters/json_formatter.py +53 -0
  8. daimyo/application/formatters/markdown_formatter.py +149 -0
  9. daimyo/application/formatters/tree_builder.py +105 -0
  10. daimyo/application/formatters/yaml_formatter.py +84 -0
  11. daimyo/application/rule_service.py +140 -0
  12. daimyo/application/scope_resolution/__init__.py +5 -0
  13. daimyo/application/scope_resolution/circular_dependency_detector.py +45 -0
  14. daimyo/application/scope_resolution/multi_parent_resolver.py +80 -0
  15. daimyo/application/scope_resolution/parent_resolver.py +84 -0
  16. daimyo/application/scope_resolution/remote_scope_fetcher.py +43 -0
  17. daimyo/application/scope_resolution/scope_resolver.py +99 -0
  18. daimyo/application/scope_resolution/shard_merger.py +82 -0
  19. daimyo/application/scope_service.py +9 -0
  20. daimyo/config/__init__.py +5 -0
  21. daimyo/config/settings.py +25 -0
  22. daimyo/domain/__init__.py +51 -0
  23. daimyo/domain/exceptions.py +68 -0
  24. daimyo/domain/models.py +296 -0
  25. daimyo/domain/protocols.py +59 -0
  26. daimyo/infrastructure/__init__.py +1 -0
  27. daimyo/infrastructure/di/__init__.py +5 -0
  28. daimyo/infrastructure/di/container.py +120 -0
  29. daimyo/infrastructure/filesystem/__init__.py +12 -0
  30. daimyo/infrastructure/filesystem/scope_loader.py +112 -0
  31. daimyo/infrastructure/filesystem/yaml_parser.py +188 -0
  32. daimyo/infrastructure/logging/__init__.py +5 -0
  33. daimyo/infrastructure/logging/logger.py +73 -0
  34. daimyo/infrastructure/remote/__init__.py +5 -0
  35. daimyo/infrastructure/remote/remote_client.py +178 -0
  36. daimyo/presentation/__init__.py +1 -0
  37. daimyo/presentation/mcp/__init__.py +1 -0
  38. daimyo/presentation/mcp/server.py +128 -0
  39. daimyo/presentation/rest/__init__.py +1 -0
  40. daimyo/presentation/rest/app.py +99 -0
  41. daimyo/presentation/rest/dependencies.py +41 -0
  42. daimyo/presentation/rest/models.py +68 -0
  43. daimyo/presentation/rest/routers/__init__.py +1 -0
  44. daimyo/presentation/rest/routers/scopes.py +279 -0
  45. daimyo-1.0.0.dist-info/METADATA +301 -0
  46. daimyo-1.0.0.dist-info/RECORD +49 -0
  47. daimyo-1.0.0.dist-info/WHEEL +4 -0
  48. daimyo-1.0.0.dist-info/entry_points.txt +2 -0
  49. daimyo-1.0.0.dist-info/licenses/LICENSE +21 -0
daimyo/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ Daimyo - Rules Server for Agents
3
+
4
+ A Python server providing rules to agents through REST and MCP interfaces.
5
+ Supports scope-based rules with inheritance, categories for filtering,
6
+ and server federation for distributed rule management.
7
+ """
8
+
9
+ __version__ = "1.0.0"
10
+
11
+ __all__ = ["__version__"]
daimyo/__main__.py ADDED
@@ -0,0 +1,205 @@
1
+ """CLI entry point for Daimyo rules server."""
2
+
3
+ from enum import Enum
4
+
5
+ import typer
6
+ import uvicorn
7
+
8
+ from daimyo import __version__
9
+ from daimyo.config import settings
10
+ from daimyo.infrastructure.logging import setup_logging
11
+
12
+ app = typer.Typer(
13
+ name="daimyo",
14
+ help="Daimyo - Rules Server for Agents",
15
+ add_completion=False,
16
+ )
17
+
18
+
19
+ class TransportType(str, Enum):
20
+ """MCP transport types."""
21
+
22
+ STDIO = "stdio"
23
+ SSE = "sse"
24
+
25
+
26
+ def version_callback(value: bool) -> None:
27
+ """Print version and exit.
28
+
29
+ :param value: Whether version flag was set
30
+ :type value: bool
31
+ :rtype: None
32
+ :raises typer.Exit: Always exits after printing version
33
+ """
34
+ if value:
35
+ typer.echo(f"daimyo version {__version__}")
36
+ raise typer.Exit()
37
+
38
+
39
+ @app.callback()
40
+ def main(
41
+ version: bool = typer.Option(
42
+ False,
43
+ "--version",
44
+ "-v",
45
+ callback=version_callback,
46
+ is_eager=True,
47
+ help="Show version and exit",
48
+ ),
49
+ ) -> None:
50
+ """Daimyo - Rules Server for Agents.
51
+
52
+ :param version: Show version flag
53
+ :type version: bool
54
+ :rtype: None
55
+ """
56
+ pass
57
+
58
+
59
+ @app.command()
60
+ def serve(
61
+ host: str = typer.Option(
62
+ None,
63
+ "--host",
64
+ help=f"Host to bind to (default: {settings.REST_HOST})",
65
+ ),
66
+ port: int = typer.Option(
67
+ None,
68
+ "--port",
69
+ help=f"Port to bind to (default: {settings.REST_PORT})",
70
+ ),
71
+ reload: bool = typer.Option(
72
+ False,
73
+ "--reload",
74
+ help="Enable auto-reload for development",
75
+ ),
76
+ ) -> None:
77
+ """Start the REST API server.
78
+
79
+ :param host: Host to bind to
80
+ :type host: str
81
+ :param port: Port to bind to
82
+ :type port: int
83
+ :param reload: Enable auto-reload for development
84
+ :type reload: bool
85
+ :rtype: None
86
+ """
87
+ setup_logging()
88
+
89
+ resolved_host = host or settings.REST_HOST
90
+ resolved_port = port or settings.REST_PORT
91
+
92
+ typer.echo(f"Starting Daimyo REST API server on {resolved_host}:{resolved_port}")
93
+ typer.echo(f"API documentation available at http://{resolved_host}:{resolved_port}/docs")
94
+
95
+ uvicorn.run(
96
+ "daimyo.presentation.rest.app:app",
97
+ host=resolved_host,
98
+ port=resolved_port,
99
+ reload=reload,
100
+ log_config=None,
101
+ )
102
+
103
+
104
+ @app.command()
105
+ def mcp(
106
+ transport: TransportType = typer.Option(
107
+ None,
108
+ "--transport",
109
+ help=f"MCP transport type (default: {settings.MCP_TRANSPORT})",
110
+ ),
111
+ ) -> None:
112
+ """Start the MCP server.
113
+
114
+ :param transport: MCP transport type
115
+ :type transport: TransportType
116
+ :rtype: None
117
+ """
118
+ setup_logging()
119
+
120
+ resolved_transport = transport.value if transport else settings.MCP_TRANSPORT
121
+
122
+ typer.echo(f"Starting Daimyo MCP server with {resolved_transport} transport")
123
+
124
+ from daimyo.presentation.mcp.server import mcp as mcp_server
125
+
126
+ if resolved_transport == "stdio":
127
+ mcp_server.run(transport="stdio")
128
+ else:
129
+ mcp_server.run(transport="sse")
130
+
131
+
132
+ @app.command()
133
+ def list_scopes() -> None:
134
+ """List all available scopes.
135
+
136
+ :rtype: None
137
+ """
138
+ setup_logging()
139
+
140
+ from daimyo.infrastructure.di import get_container
141
+
142
+ container = get_container()
143
+ repo = container.scope_repository()
144
+ scopes = repo.list_scopes()
145
+
146
+ if not scopes:
147
+ typer.echo("No scopes found.")
148
+ return
149
+
150
+ typer.echo("Available scopes:")
151
+ for scope_name in scopes:
152
+ typer.echo(f" - {scope_name}")
153
+
154
+
155
+ @app.command()
156
+ def show(
157
+ scope_name: str = typer.Argument(
158
+ ...,
159
+ help="Name of the scope to show",
160
+ ),
161
+ ) -> None:
162
+ """Show details of a specific scope.
163
+
164
+ :param scope_name: Name of the scope to show
165
+ :type scope_name: str
166
+ :rtype: None
167
+ """
168
+ setup_logging()
169
+
170
+ from daimyo.infrastructure.di import get_container
171
+
172
+ try:
173
+ container = get_container()
174
+ repo = container.scope_repository()
175
+ scope = repo.get_scope(scope_name)
176
+
177
+ if scope is None:
178
+ typer.echo(f"Error: Scope '{scope_name}' not found", err=True)
179
+ raise typer.Exit(code=1)
180
+
181
+ typer.echo(f"# Scope: {scope.metadata.name}")
182
+ typer.echo(f"\nDescription: {scope.metadata.description}")
183
+ if scope.metadata.parent:
184
+ typer.echo(f"Parent: {scope.metadata.parent}")
185
+ if scope.metadata.tags:
186
+ typer.echo(f"Tags: {scope.metadata.tags}")
187
+
188
+ typer.echo(f"\nCommandments: {len(scope.commandments.categories)} categories")
189
+ typer.echo(f"Suggestions: {len(scope.suggestions.categories)} categories")
190
+
191
+ except Exception as e:
192
+ typer.echo(f"Error: {str(e)}", err=True)
193
+ raise typer.Exit(code=1)
194
+
195
+
196
+ def cli() -> None:
197
+ """Entry point for the CLI.
198
+
199
+ :rtype: None
200
+ """
201
+ app()
202
+
203
+
204
+ if __name__ == "__main__":
205
+ cli()
@@ -0,0 +1 @@
1
+ """Application layer - business logic and use cases."""
@@ -0,0 +1,5 @@
1
+ """Category filtering services."""
2
+
3
+ from .category_filter import CategoryFilterService
4
+
5
+ __all__ = ["CategoryFilterService"]
@@ -0,0 +1,81 @@
1
+ """Category filtering service for scopes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from daimyo.application.rule_service import RuleMergingService
6
+ from daimyo.domain import MergedScope
7
+ from daimyo.infrastructure.logging import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class CategoryFilterService:
13
+ """Service for filtering categories in scopes."""
14
+
15
+ def __init__(self, rule_service: RuleMergingService):
16
+ """Initialize category filter service.
17
+
18
+ :param rule_service: Rule merging service for filtering
19
+ :type rule_service: RuleMergingService
20
+ """
21
+ self.rule_service = rule_service
22
+
23
+ @staticmethod
24
+ def parse_category_string(categories: str | None) -> list[str]:
25
+ """Parse comma-separated category string into list.
26
+
27
+ :param categories: Comma-separated category string (e.g., "python.web,python.testing")
28
+ :type categories: str | None
29
+ :returns: List of category filters, empty list if None
30
+ :rtype: list[str]
31
+ """
32
+ if not categories:
33
+ return []
34
+ return [c.strip() for c in categories.split(",")]
35
+
36
+ def apply_filters(
37
+ self, scope: MergedScope, category_filters: list[str] | None
38
+ ) -> MergedScope:
39
+ """Apply category filters to a merged scope.
40
+
41
+ Filters both commandments and suggestions by the given category prefixes.
42
+ If no filters provided, returns scope unchanged.
43
+
44
+ :param scope: Merged scope to filter
45
+ :type scope: MergedScope
46
+ :param category_filters: List of category prefix filters
47
+ :type category_filters: list[str] | None
48
+ :returns: Scope with filtered categories
49
+ :rtype: MergedScope
50
+ """
51
+ if not category_filters:
52
+ return scope
53
+
54
+ logger.debug(f"Filtering scope by categories: {category_filters}")
55
+
56
+ scope.commandments = self.rule_service.filter_categories(
57
+ scope.commandments, category_filters
58
+ )
59
+ scope.suggestions = self.rule_service.filter_categories(
60
+ scope.suggestions, category_filters
61
+ )
62
+
63
+ return scope
64
+
65
+ def filter_from_string(self, scope: MergedScope, categories: str | None) -> MergedScope:
66
+ """Parse category string and apply filters to scope.
67
+
68
+ Convenience method combining parse_category_string and apply_filters.
69
+
70
+ :param scope: Merged scope to filter
71
+ :type scope: MergedScope
72
+ :param categories: Comma-separated category string
73
+ :type categories: str | None
74
+ :returns: Scope with filtered categories
75
+ :rtype: MergedScope
76
+ """
77
+ category_list = self.parse_category_string(categories)
78
+ return self.apply_filters(scope, category_list)
79
+
80
+
81
+ __all__ = ["CategoryFilterService"]
@@ -0,0 +1,8 @@
1
+ """Output formatters for different response formats."""
2
+
3
+ from .json_formatter import JsonFormatter
4
+ from .markdown_formatter import MarkdownFormatter
5
+ from .tree_builder import CategoryTreeBuilder
6
+ from .yaml_formatter import YamlMultiDocFormatter
7
+
8
+ __all__ = ["YamlMultiDocFormatter", "JsonFormatter", "MarkdownFormatter", "CategoryTreeBuilder"]
@@ -0,0 +1,53 @@
1
+ """JSON formatter for API responses."""
2
+
3
+ from typing import Any
4
+
5
+ from daimyo.domain import MergedScope, RuleSet
6
+
7
+
8
+ class JsonFormatter:
9
+ """Format merged scope as JSON.
10
+
11
+ Output contains structured JSON with metadata, commandments, and suggestions.
12
+ """
13
+
14
+ def format(self, scope: MergedScope) -> dict[str, Any]:
15
+ """Format merged scope as JSON-serializable dictionary.
16
+
17
+ :param scope: The merged scope to format
18
+ :type scope: MergedScope
19
+ :returns: Dictionary ready for JSON serialization
20
+ :rtype: Dict[str, Any]
21
+ """
22
+ return {
23
+ "metadata": {
24
+ "name": scope.metadata.name,
25
+ "description": scope.metadata.description,
26
+ "parent": scope.metadata.parent,
27
+ "tags": scope.metadata.tags,
28
+ "sources": scope.sources,
29
+ },
30
+ "commandments": self._format_ruleset(scope.commandments),
31
+ "suggestions": self._format_ruleset(scope.suggestions),
32
+ }
33
+
34
+ def _format_ruleset(self, ruleset: RuleSet) -> dict[str, dict[str, Any]]:
35
+ """Format a ruleset as flat dictionary with category keys.
36
+
37
+ :param ruleset: The ruleset to format
38
+ :type ruleset: RuleSet
39
+ :returns: Dictionary mapping category keys to their data
40
+ :rtype: Dict[str, Dict[str, Any]]
41
+ """
42
+ result = {}
43
+
44
+ for category in ruleset.categories.values():
45
+ result[str(category.key)] = {
46
+ "when": category.when,
47
+ "rules": [rule.text for rule in category.rules],
48
+ }
49
+
50
+ return result
51
+
52
+
53
+ __all__ = ["JsonFormatter"]
@@ -0,0 +1,149 @@
1
+ """Markdown formatter for MCP API responses."""
2
+
3
+ from daimyo.application.formatters.tree_builder import CategoryTreeBuilder
4
+ from daimyo.domain import Category, MergedScope, RuleSet
5
+
6
+
7
+ class MarkdownFormatter:
8
+ """Format merged scope as markdown with hierarchy and MUST/SHOULD markers.
9
+
10
+ Features:
11
+ - Nested headings for category hierarchy (## python, ### web, #### testing)
12
+ - MUST markers for commandments, SHOULD markers for suggestions
13
+ - Include 'when' descriptions for each category
14
+ """
15
+
16
+ def format(self, scope: MergedScope) -> str:
17
+ """Format merged scope as markdown.
18
+
19
+ :param scope: The merged scope to format
20
+ :type scope: MergedScope
21
+ :returns: Markdown-formatted string
22
+ :rtype: str
23
+ """
24
+ lines = []
25
+
26
+ lines.append(f"# Rules for {scope.metadata.name}\n")
27
+ if scope.metadata.description:
28
+ lines.append(f"{scope.metadata.description}\n")
29
+
30
+ merged_tree = CategoryTreeBuilder.merge_trees(
31
+ list(scope.commandments.categories.values()),
32
+ list(scope.suggestions.categories.values())
33
+ )
34
+
35
+ lines.extend(self._format_merged_tree(merged_tree, depth=2))
36
+
37
+ return "\n".join(lines)
38
+
39
+ def _format_merged_tree(
40
+ self, tree: dict[str, dict], depth: int, path: str = ""
41
+ ) -> list[str]:
42
+ """Format merged category tree with both MUST and SHOULD rules.
43
+
44
+ :param tree: Merged category tree
45
+ :type tree: Dict[str, Dict]
46
+ :param depth: Current heading depth
47
+ :type depth: int
48
+ :param path: Current category path
49
+ :type path: str
50
+ :returns: List of markdown lines
51
+ :rtype: List[str]
52
+ """
53
+ lines = []
54
+
55
+ for key, node in sorted(tree.items()):
56
+ heading = "#" * depth
57
+ current_path = f"{path}.{key}" if path else key
58
+ lines.append(f"{heading} {key}\n")
59
+
60
+ commandments = node.get("_commandments", [])
61
+ suggestions = node.get("_suggestions", [])
62
+
63
+ when_description = None
64
+ for category in commandments:
65
+ if category.when:
66
+ when_description = category.when
67
+ break
68
+ if not when_description:
69
+ for category in suggestions:
70
+ if category.when:
71
+ when_description = category.when
72
+ break
73
+
74
+ if when_description:
75
+ lines.append(f"*{when_description}*\n")
76
+
77
+ for category in commandments:
78
+ for rule in category.rules:
79
+ lines.append(f"- **MUST**: {rule.text}")
80
+
81
+ for category in suggestions:
82
+ for rule in category.rules:
83
+ lines.append(f"- **SHOULD**: {rule.text}")
84
+
85
+ if commandments or suggestions:
86
+ lines.append("")
87
+
88
+ children = node.get("_children", {})
89
+ if children:
90
+ lines.extend(self._format_merged_tree(children, depth + 1, current_path))
91
+
92
+ return lines
93
+
94
+ def _format_ruleset_markdown(self, ruleset: RuleSet, marker: str) -> str:
95
+ """Format a ruleset as markdown with hierarchical headings.
96
+
97
+ :param ruleset: The ruleset to format
98
+ :type ruleset: RuleSet
99
+ :param marker: The marker to use (MUST or SHOULD)
100
+ :type marker: str
101
+ :returns: Markdown string
102
+ :rtype: str
103
+ """
104
+ lines = []
105
+
106
+ category_tree = CategoryTreeBuilder.build_tree(list(ruleset.categories.values()))
107
+
108
+ lines.extend(self._format_tree(category_tree, marker, depth=3))
109
+
110
+ return "\n".join(lines)
111
+
112
+ def _format_tree(
113
+ self, tree: dict[str, dict], marker: str, depth: int, path: str = ""
114
+ ) -> list[str]:
115
+ """Recursively format category tree as markdown.
116
+
117
+ :param tree: Category tree
118
+ :type tree: Dict[str, Dict]
119
+ :param marker: MUST or SHOULD
120
+ :type marker: str
121
+ :param depth: Current heading depth
122
+ :type depth: int
123
+ :param path: Current category path
124
+ :type path: str
125
+ :returns: List of markdown lines
126
+ :rtype: List[str]
127
+ """
128
+ lines = []
129
+
130
+ for key, node in sorted(tree.items()):
131
+ heading = "#" * depth
132
+ current_path = f"{path}.{key}" if path else key
133
+ lines.append(f"{heading} {key}\n")
134
+
135
+ for category in node.get("_categories", []):
136
+ lines.append(f"*{category.when}*\n")
137
+
138
+ for rule in category.rules:
139
+ lines.append(f"- **{marker}**: {rule.text}")
140
+ lines.append("")
141
+
142
+ children = node.get("_children", {})
143
+ if children:
144
+ lines.extend(self._format_tree(children, marker, depth + 1, current_path))
145
+
146
+ return lines
147
+
148
+
149
+ __all__ = ["MarkdownFormatter"]
@@ -0,0 +1,105 @@
1
+ """Category tree building utilities for formatters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from daimyo.domain import Category
6
+
7
+
8
+ class CategoryTreeBuilder:
9
+ """Utility for building hierarchical category trees."""
10
+
11
+ @staticmethod
12
+ def build_tree(categories: list[Category]) -> dict[str, dict]:
13
+ """Build a hierarchical tree from flat category list.
14
+
15
+ :param categories: List of categories
16
+ :type categories: list[Category]
17
+ :returns: Nested dictionary representing category hierarchy
18
+ :rtype: dict[str, dict]
19
+ """
20
+ tree: dict[str, dict] = {}
21
+
22
+ for category in categories:
23
+ parts = category.key.parts
24
+ current = tree
25
+
26
+ for i, part in enumerate(parts):
27
+ if part not in current:
28
+ current[part] = {"_categories": [], "_children": {}}
29
+
30
+ if i == len(parts) - 1:
31
+ current[part]["_categories"].append(category)
32
+ else:
33
+ current = current[part]["_children"]
34
+
35
+ return tree
36
+
37
+ @staticmethod
38
+ def merge_trees(commandments: list[Category], suggestions: list[Category]) -> dict[str, dict]:
39
+ """Merge commandment and suggestion categories into a single tree.
40
+
41
+ :param commandments: List of commandment categories
42
+ :type commandments: list[Category]
43
+ :param suggestions: List of suggestion categories
44
+ :type suggestions: list[Category]
45
+ :returns: Merged tree with both commandments and suggestions
46
+ :rtype: dict[str, dict]
47
+ """
48
+ tree: dict[str, dict] = {}
49
+
50
+ for category in commandments:
51
+ parts = category.key.parts
52
+ current = tree
53
+
54
+ for i, part in enumerate(parts):
55
+ if part not in current:
56
+ current[part] = {"_commandments": [], "_suggestions": [], "_children": {}}
57
+
58
+ if i == len(parts) - 1:
59
+ current[part]["_commandments"].append(category)
60
+ else:
61
+ current = current[part]["_children"]
62
+
63
+ for category in suggestions:
64
+ parts = category.key.parts
65
+ current = tree
66
+
67
+ for i, part in enumerate(parts):
68
+ if part not in current:
69
+ current[part] = {"_commandments": [], "_suggestions": [], "_children": {}}
70
+
71
+ if i == len(parts) - 1:
72
+ current[part]["_suggestions"].append(category)
73
+ else:
74
+ current = current[part]["_children"]
75
+
76
+ return tree
77
+
78
+ @staticmethod
79
+ def build_index_tree(categories: list[tuple[str, str]]) -> dict[str, dict]:
80
+ """Build tree for index display from category key-when pairs.
81
+
82
+ :param categories: List of (category_key, when_description) tuples
83
+ :type categories: list[tuple[str, str]]
84
+ :returns: Nested dictionary for rendering category index
85
+ :rtype: dict[str, dict]
86
+ """
87
+ tree: dict[str, dict] = {}
88
+
89
+ for category_key, when_desc in categories:
90
+ parts = category_key.split(".")
91
+ current = tree
92
+
93
+ for i, part in enumerate(parts):
94
+ if part not in current:
95
+ current[part] = {"_children": {}, "_key": ".".join(parts[: i + 1])}
96
+
97
+ if i == len(parts) - 1:
98
+ current[part]["_when"] = when_desc
99
+
100
+ current = current[part]["_children"]
101
+
102
+ return tree
103
+
104
+
105
+ __all__ = ["CategoryTreeBuilder"]