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/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