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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [Unreleased]
### Added
- Multilingual degradation: a zero-dependency script probe
(`unicodedata`-based histogram, `inputguard/language.py`) classifies
each input's script before rules run. Scripts without English
heuristic coverage take an explicit degraded path — rules are skipped,
a 20-point confidence penalty applies, and the result carries
`detected_language`, `heuristic_coverage`, and `degradation_note`
instead of silently scoring 100/ready.
- Additive `AnalysisResult` fields: `detected_language`,
`heuristic_coverage`, `degradation_note` (included in `to_dict()`).
- Mixed-script input: rules run whenever the covered share of letters is
at least 50%, with a `partial` note between 50–70% coverage.

## [0.2.0] — 2026-05-29
### Added
- Auto intent detection. `.analyze()` now detects whether the input
Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,35 @@ Use `warning` when you want to surface gaps to the user without blocking. Use `s

---

## Non-English input

InputGuard's rules are English-language heuristics. Before any rule runs, a zero-dependency script probe (stdlib `unicodedata` only) classifies the input's script. When the script has no heuristic coverage, InputGuard says so instead of pretending:

```python
result = guard.analyze("建造一个用户登录应用")

result.status # 'usable_with_warnings' — never 'ready'
result.clarity_score # 80 (100 minus the degradation penalty)
result.detected_intent # 'undetermined'
result.detected_language # 'zh' (coarse, script-derived guess)
result.heuristic_coverage # 'none'
result.degradation_note # explains that rules were skipped and why
```

The rules are **skipped explicitly** on uncovered scripts — running English keyword rules on text they cannot read would produce a silent, unearned verdict. A degraded result is never `ready` in either mode (strict mode returns `needs_clarification`). In v0.2 this input silently scored 100/ready; v0.3 refuses to assert a confidence it does not have.

Three additive fields on the result carry the probe's verdict:

| Field | Values |
|---|---|
| `detected_language` | coarse script-derived guess (`'en'`, `'zh'`, `'ja'`, `'ko'`, `'ru'`, `'ar'`, ...; `'und'` when unclassifiable) |
| `heuristic_coverage` | `'full'` (≥ 70% of letters covered — rules run exactly as before), `'partial'` (50–70% — rules run, note flags the uncovered remainder), `'none'` (degraded path), `'unknown'` (no letters to classify) |
| `degradation_note` | `None`, or an explanation of what was skipped and why |

Mixed input is handled by share, not by exclusion: `"make it faster 这个"` is still fully analyzed (English dominates and the rules run); input whose letters fall 50–70% inside coverage gets a `partial` note without a penalty.

---

## How intent detection works

InputGuard automatically detects what kind of coding input it is receiving. No extra parameters needed. The same `.analyze()` call handles all five intent types.
Expand Down
41 changes: 40 additions & 1 deletion inputguard/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,15 @@
import inputguard.rules # noqa: F401 — importing registers the coding domain and the 19 built-in rules
from inputguard.detector import detect_intent, normalize
from inputguard.followups import get_follow_ups
from inputguard.language import (
COVERAGE_NONE,
COVERAGE_PARTIAL,
DEGRADATION_PENALTY,
DEGRADED_INTENT,
degradation_note_for,
partial_coverage_note,
probe_script,
)
from inputguard.recommender import get_recommendations
from inputguard.registry import REGISTRY
from inputguard.scorer import calculate_score, get_status
Expand Down Expand Up @@ -36,9 +45,32 @@ def analyze(self, user_input: str, domain: str = "coding") -> AnalysisResult:
raise ValueError("user_input must be a non-empty, non-whitespace string.")

domain_signals = REGISTRY.get_domain_signals(domain)
normalized = normalize(user_input)

# Language probe (pipeline step 2): classify the input's script before
# any rule runs, so scripts outside heuristic coverage take the
# explicit degradation path instead of silently passing as ready.
probe = probe_script(normalized)
if probe.heuristic_coverage == COVERAGE_NONE:
# English-only rules are skipped outright — running them on a
# script they cannot read would produce a silent, unearned
# ready. The penalty keeps the result out of "ready" in both
# modes; the note says honestly what the tool does not know.
score = max(0, 100 - DEGRADATION_PENALTY)
return AnalysisResult(
status=get_status(score, self.mode),
clarity_score=score,
detected_intent=DEGRADED_INTENT,
gaps=[],
recommendations=[],
findings=[],
interpretation_note=None,
detected_language=probe.detected_language,
heuristic_coverage=probe.heuristic_coverage,
degradation_note=degradation_note_for(probe),
)

detected_intent = detect_intent(user_input, domain_signals)
normalized = normalize(user_input)

findings: List[RuleFinding] = []
seen_codes: Set[str] = set()
Expand Down Expand Up @@ -78,4 +110,11 @@ def analyze(self, user_input: str, domain: str = "coding") -> AnalysisResult:
findings=findings,
interpretation_note=interpretation_note,
follow_ups=follow_ups,
detected_language=probe.detected_language,
heuristic_coverage=probe.heuristic_coverage,
degradation_note=(
partial_coverage_note(probe)
if probe.heuristic_coverage == COVERAGE_PARTIAL
else None
),
)
Loading