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.
Files changed (30) hide show
  1. harness_lint-0.1.0/PKG-INFO +53 -0
  2. harness_lint-0.1.0/README.md +40 -0
  3. harness_lint-0.1.0/pyproject.toml +45 -0
  4. harness_lint-0.1.0/setup.cfg +4 -0
  5. harness_lint-0.1.0/src/harness_lint/__init__.py +3 -0
  6. harness_lint-0.1.0/src/harness_lint/accumulator.py +142 -0
  7. harness_lint-0.1.0/src/harness_lint/attribution.py +66 -0
  8. harness_lint-0.1.0/src/harness_lint/checker.py +126 -0
  9. harness_lint-0.1.0/src/harness_lint/cli.py +130 -0
  10. harness_lint-0.1.0/src/harness_lint/degradation.py +94 -0
  11. harness_lint-0.1.0/src/harness_lint/pbh_adapter.py +66 -0
  12. harness_lint-0.1.0/src/harness_lint/reporter.py +252 -0
  13. harness_lint-0.1.0/src/harness_lint/rules/__init__.py +6 -0
  14. harness_lint-0.1.0/src/harness_lint/rules/base.py +98 -0
  15. harness_lint-0.1.0/src/harness_lint/rules/hl001_eval.py +63 -0
  16. harness_lint-0.1.0/src/harness_lint/rules/hl201_function_length.py +60 -0
  17. harness_lint-0.1.0/src/harness_lint.egg-info/PKG-INFO +53 -0
  18. harness_lint-0.1.0/src/harness_lint.egg-info/SOURCES.txt +28 -0
  19. harness_lint-0.1.0/src/harness_lint.egg-info/dependency_links.txt +1 -0
  20. harness_lint-0.1.0/src/harness_lint.egg-info/entry_points.txt +2 -0
  21. harness_lint-0.1.0/src/harness_lint.egg-info/requires.txt +6 -0
  22. harness_lint-0.1.0/src/harness_lint.egg-info/top_level.txt +1 -0
  23. harness_lint-0.1.0/tests/test_accumulator.py +336 -0
  24. harness_lint-0.1.0/tests/test_attribution.py +185 -0
  25. harness_lint-0.1.0/tests/test_bootstrap.py +36 -0
  26. harness_lint-0.1.0/tests/test_checker.py +542 -0
  27. harness_lint-0.1.0/tests/test_cli.py +139 -0
  28. harness_lint-0.1.0/tests/test_degradation.py +201 -0
  29. harness_lint-0.1.0/tests/test_pbh_adapter.py +150 -0
  30. 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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """harness_lint package."""
2
+
3
+ __version__ = "0.1.0"
@@ -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()