@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
package/src/explain.ts ADDED
@@ -0,0 +1,855 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import {
5
+ createInlineHintsFromExplainReport,
6
+ type AuroraInlineHint,
7
+ } from "./lsp-inline-hints";
8
+ import { type CommandContext, type CommandResult } from "./registry";
9
+
10
+ type ExplainFormat = "text" | "json";
11
+ export type ExplainSource = "explicit" | "inferred" | "inherited" | "default" | "none";
12
+ type JsonPrimitive = string | number | boolean | null;
13
+ export type JsonValue = JsonPrimitive | readonly JsonValue[] | { readonly [key: string]: JsonValue };
14
+
15
+ export interface ExplainField<TValue> {
16
+ value: TValue;
17
+ source: ExplainSource;
18
+ note?: string;
19
+ }
20
+
21
+ interface ExplainCacheValue {
22
+ staleTime?: string;
23
+ gcTime?: string;
24
+ }
25
+
26
+ export interface ExplainQueryEntry {
27
+ kind: "query";
28
+ name: string;
29
+ key: ExplainField<readonly JsonValue[] | string>;
30
+ tags: ExplainField<readonly string[]>;
31
+ cache?: ExplainField<ExplainCacheValue | string>;
32
+ realtime?: ExplainField<string>;
33
+ auth?: ExplainField<string>;
34
+ mask?: ExplainField<string>;
35
+ estimatedPayloadBytes?: number;
36
+ suggestions: readonly string[];
37
+ }
38
+
39
+ export interface ExplainActionEntry {
40
+ kind: "action";
41
+ name: string;
42
+ invalidates: ExplainField<readonly string[]>;
43
+ publishes?: ExplainField<readonly string[]>;
44
+ auth?: ExplainField<string>;
45
+ rateLimit?: ExplainField<string>;
46
+ input?: ExplainField<string>;
47
+ errors?: ExplainField<readonly string[]>;
48
+ suggestions: readonly string[];
49
+ }
50
+
51
+ export interface ExplainModuleReportV1 {
52
+ schemaVersion: 1;
53
+ module: string;
54
+ queries: readonly ExplainQueryEntry[];
55
+ actions: readonly ExplainActionEntry[];
56
+ warnings: readonly string[];
57
+ }
58
+
59
+ interface ExplainCommandReport extends ExplainModuleReportV1 {
60
+ mode: "explain";
61
+ projectRoot: string;
62
+ requestedModule: string;
63
+ inputPath: string;
64
+ inlineHints: readonly AuroraInlineHint[];
65
+ }
66
+
67
+ interface ExplainOptions {
68
+ modulePath: string;
69
+ inputPath: string;
70
+ format: ExplainFormat;
71
+ }
72
+
73
+ const VALID_SOURCES: readonly ExplainSource[] = [
74
+ "explicit",
75
+ "inferred",
76
+ "inherited",
77
+ "default",
78
+ "none",
79
+ ];
80
+
81
+ function parseExplainOptions(args: ReadonlyArray<string>): ExplainOptions | CommandResult {
82
+ let modulePath: string | undefined;
83
+ let inputPath: string | undefined;
84
+ let format: ExplainFormat = "text";
85
+
86
+ for (let i = 0; i < args.length; i += 1) {
87
+ const arg = args[i];
88
+
89
+ if (arg === "--input") {
90
+ const value = args[i + 1];
91
+ if (!value) {
92
+ return {
93
+ exitCode: 2,
94
+ stderr: "aurora explain: --input requires a path value",
95
+ };
96
+ }
97
+
98
+ inputPath = value;
99
+ i += 1;
100
+ continue;
101
+ }
102
+
103
+ if (arg === "--format") {
104
+ const value = args[i + 1];
105
+ if (!value) {
106
+ return {
107
+ exitCode: 2,
108
+ stderr: "aurora explain: --format requires 'text' or 'json'",
109
+ };
110
+ }
111
+
112
+ if (value !== "text" && value !== "json") {
113
+ return {
114
+ exitCode: 2,
115
+ stderr: `aurora explain: invalid format '${value}'. Expected 'text' or 'json'`,
116
+ };
117
+ }
118
+
119
+ format = value;
120
+ i += 1;
121
+ continue;
122
+ }
123
+
124
+ if (arg.startsWith("--")) {
125
+ return {
126
+ exitCode: 2,
127
+ stderr: `aurora explain: unknown option '${arg}'`,
128
+ };
129
+ }
130
+
131
+ if (!modulePath) {
132
+ modulePath = arg;
133
+ continue;
134
+ }
135
+
136
+ return {
137
+ exitCode: 2,
138
+ stderr: `aurora explain: unexpected positional argument '${arg}'`,
139
+ };
140
+ }
141
+
142
+ if (!modulePath || modulePath.trim().length === 0) {
143
+ return {
144
+ exitCode: 2,
145
+ stderr: "aurora explain: module path is required (usage: aurora explain <module-path>)",
146
+ };
147
+ }
148
+
149
+ const normalizedModulePath = normalizeModulePath(modulePath);
150
+ return {
151
+ modulePath: normalizedModulePath,
152
+ inputPath: inputPath ?? defaultExplainSnapshotPath(normalizedModulePath),
153
+ format,
154
+ };
155
+ }
156
+
157
+ function defaultExplainSnapshotPath(modulePath: string): string {
158
+ const normalized = normalizeModulePath(modulePath)
159
+ .replace(/^\.{1,2}\//, "")
160
+ .replace(/[^a-zA-Z0-9._-]+/g, "__");
161
+
162
+ return `.aurora/explain/${normalized}.explain.v1.json`;
163
+ }
164
+
165
+ function normalizeModulePath(modulePath: string): string {
166
+ return modulePath.replace(/\\+/g, "/").trim();
167
+ }
168
+
169
+ function readInputJson(inputPath: string, context: CommandContext): unknown | CommandResult {
170
+ const absolutePath = resolve(context.cwd, inputPath);
171
+ let raw = "";
172
+
173
+ try {
174
+ raw = readFileSync(absolutePath, "utf8");
175
+ } catch {
176
+ return {
177
+ exitCode: 1,
178
+ stderr:
179
+ `aurora explain: unable to read input at ${inputPath}\n` +
180
+ "Run the compiler explain export first, or provide --input <path>.",
181
+ };
182
+ }
183
+
184
+ try {
185
+ return JSON.parse(raw);
186
+ } catch {
187
+ return {
188
+ exitCode: 1,
189
+ stderr: `aurora explain: invalid json input at ${inputPath}`,
190
+ };
191
+ }
192
+ }
193
+
194
+ function readModuleSourceIfExists(modulePath: string, context: CommandContext): string | undefined {
195
+ const absolutePath = resolve(context.cwd, modulePath);
196
+ try {
197
+ return readFileSync(absolutePath, "utf8");
198
+ } catch {
199
+ return undefined;
200
+ }
201
+ }
202
+
203
+ function toRecord(value: unknown): Record<string, unknown> | undefined {
204
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
205
+ return undefined;
206
+ }
207
+
208
+ return value as Record<string, unknown>;
209
+ }
210
+
211
+ function isCommandResult(value: unknown): value is CommandResult {
212
+ const source = toRecord(value);
213
+ return Boolean(source && typeof source.exitCode === "number");
214
+ }
215
+
216
+ function parseExplainReport(
217
+ payload: unknown,
218
+ requestedModule: string,
219
+ ): ExplainModuleReportV1 {
220
+ const root = toRecord(payload);
221
+ if (!root) {
222
+ throw new Error("explain input must be an object");
223
+ }
224
+
225
+ const schemaVersion = root.schemaVersion;
226
+ if (schemaVersion !== 1) {
227
+ throw new Error(`unsupported schemaVersion ${String(schemaVersion)}; expected 1`);
228
+ }
229
+
230
+ const moduleFromPayload = getOptionalString(root, ["module", "modulePath"]);
231
+ const modulePath = moduleFromPayload ?? requestedModule;
232
+ const queries = parseQueryEntries(root.queries ?? []);
233
+ const actions = parseActionEntries(root.actions ?? []);
234
+ const warnings = dedupePreservingOrder([
235
+ ...parseStringArray(root.warnings ?? [], "warnings", { allowEmpty: true }),
236
+ ]);
237
+
238
+ if (normalizeModulePath(modulePath) !== normalizeModulePath(requestedModule)) {
239
+ warnings.push(
240
+ `snapshot module '${modulePath}' does not match requested module '${requestedModule}'`,
241
+ );
242
+ }
243
+
244
+ return {
245
+ schemaVersion: 1,
246
+ module: modulePath,
247
+ queries,
248
+ actions,
249
+ warnings,
250
+ };
251
+ }
252
+
253
+ function parseQueryEntries(raw: unknown): readonly ExplainQueryEntry[] {
254
+ if (!Array.isArray(raw)) {
255
+ throw new Error("queries must be an array");
256
+ }
257
+
258
+ return raw.map((entry, index) => parseQueryEntry(entry, index));
259
+ }
260
+
261
+ function parseActionEntries(raw: unknown): readonly ExplainActionEntry[] {
262
+ if (!Array.isArray(raw)) {
263
+ throw new Error("actions must be an array");
264
+ }
265
+
266
+ return raw.map((entry, index) => parseActionEntry(entry, index));
267
+ }
268
+
269
+ function parseQueryEntry(raw: unknown, index: number): ExplainQueryEntry {
270
+ const source = toRecord(raw);
271
+ if (!source) {
272
+ throw new Error(`queries[${index}] must be an object`);
273
+ }
274
+
275
+ const label = `queries[${index}]`;
276
+
277
+ const name = getRequiredString(source, ["name", "queryName"], `${label}.name`);
278
+ const key = parseExplainField(
279
+ source.key,
280
+ `${label}.key`,
281
+ (value, valueLabel) => parseKey(value, valueLabel),
282
+ "inferred",
283
+ );
284
+ const tags = parseExplainField(
285
+ source.tags,
286
+ `${label}.tags`,
287
+ (value, valueLabel) => parseStringArray(value, valueLabel, { allowEmpty: false }),
288
+ "inferred",
289
+ );
290
+
291
+ const cache =
292
+ source.cache === undefined
293
+ ? undefined
294
+ : parseExplainField(
295
+ source.cache,
296
+ `${label}.cache`,
297
+ (value, valueLabel) => parseCacheValue(value, valueLabel),
298
+ "default",
299
+ );
300
+
301
+ const realtime =
302
+ source.realtime === undefined
303
+ ? undefined
304
+ : parseExplainField(
305
+ source.realtime,
306
+ `${label}.realtime`,
307
+ (value, valueLabel) => parseNonEmptyString(value, valueLabel),
308
+ "inferred",
309
+ );
310
+
311
+ const auth =
312
+ source.auth === undefined
313
+ ? undefined
314
+ : parseExplainField(
315
+ source.auth,
316
+ `${label}.auth`,
317
+ (value, valueLabel) => parseNonEmptyString(value, valueLabel),
318
+ "inherited",
319
+ );
320
+
321
+ const mask =
322
+ source.mask === undefined
323
+ ? undefined
324
+ : parseExplainField(
325
+ source.mask,
326
+ `${label}.mask`,
327
+ (value, valueLabel) => parseNonEmptyString(value, valueLabel),
328
+ "none",
329
+ );
330
+
331
+ const estimatedPayloadBytes =
332
+ source.estimatedPayloadBytes === undefined
333
+ ? undefined
334
+ : parseNonNegativeNumber(source.estimatedPayloadBytes, `${label}.estimatedPayloadBytes`);
335
+
336
+ const suggestions = parseStringArray(source.suggestions ?? [], `${label}.suggestions`, {
337
+ allowEmpty: true,
338
+ });
339
+
340
+ return {
341
+ kind: "query",
342
+ name,
343
+ key,
344
+ tags,
345
+ cache,
346
+ realtime,
347
+ auth,
348
+ mask,
349
+ estimatedPayloadBytes,
350
+ suggestions,
351
+ };
352
+ }
353
+
354
+ function parseActionEntry(raw: unknown, index: number): ExplainActionEntry {
355
+ const source = toRecord(raw);
356
+ if (!source) {
357
+ throw new Error(`actions[${index}] must be an object`);
358
+ }
359
+
360
+ const label = `actions[${index}]`;
361
+
362
+ const name = getRequiredString(source, ["name", "actionName"], `${label}.name`);
363
+
364
+ const invalidates = parseExplainField(
365
+ source.invalidates,
366
+ `${label}.invalidates`,
367
+ (value, valueLabel) => parseStringArray(value, valueLabel, { allowEmpty: false }),
368
+ "inferred",
369
+ );
370
+
371
+ const publishes =
372
+ source.publishes === undefined
373
+ ? undefined
374
+ : parseExplainField(
375
+ source.publishes,
376
+ `${label}.publishes`,
377
+ (value, valueLabel) => parseStringArray(value, valueLabel, { allowEmpty: true }),
378
+ "explicit",
379
+ );
380
+
381
+ const auth =
382
+ source.auth === undefined
383
+ ? undefined
384
+ : parseExplainField(
385
+ source.auth,
386
+ `${label}.auth`,
387
+ (value, valueLabel) => parseNonEmptyString(value, valueLabel),
388
+ "explicit",
389
+ );
390
+
391
+ const rateLimit =
392
+ source.rateLimit === undefined
393
+ ? undefined
394
+ : parseExplainField(
395
+ source.rateLimit,
396
+ `${label}.rateLimit`,
397
+ (value, valueLabel) => parseNonEmptyString(value, valueLabel),
398
+ "none",
399
+ );
400
+
401
+ const input =
402
+ source.input === undefined
403
+ ? undefined
404
+ : parseExplainField(
405
+ source.input,
406
+ `${label}.input`,
407
+ (value, valueLabel) => parseNonEmptyString(value, valueLabel),
408
+ "explicit",
409
+ );
410
+
411
+ const errors =
412
+ source.errors === undefined
413
+ ? undefined
414
+ : parseExplainField(
415
+ source.errors,
416
+ `${label}.errors`,
417
+ (value, valueLabel) => parseStringArray(value, valueLabel, { allowEmpty: true }),
418
+ "none",
419
+ );
420
+
421
+ const suggestions = parseStringArray(source.suggestions ?? [], `${label}.suggestions`, {
422
+ allowEmpty: true,
423
+ });
424
+
425
+ return {
426
+ kind: "action",
427
+ name,
428
+ invalidates,
429
+ publishes,
430
+ auth,
431
+ rateLimit,
432
+ input,
433
+ errors,
434
+ suggestions,
435
+ };
436
+ }
437
+
438
+ function parseExplainField<TValue>(
439
+ raw: unknown,
440
+ label: string,
441
+ parseValue: (rawValue: unknown, valueLabel: string) => TValue,
442
+ defaultSource: ExplainSource,
443
+ ): ExplainField<TValue> {
444
+ const sourceRecord = toRecord(raw);
445
+ if (sourceRecord && Object.prototype.hasOwnProperty.call(sourceRecord, "value")) {
446
+ const value = parseValue(sourceRecord.value, `${label}.value`);
447
+ const source = parseSource(sourceRecord.source, defaultSource, `${label}.source`);
448
+ const note = getOptionalString(sourceRecord, ["note", "reason"]);
449
+
450
+ return note
451
+ ? {
452
+ value,
453
+ source,
454
+ note,
455
+ }
456
+ : {
457
+ value,
458
+ source,
459
+ };
460
+ }
461
+
462
+ return {
463
+ value: parseValue(raw, label),
464
+ source: defaultSource,
465
+ };
466
+ }
467
+
468
+ function parseSource(raw: unknown, fallback: ExplainSource, label: string): ExplainSource {
469
+ if (raw === undefined) {
470
+ return fallback;
471
+ }
472
+
473
+ if (typeof raw !== "string") {
474
+ throw new Error(`${label} must be one of: ${VALID_SOURCES.join(", ")}`);
475
+ }
476
+
477
+ if (!VALID_SOURCES.includes(raw as ExplainSource)) {
478
+ throw new Error(`${label} must be one of: ${VALID_SOURCES.join(", ")}`);
479
+ }
480
+
481
+ return raw as ExplainSource;
482
+ }
483
+
484
+ function parseKey(raw: unknown, label: string): readonly JsonValue[] | string {
485
+ if (typeof raw === "string" && raw.trim().length > 0) {
486
+ return raw.trim();
487
+ }
488
+
489
+ if (!Array.isArray(raw)) {
490
+ throw new Error(`${label} must be a string or an array`);
491
+ }
492
+
493
+ for (let index = 0; index < raw.length; index += 1) {
494
+ if (!isJsonValue(raw[index])) {
495
+ throw new Error(`${label}[${index}] must be JSON-serializable`);
496
+ }
497
+ }
498
+
499
+ return raw as readonly JsonValue[];
500
+ }
501
+
502
+ function parseCacheValue(raw: unknown, label: string): ExplainCacheValue | string {
503
+ if (typeof raw === "string") {
504
+ const normalized = raw.trim();
505
+ if (normalized.length > 0) {
506
+ return normalized;
507
+ }
508
+ }
509
+
510
+ const source = toRecord(raw);
511
+ if (!source) {
512
+ throw new Error(`${label} must be a string or an object`);
513
+ }
514
+
515
+ const staleTime = getOptionalString(source, ["staleTime", "staleTimeMs"]);
516
+ const gcTime = getOptionalString(source, ["gcTime", "gcTimeMs"]);
517
+
518
+ if (!staleTime && !gcTime) {
519
+ throw new Error(`${label} must define staleTime or gcTime when using object form`);
520
+ }
521
+
522
+ return {
523
+ staleTime,
524
+ gcTime,
525
+ };
526
+ }
527
+
528
+ function parseStringArray(
529
+ raw: unknown,
530
+ label: string,
531
+ options: { allowEmpty: boolean },
532
+ ): readonly string[] {
533
+ const values =
534
+ typeof raw === "string"
535
+ ? [raw]
536
+ : Array.isArray(raw)
537
+ ? raw
538
+ : (() => {
539
+ throw new Error(`${label} must be a string or an array of strings`);
540
+ })();
541
+
542
+ const normalized: string[] = [];
543
+ for (let index = 0; index < values.length; index += 1) {
544
+ const value = values[index];
545
+ if (typeof value !== "string") {
546
+ throw new Error(`${label}[${index}] must be a string`);
547
+ }
548
+
549
+ const trimmed = value.trim();
550
+ if (trimmed.length === 0) {
551
+ continue;
552
+ }
553
+
554
+ normalized.push(trimmed);
555
+ }
556
+
557
+ const deduped = dedupePreservingOrder(normalized);
558
+ if (!options.allowEmpty && deduped.length === 0) {
559
+ throw new Error(`${label} must include at least one non-empty string`);
560
+ }
561
+
562
+ return deduped;
563
+ }
564
+
565
+ function dedupePreservingOrder(values: readonly string[]): string[] {
566
+ const seen = new Set<string>();
567
+ const output: string[] = [];
568
+
569
+ for (const value of values) {
570
+ if (seen.has(value)) {
571
+ continue;
572
+ }
573
+
574
+ seen.add(value);
575
+ output.push(value);
576
+ }
577
+
578
+ return output;
579
+ }
580
+
581
+ function parseNonNegativeNumber(raw: unknown, label: string): number {
582
+ if (typeof raw !== "number" || !Number.isFinite(raw) || raw < 0) {
583
+ throw new Error(`${label} must be a non-negative number`);
584
+ }
585
+
586
+ return raw;
587
+ }
588
+
589
+ function parseNonEmptyString(raw: unknown, label: string): string {
590
+ if (typeof raw !== "string") {
591
+ throw new Error(`${label} must be a string`);
592
+ }
593
+
594
+ const normalized = raw.trim();
595
+ if (normalized.length === 0) {
596
+ throw new Error(`${label} must be a non-empty string`);
597
+ }
598
+
599
+ return normalized;
600
+ }
601
+
602
+ function getRequiredString(
603
+ source: Record<string, unknown>,
604
+ fields: ReadonlyArray<string>,
605
+ label: string,
606
+ ): string {
607
+ const value = getOptionalString(source, fields);
608
+ if (value) {
609
+ return value;
610
+ }
611
+
612
+ throw new Error(`missing required string field (${label})`);
613
+ }
614
+
615
+ function getOptionalString(
616
+ source: Record<string, unknown>,
617
+ fields: ReadonlyArray<string>,
618
+ ): string | undefined {
619
+ for (const field of fields) {
620
+ const value = source[field];
621
+ if (typeof value === "string") {
622
+ const normalized = value.trim();
623
+ if (normalized.length > 0) {
624
+ return normalized;
625
+ }
626
+ }
627
+ }
628
+
629
+ return undefined;
630
+ }
631
+
632
+ function isJsonValue(value: unknown): value is JsonValue {
633
+ if (
634
+ value === null ||
635
+ typeof value === "string" ||
636
+ typeof value === "boolean"
637
+ ) {
638
+ return true;
639
+ }
640
+
641
+ if (typeof value === "number") {
642
+ return Number.isFinite(value);
643
+ }
644
+
645
+ if (Array.isArray(value)) {
646
+ return value.every((entry) => isJsonValue(entry));
647
+ }
648
+
649
+ if (!value || typeof value !== "object") {
650
+ return false;
651
+ }
652
+
653
+ return Object.values(value).every((entry) => isJsonValue(entry));
654
+ }
655
+
656
+ function renderExplainText(report: ExplainCommandReport): string {
657
+ const lines: string[] = [
658
+ "aurora explain report",
659
+ `module: ${report.module}`,
660
+ `requested_module: ${report.requestedModule}`,
661
+ `input: ${report.inputPath}`,
662
+ `schema_version: ${report.schemaVersion}`,
663
+ ];
664
+
665
+ if (report.queries.length === 0 && report.actions.length === 0) {
666
+ lines.push("", "no explain entries found");
667
+ }
668
+
669
+ for (const query of report.queries) {
670
+ lines.push("", `query: ${query.name}`);
671
+ lines.push(formatFieldLine("key", formatKey(query.key.value), query.key));
672
+ lines.push(formatFieldLine("tags", formatStringList(query.tags.value), query.tags));
673
+
674
+ if (query.cache) {
675
+ lines.push(formatFieldLine("cache", formatCacheValue(query.cache.value), query.cache));
676
+ }
677
+
678
+ if (query.realtime) {
679
+ lines.push(formatFieldLine("realtime", query.realtime.value, query.realtime));
680
+ }
681
+
682
+ if (query.auth) {
683
+ lines.push(formatFieldLine("auth", query.auth.value, query.auth));
684
+ }
685
+
686
+ if (query.mask) {
687
+ lines.push(formatFieldLine("mask", query.mask.value, query.mask));
688
+ }
689
+
690
+ if (query.estimatedPayloadBytes !== undefined) {
691
+ lines.push(` estimated_payload: ${formatBytes(query.estimatedPayloadBytes)}`);
692
+ }
693
+
694
+ for (const suggestion of query.suggestions) {
695
+ lines.push(` suggestion: ${suggestion}`);
696
+ }
697
+ }
698
+
699
+ for (const action of report.actions) {
700
+ lines.push("", `action: ${action.name}`);
701
+ lines.push(
702
+ formatFieldLine("invalidates", formatStringList(action.invalidates.value), action.invalidates),
703
+ );
704
+
705
+ if (action.publishes) {
706
+ lines.push(formatFieldLine("publishes", formatStringList(action.publishes.value), action.publishes));
707
+ }
708
+
709
+ if (action.auth) {
710
+ lines.push(formatFieldLine("auth", action.auth.value, action.auth));
711
+ }
712
+
713
+ if (action.rateLimit) {
714
+ lines.push(formatFieldLine("rate_limit", action.rateLimit.value, action.rateLimit));
715
+ }
716
+
717
+ if (action.input) {
718
+ lines.push(formatFieldLine("input", action.input.value, action.input));
719
+ }
720
+
721
+ if (action.errors) {
722
+ lines.push(formatFieldLine("errors", formatStringList(action.errors.value), action.errors));
723
+ }
724
+
725
+ for (const suggestion of action.suggestions) {
726
+ lines.push(` suggestion: ${suggestion}`);
727
+ }
728
+ }
729
+
730
+ if (report.warnings.length > 0) {
731
+ lines.push("", "warnings:");
732
+ for (const warning of report.warnings) {
733
+ lines.push(`- ${warning}`);
734
+ }
735
+ }
736
+
737
+ if (report.inlineHints.length > 0) {
738
+ lines.push("", `inline_hints: ${report.inlineHints.length}`);
739
+ for (const hint of report.inlineHints) {
740
+ lines.push(
741
+ `- [${hint.kind}] ${hint.symbol} @${hint.position.line + 1}:${hint.position.character + 1} ${hint.label}`,
742
+ );
743
+ }
744
+ }
745
+
746
+ return lines.join("\n");
747
+ }
748
+
749
+ function formatFieldLine(
750
+ label: string,
751
+ formattedValue: string,
752
+ field: ExplainField<unknown>,
753
+ ): string {
754
+ const note = field.note ? `; ${field.note}` : "";
755
+ return ` ${label}: ${formattedValue} (${field.source}${note})`;
756
+ }
757
+
758
+ function formatKey(value: readonly JsonValue[] | string): string {
759
+ if (typeof value === "string") {
760
+ return value;
761
+ }
762
+
763
+ return JSON.stringify(value);
764
+ }
765
+
766
+ function formatStringList(values: readonly string[]): string {
767
+ if (values.length === 0) {
768
+ return "[]";
769
+ }
770
+
771
+ return `[${values.join(", ")}]`;
772
+ }
773
+
774
+ function formatCacheValue(value: ExplainCacheValue | string): string {
775
+ if (typeof value === "string") {
776
+ return value;
777
+ }
778
+
779
+ const parts: string[] = [];
780
+ if (value.staleTime) {
781
+ parts.push(`staleTime=${value.staleTime}`);
782
+ }
783
+
784
+ if (value.gcTime) {
785
+ parts.push(`gcTime=${value.gcTime}`);
786
+ }
787
+
788
+ return parts.join(", ");
789
+ }
790
+
791
+ function formatBytes(bytes: number): string {
792
+ if (bytes < 1024) {
793
+ return `${Math.trunc(bytes)}B`;
794
+ }
795
+
796
+ if (bytes < 1024 * 1024) {
797
+ return `~${(bytes / 1024).toFixed(1)}KB`;
798
+ }
799
+
800
+ return `~${(bytes / (1024 * 1024)).toFixed(2)}MB`;
801
+ }
802
+
803
+ export function runExplainCommand(
804
+ args: ReadonlyArray<string>,
805
+ context: CommandContext,
806
+ ): CommandResult {
807
+ const options = parseExplainOptions(args);
808
+ if (isCommandResult(options)) {
809
+ return options;
810
+ }
811
+
812
+ const payload = readInputJson(options.inputPath, context);
813
+ if (isCommandResult(payload)) {
814
+ return payload;
815
+ }
816
+
817
+ let parsed: ExplainModuleReportV1;
818
+ try {
819
+ parsed = parseExplainReport(payload, options.modulePath);
820
+ } catch (error) {
821
+ return {
822
+ exitCode: 1,
823
+ stderr:
824
+ `aurora explain: failed to parse explain report at ${options.inputPath}: ` +
825
+ (error instanceof Error ? error.message : "unknown error"),
826
+ };
827
+ }
828
+
829
+ const moduleSource = readModuleSourceIfExists(options.modulePath, context);
830
+ const inlineHints =
831
+ moduleSource !== undefined
832
+ ? createInlineHintsFromExplainReport(moduleSource, parsed)
833
+ : [];
834
+
835
+ const report: ExplainCommandReport = {
836
+ mode: "explain",
837
+ projectRoot: context.cwd,
838
+ requestedModule: options.modulePath,
839
+ inputPath: options.inputPath,
840
+ inlineHints,
841
+ ...parsed,
842
+ };
843
+
844
+ if (options.format === "json") {
845
+ return {
846
+ exitCode: 0,
847
+ stdout: JSON.stringify(report, null, 2),
848
+ };
849
+ }
850
+
851
+ return {
852
+ exitCode: 0,
853
+ stdout: renderExplainText(report),
854
+ };
855
+ }