ruff-sync 0.1.3.dev3__tar.gz → 0.1.4.dev0__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.
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/TESTING.md +7 -1
- ruff_sync-0.1.4.dev0/.agents/formatters-architecture.md +219 -0
- ruff_sync-0.1.4.dev0/.agents/gitlab-reports.md +614 -0
- ruff_sync-0.1.4.dev0/.agents/issue-102-context.md +81 -0
- ruff_sync-0.1.4.dev0/.agents/skills/gh-issues/SKILL.md +181 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/ruff-sync-usage/references/ci-integration.md +1 -1
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/AGENTS.md +2 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/PKG-INFO +2 -2
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/README.md +1 -1
- ruff_sync-0.1.4.dev0/docs/assets/github-pr-annotation.png +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/ci-integration.md +2 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/pre-commit.md +3 -3
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/pre-defined-configs.md +4 -4
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/pyproject.toml +1 -1
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/src/ruff_sync/constants.py +1 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/src/ruff_sync/core.py +123 -116
- ruff_sync-0.1.4.dev0/src/ruff_sync/formatters.py +481 -0
- ruff_sync-0.1.4.dev0/tests/test_formatters.py +529 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/uv.lock +1 -1
- ruff_sync-0.1.3.dev3/src/ruff_sync/formatters.py +0 -243
- ruff_sync-0.1.3.dev3/tests/test_formatters.py +0 -154
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/mkdocs-generation/SKILL.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/mkdocs-generation/examples.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/mkdocs-generation/templates/api-reference.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/mkdocs-generation/templates/getting-started.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/mkdocs-generation/templates/index.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/mkdocs-generation/templates/mkdocs.yml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/release-notes-generation/SKILL.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/ruff-sync-usage/SKILL.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/ruff-sync-usage/references/configuration.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/skills/ruff-sync-usage/references/troubleshooting.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.agents/workflows/add-test-case.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.git-blame-ignore-revs +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.github/dependabot.yml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.github/workflows/ci.yaml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.github/workflows/complexity.yaml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.github/workflows/docs.yaml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.gitignore +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.pre-commit-config.yaml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/.pre-commit-hooks.yaml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/CONTRIBUTING.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/LICENSE.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/codecov.yml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/configs/data-science-engineering/ruff.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/configs/fastapi/ruff.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/configs/kitchen-sink/ruff.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/agent-skill.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/assets/favicon.png +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/assets/logo.png +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/assets/ruff_sync_banner.png +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/best-practices.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/configuration.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/contributing.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/examples/advanced-config.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/examples/basic-config.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/gen_ref_pages.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/index.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/installation.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/troubleshooting.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/url-resolution.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/docs/usage.md +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/mkdocs.yml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/scripts/check_dogfood.sh +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/scripts/gitclone_dogfood.sh +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/scripts/pull_dogfood.sh +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/skills-lock.json +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/src/ruff_sync/__init__.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/src/ruff_sync/__main__.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/src/ruff_sync/cli.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/src/ruff_sync/pre_commit.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tasks.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/__init__.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/conftest.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/multi_upstream_final.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/multi_upstream_initial.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/multi_upstream_up1.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/multi_upstream_up2.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_changes_final.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_changes_initial.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_changes_upstream.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_dotted_keys_final.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_dotted_keys_initial.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_dotted_keys_upstream.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_ruff_cfg_final.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_ruff_cfg_initial.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/no_ruff_cfg_upstream.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/readme_excludes_final.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/readme_excludes_initial.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/readme_excludes_upstream.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/standard_final.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/standard_initial.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/lifecycle_tomls/standard_upstream.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/ruff.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_basic.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_check.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_config_validation.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_constants.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_corner_cases.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_deprecation.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_e2e.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_git_fetch.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_pre_commit.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_project.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_scaffold.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_serialization.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_toml_operations.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_url_handling.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/test_whitespace.py +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/w_ruff_sync_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/wo_ruff_cfg/pyproject.toml +0 -0
- {ruff_sync-0.1.3.dev3 → ruff_sync-0.1.4.dev0}/tests/wo_ruff_sync_cfg/pyproject.toml +0 -0
|
@@ -51,7 +51,13 @@ def test_merge_scenarios(source, upstream, expected_keys):
|
|
|
51
51
|
### 3.3 No Autouse Fixtures
|
|
52
52
|
`autouse=True` fixtures are **never allowed**. They hide setup logic and can cause non-obvious side effects or dependencies between tests. All fixtures used by a test must be explicitly requested in the test function's arguments.
|
|
53
53
|
|
|
54
|
-
### 3.4
|
|
54
|
+
### 3.4 No unittest.mock
|
|
55
|
+
The use of `unittest.mock` or `MagicMock` is strictly forbidden because it encourages bad design and tests that lie to you. Any kind of patching is discouraged. Instead, follow these patterns:
|
|
56
|
+
1. **Dependency Injection (DI)**: Design code so that dependencies (like loggers or configurations) are passed in, rather than hard-coded. This allows for simple "Spies" or test-specific objects to be used without patching.
|
|
57
|
+
2. **Dedicated Libraries**: Use `respx` for HTTP and `pyfakefs` for filesystem operations to mock at the IO layer.
|
|
58
|
+
3. **Monkeypatch (as a last resort)**: If a dependency is truly external and not easily injectable (e.g., a global function in a library), use the built-in `monkeypatch` fixture. However, always check if the code can be redesigned for DI first.
|
|
59
|
+
|
|
60
|
+
### 3.5 Main Entry Point
|
|
55
61
|
Every test file **must** end with a main entry point block. This ensures each file is independently executable as a script (`python tests/test_foo.py`).
|
|
56
62
|
|
|
57
63
|
```python
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Formatter Pipeline Architecture
|
|
2
|
+
|
|
3
|
+
> **Purpose**: Reference document for agents and developers extending or maintaining
|
|
4
|
+
> `ruff_sync.formatters`. Covers the streaming vs. accumulating formatter taxonomy,
|
|
5
|
+
> the `finalize()` contract, how to add a new formatter, and the fingerprint strategy
|
|
6
|
+
> required by structured CI report formats (GitLab Code Quality, SARIF).
|
|
7
|
+
>
|
|
8
|
+
> **Source of truth**: `src/ruff_sync/formatters.py` and `src/ruff_sync/constants.py`.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
|
|
14
|
+
All human-readable and machine-readable output from the `check` and `pull`
|
|
15
|
+
commands flows through a single **`ResultFormatter` protocol**. This keeps
|
|
16
|
+
`core.py` and `cli.py` agnostic about the output medium — the same drift
|
|
17
|
+
detection logic emits text to a terminal, GitHub Actions workflow commands, or
|
|
18
|
+
a GitLab Code Quality JSON report.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
core.py / cli.py
|
|
22
|
+
│
|
|
23
|
+
│ fmt.error(message, file_path=..., drift_key="lint.select")
|
|
24
|
+
│ fmt.warning(...)
|
|
25
|
+
│ fmt.finalize()
|
|
26
|
+
▼
|
|
27
|
+
ResultFormatter (Protocol)
|
|
28
|
+
├── TextFormatter → stderr/stdout, human-readable
|
|
29
|
+
├── GithubFormatter → ::error file=...:: workflow commands
|
|
30
|
+
├── JsonFormatter → NDJSON, one record per line
|
|
31
|
+
└── GitlabFormatter → GitLab Code Quality JSON array (pending)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## The `ResultFormatter` Protocol
|
|
37
|
+
|
|
38
|
+
Defined in `src/ruff_sync/formatters.py`.
|
|
39
|
+
|
|
40
|
+
### Methods
|
|
41
|
+
|
|
42
|
+
| Method | When to call | Notes |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `note(message)` | Status output that always prints (e.g. "Checking…") | Streaming; never buffered |
|
|
45
|
+
| `info(message, logger)` | Informational progress | Streaming |
|
|
46
|
+
| `success(message)` | Positive outcome (e.g. "In sync ✓") | Streaming |
|
|
47
|
+
| `error(message, file_path, logger, check_name, drift_key)` | Config drift or failure | See §Semantic Fields |
|
|
48
|
+
| `warning(message, file_path, logger, check_name, drift_key)` | Non-fatal issue (e.g. stale pre-commit hook) | See §Semantic Fields |
|
|
49
|
+
| `debug(message, logger)` | Verbose internal state | Streaming |
|
|
50
|
+
| `diff(diff_text)` | Unified diff between upstream and local | Intentionally ignored by structured formatters |
|
|
51
|
+
| `finalize()` | **Always called in a `try…finally` by `core.py`** | No-op for streaming; writes report for accumulating |
|
|
52
|
+
|
|
53
|
+
### Semantic Fields on `error()` and `warning()`
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
def error(
|
|
57
|
+
self,
|
|
58
|
+
message: str,
|
|
59
|
+
file_path: pathlib.Path | None = None,
|
|
60
|
+
logger: logging.Logger | None = None,
|
|
61
|
+
check_name: str = "ruff-sync/config-drift", # machine-readable rule ID
|
|
62
|
+
drift_key: str | None = None, # e.g. "lint.select"
|
|
63
|
+
) -> None: ...
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
- **`check_name`** — machine-readable rule identifier used by structured
|
|
67
|
+
formatters (GitLab `check_name`, SARIF `ruleId`). Defaults to
|
|
68
|
+
`"ruff-sync/config-drift"`. Use a distinct name for different issue
|
|
69
|
+
classes; e.g. `"ruff-sync/precommit-stale"`.
|
|
70
|
+
- **`drift_key`** — the dotted TOML key that caused the drift (e.g.
|
|
71
|
+
`"lint.select"`). Used by accumulating formatters to build **stable,
|
|
72
|
+
per-key fingerprints** without re-parsing the message string. Pass
|
|
73
|
+
`None` when the drift cannot be attributed to a single key.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Formatter Taxonomy
|
|
78
|
+
|
|
79
|
+
### Streaming Formatters
|
|
80
|
+
|
|
81
|
+
Emit output immediately on each call. `finalize()` is a no-op.
|
|
82
|
+
|
|
83
|
+
| Class | Format | Use |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `TextFormatter` | Human text | Default terminal output |
|
|
86
|
+
| `GithubFormatter` | `::error file=…::` | GitHub Actions inline annotations |
|
|
87
|
+
| `JsonFormatter` | NDJSON | Machine consumption, piping, `jq` |
|
|
88
|
+
|
|
89
|
+
### Accumulating Formatters *(pending implementation)*
|
|
90
|
+
|
|
91
|
+
Collect issues internally and flush a single structured document in
|
|
92
|
+
`finalize()`.
|
|
93
|
+
|
|
94
|
+
| Class | Format | Use |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `GitlabFormatter` | GitLab Code Quality JSON array | `artifacts: reports: codequality:` |
|
|
97
|
+
| `SarifFormatter` *(future)* | SARIF v2.1.0 | GitHub Advanced Security, other SAST tooling |
|
|
98
|
+
|
|
99
|
+
Key difference: accumulating formatters **must** have `finalize()` called
|
|
100
|
+
to produce any output. The `try…finally` pattern in `cli.py` guarantees
|
|
101
|
+
this even when `UpstreamError` or another exception occurs.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Adding a New Formatter
|
|
106
|
+
|
|
107
|
+
1. **Add the format value** to `OutputFormat` in `src/ruff_sync/constants.py`:
|
|
108
|
+
```python
|
|
109
|
+
class OutputFormat(str, enum.Enum):
|
|
110
|
+
TEXT = "text"
|
|
111
|
+
JSON = "json"
|
|
112
|
+
GITHUB = "github"
|
|
113
|
+
GITLAB = "gitlab"
|
|
114
|
+
SARIF = "sarif" # new
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
2. **Implement the class** in `src/ruff_sync/formatters.py`. For a
|
|
118
|
+
streaming formatter, all methods emit immediately and `finalize` is a
|
|
119
|
+
no-op. For an accumulating formatter:
|
|
120
|
+
- Collect issues in `self._issues: list[...]` during `error()` / `warning()`.
|
|
121
|
+
- Write the document in `finalize()` (to `stdout` — the caller pipes to
|
|
122
|
+
a file).
|
|
123
|
+
|
|
124
|
+
3. **Add a `case`** in `get_formatter`:
|
|
125
|
+
```python
|
|
126
|
+
def get_formatter(output_format: OutputFormat) -> ResultFormatter:
|
|
127
|
+
match output_format:
|
|
128
|
+
case OutputFormat.SARIF:
|
|
129
|
+
return SarifFormatter()
|
|
130
|
+
...
|
|
131
|
+
```
|
|
132
|
+
The `match` statement is **exhaustive** — mypy will error if a new
|
|
133
|
+
`OutputFormat` value is not handled.
|
|
134
|
+
|
|
135
|
+
4. **Add tests** in `tests/test_formatters.py`. At minimum:
|
|
136
|
+
- `finalize()` on an empty formatter produces the correct "no issues"
|
|
137
|
+
document (e.g. `[]` for GitLab, a valid SARIF run with zero results).
|
|
138
|
+
- `error()` produces a record with the correct severity.
|
|
139
|
+
- Fingerprints are stable across two identical calls.
|
|
140
|
+
- `finalize()` on a streaming formatter emits nothing.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## `finalize()` Call Site
|
|
145
|
+
|
|
146
|
+
`finalize()` is always called in a `try…finally` block within `core.py`’s
|
|
147
|
+
`check()` and `pull()` coroutines:
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
fmt = get_formatter(args.output_format)
|
|
151
|
+
try:
|
|
152
|
+
...
|
|
153
|
+
finally:
|
|
154
|
+
fmt.finalize() # no-op for streaming; flushes JSON for accumulating
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`finalize()` is always called unconditionally — **do not** guard it with
|
|
158
|
+
`isinstance` or `hasattr` checks. Every formatter in the protocol provides
|
|
159
|
+
the method.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Fingerprint Strategy (Accumulating Formatters)
|
|
164
|
+
|
|
165
|
+
Stable fingerprints are required so CI platforms can track whether an issue
|
|
166
|
+
is newly introduced or already resolved between branches.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import hashlib
|
|
170
|
+
|
|
171
|
+
def _make_fingerprint(upstream_url: str, local_file: str, drift_key: str | None) -> str:
|
|
172
|
+
if drift_key:
|
|
173
|
+
raw = f"ruff-sync:drift:{upstream_url}:{local_file}:{drift_key}"
|
|
174
|
+
else:
|
|
175
|
+
raw = f"ruff-sync:drift:{upstream_url}:{local_file}"
|
|
176
|
+
return hashlib.md5(raw.encode()).hexdigest()
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Rules:**
|
|
180
|
+
- Must be deterministic — same inputs → same fingerprint every run.
|
|
181
|
+
- Must be unique per logical issue (different keys → different fingerprints).
|
|
182
|
+
- Must **not** include timestamps, UUIDs, or any runtime-variable data.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Severity Mapping
|
|
187
|
+
|
|
188
|
+
| ruff-sync scenario | GitLab severity | SARIF level |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| Config key value differs | `major` | `error` |
|
|
191
|
+
| Key missing from local | `major` | `error` |
|
|
192
|
+
| Extra local key absent upstream | `minor` | `warning` |
|
|
193
|
+
| Pre-commit hook version stale | `minor` | `warning` |
|
|
194
|
+
| Upstream unreachable | `blocker` | `error` |
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Exit Codes (Unchanged by Formatter Choice)
|
|
199
|
+
|
|
200
|
+
| Code | Meaning |
|
|
201
|
+
|---|---|
|
|
202
|
+
| 0 | In sync |
|
|
203
|
+
| 1 | Config drift detected |
|
|
204
|
+
| 2 | CLI usage error (argparse) |
|
|
205
|
+
| 3 | Pre-commit hook drift |
|
|
206
|
+
| 4 | Upstream unreachable |
|
|
207
|
+
|
|
208
|
+
CI jobs should use `when: always` (GitLab) or `if: always()` (GitHub) to
|
|
209
|
+
upload structured reports regardless of exit code.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## References
|
|
214
|
+
|
|
215
|
+
- [`src/ruff_sync/formatters.py`](../src/ruff_sync/formatters.py)
|
|
216
|
+
- [`src/ruff_sync/constants.py`](../src/ruff_sync/constants.py)
|
|
217
|
+
- [`tests/test_formatters.py`](../tests/test_formatters.py)
|
|
218
|
+
- [`.agents/gitlab-reports.md`](./gitlab-reports.md) — GitLab Code Quality implementation spec
|
|
219
|
+
- [`issue-102-context.md`](./issue-102-context.md) — full feature context for Issue #102
|