@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/ai.ts ADDED
@@ -0,0 +1,574 @@
1
+ import { runCreateFeatureCommand } from "./create-feature";
2
+ import {
3
+ toAiReadableDiagnostic,
4
+ type AiReadableDiagnostic,
5
+ } from "./ai-diagnostics";
6
+ import { type CommandContext, type CommandResult } from "./registry";
7
+
8
+ type AiOutputFormat = "text" | "json";
9
+ type AiRouteKind = "crud" | "read-only" | "form" | "dashboard" | "empty";
10
+ type AiRouteDataSource = "database" | "api" | "none";
11
+
12
+ interface AiOptions {
13
+ prompt: string;
14
+ format: AiOutputFormat;
15
+ dryRun: boolean;
16
+ nameOverride?: string;
17
+ authOverride?: string;
18
+ }
19
+
20
+ interface AiInference {
21
+ featureSlug: string;
22
+ featureName: string;
23
+ routeKind: AiRouteKind;
24
+ routeDataSource: AiRouteDataSource;
25
+ routeAuth: string;
26
+ routeRealtime: boolean;
27
+ confidence: number;
28
+ rationale: readonly string[];
29
+ createFeatureArgs: readonly string[];
30
+ }
31
+
32
+ interface AiScaffoldResult {
33
+ attempted: boolean;
34
+ created: boolean;
35
+ exitCode?: number;
36
+ stdout?: string;
37
+ stderr?: string;
38
+ }
39
+
40
+ interface AiReport {
41
+ mode: "ai";
42
+ cwd: string;
43
+ prompt: string;
44
+ generatedAt: string;
45
+ dryRun: boolean;
46
+ inferred: AiInference;
47
+ scaffold: AiScaffoldResult;
48
+ diagnostics: readonly AiReadableDiagnostic[];
49
+ references: readonly string[];
50
+ testedExamples: readonly {
51
+ readonly prompt: string;
52
+ readonly expectation: string;
53
+ }[];
54
+ }
55
+
56
+ const STOP_WORDS = new Set([
57
+ "a",
58
+ "an",
59
+ "add",
60
+ "and",
61
+ "app",
62
+ "build",
63
+ "create",
64
+ "feature",
65
+ "for",
66
+ "in",
67
+ "new",
68
+ "screen",
69
+ "the",
70
+ "to",
71
+ "with",
72
+ ]);
73
+
74
+ const DOC_REFERENCES = [
75
+ "https://aurora.dev/docs/ai/overview",
76
+ "https://aurora.dev/docs/ai/prompting-guide",
77
+ "https://aurora.dev/docs/cli/ai",
78
+ ] as const;
79
+
80
+ const TESTED_EXAMPLES = [
81
+ {
82
+ prompt: "add a guestbook feature with reactions and moderation",
83
+ expectation:
84
+ "scaffolds feature + route with CRUD defaults and admin auth inference",
85
+ },
86
+ {
87
+ prompt: "create a realtime incident dashboard from api events",
88
+ expectation: "scaffolds dashboard route with api data mode and realtime enabled",
89
+ },
90
+ ] as const;
91
+
92
+ export function runAiCommand(
93
+ args: ReadonlyArray<string>,
94
+ context: CommandContext,
95
+ ): CommandResult {
96
+ const parsed = parseAiOptions(args);
97
+ if ("exitCode" in parsed) {
98
+ return parsed;
99
+ }
100
+
101
+ const diagnostics: AiReadableDiagnostic[] = [];
102
+ const inference = inferFromPrompt(parsed.prompt, parsed.nameOverride, parsed.authOverride);
103
+
104
+ if (inference.featureSlug === "ai-feature") {
105
+ diagnostics.push(
106
+ toAiReadableDiagnostic({
107
+ code: "AI_PROMPT_UNDERSPECIFIED",
108
+ severity: "warning",
109
+ message:
110
+ "Prompt did not include enough domain-specific tokens to infer a stable feature slug.",
111
+ remediation:
112
+ "Add domain nouns (for example guestbook, billing, notifications) or pass --name <feature-slug>.",
113
+ category: "ai",
114
+ }),
115
+ );
116
+ }
117
+
118
+ let scaffold: AiScaffoldResult = {
119
+ attempted: !parsed.dryRun,
120
+ created: false,
121
+ };
122
+
123
+ if (!parsed.dryRun) {
124
+ const result = runCreateFeatureCommand(inference.createFeatureArgs, context);
125
+
126
+ scaffold = {
127
+ attempted: true,
128
+ created: result.exitCode === 0,
129
+ exitCode: result.exitCode,
130
+ stdout: result.stdout,
131
+ stderr: result.stderr,
132
+ };
133
+
134
+ if (result.exitCode !== 0) {
135
+ diagnostics.push(
136
+ toAiReadableDiagnostic({
137
+ code: "AI_SCAFFOLD_FAILED",
138
+ severity: "error",
139
+ message:
140
+ result.stderr ??
141
+ "AI scaffold execution failed while applying inferred create-feature command.",
142
+ remediation:
143
+ "Resolve the scaffold error and rerun `aurora ai` with --dry-run first for quick validation.",
144
+ category: "ai",
145
+ }),
146
+ );
147
+ }
148
+ }
149
+
150
+ const report: AiReport = {
151
+ mode: "ai",
152
+ cwd: context.cwd,
153
+ prompt: parsed.prompt,
154
+ generatedAt: new Date().toISOString(),
155
+ dryRun: parsed.dryRun,
156
+ inferred: inference,
157
+ scaffold,
158
+ diagnostics,
159
+ references: DOC_REFERENCES,
160
+ testedExamples: TESTED_EXAMPLES,
161
+ };
162
+
163
+ const exitCode = scaffold.exitCode ?? 0;
164
+
165
+ if (parsed.format === "json") {
166
+ return {
167
+ exitCode,
168
+ stdout: JSON.stringify(report, null, 2),
169
+ };
170
+ }
171
+
172
+ return {
173
+ exitCode,
174
+ stdout: renderAiTextReport(report),
175
+ };
176
+ }
177
+
178
+ function parseAiOptions(args: ReadonlyArray<string>): AiOptions | CommandResult {
179
+ const options: Omit<AiOptions, "prompt"> & { promptParts: string[] } = {
180
+ format: "text",
181
+ dryRun: false,
182
+ promptParts: [],
183
+ nameOverride: undefined,
184
+ authOverride: undefined,
185
+ };
186
+
187
+ for (let i = 0; i < args.length; i += 1) {
188
+ const arg = args[i];
189
+
190
+ if (arg === "--format") {
191
+ const value = args[i + 1];
192
+ if (!value || (value !== "text" && value !== "json")) {
193
+ return {
194
+ exitCode: 2,
195
+ stderr: "aurora ai: --format requires 'text' or 'json'",
196
+ };
197
+ }
198
+
199
+ options.format = value;
200
+ i += 1;
201
+ continue;
202
+ }
203
+
204
+ if (arg === "--dry-run") {
205
+ options.dryRun = true;
206
+ continue;
207
+ }
208
+
209
+ if (arg === "--name") {
210
+ const value = args[i + 1];
211
+ if (!value) {
212
+ return {
213
+ exitCode: 2,
214
+ stderr: "aurora ai: --name requires a feature slug",
215
+ };
216
+ }
217
+
218
+ const slug = toFeatureSlug(value);
219
+ if (!slug) {
220
+ return {
221
+ exitCode: 2,
222
+ stderr:
223
+ "aurora ai: --name must include at least one letter or number",
224
+ };
225
+ }
226
+
227
+ options.nameOverride = slug;
228
+ i += 1;
229
+ continue;
230
+ }
231
+
232
+ if (arg === "--auth") {
233
+ const value = args[i + 1];
234
+ if (!value) {
235
+ return {
236
+ exitCode: 2,
237
+ stderr:
238
+ "aurora ai: --auth requires 'public', 'user', 'admin', or 'permission:<name>'",
239
+ };
240
+ }
241
+
242
+ const parsedAuth = parseAuthMode(value);
243
+ if (!parsedAuth) {
244
+ return {
245
+ exitCode: 2,
246
+ stderr:
247
+ "aurora ai: --auth requires 'public', 'user', 'admin', or 'permission:<name>'",
248
+ };
249
+ }
250
+
251
+ options.authOverride = parsedAuth;
252
+ i += 1;
253
+ continue;
254
+ }
255
+
256
+ if (arg.startsWith("--")) {
257
+ return {
258
+ exitCode: 2,
259
+ stderr: `aurora ai: unknown option '${arg}'`,
260
+ };
261
+ }
262
+
263
+ options.promptParts.push(arg);
264
+ }
265
+
266
+ const prompt = options.promptParts.join(" ").trim();
267
+ if (!prompt) {
268
+ return {
269
+ exitCode: 2,
270
+ stderr: "aurora ai: prompt description is required",
271
+ };
272
+ }
273
+
274
+ return {
275
+ prompt,
276
+ format: options.format,
277
+ dryRun: options.dryRun,
278
+ nameOverride: options.nameOverride,
279
+ authOverride: options.authOverride,
280
+ };
281
+ }
282
+
283
+ function inferFromPrompt(
284
+ prompt: string,
285
+ nameOverride: string | undefined,
286
+ authOverride: string | undefined,
287
+ ): AiInference {
288
+ const lowerPrompt = prompt.toLowerCase();
289
+ const tokens = (lowerPrompt.match(/[a-z0-9]+/g) ?? []).filter((token) => token.length > 1);
290
+
291
+ const inferredSlug =
292
+ nameOverride ??
293
+ toFeatureSlug(
294
+ tokens
295
+ .filter((token) => !STOP_WORDS.has(token))
296
+ .slice(0, 3)
297
+ .join("-"),
298
+ ) ??
299
+ "ai-feature";
300
+
301
+ const featureSlug = inferredSlug.length > 0 ? inferredSlug : "ai-feature";
302
+ const featureName = toFeatureName(featureSlug);
303
+
304
+ const routeKind = inferRouteKind(lowerPrompt);
305
+ const routeDataSource = inferRouteDataSource(lowerPrompt);
306
+ const inferredAuth = inferRouteAuth(lowerPrompt);
307
+ const routeAuth = authOverride ?? inferredAuth.value;
308
+ const routeRealtime = inferRealtime(lowerPrompt);
309
+
310
+ const rationale = [
311
+ routeKind.reason,
312
+ routeDataSource.reason,
313
+ authOverride
314
+ ? "route auth override applied from --auth"
315
+ : inferredAuth.reason,
316
+ routeRealtime
317
+ ? "detected realtime keywords (live/chat/updates/stream)"
318
+ : "no realtime keywords detected; realtime scaffold disabled",
319
+ ];
320
+
321
+ const confidence = computeConfidence(prompt, featureSlug, tokens.length, routeRealtime);
322
+
323
+ const createFeatureArgs = [
324
+ featureSlug,
325
+ "--kind",
326
+ routeKind.value,
327
+ "--data",
328
+ routeDataSource.value,
329
+ "--auth",
330
+ routeAuth,
331
+ routeRealtime ? "--realtime" : "--no-realtime",
332
+ ] as const;
333
+
334
+ return {
335
+ featureSlug,
336
+ featureName,
337
+ routeKind: routeKind.value,
338
+ routeDataSource: routeDataSource.value,
339
+ routeAuth,
340
+ routeRealtime,
341
+ confidence,
342
+ rationale,
343
+ createFeatureArgs,
344
+ };
345
+ }
346
+
347
+ function inferRouteKind(prompt: string): { value: AiRouteKind; reason: string } {
348
+ if (containsAny(prompt, ["dashboard", "analytics", "metric", "chart"])) {
349
+ return {
350
+ value: "dashboard",
351
+ reason: "detected dashboard/analytics terms; selecting dashboard scaffold kind",
352
+ };
353
+ }
354
+
355
+ if (containsAny(prompt, ["crud", "admin", "moderation", "manage", "delete", "update"])) {
356
+ return {
357
+ value: "crud",
358
+ reason: "detected management/moderation terms; selecting CRUD scaffold kind",
359
+ };
360
+ }
361
+
362
+ if (containsAny(prompt, ["list", "feed", "timeline", "read", "viewer", "catalog"])) {
363
+ return {
364
+ value: "read-only",
365
+ reason: "detected listing/read patterns; selecting read-only scaffold kind",
366
+ };
367
+ }
368
+
369
+ if (containsAny(prompt, ["empty", "blank", "starter"])) {
370
+ return {
371
+ value: "empty",
372
+ reason: "detected explicit empty/starter intent; selecting empty scaffold kind",
373
+ };
374
+ }
375
+
376
+ return {
377
+ value: "form",
378
+ reason: "defaulting to form scaffold kind for mutation-first feature generation",
379
+ };
380
+ }
381
+
382
+ function inferRouteDataSource(prompt: string): { value: AiRouteDataSource; reason: string } {
383
+ if (containsAny(prompt, ["api", "rest", "graphql", "webhook", "http"])) {
384
+ return {
385
+ value: "api",
386
+ reason: "detected external API terms; selecting api data source mode",
387
+ };
388
+ }
389
+
390
+ if (containsAny(prompt, ["static", "local", "mock", "offline", "no data"])) {
391
+ return {
392
+ value: "none",
393
+ reason: "detected static/no-data terms; selecting none data source mode",
394
+ };
395
+ }
396
+
397
+ return {
398
+ value: "database",
399
+ reason: "defaulting to database data source mode",
400
+ };
401
+ }
402
+
403
+ function inferRouteAuth(
404
+ prompt: string,
405
+ ): { value: string; reason: string } {
406
+ const value = inferRouteAuthValue(prompt);
407
+
408
+ if (value === "admin") {
409
+ return {
410
+ value,
411
+ reason: "detected admin/moderation intent; selecting admin auth",
412
+ };
413
+ }
414
+
415
+ if (value === "user") {
416
+ return {
417
+ value,
418
+ reason: "detected account/private intent; selecting user auth",
419
+ };
420
+ }
421
+
422
+ return {
423
+ value,
424
+ reason: "defaulting to public auth",
425
+ };
426
+ }
427
+
428
+ function inferRouteAuthValue(prompt: string): string {
429
+ if (containsAny(prompt, ["admin", "moderation", "moderator", "backoffice"])) {
430
+ return "admin";
431
+ }
432
+
433
+ if (containsAny(prompt, ["account", "profile", "member", "private", "user"])) {
434
+ return "user";
435
+ }
436
+
437
+ return "public";
438
+ }
439
+
440
+ function inferRealtime(prompt: string): boolean {
441
+ return containsAny(prompt, [
442
+ "realtime",
443
+ "real-time",
444
+ "live",
445
+ "stream",
446
+ "chat",
447
+ "notification",
448
+ "updates",
449
+ "socket",
450
+ ]);
451
+ }
452
+
453
+ function computeConfidence(
454
+ prompt: string,
455
+ featureSlug: string,
456
+ tokenCount: number,
457
+ routeRealtime: boolean,
458
+ ): number {
459
+ let confidence = 0.55;
460
+
461
+ if (featureSlug !== "ai-feature") {
462
+ confidence += 0.1;
463
+ }
464
+
465
+ if (prompt.trim().length >= 24) {
466
+ confidence += 0.1;
467
+ }
468
+
469
+ if (tokenCount >= 6) {
470
+ confidence += 0.1;
471
+ }
472
+
473
+ if (routeRealtime) {
474
+ confidence += 0.05;
475
+ }
476
+
477
+ return Math.min(0.95, Math.max(0.4, Number(confidence.toFixed(2))));
478
+ }
479
+
480
+ function renderAiTextReport(report: AiReport): string {
481
+ const lines = [
482
+ "aurora ai bootstrap",
483
+ `cwd: ${report.cwd}`,
484
+ `prompt: ${report.prompt}`,
485
+ `generated_at: ${report.generatedAt}`,
486
+ `dry_run: ${report.dryRun ? "enabled" : "disabled"}`,
487
+ `feature_slug: ${report.inferred.featureSlug}`,
488
+ `feature_name: ${report.inferred.featureName}`,
489
+ `route_kind: ${report.inferred.routeKind}`,
490
+ `route_data_source: ${report.inferred.routeDataSource}`,
491
+ `route_auth: ${report.inferred.routeAuth}`,
492
+ `route_realtime: ${report.inferred.routeRealtime ? "enabled" : "disabled"}`,
493
+ `confidence: ${report.inferred.confidence}`,
494
+ `scaffold_command: aurora create-feature ${report.inferred.createFeatureArgs.join(" ")}`,
495
+ "rationale:",
496
+ ...report.inferred.rationale.map((reason) => `- ${reason}`),
497
+ ];
498
+
499
+ if (report.diagnostics.length > 0) {
500
+ lines.push("diagnostics:");
501
+ for (const diagnostic of report.diagnostics) {
502
+ lines.push(`- [${diagnostic.severity}] ${diagnostic.code}: ${diagnostic.whatWentWrong}`);
503
+ lines.push(` why: ${diagnostic.why}`);
504
+ lines.push(` fix: ${diagnostic.howToFix}`);
505
+ lines.push(` docs: ${diagnostic.docs}`);
506
+ }
507
+ } else {
508
+ lines.push("diagnostics: none");
509
+ }
510
+
511
+ lines.push("references:");
512
+ lines.push(...report.references.map((reference) => `- ${reference}`));
513
+
514
+ lines.push("tested_examples:");
515
+ lines.push(
516
+ ...report.testedExamples.map(
517
+ (example) => `- ${example.prompt} -> ${example.expectation}`,
518
+ ),
519
+ );
520
+
521
+ if (report.scaffold.attempted && report.scaffold.stdout) {
522
+ lines.push("scaffold_output:");
523
+ lines.push(...report.scaffold.stdout.split("\n").map((line) => ` ${line}`));
524
+ }
525
+
526
+ if (report.scaffold.attempted && report.scaffold.stderr) {
527
+ lines.push(`scaffold_error: ${report.scaffold.stderr}`);
528
+ }
529
+
530
+ return lines.join("\n");
531
+ }
532
+
533
+ function parseAuthMode(value: string): string | undefined {
534
+ const normalized = value.trim();
535
+
536
+ if (normalized === "public" || normalized === "user" || normalized === "admin") {
537
+ return normalized;
538
+ }
539
+
540
+ if (/^permission:[a-z0-9:_-]+$/i.test(normalized)) {
541
+ return normalized;
542
+ }
543
+
544
+ return undefined;
545
+ }
546
+
547
+ function toFeatureSlug(rawName: string): string | undefined {
548
+ const slug = rawName
549
+ .trim()
550
+ .replace(/[^a-zA-Z0-9]+/g, "-")
551
+ .replace(/^-+|-+$/g, "")
552
+ .replace(/-+/g, "-")
553
+ .toLowerCase();
554
+
555
+ return slug.length > 0 ? slug : undefined;
556
+ }
557
+
558
+ function toFeatureName(slug: string): string {
559
+ const pascal = slug
560
+ .split("-")
561
+ .filter((part) => part.length > 0)
562
+ .map((part) => part[0].toUpperCase() + part.slice(1))
563
+ .join("");
564
+
565
+ if (/^[A-Za-z_]/.test(pascal)) {
566
+ return pascal;
567
+ }
568
+
569
+ return `Feature${pascal}`;
570
+ }
571
+
572
+ function containsAny(value: string, keywords: readonly string[]): boolean {
573
+ return keywords.some((keyword) => value.includes(keyword));
574
+ }