@ainyc/canonry 3.3.9 → 3.4.5

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.
@@ -1,9 +1,11 @@
1
1
  import {
2
+ ContentActions,
3
+ RunKinds,
2
4
  __export
3
- } from "./chunk-MLKGABMK.js";
5
+ } from "./chunk-D4YFX3X4.js";
4
6
 
5
7
  // src/intelligence-service.ts
6
- import { eq, desc, asc, and, or } from "drizzle-orm";
8
+ import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
7
9
 
8
10
  // ../db/src/client.ts
9
11
  import { mkdirSync } from "fs";
@@ -1946,6 +1948,103 @@ function clamp012(value) {
1946
1948
  return value;
1947
1949
  }
1948
1950
 
1951
+ // ../intelligence/src/next-steps.ts
1952
+ var TOP_N = 5;
1953
+ var IMMEDIATE_HORIZON = 3;
1954
+ var ACTION_TITLE = {
1955
+ [ContentActions.create]: (q) => `Create a page targeting "${q}"`,
1956
+ [ContentActions.refresh]: (q) => `Refresh the page targeting "${q}"`,
1957
+ [ContentActions.expand]: (q) => `Expand coverage of "${q}"`,
1958
+ [ContentActions["add-schema"]]: (q) => `Add structured data to the page targeting "${q}"`
1959
+ };
1960
+ function mapOpportunitiesToNextSteps(opportunities, existing) {
1961
+ if (existing.length > 0) return existing;
1962
+ if (opportunities.length === 0) return [];
1963
+ return opportunities.slice(0, TOP_N).map((opp, idx) => ({
1964
+ horizon: idx < IMMEDIATE_HORIZON ? "immediate" : "short-term",
1965
+ title: ACTION_TITLE[opp.action](opp.query),
1966
+ rationale: `Score ${Math.round(opp.score)} \xB7 demand ${opp.demandSource} \xB7 ${opp.actionConfidence} confidence.`
1967
+ }));
1968
+ }
1969
+
1970
+ // ../intelligence/src/insight-severity.ts
1971
+ var SEVERITY_THRESHOLDS = {
1972
+ highTrafficImpressions: 100,
1973
+ recurrenceCount: 2,
1974
+ mediumTrafficImpressions: 10
1975
+ };
1976
+ function classifyRegressionSeverity(signals) {
1977
+ const { gscImpressions, recurrenceCount } = signals;
1978
+ if (gscImpressions === void 0 && recurrenceCount === void 0) return "high";
1979
+ const isHighTraffic = gscImpressions !== void 0 && gscImpressions >= SEVERITY_THRESHOLDS.highTrafficImpressions;
1980
+ const isRecurring = recurrenceCount !== void 0 && recurrenceCount >= SEVERITY_THRESHOLDS.recurrenceCount;
1981
+ const isModerateTraffic = gscImpressions !== void 0 && gscImpressions >= SEVERITY_THRESHOLDS.mediumTrafficImpressions;
1982
+ if (isHighTraffic && isRecurring) return "critical";
1983
+ if (isHighTraffic || isRecurring) return "high";
1984
+ if (isModerateTraffic) return "medium";
1985
+ return "low";
1986
+ }
1987
+
1988
+ // ../intelligence/src/insight-grouping.ts
1989
+ function groupInsights(insights2, keyFn = (i) => `${i.keyword} ${i.provider} ${i.type}`) {
1990
+ const order = [];
1991
+ const buckets = /* @__PURE__ */ new Map();
1992
+ for (const i of insights2) {
1993
+ const key = keyFn(i);
1994
+ const bucket = buckets.get(key);
1995
+ if (bucket) {
1996
+ bucket.push(i);
1997
+ } else {
1998
+ buckets.set(key, [i]);
1999
+ order.push(key);
2000
+ }
2001
+ }
2002
+ return order.map((key) => {
2003
+ const sorted = [...buckets.get(key)].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2004
+ const representative = sorted[sorted.length - 1];
2005
+ return {
2006
+ representative,
2007
+ count: sorted.length,
2008
+ instances: sorted,
2009
+ latest: representative.createdAt
2010
+ };
2011
+ });
2012
+ }
2013
+
2014
+ // ../intelligence/src/query-categorize.ts
2015
+ var TRANSACTIONAL_RE = /\b(buy|price|pricing|cost|hire|near me|services?|agency|consultant|company)\b/i;
2016
+ var INFORMATIONAL_RE = /\b(what|how|why|when|guide|tutorial|vs|versus|alternatives?|examples?|definition)\b/i;
2017
+ var MIN_BRAND_TOKEN_LENGTH = 3;
2018
+ function compact(value) {
2019
+ return value.toLowerCase().replace(/[^a-z0-9]/g, "");
2020
+ }
2021
+ function buildBrandTokens(canonicalDomain, displayName) {
2022
+ const seen = /* @__PURE__ */ new Set();
2023
+ const stem = canonicalDomain.toLowerCase().replace(/\.[a-z]{2,}$/, "");
2024
+ const stemCompact = compact(stem);
2025
+ if (stemCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(stemCompact);
2026
+ if (displayName) {
2027
+ const displayCompact = compact(displayName);
2028
+ if (displayCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(displayCompact);
2029
+ }
2030
+ return [...seen];
2031
+ }
2032
+ function categorizeQueryByIntent(query, brandTokens) {
2033
+ const compactQuery = compact(query);
2034
+ if (brandTokens.length > 0 && brandTokens.some((t) => compactQuery.includes(t))) {
2035
+ return "brand";
2036
+ }
2037
+ if (TRANSACTIONAL_RE.test(query)) return "lead-gen";
2038
+ if (INFORMATIONAL_RE.test(query)) return "industry";
2039
+ return "other";
2040
+ }
2041
+
2042
+ // ../intelligence/src/trend-stability.ts
2043
+ var MIN_TREND_POINTS = 4;
2044
+ function isTrendBaseline(points) {
2045
+ return points.length < MIN_TREND_POINTS;
2046
+ }
2047
+
1949
2048
  // src/intelligence-service.ts
1950
2049
  import crypto from "crypto";
1951
2050
 
@@ -1987,6 +2086,7 @@ function createLogger(module) {
1987
2086
  }
1988
2087
 
1989
2088
  // src/intelligence-service.ts
2089
+ var RECURRENCE_LOOKBACK_RUNS = 5;
1990
2090
  var log = createLogger("IntelligenceService");
1991
2091
  var IntelligenceService = class {
1992
2092
  constructor(db) {
@@ -2040,8 +2140,9 @@ var IntelligenceService = class {
2040
2140
  citedRate: result.health.overallCitedRate,
2041
2141
  insights: result.insights.length
2042
2142
  });
2043
- this.persistResult(result, runId, projectId);
2044
- return result;
2143
+ const tieredResult = this.tierResult(result, runId, projectId);
2144
+ this.persistResult(tieredResult, runId, projectId);
2145
+ return tieredResult;
2045
2146
  }
2046
2147
  /**
2047
2148
  * Analyze a single run given an explicit previous run (or null for first run).
@@ -2059,8 +2160,9 @@ var IntelligenceService = class {
2059
2160
  return result2;
2060
2161
  }
2061
2162
  const result = analyzeRuns(currentRun, previousRun);
2062
- this.persistResult(result, runRecord.id, runRecord.projectId);
2063
- return result;
2163
+ const tieredResult = this.tierResult(result, runRecord.id, runRecord.projectId);
2164
+ this.persistResult(tieredResult, runRecord.id, runRecord.projectId);
2165
+ return tieredResult;
2064
2166
  }
2065
2167
  /**
2066
2168
  * Backfill intelligence for all completed/partial runs of a project.
@@ -2151,6 +2253,59 @@ var IntelligenceService = class {
2151
2253
  });
2152
2254
  log.info("intelligence.persisted", { runId, insights: result.insights.length });
2153
2255
  }
2256
+ /**
2257
+ * Apply severity tiering to the insights of an AnalysisResult and return a
2258
+ * new result. Wraps `applySeverityTiering` so callers (analyzeAndPersist,
2259
+ * analyzeRunWithPrevious) can pass the same tiered shape both into the DB
2260
+ * write and back to the RunCoordinator / webhook dispatcher.
2261
+ */
2262
+ tierResult(result, runId, projectId) {
2263
+ if (result.insights.length === 0) return result;
2264
+ return { ...result, insights: this.applySeverityTiering(result.insights, runId, projectId) };
2265
+ }
2266
+ /**
2267
+ * Re-classify each regression insight's severity using GSC traffic +
2268
+ * recurrence signals via the pure `classifyRegressionSeverity` primitive
2269
+ * in @ainyc/canonry-intelligence. Non-regression insights are returned
2270
+ * untouched.
2271
+ */
2272
+ applySeverityTiering(rawInsights, excludeRunId, projectId) {
2273
+ const regressions = rawInsights.filter((i) => i.type === "regression");
2274
+ if (regressions.length === 0) return rawInsights;
2275
+ const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq(gscSearchData.projectId, projectId)).all();
2276
+ const gscConnected = gscRows.length > 0;
2277
+ const gscImpressionsByKeyword = /* @__PURE__ */ new Map();
2278
+ for (const row of gscRows) {
2279
+ const key = row.query.toLowerCase();
2280
+ gscImpressionsByKeyword.set(key, (gscImpressionsByKeyword.get(key) ?? 0) + row.impressions);
2281
+ }
2282
+ const recentRunIds = this.db.select({ id: runs.id }).from(runs).where(
2283
+ and(
2284
+ eq(runs.projectId, projectId),
2285
+ eq(runs.kind, RunKinds["answer-visibility"]),
2286
+ or(eq(runs.status, "completed"), eq(runs.status, "partial"))
2287
+ )
2288
+ ).orderBy(desc(runs.createdAt)).limit(RECURRENCE_LOOKBACK_RUNS + 1).all().map((r) => r.id).filter((id) => id !== excludeRunId).slice(0, RECURRENCE_LOOKBACK_RUNS);
2289
+ const haveHistory = recentRunIds.length > 0;
2290
+ const priorRegressionsByPair = /* @__PURE__ */ new Map();
2291
+ if (haveHistory) {
2292
+ const priorRows = this.db.select({ keyword: insights.keyword, provider: insights.provider }).from(insights).where(and(eq(insights.type, "regression"), inArray(insights.runId, recentRunIds))).all();
2293
+ for (const row of priorRows) {
2294
+ const key = `${row.keyword}:${row.provider}`;
2295
+ priorRegressionsByPair.set(key, (priorRegressionsByPair.get(key) ?? 0) + 1);
2296
+ }
2297
+ }
2298
+ return rawInsights.map((insight) => {
2299
+ if (insight.type !== "regression") return insight;
2300
+ const gscImpressions = gscConnected ? gscImpressionsByKeyword.get(insight.keyword.toLowerCase()) ?? 0 : void 0;
2301
+ const recurrenceCount = haveHistory ? priorRegressionsByPair.get(`${insight.keyword}:${insight.provider}`) ?? 0 : void 0;
2302
+ const severity = classifyRegressionSeverity({
2303
+ gscImpressions,
2304
+ recurrenceCount
2305
+ });
2306
+ return { ...insight, severity };
2307
+ });
2308
+ }
2154
2309
  buildRunData(runId, projectId, completedAt) {
2155
2310
  const rows = this.db.select({
2156
2311
  keyword: keywords.keyword,
@@ -2211,6 +2366,12 @@ export {
2211
2366
  buildContentTargetRows,
2212
2367
  buildContentSourceRows,
2213
2368
  buildContentGapRows,
2369
+ mapOpportunitiesToNextSteps,
2370
+ groupInsights,
2371
+ buildBrandTokens,
2372
+ categorizeQueryByIntent,
2373
+ MIN_TREND_POINTS,
2374
+ isTrendBaseline,
2214
2375
  createLogger,
2215
2376
  IntelligenceService
2216
2377
  };
package/dist/cli.js CHANGED
@@ -17,51 +17,55 @@ import {
17
17
  setGoogleAuthConfig,
18
18
  showFirstRunNotice,
19
19
  trackEvent
20
- } from "./chunk-P6D3O5JB.js";
20
+ } from "./chunk-P3PS3ZSN.js";
21
21
  import {
22
- CcReleaseSyncStatuses,
23
- CheckScopes,
24
- CheckStatuses,
25
22
  CliError,
26
- CodingAgents,
27
23
  EXIT_SYSTEM_ERROR,
28
24
  EXIT_USER_ERROR,
29
- ProviderNames,
30
- RunKinds,
31
- RunStatuses,
32
- SkillsClients,
33
25
  configExists,
34
26
  createApiClient,
35
- determineAnswerMentioned,
36
- effectiveDomains,
37
- formatRunErrorOneLine,
38
27
  getConfigDir,
39
28
  getConfigPath,
40
29
  isEndpointMissing,
41
30
  loadConfig,
42
- normalizeUrlPath,
43
- notificationEventSchema,
44
31
  printCliError,
45
- providerQuotaPolicySchema,
46
- resolveProviderInput,
47
32
  saveConfig,
48
33
  saveConfigPatch,
49
- skillsClientSchema,
50
34
  usageError
51
- } from "./chunk-24C7RMIS.js";
35
+ } from "./chunk-5OYYYY4I.js";
52
36
  import {
37
+ MIN_TREND_POINTS,
53
38
  apiKeys,
54
39
  competitors,
55
40
  createClient,
56
41
  gaAiReferrals,
57
42
  gaTrafficSnapshots,
43
+ groupInsights,
44
+ isTrendBaseline,
58
45
  migrate,
59
46
  parseJsonColumn,
60
47
  projects,
61
48
  querySnapshots,
62
49
  runs
63
- } from "./chunk-ZCPZOVVE.js";
64
- import "./chunk-MLKGABMK.js";
50
+ } from "./chunk-ZOJLW6WR.js";
51
+ import {
52
+ CcReleaseSyncStatuses,
53
+ CheckScopes,
54
+ CheckStatuses,
55
+ CodingAgents,
56
+ ProviderNames,
57
+ RunKinds,
58
+ RunStatuses,
59
+ SkillsClients,
60
+ determineAnswerMentioned,
61
+ effectiveDomains,
62
+ formatRunErrorOneLine,
63
+ normalizeUrlPath,
64
+ notificationEventSchema,
65
+ providerQuotaPolicySchema,
66
+ resolveProviderInput,
67
+ skillsClientSchema
68
+ } from "./chunk-D4YFX3X4.js";
65
69
 
66
70
  // src/cli.ts
67
71
  import { pathToFileURL } from "url";
@@ -577,7 +581,7 @@ function readStoredGroundingSources(rawResponse) {
577
581
  return result;
578
582
  }
579
583
  async function backfillInsightsCommand(project, opts) {
580
- const { IntelligenceService } = await import("./intelligence-service-FNJTFSI3.js");
584
+ const { IntelligenceService } = await import("./intelligence-service-GV6CAJ3Q.js");
581
585
  const config = loadConfig();
582
586
  const db = createClient(config.database);
583
587
  migrate(db);
@@ -5664,15 +5668,19 @@ function renderCompetitorLandscape(report) {
5664
5668
  }
5665
5669
  const rows = competitors2.map((c) => {
5666
5670
  const tone = pressureTone(c.pressureLabel);
5671
+ const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${c.theirCitedPages.length} cited URL${c.theirCitedPages.length > 1 ? "s" : ""}</summary>
5672
+ <ul>${c.theirCitedPages.map((p) => `<li><a href="${escapeHtml(p.url)}">${escapeHtml(p.url)}</a> <span class="cited-for">${escapeHtml(p.citedFor.join(", "))}</span></li>`).join("")}</ul>
5673
+ </details>` : "";
5667
5674
  return `<tr>
5668
5675
  <td>${escapeHtml(c.domain)}</td>
5669
5676
  <td><span class="badge tone-${tone}">${escapeHtml(c.pressureLabel)}</span></td>
5670
5677
  <td class="numeric">${c.citationCount} / ${c.totalCount}</td>
5671
- <td>${escapeHtml(c.citedKeywords.slice(0, 5).join(", "))}${c.citedKeywords.length > 5 ? "\u2026" : ""}</td>
5678
+ <td class="numeric">${c.sharePct}%</td>
5679
+ <td>${escapeHtml(c.citedKeywords.slice(0, 5).join(", "))}${c.citedKeywords.length > 5 ? "\u2026" : ""}${pagesDisclosure}</td>
5672
5680
  </tr>`;
5673
5681
  }).join("");
5674
5682
  const table = competitors2.length > 0 ? `<table class="report-table">
5675
- <thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th>Cited keywords</th></tr></thead>
5683
+ <thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th class="numeric">SOV</th><th>Cited keywords</th></tr></thead>
5676
5684
  <tbody>${rows}</tbody>
5677
5685
  </table>` : renderEmpty("No competitors configured.");
5678
5686
  return section(
@@ -5804,6 +5812,19 @@ function renderGsc(report) {
5804
5812
  COLORS.accent,
5805
5813
  "Clicks over time"
5806
5814
  );
5815
+ const crossoverBlocks = [];
5816
+ if (gsc.trackedButNoGsc.length > 0) {
5817
+ crossoverBlocks.push(`<div class="chart-card"><h3>AEO keywords without search demand</h3>
5818
+ <p class="section-intro">Tracked AEO keywords with no GSC impressions in this window \u2014 review whether they represent real search demand.</p>
5819
+ <ul>${gsc.trackedButNoGsc.map((k) => `<li>${escapeHtml(k)}</li>`).join("")}</ul>
5820
+ </div>`);
5821
+ }
5822
+ if (gsc.gscButNotTracked.length > 0) {
5823
+ crossoverBlocks.push(`<div class="chart-card"><h3>Search queries you should track</h3>
5824
+ <p class="section-intro">GSC top queries (by impressions) that aren't tracked in your AEO project \u2014 candidates to add as keywords.</p>
5825
+ <ul>${gsc.gscButNotTracked.map((q) => `<li>${escapeHtml(q)}</li>`).join("")}</ul>
5826
+ </div>`);
5827
+ }
5807
5828
  return section(
5808
5829
  { id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: "Top queries, category breakdown, and traffic trend from Google Search Console." },
5809
5830
  `<div class="metric-grid">
@@ -5824,7 +5845,8 @@ function renderGsc(report) {
5824
5845
  <thead><tr><th>Category</th><th class="numeric">Clicks</th><th class="numeric">Imp.</th><th class="numeric">Share</th></tr></thead>
5825
5846
  <tbody>${breakdownRows}</tbody>
5826
5847
  </table>
5827
- </div>`
5848
+ </div>
5849
+ ${crossoverBlocks.join("\n")}`
5828
5850
  );
5829
5851
  }
5830
5852
  function renderGa(report) {
@@ -6004,6 +6026,12 @@ function renderCitationsTrend(report) {
6004
6026
  renderEmpty("Run multiple visibility sweeps to see a trend.")
6005
6027
  );
6006
6028
  }
6029
+ if (isTrendBaseline(trend)) {
6030
+ return section(
6031
+ { id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time" },
6032
+ renderEmpty(`Establishing baseline (${trend.length} of ${MIN_TREND_POINTS} runs collected). Trend will appear once more sweeps are recorded.`)
6033
+ );
6034
+ }
6007
6035
  const chart = renderLineChart(
6008
6036
  trend.map((t) => ({ x: t.date, y: t.citationRate, label: formatDate(t.date) })),
6009
6037
  COLORS.positive,
@@ -6035,15 +6063,17 @@ function renderInsights(report) {
6035
6063
  renderEmpty("No insights yet \u2014 run a visibility sweep to generate alerts.")
6036
6064
  );
6037
6065
  }
6038
- const rows = list.map((i) => {
6066
+ const haveDeduped = list.every((i) => typeof i.instanceCount === "number");
6067
+ const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count }) => {
6039
6068
  const tone = severityTone(i.severity);
6069
+ const countChip = count > 1 ? ` <span class="badge tone-neutral">\xD7 ${count}</span>` : "";
6040
6070
  return `<tr>
6041
- <td><span class="badge tone-${tone}">${escapeHtml(i.severity)}</span></td>
6042
- <td>${escapeHtml(i.title)}</td>
6043
- <td>${escapeHtml(i.keyword)}</td>
6044
- <td>${escapeHtml(i.provider)}</td>
6045
- <td>${i.recommendation ? escapeHtml(i.recommendation) : '<span class="cell-pending">\u2014</span>'}</td>
6046
- </tr>`;
6071
+ <td><span class="badge tone-${tone}">${escapeHtml(i.severity)}</span></td>
6072
+ <td>${escapeHtml(i.title)}${countChip}</td>
6073
+ <td>${escapeHtml(i.keyword)}</td>
6074
+ <td>${escapeHtml(i.provider)}</td>
6075
+ <td>${i.recommendation ? escapeHtml(i.recommendation) : '<span class="cell-pending">\u2014</span>'}</td>
6076
+ </tr>`;
6047
6077
  }).join("");
6048
6078
  return section(
6049
6079
  { id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Priority-ordered findings from the most recent runs." },
@@ -6053,11 +6083,40 @@ function renderInsights(report) {
6053
6083
  </table>`
6054
6084
  );
6055
6085
  }
6086
+ function renderOpportunities(report) {
6087
+ const opps = report.contentOpportunities;
6088
+ if (opps.length === 0) return "";
6089
+ const rows = opps.slice(0, 10).map((o) => {
6090
+ const ourPage = o.ourBestPage ? `<a href="${escapeHtml(o.ourBestPage.url)}">${escapeHtml(o.ourBestPage.url)}</a>` : '<span class="cell-not-cited">\u2014</span>';
6091
+ const winning = o.winningCompetitor ? `<a href="${escapeHtml(o.winningCompetitor.url)}">${escapeHtml(o.winningCompetitor.domain)}</a>` : '<span class="cell-not-cited">\u2014</span>';
6092
+ return `<tr>
6093
+ <td>${escapeHtml(o.query)}</td>
6094
+ <td><span class="badge tone-neutral">${escapeHtml(o.action)}</span></td>
6095
+ <td class="numeric">${Math.round(o.score)}</td>
6096
+ <td>${ourPage}</td>
6097
+ <td>${winning}</td>
6098
+ <td><span class="badge tone-neutral">${escapeHtml(o.demandSource)}</span></td>
6099
+ <td><span class="badge tone-neutral">${escapeHtml(o.actionConfidence)}</span></td>
6100
+ </tr>`;
6101
+ }).join("");
6102
+ return section(
6103
+ {
6104
+ id: "content-opportunities",
6105
+ eyebrow: "Section 12",
6106
+ title: "Content Opportunities",
6107
+ intro: "Ranked, action-typed targets from the content recommendation engine. Top 10 shown."
6108
+ },
6109
+ `<table class="report-table">
6110
+ <thead><tr><th>Query</th><th>Action</th><th class="numeric">Score</th><th>Our page</th><th>Winning competitor</th><th>Demand</th><th>Confidence</th></tr></thead>
6111
+ <tbody>${rows}</tbody>
6112
+ </table>`
6113
+ );
6114
+ }
6056
6115
  function renderRecommendedNextSteps(report) {
6057
6116
  const steps = report.recommendedNextSteps;
6058
6117
  if (steps.length === 0) {
6059
6118
  return section(
6060
- { id: "recommended-next-steps", eyebrow: "Section 12", title: "Recommended Next Steps" },
6119
+ { id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps" },
6061
6120
  renderEmpty("No outstanding actions.")
6062
6121
  );
6063
6122
  }
@@ -6068,7 +6127,7 @@ function renderRecommendedNextSteps(report) {
6068
6127
  <span class="rationale">${escapeHtml(s.rationale)}</span>
6069
6128
  </div>`).join("");
6070
6129
  return section(
6071
- { id: "recommended-next-steps", eyebrow: "Section 12", title: "Recommended Next Steps" },
6130
+ { id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps" },
6072
6131
  `<div class="steps">${items}</div>`
6073
6132
  );
6074
6133
  }
@@ -6089,6 +6148,7 @@ function renderReportHtml(report, opts = {}) {
6089
6148
  renderIndexingHealth(report),
6090
6149
  renderCitationsTrend(report),
6091
6150
  renderInsights(report),
6151
+ renderOpportunities(report),
6092
6152
  renderRecommendedNextSteps(report)
6093
6153
  ].join("\n");
6094
6154
  const json = escapeJsonForScript(JSON.stringify(report));
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-P6D3O5JB.js";
3
+ } from "./chunk-P3PS3ZSN.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-24C7RMIS.js";
7
- import "./chunk-ZCPZOVVE.js";
8
- import "./chunk-MLKGABMK.js";
6
+ } from "./chunk-5OYYYY4I.js";
7
+ import "./chunk-ZOJLW6WR.js";
8
+ import "./chunk-D4YFX3X4.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-ZCPZOVVE.js";
4
- import "./chunk-MLKGABMK.js";
3
+ } from "./chunk-ZOJLW6WR.js";
4
+ import "./chunk-D4YFX3X4.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  CliError,
3
3
  canonryMcpTools,
4
4
  createApiClient
5
- } from "./chunk-24C7RMIS.js";
6
- import "./chunk-MLKGABMK.js";
5
+ } from "./chunk-5OYYYY4I.js";
6
+ import "./chunk-D4YFX3X4.js";
7
7
 
8
8
  // src/mcp/cli.ts
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "3.3.9",
3
+ "version": "3.4.5",
4
4
  "type": "module",
5
5
  "description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -60,20 +60,20 @@
60
60
  "tsup": "^8.5.1",
61
61
  "tsx": "^4.19.0",
62
62
  "@ainyc/canonry-api-routes": "0.0.0",
63
+ "@ainyc/canonry-config": "0.0.0",
63
64
  "@ainyc/canonry-contracts": "0.0.0",
64
65
  "@ainyc/canonry-db": "0.0.0",
65
66
  "@ainyc/canonry-integration-bing": "0.0.0",
66
- "@ainyc/canonry-config": "0.0.0",
67
67
  "@ainyc/canonry-intelligence": "0.0.0",
68
+ "@ainyc/canonry-integration-google": "0.0.0",
68
69
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
69
70
  "@ainyc/canonry-integration-wordpress": "0.0.0",
70
- "@ainyc/canonry-provider-cdp": "0.0.0",
71
- "@ainyc/canonry-integration-google": "0.0.0",
72
71
  "@ainyc/canonry-provider-claude": "0.0.0",
73
- "@ainyc/canonry-provider-gemini": "0.0.0",
74
- "@ainyc/canonry-provider-local": "0.0.0",
72
+ "@ainyc/canonry-provider-cdp": "0.0.0",
75
73
  "@ainyc/canonry-provider-openai": "0.0.0",
76
- "@ainyc/canonry-provider-perplexity": "0.0.0"
74
+ "@ainyc/canonry-provider-gemini": "0.0.0",
75
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
76
+ "@ainyc/canonry-provider-local": "0.0.0"
77
77
  },
78
78
  "scripts": {
79
79
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",
@@ -1,9 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
6
-
7
- export {
8
- __export
9
- };