@canonry/canonry 4.178.0 → 4.178.1

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 (26) hide show
  1. package/assets/assets/{AuditHistoryPanel-BmKdyRrR.js → AuditHistoryPanel-BCtMrEMF.js} +1 -1
  2. package/assets/assets/{BacklinksPage-DtqKPVLV.js → BacklinksPage-CIo2hA0i.js} +1 -1
  3. package/assets/assets/{HistoryPage-yyJsQiJQ.js → HistoryPage-D24U8cUG.js} +1 -1
  4. package/assets/assets/{MeasurementPropertyPage-hjW4iYtr.js → MeasurementPropertyPage-BKDdv-jJ.js} +1 -1
  5. package/assets/assets/{ProjectPage-DZKpnPvq.js → ProjectPage-DrIs_ZuR.js} +1 -1
  6. package/assets/assets/{RunRow-BZeBFb22.js → RunRow-B39IK7hK.js} +1 -1
  7. package/assets/assets/{RunsPage-tnXHsK_R.js → RunsPage-R4W1kD0B.js} +1 -1
  8. package/assets/assets/{SettingsPage-Djj5k8vv.js → SettingsPage-BAOGyt1F.js} +1 -1
  9. package/assets/assets/{SiteHealthSection-BkDncS11.js → SiteHealthSection-Bx7yVNMQ.js} +3 -3
  10. package/assets/assets/{TrafficPage-nJggXVh-.js → TrafficPage-DL8RvM2R.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-zxONbSn-.js → TrafficSourceDetailPage-COzjakZa.js} +1 -1
  12. package/assets/assets/{extract-error-message-NpQu2vBm.js → extract-error-message-B3TERCQ-.js} +1 -1
  13. package/assets/assets/index-xtxQtZRo.js +83 -0
  14. package/assets/assets/{react-sigma_core.esm.min-CxMdd8U8.js → react-sigma_core.esm.min-CbXAujub.js} +1 -1
  15. package/assets/index.html +1 -1
  16. package/dist/{chunk-YIQMTOEZ.js → chunk-JKKHGDDC.js} +79 -5
  17. package/dist/{chunk-T4JH77O6.js → chunk-OYDIND7Q.js} +71 -1
  18. package/dist/{chunk-SXKA4OXZ.js → chunk-UTW6QOGP.js} +1 -1
  19. package/dist/{chunk-A7FB2BVW.js → chunk-VIVLYBFX.js} +2 -2
  20. package/dist/{chunk-O7J5WFVZ.js → chunk-WINVGL5B.js} +101 -41
  21. package/dist/cli.js +5 -5
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-YISV327N.js → intelligence-service-YSRPTRKP.js} +2 -2
  24. package/dist/mcp.js +3 -3
  25. package/package.json +8 -8
  26. package/assets/assets/index-Dwwnkd_V.js +0 -83
@@ -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-BkDncS11.js";import"./index-Dwwnkd_V.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-B_LDVvTy.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-Bx7yVNMQ.js";import"./index-xtxQtZRo.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-B_LDVvTy.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-Dwwnkd_V.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-xtxQtZRo.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">
@@ -219,6 +219,7 @@ import {
219
219
  citationVisibilityResponseSchema,
220
220
  classifyAiReferralTrafficClass,
221
221
  classifyCitedSurface,
222
+ classifyProviderErrorMessage,
222
223
  classifySkillFile,
223
224
  classifySurfaceFromCategory,
224
225
  classifyTrafficPath,
@@ -481,6 +482,7 @@ import {
481
482
  normalizeMeasurementHost,
482
483
  normalizeMeasurementPathPrefix,
483
484
  normalizeModelId,
485
+ normalizeOnboardingEventForCollection,
484
486
  normalizeProjectAliases,
485
487
  normalizeProjectDomain,
486
488
  normalizeProjectName,
@@ -513,6 +515,7 @@ import {
513
515
  projectReportDtoSchema,
514
516
  projectSearchResponseSchema,
515
517
  projectUpsertRequestSchema,
518
+ providerAuthError,
516
519
  providerError,
517
520
  queryDtoSchema,
518
521
  queryGenerateRequestSchema,
@@ -634,7 +637,7 @@ import {
634
637
  wordpressSchemaDeployResultDtoSchema,
635
638
  wordpressSchemaStatusResultDtoSchema,
636
639
  wordpressStatusDtoSchema
637
- } from "./chunk-T4JH77O6.js";
640
+ } from "./chunk-OYDIND7Q.js";
638
641
 
639
642
  // src/intelligence-service.ts
640
643
  import { eq as eq62, desc as desc27, asc as asc11, and as and51, ne as ne8, or as or14, inArray as inArray21, gte as gte14, lte as lte12, isNull as isNull9, sql as sql25, exists } from "drizzle-orm";
@@ -710,6 +713,7 @@ __export(schema_exports, {
710
713
  gtmConnections: () => gtmConnections,
711
714
  gtmRawSnapshots: () => gtmRawSnapshots,
712
715
  healthSnapshots: () => healthSnapshots,
716
+ insightNotifyState: () => insightNotifyState,
713
717
  insights: () => insights,
714
718
  llmUsageEvents: () => llmUsageEvents,
715
719
  measurementDiscoveryConfigs: () => measurementDiscoveryConfigs,
@@ -1219,6 +1223,25 @@ var schedules = sqliteTable("schedules", {
1219
1223
  }, (table) => [
1220
1224
  uniqueIndex("idx_schedules_project_kind").on(table.projectId, table.kind)
1221
1225
  ]);
1226
+ var insightNotifyState = sqliteTable("insight_notify_state", {
1227
+ /** `${projectId}:${type}:${subject}:${window}` - stable across runs. */
1228
+ key: text("key").primaryKey(),
1229
+ projectId: text("project_id").notNull(),
1230
+ /** Insight type, e.g. `gbp-keyword-drop`. */
1231
+ type: text("type").notNull(),
1232
+ /** What the finding is about: the keyword, query, or url. */
1233
+ subject: text("subject").notNull(),
1234
+ /**
1235
+ * The finding's identity as text: its title with the magnitude number
1236
+ * neutralised, so 79% and 80% of the same drop over the same window are one
1237
+ * piece of news, while a window that advances is a different one.
1238
+ */
1239
+ fingerprint: text("fingerprint").notNull(),
1240
+ severity: text("severity").notNull(),
1241
+ /** Rounded magnitude, so a drift from 79% to 80% is not treated as new news. */
1242
+ magnitude: integer("magnitude"),
1243
+ notifiedAt: text("notified_at").notNull()
1244
+ });
1222
1245
  var doctorHealthState = sqliteTable("doctor_health_state", {
1223
1246
  projectId: text("project_id").primaryKey(),
1224
1247
  /** Worst check status observed on the last pass: ok | warn | fail. */
@@ -6990,6 +7013,32 @@ var MIGRATION_VERSIONS = [
6990
7013
  statements: [
6991
7014
  `ALTER TABLE oauth_clients ADD COLUMN registration TEXT NOT NULL DEFAULT 'operator'`
6992
7015
  ]
7016
+ },
7017
+ {
7018
+ version: 148,
7019
+ name: "insight-notify-state",
7020
+ // Insight dispatch had no memory. Health is edge-triggered and citations are
7021
+ // transition-based, but every run recomputed its insights and sent whatever
7022
+ // was high or critical, so a finding that persists alerted on every run,
7023
+ // forever. Measured on one client: a GBP keyword drop notified daily for over
7024
+ // a month with byte-identical text, because Google publishes GBP keyword data
7025
+ // about a month behind and the comparison window sat still while the calendar
7026
+ // moved on. The insights table held ONE row for it, so nothing in the stored
7027
+ // data revealed the repetition.
7028
+ statements: [
7029
+ `CREATE TABLE IF NOT EXISTS insight_notify_state (
7030
+ key TEXT PRIMARY KEY,
7031
+ project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
7032
+ type TEXT NOT NULL,
7033
+ subject TEXT NOT NULL,
7034
+ fingerprint TEXT NOT NULL,
7035
+ severity TEXT NOT NULL,
7036
+ magnitude INTEGER,
7037
+ notified_at TEXT NOT NULL
7038
+ )`,
7039
+ `CREATE INDEX IF NOT EXISTS insight_notify_state_project
7040
+ ON insight_notify_state(project_id)`
7041
+ ]
6993
7042
  }
6994
7043
  ];
6995
7044
  function addRunsMeasurementPlanVersionForeignKey(tx) {
@@ -9242,6 +9291,7 @@ var GBP_METRIC_MIN_BASELINE = 10;
9242
9291
  var GBP_KEYWORD_DROP_PCT = 40;
9243
9292
  var GBP_KEYWORD_SEVERE_PCT = 70;
9244
9293
  var GBP_KEYWORD_MIN_BASELINE = 20;
9294
+ var GBP_ACTIONS_HELD_PCT = -10;
9245
9295
  var GBP_INSIGHT_PROVIDER = "gbp";
9246
9296
  var METRIC_LABELS = {
9247
9297
  BUSINESS_DIRECTION_REQUESTS: "Direction requests",
@@ -9330,7 +9380,7 @@ function analyzeGbp(signals) {
9330
9380
  drafts.push({
9331
9381
  ...base,
9332
9382
  type: "gbp-keyword-drop",
9333
- severity: worstKeyword.dropPct >= GBP_KEYWORD_SEVERE_PCT ? "high" : "medium",
9383
+ severity: worstKeyword.dropPct >= GBP_KEYWORD_SEVERE_PCT && !actionsHeldUp(loc) ? "high" : "medium",
9334
9384
  title: `${loc.displayName}: "${worstKeyword.keyword}" impressions down ${worstKeyword.dropPct}% month-over-month${window}`,
9335
9385
  recommendation: {
9336
9386
  action: "Check whether the property still ranks for this local search term and refresh the profile",
@@ -9341,6 +9391,11 @@ function analyzeGbp(signals) {
9341
9391
  }
9342
9392
  return drafts;
9343
9393
  }
9394
+ function actionsHeldUp(loc) {
9395
+ const deltas = GBP_HEADLINE_METRICS.map((metric2) => loc.metricDeltaPct[metric2]).filter((d) => typeof d === "number");
9396
+ if (deltas.length === 0) return false;
9397
+ return deltas.every((d) => d >= GBP_ACTIONS_HELD_PCT);
9398
+ }
9344
9399
  function pickWorstMetricDrop(loc) {
9345
9400
  let worst = null;
9346
9401
  for (const metric2 of GBP_HEADLINE_METRICS) {
@@ -11782,6 +11837,24 @@ function replaceProjectQueries(tx, projectId, incomingTexts, now) {
11782
11837
  }
11783
11838
 
11784
11839
  // ../api-routes/src/queries.ts
11840
+ function generationFailure(err, fallbackMessage) {
11841
+ if (err instanceof AppError) return err;
11842
+ const message = err instanceof Error ? describeError(err) : fallbackMessage;
11843
+ switch (classifyProviderErrorMessage(message)) {
11844
+ case "PROVIDER_AUTH":
11845
+ return providerAuthError(message);
11846
+ case "RATE_LIMITED":
11847
+ return quotaExceeded("provider requests", { message });
11848
+ case "NETWORK":
11849
+ case "TIMEOUT":
11850
+ return providerError(message);
11851
+ // A malformed provider response and an unrecognized failure are both ours
11852
+ // to explain, not the caller's to retry.
11853
+ case "PARSE_ERROR":
11854
+ case "UNKNOWN":
11855
+ return internalError(message);
11856
+ }
11857
+ }
11785
11858
  async function queryRoutes(app, opts) {
11786
11859
  app.get("/projects/:name/queries", async (request, reply) => {
11787
11860
  const project = resolveProject(app.db, request.params.name);
@@ -11959,7 +12032,7 @@ async function queryRoutes(app, opts) {
11959
12032
  return reply.send({ queries: generated, provider });
11960
12033
  } catch (err) {
11961
12034
  request.log.error({ err }, "Query generation failed");
11962
- throw internalError(err instanceof Error ? err.message : "Failed to generate queries");
12035
+ throw generationFailure(err, "Failed to generate queries");
11963
12036
  }
11964
12037
  });
11965
12038
  app.get("/projects/:name/keywords", async (request, reply) => {
@@ -12086,7 +12159,7 @@ async function queryRoutes(app, opts) {
12086
12159
  return reply.send({ keywords: generated, provider });
12087
12160
  } catch (err) {
12088
12161
  request.log.error({ err }, "Keyword generation failed");
12089
- throw internalError(err instanceof Error ? err.message : "Failed to generate keywords");
12162
+ throw generationFailure(err, "Failed to generate keywords");
12090
12163
  }
12091
12164
  });
12092
12165
  }
@@ -37358,7 +37431,7 @@ async function telemetryRoutes(app, opts) {
37358
37431
  }))
37359
37432
  });
37360
37433
  }
37361
- opts.recordOnboardingEvent?.(parsed.data);
37434
+ opts.recordOnboardingEvent?.(normalizeOnboardingEventForCollection(parsed.data));
37362
37435
  return reply.status(202).send({ accepted: Boolean(opts.recordOnboardingEvent) });
37363
37436
  });
37364
37437
  }
@@ -79426,6 +79499,7 @@ export {
79426
79499
  auditLog,
79427
79500
  apiKeys,
79428
79501
  schedules,
79502
+ insightNotifyState,
79429
79503
  doctorHealthState,
79430
79504
  notifications,
79431
79505
  gscSearchData,
@@ -53,6 +53,9 @@ function quotaExceeded(metric, details) {
53
53
  function providerError(message, details) {
54
54
  return new AppError("PROVIDER_ERROR", message, 502, details);
55
55
  }
56
+ function providerAuthError(message, details) {
57
+ return new AppError("PROVIDER_AUTH", message, 502, details);
58
+ }
56
59
  function noProvider(projectName, details) {
57
60
  return new AppError(
58
61
  "NO_PROVIDER",
@@ -16157,6 +16160,44 @@ function serializeForInlineScript(value) {
16157
16160
  return JSON.stringify(value).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
16158
16161
  }
16159
16162
 
16163
+ // ../contracts/src/provider-errors.ts
16164
+ var PROVIDER_ERROR_PRIORITY = [
16165
+ "PROVIDER_AUTH",
16166
+ "RATE_LIMITED",
16167
+ "TIMEOUT",
16168
+ "NETWORK",
16169
+ "PARSE_ERROR",
16170
+ "UNKNOWN"
16171
+ ];
16172
+ function classifyProviderErrorMessage(message) {
16173
+ if (/\b401\b|\b403\b|unauthorized|forbidden|invalid[_ -]?api[_ -]?key|missing[_ -]?api[_ -]?key|authentication/i.test(message)) {
16174
+ return "PROVIDER_AUTH";
16175
+ }
16176
+ if (/\b429\b|rate[_ -]?limit|too many requests|quota[_ -]?exceeded/i.test(message)) {
16177
+ return "RATE_LIMITED";
16178
+ }
16179
+ if (/timeout|timed out|ETIMEDOUT/i.test(message)) {
16180
+ return "TIMEOUT";
16181
+ }
16182
+ if (/ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|network|fetch failed|socket hang up/i.test(message)) {
16183
+ return "NETWORK";
16184
+ }
16185
+ if (/parse|unexpected token|invalid json|malformed|JSON\.parse/i.test(message)) {
16186
+ return "PARSE_ERROR";
16187
+ }
16188
+ return "UNKNOWN";
16189
+ }
16190
+ function classifyProviderErrorMessages(messages) {
16191
+ const codes = /* @__PURE__ */ new Set();
16192
+ for (const message of messages) {
16193
+ codes.add(classifyProviderErrorMessage(message));
16194
+ }
16195
+ for (const code of PROVIDER_ERROR_PRIORITY) {
16196
+ if (codes.has(code)) return code;
16197
+ }
16198
+ return "UNKNOWN";
16199
+ }
16200
+
16160
16201
  // ../contracts/src/telemetry.ts
16161
16202
  import { z as z54 } from "zod";
16162
16203
  var ONBOARDING_FLOW_VERSION = 1;
@@ -16195,12 +16236,24 @@ var onboardingBlockReasonSchema = z54.enum([
16195
16236
  "run_rejected",
16196
16237
  "run_failed",
16197
16238
  "run_cancelled",
16239
+ // Provider-side failures the query-generation step actually hits. Without
16240
+ // these every one of them reported `unknown`, which is why the queries step
16241
+ // was the only blocker nobody could diagnose.
16242
+ "rate_limited",
16243
+ "provider_auth",
16244
+ "network",
16198
16245
  "unknown"
16199
16246
  ]);
16247
+ var onboardingSurfaceSchema = z54.enum([
16248
+ "wizard",
16249
+ "platform",
16250
+ "site_health"
16251
+ ]);
16200
16252
  var onboardingEventBaseSchema = z54.object({
16201
16253
  eventId: z54.string().uuid(),
16202
16254
  flowVersion: z54.literal(ONBOARDING_FLOW_VERSION),
16203
- onboardingSessionId: z54.string().uuid()
16255
+ onboardingSessionId: z54.string().uuid(),
16256
+ surface: onboardingSurfaceSchema.optional()
16204
16257
  });
16205
16258
  var onboardingTelemetryEventSchema = z54.discriminatedUnion("event", [
16206
16259
  onboardingEventBaseSchema.extend({
@@ -16224,11 +16277,24 @@ var onboardingTelemetryEventSchema = z54.discriminatedUnion("event", [
16224
16277
  event: z54.literal("run.requested"),
16225
16278
  origin: z54.literal("dashboard_setup"),
16226
16279
  result: z54.enum(["queued", "rejected"]),
16280
+ /**
16281
+ * What kind of run was asked for. A site-health crawl has no providers and
16282
+ * no tracked queries, so its buckets are legitimately `0`; without this
16283
+ * field that is indistinguishable from a misconfigured visibility sweep.
16284
+ * Optional on the wire and defaulted at collection for the same reason as
16285
+ * `surface`: a documented fallback that nothing applies becomes a null
16286
+ * bucket downstream.
16287
+ */
16288
+ kind: z54.enum(["answer_visibility", "site_health"]).optional(),
16227
16289
  providerCountBucket: onboardingCountBucketSchema,
16228
16290
  queryCountBucket: onboardingCountBucketSchema,
16229
16291
  reasonCode: onboardingBlockReasonSchema.optional()
16230
16292
  }).strict()
16231
16293
  ]);
16294
+ function normalizeOnboardingEventForCollection(event) {
16295
+ const surfaced = { ...event, surface: event.surface ?? "wizard" };
16296
+ return surfaced.event === "run.requested" ? { ...surfaced, kind: surfaced.kind ?? "answer_visibility" } : surfaced;
16297
+ }
16232
16298
  var telemetryEventAcceptedDtoSchema = z54.object({
16233
16299
  accepted: z54.boolean()
16234
16300
  });
@@ -16273,6 +16339,7 @@ export {
16273
16339
  forbidden,
16274
16340
  quotaExceeded,
16275
16341
  providerError,
16342
+ providerAuthError,
16276
16343
  noProvider,
16277
16344
  noQueries,
16278
16345
  runInProgress,
@@ -16956,9 +17023,12 @@ export {
16956
17023
  normalizeIdTokens,
16957
17024
  embedClientConfigForRequest,
16958
17025
  serializeForInlineScript,
17026
+ classifyProviderErrorMessage,
17027
+ classifyProviderErrorMessages,
16959
17028
  ONBOARDING_FLOW_VERSION,
16960
17029
  bucketOnboardingCount,
16961
17030
  onboardingTelemetryEventSchema,
17031
+ normalizeOnboardingEventForCollection,
16962
17032
  telemetryEventAcceptedDtoSchema,
16963
17033
  isGhostTelemetryEvent,
16964
17034
  MIN_TREND_POINTS,
@@ -92,7 +92,7 @@ import {
92
92
  trafficConnectWordpressRequestSchema,
93
93
  trafficEventKindSchema,
94
94
  trafficSeriesGranularitySchema
95
- } from "./chunk-T4JH77O6.js";
95
+ } from "./chunk-OYDIND7Q.js";
96
96
 
97
97
  // src/config.ts
98
98
  import fs from "fs";
@@ -5,11 +5,11 @@ import {
5
5
  installSkills,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-SXKA4OXZ.js";
8
+ } from "./chunk-UTW6QOGP.js";
9
9
  import {
10
10
  SKILL_MANIFEST_FILENAME,
11
11
  SkillsClients
12
- } from "./chunk-T4JH77O6.js";
12
+ } from "./chunk-OYDIND7Q.js";
13
13
 
14
14
  // src/skills-autosync.ts
15
15
  import fs from "fs";
@@ -11,7 +11,7 @@ import {
11
11
  loadConfig,
12
12
  loadConfigRaw,
13
13
  saveConfigPatch
14
- } from "./chunk-SXKA4OXZ.js";
14
+ } from "./chunk-UTW6QOGP.js";
15
15
  import {
16
16
  CC_CACHE_DIR,
17
17
  DUCKDB_SPEC,
@@ -99,6 +99,7 @@ import {
99
99
  hashAttributes,
100
100
  hashLodging,
101
101
  hashPlaceDetails,
102
+ insightNotifyState,
102
103
  insights,
103
104
  inspectUrl,
104
105
  installDuckdb,
@@ -156,7 +157,7 @@ import {
156
157
  siteCrawlSnapshots,
157
158
  toAlertView,
158
159
  usageCounters
159
- } from "./chunk-YIQMTOEZ.js";
160
+ } from "./chunk-JKKHGDDC.js";
160
161
  import {
161
162
  AGENT_MEMORY_VALUE_MAX_BYTES,
162
163
  AGENT_PROVIDER_IDS,
@@ -219,6 +220,7 @@ import {
219
220
  canonicalizeGtmAccountId,
220
221
  canonicalizeGtmContainerId,
221
222
  canonicalizeGtmResourceSelection,
223
+ classifyProviderErrorMessages,
222
224
  classifyTemplateLinkEdge,
223
225
  cloudflareQueueNameSchema,
224
226
  computeBacklinkSummaryMetrics,
@@ -294,7 +296,7 @@ import {
294
296
  validationError,
295
297
  winnabilityClassLabel,
296
298
  withRetry
297
- } from "./chunk-T4JH77O6.js";
299
+ } from "./chunk-OYDIND7Q.js";
298
300
 
299
301
  // src/telemetry.ts
300
302
  import crypto from "crypto";
@@ -6410,40 +6412,7 @@ function classifyRunAbortReason(message) {
6410
6412
  return void 0;
6411
6413
  }
6412
6414
  function classifyProviderErrors(errors) {
6413
- const codes = /* @__PURE__ */ new Set();
6414
- for (const message of errors.values()) {
6415
- codes.add(classifyOneProviderError(message));
6416
- }
6417
- const priority = [
6418
- "PROVIDER_AUTH",
6419
- "RATE_LIMITED",
6420
- "TIMEOUT",
6421
- "NETWORK",
6422
- "PARSE_ERROR",
6423
- "UNKNOWN"
6424
- ];
6425
- for (const code of priority) {
6426
- if (codes.has(code)) return code;
6427
- }
6428
- return "UNKNOWN";
6429
- }
6430
- function classifyOneProviderError(message) {
6431
- if (/\b401\b|\b403\b|unauthorized|forbidden|invalid[_ -]?api[_ -]?key|missing[_ -]?api[_ -]?key|authentication/i.test(message)) {
6432
- return "PROVIDER_AUTH";
6433
- }
6434
- if (/\b429\b|rate[_ -]?limit|too many requests|quota[_ -]?exceeded/i.test(message)) {
6435
- return "RATE_LIMITED";
6436
- }
6437
- if (/timeout|timed out|ETIMEDOUT/i.test(message)) {
6438
- return "TIMEOUT";
6439
- }
6440
- if (/ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|network|fetch failed|socket hang up/i.test(message)) {
6441
- return "NETWORK";
6442
- }
6443
- if (/parse|unexpected token|invalid json|malformed|JSON\.parse/i.test(message)) {
6444
- return "PARSE_ERROR";
6445
- }
6446
- return "UNKNOWN";
6415
+ return classifyProviderErrorMessages(errors.values());
6447
6416
  }
6448
6417
  var JobRunner = class {
6449
6418
  /**
@@ -10916,6 +10885,31 @@ function partitionDeadLinks(deadLinks) {
10916
10885
  ];
10917
10886
  return { dead, unverified };
10918
10887
  }
10888
+ function reportedTermination(summary) {
10889
+ const limits = summary.limits;
10890
+ if (limits) {
10891
+ if (limits.maxBytes && (summary.bytesRead ?? 0) >= limits.maxBytes) return "max-bytes";
10892
+ if (limits.maxDurationMs && (summary.elapsedMs ?? 0) >= limits.maxDurationMs) return "max-duration";
10893
+ if (limits.maxFetches && (summary.fetchesStarted ?? 0) >= limits.maxFetches) return "max-fetches";
10894
+ }
10895
+ return summary.terminationReason ?? "complete";
10896
+ }
10897
+ function crawlBudgetsFor(maxPages) {
10898
+ return {
10899
+ // 2x the ~745 KB/page measured on a media-heavy production site, so the
10900
+ // byte cap is headroom rather than the thing that stops the crawl.
10901
+ maxBytes: maxPages * 15e5,
10902
+ // ~5 pages/s at the engine's default concurrency of 5, doubled for slow
10903
+ // origins, and never below the engine's own 2-minute floor.
10904
+ maxDurationMs: Math.max(12e4, Math.ceil(maxPages / 5 * 1e3 * 2)),
10905
+ // Redirects and dead-link probes cost fetches without producing pages.
10906
+ maxFetches: Math.ceil(maxPages * 1.5),
10907
+ // Raised off the default of 10 because it is an ADMISSION limit: a sitemap
10908
+ // with many query-parameter variants trips it during seeding and latches a
10909
+ // soft reason that then masks whatever really stops the crawl.
10910
+ maxQueryVariants: Math.max(50, Math.ceil(maxPages / 10))
10911
+ };
10912
+ }
10919
10913
  async function executeSiteAudit(db, runId, projectId, opts = {}) {
10920
10914
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10921
10915
  const claim = db.update(runs).set({ status: "running", startedAt }).where(and12(eq15(runs.id, runId), eq15(runs.projectId, projectId), eq15(runs.status, "queued"))).run();
@@ -11216,6 +11210,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
11216
11210
  maxEdges,
11217
11211
  maxDepth: opts.maxDepth ?? null
11218
11212
  });
11213
+ const budgets = crawlBudgetsFor(maxPages);
11219
11214
  const report = await runSiteCrawl(crawlRootUrl, {
11220
11215
  mode: "summary",
11221
11216
  sitemapUrl: opts.sitemapUrl,
@@ -11223,6 +11218,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
11223
11218
  maxEdges,
11224
11219
  maxDepth: opts.maxDepth,
11225
11220
  checkDeadLinks: opts.checkDeadLinks ?? false,
11221
+ ...budgets,
11226
11222
  signal: opts.signal,
11227
11223
  onEvent: persistEvent
11228
11224
  });
@@ -11349,7 +11345,7 @@ async function executeSiteAudit(db, runId, projectId, opts = {}) {
11349
11345
  maxDepth: opts.maxDepth ?? null,
11350
11346
  checkDeadLinks: opts.checkDeadLinks ?? false,
11351
11347
  complete: crawlSummary.complete,
11352
- termination: crawlSummary.terminationReason ?? "complete",
11348
+ termination: reportedTermination(crawlSummary),
11353
11349
  detailsAvailable: true,
11354
11350
  pagesDiscovered: crawlSummary.pagesDiscovered,
11355
11351
  pagesFetched: crawlSummary.pagesFetched,
@@ -11900,7 +11896,7 @@ function readStoredGroundingSources(rawResponse) {
11900
11896
  return result;
11901
11897
  }
11902
11898
  async function backfillInsightsCommand(project, opts) {
11903
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-YISV327N.js");
11899
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-YSRPTRKP.js");
11904
11900
  const config = loadConfig();
11905
11901
  const db = createClient(config.database);
11906
11902
  migrate(db);
@@ -12890,6 +12886,21 @@ async function refreshAllIntegrations(client, projectName) {
12890
12886
  import { eq as eq18, desc as desc7, and as and15, inArray as inArray7, or as or2 } from "drizzle-orm";
12891
12887
  import crypto21 from "crypto";
12892
12888
  var log16 = createLogger("Notifier");
12889
+ var MATERIAL_MAGNITUDE_DELTA = 20;
12890
+ var SEVERITY_RANK = { low: 0, medium: 1, high: 2, critical: 3 };
12891
+ function severityRank(severity) {
12892
+ return SEVERITY_RANK[severity] ?? 0;
12893
+ }
12894
+ function insightFingerprint(insight) {
12895
+ return insight.title.replace(/\b\d+(?:\.\d+)?%/g, "N%");
12896
+ }
12897
+ function insightMagnitude(insight) {
12898
+ const match = /\b(\d+(?:\.\d+)?)%/.exec(insight.title);
12899
+ return match?.[1] === void 0 ? null : Math.round(Number(match[1]));
12900
+ }
12901
+ function insightNotifyKey(projectId, insight) {
12902
+ return `${projectId}:${insight.type}:${insight.query}:${insightFingerprint(insight)}`;
12903
+ }
12893
12904
  var Notifier = class {
12894
12905
  db;
12895
12906
  serverUrl;
@@ -13045,11 +13056,59 @@ var Notifier = class {
13045
13056
  log16.info("health.notified", { projectId, event, status, code, subscribers: notifs.length, delivered });
13046
13057
  return event;
13047
13058
  }
13059
+ /**
13060
+ * Has this finding already been said?
13061
+ *
13062
+ * The identity of a finding is its type, its subject and its wording with the
13063
+ * magnitude neutralised. So the same drop reported at 79% and at 80% over the
13064
+ * same window is one piece of news, while the same drop over an ADVANCED
13065
+ * window is a new one.
13066
+ *
13067
+ * Re-notifies on three things and nothing else:
13068
+ * - never sent before
13069
+ * - severity got worse (medium -> high -> critical)
13070
+ * - magnitude moved by more than MATERIAL_MAGNITUDE_DELTA points
13071
+ *
13072
+ * Deliberately NOT time-based. A daily repeat is exactly the behaviour this
13073
+ * exists to stop, and "re-alert after N days" is that behaviour with a delay.
13074
+ */
13075
+ isNewInsightNews(projectId, insight) {
13076
+ const key = insightNotifyKey(projectId, insight);
13077
+ const previous = this.db.select().from(insightNotifyState).where(eq18(insightNotifyState.key, key)).get();
13078
+ if (!previous) return true;
13079
+ if (severityRank(insight.severity) > severityRank(previous.severity)) return true;
13080
+ const magnitude = insightMagnitude(insight);
13081
+ if (magnitude !== null && previous.magnitude !== null && previous.magnitude !== void 0) {
13082
+ if (Math.abs(magnitude - previous.magnitude) > MATERIAL_MAGNITUDE_DELTA) return true;
13083
+ }
13084
+ return false;
13085
+ }
13086
+ /** Record a delivered insight so it stops being news. */
13087
+ rememberInsightNotified(projectId, insight) {
13088
+ const key = insightNotifyKey(projectId, insight);
13089
+ const row = {
13090
+ key,
13091
+ projectId,
13092
+ type: insight.type,
13093
+ subject: insight.query,
13094
+ fingerprint: insightFingerprint(insight),
13095
+ severity: insight.severity,
13096
+ magnitude: insightMagnitude(insight),
13097
+ notifiedAt: (/* @__PURE__ */ new Date()).toISOString()
13098
+ };
13099
+ const existing = this.db.select().from(insightNotifyState).where(eq18(insightNotifyState.key, key)).get();
13100
+ if (existing) {
13101
+ this.db.update(insightNotifyState).set(row).where(eq18(insightNotifyState.key, key)).run();
13102
+ } else {
13103
+ this.db.insert(insightNotifyState).values(row).run();
13104
+ }
13105
+ }
13048
13106
  /** Dispatch insight webhooks for critical/high severity insights after a run. */
13049
13107
  async dispatchInsightWebhooks(runId, projectId, result) {
13050
13108
  const insightEvents = [];
13051
- const criticalInsights = result.insights.filter((i) => i.severity === "critical");
13052
- const highInsights = result.insights.filter((i) => i.severity === "high");
13109
+ const unsent = result.insights.filter((i) => this.isNewInsightNews(projectId, i));
13110
+ const criticalInsights = unsent.filter((i) => i.severity === "critical");
13111
+ const highInsights = unsent.filter((i) => i.severity === "high");
13053
13112
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
13054
13113
  if (highInsights.length > 0) insightEvents.push("insight.high");
13055
13114
  if (insightEvents.length === 0) return;
@@ -13083,6 +13142,7 @@ var Notifier = class {
13083
13142
  dashboardUrl: `${this.serverUrl}/projects/${project.name}`
13084
13143
  };
13085
13144
  await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null);
13145
+ for (const insight of relevantInsights) this.rememberInsightNotified(projectId, insight);
13086
13146
  }
13087
13147
  }
13088
13148
  }
package/dist/cli.js CHANGED
@@ -26,11 +26,11 @@ import {
26
26
  showFirstRunNotice,
27
27
  trackCliCommandFinished,
28
28
  trackEvent
29
- } from "./chunk-O7J5WFVZ.js";
29
+ } from "./chunk-WINVGL5B.js";
30
30
  import {
31
31
  autoSyncSkills,
32
32
  formatAutoSyncNotice
33
- } from "./chunk-A7FB2BVW.js";
33
+ } from "./chunk-VIVLYBFX.js";
34
34
  import {
35
35
  CliError,
36
36
  EXIT_SYSTEM_ERROR,
@@ -55,7 +55,7 @@ import {
55
55
  saveConfigPatch,
56
56
  systemError,
57
57
  usageError
58
- } from "./chunk-SXKA4OXZ.js";
58
+ } from "./chunk-UTW6QOGP.js";
59
59
  import {
60
60
  CLOUDFLARE_WORKER_BINDINGS,
61
61
  CLOUDFLARE_WORKER_GENERATED_MARKER,
@@ -69,7 +69,7 @@ import {
69
69
  projects,
70
70
  queries,
71
71
  renderReportHtml
72
- } from "./chunk-YIQMTOEZ.js";
72
+ } from "./chunk-JKKHGDDC.js";
73
73
  import {
74
74
  AdsDeliverySnapshotStatuses,
75
75
  AdsHistoricalCampaignRollupStatuses,
@@ -138,7 +138,7 @@ import {
138
138
  providerQuotaPolicySchema,
139
139
  resolveProviderInput,
140
140
  winnabilityClassSchema
141
- } from "./chunk-T4JH77O6.js";
141
+ } from "./chunk-OYDIND7Q.js";
142
142
 
143
143
  // src/cli.ts
144
144
  import { pathToFileURL } from "url";