Extract provider payload normalization into ProviderNormalizer.js

main.qml mixed two things under one `parse*` prefix: turning raw CLI JSON
into a validated, bounded structure, and writing that structure onto root
properties while managing error text, the loading flag, notification memos
and fallback accounting. Only the first half is pure, and only the first
half was testable.

The pure half moves to contents/ui/ProviderNormalizer.js: the collection
bounds, the object-key guards, provider-id screening, payload shape checks,
the usage-percentage clamps, and the cost/session/account normalizers. The
parse*/state half stays in main.qml and calls into it. The boundary is
mechanical - nothing in the new file touches root, a QML property, Qt,
Kirigami or i18n. That last exclusion is load-bearing: the file is in the
gettext extraction glob, so rateWindowMetrics returns clamped numbers and
main.qml still supplies the translated label.

The bounds now have one source of truth. main.qml binds its properties to
the module rather than repeating 256/128/256/24, so a future change cannot
drift the constant away from the call sites the security check asserts.

tests/tst_provider_normalizer.qml adds 58 QtTest cases against the payloads
a broken or hostile CLI could emit: __proto__/prototype/constructor keys,
arrays past every bound, out-of-range and absent percentages, unsupported
sessions shapes, alias collisions, off-host status URLs, and unicode that
must not be re-sliced. Two of them pin a pre-existing gap rather than fix
it - SafeText.boundedDisplayText coerces structured values, so a malformed
displayName still reaches the popup as "[object Object]". Changing that is
a behavior change and belongs in its own commit; the tests are written so
that commit has to update an assertion deliberately.

Static assertions that were pinned to main.qml follow the code instead of
being weakened: the payload-shape rule now names normalizeProviderConfigEntries
and normalizeSessions, and the pace guard names rateWindowMetrics, with
added coverage that the percentage clamps survive. Nothing was relaxed to
make the checks pass.

make check: 143 QML tests pass, up from 85. main.qml drops 4522 -> 4211
lines. Verified at runtime with ./install.sh; plasmashell logs show no
ReferenceError, TypeError or SyntaxError.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
227eeb3b09Simone committed on 8/18/2026, 11:15:59 AMparent1f565f1
6 files changedLine totals unavailable