faf-python-sdk 1.1.2__py3-none-any.whl → 1.3.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.
- {faf_python_sdk-1.1.2.dist-info → faf_python_sdk-1.3.0.dist-info}/METADATA +36 -5
- faf_python_sdk-1.3.0.dist-info/RECORD +14 -0
- {faf_python_sdk-1.1.2.dist-info → faf_python_sdk-1.3.0.dist-info}/WHEEL +1 -1
- faf_sdk/__init__.py +10 -1
- faf_sdk/dart_detection.json +30 -0
- faf_sdk/detect.py +167 -0
- faf_sdk/interop.py +403 -0
- faf_sdk/py.typed +0 -0
- faf_python_sdk-1.1.2.dist-info/RECORD +0 -10
- {faf_python_sdk-1.1.2.dist-info → faf_python_sdk-1.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: faf-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Persistent project context for Python — parse, validate, and score `.faf` files. The foundation other Python FAF tools (gemini-faf-mcp, custom MCP servers, CI validators) build on. IANA-registered application/vnd.faf+yaml.
|
|
5
5
|
Project-URL: Homepage, https://faf.one
|
|
6
6
|
Project-URL: Documentation, https://github.com/Wolfe-Jam/faf-python-sdk
|
|
@@ -32,17 +32,46 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
# faf-python-sdk
|
|
34
34
|
|
|
35
|
-
**Persistent
|
|
35
|
+
**Persistent Project Context for Python. Parse, validate, score.**
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
**FAF defines. MD instructs. AI codes.**
|
|
38
38
|
|
|
39
|
+
The foundation other Python FAF tools build on. If you're building MCP servers, CI validators, or any Python tool that needs to understand project context, start here.
|
|
40
|
+
|
|
41
|
+
[](https://builder.faf.one)
|
|
39
42
|
[](https://pypi.org/project/faf-python-sdk/)
|
|
40
43
|
[](https://pypi.org/project/faf-python-sdk/)
|
|
41
|
-
[](https://github.com/Wolfe-Jam/faf-python-sdk)
|
|
42
45
|
[](https://www.iana.org/assignments/media-types/application/vnd.faf+yaml)
|
|
43
46
|
|
|
44
47
|
**Media Type:** `application/vnd.faf+yaml` (IANA registered)
|
|
45
48
|
|
|
49
|
+
## What's New in v1.3.0 — The Interop Edition
|
|
50
|
+
|
|
51
|
+
The SDK can now author AI-context files, not just parse and score them.
|
|
52
|
+
|
|
53
|
+
`faf_sdk.interop` — `generate_agents_md(faf)` and `generate_gemini_md(faf)`, Python ports of faf-cli's `src/interop/agents.ts` + `gemini.ts`, in parity with the canonical TypeScript. Deterministic BETTER-shaped projection: setup (install→build→dev ordered) · tests · layout · conventions · three-tier guardrails · definition of done · security · commit · stack. Human Context (who/why marketing) is intentionally omitted from AGENTS.md — it belongs in the README / .faf DNA, not agent ops.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from faf_sdk import parse_file, generate_agents_md
|
|
57
|
+
|
|
58
|
+
faf = parse_file("project.faf")
|
|
59
|
+
print(generate_agents_md(faf.data.raw)) # takes the raw dict — carries top-level commands / key_files / security
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Any Python FAF tool that authors an AI-context file wraps this now — never hand-roll a Markdown generator. `gemini-faf-mcp` 2.7.0's `faf_agents` / `faf_gemini` are the reference wrappers.
|
|
63
|
+
|
|
64
|
+
## What's New in v1.2.0 — The Dart Edition
|
|
65
|
+
|
|
66
|
+
Adds `detect_dart_project()`: content-aware Dart/Flutter detection from a `pubspec.yaml` (Flutter app vs package · Dart MCP / backend / CLI / library), reproducing faf-cli's engine byte-for-byte — 20 shared fixtures, parity-tested.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from faf_sdk import detect_dart_project
|
|
70
|
+
|
|
71
|
+
d = detect_dart_project(".")
|
|
72
|
+
print(d.app_type, d.framework) # e.g. "mobile" "Flutter"
|
|
73
|
+
```
|
|
74
|
+
|
|
46
75
|
## What's New in v1.1.0
|
|
47
76
|
|
|
48
77
|
**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.
|
|
@@ -175,6 +204,8 @@ root = find_project_root()
|
|
|
175
204
|
| [grok-faf-mcp](https://npmjs.com/package/grok-faf-mcp) | xAI | npm |
|
|
176
205
|
| [faf-cli](https://npmjs.com/package/faf-cli) | CLI | npm |
|
|
177
206
|
|
|
207
|
+
If `faf-python-sdk` has been useful, consider starring the repo — it helps others find it.
|
|
208
|
+
|
|
178
209
|
## Links
|
|
179
210
|
|
|
180
211
|
- **Site:** [faf.one](https://faf.one)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
faf_sdk/__init__.py,sha256=1ARRF6IEkjcvf0zJwqWQF30TjTGtgrzWi8O3YwigASc,1842
|
|
2
|
+
faf_sdk/dart_detection.json,sha256=jFtKnyyMVVFsunizteFZ2pLhYC1qC2nCIWQB9SdOqKE,1195
|
|
3
|
+
faf_sdk/detect.py,sha256=GothQNKChKSEwke74MRy63VyBYLqOzN1j3yzjsSVpJA,6310
|
|
4
|
+
faf_sdk/discovery.py,sha256=3Jj3lSzPAj9l0_VjJneLAwo1dMXvhneFhcVN4m2ejOw,8643
|
|
5
|
+
faf_sdk/interop.py,sha256=fgIm13pLZIxSrjJiq7ZXXvR5ViWezuGUxhzCbsz_eZQ,13465
|
|
6
|
+
faf_sdk/mk4.py,sha256=dA8o3YB0W19Mt_bqcR67HAw4S08zxj6IzEmubTy-pQY,5708
|
|
7
|
+
faf_sdk/parser.py,sha256=lJJysj52X0Q_aGhl4PMXY2PuMqASFHdVRiSZFDdampk,4925
|
|
8
|
+
faf_sdk/types.py,sha256=sm1ezSzCc-93bszr4ite31_xZRKjwqf2utlqN6_0QuM,6615
|
|
9
|
+
faf_sdk/validator.py,sha256=6uneOwar4GYUF52BnAQTu159kE4mh4RWRgG6onVbiG4,5730
|
|
10
|
+
faf_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
faf_python_sdk-1.3.0.dist-info/METADATA,sha256=L_RFAhO35YJ9RRSGyl5M8IIdFBU6rndw5bnKmUJmBpk,8784
|
|
12
|
+
faf_python_sdk-1.3.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
13
|
+
faf_python_sdk-1.3.0.dist-info/licenses/LICENSE,sha256=ARScF5tFhbQnYO2V5QAuCwhDHcxKdOWTOV81Pxx_j7U,1065
|
|
14
|
+
faf_python_sdk-1.3.0.dist-info/RECORD,,
|
faf_sdk/__init__.py
CHANGED
|
@@ -24,6 +24,8 @@ from .parser import parse, parse_file, stringify, FafFile
|
|
|
24
24
|
from .validator import validate, ValidationResult
|
|
25
25
|
from .mk4 import score_faf, Mk4Result, SlotState, LicenseTier
|
|
26
26
|
from .discovery import find_faf_file, find_project_root, load_fafignore
|
|
27
|
+
from .detect import detect_dart_project, DartProject
|
|
28
|
+
from .interop import generate_agents_md, generate_gemini_md, faf_meta_tag
|
|
27
29
|
from .types import (
|
|
28
30
|
FafData,
|
|
29
31
|
ProjectInfo,
|
|
@@ -34,7 +36,7 @@ from .types import (
|
|
|
34
36
|
AIScoring
|
|
35
37
|
)
|
|
36
38
|
|
|
37
|
-
__version__ = "1.
|
|
39
|
+
__version__ = "1.3.0"
|
|
38
40
|
__all__ = [
|
|
39
41
|
# Parser
|
|
40
42
|
"parse",
|
|
@@ -53,6 +55,13 @@ __all__ = [
|
|
|
53
55
|
"find_faf_file",
|
|
54
56
|
"find_project_root",
|
|
55
57
|
"load_fafignore",
|
|
58
|
+
# Detection (Dart/Flutter — A+B hybrid, parity with faf-cli)
|
|
59
|
+
"detect_dart_project",
|
|
60
|
+
"DartProject",
|
|
61
|
+
# Interop — AGENTS.md / GEMINI.md generators (parity with faf-cli src/interop)
|
|
62
|
+
"generate_agents_md",
|
|
63
|
+
"generate_gemini_md",
|
|
64
|
+
"faf_meta_tag",
|
|
56
65
|
# Types
|
|
57
66
|
"FafData",
|
|
58
67
|
"ProjectInfo",
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"_doc": "SINGLE SOURCE of Dart/Flutter detection KNOWLEDGE (A+B hybrid, the Truth-is-faf-cli spec). faf-cli imports this directly; faf-python-sdk vendors a synced copy (phase 2); MCPs consume the SDK (phase 3). To bolster Dart/Flutter support — a new state library, server framework, MCP dep — edit THIS file and every engine inherits it. The thin per-language LOGIC (pubspec parse, app-vs-package heuristic, priority branching) lives in code, not here.",
|
|
4
|
+
"flutterDeps": ["flutter"],
|
|
5
|
+
"mcpDeps": ["dart_mcp", "mcp_server", "mcp_dart", "dart_mcp_server", "mcp"],
|
|
6
|
+
"serverFrameworks": [
|
|
7
|
+
["serverpod", "Serverpod"],
|
|
8
|
+
["dart_frog", "Dart Frog"],
|
|
9
|
+
["conduit", "Conduit"],
|
|
10
|
+
["angel3_framework", "Angel3"],
|
|
11
|
+
["alfred", "Alfred"],
|
|
12
|
+
["shelf", "Shelf"]
|
|
13
|
+
],
|
|
14
|
+
"stateManagement": [
|
|
15
|
+
["flutter_riverpod", "Riverpod"],
|
|
16
|
+
["hooks_riverpod", "Riverpod"],
|
|
17
|
+
["riverpod", "Riverpod"],
|
|
18
|
+
["flutter_bloc", "Bloc"],
|
|
19
|
+
["bloc", "Bloc"],
|
|
20
|
+
["provider", "Provider"],
|
|
21
|
+
["get", "GetX"],
|
|
22
|
+
["flutter_mobx", "MobX"],
|
|
23
|
+
["mobx", "MobX"],
|
|
24
|
+
["signals", "Signals"]
|
|
25
|
+
],
|
|
26
|
+
"routing": [
|
|
27
|
+
["go_router", "go_router"],
|
|
28
|
+
["auto_route", "auto_route"]
|
|
29
|
+
]
|
|
30
|
+
}
|
faf_sdk/detect.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dart/Flutter detection — CONTENT-AWARE pubspec classification (Python).
|
|
3
|
+
|
|
4
|
+
PARITY: this mirrors faf-cli src/detect/dart.ts EXACTLY. A pubspec.yaml alone
|
|
5
|
+
does NOT mean Flutter — the same manifest backs Flutter apps, pure-Dart CLIs,
|
|
6
|
+
packages, servers (Dart Frog / Shelf / Serverpod) and MCP servers
|
|
7
|
+
(dart_mcp / mcp_server). We read the dependencies and branch.
|
|
8
|
+
|
|
9
|
+
The detection KNOWLEDGE (which deps mean what) lives in dart_detection.json —
|
|
10
|
+
a byte-identical, synced copy of faf-cli's src/detect/dart-detection.json (the
|
|
11
|
+
single source, A+B hybrid). To bolster Dart support, edit the spec in faf-cli
|
|
12
|
+
(the Truth) and re-run scripts/sync-dart-spec.sh. The thin per-language LOGIC
|
|
13
|
+
(pubspec parse, app-vs-package heuristic, priority branching) lives here.
|
|
14
|
+
|
|
15
|
+
Behavior parity with faf-cli is PROVEN by tests/test_dart_parity.py, which runs
|
|
16
|
+
the SAME shared fixtures faf-cli runs in tests/detect/dart-parity.test.ts.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any, Dict, List, Optional, Set
|
|
27
|
+
|
|
28
|
+
_SPEC: Dict[str, Any] = json.loads(
|
|
29
|
+
(Path(__file__).parent / "dart_detection.json").read_text(encoding="utf-8")
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Detection KNOWLEDGE — sourced from dart_detection.json, never hand-listed here.
|
|
33
|
+
FLUTTER_DEPS: List[str] = list(_SPEC["flutterDeps"])
|
|
34
|
+
MCP_DEPS: List[str] = list(_SPEC["mcpDeps"])
|
|
35
|
+
SERVER_FRAMEWORKS: List[List[str]] = [list(e) for e in _SPEC["serverFrameworks"]]
|
|
36
|
+
STATE_MGMT: List[List[str]] = [list(e) for e in _SPEC["stateManagement"]]
|
|
37
|
+
ROUTING: List[List[str]] = [list(e) for e in _SPEC["routing"]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class DartProject:
|
|
42
|
+
"""Classification of a Dart/Flutter project from its pubspec.yaml."""
|
|
43
|
+
|
|
44
|
+
app_type: str # 'mobile' | 'mcp' | 'backend' | 'cli' | 'library'
|
|
45
|
+
is_flutter: bool
|
|
46
|
+
framework: str # 'Flutter' | 'Dart Frog' | 'Serverpod' | 'Shelf' | ... | ''
|
|
47
|
+
state_management: str # 'Riverpod' | 'Bloc' | 'Provider' | 'GetX' | ... | ''
|
|
48
|
+
routing: str # 'go_router' | 'auto_route' | ''
|
|
49
|
+
testing: str # 'flutter_test' | 'test' | ''
|
|
50
|
+
found: str # human-readable rationale for the .faf `# found:` comment
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> Dict[str, object]:
|
|
53
|
+
"""Project to the faf-cli DartProject shape (camelCase) for parity checks."""
|
|
54
|
+
return {
|
|
55
|
+
"appType": self.app_type,
|
|
56
|
+
"isFlutter": self.is_flutter,
|
|
57
|
+
"framework": self.framework,
|
|
58
|
+
"stateManagement": self.state_management,
|
|
59
|
+
"routing": self.routing,
|
|
60
|
+
"testing": self.testing,
|
|
61
|
+
"found": self.found,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _pubspec_deps(content: str) -> Set[str]:
|
|
66
|
+
"""Collect dependency names from dependencies / dev_dependencies sections."""
|
|
67
|
+
deps: Set[str] = set()
|
|
68
|
+
in_deps = False
|
|
69
|
+
for line in content.split("\n"):
|
|
70
|
+
if re.match(r"^(dependencies|dev_dependencies|dependency_overrides):\s*$", line):
|
|
71
|
+
in_deps = True
|
|
72
|
+
continue
|
|
73
|
+
# A new top-level key (no leading whitespace) ends the dependency section.
|
|
74
|
+
if re.match(r"^\S", line):
|
|
75
|
+
in_deps = False
|
|
76
|
+
if in_deps:
|
|
77
|
+
m = re.match(r"^\s{2}([a-zA-Z0-9_]+):", line)
|
|
78
|
+
if m:
|
|
79
|
+
deps.add(m.group(1).lower())
|
|
80
|
+
return deps
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def detect_dart_project(directory: str) -> Optional[DartProject]:
|
|
84
|
+
"""Classify a Dart/Flutter project from its pubspec.yaml. None if not Dart."""
|
|
85
|
+
path = Path(directory) / "pubspec.yaml"
|
|
86
|
+
if not path.exists():
|
|
87
|
+
return None
|
|
88
|
+
try:
|
|
89
|
+
content = path.read_text(encoding="utf-8")
|
|
90
|
+
except OSError:
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
deps = _pubspec_deps(content)
|
|
94
|
+
|
|
95
|
+
def has(dep: str) -> bool:
|
|
96
|
+
return dep.lower() in deps
|
|
97
|
+
|
|
98
|
+
# Flutter: the `flutter` SDK dep, a top-level `flutter:` section, or `sdk: flutter`.
|
|
99
|
+
is_flutter = (
|
|
100
|
+
any(has(d) for d in FLUTTER_DEPS)
|
|
101
|
+
or re.search(r"^flutter:\s*$", content, re.MULTILINE) is not None
|
|
102
|
+
or re.search(r"\bsdk:\s*flutter\b", content) is not None
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
mcp_dep = next((d for d in MCP_DEPS if has(d)), None)
|
|
106
|
+
server = next((e for e in SERVER_FRAMEWORKS if has(e[0])), None)
|
|
107
|
+
state_entry = next((e for e in STATE_MGMT if has(e[0])), None)
|
|
108
|
+
state_management = state_entry[1] if state_entry else ""
|
|
109
|
+
route_entry = next((e for e in ROUTING if has(e[0])), None)
|
|
110
|
+
routing = route_entry[1] if route_entry else ""
|
|
111
|
+
testing = "flutter_test" if has("flutter_test") else ("test" if has("test") else "")
|
|
112
|
+
|
|
113
|
+
# CLI: a top-level `executables:` section, or bin/*.dart entry points.
|
|
114
|
+
has_executables = re.search(r"^executables:\s*$", content, re.MULTILINE) is not None
|
|
115
|
+
has_bin_dart = False
|
|
116
|
+
bin_dir = Path(directory) / "bin"
|
|
117
|
+
if bin_dir.is_dir():
|
|
118
|
+
try:
|
|
119
|
+
has_bin_dart = any(f.endswith(".dart") for f in os.listdir(bin_dir))
|
|
120
|
+
except OSError:
|
|
121
|
+
has_bin_dart = False
|
|
122
|
+
is_cli = has_executables or has_bin_dart
|
|
123
|
+
|
|
124
|
+
framework = ""
|
|
125
|
+
app_type: str
|
|
126
|
+
found: str
|
|
127
|
+
|
|
128
|
+
if is_flutter:
|
|
129
|
+
framework = "Flutter"
|
|
130
|
+
# App vs package: an app has lib/main.dart (the entry) or `publish_to: none`;
|
|
131
|
+
# a reusable Flutter package has neither — it's publishable, lib/ exports only.
|
|
132
|
+
is_app = (Path(directory) / "lib" / "main.dart").exists() or (
|
|
133
|
+
re.search(r"^publish_to:\s*['\"]?none\b", content, re.MULTILINE) is not None
|
|
134
|
+
)
|
|
135
|
+
if is_app:
|
|
136
|
+
app_type = "mobile"
|
|
137
|
+
found = "pubspec.yaml (Flutter app)"
|
|
138
|
+
else:
|
|
139
|
+
app_type = "library"
|
|
140
|
+
found = "pubspec.yaml (Flutter package)"
|
|
141
|
+
elif mcp_dep:
|
|
142
|
+
app_type = "mcp"
|
|
143
|
+
found = f"pubspec.yaml + {mcp_dep} (Dart MCP server)"
|
|
144
|
+
elif server:
|
|
145
|
+
app_type = "backend"
|
|
146
|
+
framework = server[1]
|
|
147
|
+
found = f"pubspec.yaml + {server[0]} (Dart backend)"
|
|
148
|
+
elif is_cli:
|
|
149
|
+
app_type = "cli"
|
|
150
|
+
found = (
|
|
151
|
+
"pubspec.yaml executables: (Dart CLI)"
|
|
152
|
+
if has_executables
|
|
153
|
+
else "pubspec.yaml + bin/*.dart (Dart CLI)"
|
|
154
|
+
)
|
|
155
|
+
else:
|
|
156
|
+
app_type = "library"
|
|
157
|
+
found = "pubspec.yaml (Dart package)"
|
|
158
|
+
|
|
159
|
+
return DartProject(
|
|
160
|
+
app_type=app_type,
|
|
161
|
+
is_flutter=is_flutter,
|
|
162
|
+
framework=framework,
|
|
163
|
+
state_management=state_management,
|
|
164
|
+
routing=routing,
|
|
165
|
+
testing=testing,
|
|
166
|
+
found=found,
|
|
167
|
+
)
|
faf_sdk/interop.py
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI-context file generators — AGENTS.md and GEMINI.md from .faf data.
|
|
3
|
+
|
|
4
|
+
Python port of faf-cli's `src/interop/agents.ts` + `gemini.ts`, kept in
|
|
5
|
+
parity with the canonical TypeScript. Deterministic projection from curated
|
|
6
|
+
truth — facts, not freewritten prose. Human Context (who/why marketing) is
|
|
7
|
+
intentionally omitted from AGENTS.md; it belongs in the README / .faf DNA,
|
|
8
|
+
not agent ops.
|
|
9
|
+
|
|
10
|
+
Both take the raw parsed .faf dict (`FafFile.data.raw`) — the .faf format
|
|
11
|
+
carries top-level `commands` / `key_files` / `security` that the typed model
|
|
12
|
+
doesn't surface.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
__all__ = ["generate_agents_md", "generate_gemini_md", "faf_meta_tag", "title_label", "slot_label"]
|
|
20
|
+
|
|
21
|
+
# --- shared helpers ----------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
_ACRONYMS = {
|
|
24
|
+
"API", "CI", "CD", "MCP", "CLI", "SDK", "UI", "UX", "AI", "ML", "DB", "ORM",
|
|
25
|
+
"OS", "HTTP", "HTTPS", "REST", "RPC", "JSON", "YAML", "XML", "SQL", "CSS",
|
|
26
|
+
"HTML", "AWS", "GCP", "CDN", "DNS", "JWT", "ID", "IP", "URL", "URI", "TS", "JS",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# Canonical .faf slot labels (from faf-cli src/core/slots.ts).
|
|
30
|
+
_SLOT_LABELS = {
|
|
31
|
+
"stack.frontend": "Framework", "stack.css_framework": "CSS",
|
|
32
|
+
"stack.ui_library": "UI Library", "stack.state_management": "State",
|
|
33
|
+
"stack.backend": "Backend", "stack.api_type": "API", "stack.runtime": "Runtime",
|
|
34
|
+
"stack.database": "Database", "stack.connection": "Connection",
|
|
35
|
+
"stack.hosting": "Hosting", "stack.build": "Build", "stack.cicd": "CI/CD",
|
|
36
|
+
"stack.monorepo_tool": "Monorepo", "stack.package_manager": "Package Manager",
|
|
37
|
+
"stack.workspaces": "Workspaces", "stack.admin": "Admin", "stack.cache": "Cache",
|
|
38
|
+
"stack.search": "Search", "stack.storage": "Storage",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Preference keys that describe human<->assistant interaction, not repo conventions.
|
|
42
|
+
_HUMAN_PREF = {
|
|
43
|
+
"commit_style", "communication", "response_style", "explanation_level",
|
|
44
|
+
"explanations", "documentation", "code_first",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Stack keys that are context/marketing, not actual stack.
|
|
48
|
+
_NON_STACK = {"target_user", "core_problem", "mission_purpose"}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _present(v: Any) -> bool:
|
|
52
|
+
"""Non-empty, not 'slotignored', non-empty list."""
|
|
53
|
+
if v is None or v == "" or v == "slotignored":
|
|
54
|
+
return False
|
|
55
|
+
if isinstance(v, (list, tuple)) and len(v) == 0:
|
|
56
|
+
return False
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _filled(v: Any) -> bool:
|
|
61
|
+
"""A string slot value carrying real content."""
|
|
62
|
+
return isinstance(v, str) and v.strip() not in ("", "slotignored")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _fmt_val(v: Any) -> str:
|
|
66
|
+
if isinstance(v, (list, tuple)):
|
|
67
|
+
return ", ".join(str(x) for x in v)
|
|
68
|
+
return str(v)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def title_label(key: str) -> str:
|
|
72
|
+
"""snake_case -> Title Case, acronym-aware. `api_type` -> 'API Type'."""
|
|
73
|
+
out: list[str] = []
|
|
74
|
+
for w in key.split("_"):
|
|
75
|
+
if not w:
|
|
76
|
+
out.append(w)
|
|
77
|
+
elif w.upper() in _ACRONYMS:
|
|
78
|
+
out.append(w.upper())
|
|
79
|
+
else:
|
|
80
|
+
out.append(w[0].upper() + w[1:])
|
|
81
|
+
return " ".join(out)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def slot_label(path: str) -> str:
|
|
85
|
+
"""Canonical display label for a .faf slot path; falls back to title_label."""
|
|
86
|
+
if path in _SLOT_LABELS:
|
|
87
|
+
return _SLOT_LABELS[path]
|
|
88
|
+
key = path.rsplit(".", 1)[-1] if "." in path else path
|
|
89
|
+
return title_label(key)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def faf_meta_tag(faf: dict) -> str:
|
|
93
|
+
"""The two-line `<!-- faf: ... -->` metastamp every faf-generated file opens with."""
|
|
94
|
+
proj = faf.get("project") or {}
|
|
95
|
+
name = str(proj.get("name") or "").strip()
|
|
96
|
+
lang = str(proj.get("main_language") or "").strip()
|
|
97
|
+
typ = str(proj.get("type") or "").strip()
|
|
98
|
+
desc = str(proj.get("goal") or "").strip()
|
|
99
|
+
line1 = f"<!-- faf: {' | '.join([name, lang, typ, desc])} -->"
|
|
100
|
+
kv = ["claim=project.faf", "family=FAF"]
|
|
101
|
+
line2 = f"<!-- faf: {' | '.join(kv)} -->"
|
|
102
|
+
return f"{line1}\n{line2}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# --- command classification (shared by both generators) ---------------------
|
|
106
|
+
|
|
107
|
+
def _classify_commands(faf: dict) -> dict:
|
|
108
|
+
commands = faf.get("commands") or {}
|
|
109
|
+
entries = [(k, v) for k, v in commands.items() if _present(v)]
|
|
110
|
+
test_cmds = [(k, v) for k, v in entries if "test" in k.lower()]
|
|
111
|
+
lint_cmds = [
|
|
112
|
+
(k, v) for k, v in entries
|
|
113
|
+
if ("lint" in k.lower() or "check" in k.lower()) and "test" not in k.lower()
|
|
114
|
+
]
|
|
115
|
+
setup_raw = [
|
|
116
|
+
(k, v) for k, v in entries
|
|
117
|
+
if not any(t in k.lower() for t in ("test", "lint", "check"))
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
def setup_rank(k: str) -> int:
|
|
121
|
+
n = k.lower()
|
|
122
|
+
if "install" in n or "deps" in n:
|
|
123
|
+
return 0
|
|
124
|
+
if "build" in n and "rebuild" not in n:
|
|
125
|
+
return 1
|
|
126
|
+
if n == "dev" or "develop" in n:
|
|
127
|
+
return 2
|
|
128
|
+
if n == "start" or "run" in n:
|
|
129
|
+
return 3
|
|
130
|
+
return 4
|
|
131
|
+
|
|
132
|
+
setup_cmds = sorted(setup_raw, key=lambda kv: (setup_rank(kv[0]), kv[0]))
|
|
133
|
+
verify_cmds = test_cmds + lint_cmds
|
|
134
|
+
return {
|
|
135
|
+
"test": test_cmds, "lint": lint_cmds, "setup": setup_cmds, "verify": verify_cmds,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _key_files(faf: dict) -> list:
|
|
140
|
+
kf = faf.get("key_files")
|
|
141
|
+
if not kf:
|
|
142
|
+
ic = faf.get("instant_context") or {}
|
|
143
|
+
kf = ic.get("key_files")
|
|
144
|
+
return kf or []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# --- AGENTS.md -------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def generate_agents_md(faf: dict) -> str:
|
|
150
|
+
"""Author a BETTER-shaped AGENTS.md from raw .faf data. Deterministic."""
|
|
151
|
+
lines: list[str] = []
|
|
152
|
+
|
|
153
|
+
def push(s: str = "") -> None:
|
|
154
|
+
lines.append(s)
|
|
155
|
+
|
|
156
|
+
proj = faf.get("project") or {}
|
|
157
|
+
ai = faf.get("ai_instructions") or {}
|
|
158
|
+
prefs = faf.get("preferences") or {}
|
|
159
|
+
security = faf.get("security") or {}
|
|
160
|
+
branch = str(proj["default_branch"]) if _present(proj.get("default_branch")) else "main"
|
|
161
|
+
|
|
162
|
+
cmds = _classify_commands(faf)
|
|
163
|
+
setup_cmds, test_cmds, lint_cmds, verify_cmds = (
|
|
164
|
+
cmds["setup"], cmds["test"], cmds["lint"], cmds["verify"],
|
|
165
|
+
)
|
|
166
|
+
test_cmd = test_cmds[0][1] if test_cmds else None
|
|
167
|
+
build_cmd = next((v for k, v in setup_cmds if "build" in k.lower()), None)
|
|
168
|
+
key_files = _key_files(faf)
|
|
169
|
+
|
|
170
|
+
push(faf_meta_tag(faf))
|
|
171
|
+
push()
|
|
172
|
+
push(f"# AGENTS.md — {proj.get('name') or 'Project'}")
|
|
173
|
+
push()
|
|
174
|
+
|
|
175
|
+
# 1 - orientation
|
|
176
|
+
bits: list[str] = []
|
|
177
|
+
if proj.get("main_language"):
|
|
178
|
+
bits.append(str(proj["main_language"]))
|
|
179
|
+
if _present(proj.get("type")):
|
|
180
|
+
bits.append(f"type: {proj['type']}")
|
|
181
|
+
if _present(proj.get("version")):
|
|
182
|
+
bits.append(f"v{proj['version']}")
|
|
183
|
+
orientation = str(proj["goal"]).strip() if proj.get("goal") else ""
|
|
184
|
+
if bits:
|
|
185
|
+
orientation += (" — " if orientation else "") + " · ".join(bits)
|
|
186
|
+
if orientation:
|
|
187
|
+
push(orientation)
|
|
188
|
+
push()
|
|
189
|
+
push(
|
|
190
|
+
"> Authored by faf — refresh with `faf export --agents` or the "
|
|
191
|
+
"`faf_agents` MCP tool. The managed block is regenerated each time; "
|
|
192
|
+
"hand-written content outside it is preserved."
|
|
193
|
+
)
|
|
194
|
+
push()
|
|
195
|
+
|
|
196
|
+
# 2 - setup & build
|
|
197
|
+
if setup_cmds:
|
|
198
|
+
push("## Setup & build")
|
|
199
|
+
push()
|
|
200
|
+
push("```bash")
|
|
201
|
+
for k, v in setup_cmds:
|
|
202
|
+
push(f"{v} # {k}")
|
|
203
|
+
push("```")
|
|
204
|
+
push()
|
|
205
|
+
|
|
206
|
+
# 3 - run the tests
|
|
207
|
+
if verify_cmds:
|
|
208
|
+
push("## Run the tests")
|
|
209
|
+
push()
|
|
210
|
+
push("```bash")
|
|
211
|
+
for _, v in verify_cmds:
|
|
212
|
+
push(v)
|
|
213
|
+
push("```")
|
|
214
|
+
push()
|
|
215
|
+
|
|
216
|
+
# 4 - where things live
|
|
217
|
+
if key_files:
|
|
218
|
+
push("## Where things live")
|
|
219
|
+
push()
|
|
220
|
+
rows = []
|
|
221
|
+
for f in key_files:
|
|
222
|
+
s = str(f)
|
|
223
|
+
at = s.find(" — ")
|
|
224
|
+
rows.append((s[:at], s[at + 3:]) if at > 0 else (s, ""))
|
|
225
|
+
if any(role for _, role in rows):
|
|
226
|
+
push("| Path | Role |")
|
|
227
|
+
push("|------|------|")
|
|
228
|
+
for path, role in rows:
|
|
229
|
+
push(f"| `{path}` | {role} |")
|
|
230
|
+
else:
|
|
231
|
+
for path, _ in rows:
|
|
232
|
+
push(f"- `{path}`")
|
|
233
|
+
push()
|
|
234
|
+
|
|
235
|
+
# 5 - conventions
|
|
236
|
+
conventions: dict[str, str] = {}
|
|
237
|
+
|
|
238
|
+
def collect(obj: Any) -> None:
|
|
239
|
+
if not isinstance(obj, dict):
|
|
240
|
+
return
|
|
241
|
+
for k, v in obj.items():
|
|
242
|
+
if k in _HUMAN_PREF or not _present(v):
|
|
243
|
+
continue
|
|
244
|
+
label = title_label(k)
|
|
245
|
+
conventions.setdefault(label, _fmt_val(v))
|
|
246
|
+
|
|
247
|
+
collect(ai.get("working_style"))
|
|
248
|
+
collect(prefs)
|
|
249
|
+
detected_conv = faf.get("conventions") or []
|
|
250
|
+
if conventions or detected_conv:
|
|
251
|
+
push("## Conventions")
|
|
252
|
+
push()
|
|
253
|
+
for label, val in conventions.items():
|
|
254
|
+
push(f"- **{label}:** {val}")
|
|
255
|
+
for c in detected_conv:
|
|
256
|
+
if _present(c):
|
|
257
|
+
push(f"- {c}")
|
|
258
|
+
push()
|
|
259
|
+
|
|
260
|
+
# 6 - guardrails
|
|
261
|
+
warnings = [w for w in (ai.get("warnings") or []) if _present(w)]
|
|
262
|
+
always = ["read the tree"]
|
|
263
|
+
if test_cmd:
|
|
264
|
+
always.append(f"run the tests (`{test_cmd}`)")
|
|
265
|
+
if build_cmd:
|
|
266
|
+
always.append("build the project")
|
|
267
|
+
for _, v in lint_cmds[:1]:
|
|
268
|
+
always.append(f"`{v}`")
|
|
269
|
+
push("## Guardrails")
|
|
270
|
+
push()
|
|
271
|
+
for w in warnings:
|
|
272
|
+
push(f"- {w}")
|
|
273
|
+
push(f"- **Always OK:** {' · '.join(dict.fromkeys(always))}.")
|
|
274
|
+
push("- **Ask first:** dependency installs, deletions, migrations, schema changes, publish/release.")
|
|
275
|
+
push(
|
|
276
|
+
f"- **Never:** force-push · push straight to `{branch}` (branch and open a PR) · commit secrets."
|
|
277
|
+
)
|
|
278
|
+
push()
|
|
279
|
+
|
|
280
|
+
# 7 - definition of done
|
|
281
|
+
dod: list[str] = []
|
|
282
|
+
for _, v in lint_cmds:
|
|
283
|
+
dod.append(f"`{v}` exits 0")
|
|
284
|
+
for _, v in test_cmds:
|
|
285
|
+
dod.append(f"`{v}` passes")
|
|
286
|
+
dod.append("changes committed with a conventional message")
|
|
287
|
+
push("## Definition of Done")
|
|
288
|
+
push()
|
|
289
|
+
push(f"Done when: {' · '.join(dict.fromkeys(dod))}.")
|
|
290
|
+
push()
|
|
291
|
+
|
|
292
|
+
# 8 - when stuck
|
|
293
|
+
push("## When stuck")
|
|
294
|
+
push()
|
|
295
|
+
push(
|
|
296
|
+
"Ask a clarifying question, propose a short plan, or open a draft PR with "
|
|
297
|
+
f"notes — do not push large speculative changes to `{branch}`."
|
|
298
|
+
)
|
|
299
|
+
push()
|
|
300
|
+
|
|
301
|
+
# 9 - security & secrets
|
|
302
|
+
if security and (_present(security.get("secrets")) or (security.get("never") or [])):
|
|
303
|
+
push("## Security & secrets")
|
|
304
|
+
push()
|
|
305
|
+
if _present(security.get("secrets")):
|
|
306
|
+
ex = f" (see `{security['example']}`)" if _present(security.get("example")) else ""
|
|
307
|
+
push(f"- Secrets live in `{security['secrets']}`{ex}. Never read or commit them.")
|
|
308
|
+
for n in security.get("never") or []:
|
|
309
|
+
if _present(n):
|
|
310
|
+
push(f"- Never read or commit `{n}`.")
|
|
311
|
+
push()
|
|
312
|
+
|
|
313
|
+
# 10 - commit & PR
|
|
314
|
+
push("## Commit & PR")
|
|
315
|
+
push()
|
|
316
|
+
if _present(prefs.get("commit_style")):
|
|
317
|
+
push(f"- Commit style: {_fmt_val(prefs['commit_style'])}")
|
|
318
|
+
else:
|
|
319
|
+
push("- Conventional Commits preferred (`feat:`, `fix:`, `chore:`, …).")
|
|
320
|
+
push(f"- Branch off `{branch}` and open a PR — never commit to `{branch}` directly.")
|
|
321
|
+
push("- If build/test scripts or layout change, refresh this file in the **same PR**.")
|
|
322
|
+
push()
|
|
323
|
+
|
|
324
|
+
# stack (reference)
|
|
325
|
+
stack = faf.get("stack") or {}
|
|
326
|
+
if isinstance(stack, dict):
|
|
327
|
+
rendered = [
|
|
328
|
+
f"- **{slot_label(f'stack.{k}')}:** {v.strip()}"
|
|
329
|
+
for k, v in stack.items()
|
|
330
|
+
if k not in _NON_STACK and _filled(v)
|
|
331
|
+
]
|
|
332
|
+
if rendered:
|
|
333
|
+
push("## Stack")
|
|
334
|
+
push()
|
|
335
|
+
lines.extend(rendered)
|
|
336
|
+
push()
|
|
337
|
+
|
|
338
|
+
gen = faf.get("generated")
|
|
339
|
+
if _present(gen):
|
|
340
|
+
push(f"*Context authored: {gen}*")
|
|
341
|
+
|
|
342
|
+
return "\n".join(lines)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
# --- GEMINI.md ------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
def generate_gemini_md(faf: dict) -> str:
|
|
348
|
+
"""GEMINI.md — Gemini CLI's own convention (hierarchical, @file-importable)."""
|
|
349
|
+
lines: list[str] = []
|
|
350
|
+
proj = faf.get("project") or {}
|
|
351
|
+
cmds = _classify_commands(faf)
|
|
352
|
+
setup_cmds, verify_cmds = cmds["setup"], cmds["verify"]
|
|
353
|
+
key_files = _key_files(faf)
|
|
354
|
+
|
|
355
|
+
lines.append(faf_meta_tag(faf))
|
|
356
|
+
lines.append("")
|
|
357
|
+
lines.append(f"# GEMINI.md — {proj.get('name') or 'Project'}")
|
|
358
|
+
lines.append("")
|
|
359
|
+
lines.append("> Authored from project.faf — refresh with the `faf_gemini` MCP tool or `faf export --gemini`.")
|
|
360
|
+
lines.append("")
|
|
361
|
+
|
|
362
|
+
if proj.get("name"):
|
|
363
|
+
lines.append(f"Project: {proj['name']}")
|
|
364
|
+
if proj.get("goal"):
|
|
365
|
+
lines.append(f"Goal: {proj['goal']}")
|
|
366
|
+
if proj.get("main_language"):
|
|
367
|
+
lines.append(f"Language: {proj['main_language']}")
|
|
368
|
+
|
|
369
|
+
if setup_cmds:
|
|
370
|
+
lines += ["", "## Setup & build", "", "```bash"]
|
|
371
|
+
for k, v in setup_cmds:
|
|
372
|
+
lines.append(f"{v} # {k}")
|
|
373
|
+
lines.append("```")
|
|
374
|
+
|
|
375
|
+
if verify_cmds:
|
|
376
|
+
lines += ["", "## Test & verify", "", "```bash"]
|
|
377
|
+
for _, v in verify_cmds:
|
|
378
|
+
lines.append(v)
|
|
379
|
+
lines.append("```")
|
|
380
|
+
|
|
381
|
+
if key_files:
|
|
382
|
+
lines += ["", "## Where things live", ""]
|
|
383
|
+
for f in key_files:
|
|
384
|
+
lines.append(f"- `{f}`")
|
|
385
|
+
|
|
386
|
+
stack = faf.get("stack") or {}
|
|
387
|
+
if isinstance(stack, dict):
|
|
388
|
+
rendered = [
|
|
389
|
+
f"- {slot_label(f'stack.{k}')}: {v.strip()}"
|
|
390
|
+
for k, v in stack.items()
|
|
391
|
+
if k not in _NON_STACK and _filled(v)
|
|
392
|
+
]
|
|
393
|
+
if rendered:
|
|
394
|
+
lines += ["", "## Stack"]
|
|
395
|
+
lines.extend(rendered)
|
|
396
|
+
|
|
397
|
+
lines += [
|
|
398
|
+
"", "## Before changing things", "",
|
|
399
|
+
"- Ask first: dependency installs, deletions, migrations, schema changes, publish/release.",
|
|
400
|
+
"- Never: force-push · push straight to `main` · commit secrets.",
|
|
401
|
+
"",
|
|
402
|
+
]
|
|
403
|
+
return "\n".join(lines)
|
faf_sdk/py.typed
ADDED
|
File without changes
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
faf_sdk/__init__.py,sha256=mN9DPryL9_sPv-QVw-rZGS6u8dcVDvB1UNu2GGQg7Pk,1445
|
|
2
|
-
faf_sdk/discovery.py,sha256=3Jj3lSzPAj9l0_VjJneLAwo1dMXvhneFhcVN4m2ejOw,8643
|
|
3
|
-
faf_sdk/mk4.py,sha256=dA8o3YB0W19Mt_bqcR67HAw4S08zxj6IzEmubTy-pQY,5708
|
|
4
|
-
faf_sdk/parser.py,sha256=lJJysj52X0Q_aGhl4PMXY2PuMqASFHdVRiSZFDdampk,4925
|
|
5
|
-
faf_sdk/types.py,sha256=sm1ezSzCc-93bszr4ite31_xZRKjwqf2utlqN6_0QuM,6615
|
|
6
|
-
faf_sdk/validator.py,sha256=6uneOwar4GYUF52BnAQTu159kE4mh4RWRgG6onVbiG4,5730
|
|
7
|
-
faf_python_sdk-1.1.2.dist-info/METADATA,sha256=ReVbpyLAD3PG_vInHUbkfJTnarFcmdBrhl_zvd_t_D4,7116
|
|
8
|
-
faf_python_sdk-1.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
-
faf_python_sdk-1.1.2.dist-info/licenses/LICENSE,sha256=ARScF5tFhbQnYO2V5QAuCwhDHcxKdOWTOV81Pxx_j7U,1065
|
|
10
|
-
faf_python_sdk-1.1.2.dist-info/RECORD,,
|
|
File without changes
|