cctx-cli 1.3.0__tar.gz → 1.4.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.
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/CHANGELOG.md +65 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/PKG-INFO +1 -1
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/__init__.py +1 -1
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/cli.py +37 -7
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/harvest.py +187 -18
- cctx_cli-1.4.0/docs/superpowers/plans/2026-05-17-harvest-check-depth.md +860 -0
- cctx_cli-1.4.0/docs/superpowers/specs/2026-05-17-harvest-check-depth-design.md +214 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/pyproject.toml +1 -1
- cctx_cli-1.4.0/tests/test_harvest_check.py +418 -0
- cctx_cli-1.3.0/tests/test_harvest_check.py +0 -139
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/.github/workflows/ci.yml +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/.github/workflows/publish.yml +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/.github/workflows/release.yml +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/.gitignore +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/CLAUDE.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/DESIGN.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/PRODUCT.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/README.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/action.yml +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/aggregate.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/inflection.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/dead_end.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/project_specific.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/retry_loop.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/scope_creep.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/stale_context.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/diagnostician/patterns/tool_thrash.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/discovery.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/exporters/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/exporters/csv.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/exporters/json.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/exporters/jsonl.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/models.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/parsers/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/parsers/claude_code.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/pricing.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/recommender/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/recommender/claude_md.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/recommender/evidence.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/github.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/report.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/templates/autopsy.html.j2 +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/terminal.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/renderers/trace_tui.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/tokenizer.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx/watcher.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/cctx-project-brief.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/demo.gif +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/demo.tape +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/health-reviews/2026-05-15-deep-review-summary.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/health-reviews/2026-05-15-health-review.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/product-reviews/2026-05-15-product-review.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-12-claude-code-parser.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-14-autopsy-v0.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-16-readme-pypi-release.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/plans/2026-05-17-project-pattern-detection.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-12-claude-code-parser-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-autopsy-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-harvest-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-14-trace-tui-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-16-readme-pypi-release-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/docs/superpowers/specs/2026-05-17-project-pattern-detection-design.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/conftest.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/conftest.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_dead_end.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_inflection.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_orchestrator.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_project_specific.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_retry_loop.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_scope_creep.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_stale_context.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/diagnostician/test_tool_thrash.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/exporters/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/exporters/test_csv.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/exporters/test_jsonl.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/README.md +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/short-clean/short-clean.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a0b4c2cf1dde0ca56.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a116ae34b1b09c332.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1c4c417b35658c9e.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a1e41a901de38f1b5.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a338f8d0c74612a24.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a34f6f3c0e7094186.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a5a5a0cff4d13308b.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a6b0a3da6a0484db5.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f73f1790b02cde5.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a7f7c17c38a9d8788.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a853259e2cd7bbe8a.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-a8d9aedb0d0c6e12d.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aa778bc1d59e4a441.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aba869dedee4a12ba.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-ada2746d9774b94db.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea0132068c64d2dd.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-aea215eff50874d5f.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments/subagents/agent-afee21f2b3852a4a0.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-attachments/with-attachments.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a051d9c9a6b2f5cc3.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a171f16f4e65cfe75.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a1b77fea2c0a2269b.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a20da4c01a54acca8.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a3c82739b1383fb14.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a49e8539611c5fe12.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a7bb58f3fff2b3e8d.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-a92b48c0331195aac.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ab96c4264099694a9.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-acb2895c5e34ffec0.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-adb2302769938fb3f.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-ae585eca15cb93b9c.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction/subagents/agent-aec9c917feb903d67.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-compaction/with-compaction.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-a1a3a21aeb76bb0a9.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-aaa1d6ecc05a78442.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/subagents/agent-af3c545ccd30036d2.meta.json +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/btwp2bzro.txt +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents/tool-results/byqjbgy4b.txt +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-subagents/with-subagents.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results/tool-results/bosbkda0h.txt +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/claude_code/with-tool-results/with-tool-results.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/scrub.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/bookkeeping_only.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/malformed_middle.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/truncated_final_line.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/unknown_attachment_shape.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/fixtures/synthetic/unknown_type.jsonl +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/parsers/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/parsers/test_claude_code.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/parsers/test_claude_code_integration.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/recommender/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/recommender/test_claude_md.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/recommender/test_evidence.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/renderers/__init__.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/renderers/test_report.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/renderers/test_terminal_renderer_full.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_aggregate.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_cli.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_cli_export.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_discovery.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_github_summary.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_harvest.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_models.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_models_project_pattern.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_recommender.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_smoke.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_terminal_renderer.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_tokenizer.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_trace_tui.py +0 -0
- {cctx_cli-1.3.0 → cctx_cli-1.4.0}/tests/test_watcher.py +0 -0
|
@@ -2,6 +2,71 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v1.4.0 (2026-05-20)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Deduplicate harvest check import, align severity badge output
|
|
10
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
11
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
12
|
+
|
|
13
|
+
- Move defaultdict import to top-level, add _words() return type
|
|
14
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
15
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
16
|
+
|
|
17
|
+
- Use removeprefix instead of lstrip to preserve .claude/skills/ dot prefix
|
|
18
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
19
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
- M15 harvest --check depth design spec ([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
24
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
25
|
+
|
|
26
|
+
- M15 harvest --check depth implementation plan
|
|
27
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
28
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
- --check-severity flag and severity badges in harvest --check output
|
|
33
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
34
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
35
|
+
|
|
36
|
+
- Check_contradictions() — always/never keyword heuristic
|
|
37
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
38
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
39
|
+
|
|
40
|
+
- Check_redundancy() — Jaccard similarity ≥ 0.8 on section word sets
|
|
41
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
42
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
43
|
+
|
|
44
|
+
- Check_staleness() — backtick function refs grepped against project source
|
|
45
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
46
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
47
|
+
|
|
48
|
+
- CheckSeverity enum, severity field on CheckFinding, new CheckIssue values
|
|
49
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
50
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
51
|
+
|
|
52
|
+
- Harvest --check depth — contradiction, redundancy, staleness detectors + --check-severity
|
|
53
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
54
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
55
|
+
|
|
56
|
+
- Wire all four detectors into check_claude_md ([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
57
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
58
|
+
|
|
59
|
+
### Refactoring
|
|
60
|
+
|
|
61
|
+
- Check_redundancy — compute _words once per section, remove dead union guard
|
|
62
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
63
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
64
|
+
|
|
65
|
+
- Check_staleness — module-level _STALENESS_EXCLUDED, min-len in regex, per-file search
|
|
66
|
+
([#87](https://github.com/jacquardlabs/cctx/pull/87),
|
|
67
|
+
[`ee08734`](https://github.com/jacquardlabs/cctx/commit/ee0873431383b285769195efc4b2f70f5d07cdeb))
|
|
68
|
+
|
|
69
|
+
|
|
5
70
|
## v1.3.0 (2026-05-17)
|
|
6
71
|
|
|
7
72
|
### Bug Fixes
|
|
@@ -147,22 +147,32 @@ def _render_check_findings(findings: list, target_dir: Path) -> None:
|
|
|
147
147
|
from rich.console import Console
|
|
148
148
|
from rich.rule import Rule
|
|
149
149
|
|
|
150
|
+
from cctx.harvest import CheckIssue, CheckSeverity
|
|
151
|
+
|
|
150
152
|
con = Console()
|
|
151
153
|
claude_md_path = target_dir / "CLAUDE.md"
|
|
152
154
|
con.print(Rule(f"cctx harvest --check — {claude_md_path}"))
|
|
153
155
|
if not findings:
|
|
154
|
-
con.print("✓ CLAUDE.md looks clean — no
|
|
156
|
+
con.print("✓ CLAUDE.md looks clean — no issues found.")
|
|
155
157
|
return
|
|
156
158
|
con.print(f"{len(findings)} issue(s) found:\n")
|
|
157
|
-
from cctx.harvest import CheckIssue
|
|
158
159
|
_ISSUE_LABEL = {
|
|
159
|
-
CheckIssue.DEAD_FILE_REF:
|
|
160
|
-
CheckIssue.DEAD_SKILL_REF:
|
|
161
|
-
CheckIssue.EMPTY_SECTION:
|
|
160
|
+
CheckIssue.DEAD_FILE_REF: "dead file reference",
|
|
161
|
+
CheckIssue.DEAD_SKILL_REF: "dead skill reference",
|
|
162
|
+
CheckIssue.EMPTY_SECTION: "empty section",
|
|
163
|
+
CheckIssue.CONTRADICTION: "contradiction",
|
|
164
|
+
CheckIssue.REDUNDANCY: "redundancy",
|
|
165
|
+
CheckIssue.STALE_IDENTIFIER: "stale identifier",
|
|
166
|
+
}
|
|
167
|
+
_SEV_BADGE = {
|
|
168
|
+
CheckSeverity.HIGH: "[HIGH]",
|
|
169
|
+
CheckSeverity.MEDIUM: "[MED]",
|
|
170
|
+
CheckSeverity.LOW: "[LOW]",
|
|
162
171
|
}
|
|
163
172
|
for f in findings:
|
|
173
|
+
badge = _SEV_BADGE.get(f.severity, " ")
|
|
164
174
|
label = _ISSUE_LABEL.get(f.issue, f.issue.value)
|
|
165
|
-
con.print(f"
|
|
175
|
+
con.print(f" {badge:<6} {f.heading} {label}: {f.detail}")
|
|
166
176
|
|
|
167
177
|
|
|
168
178
|
@click.group()
|
|
@@ -549,6 +559,14 @@ def trace(target: Path | None, latest: bool) -> None:
|
|
|
549
559
|
default=False,
|
|
550
560
|
help="Audit existing CLAUDE.md for dead references and empty sections. Exit 1 if findings.",
|
|
551
561
|
)
|
|
562
|
+
@click.option(
|
|
563
|
+
"--check-severity",
|
|
564
|
+
"check_severity",
|
|
565
|
+
default="MEDIUM",
|
|
566
|
+
type=click.Choice(["LOW", "MEDIUM", "HIGH"], case_sensitive=False),
|
|
567
|
+
show_default=True,
|
|
568
|
+
help="Minimum severity that causes --check to exit 1.",
|
|
569
|
+
)
|
|
552
570
|
def harvest(
|
|
553
571
|
target: Path,
|
|
554
572
|
since: str | None,
|
|
@@ -556,15 +574,27 @@ def harvest(
|
|
|
556
574
|
dry_run: bool,
|
|
557
575
|
target_dir: Path | None,
|
|
558
576
|
check_mode: bool,
|
|
577
|
+
check_severity: str,
|
|
559
578
|
) -> None:
|
|
560
579
|
"""Apply autopsy patches to CLAUDE.md."""
|
|
561
580
|
from cctx.harvest import apply_patches, check_claude_md, preview_patches
|
|
562
581
|
|
|
563
582
|
if check_mode:
|
|
583
|
+
from cctx.harvest import CheckSeverity
|
|
564
584
|
resolved_dir = target_dir or Path.cwd()
|
|
565
585
|
findings = check_claude_md(resolved_dir)
|
|
566
586
|
_render_check_findings(findings, resolved_dir)
|
|
567
|
-
|
|
587
|
+
_SEVERITY_ORDER = {
|
|
588
|
+
CheckSeverity.LOW: 0,
|
|
589
|
+
CheckSeverity.MEDIUM: 1,
|
|
590
|
+
CheckSeverity.HIGH: 2,
|
|
591
|
+
}
|
|
592
|
+
threshold = CheckSeverity(check_severity.lower())
|
|
593
|
+
triggering = [
|
|
594
|
+
f for f in findings
|
|
595
|
+
if _SEVERITY_ORDER[f.severity] >= _SEVERITY_ORDER[threshold]
|
|
596
|
+
]
|
|
597
|
+
raise SystemExit(1 if triggering else 0)
|
|
568
598
|
|
|
569
599
|
if apply_mode and dry_run:
|
|
570
600
|
raise click.UsageError("--apply and --dry-run are mutually exclusive.")
|
|
@@ -14,6 +14,7 @@ Layering rules (MUST respect):
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import re
|
|
17
|
+
from collections import defaultdict
|
|
17
18
|
from dataclasses import dataclass
|
|
18
19
|
from enum import Enum
|
|
19
20
|
from pathlib import Path
|
|
@@ -28,17 +29,27 @@ if TYPE_CHECKING:
|
|
|
28
29
|
# ---------------------------------------------------------------------------
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
class CheckSeverity(str, Enum):
|
|
33
|
+
LOW = "low"
|
|
34
|
+
MEDIUM = "medium"
|
|
35
|
+
HIGH = "high"
|
|
36
|
+
|
|
37
|
+
|
|
31
38
|
class CheckIssue(str, Enum):
|
|
32
|
-
DEAD_FILE_REF
|
|
33
|
-
DEAD_SKILL_REF
|
|
34
|
-
EMPTY_SECTION
|
|
39
|
+
DEAD_FILE_REF = "dead_file_ref"
|
|
40
|
+
DEAD_SKILL_REF = "dead_skill_ref"
|
|
41
|
+
EMPTY_SECTION = "empty_section"
|
|
42
|
+
CONTRADICTION = "contradiction"
|
|
43
|
+
REDUNDANCY = "redundancy"
|
|
44
|
+
STALE_IDENTIFIER = "stale_identifier"
|
|
35
45
|
|
|
36
46
|
|
|
37
47
|
@dataclass
|
|
38
48
|
class CheckFinding:
|
|
39
|
-
heading:
|
|
40
|
-
issue:
|
|
41
|
-
|
|
49
|
+
heading: str
|
|
50
|
+
issue: CheckIssue
|
|
51
|
+
severity: CheckSeverity
|
|
52
|
+
detail: str
|
|
42
53
|
|
|
43
54
|
|
|
44
55
|
class ApplyStatus(str, Enum):
|
|
@@ -212,6 +223,25 @@ _KNOWN_EXTENSIONS = {
|
|
|
212
223
|
".json", ".md", ".sh", ".bash", ".fish", ".zsh",
|
|
213
224
|
}
|
|
214
225
|
|
|
226
|
+
_STOPWORDS = {
|
|
227
|
+
"a", "an", "the", "to", "be", "is", "are", "was", "were",
|
|
228
|
+
"in", "on", "at", "of", "for", "with", "and", "or", "not",
|
|
229
|
+
"it", "this", "that", "you", "your", "use", "do",
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
_ALWAYS_NEVER_RE = re.compile(
|
|
233
|
+
r"\b(always|never)\b(.+?)(?:[.!?\n]|$)", re.IGNORECASE
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
_STALENESS_EXCLUDED = {".git", ".venv", "node_modules", "__pycache__"}
|
|
237
|
+
|
|
238
|
+
_FUNC_REF_RE = re.compile(r"`([^`/.\s]{8,})\(\)`")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _words(text: str) -> set[str]:
|
|
242
|
+
tokens = re.findall(r"\b[a-zA-Z_]\w*\b", text.lower())
|
|
243
|
+
return {t for t in tokens if t not in _STOPWORDS}
|
|
244
|
+
|
|
215
245
|
|
|
216
246
|
def _parse_sections(content: str) -> list[tuple[str, str]]:
|
|
217
247
|
"""Split markdown into (heading, body) pairs.
|
|
@@ -233,22 +263,133 @@ def _parse_sections(content: str) -> list[tuple[str, str]]:
|
|
|
233
263
|
return sections
|
|
234
264
|
|
|
235
265
|
|
|
236
|
-
def
|
|
237
|
-
|
|
266
|
+
def check_contradictions(
|
|
267
|
+
sections: list[tuple[str, str]],
|
|
268
|
+
) -> list[CheckFinding]:
|
|
269
|
+
"""Detect contradictions across sections using always/never polarity heuristic.
|
|
238
270
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
- Dead skill references: .claude/skills/ paths that don't exist
|
|
242
|
-
- Empty sections: ## headings with no content
|
|
271
|
+
Looks for "always" and "never" clauses in section bodies, extracts the
|
|
272
|
+
subject words, and flags cases where the same word has conflicting polarities.
|
|
243
273
|
|
|
244
|
-
Returns
|
|
274
|
+
Returns findings for each contradiction found (severity: HIGH).
|
|
245
275
|
"""
|
|
246
|
-
|
|
247
|
-
|
|
276
|
+
subject_map: dict[str, list[tuple[str, str]]] = defaultdict(list)
|
|
277
|
+
for heading, body in sections:
|
|
278
|
+
for match in _ALWAYS_NEVER_RE.finditer(body):
|
|
279
|
+
polarity = match.group(1).lower()
|
|
280
|
+
clause = match.group(2)
|
|
281
|
+
for word in _words(clause):
|
|
282
|
+
subject_map[word].append((polarity, heading))
|
|
283
|
+
|
|
284
|
+
findings: list[CheckFinding] = []
|
|
285
|
+
seen: set[tuple[str, str]] = set()
|
|
286
|
+
for word, occurrences in subject_map.items():
|
|
287
|
+
always_headings = [h for p, h in occurrences if p == "always"]
|
|
288
|
+
never_headings = [h for p, h in occurrences if p == "never"]
|
|
289
|
+
if always_headings and never_headings:
|
|
290
|
+
key = (always_headings[0], never_headings[0])
|
|
291
|
+
if key not in seen:
|
|
292
|
+
seen.add(key)
|
|
293
|
+
findings.append(CheckFinding(
|
|
294
|
+
heading=always_headings[0],
|
|
295
|
+
issue=CheckIssue.CONTRADICTION,
|
|
296
|
+
severity=CheckSeverity.HIGH,
|
|
297
|
+
detail=(
|
|
298
|
+
f"'{word}' is 'always' in {always_headings[0]!r}"
|
|
299
|
+
f" but 'never' in {never_headings[0]!r}"
|
|
300
|
+
),
|
|
301
|
+
))
|
|
302
|
+
return findings
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def check_redundancy(
|
|
306
|
+
sections: list[tuple[str, str]],
|
|
307
|
+
) -> list[CheckFinding]:
|
|
308
|
+
"""Detect redundancy across sections using Jaccard similarity.
|
|
309
|
+
|
|
310
|
+
Builds a word set (stopwords removed) for each section. Sections with
|
|
311
|
+
fewer than 5 words are ineligible. For all pairs of eligible sections,
|
|
312
|
+
computes Jaccard similarity of their word sets. Flags pairs with
|
|
313
|
+
similarity >= 0.8.
|
|
314
|
+
|
|
315
|
+
Returns findings for each redundancy found (severity: MEDIUM).
|
|
316
|
+
"""
|
|
317
|
+
eligible = []
|
|
318
|
+
for heading, body in sections:
|
|
319
|
+
ws = _words(body)
|
|
320
|
+
if len(ws) >= 5:
|
|
321
|
+
eligible.append((heading, body, ws))
|
|
322
|
+
|
|
323
|
+
findings: list[CheckFinding] = []
|
|
324
|
+
for i in range(len(eligible)):
|
|
325
|
+
for j in range(i + 1, len(eligible)):
|
|
326
|
+
h1, _, w1 = eligible[i]
|
|
327
|
+
h2, _, w2 = eligible[j]
|
|
328
|
+
union = w1 | w2
|
|
329
|
+
jaccard = len(w1 & w2) / len(union)
|
|
330
|
+
if jaccard >= 0.8:
|
|
331
|
+
findings.append(CheckFinding(
|
|
332
|
+
heading=h1,
|
|
333
|
+
issue=CheckIssue.REDUNDANCY,
|
|
334
|
+
severity=CheckSeverity.MEDIUM,
|
|
335
|
+
detail=f"{h1!r} and {h2!r} are {jaccard:.0%} similar",
|
|
336
|
+
))
|
|
337
|
+
return findings
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def check_staleness(
|
|
341
|
+
sections: list[tuple[str, str]],
|
|
342
|
+
project_dir: Path,
|
|
343
|
+
) -> list[CheckFinding]:
|
|
344
|
+
"""Detect stale function references in CLAUDE.md.
|
|
345
|
+
|
|
346
|
+
Scans all .py, .ts, and .js source files in the project directory and
|
|
347
|
+
searches for backtick-quoted function references (e.g., `my_function()`)
|
|
348
|
+
that are 8+ characters long. Flags references not found in the source.
|
|
349
|
+
|
|
350
|
+
Returns findings for each stale identifier found (severity: LOW).
|
|
351
|
+
"""
|
|
352
|
+
source_files = [
|
|
353
|
+
f
|
|
354
|
+
for f in (
|
|
355
|
+
list(project_dir.rglob("*.py"))
|
|
356
|
+
+ list(project_dir.rglob("*.ts"))
|
|
357
|
+
+ list(project_dir.rglob("*.js"))
|
|
358
|
+
)
|
|
359
|
+
if not any(part in _STALENESS_EXCLUDED for part in f.parts)
|
|
360
|
+
]
|
|
361
|
+
if not source_files:
|
|
248
362
|
return []
|
|
249
363
|
|
|
250
|
-
|
|
251
|
-
|
|
364
|
+
findings: list[CheckFinding] = []
|
|
365
|
+
for heading, body in sections:
|
|
366
|
+
for match in _FUNC_REF_RE.finditer(body):
|
|
367
|
+
name = match.group(1)
|
|
368
|
+
found = any(
|
|
369
|
+
name in f.read_text(encoding="utf-8", errors="ignore")
|
|
370
|
+
for f in source_files
|
|
371
|
+
)
|
|
372
|
+
if not found:
|
|
373
|
+
findings.append(CheckFinding(
|
|
374
|
+
heading=heading,
|
|
375
|
+
issue=CheckIssue.STALE_IDENTIFIER,
|
|
376
|
+
severity=CheckSeverity.LOW,
|
|
377
|
+
detail=f"'{name}()' not found in project source files",
|
|
378
|
+
))
|
|
379
|
+
return findings
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _check_structure(
|
|
383
|
+
sections: list[tuple[str, str]],
|
|
384
|
+
target_dir: Path,
|
|
385
|
+
) -> list[CheckFinding]:
|
|
386
|
+
"""Check structure issues: empty sections, dead file/skill references.
|
|
387
|
+
|
|
388
|
+
Returns findings for:
|
|
389
|
+
- Empty sections: ## headings with no content (MEDIUM)
|
|
390
|
+
- Dead file references: backtick-quoted paths that don't exist (MEDIUM)
|
|
391
|
+
- Dead skill references: .claude/skills/ paths that don't exist (MEDIUM)
|
|
392
|
+
"""
|
|
252
393
|
findings: list[CheckFinding] = []
|
|
253
394
|
|
|
254
395
|
for heading, body in sections:
|
|
@@ -259,13 +400,14 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
|
|
|
259
400
|
findings.append(CheckFinding(
|
|
260
401
|
heading=heading,
|
|
261
402
|
issue=CheckIssue.EMPTY_SECTION,
|
|
403
|
+
severity=CheckSeverity.MEDIUM,
|
|
262
404
|
detail=f"{heading!r} has no content",
|
|
263
405
|
))
|
|
264
406
|
continue
|
|
265
407
|
|
|
266
408
|
# Dead skill references
|
|
267
409
|
for match in _SKILL_REF_RE.finditer(body):
|
|
268
|
-
skill_path_str = match.group(1).
|
|
410
|
+
skill_path_str = match.group(1).removeprefix("./")
|
|
269
411
|
# Try resolving from target_dir and from home
|
|
270
412
|
candidates = [
|
|
271
413
|
target_dir / skill_path_str,
|
|
@@ -275,6 +417,7 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
|
|
|
275
417
|
findings.append(CheckFinding(
|
|
276
418
|
heading=heading,
|
|
277
419
|
issue=CheckIssue.DEAD_SKILL_REF,
|
|
420
|
+
severity=CheckSeverity.MEDIUM,
|
|
278
421
|
detail=f"skill not found: {match.group(1)!r}",
|
|
279
422
|
))
|
|
280
423
|
|
|
@@ -292,7 +435,33 @@ def check_claude_md(target_dir: Path) -> list[CheckFinding]:
|
|
|
292
435
|
findings.append(CheckFinding(
|
|
293
436
|
heading=heading,
|
|
294
437
|
issue=CheckIssue.DEAD_FILE_REF,
|
|
438
|
+
severity=CheckSeverity.MEDIUM,
|
|
295
439
|
detail=f"file not found: {token!r}",
|
|
296
440
|
))
|
|
297
441
|
|
|
298
442
|
return findings
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def check_claude_md(target_dir: Path) -> list[CheckFinding]:
|
|
446
|
+
"""Audit CLAUDE.md in target_dir for deterministically detectable issues.
|
|
447
|
+
|
|
448
|
+
Checks:
|
|
449
|
+
- Dead file/skill references and empty sections (MEDIUM)
|
|
450
|
+
- Contradictory always/never rules (HIGH)
|
|
451
|
+
- Redundant sections with Jaccard >= 0.8 (MEDIUM)
|
|
452
|
+
- Stale backtick-quoted function identifiers >= 8 chars (LOW)
|
|
453
|
+
|
|
454
|
+
Returns an empty list if CLAUDE.md doesn't exist (not an error).
|
|
455
|
+
"""
|
|
456
|
+
claude_md = target_dir / "CLAUDE.md"
|
|
457
|
+
if not claude_md.exists():
|
|
458
|
+
return []
|
|
459
|
+
|
|
460
|
+
content = claude_md.read_text(encoding="utf-8")
|
|
461
|
+
sections = _parse_sections(content)
|
|
462
|
+
return (
|
|
463
|
+
_check_structure(sections, target_dir)
|
|
464
|
+
+ check_contradictions(sections)
|
|
465
|
+
+ check_redundancy(sections)
|
|
466
|
+
+ check_staleness(sections, target_dir)
|
|
467
|
+
)
|