feat(ai): enhance AI trace warnings and reason codes system
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 4m21s
Test Suite / flutter-quality (push) Failing after 1m38s

- Added structured warning system with `AdminAiWarning` type in backend and Flutter
- Implemented detailed reason descriptors with `FlyerReasonDescriptor` for parse and match operations
- Added `legacyWarnings` field to maintain backward compatibility
- Enhanced AI trace service to collect and format warnings with item-level context
- Updated flyer import services to include detailed reason descriptions in responses
- Added Swedish diacritic preservation for cheese variants (Prästost, Herrgårdsost, Grevéost)
- Implemented UTF-8 content validation for AI responses
- Added new reason code definitions in `reason-codes.ts`
- Updated Flutter UI to display structured warnings with severity indicators
- Added error report generation and copy functionality in admin panel
- Added comprehensive test coverage for new warning system and cheese normalization

BREAKING CHANGE: AI trace warnings are now structured objects instead of simple strings
This commit is contained in:
Nils-Johan Gynther
2026-05-23 21:11:46 +02:00
parent 0fb507f247
commit d9f992ca9a
18 changed files with 1308 additions and 81 deletions
@@ -1,4 +1,13 @@
export type FlyerImportMatchVia = 'alias' | 'exact' | 'token' | 'none';
export type FlyerImportMatchVia = 'alias' | 'exact' | 'token' | 'none';
export type FlyerReasonDescriptor = {
code: string;
kind: 'parse' | 'match';
title: string;
message: string;
severity: 'info' | 'warning' | 'error';
location: string | null;
};
export type FlyerImportItem = {
flyerItemId: number | null;
@@ -18,14 +27,16 @@ export type FlyerImportItem = {
offerText: string | null;
isOffer: boolean;
offerLimitText: string | null;
parseConfidence: number;
parseReasons: string[];
matchedProductId: number | null;
matchedProductName: string | null;
matchedVia: FlyerImportMatchVia;
matchConfidence: number;
matchReasons: string[];
};
parseConfidence: number;
parseReasons: string[];
parseReasonsDetailed: FlyerReasonDescriptor[];
matchedProductId: number | null;
matchedProductName: string | null;
matchedVia: FlyerImportMatchVia;
matchConfidence: number;
matchReasons: string[];
matchReasonsDetailed: FlyerReasonDescriptor[];
};
export type FlyerImportResponse = {
sessionId: number | null;