faf-python-sdk 1.0.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,224 @@
1
+ Metadata-Version: 2.4
2
+ Name: faf-python-sdk
3
+ Version: 1.0.0
4
+ Summary: Python SDK for FAF (Foundational AI-context Format) - IANA-registered application/vnd.faf+yaml
5
+ Project-URL: Homepage, https://faf.one
6
+ Project-URL: Documentation, https://github.com/Wolfe-Jam/faf-python-sdk
7
+ Project-URL: Repository, https://github.com/Wolfe-Jam/faf-python-sdk
8
+ Project-URL: Issues, https://github.com/Wolfe-Jam/faf-python-sdk/issues
9
+ Author-email: wolfejam <wolfejam@faf.one>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai-context,claude,faf,grok,mcp,project-context,yaml
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Text Processing :: Markup
24
+ Requires-Python: >=3.8
25
+ Requires-Dist: pyyaml>=6.0
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.0; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
29
+ Requires-Dist: pytest>=7.0; extra == 'dev'
30
+ Requires-Dist: types-pyyaml>=6.0; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # faf-sdk
34
+
35
+ Python SDK for **FAF (Foundational AI-context Format)** - the IANA-registered format for AI project context.
36
+
37
+ **Media Type:** `application/vnd.faf+yaml`
38
+
39
+ ## Installation
40
+
41
+ ```bash
42
+ pip install faf-sdk
43
+ ```
44
+
45
+ ## Quick Start
46
+
47
+ ```python
48
+ from faf_sdk import parse, validate, find_faf_file
49
+
50
+ # Find and parse project.faf
51
+ path = find_faf_file()
52
+ if path:
53
+ with open(path) as f:
54
+ faf = parse(f.read())
55
+
56
+ print(f"Project: {faf.project_name}")
57
+ print(f"Score: {faf.score}%")
58
+ print(f"Stack: {faf.data.instant_context.tech_stack}")
59
+ ```
60
+
61
+ ## Core Functions
62
+
63
+ ### Parsing
64
+
65
+ ```python
66
+ from faf_sdk import parse, parse_file, stringify
67
+
68
+ # Parse from string
69
+ faf = parse(yaml_content)
70
+
71
+ # Parse from file
72
+ faf = parse_file("project.faf")
73
+
74
+ # Access typed data
75
+ print(faf.data.project.name)
76
+ print(faf.data.instant_context.what_building)
77
+ print(faf.data.stack.frontend)
78
+
79
+ # Access raw dict
80
+ print(faf.raw["project"]["goal"])
81
+
82
+ # Convert back to YAML
83
+ yaml_str = stringify(faf)
84
+ ```
85
+
86
+ ### Validation
87
+
88
+ ```python
89
+ from faf_sdk import validate
90
+
91
+ result = validate(faf)
92
+
93
+ if result.valid:
94
+ print(f"Valid! Score: {result.score}%")
95
+ else:
96
+ print("Errors:", result.errors)
97
+
98
+ print("Warnings:", result.warnings)
99
+ ```
100
+
101
+ ### File Discovery
102
+
103
+ ```python
104
+ from faf_sdk import find_faf_file, find_project_root, load_fafignore
105
+
106
+ # Find project.faf (walks up directory tree)
107
+ path = find_faf_file("/path/to/src")
108
+
109
+ # Find project root
110
+ root = find_project_root()
111
+
112
+ # Load ignore patterns
113
+ patterns = load_fafignore(root)
114
+ ```
115
+
116
+ ## FAF File Structure
117
+
118
+ A `.faf` file provides instant project context for AI:
119
+
120
+ ```yaml
121
+ faf_version: 2.5.0
122
+ ai_score: 85%
123
+ ai_confidence: HIGH
124
+
125
+ project:
126
+ name: my-project
127
+ goal: Build a CLI tool for data processing
128
+
129
+ instant_context:
130
+ what_building: CLI data processing tool
131
+ tech_stack: Python 3.11, Click, Pandas
132
+ key_files:
133
+ - src/cli.py
134
+ - src/processor.py
135
+
136
+ stack:
137
+ frontend: None
138
+ backend: Python
139
+ database: SQLite
140
+ infrastructure: Docker
141
+
142
+ human_context:
143
+ who: Data analysts
144
+ what: Process CSV files efficiently
145
+ why: Current tools are slow
146
+ ```
147
+
148
+ ## Type Definitions
149
+
150
+ The SDK provides typed access to all FAF sections:
151
+
152
+ ```python
153
+ from faf_sdk import (
154
+ FafData,
155
+ ProjectInfo,
156
+ StackInfo,
157
+ InstantContext,
158
+ ContextQuality,
159
+ HumanContext
160
+ )
161
+
162
+ # All fields are optional except faf_version and project.name
163
+ faf = parse(content)
164
+
165
+ # Typed access
166
+ project: ProjectInfo = faf.data.project
167
+ stack: StackInfo = faf.data.stack
168
+ context: InstantContext = faf.data.instant_context
169
+ ```
170
+
171
+ ## Integration Example
172
+
173
+ ```python
174
+ from faf_sdk import find_faf_file, parse_file, validate
175
+
176
+ def get_project_context():
177
+ """Load project context for AI processing"""
178
+ path = find_faf_file()
179
+ if not path:
180
+ return None
181
+
182
+ faf = parse_file(path)
183
+ result = validate(faf)
184
+
185
+ if not result.valid:
186
+ raise ValueError(f"Invalid FAF: {result.errors}")
187
+
188
+ return {
189
+ "name": faf.data.project.name,
190
+ "goal": faf.data.project.goal,
191
+ "stack": faf.data.instant_context.tech_stack if faf.data.instant_context else None,
192
+ "key_files": faf.data.instant_context.key_files if faf.data.instant_context else [],
193
+ "score": faf.score,
194
+ }
195
+
196
+ # Use in AI context
197
+ context = get_project_context()
198
+ if context:
199
+ print(f"Working on: {context['name']}")
200
+ print(f"Goal: {context['goal']}")
201
+ print(f"Tech: {context['stack']}")
202
+ ```
203
+
204
+ ## Why FAF?
205
+
206
+ Every AI conversation starts from zero. No memory of your project. No understanding of your stack. Just vibes.
207
+
208
+ FAF solves this with a single, IANA-registered file that gives AI instant project context:
209
+
210
+ - **One file, one read, full understanding**
211
+ - **19ms average execution**
212
+ - **Zero setup friction**
213
+ - **MIT licensed, works everywhere**
214
+
215
+ ## Links
216
+
217
+ - **Spec:** [github.com/Wolfe-Jam/faf](https://github.com/Wolfe-Jam/faf)
218
+ - **Site:** [faf.one](https://faf.one)
219
+ - **MCP Server:** [claude-faf-mcp](https://github.com/modelcontextprotocol/servers/tree/main/src/faf)
220
+ - **IANA Registration:** `application/vnd.faf+yaml`
221
+
222
+ ## License
223
+
224
+ MIT
@@ -0,0 +1,9 @@
1
+ faf_sdk/__init__.py,sha256=HIxYtzcWZ3NcO0FoxB2PGStw1R0ZAybBrMVcsdxpRII,1105
2
+ faf_sdk/discovery.py,sha256=3Jj3lSzPAj9l0_VjJneLAwo1dMXvhneFhcVN4m2ejOw,8643
3
+ faf_sdk/parser.py,sha256=mlG5XG5T-JBhgE8udXleKxUt-rb2MOebGkGmCieuNKY,4920
4
+ faf_sdk/types.py,sha256=B0r80EjGGo941u0b_RNyqOmqODEajf-pJ1sWtZ6ew74,6536
5
+ faf_sdk/validator.py,sha256=6uneOwar4GYUF52BnAQTu159kE4mh4RWRgG6onVbiG4,5730
6
+ faf_python_sdk-1.0.0.dist-info/METADATA,sha256=f4k9JchM1mbZHRzavoKtAa5FEpGq3NnRo_Or8uamDGw,5431
7
+ faf_python_sdk-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ faf_python_sdk-1.0.0.dist-info/licenses/LICENSE,sha256=ARScF5tFhbQnYO2V5QAuCwhDHcxKdOWTOV81Pxx_j7U,1065
9
+ faf_python_sdk-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 wolfejam
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
faf_sdk/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """
2
+ FAF Python SDK - Foundational AI-context Format
3
+
4
+ IANA-registered: application/vnd.faf+yaml
5
+ https://faf.one
6
+
7
+ Usage:
8
+ from faf_sdk import parse, validate, find_faf_file, FafFile
9
+
10
+ # Parse a .faf file
11
+ faf = parse(content)
12
+
13
+ # Validate structure
14
+ errors, warnings = validate(faf)
15
+
16
+ # Find project.faf in directory tree
17
+ path = find_faf_file("/path/to/project")
18
+ """
19
+
20
+ from .parser import parse, parse_file, stringify, FafFile
21
+ from .validator import validate, ValidationResult
22
+ from .discovery import find_faf_file, find_project_root, load_fafignore
23
+ from .types import (
24
+ FafData,
25
+ ProjectInfo,
26
+ StackInfo,
27
+ InstantContext,
28
+ ContextQuality,
29
+ HumanContext,
30
+ AIScoring
31
+ )
32
+
33
+ __version__ = "1.0.0"
34
+ __all__ = [
35
+ # Parser
36
+ "parse",
37
+ "parse_file",
38
+ "stringify",
39
+ "FafFile",
40
+ # Validator
41
+ "validate",
42
+ "ValidationResult",
43
+ # Discovery
44
+ "find_faf_file",
45
+ "find_project_root",
46
+ "load_fafignore",
47
+ # Types
48
+ "FafData",
49
+ "ProjectInfo",
50
+ "StackInfo",
51
+ "InstantContext",
52
+ "ContextQuality",
53
+ "HumanContext",
54
+ "AIScoring",
55
+ ]
faf_sdk/discovery.py ADDED
@@ -0,0 +1,378 @@
1
+ """
2
+ FAF file discovery - find .faf files and project roots
3
+
4
+ Mirrors claude-faf-mcp/src/faf-core/utils/file-utils.ts and fafignore-parser.ts
5
+ """
6
+
7
+ import os
8
+ import fnmatch
9
+ from pathlib import Path
10
+ from typing import List, Optional, Tuple
11
+
12
+
13
+ # Default ignore patterns (from TypeScript implementation)
14
+ DEFAULT_IGNORE_PATTERNS = [
15
+ # Dependencies
16
+ "node_modules/",
17
+ "vendor/",
18
+ "bower_components/",
19
+ "__pycache__/",
20
+ "*.pyc",
21
+ ".pytest_cache/",
22
+ "venv/",
23
+ ".venv/",
24
+ "env/",
25
+ ".env/",
26
+
27
+ # Build outputs
28
+ "dist/",
29
+ "build/",
30
+ "out/",
31
+ ".next/",
32
+ ".nuxt/",
33
+ ".svelte-kit/",
34
+ "target/",
35
+ "bin/",
36
+ "obj/",
37
+
38
+ # Version control
39
+ ".git/",
40
+ ".svn/",
41
+ ".hg/",
42
+
43
+ # IDE/Editor
44
+ ".vscode/",
45
+ ".idea/",
46
+ "*.swp",
47
+ "*.swo",
48
+ "*~",
49
+
50
+ # OS files
51
+ ".DS_Store",
52
+ "Thumbs.db",
53
+
54
+ # Logs
55
+ "*.log",
56
+ "logs/",
57
+ "npm-debug.log*",
58
+ "yarn-debug.log*",
59
+ "yarn-error.log*",
60
+
61
+ # Secrets/Config
62
+ ".env",
63
+ ".env.*",
64
+ "*.key",
65
+ "*.pem",
66
+ "*.p12",
67
+ "credentials.json",
68
+ "secrets/",
69
+
70
+ # Test coverage
71
+ "coverage/",
72
+ ".nyc_output/",
73
+ "htmlcov/",
74
+
75
+ # Large media (usually not needed for context)
76
+ "*.jpg",
77
+ "*.jpeg",
78
+ "*.png",
79
+ "*.gif",
80
+ "*.ico",
81
+ "*.svg",
82
+ "*.mp4",
83
+ "*.mp3",
84
+ "*.wav",
85
+ "*.pdf",
86
+ "*.zip",
87
+ "*.tar.gz",
88
+ "*.rar",
89
+
90
+ # Lock files (verbose)
91
+ "package-lock.json",
92
+ "yarn.lock",
93
+ "pnpm-lock.yaml",
94
+ "poetry.lock",
95
+ "Pipfile.lock",
96
+
97
+ # Misc
98
+ ".cache/",
99
+ "tmp/",
100
+ "temp/",
101
+ ]
102
+
103
+
104
+ def find_faf_file(start_dir: Optional[str] = None,
105
+ max_depth: int = 10) -> Optional[str]:
106
+ """
107
+ Find project.faf or .faf file by walking up directory tree
108
+
109
+ Args:
110
+ start_dir: Directory to start search (default: cwd)
111
+ max_depth: Maximum parent directories to check
112
+
113
+ Returns:
114
+ Absolute path to .faf file, or None if not found
115
+
116
+ Example:
117
+ >>> path = find_faf_file("/path/to/project/src")
118
+ >>> if path:
119
+ ... print(f"Found: {path}")
120
+ """
121
+ if start_dir is None:
122
+ start_dir = os.getcwd()
123
+
124
+ current = Path(start_dir).resolve()
125
+
126
+ for _ in range(max_depth):
127
+ # Check for modern project.faf (preferred)
128
+ project_faf = current / "project.faf"
129
+ if project_faf.exists():
130
+ return str(project_faf)
131
+
132
+ # Check for legacy .faf
133
+ legacy_faf = current / ".faf"
134
+ if legacy_faf.exists():
135
+ return str(legacy_faf)
136
+
137
+ # Move up to parent
138
+ parent = current.parent
139
+ if parent == current:
140
+ # Reached filesystem root
141
+ break
142
+ current = parent
143
+
144
+ return None
145
+
146
+
147
+ def find_project_root(start_dir: Optional[str] = None,
148
+ max_depth: int = 10) -> Optional[str]:
149
+ """
150
+ Find project root by looking for common project markers
151
+
152
+ Looks for: package.json, pyproject.toml, Cargo.toml, go.mod, etc.
153
+
154
+ Args:
155
+ start_dir: Directory to start search (default: cwd)
156
+ max_depth: Maximum parent directories to check
157
+
158
+ Returns:
159
+ Absolute path to project root, or None if not found
160
+
161
+ Example:
162
+ >>> root = find_project_root()
163
+ >>> print(f"Project root: {root}")
164
+ """
165
+ if start_dir is None:
166
+ start_dir = os.getcwd()
167
+
168
+ markers = [
169
+ "package.json",
170
+ "pyproject.toml",
171
+ "setup.py",
172
+ "requirements.txt",
173
+ "Cargo.toml",
174
+ "go.mod",
175
+ "pom.xml",
176
+ "build.gradle",
177
+ "Gemfile",
178
+ ".git",
179
+ "project.faf",
180
+ ".faf",
181
+ ]
182
+
183
+ current = Path(start_dir).resolve()
184
+
185
+ for _ in range(max_depth):
186
+ for marker in markers:
187
+ if (current / marker).exists():
188
+ return str(current)
189
+
190
+ parent = current.parent
191
+ if parent == current:
192
+ break
193
+ current = parent
194
+
195
+ return None
196
+
197
+
198
+ def load_fafignore(project_root: str) -> List[str]:
199
+ """
200
+ Load .fafignore patterns from project root
201
+
202
+ Falls back to default patterns if no .fafignore exists.
203
+
204
+ Args:
205
+ project_root: Path to project root directory
206
+
207
+ Returns:
208
+ List of ignore patterns
209
+
210
+ Example:
211
+ >>> patterns = load_fafignore("/path/to/project")
212
+ >>> for p in patterns[:5]:
213
+ ... print(p)
214
+ """
215
+ fafignore_path = Path(project_root) / ".fafignore"
216
+
217
+ if not fafignore_path.exists():
218
+ return DEFAULT_IGNORE_PATTERNS.copy()
219
+
220
+ patterns = []
221
+ try:
222
+ with open(fafignore_path, 'r', encoding='utf-8') as f:
223
+ for line in f:
224
+ line = line.strip()
225
+ # Skip empty lines and comments
226
+ if line and not line.startswith('#'):
227
+ patterns.append(line)
228
+ except IOError:
229
+ return DEFAULT_IGNORE_PATTERNS.copy()
230
+
231
+ return patterns if patterns else DEFAULT_IGNORE_PATTERNS.copy()
232
+
233
+
234
+ def should_ignore(file_path: str, patterns: List[str]) -> bool:
235
+ """
236
+ Check if a file path should be ignored based on patterns
237
+
238
+ Args:
239
+ file_path: Relative file path to check
240
+ patterns: List of ignore patterns
241
+
242
+ Returns:
243
+ True if file should be ignored
244
+
245
+ Example:
246
+ >>> patterns = load_fafignore(root)
247
+ >>> if not should_ignore("src/main.py", patterns):
248
+ ... process_file("src/main.py")
249
+ """
250
+ # Normalize path separators
251
+ file_path = file_path.replace('\\', '/')
252
+
253
+ for pattern in patterns:
254
+ # Handle directory patterns (ending with /)
255
+ if pattern.endswith('/'):
256
+ dir_pattern = pattern.rstrip('/')
257
+ if file_path.startswith(dir_pattern + '/') or file_path == dir_pattern:
258
+ return True
259
+ # Check if any component matches
260
+ parts = file_path.split('/')
261
+ if dir_pattern in parts:
262
+ return True
263
+ else:
264
+ # File pattern
265
+ if fnmatch.fnmatch(file_path, pattern):
266
+ return True
267
+ # Also check basename
268
+ if fnmatch.fnmatch(os.path.basename(file_path), pattern):
269
+ return True
270
+
271
+ return False
272
+
273
+
274
+ def list_project_files(project_root: str,
275
+ ignore_patterns: Optional[List[str]] = None,
276
+ extensions: Optional[List[str]] = None) -> List[str]:
277
+ """
278
+ List all project files, respecting .fafignore
279
+
280
+ Args:
281
+ project_root: Path to project root
282
+ ignore_patterns: Custom ignore patterns (default: load from .fafignore)
283
+ extensions: Filter by extensions (e.g., [".py", ".ts"])
284
+
285
+ Returns:
286
+ List of relative file paths
287
+
288
+ Example:
289
+ >>> files = list_project_files(root, extensions=[".py", ".ts"])
290
+ >>> print(f"Found {len(files)} source files")
291
+ """
292
+ if ignore_patterns is None:
293
+ ignore_patterns = load_fafignore(project_root)
294
+
295
+ root = Path(project_root)
296
+ files = []
297
+
298
+ for path in root.rglob("*"):
299
+ if not path.is_file():
300
+ continue
301
+
302
+ relative = str(path.relative_to(root))
303
+
304
+ # Check ignore patterns
305
+ if should_ignore(relative, ignore_patterns):
306
+ continue
307
+
308
+ # Check extensions filter
309
+ if extensions:
310
+ if path.suffix.lower() not in extensions:
311
+ continue
312
+
313
+ files.append(relative)
314
+
315
+ return sorted(files)
316
+
317
+
318
+ def create_default_fafignore(project_root: str) -> str:
319
+ """
320
+ Create a default .fafignore file
321
+
322
+ Args:
323
+ project_root: Path to project root
324
+
325
+ Returns:
326
+ Path to created .fafignore file
327
+
328
+ Example:
329
+ >>> path = create_default_fafignore(root)
330
+ >>> print(f"Created: {path}")
331
+ """
332
+ fafignore_path = Path(project_root) / ".fafignore"
333
+
334
+ content = [
335
+ "# .fafignore - Files to exclude from FAF context",
336
+ "# Similar to .gitignore syntax",
337
+ "",
338
+ "# Dependencies",
339
+ "node_modules/",
340
+ "__pycache__/",
341
+ "venv/",
342
+ ".venv/",
343
+ "",
344
+ "# Build outputs",
345
+ "dist/",
346
+ "build/",
347
+ ".next/",
348
+ "",
349
+ "# Version control",
350
+ ".git/",
351
+ "",
352
+ "# IDE",
353
+ ".vscode/",
354
+ ".idea/",
355
+ "",
356
+ "# Secrets",
357
+ ".env",
358
+ ".env.*",
359
+ "*.key",
360
+ "*.pem",
361
+ "",
362
+ "# Large files",
363
+ "*.jpg",
364
+ "*.png",
365
+ "*.mp4",
366
+ "*.pdf",
367
+ "*.zip",
368
+ "",
369
+ "# Lock files",
370
+ "package-lock.json",
371
+ "yarn.lock",
372
+ "",
373
+ ]
374
+
375
+ with open(fafignore_path, 'w', encoding='utf-8') as f:
376
+ f.write('\n'.join(content))
377
+
378
+ return str(fafignore_path)
faf_sdk/parser.py ADDED
@@ -0,0 +1,194 @@
1
+ """
2
+ Core FAF parser - YAML parsing with validation
3
+
4
+ Mirrors the TypeScript fix-once/yaml.ts implementation for cross-language compatibility.
5
+ """
6
+
7
+ import yaml
8
+ from typing import Any, Dict, Optional, Union
9
+ from dataclasses import dataclass
10
+
11
+ from .types import FafData
12
+
13
+
14
+ class FafParseError(Exception):
15
+ """Raised when FAF parsing fails"""
16
+ pass
17
+
18
+
19
+ @dataclass
20
+ class FafFile:
21
+ """
22
+ Parsed FAF file with both raw and typed access
23
+
24
+ Attributes:
25
+ data: Typed FafData object with all sections
26
+ raw: Raw dictionary from YAML parsing
27
+ path: Optional file path if loaded from disk
28
+ """
29
+ data: FafData
30
+ raw: Dict[str, Any]
31
+ path: Optional[str] = None
32
+
33
+ @property
34
+ def project_name(self) -> str:
35
+ """Quick access to project name"""
36
+ return self.data.project.name
37
+
38
+ @property
39
+ def score(self) -> Optional[int]:
40
+ """Quick access to AI score"""
41
+ return self.data.ai_score
42
+
43
+ @property
44
+ def version(self) -> str:
45
+ """Quick access to FAF version"""
46
+ return self.data.faf_version
47
+
48
+
49
+ def parse(content: Union[str, None], path: Optional[str] = None) -> FafFile:
50
+ """
51
+ Parse FAF content from string
52
+
53
+ Args:
54
+ content: YAML string content of .faf file
55
+ path: Optional file path for error messages
56
+
57
+ Returns:
58
+ FafFile object with parsed data
59
+
60
+ Raises:
61
+ FafParseError: If content is invalid
62
+
63
+ Example:
64
+ >>> content = open("project.faf").read()
65
+ >>> faf = parse(content)
66
+ >>> print(faf.project_name)
67
+ 'my-project'
68
+ """
69
+ # Handle null/empty content
70
+ if content is None:
71
+ raise FafParseError("Content is null or undefined")
72
+
73
+ if not isinstance(content, str):
74
+ raise FafParseError(f"Content must be string, got {type(content).__name__}")
75
+
76
+ content = content.strip()
77
+ if not content:
78
+ raise FafParseError("Content is empty")
79
+
80
+ # Parse YAML
81
+ try:
82
+ data = yaml.safe_load(content)
83
+ except yaml.YAMLError as e:
84
+ location = f" in {path}" if path else ""
85
+ raise FafParseError(f"Invalid YAML syntax{location}: {e}")
86
+
87
+ # Validate structure
88
+ if data is None:
89
+ raise FafParseError("YAML parsed to null - file may be empty or all comments")
90
+
91
+ if not isinstance(data, dict):
92
+ raise FafParseError(
93
+ f"FAF must be a YAML object/dictionary, got {type(data).__name__}. "
94
+ "Arrays and primitives are not valid FAF files."
95
+ )
96
+
97
+ # Convert to typed structure
98
+ try:
99
+ faf_data = FafData.from_dict(data)
100
+ except Exception as e:
101
+ raise FafParseError(f"Failed to parse FAF structure: {e}")
102
+
103
+ return FafFile(
104
+ data=faf_data,
105
+ raw=data,
106
+ path=path
107
+ )
108
+
109
+
110
+ def parse_file(filepath: str) -> FafFile:
111
+ """
112
+ Parse FAF from file path
113
+
114
+ Args:
115
+ filepath: Path to .faf file
116
+
117
+ Returns:
118
+ FafFile object with parsed data
119
+
120
+ Raises:
121
+ FafParseError: If file cannot be read or parsed
122
+ FileNotFoundError: If file doesn't exist
123
+
124
+ Example:
125
+ >>> faf = parse_file("project.faf")
126
+ >>> print(faf.data.instant_context.tech_stack)
127
+ """
128
+ try:
129
+ with open(filepath, 'r', encoding='utf-8') as f:
130
+ content = f.read()
131
+ except FileNotFoundError:
132
+ raise FileNotFoundError(f"FAF file not found: {filepath}")
133
+ except IOError as e:
134
+ raise FafParseError(f"Failed to read {filepath}: {e}")
135
+
136
+ return parse(content, path=filepath)
137
+
138
+
139
+ def stringify(data: Union[Dict[str, Any], FafFile, FafData],
140
+ default_flow_style: bool = False) -> str:
141
+ """
142
+ Convert FAF data back to YAML string
143
+
144
+ Args:
145
+ data: Dictionary, FafFile, or FafData to serialize
146
+ default_flow_style: Use flow style for collections
147
+
148
+ Returns:
149
+ YAML string
150
+
151
+ Example:
152
+ >>> yaml_str = stringify(faf.raw)
153
+ >>> with open("output.faf", "w") as f:
154
+ ... f.write(yaml_str)
155
+ """
156
+ if isinstance(data, FafFile):
157
+ data = data.raw
158
+ elif isinstance(data, FafData):
159
+ data = data.raw
160
+
161
+ return yaml.dump(
162
+ data,
163
+ default_flow_style=default_flow_style,
164
+ allow_unicode=True,
165
+ sort_keys=False,
166
+ indent=2
167
+ )
168
+
169
+
170
+ def get_field(faf: FafFile, *keys: str, default: Any = None) -> Any:
171
+ """
172
+ Safely get nested field from FAF raw data
173
+
174
+ Args:
175
+ faf: Parsed FafFile
176
+ *keys: Path to field (e.g., "project", "name")
177
+ default: Default value if not found
178
+
179
+ Returns:
180
+ Field value or default
181
+
182
+ Example:
183
+ >>> name = get_field(faf, "project", "name")
184
+ >>> stack = get_field(faf, "stack", "frontend", default="None")
185
+ """
186
+ value = faf.raw
187
+ for key in keys:
188
+ if isinstance(value, dict):
189
+ value = value.get(key)
190
+ else:
191
+ return default
192
+ if value is None:
193
+ return default
194
+ return value
faf_sdk/types.py ADDED
@@ -0,0 +1,211 @@
1
+ """
2
+ Type definitions for FAF (Foundational AI-context Format)
3
+
4
+ These mirror the TypeScript definitions in faf-cli for cross-language compatibility.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ @dataclass
12
+ class ProjectInfo:
13
+ """Core project metadata"""
14
+ name: str
15
+ goal: Optional[str] = None
16
+ main_language: Optional[str] = None
17
+ approach: Optional[str] = None
18
+ version: Optional[str] = None
19
+ license: Optional[str] = None
20
+
21
+
22
+ @dataclass
23
+ class StackInfo:
24
+ """Technical stack breakdown"""
25
+ frontend: Optional[str] = None
26
+ backend: Optional[str] = None
27
+ database: Optional[str] = None
28
+ infrastructure: Optional[str] = None
29
+ build_tool: Optional[str] = None
30
+ testing: Optional[str] = None
31
+ cicd: Optional[str] = None
32
+
33
+
34
+ @dataclass
35
+ class InstantContext:
36
+ """Quick context for AI understanding"""
37
+ what_building: Optional[str] = None
38
+ tech_stack: Optional[str] = None
39
+ deployment: Optional[str] = None
40
+ key_files: List[str] = field(default_factory=list)
41
+ commands: Dict[str, str] = field(default_factory=dict)
42
+
43
+
44
+ @dataclass
45
+ class ContextQuality:
46
+ """Quality metrics for the FAF file"""
47
+ slots_filled: Optional[str] = None
48
+ confidence: Optional[str] = None
49
+ handoff_ready: bool = False
50
+ missing_context: List[str] = field(default_factory=list)
51
+
52
+
53
+ @dataclass
54
+ class HumanContext:
55
+ """The 6 W's - human-readable context"""
56
+ who: Optional[str] = None # Target users
57
+ what: Optional[str] = None # Core problem
58
+ why: Optional[str] = None # Mission/purpose
59
+ how: Optional[str] = None # Approach
60
+ where: Optional[str] = None # Deployment
61
+ when: Optional[str] = None # Timeline
62
+
63
+
64
+ @dataclass
65
+ class AIScoring:
66
+ """AI-readiness scoring system"""
67
+ score: Optional[int] = None # 0-100
68
+ confidence: Optional[str] = None # LOW, MEDIUM, HIGH
69
+ version: Optional[str] = None
70
+
71
+
72
+ @dataclass
73
+ class AIInstructions:
74
+ """Instructions for AI assistants"""
75
+ working_style: Optional[str] = None
76
+ quality_bar: Optional[str] = None
77
+ warnings: List[str] = field(default_factory=list)
78
+ focus_areas: List[str] = field(default_factory=list)
79
+
80
+
81
+ @dataclass
82
+ class Preferences:
83
+ """Development preferences"""
84
+ quality_bar: Optional[str] = None
85
+ testing: Optional[str] = None
86
+ documentation: Optional[str] = None
87
+ code_style: Optional[str] = None
88
+
89
+
90
+ @dataclass
91
+ class State:
92
+ """Project state tracking"""
93
+ phase: Optional[str] = None
94
+ version: Optional[str] = None
95
+ focus: Optional[str] = None
96
+ milestones: List[str] = field(default_factory=list)
97
+
98
+
99
+ @dataclass
100
+ class FafData:
101
+ """
102
+ Complete FAF file structure
103
+
104
+ Represents the full parsed content of a .faf file.
105
+ All fields are optional except faf_version and project.
106
+ """
107
+ faf_version: str
108
+ project: ProjectInfo
109
+
110
+ # Optional sections
111
+ ai_score: Optional[int] = None
112
+ ai_confidence: Optional[str] = None
113
+ ai_tldr: Optional[Dict[str, str]] = None
114
+ instant_context: Optional[InstantContext] = None
115
+ context_quality: Optional[ContextQuality] = None
116
+ stack: Optional[StackInfo] = None
117
+ human_context: Optional[HumanContext] = None
118
+ ai_instructions: Optional[AIInstructions] = None
119
+ preferences: Optional[Preferences] = None
120
+ state: Optional[State] = None
121
+ tags: List[str] = field(default_factory=list)
122
+
123
+ # Raw data for unrecognized fields
124
+ raw: Dict[str, Any] = field(default_factory=dict)
125
+
126
+ @classmethod
127
+ def from_dict(cls, data: Dict[str, Any]) -> "FafData":
128
+ """Create FafData from parsed YAML dictionary"""
129
+ project_data = data.get("project", {})
130
+ if isinstance(project_data, str):
131
+ project_data = {"name": project_data}
132
+
133
+ project = ProjectInfo(
134
+ name=project_data.get("name", "unknown"),
135
+ goal=project_data.get("goal"),
136
+ main_language=project_data.get("main_language"),
137
+ approach=project_data.get("approach"),
138
+ version=project_data.get("version"),
139
+ license=project_data.get("license")
140
+ )
141
+
142
+ # Parse instant_context
143
+ instant_ctx = None
144
+ if "instant_context" in data:
145
+ ic = data["instant_context"]
146
+ instant_ctx = InstantContext(
147
+ what_building=ic.get("what_building"),
148
+ tech_stack=ic.get("tech_stack"),
149
+ deployment=ic.get("deployment"),
150
+ key_files=ic.get("key_files", []),
151
+ commands=ic.get("commands", {})
152
+ )
153
+
154
+ # Parse stack
155
+ stack = None
156
+ if "stack" in data:
157
+ s = data["stack"]
158
+ stack = StackInfo(
159
+ frontend=s.get("frontend"),
160
+ backend=s.get("backend"),
161
+ database=s.get("database"),
162
+ infrastructure=s.get("infrastructure"),
163
+ build_tool=s.get("build_tool"),
164
+ testing=s.get("testing"),
165
+ cicd=s.get("cicd")
166
+ )
167
+
168
+ # Parse context_quality
169
+ ctx_quality = None
170
+ if "context_quality" in data:
171
+ cq = data["context_quality"]
172
+ ctx_quality = ContextQuality(
173
+ slots_filled=cq.get("slots_filled"),
174
+ confidence=cq.get("confidence"),
175
+ handoff_ready=cq.get("handoff_ready", False),
176
+ missing_context=cq.get("missing_context", [])
177
+ )
178
+
179
+ # Parse human_context
180
+ human_ctx = None
181
+ if "human_context" in data:
182
+ hc = data["human_context"]
183
+ human_ctx = HumanContext(
184
+ who=hc.get("who"),
185
+ what=hc.get("what"),
186
+ why=hc.get("why"),
187
+ how=hc.get("how"),
188
+ where=hc.get("where"),
189
+ when=hc.get("when")
190
+ )
191
+
192
+ # Parse AI score
193
+ ai_score = data.get("ai_score")
194
+ if isinstance(ai_score, str) and ai_score.endswith("%"):
195
+ ai_score = int(ai_score.rstrip("%"))
196
+
197
+ return cls(
198
+ faf_version=data.get("faf_version", "2.5.0"),
199
+ project=project,
200
+ ai_score=ai_score,
201
+ ai_confidence=data.get("ai_confidence"),
202
+ ai_tldr=data.get("ai_tldr"),
203
+ instant_context=instant_ctx,
204
+ context_quality=ctx_quality,
205
+ stack=stack,
206
+ human_context=human_ctx,
207
+ preferences=None, # Add parsing if needed
208
+ state=None, # Add parsing if needed
209
+ tags=data.get("tags", []),
210
+ raw=data
211
+ )
faf_sdk/validator.py ADDED
@@ -0,0 +1,213 @@
1
+ """
2
+ FAF validation - check structure and completeness
3
+
4
+ Mirrors claude-faf-mcp/src/faf-core/commands/validate.ts
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, Dict, List, Tuple, Union
9
+
10
+ from .parser import FafFile, parse
11
+
12
+
13
+ @dataclass
14
+ class ValidationResult:
15
+ """
16
+ Result of FAF validation
17
+
18
+ Attributes:
19
+ valid: True if no errors (warnings OK)
20
+ errors: List of critical errors
21
+ warnings: List of non-critical warnings
22
+ score: Calculated completeness score (0-100)
23
+ """
24
+ valid: bool
25
+ errors: List[str] = field(default_factory=list)
26
+ warnings: List[str] = field(default_factory=list)
27
+ score: int = 0
28
+
29
+ def __bool__(self) -> bool:
30
+ return self.valid
31
+
32
+
33
+ def validate(faf: Union[FafFile, Dict[str, Any], str]) -> ValidationResult:
34
+ """
35
+ Validate FAF file structure and completeness
36
+
37
+ Args:
38
+ faf: FafFile, raw dict, or YAML string to validate
39
+
40
+ Returns:
41
+ ValidationResult with errors, warnings, and score
42
+
43
+ Example:
44
+ >>> result = validate(faf)
45
+ >>> if not result.valid:
46
+ ... print("Errors:", result.errors)
47
+ >>> print(f"Score: {result.score}%")
48
+ """
49
+ errors: List[str] = []
50
+ warnings: List[str] = []
51
+
52
+ # Parse if string
53
+ if isinstance(faf, str):
54
+ try:
55
+ faf = parse(faf)
56
+ except Exception as e:
57
+ return ValidationResult(
58
+ valid=False,
59
+ errors=[f"Parse error: {e}"],
60
+ score=0
61
+ )
62
+
63
+ # Get raw data
64
+ if isinstance(faf, FafFile):
65
+ data = faf.raw
66
+ else:
67
+ data = faf
68
+
69
+ # Required fields
70
+ if "faf_version" not in data:
71
+ errors.append("Missing required field: faf_version")
72
+
73
+ if "project" not in data:
74
+ errors.append("Missing required field: project")
75
+ else:
76
+ project = data["project"]
77
+ if isinstance(project, dict):
78
+ if "name" not in project:
79
+ errors.append("Missing required field: project.name")
80
+ elif not isinstance(project, str):
81
+ errors.append("project must be object or string")
82
+
83
+ # Recommended sections
84
+ if "instant_context" not in data:
85
+ warnings.append("Missing recommended section: instant_context")
86
+ else:
87
+ ic = data["instant_context"]
88
+ if isinstance(ic, dict):
89
+ if "what_building" not in ic:
90
+ warnings.append("Missing instant_context.what_building")
91
+ if "tech_stack" not in ic:
92
+ warnings.append("Missing instant_context.tech_stack")
93
+
94
+ if "stack" not in data:
95
+ warnings.append("Missing recommended section: stack")
96
+
97
+ # Optional but useful
98
+ if "human_context" not in data:
99
+ warnings.append("Missing section: human_context (the 6 W's)")
100
+
101
+ if "ai_instructions" not in data:
102
+ warnings.append("Missing section: ai_instructions")
103
+
104
+ # Type validations
105
+ if "tags" in data and not isinstance(data["tags"], list):
106
+ errors.append("tags must be an array")
107
+
108
+ if "ai_score" in data:
109
+ score_val = data["ai_score"]
110
+ if isinstance(score_val, str):
111
+ if not score_val.endswith("%"):
112
+ warnings.append("ai_score should end with % (e.g., '85%')")
113
+ elif not isinstance(score_val, (int, float)):
114
+ errors.append("ai_score must be number or percentage string")
115
+
116
+ # Calculate completeness score
117
+ score = _calculate_score(data)
118
+
119
+ return ValidationResult(
120
+ valid=len(errors) == 0,
121
+ errors=errors,
122
+ warnings=warnings,
123
+ score=score
124
+ )
125
+
126
+
127
+ def _calculate_score(data: Dict[str, Any]) -> int:
128
+ """
129
+ Calculate completeness score based on filled sections
130
+
131
+ Scoring breakdown:
132
+ - Required fields (30 points)
133
+ - Core sections (40 points)
134
+ - Extended sections (30 points)
135
+ """
136
+ score = 0
137
+ max_score = 100
138
+
139
+ # Required fields (30 points)
140
+ if "faf_version" in data:
141
+ score += 10
142
+ if "project" in data:
143
+ score += 10
144
+ project = data["project"]
145
+ if isinstance(project, dict):
146
+ if project.get("name"):
147
+ score += 5
148
+ if project.get("goal"):
149
+ score += 5
150
+
151
+ # Core sections (40 points)
152
+ if "instant_context" in data:
153
+ ic = data["instant_context"]
154
+ score += 5
155
+ if isinstance(ic, dict):
156
+ if ic.get("what_building"):
157
+ score += 5
158
+ if ic.get("tech_stack"):
159
+ score += 5
160
+ if ic.get("key_files"):
161
+ score += 5
162
+
163
+ if "stack" in data:
164
+ score += 10
165
+ stack = data["stack"]
166
+ if isinstance(stack, dict) and len(stack) > 2:
167
+ score += 5
168
+
169
+ if "context_quality" in data:
170
+ score += 5
171
+
172
+ # Extended sections (30 points)
173
+ if "human_context" in data:
174
+ score += 10
175
+
176
+ if "ai_instructions" in data:
177
+ score += 5
178
+
179
+ if "preferences" in data:
180
+ score += 5
181
+
182
+ if "state" in data:
183
+ score += 5
184
+
185
+ if data.get("tags"):
186
+ score += 5
187
+
188
+ return min(score, max_score)
189
+
190
+
191
+ def validate_quick(content: str) -> Tuple[bool, str]:
192
+ """
193
+ Quick validation returning simple pass/fail with message
194
+
195
+ Args:
196
+ content: YAML string to validate
197
+
198
+ Returns:
199
+ Tuple of (valid, message)
200
+
201
+ Example:
202
+ >>> valid, msg = validate_quick(yaml_content)
203
+ >>> if not valid:
204
+ ... print(f"Invalid: {msg}")
205
+ """
206
+ result = validate(content)
207
+
208
+ if not result.valid:
209
+ return False, f"Invalid: {'; '.join(result.errors)}"
210
+ elif result.warnings:
211
+ return True, f"Valid with warnings: {'; '.join(result.warnings[:2])}"
212
+ else:
213
+ return True, f"Valid (score: {result.score}%)"