@ainyc/canonry 4.54.0 → 4.55.3
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/references/server-side-traffic.md +9 -4
- package/assets/assets/{BacklinksPage-BXFT4pLI.js → BacklinksPage-buvZ4ZOd.js} +1 -1
- package/assets/assets/{ProjectPage-DAtd9Vay.js → ProjectPage-D0UqSqe7.js} +4 -4
- package/assets/assets/{RunRow-38dDceGl.js → RunRow-D-DTu1PA.js} +1 -1
- package/assets/assets/{RunsPage-AJnFLtaE.js → RunsPage-CrBpgwkO.js} +1 -1
- package/assets/assets/{SettingsPage-FT9ZAvFH.js → SettingsPage-Bgsi9tZ2.js} +1 -1
- package/assets/assets/TrafficPage-DAHXrzqz.js +1 -0
- package/assets/assets/TrafficSourceDetailPage-DCcDN3VD.js +1 -0
- package/assets/assets/extract-error-message-BGhWiJPr.js +1 -0
- package/assets/assets/{index-DLPKqyhx.js → index-CbDkoDBH.js} +62 -62
- package/assets/assets/{index-Bm3JQsW0.css → index-dxdJhCQO.css} +1 -1
- package/assets/assets/{server-traffic-GqiQYm6x.js → server-traffic-3xxyOEIX.js} +1 -1
- package/assets/assets/{trash-2-BwPzJ8NI.js → trash-2-dppRdHYI.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-CRO6Q25G.js → chunk-5EAGNVCJ.js} +423 -250
- package/dist/{chunk-J7MX3YOH.js → chunk-UOQ62KDD.js} +8 -3
- package/dist/{chunk-JHAHNKSN.js → chunk-XB6Y63NI.js} +260 -5
- package/dist/{chunk-VZPDBHBW.js → chunk-XHU35P3S.js} +367 -365
- package/dist/cli.js +14 -12
- package/dist/index.d.ts +13 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-OCREQUCQ.js → intelligence-service-4PT22FED.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +15 -14
- package/assets/assets/TrafficPage-B4A3oO8M.js +0 -1
- package/assets/assets/TrafficSourceDetailPage-8NYU1TA6.js +0 -1
- package/assets/assets/arrow-left-DgI0X1Q1.js +0 -1
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
trafficConnectVercelRequestSchema,
|
|
23
23
|
trafficConnectWordpressRequestSchema,
|
|
24
24
|
trafficEventKindSchema
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-XHU35P3S.js";
|
|
26
26
|
|
|
27
27
|
// src/config.ts
|
|
28
28
|
import fs from "fs";
|
|
@@ -3564,9 +3564,14 @@ var ApiClient = class {
|
|
|
3564
3564
|
})
|
|
3565
3565
|
);
|
|
3566
3566
|
}
|
|
3567
|
-
async listRuns(project, limit) {
|
|
3567
|
+
async listRuns(project, limit, kind) {
|
|
3568
3568
|
return this.invoke(
|
|
3569
|
-
() => getApiV1ProjectsByNameRuns({
|
|
3569
|
+
() => getApiV1ProjectsByNameRuns({
|
|
3570
|
+
client: this.heyClient,
|
|
3571
|
+
path: { name: project },
|
|
3572
|
+
// kind arrives as a free CLI string; the server validates it against the enum.
|
|
3573
|
+
query: { limit, kind }
|
|
3574
|
+
})
|
|
3570
3575
|
);
|
|
3571
3576
|
}
|
|
3572
3577
|
async getLatestRun(project) {
|
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
categorizeSourceWithCompetitors,
|
|
10
10
|
categoryLabel,
|
|
11
11
|
determineAnswerMentioned,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
effectiveDomains,
|
|
13
|
+
normalizeProjectDomain,
|
|
14
|
+
registrableDomain
|
|
15
|
+
} from "./chunk-XHU35P3S.js";
|
|
14
16
|
|
|
15
17
|
// src/intelligence-service.ts
|
|
16
18
|
import { eq, desc, asc, and, ne, or, inArray } from "drizzle-orm";
|
|
@@ -243,10 +245,19 @@ var googleConnections = sqliteTable("google_connections", {
|
|
|
243
245
|
propertyId: text("property_id"),
|
|
244
246
|
sitemapUrl: text("sitemap_url"),
|
|
245
247
|
scopes: text("scopes", { mode: "json" }).$type().notNull().default([]),
|
|
248
|
+
// The project that established this connection. Used by the OAuth callback
|
|
249
|
+
// and the DELETE route to refuse cross-project takeover (a malicious caller
|
|
250
|
+
// who points another project at the same `canonicalDomain` cannot overwrite
|
|
251
|
+
// or remove an existing connection owned by the original project). Nullable
|
|
252
|
+
// for legacy rows written before the column existed — those are treated as
|
|
253
|
+
// unowned and the first connect call to claim them succeeds. See root
|
|
254
|
+
// AGENTS.md "Deployment Posture" for the broader multi-tenancy posture.
|
|
255
|
+
createdByProjectId: text("created_by_project_id").references(() => projects.id, { onDelete: "set null" }),
|
|
246
256
|
createdAt: text("created_at").notNull(),
|
|
247
257
|
updatedAt: text("updated_at").notNull()
|
|
248
258
|
}, (table) => [
|
|
249
|
-
uniqueIndex("idx_google_conn_domain_type").on(table.domain, table.connectionType)
|
|
259
|
+
uniqueIndex("idx_google_conn_domain_type").on(table.domain, table.connectionType),
|
|
260
|
+
index("idx_google_conn_project").on(table.createdByProjectId)
|
|
250
261
|
]);
|
|
251
262
|
var gscSearchData = sqliteTable("gsc_search_data", {
|
|
252
263
|
id: text("id").primaryKey(),
|
|
@@ -319,10 +330,16 @@ var bingConnections = sqliteTable("bing_connections", {
|
|
|
319
330
|
id: text("id").primaryKey(),
|
|
320
331
|
domain: text("domain").notNull(),
|
|
321
332
|
siteUrl: text("site_url"),
|
|
333
|
+
// Same takeover-prevention column as `google_connections.createdByProjectId`.
|
|
334
|
+
// The Bing connect / disconnect routes refuse cross-project writes when an
|
|
335
|
+
// existing row's owner doesn't match. Null for legacy rows (treated as
|
|
336
|
+
// unowned).
|
|
337
|
+
createdByProjectId: text("created_by_project_id").references(() => projects.id, { onDelete: "set null" }),
|
|
322
338
|
createdAt: text("created_at").notNull(),
|
|
323
339
|
updatedAt: text("updated_at").notNull()
|
|
324
340
|
}, (table) => [
|
|
325
|
-
uniqueIndex("idx_bing_conn_domain").on(table.domain)
|
|
341
|
+
uniqueIndex("idx_bing_conn_domain").on(table.domain),
|
|
342
|
+
index("idx_bing_conn_project").on(table.createdByProjectId)
|
|
326
343
|
]);
|
|
327
344
|
var bingUrlInspections = sqliteTable("bing_url_inspections", {
|
|
328
345
|
id: text("id").primaryKey(),
|
|
@@ -2157,6 +2174,68 @@ var MIGRATION_VERSIONS = [
|
|
|
2157
2174
|
`DELETE FROM crawler_events_hourly WHERE bot_id = 'mistral-ai' AND sampled_user_agent LIKE '%MistralAI-User%'`,
|
|
2158
2175
|
`UPDATE crawler_events_hourly SET bot_id = 'mistral-bot' WHERE bot_id = 'mistral-ai'`
|
|
2159
2176
|
]
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
version: 66,
|
|
2180
|
+
name: "oauth-connections-track-owning-project",
|
|
2181
|
+
// Cross-project OAuth takeover defense. Before this column, the OAuth
|
|
2182
|
+
// callback for Google and the connect route for Bing keyed everything on
|
|
2183
|
+
// `domain` alone — an attacker who created a project pointed at a victim's
|
|
2184
|
+
// canonical domain could complete OAuth from their own Google/Bing account
|
|
2185
|
+
// and silently overwrite the legitimate refresh token under that domain
|
|
2186
|
+
// key. The new `created_by_project_id` column records the project that
|
|
2187
|
+
// first established each connection; the callback and DELETE routes refuse
|
|
2188
|
+
// cross-project writes when it doesn't match.
|
|
2189
|
+
//
|
|
2190
|
+
// Backfill: for each existing connection row, set the owner to the project
|
|
2191
|
+
// whose `canonical_domain` matches AND whose `created_at` is oldest (the
|
|
2192
|
+
// most likely original owner in a 1:N domain-shared install). Rows with no
|
|
2193
|
+
// matching project stay NULL — treated as "unowned" so a future legitimate
|
|
2194
|
+
// connect from any project can claim them.
|
|
2195
|
+
//
|
|
2196
|
+
// Uses the `run` hook so the schema-edit + backfill only fire when the
|
|
2197
|
+
// target tables exist. The legacy-keyword test scenario seeds a DB at v46
|
|
2198
|
+
// without google_connections / bing_connections (they're created in v6 but
|
|
2199
|
+
// the test bypasses the bootstrap) — without the guard, this version's
|
|
2200
|
+
// ALTER fails with "no such table".
|
|
2201
|
+
//
|
|
2202
|
+
// Idempotent: column-existence guard means re-running this version is a
|
|
2203
|
+
// no-op; the backfill UPDATE only writes rows where the column is NULL.
|
|
2204
|
+
statements: [],
|
|
2205
|
+
run: (db) => {
|
|
2206
|
+
if (tableExists(db, "google_connections") && !columnExists(db, "google_connections", "created_by_project_id")) {
|
|
2207
|
+
db.run(sql.raw(
|
|
2208
|
+
`ALTER TABLE google_connections ADD COLUMN created_by_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
|
|
2209
|
+
));
|
|
2210
|
+
db.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_google_conn_project ON google_connections(created_by_project_id)`));
|
|
2211
|
+
db.run(sql.raw(
|
|
2212
|
+
`UPDATE google_connections
|
|
2213
|
+
SET created_by_project_id = (
|
|
2214
|
+
SELECT p.id FROM projects p
|
|
2215
|
+
WHERE LOWER(p.canonical_domain) = LOWER(google_connections.domain)
|
|
2216
|
+
ORDER BY p.created_at ASC
|
|
2217
|
+
LIMIT 1
|
|
2218
|
+
)
|
|
2219
|
+
WHERE created_by_project_id IS NULL`
|
|
2220
|
+
));
|
|
2221
|
+
}
|
|
2222
|
+
if (tableExists(db, "bing_connections") && !columnExists(db, "bing_connections", "created_by_project_id")) {
|
|
2223
|
+
db.run(sql.raw(
|
|
2224
|
+
`ALTER TABLE bing_connections ADD COLUMN created_by_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`
|
|
2225
|
+
));
|
|
2226
|
+
db.run(sql.raw(`CREATE INDEX IF NOT EXISTS idx_bing_conn_project ON bing_connections(created_by_project_id)`));
|
|
2227
|
+
db.run(sql.raw(
|
|
2228
|
+
`UPDATE bing_connections
|
|
2229
|
+
SET created_by_project_id = (
|
|
2230
|
+
SELECT p.id FROM projects p
|
|
2231
|
+
WHERE LOWER(p.canonical_domain) = LOWER(bing_connections.domain)
|
|
2232
|
+
ORDER BY p.created_at ASC
|
|
2233
|
+
LIMIT 1
|
|
2234
|
+
)
|
|
2235
|
+
WHERE created_by_project_id IS NULL`
|
|
2236
|
+
));
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2160
2239
|
}
|
|
2161
2240
|
];
|
|
2162
2241
|
function isDuplicateColumnError(err) {
|
|
@@ -4079,6 +4158,172 @@ function createLogger(module) {
|
|
|
4079
4158
|
};
|
|
4080
4159
|
}
|
|
4081
4160
|
|
|
4161
|
+
// src/citation-utils.ts
|
|
4162
|
+
function domainMatches(domain, canonicalDomain) {
|
|
4163
|
+
const normalized = normalizeProjectDomain(canonicalDomain);
|
|
4164
|
+
const d = normalizeProjectDomain(domain);
|
|
4165
|
+
return d === normalized || d.endsWith(`.${normalized}`);
|
|
4166
|
+
}
|
|
4167
|
+
function pickProjectCitedDomain(citedDomains, projectDomains) {
|
|
4168
|
+
for (const cited of citedDomains) {
|
|
4169
|
+
if (projectDomains.some((pd) => domainMatches(cited, pd))) return cited;
|
|
4170
|
+
}
|
|
4171
|
+
return void 0;
|
|
4172
|
+
}
|
|
4173
|
+
function determineCitationState(normalized, domains) {
|
|
4174
|
+
for (const canonicalDomain of domains) {
|
|
4175
|
+
const bareDomain = normalizeProjectDomain(canonicalDomain);
|
|
4176
|
+
if (normalized.citedDomains.some((d) => domainMatches(d, bareDomain))) {
|
|
4177
|
+
return "cited";
|
|
4178
|
+
}
|
|
4179
|
+
const lowerDomain = bareDomain.toLowerCase();
|
|
4180
|
+
for (const source of normalized.groundingSources) {
|
|
4181
|
+
try {
|
|
4182
|
+
const uri = source.uri.toLowerCase();
|
|
4183
|
+
if (lowerDomain.includes(".") && uri.includes(lowerDomain)) {
|
|
4184
|
+
return "cited";
|
|
4185
|
+
}
|
|
4186
|
+
} catch {
|
|
4187
|
+
}
|
|
4188
|
+
if (source.title) {
|
|
4189
|
+
const titleLower = source.title.toLowerCase().replace(/^www\./, "");
|
|
4190
|
+
if (titleLower === lowerDomain || titleLower.endsWith(`.${lowerDomain}`)) {
|
|
4191
|
+
return "cited";
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
return "not-cited";
|
|
4197
|
+
}
|
|
4198
|
+
function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
4199
|
+
const overlapSet = /* @__PURE__ */ new Set();
|
|
4200
|
+
for (const d of normalized.citedDomains) {
|
|
4201
|
+
for (const cd of competitorDomains) {
|
|
4202
|
+
if (domainMatches(d, cd)) {
|
|
4203
|
+
overlapSet.add(cd);
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
for (const source of normalized.groundingSources) {
|
|
4208
|
+
const uri = source.uri.toLowerCase();
|
|
4209
|
+
for (const cd of competitorDomains) {
|
|
4210
|
+
if (uri.includes(cd.toLowerCase())) {
|
|
4211
|
+
overlapSet.add(cd);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
if (normalized.answerText) {
|
|
4216
|
+
const lowerAnswer = normalized.answerText.toLowerCase();
|
|
4217
|
+
for (const cd of competitorDomains) {
|
|
4218
|
+
if (lowerAnswer.includes(cd.toLowerCase())) {
|
|
4219
|
+
overlapSet.add(cd);
|
|
4220
|
+
}
|
|
4221
|
+
const brand = brandLabelFromDomain(cd);
|
|
4222
|
+
if (brand.length >= 4 && new RegExp(`\\b${escapeRegExp(brand)}\\b`, "i").test(lowerAnswer)) {
|
|
4223
|
+
overlapSet.add(cd);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
return [...overlapSet];
|
|
4228
|
+
}
|
|
4229
|
+
function escapeRegExp(value) {
|
|
4230
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4231
|
+
}
|
|
4232
|
+
function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
|
|
4233
|
+
if (!answerText || answerText.length < 20) return [];
|
|
4234
|
+
const ownBrandKeys = new Set(
|
|
4235
|
+
ownDomains.flatMap((domain) => collectBrandKeysFromDomain(domain))
|
|
4236
|
+
);
|
|
4237
|
+
for (const name of ownBrandNames) {
|
|
4238
|
+
const key = brandKeyFromText(name);
|
|
4239
|
+
if (key.length >= 4) ownBrandKeys.add(key);
|
|
4240
|
+
}
|
|
4241
|
+
const knownCompetitorKeys = new Set(
|
|
4242
|
+
[...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandKeysFromDomain(domain)).filter((key) => !ownBrandKeys.has(key))
|
|
4243
|
+
);
|
|
4244
|
+
if (knownCompetitorKeys.size === 0) return [];
|
|
4245
|
+
const candidatePatterns = [
|
|
4246
|
+
/^\s*(?:[-*]|\d+\.)\s+(?:\*\*)?([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50}?)(?:\*\*)?\s*[:\u2014\u2013-]/gm,
|
|
4247
|
+
/\*\*([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50})\*\*/g,
|
|
4248
|
+
/^#{1,4}\s+(?:\d+\.\s+)?(?:\*\*)?([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50}?)(?:\*\*)?$/gm,
|
|
4249
|
+
/\[([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50})\]\(https?:\/\/[^\s)]+\)/g
|
|
4250
|
+
];
|
|
4251
|
+
const genericKeys = /* @__PURE__ */ new Set([
|
|
4252
|
+
"additional",
|
|
4253
|
+
"best",
|
|
4254
|
+
"benefits",
|
|
4255
|
+
"bottomline",
|
|
4256
|
+
"comparison",
|
|
4257
|
+
"conclusion",
|
|
4258
|
+
"directorylisting",
|
|
4259
|
+
"example",
|
|
4260
|
+
"expertise",
|
|
4261
|
+
"features",
|
|
4262
|
+
"finalthoughts",
|
|
4263
|
+
"howitworks",
|
|
4264
|
+
"important",
|
|
4265
|
+
"keybenefits",
|
|
4266
|
+
"keyfeatures",
|
|
4267
|
+
"major",
|
|
4268
|
+
"note",
|
|
4269
|
+
"notable",
|
|
4270
|
+
"option",
|
|
4271
|
+
"other",
|
|
4272
|
+
"overview",
|
|
4273
|
+
"pricing",
|
|
4274
|
+
"pros",
|
|
4275
|
+
"reviews",
|
|
4276
|
+
"step",
|
|
4277
|
+
"summary",
|
|
4278
|
+
"top",
|
|
4279
|
+
"verdict",
|
|
4280
|
+
"whattolookfor",
|
|
4281
|
+
"whyitmatters",
|
|
4282
|
+
"whyitstandsout",
|
|
4283
|
+
"whywechoseit"
|
|
4284
|
+
]);
|
|
4285
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4286
|
+
for (const pattern of candidatePatterns) {
|
|
4287
|
+
let match;
|
|
4288
|
+
while ((match = pattern.exec(answerText)) !== null) {
|
|
4289
|
+
const candidate = cleanCandidateName(match[1] ?? "");
|
|
4290
|
+
const candidateKey = brandKeyFromText(candidate);
|
|
4291
|
+
if (!candidateKey) continue;
|
|
4292
|
+
if (genericKeys.has(candidateKey)) continue;
|
|
4293
|
+
if (candidate.split(/\s+/).length > 6) continue;
|
|
4294
|
+
if (matchesBrandKey(candidateKey, ownBrandKeys)) continue;
|
|
4295
|
+
if (!matchesBrandKey(candidateKey, knownCompetitorKeys)) continue;
|
|
4296
|
+
if (!seen.has(candidateKey)) seen.set(candidateKey, candidate);
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
return [...seen.values()].slice(0, 10);
|
|
4300
|
+
}
|
|
4301
|
+
function cleanCandidateName(candidate) {
|
|
4302
|
+
return candidate.replace(/^[\s"'`]+|[\s"'`.,:;!?]+$/g, "").replace(/\s+/g, " ").trim();
|
|
4303
|
+
}
|
|
4304
|
+
function collectBrandKeysFromDomain(domain) {
|
|
4305
|
+
const reg = registrableDomain(domain);
|
|
4306
|
+
if (!reg) {
|
|
4307
|
+
const hostname = normalizeProjectDomain(domain).split("/")[0] ?? "";
|
|
4308
|
+
const fallback = hostname.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
4309
|
+
return fallback.length >= 4 ? [fallback] : [];
|
|
4310
|
+
}
|
|
4311
|
+
const keys = /* @__PURE__ */ new Set();
|
|
4312
|
+
const fullKey = reg.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
4313
|
+
if (fullKey.length >= 4) keys.add(fullKey);
|
|
4314
|
+
const brand = brandLabelFromDomain(reg).replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
4315
|
+
if (brand.length >= 4) keys.add(brand);
|
|
4316
|
+
return [...keys];
|
|
4317
|
+
}
|
|
4318
|
+
function matchesBrandKey(candidateKey, brandKeys) {
|
|
4319
|
+
for (const brandKey of brandKeys) {
|
|
4320
|
+
if (candidateKey === brandKey) return true;
|
|
4321
|
+
if (candidateKey.startsWith(brandKey) || candidateKey.endsWith(brandKey)) return true;
|
|
4322
|
+
if (brandKey.startsWith(candidateKey) || brandKey.endsWith(candidateKey)) return true;
|
|
4323
|
+
}
|
|
4324
|
+
return false;
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4082
4327
|
// src/intelligence-service.ts
|
|
4083
4328
|
var RECURRENCE_LOOKBACK_RUNS = 5;
|
|
4084
4329
|
var HISTORY_WINDOW_RUNS = Math.max(PERSISTENT_GAP_THRESHOLD, 5);
|
|
@@ -4460,6 +4705,11 @@ var IntelligenceService = class {
|
|
|
4460
4705
|
});
|
|
4461
4706
|
}
|
|
4462
4707
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
4708
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq(projects.id, projectId)).get();
|
|
4709
|
+
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
4710
|
+
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
4711
|
+
ownedDomains: projectDomainRow.ownedDomains
|
|
4712
|
+
}) : [];
|
|
4463
4713
|
const rows = this.db.select({
|
|
4464
4714
|
query: queries.query,
|
|
4465
4715
|
// Denormalized query text persisted by v58 — the fallback when the
|
|
@@ -4486,7 +4736,9 @@ var IntelligenceService = class {
|
|
|
4486
4736
|
query: resolvedQuery,
|
|
4487
4737
|
provider: r.provider,
|
|
4488
4738
|
cited: r.citationState === CitationStates.cited,
|
|
4489
|
-
|
|
4739
|
+
// The project's OWN cited domain — never a co-cited competitor that
|
|
4740
|
+
// happens to sort first in the full citedDomains set.
|
|
4741
|
+
citationUrl: pickProjectCitedDomain(domains, projectDomains),
|
|
4490
4742
|
// Snapshots carry their own location for downstream detectors. In
|
|
4491
4743
|
// practice every snapshot in a single runId shares the run's
|
|
4492
4744
|
// location; the per-row column is the same value duplicated, but
|
|
@@ -4552,6 +4804,9 @@ export {
|
|
|
4552
4804
|
groupRunsByCreatedAt,
|
|
4553
4805
|
pickGroupRepresentative,
|
|
4554
4806
|
filterTrackedSnapshots,
|
|
4807
|
+
determineCitationState,
|
|
4808
|
+
computeCompetitorOverlap,
|
|
4809
|
+
extractRecommendedCompetitors,
|
|
4555
4810
|
isBlogShapedQuery,
|
|
4556
4811
|
buildInventory,
|
|
4557
4812
|
buildContentTargetRows,
|