Skip to content

fix(locale): refuse a months.voc that is not exactly twelve lines - #379

Closed
openvoiceos-bot wants to merge 1 commit into
devfrom
fix/t4767-months-count-guard
Closed

openvoiceos-bot wants to merge 1 commit into
devfrom
fix/t4767-months-count-guard

Conversation

@openvoiceos-bot

Copy link
Copy Markdown
Contributor

months.voc is read by position: line N is month N. The guard added on
#366 refused only an index past the twelfth, so a file with too FEW lines
never tripped it, and every month after the gap answered as the month before
it.

Measured on an fr tree with mars deleted, 11 lines, before this change:

months loaded: 11
la derniere semaine de avril -> 2018-03-26
la 1 semaine de avril        -> 2018-03-05
le 31 jour de avril          -> 2018-03-31

No warning and no exception. "avril" answered March.

Two places, because the two layers have different contracts

This repository states one contract per layer: loading fails loud, scanning
fails soft.
The task asked for a failure that names "the locale and the
count", and only one layer has the locale — ScopedVocabulary carries
units, months, seasons, ordinal, of, article, year_word and no
locale; load_scoped_vocabulary(lang, locale_dir) knows it.

The loader refuses the file:

ValueError: months.voc for 'fr' has 11 lines, expected exactly 12, one per
month, January first, with spelling variants grouped on their month's own
line as (a|b). Read by position, so a wrong count silently shifts every
month after the gap.

The scan keeps its softer contract and gains a backstop, for a table built
in memory or by a caller that did not come through the loader.

The guard is asymmetric, deliberately

A flat len(vocab.months) != 12 would have been wrong, because the two halves
of a wrong count do not mean the same thing.

  • More than twelve. Only the entries past the twelfth cannot be named.
    Indices 0-11 are still the real months, so a real month must still answer.
    TestAMissizedFileDoesNotRaise and its control pin exactly that on a
    13-entry table, and a flat inequality would have broken them.
  • Fewer than twelve. Every entry at or after the gap is the wrong month,
    and nothing in the table says where the gap is. No index can be trusted, so
    the table is refused whole.

So the guard is index is None or index >= 12 or len(vocab.months) < 12.

Evidence

TestAShortMonthsVocIsRefused, five cases. Against dev 6bda4d9 with only
the test file applied:

4 failed, 3 passed
FAILED test_the_loader_refuses_an_eleven_line_file
SUBFAILED 'le 3 jour de avril' answered 2018-03-03 on an 11-entry table
SUBFAILED 'la 1 semaine de avril' answered 2018-03-05 on an 11-entry table
SUBFAILED 'la derniere semaine de avril' answered 2018-03-26 on an 11-entry table

The three that pass on dev are the controls — the untouched fixture still
loads twelve, and the same utterances answer April on a full table — so the
failures are the guard and not a phrase that never matched.

With this change, the module is 15 passed, 532 subtests passed.

The full local suite reached 93% with zero failures and was then stopped: the
box was at load average 52 and the process was starved at 0.0% CPU, competing
with other lanes. CI runs it here.

Not fixed here

le 31 jour de avril against a correct table raises
ValueError: day is out of range for month from ranges.get_date_ordinal,
which calls ref_date.replace(day=ordinal) unguarded. April has 30 days.
Reproduced on origin/dev 6bda4d9 as well, so it is pre-existing and not
introduced here — and it is why the reported symptom could answer "31 March"
at all, since the shifted table landed the 31st on a month that has one.

Filed as T-6763 with the design question of whether an impossible day
answers None or clamps. It is excluded from this branch's controls for that
reason, and the exclusion is stated in the test docstring rather than left
silent.

T-4767, from reviewer-b's T-4673.

🤖 Generated with Claude Code

months.voc is read BY POSITION: line N is month N. The guard added on #366
refused only an index PAST the twelfth, so a file with too FEW lines never
tripped it and every month after the gap answered as the month before it.

Measured on an fr tree with mars deleted, 11 lines, before this change:

    la derniere semaine de avril -> 2018-03-26
    la 1 semaine de avril        -> 2018-03-05
    le 31 jour de avril          -> 2018-03-31

No warning and no exception. "avril" answered March.

The fix sits in two places, because the two layers have different contracts
and only one of them knows the locale. ScopedVocabulary has no locale field;
load_scoped_vocabulary(lang, locale_dir) does.

- The LOADER refuses the file and names the locale and the count. That is
  the loud half, and the same contract _positional_voc_reader already states
  for a malformed line.
- The SCAN keeps its softer contract and gains a backstop for a table built
  in memory or by a caller that did not come through the loader.

The guard is asymmetric on purpose. More than twelve entries: only the ones
past the twelfth cannot be named, so a real month must still answer, which
TestAMissizedFileDoesNotRaise and its control pin on a 13-entry table. Fewer
than twelve: every entry at or after the gap is the wrong month and nothing
says where the gap is, so no index is trustworthy and the table is refused
whole. A flat `!= 12` would have broken that existing test.

TestAShortMonthsVocIsRefused adds the 11-line fixture. Against dev 6bda4d9
with only the test file applied: 4 failed, 3 passed, the three passes being
the controls (the untouched fixture still loads twelve, and the same
utterances answer April on a full table), so the failures are the guard and
not a phrase that never matched.

Not fixed here: `le 31 jour de avril` raises ValueError "day is out of range
for month" from ranges.get_date_ordinal on a CORRECT table, because April has
30 days and replace(day=) is unguarded. Reproduced on dev too, so it is
pre-existing. It is why the reported symptom could answer "31 March" at all.
Filed as T-6763 and excluded from the controls, with the exclusion stated in
the test rather than left silent.

T-4767, from reviewer-b's T-4673.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the fix label Sep 28, 2026
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Tada! The results of the latest automation run are here. 🎉

I've aggregated the results of the automated checks for this PR below.

⚖️ License Check

The license check is now finished. 🏁

✅ No license violations found.

Policy: Apache 2.0 (universal donor). StrongCopyleft / NetworkCopyleft / WeakCopyleft / Other / Error categories fail. MPL allowed.

📊 Coverage

Calculating the test-to-code ratio. ➗

⚠️ 73.9% total coverage

Files below 80% coverage (32 files)
File Coverage Missing lines
ovos_date_parser/calendars.py 0.0% 2
ovos_date_parser/cycles.py 0.0% 2
ovos_date_parser/regnal.py 0.0% 2
ovos_date_parser/roman.py 0.0% 2
ovos_date_parser/dates_cs.py 53.3% 359
ovos_date_parser/dates_da.py 59.0% 246
ovos_date_parser/dates_uk.py 63.1% 343
ovos_date_parser/dates_nb.py 63.5% 179
ovos_date_parser/dates_nn.py 63.5% 179
ovos_date_parser/dates_ru.py 64.1% 299
ovos_date_parser/dates_it.py 64.5% 223
ovos_date_parser/dates_ca.py 64.8% 357
ovos_date_parser/dates_gl.py 66.1% 242
ovos_date_parser/dates_pl.py 66.3% 208
ovos_date_parser/dates_eu.py 66.6% 222
ovos_date_parser/dates_nl.py 66.7% 219
ovos_date_parser/dates_es.py 68.2% 229
ovos_date_parser/dates_ast.py 70.4% 219
ovos_date_parser/dates_en.py 71.0% 266
ovos_date_parser/dates_ro.py 71.5% 199
ovos_date_parser/dates_pt.py 71.7% 210
ovos_date_parser/dates_an.py 72.5% 172
ovos_date_parser/dates_bg.py 73.8% 106
ovos_date_parser/dates_hr.py 73.8% 107
ovos_date_parser/dates_fy.py 74.1% 158
ovos_date_parser/dates_sv.py 75.6% 144
ovos_date_parser/dates_oc.py 77.0% 161
ovos_date_parser/dates_el.py 77.2% 141
ovos_date_parser/dates_az.py 77.6% 127
ovos_date_parser/dates_sl.py 78.0% 86
ovos_date_parser/common.py 78.3% 20
ovos_date_parser/dates_de.py 78.4% 132

Full report: download the coverage-report artifact.

🔨 Build Tests

Ensuring the code is correctly packaged and ready. 📦

✅ All versions pass

Python Build Install Tests pytest
3.10 ✅ ✅ ✅ 2895 passed, 3 skipped, 14 xfailed, 58366 warnings, 2299 subtests passed in 166.08s (0:02:46)
3.11 ✅ ✅ ✅ 2895 passed, 3 skipped, 14 xfailed, 58366 warnings, 2299 subtests passed in 141.09s (0:02:21)
3.12 ✅ ✅ ✅ 2895 passed, 3 skipped, 14 xfailed, 58366 warnings, 2299 subtests passed in 164.44s (0:02:44)

Crafting a better voice assistant, one commit at a time 🎙️

@JarbasAl JarbasAl closed this Sep 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants