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,543 @@
1
+ """CLAUDE.md Generator — bridges Forge governance to AI editors.
2
+
3
+ This is the core value proposition of Forge: it translates all governance
4
+ knowledge (standards, patterns, anti-patterns, project context, journal
5
+ learnings, MCP recommendations) into a single CLAUDE.md file that any
6
+ AI coding editor can read and follow.
7
+
8
+ The generated CLAUDE.md acts as the "constitution" for the AI editor —
9
+ it tells the editor exactly how to behave in this specific project.
10
+
11
+ Supported outputs:
12
+ - CLAUDE.md for Claude Code
13
+ - .cursorrules for Cursor
14
+ - .github/copilot-instructions.md for GitHub Copilot
15
+ - Generic .ai-rules.md for any editor
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from datetime import datetime, timezone
21
+ from pathlib import Path
22
+ from typing import Any
23
+
24
+ import yaml
25
+
26
+ from forge_core.models import ProjectContext, Regulatory
27
+ from forge_core.registry import load_mcps, load_standards
28
+
29
+
30
+ def generate_editor_instructions(
31
+ project_path: Path,
32
+ context: ProjectContext,
33
+ format: str = "claude", # claude, cursor, copilot, generic
34
+ ) -> str:
35
+ """Generate complete editor instructions from Forge governance.
36
+
37
+ This is the main function — it reads ALL Forge knowledge and
38
+ produces a single document that tells the AI editor everything
39
+ it needs to know to generate correct code in this project.
40
+
41
+ Args:
42
+ project_path: Root of the project
43
+ context: Resolved project context
44
+ format: Output format (claude, cursor, copilot, generic)
45
+
46
+ Returns:
47
+ Complete instruction document as string
48
+ """
49
+ sections: list[str] = []
50
+
51
+ # ── Header ──
52
+ sections.append(_generate_header(context, format))
53
+
54
+ # ── Project Identity ──
55
+ sections.append(_generate_project_identity(context))
56
+
57
+ # ── Stack & Architecture Rules ──
58
+ sections.append(_generate_stack_rules(context))
59
+
60
+ # ── Standards (the hard rules) ──
61
+ standards = load_standards(include_user=True)
62
+ sections.append(_generate_standards_section(standards, context))
63
+
64
+ # ── Patterns & Anti-Patterns ──
65
+ sections.append(_generate_patterns_section(project_path))
66
+
67
+ # ── API Design Rules ──
68
+ sections.append(_generate_api_rules(context))
69
+
70
+ # ── Security & Compliance ──
71
+ sections.append(_generate_security_rules(context))
72
+
73
+ # ── Observability Rules ──
74
+ sections.append(_generate_observability_rules(context))
75
+
76
+ # ── AI-Specific Rules (if project uses AI) ──
77
+ if context.ai.enabled:
78
+ sections.append(_generate_ai_rules(context))
79
+
80
+ # ── Code Quality Rules ──
81
+ sections.append(_generate_code_quality_rules(context))
82
+
83
+ # ── Project Journal (learnings & nuances) ──
84
+ journal_content = _load_journal(project_path)
85
+ if journal_content:
86
+ sections.append(_generate_journal_section(journal_content))
87
+
88
+ # ── MCP Recommendations ──
89
+ mcps = load_mcps()
90
+ if mcps:
91
+ sections.append(_generate_mcp_section(mcps))
92
+
93
+ # ── Self-Audit Instructions ──
94
+ sections.append(_generate_self_audit_instructions(context))
95
+
96
+ # ── Implementation Ordering ──
97
+ sections.append(_generate_implementation_order())
98
+
99
+ return "\n\n".join(sections)
100
+
101
+
102
+ def write_editor_file(
103
+ project_path: Path,
104
+ context: ProjectContext,
105
+ format: str = "claude",
106
+ ) -> Path:
107
+ """Generate and write the editor instructions file.
108
+
109
+ Returns the path to the written file.
110
+ """
111
+ content = generate_editor_instructions(project_path, context, format)
112
+
113
+ filename_map = {
114
+ "claude": "CLAUDE.md",
115
+ "cursor": ".cursorrules",
116
+ "copilot": ".github/copilot-instructions.md",
117
+ "generic": ".ai-rules.md",
118
+ }
119
+
120
+ filename = filename_map.get(format, "CLAUDE.md")
121
+ file_path = project_path / filename
122
+
123
+ # Ensure parent directory exists (for copilot)
124
+ file_path.parent.mkdir(parents=True, exist_ok=True)
125
+
126
+ file_path.write_text(content)
127
+ return file_path
128
+
129
+
130
+ def sync_editor_file(project_path: Path, context: ProjectContext) -> list[Path]:
131
+ """Regenerate all editor instruction files that exist in the project.
132
+
133
+ Only regenerates files that already exist — doesn't create new ones.
134
+ Returns list of updated files.
135
+ """
136
+ updated = []
137
+ checks = [
138
+ ("claude", "CLAUDE.md"),
139
+ ("cursor", ".cursorrules"),
140
+ ("copilot", ".github/copilot-instructions.md"),
141
+ ("generic", ".ai-rules.md"),
142
+ ]
143
+ for fmt, filename in checks:
144
+ path = project_path / filename
145
+ if path.exists():
146
+ write_editor_file(project_path, context, fmt)
147
+ updated.append(path)
148
+
149
+ return updated
150
+
151
+
152
+ # ── Section Generators ─────────────────────────────────────────────────────
153
+
154
+ def _generate_header(context: ProjectContext, format: str) -> str:
155
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
156
+ return f"""# {context.name} — AI Editor Instructions
157
+ # Generated by Forge v{context.forge_version} on {timestamp}
158
+ # DO NOT EDIT MANUALLY — regenerate with `forge sync`
159
+ # Source of truth: .forge/context.yaml + ~/.forge/ standards"""
160
+
161
+
162
+ def _generate_project_identity(context: ProjectContext) -> str:
163
+ lines = [
164
+ "## Project Overview",
165
+ "",
166
+ f"**Name:** {context.name}",
167
+ f"**Type:** {context.type.value}",
168
+ ]
169
+ if context.description:
170
+ lines.append(f"**Purpose:** {context.description}")
171
+ if context.regulatory:
172
+ regs = ", ".join(r.value.upper() for r in context.regulatory)
173
+ lines.append(f"**Regulatory:** {regs} — all code must comply with these requirements")
174
+ return "\n".join(lines)
175
+
176
+
177
+ def _generate_stack_rules(context: ProjectContext) -> str:
178
+ return f"""## Stack & Architecture
179
+
180
+ This project uses the following stack. All code must be consistent with these choices.
181
+
182
+ - **Cloud:** {context.cloud.value}
183
+ - **Backend:** {context.backend.value}
184
+ - **Frontend:** {context.frontend.value}
185
+ - **Database:** {context.database.value}
186
+ - **Auth:** {context.auth.value}
187
+ - **API Style:** {context.api.style} with {context.api.spec}
188
+ - **IaC:** {context.cicd.iac}
189
+ - **CI/CD:** {context.cicd.provider}
190
+
191
+ Do NOT introduce alternative frameworks, libraries, or patterns that conflict with this stack
192
+ unless explicitly approved and documented in .forge/overrides/."""
193
+
194
+
195
+ def _generate_standards_section(standards: list[dict], context: ProjectContext) -> str:
196
+ if not standards:
197
+ return "## Standards\n\nNo standards configured yet."
198
+
199
+ lines = [
200
+ "## Standards — Hard Rules",
201
+ "",
202
+ "These are non-negotiable. All code must comply.",
203
+ "",
204
+ ]
205
+
206
+ for std in standards:
207
+ name = std.get("name", "unnamed")
208
+ area = std.get("area", "general")
209
+ desc = std.get("description", "")
210
+ enforcement = std.get("enforcement", "required")
211
+ rules = std.get("rules", [])
212
+
213
+ lines.append(f"### {name} ({area}) [{enforcement}]")
214
+ lines.append(f"{desc}")
215
+ lines.append("")
216
+
217
+ if rules:
218
+ for rule in rules:
219
+ lines.append(f"- {rule}")
220
+ lines.append("")
221
+
222
+ # Include examples if available
223
+ examples = std.get("examples", {})
224
+ if examples.get("good"):
225
+ lines.append("**Good:**")
226
+ for ex in examples["good"]:
227
+ lines.append(f"- `{ex}`")
228
+ lines.append("")
229
+ if examples.get("bad"):
230
+ lines.append("**Bad (never do this):**")
231
+ for ex in examples["bad"]:
232
+ lines.append(f"- `{ex}`")
233
+ lines.append("")
234
+
235
+ return "\n".join(lines)
236
+
237
+
238
+ def _generate_patterns_section(project_path: Path) -> str:
239
+ """Load patterns and anti-patterns from both global and project level."""
240
+ lines = [
241
+ "## Approved Patterns & Anti-Patterns",
242
+ "",
243
+ ]
244
+
245
+ # Global patterns
246
+ from forge_core.registry import USER_DIR
247
+ patterns_dir = USER_DIR / "patterns"
248
+ anti_dir = USER_DIR / "anti-patterns"
249
+
250
+ # Project-level overrides
251
+ project_patterns_dir = project_path / ".forge" / "overrides" / "patterns"
252
+ project_anti_dir = project_path / ".forge" / "overrides" / "anti-patterns"
253
+
254
+ patterns = _load_yaml_dir(patterns_dir) + _load_yaml_dir(project_patterns_dir)
255
+ anti_patterns = _load_yaml_dir(anti_dir) + _load_yaml_dir(project_anti_dir)
256
+
257
+ if patterns:
258
+ lines.append("### Approved Patterns (use these)")
259
+ lines.append("")
260
+ for p in patterns:
261
+ lines.append(f"**{p.get('name', 'unnamed')}**: {p.get('description', '')}")
262
+ if p.get("example"):
263
+ lines.append(f"```\n{p['example']}\n```")
264
+ lines.append("")
265
+
266
+ if anti_patterns:
267
+ lines.append("### Anti-Patterns (NEVER do these)")
268
+ lines.append("")
269
+ for ap in anti_patterns:
270
+ lines.append(f"**{ap.get('name', 'unnamed')}**: {ap.get('description', '')}")
271
+ if ap.get("instead"):
272
+ lines.append(f"**Instead:** {ap['instead']}")
273
+ if ap.get("example_bad"):
274
+ lines.append(f"```\n# BAD — do not do this\n{ap['example_bad']}\n```")
275
+ if ap.get("example_good"):
276
+ lines.append(f"```\n# GOOD — do this instead\n{ap['example_good']}\n```")
277
+ lines.append("")
278
+
279
+ if not patterns and not anti_patterns:
280
+ lines.append("No patterns or anti-patterns defined yet.")
281
+ lines.append("Add them via `forge standards` or in ~/.forge/user/patterns/")
282
+
283
+ return "\n".join(lines)
284
+
285
+
286
+ def _generate_api_rules(context: ProjectContext) -> str:
287
+ lines = [
288
+ "## API Design Rules",
289
+ "",
290
+ "When creating any API endpoint, controller, or route:",
291
+ "",
292
+ f"- Use {context.api.versioning} versioning (e.g., /v1/resource)",
293
+ f"- Document with {context.api.spec} decorators/annotations",
294
+ "- Return RFC 7807 Problem Details for all errors",
295
+ "- Include pagination (cursor-based preferred) for list endpoints",
296
+ "- Add rate limiting configuration",
297
+ "- Include health check endpoint (/health, /ready)",
298
+ "- Every endpoint must have request/response type validation",
299
+ ]
300
+
301
+ if context.api.mcp_ready:
302
+ lines.extend([
303
+ "",
304
+ "### MCP-Ready Design",
305
+ "- Every endpoint must be usable as an MCP tool — clear inputs, clear outputs, no implicit state",
306
+ "- Response schemas must be self-describing (include field descriptions)",
307
+ "- Endpoints must be independently callable without session context",
308
+ ])
309
+
310
+ return "\n".join(lines)
311
+
312
+
313
+ def _generate_security_rules(context: ProjectContext) -> str:
314
+ lines = [
315
+ "## Security Rules",
316
+ "",
317
+ f"- Authentication: {context.auth.value} — implement before any business endpoint",
318
+ "- All user input must be validated at the API boundary with typed models",
319
+ "- Secrets must NEVER appear in code or config — use environment variables or vault",
320
+ "- CORS must be explicitly configured — never use wildcard (*) in production",
321
+ "- All SQL must use parameterized queries — no string concatenation",
322
+ "- Dependencies must be scanned for vulnerabilities in CI",
323
+ "- Security headers must be set (HSTS, X-Content-Type-Options, CSP)",
324
+ ]
325
+
326
+ for reg in context.regulatory:
327
+ if reg == Regulatory.HIPAA:
328
+ lines.extend([
329
+ "",
330
+ "### HIPAA Compliance (REQUIRED)",
331
+ "- All PHI must be encrypted at rest (AES-256) and in transit (TLS 1.2+)",
332
+ "- Audit logging of ALL PHI access with user identity and timestamp",
333
+ "- Minimum necessary access via RBAC — no broad data access",
334
+ "- Session timeouts must not exceed 15 minutes of inactivity",
335
+ "- BAA must be in place for all cloud services handling PHI",
336
+ "- Never log PHI in application logs — use tokenized references",
337
+ ])
338
+ elif reg == Regulatory.FERPA:
339
+ lines.extend([
340
+ "",
341
+ "### FERPA Compliance (REQUIRED)",
342
+ "- Student education records must have restricted access controls",
343
+ "- Consent tracking for all data sharing",
344
+ "- Audit trail for all record access",
345
+ ])
346
+ elif reg == Regulatory.GDPR:
347
+ lines.extend([
348
+ "",
349
+ "### GDPR Compliance (REQUIRED)",
350
+ "- Explicit consent must be recorded for data processing",
351
+ "- Right to deletion must be implementable for all user data",
352
+ "- Data portability exports must be available",
353
+ ])
354
+ elif reg == Regulatory.SOC2:
355
+ lines.extend([
356
+ "",
357
+ "### SOC2 Compliance (REQUIRED)",
358
+ "- Access controls and audit logging on all systems",
359
+ "- Change management procedures must be documented",
360
+ "- Incident response procedures must exist",
361
+ ])
362
+
363
+ return "\n".join(lines)
364
+
365
+
366
+ def _generate_observability_rules(context: ProjectContext) -> str:
367
+ obs = context.observability
368
+ return f"""## Observability Rules
369
+
370
+ Every piece of code must be observable from day one.
371
+
372
+ - **APM:** {obs.apm} — instrument all HTTP requests, database queries, and external calls
373
+ - **Metrics:** {obs.metrics} — expose custom metrics via /metrics endpoint
374
+ - **Logs:** {obs.logs} — ALL logs must be structured JSON with correlation IDs
375
+ - **Dashboards:** {obs.dashboards} — dashboard definitions must be version-controlled
376
+ - **Tracing:** {'Enabled' if obs.tracing else 'Disabled'} — use W3C Trace Context for distributed tracing
377
+
378
+ ### When creating any new service or endpoint:
379
+ 1. Add APM instrumentation (auto or manual)
380
+ 2. Add custom metrics for business-relevant events
381
+ 3. Use structured logging with correlation ID propagation
382
+ 4. Add to the service's Grafana dashboard
383
+ 5. Define alert rules for error rate and latency"""
384
+
385
+
386
+ def _generate_ai_rules(context: ProjectContext) -> str:
387
+ ai = context.ai
388
+ providers = ", ".join(ai.providers) if ai.providers else "not specified"
389
+ return f"""## AI Integration Rules
390
+
391
+ This project uses AI capabilities internally.
392
+
393
+ - **Providers:** {providers}
394
+ - **Observability:** {'Required' if ai.observability else 'Optional'}
395
+
396
+ ### When integrating AI:
397
+ - Track per-request: model, tokens in/out, latency, cost estimate
398
+ - Implement cost controls — daily spend limits and alerts
399
+ - Add prompt injection detection on all user-facing AI inputs
400
+ - Log all AI interactions with trace IDs for debugging
401
+ - Monitor context window utilization — alert if consistently >80%
402
+ - Implement graceful degradation when AI services are unavailable
403
+ - Rate limit AI-powered endpoints separately from standard endpoints
404
+ - Never expose raw model responses to users without safety filtering
405
+ - Cache AI responses where appropriate to reduce cost and latency
406
+ - Track and alert on AI response quality metrics where measurable"""
407
+
408
+
409
+ def _generate_code_quality_rules(context: ProjectContext) -> str:
410
+ std = context.standards
411
+ return f"""## Code Quality Rules
412
+
413
+ - **Type checking:** {std.type_checking} — no exceptions
414
+ - **Linting:** {std.linting} — must pass before commit
415
+ - **Test coverage minimum:** {std.test_coverage_min}%
416
+
417
+ ### Before writing any code:
418
+ 1. Check if similar functionality already exists — do NOT duplicate
419
+ 2. Follow existing patterns in the codebase — consistency over personal preference
420
+ 3. Add types to everything — parameters, returns, variables where non-obvious
421
+ 4. Write tests for new functionality
422
+ 5. Ensure all imports are used and all exports are intentional
423
+
424
+ ### Self-audit checklist (run mentally before completing any task):
425
+ - [ ] Does this follow the project's established patterns?
426
+ - [ ] Are all types explicit and correct?
427
+ - [ ] Is error handling consistent with the rest of the codebase?
428
+ - [ ] Is there any duplicated logic that should be extracted?
429
+ - [ ] Are security boundaries respected?
430
+ - [ ] Is this observable? (logs, metrics, traces)
431
+ - [ ] Would a new team member understand this code?"""
432
+
433
+
434
+ def _generate_journal_section(journal_content: str) -> str:
435
+ return f"""## Project-Specific Context (Journal)
436
+
437
+ These are learnings and nuances specific to this project. Read and respect them.
438
+
439
+ {journal_content}"""
440
+
441
+
442
+ def _generate_mcp_section(mcps: list) -> str:
443
+ lines = [
444
+ "## Recommended MCP Servers",
445
+ "",
446
+ "Use these MCP servers for better development context:",
447
+ "",
448
+ ]
449
+ for mcp in mcps:
450
+ lines.append(f"- **{mcp.name}**: {mcp.description}")
451
+ if mcp.url:
452
+ lines.append(f" Command: `{mcp.url}`")
453
+ return "\n".join(lines)
454
+
455
+
456
+ def _generate_self_audit_instructions(context: ProjectContext) -> str:
457
+ return """## Self-Audit Instructions
458
+
459
+ After generating or modifying any code, perform these checks automatically:
460
+
461
+ ### 1. Pattern Compliance
462
+ - Does the new code follow established patterns in the codebase?
463
+ - If introducing a new pattern, is it documented and justified?
464
+ - Are there any anti-patterns present?
465
+
466
+ ### 2. Security Check
467
+ - Is all user input validated?
468
+ - Are secrets properly managed (no hardcoded values)?
469
+ - Is authentication/authorization applied correctly?
470
+ - Are SQL queries parameterized?
471
+
472
+ ### 3. API Compliance (if applicable)
473
+ - Does the endpoint have OpenAPI documentation?
474
+ - Does it follow the versioning strategy?
475
+ - Are error responses in RFC 7807 format?
476
+ - Is rate limiting configured?
477
+ - Is it MCP-ready (clear inputs/outputs, no implicit state)?
478
+
479
+ ### 4. Observability Check
480
+ - Is the code instrumented for APM?
481
+ - Are relevant custom metrics added?
482
+ - Is logging structured with correlation IDs?
483
+ - Are dashboard updates needed?
484
+
485
+ ### 5. Type Safety
486
+ - Are all types explicit?
487
+ - Are there any `any` or untyped values?
488
+ - Is input/output validation in place at boundaries?
489
+
490
+ ### 6. Duplication Check
491
+ - Does similar functionality already exist?
492
+ - Can existing utilities/services be reused?
493
+ - Should this be extracted into a shared utility?
494
+
495
+ If any check fails, fix it before presenting the code."""
496
+
497
+
498
+ def _generate_implementation_order() -> str:
499
+ return """## Implementation Order for New Features
500
+
501
+ When building anything new, follow this order to ensure each step has full context:
502
+
503
+ 1. **Types & Contracts** — Define models, interfaces, schemas first
504
+ 2. **Data Layer** — Migrations, repository methods
505
+ 3. **Business Logic** — Service classes, pure logic
506
+ 4. **API Endpoints** — Thin controllers wiring services to HTTP
507
+ 5. **Frontend** — Components consuming typed API contracts
508
+ 6. **Observability** — Instrument the new code
509
+ 7. **Tests** — Validate against acceptance criteria
510
+
511
+ This order minimizes errors because each step has complete context from the previous steps."""
512
+
513
+
514
+ # ── Helpers ────────────────────────────────────────────────────────────────
515
+
516
+ def _load_journal(project_path: Path) -> str:
517
+ """Load the project journal content (just the entries, not the header)."""
518
+ journal_path = project_path / ".forge" / "journal.md"
519
+ if not journal_path.exists():
520
+ return ""
521
+
522
+ content = journal_path.read_text()
523
+ # Extract just the entries section
524
+ marker = "## Entries"
525
+ if marker in content:
526
+ entries = content.split(marker, 1)[1].strip()
527
+ return entries if entries else ""
528
+ return ""
529
+
530
+
531
+ def _load_yaml_dir(dir_path: Path) -> list[dict]:
532
+ """Load all YAML files from a directory."""
533
+ if not dir_path.exists():
534
+ return []
535
+ results = []
536
+ for f in sorted(dir_path.glob("*.yaml")):
537
+ try:
538
+ with open(f) as fp:
539
+ data = yaml.safe_load(fp) or {}
540
+ results.append(data)
541
+ except Exception:
542
+ continue
543
+ return results