Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ jobs:
- name: Layer 2 - Output contracts
run: python -m pytest tests/test_output_contracts.py -v -q

- name: Snapshot tests
run: python -m pytest tests/test_snapshots.py -v -q

- name: State transition tests
run: python -m pytest tests/test_state.py -v -q

- name: Layer 5 - Schema validation
run: python -m pytest tests/test_schema_validation.py -v -q

Expand Down Expand Up @@ -84,13 +90,11 @@ jobs:
- name: Output contracts
run: python -m pytest tests/test_output_contracts.py -v -q

- name: Schema validation
run: python -m pytest tests/test_schema_validation.py -v -q
- name: Snapshots
run: python -m pytest tests/test_snapshots.py -v -q

- name: Dogfood
run: |
quale review --path . || echo "review OK"
quale onboard --path . || echo "onboard OK"
quale agent orient --path . || echo "orient OK"
quale core hub-risk --path . || echo "hub-risk OK"
quale core test-gaps --path . || echo "test-gaps OK"
- name: State
run: python -m pytest tests/test_state.py -v -q

- name: Structure
run: python -m pytest tests/test_structure.py -v -q
11 changes: 11 additions & 0 deletions .quale/ci-history.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,14 @@
{"timestamp": 1779920285.4544563, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 1, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 1, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": ".github/workflows/ci.yml", "clone_group": ["quale/formats/llm.py", "tests/test_reports.py"], "similarity": 0.182}], "new_identifier_count": 0}
{"timestamp": 1779922301.9146924, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 1, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 1, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "README.md", "clone_group": ["scripts/agent-init.sh"], "similarity": 0.25}], "new_identifier_count": 2}
{"timestamp": 1779924648.8677866, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 3, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 3, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "pyproject.toml", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.4}, {"file": ".quale/ci-history.jsonl", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.2}], "new_identifier_count": 0}
{"timestamp": 1779958624.1635828, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 7, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 7, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.4}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.2}, {"file": "docs/MCP_SETUP.md", "clone_group": ["tests/test_cli_smoke.py", "tests/test_performance.py"], "similarity": 0.182}], "new_identifier_count": 7}
{"timestamp": 1779958770.045349, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 7, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 7, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "SKILL.md", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.4}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.2}, {"file": "docs/MCP_SETUP.md", "clone_group": ["tests/test_cli_smoke.py", "tests/test_performance.py"], "similarity": 0.182}], "new_identifier_count": 7}
{"timestamp": 1779961493.6561697, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 2, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 2, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "docs/EFFECT_HARNESS.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.2}], "new_identifier_count": 12}
{"timestamp": 1779961958.9824195, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 2, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 2, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "docs/EFFECT_HARNESS.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.2}], "new_identifier_count": 12}
{"timestamp": 1779962163.4975307, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 2, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 2, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "docs/EFFECT_HARNESS.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.2}], "new_identifier_count": 12}
{"timestamp": 1779963009.5069184, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 3, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 3, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "pyproject.toml", "clone_group": [".editorconfig", ".github/ISSUE_TEMPLATE/01-bug-report.md", ".github/ISSUE_TEMPLATE/02-feature-request.md"], "similarity": 0.333}, {"file": "CHANGELOG.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.167}], "new_identifier_count": 2}
{"timestamp": 1779965453.5800962, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 2, "blast_radius_count": 23, "mirror_gap_ratio": 0.158, "stable_touched_count": 2, "max_blast_tier": "critical", "hub_risk_flagged": [{"file": "quale/cli.py", "hub_rank": 1}], "clone_flagged": [], "new_identifier_count": 2}
{"timestamp": 1779967141.3770032, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 8, "blast_radius_count": 15, "mirror_gap_ratio": 0.154, "stable_touched_count": 8, "max_blast_tier": "high", "hub_risk_flagged": [{"file": "scripts/evaluate_quale_effect.py", "hub_rank": 2}], "clone_flagged": [{"file": "docs/EFFECT_HARNESS.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "tests/helpers.py", "clone_group": ["scripts/quale-init.sh"], "similarity": 0.417}], "new_identifier_count": 10}
{"timestamp": 1779967473.130022, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 8, "blast_radius_count": 15, "mirror_gap_ratio": 0.154, "stable_touched_count": 8, "max_blast_tier": "high", "hub_risk_flagged": [{"file": "scripts/evaluate_quale_effect.py", "hub_rank": 2}], "clone_flagged": [{"file": "docs/EFFECT_HARNESS.md", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": "tests/helpers.py", "clone_group": ["scripts/quale-init.sh"], "similarity": 0.417}], "new_identifier_count": 10}
{"timestamp": 1779969220.6256375, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 3, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 3, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "scripts/pre-commit.sh", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": ".github/workflows/ci.yml", "clone_group": ["quale/formats/llm.py"], "similarity": 0.538}], "new_identifier_count": 7}
{"timestamp": 1779970030.2043636, "base_ref": "HEAD~1", "head_ref": "HEAD", "changed_files": 3, "blast_radius_count": 0, "mirror_gap_ratio": 0.0, "stable_touched_count": 3, "max_blast_tier": "none", "hub_risk_flagged": [], "clone_flagged": [{"file": "scripts/pre-commit.sh", "clone_group": [".github/ISSUE_TEMPLATE/config.yml", ".github/PULL_REQUEST_TEMPLATE.md", ".github/workflows/stale.yml"], "similarity": 0.083}, {"file": ".github/workflows/ci.yml", "clone_group": ["quale/formats/llm.py"], "similarity": 0.538}], "new_identifier_count": 7}
76 changes: 58 additions & 18 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,73 @@ cd quale
pip install -e ".[dev]"
```

## Merge strategy

Master is branch-protected. All changes go through feature branches + PRs.

### Branch naming

| Prefix | Purpose | Example |
|--------|---------|---------|
| `fix/` | Bug fixes | `fix/crash-on-empty-repo` |
| `feature/` | New features | `feature/mcp-server` |
| `docs/` | Documentation | `docs/readme-polish` |
| `chore/` | CI, config, tooling | `chore/update-deps` |

### Workflow

1. Branch off `master`: `git checkout -b fix/my-bug`
2. Make changes, commit with descriptive messages
3. Push: `git push -u origin fix/my-bug`
4. Open a PR against `master` via `gh pr create` or GitHub UI
5. CI checks (`test`, `guardrails`, `lint`, `security`) must pass
6. Merge via **squash** — one clean commit per PR

### Updating snapshots

If your change intentionally alters output, update golden files before merging:

```bash
UPDATE_SNAPSHOTS=1 python -m pytest tests/test_snapshots.py -v
git add tests/snapshots/
```

### Stale branches

After merging, clean up:

```bash
git branch -d fix/my-bug
git push origin --delete fix/my-bug
```

## Running tests

```bash
# Full suite
python -m pytest tests/ -v

# Specific test files
python -m pytest tests/test_cli_smoke.py -v
python -m pytest tests/test_output_contracts.py -v
# By layer
python -m pytest tests/test_cli_smoke.py -v # Smoke (all commands exit 0)
python -m pytest tests/test_output_contracts.py -v # Output quality contracts
python -m pytest tests/test_commands.py -v # CLI integration
python -m pytest tests/test_reports.py -v # Unit tests
python -m pytest tests/test_snapshots.py -v # Snapshot regression
python -m pytest tests/test_state.py -v # State transition
python -m pytest tests/test_structure.py -v # Structural guardrails

# Update snapshots when output intentionally changes
UPDATE_SNAPSHOTS=1 python -m pytest tests/test_snapshots.py -v
```

Our CI runs these classes of tests:
## CI gate matrix

| Layer | File | What it checks |
|-------|------|---------------|
| 1 | `test_cli_smoke.py` | Every command exits 0 and produces non-empty output |
| 2 | `test_output_contracts.py` | Output is useful, not just technical jargon |
| 5 | `test_schema_validation.py` | Agent JSON output matches schema |
| Job | Files | Required for merge | What it catches |
|-----|-------|--------------------|-----------------|
| `test` | Core + install + reports | ✓ | Regression bugs |
| `guardrails` | Smoke, contracts, snapshots, state, structure, dogfood | ✓ | Crashes, UX regressions, drift |
| `lint` | `ruff check quale/` | ✓ | Code style violations |
| `security` | bandit, semgrep, pip-audit, mypy | ✓ | Vulnerabilities, type errors |

## Code style

Expand All @@ -36,15 +85,6 @@ Our CI runs these classes of tests:
- Run `codespell` for typos
- All tests must pass before merging

## Pull request process

1. Create a feature branch off `master`
2. Make your changes
3. Run tests: `python -m pytest tests/ -q`
4. Run lint: `ruff check quale/`
5. Update `CHANGELOG.md`
6. Open a PR with a clear description

## Reporting issues

Use the issue templates: bug reports, feature requests, or command-specific
Expand Down
14 changes: 7 additions & 7 deletions docs/EFFECT_HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ Best for weak models: `verify_scope` (verification-only, removes edit decision).
## Using the harness

```bash
python scripts/evaluate_vocab_effect.py --dry-run --max-cases 2
python scripts/evaluate_vocab_effect.py --suite edit-context --trials 3
python scripts/evaluate_quale_effect.py --dry-run --max-cases 2
python scripts/evaluate_quale_effect.py --suite edit-context --trials 3
python scripts/analyze_effect_failures.py /tmp/quale-effect-edit-context-3trial.json
```

Expand All @@ -63,9 +63,9 @@ The harness tested several conditions. Here's how they map to current CLI comman
|-------------------|-------------|--------|-------|
| `edit-context --format tool` | `quale ec` or `quale core edit-context --format tool` | ✓ Active | Primary LLM surface, 75% accuracy |
| `verify_scope` | `quale core verify-scope` | ✓ Active | Verification-only, 83% accuracy on weak models |
| `verify_entangle` | `quale core verify-entangle` | ✓ Active | Includes git co-change signal, best all-round |
| `progressive_verify` | `quale core verify-progressive` | ✓ Active | Multi-step verification |
| `diff_edit-context` | `quale core diff-context` | ✓ Active | For PR/diff workflows, 100% accuracy |
| `verify_entangle` | `quale vp` (verify-packet has `entangled_candidates`) | ✓ Via JSON field | Closest match includes co-change signal |
| `progressive_verify` | `quale core veto-cascade` | ✓ Via veto-cascade | Closest match: multi-step deterministic → oscillatory → manual |
| `diff_edit-context` | `quale ec --diff <ref>` | ✓ Via --diff flag | Same engine, use --diff instead of --files |
| `candidate_baseline` | N/A | Baseline only | No-quale control condition |
| `route_policy` | N/A | Internal | Routing logic, not user-facing |
| `ask` | N/A | ✗ Killed | 0% accuracy, worse than baseline |
Expand All @@ -75,8 +75,8 @@ The harness tested several conditions. Here's how they map to current CLI comman
**Recommended commands for users:**
- **LLM agents**: Use `quale ec` (edit context) before every edit
- **Weak models**: Use `quale core verify-scope` for highest accuracy (83%)
- **PR reviews**: Use `quale core diff-context` for diff-aware context (100% accuracy)
- **General use**: Use `quale core verify-entangle` for best all-round performance
- **PR reviews**: Use `quale ec --diff <ref>` for diff-aware context
- **General use**: Use `quale vp` (verify-packet) for co-change signal

Decision rules:
- keep `edit-context --format tool` as primary LLM surface: 75% accuracy, 0 extra edits
Expand Down
9 changes: 2 additions & 7 deletions quale/mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,7 @@ def run(self):
self._respond(req_id, result={"content": [{"type": "text", "text": json.dumps(result)}]})
except Exception as e:
self._respond(req_id, error=str(e))
elif method == "notifications/initialized":
pass
elif method == "notifications/cancelled":
pass
elif method == "initialized":
elif method == "notifications/initialized" or method == "notifications/cancelled" or method == "initialized":
pass
else:
self._respond(req_id, error=f"Unknown method: {method}")
Expand Down Expand Up @@ -131,8 +127,7 @@ def _handle_edit_context(self, args):
if c in data.get("changed_files", []):
vtypes[c] = "source"
for c in verify_candidates[:5] if verify_candidates else []:
base = os.path.splitext(c)[0]
if any(not d.get(c) == "source" for d in [vtypes]):
if any(d.get(c) != "source" for d in [vtypes]):
vtypes[c] = "unit" if "_test" in c or ".test." in c else "integration"
return {
"schema_version": 1,
Expand Down
31 changes: 6 additions & 25 deletions quale/reports/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@
_change_acceleration,
_cross_cutting_concerns,
_deficit_analysis,
_file_in_commit,
_file_temperature,
_fused_priority_ranking,
_module_exposure_analysis,
_peer_relative_risk,
_risk_vector,
_safety_envelope,
_safe_islands_data,
_spectrum_analysis,
)
from quale.reports.analysis import (
_file_in_commit as _file_in_commit,
)
from quale.reports.analysis import (
_safe_islands_data as _safe_islands_data,
)

if TYPE_CHECKING:
pass
Expand Down Expand Up @@ -380,27 +384,6 @@ def onboard_plan(path: str = ".") -> dict:
"total_files": analysis.total_files,
}

def _safe_islands_data(analysis) -> list[str]:
"""Find structurally isolated blocks safe to edit."""
safe: list[str] = []
dir_counts: dict[str, int] = {}
for fv in analysis.file_vocabs:
d = os.path.dirname(fv.path) or "."
dir_counts.setdefault(d, 0)
dir_counts[d] += len(fv.vocabulary)
dir_files: dict[str, int] = {}
for fv in analysis.file_vocabs:
d = os.path.dirname(fv.path) or "."
dir_files[d] = dir_files.get(d, 0) + 1
avg_phrases = sum(dir_counts.values()) / max(len(dir_counts), 1)
for d, count in sorted(dir_counts.items(), key=lambda x: -x[1]):
parts = d.split(os.path.sep)
if any(p.startswith(".") for p in parts if p):
continue
if count < avg_phrases * 0.3 and dir_files.get(d, 0) <= 3:
safe.append(d)
return sorted(safe)[:10]

def refactor_effort(path: str = ".", file_path: str = "") -> dict:
"""Estimate refactoring effort for a file: blast + escape + clones + hub."""
if not vgit.is_repo(path):
Expand Down Expand Up @@ -570,8 +553,6 @@ def _count_new_identifiers(path: str, base_ref: str, head_ref: str) -> int:
new_ids = head_ids - base_ids
return len(new_ids)

return concerns[:5]

def preflight_report(path: str = ".", files: list[str] | None = None,
diff_ref: str | None = None, task: str | None = None,
enrich: bool = False) -> dict:
Expand Down
1 change: 0 additions & 1 deletion quale/reports/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import os
from collections import Counter
from typing import Any

from quale import git as vgit

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion scripts/pre-commit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
set -euo pipefail

STAGED_CLI=$(git diff --cached --name-only | grep -c "quale/cli\.py" || true)
STAGED_REPORTS=$(git diff --cached --name-only | grep -c "quale/reports\.py" || true)
STAGED_REPORTS=$(git diff --cached --name-only | grep -c "quale/reports/.*\.py" || true)
STAGED_TESTS=$(git diff --cached --name-only | grep -c "tests/" || true)

if [ "$STAGED_CLI" -eq 0 ] && [ "$STAGED_REPORTS" -eq 0 ] && [ "$STAGED_TESTS" -eq 0 ]; then
Expand Down
Loading
Loading