diff --git a/seo_mode/seo_mode/package.json b/seo_mode/seo_mode/package.json index b5ff07a..0d10aa9 100644 --- a/seo_mode/seo_mode/package.json +++ b/seo_mode/seo_mode/package.json @@ -17,6 +17,10 @@ "check:semantic-blocks": "npm run check:semantic-blocks -w server", "check:source-models": "npm run check:source-models -w server", "check:ontology-universality": "npm run check:ontology-universality -w server", + "check:context-business-value": "npm run check:context-business-value -w server", + "check:pre-wordstat": "npm run check:pre-wordstat -w server", + "check:post-wordstat": "npm run check:post-wordstat -w server", + "check:positioning-intelligence": "npm run check:positioning-intelligence -w server", "typecheck": "npm run typecheck -w server && npm run typecheck -w app", "db:migrate": "npm run db:migrate -w server" }, diff --git a/seo_mode/seo_mode/server/migrations/011_query_discovery.sql b/seo_mode/seo_mode/server/migrations/011_query_discovery.sql new file mode 100644 index 0000000..9e77669 --- /dev/null +++ b/seo_mode/seo_mode/server/migrations/011_query_discovery.sql @@ -0,0 +1,40 @@ +create table query_observations ( + id uuid primary key default gen_random_uuid(), + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete set null, + topic_id text not null, + probe_id text, + pattern_id text, + query text not null, + normalized_query text not null, + source text not null, + source_seed text, + geo text, + language text, + raw_artifact_key text, + metadata jsonb not null default '{}'::jsonb, + observed_at timestamptz not null default now(), + created_at timestamptz not null default now(), + unique(project_id, topic_id, normalized_query, source) +); + +create index query_observations_project_semantic_idx + on query_observations(project_id, semantic_run_id, observed_at desc); + +create index query_observations_project_topic_idx + on query_observations(project_id, topic_id, observed_at desc); + +create table query_probe_decisions ( + id uuid primary key default gen_random_uuid(), + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete set null, + topic_id text not null, + probe_id text not null, + approved boolean not null, + reason text not null default '', + decided_at timestamptz not null default now(), + unique(project_id, topic_id, probe_id) +); + +create index query_probe_decisions_project_semantic_idx + on query_probe_decisions(project_id, semantic_run_id, decided_at desc); diff --git a/seo_mode/seo_mode/server/migrations/012_query_candidate_decisions.sql b/seo_mode/seo_mode/server/migrations/012_query_candidate_decisions.sql new file mode 100644 index 0000000..b5a4d3c --- /dev/null +++ b/seo_mode/seo_mode/server/migrations/012_query_candidate_decisions.sql @@ -0,0 +1,29 @@ +alter table query_probe_decisions + add column if not exists decision_status text; + +update query_probe_decisions +set decision_status = case when approved then 'wordstat' else 'deleted' end +where decision_status is null; + +alter table query_probe_decisions + alter column decision_status set default 'pending'; + +alter table query_probe_decisions + alter column decision_status set not null; + +create table if not exists query_manual_candidates ( + id uuid primary key default gen_random_uuid(), + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete set null, + topic_id text not null, + query text not null, + normalized_query text not null, + decision_status text not null default 'pending', + reason text not null default '', + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + unique(project_id, semantic_run_id, topic_id, normalized_query) +); + +create index if not exists query_manual_candidates_project_semantic_idx + on query_manual_candidates(project_id, semantic_run_id, updated_at desc); diff --git a/seo_mode/seo_mode/server/migrations/013_semantic_facets.sql b/seo_mode/seo_mode/server/migrations/013_semantic_facets.sql new file mode 100644 index 0000000..a39963f --- /dev/null +++ b/seo_mode/seo_mode/server/migrations/013_semantic_facets.sql @@ -0,0 +1,20 @@ +create table if not exists seo_semantic_facets ( + project_id uuid not null references projects(id) on delete cascade, + context_hash text not null, + facet_id text not null, + relationship_to_core text not null, + facet_kind text not null, + status text not null, + default_active boolean not null default false, + selected boolean not null default false, + facet jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (project_id, context_hash, facet_id) +); + +create index if not exists seo_semantic_facets_project_context_idx + on seo_semantic_facets(project_id, context_hash, selected, updated_at desc); + +create index if not exists seo_semantic_facets_project_kind_idx + on seo_semantic_facets(project_id, facet_kind, relationship_to_core); diff --git a/seo_mode/seo_mode/server/migrations/014_pre_wordstat_pipeline.sql b/seo_mode/seo_mode/server/migrations/014_pre_wordstat_pipeline.sql new file mode 100644 index 0000000..7879556 --- /dev/null +++ b/seo_mode/seo_mode/server/migrations/014_pre_wordstat_pipeline.sql @@ -0,0 +1,92 @@ +create table if not exists query_market_seeds ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + seed_id text not null, + facet_id text not null, + topic_id text, + normalized_seed text not null, + payload jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, seed_id) +); + +create index if not exists query_market_seeds_project_facet_idx + on query_market_seeds(project_id, semantic_run_id, facet_id); + +create table if not exists query_normalized_observations ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + observation_id uuid not null references query_observations(id) on delete cascade, + facet_id text, + topic_id text, + normalized_query text not null, + contamination text not null, + payload jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, observation_id) +); + +create index if not exists query_normalized_observations_project_facet_idx + on query_normalized_observations(project_id, semantic_run_id, facet_id, contamination); + +create table if not exists query_observed_gate_results ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + observation_id uuid not null references query_observations(id) on delete cascade, + action text not null, + intent text not null, + score numeric, + payload jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, observation_id) +); + +create table if not exists query_canonical_clusters ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + cluster_id text not null, + facet_id text not null, + topic_id text, + normalized_canonical text not null, + intent text not null, + stage text not null, + payload jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, cluster_id) +); + +create index if not exists query_canonical_clusters_project_stage_idx + on query_canonical_clusters(project_id, semantic_run_id, stage, intent); + +create table if not exists query_cluster_decisions ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + cluster_id text not null, + decision_status text not null default 'pending', + target_facet_id text, + reason text not null default '', + decided_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, cluster_id) +); + +create table if not exists query_wordstat_queue_items ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + queue_item_id text not null, + cluster_id text not null, + facet_id text not null, + topic_id text, + normalized_phrase text not null, + status text not null, + payload jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, queue_item_id) +); + +create index if not exists query_wordstat_queue_items_project_status_idx + on query_wordstat_queue_items(project_id, semantic_run_id, status, facet_id); diff --git a/seo_mode/seo_mode/server/migrations/015_market_expansion.sql b/seo_mode/seo_mode/server/migrations/015_market_expansion.sql new file mode 100644 index 0000000..f8d4052 --- /dev/null +++ b/seo_mode/seo_mode/server/migrations/015_market_expansion.sql @@ -0,0 +1,55 @@ +create table if not exists query_expansion_surfaces ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + surface_id text not null, + surface_kind text not null, + relationship_to_core text not null, + status text not null, + payload jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, surface_id) +); + +create table if not exists query_expansion_surface_decisions ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + surface_id text not null, + decision_status text not null default 'suggested', + reason text not null default '', + decided_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, surface_id) +); + +create table if not exists query_expansion_seeds ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + seed_id text not null, + surface_id text not null, + normalized_phrase text not null, + payload jsonb not null, + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, seed_id) +); + +create table if not exists query_expansion_observations ( + id uuid primary key default gen_random_uuid(), + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid references runs(id) on delete cascade, + surface_id text not null, + seed_id text not null, + query text not null, + normalized_query text not null, + source text not null, + source_probe text not null, + depth integer not null default 1, + metadata jsonb not null default '{}'::jsonb, + observed_at timestamptz not null default now(), + unique(project_id, semantic_run_id, surface_id, normalized_query, source) +); + +create index if not exists query_expansion_surfaces_project_kind_idx + on query_expansion_surfaces(project_id, semantic_run_id, surface_kind, status); +create index if not exists query_expansion_observations_project_surface_idx + on query_expansion_observations(project_id, semantic_run_id, surface_id, observed_at desc); diff --git a/seo_mode/seo_mode/server/migrations/016_post_wordstat_product_fit.sql b/seo_mode/seo_mode/server/migrations/016_post_wordstat_product_fit.sql new file mode 100644 index 0000000..09c6b6d --- /dev/null +++ b/seo_mode/seo_mode/server/migrations/016_post_wordstat_product_fit.sql @@ -0,0 +1,13 @@ +create table if not exists post_wordstat_candidate_decisions ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid not null references runs(id) on delete cascade, + candidate_id text not null, + normalized_phrase text not null, + decision_status text not null default 'pending', + reason text not null default '', + decided_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, candidate_id) +); + +create index if not exists post_wordstat_candidate_decisions_project_status_idx + on post_wordstat_candidate_decisions(project_id, semantic_run_id, decision_status, decided_at desc); diff --git a/seo_mode/seo_mode/server/migrations/017_post_wordstat_cluster_decisions.sql b/seo_mode/seo_mode/server/migrations/017_post_wordstat_cluster_decisions.sql new file mode 100644 index 0000000..f00e46b --- /dev/null +++ b/seo_mode/seo_mode/server/migrations/017_post_wordstat_cluster_decisions.sql @@ -0,0 +1,14 @@ +create table if not exists post_wordstat_cluster_decisions ( + project_id uuid not null references projects(id) on delete cascade, + semantic_run_id uuid not null references runs(id) on delete cascade, + cluster_id text not null, + canonical_phrase text not null, + decision_status text not null default 'pending', + apply_to_variants boolean not null default true, + reason text not null default '', + decided_at timestamptz not null default now(), + primary key (project_id, semantic_run_id, cluster_id) +); + +create index if not exists post_wordstat_cluster_decisions_project_status_idx + on post_wordstat_cluster_decisions(project_id, semantic_run_id, decision_status, decided_at desc); diff --git a/seo_mode/seo_mode/server/package.json b/seo_mode/seo_mode/server/package.json index fe00767..3b00257 100644 --- a/seo_mode/seo_mode/server/package.json +++ b/seo_mode/seo_mode/server/package.json @@ -13,6 +13,11 @@ "check:semantic-blocks": "tsx src/scripts/checkSemanticBlockUniversality.ts", "check:source-models": "tsx src/scripts/checkSourceModelUniversality.ts", "check:ontology-universality": "tsx src/scripts/checkOntologyUniversality.ts", + "check:semantic-portfolio": "tsx src/scripts/checkSemanticPortfolioUniversality.ts", + "check:context-business-value": "tsx src/scripts/checkContextBusinessValue.ts", + "check:pre-wordstat": "tsx src/scripts/checkPreWordstatPipeline.ts", + "check:post-wordstat": "tsx src/scripts/checkPostWordstatArchitecture.ts", + "check:positioning-intelligence": "tsx src/scripts/checkPositioningIntelligence.ts", "db:migrate": "tsx src/scripts/migrate.ts" }, "dependencies": { diff --git a/seo_mode/seo_mode/server/src/analysis/semanticAnalysis.ts b/seo_mode/seo_mode/server/src/analysis/semanticAnalysis.ts index 9f686b2..49f26dc 100644 --- a/seo_mode/seo_mode/server/src/analysis/semanticAnalysis.ts +++ b/seo_mode/seo_mode/server/src/analysis/semanticAnalysis.ts @@ -235,6 +235,13 @@ export type SemanticAnalysisOutput = { sectionCount: number; intentClusterCount: number; evidenceCount: number; + sections: Array<{ + id: string; + pageId: string; + title: string; + meaning: string; + evidenceIds: string[]; + }>; } | null; }; qualitySignals: AnalysisSignal[]; @@ -676,13 +683,20 @@ function decodeHtmlEntities(value: string) { function visibleTextFromHtml(html: string) { const body = /
]*>([\s\S]*?)<\/body>/i.exec(html)?.[1] ?? html; + const mainFragments = Array.from(body.matchAll(/