@operstack/audit 0.17.0 → 0.17.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operstack/audit",
3
- "version": "0.17.0",
3
+ "version": "0.17.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/collect.mjs CHANGED
@@ -207,7 +207,7 @@ async function readSitemap(startUrl, { maxFiles = 6, budgetMs = 45000, maxUrls =
207
207
 
208
208
  export const __readSitemap = readSitemap;
209
209
 
210
- export async function collect(startUrl, { pages = 20, log = () => {}, backlinks = null, lang = 'en', rendered = false, renderedPages = 3, preparedBy = '', requestTimeout = 15000, sitemapBudgetMs = 45000, sitemapMaxFiles = 6 } = {}) {
210
+ export async function collect(startUrl, { pages = 20, log = () => {}, backlinks = null, lang = 'en', rendered = false, renderedPages = 3, preparedBy = '', visibility: givenVisibility = null, requestTimeout = 15000, sitemapBudgetMs = 45000, sitemapMaxFiles = 6 } = {}) {
211
211
  REQUEST_TIMEOUT = requestTimeout;
212
212
  const opts = { backlinks };
213
213
  const origin = new URL(startUrl).origin;
@@ -483,11 +483,26 @@ export async function collect(startUrl, { pages = 20, log = () => {}, backlinks
483
483
  * тут больше страниц или времени, и число снова разойдётся со страницей. Глубина аудита живёт в
484
484
  * его собственных шести областях и в списке задач, а не в этом числе.
485
485
  */
486
- log('visibility score');
487
- const visibility = await checkVisibility(startUrl, { ...VISIBILITY_DEFAULTS, lang });
488
- const overall = visibility.ok
489
- ? { score: visibility.score, grade: visibility.grade, areas: visibility.areas, source: 'visibility', ms: visibility.ms }
490
- : { score: null, grade: 'not measured', areas: [], source: 'visibility', error: visibility.error };
486
+ /*
487
+ * Если балл уже измерен, второй раз не меряем.
488
+ *
489
+ * Живая сверка 14.09.2026 на whitewill.ru дала 47 и 44 в двух прогонах подряд, а на habr.com
490
+ * один прогон вернул отказ. Код один и тот же, разная только удача сети: бюджет у проверки
491
+ * 8,5 секунды, и на медленном сайте карта сайта то успевает прочитаться, то нет, а движок
492
+ * честно не штрафует за непрочитанное. Два измерения одного сайта дают два числа, и покупателю
493
+ * всё равно, что оба честные: он видит 44 на странице и 47 в письме.
494
+ *
495
+ * Поэтому в воронке балл измеряется РОВНО ОДИН РАЗ, на странице, и приезжает сюда готовым
496
+ * через опцию `visibility`. Тогда страница, письмо и отчёт несут одно число по построению,
497
+ * а не по совпадению. Меряем сами только когда аудит запускают отдельно и мерить больше некому.
498
+ */
499
+ let visibility = givenVisibility;
500
+ if (visibility) log('visibility score: taken from the check the visitor already ran');
501
+ else { log('visibility score'); visibility = await checkVisibility(startUrl, { ...VISIBILITY_DEFAULTS, lang }); }
502
+ const measuredAt = visibility && visibility.checkedAt ? visibility.checkedAt : null;
503
+ const overall = visibility && visibility.ok
504
+ ? { score: visibility.score, grade: visibility.grade, areas: visibility.areas, source: givenVisibility ? 'visibility:reused' : 'visibility', measuredAt, ms: visibility.ms }
505
+ : { score: null, grade: 'not measured', areas: [], source: givenVisibility ? 'visibility:reused' : 'visibility', error: (visibility && visibility.error) || 'no visibility result' };
491
506
  // Шесть областей отчёта остаются, но это другое измерение, а не разбивка общего балла.
492
507
  const reportScore = computeOverall(checks);
493
508
 
@@ -107,6 +107,46 @@ ok('параметры нельзя поменять на ходу', Object.isFr
107
107
  }
108
108
  }
109
109
 
110
+ /*
111
+ * Балл, уже измеренный на странице, приезжает в отчёт готовым и не перемеряется.
112
+ *
113
+ * Живая сверка 14.09.2026: whitewill.ru дал 47 и 44 в двух прогонах подряд, habr.com в одном
114
+ * прогоне вернул отказ. Тот же код, разная удача сети: бюджет 8,5 секунды, и карта сайта то
115
+ * успевает прочитаться, то нет. Покупателю всё равно, что оба числа честные, он видит два разных.
116
+ * Поэтому в воронке измерение происходит один раз.
117
+ */
118
+ {
119
+ const server = createServer((req, res) => {
120
+ res.setHeader('content-type', 'text/html');
121
+ res.end('<!doctype html><html><head><title>Local</title></head><body><h1>Local</h1><p>Text.</p></body></html>');
122
+ });
123
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
124
+ const base = `http://127.0.0.1:${server.address().port}/`;
125
+
126
+ const fromPage = { ok: true, score: 63, grade: 'B', checkedAt: '2026-09-14T20:00:00.000Z', ms: 4200, areas: [
127
+ { id: 'access', label: 'Access for AI crawlers', score: 25, max: 25 },
128
+ { id: 'index', label: 'Agent index', score: 10, max: 15 },
129
+ { id: 'entity', label: 'Entity and structure', score: 12, max: 20 },
130
+ { id: 'content', label: 'Answer-first content', score: 11, max: 25 },
131
+ { id: 'trust', label: 'Freshness and sources', score: 5, max: 15 }] };
132
+
133
+ const audit = await collect(base, { pages: 2, log: () => {}, visibility: fromPage });
134
+ server.close();
135
+
136
+ is('готовый балл берётся как есть', audit.overall.score, 63);
137
+ is('и оценка буквой тоже', audit.overall.grade, 'B');
138
+ is('видно, что балл переиспользован, а не измерен заново', audit.overall.source, 'visibility:reused');
139
+ is('сохранено, когда его измерили', audit.overall.measuredAt, '2026-09-14T20:00:00.000Z');
140
+ is('пять областей приехали целиком', audit.overall.areas.length, 5);
141
+ is('и складываются в заголовок', audit.overall.areas.reduce((a, x) => a + x.score, 0), 63);
142
+ is('отчёт сходится сам с собой', verifyScores(audit), []);
143
+ // Локальный адрес движок бы мерить отказался, а балл всё равно есть: значит второго измерения не было.
144
+ ok('второго измерения не случилось', audit.overall.score === 63 && !audit.overall.error);
145
+
146
+ audit.meta.lang = 'ru';
147
+ is('и отчёт печатает именно его', Number((toHtml(audit, null).match(/overall-num[^>]*>(\d+)<span>/) || [])[1]), 63);
148
+ }
149
+
110
150
  // ---- движок в пакете это та же программа, что стоит на сайтах
111
151
  {
112
152
  const here = readFileSync(new URL('./visibility.mjs', import.meta.url), 'utf8');