forge-dev 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,264 @@
1
+ """Context Phase — resolves the full project context.
2
+
3
+ Takes detection results + user config + brief and produces
4
+ a ProjectContext that captures all decisions for this project.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from datetime import datetime, timezone
10
+ from pathlib import Path
11
+
12
+ import yaml
13
+
14
+ from forge_core.detector import ProjectDetection
15
+ from forge_core.models import (
16
+ AIConfig,
17
+ APIConfig,
18
+ AuthPattern,
19
+ BackendFramework,
20
+ CICDConfig,
21
+ CloudProvider,
22
+ DatabaseType,
23
+ FrontendFramework,
24
+ ObservabilityConfig,
25
+ ProjectContext,
26
+ ProjectType,
27
+ Regulatory,
28
+ StandardsConfig,
29
+ UserConfig,
30
+ )
31
+
32
+
33
+ def resolve_context(
34
+ detection: ProjectDetection,
35
+ user_config: UserConfig,
36
+ project_name: str | None = None,
37
+ overrides: dict | None = None,
38
+ ) -> ProjectContext:
39
+ """Resolve the full project context from detection + user defaults + overrides.
40
+
41
+ Priority order (highest wins):
42
+ 1. Explicit overrides passed in
43
+ 2. Detection results (what we found in the code)
44
+ 3. User config defaults
45
+
46
+ Args:
47
+ detection: What we found in the directory
48
+ user_config: Global user defaults
49
+ project_name: Override for project name
50
+ overrides: Explicit overrides for any field
51
+ """
52
+ overrides = overrides or {}
53
+
54
+ # Project name: override > directory name
55
+ name = project_name or overrides.get("name") or detection.path.name
56
+
57
+ # Backend: override > detected > user default (None means ask)
58
+ backend = _resolve_backend(detection, user_config, overrides)
59
+
60
+ # Frontend: override > detected > user default
61
+ frontend = _resolve_enum(
62
+ FrontendFramework,
63
+ overrides.get("frontend"),
64
+ detection.detected_stack.get("frontend"),
65
+ user_config.frontend,
66
+ )
67
+
68
+ context = ProjectContext(
69
+ name=name,
70
+ type=overrides.get("type", ProjectType.SAAS),
71
+ description=overrides.get("description", ""),
72
+ regulatory=_resolve_regulatory(overrides),
73
+ cloud=_resolve_enum(
74
+ CloudProvider,
75
+ overrides.get("cloud"),
76
+ None,
77
+ user_config.cloud,
78
+ ),
79
+ backend=backend,
80
+ frontend=frontend,
81
+ database=_resolve_enum(
82
+ DatabaseType,
83
+ overrides.get("database"),
84
+ None,
85
+ user_config.database,
86
+ ),
87
+ auth=_resolve_enum(
88
+ AuthPattern,
89
+ overrides.get("auth"),
90
+ None,
91
+ user_config.auth,
92
+ ),
93
+ ai=_merge_config(AIConfig, user_config.ai, overrides.get("ai", {})),
94
+ observability=_merge_config(
95
+ ObservabilityConfig, user_config.observability, overrides.get("observability", {})
96
+ ),
97
+ api=_merge_config(APIConfig, user_config.api, overrides.get("api", {})),
98
+ cicd=_merge_config(CICDConfig, user_config.cicd, overrides.get("cicd", {})),
99
+ standards=_merge_config(
100
+ StandardsConfig, user_config.standards, overrides.get("standards", {})
101
+ ),
102
+ forge_version=overrides.get("forge_version", "0.1.0"),
103
+ created_at=datetime.now(timezone.utc).isoformat(),
104
+ )
105
+
106
+ return context
107
+
108
+
109
+ def save_context(context: ProjectContext, project_path: Path) -> Path:
110
+ """Save ProjectContext to .forge/context.yaml."""
111
+ forge_dir = project_path / ".forge"
112
+ forge_dir.mkdir(parents=True, exist_ok=True)
113
+
114
+ context_path = forge_dir / "context.yaml"
115
+ with open(context_path, "w") as f:
116
+ yaml.dump(
117
+ context.model_dump(mode="json"),
118
+ f,
119
+ default_flow_style=False,
120
+ sort_keys=False,
121
+ )
122
+
123
+ return context_path
124
+
125
+
126
+ def load_context(project_path: Path) -> ProjectContext | None:
127
+ """Load an existing ProjectContext."""
128
+ context_path = project_path / ".forge" / "context.yaml"
129
+ if not context_path.exists():
130
+ return None
131
+ with open(context_path) as f:
132
+ data = yaml.safe_load(f) or {}
133
+ return ProjectContext(**data)
134
+
135
+
136
+ def build_context_prompt(detection: ProjectDetection, user_config: UserConfig) -> str:
137
+ """Build a prompt for the LLM to help resolve context interactively.
138
+
139
+ Used when forge init detects an empty directory and needs to
140
+ have a conversation with the user.
141
+ """
142
+ return f"""You are Forge, an AI development workflow engine. Help the user set up their project context.
143
+
144
+ ## Current Detection
145
+ {detection.summary()}
146
+
147
+ ## User Defaults
148
+ - Cloud: {user_config.cloud.value}
149
+ - Backend: {user_config.backend.value if user_config.backend else 'Not set (ask each time)'}
150
+ - Frontend: {user_config.frontend.value}
151
+ - Database: {user_config.database.value}
152
+ - Auth: {user_config.auth.value}
153
+ - AI enabled: {user_config.ai.enabled}
154
+ - Observability: {user_config.observability.apm} + {user_config.observability.metrics} + {user_config.observability.logs}
155
+
156
+ ## Instructions
157
+ Based on the detection and user defaults, help resolve the project context.
158
+ If the directory is empty, ask the user:
159
+ 1. What is this project? (name, description, type)
160
+ 2. Are there any regulatory requirements? (HIPAA, FERPA, etc.)
161
+ 3. Backend preference for this project? (user default is to ask each time)
162
+ 4. Any overrides to the defaults above?
163
+ 5. Does this project use AI capabilities?
164
+
165
+ If the directory has code, confirm the detected stack and ask about anything not detected.
166
+
167
+ Keep questions concise. One question at a time if in conversation mode.
168
+ Suggest defaults based on the user's preferences — they can just confirm."""
169
+
170
+
171
+ def get_questions_for_empty_project(user_config: UserConfig) -> list[dict]:
172
+ """Return the questions to ask for a brand new project."""
173
+ questions = [
174
+ {
175
+ "id": "mission",
176
+ "question": "What is this project? Give me a one-liner.",
177
+ "type": "text",
178
+ "required": True,
179
+ },
180
+ {
181
+ "id": "type",
182
+ "question": "What type of project is this?",
183
+ "type": "choice",
184
+ "options": [t.value for t in ProjectType],
185
+ "default": ProjectType.SAAS.value,
186
+ },
187
+ {
188
+ "id": "regulatory",
189
+ "question": "Any regulatory requirements?",
190
+ "type": "multi_choice",
191
+ "options": [r.value for r in Regulatory],
192
+ "default": [Regulatory.NONE.value],
193
+ },
194
+ ]
195
+
196
+ # Only ask about backend if user doesn't have a default
197
+ if user_config.backend is None:
198
+ questions.append({
199
+ "id": "backend",
200
+ "question": "Backend framework for this project?",
201
+ "type": "choice",
202
+ "options": [b.value for b in BackendFramework if b != BackendFramework.OTHER],
203
+ "default": BackendFramework.FASTAPI.value,
204
+ })
205
+
206
+ questions.append({
207
+ "id": "ai_enabled",
208
+ "question": "Does this project use AI capabilities internally?",
209
+ "type": "boolean",
210
+ "default": True,
211
+ })
212
+
213
+ return questions
214
+
215
+
216
+ # ── Private helpers ────────────────────────────────────────────────────────
217
+
218
+ def _resolve_backend(
219
+ detection: ProjectDetection, user_config: UserConfig, overrides: dict
220
+ ) -> BackendFramework:
221
+ """Resolve backend framework with priority: override > detected > user default."""
222
+ if "backend" in overrides:
223
+ return BackendFramework(overrides["backend"])
224
+ if "backend" in detection.detected_stack:
225
+ try:
226
+ return BackendFramework(detection.detected_stack["backend"])
227
+ except ValueError:
228
+ pass
229
+ if user_config.backend is not None:
230
+ return user_config.backend
231
+ # Default fallback
232
+ return BackendFramework.FASTAPI
233
+
234
+
235
+ def _resolve_enum(enum_class, override, detected, default):
236
+ """Resolve an enum value with priority: override > detected > default."""
237
+ for val in (override, detected, default):
238
+ if val is not None:
239
+ try:
240
+ return enum_class(val)
241
+ except (ValueError, KeyError):
242
+ continue
243
+ return default
244
+
245
+
246
+ def _resolve_regulatory(overrides: dict) -> list[Regulatory]:
247
+ """Resolve regulatory requirements from overrides."""
248
+ reg = overrides.get("regulatory", [])
249
+ if not reg:
250
+ return []
251
+ return [Regulatory(r) for r in reg if r != "none"]
252
+
253
+
254
+ def _merge_config(model_class, base_config, overrides):
255
+ """Merge a base config with overrides, returning a new config instance."""
256
+ if isinstance(base_config, dict):
257
+ base_data = base_config
258
+ else:
259
+ base_data = base_config.model_dump(mode="json")
260
+
261
+ if isinstance(overrides, dict):
262
+ base_data.update(overrides)
263
+
264
+ return model_class(**base_data)
@@ -0,0 +1,340 @@
1
+ """Intake Phase — transforms any requirement into a normalized Forge Brief.
2
+
3
+ This phase receives whatever the user has:
4
+ - A PRD document
5
+ - A user story or epic
6
+ - A vague conversation or Slack message
7
+ - A feature request
8
+
9
+ And produces a ForgeBrief — a normalized document that all subsequent
10
+ phases consume.
11
+
12
+ The key insight: the output isn't just "what features to build" but
13
+ "in what order should an AI coding agent implement them" — types first,
14
+ then infra, then services, then endpoints, then UI.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ import yaml
23
+
24
+ from forge_core.models import ForgeBrief, RequirementType
25
+
26
+
27
+ # ── AI-Optimized Implementation Ordering ───────────────────────────────────
28
+
29
+ AI_IMPLEMENTATION_PHASES = [
30
+ {
31
+ "phase": 1,
32
+ "name": "types_and_contracts",
33
+ "title": "Types, Schemas & Contracts",
34
+ "description": (
35
+ "Define all TypeScript/Python types, Pydantic models, interfaces, "
36
+ "and OpenAPI schemas. This gives the AI agent complete context for "
37
+ "everything that follows — minimizing hallucinations in later phases."
38
+ ),
39
+ "examples": [
40
+ "Pydantic models for all domain entities",
41
+ "TypeScript interfaces for frontend state",
42
+ "OpenAPI 3.1 schema definitions",
43
+ "Database migration schemas",
44
+ "Event/message schemas for async communication",
45
+ ],
46
+ },
47
+ {
48
+ "phase": 2,
49
+ "name": "infrastructure",
50
+ "title": "Infrastructure & Configuration",
51
+ "description": (
52
+ "Set up cloud resources, IaC, environment configs, secrets management. "
53
+ "Everything the code will depend on at runtime."
54
+ ),
55
+ "examples": [
56
+ "Pulumi/Bicep IaC definitions",
57
+ "Environment configuration (dev/staging/prod)",
58
+ "Secrets management setup",
59
+ "Database provisioning",
60
+ "Container registry and orchestration",
61
+ ],
62
+ },
63
+ {
64
+ "phase": 3,
65
+ "name": "auth_and_security",
66
+ "title": "Authentication & Security Foundation",
67
+ "description": (
68
+ "Implement auth flows, RBAC, security middleware. This must exist "
69
+ "before any business logic so every endpoint is secure from the start."
70
+ ),
71
+ "examples": [
72
+ "Auth provider integration (Azure AD B2C, Auth0, etc.)",
73
+ "JWT validation middleware",
74
+ "RBAC/permission system",
75
+ "CORS configuration",
76
+ "Rate limiting setup",
77
+ ],
78
+ },
79
+ {
80
+ "phase": 4,
81
+ "name": "data_layer",
82
+ "title": "Data Layer & Persistence",
83
+ "description": (
84
+ "Database models, migrations, repositories, data access patterns. "
85
+ "The AI agent needs the data layer complete before writing business logic."
86
+ ),
87
+ "examples": [
88
+ "ORM models / database schemas",
89
+ "Migration scripts",
90
+ "Repository pattern implementations",
91
+ "Seed data for development",
92
+ "Database connection management",
93
+ ],
94
+ },
95
+ {
96
+ "phase": 5,
97
+ "name": "core_services",
98
+ "title": "Core Business Services",
99
+ "description": (
100
+ "Business logic services that operate on the data layer. "
101
+ "Pure logic, no HTTP concerns — these are reusable across "
102
+ "APIs, workers, and MCP tools."
103
+ ),
104
+ "examples": [
105
+ "Domain service classes",
106
+ "Business rule validation",
107
+ "Workflow/state machine implementations",
108
+ "Integration adapters for external services",
109
+ "Event handlers",
110
+ ],
111
+ },
112
+ {
113
+ "phase": 6,
114
+ "name": "api_layer",
115
+ "title": "API Endpoints & Controllers",
116
+ "description": (
117
+ "REST/GraphQL endpoints that expose core services. "
118
+ "OpenAPI-documented, versioned, MCP-ready."
119
+ ),
120
+ "examples": [
121
+ "Route definitions with OpenAPI decorators",
122
+ "Request/response validation",
123
+ "Error handling middleware",
124
+ "API versioning",
125
+ "Health check endpoints",
126
+ ],
127
+ },
128
+ {
129
+ "phase": 7,
130
+ "name": "frontend",
131
+ "title": "Frontend Implementation",
132
+ "description": (
133
+ "UI components, pages, state management. Built against "
134
+ "the typed API contracts from Phase 1."
135
+ ),
136
+ "examples": [
137
+ "Component library / design system",
138
+ "Page layouts and routing",
139
+ "State management (React Query, Zustand, etc.)",
140
+ "Form handling with validation",
141
+ "Error boundaries and loading states",
142
+ ],
143
+ },
144
+ {
145
+ "phase": 8,
146
+ "name": "observability",
147
+ "title": "Observability & Monitoring",
148
+ "description": (
149
+ "Instrument everything — APM, metrics, logs, dashboards, alerts. "
150
+ "Including AI-specific observability if the project uses AI."
151
+ ),
152
+ "examples": [
153
+ "APM instrumentation (App Insights, etc.)",
154
+ "Custom metrics (Prometheus)",
155
+ "Structured logging (Loki)",
156
+ "Grafana dashboard definitions",
157
+ "Alert rules and notification channels",
158
+ "AI cost/safety/session tracking (if applicable)",
159
+ ],
160
+ },
161
+ {
162
+ "phase": 9,
163
+ "name": "testing",
164
+ "title": "Testing & Quality Assurance",
165
+ "description": (
166
+ "Unit tests, integration tests, E2E tests. Written after the "
167
+ "implementation to validate against acceptance criteria."
168
+ ),
169
+ "examples": [
170
+ "Unit tests for services and utilities",
171
+ "Integration tests for API endpoints",
172
+ "E2E tests for critical user flows",
173
+ "Load testing configuration",
174
+ "Security testing (OWASP baseline)",
175
+ ],
176
+ },
177
+ {
178
+ "phase": 10,
179
+ "name": "cicd_and_deploy",
180
+ "title": "CI/CD & Deployment",
181
+ "description": (
182
+ "Pipeline definitions, deployment scripts, smoke tests. "
183
+ "Everything needed to go from merge to production."
184
+ ),
185
+ "examples": [
186
+ "GitHub Actions / Azure Pipelines definitions",
187
+ "Docker build and push workflows",
188
+ "Environment promotion workflows",
189
+ "Smoke test suites",
190
+ "Rollback procedures",
191
+ ],
192
+ },
193
+ ]
194
+
195
+
196
+ def classify_requirement(content: str) -> RequirementType:
197
+ """Classify what type of requirement document this is.
198
+
199
+ Uses simple heuristics — a more sophisticated version would use
200
+ an LLM, but this gives a reasonable first pass.
201
+ """
202
+ content_lower = content.lower()
203
+
204
+ # PRD indicators
205
+ prd_signals = ["product requirements", "prd", "problem statement", "success metrics",
206
+ "user personas", "market analysis", "competitive analysis"]
207
+ if sum(1 for s in prd_signals if s in content_lower) >= 2:
208
+ return RequirementType.PRD
209
+
210
+ # Epic indicators
211
+ epic_signals = ["epic", "initiative", "objective", "key results", "okr"]
212
+ if sum(1 for s in epic_signals if s in content_lower) >= 2:
213
+ return RequirementType.EPIC
214
+
215
+ # User story indicators
216
+ story_signals = ["as a", "i want", "so that", "acceptance criteria",
217
+ "given", "when", "then", "story points"]
218
+ if sum(1 for s in story_signals if s in content_lower) >= 2:
219
+ return RequirementType.USER_STORY
220
+
221
+ # Feature request
222
+ feature_signals = ["feature", "request", "would be nice", "enhancement"]
223
+ if sum(1 for s in feature_signals if s in content_lower) >= 2:
224
+ return RequirementType.FEATURE_REQUEST
225
+
226
+ # Bug fix
227
+ bug_signals = ["bug", "fix", "broken", "error", "crash", "regression"]
228
+ if sum(1 for s in bug_signals if s in content_lower) >= 2:
229
+ return RequirementType.BUG_FIX
230
+
231
+ return RequirementType.CONVERSATION
232
+
233
+
234
+ def build_intake_prompt(content: str, requirement_type: RequirementType) -> str:
235
+ """Build the prompt for the LLM to analyze the requirement.
236
+
237
+ This prompt is designed to produce a structured ForgeBrief
238
+ from any kind of requirement input.
239
+ """
240
+ return f"""You are Forge, an AI development workflow engine. Analyze the following requirement and produce a structured brief.
241
+
242
+ ## Requirement Type Detected: {requirement_type.value}
243
+
244
+ ## Input Requirement:
245
+ {content}
246
+
247
+ ## Your Task:
248
+ Analyze this requirement and produce a JSON object with exactly this structure:
249
+
250
+ {{
251
+ "source_type": "{requirement_type.value}",
252
+ "completeness_score": <float 0-1, how complete/specific this requirement is>,
253
+ "objective": "<one paragraph: what this project/feature achieves>",
254
+ "users": ["<target user persona 1>", "<target user persona 2>"],
255
+ "features": [
256
+ {{
257
+ "name": "<feature name>",
258
+ "description": "<what it does>",
259
+ "mvp": <true/false>,
260
+ "priority": "<high/medium/low>"
261
+ }}
262
+ ],
263
+ "mvp_defined": <true if the input explicitly defines MVP scope, false if you inferred it>,
264
+ "mvp_features": ["<feature names that are MVP>"],
265
+ "external_dependencies": ["<external system or service this depends on>"],
266
+ "integrations": ["<third party integrations needed>"],
267
+ "regulatory_requirements": ["<hipaa/ferpa/gdpr/soc2/pci-dss/none>"],
268
+ "gaps": [
269
+ {{
270
+ "severity": "<critical/warning/info>",
271
+ "area": "<what area is missing info>",
272
+ "description": "<what's missing>",
273
+ "suggestion": "<what we'd recommend>"
274
+ }}
275
+ ],
276
+ "assumptions": ["<assumptions made to fill non-critical gaps>"],
277
+ "implementation_order": [
278
+ {{
279
+ "phase": <1-10>,
280
+ "name": "<phase_name>",
281
+ "title": "<Phase Title>",
282
+ "tasks": ["<specific task for this project in this phase>"]
283
+ }}
284
+ ]
285
+ }}
286
+
287
+ ## Rules:
288
+ 1. Be thorough but honest — if information is missing, flag it as a gap
289
+ 2. Don't invent features that aren't in the requirement
290
+ 3. If MVP scope isn't explicitly defined, make a reasonable suggestion and set mvp_defined to false
291
+ 4. The implementation_order MUST follow the AI-optimized sequence:
292
+ 1. types_and_contracts
293
+ 2. infrastructure
294
+ 3. auth_and_security
295
+ 4. data_layer
296
+ 5. core_services
297
+ 6. api_layer
298
+ 7. frontend
299
+ 8. observability
300
+ 9. testing
301
+ 10. cicd_and_deploy
302
+ 5. Only include phases that are relevant to this requirement
303
+ 6. Be specific in tasks — "Create User model with email, role, tenant fields" not "Create models"
304
+
305
+ Respond with ONLY the JSON object. No markdown, no explanation."""
306
+
307
+
308
+ def create_empty_brief() -> ForgeBrief:
309
+ """Create an empty brief for interactive filling."""
310
+ return ForgeBrief(
311
+ source_type=RequirementType.CONVERSATION,
312
+ implementation_order=AI_IMPLEMENTATION_PHASES,
313
+ )
314
+
315
+
316
+ def save_brief(brief: ForgeBrief, project_path: Path) -> Path:
317
+ """Save a ForgeBrief to the project's .forge/ directory."""
318
+ forge_dir = project_path / ".forge"
319
+ forge_dir.mkdir(parents=True, exist_ok=True)
320
+
321
+ brief_path = forge_dir / "brief.yaml"
322
+ with open(brief_path, "w") as f:
323
+ yaml.dump(brief.model_dump(mode="json"), f, default_flow_style=False, sort_keys=False)
324
+
325
+ return brief_path
326
+
327
+
328
+ def load_brief(project_path: Path) -> ForgeBrief | None:
329
+ """Load an existing ForgeBrief from the project."""
330
+ brief_path = project_path / ".forge" / "brief.yaml"
331
+ if not brief_path.exists():
332
+ return None
333
+ with open(brief_path) as f:
334
+ data = yaml.safe_load(f) or {}
335
+ return ForgeBrief(**data)
336
+
337
+
338
+ def get_implementation_phases() -> list[dict[str, Any]]:
339
+ """Return the standard AI implementation phases for reference."""
340
+ return AI_IMPLEMENTATION_PHASES