hexdag-forge 0.2.0.dev2__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.
- hexdag_forge-0.2.0.dev2/.gitignore +102 -0
- hexdag_forge-0.2.0.dev2/LICENSE +17 -0
- hexdag_forge-0.2.0.dev2/PKG-INFO +101 -0
- hexdag_forge-0.2.0.dev2/README.md +67 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/__init__.py +3 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/cli.py +71 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/domain/__init__.py +20 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/domain/business_profile.py +100 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/domain/enums.py +29 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/domain/generated_system.py +23 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/__init__.py +1 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/assembler.py +121 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/business_analyzer.py +161 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/django_generator.py +27 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/entity_generator.py +18 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/pipeline_generator.py +24 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/service_generator.py +55 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/system_generator.py +21 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/generator/template_engine.py +27 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/mcp/__init__.py +0 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/mcp/__main__.py +5 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/mcp/server.py +180 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/skills/forge.md +34 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/skills/forge_entity.md +25 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/skills/forge_implement.md +39 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/skills/forge_test.md +23 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_admin.py.j2 +21 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_manage.py.j2 +19 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_model.py.j2 +62 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_settings.py.j2 +63 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_urls.py.j2 +11 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/pipeline.yaml.j2 +40 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/service.py.j2 +97 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/state_machines.py.j2 +27 -0
- hexdag_forge-0.2.0.dev2/hexdag_forge/templates/system.yaml.j2 +45 -0
- hexdag_forge-0.2.0.dev2/pyproject.toml +98 -0
- hexdag_forge-0.2.0.dev2/tests/__init__.py +0 -0
- hexdag_forge-0.2.0.dev2/tests/test_domain.py +115 -0
- hexdag_forge-0.2.0.dev2/tests/test_generator/__init__.py +0 -0
- hexdag_forge-0.2.0.dev2/tests/test_generator/test_assembler.py +166 -0
- hexdag_forge-0.2.0.dev2/tests/test_generator/test_business_analyzer.py +35 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
/lib/
|
|
14
|
+
/lib64/
|
|
15
|
+
parts/
|
|
16
|
+
sdist/
|
|
17
|
+
var/
|
|
18
|
+
wheels/
|
|
19
|
+
*.egg-info/
|
|
20
|
+
.installed.cfg
|
|
21
|
+
*.egg
|
|
22
|
+
MANIFEST
|
|
23
|
+
|
|
24
|
+
# Virtual environments
|
|
25
|
+
.env
|
|
26
|
+
.venv
|
|
27
|
+
env/
|
|
28
|
+
venv/
|
|
29
|
+
ENV/
|
|
30
|
+
env.bak/
|
|
31
|
+
venv.bak/
|
|
32
|
+
|
|
33
|
+
# IDEs
|
|
34
|
+
.vscode/
|
|
35
|
+
.idea/
|
|
36
|
+
*.swp
|
|
37
|
+
*.swo
|
|
38
|
+
*~
|
|
39
|
+
|
|
40
|
+
# Testing
|
|
41
|
+
.coverage
|
|
42
|
+
.pytest_cache/
|
|
43
|
+
.tox/
|
|
44
|
+
coverage.xml
|
|
45
|
+
*.cover
|
|
46
|
+
.hypothesis/
|
|
47
|
+
.mypy_cache/
|
|
48
|
+
.dmypy.json
|
|
49
|
+
dmypy.json
|
|
50
|
+
|
|
51
|
+
# OS
|
|
52
|
+
.DS_Store
|
|
53
|
+
Thumbs.db
|
|
54
|
+
|
|
55
|
+
# Logs
|
|
56
|
+
*.log
|
|
57
|
+
logs/
|
|
58
|
+
|
|
59
|
+
# Database
|
|
60
|
+
*.db
|
|
61
|
+
*.sqlite3
|
|
62
|
+
|
|
63
|
+
# Jupyter
|
|
64
|
+
.ipynb_checkpoints/
|
|
65
|
+
|
|
66
|
+
# Environment variables
|
|
67
|
+
.env.*
|
|
68
|
+
!.env.example
|
|
69
|
+
|
|
70
|
+
# uv
|
|
71
|
+
uv.lock
|
|
72
|
+
|
|
73
|
+
# Development
|
|
74
|
+
.pre-commit-config.yaml.bak
|
|
75
|
+
outputs/
|
|
76
|
+
sbom.json
|
|
77
|
+
|
|
78
|
+
# MkDocs
|
|
79
|
+
site/
|
|
80
|
+
docs/reference/*.md
|
|
81
|
+
docs/namespaces/*.md
|
|
82
|
+
|
|
83
|
+
# HexDAG config (only in root, not in subdirectories or examples)
|
|
84
|
+
/hexdag.toml
|
|
85
|
+
|
|
86
|
+
# Plugin build artifacts
|
|
87
|
+
hexdag_plugins/*/.ruff_cache/
|
|
88
|
+
hexdag_plugins/*/__pycache__/
|
|
89
|
+
hexdag_plugins/*/.mypy_cache/
|
|
90
|
+
hexdag_plugins/*/.pytest_cache/
|
|
91
|
+
hexdag_plugins/*/dist/
|
|
92
|
+
hexdag_plugins/*/build/
|
|
93
|
+
hexdag_plugins/*/*.egg-info/
|
|
94
|
+
|
|
95
|
+
# Studio UI (React/Vite)
|
|
96
|
+
hexdag/studio/ui/node_modules/
|
|
97
|
+
hexdag/studio/ui/dist/
|
|
98
|
+
hexdag/studio/ui/.vite/
|
|
99
|
+
|
|
100
|
+
# hexdag-studio standalone package (UI)
|
|
101
|
+
hexdag-studio/ui/node_modules/
|
|
102
|
+
hexdag-studio/ui/.vite/
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2024-2026 Omniviser
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hexdag-forge
|
|
3
|
+
Version: 0.2.0.dev2
|
|
4
|
+
Summary: Generate operational systems from prompts — code generator powered by hexDAG
|
|
5
|
+
Project-URL: Homepage, https://hexdag.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/omniviser/hexdag
|
|
7
|
+
Project-URL: Documentation, https://hexdag.ai/docs/forge
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/omniviser/hexdag/issues
|
|
9
|
+
Author-email: hexDAG Team <developers@omniviser.ai>
|
|
10
|
+
License: Apache-2.0
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,code-generation,crm,erp,forge,hexdag,operational-systems,tms
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Office/Business
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Requires-Dist: hexdag>=0.7.0
|
|
22
|
+
Requires-Dist: jinja2>=3.1.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Requires-Dist: typer>=0.9.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
31
|
+
Provides-Extra: mcp
|
|
32
|
+
Requires-Dist: mcp>=1.0.0; extra == 'mcp'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# hexDAG Forge
|
|
36
|
+
|
|
37
|
+
**Generate operational systems from prompts.** Describe your business → get a complete, runnable system with YAML pipelines, Python services, Django models, and a `kind: System` manifest.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install hexdag-forge
|
|
41
|
+
|
|
42
|
+
hexdag-forge generate "I run a freight brokerage that matches shippers with carriers"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Output:
|
|
46
|
+
```
|
|
47
|
+
./generated/
|
|
48
|
+
├── system.yaml # kind: System manifest (LifecycleRunner)
|
|
49
|
+
├── pipelines/
|
|
50
|
+
│ ├── load_lifecycle.yaml # Pipeline per entity lifecycle
|
|
51
|
+
│ └── escalation.yaml
|
|
52
|
+
├── services/
|
|
53
|
+
│ └── freight_service.py # @tool/@step CRUD + domain stubs
|
|
54
|
+
├── models/
|
|
55
|
+
│ └── models.py # Django models
|
|
56
|
+
├── state_machines.py # StateMachineConfig definitions
|
|
57
|
+
├── manage.py # Django manage.py
|
|
58
|
+
├── settings.py # Django settings
|
|
59
|
+
├── urls.py # URL config
|
|
60
|
+
├── admin.py # Admin registration
|
|
61
|
+
└── profile.json # BusinessProfile (for refinement)
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## What it generates
|
|
65
|
+
|
|
66
|
+
- **YAML Pipelines** — entity lifecycle workflows using hexDAG nodes
|
|
67
|
+
- **Python Services** — `@tool`/`@step` CRUD methods (implemented) + domain-specific stubs (`NotImplementedError`) for the builder agent to fill
|
|
68
|
+
- **Django Models** — proper ORM models with fields, relationships, state choices
|
|
69
|
+
- **System Manifest** — `kind: System` with state machines, `on_enter` process mappings, terminal states
|
|
70
|
+
- **State Machines** — `StateMachineConfig` definitions with validated transitions
|
|
71
|
+
|
|
72
|
+
## CLI
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Generate from prompt
|
|
76
|
+
hexdag-forge generate "I run a freight brokerage" --output ./my-erp/
|
|
77
|
+
|
|
78
|
+
# Refine existing system
|
|
79
|
+
hexdag-forge refine ./my-erp/ "Add an Escalation entity with severity field"
|
|
80
|
+
|
|
81
|
+
# Validate generated files
|
|
82
|
+
hexdag-forge validate ./my-erp/
|
|
83
|
+
|
|
84
|
+
# MCP server for Claude Code
|
|
85
|
+
hexdag-forge mcp serve
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## MCP + Claude Code
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
hexdag-forge mcp serve
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Skills:
|
|
95
|
+
- `/forge "I run a freight brokerage"` — generate system
|
|
96
|
+
- `/forge:entity "add Carrier with mc_number, score"` — add entity
|
|
97
|
+
- `/forge:implement "fill in negotiate_rate"` — implement stub
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
Apache 2.0
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# hexDAG Forge
|
|
2
|
+
|
|
3
|
+
**Generate operational systems from prompts.** Describe your business → get a complete, runnable system with YAML pipelines, Python services, Django models, and a `kind: System` manifest.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install hexdag-forge
|
|
7
|
+
|
|
8
|
+
hexdag-forge generate "I run a freight brokerage that matches shippers with carriers"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Output:
|
|
12
|
+
```
|
|
13
|
+
./generated/
|
|
14
|
+
├── system.yaml # kind: System manifest (LifecycleRunner)
|
|
15
|
+
├── pipelines/
|
|
16
|
+
│ ├── load_lifecycle.yaml # Pipeline per entity lifecycle
|
|
17
|
+
│ └── escalation.yaml
|
|
18
|
+
├── services/
|
|
19
|
+
│ └── freight_service.py # @tool/@step CRUD + domain stubs
|
|
20
|
+
├── models/
|
|
21
|
+
│ └── models.py # Django models
|
|
22
|
+
├── state_machines.py # StateMachineConfig definitions
|
|
23
|
+
├── manage.py # Django manage.py
|
|
24
|
+
├── settings.py # Django settings
|
|
25
|
+
├── urls.py # URL config
|
|
26
|
+
├── admin.py # Admin registration
|
|
27
|
+
└── profile.json # BusinessProfile (for refinement)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## What it generates
|
|
31
|
+
|
|
32
|
+
- **YAML Pipelines** — entity lifecycle workflows using hexDAG nodes
|
|
33
|
+
- **Python Services** — `@tool`/`@step` CRUD methods (implemented) + domain-specific stubs (`NotImplementedError`) for the builder agent to fill
|
|
34
|
+
- **Django Models** — proper ORM models with fields, relationships, state choices
|
|
35
|
+
- **System Manifest** — `kind: System` with state machines, `on_enter` process mappings, terminal states
|
|
36
|
+
- **State Machines** — `StateMachineConfig` definitions with validated transitions
|
|
37
|
+
|
|
38
|
+
## CLI
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Generate from prompt
|
|
42
|
+
hexdag-forge generate "I run a freight brokerage" --output ./my-erp/
|
|
43
|
+
|
|
44
|
+
# Refine existing system
|
|
45
|
+
hexdag-forge refine ./my-erp/ "Add an Escalation entity with severity field"
|
|
46
|
+
|
|
47
|
+
# Validate generated files
|
|
48
|
+
hexdag-forge validate ./my-erp/
|
|
49
|
+
|
|
50
|
+
# MCP server for Claude Code
|
|
51
|
+
hexdag-forge mcp serve
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## MCP + Claude Code
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
hexdag-forge mcp serve
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Skills:
|
|
61
|
+
- `/forge "I run a freight brokerage"` — generate system
|
|
62
|
+
- `/forge:entity "add Carrier with mc_number, score"` — add entity
|
|
63
|
+
- `/forge:implement "fill in negotiate_rate"` — implement stub
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
Apache 2.0
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""CLI entry point for hexdag-forge — a code generator for operational systems."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path # noqa: TC003 — needed at runtime by typer
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
name="hexdag-forge",
|
|
12
|
+
help="Generate operational systems from prompts — powered by hexDAG",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
mcp_app = typer.Typer(help="MCP server commands")
|
|
17
|
+
app.add_typer(mcp_app, name="mcp")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command()
|
|
21
|
+
def generate(
|
|
22
|
+
description: Annotated[str, typer.Argument(help="Business description")],
|
|
23
|
+
output: Annotated[
|
|
24
|
+
Path, typer.Option(help="Output directory for generated files")
|
|
25
|
+
] = "./generated",
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Generate an operational system from a business description."""
|
|
28
|
+
from hexdag_forge.generator.assembler import assemble
|
|
29
|
+
from hexdag_forge.generator.business_analyzer import create_default_profile
|
|
30
|
+
|
|
31
|
+
profile = create_default_profile(description)
|
|
32
|
+
result = assemble(profile, output)
|
|
33
|
+
typer.echo(f"Generated system in {output}/")
|
|
34
|
+
typer.echo(f" Entities: {', '.join(e.name for e in result.profile.entities)}")
|
|
35
|
+
typer.echo(f" Pipelines: {len(result.pipelines)} files")
|
|
36
|
+
typer.echo(f" Services: {len(result.services)} files")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@app.command()
|
|
40
|
+
def validate(
|
|
41
|
+
output: Annotated[Path, typer.Argument(help="Path to generated system")],
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Validate generated files (YAML pipelines, Python syntax, system manifest)."""
|
|
44
|
+
from hexdag_forge.generator.assembler import validate_system
|
|
45
|
+
|
|
46
|
+
errors = validate_system(output_dir=output)
|
|
47
|
+
if errors:
|
|
48
|
+
for err in errors:
|
|
49
|
+
typer.echo(f" ERROR: {err}", err=True)
|
|
50
|
+
raise typer.Exit(1)
|
|
51
|
+
typer.echo("All generated files are valid.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@mcp_app.command("serve")
|
|
55
|
+
def mcp_serve(
|
|
56
|
+
transport: Annotated[str, typer.Option(help="Transport: stdio or sse")] = "stdio",
|
|
57
|
+
port: Annotated[int, typer.Option(help="Port for SSE transport")] = 3001,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Start the forge MCP server for Claude Code integration."""
|
|
60
|
+
from hexdag_forge.mcp.server import run_server
|
|
61
|
+
|
|
62
|
+
run_server(transport=transport, port=port)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main() -> None:
|
|
66
|
+
"""Entry point."""
|
|
67
|
+
app()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Domain models for hexdag-forge."""
|
|
2
|
+
|
|
3
|
+
from hexdag_forge.domain.business_profile import (
|
|
4
|
+
BusinessProfile,
|
|
5
|
+
EntitySpec,
|
|
6
|
+
FieldSpec,
|
|
7
|
+
RelationshipSpec,
|
|
8
|
+
)
|
|
9
|
+
from hexdag_forge.domain.enums import FieldType, RelationshipKind
|
|
10
|
+
from hexdag_forge.domain.generated_system import GeneratedSystem
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BusinessProfile",
|
|
14
|
+
"EntitySpec",
|
|
15
|
+
"FieldSpec",
|
|
16
|
+
"FieldType",
|
|
17
|
+
"GeneratedSystem",
|
|
18
|
+
"RelationshipKind",
|
|
19
|
+
"RelationshipSpec",
|
|
20
|
+
]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Domain models for business profiles and entity specifications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, field_validator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FieldSpec(BaseModel):
|
|
11
|
+
"""Specification for a single field on an entity."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
type: str = "str"
|
|
15
|
+
required: bool = True
|
|
16
|
+
default: Any = None
|
|
17
|
+
description: str = ""
|
|
18
|
+
|
|
19
|
+
@field_validator("name")
|
|
20
|
+
@classmethod
|
|
21
|
+
def validate_name(cls, v: str) -> str:
|
|
22
|
+
if not v.isidentifier():
|
|
23
|
+
msg = f"Field name must be a valid Python identifier, got: {v!r}"
|
|
24
|
+
raise ValueError(msg)
|
|
25
|
+
return v
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RelationshipSpec(BaseModel):
|
|
29
|
+
"""Specification for a relationship between entities."""
|
|
30
|
+
|
|
31
|
+
target: str
|
|
32
|
+
kind: str = "belongs_to"
|
|
33
|
+
field_name: str
|
|
34
|
+
description: str = ""
|
|
35
|
+
|
|
36
|
+
@field_validator("kind")
|
|
37
|
+
@classmethod
|
|
38
|
+
def validate_kind(cls, v: str) -> str:
|
|
39
|
+
valid = {"belongs_to", "has_many", "has_one"}
|
|
40
|
+
if v not in valid:
|
|
41
|
+
msg = f"Relationship kind must be one of {valid}, got: {v!r}"
|
|
42
|
+
raise ValueError(msg)
|
|
43
|
+
return v
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class EntitySpec(BaseModel):
|
|
47
|
+
"""Specification for an entity type in the generated system."""
|
|
48
|
+
|
|
49
|
+
name: str
|
|
50
|
+
display_name: str
|
|
51
|
+
fields: list[FieldSpec]
|
|
52
|
+
relationships: list[RelationshipSpec] = []
|
|
53
|
+
states: list[str]
|
|
54
|
+
initial_state: str
|
|
55
|
+
transitions: dict[str, list[str]]
|
|
56
|
+
description: str = ""
|
|
57
|
+
|
|
58
|
+
@field_validator("name")
|
|
59
|
+
@classmethod
|
|
60
|
+
def validate_name(cls, v: str) -> str:
|
|
61
|
+
if not v.isidentifier():
|
|
62
|
+
msg = f"Entity name must be a valid Python identifier, got: {v!r}"
|
|
63
|
+
raise ValueError(msg)
|
|
64
|
+
return v
|
|
65
|
+
|
|
66
|
+
@field_validator("initial_state")
|
|
67
|
+
@classmethod
|
|
68
|
+
def validate_initial_state(cls, v: str, info: Any) -> str:
|
|
69
|
+
states = info.data.get("states", [])
|
|
70
|
+
if states and v not in states:
|
|
71
|
+
msg = f"Initial state {v!r} must be one of the declared states: {states}"
|
|
72
|
+
raise ValueError(msg)
|
|
73
|
+
return v
|
|
74
|
+
|
|
75
|
+
@field_validator("transitions")
|
|
76
|
+
@classmethod
|
|
77
|
+
def validate_transitions(cls, v: dict[str, list[str]], info: Any) -> dict[str, list[str]]:
|
|
78
|
+
states = set(info.data.get("states", []))
|
|
79
|
+
if not states:
|
|
80
|
+
return v
|
|
81
|
+
for from_state, to_states in v.items():
|
|
82
|
+
if from_state not in states:
|
|
83
|
+
msg = f"Transition source state {from_state!r} not in declared states"
|
|
84
|
+
raise ValueError(msg)
|
|
85
|
+
for to_state in to_states:
|
|
86
|
+
if to_state not in states:
|
|
87
|
+
msg = f"Transition target state {to_state!r} not in declared states"
|
|
88
|
+
raise ValueError(msg)
|
|
89
|
+
return v
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class BusinessProfile(BaseModel):
|
|
93
|
+
"""Complete business profile extracted from a natural language description."""
|
|
94
|
+
|
|
95
|
+
business_name: str
|
|
96
|
+
business_type: str
|
|
97
|
+
description: str
|
|
98
|
+
entities: list[EntitySpec]
|
|
99
|
+
workflows: list[str] = []
|
|
100
|
+
modules: list[str] = []
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Enums for hexdag-forge domain models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FieldType(StrEnum):
|
|
9
|
+
"""Supported field types for entity fields."""
|
|
10
|
+
|
|
11
|
+
STR = "str"
|
|
12
|
+
INT = "int"
|
|
13
|
+
FLOAT = "float"
|
|
14
|
+
DECIMAL = "Decimal"
|
|
15
|
+
BOOL = "bool"
|
|
16
|
+
DATETIME = "datetime"
|
|
17
|
+
DATE = "date"
|
|
18
|
+
TEXT = "text"
|
|
19
|
+
EMAIL = "email"
|
|
20
|
+
URL = "url"
|
|
21
|
+
JSON = "json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RelationshipKind(StrEnum):
|
|
25
|
+
"""Types of entity relationships."""
|
|
26
|
+
|
|
27
|
+
BELONGS_TO = "belongs_to"
|
|
28
|
+
HAS_MANY = "has_many"
|
|
29
|
+
HAS_ONE = "has_one"
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Domain model for a fully generated operational system."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from hexdag_forge.domain.business_profile import BusinessProfile # noqa: TC001
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class GeneratedSystem(BaseModel):
|
|
11
|
+
"""The complete output artifact of the forge generation process.
|
|
12
|
+
|
|
13
|
+
Contains the business profile that was analyzed, plus all generated
|
|
14
|
+
artifacts: YAML pipelines, Python service source code, Django model
|
|
15
|
+
source (for export), state machine configs, and the system manifest.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
profile: BusinessProfile
|
|
19
|
+
pipelines: dict[str, str] = {}
|
|
20
|
+
services: dict[str, str] = {}
|
|
21
|
+
models_source: dict[str, str] = {}
|
|
22
|
+
state_machines: dict[str, dict] = {}
|
|
23
|
+
system_yaml: str = ""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""hexdag-forge generator engine — transforms BusinessProfile into runnable code."""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Assembler — orchestrates all generators and writes output files.
|
|
2
|
+
|
|
3
|
+
This is the main entry point for code generation. It takes a BusinessProfile
|
|
4
|
+
(from LLM analysis or manual construction) and produces a complete runnable
|
|
5
|
+
system in the output directory.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from hexdag_forge.domain.business_profile import BusinessProfile
|
|
14
|
+
from hexdag_forge.domain.generated_system import GeneratedSystem
|
|
15
|
+
from hexdag_forge.generator.django_generator import generate_django
|
|
16
|
+
from hexdag_forge.generator.pipeline_generator import generate_pipelines
|
|
17
|
+
from hexdag_forge.generator.service_generator import generate_services
|
|
18
|
+
from hexdag_forge.generator.system_generator import generate_system
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def assemble(profile: BusinessProfile, output_dir: Path) -> GeneratedSystem:
|
|
22
|
+
"""Generate all files from a BusinessProfile and write to output_dir.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
profile: The business profile to generate from.
|
|
26
|
+
output_dir: Directory to write generated files to.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
GeneratedSystem with metadata about what was generated.
|
|
30
|
+
"""
|
|
31
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
# Generate all artifacts
|
|
34
|
+
pipelines = generate_pipelines(profile)
|
|
35
|
+
services = generate_services(profile)
|
|
36
|
+
system_yaml = generate_system(profile)
|
|
37
|
+
django_files = generate_django(profile)
|
|
38
|
+
|
|
39
|
+
# Write pipelines
|
|
40
|
+
pipelines_dir = output_dir / "pipelines"
|
|
41
|
+
pipelines_dir.mkdir(exist_ok=True)
|
|
42
|
+
for filename, content in pipelines.items():
|
|
43
|
+
(pipelines_dir / filename).write_text(content)
|
|
44
|
+
|
|
45
|
+
# Write services
|
|
46
|
+
services_dir = output_dir / "services"
|
|
47
|
+
services_dir.mkdir(exist_ok=True)
|
|
48
|
+
for filename, content in services.items():
|
|
49
|
+
(services_dir / filename).write_text(content)
|
|
50
|
+
|
|
51
|
+
# Write system manifest
|
|
52
|
+
(output_dir / "system.yaml").write_text(system_yaml)
|
|
53
|
+
|
|
54
|
+
# Write Django files
|
|
55
|
+
for rel_path, content in django_files.items():
|
|
56
|
+
file_path = output_dir / rel_path
|
|
57
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
file_path.write_text(content)
|
|
59
|
+
|
|
60
|
+
# Write models/__init__.py
|
|
61
|
+
(output_dir / "models" / "__init__.py").write_text("")
|
|
62
|
+
|
|
63
|
+
# Write profile.json for refinement
|
|
64
|
+
(output_dir / "profile.json").write_text(profile.model_dump_json(indent=2))
|
|
65
|
+
|
|
66
|
+
return GeneratedSystem(
|
|
67
|
+
profile=profile,
|
|
68
|
+
pipelines=pipelines,
|
|
69
|
+
services=services,
|
|
70
|
+
models_source={"models/models.py": django_files["models/models.py"]},
|
|
71
|
+
state_machines={e.name: e.transitions for e in profile.entities},
|
|
72
|
+
system_yaml=system_yaml,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_system(output_dir: Path) -> list[str]:
|
|
77
|
+
"""Validate all generated files in an output directory.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
List of error messages. Empty list means valid.
|
|
81
|
+
"""
|
|
82
|
+
errors: list[str] = []
|
|
83
|
+
output_dir = Path(output_dir)
|
|
84
|
+
|
|
85
|
+
# Check required files exist
|
|
86
|
+
required = ["system.yaml", "profile.json", "settings.py", "manage.py"]
|
|
87
|
+
errors.extend(f"Missing required file: {f}" for f in required if not (output_dir / f).exists())
|
|
88
|
+
|
|
89
|
+
# Check pipelines directory
|
|
90
|
+
pipelines_dir = output_dir / "pipelines"
|
|
91
|
+
if not pipelines_dir.exists():
|
|
92
|
+
errors.append("Missing pipelines/ directory")
|
|
93
|
+
elif not list(pipelines_dir.glob("*.yaml")):
|
|
94
|
+
errors.append("No YAML files in pipelines/")
|
|
95
|
+
|
|
96
|
+
# Check services directory
|
|
97
|
+
services_dir = output_dir / "services"
|
|
98
|
+
if not services_dir.exists():
|
|
99
|
+
errors.append("Missing services/ directory")
|
|
100
|
+
elif not list(services_dir.glob("*.py")):
|
|
101
|
+
errors.append("No Python files in services/")
|
|
102
|
+
|
|
103
|
+
# Validate Python syntax
|
|
104
|
+
for py_file in output_dir.rglob("*.py"):
|
|
105
|
+
try:
|
|
106
|
+
import ast
|
|
107
|
+
|
|
108
|
+
ast.parse(py_file.read_text())
|
|
109
|
+
except SyntaxError as e:
|
|
110
|
+
errors.append(f"Syntax error in {py_file.relative_to(output_dir)}: {e}")
|
|
111
|
+
|
|
112
|
+
# Validate profile.json
|
|
113
|
+
profile_path = output_dir / "profile.json"
|
|
114
|
+
if profile_path.exists():
|
|
115
|
+
try:
|
|
116
|
+
profile_data = json.loads(profile_path.read_text())
|
|
117
|
+
BusinessProfile.model_validate(profile_data)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
errors.append(f"Invalid profile.json: {e}")
|
|
120
|
+
|
|
121
|
+
return errors
|