@littlebearapps/platform-admin-sdk 1.4.2 → 2.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 (189) hide show
  1. package/dist/templates.d.ts +1 -1
  2. package/dist/templates.js +232 -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/ActivePatterns.tsx +62 -0
  9. package/templates/full/dashboard/src/components/patterns/PatternStats.tsx +60 -0
  10. package/templates/full/dashboard/src/components/patterns/PatternTabs.tsx +116 -0
  11. package/templates/full/dashboard/src/components/patterns/SuggestionsQueue.tsx +115 -0
  12. package/templates/full/dashboard/src/components/patterns/SystemPatterns.tsx +52 -0
  13. package/templates/full/dashboard/src/components/patterns/index.ts +5 -0
  14. package/templates/full/dashboard/src/components/reports/GapDetectionReport.tsx +69 -0
  15. package/templates/full/dashboard/src/components/reports/SdkAuditReport.tsx +72 -0
  16. package/templates/full/dashboard/src/components/reports/index.ts +2 -0
  17. package/templates/full/dashboard/src/components/search/SearchModal.tsx +108 -0
  18. package/templates/full/dashboard/src/pages/api/notifications/[id]/read.ts +37 -0
  19. package/templates/full/dashboard/src/pages/api/notifications/index.ts +47 -0
  20. package/templates/full/dashboard/src/pages/api/notifications/read-all.ts +28 -0
  21. package/templates/full/dashboard/src/pages/api/notifications/unread-count.ts +31 -0
  22. package/templates/full/dashboard/src/pages/api/patterns/approve.ts +55 -0
  23. package/templates/full/dashboard/src/pages/api/patterns/cache-refresh.ts +38 -0
  24. package/templates/full/dashboard/src/pages/api/patterns/discover.ts +36 -0
  25. package/templates/full/dashboard/src/pages/api/patterns/index.ts +36 -0
  26. package/templates/full/dashboard/src/pages/api/patterns/ready-for-review.ts +39 -0
  27. package/templates/full/dashboard/src/pages/api/patterns/reject.ts +54 -0
  28. package/templates/full/dashboard/src/pages/api/patterns/stats.ts +39 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/suggestions.ts +43 -0
  30. package/templates/full/dashboard/src/pages/api/reports/audit.ts +45 -0
  31. package/templates/full/dashboard/src/pages/api/reports/usage.ts +52 -0
  32. package/templates/full/dashboard/src/pages/api/search/index.ts +74 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex.ts +28 -0
  34. package/templates/full/dashboard/src/pages/api/search/stats.ts +27 -0
  35. package/templates/full/dashboard/src/pages/api/settings/index.ts +37 -0
  36. package/templates/full/dashboard/src/pages/api/settings/update.ts +41 -0
  37. package/templates/full/dashboard/src/pages/api/topology/index.ts +56 -0
  38. package/templates/full/dashboard/src/pages/notifications.astro +11 -0
  39. package/templates/full/migrations/008_auditor.sql +99 -0
  40. package/templates/full/migrations/010_pricing_versions.sql +110 -0
  41. package/templates/full/migrations/011_multi_account.sql +51 -0
  42. package/templates/full/scripts/ops/set-kv-pricing.ts +182 -0
  43. package/templates/full/scripts/ops/universal-backfill.ts +147 -0
  44. package/templates/full/workers/lib/ai-judge-schema.ts +181 -0
  45. package/templates/full/workers/lib/auditor/comprehensive-report.ts +407 -0
  46. package/templates/full/workers/lib/auditor/feature-coverage.ts +348 -0
  47. package/templates/full/workers/lib/auditor/index.ts +9 -0
  48. package/templates/full/workers/lib/auditor/types.ts +167 -0
  49. package/templates/full/workers/platform-auditor.ts +1071 -0
  50. package/templates/full/wrangler.auditor.jsonc.hbs +75 -0
  51. package/templates/shared/.github/workflows/contract-check.yml.hbs +42 -0
  52. package/templates/shared/.github/workflows/dashboard-deploy.yml.hbs +39 -0
  53. package/templates/shared/.github/workflows/platform-check.yml.hbs +28 -0
  54. package/templates/shared/.github/workflows/security.yml +33 -0
  55. package/templates/shared/config/observability.yaml.hbs +276 -0
  56. package/templates/shared/contracts/schemas/envelope.v1.schema.json +64 -0
  57. package/templates/shared/contracts/schemas/error_report.v1.schema.json +65 -0
  58. package/templates/shared/contracts/types/telemetry-envelope.types.ts +139 -0
  59. package/templates/shared/dashboard/astro.config.mjs +21 -0
  60. package/templates/shared/dashboard/package.json.hbs +29 -0
  61. package/templates/shared/dashboard/src/components/Header.astro +29 -0
  62. package/templates/shared/dashboard/src/components/Nav.astro.hbs +59 -0
  63. package/templates/shared/dashboard/src/components/infrastructure/AlertHistory.tsx +57 -0
  64. package/templates/shared/dashboard/src/components/infrastructure/InfrastructureStats.tsx +73 -0
  65. package/templates/shared/dashboard/src/components/infrastructure/ServiceRegistry.tsx +55 -0
  66. package/templates/shared/dashboard/src/components/infrastructure/UptimeStatus.tsx +56 -0
  67. package/templates/shared/dashboard/src/components/infrastructure/index.ts +4 -0
  68. package/templates/shared/dashboard/src/components/overview/ActivityFeed.tsx +134 -0
  69. package/templates/shared/dashboard/src/components/overview/CostQuadrant.tsx +131 -0
  70. package/templates/shared/dashboard/src/components/overview/ErrorsQuadrant.tsx +113 -0
  71. package/templates/shared/dashboard/src/components/overview/HealthQuadrant.tsx +87 -0
  72. package/templates/shared/dashboard/src/components/overview/MissionControl.tsx +139 -0
  73. package/templates/shared/dashboard/src/components/resources/AllowanceStatus.tsx +44 -0
  74. package/templates/shared/dashboard/src/components/resources/CostCentreOverview.tsx +42 -0
  75. package/templates/shared/dashboard/src/components/resources/ResourceTabs.tsx +69 -0
  76. package/templates/shared/dashboard/src/components/resources/index.ts +3 -0
  77. package/templates/shared/dashboard/src/components/settings/SettingsCard.tsx +21 -0
  78. package/templates/shared/dashboard/src/components/settings/index.ts +1 -0
  79. package/templates/shared/dashboard/src/components/ui/AlertBanner.tsx +39 -0
  80. package/templates/shared/dashboard/src/components/ui/Breadcrumbs.tsx +27 -0
  81. package/templates/shared/dashboard/src/components/ui/EmptyState.tsx +26 -0
  82. package/templates/shared/dashboard/src/components/ui/ErrorBoundary.tsx +42 -0
  83. package/templates/shared/dashboard/src/components/ui/LoadingSkeleton.tsx +18 -0
  84. package/templates/shared/dashboard/src/components/ui/PageShell.tsx +26 -0
  85. package/templates/shared/dashboard/src/components/ui/Sparkline.tsx +127 -0
  86. package/templates/shared/dashboard/src/components/ui/StatusDot.tsx +21 -0
  87. package/templates/shared/dashboard/src/components/ui/Toast.tsx +44 -0
  88. package/templates/shared/dashboard/src/components/ui/index.ts +9 -0
  89. package/templates/shared/dashboard/src/components/usage/AnomaliesWidget.tsx +68 -0
  90. package/templates/shared/dashboard/src/components/usage/HourlyUsageChart.tsx +55 -0
  91. package/templates/shared/dashboard/src/components/usage/PlanAllowanceDashboard.tsx +67 -0
  92. package/templates/shared/dashboard/src/components/usage/ProjectCostBreakdown.tsx +55 -0
  93. package/templates/shared/dashboard/src/components/usage/index.ts +4 -0
  94. package/templates/shared/dashboard/src/env.d.ts.hbs +34 -0
  95. package/templates/shared/dashboard/src/layouts/DashboardLayout.astro +37 -0
  96. package/templates/shared/dashboard/src/lib/cloudflare/costs.ts +21 -0
  97. package/templates/shared/dashboard/src/lib/fetch.ts +29 -0
  98. package/templates/shared/dashboard/src/lib/types.ts +72 -0
  99. package/templates/shared/dashboard/src/middleware/auth.ts +100 -0
  100. package/templates/shared/dashboard/src/middleware/index.ts +1 -0
  101. package/templates/shared/dashboard/src/pages/api/costs/overview.ts +65 -0
  102. package/templates/shared/dashboard/src/pages/api/costs/providers.ts +47 -0
  103. package/templates/shared/dashboard/src/pages/api/infrastructure/services.ts +55 -0
  104. package/templates/shared/dashboard/src/pages/api/infrastructure/stats.ts +99 -0
  105. package/templates/shared/dashboard/src/pages/api/overview/summary.ts +311 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/allowances.ts +56 -0
  107. package/templates/shared/dashboard/src/pages/api/usage/anomalies.ts +45 -0
  108. package/templates/shared/dashboard/src/pages/api/usage/billing.ts +53 -0
  109. package/templates/shared/dashboard/src/pages/api/usage/circuit-breakers.ts +44 -0
  110. package/templates/shared/dashboard/src/pages/api/usage/granular.ts +50 -0
  111. package/templates/shared/dashboard/src/pages/api/usage/hourly.ts +45 -0
  112. package/templates/shared/dashboard/src/pages/api/usage/projects.ts +51 -0
  113. package/templates/shared/dashboard/src/pages/api/usage/status.ts +42 -0
  114. package/templates/shared/dashboard/src/pages/api/user/identity.ts +11 -0
  115. package/templates/shared/dashboard/src/pages/dashboard.astro +11 -0
  116. package/templates/shared/dashboard/src/pages/index.astro +3 -0
  117. package/templates/shared/dashboard/src/pages/resources.astro +11 -0
  118. package/templates/shared/dashboard/src/pages/settings/index.astro +28 -0
  119. package/templates/shared/dashboard/src/pages/settings/notifications.astro +34 -0
  120. package/templates/shared/dashboard/src/pages/settings/thresholds.astro +39 -0
  121. package/templates/shared/dashboard/src/pages/settings/usage.astro +28 -0
  122. package/templates/shared/dashboard/src/styles/global.css +29 -0
  123. package/templates/shared/dashboard/tailwind.config.mjs +9 -0
  124. package/templates/shared/dashboard/tsconfig.json +9 -0
  125. package/templates/shared/dashboard/wrangler.json.hbs +47 -0
  126. package/templates/shared/docs/architecture.md +89 -0
  127. package/templates/shared/docs/post-deploy-runbook.md +126 -0
  128. package/templates/shared/docs/troubleshooting.md +91 -0
  129. package/templates/shared/package.json.hbs +17 -1
  130. package/templates/shared/scripts/ops/backfill-cloudflare-daily.ts +145 -0
  131. package/templates/shared/scripts/ops/backfill-cloudflare-hourly.ts +473 -0
  132. package/templates/shared/scripts/ops/backfill-monthly-rollups.ts +125 -0
  133. package/templates/shared/scripts/ops/discover-graphql-datasets.ts +482 -0
  134. package/templates/shared/scripts/ops/reset-budget-state.ts +279 -0
  135. package/templates/shared/scripts/ops/validate-controls.js +141 -0
  136. package/templates/shared/scripts/ops/validate-pipeline.ts +237 -0
  137. package/templates/shared/scripts/ops/verify-account-completeness.ts +236 -0
  138. package/templates/shared/scripts/validate-schemas.js +61 -0
  139. package/templates/shared/tests/contract/validate-schemas.test.ts +130 -0
  140. package/templates/shared/tests/fixtures/telemetry-envelope-invalid.json +9 -0
  141. package/templates/shared/tests/fixtures/telemetry-envelope-valid.json +27 -0
  142. package/templates/shared/tests/helpers/mock-d1.ts +61 -0
  143. package/templates/shared/tests/helpers/mock-kv.ts +37 -0
  144. package/templates/shared/tests/unit/workers/batch-persistence.test.ts +133 -0
  145. package/templates/shared/tests/unit/workers/budget-enforcement.test.ts +214 -0
  146. package/templates/shared/vitest.config.ts +18 -0
  147. package/templates/shared/workers/lib/usage/collectors/anthropic.ts +114 -0
  148. package/templates/shared/workers/lib/usage/collectors/apify.ts +96 -0
  149. package/templates/shared/workers/lib/usage/collectors/custom-http.ts +151 -0
  150. package/templates/shared/workers/lib/usage/collectors/deepseek.ts +92 -0
  151. package/templates/shared/workers/lib/usage/collectors/gemini.ts +263 -0
  152. package/templates/shared/workers/lib/usage/collectors/github.ts +362 -0
  153. package/templates/shared/workers/lib/usage/collectors/index.ts +31 -15
  154. package/templates/shared/workers/lib/usage/collectors/minimax.ts +106 -0
  155. package/templates/shared/workers/lib/usage/collectors/openai.ts +171 -0
  156. package/templates/shared/workers/lib/usage/collectors/resend.ts +79 -0
  157. package/templates/shared/workers/lib/usage/collectors/stripe.ts +192 -0
  158. package/templates/shared/workers/lib/usage/shared/types.ts +46 -0
  159. package/templates/shared/workers/platform-usage.ts +98 -8
  160. package/templates/standard/dashboard/src/components/errors/ErrorStats.tsx +53 -0
  161. package/templates/standard/dashboard/src/components/errors/ErrorsTable.tsx +133 -0
  162. package/templates/standard/dashboard/src/components/errors/index.ts +2 -0
  163. package/templates/standard/dashboard/src/components/health/CircuitBreakerEvents.tsx +69 -0
  164. package/templates/standard/dashboard/src/components/health/CircuitBreakerPanel.tsx +97 -0
  165. package/templates/standard/dashboard/src/components/health/DlqStatusCard.tsx +52 -0
  166. package/templates/standard/dashboard/src/components/health/HealthTabs.tsx +86 -0
  167. package/templates/standard/dashboard/src/components/health/index.ts +4 -0
  168. package/templates/standard/dashboard/src/lib/errors.ts +28 -0
  169. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/mute.ts +49 -0
  170. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint]/resolve.ts +36 -0
  171. package/templates/standard/dashboard/src/pages/api/errors/[fingerprint].ts +55 -0
  172. package/templates/standard/dashboard/src/pages/api/errors/index.ts +58 -0
  173. package/templates/standard/dashboard/src/pages/api/errors/stats.ts +55 -0
  174. package/templates/standard/dashboard/src/pages/api/health/audit-history.ts +37 -0
  175. package/templates/standard/dashboard/src/pages/api/health/dlq.ts +43 -0
  176. package/templates/standard/dashboard/src/pages/circuit-breakers.astro +13 -0
  177. package/templates/standard/dashboard/src/pages/errors.astro +13 -0
  178. package/templates/standard/dashboard/src/pages/health.astro +11 -0
  179. package/templates/standard/migrations/009_topology_mapper.sql +65 -0
  180. package/templates/standard/tests/unit/error-collector/capture.test.ts +106 -0
  181. package/templates/standard/tests/unit/error-collector/fingerprint.test.ts +155 -0
  182. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +37 -3
  183. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +32 -1
  184. package/templates/standard/workers/lib/mapper/attribution-check.ts +339 -0
  185. package/templates/standard/workers/lib/mapper/index.ts +7 -0
  186. package/templates/standard/workers/platform-mapper.ts +482 -0
  187. package/templates/standard/workers/platform-sdk-test-client.ts +125 -0
  188. package/templates/standard/wrangler.mapper.jsonc.hbs +85 -0
  189. package/templates/standard/wrangler.sdk-test-client.jsonc.hbs +62 -0
@@ -0,0 +1,1071 @@
1
+ /**
2
+ * Platform Auditor Worker
3
+ *
4
+ * SDK integration auditor using 4-layer triangulation:
5
+ * 1. Config checks — Verify PLATFORM_CACHE KV and PLATFORM_TELEMETRY queue bindings
6
+ * 2. Code smell tests — String matching for SDK patterns in source files
7
+ * 3. Runtime checks — Cross-reference D1 system_health_checks heartbeats
8
+ * 4. AI Judge — Gemini via AI Gateway deep code analysis (rubric-based scoring)
9
+ *
10
+ * Audit targets are loaded from KV (CONFIG:AUDIT_TARGETS) — configure in config/audit-targets.yaml
11
+ * and sync via `npm run sync:config`.
12
+ *
13
+ * Schedule:
14
+ * Sunday Midnight UTC — Full deep scan (all dimensions, all projects)
15
+ * Wednesday Midnight UTC — Focused scan (weak dimensions, projects < threshold)
16
+ *
17
+ * @module workers/platform-auditor
18
+ */
19
+
20
+ import type {
21
+ KVNamespace,
22
+ ExecutionContext,
23
+ ScheduledEvent,
24
+ D1Database,
25
+ Fetcher,
26
+ } from '@cloudflare/workers-types';
27
+ import {
28
+ withFeatureBudget,
29
+ withCronBudget,
30
+ CircuitBreakerError,
31
+ completeTracking,
32
+ createLogger,
33
+ createLoggerFromRequest,
34
+ createTraceContext,
35
+ health,
36
+ pingHeartbeat,
37
+ type Logger,
38
+ } from '@littlebearapps/platform-consumer-sdk';
39
+ import {
40
+ validateAIJudgeResponse,
41
+ calculateCompositeScore,
42
+ formatValidationErrors,
43
+ DEFAULT_RUBRIC_WEIGHTS,
44
+ type RubricScores,
45
+ type CategorisedIssue,
46
+ } from './lib/ai-judge-schema';
47
+ import {
48
+ runFeatureCoverageAudit,
49
+ storeFeatureCoverageAudit,
50
+ type FeatureCoverageReport,
51
+ } from './lib/auditor/feature-coverage';
52
+ import {
53
+ generateComprehensiveReport,
54
+ storeComprehensiveReport,
55
+ } from './lib/auditor/comprehensive-report';
56
+ import type {
57
+ AuditorEnv,
58
+ AuditTarget,
59
+ AuditTargetsConfig,
60
+ AuditDimension,
61
+ AuditStatus,
62
+ ScanType,
63
+ ConfigCheckResult,
64
+ CodeSmellResult,
65
+ RuntimeCheckResult,
66
+ AIJudgeResult,
67
+ BehavioralResult,
68
+ ProjectAuditResult,
69
+ } from './lib/auditor/types';
70
+
71
+ // =============================================================================
72
+ // Feature IDs (register these in budgets.yaml)
73
+ // =============================================================================
74
+
75
+ const FEATURE_AUDITOR = 'platform:monitor:auditor';
76
+ const FEATURE_HEARTBEAT = 'platform:heartbeat:auditor';
77
+
78
+ // =============================================================================
79
+ // Constants (defaults — overridden by audit-targets.yaml config)
80
+ // =============================================================================
81
+
82
+ const DEFAULT_HEARTBEAT_FRESHNESS_HOURS = 24;
83
+ const DEFAULT_AI_CACHE_TTL_SECONDS = 3 * 24 * 60 * 60; // 3 days
84
+ const DEFAULT_FOCUSED_SCAN_THRESHOLD = 90;
85
+ const DEEP_SCAN_FILE_LIMIT = 50000; // Max chars per file
86
+ const SUPPLEMENTARY_CONTENT_WARN_LIMIT = 3_000_000; // ~750K tokens safety
87
+ const DEFAULT_BEHAVIORAL_ANALYSIS_DAYS = 30;
88
+ const DEFAULT_HOTSPOT_MIN_COMMITS = 3;
89
+ const AI_MAX_RETRIES = 2;
90
+
91
+ const GITHUB_API_BASE = 'https://api.github.com';
92
+ const AI_GATEWAY_URL = 'https://gateway.ai.cloudflare.com/v1';
93
+
94
+ const DEFAULT_SDK_PATTERNS = [
95
+ 'withFeatureBudget', 'withCronBudget', 'withQueueBudget',
96
+ 'trackedEnv', 'platform-sdk', 'completeTracking',
97
+ 'CircuitBreakerError', 'PLATFORM_CACHE', 'PLATFORM_TELEMETRY',
98
+ ];
99
+
100
+ // =============================================================================
101
+ // Wrangler Config Shape (for JSONC parsing)
102
+ // =============================================================================
103
+
104
+ interface WranglerConfig {
105
+ kv_namespaces?: Array<{ binding: string }>;
106
+ queues?: { producers?: Array<{ binding: string }> };
107
+ observability?: {
108
+ enabled?: boolean;
109
+ logs?: { enabled?: boolean; sampling_rate?: number; invocation_logs?: boolean };
110
+ traces?: { enabled?: boolean; head_sampling_rate?: number };
111
+ };
112
+ upload_source_maps?: boolean;
113
+ }
114
+
115
+ // =============================================================================
116
+ // GitHub Types
117
+ // =============================================================================
118
+
119
+ interface GitHubCommit {
120
+ sha: string;
121
+ commit: { message: string; author: { name: string; date: string } };
122
+ files?: Array<{ filename: string; status: string; patch?: string }>;
123
+ }
124
+
125
+ // =============================================================================
126
+ // CONFIG LOADING
127
+ // =============================================================================
128
+
129
+ /** Load audit targets config from KV */
130
+ async function loadAuditConfig(
131
+ env: AuditorEnv,
132
+ log: Logger
133
+ ): Promise<AuditTargetsConfig> {
134
+ try {
135
+ const configJson = await env.PLATFORM_CACHE.get('CONFIG:AUDIT_TARGETS');
136
+ if (configJson) {
137
+ return JSON.parse(configJson) as AuditTargetsConfig;
138
+ }
139
+ } catch (error) {
140
+ log.warn('Failed to load audit targets from KV, using defaults', error);
141
+ }
142
+
143
+ return {
144
+ targets: {},
145
+ sdkPatterns: DEFAULT_SDK_PATTERNS,
146
+ rubricWeights: { ...DEFAULT_RUBRIC_WEIGHTS },
147
+ heartbeatFreshnessHours: DEFAULT_HEARTBEAT_FRESHNESS_HOURS,
148
+ focusedScanThreshold: DEFAULT_FOCUSED_SCAN_THRESHOLD,
149
+ aiCacheTtlSeconds: DEFAULT_AI_CACHE_TTL_SECONDS,
150
+ behavioralAnalysisDays: DEFAULT_BEHAVIORAL_ANALYSIS_DAYS,
151
+ hotspotMinCommits: DEFAULT_HOTSPOT_MIN_COMMITS,
152
+ };
153
+ }
154
+
155
+ // =============================================================================
156
+ // SCAN TYPE HELPERS
157
+ // =============================================================================
158
+
159
+ function determineScanType(scheduledTime: number): ScanType {
160
+ const day = new Date(scheduledTime).getUTCDay();
161
+ return day === 0 ? 'full' : 'focused';
162
+ }
163
+
164
+ async function getWeakDimensions(
165
+ project: string,
166
+ threshold: number,
167
+ env: AuditorEnv,
168
+ log: Logger
169
+ ): Promise<{ dimensions: AuditDimension[]; previousScore: number }> {
170
+ try {
171
+ const latest = await env.PLATFORM_DB.prepare(
172
+ `SELECT composite_score, rubric_sdk, rubric_observability, rubric_cost_protection, rubric_security
173
+ FROM audit_results
174
+ WHERE project = ? AND scan_type = 'full' AND ai_judge_score IS NOT NULL
175
+ ORDER BY created_at DESC LIMIT 1`
176
+ ).bind(project).first<{
177
+ composite_score: number;
178
+ rubric_sdk: number | null;
179
+ rubric_observability: number | null;
180
+ rubric_cost_protection: number | null;
181
+ rubric_security: number | null;
182
+ }>();
183
+
184
+ if (!latest) {
185
+ return { dimensions: ['sdk', 'observability', 'cost', 'security'], previousScore: 0 };
186
+ }
187
+
188
+ const weak: AuditDimension[] = [];
189
+ if ((latest.rubric_sdk ?? 0) < 4) weak.push('sdk');
190
+ if ((latest.rubric_observability ?? 0) < 4) weak.push('observability');
191
+ if ((latest.rubric_cost_protection ?? 0) < 4) weak.push('cost');
192
+ if ((latest.rubric_security ?? 0) < 4) weak.push('security');
193
+
194
+ if (weak.length === 0 && latest.composite_score < threshold) {
195
+ return { dimensions: ['sdk', 'observability', 'cost', 'security'], previousScore: latest.composite_score };
196
+ }
197
+
198
+ return { dimensions: weak, previousScore: latest.composite_score };
199
+ } catch (error) {
200
+ log.warn('Failed to get weak dimensions, defaulting to full scan', error, { project });
201
+ return { dimensions: ['sdk', 'observability', 'cost', 'security'], previousScore: 0 };
202
+ }
203
+ }
204
+
205
+ // =============================================================================
206
+ // CHECK FUNCTIONS
207
+ // =============================================================================
208
+
209
+ /** Layer 1: Config checks (wrangler bindings) */
210
+ function checkConfig(config: WranglerConfig | null): ConfigCheckResult {
211
+ if (!config) {
212
+ return {
213
+ hasPlatformCache: false, hasPlatformTelemetry: false,
214
+ observabilityEnabled: false, logsEnabled: false,
215
+ tracesEnabled: false, traceSamplingRate: null,
216
+ logSamplingRate: null, invocationLogsEnabled: false,
217
+ sourceMapsEnabled: false, issues: ['Wrangler config not found or invalid'],
218
+ };
219
+ }
220
+
221
+ const kvBindings = (config.kv_namespaces ?? []).map((kv) => kv.binding);
222
+ const queueBindings = (config.queues?.producers ?? []).map((q) => q.binding);
223
+ const obs = config.observability;
224
+ const issues: string[] = [];
225
+
226
+ const hasPlatformCache = kvBindings.includes('PLATFORM_CACHE');
227
+ const hasPlatformTelemetry = queueBindings.includes('PLATFORM_TELEMETRY');
228
+ const observabilityEnabled = obs?.enabled ?? false;
229
+ const logsEnabled = obs?.logs?.enabled ?? false;
230
+ const tracesEnabled = obs?.traces?.enabled ?? false;
231
+
232
+ if (!hasPlatformCache) issues.push('Missing PLATFORM_CACHE KV binding');
233
+ if (!hasPlatformTelemetry) issues.push('Missing PLATFORM_TELEMETRY queue binding');
234
+ if (!observabilityEnabled) issues.push('Observability not enabled');
235
+
236
+ return {
237
+ hasPlatformCache, hasPlatformTelemetry,
238
+ observabilityEnabled, logsEnabled,
239
+ tracesEnabled,
240
+ traceSamplingRate: obs?.traces?.head_sampling_rate ?? null,
241
+ logSamplingRate: obs?.logs?.sampling_rate ?? null,
242
+ invocationLogsEnabled: obs?.logs?.invocation_logs ?? false,
243
+ sourceMapsEnabled: config.upload_source_maps ?? false,
244
+ issues,
245
+ };
246
+ }
247
+
248
+ /** Layer 2: Code smell checks (SDK pattern detection) */
249
+ function checkCodeSmells(
250
+ content: string | null,
251
+ patterns: string[] = DEFAULT_SDK_PATTERNS
252
+ ): CodeSmellResult {
253
+ if (!content) {
254
+ return {
255
+ hasSdkFolder: false, hasWithFeatureBudget: false,
256
+ hasTrackedEnv: false, hasCircuitBreakerError: false,
257
+ hasErrorLogging: false, patternCounts: {},
258
+ };
259
+ }
260
+
261
+ const patternCounts: Record<string, number> = {};
262
+ for (const pattern of patterns) {
263
+ const matches = content.match(new RegExp(pattern, 'g'));
264
+ patternCounts[pattern] = matches?.length ?? 0;
265
+ }
266
+
267
+ return {
268
+ hasSdkFolder: content.includes('platform-sdk') || content.includes('@littlebearapps/platform'),
269
+ hasWithFeatureBudget: (patternCounts['withFeatureBudget'] ?? 0) > 0,
270
+ hasTrackedEnv: (patternCounts['trackedEnv'] ?? 0) > 0,
271
+ hasCircuitBreakerError: (patternCounts['CircuitBreakerError'] ?? 0) > 0,
272
+ hasErrorLogging: content.includes('log.error') || content.includes('console.error'),
273
+ patternCounts,
274
+ };
275
+ }
276
+
277
+ /** Layer 3: Runtime checks (heartbeat freshness) */
278
+ async function checkRuntime(
279
+ project: string,
280
+ freshnessHours: number,
281
+ env: AuditorEnv,
282
+ log: Logger
283
+ ): Promise<RuntimeCheckResult> {
284
+ try {
285
+ const result = await env.PLATFORM_DB.prepare(
286
+ `SELECT MAX(last_heartbeat) as latest
287
+ FROM system_health_checks
288
+ WHERE project_id = ?`
289
+ ).bind(project).first<{ latest: number | null }>();
290
+
291
+ if (!result?.latest) {
292
+ return { hasRecentHeartbeat: false, hoursSinceHeartbeat: null, lastHeartbeatTime: null };
293
+ }
294
+
295
+ const hoursSince = (Date.now() / 1000 - result.latest) / 3600;
296
+ return {
297
+ hasRecentHeartbeat: hoursSince < freshnessHours,
298
+ hoursSinceHeartbeat: Math.round(hoursSince),
299
+ lastHeartbeatTime: new Date(result.latest * 1000).toISOString(),
300
+ };
301
+ } catch (error) {
302
+ log.error('Runtime check failed', error, { project });
303
+ return { hasRecentHeartbeat: false, hoursSinceHeartbeat: null, lastHeartbeatTime: null };
304
+ }
305
+ }
306
+
307
+ // =============================================================================
308
+ // AI JUDGE
309
+ // =============================================================================
310
+
311
+ /** Layer 4: AI Judge analysis via Gemini AI Gateway */
312
+ async function auditWithGemini(
313
+ project: string,
314
+ entryPointContent: string,
315
+ wranglerContent: string | null,
316
+ staticChecks: Record<string, boolean>,
317
+ supplementaryContent: string,
318
+ config: AuditTargetsConfig,
319
+ env: AuditorEnv,
320
+ log: Logger
321
+ ): Promise<AIJudgeResult | null> {
322
+ // Check content hash cache
323
+ const contentHash = await hashContent(`${project}:${entryPointContent}:${supplementaryContent}`);
324
+ const cacheKey = `AUDIT:AI:${contentHash}`;
325
+
326
+ const cached = await env.PLATFORM_CACHE.get(cacheKey);
327
+ if (cached) {
328
+ log.debug('AI Judge cache hit', { project, cacheKey });
329
+ try {
330
+ return JSON.parse(cached) as AIJudgeResult;
331
+ } catch { /* cache corrupt, proceed with fresh analysis */ }
332
+ }
333
+
334
+ const prompt = buildAIPrompt(project, entryPointContent, wranglerContent, staticChecks, supplementaryContent);
335
+ const weights = config.rubricWeights ?? DEFAULT_RUBRIC_WEIGHTS;
336
+
337
+ for (let attempt = 0; attempt <= AI_MAX_RETRIES; attempt++) {
338
+ try {
339
+ const response = await callGemini(prompt, env, log);
340
+ if (!response) return null;
341
+
342
+ const parsed = JSON.parse(response);
343
+ const validation = validateAIJudgeResponse(parsed);
344
+
345
+ if (validation.success) {
346
+ const data = validation.data;
347
+ const rubricScores = data.rubricScores;
348
+ const compositeScore = calculateCompositeScore(rubricScores, weights);
349
+
350
+ const result: AIJudgeResult = {
351
+ score: compositeScore,
352
+ summary: data.summary,
353
+ issues: JSON.stringify(data.issues),
354
+ categorisedIssues: JSON.stringify(data.issues),
355
+ reasoning: data.reasoning,
356
+ rubricScores: {
357
+ sdk: rubricScores.sdk.score,
358
+ observability: rubricScores.observability.score,
359
+ costProtection: rubricScores.costProtection.score,
360
+ security: rubricScores.security.score,
361
+ },
362
+ rubricEvidence: JSON.stringify({
363
+ sdk: rubricScores.sdk.evidence,
364
+ observability: rubricScores.observability.evidence,
365
+ costProtection: rubricScores.costProtection.evidence,
366
+ security: rubricScores.security.evidence,
367
+ }),
368
+ validationRetries: attempt,
369
+ cachedAt: new Date().toISOString(),
370
+ };
371
+
372
+ // Cache result
373
+ await env.PLATFORM_CACHE.put(cacheKey, JSON.stringify(result), {
374
+ expirationTtl: config.aiCacheTtlSeconds ?? DEFAULT_AI_CACHE_TTL_SECONDS,
375
+ });
376
+
377
+ log.info('AI Judge complete', { project, score: compositeScore, attempt });
378
+ return result;
379
+ }
380
+
381
+ // Validation failed — retry with error feedback
382
+ log.warn('AI Judge validation failed', { project, attempt, errors: validation.errors });
383
+ if (attempt < AI_MAX_RETRIES) {
384
+ prompt.concat('\n\n' + formatValidationErrors(validation.errors));
385
+ }
386
+ } catch (error) {
387
+ log.error('AI Judge attempt failed', error, { project, attempt });
388
+ }
389
+ }
390
+
391
+ return null;
392
+ }
393
+
394
+ /** Build AI Judge prompt */
395
+ function buildAIPrompt(
396
+ project: string,
397
+ entryPoint: string,
398
+ wrangler: string | null,
399
+ staticChecks: Record<string, boolean>,
400
+ supplementary: string
401
+ ): string {
402
+ return `You are an expert Cloudflare Workers SDK integration auditor.
403
+
404
+ Analyse the following project "${project}" and score it on 4 dimensions (1-5 scale):
405
+ - sdk: SDK integration patterns (withFeatureBudget, completeTracking, circuit breakers)
406
+ - observability: Logging, tracing, metrics, error reporting
407
+ - costProtection: Circuit breakers, budgets, DLQ handling, rate limiting
408
+ - security: Authentication, input validation, secrets handling, CORS
409
+
410
+ ## Entry Point:
411
+ \`\`\`typescript
412
+ ${entryPoint.slice(0, DEEP_SCAN_FILE_LIMIT)}
413
+ \`\`\`
414
+
415
+ ${wrangler ? `## Wrangler Config:\n\`\`\`jsonc\n${wrangler.slice(0, 5000)}\n\`\`\`` : ''}
416
+
417
+ ## Static Check Results:
418
+ ${JSON.stringify(staticChecks, null, 2)}
419
+
420
+ ${supplementary ? `## Additional Source Files:\n${supplementary}` : ''}
421
+
422
+ Respond with ONLY valid JSON matching this schema:
423
+ {
424
+ "reasoning": "Chain-of-thought analysis...",
425
+ "rubricScores": {
426
+ "sdk": { "score": 1-5, "evidence": ["..."] },
427
+ "observability": { "score": 1-5, "evidence": ["..."] },
428
+ "costProtection": { "score": 1-5, "evidence": ["..."] },
429
+ "security": { "score": 1-5, "evidence": ["..."] }
430
+ },
431
+ "issues": [{ "category": "sdk|observability|cost|security", "severity": "critical|high|medium|low", "description": "...", "file": "...", "line": 123 }],
432
+ "summary": "Brief summary..."
433
+ }`;
434
+ }
435
+
436
+ /** Call Gemini via AI Gateway */
437
+ async function callGemini(
438
+ prompt: string,
439
+ env: AuditorEnv,
440
+ log: Logger
441
+ ): Promise<string | null> {
442
+ const gatewayUrl = `${AI_GATEWAY_URL}/${env.CLOUDFLARE_ACCOUNT_ID}/platform/google-ai-studio/v1beta/models/gemini-2.0-flash-lite:generateContent`;
443
+
444
+ try {
445
+ const response = await fetch(gatewayUrl, {
446
+ method: 'POST',
447
+ headers: {
448
+ 'Content-Type': 'application/json',
449
+ 'cf-aig-authorization': `Bearer ${env.PLATFORM_AI_GATEWAY_KEY}`,
450
+ 'x-goog-api-key': env.GEMINI_API_KEY,
451
+ },
452
+ body: JSON.stringify({
453
+ contents: [{ parts: [{ text: prompt }] }],
454
+ generationConfig: {
455
+ temperature: 0.1,
456
+ maxOutputTokens: 4096,
457
+ responseMimeType: 'application/json',
458
+ },
459
+ }),
460
+ });
461
+
462
+ if (!response.ok) {
463
+ const errorBody = await response.text();
464
+ log.error('Gemini API error', new Error(`HTTP ${response.status}`), {
465
+ status: response.status,
466
+ errorBody: errorBody.substring(0, 500),
467
+ });
468
+ return null;
469
+ }
470
+
471
+ const data = await response.json() as {
472
+ candidates?: Array<{ content?: { parts?: Array<{ text?: string }> } }>;
473
+ };
474
+
475
+ return data.candidates?.[0]?.content?.parts?.[0]?.text ?? null;
476
+ } catch (error) {
477
+ log.error('Gemini fetch failed', error);
478
+ return null;
479
+ }
480
+ }
481
+
482
+ // =============================================================================
483
+ // STATUS DETERMINATION
484
+ // =============================================================================
485
+
486
+ function determineStatus(
487
+ config: ConfigCheckResult,
488
+ code: CodeSmellResult,
489
+ runtime: RuntimeCheckResult
490
+ ): AuditStatus {
491
+ const hasConfig = config.hasPlatformCache && config.hasPlatformTelemetry;
492
+ const hasCode = code.hasWithFeatureBudget || code.hasTrackedEnv;
493
+ const hasRuntime = runtime.hasRecentHeartbeat;
494
+
495
+ if (hasConfig && hasCode && hasRuntime) return 'HEALTHY';
496
+ if (hasConfig && hasCode && !hasRuntime) return 'ZOMBIE';
497
+ if (hasConfig && !hasCode) return 'BROKEN';
498
+ if (!hasConfig && hasCode) return 'BROKEN';
499
+ if (!hasConfig && !hasCode && hasRuntime) return 'UNTRACKED';
500
+ return 'NOT_INTEGRATED';
501
+ }
502
+
503
+ function generateStatusMessage(
504
+ status: AuditStatus,
505
+ config: ConfigCheckResult,
506
+ code: CodeSmellResult,
507
+ runtime: RuntimeCheckResult
508
+ ): string {
509
+ switch (status) {
510
+ case 'HEALTHY': return 'SDK fully integrated and active';
511
+ case 'ZOMBIE': return `SDK integrated but no heartbeat in ${runtime.hoursSinceHeartbeat ?? '?'}h`;
512
+ case 'BROKEN': return `SDK misconfigured: ${config.issues.join(', ')}`;
513
+ case 'UNTRACKED': return 'Runtime activity detected but no SDK integration';
514
+ case 'NOT_INTEGRATED': return 'No SDK integration detected';
515
+ }
516
+ }
517
+
518
+ // =============================================================================
519
+ // BEHAVIORAL ANALYSIS
520
+ // =============================================================================
521
+
522
+ async function runBehavioralAnalysis(
523
+ project: string,
524
+ repo: string,
525
+ sdkPatterns: string[],
526
+ config: AuditTargetsConfig,
527
+ env: AuditorEnv,
528
+ log: Logger
529
+ ): Promise<BehavioralResult> {
530
+ const days = config.behavioralAnalysisDays ?? DEFAULT_BEHAVIORAL_ANALYSIS_DAYS;
531
+ const minCommits = config.hotspotMinCommits ?? DEFAULT_HOTSPOT_MIN_COMMITS;
532
+
533
+ const commits = await fetchCommitHistory(repo, env.GITHUB_TOKEN, days, log);
534
+ if (commits.length === 0) {
535
+ return { hotspots: [], regressions: [], analysisWindow: days };
536
+ }
537
+
538
+ // Fetch details for recent commits (limit to 20 for performance)
539
+ const recentCommits = commits.slice(0, 20);
540
+ const commitDetails = new Map<string, GitHubCommit>();
541
+ for (const commit of recentCommits) {
542
+ const details = await fetchCommitDetails(repo, commit.sha, env.GITHUB_TOKEN, log);
543
+ if (details) commitDetails.set(commit.sha, details);
544
+ }
545
+
546
+ // Analyze hotspots
547
+ const fileChanges = new Map<string, { count: number; hasSdkPatterns: boolean }>();
548
+ for (const commit of commits) {
549
+ const details = commitDetails.get(commit.sha);
550
+ if (!details?.files) continue;
551
+ for (const file of details.files) {
552
+ if (!file.filename.match(/\.(ts|tsx|js|jsx)$/)) continue;
553
+ const existing = fileChanges.get(file.filename);
554
+ if (existing) { existing.count++; }
555
+ else {
556
+ const hasSdk = file.filename.includes('platform-sdk') || file.filename.includes('circuit-breaker');
557
+ fileChanges.set(file.filename, { count: 1, hasSdkPatterns: hasSdk });
558
+ }
559
+ }
560
+ }
561
+
562
+ const hotspots = [...fileChanges.entries()]
563
+ .filter(([, data]) => data.count >= minCommits)
564
+ .map(([filePath, data]) => ({
565
+ filePath,
566
+ changeCount: data.count,
567
+ hasSdkPatterns: data.hasSdkPatterns,
568
+ hotspotScore: data.count * (data.hasSdkPatterns ? 1 : 3),
569
+ }))
570
+ .sort((a, b) => b.hotspotScore - a.hotspotScore)
571
+ .slice(0, 10);
572
+
573
+ // Detect regressions
574
+ const regressions: BehavioralResult['regressions'] = [];
575
+ for (const commit of commits) {
576
+ const details = commitDetails.get(commit.sha);
577
+ if (!details?.files) continue;
578
+
579
+ const allAdded: string[] = [];
580
+ for (const f of details.files) {
581
+ if (!f.patch) continue;
582
+ for (const line of f.patch.split('\n')) {
583
+ if (line.startsWith('+') && !line.startsWith('+++')) allAdded.push(line);
584
+ }
585
+ }
586
+
587
+ for (const file of details.files) {
588
+ if (!file.patch || file.status === 'removed') continue;
589
+ const removed = file.patch.split('\n').filter((l) => l.startsWith('-') && !l.startsWith('---'));
590
+ const added = file.patch.split('\n').filter((l) => l.startsWith('+') && !l.startsWith('+++'));
591
+
592
+ for (const pattern of sdkPatterns) {
593
+ const removedCount = removed.filter((l) => l.includes(pattern)).length;
594
+ const addedCount = added.filter((l) => l.includes(pattern)).length;
595
+ if (removedCount > addedCount) {
596
+ const addedElsewhere = allAdded.some((l) => l.includes(pattern));
597
+ if (!addedElsewhere) {
598
+ regressions.push({
599
+ commitSha: commit.sha,
600
+ commitMessage: commit.commit.message.split('\n')[0].substring(0, 100),
601
+ removedPattern: pattern,
602
+ filePath: file.filename,
603
+ });
604
+ }
605
+ }
606
+ }
607
+ }
608
+ }
609
+
610
+ log.info('Behavioral analysis complete', {
611
+ project, commitsAnalyzed: commits.length,
612
+ hotspotsFound: hotspots.length, regressionsFound: regressions.length,
613
+ });
614
+
615
+ return { hotspots, regressions, analysisWindow: days };
616
+ }
617
+
618
+ // =============================================================================
619
+ // GITHUB HELPERS
620
+ // =============================================================================
621
+
622
+ async function fetchGitHubFile(
623
+ repo: string, path: string, token: string, log: Logger
624
+ ): Promise<string | null> {
625
+ try {
626
+ const response = await fetch(`${GITHUB_API_BASE}/repos/${repo}/contents/${path}`, {
627
+ headers: {
628
+ Authorization: `Bearer ${token}`,
629
+ Accept: 'application/vnd.github.v3.raw',
630
+ 'User-Agent': 'platform-auditor/1.0',
631
+ },
632
+ });
633
+ if (!response.ok) {
634
+ if (response.status !== 404) {
635
+ const body = await response.text();
636
+ log.error('GitHub API error', new Error(`HTTP ${response.status}`), { repo, path, body: body.substring(0, 500) });
637
+ }
638
+ return null;
639
+ }
640
+ return await response.text();
641
+ } catch (error) {
642
+ log.error('GitHub fetch failed', error, { repo, path });
643
+ return null;
644
+ }
645
+ }
646
+
647
+ async function fetchDeepScanFiles(
648
+ target: AuditTarget, dimensions: AuditDimension[],
649
+ env: AuditorEnv, log: Logger
650
+ ): Promise<string> {
651
+ const files = new Set<string>();
652
+ for (const dim of dimensions) {
653
+ for (const f of target.deepScanFiles[dim] || []) files.add(f);
654
+ }
655
+
656
+ let content = '';
657
+ for (const filePath of files) {
658
+ const fileContent = await fetchGitHubFile(target.repo, filePath, env.GITHUB_TOKEN, log);
659
+ if (fileContent) {
660
+ content += `\n\n## ${filePath}:\n\`\`\`typescript\n${fileContent.slice(0, DEEP_SCAN_FILE_LIMIT)}\n\`\`\``;
661
+ }
662
+ }
663
+
664
+ if (content.length > SUPPLEMENTARY_CONTENT_WARN_LIMIT) {
665
+ log.warn('Deep scan content exceeds safety threshold', {
666
+ repo: target.repo, contentLength: content.length,
667
+ estimatedTokens: Math.round(content.length / 4),
668
+ });
669
+ }
670
+
671
+ return content;
672
+ }
673
+
674
+ async function fetchCommitHistory(
675
+ repo: string, token: string, days: number, log: Logger
676
+ ): Promise<GitHubCommit[]> {
677
+ const since = new Date();
678
+ since.setDate(since.getDate() - days);
679
+ try {
680
+ const response = await fetch(
681
+ `${GITHUB_API_BASE}/repos/${repo}/commits?since=${since.toISOString()}&per_page=100`,
682
+ { headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'platform-auditor/1.0' } }
683
+ );
684
+ if (!response.ok) { await response.text(); return []; }
685
+ return (await response.json()) as GitHubCommit[];
686
+ } catch { return []; }
687
+ }
688
+
689
+ async function fetchCommitDetails(
690
+ repo: string, sha: string, token: string, log: Logger
691
+ ): Promise<GitHubCommit | null> {
692
+ try {
693
+ const response = await fetch(`${GITHUB_API_BASE}/repos/${repo}/commits/${sha}`, {
694
+ headers: { Authorization: `Bearer ${token}`, Accept: 'application/vnd.github.v3+json', 'User-Agent': 'platform-auditor/1.0' },
695
+ });
696
+ if (!response.ok) { await response.text(); return null; }
697
+ return (await response.json()) as GitHubCommit;
698
+ } catch { return null; }
699
+ }
700
+
701
+ // =============================================================================
702
+ // UTILITIES
703
+ // =============================================================================
704
+
705
+ function generateAuditId(): string {
706
+ return `audit-${new Date().toISOString().replace(/[:.]/g, '-')}`;
707
+ }
708
+
709
+ async function hashContent(content: string): Promise<string> {
710
+ const encoder = new TextEncoder();
711
+ const data = encoder.encode(content);
712
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
713
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
714
+ return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('').slice(0, 16);
715
+ }
716
+
717
+ function parseJsonc(content: string): WranglerConfig | null {
718
+ try {
719
+ let result = '';
720
+ let i = 0;
721
+ while (i < content.length) {
722
+ if (content[i] === '"') {
723
+ result += content[i++];
724
+ while (i < content.length && content[i] !== '"') {
725
+ if (content[i] === '\\' && i + 1 < content.length) result += content[i++];
726
+ result += content[i++];
727
+ }
728
+ if (i < content.length) result += content[i++];
729
+ } else if (content[i] === '/' && content[i + 1] === '/') {
730
+ while (i < content.length && content[i] !== '\n') i++;
731
+ } else if (content[i] === '/' && content[i + 1] === '*') {
732
+ i += 2;
733
+ while (i < content.length - 1 && !(content[i] === '*' && content[i + 1] === '/')) i++;
734
+ i += 2;
735
+ } else {
736
+ result += content[i++];
737
+ }
738
+ }
739
+ return JSON.parse(result) as WranglerConfig;
740
+ } catch {
741
+ return null;
742
+ }
743
+ }
744
+
745
+ // =============================================================================
746
+ // D1 STORAGE
747
+ // =============================================================================
748
+
749
+ async function storeAuditResult(
750
+ auditId: string,
751
+ result: ProjectAuditResult,
752
+ env: AuditorEnv,
753
+ log: Logger
754
+ ): Promise<void> {
755
+ try {
756
+ await env.PLATFORM_DB.prepare(
757
+ `INSERT INTO audit_results (
758
+ audit_id, project, status, status_message,
759
+ has_platform_cache, has_platform_telemetry, observability_enabled, logs_enabled, config_issues,
760
+ has_sdk_folder, has_with_feature_budget, has_tracked_env, has_circuit_breaker_error, has_error_logging,
761
+ has_recent_heartbeat, hours_since_heartbeat,
762
+ ai_judge_score, ai_judge_summary, ai_judge_issues, ai_cached_at,
763
+ traces_enabled, trace_sampling_rate, log_sampling_rate, invocation_logs_enabled, source_maps_enabled,
764
+ rubric_sdk, rubric_observability, rubric_cost_protection, rubric_security,
765
+ rubric_evidence, ai_reasoning, ai_categorised_issues, ai_validation_retries,
766
+ scan_type, focused_dimensions, composite_score
767
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
768
+ ).bind(
769
+ auditId, result.project, result.status, result.statusMessage,
770
+ result.config.hasPlatformCache ? 1 : 0,
771
+ result.config.hasPlatformTelemetry ? 1 : 0,
772
+ result.config.observabilityEnabled ? 1 : 0,
773
+ result.config.logsEnabled ? 1 : 0,
774
+ JSON.stringify(result.config.issues),
775
+ result.codeSmells.hasSdkFolder ? 1 : 0,
776
+ result.codeSmells.hasWithFeatureBudget ? 1 : 0,
777
+ result.codeSmells.hasTrackedEnv ? 1 : 0,
778
+ result.codeSmells.hasCircuitBreakerError ? 1 : 0,
779
+ result.codeSmells.hasErrorLogging ? 1 : 0,
780
+ result.runtime.hasRecentHeartbeat ? 1 : 0,
781
+ result.runtime.hoursSinceHeartbeat,
782
+ result.aiJudge?.score ?? null,
783
+ result.aiJudge?.summary ?? null,
784
+ result.aiJudge?.issues ?? null,
785
+ result.aiJudge?.cachedAt ?? null,
786
+ result.config.tracesEnabled ? 1 : 0,
787
+ result.config.traceSamplingRate,
788
+ result.config.logSamplingRate,
789
+ result.config.invocationLogsEnabled ? 1 : 0,
790
+ result.config.sourceMapsEnabled ? 1 : 0,
791
+ result.aiJudge?.rubricScores.sdk ?? null,
792
+ result.aiJudge?.rubricScores.observability ?? null,
793
+ result.aiJudge?.rubricScores.costProtection ?? null,
794
+ result.aiJudge?.rubricScores.security ?? null,
795
+ result.aiJudge?.rubricEvidence ?? null,
796
+ result.aiJudge?.reasoning ?? null,
797
+ result.aiJudge?.categorisedIssues ?? null,
798
+ result.aiJudge?.validationRetries ?? 0,
799
+ result.scanType,
800
+ result.focusedDimensions ? JSON.stringify(result.focusedDimensions) : null,
801
+ result.compositeScore
802
+ ).run();
803
+
804
+ log.debug('Stored audit result', { project: result.project, auditId });
805
+ } catch (error) {
806
+ log.error('Failed to store audit result', error, { project: result.project });
807
+ }
808
+ }
809
+
810
+ async function storeBehavioralResults(
811
+ project: string,
812
+ result: BehavioralResult,
813
+ env: AuditorEnv,
814
+ log: Logger
815
+ ): Promise<void> {
816
+ const auditDate = new Date().toISOString().split('T')[0];
817
+ try {
818
+ for (const hotspot of result.hotspots) {
819
+ await env.PLATFORM_DB.prepare(
820
+ `INSERT OR REPLACE INTO audit_file_hotspots (
821
+ project, file_path, change_count, has_sdk_patterns, hotspot_score, audit_date
822
+ ) VALUES (?, ?, ?, ?, ?, ?)`
823
+ ).bind(project, hotspot.filePath, hotspot.changeCount, hotspot.hasSdkPatterns ? 1 : 0, hotspot.hotspotScore, auditDate).run();
824
+ }
825
+ for (const reg of result.regressions) {
826
+ await env.PLATFORM_DB.prepare(
827
+ `INSERT OR IGNORE INTO audit_sdk_regressions (
828
+ project, commit_sha, commit_message, file_path, regression_type, audit_date
829
+ ) VALUES (?, ?, ?, ?, ?, ?)`
830
+ ).bind(project, reg.commitSha, reg.commitMessage, reg.filePath, `${reg.removedPattern}_removed`, auditDate).run();
831
+ }
832
+ } catch (error) {
833
+ log.error('Failed to store behavioral results', error, { project });
834
+ }
835
+ }
836
+
837
+ // =============================================================================
838
+ // ALERT SENDING
839
+ // =============================================================================
840
+
841
+ async function sendAlerts(
842
+ auditId: string,
843
+ results: ProjectAuditResult[],
844
+ env: AuditorEnv,
845
+ log: Logger
846
+ ): Promise<void> {
847
+ const unhealthy = results.filter((r) => r.status !== 'HEALTHY');
848
+ if (unhealthy.length === 0) return;
849
+
850
+ try {
851
+ await env.ALERT_ROUTER.fetch('https://internal/alerts', {
852
+ method: 'POST',
853
+ headers: { 'Content-Type': 'application/json' },
854
+ body: JSON.stringify({
855
+ source: 'platform-auditor',
856
+ auditId,
857
+ alerts: unhealthy.map((r) => ({
858
+ project: r.project,
859
+ status: r.status,
860
+ message: r.statusMessage,
861
+ score: r.compositeScore,
862
+ })),
863
+ }),
864
+ });
865
+ } catch (error) {
866
+ log.warn('Failed to send alerts (non-fatal)', error);
867
+ }
868
+ }
869
+
870
+ // =============================================================================
871
+ // MAIN WORKER
872
+ // =============================================================================
873
+
874
+ export default {
875
+ async scheduled(event: ScheduledEvent, env: AuditorEnv, ctx: ExecutionContext): Promise<void> {
876
+ const log = createLogger({ worker: 'platform-auditor', featureId: FEATURE_AUDITOR });
877
+ const scanType = determineScanType(event.scheduledTime);
878
+ log.info('Audit triggered', { scan_type: scanType });
879
+
880
+ try {
881
+ const trackedEnv = withCronBudget(env, FEATURE_AUDITOR, {
882
+ ctx, cronExpression: '0 0 * * 0,3',
883
+ });
884
+
885
+ const config = await loadAuditConfig(env, log);
886
+ if (Object.keys(config.targets).length === 0) {
887
+ log.warn('No audit targets configured — add targets to config/audit-targets.yaml and sync');
888
+ await completeTracking(trackedEnv);
889
+ return;
890
+ }
891
+
892
+ const auditId = generateAuditId();
893
+ const results: ProjectAuditResult[] = [];
894
+
895
+ for (const [project, target] of Object.entries(config.targets)) {
896
+ let focusedDimensions: AuditDimension[] | null = null;
897
+ if (scanType === 'focused') {
898
+ const { dimensions, previousScore } = await getWeakDimensions(
899
+ project, config.focusedScanThreshold, env, log
900
+ );
901
+ if (previousScore >= config.focusedScanThreshold && dimensions.length === 0) {
902
+ log.info('Skipping project (above threshold)', { project, previousScore });
903
+ continue;
904
+ }
905
+ focusedDimensions = dimensions;
906
+ }
907
+
908
+ try {
909
+ // Layer 1: Config
910
+ const wranglerContent = await fetchGitHubFile(target.repo, target.wranglerPath, env.GITHUB_TOKEN, log);
911
+ const wranglerConfig = wranglerContent ? parseJsonc(wranglerContent) : null;
912
+ const configResult = checkConfig(wranglerConfig);
913
+
914
+ // Layer 2: Code smells
915
+ let entryPointContent = await fetchGitHubFile(target.repo, target.entryPoint, env.GITHUB_TOKEN, log);
916
+ if (!entryPointContent && target.entryPointFallback) {
917
+ entryPointContent = await fetchGitHubFile(target.repo, target.entryPointFallback, env.GITHUB_TOKEN, log);
918
+ }
919
+ const codeResult = checkCodeSmells(entryPointContent, config.sdkPatterns);
920
+
921
+ // Layer 3: Runtime
922
+ const runtimeResult = await checkRuntime(project, config.heartbeatFreshnessHours, env, log);
923
+
924
+ // Layer 4: AI Judge
925
+ let aiJudge: AIJudgeResult | null = null;
926
+ if (env.PLATFORM_AI_GATEWAY_KEY && entryPointContent) {
927
+ const staticChecks = {
928
+ hasPlatformCache: configResult.hasPlatformCache,
929
+ hasPlatformTelemetry: configResult.hasPlatformTelemetry,
930
+ observabilityEnabled: configResult.observabilityEnabled,
931
+ hasFeatureBudget: codeResult.hasWithFeatureBudget,
932
+ };
933
+
934
+ let supplementary = '';
935
+ const dims = scanType === 'full'
936
+ ? (['sdk', 'observability', 'cost', 'security'] as AuditDimension[])
937
+ : (focusedDimensions ?? []);
938
+ if (dims.length > 0) {
939
+ supplementary = await fetchDeepScanFiles(target, dims, env, log);
940
+ }
941
+
942
+ aiJudge = await auditWithGemini(
943
+ project, entryPointContent, wranglerContent, staticChecks, supplementary, config, env, log
944
+ );
945
+ }
946
+
947
+ // Determine status
948
+ const status = determineStatus(configResult, codeResult, runtimeResult);
949
+ const statusMessage = generateStatusMessage(status, configResult, codeResult, runtimeResult);
950
+ const compositeScore = aiJudge?.score ?? 0;
951
+
952
+ // Behavioral analysis
953
+ let behavioral: BehavioralResult | null = null;
954
+ try {
955
+ behavioral = await runBehavioralAnalysis(project, target.repo, config.sdkPatterns, config, env, log);
956
+ await storeBehavioralResults(project, behavioral, env, log);
957
+ } catch (error) {
958
+ log.warn('Behavioral analysis failed (non-fatal)', error, { project });
959
+ }
960
+
961
+ const auditResult: ProjectAuditResult = {
962
+ project, auditId, status, statusMessage, scanType,
963
+ focusedDimensions, config: configResult, codeSmells: codeResult,
964
+ runtime: runtimeResult, aiJudge: aiJudge, behavioral,
965
+ compositeScore, timestamp: new Date().toISOString(),
966
+ };
967
+
968
+ results.push(auditResult);
969
+ await storeAuditResult(auditId, auditResult, env, log);
970
+ } catch (error) {
971
+ log.error('Project audit failed', error, { project });
972
+ }
973
+ }
974
+
975
+ // Store latest in KV
976
+ await env.PLATFORM_CACHE.put('AUDIT:LATEST', JSON.stringify({
977
+ auditId, results, scanType, timestamp: new Date().toISOString(),
978
+ }), { expirationTtl: 30 * 24 * 60 * 60 });
979
+
980
+ // Feature coverage + comprehensive report (full scans only)
981
+ if (scanType === 'full') {
982
+ let featureCoverage: FeatureCoverageReport | null = null;
983
+ try {
984
+ featureCoverage = await runFeatureCoverageAudit(env, log);
985
+ await storeFeatureCoverageAudit(env, featureCoverage, log);
986
+ } catch (error) { log.warn('Feature coverage audit failed (non-fatal)', error); }
987
+
988
+ try {
989
+ const report = await generateComprehensiveReport(env, featureCoverage, log);
990
+ await storeComprehensiveReport(env, report, log);
991
+ } catch (error) { log.warn('Comprehensive report failed (non-fatal)', error); }
992
+ }
993
+
994
+ await sendAlerts(auditId, results, env, log);
995
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
996
+ await health(FEATURE_HEARTBEAT, env.PLATFORM_CACHE as any, env.PLATFORM_TELEMETRY, ctx);
997
+ await completeTracking(trackedEnv);
998
+ pingHeartbeat(ctx, env.GATUS_HEARTBEAT_URL, env.GATUS_TOKEN, true);
999
+
1000
+ log.info('Audit complete', {
1001
+ auditId, scanType, totalProjects: results.length,
1002
+ healthy: results.filter((r) => r.status === 'HEALTHY').length,
1003
+ });
1004
+ } catch (error) {
1005
+ if (error instanceof CircuitBreakerError) {
1006
+ log.warn('Circuit breaker STOP', error);
1007
+ return;
1008
+ }
1009
+ pingHeartbeat(ctx, env.GATUS_HEARTBEAT_URL, env.GATUS_TOKEN, false);
1010
+ log.error('Audit failed', error);
1011
+ }
1012
+ },
1013
+
1014
+ async fetch(request: Request, env: AuditorEnv, ctx: ExecutionContext): Promise<Response> {
1015
+ const url = new URL(request.url);
1016
+
1017
+ if (url.pathname === '/health') {
1018
+ return Response.json({ status: 'ok', service: 'platform-auditor', timestamp: new Date().toISOString() });
1019
+ }
1020
+
1021
+ const traceContext = createTraceContext(request, env);
1022
+ const log = createLoggerFromRequest(request, env, 'platform-auditor', FEATURE_AUDITOR);
1023
+
1024
+ try {
1025
+ const trackedEnv = withFeatureBudget(env, FEATURE_AUDITOR, { ctx });
1026
+
1027
+ if (url.pathname === '/audit' && request.method === 'GET') {
1028
+ const requestedType = url.searchParams.get('type') || 'full';
1029
+ const fakeDay = requestedType === 'focused' ? 3 : 0;
1030
+ const fakeDate = new Date();
1031
+ fakeDate.setUTCDate(fakeDate.getUTCDate() + ((fakeDay - fakeDate.getUTCDay() + 7) % 7));
1032
+ await this.scheduled({ scheduledTime: fakeDate.getTime(), cron: '0 0 * * 0,3', noRetry: () => {} } as unknown as ScheduledEvent, env, ctx);
1033
+ await completeTracking(trackedEnv);
1034
+ return Response.json({ status: 'audit_triggered', scan_type: requestedType });
1035
+ }
1036
+
1037
+ if (url.pathname === '/audit/latest') {
1038
+ const latest = await env.PLATFORM_CACHE.get('AUDIT:LATEST');
1039
+ await completeTracking(trackedEnv);
1040
+ if (!latest) return Response.json({ error: 'No audit results found' }, { status: 404 });
1041
+ return Response.json(JSON.parse(latest));
1042
+ }
1043
+
1044
+ if (url.pathname.startsWith('/audit/latest/')) {
1045
+ const project = url.pathname.split('/').pop();
1046
+ const latest = await env.PLATFORM_CACHE.get('AUDIT:LATEST');
1047
+ await completeTracking(trackedEnv);
1048
+ if (!latest) return Response.json({ error: 'No audit results' }, { status: 404 });
1049
+ const parsed = JSON.parse(latest) as { auditId: string; results: ProjectAuditResult[] };
1050
+ const result = parsed.results.find((r) => r.project === project);
1051
+ if (!result) return Response.json({ error: `Project ${project} not found` }, { status: 404 });
1052
+ return Response.json({ auditId: parsed.auditId, result });
1053
+ }
1054
+
1055
+ await completeTracking(trackedEnv);
1056
+ return Response.json({
1057
+ service: 'platform-auditor',
1058
+ endpoints: [
1059
+ 'GET /health', 'GET /audit?type=full|focused',
1060
+ 'GET /audit/latest', 'GET /audit/latest/:project',
1061
+ ],
1062
+ });
1063
+ } catch (error) {
1064
+ if (error instanceof CircuitBreakerError) {
1065
+ return Response.json({ error: 'Service temporarily unavailable', code: 'CIRCUIT_BREAKER' }, { status: 503, headers: { 'Retry-After': '60' } });
1066
+ }
1067
+ log.error('Request failed', error, { path: url.pathname, traceId: traceContext.traceId });
1068
+ return Response.json({ error: 'Internal server error', traceId: traceContext.traceId }, { status: 500 });
1069
+ }
1070
+ },
1071
+ };