@agent-native/core-corpus 0.221.0 → 0.221.1-nightly-20261028032829

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 (86) 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/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  5. package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -0
  6. package/corpus/templates/analytics/changelog/2026-10-22-outside-assistants-can-open-the-traffic-dashboard-inside-the.md +6 -0
  7. package/corpus/templates/analytics/package.json +4 -0
  8. package/corpus/templates/analytics/scripts/first-touch-radar-live.ts +110 -0
  9. package/corpus/templates/analytics/scripts/radar/ab.ts +10 -0
  10. package/corpus/templates/analytics/scripts/radar/backtest.ts +399 -0
  11. package/corpus/templates/analytics/scripts/radar/campaign.ts +53 -10
  12. package/corpus/templates/analytics/scripts/radar/checks.ts +10 -3
  13. package/corpus/templates/analytics/scripts/radar/live.ts +17 -0
  14. package/corpus/templates/analytics/scripts/radar/query.ts +312 -83
  15. package/corpus/templates/analytics/scripts/radar/report.ts +199 -19
  16. package/corpus/templates/analytics/scripts/radar/sql.ts +89 -18
  17. package/corpus/templates/analytics/scripts/radar/warehouse-sql.ts +322 -21
  18. package/corpus/templates/analytics/scripts/radar/watch.ts +203 -129
  19. package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  20. package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  21. package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  22. package/corpus/templates/calendar/AGENTS.md +1 -0
  23. package/corpus/templates/calendar/actions/manage-event-draft.ts +33 -11
  24. package/corpus/templates/calendar/changelog/2026-10-22-outside-assistants-can-open-invite-drafts-in-calendar-for-re.md +6 -0
  25. package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  26. package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  27. package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  28. package/corpus/templates/content/AGENTS.md +1 -0
  29. package/corpus/templates/content/actions/_preview-document-draft-settlement.ts +31 -0
  30. package/corpus/templates/content/actions/_preview-document-draft.ts +1 -0
  31. package/corpus/templates/content/actions/resolve-preview-document-draft.ts +267 -200
  32. package/corpus/templates/content/actions/share-local-file-document.ts +9 -1
  33. package/corpus/templates/content/actions/update-preview-document-draft.ts +7 -0
  34. package/corpus/templates/content/app/components/editor/CommentAiRecipient.tsx +16 -4
  35. package/corpus/templates/content/app/components/editor/CommentComposer.tsx +1 -1
  36. package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +71 -209
  37. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +358 -24
  38. package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +43 -6
  39. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +52 -14
  40. package/corpus/templates/content/app/components/editor/SuggestionControls.tsx +191 -0
  41. package/corpus/templates/content/app/components/editor/SuggestionReviewCard.tsx +1 -0
  42. package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +80 -91
  43. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -0
  44. package/corpus/templates/content/app/components/editor/comment-ai.tsx +23 -5
  45. package/corpus/templates/content/app/global.css +7 -0
  46. package/corpus/templates/content/app/hooks/use-documents.ts +52 -5
  47. package/corpus/templates/content/app/i18n-data.ts +6 -0
  48. package/corpus/templates/content/app/lib/document-content-hash.ts +16 -0
  49. package/corpus/templates/content/changelog/2026-10-17-choosing-use-saved-after-an-interrupted-recovery-no-longer-l.md +6 -0
  50. package/corpus/templates/content/changelog/2026-10-17-renaming-a-page-while-an-agent-edits-it-no-longer-stops-your.md +6 -0
  51. package/corpus/templates/content/changelog/2026-10-22-outside-assistants-can-open-shareable-local-file-copies-in-c.md +6 -0
  52. package/corpus/templates/content/changelog/2026-10-22-pages-using-the-live-copy-keep-unsent-edits-through-a-refres.md +6 -0
  53. 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
  54. package/corpus/templates/content/changelog/2026-10-26-opening-another-comment-thread-while-you-re-writing-a-new-co.md +6 -0
  55. package/corpus/templates/content/changelog/2026-10-26-when-the-ai-answers-in-a-long-comment-thread-the-card-shows-.md +6 -0
  56. package/corpus/templates/content/changelog/2026-10-27-an-ai-suggestion-in-a-comment-thread-uses-the-same-group-as.md +6 -0
  57. package/corpus/templates/content/changelog/2026-10-27-restoring-page-history-preserves-text-without-exposing-unaut.md +6 -0
  58. package/corpus/templates/content/server/db/schema.ts +1 -0
  59. package/corpus/templates/content/server/lib/private-icon-references.ts +37 -14
  60. package/corpus/templates/content/server/plugins/db.ts +5 -0
  61. package/corpus/templates/content/shared/api.ts +5 -0
  62. package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  63. package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  64. package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +326 -0
  65. package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +6 -0
  66. package/corpus/templates/design/AGENTS.md +3 -1
  67. package/corpus/templates/design/actions/export-coding-handoff.ts +8 -1
  68. package/corpus/templates/design/actions/generate-screens.ts +6 -0
  69. package/corpus/templates/design/changelog/2026-10-22-outside-assistants-can-generate-screens-and-open-coding-hand.md +6 -0
  70. package/corpus/templates/design/server/plugins/agent-chat.ts +3 -1
  71. package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  72. package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +41 -18
  73. package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  74. package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  75. package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  76. package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  77. package/corpus/templates/slides/AGENTS.md +6 -0
  78. package/corpus/templates/slides/actions/clone-context-slide.ts +13 -0
  79. package/corpus/templates/slides/changelog/2026-10-22-outside-assistants-can-clone-a-slide-from-existing-context-a.md +6 -0
  80. package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -0
  81. package/corpus/templates/slides-2/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
  82. package/corpus/templates/slides-2/AGENTS.md +6 -0
  83. package/corpus/templates/slides-2/actions/clone-context-slide.ts +6 -0
  84. package/corpus/templates/slides-2/changelog/2026-10-22-outside-assistants-can-clone-a-slide-from-existing-context-a.md +6 -0
  85. package/corpus/templates/slides-2/server/plugins/agent-chat.ts +1 -0
  86. 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.
@@ -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
+ }
@@ -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
+ }
@@ -133,6 +133,16 @@ export function buildPostgresAbSql(): string {
133
133
  export function buildBigQueryAbSql(eventsTableRange: string): string {
134
134
  const cte = bigQueryFirstTouchEventsCte(eventsTableRange, {
135
135
  includeAllApps: true,
136
+ columns: {
137
+ id: true,
138
+ eventDate: false,
139
+ sessionId: false,
140
+ url: false,
141
+ personKey: true,
142
+ identityEmail: false,
143
+ referralSource: false,
144
+ properties: true,
145
+ },
136
146
  });
137
147
  return `${cte},
138
148
  experiment_events AS (
@@ -0,0 +1,399 @@
1
+ import {
2
+ RadarQueryError,
3
+ resolveRadarBigQueryEventsTable,
4
+ resolveRadarFirstPartyBackend,
5
+ resolveRadarOrganizationId,
6
+ getRadarBigQueryScanBudget,
7
+ runBigQuery,
8
+ setRadarBigQueryScanBudget,
9
+ } from "./query.ts";
10
+ import {
11
+ buildBigQueryEventStatsSql,
12
+ buildBigQueryUploadRejectionSql,
13
+ } from "./warehouse-sql.ts";
14
+ import {
15
+ createEventChecks,
16
+ loadUploadRejectionRows,
17
+ uploadRejectionCheck,
18
+ } from "./watch.ts";
19
+
20
+ interface BacktestWindow {
21
+ start: string;
22
+ end: string;
23
+ }
24
+
25
+ interface Observation {
26
+ check: string;
27
+ app: string;
28
+ period: string;
29
+ status: string;
30
+ numerator: number | null;
31
+ denominator: number | null;
32
+ value: number | null;
33
+ detail: string | null;
34
+ }
35
+
36
+ export interface RadarBacktestResult {
37
+ incident: BacktestWindow;
38
+ clean: BacktestWindow;
39
+ observations: Observation[];
40
+ scanEstimateGb: number;
41
+ maxTotalGb: number;
42
+ }
43
+
44
+ export interface RadarBacktestOptions {
45
+ maxTotalGb?: number;
46
+ }
47
+
48
+ export const DEFAULT_BACKTEST_MAX_TOTAL_GB = 100;
49
+
50
+ const INCIDENT: BacktestWindow = {
51
+ start: "2026-10-09",
52
+ end: "2026-10-23",
53
+ };
54
+ const CLEAN: BacktestWindow = { start: "2026-10-02", end: "2026-10-08" };
55
+ const TRACKED_CHECKS = new Set([
56
+ "builder-connect-success",
57
+ "env-key-share",
58
+ "ai-connection-none",
59
+ "replay-upload-rejection",
60
+ ]);
61
+ const BACKTEST_EVENT_NAMES = [
62
+ "signup",
63
+ "builder_connect_succeeded",
64
+ "builder connect succeeded",
65
+ "builder_connect_failed",
66
+ "builder connect failed",
67
+ "generation_started",
68
+ "design_prompt_submitted",
69
+ ] as const;
70
+
71
+ function addDays(date: string, amount: number): string {
72
+ const result = new Date(`${date}T00:00:00.000Z`);
73
+ result.setUTCDate(result.getUTCDate() + amount);
74
+ return result.toISOString().slice(0, 10);
75
+ }
76
+
77
+ function dateOf(value: unknown): string {
78
+ if (value instanceof Date) return value.toISOString().slice(0, 10);
79
+ const text = String(value ?? "");
80
+ const parsed = new Date(
81
+ text.includes("T") ? text : `${text.replace(" ", "T")}Z`,
82
+ );
83
+ return Number.isFinite(parsed.getTime())
84
+ ? parsed.toISOString().slice(0, 10)
85
+ : text.slice(0, 10);
86
+ }
87
+
88
+ export function backtestHourOf(value: unknown): string {
89
+ if (value instanceof Date) return value.toISOString().slice(0, 13);
90
+ const text = String(value ?? "").replace(" ", "T");
91
+ return text.slice(0, 13);
92
+ }
93
+
94
+ export function noReplayRejectionObservation(
95
+ date: string,
96
+ app: string,
97
+ label: "clean" | "incident",
98
+ ): Observation {
99
+ return {
100
+ check: "replay-upload-rejection",
101
+ app,
102
+ period: `${label}:${date}`,
103
+ status: "insufficient",
104
+ numerator: 0,
105
+ denominator: null,
106
+ value: null,
107
+ detail: `No replay upload rejection events establish recorder coverage for ${app} on ${date}.`,
108
+ };
109
+ }
110
+
111
+ export function parseRadarBacktestArgs(args: readonly string[]): {
112
+ maxTotalGb: number;
113
+ } {
114
+ let maxTotalGb = DEFAULT_BACKTEST_MAX_TOTAL_GB;
115
+ for (let index = 0; index < args.length; index += 1) {
116
+ if (args[index] !== "--max-total-gb") {
117
+ throw new RadarQueryError(`Unknown argument: ${args[index]}`, 2);
118
+ }
119
+ const value = Number(args[index + 1]);
120
+ if (!Number.isFinite(value) || value <= 0) {
121
+ throw new RadarQueryError("--max-total-gb must be positive.", 2);
122
+ }
123
+ maxTotalGb = value;
124
+ index += 1;
125
+ }
126
+ return { maxTotalGb };
127
+ }
128
+
129
+ function datesBetween(window: BacktestWindow): string[] {
130
+ const dates: string[] = [];
131
+ for (let date = window.start; date <= window.end; date = addDays(date, 1)) {
132
+ dates.push(date);
133
+ }
134
+ return dates;
135
+ }
136
+
137
+ function hourKeys(window: BacktestWindow): string[] {
138
+ return datesBetween(window).flatMap((date) =>
139
+ Array.from(
140
+ { length: 24 },
141
+ (_, hour) => `${date}T${String(hour).padStart(2, "0")}`,
142
+ ),
143
+ );
144
+ }
145
+
146
+ function observation(
147
+ check: string,
148
+ app: string,
149
+ period: string,
150
+ result: {
151
+ status: string;
152
+ numerator?: number | null;
153
+ denominator?: number | null;
154
+ value?: number | null;
155
+ detail?: string;
156
+ },
157
+ ): Observation {
158
+ return {
159
+ check,
160
+ app,
161
+ period,
162
+ status: result.status,
163
+ numerator: result.numerator ?? null,
164
+ denominator: result.denominator ?? null,
165
+ value: result.value ?? null,
166
+ detail: result.detail ?? null,
167
+ };
168
+ }
169
+
170
+ export async function runRadarBacktest(
171
+ options: RadarBacktestOptions = {},
172
+ ): Promise<RadarBacktestResult> {
173
+ const maxTotalGb = options.maxTotalGb ?? DEFAULT_BACKTEST_MAX_TOTAL_GB;
174
+ const backend = await resolveRadarFirstPartyBackend();
175
+ if (backend.sink !== "bigquery") {
176
+ throw new RadarQueryError(
177
+ "The historical backtest requires the BigQuery first-party event sink.",
178
+ 2,
179
+ );
180
+ }
181
+ const orgId = await resolveRadarOrganizationId();
182
+ const table = await resolveRadarBigQueryEventsTable();
183
+ const eventStart = addDays(CLEAN.start, -7);
184
+ setRadarBigQueryScanBudget(maxTotalGb);
185
+ try {
186
+ const params = [
187
+ { name: "start_date", type: "DATE" as const, value: eventStart },
188
+ { name: "end_date", type: "DATE" as const, value: INCIDENT.end },
189
+ { name: "org_id", type: "STRING" as const, value: orgId },
190
+ ];
191
+ const eventStats = await runBigQuery(
192
+ buildBigQueryEventStatsSql(table, "BOTH", BACKTEST_EVENT_NAMES),
193
+ {
194
+ maxGb: maxTotalGb,
195
+ fresh: true,
196
+ cache: false,
197
+ params,
198
+ },
199
+ );
200
+ const hourlyRows = eventStats.rows.filter(
201
+ (row) => row.period_grain === "hourly",
202
+ );
203
+ const dailyRows = eventStats.rows.filter(
204
+ (row) => row.period_grain === "daily",
205
+ );
206
+ const observations: Observation[] = [];
207
+
208
+ for (const [label, window] of [
209
+ ["clean", CLEAN],
210
+ ["incident", INCIDENT],
211
+ ] as const) {
212
+ for (const hour of hourKeys(window)) {
213
+ const targetDate = hour.slice(0, 10);
214
+ const hourOfDay = hour.slice(11, 13);
215
+ const baselineStart = addDays(targetDate, -7);
216
+ const relevantHourlyRows = hourlyRows.filter((row) => {
217
+ const period = backtestHourOf(row.period_start);
218
+ const rowDate = dateOf(row.period_start);
219
+ return (
220
+ period.slice(11, 13) === hourOfDay &&
221
+ rowDate >= baselineStart &&
222
+ rowDate <= targetDate
223
+ );
224
+ });
225
+ const checks = createEventChecks(
226
+ relevantHourlyRows,
227
+ [],
228
+ [],
229
+ [],
230
+ [],
231
+ hour.slice(0, 10),
232
+ hour.slice(0, 10),
233
+ hour,
234
+ "hourly",
235
+ ).checks;
236
+ for (const check of checks) {
237
+ if (check.id === "builder-connect-success") {
238
+ observations.push(
239
+ observation(check.id, check.app, `${label}:${hour}`, check),
240
+ );
241
+ }
242
+ }
243
+ }
244
+ for (const date of datesBetween(window)) {
245
+ const relevantDailyRows = dailyRows.filter(
246
+ (row) => dateOf(row.period_start) === date,
247
+ );
248
+ const checks = createEventChecks(
249
+ relevantDailyRows,
250
+ [],
251
+ [],
252
+ [],
253
+ [],
254
+ date,
255
+ date,
256
+ `${date}T00`,
257
+ "daily",
258
+ ).checks;
259
+ for (const check of checks) {
260
+ if (
261
+ (check.id === "env-key-share" ||
262
+ check.id === "ai-connection-none") &&
263
+ (check.id !== "ai-connection-none" || check.app === "slides")
264
+ ) {
265
+ observations.push(
266
+ observation(check.id, check.app, `${label}:${date}`, check),
267
+ );
268
+ }
269
+ }
270
+ }
271
+ }
272
+
273
+ const rejectionResult = await runBigQuery(
274
+ buildBigQueryUploadRejectionSql(table),
275
+ {
276
+ maxGb: maxTotalGb,
277
+ fresh: true,
278
+ cache: false,
279
+ params: [
280
+ { name: "start_date", type: "DATE", value: CLEAN.start },
281
+ { name: "end_date", type: "DATE", value: INCIDENT.end },
282
+ { name: "org_id", type: "STRING", value: orgId },
283
+ ],
284
+ },
285
+ );
286
+ const rejectionRows = rejectionResult.rows;
287
+ const rejectionAttributionRows =
288
+ rejectionRows.length > 0
289
+ ? await loadUploadRejectionRows(
290
+ backend,
291
+ CLEAN.start,
292
+ orgId,
293
+ rejectionRows,
294
+ INCIDENT.end,
295
+ )
296
+ : undefined;
297
+ for (const window of [CLEAN, INCIDENT]) {
298
+ for (const date of datesBetween(window)) {
299
+ const rows = (rejectionAttributionRows ?? []).filter(
300
+ (row) => dateOf(row.event_date) === date,
301
+ );
302
+ const checkedApps = new Set<string>();
303
+ for (const row of rows) {
304
+ const check = uploadRejectionCheck(row);
305
+ if (check) {
306
+ checkedApps.add(check.app);
307
+ const label = date >= INCIDENT.start ? "incident" : "clean";
308
+ observations.push(
309
+ observation(check.id, check.app, `${label}:${date}`, check),
310
+ );
311
+ }
312
+ }
313
+ for (const app of ["design", "slides", "clips"]) {
314
+ if (checkedApps.has(app)) continue;
315
+ const label = date >= INCIDENT.start ? "incident" : "clean";
316
+ observations.push(noReplayRejectionObservation(date, app, label));
317
+ }
318
+ }
319
+ }
320
+
321
+ const scanEstimateGb =
322
+ (getRadarBigQueryScanBudget()?.reservedBytes ?? 0) / 1_000_000_000;
323
+ return {
324
+ incident: INCIDENT,
325
+ clean: CLEAN,
326
+ observations,
327
+ scanEstimateGb,
328
+ maxTotalGb,
329
+ };
330
+ } finally {
331
+ setRadarBigQueryScanBudget(undefined);
332
+ }
333
+ }
334
+
335
+ export function formatRadarBacktest(result: RadarBacktestResult): string {
336
+ const lines = [
337
+ "| Check | Window | App | Fired periods | Quiet periods | Unavailable periods | Insufficient periods | Evidence |",
338
+ "| --- | --- | --- | ---: | ---: | ---: | ---: | --- |",
339
+ ];
340
+ for (const check of TRACKED_CHECKS) {
341
+ for (const window of ["clean", "incident"] as const) {
342
+ const rows = result.observations.filter(
343
+ (row) => row.check === check && row.period.startsWith(`${window}:`),
344
+ );
345
+ const periodRows = new Map<string, Observation>();
346
+ const statusPriority: Record<string, number> = {
347
+ clear: 1,
348
+ observed: 1,
349
+ insufficient: 2,
350
+ unavailable: 3,
351
+ fired: 4,
352
+ };
353
+ for (const row of rows) {
354
+ const key = `${row.period}|${row.app}`;
355
+ const current = periodRows.get(key);
356
+ if (
357
+ !current ||
358
+ (statusPriority[row.status] ?? 0) >
359
+ (statusPriority[current.status] ?? 0)
360
+ ) {
361
+ periodRows.set(key, row);
362
+ }
363
+ }
364
+ const evaluated = [...periodRows.values()];
365
+ const apps = [...new Set(evaluated.map((row) => row.app))].sort();
366
+ const fired = evaluated.filter((row) => row.status === "fired");
367
+ const unavailable = evaluated.filter(
368
+ (row) => row.status === "unavailable",
369
+ );
370
+ const quiet = evaluated.filter(
371
+ (row) => row.status === "clear" || row.status === "observed",
372
+ );
373
+ const insufficient = evaluated.filter(
374
+ (row) => row.status === "insufficient",
375
+ );
376
+ const evidence = [...fired]
377
+ .sort((left, right) => (right.numerator ?? 0) - (left.numerator ?? 0))
378
+ .slice(0, 3)
379
+ .map(
380
+ (row) =>
381
+ `${row.period.slice(window.length + 1)} ${row.app}: ${row.detail ?? `${row.numerator ?? 0}/${row.denominator ?? 0}`}`,
382
+ )
383
+ .join("; ");
384
+ lines.push(
385
+ `| ${check} | ${window} | ${apps.join(", ") || "-"} | ${fired.length} | ${quiet.length} | ${unavailable.length} | ${insufficient.length} | ${evidence || "-"} |`,
386
+ );
387
+ }
388
+ }
389
+ lines.push(
390
+ "",
391
+ `Incident: ${result.incident.start} through ${result.incident.end}; clean baseline: ${result.clean.start} through ${result.clean.end}.`,
392
+ `BigQuery dry-run estimate: ${result.scanEstimateGb.toFixed(2)} GB / ${result.maxTotalGb} GB.`,
393
+ );
394
+ return lines.join("\n");
395
+ }
396
+
397
+ export function trackedRadarBacktestChecks(): string[] {
398
+ return [...TRACKED_CHECKS];
399
+ }