@littlebearapps/platform-admin-sdk 1.4.2 → 1.5.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 (111) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +121 -2
  3. package/package.json +1 -1
  4. package/templates/full/config/audit-targets.yaml +72 -0
  5. package/templates/full/dashboard/src/components/notifications/NotificationBell.tsx +30 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationList.tsx +116 -0
  7. package/templates/full/dashboard/src/components/notifications/index.ts +2 -0
  8. package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
  9. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  10. package/templates/full/dashboard/src/components/patterns/index.ts +2 -0
  11. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  12. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  13. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  14. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  15. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  16. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  17. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  18. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  19. package/templates/full/migrations/008_auditor.sql +99 -0
  20. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  21. package/templates/full/migrations/011_multi_account.sql +51 -0
  22. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  23. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  24. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  25. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  26. package/templates/full/workers/lib/auditor/index.ts +9 -0
  27. package/templates/full/workers/lib/auditor/types.ts +167 -0
  28. package/templates/full/workers/platform-auditor.ts +1071 -0
  29. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  30. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  31. package/templates/shared/config/observability.yaml.hbs +276 -0
  32. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  33. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  34. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  35. package/templates/shared/dashboard/astro.config.mjs +21 -0
  36. package/templates/shared/dashboard/package.json.hbs +29 -0
  37. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  38. package/templates/shared/dashboard/src/components/Nav.astro.hbs +57 -0
  39. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  40. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  41. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  42. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  43. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  44. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  45. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  46. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  47. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  48. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  49. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  50. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  51. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  52. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  53. package/templates/shared/dashboard/src/components/ui/index.ts +3 -0
  54. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  55. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  56. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  57. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  58. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  59. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  60. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  61. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  62. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  63. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  64. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  65. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  66. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  67. package/templates/shared/dashboard/src/styles/global.css +29 -0
  68. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  69. package/templates/shared/dashboard/tsconfig.json +9 -0
  70. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  71. package/templates/shared/package.json.hbs +12 -1
  72. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  73. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  74. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  75. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  76. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  77. package/templates/shared/scripts/validate-schemas.js +61 -0
  78. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  79. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  80. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  81. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  82. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  83. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  84. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  85. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  86. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  87. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  88. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  89. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  90. package/templates/shared/workers/platform-usage.ts +98 -8
  91. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  92. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  93. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  94. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  95. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  96. package/templates/standard/dashboard/src/components/health/index.ts +2 -0
  97. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  98. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  99. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  100. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  101. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  102. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  103. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  104. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  105. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  106. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  107. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  108. package/templates/standard/workers/platform-mapper.ts +482 -0
  109. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  110. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  111. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env npx tsx
2
+ /**
3
+ * Set KV Pricing Configuration
4
+ *
5
+ * This script sets the Cloudflare pricing configuration in the PLATFORM_CACHE KV namespace.
6
+ * The platform-usage worker reads this configuration at runtime to calculate costs.
7
+ *
8
+ * Usage:
9
+ * npx tsx scripts/ops/set-kv-pricing.ts [--dry-run]
10
+ *
11
+ * Prerequisites:
12
+ * - wrangler authenticated (npx wrangler login)
13
+ * - KV namespace ID set below (or via PLATFORM_CACHE_KV_ID env var)
14
+ *
15
+ * Pricing sources:
16
+ * - https://developers.cloudflare.com/workers/platform/pricing/
17
+ * - https://developers.cloudflare.com/d1/platform/pricing/
18
+ * - https://developers.cloudflare.com/kv/platform/pricing/
19
+ * - https://developers.cloudflare.com/r2/pricing/
20
+ * - https://developers.cloudflare.com/vectorize/platform/pricing/
21
+ * - https://developers.cloudflare.com/workers-ai/platform/pricing/
22
+ * - https://developers.cloudflare.com/durable-objects/platform/pricing/
23
+ * - https://developers.cloudflare.com/queues/platform/pricing/
24
+ * - https://developers.cloudflare.com/pages/platform/pricing/
25
+ */
26
+
27
+ const PRICING_KEY = 'platform-usage:pricing:v1';
28
+
29
+ // TODO: Set your PLATFORM_CACHE KV namespace ID here or via PLATFORM_CACHE_KV_ID env var
30
+ const KV_NAMESPACE_ID = process.env.PLATFORM_CACHE_KV_ID || 'YOUR_PLATFORM_CACHE_KV_ID';
31
+
32
+ /**
33
+ * Current Cloudflare pricing (as of January 2026)
34
+ *
35
+ * Notes:
36
+ * - All prices in USD
37
+ * - Workers Paid plan includes 10M requests/month and 30M CPU ms/month
38
+ * - D1: First 25B rows read and 50M rows written are included
39
+ * - KV: First 10M reads, 1M writes, 1M deletes, 1M lists, 1GB storage included
40
+ * - R2: First 10GB storage, 1M Class A ops, 10M Class B ops included
41
+ * - Vectorize: First 10M stored dimensions and 50M queried dimensions included
42
+ * - Workers AI: First 10,000 neurons/day included
43
+ * - Durable Objects: First 1M requests, 400K GB-seconds, 1GB storage included
44
+ * - Queues: First 1M operations included
45
+ * - Pages: 500 builds/month included
46
+ *
47
+ * When Cloudflare updates pricing, update this config and re-run the script.
48
+ * Also create a new row in pricing_versions (migration 010) for audit trail.
49
+ */
50
+ const PRICING_CONFIG = {
51
+ version: '2026-01-14',
52
+ lastUpdated: new Date().toISOString(),
53
+ source: 'cloudflare-pricing-pages',
54
+
55
+ workers: {
56
+ baseCostMonthly: 5.0,
57
+ includedRequests: 10_000_000,
58
+ requestsPerMillion: 0.3,
59
+ cpuMsPerMillion: 0.02,
60
+ },
61
+
62
+ d1: {
63
+ rowsReadPerBillion: 0.001,
64
+ rowsWrittenPerMillion: 1.0,
65
+ storagePerGb: 0.75,
66
+ },
67
+
68
+ kv: {
69
+ readsPerMillion: 0.5,
70
+ writesPerMillion: 5.0,
71
+ deletesPerMillion: 5.0,
72
+ listsPerMillion: 5.0,
73
+ storagePerGb: 0.5,
74
+ },
75
+
76
+ r2: {
77
+ storagePerGbMonth: 0.015,
78
+ classAPerMillion: 4.5,
79
+ classBPerMillion: 0.36,
80
+ },
81
+
82
+ vectorize: {
83
+ storedDimensionsPerMillion: 0.01,
84
+ queriedDimensionsPerMillion: 0.04,
85
+ },
86
+
87
+ workersAI: {
88
+ neuronsPerThousand: 0.011,
89
+ },
90
+
91
+ durableObjects: {
92
+ requestsPerMillion: 0.15,
93
+ gbSecondsPerMillion: 12.5,
94
+ storagePerGbMonth: 0.2,
95
+ readsPerMillion: 0.2,
96
+ writesPerMillion: 1.0,
97
+ deletesPerMillion: 1.0,
98
+ },
99
+
100
+ queues: {
101
+ messagesPerMillion: 0.4,
102
+ operationsPerMillion: 0.4,
103
+ },
104
+
105
+ pages: {
106
+ buildCost: 0.0,
107
+ bandwidthPerGb: 0.0,
108
+ },
109
+ };
110
+
111
+ async function main() {
112
+ const isDryRun = process.argv.includes('--dry-run');
113
+
114
+ if (KV_NAMESPACE_ID === 'YOUR_PLATFORM_CACHE_KV_ID') {
115
+ console.error('ERROR: Set PLATFORM_CACHE_KV_ID env var or update KV_NAMESPACE_ID in this script.');
116
+ console.error('Find your namespace ID: npx wrangler kv namespace list');
117
+ process.exit(1);
118
+ }
119
+
120
+ console.log('='.repeat(60));
121
+ console.log('Platform Usage Pricing Configuration');
122
+ console.log('='.repeat(60));
123
+ console.log(`Version: ${PRICING_CONFIG.version}`);
124
+ console.log(`KV Key: ${PRICING_KEY}`);
125
+ console.log(`Namespace: ${KV_NAMESPACE_ID}`);
126
+ console.log(`Dry Run: ${isDryRun}`);
127
+ console.log('');
128
+
129
+ console.log('Pricing Summary:');
130
+ console.log('-'.repeat(40));
131
+ console.log(`Workers: $${PRICING_CONFIG.workers.requestsPerMillion}/M requests`);
132
+ console.log(
133
+ `D1: $${PRICING_CONFIG.d1.rowsReadPerBillion}/B rows read, $${PRICING_CONFIG.d1.rowsWrittenPerMillion}/M rows written`
134
+ );
135
+ console.log(
136
+ `KV: $${PRICING_CONFIG.kv.readsPerMillion}/M reads, $${PRICING_CONFIG.kv.writesPerMillion}/M writes`
137
+ );
138
+ console.log(
139
+ `R2: $${PRICING_CONFIG.r2.storagePerGbMonth}/GB storage, $${PRICING_CONFIG.r2.classAPerMillion}/M Class A`
140
+ );
141
+ console.log(`Vectorize: $${PRICING_CONFIG.vectorize.storedDimensionsPerMillion}/M stored dims`);
142
+ console.log(`Workers AI: $${PRICING_CONFIG.workersAI.neuronsPerThousand}/1K neurons`);
143
+ console.log(`Durable Objects: $${PRICING_CONFIG.durableObjects.requestsPerMillion}/M requests`);
144
+ console.log(`Queues: $${PRICING_CONFIG.queues.messagesPerMillion}/M messages`);
145
+ console.log('');
146
+
147
+ if (isDryRun) {
148
+ console.log('[DRY RUN] Would write the following to KV:');
149
+ console.log(JSON.stringify(PRICING_CONFIG, null, 2));
150
+ return;
151
+ }
152
+
153
+ // Write to KV using wrangler
154
+ const { execSync } = await import('child_process');
155
+ const pricingJson = JSON.stringify(PRICING_CONFIG);
156
+
157
+ try {
158
+ console.log('Writing pricing to KV...');
159
+ execSync(
160
+ `npx wrangler kv key put "${PRICING_KEY}" '${pricingJson}' --namespace-id ${KV_NAMESPACE_ID} --remote`,
161
+ { stdio: 'inherit', cwd: process.cwd() }
162
+ );
163
+ console.log('');
164
+ console.log('Pricing configuration saved successfully.');
165
+ console.log('');
166
+ console.log('To verify:');
167
+ console.log(
168
+ ` npx wrangler kv key get "${PRICING_KEY}" --namespace-id ${KV_NAMESPACE_ID} --remote`
169
+ );
170
+ } catch (error) {
171
+ console.error('Failed to write pricing to KV:', error);
172
+ console.log('');
173
+ console.log('Alternative: Copy the JSON below and use Cloudflare dashboard:');
174
+ console.log('1. Go to Workers & Pages > KV > PLATFORM_CACHE');
175
+ console.log(`2. Create key: ${PRICING_KEY}`);
176
+ console.log('3. Paste value:');
177
+ console.log(JSON.stringify(PRICING_CONFIG, null, 2));
178
+ process.exit(1);
179
+ }
180
+ }
181
+
182
+ main();
@@ -0,0 +1,181 @@
1
+ /**
2
+ * AI Judge Response Schema
3
+ *
4
+ * Zod schemas for validating AI Judge responses from Gemini via AI Gateway.
5
+ * Implements rubric-based scoring with chain-of-thought reasoning.
6
+ * Designed to be lenient to handle AI output variations.
7
+ *
8
+ * Rubric dimensions (1-5 scale):
9
+ * sdk (30%) — SDK integration patterns, feature budgets, tracking
10
+ * observability (25%) — Logging, tracing, metrics, error reporting
11
+ * costProtection (25%) — Circuit breakers, budgets, DLQ, rate limiting
12
+ * security (20%) — Auth, validation, secrets, CORS
13
+ *
14
+ * @module workers/lib/ai-judge-schema
15
+ */
16
+
17
+ import { z } from 'zod';
18
+
19
+ // -----------------------------------------------------------------------------
20
+ // Rubric Score Schema (1-5 scale with evidence)
21
+ // -----------------------------------------------------------------------------
22
+
23
+ export const RubricScoreSchema = z.object({
24
+ score: z
25
+ .number()
26
+ .min(1)
27
+ .max(5)
28
+ .transform((v) => Math.round(v)),
29
+ evidence: z.array(z.string()).default(['No specific evidence cited']),
30
+ });
31
+
32
+ export type RubricScore = z.infer<typeof RubricScoreSchema>;
33
+
34
+ // -----------------------------------------------------------------------------
35
+ // Rubric Scores (all four dimensions) - lenient with aliases
36
+ // -----------------------------------------------------------------------------
37
+
38
+ export const RubricScoresSchema = z
39
+ .object({
40
+ sdk: RubricScoreSchema.optional(),
41
+ observability: RubricScoreSchema.optional(),
42
+ costProtection: RubricScoreSchema.optional(),
43
+ cost: RubricScoreSchema.optional(), // Alias for costProtection
44
+ security: RubricScoreSchema.optional(),
45
+ })
46
+ .transform((scores) => ({
47
+ sdk: scores.sdk ?? { score: 3, evidence: ['Not evaluated'] },
48
+ observability: scores.observability ?? { score: 3, evidence: ['Not evaluated'] },
49
+ costProtection: scores.costProtection ??
50
+ scores.cost ?? { score: 3, evidence: ['Not evaluated'] },
51
+ security: scores.security ?? { score: 3, evidence: ['Not evaluated'] },
52
+ }));
53
+
54
+ export type RubricScores = z.infer<typeof RubricScoresSchema>;
55
+
56
+ // -----------------------------------------------------------------------------
57
+ // Static Check Correction (lenient)
58
+ // -----------------------------------------------------------------------------
59
+
60
+ export const StaticCheckCorrectionSchema = z.object({
61
+ field: z.string().optional().default('unknown'),
62
+ expected: z.boolean().optional().default(true),
63
+ actual: z.boolean().optional().default(false),
64
+ reason: z.string().optional().default(''),
65
+ confidence: z
66
+ .union([z.number(), z.string().transform((v) => parseFloat(v))])
67
+ .optional()
68
+ .default(0.8)
69
+ .transform((v) => (typeof v === 'number' && v > 1 ? v / 100 : v)),
70
+ });
71
+
72
+ export type StaticCheckCorrection = z.infer<typeof StaticCheckCorrectionSchema>;
73
+
74
+ // -----------------------------------------------------------------------------
75
+ // Categorised Issue (lenient)
76
+ // -----------------------------------------------------------------------------
77
+
78
+ export const CategorisedIssueSchema = z.object({
79
+ category: z.enum(['sdk', 'observability', 'cost', 'security']).catch('sdk'),
80
+ severity: z.enum(['critical', 'high', 'medium', 'low']).catch('medium'),
81
+ description: z.string().optional().default('No description'),
82
+ file: z.string().optional().nullable(),
83
+ line: z
84
+ .union([z.number(), z.string().transform((v) => parseInt(v, 10))])
85
+ .optional()
86
+ .nullable(),
87
+ });
88
+
89
+ export type CategorisedIssue = z.infer<typeof CategorisedIssueSchema>;
90
+
91
+ // -----------------------------------------------------------------------------
92
+ // Full AI Judge Response (lenient)
93
+ // -----------------------------------------------------------------------------
94
+
95
+ export const AIJudgeResponseSchema = z.object({
96
+ reasoning: z.string().optional().default('Analysis completed.'),
97
+ rubricScores: z.preprocess((val) => val ?? {}, RubricScoresSchema),
98
+ staticCheckCorrections: z.array(StaticCheckCorrectionSchema).optional().default([]),
99
+ issues: z.array(CategorisedIssueSchema).optional().default([]),
100
+ summary: z.string().optional().default('Analysis complete.'),
101
+ });
102
+
103
+ export type AIJudgeResponse = z.infer<typeof AIJudgeResponseSchema>;
104
+
105
+ // -----------------------------------------------------------------------------
106
+ // Rubric Weights (for composite score calculation)
107
+ // -----------------------------------------------------------------------------
108
+
109
+ /** Default rubric weights — configurable via audit-targets.yaml */
110
+ export const DEFAULT_RUBRIC_WEIGHTS = {
111
+ sdk: 0.3,
112
+ observability: 0.25,
113
+ costProtection: 0.25,
114
+ security: 0.2,
115
+ } as const;
116
+
117
+ // -----------------------------------------------------------------------------
118
+ // Composite Score Calculation
119
+ // -----------------------------------------------------------------------------
120
+
121
+ /**
122
+ * Calculate composite score from rubric scores.
123
+ * Converts 1-5 scale to 0-100 scale using weighted average.
124
+ */
125
+ export function calculateCompositeScore(
126
+ rubricScores: RubricScores,
127
+ weights: typeof DEFAULT_RUBRIC_WEIGHTS = DEFAULT_RUBRIC_WEIGHTS
128
+ ): number {
129
+ const weightedSum =
130
+ rubricScores.sdk.score * weights.sdk +
131
+ rubricScores.observability.score * weights.observability +
132
+ rubricScores.costProtection.score * weights.costProtection +
133
+ rubricScores.security.score * weights.security;
134
+
135
+ return Math.round(((weightedSum - 1) / 4) * 100);
136
+ }
137
+
138
+ // -----------------------------------------------------------------------------
139
+ // Validation Helper
140
+ // -----------------------------------------------------------------------------
141
+
142
+ export interface ValidationResult {
143
+ success: true;
144
+ data: AIJudgeResponse;
145
+ }
146
+
147
+ export interface ValidationError {
148
+ success: false;
149
+ errors: string[];
150
+ }
151
+
152
+ /**
153
+ * Validate and parse AI Judge response.
154
+ * Returns structured result with either parsed data or error messages.
155
+ */
156
+ export function validateAIJudgeResponse(response: unknown): ValidationResult | ValidationError {
157
+ const result = AIJudgeResponseSchema.safeParse(response);
158
+
159
+ if (result.success) {
160
+ return { success: true, data: result.data };
161
+ }
162
+
163
+ return {
164
+ success: false,
165
+ errors: result.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`),
166
+ };
167
+ }
168
+
169
+ // -----------------------------------------------------------------------------
170
+ // Prompt Error Feedback
171
+ // -----------------------------------------------------------------------------
172
+
173
+ /**
174
+ * Format validation errors for inclusion in retry prompt.
175
+ */
176
+ export function formatValidationErrors(errors: string[]): string {
177
+ return `Your previous response had validation errors:
178
+ ${errors.map((e) => `- ${e}`).join('\n')}
179
+
180
+ Please fix these issues and respond again with valid JSON.`;
181
+ }