@houtini/seo-audit-console 0.3.0 → 0.4.6

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 (41) hide show
  1. package/README.md +8 -3
  2. package/dist/audit/recon.d.ts +124 -0
  3. package/dist/audit/recon.d.ts.map +1 -0
  4. package/dist/audit/recon.js +257 -0
  5. package/dist/audit/recon.js.map +1 -0
  6. package/dist/core/AuditDatabase.d.ts.map +1 -1
  7. package/dist/core/AuditDatabase.js +39 -0
  8. package/dist/core/AuditDatabase.js.map +1 -1
  9. package/dist/core/DataForSeoClient.d.ts +9 -2
  10. package/dist/core/DataForSeoClient.d.ts.map +1 -1
  11. package/dist/core/DataForSeoClient.js +11 -3
  12. package/dist/core/DataForSeoClient.js.map +1 -1
  13. package/dist/core/FirecrawlClient.d.ts +39 -0
  14. package/dist/core/FirecrawlClient.d.ts.map +1 -0
  15. package/dist/core/FirecrawlClient.js +86 -0
  16. package/dist/core/FirecrawlClient.js.map +1 -0
  17. package/dist/core/SupadataClient.d.ts +29 -0
  18. package/dist/core/SupadataClient.d.ts.map +1 -0
  19. package/dist/core/SupadataClient.js +63 -0
  20. package/dist/core/SupadataClient.js.map +1 -0
  21. package/dist/core/passageScore.d.ts +40 -9
  22. package/dist/core/passageScore.d.ts.map +1 -1
  23. package/dist/core/passageScore.js +29 -3
  24. package/dist/core/passageScore.js.map +1 -1
  25. package/dist/core/reconFetch.d.ts +29 -0
  26. package/dist/core/reconFetch.d.ts.map +1 -0
  27. package/dist/core/reconFetch.js +49 -0
  28. package/dist/core/reconFetch.js.map +1 -0
  29. package/dist/core/reconResearch.d.ts +50 -0
  30. package/dist/core/reconResearch.d.ts.map +1 -0
  31. package/dist/core/reconResearch.js +122 -0
  32. package/dist/core/reconResearch.js.map +1 -0
  33. package/dist/core/serpRecon.d.ts +52 -0
  34. package/dist/core/serpRecon.d.ts.map +1 -0
  35. package/dist/core/serpRecon.js +99 -0
  36. package/dist/core/serpRecon.js.map +1 -0
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +334 -6
  39. package/dist/server.js.map +1 -1
  40. package/package.json +1 -1
  41. package/server.json +18 -4
package/README.md CHANGED
@@ -123,6 +123,7 @@ Each of these is a real procedure I use, and each is one prompt. The expanded ve
123
123
  9. **Reporting.** *"Show me the dashboard"* in the chat, *"serve the dashboard"* for the live version in your browser, or *"export the report"* as one self-contained HTML file you can send a client. → [dashboard.md](manual/dashboard.md)
124
124
  10. **The content opportunity report.** If you write for a living, this is your page one: *"Content opportunities for mysite.com"* returns what to WRITE next (demand you already earn impressions for with no winning page), what to REFRESH (pages bleeding clicks), which SNIPPETS to rewrite (ranking well, under-clicked), and which clusters to STRENGTHEN (position 4-20, one push from the money). Every row traces to your own Search Console data - no invented keyword ideas - and *"draft the brief for row 1"* turns any of them into a grounded outline. → [competitive.md](manual/competitive.md)
125
125
  11. **Market Sizing and Prioritisation.** *"Size the market: mysite.com vs rival1.com and rival2.com"* - the keyword universe, total monthly demand, and share of voice per topic cluster, from one cached call per domain. The engagement-opening read that used to need a Semrush subscription. → [competitive.md](manual/competitive.md)
126
+ 12. **Content recon - why is this page losing?** *"Run content recon on mysite.com"* takes your worst declining pages, pulls the live Google SERP for each, and reads whether the **AI Overview** cites you. The verdict is the honest bit: *rank but not cited* is a data-accuracy or freshness problem, not a rewrite. It transcribes the ranking videos (Supadata), reads the reachable competitors (Firecrawl, plus a free browser-profile fetch that even gets Reddit), and writes the gaps into a tracked to-do board you can annotate and re-measure. → [competitive.md](manual/competitive.md)
126
127
 
127
128
  ![Ranking distribution over time - impressions by position bucket](assets/search-performance.png)
128
129
 
@@ -132,7 +133,9 @@ Each of these is a real procedure I use, and each is one prompt. The expanded ve
132
133
 
133
134
  Everything above works with just your Search Console data. But GSC can only describe searches where you already appear. The moment your question is "how big is this market?" or "what do competitors rank for that I don't?", you need third-party data - and that's [DataForSEO](https://dataforseo.com/?aff=213701): a pay-as-you-go API for volumes, live rankings, competitor data and Lighthouse runs. No subscription; calls cost fractions of a cent to a few cents, cached for 20 days, and only ever run when you ask. My own usage runs to a few dollars a month.
134
135
 
135
- It unlocks the Semrush-replacement layer: the organic visibility overview for any domain, any site's top pages and ranked keywords (including which keywords cite a site in **AI Overviews**), the content gap, topic gaps, search intent, lab Core Web Vitals and backlinks. The full workflows and setup: [competitive.md](manual/competitive.md).
136
+ It unlocks the Semrush-replacement layer: the organic visibility overview for any domain, any site's top pages and ranked keywords (including which keywords cite a site in **AI Overviews**), the content gap, topic gaps, search intent, lab Core Web Vitals and backlinks. It also drives **content recon** - `recon_targets` pulls the live Google SERP for a losing page, reads whether the AI Overview cites you, and tells you *why* you're behind. The full workflows and setup: [competitive.md](manual/competitive.md).
137
+
138
+ **Two more optional keys, for content recon only.** When you ask recon to fetch the competitors it finds, it routes each by source: videos to [Supadata](https://supadata.ai) (`SUPADATA_API_KEY`) for transcripts, and competitor pages to [Firecrawl](https://firecrawl.link/2d1PLD8) (`FIRECRAWL_API_KEY`) for clean markdown, with a free browser-profile fetch behind it. Both are pay-as-you-go and entirely optional - recon still classifies the page and lists the competitors without them. Grab a Firecrawl key here: **https://firecrawl.link/2d1PLD8**.
136
139
 
137
140
  ---
138
141
 
@@ -169,7 +172,10 @@ The one-line version - full descriptions, inputs and example prompts for every t
169
172
  | `competitors_domain` · `page_intersection` · `topic_gaps` | Competitors · content gap · topic gaps |
170
173
  | `domain_visibility` · `top_pages` · `ranked_keywords` | The Semrush-style views, any domain (+ `aioOnly` for AI Overview citations) |
171
174
  | `page_lighthouse` · `pull_backlinks` | Lab CWV · backlink profile with live status |
172
- | `get_dashboard` · `export_report` | In-chat dashboard · shareable HTML |
175
+ | `market_sizing` · `serp_features` | Share of voice vs named competitors · AI-Overview / snippet / video exposure by volume |
176
+ | `keyword_list` · `content_opportunities` | Cluster a keyword list (own / weak / absent) · the content marketer's report |
177
+ | `recon_targets` · `save_recon_todo` · `recon_todos` | **Content recon:** why a page is losing (live SERP + AI Overview) → a tracked, annotatable to-do board |
178
+ | `get_dashboard` · `serve_dashboard` · `export_report` | In-chat dashboard · the same in a local browser tab · shareable HTML |
173
179
  | `composition_cookbook` | The data-surface map and recipes for bespoke analyses |
174
180
  | `data_storage` | Per-property disk usage and row counts, with confirm-gated pruning |
175
181
  | `normalize_url` · `data_location` · `list_properties` · `seo_audit_help` | Utilities and the help menu |
@@ -194,7 +200,6 @@ Your Search Console data and the crawl live in local SQLite files under `SAC_DAT
194
200
 
195
201
  A few things I'm building next, in rough order:
196
202
 
197
- - **List mode.** Paste any list of URLs - a migration map, old ranking pages, a PPC export - and have it status-checked and audited. The migration-verification workflow, basically.
198
203
  - **Structured-data opportunities, by template.** Not "you have no schema" (most modern stores have plenty), but "this template could earn review stars or an FAQ rich result and doesn't."
199
204
  - **A per-page content scorecard.** A dedicated table of *every* ranking phrase your copy misses, not just the top query.
200
205
  - **More agent readiness.** A WebMCP advisory (which tool actions your site could expose to agents) and the agent-commerce protocols.
@@ -0,0 +1,124 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { OwnPageView } from '../core/reconFetch.js';
3
+ import type { SerpReconResult, ReconVerdict } from '../core/serpRecon.js';
4
+ /**
5
+ * Content recon core: pick the pages worth a deep competitive look, and turn what we can
6
+ * determine WITHOUT judgement (schema gaps, freshness, cannibalisation, SERP format) into
7
+ * a trackable to-do ledger. The judgement to-dos (what competitors cover that we don't)
8
+ * are written back by the research session via save_recon_todo.
9
+ */
10
+ export interface ReconTarget {
11
+ urlKey: string;
12
+ query: string;
13
+ impressions: number;
14
+ clicks: number;
15
+ position: number;
16
+ priorPosition: number | null;
17
+ slipped: boolean;
18
+ competingUrls: number;
19
+ }
20
+ /** Pick declining / striking-distance pages: high impressions, currently pos 3-15, top query per page. */
21
+ export declare function selectReconTargets(db: Database.Database, opts: {
22
+ limit?: number;
23
+ minImpressions?: number;
24
+ maxDate: string;
25
+ }): ReconTarget[];
26
+ /** What the CRAWL says about this URL — the reality check the SERP + GSC read has to survive.
27
+ * GSC history is legacy: it keeps reporting impressions for a URL that has since been
28
+ * canonicalised away or set noindex, and classifying off that alone produced a
29
+ * "defend-and-deepen, produce a companion video" verdict for a non-indexable duplicate. */
30
+ export interface PageState {
31
+ urlKey: string;
32
+ crawled: boolean;
33
+ indexable: boolean | null;
34
+ indexableReason: string | null;
35
+ canonicalKey: string | null;
36
+ canonicalisedAway: boolean;
37
+ statusCode: number | null;
38
+ inlinkCount: number | null;
39
+ ipr: number | null;
40
+ cannotRank: boolean;
41
+ }
42
+ export declare function pageState(db: Database.Database, urlKey: string): PageState;
43
+ /** A crawl row that contradicts the SERP/GSC read WINS. Returns null when there's no conflict. */
44
+ export declare function crawlRealityOverride(v: {
45
+ verdict: ReconVerdict;
46
+ note: string;
47
+ }, state: PageState): {
48
+ verdict: ReconVerdict;
49
+ note: string;
50
+ } | null;
51
+ export interface DraftTodo {
52
+ action: string;
53
+ type: string;
54
+ rationale: string;
55
+ evidence: Record<string, unknown>;
56
+ priority: number;
57
+ }
58
+ /**
59
+ * Priority = OPPORTUNITY, not impressions. Raw impressions ranked a #4 defend-and-deepen page
60
+ * above the genuinely broken one, i.e. roughly the wrong working order. This prices the clicks
61
+ * actually available: impressions x (CTR at a realistic target position - CTR where we are now),
62
+ * damped by the verdict's headroom, then weighted by to-do type.
63
+ */
64
+ export interface PriorityBasis {
65
+ model: string;
66
+ impressions: number;
67
+ position: number;
68
+ positionSource: 'live-serp' | 'gsc-average';
69
+ targetPosition: number;
70
+ opportunityClicks: number;
71
+ verdictFactor: number;
72
+ }
73
+ export declare function opportunityBasis(impressions: number, organicRank: number | null, gscPosition: number, verdict: string): PriorityBasis;
74
+ /** The entity-ish terms of a query: the tokens a sibling page would have to share to be competing.
75
+ * Minimum length 2 so short but load-bearing terms ("vr", "ai") survive. */
76
+ export declare function queryCore(query: string): string[];
77
+ export interface ClusterCannibalisation {
78
+ core: string[];
79
+ clusterQueries: number;
80
+ collidingQueries: number;
81
+ urls: {
82
+ urlKey: string;
83
+ impressions: number;
84
+ position: number;
85
+ queries: number;
86
+ }[];
87
+ cannibalising: boolean;
88
+ }
89
+ /**
90
+ * Competition is judged PER QUERY and then unioned across the cluster. Judging it on cluster
91
+ * TOTALS is what hid the overlap: the leader accumulates across every query in the topic, so a
92
+ * genuine rival page falls under the 10%-of-leader guard and disappears. Here a URL qualifies if
93
+ * it competes on any single cluster query (>=10 impressions, >=10% of that query's leader, avg
94
+ * position inside 20) — and we separately count the queries where two or more of them collide,
95
+ * which is what distinguishes cannibalisation from ordinary topical coverage.
96
+ */
97
+ export declare function clusterCannibalisation(db: Database.Database, query: string, maxDate: string, minImpressions?: number): ClusterCannibalisation;
98
+ /** The to-dos we can generate deterministically from our page + the SERP (no judgement). */
99
+ export declare function deterministicTodos(target: ReconTarget, own: OwnPageView, serp: SerpReconResult, verdict: {
100
+ verdict: ReconVerdict;
101
+ note: string;
102
+ }, todayIso: string, ctx?: {
103
+ state?: PageState;
104
+ cluster?: ClusterCannibalisation;
105
+ }): DraftTodo[];
106
+ export declare function persistReconPage(db: Database.Database, p: {
107
+ urlKey: string;
108
+ query: string;
109
+ verdict: string;
110
+ verdictNote: string;
111
+ organicRank: number | null;
112
+ gscPosition: number;
113
+ gscImpressions: number;
114
+ aioPresent: boolean;
115
+ aioCitesUs: boolean | null;
116
+ videoPresent: boolean;
117
+ hasProductSchema: boolean;
118
+ schemaTypes: string[];
119
+ competitors: unknown;
120
+ }): void;
121
+ /** Insert to-dos for a page. `baseline` snapshots the rank/citation state so outcome is measurable.
122
+ * De-dupes on (url_key, action) so a re-run doesn't pile up identical open items. */
123
+ export declare function insertTodos(db: Database.Database, urlKey: string, query: string, todos: DraftTodo[], baseline: Record<string, unknown>, source: 'auto' | 'research'): number;
124
+ //# sourceMappingURL=recon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recon.d.ts","sourceRoot":"","sources":["../../src/audit/recon.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,257 @@
1
+ import { expectedCtr } from '../core/ctrModel.js';
2
+ /** Pick declining / striking-distance pages: high impressions, currently pos 3-15, top query per page. */
3
+ export function selectReconTargets(db, opts) {
4
+ const { limit = 5, minImpressions = 300, maxDate } = opts;
5
+ const rows = db.prepare(`
6
+ WITH cur AS (
7
+ SELECT page_key, query, SUM(impressions) imp, SUM(clicks) clk,
8
+ SUM(position*impressions)*1.0/NULLIF(SUM(impressions),0) pos
9
+ FROM search_analytics
10
+ WHERE page_key IS NOT NULL AND query IS NOT NULL
11
+ AND date > date(?, '-28 days') AND date <= ?
12
+ GROUP BY page_key, query
13
+ ),
14
+ ranked AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY page_key ORDER BY imp DESC) rn FROM cur),
15
+ prior AS (
16
+ SELECT page_key, query, SUM(position*impressions)*1.0/NULLIF(SUM(impressions),0) pos
17
+ FROM search_analytics
18
+ WHERE page_key IS NOT NULL AND query IS NOT NULL
19
+ AND date > date(?, '-56 days') AND date <= date(?, '-28 days')
20
+ GROUP BY page_key, query
21
+ )
22
+ SELECT r.page_key, r.query, r.imp, r.clk, r.pos, p.pos AS prior_pos
23
+ FROM ranked r
24
+ LEFT JOIN prior p ON p.page_key = r.page_key AND p.query = r.query
25
+ WHERE r.rn = 1 AND r.pos BETWEEN 3 AND 15 AND r.imp >= ?
26
+ ORDER BY r.imp DESC
27
+ LIMIT ?`).all(maxDate, maxDate, maxDate, maxDate, minImpressions, limit);
28
+ const cannStmt = db.prepare(`
29
+ SELECT COUNT(*) n FROM (
30
+ SELECT page_key FROM search_analytics
31
+ WHERE query = ? AND page_key IS NOT NULL AND date > date(?, '-28 days') AND date <= ?
32
+ GROUP BY page_key HAVING SUM(impressions) >= 10
33
+ )`);
34
+ return rows.map(r => {
35
+ const pos = Math.round(r.pos * 10) / 10;
36
+ const priorPosition = r.prior_pos != null ? Math.round(r.prior_pos * 10) / 10 : null;
37
+ const cann = cannStmt.get(r.query, maxDate, maxDate);
38
+ return {
39
+ urlKey: r.page_key, query: r.query, impressions: r.imp, clicks: r.clk, position: pos,
40
+ priorPosition,
41
+ slipped: priorPosition != null && pos > priorPosition + 0.5,
42
+ competingUrls: cann.n,
43
+ };
44
+ });
45
+ }
46
+ export function pageState(db, urlKey) {
47
+ const row = db.prepare(`SELECT status_code, indexable, indexable_reason, canonical_key, inlink_count, ipr FROM pages WHERE url_key = ?`).get(urlKey);
48
+ if (!row) {
49
+ return { urlKey, crawled: false, indexable: null, indexableReason: null, canonicalKey: null, canonicalisedAway: false, statusCode: null, inlinkCount: null, ipr: null, cannotRank: false };
50
+ }
51
+ const canonicalisedAway = !!row.canonical_key && row.canonical_key !== urlKey;
52
+ const indexable = row.indexable == null ? null : !!row.indexable;
53
+ return {
54
+ urlKey, crawled: true, indexable, indexableReason: row.indexable_reason,
55
+ canonicalKey: row.canonical_key, canonicalisedAway,
56
+ statusCode: row.status_code, inlinkCount: row.inlink_count, ipr: row.ipr,
57
+ cannotRank: indexable === false || canonicalisedAway,
58
+ };
59
+ }
60
+ /** A crawl row that contradicts the SERP/GSC read WINS. Returns null when there's no conflict. */
61
+ export function crawlRealityOverride(v, state) {
62
+ if (!state.cannotRank)
63
+ return null;
64
+ const why = state.indexable === false
65
+ ? `the crawl marks it not indexable (${state.indexableReason ?? 'reason not recorded'})`
66
+ : `it declares a canonical pointing at ${state.canonicalKey}`;
67
+ const equity = `${state.inlinkCount ?? 0} internal link(s), iPR ${state.ipr ?? 0}`;
68
+ return {
69
+ verdict: 'page-cannot-rank',
70
+ note: `This URL cannot rank as it stands: ${why} (${equity}). Its Search Console history is legacy — the SERP read ("${v.verdict}") describes whoever Google actually ranks, not this URL. Resolve the duplicate first; content work here is wasted.`,
71
+ };
72
+ }
73
+ const TYPE_WEIGHT = {
74
+ 'content-gap': 1.0, schema: 0.9, research: 0.8, cannibalisation: 0.7, format: 0.6, freshness: 0.3,
75
+ };
76
+ /** How much headroom the verdict implies. A page already sitting at #4 with nobody editorial
77
+ * above it has far less to gain than one losing a competitive gap, even at higher impressions. */
78
+ const VERDICT_FACTOR = {
79
+ 'competitive-gap': 1.0, 'accuracy-or-freshness': 1.0, 'consolidate-weak-page': 0.9,
80
+ 'page-cannot-rank': 0.8, 'defend-and-deepen': 0.4,
81
+ };
82
+ const looksCommercial = (q) => /\b(best|top|vs|review|compare|comparison|cheap|budget|buy)\b/i.test(q);
83
+ export function opportunityBasis(impressions, organicRank, gscPosition, verdict) {
84
+ const position = Math.max(1, organicRank ?? gscPosition);
85
+ const targetPosition = position <= 3 ? 1 : position <= 10 ? 3 : 8;
86
+ const opportunityClicks = Math.max(0, impressions * (expectedCtr(targetPosition) - expectedCtr(position)));
87
+ return {
88
+ model: 'impressions x (expectedCtr(target) - expectedCtr(current)) x verdictFactor x typeWeight',
89
+ impressions, position: Math.round(position * 10) / 10,
90
+ positionSource: organicRank != null ? 'live-serp' : 'gsc-average',
91
+ targetPosition,
92
+ opportunityClicks: Math.round(opportunityClicks),
93
+ verdictFactor: VERDICT_FACTOR[verdict] ?? 1,
94
+ };
95
+ }
96
+ /** Cannibalisation at CLUSTER level, not one query at a time. Keying off a single query missed a
97
+ * four-URL overlap that only showed up across the whole "<entity>" query family. */
98
+ const CLUSTER_STOP = new Set(['the', 'and', 'for', 'with', 'best', 'top', 'vs', 'versus', 'review', 'reviews', 'guide', 'how', 'what', 'which', 'why', 'you', 'your', 'cheap', 'budget', 'buy', 'compare', 'comparison', '2024', '2025', '2026', '2027']);
99
+ /** The entity-ish terms of a query: the tokens a sibling page would have to share to be competing.
100
+ * Minimum length 2 so short but load-bearing terms ("vr", "ai") survive. */
101
+ export function queryCore(query) {
102
+ return [...new Set(query.toLowerCase().split(/[^a-z0-9]+/).filter(t => t.length >= 2 && !CLUSTER_STOP.has(t)))].slice(0, 4);
103
+ }
104
+ /**
105
+ * Competition is judged PER QUERY and then unioned across the cluster. Judging it on cluster
106
+ * TOTALS is what hid the overlap: the leader accumulates across every query in the topic, so a
107
+ * genuine rival page falls under the 10%-of-leader guard and disappears. Here a URL qualifies if
108
+ * it competes on any single cluster query (>=10 impressions, >=10% of that query's leader, avg
109
+ * position inside 20) — and we separately count the queries where two or more of them collide,
110
+ * which is what distinguishes cannibalisation from ordinary topical coverage.
111
+ */
112
+ export function clusterCannibalisation(db, query, maxDate, minImpressions = 10) {
113
+ const core = queryCore(query);
114
+ const none = { core, clusterQueries: 0, collidingQueries: 0, urls: [], cannibalising: false };
115
+ if (!core.length)
116
+ return none;
117
+ const like = core.map(() => 'query LIKE ?').join(' AND ');
118
+ const terms = core.map(t => `%${t}%`);
119
+ const win = `date > date(?, '-28 days') AND date <= ?`;
120
+ const cte = `WITH q AS (
121
+ SELECT query, page_key, SUM(impressions) impr,
122
+ SUM(position*impressions)*1.0/NULLIF(SUM(impressions),0) pos
123
+ FROM search_analytics
124
+ WHERE page_key IS NOT NULL AND query IS NOT NULL AND ${win} AND ${like}
125
+ GROUP BY query, page_key
126
+ ),
127
+ lead AS (SELECT query, MAX(impr) top FROM q GROUP BY query),
128
+ comp AS (
129
+ SELECT q.* FROM q JOIN lead l ON l.query = q.query
130
+ WHERE q.impr >= ? AND q.impr >= l.top * 0.1 AND q.pos < 20
131
+ )`;
132
+ const args = [maxDate, maxDate, ...terms, minImpressions];
133
+ const urls = db.prepare(`${cte}
134
+ SELECT page_key urlKey, SUM(impr) impressions,
135
+ SUM(pos*impr)*1.0/NULLIF(SUM(impr),0) position,
136
+ COUNT(DISTINCT query) queries
137
+ FROM comp GROUP BY page_key ORDER BY impressions DESC`).all(...args);
138
+ const { colliding } = db.prepare(`${cte} SELECT COUNT(*) colliding FROM (SELECT query FROM comp GROUP BY query HAVING COUNT(DISTINCT page_key) >= 2)`).get(...args);
139
+ const { n } = db.prepare(`SELECT COUNT(DISTINCT query) n FROM search_analytics WHERE query IS NOT NULL AND ${win} AND ${like}`).get(maxDate, maxDate, ...terms);
140
+ return {
141
+ core, clusterQueries: n, collidingQueries: colliding,
142
+ urls: urls.map(r => ({ ...r, position: Math.round(r.position * 10) / 10 })),
143
+ cannibalising: urls.length >= 2 && colliding >= 1,
144
+ };
145
+ }
146
+ /** The to-dos we can generate deterministically from our page + the SERP (no judgement). */
147
+ export function deterministicTodos(target, own, serp, verdict, todayIso, ctx = {}) {
148
+ const out = [];
149
+ const basis = opportunityBasis(target.impressions, serp.ourOrganicRank, target.position, verdict.verdict);
150
+ const w = (type) => Math.max(1, Math.round(basis.opportunityClicks * basis.verdictFactor * (TYPE_WEIGHT[type] ?? 0.5)));
151
+ // A URL that cannot rank gets ONE to-do: resolve it. Everything downstream (schema, freshness,
152
+ // a companion video) is work against a page Google will not serve.
153
+ const state = ctx.state;
154
+ if (state?.cannotRank) {
155
+ return [{
156
+ action: state.canonicalisedAway
157
+ ? `Resolve the duplicate — this URL canonicalises to ${state.canonicalKey}; merge or de-duplicate before any content work`
158
+ : `Restore indexability — the crawl marks this URL not indexable (${state.indexableReason ?? 'reason not recorded'})`,
159
+ type: 'cannibalisation',
160
+ rationale: `${verdict.note} Its GSC impressions are historical, so they are not evidence the page can win today.`,
161
+ evidence: {
162
+ indexable: state.indexable, indexableReason: state.indexableReason, canonicalKey: state.canonicalKey,
163
+ statusCode: state.statusCode, inlinkCount: state.inlinkCount, ipr: state.ipr, source: 'crawl (pages table)',
164
+ },
165
+ priority: w('cannibalisation'),
166
+ }];
167
+ }
168
+ // Verdict-driven headline action.
169
+ if (verdict.verdict === 'accuracy-or-freshness') {
170
+ out.push({ action: 'Update the facts to current and mark them up so they are machine-liftable', type: 'schema',
171
+ rationale: `You rank organic #${serp.ourOrganicRank} but the AI Overview does not cite you — a data-accuracy/freshness signal.`,
172
+ evidence: { organicRank: serp.ourOrganicRank, aioCitesUs: false, aioReferencesChecked: serp.aioReferences.length }, priority: w('schema') + 1 });
173
+ }
174
+ // Schema: commercial/listicle page without Product/Review markup.
175
+ if (!own.hasProductOrReview && looksCommercial(target.query)) {
176
+ out.push({ action: 'Add Product + Review/AggregateRating schema per item, with current specs and price',
177
+ type: 'schema', rationale: 'A comparison/review page with no item-level markup cannot be lifted into rich results or an AI Overview citation cleanly.',
178
+ evidence: { schemaTypes: own.jsonLdTypes, hasProductOrReview: false }, priority: w('schema') });
179
+ }
180
+ // Freshness: stale dateModified (older than ~90 days from today).
181
+ if (own.dateModified) {
182
+ const ageDays = Math.round((Date.parse(todayIso) - Date.parse(own.dateModified)) / 86400000);
183
+ if (ageDays > 90) {
184
+ out.push({ action: `Refresh the content (dateModified is ${ageDays} days old) — genuinely update, don't just restamp`,
185
+ type: 'freshness', rationale: 'Stale content loses ground on freshness-sensitive queries; a real update (not a date bump) is the lever.',
186
+ evidence: { dateModified: own.dateModified, ageDays }, priority: w('freshness') });
187
+ }
188
+ }
189
+ // Cannibalisation — cluster first (the real overlap), falling back to the single query.
190
+ const cluster = ctx.cluster;
191
+ if (cluster?.cannibalising) {
192
+ out.push({ action: `Consolidate cannibalisation — ${cluster.urls.length} of your URLs compete across the "${cluster.core.join(' ')}" cluster, colliding on ${cluster.collidingQueries} quer${cluster.collidingQueries === 1 ? 'y' : 'ies'}`,
193
+ type: 'cannibalisation',
194
+ rationale: 'Multiple own URLs competing across one topic cluster split signals; pick a canonical winner and point the others at it. Judged across the cluster, not one query at a time — a single-query count misses siblings that share the topic under different phrasings.',
195
+ evidence: { core: cluster.core, clusterQueries: cluster.clusterQueries, collidingQueries: cluster.collidingQueries, urls: cluster.urls, seedQuery: target.query },
196
+ priority: w('cannibalisation') });
197
+ }
198
+ else if (target.competingUrls >= 2) {
199
+ out.push({ action: `Consolidate cannibalisation — ${target.competingUrls} of your URLs rank for "${target.query}"`,
200
+ type: 'cannibalisation', rationale: 'Multiple own URLs competing for one query split signals; pick a canonical winner.',
201
+ evidence: { competingUrls: target.competingUrls, query: target.query }, priority: w('cannibalisation') });
202
+ }
203
+ // Format: AIO/SERP leans on video.
204
+ const ytRefs = serp.aioReferences.filter(r => /youtube|youtu\.be/.test(r.domain)).length;
205
+ if ((serp.videoPresent || ytRefs >= 2)) {
206
+ out.push({ action: 'Produce a companion video',
207
+ type: 'format', rationale: `The SERP favours video${ytRefs ? ` and ${ytRefs} of the AI Overview's references are YouTube` : ''}.`,
208
+ evidence: { videoPresent: serp.videoPresent, youtubeAioRefs: ytRefs }, priority: w('format') });
209
+ }
210
+ // Research pointer: hand the session the competitor set to diff (coverage + originality).
211
+ const competitors = [
212
+ ...serp.organicAbove.map(o => ({ rank: o.rank, url: o.url, kind: 'organic-above' })),
213
+ ...serp.aioReferences.slice(0, 6).map(r => ({ rank: r.rank, url: r.url, kind: 'aio-reference' })),
214
+ ...serp.videoItems.slice(0, 3).map(v => ({ url: v.url, kind: 'video' })),
215
+ ];
216
+ out.push({ action: 'Diff our coverage against the competitors Google rewards — add table-stakes we lack, amplify what only we have',
217
+ type: 'research', rationale: 'Scrape the ranking competitors and transcribe the ranking videos, then compare to our sections.',
218
+ evidence: { competitors }, priority: w('research') });
219
+ return out;
220
+ }
221
+ export function persistReconPage(db, p) {
222
+ db.prepare(`INSERT INTO recon_page
223
+ (url_key, query, verdict, verdict_note, organic_rank, gsc_position, gsc_impressions,
224
+ aio_present, aio_cites_us, video_present, has_product_schema, schema_types, competitors, researched_at)
225
+ VALUES (@urlKey,@query,@verdict,@verdictNote,@organicRank,@gscPosition,@gscImpressions,
226
+ @aioPresent,@aioCitesUs,@videoPresent,@hasProductSchema,@schemaTypes,@competitors, datetime('now'))
227
+ ON CONFLICT(url_key) DO UPDATE SET query=excluded.query, verdict=excluded.verdict, verdict_note=excluded.verdict_note,
228
+ organic_rank=excluded.organic_rank, gsc_position=excluded.gsc_position, gsc_impressions=excluded.gsc_impressions,
229
+ aio_present=excluded.aio_present, aio_cites_us=excluded.aio_cites_us, video_present=excluded.video_present,
230
+ has_product_schema=excluded.has_product_schema, schema_types=excluded.schema_types,
231
+ competitors=excluded.competitors, researched_at=datetime('now')`).run({
232
+ urlKey: p.urlKey, query: p.query, verdict: p.verdict, verdictNote: p.verdictNote,
233
+ organicRank: p.organicRank, gscPosition: p.gscPosition, gscImpressions: p.gscImpressions,
234
+ aioPresent: p.aioPresent ? 1 : 0, aioCitesUs: p.aioCitesUs == null ? null : (p.aioCitesUs ? 1 : 0), videoPresent: p.videoPresent ? 1 : 0,
235
+ hasProductSchema: p.hasProductSchema ? 1 : 0, schemaTypes: JSON.stringify(p.schemaTypes),
236
+ competitors: JSON.stringify(p.competitors),
237
+ });
238
+ }
239
+ /** Insert to-dos for a page. `baseline` snapshots the rank/citation state so outcome is measurable.
240
+ * De-dupes on (url_key, action) so a re-run doesn't pile up identical open items. */
241
+ export function insertTodos(db, urlKey, query, todos, baseline, source) {
242
+ const exists = db.prepare(`SELECT 1 FROM recon_todo WHERE url_key=? AND action=? AND status NOT IN ('shipped','dismissed')`);
243
+ const ins = db.prepare(`INSERT INTO recon_todo (url_key, query, action, type, rationale, evidence, priority, source, baseline)
244
+ VALUES (?,?,?,?,?,?,?,?,?)`);
245
+ let n = 0;
246
+ const tx = db.transaction(() => {
247
+ for (const t of todos) {
248
+ if (exists.get(urlKey, t.action))
249
+ continue;
250
+ ins.run(urlKey, query, t.action, t.type, t.rationale, JSON.stringify(t.evidence), t.priority, source, JSON.stringify(baseline));
251
+ n++;
252
+ }
253
+ });
254
+ tx();
255
+ return n;
256
+ }
257
+ //# sourceMappingURL=recon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recon.js","sourceRoot":"","sources":["../../src/audit/recon.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"AuditDatabase.d.ts","sourceRoot":"","sources":["../../src/core/AuditDatabase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAItC,MAAM,MAAM,YAAY,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAEhE,qBAAa,aAAa;IACxB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;gBAEnB,MAAM,EAAE,MAAM;IAgB1B,OAAO,CAAC,gBAAgB;IAgYxB,2EAA2E;IAC3E,OAAO,CAAC,OAAO;IAyDf,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI;IAWjF,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS;IAMhH,2FAA2F;IAC3F,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMhE,iFAAiF;IACjF,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS;IAO5D,KAAK,IAAI,IAAI;CAGd"}
1
+ {"version":3,"file":"AuditDatabase.d.ts","sourceRoot":"","sources":["../../src/core/AuditDatabase.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AAItC,MAAM,MAAM,YAAY,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;AACvD,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAEhE,qBAAa,aAAa;IACxB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,CAAC;gBAEnB,MAAM,EAAE,MAAM;IAgB1B,OAAO,CAAC,gBAAgB;IAwaxB,2EAA2E;IAC3E,OAAO,CAAC,OAAO;IAyDf,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI;IAWjF,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,SAAS;IAMhH,2FAA2F;IAC3F,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMhE,iFAAiF;IACjF,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS;IAO5D,KAAK,IAAI,IAAI;CAGd"}
@@ -392,6 +392,45 @@ export class AuditDatabase {
392
392
  PRIMARY KEY (qid, related_qid, relation)
393
393
  );
394
394
  CREATE INDEX IF NOT EXISTS idx_entity_edge_related ON entity_edge(related_qid);
395
+ `);
396
+ // ── Content recon (recon_targets) — per-page SERP/AIO classification + a trackable
397
+ // to-do ledger with baseline→outcome so a fix's effect on rank/citation is measurable.
398
+ this.db.exec(`
399
+ CREATE TABLE IF NOT EXISTS recon_page (
400
+ url_key TEXT PRIMARY KEY, -- OUR page under recon
401
+ query TEXT, -- the primary money query analysed
402
+ verdict TEXT, -- defend-and-deepen | accuracy-or-freshness | consolidate-weak-page | competitive-gap
403
+ verdict_note TEXT,
404
+ organic_rank INTEGER, -- our live organic rank (DataForSEO), distinct from GSC avg
405
+ gsc_position REAL, -- GSC blended average position (for contrast)
406
+ gsc_impressions INTEGER,
407
+ aio_present INTEGER DEFAULT 0,
408
+ aio_cites_us INTEGER DEFAULT 0,
409
+ video_present INTEGER DEFAULT 0,
410
+ has_product_schema INTEGER DEFAULT 0,
411
+ schema_types TEXT, -- JSON string[]
412
+ competitors TEXT, -- JSON { organicAbove[], aioReferences[], videoItems[] }
413
+ researched_at TEXT DEFAULT (datetime('now'))
414
+ );
415
+ CREATE TABLE IF NOT EXISTS recon_todo (
416
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
417
+ url_key TEXT NOT NULL,
418
+ query TEXT,
419
+ action TEXT NOT NULL, -- the to-do ("Add a Meta Quest 3S section")
420
+ type TEXT, -- content-gap | schema | freshness | format | cannibalisation | originality
421
+ rationale TEXT,
422
+ evidence TEXT, -- JSON datapoint backing the action
423
+ priority REAL DEFAULT 0,
424
+ status TEXT DEFAULT 'open', -- open | researching | drafted | shipped | dismissed
425
+ source TEXT DEFAULT 'auto', -- auto (deterministic) | research (session-discovered)
426
+ baseline TEXT, -- JSON snapshot at creation (rank/citation/position)
427
+ outcome TEXT, -- JSON snapshot after 'shipped' (re-measured)
428
+ notes TEXT, -- append-only annotation log (timestamped lines)
429
+ created_at TEXT DEFAULT (datetime('now')),
430
+ updated_at TEXT DEFAULT (datetime('now'))
431
+ );
432
+ CREATE INDEX IF NOT EXISTS idx_recon_todo_url ON recon_todo(url_key);
433
+ CREATE INDEX IF NOT EXISTS idx_recon_todo_status ON recon_todo(status);
395
434
  `);
396
435
  this.migrate();
397
436
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AuditDatabase.js","sourceRoot":"","sources":["../../src/core/AuditDatabase.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AuditDatabase.js","sourceRoot":"","sources":["../../src/core/AuditDatabase.ts"],"names":[],"mappings":"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"}
@@ -30,8 +30,15 @@ export declare class DataForSeoClient {
30
30
  private loc;
31
31
  /** KEYWORDS_DATA — true monthly search volume + CPC + competition (cheap). */
32
32
  searchVolume(keywords: string[], location?: string | number, languageCode?: string): Promise<DfsResponse>;
33
- /** SERP — live Google organic results (per-request cost; use sparingly). */
34
- serpOrganic(keyword: string, location?: string | number, languageCode?: string, depth?: number): Promise<DfsResponse>;
33
+ /** SERP — live Google organic results (per-request cost; use sparingly).
34
+ * `depth` = how many ORGANIC results to collect (not total SERP elements), so a domain absent
35
+ * at depth=20 is absent from the top 20 organic — nothing is known about its position beyond that.
36
+ * `loadAsyncAiOverview` makes DataForSEO wait for a client-side-loaded AI Overview and return its
37
+ * references; without it those SERPs come back with an empty AIO (asynchronous_ai_overview:true)
38
+ * and citation is unknowable. Small surcharge, refunded when no async AIO is found. */
39
+ serpOrganic(keyword: string, location?: string | number, languageCode?: string, depth?: number, opts?: {
40
+ loadAsyncAiOverview?: boolean;
41
+ }): Promise<DfsResponse>;
35
42
  /**
36
43
  * Related terms for a keyword — People Also Ask questions + related searches,
37
44
  * parsed from the SERP advanced response. Powers the click-through "related
@@ -1 +1 @@
1
- {"version":3,"file":"DataForSeoClient.d.ts","sourceRoot":"","sources":["../../src/core/DataForSeoClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DataForSeoClient.d.ts","sourceRoot":"","sources":["../../src/core/DataForSeoClient.ts"],"names":[],"mappings":"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"}
@@ -105,10 +105,18 @@ export class DataForSeoClient {
105
105
  { keywords, ...this.loc(location), language_code: languageCode },
106
106
  ]);
107
107
  }
108
- /** SERP — live Google organic results (per-request cost; use sparingly). */
109
- async serpOrganic(keyword, location, languageCode = 'en', depth = 20) {
108
+ /** SERP — live Google organic results (per-request cost; use sparingly).
109
+ * `depth` = how many ORGANIC results to collect (not total SERP elements), so a domain absent
110
+ * at depth=20 is absent from the top 20 organic — nothing is known about its position beyond that.
111
+ * `loadAsyncAiOverview` makes DataForSEO wait for a client-side-loaded AI Overview and return its
112
+ * references; without it those SERPs come back with an empty AIO (asynchronous_ai_overview:true)
113
+ * and citation is unknowable. Small surcharge, refunded when no async AIO is found. */
114
+ async serpOrganic(keyword, location, languageCode = 'en', depth = 20, opts = {}) {
110
115
  return this.call('/v3/serp/google/organic/live/advanced', [
111
- { keyword, ...this.loc(location), language_code: languageCode, depth },
116
+ {
117
+ keyword, ...this.loc(location), language_code: languageCode, depth,
118
+ ...(opts.loadAsyncAiOverview ? { load_async_ai_overview: true } : {}),
119
+ },
112
120
  ]);
113
121
  }
114
122
  /**