@operstack/audit 0.33.3 → 0.34.0
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.
- package/package.json +8 -3
- package/src/collect.mjs +222 -46
- package/src/consistency.mjs +90 -0
- package/src/etalon.mjs +37 -0
- package/src/i18n.mjs +26 -20
- package/src/index.mjs +6 -0
- package/src/narrative.mjs +57 -21
- package/src/niche.mjs +91 -0
- package/src/prompts.mjs +146 -5
- package/src/render.mjs +74 -16
- package/src/rendered.mjs +12 -2
- package/src/review.mjs +271 -0
- package/src/tasks-pdf.mjs +67 -0
- package/src/test-agent-surface.mjs +1 -1
- package/src/test-etalon.mjs +42 -0
- package/src/test-narrative.mjs +11 -2
- package/src/test-prompts.mjs +2 -2
- package/src/test-rendered.mjs +11 -2
- package/src/test-shopify-store.mjs +159 -0
- package/src/verify.mjs +347 -0
- package/src/visibility.mjs +40 -10
package/src/visibility.mjs
CHANGED
|
@@ -241,7 +241,7 @@ const bare = (h) => String(h || '').toLowerCase().replace(/^www\./, '');
|
|
|
241
241
|
|
|
242
242
|
/** Файлы и служебные разделы: это не страницы, по которым судят о сайте. */
|
|
243
243
|
const FILE_EXT = /\.(?!html?$|php$|aspx?$)[a-z0-9]{2,5}$/i;
|
|
244
|
-
const UTILITY = /\/(cart|basket|checkout|wishlist|compare|my-account|account|login|log-in|signin|sign-in|register|logout|wp-admin|wp-login\.php|wp-json|feed|rss|tag|tags|author|search|cdn-cgi)(\/|$)/i;
|
|
244
|
+
const UTILITY = /\/(cart|basket|checkout|wishlist|compare|my-account|account|login|customer_authentication|customer_identity|log-in|signin|sign-in|register|logout|wp-admin|wp-login\.php|wp-json|feed|rss|tag|tags|author|search|cdn-cgi)(\/|$)/i;
|
|
245
245
|
|
|
246
246
|
/**
|
|
247
247
|
* Свои ссылки страницы в порядке появления, без повторов.
|
|
@@ -249,6 +249,14 @@ const UTILITY = /\/(cart|basket|checkout|wishlist|compare|my-account|account|log
|
|
|
249
249
|
* @param {string} baseUrl адрес страницы, от него считаются относительные ссылки
|
|
250
250
|
* @param {Iterable<string>} [ownHosts] хосты сайта; по умолчанию хост baseUrl, с www и без
|
|
251
251
|
*/
|
|
252
|
+
/** Адреса из простого текста или markdown. Хвостовые знаки препинания, обратные кавычки и
|
|
253
|
+
* звёздочки к адресу не относятся: `https://x.com/a`. или (https://x.com/b), в конце строки. */
|
|
254
|
+
export function linksInText(text) {
|
|
255
|
+
return [...String(text || '').matchAll(/https?:\/\/[^\s)\]>"'`<]+/g)]
|
|
256
|
+
.map((m) => m[0].replace(/[.,;:!?*_'"`]+$/, ''))
|
|
257
|
+
.filter((u) => { try { return Boolean(new URL(u).host); } catch { return false; } });
|
|
258
|
+
}
|
|
259
|
+
|
|
252
260
|
export function internalLinks(html, baseUrl, ownHosts = []) {
|
|
253
261
|
let base;
|
|
254
262
|
try { base = new URL(baseUrl); } catch { return []; }
|
|
@@ -531,13 +539,29 @@ async function readSitemap(origin, robotsText, timeoutFn = () => 3000) {
|
|
|
531
539
|
const r = await get(u, { timeout: timeoutFn() });
|
|
532
540
|
if (r.error === 'timeout') { timedOut = true; continue; }
|
|
533
541
|
if (!r.ok || !/<(urlset|sitemapindex)/.test(r.text)) continue;
|
|
534
|
-
|
|
542
|
+
const locsOf = (text) => [...text.matchAll(/<loc>\s*([^<\s]+)\s*<\/loc>/g)].map((m) => m[1].replace(/&/g, '&'));
|
|
543
|
+
const pagesOf = (list) => list.filter((l) => { try { const p = new URL(l).pathname; return !FILE_EXT.test(p) && !UTILITY.test(p); } catch { return false; } });
|
|
544
|
+
let locs = locsOf(r.text);
|
|
535
545
|
let lastmod = /<lastmod>/.test(r.text);
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
546
|
+
/*
|
|
547
|
+
* Из индекса берём первую вложенную карту, в которой есть страницы. До 0.33.4 брали просто
|
|
548
|
+
* первую, а у Shopify это sitemap_agentic_discovery.xml с одним agents.md: в выборку шёл файл
|
|
549
|
+
* не HTML, он считался «не ответившей страницей», и балл не считался вовсе (brainmd.com,
|
|
550
|
+
* 12.10.2026). Смотрим не больше трёх вложенных карт.
|
|
551
|
+
*/
|
|
552
|
+
if (/<sitemapindex/.test(r.text) && locs.length) {
|
|
553
|
+
const children = locs;
|
|
554
|
+
for (const childUrl of children.slice(0, 3)) {
|
|
555
|
+
if (timeoutFn() <= 500) break;
|
|
556
|
+
const child = await get(childUrl, { timeout: timeoutFn() });
|
|
557
|
+
if (!child.ok) continue;
|
|
558
|
+
const childLocs = locsOf(child.text);
|
|
559
|
+
lastmod = lastmod || /<lastmod>/.test(child.text);
|
|
560
|
+
if (pagesOf(childLocs).length) { locs = childLocs; break; }
|
|
561
|
+
}
|
|
562
|
+
if (locs === children) locs = [];
|
|
539
563
|
}
|
|
540
|
-
return { found: true, url: u, count: locs.length, lastmod, pages: locs
|
|
564
|
+
return { found: true, url: u, count: locs.length, lastmod, pages: pagesOf(locs) };
|
|
541
565
|
}
|
|
542
566
|
return { found: false, url: '', count: 0, lastmod: false, pages: [], timedOut };
|
|
543
567
|
}
|
|
@@ -672,7 +696,7 @@ export async function checkVisibility(input, { budgetMs = 8500, lang = 'en', sam
|
|
|
672
696
|
* сайт без карты мерился по одной главной. Запросов не добавляет, главная уже прочитана.
|
|
673
697
|
* Добавлено 30.09.2026.
|
|
674
698
|
*/
|
|
675
|
-
const fromSitemap = sitemap.pages.filter(isOwn);
|
|
699
|
+
const fromSitemap = sitemap.pages.filter(isOwn).filter((u) => { try { const p = new URL(u).pathname; return !FILE_EXT.test(p) && !UTILITY.test(p); } catch { return false; } });
|
|
676
700
|
const allowedByRobots = robotsAllows(robotsRes.ok ? robotsRes.text : '');
|
|
677
701
|
const fromHome = fromSitemap.length > samplePages ? [] : internalLinks(home.text, home.url, [host]).filter((l) => allowedByRobots(l));
|
|
678
702
|
const poolSeen = new Set();
|
|
@@ -713,9 +737,15 @@ export async function checkVisibility(input, { budgetMs = 8500, lang = 'en', sam
|
|
|
713
737
|
}
|
|
714
738
|
const read = async (list) => Promise.all(list.map((p) => get(p, { timeout: Math.min(4000, left() - 300) })));
|
|
715
739
|
let rs = await read(picks);
|
|
740
|
+
/*
|
|
741
|
+
* Адрес, который ответил 200, но не HTML (markdown, PDF, JSON), это не «страница не ответила»,
|
|
742
|
+
* а не страница вовсе. Повторять его бессмысленно, и в счёт выборки он не идёт: иначе один
|
|
743
|
+
* agents.md в карте Shopify отменял балл всему сайту (12.10.2026).
|
|
744
|
+
*/
|
|
745
|
+
const notPages = new Set(picks.filter((p, i) => rs[i].ok && !/html/i.test(rs[i].type)));
|
|
716
746
|
// Одна повторная попытка по тем, что не ответили: страницы маленькие, а разница в балле
|
|
717
747
|
// между «прочитали две» и «прочитали одну» доходила до двадцати пунктов.
|
|
718
|
-
const failed = picks.filter((p, i) => !(rs[i].ok && /html/i.test(rs[i].type)));
|
|
748
|
+
const failed = picks.filter((p, i) => !notPages.has(p) && !(rs[i].ok && /html/i.test(rs[i].type)));
|
|
719
749
|
if (failed.length) {
|
|
720
750
|
// Повтору даётся своё время сверх общего бюджета, один раз и не больше трёх секунд.
|
|
721
751
|
// Без этого на медленном сайте повтор упирался в тот же истёкший бюджет и проверка
|
|
@@ -732,13 +762,13 @@ export async function checkVisibility(input, { budgetMs = 8500, lang = 'en', sam
|
|
|
732
762
|
// Дубли по адресу: повтор мог вернуть ту же страницу, что и первая попытка.
|
|
733
763
|
const seenUrl = new Set();
|
|
734
764
|
sampled = sampled.filter((p) => (seenUrl.has(p.url) ? false : seenUrl.add(p.url)));
|
|
735
|
-
wantedPages = Math.min(picks.length, samplePages);
|
|
765
|
+
wantedPages = Math.min(picks.length - notPages.size, samplePages);
|
|
736
766
|
}
|
|
737
767
|
const pages = [homePage, ...sampled];
|
|
738
768
|
|
|
739
769
|
// llms.txt
|
|
740
770
|
const llmsIsText = llmsRes.ok && !/<html/i.test(llmsRes.text.slice(0, 400));
|
|
741
|
-
const llmsLinks = llmsIsText ?
|
|
771
|
+
const llmsLinks = llmsIsText ? linksInText(llmsRes.text) : [];
|
|
742
772
|
const llmsForeign = llmsLinks.filter((l) => { try { return !new URL(l).host.endsWith(host.replace(/^www\./, '')); } catch { return false; } });
|
|
743
773
|
|
|
744
774
|
// Area 1: access for AI crawlers (25)
|