@operstack/audit 0.32.0 → 0.32.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/README.md +4 -2
- package/package.json +2 -2
- package/src/prospect.mjs +30 -6
- package/src/test-prospect-count.mjs +12 -0
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/oper-stack/brand/main/og-audit.png" alt="OperStack audit-generator" width="100%">
|
|
2
|
+
|
|
1
3
|
# @operstack/audit
|
|
2
4
|
|
|
3
5
|
The SEO, AEO and GEO audit generator behind the OperStack audit service. It collects the public signals of a site into one JSON file, leaves the narrative to an analyst or an agent, and renders a print-ready report.
|
|
@@ -90,10 +92,10 @@ Technically: `draft` fills the narrative fields (`summary`, `client.subject`, `o
|
|
|
90
92
|
**If you do not edit the site yourself.** Most people who read an audit now fix their site by telling an AI assistant what to do. This turns every finding into a task written for that assistant: copy it, paste it into Cursor or Claude Code, and it does the work. Each task says what the state is today, what to change, and how you will know it is finished, so the assistant cannot wander off and cannot quietly invent facts about your business.
|
|
91
93
|
|
|
92
94
|
```
|
|
93
|
-
npx @operstack/audit prompts audit.json --lang en
|
|
95
|
+
npx @operstack/audit prompts audit.json --lang en
|
|
94
96
|
```
|
|
95
97
|
|
|
96
|
-
Technically: every open check is rendered as a Markdown block with `Now` (the measured value), `Task` (the action and its boundaries) and `How to check` (the acceptance test), plus a standing rule against inventing business facts. Twenty checks carry text written for that specific finding; the rest fall back to the Fix action with a generic acceptance test. `--limit 3` is
|
|
98
|
+
Technically: every open check is rendered as a Markdown block with `Now` (the measured value), `Task` (the action and its boundaries) and `How to check` (the acceptance test), plus a standing rule against inventing business facts. Twenty checks carry text written for that specific finding; the rest fall back to the Fix action with a generic acceptance test. Every open finding becomes a task, and that is the free behaviour: no account, no key, no trimmed preview. `--limit 3` is there when you want only the three tasks that move the score most. The paid audit sells the reading and the work, not the list. `renderFixChecklist(plan, { withPrompts: true })` embeds the same tasks in the executor checklist.
|
|
97
99
|
|
|
98
100
|
## The Fix package: turning findings into agreed work
|
|
99
101
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@operstack/audit",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.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",
|
|
@@ -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 && node src/test-ru-sources.mjs && node src/test-sample-fairness.mjs && node test-engines.mjs && node test-seo-basics.mjs && node test-polite.mjs && node src/test-handover.mjs",
|
|
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 && node src/test-sample-fairness.mjs && node test-engines.mjs && node test-seo-basics.mjs && node test-polite.mjs && node src/test-handover.mjs && node src/test-prospect-count.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/prospect.mjs
CHANGED
|
@@ -85,9 +85,24 @@ export const OWNER_LINES = {
|
|
|
85
85
|
en: 'On a phone your page cannot be pinched to zoom: the code forbids it.',
|
|
86
86
|
ru: 'С телефона страницу нельзя увеличить пальцами, масштабирование запрещено в коде.',
|
|
87
87
|
},
|
|
88
|
+
/*
|
|
89
|
+
* Фраза от числа (задание Максима 26.09.2026): «Not one page…» стояло и при «5 of 8», то есть
|
|
90
|
+
* письмо говорило неправду про сайт, где половина страниц в порядке. Число берётся из самой
|
|
91
|
+
* проверки («5 of 8 sampled page(s) open…»).
|
|
92
|
+
*/
|
|
88
93
|
'answer-first': {
|
|
89
|
-
en:
|
|
90
|
-
|
|
94
|
+
en: (c) => {
|
|
95
|
+
const [have, of] = countOf(c);
|
|
96
|
+
return have > 0 && of > have
|
|
97
|
+
? `Only ${have} of your ${of} checked pages open with a short direct answer, and that opening is exactly what gets quoted.`
|
|
98
|
+
: 'Not one page opens with a short direct answer, and that opening is exactly what gets quoted.';
|
|
99
|
+
},
|
|
100
|
+
ru: (c) => {
|
|
101
|
+
const [have, of] = countOf(c);
|
|
102
|
+
return have > 0 && of > have
|
|
103
|
+
? `Короткий прямой ответ в первом абзаце есть только на ${have} из ${of} проверенных страниц, а цитируют именно его.`
|
|
104
|
+
: 'Ни на одной странице нет короткого прямого ответа в первом абзаце, а цитируют именно его.';
|
|
105
|
+
},
|
|
91
106
|
},
|
|
92
107
|
'tables': {
|
|
93
108
|
en: 'Your pages have no tables, and a table is the easiest thing for an assistant to lift and quote.',
|
|
@@ -164,8 +179,17 @@ const LEAD_ORDER = [
|
|
|
164
179
|
'conv-form-fields', 'conv-contact-path', 'agent-markdown', 'h1', 'dates', 'utility',
|
|
165
180
|
];
|
|
166
181
|
|
|
182
|
+
/** «5 of 8 sampled page(s)…» в находке: сколько страниц в порядке и сколько проверено. */
|
|
183
|
+
function countOf(check) {
|
|
184
|
+
const m = String(check?.value || '').match(/(\d+)\s+(?:of|из)\s+(\d+)/);
|
|
185
|
+
return m ? [Number(m[1]), Number(m[2])] : [0, 0];
|
|
186
|
+
}
|
|
187
|
+
|
|
167
188
|
/** Фраза владельцу на нужном языке. Нет перевода, значит нет фразы, и печатается сама находка. */
|
|
168
|
-
const line = (id, lang = 'en'
|
|
189
|
+
const line = (id, lang = 'en', check = null) => {
|
|
190
|
+
const say = (OWNER_LINES[id] || {})[lang === 'ru' ? 'ru' : 'en'];
|
|
191
|
+
return (typeof say === 'function' ? say(check) : say) || '';
|
|
192
|
+
};
|
|
169
193
|
|
|
170
194
|
/** Вес области при подсчёте одной общей оценки. Считаем только то, что правда измерено. */
|
|
171
195
|
const WEIGHT = {
|
|
@@ -207,15 +231,15 @@ export function worstFindings(audit, limit = 3, lang = 'en') {
|
|
|
207
231
|
const s = (SEVERITY[a.status] ?? 2) - (SEVERITY[b.status] ?? 2);
|
|
208
232
|
if (s) return s;
|
|
209
233
|
// При равной тяжести вперёд идёт то, для чего есть фраза владельцу: она и попадёт в письмо.
|
|
210
|
-
return (line(b.id, lang) ? 1 : 0) - (line(a.id, lang) ? 1 : 0);
|
|
234
|
+
return (line(b.id, lang, b) ? 1 : 0) - (line(a.id, lang, a) ? 1 : 0);
|
|
211
235
|
});
|
|
212
236
|
return open.slice(0, limit).map((c) => ({
|
|
213
237
|
id: c.id,
|
|
214
238
|
status: c.status,
|
|
215
239
|
label: c.label,
|
|
216
240
|
value: c.value,
|
|
217
|
-
say: line(c.id, lang) || `${c.label}: ${c.value}`,
|
|
218
|
-
translated: Boolean(line(c.id, lang)),
|
|
241
|
+
say: line(c.id, lang, c) || `${c.label}: ${c.value}`,
|
|
242
|
+
translated: Boolean(line(c.id, lang, c)),
|
|
219
243
|
}));
|
|
220
244
|
}
|
|
221
245
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Фраза про короткий ответ зависит от числа страниц (задание 26.09.2026): «Not one page» только при нуле.
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { prospectRow } from './prospect.mjs';
|
|
4
|
+
const audit = (value, status) => ({ meta: { host: 'a.test' }, scores: {}, checks: [{ id: 'answer-first', status, label: 'Answer-first opening paragraph', value }] });
|
|
5
|
+
const warn = prospectRow(audit('5 of 8 sampled page(s) open with 20 to 90 words carrying a figure, right after the H1', 'warn'));
|
|
6
|
+
assert.equal(warn.say, 'Only 5 of your 8 checked pages open with a short direct answer, and that opening is exactly what gets quoted.');
|
|
7
|
+
const bad = prospectRow(audit('0 of 8 sampled page(s) open with 20 to 90 words carrying a figure, right after the H1', 'bad'));
|
|
8
|
+
assert.match(bad.say, /^Not one page opens/);
|
|
9
|
+
const ru = prospectRow(audit('5 of 8 sampled page(s) open with 20 to 90 words carrying a figure, right after the H1', 'warn'), { lang: 'ru' });
|
|
10
|
+
assert.equal(ru.say, 'Короткий прямой ответ в первом абзаце есть только на 5 из 8 проверенных страниц, а цитируют именно его.');
|
|
11
|
+
assert.match(prospectRow(audit('', 'bad')).say, /^Not one page opens/, 'без числа прежняя фраза');
|
|
12
|
+
console.log('test-prospect-count: ok');
|