faf-python-sdk 1.2.0__tar.gz → 1.3.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.
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/CHANGELOG.md +24 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/CLAUDE.md +7 -5
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/PKG-INFO +18 -5
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/README.md +16 -3
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/faf_sdk/__init__.py +6 -1
- faf_python_sdk-1.3.0/faf_sdk/interop.py +403 -0
- faf_python_sdk-1.3.0/faf_sdk/py.typed +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/project.faf +2 -2
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/pyproject.toml +6 -2
- faf_python_sdk-1.3.0/tests/test_interop.py +178 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/.github/workflows/pypi.yml +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/.github/workflows/testpypi.yml +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/.gitignore +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/CODE_OF_CONDUCT.md +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/CONTRIBUTING.md +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/LICENSE +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/SECURITY.md +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/docs/GROK-INTEGRATION.md +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/docs/TECHNICAL-SPEC.md +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/examples/basic_usage.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/examples/grok_integration.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/faf_sdk/dart_detection.json +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/faf_sdk/detect.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/faf_sdk/discovery.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/faf_sdk/mk4.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/faf_sdk/parser.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/faf_sdk/types.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/faf_sdk/validator.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/scripts/sync-dart-spec.sh +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/__init__.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/dart_parity_fixtures.json +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/stress_test.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/test_dart_parity.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/test_discovery.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/test_mk4.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/test_parser.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/test_validator.py +0 -0
- {faf_python_sdk-1.2.0 → faf_python_sdk-1.3.0}/tests/test_wjttc.py +0 -0
|
@@ -3,6 +3,30 @@
|
|
|
3
3
|
All notable changes to faf-python-sdk are documented here.
|
|
4
4
|
Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
5
|
|
|
6
|
+
## [1.3.0] - 2026-09-06 — The Interop Edition
|
|
7
|
+
|
|
8
|
+
The SDK can now author AI-context files, not just parse and score them.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `faf_sdk.interop` — `generate_agents_md(faf)` and `generate_gemini_md(faf)`,
|
|
12
|
+
Python ports of faf-cli's `src/interop/agents.ts` + `gemini.ts`, kept in
|
|
13
|
+
parity with the canonical TypeScript. Deterministic BETTER-shaped projection:
|
|
14
|
+
`## Setup & build` (install→build→dev ordered) · `## Run the tests` ·
|
|
15
|
+
`## Where things live` · `## Conventions` · three-tier `## Guardrails` ·
|
|
16
|
+
`## Definition of Done` · `## When stuck` · `## Security & secrets` ·
|
|
17
|
+
`## Commit & PR` · `## Stack`. Human Context (who/why marketing) is
|
|
18
|
+
intentionally omitted from AGENTS.md — it belongs in the README / .faf DNA.
|
|
19
|
+
- `faf_meta_tag(faf)`, `title_label(key)`, `slot_label(path)` — the shared
|
|
20
|
+
label + metastamp helpers, also from `src/interop`.
|
|
21
|
+
- Both generators take the **raw parsed dict** (`FafFile.data.raw`) — the .faf
|
|
22
|
+
format carries top-level `commands` / `key_files` / `security` that the typed
|
|
23
|
+
model doesn't surface.
|
|
24
|
+
- 17 tests, including deterministic-output and human-context-omission guards.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- `[tool.mypy] python_version` was `"3.9"` — rejected by modern mypy
|
|
28
|
+
(`must be 3.10 or higher`). Set to `"3.10"`.
|
|
29
|
+
|
|
6
30
|
## [1.2.0] - 2026-06-16 — The Dart Edition
|
|
7
31
|
|
|
8
32
|
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.
|
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
<!-- faf:
|
|
1
|
+
<!-- faf:start -->
|
|
2
|
+
<!-- faf: faf-python-sdk | Python | | Python SDK for parsing, validating, scoring, and authoring .faf project context — the Mk4 engine + interop generators other Python FAF tools build on -->
|
|
2
3
|
<!-- faf: claim=project.faf | family=FAF -->
|
|
3
4
|
|
|
4
5
|
# CLAUDE.md — faf-python-sdk
|
|
5
6
|
|
|
6
7
|
## What This Is
|
|
7
8
|
|
|
8
|
-
Python SDK for parsing, validating, and
|
|
9
|
+
Python SDK for parsing, validating, scoring, and authoring .faf project context — the Mk4 engine + interop generators other Python FAF tools build on
|
|
9
10
|
|
|
10
11
|
## Stack
|
|
11
12
|
|
|
12
13
|
- **Language:** Python
|
|
13
14
|
- **Backend:** Python
|
|
14
|
-
- **
|
|
15
|
+
- **API:** SDK (library)
|
|
15
16
|
- **Runtime:** Python 3.8+
|
|
16
17
|
- **Hosting:** PyPI
|
|
17
18
|
- **Build:** hatchling
|
|
18
|
-
- **
|
|
19
|
+
- **CI/CD:** GitHub Actions
|
|
19
20
|
|
|
20
21
|
## Context
|
|
21
22
|
|
|
@@ -28,4 +29,5 @@ Python SDK for parsing, validating, and scoring .faf files with Mk4 Championship
|
|
|
28
29
|
|
|
29
30
|
---
|
|
30
31
|
|
|
31
|
-
*STATUS: BI-SYNC ACTIVE — 2026-
|
|
32
|
+
*STATUS: BI-SYNC ACTIVE — 2026-09-06T18:19:41.228Z*
|
|
33
|
+
<!-- faf:end -->
|
|
@@ -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
|
|
@@ -41,17 +41,30 @@ The foundation other Python FAF tools build on. If you're building MCP servers,
|
|
|
41
41
|
[](https://builder.faf.one)
|
|
42
42
|
[](https://pypi.org/project/faf-python-sdk/)
|
|
43
43
|
[](https://pypi.org/project/faf-python-sdk/)
|
|
44
|
-
[](https://github.com/Wolfe-Jam/faf-python-sdk)
|
|
45
45
|
[](https://www.iana.org/assignments/media-types/application/vnd.faf+yaml)
|
|
46
46
|
|
|
47
47
|
**Media Type:** `application/vnd.faf+yaml` (IANA registered)
|
|
48
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
|
+
|
|
49
64
|
## What's New in v1.2.0 — The Dart Edition
|
|
50
65
|
|
|
51
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.
|
|
52
67
|
|
|
53
|
-
The SDK's first detection capability — it now reads a `pubspec.yaml` and tells you *what kind* of Dart project it is, not just parse/validate/score. The detection KNOWLEDGE is vendored byte-identical from faf-cli (the single source) and ships in the wheel:
|
|
54
|
-
|
|
55
68
|
```python
|
|
56
69
|
from faf_sdk import detect_dart_project
|
|
57
70
|
|
|
@@ -9,17 +9,30 @@ The foundation other Python FAF tools build on. If you're building MCP servers,
|
|
|
9
9
|
[](https://builder.faf.one)
|
|
10
10
|
[](https://pypi.org/project/faf-python-sdk/)
|
|
11
11
|
[](https://pypi.org/project/faf-python-sdk/)
|
|
12
|
-
[](https://github.com/Wolfe-Jam/faf-python-sdk)
|
|
13
13
|
[](https://www.iana.org/assignments/media-types/application/vnd.faf+yaml)
|
|
14
14
|
|
|
15
15
|
**Media Type:** `application/vnd.faf+yaml` (IANA registered)
|
|
16
16
|
|
|
17
|
+
## What's New in v1.3.0 — The Interop Edition
|
|
18
|
+
|
|
19
|
+
The SDK can now author AI-context files, not just parse and score them.
|
|
20
|
+
|
|
21
|
+
`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.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from faf_sdk import parse_file, generate_agents_md
|
|
25
|
+
|
|
26
|
+
faf = parse_file("project.faf")
|
|
27
|
+
print(generate_agents_md(faf.data.raw)) # takes the raw dict — carries top-level commands / key_files / security
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
17
32
|
## What's New in v1.2.0 — The Dart Edition
|
|
18
33
|
|
|
19
34
|
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.
|
|
20
35
|
|
|
21
|
-
The SDK's first detection capability — it now reads a `pubspec.yaml` and tells you *what kind* of Dart project it is, not just parse/validate/score. The detection KNOWLEDGE is vendored byte-identical from faf-cli (the single source) and ships in the wheel:
|
|
22
|
-
|
|
23
36
|
```python
|
|
24
37
|
from faf_sdk import detect_dart_project
|
|
25
38
|
|
|
@@ -25,6 +25,7 @@ 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
27
|
from .detect import detect_dart_project, DartProject
|
|
28
|
+
from .interop import generate_agents_md, generate_gemini_md, faf_meta_tag
|
|
28
29
|
from .types import (
|
|
29
30
|
FafData,
|
|
30
31
|
ProjectInfo,
|
|
@@ -35,7 +36,7 @@ from .types import (
|
|
|
35
36
|
AIScoring
|
|
36
37
|
)
|
|
37
38
|
|
|
38
|
-
__version__ = "1.
|
|
39
|
+
__version__ = "1.3.0"
|
|
39
40
|
__all__ = [
|
|
40
41
|
# Parser
|
|
41
42
|
"parse",
|
|
@@ -57,6 +58,10 @@ __all__ = [
|
|
|
57
58
|
# Detection (Dart/Flutter — A+B hybrid, parity with faf-cli)
|
|
58
59
|
"detect_dart_project",
|
|
59
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",
|
|
60
65
|
# Types
|
|
61
66
|
"FafData",
|
|
62
67
|
"ProjectInfo",
|
|
@@ -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)
|
|
File without changes
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
faf_version:
|
|
1
|
+
faf_version: "3.0"
|
|
2
2
|
project:
|
|
3
3
|
name: faf-python-sdk
|
|
4
|
-
goal: Python SDK for parsing, validating, and
|
|
4
|
+
goal: Python SDK for parsing, validating, scoring, and authoring .faf project context — the Mk4 engine + interop generators other Python FAF tools build on
|
|
5
5
|
main_language: Python
|
|
6
6
|
human_context:
|
|
7
7
|
who: Python developers building AI tools, MCP servers, and CI pipelines
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "faf-python-sdk"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -61,7 +61,7 @@ packages = ["faf_sdk"]
|
|
|
61
61
|
artifacts = ["faf_sdk/dart_detection.json"]
|
|
62
62
|
|
|
63
63
|
[tool.mypy]
|
|
64
|
-
python_version = "3.
|
|
64
|
+
python_version = "3.10"
|
|
65
65
|
warn_return_any = true
|
|
66
66
|
warn_unused_configs = true
|
|
67
67
|
disallow_untyped_defs = true
|
|
@@ -69,3 +69,7 @@ disallow_untyped_defs = true
|
|
|
69
69
|
[tool.pytest.ini_options]
|
|
70
70
|
testpaths = ["tests"]
|
|
71
71
|
python_files = "test_*.py"
|
|
72
|
+
|
|
73
|
+
# PEP 561 — mark package as typed for mypy consumers (gemini-faf-mcp, etc.)
|
|
74
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
75
|
+
"faf_sdk/py.typed" = "faf_sdk/py.typed"
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Tests for faf_sdk.interop — AGENTS.md / GEMINI.md generators."""
|
|
2
|
+
|
|
3
|
+
from faf_sdk import faf_meta_tag, generate_agents_md, generate_gemini_md
|
|
4
|
+
from faf_sdk.interop import slot_label, title_label
|
|
5
|
+
|
|
6
|
+
# A representative .faf as the raw parsed dict.
|
|
7
|
+
SAMPLE = {
|
|
8
|
+
"faf_version": "3.0",
|
|
9
|
+
"project": {
|
|
10
|
+
"name": "widget-api",
|
|
11
|
+
"goal": "REST API for widget inventory",
|
|
12
|
+
"main_language": "TypeScript",
|
|
13
|
+
"type": "api",
|
|
14
|
+
"version": "2.1.0",
|
|
15
|
+
},
|
|
16
|
+
"commands": {
|
|
17
|
+
"install": "npm ci",
|
|
18
|
+
"build": "npm run build",
|
|
19
|
+
"dev": "npm run dev",
|
|
20
|
+
"test": "npm test",
|
|
21
|
+
"lint": "npm run lint",
|
|
22
|
+
},
|
|
23
|
+
"key_files": ["src/server.ts — the entry point", "src/routes/ — the handlers"],
|
|
24
|
+
"ai_instructions": {
|
|
25
|
+
"warnings": ["Never touch the billing module without a review"],
|
|
26
|
+
"working_style": {"testing": "required", "quality_bar": "zero_errors"},
|
|
27
|
+
},
|
|
28
|
+
"preferences": {"commit_style": "conventional", "communication": "direct"},
|
|
29
|
+
"security": {"secrets": ".env", "example": ".env.example"},
|
|
30
|
+
"stack": {
|
|
31
|
+
"backend": "fastify",
|
|
32
|
+
"database": "postgres",
|
|
33
|
+
"api_type": "rest",
|
|
34
|
+
"target_user": "developers",
|
|
35
|
+
},
|
|
36
|
+
"generated": "2026-09-06T00:00:00Z",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --- helpers ---------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def test_title_label_acronyms():
|
|
43
|
+
assert title_label("api_type") == "API Type"
|
|
44
|
+
assert title_label("mcp_sdk") == "MCP SDK"
|
|
45
|
+
assert title_label("runtime") == "Runtime"
|
|
46
|
+
assert title_label("state_management") == "State Management"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_slot_label_canonical_then_fallback():
|
|
50
|
+
assert slot_label("stack.cicd") == "CI/CD"
|
|
51
|
+
assert slot_label("stack.api_type") == "API"
|
|
52
|
+
assert slot_label("stack.made_up_key") == "Made Up Key"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_faf_meta_tag_two_lines():
|
|
56
|
+
tag = faf_meta_tag(SAMPLE)
|
|
57
|
+
line1, line2 = tag.split("\n")
|
|
58
|
+
assert line1 == (
|
|
59
|
+
"<!-- faf: widget-api | TypeScript | api | REST API for widget inventory -->"
|
|
60
|
+
)
|
|
61
|
+
assert line2 == "<!-- faf: claim=project.faf | family=FAF -->"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# --- AGENTS.md -----------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
def test_agents_md_sections_present():
|
|
67
|
+
md = generate_agents_md(SAMPLE)
|
|
68
|
+
for section in (
|
|
69
|
+
"# AGENTS.md — widget-api",
|
|
70
|
+
"## Setup & build",
|
|
71
|
+
"## Run the tests",
|
|
72
|
+
"## Where things live",
|
|
73
|
+
"## Conventions",
|
|
74
|
+
"## Guardrails",
|
|
75
|
+
"## Definition of Done",
|
|
76
|
+
"## When stuck",
|
|
77
|
+
"## Security & secrets",
|
|
78
|
+
"## Commit & PR",
|
|
79
|
+
"## Stack",
|
|
80
|
+
):
|
|
81
|
+
assert section in md, f"missing {section!r}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_agents_md_omits_human_context():
|
|
85
|
+
md = generate_agents_md({**SAMPLE, "human_context": {"who": "devs", "why": "money"}})
|
|
86
|
+
assert "## Human Context" not in md
|
|
87
|
+
assert "## Context" not in md
|
|
88
|
+
assert "money" not in md
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_agents_md_setup_ordered_install_build_dev():
|
|
92
|
+
md = generate_agents_md(SAMPLE)
|
|
93
|
+
block = md.split("## Setup & build")[1].split("```")[1]
|
|
94
|
+
# first line is the ```bash language tag
|
|
95
|
+
lines = [ln for ln in block.strip().splitlines()][1:]
|
|
96
|
+
assert lines[0].startswith("npm ci")
|
|
97
|
+
assert lines[1].startswith("npm run build")
|
|
98
|
+
assert lines[2].startswith("npm run dev")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_agents_md_guardrails_always_render_even_with_no_data():
|
|
102
|
+
md = generate_agents_md({"faf_version": "3.0", "project": {"name": "bare"}})
|
|
103
|
+
assert "## Guardrails" in md
|
|
104
|
+
assert "## Definition of Done" in md
|
|
105
|
+
assert "## Commit & PR" in md
|
|
106
|
+
assert "**Never:** force-push" in md
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_agents_md_key_files_table_when_roles_present():
|
|
110
|
+
md = generate_agents_md(SAMPLE)
|
|
111
|
+
assert "| Path | Role |" in md
|
|
112
|
+
assert "| `src/server.ts` | the entry point |" in md
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_agents_md_security_never_leaks_values():
|
|
116
|
+
md = generate_agents_md(SAMPLE)
|
|
117
|
+
assert "Secrets live in `.env`" in md
|
|
118
|
+
assert "see `.env.example`" in md
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_agents_md_stack_drops_non_stack_marketing_keys():
|
|
122
|
+
md = generate_agents_md(SAMPLE)
|
|
123
|
+
assert "developers" not in md.split("## Stack")[1] # target_user filtered
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_agents_md_deterministic():
|
|
127
|
+
assert generate_agents_md(SAMPLE) == generate_agents_md(dict(SAMPLE))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_agents_md_human_prefs_excluded_from_conventions():
|
|
131
|
+
md = generate_agents_md(SAMPLE)
|
|
132
|
+
conv = md.split("## Conventions")[1].split("##")[0]
|
|
133
|
+
assert "communication" not in conv.lower()
|
|
134
|
+
assert "Testing" in conv
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
# --- GEMINI.md ----------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def test_gemini_md_structure():
|
|
140
|
+
md = generate_gemini_md(SAMPLE)
|
|
141
|
+
assert md.startswith("<!-- faf:")
|
|
142
|
+
assert "# GEMINI.md — widget-api" in md
|
|
143
|
+
assert "Project: widget-api" in md
|
|
144
|
+
assert "## Setup & build" in md
|
|
145
|
+
assert "## Test & verify" in md
|
|
146
|
+
assert "## Before changing things" in md
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_gemini_md_no_guardrail_ladder():
|
|
150
|
+
# GEMINI.md is lighter — no Definition of Done / When stuck
|
|
151
|
+
md = generate_gemini_md(SAMPLE)
|
|
152
|
+
assert "## Definition of Done" not in md
|
|
153
|
+
assert "## Guardrails" not in md
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# --- edge cases -------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
def test_minimal_faf_does_not_crash():
|
|
159
|
+
minimal = {"faf_version": "3.0", "project": {"name": "x"}}
|
|
160
|
+
assert "# AGENTS.md — x" in generate_agents_md(minimal)
|
|
161
|
+
assert "# GEMINI.md — x" in generate_gemini_md(minimal)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_key_files_from_instant_context_fallback():
|
|
165
|
+
faf = {
|
|
166
|
+
"faf_version": "3.0",
|
|
167
|
+
"project": {"name": "y"},
|
|
168
|
+
"instant_context": {"key_files": ["a.py", "b.py"]},
|
|
169
|
+
}
|
|
170
|
+
md = generate_agents_md(faf)
|
|
171
|
+
assert "- `a.py`" in md
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_commands_from_instant_context_are_not_used_at_toplevel():
|
|
175
|
+
# top-level `commands` is the source; instant_context.commands is separate
|
|
176
|
+
faf = {"faf_version": "3.0", "project": {"name": "z"}, "commands": {"test": "go test ./..."}}
|
|
177
|
+
md = generate_agents_md(faf)
|
|
178
|
+
assert "go test ./..." in md
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|