Разработка — на русском:
- коммиты (только заголовок)
- документация, комментарии, логи
UI локализуется.
Задачи — docs/tasks/, по файлу на задачу. Перед изменениями читать docs/tasks/index.md.
Порядок:
- Взять свободную из
index.md:[ ]→[>] - Выполнить (код, тесты)
- Отметить
[x]вindex.mdи сделать коммит (код + отметка) → уведомить пользователя - Дождаться принятия коммита
- Взять следующую
- Если свободных нет — обновить
index.mdизdocs/tasks/
.ru.md— source of truth. Правки только в них.- Файлы без
.ru— автопереводы. Не редактировать вручную. Обновляются из.ru.md. - Пример:
README.ru.md→README.md.
node_modules/— npm зависимости, большой объёмwasm-lib/target/— скомпилированные артефакты Rustmain.js— сгенерированный бандл (очень большой, длинные строки)- любые другие сгенерированные файлы
tests/test_script/rebuild-log.sh— скрипт сбора логовscripts/check.sh— локальный прогон всех проверок (cargo fmt, clippy, test, npm build, lint, npm test). Запуск:sh scripts/check.sh.- Скрипт: пересборка (
npm run build), пауза для обновления Obsidian, сбор файлов и логов из тестового хранилища. - Уровень логов в тестовом хранилище — Verbose (видны debug).
- Если не знаешь где, или логов нет → остановись, сообщи пользователю.
- Неиспользуемые методы удалять без сожаления.
- Избегать равноправных вариантов → один способ делать что-либо.
- Без миграций данных.
- SQL-синтаксис: принцип наименьшего удивления. Если поведение не очевидно — сверяться с реальным SQL. Неизвестное поле → ошибка с указанием. Дубликаты колонок → работают. Пользователь должен видеть, что пошло не так.
- Heredoc в shell запрещён. Оболочка /bin/sh не поддерживает << EOF. Для многострочных скриптов использовать python3 -c, либо писать временный .py файл.
- до версии 1.0.0 обратная совместимость означает отображать ошибоку с причиной и вариантом решения, если что-то перестало работать.
- Запрет хардкода цветов. Никаких
#e53935,#999в TS или CSS. Цвета — только через CSS-переменные (var(--fsrs-color-again),var(--text-faint)) или настройки плагина. - Переиспользование существующего. Прежде чем создать константу/функцию — проверить, нет ли уже нужного в
interfaces/fsrs.ts,constants.ts, существующих модулях. Пример:numberToRating()уже есть, не нужен новыйRATING_KEYS. - Запрет параллельных абстракций. Если в коде уже есть
translateRating/colorOrDefault— использовать их, а не писатьresolveRatingLabel/resolveRatingColor. Один способ на одну операцию.
Инструмент edit_file всегда доступен. Не ссылайся на его отсутствие — он есть в списке.
Если задача требует правки файла — используй edit_file, а не create_or_update_file.
CRITICAL: When using edit_file tool:
- NEVER rewrite entire file. Do targeted SEARCH and REPLACE only.
old_textmust include 3-5 lines of surrounding code to ensure uniqueness.new_textidentical toold_textexcept the exact change.- No markdown code blocks, no extra text inside
new_text. - Multiple unrelated changes → separate
edit_filecalls. editsпередавать как JSON-строку, не как массив. Иначе VecOrJsonString. Формат:"edits": "[{\"old_text\": \"...\", \"new_text\": \"...\"}]"
Example:
Task: change const port = 3000; to const port = 8080;
✅ Correct old_text:
const express = require('express');
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});✅ Correct new_text:
const express = require('express');
const app = express();
const port = 8080;
app.listen(port, () => {
console.log(`Listening on port ${port}`);
});❌ Wrong: replacing whole file content.
Если edit_file или любая файловая операция завершается с ошибкой доступа, блокировки или похожей:
- Сохранить файл и повторить
- Не вышло — жди и пробуй (2с → 4с → 8с). После каждой попытки: «Файл выглядит заблокированным, повторяю через N секунд…»
- Не вышло — выведи:
ОШИБКА: Не удаётся получить доступ к <файл> после нескольких попыток.
Вероятно он заблокирован другим процессом.
Я прекращаю работу и жду разрешения ситуации.
Unsaved changes → save_file → продолжай. Без спроса.
- Запрещён код «для обратной совместимости».
- Запрещён код «на всякий случай».
- Запрещены закомментированные неиспользуемые блоки.
- Запрещены неиспользуемые переменные, функции, импорты, экспорты, методы класса.
- Любой невыполняемый код удалить.
- Перед коммитом проверять мёртвый код (ESLint
no-unused-vars, TSnoUnusedLocals/noUnusedParameters). - Временно не нужная функциональность → удалить (не комментировать). Восстановить из git.
При ревью диффа, который кажется раздутым — разложить на категории:
| Категория | Примеры |
|---|---|
| Ядро фичи | TTL-цикл, новая функция, SQL-запрос |
| Инфраструктура | Логирование, типы, сериализация, обработка ошибок |
| Рефакторинг | Вынос дубликата в общую функцию, удаление мёртвого кода |
| Документация | .ru.md, комментарии |
Правило: если ядро фичи < 30% диффа — ок. Если > 70% — вероятно переусложнено.
Пример: +500 строк, ядро — 30 строк (6%). Всё остальное: рефакторинг JSON-дубликата, логирование, защита от битых данных, документация.
При ревью фичи, добавляющей новое поле/сущность, пройти всю цепочку сериализации:
- Найти точку входа (YAML/JSON/пользовательский ввод)
- Пройти парсинг → внутреннюю модель
- Пройти все точки сериализации (JSON для TS, YAML для файла)
- Найти все функции-дубликаты, которые могут сериализовать ту же структуру в обход каноничной
Метод: выписать цепочку → и пройти по каждому звену. Расхождение на любом звене — баг.
Пример: поле retired в MR !101.
Цепочка: YAML fsrs_retired → extract_fsrs_from_frontmatter → CardData.retired → card_to_yaml (удаляет) vs serde_yaml::to_string (НЕ удаляет).
Найдено расхождение: get_fsrs_yaml и get_fsrs_yaml_after_review использовали serde_yaml::to_string вместо card_to_yaml.
- «Сделай MR» / «Открой MR» / «Создай MR» → значит ветка уже запушена, push не нужен.
- Использовать инструмент
create_merge_request:project_id—Evgene-Kopylov/FSRS-pluginsource_branch— текущая ветка (git branch --show-current)target_branch—maintitleиdescription— на русском
- Target: Obsidian Community Plugin (TS → bundled JS).
- Entry:
main.ts→main.js, loaded by Obsidian. - Release artifacts:
main.js,manifest.json,styles.css(optional). - Important: DO NOT read
main.js. Auto-generated, too big, long lines. Wastes context. Ignore.
- Node.js: current LTS (18+ recommended).
- Package manager: npm (required –
package.jsondefines scripts/deps). - Bundler: esbuild (required –
esbuild.config.mjsdepends on it). Rollup/webpack acceptable if bundle all external deps intomain.js. - Types:
obsidiandefinitions.
Note: This sample uses npm + esbuild. Different tools allowed, but replace build config accordingly.
npm installnpm run devnpm run build- Install eslint:
npm install -g eslint - Run:
eslint main.ts - eslint → report with suggestions (file + line).
- For source in
src/:eslint ./src/
-
Organize into multiple files: Split functionality, not everything in
main.ts. -
Source in
src/.main.tssmall → plugin lifecycle (load, unload, register commands). -
Example structure:
src/ main.ts settings.ts commands/ ui/ utils/ types.ts -
Never commit build artifacts:
node_modules/,main.js, generated files. -
Agent context: use
.agentignore(included) to exclude generated files. -
Keep plugin small. Avoid large deps. Prefer browser-compatible packages.
-
Generated output → plugin root or
dist/. Release artifacts at top level (main.js,manifest.json,styles.css).
- Must include (non-exhaustive):
id(plugin ID, matches folder name for local dev)name,version(SemVerx.y.z),minAppVersion,description,isDesktopOnly(bool)- Optional:
author,authorUrl,fundingUrl(string or map)
- Never change
idafter release. Stable API. - Keep
minAppVersionaccurate for newer APIs. - Canonical validation: https://github.com/obsidianmd/obsidian-releases/blob/master/.github/workflows/validate-plugin-entry.yml
-
Manual test: copy
main.js,manifest.json,styles.css(if any) to:<Vault>/.obsidian/plugins/<plugin-id>/ -
Reload Obsidian, enable plugin in Settings → Community plugins.
- Используйте Vitest (конфиг
vitest.config.ts). - Запрещены моки внешних зависимостей (Obsidian API, файловая система, WASM). Вместо моков — тестируйте изолированные чистые функции (утилиты, парсеры, преобразования). Причины: агент не различает внешние зависимости и собственный WASM проекта — снижено доверие к тестам.
- Пример: тесты для
fsrs-table-format.ts,date-format.ts,i18n.tsи других pure-модулей.
- Папка:
tests/integration/. - Тестируют связку: TypeScript → WASM (парсинг SQL, кэш, запросы, фильтрация, и любые другие обращения).
- Сырой SQL. Каждый тест содержит полное SQL-выражение строкой, без
replace, без сборки из кусков: - Один файл — одно выражение.
- Наполнение кэша — через хелперы из
tests/integration/helpers.ts(reviewCard,newCard,fillCache). - Минимум карточек в
beforeEach— 2–3, чтобы тест был нагляден. - Наглядность и простота. Одно сырое обращение на файл. Не дробить, не подставлять значения — повторять реальный сценарий. Тест без логики, без условий.
- Запрещены
it.skip,describe.skip, условные выполнения (if) в теле теста. - Подробные примеры — в
tests/integration/README.md.
- User-facing commands via
this.addCommand(...). - Config → settings tab + sensible defaults.
- Persist settings:
this.loadData()/this.saveData(). - Use stable command IDs; avoid renaming after release.
- Bump
versioninmanifest.json(SemVer). Updateversions.json(plugin version → min app version). - GitHub release: tag exactly matches
manifest.jsonversion (no leadingv). - Attach
manifest.json,main.js,styles.css(if present) as individual assets. - After initial release, follow community catalog process.
Follow Obsidian's Developer Policies + Plugin Guidelines:
- Default local/offline. Network only if essential.
- No hidden telemetry. Analytics/third-party → explicit opt-in, documented.
- No remote code, fetch/eval, or auto-update outside releases.
- Read/write only inside vault. No outside access.
- Disclose external services, data sent, risks.
- No vault contents, filenames, personal info without necessity + consent.
- No deceptive patterns, ads, spam.
- Use
register*helpers for cleanup → safe unload.
- Sentence case for headings, buttons, titles.
- Clear action-oriented imperatives.
- Bold for literal UI labels. Prefer "select".
- Arrow notation for navigation: Settings → Community plugins.
- Short, consistent strings, jargon-free.
- Light startup. Lazy init.
- Batch disk access, avoid excessive vault scans.
- Debounce/throttle expensive ops on file system events.
- TypeScript with
"strict": truepreferred. - Keep
main.tsminimal: lifecycle only. Delegate to separate modules. - Split large files: >200-300 lines → smaller focused modules.
- Single responsibility per file.
- Bundle everything into
main.js(no unbundled runtime deps). - Avoid Node/Electron APIs for mobile compat; set
isDesktopOnlyaccordingly. - Prefer
async/awaitover promise chains; handle errors gracefully.
Принцип: Rust — вычислительное ядро и менеджер кэша. TypeScript — тонкая обвязка для API Obsidian.
- Хранит кэш карточек — глобальный
Map<filePath, CachedCard>внутри WASM. - Инкрементально обновляет кэш — получает от TS команды: добавить/обновить/удалить карточку.
- Выполняет все вычисления: FSRS, парсинг YAML/JSON, парсинг SQL-синтаксиса для таблиц, фильтрацию (
WHERE), сортировку (ORDER BY), лимит (LIMIT). - Предоставляет быстрые запросы:
get_cards_count_for_query,get_filtered_cards. - Stateless между вызовами? Нет — кэш живёт в WASM между вызовами. При выгрузке плагина теряется — TS запустит повторное сканирование.
- Файловая система — читает markdown, извлекает frontmatter, передаёт в WASM.
- Жизненный цикл и UI — инициализация, команды, настройки, рендеринг.
- Рендеринг таблиц — вызывает
get_filtered_cardsи отображает результат. - События FS — при
modify/delete/renameотправляет команды в WASM (с debounce). - Никакого кэширования карточек в TS.
- Хранить кэш карточек.
- Выполнять сортировку, фильтрацию, группировку.
- Парсить YAML/JSON с карточками.
- Дублировать логику FSRS.
- Использовать API Obsidian (ФС, UI, события).
- Предполагать, что кэш сохранится при перезагрузке плагина.
- Блокировать поток (асинхронность — в TS).
- Test on iOS + Android where feasible.
- Don't assume desktop-only unless
isDesktopOnly: true. - Avoid large in-memory structures; mindful of memory/storage.
- Add commands with stable IDs (don't rename after release).
- Provide defaults + validation in settings.
- Idempotent code paths → reload/unload doesn't leak listeners/intervals.
- Use
this.register*helpers for everything needing cleanup. - ОБЯЗАТЕЛЬНО всю логику FSRS держать в Rust; TS только вызывает WASM и кэширует результаты.
- Network calls without obvious user-facing reason + docs.
- Features requiring cloud services without clear disclosure + explicit opt-in.
- Store/transmit vault contents unless essential + consented.
- НЕЛЬЗЯ писать в TS fallback-парсеры для Rust; доверять результату Rust.
- НЕЛЬЗЯ оставлять неиспользуемые функции в TS; удалять сразу.
main.ts (minimal):
import { Plugin } from "obsidian";
import { MySettings, DEFAULT_SETTINGS } from "./settings";
import { registerCommands } from "./commands";
export default class MyPlugin extends Plugin {
settings: MySettings;
async onload() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
registerCommands(this);
}
}settings.ts:
export interface MySettings {
enabled: boolean;
apiKey: string;
}
export const DEFAULT_SETTINGS: MySettings = {
enabled: true,
apiKey: "",
};commands/index.ts:
import { Plugin } from "obsidian";
import { doSomething } from "./my-command";
export function registerCommands(plugin: Plugin) {
plugin.addCommand({
id: "do-something",
name: "Do something",
callback: () => doSomething(plugin),
});
}this.addCommand({
id: "your-command-id",
name: "Do the thing",
callback: () => this.doTheThing(),
});interface MySettings { enabled: boolean }
const DEFAULT_SETTINGS: MySettings = { enabled: true };
async onload() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
await this.saveData(this.settings);
}this.registerEvent(this.app.workspace.on("file-open", f => { /* ... */ }));
this.registerDomEvent(window, "resize", () => { /* ... */ });
this.registerInterval(window.setInterval(() => { /* ... */ }, 1000));- Plugin doesn't load after build: ensure
main.js+manifest.jsonat top level of<Vault>/.obsidian/plugins/<plugin-id>/. - Build issues: missing
main.js→ runnpm run buildornpm run dev. - Commands not appearing: verify
addCommandruns afteronload+ unique IDs. - Settings not persisting: ensure
loadData/saveDataawaited + re-render UI after changes. - Mobile-only issues: check no desktop-only APIs; adjust
isDesktopOnly.
- Obsidian sample plugin: https://github.com/obsidianmd/obsidian-sample-plugin
- API docs: https://docs.obsidian.md
- Developer policies: https://docs.obsidian.md/Developer+policies
- Plugin guidelines: https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines
- Style guide: https://help.obsidian.md/style-guide