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.
Files changed (41) hide show
  1. hexdag_forge-0.2.0.dev2/.gitignore +102 -0
  2. hexdag_forge-0.2.0.dev2/LICENSE +17 -0
  3. hexdag_forge-0.2.0.dev2/PKG-INFO +101 -0
  4. hexdag_forge-0.2.0.dev2/README.md +67 -0
  5. hexdag_forge-0.2.0.dev2/hexdag_forge/__init__.py +3 -0
  6. hexdag_forge-0.2.0.dev2/hexdag_forge/cli.py +71 -0
  7. hexdag_forge-0.2.0.dev2/hexdag_forge/domain/__init__.py +20 -0
  8. hexdag_forge-0.2.0.dev2/hexdag_forge/domain/business_profile.py +100 -0
  9. hexdag_forge-0.2.0.dev2/hexdag_forge/domain/enums.py +29 -0
  10. hexdag_forge-0.2.0.dev2/hexdag_forge/domain/generated_system.py +23 -0
  11. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/__init__.py +1 -0
  12. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/assembler.py +121 -0
  13. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/business_analyzer.py +161 -0
  14. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/django_generator.py +27 -0
  15. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/entity_generator.py +18 -0
  16. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/pipeline_generator.py +24 -0
  17. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/service_generator.py +55 -0
  18. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/system_generator.py +21 -0
  19. hexdag_forge-0.2.0.dev2/hexdag_forge/generator/template_engine.py +27 -0
  20. hexdag_forge-0.2.0.dev2/hexdag_forge/mcp/__init__.py +0 -0
  21. hexdag_forge-0.2.0.dev2/hexdag_forge/mcp/__main__.py +5 -0
  22. hexdag_forge-0.2.0.dev2/hexdag_forge/mcp/server.py +180 -0
  23. hexdag_forge-0.2.0.dev2/hexdag_forge/skills/forge.md +34 -0
  24. hexdag_forge-0.2.0.dev2/hexdag_forge/skills/forge_entity.md +25 -0
  25. hexdag_forge-0.2.0.dev2/hexdag_forge/skills/forge_implement.md +39 -0
  26. hexdag_forge-0.2.0.dev2/hexdag_forge/skills/forge_test.md +23 -0
  27. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_admin.py.j2 +21 -0
  28. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_manage.py.j2 +19 -0
  29. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_model.py.j2 +62 -0
  30. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_settings.py.j2 +63 -0
  31. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/django_urls.py.j2 +11 -0
  32. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/pipeline.yaml.j2 +40 -0
  33. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/service.py.j2 +97 -0
  34. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/state_machines.py.j2 +27 -0
  35. hexdag_forge-0.2.0.dev2/hexdag_forge/templates/system.yaml.j2 +45 -0
  36. hexdag_forge-0.2.0.dev2/pyproject.toml +98 -0
  37. hexdag_forge-0.2.0.dev2/tests/__init__.py +0 -0
  38. hexdag_forge-0.2.0.dev2/tests/test_domain.py +115 -0
  39. hexdag_forge-0.2.0.dev2/tests/test_generator/__init__.py +0 -0
  40. hexdag_forge-0.2.0.dev2/tests/test_generator/test_assembler.py +166 -0
  41. 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,3 @@
1
+ """hexdag-forge — Build operational systems from prompts."""
2
+
3
+ __version__ = "0.1.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