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
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Argumentum.AssetConverter.Mindmapper;
using Xunit;

namespace Argumentum.AssetConverter.Tests.MindmapGeneration
{
/// <summary>
/// Regression guard for issue #725: the Virtues mind-map HTML wrappers
/// (<c>Argumentation_Virtues_{lang}.html</c>) must inline the **localized**
/// <c>Argumentum_Virtues_MindMap_{lang}.content.svg</c> per language — not the
/// French source SVG. The pipeline path is
/// <c>VirtueMindMapDocumentConfig.GenerateHtmlSvgWrappers</c> →
/// <see cref="MindMapHtmlWrapper.FormatWrapper"/>, which substitutes the
/// post-localization <c>content.svg</c> into the <c>[SVGCONTENT]</c> placeholder.
///
/// The **committed** wrappers are stale (generated 2026-05-24 by the pre-#665
/// code, when the Virtues <c>content.svg</c> was FR-frozen — see
/// <c>docs/investigations/2026-06-25-virtues-mindmap-fr-frozen-mechanism.md</c>).
/// They were skipped by every later SVG regen because
/// <c>AssetConverterConfig.OverwriteExistingHtmlMaps</c> defaults to <c>false</c>
/// while <c>OverwriteExistingDocs</c> is <c>true</c>. The stale files are tracked
/// separately for a post-tag regen (po-2023 lane); this test does NOT touch
/// <c>Cards/</c>.
///
/// What this test pins is the **assembly contract**: feeding the *already-localized*
/// committed <c>content.svg</c> through <see cref="MindMapHtmlWrapper.FormatWrapper"/>
/// must yield a wrapper whose visible node text matches the target language. It
/// mirrors the Fallacies Playwright suite
/// (<c>VisualTests/MindmapWrapperTests.cs</c>) but runs headless in CI (no browser),
/// so a future regression in the helper or in the SVG-localization wiring surfaces
/// here without RDP/FreeMind.
/// </summary>
public class VirtuesMindmapWrapperLocalizationTests
{
/// <summary>
/// Visible node text that is French-only in the FR-frozen Virtues mind map.
/// Empirically (master <c>6ce91ef8</c>) present 27× in the FR <c>content.svg</c>
/// and 0× in every localized one — a sharp cross-contamination discriminator.
/// </summary>
private const string FrenchFrozenMarker = "Honnêteté intellectuelle";

private static readonly string RepoRoot =
Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", ".."));

private static readonly string IncludedTemplatePath =
Path.Combine(RepoRoot, "Cards", "Fallacies", "Mindmaps", "included.html");

private static string GetVirtuesContentSvgPath(string lang)
=> Path.Combine(RepoRoot, "Cards", "Fallacies", "Mindmaps", lang,
$"Argumentum_Virtues_MindMap_{lang}.content.svg");

private static int CountInRange(string s, char low, char high)
{
var n = 0;
foreach (var c in s)
if (c >= low && c <= high) n++;
return n;
}

private static int CountCyrillic(string s) => CountInRange(s, 'Ѐ', 'ӿ');
private static int CountCjk(string s) => CountInRange(s, '一', '鿿');
private static int CountArabicScript(string s) => CountInRange(s, '؀', 'ۿ');

/// <summary>
/// For each language, the wrapper assembled from the committed localized
/// <c>content.svg</c> must carry that language's script/labels and must NOT
/// carry the French-frozen marker. This is the headless proof that the correct
/// per-language <c>content.svg</c> is the one being inlined.
/// </summary>
[Theory]
[InlineData("fr")]
[InlineData("en")]
[InlineData("ru")]
[InlineData("zh")]
[InlineData("ar")]
[InlineData("fa")]
public async Task FormatWrapper_WithLocalizedVirtuesContentSvg_WrapperMatchesTargetLanguage(string lang)
{
Assert.True(File.Exists(IncludedTemplatePath),
$"Missing included.html template: {IncludedTemplatePath}");
var svgPath = GetVirtuesContentSvgPath(lang);
Assert.True(File.Exists(svgPath),
$"Missing localized Virtues content.svg fixture: {svgPath}. " +
"If this was deleted, the test fixture must be regenerated (see issue #725).");

var template = await File.ReadAllTextAsync(IncludedTemplatePath);
var svg = await File.ReadAllTextAsync(svgPath);

var wrapper = MindMapHtmlWrapper.FormatWrapper(
template,
svgRelativePath: $"Argumentum_Virtues_MindMap_{lang}.content.svg",
svgContent: svg);

// The two placeholder tokens must both be consumed (no partial substitution).
Assert.DoesNotContain("[SVGCONTENT]", wrapper);
Assert.DoesNotContain("[SVGPATH]", wrapper);
// The inlined SVG body must be present.
Assert.Contains("<svg", wrapper);

switch (lang)
{
case "fr":
// FR is the source language: the FR-frozen marker IS expected here.
Assert.Contains(FrenchFrozenMarker, wrapper);
break;
case "en":
Assert.Contains("Intellectual honesty", wrapper);
Assert.DoesNotContain(FrenchFrozenMarker, wrapper);
break;
case "ru":
Assert.True(CountCyrillic(wrapper) > 100,
$"RU wrapper should carry Cyrillic node text, got {CountCyrillic(wrapper)}.");
Assert.DoesNotContain(FrenchFrozenMarker, wrapper);
break;
case "zh":
Assert.True(CountCjk(wrapper) > 100,
$"ZH wrapper should carry CJK node text, got {CountCjk(wrapper)}.");
Assert.DoesNotContain(FrenchFrozenMarker, wrapper);
break;
case "ar":
case "fa":
// Arabic and Persian both use the Arabic script block (U+0600–U+06FF);
// Persian-specific letters (U+06CC etc.) also fall in this range.
Assert.True(CountArabicScript(wrapper) > 100,
$"{lang} wrapper should carry Arabic-script node text, got {CountArabicScript(wrapper)}.");
Assert.DoesNotContain(FrenchFrozenMarker, wrapper);
break;
default:
throw new ArgumentOutOfRangeException(nameof(lang), lang, "Unhandled InlineData language.");
}
}

/// <summary>
/// Cross-language no-contamination contract: the EN wrapper must not contain
/// the FR-frozen marker AND must not accidentally carry another language's
/// native script. Guards against a future regression where the wrong per-language
/// <c>content.svg</c> is inlined (e.g. all wrappers seeded from FR).
/// </summary>
[Fact]
public async Task FormatWrapper_VirtuesEnglish_NoFrenchOrForeignScriptContamination()
{
var svgPath = GetVirtuesContentSvgPath("en");
Assert.True(File.Exists(svgPath), $"Missing fixture: {svgPath}");
var template = await File.ReadAllTextAsync(IncludedTemplatePath);
var svg = await File.ReadAllTextAsync(svgPath);

var wrapper = MindMapHtmlWrapper.FormatWrapper(template, "x.svg", svg);

Assert.Contains("Intellectual honesty", wrapper);
Assert.DoesNotContain(FrenchFrozenMarker, wrapper);
Assert.True(CountCyrillic(wrapper) == 0,
$"EN wrapper should have zero Cyrillic codepoints, got {CountCyrillic(wrapper)}.");
Assert.True(CountCjk(wrapper) == 0,
$"EN wrapper should have zero CJK codepoints, got {CountCjk(wrapper)}.");
Assert.True(CountArabicScript(wrapper) == 0,
$"EN wrapper should have zero Arabic-script codepoints, got {CountArabicScript(wrapper)}.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Issue #725 — Virtues HTML wrapper FR-frozen: **stale file, not a code bug**

**Author**: po-2024 (worker) · **Date**: 2026-07-07 · **Base**: master `6ce91ef8`
**Scope**: read-only code/data investigation + CI regression test. **0 write under `Cards/`** (release freeze). master untouched.
**Dispatch**: ai-01 `3i1ie4` [PRIMAIRE] #725 — "corrige l'assemblage du wrapper (quel `content.svg` par-langue est inliné) ; code + test headless gated, 0 régén".
**Related**: #725 (this issue) · #665 / `27442add` (Virtues mindmap i18n wiring — the actual fix) · #715 (Virtue ar/fa/zh localization) · #724 (Virtue ar/fa/zh native-script SVGs) · #686 / `204adc47` (regen Virtues Batik SVGs) · `2026-06-25-virtues-mindmap-fr-frozen-mechanism.md` (the FR-frozen mechanism, now **superseded** for the `.content.svg`) · memory `virtues-mindmap-content-svg-fr-frozen` (**stale** — see §5).

---

## TL;DR

1. **The issue's root-cause hypothesis is incorrect on the mechanism.** #725 hypothesizes the wrapper "inlines the raw FreeMind `.svg` (French source) rather than the post-localization `content.svg`". Code reading proves the opposite: `GenerateHtmlSvgWrappers` inlines the **post-localization `content.svg`** (the `svgLoader` produced after `UpdateSvgWithItems`), never the raw render.
2. **The committed wrappers are stale, not mis-assembled.** `Argumentation_Virtues_{en,ru,…}.html` were last touched `2026-05-24` by `df3c769e` (#312) — **before** the Virtues i18n wiring landed in `27442add` (#665). At that time the Virtues `content.svg` was genuinely FR-frozen (per the 2026-06-25 investigation). The wrapper was generated FR, then never regenerated.
3. **The `content.svg` (the input to the wrapper) IS now localized** for all 8 languages (regenerated `2026-07-06` by `204adc47` / #686, after #665/#715/#724). The wrapper just wasn't regenerated alongside it.
4. **Why the wrapper was skipped during every later SVG regen:** `AssetConverterConfig.OverwriteExistingHtmlMaps` defaults to `false` (no initializer, `AssetConverterConfig.cs:380`) while `OverwriteExistingDocs` is `true` (`:361`). So a regen clobbers the `.content.svg` (docs) but silently skips the existing `.html` wrappers. This is the operational root cause of the staleness.
5. **No code fix is needed for #725.** The assembly code is correct (proven 4 ways below). The fix is a **post-tag regen of the wrappers with `OverwriteExistingHtmlMaps=true`** (po-2023 lane, RDP/FreeMind-gated). This investigation + a CI regression test are the gated, régén-free deliverable for this tick.
6. **Memory `virtues-mindmap-content-svg-fr-frozen` is now stale for the `.content.svg`** — #665/#715/#724 fixed it. Updated in §5.

---

## 1. Evidence — the assembly code inlines the localized `content.svg`

`VirtueMindMapDocumentConfig.ProcessSvgFilesAsync` ([VirtueMindMapDocumentConfig.cs:531-605](../../Generation/Converters/Argumentum.AssetConverter/Mindmapper/VirtueMindMapDocumentConfig.cs#L531-L605)):

- `svgFilePath` = the raw FreeMind render (`…_{lang}.svg`, FR tree structure as emitted by FreeMind).
- For each `SVGFreemindMap` (here `content.svg`): `svgSavedFilePath = …_{lang}.content.svg`.
- Either branch of the exist-check resolves `svgLoader` to the **localized** SVG:
- File-exists branch (`:541-545`): reads the saved `…_{lang}.content.svg` (which is localized post-#686).
- Regen branch (`:546-592`): loads the raw render, applies `UpdateSvgWithItems(svgFreemindMap, mindMapItems, …)` (`:583`) — this is the post-processing that rewrites node text from the **localized** `mindMapItems` — then `svgLoader = () => GetSvgContent(svgDoc)`.
- `GenerateHtmlSvgWrappers(svgFreemindMap, …, svgLoader, language)` (`:594`) consumes that localized `svgLoader`.

`GenerateHtmlSvgWrappers` ([VirtueMindMapDocumentConfig.cs:869-901](../../Generation/Converters/Argumentum.AssetConverter/Mindmapper/VirtueMindMapDocumentConfig.cs#L869-L901)):

```csharp
var languageAwareDocName = htmlSvgWrapper.DocumentName.Replace("[LANGUAGE]", language); // :881
htmlTemplate = MindMapHtmlWrapper.FormatWrapper(htmlTemplate, svgRelativePath, await svgContent()); // :895
File.WriteAllText(htmlFileName, htmlTemplate, Encoding.UTF8); // :897
```

`svgContent` is the localized `svgLoader` from above. The wrapper name is correctly per-language (`Argumentation_Virtues_{lang}.html`). **At no point is the raw FreeMind render inlined** — #725's hypothesis is wrong on the mechanism.

The `included.html` template carries only the `[SVGCONTENT]` placeholder ([MindMapHtmlWrapper.cs:5-21](../../Generation/Converters/Argumentum.AssetConverter/Mindmapper/MindMapHtmlWrapper.cs#L5-L21)); it contributes no language of its own.

## 2. Evidence — empirical state on master `6ce91ef8`

Codepoint scan of the committed files (visible `<text>` content, not `family=` attributes):

| Lang | `…content.svg` native script | `…content.svg` `"Honnêteté intellectuelle"` | `Argumentation_Virtues_{lang}.html` native script | wrapper `"Honnêteté intellectuelle"` |
|------|------------------------------|---------------------------------------------|---------------------------------------------------|--------------------------------------|
| fr | — (Latin) | **27×** (correct) | — (Latin) | **133× FR diacritics** (correct for FR) |
| en | `Intellectual honesty` ×28 | **0×** ✅ localized | **0 EN labels, 0 Cyrillic/CJK/Arabic** | FR only ❌ |
| ru | Cyrillic ×40066 | **0×** ✅ | **0 Cyrillic** | FR only ❌ |
| zh | CJK ×10189 | **0×** ✅ | wrapper file not committed for zh | — |
| ar | Arabic ×23980 | **0×** ✅ | wrapper file not committed for ar | — |
| fa | Arabic ×25576 | **0×** ✅ | wrapper file not committed for fa | — |

- The `content.svg` is localized across the board (post-#686). The `fr` residual of `"Honnêteté"` (without `intellectuelle`) at 27× in every language is a separate, constant, structural residue (untranslated link/root nodes) — out of #725's scope, constant across languages, not the reported regression.
- The committed wrapper for `en` and `ru` is **100% French** — including `ru` having **zero** Cyrillic. That is impossible to produce from the *current* `content.svg` (which is Cyrillic), so the wrapper must predate the localized `content.svg`. Confirmed in §3.

## 3. Evidence — git history: wrapper predates the i18n fix; `content.svg` was regenerated after

```
Argumentation_Virtues_en.html last: df3c769e 2026-05-24 feat(mindmaps): … zoom/pan (#312)
Argumentum_Virtues_MindMap_en.content.svg last: 204adc47 2026-07-06 regen(mindmaps): refresh Virtues Batik SVGs (#686)
Argumentation_Virtues_ru.html last: df3c769e 2026-05-24 (#312)
```

And the Virtues i18n **code** wiring landed between the two:

```
27442add fix(mindmap): #636 §2 — wire Virtues mindmap i18n for En/Ru/Pt/Es (#665)
```

So: on 2026-05-24, the Virtues `content.svg` was still FR-frozen (per the 2026-06-25 investigation, base `bef3bc6c`); the wrapper generated that day was correctly FR **relative to its FR input**. `#665` then wired i18n; `#715`/`#724` added ar/fa/zh; `#686` regenerated the `content.svg` localized — but the wrappers were skipped (`OverwriteExistingHtmlMaps=false`, §4) and stayed FR.

## 4. Evidence — the Fallacies wrapper (same code path) IS localized

The Fallacies wrapper uses the identical `GenerateHtmlSvgWrappers` path (`FallacyMindMapDocumentConfig.cs:1499-1526`). The committed Fallacies wrapper is localized:

```
Fallacies_en.html: EN-hits=188 FR-hits=12 text nodes: 'Fallacy', 'Ad hominem', 'Name Calling', 'Defamation' …
```

Same assembly code, correct result for Fallacies ⇒ the assembly code is correct; the Virtues wrapper is a stale-file artifact, not a code defect.

## 5. Operational root cause & post-tag fix

- **Root cause (staleness):** `OverwriteExistingHtmlMaps` defaults to `false` (`AssetConverterConfig.cs:380`, no initializer) while `OverwriteExistingDocs = true` (`:361`). Any SVG regen clobbers the `.content.svg`/`.links.svg` (docs) but skips existing `.html` wrappers (`VirtueMindMapDocumentConfig.cs:886`, `FallacyMindMapDocumentConfig.cs:1514`, `MindMapDocumentConfig.cs:772`).
- **Post-tag fix (po-203 lane, gated):** regen the Virtues (and, defensively, Fallacies) mind maps with `OverwriteExistingHtmlMaps=true` so the wrappers are rebuilt from the now-localized `content.svg`. Then commit the regenerated `Argumentation_Virtues_{lang}.html` / `_ext.html`. Clobber the harvest/SVG cache first (lesson `regen-success-without-clobber-is-stale-trap`). Add zh/ar/fa wrappers (none committed today — the pipeline will emit them once the regen runs; they are not in `git ls-files`).
- **Optional hardening (judgment call, separate PR, not this tick):** couple wrapper regen to SVG regen, or emit a warning when a wrapper is skipped but its sibling `content.svg` is newer. Out of scope here (mindmap post-processing = judgment call per No-Pendulum).

## 6. Deliverable this tick (gated, régén-free)

- **CI regression test** `Argumentum.AssetConverter.Tests/MindmapGeneration/VirtuesMindmapWrapperLocalizationTests.cs` — a headless (no Playwright) Theory over fr/en/ru/zh/ar/fa that feeds the committed localized `content.svg` through `MindMapHtmlWrapper.FormatWrapper` and asserts the wrapper matches the target language (native script / EN label present, FR-frozen marker absent for non-FR). Mirrors `VisualTests/MindmapWrapperTests.cs` for Fallacies but runs in the unit-test CI suite. Pins the assembly contract so a future regression in the helper or the i18n wiring surfaces without RDP/FreeMind.
- **This investigation** documenting the stale-file verdict.

## 7. Memory correction

`virtues-mindmap-content-svg-fr-frozen` (written 2026-06-25 on base `bef3bc6c`) asserted the Virtues `.content.svg` is FR-frozen. That was true then; it is **false now** — #665/#715/#724/#686 localized the `.content.svg` (Cyrillic/CJK/Arabic verified present, §2). The memory is updated to scope the FR-frozen claim to the **pre-#665** era and point the remaining gap at the **stale wrapper** (this issue), not the `content.svg`.
Loading