anton-bakker-feedback 0.4.3 → 1.0.0

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.
Files changed (71) hide show
  1. package/README.md +171 -0
  2. package/dist/chunks/{i18n-Cba0xHU-.js → i18n-Cn2yFMbd.js} +4 -2
  3. package/dist/chunks/i18n-Cn2yFMbd.js.map +1 -0
  4. package/dist/chunks/ingest-transport-DAA_pJNb.js +245 -0
  5. package/dist/chunks/ingest-transport-DAA_pJNb.js.map +1 -0
  6. package/dist/chunks/normalisers-Cbd-i4Jh.js +97 -0
  7. package/dist/chunks/normalisers-Cbd-i4Jh.js.map +1 -0
  8. package/dist/chunks/{observability-CfwHKObh.js → observability-DUK725zv.js} +3 -3
  9. package/dist/chunks/{observability-CfwHKObh.js.map → observability-DUK725zv.js.map} +1 -1
  10. package/dist/chunks/{submitResolver-DsfkrUHY.js → submitResolver-wfr4kbEM.js} +60 -39
  11. package/dist/chunks/submitResolver-wfr4kbEM.js.map +1 -0
  12. package/dist/core/config.d.ts +11 -0
  13. package/dist/core/config.d.ts.map +1 -1
  14. package/dist/core/index.d.ts +1 -0
  15. package/dist/core/index.d.ts.map +1 -1
  16. package/dist/core/ingest-transport.d.ts +97 -0
  17. package/dist/core/ingest-transport.d.ts.map +1 -1
  18. package/dist/core/normalisers.d.ts +29 -0
  19. package/dist/core/normalisers.d.ts.map +1 -0
  20. package/dist/core/observability.d.ts.map +1 -1
  21. package/dist/core/request-headers.d.ts +22 -0
  22. package/dist/core/request-headers.d.ts.map +1 -0
  23. package/dist/core/state-metrics.d.ts +19 -0
  24. package/dist/core/state-metrics.d.ts.map +1 -0
  25. package/dist/core/transforms.d.ts.map +1 -1
  26. package/dist/core/types.d.ts +93 -3
  27. package/dist/core/types.d.ts.map +1 -1
  28. package/dist/index.js +17 -99
  29. package/dist/index.js.map +1 -1
  30. package/dist/react/FeedbackConfirm.d.ts.map +1 -1
  31. package/dist/react/FeedbackDialogue.d.ts.map +1 -1
  32. package/dist/react/admin/ActivityTimeline.d.ts +9 -0
  33. package/dist/react/admin/ActivityTimeline.d.ts.map +1 -0
  34. package/dist/react/admin/AnnotationComposer.d.ts +30 -0
  35. package/dist/react/admin/AnnotationComposer.d.ts.map +1 -0
  36. package/dist/react/admin/FeedbackListPage.d.ts.map +1 -1
  37. package/dist/react/admin/FeedbackUpdatePage.d.ts +6 -0
  38. package/dist/react/admin/FeedbackUpdatePage.d.ts.map +1 -1
  39. package/dist/react/admin/LifecycleControls.d.ts +28 -0
  40. package/dist/react/admin/LifecycleControls.d.ts.map +1 -0
  41. package/dist/react/admin/__tests__/ActivityTimeline.test.d.ts +2 -0
  42. package/dist/react/admin/__tests__/ActivityTimeline.test.d.ts.map +1 -0
  43. package/dist/react/admin/__tests__/AnnotationComposer.test.d.ts +2 -0
  44. package/dist/react/admin/__tests__/AnnotationComposer.test.d.ts.map +1 -0
  45. package/dist/react/admin/__tests__/LifecycleControls.test.d.ts +2 -0
  46. package/dist/react/admin/__tests__/LifecycleControls.test.d.ts.map +1 -0
  47. package/dist/react/admin/hooks/can.d.ts +47 -0
  48. package/dist/react/admin/hooks/can.d.ts.map +1 -0
  49. package/dist/react/admin/hooks/useStatusTransition.d.ts +16 -2
  50. package/dist/react/admin/hooks/useStatusTransition.d.ts.map +1 -1
  51. package/dist/react/admin/index.d.ts +14 -1
  52. package/dist/react/admin/index.d.ts.map +1 -1
  53. package/dist/react/admin/index.js +312 -29
  54. package/dist/react/admin/index.js.map +1 -1
  55. package/dist/react/admin/types.d.ts +13 -0
  56. package/dist/react/admin/types.d.ts.map +1 -1
  57. package/dist/react/resolver/submitResolver.d.ts +10 -7
  58. package/dist/react/resolver/submitResolver.d.ts.map +1 -1
  59. package/dist/react.js +26 -9
  60. package/dist/react.js.map +1 -1
  61. package/dist/server/index.d.ts +4 -5
  62. package/dist/server/index.d.ts.map +1 -1
  63. package/dist/server/listHandler.d.ts +15 -8
  64. package/dist/server/listHandler.d.ts.map +1 -1
  65. package/dist/server.js +22 -170
  66. package/dist/server.js.map +1 -1
  67. package/package.json +13 -1
  68. package/dist/chunks/i18n-Cba0xHU-.js.map +0 -1
  69. package/dist/chunks/submitResolver-DsfkrUHY.js.map +0 -1
  70. package/dist/server/FeedbackService.d.ts +0 -46
  71. package/dist/server/FeedbackService.d.ts.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-metrics.d.ts","sourceRoot":"","sources":["../../src/core/state-metrics.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAkB,aAAa,EAAE,MAAM,YAAY,CAAC;AAG/E,YAAY,EAAE,aAAa,EAAE,CAAC;AAE9B;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,SAAS,GAAG,aAAa,EAAE,CAyC3F"}
@@ -1 +1 @@
1
- {"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../../src/core/transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,eAAe,EAEhB,MAAM,YAAY,CAAC;AAMpB,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,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;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,YAAY,CAad;AAMD;;;GAGG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAYrE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAYnF;AAMD;;;GAGG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,kBAAkB,EACxB,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,eAAe,GAAG,QAAQ,CAAC,CAmBpE;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,eAAe,EAAE,EAC9B,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,aAAa,CAAC,EAChD,MAAM,CAAC,EAAE,MAAM,GACd,eAAe,EAAE,CAMnB;AAMD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,cAAc,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CACL,kBAAkB,EAClB,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,gBAAgB,GAAG,YAAY,GAAG,YAAY,CAChF,CAmBA"}
1
+ {"version":3,"file":"transforms.d.ts","sourceRoot":"","sources":["../../src/core/transforms.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,cAAc,EACd,kBAAkB,EAClB,eAAe,EAEhB,MAAM,YAAY,CAAC;AAMpB,gDAAgD;AAChD,MAAM,WAAW,YAAY;IAC3B,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;AAED;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CACvB,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,MAAM,EACV,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,GAChB,YAAY,CAad;AAMD;;;GAGG;AACH,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAYrE;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAYnF;AAMD;;;GAGG;AACH,wBAAgB,WAAW,CACzB,IAAI,EAAE,kBAAkB,EACxB,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,EACrB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CAAC,kBAAkB,EAAE,WAAW,GAAG,eAAe,GAAG,QAAQ,CAAC,CAmBpE;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,eAAe,EAAE,EAC9B,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,aAAa,CAAC,EAChD,MAAM,CAAC,EAAE,MAAM,GACd,eAAe,EAAE,CAMnB;AAMD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,cAAc,EACzB,MAAM,CAAC,EAAE,MAAM,GACd,IAAI,CACL,kBAAkB,EAClB,QAAQ,GAAG,QAAQ,GAAG,QAAQ,GAAG,gBAAgB,GAAG,YAAY,GAAG,YAAY,CAChF,CAqBA"}
@@ -7,16 +7,69 @@
7
7
  /** Type of feedback being submitted */
8
8
  export type FeedbackType = 'bug' | 'feature' | 'ux' | 'other';
9
9
  /**
10
- * Lifecycle status of a feedback item.
11
- * Exactly: 'open' | 'approved' | 'deferred' | 'resolved' | 'wontfix'
10
+ * Lifecycle status of a feedback item (12-state QA lifecycle).
11
+ * Widened from 5 legacy states — see LegacyFeedbackStatus below.
12
12
  */
13
- export type FeedbackStatus = 'open' | 'approved' | 'deferred' | 'resolved' | 'wontfix';
13
+ export type FeedbackStatus = 'new' | 'triaged' | 'assigned' | 'in_progress' | 'fixed' | 'pending_retest' | 'verified' | 'closed' | 'deferred' | 'rejected' | 'duplicate' | 'reopened';
14
+ /**
15
+ * Legacy 5-state status set (pre-12-state QA lifecycle).
16
+ * For normaliser reference only — 4 of 5 values absent from new FeedbackStatus.
17
+ */
18
+ export type LegacyFeedbackStatus = 'open' | 'approved' | 'deferred' | 'resolved' | 'wontfix';
14
19
  /** Scope of the feedback issue */
15
20
  export type FeedbackScope = 'isolated' | 'cross-cutting' | 'systemic';
16
21
  /** Estimated size/effort for the feedback item */
17
22
  export type FeedbackSize = 'trivial' | 'small' | 'medium' | 'large';
18
23
  /** Environment where feedback was submitted */
19
24
  export type FeedbackEnvironment = 'development' | 'staging' | 'production';
25
+ /**
26
+ * Severity level (reporter-set) — DISTINCT from priority (team-calculated).
27
+ * IEEE 1044 separates severity (reporter's observation) from priority (team's scheduling decision).
28
+ */
29
+ export type SeverityLevel = 'blocker' | 'major' | 'minor' | 'cosmetic';
30
+ /**
31
+ * How reliably the issue can be reproduced.
32
+ */
33
+ export type Reproducibility = 'always' | 'sometimes' | 'once' | 'unknown';
34
+ /**
35
+ * Detection activity — which phase/test/charter surfaced this issue.
36
+ * Closed enum per IEEE 1044 lifecycle phases for ODC analytical value.
37
+ */
38
+ export type DetectionActivity = 'requirements_review' | 'design_review' | 'code_inspection' | 'unit_test' | 'integration_test' | 'system_test' | 'acceptance_test' | 'operational_use';
39
+ /**
40
+ * ODC (Orthogonal Defect Classification) root cause type.
41
+ * 8-value ODC Defect Type taxonomy for defect distribution analysis.
42
+ */
43
+ export type OdcRootCauseType = 'function' | 'assignment' | 'interface' | 'checking' | 'timing_serialization' | 'build_package_merge' | 'documentation' | 'algorithm';
44
+ /**
45
+ * Activity event kind — every event type a feedback item can produce.
46
+ */
47
+ export type ActivityKind = 'comment' | 'note' | 'question' | 'answer' | 'status_change' | 'assignment' | 'classification_change' | 'reopen';
48
+ /**
49
+ * Actor who performed an activity.
50
+ */
51
+ export interface ActivityActor {
52
+ userId: string;
53
+ displayName: string;
54
+ }
55
+ /**
56
+ * Append-only activity entry — canonical event model.
57
+ * before/after typed as unknown for generic usage across all event kinds.
58
+ * schemaVersion enables future upcasting without ambiguity.
59
+ */
60
+ export interface ActivityEntry {
61
+ id: string;
62
+ actor: ActivityActor;
63
+ occurredAt: string;
64
+ recordedAt: string;
65
+ source: string;
66
+ kind: ActivityKind;
67
+ body?: string;
68
+ before?: unknown;
69
+ after?: unknown;
70
+ reason?: string;
71
+ schemaVersion: number;
72
+ }
20
73
  /** A reporter who submitted or upvoted a feedback item */
21
74
  export interface Reporter {
22
75
  userId: string;
@@ -38,6 +91,35 @@ export interface CorrectionEntry {
38
91
  correctedAt: string;
39
92
  reason?: string;
40
93
  }
94
+ /**
95
+ * Structured report fields — decomposition of the content blob.
96
+ * Supports better matching and deduplication.
97
+ */
98
+ export interface ReportFields {
99
+ expected?: string;
100
+ actual?: string;
101
+ stepsToReproduce?: string[];
102
+ }
103
+ /**
104
+ * Resolution traceability fields per IEEE 1044.
105
+ * disposition is required; commit/version/verification are optional.
106
+ */
107
+ export interface ResolutionFields {
108
+ fixCommit?: string;
109
+ fixVersion?: string;
110
+ verifiedIn?: string;
111
+ disposition: string;
112
+ }
113
+ /**
114
+ * Duration spent in a particular state.
115
+ * exitedAt is undefined for the current state.
116
+ */
117
+ export interface StateDuration {
118
+ status: FeedbackStatus;
119
+ enteredAt: string;
120
+ exitedAt?: string;
121
+ durationMs: number;
122
+ }
41
123
  /** Browser and server context captured at submission time */
42
124
  export interface FeedbackContext {
43
125
  url: string;
@@ -93,6 +175,14 @@ export interface StoredFeedbackItem {
93
175
  reporterCount: number;
94
176
  dialogueHistory: DialogueEntry[];
95
177
  corrections: CorrectionEntry[];
178
+ activity?: ActivityEntry[];
179
+ severity?: SeverityLevel;
180
+ report?: ReportFields;
181
+ reproducibility?: Reproducibility;
182
+ lastWorkingVersion?: string;
183
+ resolution?: ResolutionFields;
184
+ detectionActivity?: DetectionActivity;
185
+ rootCauseType?: OdcRootCauseType;
96
186
  environment?: FeedbackEnvironment;
97
187
  version?: string;
98
188
  tags: string[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
- import { D, S, d, r, t, v } from "./chunks/i18n-Cba0xHU-.js";
2
- import { e, i, r as r2, a, w } from "./chunks/observability-CfwHKObh.js";
1
+ import { D, S, d, r, t, v } from "./chunks/i18n-Cn2yFMbd.js";
2
+ import { F, I, b, d as d2, c, a } from "./chunks/ingest-transport-DAA_pJNb.js";
3
+ import { n, a as a2, b as b2, c as c2 } from "./chunks/normalisers-Cbd-i4Jh.js";
4
+ import { e, i, r as r2, a as a3, w } from "./chunks/observability-DUK725zv.js";
3
5
  const BASE_PRIORITY = {
4
6
  bug: 60,
5
7
  ux: 40,
@@ -32,7 +34,7 @@ function buildGSI1SK(priority, id) {
32
34
  function processFeedbackCapture(input, nowIso) {
33
35
  const now = nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
34
36
  const id = generateFeedbackId(now);
35
- const status = "open";
37
+ const status = "new";
36
38
  const tags = input.tags ?? [];
37
39
  const environment = input.environment;
38
40
  const priority = calculatePriority(input.type, 1, input.hasScreenshot, environment);
@@ -130,97 +132,6 @@ function createMemoryTransport() {
130
132
  }
131
133
  };
132
134
  }
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
- }
224
135
  const VALIDATION_RULES = {
225
136
  CONTENT_MIN_LENGTH: 10,
226
137
  CONTENT_MAX_LENGTH: 5e3,
@@ -390,7 +301,7 @@ function appendCorrection(corrections, correction, nowIso) {
390
301
  }
391
302
  function applyStatusTransition(item, newStatus, nowIso) {
392
303
  const now = nowIso ?? (/* @__PURE__ */ new Date()).toISOString();
393
- const resolvedAt = newStatus === "resolved" ? now : void 0;
304
+ const resolvedAt = newStatus === "closed" ? now : void 0;
394
305
  return {
395
306
  status: newStatus,
396
307
  GSI1PK: `STATUS#${newStatus}`,
@@ -506,7 +417,7 @@ function createErrorResponse(e2) {
506
417
  }
507
418
  export {
508
419
  D as DEFAULT_LOCALE,
509
- FEEDBACK_COLLECTION,
420
+ F as FEEDBACK_COLLECTION,
510
421
  FeedbackConfigError,
511
422
  FeedbackDialogueError,
512
423
  FeedbackDuplicateError,
@@ -514,7 +425,8 @@ export {
514
425
  FeedbackScreenshotError,
515
426
  FeedbackTransportError,
516
427
  FeedbackValidationError,
517
- INGEST_WRITE_SCOPE,
428
+ I as INGEST_READ_SCOPE,
429
+ b as INGEST_WRITE_SCOPE,
518
430
  S as SUPPORTED_LOCALES,
519
431
  VALIDATION_RULES,
520
432
  addReporter,
@@ -524,18 +436,24 @@ export {
524
436
  buildKeys,
525
437
  calculatePriority,
526
438
  createErrorResponse,
527
- createIngestTransport,
439
+ d2 as createIngestAnnotateClient,
440
+ c as createIngestReader,
441
+ a as createIngestTransport,
528
442
  createMemoryTransport,
529
443
  d as detectLocale,
530
444
  e as emitMetric,
531
445
  generateFeedbackId,
532
446
  i as initTracer,
533
447
  isFeedbackError,
448
+ n as normaliseActivityArrays,
449
+ a2 as normaliseLegacyStatus,
450
+ b2 as normaliseReportContent,
451
+ c2 as normaliseStoredItem,
534
452
  parseGSI2PK,
535
453
  parseSK,
536
454
  processFeedbackCapture,
537
455
  r2 as recordDialogueExchange,
538
- a as recordSubmitEvent,
456
+ a3 as recordSubmitEvent,
539
457
  r as resolveConfig,
540
458
  t,
541
459
  validateAppName,
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/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":"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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 = 'new';\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 // 'resolvedAt' is stamped when the item reaches the terminal 'closed' state\n // (the new-lifecycle successor of the legacy 'resolved' value — see normaliseLegacyStatus).\n const resolvedAt: string | undefined = newStatus === 'closed' ? 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 +1 @@
1
- {"version":3,"file":"FeedbackConfirm.d.ts","sourceRoot":"","sources":["../../src/react/FeedbackConfirm.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAKN,MAAM,OAAO,CAAC;AAUf,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0DD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE,oBAAoB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAuV7F"}
1
+ {"version":3,"file":"FeedbackConfirm.d.ts","sourceRoot":"","sources":["../../src/react/FeedbackConfirm.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAKN,MAAM,OAAO,CAAC;AAWf,MAAM,WAAW,oBAAoB;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0DD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,EAAE,oBAAoB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAwV7F"}
@@ -1 +1 @@
1
- {"version":3,"file":"FeedbackDialogue.d.ts","sourceRoot":"","sources":["../../src/react/FeedbackDialogue.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAMN,MAAM,OAAO,CAAC;AAWf,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAmCD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,SAAS,EAAE,EAAE,qBAAqB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAiN/F"}
1
+ {"version":3,"file":"FeedbackDialogue.d.ts","sourceRoot":"","sources":["../../src/react/FeedbackDialogue.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAMN,MAAM,OAAO,CAAC;AAYf,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAmCD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,SAAS,EAAE,EAAE,qBAAqB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,GAAG,IAAI,CAoN/F"}
@@ -0,0 +1,9 @@
1
+ import { ActivityEntry } from '../../core/types.js';
2
+ import * as React from 'react';
3
+ interface ActivityTimelineProps {
4
+ activity: ActivityEntry[];
5
+ className?: string;
6
+ }
7
+ export declare function ActivityTimeline({ activity, className }: ActivityTimelineProps): React.ReactElement;
8
+ export {};
9
+ //# sourceMappingURL=ActivityTimeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ActivityTimeline.d.ts","sourceRoot":"","sources":["../../../src/react/admin/ActivityTimeline.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,UAAU,qBAAqB;IAC7B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,qBAAqB,GAAG,KAAK,CAAC,YAAY,CAuBnG"}
@@ -0,0 +1,30 @@
1
+ import { StoredFeedbackItem } from '../../core/types.js';
2
+ import { FeedbackAnnotateClient } from '../../core/ingest-transport.js';
3
+ import { CapabilityKey } from './hooks/can.js';
4
+ /**
5
+ * AnnotationComposer.tsx
6
+ * Location: src/react/admin/AnnotationComposer.tsx
7
+ *
8
+ * Capability-gated comment/note/question/answer form calling appendActivity.
9
+ * Surfaces AnnotateResult.conflictReason inline.
10
+ * Hidden entirely when no annotation capability is allowed.
11
+ */
12
+ import * as React from 'react';
13
+ export interface AnnotationComposerProps {
14
+ feedbackId: string;
15
+ item: StoredFeedbackItem;
16
+ currentUser: {
17
+ userId: string;
18
+ name?: string;
19
+ };
20
+ can: (cap: CapabilityKey) => boolean;
21
+ appendActivity: FeedbackAnnotateClient['appendActivity'];
22
+ appSource: string;
23
+ onAppended?: () => void;
24
+ }
25
+ /**
26
+ * AnnotationComposer calls appendActivity with a correctly-constructed ActivityEntry
27
+ * (id, actor, occurredAt, recordedAt, source, kind, body, schemaVersion) on submit.
28
+ */
29
+ export declare const AnnotationComposer: React.ForwardRefExoticComponent<AnnotationComposerProps & React.RefAttributes<HTMLDivElement>>;
30
+ //# sourceMappingURL=AnnotationComposer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnnotationComposer.d.ts","sourceRoot":"","sources":["../../../src/react/admin/AnnotationComposer.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,qBAAqB,CAAC;AAC7E,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEpD,MAAM,WAAW,uBAAuB;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,kBAAkB,CAAC;IACzB,WAAW,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,GAAG,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,OAAO,CAAC;IACrC,cAAc,EAAE,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,kBAAkB,gGAwG9B,CAAC"}