faf-python-sdk 1.0.2__tar.gz → 1.1.0__tar.gz

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.
Files changed (26) hide show
  1. faf_python_sdk-1.1.0/PKG-INFO +180 -0
  2. faf_python_sdk-1.1.0/README.md +148 -0
  3. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/docs/GROK-INTEGRATION.md +1 -1
  4. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/faf_sdk/__init__.py +7 -1
  5. faf_python_sdk-1.1.0/faf_sdk/mk4.py +214 -0
  6. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/faf_sdk/parser.py +1 -1
  7. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/faf_sdk/types.py +2 -0
  8. faf_python_sdk-1.1.0/project.faf +25 -0
  9. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/pyproject.toml +2 -2
  10. faf_python_sdk-1.1.0/tests/test_mk4.py +616 -0
  11. faf_python_sdk-1.1.0/tests/test_wjttc.py +798 -0
  12. faf_python_sdk-1.0.2/PKG-INFO +0 -224
  13. faf_python_sdk-1.0.2/README.md +0 -192
  14. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/.github/workflows/pypi.yml +0 -0
  15. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/.gitignore +0 -0
  16. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/LICENSE +0 -0
  17. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/docs/TECHNICAL-SPEC.md +0 -0
  18. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/examples/basic_usage.py +0 -0
  19. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/examples/grok_integration.py +0 -0
  20. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/faf_sdk/discovery.py +0 -0
  21. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/faf_sdk/validator.py +0 -0
  22. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/tests/__init__.py +0 -0
  23. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/tests/stress_test.py +0 -0
  24. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/tests/test_discovery.py +0 -0
  25. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/tests/test_parser.py +0 -0
  26. {faf_python_sdk-1.0.2 → faf_python_sdk-1.1.0}/tests/test_validator.py +0 -0
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: faf-python-sdk
3
+ Version: 1.1.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-python-sdk
34
+
35
+ > **Python SDK for FAF** — parse, validate, and score `.faf` files with the Mk4 Championship Scoring Engine. The foundation for [gemini-faf-mcp](https://pypi.org/project/gemini-faf-mcp/).
36
+
37
+ [![PyPI](https://img.shields.io/pypi/v/faf-python-sdk?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/faf-python-sdk/)
38
+ [![Downloads](https://img.shields.io/pypi/dm/faf-python-sdk?style=for-the-badge&color=blue)](https://pypi.org/project/faf-python-sdk/)
39
+ [![Tests](https://img.shields.io/badge/tests-175%20passing-brightgreen?style=for-the-badge)](https://github.com/Wolfe-Jam/faf-python-sdk)
40
+ [![IANA](https://img.shields.io/badge/IANA-registered-informational?style=for-the-badge)](https://www.iana.org/assignments/media-types/application/vnd.faf+yaml)
41
+
42
+ **Media Type:** `application/vnd.faf+yaml` (IANA registered)
43
+
44
+ ## What's New in v1.1.0
45
+
46
+ **Mk4 Championship Scoring Engine** — the same 33-slot scoring algorithm used by the Rust compiler and TypeScript CLI, now in Python. Same slots, same formula, same scores. Every FAF tool in every language now agrees on what 100% means.
47
+
48
+ - `score_faf()` — Mk4 scoring with 21-slot Base or 33-slot Enterprise tiers
49
+ - 100% parity with `faf-wasm-sdk` (Rust) and `faf-cli` (TypeScript)
50
+ - 3 crash bugs fixed (malformed YAML, null project fields)
51
+ - 175 tests including 88 WJTTC championship-grade tests (concurrency, adversarial input, security)
52
+
53
+ **Why this matters:** If you're building on FAF in Python — MCP servers, Gemini extensions, CI pipelines — your scores now match every other FAF tool exactly. No more "it scored 85% in the CLI but 60% in Python." One engine, one truth.
54
+
55
+ ## Installation
56
+
57
+ ```bash
58
+ pip install faf-python-sdk
59
+ ```
60
+
61
+ ## Quick Start
62
+
63
+ ```python
64
+ from faf_sdk import parse_file, score_faf
65
+
66
+ # Parse a .faf file
67
+ faf = parse_file("project.faf")
68
+ print(f"Project: {faf.project_name}")
69
+
70
+ # Score it with the Mk4 engine
71
+ with open("project.faf") as f:
72
+ result = score_faf(f.read())
73
+
74
+ print(f"Score: {result.score}% {result.tier}")
75
+ print(f"Slots: {result.populated}/{result.total} populated")
76
+ ```
77
+
78
+ ## Mk4 Scoring
79
+
80
+ The Mk4 engine scores `.faf` files by checking 21 universal slots (project metadata, human context, tech stack). Each slot is **Populated**, **Empty**, or **Slotignored**. The score is the percentage of active slots that are populated.
81
+
82
+ ```python
83
+ from faf_sdk import score_faf, LicenseTier
84
+
85
+ # Base scoring (21 slots)
86
+ result = score_faf(yaml_content)
87
+ print(result.score) # 0-100
88
+ print(result.tier) # Trophy/Gold/Silver/Bronze/Green/Yellow/Red
89
+ print(result.populated) # slots with real data
90
+ print(result.active) # total minus slotignored
91
+ print(result.slots) # per-slot breakdown
92
+
93
+ # Enterprise scoring (33 slots — adds monorepo/infra)
94
+ result = score_faf(yaml_content, LicenseTier.ENTERPRISE)
95
+ ```
96
+
97
+ **Placeholder rejection:** Values like `"null"`, `"unknown"`, `"n/a"`, `"Describe your project goal"` are detected and scored as Empty — not Populated.
98
+
99
+ **Slotignored:** Set any slot to `slotignored` to exclude it from scoring. A backend-only project can mark `frontend: slotignored` and still reach 100%.
100
+
101
+ ## Parsing
102
+
103
+ ```python
104
+ from faf_sdk import parse, parse_file, stringify
105
+
106
+ # Parse from string or file
107
+ faf = parse(yaml_content)
108
+ faf = parse_file("project.faf")
109
+
110
+ # Typed access
111
+ print(faf.data.project.name)
112
+ print(faf.data.project.goal)
113
+ print(faf.data.stack.backend)
114
+ print(faf.data.human_context.who)
115
+
116
+ # Raw dict access
117
+ print(faf.raw["project"]["goal"])
118
+
119
+ # Convert back to YAML
120
+ yaml_str = stringify(faf)
121
+ ```
122
+
123
+ ## Validation
124
+
125
+ ```python
126
+ from faf_sdk import validate
127
+
128
+ result = validate(faf)
129
+
130
+ if result.valid:
131
+ print(f"Valid! Score: {result.score}%")
132
+ else:
133
+ print("Errors:", result.errors)
134
+
135
+ print("Warnings:", result.warnings)
136
+ ```
137
+
138
+ ## File Discovery
139
+
140
+ ```python
141
+ from faf_sdk import find_faf_file, find_project_root
142
+
143
+ # Find project.faf (walks up directory tree)
144
+ path = find_faf_file("/path/to/src")
145
+
146
+ # Find project root by markers (package.json, pyproject.toml, .git, etc.)
147
+ root = find_project_root()
148
+ ```
149
+
150
+ ## API Reference
151
+
152
+ | Function | Returns | Description |
153
+ |----------|---------|-------------|
154
+ | `score_faf(yaml, tier?)` | `Mk4Result` | Mk4 score (21 or 33 slots) |
155
+ | `parse(content)` | `FafFile` | Parse YAML string |
156
+ | `parse_file(path)` | `FafFile` | Parse from file path |
157
+ | `validate(faf)` | `ValidationResult` | Structure validation + warnings |
158
+ | `stringify(data)` | `str` | Convert back to YAML |
159
+ | `find_faf_file(dir?)` | `str \| None` | Find project.faf in tree |
160
+ | `find_project_root(dir?)` | `str \| None` | Find project root |
161
+
162
+ ## FAF Ecosystem
163
+
164
+ | Package | Platform | Registry |
165
+ |---------|----------|----------|
166
+ | **faf-python-sdk** | **Python foundation** | **PyPI** |
167
+ | [gemini-faf-mcp](https://pypi.org/project/gemini-faf-mcp/) | Google Gemini | PyPI |
168
+ | [claude-faf-mcp](https://npmjs.com/package/claude-faf-mcp) | Anthropic | npm + MCP #2759 |
169
+ | [grok-faf-mcp](https://npmjs.com/package/grok-faf-mcp) | xAI | npm |
170
+ | [faf-cli](https://npmjs.com/package/faf-cli) | CLI | npm |
171
+
172
+ ## Links
173
+
174
+ - **Site:** [faf.one](https://faf.one)
175
+ - **IANA Registration:** [application/vnd.faf+yaml](https://www.iana.org/assignments/media-types/application/vnd.faf+yaml)
176
+ - **Gemini MCP:** [gemini-faf-mcp](https://pypi.org/project/gemini-faf-mcp/)
177
+
178
+ ## License
179
+
180
+ MIT
@@ -0,0 +1,148 @@
1
+ # faf-python-sdk
2
+
3
+ > **Python SDK for FAF** — parse, validate, and score `.faf` files with the Mk4 Championship Scoring Engine. The foundation for [gemini-faf-mcp](https://pypi.org/project/gemini-faf-mcp/).
4
+
5
+ [![PyPI](https://img.shields.io/pypi/v/faf-python-sdk?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/faf-python-sdk/)
6
+ [![Downloads](https://img.shields.io/pypi/dm/faf-python-sdk?style=for-the-badge&color=blue)](https://pypi.org/project/faf-python-sdk/)
7
+ [![Tests](https://img.shields.io/badge/tests-175%20passing-brightgreen?style=for-the-badge)](https://github.com/Wolfe-Jam/faf-python-sdk)
8
+ [![IANA](https://img.shields.io/badge/IANA-registered-informational?style=for-the-badge)](https://www.iana.org/assignments/media-types/application/vnd.faf+yaml)
9
+
10
+ **Media Type:** `application/vnd.faf+yaml` (IANA registered)
11
+
12
+ ## What's New in v1.1.0
13
+
14
+ **Mk4 Championship Scoring Engine** — the same 33-slot scoring algorithm used by the Rust compiler and TypeScript CLI, now in Python. Same slots, same formula, same scores. Every FAF tool in every language now agrees on what 100% means.
15
+
16
+ - `score_faf()` — Mk4 scoring with 21-slot Base or 33-slot Enterprise tiers
17
+ - 100% parity with `faf-wasm-sdk` (Rust) and `faf-cli` (TypeScript)
18
+ - 3 crash bugs fixed (malformed YAML, null project fields)
19
+ - 175 tests including 88 WJTTC championship-grade tests (concurrency, adversarial input, security)
20
+
21
+ **Why this matters:** If you're building on FAF in Python — MCP servers, Gemini extensions, CI pipelines — your scores now match every other FAF tool exactly. No more "it scored 85% in the CLI but 60% in Python." One engine, one truth.
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install faf-python-sdk
27
+ ```
28
+
29
+ ## Quick Start
30
+
31
+ ```python
32
+ from faf_sdk import parse_file, score_faf
33
+
34
+ # Parse a .faf file
35
+ faf = parse_file("project.faf")
36
+ print(f"Project: {faf.project_name}")
37
+
38
+ # Score it with the Mk4 engine
39
+ with open("project.faf") as f:
40
+ result = score_faf(f.read())
41
+
42
+ print(f"Score: {result.score}% {result.tier}")
43
+ print(f"Slots: {result.populated}/{result.total} populated")
44
+ ```
45
+
46
+ ## Mk4 Scoring
47
+
48
+ The Mk4 engine scores `.faf` files by checking 21 universal slots (project metadata, human context, tech stack). Each slot is **Populated**, **Empty**, or **Slotignored**. The score is the percentage of active slots that are populated.
49
+
50
+ ```python
51
+ from faf_sdk import score_faf, LicenseTier
52
+
53
+ # Base scoring (21 slots)
54
+ result = score_faf(yaml_content)
55
+ print(result.score) # 0-100
56
+ print(result.tier) # Trophy/Gold/Silver/Bronze/Green/Yellow/Red
57
+ print(result.populated) # slots with real data
58
+ print(result.active) # total minus slotignored
59
+ print(result.slots) # per-slot breakdown
60
+
61
+ # Enterprise scoring (33 slots — adds monorepo/infra)
62
+ result = score_faf(yaml_content, LicenseTier.ENTERPRISE)
63
+ ```
64
+
65
+ **Placeholder rejection:** Values like `"null"`, `"unknown"`, `"n/a"`, `"Describe your project goal"` are detected and scored as Empty — not Populated.
66
+
67
+ **Slotignored:** Set any slot to `slotignored` to exclude it from scoring. A backend-only project can mark `frontend: slotignored` and still reach 100%.
68
+
69
+ ## Parsing
70
+
71
+ ```python
72
+ from faf_sdk import parse, parse_file, stringify
73
+
74
+ # Parse from string or file
75
+ faf = parse(yaml_content)
76
+ faf = parse_file("project.faf")
77
+
78
+ # Typed access
79
+ print(faf.data.project.name)
80
+ print(faf.data.project.goal)
81
+ print(faf.data.stack.backend)
82
+ print(faf.data.human_context.who)
83
+
84
+ # Raw dict access
85
+ print(faf.raw["project"]["goal"])
86
+
87
+ # Convert back to YAML
88
+ yaml_str = stringify(faf)
89
+ ```
90
+
91
+ ## Validation
92
+
93
+ ```python
94
+ from faf_sdk import validate
95
+
96
+ result = validate(faf)
97
+
98
+ if result.valid:
99
+ print(f"Valid! Score: {result.score}%")
100
+ else:
101
+ print("Errors:", result.errors)
102
+
103
+ print("Warnings:", result.warnings)
104
+ ```
105
+
106
+ ## File Discovery
107
+
108
+ ```python
109
+ from faf_sdk import find_faf_file, find_project_root
110
+
111
+ # Find project.faf (walks up directory tree)
112
+ path = find_faf_file("/path/to/src")
113
+
114
+ # Find project root by markers (package.json, pyproject.toml, .git, etc.)
115
+ root = find_project_root()
116
+ ```
117
+
118
+ ## API Reference
119
+
120
+ | Function | Returns | Description |
121
+ |----------|---------|-------------|
122
+ | `score_faf(yaml, tier?)` | `Mk4Result` | Mk4 score (21 or 33 slots) |
123
+ | `parse(content)` | `FafFile` | Parse YAML string |
124
+ | `parse_file(path)` | `FafFile` | Parse from file path |
125
+ | `validate(faf)` | `ValidationResult` | Structure validation + warnings |
126
+ | `stringify(data)` | `str` | Convert back to YAML |
127
+ | `find_faf_file(dir?)` | `str \| None` | Find project.faf in tree |
128
+ | `find_project_root(dir?)` | `str \| None` | Find project root |
129
+
130
+ ## FAF Ecosystem
131
+
132
+ | Package | Platform | Registry |
133
+ |---------|----------|----------|
134
+ | **faf-python-sdk** | **Python foundation** | **PyPI** |
135
+ | [gemini-faf-mcp](https://pypi.org/project/gemini-faf-mcp/) | Google Gemini | PyPI |
136
+ | [claude-faf-mcp](https://npmjs.com/package/claude-faf-mcp) | Anthropic | npm + MCP #2759 |
137
+ | [grok-faf-mcp](https://npmjs.com/package/grok-faf-mcp) | xAI | npm |
138
+ | [faf-cli](https://npmjs.com/package/faf-cli) | CLI | npm |
139
+
140
+ ## Links
141
+
142
+ - **Site:** [faf.one](https://faf.one)
143
+ - **IANA Registration:** [application/vnd.faf+yaml](https://www.iana.org/assignments/media-types/application/vnd.faf+yaml)
144
+ - **Gemini MCP:** [gemini-faf-mcp](https://pypi.org/project/gemini-faf-mcp/)
145
+
146
+ ## License
147
+
148
+ MIT
@@ -284,7 +284,7 @@ Common errors:
284
284
  The SDK includes comprehensive tests:
285
285
 
286
286
  ```bash
287
- pip install faf-sdk[dev]
287
+ pip install faf-python-sdk[dev]
288
288
  pytest tests/ -v
289
289
  ```
290
290
 
@@ -19,6 +19,7 @@ Usage:
19
19
 
20
20
  from .parser import parse, parse_file, stringify, FafFile
21
21
  from .validator import validate, ValidationResult
22
+ from .mk4 import score_faf, Mk4Result, SlotState, LicenseTier
22
23
  from .discovery import find_faf_file, find_project_root, load_fafignore
23
24
  from .types import (
24
25
  FafData,
@@ -30,7 +31,7 @@ from .types import (
30
31
  AIScoring
31
32
  )
32
33
 
33
- __version__ = "1.0.0"
34
+ __version__ = "1.1.0"
34
35
  __all__ = [
35
36
  # Parser
36
37
  "parse",
@@ -40,6 +41,11 @@ __all__ = [
40
41
  # Validator
41
42
  "validate",
42
43
  "ValidationResult",
44
+ # Mk4 Scoring Engine
45
+ "score_faf",
46
+ "Mk4Result",
47
+ "SlotState",
48
+ "LicenseTier",
43
49
  # Discovery
44
50
  "find_faf_file",
45
51
  "find_project_root",
@@ -0,0 +1,214 @@
1
+ """
2
+ Mk4 Championship Engine — 33-Slot Scoring
3
+
4
+ Ported from faf-wasm-sdk/src/mk4.rs (100% parity).
5
+ Philosophy: Populated, Empty, or Slotignored.
6
+ """
7
+
8
+ from dataclasses import dataclass, field
9
+ from enum import Enum
10
+ from typing import List, Tuple, Dict, Any
11
+
12
+ import yaml
13
+
14
+
15
+ class SlotState(Enum):
16
+ EMPTY = "empty"
17
+ POPULATED = "populated"
18
+ SLOTIGNORED = "slotignored"
19
+
20
+
21
+ class LicenseTier(Enum):
22
+ BASE = "base"
23
+ ENTERPRISE = "enterprise"
24
+
25
+
26
+ @dataclass
27
+ class Mk4Result:
28
+ score: int
29
+ tier: str
30
+ populated: int
31
+ ignored: int
32
+ active: int
33
+ total: int
34
+ slots: List[Tuple[str, SlotState]]
35
+
36
+ def to_dict(self) -> Dict[str, Any]:
37
+ return {
38
+ "score": self.score,
39
+ "tier": self.tier,
40
+ "populated": self.populated,
41
+ "empty": self.total - self.populated - self.ignored,
42
+ "ignored": self.ignored,
43
+ "active": self.active,
44
+ "total": self.total,
45
+ "slots": {name: state.value for name, state in self.slots},
46
+ }
47
+
48
+
49
+ # 8 placeholder strings — case-insensitive rejection (mk4.rs lines 224-233)
50
+ _PLACEHOLDERS = frozenset([
51
+ "describe your project goal",
52
+ "development teams",
53
+ "cloud platform",
54
+ "null",
55
+ "none",
56
+ "unknown",
57
+ "n/a",
58
+ "not applicable",
59
+ ])
60
+
61
+
62
+ def score_faf(yaml_content: str, tier: LicenseTier = LicenseTier.BASE) -> Mk4Result:
63
+ """Calculate the official FAF Mk4 score from YAML content."""
64
+ try:
65
+ doc = yaml.safe_load(yaml_content)
66
+ except yaml.YAMLError:
67
+ doc = {}
68
+ if not isinstance(doc, dict):
69
+ doc = {}
70
+
71
+ slot_paths = _get_slot_paths(tier)
72
+ populated = 0
73
+ ignored = 0
74
+ slots: List[Tuple[str, SlotState]] = []
75
+
76
+ for path in slot_paths:
77
+ state = _get_slot_state(doc, path)
78
+ if state == SlotState.POPULATED:
79
+ populated += 1
80
+ elif state == SlotState.SLOTIGNORED:
81
+ ignored += 1
82
+ slots.append((path, state))
83
+
84
+ total = 33 if tier == LicenseTier.ENTERPRISE else 21
85
+ active = total - ignored
86
+
87
+ if active == 0:
88
+ score_val = 0
89
+ else:
90
+ score_val = round((populated / active) * 100)
91
+
92
+ return Mk4Result(
93
+ score=score_val,
94
+ tier=_score_to_tier(score_val),
95
+ populated=populated,
96
+ ignored=ignored,
97
+ active=active,
98
+ total=total,
99
+ slots=slots,
100
+ )
101
+
102
+
103
+ def _get_slot_paths(tier: LicenseTier) -> List[str]:
104
+ """The Universal DNA Map — exact order from mk4.rs lines 126-176."""
105
+ slots = [
106
+ # Project Meta (3)
107
+ "project.name",
108
+ "project.goal",
109
+ "project.main_language",
110
+ # Human Context (6)
111
+ "human_context.who",
112
+ "human_context.what",
113
+ "human_context.why",
114
+ "human_context.where",
115
+ "human_context.when",
116
+ "human_context.how",
117
+ # Frontend Stack (4)
118
+ "stack.frontend",
119
+ "stack.css_framework",
120
+ "stack.ui_library",
121
+ "stack.state_management",
122
+ # Backend Stack (5)
123
+ "stack.backend",
124
+ "stack.api_type",
125
+ "stack.runtime",
126
+ "stack.database",
127
+ "stack.connection",
128
+ # Universal Stack (3)
129
+ "stack.hosting",
130
+ "stack.build",
131
+ "stack.cicd",
132
+ ]
133
+
134
+ if tier == LicenseTier.ENTERPRISE:
135
+ slots.extend([
136
+ # Enterprise Infra (5)
137
+ "stack.monorepo_tool",
138
+ "stack.package_manager",
139
+ "stack.workspaces",
140
+ "monorepo.packages_count",
141
+ "monorepo.build_orchestrator",
142
+ # Enterprise App (4)
143
+ "stack.admin",
144
+ "stack.cache",
145
+ "stack.search",
146
+ "stack.storage",
147
+ # Enterprise Ops (3)
148
+ "monorepo.versioning_strategy",
149
+ "monorepo.shared_configs",
150
+ "monorepo.remote_cache",
151
+ ])
152
+
153
+ return slots
154
+
155
+
156
+ def _get_slot_state(doc: dict, path: str) -> SlotState:
157
+ """Determine the state of a specific slot (mk4.rs lines 179-219)."""
158
+ parts = path.split(".")
159
+ current = doc
160
+
161
+ for part in parts:
162
+ if isinstance(current, dict) and part in current:
163
+ current = current[part]
164
+ else:
165
+ return SlotState.EMPTY
166
+
167
+ # None = YAML null/~ (mk4.rs line 217: _ => Empty)
168
+ if current is None:
169
+ return SlotState.EMPTY
170
+
171
+ # String values (mk4.rs lines 192-200)
172
+ if isinstance(current, str):
173
+ s = current.strip()
174
+ if s == "slotignored":
175
+ return SlotState.SLOTIGNORED
176
+ if _is_valid_populated(s):
177
+ return SlotState.POPULATED
178
+ return SlotState.EMPTY
179
+
180
+ # Numbers and bools — always Populated (mk4.rs line 202)
181
+ if isinstance(current, (int, float, bool)):
182
+ return SlotState.POPULATED
183
+
184
+ # Sequences — Populated if non-empty (mk4.rs lines 203-209)
185
+ if isinstance(current, list):
186
+ return SlotState.POPULATED if current else SlotState.EMPTY
187
+
188
+ # Mappings — Populated if non-empty (mk4.rs lines 210-216)
189
+ if isinstance(current, dict):
190
+ return SlotState.POPULATED if current else SlotState.EMPTY
191
+
192
+ return SlotState.EMPTY
193
+
194
+
195
+ def _is_valid_populated(s: str) -> bool:
196
+ """Placeholder rejection — 8 magic strings (mk4.rs lines 223-236)."""
197
+ return len(s) > 0 and s.lower() not in _PLACEHOLDERS
198
+
199
+
200
+ def _score_to_tier(score: int) -> str:
201
+ """Mk4 official tier calculation (mk4.rs lines 239-247)."""
202
+ if score >= 100:
203
+ return "\U0001f3c6" # Trophy
204
+ if score >= 99:
205
+ return "\U0001f947" # Gold
206
+ if score >= 95:
207
+ return "\U0001f948" # Silver
208
+ if score >= 85:
209
+ return "\U0001f949" # Bronze
210
+ if score >= 70:
211
+ return "\U0001f7e2" # Green
212
+ if score >= 55:
213
+ return "\U0001f7e1" # Yellow
214
+ return "\U0001f534" # Red
@@ -183,7 +183,7 @@ def get_field(faf: FafFile, *keys: str, default: Any = None) -> Any:
183
183
  >>> name = get_field(faf, "project", "name")
184
184
  >>> stack = get_field(faf, "stack", "frontend", default="None")
185
185
  """
186
- value = faf.raw
186
+ value: Any = faf.raw
187
187
  for key in keys:
188
188
  if isinstance(value, dict):
189
189
  value = value.get(key)
@@ -129,6 +129,8 @@ class FafData:
129
129
  project_data = data.get("project", {})
130
130
  if isinstance(project_data, str):
131
131
  project_data = {"name": project_data}
132
+ elif not isinstance(project_data, dict):
133
+ project_data = {}
132
134
 
133
135
  project = ProjectInfo(
134
136
  name=project_data.get("name", "unknown"),
@@ -0,0 +1,25 @@
1
+ faf_version: '2.5.0'
2
+ project:
3
+ name: faf-python-sdk
4
+ goal: Python SDK for parsing, validating, and scoring .faf files with Mk4 Championship Engine
5
+ main_language: Python
6
+ human_context:
7
+ who: Python developers building AI tools, MCP servers, and CI pipelines
8
+ what: Parse, validate, and score FAF project DNA files with 100% Mk4 parity
9
+ why: Universal scoring across all FAF tools — same slots, same formula, same scores
10
+ where: PyPI (pip install faf-python-sdk)
11
+ when: Every AI session that needs project context
12
+ how: Import faf_sdk, call score_faf() or parse_file()
13
+ stack:
14
+ frontend: slotignored
15
+ css_framework: slotignored
16
+ ui_library: slotignored
17
+ state_management: slotignored
18
+ backend: Python
19
+ api_type: SDK (library)
20
+ runtime: Python 3.8+
21
+ database: slotignored
22
+ connection: slotignored
23
+ hosting: PyPI
24
+ build: hatchling
25
+ cicd: GitHub Actions
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "faf-python-sdk"
7
- version = "1.0.2"
7
+ version = "1.1.0"
8
8
  description = "Python SDK for FAF (Foundational AI-context Format) - IANA-registered application/vnd.faf+yaml"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -56,7 +56,7 @@ Issues = "https://github.com/Wolfe-Jam/faf-python-sdk/issues"
56
56
  packages = ["faf_sdk"]
57
57
 
58
58
  [tool.mypy]
59
- python_version = "3.8"
59
+ python_version = "3.9"
60
60
  warn_return_any = true
61
61
  warn_unused_configs = true
62
62
  disallow_untyped_defs = true