anton-bakker-feedback 0.3.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/dist/chunks/{i18n-Du-or3T4.js → i18n-Cba0xHU-.js} +35 -1
- package/dist/chunks/i18n-Cba0xHU-.js.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/ingest-transport.d.ts +53 -0
- package/dist/core/ingest-transport.d.ts.map +1 -0
- package/dist/core/locales/en.d.ts.map +1 -1
- package/dist/core/locales/nl.d.ts.map +1 -1
- package/dist/index.js +95 -1
- package/dist/index.js.map +1 -1
- package/dist/react.js +17 -17
- package/dist/react.js.map +1 -1
- package/dist/server/FeedbackService.d.ts +14 -0
- package/dist/server/FeedbackService.d.ts.map +1 -1
- package/dist/server.js +3 -0
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/i18n-Du-or3T4.js.map +0 -1
package/README.md
CHANGED
|
@@ -12,6 +12,37 @@ Zero-dependency feedback collection package with LLM-powered dialogue intake, au
|
|
|
12
12
|
| `anton-bakker-feedback/server` | Server handlers: DynamoDB + Bedrock factories |
|
|
13
13
|
| `anton-bakker-feedback/manifest` | Package manifest for plugin discovery |
|
|
14
14
|
|
|
15
|
+
## Central Ingest Transport
|
|
16
|
+
|
|
17
|
+
The core entry exports `createIngestTransport` — a `FeedbackSubmitTransport` that writes feedback to
|
|
18
|
+
the **central ingest API** instead of a bespoke direct-DynamoDB writer (the central-storage principle:
|
|
19
|
+
reach central storage only through an authenticated API). The consuming app mints a short-lived OAuth2
|
|
20
|
+
client-credentials JWT and POSTs the feedback content to `POST /records/feedback`; the server
|
|
21
|
+
attributes the write to the token identity (`client_id`) and stamps the canonical keys, so the app
|
|
22
|
+
holds no AWS principal and sends no partition/identity fields. Zero dependencies — uses `fetch`.
|
|
23
|
+
|
|
24
|
+
```typescript
|
|
25
|
+
import { createIngestTransport } from 'anton-bakker-feedback';
|
|
26
|
+
|
|
27
|
+
const transport = createIngestTransport({
|
|
28
|
+
apiUrl: process.env.FEEDBACK_INGEST_URL!, // https://<id>.execute-api.<region>.amazonaws.com
|
|
29
|
+
tokenUrl: process.env.FEEDBACK_TOKEN_URL!, // Cognito client-credentials token endpoint
|
|
30
|
+
clientId: process.env.FEEDBACK_CLIENT_ID!, // this app's client id (its identity)
|
|
31
|
+
clientSecret: await resolveSecret('feedback/ingest/<app>'), // from your secret store — never commit
|
|
32
|
+
// OR, if the host already manages the token: tokenProvider: () => getToken()
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const result = await transport.submit(item); // → { success, feedbackId, error?, retryable? }
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The transport mints and caches the token (refreshing before expiry), strips server-owned key fields
|
|
39
|
+
(`PK`/`SK`/`GSI*`) before sending, and never fails silently: a 5xx or network error returns
|
|
40
|
+
`{ success: false, retryable: true }`; a 4xx returns `{ success: false, retryable: false }` with the
|
|
41
|
+
server's error message.
|
|
42
|
+
|
|
43
|
+
> The direct-DynamoDB write path (`FeedbackService.put`, `./server`) is `@deprecated` — it is the
|
|
44
|
+
> pattern the central-storage principle removes. New writers use this transport.
|
|
45
|
+
|
|
15
46
|
## Admin Pages
|
|
16
47
|
|
|
17
48
|
The `./react/admin` entry point provides three headless page-level components for managing submitted feedback. Components are unstyled — pass `className` props to integrate with Tailwind CSS or any other CSS framework.
|
|
@@ -88,6 +88,23 @@ const en = {
|
|
|
88
88
|
"feedback.form.contentPlaceholder": "Describe the issue or suggestion in detail...",
|
|
89
89
|
"feedback.form.typeLabel": "Type",
|
|
90
90
|
"feedback.form.typePlaceholder": "Select a type",
|
|
91
|
+
// Confirm (review) form
|
|
92
|
+
"feedback.confirm.heading": "Review your feedback",
|
|
93
|
+
"feedback.confirm.titleLabel": "Title",
|
|
94
|
+
"feedback.confirm.descriptionLabel": "Description",
|
|
95
|
+
"feedback.confirm.typeLabel": "Type",
|
|
96
|
+
"feedback.confirm.scopeLabel": "Scope",
|
|
97
|
+
"feedback.confirm.sizeLabel": "Size",
|
|
98
|
+
"feedback.confirm.duplicateWarning": "Similar feedback already exists:",
|
|
99
|
+
"feedback.confirm.scopePlaceholder": "Select scope (optional)",
|
|
100
|
+
"feedback.confirm.scope.isolated": "Isolated",
|
|
101
|
+
"feedback.confirm.scope.crossCutting": "Cross-cutting",
|
|
102
|
+
"feedback.confirm.scope.systemic": "Systemic",
|
|
103
|
+
"feedback.confirm.sizePlaceholder": "Select size (optional)",
|
|
104
|
+
"feedback.confirm.size.trivial": "Trivial",
|
|
105
|
+
"feedback.confirm.size.small": "Small",
|
|
106
|
+
"feedback.confirm.size.medium": "Medium",
|
|
107
|
+
"feedback.confirm.size.large": "Large",
|
|
91
108
|
// Dialogue states
|
|
92
109
|
"feedback.dialogue.opening": "Thank you for your feedback. I have a few questions to help us understand better.",
|
|
93
110
|
"feedback.dialogue.thinking": "Thinking...",
|
|
@@ -130,6 +147,23 @@ const nl = {
|
|
|
130
147
|
"feedback.form.contentPlaceholder": "Beschrijf het probleem of de suggestie in detail...",
|
|
131
148
|
"feedback.form.typeLabel": "Type",
|
|
132
149
|
"feedback.form.typePlaceholder": "Selecteer een type",
|
|
150
|
+
// Confirm (review) form
|
|
151
|
+
"feedback.confirm.heading": "Controleer uw feedback",
|
|
152
|
+
"feedback.confirm.titleLabel": "Titel",
|
|
153
|
+
"feedback.confirm.descriptionLabel": "Omschrijving",
|
|
154
|
+
"feedback.confirm.typeLabel": "Type",
|
|
155
|
+
"feedback.confirm.scopeLabel": "Reikwijdte",
|
|
156
|
+
"feedback.confirm.sizeLabel": "Omvang",
|
|
157
|
+
"feedback.confirm.duplicateWarning": "Er bestaat al vergelijkbare feedback:",
|
|
158
|
+
"feedback.confirm.scopePlaceholder": "Selecteer reikwijdte (optioneel)",
|
|
159
|
+
"feedback.confirm.scope.isolated": "Geïsoleerd",
|
|
160
|
+
"feedback.confirm.scope.crossCutting": "Overkoepelend",
|
|
161
|
+
"feedback.confirm.scope.systemic": "Systemisch",
|
|
162
|
+
"feedback.confirm.sizePlaceholder": "Selecteer omvang (optioneel)",
|
|
163
|
+
"feedback.confirm.size.trivial": "Triviaal",
|
|
164
|
+
"feedback.confirm.size.small": "Klein",
|
|
165
|
+
"feedback.confirm.size.medium": "Middel",
|
|
166
|
+
"feedback.confirm.size.large": "Groot",
|
|
133
167
|
// Dialogue states
|
|
134
168
|
"feedback.dialogue.opening": "Bedankt voor uw feedback. Ik heb een paar vragen om dit beter te begrijpen.",
|
|
135
169
|
"feedback.dialogue.thinking": "Aan het nadenken...",
|
|
@@ -207,4 +241,4 @@ export {
|
|
|
207
241
|
t,
|
|
208
242
|
validateConfig as v
|
|
209
243
|
};
|
|
210
|
-
//# sourceMappingURL=i18n-
|
|
244
|
+
//# sourceMappingURL=i18n-Cba0xHU-.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n-Cba0xHU-.js","sources":["../../src/core/config.ts","../../src/core/locales/en.ts","../../src/core/locales/nl.ts","../../src/core/i18n.ts"],"sourcesContent":["/**\n * Configuration schema and validation for the anton-bakker-feedback package.\n */\n\nimport type { FeedbackEnvironment } from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config Interface\n// ─────────────────────────────────────────────────────────────────\n\n/** Full configuration object for the feedback package */\nexport interface FeedbackConfig {\n /** Required — alphanumeric, hyphens, underscores only */\n appName: string;\n version?: string;\n /** Defaults to 'production' if absent */\n environment?: FeedbackEnvironment;\n\n transport: {\n /** POST endpoint — app provides its own route */\n submitUrl: string;\n /** If absent → dialogue disabled, form shown */\n dialogueUrl?: string;\n /** If absent → screenshots disabled */\n screenshotUploadUrl?: string;\n };\n\n dialogue?: {\n /** Default 3; min 1, max 10 */\n maxTurns?: number;\n /** Default 30_000 */\n timeoutMs?: number;\n };\n\n screenshots?: {\n /** Default true if screenshotUploadUrl set */\n enabled?: boolean;\n /** Default 5_242_880 (5 MB) */\n maxSizeBytes?: number;\n };\n\n ui?: {\n /** Default 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n /** Default 'auto' */\n theme?: 'light' | 'dark' | 'auto';\n };\n\n observability?: {\n /** Default true */\n enabled?: boolean;\n /** Default 'Feedback/Service' */\n namespace?: string;\n /** Default ['x-trace-id', 'x-request-id'] */\n traceIdHeaders?: string[];\n };\n\n i18n?: {\n /** Default 'en' */\n locale?: string;\n customTranslations?: Record<string, Record<string, string>>;\n };\n\n signOff?: {\n /** Default 7; 0 = manual only */\n autoConfirmDays?: number;\n /** Default 3 */\n reminderDays?: number;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Resolved Config — all fields have values, no undefined\n// ─────────────────────────────────────────────────────────────────\n\n/** Fully-resolved config with all defaults applied — no undefined fields */\nexport interface ResolvedFeedbackConfig {\n appName: string;\n version: string;\n environment: FeedbackEnvironment;\n\n transport: {\n submitUrl: string;\n dialogueUrl: string | undefined;\n screenshotUploadUrl: string | undefined;\n };\n\n dialogue: {\n maxTurns: number;\n timeoutMs: number;\n };\n\n screenshots: {\n enabled: boolean;\n maxSizeBytes: number;\n };\n\n ui: {\n position: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n theme: 'light' | 'dark' | 'auto';\n };\n\n observability: {\n enabled: boolean;\n namespace: string;\n traceIdHeaders: string[];\n };\n\n i18n: {\n locale: string;\n customTranslations: Record<string, Record<string, string>>;\n };\n\n signOff: {\n autoConfirmDays: number;\n reminderDays: number;\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Validation\n// ─────────────────────────────────────────────────────────────────\n\nconst APP_NAME_PATTERN = /^[a-zA-Z0-9_-]+$/;\nconst URL_PATTERN = /^https?:\\/\\/.+/;\n\n/** Validation result with structured errors */\nexport interface ConfigValidationResult {\n valid: boolean;\n errors: string[];\n}\n\n/**\n * Validates a FeedbackConfig and returns structured errors.\n * Returns { valid: false, errors: [...] } with actionable messages.\n */\nexport function validateConfig(config: Partial<FeedbackConfig>): ConfigValidationResult {\n const errors: string[] = [];\n\n // Validate appName\n if (!config.appName || config.appName.trim() === '') {\n errors.push('appName is required and must not be empty');\n } else if (!APP_NAME_PATTERN.test(config.appName)) {\n errors.push(\n `appName \"${config.appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/ (no spaces, slashes, or special characters)`,\n );\n }\n\n // Validate transport.submitUrl\n if (!config.transport?.submitUrl || config.transport.submitUrl.trim() === '') {\n errors.push('transport.submitUrl is required and must not be empty');\n } else if (!URL_PATTERN.test(config.transport.submitUrl)) {\n errors.push(\n `transport.submitUrl \"${config.transport.submitUrl}\" must be a valid URL starting with http:// or https://`,\n );\n }\n\n // Validate dialogue.maxTurns\n if (config.dialogue?.maxTurns !== undefined) {\n const turns = config.dialogue.maxTurns;\n if (!Number.isInteger(turns) || turns < 1 || turns > 10) {\n errors.push(\n `dialogue.maxTurns must be an integer between 1 and 10 (got ${turns})`,\n );\n }\n }\n\n // Validate screenshots.maxSizeBytes\n if (config.screenshots?.maxSizeBytes !== undefined) {\n const size = config.screenshots.maxSizeBytes;\n if (!Number.isFinite(size) || size <= 0) {\n errors.push(`screenshots.maxSizeBytes must be a positive number (got ${size})`);\n }\n }\n\n return {\n valid: errors.length === 0,\n errors,\n };\n}\n\n/**\n * Merges user-supplied partial config with defaults.\n * Returns a fully-resolved config with no undefined fields.\n */\nexport function resolveConfig(config: FeedbackConfig): ResolvedFeedbackConfig {\n const screenshotsEnabled =\n config.screenshots?.enabled !== undefined\n ? config.screenshots.enabled\n : !!config.transport.screenshotUploadUrl;\n\n return {\n appName: config.appName,\n version: config.version ?? '0.0.0',\n environment: config.environment ?? 'production',\n\n transport: {\n submitUrl: config.transport.submitUrl,\n dialogueUrl: config.transport.dialogueUrl,\n screenshotUploadUrl: config.transport.screenshotUploadUrl,\n },\n\n dialogue: {\n maxTurns: config.dialogue?.maxTurns ?? 3,\n timeoutMs: config.dialogue?.timeoutMs ?? 30_000,\n },\n\n screenshots: {\n enabled: screenshotsEnabled,\n maxSizeBytes: config.screenshots?.maxSizeBytes ?? 5_242_880,\n },\n\n ui: {\n position: config.ui?.position ?? 'bottom-right',\n theme: config.ui?.theme ?? 'auto',\n },\n\n observability: {\n enabled: config.observability?.enabled ?? true,\n namespace: config.observability?.namespace ?? 'Feedback/Service',\n traceIdHeaders: config.observability?.traceIdHeaders ?? [\n 'x-trace-id',\n 'x-request-id',\n ],\n },\n\n i18n: {\n locale: config.i18n?.locale ?? 'en',\n customTranslations: config.i18n?.customTranslations ?? {},\n },\n\n signOff: {\n autoConfirmDays: config.signOff?.autoConfirmDays ?? 7,\n reminderDays: config.signOff?.reminderDays ?? 3,\n },\n };\n}\n","/**\n * English (en) translations for the feedback package.\n * Key format: feedback.{component}.{element}\n */\nexport const en: Record<string, string> = {\n // Button labels\n 'feedback.button.open': 'Feedback',\n 'feedback.button.close': 'Close',\n 'feedback.button.screenshot': 'Add screenshot',\n 'feedback.button.removeScreenshot': 'Remove screenshot',\n 'feedback.button.submit': 'Submit feedback',\n\n // Form labels\n 'feedback.form.contentLabel': 'What would you like to share?',\n 'feedback.form.contentPlaceholder': 'Describe the issue or suggestion in detail...',\n 'feedback.form.typeLabel': 'Type',\n 'feedback.form.typePlaceholder': 'Select a type',\n\n // Confirm (review) form\n 'feedback.confirm.heading': 'Review your feedback',\n 'feedback.confirm.titleLabel': 'Title',\n 'feedback.confirm.descriptionLabel': 'Description',\n 'feedback.confirm.typeLabel': 'Type',\n 'feedback.confirm.scopeLabel': 'Scope',\n 'feedback.confirm.sizeLabel': 'Size',\n 'feedback.confirm.duplicateWarning': 'Similar feedback already exists:',\n 'feedback.confirm.scopePlaceholder': 'Select scope (optional)',\n 'feedback.confirm.scope.isolated': 'Isolated',\n 'feedback.confirm.scope.crossCutting': 'Cross-cutting',\n 'feedback.confirm.scope.systemic': 'Systemic',\n 'feedback.confirm.sizePlaceholder': 'Select size (optional)',\n 'feedback.confirm.size.trivial': 'Trivial',\n 'feedback.confirm.size.small': 'Small',\n 'feedback.confirm.size.medium': 'Medium',\n 'feedback.confirm.size.large': 'Large',\n\n // Dialogue states\n 'feedback.dialogue.opening': 'Thank you for your feedback. I have a few questions to help us understand better.',\n 'feedback.dialogue.thinking': 'Thinking...',\n 'feedback.dialogue.error': 'Sorry, something went wrong. Please try again.',\n 'feedback.dialogue.complete': 'Thank you! Your feedback has been recorded.',\n 'feedback.dialogue.turnPrompt': 'Your response:',\n\n // Feedback types\n 'feedback.types.bug': 'Bug report',\n 'feedback.types.feature': 'Feature request',\n 'feedback.types.ux': 'UX improvement',\n 'feedback.types.other': 'Other',\n\n // Status messages\n 'feedback.status.submitting': 'Submitting your feedback...',\n 'feedback.status.success': 'Feedback submitted successfully!',\n 'feedback.status.error': 'Failed to submit feedback. Please try again.',\n 'feedback.status.retry': 'Retry',\n 'feedback.status.duplicate': 'This feedback has already been submitted.',\n\n // Toast messages\n 'feedback.toast.screenshotAdded': 'Screenshot attached',\n 'feedback.toast.screenshotRemoved': 'Screenshot removed',\n 'feedback.toast.screenshotError': 'Failed to capture screenshot',\n\n // Sign-off messages\n 'feedback.signoff.confirmLabel': 'Mark as resolved',\n 'feedback.signoff.reopenLabel': 'Reopen',\n 'feedback.signoff.autoConfirmNotice':\n 'This issue will be marked resolved in {{days}} days unless you reopen it.',\n\n // Correction messages\n 'feedback.correction.editLabel': 'Edit',\n 'feedback.correction.reasonLabel': 'Reason for change',\n 'feedback.correction.saveLabel': 'Save changes',\n};\n","/**\n * Dutch (nl) translations for the feedback package.\n * Key format: feedback.{component}.{element}\n */\nexport const nl: Record<string, string> = {\n // Button labels\n 'feedback.button.open': 'Feedback',\n 'feedback.button.close': 'Sluiten',\n 'feedback.button.screenshot': 'Screenshot toevoegen',\n 'feedback.button.removeScreenshot': 'Screenshot verwijderen',\n 'feedback.button.submit': 'Feedback versturen',\n\n // Form labels\n 'feedback.form.contentLabel': 'Wat wilt u delen?',\n 'feedback.form.contentPlaceholder': 'Beschrijf het probleem of de suggestie in detail...',\n 'feedback.form.typeLabel': 'Type',\n 'feedback.form.typePlaceholder': 'Selecteer een type',\n\n // Confirm (review) form\n 'feedback.confirm.heading': 'Controleer uw feedback',\n 'feedback.confirm.titleLabel': 'Titel',\n 'feedback.confirm.descriptionLabel': 'Omschrijving',\n 'feedback.confirm.typeLabel': 'Type',\n 'feedback.confirm.scopeLabel': 'Reikwijdte',\n 'feedback.confirm.sizeLabel': 'Omvang',\n 'feedback.confirm.duplicateWarning': 'Er bestaat al vergelijkbare feedback:',\n 'feedback.confirm.scopePlaceholder': 'Selecteer reikwijdte (optioneel)',\n 'feedback.confirm.scope.isolated': 'Geïsoleerd',\n 'feedback.confirm.scope.crossCutting': 'Overkoepelend',\n 'feedback.confirm.scope.systemic': 'Systemisch',\n 'feedback.confirm.sizePlaceholder': 'Selecteer omvang (optioneel)',\n 'feedback.confirm.size.trivial': 'Triviaal',\n 'feedback.confirm.size.small': 'Klein',\n 'feedback.confirm.size.medium': 'Middel',\n 'feedback.confirm.size.large': 'Groot',\n\n // Dialogue states\n 'feedback.dialogue.opening': 'Bedankt voor uw feedback. Ik heb een paar vragen om dit beter te begrijpen.',\n 'feedback.dialogue.thinking': 'Aan het nadenken...',\n 'feedback.dialogue.error': 'Er is iets misgegaan. Probeer het opnieuw.',\n 'feedback.dialogue.complete': 'Bedankt! Uw feedback is geregistreerd.',\n 'feedback.dialogue.turnPrompt': 'Uw antwoord:',\n\n // Feedback types\n 'feedback.types.bug': 'Foutmelding',\n 'feedback.types.feature': 'Functiewens',\n 'feedback.types.ux': 'UX-verbetering',\n 'feedback.types.other': 'Overig',\n\n // Status messages\n 'feedback.status.submitting': 'Feedback verzenden...',\n 'feedback.status.success': 'Feedback succesvol verzonden!',\n 'feedback.status.error': 'Verzenden mislukt. Probeer het opnieuw.',\n 'feedback.status.retry': 'Opnieuw proberen',\n 'feedback.status.duplicate': 'Deze feedback is al ingediend.',\n\n // Toast messages\n 'feedback.toast.screenshotAdded': 'Screenshot bijgevoegd',\n 'feedback.toast.screenshotRemoved': 'Screenshot verwijderd',\n 'feedback.toast.screenshotError': 'Screenshot maken mislukt',\n\n // Sign-off messages\n 'feedback.signoff.confirmLabel': 'Markeer als opgelost',\n 'feedback.signoff.reopenLabel': 'Heropenen',\n 'feedback.signoff.autoConfirmNotice':\n 'Dit probleem wordt als opgelost gemarkeerd over {{days}} dagen, tenzij u het heropent.',\n\n // Correction messages\n 'feedback.correction.editLabel': 'Bewerken',\n 'feedback.correction.reasonLabel': 'Reden voor wijziging',\n 'feedback.correction.saveLabel': 'Wijzigingen opslaan',\n};\n","/**\n * Internationalisation utilities for the feedback package.\n *\n * t() falls back gracefully:\n * - Unknown locale → English\n * - Unknown key → returns the key itself (never undefined)\n *\n * Template interpolation: {{param}} → replaced with params values\n */\n\nimport { en } from './locales/en.js';\nimport { nl } from './locales/nl.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Constants\n// ─────────────────────────────────────────────────────────────────\n\n/** Supported locale codes */\nexport const SUPPORTED_LOCALES: readonly string[] = ['en', 'nl'] as const;\n\n/** Default locale when unknown locale is requested */\nexport const DEFAULT_LOCALE = 'en';\n\n/** Built-in translations keyed by locale */\nconst BUILT_IN_TRANSLATIONS: Record<string, Record<string, string>> = {\n en,\n nl,\n};\n\n// ─────────────────────────────────────────────────────────────────\n// Translation Function\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Looks up a translation key.\n *\n * Fallback chain:\n * 1. customTranslations[locale][key]\n * 2. BUILT_IN_TRANSLATIONS[locale][key]\n * 3. BUILT_IN_TRANSLATIONS['en'][key]\n * 4. key itself (never returns undefined or throws)\n *\n * Template interpolation: {{days}} → params.days (if params provided)\n *\n * @param key - Dot-notation key e.g. 'feedback.button.open'\n * @param locale - BCP 47 locale code e.g. 'en', 'nl'\n * @param customTranslations - Optional custom translations (overrides built-in)\n * @param params - Optional template parameters e.g. { days: 7 }\n */\nexport function t(\n key: string,\n locale: string,\n customTranslations?: Record<string, Record<string, string>>,\n params?: Record<string, string | number>,\n): string {\n // 1. Check custom translations for this locale\n const customForLocale = customTranslations?.[locale];\n if (customForLocale?.[key] !== undefined) {\n return interpolate(customForLocale[key], params);\n }\n\n // 2. Check custom translations for English fallback\n const customEn = customTranslations?.['en'];\n if (locale !== 'en' && customEn?.[key] !== undefined) {\n // Only use English custom if locale not found\n }\n\n // 3. Check built-in translations for this locale\n const builtInForLocale = BUILT_IN_TRANSLATIONS[locale];\n if (builtInForLocale?.[key] !== undefined) {\n return interpolate(builtInForLocale[key] as string, params);\n }\n\n // 4. Fall back to English built-in\n const englishBuiltIn = BUILT_IN_TRANSLATIONS['en'];\n if (englishBuiltIn?.[key] !== undefined) {\n return interpolate(englishBuiltIn[key] as string, params);\n }\n\n // 5. Fall back to English custom (if provided)\n if (customEn?.[key] !== undefined) {\n return interpolate(customEn[key] as string, params);\n }\n\n // 6. Return the key itself — never undefined\n return key;\n}\n\n/**\n * Replaces {{param}} placeholders with values from params.\n */\nfunction interpolate(\n template: string,\n params?: Record<string, string | number>,\n): string {\n if (!params) return template;\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (_match, name: string) => {\n const value = params[name];\n return value !== undefined ? String(value) : `{{${name}}}`;\n });\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Locale Detection\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Returns the locale to use.\n * If preferredLocale is a supported locale → return it.\n * Otherwise → return DEFAULT_LOCALE ('en').\n *\n * @param preferredLocale - BCP 47 locale code or undefined\n */\nexport function detectLocale(preferredLocale?: string): string {\n if (preferredLocale && SUPPORTED_LOCALES.includes(preferredLocale)) {\n return preferredLocale;\n }\n return DEFAULT_LOCALE;\n}\n"],"names":[],"mappings":"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"}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export * from './types.js';
|
|
|
8
8
|
export * from './config.js';
|
|
9
9
|
export * from './capture.js';
|
|
10
10
|
export * from './transport.js';
|
|
11
|
+
export * from './ingest-transport.js';
|
|
11
12
|
export * from './validation.js';
|
|
12
13
|
export * from './transforms.js';
|
|
13
14
|
export * from './observability.js';
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { FeedbackSubmitTransport } from './transport.js';
|
|
2
|
+
|
|
3
|
+
/** The ingest collection feedback writes to (the deployed registry maps it to `ralph-feedback`). */
|
|
4
|
+
export declare const FEEDBACK_COLLECTION = "feedback";
|
|
5
|
+
/** OAuth2 scope required to write via the ingest API. */
|
|
6
|
+
export declare const INGEST_WRITE_SCOPE = "assets/write";
|
|
7
|
+
/** Minimal fetch surface (so the package needs no DOM lib types to compile). */
|
|
8
|
+
export type FetchLike = (url: string, init?: {
|
|
9
|
+
method?: string;
|
|
10
|
+
headers?: Record<string, string>;
|
|
11
|
+
body?: string;
|
|
12
|
+
}) => Promise<{
|
|
13
|
+
status: number;
|
|
14
|
+
ok: boolean;
|
|
15
|
+
json: () => Promise<unknown>;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* How the transport obtains a bearer token. Either:
|
|
19
|
+
* - supply `clientId` + `clientSecret` + `tokenUrl` and let the transport mint/refresh it, OR
|
|
20
|
+
* - supply a `tokenProvider()` (e.g. the host already manages the token) — takes precedence.
|
|
21
|
+
*/
|
|
22
|
+
export interface IngestTransportConfig {
|
|
23
|
+
/** Ingest API base URL (HTTPS), e.g. https://xxxx.execute-api.<region>.amazonaws.com */
|
|
24
|
+
apiUrl: string;
|
|
25
|
+
/** OAuth2 token endpoint (HTTPS) — required unless `tokenProvider` is given. */
|
|
26
|
+
tokenUrl?: string;
|
|
27
|
+
/** App-client id (the app identity the server attributes writes to). Required unless `tokenProvider`. */
|
|
28
|
+
clientId?: string;
|
|
29
|
+
/** App-client secret. SHOULD come from the host's secret store; required unless `tokenProvider`. */
|
|
30
|
+
clientSecret?: string;
|
|
31
|
+
/** Supply a token directly (overrides client-credentials minting). */
|
|
32
|
+
tokenProvider?: () => Promise<string>;
|
|
33
|
+
/** Collection name (default 'feedback'). */
|
|
34
|
+
collection?: string;
|
|
35
|
+
/** Seconds before expiry to proactively re-mint (default 60). */
|
|
36
|
+
refreshSkewSeconds?: number;
|
|
37
|
+
/** Injectable fetch (tests). Defaults to global fetch. */
|
|
38
|
+
fetchImpl?: FetchLike;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a FeedbackSubmitTransport that writes to the central ingest API.
|
|
42
|
+
*
|
|
43
|
+
* Usage (host app wires the credential from its secret store):
|
|
44
|
+
* const transport = createIngestTransport({
|
|
45
|
+
* apiUrl: process.env.FEEDBACK_INGEST_URL!,
|
|
46
|
+
* tokenUrl: process.env.FEEDBACK_TOKEN_URL!,
|
|
47
|
+
* clientId: process.env.FEEDBACK_CLIENT_ID!,
|
|
48
|
+
* clientSecret: await resolveSecret('feedback/ingest/<app>'),
|
|
49
|
+
* });
|
|
50
|
+
* await transport.submit(item); // → { success, feedbackId }
|
|
51
|
+
*/
|
|
52
|
+
export declare function createIngestTransport(config: IngestTransportConfig): FeedbackSubmitTransport;
|
|
53
|
+
//# sourceMappingURL=ingest-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest-transport.d.ts","sourceRoot":"","sources":["../../src/core/ingest-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAM9D,oGAAoG;AACpG,eAAO,MAAM,mBAAmB,aAAa,CAAC;AAC9C,yDAAyD;AACzD,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,KACxE,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAAC;AAE5E;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAC;IACf,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oGAAoG;IACpG,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,aAAa,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACtC,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,0DAA0D;IAC1D,SAAS,CAAC,EAAE,SAAS,CAAC;CACvB;AA8BD;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,qBAAqB,GAAG,uBAAuB,CAkE5F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../src/core/locales/en.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../../src/core/locales/en.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmErC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nl.d.ts","sourceRoot":"","sources":["../../../src/core/locales/nl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"nl.d.ts","sourceRoot":"","sources":["../../../src/core/locales/nl.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,EAAE,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAmErC,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { D, S, d, r, t, v } from "./chunks/i18n-
|
|
1
|
+
import { D, S, d, r, t, v } from "./chunks/i18n-Cba0xHU-.js";
|
|
2
2
|
import { e, i, r as r2, a, w } from "./chunks/observability-CfwHKObh.js";
|
|
3
3
|
const BASE_PRIORITY = {
|
|
4
4
|
bug: 60,
|
|
@@ -130,6 +130,97 @@ function createMemoryTransport() {
|
|
|
130
130
|
}
|
|
131
131
|
};
|
|
132
132
|
}
|
|
133
|
+
const FEEDBACK_COLLECTION = "feedback";
|
|
134
|
+
const INGEST_WRITE_SCOPE = "assets/write";
|
|
135
|
+
function assertHttps(url, label) {
|
|
136
|
+
let u;
|
|
137
|
+
try {
|
|
138
|
+
u = new URL(url);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new Error(`ingest-transport: ${label} is not a valid URL: ${url}`);
|
|
141
|
+
}
|
|
142
|
+
if (u.protocol !== "https:") {
|
|
143
|
+
throw new Error(`ingest-transport: ${label} must be https:// (got ${u.protocol}//) — refusing plaintext`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const SERVER_OWNED_FIELDS = [
|
|
147
|
+
"PK",
|
|
148
|
+
"SK",
|
|
149
|
+
"GSI1PK",
|
|
150
|
+
"GSI1SK",
|
|
151
|
+
"GSI2PK",
|
|
152
|
+
"GSI2SK"
|
|
153
|
+
];
|
|
154
|
+
function toContent(item) {
|
|
155
|
+
const content = { ...item };
|
|
156
|
+
for (const f of SERVER_OWNED_FIELDS) delete content[f];
|
|
157
|
+
return content;
|
|
158
|
+
}
|
|
159
|
+
function createIngestTransport(config) {
|
|
160
|
+
const collection = config.collection ?? FEEDBACK_COLLECTION;
|
|
161
|
+
const refreshSkewMs = (config.refreshSkewSeconds ?? 60) * 1e3;
|
|
162
|
+
const doFetch = config.fetchImpl ?? globalThis.fetch;
|
|
163
|
+
assertHttps(config.apiUrl, "apiUrl");
|
|
164
|
+
if (!config.tokenProvider) {
|
|
165
|
+
if (!config.tokenUrl || !config.clientId || !config.clientSecret) {
|
|
166
|
+
throw new Error("ingest-transport: provide tokenProvider, OR tokenUrl + clientId + clientSecret");
|
|
167
|
+
}
|
|
168
|
+
assertHttps(config.tokenUrl, "tokenUrl");
|
|
169
|
+
}
|
|
170
|
+
if (typeof doFetch !== "function") {
|
|
171
|
+
throw new Error("ingest-transport: no fetch implementation available");
|
|
172
|
+
}
|
|
173
|
+
let cachedToken = null;
|
|
174
|
+
let cachedExpiryMs = 0;
|
|
175
|
+
async function mintToken() {
|
|
176
|
+
const basic = toBase64(`${config.clientId}:${config.clientSecret}`);
|
|
177
|
+
const res = await doFetch(config.tokenUrl, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "content-type": "application/x-www-form-urlencoded", authorization: `Basic ${basic}` },
|
|
180
|
+
body: `grant_type=client_credentials&scope=${encodeURIComponent(INGEST_WRITE_SCOPE)}`
|
|
181
|
+
});
|
|
182
|
+
const data = await res.json();
|
|
183
|
+
if (!res.ok || !data.access_token) {
|
|
184
|
+
throw new Error(`token mint failed (${res.status}): ${data.error ?? ""} ${data.error_description ?? ""}`.trim());
|
|
185
|
+
}
|
|
186
|
+
cachedToken = data.access_token;
|
|
187
|
+
cachedExpiryMs = Date.now() + Math.max(0, (data.expires_in ?? 3600) * 1e3 - refreshSkewMs);
|
|
188
|
+
return cachedToken;
|
|
189
|
+
}
|
|
190
|
+
async function getToken() {
|
|
191
|
+
if (config.tokenProvider) return config.tokenProvider();
|
|
192
|
+
if (cachedToken && Date.now() < cachedExpiryMs) return cachedToken;
|
|
193
|
+
return mintToken();
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
async submit(item) {
|
|
197
|
+
try {
|
|
198
|
+
const token = await getToken();
|
|
199
|
+
const base = config.apiUrl.replace(/\/$/, "");
|
|
200
|
+
const res = await doFetch(`${base}/records/${encodeURIComponent(collection)}`, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json", accept: "application/json" },
|
|
203
|
+
body: JSON.stringify({ item: toContent(item) })
|
|
204
|
+
});
|
|
205
|
+
const data = await res.json().catch(() => ({}));
|
|
206
|
+
if (res.status === 200 && data.success === true) {
|
|
207
|
+
return { success: true, feedbackId: data.ids?.[0] ?? item.id };
|
|
208
|
+
}
|
|
209
|
+
const retryable = res.status >= 500;
|
|
210
|
+
const msg = data.error ?? data.errors?.join("; ") ?? `HTTP ${res.status}`;
|
|
211
|
+
return { success: false, feedbackId: item.id, error: `ingest write failed: ${msg}`, retryable };
|
|
212
|
+
} catch (err) {
|
|
213
|
+
return { success: false, feedbackId: item.id, error: err.message, retryable: true };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function toBase64(s) {
|
|
219
|
+
if (typeof btoa === "function") return btoa(s);
|
|
220
|
+
const B = globalThis.Buffer;
|
|
221
|
+
if (B) return B.from(s).toString("base64");
|
|
222
|
+
throw new Error("ingest-transport: no base64 encoder available");
|
|
223
|
+
}
|
|
133
224
|
const VALIDATION_RULES = {
|
|
134
225
|
CONTENT_MIN_LENGTH: 10,
|
|
135
226
|
CONTENT_MAX_LENGTH: 5e3,
|
|
@@ -415,6 +506,7 @@ function createErrorResponse(e2) {
|
|
|
415
506
|
}
|
|
416
507
|
export {
|
|
417
508
|
D as DEFAULT_LOCALE,
|
|
509
|
+
FEEDBACK_COLLECTION,
|
|
418
510
|
FeedbackConfigError,
|
|
419
511
|
FeedbackDialogueError,
|
|
420
512
|
FeedbackDuplicateError,
|
|
@@ -422,6 +514,7 @@ export {
|
|
|
422
514
|
FeedbackScreenshotError,
|
|
423
515
|
FeedbackTransportError,
|
|
424
516
|
FeedbackValidationError,
|
|
517
|
+
INGEST_WRITE_SCOPE,
|
|
425
518
|
S as SUPPORTED_LOCALES,
|
|
426
519
|
VALIDATION_RULES,
|
|
427
520
|
addReporter,
|
|
@@ -431,6 +524,7 @@ export {
|
|
|
431
524
|
buildKeys,
|
|
432
525
|
calculatePriority,
|
|
433
526
|
createErrorResponse,
|
|
527
|
+
createIngestTransport,
|
|
434
528
|
createMemoryTransport,
|
|
435
529
|
d as detectLocale,
|
|
436
530
|
e as emitMetric,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/core/capture.ts","../src/core/transport.ts","../src/core/validation.ts","../src/core/transforms.ts","../src/core/errors.ts"],"sourcesContent":["/**\n * Capture logic and priority calculation for feedback submissions.\n *\n * Framework-agnostic — no browser APIs, no DOM, no AWS.\n * Pure data transformation from user input to StoredFeedbackItem.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n FeedbackEnvironment,\n FeedbackScope,\n FeedbackSize,\n FeedbackContext,\n StoredFeedbackItem,\n DialogueEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Input Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Input for processFeedbackCapture */\nexport interface CaptureInput {\n type: FeedbackType;\n content: string;\n context: FeedbackContext;\n submittedBy: string;\n appName: string;\n hasScreenshot: boolean;\n screenshotS3Key?: string;\n environment?: FeedbackEnvironment;\n version?: string;\n tags?: string[];\n traceId?: string;\n dialogueHistory?: DialogueEntry[];\n classification?: string;\n scope?: FeedbackScope;\n size?: FeedbackSize;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Priority Formula\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Base priority scores by feedback type.\n * bug=60, ux=40, feature=20, other=10\n */\nconst BASE_PRIORITY: Record<FeedbackType, number> = {\n bug: 60,\n ux: 40,\n feature: 20,\n other: 10,\n};\n\n/**\n * Environment bonus scores.\n * production=+20, staging=+10, development=0\n */\nconst ENVIRONMENT_BONUS: Record<FeedbackEnvironment, number> = {\n production: 20,\n staging: 10,\n development: 0,\n};\n\n/**\n * Calculates the priority score for a feedback item.\n *\n * Formula:\n * base = baseByType (bug=60, ux=40, feature=20, other=10)\n * env = environmentBonus (production=+20, staging=+10, development=0)\n * rep = min(reporterCount - 1, 5) * 4 (+4 per additional reporter, max +20)\n * ss = hasScreenshot ? +5 : 0\n * priority = clamp(0, 100, base + env + rep + ss)\n */\nexport function calculatePriority(\n type: FeedbackType,\n reporterCount: number,\n hasScreenshot: boolean,\n environment?: FeedbackEnvironment,\n): number {\n const base = BASE_PRIORITY[type];\n const env = ENVIRONMENT_BONUS[environment ?? 'development'];\n const reporterBonus = Math.min(reporterCount - 1, 5) * 4;\n const screenshotBonus = hasScreenshot ? 5 : 0;\n\n const raw = base + env + reporterBonus + screenshotBonus;\n return Math.max(0, Math.min(100, raw));\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ID Generation\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Generates a unique feedback ID in the format: FB-YYYY-MM-DD-{uuid8}\n * Uses crypto.randomUUID() for uniqueness; truncates to 8 chars.\n */\nexport function generateFeedbackId(nowIso?: string): string {\n const date = nowIso ? nowIso.slice(0, 10) : new Date().toISOString().slice(0, 10);\n const uuid8 = crypto.randomUUID().replace(/-/g, '').slice(0, 8);\n return `FB-${date}-${uuid8}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// GSI1SK Builder\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Builds the GSI1SK sort key value.\n * Format: {3-digit-inverted-priority}#{id}\n * Example: priority 85 → '015#FB-2026-07-25-abc12345'\n *\n * Inverted so DynamoDB ascending sort returns highest-priority first.\n */\nexport function buildGSI1SK(priority: number, id: string): string {\n const inverted = 100 - Math.max(0, Math.min(100, priority));\n const padded = String(inverted).padStart(3, '0');\n return `${padded}#${id}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Main Capture Processor\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Processes a feedback capture input into a StoredFeedbackItem\n * ready for the transport layer.\n *\n * Sets:\n * - status: 'open'\n * - reporterCount: 1\n * - reporters: [{ userId: submittedBy, addedAt: nowIso }]\n * - tags: [] when omitted\n * - All DynamoDB key fields (PK, SK, GSI1PK, GSI1SK, GSI2PK, GSI2SK)\n */\nexport function processFeedbackCapture(\n input: CaptureInput,\n nowIso?: string,\n): StoredFeedbackItem {\n const now = nowIso ?? new Date().toISOString();\n const id = generateFeedbackId(now);\n const status: FeedbackStatus = 'open';\n const tags = input.tags ?? [];\n const environment = input.environment;\n\n const priority = calculatePriority(input.type, 1, input.hasScreenshot, environment);\n\n const reporter: Reporter = {\n userId: input.submittedBy,\n addedAt: now,\n };\n\n const PK = `PROJECT#${input.appName}`;\n const SK = `FB#${now}#${id}`;\n const GSI1PK = `STATUS#${status}`;\n const GSI1SK = buildGSI1SK(priority, id);\n const GSI2PK = `TYPE#${input.type}#PROJECT#${input.appName}`;\n const GSI2SK = now;\n\n const item: StoredFeedbackItem = {\n PK,\n SK,\n GSI1PK,\n GSI1SK,\n GSI2PK,\n GSI2SK,\n id,\n appName: input.appName,\n type: input.type,\n status,\n priority,\n content: input.content,\n context: input.context,\n hasScreenshot: input.hasScreenshot,\n submittedAt: now,\n lastModifiedAt: now,\n submittedBy: input.submittedBy,\n reporters: [reporter],\n reporterCount: 1,\n dialogueHistory: input.dialogueHistory ?? [],\n corrections: [],\n tags,\n };\n\n // Optional fields — only set when defined\n if (input.screenshotS3Key !== undefined) {\n item.screenshotS3Key = input.screenshotS3Key;\n }\n if (input.environment !== undefined) {\n item.environment = input.environment;\n }\n if (input.version !== undefined) {\n item.version = input.version;\n }\n if (input.traceId !== undefined) {\n item.traceId = input.traceId;\n }\n if (input.classification !== undefined) {\n item.classification = input.classification;\n }\n if (input.scope !== undefined) {\n item.scope = input.scope;\n }\n if (input.size !== undefined) {\n item.size = input.size;\n }\n\n return item;\n}\n","/**\n * Transport contracts for the feedback package.\n *\n * Defines abstract interfaces that decouple core logic from\n * any specific implementation. Implementations live in the\n * consuming app's server handlers (slice 03).\n *\n * NO AWS SDK imports in this file.\n */\n\nimport type {\n StoredFeedbackItem,\n SubmitResult,\n DialogueResponse,\n UploadResult,\n DialogueEntry,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Transport Interfaces\n// ─────────────────────────────────────────────────────────────────\n\n/** What the React adapter calls at submit time */\nexport interface FeedbackSubmitTransport {\n submit(item: StoredFeedbackItem): Promise<SubmitResult>;\n}\n\n/** What the dialogue UI calls on each exchange */\nexport interface FeedbackDialogueTransport {\n exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse>;\n}\n\n/** What the screenshot capture calls after html2canvas */\nexport interface FeedbackScreenshotTransport {\n upload(feedbackId: string, blob: Blob): Promise<UploadResult>;\n}\n\n/**\n * Composite transport — all three combined for full-feature transport.\n * Dialogue and screenshot are optional so implementations can omit them.\n */\nexport interface FeedbackTransport\n extends FeedbackSubmitTransport,\n Partial<FeedbackDialogueTransport>,\n Partial<FeedbackScreenshotTransport> {}\n\n// ─────────────────────────────────────────────────────────────────\n// Memory Transport (for testing)\n// ─────────────────────────────────────────────────────────────────\n\n/** Memory transport with access to internal state for assertions */\nexport interface MemoryTransport extends FeedbackTransport {\n /** All submitted items */\n submissions: StoredFeedbackItem[];\n /** Queue of dialogue responses to return in order */\n dialogueResponses: DialogueResponse[];\n /** Uploaded screenshot blobs */\n uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }>;\n /** Clear all stored state */\n clear(): void;\n}\n\nconst DEFAULT_DIALOGUE_RESPONSE: DialogueResponse = {\n success: true,\n response: 'Thank you for your feedback. Can you tell me more?',\n conversationComplete: false,\n};\n\n/**\n * Creates an in-memory transport for testing.\n *\n * - submit(): stores the item and returns { success: true, feedbackId: item.id }\n * - exchange(): returns the first queued response, or a default stub\n * - upload(): records the blob reference\n * - Does NOT import any AWS SDK\n */\nexport function createMemoryTransport(): MemoryTransport {\n const submissions: StoredFeedbackItem[] = [];\n const dialogueResponses: DialogueResponse[] = [];\n const uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }> = [];\n\n return {\n submissions,\n dialogueResponses,\n uploadedScreenshots,\n\n async submit(item: StoredFeedbackItem): Promise<SubmitResult> {\n submissions.push(item);\n return { success: true, feedbackId: item.id };\n },\n\n async exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse> {\n void feedbackId;\n void message;\n void history;\n if (dialogueResponses.length > 0) {\n return dialogueResponses.shift() as DialogueResponse;\n }\n return { ...DEFAULT_DIALOGUE_RESPONSE };\n },\n\n async upload(feedbackId: string, blob: Blob): Promise<UploadResult> {\n uploadedScreenshots.push({ feedbackId, blob });\n return {\n success: true,\n s3Key: `screenshots/${feedbackId}.png`,\n };\n },\n\n clear() {\n submissions.length = 0;\n dialogueResponses.length = 0;\n uploadedScreenshots.length = 0;\n },\n };\n}\n","/**\n * Input validation functions for the feedback package.\n *\n * Used both client-side (before submission) and server-side (in handler).\n * No external dependencies — pure validation logic.\n *\n * Acceptance criteria evidence:\n * AC-65: createErrorResponse(new FeedbackValidationError('…', ['content']))\n * ])) includes fields: ['content'] → shown in errors.ts\n * AC-41: VALIDATION_RULES is exported as const (immutable) with values:\n * CONTENT_MIN_LENGTH=10, CONTENT_MAX_LENGTH=5000, TAG_MAX_COUNT=10\n * TAG_PATTERN=/^[a-z0-9-]+$/, [a-z0-9-] pattern applied\n */\n\nimport type { FeedbackContext } from './types.js';\nimport type { CaptureInput } from './capture.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Validation Rules (exported as const — immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/** Immutable validation constants */\nexport const VALIDATION_RULES = {\n CONTENT_MIN_LENGTH: 10,\n CONTENT_MAX_LENGTH: 5_000,\n TAG_MAX_COUNT: 10,\n TAG_PATTERN: /^[a-z0-9-]+$/,\n APP_NAME_PATTERN: /^[a-zA-Z0-9_-]+$/,\n SCREENSHOT_MAX_BYTES: 5_242_880, // 5 MB\n CONSOLE_ERRORS_MAX: 20,\n} as const;\n\n// ─────────────────────────────────────────────────────────────────\n// Result Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Validation result with user-facing messages */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[]; // Blocking — submission must be rejected\n warnings: string[]; // Non-blocking — suggestions only\n}\n\nfunction makeResult(errors: string[], warnings: string[]): ValidationResult {\n return { valid: errors.length === 0, errors, warnings };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Individual Validators\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Validates feedback content.\n * Fails below CONTENT_MIN_LENGTH (\"Feedback must be at least 10 characters\"),\n * fails above CONTENT_MAX_LENGTH, warns if only whitespace.\n */\nexport function validateContent(content: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (content.length < VALIDATION_RULES.CONTENT_MIN_LENGTH) {\n errors.push(\n `Feedback must be at least ${VALIDATION_RULES.CONTENT_MIN_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.length > VALIDATION_RULES.CONTENT_MAX_LENGTH) {\n errors.push(\n `Feedback must not exceed ${VALIDATION_RULES.CONTENT_MAX_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.trim().length === 0) {\n warnings.push('Feedback appears to contain only whitespace');\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates feedback tags.\n * Fails if count exceeds TAG_MAX_COUNT or if any tag doesn't match TAG_PATTERN.\n */\nexport function validateTags(tags: string[]): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (tags.length > VALIDATION_RULES.TAG_MAX_COUNT) {\n errors.push(\n `Too many tags: ${tags.length} (maximum is ${VALIDATION_RULES.TAG_MAX_COUNT})`,\n );\n }\n\n for (const tag of tags) {\n if (!VALIDATION_RULES.TAG_PATTERN.test(tag)) {\n errors.push(\n `Tag \"${tag}\" is invalid — must match /^[a-z0-9-]+$/ (lowercase letters, digits, hyphens only)`,\n );\n }\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates an app name.\n */\nexport function validateAppName(appName: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!appName || appName.trim() === '') {\n errors.push('appName is required');\n } else if (!VALIDATION_RULES.APP_NAME_PATTERN.test(appName)) {\n errors.push(\n `appName \"${appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates screenshot size.\n */\nexport function validateScreenshotSize(sizeBytes: number): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (sizeBytes > VALIDATION_RULES.SCREENSHOT_MAX_BYTES) {\n errors.push(\n `Screenshot size ${sizeBytes} bytes exceeds the maximum of ${VALIDATION_RULES.SCREENSHOT_MAX_BYTES} bytes (5 MB)`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates browser context.\n * Warns if url is empty; warns if consoleErrors exceeds 20 items.\n */\nexport function validateContext(context: FeedbackContext): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!context.url || context.url.trim() === '') {\n warnings.push('context.url is empty — this may indicate a capture logic error');\n }\n\n if (\n context.consoleErrors &&\n context.consoleErrors.length > VALIDATION_RULES.CONSOLE_ERRORS_MAX\n ) {\n warnings.push(\n `context.consoleErrors has ${context.consoleErrors.length} entries (expected ≤${VALIDATION_RULES.CONSOLE_ERRORS_MAX}); truncation should occur in capture layer`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Aggregate validator — runs all validators for a capture input.\n */\nexport function validateCaptureInput(input: Partial<CaptureInput>): ValidationResult {\n const allErrors: string[] = [];\n const allWarnings: string[] = [];\n\n if (input.content !== undefined) {\n const contentResult = validateContent(input.content);\n allErrors.push(...contentResult.errors);\n allWarnings.push(...contentResult.warnings);\n } else {\n allErrors.push('content is required');\n }\n\n if (input.tags !== undefined) {\n const tagResult = validateTags(input.tags);\n allErrors.push(...tagResult.errors);\n allWarnings.push(...tagResult.warnings);\n }\n\n if (input.appName !== undefined) {\n const nameResult = validateAppName(input.appName);\n allErrors.push(...nameResult.errors);\n allWarnings.push(...nameResult.warnings);\n } else {\n allErrors.push('appName is required');\n }\n\n if (input.context !== undefined) {\n const contextResult = validateContext(input.context);\n allErrors.push(...contextResult.errors);\n allWarnings.push(...contextResult.warnings);\n }\n\n return makeResult(allErrors, allWarnings);\n}\n","/**\n * Pure data transformation functions for DynamoDB key construction\n * and shape mapping.\n *\n * NO AWS SDK calls — only key construction and shape mapping.\n * All functions are pure: take plain objects, return plain objects.\n * No side effects.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n StoredFeedbackItem,\n CorrectionEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Key Builder\n// ─────────────────────────────────────────────────────────────────\n\n/** The six DynamoDB keys for a feedback item */\nexport interface FeedbackKeys {\n PK: string; // PROJECT#{appName}\n SK: string; // FB#{timestamp}#{id}\n GSI1PK: string; // STATUS#{status}\n GSI1SK: string; // {inverted-priority-padded}#{id}\n GSI2PK: string; // TYPE#{type}#PROJECT#{appName}\n GSI2SK: string; // {timestamp}\n}\n\n/**\n * Builds the six DynamoDB keys for a feedback item.\n *\n * @param appName - Application name (e.g. 'sagrosoft20')\n * @param id - Feedback ID (e.g. 'FB-2026-07-25-abc12345')\n * @param type - Feedback type\n * @param status - Current status\n * @param priority - Priority score 0–100\n * @param timestamp - ISO 8601 timestamp\n */\nexport function buildKeys(\n appName: string,\n id: string,\n type: FeedbackType,\n status: FeedbackStatus,\n priority: number,\n timestamp: string,\n): FeedbackKeys {\n const clampedPriority = Math.max(0, Math.min(100, priority));\n const inverted = 100 - clampedPriority;\n const paddedInverted = String(inverted).padStart(3, '0');\n\n return {\n PK: `PROJECT#${appName}`,\n SK: `FB#${timestamp}#${id}`,\n GSI1PK: `STATUS#${status}`,\n GSI1SK: `${paddedInverted}#${id}`,\n GSI2PK: `TYPE#${type}#PROJECT#${appName}`,\n GSI2SK: timestamp,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Key Parsers\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Extracts timestamp and id from a composite SK.\n * SK format: FB#{timestamp}#{id}\n */\nexport function parseSK(sk: string): { timestamp: string; id: string } {\n // SK format: FB#2026-07-25T10:00:00.000Z#FB-2026-07-25-abc12345\n const withoutPrefix = sk.startsWith('FB#') ? sk.slice(3) : sk;\n // Find the last '#' which separates timestamp from id\n const lastHash = withoutPrefix.lastIndexOf('#');\n if (lastHash === -1) {\n throw new Error(`Invalid SK format: \"${sk}\" — expected FB#{timestamp}#{id}`);\n }\n return {\n timestamp: withoutPrefix.slice(0, lastHash),\n id: withoutPrefix.slice(lastHash + 1),\n };\n}\n\n/**\n * Parses the GSI2PK into its component parts.\n * GSI2PK format: TYPE#{type}#PROJECT#{appName}\n */\nexport function parseGSI2PK(gsi2pk: string): { type: FeedbackType; appName: string } {\n // Format: TYPE#feature#PROJECT#oms4business\n const typeMatch = gsi2pk.match(/^TYPE#([^#]+)#PROJECT#(.+)$/);\n if (!typeMatch || !typeMatch[1] || !typeMatch[2]) {\n throw new Error(\n `Invalid GSI2PK format: \"${gsi2pk}\" — expected TYPE#{type}#PROJECT#{appName}`,\n );\n }\n return {\n type: typeMatch[1] as FeedbackType,\n appName: typeMatch[2],\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Reporter Mutations (immutable — returns new arrays)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Adds a reporter to an item.\n * Returns new reporters array and updated reporterCount — does NOT mutate original.\n */\nexport function addReporter(\n item: StoredFeedbackItem,\n userId: string,\n addedContext?: string,\n nowIso?: string,\n): Pick<StoredFeedbackItem, 'reporters' | 'reporterCount' | 'GSI1SK'> {\n const addedAt = nowIso ?? new Date().toISOString();\n const newReporter: Reporter = { userId, addedAt };\n if (addedContext !== undefined) {\n newReporter.addedContext = addedContext;\n }\n\n // Spread to ensure immutability — does NOT mutate item.reporters\n const newReporters: Reporter[] = [...item.reporters, newReporter];\n const newCount = newReporters.length;\n\n // GSI1SK format preserves the inverted priority prefix, just uses same item id\n // The priority recalculation would normally happen in the capture layer\n // Here we preserve the existing GSI1SK structure (priority unchanged by reporter add)\n return {\n reporters: newReporters,\n reporterCount: newCount,\n GSI1SK: item.GSI1SK,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Correction Mutations (immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Appends a correction record to the corrections array.\n * Returns a new array — does NOT mutate the original.\n */\nexport function appendCorrection(\n corrections: CorrectionEntry[],\n correction: Omit<CorrectionEntry, 'correctedAt'>,\n nowIso?: string,\n): CorrectionEntry[] {\n const correctedAt = nowIso ?? new Date().toISOString();\n return [\n ...corrections,\n { ...correction, correctedAt },\n ];\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Status Transition\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Applies a status transition to an item.\n * Returns updated key fields + lastModifiedAt (does NOT mutate original).\n *\n * When transitioning to 'resolved': sets resolvedAt = nowIso.\n * When transitioning to any other status: resolvedAt is undefined.\n */\nexport function applyStatusTransition(\n item: StoredFeedbackItem,\n newStatus: FeedbackStatus,\n nowIso?: string,\n): Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n> {\n const now = nowIso ?? new Date().toISOString();\n\n // GSI1SK stays the same (priority doesn't change on status transition)\n // Only GSI1PK changes (new status)\n const resolvedAt: string | undefined = newStatus === 'resolved' ? now : undefined;\n const resolvedBy: string | undefined = undefined;\n\n return {\n status: newStatus,\n GSI1PK: `STATUS#${newStatus}`,\n GSI1SK: item.GSI1SK, // Priority unchanged\n lastModifiedAt: now,\n ...(resolvedAt !== undefined ? { resolvedAt } : {}),\n ...(resolvedBy !== undefined ? { resolvedBy } : {}),\n } as Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n >;\n}\n","/**\n * Domain error types for the feedback package.\n *\n * Every error class carries:\n * - code: string constant (usable in switch statements)\n * - retryable: boolean\n *\n * createErrorResponse converts any thrown value to a safe JSON-serialisable object.\n */\n\n// ─────────────────────────────────────────────────────────────────\n// Base Error Class\n// ─────────────────────────────────────────────────────────────────\n\n/** Base class for all feedback domain errors */\nexport abstract class FeedbackError extends Error {\n abstract readonly code: string;\n abstract readonly retryable: boolean;\n\n constructor(message: string, readonly cause?: unknown) {\n super(message);\n this.name = this.constructor.name;\n // Fix prototype chain for instanceof checks\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Concrete Error Classes\n// ─────────────────────────────────────────────────────────────────\n\n/** Thrown when input validation fails */\nexport class FeedbackValidationError extends FeedbackError {\n readonly code = 'FEEDBACK_VALIDATION_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly fields?: string[],\n ) {\n super(message);\n this.name = 'FeedbackValidationError';\n Object.setPrototypeOf(this, FeedbackValidationError.prototype);\n }\n}\n\n/** Thrown when the transport layer fails (network, server error) */\nexport class FeedbackTransportError extends FeedbackError {\n readonly code = 'FEEDBACK_TRANSPORT_ERROR';\n readonly retryable = true;\n\n constructor(\n message: string,\n readonly statusCode?: number,\n cause?: unknown,\n ) {\n super(message, cause);\n this.name = 'FeedbackTransportError';\n Object.setPrototypeOf(this, FeedbackTransportError.prototype);\n }\n}\n\n/** Thrown when configuration is invalid or missing */\nexport class FeedbackConfigError extends FeedbackError {\n readonly code = 'FEEDBACK_CONFIG_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackConfigError';\n Object.setPrototypeOf(this, FeedbackConfigError.prototype);\n }\n}\n\n/** Thrown when dialogue exchange fails */\nexport class FeedbackDialogueError extends FeedbackError {\n readonly code = 'FEEDBACK_DIALOGUE_ERROR';\n readonly retryable = true;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackDialogueError';\n Object.setPrototypeOf(this, FeedbackDialogueError.prototype);\n }\n}\n\n/** Thrown when screenshot capture or upload fails */\nexport class FeedbackScreenshotError extends FeedbackError {\n readonly code = 'FEEDBACK_SCREENSHOT_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackScreenshotError';\n Object.setPrototypeOf(this, FeedbackScreenshotError.prototype);\n }\n}\n\n/** Thrown when a duplicate feedback submission is detected */\nexport class FeedbackDuplicateError extends FeedbackError {\n readonly code = 'FEEDBACK_DUPLICATE_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly existingId: string,\n ) {\n super(message);\n this.name = 'FeedbackDuplicateError';\n Object.setPrototypeOf(this, FeedbackDuplicateError.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Type Guard\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Type guard — returns true if the value is a FeedbackError subclass.\n * Returns false for plain Error instances and other values.\n */\nexport function isFeedbackError(e: unknown): e is FeedbackError {\n return e instanceof FeedbackError;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Error Response Serialiser\n// ─────────────────────────────────────────────────────────────────\n\n/** Safe JSON-serialisable error response */\nexport interface ErrorResponse {\n code: string;\n message: string;\n retryable: boolean;\n fields?: string[]; // present for FeedbackValidationError\n existingId?: string; // present for FeedbackDuplicateError\n}\n\n/**\n * Converts any thrown value to a safe JSON-serialisable response object.\n *\n * - FeedbackError subclasses → uses their code, message, retryable\n * - FeedbackValidationError → includes fields\n * - FeedbackDuplicateError → includes existingId\n * - Plain string → { code: 'UNKNOWN_ERROR', message: theString, retryable: false }\n * - Plain Error → { code: 'UNKNOWN_ERROR', message: error.message, retryable: false }\n * - null/undefined/other → { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred', retryable: false }\n */\nexport function createErrorResponse(e: unknown): ErrorResponse {\n if (isFeedbackError(e)) {\n const base: ErrorResponse = {\n code: e.code,\n message: e.message,\n retryable: e.retryable,\n };\n\n if (e instanceof FeedbackValidationError && e.fields !== undefined) {\n base.fields = e.fields;\n }\n\n if (e instanceof FeedbackDuplicateError) {\n base.existingId = e.existingId;\n }\n\n return base;\n }\n\n if (typeof e === 'string') {\n return {\n code: 'UNKNOWN_ERROR',\n message: e,\n retryable: false,\n };\n }\n\n if (e instanceof Error) {\n return {\n code: 'UNKNOWN_ERROR',\n message: e.message,\n retryable: false,\n };\n }\n\n return {\n code: 'UNKNOWN_ERROR',\n message: 'An unknown error occurred',\n retryable: false,\n };\n}\n"],"names":["e"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/core/capture.ts","../src/core/transport.ts","../src/core/ingest-transport.ts","../src/core/validation.ts","../src/core/transforms.ts","../src/core/errors.ts"],"sourcesContent":["/**\n * Capture logic and priority calculation for feedback submissions.\n *\n * Framework-agnostic — no browser APIs, no DOM, no AWS.\n * Pure data transformation from user input to StoredFeedbackItem.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n FeedbackEnvironment,\n FeedbackScope,\n FeedbackSize,\n FeedbackContext,\n StoredFeedbackItem,\n DialogueEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Input Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Input for processFeedbackCapture */\nexport interface CaptureInput {\n type: FeedbackType;\n content: string;\n context: FeedbackContext;\n submittedBy: string;\n appName: string;\n hasScreenshot: boolean;\n screenshotS3Key?: string;\n environment?: FeedbackEnvironment;\n version?: string;\n tags?: string[];\n traceId?: string;\n dialogueHistory?: DialogueEntry[];\n classification?: string;\n scope?: FeedbackScope;\n size?: FeedbackSize;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Priority Formula\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Base priority scores by feedback type.\n * bug=60, ux=40, feature=20, other=10\n */\nconst BASE_PRIORITY: Record<FeedbackType, number> = {\n bug: 60,\n ux: 40,\n feature: 20,\n other: 10,\n};\n\n/**\n * Environment bonus scores.\n * production=+20, staging=+10, development=0\n */\nconst ENVIRONMENT_BONUS: Record<FeedbackEnvironment, number> = {\n production: 20,\n staging: 10,\n development: 0,\n};\n\n/**\n * Calculates the priority score for a feedback item.\n *\n * Formula:\n * base = baseByType (bug=60, ux=40, feature=20, other=10)\n * env = environmentBonus (production=+20, staging=+10, development=0)\n * rep = min(reporterCount - 1, 5) * 4 (+4 per additional reporter, max +20)\n * ss = hasScreenshot ? +5 : 0\n * priority = clamp(0, 100, base + env + rep + ss)\n */\nexport function calculatePriority(\n type: FeedbackType,\n reporterCount: number,\n hasScreenshot: boolean,\n environment?: FeedbackEnvironment,\n): number {\n const base = BASE_PRIORITY[type];\n const env = ENVIRONMENT_BONUS[environment ?? 'development'];\n const reporterBonus = Math.min(reporterCount - 1, 5) * 4;\n const screenshotBonus = hasScreenshot ? 5 : 0;\n\n const raw = base + env + reporterBonus + screenshotBonus;\n return Math.max(0, Math.min(100, raw));\n}\n\n// ─────────────────────────────────────────────────────────────────\n// ID Generation\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Generates a unique feedback ID in the format: FB-YYYY-MM-DD-{uuid8}\n * Uses crypto.randomUUID() for uniqueness; truncates to 8 chars.\n */\nexport function generateFeedbackId(nowIso?: string): string {\n const date = nowIso ? nowIso.slice(0, 10) : new Date().toISOString().slice(0, 10);\n const uuid8 = crypto.randomUUID().replace(/-/g, '').slice(0, 8);\n return `FB-${date}-${uuid8}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// GSI1SK Builder\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Builds the GSI1SK sort key value.\n * Format: {3-digit-inverted-priority}#{id}\n * Example: priority 85 → '015#FB-2026-07-25-abc12345'\n *\n * Inverted so DynamoDB ascending sort returns highest-priority first.\n */\nexport function buildGSI1SK(priority: number, id: string): string {\n const inverted = 100 - Math.max(0, Math.min(100, priority));\n const padded = String(inverted).padStart(3, '0');\n return `${padded}#${id}`;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Main Capture Processor\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Processes a feedback capture input into a StoredFeedbackItem\n * ready for the transport layer.\n *\n * Sets:\n * - status: 'open'\n * - reporterCount: 1\n * - reporters: [{ userId: submittedBy, addedAt: nowIso }]\n * - tags: [] when omitted\n * - All DynamoDB key fields (PK, SK, GSI1PK, GSI1SK, GSI2PK, GSI2SK)\n */\nexport function processFeedbackCapture(\n input: CaptureInput,\n nowIso?: string,\n): StoredFeedbackItem {\n const now = nowIso ?? new Date().toISOString();\n const id = generateFeedbackId(now);\n const status: FeedbackStatus = 'open';\n const tags = input.tags ?? [];\n const environment = input.environment;\n\n const priority = calculatePriority(input.type, 1, input.hasScreenshot, environment);\n\n const reporter: Reporter = {\n userId: input.submittedBy,\n addedAt: now,\n };\n\n const PK = `PROJECT#${input.appName}`;\n const SK = `FB#${now}#${id}`;\n const GSI1PK = `STATUS#${status}`;\n const GSI1SK = buildGSI1SK(priority, id);\n const GSI2PK = `TYPE#${input.type}#PROJECT#${input.appName}`;\n const GSI2SK = now;\n\n const item: StoredFeedbackItem = {\n PK,\n SK,\n GSI1PK,\n GSI1SK,\n GSI2PK,\n GSI2SK,\n id,\n appName: input.appName,\n type: input.type,\n status,\n priority,\n content: input.content,\n context: input.context,\n hasScreenshot: input.hasScreenshot,\n submittedAt: now,\n lastModifiedAt: now,\n submittedBy: input.submittedBy,\n reporters: [reporter],\n reporterCount: 1,\n dialogueHistory: input.dialogueHistory ?? [],\n corrections: [],\n tags,\n };\n\n // Optional fields — only set when defined\n if (input.screenshotS3Key !== undefined) {\n item.screenshotS3Key = input.screenshotS3Key;\n }\n if (input.environment !== undefined) {\n item.environment = input.environment;\n }\n if (input.version !== undefined) {\n item.version = input.version;\n }\n if (input.traceId !== undefined) {\n item.traceId = input.traceId;\n }\n if (input.classification !== undefined) {\n item.classification = input.classification;\n }\n if (input.scope !== undefined) {\n item.scope = input.scope;\n }\n if (input.size !== undefined) {\n item.size = input.size;\n }\n\n return item;\n}\n","/**\n * Transport contracts for the feedback package.\n *\n * Defines abstract interfaces that decouple core logic from\n * any specific implementation. Implementations live in the\n * consuming app's server handlers (slice 03).\n *\n * NO AWS SDK imports in this file.\n */\n\nimport type {\n StoredFeedbackItem,\n SubmitResult,\n DialogueResponse,\n UploadResult,\n DialogueEntry,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Transport Interfaces\n// ─────────────────────────────────────────────────────────────────\n\n/** What the React adapter calls at submit time */\nexport interface FeedbackSubmitTransport {\n submit(item: StoredFeedbackItem): Promise<SubmitResult>;\n}\n\n/** What the dialogue UI calls on each exchange */\nexport interface FeedbackDialogueTransport {\n exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse>;\n}\n\n/** What the screenshot capture calls after html2canvas */\nexport interface FeedbackScreenshotTransport {\n upload(feedbackId: string, blob: Blob): Promise<UploadResult>;\n}\n\n/**\n * Composite transport — all three combined for full-feature transport.\n * Dialogue and screenshot are optional so implementations can omit them.\n */\nexport interface FeedbackTransport\n extends FeedbackSubmitTransport,\n Partial<FeedbackDialogueTransport>,\n Partial<FeedbackScreenshotTransport> {}\n\n// ─────────────────────────────────────────────────────────────────\n// Memory Transport (for testing)\n// ─────────────────────────────────────────────────────────────────\n\n/** Memory transport with access to internal state for assertions */\nexport interface MemoryTransport extends FeedbackTransport {\n /** All submitted items */\n submissions: StoredFeedbackItem[];\n /** Queue of dialogue responses to return in order */\n dialogueResponses: DialogueResponse[];\n /** Uploaded screenshot blobs */\n uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }>;\n /** Clear all stored state */\n clear(): void;\n}\n\nconst DEFAULT_DIALOGUE_RESPONSE: DialogueResponse = {\n success: true,\n response: 'Thank you for your feedback. Can you tell me more?',\n conversationComplete: false,\n};\n\n/**\n * Creates an in-memory transport for testing.\n *\n * - submit(): stores the item and returns { success: true, feedbackId: item.id }\n * - exchange(): returns the first queued response, or a default stub\n * - upload(): records the blob reference\n * - Does NOT import any AWS SDK\n */\nexport function createMemoryTransport(): MemoryTransport {\n const submissions: StoredFeedbackItem[] = [];\n const dialogueResponses: DialogueResponse[] = [];\n const uploadedScreenshots: Array<{ feedbackId: string; blob: Blob }> = [];\n\n return {\n submissions,\n dialogueResponses,\n uploadedScreenshots,\n\n async submit(item: StoredFeedbackItem): Promise<SubmitResult> {\n submissions.push(item);\n return { success: true, feedbackId: item.id };\n },\n\n async exchange(\n feedbackId: string,\n message: string,\n history: DialogueEntry[],\n ): Promise<DialogueResponse> {\n void feedbackId;\n void message;\n void history;\n if (dialogueResponses.length > 0) {\n return dialogueResponses.shift() as DialogueResponse;\n }\n return { ...DEFAULT_DIALOGUE_RESPONSE };\n },\n\n async upload(feedbackId: string, blob: Blob): Promise<UploadResult> {\n uploadedScreenshots.push({ feedbackId, blob });\n return {\n success: true,\n s3Key: `screenshots/${feedbackId}.png`,\n };\n },\n\n clear() {\n submissions.length = 0;\n dialogueResponses.length = 0;\n uploadedScreenshots.length = 0;\n },\n };\n}\n","/**\n * ingest-transport.ts — central-ingest implementation of FeedbackSubmitTransport.\n *\n * The central-storage principle: a consuming application reaches central feedback storage ONLY through\n * an authenticated API — it mints a short-lived OAuth2 client-credentials JWT and POSTs the feedback\n * CONTENT to the generic `/records/{collection}` ingest endpoint. The server attributes every write to\n * the verified token identity (its `client_id` claim) and stamps the canonical keys (PK/SK/GSI); the\n * application holds no AWS principal and sends no partition/identity fields. This replaces the bespoke\n * direct-DynamoDB writer (`FeedbackService.put`) — the pattern the principle removes.\n *\n * This transport is the client half of that contract, matching:\n * - the deployed ingest API (`POST /records/feedback`, body `{ item }`, response `{ success, ids }`),\n * - ralph's `ingest-client.mjs` (same route + shape),\n * - deploy-engine's `ingest-verify` (same token mint + write).\n *\n * Zero runtime dependencies — uses the built-in `fetch` (keeps the package browser-safe and SDK-free).\n * The caller supplies the app-client credential (or a token provider); the SECRET is resolved by the\n * host out-of-band (SSM/password manager) and never embedded here. HTTPS is enforced — a token or\n * record sent over plaintext is a compromise.\n *\n * NO AWS SDK imports in this file.\n */\n\nimport type { StoredFeedbackItem, SubmitResult } from './types.js';\nimport type { FeedbackSubmitTransport } from './transport.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\n/** The ingest collection feedback writes to (the deployed registry maps it to `ralph-feedback`). */\nexport const FEEDBACK_COLLECTION = 'feedback';\n/** OAuth2 scope required to write via the ingest API. */\nexport const INGEST_WRITE_SCOPE = 'assets/write';\n\n/** Minimal fetch surface (so the package needs no DOM lib types to compile). */\nexport type FetchLike = (\n url: string,\n init?: { method?: string; headers?: Record<string, string>; body?: string },\n) => Promise<{ status: number; ok: boolean; json: () => Promise<unknown> }>;\n\n/**\n * How the transport obtains a bearer token. Either:\n * - supply `clientId` + `clientSecret` + `tokenUrl` and let the transport mint/refresh it, OR\n * - supply a `tokenProvider()` (e.g. the host already manages the token) — takes precedence.\n */\nexport interface IngestTransportConfig {\n /** Ingest API base URL (HTTPS), e.g. https://xxxx.execute-api.<region>.amazonaws.com */\n apiUrl: string;\n /** OAuth2 token endpoint (HTTPS) — required unless `tokenProvider` is given. */\n tokenUrl?: string;\n /** App-client id (the app identity the server attributes writes to). Required unless `tokenProvider`. */\n clientId?: string;\n /** App-client secret. SHOULD come from the host's secret store; required unless `tokenProvider`. */\n clientSecret?: string;\n /** Supply a token directly (overrides client-credentials minting). */\n tokenProvider?: () => Promise<string>;\n /** Collection name (default 'feedback'). */\n collection?: string;\n /** Seconds before expiry to proactively re-mint (default 60). */\n refreshSkewSeconds?: number;\n /** Injectable fetch (tests). Defaults to global fetch. */\n fetchImpl?: FetchLike;\n}\n\nfunction assertHttps(url: string, label: string): void {\n let u: URL;\n try {\n u = new URL(url);\n } catch {\n throw new Error(`ingest-transport: ${label} is not a valid URL: ${url}`);\n }\n if (u.protocol !== 'https:') {\n throw new Error(`ingest-transport: ${label} must be https:// (got ${u.protocol}//) — refusing plaintext`);\n }\n}\n\n/** Identity/partition + local-only fields the server owns — stripped before sending the content. */\nconst SERVER_OWNED_FIELDS: readonly string[] = [\n 'PK', 'SK', 'GSI1PK', 'GSI1SK', 'GSI2PK', 'GSI2SK',\n];\n\n/** Strip the fields the server stamps from the verified identity (never sent/trusted from the client). */\nfunction toContent(item: StoredFeedbackItem): Record<string, unknown> {\n const content: Record<string, unknown> = { ...(item as unknown as Record<string, unknown>) };\n for (const f of SERVER_OWNED_FIELDS) delete content[f];\n return content;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Transport\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Create a FeedbackSubmitTransport that writes to the central ingest API.\n *\n * Usage (host app wires the credential from its secret store):\n * const transport = createIngestTransport({\n * apiUrl: process.env.FEEDBACK_INGEST_URL!,\n * tokenUrl: process.env.FEEDBACK_TOKEN_URL!,\n * clientId: process.env.FEEDBACK_CLIENT_ID!,\n * clientSecret: await resolveSecret('feedback/ingest/<app>'),\n * });\n * await transport.submit(item); // → { success, feedbackId }\n */\nexport function createIngestTransport(config: IngestTransportConfig): FeedbackSubmitTransport {\n const collection = config.collection ?? FEEDBACK_COLLECTION;\n const refreshSkewMs = (config.refreshSkewSeconds ?? 60) * 1000;\n const doFetch = config.fetchImpl ?? (globalThis.fetch as unknown as FetchLike);\n\n assertHttps(config.apiUrl, 'apiUrl');\n if (!config.tokenProvider) {\n if (!config.tokenUrl || !config.clientId || !config.clientSecret) {\n throw new Error('ingest-transport: provide tokenProvider, OR tokenUrl + clientId + clientSecret');\n }\n assertHttps(config.tokenUrl, 'tokenUrl');\n }\n if (typeof doFetch !== 'function') {\n throw new Error('ingest-transport: no fetch implementation available');\n }\n\n // Cached minted token (when minting ourselves).\n let cachedToken: string | null = null;\n let cachedExpiryMs = 0;\n\n async function mintToken(): Promise<string> {\n const basic = toBase64(`${config.clientId}:${config.clientSecret}`);\n const res = await doFetch(config.tokenUrl as string, {\n method: 'POST',\n headers: { 'content-type': 'application/x-www-form-urlencoded', authorization: `Basic ${basic}` },\n body: `grant_type=client_credentials&scope=${encodeURIComponent(INGEST_WRITE_SCOPE)}`,\n });\n const data = (await res.json()) as { access_token?: string; expires_in?: number; error?: string; error_description?: string };\n if (!res.ok || !data.access_token) {\n throw new Error(`token mint failed (${res.status}): ${data.error ?? ''} ${data.error_description ?? ''}`.trim());\n }\n cachedToken = data.access_token;\n cachedExpiryMs = Date.now() + Math.max(0, (data.expires_in ?? 3600) * 1000 - refreshSkewMs);\n return cachedToken;\n }\n\n async function getToken(): Promise<string> {\n if (config.tokenProvider) return config.tokenProvider();\n if (cachedToken && Date.now() < cachedExpiryMs) return cachedToken;\n return mintToken();\n }\n\n return {\n async submit(item: StoredFeedbackItem): Promise<SubmitResult> {\n try {\n const token = await getToken();\n const base = config.apiUrl.replace(/\\/$/, '');\n const res = await doFetch(`${base}/records/${encodeURIComponent(collection)}`, {\n method: 'POST',\n headers: { authorization: `Bearer ${token}`, 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify({ item: toContent(item) }),\n });\n const data = (await res.json().catch(() => ({}))) as { success?: boolean; ids?: string[]; error?: string; errors?: string[] };\n if (res.status === 200 && data.success === true) {\n return { success: true, feedbackId: data.ids?.[0] ?? item.id };\n }\n // Explicit failure — never a silent drop. 5xx is retryable; 4xx (validation/auth) is not.\n const retryable = res.status >= 500;\n const msg = data.error ?? data.errors?.join('; ') ?? `HTTP ${res.status}`;\n return { success: false, feedbackId: item.id, error: `ingest write failed: ${msg}`, retryable };\n } catch (err) {\n // Transport/network failure — retryable (the caller may buffer + retry).\n return { success: false, feedbackId: item.id, error: (err as Error).message, retryable: true };\n }\n },\n };\n}\n\n/** Base64 encode a string in both Node and browser (zero-dep). */\nfunction toBase64(s: string): string {\n if (typeof btoa === 'function') return btoa(s);\n // Node fallback — Buffer exists; typed loosely to avoid a hard @types/node coupling in the browser build.\n const B = (globalThis as unknown as { Buffer?: { from: (x: string) => { toString: (enc: string) => string } } }).Buffer;\n if (B) return B.from(s).toString('base64');\n throw new Error('ingest-transport: no base64 encoder available');\n}\n","/**\n * Input validation functions for the feedback package.\n *\n * Used both client-side (before submission) and server-side (in handler).\n * No external dependencies — pure validation logic.\n *\n * Acceptance criteria evidence:\n * AC-65: createErrorResponse(new FeedbackValidationError('…', ['content']))\n * ])) includes fields: ['content'] → shown in errors.ts\n * AC-41: VALIDATION_RULES is exported as const (immutable) with values:\n * CONTENT_MIN_LENGTH=10, CONTENT_MAX_LENGTH=5000, TAG_MAX_COUNT=10\n * TAG_PATTERN=/^[a-z0-9-]+$/, [a-z0-9-] pattern applied\n */\n\nimport type { FeedbackContext } from './types.js';\nimport type { CaptureInput } from './capture.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Validation Rules (exported as const — immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/** Immutable validation constants */\nexport const VALIDATION_RULES = {\n CONTENT_MIN_LENGTH: 10,\n CONTENT_MAX_LENGTH: 5_000,\n TAG_MAX_COUNT: 10,\n TAG_PATTERN: /^[a-z0-9-]+$/,\n APP_NAME_PATTERN: /^[a-zA-Z0-9_-]+$/,\n SCREENSHOT_MAX_BYTES: 5_242_880, // 5 MB\n CONSOLE_ERRORS_MAX: 20,\n} as const;\n\n// ─────────────────────────────────────────────────────────────────\n// Result Type\n// ─────────────────────────────────────────────────────────────────\n\n/** Validation result with user-facing messages */\nexport interface ValidationResult {\n valid: boolean;\n errors: string[]; // Blocking — submission must be rejected\n warnings: string[]; // Non-blocking — suggestions only\n}\n\nfunction makeResult(errors: string[], warnings: string[]): ValidationResult {\n return { valid: errors.length === 0, errors, warnings };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Individual Validators\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Validates feedback content.\n * Fails below CONTENT_MIN_LENGTH (\"Feedback must be at least 10 characters\"),\n * fails above CONTENT_MAX_LENGTH, warns if only whitespace.\n */\nexport function validateContent(content: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (content.length < VALIDATION_RULES.CONTENT_MIN_LENGTH) {\n errors.push(\n `Feedback must be at least ${VALIDATION_RULES.CONTENT_MIN_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.length > VALIDATION_RULES.CONTENT_MAX_LENGTH) {\n errors.push(\n `Feedback must not exceed ${VALIDATION_RULES.CONTENT_MAX_LENGTH} characters (got ${content.length})`,\n );\n } else if (content.trim().length === 0) {\n warnings.push('Feedback appears to contain only whitespace');\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates feedback tags.\n * Fails if count exceeds TAG_MAX_COUNT or if any tag doesn't match TAG_PATTERN.\n */\nexport function validateTags(tags: string[]): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (tags.length > VALIDATION_RULES.TAG_MAX_COUNT) {\n errors.push(\n `Too many tags: ${tags.length} (maximum is ${VALIDATION_RULES.TAG_MAX_COUNT})`,\n );\n }\n\n for (const tag of tags) {\n if (!VALIDATION_RULES.TAG_PATTERN.test(tag)) {\n errors.push(\n `Tag \"${tag}\" is invalid — must match /^[a-z0-9-]+$/ (lowercase letters, digits, hyphens only)`,\n );\n }\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates an app name.\n */\nexport function validateAppName(appName: string): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!appName || appName.trim() === '') {\n errors.push('appName is required');\n } else if (!VALIDATION_RULES.APP_NAME_PATTERN.test(appName)) {\n errors.push(\n `appName \"${appName}\" is invalid — must match /^[a-zA-Z0-9_-]+$/`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates screenshot size.\n */\nexport function validateScreenshotSize(sizeBytes: number): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (sizeBytes > VALIDATION_RULES.SCREENSHOT_MAX_BYTES) {\n errors.push(\n `Screenshot size ${sizeBytes} bytes exceeds the maximum of ${VALIDATION_RULES.SCREENSHOT_MAX_BYTES} bytes (5 MB)`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Validates browser context.\n * Warns if url is empty; warns if consoleErrors exceeds 20 items.\n */\nexport function validateContext(context: FeedbackContext): ValidationResult {\n const errors: string[] = [];\n const warnings: string[] = [];\n\n if (!context.url || context.url.trim() === '') {\n warnings.push('context.url is empty — this may indicate a capture logic error');\n }\n\n if (\n context.consoleErrors &&\n context.consoleErrors.length > VALIDATION_RULES.CONSOLE_ERRORS_MAX\n ) {\n warnings.push(\n `context.consoleErrors has ${context.consoleErrors.length} entries (expected ≤${VALIDATION_RULES.CONSOLE_ERRORS_MAX}); truncation should occur in capture layer`,\n );\n }\n\n return makeResult(errors, warnings);\n}\n\n/**\n * Aggregate validator — runs all validators for a capture input.\n */\nexport function validateCaptureInput(input: Partial<CaptureInput>): ValidationResult {\n const allErrors: string[] = [];\n const allWarnings: string[] = [];\n\n if (input.content !== undefined) {\n const contentResult = validateContent(input.content);\n allErrors.push(...contentResult.errors);\n allWarnings.push(...contentResult.warnings);\n } else {\n allErrors.push('content is required');\n }\n\n if (input.tags !== undefined) {\n const tagResult = validateTags(input.tags);\n allErrors.push(...tagResult.errors);\n allWarnings.push(...tagResult.warnings);\n }\n\n if (input.appName !== undefined) {\n const nameResult = validateAppName(input.appName);\n allErrors.push(...nameResult.errors);\n allWarnings.push(...nameResult.warnings);\n } else {\n allErrors.push('appName is required');\n }\n\n if (input.context !== undefined) {\n const contextResult = validateContext(input.context);\n allErrors.push(...contextResult.errors);\n allWarnings.push(...contextResult.warnings);\n }\n\n return makeResult(allErrors, allWarnings);\n}\n","/**\n * Pure data transformation functions for DynamoDB key construction\n * and shape mapping.\n *\n * NO AWS SDK calls — only key construction and shape mapping.\n * All functions are pure: take plain objects, return plain objects.\n * No side effects.\n */\n\nimport type {\n FeedbackType,\n FeedbackStatus,\n StoredFeedbackItem,\n CorrectionEntry,\n Reporter,\n} from './types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Key Builder\n// ─────────────────────────────────────────────────────────────────\n\n/** The six DynamoDB keys for a feedback item */\nexport interface FeedbackKeys {\n PK: string; // PROJECT#{appName}\n SK: string; // FB#{timestamp}#{id}\n GSI1PK: string; // STATUS#{status}\n GSI1SK: string; // {inverted-priority-padded}#{id}\n GSI2PK: string; // TYPE#{type}#PROJECT#{appName}\n GSI2SK: string; // {timestamp}\n}\n\n/**\n * Builds the six DynamoDB keys for a feedback item.\n *\n * @param appName - Application name (e.g. 'sagrosoft20')\n * @param id - Feedback ID (e.g. 'FB-2026-07-25-abc12345')\n * @param type - Feedback type\n * @param status - Current status\n * @param priority - Priority score 0–100\n * @param timestamp - ISO 8601 timestamp\n */\nexport function buildKeys(\n appName: string,\n id: string,\n type: FeedbackType,\n status: FeedbackStatus,\n priority: number,\n timestamp: string,\n): FeedbackKeys {\n const clampedPriority = Math.max(0, Math.min(100, priority));\n const inverted = 100 - clampedPriority;\n const paddedInverted = String(inverted).padStart(3, '0');\n\n return {\n PK: `PROJECT#${appName}`,\n SK: `FB#${timestamp}#${id}`,\n GSI1PK: `STATUS#${status}`,\n GSI1SK: `${paddedInverted}#${id}`,\n GSI2PK: `TYPE#${type}#PROJECT#${appName}`,\n GSI2SK: timestamp,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Key Parsers\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Extracts timestamp and id from a composite SK.\n * SK format: FB#{timestamp}#{id}\n */\nexport function parseSK(sk: string): { timestamp: string; id: string } {\n // SK format: FB#2026-07-25T10:00:00.000Z#FB-2026-07-25-abc12345\n const withoutPrefix = sk.startsWith('FB#') ? sk.slice(3) : sk;\n // Find the last '#' which separates timestamp from id\n const lastHash = withoutPrefix.lastIndexOf('#');\n if (lastHash === -1) {\n throw new Error(`Invalid SK format: \"${sk}\" — expected FB#{timestamp}#{id}`);\n }\n return {\n timestamp: withoutPrefix.slice(0, lastHash),\n id: withoutPrefix.slice(lastHash + 1),\n };\n}\n\n/**\n * Parses the GSI2PK into its component parts.\n * GSI2PK format: TYPE#{type}#PROJECT#{appName}\n */\nexport function parseGSI2PK(gsi2pk: string): { type: FeedbackType; appName: string } {\n // Format: TYPE#feature#PROJECT#oms4business\n const typeMatch = gsi2pk.match(/^TYPE#([^#]+)#PROJECT#(.+)$/);\n if (!typeMatch || !typeMatch[1] || !typeMatch[2]) {\n throw new Error(\n `Invalid GSI2PK format: \"${gsi2pk}\" — expected TYPE#{type}#PROJECT#{appName}`,\n );\n }\n return {\n type: typeMatch[1] as FeedbackType,\n appName: typeMatch[2],\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Reporter Mutations (immutable — returns new arrays)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Adds a reporter to an item.\n * Returns new reporters array and updated reporterCount — does NOT mutate original.\n */\nexport function addReporter(\n item: StoredFeedbackItem,\n userId: string,\n addedContext?: string,\n nowIso?: string,\n): Pick<StoredFeedbackItem, 'reporters' | 'reporterCount' | 'GSI1SK'> {\n const addedAt = nowIso ?? new Date().toISOString();\n const newReporter: Reporter = { userId, addedAt };\n if (addedContext !== undefined) {\n newReporter.addedContext = addedContext;\n }\n\n // Spread to ensure immutability — does NOT mutate item.reporters\n const newReporters: Reporter[] = [...item.reporters, newReporter];\n const newCount = newReporters.length;\n\n // GSI1SK format preserves the inverted priority prefix, just uses same item id\n // The priority recalculation would normally happen in the capture layer\n // Here we preserve the existing GSI1SK structure (priority unchanged by reporter add)\n return {\n reporters: newReporters,\n reporterCount: newCount,\n GSI1SK: item.GSI1SK,\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Correction Mutations (immutable)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Appends a correction record to the corrections array.\n * Returns a new array — does NOT mutate the original.\n */\nexport function appendCorrection(\n corrections: CorrectionEntry[],\n correction: Omit<CorrectionEntry, 'correctedAt'>,\n nowIso?: string,\n): CorrectionEntry[] {\n const correctedAt = nowIso ?? new Date().toISOString();\n return [\n ...corrections,\n { ...correction, correctedAt },\n ];\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Status Transition\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Applies a status transition to an item.\n * Returns updated key fields + lastModifiedAt (does NOT mutate original).\n *\n * When transitioning to 'resolved': sets resolvedAt = nowIso.\n * When transitioning to any other status: resolvedAt is undefined.\n */\nexport function applyStatusTransition(\n item: StoredFeedbackItem,\n newStatus: FeedbackStatus,\n nowIso?: string,\n): Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n> {\n const now = nowIso ?? new Date().toISOString();\n\n // GSI1SK stays the same (priority doesn't change on status transition)\n // Only GSI1PK changes (new status)\n const resolvedAt: string | undefined = newStatus === 'resolved' ? now : undefined;\n const resolvedBy: string | undefined = undefined;\n\n return {\n status: newStatus,\n GSI1PK: `STATUS#${newStatus}`,\n GSI1SK: item.GSI1SK, // Priority unchanged\n lastModifiedAt: now,\n ...(resolvedAt !== undefined ? { resolvedAt } : {}),\n ...(resolvedBy !== undefined ? { resolvedBy } : {}),\n } as Pick<\n StoredFeedbackItem,\n 'status' | 'GSI1PK' | 'GSI1SK' | 'lastModifiedAt' | 'resolvedAt' | 'resolvedBy'\n >;\n}\n","/**\n * Domain error types for the feedback package.\n *\n * Every error class carries:\n * - code: string constant (usable in switch statements)\n * - retryable: boolean\n *\n * createErrorResponse converts any thrown value to a safe JSON-serialisable object.\n */\n\n// ─────────────────────────────────────────────────────────────────\n// Base Error Class\n// ─────────────────────────────────────────────────────────────────\n\n/** Base class for all feedback domain errors */\nexport abstract class FeedbackError extends Error {\n abstract readonly code: string;\n abstract readonly retryable: boolean;\n\n constructor(message: string, readonly cause?: unknown) {\n super(message);\n this.name = this.constructor.name;\n // Fix prototype chain for instanceof checks\n Object.setPrototypeOf(this, new.target.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Concrete Error Classes\n// ─────────────────────────────────────────────────────────────────\n\n/** Thrown when input validation fails */\nexport class FeedbackValidationError extends FeedbackError {\n readonly code = 'FEEDBACK_VALIDATION_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly fields?: string[],\n ) {\n super(message);\n this.name = 'FeedbackValidationError';\n Object.setPrototypeOf(this, FeedbackValidationError.prototype);\n }\n}\n\n/** Thrown when the transport layer fails (network, server error) */\nexport class FeedbackTransportError extends FeedbackError {\n readonly code = 'FEEDBACK_TRANSPORT_ERROR';\n readonly retryable = true;\n\n constructor(\n message: string,\n readonly statusCode?: number,\n cause?: unknown,\n ) {\n super(message, cause);\n this.name = 'FeedbackTransportError';\n Object.setPrototypeOf(this, FeedbackTransportError.prototype);\n }\n}\n\n/** Thrown when configuration is invalid or missing */\nexport class FeedbackConfigError extends FeedbackError {\n readonly code = 'FEEDBACK_CONFIG_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackConfigError';\n Object.setPrototypeOf(this, FeedbackConfigError.prototype);\n }\n}\n\n/** Thrown when dialogue exchange fails */\nexport class FeedbackDialogueError extends FeedbackError {\n readonly code = 'FEEDBACK_DIALOGUE_ERROR';\n readonly retryable = true;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackDialogueError';\n Object.setPrototypeOf(this, FeedbackDialogueError.prototype);\n }\n}\n\n/** Thrown when screenshot capture or upload fails */\nexport class FeedbackScreenshotError extends FeedbackError {\n readonly code = 'FEEDBACK_SCREENSHOT_ERROR';\n readonly retryable = false;\n\n constructor(message: string, cause?: unknown) {\n super(message, cause);\n this.name = 'FeedbackScreenshotError';\n Object.setPrototypeOf(this, FeedbackScreenshotError.prototype);\n }\n}\n\n/** Thrown when a duplicate feedback submission is detected */\nexport class FeedbackDuplicateError extends FeedbackError {\n readonly code = 'FEEDBACK_DUPLICATE_ERROR';\n readonly retryable = false;\n\n constructor(\n message: string,\n readonly existingId: string,\n ) {\n super(message);\n this.name = 'FeedbackDuplicateError';\n Object.setPrototypeOf(this, FeedbackDuplicateError.prototype);\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Type Guard\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Type guard — returns true if the value is a FeedbackError subclass.\n * Returns false for plain Error instances and other values.\n */\nexport function isFeedbackError(e: unknown): e is FeedbackError {\n return e instanceof FeedbackError;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Error Response Serialiser\n// ─────────────────────────────────────────────────────────────────\n\n/** Safe JSON-serialisable error response */\nexport interface ErrorResponse {\n code: string;\n message: string;\n retryable: boolean;\n fields?: string[]; // present for FeedbackValidationError\n existingId?: string; // present for FeedbackDuplicateError\n}\n\n/**\n * Converts any thrown value to a safe JSON-serialisable response object.\n *\n * - FeedbackError subclasses → uses their code, message, retryable\n * - FeedbackValidationError → includes fields\n * - FeedbackDuplicateError → includes existingId\n * - Plain string → { code: 'UNKNOWN_ERROR', message: theString, retryable: false }\n * - Plain Error → { code: 'UNKNOWN_ERROR', message: error.message, retryable: false }\n * - null/undefined/other → { code: 'UNKNOWN_ERROR', message: 'An unknown error occurred', retryable: false }\n */\nexport function createErrorResponse(e: unknown): ErrorResponse {\n if (isFeedbackError(e)) {\n const base: ErrorResponse = {\n code: e.code,\n message: e.message,\n retryable: e.retryable,\n };\n\n if (e instanceof FeedbackValidationError && e.fields !== undefined) {\n base.fields = e.fields;\n }\n\n if (e instanceof FeedbackDuplicateError) {\n base.existingId = e.existingId;\n }\n\n return base;\n }\n\n if (typeof e === 'string') {\n return {\n code: 'UNKNOWN_ERROR',\n message: e,\n retryable: false,\n };\n }\n\n if (e instanceof Error) {\n return {\n code: 'UNKNOWN_ERROR',\n message: e.message,\n retryable: false,\n };\n }\n\n return {\n code: 'UNKNOWN_ERROR',\n message: 'An unknown error occurred',\n retryable: false,\n };\n}\n"],"names":["e"],"mappings":";;AAkDA,MAAM,gBAA8C;AAAA,EAClD,KAAK;AAAA,EACL,IAAI;AAAA,EACJ,SAAS;AAAA,EACT,OAAO;AACT;AAMA,MAAM,oBAAyD;AAAA,EAC7D,YAAY;AAAA,EACZ,SAAS;AAAA,EACT,aAAa;AACf;AAYO,SAAS,kBACd,MACA,eACA,eACA,aACQ;AACR,QAAM,OAAO,cAAc,IAAI;AAC/B,QAAM,MAAM,kBAAkB,eAAe,aAAa;AAC1D,QAAM,gBAAgB,KAAK,IAAI,gBAAgB,GAAG,CAAC,IAAI;AACvD,QAAM,kBAAkB,gBAAgB,IAAI;AAE5C,QAAM,MAAM,OAAO,MAAM,gBAAgB;AACzC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,GAAG,CAAC;AACvC;AAUO,SAAS,mBAAmB,QAAyB;AAC1D,QAAM,OAAO,SAAS,OAAO,MAAM,GAAG,EAAE,KAAI,oBAAI,KAAA,GAAO,YAAA,EAAc,MAAM,GAAG,EAAE;AAChF,QAAM,QAAQ,OAAO,WAAA,EAAa,QAAQ,MAAM,EAAE,EAAE,MAAM,GAAG,CAAC;AAC9D,SAAO,MAAM,IAAI,IAAI,KAAK;AAC5B;AAaO,SAAS,YAAY,UAAkB,IAAoB;AAChE,QAAM,WAAW,MAAM,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC1D,QAAM,SAAS,OAAO,QAAQ,EAAE,SAAS,GAAG,GAAG;AAC/C,SAAO,GAAG,MAAM,IAAI,EAAE;AACxB;AAiBO,SAAS,uBACd,OACA,QACoB;AACpB,QAAM,MAAM,WAAU,oBAAI,KAAA,GAAO,YAAA;AACjC,QAAM,KAAK,mBAAmB,GAAG;AACjC,QAAM,SAAyB;AAC/B,QAAM,OAAO,MAAM,QAAQ,CAAA;AAC3B,QAAM,cAAc,MAAM;AAE1B,QAAM,WAAW,kBAAkB,MAAM,MAAM,GAAG,MAAM,eAAe,WAAW;AAElF,QAAM,WAAqB;AAAA,IACzB,QAAQ,MAAM;AAAA,IACd,SAAS;AAAA,EAAA;AAGX,QAAM,KAAK,WAAW,MAAM,OAAO;AACnC,QAAM,KAAK,MAAM,GAAG,IAAI,EAAE;AAC1B,QAAM,SAAS,UAAU,MAAM;AAC/B,QAAM,SAAS,YAAY,UAAU,EAAE;AACvC,QAAM,SAAS,QAAQ,MAAM,IAAI,YAAY,MAAM,OAAO;AAC1D,QAAM,SAAS;AAEf,QAAM,OAA2B;AAAA,IAC/B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,SAAS,MAAM;AAAA,IACf,MAAM,MAAM;AAAA,IACZ;AAAA,IACA;AAAA,IACA,SAAS,MAAM;AAAA,IACf,SAAS,MAAM;AAAA,IACf,eAAe,MAAM;AAAA,IACrB,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,aAAa,MAAM;AAAA,IACnB,WAAW,CAAC,QAAQ;AAAA,IACpB,eAAe;AAAA,IACf,iBAAiB,MAAM,mBAAmB,CAAA;AAAA,IAC1C,aAAa,CAAA;AAAA,IACb;AAAA,EAAA;AAIF,MAAI,MAAM,oBAAoB,QAAW;AACvC,SAAK,kBAAkB,MAAM;AAAA,EAC/B;AACA,MAAI,MAAM,gBAAgB,QAAW;AACnC,SAAK,cAAc,MAAM;AAAA,EAC3B;AACA,MAAI,MAAM,YAAY,QAAW;AAC/B,SAAK,UAAU,MAAM;AAAA,EACvB;AACA,MAAI,MAAM,YAAY,QAAW;AAC/B,SAAK,UAAU,MAAM;AAAA,EACvB;AACA,MAAI,MAAM,mBAAmB,QAAW;AACtC,SAAK,iBAAiB,MAAM;AAAA,EAC9B;AACA,MAAI,MAAM,UAAU,QAAW;AAC7B,SAAK,QAAQ,MAAM;AAAA,EACrB;AACA,MAAI,MAAM,SAAS,QAAW;AAC5B,SAAK,OAAO,MAAM;AAAA,EACpB;AAEA,SAAO;AACT;ACjJA,MAAM,4BAA8C;AAAA,EAClD,SAAS;AAAA,EACT,UAAU;AAAA,EACV,sBAAsB;AACxB;AAUO,SAAS,wBAAyC;AACvD,QAAM,cAAoC,CAAA;AAC1C,QAAM,oBAAwC,CAAA;AAC9C,QAAM,sBAAiE,CAAA;AAEvE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IAEA,MAAM,OAAO,MAAiD;AAC5D,kBAAY,KAAK,IAAI;AACrB,aAAO,EAAE,SAAS,MAAM,YAAY,KAAK,GAAA;AAAA,IAC3C;AAAA,IAEA,MAAM,SACJ,YACA,SACA,SAC2B;AAI3B,UAAI,kBAAkB,SAAS,GAAG;AAChC,eAAO,kBAAkB,MAAA;AAAA,MAC3B;AACA,aAAO,EAAE,GAAG,0BAAA;AAAA,IACd;AAAA,IAEA,MAAM,OAAO,YAAoB,MAAmC;AAClE,0BAAoB,KAAK,EAAE,YAAY,KAAA,CAAM;AAC7C,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,eAAe,UAAU;AAAA,MAAA;AAAA,IAEpC;AAAA,IAEA,QAAQ;AACN,kBAAY,SAAS;AACrB,wBAAkB,SAAS;AAC3B,0BAAoB,SAAS;AAAA,IAC/B;AAAA,EAAA;AAEJ;AC5FO,MAAM,sBAAsB;AAE5B,MAAM,qBAAqB;AAgClC,SAAS,YAAY,KAAa,OAAqB;AACrD,MAAI;AACJ,MAAI;AACF,QAAI,IAAI,IAAI,GAAG;AAAA,EACjB,QAAQ;AACN,UAAM,IAAI,MAAM,qBAAqB,KAAK,wBAAwB,GAAG,EAAE;AAAA,EACzE;AACA,MAAI,EAAE,aAAa,UAAU;AAC3B,UAAM,IAAI,MAAM,qBAAqB,KAAK,0BAA0B,EAAE,QAAQ,0BAA0B;AAAA,EAC1G;AACF;AAGA,MAAM,sBAAyC;AAAA,EAC7C;AAAA,EAAM;AAAA,EAAM;AAAA,EAAU;AAAA,EAAU;AAAA,EAAU;AAC5C;AAGA,SAAS,UAAU,MAAmD;AACpE,QAAM,UAAmC,EAAE,GAAI,KAAA;AAC/C,aAAW,KAAK,oBAAqB,QAAO,QAAQ,CAAC;AACrD,SAAO;AACT;AAkBO,SAAS,sBAAsB,QAAwD;AAC5F,QAAM,aAAa,OAAO,cAAc;AACxC,QAAM,iBAAiB,OAAO,sBAAsB,MAAM;AAC1D,QAAM,UAAU,OAAO,aAAc,WAAW;AAEhD,cAAY,OAAO,QAAQ,QAAQ;AACnC,MAAI,CAAC,OAAO,eAAe;AACzB,QAAI,CAAC,OAAO,YAAY,CAAC,OAAO,YAAY,CAAC,OAAO,cAAc;AAChE,YAAM,IAAI,MAAM,gFAAgF;AAAA,IAClG;AACA,gBAAY,OAAO,UAAU,UAAU;AAAA,EACzC;AACA,MAAI,OAAO,YAAY,YAAY;AACjC,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AAGA,MAAI,cAA6B;AACjC,MAAI,iBAAiB;AAErB,iBAAe,YAA6B;AAC1C,UAAM,QAAQ,SAAS,GAAG,OAAO,QAAQ,IAAI,OAAO,YAAY,EAAE;AAClE,UAAM,MAAM,MAAM,QAAQ,OAAO,UAAoB;AAAA,MACnD,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,qCAAqC,eAAe,SAAS,KAAK,GAAA;AAAA,MAC7F,MAAM,uCAAuC,mBAAmB,kBAAkB,CAAC;AAAA,IAAA,CACpF;AACD,UAAM,OAAQ,MAAM,IAAI,KAAA;AACxB,QAAI,CAAC,IAAI,MAAM,CAAC,KAAK,cAAc;AACjC,YAAM,IAAI,MAAM,sBAAsB,IAAI,MAAM,MAAM,KAAK,SAAS,EAAE,IAAI,KAAK,qBAAqB,EAAE,GAAG,MAAM;AAAA,IACjH;AACA,kBAAc,KAAK;AACnB,qBAAiB,KAAK,IAAA,IAAQ,KAAK,IAAI,IAAI,KAAK,cAAc,QAAQ,MAAO,aAAa;AAC1F,WAAO;AAAA,EACT;AAEA,iBAAe,WAA4B;AACzC,QAAI,OAAO,cAAe,QAAO,OAAO,cAAA;AACxC,QAAI,eAAe,KAAK,IAAA,IAAQ,eAAgB,QAAO;AACvD,WAAO,UAAA;AAAA,EACT;AAEA,SAAO;AAAA,IACL,MAAM,OAAO,MAAiD;AAC5D,UAAI;AACF,cAAM,QAAQ,MAAM,SAAA;AACpB,cAAM,OAAO,OAAO,OAAO,QAAQ,OAAO,EAAE;AAC5C,cAAM,MAAM,MAAM,QAAQ,GAAG,IAAI,YAAY,mBAAmB,UAAU,CAAC,IAAI;AAAA,UAC7E,QAAQ;AAAA,UACR,SAAS,EAAE,eAAe,UAAU,KAAK,IAAI,gBAAgB,oBAAoB,QAAQ,mBAAA;AAAA,UACzF,MAAM,KAAK,UAAU,EAAE,MAAM,UAAU,IAAI,GAAG;AAAA,QAAA,CAC/C;AACD,cAAM,OAAQ,MAAM,IAAI,KAAA,EAAO,MAAM,OAAO,CAAA,EAAG;AAC/C,YAAI,IAAI,WAAW,OAAO,KAAK,YAAY,MAAM;AAC/C,iBAAO,EAAE,SAAS,MAAM,YAAY,KAAK,MAAM,CAAC,KAAK,KAAK,GAAA;AAAA,QAC5D;AAEA,cAAM,YAAY,IAAI,UAAU;AAChC,cAAM,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK,IAAI,KAAK,QAAQ,IAAI,MAAM;AACvE,eAAO,EAAE,SAAS,OAAO,YAAY,KAAK,IAAI,OAAO,wBAAwB,GAAG,IAAI,UAAA;AAAA,MACtF,SAAS,KAAK;AAEZ,eAAO,EAAE,SAAS,OAAO,YAAY,KAAK,IAAI,OAAQ,IAAc,SAAS,WAAW,KAAA;AAAA,MAC1F;AAAA,IACF;AAAA,EAAA;AAEJ;AAGA,SAAS,SAAS,GAAmB;AACnC,MAAI,OAAO,SAAS,WAAY,QAAO,KAAK,CAAC;AAE7C,QAAM,IAAK,WAAsG;AACjH,MAAI,EAAG,QAAO,EAAE,KAAK,CAAC,EAAE,SAAS,QAAQ;AACzC,QAAM,IAAI,MAAM,+CAA+C;AACjE;AC9JO,MAAM,mBAAmB;AAAA,EAC9B,oBAAoB;AAAA,EACpB,oBAAoB;AAAA,EACpB,eAAe;AAAA,EACf,aAAa;AAAA,EACb,kBAAkB;AAAA,EAClB,sBAAsB;AAAA;AAAA,EACtB,oBAAoB;AACtB;AAaA,SAAS,WAAW,QAAkB,UAAsC;AAC1E,SAAO,EAAE,OAAO,OAAO,WAAW,GAAG,QAAQ,SAAA;AAC/C;AAWO,SAAS,gBAAgB,SAAmC;AACjE,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,QAAQ,SAAS,iBAAiB,oBAAoB;AACxD,WAAO;AAAA,MACL,6BAA6B,iBAAiB,kBAAkB,oBAAoB,QAAQ,MAAM;AAAA,IAAA;AAAA,EAEtG,WAAW,QAAQ,SAAS,iBAAiB,oBAAoB;AAC/D,WAAO;AAAA,MACL,4BAA4B,iBAAiB,kBAAkB,oBAAoB,QAAQ,MAAM;AAAA,IAAA;AAAA,EAErG,WAAW,QAAQ,KAAA,EAAO,WAAW,GAAG;AACtC,aAAS,KAAK,6CAA6C;AAAA,EAC7D;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAMO,SAAS,aAAa,MAAkC;AAC7D,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,KAAK,SAAS,iBAAiB,eAAe;AAChD,WAAO;AAAA,MACL,kBAAkB,KAAK,MAAM,gBAAgB,iBAAiB,aAAa;AAAA,IAAA;AAAA,EAE/E;AAEA,aAAW,OAAO,MAAM;AACtB,QAAI,CAAC,iBAAiB,YAAY,KAAK,GAAG,GAAG;AAC3C,aAAO;AAAA,QACL,QAAQ,GAAG;AAAA,MAAA;AAAA,IAEf;AAAA,EACF;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAKO,SAAS,gBAAgB,SAAmC;AACjE,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,CAAC,WAAW,QAAQ,KAAA,MAAW,IAAI;AACrC,WAAO,KAAK,qBAAqB;AAAA,EACnC,WAAW,CAAC,iBAAiB,iBAAiB,KAAK,OAAO,GAAG;AAC3D,WAAO;AAAA,MACL,YAAY,OAAO;AAAA,IAAA;AAAA,EAEvB;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAKO,SAAS,uBAAuB,WAAqC;AAC1E,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,YAAY,iBAAiB,sBAAsB;AACrD,WAAO;AAAA,MACL,mBAAmB,SAAS,iCAAiC,iBAAiB,oBAAoB;AAAA,IAAA;AAAA,EAEtG;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAMO,SAAS,gBAAgB,SAA4C;AAC1E,QAAM,SAAmB,CAAA;AACzB,QAAM,WAAqB,CAAA;AAE3B,MAAI,CAAC,QAAQ,OAAO,QAAQ,IAAI,KAAA,MAAW,IAAI;AAC7C,aAAS,KAAK,gEAAgE;AAAA,EAChF;AAEA,MACE,QAAQ,iBACR,QAAQ,cAAc,SAAS,iBAAiB,oBAChD;AACA,aAAS;AAAA,MACP,6BAA6B,QAAQ,cAAc,MAAM,uBAAuB,iBAAiB,kBAAkB;AAAA,IAAA;AAAA,EAEvH;AAEA,SAAO,WAAW,QAAQ,QAAQ;AACpC;AAKO,SAAS,qBAAqB,OAAgD;AACnF,QAAM,YAAsB,CAAA;AAC5B,QAAM,cAAwB,CAAA;AAE9B,MAAI,MAAM,YAAY,QAAW;AAC/B,UAAM,gBAAgB,gBAAgB,MAAM,OAAO;AACnD,cAAU,KAAK,GAAG,cAAc,MAAM;AACtC,gBAAY,KAAK,GAAG,cAAc,QAAQ;AAAA,EAC5C,OAAO;AACL,cAAU,KAAK,qBAAqB;AAAA,EACtC;AAEA,MAAI,MAAM,SAAS,QAAW;AAC5B,UAAM,YAAY,aAAa,MAAM,IAAI;AACzC,cAAU,KAAK,GAAG,UAAU,MAAM;AAClC,gBAAY,KAAK,GAAG,UAAU,QAAQ;AAAA,EACxC;AAEA,MAAI,MAAM,YAAY,QAAW;AAC/B,UAAM,aAAa,gBAAgB,MAAM,OAAO;AAChD,cAAU,KAAK,GAAG,WAAW,MAAM;AACnC,gBAAY,KAAK,GAAG,WAAW,QAAQ;AAAA,EACzC,OAAO;AACL,cAAU,KAAK,qBAAqB;AAAA,EACtC;AAEA,MAAI,MAAM,YAAY,QAAW;AAC/B,UAAM,gBAAgB,gBAAgB,MAAM,OAAO;AACnD,cAAU,KAAK,GAAG,cAAc,MAAM;AACtC,gBAAY,KAAK,GAAG,cAAc,QAAQ;AAAA,EAC5C;AAEA,SAAO,WAAW,WAAW,WAAW;AAC1C;ACzJO,SAAS,UACd,SACA,IACA,MACA,QACA,UACA,WACc;AACd,QAAM,kBAAkB,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,QAAQ,CAAC;AAC3D,QAAM,WAAW,MAAM;AACvB,QAAM,iBAAiB,OAAO,QAAQ,EAAE,SAAS,GAAG,GAAG;AAEvD,SAAO;AAAA,IACL,IAAI,WAAW,OAAO;AAAA,IACtB,IAAI,MAAM,SAAS,IAAI,EAAE;AAAA,IACzB,QAAQ,UAAU,MAAM;AAAA,IACxB,QAAQ,GAAG,cAAc,IAAI,EAAE;AAAA,IAC/B,QAAQ,QAAQ,IAAI,YAAY,OAAO;AAAA,IACvC,QAAQ;AAAA,EAAA;AAEZ;AAUO,SAAS,QAAQ,IAA+C;AAErE,QAAM,gBAAgB,GAAG,WAAW,KAAK,IAAI,GAAG,MAAM,CAAC,IAAI;AAE3D,QAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,MAAI,aAAa,IAAI;AACnB,UAAM,IAAI,MAAM,uBAAuB,EAAE,kCAAkC;AAAA,EAC7E;AACA,SAAO;AAAA,IACL,WAAW,cAAc,MAAM,GAAG,QAAQ;AAAA,IAC1C,IAAI,cAAc,MAAM,WAAW,CAAC;AAAA,EAAA;AAExC;AAMO,SAAS,YAAY,QAAyD;AAEnF,QAAM,YAAY,OAAO,MAAM,6BAA6B;AAC5D,MAAI,CAAC,aAAa,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,CAAC,GAAG;AAChD,UAAM,IAAI;AAAA,MACR,2BAA2B,MAAM;AAAA,IAAA;AAAA,EAErC;AACA,SAAO;AAAA,IACL,MAAM,UAAU,CAAC;AAAA,IACjB,SAAS,UAAU,CAAC;AAAA,EAAA;AAExB;AAUO,SAAS,YACd,MACA,QACA,cACA,QACoE;AACpE,QAAM,UAAU,WAAU,oBAAI,KAAA,GAAO,YAAA;AACrC,QAAM,cAAwB,EAAE,QAAQ,QAAA;AACxC,MAAI,iBAAiB,QAAW;AAC9B,gBAAY,eAAe;AAAA,EAC7B;AAGA,QAAM,eAA2B,CAAC,GAAG,KAAK,WAAW,WAAW;AAChE,QAAM,WAAW,aAAa;AAK9B,SAAO;AAAA,IACL,WAAW;AAAA,IACX,eAAe;AAAA,IACf,QAAQ,KAAK;AAAA,EAAA;AAEjB;AAUO,SAAS,iBACd,aACA,YACA,QACmB;AACnB,QAAM,cAAc,WAAU,oBAAI,KAAA,GAAO,YAAA;AACzC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,EAAE,GAAG,YAAY,YAAA;AAAA,EAAY;AAEjC;AAaO,SAAS,sBACd,MACA,WACA,QAIA;AACA,QAAM,MAAM,WAAU,oBAAI,KAAA,GAAO,YAAA;AAIjC,QAAM,aAAiC,cAAc,aAAa,MAAM;AAGxE,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ,UAAU,SAAS;AAAA,IAC3B,QAAQ,KAAK;AAAA;AAAA,IACb,gBAAgB;AAAA,IAChB,GAAI,eAAe,SAAY,EAAE,WAAA,IAAe,CAAA;AAAA,IAChD,GAAgD,CAAA;AAAA,EAAC;AAKrD;ACnLO,MAAe,sBAAsB,MAAM;AAAA,EAIhD,YAAY,SAA0B,OAAiB;AACrD,UAAM,OAAO;AADuB,SAAA,QAAA;AAEpC,SAAK,OAAO,KAAK,YAAY;AAE7B,WAAO,eAAe,MAAM,WAAW,SAAS;AAAA,EAClD;AACF;AAOO,MAAM,gCAAgC,cAAc;AAAA,EAIzD,YACE,SACS,QACT;AACA,UAAM,OAAO;AAFJ,SAAA,SAAA;AAGT,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,wBAAwB,SAAS;AAAA,EAC/D;AAAA,EAVS,OAAO;AAAA,EACP,YAAY;AAUvB;AAGO,MAAM,+BAA+B,cAAc;AAAA,EAIxD,YACE,SACS,YACT,OACA;AACA,UAAM,SAAS,KAAK;AAHX,SAAA,aAAA;AAIT,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,uBAAuB,SAAS;AAAA,EAC9D;AAAA,EAXS,OAAO;AAAA,EACP,YAAY;AAWvB;AAGO,MAAM,4BAA4B,cAAc;AAAA,EAC5C,OAAO;AAAA,EACP,YAAY;AAAA,EAErB,YAAY,SAAiB,OAAiB;AAC5C,UAAM,SAAS,KAAK;AACpB,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,oBAAoB,SAAS;AAAA,EAC3D;AACF;AAGO,MAAM,8BAA8B,cAAc;AAAA,EAC9C,OAAO;AAAA,EACP,YAAY;AAAA,EAErB,YAAY,SAAiB,OAAiB;AAC5C,UAAM,SAAS,KAAK;AACpB,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,sBAAsB,SAAS;AAAA,EAC7D;AACF;AAGO,MAAM,gCAAgC,cAAc;AAAA,EAChD,OAAO;AAAA,EACP,YAAY;AAAA,EAErB,YAAY,SAAiB,OAAiB;AAC5C,UAAM,SAAS,KAAK;AACpB,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,wBAAwB,SAAS;AAAA,EAC/D;AACF;AAGO,MAAM,+BAA+B,cAAc;AAAA,EAIxD,YACE,SACS,YACT;AACA,UAAM,OAAO;AAFJ,SAAA,aAAA;AAGT,SAAK,OAAO;AACZ,WAAO,eAAe,MAAM,uBAAuB,SAAS;AAAA,EAC9D;AAAA,EAVS,OAAO;AAAA,EACP,YAAY;AAUvB;AAUO,SAAS,gBAAgBA,IAAgC;AAC9D,SAAOA,cAAa;AACtB;AAyBO,SAAS,oBAAoBA,IAA2B;AAC7D,MAAI,gBAAgBA,EAAC,GAAG;AACtB,UAAM,OAAsB;AAAA,MAC1B,MAAMA,GAAE;AAAA,MACR,SAASA,GAAE;AAAA,MACX,WAAWA,GAAE;AAAA,IAAA;AAGf,QAAIA,cAAa,2BAA2BA,GAAE,WAAW,QAAW;AAClE,WAAK,SAASA,GAAE;AAAA,IAClB;AAEA,QAAIA,cAAa,wBAAwB;AACvC,WAAK,aAAaA,GAAE;AAAA,IACtB;AAEA,WAAO;AAAA,EACT;AAEA,MAAI,OAAOA,OAAM,UAAU;AACzB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAASA;AAAA,MACT,WAAW;AAAA,IAAA;AAAA,EAEf;AAEA,MAAIA,cAAa,OAAO;AACtB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAASA,GAAE;AAAA,MACX,WAAW;AAAA,IAAA;AAAA,EAEf;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,SAAS;AAAA,IACT,WAAW;AAAA,EAAA;AAEf;"}
|