@operstack/audit 0.27.0 → 0.28.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/visibility.mjs +22 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operstack/audit",
3
- "version": "0.27.0",
3
+ "version": "0.28.0",
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",
@@ -50,24 +50,27 @@ const AI_AGENTS = [
50
50
  * измерили, и под каждым движком видно, из чего оно сложилось. Это принципиально: конкурент
51
51
  * показывает такие числа без объяснения, откуда они, и мы за это его же и критикуем.
52
52
  *
53
- * Почему веса у движков разные. ChatGPT, Perplexity и Claude достают страницу живьём в момент
54
- * ответа, поэтому у них решает доступ их собственным роботам и то, есть ли что процитировать.
55
- * Gemini и Copilot отвечают из чужого индекса, Google и Bing, поэтому у них сильнее весит
56
- * разметка: в индексе страницу сначала надо понять. Веса названы здесь и больше нигде, чтобы
57
- * их нельзя было тихо поменять в двух местах по-разному.
53
+ * Формула здесь ТА ЖЕ, что у общего балла, и это принципиально. В первой версии я дал двум
54
+ * группам движков разные веса: у тех, кто достаёт страницу живьём, тяжелее доступ, у тех, кто
55
+ * отвечает из индекса, тяжелее разметка. Обоснование звучало складно, но замером не было: это
56
+ * было моё допущение, то есть ровно то, за что мы критикуем конкурента, который показывает такие
57
+ * числа без объяснения, откуда они.
58
+ *
59
+ * Поэтому веса взяты один в один из общего балла, а отличаются движки друг от друга только одним
60
+ * и настоящим: что robots.txt отвечает ИХ собственным роботам. У сайта, который никого не закрыл,
61
+ * все пять чисел совпадут с общим баллом, и это правда, а не скучная правда вместо красивой лжи.
58
62
  *
59
63
  * Неизмеренная область в расчёт не идёт вовсе, и оставшиеся веса делятся заново. Правило то же,
60
64
  * что и у общего балла: не измерили, значит не считаем ни в плюс, ни в минус.
61
65
  */
62
- const LIVE_MIX = Object.freeze({ access: 0.45, content: 0.30, trust: 0.15, entity: 0.10 });
63
- const INDEX_MIX = Object.freeze({ access: 0.30, entity: 0.30, content: 0.25, trust: 0.15 });
66
+ const SCORE_MIX = Object.freeze({ access: 0.25, index: 0.15, entity: 0.20, content: 0.25, trust: 0.15 });
64
67
 
65
68
  const ENGINES = [
66
- { id: 'chatgpt', label: 'ChatGPT', agents: ['oai-searchbot', 'chatgpt-user'], mix: LIVE_MIX, live: true },
67
- { id: 'perplexity', label: 'Perplexity', agents: ['perplexitybot', 'perplexity-user'], mix: LIVE_MIX, live: true },
68
- { id: 'claude', label: 'Claude', agents: ['claude-searchbot', 'claude-user'], mix: LIVE_MIX, live: true },
69
- { id: 'gemini', label: 'Gemini', agents: ['google-extended'], mix: INDEX_MIX, live: false },
70
- { id: 'copilot', label: 'Copilot', agents: ['bingbot'], mix: INDEX_MIX, live: false },
69
+ { id: 'chatgpt', label: 'ChatGPT', agents: ['oai-searchbot', 'chatgpt-user'] },
70
+ { id: 'perplexity', label: 'Perplexity', agents: ['perplexitybot', 'perplexity-user'] },
71
+ { id: 'claude', label: 'Claude', agents: ['claude-searchbot', 'claude-user'] },
72
+ { id: 'gemini', label: 'Gemini', agents: ['google-extended'] },
73
+ { id: 'copilot', label: 'Copilot', agents: ['bingbot'] },
71
74
  ];
72
75
 
73
76
  /**
@@ -96,8 +99,6 @@ export const MESSAGES = {
96
99
  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.',
97
100
  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.',
98
101
  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' },
99
- engineLive: 'fetches the page live when it answers',
100
- engineIndex: 'answers from an index, so schema weighs more',
101
102
  llmsUnreadable: (status) => `The site did not let this check read llms.txt${status ? ` (HTTP ${status})` : ''}, so whether the site has a map for agents could not be measured. It is not counted for or against the site: a file we were refused is not the same as a file that is not there.`,
102
103
  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.',
103
104
  sampleShort: (read, wanted) => `Only ${read} of ${wanted} pages answered in time, twice in a row, so there was not enough of the site to score. We do not show a number here: a score that goes up because we read less is a score nobody can defend. The site may simply be slow right now; run the check again in a minute.`,
@@ -149,8 +150,6 @@ export const MESSAGES = {
149
150
  rulesUnknown: ' Файл robots.txt прочитать тоже не вышло, поэтому про роботов ИИ сказать нечего, и в плюс или в минус сайту это не зачтено.',
150
151
  howToCheckBots: ' Чтобы знать точно, посмотрите в журнале сервера, приходят ли GPTBot, ClaudeBot и PerplexityBot, либо спросите у тех, кто настраивал защиту, кто у неё в белом списке.',
151
152
  area: { access: 'Могут ли роботы ИИ прочитать сайт', index: 'Есть ли карта для агентов (llms.txt)', entity: 'Понятно ли, кто вы (разметка)', content: 'Есть ли что процитировать', trust: 'Можно ли датировать и доверять' },
152
- engineLive: 'достаёт страницу живьём в момент ответа',
153
- engineIndex: 'отвечает из индекса, поэтому разметка весит больше',
154
153
  llmsUnreadable: (status) => `Сайт не дал этой проверке прочитать llms.txt${status ? ` (код ${status})` : ''}, поэтому есть ли у сайта карта для агентов, измерить не вышло. В плюс или в минус это не зачтено: файл, который нам не отдали, это не то же самое, что файла нет.`,
155
154
  robotsMissing: 'Файла robots.txt на сайте нет. По стандарту это значит, что не запрещено ничего, то есть читать сайт может любой робот ИИ. Заводить файл необязательно: он нужен, только когда есть что закрывать.',
156
155
  sampleShort: (read, wanted) => `Из ${wanted} страниц ответили ${read}, и со второй попытки тоже, поэтому считать балл не по чему. Числа здесь не будет: балл, который растёт оттого, что мы прочитали меньше, защитить нельзя. Возможно, сайт сейчас просто медленный: запустите проверку через минуту.`,
@@ -370,15 +369,16 @@ export function engineReadiness(verdicts, parts, T, label = (v) => v.label) {
370
369
  const known = own.filter((v) => v.verdict !== 'unknown');
371
370
  const share = {
372
371
  // Доступ считается по роботам ЭТОГО движка, а не по всем четырнадцати: закрытый Applebot
373
- // не мешает ChatGPT процитировать страницу.
372
+ // не мешает ChatGPT процитировать страницу. Это единственное, чем движки тут отличаются.
374
373
  access: known.length ? known.filter((v) => v.verdict !== 'blocked').length / known.length : null,
374
+ index: parts.index,
375
375
  entity: parts.entity,
376
376
  content: parts.content,
377
377
  trust: parts.trust,
378
378
  };
379
379
  let sum = 0;
380
380
  let weight = 0;
381
- for (const [key, w] of Object.entries(e.mix)) {
381
+ for (const [key, w] of Object.entries(SCORE_MIX)) {
382
382
  if (share[key] === null || share[key] === undefined) continue;
383
383
  sum += share[key] * w;
384
384
  weight += w;
@@ -386,7 +386,7 @@ export function engineReadiness(verdicts, parts, T, label = (v) => v.label) {
386
386
  const score = weight ? Math.round((sum / weight) * 100) : null;
387
387
  const blocked = own.filter((v) => v.verdict === 'blocked').map(label);
388
388
  // Что тянет вниз: самая слабая из измеренных областей, кроме доступа, о нём сказано отдельно.
389
- const areasByWeak = [['content', share.content], ['trust', share.trust], ['entity', share.entity]]
389
+ const areasByWeak = [['content', share.content], ['trust', share.trust], ['entity', share.entity], ['index', share.index]]
390
390
  .filter(([, v]) => v !== null && v !== undefined)
391
391
  .sort((a, b) => a[1] - b[1]);
392
392
  const weakest = areasByWeak.length && areasByWeak[0][1] < 0.999 ? T.area[areasByWeak[0][0]] : null;
@@ -394,7 +394,8 @@ export function engineReadiness(verdicts, parts, T, label = (v) => v.label) {
394
394
  id: e.id,
395
395
  label: e.label,
396
396
  score,
397
- how: e.live ? T.engineLive : T.engineIndex,
397
+ // Вместо рассуждений о природе движка говорим факт: чьи роботы спрошены.
398
+ how: own.map(label).join(', '),
398
399
  blocked,
399
400
  weakest,
400
401
  };
@@ -817,6 +818,7 @@ export async function checkVisibility(input, { budgetMs = 8500, lang = 'en', sam
817
818
  crawlers: verdicts.map((v) => ({ label: agentLabel(v), kind: v.kind, verdict: v.verdict })),
818
819
  engines: engineReadiness(verdicts, {
819
820
  access: null, // считается внутри по роботам каждого движка
821
+ index: llmsRead ? index / 15 : null,
820
822
  entity: entity / 20,
821
823
  content: sampleOk ? content / 25 : null,
822
824
  trust: sampleOk ? trust / 15 : null,