harness-lint 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- harness_lint-0.1.0/PKG-INFO +53 -0
- harness_lint-0.1.0/README.md +40 -0
- harness_lint-0.1.0/pyproject.toml +45 -0
- harness_lint-0.1.0/setup.cfg +4 -0
- harness_lint-0.1.0/src/harness_lint/__init__.py +3 -0
- harness_lint-0.1.0/src/harness_lint/accumulator.py +142 -0
- harness_lint-0.1.0/src/harness_lint/attribution.py +66 -0
- harness_lint-0.1.0/src/harness_lint/checker.py +126 -0
- harness_lint-0.1.0/src/harness_lint/cli.py +130 -0
- harness_lint-0.1.0/src/harness_lint/degradation.py +94 -0
- harness_lint-0.1.0/src/harness_lint/pbh_adapter.py +66 -0
- harness_lint-0.1.0/src/harness_lint/reporter.py +252 -0
- harness_lint-0.1.0/src/harness_lint/rules/__init__.py +6 -0
- harness_lint-0.1.0/src/harness_lint/rules/base.py +98 -0
- harness_lint-0.1.0/src/harness_lint/rules/hl001_eval.py +63 -0
- harness_lint-0.1.0/src/harness_lint/rules/hl201_function_length.py +60 -0
- harness_lint-0.1.0/src/harness_lint.egg-info/PKG-INFO +53 -0
- harness_lint-0.1.0/src/harness_lint.egg-info/SOURCES.txt +28 -0
- harness_lint-0.1.0/src/harness_lint.egg-info/dependency_links.txt +1 -0
- harness_lint-0.1.0/src/harness_lint.egg-info/entry_points.txt +2 -0
- harness_lint-0.1.0/src/harness_lint.egg-info/requires.txt +6 -0
- harness_lint-0.1.0/src/harness_lint.egg-info/top_level.txt +1 -0
- harness_lint-0.1.0/tests/test_accumulator.py +336 -0
- harness_lint-0.1.0/tests/test_attribution.py +185 -0
- harness_lint-0.1.0/tests/test_bootstrap.py +36 -0
- harness_lint-0.1.0/tests/test_checker.py +542 -0
- harness_lint-0.1.0/tests/test_cli.py +139 -0
- harness_lint-0.1.0/tests/test_degradation.py +201 -0
- harness_lint-0.1.0/tests/test_pbh_adapter.py +150 -0
- harness_lint-0.1.0/tests/test_reporter.py +384 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: harness-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: PBH生态的第一个果实工具 — AI生成代码缺陷静态检查器
|
|
5
|
+
Author-email: renjianguojinqianfan <18108097611@163.com>
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: typer>=0.9.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
11
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
12
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
13
|
+
|
|
14
|
+
# harness-lint
|
|
15
|
+
|
|
16
|
+
> PBH 生态的第一个果实工具 — 专门检测 AI 生成代码中典型坏习惯的静态检查器。
|
|
17
|
+
|
|
18
|
+
## 快速开始
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e ".[dev]"
|
|
22
|
+
make verify
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 开发命令
|
|
26
|
+
|
|
27
|
+
| 命令 | 说明 |
|
|
28
|
+
|------|------|
|
|
29
|
+
| `make verify` | lint + 测试 + 覆盖率 |
|
|
30
|
+
| `make test` | 运行测试 |
|
|
31
|
+
| `make lint` | 代码风格检查 |
|
|
32
|
+
| `make fix` | 自动修复风格问题 |
|
|
33
|
+
|
|
34
|
+
## 项目结构
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
harness-lint/
|
|
38
|
+
├── src/harness_lint/ # 主代码
|
|
39
|
+
├── tests/ # 测试
|
|
40
|
+
├── tasks/ # 任务拆解
|
|
41
|
+
├── docs/ # 文档
|
|
42
|
+
├── AGENTS.md # AI 协作协议
|
|
43
|
+
├── Makefile
|
|
44
|
+
└── pyproject.toml
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## AI 协作
|
|
48
|
+
|
|
49
|
+
本项目遵循 PBH 协议。AI 助手请阅读 `AGENTS.md` 了解项目规则和工作准则。
|
|
50
|
+
|
|
51
|
+
## 许可证
|
|
52
|
+
|
|
53
|
+
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# harness-lint
|
|
2
|
+
|
|
3
|
+
> PBH 生态的第一个果实工具 — 专门检测 AI 生成代码中典型坏习惯的静态检查器。
|
|
4
|
+
|
|
5
|
+
## 快速开始
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
make verify
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## 开发命令
|
|
13
|
+
|
|
14
|
+
| 命令 | 说明 |
|
|
15
|
+
|------|------|
|
|
16
|
+
| `make verify` | lint + 测试 + 覆盖率 |
|
|
17
|
+
| `make test` | 运行测试 |
|
|
18
|
+
| `make lint` | 代码风格检查 |
|
|
19
|
+
| `make fix` | 自动修复风格问题 |
|
|
20
|
+
|
|
21
|
+
## 项目结构
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
harness-lint/
|
|
25
|
+
├── src/harness_lint/ # 主代码
|
|
26
|
+
├── tests/ # 测试
|
|
27
|
+
├── tasks/ # 任务拆解
|
|
28
|
+
├── docs/ # 文档
|
|
29
|
+
├── AGENTS.md # AI 协作协议
|
|
30
|
+
├── Makefile
|
|
31
|
+
└── pyproject.toml
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## AI 协作
|
|
35
|
+
|
|
36
|
+
本项目遵循 PBH 协议。AI 助手请阅读 `AGENTS.md` 了解项目规则和工作准则。
|
|
37
|
+
|
|
38
|
+
## 许可证
|
|
39
|
+
|
|
40
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "harness-lint"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "PBH生态的第一个果实工具 — AI生成代码缺陷静态检查器"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "renjianguojinqianfan", email = "18108097611@163.com"},
|
|
13
|
+
]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"typer>=0.9.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest>=7.0",
|
|
21
|
+
"pytest-cov>=4.0",
|
|
22
|
+
"ruff>=0.1.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
"harness-lint" = "harness_lint.cli:cli"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.packages.find]
|
|
29
|
+
where = ["src"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
33
|
+
pythonpath = ["src"]
|
|
34
|
+
addopts = "-v --cov=src --cov-fail-under=85"
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
line-length = 100
|
|
38
|
+
target-version = "py311"
|
|
39
|
+
exclude = [".harness/", "configs/", "tests/fixtures/"]
|
|
40
|
+
|
|
41
|
+
[tool.ruff.lint]
|
|
42
|
+
select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "SIM"]
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint.pydocstyle]
|
|
45
|
+
convention = "google"
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Deviation cost accumulation mechanism for Harness-Lint.
|
|
2
|
+
|
|
3
|
+
Provides pattern-based cost escalation: repeated violations of the same
|
|
4
|
+
rule trigger elevated warnings and, in the evaluate phase, severity
|
|
5
|
+
upgrades to Error.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections import defaultdict
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from harness_lint.rules.base import Rule, Violation
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class PatternWarning:
|
|
18
|
+
"""A pattern-level warning for repeated rule violations.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
rule_id: The identifier of the rule that was repeatedly violated.
|
|
22
|
+
count: Total number of occurrences for this rule.
|
|
23
|
+
suggestion: Human-readable guidance referencing AGENTS.md.
|
|
24
|
+
description: Human-readable rule name (from Rule.name).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
rule_id: str
|
|
28
|
+
count: int
|
|
29
|
+
suggestion: str
|
|
30
|
+
description: str = ""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _count_violations(violations: list[Violation]) -> dict[str, int]:
|
|
34
|
+
"""Count violations per rule_id."""
|
|
35
|
+
counts: dict[str, int] = defaultdict(int)
|
|
36
|
+
for v in violations:
|
|
37
|
+
counts[v.rule_id] += 1
|
|
38
|
+
return counts
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _collect_first_refs(
|
|
42
|
+
violations: list[Violation],
|
|
43
|
+
elevated_rule_ids: set[str],
|
|
44
|
+
) -> dict[str, str]:
|
|
45
|
+
"""Collect first agente_ref per elevated rule."""
|
|
46
|
+
first_refs: dict[str, str] = {}
|
|
47
|
+
for v in violations:
|
|
48
|
+
if v.rule_id in elevated_rule_ids and v.rule_id not in first_refs:
|
|
49
|
+
first_refs[v.rule_id] = v.agente_ref
|
|
50
|
+
return first_refs
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _build_pattern_warnings(
|
|
54
|
+
counts: dict[str, int],
|
|
55
|
+
elevated_rule_ids: set[str],
|
|
56
|
+
first_refs: dict[str, str],
|
|
57
|
+
rule_map: dict[str, Rule],
|
|
58
|
+
) -> list[PatternWarning]:
|
|
59
|
+
"""Build pattern warnings for elevated rules."""
|
|
60
|
+
return [
|
|
61
|
+
PatternWarning(
|
|
62
|
+
rule_id=rid,
|
|
63
|
+
count=counts[rid],
|
|
64
|
+
suggestion=f"建议在 {first_refs[rid]} 中明确相关规范",
|
|
65
|
+
description=rule_map[rid].name if rid in rule_map else rid,
|
|
66
|
+
)
|
|
67
|
+
for rid in sorted(elevated_rule_ids)
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _process_violations(
|
|
72
|
+
violations: list[Violation],
|
|
73
|
+
elevated_rule_ids: set[str],
|
|
74
|
+
counts: dict[str, int],
|
|
75
|
+
phase: str | None,
|
|
76
|
+
) -> list[Violation]:
|
|
77
|
+
"""Update severity/phenomenon for elevated rules."""
|
|
78
|
+
processed: list[Violation] = []
|
|
79
|
+
for v in violations:
|
|
80
|
+
if v.rule_id in elevated_rule_ids:
|
|
81
|
+
new_severity = "Error" if phase == "evaluate" else v.severity
|
|
82
|
+
new_phenomenon = f"该冲突已持续出现 {counts[v.rule_id]} 次,尚未解决"
|
|
83
|
+
processed.append(
|
|
84
|
+
Violation(
|
|
85
|
+
file=v.file,
|
|
86
|
+
line=v.line,
|
|
87
|
+
column=v.column,
|
|
88
|
+
rule_id=v.rule_id,
|
|
89
|
+
severity=new_severity,
|
|
90
|
+
phenomenon=new_phenomenon,
|
|
91
|
+
attribution=v.attribution,
|
|
92
|
+
agente_ref=v.agente_ref,
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
else:
|
|
96
|
+
processed.append(v)
|
|
97
|
+
return processed
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def apply_accumulation(
|
|
101
|
+
violations: list[Violation],
|
|
102
|
+
phase: str | None,
|
|
103
|
+
rules: list[Rule] | None = None,
|
|
104
|
+
threshold: int = 3,
|
|
105
|
+
) -> tuple[list[Violation], list[PatternWarning]]:
|
|
106
|
+
"""Apply cost accumulation to violations.
|
|
107
|
+
|
|
108
|
+
Counts violations per *rule_id*. For rules whose count meets or
|
|
109
|
+
exceeds *threshold*:
|
|
110
|
+
|
|
111
|
+
- *phase* == ``"evaluate"``: upgrade severity to ``"Error"``
|
|
112
|
+
- Otherwise: keep severity but update phenomenon
|
|
113
|
+
- In both cases: generate a :class:`PatternWarning`
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
violations: Raw violations collected by the checker.
|
|
117
|
+
phase: Current PBH phase (``"execute"``, ``"evaluate"``, or None).
|
|
118
|
+
rules: Optional list of rules to look up rule names for descriptions.
|
|
119
|
+
threshold: Minimum count before escalation (default 3).
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Tuple of (processed_violations, pattern_warnings).
|
|
123
|
+
"""
|
|
124
|
+
if not violations:
|
|
125
|
+
return [], []
|
|
126
|
+
|
|
127
|
+
rule_map: dict[str, Rule] = {}
|
|
128
|
+
if rules:
|
|
129
|
+
for r in rules:
|
|
130
|
+
rule_map[r.rule_id] = r
|
|
131
|
+
|
|
132
|
+
counts = _count_violations(violations)
|
|
133
|
+
elevated_rule_ids = {rid for rid, cnt in counts.items() if cnt >= threshold}
|
|
134
|
+
first_refs = _collect_first_refs(violations, elevated_rule_ids)
|
|
135
|
+
pattern_warnings = _build_pattern_warnings(
|
|
136
|
+
counts, elevated_rule_ids, first_refs, rule_map
|
|
137
|
+
)
|
|
138
|
+
processed = _process_violations(
|
|
139
|
+
violations, elevated_rule_ids, counts, phase
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return processed, pattern_warnings
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Attribution validation for Harness-Lint rules and violations.
|
|
2
|
+
|
|
3
|
+
Provides utilities to verify that every rule and every violation carries
|
|
4
|
+
a complete attribution chain pointing back to AGENTS.md.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from harness_lint.rules.base import Rule, Violation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validate_rule_attribution(rule: Rule) -> list[str]:
|
|
13
|
+
"""Validate a rule's attribution chain.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
rule: The rule to validate.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
A list of error messages. An empty list means the rule passes
|
|
20
|
+
validation.
|
|
21
|
+
"""
|
|
22
|
+
errors: list[str] = []
|
|
23
|
+
if not rule.agente_ref:
|
|
24
|
+
errors.append(f"{rule.rule_id}: agente_ref is empty")
|
|
25
|
+
if not rule.attribution:
|
|
26
|
+
errors.append(f"{rule.rule_id}: attribution is empty")
|
|
27
|
+
if rule.agente_ref and not rule.agente_ref.startswith("AGENTS.md"):
|
|
28
|
+
errors.append(f"{rule.rule_id}: agente_ref must start with 'AGENTS.md'")
|
|
29
|
+
return errors
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def validate_violation(violation: Violation) -> list[str]:
|
|
33
|
+
"""Validate a violation's full attribution chain.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
violation: The violation to validate.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
A list of error messages. An empty list means the violation passes
|
|
40
|
+
validation.
|
|
41
|
+
"""
|
|
42
|
+
errors: list[str] = []
|
|
43
|
+
if not violation.phenomenon:
|
|
44
|
+
errors.append("phenomenon is empty")
|
|
45
|
+
if not violation.attribution:
|
|
46
|
+
errors.append("attribution is empty")
|
|
47
|
+
if not violation.agente_ref:
|
|
48
|
+
errors.append("agente_ref is empty")
|
|
49
|
+
if violation.agente_ref and not violation.agente_ref.startswith("AGENTS.md"):
|
|
50
|
+
errors.append("agente_ref must start with 'AGENTS.md'")
|
|
51
|
+
return errors
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def validate_ruleset(rules: list[Rule]) -> list[str]:
|
|
55
|
+
"""Validate all rules in a ruleset.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
rules: List of rules to validate.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Aggregated error messages from all rules.
|
|
62
|
+
"""
|
|
63
|
+
all_errors: list[str] = []
|
|
64
|
+
for rule in rules:
|
|
65
|
+
all_errors.extend(validate_rule_attribution(rule))
|
|
66
|
+
return all_errors
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Checker engine: file traversal, AST parsing, and rule execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from harness_lint.accumulator import apply_accumulation
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from harness_lint.accumulator import PatternWarning
|
|
14
|
+
from harness_lint.rules.base import Rule, Violation
|
|
15
|
+
|
|
16
|
+
_DEFAULT_IGNORE_DIRS = {
|
|
17
|
+
".git",
|
|
18
|
+
"__pycache__",
|
|
19
|
+
".venv",
|
|
20
|
+
".mypy_cache",
|
|
21
|
+
".pytest_cache",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Context:
|
|
26
|
+
"""Simplified context object for phase-aware rule activation."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
phase: str | None = None,
|
|
31
|
+
harness_version: str | None = None,
|
|
32
|
+
hint: str = "",
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Initialize context with optional phase.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
phase: The current PBH phase ("plan", "execute", "evaluate", "done", or None).
|
|
38
|
+
harness_version: The harness version from progress.json, if present.
|
|
39
|
+
hint: Human-readable hint based on the current phase.
|
|
40
|
+
"""
|
|
41
|
+
self.phase = phase
|
|
42
|
+
self.harness_version = harness_version
|
|
43
|
+
self.hint = hint
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def collect_files(path: str) -> list[str]:
|
|
47
|
+
"""Recursively collect Python files, applying default ignore patterns.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path: Root directory to scan.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Sorted list of absolute file paths for all ``.py`` files found
|
|
54
|
+
under *path*, excluding files inside default ignored directories.
|
|
55
|
+
"""
|
|
56
|
+
result: list[str] = []
|
|
57
|
+
root = Path(path)
|
|
58
|
+
|
|
59
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
60
|
+
dirnames[:] = [
|
|
61
|
+
d for d in dirnames if d not in _DEFAULT_IGNORE_DIRS
|
|
62
|
+
]
|
|
63
|
+
for filename in filenames:
|
|
64
|
+
if filename.endswith(".py"):
|
|
65
|
+
result.append(str(Path(dirpath) / filename))
|
|
66
|
+
|
|
67
|
+
return sorted(result)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _is_rule_active(rule: Rule, phase: str | None) -> bool:
|
|
71
|
+
"""Determine whether a rule is active for the given phase.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
rule: The rule to evaluate.
|
|
75
|
+
phase: Current phase ("execute", "evaluate", or None).
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
True if the rule should be executed for the given phase.
|
|
79
|
+
"""
|
|
80
|
+
if phase is None:
|
|
81
|
+
return True
|
|
82
|
+
return phase in rule.phases
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def check(
|
|
86
|
+
path: str, context: Context, rules: list[Rule]
|
|
87
|
+
) -> tuple[list[Violation], list[PatternWarning]]:
|
|
88
|
+
"""Check all Python files in *path* against activated rules.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
path: Root directory to scan.
|
|
92
|
+
context: Phase-aware context controlling rule activation.
|
|
93
|
+
rules: List of rules to (potentially) apply.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Tuple of (sorted violations, pattern warnings). Violations are
|
|
97
|
+
sorted by file path. Pattern warnings are generated for rules
|
|
98
|
+
whose violation count meets or exceeds the escalation threshold.
|
|
99
|
+
Invalid Python syntax is silently skipped rather than raising.
|
|
100
|
+
"""
|
|
101
|
+
files = collect_files(path)
|
|
102
|
+
active_rules = [r for r in rules if _is_rule_active(r, context.phase)]
|
|
103
|
+
all_violations: list[Violation] = []
|
|
104
|
+
|
|
105
|
+
for file_path in files:
|
|
106
|
+
try:
|
|
107
|
+
with open(file_path, encoding="utf-8") as f:
|
|
108
|
+
content = f.read()
|
|
109
|
+
except OSError:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
tree = ast.parse(content)
|
|
114
|
+
except SyntaxError:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
for rule in active_rules:
|
|
118
|
+
violations = rule.check(file_path, content, tree)
|
|
119
|
+
if violations:
|
|
120
|
+
all_violations.extend(violations)
|
|
121
|
+
|
|
122
|
+
sorted_violations = sorted(all_violations, key=lambda v: v.file)
|
|
123
|
+
processed_violations, pattern_warnings = apply_accumulation(
|
|
124
|
+
sorted_violations, phase=context.phase, rules=active_rules
|
|
125
|
+
)
|
|
126
|
+
return processed_violations, pattern_warnings
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""CLI entry point for Harness-Lint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
|
|
7
|
+
from harness_lint.accumulator import PatternWarning
|
|
8
|
+
from harness_lint.checker import check, collect_files
|
|
9
|
+
from harness_lint.degradation import format_degradation_notice, is_harness_lint_enabled
|
|
10
|
+
from harness_lint.pbh_adapter import get_context
|
|
11
|
+
from harness_lint.reporter import format_json, format_summary, format_terminal
|
|
12
|
+
from harness_lint.rules import HL001EvalRule, HL201FunctionLengthRule
|
|
13
|
+
from harness_lint.rules.base import Rule, Violation
|
|
14
|
+
|
|
15
|
+
app = typer.Typer()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _get_default_rules() -> list[Rule]:
|
|
19
|
+
"""Return the built-in rule instances."""
|
|
20
|
+
return [HL001EvalRule(), HL201FunctionLengthRule()]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _has_errors(violations: list[Violation]) -> bool:
|
|
24
|
+
"""Check if any violation has Error severity."""
|
|
25
|
+
return any(v.severity == "Error" for v in violations)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _has_warnings(violations: list[Violation]) -> bool:
|
|
29
|
+
"""Check if any violation has Warning severity."""
|
|
30
|
+
return any(v.severity == "Warning" for v in violations)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def version_callback(value: bool) -> None:
|
|
34
|
+
"""Print version and exit."""
|
|
35
|
+
if value:
|
|
36
|
+
typer.echo("harness-lint 0.1.0")
|
|
37
|
+
raise typer.Exit()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.callback()
|
|
41
|
+
def main(
|
|
42
|
+
version: bool = typer.Option(
|
|
43
|
+
False,
|
|
44
|
+
"--version",
|
|
45
|
+
"-v",
|
|
46
|
+
help="Show version and exit.",
|
|
47
|
+
is_eager=True,
|
|
48
|
+
callback=version_callback,
|
|
49
|
+
),
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Harness-Lint CLI."""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _handle_degradation() -> None:
|
|
55
|
+
"""Check if harness-lint is enabled and exit early if not."""
|
|
56
|
+
if is_harness_lint_enabled():
|
|
57
|
+
return
|
|
58
|
+
notice = format_degradation_notice()
|
|
59
|
+
if notice:
|
|
60
|
+
typer.echo(notice)
|
|
61
|
+
else:
|
|
62
|
+
typer.echo("当前未启用 Harness-Lint")
|
|
63
|
+
raise typer.Exit(code=0)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _format_output(
|
|
67
|
+
violations: list[Violation],
|
|
68
|
+
files_checked: int,
|
|
69
|
+
pattern_warnings: list[PatternWarning],
|
|
70
|
+
fmt: str,
|
|
71
|
+
phase: str | None,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Format violations according to the requested output format."""
|
|
74
|
+
if fmt == "json":
|
|
75
|
+
return format_json(
|
|
76
|
+
violations, files_checked, pattern_warnings, phase=phase
|
|
77
|
+
)
|
|
78
|
+
if fmt == "summary":
|
|
79
|
+
return format_summary(
|
|
80
|
+
violations, files_checked, pattern_warnings, phase=phase
|
|
81
|
+
)
|
|
82
|
+
return format_terminal(
|
|
83
|
+
violations, files_checked, pattern_warnings, phase=phase
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _determine_exit_code(violations: list[Violation], strict: bool) -> int:
|
|
88
|
+
"""Return the exit code based on violations and strict mode."""
|
|
89
|
+
if _has_errors(violations):
|
|
90
|
+
return 1
|
|
91
|
+
if strict and _has_warnings(violations):
|
|
92
|
+
return 1
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.command()
|
|
97
|
+
def run(
|
|
98
|
+
path: str = typer.Argument(".", help="Target directory to lint."),
|
|
99
|
+
fmt: str = typer.Option(
|
|
100
|
+
"terminal",
|
|
101
|
+
"--format",
|
|
102
|
+
help="Output format: terminal, json, summary.",
|
|
103
|
+
),
|
|
104
|
+
strict: bool = typer.Option(
|
|
105
|
+
False,
|
|
106
|
+
"--strict",
|
|
107
|
+
help="Treat warnings as errors for exit code.",
|
|
108
|
+
),
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Run Harness-Lint against the given path."""
|
|
111
|
+
_handle_degradation()
|
|
112
|
+
|
|
113
|
+
context = get_context(path)
|
|
114
|
+
rules = _get_default_rules()
|
|
115
|
+
files = collect_files(path)
|
|
116
|
+
files_checked = len(files)
|
|
117
|
+
|
|
118
|
+
violations, pattern_warnings = check(path, context, rules)
|
|
119
|
+
|
|
120
|
+
output = _format_output(
|
|
121
|
+
violations, files_checked, pattern_warnings, fmt, context.phase
|
|
122
|
+
)
|
|
123
|
+
typer.echo(output)
|
|
124
|
+
|
|
125
|
+
raise typer.Exit(code=_determine_exit_code(violations, strict))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def cli() -> None:
|
|
129
|
+
"""Entry point for setuptools console script."""
|
|
130
|
+
app()
|