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.
- hexdag_forge/__init__.py +3 -0
- hexdag_forge/cli.py +71 -0
- hexdag_forge/domain/__init__.py +20 -0
- hexdag_forge/domain/business_profile.py +100 -0
- hexdag_forge/domain/enums.py +29 -0
- hexdag_forge/domain/generated_system.py +23 -0
- hexdag_forge/generator/__init__.py +1 -0
- hexdag_forge/generator/assembler.py +121 -0
- hexdag_forge/generator/business_analyzer.py +161 -0
- hexdag_forge/generator/django_generator.py +27 -0
- hexdag_forge/generator/entity_generator.py +18 -0
- hexdag_forge/generator/pipeline_generator.py +24 -0
- hexdag_forge/generator/service_generator.py +55 -0
- hexdag_forge/generator/system_generator.py +21 -0
- hexdag_forge/generator/template_engine.py +27 -0
- hexdag_forge/mcp/__init__.py +0 -0
- hexdag_forge/mcp/__main__.py +5 -0
- hexdag_forge/mcp/server.py +180 -0
- hexdag_forge/skills/forge.md +34 -0
- hexdag_forge/skills/forge_entity.md +25 -0
- hexdag_forge/skills/forge_implement.md +39 -0
- hexdag_forge/skills/forge_test.md +23 -0
- hexdag_forge/templates/django_admin.py.j2 +21 -0
- hexdag_forge/templates/django_manage.py.j2 +19 -0
- hexdag_forge/templates/django_model.py.j2 +62 -0
- hexdag_forge/templates/django_settings.py.j2 +63 -0
- hexdag_forge/templates/django_urls.py.j2 +11 -0
- hexdag_forge/templates/pipeline.yaml.j2 +40 -0
- hexdag_forge/templates/service.py.j2 +97 -0
- hexdag_forge/templates/state_machines.py.j2 +27 -0
- hexdag_forge/templates/system.yaml.j2 +45 -0
- hexdag_forge-0.2.0.dev2.dist-info/METADATA +101 -0
- hexdag_forge-0.2.0.dev2.dist-info/RECORD +36 -0
- hexdag_forge-0.2.0.dev2.dist-info/WHEEL +4 -0
- hexdag_forge-0.2.0.dev2.dist-info/entry_points.txt +2 -0
- 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,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,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 %}
|