@ainyc/canonry 4.27.0 → 4.27.2
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.
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
gaTrafficSummaries,
|
|
51
51
|
gaTrafficWindowSummaries,
|
|
52
52
|
groupInsights,
|
|
53
|
+
groupRunsByCreatedAt,
|
|
53
54
|
gscCoverageSnapshots,
|
|
54
55
|
gscSearchData,
|
|
55
56
|
gscUrlInspections,
|
|
@@ -60,6 +61,7 @@ import {
|
|
|
60
61
|
mapOpportunitiesToNextSteps,
|
|
61
62
|
notifications,
|
|
62
63
|
parseJsonColumn,
|
|
64
|
+
pickGroupRepresentative,
|
|
63
65
|
projects,
|
|
64
66
|
queries,
|
|
65
67
|
querySnapshots,
|
|
@@ -68,7 +70,7 @@ import {
|
|
|
68
70
|
schedules,
|
|
69
71
|
trafficSources,
|
|
70
72
|
usageCounters
|
|
71
|
-
} from "./chunk-
|
|
73
|
+
} from "./chunk-NXXD6TX7.js";
|
|
72
74
|
import {
|
|
73
75
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
74
76
|
AGENT_PROVIDER_IDS,
|
|
@@ -2317,12 +2319,16 @@ async function analyticsRoutes(app) {
|
|
|
2317
2319
|
const project = resolveProject(app.db, request.params.name);
|
|
2318
2320
|
const window = parseWindow(request.query.window);
|
|
2319
2321
|
const cutoff = windowCutoff(window);
|
|
2320
|
-
const
|
|
2322
|
+
const completedRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial");
|
|
2323
|
+
const latestGroup = groupRunsByCreatedAt(completedRuns)[0] ?? [];
|
|
2324
|
+
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
2325
|
+
const latestRun = pickGroupRepresentative(latestGroup);
|
|
2321
2326
|
if (!latestRun) {
|
|
2322
2327
|
return reply.send({ cited: [], gap: [], uncited: [], mentionedQueries: [], mentionGap: [], notMentioned: [], runId: "", window });
|
|
2323
2328
|
}
|
|
2324
2329
|
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
2325
2330
|
const windowRunIds = windowRuns.map((r) => r.id);
|
|
2331
|
+
const runIdToCreatedAt = new Map(windowRuns.map((r) => [r.id, r.createdAt]));
|
|
2326
2332
|
const consistencyMap = /* @__PURE__ */ new Map();
|
|
2327
2333
|
if (windowRunIds.length > 0) {
|
|
2328
2334
|
const allWindowSnaps = app.db.select({
|
|
@@ -2333,14 +2339,15 @@ async function analyticsRoutes(app) {
|
|
|
2333
2339
|
answerText: querySnapshots.answerText
|
|
2334
2340
|
}).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all();
|
|
2335
2341
|
for (const s of allWindowSnaps) {
|
|
2342
|
+
const timePoint = runIdToCreatedAt.get(s.runId) ?? s.runId;
|
|
2336
2343
|
let entry = consistencyMap.get(s.queryId);
|
|
2337
2344
|
if (!entry) {
|
|
2338
2345
|
entry = { citedRuns: /* @__PURE__ */ new Set(), totalRuns: /* @__PURE__ */ new Set(), mentionedRuns: /* @__PURE__ */ new Set() };
|
|
2339
2346
|
consistencyMap.set(s.queryId, entry);
|
|
2340
2347
|
}
|
|
2341
|
-
entry.totalRuns.add(
|
|
2342
|
-
if (s.citationState === CitationStates.cited) entry.citedRuns.add(
|
|
2343
|
-
if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(
|
|
2348
|
+
entry.totalRuns.add(timePoint);
|
|
2349
|
+
if (s.citationState === CitationStates.cited) entry.citedRuns.add(timePoint);
|
|
2350
|
+
if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(timePoint);
|
|
2344
2351
|
}
|
|
2345
2352
|
}
|
|
2346
2353
|
const rawSnapshots = app.db.select({
|
|
@@ -2351,7 +2358,7 @@ async function analyticsRoutes(app) {
|
|
|
2351
2358
|
answerMentioned: querySnapshots.answerMentioned,
|
|
2352
2359
|
answerText: querySnapshots.answerText,
|
|
2353
2360
|
competitorOverlap: querySnapshots.competitorOverlap
|
|
2354
|
-
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(
|
|
2361
|
+
}).from(querySnapshots).leftJoin(queries, eq10(querySnapshots.queryId, queries.id)).where(inArray2(querySnapshots.runId, latestGroupRunIds)).all();
|
|
2355
2362
|
const snapshots = rawSnapshots.map((s) => ({
|
|
2356
2363
|
...s,
|
|
2357
2364
|
resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
|
|
@@ -2435,11 +2442,12 @@ async function analyticsRoutes(app) {
|
|
|
2435
2442
|
const project = resolveProject(app.db, request.params.name);
|
|
2436
2443
|
const window = parseWindow(request.query.window);
|
|
2437
2444
|
const cutoff = windowCutoff(window);
|
|
2438
|
-
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
2445
|
+
const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt), desc3(runs.id)).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
|
|
2439
2446
|
if (windowRuns.length === 0) {
|
|
2440
2447
|
return reply.send({ overall: [], byQuery: {}, runId: "", window });
|
|
2441
2448
|
}
|
|
2442
|
-
const
|
|
2449
|
+
const latestGroup = groupRunsByCreatedAt(windowRuns)[0] ?? [];
|
|
2450
|
+
const latestRunId = pickGroupRepresentative(latestGroup)?.id ?? windowRuns[0].id;
|
|
2443
2451
|
const windowRunIds = windowRuns.map((r) => r.id);
|
|
2444
2452
|
const snapshots = app.db.select({
|
|
2445
2453
|
queryId: querySnapshots.queryId,
|
|
@@ -2604,7 +2612,7 @@ function buildCategoryCounts(counts) {
|
|
|
2604
2612
|
}
|
|
2605
2613
|
|
|
2606
2614
|
// ../api-routes/src/intelligence.ts
|
|
2607
|
-
import { eq as eq11, desc as desc4, and as and3 } from "drizzle-orm";
|
|
2615
|
+
import { eq as eq11, desc as desc4, and as and3, inArray as inArray3 } from "drizzle-orm";
|
|
2608
2616
|
function emptyHealthSnapshot(projectId) {
|
|
2609
2617
|
return {
|
|
2610
2618
|
id: `no-data:${projectId}`,
|
|
@@ -2648,6 +2656,44 @@ function mapHealthRow(r) {
|
|
|
2648
2656
|
status: "ready"
|
|
2649
2657
|
};
|
|
2650
2658
|
}
|
|
2659
|
+
function aggregateHealthSnapshots(projectId, rows) {
|
|
2660
|
+
if (rows.length === 1) return mapHealthRow(rows[0]);
|
|
2661
|
+
let totalPairs = 0;
|
|
2662
|
+
let citedPairs = 0;
|
|
2663
|
+
const mergedProviders = {};
|
|
2664
|
+
let newestCreatedAt = "";
|
|
2665
|
+
const runIds = [];
|
|
2666
|
+
for (const row of rows) {
|
|
2667
|
+
totalPairs += row.totalPairs;
|
|
2668
|
+
citedPairs += row.citedPairs;
|
|
2669
|
+
if (row.createdAt > newestCreatedAt) newestCreatedAt = row.createdAt;
|
|
2670
|
+
if (row.runId) runIds.push(row.runId);
|
|
2671
|
+
const providerBreakdown = parseJsonColumn(row.providerBreakdown, {});
|
|
2672
|
+
for (const [provider, entry] of Object.entries(providerBreakdown)) {
|
|
2673
|
+
const existing = mergedProviders[provider] ?? { total: 0, cited: 0, citedRate: 0 };
|
|
2674
|
+
existing.total += entry.total;
|
|
2675
|
+
existing.cited += entry.cited;
|
|
2676
|
+
mergedProviders[provider] = existing;
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
for (const entry of Object.values(mergedProviders)) {
|
|
2680
|
+
entry.citedRate = entry.total > 0 ? entry.cited / entry.total : 0;
|
|
2681
|
+
}
|
|
2682
|
+
const overallCitedRate = totalPairs > 0 ? citedPairs / totalPairs : 0;
|
|
2683
|
+
return {
|
|
2684
|
+
// Synthetic id so consumers can tell this is an aggregate; concatenate
|
|
2685
|
+
// source runIds for traceability without inventing a new schema column.
|
|
2686
|
+
id: `group:${runIds.join(",")}`,
|
|
2687
|
+
projectId,
|
|
2688
|
+
runId: runIds[0] ?? null,
|
|
2689
|
+
overallCitedRate,
|
|
2690
|
+
totalPairs,
|
|
2691
|
+
citedPairs,
|
|
2692
|
+
providerBreakdown: mergedProviders,
|
|
2693
|
+
createdAt: newestCreatedAt,
|
|
2694
|
+
status: "ready"
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2651
2697
|
async function intelligenceRoutes(app) {
|
|
2652
2698
|
app.get("/projects/:name/insights", async (request, reply) => {
|
|
2653
2699
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -2679,11 +2725,27 @@ async function intelligenceRoutes(app) {
|
|
|
2679
2725
|
});
|
|
2680
2726
|
app.get("/projects/:name/health/latest", async (request, reply) => {
|
|
2681
2727
|
const project = resolveProject(app.db, request.params.name);
|
|
2682
|
-
const
|
|
2683
|
-
|
|
2728
|
+
const projectVisRuns = app.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(and3(
|
|
2729
|
+
eq11(runs.projectId, project.id),
|
|
2730
|
+
eq11(runs.kind, RunKinds["answer-visibility"]),
|
|
2731
|
+
inArray3(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
2732
|
+
)).orderBy(desc4(runs.createdAt), desc4(runs.id)).all();
|
|
2733
|
+
const latestGroup = groupRunsByCreatedAt(projectVisRuns)[0] ?? [];
|
|
2734
|
+
const latestGroupRunIds = latestGroup.map((r) => r.id);
|
|
2735
|
+
if (latestGroupRunIds.length > 0) {
|
|
2736
|
+
const groupRows = app.db.select().from(healthSnapshots).where(and3(
|
|
2737
|
+
eq11(healthSnapshots.projectId, project.id),
|
|
2738
|
+
inArray3(healthSnapshots.runId, latestGroupRunIds)
|
|
2739
|
+
)).all();
|
|
2740
|
+
if (groupRows.length > 0) {
|
|
2741
|
+
return reply.send(aggregateHealthSnapshots(project.id, groupRows));
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
const fallback = app.db.select().from(healthSnapshots).where(eq11(healthSnapshots.projectId, project.id)).orderBy(desc4(healthSnapshots.createdAt)).limit(1).get();
|
|
2745
|
+
if (!fallback) {
|
|
2684
2746
|
return reply.send(emptyHealthSnapshot(project.id));
|
|
2685
2747
|
}
|
|
2686
|
-
return reply.send(mapHealthRow(
|
|
2748
|
+
return reply.send(mapHealthRow(fallback));
|
|
2687
2749
|
});
|
|
2688
2750
|
app.get("/projects/:name/health/history", async (request, reply) => {
|
|
2689
2751
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -2695,7 +2757,7 @@ async function intelligenceRoutes(app) {
|
|
|
2695
2757
|
}
|
|
2696
2758
|
|
|
2697
2759
|
// ../api-routes/src/report.ts
|
|
2698
|
-
import { and as and5, desc as desc6, eq as eq13, gte, inArray as
|
|
2760
|
+
import { and as and5, desc as desc6, eq as eq13, gte, inArray as inArray5, lt, lte, ne, or as or2, sql as sql3 } from "drizzle-orm";
|
|
2699
2761
|
|
|
2700
2762
|
// ../api-routes/src/report-renderer.ts
|
|
2701
2763
|
var COLORS = {
|
|
@@ -4931,7 +4993,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
4931
4993
|
}
|
|
4932
4994
|
|
|
4933
4995
|
// ../api-routes/src/content-data.ts
|
|
4934
|
-
import { and as and4, eq as eq12, desc as desc5, inArray as
|
|
4996
|
+
import { and as and4, eq as eq12, desc as desc5, inArray as inArray4 } from "drizzle-orm";
|
|
4935
4997
|
var RECENT_RUNS_WINDOW = 5;
|
|
4936
4998
|
function loadOrchestratorInput(db, project, locationFilter = void 0) {
|
|
4937
4999
|
const projectId = project.id;
|
|
@@ -5061,7 +5123,7 @@ function listRecentAnswerVisibilityRunIds(db, projectId, limit, locationFilter)
|
|
|
5061
5123
|
// Queued/running/failed/cancelled runs may have partial or no
|
|
5062
5124
|
// snapshots; including them risks pointing latestRunId at a run with
|
|
5063
5125
|
// no usable evidence.
|
|
5064
|
-
|
|
5126
|
+
inArray4(runs.status, [RunStatuses.completed, RunStatuses.partial])
|
|
5065
5127
|
)
|
|
5066
5128
|
).orderBy(desc5(runs.createdAt)).all();
|
|
5067
5129
|
const filtered = locationFilter === void 0 ? rows : rows.filter((r) => (r.location ?? null) === locationFilter);
|
|
@@ -5103,7 +5165,7 @@ function buildCandidateQueries(opts) {
|
|
|
5103
5165
|
const queryRows = opts.db.select({ id: queries.id, text: queries.query }).from(queries).where(eq12(queries.projectId, opts.projectId)).all();
|
|
5104
5166
|
const queryIdByText = new Map(queryRows.map((r) => [r.text, r.id]));
|
|
5105
5167
|
const candidateQueryIds = opts.candidateQueryStrings.map((q) => queryIdByText.get(q)).filter((id) => Boolean(id));
|
|
5106
|
-
const snapshotRows = opts.db.select().from(querySnapshots).where(
|
|
5168
|
+
const snapshotRows = opts.db.select().from(querySnapshots).where(inArray4(querySnapshots.runId, opts.recentRunIds)).all().filter((r) => candidateQueryIds.includes(r.queryId));
|
|
5107
5169
|
const snapshotsByQuery = /* @__PURE__ */ new Map();
|
|
5108
5170
|
for (const row of snapshotRows) {
|
|
5109
5171
|
const list = snapshotsByQuery.get(row.queryId) ?? [];
|
|
@@ -5317,7 +5379,11 @@ function categorizeQuery(query, projectDisplayName, canonicalDomain) {
|
|
|
5317
5379
|
return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectDisplayName));
|
|
5318
5380
|
}
|
|
5319
5381
|
function loadSnapshotsForRun(db, runId) {
|
|
5320
|
-
|
|
5382
|
+
return loadSnapshotsForRunIds(db, [runId]);
|
|
5383
|
+
}
|
|
5384
|
+
function loadSnapshotsForRunIds(db, runIds) {
|
|
5385
|
+
if (runIds.length === 0) return [];
|
|
5386
|
+
const rows = db.select().from(querySnapshots).where(inArray5(querySnapshots.runId, [...runIds])).all();
|
|
5321
5387
|
return rows.map((r) => ({
|
|
5322
5388
|
id: r.id,
|
|
5323
5389
|
runId: r.runId,
|
|
@@ -5897,7 +5963,7 @@ function buildInsightList(db, projectId, locationFilter) {
|
|
|
5897
5963
|
)
|
|
5898
5964
|
).orderBy(desc6(runs.createdAt)).all().filter((r) => locationFilter === void 0 || (r.location ?? null) === locationFilter).slice(0, INSIGHT_LOOKBACK_RUNS).map((r) => r.id);
|
|
5899
5965
|
if (recentRunIds.length === 0) return [];
|
|
5900
|
-
const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId),
|
|
5966
|
+
const rows = db.select().from(insights).where(and5(eq13(insights.projectId, projectId), inArray5(insights.runId, recentRunIds))).orderBy(desc6(insights.createdAt)).all();
|
|
5901
5967
|
const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
5902
5968
|
const flat = rows.filter((r) => !r.dismissed).map((r) => {
|
|
5903
5969
|
const recommendation = parseJsonColumn(r.recommendation, null);
|
|
@@ -6436,13 +6502,15 @@ function buildWhatsChanged(input) {
|
|
|
6436
6502
|
function buildProjectReport(db, projectName) {
|
|
6437
6503
|
const project = resolveProject(db, projectName);
|
|
6438
6504
|
const queryLookup = loadQueryLookup(db, project.id);
|
|
6439
|
-
const allRuns = db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc6(runs.createdAt)).all();
|
|
6505
|
+
const allRuns = db.select().from(runs).where(eq13(runs.projectId, project.id)).orderBy(desc6(runs.createdAt), desc6(runs.id)).all();
|
|
6440
6506
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
6441
|
-
const
|
|
6442
|
-
(r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial
|
|
6443
|
-
)
|
|
6444
|
-
const
|
|
6445
|
-
const
|
|
6507
|
+
const completedVisRunGroups = groupRunsByCreatedAt(
|
|
6508
|
+
visibilityRuns.filter((r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial)
|
|
6509
|
+
);
|
|
6510
|
+
const latestVisRunGroup = completedVisRunGroups[0] ?? [];
|
|
6511
|
+
const representativeLatestRun = pickGroupRepresentative(latestVisRunGroup) ?? visibilityRuns[0] ?? null;
|
|
6512
|
+
const latestSnapshots = loadSnapshotsForRunIds(db, latestVisRunGroup.map((r) => r.id));
|
|
6513
|
+
const latestRunLocation = representativeLatestRun?.location ?? null;
|
|
6446
6514
|
const competitorRows = db.select().from(competitors).where(eq13(competitors.projectId, project.id)).all();
|
|
6447
6515
|
const competitorDomains = competitorRows.map((c) => c.domain);
|
|
6448
6516
|
const ownedDomains = parseJsonColumn(project.ownedDomains, []);
|
|
@@ -6509,7 +6577,7 @@ function buildProjectReport(db, projectName) {
|
|
|
6509
6577
|
const previousPoint = citationsTrend.length >= 2 ? citationsTrend.at(-2) : null;
|
|
6510
6578
|
let trend = "unknown";
|
|
6511
6579
|
if (!trendBaseline && latestPoint) {
|
|
6512
|
-
const latestRunOnTrend =
|
|
6580
|
+
const latestRunOnTrend = representativeLatestRun?.id === latestPoint.runId;
|
|
6513
6581
|
const currentRate = latestRunOnTrend ? latestPoint.citationRate : citationRate;
|
|
6514
6582
|
const priorRate = latestRunOnTrend ? previousPoint?.citationRate : latestPoint.citationRate;
|
|
6515
6583
|
if (priorRate !== void 0) {
|
|
@@ -6531,7 +6599,7 @@ function buildProjectReport(db, projectName) {
|
|
|
6531
6599
|
const periodStart = citationsTrend[0]?.date ?? null;
|
|
6532
6600
|
const periodEnd = citationsTrend.at(-1)?.date ?? null;
|
|
6533
6601
|
const configuredLocations = parseJsonColumn(project.locations, []);
|
|
6534
|
-
const reportLocation = buildLocationMeta(
|
|
6602
|
+
const reportLocation = buildLocationMeta(representativeLatestRun?.location ?? null, configuredLocations);
|
|
6535
6603
|
const providerLocationHandling = reportLocation ? buildProviderLocationHandling(citationScorecard.providers) : [];
|
|
6536
6604
|
const executiveSummary = {
|
|
6537
6605
|
citationRate,
|
|
@@ -6657,7 +6725,7 @@ async function reportRoutes(app) {
|
|
|
6657
6725
|
}
|
|
6658
6726
|
|
|
6659
6727
|
// ../api-routes/src/citations.ts
|
|
6660
|
-
import { eq as eq14, inArray as
|
|
6728
|
+
import { eq as eq14, inArray as inArray6 } from "drizzle-orm";
|
|
6661
6729
|
async function citationRoutes(app) {
|
|
6662
6730
|
app.get("/projects/:name/citations/visibility", async (request, reply) => {
|
|
6663
6731
|
const project = resolveProject(app.db, request.params.name);
|
|
@@ -6681,7 +6749,7 @@ async function citationRoutes(app) {
|
|
|
6681
6749
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
6682
6750
|
answerMentioned: querySnapshots.answerMentioned,
|
|
6683
6751
|
createdAt: querySnapshots.createdAt
|
|
6684
|
-
}).from(querySnapshots).where(
|
|
6752
|
+
}).from(querySnapshots).where(inArray6(querySnapshots.runId, projectRuns.map((r) => r.id))).all();
|
|
6685
6753
|
if (rawSnapshots.length === 0) {
|
|
6686
6754
|
return reply.send(emptyCitationVisibility("no-runs-yet"));
|
|
6687
6755
|
}
|
|
@@ -6821,7 +6889,7 @@ function normalizeDomain2(domain) {
|
|
|
6821
6889
|
}
|
|
6822
6890
|
|
|
6823
6891
|
// ../api-routes/src/composites.ts
|
|
6824
|
-
import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as
|
|
6892
|
+
import { eq as eq15, and as and6, desc as desc7, sql as sql4, like, or as or3, inArray as inArray7 } from "drizzle-orm";
|
|
6825
6893
|
var TOP_INSIGHT_LIMIT = 5;
|
|
6826
6894
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
6827
6895
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
@@ -6839,15 +6907,17 @@ async function compositeRoutes(app) {
|
|
|
6839
6907
|
const project = resolveProject(app.db, request.params.name);
|
|
6840
6908
|
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
6841
6909
|
const sinceIso = parseSinceFilter(request.query.since);
|
|
6842
|
-
const allRunsRaw = app.db.select().from(runs).where(eq15(runs.projectId, project.id)).orderBy(desc7(runs.createdAt)).all();
|
|
6910
|
+
const allRunsRaw = app.db.select().from(runs).where(eq15(runs.projectId, project.id)).orderBy(desc7(runs.createdAt), desc7(runs.id)).all();
|
|
6843
6911
|
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
6844
6912
|
const totalRuns = allRuns.length;
|
|
6845
6913
|
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
6846
6914
|
const completedVisRuns = visibilityRuns.filter(
|
|
6847
6915
|
(r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial
|
|
6848
6916
|
);
|
|
6849
|
-
const
|
|
6850
|
-
const
|
|
6917
|
+
const visRunGroups = groupRunsByCreatedAt(completedVisRuns);
|
|
6918
|
+
const latestVisRunGroup = visRunGroups[0] ?? [];
|
|
6919
|
+
const previousVisRunGroup = visRunGroups[1] ?? [];
|
|
6920
|
+
const previousVisibilityRun = pickGroupRepresentative(previousVisRunGroup);
|
|
6851
6921
|
const latestRunRow = allRuns[0] ?? null;
|
|
6852
6922
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
6853
6923
|
const healthRow = app.db.select().from(healthSnapshots).where(eq15(healthSnapshots.projectId, project.id)).orderBy(desc7(healthSnapshots.createdAt)).limit(1).get();
|
|
@@ -6856,11 +6926,11 @@ async function compositeRoutes(app) {
|
|
|
6856
6926
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
6857
6927
|
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
6858
6928
|
const snapshotRunIds = new Set(sparklineRunIds);
|
|
6859
|
-
|
|
6860
|
-
|
|
6929
|
+
for (const run of latestVisRunGroup) snapshotRunIds.add(run.id);
|
|
6930
|
+
for (const run of previousVisRunGroup) snapshotRunIds.add(run.id);
|
|
6861
6931
|
const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds]);
|
|
6862
|
-
const latestSnapshots =
|
|
6863
|
-
const previousSnapshots =
|
|
6932
|
+
const latestSnapshots = latestVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
|
|
6933
|
+
const previousSnapshots = previousVisRunGroup.flatMap((r) => snapshotsByRun.get(r.id) ?? []);
|
|
6864
6934
|
const { queryCounts, providers } = summarizeFromSnapshots(latestSnapshots);
|
|
6865
6935
|
const transitions = summarizeTransitionsFromSnapshots(
|
|
6866
6936
|
latestSnapshots,
|
|
@@ -7024,7 +7094,7 @@ function loadSnapshotsByRunIds(app, runIds) {
|
|
|
7024
7094
|
citationState: querySnapshots.citationState,
|
|
7025
7095
|
competitorOverlap: querySnapshots.competitorOverlap,
|
|
7026
7096
|
citedDomains: querySnapshots.citedDomains
|
|
7027
|
-
}).from(querySnapshots).where(
|
|
7097
|
+
}).from(querySnapshots).where(inArray7(querySnapshots.runId, [...runIds])).all();
|
|
7028
7098
|
for (const row of rows) {
|
|
7029
7099
|
const list = result.get(row.runId) ?? [];
|
|
7030
7100
|
list.push({
|
|
@@ -22598,7 +22668,7 @@ import crypto24 from "crypto";
|
|
|
22598
22668
|
import fs7 from "fs";
|
|
22599
22669
|
import path9 from "path";
|
|
22600
22670
|
import os5 from "os";
|
|
22601
|
-
import { and as and16, eq as eq27, inArray as
|
|
22671
|
+
import { and as and16, eq as eq27, inArray as inArray8, sql as sql10 } from "drizzle-orm";
|
|
22602
22672
|
|
|
22603
22673
|
// src/run-telemetry.ts
|
|
22604
22674
|
import crypto23 from "crypto";
|
|
@@ -22939,7 +23009,7 @@ var JobRunner = class {
|
|
|
22939
23009
|
this.registry = registry;
|
|
22940
23010
|
}
|
|
22941
23011
|
recoverStaleRuns() {
|
|
22942
|
-
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
23012
|
+
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray8(runs.status, ["running", "queued"])).all();
|
|
22943
23013
|
if (stale.length === 0) return;
|
|
22944
23014
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
22945
23015
|
for (const run of stale) {
|
|
@@ -23002,7 +23072,7 @@ var JobRunner = class {
|
|
|
23002
23072
|
}
|
|
23003
23073
|
log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
|
|
23004
23074
|
const scopedQueryNames = parseJsonColumn(existingRun.queries, null);
|
|
23005
|
-
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId),
|
|
23075
|
+
projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and16(eq27(queries.projectId, projectId), inArray8(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq27(queries.projectId, projectId)).all();
|
|
23006
23076
|
const projectCompetitors = this.db.select().from(competitors).where(eq27(competitors.projectId, projectId)).all();
|
|
23007
23077
|
const competitorDomains = projectCompetitors.map((c) => c.domain);
|
|
23008
23078
|
const allDomains = effectiveDomains({
|
|
@@ -24656,7 +24726,7 @@ var Scheduler = class {
|
|
|
24656
24726
|
};
|
|
24657
24727
|
|
|
24658
24728
|
// src/notifier.ts
|
|
24659
|
-
import { eq as eq35, desc as desc16, and as and22, or as or4 } from "drizzle-orm";
|
|
24729
|
+
import { eq as eq35, desc as desc16, and as and22, inArray as inArray9, or as or4 } from "drizzle-orm";
|
|
24660
24730
|
import crypto31 from "crypto";
|
|
24661
24731
|
var log10 = createLogger("Notifier");
|
|
24662
24732
|
var Notifier = class {
|
|
@@ -24761,41 +24831,76 @@ var Notifier = class {
|
|
|
24761
24831
|
}
|
|
24762
24832
|
}
|
|
24763
24833
|
computeTransitions(runId, projectId) {
|
|
24834
|
+
const thisRun = this.db.select().from(runs).where(eq35(runs.id, runId)).get();
|
|
24835
|
+
if (!thisRun) return [];
|
|
24836
|
+
const groupSiblings = this.db.select().from(runs).where(and22(
|
|
24837
|
+
eq35(runs.projectId, projectId),
|
|
24838
|
+
eq35(runs.kind, thisRun.kind),
|
|
24839
|
+
eq35(runs.createdAt, thisRun.createdAt)
|
|
24840
|
+
)).all();
|
|
24841
|
+
const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
|
|
24842
|
+
if (stillPending) return [];
|
|
24843
|
+
const completedPartialSiblings = groupSiblings.filter(
|
|
24844
|
+
(r) => r.status === "completed" || r.status === "partial"
|
|
24845
|
+
);
|
|
24846
|
+
if (completedPartialSiblings.length === 0) return [];
|
|
24847
|
+
const winner = completedPartialSiblings.reduce((best, candidate) => {
|
|
24848
|
+
const candFinish = candidate.finishedAt ?? "";
|
|
24849
|
+
const bestFinish = best.finishedAt ?? "";
|
|
24850
|
+
if (candFinish > bestFinish) return candidate;
|
|
24851
|
+
if (candFinish < bestFinish) return best;
|
|
24852
|
+
return candidate.id > best.id ? candidate : best;
|
|
24853
|
+
});
|
|
24854
|
+
if (winner.id !== runId) return [];
|
|
24855
|
+
const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq35(projects.id, projectId)).get();
|
|
24856
|
+
const locationCount = Math.max(
|
|
24857
|
+
1,
|
|
24858
|
+
parseJsonColumn(projectLocations?.locations ?? null, []).length
|
|
24859
|
+
);
|
|
24860
|
+
const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
|
|
24764
24861
|
const recentRuns = this.db.select().from(runs).where(
|
|
24765
24862
|
and22(
|
|
24766
24863
|
eq35(runs.projectId, projectId),
|
|
24864
|
+
eq35(runs.kind, thisRun.kind),
|
|
24767
24865
|
or4(eq35(runs.status, "completed"), eq35(runs.status, "partial"))
|
|
24768
24866
|
)
|
|
24769
|
-
).orderBy(desc16(runs.createdAt)).limit(
|
|
24770
|
-
|
|
24771
|
-
const
|
|
24772
|
-
|
|
24773
|
-
|
|
24867
|
+
).orderBy(desc16(runs.createdAt), desc16(runs.id)).limit(RECENT_FETCH_LIMIT).all();
|
|
24868
|
+
const groups = groupRunsByCreatedAt(recentRuns);
|
|
24869
|
+
const currentGroupIdx = groups.findIndex((g) => g[0]?.createdAt === thisRun.createdAt);
|
|
24870
|
+
if (currentGroupIdx < 0) return [];
|
|
24871
|
+
const currentGroup = groups[currentGroupIdx] ?? [];
|
|
24872
|
+
const previousGroup = groups[currentGroupIdx + 1] ?? [];
|
|
24873
|
+
if (currentGroup.length === 0 || previousGroup.length === 0) return [];
|
|
24874
|
+
const currentRunIds = currentGroup.map((r) => r.id);
|
|
24875
|
+
const previousRunIds = previousGroup.map((r) => r.id);
|
|
24774
24876
|
const currentSnapshots = this.db.select({
|
|
24775
24877
|
queryId: querySnapshots.queryId,
|
|
24776
24878
|
query: queries.query,
|
|
24777
24879
|
provider: querySnapshots.provider,
|
|
24880
|
+
location: querySnapshots.location,
|
|
24778
24881
|
citationState: querySnapshots.citationState
|
|
24779
|
-
}).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(
|
|
24882
|
+
}).from(querySnapshots).leftJoin(queries, eq35(querySnapshots.queryId, queries.id)).where(inArray9(querySnapshots.runId, currentRunIds)).all();
|
|
24780
24883
|
const previousSnapshots = this.db.select({
|
|
24781
24884
|
queryId: querySnapshots.queryId,
|
|
24782
24885
|
provider: querySnapshots.provider,
|
|
24886
|
+
location: querySnapshots.location,
|
|
24783
24887
|
citationState: querySnapshots.citationState
|
|
24784
|
-
}).from(querySnapshots).where(
|
|
24888
|
+
}).from(querySnapshots).where(inArray9(querySnapshots.runId, previousRunIds)).all();
|
|
24785
24889
|
const prevMap = /* @__PURE__ */ new Map();
|
|
24786
24890
|
for (const s of previousSnapshots) {
|
|
24787
|
-
prevMap.set(`${s.queryId}:${s.provider}`, s.citationState);
|
|
24891
|
+
prevMap.set(`${s.queryId}:${s.provider}:${s.location ?? ""}`, s.citationState);
|
|
24788
24892
|
}
|
|
24789
24893
|
const transitions = [];
|
|
24790
24894
|
for (const s of currentSnapshots) {
|
|
24791
|
-
const key = `${s.queryId}:${s.provider}`;
|
|
24895
|
+
const key = `${s.queryId}:${s.provider}:${s.location ?? ""}`;
|
|
24792
24896
|
const prevState = prevMap.get(key);
|
|
24793
24897
|
if (prevState && prevState !== s.citationState) {
|
|
24794
24898
|
transitions.push({
|
|
24795
24899
|
query: s.query ?? s.queryId,
|
|
24796
24900
|
from: prevState,
|
|
24797
24901
|
to: s.citationState,
|
|
24798
|
-
provider: s.provider
|
|
24902
|
+
provider: s.provider,
|
|
24903
|
+
location: s.location
|
|
24799
24904
|
});
|
|
24800
24905
|
}
|
|
24801
24906
|
}
|
|
@@ -1960,6 +1960,33 @@ function migrate(db) {
|
|
|
1960
1960
|
}
|
|
1961
1961
|
}
|
|
1962
1962
|
|
|
1963
|
+
// ../db/src/run-helpers.ts
|
|
1964
|
+
function groupRunsByCreatedAt(rows) {
|
|
1965
|
+
const groups = [];
|
|
1966
|
+
let current = [];
|
|
1967
|
+
let currentCreatedAt = null;
|
|
1968
|
+
for (const row of rows) {
|
|
1969
|
+
if (row.createdAt === currentCreatedAt) {
|
|
1970
|
+
current.push(row);
|
|
1971
|
+
} else {
|
|
1972
|
+
if (current.length > 0) groups.push(current);
|
|
1973
|
+
current = [row];
|
|
1974
|
+
currentCreatedAt = row.createdAt;
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
if (current.length > 0) groups.push(current);
|
|
1978
|
+
return groups;
|
|
1979
|
+
}
|
|
1980
|
+
function pickGroupRepresentative(group) {
|
|
1981
|
+
if (group.length === 0) return null;
|
|
1982
|
+
let best = group[0];
|
|
1983
|
+
for (let i = 1; i < group.length; i++) {
|
|
1984
|
+
const candidate = group[i];
|
|
1985
|
+
if (candidate.id > best.id) best = candidate;
|
|
1986
|
+
}
|
|
1987
|
+
return best;
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1963
1990
|
// ../intelligence/src/regressions.ts
|
|
1964
1991
|
function detectRegressions(currentRun, previousRun) {
|
|
1965
1992
|
const regressions = [];
|
|
@@ -3560,20 +3587,49 @@ var IntelligenceService = class {
|
|
|
3560
3587
|
const key = row.query.toLowerCase();
|
|
3561
3588
|
gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
|
|
3562
3589
|
}
|
|
3563
|
-
const
|
|
3590
|
+
const projectRow = this.db.select({ locations: projects.locations }).from(projects).where(eq(projects.id, projectId)).get();
|
|
3591
|
+
const locationCount = Math.max(
|
|
3592
|
+
1,
|
|
3593
|
+
parseJsonColumn(projectRow?.locations ?? null, []).length
|
|
3594
|
+
);
|
|
3595
|
+
const ROWS_PER_GROUP_BUDGET = Math.max(2, locationCount);
|
|
3596
|
+
const recentRunRows = this.db.select({ id: runs.id, createdAt: runs.createdAt }).from(runs).where(
|
|
3564
3597
|
and(
|
|
3565
3598
|
eq(runs.projectId, projectId),
|
|
3566
3599
|
eq(runs.kind, RunKinds["answer-visibility"]),
|
|
3567
3600
|
or(eq(runs.status, "completed"), eq(runs.status, "partial"))
|
|
3568
3601
|
)
|
|
3569
|
-
).orderBy(desc(runs.createdAt)).limit(RECURRENCE_LOOKBACK_RUNS + 1)
|
|
3602
|
+
).orderBy(desc(runs.createdAt), desc(runs.id)).limit((RECURRENCE_LOOKBACK_RUNS + 1) * ROWS_PER_GROUP_BUDGET).all();
|
|
3603
|
+
const recentGroups = groupRunsByCreatedAt(recentRunRows);
|
|
3604
|
+
const recentRunIds = [];
|
|
3605
|
+
const recentRunIdToCreatedAt = /* @__PURE__ */ new Map();
|
|
3606
|
+
let consumedGroups = 0;
|
|
3607
|
+
for (const group of recentGroups) {
|
|
3608
|
+
const groupIds = group.map((r) => r.id);
|
|
3609
|
+
if (groupIds.includes(excludeRunId)) continue;
|
|
3610
|
+
for (const r of group) recentRunIdToCreatedAt.set(r.id, r.createdAt);
|
|
3611
|
+
recentRunIds.push(...groupIds);
|
|
3612
|
+
consumedGroups++;
|
|
3613
|
+
if (consumedGroups >= RECURRENCE_LOOKBACK_RUNS) break;
|
|
3614
|
+
}
|
|
3570
3615
|
const haveHistory = recentRunIds.length > 0;
|
|
3571
3616
|
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
3572
3617
|
if (haveHistory) {
|
|
3573
|
-
const priorRows = this.db.select({ query: insights.query, provider: insights.provider }).from(insights).where(and(eq(insights.type, "regression"), inArray(insights.runId, recentRunIds))).all();
|
|
3618
|
+
const priorRows = this.db.select({ query: insights.query, provider: insights.provider, runId: insights.runId }).from(insights).where(and(eq(insights.type, "regression"), inArray(insights.runId, recentRunIds))).all();
|
|
3619
|
+
const regressionGroups = /* @__PURE__ */ new Map();
|
|
3574
3620
|
for (const row of priorRows) {
|
|
3621
|
+
if (!row.runId) continue;
|
|
3575
3622
|
const key = `${row.query}:${row.provider}`;
|
|
3576
|
-
|
|
3623
|
+
const groupKey = recentRunIdToCreatedAt.get(row.runId) ?? row.runId;
|
|
3624
|
+
let groups = regressionGroups.get(key);
|
|
3625
|
+
if (!groups) {
|
|
3626
|
+
groups = /* @__PURE__ */ new Set();
|
|
3627
|
+
regressionGroups.set(key, groups);
|
|
3628
|
+
}
|
|
3629
|
+
groups.add(groupKey);
|
|
3630
|
+
}
|
|
3631
|
+
for (const [key, groups] of regressionGroups) {
|
|
3632
|
+
priorRegressionsByPair.set(key, groups.size);
|
|
3577
3633
|
}
|
|
3578
3634
|
}
|
|
3579
3635
|
return rawInsights.map((insight) => {
|
|
@@ -3649,6 +3705,8 @@ export {
|
|
|
3649
3705
|
extractLegacyCredentials,
|
|
3650
3706
|
dropLegacyCredentialColumns,
|
|
3651
3707
|
migrate,
|
|
3708
|
+
groupRunsByCreatedAt,
|
|
3709
|
+
pickGroupRepresentative,
|
|
3652
3710
|
isBlogShapedQuery,
|
|
3653
3711
|
buildInventory,
|
|
3654
3712
|
buildContentTargetRows,
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
setTelemetrySource,
|
|
21
21
|
showFirstRunNotice,
|
|
22
22
|
trackEvent
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-ICWFH4JA.js";
|
|
24
24
|
import {
|
|
25
25
|
CliError,
|
|
26
26
|
EXIT_SYSTEM_ERROR,
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
queries,
|
|
50
50
|
querySnapshots,
|
|
51
51
|
runs
|
|
52
|
-
} from "./chunk-
|
|
52
|
+
} from "./chunk-NXXD6TX7.js";
|
|
53
53
|
import {
|
|
54
54
|
CcReleaseSyncStatuses,
|
|
55
55
|
CheckScopes,
|
|
@@ -621,7 +621,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
621
621
|
return result;
|
|
622
622
|
}
|
|
623
623
|
async function backfillInsightsCommand(project, opts) {
|
|
624
|
-
const { IntelligenceService } = await import("./intelligence-service-
|
|
624
|
+
const { IntelligenceService } = await import("./intelligence-service-Z6QIELKP.js");
|
|
625
625
|
const config = loadConfig();
|
|
626
626
|
const db = createClient(config.database);
|
|
627
627
|
migrate(db);
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createServer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-ICWFH4JA.js";
|
|
4
4
|
import {
|
|
5
5
|
loadConfig
|
|
6
6
|
} from "./chunk-2FAEQ56I.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-NXXD6TX7.js";
|
|
8
8
|
import "./chunk-HVW665A4.js";
|
|
9
9
|
export {
|
|
10
10
|
createServer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainyc/canonry",
|
|
3
|
-
"version": "4.27.
|
|
3
|
+
"version": "4.27.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
|
|
6
6
|
"license": "FSL-1.1-ALv2",
|
|
@@ -60,18 +60,18 @@
|
|
|
60
60
|
"tsup": "^8.5.1",
|
|
61
61
|
"tsx": "^4.19.0",
|
|
62
62
|
"@ainyc/canonry-api-routes": "0.0.0",
|
|
63
|
-
"@ainyc/canonry-config": "0.0.0",
|
|
64
63
|
"@ainyc/canonry-contracts": "0.0.0",
|
|
65
|
-
"@ainyc/canonry-
|
|
66
|
-
"@ainyc/canonry-integration-commoncrawl": "0.0.0",
|
|
67
|
-
"@ainyc/canonry-integration-google": "0.0.0",
|
|
64
|
+
"@ainyc/canonry-config": "0.0.0",
|
|
68
65
|
"@ainyc/canonry-intelligence": "0.0.0",
|
|
66
|
+
"@ainyc/canonry-integration-commoncrawl": "0.0.0",
|
|
69
67
|
"@ainyc/canonry-integration-cloud-run": "0.0.0",
|
|
68
|
+
"@ainyc/canonry-db": "0.0.0",
|
|
69
|
+
"@ainyc/canonry-integration-bing": "0.0.0",
|
|
70
70
|
"@ainyc/canonry-integration-traffic": "0.0.0",
|
|
71
|
+
"@ainyc/canonry-integration-google": "0.0.0",
|
|
71
72
|
"@ainyc/canonry-integration-wordpress": "0.0.0",
|
|
72
73
|
"@ainyc/canonry-provider-cdp": "0.0.0",
|
|
73
74
|
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
74
|
-
"@ainyc/canonry-integration-bing": "0.0.0",
|
|
75
75
|
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
76
76
|
"@ainyc/canonry-provider-local": "0.0.0",
|
|
77
77
|
"@ainyc/canonry-provider-perplexity": "0.0.0",
|