hexdag-forge 0.2.0.dev2__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 (36) hide show
  1. hexdag_forge/__init__.py +3 -0
  2. hexdag_forge/cli.py +71 -0
  3. hexdag_forge/domain/__init__.py +20 -0
  4. hexdag_forge/domain/business_profile.py +100 -0
  5. hexdag_forge/domain/enums.py +29 -0
  6. hexdag_forge/domain/generated_system.py +23 -0
  7. hexdag_forge/generator/__init__.py +1 -0
  8. hexdag_forge/generator/assembler.py +121 -0
  9. hexdag_forge/generator/business_analyzer.py +161 -0
  10. hexdag_forge/generator/django_generator.py +27 -0
  11. hexdag_forge/generator/entity_generator.py +18 -0
  12. hexdag_forge/generator/pipeline_generator.py +24 -0
  13. hexdag_forge/generator/service_generator.py +55 -0
  14. hexdag_forge/generator/system_generator.py +21 -0
  15. hexdag_forge/generator/template_engine.py +27 -0
  16. hexdag_forge/mcp/__init__.py +0 -0
  17. hexdag_forge/mcp/__main__.py +5 -0
  18. hexdag_forge/mcp/server.py +180 -0
  19. hexdag_forge/skills/forge.md +34 -0
  20. hexdag_forge/skills/forge_entity.md +25 -0
  21. hexdag_forge/skills/forge_implement.md +39 -0
  22. hexdag_forge/skills/forge_test.md +23 -0
  23. hexdag_forge/templates/django_admin.py.j2 +21 -0
  24. hexdag_forge/templates/django_manage.py.j2 +19 -0
  25. hexdag_forge/templates/django_model.py.j2 +62 -0
  26. hexdag_forge/templates/django_settings.py.j2 +63 -0
  27. hexdag_forge/templates/django_urls.py.j2 +11 -0
  28. hexdag_forge/templates/pipeline.yaml.j2 +40 -0
  29. hexdag_forge/templates/service.py.j2 +97 -0
  30. hexdag_forge/templates/state_machines.py.j2 +27 -0
  31. hexdag_forge/templates/system.yaml.j2 +45 -0
  32. hexdag_forge-0.2.0.dev2.dist-info/METADATA +101 -0
  33. hexdag_forge-0.2.0.dev2.dist-info/RECORD +36 -0
  34. hexdag_forge-0.2.0.dev2.dist-info/WHEEL +4 -0
  35. hexdag_forge-0.2.0.dev2.dist-info/entry_points.txt +2 -0
  36. hexdag_forge-0.2.0.dev2.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,27 @@
1
+ """Jinja2 template rendering engine for code generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from jinja2 import Environment, FileSystemLoader
8
+
9
+ _TEMPLATE_DIR = Path(__file__).parent.parent / "templates"
10
+
11
+
12
+ def create_env() -> Environment:
13
+ """Create a Jinja2 environment configured for code generation."""
14
+ return Environment( # nosec B701 — code generation templates, not HTML
15
+ loader=FileSystemLoader(str(_TEMPLATE_DIR)),
16
+ keep_trailing_newline=True,
17
+ trim_blocks=True,
18
+ lstrip_blocks=True,
19
+ autoescape=False,
20
+ )
21
+
22
+
23
+ def render_template(template_name: str, **context: object) -> str:
24
+ """Render a Jinja2 template with the given context."""
25
+ env = create_env()
26
+ template = env.get_template(template_name)
27
+ return template.render(**context)
File without changes
@@ -0,0 +1,5 @@
1
+ """Entry point for running the forge MCP server via `python -m hexdag_forge.mcp`."""
2
+
3
+ from hexdag_forge.mcp.server import mcp
4
+
5
+ mcp.run(transport="stdio")
@@ -0,0 +1,180 @@
1
+ """MCP server for hexdag-forge.
2
+
3
+ Exposes forge tools via FastMCP so Claude Code can generate
4
+ and manage operational systems.
5
+
6
+ Usage:
7
+ hexdag-forge mcp serve # stdio (for Claude Code)
8
+ hexdag-forge mcp serve --transport sse --port 3001
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+
16
+ from mcp.server.fastmcp import FastMCP
17
+
18
+ from hexdag_forge.domain.business_profile import BusinessProfile
19
+ from hexdag_forge.generator.assembler import assemble, validate_system
20
+ from hexdag_forge.generator.business_analyzer import create_default_profile
21
+
22
+ mcp = FastMCP(
23
+ "hexdag-forge",
24
+ instructions="Generate operational systems (ERP/TMS/CRM) from business descriptions. "
25
+ "Use forge_generate to create a system, forge_get_schema to inspect it, "
26
+ "forge_list_stubs to find methods that need implementation.",
27
+ )
28
+
29
+
30
+ @mcp.tool()
31
+ async def forge_generate(description: str, output_dir: str = "./generated") -> str:
32
+ """Generate a complete operational system from a business description.
33
+
34
+ Creates YAML pipelines, Python services, Django models, state machines,
35
+ and a kind: System manifest in the output directory.
36
+ """
37
+ profile = create_default_profile(description)
38
+ result = assemble(profile, Path(output_dir))
39
+
40
+ summary = {
41
+ "output_dir": str(output_dir),
42
+ "business_name": result.profile.business_name,
43
+ "entities": [
44
+ {
45
+ "name": e.name,
46
+ "display_name": e.display_name,
47
+ "fields": len(e.fields),
48
+ "states": e.states,
49
+ }
50
+ for e in result.profile.entities
51
+ ],
52
+ "pipelines": list(result.pipelines.keys()),
53
+ "services": list(result.services.keys()),
54
+ }
55
+ return json.dumps(summary, indent=2)
56
+
57
+
58
+ @mcp.tool()
59
+ async def forge_generate_from_profile(profile_json: str, output_dir: str = "./generated") -> str:
60
+ """Generate a system from a pre-built BusinessProfile JSON.
61
+
62
+ Use this when you've already extracted the entities, fields, states,
63
+ and relationships — pass the full BusinessProfile as JSON.
64
+ """
65
+ profile = BusinessProfile.model_validate_json(profile_json)
66
+ result = assemble(profile, Path(output_dir))
67
+
68
+ return json.dumps(
69
+ {
70
+ "output_dir": str(output_dir),
71
+ "entities": [e.name for e in result.profile.entities],
72
+ "pipelines": list(result.pipelines.keys()),
73
+ "services": list(result.services.keys()),
74
+ },
75
+ indent=2,
76
+ )
77
+
78
+
79
+ @mcp.tool()
80
+ async def forge_validate(output_dir: str) -> str:
81
+ """Validate all generated files in a directory.
82
+
83
+ Checks: required files exist, Python syntax valid, profile.json valid.
84
+ """
85
+ errors = validate_system(Path(output_dir))
86
+ return json.dumps({"valid": len(errors) == 0, "errors": errors}, indent=2)
87
+
88
+
89
+ @mcp.tool()
90
+ async def forge_get_schema(output_dir: str) -> str:
91
+ """Read the BusinessProfile from a generated system.
92
+
93
+ Returns the full schema: entities with fields, states, transitions, relationships.
94
+ """
95
+ profile_path = Path(output_dir) / "profile.json"
96
+ if not profile_path.exists():
97
+ return json.dumps({"error": f"No profile.json found in {output_dir}"})
98
+
99
+ profile = BusinessProfile.model_validate_json(profile_path.read_text())
100
+ return json.dumps(profile.model_dump(), indent=2, default=str)
101
+
102
+
103
+ @mcp.tool()
104
+ async def forge_list_stubs(output_dir: str) -> str:
105
+ """List all NotImplementedError stubs in generated services.
106
+
107
+ Shows methods that need actual business logic implementation.
108
+ """
109
+ import ast
110
+
111
+ stubs: list[dict[str, str]] = []
112
+ services_dir = Path(output_dir) / "services"
113
+ if not services_dir.exists():
114
+ return json.dumps([])
115
+
116
+ for py_file in services_dir.glob("*.py"):
117
+ tree = ast.parse(py_file.read_text())
118
+ for node in ast.walk(tree):
119
+ if isinstance(node, ast.AsyncFunctionDef):
120
+ for stmt in ast.walk(node):
121
+ if isinstance(stmt, ast.Raise) and isinstance(stmt.exc, ast.Call):
122
+ func = stmt.exc.func
123
+ if isinstance(func, ast.Name) and func.id == "NotImplementedError":
124
+ stubs.append({
125
+ "service": py_file.stem,
126
+ "method": node.name,
127
+ "file": str(py_file.relative_to(output_dir)),
128
+ "docstring": ast.get_docstring(node) or "",
129
+ })
130
+
131
+ return json.dumps(stubs, indent=2)
132
+
133
+
134
+ @mcp.tool()
135
+ async def forge_implement_stub(
136
+ output_dir: str,
137
+ service_name: str,
138
+ method_name: str,
139
+ implementation: str,
140
+ ) -> str:
141
+ """Replace a NotImplementedError stub with actual implementation code.
142
+
143
+ The implementation should be the method body lines (properly indented with 8 spaces).
144
+ """
145
+ import ast
146
+ import re
147
+
148
+ service_path = Path(output_dir) / "services" / f"{service_name}.py"
149
+ if not service_path.exists():
150
+ return json.dumps({"error": f"Service file not found: {service_name}.py"})
151
+
152
+ source = service_path.read_text()
153
+
154
+ pattern = rf"( raise NotImplementedError\([^)]*{re.escape(method_name)}[^)]*\))"
155
+ match = re.search(pattern, source)
156
+ if not match:
157
+ return json.dumps({"error": f"Stub for {method_name} not found"})
158
+
159
+ new_source = source[: match.start()] + implementation + source[match.end() :]
160
+
161
+ try:
162
+ ast.parse(new_source)
163
+ except SyntaxError as e:
164
+ return json.dumps({"error": f"Syntax error in implementation: {e}"})
165
+
166
+ service_path.write_text(new_source)
167
+ return json.dumps({"success": True, "file": str(service_path)})
168
+
169
+
170
+ def run_server(transport: str = "stdio", port: int = 3001) -> None:
171
+ """Start the MCP server."""
172
+ if transport == "stdio":
173
+ mcp.run(transport="stdio")
174
+ elif transport == "sse":
175
+ mcp.run(
176
+ transport="sse",
177
+ sse_params={"host": "0.0.0.0", "port": port}, # nosec B104
178
+ )
179
+ else:
180
+ mcp.run(transport=transport)
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: forge
3
+ description: Generate an operational system from a business description
4
+ ---
5
+
6
+ Use the forge MCP server to generate a complete operational system.
7
+
8
+ ## Steps
9
+
10
+ 1. Call `forge_generate` with the user's business description and an output directory.
11
+ - If the user provides a detailed BusinessProfile JSON, use `forge_generate_from_profile` instead.
12
+
13
+ 2. Show the user a summary of what was generated:
14
+ - Entity names, their fields and states
15
+ - Generated pipeline files
16
+ - Generated service files (with CRUD + domain stubs)
17
+
18
+ 3. Ask if the user wants to refine the generated system:
19
+ - If yes, call `forge_refine` with their instructions
20
+ - Show what changed
21
+
22
+ 4. List any stubs that need implementation:
23
+ - Call `forge_list_stubs` to show NotImplementedError methods
24
+ - Ask if the user wants to implement any of them
25
+
26
+ 5. Validate the output:
27
+ - Call `forge_validate` to check all files are valid
28
+
29
+ ## Notes
30
+
31
+ - The generated system includes: system.yaml, YAML pipelines, Python services, Django models, state machines
32
+ - Services have `@tool`/`@step` decorated CRUD methods (fully implemented) and domain-specific stubs (NotImplementedError)
33
+ - The system.yaml uses hexDAG's `kind: System` with LifecycleRunner (state machine driven)
34
+ - profile.json is saved for future refinement
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: forge:entity
3
+ description: Add or modify an entity type in a generated forge system
4
+ ---
5
+
6
+ Use the forge MCP server to add or modify entities in a generated system.
7
+
8
+ ## Adding an entity
9
+
10
+ 1. Call `forge_get_schema` to see the current entities in the system
11
+ 2. Build the entity spec from the user's description (name, fields, states, transitions, relationships)
12
+ 3. Load the current profile, add the new entity, and call `forge_generate_from_profile` to regenerate
13
+
14
+ ## Modifying an entity
15
+
16
+ 1. Call `forge_get_schema` to see current entity definition
17
+ 2. Modify the profile JSON with the user's requested changes
18
+ 3. Call `forge_generate_from_profile` with the updated profile to regenerate all files
19
+
20
+ ## Notes
21
+
22
+ - Entity names must be valid Python identifiers (snake_case)
23
+ - Every entity needs at least 2 states and a valid initial_state
24
+ - Transitions must only reference declared states
25
+ - Relationships use: belongs_to, has_many, has_one
@@ -0,0 +1,39 @@
1
+ ---
2
+ name: forge:implement
3
+ description: Implement a service stub method with business logic
4
+ ---
5
+
6
+ Use the forge MCP server to fill in NotImplementedError stubs with actual implementations.
7
+
8
+ ## Steps
9
+
10
+ 1. Call `forge_list_stubs` to see all stubs that need implementation
11
+ - Shows: service name, method name, docstring with expected behavior
12
+
13
+ 2. If the user specifies which stub, proceed. Otherwise ask them to pick one.
14
+
15
+ 3. Call `forge_get_schema` to understand the entity fields, states, and relationships — this provides context for writing the implementation.
16
+
17
+ 4. Write the implementation code:
18
+ - The code should replace the `raise NotImplementedError(...)` line
19
+ - Keep the same indentation level (8 spaces)
20
+ - Use `self._db` for database operations
21
+ - Return a dict with the result
22
+
23
+ 5. Call `forge_implement_stub` with the service name, method name, and implementation code.
24
+
25
+ 6. Call `forge_validate` to verify the implementation has valid syntax.
26
+
27
+ ## Example
28
+
29
+ For a stub like `negotiate_rate_with_carrier`:
30
+ ```python
31
+ # Compare offered rate vs target
32
+ load = await self._db.get("load", load_id)
33
+ target = load.get("target_rate", 0)
34
+ offered = kwargs.get("offered_rate", 0)
35
+ if offered <= target:
36
+ return {"action": "accept", "rate": offered}
37
+ counter = target * 1.05
38
+ return {"action": "counter", "counter_rate": counter}
39
+ ```
@@ -0,0 +1,23 @@
1
+ ---
2
+ name: forge:test
3
+ description: Validate and test a generated forge system
4
+ ---
5
+
6
+ Use the forge MCP server to validate generated files.
7
+
8
+ ## Steps
9
+
10
+ 1. Call `forge_validate` with the output directory
11
+ - Checks: required files exist, Python syntax valid, profile.json valid, YAML files present
12
+
13
+ 2. If there are errors, show them and suggest fixes.
14
+
15
+ 3. For deeper testing:
16
+ - Read the generated pipeline YAML files and check they reference valid services
17
+ - Read the system.yaml and verify state machines and processes are consistent
18
+ - Check that all entity states referenced in transitions are declared
19
+
20
+ ## Notes
21
+
22
+ - `forge_validate` does not execute pipelines — it only checks file structure and syntax
23
+ - For runtime testing, the user needs to install the generated system and run it with hexDAG
@@ -0,0 +1,21 @@
1
+ """Django admin registration for {{ profile.business_name }}.
2
+
3
+ Auto-generated by hexdag-forge.
4
+ """
5
+
6
+ from django.contrib import admin
7
+
8
+ from models.models import (
9
+ {% for entity in profile.entities %}
10
+ {{ entity.name | capitalize }},
11
+ {% endfor %}
12
+ )
13
+
14
+ {% for entity in profile.entities %}
15
+
16
+ @admin.register({{ entity.name | capitalize }})
17
+ class {{ entity.name | capitalize }}Admin(admin.ModelAdmin):
18
+ list_display = ("__str__", "status"{% for field in entity.fields[:3] %}, "{{ field.name }}"{% endfor %}, "updated_at")
19
+ list_filter = ("status",)
20
+ search_fields = ({% for field in entity.fields[:3] %}"{{ field.name }}", {% endfor %})
21
+ {% endfor %}
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env python
2
+ """Django management script for {{ profile.business_name }}.
3
+
4
+ Auto-generated by hexdag-forge.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+
10
+
11
+ def main() -> None:
12
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
13
+ from django.core.management import execute_from_command_line
14
+
15
+ execute_from_command_line(sys.argv)
16
+
17
+
18
+ if __name__ == "__main__":
19
+ main()
@@ -0,0 +1,62 @@
1
+ """Generated Django models for {{ profile.business_name }}.
2
+
3
+ Auto-generated by hexdag-forge. Modify as needed.
4
+ """
5
+
6
+ from django.db import models
7
+
8
+
9
+ {% for entity in profile.entities %}
10
+ class {{ entity.name | capitalize }}(models.Model):
11
+ """{{ entity.display_name }}{% if entity.description %} — {{ entity.description }}{% endif %}"""
12
+
13
+ {% for field in entity.fields %}
14
+ {% if field.type == "str" %}
15
+ {{ field.name }} = models.CharField(max_length=255{% if not field.required %}, blank=True, null=True{% endif %}{% if field.default is not none %}, default="{{ field.default }}"{% endif %})
16
+ {% elif field.type == "text" %}
17
+ {{ field.name }} = models.TextField({% if not field.required %}blank=True, null=True{% endif %}{% if field.default is not none %}, default="{{ field.default }}"{% endif %})
18
+ {% elif field.type == "int" %}
19
+ {{ field.name }} = models.IntegerField({% if not field.required %}null=True, blank=True{% endif %}{% if field.default is not none %}, default={{ field.default }}{% endif %})
20
+ {% elif field.type == "float" %}
21
+ {{ field.name }} = models.FloatField({% if not field.required %}null=True, blank=True{% endif %}{% if field.default is not none %}, default={{ field.default }}{% endif %})
22
+ {% elif field.type == "Decimal" %}
23
+ {{ field.name }} = models.DecimalField(max_digits=12, decimal_places=2{% if not field.required %}, null=True, blank=True{% endif %}{% if field.default is not none %}, default={{ field.default }}{% endif %})
24
+ {% elif field.type == "bool" %}
25
+ {{ field.name }} = models.BooleanField(default={% if field.default is not none %}{{ field.default }}{% else %}False{% endif %})
26
+ {% elif field.type == "datetime" %}
27
+ {{ field.name }} = models.DateTimeField({% if not field.required %}null=True, blank=True{% endif %})
28
+ {% elif field.type == "date" %}
29
+ {{ field.name }} = models.DateField({% if not field.required %}null=True, blank=True{% endif %})
30
+ {% elif field.type == "email" %}
31
+ {{ field.name }} = models.EmailField({% if not field.required %}blank=True, null=True{% endif %})
32
+ {% elif field.type == "url" %}
33
+ {{ field.name }} = models.URLField({% if not field.required %}blank=True, null=True{% endif %})
34
+ {% elif field.type == "json" %}
35
+ {{ field.name }} = models.JSONField(default=dict{% if not field.required %}, blank=True, null=True{% endif %})
36
+ {% else %}
37
+ {{ field.name }} = models.CharField(max_length=255{% if not field.required %}, blank=True, null=True{% endif %})
38
+ {% endif %}
39
+ {% endfor %}
40
+ {% for rel in entity.relationships %}
41
+ {% if rel.kind == "belongs_to" %}
42
+ {{ rel.field_name }} = models.ForeignKey("{{ rel.target | capitalize }}", on_delete=models.SET_NULL, null=True, blank=True, related_name="{{ entity.name }}_set")
43
+ {% endif %}
44
+ {% endfor %}
45
+
46
+ STATUS_CHOICES = [
47
+ {% for state in entity.states %}
48
+ ("{{ state }}", "{{ state }}"),
49
+ {% endfor %}
50
+ ]
51
+ status = models.CharField(max_length=50, choices=STATUS_CHOICES, default="{{ entity.initial_state }}")
52
+ created_at = models.DateTimeField(auto_now_add=True)
53
+ updated_at = models.DateTimeField(auto_now=True)
54
+
55
+ class Meta:
56
+ ordering = ["-updated_at"]
57
+
58
+ def __str__(self) -> str:
59
+ return f"{{ entity.display_name }} #{ '{' }self.pk{ '}' } [{ '{' }self.status{ '}' }]"
60
+
61
+
62
+ {% endfor %}
@@ -0,0 +1,63 @@
1
+ """Django settings for {{ profile.business_name }}.
2
+
3
+ Auto-generated by hexdag-forge. Modify as needed.
4
+ """
5
+
6
+ import os
7
+ from pathlib import Path
8
+
9
+ BASE_DIR = Path(__file__).resolve().parent
10
+
11
+ SECRET_KEY = os.environ.get("DJANGO_SECRET_KEY", "change-me-in-production")
12
+ DEBUG = os.environ.get("DJANGO_DEBUG", "True").lower() in ("true", "1")
13
+ ALLOWED_HOSTS = os.environ.get("DJANGO_ALLOWED_HOSTS", "*").split(",")
14
+
15
+ INSTALLED_APPS = [
16
+ "django.contrib.admin",
17
+ "django.contrib.auth",
18
+ "django.contrib.contenttypes",
19
+ "django.contrib.sessions",
20
+ "django.contrib.messages",
21
+ "django.contrib.staticfiles",
22
+ ]
23
+
24
+ MIDDLEWARE = [
25
+ "django.middleware.security.SecurityMiddleware",
26
+ "django.contrib.sessions.middleware.SessionMiddleware",
27
+ "django.middleware.common.CommonMiddleware",
28
+ "django.middleware.csrf.CsrfViewMiddleware",
29
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
30
+ "django.contrib.messages.middleware.MessageMiddleware",
31
+ ]
32
+
33
+ ROOT_URLCONF = "urls"
34
+
35
+ TEMPLATES = [
36
+ {
37
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
38
+ "DIRS": [],
39
+ "APP_DIRS": True,
40
+ "OPTIONS": {
41
+ "context_processors": [
42
+ "django.template.context_processors.debug",
43
+ "django.template.context_processors.request",
44
+ "django.contrib.auth.context_processors.auth",
45
+ "django.contrib.messages.context_processors.messages",
46
+ ],
47
+ },
48
+ },
49
+ ]
50
+
51
+ DATABASES = {
52
+ "default": {
53
+ "ENGINE": "django.db.backends.sqlite3",
54
+ "NAME": BASE_DIR / "db.sqlite3",
55
+ }
56
+ }
57
+
58
+ LANGUAGE_CODE = "en-us"
59
+ TIME_ZONE = "UTC"
60
+ USE_I18N = True
61
+ USE_TZ = True
62
+ STATIC_URL = "static/"
63
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
@@ -0,0 +1,11 @@
1
+ """URL configuration for {{ profile.business_name }}.
2
+
3
+ Auto-generated by hexdag-forge.
4
+ """
5
+
6
+ from django.contrib import admin
7
+ from django.urls import path
8
+
9
+ urlpatterns = [
10
+ path("admin/", admin.site.urls),
11
+ ]
@@ -0,0 +1,40 @@
1
+ apiVersion: hexdag/v1
2
+ kind: Pipeline
3
+ metadata:
4
+ name: {{ entity.name }}-lifecycle
5
+ description: "Lifecycle pipeline for {{ entity.display_name }}"
6
+
7
+ spec:
8
+ services:
9
+ db:
10
+ class: "services.{{ entity.name | capitalize }}Service"
11
+
12
+ nodes:
13
+ - kind: service_call_node
14
+ metadata:
15
+ name: get_{{ entity.name }}
16
+ spec:
17
+ service: db
18
+ method: get_{{ entity.name }}
19
+ input_mapping:
20
+ {{ entity.name }}_id: "$input.{{ entity.name }}_id"
21
+
22
+ - kind: expression_node
23
+ metadata:
24
+ name: validate_transition
25
+ spec:
26
+ expressions:
27
+ current_state: "get_{{ entity.name }}.status"
28
+ target_state: "$input.target_state"
29
+ output_fields: [current_state, target_state]
30
+
31
+ - kind: service_call_node
32
+ metadata:
33
+ name: do_transition
34
+ spec:
35
+ service: db
36
+ method: transition_{{ entity.name }}
37
+ input_mapping:
38
+ {{ entity.name }}_id: "$input.{{ entity.name }}_id"
39
+ to_state: "validate_transition.target_state"
40
+ reason: "$input.reason"
@@ -0,0 +1,97 @@
1
+ """Generated service for {{ entity.display_name }} entity.
2
+
3
+ Auto-generated by hexdag-forge. CRUD methods are fully implemented.
4
+ Domain-specific stubs (marked with NotImplementedError) should be filled
5
+ in by a developer or the builder agent.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any
11
+
12
+ from hexdag.kernel.service import Service, step, tool
13
+
14
+
15
+ class {{ entity.name | capitalize }}Service(Service):
16
+ """Service for {{ entity.display_name }} operations.
17
+
18
+ {{ entity.description }}
19
+ """
20
+
21
+ def __init__(self, db: Any) -> None:
22
+ self._db = db
23
+
24
+ # ── CRUD (fully implemented) ──────────────────────────────
25
+
26
+ @tool
27
+ @step
28
+ async def create_{{ entity.name }}(self{% for field in entity.fields %}, {{ field.name }}: {{ field.type }}{% if not field.required %} | None = None{% endif %}{% endfor %}) -> dict:
29
+ """Create a new {{ entity.display_name }}.
30
+
31
+ Returns:
32
+ Dict with the created record ID and initial state.
33
+ """
34
+ data = {
35
+ {% for field in entity.fields %}
36
+ "{{ field.name }}": {{ field.name }},
37
+ {% endfor %}
38
+ }
39
+ record = await self._db.create("{{ entity.name }}", data, initial_state="{{ entity.initial_state }}")
40
+ return record
41
+
42
+ @tool
43
+ @step
44
+ async def get_{{ entity.name }}(self, {{ entity.name }}_id: str) -> dict:
45
+ """Get {{ entity.display_name }} by ID."""
46
+ return await self._db.get("{{ entity.name }}", {{ entity.name }}_id)
47
+
48
+ @tool
49
+ @step
50
+ async def list_{{ entity.name }}s(self, status: str | None = None, limit: int = 50) -> list[dict]:
51
+ """List {{ entity.display_name }} records, optionally filtered by status."""
52
+ return await self._db.list_all("{{ entity.name }}", status=status, limit=limit)
53
+
54
+ @tool
55
+ @step
56
+ async def update_{{ entity.name }}(self, {{ entity.name }}_id: str, **data: Any) -> dict:
57
+ """Update {{ entity.display_name }} fields."""
58
+ return await self._db.update("{{ entity.name }}", {{ entity.name }}_id, data)
59
+
60
+ @tool
61
+ @step
62
+ async def delete_{{ entity.name }}(self, {{ entity.name }}_id: str) -> dict:
63
+ """Delete {{ entity.display_name }} by ID."""
64
+ return await self._db.delete("{{ entity.name }}", {{ entity.name }}_id)
65
+
66
+ @tool
67
+ @step
68
+ async def transition_{{ entity.name }}(self, {{ entity.name }}_id: str, to_state: str, reason: str = "") -> dict:
69
+ """Transition {{ entity.display_name }} to a new state.
70
+
71
+ Valid transitions from each state:
72
+ {% for from_state, to_states in entity.transitions.items() %}
73
+ {{ from_state }} → {{ to_states | join(", ") }}
74
+ {% endfor %}
75
+ """
76
+ return await self._db.transition("{{ entity.name }}", {{ entity.name }}_id, to_state, reason=reason)
77
+
78
+ {% for workflow in workflows %}
79
+ # ── Domain stub: {{ workflow }} ───────────────────────────
80
+
81
+ @step
82
+ async def {{ workflow | replace(" ", "_") | lower }}(self, {{ entity.name }}_id: str, **kwargs: Any) -> dict:
83
+ """{{ workflow }}.
84
+
85
+ TODO: Implement this method with actual business logic.
86
+ The builder agent or a developer should fill this in.
87
+
88
+ Args:
89
+ {{ entity.name }}_id: The {{ entity.display_name }} ID.
90
+ **kwargs: Additional parameters.
91
+
92
+ Returns:
93
+ Dict with operation result.
94
+ """
95
+ raise NotImplementedError("Implement: {{ workflow }}")
96
+
97
+ {% endfor %}