@ainyc/canonry 4.190.1 → 4.192.0
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/assets/agent-workspace/skills/canonry/SKILL.md +124 -216
- package/assets/assets/{AuditHistoryPanel-Dob_85ay.js → AuditHistoryPanel-C7MwfCl9.js} +1 -1
- package/assets/assets/{BacklinksPage-BDm4gQsa.js → BacklinksPage-1Nq1zbRk.js} +1 -1
- package/assets/assets/{ChartPrimitives-DLEICrrL.js → ChartPrimitives-Bxa_vFmq.js} +1 -1
- package/assets/assets/{HistoryPage-BUGJKNWJ.js → HistoryPage-BNACyEiz.js} +1 -1
- package/assets/assets/MeasurementPropertyPage-BqjpIYAw.js +1 -0
- package/assets/assets/ProjectPage-BhBzznlv.js +8 -0
- package/assets/assets/{RunRow-Cja3vahD.js → RunRow-DINPo5ES.js} +1 -1
- package/assets/assets/{RunsPage-BdVfLW9Z.js → RunsPage-BFuDLm7p.js} +1 -1
- package/assets/assets/{SettingsPage-Xw9rdDPd.js → SettingsPage-CkqmyTSK.js} +1 -1
- package/assets/assets/SiteHealthSection-DzFKj9lX.js +4 -0
- package/assets/assets/{TrafficPage-D5ZqJrJm.js → TrafficPage-CkIGVLET.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-DB5V8cdH.js → TrafficSourceDetailPage-CTmn_FmZ.js} +1 -1
- package/assets/assets/{extract-error-message-lzuarNEg.js → extract-error-message-CC2kxd3e.js} +1 -1
- package/assets/assets/{index-BjjM151x.css → index-BtTEcbcB.css} +1 -1
- package/assets/assets/{index-YVHujfy1.js → index-CCPMPGar.js} +26 -26
- package/assets/assets/{react-sigma_core.esm.min-Ddu-c40B.js → react-sigma_core.esm.min-DkpQa1sf.js} +1 -1
- package/assets/assets/{vendor-lucide-dNHw1I5G.js → vendor-lucide-CW2qe0uH.js} +1 -1
- package/assets/assets/{vendor-markdown-9tTeP8v7.js → vendor-markdown-B-Oq2NYq.js} +1 -1
- package/assets/assets/{vendor-radix-BpmvLvpp.js → vendor-radix-twS1M3FF.js} +1 -1
- package/assets/assets/{vendor-recharts-FA0uySU7.js → vendor-recharts-D-cUuLZo.js} +1 -1
- package/assets/assets/{vendor-tanstack-CYBjbqVr.js → vendor-tanstack-ZXXkndVj.js} +1 -1
- package/assets/index.html +7 -7
- package/dist/{chunk-2II4ZMMH.js → chunk-32Q2NZ4S.js} +2 -2
- package/dist/{chunk-32QBVRMN.js → chunk-36RKISAI.js} +213 -32
- package/dist/{chunk-ROPJUNQN.js → chunk-BULKBROE.js} +1368 -1223
- package/dist/{chunk-ECPN4OUR.js → chunk-FTKZAOFD.js} +649 -249
- package/dist/{chunk-BE366UCM.js → chunk-JOSSB4QC.js} +73 -22
- package/dist/cli.js +65 -10
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-QCKX2W4G.js → intelligence-service-MEQ5VX5L.js} +2 -2
- package/dist/mcp.js +13 -8
- package/package.json +11 -11
- package/assets/assets/MeasurementPropertyPage-H2g4VdcA.js +0 -1
- package/assets/assets/ProjectPage-d7iOSkrd.js +0 -8
- package/assets/assets/SiteHealthSection-CjioXMWy.js +0 -4
|
@@ -88,6 +88,8 @@ import {
|
|
|
88
88
|
MeasurementRunScopeError,
|
|
89
89
|
PROJECTS_WRITE_SCOPE,
|
|
90
90
|
READ_ONLY_SCOPE,
|
|
91
|
+
REPORT_VISIBILITY_COPY,
|
|
92
|
+
RESEARCH_RUN_SCOPE,
|
|
91
93
|
ResearchQueryStatuses,
|
|
92
94
|
ResearchRunStatuses,
|
|
93
95
|
RunKinds,
|
|
@@ -381,6 +383,7 @@ import {
|
|
|
381
383
|
inclusiveDayCount,
|
|
382
384
|
indexingRequestResponseDtoSchema,
|
|
383
385
|
internalError,
|
|
386
|
+
intersectScopes,
|
|
384
387
|
isBrowserProvider,
|
|
385
388
|
isLocationRedirectStatus,
|
|
386
389
|
isReadOnlyKey,
|
|
@@ -559,7 +562,16 @@ import {
|
|
|
559
562
|
reportComparisonWindowDays,
|
|
560
563
|
reportConfidenceLabel,
|
|
561
564
|
reportHorizonLabel,
|
|
565
|
+
reportQueryClassLabel,
|
|
562
566
|
reportSeverityLabel,
|
|
567
|
+
reportVisibilityComparison,
|
|
568
|
+
reportVisibilityEvidence,
|
|
569
|
+
reportVisibilityHistoryLabel,
|
|
570
|
+
reportVisibilityLocationLabel,
|
|
571
|
+
reportVisibilityMeasurementLabel,
|
|
572
|
+
reportVisibilityRate,
|
|
573
|
+
researchBatchCreateSchema,
|
|
574
|
+
researchBatchSchema,
|
|
563
575
|
researchDailyLimitExceeded,
|
|
564
576
|
researchRunCreateSchema,
|
|
565
577
|
researchRunDetailSchema,
|
|
@@ -570,6 +582,7 @@ import {
|
|
|
570
582
|
resolveMeasurementRunQueryScope,
|
|
571
583
|
resolveMeasurementRunScope,
|
|
572
584
|
resolveSnapshotRequestQueries,
|
|
585
|
+
restrictedWriteScopes,
|
|
573
586
|
resultsClearRequestSchema,
|
|
574
587
|
resultsClearResponseSchema,
|
|
575
588
|
resultsExportDtoSchema,
|
|
@@ -676,10 +689,10 @@ import {
|
|
|
676
689
|
wordpressSchemaDeployResultDtoSchema,
|
|
677
690
|
wordpressSchemaStatusResultDtoSchema,
|
|
678
691
|
wordpressStatusDtoSchema
|
|
679
|
-
} from "./chunk-
|
|
692
|
+
} from "./chunk-BULKBROE.js";
|
|
680
693
|
|
|
681
694
|
// src/intelligence-service.ts
|
|
682
|
-
import { eq as eq67, desc as desc29, asc as asc12, and as and57, ne as ne10, or as
|
|
695
|
+
import { eq as eq67, desc as desc29, asc as asc12, and as and57, ne as ne10, or as or15, inArray as inArray25, gte as gte18, lte as lte15, isNull as isNull12, sql as sql26, exists } from "drizzle-orm";
|
|
683
696
|
|
|
684
697
|
// ../db/src/client.ts
|
|
685
698
|
import { mkdirSync } from "fs";
|
|
@@ -1181,6 +1194,8 @@ var apiKeys = sqliteTable("api_keys", {
|
|
|
1181
1194
|
* project so a stale scoped key never outlives its project.
|
|
1182
1195
|
*/
|
|
1183
1196
|
projectId: text("project_id").references(() => projects.id, { onDelete: "cascade" }),
|
|
1197
|
+
/** Internal delegated session identity, never accepted by the key-create API. */
|
|
1198
|
+
delegatedUserId: text("delegated_user_id").references(() => users.id, { onDelete: "cascade" }),
|
|
1184
1199
|
createdAt: text("created_at").notNull(),
|
|
1185
1200
|
lastUsedAt: text("last_used_at"),
|
|
1186
1201
|
revokedAt: text("revoked_at")
|
|
@@ -7177,6 +7192,13 @@ var MIGRATION_VERSIONS = [
|
|
|
7177
7192
|
`ALTER TABLE research_runs ADD COLUMN template TEXT`,
|
|
7178
7193
|
`ALTER TABLE research_run_queries ADD COLUMN query_class TEXT`
|
|
7179
7194
|
]
|
|
7195
|
+
},
|
|
7196
|
+
{
|
|
7197
|
+
version: 154,
|
|
7198
|
+
name: "delegated-api-key-user",
|
|
7199
|
+
statements: [
|
|
7200
|
+
`ALTER TABLE api_keys ADD COLUMN delegated_user_id TEXT REFERENCES users(id) ON DELETE CASCADE`
|
|
7201
|
+
]
|
|
7180
7202
|
}
|
|
7181
7203
|
];
|
|
7182
7204
|
function addRunsMeasurementPlanVersionForeignKey(tx) {
|
|
@@ -8471,14 +8493,14 @@ function buildCitationScorecard(snapshots, queryLookup) {
|
|
|
8471
8493
|
const providerRates = providerList.map((provider) => {
|
|
8472
8494
|
const counts2 = providerCounts.get(provider) ?? { cited: 0, mentioned: 0, total: 0 };
|
|
8473
8495
|
const citationRate = counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0;
|
|
8474
|
-
const
|
|
8496
|
+
const mentionRate3 = counts2.total > 0 ? Math.round(counts2.mentioned / counts2.total * 100) : 0;
|
|
8475
8497
|
return {
|
|
8476
8498
|
provider,
|
|
8477
8499
|
citedCount: counts2.cited,
|
|
8478
8500
|
mentionedCount: counts2.mentioned,
|
|
8479
8501
|
totalCount: counts2.total,
|
|
8480
8502
|
citationRate,
|
|
8481
|
-
mentionRate:
|
|
8503
|
+
mentionRate: mentionRate3
|
|
8482
8504
|
};
|
|
8483
8505
|
});
|
|
8484
8506
|
return { queries: queryList, providers: providerList, matrix, providerRates };
|
|
@@ -9635,7 +9657,7 @@ function buildRunHistory(runs2, snapshotsByRunId, limit = DEFAULT_RUN_HISTORY_LI
|
|
|
9635
9657
|
const citedCount = [...queryCited.values()].filter(Boolean).length;
|
|
9636
9658
|
const citationRate = totalCount > 0 ? Math.round(citedCount / totalCount * 100) : 0;
|
|
9637
9659
|
const mentionedCount = [...queryMentioned.values()].filter(Boolean).length;
|
|
9638
|
-
const
|
|
9660
|
+
const mentionRate3 = totalCount > 0 ? Math.round(mentionedCount / totalCount * 100) : 0;
|
|
9639
9661
|
return {
|
|
9640
9662
|
runId: run.id,
|
|
9641
9663
|
createdAt: run.createdAt,
|
|
@@ -9643,7 +9665,7 @@ function buildRunHistory(runs2, snapshotsByRunId, limit = DEFAULT_RUN_HISTORY_LI
|
|
|
9643
9665
|
totalCount,
|
|
9644
9666
|
citationRate,
|
|
9645
9667
|
mentionedCount,
|
|
9646
|
-
mentionRate:
|
|
9668
|
+
mentionRate: mentionRate3,
|
|
9647
9669
|
status: run.status
|
|
9648
9670
|
};
|
|
9649
9671
|
});
|
|
@@ -10795,10 +10817,6 @@ var ADS_MUTATION_SCOPES = /* @__PURE__ */ new Set([
|
|
|
10795
10817
|
ADS_APPROVE_SCOPE,
|
|
10796
10818
|
ADS_ACTIVATE_SCOPE
|
|
10797
10819
|
]);
|
|
10798
|
-
function isAdsMutationOnlyKey(scopes) {
|
|
10799
|
-
const writeGrants = scopes.filter((scope) => scope === "*" || scope === "write" || scope.endsWith(".write") || scope === ADS_APPROVE_SCOPE || scope === ADS_ACTIVATE_SCOPE);
|
|
10800
|
-
return writeGrants.length > 0 && writeGrants.every((scope) => ADS_MUTATION_SCOPES.has(scope));
|
|
10801
|
-
}
|
|
10802
10820
|
function isAdsWriteRoute(url) {
|
|
10803
10821
|
const rest = projectRouteRest(url);
|
|
10804
10822
|
return rest !== null && rest.startsWith("ads/");
|
|
@@ -10829,8 +10847,8 @@ var VIEWER_DENIED_MESSAGE = "Your account has view-only access, so it cannot mak
|
|
|
10829
10847
|
var ADMIN_ONLY_MESSAGE = "Only an administrator account can use this.";
|
|
10830
10848
|
function requireAdminSession(request) {
|
|
10831
10849
|
const principal = request.principal;
|
|
10832
|
-
|
|
10833
|
-
if (
|
|
10850
|
+
const role = principal?.kind === "user" ? principal.role : principal?.delegatedUser?.role;
|
|
10851
|
+
if (!role || role === UserRoles.admin) return;
|
|
10834
10852
|
throw forbidden(ADMIN_ONLY_MESSAGE);
|
|
10835
10853
|
}
|
|
10836
10854
|
function requirePaidReadScope(request) {
|
|
@@ -10841,16 +10859,20 @@ function requirePaidReadScope(request) {
|
|
|
10841
10859
|
throw forbidden("This API key was not granted access to OpenAI Ads paid reads.");
|
|
10842
10860
|
}
|
|
10843
10861
|
}
|
|
10862
|
+
function canRunResearch(request, allowViewers) {
|
|
10863
|
+
const principal = request.principal;
|
|
10864
|
+
if (!principal) return true;
|
|
10865
|
+
if (principal.kind === "user" && principal.role === UserRoles.viewer && !allowViewers) return false;
|
|
10866
|
+
return principal.scopes.includes(WILDCARD_SCOPE) || principal.scopes.includes(RESEARCH_RUN_SCOPE);
|
|
10867
|
+
}
|
|
10844
10868
|
function requireResearchGrant(request, allowViewers) {
|
|
10845
10869
|
const principal = request.principal;
|
|
10846
10870
|
if (!principal) return;
|
|
10847
|
-
if (
|
|
10848
|
-
|
|
10849
|
-
throw forbidden("
|
|
10871
|
+
if (canRunResearch(request, allowViewers)) return;
|
|
10872
|
+
if (principal.kind === "user" && principal.role === UserRoles.viewer && !allowViewers) {
|
|
10873
|
+
throw forbidden("Research queries are not enabled for viewer accounts on this deployment.");
|
|
10850
10874
|
}
|
|
10851
|
-
|
|
10852
|
-
if (principal.role === UserRoles.viewer && allowViewers) return;
|
|
10853
|
-
throw forbidden("Research queries are not enabled for viewer accounts on this deployment.");
|
|
10875
|
+
throw forbidden('This credential was not granted the "research.run" capability.');
|
|
10854
10876
|
}
|
|
10855
10877
|
function requireBroadInstanceKey(request) {
|
|
10856
10878
|
const principal = request.principal;
|
|
@@ -10893,8 +10915,8 @@ function shouldSkipAuth(url) {
|
|
|
10893
10915
|
}
|
|
10894
10916
|
var USERS_READ_SCOPE = "users.read";
|
|
10895
10917
|
var USERS_WRITE_SCOPE = "users.write";
|
|
10896
|
-
function scopesForRole(role) {
|
|
10897
|
-
return role === UserRoles.admin ? [WILDCARD_SCOPE] : [READ_ONLY_SCOPE];
|
|
10918
|
+
function scopesForRole(role, researchAllowViewers = false) {
|
|
10919
|
+
return role === UserRoles.admin ? [WILDCARD_SCOPE] : researchAllowViewers ? [READ_ONLY_SCOPE, RESEARCH_RUN_SCOPE] : [READ_ONLY_SCOPE];
|
|
10898
10920
|
}
|
|
10899
10921
|
function queryValue(request, key) {
|
|
10900
10922
|
const raw = request.query?.[key];
|
|
@@ -10984,7 +11006,7 @@ function enforceEmbedProjectTabs(request, configuredTabs) {
|
|
|
10984
11006
|
if (tabs.includes("report") && isReportRead(url)) return;
|
|
10985
11007
|
throw forbidden("This endpoint is not available for the configured embed tabs.");
|
|
10986
11008
|
}
|
|
10987
|
-
function resolveSignedInPerson(app, request, reply, cookie) {
|
|
11009
|
+
function resolveSignedInPerson(app, request, reply, cookie, researchAllowViewers) {
|
|
10988
11010
|
const sessionId = parseCookieHeader(request.headers.cookie)[USER_SESSION_COOKIE_NAME];
|
|
10989
11011
|
if (!sessionId) return false;
|
|
10990
11012
|
const resolved = resolveUserSession(app.db, sessionId);
|
|
@@ -10993,7 +11015,7 @@ function resolveSignedInPerson(app, request, reply, cookie) {
|
|
|
10993
11015
|
kind: "user",
|
|
10994
11016
|
id: resolved.user.id,
|
|
10995
11017
|
name: resolved.user.name,
|
|
10996
|
-
scopes: scopesForRole(resolved.user.role),
|
|
11018
|
+
scopes: scopesForRole(resolved.user.role, researchAllowViewers),
|
|
10997
11019
|
projectId: null,
|
|
10998
11020
|
role: resolved.user.role,
|
|
10999
11021
|
viaCookie: true
|
|
@@ -11009,12 +11031,24 @@ function resolveSignedInPerson(app, request, reply, cookie) {
|
|
|
11009
11031
|
}
|
|
11010
11032
|
function applyRoleGates(request) {
|
|
11011
11033
|
const principal = request.principal;
|
|
11012
|
-
|
|
11013
|
-
|
|
11034
|
+
const role = principal?.kind === "user" ? principal.role : principal?.delegatedUser?.role;
|
|
11035
|
+
if (!principal || !role) return;
|
|
11036
|
+
request.readSemanticGrant = role === UserRoles.viewer && isReadSemanticRoute(request);
|
|
11037
|
+
applyScopedWriteGates(request);
|
|
11014
11038
|
if (isReadOnlyKey(principal.scopes) && WRITE_METHODS2.has(request.method) && !request.readSemanticGrant && !isPaidReadRoute(request) && !isTransportEnvelopeRoute(request)) {
|
|
11015
11039
|
throw forbidden(VIEWER_DENIED_MESSAGE);
|
|
11016
11040
|
}
|
|
11017
11041
|
}
|
|
11042
|
+
function applyScopedWriteGates(request) {
|
|
11043
|
+
if (!WRITE_METHODS2.has(request.method) || isTransportEnvelopeRoute(request) || request.readSemanticGrant) return;
|
|
11044
|
+
const scopes = principalScopes(request) ?? [];
|
|
11045
|
+
const restricted = restrictedWriteScopes(scopes);
|
|
11046
|
+
if (!restricted) return;
|
|
11047
|
+
const required2 = request.routeOptions.config.writeScope;
|
|
11048
|
+
if (required2 && restricted.includes(required2)) return;
|
|
11049
|
+
if (restricted.some((scope) => ADS_MUTATION_SCOPES.has(scope)) && isAdsWriteRoute(request.url.split("?")[0])) return;
|
|
11050
|
+
throw forbidden("This credential can only perform operations explicitly granted by its scopes.");
|
|
11051
|
+
}
|
|
11018
11052
|
async function authPlugin(app, opts = {}) {
|
|
11019
11053
|
app.addHook("onRequest", async (request, reply) => {
|
|
11020
11054
|
const url = request.url.split("?")[0];
|
|
@@ -11035,10 +11069,9 @@ async function authPlugin(app, opts = {}) {
|
|
|
11035
11069
|
if (granted) {
|
|
11036
11070
|
const account = app.db.select().from(users).where(eq2(users.id, granted.userId)).get();
|
|
11037
11071
|
if (!account) throw authInvalid();
|
|
11038
|
-
const roleScopes = scopesForRole(account.role);
|
|
11072
|
+
const roleScopes = scopesForRole(account.role, opts.researchAllowViewers);
|
|
11039
11073
|
const requested = (granted.scope ?? "").split(/\s+/).filter((part) => part.length > 0 && part !== "offline_access");
|
|
11040
|
-
const
|
|
11041
|
-
const effective = narrowed.length > 0 ? narrowed : [READ_ONLY_SCOPE];
|
|
11074
|
+
const effective = intersectScopes(roleScopes, requested);
|
|
11042
11075
|
request.principal = {
|
|
11043
11076
|
kind: "user",
|
|
11044
11077
|
id: account.id,
|
|
@@ -11055,7 +11088,7 @@ async function authPlugin(app, opts = {}) {
|
|
|
11055
11088
|
}
|
|
11056
11089
|
throw authInvalid();
|
|
11057
11090
|
}
|
|
11058
|
-
} else if (resolveSignedInPerson(app, request, reply, opts.userSessionCookie)) {
|
|
11091
|
+
} else if (resolveSignedInPerson(app, request, reply, opts.userSessionCookie, opts.researchAllowViewers ?? false)) {
|
|
11059
11092
|
applyRoleGates(request);
|
|
11060
11093
|
assertSameOriginWrite(request);
|
|
11061
11094
|
enforceEmbedProjectTabs(request, opts.embedProjectTabs);
|
|
@@ -11078,7 +11111,10 @@ async function authPlugin(app, opts = {}) {
|
|
|
11078
11111
|
throw authRequired();
|
|
11079
11112
|
}
|
|
11080
11113
|
app.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq2(apiKeys.id, key.id)).run();
|
|
11081
|
-
|
|
11114
|
+
let scopes = Array.isArray(key.scopes) ? key.scopes : [];
|
|
11115
|
+
const delegatedUser = key.delegatedUserId ? app.db.select().from(users).where(eq2(users.id, key.delegatedUserId)).get() : void 0;
|
|
11116
|
+
if (key.delegatedUserId && !delegatedUser) throw authInvalid();
|
|
11117
|
+
if (delegatedUser) scopes = intersectScopes(scopesForRole(delegatedUser.role, opts.researchAllowViewers), scopes);
|
|
11082
11118
|
request.apiKey = { id: key.id, name: key.name, scopes, projectId: key.projectId ?? null };
|
|
11083
11119
|
request.principal = {
|
|
11084
11120
|
kind: "api-key",
|
|
@@ -11086,15 +11122,15 @@ async function authPlugin(app, opts = {}) {
|
|
|
11086
11122
|
name: key.name,
|
|
11087
11123
|
scopes,
|
|
11088
11124
|
projectId: key.projectId ?? null,
|
|
11089
|
-
viaCookie: keyArrivedInCookie
|
|
11125
|
+
viaCookie: keyArrivedInCookie,
|
|
11126
|
+
...delegatedUser ? { delegatedUser: { id: delegatedUser.id, name: delegatedUser.name, role: delegatedUser.role } } : {}
|
|
11090
11127
|
};
|
|
11091
11128
|
assertSameOriginWrite(request);
|
|
11092
|
-
|
|
11129
|
+
applyRoleGates(request);
|
|
11130
|
+
applyScopedWriteGates(request);
|
|
11131
|
+
if (isReadOnlyKey(scopes) && WRITE_METHODS2.has(request.method) && !request.readSemanticGrant && !isTransportEnvelopeRoute(request)) {
|
|
11093
11132
|
throw forbidden("This API key is read-only and cannot perform write operations.");
|
|
11094
11133
|
}
|
|
11095
|
-
if (isAdsMutationOnlyKey(scopes) && WRITE_METHODS2.has(request.method) && !isAdsWriteRoute(url)) {
|
|
11096
|
-
throw forbidden("This API key can only perform OpenAI Ads write operations.");
|
|
11097
|
-
}
|
|
11098
11134
|
enforceEmbedProjectTabs(request, opts.embedProjectTabs);
|
|
11099
11135
|
if (key.projectId) {
|
|
11100
11136
|
const match = url.match(/\/projects\/([^/?#]+)/);
|
|
@@ -11149,7 +11185,7 @@ function redirectUriAllowed(registered, presented) {
|
|
|
11149
11185
|
function isLoopbackHost(hostname) {
|
|
11150
11186
|
return hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]" || hostname === "localhost";
|
|
11151
11187
|
}
|
|
11152
|
-
var SUPPORTED_SCOPES = ["read", "offline_access"];
|
|
11188
|
+
var SUPPORTED_SCOPES = ["read", RESEARCH_RUN_SCOPE, "offline_access"];
|
|
11153
11189
|
function resolveRequestedScope(requested) {
|
|
11154
11190
|
if (!requested || !requested.trim()) return "read";
|
|
11155
11191
|
const parts = requested.trim().split(/\s+/);
|
|
@@ -11183,6 +11219,7 @@ function consentPage(o) {
|
|
|
11183
11219
|
<dt>Permissions</dt><dd>${escapeHtml(o.scope)}</dd>
|
|
11184
11220
|
<dt>Redirects to</dt><dd class="uri">${escapeHtml(o.redirectUri)}</dd>
|
|
11185
11221
|
</dl>
|
|
11222
|
+
${o.scope.split(/\s+/).includes(RESEARCH_RUN_SCOPE) ? `<p class="warn">Run research queries using the operator's provider quota, subject to the shared daily research limit. This does not grant permission to change tracking or settings.</p>` : ""}
|
|
11186
11223
|
<input type="hidden" name="csrf" value="${escapeHtml(o.csrf)}">
|
|
11187
11224
|
<div class="row">
|
|
11188
11225
|
<button type="submit" name="approve" value="no" class="ghost">Deny</button>
|
|
@@ -13293,7 +13330,7 @@ function normalizeMeasurementLocation(value) {
|
|
|
13293
13330
|
return normalized || null;
|
|
13294
13331
|
}
|
|
13295
13332
|
function words(value) {
|
|
13296
|
-
return value.normalize("NFKC").
|
|
13333
|
+
return value.normalize("NFKC").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
13297
13334
|
}
|
|
13298
13335
|
function measurementMentionAliasKey(value) {
|
|
13299
13336
|
return words(value).join("\0");
|
|
@@ -13326,10 +13363,11 @@ function indexMentionAliases(aliases) {
|
|
|
13326
13363
|
}
|
|
13327
13364
|
return byFirstWord;
|
|
13328
13365
|
}
|
|
13329
|
-
function mentionedTargetsForAliases(answerText, aliases) {
|
|
13366
|
+
function mentionedTargetsForAliases(answerText, aliases, preparedWords) {
|
|
13330
13367
|
const result = /* @__PURE__ */ new Set();
|
|
13331
|
-
|
|
13332
|
-
|
|
13368
|
+
const unknown = /* @__PURE__ */ new Set();
|
|
13369
|
+
if (answerText === null) return { mentioned: result, unknown };
|
|
13370
|
+
const textWords = preparedWords ?? words(answerText);
|
|
13333
13371
|
for (let start = 0; start < textWords.length; ) {
|
|
13334
13372
|
const matches = (aliases.get(textWords[start]) ?? []).filter((alias2) => aliasMatchesAt(textWords, alias2.words, start));
|
|
13335
13373
|
if (matches.length === 0) {
|
|
@@ -13339,16 +13377,46 @@ function mentionedTargetsForAliases(answerText, aliases) {
|
|
|
13339
13377
|
const longest = Math.max(...matches.map((match) => match.words.length));
|
|
13340
13378
|
const owners = sortedUnique(matches.filter((match) => match.words.length === longest).map((match) => match.targetId));
|
|
13341
13379
|
if (owners.length === 1) result.add(owners[0]);
|
|
13380
|
+
else for (const owner of owners) unknown.add(owner);
|
|
13342
13381
|
start += longest;
|
|
13343
13382
|
}
|
|
13344
|
-
|
|
13383
|
+
for (const owner of result) unknown.delete(owner);
|
|
13384
|
+
return { mentioned: result, unknown };
|
|
13345
13385
|
}
|
|
13346
|
-
function
|
|
13347
|
-
|
|
13386
|
+
function identityAmbiguityPatterns(aliases) {
|
|
13387
|
+
const patterns = aliases.flatMap((alias2) => {
|
|
13388
|
+
const name = words(alias2).join(" ").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13389
|
+
if (!name) return [];
|
|
13390
|
+
return [{
|
|
13391
|
+
clarification: new RegExp(`(?:^| )which (?:specific )?${name} (?:(?:(?:do|did) )?you mean|(?:(?:are|were) you|you(?: re| are| were)?) (?:asking about|referring to|talking about))(?= |$)`, "u"),
|
|
13392
|
+
multipleEntities: [
|
|
13393
|
+
new RegExp(`(?:^| )${name} (?:turns out to |can |could |may )?refers? to (?:a )?(?:few |several |multiple |different )`, "u"),
|
|
13394
|
+
new RegExp(`(?:^| )${name} is (?:a |the )?name shared by(?= |$)`, "u"),
|
|
13395
|
+
new RegExp(`(?:^| )(?:several|multiple|different) (?:places|properties|businesses|communities|entities) (?:called|named) ${name}(?= |$)`, "u")
|
|
13396
|
+
]
|
|
13397
|
+
}];
|
|
13398
|
+
});
|
|
13399
|
+
return {
|
|
13400
|
+
clarification: patterns.map((pattern) => pattern.clarification),
|
|
13401
|
+
multipleEntities: patterns.flatMap((pattern) => pattern.multipleEntities)
|
|
13402
|
+
};
|
|
13348
13403
|
}
|
|
13349
|
-
function
|
|
13350
|
-
|
|
13351
|
-
|
|
13404
|
+
function resolveMentionIdentity(text2, targets, aliases, citedTargetIds, ambiguityPatterns = new Map(targets.map((target) => [target.id, identityAmbiguityPatterns(target.aliases)]))) {
|
|
13405
|
+
const textWords = text2 === null ? [] : words(text2);
|
|
13406
|
+
const state = mentionedTargetsForAliases(text2, aliases, textWords);
|
|
13407
|
+
if (text2 === null) return state;
|
|
13408
|
+
const normalized = textWords.join(" ");
|
|
13409
|
+
for (const target of targets) {
|
|
13410
|
+
if (!state.mentioned.has(target.id)) continue;
|
|
13411
|
+
const patterns = ambiguityPatterns.get(target.id);
|
|
13412
|
+
const qualified = citedTargetIds.has(target.id) || (target.identityAliases?.length ?? 0) > 0 && containsAnyAlias(text2, target.identityAliases);
|
|
13413
|
+
const unresolved = patterns?.clarification.some((pattern) => pattern.test(normalized)) || !qualified && (patterns?.multipleEntities.some((pattern) => pattern.test(normalized)) || (target.identityAliases?.length ?? 0) > 0);
|
|
13414
|
+
if (unresolved) {
|
|
13415
|
+
state.mentioned.delete(target.id);
|
|
13416
|
+
state.unknown.add(target.id);
|
|
13417
|
+
}
|
|
13418
|
+
}
|
|
13419
|
+
return state;
|
|
13352
13420
|
}
|
|
13353
13421
|
function observationSource(observation) {
|
|
13354
13422
|
if (observation.citedUrls !== null) {
|
|
@@ -13364,15 +13432,28 @@ function observationSource(observation) {
|
|
|
13364
13432
|
complete: observation.historicalCitedUrlsComplete === true
|
|
13365
13433
|
};
|
|
13366
13434
|
}
|
|
13367
|
-
function prepareReport(input, options = {}) {
|
|
13435
|
+
function prepareReport(input, options = {}, detailedEvidence = true) {
|
|
13368
13436
|
const ambiguous = /* @__PURE__ */ new Set();
|
|
13369
13437
|
const unmatched = /* @__PURE__ */ new Set();
|
|
13370
13438
|
const bridged = /* @__PURE__ */ new Set();
|
|
13371
13439
|
const candidates = /* @__PURE__ */ new Map();
|
|
13372
13440
|
const mentionAliases = compiledMentionAliases(input.targets);
|
|
13373
13441
|
const aliasesByFirstWord = indexMentionAliases(mentionAliases);
|
|
13442
|
+
const ambiguityPatterns = new Map(input.targets.map((target) => [target.id, identityAmbiguityPatterns(target.aliases)]));
|
|
13443
|
+
const slotsByIdentity = /* @__PURE__ */ new Map();
|
|
13444
|
+
const slotsByLegacyIdentity = /* @__PURE__ */ new Map();
|
|
13445
|
+
for (const slot of input.expectedSlots) {
|
|
13446
|
+
for (const [index2, key] of [
|
|
13447
|
+
[slotsByIdentity, JSON.stringify([slot.executionId, slot.provider])],
|
|
13448
|
+
[slotsByLegacyIdentity, JSON.stringify([slot.queryText, slot.provider, normalizeMeasurementLocation(slot.location)])]
|
|
13449
|
+
]) {
|
|
13450
|
+
const slots = index2.get(key) ?? [];
|
|
13451
|
+
slots.push(slot);
|
|
13452
|
+
index2.set(key, slots);
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13374
13455
|
for (const observation of input.observations) {
|
|
13375
|
-
const slots = observation.executionId !== null ?
|
|
13456
|
+
const slots = (observation.executionId !== null ? slotsByIdentity.get(JSON.stringify([observation.executionId, observation.provider])) : slotsByLegacyIdentity.get(JSON.stringify([observation.queryText, observation.provider, normalizeMeasurementLocation(observation.location)]))) ?? [];
|
|
13376
13457
|
if (slots.length === 0) {
|
|
13377
13458
|
unmatched.add(observation.id);
|
|
13378
13459
|
continue;
|
|
@@ -13409,7 +13490,9 @@ function prepareReport(input, options = {}) {
|
|
|
13409
13490
|
historical: source.historical,
|
|
13410
13491
|
sourceComplete: source.complete,
|
|
13411
13492
|
sourceUrls: source.urls,
|
|
13412
|
-
mentionedTargetIds:
|
|
13493
|
+
mentionedTargetIds: /* @__PURE__ */ new Set(),
|
|
13494
|
+
unknownMentionTargetIds: /* @__PURE__ */ new Set(),
|
|
13495
|
+
citedTargetIds: /* @__PURE__ */ new Set()
|
|
13413
13496
|
});
|
|
13414
13497
|
}
|
|
13415
13498
|
const edgesByExecution = /* @__PURE__ */ new Map();
|
|
@@ -13433,6 +13516,15 @@ function prepareReport(input, options = {}) {
|
|
|
13433
13516
|
mentionAliases.map((alias2) => alias2.targetId)
|
|
13434
13517
|
);
|
|
13435
13518
|
for (const observation of observationsBySlot.values()) {
|
|
13519
|
+
const citedTargetIds = new Set(observation.sourceUrls.flatMap((url) => {
|
|
13520
|
+
const source = sourceAttribution(url);
|
|
13521
|
+
return source.classification === "matched" ? source.matchedTargetIds : [];
|
|
13522
|
+
}));
|
|
13523
|
+
const mentions = resolveMentionIdentity(observation.input.answerText, input.targets, aliasesByFirstWord, citedTargetIds, ambiguityPatterns);
|
|
13524
|
+
observation.mentionedTargetIds = mentions.mentioned;
|
|
13525
|
+
observation.unknownMentionTargetIds = mentions.unknown;
|
|
13526
|
+
observation.citedTargetIds = citedTargetIds;
|
|
13527
|
+
if (!detailedEvidence) continue;
|
|
13436
13528
|
const edges = [...edgesByExecution.get(observation.slot.executionId) ?? []].sort((left, right) => compareText2(left.id, right.id));
|
|
13437
13529
|
for (const edge of edges) {
|
|
13438
13530
|
const sources = observation.sourceUrls.map((sourceUrl) => {
|
|
@@ -13502,6 +13594,17 @@ function prepareReport(input, options = {}) {
|
|
|
13502
13594
|
}
|
|
13503
13595
|
};
|
|
13504
13596
|
}
|
|
13597
|
+
function buildMeasurementObservationSignals(input) {
|
|
13598
|
+
const prepared = prepareReport(input, {}, false);
|
|
13599
|
+
return [...prepared.observationsBySlot.values()].map((observation) => ({
|
|
13600
|
+
observationId: observation.input.id,
|
|
13601
|
+
mentionedTargetIds: [...observation.mentionedTargetIds],
|
|
13602
|
+
unknownMentionTargetIds: [...observation.unknownMentionTargetIds],
|
|
13603
|
+
citedTargetIds: [...observation.citedTargetIds],
|
|
13604
|
+
sourceComplete: observation.sourceComplete,
|
|
13605
|
+
sourceUrls: observation.sourceUrls
|
|
13606
|
+
}));
|
|
13607
|
+
}
|
|
13505
13608
|
function slotsForEdges(expectedSlots, edges) {
|
|
13506
13609
|
const executionIds = new Set(edges.map((edge) => edge.executionId));
|
|
13507
13610
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -13562,6 +13665,9 @@ function mentionRate(target, slots, prepared) {
|
|
|
13562
13665
|
if (!status.complete || !status.answerComplete) {
|
|
13563
13666
|
return { numerator: null, denominator: null, rate: null, reason: "incomplete" };
|
|
13564
13667
|
}
|
|
13668
|
+
if (slots.some((slot) => prepared.observationsBySlot.get(slot.id)?.unknownMentionTargetIds.has(target.id))) {
|
|
13669
|
+
return unavailable("identity-ambiguous");
|
|
13670
|
+
}
|
|
13565
13671
|
return { numerator, denominator: slots.length, rate: numerator / slots.length };
|
|
13566
13672
|
}
|
|
13567
13673
|
function providersFor(slots) {
|
|
@@ -13679,6 +13785,7 @@ function buildMeasurementOverviewIndexes(input, prepared, onEvidenceIndexed) {
|
|
|
13679
13785
|
const answeredSlotIds = /* @__PURE__ */ new Set();
|
|
13680
13786
|
const sourceCompleteSlotIds = /* @__PURE__ */ new Set();
|
|
13681
13787
|
const mentionedSlotIdsByTargetId = /* @__PURE__ */ new Map();
|
|
13788
|
+
const unknownMentionSlotIdsByTargetId = /* @__PURE__ */ new Map();
|
|
13682
13789
|
for (const [slotId, observation] of prepared.observationsBySlot) {
|
|
13683
13790
|
if (observation.sourceComplete) sourceCompleteSlotIds.add(slotId);
|
|
13684
13791
|
if (observation.input.answerText === null) continue;
|
|
@@ -13686,6 +13793,9 @@ function buildMeasurementOverviewIndexes(input, prepared, onEvidenceIndexed) {
|
|
|
13686
13793
|
for (const targetId of observation.mentionedTargetIds) {
|
|
13687
13794
|
addToSet(mentionedSlotIdsByTargetId, targetId, slotId);
|
|
13688
13795
|
}
|
|
13796
|
+
for (const targetId of observation.unknownMentionTargetIds) {
|
|
13797
|
+
addToSet(unknownMentionSlotIdsByTargetId, targetId, slotId);
|
|
13798
|
+
}
|
|
13689
13799
|
}
|
|
13690
13800
|
const assignedSlotIdsByEdgeId = /* @__PURE__ */ new Map();
|
|
13691
13801
|
const ambiguousEvidenceKeysByTargetId = /* @__PURE__ */ new Map();
|
|
@@ -13710,6 +13820,7 @@ function buildMeasurementOverviewIndexes(input, prepared, onEvidenceIndexed) {
|
|
|
13710
13820
|
answeredSlotIds,
|
|
13711
13821
|
sourceCompleteSlotIds,
|
|
13712
13822
|
mentionedSlotIdsByTargetId,
|
|
13823
|
+
unknownMentionSlotIdsByTargetId,
|
|
13713
13824
|
assignedSlotIdsByEdgeId,
|
|
13714
13825
|
ambiguousEvidenceKeysByTargetId
|
|
13715
13826
|
};
|
|
@@ -13733,27 +13844,34 @@ function mentionedForEdge(observation, edge, mentionableIds) {
|
|
|
13733
13844
|
if (edge.type !== "target") return null;
|
|
13734
13845
|
if (observation.input.answerText === null) return null;
|
|
13735
13846
|
if (!mentionableIds.has(edge.targetId)) return null;
|
|
13847
|
+
if (observation.unknownMentionTargetIds.has(edge.targetId)) return null;
|
|
13736
13848
|
return observation.mentionedTargetIds.has(edge.targetId);
|
|
13737
13849
|
}
|
|
13738
13850
|
function mentionableTargets(input, targetIds) {
|
|
13739
13851
|
return input.targets.filter((target) => targetIds.has(target.id) && target.aliases.some((alias2) => words(alias2).length > 0));
|
|
13740
13852
|
}
|
|
13741
|
-
function scopeMentionRate(input, targetIds, slots, answered2, prepared) {
|
|
13853
|
+
function scopeMentionRate(input, targetIds, slots, answered2, prepared, edges) {
|
|
13742
13854
|
const mentionable = mentionableTargets(input, targetIds);
|
|
13743
13855
|
if (mentionable.length === 0) return unavailable("aliasless");
|
|
13744
13856
|
if (slots.length === 0) return unavailable("no-population");
|
|
13745
|
-
if (answered2.length
|
|
13746
|
-
const
|
|
13747
|
-
const
|
|
13748
|
-
|
|
13749
|
-
|
|
13750
|
-
|
|
13857
|
+
if (answered2.length !== slots.length) return unavailable("evidence-incomplete");
|
|
13858
|
+
const mentionableIds = new Set(mentionable.map((target) => target.id));
|
|
13859
|
+
const idsByExecution = /* @__PURE__ */ new Map();
|
|
13860
|
+
for (const edge of edges) if (mentionableIds.has(edge.targetId)) addToSet(idsByExecution, edge.executionId, edge.targetId);
|
|
13861
|
+
let numerator = 0;
|
|
13862
|
+
for (const slot of answered2) {
|
|
13863
|
+
const observation = prepared.observationsBySlot.get(slot.id);
|
|
13864
|
+
const ids = [...idsByExecution.get(slot.executionId) ?? []];
|
|
13865
|
+
if (ids.length === 0) return unavailable("aliasless");
|
|
13866
|
+
if (ids.some((id) => observation.mentionedTargetIds.has(id))) numerator++;
|
|
13867
|
+
else if (ids.some((id) => observation.unknownMentionTargetIds.has(id))) return unavailable("identity-ambiguous");
|
|
13868
|
+
}
|
|
13751
13869
|
return { numerator, denominator: answered2.length, rate: numerator / answered2.length };
|
|
13752
13870
|
}
|
|
13753
13871
|
function indexedScopeCitationRate(slots, edges, indexes) {
|
|
13754
13872
|
if (slots.length === 0) return unavailable("no-population");
|
|
13755
13873
|
const basis = slots.filter((slot) => indexes.sourceCompleteSlotIds.has(slot.id));
|
|
13756
|
-
if (basis.length
|
|
13874
|
+
if (basis.length !== slots.length) return unavailable("evidence-incomplete");
|
|
13757
13875
|
const assignedSlots = /* @__PURE__ */ new Set();
|
|
13758
13876
|
for (const edge of edges) {
|
|
13759
13877
|
for (const slotId of indexes.assignedSlotIdsByEdgeId.get(edge.id) ?? []) assignedSlots.add(slotId);
|
|
@@ -13765,8 +13883,10 @@ function targetMentionRate(targets, slots, answered2, indexes) {
|
|
|
13765
13883
|
const mentionable = targets?.filter((target) => target.aliases.some((alias2) => words(alias2).length > 0)) ?? [];
|
|
13766
13884
|
if (mentionable.length === 0) return unavailable("aliasless");
|
|
13767
13885
|
if (slots.length === 0) return unavailable("no-population");
|
|
13768
|
-
if (answered2.length
|
|
13886
|
+
if (answered2.length !== slots.length) return unavailable("evidence-incomplete");
|
|
13769
13887
|
const mentioned = indexes.mentionedSlotIdsByTargetId.get(mentionable[0].id) ?? /* @__PURE__ */ new Set();
|
|
13888
|
+
const unknown = indexes.unknownMentionSlotIdsByTargetId.get(mentionable[0].id);
|
|
13889
|
+
if (unknown && answered2.some((slot) => unknown.has(slot.id))) return unavailable("identity-ambiguous");
|
|
13770
13890
|
const numerator = answered2.filter((slot) => mentioned.has(slot.id)).length;
|
|
13771
13891
|
return { numerator, denominator: answered2.length, rate: numerator / answered2.length };
|
|
13772
13892
|
}
|
|
@@ -13783,13 +13903,23 @@ function scopeBrandPresence(input, slots, answered2, prepared) {
|
|
|
13783
13903
|
const numerator = presenceIn(answered2, input.projectBrandNames, prepared);
|
|
13784
13904
|
return { numerator, denominator: answered2.length, rate: numerator / answered2.length };
|
|
13785
13905
|
}
|
|
13786
|
-
function scopePropertiesMentioned(input, targetIds, slots, answered2,
|
|
13906
|
+
function scopePropertiesMentioned(input, targetIds, slots, answered2, indexes) {
|
|
13787
13907
|
const mentionable = mentionableTargets(input, targetIds);
|
|
13788
13908
|
if (mentionable.length === 0) return unavailable("aliasless");
|
|
13789
13909
|
if (slots.length === 0) return unavailable("no-population");
|
|
13790
13910
|
if (answered2.length === 0) return unavailable("evidence-incomplete");
|
|
13791
|
-
|
|
13792
|
-
const
|
|
13911
|
+
let numerator = 0;
|
|
13912
|
+
for (const target of mentionable) {
|
|
13913
|
+
const own = indexedSlotsForEdges(indexes.targetEdgesByTargetId.get(target.id) ?? [], indexes);
|
|
13914
|
+
const mentioned = indexes.mentionedSlotIdsByTargetId.get(target.id);
|
|
13915
|
+
if (mentioned && own.some((slot) => mentioned.has(slot.id))) {
|
|
13916
|
+
numerator++;
|
|
13917
|
+
continue;
|
|
13918
|
+
}
|
|
13919
|
+
const unknown = indexes.unknownMentionSlotIdsByTargetId.get(target.id);
|
|
13920
|
+
if (unknown && own.some((slot) => unknown.has(slot.id))) return unavailable("identity-ambiguous");
|
|
13921
|
+
if (own.length === 0 || own.some((slot) => !indexes.answeredSlotIds.has(slot.id))) return unavailable("evidence-incomplete");
|
|
13922
|
+
}
|
|
13793
13923
|
return { numerator, denominator: mentionable.length, rate: numerator / mentionable.length };
|
|
13794
13924
|
}
|
|
13795
13925
|
function scopeNamedShareOfVoice(identities, answered2, prepared) {
|
|
@@ -13846,8 +13976,8 @@ function createMeasurementOverviewEvaluator(input, options = {}) {
|
|
|
13846
13976
|
eligibleSlots: slots.length,
|
|
13847
13977
|
answeredSlots: answered2.length,
|
|
13848
13978
|
includesHistoricalData: prepared.diagnostics.bridgedObservationIds.length > 0 || prepared.diagnostics.historicalObservationIds.length > 0,
|
|
13849
|
-
propertiesMentioned: scopePropertiesMentioned(input, targetIds, slots, answered2,
|
|
13850
|
-
mentionCoverage: scopeMentionRate(input, targetIds, slots, answered2, prepared),
|
|
13979
|
+
propertiesMentioned: scopePropertiesMentioned(input, targetIds, slots, answered2, indexes),
|
|
13980
|
+
mentionCoverage: scopeMentionRate(input, targetIds, slots, answered2, prepared, edges),
|
|
13851
13981
|
citationCoverage: indexedScopeCitationRate(slots, edges, indexes),
|
|
13852
13982
|
brandPresence: scopeBrandPresence(input, slots, answered2, prepared),
|
|
13853
13983
|
namedShareOfVoice: scopeNamedShareOfVoice(namedIdentities ?? [], answered2, prepared),
|
|
@@ -14127,6 +14257,7 @@ function buildMeasurementPlanV2ReportInput(revision, plan, manifest, snapshots)
|
|
|
14127
14257
|
// The revision already decided this Property cannot be mentioned. Feeding
|
|
14128
14258
|
// its aliases in anyway would turn "not applicable" into a 0% reading.
|
|
14129
14259
|
aliases: target.mentionNotApplicable ? [] : target.aliases,
|
|
14260
|
+
...target.identityAliases === void 0 ? {} : { identityAliases: target.identityAliases },
|
|
14130
14261
|
urls: target.urlMatchers.map((matcher, index2) => matcherInput(target.stableKey, matcher, index2))
|
|
14131
14262
|
})),
|
|
14132
14263
|
groups: plan.groups.map((group) => ({
|
|
@@ -14406,6 +14537,8 @@ function activeMeasurementPlan(db, projectId) {
|
|
|
14406
14537
|
}
|
|
14407
14538
|
function metricReason(reason) {
|
|
14408
14539
|
switch (reason) {
|
|
14540
|
+
case "identity-ambiguous":
|
|
14541
|
+
return "identity_ambiguous";
|
|
14409
14542
|
case "no-population":
|
|
14410
14543
|
return "no_population";
|
|
14411
14544
|
case "incomplete":
|
|
@@ -17537,6 +17670,14 @@ function compileMeasurementDraft(authoring, context) {
|
|
|
17537
17670
|
aliasClaims.set(identity, target.stableKey);
|
|
17538
17671
|
});
|
|
17539
17672
|
const aliases = canonicalStrings(target.aliases);
|
|
17673
|
+
const aliasKeys = aliases.map(measurementMentionAliasKey).filter(Boolean);
|
|
17674
|
+
target.identityAliases?.forEach((identityAlias, index2) => {
|
|
17675
|
+
const identityKey = measurementMentionAliasKey(identityAlias);
|
|
17676
|
+
const qualifiesAlias = aliasKeys.some((aliasKey) => identityKey !== aliasKey && `\0${identityKey}\0`.includes(`\0${aliasKey}\0`));
|
|
17677
|
+
if (!qualifiesAlias) {
|
|
17678
|
+
sink.fail("target-identity-alias-unqualified", "An identity phrase must include a Property alias and additional identifying context.", ["targets", targetIndex, "identityAliases", index2]);
|
|
17679
|
+
}
|
|
17680
|
+
});
|
|
17540
17681
|
if (aliases.length === 0) {
|
|
17541
17682
|
sink.warn("target-without-aliases", `Target "${target.label}" has no aliases, so it can be cited but never mentioned.`, ["targets", targetIndex, "aliases"]);
|
|
17542
17683
|
}
|
|
@@ -17545,6 +17686,7 @@ function compileMeasurementDraft(authoring, context) {
|
|
|
17545
17686
|
stableKey: target.stableKey,
|
|
17546
17687
|
label: target.label,
|
|
17547
17688
|
aliases,
|
|
17689
|
+
...target.identityAliases === void 0 ? {} : { identityAliases: canonicalStrings(target.identityAliases) },
|
|
17548
17690
|
urlMatchers: matchers,
|
|
17549
17691
|
mentionNotApplicable: aliases.length === 0,
|
|
17550
17692
|
discoveryIdentity: target.discoveryIdentity ?? null
|
|
@@ -18080,6 +18222,7 @@ function mergeTargets(authoring, body) {
|
|
|
18080
18222
|
const merged = {
|
|
18081
18223
|
...survivor,
|
|
18082
18224
|
aliases: unique([...survivor.aliases, ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey)).flatMap((target) => target.aliases)]),
|
|
18225
|
+
...[survivor, ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey))].some((target) => target.identityAliases !== void 0) ? { identityAliases: unique([...survivor.identityAliases ?? [], ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey)).flatMap((target) => target.identityAliases ?? [])]) } : {},
|
|
18083
18226
|
urlMatchers: unique([...survivor.urlMatchers, ...authoring.targets.filter((target) => absorbedSet.has(target.stableKey)).flatMap((target) => target.urlMatchers)])
|
|
18084
18227
|
};
|
|
18085
18228
|
const assignments = [];
|
|
@@ -19243,6 +19386,7 @@ function seedAuthoring(project, active, context, opts) {
|
|
|
19243
19386
|
label: target.label,
|
|
19244
19387
|
status: "included",
|
|
19245
19388
|
aliases: [...target.aliases],
|
|
19389
|
+
...target.identityAliases === void 0 ? {} : { identityAliases: [...target.identityAliases] },
|
|
19246
19390
|
urlMatchers: target.urlMatchers.map(formatDraftMatcher),
|
|
19247
19391
|
source: target.discoveryIdentity ? "sitemap" : "manual",
|
|
19248
19392
|
...target.discoveryIdentity ? { discoveryIdentity: target.discoveryIdentity } : {}
|
|
@@ -21169,6 +21313,7 @@ function questionRows(plan, target, materialized, filters = {}) {
|
|
|
21169
21313
|
}
|
|
21170
21314
|
const ownEdgesByExecution = new Map(materialized.input.usageEdges.filter((edge) => edge.type === "target" && edge.targetId === targetKey).filter((edge) => assignmentsByExecution.has(edge.executionId)).map((edge) => [edge.executionId, edge]));
|
|
21171
21315
|
const ownEdgeIds = new Set([...ownEdgesByExecution.values()].map((edge) => edge.id));
|
|
21316
|
+
const answerBySlot = new Map(materialized.evidence.answers.filter((answer) => ownEdgeIds.has(answer.usageEdgeId)).map((answer) => [answer.expectedSlotId, answer]));
|
|
21172
21317
|
const citedSlotIds = new Set(materialized.evidence.evidence.filter((row) => ownEdgeIds.has(row.usageEdgeId) && row.classification === "assigned").map((row) => row.expectedSlotId));
|
|
21173
21318
|
const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
|
|
21174
21319
|
const rows = [];
|
|
@@ -21200,8 +21345,8 @@ function questionRows(plan, target, materialized, filters = {}) {
|
|
|
21200
21345
|
servedModel: snapshot?.servedModel ?? null,
|
|
21201
21346
|
location: slot.location,
|
|
21202
21347
|
status: answered2 ? "answered" : "missing",
|
|
21203
|
-
mentioned: answered2 && !target.mentionNotApplicable ?
|
|
21204
|
-
cited: answered2 ?
|
|
21348
|
+
mentioned: answered2 && !target.mentionNotApplicable ? answerBySlot.get(slot.id)?.mentioned ?? null : null,
|
|
21349
|
+
cited: answered2 ? citedSlotIds.has(slot.id) ? true : incompleteObservationIds.has(snapshot.id) ? null : false : null,
|
|
21205
21350
|
recommendedInstead: answered2 ? recommendedInstead(snapshot.recommendedCompetitors, target) : [],
|
|
21206
21351
|
answerExcerpt: answered2 ? excerptOf(answer) : null,
|
|
21207
21352
|
edgeId: edge.id
|
|
@@ -21382,6 +21527,8 @@ function runIdentity(run) {
|
|
|
21382
21527
|
}
|
|
21383
21528
|
function metricReason2(reason) {
|
|
21384
21529
|
switch (reason) {
|
|
21530
|
+
case "identity-ambiguous":
|
|
21531
|
+
return "identity_ambiguous";
|
|
21385
21532
|
case "no-population":
|
|
21386
21533
|
return "no_population";
|
|
21387
21534
|
case "incomplete":
|
|
@@ -21431,6 +21578,7 @@ function targetAnswers(plan, target, materialized, filters) {
|
|
|
21431
21578
|
const assignmentsByExecution = new Map(plan.assignments.filter((assignment) => assignment.targetKey === target.stableKey).filter((assignment) => filters.queryClass === "all" || assignment.queryClass === filters.queryClass).map((assignment) => [assignment.executionNodeKey, assignment]));
|
|
21432
21579
|
const edgesByExecution = new Map(materialized.input.usageEdges.filter((edge) => edge.type === "target" && edge.targetId === target.stableKey).filter((edge) => assignmentsByExecution.has(edge.executionId)).map((edge) => [edge.executionId, edge]));
|
|
21433
21580
|
const ownEdgeIds = new Set([...edgesByExecution.values()].map((edge) => edge.id));
|
|
21581
|
+
const answerBySlot = new Map(materialized.evidence.answers.filter((answer) => ownEdgeIds.has(answer.usageEdgeId)).map((answer) => [answer.expectedSlotId, answer]));
|
|
21434
21582
|
const citedSlotIds = new Set(materialized.evidence.evidence.filter((row) => ownEdgeIds.has(row.usageEdgeId) && row.classification === "assigned").map((row) => row.expectedSlotId));
|
|
21435
21583
|
const incompleteObservationIds = new Set(materialized.evidence.diagnostics.evidenceIncompleteObservationIds);
|
|
21436
21584
|
const questionsById = new Map(plan.querySnapshots.map((question) => [question.queryId, question.queryText]));
|
|
@@ -21449,8 +21597,8 @@ function targetAnswers(plan, target, materialized, filters) {
|
|
|
21449
21597
|
provider: slot.provider,
|
|
21450
21598
|
question: questionsById.get(assignment.queryId) ?? slot.queryText,
|
|
21451
21599
|
snapshot,
|
|
21452
|
-
mentioned: target.mentionNotApplicable ? null :
|
|
21453
|
-
cited:
|
|
21600
|
+
mentioned: target.mentionNotApplicable ? null : answerBySlot.get(slot.id)?.mentioned ?? null,
|
|
21601
|
+
cited: citedSlotIds.has(slot.id) ? true : incompleteObservationIds.has(snapshot.id) ? null : false
|
|
21454
21602
|
});
|
|
21455
21603
|
}
|
|
21456
21604
|
return { expected: slots.length, answers };
|
|
@@ -22065,10 +22213,10 @@ function selectedClasses(value) {
|
|
|
22065
22213
|
function unavailable4(reason) {
|
|
22066
22214
|
return { numerator: null, denominator: null, rate: null, reason };
|
|
22067
22215
|
}
|
|
22068
|
-
function rate(values, expected) {
|
|
22216
|
+
function rate(values, expected, unknownReason = "evidence-incomplete") {
|
|
22069
22217
|
if (expected === 0) return unavailable4("no-population");
|
|
22070
22218
|
if (values.length !== expected) return unavailable4("incomplete");
|
|
22071
|
-
if (values.some((value) => value === null)) return unavailable4(
|
|
22219
|
+
if (values.some((value) => value === null)) return unavailable4(unknownReason);
|
|
22072
22220
|
const denominator = values.length;
|
|
22073
22221
|
if (denominator === 0) return unavailable4("no-population");
|
|
22074
22222
|
const numerator = values.filter(Boolean).length;
|
|
@@ -22170,17 +22318,30 @@ function targetValues(candidate, targets, targetKey) {
|
|
|
22170
22318
|
const targetKeys = targetKey === void 0 ? [...new Set(candidate.edges.map((edge) => edge.targetKey))] : [targetKey];
|
|
22171
22319
|
const eligible = targetKeys.filter((key) => targets.get(key)?.mentionEligible === true);
|
|
22172
22320
|
const observation = candidate.observation;
|
|
22321
|
+
const citation = observation && targetKeys.some((key) => observation.citedTargetKeys.includes(key)) ? true : observation?.citationComplete && targetKeys.length > 0 ? false : null;
|
|
22173
22322
|
const mentionComplete = observation !== null && (observation.mentionComplete ?? observation.answerText !== null);
|
|
22174
22323
|
if (!observation || !mentionComplete || eligible.length === 0) {
|
|
22175
22324
|
return {
|
|
22176
22325
|
mention: null,
|
|
22177
|
-
citation
|
|
22326
|
+
citation
|
|
22178
22327
|
};
|
|
22179
22328
|
}
|
|
22180
|
-
return {
|
|
22181
|
-
|
|
22182
|
-
|
|
22183
|
-
}
|
|
22329
|
+
if (eligible.some((key) => observation.mentionedTargetKeys.includes(key))) return { mention: true, citation };
|
|
22330
|
+
if (eligible.some((key) => observation.unknownMentionTargetKeys?.includes(key))) {
|
|
22331
|
+
return { mention: null, citation, mentionUnavailableReason: "identity-ambiguous" };
|
|
22332
|
+
}
|
|
22333
|
+
return { mention: false, citation };
|
|
22334
|
+
}
|
|
22335
|
+
function citationForCoverage(candidate, targets, targetKey) {
|
|
22336
|
+
return candidate.observation?.citationComplete ? targetValues(candidate, targets, targetKey).citation : null;
|
|
22337
|
+
}
|
|
22338
|
+
function mentionRate2(candidates, targets, targetKey) {
|
|
22339
|
+
const values = candidates.map((candidate) => targetValues(candidate, targets, targetKey));
|
|
22340
|
+
return rate(
|
|
22341
|
+
values.map((value) => value.mention),
|
|
22342
|
+
values.length,
|
|
22343
|
+
values.some((value) => value.mentionUnavailableReason === "identity-ambiguous") ? "identity-ambiguous" : "evidence-incomplete"
|
|
22344
|
+
);
|
|
22184
22345
|
}
|
|
22185
22346
|
function answered(candidate) {
|
|
22186
22347
|
return candidate.observation !== null;
|
|
@@ -22194,21 +22355,30 @@ function selectedTargetKeys(candidates) {
|
|
|
22194
22355
|
function targetMetrics(candidates, targets, targetKey) {
|
|
22195
22356
|
const own = candidates.filter((candidate) => candidate.edges.some((edge) => edge.targetKey === targetKey));
|
|
22196
22357
|
return {
|
|
22197
|
-
mentionCoverage:
|
|
22198
|
-
citationCoverage: rate(own.map((candidate) =>
|
|
22358
|
+
mentionCoverage: mentionRate2(own, targets, targetKey),
|
|
22359
|
+
citationCoverage: rate(own.map((candidate) => citationForCoverage(candidate, targets, targetKey)), own.length)
|
|
22360
|
+
};
|
|
22361
|
+
}
|
|
22362
|
+
function targetPresence(candidates, targets, targetKey) {
|
|
22363
|
+
const values = candidates.filter((candidate) => candidate.edges.some((edge) => edge.targetKey === targetKey)).map((candidate) => targetValues(candidate, targets, targetKey));
|
|
22364
|
+
const anyKnown = (signals) => signals.includes(true) ? true : signals.length === 0 || signals.includes(null) ? null : false;
|
|
22365
|
+
return {
|
|
22366
|
+
targetKey,
|
|
22367
|
+
mention: anyKnown(values.map((value) => value.mention)),
|
|
22368
|
+
citation: anyKnown(values.map((value) => value.citation)),
|
|
22369
|
+
identityAmbiguous: values.some((value) => value.mentionUnavailableReason === "identity-ambiguous")
|
|
22199
22370
|
};
|
|
22200
22371
|
}
|
|
22201
|
-
function outcomeCounts(
|
|
22202
|
-
const counts2 = { bothSignals: 0, mentionedOnly: 0, citedOnly: 0, neither: 0, notMeasured: 0, total:
|
|
22203
|
-
for (const
|
|
22204
|
-
|
|
22205
|
-
if (metrics.mentionCoverage.numerator === null || metrics.citationCoverage.numerator === null) {
|
|
22372
|
+
function outcomeCounts(rows) {
|
|
22373
|
+
const counts2 = { bothSignals: 0, mentionedOnly: 0, citedOnly: 0, neither: 0, notMeasured: 0, total: rows.length };
|
|
22374
|
+
for (const row of rows) {
|
|
22375
|
+
if (row.mention === null || row.citation === null) {
|
|
22206
22376
|
counts2.notMeasured++;
|
|
22207
|
-
} else if (
|
|
22377
|
+
} else if (row.mention && row.citation) {
|
|
22208
22378
|
counts2.bothSignals++;
|
|
22209
|
-
} else if (
|
|
22379
|
+
} else if (row.mention) {
|
|
22210
22380
|
counts2.mentionedOnly++;
|
|
22211
|
-
} else if (
|
|
22381
|
+
} else if (row.citation) {
|
|
22212
22382
|
counts2.citedOnly++;
|
|
22213
22383
|
} else {
|
|
22214
22384
|
counts2.neither++;
|
|
@@ -22223,19 +22393,20 @@ function coverageSummary(candidates, definition, targets = targetMap(definition)
|
|
|
22223
22393
|
rows,
|
|
22224
22394
|
queryCount: new Set(rows.map((candidate) => candidate.slot.queryKey)).size,
|
|
22225
22395
|
answerCount: rows.filter(answered).length,
|
|
22226
|
-
mentionCoverage:
|
|
22227
|
-
citationCoverage: rate(rows.map((candidate) =>
|
|
22396
|
+
mentionCoverage: mentionRate2(rows, targets),
|
|
22397
|
+
citationCoverage: rate(rows.map((candidate) => citationForCoverage(candidate, targets)), rows.length)
|
|
22228
22398
|
};
|
|
22229
22399
|
}
|
|
22230
22400
|
function summary(candidates, definition, populationTargetKeys = selectedTargetKeys(candidates)) {
|
|
22231
22401
|
const targets = targetMap(definition);
|
|
22232
22402
|
const base = coverageSummary(candidates, definition, targets);
|
|
22233
22403
|
const { rows } = base;
|
|
22234
|
-
const targetRows = populationTargetKeys.map((targetKey) => (
|
|
22404
|
+
const targetRows = populationTargetKeys.map((targetKey) => targetPresence(rows, targets, targetKey));
|
|
22235
22405
|
const eligibleTargets = targetRows.filter((row) => targets.get(row.targetKey)?.mentionEligible === true);
|
|
22236
22406
|
const propertyReach = rate(
|
|
22237
|
-
eligibleTargets.map((row) => row.
|
|
22238
|
-
eligibleTargets.length
|
|
22407
|
+
eligibleTargets.map((row) => row.mention),
|
|
22408
|
+
eligibleTargets.length,
|
|
22409
|
+
eligibleTargets.some((row) => row.mention === null && row.identityAmbiguous) ? "identity-ambiguous" : "evidence-incomplete"
|
|
22239
22410
|
);
|
|
22240
22411
|
return {
|
|
22241
22412
|
queryCount: base.queryCount,
|
|
@@ -22243,7 +22414,7 @@ function summary(candidates, definition, populationTargetKeys = selectedTargetKe
|
|
|
22243
22414
|
mentionCoverage: base.mentionCoverage,
|
|
22244
22415
|
citationCoverage: base.citationCoverage,
|
|
22245
22416
|
propertyReach,
|
|
22246
|
-
outcomes: outcomeCounts(
|
|
22417
|
+
outcomes: outcomeCounts(targetRows)
|
|
22247
22418
|
};
|
|
22248
22419
|
}
|
|
22249
22420
|
function queryRows(candidates, definition, selection) {
|
|
@@ -22290,6 +22461,7 @@ function evidenceRows(candidates, definition, selection) {
|
|
|
22290
22461
|
location: candidate.slot.location,
|
|
22291
22462
|
targetKeys: selectedTargetKeys([candidate]),
|
|
22292
22463
|
mentioned: signals.mention,
|
|
22464
|
+
...signals.mentionUnavailableReason === void 0 ? {} : { mentionUnavailableReason: signals.mentionUnavailableReason },
|
|
22293
22465
|
cited: signals.citation,
|
|
22294
22466
|
// Answer bodies are intentionally limited to the stable query-key
|
|
22295
22467
|
// drill-in. A query-id is enough to locate an evidence page, but it
|
|
@@ -22360,8 +22532,8 @@ function breakdown(candidates, definition, populationTargetKeys = selectedTarget
|
|
|
22360
22532
|
const groups = definition.groups.map((group) => ({ group, targetKeys: group.targetKeys.filter((targetKey) => populationTargetSet.has(targetKey)) })).filter(({ targetKeys }) => targetKeys.length > 0).map(({ group, targetKeys }) => {
|
|
22361
22533
|
const groupTargetKeys = new Set(targetKeys);
|
|
22362
22534
|
const own = candidates.map((candidate) => narrowedToTargetKeys(candidate, groupTargetKeys)).filter((candidate) => candidate !== null);
|
|
22363
|
-
const metrics =
|
|
22364
|
-
const citations = rate(own.map((candidate) =>
|
|
22535
|
+
const metrics = mentionRate2(own, targets);
|
|
22536
|
+
const citations = rate(own.map((candidate) => citationForCoverage(candidate, targets)), own.length);
|
|
22365
22537
|
return {
|
|
22366
22538
|
id: group.id,
|
|
22367
22539
|
label: group.label,
|
|
@@ -22756,52 +22928,42 @@ function v2Definition(plan, revision, manifest, run) {
|
|
|
22756
22928
|
edges
|
|
22757
22929
|
};
|
|
22758
22930
|
}
|
|
22759
|
-
function v2Observations(plan, revision, run, snapshots) {
|
|
22931
|
+
function v2Observations(plan, revision, run, snapshots, includeEvidence = true) {
|
|
22760
22932
|
const manifest = measurementRunExpectedSlots(run, plan);
|
|
22761
22933
|
const materialized = buildMeasurementPlanV2ReportInput(revision, plan, manifest, adapterSnapshots(snapshots));
|
|
22762
|
-
const
|
|
22934
|
+
const signalsByObservation = new Map(buildMeasurementObservationSignals(materialized.input).map((signal) => [signal.observationId, signal]));
|
|
22763
22935
|
const rawById = new Map(snapshots.map((snapshot) => [snapshot.id, snapshot]));
|
|
22764
|
-
const answersByObservation = /* @__PURE__ */ new Map();
|
|
22765
|
-
for (const answer of evidence.answers) {
|
|
22766
|
-
const rows = answersByObservation.get(answer.observationId) ?? [];
|
|
22767
|
-
rows.push(answer);
|
|
22768
|
-
answersByObservation.set(answer.observationId, rows);
|
|
22769
|
-
}
|
|
22770
|
-
const edgeById = new Map(materialized.input.usageEdges.map((edge) => [edge.id, edge]));
|
|
22771
22936
|
const competitors2 = exactCompetitorMatchers(plan);
|
|
22772
22937
|
const output = [];
|
|
22773
22938
|
for (const observation of materialized.input.observations) {
|
|
22774
22939
|
const raw = rawById.get(observation.id);
|
|
22775
22940
|
if (!raw || observation.executionId === null) continue;
|
|
22776
|
-
const
|
|
22777
|
-
const
|
|
22778
|
-
const edge = edgeById.get(answer.usageEdgeId);
|
|
22779
|
-
return edge?.type === "target" ? edge.targetId : null;
|
|
22780
|
-
};
|
|
22781
|
-
const signals = competitorSignals(raw, competitors2);
|
|
22941
|
+
const attribution = signalsByObservation.get(observation.id);
|
|
22942
|
+
const signals = includeEvidence ? competitorSignals(raw, competitors2) : { mentioned: [], cited: [] };
|
|
22782
22943
|
output.push({
|
|
22783
22944
|
slotId: `slot:${observation.executionId}:${observation.provider}`,
|
|
22784
22945
|
answerId: raw.id,
|
|
22785
22946
|
model: raw.servedModel?.trim() || null,
|
|
22786
|
-
answerText: raw.answerText,
|
|
22947
|
+
answerText: includeEvidence ? raw.answerText : null,
|
|
22787
22948
|
// `answerMentioned` is the legacy project-level boolean. It cannot say
|
|
22788
22949
|
// WHICH Property an answer named, so a portfolio may use it neither as
|
|
22789
22950
|
// a positive nor as a measured negative when the answer body is absent.
|
|
22790
22951
|
mentionComplete: raw.answerText !== null,
|
|
22791
|
-
mentionedTargetKeys:
|
|
22792
|
-
|
|
22793
|
-
|
|
22952
|
+
mentionedTargetKeys: attribution?.mentionedTargetIds ?? [],
|
|
22953
|
+
unknownMentionTargetKeys: attribution?.unknownMentionTargetIds ?? [],
|
|
22954
|
+
citedTargetKeys: attribution?.citedTargetIds ?? [],
|
|
22955
|
+
citationComplete: attribution?.sourceComplete ?? false,
|
|
22794
22956
|
competitorMentionDomains: signals.mentioned,
|
|
22795
22957
|
competitorCitationDomains: signals.cited,
|
|
22796
|
-
observedCompetitorNames: observedCompetitorNames2(raw.recommendedCompetitors),
|
|
22797
|
-
sources:
|
|
22958
|
+
observedCompetitorNames: includeEvidence ? observedCompetitorNames2(raw.recommendedCompetitors) : [],
|
|
22959
|
+
sources: includeEvidence ? attribution?.sourceUrls ?? [] : [],
|
|
22798
22960
|
createdAt: raw.createdAt
|
|
22799
22961
|
});
|
|
22800
22962
|
}
|
|
22801
22963
|
return { manifest, observations: output };
|
|
22802
22964
|
}
|
|
22803
|
-
function advancedRun(run, version, plan, snapshots, comparableDefinitionIds) {
|
|
22804
|
-
const populated = v2Observations(plan, version.revision, run, snapshots);
|
|
22965
|
+
function advancedRun(run, version, plan, snapshots, comparableDefinitionIds, includeEvidence = true) {
|
|
22966
|
+
const populated = v2Observations(plan, version.revision, run, snapshots, includeEvidence);
|
|
22805
22967
|
return {
|
|
22806
22968
|
id: run.id,
|
|
22807
22969
|
createdAt: run.createdAt,
|
|
@@ -22848,7 +23010,7 @@ function simpleModelFingerprint(definition) {
|
|
|
22848
23010
|
const engines = [...definition.engines].map((engine) => ({ provider: normalizedText3(engine.provider), requestedModel: engine.requestedModel })).sort((left, right) => left.provider.localeCompare(right.provider, "en"));
|
|
22849
23011
|
return `simple-model:${createHash4("sha256").update(JSON.stringify(engines)).digest("hex")}`;
|
|
22850
23012
|
}
|
|
22851
|
-
function frozenSimpleRun(run, definition, snapshots) {
|
|
23013
|
+
function frozenSimpleRun(run, definition, snapshots, includeEvidence = true) {
|
|
22852
23014
|
const matcher = compileBrandAliases(effectiveBrandNames({
|
|
22853
23015
|
displayName: definition.identity.displayName,
|
|
22854
23016
|
aliases: definition.identity.aliases,
|
|
@@ -22896,20 +23058,20 @@ function frozenSimpleRun(run, definition, snapshots) {
|
|
|
22896
23058
|
if (!slotKeys.has(slotId)) throw new Error(`Frozen simple definition has no slot for stored snapshot ${snapshot.id}`);
|
|
22897
23059
|
const preparedAnswerText = prepareBrandMatchText(snapshot.answerText);
|
|
22898
23060
|
const mentioned = snapshot.answerText !== null ? matcherMatchesText(matcher, preparedAnswerText) : snapshot.answerMentioned === true;
|
|
22899
|
-
const competitorSignalsForSnapshot = competitors2 === null ? { mentioned: [], cited: [] } : competitorSignals(snapshot, competitors2, preparedAnswerText);
|
|
23061
|
+
const competitorSignalsForSnapshot = !includeEvidence || competitors2 === null ? { mentioned: [], cited: [] } : competitorSignals(snapshot, competitors2, preparedAnswerText);
|
|
22900
23062
|
observations.push({
|
|
22901
23063
|
slotId,
|
|
22902
23064
|
answerId: snapshot.id,
|
|
22903
23065
|
model: snapshot.servedModel?.trim() || null,
|
|
22904
|
-
answerText: snapshot.answerText,
|
|
23066
|
+
answerText: includeEvidence ? snapshot.answerText : null,
|
|
22905
23067
|
mentionComplete: snapshot.answerText !== null || snapshot.answerMentioned !== null,
|
|
22906
23068
|
mentionedTargetKeys: mentioned ? ["project"] : [],
|
|
22907
23069
|
citedTargetKeys: snapshot.citationState === "cited" ? ["project"] : [],
|
|
22908
23070
|
citationComplete: true,
|
|
22909
23071
|
competitorMentionDomains: competitorSignalsForSnapshot.mentioned,
|
|
22910
23072
|
competitorCitationDomains: competitorSignalsForSnapshot.cited,
|
|
22911
|
-
observedCompetitorNames: observedCompetitorNames2(snapshot.recommendedCompetitors),
|
|
22912
|
-
sources: snapshot.citedUrls ?? [],
|
|
23073
|
+
observedCompetitorNames: includeEvidence ? observedCompetitorNames2(snapshot.recommendedCompetitors) : [],
|
|
23074
|
+
sources: includeEvidence ? snapshot.citedUrls ?? [] : [],
|
|
22913
23075
|
createdAt: snapshot.createdAt
|
|
22914
23076
|
});
|
|
22915
23077
|
}
|
|
@@ -22941,7 +23103,7 @@ function frozenSimpleRun(run, definition, snapshots) {
|
|
|
22941
23103
|
observations
|
|
22942
23104
|
};
|
|
22943
23105
|
}
|
|
22944
|
-
function legacySimpleRun(project, run, snapshots) {
|
|
23106
|
+
function legacySimpleRun(project, run, snapshots, includeEvidence = true) {
|
|
22945
23107
|
const slots = snapshots.map((snapshot) => ({
|
|
22946
23108
|
id: `slot:legacy:${snapshot.id}`,
|
|
22947
23109
|
executionId: `legacy:${snapshot.id}`,
|
|
@@ -22979,7 +23141,7 @@ function legacySimpleRun(project, run, snapshots) {
|
|
|
22979
23141
|
slotId: `slot:legacy:${snapshot.id}`,
|
|
22980
23142
|
answerId: snapshot.id,
|
|
22981
23143
|
model: snapshot.servedModel?.trim() || null,
|
|
22982
|
-
answerText: snapshot.answerText,
|
|
23144
|
+
answerText: includeEvidence ? snapshot.answerText : null,
|
|
22983
23145
|
// A legacy answer body cannot be matched against today's identity. Only
|
|
22984
23146
|
// the stored boolean is a frozen mention fact; null stays incomplete.
|
|
22985
23147
|
mentionComplete: snapshot.answerMentioned !== null,
|
|
@@ -22988,8 +23150,8 @@ function legacySimpleRun(project, run, snapshots) {
|
|
|
22988
23150
|
citationComplete: true,
|
|
22989
23151
|
competitorMentionDomains: [],
|
|
22990
23152
|
competitorCitationDomains: [],
|
|
22991
|
-
observedCompetitorNames: observedCompetitorNames2(snapshot.recommendedCompetitors),
|
|
22992
|
-
sources: snapshot.citedUrls ?? [],
|
|
23153
|
+
observedCompetitorNames: includeEvidence ? observedCompetitorNames2(snapshot.recommendedCompetitors) : [],
|
|
23154
|
+
sources: includeEvidence ? snapshot.citedUrls ?? [] : [],
|
|
22993
23155
|
createdAt: snapshot.createdAt
|
|
22994
23156
|
}))
|
|
22995
23157
|
};
|
|
@@ -23127,41 +23289,37 @@ function advancedReaderInput(db, projectId, active, query) {
|
|
|
23127
23289
|
const presentationComparableIds = new Set(comparableVersionIds(allVersionRowsById, presentationVersion.id));
|
|
23128
23290
|
const activeComparableIds = new Set(comparableVersionIds(allVersionRowsById, active.version.id));
|
|
23129
23291
|
const sourceRuns = completedVisibilityRuns(db, projectId, false, query);
|
|
23130
|
-
const snapshotsByRun = loadVisibilitySnapshots(db, sourceRuns.map((run) => run.id));
|
|
23131
23292
|
const sourceCandidates = sourceRuns.flatMap((run) => {
|
|
23132
23293
|
if (run.measurementPlanVersionId === null) return [];
|
|
23133
23294
|
const source = versions.get(run.measurementPlanVersionId);
|
|
23134
|
-
if (!source) return [];
|
|
23135
|
-
|
|
23136
|
-
|
|
23137
|
-
source.row,
|
|
23138
|
-
source.plan,
|
|
23139
|
-
snapshotsByRun.get(run.id) ?? [],
|
|
23140
|
-
comparableVersionIds(allVersionRowsById, source.row.id)
|
|
23141
|
-
);
|
|
23142
|
-
return [{ run, source, own }];
|
|
23143
|
-
}).filter((candidate) => query.revision === void 0 || presentationComparableIds.has(candidate.source.row.id));
|
|
23295
|
+
if (!source || query.revision !== void 0 && !presentationComparableIds.has(source.row.id)) return [];
|
|
23296
|
+
return [{ run, source }];
|
|
23297
|
+
});
|
|
23144
23298
|
if (query.runId !== void 0 && !sourceCandidates.some((candidate) => candidate.run.id === query.runId)) {
|
|
23145
23299
|
throw validationError(`Measurement run "${query.runId}" is not an eligible advanced result.`);
|
|
23146
23300
|
}
|
|
23301
|
+
const compatibleSourceCandidates = sourceCandidates.filter((candidate) => activeComparableIds.has(candidate.source.row.id));
|
|
23302
|
+
const preferredSource = query.runId === void 0 ? compatibleSourceCandidates.at(0) ?? sourceCandidates.at(0) : void 0;
|
|
23303
|
+
const selectedSource = query.runId === void 0 ? preferredSource : sourceCandidates.find((candidate) => candidate.run.id === query.runId);
|
|
23147
23304
|
const candidates = sourceCandidates.map((candidate) => {
|
|
23148
|
-
|
|
23149
|
-
|
|
23305
|
+
const usePresentation = presentationComparableIds.has(candidate.source.row.id);
|
|
23306
|
+
const version = usePresentation ? presentationVersion : candidate.source.row;
|
|
23307
|
+
const plan = usePresentation ? presentationPlan : candidate.source.plan;
|
|
23308
|
+
const snapshots = loadVisibilitySnapshots(db, [candidate.run.id]).get(candidate.run.id) ?? [];
|
|
23150
23309
|
return advancedRun(
|
|
23151
23310
|
candidate.run,
|
|
23152
|
-
|
|
23153
|
-
|
|
23154
|
-
|
|
23155
|
-
[...presentationComparableIds]
|
|
23311
|
+
version,
|
|
23312
|
+
plan,
|
|
23313
|
+
snapshots,
|
|
23314
|
+
usePresentation ? [...presentationComparableIds] : comparableVersionIds(allVersionRowsById, candidate.source.row.id),
|
|
23315
|
+
candidate.run.id === selectedSource?.run.id
|
|
23156
23316
|
);
|
|
23157
23317
|
});
|
|
23158
|
-
const
|
|
23159
|
-
const preferredSource = query.runId === void 0 ? compatibleSourceCandidates.at(0) ?? sourceCandidates.at(0) : void 0;
|
|
23160
|
-
const selectedSource = query.runId === void 0 ? preferredSource : sourceCandidates.find((candidate) => candidate.run.id === query.runId);
|
|
23318
|
+
const selectedDefinition = candidates.find((candidate) => candidate.id === selectedSource?.run.id)?.definition;
|
|
23161
23319
|
return {
|
|
23162
23320
|
mode: "advanced",
|
|
23163
23321
|
activeRevision: active.version.revision,
|
|
23164
|
-
pendingAssignmentCount: selectedSource !== void 0 && activeComparableIds.has(selectedSource.source.row.id) ? 0 : pendingAssignments(active.plan, selectedSource?.source.plan,
|
|
23322
|
+
pendingAssignmentCount: selectedSource !== void 0 && activeComparableIds.has(selectedSource.source.row.id) ? 0 : pendingAssignments(active.plan, selectedSource?.source.plan, selectedDefinition),
|
|
23165
23323
|
selection: query,
|
|
23166
23324
|
activeDefinition: activeV2Definition(presentationPlan, presentationVersion.revision),
|
|
23167
23325
|
...preferredSource === void 0 ? {} : { preferredRunId: preferredSource.run.id },
|
|
@@ -23174,11 +23332,12 @@ function simpleReaderInput(db, project, query) {
|
|
|
23174
23332
|
eq24(simpleMeasurementDefinitions.projectId, project.id),
|
|
23175
23333
|
inArray9(simpleMeasurementDefinitions.runId, sourceRuns.map((run) => run.id))
|
|
23176
23334
|
)).all().map((row) => [row.runId, row.definition]));
|
|
23177
|
-
const
|
|
23335
|
+
const selectedRunId = sourceRuns[0]?.id;
|
|
23178
23336
|
const candidates = sourceRuns.map((run) => {
|
|
23179
|
-
const snapshots =
|
|
23337
|
+
const snapshots = loadVisibilitySnapshots(db, [run.id]).get(run.id) ?? [];
|
|
23180
23338
|
const definition = frozen.get(run.id);
|
|
23181
|
-
|
|
23339
|
+
const includeEvidence = run.id === selectedRunId;
|
|
23340
|
+
return definition ? frozenSimpleRun(run, definition, snapshots, includeEvidence) : legacySimpleRun(project, run, snapshots, includeEvidence);
|
|
23182
23341
|
});
|
|
23183
23342
|
if (query.runId !== void 0 && !candidates.some((run) => run.id === query.runId)) {
|
|
23184
23343
|
throw validationError(`Measurement run "${query.runId}" is not an eligible simple result.`);
|
|
@@ -23192,37 +23351,28 @@ function simpleReaderInput(db, project, query) {
|
|
|
23192
23351
|
runs: candidates
|
|
23193
23352
|
};
|
|
23194
23353
|
}
|
|
23354
|
+
function readVisibilityReport(db, project, rawQuery) {
|
|
23355
|
+
const query = parseQuery(rawQuery);
|
|
23356
|
+
const active = activeMeasurementPlan(db, project.id);
|
|
23357
|
+
const mode = query.mode === "auto" ? active === null ? "simple" : "advanced" : query.mode;
|
|
23358
|
+
try {
|
|
23359
|
+
if (mode === "advanced") {
|
|
23360
|
+
if (active === null) throw validationError("This project has no advanced measurement plan.");
|
|
23361
|
+
if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) return unsupportedAdvancedResponse(query, active);
|
|
23362
|
+
return buildVisibilityReport(advancedReaderInput(db, project.id, active, query));
|
|
23363
|
+
}
|
|
23364
|
+
return buildVisibilityReport(simpleReaderInput(db, project, query));
|
|
23365
|
+
} catch (error) {
|
|
23366
|
+
if (error instanceof VisibilityReportCursorError || error instanceof VisibilityReportScopeError) {
|
|
23367
|
+
throw validationError(error.message);
|
|
23368
|
+
}
|
|
23369
|
+
throw error;
|
|
23370
|
+
}
|
|
23371
|
+
}
|
|
23195
23372
|
async function visibilityReportRoutes(app) {
|
|
23196
23373
|
app.get(
|
|
23197
23374
|
"/projects/:name/visibility-report",
|
|
23198
|
-
async (request) =>
|
|
23199
|
-
const project = resolveProject(app.db, request.params.name);
|
|
23200
|
-
const query = parseQuery(request.query);
|
|
23201
|
-
const active = activeMeasurementPlan(app.db, project.id);
|
|
23202
|
-
const mode = query.mode === "auto" ? active === null ? "simple" : "advanced" : query.mode;
|
|
23203
|
-
if (mode === "advanced") {
|
|
23204
|
-
if (active === null) throw validationError("This project has no advanced measurement plan.");
|
|
23205
|
-
if (active.plan.schemaVersion !== MEASUREMENT_PLAN_V2_SCHEMA_VERSION) {
|
|
23206
|
-
return unsupportedAdvancedResponse(query, active);
|
|
23207
|
-
}
|
|
23208
|
-
try {
|
|
23209
|
-
return buildVisibilityReport(advancedReaderInput(app.db, project.id, active, query));
|
|
23210
|
-
} catch (error) {
|
|
23211
|
-
if (error instanceof VisibilityReportCursorError || error instanceof VisibilityReportScopeError) {
|
|
23212
|
-
throw validationError(error.message);
|
|
23213
|
-
}
|
|
23214
|
-
throw error;
|
|
23215
|
-
}
|
|
23216
|
-
}
|
|
23217
|
-
try {
|
|
23218
|
-
return buildVisibilityReport(simpleReaderInput(app.db, project, query));
|
|
23219
|
-
} catch (error) {
|
|
23220
|
-
if (error instanceof VisibilityReportCursorError || error instanceof VisibilityReportScopeError) {
|
|
23221
|
-
throw validationError(error.message);
|
|
23222
|
-
}
|
|
23223
|
-
throw error;
|
|
23224
|
-
}
|
|
23225
|
-
}
|
|
23375
|
+
async (request) => readVisibilityReport(app.db, resolveProject(app.db, request.params.name), request.query)
|
|
23226
23376
|
);
|
|
23227
23377
|
}
|
|
23228
23378
|
|
|
@@ -23471,6 +23621,7 @@ function frozenAssignmentSignature(plan, assignment) {
|
|
|
23471
23621
|
target: {
|
|
23472
23622
|
stableKey: target.stableKey,
|
|
23473
23623
|
aliases: target.aliases,
|
|
23624
|
+
...target.identityAliases === void 0 ? {} : { identityAliases: target.identityAliases },
|
|
23474
23625
|
urlMatchers: target.urlMatchers,
|
|
23475
23626
|
mentionNotApplicable: target.mentionNotApplicable,
|
|
23476
23627
|
discoveryIdentity: target.discoveryIdentity
|
|
@@ -23972,6 +24123,7 @@ function compileExecution(state, target, queryId, queryText, queryClass, classif
|
|
|
23972
24123
|
label: target.label,
|
|
23973
24124
|
status: "included",
|
|
23974
24125
|
aliases: [...target.aliases],
|
|
24126
|
+
...target.identityAliases === void 0 ? {} : { identityAliases: [...target.identityAliases] },
|
|
23975
24127
|
urlMatchers: target.urlMatchers.map(draftMatcher),
|
|
23976
24128
|
source: target.discoveryIdentity ? "sitemap" : "manual",
|
|
23977
24129
|
...target.discoveryIdentity ? { discoveryIdentity: target.discoveryIdentity } : {}
|
|
@@ -28735,10 +28887,10 @@ function renderExecutiveSummary(report) {
|
|
|
28735
28887
|
const trendLabel = s.trend === "up" ? "\u2191 Up" : s.trend === "down" ? "\u2193 Down" : s.trend === "flat" ? "\u2192 Flat" : "\u2014";
|
|
28736
28888
|
const trendTone = s.trend === "up" ? "positive" : s.trend === "down" ? "negative" : "neutral";
|
|
28737
28889
|
const queryNoun = s.totalQueryCount === 1 ? "query" : "queries";
|
|
28738
|
-
const citedFragment = s.totalQueryCount > 0 ? `${s.citedQueryCount}/${s.totalQueryCount} ${queryNoun} cited` : "no queries";
|
|
28739
|
-
const mentionedFragment = s.totalQueryCount > 0 ? `${s.mentionedQueryCount}/${s.totalQueryCount} ${queryNoun} mentioned` : "no queries";
|
|
28740
|
-
const headlineTitle = s.totalQueryCount > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} cite ${report.meta.project.displayName}` : "No AI citation data yet";
|
|
28741
|
-
const headlineSubtitle = s.totalQueryCount > 0 ? `${s.citationRate}% citation coverage and ${s.mentionRate}% mention coverage across ${s.providerCount} ${pluralize(s.providerCount, "provider")}.` : "Run a check to populate the first citation and mention baseline.";
|
|
28890
|
+
const citedFragment = (s.totalQueryCount ?? 0) > 0 ? `${s.citedQueryCount}/${s.totalQueryCount} ${queryNoun} cited` : "no queries";
|
|
28891
|
+
const mentionedFragment = (s.totalQueryCount ?? 0) > 0 ? `${s.mentionedQueryCount}/${s.totalQueryCount} ${queryNoun} mentioned` : "no queries";
|
|
28892
|
+
const headlineTitle = (s.totalQueryCount ?? 0) > 0 ? `${s.citedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} cite ${report.meta.project.displayName}` : "No AI citation data yet";
|
|
28893
|
+
const headlineSubtitle = (s.totalQueryCount ?? 0) > 0 ? `${s.citationRate}% citation coverage and ${s.mentionRate}% mention coverage across ${s.providerCount} ${pluralize(s.providerCount, "provider")}.` : "Run a check to populate the first citation and mention baseline.";
|
|
28742
28894
|
const priorityActions = report.agencyDiagnostics.priorities.length > 0 ? report.agencyDiagnostics.priorities : report.actionPlan;
|
|
28743
28895
|
const actionCount = dedupeReportActions(report, priorityActions).length;
|
|
28744
28896
|
const heroHtml = `<div class="executive-hero">
|
|
@@ -29835,9 +29987,9 @@ function renderAudienceActionPlan(report, audience) {
|
|
|
29835
29987
|
function renderClientSummary(report) {
|
|
29836
29988
|
const s = report.executiveSummary;
|
|
29837
29989
|
const sc = report.citationScorecard;
|
|
29838
|
-
const totalQ = s.totalQueryCount;
|
|
29990
|
+
const totalQ = s.totalQueryCount ?? 0;
|
|
29839
29991
|
const heroNumber = totalQ > 0 ? `${s.mentionRate}%` : "\u2014";
|
|
29840
|
-
const heroSentence = totalQ > 0 ? `When customers asked AI ${totalQ} ${pluralize(totalQ, "query", "queries")} about your industry, AI mentioned you in ${s.mentionedQueryCount} of ${totalQ === 1 ? "them" : "those
|
|
29992
|
+
const heroSentence = totalQ > 0 ? `When customers asked AI ${totalQ} ${pluralize(totalQ, "query", "queries")} about your industry, AI mentioned you in ${s.mentionedQueryCount} of ${totalQ === 1 ? "them" : "those queries"}.` : "No AI check has been run yet. Run a check to see how AI tools answer customer queries about your business.";
|
|
29841
29993
|
const trend = clientTrendCopy(report.whatsChanged.mentionRate);
|
|
29842
29994
|
const heroTrend = trend ? `<p class="client-hero-trend tone-${trend.tone}"><span style="margin-right:6px;">${trend.arrow}</span>${escapeHtml2(trend.text)}</p>` : "";
|
|
29843
29995
|
const hero = `<div class="client-hero">
|
|
@@ -29851,12 +30003,12 @@ function renderClientSummary(report) {
|
|
|
29851
30003
|
<div class="client-metric-tile">
|
|
29852
30004
|
<div class="label">AI mentions your name</div>
|
|
29853
30005
|
<div class="value">${s.mentionRate}%</div>
|
|
29854
|
-
<div class="subtitle">${totalQ > 0 ? `Says your name in ${s.mentionedQueryCount} of ${totalQ} ${pluralize(totalQ, "
|
|
30006
|
+
<div class="subtitle">${totalQ > 0 ? `Says your name in ${s.mentionedQueryCount} of ${totalQ} ${pluralize(totalQ, "query", "queries")}` : "No data yet"}</div>
|
|
29855
30007
|
</div>
|
|
29856
30008
|
<div class="client-metric-tile">
|
|
29857
30009
|
<div class="label">AI links to your website</div>
|
|
29858
30010
|
<div class="value">${s.citationRate}%</div>
|
|
29859
|
-
<div class="subtitle">${totalQ > 0 ? `Cites your site as a source in ${s.citedQueryCount} of ${totalQ} ${pluralize(totalQ, "
|
|
30011
|
+
<div class="subtitle">${totalQ > 0 ? `Cites your site as a source in ${s.citedQueryCount} of ${totalQ} ${pluralize(totalQ, "query", "queries")}` : "No data yet"}</div>
|
|
29860
30012
|
</div>
|
|
29861
30013
|
<div class="client-metric-tile">
|
|
29862
30014
|
<div class="label">AI tools tested</div>
|
|
@@ -29997,9 +30149,9 @@ function renderReportHtml(report, opts = {}) {
|
|
|
29997
30149
|
const audience = opts.audience ?? "agency";
|
|
29998
30150
|
const title = opts.title ?? `Canonry ${audience} report \u2014 ${report.meta.project.displayName}`;
|
|
29999
30151
|
const sections = audience === "client" ? [
|
|
30000
|
-
renderClientSummary(report),
|
|
30001
|
-
renderReportShareOfVoice(report),
|
|
30002
|
-
renderWhatsChanged(report, "client"),
|
|
30152
|
+
report.visibility ? renderReportVisibility(report.visibility) : renderClientSummary(report),
|
|
30153
|
+
report.visibility?.selection.mode === "advanced" ? "" : renderReportShareOfVoice(report),
|
|
30154
|
+
report.visibility ? "" : renderWhatsChanged(report, "client"),
|
|
30003
30155
|
// Server-side AI visibility runs between WhatsChanged and the action
|
|
30004
30156
|
// plan in BOTH the SPA and HTML so clients see the same ordered set
|
|
30005
30157
|
// of sections in either surface (per the report-parity rule).
|
|
@@ -30007,13 +30159,13 @@ function renderReportHtml(report, opts = {}) {
|
|
|
30007
30159
|
renderAudienceActionPlan(report, "client"),
|
|
30008
30160
|
renderClientEvidenceSummary(report)
|
|
30009
30161
|
].join("\n") : [
|
|
30010
|
-
renderExecutiveSummary(report),
|
|
30011
|
-
renderReportShareOfVoice(report),
|
|
30012
|
-
renderWhatsChanged(report, "agency"),
|
|
30162
|
+
report.visibility ? renderReportVisibility(report.visibility) : renderExecutiveSummary(report),
|
|
30163
|
+
report.visibility?.selection.mode === "advanced" ? "" : renderReportShareOfVoice(report),
|
|
30164
|
+
report.visibility ? "" : renderWhatsChanged(report, "agency"),
|
|
30013
30165
|
renderAudienceActionPlan(report, "agency"),
|
|
30014
|
-
renderAgencyDiagnostics(report),
|
|
30015
|
-
renderCitationScorecard(report),
|
|
30016
|
-
renderCompetitorLandscape(report),
|
|
30166
|
+
report.visibility?.selection.mode === "advanced" ? "" : renderAgencyDiagnostics(report),
|
|
30167
|
+
report.visibility?.selection.mode === "advanced" ? "" : renderCitationScorecard(report),
|
|
30168
|
+
report.visibility?.selection.mode === "advanced" ? "" : renderCompetitorLandscape(report),
|
|
30017
30169
|
renderAiSourceOrigin(report),
|
|
30018
30170
|
renderGsc(report),
|
|
30019
30171
|
renderGa(report),
|
|
@@ -30021,7 +30173,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
30021
30173
|
renderAiReferrals(report),
|
|
30022
30174
|
renderServerActivity(report, "agency"),
|
|
30023
30175
|
renderIndexingHealth(report),
|
|
30024
|
-
renderCitationsTrend(report),
|
|
30176
|
+
report.visibility?.selection.mode === "advanced" ? "" : renderCitationsTrend(report),
|
|
30025
30177
|
renderInsights(report),
|
|
30026
30178
|
renderOpportunities(report),
|
|
30027
30179
|
renderContentGaps(report),
|
|
@@ -30043,7 +30195,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
30043
30195
|
<header class="header">
|
|
30044
30196
|
<div class="eyebrow">AI Visibility Report</div>
|
|
30045
30197
|
<h1>${escapeHtml2(report.meta.project.displayName)}</h1>
|
|
30046
|
-
<div class="subtitle">${escapeHtml2(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml2(report.meta.project.country)} / ${escapeHtml2(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Last ${report.meta.periodDays} days \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
|
|
30198
|
+
<div class="subtitle">${escapeHtml2(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml2(report.meta.project.country)} / ${escapeHtml2(report.meta.project.language.toUpperCase())}${report.visibility?.selection.mode === "advanced" ? ` \xB7 ${escapeHtml2(reportVisibilityLocationLabel(report.visibility))}` : renderHeaderLocationFragment(report.meta.location)} \xB7 Last ${report.meta.periodDays} days \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
|
|
30047
30199
|
</header>
|
|
30048
30200
|
${sections}
|
|
30049
30201
|
<footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml2(formatIsoDate(report.meta.generatedAt))}</footer>
|
|
@@ -30052,6 +30204,15 @@ function renderReportHtml(report, opts = {}) {
|
|
|
30052
30204
|
</body>
|
|
30053
30205
|
</html>`;
|
|
30054
30206
|
}
|
|
30207
|
+
function renderReportVisibility(visibility) {
|
|
30208
|
+
const populations = visibility.populations.filter((population) => population.queryClass !== "unknown" || population.summary.answerCount > 0);
|
|
30209
|
+
const historyPopulations = visibility.populations.filter((population) => population.queryClass !== "unknown" || population.trend.some((point) => point.answerCount > 0));
|
|
30210
|
+
const rateCell = (rate2) => `<td><strong>${escapeHtml2(reportVisibilityRate(rate2))}</strong><p class="muted">${escapeHtml2(reportVisibilityEvidence(rate2))}</p></td>`;
|
|
30211
|
+
const headers = (labels) => `<thead><tr>${labels.map((label) => `<th>${escapeHtml2(label)}</th>`).join("")}</tr></thead>`;
|
|
30212
|
+
const summary2 = `<table>${headers([REPORT_VISIBILITY_COPY.queryType, REPORT_VISIBILITY_COPY.queries, REPORT_VISIBILITY_COPY.answers, REPORT_VISIBILITY_COPY.mentioned, REPORT_VISIBILITY_COPY.cited])}<tbody>${populations.map((population) => `<tr><td>${escapeHtml2(reportQueryClassLabel(population.queryClass))}</td><td>${population.summary.queryCount}</td><td>${population.summary.answerCount}</td>${rateCell(population.summary.mentionCoverage)}${rateCell(population.summary.citationCoverage)}</tr>`).join("")}</tbody></table>`;
|
|
30213
|
+
const trend = `<details><summary>${escapeHtml2(reportVisibilityHistoryLabel(visibility))}</summary><div class="table-scroll"><table>${headers([REPORT_VISIBILITY_COPY.date, REPORT_VISIBILITY_COPY.queryType, REPORT_VISIBILITY_COPY.mentioned, REPORT_VISIBILITY_COPY.cited, REPORT_VISIBILITY_COPY.comparison])}<tbody>${historyPopulations.flatMap((population) => population.trend.map((point) => `<tr><td><time datetime="${escapeHtml2(point.createdAt)}">${escapeHtml2(point.createdAt.slice(0, 10))}</time></td><td>${escapeHtml2(reportQueryClassLabel(population.queryClass))}</td>${rateCell(point.mentionCoverage)}${rateCell(point.citationCoverage)}<td>${escapeHtml2(reportVisibilityComparison(point.continuity.state, point.continuity.comparedRunId !== null && !population.trend.some((previous) => previous.runId === point.continuity.comparedRunId)))}</td></tr>`)).join("")}</tbody></table></div></details>`;
|
|
30214
|
+
return `<section id="client-summary" class="report-section" aria-label="${escapeHtml2(REPORT_VISIBILITY_COPY.title)}"><h2>${escapeHtml2(REPORT_VISIBILITY_COPY.title)}</h2><p>${escapeHtml2(visibility.selection.mode === "advanced" ? REPORT_VISIBILITY_COPY.description : REPORT_VISIBILITY_COPY.simpleDescription)}</p><p>${escapeHtml2(reportVisibilityMeasurementLabel(visibility))}</p><div class="table-scroll">${summary2}</div>${trend}</section>`;
|
|
30215
|
+
}
|
|
30055
30216
|
|
|
30056
30217
|
// ../api-routes/src/ga-ai-referral-aggregation.ts
|
|
30057
30218
|
function normalizeAiTrafficClass(value) {
|
|
@@ -30913,7 +31074,6 @@ function buildIndexingHealth(db, projectId) {
|
|
|
30913
31074
|
}
|
|
30914
31075
|
function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
30915
31076
|
const visibilityRuns = db.select().from(runs).where(and29(eq33(runs.projectId, projectId), eq33(runs.kind, RunKinds["answer-visibility"]), notProbeRun())).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter);
|
|
30916
|
-
const totalQueries = queryLookup.byId.size;
|
|
30917
31077
|
const points = [];
|
|
30918
31078
|
for (const run of visibilityRuns) {
|
|
30919
31079
|
if (run.status !== RunStatuses.completed) continue;
|
|
@@ -30935,10 +31095,11 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
30935
31095
|
providerCounts.set(snap.provider, counts2);
|
|
30936
31096
|
}
|
|
30937
31097
|
if (considered === 0) continue;
|
|
31098
|
+
const totalQueries = new Set(snaps.map((snapshot) => snapshot.queryId)).size;
|
|
30938
31099
|
const citedQueryCount = citedQueryIds.size;
|
|
30939
31100
|
const mentionedQueryCount = mentionedQueryIds.size;
|
|
30940
31101
|
const citationRate = totalQueries > 0 ? Math.round(citedQueryCount / totalQueries * 100) : 0;
|
|
30941
|
-
const
|
|
31102
|
+
const mentionRate3 = totalQueries > 0 ? Math.round(mentionedQueryCount / totalQueries * 100) : 0;
|
|
30942
31103
|
const providerRates = [...providerCounts.entries()].map(([provider, counts2]) => ({
|
|
30943
31104
|
provider,
|
|
30944
31105
|
citationRate: counts2.total > 0 ? Math.round(counts2.cited / counts2.total * 100) : 0,
|
|
@@ -30950,7 +31111,7 @@ function buildCitationsTrend(db, projectId, queryLookup, locationFilter) {
|
|
|
30950
31111
|
citationRate,
|
|
30951
31112
|
citedQueryCount,
|
|
30952
31113
|
totalQueryCount: totalQueries,
|
|
30953
|
-
mentionRate:
|
|
31114
|
+
mentionRate: mentionRate3,
|
|
30954
31115
|
mentionedQueryCount,
|
|
30955
31116
|
providerRates
|
|
30956
31117
|
});
|
|
@@ -31310,7 +31471,7 @@ function buildReportActionPlan(input) {
|
|
|
31310
31471
|
});
|
|
31311
31472
|
}
|
|
31312
31473
|
if (actions.length === 0) {
|
|
31313
|
-
const neverMeasured = input.citationScorecard.providers.length
|
|
31474
|
+
const neverMeasured = !(input.hasVisibilityMeasurement ?? input.citationScorecard.providers.length > 0);
|
|
31314
31475
|
actions.push({
|
|
31315
31476
|
audience: "both",
|
|
31316
31477
|
priority: 90,
|
|
@@ -31343,12 +31504,12 @@ function mentionTrendSentence(delta) {
|
|
|
31343
31504
|
function buildClientSummary(reportLike) {
|
|
31344
31505
|
const s = reportLike.executiveSummary;
|
|
31345
31506
|
const queryNoun = s.totalQueryCount === 1 ? "query" : "queries";
|
|
31346
|
-
const headline = s.totalQueryCount > 0 ? `${s.mentionedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} mention the brand in AI answers` : "No tracked queries have completed a check yet";
|
|
31347
|
-
const overview = s.totalQueryCount > 0 ? `${reportLike.canonicalDomain} is mentioned on ${s.mentionRate}% of tracked queries and cited on ${s.citationRate}% of tracked queries. ${mentionTrendSentence(reportLike.whatsChanged.mentionRate)}` : "At least one completed check is needed before this can summarize how the brand appears in AI answers.";
|
|
31507
|
+
const headline = (s.totalQueryCount ?? 0) > 0 ? `${s.mentionedQueryCount} of ${s.totalQueryCount} tracked ${queryNoun} mention the brand in AI answers` : "No tracked queries have completed a check yet";
|
|
31508
|
+
const overview = (s.totalQueryCount ?? 0) > 0 ? `${reportLike.canonicalDomain} is mentioned on ${s.mentionRate}% of tracked queries and cited on ${s.citationRate}% of tracked queries. ${mentionTrendSentence(reportLike.whatsChanged.mentionRate)}` : "At least one completed check is needed before this can summarize how the brand appears in AI answers.";
|
|
31348
31509
|
const confidenceNotes = [];
|
|
31349
31510
|
if (s.totalQueryCount === 0) {
|
|
31350
31511
|
confidenceNotes.push("Confidence is low until the first tracked query check completes.");
|
|
31351
|
-
} else if (s.totalQueryCount < 5) {
|
|
31512
|
+
} else if ((s.totalQueryCount ?? 0) < 5) {
|
|
31352
31513
|
confidenceNotes.push("Directional read: the tracked query set is still small, so each query has outsized impact on the percentage.");
|
|
31353
31514
|
}
|
|
31354
31515
|
if (isTrendBaseline(reportLike.citationsTrend)) {
|
|
@@ -31473,7 +31634,7 @@ function buildWhatsChanged(input) {
|
|
|
31473
31634
|
direction: rateDirection(citationRateSmoothed.deltaAbs, RATE_REAL_MOVEMENT_THRESHOLD_PP)
|
|
31474
31635
|
} : null;
|
|
31475
31636
|
const mentionRateSmoothed = smoothedRunDelta(citationsTrend, (p) => p.mentionRate);
|
|
31476
|
-
const
|
|
31637
|
+
const mentionRate3 = enoughHistory && mentionRateSmoothed ? {
|
|
31477
31638
|
...mentionRateSmoothed,
|
|
31478
31639
|
direction: rateDirection(mentionRateSmoothed.deltaAbs, RATE_REAL_MOVEMENT_THRESHOLD_PP)
|
|
31479
31640
|
} : null;
|
|
@@ -31520,7 +31681,7 @@ function buildWhatsChanged(input) {
|
|
|
31520
31681
|
enoughHistory,
|
|
31521
31682
|
headline,
|
|
31522
31683
|
citationRate,
|
|
31523
|
-
mentionRate:
|
|
31684
|
+
mentionRate: mentionRate3,
|
|
31524
31685
|
citedQueryCount,
|
|
31525
31686
|
mentionedQueryCount,
|
|
31526
31687
|
gscClicksDelta,
|
|
@@ -31534,14 +31695,30 @@ function buildWhatsChanged(input) {
|
|
|
31534
31695
|
function buildProjectReport(db, projectName, periodDays) {
|
|
31535
31696
|
const project = resolveProject(db, projectName);
|
|
31536
31697
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
31698
|
+
const canonicalVisibility = readVisibilityReport(db, project, { queryClass: "all", scope: "project" });
|
|
31699
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
31700
|
+
const historyWindow = { from: new Date(Date.parse(generatedAt) - periodDays * 864e5).toISOString(), to: generatedAt };
|
|
31701
|
+
const visibility = {
|
|
31702
|
+
selection: canonicalVisibility.selection,
|
|
31703
|
+
historyWindow,
|
|
31704
|
+
populations: canonicalVisibility.populations.map(({ queryClass, summary: summary2, trend: trend2 }) => ({
|
|
31705
|
+
queryClass,
|
|
31706
|
+
summary: summary2,
|
|
31707
|
+
// Keep each point's actual comparison. Renderers disclose when the prior
|
|
31708
|
+
// measurement is outside this window rather than inventing a baseline.
|
|
31709
|
+
trend: trend2.filter((point) => point.createdAt >= historyWindow.from && point.createdAt <= historyWindow.to)
|
|
31710
|
+
}))
|
|
31711
|
+
};
|
|
31712
|
+
const canonicalVisibilitySupported = visibility.selection.availability.state === "available";
|
|
31713
|
+
const advancedVisibility = canonicalVisibilitySupported && visibility.selection.mode === "advanced";
|
|
31537
31714
|
const comparisonWindowDays = reportComparisonWindowDays(periodDays);
|
|
31538
31715
|
const allRuns = db.select().from(runs).where(and29(eq33(runs.projectId, project.id), notProbeRun())).orderBy(desc14(runs.createdAt), desc14(runs.id)).all();
|
|
31539
31716
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
31540
31717
|
const completedVisRunGroups = groupRunsByCreatedAt(
|
|
31541
31718
|
visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
|
|
31542
31719
|
);
|
|
31543
|
-
const latestVisRunGroup = completedVisRunGroups[0] ?? [];
|
|
31544
|
-
const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
|
|
31720
|
+
const latestVisRunGroup = advancedVisibility ? visibilityRuns.filter((run) => run.id === visibility.selection.run.id) : completedVisRunGroups[0] ?? [];
|
|
31721
|
+
const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? (advancedVisibility ? null : visibilityRuns[0] ?? null);
|
|
31545
31722
|
const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
|
|
31546
31723
|
const latestRunLocation = representativeLatestRun?.location ?? null;
|
|
31547
31724
|
const competitorRows2 = db.select().from(competitors).where(eq33(competitors.projectId, project.id)).all();
|
|
@@ -31594,7 +31771,8 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31594
31771
|
}));
|
|
31595
31772
|
}
|
|
31596
31773
|
Object.assign(mentionLandscape, mentionLandscape.nonBrand);
|
|
31597
|
-
const
|
|
31774
|
+
const advancedSourceSnapshots = advancedVisibility && visibility.selection.run.id !== null ? db.select({ citedDomains: querySnapshots.citedDomains, provider: querySnapshots.provider, location: querySnapshots.location }).from(querySnapshots).where(eq33(querySnapshots.runId, visibility.selection.run.id)).all() : [];
|
|
31775
|
+
const aiSourceOrigin = buildAiSourceOrigin(advancedVisibility ? advancedSourceSnapshots : latestSnapshots, projectDomains, competitorDomains);
|
|
31598
31776
|
const trackedQueries = [...queryLookup.byId.values()];
|
|
31599
31777
|
const gscSection = buildGscSection(
|
|
31600
31778
|
db,
|
|
@@ -31629,7 +31807,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31629
31807
|
insights: insightList,
|
|
31630
31808
|
comparisonWindowDays
|
|
31631
31809
|
});
|
|
31632
|
-
const totalQueryCount =
|
|
31810
|
+
const totalQueryCount = new Set(latestSnapshots.map((snapshot) => snapshot.queryId)).size;
|
|
31633
31811
|
const citedQueryIds = /* @__PURE__ */ new Set();
|
|
31634
31812
|
const mentionedQueryIds = /* @__PURE__ */ new Set();
|
|
31635
31813
|
for (const snap of latestSnapshots) {
|
|
@@ -31640,7 +31818,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31640
31818
|
const citedQueryCount = citedQueryIds.size;
|
|
31641
31819
|
const mentionedQueryCount = mentionedQueryIds.size;
|
|
31642
31820
|
const citationRate = totalQueryCount > 0 ? Math.round(citedQueryCount / totalQueryCount * 100) : 0;
|
|
31643
|
-
const
|
|
31821
|
+
const mentionRate3 = totalQueryCount > 0 ? Math.round(mentionedQueryCount / totalQueryCount * 100) : 0;
|
|
31644
31822
|
const trendBaseline = isTrendBaseline(citationsTrend);
|
|
31645
31823
|
const latestPoint = citationsTrend.at(-1);
|
|
31646
31824
|
const previousPoint = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
|
|
@@ -31665,21 +31843,22 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31665
31843
|
insightList,
|
|
31666
31844
|
competitorLandscape.competitors
|
|
31667
31845
|
);
|
|
31668
|
-
const
|
|
31669
|
-
const
|
|
31846
|
+
const visibilityDates = visibility.populations.flatMap((population) => population.trend.map((point) => point.createdAt)).sort();
|
|
31847
|
+
const periodStart = advancedVisibility ? visibilityDates[0] ?? visibility.selection.measurement.completedAt : citationsTrend[0]?.date ?? null;
|
|
31848
|
+
const periodEnd = advancedVisibility ? visibility.selection.measurement.completedAt : citationsTrend.at(-1)?.date ?? null;
|
|
31670
31849
|
const configuredLocations = project.locations;
|
|
31671
31850
|
const reportLocation = buildLocationMeta(representativeLatestRun?.location ?? null, configuredLocations);
|
|
31672
|
-
const providerLocationHandling = reportLocation ? buildProviderLocationHandling(citationScorecard.providers) : [];
|
|
31851
|
+
const providerLocationHandling = advancedVisibility ? buildProviderLocationHandling([...new Set(advancedSourceSnapshots.filter((snapshot) => snapshot.location !== null).map((snapshot) => snapshot.provider))]) : reportLocation ? buildProviderLocationHandling(citationScorecard.providers) : [];
|
|
31673
31852
|
const executiveSummary = {
|
|
31674
31853
|
citationRate,
|
|
31675
31854
|
citedQueryCount,
|
|
31676
31855
|
totalQueryCount,
|
|
31677
|
-
mentionRate:
|
|
31856
|
+
mentionRate: mentionRate3,
|
|
31678
31857
|
mentionedQueryCount,
|
|
31679
31858
|
trend,
|
|
31680
31859
|
queryCount: queryLookup.byId.size,
|
|
31681
31860
|
competitorCount: competitorDomains.length,
|
|
31682
|
-
providerCount: citationScorecard.providers.length,
|
|
31861
|
+
providerCount: advancedVisibility ? new Set(advancedSourceSnapshots.map((snapshot) => snapshot.provider)).size : citationScorecard.providers.length,
|
|
31683
31862
|
gsc: gscSection ? {
|
|
31684
31863
|
clicks: gscSection.totalClicks,
|
|
31685
31864
|
impressions: gscSection.totalImpressions,
|
|
@@ -31700,6 +31879,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31700
31879
|
canonicalDomain: project.canonicalDomain,
|
|
31701
31880
|
competitorDomains: actionPlanCompetitorDomains,
|
|
31702
31881
|
citationScorecard,
|
|
31882
|
+
hasVisibilityMeasurement: advancedVisibility ? visibility.selection.run.id !== null : void 0,
|
|
31703
31883
|
aiSourceOrigin,
|
|
31704
31884
|
gsc: gscSection,
|
|
31705
31885
|
indexingHealth: indexingHealthSection,
|
|
@@ -31721,6 +31901,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31721
31901
|
canonicalDomain: project.canonicalDomain,
|
|
31722
31902
|
competitorDomains,
|
|
31723
31903
|
citationScorecard,
|
|
31904
|
+
hasVisibilityMeasurement: advancedVisibility ? visibility.selection.run.id !== null : void 0,
|
|
31724
31905
|
aiSourceOrigin,
|
|
31725
31906
|
gsc: gscSection,
|
|
31726
31907
|
indexingHealth: indexingHealthSection,
|
|
@@ -31732,7 +31913,7 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31732
31913
|
});
|
|
31733
31914
|
return {
|
|
31734
31915
|
meta: {
|
|
31735
|
-
generatedAt
|
|
31916
|
+
generatedAt,
|
|
31736
31917
|
project: {
|
|
31737
31918
|
id: project.id,
|
|
31738
31919
|
name: project.name,
|
|
@@ -31747,7 +31928,18 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31747
31928
|
periodEnd,
|
|
31748
31929
|
periodDays
|
|
31749
31930
|
},
|
|
31750
|
-
|
|
31931
|
+
...canonicalVisibilitySupported ? { visibility } : {},
|
|
31932
|
+
executiveSummary: advancedVisibility ? {
|
|
31933
|
+
...executiveSummary,
|
|
31934
|
+
visibilityBasis: "frozen-populations",
|
|
31935
|
+
citationRate: null,
|
|
31936
|
+
citedQueryCount: null,
|
|
31937
|
+
totalQueryCount: null,
|
|
31938
|
+
mentionRate: null,
|
|
31939
|
+
mentionedQueryCount: null,
|
|
31940
|
+
trend: "unknown",
|
|
31941
|
+
findings: []
|
|
31942
|
+
} : { ...executiveSummary, visibilityBasis: "legacy-project-queries" },
|
|
31751
31943
|
citationScorecard,
|
|
31752
31944
|
competitorLandscape,
|
|
31753
31945
|
mentionLandscape,
|
|
@@ -31758,12 +31950,28 @@ function buildProjectReport(db, projectName, periodDays) {
|
|
|
31758
31950
|
aiReferrals: aiReferralsSection,
|
|
31759
31951
|
serverActivity: serverActivitySection,
|
|
31760
31952
|
indexingHealth: indexingHealthSection,
|
|
31761
|
-
citationsTrend,
|
|
31762
|
-
whatsChanged
|
|
31953
|
+
citationsTrend: advancedVisibility ? [] : citationsTrend,
|
|
31954
|
+
whatsChanged: advancedVisibility ? {
|
|
31955
|
+
...whatsChanged,
|
|
31956
|
+
enoughHistory: false,
|
|
31957
|
+
headline: "See measurement history for each query type.",
|
|
31958
|
+
citationRate: null,
|
|
31959
|
+
mentionRate: null,
|
|
31960
|
+
citedQueryCount: null,
|
|
31961
|
+
mentionedQueryCount: null,
|
|
31962
|
+
providerMovements: [],
|
|
31963
|
+
wins: [],
|
|
31964
|
+
regressions: []
|
|
31965
|
+
} : whatsChanged,
|
|
31763
31966
|
insights: insightList,
|
|
31764
31967
|
recommendedNextSteps,
|
|
31765
31968
|
actionPlan,
|
|
31766
|
-
clientSummary
|
|
31969
|
+
clientSummary: advancedVisibility ? {
|
|
31970
|
+
...clientSummary,
|
|
31971
|
+
headline: "AI visibility by query type",
|
|
31972
|
+
overview: "Mention and citation coverage use the properties assigned to each measured query.",
|
|
31973
|
+
confidenceNotes: []
|
|
31974
|
+
} : clientSummary,
|
|
31767
31975
|
agencyDiagnostics,
|
|
31768
31976
|
contentOpportunities,
|
|
31769
31977
|
contentGaps,
|
|
@@ -34078,7 +34286,7 @@ function summarizeFromSnapshots(snapshots) {
|
|
|
34078
34286
|
if (wasMentioned) mentionedQueries += 1;
|
|
34079
34287
|
}
|
|
34080
34288
|
const notMentionedQueries = totalQueries - mentionedQueries;
|
|
34081
|
-
const
|
|
34289
|
+
const mentionRate3 = totalQueries === 0 ? 0 : Number((mentionedQueries / totalQueries).toFixed(4));
|
|
34082
34290
|
const providers = [...perProvider.entries()].map(([provider, { cited, total }]) => ({
|
|
34083
34291
|
provider,
|
|
34084
34292
|
cited,
|
|
@@ -34093,7 +34301,7 @@ function summarizeFromSnapshots(snapshots) {
|
|
|
34093
34301
|
citedRate,
|
|
34094
34302
|
mentionedQueries,
|
|
34095
34303
|
notMentionedQueries,
|
|
34096
|
-
mentionRate:
|
|
34304
|
+
mentionRate: mentionRate3
|
|
34097
34305
|
},
|
|
34098
34306
|
providers
|
|
34099
34307
|
};
|
|
@@ -34529,6 +34737,8 @@ var SCHEMA_TABLE = {
|
|
|
34529
34737
|
DiscoveryPromoteResult: discoveryPromoteResultSchema,
|
|
34530
34738
|
DiscoverySessionDetailDto: discoverySessionDetailDtoSchema,
|
|
34531
34739
|
DiscoverySessionDto: discoverySessionDtoSchema,
|
|
34740
|
+
ResearchBatchCreate: researchBatchCreateSchema,
|
|
34741
|
+
ResearchBatchDto: researchBatchSchema,
|
|
34532
34742
|
ResearchRunCreate: researchRunCreateSchema,
|
|
34533
34743
|
ResearchRunDetailDto: researchRunDetailSchema,
|
|
34534
34744
|
ResearchRunListDto: researchRunListSchema,
|
|
@@ -36230,6 +36440,16 @@ var routeCatalog = [
|
|
|
36230
36440
|
requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ResearchRunCreate" } } } },
|
|
36231
36441
|
responses: { 200: jsonResponse("Idempotent request returned its existing research run.", "ResearchRunDetailDto"), 202: jsonResponse("Research batch queued.", "ResearchRunDetailDto"), 400: errorResponse("Invalid provider, model, location, or request."), 403: errorResponse("Paid research was not granted to this principal."), 404: errorResponse("Project not found."), 409: errorResponse("Idempotency key was reused with a different payload."), 422: errorResponse("Research executor is unavailable on this deployment."), 429: errorResponse("The viewer reached the per-project UTC-day research limit.") }
|
|
36232
36442
|
},
|
|
36443
|
+
{
|
|
36444
|
+
method: "post",
|
|
36445
|
+
path: "/api/v1/projects/{name}/research/batches",
|
|
36446
|
+
summary: "Start bounded explicit research destinations",
|
|
36447
|
+
description: "Queues one to twenty saved research runs atomically, with no more than fifty exact editor questions in total. Every destination explicitly pins its configured API provider, model, and configured location (or null); a scoped destination also pins the published plan revision. The root idempotency key covers the complete ordered destination list: a retry returns the same saved runs even if plans, templates, or provider defaults later change. A changed, reordered, added, or removed destination conflicts. Research never creates tracked queries, shared runs, snapshots, insights, or notifications.",
|
|
36448
|
+
tags: ["research"],
|
|
36449
|
+
parameters: [nameParameter],
|
|
36450
|
+
requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/ResearchBatchCreate" } } } },
|
|
36451
|
+
responses: { 200: jsonResponse("Idempotent request returned its existing saved runs.", "ResearchBatchDto"), 202: jsonResponse("All research destinations were queued.", "ResearchBatchDto"), 400: errorResponse("Each destination must explicitly supply a configured provider, model, location or null, and expected plan revision when scoped. The request permits at most 20 destinations and 50 total concrete queries."), 403: errorResponse("Paid research was not granted to this principal."), 404: errorResponse("Project not found."), 409: errorResponse("The root idempotency key was reused with different ordered destinations."), 422: errorResponse("Research execution is not available on this deployment."), 429: errorResponse("The viewer reached the per-project UTC-day research limit.") }
|
|
36452
|
+
},
|
|
36233
36453
|
{
|
|
36234
36454
|
method: "get",
|
|
36235
36455
|
path: "/api/v1/projects/{name}/query-tracking",
|
|
@@ -36273,7 +36493,7 @@ var routeCatalog = [
|
|
|
36273
36493
|
path: "/api/v1/projects/{name}/research/runs",
|
|
36274
36494
|
summary: "List saved research query batches",
|
|
36275
36495
|
tags: ["research"],
|
|
36276
|
-
parameters: [nameParameter, { name: "limit", in: "query", description: "Max runs, default 20 and maximum 100.", schema: integerSchema }],
|
|
36496
|
+
parameters: [nameParameter, { name: "limit", in: "query", description: "Max runs, default 20 and maximum 100.", schema: integerSchema }, { name: "cursor", in: "query", description: "Opaque cursor from the previous page.", schema: stringSchema }],
|
|
36277
36497
|
responses: { 200: jsonResponse("Research runs returned newest first.", "ResearchRunListDto"), 404: errorResponse("Project not found.") }
|
|
36278
36498
|
},
|
|
36279
36499
|
{
|
|
@@ -41978,7 +42198,7 @@ async function keysRoutes(app) {
|
|
|
41978
42198
|
if (!row) {
|
|
41979
42199
|
throw notFound("API key", id);
|
|
41980
42200
|
}
|
|
41981
|
-
return toApiKeyDto(row, projectNameById(app.db));
|
|
42201
|
+
return toApiKeyDto({ ...row, scopes: request.apiKey?.scopes ?? row.scopes }, projectNameById(app.db));
|
|
41982
42202
|
});
|
|
41983
42203
|
app.post("/keys", async (request) => {
|
|
41984
42204
|
requireAdminSession(request);
|
|
@@ -83168,22 +83388,33 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
83168
83388
|
|
|
83169
83389
|
// ../api-routes/src/research.ts
|
|
83170
83390
|
import crypto49 from "crypto";
|
|
83171
|
-
import {
|
|
83391
|
+
import { z as z5 } from "zod";
|
|
83392
|
+
import { and as and55, count as count3, desc as desc28, eq as eq65, gte as gte17, lt as lt11, or as or14, sql as sql25 } from "drizzle-orm";
|
|
83172
83393
|
var sameLocation = (a, b) => a.label === b.label && a.city === b.city && a.region === b.region && a.country === b.country && a.timezone === b.timezone;
|
|
83394
|
+
var BATCH_RECEIPT_PREFIX = "__canonry_research_batch__:";
|
|
83173
83395
|
async function researchRoutes(app, opts) {
|
|
83174
83396
|
const viewerDailyRunLimit = opts.viewerDailyRunLimit ?? DEFAULT_VIEWER_RESEARCH_DAILY_RUN_LIMIT;
|
|
83175
83397
|
if (!Number.isInteger(viewerDailyRunLimit) || viewerDailyRunLimit <= 0) {
|
|
83176
83398
|
throw new Error("viewerDailyRunLimit must be a positive integer");
|
|
83177
83399
|
}
|
|
83178
83400
|
app.post("/projects/:name/research/runs", {
|
|
83179
|
-
config: { paidRead: true }
|
|
83401
|
+
config: { paidRead: true, writeScope: RESEARCH_RUN_SCOPE }
|
|
83180
83402
|
}, async (request, reply) => {
|
|
83181
83403
|
requireResearchGrant(request, opts.allowViewers ?? false);
|
|
83182
83404
|
const project = resolveProject(app.db, request.params.name);
|
|
83183
|
-
if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
|
|
83184
83405
|
const parsed = researchRunCreateSchema.safeParse(request.body ?? {});
|
|
83185
83406
|
if (!parsed.success) throw validationError("Invalid research run request", { issues: parsed.error.issues });
|
|
83186
83407
|
const input = parsed.data;
|
|
83408
|
+
if (input.idempotencyKey?.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
|
|
83409
|
+
const requestHash2 = directResearchRequestHash(input);
|
|
83410
|
+
const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
|
|
83411
|
+
if (existingReceipt) {
|
|
83412
|
+
assertSameDirectResearchRequest(existingReceipt, input, requestHash2);
|
|
83413
|
+
const result2 = getDetail(app, project.id, existingReceipt.id);
|
|
83414
|
+
if (existingReceipt.status === ResearchRunStatuses.queued) opts.onResearchRunRequested?.(existingReceipt.id, project.id);
|
|
83415
|
+
return reply.status(200).send(result2);
|
|
83416
|
+
}
|
|
83417
|
+
if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
|
|
83187
83418
|
const adapters = opts.providerAdapters ?? [];
|
|
83188
83419
|
const configured = new Set(opts.configuredProviderNames ?? []);
|
|
83189
83420
|
const providerName = input.provider ?? project.providers.find((name) => configured.has(name) && adapters.some((adapter2) => adapter2.name === name && adapter2.mode === "api")) ?? adapters.find((adapter2) => adapter2.mode === "api" && configured.has(adapter2.name))?.name;
|
|
@@ -83193,37 +83424,34 @@ async function researchRoutes(app, opts) {
|
|
|
83193
83424
|
adapter.modelValidationPattern.lastIndex = 0;
|
|
83194
83425
|
if (!adapter.modelConfigurable || !adapter.modelValidationPattern.test(input.model)) throw validationError(`Invalid model "${input.model}" for provider "${providerName}".`, { provider: providerName, model: input.model, hint: adapter.modelValidationHint });
|
|
83195
83426
|
}
|
|
83196
|
-
const existingReceipt = input.idempotencyKey ? app.db.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get() : void 0;
|
|
83197
83427
|
const active = activeMeasurementPlan(app.db, project.id);
|
|
83198
|
-
const scope = input.scope ?
|
|
83199
|
-
const template = input.template ? existingReceipt ? resolveStoredResearchTemplate(existingReceipt.template, input.template, input.idempotencyKey) : resolveResearchTemplate(app, project.id, input.template, scope) : null;
|
|
83428
|
+
const scope = input.scope ? resolveResearchScope(active, input.scope) : null;
|
|
83200
83429
|
const location = input.location === void 0 ? scope ? null : project.defaultLocation ? project.locations.find((item2) => item2.label === project.defaultLocation) ?? null : null : input.location;
|
|
83201
83430
|
if (location && !project.locations.some((item2) => sameLocation(item2, location))) throw validationError("Research location must exactly match a configured project location.", { location });
|
|
83431
|
+
const template = input.template ? resolveResearchTemplate(app, project.id, input.template, scope, location) : null;
|
|
83202
83432
|
const requestedModel2 = input.model ?? null;
|
|
83203
83433
|
const resolvedModel = requestedModel2 ?? (project.providerModels[providerName] || opts.getEffectiveProviderModels?.()[providerName] || adapter.defaultModel);
|
|
83204
83434
|
adapter.modelValidationPattern.lastIndex = 0;
|
|
83205
83435
|
if (!adapter.modelValidationPattern.test(resolvedModel)) throw validationError('Invalid resolved model "' + resolvedModel + '" for provider "' + providerName + '".', { provider: providerName, model: resolvedModel, hint: adapter.modelValidationHint });
|
|
83206
83436
|
if (deduplicateResearchQueries(input.queries).length !== input.queries.length) throw validationError("Research queries must be unique within a batch.");
|
|
83207
83437
|
const queryClasses = classifyResearchQueries(project, active?.plan ?? null, input.queries);
|
|
83208
|
-
const normalized = { queries: input.queries, provider: providerName, model: requestedModel2, location: location ?? null, ...scope ? { scope } : {}, ...template ? { template } : {} };
|
|
83209
|
-
const requestHash2 = crypto49.createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
83210
83438
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
83211
83439
|
const initiatedBy = researchPrincipal(request);
|
|
83212
83440
|
const decision = app.db.transaction((tx) => {
|
|
83213
83441
|
if (input.idempotencyKey) {
|
|
83214
83442
|
const existing = tx.select().from(researchRuns).where(and55(eq65(researchRuns.projectId, project.id), eq65(researchRuns.idempotencyKey, input.idempotencyKey))).get();
|
|
83215
83443
|
if (existing) {
|
|
83216
|
-
|
|
83444
|
+
assertSameDirectResearchRequest(existing, input, requestHash2);
|
|
83217
83445
|
return { reused: true, id: existing.id, shouldDispatch: existing.status === ResearchRunStatuses.queued };
|
|
83218
83446
|
}
|
|
83219
83447
|
}
|
|
83220
|
-
if (initiatedBy?.
|
|
83448
|
+
if (initiatedBy?.limited) {
|
|
83221
83449
|
const { start, end, date } = utcDayBounds(now);
|
|
83222
83450
|
const used = tx.select({ value: count3() }).from(researchRuns).where(and55(
|
|
83223
83451
|
eq65(researchRuns.projectId, project.id),
|
|
83224
83452
|
gte17(researchRuns.createdAt, start),
|
|
83225
83453
|
lt11(researchRuns.createdAt, end),
|
|
83226
|
-
sql25`json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer}`
|
|
83454
|
+
sql25`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
83227
83455
|
)).get()?.value ?? 0;
|
|
83228
83456
|
if (used >= viewerDailyRunLimit) {
|
|
83229
83457
|
throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
|
|
@@ -83240,17 +83468,108 @@ async function researchRoutes(app, opts) {
|
|
|
83240
83468
|
if (decision.reused) return reply.status(200).send(result);
|
|
83241
83469
|
return reply.status(202).send(result);
|
|
83242
83470
|
});
|
|
83471
|
+
app.post("/projects/:name/research/batches", {
|
|
83472
|
+
config: { paidRead: true, writeScope: RESEARCH_RUN_SCOPE }
|
|
83473
|
+
}, async (request, reply) => {
|
|
83474
|
+
requireResearchGrant(request, opts.allowViewers ?? false);
|
|
83475
|
+
const project = resolveProject(app.db, request.params.name);
|
|
83476
|
+
const parsed = researchBatchCreateSchema.safeParse(request.body ?? {});
|
|
83477
|
+
if (!parsed.success) throw validationError("Invalid research batch request", { issues: parsed.error.issues });
|
|
83478
|
+
const input = parsed.data;
|
|
83479
|
+
if (input.idempotencyKey.startsWith(BATCH_RECEIPT_PREFIX)) throw validationError("This idempotency key prefix is reserved for internal research batch receipts.");
|
|
83480
|
+
const requestHash2 = researchBatchRequestHash(input);
|
|
83481
|
+
const receiptKeys = input.runs.map((_run, index2) => batchChildReceiptKey(input.idempotencyKey, index2));
|
|
83482
|
+
const firstReceipt = app.db.select().from(researchRuns).where(and55(
|
|
83483
|
+
eq65(researchRuns.projectId, project.id),
|
|
83484
|
+
eq65(researchRuns.idempotencyKey, receiptKeys[0])
|
|
83485
|
+
)).get();
|
|
83486
|
+
if (firstReceipt) {
|
|
83487
|
+
if (firstReceipt.requestHash !== requestHash2) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
|
|
83488
|
+
const saved = receiptKeys.map((key) => app.db.select().from(researchRuns).where(and55(
|
|
83489
|
+
eq65(researchRuns.projectId, project.id),
|
|
83490
|
+
eq65(researchRuns.idempotencyKey, key)
|
|
83491
|
+
)).get());
|
|
83492
|
+
if (saved.some((row) => !row || row.requestHash !== requestHash2)) throw alreadyExists("Research batch idempotency key", input.idempotencyKey);
|
|
83493
|
+
const runs3 = saved.map((row) => getDetail(app, project.id, row.id));
|
|
83494
|
+
for (const run of runs3) if (run.status === ResearchRunStatuses.queued) opts.onResearchRunRequested?.(run.id, project.id);
|
|
83495
|
+
return reply.status(200).send({ runs: runs3 });
|
|
83496
|
+
}
|
|
83497
|
+
if (!opts.onResearchRunRequested) throw missingDependency("Research execution is not available on this deployment.", { reason: "no-research-handler" });
|
|
83498
|
+
const active = activeMeasurementPlan(app.db, project.id);
|
|
83499
|
+
const prepared = input.runs.map((run) => prepareBatchRun(app, opts, project, active, run));
|
|
83500
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
83501
|
+
const initiatedBy = researchPrincipal(request);
|
|
83502
|
+
const decision = app.db.transaction((tx) => {
|
|
83503
|
+
if (initiatedBy?.limited) {
|
|
83504
|
+
const { start, end, date } = utcDayBounds(now);
|
|
83505
|
+
const used = tx.select({ value: count3() }).from(researchRuns).where(and55(
|
|
83506
|
+
eq65(researchRuns.projectId, project.id),
|
|
83507
|
+
gte17(researchRuns.createdAt, start),
|
|
83508
|
+
lt11(researchRuns.createdAt, end),
|
|
83509
|
+
sql25`(json_extract(${researchRuns.initiatedBy}, '$.role') = ${UserRoles.viewer} OR json_extract(${researchRuns.initiatedBy}, '$.limited') = 1)`
|
|
83510
|
+
)).get()?.value ?? 0;
|
|
83511
|
+
if (used + prepared.length > viewerDailyRunLimit) throw researchDailyLimitExceeded(project.name, viewerDailyRunLimit, date);
|
|
83512
|
+
}
|
|
83513
|
+
const ids = [];
|
|
83514
|
+
for (const [index2, run] of prepared.entries()) {
|
|
83515
|
+
const id = crypto49.randomUUID();
|
|
83516
|
+
ids.push(id);
|
|
83517
|
+
tx.insert(researchRuns).values({
|
|
83518
|
+
id,
|
|
83519
|
+
projectId: project.id,
|
|
83520
|
+
status: ResearchRunStatuses.queued,
|
|
83521
|
+
provider: run.providerName,
|
|
83522
|
+
requestedModel: run.requestedModel,
|
|
83523
|
+
resolvedModel: run.resolvedModel,
|
|
83524
|
+
location: run.location,
|
|
83525
|
+
totalQueries: run.input.queries.length,
|
|
83526
|
+
scope: run.scope,
|
|
83527
|
+
template: run.template,
|
|
83528
|
+
idempotencyKey: receiptKeys[index2],
|
|
83529
|
+
requestHash: requestHash2,
|
|
83530
|
+
initiatedBy,
|
|
83531
|
+
createdAt: now
|
|
83532
|
+
}).run();
|
|
83533
|
+
for (const [position, query] of run.input.queries.entries()) tx.insert(researchRunQueries).values({
|
|
83534
|
+
id: crypto49.randomUUID(),
|
|
83535
|
+
researchRunId: id,
|
|
83536
|
+
position,
|
|
83537
|
+
queryText: query,
|
|
83538
|
+
queryClass: run.queryClasses[position] ?? null,
|
|
83539
|
+
status: ResearchQueryStatuses.queued,
|
|
83540
|
+
requestedModel: run.requestedModel,
|
|
83541
|
+
resolvedModel: run.resolvedModel,
|
|
83542
|
+
groundingSources: [],
|
|
83543
|
+
citedDomains: [],
|
|
83544
|
+
searchQueries: [],
|
|
83545
|
+
createdAt: now
|
|
83546
|
+
}).run();
|
|
83547
|
+
writeAuditLog(tx, { projectId: project.id, actor: initiatedBy ? `${initiatedBy.kind}:${initiatedBy.id}` : "api", action: "research.created", entityType: "research_run", entityId: id });
|
|
83548
|
+
}
|
|
83549
|
+
return { ids };
|
|
83550
|
+
});
|
|
83551
|
+
const runs2 = decision.ids.map((id) => getDetail(app, project.id, id));
|
|
83552
|
+
for (const id of decision.ids) opts.onResearchRunRequested(id, project.id);
|
|
83553
|
+
return reply.status(202).send({ runs: runs2 });
|
|
83554
|
+
});
|
|
83243
83555
|
app.get("/projects/:name/research/runs", async (request) => {
|
|
83244
83556
|
const project = resolveProject(app.db, request.params.name);
|
|
83245
83557
|
const requested = Number.parseInt(request.query.limit ?? "", 10);
|
|
83246
83558
|
const limit = Number.isInteger(requested) && requested > 0 ? Math.min(requested, 100) : 20;
|
|
83247
|
-
const
|
|
83559
|
+
const cursor = request.query.cursor ? parseResearchCursor(request.query.cursor, project.id) : null;
|
|
83560
|
+
const rows = app.db.select().from(researchRuns).where(and55(
|
|
83561
|
+
eq65(researchRuns.projectId, project.id),
|
|
83562
|
+
cursor ? or14(lt11(researchRuns.createdAt, cursor.createdAt), and55(eq65(researchRuns.createdAt, cursor.createdAt), lt11(researchRuns.id, cursor.id))) : void 0
|
|
83563
|
+
)).orderBy(desc28(researchRuns.createdAt), desc28(researchRuns.id)).limit(limit + 1).all();
|
|
83564
|
+
const runs2 = rows.slice(0, limit).map(serializeRun);
|
|
83565
|
+
const last = runs2.at(-1);
|
|
83566
|
+
const nextCursor = rows.length > limit && last ? Buffer.from(JSON.stringify({ projectId: project.id, createdAt: last.createdAt, id: last.id })).toString("base64url") : null;
|
|
83248
83567
|
const configured = new Set(opts.configuredProviderNames ?? []);
|
|
83249
83568
|
const effectiveModels2 = opts.getEffectiveProviderModels?.() ?? {};
|
|
83250
|
-
const providers =
|
|
83569
|
+
const providers = (opts.providerAdapters ?? []).filter((adapter) => adapter.mode === "api" && !isBrowserProvider(adapter.name) && configured.has(adapter.name)).map((adapter) => {
|
|
83251
83570
|
const defaultModel = project.providerModels[adapter.name] || effectiveModels2[adapter.name] || adapter.defaultModel;
|
|
83252
|
-
const
|
|
83253
|
-
const models =
|
|
83571
|
+
const cached = opts.getCachedProviderModels?.(adapter.name) ?? [];
|
|
83572
|
+
const models = cached.length ? cached : adapter.knownModels;
|
|
83254
83573
|
return {
|
|
83255
83574
|
name: adapter.name,
|
|
83256
83575
|
displayName: adapter.displayName,
|
|
@@ -83258,20 +83577,94 @@ async function researchRoutes(app, opts) {
|
|
|
83258
83577
|
defaultModel,
|
|
83259
83578
|
knownModels: [...new Map([{ id: defaultModel, displayName: defaultModel }, ...models].map((model) => [model.id, { id: model.id, displayName: model.displayName }])).values()]
|
|
83260
83579
|
};
|
|
83261
|
-
})
|
|
83262
|
-
|
|
83580
|
+
});
|
|
83581
|
+
const canRun = canRunResearch(request, opts.allowViewers ?? false);
|
|
83582
|
+
const limited = request.principal !== void 0 && !request.principal.scopes.includes(WILDCARD_SCOPE);
|
|
83583
|
+
return { runs: runs2, nextCursor, providers, access: { canRun, dailyRunLimit: canRun && limited ? viewerDailyRunLimit : null } };
|
|
83263
83584
|
});
|
|
83264
83585
|
app.get("/projects/:name/research/runs/:runId", async (request) => {
|
|
83265
83586
|
const project = resolveProject(app.db, request.params.name);
|
|
83266
83587
|
return getDetail(app, project.id, request.params.runId);
|
|
83267
83588
|
});
|
|
83268
83589
|
}
|
|
83590
|
+
var DIRECT_REQUEST_HASH_VERSION = "direct-v2:";
|
|
83591
|
+
function directResearchRequestHash(input) {
|
|
83592
|
+
const { idempotencyKey: _key, ...request } = input;
|
|
83593
|
+
return DIRECT_REQUEST_HASH_VERSION + crypto49.createHash("sha256").update(JSON.stringify(request)).digest("hex");
|
|
83594
|
+
}
|
|
83595
|
+
function assertSameDirectResearchRequest(existing, input, hash) {
|
|
83596
|
+
if (existing.requestHash?.startsWith(DIRECT_REQUEST_HASH_VERSION)) {
|
|
83597
|
+
if (existing.requestHash !== hash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
|
|
83598
|
+
return;
|
|
83599
|
+
}
|
|
83600
|
+
const scope = input.scope ? resolveStoredResearchScope(existing.scope, input.scope, input.idempotencyKey) : null;
|
|
83601
|
+
const template = input.template ? resolveStoredResearchTemplate(existing.template, input.template, input.idempotencyKey) : null;
|
|
83602
|
+
if (input.template?.bindingLocation !== void 0) throw alreadyExists("Research idempotency key", input.idempotencyKey);
|
|
83603
|
+
const normalized = {
|
|
83604
|
+
queries: input.queries,
|
|
83605
|
+
provider: input.provider ?? existing.provider,
|
|
83606
|
+
model: input.model ?? null,
|
|
83607
|
+
location: input.location === void 0 ? existing.location ?? null : input.location,
|
|
83608
|
+
...scope ? { scope } : {},
|
|
83609
|
+
...template ? { template } : {}
|
|
83610
|
+
};
|
|
83611
|
+
if (crypto49.createHash("sha256").update(JSON.stringify(normalized)).digest("hex") !== existing.requestHash) throw alreadyExists("Research idempotency key", input.idempotencyKey);
|
|
83612
|
+
}
|
|
83613
|
+
var researchCursorSchema = z5.object({ projectId: z5.string().min(1), createdAt: z5.string().datetime({ offset: true }), id: z5.string().min(1).max(256) }).strict();
|
|
83614
|
+
function parseResearchCursor(value, projectId) {
|
|
83615
|
+
try {
|
|
83616
|
+
if (value.length > 2048 || !/^[\w-]+$/.test(value)) throw new Error("Invalid encoding");
|
|
83617
|
+
const cursor = researchCursorSchema.parse(JSON.parse(Buffer.from(value, "base64url").toString("utf8")));
|
|
83618
|
+
if (cursor.projectId !== projectId) throw new Error("Different project");
|
|
83619
|
+
return cursor;
|
|
83620
|
+
} catch {
|
|
83621
|
+
throw validationError("Invalid research history cursor. Reload the first page and try again.");
|
|
83622
|
+
}
|
|
83623
|
+
}
|
|
83269
83624
|
function resolveStoredResearchScope(stored, selection, idempotencyKey) {
|
|
83270
83625
|
if (!stored || stored.kind !== selection.kind || stored.key !== selection.key || selection.expectedPlanRevision !== void 0 && selection.expectedPlanRevision !== stored.planRevision) {
|
|
83271
83626
|
throw alreadyExists("Research idempotency key", idempotencyKey);
|
|
83272
83627
|
}
|
|
83273
83628
|
return stored;
|
|
83274
83629
|
}
|
|
83630
|
+
function batchChildReceiptKey(rootKey, index2) {
|
|
83631
|
+
const derived = crypto49.createHash("sha256").update(`${rootKey}\0${index2}`).digest("hex");
|
|
83632
|
+
return `${BATCH_RECEIPT_PREFIX}${derived}:${index2}`;
|
|
83633
|
+
}
|
|
83634
|
+
function researchBatchRequestHash(input) {
|
|
83635
|
+
return crypto49.createHash("sha256").update(JSON.stringify({ runs: input.runs })).digest("hex");
|
|
83636
|
+
}
|
|
83637
|
+
function prepareBatchRun(app, opts, project, active, input) {
|
|
83638
|
+
const adapters = opts.providerAdapters ?? [];
|
|
83639
|
+
const configured = new Set(opts.configuredProviderNames ?? []);
|
|
83640
|
+
const providerName = input.provider;
|
|
83641
|
+
const adapter = adapters.find((candidate) => candidate.name === providerName);
|
|
83642
|
+
if (!adapter || adapter.mode !== "api" || isBrowserProvider(providerName) || !configured.has(providerName)) {
|
|
83643
|
+
throw validationError("Research requires a configured API provider.", { provider: providerName, validProviders: adapters.filter((a) => a.mode === "api" && configured.has(a.name)).map((a) => a.name) });
|
|
83644
|
+
}
|
|
83645
|
+
adapter.modelValidationPattern.lastIndex = 0;
|
|
83646
|
+
if (!adapter.modelValidationPattern.test(input.model) || !adapter.modelConfigurable && input.model !== adapter.defaultModel) {
|
|
83647
|
+
throw validationError(`Invalid model "${input.model}" for provider "${providerName}".`, { provider: providerName, model: input.model, hint: adapter.modelValidationHint });
|
|
83648
|
+
}
|
|
83649
|
+
const location = input.location;
|
|
83650
|
+
if (location && !project.locations.some((item2) => sameLocation(item2, location))) {
|
|
83651
|
+
throw validationError("Research location must exactly match a configured project location.", { location });
|
|
83652
|
+
}
|
|
83653
|
+
if (deduplicateResearchQueries(input.queries).length !== input.queries.length) throw validationError("Research queries must be unique within each batch destination.");
|
|
83654
|
+
const scope = input.scope ? resolveResearchScope(active, input.scope) : null;
|
|
83655
|
+
const template = input.template ? resolveResearchTemplate(app, project.id, input.template, scope, location) : null;
|
|
83656
|
+
return {
|
|
83657
|
+
input,
|
|
83658
|
+
providerName,
|
|
83659
|
+
requestedModel: input.model,
|
|
83660
|
+
// The batch contract requires the model so retries never consult a project or instance default.
|
|
83661
|
+
resolvedModel: input.model,
|
|
83662
|
+
location,
|
|
83663
|
+
scope,
|
|
83664
|
+
template,
|
|
83665
|
+
queryClasses: classifyResearchQueries(project, active?.plan ?? null, input.queries)
|
|
83666
|
+
};
|
|
83667
|
+
}
|
|
83275
83668
|
function resolveResearchScope(active, selection) {
|
|
83276
83669
|
if (!active || active.plan.schemaVersion !== 2) {
|
|
83277
83670
|
throw validationError("Research scope requires an active Advanced Measurement plan.");
|
|
@@ -83292,8 +83685,7 @@ function resolveStoredResearchTemplate(stored, selection, idempotencyKey) {
|
|
|
83292
83685
|
}
|
|
83293
83686
|
return stored;
|
|
83294
83687
|
}
|
|
83295
|
-
function resolveResearchTemplate(app, projectId, selection, scope) {
|
|
83296
|
-
if (!scope) throw validationError("Research templates require a selected market or property scope.");
|
|
83688
|
+
function resolveResearchTemplate(app, projectId, selection, scope, location) {
|
|
83297
83689
|
const savedTemplate = app.db.select().from(measurementQueryTemplates).where(and55(
|
|
83298
83690
|
eq65(measurementQueryTemplates.projectId, projectId),
|
|
83299
83691
|
eq65(measurementQueryTemplates.id, selection.templateId)
|
|
@@ -83303,7 +83695,12 @@ function resolveResearchTemplate(app, projectId, selection, scope) {
|
|
|
83303
83695
|
if (templateVersion !== selection.templateVersion) {
|
|
83304
83696
|
throw validationError("The selected research template is stale. Reload it and try again.");
|
|
83305
83697
|
}
|
|
83306
|
-
const
|
|
83698
|
+
const bindingLocation = selection.bindingLocation === void 0 ? location : selection.bindingLocation;
|
|
83699
|
+
if (bindingLocation) {
|
|
83700
|
+
const project = app.db.select({ locations: projects.locations }).from(projects).where(eq65(projects.id, projectId)).get();
|
|
83701
|
+
if (!project.locations.some((item2) => sameLocation(item2, bindingLocation))) throw validationError("Research template binding location must match a configured project location.");
|
|
83702
|
+
}
|
|
83703
|
+
const { bindings, output } = expandResearchTemplate(savedTemplate, scope, bindingLocation);
|
|
83307
83704
|
if (!output || output.length > 4e3) throw validationError("Research template output must be between 1 and 4000 characters.");
|
|
83308
83705
|
return { templateId: selection.templateId, templateVersion, template: savedTemplate.pattern, bindings, output };
|
|
83309
83706
|
}
|
|
@@ -83327,11 +83724,13 @@ function serializeQuery(row) {
|
|
|
83327
83724
|
function researchPrincipal(request) {
|
|
83328
83725
|
const principal = request.principal;
|
|
83329
83726
|
if (!principal) return null;
|
|
83727
|
+
const actor = principal.delegatedUser ? { ...principal.delegatedUser, kind: "user" } : principal;
|
|
83330
83728
|
return {
|
|
83331
|
-
kind:
|
|
83332
|
-
id:
|
|
83333
|
-
name:
|
|
83334
|
-
role:
|
|
83729
|
+
kind: actor.kind,
|
|
83730
|
+
id: actor.id,
|
|
83731
|
+
name: actor.name,
|
|
83732
|
+
role: actor.kind === "user" ? actor.role ?? null : null,
|
|
83733
|
+
...!principal.scopes.includes(WILDCARD_SCOPE) ? { limited: true } : {}
|
|
83335
83734
|
};
|
|
83336
83735
|
}
|
|
83337
83736
|
function utcDayBounds(now) {
|
|
@@ -83455,6 +83854,7 @@ async function apiRoutes(app, opts) {
|
|
|
83455
83854
|
});
|
|
83456
83855
|
if (!opts.skipAuth) {
|
|
83457
83856
|
await authPlugin(api, {
|
|
83857
|
+
researchAllowViewers: opts.researchAllowViewers,
|
|
83458
83858
|
// A bearer that is not an api key is tried as an OAuth access token.
|
|
83459
83859
|
// Wired unconditionally: the table is empty until an operator registers
|
|
83460
83860
|
// a client, so an install with no OAuth in use pays one indexed miss.
|
|
@@ -83671,7 +84071,7 @@ async function apiRoutes(app, opts) {
|
|
|
83671
84071
|
embedQueries: opts.embedQueries
|
|
83672
84072
|
});
|
|
83673
84073
|
await api.register(researchRoutes, {
|
|
83674
|
-
|
|
84074
|
+
getCachedProviderModels: opts.getCachedProviderModels,
|
|
83675
84075
|
getEffectiveProviderModels: opts.getEffectiveProviderModels,
|
|
83676
84076
|
providerAdapters: opts.providerAdapters,
|
|
83677
84077
|
configuredProviderNames: opts.providerSummary?.filter((provider) => provider.configured).map((provider) => provider.name),
|
|
@@ -84114,7 +84514,7 @@ function analyzableRunPredicates(projectId) {
|
|
|
84114
84514
|
eq67(runs.projectId, projectId),
|
|
84115
84515
|
// Only this kind writes query_snapshots.
|
|
84116
84516
|
eq67(runs.kind, RunKinds["answer-visibility"]),
|
|
84117
|
-
|
|
84517
|
+
or15(eq67(runs.status, RunStatuses.completed), eq67(runs.status, RunStatuses.partial)),
|
|
84118
84518
|
// Defensive: RunCoordinator already skips probes before analyzeAndPersist
|
|
84119
84519
|
// is called, but a future call site invoking it directly for a probe must
|
|
84120
84520
|
// still not pollute the intelligence window.
|
|
@@ -84704,7 +85104,7 @@ var IntelligenceService = class {
|
|
|
84704
85104
|
and57(
|
|
84705
85105
|
eq67(runs.projectId, projectId),
|
|
84706
85106
|
eq67(runs.kind, RunKinds["answer-visibility"]),
|
|
84707
|
-
|
|
85107
|
+
or15(eq67(runs.status, "completed"), eq67(runs.status, "partial")),
|
|
84708
85108
|
// Defensive — see top of file.
|
|
84709
85109
|
ne10(runs.trigger, RunTriggers.probe)
|
|
84710
85110
|
)
|