@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/.agents/skills/first-touch-radar/SKILL.md +19 -14
- package/corpus/templates/analytics/.agents/skills/metric-watch/SKILL.md +5 -2
- package/corpus/templates/analytics/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/analytics/actions/open-traffic-dashboard.ts +1 -0
- package/corpus/templates/analytics/changelog/2026-10-22-outside-assistants-can-open-the-traffic-dashboard-inside-the.md +6 -0
- package/corpus/templates/analytics/package.json +4 -0
- package/corpus/templates/analytics/scripts/first-touch-radar-live.ts +110 -0
- package/corpus/templates/analytics/scripts/radar/ab.ts +10 -0
- package/corpus/templates/analytics/scripts/radar/backtest.ts +399 -0
- package/corpus/templates/analytics/scripts/radar/campaign.ts +53 -10
- package/corpus/templates/analytics/scripts/radar/checks.ts +10 -3
- package/corpus/templates/analytics/scripts/radar/live.ts +17 -0
- package/corpus/templates/analytics/scripts/radar/query.ts +312 -83
- package/corpus/templates/analytics/scripts/radar/report.ts +199 -19
- package/corpus/templates/analytics/scripts/radar/sql.ts +89 -18
- package/corpus/templates/analytics/scripts/radar/warehouse-sql.ts +322 -21
- package/corpus/templates/analytics/scripts/radar/watch.ts +203 -129
- package/corpus/templates/assets/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/brain/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/calendar/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/calendar/AGENTS.md +1 -0
- package/corpus/templates/calendar/actions/manage-event-draft.ts +33 -11
- package/corpus/templates/calendar/changelog/2026-10-22-outside-assistants-can-open-invite-drafts-in-calendar-for-re.md +6 -0
- package/corpus/templates/chat/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/clips/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/content/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/content/AGENTS.md +1 -0
- package/corpus/templates/content/actions/_preview-document-draft-settlement.ts +31 -0
- package/corpus/templates/content/actions/_preview-document-draft.ts +1 -0
- package/corpus/templates/content/actions/resolve-preview-document-draft.ts +267 -200
- package/corpus/templates/content/actions/share-local-file-document.ts +9 -1
- package/corpus/templates/content/actions/update-preview-document-draft.ts +7 -0
- package/corpus/templates/content/app/components/editor/CommentAiRecipient.tsx +16 -4
- package/corpus/templates/content/app/components/editor/CommentComposer.tsx +1 -1
- package/corpus/templates/content/app/components/editor/CommentsSidebar.tsx +71 -209
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +358 -24
- package/corpus/templates/content/app/components/editor/PageDraftRecovery.tsx +43 -6
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +52 -14
- package/corpus/templates/content/app/components/editor/SuggestionControls.tsx +191 -0
- package/corpus/templates/content/app/components/editor/SuggestionReviewCard.tsx +1 -0
- package/corpus/templates/content/app/components/editor/ThreadSuggestion.tsx +80 -91
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +4 -0
- package/corpus/templates/content/app/components/editor/comment-ai.tsx +23 -5
- package/corpus/templates/content/app/global.css +7 -0
- package/corpus/templates/content/app/hooks/use-documents.ts +52 -5
- package/corpus/templates/content/app/i18n-data.ts +6 -0
- package/corpus/templates/content/app/lib/document-content-hash.ts +16 -0
- package/corpus/templates/content/changelog/2026-10-17-choosing-use-saved-after-an-interrupted-recovery-no-longer-l.md +6 -0
- package/corpus/templates/content/changelog/2026-10-17-renaming-a-page-while-an-agent-edits-it-no-longer-stops-your.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-outside-assistants-can-open-shareable-local-file-copies-in-c.md +6 -0
- package/corpus/templates/content/changelog/2026-10-22-pages-using-the-live-copy-keep-unsent-edits-through-a-refres.md +6 -0
- 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
- package/corpus/templates/content/changelog/2026-10-26-opening-another-comment-thread-while-you-re-writing-a-new-co.md +6 -0
- package/corpus/templates/content/changelog/2026-10-26-when-the-ai-answers-in-a-long-comment-thread-the-card-shows-.md +6 -0
- package/corpus/templates/content/changelog/2026-10-27-an-ai-suggestion-in-a-comment-thread-uses-the-same-group-as.md +6 -0
- package/corpus/templates/content/changelog/2026-10-27-restoring-page-history-preserves-text-without-exposing-unaut.md +6 -0
- package/corpus/templates/content/server/db/schema.ts +1 -0
- package/corpus/templates/content/server/lib/private-icon-references.ts +37 -14
- package/corpus/templates/content/server/plugins/db.ts +5 -0
- package/corpus/templates/content/shared/api.ts +5 -0
- package/corpus/templates/crm/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/design/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/design/.generated/bridge/native-builtin-metadata.generated.ts +326 -0
- package/corpus/templates/design/.generated/bridge/native-worker-source.generated.ts +6 -0
- package/corpus/templates/design/AGENTS.md +3 -1
- package/corpus/templates/design/actions/export-coding-handoff.ts +8 -1
- package/corpus/templates/design/actions/generate-screens.ts +6 -0
- package/corpus/templates/design/changelog/2026-10-22-outside-assistants-can-generate-screens-and-open-coding-hand.md +6 -0
- package/corpus/templates/design/server/plugins/agent-chat.ts +3 -1
- package/corpus/templates/dispatch/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +41 -18
- package/corpus/templates/forms/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/mail/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/plan/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/slides/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/slides/AGENTS.md +6 -0
- package/corpus/templates/slides/actions/clone-context-slide.ts +13 -0
- package/corpus/templates/slides/changelog/2026-10-22-outside-assistants-can-clone-a-slide-from-existing-context-a.md +6 -0
- package/corpus/templates/slides/server/plugins/agent-chat.ts +1 -0
- package/corpus/templates/slides-2/.deploy-tmp/agent-native-provided-plugins.mjs +10 -0
- package/corpus/templates/slides-2/AGENTS.md +6 -0
- package/corpus/templates/slides-2/actions/clone-context-slide.ts +6 -0
- package/corpus/templates/slides-2/changelog/2026-10-22-outside-assistants-can-clone-a-slide-from-existing-context-a.md +6 -0
- package/corpus/templates/slides-2/server/plugins/agent-chat.ts +1 -0
- package/package.json +1 -1
package/corpus/README.md
CHANGED
|
@@ -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
|
|
14
|
-
|
|
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
|
|
28
|
-
ready. Include counts and the private
|
|
29
|
-
day. Never send Slack or email.
|
|
30
|
-
7. Keep recipient emails and prompt text out of logs, reports,
|
|
31
|
-
commits. The campaign check reads its mode-0600 recipient file
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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({}),
|
|
@@ -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
|
+
}
|