@operstack/audit 0.21.4 → 0.22.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@operstack/audit",
3
- "version": "0.21.4",
3
+ "version": "0.22.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",
@@ -29,7 +29,7 @@
29
29
  "LICENSE"
30
30
  ],
31
31
  "scripts": {
32
- "test": "node src/test-scores.mjs && node src/test-fix.mjs && node src/test-foundation.mjs && node src/test-narrative.mjs && node src/test-prompts.mjs && node src/test-sitemap.mjs && node src/test-overall.mjs && node src/test-blocked.mjs && node src/test-unmeasured.mjs && node src/test-rendered.mjs && node src/test-sample.mjs && node src/test-demand.mjs && node src/test-agent-surface.mjs && node src/test-agency.mjs && node src/test-i18n.mjs examples/sample-audit.json && node bin/audit.mjs check examples/sample-audit.json && node bin/audit.mjs render examples/sample-audit.json --out /tmp/operstack-audit-sample.html",
32
+ "test": "node src/test-scores.mjs && node src/test-fix.mjs && node src/test-foundation.mjs && node src/test-narrative.mjs && node src/test-prompts.mjs && node src/test-sitemap.mjs && node src/test-overall.mjs && node src/test-blocked.mjs && node src/test-unmeasured.mjs && node src/test-rendered.mjs && node src/test-sample.mjs && node src/test-demand.mjs && node src/test-agent-surface.mjs && node src/test-agency.mjs && node src/test-i18n.mjs examples/sample-audit.json && node bin/audit.mjs check examples/sample-audit.json && node bin/audit.mjs render examples/sample-audit.json --out /tmp/operstack-audit-sample.html && node src/test-ru-sources.mjs",
33
33
  "sample": "node bin/audit.mjs render examples/sample-audit.json --pdf",
34
34
  "verify:live": "node src/verify-live.mjs"
35
35
  },
package/src/collect.mjs CHANGED
@@ -52,6 +52,13 @@ async function follow(url, max = 5) {
52
52
 
53
53
  const row = (id, group, label, status, value, comment = '') => ({ id, group, label, status, value, comment });
54
54
 
55
+ /**
56
+ * Чем в тексте называют источник цифры, по-английски и по-русски. Список слово в слово совпадает
57
+ * с SOURCE_RE в visibility.mjs: два движка должны считать одно и то же, иначе балл в бесплатной
58
+ * проверке и в платном отчёте разойдётся на ровном месте.
59
+ */
60
+ const SOURCE_RE = /(?<!\p{L})(according to|source:|sources:|data from|reported by|published by|registry|statistics office|central bank|по данным|источник:|источники:|согласно|по информации|по сведениям|росстат|центробанк|банк россии|росреестр|минфин|минэкономразвития)(?!\p{L})/giu;
61
+
55
62
  function analysePage(url, html) {
56
63
  const root = parse(html, { blockTextElements: { script: true, style: true, noscript: true } });
57
64
  const attr = (sel, a) => root.querySelector(sel)?.getAttribute(a) ?? '';
@@ -147,7 +154,13 @@ function analysePage(url, html) {
147
154
  const tables = (bodyHtml.match(/<table[\s>]/gi) || []).length;
148
155
  // A source is named either in words, or the way careful writing actually does it: a link to
149
156
  // somebody else's site sitting in the same paragraph as the figure it backs.
150
- const sourcePhrases = (text.match(/\b(according to|source:|sources:|data from|reported by|published by|registry|statistics office|central bank)\b/gi) || []).length;
157
+ // Слова, которыми называют источник цифры. Список и границы слова те же, что в движке
158
+ // бесплатной проверки (visibility.mjs, SOURCE_RE): до 16.09.2026 здесь были только английские
159
+ // выражения, и русский сайт, честно написавший «по данным Росстата», получал ноль. Живой
160
+ // пользователь сказал об этом прямо: на русском сайте эти баллы недостижимы.
161
+ // Обычная граница слова \b для кириллицы не работает, она построена на латинице, поэтому
162
+ // границы заданы через отсутствие буквы рядом.
163
+ const sourcePhrases = (text.match(SOURCE_RE) || []).length;
151
164
  const ownHostname = (() => { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; } })();
152
165
  // Абзацы, в которых вообще есть цифра. Без этого счётчика «цифры без источника» вменяется
153
166
  // странице, где цифр нет ни одной, например политике конфиденциальности.
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Русский сайт, который честно называет источник цифры, получает за это балл.
4
+ *
5
+ * 16.09.2026 живой пользователь сказал прямо: «поиск источника цифр англоязычный, на русском
6
+ * сайте эти пять баллов недостижимы честно». Так и было: в движке платного аудита список слов
7
+ * состоял только из английских выражений, хотя в движке бесплатной проверки русские уже были.
8
+ * Два движка считали одно и то же по-разному.
9
+ */
10
+ import { readFileSync } from 'node:fs';
11
+ import { resolve, dirname } from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const src = readFileSync(resolve(here, 'collect.mjs'), 'utf8');
16
+ const vis = readFileSync(resolve(here, 'visibility.mjs'), 'utf8');
17
+
18
+ let bad = 0;
19
+ const ok = (n, c) => { if (c) console.log(`ok ${n}`); else { bad++; console.error(`FAIL ${n}`); } };
20
+
21
+ const line = (text) => (text.match(/^const SOURCE_RE = .+$/m) || [''])[0];
22
+ ok('списки слов в обоих движках совпадают слово в слово', line(src) === line(vis) && line(src).length > 60);
23
+
24
+ const RE = new RegExp(line(src).replace(/^const SOURCE_RE = \//, '').replace(/\/giu;$/, ''), 'giu');
25
+ const count = (t) => (t.match(RE) || []).length;
26
+
27
+ for (const t of ['По данным Росстата, рынок вырос на 12%.',
28
+ 'Источник: отчёт Банка России за 2025 год.',
29
+ 'Согласно исследованию, доля выросла до 40%.',
30
+ 'По информации Минфина, ставка снижена.',
31
+ 'По сведениям Росреестра, сделок стало больше.'])
32
+ ok(`русский источник виден: ${t.slice(0, 34)}…`, count(t) > 0);
33
+
34
+ for (const t of ['According to Rosstat, the market grew 12%.', 'Source: Bank of Russia report.'])
35
+ ok(`английский по-прежнему виден: ${t.slice(0, 30)}…`, count(t) > 0);
36
+
37
+ for (const t of ['Мы выросли на 12% за год.', 'Наша команда работает с 2019 года.', 'Согласнее всех был директор.'])
38
+ ok(`пустая похвальба источником не считается: ${t.slice(0, 28)}…`, count(t) === 0);
39
+
40
+ console.log(bad ? `\nпровалов: ${bad}` : '\nвсё сходится');
41
+ process.exit(bad ? 1 : 0);