faf-python-sdk 1.1.2__tar.gz → 1.2.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/.github/workflows/testpypi.yml +45 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/CHANGELOG.md +13 -0
- faf_python_sdk-1.2.0/CLAUDE.md +31 -0
- faf_python_sdk-1.2.0/CONTRIBUTING.md +152 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/PKG-INFO +22 -4
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/README.md +21 -3
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/faf_sdk/__init__.py +5 -1
- faf_python_sdk-1.2.0/faf_sdk/dart_detection.json +30 -0
- faf_python_sdk-1.2.0/faf_sdk/detect.py +167 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/pyproject.toml +6 -1
- faf_python_sdk-1.2.0/scripts/sync-dart-spec.sh +52 -0
- faf_python_sdk-1.2.0/tests/dart_parity_fixtures.json +310 -0
- faf_python_sdk-1.2.0/tests/test_dart_parity.py +40 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/.github/workflows/pypi.yml +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/.gitignore +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/CODE_OF_CONDUCT.md +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/LICENSE +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/SECURITY.md +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/docs/GROK-INTEGRATION.md +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/docs/TECHNICAL-SPEC.md +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/examples/basic_usage.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/examples/grok_integration.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/faf_sdk/discovery.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/faf_sdk/mk4.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/faf_sdk/parser.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/faf_sdk/types.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/faf_sdk/validator.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/project.faf +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/tests/__init__.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/tests/stress_test.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/tests/test_discovery.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/tests/test_mk4.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/tests/test_parser.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/tests/test_validator.py +0 -0
- {faf_python_sdk-1.1.2 → faf_python_sdk-1.2.0}/tests/test_wjttc.py +0 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish to TestPyPI
|
|
2
|
+
|
|
3
|
+
# Fires on every version tag push (e.g. `git push origin v0.1.5`).
|
|
4
|
+
# Stage 1 of the publish flow: stage to TestPyPI for verification BEFORE
|
|
5
|
+
# creating a GitHub Release (which then fires pypi.yml for production).
|
|
6
|
+
#
|
|
7
|
+
# Flow:
|
|
8
|
+
# 1. git tag v0.1.5 && git push origin v0.1.5 → this workflow runs
|
|
9
|
+
# 2. verify install from TestPyPI → manual / via verify_install.py
|
|
10
|
+
# 3. gh release create v0.1.5 → triggers pypi.yml (production)
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
tags:
|
|
15
|
+
- 'v*'
|
|
16
|
+
workflow_dispatch: # Manual trigger
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
publish-testpypi:
|
|
20
|
+
name: Publish to TestPyPI
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
environment: testpypi
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write # Required for OIDC trusted publishing
|
|
25
|
+
|
|
26
|
+
steps:
|
|
27
|
+
- uses: actions/checkout@v6
|
|
28
|
+
|
|
29
|
+
- name: Set up Python
|
|
30
|
+
uses: actions/setup-python@v6
|
|
31
|
+
with:
|
|
32
|
+
python-version: '3.12'
|
|
33
|
+
|
|
34
|
+
- name: Install build tools
|
|
35
|
+
run: pip install build
|
|
36
|
+
|
|
37
|
+
- name: Build package
|
|
38
|
+
run: python -m build
|
|
39
|
+
|
|
40
|
+
- name: Publish to TestPyPI
|
|
41
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
42
|
+
with:
|
|
43
|
+
repository-url: https://test.pypi.org/legacy/
|
|
44
|
+
# No token — uses OIDC trusted publisher configured at
|
|
45
|
+
# https://test.pypi.org/manage/project/faf-python-sdk/settings/publishing/
|
|
@@ -3,6 +3,19 @@
|
|
|
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.2.0] - 2026-06-16 — The Dart Edition
|
|
7
|
+
|
|
8
|
+
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.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `detect_dart_project(dir)` → `DartProject` — the SDK's first detection capability. Reads `pubspec.yaml` and classifies: Flutter app vs reusable package, Dart MCP server, Dart backend (Serverpod / Dart Frog / Shelf / …), Dart CLI, or library. Exported from `faf_sdk`.
|
|
12
|
+
- `faf_sdk/dart_detection.json` — the detection KNOWLEDGE spec, vendored byte-identical from faf-cli (the single source); ships in the wheel, loaded at runtime.
|
|
13
|
+
- `tests/test_dart_parity.py` — 20 shared fixtures run identically by faf-cli and this SDK; parity proven by test, not by eye.
|
|
14
|
+
- `scripts/sync-dart-spec.sh` — vendor + `--check` (byte-identity) the spec & fixtures from faf-cli.
|
|
15
|
+
|
|
16
|
+
### Notes
|
|
17
|
+
- Mirrors faf-cli `src/detect/dart.ts` exactly (A+B hybrid). To bolster Dart support, edit the spec in faf-cli (the Truth) and re-sync. No new runtime dependencies.
|
|
18
|
+
|
|
6
19
|
## [1.1.2] - 2026-04-26
|
|
7
20
|
|
|
8
21
|
### Changed
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<!-- faf: faf-python-sdk | Python | | Python SDK for parsing, validating, and scoring .faf files with Mk4 Championship Engine -->
|
|
2
|
+
<!-- faf: claim=project.faf | family=FAF -->
|
|
3
|
+
|
|
4
|
+
# CLAUDE.md — faf-python-sdk
|
|
5
|
+
|
|
6
|
+
## What This Is
|
|
7
|
+
|
|
8
|
+
Python SDK for parsing, validating, and scoring .faf files with Mk4 Championship Engine
|
|
9
|
+
|
|
10
|
+
## Stack
|
|
11
|
+
|
|
12
|
+
- **Language:** Python
|
|
13
|
+
- **Backend:** Python
|
|
14
|
+
- **Api Type:** SDK (library)
|
|
15
|
+
- **Runtime:** Python 3.8+
|
|
16
|
+
- **Hosting:** PyPI
|
|
17
|
+
- **Build:** hatchling
|
|
18
|
+
- **Cicd:** GitHub Actions
|
|
19
|
+
|
|
20
|
+
## Context
|
|
21
|
+
|
|
22
|
+
- **Who:** Python developers building AI tools, MCP servers, and CI pipelines
|
|
23
|
+
- **What:** Parse, validate, and score FAF project DNA files with 100% Mk4 parity
|
|
24
|
+
- **Why:** Universal scoring across all FAF tools — same slots, same formula, same scores
|
|
25
|
+
- **Where:** PyPI (pip install faf-python-sdk)
|
|
26
|
+
- **When:** Every AI session that needs project context
|
|
27
|
+
- **How:** Import faf_sdk, call score_faf() or parse_file()
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
*STATUS: BI-SYNC ACTIVE — 2026-06-16T20:16:30.936Z*
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Contributions are welcome. Bug fixes, doc improvements, new validators,
|
|
4
|
+
new scoring tactics in `mk4`, new discovery sources — all useful.
|
|
5
|
+
|
|
6
|
+
This file describes **how to land a change cleanly**.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
git clone https://github.com/Wolfe-Jam/faf-python-sdk
|
|
14
|
+
cd faf-python-sdk
|
|
15
|
+
python3 -m venv .venv
|
|
16
|
+
source .venv/bin/activate
|
|
17
|
+
pip install -e ".[dev]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. You're ready to run tests and ship a fix.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Before opening a PR
|
|
25
|
+
|
|
26
|
+
Run the full check pass:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pytest tests/ -v # all tests must pass
|
|
30
|
+
mypy faf_sdk/ # strict typing — no untyped defs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
If both come back clean, you're good to push. PRs that don't pass tests
|
|
34
|
+
will not be reviewed.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## PR conventions
|
|
39
|
+
|
|
40
|
+
| Type of change | Required |
|
|
41
|
+
|---|---|
|
|
42
|
+
| Bug fix | A regression test that fails on the bug, passes after the fix. The test comes **with** the fix, not after. |
|
|
43
|
+
| New feature | Tests for the new surface. If it touches the `.faf` format itself, see "Architecture decisions" below — the SDK doesn't extend the format unilaterally. |
|
|
44
|
+
| Doc-only | No tests required. README + CHANGELOG entries still expected. |
|
|
45
|
+
| Refactor | Existing tests must pass unchanged. Coverage must stay ≥ existing baseline. |
|
|
46
|
+
|
|
47
|
+
`mypy faf_sdk/` must be clean. Strict mode is on (`disallow_untyped_defs = true`).
|
|
48
|
+
The `[tool.mypy]` section in `pyproject.toml` is authoritative.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Branch model
|
|
53
|
+
|
|
54
|
+
- `main` is always shippable. Tagged releases come from `main`.
|
|
55
|
+
- Work on feature branches. PR → squash-merge into `main`.
|
|
56
|
+
- Don't open PRs against tagged commits. Tags are immutable; any
|
|
57
|
+
fix lands on `main` and gets a new tag if it's release-worthy.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Commit messages
|
|
62
|
+
|
|
63
|
+
- Imperative mood: "fix: handle empty .faf gracefully" not "fixed" / "fixes".
|
|
64
|
+
- Conventional-commits prefix is appreciated but not enforced
|
|
65
|
+
(`fix:` / `feat:` / `refactor:` / `chore:` / `docs:` / `test:`).
|
|
66
|
+
- Body explains the **why** when it's non-obvious. The diff explains
|
|
67
|
+
the what.
|
|
68
|
+
- No marketing language in commit subjects. Commit messages are
|
|
69
|
+
technical, not promotional.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Code style
|
|
74
|
+
|
|
75
|
+
- **Names over comments.** A well-named function or variable doesn't
|
|
76
|
+
need a comment explaining what it does.
|
|
77
|
+
- **WHY-comments are welcome** where the *why* isn't obvious — a
|
|
78
|
+
hidden constraint, a workaround for a specific bug, a non-obvious
|
|
79
|
+
invariant.
|
|
80
|
+
- **No marketing prose in code comments.** Internal docs are
|
|
81
|
+
documentation, not pitch material.
|
|
82
|
+
- Type hints on all public signatures. `disallow_untyped_defs` is on.
|
|
83
|
+
- `from __future__ import annotations` at the top of any new module
|
|
84
|
+
using `|` union types — keeps Python 3.8/3.9 compatibility.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Adding a new validator / scorer / parser
|
|
89
|
+
|
|
90
|
+
The SDK's modules are deliberately narrow:
|
|
91
|
+
|
|
92
|
+
| Module | Job |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `parser` | Read `.faf` (YAML) into typed dicts |
|
|
95
|
+
| `validator` | Check `.faf` against the schema |
|
|
96
|
+
| `mk4` | Score `.faf` (the FAF "ECU" — championship tiers) |
|
|
97
|
+
| `discovery` | Find `.faf` files in a project |
|
|
98
|
+
| `types` | Shared dataclasses + protocols |
|
|
99
|
+
|
|
100
|
+
When adding to one of these, **stay in lane**. A scoring change goes in
|
|
101
|
+
`mk4`, not `validator`. A new file-finder goes in `discovery`. Cross-cutting
|
|
102
|
+
features need a small RFC in the PR description before code lands.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Architecture decisions
|
|
107
|
+
|
|
108
|
+
The SDK has firm design rules that aren't up for debate in PRs:
|
|
109
|
+
|
|
110
|
+
1. **The `.faf` format spec lives in the FAF organization, not the SDK.**
|
|
111
|
+
PRs that change parser/validator behavior to "support a new format
|
|
112
|
+
feature" before the format spec adds it will be closed. The SDK
|
|
113
|
+
*implements* the spec; it doesn't *extend* it.
|
|
114
|
+
2. **MIT-licensed and dependency-light.** The SDK has one runtime
|
|
115
|
+
dependency (`pyyaml`) and that's the floor we want to defend. PRs
|
|
116
|
+
that add runtime deps need strong justification.
|
|
117
|
+
3. **The SDK is the foundation other Python FAF tools build on**
|
|
118
|
+
(gemini-faf-mcp, custom MCP servers, CI validators). Breaking
|
|
119
|
+
changes to public surfaces go behind a major version bump.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## CI doctrine
|
|
124
|
+
|
|
125
|
+
Two rules from the project's CI philosophy:
|
|
126
|
+
|
|
127
|
+
1. **Red means real.** A red `test` job is a real, actionable failure.
|
|
128
|
+
We don't tolerate flaky tests — if a test fails intermittently,
|
|
129
|
+
that's a bug in the test, fix it before merging anything else.
|
|
130
|
+
2. **Type-check is observability, not a gate.** mypy failures show up
|
|
131
|
+
but tests are the only hard gate. Type cleanup PRs are welcome but
|
|
132
|
+
never urgent.
|
|
133
|
+
|
|
134
|
+
If CI goes red after a merge, the breaking change owns the
|
|
135
|
+
fix — revert is on the table, no shame.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Where to file issues
|
|
140
|
+
|
|
141
|
+
[github.com/Wolfe-Jam/faf-python-sdk/issues](https://github.com/Wolfe-Jam/faf-python-sdk/issues)
|
|
142
|
+
|
|
143
|
+
For security issues: don't open a public issue. Email
|
|
144
|
+
**team@faf.one** with details.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT. Fork it, ship it, embed it, enjoy it.
|
|
151
|
+
|
|
152
|
+
**Don't copy FAF brand. Do your own.**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: faf-python-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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,33 @@ 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.2.0 — The Dart Edition
|
|
50
|
+
|
|
51
|
+
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
|
+
|
|
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
|
+
```python
|
|
56
|
+
from faf_sdk import detect_dart_project
|
|
57
|
+
|
|
58
|
+
d = detect_dart_project(".")
|
|
59
|
+
print(d.app_type, d.framework) # e.g. "mobile" "Flutter"
|
|
60
|
+
```
|
|
61
|
+
|
|
46
62
|
## What's New in v1.1.0
|
|
47
63
|
|
|
48
64
|
**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 +191,8 @@ root = find_project_root()
|
|
|
175
191
|
| [grok-faf-mcp](https://npmjs.com/package/grok-faf-mcp) | xAI | npm |
|
|
176
192
|
| [faf-cli](https://npmjs.com/package/faf-cli) | CLI | npm |
|
|
177
193
|
|
|
194
|
+
If `faf-python-sdk` has been useful, consider starring the repo — it helps others find it.
|
|
195
|
+
|
|
178
196
|
## Links
|
|
179
197
|
|
|
180
198
|
- **Site:** [faf.one](https://faf.one)
|
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
# faf-python-sdk
|
|
2
2
|
|
|
3
|
-
**Persistent
|
|
3
|
+
**Persistent Project Context for Python. Parse, validate, score.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**FAF defines. MD instructs. AI codes.**
|
|
6
6
|
|
|
7
|
+
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.
|
|
8
|
+
|
|
9
|
+
[](https://builder.faf.one)
|
|
7
10
|
[](https://pypi.org/project/faf-python-sdk/)
|
|
8
11
|
[](https://pypi.org/project/faf-python-sdk/)
|
|
9
|
-
[](https://github.com/Wolfe-Jam/faf-python-sdk)
|
|
10
13
|
[](https://www.iana.org/assignments/media-types/application/vnd.faf+yaml)
|
|
11
14
|
|
|
12
15
|
**Media Type:** `application/vnd.faf+yaml` (IANA registered)
|
|
13
16
|
|
|
17
|
+
## What's New in v1.2.0 — The Dart Edition
|
|
18
|
+
|
|
19
|
+
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
|
+
|
|
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
|
+
```python
|
|
24
|
+
from faf_sdk import detect_dart_project
|
|
25
|
+
|
|
26
|
+
d = detect_dart_project(".")
|
|
27
|
+
print(d.app_type, d.framework) # e.g. "mobile" "Flutter"
|
|
28
|
+
```
|
|
29
|
+
|
|
14
30
|
## What's New in v1.1.0
|
|
15
31
|
|
|
16
32
|
**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.
|
|
@@ -143,6 +159,8 @@ root = find_project_root()
|
|
|
143
159
|
| [grok-faf-mcp](https://npmjs.com/package/grok-faf-mcp) | xAI | npm |
|
|
144
160
|
| [faf-cli](https://npmjs.com/package/faf-cli) | CLI | npm |
|
|
145
161
|
|
|
162
|
+
If `faf-python-sdk` has been useful, consider starring the repo — it helps others find it.
|
|
163
|
+
|
|
146
164
|
## Links
|
|
147
165
|
|
|
148
166
|
- **Site:** [faf.one](https://faf.one)
|
|
@@ -24,6 +24,7 @@ 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
|
|
27
28
|
from .types import (
|
|
28
29
|
FafData,
|
|
29
30
|
ProjectInfo,
|
|
@@ -34,7 +35,7 @@ from .types import (
|
|
|
34
35
|
AIScoring
|
|
35
36
|
)
|
|
36
37
|
|
|
37
|
-
__version__ = "1.
|
|
38
|
+
__version__ = "1.2.0"
|
|
38
39
|
__all__ = [
|
|
39
40
|
# Parser
|
|
40
41
|
"parse",
|
|
@@ -53,6 +54,9 @@ __all__ = [
|
|
|
53
54
|
"find_faf_file",
|
|
54
55
|
"find_project_root",
|
|
55
56
|
"load_fafignore",
|
|
57
|
+
# Detection (Dart/Flutter — A+B hybrid, parity with faf-cli)
|
|
58
|
+
"detect_dart_project",
|
|
59
|
+
"DartProject",
|
|
56
60
|
# Types
|
|
57
61
|
"FafData",
|
|
58
62
|
"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
|
+
}
|
|
@@ -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
|
+
)
|
|
@@ -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.2.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"}
|
|
@@ -55,6 +55,11 @@ Issues = "https://github.com/Wolfe-Jam/faf-python-sdk/issues"
|
|
|
55
55
|
[tool.hatch.build.targets.wheel]
|
|
56
56
|
packages = ["faf_sdk"]
|
|
57
57
|
|
|
58
|
+
# Ship the vendored Dart-detection KNOWLEDGE spec (loaded at runtime by detect.py).
|
|
59
|
+
# Synced byte-for-byte from faf-cli src/detect/dart-detection.json (the Truth).
|
|
60
|
+
[tool.hatch.build]
|
|
61
|
+
artifacts = ["faf_sdk/dart_detection.json"]
|
|
62
|
+
|
|
58
63
|
[tool.mypy]
|
|
59
64
|
python_version = "3.9"
|
|
60
65
|
warn_return_any = true
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# sync-dart-spec.sh — vendor the Dart-detection assets from faf-cli (the Truth).
|
|
4
|
+
#
|
|
5
|
+
# The .faf detection KNOWLEDGE lives ONCE, in faf-cli. This script copies it,
|
|
6
|
+
# byte-for-byte, into faf-python-sdk so the Python detector and its parity
|
|
7
|
+
# fixtures stay identical to the TypeScript source (A+B hybrid).
|
|
8
|
+
#
|
|
9
|
+
# ./scripts/sync-dart-spec.sh # copy faf-cli -> sdk (default)
|
|
10
|
+
# ./scripts/sync-dart-spec.sh --check # verify identical, exit 1 on drift (CI)
|
|
11
|
+
#
|
|
12
|
+
# Override the faf-cli location with FAF_CLI=/path/to/cli
|
|
13
|
+
set -euo pipefail
|
|
14
|
+
|
|
15
|
+
SDK_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
16
|
+
CLI="${FAF_CLI:-$HOME/FAF/cli}"
|
|
17
|
+
|
|
18
|
+
# source (faf-cli, the Truth) -> dest (sdk, vendored)
|
|
19
|
+
SPEC_SRC="$CLI/src/detect/dart-detection.json"
|
|
20
|
+
SPEC_DST="$SDK_ROOT/faf_sdk/dart_detection.json"
|
|
21
|
+
FIX_SRC="$CLI/tests/detect/dart-parity-fixtures.json"
|
|
22
|
+
FIX_DST="$SDK_ROOT/tests/dart_parity_fixtures.json"
|
|
23
|
+
|
|
24
|
+
mode="${1:-copy}"
|
|
25
|
+
|
|
26
|
+
check_pair() {
|
|
27
|
+
local src="$1" dst="$2"
|
|
28
|
+
if [ ! -f "$src" ]; then
|
|
29
|
+
echo "❌ source missing: $src (set FAF_CLI=/path/to/cli)"; exit 1
|
|
30
|
+
fi
|
|
31
|
+
if [ ! -f "$dst" ] || ! diff -q "$src" "$dst" >/dev/null 2>&1; then
|
|
32
|
+
echo "❌ DRIFT: $dst differs from faf-cli source $src"
|
|
33
|
+
echo " run: ./scripts/sync-dart-spec.sh"
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
echo "✅ identical: $(basename "$dst")"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if [ "$mode" = "--check" ]; then
|
|
40
|
+
echo "Checking Dart-spec parity against faf-cli ($CLI)…"
|
|
41
|
+
check_pair "$SPEC_SRC" "$SPEC_DST"
|
|
42
|
+
check_pair "$FIX_SRC" "$FIX_DST"
|
|
43
|
+
echo "✅ vendored Dart assets are in sync with faf-cli"
|
|
44
|
+
else
|
|
45
|
+
for pair in "$SPEC_SRC:$SPEC_DST" "$FIX_SRC:$FIX_DST"; do
|
|
46
|
+
src="${pair%%:*}"; dst="${pair##*:}"
|
|
47
|
+
[ -f "$src" ] || { echo "❌ source missing: $src (set FAF_CLI=/path/to/cli)"; exit 1; }
|
|
48
|
+
cp "$src" "$dst"
|
|
49
|
+
echo "synced: $src -> $dst"
|
|
50
|
+
done
|
|
51
|
+
echo "✅ Dart assets vendored from faf-cli. Commit the changes."
|
|
52
|
+
fi
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"_doc": "SHARED cross-language parity fixtures for Dart/Flutter detection (A+B hybrid). faf-cli (tests/detect/dart-parity.test.ts) and faf-python-sdk (tests/test_dart_parity.py) run THIS SAME file: identical input pubspecs -> identical expected classification. The expected values are the faf-cli (TypeScript) Truth; the Python detector must reproduce them byte-for-byte. A byte-identical copy is vendored to faf-python-sdk/tests/dart_parity_fixtures.json via faf-python-sdk/scripts/sync-dart-spec.sh. If the two engines ever diverge, one suite goes red. Edit fixtures HERE (faf-cli, the Truth), then re-sync. 'files' maps a relative path to its content ('' = empty file, parent dirs created). 'expected' is the full DartProject projection.",
|
|
4
|
+
"fixtures": [
|
|
5
|
+
{
|
|
6
|
+
"name": "flutter-app-canonical (main.dart + riverpod + go_router + flutter_test)",
|
|
7
|
+
"files": {
|
|
8
|
+
"pubspec.yaml": "name: my_app\ndependencies:\n flutter:\n sdk: flutter\n flutter_riverpod: ^2.5.0\n go_router: ^14.0.0\ndev_dependencies:\n flutter_test:\n sdk: flutter\n",
|
|
9
|
+
"lib/main.dart": ""
|
|
10
|
+
},
|
|
11
|
+
"expected": {
|
|
12
|
+
"appType": "mobile",
|
|
13
|
+
"isFlutter": true,
|
|
14
|
+
"framework": "Flutter",
|
|
15
|
+
"stateManagement": "Riverpod",
|
|
16
|
+
"routing": "go_router",
|
|
17
|
+
"testing": "flutter_test",
|
|
18
|
+
"found": "pubspec.yaml (Flutter app)"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "flutter-app-publish-none (no main.dart, bloc)",
|
|
23
|
+
"files": {
|
|
24
|
+
"pubspec.yaml": "name: my_app\npublish_to: none\ndependencies:\n flutter:\n sdk: flutter\n flutter_bloc: ^8.1.0\n"
|
|
25
|
+
},
|
|
26
|
+
"expected": {
|
|
27
|
+
"appType": "mobile",
|
|
28
|
+
"isFlutter": true,
|
|
29
|
+
"framework": "Flutter",
|
|
30
|
+
"stateManagement": "Bloc",
|
|
31
|
+
"routing": "",
|
|
32
|
+
"testing": "",
|
|
33
|
+
"found": "pubspec.yaml (Flutter app)"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "flutter-package (flutter dep, publishable, provider)",
|
|
38
|
+
"files": {
|
|
39
|
+
"pubspec.yaml": "name: my_widgets\ndependencies:\n flutter:\n sdk: flutter\n provider: ^6.1.0\n"
|
|
40
|
+
},
|
|
41
|
+
"expected": {
|
|
42
|
+
"appType": "library",
|
|
43
|
+
"isFlutter": true,
|
|
44
|
+
"framework": "Flutter",
|
|
45
|
+
"stateManagement": "Provider",
|
|
46
|
+
"routing": "",
|
|
47
|
+
"testing": "",
|
|
48
|
+
"found": "pubspec.yaml (Flutter package)"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"name": "flutter-section-only (top-level flutter:, no flutter dep)",
|
|
53
|
+
"files": {
|
|
54
|
+
"pubspec.yaml": "name: theme_pack\ndependencies:\n collection: ^1.18.0\nflutter:\n uses-material-design: true\n"
|
|
55
|
+
},
|
|
56
|
+
"expected": {
|
|
57
|
+
"appType": "library",
|
|
58
|
+
"isFlutter": true,
|
|
59
|
+
"framework": "Flutter",
|
|
60
|
+
"stateManagement": "",
|
|
61
|
+
"routing": "",
|
|
62
|
+
"testing": "",
|
|
63
|
+
"found": "pubspec.yaml (Flutter package)"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"name": "flutter-app-getx-autoroute (publish_to none, get + auto_route)",
|
|
68
|
+
"files": {
|
|
69
|
+
"pubspec.yaml": "name: shop_app\npublish_to: none\ndependencies:\n flutter:\n sdk: flutter\n get: ^4.6.0\n auto_route: ^9.0.0\n"
|
|
70
|
+
},
|
|
71
|
+
"expected": {
|
|
72
|
+
"appType": "mobile",
|
|
73
|
+
"isFlutter": true,
|
|
74
|
+
"framework": "Flutter",
|
|
75
|
+
"stateManagement": "GetX",
|
|
76
|
+
"routing": "auto_route",
|
|
77
|
+
"testing": "",
|
|
78
|
+
"found": "pubspec.yaml (Flutter app)"
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"name": "flutter-app-bloc-precedence (flutter_bloc beats provider)",
|
|
83
|
+
"files": {
|
|
84
|
+
"pubspec.yaml": "name: my_app\ndependencies:\n flutter:\n sdk: flutter\n provider: ^6.1.0\n flutter_bloc: ^8.1.0\ndev_dependencies:\n flutter_test:\n sdk: flutter\n",
|
|
85
|
+
"lib/main.dart": ""
|
|
86
|
+
},
|
|
87
|
+
"expected": {
|
|
88
|
+
"appType": "mobile",
|
|
89
|
+
"isFlutter": true,
|
|
90
|
+
"framework": "Flutter",
|
|
91
|
+
"stateManagement": "Bloc",
|
|
92
|
+
"routing": "",
|
|
93
|
+
"testing": "flutter_test",
|
|
94
|
+
"found": "pubspec.yaml (Flutter app)"
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "flutter-app-mobx (flutter_mobx beats mobx)",
|
|
99
|
+
"files": {
|
|
100
|
+
"pubspec.yaml": "name: my_app\ndependencies:\n flutter:\n sdk: flutter\n flutter_mobx: ^2.2.0\n mobx: ^2.3.0\n",
|
|
101
|
+
"lib/main.dart": ""
|
|
102
|
+
},
|
|
103
|
+
"expected": {
|
|
104
|
+
"appType": "mobile",
|
|
105
|
+
"isFlutter": true,
|
|
106
|
+
"framework": "Flutter",
|
|
107
|
+
"stateManagement": "MobX",
|
|
108
|
+
"routing": "",
|
|
109
|
+
"testing": "",
|
|
110
|
+
"found": "pubspec.yaml (Flutter app)"
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"name": "dart-mcp-server (dart_mcp)",
|
|
115
|
+
"files": {
|
|
116
|
+
"pubspec.yaml": "name: my_mcp\ndependencies:\n dart_mcp: ^0.2.0\n"
|
|
117
|
+
},
|
|
118
|
+
"expected": {
|
|
119
|
+
"appType": "mcp",
|
|
120
|
+
"isFlutter": false,
|
|
121
|
+
"framework": "",
|
|
122
|
+
"stateManagement": "",
|
|
123
|
+
"routing": "",
|
|
124
|
+
"testing": "",
|
|
125
|
+
"found": "pubspec.yaml + dart_mcp (Dart MCP server)"
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "dart-mcp-server (mcp_server)",
|
|
130
|
+
"files": {
|
|
131
|
+
"pubspec.yaml": "name: my_mcp\ndependencies:\n mcp_server: ^0.1.0\n"
|
|
132
|
+
},
|
|
133
|
+
"expected": {
|
|
134
|
+
"appType": "mcp",
|
|
135
|
+
"isFlutter": false,
|
|
136
|
+
"framework": "",
|
|
137
|
+
"stateManagement": "",
|
|
138
|
+
"routing": "",
|
|
139
|
+
"testing": "",
|
|
140
|
+
"found": "pubspec.yaml + mcp_server (Dart MCP server)"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
"name": "dart-mcp-server (generic mcp)",
|
|
145
|
+
"files": {
|
|
146
|
+
"pubspec.yaml": "name: my_mcp\ndependencies:\n mcp: ^0.1.0\n"
|
|
147
|
+
},
|
|
148
|
+
"expected": {
|
|
149
|
+
"appType": "mcp",
|
|
150
|
+
"isFlutter": false,
|
|
151
|
+
"framework": "",
|
|
152
|
+
"stateManagement": "",
|
|
153
|
+
"routing": "",
|
|
154
|
+
"testing": "",
|
|
155
|
+
"found": "pubspec.yaml + mcp (Dart MCP server)"
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
"name": "flutter-beats-mcp (Flutter app that deps mcp -> still mobile)",
|
|
160
|
+
"files": {
|
|
161
|
+
"pubspec.yaml": "name: my_app\npublish_to: none\ndependencies:\n flutter:\n sdk: flutter\n mcp: ^0.1.0\n"
|
|
162
|
+
},
|
|
163
|
+
"expected": {
|
|
164
|
+
"appType": "mobile",
|
|
165
|
+
"isFlutter": true,
|
|
166
|
+
"framework": "Flutter",
|
|
167
|
+
"stateManagement": "",
|
|
168
|
+
"routing": "",
|
|
169
|
+
"testing": "",
|
|
170
|
+
"found": "pubspec.yaml (Flutter app)"
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "dart-backend-serverpod",
|
|
175
|
+
"files": {
|
|
176
|
+
"pubspec.yaml": "name: my_api\ndependencies:\n serverpod: ^2.1.0\n"
|
|
177
|
+
},
|
|
178
|
+
"expected": {
|
|
179
|
+
"appType": "backend",
|
|
180
|
+
"isFlutter": false,
|
|
181
|
+
"framework": "Serverpod",
|
|
182
|
+
"stateManagement": "",
|
|
183
|
+
"routing": "",
|
|
184
|
+
"testing": "",
|
|
185
|
+
"found": "pubspec.yaml + serverpod (Dart backend)"
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
"name": "dart-backend-dartfrog-precedence (dart_frog beats shelf)",
|
|
190
|
+
"files": {
|
|
191
|
+
"pubspec.yaml": "name: my_api\ndependencies:\n dart_frog: ^1.1.0\n shelf: ^1.4.0\n"
|
|
192
|
+
},
|
|
193
|
+
"expected": {
|
|
194
|
+
"appType": "backend",
|
|
195
|
+
"isFlutter": false,
|
|
196
|
+
"framework": "Dart Frog",
|
|
197
|
+
"stateManagement": "",
|
|
198
|
+
"routing": "",
|
|
199
|
+
"testing": "",
|
|
200
|
+
"found": "pubspec.yaml + dart_frog (Dart backend)"
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
"name": "dart-backend-shelf (shelf_router is noise)",
|
|
205
|
+
"files": {
|
|
206
|
+
"pubspec.yaml": "name: my_api\ndependencies:\n shelf: ^1.4.0\n shelf_router: ^1.1.0\n"
|
|
207
|
+
},
|
|
208
|
+
"expected": {
|
|
209
|
+
"appType": "backend",
|
|
210
|
+
"isFlutter": false,
|
|
211
|
+
"framework": "Shelf",
|
|
212
|
+
"stateManagement": "",
|
|
213
|
+
"routing": "",
|
|
214
|
+
"testing": "",
|
|
215
|
+
"found": "pubspec.yaml + shelf (Dart backend)"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "dart-cli-executables",
|
|
220
|
+
"files": {
|
|
221
|
+
"pubspec.yaml": "name: mytool\nexecutables:\n mytool:\ndependencies:\n args: ^2.5.0\n"
|
|
222
|
+
},
|
|
223
|
+
"expected": {
|
|
224
|
+
"appType": "cli",
|
|
225
|
+
"isFlutter": false,
|
|
226
|
+
"framework": "",
|
|
227
|
+
"stateManagement": "",
|
|
228
|
+
"routing": "",
|
|
229
|
+
"testing": "",
|
|
230
|
+
"found": "pubspec.yaml executables: (Dart CLI)"
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
"name": "dart-cli-bin (bin/*.dart entry, no executables:)",
|
|
235
|
+
"files": {
|
|
236
|
+
"pubspec.yaml": "name: mytool\ndependencies:\n args: ^2.5.0\n",
|
|
237
|
+
"bin/mytool.dart": ""
|
|
238
|
+
},
|
|
239
|
+
"expected": {
|
|
240
|
+
"appType": "cli",
|
|
241
|
+
"isFlutter": false,
|
|
242
|
+
"framework": "",
|
|
243
|
+
"stateManagement": "",
|
|
244
|
+
"routing": "",
|
|
245
|
+
"testing": "",
|
|
246
|
+
"found": "pubspec.yaml + bin/*.dart (Dart CLI)"
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "dart-library-plain",
|
|
251
|
+
"files": {
|
|
252
|
+
"pubspec.yaml": "name: my_package\ndependencies:\n meta: ^1.12.0\n"
|
|
253
|
+
},
|
|
254
|
+
"expected": {
|
|
255
|
+
"appType": "library",
|
|
256
|
+
"isFlutter": false,
|
|
257
|
+
"framework": "",
|
|
258
|
+
"stateManagement": "",
|
|
259
|
+
"routing": "",
|
|
260
|
+
"testing": "",
|
|
261
|
+
"found": "pubspec.yaml (Dart package)"
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"name": "dart-library-with-test (test dev_dep)",
|
|
266
|
+
"files": {
|
|
267
|
+
"pubspec.yaml": "name: my_package\ndev_dependencies:\n test: ^1.25.0\n"
|
|
268
|
+
},
|
|
269
|
+
"expected": {
|
|
270
|
+
"appType": "library",
|
|
271
|
+
"isFlutter": false,
|
|
272
|
+
"framework": "",
|
|
273
|
+
"stateManagement": "",
|
|
274
|
+
"routing": "",
|
|
275
|
+
"testing": "test",
|
|
276
|
+
"found": "pubspec.yaml (Dart package)"
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
"name": "dart-library-dependency-overrides-noise",
|
|
281
|
+
"files": {
|
|
282
|
+
"pubspec.yaml": "name: my_package\ndependencies:\n collection: ^1.18.0\ndependency_overrides:\n meta: ^1.0.0\n"
|
|
283
|
+
},
|
|
284
|
+
"expected": {
|
|
285
|
+
"appType": "library",
|
|
286
|
+
"isFlutter": false,
|
|
287
|
+
"framework": "",
|
|
288
|
+
"stateManagement": "",
|
|
289
|
+
"routing": "",
|
|
290
|
+
"testing": "",
|
|
291
|
+
"found": "pubspec.yaml (Dart package)"
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"name": "mcp-beats-cli (dart_mcp dep + executables -> mcp)",
|
|
296
|
+
"files": {
|
|
297
|
+
"pubspec.yaml": "name: my_mcp\nexecutables:\n my_mcp:\ndependencies:\n dart_mcp: ^0.2.0\n"
|
|
298
|
+
},
|
|
299
|
+
"expected": {
|
|
300
|
+
"appType": "mcp",
|
|
301
|
+
"isFlutter": false,
|
|
302
|
+
"framework": "",
|
|
303
|
+
"stateManagement": "",
|
|
304
|
+
"routing": "",
|
|
305
|
+
"testing": "",
|
|
306
|
+
"found": "pubspec.yaml + dart_mcp (Dart MCP server)"
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
]
|
|
310
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cross-language PARITY — the Python Dart detector must reproduce faf-cli's
|
|
3
|
+
(TypeScript) classification byte-for-byte on the SHARED fixtures.
|
|
4
|
+
|
|
5
|
+
tests/dart_parity_fixtures.json is a byte-identical, synced copy of faf-cli's
|
|
6
|
+
tests/detect/dart-parity-fixtures.json (see scripts/sync-dart-spec.sh). faf-cli
|
|
7
|
+
runs the same fixtures in tests/detect/dart-parity.test.ts and asserts the same
|
|
8
|
+
expected projection. Same input + same expected on both engines = parity proven
|
|
9
|
+
by test, not by eye. If the engines ever diverge, one suite goes red.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from faf_sdk import detect_dart_project
|
|
19
|
+
|
|
20
|
+
_FIXTURE_FILE = Path(__file__).parent / "dart_parity_fixtures.json"
|
|
21
|
+
FIXTURES = json.loads(_FIXTURE_FILE.read_text(encoding="utf-8"))["fixtures"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _materialize(root: Path, files: Dict[str, str]) -> None:
|
|
25
|
+
for rel, content in files.items():
|
|
26
|
+
p = root / rel
|
|
27
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
p.write_text(content, encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_fixture_set_is_non_trivial() -> None:
|
|
32
|
+
assert len(FIXTURES) >= 20
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.parametrize("fx", FIXTURES, ids=[f["name"] for f in FIXTURES])
|
|
36
|
+
def test_dart_parity(fx: Dict[str, Any], tmp_path: Path) -> None:
|
|
37
|
+
_materialize(tmp_path, fx["files"])
|
|
38
|
+
result = detect_dart_project(str(tmp_path))
|
|
39
|
+
assert result is not None, f"{fx['name']}: detector returned None"
|
|
40
|
+
assert result.to_dict() == fx["expected"], fx["name"]
|
|
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
|