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.
- package/README.md +171 -0
- package/dist/chunks/{i18n-Cba0xHU-.js → i18n-Cn2yFMbd.js} +4 -2
- package/dist/chunks/i18n-Cn2yFMbd.js.map +1 -0
- package/dist/chunks/ingest-transport-DAA_pJNb.js +245 -0
- package/dist/chunks/ingest-transport-DAA_pJNb.js.map +1 -0
- package/dist/chunks/normalisers-Cbd-i4Jh.js +97 -0
- package/dist/chunks/normalisers-Cbd-i4Jh.js.map +1 -0
- package/dist/chunks/{observability-CfwHKObh.js → observability-DUK725zv.js} +3 -3
- package/dist/chunks/{observability-CfwHKObh.js.map → observability-DUK725zv.js.map} +1 -1
- package/dist/chunks/{submitResolver-DsfkrUHY.js → submitResolver-wfr4kbEM.js} +60 -39
- package/dist/chunks/submitResolver-wfr4kbEM.js.map +1 -0
- package/dist/core/config.d.ts +11 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/ingest-transport.d.ts +97 -0
- package/dist/core/ingest-transport.d.ts.map +1 -1
- package/dist/core/normalisers.d.ts +29 -0
- package/dist/core/normalisers.d.ts.map +1 -0
- package/dist/core/observability.d.ts.map +1 -1
- package/dist/core/request-headers.d.ts +22 -0
- package/dist/core/request-headers.d.ts.map +1 -0
- package/dist/core/state-metrics.d.ts +19 -0
- package/dist/core/state-metrics.d.ts.map +1 -0
- package/dist/core/transforms.d.ts.map +1 -1
- package/dist/core/types.d.ts +93 -3
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.js +17 -99
- package/dist/index.js.map +1 -1
- package/dist/react/FeedbackConfirm.d.ts.map +1 -1
- package/dist/react/FeedbackDialogue.d.ts.map +1 -1
- package/dist/react/admin/ActivityTimeline.d.ts +9 -0
- package/dist/react/admin/ActivityTimeline.d.ts.map +1 -0
- package/dist/react/admin/AnnotationComposer.d.ts +30 -0
- package/dist/react/admin/AnnotationComposer.d.ts.map +1 -0
- package/dist/react/admin/FeedbackListPage.d.ts.map +1 -1
- package/dist/react/admin/FeedbackUpdatePage.d.ts +6 -0
- package/dist/react/admin/FeedbackUpdatePage.d.ts.map +1 -1
- package/dist/react/admin/LifecycleControls.d.ts +28 -0
- package/dist/react/admin/LifecycleControls.d.ts.map +1 -0
- package/dist/react/admin/__tests__/ActivityTimeline.test.d.ts +2 -0
- package/dist/react/admin/__tests__/ActivityTimeline.test.d.ts.map +1 -0
- package/dist/react/admin/__tests__/AnnotationComposer.test.d.ts +2 -0
- package/dist/react/admin/__tests__/AnnotationComposer.test.d.ts.map +1 -0
- package/dist/react/admin/__tests__/LifecycleControls.test.d.ts +2 -0
- package/dist/react/admin/__tests__/LifecycleControls.test.d.ts.map +1 -0
- package/dist/react/admin/hooks/can.d.ts +47 -0
- package/dist/react/admin/hooks/can.d.ts.map +1 -0
- package/dist/react/admin/hooks/useStatusTransition.d.ts +16 -2
- package/dist/react/admin/hooks/useStatusTransition.d.ts.map +1 -1
- package/dist/react/admin/index.d.ts +14 -1
- package/dist/react/admin/index.d.ts.map +1 -1
- package/dist/react/admin/index.js +312 -29
- package/dist/react/admin/index.js.map +1 -1
- package/dist/react/admin/types.d.ts +13 -0
- package/dist/react/admin/types.d.ts.map +1 -1
- package/dist/react/resolver/submitResolver.d.ts +10 -7
- package/dist/react/resolver/submitResolver.d.ts.map +1 -1
- package/dist/react.js +26 -9
- package/dist/react.js.map +1 -1
- package/dist/server/index.d.ts +4 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/listHandler.d.ts +15 -8
- package/dist/server/listHandler.d.ts.map +1 -1
- package/dist/server.js +22 -170
- package/dist/server.js.map +1 -1
- package/package.json +13 -1
- package/dist/chunks/i18n-Cba0xHU-.js.map +0 -1
- package/dist/chunks/submitResolver-DsfkrUHY.js.map +0 -1
- package/dist/server/FeedbackService.d.ts +0 -46
- package/dist/server/FeedbackService.d.ts.map +0 -1
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const NORMALISER_SCHEMA_VERSION = 1;
|
|
2
|
+
const LEGACY_STATUS_MAP = {
|
|
3
|
+
open: "new",
|
|
4
|
+
approved: "triaged",
|
|
5
|
+
deferred: "deferred",
|
|
6
|
+
resolved: "closed",
|
|
7
|
+
wontfix: "rejected"
|
|
8
|
+
};
|
|
9
|
+
const NEW_STATUS_VALUES = /* @__PURE__ */ new Set([
|
|
10
|
+
"new",
|
|
11
|
+
"triaged",
|
|
12
|
+
"assigned",
|
|
13
|
+
"in_progress",
|
|
14
|
+
"fixed",
|
|
15
|
+
"pending_retest",
|
|
16
|
+
"verified",
|
|
17
|
+
"closed",
|
|
18
|
+
"deferred",
|
|
19
|
+
"rejected",
|
|
20
|
+
"duplicate",
|
|
21
|
+
"reopened"
|
|
22
|
+
]);
|
|
23
|
+
function normaliseLegacyStatus(status) {
|
|
24
|
+
if (NEW_STATUS_VALUES.has(status)) return status;
|
|
25
|
+
const mapped = LEGACY_STATUS_MAP[status];
|
|
26
|
+
return mapped ?? "new";
|
|
27
|
+
}
|
|
28
|
+
function stableActivityId(sourceKind, index, occurredAt) {
|
|
29
|
+
return `legacy:${sourceKind}:${index}:${occurredAt}`;
|
|
30
|
+
}
|
|
31
|
+
function dialogueToActivity(entry, index) {
|
|
32
|
+
const kind = entry.role === "assistant" ? "answer" : "comment";
|
|
33
|
+
return {
|
|
34
|
+
id: stableActivityId("dialogue", index, entry.timestamp),
|
|
35
|
+
actor: { userId: entry.role, displayName: entry.role },
|
|
36
|
+
occurredAt: entry.timestamp,
|
|
37
|
+
recordedAt: entry.timestamp,
|
|
38
|
+
source: "legacy-normaliser",
|
|
39
|
+
kind,
|
|
40
|
+
body: entry.content,
|
|
41
|
+
schemaVersion: NORMALISER_SCHEMA_VERSION
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function correctionToActivity(entry, index) {
|
|
45
|
+
const isStatus = entry.field === "status";
|
|
46
|
+
const kind = isStatus ? "status_change" : "classification_change";
|
|
47
|
+
const before = isStatus ? normaliseLegacyStatus(entry.before) : entry.before;
|
|
48
|
+
const after = isStatus ? normaliseLegacyStatus(entry.after) : entry.after;
|
|
49
|
+
return {
|
|
50
|
+
id: stableActivityId("correction", index, entry.correctedAt),
|
|
51
|
+
actor: { userId: entry.correctedBy, displayName: entry.correctedBy },
|
|
52
|
+
occurredAt: entry.correctedAt,
|
|
53
|
+
recordedAt: entry.correctedAt,
|
|
54
|
+
source: "legacy-normaliser",
|
|
55
|
+
kind,
|
|
56
|
+
before,
|
|
57
|
+
after,
|
|
58
|
+
...entry.reason !== void 0 ? { reason: entry.reason } : {},
|
|
59
|
+
schemaVersion: NORMALISER_SCHEMA_VERSION
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function normaliseActivityArrays(item) {
|
|
63
|
+
const existing = item.activity ?? [];
|
|
64
|
+
const existingIds = new Set(existing.map((e) => e.id));
|
|
65
|
+
const synthesised = [
|
|
66
|
+
...(item.dialogueHistory ?? []).map((d, i) => dialogueToActivity(d, i)),
|
|
67
|
+
...(item.corrections ?? []).map((c, i) => correctionToActivity(c, i))
|
|
68
|
+
];
|
|
69
|
+
const toAppend = synthesised.filter((e) => !existingIds.has(e.id));
|
|
70
|
+
if (toAppend.length === 0) {
|
|
71
|
+
return item.activity ? item : { ...item, activity: existing };
|
|
72
|
+
}
|
|
73
|
+
const merged = [...existing, ...toAppend].sort(
|
|
74
|
+
(a, b) => new Date(a.occurredAt).getTime() - new Date(b.occurredAt).getTime()
|
|
75
|
+
);
|
|
76
|
+
return { ...item, activity: merged };
|
|
77
|
+
}
|
|
78
|
+
function normaliseReportContent(item) {
|
|
79
|
+
const hasActual = Boolean(item.report?.actual);
|
|
80
|
+
const content = item.content?.trim();
|
|
81
|
+
if (hasActual || !content) return item;
|
|
82
|
+
const report = { ...item.report ?? {}, actual: item.content };
|
|
83
|
+
return { ...item, report };
|
|
84
|
+
}
|
|
85
|
+
function normaliseStoredItem(item) {
|
|
86
|
+
const status = normaliseLegacyStatus(item.status);
|
|
87
|
+
const withStatus = status === item.status ? item : { ...item, status };
|
|
88
|
+
const withActivity = normaliseActivityArrays(withStatus);
|
|
89
|
+
return normaliseReportContent(withActivity);
|
|
90
|
+
}
|
|
91
|
+
export {
|
|
92
|
+
normaliseLegacyStatus as a,
|
|
93
|
+
normaliseReportContent as b,
|
|
94
|
+
normaliseStoredItem as c,
|
|
95
|
+
normaliseActivityArrays as n
|
|
96
|
+
};
|
|
97
|
+
//# sourceMappingURL=normalisers-Cbd-i4Jh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalisers-Cbd-i4Jh.js","sources":["../../src/core/normalisers.ts"],"sourcesContent":["/**\n * normalisers.ts — read-time legacy normalisers (core layer, ZERO framework deps).\n *\n * Location: src/core/normalisers.ts\n *\n * Purpose: reconcile legacy stored rows into the current canonical model WITHOUT ever\n * rewriting stored data. Pure, idempotent, side-effect-free functions. Consumed by\n * server code, the admin UI, and future CLIs — so it lives in the zero-dependency core\n * layer (imports only ./types.js).\n *\n * Contract (design/feedback-annotations/02-lifecycle-and-guard/DESIGN.md §WP-1):\n * - normaliseLegacyStatus: map the 5 legacy status values onto the 12-state QA lifecycle;\n * new values pass through unchanged.\n * - normaliseActivityArrays: merge dialogueHistory + corrections into activity[] (Tolerant\n * Reader), idempotent by a deterministic per-entry id.\n * - normaliseReportContent: content-only legacy item → report.actual = content fallback.\n * - normaliseStoredItem: compose all three; never mutate the input (returns a new object).\n */\n\nimport type {\n StoredFeedbackItem,\n FeedbackStatus,\n LegacyFeedbackStatus,\n ActivityEntry,\n ActivityKind,\n DialogueEntry,\n CorrectionEntry,\n ReportFields,\n} from './types.js';\n\n/** Schema version stamped on activity entries synthesised by the normaliser. */\nconst NORMALISER_SCHEMA_VERSION = 1;\n\n/**\n * Legacy 5-state → 12-state QA lifecycle mapping (design §WP-1).\n * open → new, approved → triaged, deferred → deferred, resolved → closed, wontfix → rejected\n */\nconst LEGACY_STATUS_MAP: Record<LegacyFeedbackStatus, FeedbackStatus> = {\n open: 'new',\n approved: 'triaged',\n deferred: 'deferred',\n resolved: 'closed',\n wontfix: 'rejected',\n};\n\n/** The set of valid new-lifecycle status values (for pass-through detection). */\nconst NEW_STATUS_VALUES: ReadonlySet<string> = new Set<FeedbackStatus>([\n 'new', 'triaged', 'assigned', 'in_progress', 'fixed', 'pending_retest',\n 'verified', 'closed', 'deferred', 'rejected', 'duplicate', 'reopened',\n]);\n\n/**\n * Map a legacy status value to the new QA lifecycle. New values pass through unchanged.\n * Unknown values fall back to 'new' (Tolerant Reader — never throw on stored data).\n *\n * @param status - a legacy or new status value\n * @returns the equivalent new-lifecycle FeedbackStatus\n */\nexport function normaliseLegacyStatus(status: LegacyFeedbackStatus | FeedbackStatus): FeedbackStatus {\n if (NEW_STATUS_VALUES.has(status)) return status as FeedbackStatus;\n const mapped = LEGACY_STATUS_MAP[status as LegacyFeedbackStatus];\n return mapped ?? 'new';\n}\n\n/**\n * Deterministic per-entry id for a synthesised activity entry. Stable across repeated\n * normalisation (idempotency): same source field + position → same id, so re-merging\n * never duplicates an entry.\n */\nfunction stableActivityId(sourceKind: string, index: number, occurredAt: string): string {\n return `legacy:${sourceKind}:${index}:${occurredAt}`;\n}\n\n/** Convert a legacy DialogueEntry to an ActivityEntry (user → comment, assistant → answer). */\nfunction dialogueToActivity(entry: DialogueEntry, index: number): ActivityEntry {\n const kind: ActivityKind = entry.role === 'assistant' ? 'answer' : 'comment';\n return {\n id: stableActivityId('dialogue', index, entry.timestamp),\n actor: { userId: entry.role, displayName: entry.role },\n occurredAt: entry.timestamp,\n recordedAt: entry.timestamp,\n source: 'legacy-normaliser',\n kind,\n body: entry.content,\n schemaVersion: NORMALISER_SCHEMA_VERSION,\n };\n}\n\n/**\n * Convert a legacy CorrectionEntry to an ActivityEntry. A correction to the `status` field\n * becomes a `status_change` entry (so deriveStateDurations can compute MTTR/time-in-state\n * from activity[] alone); any other field becomes a `classification_change`. The new status\n * value is normalised onto the current lifecycle.\n */\nfunction correctionToActivity(entry: CorrectionEntry, index: number): ActivityEntry {\n const isStatus = entry.field === 'status';\n const kind: ActivityKind = isStatus ? 'status_change' : 'classification_change';\n const before = isStatus\n ? normaliseLegacyStatus(entry.before as LegacyFeedbackStatus | FeedbackStatus)\n : entry.before;\n const after = isStatus\n ? normaliseLegacyStatus(entry.after as LegacyFeedbackStatus | FeedbackStatus)\n : entry.after;\n return {\n id: stableActivityId('correction', index, entry.correctedAt),\n actor: { userId: entry.correctedBy, displayName: entry.correctedBy },\n occurredAt: entry.correctedAt,\n recordedAt: entry.correctedAt,\n source: 'legacy-normaliser',\n kind,\n before,\n after,\n ...(entry.reason !== undefined ? { reason: entry.reason } : {}),\n schemaVersion: NORMALISER_SCHEMA_VERSION,\n };\n}\n\n/**\n * Merge legacy dialogueHistory + corrections into activity[]. Idempotent: entries already\n * present (by stable id) are not re-appended, so applying twice equals applying once.\n * Returns a new object; never mutates the input. Stored data is never rewritten.\n */\nexport function normaliseActivityArrays(item: StoredFeedbackItem): StoredFeedbackItem {\n const existing = item.activity ?? [];\n const existingIds = new Set(existing.map((e) => e.id));\n\n const synthesised: ActivityEntry[] = [\n ...(item.dialogueHistory ?? []).map((d, i) => dialogueToActivity(d, i)),\n ...(item.corrections ?? []).map((c, i) => correctionToActivity(c, i)),\n ];\n\n const toAppend = synthesised.filter((e) => !existingIds.has(e.id));\n if (toAppend.length === 0) {\n // Nothing new to merge — return input shape unchanged (but still a defined activity[]).\n return item.activity ? item : { ...item, activity: existing };\n }\n\n const merged = [...existing, ...toAppend].sort(\n (a, b) => new Date(a.occurredAt).getTime() - new Date(b.occurredAt).getTime(),\n );\n return { ...item, activity: merged };\n}\n\n/**\n * Tolerant-Reader fallback for the structured report: if report.actual is not set and the\n * legacy `content` blob is non-empty, set report.actual = content. The content field is\n * always preserved. Returns a new object; never mutates the input.\n */\nexport function normaliseReportContent(item: StoredFeedbackItem): StoredFeedbackItem {\n const hasActual = Boolean(item.report?.actual);\n const content = item.content?.trim();\n if (hasActual || !content) return item;\n\n const report: ReportFields = { ...(item.report ?? {}), actual: item.content };\n return { ...item, report };\n}\n\n/**\n * Apply all read-time normalisers in sequence: legacy status → activity merge → report\n * content fallback. Pure and idempotent; returns a new object and never mutates the input.\n * Rendering/consumer code should call this before reading status or any optional field.\n */\nexport function normaliseStoredItem(item: StoredFeedbackItem): StoredFeedbackItem {\n const status = normaliseLegacyStatus(item.status as LegacyFeedbackStatus | FeedbackStatus);\n const withStatus: StoredFeedbackItem = status === item.status ? item : { ...item, status };\n const withActivity = normaliseActivityArrays(withStatus);\n return normaliseReportContent(withActivity);\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -65,10 +65,10 @@ function emitMetric(metricName, value, unit, dimensions) {
|
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
try {
|
|
68
|
-
const
|
|
68
|
+
const cloudwatchModuleId = "@aws-sdk/client-cloudwatch";
|
|
69
69
|
const { CloudWatchClient, PutMetricDataCommand } = await import(
|
|
70
70
|
/* @vite-ignore */
|
|
71
|
-
|
|
71
|
+
cloudwatchModuleId
|
|
72
72
|
);
|
|
73
73
|
const client = new CloudWatchClient({ region: process.env["AWS_REGION"] });
|
|
74
74
|
await client.send(
|
|
@@ -99,4 +99,4 @@ export {
|
|
|
99
99
|
recordDialogueExchange as r,
|
|
100
100
|
withSpan as w
|
|
101
101
|
};
|
|
102
|
-
//# sourceMappingURL=observability-
|
|
102
|
+
//# sourceMappingURL=observability-DUK725zv.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability-
|
|
1
|
+
{"version":3,"file":"observability-DUK725zv.js","sources":["../../src/core/observability.ts"],"sourcesContent":["/**\n * OpenTelemetry span helpers with zero AWS dependencies.\n *\n * @opentelemetry/api is declared as an optional peer dependency.\n * If the consuming app has it installed, traces are emitted.\n * If not, functions degrade gracefully to no-ops.\n *\n * CRITICAL: No hard import of '@opentelemetry/api' — only dynamic import.\n * Dynamic import used for runtime availability check.\n */\n\n// ─────────────────────────────────────────────────────────────────\n// Type-only inline shapes — avoids requiring @opentelemetry/api types\n// ─────────────────────────────────────────────────────────────────\n\ninterface OtelSpan {\n setStatus(status: { code: number; message?: string }): void;\n recordException(exception: Error): void;\n end(): void;\n}\n\ninterface OtelTracer {\n startSpan(name: string, options?: { attributes?: Record<string, string | number | boolean> }): OtelSpan;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Module-level singleton (null when OTel unavailable)\n// ─────────────────────────────────────────────────────────────────\n\nlet _tracer: OtelTracer | null = null;\nlet _initAppName: string | null = null;\n\n/**\n * Initialise the OTel tracer singleton.\n * Idempotent — calling twice with the same appName is a no-op.\n * Safe to call when @opentelemetry/api is not installed (no-op).\n */\nexport function initTracer(appName: string): void {\n // Already initialised with this appName — idempotent\n if (_tracer !== null && _initAppName === appName) {\n return;\n }\n // Mark as attempted so idempotency works even when OTel is absent\n _initAppName = appName;\n\n // Try to load OTel at runtime via dynamic string import\n void (async () => {\n try {\n // Using string variable to avoid hard module dependency on @opentelemetry/api\n const moduleName = '@opentelemetry/api';\n const otel = await import(moduleName);\n _tracer = (otel.trace.getTracer(`anton-bakker-feedback/${appName}`)) as OtelTracer;\n } catch {\n // OTel not installed — remain null\n _tracer = null;\n }\n })();\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Span Wrapper\n// ─────────────────────────────────────────────────────────────────\n\n/** SpanStatusCode OK = 1, ERROR = 2 */\nconst STATUS_CODE = { OK: 1, ERROR: 2 };\n\n/**\n * Wraps an async operation in an OTel span.\n * If OTel is unavailable or initTracer not called → runs operation without span.\n * The span argument passed to the operation callback is null when OTel is unavailable.\n *\n * Span name convention: feedback.{operation} (e.g., feedback.submit, feedback.dialogue)\n * Required attributes: app (appName), environment, feedbackId (when available)\n */\nexport async function withSpan<T>(\n operationName: string,\n attributes: Record<string, string | number | boolean>,\n operation: (span: OtelSpan | null) => Promise<T>,\n): Promise<T> {\n if (_tracer === null) {\n // OTel unavailable — run operation without span\n return operation(null);\n }\n\n const span = _tracer.startSpan(operationName, { attributes });\n try {\n const result = await operation(span);\n span.setStatus({ code: STATUS_CODE.OK });\n return result;\n } catch (error) {\n span.setStatus({\n code: STATUS_CODE.ERROR,\n message: error instanceof Error ? error.message : String(error),\n });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Convenience Event Recorders\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Records a feedback submission span event.\n * No-op when OTel unavailable.\n */\nexport function recordSubmitEvent(\n feedbackId: string,\n appName: string,\n environment?: string,\n): void {\n void withSpan(\n 'feedback.submit',\n {\n app: appName,\n feedbackId,\n environment: environment ?? 'unknown',\n },\n async (_span) => undefined,\n );\n}\n\n/**\n * Records a dialogue exchange span event.\n * No-op when OTel unavailable.\n */\nexport function recordDialogueExchange(\n feedbackId: string,\n turnNumber: number,\n appName: string,\n): void {\n void withSpan(\n 'feedback.dialogue',\n {\n app: appName,\n feedbackId,\n turnNumber,\n },\n async (_span) => undefined,\n );\n}\n\n// ─────────────────────────────────────────────────────────────────\n// CloudWatch Metric Emission (fire-and-forget)\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Emits a metric to CloudWatch (fire-and-forget).\n *\n * Only active when process.env.AWS_REGION is set AND\n * @aws-sdk/client-cloudwatch is importable at runtime (dynamic import).\n * If either condition is absent, logs to console.debug instead.\n * NEVER throws.\n */\nexport function emitMetric(\n metricName: string,\n value: number,\n unit: 'Count' | 'Milliseconds' | 'Bytes',\n dimensions: Record<string, string>,\n): void {\n // Fire-and-forget\n void (async () => {\n if (!process.env['AWS_REGION']) {\n console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);\n return;\n }\n\n try {\n // Optional runtime dependency — not a declared dependency of this package. The dynamic import\n // keeps it external/tree-shaken (@vite-ignore); typing the specifier as `string` tells the\n // compiler not to resolve this optional module at build time (it isn't installed here). This\n // is NOT import obfuscation — the module id is a plain literal; it is only widened to `string`\n // so an absent optional dep doesn't fail the build. The host supplies the SDK when AWS_REGION\n // is set; otherwise the import throws and we fall through to console.debug.\n const cloudwatchModuleId: string = '@aws-sdk/client-cloudwatch';\n const { CloudWatchClient, PutMetricDataCommand } = await import(/* @vite-ignore */ cloudwatchModuleId);\n\n const client = new CloudWatchClient({ region: process.env['AWS_REGION'] });\n await client.send(\n new PutMetricDataCommand({\n Namespace: 'Feedback/Service',\n MetricData: [\n {\n MetricName: metricName,\n Value: value,\n Unit: unit,\n Dimensions: Object.entries(dimensions).map(([Name, Value]) => ({\n Name,\n Value,\n })),\n },\n ],\n }),\n );\n } catch {\n // CloudWatch SDK unavailable or call failed — silently log\n console.debug(`[feedback-metrics] ${metricName}=${value} ${unit}`, dimensions);\n }\n })();\n}\n"],"names":[],"mappings":"AA6BA,IAAI,UAA6B;AACjC,IAAI,eAA8B;AAO3B,SAAS,WAAW,SAAuB;AAEhD,MAAI,YAAY,QAAQ,iBAAiB,SAAS;AAChD;AAAA,EACF;AAEA,iBAAe;AAGf,QAAM,YAAY;AAChB,QAAI;AAEF,YAAM,aAAa;AACnB,YAAM,OAAO,MAAM,OAAO;AAC1B,gBAAW,KAAK,MAAM,UAAU,yBAAyB,OAAO,EAAE;AAAA,IACpE,QAAQ;AAEN,gBAAU;AAAA,IACZ;AAAA,EACF,GAAA;AACF;AAOA,MAAM,cAAc,EAAE,IAAI,GAAG,OAAO,EAAA;AAUpC,eAAsB,SACpB,eACA,YACA,WACY;AACZ,MAAI,YAAY,MAAM;AAEpB,WAAO,UAAU,IAAI;AAAA,EACvB;AAEA,QAAM,OAAO,QAAQ,UAAU,eAAe,EAAE,YAAY;AAC5D,MAAI;AACF,UAAM,SAAS,MAAM,UAAU,IAAI;AACnC,SAAK,UAAU,EAAE,MAAM,YAAY,IAAI;AACvC,WAAO;AAAA,EACT,SAAS,OAAO;AACd,SAAK,UAAU;AAAA,MACb,MAAM,YAAY;AAAA,MAClB,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAAA,CAC/D;AACD,SAAK,gBAAgB,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC,CAAC;AAC9E,UAAM;AAAA,EACR,UAAA;AACE,SAAK,IAAA;AAAA,EACP;AACF;AAUO,SAAS,kBACd,YACA,SACA,aACM;AACN,OAAK;AAAA,IACH;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL;AAAA,MACA,aAAa,eAAe;AAAA,IAAA;AAAA,IAE9B,OAAO,UAAU;AAAA,EAAA;AAErB;AAMO,SAAS,uBACd,YACA,YACA,SACM;AACN,OAAK;AAAA,IACH;AAAA,IACA;AAAA,MACE,KAAK;AAAA,MACL;AAAA,MACA;AAAA,IAAA;AAAA,IAEF,OAAO,UAAU;AAAA,EAAA;AAErB;AAcO,SAAS,WACd,YACA,OACA,MACA,YACM;AAEN,QAAM,YAAY;AAChB,QAAI,CAAC,QAAQ,IAAI,YAAY,GAAG;AAC9B,cAAQ,MAAM,sBAAsB,UAAU,IAAI,KAAK,IAAI,IAAI,IAAI,UAAU;AAC7E;AAAA,IACF;AAEA,QAAI;AAOF,YAAM,qBAA6B;AACnC,YAAM,EAAE,kBAAkB,qBAAA,IAAyB,MAAM;AAAA;AAAA,QAA0B;AAAA;AAEnF,YAAM,SAAS,IAAI,iBAAiB,EAAE,QAAQ,QAAQ,IAAI,YAAY,GAAG;AACzE,YAAM,OAAO;AAAA,QACX,IAAI,qBAAqB;AAAA,UACvB,WAAW;AAAA,UACX,YAAY;AAAA,YACV;AAAA,cACE,YAAY;AAAA,cACZ,OAAO;AAAA,cACP,MAAM;AAAA,cACN,YAAY,OAAO,QAAQ,UAAU,EAAE,IAAI,CAAC,CAAC,MAAM,KAAK,OAAO;AAAA,gBAC7D;AAAA,gBACA;AAAA,cAAA,EACA;AAAA,YAAA;AAAA,UACJ;AAAA,QACF,CACD;AAAA,MAAA;AAAA,IAEL,QAAQ;AAEN,cAAQ,MAAM,sBAAsB,UAAU,IAAI,KAAK,IAAI,IAAI,IAAI,UAAU;AAAA,IAC/E;AAAA,EACF,GAAA;AACF;"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { w as withSpan } from "./observability-
|
|
1
|
+
import { w as withSpan } from "./observability-DUK725zv.js";
|
|
2
|
+
import { a as createIngestTransport } from "./ingest-transport-DAA_pJNb.js";
|
|
2
3
|
const VALID_TYPES = ["bug", "feature", "ux", "other"];
|
|
3
4
|
function validatePayload(body) {
|
|
4
5
|
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
@@ -36,34 +37,61 @@ function validatePayload(body) {
|
|
|
36
37
|
}
|
|
37
38
|
};
|
|
38
39
|
}
|
|
40
|
+
function resolveTransport(config) {
|
|
41
|
+
if (config.transport) return config.transport;
|
|
42
|
+
if (config.apiUrl && (config.tokenProvider || config.tokenUrl && config.clientId && config.clientSecret)) {
|
|
43
|
+
return createIngestTransport(config);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function buildStoredItem(payload, config, feedbackId, now) {
|
|
48
|
+
return {
|
|
49
|
+
PK: `PROJECT#${config.appName}`,
|
|
50
|
+
SK: `FB#${now}#${feedbackId}`,
|
|
51
|
+
GSI1PK: "STATUS#open",
|
|
52
|
+
GSI1SK: `015#${feedbackId}`,
|
|
53
|
+
GSI2PK: `TYPE#${payload.type}#PROJECT#${config.appName}`,
|
|
54
|
+
GSI2SK: now,
|
|
55
|
+
id: feedbackId,
|
|
56
|
+
appName: config.appName,
|
|
57
|
+
type: payload.type,
|
|
58
|
+
status: "new",
|
|
59
|
+
...payload.scope !== void 0 && { scope: payload.scope },
|
|
60
|
+
...payload.size !== void 0 && { size: payload.size },
|
|
61
|
+
priority: 0,
|
|
62
|
+
content: payload.title,
|
|
63
|
+
context: {
|
|
64
|
+
url: payload.systemInfo?.url ?? "",
|
|
65
|
+
...payload.systemInfo?.userAgent !== void 0 && { userAgent: payload.systemInfo.userAgent },
|
|
66
|
+
...payload.systemInfo?.viewport !== void 0 && { viewport: payload.systemInfo.viewport },
|
|
67
|
+
...payload.systemInfo?.locale !== void 0 && { locale: payload.systemInfo.locale },
|
|
68
|
+
...payload.consoleErrors !== void 0 && { consoleErrors: payload.consoleErrors }
|
|
69
|
+
},
|
|
70
|
+
hasScreenshot: Array.isArray(payload.screenshots) && payload.screenshots.length > 0,
|
|
71
|
+
submittedAt: now,
|
|
72
|
+
lastModifiedAt: now,
|
|
73
|
+
submittedBy: "",
|
|
74
|
+
reporters: [],
|
|
75
|
+
reporterCount: 0,
|
|
76
|
+
dialogueHistory: [],
|
|
77
|
+
corrections: [],
|
|
78
|
+
environment: config.environment ?? "production",
|
|
79
|
+
tags: []
|
|
80
|
+
};
|
|
81
|
+
}
|
|
39
82
|
async function storeFeedback(payload, config, feedbackId) {
|
|
40
|
-
|
|
83
|
+
const transport = resolveTransport(config);
|
|
84
|
+
if (!transport) {
|
|
41
85
|
return;
|
|
42
86
|
}
|
|
43
|
-
const dynamoPrefix = ["@aws", "-sdk/client-dynamo", "db"].join("");
|
|
44
|
-
const { DynamoDBClient, PutItemCommand } = await import(
|
|
45
|
-
/* @vite-ignore */
|
|
46
|
-
dynamoPrefix
|
|
47
|
-
);
|
|
48
|
-
const client = new DynamoDBClient({ region: config.awsRegion });
|
|
49
87
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
appName: { S: config.appName },
|
|
58
|
-
type: { S: payload.type },
|
|
59
|
-
status: { S: "open" },
|
|
60
|
-
content: { S: payload.title },
|
|
61
|
-
submittedAt: { S: now },
|
|
62
|
-
lastModifiedAt: { S: now },
|
|
63
|
-
environment: { S: config.environment ?? "production" }
|
|
64
|
-
}
|
|
65
|
-
})
|
|
66
|
-
);
|
|
88
|
+
const item = buildStoredItem(payload, config, feedbackId, now);
|
|
89
|
+
const result = await transport.submit(item);
|
|
90
|
+
if (!result.success) {
|
|
91
|
+
const err = new Error(result.error ?? "ingest write failed");
|
|
92
|
+
err.retryable = result.retryable ?? true;
|
|
93
|
+
throw err;
|
|
94
|
+
}
|
|
67
95
|
}
|
|
68
96
|
function generateId() {
|
|
69
97
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
@@ -101,21 +129,14 @@ function createIntakeResolver(config) {
|
|
|
101
129
|
}
|
|
102
130
|
res.status(201).json({
|
|
103
131
|
id: feedbackId,
|
|
104
|
-
status: "
|
|
132
|
+
status: "new"
|
|
105
133
|
});
|
|
106
134
|
} catch (storageError) {
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
});
|
|
113
|
-
} else {
|
|
114
|
-
res.status(503).json({
|
|
115
|
-
retryable: true,
|
|
116
|
-
error: "Failed to store feedback. Please try again."
|
|
117
|
-
});
|
|
118
|
-
}
|
|
135
|
+
const retryable = storageError instanceof Error && storageError.retryable !== false;
|
|
136
|
+
res.status(503).json({
|
|
137
|
+
retryable,
|
|
138
|
+
error: retryable ? "Storage service temporarily unavailable. Please try again." : "Failed to store feedback."
|
|
139
|
+
});
|
|
119
140
|
}
|
|
120
141
|
}
|
|
121
142
|
);
|
|
@@ -130,4 +151,4 @@ function createIntakeResolver(config) {
|
|
|
130
151
|
export {
|
|
131
152
|
createIntakeResolver as c
|
|
132
153
|
};
|
|
133
|
-
//# sourceMappingURL=submitResolver-
|
|
154
|
+
//# sourceMappingURL=submitResolver-wfr4kbEM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"submitResolver-wfr4kbEM.js","sources":["../../src/react/resolver/submitResolver.ts"],"sourcesContent":["/**\n * submitResolver — CRUD resolver for the feedback intake submit path.\n *\n * Server-only module — NOT imported by the client bundle.\n * Tree-shakeable: the /react subpath export does NOT re-export this.\n *\n * Factory: createIntakeResolver(config: SubmitHandlerConfig): RequestHandler\n *\n * HTTP contract:\n * POST /api/feedback/submit\n * Content-Type: application/json\n * Body: SubmitPayload\n * → 201 { id: string; status: string }\n * → 400 { errors: string[] }\n * → 503 { retryable: true; error: string }\n *\n * OTel span: 'feedback.submit' with attributes:\n * app, feedbackId (after store), environment\n *\n * Storage: writes via the authenticated central ingest API (`createIngestTransport().submit()`),\n * NOT direct DynamoDB. No AWS SDK import in this file.\n *\n * No Bedrock dependency — resolver calls submit handler only, not dialogue handler.\n */\n\nimport { withSpan } from '../../core/observability.js';\nimport { createIngestTransport, type IngestTransportConfig } from '../../core/ingest-transport.js';\nimport type { FeedbackSubmitTransport } from '../../core/transport.js';\nimport type {\n FeedbackEnvironment,\n FeedbackType,\n FeedbackScope,\n FeedbackSize,\n StoredFeedbackItem,\n} from '../../core/types.js';\n\n// ─────────────────────────────────────────────────────────────────\n// Request / Response types\n// ─────────────────────────────────────────────────────────────────\n\n/** Minimal HTTP request shape (Lambda/Express compatible) */\nexport interface RequestBody {\n headers?: Record<string, string | string[] | undefined>;\n body?: unknown;\n}\n\n/** Minimal HTTP response shape (Lambda/Express compatible) */\nexport interface ResponseBody {\n statusCode: number;\n headers: Record<string, string>;\n body: string;\n}\n\n/** Express-compatible RequestHandler */\nexport type RequestHandler = (\n req: RequestBody,\n res: {\n status: (code: number) => { json: (body: unknown) => void };\n json: (body: unknown) => void;\n },\n) => Promise<void>;\n\n// ─────────────────────────────────────────────────────────────────\n// Submit Payload (what the client POSTs)\n// ─────────────────────────────────────────────────────────────────\n\nexport interface SubmitPayload {\n title: string;\n description?: string | undefined;\n type: FeedbackType;\n scope?: FeedbackScope | undefined;\n size?: FeedbackSize | undefined;\n screenshots?: Array<{ dataUrl: string; capturedAt: string }> | undefined;\n consoleErrors?: string[] | undefined;\n systemInfo?: {\n url?: string | undefined;\n userAgent?: string | undefined;\n viewport?: { width: number; height: number } | undefined;\n locale?: string | undefined;\n } | undefined;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Config\n// ─────────────────────────────────────────────────────────────────\n\nexport interface SubmitHandlerConfig extends Partial<IngestTransportConfig> {\n /** Application name — used in OTel attributes and feedback ID */\n appName: string;\n /** Deployment environment */\n environment?: FeedbackEnvironment;\n /**\n * Pre-constructed submit transport (tests / custom wiring). Takes precedence over ingest config.\n * When neither a transport nor ingest config is supplied, storage is skipped (Minimal level).\n */\n transport?: FeedbackSubmitTransport;\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Validation\n// ─────────────────────────────────────────────────────────────────\n\nconst VALID_TYPES: FeedbackType[] = ['bug', 'feature', 'ux', 'other'];\n\nfunction validatePayload(body: unknown): { valid: true; payload: SubmitPayload } | { valid: false; errors: string[] } {\n if (!body || typeof body !== 'object' || Array.isArray(body)) {\n return { valid: false, errors: ['Request body must be a JSON object'] };\n }\n\n const obj = body as Record<string, unknown>;\n const errors: string[] = [];\n\n // title: required, string, min 1 char\n if (!obj['title'] || typeof obj['title'] !== 'string' || (obj['title'] as string).trim().length === 0) {\n errors.push('title is required and must be a non-empty string');\n } else if ((obj['title'] as string).trim().length < 1) {\n errors.push('title must be at least 1 character');\n }\n\n // type: required, must be valid\n if (!obj['type']) {\n errors.push('type is required');\n } else if (!VALID_TYPES.includes(obj['type'] as FeedbackType)) {\n errors.push(`type must be one of: ${VALID_TYPES.join(', ')} (got: ${String(obj['type'])})`);\n }\n\n // description: optional string\n if (obj['description'] !== undefined && typeof obj['description'] !== 'string') {\n errors.push('description must be a string if provided');\n }\n\n if (errors.length > 0) {\n return { valid: false, errors };\n }\n\n return {\n valid: true,\n payload: {\n title: (obj['title'] as string).trim(),\n ...(obj['description'] !== undefined && { description: obj['description'] as string }),\n type: obj['type'] as FeedbackType,\n ...(obj['scope'] !== undefined && { scope: obj['scope'] as FeedbackScope }),\n ...(obj['size'] !== undefined && { size: obj['size'] as FeedbackSize }),\n ...(obj['screenshots'] !== undefined && { screenshots: obj['screenshots'] as SubmitPayload['screenshots'] }),\n ...(obj['consoleErrors'] !== undefined && { consoleErrors: obj['consoleErrors'] as string[] }),\n ...(obj['systemInfo'] !== undefined && { systemInfo: obj['systemInfo'] as SubmitPayload['systemInfo'] }),\n },\n };\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Central-ingest store\n// ─────────────────────────────────────────────────────────────────\n\n/** Resolve a submit transport from config, or null at Minimal level (no storage wired). */\nfunction resolveTransport(config: SubmitHandlerConfig): FeedbackSubmitTransport | null {\n if (config.transport) return config.transport;\n if (config.apiUrl && (config.tokenProvider || (config.tokenUrl && config.clientId && config.clientSecret))) {\n return createIngestTransport(config as IngestTransportConfig);\n }\n return null;\n}\n\n/**\n * Build a StoredFeedbackItem from the submit payload. The server owns the canonical keys\n * (PK/SK/GSI*) and stamps them from the verified token identity, so the client-side values here are\n * placeholders — the transport strips them before sending the content.\n */\nfunction buildStoredItem(\n payload: SubmitPayload,\n config: SubmitHandlerConfig,\n feedbackId: string,\n now: string,\n): StoredFeedbackItem {\n return {\n PK: `PROJECT#${config.appName}`,\n SK: `FB#${now}#${feedbackId}`,\n GSI1PK: 'STATUS#open',\n GSI1SK: `015#${feedbackId}`,\n GSI2PK: `TYPE#${payload.type}#PROJECT#${config.appName}`,\n GSI2SK: now,\n id: feedbackId,\n appName: config.appName,\n type: payload.type,\n status: 'new',\n ...(payload.scope !== undefined && { scope: payload.scope }),\n ...(payload.size !== undefined && { size: payload.size }),\n priority: 0,\n content: payload.title,\n context: {\n url: payload.systemInfo?.url ?? '',\n ...(payload.systemInfo?.userAgent !== undefined && { userAgent: payload.systemInfo.userAgent }),\n ...(payload.systemInfo?.viewport !== undefined && { viewport: payload.systemInfo.viewport }),\n ...(payload.systemInfo?.locale !== undefined && { locale: payload.systemInfo.locale }),\n ...(payload.consoleErrors !== undefined && { consoleErrors: payload.consoleErrors }),\n },\n hasScreenshot: Array.isArray(payload.screenshots) && payload.screenshots.length > 0,\n submittedAt: now,\n lastModifiedAt: now,\n submittedBy: '',\n reporters: [],\n reporterCount: 0,\n dialogueHistory: [],\n corrections: [],\n environment: config.environment ?? 'production',\n tags: [],\n };\n}\n\n/**\n * Store the feedback via the authenticated central ingest API (`createIngestTransport().submit()`).\n * No-op at Minimal level (no transport/ingest config). Throws on failure so the handler maps it to\n * a 503 retryable response — the error carries `retryable` for the caller's classification.\n */\nasync function storeFeedback(\n payload: SubmitPayload,\n config: SubmitHandlerConfig,\n feedbackId: string,\n): Promise<void> {\n const transport = resolveTransport(config);\n if (!transport) {\n // Minimal level — no storage.\n return;\n }\n\n const now = new Date().toISOString();\n const item = buildStoredItem(payload, config, feedbackId, now);\n\n const result = await transport.submit(item);\n if (!result.success) {\n const err = new Error(result.error ?? 'ingest write failed') as Error & { retryable?: boolean };\n err.retryable = result.retryable ?? true;\n throw err;\n }\n}\n\n// ─────────────────────────────────────────────────────────────────\n// Feedback ID generation\n// ─────────────────────────────────────────────────────────────────\n\nfunction generateId(): string {\n const date = 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// Handler Factory\n// ─────────────────────────────────────────────────────────────────\n\n/**\n * Creates an Express/Lambda-compatible request handler for the submit endpoint.\n *\n * Validates payload → stores via the central ingest API (if configured) → returns 201 with ID.\n * Returns 503 with retryable:true when the ingest write fails.\n * No Bedrock dependency.\n */\nexport function createIntakeResolver(config: SubmitHandlerConfig): RequestHandler {\n return async (req, res) => {\n // ── Content-Type validation ───────────────────────────────────\n const contentType = (req.headers?.['content-type'] ?? '').toString();\n if (!contentType.includes('application/json')) {\n res.status(400).json({\n errors: ['Content-Type must be application/json'],\n });\n return;\n }\n\n // ── Body validation ───────────────────────────────────────────\n const validation = validatePayload(req.body);\n if (!validation.valid) {\n res.status(400).json({ errors: validation.errors });\n return;\n }\n\n const { payload } = validation;\n const feedbackId = generateId();\n\n // ── OTel span ─────────────────────────────────────────────────\n try {\n await withSpan(\n 'feedback.submit',\n {\n app: config.appName,\n environment: config.environment ?? 'production',\n },\n async (span) => {\n try {\n await storeFeedback(payload, config, feedbackId);\n\n // Attach feedbackId after successful store\n if (span) {\n (span as unknown as { setAttribute?: (k: string, v: string) => void })\n .setAttribute?.('feedbackId', feedbackId);\n }\n\n res.status(201).json({\n id: feedbackId,\n status: 'new',\n });\n } catch (storageError) {\n // storeFeedback throws on ingest failure; the thrown error carries `retryable`.\n const retryable =\n storageError instanceof Error &&\n (storageError as Error & { retryable?: boolean }).retryable !== false;\n\n res.status(503).json({\n retryable,\n error: retryable\n ? 'Storage service temporarily unavailable. Please try again.'\n : 'Failed to store feedback.',\n });\n }\n },\n );\n } catch {\n res.status(503).json({\n retryable: true,\n error: 'Internal error. Please try again.',\n });\n }\n };\n}\n"],"names":[],"mappings":"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"}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export interface FeedbackConfig {
|
|
|
14
14
|
dialogueUrl?: string;
|
|
15
15
|
/** If absent → screenshots disabled */
|
|
16
16
|
screenshotUploadUrl?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Optional per-request auth token provider. When set, its resolved value is
|
|
19
|
+
* attached to submit/dialogue requests as `Authorization: Bearer <token>`.
|
|
20
|
+
* Lets the host (which owns auth) identify the submitter server-side instead
|
|
21
|
+
* of the request being anonymous. May be sync or async.
|
|
22
|
+
*/
|
|
23
|
+
getAuthToken?: () => string | null | undefined | Promise<string | null | undefined>;
|
|
24
|
+
/** Optional static extra headers merged into submit/dialogue requests. */
|
|
25
|
+
headers?: Record<string, string>;
|
|
17
26
|
};
|
|
18
27
|
dialogue?: {
|
|
19
28
|
/** Default 3; min 1, max 10 */
|
|
@@ -62,6 +71,8 @@ export interface ResolvedFeedbackConfig {
|
|
|
62
71
|
submitUrl: string;
|
|
63
72
|
dialogueUrl: string | undefined;
|
|
64
73
|
screenshotUploadUrl: string | undefined;
|
|
74
|
+
getAuthToken: (() => string | null | undefined | Promise<string | null | undefined>) | undefined;
|
|
75
|
+
headers: Record<string, string> | undefined;
|
|
65
76
|
};
|
|
66
77
|
dialogue: {
|
|
67
78
|
maxTurns: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAMtD,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yCAAyC;IACzC,WAAW,CAAC,EAAE,mBAAmB,CAAC;IAElC,SAAS,EAAE;QACT,iDAAiD;QACjD,SAAS,EAAE,MAAM,CAAC;QAClB,gDAAgD;QAChD,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,uCAAuC;QACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/core/config.ts"],"names":[],"mappings":"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"}
|
package/dist/core/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export * from './transport.js';
|
|
|
11
11
|
export * from './ingest-transport.js';
|
|
12
12
|
export * from './validation.js';
|
|
13
13
|
export * from './transforms.js';
|
|
14
|
+
export * from './normalisers.js';
|
|
14
15
|
export * from './observability.js';
|
|
15
16
|
export * from './i18n.js';
|
|
16
17
|
export * from './errors.js';
|
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC"}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { StoredFeedbackItem, ActivityEntry, FeedbackStatus, Reporter } from './types.js';
|
|
1
2
|
import { FeedbackSubmitTransport } from './transport.js';
|
|
2
3
|
|
|
3
4
|
/** The ingest collection feedback writes to (the deployed registry maps it to `ralph-feedback`). */
|
|
4
5
|
export declare const FEEDBACK_COLLECTION = "feedback";
|
|
5
6
|
/** OAuth2 scope required to write via the ingest API. */
|
|
6
7
|
export declare const INGEST_WRITE_SCOPE = "assets/write";
|
|
8
|
+
/** OAuth2 scope required to read via the ingest API. */
|
|
9
|
+
export declare const INGEST_READ_SCOPE = "assets/read";
|
|
7
10
|
/** Minimal fetch surface (so the package needs no DOM lib types to compile). */
|
|
8
11
|
export type FetchLike = (url: string, init?: {
|
|
9
12
|
method?: string;
|
|
@@ -50,4 +53,98 @@ export interface IngestTransportConfig {
|
|
|
50
53
|
* await transport.submit(item); // → { success, feedbackId }
|
|
51
54
|
*/
|
|
52
55
|
export declare function createIngestTransport(config: IngestTransportConfig): FeedbackSubmitTransport;
|
|
56
|
+
/**
|
|
57
|
+
* The read half of the central-storage contract. `list()` mints/reuses a short-lived
|
|
58
|
+
* client-credentials JWT (scope `assets/read`), GETs `/records/{collection}` with a Bearer token,
|
|
59
|
+
* and returns the app's OWN-DATA partition as an array. The server derives the app from the verified
|
|
60
|
+
* token identity and returns only that app's items — there is NO server-side GSI query or SK range,
|
|
61
|
+
* so any status/limit/paging filtering is the caller's responsibility (applied client-side).
|
|
62
|
+
*
|
|
63
|
+
* Zero AWS SDK — `fetch`-only, mirroring `createIngestTransport`.
|
|
64
|
+
*/
|
|
65
|
+
export interface FeedbackReader {
|
|
66
|
+
/** Read this app's own feedback partition. Returns an array (empty when the partition is empty). */
|
|
67
|
+
list(): Promise<StoredFeedbackItem[]>;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a reader that fetches this app's feedback partition via the central ingest API.
|
|
71
|
+
*
|
|
72
|
+
* Usage (host app wires the credential from its secret store):
|
|
73
|
+
* const reader = createIngestReader({
|
|
74
|
+
* apiUrl: process.env.FEEDBACK_INGEST_URL!,
|
|
75
|
+
* tokenUrl: process.env.FEEDBACK_TOKEN_URL!,
|
|
76
|
+
* clientId: process.env.FEEDBACK_CLIENT_ID!,
|
|
77
|
+
* clientSecret: await resolveSecret('ralph/ingest/feedback'),
|
|
78
|
+
* });
|
|
79
|
+
* const items = await reader.list(); // → StoredFeedbackItem[]
|
|
80
|
+
*/
|
|
81
|
+
export declare function createIngestReader(config: IngestTransportConfig): FeedbackReader;
|
|
82
|
+
/**
|
|
83
|
+
* Result of an annotate or transition operation.
|
|
84
|
+
* Mirrors the existing SubmitResult shape with the addition of conflictReason
|
|
85
|
+
* for classifying non-retryable server rejections.
|
|
86
|
+
*/
|
|
87
|
+
export interface AnnotateResult {
|
|
88
|
+
success: boolean;
|
|
89
|
+
feedbackId: string;
|
|
90
|
+
error?: string;
|
|
91
|
+
retryable?: boolean;
|
|
92
|
+
/** Specific reason for a server rejection (HTTP 409/403/404). */
|
|
93
|
+
conflictReason?: 'stale_status' | 'missing_capability' | 'not_found';
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Options for transitionStatus operation.
|
|
97
|
+
* Supports optimistic concurrency control (currentStatus), duplicate merging
|
|
98
|
+
* (duplicateOf + duplicateReporters), and metadata (reason, assignee).
|
|
99
|
+
*/
|
|
100
|
+
export interface TransitionStatusOptions {
|
|
101
|
+
/** Expected current status for optimistic concurrency control. Server returns HTTP 409 if stale. */
|
|
102
|
+
currentStatus?: FeedbackStatus;
|
|
103
|
+
/** Human-readable reason for the transition. */
|
|
104
|
+
reason?: string;
|
|
105
|
+
/** User ID to assign the item to. */
|
|
106
|
+
assignee?: string;
|
|
107
|
+
/** Feedback ID this item is a duplicate of (when transitioning to 'duplicate'). */
|
|
108
|
+
duplicateOf?: string;
|
|
109
|
+
/** Reporter list from the duplicate item to merge onto the canonical item atomically. */
|
|
110
|
+
duplicateReporters?: Reporter[];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The annotate/transition half of the central-storage contract.
|
|
114
|
+
* Provides atomic operations for appending activity and transitioning status.
|
|
115
|
+
*/
|
|
116
|
+
export interface FeedbackAnnotateClient {
|
|
117
|
+
/**
|
|
118
|
+
* Atomically append an activity entry to a feedback item.
|
|
119
|
+
* Maps to POST /records/feedback/{feedbackId}/activity
|
|
120
|
+
*/
|
|
121
|
+
appendActivity(feedbackId: string, entry: ActivityEntry): Promise<AnnotateResult>;
|
|
122
|
+
/**
|
|
123
|
+
* Atomically transition a feedback item's status with optimistic concurrency control.
|
|
124
|
+
* Maps to PATCH /records/feedback/{feedbackId}/status
|
|
125
|
+
*/
|
|
126
|
+
transitionStatus(feedbackId: string, toStatus: FeedbackStatus, opts?: TransitionStatusOptions): Promise<AnnotateResult>;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create a FeedbackAnnotateClient that writes to the central ingest API.
|
|
130
|
+
* Reuses the same config validation and token management as createIngestTransport.
|
|
131
|
+
*
|
|
132
|
+
* Error mapping (all returned as AnnotateResult, never thrown):
|
|
133
|
+
* - HTTP 409 → conflictReason='stale_status', retryable=false
|
|
134
|
+
* - HTTP 403 → conflictReason='missing_capability', retryable=false
|
|
135
|
+
* - HTTP 404 → conflictReason='not_found', retryable=false
|
|
136
|
+
* - HTTP 5xx → retryable=true
|
|
137
|
+
* - fetch throws → retryable=true
|
|
138
|
+
*
|
|
139
|
+
* Usage:
|
|
140
|
+
* const annotateClient = createIngestAnnotateClient({
|
|
141
|
+
* apiUrl: process.env.FEEDBACK_INGEST_URL!,
|
|
142
|
+
* tokenUrl: process.env.FEEDBACK_TOKEN_URL!,
|
|
143
|
+
* clientId: process.env.FEEDBACK_CLIENT_ID!,
|
|
144
|
+
* clientSecret: await resolveSecret('feedback/ingest/<app>'),
|
|
145
|
+
* });
|
|
146
|
+
* await annotateClient.appendActivity(feedbackId, activityEntry);
|
|
147
|
+
* await annotateClient.transitionStatus(feedbackId, 'triaged', { currentStatus: 'new' });
|
|
148
|
+
*/
|
|
149
|
+
export declare function createIngestAnnotateClient(config: IngestTransportConfig): FeedbackAnnotateClient;
|
|
53
150
|
//# sourceMappingURL=ingest-transport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ingest-transport.d.ts","sourceRoot":"","sources":["../../src/core/ingest-transport.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"ingest-transport.d.ts","sourceRoot":"","sources":["../../src/core/ingest-transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StoredFeedbackItem, FeedbackStatus, LegacyFeedbackStatus } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Map a legacy status value to the new QA lifecycle. New values pass through unchanged.
|
|
5
|
+
* Unknown values fall back to 'new' (Tolerant Reader — never throw on stored data).
|
|
6
|
+
*
|
|
7
|
+
* @param status - a legacy or new status value
|
|
8
|
+
* @returns the equivalent new-lifecycle FeedbackStatus
|
|
9
|
+
*/
|
|
10
|
+
export declare function normaliseLegacyStatus(status: LegacyFeedbackStatus | FeedbackStatus): FeedbackStatus;
|
|
11
|
+
/**
|
|
12
|
+
* Merge legacy dialogueHistory + corrections into activity[]. Idempotent: entries already
|
|
13
|
+
* present (by stable id) are not re-appended, so applying twice equals applying once.
|
|
14
|
+
* Returns a new object; never mutates the input. Stored data is never rewritten.
|
|
15
|
+
*/
|
|
16
|
+
export declare function normaliseActivityArrays(item: StoredFeedbackItem): StoredFeedbackItem;
|
|
17
|
+
/**
|
|
18
|
+
* Tolerant-Reader fallback for the structured report: if report.actual is not set and the
|
|
19
|
+
* legacy `content` blob is non-empty, set report.actual = content. The content field is
|
|
20
|
+
* always preserved. Returns a new object; never mutates the input.
|
|
21
|
+
*/
|
|
22
|
+
export declare function normaliseReportContent(item: StoredFeedbackItem): StoredFeedbackItem;
|
|
23
|
+
/**
|
|
24
|
+
* Apply all read-time normalisers in sequence: legacy status → activity merge → report
|
|
25
|
+
* content fallback. Pure and idempotent; returns a new object and never mutates the input.
|
|
26
|
+
* Rendering/consumer code should call this before reading status or any optional field.
|
|
27
|
+
*/
|
|
28
|
+
export declare function normaliseStoredItem(item: StoredFeedbackItem): StoredFeedbackItem;
|
|
29
|
+
//# sourceMappingURL=normalisers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalisers.d.ts","sourceRoot":"","sources":["../../src/core/normalisers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,cAAc,EACd,oBAAoB,EAMrB,MAAM,YAAY,CAAC;AAuBpB;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,oBAAoB,GAAG,cAAc,GAAG,cAAc,CAInG;AAuDD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,CAmBpF;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,CAOnF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,kBAAkB,GAAG,kBAAkB,CAKhF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../src/core/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,UAAU,QAAQ;IAChB,SAAS,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5D,eAAe,CAAC,SAAS,EAAE,KAAK,GAAG,IAAI,CAAC;IACxC,GAAG,IAAI,IAAI,CAAC;CACb;AAaD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAoBhD;AASD;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAC9B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EACrD,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC/C,OAAO,CAAC,CAAC,CAAC,CAqBZ;AAMD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAUN;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,IAAI,CAUN;AAMD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,GAAG,cAAc,GAAG,OAAO,EACxC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,IAAI,
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../src/core/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH,UAAU,QAAQ;IAChB,SAAS,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5D,eAAe,CAAC,SAAS,EAAE,KAAK,GAAG,IAAI,CAAC;IACxC,GAAG,IAAI,IAAI,CAAC;CACb;AAaD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAoBhD;AASD;;;;;;;GAOG;AACH,wBAAsB,QAAQ,CAAC,CAAC,EAC9B,aAAa,EAAE,MAAM,EACrB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,EACrD,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,GAC/C,OAAO,CAAC,CAAC,CAAC,CAqBZ;AAMD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACnB,IAAI,CAUN;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GACd,IAAI,CAUN;AAMD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,OAAO,GAAG,cAAc,GAAG,OAAO,EACxC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,IAAI,CAwCN"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* request-headers.ts
|
|
3
|
+
* Location: src/core/request-headers.ts
|
|
4
|
+
*
|
|
5
|
+
* Builds the headers for submit/dialogue requests, attaching an Authorization
|
|
6
|
+
* bearer token when the host provides a `transport.getAuthToken` resolver.
|
|
7
|
+
*
|
|
8
|
+
* Why: the host owns authentication. Without this, feedback POSTs carry no
|
|
9
|
+
* identity and the server records the submitter as "anonymous". A single helper
|
|
10
|
+
* keeps the auth/header behaviour identical for the submit and dialogue calls.
|
|
11
|
+
*/
|
|
12
|
+
export interface TransportAuth {
|
|
13
|
+
getAuthToken?: (() => string | null | undefined | Promise<string | null | undefined>) | undefined;
|
|
14
|
+
headers?: Record<string, string> | undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build request headers: JSON content-type + any static transport.headers +
|
|
18
|
+
* `Authorization: Bearer <token>` when getAuthToken resolves to a non-empty
|
|
19
|
+
* token. Never throws — a failing/empty token provider simply omits the header.
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildRequestHeaders(transport: TransportAuth): Promise<Record<string, string>>;
|
|
22
|
+
//# sourceMappingURL=request-headers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request-headers.d.ts","sourceRoot":"","sources":["../../src/core/request-headers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC,GAAG,SAAS,CAAC;IAClG,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;CAC9C;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAcnG"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ActivityEntry, StateDuration } from './types.js';
|
|
2
|
+
|
|
3
|
+
export type { StateDuration };
|
|
4
|
+
/**
|
|
5
|
+
* Derives state durations from an activity stream.
|
|
6
|
+
* Filters status_change entries, pairs them chronologically to produce one StateDuration per state visit.
|
|
7
|
+
* The current state (last entry) has undefined exitedAt and durationMs = time from entry to now.
|
|
8
|
+
*
|
|
9
|
+
* @param activity - Array of ActivityEntry objects (may be empty or undefined)
|
|
10
|
+
* @returns Array of StateDuration objects, one per state visit
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const durations = deriveStateDurations(item.activity);
|
|
14
|
+
* const mttr = durations
|
|
15
|
+
* .filter(d => ['in_progress', 'fixed', 'pending_retest'].includes(d.status))
|
|
16
|
+
* .reduce((sum, d) => sum + d.durationMs, 0);
|
|
17
|
+
*/
|
|
18
|
+
export declare function deriveStateDurations(activity: ActivityEntry[] | undefined): StateDuration[];
|
|
19
|
+
//# sourceMappingURL=state-metrics.d.ts.map
|