@sansavision/aurora 0.1.0-alpha.20260212.4

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 (150) hide show
  1. package/README.md +4 -0
  2. package/package.json +17 -0
  3. package/src/ai-diagnostics.ts +156 -0
  4. package/src/ai.ts +574 -0
  5. package/src/analyze.ts +669 -0
  6. package/src/bin/aurora.ts +15 -0
  7. package/src/build.ts +431 -0
  8. package/src/bun-test-shims.d.ts +17 -0
  9. package/src/create-feature.ts +419 -0
  10. package/src/create-route.ts +581 -0
  11. package/src/create.ts +425 -0
  12. package/src/dev.ts +126 -0
  13. package/src/devtools.ts +1143 -0
  14. package/src/doctor.ts +611 -0
  15. package/src/explain.ts +855 -0
  16. package/src/help.ts +39 -0
  17. package/src/index.ts +34 -0
  18. package/src/init.ts +1011 -0
  19. package/src/inspect-cache.ts +464 -0
  20. package/src/lsp-inline-hints.ts +254 -0
  21. package/src/node-shims.d.ts +26 -0
  22. package/src/process.d.ts +11 -0
  23. package/src/query-profiler.ts +520 -0
  24. package/src/realtime-monitor.ts +389 -0
  25. package/src/registry.ts +303 -0
  26. package/src/run.ts +37 -0
  27. package/src/start.ts +56 -0
  28. package/src/test.ts +289 -0
  29. package/templates/basic/README.md +16 -0
  30. package/templates/basic/package.json +10 -0
  31. package/templates/basic/src/actions/createMessage.action.server.ts +22 -0
  32. package/templates/basic/src/lib/auth.server.ts +11 -0
  33. package/templates/basic/src/queries/listMessages.server.ts +17 -0
  34. package/templates/basic/src/routes/index.tsx +12 -0
  35. package/templates/blog/README.md +17 -0
  36. package/templates/blog/package.json +12 -0
  37. package/templates/blog/public/assets/og-default.svg +17 -0
  38. package/templates/blog/src/content/loadPosts.server.ts +22 -0
  39. package/templates/blog/src/content/posts/hello-world.md +11 -0
  40. package/templates/blog/src/content/posts/release-notes.md +9 -0
  41. package/templates/blog/src/routes/index.tsx +22 -0
  42. package/templates/blog/src/routes/posts/[slug].tsx +19 -0
  43. package/templates/blog/src/seo/meta.ts +19 -0
  44. package/templates/dashboard/README.md +18 -0
  45. package/templates/dashboard/package.json +10 -0
  46. package/templates/dashboard/src/actions/acknowledgeAlert.action.server.ts +6 -0
  47. package/templates/dashboard/src/queries/getDashboardMetrics.server.ts +30 -0
  48. package/templates/dashboard/src/realtime/useDashboardRealtime.client.ts +13 -0
  49. package/templates/dashboard/src/routes/index.tsx +19 -0
  50. package/templates/dashboard/src/widgets/DataGrid.client.ts +8 -0
  51. package/templates/dashboard/src/widgets/MetricChart.client.ts +8 -0
  52. package/templates/desktop/README.md +18 -0
  53. package/templates/desktop/package.json +11 -0
  54. package/templates/desktop/src/actions/saveDesktopPreference.action.server.ts +28 -0
  55. package/templates/desktop/src/desktop/secureStorage.client.ts +20 -0
  56. package/templates/desktop/src/desktop/tauriBridge.client.ts +14 -0
  57. package/templates/desktop/src/queries/getDesktopSyncStatus.server.ts +9 -0
  58. package/templates/desktop/src/routes/index.tsx +27 -0
  59. package/templates/desktop/src/sync/offlineSyncBoundary.server.ts +27 -0
  60. package/templates/feature-skeleton/README.md +13 -0
  61. package/templates/feature-skeleton/actions/createFeature.action.server.ts +19 -0
  62. package/templates/feature-skeleton/index.ts +8 -0
  63. package/templates/feature-skeleton/queries/listFeature.server.ts +15 -0
  64. package/templates/feature-skeleton/realtime/useFeatureRealtime.client.ts +16 -0
  65. package/templates/feature-skeleton/template.manifest.json +15 -0
  66. package/templates/feature-skeleton/ui/FeatureView.client.tsx +14 -0
  67. package/templates/mobile/README.md +17 -0
  68. package/templates/mobile/package.json +11 -0
  69. package/templates/mobile/src/mobile/auth/session-handoff.client.ts +69 -0
  70. package/templates/mobile/src/mobile/generated/mobile-api-sdk.ts +62 -0
  71. package/templates/mobile/src/mobile/transport/mobile-api-transport.client.ts +122 -0
  72. package/templates/mobile/src/routes/index.tsx +134 -0
  73. package/templates/monorepo/README.md +18 -0
  74. package/templates/monorepo/apps/web/package.json +9 -0
  75. package/templates/monorepo/apps/web/src/routes/index.tsx +1 -0
  76. package/templates/monorepo/package.json +13 -0
  77. package/templates/monorepo/packages/shared/README.md +3 -0
  78. package/templates/monorepo/packages/ui/README.md +3 -0
  79. package/templates/saas/README.md +17 -0
  80. package/templates/saas/package.json +10 -0
  81. package/templates/saas/src/admin/getDashboard.server.ts +18 -0
  82. package/templates/saas/src/auth/session.server.ts +13 -0
  83. package/templates/saas/src/billing/checkout.server.ts +11 -0
  84. package/templates/saas/src/email/sendWelcome.server.ts +8 -0
  85. package/templates/saas/src/realtime/notifications.server.ts +8 -0
  86. package/templates/saas/src/routes/index.tsx +20 -0
  87. package/test/ai.test.ts +94 -0
  88. package/test/analyze.test.ts +301 -0
  89. package/test/build.test.ts +135 -0
  90. package/test/create-feature.test.ts +145 -0
  91. package/test/create-route.test.ts +117 -0
  92. package/test/create.test.ts +222 -0
  93. package/test/dev.test.ts +52 -0
  94. package/test/devtools.test.ts +130 -0
  95. package/test/doctor.test.ts +129 -0
  96. package/test/explain.test.ts +232 -0
  97. package/test/feature-skeleton.test.ts +53 -0
  98. package/test/fixtures/analyze/cache-input.invalid.json +1 -0
  99. package/test/fixtures/analyze/cache-input.missing-keyhash.v1.json +10 -0
  100. package/test/fixtures/analyze/cache-input.unsupported-version.v2.json +10 -0
  101. package/test/fixtures/analyze/cache-input.v1.json +12 -0
  102. package/test/fixtures/analyze/compiler-manifest/manifest.json +11 -0
  103. package/test/fixtures/analyze/guardrails-input.unsupported-version.v2.json +4 -0
  104. package/test/fixtures/analyze/guardrails-input.v1.json +49 -0
  105. package/test/fixtures/analyze/query-input.invalid-cache-status.v1.json +11 -0
  106. package/test/fixtures/analyze/query-input.unsupported-version.v2.json +11 -0
  107. package/test/fixtures/analyze/query-input.v1.json +18 -0
  108. package/test/fixtures/analyze/realtime-input.missing-lag-p95.v1.json +10 -0
  109. package/test/fixtures/analyze/realtime-input.unsupported-version.v2.json +8 -0
  110. package/test/fixtures/analyze/realtime-input.v1.json +12 -0
  111. package/test/fixtures/cache-inspector/cache-input.v1.json +23 -0
  112. package/test/fixtures/cache-inspector/invalid.json +1 -0
  113. package/test/fixtures/cache-inspector/snapshot.v1.json +34 -0
  114. package/test/fixtures/cache-inspector/unsupported-version.v2.json +13 -0
  115. package/test/fixtures/devtools/healthy.v1.json +130 -0
  116. package/test/fixtures/devtools/invalid.json +1 -0
  117. package/test/fixtures/devtools/unsupported-version.v2.json +8 -0
  118. package/test/fixtures/devtools/warn.v1.json +114 -0
  119. package/test/fixtures/doctor/clean/src/page.tsx +3 -0
  120. package/test/fixtures/doctor/findings/src/accessibility.client.tsx +7 -0
  121. package/test/fixtures/doctor/findings/src/migration.config.ts +3 -0
  122. package/test/fixtures/doctor/findings/src/page.client.tsx +5 -0
  123. package/test/fixtures/doctor/findings/src/perf.server.ts +15 -0
  124. package/test/fixtures/doctor/findings/src/routes.js +3 -0
  125. package/test/fixtures/doctor/findings/src/security.server.ts +7 -0
  126. package/test/fixtures/doctor/findings/src/users.server.ts +3 -0
  127. package/test/fixtures/doctor/governance/src/features/analytics/OWNERS.ts +2 -0
  128. package/test/fixtures/doctor/governance/src/features/analytics/page.tsx +3 -0
  129. package/test/fixtures/doctor/governance/src/features/billing/page.tsx +3 -0
  130. package/test/fixtures/explain/invalid.json +1 -0
  131. package/test/fixtures/explain/module-report.unsupported-version.v2.json +6 -0
  132. package/test/fixtures/explain/module-report.v1.json +72 -0
  133. package/test/fixtures/query-profiler/healthy.v1.json +11 -0
  134. package/test/fixtures/query-profiler/invalid.json +1 -0
  135. package/test/fixtures/query-profiler/unsupported-version.v2.json +6 -0
  136. package/test/fixtures/query-profiler/warning.v1.json +10 -0
  137. package/test/fixtures/realtime-monitor/healthy.v1.json +8 -0
  138. package/test/fixtures/realtime-monitor/invalid.json +1 -0
  139. package/test/fixtures/realtime-monitor/unsupported-version.v2.json +8 -0
  140. package/test/fixtures/realtime-monitor/warning.v1.json +8 -0
  141. package/test/help-parity.test.ts +104 -0
  142. package/test/init.test.ts +164 -0
  143. package/test/inspect-cache.test.ts +112 -0
  144. package/test/lsp-inline-hints.test.ts +65 -0
  145. package/test/query-profiler.test.ts +123 -0
  146. package/test/realtime-monitor.test.ts +115 -0
  147. package/test/registry.test.ts +41 -0
  148. package/test/start.test.ts +23 -0
  149. package/test/test-command.test.ts +65 -0
  150. package/tsconfig.json +19 -0
@@ -0,0 +1,26 @@
1
+ declare module "node:fs" {
2
+ export interface Dirent {
3
+ name: string;
4
+ isDirectory(): boolean;
5
+ isFile(): boolean;
6
+ }
7
+
8
+ export interface Stats {
9
+ isDirectory(): boolean;
10
+ }
11
+
12
+ export function readFileSync(path: string, encoding: "utf8"): string;
13
+ export function writeFileSync(path: string, data: string, encoding: "utf8"): void;
14
+ export function existsSync(path: string): boolean;
15
+ export function mkdirSync(path: string, options?: { recursive?: boolean }): void;
16
+ export function readdirSync(path: string): string[];
17
+ export function readdirSync(path: string, options: { withFileTypes: true }): Dirent[];
18
+ export function statSync(path: string): Stats;
19
+ }
20
+
21
+ declare module "node:path" {
22
+ export function dirname(path: string): string;
23
+ export function join(...paths: string[]): string;
24
+ export function resolve(...paths: string[]): string;
25
+ export function relative(from: string, to: string): string;
26
+ }
@@ -0,0 +1,11 @@
1
+ declare const process: {
2
+ argv: string[];
3
+ cwd: () => string;
4
+ stdout: {
5
+ write: (chunk: string) => void;
6
+ };
7
+ stderr: {
8
+ write: (chunk: string) => void;
9
+ };
10
+ exitCode: number;
11
+ };
@@ -0,0 +1,520 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import { type CommandContext, type CommandResult } from "./registry";
5
+
6
+ type QueryProfilerFormat = "text" | "json";
7
+ type QueryProfilerStatus = "ok" | "warn";
8
+ type QueryCacheStatus = "hit" | "miss" | "stale";
9
+
10
+ interface QueryProfilerOptions {
11
+ inputPath: string;
12
+ format: QueryProfilerFormat;
13
+ maxP95Ms: number;
14
+ minCacheHitRatio: number;
15
+ limit: number;
16
+ }
17
+
18
+ interface QuerySample {
19
+ queryName: string;
20
+ keyHash: string;
21
+ durationMs: number;
22
+ cacheStatus: QueryCacheStatus;
23
+ }
24
+
25
+ interface QueryProfilerIssue {
26
+ code: "QUERY_P95_EXCEEDED" | "CACHE_HIT_RATIO_BELOW_MIN";
27
+ message: string;
28
+ }
29
+
30
+ interface SlowQueryEntry {
31
+ queryName: string;
32
+ samples: number;
33
+ avgMs: number;
34
+ p95Ms: number;
35
+ cacheHitRatio: number;
36
+ }
37
+
38
+ interface QueryProfilerReport {
39
+ mode: "query-profiler";
40
+ projectRoot: string;
41
+ inputPath: string;
42
+ status: QueryProfilerStatus;
43
+ samples: number;
44
+ avgMs: number;
45
+ p95Ms: number;
46
+ cacheHits: number;
47
+ cacheMisses: number;
48
+ cacheStale: number;
49
+ cacheHitRatio: number;
50
+ thresholds: {
51
+ maxP95Ms: number;
52
+ minCacheHitRatio: number;
53
+ };
54
+ issues: QueryProfilerIssue[];
55
+ slowQueries: SlowQueryEntry[];
56
+ }
57
+
58
+ const DEFAULT_MAX_P95_MS = 80;
59
+ const DEFAULT_MIN_CACHE_HIT_RATIO = 0.6;
60
+ const DEFAULT_LIMIT = 10;
61
+ const MAX_LIMIT = 200;
62
+
63
+ function parseNonNegativeNumber(raw: string | undefined, flag: string): number | CommandResult {
64
+ if (!raw) {
65
+ return {
66
+ exitCode: 2,
67
+ stderr: `aurora profile-query: ${flag} requires a numeric value`,
68
+ };
69
+ }
70
+
71
+ const parsed = Number(raw);
72
+ if (!Number.isFinite(parsed) || parsed < 0) {
73
+ return {
74
+ exitCode: 2,
75
+ stderr: `aurora profile-query: ${flag} must be a non-negative number`,
76
+ };
77
+ }
78
+
79
+ return parsed;
80
+ }
81
+
82
+ function parseRatio(raw: string | undefined, flag: string): number | CommandResult {
83
+ const parsed = parseNonNegativeNumber(raw, flag);
84
+ if (typeof parsed !== "number") {
85
+ return parsed;
86
+ }
87
+
88
+ if (parsed > 1) {
89
+ return {
90
+ exitCode: 2,
91
+ stderr: `aurora profile-query: ${flag} must be between 0 and 1`,
92
+ };
93
+ }
94
+
95
+ return parsed;
96
+ }
97
+
98
+ function parseLimit(raw: string | undefined): number | CommandResult {
99
+ if (!raw) {
100
+ return {
101
+ exitCode: 2,
102
+ stderr: "aurora profile-query: --limit requires an integer value",
103
+ };
104
+ }
105
+
106
+ const parsed = Number(raw);
107
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_LIMIT) {
108
+ return {
109
+ exitCode: 2,
110
+ stderr: `aurora profile-query: --limit must be an integer between 1 and ${MAX_LIMIT}`,
111
+ };
112
+ }
113
+
114
+ return parsed;
115
+ }
116
+
117
+ function parseQueryProfilerOptions(args: ReadonlyArray<string>): QueryProfilerOptions | CommandResult {
118
+ const options: QueryProfilerOptions = {
119
+ inputPath: "",
120
+ format: "text",
121
+ maxP95Ms: DEFAULT_MAX_P95_MS,
122
+ minCacheHitRatio: DEFAULT_MIN_CACHE_HIT_RATIO,
123
+ limit: DEFAULT_LIMIT,
124
+ };
125
+
126
+ for (let i = 0; i < args.length; i += 1) {
127
+ const arg = args[i];
128
+ if (arg === "--input") {
129
+ const value = args[i + 1];
130
+ if (!value) {
131
+ return {
132
+ exitCode: 2,
133
+ stderr: "aurora profile-query: --input requires a path value",
134
+ };
135
+ }
136
+
137
+ options.inputPath = value;
138
+ i += 1;
139
+ continue;
140
+ }
141
+
142
+ if (arg === "--format") {
143
+ const value = args[i + 1];
144
+ if (!value) {
145
+ return {
146
+ exitCode: 2,
147
+ stderr: "aurora profile-query: --format requires 'text' or 'json'",
148
+ };
149
+ }
150
+
151
+ if (value !== "text" && value !== "json") {
152
+ return {
153
+ exitCode: 2,
154
+ stderr: `aurora profile-query: invalid format '${value}'. Expected 'text' or 'json'`,
155
+ };
156
+ }
157
+
158
+ options.format = value;
159
+ i += 1;
160
+ continue;
161
+ }
162
+
163
+ if (arg === "--max-p95-ms") {
164
+ const parsed = parseNonNegativeNumber(args[i + 1], "--max-p95-ms");
165
+ if (typeof parsed !== "number") {
166
+ return parsed;
167
+ }
168
+
169
+ options.maxP95Ms = parsed;
170
+ i += 1;
171
+ continue;
172
+ }
173
+
174
+ if (arg === "--min-cache-hit-ratio") {
175
+ const parsed = parseRatio(args[i + 1], "--min-cache-hit-ratio");
176
+ if (typeof parsed !== "number") {
177
+ return parsed;
178
+ }
179
+
180
+ options.minCacheHitRatio = parsed;
181
+ i += 1;
182
+ continue;
183
+ }
184
+
185
+ if (arg === "--limit") {
186
+ const parsed = parseLimit(args[i + 1]);
187
+ if (typeof parsed !== "number") {
188
+ return parsed;
189
+ }
190
+
191
+ options.limit = parsed;
192
+ i += 1;
193
+ continue;
194
+ }
195
+
196
+ return {
197
+ exitCode: 2,
198
+ stderr: `aurora profile-query: unknown option '${arg}'`,
199
+ };
200
+ }
201
+
202
+ if (!options.inputPath) {
203
+ return {
204
+ exitCode: 2,
205
+ stderr: "aurora profile-query: --input is required",
206
+ };
207
+ }
208
+
209
+ return options;
210
+ }
211
+
212
+ function toRecord(value: unknown): Record<string, unknown> | undefined {
213
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
214
+ return undefined;
215
+ }
216
+
217
+ return value as Record<string, unknown>;
218
+ }
219
+
220
+ function isCommandResult(value: unknown): value is CommandResult {
221
+ const source = toRecord(value);
222
+ return Boolean(source && typeof source.exitCode === "number");
223
+ }
224
+
225
+ function getRequiredString(source: Record<string, unknown>, fields: ReadonlyArray<string>): string {
226
+ for (const field of fields) {
227
+ const value = source[field];
228
+ if (typeof value === "string" && value.trim().length > 0) {
229
+ return value.trim();
230
+ }
231
+ }
232
+
233
+ throw new Error(`missing required string field (${fields.join(" or ")})`);
234
+ }
235
+
236
+ function getRequiredNumber(source: Record<string, unknown>, fields: ReadonlyArray<string>): number {
237
+ for (const field of fields) {
238
+ const value = source[field];
239
+ if (typeof value === "number" && Number.isFinite(value)) {
240
+ return value;
241
+ }
242
+ }
243
+
244
+ throw new Error(`missing required numeric field (${fields.join(" or ")})`);
245
+ }
246
+
247
+ function normalizeCacheStatus(rawStatus: unknown, rawHitValue: unknown): QueryCacheStatus {
248
+ if (typeof rawStatus === "string") {
249
+ if (rawStatus === "hit" || rawStatus === "miss" || rawStatus === "stale") {
250
+ return rawStatus;
251
+ }
252
+ }
253
+
254
+ if (typeof rawHitValue === "boolean") {
255
+ return rawHitValue ? "hit" : "miss";
256
+ }
257
+
258
+ throw new Error("missing required cache status field (cacheStatus/cache/status or hit)");
259
+ }
260
+
261
+ function readInputJson(inputPath: string, context: CommandContext): unknown | CommandResult {
262
+ const absolutePath = resolve(context.cwd, inputPath);
263
+ let raw = "";
264
+
265
+ try {
266
+ raw = readFileSync(absolutePath, "utf8");
267
+ } catch {
268
+ return {
269
+ exitCode: 1,
270
+ stderr: `aurora profile-query: unable to read input at ${inputPath}`,
271
+ };
272
+ }
273
+
274
+ try {
275
+ return JSON.parse(raw);
276
+ } catch {
277
+ return {
278
+ exitCode: 1,
279
+ stderr: `aurora profile-query: invalid json input at ${inputPath}`,
280
+ };
281
+ }
282
+ }
283
+
284
+ function assertSupportedSchemaVersion(payload: unknown): void {
285
+ const source = toRecord(payload);
286
+ if (!source) {
287
+ return;
288
+ }
289
+
290
+ const rawSchemaVersion = source.schemaVersion;
291
+ if (rawSchemaVersion === undefined) {
292
+ return;
293
+ }
294
+
295
+ if (rawSchemaVersion !== 1) {
296
+ throw new Error(`unsupported schemaVersion ${String(rawSchemaVersion)} for query input; expected 1`);
297
+ }
298
+ }
299
+
300
+ function normalizeSamples(payload: unknown): unknown[] {
301
+ if (Array.isArray(payload)) {
302
+ return payload;
303
+ }
304
+
305
+ const source = toRecord(payload);
306
+ if (source && Array.isArray(source.samples)) {
307
+ return source.samples;
308
+ }
309
+
310
+ throw new Error("query profiler input must be an array or an object with samples[]");
311
+ }
312
+
313
+ function percentile(values: ReadonlyArray<number>, ratio: number): number {
314
+ if (values.length === 0) {
315
+ return 0;
316
+ }
317
+
318
+ const sorted = [...values].sort((left, right) => left - right);
319
+ const index = Math.ceil(sorted.length * ratio) - 1;
320
+ const bounded = Math.min(Math.max(index, 0), sorted.length - 1);
321
+ return sorted[bounded] ?? 0;
322
+ }
323
+
324
+ function average(values: ReadonlyArray<number>): number {
325
+ if (values.length === 0) {
326
+ return 0;
327
+ }
328
+
329
+ const sum = values.reduce((total, value) => total + value, 0);
330
+ return sum / values.length;
331
+ }
332
+
333
+ function buildQueryProfilerReport(
334
+ options: QueryProfilerOptions,
335
+ context: CommandContext,
336
+ ): QueryProfilerReport | CommandResult {
337
+ const payload = readInputJson(options.inputPath, context);
338
+ if (isCommandResult(payload)) {
339
+ return payload;
340
+ }
341
+
342
+ let normalizedSamples: QuerySample[] = [];
343
+ try {
344
+ assertSupportedSchemaVersion(payload);
345
+ const rawSamples = normalizeSamples(payload);
346
+ normalizedSamples = rawSamples.map((sample) => {
347
+ const source = toRecord(sample);
348
+ if (!source) {
349
+ throw new Error("query sample must be an object");
350
+ }
351
+
352
+ return {
353
+ queryName: getRequiredString(source, ["queryName", "name", "query"]),
354
+ keyHash: getRequiredString(source, ["keyHash", "key", "hash"]),
355
+ durationMs: getRequiredNumber(source, ["durationMs", "duration", "ms"]),
356
+ cacheStatus: normalizeCacheStatus(source.cacheStatus ?? source.cache ?? source.status, source.hit),
357
+ };
358
+ });
359
+ } catch (error) {
360
+ const reason = error instanceof Error ? error.message : String(error);
361
+ return {
362
+ exitCode: 1,
363
+ stderr: `aurora profile-query: ${reason}`,
364
+ };
365
+ }
366
+
367
+ const durations = normalizedSamples.map((sample) => sample.durationMs);
368
+ const p95Ms = percentile(durations, 0.95);
369
+ const avgMs = average(durations);
370
+ const cacheHits = normalizedSamples.filter((sample) => sample.cacheStatus === "hit").length;
371
+ const cacheMisses = normalizedSamples.filter((sample) => sample.cacheStatus === "miss").length;
372
+ const cacheStale = normalizedSamples.filter((sample) => sample.cacheStatus === "stale").length;
373
+ const cacheHitRatio = normalizedSamples.length === 0 ? 1 : cacheHits / normalizedSamples.length;
374
+
375
+ const perQuery = new Map<
376
+ string,
377
+ {
378
+ durations: number[];
379
+ hits: number;
380
+ samples: number;
381
+ }
382
+ >();
383
+
384
+ for (const sample of normalizedSamples) {
385
+ const current = perQuery.get(sample.queryName) ?? {
386
+ durations: [],
387
+ hits: 0,
388
+ samples: 0,
389
+ };
390
+ current.durations.push(sample.durationMs);
391
+ current.samples += 1;
392
+ if (sample.cacheStatus === "hit") {
393
+ current.hits += 1;
394
+ }
395
+ perQuery.set(sample.queryName, current);
396
+ }
397
+
398
+ const slowQueries: SlowQueryEntry[] = [...perQuery.entries()]
399
+ .map(([queryName, summary]) => ({
400
+ queryName,
401
+ samples: summary.samples,
402
+ avgMs: average(summary.durations),
403
+ p95Ms: percentile(summary.durations, 0.95),
404
+ cacheHitRatio: summary.samples === 0 ? 1 : summary.hits / summary.samples,
405
+ }))
406
+ .sort((left, right) => {
407
+ if (right.p95Ms !== left.p95Ms) {
408
+ return right.p95Ms - left.p95Ms;
409
+ }
410
+ if (right.avgMs !== left.avgMs) {
411
+ return right.avgMs - left.avgMs;
412
+ }
413
+ return left.queryName.localeCompare(right.queryName);
414
+ })
415
+ .slice(0, options.limit);
416
+
417
+ const issues: QueryProfilerIssue[] = [];
418
+ if (p95Ms > options.maxP95Ms) {
419
+ issues.push({
420
+ code: "QUERY_P95_EXCEEDED",
421
+ message: `p95 ${p95Ms.toFixed(2)}ms exceeds threshold ${options.maxP95Ms.toFixed(2)}ms`,
422
+ });
423
+ }
424
+
425
+ if (cacheHitRatio < options.minCacheHitRatio) {
426
+ issues.push({
427
+ code: "CACHE_HIT_RATIO_BELOW_MIN",
428
+ message:
429
+ `cache hit ratio ${cacheHitRatio.toFixed(4)} is below threshold ` +
430
+ `${options.minCacheHitRatio.toFixed(4)}`,
431
+ });
432
+ }
433
+
434
+ const status: QueryProfilerStatus = issues.length > 0 ? "warn" : "ok";
435
+
436
+ return {
437
+ mode: "query-profiler",
438
+ projectRoot: context.cwd,
439
+ inputPath: options.inputPath,
440
+ status,
441
+ samples: normalizedSamples.length,
442
+ avgMs,
443
+ p95Ms,
444
+ cacheHits,
445
+ cacheMisses,
446
+ cacheStale,
447
+ cacheHitRatio,
448
+ thresholds: {
449
+ maxP95Ms: options.maxP95Ms,
450
+ minCacheHitRatio: options.minCacheHitRatio,
451
+ },
452
+ issues,
453
+ slowQueries,
454
+ };
455
+ }
456
+
457
+ function renderQueryProfilerText(report: QueryProfilerReport): string {
458
+ const lines = [
459
+ "aurora query profiler report",
460
+ `project_root: ${report.projectRoot}`,
461
+ `input_path: ${report.inputPath}`,
462
+ `status: ${report.status}`,
463
+ `samples: ${report.samples}`,
464
+ `avg_ms: ${report.avgMs.toFixed(2)}`,
465
+ `p95_ms: ${report.p95Ms.toFixed(2)}`,
466
+ `cache_hit_ratio: ${report.cacheHitRatio.toFixed(4)}`,
467
+ `cache_hits: ${report.cacheHits}`,
468
+ `cache_misses: ${report.cacheMisses}`,
469
+ `cache_stale: ${report.cacheStale}`,
470
+ `max_p95_ms: ${report.thresholds.maxP95Ms.toFixed(2)}`,
471
+ `min_cache_hit_ratio: ${report.thresholds.minCacheHitRatio.toFixed(4)}`,
472
+ ];
473
+
474
+ if (report.issues.length > 0) {
475
+ lines.push("issues:");
476
+ for (const issue of report.issues) {
477
+ lines.push(`- [${issue.code}] ${issue.message}`);
478
+ }
479
+ }
480
+
481
+ if (report.slowQueries.length > 0) {
482
+ lines.push("slow_queries:");
483
+ for (const entry of report.slowQueries) {
484
+ lines.push(
485
+ `- ${entry.queryName} samples=${entry.samples} avg_ms=${entry.avgMs.toFixed(2)} ` +
486
+ `p95_ms=${entry.p95Ms.toFixed(2)} cache_hit_ratio=${entry.cacheHitRatio.toFixed(4)}`,
487
+ );
488
+ }
489
+ }
490
+
491
+ return lines.join("\n");
492
+ }
493
+
494
+ export function runQueryProfilerCommand(
495
+ args: ReadonlyArray<string>,
496
+ context: CommandContext,
497
+ ): CommandResult {
498
+ const options = parseQueryProfilerOptions(args);
499
+ if ("exitCode" in options) {
500
+ return options;
501
+ }
502
+
503
+ const report = buildQueryProfilerReport(options, context);
504
+ if ("exitCode" in report) {
505
+ return report;
506
+ }
507
+
508
+ const exitCode = report.status === "warn" ? 1 : 0;
509
+ if (options.format === "json") {
510
+ return {
511
+ exitCode,
512
+ stdout: JSON.stringify(report, null, 2),
513
+ };
514
+ }
515
+
516
+ return {
517
+ exitCode,
518
+ stdout: renderQueryProfilerText(report),
519
+ };
520
+ }