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.
- forge_core/__init__.py +3 -0
- forge_core/agents/__init__.py +1 -0
- forge_core/auditor.py +330 -0
- forge_core/cli.py +552 -0
- forge_core/detector.py +209 -0
- forge_core/editor_bridge.py +543 -0
- forge_core/models.py +332 -0
- forge_core/phases/__init__.py +1 -0
- forge_core/phases/coherence.py +293 -0
- forge_core/phases/context.py +264 -0
- forge_core/phases/intake.py +340 -0
- forge_core/registry.py +247 -0
- forge_core/standards/api-first-design.yaml +24 -0
- forge_core/standards/microservice-packaging.yaml +30 -0
- forge_core/standards/observability.yaml +31 -0
- forge_core/standards/security-baseline.yaml +43 -0
- forge_core/standards/type-safety.yaml +23 -0
- forge_core/templates/__init__.py +1 -0
- forge_core/utils/__init__.py +1 -0
- forge_dev-0.1.0.dist-info/METADATA +134 -0
- forge_dev-0.1.0.dist-info/RECORD +25 -0
- forge_dev-0.1.0.dist-info/WHEEL +4 -0
- forge_dev-0.1.0.dist-info/entry_points.txt +2 -0
- mcp_server/__init__.py +1 -0
- mcp_server/server.py +1086 -0
forge_core/models.py
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Core configuration models for Forge.
|
|
2
|
+
|
|
3
|
+
These models define the structure of all Forge configuration files:
|
|
4
|
+
- Global user config (~/.forge/user/config.yaml)
|
|
5
|
+
- Project context (.forge/context.yaml)
|
|
6
|
+
- Standards and patterns
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ── Enums ──────────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
class CloudProvider(str, Enum):
|
|
21
|
+
AZURE = "azure"
|
|
22
|
+
AWS = "aws"
|
|
23
|
+
GCP = "gcp"
|
|
24
|
+
RAILWAY = "railway"
|
|
25
|
+
VERCEL = "vercel"
|
|
26
|
+
FLY = "fly"
|
|
27
|
+
OTHER = "other"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BackendFramework(str, Enum):
|
|
31
|
+
FASTAPI = "python/fastapi"
|
|
32
|
+
DJANGO = "python/django"
|
|
33
|
+
FLASK = "python/flask"
|
|
34
|
+
EXPRESS = "node/express"
|
|
35
|
+
FASTIFY = "node/fastify"
|
|
36
|
+
NESTJS = "node/nestjs"
|
|
37
|
+
OTHER = "other"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FrontendFramework(str, Enum):
|
|
41
|
+
REACT = "react"
|
|
42
|
+
NEXT = "nextjs"
|
|
43
|
+
VUE = "vue"
|
|
44
|
+
SVELTE = "svelte"
|
|
45
|
+
ANGULAR = "angular"
|
|
46
|
+
OTHER = "other"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class DatabaseType(str, Enum):
|
|
50
|
+
POSTGRESQL = "postgresql"
|
|
51
|
+
MYSQL = "mysql"
|
|
52
|
+
MONGODB = "mongodb"
|
|
53
|
+
ORACLE = "oracle"
|
|
54
|
+
SQLSERVER = "sqlserver"
|
|
55
|
+
SQLITE = "sqlite"
|
|
56
|
+
COSMOSDB = "cosmosdb"
|
|
57
|
+
OTHER = "other"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AuthPattern(str, Enum):
|
|
61
|
+
AZURE_AD_B2C = "azure-ad-b2c"
|
|
62
|
+
AUTH0 = "auth0"
|
|
63
|
+
CLERK = "clerk"
|
|
64
|
+
SUPABASE_AUTH = "supabase-auth"
|
|
65
|
+
KEYCLOAK = "keycloak"
|
|
66
|
+
CUSTOM_JWT = "custom-jwt"
|
|
67
|
+
NONE = "none"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ProjectType(str, Enum):
|
|
71
|
+
SAAS = "saas"
|
|
72
|
+
API = "api"
|
|
73
|
+
INTERNAL_TOOL = "internal-tool"
|
|
74
|
+
LIBRARY = "library"
|
|
75
|
+
CLI = "cli"
|
|
76
|
+
MICROSERVICE = "microservice"
|
|
77
|
+
OTHER = "other"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Regulatory(str, Enum):
|
|
81
|
+
HIPAA = "hipaa"
|
|
82
|
+
FERPA = "ferpa"
|
|
83
|
+
GDPR = "gdpr"
|
|
84
|
+
SOC2 = "soc2"
|
|
85
|
+
PCI_DSS = "pci-dss"
|
|
86
|
+
NONE = "none"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class RequirementType(str, Enum):
|
|
90
|
+
PRD = "prd"
|
|
91
|
+
USER_STORY = "user-story"
|
|
92
|
+
EPIC = "epic"
|
|
93
|
+
FEATURE_REQUEST = "feature-request"
|
|
94
|
+
BUG_FIX = "bug-fix"
|
|
95
|
+
CONVERSATION = "conversation"
|
|
96
|
+
UNKNOWN = "unknown"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ProjectState(str, Enum):
|
|
100
|
+
EMPTY = "empty"
|
|
101
|
+
HAS_DOCS = "has-docs"
|
|
102
|
+
HAS_CODE = "has-code"
|
|
103
|
+
HAS_FORGE = "has-forge"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# ── Configuration Models ───────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
class AIConfig(BaseModel):
|
|
109
|
+
"""AI-specific configuration for projects that use AI internally."""
|
|
110
|
+
model_config = ConfigDict(extra="allow")
|
|
111
|
+
|
|
112
|
+
enabled: bool = Field(default=False, description="Whether the project uses AI capabilities")
|
|
113
|
+
providers: list[str] = Field(default_factory=list, description="AI providers used (anthropic, openai, etc.)")
|
|
114
|
+
observability: bool = Field(default=True, description="Track AI traces, costs, safety, sessions")
|
|
115
|
+
cost_alerts: bool = Field(default=True, description="Enable cost alerting for AI usage")
|
|
116
|
+
safety_checks: bool = Field(default=True, description="Enable prompt injection and safety monitoring")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ObservabilityConfig(BaseModel):
|
|
120
|
+
"""Observability stack configuration."""
|
|
121
|
+
model_config = ConfigDict(extra="allow")
|
|
122
|
+
|
|
123
|
+
apm: str = Field(default="azure-app-insights", description="APM provider")
|
|
124
|
+
metrics: str = Field(default="prometheus", description="Metrics collection")
|
|
125
|
+
logs: str = Field(default="loki", description="Log aggregation")
|
|
126
|
+
dashboards: str = Field(default="grafana", description="Dashboard provider")
|
|
127
|
+
tracing: bool = Field(default=True, description="Distributed tracing enabled")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class APIConfig(BaseModel):
|
|
131
|
+
"""API design configuration."""
|
|
132
|
+
model_config = ConfigDict(extra="allow")
|
|
133
|
+
|
|
134
|
+
style: str = Field(default="rest", description="API style (rest, graphql)")
|
|
135
|
+
spec: str = Field(default="openapi-3.1", description="API specification format")
|
|
136
|
+
mcp_ready: bool = Field(default=True, description="APIs designed for MCP consumption")
|
|
137
|
+
versioning: str = Field(default="url-prefix", description="API versioning strategy")
|
|
138
|
+
rate_limiting: bool = Field(default=True, description="Enable rate limiting")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class CICDConfig(BaseModel):
|
|
142
|
+
"""CI/CD configuration."""
|
|
143
|
+
model_config = ConfigDict(extra="allow")
|
|
144
|
+
|
|
145
|
+
provider: str = Field(default="github-actions", description="CI/CD provider")
|
|
146
|
+
iac: str = Field(default="pulumi", description="Infrastructure as Code tool")
|
|
147
|
+
environments: list[str] = Field(
|
|
148
|
+
default_factory=lambda: ["dev", "staging", "production"],
|
|
149
|
+
description="Deployment environments"
|
|
150
|
+
)
|
|
151
|
+
auto_deploy_dev: bool = Field(default=True, description="Auto-deploy to dev on merge")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class StandardsConfig(BaseModel):
|
|
155
|
+
"""Code standards enforcement."""
|
|
156
|
+
model_config = ConfigDict(extra="allow")
|
|
157
|
+
|
|
158
|
+
type_checking: str = Field(default="strict", description="Type checking level")
|
|
159
|
+
linting: str = Field(default="enforced", description="Linting enforcement level")
|
|
160
|
+
test_coverage_min: int = Field(default=80, description="Minimum test coverage percentage")
|
|
161
|
+
patterns: list[str] = Field(default_factory=list, description="Approved patterns")
|
|
162
|
+
anti_patterns: list[str] = Field(default_factory=list, description="Prohibited patterns")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class MCPEntry(BaseModel):
|
|
166
|
+
"""An MCP server entry in the registry."""
|
|
167
|
+
model_config = ConfigDict(extra="allow")
|
|
168
|
+
|
|
169
|
+
name: str = Field(..., description="MCP server name")
|
|
170
|
+
description: str = Field(default="", description="What this MCP provides")
|
|
171
|
+
url: str = Field(default="", description="MCP server URL or command")
|
|
172
|
+
transport: str = Field(default="stdio", description="Transport type (stdio, http)")
|
|
173
|
+
auto_suggest: bool = Field(default=False, description="Suggest when relevant dependencies detected")
|
|
174
|
+
conditions: list[str] = Field(
|
|
175
|
+
default_factory=list,
|
|
176
|
+
description="Conditions that trigger auto-suggestion"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── Top-Level Configs ──────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
class UserConfig(BaseModel):
|
|
183
|
+
"""Global user configuration (~/.forge/user/config.yaml).
|
|
184
|
+
|
|
185
|
+
These are the user's defaults — applied to every new project unless overridden.
|
|
186
|
+
"""
|
|
187
|
+
model_config = ConfigDict(extra="allow")
|
|
188
|
+
|
|
189
|
+
cloud: CloudProvider = Field(default=CloudProvider.AZURE)
|
|
190
|
+
backend: BackendFramework | None = Field(
|
|
191
|
+
default=None,
|
|
192
|
+
description="Default backend. None means ask each time."
|
|
193
|
+
)
|
|
194
|
+
frontend: FrontendFramework = Field(default=FrontendFramework.REACT)
|
|
195
|
+
database: DatabaseType = Field(default=DatabaseType.POSTGRESQL)
|
|
196
|
+
auth: AuthPattern = Field(default=AuthPattern.AZURE_AD_B2C)
|
|
197
|
+
ai: AIConfig = Field(default_factory=AIConfig)
|
|
198
|
+
observability: ObservabilityConfig = Field(default_factory=ObservabilityConfig)
|
|
199
|
+
api: APIConfig = Field(default_factory=APIConfig)
|
|
200
|
+
cicd: CICDConfig = Field(default_factory=CICDConfig)
|
|
201
|
+
standards: StandardsConfig = Field(default_factory=StandardsConfig)
|
|
202
|
+
mcps: list[MCPEntry] = Field(default_factory=list)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class ProjectContext(BaseModel):
|
|
206
|
+
"""Per-project context (.forge/context.yaml).
|
|
207
|
+
|
|
208
|
+
Captures all decisions made for this specific project.
|
|
209
|
+
"""
|
|
210
|
+
model_config = ConfigDict(extra="allow")
|
|
211
|
+
|
|
212
|
+
# Project identity
|
|
213
|
+
name: str = Field(..., description="Project name")
|
|
214
|
+
type: ProjectType = Field(default=ProjectType.SAAS)
|
|
215
|
+
description: str = Field(default="", description="One-line project description")
|
|
216
|
+
regulatory: list[Regulatory] = Field(default_factory=list)
|
|
217
|
+
|
|
218
|
+
# Stack decisions
|
|
219
|
+
cloud: CloudProvider = Field(default=CloudProvider.AZURE)
|
|
220
|
+
backend: BackendFramework = Field(default=BackendFramework.FASTAPI)
|
|
221
|
+
frontend: FrontendFramework = Field(default=FrontendFramework.REACT)
|
|
222
|
+
database: DatabaseType = Field(default=DatabaseType.POSTGRESQL)
|
|
223
|
+
auth: AuthPattern = Field(default=AuthPattern.AZURE_AD_B2C)
|
|
224
|
+
|
|
225
|
+
# Feature configs
|
|
226
|
+
ai: AIConfig = Field(default_factory=AIConfig)
|
|
227
|
+
observability: ObservabilityConfig = Field(default_factory=ObservabilityConfig)
|
|
228
|
+
api: APIConfig = Field(default_factory=APIConfig)
|
|
229
|
+
cicd: CICDConfig = Field(default_factory=CICDConfig)
|
|
230
|
+
standards: StandardsConfig = Field(default_factory=StandardsConfig)
|
|
231
|
+
|
|
232
|
+
# State
|
|
233
|
+
forge_version: str = Field(default="0.1.0", description="Forge version used to create")
|
|
234
|
+
created_at: str = Field(default="", description="ISO timestamp of creation")
|
|
235
|
+
last_audit: str = Field(default="", description="ISO timestamp of last audit")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class ForgeBrief(BaseModel):
|
|
239
|
+
"""Normalized requirement document produced by the Intake phase.
|
|
240
|
+
|
|
241
|
+
This is what every requirement gets converted into, regardless of
|
|
242
|
+
whether the input was a PRD, user story, Slack message, or conversation.
|
|
243
|
+
"""
|
|
244
|
+
model_config = ConfigDict(extra="allow")
|
|
245
|
+
|
|
246
|
+
# Classification
|
|
247
|
+
source_type: RequirementType = Field(default=RequirementType.UNKNOWN)
|
|
248
|
+
completeness_score: float = Field(
|
|
249
|
+
default=0.0, ge=0.0, le=1.0,
|
|
250
|
+
description="How complete the requirement is (0=vague, 1=fully specified)"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Core content
|
|
254
|
+
objective: str = Field(default="", description="What this project/feature achieves")
|
|
255
|
+
users: list[str] = Field(default_factory=list, description="Target user personas")
|
|
256
|
+
features: list[dict[str, Any]] = Field(
|
|
257
|
+
default_factory=list,
|
|
258
|
+
description="List of features with name, description, mvp flag, priority"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# Scope
|
|
262
|
+
mvp_defined: bool = Field(default=False, description="Whether MVP scope was defined in source")
|
|
263
|
+
mvp_features: list[str] = Field(default_factory=list, description="Features in MVP scope")
|
|
264
|
+
|
|
265
|
+
# Dependencies & constraints
|
|
266
|
+
external_dependencies: list[str] = Field(default_factory=list)
|
|
267
|
+
integrations: list[str] = Field(default_factory=list)
|
|
268
|
+
regulatory_requirements: list[Regulatory] = Field(default_factory=list)
|
|
269
|
+
|
|
270
|
+
# Gaps detected
|
|
271
|
+
gaps: list[dict[str, str]] = Field(
|
|
272
|
+
default_factory=list,
|
|
273
|
+
description="Detected gaps: [{severity, area, description, suggestion}]"
|
|
274
|
+
)
|
|
275
|
+
assumptions: list[str] = Field(
|
|
276
|
+
default_factory=list,
|
|
277
|
+
description="Assumptions made to fill non-critical gaps"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Implementation guidance for AI coding agents
|
|
281
|
+
implementation_order: list[dict[str, Any]] = Field(
|
|
282
|
+
default_factory=list,
|
|
283
|
+
description="Ordered phases for AI-agent implementation"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class ForgeTask(BaseModel):
|
|
288
|
+
"""A single implementation task optimized for AI coding agents."""
|
|
289
|
+
model_config = ConfigDict(extra="allow")
|
|
290
|
+
|
|
291
|
+
id: str = Field(..., description="Task identifier (e.g., T001)")
|
|
292
|
+
phase: str = Field(..., description="Implementation phase this belongs to")
|
|
293
|
+
title: str = Field(..., description="What this task accomplishes")
|
|
294
|
+
description: str = Field(default="", description="Detailed description")
|
|
295
|
+
files_affected: list[str] = Field(default_factory=list, description="Files to create/modify")
|
|
296
|
+
dependencies: list[str] = Field(default_factory=list, description="Task IDs this depends on")
|
|
297
|
+
types_needed: list[str] = Field(
|
|
298
|
+
default_factory=list,
|
|
299
|
+
description="Types/interfaces that must exist before this task"
|
|
300
|
+
)
|
|
301
|
+
acceptance_criteria: list[str] = Field(
|
|
302
|
+
default_factory=list,
|
|
303
|
+
description="Verifiable criteria (tests that must pass)"
|
|
304
|
+
)
|
|
305
|
+
estimated_complexity: str = Field(default="medium", description="low/medium/high")
|
|
306
|
+
status: str = Field(default="pending", description="pending/in-progress/done/blocked")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class MaturityReport(BaseModel):
|
|
310
|
+
"""Assessment of an existing project against Forge standards."""
|
|
311
|
+
model_config = ConfigDict(extra="allow")
|
|
312
|
+
|
|
313
|
+
project_name: str = Field(...)
|
|
314
|
+
assessed_at: str = Field(default="", description="ISO timestamp")
|
|
315
|
+
forge_version: str = Field(default="0.1.0")
|
|
316
|
+
|
|
317
|
+
overall_score: float = Field(default=0.0, ge=0.0, le=1.0)
|
|
318
|
+
|
|
319
|
+
categories: dict[str, dict[str, Any]] = Field(
|
|
320
|
+
default_factory=dict,
|
|
321
|
+
description="Scores by category: {category: {score, findings, recommendations}}"
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
opportunities: list[dict[str, Any]] = Field(
|
|
325
|
+
default_factory=list,
|
|
326
|
+
description="Improvement opportunities: [{priority, category, description, effort}]"
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
plan: list[ForgeTask] = Field(
|
|
330
|
+
default_factory=list,
|
|
331
|
+
description="Suggested tasks to close gaps"
|
|
332
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Forge workflow phases."""
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Coherence Checker — validates standards, patterns, and workflow changes.
|
|
2
|
+
|
|
3
|
+
When a new standard is added or the workflow is modified, the coherence
|
|
4
|
+
checker verifies:
|
|
5
|
+
1. No conflicts between standards
|
|
6
|
+
2. No race conditions between agents
|
|
7
|
+
3. No philosophical contradictions
|
|
8
|
+
4. Impact on existing projects
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class CoherenceIssue:
|
|
21
|
+
"""A detected coherence issue."""
|
|
22
|
+
severity: str # error, warning, info
|
|
23
|
+
category: str # conflict, race_condition, philosophy, impact
|
|
24
|
+
description: str
|
|
25
|
+
standard_a: str = ""
|
|
26
|
+
standard_b: str = ""
|
|
27
|
+
suggestion: str = ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class CoherenceReport:
|
|
32
|
+
"""Result of a coherence check."""
|
|
33
|
+
issues: list[CoherenceIssue] = field(default_factory=list)
|
|
34
|
+
passed: bool = True
|
|
35
|
+
|
|
36
|
+
def add_issue(self, issue: CoherenceIssue) -> None:
|
|
37
|
+
self.issues.append(issue)
|
|
38
|
+
if issue.severity == "error":
|
|
39
|
+
self.passed = False
|
|
40
|
+
|
|
41
|
+
def summary(self) -> str:
|
|
42
|
+
if not self.issues:
|
|
43
|
+
return "✓ No coherence issues detected."
|
|
44
|
+
|
|
45
|
+
lines = []
|
|
46
|
+
errors = [i for i in self.issues if i.severity == "error"]
|
|
47
|
+
warnings = [i for i in self.issues if i.severity == "warning"]
|
|
48
|
+
infos = [i for i in self.issues if i.severity == "info"]
|
|
49
|
+
|
|
50
|
+
if errors:
|
|
51
|
+
lines.append(f"✗ {len(errors)} error(s)")
|
|
52
|
+
for e in errors:
|
|
53
|
+
lines.append(f" ERROR [{e.category}]: {e.description}")
|
|
54
|
+
if e.suggestion:
|
|
55
|
+
lines.append(f" → {e.suggestion}")
|
|
56
|
+
|
|
57
|
+
if warnings:
|
|
58
|
+
lines.append(f"⚠ {len(warnings)} warning(s)")
|
|
59
|
+
for w in warnings:
|
|
60
|
+
lines.append(f" WARN [{w.category}]: {w.description}")
|
|
61
|
+
if w.suggestion:
|
|
62
|
+
lines.append(f" → {w.suggestion}")
|
|
63
|
+
|
|
64
|
+
if infos:
|
|
65
|
+
lines.append(f"ℹ {len(infos)} info(s)")
|
|
66
|
+
for i in infos:
|
|
67
|
+
lines.append(f" INFO [{i.category}]: {i.description}")
|
|
68
|
+
|
|
69
|
+
return "\n".join(lines)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def check_standards_coherence(standards: list[dict]) -> CoherenceReport:
|
|
73
|
+
"""Check all standards for internal coherence.
|
|
74
|
+
|
|
75
|
+
Looks for:
|
|
76
|
+
- Contradictory rules (e.g., "always use ORM" vs "always use raw SQL")
|
|
77
|
+
- Overlapping scope (two standards that govern the same area differently)
|
|
78
|
+
- Missing dependencies (standard A requires B but B doesn't exist)
|
|
79
|
+
"""
|
|
80
|
+
report = CoherenceReport()
|
|
81
|
+
|
|
82
|
+
# Group standards by area/category
|
|
83
|
+
by_area: dict[str, list[dict]] = {}
|
|
84
|
+
for std in standards:
|
|
85
|
+
area = std.get("area", std.get("category", "general"))
|
|
86
|
+
by_area.setdefault(area, []).append(std)
|
|
87
|
+
|
|
88
|
+
# Check for overlapping areas with different rules
|
|
89
|
+
for area, stds in by_area.items():
|
|
90
|
+
if len(stds) > 1:
|
|
91
|
+
names = [s.get("name", s.get("_file", "unknown")) for s in stds]
|
|
92
|
+
report.add_issue(CoherenceIssue(
|
|
93
|
+
severity="warning",
|
|
94
|
+
category="overlap",
|
|
95
|
+
description=(
|
|
96
|
+
f"Multiple standards govern '{area}': {names}. "
|
|
97
|
+
f"Verify they don't contradict each other."
|
|
98
|
+
),
|
|
99
|
+
suggestion="Review and consolidate or explicitly define precedence.",
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
# Check for conflicting enforcement levels
|
|
103
|
+
enforcement_standards = [
|
|
104
|
+
s for s in standards
|
|
105
|
+
if "enforcement" in s or "level" in s
|
|
106
|
+
]
|
|
107
|
+
for i, std_a in enumerate(enforcement_standards):
|
|
108
|
+
for std_b in enforcement_standards[i + 1:]:
|
|
109
|
+
area_a = std_a.get("area", "")
|
|
110
|
+
area_b = std_b.get("area", "")
|
|
111
|
+
if area_a == area_b:
|
|
112
|
+
level_a = std_a.get("enforcement", std_a.get("level", ""))
|
|
113
|
+
level_b = std_b.get("enforcement", std_b.get("level", ""))
|
|
114
|
+
if level_a != level_b:
|
|
115
|
+
report.add_issue(CoherenceIssue(
|
|
116
|
+
severity="error",
|
|
117
|
+
category="conflict",
|
|
118
|
+
description=(
|
|
119
|
+
f"Conflicting enforcement for '{area_a}': "
|
|
120
|
+
f"'{level_a}' vs '{level_b}'"
|
|
121
|
+
),
|
|
122
|
+
standard_a=std_a.get("name", ""),
|
|
123
|
+
standard_b=std_b.get("name", ""),
|
|
124
|
+
suggestion="Resolve which enforcement level should apply.",
|
|
125
|
+
))
|
|
126
|
+
|
|
127
|
+
return report
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def check_new_standard(
|
|
131
|
+
new_standard: dict,
|
|
132
|
+
existing_standards: list[dict],
|
|
133
|
+
) -> CoherenceReport:
|
|
134
|
+
"""Check if a new standard is coherent with existing ones.
|
|
135
|
+
|
|
136
|
+
This is called before adding a new standard to verify it
|
|
137
|
+
won't break anything.
|
|
138
|
+
"""
|
|
139
|
+
report = CoherenceReport()
|
|
140
|
+
|
|
141
|
+
new_area = new_standard.get("area", new_standard.get("category", "general"))
|
|
142
|
+
new_name = new_standard.get("name", "new standard")
|
|
143
|
+
|
|
144
|
+
# Check for area conflicts
|
|
145
|
+
for existing in existing_standards:
|
|
146
|
+
ex_area = existing.get("area", existing.get("category", "general"))
|
|
147
|
+
ex_name = existing.get("name", existing.get("_file", "unknown"))
|
|
148
|
+
|
|
149
|
+
if ex_area == new_area:
|
|
150
|
+
report.add_issue(CoherenceIssue(
|
|
151
|
+
severity="warning",
|
|
152
|
+
category="overlap",
|
|
153
|
+
description=(
|
|
154
|
+
f"New standard '{new_name}' covers the same area ('{new_area}') "
|
|
155
|
+
f"as existing standard '{ex_name}'."
|
|
156
|
+
),
|
|
157
|
+
standard_a=new_name,
|
|
158
|
+
standard_b=ex_name,
|
|
159
|
+
suggestion=(
|
|
160
|
+
"Consider merging with the existing standard or "
|
|
161
|
+
"explicitly defining how they interact."
|
|
162
|
+
),
|
|
163
|
+
))
|
|
164
|
+
|
|
165
|
+
# Check for keyword conflicts in rules
|
|
166
|
+
new_rules = set(_extract_keywords(new_standard))
|
|
167
|
+
ex_rules = set(_extract_keywords(existing))
|
|
168
|
+
conflicts = new_rules & ex_rules
|
|
169
|
+
if conflicts and ex_area == new_area:
|
|
170
|
+
report.add_issue(CoherenceIssue(
|
|
171
|
+
severity="info",
|
|
172
|
+
category="overlap",
|
|
173
|
+
description=(
|
|
174
|
+
f"Overlapping keywords between '{new_name}' and "
|
|
175
|
+
f"'{ex_name}': {conflicts}"
|
|
176
|
+
),
|
|
177
|
+
suggestion="Verify these standards don't give contradictory guidance.",
|
|
178
|
+
))
|
|
179
|
+
|
|
180
|
+
return report
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def check_workflow_change(
|
|
184
|
+
change_description: str,
|
|
185
|
+
current_standards: list[dict],
|
|
186
|
+
) -> CoherenceReport:
|
|
187
|
+
"""Check if a workflow-level change would cause issues.
|
|
188
|
+
|
|
189
|
+
This is a higher-level check for when the workflow itself
|
|
190
|
+
changes (not just a standard).
|
|
191
|
+
"""
|
|
192
|
+
report = CoherenceReport()
|
|
193
|
+
|
|
194
|
+
# Basic impact analysis — in a real implementation, this would
|
|
195
|
+
# be much more sophisticated, possibly using an LLM to analyze
|
|
196
|
+
# the change against the full workflow.
|
|
197
|
+
change_lower = change_description.lower()
|
|
198
|
+
|
|
199
|
+
# Check for phase ordering changes
|
|
200
|
+
if any(word in change_lower for word in ["reorder", "phase", "sequence", "before", "after"]):
|
|
201
|
+
report.add_issue(CoherenceIssue(
|
|
202
|
+
severity="warning",
|
|
203
|
+
category="philosophy",
|
|
204
|
+
description=(
|
|
205
|
+
"This change may affect the AI-optimized implementation order. "
|
|
206
|
+
"The current order (types → infra → auth → data → services → "
|
|
207
|
+
"api → frontend → observability → testing → cicd) is designed "
|
|
208
|
+
"to minimize AI hallucinations."
|
|
209
|
+
),
|
|
210
|
+
suggestion=(
|
|
211
|
+
"Verify the new order still provides complete context at each "
|
|
212
|
+
"phase for the AI coding agent."
|
|
213
|
+
),
|
|
214
|
+
))
|
|
215
|
+
|
|
216
|
+
# Check for security changes
|
|
217
|
+
if any(word in change_lower for word in ["auth", "security", "rbac", "permission"]):
|
|
218
|
+
report.add_issue(CoherenceIssue(
|
|
219
|
+
severity="info",
|
|
220
|
+
category="impact",
|
|
221
|
+
description=(
|
|
222
|
+
"Security-related workflow changes may require re-auditing "
|
|
223
|
+
"all existing projects."
|
|
224
|
+
),
|
|
225
|
+
suggestion="Run `forge assess` on existing projects after this change.",
|
|
226
|
+
))
|
|
227
|
+
|
|
228
|
+
return report
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def build_impact_prompt(
|
|
232
|
+
change_description: str,
|
|
233
|
+
current_standards: list[dict],
|
|
234
|
+
affected_projects: list[str],
|
|
235
|
+
) -> str:
|
|
236
|
+
"""Build a prompt for the LLM to do a deep impact analysis.
|
|
237
|
+
|
|
238
|
+
Used when the basic checks aren't enough and we need AI
|
|
239
|
+
to reason about the change's impact.
|
|
240
|
+
"""
|
|
241
|
+
standards_summary = "\n".join(
|
|
242
|
+
f"- {s.get('name', 'unnamed')}: {s.get('description', '')}"
|
|
243
|
+
for s in current_standards
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return f"""You are Forge's coherence analyzer. A workflow change is being proposed.
|
|
247
|
+
|
|
248
|
+
## Proposed Change
|
|
249
|
+
{change_description}
|
|
250
|
+
|
|
251
|
+
## Current Standards
|
|
252
|
+
{standards_summary}
|
|
253
|
+
|
|
254
|
+
## Projects That May Be Affected
|
|
255
|
+
{', '.join(affected_projects) if affected_projects else 'None tracked'}
|
|
256
|
+
|
|
257
|
+
## Analyze For:
|
|
258
|
+
1. **Conflicts**: Does this change contradict any existing standard?
|
|
259
|
+
2. **Race Conditions**: Could this cause agents to produce conflicting outputs?
|
|
260
|
+
3. **Philosophy Changes**: Does this shift the fundamental approach?
|
|
261
|
+
4. **Implementation Impact**: What needs to change in existing projects?
|
|
262
|
+
5. **Breaking Changes**: Will this break anything for users on older versions?
|
|
263
|
+
|
|
264
|
+
Respond with a JSON object:
|
|
265
|
+
{{
|
|
266
|
+
"issues": [
|
|
267
|
+
{{
|
|
268
|
+
"severity": "error|warning|info",
|
|
269
|
+
"category": "conflict|race_condition|philosophy|impact|breaking",
|
|
270
|
+
"description": "...",
|
|
271
|
+
"suggestion": "..."
|
|
272
|
+
}}
|
|
273
|
+
],
|
|
274
|
+
"safe_to_apply": true/false,
|
|
275
|
+
"migration_needed": true/false,
|
|
276
|
+
"migration_steps": ["step 1", "step 2"]
|
|
277
|
+
}}"""
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ── Private helpers ────────────────────────────────────────────────────────
|
|
281
|
+
|
|
282
|
+
def _extract_keywords(standard: dict) -> list[str]:
|
|
283
|
+
"""Extract meaningful keywords from a standard for comparison."""
|
|
284
|
+
keywords = []
|
|
285
|
+
for key in ("rules", "requirements", "guidelines", "must", "must_not"):
|
|
286
|
+
val = standard.get(key, [])
|
|
287
|
+
if isinstance(val, list):
|
|
288
|
+
for item in val:
|
|
289
|
+
if isinstance(item, str):
|
|
290
|
+
keywords.extend(item.lower().split())
|
|
291
|
+
elif isinstance(val, str):
|
|
292
|
+
keywords.extend(val.lower().split())
|
|
293
|
+
return [k for k in keywords if len(k) > 3] # filter noise
|