litclaude-ai 0.2.2

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 (156) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/LICENSE +21 -0
  3. package/README.md +369 -0
  4. package/README_ko-KR.md +374 -0
  5. package/RELEASE_CHECKLIST.md +165 -0
  6. package/bin/litclaude-ai.js +643 -0
  7. package/cover.png +0 -0
  8. package/docs/agents.md +67 -0
  9. package/docs/hooks.md +134 -0
  10. package/docs/lsp.md +40 -0
  11. package/docs/migration.md +209 -0
  12. package/docs/workflow-compatibility-audit.md +119 -0
  13. package/generate_cover.py +123 -0
  14. package/package.json +48 -0
  15. package/plugins/litclaude/.claude-plugin/plugin.json +25 -0
  16. package/plugins/litclaude/.lsp.json +13 -0
  17. package/plugins/litclaude/.mcp.json +9 -0
  18. package/plugins/litclaude/agents/boulder-executor.md +12 -0
  19. package/plugins/litclaude/agents/librarian-researcher.md +15 -0
  20. package/plugins/litclaude/agents/oracle-verifier.md +16 -0
  21. package/plugins/litclaude/agents/prometheus-planner.md +13 -0
  22. package/plugins/litclaude/agents/qa-runner.md +16 -0
  23. package/plugins/litclaude/agents/quality-reviewer.md +17 -0
  24. package/plugins/litclaude/bin/litclaude-hook.js +110 -0
  25. package/plugins/litclaude/bin/litclaude-hud.js +271 -0
  26. package/plugins/litclaude/bin/litclaude-lsp-doctor.js +15 -0
  27. package/plugins/litclaude/bin/litclaude-mcp.js +70 -0
  28. package/plugins/litclaude/commands/deep-interview.md +21 -0
  29. package/plugins/litclaude/commands/dynamic-workflow.md +36 -0
  30. package/plugins/litclaude/commands/lit-loop.md +40 -0
  31. package/plugins/litclaude/commands/lit-plan.md +35 -0
  32. package/plugins/litclaude/commands/litgoal.md +30 -0
  33. package/plugins/litclaude/commands/review-work.md +35 -0
  34. package/plugins/litclaude/commands/start-work.md +36 -0
  35. package/plugins/litclaude/hooks/hooks.json +54 -0
  36. package/plugins/litclaude/lib/context-pressure.mjs +25 -0
  37. package/plugins/litclaude/lib/hud-accent-palette.mjs +58 -0
  38. package/plugins/litclaude/lib/litgoal/cli.mjs +266 -0
  39. package/plugins/litclaude/lib/litgoal/ledger.mjs +16 -0
  40. package/plugins/litclaude/lib/litgoal/paths.mjs +7 -0
  41. package/plugins/litclaude/lib/litgoal/state.mjs +67 -0
  42. package/plugins/litclaude/lib/mutated-file-paths.mjs +63 -0
  43. package/plugins/litclaude/lib/start-work-continuation.mjs +99 -0
  44. package/plugins/litclaude/lib/workflow-check.mjs +83 -0
  45. package/plugins/litclaude/skills/ai-slop-remover/SKILL.md +142 -0
  46. package/plugins/litclaude/skills/comment-checker/SKILL.md +55 -0
  47. package/plugins/litclaude/skills/debugging/SKILL.md +70 -0
  48. package/plugins/litclaude/skills/debugging/references/methodology/00-setup.md +108 -0
  49. package/plugins/litclaude/skills/debugging/references/methodology/02-investigate.md +126 -0
  50. package/plugins/litclaude/skills/debugging/references/methodology/04-oracle-triple.md +106 -0
  51. package/plugins/litclaude/skills/debugging/references/methodology/05-escalate.md +69 -0
  52. package/plugins/litclaude/skills/debugging/references/methodology/06-fix.md +116 -0
  53. package/plugins/litclaude/skills/debugging/references/methodology/08-qa.md +94 -0
  54. package/plugins/litclaude/skills/debugging/references/methodology/09-cleanup.md +164 -0
  55. package/plugins/litclaude/skills/debugging/references/methodology/partial-runtime-evidence.md +228 -0
  56. package/plugins/litclaude/skills/debugging/references/runtimes/bundled-js-binary.md +415 -0
  57. package/plugins/litclaude/skills/debugging/references/runtimes/go.md +252 -0
  58. package/plugins/litclaude/skills/debugging/references/runtimes/native-binary.md +484 -0
  59. package/plugins/litclaude/skills/debugging/references/runtimes/node.md +260 -0
  60. package/plugins/litclaude/skills/debugging/references/runtimes/python.md +248 -0
  61. package/plugins/litclaude/skills/debugging/references/runtimes/rust.md +234 -0
  62. package/plugins/litclaude/skills/debugging/references/tools/ghidra.md +212 -0
  63. package/plugins/litclaude/skills/debugging/references/tools/playwright-cli.md +194 -0
  64. package/plugins/litclaude/skills/debugging/references/tools/pwndbg.md +263 -0
  65. package/plugins/litclaude/skills/debugging/references/tools/pwntools.md +265 -0
  66. package/plugins/litclaude/skills/deep-interview/SKILL.md +323 -0
  67. package/plugins/litclaude/skills/deep-interview/scripts/render_progress.py +193 -0
  68. package/plugins/litclaude/skills/frontend-ui-ux/SKILL.md +62 -0
  69. package/plugins/litclaude/skills/lit-loop/SKILL.md +144 -0
  70. package/plugins/litclaude/skills/lit-plan/SKILL.md +125 -0
  71. package/plugins/litclaude/skills/litgoal/SKILL.md +219 -0
  72. package/plugins/litclaude/skills/lsp/SKILL.md +63 -0
  73. package/plugins/litclaude/skills/programming/SKILL.md +106 -0
  74. package/plugins/litclaude/skills/programming/references/go/README.md +90 -0
  75. package/plugins/litclaude/skills/programming/references/go/backend-stack.md +641 -0
  76. package/plugins/litclaude/skills/programming/references/go/bootstrap.md +328 -0
  77. package/plugins/litclaude/skills/programming/references/go/bubbletea-v2.md +360 -0
  78. package/plugins/litclaude/skills/programming/references/go/cobra-stack.md +468 -0
  79. package/plugins/litclaude/skills/programming/references/go/concurrency.md +362 -0
  80. package/plugins/litclaude/skills/programming/references/go/data-modeling.md +329 -0
  81. package/plugins/litclaude/skills/programming/references/go/error-handling.md +359 -0
  82. package/plugins/litclaude/skills/programming/references/go/golangci-strict.md +236 -0
  83. package/plugins/litclaude/skills/programming/references/go/grpc-connect.md +375 -0
  84. package/plugins/litclaude/skills/programming/references/go/libraries.md +337 -0
  85. package/plugins/litclaude/skills/programming/references/go/one-liners.md +202 -0
  86. package/plugins/litclaude/skills/programming/references/go/sqlc-pgx.md +471 -0
  87. package/plugins/litclaude/skills/programming/references/go/testing.md +467 -0
  88. package/plugins/litclaude/skills/programming/references/go/type-patterns.md +298 -0
  89. package/plugins/litclaude/skills/programming/references/python/README.md +314 -0
  90. package/plugins/litclaude/skills/programming/references/python/async-anyio.md +442 -0
  91. package/plugins/litclaude/skills/programming/references/python/data-modeling.md +233 -0
  92. package/plugins/litclaude/skills/programming/references/python/data-processing.md +133 -0
  93. package/plugins/litclaude/skills/programming/references/python/error-handling.md +218 -0
  94. package/plugins/litclaude/skills/programming/references/python/fastapi-stack.md +316 -0
  95. package/plugins/litclaude/skills/programming/references/python/httpx2-optimization.md +360 -0
  96. package/plugins/litclaude/skills/programming/references/python/libraries.md +307 -0
  97. package/plugins/litclaude/skills/programming/references/python/one-liners.md +268 -0
  98. package/plugins/litclaude/skills/programming/references/python/orjson-stack.md +378 -0
  99. package/plugins/litclaude/skills/programming/references/python/pydantic-ai.md +285 -0
  100. package/plugins/litclaude/skills/programming/references/python/pyproject-strict.md +232 -0
  101. package/plugins/litclaude/skills/programming/references/python/textual-tui.md +201 -0
  102. package/plugins/litclaude/skills/programming/references/python/type-patterns.md +176 -0
  103. package/plugins/litclaude/skills/programming/references/rust/README.md +317 -0
  104. package/plugins/litclaude/skills/programming/references/rust/async-tokio.md +299 -0
  105. package/plugins/litclaude/skills/programming/references/rust/axum-stack.md +467 -0
  106. package/plugins/litclaude/skills/programming/references/rust/cargo-strict.md +317 -0
  107. package/plugins/litclaude/skills/programming/references/rust/clap-stack.md +409 -0
  108. package/plugins/litclaude/skills/programming/references/rust/concurrency.md +375 -0
  109. package/plugins/litclaude/skills/programming/references/rust/libraries.md +439 -0
  110. package/plugins/litclaude/skills/programming/references/rust/one-liners.md +291 -0
  111. package/plugins/litclaude/skills/programming/references/rust/proptest-insta.md +429 -0
  112. package/plugins/litclaude/skills/programming/references/rust/type-state.md +354 -0
  113. package/plugins/litclaude/skills/programming/references/rust/unsafe-discipline.md +250 -0
  114. package/plugins/litclaude/skills/programming/references/rust/zero-cost-safety.md +527 -0
  115. package/plugins/litclaude/skills/programming/references/rust-ub/README.md +289 -0
  116. package/plugins/litclaude/skills/programming/references/rust-ub/miri-sanitizers-loom.md +411 -0
  117. package/plugins/litclaude/skills/programming/references/rust-ub/ub-taxonomy.md +269 -0
  118. package/plugins/litclaude/skills/programming/references/typescript/README.md +195 -0
  119. package/plugins/litclaude/skills/programming/references/typescript/backend-hono.md +672 -0
  120. package/plugins/litclaude/skills/programming/references/typescript/bootstrap.md +199 -0
  121. package/plugins/litclaude/skills/programming/references/typescript/data-modeling.md +202 -0
  122. package/plugins/litclaude/skills/programming/references/typescript/error-handling.md +169 -0
  123. package/plugins/litclaude/skills/programming/references/typescript/tsconfig-strict.md +152 -0
  124. package/plugins/litclaude/skills/programming/references/typescript/type-patterns.md +196 -0
  125. package/plugins/litclaude/skills/programming/scripts/go/check-no-excuse-rules.sh +173 -0
  126. package/plugins/litclaude/skills/programming/scripts/go/new-project.py +138 -0
  127. package/plugins/litclaude/skills/programming/scripts/go/templates/.editorconfig +13 -0
  128. package/plugins/litclaude/skills/programming/scripts/go/templates/.golangci.yml +95 -0
  129. package/plugins/litclaude/skills/programming/scripts/go/templates/AGENTS.md.tmpl +24 -0
  130. package/plugins/litclaude/skills/programming/scripts/go/templates/README.md.tmpl +12 -0
  131. package/plugins/litclaude/skills/programming/scripts/go/templates/Taskfile.yml +40 -0
  132. package/plugins/litclaude/skills/programming/scripts/go/templates/ci.yml +37 -0
  133. package/plugins/litclaude/skills/programming/scripts/go/templates/config.go +24 -0
  134. package/plugins/litclaude/skills/programming/scripts/go/templates/gitignore +15 -0
  135. package/plugins/litclaude/skills/programming/scripts/go/templates/main.go.tmpl +22 -0
  136. package/plugins/litclaude/skills/programming/scripts/go/templates/run.go +15 -0
  137. package/plugins/litclaude/skills/programming/scripts/python/check-no-excuse-rules.py +687 -0
  138. package/plugins/litclaude/skills/programming/scripts/python/new-project.py +172 -0
  139. package/plugins/litclaude/skills/programming/scripts/python/new-script.py +116 -0
  140. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.py +296 -0
  141. package/plugins/litclaude/skills/programming/scripts/rust/check-no-excuse-rules.sh +158 -0
  142. package/plugins/litclaude/skills/programming/scripts/rust/new-project.py +175 -0
  143. package/plugins/litclaude/skills/programming/scripts/typescript/check-no-excuse-rules.ts +282 -0
  144. package/plugins/litclaude/skills/programming/scripts/typescript/new-project.ts +177 -0
  145. package/plugins/litclaude/skills/refactor/SKILL.md +73 -0
  146. package/plugins/litclaude/skills/remove-ai-slops/SKILL.md +52 -0
  147. package/plugins/litclaude/skills/review-work/SKILL.md +331 -0
  148. package/plugins/litclaude/skills/rules/SKILL.md +66 -0
  149. package/plugins/litclaude/skills/start-work/SKILL.md +132 -0
  150. package/scripts/audit-plan-checkboxes.mjs +37 -0
  151. package/scripts/doctor.mjs +41 -0
  152. package/scripts/inspect-agent-tools.mjs +27 -0
  153. package/scripts/postinstall.mjs +50 -0
  154. package/scripts/qa-claude-plugin-smoke.sh +60 -0
  155. package/scripts/qa-portable-install.sh +136 -0
  156. package/scripts/validate-plugin.mjs +72 -0
@@ -0,0 +1,172 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = [
5
+ # "typer",
6
+ # "rich",
7
+ # ]
8
+ # ///
9
+
10
+ # ─── How to run ───
11
+ # 1. Install uv (if not installed):
12
+ # curl -LsSf https://astral.sh/uv/install.sh | sh
13
+ # 2. Run:
14
+ # uv run new-project.py myproject
15
+ # uv run new-project.py myproject --path ./workspace
16
+ # uv run new-project.py myproject --lib # library (publishable)
17
+ # ──────────────────
18
+
19
+ """Scaffold a new Python project with ultra-strict config from pyproject-strict.md.
20
+
21
+ Creates via `uv init`, then injects basedpyright + ruff ALL + pytest config.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import subprocess
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ import typer
31
+ from rich import print as rprint
32
+
33
+ # ── Strict tool config (from pyproject-strict.md) ──
34
+
35
+ TOOL_CONFIG = '''
36
+ [dependency-groups]
37
+ dev = [
38
+ "basedpyright>=1.21",
39
+ "ruff>=0.8",
40
+ "pytest>=8",
41
+ "pytest-cov>=5",
42
+ ]
43
+
44
+ [tool.basedpyright]
45
+ typeCheckingMode = "all"
46
+ pythonVersion = "3.13"
47
+ reportMissingTypeStubs = false
48
+ reportUnknownMemberType = false
49
+ reportUnknownArgumentType = false
50
+ reportUnknownVariableType = false
51
+ reportUnknownLambdaType = false
52
+ reportUnknownParameterType = false
53
+ reportMissingParameterType = false
54
+ reportUnnecessaryIsInstance = false
55
+ reportUnusedCallResult = false
56
+ reportImplicitOverride = false
57
+
58
+ [tool.ruff]
59
+ target-version = "py313"
60
+ line-length = 120
61
+
62
+ [tool.ruff.lint]
63
+ select = ["ALL"]
64
+ ignore = [
65
+ "COM812", # trailing comma (conflicts with formatter)
66
+ "ISC001", # single-line string concat (conflicts with formatter)
67
+ "D1", # undocumented-public-* (too noisy early on)
68
+ "ANN101", # deprecated: self annotation
69
+ "ANN102", # deprecated: cls annotation
70
+ "S101", # assert used (pytest needs it)
71
+ "PLR2004", # magic-value-comparison (test data)
72
+ "FBT", # boolean-trap (too strict for CLIs)
73
+ "TD", # flake8-todos (noisy)
74
+ "FIX", # fixme (noisy)
75
+ ]
76
+
77
+ [tool.ruff.lint.per-file-ignores]
78
+ "tests/**/*.py" = ["S101", "PLR2004", "SLF001", "D", "ARG", "ANN"]
79
+
80
+ [tool.ruff.lint.pydocstyle]
81
+ convention = "google"
82
+
83
+ [tool.pytest.ini_options]
84
+ testpaths = ["tests"]
85
+ addopts = "-ra --strict-markers --strict-config"
86
+ '''
87
+
88
+ GITIGNORE = """\
89
+ __pycache__/
90
+ *.py[cod]
91
+ *.so
92
+ .venv/
93
+ dist/
94
+ *.egg-info/
95
+ .coverage
96
+ htmlcov/
97
+ .basedpyright/
98
+ .ruff_cache/
99
+ """
100
+
101
+
102
+ def main(
103
+ name: str = typer.Argument(help="Project name"),
104
+ path: Path = typer.Option(Path("."), "--path", "-p", help="Parent directory"),
105
+ lib: bool = typer.Option(False, "--lib", help="Create as publishable library (uv init --lib)"),
106
+ ) -> None:
107
+ """Create a new Python project with ultra-strict config."""
108
+ project_dir = path / name
109
+
110
+ if project_dir.exists():
111
+ rprint(f"[red]Error:[/red] {project_dir} already exists")
112
+ raise SystemExit(1)
113
+
114
+ # Run uv init
115
+ cmd = ["uv", "init", "--lib" if lib else "--app", str(project_dir)]
116
+ result = subprocess.run(cmd, capture_output=True, text=True)
117
+ if result.returncode != 0:
118
+ rprint(f"[red]uv init failed:[/red] {result.stderr}")
119
+ raise SystemExit(1)
120
+
121
+ # Read existing pyproject.toml
122
+ pyproject_path = project_dir / "pyproject.toml"
123
+ content = pyproject_path.read_text()
124
+
125
+ # Remove the default [dependency-groups] if uv init created one
126
+ # (we'll replace it with our strict version)
127
+ lines = content.splitlines(keepends=True)
128
+ filtered: list[str] = []
129
+ skip = False
130
+ for line in lines:
131
+ if line.strip().startswith("[dependency-groups]"):
132
+ skip = True
133
+ continue
134
+ if skip and line.strip().startswith("["):
135
+ skip = False
136
+ if not skip:
137
+ filtered.append(line)
138
+
139
+ content = "".join(filtered).rstrip("\n") + "\n"
140
+
141
+ # Append strict tool config
142
+ content += TOOL_CONFIG
143
+
144
+ pyproject_path.write_text(content)
145
+
146
+ # Add dev dependencies
147
+ subprocess.run(
148
+ ["uv", "add", "--dev", "basedpyright", "ruff", "pytest", "pytest-cov"],
149
+ cwd=project_dir,
150
+ capture_output=True,
151
+ )
152
+
153
+ # Create tests directory
154
+ tests_dir = project_dir / "tests"
155
+ tests_dir.mkdir(exist_ok=True)
156
+ (tests_dir / "__init__.py").touch()
157
+
158
+ # Overwrite .gitignore
159
+ (project_dir / ".gitignore").write_text(GITIGNORE)
160
+
161
+ # Create py.typed marker for libraries
162
+ if lib:
163
+ src_dir = project_dir / "src" / name.replace("-", "_")
164
+ if src_dir.exists():
165
+ (src_dir / "py.typed").touch()
166
+
167
+ rprint(f"[green]✓[/green] Created: [bold]{project_dir}[/bold]")
168
+ rprint(f" cd {name} && uv sync && uv run basedpyright . && uv run ruff check .")
169
+
170
+
171
+ if __name__ == "__main__":
172
+ typer.run(main)
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = [
5
+ # "typer",
6
+ # "rich",
7
+ # ]
8
+ # ///
9
+
10
+ # ─── How to run ───
11
+ # 1. Install uv (if not installed):
12
+ # curl -LsSf https://astral.sh/uv/install.sh | sh
13
+ # 2. Run:
14
+ # uv run new-script.py my_tool
15
+ # uv run new-script.py my_tool --output ./scripts/my_tool.py
16
+ # uv run new-script.py my_tool --deps 'httpx2[http2,brotli,zstd]' --deps rich --deps polars
17
+ # uv run new-script.py my_tool --py 3.13
18
+ # ──────────────────
19
+
20
+ """Generate a PEP 723 Python script with all boilerplate pre-filled.
21
+
22
+ Creates a new .py file with:
23
+ - uv shebang
24
+ - PEP 723 inline metadata (requires-python + dependencies)
25
+ - Mandatory "How to run" comment block
26
+ - from __future__ import annotations
27
+ - main() + if __name__ guard
28
+
29
+ By default writes to a temp directory and prints the path.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import os
35
+ import stat
36
+ import sys
37
+ import tempfile
38
+ from pathlib import Path
39
+
40
+ import typer
41
+ from rich import print as rprint
42
+
43
+
44
+ TEMPLATE = '''\
45
+ #!/usr/bin/env -S uv run --script
46
+ # /// script
47
+ # requires-python = ">={python_version}"
48
+ # dependencies = [
49
+ {deps_block}# ]
50
+ # ///
51
+
52
+ # ─── How to run ───
53
+ # 1. Install uv (if not installed):
54
+ # curl -LsSf https://astral.sh/uv/install.sh | sh
55
+ # 2. Run directly (no venv, no pip install needed):
56
+ # uv run {filename} {args_hint}
57
+ # 3. Or make executable and run:
58
+ # chmod +x {filename} && ./{filename}
59
+ # ──────────────────
60
+
61
+ from __future__ import annotations
62
+
63
+
64
+ def main() -> None:
65
+ """TODO: implement."""
66
+
67
+
68
+ if __name__ == "__main__":
69
+ main()
70
+ '''
71
+
72
+
73
+ def main(
74
+ name: str = typer.Argument(help="Script name (without .py extension)"),
75
+ output: Path | None = typer.Option(None, "--output", "-o", help="Output path. Default: OS temp directory."),
76
+ deps: list[str] = typer.Option([], "--deps", "-d", help="Dependencies to include (repeat --deps for each)."),
77
+ py: str = typer.Option("3.13", "--py", help="Minimum Python version."),
78
+ ) -> None:
79
+ """Generate a new PEP 723 script with all boilerplate pre-filled."""
80
+ filename = f"{name}.py" if not name.endswith(".py") else name
81
+ stem = filename.removesuffix(".py")
82
+
83
+ if output is not None:
84
+ dest = Path(output)
85
+ else:
86
+ tmp_dir = Path(tempfile.gettempdir()) / "uv-scripts"
87
+ tmp_dir.mkdir(exist_ok=True)
88
+ dest = tmp_dir / filename
89
+
90
+ dep_list = deps or []
91
+ if dep_list:
92
+ deps_block = "".join(f'# "{d}",\n' for d in dep_list)
93
+ else:
94
+ deps_block = '# # add deps here, e.g.: "httpx2[http2,brotli,zstd]"\n'
95
+
96
+ content = TEMPLATE.format(
97
+ python_version=py,
98
+ deps_block=deps_block,
99
+ filename=filename,
100
+ args_hint="",
101
+ )
102
+
103
+ dest.parent.mkdir(parents=True, exist_ok=True)
104
+ dest.write_text(content)
105
+
106
+ # Make executable on Unix
107
+ if sys.platform != "win32":
108
+ st = dest.stat()
109
+ dest.chmod(st.st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
110
+
111
+ rprint(f"[green]✓[/green] Created: [bold]{dest}[/bold]")
112
+ rprint(f" Run: [cyan]uv run {dest}[/cyan]")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ typer.run(main)
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env -S uv run --script
2
+ # /// script
3
+ # requires-python = ">=3.11"
4
+ # dependencies = []
5
+ # ///
6
+ #
7
+ # How to run:
8
+ # uv run --script check-no-excuse-rules.py src/lib.rs src/main.rs
9
+ # uv run --script check-no-excuse-rules.py src/ # recursively finds .rs files
10
+ # uv run --script check-no-excuse-rules.py . # entire tree
11
+ #
12
+ # No-excuse rule checker for Rust files — Python rewrite of check-no-excuse-rules.sh.
13
+ # Only rules enforceable via pure text matching live here.
14
+ # Everything semantic is on clippy + miri + nextest.
15
+ #
16
+ # Rules:
17
+ # unwrap .unwrap() outside tests without // SAFE-UNWRAP:
18
+ # expect .expect() outside tests without // SAFE-EXPECT:
19
+ # placeholder-macro todo!/unimplemented!/unreachable!/unreachable_unchecked! in committed code
20
+ # box-dyn-error Box<dyn Error> in non-test code
21
+ # lib-panic panic!() in library code
22
+ # unsafe-no-safety unsafe { without // SAFETY: in preceding 5 lines
23
+ # unjustified-clippy-allow #[allow(clippy::...)] without // CLIPPY-ALLOW:
24
+ # narrowing-as-cast possible narrowing 'as' cast
25
+ #
26
+ # Opt-out: place the appropriate comment on the previous line:
27
+ # // SAFE-UNWRAP: <reason>
28
+ # // SAFE-EXPECT: <reason>
29
+ # // SAFETY: <reason> (for unsafe blocks, within 5 lines above)
30
+ # // CLIPPY-ALLOW: <reason>
31
+ #
32
+ # Test paths (exempt from unwrap/expect/placeholder/box-dyn-error/lib-panic):
33
+ # tests/, benches/, examples/, build.rs, *_test.rs, #[cfg(test)] regions
34
+
35
+ from __future__ import annotations
36
+
37
+ import re
38
+ import sys
39
+ from pathlib import Path
40
+
41
+ # ---------------------------------------------------------------------------
42
+ # Patterns (compiled once)
43
+ # ---------------------------------------------------------------------------
44
+
45
+ RE_UNWRAP = re.compile(r"\.unwrap\(\)")
46
+ RE_EXPECT = re.compile(r"\.expect\(")
47
+ RE_PLACEHOLDER = re.compile(r"\b(todo!|unimplemented!|unreachable!|unreachable_unchecked!)")
48
+ RE_BOX_DYN_ERROR = re.compile(r"Box<dyn\s+Error")
49
+ RE_PANIC = re.compile(r"\bpanic!\(")
50
+ RE_UNSAFE_BLOCK = re.compile(r"\bunsafe\s*\{")
51
+ RE_CLIPPY_ALLOW = re.compile(r"#\[allow\(clippy::")
52
+ RE_CFG_TEST = re.compile(r"#\[cfg\(test\)\]")
53
+ RE_SAFE_UNWRAP = re.compile(r"//\s*SAFE-UNWRAP:")
54
+ RE_SAFE_EXPECT = re.compile(r"//\s*SAFE-EXPECT:")
55
+ RE_SAFETY = re.compile(r"//\s*SAFETY:")
56
+ RE_CLIPPY_ALLOW_JUST = re.compile(r"//\s*CLIPPY-ALLOW:")
57
+
58
+ # Narrowing cast: (wider) as (narrower)
59
+ # Wider types that lose bits when cast to narrower targets.
60
+ # No leading \b — must match e.g. `999u64 as u32` where a digit precedes the type.
61
+ _WIDER = r"(?:u16|u32|u64|u128|usize|i16|i32|i64|i128|isize)"
62
+ _NARROWER = r"(?:u8|u16|u32|i8|i16|i32)"
63
+ RE_NARROWING_CAST = re.compile(
64
+ rf"{_WIDER}\s+as\s+{_NARROWER}"
65
+ )
66
+
67
+ # Test-path fragments.
68
+ _TEST_PATH_PARTS = {"tests", "benches", "examples"}
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Helpers
73
+ # ---------------------------------------------------------------------------
74
+
75
+ violations = 0
76
+
77
+
78
+ def report(file: str, line: int, rule: str, detail: str) -> None:
79
+ """Emit a GitHub-Actions-compatible error annotation to stderr."""
80
+ global violations
81
+ print(f"::error file={file},line={line}::[{rule}] {detail}", file=sys.stderr)
82
+ violations += 1
83
+
84
+
85
+ def is_test_path(path: Path) -> bool:
86
+ """Return True if *path* is in a test/bench/example directory or is a test file."""
87
+ parts = path.parts
88
+ for part in parts:
89
+ if part in _TEST_PATH_PARTS:
90
+ return True
91
+ if path.name == "build.rs":
92
+ return True
93
+ if path.name.endswith("_test.rs"):
94
+ return True
95
+ return False
96
+
97
+
98
+ def is_lib_path(file: Path) -> bool:
99
+ """Heuristic: is this file library code (not main.rs, not src/bin/*)."""
100
+ parts = path_parts_str(file)
101
+ # Must live under src/
102
+ if "src" not in parts:
103
+ return False
104
+ if file.name == "main.rs":
105
+ return False
106
+ # src/bin/* is binary code
107
+ try:
108
+ src_idx = parts.index("src")
109
+ if src_idx + 1 < len(parts) and parts[src_idx + 1] == "bin":
110
+ return False
111
+ except ValueError:
112
+ return False
113
+ return True
114
+
115
+
116
+ def path_parts_str(p: Path) -> list[str]:
117
+ return list(p.parts)
118
+
119
+
120
+ def strip_line_comment(line: str) -> str:
121
+ """Return the portion of *line* before any ``//`` line comment.
122
+
123
+ This is a crude heuristic — it does not handle ``//`` inside string
124
+ literals, but matches the behaviour of the bash version.
125
+ """
126
+ idx = line.find("//")
127
+ if idx == -1:
128
+ return line
129
+ return line[:idx]
130
+
131
+
132
+ def collect_rs_files(args: list[str]) -> list[Path]:
133
+ """Expand CLI arguments: files are kept as-is, directories are walked."""
134
+ result: list[Path] = []
135
+ for arg in args:
136
+ p = Path(arg)
137
+ if p.is_file():
138
+ if p.suffix == ".rs":
139
+ result.append(p)
140
+ elif p.is_dir():
141
+ result.extend(sorted(p.rglob("*.rs")))
142
+ # Ignore non-existent / non-.rs
143
+ return result
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Main checker
148
+ # ---------------------------------------------------------------------------
149
+
150
+ def check_file(file: Path) -> None:
151
+ in_test_file = is_test_path(file)
152
+ in_cfg_test = False
153
+ cfg_test_brace_depth = 0
154
+
155
+ try:
156
+ lines = file.read_text(encoding="utf-8", errors="replace").splitlines()
157
+ except OSError as exc:
158
+ print(f"warning: cannot read {file}: {exc}", file=sys.stderr)
159
+ return
160
+
161
+ for line_no_0, raw_line in enumerate(lines):
162
+ line_no = line_no_0 + 1 # 1-indexed
163
+
164
+ # --- #[cfg(test)] region tracker ---
165
+ if RE_CFG_TEST.search(raw_line):
166
+ in_cfg_test = True
167
+ cfg_test_brace_depth = 0
168
+
169
+ if in_cfg_test:
170
+ opens = raw_line.count("{")
171
+ closes = raw_line.count("}")
172
+ cfg_test_brace_depth += opens - closes
173
+ if cfg_test_brace_depth <= 0 and not RE_CFG_TEST.search(raw_line):
174
+ in_cfg_test = False
175
+
176
+ exempt = in_test_file or in_cfg_test
177
+ code_only = strip_line_comment(raw_line)
178
+
179
+ if not exempt:
180
+ # .unwrap()
181
+ if RE_UNWRAP.search(code_only):
182
+ prev = lines[line_no_0 - 1] if line_no_0 > 0 else ""
183
+ if not RE_SAFE_UNWRAP.search(prev):
184
+ report(
185
+ str(file), line_no, "unwrap",
186
+ ".unwrap() outside tests - use ? / ok_or / pattern match "
187
+ "or annotate previous line with // SAFE-UNWRAP: <reason>",
188
+ )
189
+
190
+ # .expect(...)
191
+ if RE_EXPECT.search(code_only):
192
+ prev = lines[line_no_0 - 1] if line_no_0 > 0 else ""
193
+ if not RE_SAFE_EXPECT.search(prev):
194
+ report(
195
+ str(file), line_no, "expect",
196
+ ".expect() outside tests - use ? or annotate previous "
197
+ "line with // SAFE-EXPECT: <reason>",
198
+ )
199
+
200
+ # todo!/unimplemented!/unreachable!/unreachable_unchecked!
201
+ if RE_PLACEHOLDER.search(code_only):
202
+ report(
203
+ str(file), line_no, "placeholder-macro",
204
+ "todo!/unimplemented!/unreachable! in committed code",
205
+ )
206
+
207
+ # Box<dyn Error>
208
+ if RE_BOX_DYN_ERROR.search(code_only):
209
+ report(
210
+ str(file), line_no, "box-dyn-error",
211
+ "Box<dyn Error> in non-test code - use anyhow::Error (apps) "
212
+ "or thiserror enum (libs)",
213
+ )
214
+
215
+ # panic!() in library code
216
+ if is_lib_path(file) and RE_PANIC.search(code_only):
217
+ report(
218
+ str(file), line_no, "lib-panic",
219
+ "panic!() in library code - return Result",
220
+ )
221
+
222
+ # unsafe { without // SAFETY: — always enforced, even in tests
223
+ if RE_UNSAFE_BLOCK.search(code_only):
224
+ start = max(0, line_no_0 - 5)
225
+ window = "\n".join(lines[start : line_no_0 + 1])
226
+ if not RE_SAFETY.search(window):
227
+ report(
228
+ str(file), line_no, "unsafe-no-safety-comment",
229
+ "unsafe block without // SAFETY: comment in preceding 5 lines",
230
+ )
231
+
232
+ # #[allow(clippy::...)] without // CLIPPY-ALLOW: — always enforced
233
+ if RE_CLIPPY_ALLOW.search(code_only):
234
+ prev = lines[line_no_0 - 1] if line_no_0 > 0 else ""
235
+ if not RE_CLIPPY_ALLOW_JUST.search(prev):
236
+ report(
237
+ str(file), line_no, "unjustified-clippy-allow",
238
+ "#[allow(clippy::...)] without // CLIPPY-ALLOW: <reason> on "
239
+ "previous line",
240
+ )
241
+
242
+ # Narrowing numeric `as` casts
243
+ if RE_NARROWING_CAST.search(code_only):
244
+ report(
245
+ str(file), line_no, "narrowing-as-cast",
246
+ "possible narrowing 'as' cast - use TryFrom / try_into() for "
247
+ "fallible conversion",
248
+ )
249
+
250
+
251
+ # ---------------------------------------------------------------------------
252
+ # Entry point
253
+ # ---------------------------------------------------------------------------
254
+
255
+ def main() -> None:
256
+ global violations
257
+
258
+ if len(sys.argv) < 2:
259
+ print(f"Usage: {sys.argv[0]} <file.rs|dir> [file.rs|dir ...]", file=sys.stderr)
260
+ sys.exit(2)
261
+
262
+ files = collect_rs_files(sys.argv[1:])
263
+ if not files:
264
+ print("warning: no .rs files found in the given arguments", file=sys.stderr)
265
+ sys.exit(0)
266
+
267
+ for f in files:
268
+ check_file(f)
269
+
270
+ if violations > 0:
271
+ print("", file=sys.stderr)
272
+ print(
273
+ f"rust-programmer: {violations} violation(s). Fix before declaring work done.",
274
+ file=sys.stderr,
275
+ )
276
+ print("", file=sys.stderr)
277
+ print("Then run the full toolchain gate:", file=sys.stderr)
278
+ print(" cargo +stable fmt --all -- --check", file=sys.stderr)
279
+ print(
280
+ " cargo +stable clippy --all-targets --all-features -- -D warnings",
281
+ file=sys.stderr,
282
+ )
283
+ print(" cargo nextest run --all-targets --all-features", file=sys.stderr)
284
+ print(
285
+ " cargo +nightly miri nextest run --all-features # if unsafe touched",
286
+ file=sys.stderr,
287
+ )
288
+ print(" cargo machete", file=sys.stderr)
289
+ print(" cargo deny check", file=sys.stderr)
290
+ sys.exit(1)
291
+
292
+ print(f"rust-programmer: no-excuse rules passed for {len(files)} file(s).")
293
+
294
+
295
+ if __name__ == "__main__":
296
+ main()