diff --git a/.opencode/skills/repo-readme/SKILL.md b/.opencode/skills/repo-readme/SKILL.md index 0159a63..92973aa 100644 --- a/.opencode/skills/repo-readme/SKILL.md +++ b/.opencode/skills/repo-readme/SKILL.md @@ -54,6 +54,10 @@ repos/{owner}/{repo}/contents/README.md` с base64-контентом и SHA. Витрина **slaid098.dev** скачивает raw `README.md` из каждого репо и извлекает фрагменты между разделителями: +- `` ... `` — английский + tagline (короткая фраза для карточки). +- `` ... `` — русский + tagline. - `` ... `` — английский блок Why/What для карточки. - `` ... `` — английская @@ -73,7 +77,12 @@ repos/{owner}/{repo}/contents/README.md` с base64-контентом и SHA. ```markdown # 🚀 {repo_name} -> {tagline} + +> {tagline_en} + + +> {tagline_ru} + [English](#-english) | [Русский](#-русский) @@ -146,12 +155,13 @@ repos/{owner}/{repo}/contents/README.md` с base64-контентом и SHA. 👉 **[slaid098.dev/support](https://slaid098.dev/support)** ``` -`validate` проверяет: наличие всех 4 пар EN/RU разделителей (summary + -features), непустой контент между ними, ссылку `slaid098.dev/support`, секции -Quick Start (EN) и Быстрый старт (RU), language switcher `[English]` / -`[Русский]`, заголовок `## 🇷🇺 Русский` (не "Русская версия"), anchor -`[Русский](#-русский)` (не `#-русская-версия`). Шаги `quick_start_steps_*` -не влияют на валидацию — они рендерятся вне delimiter-пар (summary/features). +`validate` проверяет: наличие всех 6 пар EN/RU разделителей (tagline + summary + +features), непустой контент между ними, H1 title prefix `# 🚀 `, ссылку +`slaid098.dev/support`, секции Quick Start (EN) и Быстрый старт (RU), language +switcher `[English]` / `[Русский]`, заголовок `## 🇷🇺 Русский` (не "Русская +версия"), anchor `[Русский](#-русский)` (не `#-русская-версия`). Шаги +`quick_start_steps_*` не влияют на валидацию — они рендерятся вне delimiter-пар +(summary/features). ## 6. Независимость от repo-init @@ -167,8 +177,17 @@ Quick Start (EN) и Быстрый старт (RU), language switcher `[English] `create-readme`: - `mode` — `"create"` | `"validate"` (обязательный). -- `repo_name`, `tagline`, `why_en`, `what_en`, `why_ru`, `what_ru`, - `quick_start`, `features_en`, `features_ru` — обязательны для `create`. +- `repo_name`, `tagline_en`, `tagline_ru`, `why_en`, `what_en`, `why_ru`, + `what_ru`, `quick_start`, `features_en`, `features_ru` — обязательны для + `create`. +- `repo_name` — должен быть **lowercase kebab-case** (regex + `^[a-z0-9]+(-[a-z0-9]+)*$`): только `a-z`, `0-9`, одиночные дефисы. Uppercase, + underscores, пробелы, leading/trailing/consecutive dashes — отвергаются. +- `tagline_en` — короткий английский tagline (1 предложение). **Не должен + содержать кириллицы** (валидируется regex `/[ЁА-яё]/`). +- `tagline_ru` — короткий русский tagline (1 предложение). **Должен содержать + кириллицу** (валидируется regex `/[ЁА-яё]/`). Гарантирует, что русский + tagline реально на русском, а не копия английского. - `features_en` / `features_ru` — массив `{ emoji, name, description }[]`. - `custom_sections_en` / `custom_sections_ru` — массивы `{ title, content }` (optional). @@ -213,7 +232,8 @@ Quick Start (EN) и Быстрый старт (RU), language switcher `[English] { "mode": "create", "repo_name": "my-userscript", - "tagline": "One-line tagline.", + "tagline_en": "One-line tagline.", + "tagline_ru": "Короткий теглайн.", "why_en": "Why this exists.", "what_en": "What it does.", "why_ru": "Зачем этот проект.", @@ -249,3 +269,19 @@ Access at [http://localhost:4096](http://localhost:4096) `include_clone: false` + `quick_start: ""` → bash-блок не рендерится (только шаги). Если `quick_start` непустой — bash-блок рендерится перед шагами. + +## 9. Breaking change: tagline → tagline_en + tagline_ru + +Параметр `tagline` **удалён** (PR #150). Раньше был один английский tagline; +теперь два — `tagline_en` (EN) и `tagline_ru` (RU) — оба обязательны для +`create`. Витрина slaid098.dev парсит оба через delimiter-теги +`` и ``. + +**Миграция:** замени `"tagline": "..."` на `"tagline_en": "..."` + +`"tagline_ru": "..."`. Старые вызовы с `tagline` падают с +`❌ tagline_en is required for create mode`. + +**Существующие README** (без tagline delimiter-тегов) станут invalid при +`validate` — `Missing delimiter` и +`Missing delimiter`. Регенерация README через +`create` (с новыми параметрами) делается отдельным шагом после merge. diff --git a/.opencode/tools/create-readme.ts b/.opencode/tools/create-readme.ts index 97813f6..5530ac5 100644 --- a/.opencode/tools/create-readme.ts +++ b/.opencode/tools/create-readme.ts @@ -8,7 +8,8 @@ type CustomSection = { title: string; content: string } type CreateArgs = { repo_name: string - tagline: string + tagline_en: string + tagline_ru: string why_en: string what_en: string why_ru: string @@ -79,7 +80,12 @@ function generateReadme(args: CreateArgs): string { const stepsRu = renderSteps(args.quick_start_steps_ru) return `# 🚀 ${args.repo_name} -> ${args.tagline} + +> ${args.tagline_en} + + +> ${args.tagline_ru} + [English](#-english) | [Русский](#-русский) @@ -131,6 +137,10 @@ function validateReadme(content: string): { ok: boolean; issues: string[] } { const issues: string[] = [] const delimiters = [ + "tagline-en:start", + "tagline-en:end", + "tagline-ru:start", + "tagline-ru:end", "summary-en:start", "summary-en:end", "features-en:start", @@ -146,6 +156,8 @@ function validateReadme(content: string): { ok: boolean; issues: string[] } { } const pairs = [ + { start: "", end: "", label: "EN tagline" }, + { start: "", end: "", label: "RU tagline" }, { start: "", end: "", label: "EN summary" }, { start: "", end: "", label: "EN features" }, { start: "", end: "", label: "RU summary" }, @@ -157,6 +169,8 @@ function validateReadme(content: string): { ok: boolean; issues: string[] } { issues.push(`${p.label} content between delimiters is empty`) } + if (!content.includes("# 🚀 ")) + issues.push("Missing H1 title prefix '# 🚀 '") if (!content.includes("slaid098.dev/support")) issues.push("Missing Support link (slaid098.dev/support)") if (!content.includes("Quick Start")) @@ -188,10 +202,14 @@ export default tool({ .string() .optional() .describe("Repository name (e.g. 'anti-detect-mcp'). Required for create mode."), - tagline: tool.schema + tagline_en: tool.schema .string() .optional() - .describe("Short English tagline (1 sentence). Required for create mode."), + .describe("Short English tagline (1 sentence). Required for create mode. Must not contain Cyrillic."), + tagline_ru: tool.schema + .string() + .optional() + .describe("Short Russian tagline (1 sentence). Required for create mode. Must contain Cyrillic."), why_en: tool.schema .string() .optional() @@ -290,7 +308,8 @@ export default tool({ if (args.mode === "create") { const required: Record = { repo_name: args.repo_name, - tagline: args.tagline, + tagline_en: args.tagline_en, + tagline_ru: args.tagline_ru, why_en: args.why_en, what_en: args.what_en, why_ru: args.why_ru, @@ -304,6 +323,13 @@ export default tool({ if (!args.features_ru || args.features_ru.length === 0) return `❌ features_ru is required for create mode` + if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(args.repo_name!)) + return `❌ repo_name must be lowercase kebab-case (a-z0-9 with single dashes), got: "${args.repo_name}"` + if (/[ЁА-яё]/.test(args.tagline_en!)) + return `❌ tagline_en must be English (no Cyrillic), got Cyrillic chars` + if (!/[ЁА-яё]/.test(args.tagline_ru!)) + return `❌ tagline_ru must contain Cyrillic (Russian text), got: "${args.tagline_ru}"` + const hasBash = args.include_clone !== false || (args.quick_start ?? "") !== "" const hasStepsEn = !!args.quick_start_steps_en && args.quick_start_steps_en.length > 0 const hasStepsRu = !!args.quick_start_steps_ru && args.quick_start_steps_ru.length > 0 @@ -314,7 +340,8 @@ export default tool({ const content = generateReadme({ repo_name: args.repo_name!, - tagline: args.tagline!, + tagline_en: args.tagline_en!, + tagline_ru: args.tagline_ru!, why_en: args.why_en!, what_en: args.what_en!, why_ru: args.why_ru!, diff --git a/docs/decisions/064-pr-151-bilingual-tagline.md b/docs/decisions/064-pr-151-bilingual-tagline.md new file mode 100644 index 0000000..63668f9 --- /dev/null +++ b/docs/decisions/064-pr-151-bilingual-tagline.md @@ -0,0 +1,87 @@ +# ADR-064: Bilingual tagline and repo_name validation (PR-151) + +## Статус +Accepted (2026-07-30) + +## Контекст +ADR-062 (PR-146) и ADR-063 (PR-149) эволюционировали тулзу `create-readme` +вокруг Quick Start. Параметр `tagline` оставался единым английским теглайном +с момента ADR-058 (PR-116, README standard v2). Витрина **slaid098.dev** +парсит README через delimiter-теги (summary/features), но tagline +рендерился вне delimiterов — парсер не мог его надёжно извлечь, и RU tagline +отсутствовал вообще. Дополнительно: `repo_name` принимал любой строки +(uppercase, underscores, пробелы) — генерировал `git clone` ссылки вида +`https://github.com/slaid098/MyRepo.git` (валидные, но не matching actual +repo slug); `tagline_en` никто не проверял на отсутствие кириллицы, а +`tagline_ru` не существовало. Issue #150 требует: (A) билингвальный tagline +EN+RU с delimiter-тегами, (B) валидация английского `repo_name` (lowercase +kebab-case) и `tagline_en` (no Cyrillic), `tagline_ru` (require Cyrillic). + +## Решение +1. **`CreateArgs` type** — `tagline: string` переименован в `tagline_en: + string` + `tagline_ru: string` (оба required, без `?`). Старый параметр + удалён (breaking). +2. **Рендер top-блока** — tagline обёрнут в delimiter-пары: + ``` + # 🚀 {repo_name} + + > {tagline_en} + + + > {tagline_ru} + + + [English](#-english) | [Русский](#-русский) + ``` + EN-пара идёт первой, затем RU-пара, подряд без пустых строк между ними; + пустая строка перед language switcher. Spacing критичен для парсера. +3. **Content-валидация в `execute()`** — после presence-чеков (required + Record, falsy `!v`), ДО Quick Start guard (PR #149), добавлены: + - `repo_name`: `^[a-z0-9]+(-[a-z0-9]+)*$` — lowercase kebab-case, только + `a-z`/`0-9`, одиночные дефисы (не leading/trailing/consecutive). + - `tagline_en`: `/[ЁА-яё]/` reject — если есть кириллица → ошибка. + - `tagline_ru`: `/[ЁА-яё]/` require — если нет кириллицы → ошибка + (ловит копию английского tagline). + Порядок: presence → content (regex) → Quick Start guard. +4. **`validateReadme`** — добавлено 4 delimiter'а в массив (`tagline-en/ru + start/end`, итого 12), +2 пары в `pairs` (EN/RU tagline, с empty-content + check), +check H1 prefix `# 🚀 ` (substring). repo_name regex в validate + НЕ добавлен — legacy README с uppercase repo_name не должны ломаться + (валидируется только в create mode). +5. **Schema** — `tagline` переименован в `tagline_en`, обновлён `.describe()` + ("Must not contain Cyrillic"); добавлен `tagline_ru` (".describe": "Must + contain Cyrillic"). Оба `.optional()` (required-логика в `execute()`). +6. **SKILL.md** — раздел 4 (delimiters): +tagline-en/ru; раздел 5 (шаблон): + top-блок с delimiter-тегами; раздел 5 (validate): "6 пар EN/RU + разделителей (tagline + summary + features)" + H1 prefix; раздел 7 + (параметры): `tagline_en`/`tagline_ru` с правилами, `repo_name` с regex; + раздел 8 (пример): `tagline_en`/`tagline_ru`; новый раздел 9 "Breaking + change: tagline → tagline_en + tagline_ru" с инструкцией миграции. +7. **Backward compatibility** — BREAKING: `tagline` удалён, старые вызовы + падают с `❌ tagline_en is required for create mode`. Существующие README + (без tagline delimiter-тегов) станут invalid при `validate` — + `Missing delimiter` и тег. Регенерация README + отдельный шаг после merge. + +## Альтернативы +- **Оставить `tagline` как alias к `tagline_en`** — отвергнуто: молчаливый + дуализм (то `tagline`, то `tagline_en`) запутывает пользователей и парсер. + Явный breaking с понятным сообщением `❌ tagline_en is required` лучше для + миграции (одна правка вызова, а не поиск «почему RU tagline пустой»). +- **Один `tagline` + авто-перевод** — отвергнуто: перевод tagline — не + responsibility тулзы, качество перевода критично для карточки slaid098.dev. + Явный `tagline_ru` с require-Cyrillic-чеком гарантирует осмысленный русский. +- **Валидировать `repo_name` в `validateReadme`** — отвергнуто: legacy README + с uppercase repo_name (например, старые репо до стандарта) не должны + ломаться при `validate`. Regex только в `create` (новые README генерируются + по стандарту). `validate` проверяет структуру (delimiterы, заголовки), не + контент repo_name. +- **Строгий H1 check (regex на полный `# 🚀 {repo_name}`)** — отвергнуто: + `validate` не знает `repo_name` (работает с произвольным README). + Substring-чек `# 🚀 ` ловит отсутствие префикса, не валидирует что после + него. Достаточно для regression-детекции. +- **Три отдельных tagline-чек (presence + regex EN + regex RU) как один + guard** — отвергнуто: три независимых `if return` с разными сообщениями + читаемее, чем комбинированный guard. Пользователь видит конкретную ошибку + (repo_name / tagline_en / tagline_ru), а не «одна из трёх валидаций не + прошла». \ No newline at end of file diff --git a/docs/handoff/pr-151-bilingual-tagline.md b/docs/handoff/pr-151-bilingual-tagline.md new file mode 100644 index 0000000..b301bfe --- /dev/null +++ b/docs/handoff/pr-151-bilingual-tagline.md @@ -0,0 +1,58 @@ +--- +pr: 151 +title: feat(create-readme): add bilingual tagline and repo_name validation +--- + +## Что сделано + +В `.opencode/tools/create-readme.ts` (mode `create`): параметр `tagline` +переименован в `tagline_en` + добавлен `tagline_ru` (оба required). Рендер +top-блока теперь обёрнут в delimiter-теги `` и +`` (для парсера slaid098.dev). Добавлена +content-валидация в `execute()` ПОСЛЕ presence-чеков, ДО Quick Start guard: +`repo_name` — lowercase kebab-case regex `^[a-z0-9]+(-[a-z0-9]+)*$`; +`tagline_en` — no Cyrillic (`/[ЁА-яё]/`); `tagline_ru` — require Cyrillic. +`validateReadme`: +4 delimiter'а (tagline-en/ru start/end, итого 12), ++2 пары (EN/RU tagline), +check H1 prefix `# 🚀 `. Schema обновлена +(`tagline_en`/`tagline_ru` с обновлёнными `.describe()`). SKILL.md обновлён: +разделители tagline в список, шаблон top-блока, таблица параметров, пример +вызова, новый раздел 9 «Breaking change: tagline → tagline_en + tagline_ru». +Smoke-тест: 7 сценариев, 16 assertion'ов, все PASS (happy path + validate ok, +repo_name uppercase, repo_name underscore, tagline_en Cyrillic, tagline_ru no +Cyrillic, old tagline breaking, validateReadme на старом README без tagline +delimiterов). + +## Почему + +Issue #150: slaid098.dev парсер нуждается в отдельном RU tagline (раньше был +только один английский `tagline`). Валидация гарантирует: `repo_name` — +строго английский kebab-case (lowercase, без uppercase/underscore/пробелов); +`tagline_en` — реально на английском (без кириллицы); `tagline_ru` — реально +на русском (содержит кириллицу, не копия английского). Breaking change: +параметр `tagline` удалён — миграция на `tagline_en` + `tagline_ru`. + +## Pending + +Регенерация существующих README в репо slaid098 через `create-readme` с +новыми параметрами (`tagline_en`/`tagline_ru`) — отдельный шаг после merge +(PR/issue вне scope #150). Существующие README без tagline delimiter-тегов +станут invalid при `validate` (ожидаемо). + +## Watch out + +**BREAKING CHANGE**: параметр `tagline` удалён из `create-readme` (mode +`create`). Старые вызовы с `tagline` падают с `❌ tagline_en is required for +create mode`. Миграция: заменить `tagline` на `tagline_en` + добавить +`tagline_ru`. Существующие README без tagline delimiter-тегов станут +invalid при `validate` (`Missing delimiter`) — +регенерация отдельный шаг (см. Pending). + +Порядок валидации в `execute()` (create mode): presence-чеки (required +Record) → content-чеки (regex: repo_name, tagline_en, tagline_ru) → Quick +Start guard (PR #149). Content-чеки используют non-null assertion `args.x!` +— безопасно, т.к. presence-чеки уже прошли. `validateReadme` НЕ проверяет +repo_name regex — только create mode (legacy README с uppercase repo_name не +должны ломаться). H1 prefix check `# 🚀 ` — substring-чек, не строгий (ищет +presence префикса, не валидирует что после него). ADR-064 отмечает +backward-compat альтернативу «оставить tagline alias» — отвергнута (явный +breaking лучше молчаливого дуализма). \ No newline at end of file diff --git a/docs/project-map/README.md b/docs/project-map/README.md index 3e281b6..722095d 100644 --- a/docs/project-map/README.md +++ b/docs/project-map/README.md @@ -36,7 +36,7 @@ opencode-config/ │ │ ├── python-development/SKILL.md # Python dev patterns │ │ ├── release/SKILL.md # Tag + GitHub Release │ │ ├── repo-init/SKILL.md # New repository bootstrap -│ │ ├── repo-readme/SKILL.md # Standardized README generation (create-readme tool: create vs validate, bilingual Why/What + Features table, GitHub metadata, quick_start_steps clickable steps + conditional bash block) — PR#112, PR#116, PR#118, PR#130, PR#146 +│ │ ├── repo-readme/SKILL.md # Standardized README generation (create-readme tool: create vs validate, bilingual Why/What + Features table, GitHub metadata, quick_start_steps clickable steps + conditional bash block; tagline_en/tagline_ru required + delimiter-теги, breaking change note) — PR#112, PR#116, PR#118, PR#130, PR#146, PR#151 │ │ ├── tunnel/SKILL.md # Cloudflare tunnel toggle (tool `tunnel()`: 1-й вызов start, 2-й stop) — PR#63 (восстановлен, удалён в PR#42) │ │ ├── run-tests/SKILL.md # Test runner guide │ │ └── spec/SKILL.md # 9-phase spec generation @@ -45,7 +45,7 @@ opencode-config/ │ │ ├── commit.ts # commit tool wrapper (1 arg message, validates format+staged) — PR#38 │ │ ├── create-issue.ts # create-issue tool wrapper (3 args, validates format+labels; optional repo?: string) — PR#38, PR#65 │ │ ├── create-pr.ts # create-pr tool wrapper (3 args, validates format+Closes #N; optional repo?: string) — PR#38, PR#65 -│ │ ├── create-readme.ts # create-readme tool (TS plugin, modes: create/validate; standardized bilingual README with features table, include_clone/development_en/ru/quick_start_steps_en/ru optional params, clickable access_url [url](url), conditional bash block via hasBashBlock, RU heading 'Русский' + anchor checks, 4 delimiter pairs for slaid098.dev; local fs + remote gh api) — PR#112, PR#116, PR#118, PR#130, PR#146, PR#149 (quick_start optional + guard: Quick Start non-empty per-lang via hasBash/hasStepsEn/hasStepsRu; hasBashBlock handles undefined via ?? "") +│ │ ├── create-readme.ts # create-readme tool (TS plugin, modes: create/validate; standardized bilingual README with features table, include_clone/development_en/ru/quick_start_steps_en/ru optional params, clickable access_url [url](url), conditional bash block via hasBashBlock, RU heading 'Русский' + anchor checks, 6 delimiter pairs for slaid098.dev (summary-en/ru, features-en/ru, tagline-en/ru); tagline_en+tagline_ru required (BREAKING: tagline removed PR#151), content-валидация repo_name (lowercase kebab-case) + tagline_en (no Cyrillic) + tagline_ru (require Cyrillic), validateReadme H1 prefix check `# 🚀 `; local fs + remote gh api) — PR#112, PR#116, PR#118, PR#130, PR#146, PR#149 (quick_start optional + guard), PR#151 (bilingual tagline + repo_name validation, breaking) │ │ ├── draw-image.ts # draw-image tool wrapper (opencode plugin, 5 args: template/title/subtitle?/slots?/out?; spawnSync node cli.ts render → sharp PNG) — PR#133 │ │ ├── merge-pr.ts # merge-pr tool wrapper (orchestrator-safe gh pr merge; optional repo?: string) — PR#30, PR#65 │ │ ├── memory-access.ts # memory-access tool (bump frontmatter last_accessed/access_count, regex replace, atomic write tmp+rename) — PR#101