@operstack/audit 0.19.0 → 0.19.1
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 +1 -1
- package/src/test-unmeasured.mjs +31 -2
- package/src/visibility.mjs +16 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operstack/audit",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.1",
|
|
4
4
|
"description": "SEO, AEO and GEO audit generator: collect public signals from a site into audit.json, let an analyst or an agent fill the narrative, render the OperStack report as HTML and PDF.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/test-unmeasured.mjs
CHANGED
|
@@ -27,9 +27,10 @@ const is = (n, a, b) => ok(`${n} (ждали ${JSON.stringify(b)}, вышло ${
|
|
|
27
27
|
const PAGE = `<!doctype html><html lang="ru"><head><title>Пример</title><meta name="description" content="Описание"></head>
|
|
28
28
|
<body><main><h1>Пример сайта</h1>${'<p>Обычный абзац текста про товар и цену, без цифр, чтобы было что цитировать. </p>'.repeat(40)}</main></body></html>`;
|
|
29
29
|
|
|
30
|
-
const serve = ({ robots }) => new Promise((resolve) => {
|
|
30
|
+
const serve = ({ robots, status = null }) => new Promise((resolve) => {
|
|
31
31
|
const s = createServer((req, res) => {
|
|
32
32
|
if (req.url.startsWith('/robots.txt')) {
|
|
33
|
+
if (status) { res.writeHead(status, { 'content-type': 'text/html' }); res.end('<html>нет</html>'); return; }
|
|
33
34
|
if (robots === null) { res.writeHead(403, { 'content-type': 'text/html' }); res.end('<html>denied</html>'); return; }
|
|
34
35
|
res.writeHead(200, { 'content-type': 'text/plain' }); res.end(robots); return;
|
|
35
36
|
}
|
|
@@ -99,5 +100,33 @@ if (!dnsOk) {
|
|
|
99
100
|
ok('подпись в отчёте прежняя', /дают в сумме ровно/.test((toHtml({ ...fixture, meta: { ...fixture.meta, lang: 'ru' }, overall: { ...fixture.overall, score: v2.score, areas: v2.areas } }, null).match(/overall-note">([^<]*)</) || [])[1] || ''));
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
// ---- файла нет это ОТВЕТ, а не незнание
|
|
104
|
+
//
|
|
105
|
+
// Первая версия правки считала неизмеренным любой неуспех, и сайт без robots.txt терял 25
|
|
106
|
+
// заслуженных баллов: по стандарту отсутствие файла означает, что не запрещено ничего.
|
|
107
|
+
// Поймано на живом example.com сразу после выкладки: балл упал с 30 до 7.
|
|
108
|
+
for (const code of [404, 410]) {
|
|
109
|
+
const { s: s3, url: url3 } = await serve({ status: code });
|
|
110
|
+
let v3;
|
|
111
|
+
try { v3 = await checkVisibility(url3, { budgetMs: 8500, samplePages: 3, lang: 'ru' }); } finally { s3.close(); }
|
|
112
|
+
const a3 = v3.areas.find((a) => a.id === 'access');
|
|
113
|
+
is(`robots.txt ${code}: область измерена`, a3.measured, true);
|
|
114
|
+
is(`robots.txt ${code}: и оценена полностью`, a3.score, 25);
|
|
115
|
+
ok(`robots.txt ${code}: сказано, что файла нет`, /Файла robots\.txt на сайте нет/.test(a3.findings[0].text));
|
|
116
|
+
ok(`robots.txt ${code}: и что это значит «не запрещено ничего»`, /не запрещено ничего/.test(a3.findings[0].text));
|
|
117
|
+
ok(`robots.txt ${code}: не назвали это непрочитанным`, !a3.findings.some((f) => f.id === 'robots-unreadable'));
|
|
118
|
+
is(`robots.txt ${code}: балл это прямая сумма`, v3.score, v3.areas.reduce((t, a) => t + a.score, 0));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- а отказ и сбой это по-прежнему незнание
|
|
122
|
+
for (const code of [401, 403, 429, 500]) {
|
|
123
|
+
const { s: s4, url: url4 } = await serve({ status: code });
|
|
124
|
+
let v4;
|
|
125
|
+
try { v4 = await checkVisibility(url4, { budgetMs: 8500, samplePages: 3, lang: 'ru' }); } finally { s4.close(); }
|
|
126
|
+
const a4 = v4.areas.find((a) => a.id === 'access');
|
|
127
|
+
is(`robots.txt ${code}: область не измерена`, a4.measured, false);
|
|
128
|
+
ok(`robots.txt ${code}: код назван в тексте`, new RegExp(`код ${code}`).test(a4.findings[0].text));
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
if (bad) { console.error(`\n${bad} тест(ов) упало`); process.exit(1); }
|
|
103
|
-
console.log('\nнепрочитанное не считается ни в плюс, ни в
|
|
132
|
+
console.log('\nнепрочитанное не считается ни в плюс, ни в минус, а отсутствующее это ответ');
|
package/src/visibility.mjs
CHANGED
|
@@ -58,6 +58,7 @@ export const MESSAGES = {
|
|
|
58
58
|
rulesUnknown: ' Its robots.txt could not be read either, so there is nothing to say about AI crawlers: that is not counted for or against the site.',
|
|
59
59
|
howToCheckBots: ' To know for certain, open the server log and look for GPTBot, ClaudeBot and PerplexityBot, or ask whoever runs the protection which bots are on the allowlist.',
|
|
60
60
|
area: { access: 'Can AI crawlers read it', index: 'Is there a map for agents (llms.txt)', entity: 'Is the entity clear (schema)', content: 'Is there something to quote', trust: 'Can it be dated and trusted' },
|
|
61
|
+
robotsMissing: 'There is no robots.txt on the site. By the standard that means nothing is disallowed, so every AI crawler may read it. A file is not required; add one only when something needs closing off.',
|
|
61
62
|
robotsUnreadable: (status) => `The site did not let this check read robots.txt${status ? ` (HTTP ${status})` : ''}, so whether AI crawlers are allowed could not be measured. It is not counted for or against the site: an unread file is not good news. The owner can open it in a browser, or ask whoever runs the site's protection.`,
|
|
62
63
|
robotsAllBlocked: 'robots.txt disallows the whole site for every crawler. Nothing can read it.',
|
|
63
64
|
fetchersBlocked: (list) => `Blocked answer-engine fetchers: ${list}. These are the bots that cite pages live.`,
|
|
@@ -104,6 +105,7 @@ export const MESSAGES = {
|
|
|
104
105
|
rulesUnknown: ' Файл robots.txt прочитать тоже не вышло, поэтому про роботов ИИ сказать нечего, и в плюс или в минус сайту это не зачтено.',
|
|
105
106
|
howToCheckBots: ' Чтобы знать точно, посмотрите в журнале сервера, приходят ли GPTBot, ClaudeBot и PerplexityBot, либо спросите у тех, кто настраивал защиту, кто у неё в белом списке.',
|
|
106
107
|
area: { access: 'Могут ли роботы ИИ прочитать сайт', index: 'Есть ли карта для агентов (llms.txt)', entity: 'Понятно ли, кто вы (разметка)', content: 'Есть ли что процитировать', trust: 'Можно ли датировать и доверять' },
|
|
108
|
+
robotsMissing: 'Файла robots.txt на сайте нет. По стандарту это значит, что не запрещено ничего, то есть читать сайт может любой робот ИИ. Заводить файл необязательно: он нужен, только когда есть что закрывать.',
|
|
107
109
|
robotsUnreadable: (status) => `Сайт не дал этой проверке прочитать robots.txt${status ? ` (код ${status})` : ''}, поэтому допущены роботы ИИ или нет, измерить не вышло. В плюс или в минус сайту это не зачтено: непрочитанный файл это не хорошая новость. Владелец может открыть его в браузере сам или спросить у тех, кто настраивал защиту сайта.`,
|
|
108
110
|
robotsAllBlocked: 'robots.txt закрывает весь сайт для всех роботов. Его никто не может прочитать.',
|
|
109
111
|
fetchersBlocked: (list) => `Закрыты поисковые роботы ответных систем: ${list}. Именно они достают страницу, чтобы процитировать её в ответе.`,
|
|
@@ -439,12 +441,25 @@ export async function checkVisibility(input, { budgetMs = 8500, lang = 'en', sam
|
|
|
439
441
|
* Незнание это не хорошая новость. Область помечается неизмеренной, из знаменателя выпадает,
|
|
440
442
|
* и балл считается по тому, что действительно прочитано.
|
|
441
443
|
*/
|
|
442
|
-
|
|
444
|
+
/*
|
|
445
|
+
* «Файла нет» и «нам его не дали» это разные вещи, и путать их нельзя ни в одну сторону.
|
|
446
|
+
*
|
|
447
|
+
* 404 и 410 это определённый ответ: файла на сайте нет, а по стандарту отсутствие robots.txt
|
|
448
|
+
* означает, что не запрещено ничего. Это измерение, и область получает свои баллы честно.
|
|
449
|
+
* Первая версия правки 15.09.2026 считала неизмеренным любой неуспех, и сайт без robots.txt
|
|
450
|
+
* (таких много среди небольших) терял 25 заслуженных баллов: example.com упал с 30 до 7.
|
|
451
|
+
*
|
|
452
|
+
* Неизмеренное это только отказ и сбой: 401, 403, 405, 429, пятисотые, таймаут, недоступность.
|
|
453
|
+
* Тогда мы действительно не знаем, что в файле написано.
|
|
454
|
+
*/
|
|
455
|
+
const robotsMissing = robotsRes.status === 404 || robotsRes.status === 410;
|
|
456
|
+
const robotsRead = robotsRes.ok || robotsMissing;
|
|
443
457
|
let access = 25;
|
|
444
458
|
if (starBlocked) access = 0; else { access -= Math.min(15, blockedSearch.length * 4); access -= Math.min(8, blockedTrain.length * 2); if (noai) access -= 5; }
|
|
445
459
|
access = Math.max(0, access);
|
|
446
460
|
const accessFindings = [];
|
|
447
461
|
if (!robotsRead) accessFindings.push({ id: 'robots-unreadable', level: 'na', text: T.robotsUnreadable(robotsRes.status || 0) });
|
|
462
|
+
else if (robotsMissing && !accessFindings.length) accessFindings.push({ level: 'pass', text: T.robotsMissing });
|
|
448
463
|
if (starBlocked) accessFindings.push({ id: 'robots-all-blocked', level: 'fail', text: T.robotsAllBlocked });
|
|
449
464
|
if (blockedSearch.length) accessFindings.push({ id: 'robots-fetchers-blocked', level: 'fail', text: T.fetchersBlocked(blockedSearch.map(agentLabel).join(', ')) });
|
|
450
465
|
if (blockedTrain.length) accessFindings.push({ id: 'robots-training-blocked', level: 'warn', text: T.trainingBlocked(blockedTrain.map(agentLabel).join(', ')) });
|