anton-bakker-feedback 0.2.0 → 0.3.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/dist/chunks/{i18n-RpKqSWAT.js → i18n-Du-or3T4.js} +3 -102
- package/dist/chunks/i18n-Du-or3T4.js.map +1 -0
- package/dist/chunks/observability-CfwHKObh.js +102 -0
- package/dist/chunks/observability-CfwHKObh.js.map +1 -0
- package/dist/chunks/submitResolver-DsfkrUHY.js +133 -0
- package/dist/chunks/submitResolver-DsfkrUHY.js.map +1 -0
- package/dist/config/database.d.ts +76 -0
- package/dist/config/database.d.ts.map +1 -0
- package/dist/core/formTypes.d.ts +65 -0
- package/dist/core/formTypes.d.ts.map +1 -0
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/models/StoredFeedbackItem.d.ts +79 -0
- package/dist/models/StoredFeedbackItem.d.ts.map +1 -0
- package/dist/react/admin/FeedbackDetailPage.d.ts +7 -25
- package/dist/react/admin/FeedbackDetailPage.d.ts.map +1 -1
- package/dist/react/admin/FeedbackListPage.d.ts +20 -0
- package/dist/react/admin/FeedbackListPage.d.ts.map +1 -0
- package/dist/react/admin/FeedbackUpdatePage.d.ts +8 -30
- package/dist/react/admin/FeedbackUpdatePage.d.ts.map +1 -1
- package/dist/react/admin/__tests__/FeedbackListPage.test.d.ts.map +1 -0
- package/dist/react/admin/index.d.ts +3 -2
- package/dist/react/admin/index.d.ts.map +1 -1
- package/dist/react/admin/index.js +352 -1259
- package/dist/react/admin/index.js.map +1 -1
- package/dist/react/admin/types.d.ts +20 -14
- package/dist/react/admin/types.d.ts.map +1 -1
- package/dist/react.js +9 -136
- package/dist/react.js.map +1 -1
- package/dist/server/FeedbackService.d.ts +32 -0
- package/dist/server/FeedbackService.d.ts.map +1 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/listHandler.d.ts +17 -0
- package/dist/server/listHandler.d.ts.map +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +202 -1
- package/dist/server.js.map +1 -1
- package/dist/types/context.d.ts +92 -0
- package/dist/types/context.d.ts.map +1 -0
- package/dist/types/feedback.d.ts +30 -0
- package/dist/types/feedback.d.ts.map +1 -0
- package/package.json +7 -1
- package/dist/chunks/i18n-RpKqSWAT.js.map +0 -1
- package/dist/react/admin/__tests__/FeedbackUpdatePage.integration.test.d.ts +0 -2
- package/dist/react/admin/__tests__/FeedbackUpdatePage.integration.test.d.ts.map +0 -1
- package/dist/react/admin/components/BrowserContext.d.ts +0 -32
- package/dist/react/admin/components/BrowserContext.d.ts.map +0 -1
- package/dist/react/admin/components/DataTable.d.ts +0 -55
- package/dist/react/admin/components/DataTable.d.ts.map +0 -1
- package/dist/react/admin/components/DialogueHistory.d.ts +0 -27
- package/dist/react/admin/components/DialogueHistory.d.ts.map +0 -1
- package/dist/react/admin/components/EditableFields.d.ts +0 -126
- package/dist/react/admin/components/EditableFields.d.ts.map +0 -1
- package/dist/react/admin/components/FeedbackListPage.d.ts +0 -19
- package/dist/react/admin/components/FeedbackListPage.d.ts.map +0 -1
- package/dist/react/admin/components/FeedbackListPage.test.d.ts.map +0 -1
- package/dist/react/admin/components/FilterControls.d.ts +0 -28
- package/dist/react/admin/components/FilterControls.d.ts.map +0 -1
- package/dist/react/admin/components/NavigationLink.d.ts +0 -31
- package/dist/react/admin/components/NavigationLink.d.ts.map +0 -1
- package/dist/react/admin/components/QuickActions.d.ts +0 -43
- package/dist/react/admin/components/QuickActions.d.ts.map +0 -1
- package/dist/react/admin/components/ScreenshotViewer.d.ts +0 -38
- package/dist/react/admin/components/ScreenshotViewer.d.ts.map +0 -1
- package/dist/react/admin/components/SortControls.d.ts +0 -29
- package/dist/react/admin/components/SortControls.d.ts.map +0 -1
- package/dist/react/admin/components/StatusBadge.d.ts +0 -23
- package/dist/react/admin/components/StatusBadge.d.ts.map +0 -1
- package/dist/react/admin/components/TypeBadge.d.ts +0 -23
- package/dist/react/admin/components/TypeBadge.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/BrowserContext.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/BrowserContext.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/DialogueHistory.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/DialogueHistory.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/EditableFields.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/EditableFields.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/QuickActions.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/QuickActions.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/ScreenshotViewer.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/ScreenshotViewer.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/badges.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/badges.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/controls.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/controls.test.d.ts.map +0 -1
- package/dist/react/admin/components/__tests__/table-navigation.test.d.ts +0 -2
- package/dist/react/admin/components/__tests__/table-navigation.test.d.ts.map +0 -1
- package/dist/react/admin/components/index.d.ts +0 -13
- package/dist/react/admin/components/index.d.ts.map +0 -1
- package/dist/react/admin/hooks/useFeedbackFilters.d.ts +0 -31
- package/dist/react/admin/hooks/useFeedbackFilters.d.ts.map +0 -1
- package/dist/react/admin/hooks/useFeedbackSort.d.ts +0 -42
- package/dist/react/admin/hooks/useFeedbackSort.d.ts.map +0 -1
- /package/dist/react/admin/{components → __tests__}/FeedbackListPage.test.d.ts +0 -0
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/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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"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/models/StoredFeedbackItem.ts
|
|
3
|
+
*
|
|
4
|
+
* Unified StoredFeedbackItem model and schema re-export.
|
|
5
|
+
*
|
|
6
|
+
* The canonical DynamoDB row definition lives in src/core/types.ts.
|
|
7
|
+
* This module provides the model-layer surface with type-specific
|
|
8
|
+
* field extensions and validation helpers.
|
|
9
|
+
*/
|
|
10
|
+
export type { StoredFeedbackItem, FeedbackType, FeedbackStatus, FeedbackScope, FeedbackSize, FeedbackEnvironment, Reporter, DialogueEntry, CorrectionEntry, FeedbackContext, } from '../core/types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Bug-specific fields captured during feedback submission.
|
|
13
|
+
* Used when type === 'bug'.
|
|
14
|
+
*/
|
|
15
|
+
export interface BugFields {
|
|
16
|
+
/** What the user expected to happen */
|
|
17
|
+
expectedBehaviour?: string;
|
|
18
|
+
/** Steps to reproduce the bug */
|
|
19
|
+
stepsToReproduce?: string;
|
|
20
|
+
/** Actual behaviour observed */
|
|
21
|
+
actualBehaviour?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Feature request-specific fields.
|
|
25
|
+
* Used when type === 'feature'.
|
|
26
|
+
*/
|
|
27
|
+
export interface FeatureFields {
|
|
28
|
+
/** Business justification for the feature */
|
|
29
|
+
businessValue?: string;
|
|
30
|
+
/** Proposed solution or approach */
|
|
31
|
+
proposedSolution?: string;
|
|
32
|
+
/** Who would benefit from this feature */
|
|
33
|
+
beneficiaries?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Task-specific fields.
|
|
37
|
+
* Used when type === 'task'.
|
|
38
|
+
*/
|
|
39
|
+
export interface TaskFields {
|
|
40
|
+
/** Desired outcome of the task */
|
|
41
|
+
desiredOutcome?: string;
|
|
42
|
+
/** Deadline or urgency notes */
|
|
43
|
+
deadline?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Fields populated by AI enrichment after submission.
|
|
47
|
+
* All optional — populated asynchronously by the AI service.
|
|
48
|
+
*/
|
|
49
|
+
export interface AIEnrichedFields {
|
|
50
|
+
/** AI-calculated priority score 0–100 */
|
|
51
|
+
priority?: number;
|
|
52
|
+
/** AI-determined scope classification */
|
|
53
|
+
scope?: 'isolated' | 'cross-cutting' | 'systemic';
|
|
54
|
+
/** AI-generated tags */
|
|
55
|
+
tags?: string[];
|
|
56
|
+
/** AI-estimated size/effort */
|
|
57
|
+
size?: 'trivial' | 'small' | 'medium' | 'large';
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build all DynamoDB key fields for a StoredFeedbackItem.
|
|
61
|
+
* Used by the server-side storage layer to populate key fields
|
|
62
|
+
* before writing to DynamoDB.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildKeys(params: {
|
|
65
|
+
appName: string;
|
|
66
|
+
id: string;
|
|
67
|
+
type: string;
|
|
68
|
+
status: string;
|
|
69
|
+
priority: number;
|
|
70
|
+
timestamp: string;
|
|
71
|
+
}): {
|
|
72
|
+
PK: string;
|
|
73
|
+
SK: string;
|
|
74
|
+
GSI1PK: string;
|
|
75
|
+
GSI1SK: string;
|
|
76
|
+
GSI2PK: string;
|
|
77
|
+
GSI2SK: string;
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=StoredFeedbackItem.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StoredFeedbackItem.d.ts","sourceRoot":"","sources":["../../src/models/StoredFeedbackItem.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,YAAY,EACV,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,QAAQ,EACR,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,kBAAkB,CAAC;AAM1B;;;GAGG;AACH,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,iCAAiC;IACjC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gCAAgC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oCAAoC;IACpC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,kCAAkC;IAClC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,KAAK,CAAC,EAAE,UAAU,GAAG,eAAe,GAAG,UAAU,CAAC;IAClD,wBAAwB;IACxB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,+BAA+B;IAC/B,IAAI,CAAC,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,CAAC;CACjD;AAMD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG;IACF,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB,CAYA"}
|
|
@@ -1,30 +1,12 @@
|
|
|
1
|
-
import { default as React } from 'react';
|
|
2
1
|
import { FeedbackDetailPageProps } from './types.js';
|
|
3
|
-
|
|
4
2
|
/**
|
|
5
|
-
* FeedbackDetailPage
|
|
3
|
+
* FeedbackDetailPage.tsx
|
|
4
|
+
* Location: src/react/admin/FeedbackDetailPage.tsx
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* Renders:
|
|
11
|
-
* - Header: title (content truncated), type badge, status badge, priority
|
|
12
|
-
* - Metadata grid: environment, submitted date, reporter count, assignee
|
|
13
|
-
* - Description / full content
|
|
14
|
-
* - Dialogue history (conversation turns)
|
|
15
|
-
* - Screenshot viewer
|
|
16
|
-
* - Browser context
|
|
17
|
-
* - Quick actions (status transitions, edit, back to list)
|
|
18
|
-
*
|
|
19
|
-
* No styling applied — consumer provides CSS via `className` prop.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* <FeedbackDetailPage
|
|
23
|
-
* feedbackId="FB-2026-07-28-abc12345"
|
|
24
|
-
* fetchItem={(id) => api.getFeedback(id)}
|
|
25
|
-
* onNavigate={(path) => router.push(path)}
|
|
26
|
-
* currentUser={{ userId: 'user-1', name: 'Alice' }}
|
|
27
|
-
* />
|
|
6
|
+
* Admin detail page for viewing a single feedback item.
|
|
7
|
+
* Uses DetailPage + DetailSection + DetailRow from anton-bakker-tailwind-components.
|
|
28
8
|
*/
|
|
29
|
-
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
declare const FeedbackDetailPage: React.ForwardRefExoticComponent<FeedbackDetailPageProps & React.RefAttributes<HTMLDivElement>>;
|
|
11
|
+
export { FeedbackDetailPage };
|
|
30
12
|
//# sourceMappingURL=FeedbackDetailPage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FeedbackDetailPage.d.ts","sourceRoot":"","sources":["../../../src/react/admin/FeedbackDetailPage.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"FeedbackDetailPage.d.ts","sourceRoot":"","sources":["../../../src/react/admin/FeedbackDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAG1D,QAAA,MAAM,kBAAkB,gGAoGvB,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FeedbackStatus, FeedbackType } from '../../core/types.js';
|
|
2
|
+
import { FeedbackListPageProps } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* FeedbackListPage.tsx
|
|
5
|
+
* Location: src/react/admin/FeedbackListPage.tsx
|
|
6
|
+
*
|
|
7
|
+
* Admin list page for browsing and filtering feedback items.
|
|
8
|
+
* Thin wrapper around ListPage from anton-bakker-tailwind-components
|
|
9
|
+
* for visual consistency with sagrosoft20.
|
|
10
|
+
*/
|
|
11
|
+
import * as React from 'react';
|
|
12
|
+
declare function StatusBadge({ status }: {
|
|
13
|
+
status: FeedbackStatus;
|
|
14
|
+
}): React.JSX.Element;
|
|
15
|
+
declare function TypeBadge({ type }: {
|
|
16
|
+
type: FeedbackType;
|
|
17
|
+
}): React.JSX.Element;
|
|
18
|
+
declare const FeedbackListPage: React.ForwardRefExoticComponent<FeedbackListPageProps & React.RefAttributes<HTMLDivElement>>;
|
|
19
|
+
export { FeedbackListPage, StatusBadge, TypeBadge };
|
|
20
|
+
//# sourceMappingURL=FeedbackListPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedbackListPage.d.ts","sourceRoot":"","sources":["../../../src/react/admin/FeedbackListPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAsB,cAAc,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC5F,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAiBxD,iBAAS,WAAW,CAAC,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,cAAc,CAAA;CAAE,qBAE1D;AAED,iBAAS,SAAS,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,YAAY,CAAA;CAAE,qBAElD;AAED,QAAA,MAAM,gBAAgB,8FA0FrB,CAAC;AAGF,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC"}
|
|
@@ -1,34 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { StoredFeedbackItem } from '../../core/types.js';
|
|
3
|
-
|
|
4
|
-
export interface FeedbackUpdatePageProps {
|
|
5
|
-
/** The ID of the feedback item to edit */
|
|
6
|
-
feedbackId: string;
|
|
7
|
-
/** Fetches the item by ID; result populates the form */
|
|
8
|
-
fetchItem: (id: string) => Promise<StoredFeedbackItem>;
|
|
9
|
-
/** Persists the changed fields; receives only the diff-patch */
|
|
10
|
-
updateItem: (id: string, patch: Partial<StoredFeedbackItem>) => Promise<void>;
|
|
11
|
-
/** Called to navigate (back to detail, cancel, after save) */
|
|
12
|
-
onNavigate: (path: string) => void;
|
|
13
|
-
/** Optional current user for "Assign to me" and correction tracking */
|
|
14
|
-
currentUser?: {
|
|
15
|
-
userId: string;
|
|
16
|
-
name?: string;
|
|
17
|
-
};
|
|
18
|
-
/** Optional CSS class on the root element */
|
|
19
|
-
className?: string;
|
|
20
|
-
}
|
|
1
|
+
import { FeedbackUpdatePageProps } from './types.js';
|
|
21
2
|
/**
|
|
22
|
-
* FeedbackUpdatePage
|
|
3
|
+
* FeedbackUpdatePage.tsx
|
|
4
|
+
* Location: src/react/admin/FeedbackUpdatePage.tsx
|
|
23
5
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* feedbackId="FB-2026-07-28-abc12345"
|
|
27
|
-
* fetchItem={api.getItem}
|
|
28
|
-
* updateItem={api.updateItem}
|
|
29
|
-
* onNavigate={navigate}
|
|
30
|
-
* currentUser={{ userId: 'user-123' }}
|
|
31
|
-
* />
|
|
6
|
+
* Admin update page for editing mutable fields of a feedback item.
|
|
7
|
+
* Uses UpdatePage + EnumField + TextField + TextareaField from anton-bakker-tailwind-components.
|
|
32
8
|
*/
|
|
33
|
-
|
|
9
|
+
import * as React from 'react';
|
|
10
|
+
declare const FeedbackUpdatePage: React.ForwardRefExoticComponent<FeedbackUpdatePageProps & React.RefAttributes<HTMLDivElement>>;
|
|
11
|
+
export { FeedbackUpdatePage };
|
|
34
12
|
//# sourceMappingURL=FeedbackUpdatePage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FeedbackUpdatePage.d.ts","sourceRoot":"","sources":["../../../src/react/admin/FeedbackUpdatePage.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"FeedbackUpdatePage.d.ts","sourceRoot":"","sources":["../../../src/react/admin/FeedbackUpdatePage.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAiC1D,QAAA,MAAM,kBAAkB,gGAiMvB,CAAC;AAGF,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"FeedbackListPage.test.d.ts","sourceRoot":"","sources":["../../../../src/react/admin/__tests__/FeedbackListPage.test.tsx"],"names":[],"mappings":""}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export { FeedbackListPage } from './
|
|
1
|
+
export { FeedbackListPage, StatusBadge, TypeBadge } from './FeedbackListPage.js';
|
|
2
2
|
export { FeedbackDetailPage } from './FeedbackDetailPage.js';
|
|
3
3
|
export { FeedbackUpdatePage } from './FeedbackUpdatePage.js';
|
|
4
|
-
export
|
|
4
|
+
export { useStatusTransition } from './hooks/useStatusTransition.js';
|
|
5
|
+
export type { AdminPageProps, FeedbackListPageProps, FeedbackDetailPageProps, FeedbackUpdatePageProps, } from './types.js';
|
|
5
6
|
export type { StoredFeedbackItem, FeedbackStatus, FeedbackType, FeedbackScope, FeedbackSize, FeedbackEnvironment, Reporter, DialogueEntry, CorrectionEntry, FeedbackContext, } from '../../core/types.js';
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/admin/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/admin/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAG7D,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAGrE,YAAY,EACV,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,YAAY,CAAC;AAGpB,YAAY,EACV,kBAAkB,EAClB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,mBAAmB,EACnB,QAAQ,EACR,aAAa,EACb,eAAe,EACf,eAAe,GAChB,MAAM,qBAAqB,CAAC"}
|