@canonry/canonry 4.162.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.
Files changed (25) hide show
  1. package/assets/assets/{AuditHistoryPanel-BTtqk1mK.js → AuditHistoryPanel-FDQx9TS5.js} +1 -1
  2. package/assets/assets/{BacklinksPage-wqRYkJFG.js → BacklinksPage-DVvsQsdk.js} +1 -1
  3. package/assets/assets/{HistoryPage-Dj1FU5WV.js → HistoryPage-31HR6TIJ.js} +1 -1
  4. package/assets/assets/{MeasurementPropertyPage-CPCYzznG.js → MeasurementPropertyPage-Dps7MF7r.js} +1 -1
  5. package/assets/assets/{ProjectPage-7vg1El9D.js → ProjectPage-BijTUrpk.js} +6 -6
  6. package/assets/assets/{RunRow-CTgtgq5x.js → RunRow-9DiFrBlc.js} +1 -1
  7. package/assets/assets/{RunsPage-qS7etC5f.js → RunsPage-DQlAc4J9.js} +1 -1
  8. package/assets/assets/{SettingsPage-BlUWPiA7.js → SettingsPage-c1nxJsbL.js} +1 -1
  9. package/assets/assets/SiteHealthSection-0B0oUUQx.js +4 -0
  10. package/assets/assets/{TrafficPage-BSWUsXM1.js → TrafficPage-HzwQyLzl.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-DOTtU4SK.js → TrafficSourceDetailPage-BTVAQjRd.js} +1 -1
  12. package/assets/assets/{extract-error-message-DOWCsWL0.js → extract-error-message-B5bhJRmf.js} +1 -1
  13. package/assets/assets/{index-BYGqta_M.js → index-CQsvCUM0.js} +19 -19
  14. package/assets/assets/{react-sigma_core.esm.min-C4kgfZfm.js → react-sigma_core.esm.min-6BxcniCA.js} +1 -1
  15. package/assets/index.html +1 -1
  16. package/dist/{chunk-NG7OR6GO.js → chunk-ESH3LPYQ.js} +120 -7
  17. package/dist/{chunk-C6ZZH3XI.js → chunk-G5UZTODF.js} +108 -32
  18. package/dist/{chunk-2TLRE2L3.js → chunk-INXI6PO7.js} +3 -3
  19. package/dist/{chunk-FZAAGXTK.js → chunk-MRHYSEZB.js} +80 -30
  20. package/dist/cli.js +4 -4
  21. package/dist/index.js +4 -4
  22. package/dist/{intelligence-service-ANRFEQWX.js → intelligence-service-AFAFGLCM.js} +2 -2
  23. package/dist/mcp.js +2 -2
  24. package/package.json +12 -12
  25. package/assets/assets/SiteHealthSection-tUFchSy-.js +0 -4
@@ -1,4 +1,4 @@
1
- import{g as Bt,t as S,r as C}from"./vendor-tanstack-Dock0eH1.js";import{e as Ht,G as Wt}from"./SiteHealthSection-tUFchSy-.js";import"./index-BYGqta_M.js";import"./vendor-radix-4UOaVWza.js";import"./vendor-recharts-D8rt5lKn.js";import"./vendor-lucide-nYKv-BB7.js";import"./vendor-markdown-Bn7fRwrx.js";import"./vendor-yaml-GWJz5H2G.js";import"./ChartPrimitives-BoYe2M-i.js";function Vt(r,n){if(typeof r!="object"||!r)return r;var i=r[Symbol.toPrimitive];if(i!==void 0){var e=i.call(r,n);if(typeof e!="object")return e;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(r)}function ae(r){var n=Vt(r,"string");return typeof n=="symbol"?n:n+""}function A(r,n){if(!(r instanceof n))throw new TypeError("Cannot call a class as a function")}function He(r,n){for(var i=0;i<n.length;i++){var e=n[i];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(r,ae(e.key),e)}}function L(r,n,i){return n&&He(r.prototype,n),i&&He(r,i),Object.defineProperty(r,"prototype",{writable:!1}),r}function Q(r){return Q=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(n){return n.__proto__||Object.getPrototypeOf(n)},Q(r)}function yt(){try{var r=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return(yt=function(){return!!r})()}function jt(r){if(r===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return r}function Zt(r,n){if(n&&(typeof n=="object"||typeof n=="function"))return n;if(n!==void 0)throw new TypeError("Derived constructors may only return object or undefined");return jt(r)}function I(r,n,i){return n=Q(n),Zt(r,yt()?Reflect.construct(n,i||[],Q(r).constructor):n.apply(r,i))}function xe(r,n){return xe=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,e){return i.__proto__=e,i},xe(r,n)}function G(r,n){if(typeof n!="function"&&n!==null)throw new TypeError("Super expression must either be null or a function");r.prototype=Object.create(n&&n.prototype,{constructor:{value:r,writable:!0,configurable:!0}}),Object.defineProperty(r,"prototype",{writable:!1}),n&&xe(r,n)}function qt(r){if(Array.isArray(r))return r}function Yt(r,n){var i=r==null?null:typeof Symbol<"u"&&r[Symbol.iterator]||r["@@iterator"];if(i!=null){var e,t,a,o,s=[],l=!0,h=!1;try{if(a=(i=i.call(r)).next,n===0){if(Object(i)!==i)return;l=!1}else for(;!(l=(e=a.call(i)).done)&&(s.push(e.value),s.length!==n);l=!0);}catch(u){h=!0,t=u}finally{try{if(!l&&i.return!=null&&(o=i.return(),Object(o)!==o))return}finally{if(h)throw t}}return s}}function Se(r,n){(n==null||n>r.length)&&(n=r.length);for(var i=0,e=Array(n);i<n;i++)e[i]=r[i];return e}function Tt(r,n){if(r){if(typeof r=="string")return Se(r,n);var i={}.toString.call(r).slice(8,-1);return i==="Object"&&r.constructor&&(i=r.constructor.name),i==="Map"||i==="Set"?Array.from(r):i==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?Se(r,n):void 0}}function Xt(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
1
+ import{g as Bt,t as S,r as C}from"./vendor-tanstack-Dock0eH1.js";import{e as Ht,G as Wt}from"./SiteHealthSection-0B0oUUQx.js";import"./index-CQsvCUM0.js";import"./vendor-radix-4UOaVWza.js";import"./vendor-recharts-D8rt5lKn.js";import"./vendor-lucide-nYKv-BB7.js";import"./vendor-markdown-Bn7fRwrx.js";import"./vendor-yaml-GWJz5H2G.js";import"./ChartPrimitives-BoYe2M-i.js";function Vt(r,n){if(typeof r!="object"||!r)return r;var i=r[Symbol.toPrimitive];if(i!==void 0){var e=i.call(r,n);if(typeof e!="object")return e;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(r)}function ae(r){var n=Vt(r,"string");return typeof n=="symbol"?n:n+""}function A(r,n){if(!(r instanceof n))throw new TypeError("Cannot call a class as a function")}function He(r,n){for(var i=0;i<n.length;i++){var e=n[i];e.enumerable=e.enumerable||!1,e.configurable=!0,"value"in e&&(e.writable=!0),Object.defineProperty(r,ae(e.key),e)}}function L(r,n,i){return n&&He(r.prototype,n),i&&He(r,i),Object.defineProperty(r,"prototype",{writable:!1}),r}function Q(r){return Q=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(n){return n.__proto__||Object.getPrototypeOf(n)},Q(r)}function yt(){try{var r=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],function(){}))}catch{}return(yt=function(){return!!r})()}function jt(r){if(r===void 0)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return r}function Zt(r,n){if(n&&(typeof n=="object"||typeof n=="function"))return n;if(n!==void 0)throw new TypeError("Derived constructors may only return object or undefined");return jt(r)}function I(r,n,i){return n=Q(n),Zt(r,yt()?Reflect.construct(n,i||[],Q(r).constructor):n.apply(r,i))}function xe(r,n){return xe=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(i,e){return i.__proto__=e,i},xe(r,n)}function G(r,n){if(typeof n!="function"&&n!==null)throw new TypeError("Super expression must either be null or a function");r.prototype=Object.create(n&&n.prototype,{constructor:{value:r,writable:!0,configurable:!0}}),Object.defineProperty(r,"prototype",{writable:!1}),n&&xe(r,n)}function qt(r){if(Array.isArray(r))return r}function Yt(r,n){var i=r==null?null:typeof Symbol<"u"&&r[Symbol.iterator]||r["@@iterator"];if(i!=null){var e,t,a,o,s=[],l=!0,h=!1;try{if(a=(i=i.call(r)).next,n===0){if(Object(i)!==i)return;l=!1}else for(;!(l=(e=a.call(i)).done)&&(s.push(e.value),s.length!==n);l=!0);}catch(u){h=!0,t=u}finally{try{if(!l&&i.return!=null&&(o=i.return(),Object(o)!==o))return}finally{if(h)throw t}}return s}}function Se(r,n){(n==null||n>r.length)&&(n=r.length);for(var i=0,e=Array(n);i<n;i++)e[i]=r[i];return e}function Tt(r,n){if(r){if(typeof r=="string")return Se(r,n);var i={}.toString.call(r).slice(8,-1);return i==="Object"&&r.constructor&&(i=r.constructor.name),i==="Map"||i==="Set"?Array.from(r):i==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(i)?Se(r,n):void 0}}function Xt(){throw new TypeError(`Invalid attempt to destructure non-iterable instance.
2
2
  In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}function ee(r,n){return qt(r)||Yt(r,n)||Tt(r,n)||Xt()}var pe={black:"#000000",silver:"#C0C0C0",gray:"#808080",grey:"#808080",white:"#FFFFFF",maroon:"#800000",red:"#FF0000",purple:"#800080",fuchsia:"#FF00FF",green:"#008000",lime:"#00FF00",olive:"#808000",yellow:"#FFFF00",navy:"#000080",blue:"#0000FF",teal:"#008080",aqua:"#00FFFF",darkblue:"#00008B",mediumblue:"#0000CD",darkgreen:"#006400",darkcyan:"#008B8B",deepskyblue:"#00BFFF",darkturquoise:"#00CED1",mediumspringgreen:"#00FA9A",springgreen:"#00FF7F",cyan:"#00FFFF",midnightblue:"#191970",dodgerblue:"#1E90FF",lightseagreen:"#20B2AA",forestgreen:"#228B22",seagreen:"#2E8B57",darkslategray:"#2F4F4F",darkslategrey:"#2F4F4F",limegreen:"#32CD32",mediumseagreen:"#3CB371",turquoise:"#40E0D0",royalblue:"#4169E1",steelblue:"#4682B4",darkslateblue:"#483D8B",mediumturquoise:"#48D1CC",indigo:"#4B0082",darkolivegreen:"#556B2F",cadetblue:"#5F9EA0",cornflowerblue:"#6495ED",rebeccapurple:"#663399",mediumaquamarine:"#66CDAA",dimgray:"#696969",dimgrey:"#696969",slateblue:"#6A5ACD",olivedrab:"#6B8E23",slategray:"#708090",slategrey:"#708090",lightslategray:"#778899",lightslategrey:"#778899",mediumslateblue:"#7B68EE",lawngreen:"#7CFC00",chartreuse:"#7FFF00",aquamarine:"#7FFFD4",skyblue:"#87CEEB",lightskyblue:"#87CEFA",blueviolet:"#8A2BE2",darkred:"#8B0000",darkmagenta:"#8B008B",saddlebrown:"#8B4513",darkseagreen:"#8FBC8F",lightgreen:"#90EE90",mediumpurple:"#9370DB",darkviolet:"#9400D3",palegreen:"#98FB98",darkorchid:"#9932CC",yellowgreen:"#9ACD32",sienna:"#A0522D",brown:"#A52A2A",darkgray:"#A9A9A9",darkgrey:"#A9A9A9",lightblue:"#ADD8E6",greenyellow:"#ADFF2F",paleturquoise:"#AFEEEE",lightsteelblue:"#B0C4DE",powderblue:"#B0E0E6",firebrick:"#B22222",darkgoldenrod:"#B8860B",mediumorchid:"#BA55D3",rosybrown:"#BC8F8F",darkkhaki:"#BDB76B",mediumvioletred:"#C71585",indianred:"#CD5C5C",peru:"#CD853F",chocolate:"#D2691E",tan:"#D2B48C",lightgray:"#D3D3D3",lightgrey:"#D3D3D3",thistle:"#D8BFD8",orchid:"#DA70D6",goldenrod:"#DAA520",palevioletred:"#DB7093",crimson:"#DC143C",gainsboro:"#DCDCDC",plum:"#DDA0DD",burlywood:"#DEB887",lightcyan:"#E0FFFF",lavender:"#E6E6FA",darksalmon:"#E9967A",violet:"#EE82EE",palegoldenrod:"#EEE8AA",lightcoral:"#F08080",khaki:"#F0E68C",aliceblue:"#F0F8FF",honeydew:"#F0FFF0",azure:"#F0FFFF",sandybrown:"#F4A460",wheat:"#F5DEB3",beige:"#F5F5DC",whitesmoke:"#F5F5F5",mintcream:"#F5FFFA",ghostwhite:"#F8F8FF",salmon:"#FA8072",antiquewhite:"#FAEBD7",linen:"#FAF0E6",lightgoldenrodyellow:"#FAFAD2",oldlace:"#FDF5E6",magenta:"#FF00FF",deeppink:"#FF1493",orangered:"#FF4500",tomato:"#FF6347",hotpink:"#FF69B4",coral:"#FF7F50",darkorange:"#FF8C00",lightsalmon:"#FFA07A",orange:"#FFA500",lightpink:"#FFB6C1",pink:"#FFC0CB",gold:"#FFD700",peachpuff:"#FFDAB9",navajowhite:"#FFDEAD",moccasin:"#FFE4B5",bisque:"#FFE4C4",mistyrose:"#FFE4E1",blanchedalmond:"#FFEBCD",papayawhip:"#FFEFD5",lavenderblush:"#FFF0F5",seashell:"#FFF5EE",cornsilk:"#FFF8DC",lemonchiffon:"#FFFACD",floralwhite:"#FFFAF0",snow:"#FFFAFA",lightyellow:"#FFFFE0",ivory:"#FFFFF0"},Ct=new Int8Array(4),be=new Int32Array(Ct.buffer,0,1),$t=new Float32Array(Ct.buffer,0,1),Kt=/^\s*rgba?\s*\(/,Jt=/^\s*rgba?\s*\(\s*([0-9]*)\s*,\s*([0-9]*)\s*,\s*([0-9]*)(?:\s*,\s*(.*)?)?\)\s*$/;function Qt(r){var n=0,i=0,e=0,t=1;if(r[0]==="#")r.length===4?(n=parseInt(r.charAt(1)+r.charAt(1),16),i=parseInt(r.charAt(2)+r.charAt(2),16),e=parseInt(r.charAt(3)+r.charAt(3),16)):(n=parseInt(r.charAt(1)+r.charAt(2),16),i=parseInt(r.charAt(3)+r.charAt(4),16),e=parseInt(r.charAt(5)+r.charAt(6),16)),r.length===9&&(t=parseInt(r.charAt(7)+r.charAt(8),16)/255);else if(Kt.test(r)){var a=r.match(Jt);a&&(n=+a[1],i=+a[2],e=+a[3],a[4]&&(t=+a[4]))}return{r:n,g:i,b:e,a:t}}var J={};for(var le in pe)J[le]=oe(pe[le]),J[pe[le]]=J[le];function Rt(r,n,i,e,t){return be[0]=e<<24|i<<16|n<<8|r,be[0]=be[0]&4278190079,$t[0]}function oe(r){if(r=r.toLowerCase(),typeof J[r]<"u")return J[r];var n=Qt(r),i=n.r,e=n.g,t=n.b,a=n.a;a=a*255|0;var o=Rt(i,e,t,a);return J[r]=o,o}var Ee={};function _t(r){if(typeof Ee[r]<"u")return Ee[r];var n=(r&16711680)>>>16,i=(r&65280)>>>8,e=r&255,t=255,a=Rt(n,i,e,t);return Ee[r]=a,a}function We(r,n,i,e){return i+(n<<8)+(r<<16)}function Ve(r,n,i,e,t,a){var o=Math.floor(i/a*t),s=Math.floor(r.drawingBufferHeight/a-e/a*t),l=new Uint8Array(4);r.bindFramebuffer(r.FRAMEBUFFER,n),r.readPixels(o,s,1,1,r.RGBA,r.UNSIGNED_BYTE,l);var h=ee(l,4),u=h[0],d=h[1],f=h[2],g=h[3];return[u,d,f,g]}function c(r,n,i){return(n=ae(n))in r?Object.defineProperty(r,n,{value:i,enumerable:!0,configurable:!0,writable:!0}):r[n]=i,r}function je(r,n){var i=Object.keys(r);if(Object.getOwnPropertySymbols){var e=Object.getOwnPropertySymbols(r);n&&(e=e.filter(function(t){return Object.getOwnPropertyDescriptor(r,t).enumerable})),i.push.apply(i,e)}return i}function b(r){for(var n=1;n<arguments.length;n++){var i=arguments[n]!=null?arguments[n]:{};n%2?je(Object(i),!0).forEach(function(e){c(r,e,i[e])}):Object.getOwnPropertyDescriptors?Object.defineProperties(r,Object.getOwnPropertyDescriptors(i)):je(Object(i)).forEach(function(e){Object.defineProperty(r,e,Object.getOwnPropertyDescriptor(i,e))})}return r}function ei(r,n){for(;!{}.hasOwnProperty.call(r,n)&&(r=Q(r))!==null;);return r}function Ae(){return Ae=typeof Reflect<"u"&&Reflect.get?Reflect.get.bind():function(r,n,i){var e=ei(r,n);if(e){var t=Object.getOwnPropertyDescriptor(e,n);return t.get?t.get.call(arguments.length<3?r:i):t.value}},Ae.apply(null,arguments)}function wt(r,n,i,e){var t=Ae(Q(r.prototype),n,i);return typeof t=="function"?function(a){return t.apply(i,a)}:t}function ti(r){return r.normalized?1:r.size}function ye(r){var n=0;return r.forEach(function(i){return n+=ti(i)}),n}function xt(r,n,i){var e=r==="VERTEX"?n.VERTEX_SHADER:n.FRAGMENT_SHADER,t=n.createShader(e);if(t===null)throw new Error("loadShader: error while creating the shader");n.shaderSource(t,i),n.compileShader(t);var a=n.getShaderParameter(t,n.COMPILE_STATUS);if(!a){var o=n.getShaderInfoLog(t);throw n.deleteShader(t),new Error(`loadShader: error while compiling the shader:
3
3
  `.concat(o,`
4
4
  `).concat(i))}return t}function ii(r,n){return xt("VERTEX",r,n)}function ri(r,n){return xt("FRAGMENT",r,n)}function ni(r,n){var i=r.createProgram();if(i===null)throw new Error("loadProgram: error while creating the program.");var e,t;for(e=0,t=n.length;e<t;e++)r.attachShader(i,n[e]);r.linkProgram(i);var a=r.getProgramParameter(i,r.LINK_STATUS);if(!a){var o=r.getProgramInfoLog(i);throw r.deleteProgram(i),new Error("loadProgram: error while linking the program: ".concat(o))}return i}function Ze(r){var n=r.gl,i=r.buffer,e=r.program,t=r.vertexShader,a=r.fragmentShader;n.deleteShader(t),n.deleteShader(a),n.deleteProgram(e),n.deleteBuffer(i)}var qe=`#define PICKING_MODE
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
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-BYGqta_M.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-CQsvCUM0.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Dock0eH1.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-4UOaVWza.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-D8rt5lKn.js">
@@ -8820,17 +8820,56 @@ function deriveSiteHealthState(input) {
8820
8820
  }
8821
8821
  var TEMPLATE_LINK_RATIO_THRESHOLD = 0.7;
8822
8822
  var TEMPLATE_LINK_MIN_FETCHED_PAGES = 15;
8823
+ var siteCrawlLinkPlacementSchema = z25.enum(["navigation", "content", "unknown"]);
8824
+ var SiteCrawlLinkPlacements = siteCrawlLinkPlacementSchema.enum;
8825
+ var siteCrawlPlacementOccurrencesSchema = z25.object({
8826
+ navigation: z25.number().int().nonnegative(),
8827
+ content: z25.number().int().nonnegative(),
8828
+ unknown: z25.number().int().nonnegative()
8829
+ });
8830
+ var siteHealthLinkClassificationSourceSchema = z25.enum([
8831
+ "placement",
8832
+ "ubiquity",
8833
+ "unmeasured"
8834
+ ]);
8835
+ var SiteHealthLinkClassificationSources = siteHealthLinkClassificationSourceSchema.enum;
8823
8836
  var siteHealthTemplateDetectionSchema = z25.enum([
8824
8837
  "applied",
8838
+ "applied-placement",
8839
+ "applied-placement-with-ubiquity",
8840
+ "applied-placement-partial",
8825
8841
  "unavailable-too-few-pages",
8826
8842
  "unavailable-legacy-scan"
8827
8843
  ]);
8828
8844
  var SiteHealthTemplateDetections = siteHealthTemplateDetectionSchema.enum;
8845
+ function isPlacementTemplateDetection(detection) {
8846
+ switch (detection) {
8847
+ case "applied-placement":
8848
+ case "applied-placement-with-ubiquity":
8849
+ case "applied-placement-partial":
8850
+ return true;
8851
+ case "applied":
8852
+ case "unavailable-too-few-pages":
8853
+ case "unavailable-legacy-scan":
8854
+ return false;
8855
+ }
8856
+ }
8829
8857
  var siteHealthLinkKindSchema = z25.enum(["all", "content", "template"]);
8830
8858
  var SiteHealthLinkKinds = siteHealthLinkKindSchema.enum;
8831
8859
  function normalizeTemplateAnchorText(anchor) {
8832
8860
  return anchor.trim().replace(/\s+/gu, " ").toLowerCase();
8833
8861
  }
8862
+ var siteCrawlEdgeRelationSchema = z25.enum(["anchor", "redirect", "canonical", "link"]);
8863
+ var SiteCrawlEdgeRelations = siteCrawlEdgeRelationSchema.enum;
8864
+ function isAnchorLinkRelation(relation) {
8865
+ return relation === SiteCrawlEdgeRelations.anchor;
8866
+ }
8867
+ function placementLinkDecision(occurrences) {
8868
+ if (!occurrences) return null;
8869
+ if (occurrences.content > 0) return SiteCrawlLinkPlacements.content;
8870
+ if (occurrences.navigation > 0) return SiteCrawlLinkPlacements.navigation;
8871
+ return null;
8872
+ }
8834
8873
  function createTemplateLinkPairIndex() {
8835
8874
  return { sourcePagesByPair: /* @__PURE__ */ new Map() };
8836
8875
  }
@@ -8865,8 +8904,63 @@ function templateLinkRatio(index, pagesFetched, edge) {
8865
8904
  function isTemplateLinkRatio(ratio) {
8866
8905
  return ratio != null && ratio >= TEMPLATE_LINK_RATIO_THRESHOLD;
8867
8906
  }
8868
- function templateLinkDetection(pagesFetched) {
8869
- return Number.isFinite(pagesFetched) && pagesFetched >= TEMPLATE_LINK_MIN_FETCHED_PAGES ? SiteHealthTemplateDetections.applied : SiteHealthTemplateDetections["unavailable-too-few-pages"];
8907
+ function templateLinkUbiquityAvailable(pagesFetched) {
8908
+ return Number.isFinite(pagesFetched) && pagesFetched >= TEMPLATE_LINK_MIN_FETCHED_PAGES;
8909
+ }
8910
+ function templateLinkPlacementAvailable(rulesetVersion) {
8911
+ return typeof rulesetVersion === "string" && rulesetVersion.trim().length > 0;
8912
+ }
8913
+ function createTemplateLinkDetectionTally() {
8914
+ return { usedUbiquityFallback: false, leftUnmeasuredAnchor: false };
8915
+ }
8916
+ function observeTemplateLinkDetection(tally, edge, classification) {
8917
+ if (classification.source === SiteHealthLinkClassificationSources.ubiquity) {
8918
+ tally.usedUbiquityFallback = true;
8919
+ }
8920
+ if (classification.source === SiteHealthLinkClassificationSources.unmeasured && isAnchorLinkRelation(edge.relation)) {
8921
+ tally.leftUnmeasuredAnchor = true;
8922
+ }
8923
+ }
8924
+ function templateLinkDetection(input) {
8925
+ if (!input.placementAvailable) {
8926
+ return input.ubiquityAvailable ? SiteHealthTemplateDetections.applied : SiteHealthTemplateDetections["unavailable-too-few-pages"];
8927
+ }
8928
+ if (input.tally.usedUbiquityFallback) return SiteHealthTemplateDetections["applied-placement-with-ubiquity"];
8929
+ if (input.tally.leftUnmeasuredAnchor) return SiteHealthTemplateDetections["applied-placement-partial"];
8930
+ return SiteHealthTemplateDetections["applied-placement"];
8931
+ }
8932
+ function classifyTemplateLinkEdge(edge, context) {
8933
+ const placement = context.placementAvailable ? placementLinkDecision(edge.placementOccurrences) : null;
8934
+ if (placement !== null) {
8935
+ return {
8936
+ edgeKey: edge.edgeKey,
8937
+ isTemplate: placement === SiteCrawlLinkPlacements.navigation,
8938
+ templateRatio: null,
8939
+ source: SiteHealthLinkClassificationSources.placement
8940
+ };
8941
+ }
8942
+ const ratio = context.ubiquityAvailable ? templateLinkRatio(context.index, context.pagesFetched, edge) : null;
8943
+ if (ratio == null) {
8944
+ return {
8945
+ edgeKey: edge.edgeKey,
8946
+ isTemplate: false,
8947
+ templateRatio: null,
8948
+ source: SiteHealthLinkClassificationSources.unmeasured
8949
+ };
8950
+ }
8951
+ return {
8952
+ edgeKey: edge.edgeKey,
8953
+ isTemplate: isTemplateLinkRatio(ratio),
8954
+ templateRatio: ratio,
8955
+ source: SiteHealthLinkClassificationSources.ubiquity
8956
+ };
8957
+ }
8958
+ function templateLinkSource(detection, edge) {
8959
+ if (edge.isTemplate == null) return SiteHealthLinkClassificationSources.unmeasured;
8960
+ if (isPlacementTemplateDetection(detection) && placementLinkDecision(edge.placementOccurrences) != null) {
8961
+ return SiteHealthLinkClassificationSources.placement;
8962
+ }
8963
+ return edge.templateRatio != null ? SiteHealthLinkClassificationSources.ubiquity : SiteHealthLinkClassificationSources.unmeasured;
8870
8964
  }
8871
8965
  var siteCrawlPageSchema = z25.object({
8872
8966
  nodeKey: z25.string(),
@@ -8987,17 +9081,29 @@ var siteCrawlEdgeSchema = z25.object({
8987
9081
  nofollowOccurrences: z25.number().int().nonnegative(),
8988
9082
  anchors: z25.array(z25.string()).default([]),
8989
9083
  /**
8990
- * True when this link repeats across the scan as nav, header, or footer
8991
- * chrome. Null means the scan never classified its links, which the
8992
- * response's `templateDetection` explains; it never means "not a nav link".
9084
+ * True when this link is nav, header, or footer chrome. Null means this link
9085
+ * was never classified, which `templateSource` and the response's
9086
+ * `templateDetection` explain; it never means "not a nav link".
8993
9087
  */
8994
9088
  isTemplate: z25.union([z25.boolean(), z25.null()]),
8995
9089
  /**
8996
9090
  * Share of fetched pages carrying this link's LEAST ubiquitous anchor: the
8997
9091
  * most editorial thing anyone says when linking these two pages. At or above
8998
- * the threshold means every anchor on the link is chrome.
9092
+ * the threshold means every anchor on the link is chrome. Present exactly
9093
+ * when `templateSource` is `ubiquity`, because it is that rule's evidence.
9094
+ */
9095
+ templateRatio: z25.union([z25.number(), z25.null()]),
9096
+ /**
9097
+ * Which rule decided `isTemplate`. Read this before comparing counts across
9098
+ * scans: `placement` and `ubiquity` do not measure the same thing.
9099
+ */
9100
+ templateSource: siteHealthLinkClassificationSourceSchema,
9101
+ /**
9102
+ * Where this link's occurrences sat in the page. Null when the scan predates
9103
+ * the crawler's landmark ruleset, which is why the ubiquity fallback still
9104
+ * exists.
8999
9105
  */
9000
- templateRatio: z25.union([z25.number(), z25.null()])
9106
+ placementOccurrences: z25.union([siteCrawlPlacementOccurrencesSchema, z25.null()])
9001
9107
  });
9002
9108
  var siteCrawlGraphNodeSchema = z25.object({
9003
9109
  nodeKey: z25.string(),
@@ -15147,6 +15253,7 @@ export {
15147
15253
  SiteCrawlFetchedStates,
15148
15254
  deriveSiteHealthState,
15149
15255
  TEMPLATE_LINK_RATIO_THRESHOLD,
15256
+ SiteHealthLinkClassificationSources,
15150
15257
  siteHealthTemplateDetectionSchema,
15151
15258
  SiteHealthTemplateDetections,
15152
15259
  siteHealthLinkKindSchema,
@@ -15155,7 +15262,13 @@ export {
15155
15262
  observeTemplateLinkEdges,
15156
15263
  templateLinkRatio,
15157
15264
  isTemplateLinkRatio,
15265
+ templateLinkUbiquityAvailable,
15266
+ templateLinkPlacementAvailable,
15267
+ createTemplateLinkDetectionTally,
15268
+ observeTemplateLinkDetection,
15158
15269
  templateLinkDetection,
15270
+ classifyTemplateLinkEdge,
15271
+ templateLinkSource,
15159
15272
  siteCrawlAuditFactorSchema,
15160
15273
  siteCrawlCriticalDefectSchema,
15161
15274
  siteCrawlPageAuditSchema,
@@ -539,8 +539,9 @@ import {
539
539
  surfaceClassFromCompetitorType,
540
540
  surfaceClassLabel,
541
541
  telemetryEventAcceptedDtoSchema,
542
- templateLinkDetection,
543
542
  templateLinkRatio,
543
+ templateLinkSource,
544
+ templateLinkUbiquityAvailable,
544
545
  textContainsAnyBrandAlias,
545
546
  textContainsBrandAlias,
546
547
  textContainsDomain,
@@ -580,7 +581,7 @@ import {
580
581
  wordpressSchemaDeployResultDtoSchema,
581
582
  wordpressSchemaStatusResultDtoSchema,
582
583
  wordpressStatusDtoSchema
583
- } from "./chunk-NG7OR6GO.js";
584
+ } from "./chunk-ESH3LPYQ.js";
584
585
 
585
586
  // src/intelligence-service.ts
586
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";
@@ -1344,6 +1345,14 @@ var siteCrawlSnapshots = sqliteTable("site_crawl_snapshots", {
1344
1345
  * pass for "this site has no nav".
1345
1346
  */
1346
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"),
1347
1356
  createdAt: text("created_at").notNull(),
1348
1357
  updatedAt: text("updated_at").notNull()
1349
1358
  }, (table) => [
@@ -1437,28 +1446,46 @@ var siteCrawlEdges = sqliteTable("site_crawl_edges", {
1437
1446
  nofollowOccurrences: integer("nofollow_occurrences").notNull().default(0),
1438
1447
  anchors: text("anchors", { mode: "json" }).$type().notNull().default([]),
1439
1448
  /**
1440
- * True when EVERY (target page, anchor) pair on this link appears on at
1441
- * least `TEMPLATE_LINK_RATIO_THRESHOLD` of the attempt's fetched pages: a
1442
- * nav, header, or footer link rather than an editorial one. Computed once at
1443
- * publish time by the contract's `classifyTemplateLinks`, so the map, the
1444
- * 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.
1445
1452
  *
1446
- * One row aggregates every anchor between the same two pages, so a link that
1447
- * is BOTH a footer link and an in-prose link is one row with two anchors. It
1448
- * is editorial, because the in-prose link exists.
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.
1449
1458
  *
1450
- * NULL means this row was never classified, which reads report as
1451
- * `unavailable-legacy-scan`. It never means "not a template link": the
1452
- * too-few-pages guard writes an explicit `false` and records its reason on
1453
- * the snapshot instead.
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".
1454
1470
  */
1455
1471
  isTemplate: integer("is_template", { mode: "boolean" }),
1456
1472
  /**
1457
- * Share of fetched pages carrying this link's LEAST ubiquitous anchor, which
1458
- * is the one that decides `is_template`. NULL when the link has no
1459
- * measurable pair (an unresolved target or no anchor).
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.
1460
1477
  */
1461
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"),
1462
1489
  createdAt: text("created_at").notNull(),
1463
1490
  updatedAt: text("updated_at").notNull()
1464
1491
  }, (table) => [
@@ -3001,7 +3028,7 @@ function* streamSiteCrawlTemplateLinkEdges(tx, attemptId, batchSize = 2e3) {
3001
3028
  let after = "";
3002
3029
  for (; ; ) {
3003
3030
  const rows = tx.all(sql2`
3004
- SELECT edge_key, source_node_key, target_node_key, anchors
3031
+ SELECT edge_key, source_node_key, target_node_key, anchors, relation
3005
3032
  FROM site_crawl_edges
3006
3033
  WHERE attempt_id = ${attemptId} AND edge_key > ${after}
3007
3034
  ORDER BY edge_key
@@ -3012,7 +3039,8 @@ function* streamSiteCrawlTemplateLinkEdges(tx, attemptId, batchSize = 2e3) {
3012
3039
  edgeKey: row.edge_key,
3013
3040
  sourceNodeKey: row.source_node_key,
3014
3041
  targetNodeKey: row.target_node_key,
3015
- anchors: parseJsonColumn(row.anchors, [])
3042
+ anchors: parseJsonColumn(row.anchors, []),
3043
+ relation: row.relation
3016
3044
  }));
3017
3045
  after = rows[rows.length - 1].edge_key;
3018
3046
  if (rows.length < batchSize) return;
@@ -3035,7 +3063,8 @@ function backfillSiteCrawlTemplateLinks(tx) {
3035
3063
  ON s.project_id = a.project_id AND s.run_id = a.run_id AND s.attempt_id = a.id
3036
3064
  `));
3037
3065
  for (const attempt of attempts) {
3038
- const detection = templateLinkDetection(attempt.pages_fetched);
3066
+ const ubiquityAvailable = templateLinkUbiquityAvailable(attempt.pages_fetched);
3067
+ const detection = ubiquityAvailable ? SiteHealthTemplateDetections.applied : SiteHealthTemplateDetections["unavailable-too-few-pages"];
3039
3068
  const scope = { attemptId: attempt.attempt_id, projectId: attempt.project_id, runId: attempt.run_id };
3040
3069
  tx.run(sql2`
3041
3070
  UPDATE site_crawl_edges SET is_template = 0, template_ratio = NULL
@@ -6206,6 +6235,35 @@ var MIGRATION_VERSIONS = [
6206
6235
  updated_at TEXT NOT NULL
6207
6236
  )`
6208
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
+ ]
6209
6267
  }
6210
6268
  ];
6211
6269
  function addRunsMeasurementPlanVersionForeignKey(tx) {
@@ -28445,7 +28503,7 @@ var crawlLimitParameter = {
28445
28503
  var linkKindParameter = {
28446
28504
  name: "linkKind",
28447
28505
  in: "query",
28448
- description: 'Restrict links to `content` (excludes nav, header, and footer links) or `template` (only those). Defaults to `all`. A scan whose links were never classified matches neither, which is why every link response also carries `templateDetection`: an empty `content` list means "could not tell" when detection is not `applied`.',
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`).',
28449
28507
  schema: { type: "string", enum: ["all", "content", "template"], default: "all" }
28450
28508
  };
28451
28509
  var measurementOverviewSortParameter = {
@@ -34010,7 +34068,7 @@ var routeCatalog = [
34010
34068
  method: "get",
34011
34069
  path: "/api/v1/projects/{name}/technical-aeo/graph",
34012
34070
  summary: "Get a persisted Site Health graph projection",
34013
- 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.",
34014
34072
  tags: ["technical-aeo"],
34015
34073
  parameters: [
34016
34074
  nameParameter,
@@ -34150,7 +34208,7 @@ var routeCatalog = [
34150
34208
  method: "get",
34151
34209
  path: "/api/v1/projects/{name}/technical-aeo/internal-links",
34152
34210
  summary: "List persisted internal crawl links",
34153
- 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, and `templateDetection` says whether nav and footer links could be told apart for this scan.",
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.",
34154
34212
  tags: ["technical-aeo"],
34155
34213
  parameters: [
34156
34214
  nameParameter,
@@ -59124,7 +59182,15 @@ function countLivePageHealthChecks(auditFields) {
59124
59182
  }
59125
59183
  return checks;
59126
59184
  }
59127
- function mapCrawlEdge(row) {
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);
59128
59194
  return {
59129
59195
  edgeKey: row.edgeKey,
59130
59196
  sourceNodeKey: row.sourceNodeKey,
@@ -59139,7 +59205,13 @@ function mapCrawlEdge(row) {
59139
59205
  nofollowOccurrences: row.nofollowOccurrences,
59140
59206
  anchors: row.anchors,
59141
59207
  isTemplate: row.isTemplate,
59142
- templateRatio: row.templateRatio
59208
+ templateRatio: row.templateRatio,
59209
+ templateSource: templateLinkSource(detection, {
59210
+ isTemplate: row.isTemplate,
59211
+ templateRatio: row.templateRatio,
59212
+ placementOccurrences
59213
+ }),
59214
+ placementOccurrences
59143
59215
  };
59144
59216
  }
59145
59217
  function templateDetectionOf(value) {
@@ -59772,7 +59844,8 @@ async function technicalAeoRoutes(app, opts) {
59772
59844
  distance: distances.get(row.nodeKey) ?? 0,
59773
59845
  relationToFocus: row.nodeKey === focus.nodeKey ? "focus" : directRelations.get(row.nodeKey) ?? "transitive"
59774
59846
  })).sort((a, b) => a.distance - b.distance || a.nodeKey.localeCompare(b.nodeKey));
59775
- 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(mapCrawlEdge);
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));
59776
59849
  const omittedNodes = omittedNodeKeys.size + Math.max(0, distances.size - pageRows.length);
59777
59850
  const omittedEdges = omittedEdgeKeys.size + (queryTruncated ? 1 : 0) + Math.max(0, edgeRows.size - edges.length);
59778
59851
  const truncated = omittedNodes > 0 || omittedEdges > 0;
@@ -59921,6 +59994,7 @@ async function technicalAeoRoutes(app, opts) {
59921
59994
  inArray20(siteCrawlPages.nodeKey, pathKeys)
59922
59995
  )).all();
59923
59996
  const pageByKey = new Map(pathRows.map((row) => [row.nodeKey, row]));
59997
+ const pathDetection = templateDetectionOf(snapshot.templateDetection);
59924
59998
  return {
59925
59999
  project: project.name,
59926
60000
  runId: snapshot.runId,
@@ -59932,7 +60006,7 @@ async function technicalAeoRoutes(app, opts) {
59932
60006
  maxDepth,
59933
60007
  visitedNodes: visited.size,
59934
60008
  nodes: pathKeys.map((key) => mapCrawlPage(pageByKey.get(key))),
59935
- edges: pathEdges.map(mapCrawlEdge)
60009
+ edges: pathEdges.map((row) => mapCrawlEdge(row, pathDetection))
59936
60010
  };
59937
60011
  });
59938
60012
  app.get("/projects/:name/technical-aeo/changes", async (request) => {
@@ -60210,8 +60284,10 @@ async function technicalAeoRoutes(app, opts) {
60210
60284
  )).all();
60211
60285
  const beforePages = new Map(pageRowsFor(beforeScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
60212
60286
  const afterPages = new Map(pageRowsFor(afterScope).map((row) => [row.nodeKey, mapCrawlPage(row)]));
60213
- const beforeLinks = new Map(edgeRowsFor(beforeScope).map((row) => [row.edgeKey, mapCrawlEdge(row)]));
60214
- const afterLinks = new Map(edgeRowsFor(afterScope).map((row) => [row.edgeKey, mapCrawlEdge(row)]));
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)]));
60215
60291
  const changes = visibleKeys.map((row) => {
60216
60292
  if (row.entity === "page") {
60217
60293
  const before2 = beforePages.get(row.key) ?? null;
@@ -60502,7 +60578,7 @@ async function technicalAeoRoutes(app, opts) {
60502
60578
  nextCursor: nextOffset < total ? encodeCursor2(nextOffset) : null,
60503
60579
  templateDetection,
60504
60580
  linkKind,
60505
- edges: rows.map(mapCrawlEdge)
60581
+ edges: rows.map((row) => mapCrawlEdge(row, templateDetection))
60506
60582
  };
60507
60583
  });
60508
60584
  app.get("/projects/:name/technical-aeo/internal-links/neighbors", async (request) => {
@@ -60565,8 +60641,8 @@ async function technicalAeoRoutes(app, opts) {
60565
60641
  url: request.query.url ?? null,
60566
60642
  templateDetection,
60567
60643
  linkKind,
60568
- inbound: inboundRows.slice(0, limit).map(mapCrawlEdge),
60569
- 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)),
60570
60646
  inboundTruncated: inboundRows.length > limit,
60571
60647
  outboundTruncated: outboundRows.length > limit
60572
60648
  };
@@ -81,7 +81,7 @@ import {
81
81
  trafficConnectWordpressRequestSchema,
82
82
  trafficEventKindSchema,
83
83
  trafficSeriesGranularitySchema
84
- } from "./chunk-NG7OR6GO.js";
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 whether that classification ran, so an empty content-only list is not evidence of no content links. Use the neighbors tool for one page rather than loading a graph.",
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,