@canonry/canonry 4.161.0 → 4.163.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/assets/{AuditHistoryPanel-C_1twF00.js → AuditHistoryPanel-FDQx9TS5.js} +1 -1
- package/assets/assets/{BacklinksPage-CfAuMCaN.js → BacklinksPage-DVvsQsdk.js} +1 -1
- package/assets/assets/ChartPrimitives-BoYe2M-i.js +1 -0
- package/assets/assets/{HistoryPage-DrRxOL3j.js → HistoryPage-31HR6TIJ.js} +1 -1
- package/assets/assets/{MeasurementPropertyPage-BFtxx5Jw.js → MeasurementPropertyPage-Dps7MF7r.js} +1 -1
- package/assets/assets/ProjectPage-BijTUrpk.js +9 -0
- package/assets/assets/{RunRow-DVZXibR2.js → RunRow-9DiFrBlc.js} +1 -1
- package/assets/assets/{RunsPage-CAUMy7By.js → RunsPage-DQlAc4J9.js} +1 -1
- package/assets/assets/{SettingsPage-CcSOmQAl.js → SettingsPage-c1nxJsbL.js} +1 -1
- package/assets/assets/SiteHealthSection-0B0oUUQx.js +4 -0
- package/assets/assets/{TrafficPage-BWM33jnJ.js → TrafficPage-HzwQyLzl.js} +1 -1
- package/assets/assets/{TrafficSourceDetailPage-DvEZ_JGY.js → TrafficSourceDetailPage-BTVAQjRd.js} +1 -1
- package/assets/assets/{extract-error-message-CnKOJkYB.js → extract-error-message-B5bhJRmf.js} +1 -1
- package/assets/assets/{index-CACRaOmZ.css → index-CHluSPlN.css} +1 -1
- package/assets/assets/{index-CQGAqmDx.js → index-CQsvCUM0.js} +20 -20
- package/assets/assets/{react-sigma_core.esm.min-CHCAIMBW.js → react-sigma_core.esm.min-6BxcniCA.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-EXDO7EPL.js → chunk-ESH3LPYQ.js} +3456 -3231
- package/dist/{chunk-J27J2DF6.js → chunk-G5UZTODF.js} +143 -34
- package/dist/{chunk-HCGTKQCG.js → chunk-INXI6PO7.js} +3 -3
- package/dist/{chunk-W6OWEPZG.js → chunk-MRHYSEZB.js} +80 -30
- package/dist/cli.js +29 -8
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-HKDIVTZB.js → intelligence-service-AFAFGLCM.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +11 -11
- package/assets/assets/ChartPrimitives-DXVLCdBi.js +0 -1
- package/assets/assets/ProjectPage-Djvhx4HD.js +0 -12
- package/assets/assets/SiteHealthSection-DYjEWd1I.js +0 -4
|
@@ -183,6 +183,7 @@ import {
|
|
|
183
183
|
buildHarvestAnchorTerms,
|
|
184
184
|
buildMeasurementExecutionIdentity,
|
|
185
185
|
buildMeasurementRunManifestV1,
|
|
186
|
+
calendarDateRange,
|
|
186
187
|
calendarMonthBounds,
|
|
187
188
|
canonicalMeasurementExecutionIdentityJson,
|
|
188
189
|
canonicalMeasurementPlanJson,
|
|
@@ -324,6 +325,7 @@ import {
|
|
|
324
325
|
keywordDtoSchema,
|
|
325
326
|
keywordGenerateRequestSchema,
|
|
326
327
|
latestProjectRunDtoSchema,
|
|
328
|
+
linearTrend,
|
|
327
329
|
locationContextSchema,
|
|
328
330
|
loginRequestSchema,
|
|
329
331
|
mapWithConcurrency,
|
|
@@ -537,8 +539,9 @@ import {
|
|
|
537
539
|
surfaceClassFromCompetitorType,
|
|
538
540
|
surfaceClassLabel,
|
|
539
541
|
telemetryEventAcceptedDtoSchema,
|
|
540
|
-
templateLinkDetection,
|
|
541
542
|
templateLinkRatio,
|
|
543
|
+
templateLinkSource,
|
|
544
|
+
templateLinkUbiquityAvailable,
|
|
542
545
|
textContainsAnyBrandAlias,
|
|
543
546
|
textContainsBrandAlias,
|
|
544
547
|
textContainsDomain,
|
|
@@ -578,7 +581,7 @@ import {
|
|
|
578
581
|
wordpressSchemaDeployResultDtoSchema,
|
|
579
582
|
wordpressSchemaStatusResultDtoSchema,
|
|
580
583
|
wordpressStatusDtoSchema
|
|
581
|
-
} from "./chunk-
|
|
584
|
+
} from "./chunk-ESH3LPYQ.js";
|
|
582
585
|
|
|
583
586
|
// src/intelligence-service.ts
|
|
584
587
|
import { eq as eq59, desc as desc26, asc as asc11, and as and47, ne as ne7, or as or13, inArray as inArray21, gte as gte14, lte as lte12 } from "drizzle-orm";
|
|
@@ -1342,6 +1345,14 @@ var siteCrawlSnapshots = sqliteTable("site_crawl_snapshots", {
|
|
|
1342
1345
|
* pass for "this site has no nav".
|
|
1343
1346
|
*/
|
|
1344
1347
|
templateDetection: text("template_detection"),
|
|
1348
|
+
/**
|
|
1349
|
+
* The crawler's landmark ruleset version, from the crawl summary's
|
|
1350
|
+
* `linkPlacementRulesetVersion`. NULL means this scan recorded no placement
|
|
1351
|
+
* at all, which is the one thing that decides whether its links could be
|
|
1352
|
+
* classified by where they sit or only by how often they repeat. It cannot be
|
|
1353
|
+
* backfilled: a pre-4.7.0 crawl never observed placement.
|
|
1354
|
+
*/
|
|
1355
|
+
linkPlacementRulesetVersion: text("link_placement_ruleset_version"),
|
|
1345
1356
|
createdAt: text("created_at").notNull(),
|
|
1346
1357
|
updatedAt: text("updated_at").notNull()
|
|
1347
1358
|
}, (table) => [
|
|
@@ -1435,28 +1446,46 @@ var siteCrawlEdges = sqliteTable("site_crawl_edges", {
|
|
|
1435
1446
|
nofollowOccurrences: integer("nofollow_occurrences").notNull().default(0),
|
|
1436
1447
|
anchors: text("anchors", { mode: "json" }).$type().notNull().default([]),
|
|
1437
1448
|
/**
|
|
1438
|
-
* True
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
* publish time by the contract's `classifyTemplateLinks`, so the map, the
|
|
1442
|
-
* API filters, and the agents all read the same decision.
|
|
1449
|
+
* True for a nav, header, or footer link rather than an editorial one.
|
|
1450
|
+
* Computed once at publish time by the contract's `classifyTemplateLinkEdge`,
|
|
1451
|
+
* so the map, the API filters, and the agents all read the same decision.
|
|
1443
1452
|
*
|
|
1444
|
-
*
|
|
1445
|
-
*
|
|
1446
|
-
*
|
|
1453
|
+
* The rule is DOM placement where the crawl recorded it: any occurrence
|
|
1454
|
+
* inside a main or article landmark makes the whole link editorial, and
|
|
1455
|
+
* navigation with no content occurrence makes it chrome. Where the page
|
|
1456
|
+
* declares no landmark that answers the question, `template_ratio` below is
|
|
1457
|
+
* the fallback.
|
|
1447
1458
|
*
|
|
1448
|
-
*
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1451
|
-
*
|
|
1459
|
+
* Classification writes a STRICT BOOLEAN and never a NULL. A link no rule
|
|
1460
|
+
* could measure is a real `false` whose `templateSource` reads `unmeasured`,
|
|
1461
|
+
* because "not shown to be chrome" is what a content link means here. A third
|
|
1462
|
+
* stored state would be invisible to every reader that treats this as a
|
|
1463
|
+
* boolean (the layout input, the graph sample, the totals, the map legend,
|
|
1464
|
+
* the inspector tiles) while only the two SQL link filters honoured it, and
|
|
1465
|
+
* one definition of a content link is the whole point of the column.
|
|
1466
|
+
*
|
|
1467
|
+
* NULL therefore means exactly one thing: this row predates classification
|
|
1468
|
+
* entirely, which reads report as `unavailable-legacy-scan`. It never means
|
|
1469
|
+
* "not a template link".
|
|
1452
1470
|
*/
|
|
1453
1471
|
isTemplate: integer("is_template", { mode: "boolean" }),
|
|
1454
1472
|
/**
|
|
1455
|
-
* Share of fetched pages carrying this link's LEAST ubiquitous anchor
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1473
|
+
* Share of fetched pages carrying this link's LEAST ubiquitous anchor. NULL
|
|
1474
|
+
* when the link has no measurable pair (an unresolved target or no anchor)
|
|
1475
|
+
* OR when DOM placement decided it, because it is the fallback rule's own
|
|
1476
|
+
* evidence and had no vote.
|
|
1458
1477
|
*/
|
|
1459
1478
|
templateRatio: real("template_ratio"),
|
|
1479
|
+
/**
|
|
1480
|
+
* Occurrences of this link split by where they sat in the source page, from
|
|
1481
|
+
* the crawler's `placementOccurrences`. NULL means this scan recorded no
|
|
1482
|
+
* placement for this link, which is a real state (a pre-4.7.0 crawl) and
|
|
1483
|
+
* never zeros: `{0, 0, 0}` is what a redirect or canonical edge legitimately
|
|
1484
|
+
* carries, since a non-anchor edge has no position in a page.
|
|
1485
|
+
*/
|
|
1486
|
+
placementNavigationOccurrences: integer("placement_navigation_occurrences"),
|
|
1487
|
+
placementContentOccurrences: integer("placement_content_occurrences"),
|
|
1488
|
+
placementUnknownOccurrences: integer("placement_unknown_occurrences"),
|
|
1460
1489
|
createdAt: text("created_at").notNull(),
|
|
1461
1490
|
updatedAt: text("updated_at").notNull()
|
|
1462
1491
|
}, (table) => [
|
|
@@ -2999,7 +3028,7 @@ function* streamSiteCrawlTemplateLinkEdges(tx, attemptId, batchSize = 2e3) {
|
|
|
2999
3028
|
let after = "";
|
|
3000
3029
|
for (; ; ) {
|
|
3001
3030
|
const rows = tx.all(sql2`
|
|
3002
|
-
SELECT edge_key, source_node_key, target_node_key, anchors
|
|
3031
|
+
SELECT edge_key, source_node_key, target_node_key, anchors, relation
|
|
3003
3032
|
FROM site_crawl_edges
|
|
3004
3033
|
WHERE attempt_id = ${attemptId} AND edge_key > ${after}
|
|
3005
3034
|
ORDER BY edge_key
|
|
@@ -3010,7 +3039,8 @@ function* streamSiteCrawlTemplateLinkEdges(tx, attemptId, batchSize = 2e3) {
|
|
|
3010
3039
|
edgeKey: row.edge_key,
|
|
3011
3040
|
sourceNodeKey: row.source_node_key,
|
|
3012
3041
|
targetNodeKey: row.target_node_key,
|
|
3013
|
-
anchors: parseJsonColumn(row.anchors, [])
|
|
3042
|
+
anchors: parseJsonColumn(row.anchors, []),
|
|
3043
|
+
relation: row.relation
|
|
3014
3044
|
}));
|
|
3015
3045
|
after = rows[rows.length - 1].edge_key;
|
|
3016
3046
|
if (rows.length < batchSize) return;
|
|
@@ -3033,7 +3063,8 @@ function backfillSiteCrawlTemplateLinks(tx) {
|
|
|
3033
3063
|
ON s.project_id = a.project_id AND s.run_id = a.run_id AND s.attempt_id = a.id
|
|
3034
3064
|
`));
|
|
3035
3065
|
for (const attempt of attempts) {
|
|
3036
|
-
const
|
|
3066
|
+
const ubiquityAvailable = templateLinkUbiquityAvailable(attempt.pages_fetched);
|
|
3067
|
+
const detection = ubiquityAvailable ? SiteHealthTemplateDetections.applied : SiteHealthTemplateDetections["unavailable-too-few-pages"];
|
|
3037
3068
|
const scope = { attemptId: attempt.attempt_id, projectId: attempt.project_id, runId: attempt.run_id };
|
|
3038
3069
|
tx.run(sql2`
|
|
3039
3070
|
UPDATE site_crawl_edges SET is_template = 0, template_ratio = NULL
|
|
@@ -6204,6 +6235,35 @@ var MIGRATION_VERSIONS = [
|
|
|
6204
6235
|
updated_at TEXT NOT NULL
|
|
6205
6236
|
)`
|
|
6206
6237
|
]
|
|
6238
|
+
},
|
|
6239
|
+
{
|
|
6240
|
+
version: 138,
|
|
6241
|
+
name: "site-crawl-link-placement",
|
|
6242
|
+
// The crawler (aeo-audit 4.7.0) now reports where each link occurrence sat
|
|
6243
|
+
// in its page, from the page's own landmarks. That is ground truth about a
|
|
6244
|
+
// link, and it replaces ubiquity as the rule that separates nav from
|
|
6245
|
+
// content: ubiquity keys on (target URL, anchor text), so it cannot see an
|
|
6246
|
+
// editorial link whose anchor text matches the nav's, which is the common
|
|
6247
|
+
// case because good anchor text reuses the destination's name. Measured on
|
|
6248
|
+
// canonry.ai: 53 newly added editorial links moved the content-link count
|
|
6249
|
+
// by ZERO.
|
|
6250
|
+
//
|
|
6251
|
+
// These columns are deliberately nullable and deliberately NOT backfilled.
|
|
6252
|
+
// A crawl captured before the landmark ruleset existed never observed
|
|
6253
|
+
// placement, so there is nothing to derive from and any value written here
|
|
6254
|
+
// would be invented. Those scans keep the ubiquity classification they
|
|
6255
|
+
// already have, and their snapshots keep a NULL ruleset version, which is
|
|
6256
|
+
// exactly what makes a read say `applied` rather than `applied-placement`.
|
|
6257
|
+
// Re-running a scan is what upgrades it.
|
|
6258
|
+
//
|
|
6259
|
+
// Idempotent: `ALTER TABLE ADD COLUMN` errors with "duplicate column name",
|
|
6260
|
+
// which the runner already swallows, and there is no data pass to repeat.
|
|
6261
|
+
statements: [
|
|
6262
|
+
`ALTER TABLE site_crawl_edges ADD COLUMN placement_navigation_occurrences INTEGER`,
|
|
6263
|
+
`ALTER TABLE site_crawl_edges ADD COLUMN placement_content_occurrences INTEGER`,
|
|
6264
|
+
`ALTER TABLE site_crawl_edges ADD COLUMN placement_unknown_occurrences INTEGER`,
|
|
6265
|
+
`ALTER TABLE site_crawl_snapshots ADD COLUMN link_placement_ruleset_version TEXT`
|
|
6266
|
+
]
|
|
6207
6267
|
}
|
|
6208
6268
|
];
|
|
6209
6269
|
function addRunsMeasurementPlanVersionForeignKey(tx) {
|
|
@@ -28443,7 +28503,7 @@ var crawlLimitParameter = {
|
|
|
28443
28503
|
var linkKindParameter = {
|
|
28444
28504
|
name: "linkKind",
|
|
28445
28505
|
in: "query",
|
|
28446
|
-
description: 'Restrict links to `content` (excludes nav, header, and footer links) or `template` (only those). Defaults to `all`. A
|
|
28506
|
+
description: 'Restrict links to `content` (excludes nav, header, and footer links) or `template` (only those). Defaults to `all`. A link neither rule could classify matches neither filter, which is why every link response also carries `templateDetection` and every edge carries `templateSource`: an empty `content` list means "could not tell" under an `unavailable-*` detection, and `templateSource` says whether a link that IS classified was decided by where it sits in the page (`placement`) or by how many pages repeat it (`ubiquity`).',
|
|
28447
28507
|
schema: { type: "string", enum: ["all", "content", "template"], default: "all" }
|
|
28448
28508
|
};
|
|
28449
28509
|
var measurementOverviewSortParameter = {
|
|
@@ -34008,7 +34068,7 @@ var routeCatalog = [
|
|
|
34008
34068
|
method: "get",
|
|
34009
34069
|
path: "/api/v1/projects/{name}/technical-aeo/graph",
|
|
34010
34070
|
summary: "Get a persisted Site Health graph projection",
|
|
34011
|
-
description: "Returns the deterministic graph projection computed once when the latest complete or selected historical crawl was published. ForceAtlas2 positions and the exact internal-anchor edge sample are persisted, so reads run no layout physics and never rescan the crawl edge table. Nav, header, and footer links are excluded from the layout physics but retained in the sample and tagged `isTemplate`, so a viewer can draw them without a refetch and without any node moving. The response is bounded to 20,000 nodes / 50,000 edges; `layout`, `omittedNodes`, `omittedEdges`, and `sampled` disclose legacy/unavailable layouts and intentional truncation, and `templateDetection` says whether template links could be told apart at all.",
|
|
34071
|
+
description: "Returns the deterministic graph projection computed once when the latest complete or selected historical crawl was published. ForceAtlas2 positions and the exact internal-anchor edge sample are persisted, so reads run no layout physics and never rescan the crawl edge table. Nav, header, and footer links are excluded from the layout physics but retained in the sample and tagged `isTemplate`, so a viewer can draw them without a refetch and without any node moving. The response is bounded to 20,000 nodes / 50,000 edges; `layout`, `omittedNodes`, `omittedEdges`, and `sampled` disclose legacy/unavailable layouts and intentional truncation, and `templateDetection` says whether template links could be told apart at all and by which rule.",
|
|
34012
34072
|
tags: ["technical-aeo"],
|
|
34013
34073
|
parameters: [
|
|
34014
34074
|
nameParameter,
|
|
@@ -34148,7 +34208,7 @@ var routeCatalog = [
|
|
|
34148
34208
|
method: "get",
|
|
34149
34209
|
path: "/api/v1/projects/{name}/technical-aeo/internal-links",
|
|
34150
34210
|
summary: "List persisted internal crawl links",
|
|
34151
|
-
description: "Cursor-paged internal edges for the latest or selected crawl. Optional source/target/followability/link-kind filters remain project-, run-, and attempt-scoped. `total` counts exactly what the requested filters match,
|
|
34211
|
+
description: "Cursor-paged internal edges for the latest or selected crawl. Optional source/target/followability/link-kind filters remain project-, run-, and attempt-scoped. `total` counts exactly what the requested filters match, `templateDetection` says whether nav and footer links could be told apart for this scan and by which rule, and the `templateSource` plus `placementOccurrences` on each edge show the evidence behind its own classification.",
|
|
34152
34212
|
tags: ["technical-aeo"],
|
|
34153
34213
|
parameters: [
|
|
34154
34214
|
nameParameter,
|
|
@@ -37627,6 +37687,7 @@ async function googleRoutes(app, opts) {
|
|
|
37627
37687
|
clicks: sql13`COALESCE(SUM(${gscSearchData.clicks}), 0)`,
|
|
37628
37688
|
impressions: sql13`COALESCE(SUM(${gscSearchData.impressions}), 0)`
|
|
37629
37689
|
}).from(gscSearchData).where(and30(...conditions)).groupBy(gscSearchData.date).orderBy(gscSearchData.date).all();
|
|
37690
|
+
const propertyDates = new Set(dailyTotals.map((d) => d.date));
|
|
37630
37691
|
const daily = mergeGscDailyTotalsWithFallback(
|
|
37631
37692
|
dailyTotals,
|
|
37632
37693
|
dimensionedRows.map((r) => ({
|
|
@@ -37639,15 +37700,36 @@ async function googleRoutes(app, opts) {
|
|
|
37639
37700
|
date: d.date,
|
|
37640
37701
|
clicks: d.clicks,
|
|
37641
37702
|
impressions: d.impressions,
|
|
37642
|
-
ctr: d.impressions > 0 ? d.clicks / d.impressions : 0
|
|
37703
|
+
ctr: d.impressions > 0 ? d.clicks / d.impressions : 0,
|
|
37704
|
+
position: propertyDates.has(d.date) ? d.position : null
|
|
37643
37705
|
}));
|
|
37644
37706
|
const totalClicks = daily.reduce((sum, d) => sum + d.clicks, 0);
|
|
37645
37707
|
const totalImpressions = daily.reduce((sum, d) => sum + d.impressions, 0);
|
|
37708
|
+
let positionWeight = 0;
|
|
37709
|
+
let positionWeighted = 0;
|
|
37710
|
+
for (const d of daily) {
|
|
37711
|
+
if (d.position === null || d.impressions <= 0) continue;
|
|
37712
|
+
positionWeight += d.impressions;
|
|
37713
|
+
positionWeighted += d.position * d.impressions;
|
|
37714
|
+
}
|
|
37715
|
+
const byDate = new Map(daily.map((d) => [d.date, d]));
|
|
37716
|
+
const denseDates = daily.length === 0 ? [] : calendarDateRange(daily[0].date, daily[daily.length - 1].date);
|
|
37717
|
+
const densify = (pick) => denseDates.map((date) => {
|
|
37718
|
+
const row = byDate.get(date);
|
|
37719
|
+
return row ? pick(row) : null;
|
|
37720
|
+
});
|
|
37646
37721
|
return {
|
|
37647
37722
|
totals: {
|
|
37648
37723
|
clicks: totalClicks,
|
|
37649
37724
|
impressions: totalImpressions,
|
|
37650
37725
|
ctr: totalImpressions > 0 ? totalClicks / totalImpressions : 0,
|
|
37726
|
+
position: positionWeight > 0 ? positionWeighted / positionWeight : null,
|
|
37727
|
+
/**
|
|
37728
|
+
* How many days actually carried a property-level position. Below
|
|
37729
|
+
* `days`, the position figure describes a SUBSET of the window, and a
|
|
37730
|
+
* surface must say so rather than present it as the window's average.
|
|
37731
|
+
*/
|
|
37732
|
+
positionDays: daily.filter((d) => d.position !== null).length,
|
|
37651
37733
|
days: daily.length
|
|
37652
37734
|
},
|
|
37653
37735
|
daily,
|
|
@@ -37661,7 +37743,16 @@ async function googleRoutes(app, opts) {
|
|
|
37661
37743
|
// contain the rows beside it. When only one side is given, the other is
|
|
37662
37744
|
// dropped rather than reported reversed: an absent bound is honest about
|
|
37663
37745
|
// being unspecified, a reversed pair is not.
|
|
37664
|
-
window: resolveReportedWindow(resolvedWindow, startDate, endDate)
|
|
37746
|
+
window: resolveReportedWindow(resolvedWindow, startDate, endDate),
|
|
37747
|
+
// Fitted server-side so the dashboard, the CLI, and the report all draw
|
|
37748
|
+
// the SAME line (UI/CLI parity — a chart-only regression is invisible to
|
|
37749
|
+
// an agent), and fitted over the CALENDAR, not over the rows.
|
|
37750
|
+
trends: {
|
|
37751
|
+
clicks: linearTrend(densify((d) => d.clicks)),
|
|
37752
|
+
impressions: linearTrend(densify((d) => d.impressions)),
|
|
37753
|
+
ctr: linearTrend(densify((d) => d.ctr)),
|
|
37754
|
+
position: linearTrend(densify((d) => d.position))
|
|
37755
|
+
}
|
|
37665
37756
|
};
|
|
37666
37757
|
});
|
|
37667
37758
|
app.get("/projects/:name/google/gsc/top-pages", async (request) => {
|
|
@@ -59091,7 +59182,15 @@ function countLivePageHealthChecks(auditFields) {
|
|
|
59091
59182
|
}
|
|
59092
59183
|
return checks;
|
|
59093
59184
|
}
|
|
59094
|
-
function
|
|
59185
|
+
function mapPlacementOccurrences(row) {
|
|
59186
|
+
const navigation = row.placementNavigationOccurrences;
|
|
59187
|
+
const content = row.placementContentOccurrences;
|
|
59188
|
+
const unknown = row.placementUnknownOccurrences;
|
|
59189
|
+
if (navigation == null || content == null || unknown == null) return null;
|
|
59190
|
+
return { navigation, content, unknown };
|
|
59191
|
+
}
|
|
59192
|
+
function mapCrawlEdge(row, detection) {
|
|
59193
|
+
const placementOccurrences = mapPlacementOccurrences(row);
|
|
59095
59194
|
return {
|
|
59096
59195
|
edgeKey: row.edgeKey,
|
|
59097
59196
|
sourceNodeKey: row.sourceNodeKey,
|
|
@@ -59106,7 +59205,13 @@ function mapCrawlEdge(row) {
|
|
|
59106
59205
|
nofollowOccurrences: row.nofollowOccurrences,
|
|
59107
59206
|
anchors: row.anchors,
|
|
59108
59207
|
isTemplate: row.isTemplate,
|
|
59109
|
-
templateRatio: row.templateRatio
|
|
59208
|
+
templateRatio: row.templateRatio,
|
|
59209
|
+
templateSource: templateLinkSource(detection, {
|
|
59210
|
+
isTemplate: row.isTemplate,
|
|
59211
|
+
templateRatio: row.templateRatio,
|
|
59212
|
+
placementOccurrences
|
|
59213
|
+
}),
|
|
59214
|
+
placementOccurrences
|
|
59110
59215
|
};
|
|
59111
59216
|
}
|
|
59112
59217
|
function templateDetectionOf(value) {
|
|
@@ -59739,7 +59844,8 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
59739
59844
|
distance: distances.get(row.nodeKey) ?? 0,
|
|
59740
59845
|
relationToFocus: row.nodeKey === focus.nodeKey ? "focus" : directRelations.get(row.nodeKey) ?? "transitive"
|
|
59741
59846
|
})).sort((a, b) => a.distance - b.distance || a.nodeKey.localeCompare(b.nodeKey));
|
|
59742
|
-
const
|
|
59847
|
+
const subgraphDetection = templateDetectionOf(snapshot.templateDetection);
|
|
59848
|
+
const edges = [...edgeRows.values()].filter((edge) => edge.targetNodeKey && persistedNodeKeys.has(edge.sourceNodeKey) && persistedNodeKeys.has(edge.targetNodeKey)).sort((a, b) => a.edgeKey.localeCompare(b.edgeKey)).map((row) => mapCrawlEdge(row, subgraphDetection));
|
|
59743
59849
|
const omittedNodes = omittedNodeKeys.size + Math.max(0, distances.size - pageRows.length);
|
|
59744
59850
|
const omittedEdges = omittedEdgeKeys.size + (queryTruncated ? 1 : 0) + Math.max(0, edgeRows.size - edges.length);
|
|
59745
59851
|
const truncated = omittedNodes > 0 || omittedEdges > 0;
|
|
@@ -59888,6 +59994,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
59888
59994
|
inArray20(siteCrawlPages.nodeKey, pathKeys)
|
|
59889
59995
|
)).all();
|
|
59890
59996
|
const pageByKey = new Map(pathRows.map((row) => [row.nodeKey, row]));
|
|
59997
|
+
const pathDetection = templateDetectionOf(snapshot.templateDetection);
|
|
59891
59998
|
return {
|
|
59892
59999
|
project: project.name,
|
|
59893
60000
|
runId: snapshot.runId,
|
|
@@ -59899,7 +60006,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
59899
60006
|
maxDepth,
|
|
59900
60007
|
visitedNodes: visited.size,
|
|
59901
60008
|
nodes: pathKeys.map((key) => mapCrawlPage(pageByKey.get(key))),
|
|
59902
|
-
edges: pathEdges.map(mapCrawlEdge)
|
|
60009
|
+
edges: pathEdges.map((row) => mapCrawlEdge(row, pathDetection))
|
|
59903
60010
|
};
|
|
59904
60011
|
});
|
|
59905
60012
|
app.get("/projects/:name/technical-aeo/changes", async (request) => {
|
|
@@ -60177,8 +60284,10 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
60177
60284
|
)).all();
|
|
60178
60285
|
const beforePages = new Map(pageRowsFor(beforeScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
|
|
60179
60286
|
const afterPages = new Map(pageRowsFor(afterScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
|
|
60180
|
-
const
|
|
60181
|
-
const
|
|
60287
|
+
const beforeDetection = templateDetectionOf(beforeSnapshot.templateDetection);
|
|
60288
|
+
const afterDetection = templateDetectionOf(afterSnapshot.templateDetection);
|
|
60289
|
+
const beforeLinks = new Map(edgeRowsFor(beforeScope).map((row) => [row.edgeKey, mapCrawlEdge(row, beforeDetection)]));
|
|
60290
|
+
const afterLinks = new Map(edgeRowsFor(afterScope).map((row) => [row.edgeKey, mapCrawlEdge(row, afterDetection)]));
|
|
60182
60291
|
const changes = visibleKeys.map((row) => {
|
|
60183
60292
|
if (row.entity === "page") {
|
|
60184
60293
|
const before2 = beforePages.get(row.key) ?? null;
|
|
@@ -60469,7 +60578,7 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
60469
60578
|
nextCursor: nextOffset < total ? encodeCursor2(nextOffset) : null,
|
|
60470
60579
|
templateDetection,
|
|
60471
60580
|
linkKind,
|
|
60472
|
-
edges: rows.map(mapCrawlEdge)
|
|
60581
|
+
edges: rows.map((row) => mapCrawlEdge(row, templateDetection))
|
|
60473
60582
|
};
|
|
60474
60583
|
});
|
|
60475
60584
|
app.get("/projects/:name/technical-aeo/internal-links/neighbors", async (request) => {
|
|
@@ -60532,8 +60641,8 @@ async function technicalAeoRoutes(app, opts) {
|
|
|
60532
60641
|
url: request.query.url ?? null,
|
|
60533
60642
|
templateDetection,
|
|
60534
60643
|
linkKind,
|
|
60535
|
-
inbound: inboundRows.slice(0, limit).map(mapCrawlEdge),
|
|
60536
|
-
outbound: outboundRows.slice(0, limit).map(mapCrawlEdge),
|
|
60644
|
+
inbound: inboundRows.slice(0, limit).map((row) => mapCrawlEdge(row, templateDetection)),
|
|
60645
|
+
outbound: outboundRows.slice(0, limit).map((row) => mapCrawlEdge(row, templateDetection)),
|
|
60537
60646
|
inboundTruncated: inboundRows.length > limit,
|
|
60538
60647
|
outboundTruncated: outboundRows.length > limit
|
|
60539
60648
|
};
|
|
@@ -81,7 +81,7 @@ import {
|
|
|
81
81
|
trafficConnectWordpressRequestSchema,
|
|
82
82
|
trafficEventKindSchema,
|
|
83
83
|
trafficSeriesGranularitySchema
|
|
84
|
-
} from "./chunk-
|
|
84
|
+
} from "./chunk-ESH3LPYQ.js";
|
|
85
85
|
|
|
86
86
|
// src/config.ts
|
|
87
87
|
import fs from "fs";
|
|
@@ -8637,7 +8637,7 @@ var technicalAeoStructureInputSchema = z3.object({
|
|
|
8637
8637
|
limit: z3.number().int().positive().max(100).optional()
|
|
8638
8638
|
});
|
|
8639
8639
|
var linkKindSchema = z3.enum(["all", "content", "template"]).optional().describe(
|
|
8640
|
-
"Restrict to content links (excludes nav, header, and footer links) or to template links only. Defaults to all. Check templateDetection before reading an empty content list as a real zero."
|
|
8640
|
+
"Restrict to content links (excludes nav, header, and footer links) or to template links only. Defaults to all. Check templateDetection before reading an empty content list as a real zero, and before comparing a count with an older scan: it says whether the split came from where each link sits in the page or from how many pages repeat it."
|
|
8641
8641
|
);
|
|
8642
8642
|
var technicalAeoInternalLinksInputSchema = z3.object({
|
|
8643
8643
|
project: projectNameSchema,
|
|
@@ -10651,7 +10651,7 @@ var canonryMcpTools = [
|
|
|
10651
10651
|
defineTool({
|
|
10652
10652
|
name: "canonry_technical_aeo_internal_links",
|
|
10653
10653
|
title: "List Technical AEO internal links",
|
|
10654
|
-
description: "Read a bounded, cursor-paged list of persisted internal crawl edges. Filter by source URL, target URL, followability, or link kind. Nav, header, and footer links are marked isTemplate; templateDetection reports
|
|
10654
|
+
description: "Read a bounded, cursor-paged list of persisted internal crawl edges. Filter by source URL, target URL, followability, or link kind. Nav, header, and footer links are marked isTemplate; templateSource says which rule decided each one (placement, ubiquity, or unmeasured when neither had evidence) and templateDetection reports the same for the scan, so an empty content-only list is not evidence of no content links and two scans classified by different rules are never compared as if they were the same measurement. Use the neighbors tool for one page rather than loading a graph.",
|
|
10655
10655
|
access: "read",
|
|
10656
10656
|
tier: "monitoring",
|
|
10657
10657
|
inputSchema: technicalAeoInternalLinksInputSchema,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
loadConfig,
|
|
11
11
|
loadConfigRaw,
|
|
12
12
|
saveConfigPatch
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-INXI6PO7.js";
|
|
14
14
|
import {
|
|
15
15
|
CC_CACHE_DIR,
|
|
16
16
|
DUCKDB_SPEC,
|
|
@@ -139,7 +139,7 @@ import {
|
|
|
139
139
|
siteCrawlSnapshots,
|
|
140
140
|
toAlertView,
|
|
141
141
|
usageCounters
|
|
142
|
-
} from "./chunk-
|
|
142
|
+
} from "./chunk-G5UZTODF.js";
|
|
143
143
|
import {
|
|
144
144
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
145
145
|
AGENT_PROVIDER_IDS,
|
|
@@ -172,7 +172,7 @@ import {
|
|
|
172
172
|
SITE_CRAWL_GRAPH_MAX_NODES,
|
|
173
173
|
SKILL_MANIFEST_FILENAME,
|
|
174
174
|
SchedulableRunKinds,
|
|
175
|
-
|
|
175
|
+
SiteHealthLinkClassificationSources,
|
|
176
176
|
SkillsClients,
|
|
177
177
|
TEMPLATE_LINK_RATIO_THRESHOLD,
|
|
178
178
|
TrafficEventConfidences,
|
|
@@ -193,11 +193,13 @@ import {
|
|
|
193
193
|
bucketOnboardingCount,
|
|
194
194
|
buildRunErrorFromMessages,
|
|
195
195
|
classifySkillFile,
|
|
196
|
+
classifyTemplateLinkEdge,
|
|
196
197
|
cloudflareQueueNameSchema,
|
|
197
198
|
coerceSkillManifest,
|
|
198
199
|
computeBacklinkSummaryMetrics,
|
|
199
200
|
contentActionLabel,
|
|
200
201
|
contentBriefDtoSchema,
|
|
202
|
+
createTemplateLinkDetectionTally,
|
|
201
203
|
createTemplateLinkPairIndex,
|
|
202
204
|
deriveCitedUrlCandidates,
|
|
203
205
|
deriveIndexCoverage,
|
|
@@ -220,7 +222,6 @@ import {
|
|
|
220
222
|
isGhostTelemetryEvent,
|
|
221
223
|
isRetryableHttpError,
|
|
222
224
|
isSelfLink,
|
|
223
|
-
isTemplateLinkRatio,
|
|
224
225
|
isVertexGroundingRedirect,
|
|
225
226
|
mapWithConcurrency,
|
|
226
227
|
normalizeIdTokens,
|
|
@@ -229,6 +230,7 @@ import {
|
|
|
229
230
|
normalizeSiteAuditRunRequest,
|
|
230
231
|
normalizeUrlPath,
|
|
231
232
|
notFound,
|
|
233
|
+
observeTemplateLinkDetection,
|
|
232
234
|
observeTemplateLinkEdges,
|
|
233
235
|
parseMeasurementRunManifestV1,
|
|
234
236
|
parseOriginList,
|
|
@@ -244,13 +246,14 @@ import {
|
|
|
244
246
|
splitList,
|
|
245
247
|
startOfDayHourInTimeZone,
|
|
246
248
|
templateLinkDetection,
|
|
247
|
-
|
|
249
|
+
templateLinkPlacementAvailable,
|
|
250
|
+
templateLinkUbiquityAvailable,
|
|
248
251
|
textContainsBrandAlias,
|
|
249
252
|
textContainsDomain,
|
|
250
253
|
validationError,
|
|
251
254
|
winnabilityClassLabel,
|
|
252
255
|
withRetry
|
|
253
|
-
} from "./chunk-
|
|
256
|
+
} from "./chunk-ESH3LPYQ.js";
|
|
254
257
|
|
|
255
258
|
// src/telemetry.ts
|
|
256
259
|
import crypto from "crypto";
|
|
@@ -7304,7 +7307,7 @@ var COMPONENT_PACKING_SPACING_FACTOR = 3;
|
|
|
7304
7307
|
function siteCrawlGraphLayoutSettingsFingerprint(settings) {
|
|
7305
7308
|
return crypto16.createHash("sha256").update(JSON.stringify(settings)).digest("hex").slice(0, 8);
|
|
7306
7309
|
}
|
|
7307
|
-
var SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM = "site-health-fa2-
|
|
7310
|
+
var SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM = "site-health-fa2-v5";
|
|
7308
7311
|
var SITE_CRAWL_GRAPH_LAYOUT_VERSION = `${SITE_CRAWL_GRAPH_LAYOUT_ALGORITHM}-${siteCrawlGraphLayoutSettingsFingerprint({
|
|
7309
7312
|
forceAtlas: FORCE_ATLAS_SETTINGS,
|
|
7310
7313
|
noverlap: NOVERLAP_SETTINGS,
|
|
@@ -7860,6 +7863,13 @@ function deleteSiteCrawlGraphLayout(db, scope) {
|
|
|
7860
7863
|
import { and as and11, asc as asc2, eq as eq14, gt, inArray as inArray4 } from "drizzle-orm";
|
|
7861
7864
|
var TEMPLATE_LINK_READ_BATCH = 2e3;
|
|
7862
7865
|
var TEMPLATE_LINK_WRITE_BATCH = 500;
|
|
7866
|
+
function placementOccurrencesOf(row) {
|
|
7867
|
+
const { placementNavigationOccurrences: navigation } = row;
|
|
7868
|
+
const { placementContentOccurrences: content } = row;
|
|
7869
|
+
const { placementUnknownOccurrences: unknown } = row;
|
|
7870
|
+
if (navigation == null || content == null || unknown == null) return null;
|
|
7871
|
+
return { navigation, content, unknown };
|
|
7872
|
+
}
|
|
7863
7873
|
function* streamAttemptEdges(db, scope) {
|
|
7864
7874
|
let after = "";
|
|
7865
7875
|
for (; ; ) {
|
|
@@ -7867,7 +7877,11 @@ function* streamAttemptEdges(db, scope) {
|
|
|
7867
7877
|
edgeKey: siteCrawlEdges.edgeKey,
|
|
7868
7878
|
sourceNodeKey: siteCrawlEdges.sourceNodeKey,
|
|
7869
7879
|
targetNodeKey: siteCrawlEdges.targetNodeKey,
|
|
7870
|
-
anchors: siteCrawlEdges.anchors
|
|
7880
|
+
anchors: siteCrawlEdges.anchors,
|
|
7881
|
+
relation: siteCrawlEdges.relation,
|
|
7882
|
+
placementNavigationOccurrences: siteCrawlEdges.placementNavigationOccurrences,
|
|
7883
|
+
placementContentOccurrences: siteCrawlEdges.placementContentOccurrences,
|
|
7884
|
+
placementUnknownOccurrences: siteCrawlEdges.placementUnknownOccurrences
|
|
7871
7885
|
}).from(siteCrawlEdges).where(and11(
|
|
7872
7886
|
eq14(siteCrawlEdges.projectId, scope.projectId),
|
|
7873
7887
|
eq14(siteCrawlEdges.runId, scope.runId),
|
|
@@ -7875,7 +7889,14 @@ function* streamAttemptEdges(db, scope) {
|
|
|
7875
7889
|
gt(siteCrawlEdges.edgeKey, after)
|
|
7876
7890
|
)).orderBy(asc2(siteCrawlEdges.edgeKey)).limit(TEMPLATE_LINK_READ_BATCH).all();
|
|
7877
7891
|
if (rows.length === 0) return;
|
|
7878
|
-
yield rows
|
|
7892
|
+
yield rows.map((row) => ({
|
|
7893
|
+
edgeKey: row.edgeKey,
|
|
7894
|
+
sourceNodeKey: row.sourceNodeKey,
|
|
7895
|
+
targetNodeKey: row.targetNodeKey,
|
|
7896
|
+
anchors: row.anchors,
|
|
7897
|
+
relation: row.relation,
|
|
7898
|
+
placementOccurrences: placementOccurrencesOf(row)
|
|
7899
|
+
}));
|
|
7879
7900
|
after = rows[rows.length - 1].edgeKey;
|
|
7880
7901
|
if (rows.length < TEMPLATE_LINK_READ_BATCH) return;
|
|
7881
7902
|
}
|
|
@@ -7885,39 +7906,56 @@ function* chunked(values, size = TEMPLATE_LINK_WRITE_BATCH) {
|
|
|
7885
7906
|
yield values.slice(offset, offset + size);
|
|
7886
7907
|
}
|
|
7887
7908
|
}
|
|
7888
|
-
function
|
|
7909
|
+
function writeGroupKey(isTemplate, templateRatio) {
|
|
7910
|
+
return `${isTemplate ? "1" : "0"}${templateRatio ?? ""}`;
|
|
7911
|
+
}
|
|
7912
|
+
function classifySiteCrawlTemplateLinks(db, scope, pagesFetched, placementRulesetVersion) {
|
|
7889
7913
|
const scopeFilter = and11(
|
|
7890
7914
|
eq14(siteCrawlEdges.projectId, scope.projectId),
|
|
7891
7915
|
eq14(siteCrawlEdges.runId, scope.runId),
|
|
7892
7916
|
eq14(siteCrawlEdges.attemptId, scope.attemptId)
|
|
7893
7917
|
);
|
|
7894
|
-
const
|
|
7918
|
+
const placementAvailable = templateLinkPlacementAvailable(placementRulesetVersion);
|
|
7919
|
+
const ubiquityAvailable = templateLinkUbiquityAvailable(pagesFetched);
|
|
7895
7920
|
db.update(siteCrawlEdges).set({ isTemplate: false, templateRatio: null }).where(scopeFilter).run();
|
|
7896
|
-
if (detection !== SiteHealthTemplateDetections.applied) {
|
|
7897
|
-
return { detection, templateEdgeCount: 0 };
|
|
7898
|
-
}
|
|
7899
7921
|
const index = createTemplateLinkPairIndex();
|
|
7900
|
-
|
|
7901
|
-
|
|
7922
|
+
if (ubiquityAvailable) {
|
|
7923
|
+
for (const batch of streamAttemptEdges(db, scope)) observeTemplateLinkEdges(index, batch);
|
|
7924
|
+
}
|
|
7925
|
+
const edgeKeysByValue = /* @__PURE__ */ new Map();
|
|
7902
7926
|
let templateEdgeCount = 0;
|
|
7927
|
+
let placementEdgeCount = 0;
|
|
7928
|
+
const tally = createTemplateLinkDetectionTally();
|
|
7903
7929
|
for (const batch of streamAttemptEdges(db, scope)) {
|
|
7904
7930
|
for (const edge of batch) {
|
|
7905
|
-
const
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
if (
|
|
7909
|
-
|
|
7910
|
-
|
|
7931
|
+
const decision = classifyTemplateLinkEdge(edge, { index, pagesFetched, placementAvailable, ubiquityAvailable });
|
|
7932
|
+
observeTemplateLinkDetection(tally, edge, decision);
|
|
7933
|
+
if (decision.source === SiteHealthLinkClassificationSources.placement) placementEdgeCount += 1;
|
|
7934
|
+
if (decision.isTemplate) templateEdgeCount += 1;
|
|
7935
|
+
if (!decision.isTemplate && decision.templateRatio == null) continue;
|
|
7936
|
+
const key = writeGroupKey(decision.isTemplate, decision.templateRatio);
|
|
7937
|
+
const group = edgeKeysByValue.get(key);
|
|
7938
|
+
if (group) group.edgeKeys.push(decision.edgeKey);
|
|
7939
|
+
else {
|
|
7940
|
+
edgeKeysByValue.set(key, {
|
|
7941
|
+
isTemplate: decision.isTemplate,
|
|
7942
|
+
templateRatio: decision.templateRatio,
|
|
7943
|
+
edgeKeys: [decision.edgeKey]
|
|
7944
|
+
});
|
|
7945
|
+
}
|
|
7911
7946
|
}
|
|
7912
7947
|
}
|
|
7913
7948
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
7914
|
-
for (const
|
|
7915
|
-
const
|
|
7916
|
-
|
|
7917
|
-
db.update(siteCrawlEdges).set({ isTemplate, templateRatio: ratio, updatedAt: now }).where(and11(scopeFilter, inArray4(siteCrawlEdges.edgeKey, chunk))).run();
|
|
7949
|
+
for (const group of edgeKeysByValue.values()) {
|
|
7950
|
+
for (const chunk of chunked(group.edgeKeys)) {
|
|
7951
|
+
db.update(siteCrawlEdges).set({ isTemplate: group.isTemplate, templateRatio: group.templateRatio, updatedAt: now }).where(and11(scopeFilter, inArray4(siteCrawlEdges.edgeKey, chunk))).run();
|
|
7918
7952
|
}
|
|
7919
7953
|
}
|
|
7920
|
-
return {
|
|
7954
|
+
return {
|
|
7955
|
+
detection: templateLinkDetection({ placementAvailable, ubiquityAvailable, tally }),
|
|
7956
|
+
templateEdgeCount,
|
|
7957
|
+
placementEdgeCount
|
|
7958
|
+
};
|
|
7921
7959
|
}
|
|
7922
7960
|
|
|
7923
7961
|
// src/execute-site-audit.ts
|
|
@@ -8230,6 +8268,11 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8230
8268
|
for (const page of pages) persistPage(tx, page, now);
|
|
8231
8269
|
bindReferences(tx, changedUrls, now);
|
|
8232
8270
|
};
|
|
8271
|
+
const edgePlacementColumns = (edge) => ({
|
|
8272
|
+
placementNavigationOccurrences: edge.placementOccurrences?.navigation ?? null,
|
|
8273
|
+
placementContentOccurrences: edge.placementOccurrences?.content ?? null,
|
|
8274
|
+
placementUnknownOccurrences: edge.placementOccurrences?.unknown ?? null
|
|
8275
|
+
});
|
|
8233
8276
|
const persistEdge = (tx, edge, now) => {
|
|
8234
8277
|
if (isSelfLink(edge.from, edge.to)) return;
|
|
8235
8278
|
observedEdges.set(edge.key, edge);
|
|
@@ -8250,6 +8293,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8250
8293
|
followableOccurrences: edge.followableOccurrences,
|
|
8251
8294
|
nofollowOccurrences: edge.nofollowOccurrences,
|
|
8252
8295
|
anchors: edge.anchorSummaries.map((anchor) => anchor.text),
|
|
8296
|
+
...edgePlacementColumns(edge),
|
|
8253
8297
|
createdAt: now,
|
|
8254
8298
|
updatedAt: now
|
|
8255
8299
|
}).onConflictDoUpdate({
|
|
@@ -8266,6 +8310,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8266
8310
|
followableOccurrences: edge.followableOccurrences,
|
|
8267
8311
|
nofollowOccurrences: edge.nofollowOccurrences,
|
|
8268
8312
|
anchors: edge.anchorSummaries.map((anchor) => anchor.text),
|
|
8313
|
+
...edgePlacementColumns(edge),
|
|
8269
8314
|
updatedAt: now
|
|
8270
8315
|
}
|
|
8271
8316
|
}).run();
|
|
@@ -8373,16 +8418,20 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8373
8418
|
if (crawlSummary.complete && !hasAuditedPages) {
|
|
8374
8419
|
throw emptyCompleteCrawlError(crawlSummary.rootUrl, crawlSummary.finalRootUrl, crawlSummary.pagesObserved);
|
|
8375
8420
|
}
|
|
8421
|
+
const placementRulesetVersion = crawlSummary.linkPlacementRulesetVersion ?? null;
|
|
8376
8422
|
const templateLinks = classifySiteCrawlTemplateLinks(
|
|
8377
8423
|
db,
|
|
8378
8424
|
{ projectId, runId, attemptId },
|
|
8379
|
-
crawlSummary.pagesFetched
|
|
8425
|
+
crawlSummary.pagesFetched,
|
|
8426
|
+
placementRulesetVersion
|
|
8380
8427
|
);
|
|
8381
8428
|
log12.info("template-links", {
|
|
8382
8429
|
runId,
|
|
8383
8430
|
projectId,
|
|
8384
8431
|
detection: templateLinks.detection,
|
|
8385
|
-
templateEdges: templateLinks.templateEdgeCount
|
|
8432
|
+
templateEdges: templateLinks.templateEdgeCount,
|
|
8433
|
+
placementEdges: templateLinks.placementEdgeCount,
|
|
8434
|
+
placementRulesetVersion
|
|
8386
8435
|
});
|
|
8387
8436
|
const graphLayout = await prepareSiteCrawlGraphLayout(db, {
|
|
8388
8437
|
projectId,
|
|
@@ -8497,6 +8546,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
|
|
|
8497
8546
|
deadLinksChecked,
|
|
8498
8547
|
deadLinksFound,
|
|
8499
8548
|
templateDetection: templateLinks.detection,
|
|
8549
|
+
linkPlacementRulesetVersion: placementRulesetVersion,
|
|
8500
8550
|
createdAt: finishedAt,
|
|
8501
8551
|
updatedAt: finishedAt
|
|
8502
8552
|
}).run();
|
|
@@ -9034,7 +9084,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
9034
9084
|
return result;
|
|
9035
9085
|
}
|
|
9036
9086
|
async function backfillInsightsCommand(project, opts) {
|
|
9037
|
-
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-
|
|
9087
|
+
const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-AFAFGLCM.js");
|
|
9038
9088
|
const config = loadConfig();
|
|
9039
9089
|
const db = createClient(config.database);
|
|
9040
9090
|
migrate(db);
|