@pseolint/core 0.5.7 → 0.5.11

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 (34) hide show
  1. package/dist/auditor.d.ts.map +1 -1
  2. package/dist/auditor.js +86 -7
  3. package/dist/auditor.js.map +1 -1
  4. package/dist/per-template-scoring.d.ts +30 -0
  5. package/dist/per-template-scoring.d.ts.map +1 -0
  6. package/dist/per-template-scoring.js +322 -0
  7. package/dist/per-template-scoring.js.map +1 -0
  8. package/dist/rules/content/regurgitated-content.d.ts +3 -0
  9. package/dist/rules/content/regurgitated-content.d.ts.map +1 -0
  10. package/dist/rules/content/regurgitated-content.js +112 -0
  11. package/dist/rules/content/regurgitated-content.js.map +1 -0
  12. package/dist/rules/content/translation-no-op.d.ts +11 -0
  13. package/dist/rules/content/translation-no-op.d.ts.map +1 -0
  14. package/dist/rules/content/translation-no-op.js +101 -0
  15. package/dist/rules/content/translation-no-op.js.map +1 -0
  16. package/dist/rules/content/value-add.d.ts +10 -0
  17. package/dist/rules/content/value-add.d.ts.map +1 -0
  18. package/dist/rules/content/value-add.js +117 -0
  19. package/dist/rules/content/value-add.js.map +1 -0
  20. package/dist/scrape-strategy.d.ts +19 -0
  21. package/dist/scrape-strategy.d.ts.map +1 -1
  22. package/dist/scrape-strategy.js +151 -1
  23. package/dist/scrape-strategy.js.map +1 -1
  24. package/dist/site-classifier.d.ts +49 -0
  25. package/dist/site-classifier.d.ts.map +1 -1
  26. package/dist/site-classifier.js +68 -0
  27. package/dist/site-classifier.js.map +1 -1
  28. package/dist/template-detection.d.ts +51 -0
  29. package/dist/template-detection.d.ts.map +1 -0
  30. package/dist/template-detection.js +139 -0
  31. package/dist/template-detection.js.map +1 -0
  32. package/dist/types.d.ts +60 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ /**
2
+ * v0.6 Per-template scoring — aggregates findings by template cluster,
3
+ * computes per-template verdict/risk/categories, and computes the variance
4
+ * metric (fire-rates, uniformity, top driver).
5
+ *
6
+ * Also exposes `siteVerdictFromTemplates` per spec §15.1.
7
+ *
8
+ * See spec §4–§5, §15.1.
9
+ */
10
+ import type { RuleResult, Template, Verdict } from "./types.js";
11
+ import type { TemplateCandidate } from "./template-detection.js";
12
+ export declare function verdictRank(v: Verdict): number;
13
+ /**
14
+ * Score all templates. Takes the full findings list (already enriched + overrides applied)
15
+ * and the template candidates. Returns a Template[] array.
16
+ *
17
+ * Per-page findings are tagged with their template signature on the RuleResult.
18
+ * Site-level findings (no pageUrl) remain untagged.
19
+ */
20
+ export declare function scoreTemplates(findings: RuleResult[], candidates: TemplateCandidate[], urlToTemplate: Map<string, string>, totalDiscoveredUrls: number): Template[];
21
+ /**
22
+ * Aggregate site verdict from per-template verdicts per spec §15.1.
23
+ *
24
+ * - Filter to templates with totalUrls / totalDiscoveredUrls >= 5%
25
+ * - Long-tail bucket is excluded
26
+ * - Take the worst verdict among qualifying templates
27
+ * - If no template meets the 5% threshold, return null (caller uses legacy verdict)
28
+ */
29
+ export declare function siteVerdictFromTemplates(templates: Template[]): Verdict | null;
30
+ //# sourceMappingURL=per-template-scoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"per-template-scoring.d.ts","sourceRoot":"","sources":["../src/per-template-scoring.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAGV,UAAU,EACV,QAAQ,EAER,OAAO,EACR,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAQjE,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAE9C;AA0PD;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,UAAU,EAAE,EACtB,UAAU,EAAE,iBAAiB,EAAE,EAC/B,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,mBAAmB,EAAE,MAAM,GAC1B,QAAQ,EAAE,CA4DZ;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CACtC,SAAS,EAAE,QAAQ,EAAE,GACpB,OAAO,GAAG,IAAI,CAkBhB"}
@@ -0,0 +1,322 @@
1
+ /**
2
+ * v0.6 Per-template scoring — aggregates findings by template cluster,
3
+ * computes per-template verdict/risk/categories, and computes the variance
4
+ * metric (fire-rates, uniformity, top driver).
5
+ *
6
+ * Also exposes `siteVerdictFromTemplates` per spec §15.1.
7
+ *
8
+ * See spec §4–§5, §15.1.
9
+ */
10
+ import { LONGTAIL_SIGNATURE } from "./template-detection.js";
11
+ /** Minimum template coverage fraction to count for site verdict (spec §15.1). */
12
+ const SITE_VERDICT_MIN_COVERAGE = 0.05;
13
+ const VERDICT_LADDER = ["ready", "caution", "concerning", "critical"];
14
+ export function verdictRank(v) {
15
+ return VERDICT_LADDER.indexOf(v);
16
+ }
17
+ function verdictForRisk(risk) {
18
+ if (risk <= 20)
19
+ return "ready";
20
+ if (risk <= 40)
21
+ return "caution";
22
+ if (risk <= 60)
23
+ return "concerning";
24
+ return "critical";
25
+ }
26
+ function gradeForPenalty(penalty) {
27
+ if (penalty <= 20)
28
+ return "A";
29
+ if (penalty <= 40)
30
+ return "B";
31
+ if (penalty <= 60)
32
+ return "C";
33
+ if (penalty <= 80)
34
+ return "D";
35
+ return "F";
36
+ }
37
+ const CATEGORY_MAP = {
38
+ spam: "integrity",
39
+ content: "integrity",
40
+ cannibal: "integrity",
41
+ links: "discoverability",
42
+ tech: "discoverability",
43
+ aeo: "citation",
44
+ schema: "citation",
45
+ data: "data",
46
+ audit: "audit",
47
+ };
48
+ const RULE_IMPACTS = {
49
+ "spam/near-duplicate": { baseImpact: 25, perInstance: 5, maxImpact: 80 },
50
+ "spam/entity-swap": { baseImpact: 25, perInstance: 5, maxImpact: 80 },
51
+ "spam/doorway-pattern": { baseImpact: 30, perInstance: 0, maxImpact: 30 },
52
+ "spam/template-coverage": { baseImpact: 15, perInstance: 3, maxImpact: 60 },
53
+ "spam/template-diversity": { baseImpact: 12, perInstance: 3, maxImpact: 50 },
54
+ "spam/boilerplate-ratio": { baseImpact: 10, perInstance: 2, maxImpact: 40 },
55
+ "spam/thin-content": { baseImpact: 8, perInstance: 2, maxImpact: 40 },
56
+ "spam/publication-velocity": { baseImpact: 8, perInstance: 2, maxImpact: 30 },
57
+ "cannibal/url-pattern": { baseImpact: 10, perInstance: 2, maxImpact: 40 },
58
+ "content/unique-value": { baseImpact: 10, perInstance: 2, maxImpact: 40 },
59
+ "content/meta-uniqueness": { baseImpact: 8, perInstance: 2, maxImpact: 40 },
60
+ "content/missing-author": { baseImpact: 4, perInstance: 1, maxImpact: 20 },
61
+ "content/eeat-signals": { baseImpact: 4, perInstance: 1, maxImpact: 20 },
62
+ "content/title-uniqueness": { baseImpact: 8, perInstance: 2, maxImpact: 25 },
63
+ "content/heading-structure": { baseImpact: 5, perInstance: 1, maxImpact: 20 },
64
+ "content/image-alt-text": { baseImpact: 3, perInstance: 1, maxImpact: 20 },
65
+ "content/translation-no-op": { baseImpact: 30, perInstance: 10, maxImpact: 60 },
66
+ "content/regurgitated-content": { baseImpact: 15, perInstance: 5, maxImpact: 35 },
67
+ "content/value-add": { baseImpact: 25, perInstance: 8, maxImpact: 50 },
68
+ "tech/canonical-consistency": { baseImpact: 8, perInstance: 1, maxImpact: 25 },
69
+ "tech/canonical-noindex-conflict": { baseImpact: 10, perInstance: 2, maxImpact: 40 },
70
+ "tech/robots-noindex-conflict": { baseImpact: 10, perInstance: 2, maxImpact: 40 },
71
+ "tech/redirect-chain": { baseImpact: 5, perInstance: 1, maxImpact: 25 },
72
+ "tech/sitemap-completeness": { baseImpact: 8, perInstance: 1, maxImpact: 30 },
73
+ "tech/robots-sitemap-presence": { baseImpact: 8, perInstance: 0, maxImpact: 8 },
74
+ "tech/soft-404": { baseImpact: 6, perInstance: 1, maxImpact: 30 },
75
+ "tech/hreflang-consistency": { baseImpact: 5, perInstance: 0, maxImpact: 5 },
76
+ "tech/og-completeness": { baseImpact: 4, perInstance: 1, maxImpact: 20 },
77
+ "links/orphan-pages": { baseImpact: 5, perInstance: 1, maxImpact: 25 },
78
+ "links/dead-ends": { baseImpact: 3, perInstance: 1, maxImpact: 20 },
79
+ "links/cluster-connectivity": { baseImpact: 5, perInstance: 1, maxImpact: 25 },
80
+ "links/link-depth": { baseImpact: 3, perInstance: 1, maxImpact: 20 },
81
+ "aeo/citable-facts": { baseImpact: 2, perInstance: 1, maxImpact: 25 },
82
+ "aeo/answer-first": { baseImpact: 3, perInstance: 1, maxImpact: 25 },
83
+ "aeo/summary-bait": { baseImpact: 4, perInstance: 1, maxImpact: 25 },
84
+ "aeo/crawler-access": { baseImpact: 8, perInstance: 0, maxImpact: 8 },
85
+ "aeo/freshness-signals": { baseImpact: 2, perInstance: 1, maxImpact: 20 },
86
+ "aeo/llms-txt": { baseImpact: 4, perInstance: 0, maxImpact: 4 },
87
+ "aeo/faq-coverage": { baseImpact: 2, perInstance: 1, maxImpact: 15 },
88
+ "aeo/content-modularity": { baseImpact: 2, perInstance: 1, maxImpact: 15 },
89
+ "schema/json-ld-valid": { baseImpact: 8, perInstance: 2, maxImpact: 35 },
90
+ "schema/required-fields": { baseImpact: 6, perInstance: 1, maxImpact: 30 },
91
+ "schema/consistency": { baseImpact: 3, perInstance: 1, maxImpact: 15 },
92
+ "data/data-binding": { baseImpact: 6, perInstance: 1, maxImpact: 30 },
93
+ };
94
+ const DEFAULT_RULE_IMPACT = { baseImpact: 5, perInstance: 1, maxImpact: 25 };
95
+ const CONFIDENCE_MULTIPLIER = {
96
+ high: 1.0,
97
+ medium: 0.6,
98
+ low: 0.3,
99
+ speculative: 0.1,
100
+ };
101
+ /** Compute simple risk + categories from a scoped set of findings. */
102
+ function computeRiskAndCategories(findings, pageCount) {
103
+ const bucketInfoOnly = {
104
+ integrity: 0, discoverability: 0, citation: 0, data: 0, audit: 0,
105
+ };
106
+ const bucketNonInfo = {
107
+ integrity: 0, discoverability: 0, citation: 0, data: 0, audit: 0,
108
+ };
109
+ const bucketIssues = {
110
+ integrity: 0, discoverability: 0, citation: 0, data: 0, audit: 0,
111
+ };
112
+ let blockers = 0;
113
+ const groups = new Map();
114
+ for (const finding of findings) {
115
+ const namespace = finding.ruleId.split("/")[0];
116
+ const bucket = CATEGORY_MAP[namespace];
117
+ if (!bucket)
118
+ continue;
119
+ if (bucket !== "audit")
120
+ bucketIssues[bucket] += 1;
121
+ if (bucket === "audit")
122
+ continue;
123
+ if (finding.severity === "critical" || finding.severity === "error")
124
+ blockers += 1;
125
+ const arr = groups.get(finding.ruleId) ?? [];
126
+ arr.push(finding);
127
+ groups.set(finding.ruleId, arr);
128
+ }
129
+ for (const [ruleId, group] of groups) {
130
+ const namespace = ruleId.split("/")[0];
131
+ const bucket = CATEGORY_MAP[namespace];
132
+ if (!bucket || bucket === "audit")
133
+ continue;
134
+ const impactSpec = RULE_IMPACTS[ruleId] ?? DEFAULT_RULE_IMPACT;
135
+ const count = group.length;
136
+ const rawImpact = impactSpec.baseImpact + Math.max(0, count - 1) * impactSpec.perInstance;
137
+ const cap = impactSpec.maxImpact ?? Number.POSITIVE_INFINITY;
138
+ const cappedImpact = Math.min(cap, rawImpact);
139
+ let bestMultiplier = 0;
140
+ for (const f of group) {
141
+ const conf = f.confidence ?? "high";
142
+ const m = CONFIDENCE_MULTIPLIER[conf] ?? 1.0;
143
+ if (m > bestMultiplier)
144
+ bestMultiplier = m;
145
+ }
146
+ if (bestMultiplier === 0)
147
+ bestMultiplier = CONFIDENCE_MULTIPLIER.high;
148
+ const weighted = cappedImpact * bestMultiplier;
149
+ const isInfoOnly = group.every((f) => f.severity === "info");
150
+ if (isInfoOnly) {
151
+ bucketInfoOnly[bucket] += weighted;
152
+ }
153
+ else {
154
+ bucketNonInfo[bucket] += weighted;
155
+ }
156
+ }
157
+ const bucketRaw = {
158
+ integrity: 0, discoverability: 0, citation: 0, data: 0, audit: 0,
159
+ };
160
+ for (const key of ["integrity", "discoverability", "citation", "data"]) {
161
+ const info = Math.min(50, bucketInfoOnly[key]);
162
+ const nonInfo = Math.min(100, bucketNonInfo[key]);
163
+ bucketRaw[key] = Math.min(100, info + nonInfo);
164
+ }
165
+ // Use equal weights for per-template scoring — no site-type profile here
166
+ // (the classification-based profile applies at the site level, not per-template).
167
+ const weighted = bucketRaw.integrity * 0.40 +
168
+ bucketRaw.discoverability * 0.25 +
169
+ bucketRaw.citation * 0.25 +
170
+ bucketRaw.data * 0.10;
171
+ // Blocker density floor (same formula as site-level).
172
+ const blockerRatio = pageCount > 0 ? blockers / pageCount : 0;
173
+ const blockerFloor = blockerRatio >= 0.5 ? 60 :
174
+ blockerRatio >= 0.3 ? 45 :
175
+ blockerRatio >= 0.15 ? 25 :
176
+ 0;
177
+ const risk = Math.round(Math.min(100, Math.max(weighted, blockerFloor)));
178
+ const categories = {
179
+ integrity: { grade: gradeForPenalty(bucketRaw.integrity), issues: bucketIssues.integrity },
180
+ discoverability: { grade: gradeForPenalty(bucketRaw.discoverability), issues: bucketIssues.discoverability },
181
+ citation: { grade: gradeForPenalty(bucketRaw.citation), issues: bucketIssues.citation },
182
+ data: { grade: gradeForPenalty(bucketRaw.data), issues: bucketIssues.data },
183
+ audit: { grade: "A", issues: 0 },
184
+ };
185
+ return { risk, categories };
186
+ }
187
+ /**
188
+ * Compute the TemplateVariance metric for a template.
189
+ *
190
+ * For each audited URL, we check which rules fired on it. Then:
191
+ * - ruleFireRates[ruleId] = URLs where rule fired / total audited URLs
192
+ * - uniformityScore = 1 - mean(stdev(per-rule binary fire patterns))
193
+ * - topDriver = rule with highest fire rate
194
+ *
195
+ * spec §5.1-§5.3.
196
+ */
197
+ function computeVariance(auditedUrls, findingsByUrl) {
198
+ if (auditedUrls.length === 0) {
199
+ return { ruleFireRates: {}, uniformityScore: 1, topDriver: null };
200
+ }
201
+ // Collect all rule IDs that fired at least once across audited URLs.
202
+ const allRules = new Set();
203
+ for (const url of auditedUrls) {
204
+ const rules = findingsByUrl.get(url);
205
+ if (rules) {
206
+ for (const r of rules)
207
+ allRules.add(r);
208
+ }
209
+ }
210
+ if (allRules.size === 0) {
211
+ return { ruleFireRates: {}, uniformityScore: 1, topDriver: null };
212
+ }
213
+ const n = auditedUrls.length;
214
+ const ruleFireRates = {};
215
+ const ruleStdevs = [];
216
+ for (const ruleId of allRules) {
217
+ // Binary fire pattern across samples (1 = fired, 0 = did not fire).
218
+ const pattern = auditedUrls.map((url) => (findingsByUrl.get(url)?.has(ruleId) ? 1 : 0));
219
+ const rate = pattern.reduce((s, v) => s + v, 0) / n;
220
+ ruleFireRates[ruleId] = rate;
221
+ // Sample standard deviation of binary pattern.
222
+ if (n <= 1) {
223
+ ruleStdevs.push(0);
224
+ }
225
+ else {
226
+ const variance = pattern.reduce((s, v) => s + (v - rate) ** 2, 0) / (n - 1);
227
+ ruleStdevs.push(Math.sqrt(variance));
228
+ }
229
+ }
230
+ const avgStdev = ruleStdevs.length > 0
231
+ ? ruleStdevs.reduce((s, v) => s + v, 0) / ruleStdevs.length
232
+ : 0;
233
+ const uniformityScore = Math.max(0, Math.min(1, 1 - avgStdev));
234
+ let topDriver = null;
235
+ for (const [ruleId, fireRate] of Object.entries(ruleFireRates)) {
236
+ if (!topDriver || fireRate > topDriver.fireRate) {
237
+ topDriver = { ruleId, fireRate };
238
+ }
239
+ }
240
+ return { ruleFireRates, uniformityScore, topDriver };
241
+ }
242
+ /**
243
+ * Score all templates. Takes the full findings list (already enriched + overrides applied)
244
+ * and the template candidates. Returns a Template[] array.
245
+ *
246
+ * Per-page findings are tagged with their template signature on the RuleResult.
247
+ * Site-level findings (no pageUrl) remain untagged.
248
+ */
249
+ export function scoreTemplates(findings, candidates, urlToTemplate, totalDiscoveredUrls) {
250
+ if (candidates.length === 0)
251
+ return [];
252
+ const templates = [];
253
+ for (const candidate of candidates) {
254
+ // Skip longtail from per-template scoring — it's a catch-all, not a real template.
255
+ // We still compute it but mark it as low-priority for verdict aggregation.
256
+ const auditedUrlSet = new Set(candidate.urls);
257
+ // Tag findings that belong to this template.
258
+ const templateFindings = [];
259
+ const findingsByUrl = new Map();
260
+ for (const finding of findings) {
261
+ if (!finding.pageUrl)
262
+ continue; // site-level finding
263
+ if (!auditedUrlSet.has(finding.pageUrl))
264
+ continue;
265
+ // Skip audit/* diagnostics from per-template scoring.
266
+ if (finding.ruleId.startsWith("audit/"))
267
+ continue;
268
+ templateFindings.push(finding);
269
+ // Track per-URL fired rules for variance metric.
270
+ const existing = findingsByUrl.get(finding.pageUrl) ?? new Set();
271
+ existing.add(finding.ruleId);
272
+ findingsByUrl.set(finding.pageUrl, existing);
273
+ }
274
+ // Tag findings with their template.
275
+ for (const finding of findings) {
276
+ if (finding.pageUrl && auditedUrlSet.has(finding.pageUrl) && !finding.template) {
277
+ finding.template = candidate.signature;
278
+ }
279
+ }
280
+ const auditedUrls = candidate.urls;
281
+ const { risk, categories } = computeRiskAndCategories(templateFindings, auditedUrls.length);
282
+ const verdict = verdictForRisk(risk);
283
+ const variance = computeVariance(auditedUrls, findingsByUrl);
284
+ // Collect finding IDs — use ruleId+pageUrl as a stable reference key.
285
+ const findingIds = templateFindings.map((f) => `${f.ruleId}:${f.pageUrl ?? ""}`);
286
+ templates.push({
287
+ signature: candidate.signature,
288
+ totalUrls: candidate.count,
289
+ totalDiscoveredUrls,
290
+ auditedUrls,
291
+ verdict,
292
+ risk,
293
+ categories,
294
+ variance,
295
+ findingIds,
296
+ });
297
+ }
298
+ return templates;
299
+ }
300
+ /**
301
+ * Aggregate site verdict from per-template verdicts per spec §15.1.
302
+ *
303
+ * - Filter to templates with totalUrls / totalDiscoveredUrls >= 5%
304
+ * - Long-tail bucket is excluded
305
+ * - Take the worst verdict among qualifying templates
306
+ * - If no template meets the 5% threshold, return null (caller uses legacy verdict)
307
+ */
308
+ export function siteVerdictFromTemplates(templates) {
309
+ const qualifying = templates.filter((t) => t.signature !== LONGTAIL_SIGNATURE &&
310
+ t.totalDiscoveredUrls > 0 &&
311
+ t.totalUrls / t.totalDiscoveredUrls >= SITE_VERDICT_MIN_COVERAGE);
312
+ if (qualifying.length === 0)
313
+ return null;
314
+ let worstVerdict = "ready";
315
+ for (const t of qualifying) {
316
+ if (verdictRank(t.verdict) > verdictRank(worstVerdict)) {
317
+ worstVerdict = t.verdict;
318
+ }
319
+ }
320
+ return worstVerdict;
321
+ }
322
+ //# sourceMappingURL=per-template-scoring.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"per-template-scoring.js","sourceRoot":"","sources":["../src/per-template-scoring.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { ParsedPage, RuleResult } from "../../types.js";
2
+ export declare function regurgitatedContentRule(pages: ParsedPage[]): RuleResult[];
3
+ //# sourceMappingURL=regurgitated-content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regurgitated-content.d.ts","sourceRoot":"","sources":["../../../src/rules/content/regurgitated-content.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAiG7D,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CA8BzE"}
@@ -0,0 +1,112 @@
1
+ import * as cheerio from "cheerio";
2
+ const RULE_ID = "content/regurgitated-content";
3
+ /**
4
+ * Detects the Google Places API regurgitation pattern: sites that lift
5
+ * names, reviews, addresses, and photos from the Places API and present
6
+ * them as a "directory" without any curation or value-add.
7
+ *
8
+ * v1: heuristic-only. Fires when >=2 distinct signals are present per page.
9
+ * External corpus checks (Wikipedia / Tripadvisor n-gram overlap) deferred to v0.6.
10
+ */
11
+ // Text-search regexes — kept as-is (no DOM-parsing fragility).
12
+ const GOOGLE_ATTRIBUTION_RE = /powered by google/i;
13
+ const GOOGLE_IMG_HOSTS = [
14
+ "googleusercontent.com",
15
+ "lh3.googleusercontent.com",
16
+ "maps.googleapis.com/maps/api/place/photo",
17
+ "streetviewpixels-pa.googleapis.com",
18
+ ];
19
+ const STATIC_MAP_SRC_RE = /maps\.googleapis\.com\/maps\/api\/staticmap/i;
20
+ const MAPS_EMBED_SRC_RE = /(?:maps\.google\.com|google\.com\/maps)\/embed/i;
21
+ const PLACES_API_JS_RE = /google\.maps\.places\.(?:Service|PlacesService|AutocompleteService)|places\.PlacesService\b/;
22
+ // Star-rating patterns: Unicode stars (U+2605, U+2606, U+2729), HTML entities,
23
+ // fraction-of-five numerics (4.5/5), or the literal word "stars".
24
+ const STAR_RATING_RE = /&#9733;|&#9734;|★|☆|✩|\d+(?:\.\d+)?\/5|\d+\s*stars?/i;
25
+ const MIN_IMAGES_FOR_RATIO_CHECK = 3;
26
+ const GOOGLE_IMG_RATIO_THRESHOLD = 0.6;
27
+ const MIN_REVIEW_BLOCKS = 5;
28
+ function checkGoogleAttribution($) {
29
+ const label = "Google Places attribution";
30
+ if (GOOGLE_ATTRIBUTION_RE.test($.text()))
31
+ return { fired: true, label };
32
+ const hasAnchor = $('a[href*="google.com/maps"][rel*="noopener"]').length > 0;
33
+ return { fired: hasAnchor, label };
34
+ }
35
+ function isGoogleImgHost(src) {
36
+ return GOOGLE_IMG_HOSTS.some((host) => src.includes(host));
37
+ }
38
+ function checkGoogleImagesDominate($) {
39
+ const label = "Google-hosted images dominate (>=60%)";
40
+ const srcs = $("img[src]")
41
+ .map((_, el) => $(el).attr("src") ?? "")
42
+ .get()
43
+ .filter(Boolean);
44
+ if (srcs.length < MIN_IMAGES_FOR_RATIO_CHECK)
45
+ return { fired: false, label };
46
+ const googleCount = srcs.filter(isGoogleImgHost).length;
47
+ return { fired: googleCount / srcs.length >= GOOGLE_IMG_RATIO_THRESHOLD, label };
48
+ }
49
+ function checkStaticMapsEmbed($, html) {
50
+ const label = "Google Static Maps or Maps embed";
51
+ if (STATIC_MAP_SRC_RE.test(html))
52
+ return { fired: true, label };
53
+ const hasEmbedIframe = $("iframe[src]")
54
+ .map((_, el) => $(el).attr("src") ?? "")
55
+ .get()
56
+ .some((src) => MAPS_EMBED_SRC_RE.test(src));
57
+ return { fired: hasEmbedIframe, label };
58
+ }
59
+ function checkPlacesApiJs(html) {
60
+ return { fired: PLACES_API_JS_RE.test(html), label: "Places API JS markers" };
61
+ }
62
+ function eeatSignalCount(page) {
63
+ const { metaAuthor, schemaAuthor, bylineElement, relAuthorLink } = page.authorSignals;
64
+ let n = 0;
65
+ if (metaAuthor !== "" || schemaAuthor || bylineElement || relAuthorLink)
66
+ n += 1;
67
+ if (page.publishedDate)
68
+ n += 1;
69
+ if (page.resolvedHrefs.some((h) => /\/about\b/i.test(h)))
70
+ n += 1;
71
+ return n;
72
+ }
73
+ function checkAggregatorFootprint($, eeat) {
74
+ const label = "Aggregator footprint (5+ review blocks, no author signal)";
75
+ if (eeat >= 2)
76
+ return { fired: false, label };
77
+ const count = $("div, li, article, section")
78
+ .filter((_, el) => STAR_RATING_RE.test($(el).text()))
79
+ .length;
80
+ return { fired: count >= MIN_REVIEW_BLOCKS, label };
81
+ }
82
+ export function regurgitatedContentRule(pages) {
83
+ const findings = [];
84
+ for (const page of pages) {
85
+ const html = page.html ?? "";
86
+ if (!html)
87
+ continue;
88
+ const $ = cheerio.load(html);
89
+ const eeat = eeatSignalCount(page);
90
+ const signals = [
91
+ checkGoogleAttribution($),
92
+ checkGoogleImagesDominate($),
93
+ checkStaticMapsEmbed($, html),
94
+ checkPlacesApiJs(html),
95
+ checkAggregatorFootprint($, eeat),
96
+ ];
97
+ const fired = signals.filter((s) => s.fired);
98
+ if (fired.length < 2)
99
+ continue;
100
+ const signalList = fired.map((s) => s.label).join("; ");
101
+ findings.push({
102
+ ruleId: RULE_ID,
103
+ severity: "warning",
104
+ message: `${page.url}: ${fired.length} Google Places regurgitation signals -- ${signalList}.`,
105
+ pageUrl: page.url,
106
+ fix: "Replace API-lifted data (photos, reviews, ratings) with original curated content. Add editorial selection rationale, local expertise, or unique commentary not available via the raw Places API. The presence of Powered by Google attribution or >60% googleusercontent images indicates content scraped without value-add.",
107
+ confidence: "speculative",
108
+ });
109
+ }
110
+ return findings;
111
+ }
112
+ //# sourceMappingURL=regurgitated-content.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regurgitated-content.js","sourceRoot":"","sources":["../../../src/rules/content/regurgitated-content.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { ParsedPage, RuleResult } from "../../types.js";
2
+ /**
3
+ * content/translation-no-op -- detects locale variants of the same template
4
+ * that were never actually translated (similarity >= 0.95).
5
+ *
6
+ * Groups pages whose URL paths differ only by a leading locale segment
7
+ * (e.g. `/en/`, `/fr/`, `/it/`). Within each group, fires one error per
8
+ * cluster of identical/near-identical variants.
9
+ */
10
+ export declare function translationNoOpRule(pages: ParsedPage[]): RuleResult[];
11
+ //# sourceMappingURL=translation-no-op.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translation-no-op.d.ts","sourceRoot":"","sources":["../../../src/rules/content/translation-no-op.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAoC7D;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAgErE"}
@@ -0,0 +1,101 @@
1
+ import { hammingDistance, simHashFromText, similarityFromDistance } from "../../algorithms/simhash.js";
2
+ const LOCALE_PREFIX_RE = /^\/([a-z]{2}(-[a-z]{2})?)(\/|$)/i;
3
+ const SIMILARITY_THRESHOLD = 0.95;
4
+ /**
5
+ * Minimum body word count before translation-no-op evaluates a page. Below
6
+ * this floor the issue is "no content at all" — better surfaced by
7
+ * `spam/thin-content` — and pairwise simhash similarity collapses to ~1.0
8
+ * for any two empty/near-empty pages, producing a confusingly-redundant
9
+ * "you didn't translate" finding when really there's nothing to translate.
10
+ * v0.5.6 refinement.
11
+ */
12
+ const MIN_WORDS_FOR_TRANSLATION_CHECK = 30;
13
+ /**
14
+ * Strips a leading locale segment from a URL path.
15
+ * `/en/about` -> `/about`, `/fr-CA/about` -> `/about`, `/about` -> `/about`
16
+ */
17
+ function stripLocalePrefix(pathname) {
18
+ const m = LOCALE_PREFIX_RE.exec(pathname);
19
+ if (!m)
20
+ return pathname;
21
+ // m[3] is "/" or "" (end-of-string)
22
+ return m[3] === "/" ? pathname.slice(m[0].length - 1) : "/";
23
+ }
24
+ /**
25
+ * Returns the path component of a URL, or "/" on parse failure.
26
+ */
27
+ function getPathname(url) {
28
+ try {
29
+ return new URL(url).pathname;
30
+ }
31
+ catch {
32
+ return "/";
33
+ }
34
+ }
35
+ /**
36
+ * content/translation-no-op -- detects locale variants of the same template
37
+ * that were never actually translated (similarity >= 0.95).
38
+ *
39
+ * Groups pages whose URL paths differ only by a leading locale segment
40
+ * (e.g. `/en/`, `/fr/`, `/it/`). Within each group, fires one error per
41
+ * cluster of identical/near-identical variants.
42
+ */
43
+ export function translationNoOpRule(pages) {
44
+ const findings = [];
45
+ // Build map: basePath -> [{ page, locale }]
46
+ const groups = new Map();
47
+ for (const page of pages) {
48
+ const pathname = getPathname(page.url);
49
+ const m = LOCALE_PREFIX_RE.exec(pathname);
50
+ if (!m)
51
+ continue; // no locale prefix -- skip
52
+ const locale = m[1].toLowerCase();
53
+ const basePath = stripLocalePrefix(pathname);
54
+ const bucket = groups.get(basePath) ?? [];
55
+ bucket.push({ page, locale });
56
+ groups.set(basePath, bucket);
57
+ }
58
+ for (const [basePath, members] of groups) {
59
+ if (members.length < 2)
60
+ continue;
61
+ // Skip the cluster when every variant is below the min-content floor —
62
+ // the real issue is thin-content, not a translation no-op. (Mixed-case
63
+ // clusters where some variants are full-content still evaluate, since a
64
+ // single thin variant against a full one IS a translation issue.)
65
+ const allBelowMin = members.every((m) => m.page.contentText.split(/\s+/).filter(Boolean).length < MIN_WORDS_FOR_TRANSLATION_CHECK);
66
+ if (allBelowMin)
67
+ continue;
68
+ const hashes = members.map((m) => simHashFromText(m.page.contentText));
69
+ let minSim = 1;
70
+ let maxSim = 0;
71
+ let anyAbove = false;
72
+ for (let i = 0; i < members.length; i += 1) {
73
+ for (let j = i + 1; j < members.length; j += 1) {
74
+ const sim = similarityFromDistance(hammingDistance(hashes[i], hashes[j]));
75
+ if (sim < minSim)
76
+ minSim = sim;
77
+ if (sim > maxSim)
78
+ maxSim = sim;
79
+ if (sim >= SIMILARITY_THRESHOLD)
80
+ anyAbove = true;
81
+ }
82
+ }
83
+ if (!anyAbove)
84
+ continue;
85
+ const urls = members.map((m) => m.page.url);
86
+ const simLabel = minSim === maxSim
87
+ ? `${(minSim * 100).toFixed(0)}%`
88
+ : `${(minSim * 100).toFixed(0)}%--${(maxSim * 100).toFixed(0)}%`;
89
+ findings.push({
90
+ ruleId: "content/translation-no-op",
91
+ severity: "error",
92
+ message: `${members.length} locale variants of "${basePath}" share identical content ` +
93
+ `(similarity ${simLabel}). Translate the body or consolidate to the canonical version.`,
94
+ pageUrl: urls[0],
95
+ relatedUrls: urls.slice(1, 6),
96
+ fix: "Either provide unique translated body content for each locale variant, or redirect all locale variants to the canonical URL and use hreflang only on the canonical.",
97
+ });
98
+ }
99
+ return findings;
100
+ }
101
+ //# sourceMappingURL=translation-no-op.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translation-no-op.js","sourceRoot":"","sources":["../../../src/rules/content/translation-no-op.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import type { ParsedPage, RuleResult } from "../../types.js";
2
+ /**
3
+ * content/value-add — second-pass composite rule.
4
+ *
5
+ * Reads from existing findings instead of parsing pages directly.
6
+ * Aggregates 5 per-page signal scores into a single 0-1 quality score.
7
+ * Fires ONE critical/error finding per page when score < 0.5.
8
+ */
9
+ export declare function valueAddRule(pages: ParsedPage[], findings: RuleResult[]): RuleResult[];
10
+ //# sourceMappingURL=value-add.d.ts.map