fix: safe status-recommendation flow, MO-016 order independence, brand constant, message codes

- Add a single shared, pure vehicle-status evaluator (app/services/vehicle_status.py)
  used identically by the data-quality scanner, a new non-mutating status-recommendation
  preview endpoint, and a transactional apply endpoint with optimistic-concurrency token
  revalidation -- eliminates the old opaque "calculate and apply" action and the unsafe
  "maintenance + active booking -> auto rented" shortcut. Frontend
  DataQualityIssueDetail.tsx now shows a review/decide/confirm panel with localized
  why/evidence/consequence text in nl-BE/en-GB/fr-BE, with an exact "Change status to
  <status>" confirm action per the brief.
- Fix MO-016 issue-order dependency: resolving the booking-overlap issue before vs.
  after the status-conflict issue now converges on the same final vehicle status,
  proven by test_mo_016_status_conflict_recommendation_is_order_independent.
- Make "Fleet Ops" a non-localizable brand constant (frontend/src/product.ts,
  backend PRODUCT_NAME) via {{productName}} interpolation everywhere the brand name
  appeared in locale prose; add a permanent test guarding against a translation file
  ever defining the brand name or an "appName" key again.
- Convert dynamic backend prose to stable message codes + params: return status
  reasons, audit field/actor-type labels, automation last_error, and search
  section/vehicle/booking/issue results all now carry codes the frontend localizes,
  with raw technical text demoted to a "Technical details" disclosure.
- docs/fleet-ops-correction/: gap audit, i18n inventory, and the vehicle-status
  decision table documenting the evaluator's rules and safe-status principles.

148 backend tests + Ruff + mypy green; Alembic migration verified upgrade/downgrade;
frontend tsc/build and the i18n-coverage Playwright suite green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
NuklearRabbit
2026-08-03 21:37:34 +02:00
co-authored by Claude Sonnet 5
parent 18344bc8b7
commit 6deb95524d
68 changed files with 1734 additions and 225 deletions
+113
View File
@@ -71,3 +71,116 @@ test("no locale file contains an empty string value", () => {
}
}
});
// --- Brand-invariant: "Fleet Ops" is a fixed constant, never a translation value ---
// (see frontend/src/product.ts and docs/fleet-ops-correction/current-gap-audit.md §1).
// A regression here means someone re-introduced a per-locale brand key/value instead of
// interpolating {{productName}} from the shared constant.
test("no locale file defines an 'appName' key or the literal brand string", () => {
for (const language of LANGUAGES) {
for (const namespace of namespaces) {
const data = loadNamespace(language, namespace);
const raw = JSON.stringify(data);
expect(
raw.includes("Fleet Ops"),
`${language}/${namespace}.json contains the literal brand string "Fleet Ops" -- ` +
`use {{productName}} interpolation instead so the brand can never drift per locale`,
).toBe(false);
const keys = collectKeyPaths(data);
expect(
keys.some((k) => k === "appName" || k.endsWith(".appName")),
`${language}/${namespace}.json defines an "appName" key -- the brand name must come ` +
`from the PRODUCT_NAME constant, never a translatable key`,
).toBe(false);
}
}
});
// --- Translation-quality: prove values were actually translated, not copy-pasted ---
// Sleutelpariteit alone doesn't prove translation happened (a locale file could contain
// the literal English string under the right key and still pass). For every "real prose"
// string (>=8 chars, not on the allowlist below), assert nl-BE and fr-BE differ from
// en-GB, and that fr-BE differs from nl-BE -- catching both "still English" and
// "Dutch text copy-pasted into French" in one pass.
// Exact (namespace, key-path) pairs that are legitimately identical across two or more
// locales: real proper nouns/brand names, deliberately-untranslated role titles, and
// genuine cross-language cognates (identical spelling in Dutch/French/English). This is
// a precise allowlist by key path, not a broad word-level allowlist, so it can't quietly
// hide an unrelated real mistranslation under the same key in a different namespace.
const IDENTICAL_VALUE_ALLOWLIST = new Set([
"audit.title", // "Audit trail" kept as an established cross-language compliance term
"audit.diff.was", // "{{field}}: was {{value}}" -- "was" is spelled identically in Dutch
"auth.roleOperationsManager", // deliberately-untranslated role title (see demo.json roles)
"auth.roleRentalEmployee",
"demo.scenarios.roles.operations_manager", // same convention, scenario-overview role labels
"demo.scenarios.roles.rental_employee",
"common.language.nl-BE", // language-picker options show each language's own endonym
"common.language.fr-BE",
"common.footer.productLine", // "{{productName}} Demo" -- brief-specified exact footer text
"common.orgName", // "Northstar Mobility" -- fictional org proper noun, same in all 3
"dashboard.attention.openRecord", // "Open {{title}}" -- "open" is also the Dutch imperative
"demo.scenarios.durationValue", // "± {{minutes}} min" -- unit abbreviation, same in all 3
"demo.scenarios.startScenario", // "start"/"scenario" are naturalised loanwords in Dutch
"demo.about.limitationsTitle", // "Limitations" -- identical spelling in French
"demo.integrationSummary.titles.mcp_hub", // "ITWorx MCP Hub" -- proper noun
"fleet.list.columns.attention", // "Attention" -- identical spelling in French
"fleet.detail.tabs.inspections", // "Inspections" -- identical spelling in French
"integrations.cards.orchestrationKicker", // "Orchestration" -- identical in French
"knowledge.questionLabel", // "Question" -- identical spelling in French
"knowledge.retrievalFlow.question",
"knowledge.questionLabelExchange",
"navigation.items.audit", // "Audit trail" kept as an established cross-language term
"returns.result.inspection", // "Inspection" -- identical spelling in French
]);
function isTranslatableProse(value: unknown): value is string {
if (typeof value !== "string") return false;
if (value.trim().length < 8) return false;
// Strip interpolation placeholders and non-letter characters; if nothing substantial
// remains (pure numbers/punctuation/units), it's not "prose" that needs translating.
const stripped = value
.replace(/\{\{[^}]+\}\}/g, " ")
.replace(/[^a-zA-Zà-öø-ÿÀ-ÖØ-ß]/g, "");
return stripped.trim().length >= 3;
}
test("nl-BE and fr-BE translations are not suspiciously identical to en-GB or each other", () => {
for (const namespace of namespaces) {
const en = loadNamespace("en-GB", namespace);
const nl = loadNamespace("nl-BE", namespace);
const fr = loadNamespace("fr-BE", namespace);
const keys = collectKeyPaths(en);
for (const keyPath of keys) {
if (IDENTICAL_VALUE_ALLOWLIST.has(`${namespace}.${keyPath}`)) continue;
const at = (data: Record<string, unknown>) =>
keyPath.split(".").reduce<unknown>((acc, part) => {
if (acc && typeof acc === "object") return (acc as Record<string, unknown>)[part];
return undefined;
}, data);
const enValue = at(en);
if (!isTranslatableProse(enValue)) continue;
const nlValue = at(nl);
const frValue = at(fr);
expect(
nlValue,
`${namespace}.json:${keyPath} — nl-BE is identical to en-GB ("${enValue}"); ` +
`looks untranslated (add to IDENTICAL_VALUE_ALLOWLIST if this is intentional)`,
).not.toBe(enValue);
expect(
frValue,
`${namespace}.json:${keyPath} — fr-BE is identical to en-GB ("${enValue}"); ` +
`looks untranslated (add to IDENTICAL_VALUE_ALLOWLIST if this is intentional)`,
).not.toBe(enValue);
expect(
frValue,
`${namespace}.json:${keyPath} — fr-BE is identical to nl-BE ("${nlValue}"); ` +
`looks like Dutch text was copy-pasted into the French locale`,
).not.toBe(nlValue);
}
}
});