reprompt-cli 1.9.0__tar.gz → 1.9.1__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.
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/PKG-INFO +1 -1
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/pyproject.toml +1 -1
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/__init__.py +1 -1
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/cli.py +12 -6
- reprompt_cli-1.9.1/src/reprompt/core/lint.py +242 -0
- reprompt_cli-1.9.1/tests/test_lint.py +237 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/uv.lock +1 -1
- reprompt_cli-1.9.0/src/reprompt/core/lint.py +0 -112
- reprompt_cli-1.9.0/tests/test_lint.py +0 -81
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.editorconfig +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/dependabot.yml +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/workflows/ci.yml +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.github/workflows/publish.yml +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.gitignore +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.pre-commit-config.yaml +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.pre-commit-hooks.yaml +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.testmondata-shm +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/.testmondata-wal +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/CHANGELOG.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/CODE_OF_CONDUCT.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/CONTRIBUTING.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/LICENSE +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/README.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/SECURITY.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/action.yml +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/demo.gif +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-128.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-16.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-256.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-32.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-48.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-512.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon-96.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/brand-icon.svg +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-128.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-16.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-256.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-32.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-48.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-512.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon-96.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-favicon.svg +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-128.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-16.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-256.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-32.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-48.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-512.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon-96.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/cli-icon.svg +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-128.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-16.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-256.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-32.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-48.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-512.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon-96.png +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/favicon.svg +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/icons/generate.sh +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/scripts/generate_demo_data.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/aider.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/base.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/chatgpt.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/claude_chat.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/claude_code.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/cline.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/codex.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/cursor.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/filters.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/gemini.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/adapters/openclaw.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/handler.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/host.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/manifest.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/bridge/protocol.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/commands/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/commands/telemetry.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/commands/wrapped.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/config.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/agent.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/analyzer.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/compress.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/conversation.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/cost.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/dashboard.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/dedup.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/digest.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/distill.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/effectiveness.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/extractors.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/extractors_zh.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/insights.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/lang_detect.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/library.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/merge_view.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/models.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/persona.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/pipeline.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/privacy.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/privacy_scan.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/prompt_dna.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/recommend.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/repetition.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/scorer.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/segmenter.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/session_meta.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/session_quality.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/session_type.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/style.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/suggestions.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/templates.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/timeutil.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/trends.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/core/wrapped.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/demo.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/base.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/local_embed.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/ollama.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/openai_embed.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/embeddings/tfidf.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/mcp.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/mcp_main.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/agent_terminal.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/chartjs.min.js +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/compress_terminal.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/dashboard_terminal.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/distill_terminal.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/export.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/html_report.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/json_out.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/markdown.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/repetition_terminal.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/sessions_terminal.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/terminal.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/wrapped_html.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/output/wrapped_terminal.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/py.typed +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/sharing/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/sharing/client.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/sharing/clipboard.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/storage/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/storage/db.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/collector.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/consent.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/events.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/prompt.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/queue.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/src/reprompt/telemetry/sender.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/__init__.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/conftest.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/aider_chat_history.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/chatgpt_conversations.json +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/claude_chat_export.json +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/claude_session.jsonl +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/export/default_export.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/export/full_export.md +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/gemini_session.json +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/fixtures/openclaw_session.jsonl +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_aider.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_chatgpt.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_claude.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_claude_chat.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_cline.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_gemini.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_adapter_openclaw.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_agent.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_agent_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_analyzer.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_e2e.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_handler.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_integration.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_manifest.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_bridge_protocol.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cli_deprecations.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cli_library_effectiveness.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_clipboard.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_codex_adapter.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compare_best_worst.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress_dna.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress_html.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_compress_insights.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_config.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_conversation.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_copy_flag.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cost.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_coverage_boost.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_cursor_adapter.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_dashboard.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db_digest.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db_effectiveness.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db_session_quality.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_db_trends.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_dedup.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_demo.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_deprecated_commands.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_digest.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_digest_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_distill.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_distill_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_distill_weights.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_e2e.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_effectiveness.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_embeddings_local.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_embeddings_ollama.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_embeddings_openai.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_empty_state.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_export.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_export_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_export_snapshot.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_extractors.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_extractors_routing.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_extractors_zh.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_extractors_zh_e2e.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_html_report.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_import_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_import_e2e.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_insights.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_insights_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_insights_expanded.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_install_hook.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_lang_detect.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_library.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_lint_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_markdown.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_mcp.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_merge_view.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_models.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_output.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_parse_conversation_base.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_parse_conversation_chatgpt.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_parse_conversation_claude.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_persona.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_pipeline.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy_e2e.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy_output.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_privacy_scan.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_prompt_dna.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_public_api.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_recommend.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_repetition.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_repetition_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_repetition_output.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_schema_version.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_score_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_scorer.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_segmenter.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_session_quality.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_session_type.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_sessions_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_sessions_output.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_share_e2e.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_sharing_client.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_source_filter.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_style.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_style_trends.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_suggestions.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_collector.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_consent.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_e2e.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_events.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_prompt.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_queue.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_telemetry_sender.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_template_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_templates.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_timeutil.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_trends.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_trends_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_use_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_cli.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_e2e.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_html.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_output.py +0 -0
- {reprompt_cli-1.9.0 → reprompt_cli-1.9.1}/tests/test_wrapped_share.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reprompt-cli
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.1
|
|
4
4
|
Summary: Discover, analyze, and optimize your prompts from AI coding sessions
|
|
5
5
|
Project-URL: Homepage, https://github.com/reprompt-dev/reprompt
|
|
6
6
|
Project-URL: Repository, https://github.com/reprompt-dev/reprompt
|
|
@@ -820,13 +820,19 @@ def lint(
|
|
|
820
820
|
import json as json_mod
|
|
821
821
|
|
|
822
822
|
from reprompt.config import Settings
|
|
823
|
-
from reprompt.core.lint import format_lint_results, lint_prompts
|
|
823
|
+
from reprompt.core.lint import format_lint_results, lint_prompts, load_lint_config
|
|
824
824
|
from reprompt.core.pipeline import get_adapters
|
|
825
825
|
from reprompt.storage.db import PromptDB
|
|
826
826
|
|
|
827
827
|
settings = Settings()
|
|
828
828
|
db = PromptDB(settings.db_path)
|
|
829
829
|
|
|
830
|
+
# Load lint config from .reprompt.toml / pyproject.toml
|
|
831
|
+
lint_config = load_lint_config()
|
|
832
|
+
|
|
833
|
+
# CLI flags override config file
|
|
834
|
+
effective_threshold = score_threshold if score_threshold > 0 else lint_config.score_threshold
|
|
835
|
+
|
|
830
836
|
# Collect prompts from DB (already scanned)
|
|
831
837
|
rows = db.get_all_prompts()
|
|
832
838
|
texts = [r["text"] for r in rows]
|
|
@@ -858,11 +864,11 @@ def lint(
|
|
|
858
864
|
console.print("No prompts found. Run [bold]reprompt scan[/bold] first.")
|
|
859
865
|
raise typer.Exit(0)
|
|
860
866
|
|
|
861
|
-
violations = lint_prompts(texts)
|
|
867
|
+
violations = lint_prompts(texts, config=lint_config)
|
|
862
868
|
|
|
863
869
|
# Score threshold mode (CI integration)
|
|
864
870
|
score_data = None
|
|
865
|
-
if
|
|
871
|
+
if effective_threshold > 0:
|
|
866
872
|
from reprompt.core.extractors import extract_features
|
|
867
873
|
from reprompt.core.scorer import score_prompt
|
|
868
874
|
|
|
@@ -875,8 +881,8 @@ def lint(
|
|
|
875
881
|
"avg_score": round(avg_score, 1),
|
|
876
882
|
"min_score": min(scores) if scores else 0,
|
|
877
883
|
"max_score": max(scores) if scores else 0,
|
|
878
|
-
"threshold":
|
|
879
|
-
"pass": avg_score >=
|
|
884
|
+
"threshold": effective_threshold,
|
|
885
|
+
"pass": avg_score >= effective_threshold,
|
|
880
886
|
}
|
|
881
887
|
|
|
882
888
|
if json_output:
|
|
@@ -905,7 +911,7 @@ def lint(
|
|
|
905
911
|
console.print(
|
|
906
912
|
f"\n Score: avg {score_data['avg_score']}/100"
|
|
907
913
|
f" (min {score_data['min_score']}, max {score_data['max_score']})"
|
|
908
|
-
f" — threshold {
|
|
914
|
+
f" — threshold {effective_threshold} → {status}"
|
|
909
915
|
)
|
|
910
916
|
|
|
911
917
|
if copy:
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Prompt quality linting rules.
|
|
2
|
+
|
|
3
|
+
Checks prompts against configurable quality rules and returns violations.
|
|
4
|
+
Designed for CI integration — each rule produces a severity + message.
|
|
5
|
+
|
|
6
|
+
Configuration loaded from (highest priority wins):
|
|
7
|
+
1. CLI flags (--score-threshold, --strict)
|
|
8
|
+
2. .reprompt.toml in CWD or parents
|
|
9
|
+
3. [tool.reprompt.lint] in pyproject.toml
|
|
10
|
+
4. Built-in defaults
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
if sys.version_info >= (3, 11):
|
|
20
|
+
import tomllib
|
|
21
|
+
else:
|
|
22
|
+
try:
|
|
23
|
+
import tomllib
|
|
24
|
+
except ModuleNotFoundError: # Python 3.10
|
|
25
|
+
import tomli as tomllib # type: ignore[no-redefine]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LintViolation:
|
|
30
|
+
"""A single lint rule violation."""
|
|
31
|
+
|
|
32
|
+
rule: str
|
|
33
|
+
severity: str # "error" | "warning"
|
|
34
|
+
message: str
|
|
35
|
+
prompt_text: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class LintConfig:
|
|
40
|
+
"""Configuration for lint rules. Loaded from .reprompt.toml or pyproject.toml."""
|
|
41
|
+
|
|
42
|
+
# Rule thresholds (0 = disabled)
|
|
43
|
+
min_length: int = 20
|
|
44
|
+
short_prompt: int = 40
|
|
45
|
+
|
|
46
|
+
# Boolean rules (False = disabled)
|
|
47
|
+
vague_prompt: bool = True
|
|
48
|
+
debug_needs_reference: bool = True
|
|
49
|
+
|
|
50
|
+
# File extensions for debug-needs-reference
|
|
51
|
+
file_extensions: list[str] = field(
|
|
52
|
+
default_factory=lambda: [".py", ".ts", ".js", ".go", ".rs", ".java", ".rb", ".cpp", ".c"]
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# CI score threshold (0 = disabled, set via --score-threshold or config)
|
|
56
|
+
score_threshold: int = 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# --- Default config ---
|
|
60
|
+
DEFAULT_CONFIG = LintConfig()
|
|
61
|
+
|
|
62
|
+
VAGUE_STARTERS = frozenset(
|
|
63
|
+
{"fix it", "fix this", "do it", "help me", "change it", "update it", "make it work"}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_lint_config(start_dir: Path | None = None) -> LintConfig:
|
|
68
|
+
"""Load lint config from .reprompt.toml or pyproject.toml, walking up from start_dir.
|
|
69
|
+
|
|
70
|
+
Returns DEFAULT_CONFIG if no config file found.
|
|
71
|
+
"""
|
|
72
|
+
if start_dir is None:
|
|
73
|
+
start_dir = Path.cwd()
|
|
74
|
+
|
|
75
|
+
# Walk up to find config files
|
|
76
|
+
current = start_dir.resolve()
|
|
77
|
+
for _ in range(20): # safety limit
|
|
78
|
+
# Check .reprompt.toml first (project-specific)
|
|
79
|
+
reprompt_toml = current / ".reprompt.toml"
|
|
80
|
+
if reprompt_toml.is_file():
|
|
81
|
+
return _parse_reprompt_toml(reprompt_toml)
|
|
82
|
+
|
|
83
|
+
# Check pyproject.toml
|
|
84
|
+
pyproject = current / "pyproject.toml"
|
|
85
|
+
if pyproject.is_file():
|
|
86
|
+
config = _parse_pyproject_toml(pyproject)
|
|
87
|
+
if config is not None:
|
|
88
|
+
return config
|
|
89
|
+
|
|
90
|
+
parent = current.parent
|
|
91
|
+
if parent == current:
|
|
92
|
+
break
|
|
93
|
+
current = parent
|
|
94
|
+
|
|
95
|
+
return LintConfig()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _parse_reprompt_toml(path: Path) -> LintConfig:
|
|
99
|
+
"""Parse a .reprompt.toml file."""
|
|
100
|
+
try:
|
|
101
|
+
with open(path, "rb") as f:
|
|
102
|
+
data = tomllib.load(f)
|
|
103
|
+
return _build_config(data.get("lint", {}))
|
|
104
|
+
except Exception:
|
|
105
|
+
return LintConfig()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _parse_pyproject_toml(path: Path) -> LintConfig | None:
|
|
109
|
+
"""Parse [tool.reprompt.lint] from pyproject.toml. Returns None if section missing."""
|
|
110
|
+
try:
|
|
111
|
+
with open(path, "rb") as f:
|
|
112
|
+
data = tomllib.load(f)
|
|
113
|
+
lint_section = data.get("tool", {}).get("reprompt", {}).get("lint")
|
|
114
|
+
if lint_section is None:
|
|
115
|
+
return None
|
|
116
|
+
return _build_config(lint_section)
|
|
117
|
+
except Exception:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _build_config(lint_data: dict) -> LintConfig:
|
|
122
|
+
"""Build LintConfig from a parsed TOML dict."""
|
|
123
|
+
config = LintConfig()
|
|
124
|
+
|
|
125
|
+
# Top-level lint settings
|
|
126
|
+
if "score-threshold" in lint_data:
|
|
127
|
+
config.score_threshold = int(lint_data["score-threshold"])
|
|
128
|
+
|
|
129
|
+
# Rule settings
|
|
130
|
+
rules = lint_data.get("rules", {})
|
|
131
|
+
|
|
132
|
+
# min-length: int threshold or false
|
|
133
|
+
if "min-length" in rules:
|
|
134
|
+
val = rules["min-length"]
|
|
135
|
+
config.min_length = int(val) if val else 0
|
|
136
|
+
|
|
137
|
+
# short-prompt: int threshold or false
|
|
138
|
+
if "short-prompt" in rules:
|
|
139
|
+
val = rules["short-prompt"]
|
|
140
|
+
config.short_prompt = int(val) if val else 0
|
|
141
|
+
|
|
142
|
+
# vague-prompt: bool
|
|
143
|
+
if "vague-prompt" in rules:
|
|
144
|
+
config.vague_prompt = bool(rules["vague-prompt"])
|
|
145
|
+
|
|
146
|
+
# debug-needs-reference: bool
|
|
147
|
+
if "debug-needs-reference" in rules:
|
|
148
|
+
config.debug_needs_reference = bool(rules["debug-needs-reference"])
|
|
149
|
+
|
|
150
|
+
# file-extensions: list
|
|
151
|
+
if "file-extensions" in rules:
|
|
152
|
+
config.file_extensions = list(rules["file-extensions"])
|
|
153
|
+
|
|
154
|
+
return config
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def lint_prompt(text: str, config: LintConfig | None = None) -> list[LintViolation]:
|
|
158
|
+
"""Lint a single prompt against quality rules."""
|
|
159
|
+
if config is None:
|
|
160
|
+
config = DEFAULT_CONFIG
|
|
161
|
+
|
|
162
|
+
violations: list[LintViolation] = []
|
|
163
|
+
stripped = text.strip().lower()
|
|
164
|
+
|
|
165
|
+
# Rule 1: Too short
|
|
166
|
+
if config.min_length > 0 and len(stripped) < config.min_length:
|
|
167
|
+
violations.append(
|
|
168
|
+
LintViolation(
|
|
169
|
+
rule="min-length",
|
|
170
|
+
severity="error",
|
|
171
|
+
message=f"Prompt is too short ({len(stripped)} chars, minimum {config.min_length})",
|
|
172
|
+
prompt_text=text,
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
elif config.short_prompt > 0 and len(stripped) < config.short_prompt:
|
|
176
|
+
violations.append(
|
|
177
|
+
LintViolation(
|
|
178
|
+
rule="short-prompt",
|
|
179
|
+
severity="warning",
|
|
180
|
+
message=(f"Prompt is short ({len(stripped)} chars) — consider adding context"),
|
|
181
|
+
prompt_text=text,
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# Rule 2: Vague starter
|
|
186
|
+
if config.vague_prompt and stripped in VAGUE_STARTERS:
|
|
187
|
+
violations.append(
|
|
188
|
+
LintViolation(
|
|
189
|
+
rule="vague-prompt",
|
|
190
|
+
severity="error",
|
|
191
|
+
message="Prompt is too vague — specify what to fix/change and where",
|
|
192
|
+
prompt_text=text,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Rule 3: No file/function reference in debug prompts
|
|
197
|
+
if config.debug_needs_reference:
|
|
198
|
+
debug_words = {"fix", "debug", "bug", "error", "broken", "failing", "crash"}
|
|
199
|
+
has_debug_intent = any(w in stripped for w in debug_words)
|
|
200
|
+
indicators = list(config.file_extensions) + ["()", "line ", "file "]
|
|
201
|
+
has_reference = any(indicator in text for indicator in indicators)
|
|
202
|
+
min_len = config.min_length if config.min_length > 0 else 20
|
|
203
|
+
if has_debug_intent and not has_reference and len(stripped) >= min_len:
|
|
204
|
+
violations.append(
|
|
205
|
+
LintViolation(
|
|
206
|
+
rule="debug-needs-reference",
|
|
207
|
+
severity="warning",
|
|
208
|
+
message="Debug prompt lacks file/function reference — add specifics",
|
|
209
|
+
prompt_text=text,
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
return violations
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def lint_prompts(texts: list[str], config: LintConfig | None = None) -> list[LintViolation]:
|
|
217
|
+
"""Lint multiple prompts and return all violations."""
|
|
218
|
+
violations: list[LintViolation] = []
|
|
219
|
+
for text in texts:
|
|
220
|
+
violations.extend(lint_prompt(text, config))
|
|
221
|
+
return violations
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def format_lint_results(violations: list[LintViolation], total_prompts: int) -> str:
|
|
225
|
+
"""Format lint results for terminal/CI output."""
|
|
226
|
+
if not violations:
|
|
227
|
+
return f"✓ {total_prompts} prompts checked, no issues found"
|
|
228
|
+
|
|
229
|
+
errors = [v for v in violations if v.severity == "error"]
|
|
230
|
+
warnings = [v for v in violations if v.severity == "warning"]
|
|
231
|
+
|
|
232
|
+
lines: list[str] = []
|
|
233
|
+
lines.append(f"Checked {total_prompts} prompts\n")
|
|
234
|
+
|
|
235
|
+
for v in violations:
|
|
236
|
+
prefix = "✗" if v.severity == "error" else "!"
|
|
237
|
+
display = v.prompt_text[:60] + "..." if len(v.prompt_text) > 60 else v.prompt_text
|
|
238
|
+
lines.append(f' {prefix} [{v.rule}] "{display}"')
|
|
239
|
+
lines.append(f" {v.message}")
|
|
240
|
+
|
|
241
|
+
lines.append(f"\n{len(errors)} error(s), {len(warnings)} warning(s)")
|
|
242
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Tests for prompt linting rules and configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from reprompt.core.lint import (
|
|
8
|
+
LintConfig,
|
|
9
|
+
LintViolation,
|
|
10
|
+
format_lint_results,
|
|
11
|
+
lint_prompt,
|
|
12
|
+
lint_prompts,
|
|
13
|
+
load_lint_config,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestLintPrompt:
|
|
18
|
+
def test_good_prompt_no_violations(self):
|
|
19
|
+
violations = lint_prompt("fix the authentication bug in auth.py — login returns 401")
|
|
20
|
+
assert violations == []
|
|
21
|
+
|
|
22
|
+
def test_too_short_error(self):
|
|
23
|
+
violations = lint_prompt("fix bug")
|
|
24
|
+
assert len(violations) == 1
|
|
25
|
+
assert violations[0].rule == "min-length"
|
|
26
|
+
assert violations[0].severity == "error"
|
|
27
|
+
|
|
28
|
+
def test_short_warning(self):
|
|
29
|
+
violations = lint_prompt("fix the auth bug please")
|
|
30
|
+
rules = [v.rule for v in violations]
|
|
31
|
+
assert "short-prompt" in rules
|
|
32
|
+
|
|
33
|
+
def test_vague_prompt_error(self):
|
|
34
|
+
violations = lint_prompt("fix it")
|
|
35
|
+
rules = [v.rule for v in violations]
|
|
36
|
+
assert "vague-prompt" in rules
|
|
37
|
+
|
|
38
|
+
def test_debug_without_reference(self):
|
|
39
|
+
violations = lint_prompt("fix the authentication error in the login flow")
|
|
40
|
+
rules = [v.rule for v in violations]
|
|
41
|
+
assert "debug-needs-reference" in rules
|
|
42
|
+
|
|
43
|
+
def test_debug_with_reference_ok(self):
|
|
44
|
+
violations = lint_prompt("fix the authentication error in auth.py login flow")
|
|
45
|
+
rules = [v.rule for v in violations]
|
|
46
|
+
assert "debug-needs-reference" not in rules
|
|
47
|
+
|
|
48
|
+
def test_non_debug_no_reference_warning(self):
|
|
49
|
+
"""Non-debug prompts don't need file references."""
|
|
50
|
+
violations = lint_prompt("add pagination to the search results page")
|
|
51
|
+
rules = [v.rule for v in violations]
|
|
52
|
+
assert "debug-needs-reference" not in rules
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestLintPrompts:
|
|
56
|
+
def test_multiple_prompts(self):
|
|
57
|
+
violations = lint_prompts(
|
|
58
|
+
[
|
|
59
|
+
"fix the authentication bug in auth.py — login returns 401",
|
|
60
|
+
"fix it",
|
|
61
|
+
"add pagination to search results with cursor-based navigation",
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
assert len(violations) >= 1 # "fix it" triggers violations
|
|
65
|
+
|
|
66
|
+
def test_empty_list(self):
|
|
67
|
+
assert lint_prompts([]) == []
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class TestFormatResults:
|
|
71
|
+
def test_no_violations(self):
|
|
72
|
+
result = format_lint_results([], 10)
|
|
73
|
+
assert "no issues found" in result
|
|
74
|
+
assert "10" in result
|
|
75
|
+
|
|
76
|
+
def test_with_violations(self):
|
|
77
|
+
violations = lint_prompt("fix it")
|
|
78
|
+
result = format_lint_results(violations, 5)
|
|
79
|
+
assert "error" in result
|
|
80
|
+
assert "fix it" in result
|
|
81
|
+
|
|
82
|
+
def test_truncates_long_prompts(self):
|
|
83
|
+
long_prompt = "x" * 100
|
|
84
|
+
v = LintViolation(rule="test", severity="error", message="test", prompt_text=long_prompt)
|
|
85
|
+
result = format_lint_results([v], 1)
|
|
86
|
+
assert "..." in result
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# -- LintConfig tests --
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class TestLintConfig:
|
|
93
|
+
def test_default_config(self):
|
|
94
|
+
config = LintConfig()
|
|
95
|
+
assert config.min_length == 20
|
|
96
|
+
assert config.short_prompt == 40
|
|
97
|
+
assert config.vague_prompt is True
|
|
98
|
+
assert config.debug_needs_reference is True
|
|
99
|
+
assert config.score_threshold == 0
|
|
100
|
+
assert ".py" in config.file_extensions
|
|
101
|
+
|
|
102
|
+
def test_custom_min_length(self):
|
|
103
|
+
config = LintConfig(min_length=10)
|
|
104
|
+
# "fix bug" is 7 chars — under 10
|
|
105
|
+
violations = lint_prompt("fix bug", config=config)
|
|
106
|
+
assert any(v.rule == "min-length" for v in violations)
|
|
107
|
+
# "very short" is 10 chars — at threshold, should pass
|
|
108
|
+
violations = lint_prompt("0123456789", config=config)
|
|
109
|
+
assert not any(v.rule == "min-length" for v in violations)
|
|
110
|
+
|
|
111
|
+
def test_disabled_min_length(self):
|
|
112
|
+
config = LintConfig(min_length=0)
|
|
113
|
+
violations = lint_prompt("hi", config=config)
|
|
114
|
+
assert not any(v.rule == "min-length" for v in violations)
|
|
115
|
+
|
|
116
|
+
def test_disabled_short_prompt(self):
|
|
117
|
+
config = LintConfig(min_length=0, short_prompt=0)
|
|
118
|
+
violations = lint_prompt("fix the auth bug please", config=config)
|
|
119
|
+
assert not any(v.rule == "short-prompt" for v in violations)
|
|
120
|
+
|
|
121
|
+
def test_disabled_vague_prompt(self):
|
|
122
|
+
config = LintConfig(min_length=0, vague_prompt=False)
|
|
123
|
+
violations = lint_prompt("fix it", config=config)
|
|
124
|
+
assert not any(v.rule == "vague-prompt" for v in violations)
|
|
125
|
+
|
|
126
|
+
def test_disabled_debug_needs_reference(self):
|
|
127
|
+
config = LintConfig(debug_needs_reference=False)
|
|
128
|
+
violations = lint_prompt("fix the authentication error in the login flow", config=config)
|
|
129
|
+
assert not any(v.rule == "debug-needs-reference" for v in violations)
|
|
130
|
+
|
|
131
|
+
def test_custom_file_extensions(self):
|
|
132
|
+
config = LintConfig(file_extensions=[".py", ".ts"])
|
|
133
|
+
# .go is not in extensions, so no reference detected
|
|
134
|
+
violations = lint_prompt("fix the authentication error in auth.go flow", config=config)
|
|
135
|
+
assert any(v.rule == "debug-needs-reference" for v in violations)
|
|
136
|
+
# .py IS in extensions
|
|
137
|
+
violations = lint_prompt("fix the authentication error in auth.py flow", config=config)
|
|
138
|
+
assert not any(v.rule == "debug-needs-reference" for v in violations)
|
|
139
|
+
|
|
140
|
+
def test_custom_short_threshold(self):
|
|
141
|
+
config = LintConfig(short_prompt=60)
|
|
142
|
+
violations = lint_prompt("add pagination to the search results page")
|
|
143
|
+
rules_default = [v.rule for v in violations]
|
|
144
|
+
assert "short-prompt" not in rules_default # 42 chars, under 40 default = no warning
|
|
145
|
+
|
|
146
|
+
violations = lint_prompt("add pagination to the search results page", config=config)
|
|
147
|
+
rules_custom = [v.rule for v in violations]
|
|
148
|
+
assert "short-prompt" in rules_custom # 42 chars, under 60 threshold
|
|
149
|
+
|
|
150
|
+
def test_lint_prompts_with_config(self):
|
|
151
|
+
config = LintConfig(min_length=0, short_prompt=0, vague_prompt=False)
|
|
152
|
+
violations = lint_prompts(["fix it", "hi", "ok"], config=config)
|
|
153
|
+
# With all rules disabled, everything passes
|
|
154
|
+
assert violations == []
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# -- Config file loading tests --
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestLoadLintConfig:
|
|
161
|
+
def test_no_config_returns_defaults(self, tmp_path: Path):
|
|
162
|
+
config = load_lint_config(start_dir=tmp_path)
|
|
163
|
+
assert config.min_length == 20
|
|
164
|
+
assert config.short_prompt == 40
|
|
165
|
+
assert config.score_threshold == 0
|
|
166
|
+
|
|
167
|
+
def test_reprompt_toml(self, tmp_path: Path):
|
|
168
|
+
toml_file = tmp_path / ".reprompt.toml"
|
|
169
|
+
toml_file.write_text(
|
|
170
|
+
"[lint]\nscore-threshold = 60\n\n[lint.rules]\nmin-length = 10\nvague-prompt = false\n"
|
|
171
|
+
)
|
|
172
|
+
config = load_lint_config(start_dir=tmp_path)
|
|
173
|
+
assert config.score_threshold == 60
|
|
174
|
+
assert config.min_length == 10
|
|
175
|
+
assert config.vague_prompt is False
|
|
176
|
+
# Unset rules keep defaults
|
|
177
|
+
assert config.short_prompt == 40
|
|
178
|
+
assert config.debug_needs_reference is True
|
|
179
|
+
|
|
180
|
+
def test_pyproject_toml(self, tmp_path: Path):
|
|
181
|
+
pyproject = tmp_path / "pyproject.toml"
|
|
182
|
+
pyproject.write_text(
|
|
183
|
+
"[tool.reprompt.lint]\n"
|
|
184
|
+
"score-threshold = 45\n\n"
|
|
185
|
+
"[tool.reprompt.lint.rules]\n"
|
|
186
|
+
"short-prompt = 0\n"
|
|
187
|
+
"debug-needs-reference = false\n"
|
|
188
|
+
)
|
|
189
|
+
config = load_lint_config(start_dir=tmp_path)
|
|
190
|
+
assert config.score_threshold == 45
|
|
191
|
+
assert config.short_prompt == 0
|
|
192
|
+
assert config.debug_needs_reference is False
|
|
193
|
+
|
|
194
|
+
def test_reprompt_toml_takes_precedence(self, tmp_path: Path):
|
|
195
|
+
"""When both exist, .reprompt.toml wins."""
|
|
196
|
+
(tmp_path / ".reprompt.toml").write_text("[lint]\nscore-threshold = 70\n")
|
|
197
|
+
(tmp_path / "pyproject.toml").write_text("[tool.reprompt.lint]\nscore-threshold = 30\n")
|
|
198
|
+
config = load_lint_config(start_dir=tmp_path)
|
|
199
|
+
assert config.score_threshold == 70
|
|
200
|
+
|
|
201
|
+
def test_walks_up_directories(self, tmp_path: Path):
|
|
202
|
+
"""Config in parent dir should be found."""
|
|
203
|
+
(tmp_path / ".reprompt.toml").write_text("[lint]\nscore-threshold = 55\n")
|
|
204
|
+
subdir = tmp_path / "src" / "module"
|
|
205
|
+
subdir.mkdir(parents=True)
|
|
206
|
+
config = load_lint_config(start_dir=subdir)
|
|
207
|
+
assert config.score_threshold == 55
|
|
208
|
+
|
|
209
|
+
def test_pyproject_without_reprompt_section(self, tmp_path: Path):
|
|
210
|
+
"""pyproject.toml without [tool.reprompt] should return defaults."""
|
|
211
|
+
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'foo'\n")
|
|
212
|
+
config = load_lint_config(start_dir=tmp_path)
|
|
213
|
+
assert config.min_length == 20 # default
|
|
214
|
+
|
|
215
|
+
def test_invalid_toml_returns_defaults(self, tmp_path: Path):
|
|
216
|
+
(tmp_path / ".reprompt.toml").write_text("this is not valid toml {{{{")
|
|
217
|
+
config = load_lint_config(start_dir=tmp_path)
|
|
218
|
+
assert config.min_length == 20 # graceful fallback
|
|
219
|
+
|
|
220
|
+
def test_file_extensions_config(self, tmp_path: Path):
|
|
221
|
+
(tmp_path / ".reprompt.toml").write_text(
|
|
222
|
+
'[lint.rules]\nfile-extensions = [".py", ".ts", ".vue"]\n'
|
|
223
|
+
)
|
|
224
|
+
config = load_lint_config(start_dir=tmp_path)
|
|
225
|
+
assert config.file_extensions == [".py", ".ts", ".vue"]
|
|
226
|
+
|
|
227
|
+
def test_disable_all_rules(self, tmp_path: Path):
|
|
228
|
+
(tmp_path / ".reprompt.toml").write_text(
|
|
229
|
+
"[lint.rules]\n"
|
|
230
|
+
"min-length = 0\n"
|
|
231
|
+
"short-prompt = 0\n"
|
|
232
|
+
"vague-prompt = false\n"
|
|
233
|
+
"debug-needs-reference = false\n"
|
|
234
|
+
)
|
|
235
|
+
config = load_lint_config(start_dir=tmp_path)
|
|
236
|
+
violations = lint_prompts(["fix it", "hi", "ok"], config=config)
|
|
237
|
+
assert violations == []
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
"""Prompt quality linting rules.
|
|
2
|
-
|
|
3
|
-
Checks prompts against configurable quality rules and returns violations.
|
|
4
|
-
Designed for CI integration — each rule produces a severity + message.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
from dataclasses import dataclass
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@dataclass
|
|
13
|
-
class LintViolation:
|
|
14
|
-
"""A single lint rule violation."""
|
|
15
|
-
|
|
16
|
-
rule: str
|
|
17
|
-
severity: str # "error" | "warning"
|
|
18
|
-
message: str
|
|
19
|
-
prompt_text: str
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
# --- Default thresholds ---
|
|
23
|
-
MIN_LENGTH = 20
|
|
24
|
-
SHORT_WARNING_LENGTH = 40
|
|
25
|
-
VAGUE_STARTERS = frozenset(
|
|
26
|
-
{"fix it", "fix this", "do it", "help me", "change it", "update it", "make it work"}
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def lint_prompt(text: str) -> list[LintViolation]:
|
|
31
|
-
"""Lint a single prompt against quality rules."""
|
|
32
|
-
violations: list[LintViolation] = []
|
|
33
|
-
stripped = text.strip().lower()
|
|
34
|
-
|
|
35
|
-
# Rule 1: Too short
|
|
36
|
-
if len(stripped) < MIN_LENGTH:
|
|
37
|
-
violations.append(
|
|
38
|
-
LintViolation(
|
|
39
|
-
rule="min-length",
|
|
40
|
-
severity="error",
|
|
41
|
-
message=f"Prompt is too short ({len(stripped)} chars, minimum {MIN_LENGTH})",
|
|
42
|
-
prompt_text=text,
|
|
43
|
-
)
|
|
44
|
-
)
|
|
45
|
-
elif len(stripped) < SHORT_WARNING_LENGTH:
|
|
46
|
-
violations.append(
|
|
47
|
-
LintViolation(
|
|
48
|
-
rule="short-prompt",
|
|
49
|
-
severity="warning",
|
|
50
|
-
message=f"Prompt is short ({len(stripped)} chars) — consider adding context",
|
|
51
|
-
prompt_text=text,
|
|
52
|
-
)
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
# Rule 2: Vague starter
|
|
56
|
-
if stripped in VAGUE_STARTERS:
|
|
57
|
-
violations.append(
|
|
58
|
-
LintViolation(
|
|
59
|
-
rule="vague-prompt",
|
|
60
|
-
severity="error",
|
|
61
|
-
message="Prompt is too vague — specify what to fix/change and where",
|
|
62
|
-
prompt_text=text,
|
|
63
|
-
)
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
# Rule 3: No file/function reference in debug prompts
|
|
67
|
-
debug_words = {"fix", "debug", "bug", "error", "broken", "failing", "crash"}
|
|
68
|
-
has_debug_intent = any(w in stripped for w in debug_words)
|
|
69
|
-
has_reference = any(
|
|
70
|
-
indicator in text
|
|
71
|
-
for indicator in (".py", ".ts", ".js", ".go", ".rs", "()", "line ", "file ")
|
|
72
|
-
)
|
|
73
|
-
if has_debug_intent and not has_reference and len(stripped) >= MIN_LENGTH:
|
|
74
|
-
violations.append(
|
|
75
|
-
LintViolation(
|
|
76
|
-
rule="debug-needs-reference",
|
|
77
|
-
severity="warning",
|
|
78
|
-
message="Debug prompt lacks file/function reference — add specifics",
|
|
79
|
-
prompt_text=text,
|
|
80
|
-
)
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
return violations
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def lint_prompts(texts: list[str]) -> list[LintViolation]:
|
|
87
|
-
"""Lint multiple prompts and return all violations."""
|
|
88
|
-
violations: list[LintViolation] = []
|
|
89
|
-
for text in texts:
|
|
90
|
-
violations.extend(lint_prompt(text))
|
|
91
|
-
return violations
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def format_lint_results(violations: list[LintViolation], total_prompts: int) -> str:
|
|
95
|
-
"""Format lint results for terminal/CI output."""
|
|
96
|
-
if not violations:
|
|
97
|
-
return f"✓ {total_prompts} prompts checked, no issues found"
|
|
98
|
-
|
|
99
|
-
errors = [v for v in violations if v.severity == "error"]
|
|
100
|
-
warnings = [v for v in violations if v.severity == "warning"]
|
|
101
|
-
|
|
102
|
-
lines: list[str] = []
|
|
103
|
-
lines.append(f"Checked {total_prompts} prompts\n")
|
|
104
|
-
|
|
105
|
-
for v in violations:
|
|
106
|
-
prefix = "✗" if v.severity == "error" else "!"
|
|
107
|
-
display = v.prompt_text[:60] + "..." if len(v.prompt_text) > 60 else v.prompt_text
|
|
108
|
-
lines.append(f' {prefix} [{v.rule}] "{display}"')
|
|
109
|
-
lines.append(f" {v.message}")
|
|
110
|
-
|
|
111
|
-
lines.append(f"\n{len(errors)} error(s), {len(warnings)} warning(s)")
|
|
112
|
-
return "\n".join(lines)
|