docs: review all

This commit is contained in:
2026-10-04 07:18:50 +05:00
parent 8cb3f1a8f0
commit 1d337f065b
21 changed files with 696 additions and 495 deletions
+6 -2
View File
@@ -175,8 +175,9 @@ report-only (`warn`), baseline — 0 срабатываний.
- **Неиспользуемые токены**: выводится **варнинг** (определены, но нигде не
используются) — выход он не меняет. Проверка видит только ссылки `var(--x)`,
поэтому чтение токена из JS (`getPropertyValue`) она не учтёт; решение
оставить проверку неблокирующей зафиксировано в `docs/quality-gates.md`.
Провалами (exit 1) считаются parity, hct-авторство и резолв `var()`.
оставить проверку неблокирующей — `docs/decisions.md`, раздел 6 (как оно
выглядит в гейте — `docs/quality-gates.md`). Провалами (exit 1) считаются
parity, hct-авторство и резолв `var()`.
## Тесты кастомных линт-правил
@@ -187,6 +188,9 @@ report-only (`warn`), baseline — 0 срабатываний.
`no-token-definition-in-svelte`) через `RuleTester` со строковыми кейсами;
`no-undefined-in-svelte` — через `Linter` API, т.к. читает словарь токенов из
`__fixtures__/src/app.css` (не из реального `src/app.css`).
- `__tests__/interface-props.test.ts` и
`__tests__/no-string-union-alias.test.ts` — конвенции плагина `conventions`
(интерфейс пропсов, запрет union-алиасов).
- Хелпер `__tests__/helpers.ts` собирает `Linter` с `cwd = __fixtures__` —
`process.cwd()` не трогается, реальные `src/` не читаются.
+1 -1
View File
@@ -63,7 +63,7 @@ export default tseslint.config(
"e2e/helpers/page.ts",
"e2e/navigation.spec.ts",
"e2e/catalog.spec.ts",
"e2e/pipeline.spec.ts",
"e2e/tool-flows.spec.ts",
"e2e/text-and-verdicts.spec.ts",
"e2e/tools-smoke.spec.ts",
"e2e/generators.spec.ts",
+2 -2
View File
@@ -1,7 +1,7 @@
import { defineConfig } from "playwright/test";
const browserContractTests = [
"**/pipeline.spec.ts",
"**/tool-flows.spec.ts",
"**/png-fixtures.spec.ts",
"**/text-and-verdicts.spec.ts",
];
@@ -10,7 +10,7 @@ const mobileTests = [
"**/navigation.spec.ts",
"**/catalog.spec.ts",
"**/generators.spec.ts",
"**/pipeline.spec.ts",
"**/tool-flows.spec.ts",
];
export default defineConfig({
+57 -13
View File
@@ -8,30 +8,35 @@ const MAP_PATH = fileURLToPath(
);
const SECTION_START = /^## 1\.\s/;
const SECTION_END = /^## 2\.\s/;
const SECTION_PLANNED = /^## 2\.\s/;
// Все slug страниц содержат дефис, поэтому формат строгий: это отсекает слова
// описания вроде `median-cut` из «colors (k, median-cut)».
const ID_SHAPE = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
const ID_TOKEN = /[a-z][a-z0-9]*(?:-[a-z0-9]+)+/g;
type Mention = { id: string; line: number };
/**
* Буллиты раздела «1. Реализовано» вместе с номером первой строки.
* Перенесённые строки приклеиваются к буллету: иначе slug, оказавшийся на
* следующей строке, молча выпадал бы из разбора и страница считалась бы
* неописанной.
* Буллиты раздела вместе с номером первой строки. Перенесённые строки
* приклеиваются к буллету: иначе slug, оказавшийся на следующей строке, молча
* выпадал бы из разбора и страница считалась бы неописанной.
*/
function implementedBullets(
function sectionBullets(
markdown: string,
startPattern: RegExp,
endPattern: RegExp | null,
): { text: string; line: number }[] {
const lines = markdown.split("\n");
const start = lines.findIndex((l) => SECTION_START.test(l));
const end = lines.findIndex((l) => SECTION_END.test(l));
const start = lines.findIndex((l) => startPattern.test(l));
const end =
endPattern === null
? lines.length
: lines.findIndex((l) => endPattern.test(l));
if (start === -1 || end === -1 || end <= start) {
throw new Error(
"docs/tools-map.md: не найден раздел «1. Реализовано» или «2.» — " +
"тест разбирает только реализованные инструменты",
`docs/tools-map.md: не найден раздел по ${startPattern} — тест не может ` +
"определить границы разделов карты",
);
}
@@ -47,7 +52,7 @@ function implementedBullets(
!/^#{1,6}\s/.test(text) &&
!/^[-*]\s/.test(text);
if (isBullet) {
bullets.push({ text: text.slice(2), line: start + i + 1 });
bullets.push({ text: text.slice(2), line: i + 1 });
} else if (isContinuation) {
previous.text += ` ${text}`;
}
@@ -61,7 +66,11 @@ function implementedBullets(
*/
function mentionedIds(markdown: string): Mention[] {
const found: Mention[] = [];
for (const { text, line } of implementedBullets(markdown)) {
for (const { text, line } of sectionBullets(
markdown,
SECTION_START,
SECTION_PLANNED,
)) {
const head = text.split("—")[0];
for (const raw of head.split(/[/,]/)) {
const id = raw.trim().replace(/`/g, "");
@@ -71,14 +80,35 @@ function mentionedIds(markdown: string): Mention[] {
return found;
}
/**
* Плановые разделы «2. Можно добавить» и «3. Идеи» устроены прозой: slug стоит
* где угодно в буллите, после тире и в скобках. Поэтому берём все токены
* нужной формы из всего текста буллита, а не из «головы» до тире.
*/
function plannedIds(markdown: string): Mention[] {
const found: Mention[] = [];
for (const { text, line } of sectionBullets(
markdown,
SECTION_PLANNED,
null,
)) {
for (const id of text.replace(/`/g, "").match(ID_TOKEN) ?? []) {
found.push({ id, line });
}
}
return found;
}
describe("docs/tools-map.md ↔ страницы реестра", () => {
const markdown = readFileSync(MAP_PATH, "utf8");
const mentioned = mentionedIds(markdown);
const planned = plannedIds(markdown);
const registrySlugs = new Set(PAGES.map((page) => page.slug));
it("разбирает непустой список slug (парсер не сломался молча)", () => {
expect(implementedBullets(markdown).length).toBeGreaterThan(0);
expect(mentioned.length).toBeGreaterThan(0);
expect(new Set(mentioned.map((m) => m.id)).size).toBeGreaterThan(50);
expect(new Set(planned.map((m) => m.id)).size).toBeGreaterThan(10);
});
it("в карте нет slug, которых нет в реестре", () => {
@@ -116,4 +146,18 @@ describe("docs/tools-map.md ↔ страницы реестра", () => {
}
expect(dupes, `повторяющиеся slug:\n${dupes.join("\n")}`).toEqual([]);
});
it("плановый slug уже не реализован (перенос из «2./3.» в «1.»)", () => {
const stale = planned.filter((m) => registrySlugs.has(m.id));
expect(
stale,
stale
.map(
(m) =>
`${m.id} (docs/tools-map.md:${m.line}) — страница уже есть в реестре, ` +
"перенеси её из планового раздела в «1. Реализовано»",
)
.join("\n"),
).toEqual([]);
});
});