Skip to content

Commit b25154f

Browse files
committed
v1.0.5: syllableMode, heuristic-as-override, dead code cleanup
Added: - Language::syllableMode (tex|heuristic|composite), default tex - HeuristicSyllableCounter::hasWord() per-word lookup - 39 new tests: HeuristicSyllableCounterTest, CompositeSyllableCounterTest Changed: - Heuristic complements TeX, not replaces it (composite mode) - hasRules() checks only problemWords, not subtract/addPatterns - CompositeSyllableCounter fallback delegates to last chain counter - wordsWithNSyllables() renamed to wordsWithMoreThanNSyllables() Fixed: - Non-ASCII: [^a-z] to [^\p{L}]/u, all preg_* with /u flag - WienerSachtextformel: throws on invalid variant (not 1-4) - SzigrisztPazos: eliminated redundant *100/100 Removed: - FormulaResult::gradeLabel (always null in 16/17 formulas) - Engine::\ (local variable instead) - Pattern::toString() (dead production code)
1 parent e9f226f commit b25154f

34 files changed

Lines changed: 653 additions & 94 deletions

‎CHANGELOG.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,32 @@
22

33
All notable changes to ReadSight will be documented in this file.
44

5+
## [1.0.5] - 2026-06-26
6+
7+
### Added
8+
- `Language::syllableMode` — per-language syllable counting strategy (`tex`|`heuristic`|`composite`), default `tex`
9+
- `HeuristicSyllableCounter::hasWord()` — per-word lookup in problemWords
10+
- 39 new tests: `HeuristicSyllableCounterTest` (25), `CompositeSyllableCounterTest` (9), `LanguageTest` (+5)
11+
12+
### Changed
13+
- **Heuristic now complements TeX, not replaces it**: `composite` mode uses heuristic only for explicit problemWords, falling through to TeX for everything else
14+
- `hasRules()` now checks only `problemWords` (not subtract/addPatterns) — allows TeX fallthrough for non-problem words
15+
- `CompositeSyllableCounter` fallback: `return 1` → delegates to last counter in chain
16+
17+
### Fixed
18+
- **Non-ASCII support**: `[^a-z]` → `[^\p{L}]/u`, all `preg_*` calls now use `/u` flag — Cyrillic, German umlauts, accented characters correctly preserved
19+
- `WienerSachtextformel`: invalid variant numbers (0, 5+) now throw `\InvalidArgumentException` instead of silently computing variant 4
20+
- `SzigrisztPazos`: redundant `*100/100` eliminated, display value preserved in inputs
21+
22+
### Removed
23+
- `FormulaResult::gradeLabel` — dead field, always `null` across 16 of 17 formulas (Lix duplicated `interpretation`)
24+
- `Engine::$languageRepository` — unused property (kept as local variable in constructor)
25+
- `Pattern::toString()` — dead production code
26+
- `wordsWithNSyllables()` → renamed to `wordsWithMoreThanNSyllables()` (name now matches behaviour: `> N`)
27+
28+
### Data
29+
- `en-us.json`, `en-gb.json` — added `"syllableMode": "composite"`
30+
531
## [1.0.4] - 2026-06-26
632

733
### Added

‎CODE_REVIEW.md‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Code Review Report — ReadSight v1.0.4
2+
3+
**Date:** 2026-06-26
4+
**Scope:** `src/` (48 files), `tests/` (18 files), root config
5+
**Verification:** PHPStan level max (0 errors), PHPUnit (262 tests, 1034 assertions — green)
6+
7+
---
8+
9+
## All findings (10)
10+
11+
| # | Severity | File(s) | Description | Status |
12+
|---|----------|---------|-------------|--------|
13+
| C1 | CRITICAL | `HeuristicSyllableCounter`, `CompositeSyllableCounter`, `Engine` | Heuristic полностью подавляет TeX для английского — `hasRules()` возвращал true из-за subtract/addPatterns, Composite никогда не доходил до TeX | ✓ fixed |
14+
| M1 | MEDIUM | `HeuristicSyllableCounter.php:71` | `preg_replace('/[^a-z]/', ...)` вырезал все не-ASCII символы; `preg_split`/`preg_match` без флага `/u` ломали UTF-8 | ✓ fixed |
15+
| M2 | MEDIUM | `TextAnalyzer.php:96`, `Engine.php:179` | `wordsWithNSyllables()` считает слова с **>N** слогов, называется «с N слогами» | ✓ fixed |
16+
| L1 | LOW | `FormulaResult.php:16` | `gradeLabel` всегда `null` (кроме Lix, где дублирует `interpretation`) | ✓ fixed |
17+
| L2 | LOW | `Engine.php:35` | `$languageRepository` присвоен в конструкторе, нигде больше не прочитан | ✓ fixed |
18+
| L3 | LOW | `Pattern.php:22` | `toString()` никогда не вызывается в production | ✓ fixed |
19+
| L4 | LOW | `WienerSachtextformel.php:45-55` | variant=5 молча считается как variant=4 | ✓ fixed |
20+
| L5 | LOW | `CompositeSyllableCounter.php:25` | Fallback `return 1` | ✓ fixed |
21+
| I1 | INFO | `SzigrisztPazos.php:38-40` | `$S = x * 100.0; ... $S / 100.0` — избыточное умножение/деление | ✓ fixed |
22+
| I2 | INFO | `HeuristicSyllableCounter.php:137` | `splitSyllables` делит строку арифметически, не лингвистически | ✓ fixed |
23+
24+
---
25+
26+
## Fixed (10) — все исправлено
27+
28+
### Раунд 1 — Heuristic + TeX архитектура (C1, M1, L5)
29+
30+
**Проблема:** `HeuristicSyllableCounter` полностью заменял TeX для английского, не дополнял.
31+
32+
**Решение:**
33+
- `HeuristicSyllableCounter::hasRules()` → теперь только `problemWords !== []`
34+
- Добавлен `hasWord(string)` — проверка конкретного слова
35+
- `CompositeSyllableCounter` — `countSyllables`/`splitSyllables` используют `hasWord()` вместо `hasRules()`: problemWord → heuristic, остальное → TeX
36+
- `Language` — новое поле `syllableMode` (`tex`|`heuristic`|`composite`), дефолт `tex`
37+
- `Engine::loadSyllableCounter` — диспетчеризация по `syllableMode`
38+
- `[^a-z]` → `[^\p{L}]/u`, все `preg_*` получили флаг `/u`
39+
- `en-us.json`, `en-gb.json` → `"syllableMode": "composite"`
40+
- +39 тестов (`HeuristicSyllableCounterTest`, `CompositeSyllableCounterTest`, `LanguageTest`)
41+
42+
### M2 + L2 + L3 + L4 — чистка (раунд 2)
43+
44+
- **M2** — `wordsWithNSyllables` → `wordsWithMoreThanNSyllables` в `TextAnalyzer` + `Engine`
45+
- **L2** — удалён `$languageRepository` из Engine (локальная переменная вместо свойства)
46+
- **L3** — удалён `Pattern::toString()` + тесты к нему
47+
- **L4** — `WienerSachtextformel`: variant вне [1,4] → `\InvalidArgumentException`
48+
49+
### Раунд 3 — добивка (L1, I1, I2)
50+
51+
- **L1** — удалён `gradeLabel` из `FormulaResult` (17 формул + тест)
52+
- **I1** — `SzigrisztPazos`: устранено `*100/100`, промежуточное значение `syllablesPer100` сохранено для inputs
53+
- **I2** — `HeuristicSyllableCounter::splitSyllables`: добавлен doc-комментарий с описанием ограничения
54+
55+
---
56+
57+
## Open (0)

‎README.md‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,17 @@ $engine->addHyphenations([
237237
```
238238
Engine (facade)
239239
├── TextAnalyzer (syllable counting, text metrics)
240-
│ ├── LiangHyphenator (TeX hyphenation algorithm)
240+
│ ├── SyllableCounter (strategy: tex | heuristic | composite)
241+
│ │ ├── CompositeSyllableCounter (problemWords → heuristic, rest → TeX)
242+
│ │ ├── HeuristicSyllableCounter (vowel patterns + word list)
243+
│ │ └── TexSyllableCounter → LiangHyphenator (TeX hyphenation)
244+
│ ├── LiangHyphenator
241245
│ │ ├── TexSource (parses .tex from hyph-utf8)
242246
│ │ ├── PatternsCollection (pattern data)
243247
│ │ ├── HyphenationExceptionsCollection (word overrides)
244248
│ │ └── JsonPatternCache (compiled patterns)
245249
│ └── TextSplitter (word/sentence/letter counting)
246-
├── Language (JSON config per language)
250+
├── Language (JSON config per language, syllableMode + formulaConfigs)
247251
└── FormulaRegistry (17 formulas)
248252
├── FleschReadingEase (with lang-specific coefficients)
249253
├── GunningFog, SMOG, ColemanLiau, ARI, LIX (universal)
@@ -266,7 +270,7 @@ Engine (facade)
266270
```bash
267271
composer install # Install dependencies
268272

269-
composer test # Run PHPUnit (232 tests)
273+
composer test # Run PHPUnit (259 tests)
270274
composer test:coverage # With HTML coverage report
271275
composer analyse # PHPStan level max
272276
composer cs:check # PHP CS Fixer (dry-run)
@@ -278,12 +282,12 @@ composer check # All checks: CS + PHPStan + Tests
278282

279283
| Metric | Value |
280284
|---|---|
281-
| Tests | **232** |
282-
| Assertions | **1 071** |
285+
| Tests | **259** |
286+
| Assertions | **1 031** |
283287
| PHPStan | **Level max, 0 errors** |
284288
| PHP | 8.5.4 |
285289
| Source classes | 48 |
286-
| Test classes | 18 |
290+
| Test classes | 20 |
287291
| Supported languages | 86 |
288292
| Writing systems | 19 |
289293
| Readability formulas | 17 |

‎data/languages/en-gb.json‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎data/languages/en-us.json‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/Engine.php‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
use GlobusStudio\ReadSight\Hyphenation\Source\TexSource;
1616
use GlobusStudio\ReadSight\Language\JsonLanguageRepository;
1717
use GlobusStudio\ReadSight\Language\Language;
18-
use GlobusStudio\ReadSight\Language\LanguageRepository;
18+
1919
use GlobusStudio\ReadSight\Syllable\CompositeSyllableCounter;
2020
use GlobusStudio\ReadSight\Syllable\HeuristicSyllableCounter;
2121
use GlobusStudio\ReadSight\Syllable\SyllableCounter;
@@ -32,7 +32,6 @@ final class Engine
3232
private readonly Hyphenator $hyphenator;
3333
private readonly SyllableCounter $syllableCounter;
3434
private readonly TextAnalyzer $text;
35-
private readonly LanguageRepository $languageRepository;
3635
private readonly FormulaRegistry $formulaRegistry;
3736

3837
public function __construct(
@@ -43,8 +42,8 @@ public function __construct(
4342
) {
4443
$config = self::resolveConfig($patternsDir, $languagesDir, $cacheDir);
4544

46-
$this->languageRepository = new JsonLanguageRepository($config->languagesDir);
47-
$this->language = $this->languageRepository->find($language);
45+
$languageRepository = new JsonLanguageRepository($config->languagesDir);
46+
$this->language = $languageRepository->find($language);
4847

4948
$this->hyphenator = $this->loadHyphenator($this->language, $config->patternsDir, $config->cacheDir);
5049
$this->syllableCounter = $this->loadSyllableCounter();
@@ -176,9 +175,9 @@ public function polysyllableCount(string $text, bool $countProperNouns = true):
176175
return $this->text->polysyllableCount($text, $countProperNouns);
177176
}
178177

179-
public function wordsWithNSyllables(string $text, int $n, bool $countProperNouns = true): int
178+
public function wordsWithMoreThanNSyllables(string $text, int $n, bool $countProperNouns = true): int
180179
{
181-
return $this->text->wordsWithNSyllables($text, $n, $countProperNouns);
180+
return $this->text->wordsWithMoreThanNSyllables($text, $n, $countProperNouns);
182181
}
183182

184183
/** @return array<int, int> */
@@ -331,15 +330,19 @@ private static function resolveConfig(?string $patternsDir, ?string $languagesDi
331330
private function loadSyllableCounter(): SyllableCounter
332331
{
333332
$tex = new TexSyllableCounter($this->hyphenator);
333+
$mode = $this->language->syllableMode;
334334

335-
if ($this->language->syllableHeuristics !== null) {
336-
$heuristic = new HeuristicSyllableCounter($this->language->syllableHeuristics);
337-
if ($heuristic->hasRules()) {
338-
return new CompositeSyllableCounter([$heuristic, $tex]);
339-
}
335+
if ($mode === 'tex' || $this->language->syllableHeuristics === null) {
336+
return $tex;
337+
}
338+
339+
$heuristic = new HeuristicSyllableCounter($this->language->syllableHeuristics);
340+
341+
if ($mode === 'heuristic') {
342+
return $heuristic;
340343
}
341344

342-
return $tex;
345+
return new CompositeSyllableCounter([$heuristic, $tex]);
343346
}
344347

345348
private function loadHyphenator(Language $language, string $patternsDir, string $cacheDir): Hyphenator

‎src/Formula/AutomatedReadabilityIndex.php‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ public function calculate(TextStatistics $stats, Language $language): FormulaRes
3838
score: \round($score, 1),
3939
gradeLevel: \min(\max(\round($score, 1), 0.0), 18.0),
4040
interpretation: $this->interpret($score),
41-
gradeLabel: null,
4241
inputs: [
4342
'charsPerWord' => \round($stats->letterCount / $wordCount, 2),
4443
'wordsPerSentence' => \round($wordCount / $sentenceCount, 2),

‎src/Formula/ColemanLiau.php‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ public function calculate(TextStatistics $stats, Language $language): FormulaRes
4141
score: \round($score, 1),
4242
gradeLevel: \min(\max(\round($score, 1), 0.0), 18.0),
4343
interpretation: $this->interpret($score),
44-
gradeLabel: null,
4544
inputs: [
4645
'L' => \round($L, 2),
4746
'S' => \round($S, 2),

‎src/Formula/Crawford.php‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ public function calculate(TextStatistics $stats, Language $language): FormulaRes
4141
score: \round($score, 1),
4242
gradeLevel: null,
4343
interpretation: $this->interpret($score),
44-
gradeLabel: null,
4544
inputs: [
4645
'avgLettersPerWord' => \round($averageLetters, 2),
4746
'sentencesPer100Words' => \round($sentencesPer100, 2),

‎src/Formula/DaleChall.php‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public function calculate(TextStatistics $stats, Language $language): FormulaRes
3636
score: \round($adjusted, 1),
3737
gradeLevel: null,
3838
interpretation: $this->interpret($adjusted),
39-
gradeLabel: null,
4039
inputs: [
4140
'difficultWordPct' => \round($difficultPct, 1),
4241
'rawScore' => \round($rawScore, 4),

0 commit comments

Comments
 (0)