Skip to content

test(localization): #204 #216 field-mapping contract — pure extraction + 12 tests - #504

Merged
jsboige merged 1 commit into
masterfrom
test/204-localization-field-mapping-contract
Jun 16, 2026
Merged

test(localization): #204 #216 field-mapping contract — pure extraction + 12 tests#504
jsboige merged 1 commit into
masterfrom
test/204-localization-field-mapping-contract

Conversation

@jsboige

@jsboige jsboige commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What

Primaire of ai-01 dispatch bwpx5q (gate-safe #204 deep-queue). Extracts the #216 root-cause site — the field-conversion transform inside CardSetLocalization.TranslateCardSetInfo — into pure, output-neutral static methods, and pins its contract with 12 tests.

#216 in one line: LocalizationConfig.FrontFieldConversions did template.Replace(sourceField, destField) on field names that were absent from the Mustache template. string.Replace is silent when the pattern is missing (returns the string unchanged), so every non-FR PDF silently kept its French content with no error — a silent false-pass. This PR makes that contract directly unit-testable and adds the regression guard that would have caught the original bug.

How (output-neutral, gate-safe)

Two new public static methods on CardSetLocalization:

  • ApplyFieldConversions(template, fieldConversions, exceptionPatterns, destLanguage) → string — byte-identical reimplementation of the previous inline logic (field-token replace + exception backtrack). TranslateCardSetInfo now calls it (3 lines replacing 31). FormatField delegates to a shared FormatFieldToken so production + the pure helpers + the 4 existing localization test files all share one source of truth.
  • FindAbsentSourceFields(template, fieldConversions, destLanguage) → IReadOnlyList<string> — the fail-loud companion: returns the source field names whose token is not in the template (the ones ApplyFieldConversions would silently skip). Pure capability.

Why FindAbsentSourceFields is not yet wired to Logger

The dispatch's DoD asked for "champ absent → fail-loud/log, PAS silencieux", but the gate holds even output-neutral pipeline behaviour changes (cf #472). Wiring Logger.Log on absent fields would emit new log lines in the production path = a behaviour change. So this PR delivers the capability + contract (the pure method + tests) and defers the 1-line production wiring to a post-gate follow-up. The produced template is byte-identical to today → unambiguously output-neutral.

If ai-01 wants the logging live now (lifting the gate for it), it's a one-line addition in TranslateCardSetInfo.

Tests — FieldMappingContractTests (12)

  • (a) present → replaced: ApplyFieldConversions_PresentField_IsReplaced, ..._ReplacesAllOccurrences_NotJustFirst (global, not first-match).
  • (b) absent → hazard + fail-loud: ..._AbsentField_IsSilentlySkipped_NoThrow (pins the silent-skip as the bug: non-FR PDFs contain French text — translations not applied #216 hazard), FindAbsentSourceFields_ReportsAbsentField_AndOmitsPresentOne, ..._ReturnsEmpty_WhenEveryFieldIsPresent.
  • dest-lang edges: ApplyFieldConversions_NoConversionForDestLang_SkipsSilently, FindAbsentSourceFields_NoConversionForDestLang_IsNotReportedAbsent (a field with no en entry is out of scope, not "absent").
  • (c) 4-CardSet lockstep (the bug: non-FR PDFs contain French text — translations not applied #216 regression guard): EveryMappedFrontField_IsPresentInRealTemplate_NoSilentFalsePass — Theory over Fallacies / Virtues / Rules / Scenarii, each against its real representative template (Argumentum_Fallacies_Face_fr.json, Argumentum_Virtues_Face_fr.json, Argumentum_Rules_fr.json, Argumentum_Scenarii_Face_fr.json). If any CardSet ever gains a front-field mapping whose source name isn't in the template, this fails loud — the signal bug: non-FR PDFs contain French text — translations not applied #216 lacked.
  • grounding: ApplyFieldConversions_OnRealFallaciesTemplate_ReplacesFrenchPlaceholders — the pure method fed the real config + template performs the same text_fr → text_en fix as the production path (behaviour-preserving, not just structurally pure).

Verification

  • New class: 12/12 pass.
  • Full suite on branch: 318 passed / 0 failed / 5 skipped (306 post-test(printplay): #204 extract recto-verso back reorder to pure method + 9 tests #500 master baseline + 12 new). No regression — the 4 existing localization test files (Fallacies/Rules/Virtues/Scenarii LocalizationTests) still pass through TranslateCardSetInfoApplyFieldConversions, confirming byte-identical output.
  • Diff: CardSetLocalization.cs (extract + delegate, net +~85/-25), 1 new test file. No CSV, no config, no RowsetNb/rscount, no workflow/rules, no production Logger change.

Scope notes

  • The 4 sibling test files each re-implement the apply loop inline (loc.FormatField + manual Replace). Migrating them onto ApplyFieldConversions (single source of truth) is a clean follow-up — out of scope here to keep this PR a focused contract extraction.
  • This is test-only / output-neutral → mergeable during the release gate per ai-01's gate-safe framing.

Contributes to #204. Refs #216.

🤖 Worker po-2024 — primaire of dispatch bwpx5q.

…n + 12 tests

Extract the field-conversion transform from CardSetLocalization.TranslateCardSetInfo
(the #216 root-cause site) into a pure output-neutral static ApplyFieldConversions, plus
a fail-loud companion FindAbsentSourceFields that surfaces the silent false-pass: source
field names mapped in FrontFieldConversions but absent from the Mustache template.
template.Replace silently no-ops when the pattern is missing, so a wrong mapping name
leaves content untranslated with no error — exactly the #216 footgun.

- ApplyFieldConversions: byte-identical to the previous inline logic (full suite 318/0/5,
  no regression). FormatField delegates to a shared static FormatFieldToken so production
  and the pure helpers share one source of truth.
- FindAbsentSourceFields: pure capability, NOT yet wired to Logger in production. That
  one-line wiring is a deliberate post-gate follow-up to keep this extraction output-
  neutral during the release gate (the gate holds even output-neutral pipeline behaviour
  changes, cf #472).
- FieldMappingContractTests (12): (a) present field -> replaced, global not first-match;
  (b) absent field -> silent skip documented as the hazard, AND surfaced by
  FindAbsentSourceFields; no-dest-lang conversion skipped (and not reported absent);
  (c) 4-CardSet lockstep (Fallacies/Virtues/Rules/Scenarii) — every 'en'-mapped front
  field is present in a real template, the regression guard that would have caught #216;
  plus real-Fallacies-template grounding.

Contributes to #204. Refs #216.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clusterManager-Myia

Copy link
Copy Markdown
Collaborator

[NanoClaw] LGTM — test + refactor, 2 files (+297/-32)

Clean extraction of the #216 root cause: ApplyFieldConversions and FindAbsentSourceFields pulled out as pure static methods from inline logic (output-neutral, byte-identical). 12 contract tests covering the silent false-pass hazard (field absent from template → string.Replace silently returns unchanged). All 4 card sets (Fallacies/Rules/Virtues/Scenarii) pinned. Good regression guard.

No concerns.

@jsboige
jsboige merged commit 5ed6e7d into master Jun 16, 2026
3 checks passed
@jsboige
jsboige deleted the test/204-localization-field-mapping-contract branch June 16, 2026 20:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants