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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Turkish quality corpus fixtures under `tests/Fixtures/tr/` (60 cases: 30 clean, 10 profane, 10 obfuscated, 10 edge)
- `CorpusLoader` and `CorpusQualityReport` support classes for fixture validation and quality metrics
- `tests/TurkishCorpusQualityTest.php` with aggregate FP/FN/coverage gate
- `docs/dictionary-expansion-policy.md` — criteria for future Turkish dictionary entry PRs

### Changed

- `CONTRIBUTING.md` — corpus authoring rules and dictionary expansion policy link

## [0.2.0] - 2026-07-03

Dictionary infrastructure release — author-only dictionary rows with build-time normalized key generation.
Expand Down
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ ci: add PHP 8.3 to test matrix
- False-positive regression tests are as important as true-positive tests
- Prefer simple, readable code over abstractions

## Turkish Dictionary Quality Corpus

VerbaGuard maintains a fixture-based Turkish quality corpus under `tests/Fixtures/tr/`.

- **Clean corpus** — false-positive candidates (`expect.detected: false`)
- **Profane / obfuscated corpus** — detection cases (`expect.detected: true`)
- **Edge cases** — boundary behavior with explicit expectations

Quality gate (CI): false positives = 0, false negatives = 0, detection coverage = 100%.

### Adding corpus cases

1. Use stable `id` values (`category.slug`) unique across all fixture files.
2. Required fields: `id`, `text`, `expect.detected`.
3. Set `expect.terms` to seed dictionary terms only when asserting canonical matches.
4. Empty `text` is allowed only in `edge_cases.php`.
5. Run `composer test` — `TurkishCorpusQualityTest` must pass.

### Dictionary expansion PRs

Before adding Turkish dictionary entries, read [docs/dictionary-expansion-policy.md](docs/dictionary-expansion-policy.md).

New TR entry PRs **must** include:

- Clean neighbor cases (false-positive documentation)
- Profane and obfuscated positive cases
- A risk assessment in the PR description
- Passing aggregate corpus gate (FP = 0)

Do not add dictionary entries without corresponding corpus coverage.

## Reporting Bugs

Use the [bug report issue template](.github/ISSUE_TEMPLATE/bug_report.yml). Include:
Expand Down
120 changes: 120 additions & 0 deletions docs/dictionary-expansion-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Turkish Dictionary Expansion Policy

This document defines the quality gate for adding or changing Turkish dictionary entries in VerbaGuard.

For corpus format and test infrastructure, see [`tests/Fixtures/tr/README.md`](../tests/Fixtures/tr/README.md).

---

## Principles

1. **False positives are worse than false negatives** — see [`FOUNDATION.md`](../FOUNDATION.md).
2. **Corpus before expansion** — measurable quality ground must exist before growing the dictionary.
3. **Short terms need extra care** — tokens of three characters or fewer have higher false-positive risk in real text.

---

## Pre-merge checklist for new TR entries

Before merging a pull request that adds or changes Turkish dictionary rows:

1. Add **clean neighbor cases** documenting false-positive risk.
2. Add **profane detection cases** for the new term (direct usage).
3. Add **obfuscated detection cases** where evasion is realistic (leetspeak, separators, Turkish folding).
4. Ensure the aggregate corpus gate passes:
- false positives = **0**
- false negatives = **0**
- detection coverage = **100%** (profane + obfuscated corpus)
5. Include a **risk assessment** in the PR description (template below).

Do not merge dictionary expansion PRs that increase false positives without explicit project approval.

---

## Clean neighbor cases (required)

Every new entry must include clean corpus cases showing the term does **not** match innocuous text.

| Term length | Minimum clean neighbor cases |
|-------------|------------------------------|
| ≤2 characters | 8 |
| 3 characters | 5 |
| ≥4 characters | 3 |

Neighbor cases should cover:

- Substring collisions (term embedded in longer innocent words)
- Similar spellings and Turkish character variants
- Realistic sentence context where the innocent word appears

Add cases to [`tests/Fixtures/tr/clean.php`](../tests/Fixtures/tr/clean.php) with `tags` and optional `risk`.

---

## Profane and obfuscated positive cases (required)

For each new entry, add:

- At least **3** cases to [`tests/Fixtures/tr/profane.php`](../tests/Fixtures/tr/profane.php) (direct token and sentence usage)
- At least **3** cases to [`tests/Fixtures/tr/obfuscated.php`](../tests/Fixtures/tr/obfuscated.php) (realistic evasion)

Each positive case should set `expect.terms` to the canonical dictionary term.

---

## Short-term evaluation (≤3 characters)

Short dictionary terms (e.g. `aq`, `mal`, `amk`) require:

- Explicit `risk` annotation on related clean cases
- Extra clean neighbor coverage per table above
- Documented trade-off in the PR risk assessment
- Reviewer acknowledgment that standalone-token matching is intentional

Short-term false positives are a **dictionary design** concern, not a matcher defect.

---

## False-positive gate

The CI quality gate enforced by [`tests/TurkishCorpusQualityTest.php`](../tests/TurkishCorpusQualityTest.php) requires:

```
falsePositiveCount() === 0
falseNegativeCount() === 0
detectionCoverage() === 100.0 // profane + obfuscated corpus
```

Any PR that increases false positives must either:

- Revise or reject the dictionary entry, or
- Add clean corpus cases proving the new behavior is acceptable (rare; requires explicit approval)

---

## Risk assessment template

Include this block in dictionary expansion PRs:

```markdown
## Dictionary entry risk assessment

- Term: `example`
- Length: 3
- Category / severity: profanity / medium
- Clean neighbors added: clean.example.* (5 cases)
- Profane cases added: profane.example.* (3 cases)
- Obfuscated cases added: obfuscated.example.* (3 cases)
- Known FP risk: [none | documented in corpus notes]
- Short-term trade-off: [N/A | standalone token may match innocuous text in edge contexts]
```

---

## Related documents

| Document | Purpose |
|----------|---------|
| [`FOUNDATION.md`](../FOUNDATION.md) | Correctness policy |
| [`docs/specification.md`](specification.md) | Dictionary author format |
| [`CONTRIBUTING.md`](../CONTRIBUTING.md) | Contribution workflow |
58 changes: 58 additions & 0 deletions tests/Fixtures/tr/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Turkish Quality Corpus

Test-only fixture corpus for measuring Turkish dictionary quality against the seed dictionary in `data/tr.php`.

## Files

| File | Purpose | Expected detection |
|------|---------|-------------------|
| `clean.php` | Innocuous text; false-positive candidates | `false` |
| `profane.php` | Direct profanity and insult detection | `true` |
| `obfuscated.php` | Leetspeak, separators, folding, repeats | `true` |
| `edge_cases.php` | Boundary behavior (mixed expectations) | per case |

## Case format

```php
[
'id' => 'clean.malzeme.embedded',
'text' => 'malzeme listesi hazır',
'expect' => [
'detected' => false,
],
'tags' => ['substring', 'mal-risk'],
'note' => 'mal must not match inside malzeme',
]
```

### Required fields

- `id` — stable identifier (`category.slug`)
- `text` — UTF-8 input
- `expect.detected` — boolean

### Optional fields

- `expect.terms` — expected canonical dictionary terms (seed terms only)
- `expect.match_count` — expected number of matches
- `expect.severity` — expected highest severity
- `expect.categories` — expected match categories
- `tags` — filtering labels
- `note` — human-readable rationale
- `risk` — dictionary expansion risk hint (`low`, `medium`, `high`)

## Rules

- Empty `text` is allowed **only** in `edge_cases.php`.
- `expect.terms` must reference seed terms: `amk`, `aq`, `siktir`, `orospu`, `mal`.
- IDs must be unique across all fixture files.

## Quality gate

`TurkishCorpusQualityTest` enforces:

- false positives = 0
- false negatives = 0
- detection coverage = 100% (profane + obfuscated corpus)

See [docs/dictionary-expansion-policy.md](../../docs/dictionary-expansion-policy.md) before adding dictionary entries.
Loading
Loading