@agent-native/core-corpus 0.221.0 → 0.222.0-nightly-20261028092719

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 (98) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/.agents/skills/first-touch-radar/SKILL.md +19 -14
  3. package/corpus/templates/analytics/.agents/skills/metric-watch/SKILL.md +5 -2
  4. package/corpus/templates/analytics/.agents/skills/onboarding-journeys/SKILL.md +22 -4
  5. package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  6. package/corpus/templates/analytics/actions/get-onboarding-journey.ts +3 -2
  7. package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -0
  8. package/corpus/templates/analytics/changelog/2026-10-22-outside-assistants-can-open-the-traffic-dashboard-inside-the.md +6 -0
  9. package/corpus/templates/analytics/package.json +4 -0
  10. package/corpus/templates/analytics/scripts/first-touch-radar-live.ts +110 -0
  11. package/corpus/templates/analytics/scripts/journey-snapshot-plan.ts +148 -4
  12. package/corpus/templates/analytics/scripts/journey-snapshot.ts +308 -120
  13. package/corpus/templates/analytics/scripts/radar/ab.ts +10 -0
  14. package/corpus/templates/analytics/scripts/radar/backtest.ts +399 -0
  15. package/corpus/templates/analytics/scripts/radar/campaign.ts +53 -10
  16. package/corpus/templates/analytics/scripts/radar/checks.ts +10 -3
  17. package/corpus/templates/analytics/scripts/radar/live.ts +17 -0
  18. package/corpus/templates/analytics/scripts/radar/query.ts +312 -83
  19. package/corpus/templates/analytics/scripts/radar/report.ts +199 -19
  20. package/corpus/templates/analytics/scripts/radar/sql.ts +89 -18
  21. package/corpus/templates/analytics/scripts/radar/warehouse-sql.ts +322 -21
  22. package/corpus/templates/analytics/scripts/radar/watch.ts +203 -129
  23. package/corpus/templates/analytics/server/lib/onboarding-journey.ts +10 -2
  24. package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  25. package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  26. package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  27. package/corpus/templates/calendar/AGENTS.md +1 -0
  28. package/corpus/templates/calendar/actions/manage-event-draft.ts +33 -11
  29. package/corpus/templates/calendar/changelog/2026-10-22-outside-assistants-can-open-invite-drafts-in-calendar-for-re.md +6 -0
  30. package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  31. package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  32. package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  33. package/corpus/templates/content/AGENTS.md +1 -0
  34. package/corpus/templates/content/actions/_preview-document-draft-settlement.ts +31 -0
  35. package/corpus/templates/content/actions/_preview-document-draft.ts +1 -0
  36. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +267 -200
  37. package/corpus/templates/content/actions/share-local-file-document.ts +9 -1
  38. package/corpus/templates/content/actions/update-preview-document-draft.ts +7 -0
  39. package/corpus/templates/content/app/components/editor/CommentAiRecipient.tsx +16 -4
  40. package/corpus/templates/content/app/components/editor/CommentComposer.tsx +1 -1
  41. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +71 -209
  42. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +358 -24
  43. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +43 -6
  44. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +52 -14
  45. package/corpus/templates/content/app/components/editor/SuggestionControls.tsx +191 -0
  46. package/corpus/templates/content/app/components/editor/SuggestionReviewCard.tsx +1 -0
  47. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +80 -91
  48. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -0
  49. package/corpus/templates/content/app/components/editor/comment-ai.tsx +23 -5
  50. package/corpus/templates/content/app/global.css +7 -0
  51. package/corpus/templates/content/app/hooks/use-documents.ts +52 -5
  52. package/corpus/templates/content/app/i18n-data.ts +6 -0
  53. package/corpus/templates/content/app/lib/document-content-hash.ts +16 -0
  54. package/corpus/templates/content/changelog/2026-10-17-choosing-use-saved-after-an-interrupted-recovery-no-longer-l.md +6 -0
  55. package/corpus/templates/content/changelog/2026-10-17-renaming-a-page-while-an-agent-edits-it-no-longer-stops-your.md +6 -0
  56. package/corpus/templates/content/changelog/2026-10-22-outside-assistants-can-open-shareable-local-file-copies-in-c.md +6 -0
  57. package/corpus/templates/content/changelog/2026-10-22-pages-using-the-live-copy-keep-unsent-edits-through-a-refres.md +6 -0
  58. package/corpus/templates/content/changelog/2026-10-26-a-comment-sent-to-ai-says-what-it-asks-for-on-its-send-butto.md +6 -0
  59. package/corpus/templates/content/changelog/2026-10-26-opening-another-comment-thread-while-you-re-writing-a-new-co.md +6 -0
  60. package/corpus/templates/content/changelog/2026-10-26-when-the-ai-answers-in-a-long-comment-thread-the-card-shows-.md +6 -0
  61. package/corpus/templates/content/changelog/2026-10-27-an-ai-suggestion-in-a-comment-thread-uses-the-same-group-as.md +6 -0
  62. package/corpus/templates/content/changelog/2026-10-27-restoring-page-history-preserves-text-without-exposing-unaut.md +6 -0
  63. package/corpus/templates/content/server/db/schema.ts +1 -0
  64. package/corpus/templates/content/server/lib/private-icon-references.ts +37 -14
  65. package/corpus/templates/content/server/plugins/db.ts +5 -0
  66. package/corpus/templates/content/shared/api.ts +5 -0
  67. package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  68. package/corpus/templates/design/.agents/skills/journey-storyboards/SKILL.md +8 -5
  69. package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  70. package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +326 -0
  71. package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +6 -0
  72. package/corpus/templates/design/AGENTS.md +3 -1
  73. package/corpus/templates/design/actions/create-journey-canvas.ts +1 -1
  74. package/corpus/templates/design/actions/export-coding-handoff.ts +8 -1
  75. package/corpus/templates/design/actions/generate-screens.ts +6 -0
  76. package/corpus/templates/design/changelog/2026-10-22-outside-assistants-can-generate-screens-and-open-coding-hand.md +6 -0
  77. package/corpus/templates/design/server/lib/builder-identity.ts +203 -29
  78. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -1
  79. package/corpus/templates/design/server/plugins/auth.ts +9 -4
  80. package/corpus/templates/design/server/routes/api/builder-identity/exchange.post.ts +1 -1
  81. package/corpus/templates/design/shared/journey-canvas-messages.ts +1 -2
  82. package/corpus/templates/design/shared/journey-canvas.ts +143 -55
  83. package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  84. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +41 -18
  85. package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  86. package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  87. package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  88. package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  89. package/corpus/templates/slides/AGENTS.md +6 -0
  90. package/corpus/templates/slides/actions/clone-context-slide.ts +13 -0
  91. package/corpus/templates/slides/changelog/2026-10-22-outside-assistants-can-clone-a-slide-from-existing-context-a.md +6 -0
  92. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -0
  93. package/corpus/templates/slides-2/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  94. package/corpus/templates/slides-2/AGENTS.md +6 -0
  95. package/corpus/templates/slides-2/actions/clone-context-slide.ts +6 -0
  96. package/corpus/templates/slides-2/changelog/2026-10-22-outside-assistants-can-clone-a-slide-from-existing-context-a.md +6 -0
  97. package/corpus/templates/slides-2/server/plugins/agent-chat.ts +1 -0
  98. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 14961
35
+ - template files: 15033
@@ -10,8 +10,10 @@ scope: dev
10
10
 
11
11
  1. Run `pnpm --filter analytics radar:watch --window daily` and read both the
12
12
  JSON and Markdown under `.tmp/first-touch-radar/runs/`.
13
- 2. Treat exit 0 as clear, exit 1 as one or more fired checks, and exit 2 as
14
- incomplete evidence. Never turn a missing source or failed query into zero.
13
+ 2. Treat exit 0 as clear, exit 1 as a fired, unavailable, or insufficient
14
+ check needing attention, and exit 2 as a run that could not complete. Keep
15
+ unavailable and insufficient metrics explicit; never turn missing evidence
16
+ into zero or a pass.
15
17
  3. On a fire, run `pnpm --filter analytics radar:triage` and inspect the
16
18
  outcome-ranked sessions. Use `metric-watch` and `replay-triage` for details.
17
19
  4. For a weekly review, run `pnpm --filter analytics radar:report --week
@@ -24,18 +26,21 @@ scope: dev
24
26
  report-wide cap, and treat a reached cap as incomplete evidence.
25
27
  5. Run `feedback-ingest` for Slack themes. Searches are read-only and begin at
26
28
  the saved watermark.
27
- 6. Send one Telegram to Steve only when a check fired or the weekly report is
28
- ready. Include counts and the private run-file path. Send nothing on a quiet
29
- day. Never send Slack or email.
30
- 7. Keep recipient emails and prompt text out of logs, reports, Telegram, and
31
- commits. The campaign check reads its mode-0600 recipient file in memory and
32
- emits aggregate counts only. With a PostgreSQL sink, recipient identifiers
33
- are parameters to the read-only query and caching is disabled. A BigQuery
34
- sink cannot run campaign follow-up until a keyed recipient matching
35
- function is configured; report it as incomplete rather than treating it as
36
- zero. Unknown signup campaign labels are grouped as `other`. Anonymous
37
- landings cannot be assigned to a recipient; do not interpret identified
38
- counts as a complete click total.
29
+ 6. Send one Telegram to Steve only when a check fired, is unavailable or is
30
+ insufficient, or the weekly report is ready. Include counts and the private
31
+ run-file path. Send nothing on a quiet day. Never send Slack or email.
32
+ 7. Keep recipient emails, HMAC keys, and prompt text out of logs, reports,
33
+ Telegram, and commits. The campaign check reads its mode-0600 recipient file
34
+ in memory, binds keyed email digests as query parameters, disables local and
35
+ BigQuery result caching, and emits aggregate counts only. BigQuery matching
36
+ requires `ANALYTICS_RADAR_CAMPAIGN_HMAC_KEY` and a configured
37
+ `ANALYTICS_RADAR_CAMPAIGN_HMAC_FUNCTION` backed by the same secret server-side.
38
+ The function accepts a normalized email and returns lowercase hex
39
+ `HMAC_SHA256`; keep its key in Secret Manager and never pass the key to
40
+ BigQuery. When either setting is missing, keep the campaign check
41
+ unavailable. Unknown signup campaign labels are grouped as `other`.
42
+ Anonymous landings cannot be assigned to a recipient; do not interpret
43
+ identified counts as a complete click total.
39
44
  8. This radar changes scripts and skills only. Do not make user-visible UI
40
45
  changes. If UI work becomes necessary, stop that scope and request Steve's
41
46
  screenshot approval before implementing it.
@@ -11,10 +11,13 @@ scope: dev
11
11
  - Run `pnpm --filter analytics radar:watch --window daily`; use `--window
12
12
  hourly` for provisioning and environment-key checks.
13
13
  - Read the check status, numerator, denominator, seven-day comparison, and
14
- coverage. `insufficient` is not a pass, and `unavailable` means the run exits
15
- 2.
14
+ coverage. `insufficient` is not a pass. `unavailable` and `insufficient`
15
+ remain explicit and make a completed watch exit 1; exit 2 means the run could
16
+ not complete.
16
17
  - Compare daily thresholds with the incident window and an ordinary week before
17
18
  changing them. Record both windows and the observed counts in the PR or report.
19
+ - Use `pnpm --filter analytics radar:backtest` to compare the Oct 9–23 incident
20
+ window with the Oct 2–8 clean week.
18
21
  - Action success uses a weighted `action.response` rate. Keep its app-specific
19
22
  threshold below the quiet-week distribution; do not copy the 95% starting
20
23
  value without a backtest.
@@ -18,7 +18,8 @@ The pipeline runs without a UI. Codex does all four steps from the terminal:
18
18
  2. Run the capture CLI from the agent-native checkout:
19
19
  `pnpm --filter analytics journey:capture --tree tree.json --out ./frames`
20
20
  (`--per-node 2 --concurrency 3`, `--min-aspect` / `--max-aspect`, `--dry-run`
21
- to print the plan, `--upload` for private `attachmentRef`s).
21
+ to print the plan, `--upload` for private `attachmentRef`s, or
22
+ `--upload --upload-target cdn` for standard Builder CDN URLs).
22
23
  3. Call Design's `create-journey-canvas` with
23
24
  `{ title, tree, frames: [{ nodeKey, exampleIndex, imageUrl?, attachmentRef?, width, height, capturedAt }], designId? }`
24
25
  -> `{ designId, url, nodeCount, frameCount }`. Build `frames` from
@@ -315,12 +316,14 @@ extraction time. Each snapshot also says whether its frame capture or upload
315
316
  succeeded and includes the failure reason when it did not. This is visible
316
317
  user-role text at the seek, not proof of an exact source event or attempt-ID
317
318
  relationship. Unsupported transform, mask, or rounded clipping geometry fails
318
- the extraction explicitly. It is never uploaded, including when `--upload` stores PNGs
319
- through the private attachment action.
319
+ the extraction explicitly. The prompt sidecar itself is never uploaded; PNG
320
+ screenshots can use the private attachment action or the explicitly selected
321
+ `cdn.builder.io` upload target.
320
322
 
321
323
  Keep recorded URLs and CSS intact for rrweb playback; network controls belong
322
324
  at the capture boundary. `--upload` stores PNGs through the private upload
323
- action, which checks recording access again. Design's own sandboxed template
325
+ action by default; `--upload-target cdn` uses `upload-image` and accepts only
326
+ HTTPS `cdn.builder.io` URLs. Design's own sandboxed template
324
327
  previews opt into the cooperative iframe recorder, so their child DOM can be
325
328
  replayed without granting the parent access to the preview document or
326
329
  credentials.
@@ -328,3 +331,18 @@ credentials.
328
331
  Viewport comes from the recording's first rrweb Meta event, stored by replay
329
332
  ingest in `session_recordings.metadata.viewport` (`first` and `last`). Older
330
333
  recordings have none.
334
+
335
+ ## Weekly snapshot CLI
336
+
337
+ Use the cached weekly pipeline when generating or refreshing app storyboards:
338
+
339
+ ```bash
340
+ pnpm --filter analytics journey:snapshot --week 2026-10-19 --apps clips,design,slides
341
+ pnpm --filter analytics journey:snapshot --week 2026-10-19 --apps design --experiment first_run_builder_credits
342
+ ```
343
+
344
+ `--week` is a closed UTC Monday week. The query starts the previous day to include sessions already underway. Without `--week`, the CLI selects the last full week. It runs the pinned cost preflight and reads one tree per missing app/week/cohort from deployed Analytics; complete trees are cached by app, dates, preset, app URL, and authenticated principal. `--dry-run` prints the request without network access. `--refresh` bypasses existing tree, screenshot, and Design caches and should be used only for an intentional refresh.
345
+
346
+ The output lives under `.tmp/onboarding-journey-snapshots/runs/<week>/`: each app with captured session frames has a Design canvas, and each app has a per-app tree cache and screenshot manifests; the shared `summary.md` includes the cohort trees. Each canvas has a 36 screenshot-backed frame ceiling allocated across its cohort bands. Multi-band canvases place independent trees side by side and keep each band's own root denominator. Same-app cohort trees use `app::cohort::step` node keys so the source app and band denominator stay distinct. Canvas cache keys include the experiment and cohort layout so different runs cannot overwrite one another. The optional experiment accepts `first_run_builder_credits` and `first_run_builder_credits_v2`; control and credits share one canvas, and `summary.md` reports per-arm forks and matching-step week-over-week changes from the full Analytics trees, including cohorts with zero screenshot frames. The canvas contains only screenshot-backed cohorts. Clips first output is `recording_ready`.
347
+
348
+ Design cohorts split web, `/mcp/oauth`, and unknown signup paths. Sessionless MCP OAuth signup events appear as a separate event-grain cohort in `summary.md`, with a week-over-week event-count delta; they have no web replay and must not be represented as screenshot steps. Captured PNGs use `journey:capture --upload-target cdn`; the CLI verifies each result is an HTTPS `cdn.builder.io` URL before passing it to Design. Capture failures remain explicit in the run summary, and the pipeline does not create screenshotless cards.
@@ -0,0 +1,10 @@
1
+ // AUTO-GENERATED by @agent-native/core deploy build
2
+ import { markDefaultPluginProvided } from "@agent-native/core/server/edge";
3
+
4
+ const pluginStems = ["agent-chat","analytics-alert-jobs","analytics-rollup-backfill-jobs","auth","collab","config","content-metrics-collection-jobs","core-routes","creative-context","dashboard-report-jobs","db","feature-flags","labs","onboarding","otel","session-replay-retention-jobs","transactional-emails","uptime-monitor-jobs"];
5
+
6
+ export default function markBuildDiscoveredPlugins(nitroApp) {
7
+ for (const stem of pluginStems) {
8
+ markDefaultPluginProvided(nitroApp, stem);
9
+ }
10
+ }
@@ -14,6 +14,7 @@ import {
14
14
  DEFAULT_ONBOARDING_EVENT_ROW_BUDGET,
15
15
  getOnboardingJourney,
16
16
  MAX_ONBOARDING_EVENT_ROW_BUDGET,
17
+ ONBOARDING_JOURNEY_EXPERIMENT_IDS,
17
18
  ONBOARDING_EVENTS_DEFAULT_MAX_TOTAL_BYTES_BILLED,
18
19
  ONBOARDING_EVENTS_MAX_BYTES_BILLED,
19
20
  ONBOARDING_EVENTS_MAX_TOTAL_BYTES_BILLED,
@@ -192,10 +193,10 @@ export default defineAction({
192
193
  "Leave out examples whose viewport was not recorded or cannot be read. Without it they are kept with viewport null and a viewportReason.",
193
194
  ),
194
195
  experimentId: z
195
- .literal("first_run_builder_credits")
196
+ .enum(ONBOARDING_JOURNEY_EXPERIMENT_IDS)
196
197
  .optional()
197
198
  .describe(
198
- "Return session trees for the control and credits arms from one event read. Sessions with conflicting or missing assignment properties remain outside both arms.",
199
+ "Experiment ID to match exactly against each exposure event's experiment_id: first_run_builder_credits or first_run_builder_credits_v2. Returns control and credits session trees from one event read; events for other IDs are not included. Sessions with conflicting or missing assignments remain outside both arms.",
199
200
  ),
200
201
  splitDesignFirstTouch: z
201
202
  .boolean()
@@ -15,6 +15,7 @@ function trafficDashboardDeepLink(): string {
15
15
  }
16
16
 
17
17
  export default defineAction({
18
+ mcpTool: true,
18
19
  description:
19
20
  "Open the first-party traffic dashboard in the real Analytics app. Use this directly when the user asks to see their traffic dashboard, site traffic, app traffic, or first-party analytics dashboard inline in ChatGPT or Claude. Do not call view-screen, ask_app, or broad resource discovery first for this known dashboard.",
20
21
  schema: z.object({}),
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-10-22
4
+ ---
5
+
6
+ Outside assistants can open the Traffic dashboard in Analytics.
@@ -16,9 +16,13 @@
16
16
  "journey:capture": "tsx scripts/journey-capture.ts",
17
17
  "radar:query": "tsx scripts/radar/query.ts",
18
18
  "radar:watch": "tsx scripts/radar/watch.ts",
19
+ "radar:live-watch": "tsx scripts/first-touch-radar-live.ts watch",
19
20
  "radar:triage": "tsx scripts/radar/triage.ts",
20
21
  "radar:ab": "tsx scripts/radar/ab.ts",
22
+ "radar:live-ab": "tsx scripts/first-touch-radar-live.ts ab",
21
23
  "radar:report": "tsx scripts/radar/report.ts",
24
+ "radar:live-report": "tsx scripts/first-touch-radar-live.ts report",
25
+ "radar:backtest": "tsx scripts/first-touch-radar-live.ts backtest",
22
26
  "journey:snapshot": "tsx scripts/journey-snapshot.ts",
23
27
  "source-index": "tsx scripts/build-source-index.ts",
24
28
  "backfill:first-party-bigquery": "tsx scripts/first-party-analytics-bigquery-backfill.ts",
@@ -0,0 +1,110 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import {
6
+ runAbReadout,
7
+ formatAbMarkdown,
8
+ parseAbReadoutArgs,
9
+ } from "./radar/ab.ts";
10
+ import {
11
+ formatRadarBacktest,
12
+ parseRadarBacktestArgs,
13
+ runRadarBacktest,
14
+ } from "./radar/backtest.ts";
15
+ import { liveDatabaseUrlFromEnv } from "./radar/live.ts";
16
+ import {
17
+ REPO_ROOT,
18
+ RadarQueryError,
19
+ configureRadarRuntimeEnvironment,
20
+ closeRadarQueries,
21
+ radarRunCliExitCode,
22
+ } from "./radar/query.ts";
23
+ import {
24
+ buildWeeklyReport,
25
+ formatWeeklyMarkdown,
26
+ parseReportArgs,
27
+ writeWeeklyReport,
28
+ } from "./radar/report.ts";
29
+ import { parseWatchArgs, runWatch, writeWatchRun } from "./radar/watch.ts";
30
+
31
+ function loadLiveDatabaseUrl(): void {
32
+ const configuredFile = process.env.ANALYTICS_RADAR_ENV_FILE;
33
+ const envFile = path.resolve(
34
+ REPO_ROOT,
35
+ configuredFile ?? "templates/analytics/.env",
36
+ );
37
+ let contents: string;
38
+ try {
39
+ contents = fs.readFileSync(envFile, "utf8");
40
+ } catch {
41
+ throw new RadarQueryError(
42
+ "Live radar environment file could not be read.",
43
+ 2,
44
+ );
45
+ }
46
+ configureRadarRuntimeEnvironment({
47
+ selectedFile: envFile,
48
+ contents,
49
+ databaseUrl: liveDatabaseUrlFromEnv(contents),
50
+ });
51
+ }
52
+
53
+ async function main(): Promise<void> {
54
+ try {
55
+ const [command, ...args] = process.argv.slice(2);
56
+ if (!command || !["watch", "report", "ab", "backtest"].includes(command)) {
57
+ throw new RadarQueryError(
58
+ "Usage: radar:live-watch [watch options], radar:live-report [report options], radar:live-ab [A/B options], or radar:backtest [--max-total-gb N].",
59
+ 2,
60
+ );
61
+ }
62
+ loadLiveDatabaseUrl();
63
+
64
+ if (command === "watch") {
65
+ const run = await runWatch(parseWatchArgs(args));
66
+ const paths = await writeWatchRun(run);
67
+ console.log(JSON.stringify({ ...paths, exitCode: run.exitCode }));
68
+ process.exitCode = run.exitCode;
69
+ } else if (command === "report") {
70
+ const { week, skipJourneySnapshot, maxTotalGb, freshBigQuery } =
71
+ parseReportArgs(args);
72
+ if (!week) {
73
+ throw new RadarQueryError(
74
+ "Usage: radar:live-report --week YYYY-MM-DD [--max-total-gb N]",
75
+ 2,
76
+ );
77
+ }
78
+ const report = await buildWeeklyReport(week, {
79
+ skipJourneySnapshot,
80
+ maxTotalGb,
81
+ freshBigQuery,
82
+ });
83
+ const paths = await writeWeeklyReport(report);
84
+ console.log(formatWeeklyMarkdown(report));
85
+ console.log(JSON.stringify({ ...paths, exitCode: report.exitCode }));
86
+ process.exitCode = report.exitCode;
87
+ } else if (command === "ab") {
88
+ const result = await runAbReadout(parseAbReadoutArgs(args));
89
+ console.log(formatAbMarkdown(result));
90
+ console.log(JSON.stringify(result));
91
+ } else {
92
+ const result = await runRadarBacktest(parseRadarBacktestArgs(args));
93
+ console.log(formatRadarBacktest(result));
94
+ }
95
+ } catch (error) {
96
+ console.error(
97
+ error instanceof Error ? error.message : "Live radar could not run.",
98
+ );
99
+ process.exitCode = radarRunCliExitCode(error);
100
+ } finally {
101
+ await closeRadarQueries();
102
+ }
103
+ }
104
+
105
+ if (
106
+ process.argv[1] &&
107
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
108
+ ) {
109
+ await main();
110
+ }
@@ -14,8 +14,8 @@ export const SNAPSHOT_PRESET = Object.freeze({
14
14
  maxTotalBytesBilled: 275_000_000_000,
15
15
  minWidth: 1100,
16
16
  maxWidth: 1920,
17
- minAspect: 1.25,
18
- maxAspect: 1.8,
17
+ minAspect: 1.2,
18
+ maxAspect: 2.1,
19
19
  requireKnownViewport: true,
20
20
  recency: "newest" as const,
21
21
  settleMs: 300,
@@ -32,6 +32,7 @@ export interface SnapshotExample {
32
32
 
33
33
  export interface SnapshotNode {
34
34
  key: string;
35
+ cohortKey?: string;
35
36
  label: string;
36
37
  parentKey: string | null;
37
38
  depth: number;
@@ -47,7 +48,9 @@ export interface SnapshotNode {
47
48
  export interface SnapshotTree {
48
49
  window: { from: string; to: string };
49
50
  app: string;
51
+ populationGrain?: "sessions" | "signup_events";
50
52
  rootN: number;
53
+ appRootN?: Record<string, number>;
51
54
  coverage: {
52
55
  sessionsWithEvents: number;
53
56
  sessionsWithReplay: number | null;
@@ -60,12 +63,61 @@ export interface SnapshotTree {
60
63
  [key: string]: unknown;
61
64
  }
62
65
 
66
+ export function snapshotPopulationUnit(
67
+ tree: Pick<SnapshotTree, "populationGrain">,
68
+ ): "sessions" | "signup events" {
69
+ return tree.populationGrain === "signup_events"
70
+ ? "signup events"
71
+ : "sessions";
72
+ }
73
+
63
74
  export interface SnapshotBand {
64
75
  key: string;
65
76
  tree: SnapshotTree;
66
77
  }
67
78
 
68
- function arrayItemAt<T>(items: readonly T[], index: number): T | undefined {
79
+ export interface SnapshotSummaryBand extends SnapshotBand {
80
+ label: string;
81
+ previousTree?: SnapshotTree;
82
+ }
83
+
84
+ export function snapshotSummaryBands(
85
+ bands: readonly (SnapshotBand & { label: string })[],
86
+ previousTrees: ReadonlyMap<string, SnapshotTree>,
87
+ ): SnapshotSummaryBand[] {
88
+ return bands.map(({ key, label, tree }) => {
89
+ const previousTree = previousTrees.get(key);
90
+ return {
91
+ key,
92
+ label,
93
+ tree,
94
+ ...(previousTree ? { previousTree } : {}),
95
+ };
96
+ });
97
+ }
98
+
99
+ export function snapshotCanvasTitle(input: {
100
+ app: string;
101
+ week: string;
102
+ experimentId?: string;
103
+ singleBand?: Pick<SnapshotSummaryBand, "key" | "label">;
104
+ hasFirstTouchBands: boolean;
105
+ }): string {
106
+ const suffixes = [
107
+ ...(input.experimentId ? [`Experiment ${input.experimentId}`] : []),
108
+ ...(input.singleBand && input.singleBand.key !== input.app
109
+ ? [input.singleBand.label]
110
+ : input.hasFirstTouchBands
111
+ ? ["first-touch cohorts"]
112
+ : []),
113
+ ];
114
+ return `Onboarding snapshot · ${input.app} · ${input.week}${suffixes.length ? ` · ${suffixes.join(" · ")}` : ""}`;
115
+ }
116
+
117
+ export function arrayItemAt<T>(
118
+ items: readonly T[],
119
+ index: number,
120
+ ): T | undefined {
69
121
  return items.slice(index, index + 1)[0];
70
122
  }
71
123
 
@@ -95,6 +147,12 @@ export function validateSnapshotCostEstimate(
95
147
  return estimatedBytes as number;
96
148
  }
97
149
 
150
+ export function isBuilderCdnUrl(value: unknown): value is string {
151
+ if (typeof value !== "string" || !URL.canParse(value)) return false;
152
+ const url = new URL(value);
153
+ return url.protocol === "https:" && url.hostname === "cdn.builder.io";
154
+ }
155
+
98
156
  export function defaultSnapshotWeek(now = new Date()): string {
99
157
  if (!Number.isFinite(now.valueOf())) {
100
158
  throw new Error("The current date must be valid.");
@@ -173,9 +231,21 @@ export function snapshotCanvasCacheKey(input: {
173
231
  designUrl: string;
174
232
  designScopeHash: string;
175
233
  analyticsScopeHash: string;
234
+ layout: {
235
+ experimentId: string | null;
236
+ cohortKeys: readonly string[];
237
+ };
176
238
  }): string {
177
239
  return createHash("sha256")
178
- .update(JSON.stringify(input))
240
+ .update(
241
+ JSON.stringify({
242
+ ...input,
243
+ layout: {
244
+ ...input.layout,
245
+ cohortKeys: [...input.layout.cohortKeys].sort(),
246
+ },
247
+ }),
248
+ )
179
249
  .digest("hex")
180
250
  .slice(0, 20);
181
251
  }
@@ -363,6 +433,80 @@ export function selectCaptureTree<T extends SnapshotTree>(
363
433
  };
364
434
  }
365
435
 
436
+ export function combineSnapshotBands(
437
+ bands: readonly SnapshotBand[],
438
+ sourceApp?: string,
439
+ ): SnapshotTree {
440
+ const first = arrayItemAt(bands, 0);
441
+ if (bands.length < 2 || !first) {
442
+ throw new Error(
443
+ "At least two cohorts are required for a side-by-side tree.",
444
+ );
445
+ }
446
+ if (sourceApp !== undefined && !/^[a-z][a-z0-9-]{0,127}$/.test(sourceApp)) {
447
+ throw new Error(
448
+ `Invalid source app for side-by-side cohorts: ${sourceApp}`,
449
+ );
450
+ }
451
+ const appRootN: Record<string, number> = {};
452
+ const nodes: SnapshotNode[] = [];
453
+ const nodeKeys = new Set<string>();
454
+
455
+ for (const band of bands) {
456
+ if (!/^[a-z][a-z0-9-]{0,127}$/.test(band.key)) {
457
+ throw new Error(`Invalid side-by-side cohort key: ${band.key}`);
458
+ }
459
+ if (
460
+ band.tree.window.from !== first.tree.window.from ||
461
+ band.tree.window.to !== first.tree.window.to
462
+ ) {
463
+ throw new Error("Side-by-side journey cohorts must use the same window.");
464
+ }
465
+ if (!Number.isSafeInteger(band.tree.rootN) || band.tree.rootN < 1) {
466
+ throw new Error(`Cohort ${band.key} needs a positive root denominator.`);
467
+ }
468
+ const rootKey = sourceApp ? `${sourceApp}::${band.key}` : band.key;
469
+ if (Object.prototype.hasOwnProperty.call(appRootN, rootKey)) {
470
+ throw new Error(`Duplicate side-by-side cohort key: ${rootKey}`);
471
+ }
472
+ appRootN[rootKey] = band.tree.rootN;
473
+ const prefix = sourceApp ? `${sourceApp}::${band.key}` : band.key;
474
+ for (const node of band.tree.nodes) {
475
+ const key = `${prefix}::${node.key}`;
476
+ if (key.length > 2048 || nodeKeys.has(key)) {
477
+ throw new Error(`Side-by-side journey node key is invalid: ${key}`);
478
+ }
479
+ nodeKeys.add(key);
480
+ nodes.push({
481
+ ...node,
482
+ key,
483
+ ...(sourceApp ? { cohortKey: band.key } : {}),
484
+ parentKey: node.parentKey ? `${prefix}::${node.parentKey}` : null,
485
+ });
486
+ }
487
+ }
488
+
489
+ const sessionsWithReplay = bands.reduce<number | null>((total, band) => {
490
+ const count = band.tree.coverage.sessionsWithReplay;
491
+ return total === null || count === null ? null : total + count;
492
+ }, 0);
493
+ return {
494
+ window: first.tree.window,
495
+ app: "all",
496
+ rootN: bands.reduce((total, band) => total + band.tree.rootN, 0),
497
+ appRootN,
498
+ coverage: {
499
+ sessionsWithEvents: bands.reduce(
500
+ (total, band) => total + band.tree.coverage.sessionsWithEvents,
501
+ 0,
502
+ ),
503
+ sessionsWithReplay,
504
+ truncated: bands.some((band) => band.tree.coverage.truncated),
505
+ },
506
+ nodes,
507
+ };
508
+ }
509
+
366
510
  export function allocateFrameBudgets(
367
511
  bands: readonly SnapshotBand[],
368
512
  maxFrames = SNAPSHOT_MAX_FRAMES,