@ainyc/canonry 4.34.0 → 4.35.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/README.md +2 -2
- package/assets/assets/index-Cfv0_lwq.css +1 -0
- package/assets/assets/index-u7ZXZ5mA.js +302 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-5EBN7736.js → chunk-B3FBOECD.js} +1 -1
- package/dist/{chunk-XW3F5EEW.js → chunk-EM5GVF3C.js} +73 -20
- package/dist/{chunk-7256SFYT.js → chunk-MLS5KJWK.js} +155 -38
- package/dist/{chunk-7AF6B3L6.js → chunk-NLV4MZZF.js} +1084 -279
- package/dist/cli.js +100 -563
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-3P2DMYRR.js → intelligence-service-WAJOEOJV.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +5 -5
- package/assets/assets/index-47V0U52s.js +0 -302
- package/assets/assets/index-CNKAwZMB.css +0 -1
package/assets/index.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
|
|
13
13
|
<link rel="apple-touch-icon" href="./apple-touch-icon.png" />
|
|
14
14
|
<title>Canonry</title>
|
|
15
|
-
<script type="module" crossorigin src="./assets/index-
|
|
16
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
15
|
+
<script type="module" crossorigin src="./assets/index-u7ZXZ5mA.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="./assets/index-Cfv0_lwq.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
19
|
<div id="root"></div>
|
|
@@ -99,8 +99,12 @@ var citationStateSchema = z2.enum(["cited", "not-cited"]);
|
|
|
99
99
|
var CitationStates = citationStateSchema.enum;
|
|
100
100
|
var visibilityStateSchema = z2.enum(["visible", "not-visible"]);
|
|
101
101
|
var VisibilityStates = visibilityStateSchema.enum;
|
|
102
|
+
var mentionStateSchema = z2.enum(["mentioned", "not-mentioned"]);
|
|
103
|
+
var MentionStates = mentionStateSchema.enum;
|
|
102
104
|
var computedTransitionSchema = z2.enum(["new", "cited", "lost", "emerging", "not-cited"]);
|
|
103
105
|
var ComputedTransitions = computedTransitionSchema.enum;
|
|
106
|
+
var mentionTransitionSchema = z2.enum(["new", "mentioned", "lost", "emerging", "not-mentioned"]);
|
|
107
|
+
var MentionTransitions = mentionTransitionSchema.enum;
|
|
104
108
|
var runTriggerRequestSchema = z2.object({
|
|
105
109
|
kind: z2.literal(RunKinds["answer-visibility"]).optional(),
|
|
106
110
|
trigger: z2.literal(RunTriggers.manual).optional(),
|
|
@@ -211,7 +215,10 @@ var querySnapshotDtoSchema = z2.object({
|
|
|
211
215
|
provider: providerNameSchema,
|
|
212
216
|
citationState: citationStateSchema,
|
|
213
217
|
answerMentioned: z2.boolean().optional(),
|
|
218
|
+
/** @deprecated legacy name for `mentionState`; same data, kept for backwards compatibility. */
|
|
214
219
|
visibilityState: visibilityStateSchema.optional(),
|
|
220
|
+
/** Mention state for this snapshot — see `mentionStateSchema`. Prefer this over the legacy `visibilityState`. */
|
|
221
|
+
mentionState: mentionStateSchema.optional(),
|
|
215
222
|
transition: computedTransitionSchema.optional(),
|
|
216
223
|
answerText: z2.string().nullable().optional(),
|
|
217
224
|
citedDomains: z2.array(z2.string()).default([]),
|
|
@@ -235,8 +242,14 @@ var snapshotDiffRowSchema = z2.object({
|
|
|
235
242
|
run2State: citationStateSchema.nullable(),
|
|
236
243
|
run1AnswerMentioned: z2.boolean().nullable(),
|
|
237
244
|
run2AnswerMentioned: z2.boolean().nullable(),
|
|
245
|
+
/** @deprecated legacy name for `run1MentionState`. */
|
|
238
246
|
run1VisibilityState: visibilityStateSchema.nullable(),
|
|
247
|
+
/** @deprecated legacy name for `run2MentionState`. */
|
|
239
248
|
run2VisibilityState: visibilityStateSchema.nullable(),
|
|
249
|
+
/** Mention state in run 1 — prefer this over the legacy `run1VisibilityState`. */
|
|
250
|
+
run1MentionState: mentionStateSchema.nullable().optional(),
|
|
251
|
+
/** Mention state in run 2 — prefer this over the legacy `run2VisibilityState`. */
|
|
252
|
+
run2MentionState: mentionStateSchema.nullable().optional(),
|
|
240
253
|
changed: z2.boolean(),
|
|
241
254
|
visibilityChanged: z2.boolean()
|
|
242
255
|
});
|
|
@@ -409,6 +422,7 @@ var projectUpsertRequestSchema = z4.object({
|
|
|
409
422
|
displayName: z4.string().min(1),
|
|
410
423
|
canonicalDomain: z4.string().min(1),
|
|
411
424
|
ownedDomains: z4.array(z4.string().min(1)).optional(),
|
|
425
|
+
aliases: z4.array(z4.string()).optional(),
|
|
412
426
|
country: z4.string().length(2),
|
|
413
427
|
language: z4.string().min(2),
|
|
414
428
|
tags: z4.array(z4.string()).optional(),
|
|
@@ -425,6 +439,7 @@ var projectDtoSchema = z4.object({
|
|
|
425
439
|
displayName: z4.string().optional(),
|
|
426
440
|
canonicalDomain: z4.string(),
|
|
427
441
|
ownedDomains: z4.array(z4.string()).default([]),
|
|
442
|
+
aliases: z4.array(z4.string()).default([]),
|
|
428
443
|
country: z4.string().length(2),
|
|
429
444
|
language: z4.string().min(2),
|
|
430
445
|
tags: z4.array(z4.string()).default([]),
|
|
@@ -548,6 +563,31 @@ function effectiveDomains(project) {
|
|
|
548
563
|
}
|
|
549
564
|
return result;
|
|
550
565
|
}
|
|
566
|
+
function normalizeProjectAliases(displayName, aliases) {
|
|
567
|
+
if (!aliases || aliases.length === 0) return [];
|
|
568
|
+
const displayKey = displayName?.trim().toLowerCase() ?? "";
|
|
569
|
+
const seen = /* @__PURE__ */ new Set();
|
|
570
|
+
const result = [];
|
|
571
|
+
for (const raw of aliases) {
|
|
572
|
+
const trimmed = raw.trim();
|
|
573
|
+
if (!trimmed) continue;
|
|
574
|
+
const key = trimmed.toLowerCase();
|
|
575
|
+
if (key === displayKey) continue;
|
|
576
|
+
if (seen.has(key)) continue;
|
|
577
|
+
seen.add(key);
|
|
578
|
+
result.push(trimmed);
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
function effectiveBrandNames(project) {
|
|
583
|
+
const names = [];
|
|
584
|
+
const display = project.displayName?.trim() ?? "";
|
|
585
|
+
if (display) names.push(display);
|
|
586
|
+
for (const alias of normalizeProjectAliases(project.displayName, project.aliases)) {
|
|
587
|
+
names.push(alias);
|
|
588
|
+
}
|
|
589
|
+
return names;
|
|
590
|
+
}
|
|
551
591
|
|
|
552
592
|
// ../contracts/src/config-schema.ts
|
|
553
593
|
var configMetadataSchema = z5.object({
|
|
@@ -584,6 +624,7 @@ var configSpecSchema = z5.object({
|
|
|
584
624
|
displayName: z5.string().min(1),
|
|
585
625
|
canonicalDomain: z5.string().min(1),
|
|
586
626
|
ownedDomains: z5.array(z5.string().min(1)).optional().default([]),
|
|
627
|
+
aliases: z5.array(z5.string().min(1)).optional().default([]),
|
|
587
628
|
country: z5.string().length(2),
|
|
588
629
|
language: z5.string().min(2),
|
|
589
630
|
queries: configQueryListSchema.optional(),
|
|
@@ -1407,7 +1448,7 @@ var BUSINESS_SUFFIXES = [
|
|
|
1407
1448
|
"inc",
|
|
1408
1449
|
"ltd"
|
|
1409
1450
|
];
|
|
1410
|
-
function extractAnswerMentions(answerText,
|
|
1451
|
+
function extractAnswerMentions(answerText, brandNames, domains) {
|
|
1411
1452
|
if (!answerText) return { mentioned: false, matchedTerms: [] };
|
|
1412
1453
|
const matchedTerms = [];
|
|
1413
1454
|
const lowerAnswer = answerText.toLowerCase();
|
|
@@ -1420,18 +1461,21 @@ function extractAnswerMentions(answerText, displayName, domains) {
|
|
|
1420
1461
|
}
|
|
1421
1462
|
const answerNormalized = normalizeText(answerText);
|
|
1422
1463
|
const answerBrandKey = brandKeyFromText(answerText);
|
|
1423
|
-
const
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1464
|
+
for (const brandName of brandNames) {
|
|
1465
|
+
if (!brandName || !brandName.trim()) continue;
|
|
1466
|
+
const normalizedCandidates = brandNormalizedCandidates(brandName);
|
|
1467
|
+
const brandKeyCandidates = brandKeyCandidatesForMatch(brandName);
|
|
1468
|
+
const matchesNormalized = normalizedCandidates.some(
|
|
1469
|
+
(c) => new RegExp(`\\b${escapeRegExp(c)}\\b`).test(answerNormalized)
|
|
1470
|
+
);
|
|
1471
|
+
const matchesBrandKey = brandKeyCandidates.some(
|
|
1472
|
+
(c) => c.length >= MIN_BRAND_KEY_LENGTH && answerBrandKey.includes(c)
|
|
1473
|
+
);
|
|
1474
|
+
if (matchesNormalized || matchesBrandKey) {
|
|
1475
|
+
matchedTerms.push(brandName);
|
|
1476
|
+
}
|
|
1433
1477
|
}
|
|
1434
|
-
const brandTokens = collectBrandTokens(
|
|
1478
|
+
const brandTokens = collectBrandTokens(brandNames, domains);
|
|
1435
1479
|
const allTokens = [...brandTokens.primary, ...brandTokens.secondary];
|
|
1436
1480
|
const secondarySet = new Set(brandTokens.secondary);
|
|
1437
1481
|
let tokenMatches = 0;
|
|
@@ -1454,12 +1498,15 @@ function extractAnswerMentions(answerText, displayName, domains) {
|
|
|
1454
1498
|
});
|
|
1455
1499
|
return { mentioned: dedupedFinal.length > 0, matchedTerms: dedupedFinal };
|
|
1456
1500
|
}
|
|
1457
|
-
function determineAnswerMentioned(answerText,
|
|
1458
|
-
return extractAnswerMentions(answerText,
|
|
1501
|
+
function determineAnswerMentioned(answerText, brandNames, domains) {
|
|
1502
|
+
return extractAnswerMentions(answerText, brandNames, domains).mentioned;
|
|
1459
1503
|
}
|
|
1460
1504
|
function visibilityStateFromAnswerMentioned(answerMentioned) {
|
|
1461
1505
|
return answerMentioned ? "visible" : "not-visible";
|
|
1462
1506
|
}
|
|
1507
|
+
function mentionStateFromAnswerMentioned(answerMentioned) {
|
|
1508
|
+
return answerMentioned ? "mentioned" : "not-mentioned";
|
|
1509
|
+
}
|
|
1463
1510
|
function brandKeyFromText(value) {
|
|
1464
1511
|
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1465
1512
|
}
|
|
@@ -1472,14 +1519,17 @@ function domainMentioned(lowerAnswer, normalizedDomain) {
|
|
|
1472
1519
|
];
|
|
1473
1520
|
return patterns.some((pattern) => pattern.test(lowerAnswer));
|
|
1474
1521
|
}
|
|
1475
|
-
function collectBrandTokens(
|
|
1522
|
+
function collectBrandTokens(brandNames, domains) {
|
|
1476
1523
|
const primary = /* @__PURE__ */ new Set();
|
|
1477
1524
|
const secondary = /* @__PURE__ */ new Set();
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1525
|
+
for (const brandName of brandNames) {
|
|
1526
|
+
if (!brandName || !brandName.trim()) continue;
|
|
1527
|
+
const distinctiveWords = extractDistinctiveTokens(brandName);
|
|
1528
|
+
if (distinctiveWords.length > 0) {
|
|
1529
|
+
primary.add(distinctiveWords[0]);
|
|
1530
|
+
for (let i = 1; i < distinctiveWords.length; i++) {
|
|
1531
|
+
secondary.add(distinctiveWords[i]);
|
|
1532
|
+
}
|
|
1483
1533
|
}
|
|
1484
1534
|
}
|
|
1485
1535
|
for (const domain of domains) {
|
|
@@ -2671,6 +2721,8 @@ export {
|
|
|
2671
2721
|
registrableDomain,
|
|
2672
2722
|
brandLabelFromDomain,
|
|
2673
2723
|
effectiveDomains,
|
|
2724
|
+
normalizeProjectAliases,
|
|
2725
|
+
effectiveBrandNames,
|
|
2674
2726
|
projectConfigSchema,
|
|
2675
2727
|
resolveConfigSpecQueries,
|
|
2676
2728
|
wordpressEnvSchema,
|
|
@@ -2700,6 +2752,7 @@ export {
|
|
|
2700
2752
|
extractAnswerMentions,
|
|
2701
2753
|
determineAnswerMentioned,
|
|
2702
2754
|
visibilityStateFromAnswerMentioned,
|
|
2755
|
+
mentionStateFromAnswerMentioned,
|
|
2703
2756
|
brandKeyFromText,
|
|
2704
2757
|
MemorySources,
|
|
2705
2758
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
categoryLabel,
|
|
9
9
|
determineAnswerMentioned,
|
|
10
10
|
normalizeProjectDomain
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-EM5GVF3C.js";
|
|
12
12
|
|
|
13
13
|
// src/intelligence-service.ts
|
|
14
14
|
import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
|
|
@@ -68,6 +68,7 @@ var projects = sqliteTable("projects", {
|
|
|
68
68
|
displayName: text("display_name").notNull(),
|
|
69
69
|
canonicalDomain: text("canonical_domain").notNull(),
|
|
70
70
|
ownedDomains: text("owned_domains").notNull().default("[]"),
|
|
71
|
+
aliases: text("aliases").notNull().default("[]"),
|
|
71
72
|
country: text("country").notNull(),
|
|
72
73
|
language: text("language").notNull(),
|
|
73
74
|
tags: text("tags").notNull().default("[]"),
|
|
@@ -153,7 +154,12 @@ var querySnapshots = sqliteTable("query_snapshots", {
|
|
|
153
154
|
]);
|
|
154
155
|
var auditLog = sqliteTable("audit_log", {
|
|
155
156
|
id: text("id").primaryKey(),
|
|
156
|
-
|
|
157
|
+
// SET NULL (not CASCADE) so deleting a project preserves its audit trail.
|
|
158
|
+
// The DELETE /projects route writes a "project.deleted" row immediately
|
|
159
|
+
// before the delete — a CASCADE here would wipe that record before any
|
|
160
|
+
// reader could see it (the deletion would erase the only evidence it
|
|
161
|
+
// happened). Detached rows surface in audit queries with project_id=NULL.
|
|
162
|
+
projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }),
|
|
157
163
|
actor: text("actor").notNull(),
|
|
158
164
|
action: text("action").notNull(),
|
|
159
165
|
entityType: text("entity_type").notNull(),
|
|
@@ -1885,6 +1891,55 @@ var MIGRATION_VERSIONS = [
|
|
|
1885
1891
|
`CREATE INDEX IF NOT EXISTS idx_snapshots_location ON query_snapshots(location)`,
|
|
1886
1892
|
`CREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON query_snapshots(created_at)`
|
|
1887
1893
|
]
|
|
1894
|
+
},
|
|
1895
|
+
{
|
|
1896
|
+
version: 59,
|
|
1897
|
+
name: "projects-aliases",
|
|
1898
|
+
statements: [
|
|
1899
|
+
`ALTER TABLE projects ADD COLUMN aliases TEXT NOT NULL DEFAULT '[]'`
|
|
1900
|
+
]
|
|
1901
|
+
},
|
|
1902
|
+
{
|
|
1903
|
+
version: 60,
|
|
1904
|
+
name: "audit-log-preserve-on-project-delete",
|
|
1905
|
+
// The legacy `audit_log.project_id` FK was `ON DELETE CASCADE`, so any
|
|
1906
|
+
// `DELETE /projects/:name` call cascade-wiped every audit row for that
|
|
1907
|
+
// project — including the `project.deleted` row the route handler had
|
|
1908
|
+
// just written in the same path. The deletion erased the only record
|
|
1909
|
+
// that the deletion happened, defeating the entire purpose of the
|
|
1910
|
+
// audit log.
|
|
1911
|
+
//
|
|
1912
|
+
// Fix: rebuild `audit_log` with `project_id` as `ON DELETE SET NULL`.
|
|
1913
|
+
// Existing rows survive verbatim; future deletions detach audit rows
|
|
1914
|
+
// from the project (project_id=NULL) instead of erasing them. SQLite
|
|
1915
|
+
// can't change FK behavior in place — same canonical table-rebuild
|
|
1916
|
+
// pattern v58 used for `query_snapshots`.
|
|
1917
|
+
statements: [
|
|
1918
|
+
`CREATE TABLE IF NOT EXISTS audit_log_v60 (
|
|
1919
|
+
id TEXT PRIMARY KEY,
|
|
1920
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1921
|
+
actor TEXT NOT NULL,
|
|
1922
|
+
action TEXT NOT NULL,
|
|
1923
|
+
entity_type TEXT NOT NULL,
|
|
1924
|
+
entity_id TEXT,
|
|
1925
|
+
diff TEXT,
|
|
1926
|
+
created_at TEXT NOT NULL
|
|
1927
|
+
)`,
|
|
1928
|
+
// LEFT JOIN guard mirrors v58: if a pre-existing row carries a
|
|
1929
|
+
// dangling project_id (from a pre-FK era or a write with
|
|
1930
|
+
// PRAGMA foreign_keys=OFF), the join nulls it out rather than
|
|
1931
|
+
// failing the migration on the new FK validation.
|
|
1932
|
+
`INSERT INTO audit_log_v60 (
|
|
1933
|
+
id, project_id, actor, action, entity_type, entity_id, diff, created_at
|
|
1934
|
+
)
|
|
1935
|
+
SELECT a.id, p.id, a.actor, a.action, a.entity_type, a.entity_id, a.diff, a.created_at
|
|
1936
|
+
FROM audit_log a
|
|
1937
|
+
LEFT JOIN projects p ON p.id = a.project_id`,
|
|
1938
|
+
`DROP TABLE audit_log`,
|
|
1939
|
+
`ALTER TABLE audit_log_v60 RENAME TO audit_log`,
|
|
1940
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_log_project ON audit_log(project_id)`,
|
|
1941
|
+
`CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)`
|
|
1942
|
+
]
|
|
1888
1943
|
}
|
|
1889
1944
|
];
|
|
1890
1945
|
function isDuplicateColumnError(err) {
|
|
@@ -2074,19 +2129,26 @@ function filterTrackedSnapshots(rows) {
|
|
|
2074
2129
|
}
|
|
2075
2130
|
|
|
2076
2131
|
// ../intelligence/src/regressions.ts
|
|
2132
|
+
function snapshotKey(snap) {
|
|
2133
|
+
const loc = snap.location ?? "__none__";
|
|
2134
|
+
return JSON.stringify([snap.query, snap.provider, loc]);
|
|
2135
|
+
}
|
|
2077
2136
|
function detectRegressions(currentRun, previousRun) {
|
|
2137
|
+
if ((currentRun.location ?? null) !== (previousRun.location ?? null)) {
|
|
2138
|
+
return [];
|
|
2139
|
+
}
|
|
2078
2140
|
const regressions = [];
|
|
2079
2141
|
const previousCited = /* @__PURE__ */ new Map();
|
|
2080
2142
|
for (const snap of previousRun.snapshots) {
|
|
2081
2143
|
if (snap.cited) {
|
|
2082
|
-
previousCited.set(
|
|
2144
|
+
previousCited.set(snapshotKey(snap), {
|
|
2083
2145
|
citationUrl: snap.citationUrl,
|
|
2084
2146
|
position: snap.position
|
|
2085
2147
|
});
|
|
2086
2148
|
}
|
|
2087
2149
|
}
|
|
2088
2150
|
for (const snap of currentRun.snapshots) {
|
|
2089
|
-
const key =
|
|
2151
|
+
const key = snapshotKey(snap);
|
|
2090
2152
|
if (!snap.cited && previousCited.has(key)) {
|
|
2091
2153
|
const prev = previousCited.get(key);
|
|
2092
2154
|
regressions.push({
|
|
@@ -2103,16 +2165,23 @@ function detectRegressions(currentRun, previousRun) {
|
|
|
2103
2165
|
}
|
|
2104
2166
|
|
|
2105
2167
|
// ../intelligence/src/gains.ts
|
|
2168
|
+
function snapshotKey2(snap) {
|
|
2169
|
+
const loc = snap.location ?? "__none__";
|
|
2170
|
+
return JSON.stringify([snap.query, snap.provider, loc]);
|
|
2171
|
+
}
|
|
2106
2172
|
function detectGains(currentRun, previousRun) {
|
|
2173
|
+
if ((currentRun.location ?? null) !== (previousRun.location ?? null)) {
|
|
2174
|
+
return [];
|
|
2175
|
+
}
|
|
2107
2176
|
const gains = [];
|
|
2108
2177
|
const previousCited = /* @__PURE__ */ new Set();
|
|
2109
2178
|
for (const snap of previousRun.snapshots) {
|
|
2110
2179
|
if (snap.cited) {
|
|
2111
|
-
previousCited.add(
|
|
2180
|
+
previousCited.add(snapshotKey2(snap));
|
|
2112
2181
|
}
|
|
2113
2182
|
}
|
|
2114
2183
|
for (const snap of currentRun.snapshots) {
|
|
2115
|
-
const key =
|
|
2184
|
+
const key = snapshotKey2(snap);
|
|
2116
2185
|
if (snap.cited && !previousCited.has(key)) {
|
|
2117
2186
|
gains.push({
|
|
2118
2187
|
query: snap.query,
|
|
@@ -2173,17 +2242,26 @@ function computeHealthTrend(runs2) {
|
|
|
2173
2242
|
|
|
2174
2243
|
// ../intelligence/src/causes.ts
|
|
2175
2244
|
function analyzeCause(regression, currentSnapshots) {
|
|
2176
|
-
const
|
|
2177
|
-
(s) => s.query === regression.query && s.provider === regression.provider && !s.cited
|
|
2245
|
+
const matchingSnaps = currentSnapshots.filter(
|
|
2246
|
+
(s) => s.query === regression.query && s.provider === regression.provider && !s.cited
|
|
2178
2247
|
);
|
|
2179
|
-
|
|
2180
|
-
|
|
2248
|
+
const withCompetitor = matchingSnaps.find((s) => s.competitorDomains?.length);
|
|
2249
|
+
if (withCompetitor) {
|
|
2250
|
+
const competitor = withCompetitor.competitorDomains[0];
|
|
2181
2251
|
return {
|
|
2182
2252
|
cause: "competitor_gain",
|
|
2183
2253
|
competitorDomain: competitor,
|
|
2184
2254
|
details: `Competitor ${competitor} now cited for "${regression.query}" on ${regression.provider}`
|
|
2185
2255
|
};
|
|
2186
2256
|
}
|
|
2257
|
+
const withCited = matchingSnaps.find((s) => s.citedDomains?.length);
|
|
2258
|
+
if (withCited) {
|
|
2259
|
+
const top = withCited.citedDomains.slice(0, 3);
|
|
2260
|
+
return {
|
|
2261
|
+
cause: "third_party_displacement",
|
|
2262
|
+
details: `${regression.provider} now grounds on ${top.join(", ")} for "${regression.query}" \u2014 none are tracked competitors.`
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2187
2265
|
return {
|
|
2188
2266
|
cause: "unknown",
|
|
2189
2267
|
details: `No specific cause identified for loss of "${regression.query}" on ${regression.provider}`
|
|
@@ -2626,10 +2704,13 @@ function buildDrivers(input) {
|
|
|
2626
2704
|
if (input.action === "create" && input.position === null) {
|
|
2627
2705
|
drivers.push("no existing page");
|
|
2628
2706
|
}
|
|
2629
|
-
if (input.position !== null
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2707
|
+
if (input.position !== null) {
|
|
2708
|
+
const positionDisplay = Math.round(input.position);
|
|
2709
|
+
if (input.position > 30) {
|
|
2710
|
+
drivers.push(`page ranks #${positionDisplay} (effectively invisible)`);
|
|
2711
|
+
} else if (input.position > 10) {
|
|
2712
|
+
drivers.push(`page ranks #${positionDisplay}`);
|
|
2713
|
+
}
|
|
2633
2714
|
}
|
|
2634
2715
|
if (input.action === "add-schema") {
|
|
2635
2716
|
drivers.push("cited by LLMs but lacks structured data");
|
|
@@ -2900,7 +2981,7 @@ function classifyRegressionSeverity(signals) {
|
|
|
2900
2981
|
}
|
|
2901
2982
|
|
|
2902
2983
|
// ../intelligence/src/insight-grouping.ts
|
|
2903
|
-
function groupInsights(insights2, keyFn = (i) =>
|
|
2984
|
+
function groupInsights(insights2, keyFn = (i) => JSON.stringify([i.query, i.provider, i.type])) {
|
|
2904
2985
|
const order = [];
|
|
2905
2986
|
const buckets = /* @__PURE__ */ new Map();
|
|
2906
2987
|
for (const i of insights2) {
|
|
@@ -2932,14 +3013,15 @@ var MIN_BRAND_TOKEN_LENGTH = 3;
|
|
|
2932
3013
|
function compact(value) {
|
|
2933
3014
|
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2934
3015
|
}
|
|
2935
|
-
function buildBrandTokens(canonicalDomain,
|
|
3016
|
+
function buildBrandTokens(canonicalDomain, brandNames = []) {
|
|
2936
3017
|
const seen = /* @__PURE__ */ new Set();
|
|
2937
3018
|
const stem = canonicalDomain.toLowerCase().replace(/\.[a-z]{2,}$/, "");
|
|
2938
3019
|
const stemCompact = compact(stem);
|
|
2939
3020
|
if (stemCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(stemCompact);
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
3021
|
+
for (const name of brandNames) {
|
|
3022
|
+
if (!name) continue;
|
|
3023
|
+
const nameCompact = compact(name);
|
|
3024
|
+
if (nameCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(nameCompact);
|
|
2943
3025
|
}
|
|
2944
3026
|
return [...seen];
|
|
2945
3027
|
}
|
|
@@ -3086,7 +3168,7 @@ function extractHostFromUri(uri) {
|
|
|
3086
3168
|
}
|
|
3087
3169
|
|
|
3088
3170
|
// ../intelligence/src/mention-landscape.ts
|
|
3089
|
-
function buildMentionLandscape(snapshots, competitorDomains,
|
|
3171
|
+
function buildMentionLandscape(snapshots, competitorDomains, projectBrandNames, projectDomains, queryLookup) {
|
|
3090
3172
|
let projectMentionCount = 0;
|
|
3091
3173
|
let totalAnswerSnapshots = 0;
|
|
3092
3174
|
const competitorMap = /* @__PURE__ */ new Map();
|
|
@@ -3100,13 +3182,13 @@ function buildMentionLandscape(snapshots, competitorDomains, projectDisplayName,
|
|
|
3100
3182
|
const q = queryLookup.byId.get(snap.queryId);
|
|
3101
3183
|
const projectMentioned = snap.answerMentioned ?? determineAnswerMentioned(
|
|
3102
3184
|
text2,
|
|
3103
|
-
|
|
3185
|
+
[...projectBrandNames],
|
|
3104
3186
|
[...projectDomains]
|
|
3105
3187
|
);
|
|
3106
3188
|
if (projectMentioned) projectMentionCount++;
|
|
3107
3189
|
for (const competitor of competitorDomains) {
|
|
3108
3190
|
const brand = brandLabelFromDomain(competitor);
|
|
3109
|
-
const mentioned = determineAnswerMentioned(text2, brand, [competitor]);
|
|
3191
|
+
const mentioned = determineAnswerMentioned(text2, brand ? [brand] : [], [competitor]);
|
|
3110
3192
|
if (mentioned) {
|
|
3111
3193
|
const entry = competitorMap.get(competitor);
|
|
3112
3194
|
entry.count++;
|
|
@@ -3474,17 +3556,29 @@ var IntelligenceService = class {
|
|
|
3474
3556
|
log.info("intelligence.skip", { runId, reason: "run not in recent completed list" });
|
|
3475
3557
|
return null;
|
|
3476
3558
|
}
|
|
3477
|
-
const currentRun = this.buildRunData(
|
|
3559
|
+
const currentRun = this.buildRunData(
|
|
3560
|
+
runId,
|
|
3561
|
+
projectId,
|
|
3562
|
+
currentRunRecord.finishedAt ?? currentRunRecord.createdAt,
|
|
3563
|
+
currentRunRecord.location ?? null
|
|
3564
|
+
);
|
|
3478
3565
|
if (currentRun.snapshots.length === 0) {
|
|
3479
3566
|
log.info("intelligence.skip", { runId, reason: "no snapshots" });
|
|
3480
3567
|
return null;
|
|
3481
3568
|
}
|
|
3482
3569
|
const orderedRecent = [...recentRuns].reverse();
|
|
3483
|
-
const
|
|
3484
|
-
const
|
|
3485
|
-
const
|
|
3570
|
+
const currentLocation = currentRunRecord.location ?? null;
|
|
3571
|
+
const sameLocationOrdered = orderedRecent.filter((r) => (r.location ?? null) === currentLocation);
|
|
3572
|
+
const currentLocIdx = sameLocationOrdered.findIndex((r) => r.id === runId);
|
|
3573
|
+
const previousRunRecord = currentLocIdx > 0 ? sameLocationOrdered[currentLocIdx - 1] : null;
|
|
3574
|
+
const previousRun = previousRunRecord ? this.buildRunData(
|
|
3575
|
+
previousRunRecord.id,
|
|
3576
|
+
projectId,
|
|
3577
|
+
previousRunRecord.finishedAt ?? previousRunRecord.createdAt,
|
|
3578
|
+
previousRunRecord.location ?? null
|
|
3579
|
+
) : null;
|
|
3486
3580
|
const trackedCompetitors = this.loadTrackedCompetitors(projectId);
|
|
3487
|
-
const history =
|
|
3581
|
+
const history = sameLocationOrdered.slice(0, currentLocIdx + 1).map((r) => r.id === runId ? currentRun : this.buildRunData(r.id, projectId, r.finishedAt ?? r.createdAt, r.location ?? null));
|
|
3488
3582
|
if (!previousRun) {
|
|
3489
3583
|
const result2 = analyzeRuns(currentRun, currentRun, { trackedCompetitors, history });
|
|
3490
3584
|
log.info("intelligence.analyzed", {
|
|
@@ -3519,13 +3613,23 @@ var IntelligenceService = class {
|
|
|
3519
3613
|
* Used by backfill where we control the run ordering.
|
|
3520
3614
|
*/
|
|
3521
3615
|
analyzeRunWithPrevious(runRecord, previousRunRecord, historyRecords) {
|
|
3522
|
-
const currentRun = this.buildRunData(
|
|
3616
|
+
const currentRun = this.buildRunData(
|
|
3617
|
+
runRecord.id,
|
|
3618
|
+
runRecord.projectId,
|
|
3619
|
+
runRecord.finishedAt ?? runRecord.createdAt,
|
|
3620
|
+
runRecord.location ?? null
|
|
3621
|
+
);
|
|
3523
3622
|
if (currentRun.snapshots.length === 0) {
|
|
3524
3623
|
return null;
|
|
3525
3624
|
}
|
|
3526
|
-
const previousRun = previousRunRecord ? this.buildRunData(
|
|
3625
|
+
const previousRun = previousRunRecord ? this.buildRunData(
|
|
3626
|
+
previousRunRecord.id,
|
|
3627
|
+
previousRunRecord.projectId,
|
|
3628
|
+
previousRunRecord.finishedAt ?? previousRunRecord.createdAt,
|
|
3629
|
+
previousRunRecord.location ?? null
|
|
3630
|
+
) : null;
|
|
3527
3631
|
const trackedCompetitors = this.loadTrackedCompetitors(runRecord.projectId);
|
|
3528
|
-
const history = (historyRecords ?? []).map((r) => r.id === runRecord.id ? currentRun : this.buildRunData(r.id, r.projectId, r.finishedAt ?? r.createdAt));
|
|
3632
|
+
const history = (historyRecords ?? []).map((r) => r.id === runRecord.id ? currentRun : this.buildRunData(r.id, r.projectId, r.finishedAt ?? r.createdAt, r.location ?? null));
|
|
3529
3633
|
if (!previousRun) {
|
|
3530
3634
|
const result2 = analyzeRuns(currentRun, currentRun, { trackedCompetitors, history });
|
|
3531
3635
|
this.persistResult(this.emptyAnalysisResult(result2), runRecord.id, runRecord.projectId);
|
|
@@ -3569,10 +3673,12 @@ var IntelligenceService = class {
|
|
|
3569
3673
|
let totalInsights = 0;
|
|
3570
3674
|
for (let i = 0; i < targetRuns.length; i++) {
|
|
3571
3675
|
const run = targetRuns[i];
|
|
3572
|
-
const
|
|
3573
|
-
const
|
|
3574
|
-
const
|
|
3575
|
-
const
|
|
3676
|
+
const runLocation = run.location ?? null;
|
|
3677
|
+
const sameLocationRuns = allRuns.filter((r) => (r.location ?? null) === runLocation);
|
|
3678
|
+
const sameLocIdx = sameLocationRuns.indexOf(run);
|
|
3679
|
+
const previousRun = sameLocIdx > 0 ? sameLocationRuns[sameLocIdx - 1] : null;
|
|
3680
|
+
const historyStart = Math.max(0, sameLocIdx - (HISTORY_WINDOW_RUNS - 1));
|
|
3681
|
+
const historyRecords = sameLocationRuns.slice(historyStart, sameLocIdx + 1);
|
|
3576
3682
|
const result = this.analyzeRunWithPrevious(run, previousRun, historyRecords);
|
|
3577
3683
|
if (result) {
|
|
3578
3684
|
processed++;
|
|
@@ -3729,13 +3835,14 @@ var IntelligenceService = class {
|
|
|
3729
3835
|
return { ...insight, severity };
|
|
3730
3836
|
});
|
|
3731
3837
|
}
|
|
3732
|
-
buildRunData(runId, projectId, completedAt) {
|
|
3838
|
+
buildRunData(runId, projectId, completedAt, location = null) {
|
|
3733
3839
|
const rows = this.db.select({
|
|
3734
3840
|
query: queries.query,
|
|
3735
3841
|
provider: querySnapshots.provider,
|
|
3736
3842
|
citationState: querySnapshots.citationState,
|
|
3737
3843
|
citedDomains: querySnapshots.citedDomains,
|
|
3738
|
-
competitorOverlap: querySnapshots.competitorOverlap
|
|
3844
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
3845
|
+
snapshotLocation: querySnapshots.location
|
|
3739
3846
|
}).from(querySnapshots).leftJoin(queries, eq(querySnapshots.queryId, queries.id)).where(eq(querySnapshots.runId, runId)).all();
|
|
3740
3847
|
const snapshots = rows.map((r) => {
|
|
3741
3848
|
const domains = parseJsonColumn(r.citedDomains, []);
|
|
@@ -3745,10 +3852,20 @@ var IntelligenceService = class {
|
|
|
3745
3852
|
provider: r.provider,
|
|
3746
3853
|
cited: r.citationState === CitationStates.cited,
|
|
3747
3854
|
citationUrl: domains[0] ?? void 0,
|
|
3748
|
-
|
|
3855
|
+
// Snapshots carry their own location for downstream detectors. In
|
|
3856
|
+
// practice every snapshot in a single runId shares the run's
|
|
3857
|
+
// location; the per-row column is the same value duplicated, but
|
|
3858
|
+
// we read it from the snapshot row so a stale runs.location can't
|
|
3859
|
+
// mask snapshot truth.
|
|
3860
|
+
location: r.snapshotLocation ?? location ?? null,
|
|
3861
|
+
competitorDomains: competitors2,
|
|
3862
|
+
// citedDomains is the FULL set (tracked competitors + third-party
|
|
3863
|
+
// sources). Cause analysis uses it to name the displacing source
|
|
3864
|
+
// when no tracked competitor appears in the response.
|
|
3865
|
+
citedDomains: domains
|
|
3749
3866
|
};
|
|
3750
3867
|
});
|
|
3751
|
-
return { runId, projectId, completedAt, snapshots };
|
|
3868
|
+
return { runId, projectId, completedAt, location, snapshots };
|
|
3752
3869
|
}
|
|
3753
3870
|
};
|
|
3754
3871
|
|