diff --git a/.ai/conventions/code-health.md b/.ai/conventions/code-health.md new file mode 100644 index 0000000..6662e11 --- /dev/null +++ b/.ai/conventions/code-health.md @@ -0,0 +1,65 @@ +# Code health и границы изменения + +## Search → reuse → converge + +Перед utility, hook, wrapper, dependency или новым technical pattern: + +1. Проследи поведение, инварианты и callers у ответственного owner. +2. Ищи аналоги по имени **и реализации** (`rg` по imports, API, выражению). +3. Расширь подходящий owner/helper/pattern; выбирай минимальное решение, + полностью закрывающее текущую задачу. Новый файл сам по себе не улучшение. + +Уменьшай количество способов решать одну техническую задачу: конфигурацию +release-it и commitlint поддерживай средствами этих инструментов. Сходный синтаксис +без общей ответственности не требует DRY. Критерий dependency — в +[dependencies](dependencies.md). + +## Change scope + +Consistency с подходящим project pattern предпочтительнее универсальности. +Новые layers, wrappers, generic helpers, factories, schemas, validators, scripts +и дополнительные проверки требуют текущей ответственности или риска. +Сначала используй native API/установленный stack; не строй на гипотетических +consumers и будущих requirements. + +Не вводи shared tooling для других репозиториев как обязательную зависимость +их сборки, CI или маршрута инструкций. Каждый repo владеет своими checks. +Trust-boundary validation, authorization, atomicity, accessibility и необходимая +regression proof остаются частью полного решения. + +## Boy Scout review + +После изменения проверь affected area: новое дублирование, пропущенный helper, +competing patterns, небольшие связанные legacy issues и conventions. +Исправляй в рамках задачи только непосредственно связанные, небольшие, +low-risk, behavior-preserving проблемы, которые легко проверить. +Более широкие находки сохраняй для отдельной migration; массовый cleanup +не является условием завершения текущей задачи. + +## Persist decisions и ratchet + +Устойчивый pattern должен находиться через код и manifest. Добавляй convention, +только если следующей сессии всё ещё нужно явное project decision rule. +У каждого правила один owner; AGENTS содержит trigger и ссылку. +Устройство root tooling находится в README и конфигурации; их чтение не +требует соседних checkout. Audit/backlog не помещай в conventions. + +Legacy фиксируется отдельно; новый diff не добавляет нарушений. При migration +закрывай подтверждённые baseline findings с evidence, сохраняя область проверки. +Зелёный lint не доказывает отсутствие semantic debt. Работа с baseline начинается +с [migration plan](../migrations/conventions-v1/plan.md), только когда задача +затрагивает migration или найденный там owner. + +## Domain values + +Finite business vocabulary (status, error code, operation kind, role) принадлежит +domain/module/entity owner. Перед определением найди существующие types/values; +в comparisons используй символ владельца, без конкурирующих словарей. +Для нового project-owned vocabulary без аналога: `as const` + derived union. +Сохраняй wire values; generated/external enums не переписывай. + +Numeric policy называй по смыслу и единицам (`TIMEOUT_MS`, `PAGE_SIZE`). +Индексы, `index + 1`, `Math.max(value, 0)` и обычные labels/log messages +не выноси в constants без semantic benefit. Локальный lint/i18n profile всё равно +применим. Не создавай tests существования констант или custom magic-string analyzer. +Правила приложений принадлежат их локальным conventions. diff --git a/.ai/conventions/dependencies.md b/.ai/conventions/dependencies.md new file mode 100644 index 0000000..8f4c908 --- /dev/null +++ b/.ai/conventions/dependencies.md @@ -0,0 +1,29 @@ +# Dependencies + +## Перед добавлением + +1. Найди аналог по назначению и реализации в affected repo. +2. Проверь его direct dependencies, текущий stack и native/stdlib API. +3. Определи масштаб: один простой случай или повторяющаяся техническая задача. +4. Используй существующее решение, если оно полностью покрывает поведение. + Новый пакет оправдан меньшей общей сложностью, а не сокращением одной строки. + +Повтор non-business задачи в примерно 3+ независимых местах — сигнал пересмотреть +pattern, а не требование немедленно извлечь abstraction. Сравни общий helper, +native API и зрелую небольшую dependency: parsing, serialization, dates, +debounce, class composition. Три `Array.map` не требуют utility library. +Не продолжай копировать boilerplate, если один принятый способ дешевле сопровождать. + +## При выборе пакета + +- Укажи конкретную потребность, альтернативу из текущего stack и существенную + цену: совместимость, browser bundle/server runtime, maintenance/security. +- Импортируемый пакет объявляй напрямую в repo владельца; наличие в соседнем + repo или транзитивном дереве не является dependency contract. +- Сохраняй exact versions и lockfile. Перед Node-командами активируй `.nvmrc` + через `nvm use --silent`; воспроизводимая установка — `npm ci` в нужном repo. +- Peer-конфликты решай совместимыми версиями, без `--force`/`--legacy-peer-deps`. + Версии и compatibility принадлежат местному manifest/lockfile. +- После изменения запускай relevant install/type/build/behavior checks и + `npm audit` при изменении зависимостей. Не добавляй второй analyzer, когда задача решается + настройкой существующего; [карта проверок](guardrails.md). diff --git a/.ai/conventions/git.md b/.ai/conventions/git.md new file mode 100644 index 0000000..9d1aab0 --- /dev/null +++ b/.ai/conventions/git.md @@ -0,0 +1,20 @@ +# Git в workspace + +- Выполняй Git-команды из репозитория владельца: root, frontend, backend, + docs и specs имеют независимые history, index и branches. +- Новая работа: ветка `codex/` от актуального `develop`, затем PR в `develop`. + Сохраняй чужие staged/unstaged/untracked изменения; не переноси их в свой commit. +- Commit содержит одну связанную задачу и её необходимые проверки/документацию. + Межрепозиторное изменение оформляй отдельными commits/PR с взаимными ссылками. +- Commits и заголовки PR — Conventional Commits; допустим `spec:`. + Формат проверяет local commitlint; правила других repo принадлежат их AGENTS. +- Перед commit проверь `git diff --cached --name-status` и сам staged diff + в каждом affected repo. Root tracking ограничен [.gitignore](../../.gitignore): + новый общий файл получает точечный allowlist; проверь staged paths вручную. `git add -f` не используется. +- Не выполняй `git clean -fdx`, reset чужих изменений, force-push, удаление или + перенос опубликованных тегов как обычную очистку. Для опасной операции нужны + явная необходимость, сохранение данных и соответствующая авторизация. +- Обычный PR допускает squash. Release/hotfix, синхронизация main/develop, + release-конфигурация требуют чтения + [процесса релиза](../../README.md#релизы-и-ветки): он владеет merge policy, + версиями и тегами. Прямой push в `main` запрещён; версии вручную не повышай. diff --git a/.ai/conventions/guardrails.md b/.ai/conventions/guardrails.md new file mode 100644 index 0000000..44f1fe8 --- /dev/null +++ b/.ai/conventions/guardrails.md @@ -0,0 +1,15 @@ +# Root проверки + +Root CI содержит только `commits`: штатный commitlint проверяет заголовок PR +и коммиты между BASE и HEAD. Установка — `npm ci --ignore-scripts`, Node — из +местной `.nvmrc`. Самописные проверки и дополнительные CI workflow не добавляются. + +Локально перед Node-командами выполни `nvm use --silent`. Для изменений проверь +JSON/config syntax, тексты, ссылки и `git diff --check`; новые файлы просмотри +отдельно. Перед commit проверь staged paths относительно allowlist `.gitignore`. +Зависимости устанавливай через `npm ci`; при их изменении выполни `npm audit`. + +Release-it использует встроенные проверки ветки, чистого дерева и наличия +коммитов. Перед подготовкой релиза вручную проверь теги и соответствие версии +последнему принятому тегу; повторно не повышай уже подготовленную версию. +Процесс — в [README](../../README.md#релизы-и-ветки). diff --git a/.ai/conventions/testing.md b/.ai/conventions/testing.md new file mode 100644 index 0000000..9e9e6c5 --- /dev/null +++ b/.ai/conventions/testing.md @@ -0,0 +1,27 @@ +# Test Intent + +Перед нетривиальным тестом сформулируй наблюдаемый результат и риск регрессии +в описании задачи/PR, рядом с тестом либо в рабочем отчёте. Отдельный artifact +не обязателен. Для trivial low-risk изменения отдельный Test Intent не нужен. + +Краткая форма, только применимые пункты: + +- **Поведение:** что увидит пользователь/caller и какую ошибку должен поймать тест. +- **Варианты и границы:** значимые альтернативы, edge/error states, concurrency. +- **Исключено:** какие детали реализации не являются контрактом. +- **Уровень:** самая дешёвая boundary, которая надёжно различает правильный + и сломанный исход; критерии — в local testing convention. + +Пример: «Два refresh с одним token: один успешный ответ, второй 401; +новая session остаётся рабочей. Проверить на реальной изолированной SQLite. +Не проверять порядок private calls». Mock race не доказывает DB atomicity. + +Для каждого теста ответь: «Какой regression он обнаружит?» Не создавай +`Foo.test.tsx` из-за появления `Foo.tsx`. По умолчанию не тестируй obvious JSX, +CSS classes, каждый wrapper/aria attribute, decorators, trivial CRUD wiring, +framework и сторонние библиотеки. Проверяй доступность через пользовательское +действие/смысл, когда это риск; декларативный атрибут сам по себе не test intent. + +Bug fix сначала воспроизводит исход на responsible boundary. После подходящих +проверок остановись: количество тестов и coverage ради coverage не являются целью. +Новые harness/framework/fixtures не нужны, когда существующий setup покрывает риск. diff --git a/.ai/migrations/conventions-v1/audit.md b/.ai/migrations/conventions-v1/audit.md new file mode 100644 index 0000000..01d194e --- /dev/null +++ b/.ai/migrations/conventions-v1/audit.md @@ -0,0 +1,177 @@ +# Conventions v1: semantic audit + +Дата: 2026-09-15. Read-only audit production code; conventions изменены отдельно. +Snapshot: root `2567a755569e6ef6ba12063b62f202fe52884ce7`, frontend +`0244acf429c525b23117f165bbe52ef5403eefd3`, backend +`660b250ecd05e38784690b9a9adb381764c0ca7c`. На старте worktrees чистые. + +## Scope и метод + +Прочитаны existing AGENTS/.ai, manifests, TypeScript/Biome/ESLint, dependency +rules, test/Storybook setup и OpenAPI flow. Проверены representative paths: +auth/refresh, bank cards/operations/summary, portfolio/income page VM, account +errors, instrument search и T-Bank/MOEX/NCR boundaries. `rg` использован для +callers, HTTP/Prisma/domain imports, raw fetch/SQL, logging, error strings и +state patterns. Точечная executable reproduction проверяет ValidationPipe → filter. + +Это reviewed inventory подтверждённых findings, не полный автоматический +счётчик всех файлов. P0 — явное архитектурное нарушение / высокий риск; +P1 — неверная ответственность / существенная inconsistency; +P2 — cleanup / naming / менее важная consistency issue. +Confidence: High / Medium / Low. P0 по проверенной области не выявлены; +это не security certification. + +| Severity | Findings | +| --- | --- | +| P0 | 0 | +| P1 | 4 | +| P2 | 3 | + +## BE-001 — Validation теряет ошибки полей + +- **Location:** [configureApp](../../../backend/src/main.ts), строка 28; + [HttpExceptionFilter](../../../backend/src/common/filters/http-exception.filter.ts), + `catch` / `validationErrors`. +- **Convention:** [Errors](../../../backend/.ai/conventions/error-handling.md), + [требование](../../../specs/openspec/specs/user-authentication/spec.md#requirement-стандартные-ответы-об-ошибках). +- **Current state:** стандартный ValidationPipe формирует `message: string[]`; + filter трактует его как `ValidationError[]`, возвращая пустой `errors`. +- **Expected state:** invalid request → 400 / VALIDATION_ERROR с применимыми + `{ pointer, detail }`; вложенные поля обрабатываются согласно контракту. +- **Severity:** P1. **Confidence:** High. +- **Reason:** публичный контракт теряет field details; типовой filter unit test + и OpenAPI schema parity не проходят реальную validation pipeline. +- **Suggested migration:** отдельный behavior fix в Auth/API validation slice: + согласовать output pipe с filter; доказать через configured HTTP request. + Не добавлять новый validation framework. + +## BE-002 — Repository решает business eligibility и HTTP outcome + +- **Location:** [BankCardsRepository](../../../backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts), + `card(active)` и `write`; callers в + [BankOperationsService](../../../backend/src/modules/banking/bank-cards/application/bank-operations.service.ts). +- **Convention:** [Data](../../../backend/.ai/conventions/data.md), + [application boundaries](../../../backend/.ai/architecture.md#ответственность-и-направление-зависимостей). +- **Current state:** repository импортирует application `conflict/missing`, + решает запрет archived card и переводит общий P2002 в DuplicateCategory. +- **Expected state:** repository владеет owner-filtered reads, transaction/retry + и техническим DB outcome; application/domain определяет eligibility/public error. +- **Severity:** P1. **Confidence:** High. +- **Reason:** storage helper знает business policy нескольких callers; + P2002 не всегда означает duplicate category. Утечка доступа не утверждается. +- **Suggested migration:** Cards/Operations slice: перенести policy и mapping + к ответственным сценариям, сохранив lookup/check/write внутри той же transaction, + retry budget, idempotency, CAS и действующие public codes. + +## FE-001 — Самостоятельные банковские действия находятся в entity UI + +- **Location:** [BankCardForm](../../../frontend/src/entities/bank-card/ui/BankCardForm.tsx), + [CategoryManager](../../../frontend/src/entities/bank-card/ui/CategoryManager.tsx); + consumers — CardsPage и CardPage. +- **Convention:** [FSD ownership](../../../frontend/.ai/architecture.md#выбор-владельца). +- **Current state:** entity UI координирует создание/редактирование карты, + idempotency intent, submit, version adoption, создание/редактирование категорий. +- **Expected state:** самостоятельные пользовательские workflows имеют feature + owner; entity сохраняет модели, queries и presentation понятия карты/категории. +- **Severity:** P1. **Confidence:** High. +- **Reason:** это полноценные reused capabilities с собственным lifecycle, + а не вывод о слое из наличия одной кнопки или mutation. +- **Suggested migration:** по одному действию в Cards slice; presentation fields + сохранять/reuse, не перемещать всю entity и все её hooks механически. + +## FE-002 — Views банковских страниц владеют query state selection + +- **Location:** [CardPage.view](../../../frontend/src/pages/card/ui/CardPage.view.tsx), + [CardsPage.view](../../../frontend/src/pages/cards/ui/CardsPage.view.tsx), + соответствующие `CardPage.vm.ts` / `CardsPage.vm.ts` рядом. +- **Convention:** [Container/VM/view](../../../frontend/.ai/conventions/ui.md#container-vm-и-view). +- **Current state:** VM отдаёт query objects; views выбирают initial/cached/error + states через `data/isLoading/isError/isPlaceholderData` и вызывают `refetch`. +- **Expected state:** VM выбирает presentation states и retry callbacks, + views отображают данные/действия. DOM scroll/focus остаются UI responsibility. +- **Severity:** P1. **Confidence:** High. +- **Reason:** presentation зависит от query lifecycle и принимает решения, + которые existing account-income pattern уже размещает в VM. +- **Suggested migration:** Cards slice, сначала один экран: перенести state + selection без изменения loading/cached/recovery, URL pagination и scroll anchor. + Не строить generic state machine для всех страниц. + +## FE-003 — Application navigation находится в shared UI + +- **Location:** [AppShell](../../../frontend/src/shared/ui/AppShell.tsx), + `items`, `useLocation`, `Outlet`, nav rendering; public export в shared/ui. +- **Convention:** [FSD и устройство](../../../frontend/.ai/architecture.md). +- **Current state:** shared owner содержит конкретные `/accounts`, `/cards`, + `/stocks`, `/bonds`, `/settings` и application navigation. +- **Expected state:** navigation/composition принадлежит app; shared сохраняет + только действительно общие presentation primitives при текущей потребности. +- **Severity:** P2. **Confidence:** High. +- **Reason:** известное legacy placement уже отмечалось старым convention; + dependency graph не доказывает отсутствие business meaning в shared. +- **Suggested migration:** отдельный navigation slice вместе с app callers и CSS, + без новой оболочки над Mantine и без переразметки пользовательского UI. + +## FE-004 — Connection recovery классифицируется повторно + +- **Location:** [AccountPageError](../../../frontend/src/pages/account/ui/components/AccountPageError.view.tsx), + [AccountsPageError](../../../frontend/src/pages/accounts/ui/components/AccountsPageError.view.tsx), + [StocksPage.view](../../../frontend/src/pages/stocks/ui/StocksPage.view.tsx), + [BondsPage.view](../../../frontend/src/pages/bonds/ui/BondsPage.view.tsx). +- **Convention:** [Frontend errors](../../../frontend/.ai/conventions/data.md#errors), + [pattern convergence](../../conventions/code-health.md#search--reuse--converge). +- **Current state:** несколько owners повторяют `TBANK_NOT_CONNECTED` / + `INVALID_TBANK_TOKEN` comparisons; presentation/retry actions различаются. +- **Expected state:** общая классификация принадлежит одному допустимому owner, + локальный текст/actions остаются у сценария; unknown code сохраняет fallback. +- **Severity:** P2. **Confidence:** High. +- **Reason:** повторяется техническое decision rule. Различный UI сам по себе + не нарушение; generated `code: string` не разрешает выдумывать закрытый enum. +- **Suggested migration:** Accounts/Portfolio, затем Instruments/Settings: + унифицировать только classification с сохранением всех текущих recovery paths. + +## BE-003 — Invalid-token protocol основан на Error.message + +- **Location:** [TbankClient](../../../backend/src/integrations/tbank/tbank.client.ts), + `call` (throw Error); [TbankTokenService](../../../backend/src/integrations/tbank/tbank-token.service.ts), + `translateInvalidToken`; [InstrumentMetadataService](../../../backend/src/integrations/tbank/instrument-metadata.service.ts), + invalid-token catch. +- **Convention:** [Errors](../../../backend/.ai/conventions/error-handling.md), + [Integrations](../../../backend/.ai/conventions/integrations.md). +- **Current state:** low-level client бросает `Error('INVALID_TBANK_TOKEN')`, + два consumers сравнивают строку сообщения для выбора recovery/HTTP mapping. +- **Expected state:** один явный local typed/code outcome на integration boundary, + public HTTP translation остаётся у существующего owner. +- **Severity:** P2. **Confidence:** High. +- **Reason:** изменение диагностического текста ломает machine behavior; + raw provider exception сейчас уже скрыт, подтверждённой утечки нет. +- **Suggested migration:** Instruments/Settings integration slice; заменить + marker и всех consumers одной связной правкой, сохранив auth/unavailable + distinction и wire codes. Общий enterprise error framework не нужен. + +## Что не признано нарушением + +- Page-local components account/income/events и их flat views допустимы. + Размер файла, `map` или handler сами по себе не определяют feature/entity. +- Прямые Prisma queries связных auth/banking/connection services допускаются + existing data convention; standalone use cases уже используют repository. +- URL-applied values и editable draft dates — разные состояния; синхронизация + draft после navigation не равна лишнему server-state cache. +- Повторы `filter(Boolean).join(" · ")` формируют display metadata, а не CSS + classes; это не основание устанавливать `clsx`. +- `ProblemDetailsDto.code: string` и typed wire literals — ограничение принятого + генератора, не основание создавать ручную копию backend enum. +- Stories показывают synthetic presentation; отсутствие story/test у каждого + component не считается finding. По выполненным поискам не найдены raw fetch + вне shared/api, production console или raw SQL; это не доказательство полноты. + +## Evidence BE-001 + +Из backend после `nvm use --silent`: одноразовый `node -r ts-node/register` +загрузил actual CredentialsDto, ValidationPipe и HttpExceptionFilter. +Input `{ email: 'bad', password: 'x' }` → exception `message: string[]` → +`{ status: 400, code: 'VALIDATION_ERROR', errors: [] }` (assert PASS). +DB/порты/внешняя сеть не использовались. Это proof пары pipe/filter; +исправление всё равно требует configured HTTP regression test. + +Immutable baseline этого анализа; статусы закрытия и evidence будущих работ +вести в [plan](plan.md), не переписывая snapshot под новый код. diff --git a/.ai/migrations/conventions-v1/plan.md b/.ai/migrations/conventions-v1/plan.md new file mode 100644 index 0000000..1767e80 --- /dev/null +++ b/.ai/migrations/conventions-v1/plan.md @@ -0,0 +1,88 @@ +# Conventions v1: migration plan + +Исходный [audit](audit.md): 7 открытых findings — P0: 0, P1: 4, P2: 3. +Это число reviewed findings, а не все violations репозитория. Production +refactoring в задаче conventions не выполнялся. Порядок ниже — рекомендуемые +отдельные работы, без обязательного запуска SDD или repository-wide cleanup. + +## Vertical slices + +| Slice / порядок | Findings и target ownership | Сохранить / Test Intent | Migration и verification | +| --- | --- | --- | --- | +| 1. Auth: invalid request | BE-001; configureApp validation + HTTP filter | 400/code, field pointers/details, safe unknown errors, whitelist semantics; invalid request не вызывает use case | Отдельный behavior fix и configured HTTP regression, без структурного рефакторинга. Auth e2e, filter tests, contract check, backend verify | +| 2. Cards: создание/редактирование | FE-001; feature workflows + entity presentation/queries | Submit blocking, ввод после failure, idempotency ID, version conflict/adoption, archived editing | Сначала одно действие, затем следующее. Только structural change. Existing banking/form RTL/MSW, relevant stories, deps/knip и frontend verify | +| 3. Cards/Operations: просмотр и запись | FE-002 + BE-002; page VM states; application policy; repository transaction | Cached snapshot/refetch failure, URL period/page, scroll anchor; user isolation, CAS, atomic transfer/refund, balances | Раздельные frontend/backend commits; не менять behavior вместе с переносом. RTL/MSW и browser pagination; banking unit + SQLite/HTTP, relevant durability, verify обоих repos | +| 4. Accounts/Portfolio: recovery | FE-004, первая группа consumers; общий classifier у допустимого owner | NOT_FOUND → accounts, connection → settings, transient error → retry; сохранить различия текста/actions и fallback unknown code | Structural convergence одного decision rule. Account/portfolio RTL/MSW и frontend verify; оставшиеся instrument consumers учитываются до полного закрытия FE-004 | +| 5. Instruments/Settings: connection | FE-004 оставшиеся consumers + BE-003; integration marker/translation | Invalid token и unavailable различаются; bounded retry, cache isolation/generation, metadata fallback | Сначала marker и все backend callers, отдельно frontend classifier reuse. T-Bank/token/metadata tests, relevant API e2e, frontend recovery tests, verify affected repos | +| 6. Navigation между областями | FE-003; app composition | Active links desktop/mobile, nested Outlet, breadcrumbs, auth fallback, keyboard и narrow viewport | Перенос existing AppShell с CSS/callers, без нового wrapper/UI redesign. Route tests, deps/knip, browser proof и frontend verify | + +Каждый slice начинает с классификации responsibility и просмотра всех callers. +До edits зафиксируй target owner, сохраняемое behavior и применимый +[Test Intent](../../conventions/testing.md). Structural refactor и behavior +change идут отдельными шагами/commits; завершение требует semantic review, +relevant checks и обновления статуса finding с evidence. + +## Ratchet + +Все семь IDs пока **Open**. Следующая migration фиксирует здесь: +`ID → Open/Closed → repo commit/PR → команда и результат`. +Закрывай finding только после миграции всех перечисленных consumers; частичный +перенос не уменьшает count. Новую находку добавляй отдельным ID с severity, +confidence и scope; изменение охвата не выдавай за улучшение качества. + +Цель — последовательно уменьшать open baseline и не добавлять новых нарушений +в diff. Existing exceptions остаются видимыми и не становятся образцами нового +кода. Исходный audit сохраняет facts своего snapshot. + +## Deterministic follow-ups + +Полный mapping — в [guardrails](../../conventions/guardrails.md). +Existing dependency-cruiser поддерживает `--ignore-known` (подтверждено local +CLI help). При отдельной работе над same-layer imports сначала проверить +candidate rule; если существующих violations нет, baseline не нужен. +При legacy использовать native exact baseline, без directory-wide exclusions. + +Backend import boundaries и `no-console` при повторном regression можно +закрепить встроенными ESLint rules. Markdown `.ai` можно включить явными путями +в existing markdownlint workflow, когда согласована доступность соседних +checkout; сегодня проверка `.ai` выполнена локально тем же инструментом. +Новые analyzers, scripts, dependencies или custom baseline infrastructure +в этом изменении не создаются. + +## Verification conventions v1 + +Проверено 2026-09-15 на согласованном workspace из трёх веток +`codex/engineering-conventions`; docs/specs остались без изменений. + +| Проверка | Результат | +| --- | --- | +| Root `python3 test-workspace-tracking.py` | PASS: exact `.ai` allowlist, соседние repos и вымышленные local/private files сохранены | +| Из docs: `npm exec -- markdownlint-cli2 --config .markdownlint-cli2.jsonc '../AGENTS.md' '../README.md' '../.ai/**/*.md' '../frontend/AGENTS.md' '../frontend/.ai/**/*.md' '../backend/AGENTS.md' '../backend/.ai/**/*.md'` | PASS: 31 Markdown, включая новые файлы, 0 ошибок style/relative links/anchors | +| Docs `npm run verify` | PASS: 42 Markdown, 0 ошибок; проверены также входящие межрепозиторные ссылки | +| Frontend `npm run deps`, `npm run deps:storybook` | PASS: 297/298 modules, 0 нарушений настроенных dependency rules | +| Backend ValidationPipe → filter reproduction | Подтверждён BE-001: invalid CredentialsDto → 400/VALIDATION_ERROR, `errors: []` | +| `git diff --check` в root/frontend/backend/docs/specs | PASS | + +Semantic review первого прохода: маршруты UI-only, API/contract, auth, tests-only, helper/package, +Prisma, logging и migration достигают нужных owners; ссылки compatibility +не дублируют таблицы routing. Общие policy имеют root owner, stack-specific +исключения остались локальными. Проверены новые файлы и отсутствие production, +manifest/lockfile, generated code, CI/release и docs/specs изменений. + +Application build/coverage/e2e целиком не запускались: изменение Markdown и +root tracking не меняет production behavior. Existing dependency checks и +воспроизведение BE-001 подтверждают только указанные границы, не отсутствие +всех semantic/security проблем. Full gates выполняются при будущих code slices. + +## Упрощение структуры после review + +Conventions frontend/backend именуются без repo-префиксов. Устройство и правила +архитектуры объединены в единственный `.ai/architecture.md` каждого repo. +Routing и карта источников объединены в `AGENTS.md`; прежние CONVENTIONS/SOURCES +и отдельные architecture conventions удалены без compatibility-заглушек. +Активные ссылки обновлены; в audit изменены только ссылки, findings сохранены. +Исторические архивы specs остаются источниками своего времени. + +Повторная проверка: existing markdownlint-cli2 с docs config по root/frontend/backend +AGENTS, README и `.ai/**/*.md` — 27 файлов, 0 ошибок, включая относительные ссылки +и якоря. `git diff --check` — PASS. Production-код и dependencies не менялись. diff --git a/.env.compose.example b/.env.compose.example new file mode 100644 index 0000000..d9639ab --- /dev/null +++ b/.env.compose.example @@ -0,0 +1,12 @@ +# Copy to a restricted, untracked file. Never commit filled values. +# Full release image references; use published version tags, not latest. +FRONTEND_IMAGE=git.personal.ksv741.ru/wealthledger/frontend:0.1.0 +BACKEND_IMAGE=git.personal.ksv741.ru/wealthledger/backend:0.1.0 +FRONTEND_PORT=8080 +FRONTEND_ORIGIN=https://wealthledger.example.com +# Absolute directory, pre-created with owner UID/GID 1000:1000 and mode 0700. +DATA_DIRECTORY=/srv/wealthledger/data +# Generate once and keep across updates. Store an independent key backup. +JWT_SECRET= +TOKEN_ENCRYPTION_KEY= +LOG_LEVEL=info diff --git a/.env.development.example b/.env.development.example new file mode 100644 index 0000000..6ea0252 --- /dev/null +++ b/.env.development.example @@ -0,0 +1,10 @@ +# Local-only fixture values. Never use this file for a server deployment. +# Base-file interpolation happens before overrides; these references are replaced. +FRONTEND_IMAGE=wealthledger-frontend:development +BACKEND_IMAGE=wealthledger-backend:development +DATA_DIRECTORY=/unused-development-volume +FRONTEND_PORT=5173 +FRONTEND_ORIGIN=http://localhost:5173 +JWT_SECRET=local-development-only-not-a-server-secret +TOKEN_ENCRYPTION_KEY=MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY= +LOG_LEVEL=info diff --git a/.gitea/workflows/commits.yml b/.gitea/workflows/commits.yml index 2372d2c..c4c8d9e 100644 --- a/.gitea/workflows/commits.yml +++ b/.gitea/workflows/commits.yml @@ -22,5 +22,5 @@ jobs: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} run: | - printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict - npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA" + printf '%s\n' "$PR_TITLE" | npm exec --no -- commitlint --strict + npm exec --no -- commitlint --strict --from "$BASE_SHA" --to "$HEAD_SHA" diff --git a/.gitea/workflows/security.yml b/.gitea/workflows/security.yml deleted file mode 100644 index 247a371..0000000 --- a/.gitea/workflows/security.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Workspace security - -on: - push: - branches: - - '**' - pull_request: - schedule: - - cron: '17 5 * * *' - -jobs: - security: - runs-on: ubuntu-latest - steps: - - uses: https://github.com/actions/checkout@v4 - with: - fetch-depth: 0 - - name: Record checked revision - run: git rev-parse HEAD - - name: Scan history with Gitleaks 8.30.1 - run: | - curl --fail --location --silent --show-error https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz -o /tmp/gitleaks.tar.gz - echo '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb /tmp/gitleaks.tar.gz' | sha256sum --check - mkdir -p /tmp/gitleaks-bin - tar -xzf /tmp/gitleaks.tar.gz -C /tmp/gitleaks-bin gitleaks - /tmp/gitleaks-bin/gitleaks git --log-opts=--all --redact=100 --no-banner - - uses: https://github.com/actions/setup-node@v4 - with: - node-version-file: .nvmrc - - name: Audit runtime and development dependencies - run: npm audit --include=dev --audit-level=low diff --git a/.gitea/workflows/verify.yml b/.gitea/workflows/verify.yml deleted file mode 100644 index b64c773..0000000 --- a/.gitea/workflows/verify.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Workspace verify - -on: - push: - branches: ['**'] - pull_request: - -jobs: - verify: - runs-on: ubuntu-latest - steps: - - uses: https://github.com/actions/checkout@v4 - - uses: https://github.com/actions/setup-node@v4 - with: - node-version-file: .nvmrc - - run: npm ci - - run: npm run verify diff --git a/.gitignore b/.gitignore index 49d18c8..1dc0617 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,25 @@ !/.gitignore !/AGENTS.md !/README.md -!/test-workspace-tracking.py +!/compose.yaml +!/compose.development.yaml +!/.env.compose.example +!/.env.development.example +!/.ai/ +/.ai/* +!/.ai/conventions/ +/.ai/conventions/* +!/.ai/conventions/git.md +!/.ai/conventions/dependencies.md +!/.ai/conventions/code-health.md +!/.ai/conventions/testing.md +!/.ai/conventions/guardrails.md +!/.ai/migrations/ +/.ai/migrations/* +!/.ai/migrations/conventions-v1/ +/.ai/migrations/conventions-v1/* +!/.ai/migrations/conventions-v1/audit.md +!/.ai/migrations/conventions-v1/plan.md !/.nvmrc !/package.json !/package-lock.json @@ -11,14 +29,8 @@ !/CHANGELOG.md !/commitlint.config.cjs !/renovate.json -!/test-release.cjs -!/scripts/ -/scripts/* -!/scripts/check-release.cjs !/.gitea/ /.gitea/* !/.gitea/workflows/ /.gitea/workflows/* !/.gitea/workflows/commits.yml -!/.gitea/workflows/verify.yml -!/.gitea/workflows/security.yml diff --git a/.release-it.json b/.release-it.json index e5f07b0..f2bbfa2 100644 --- a/.release-it.json +++ b/.release-it.json @@ -18,9 +18,6 @@ "gitlab": { "release": false }, - "hooks": { - "after:git:init": "node scripts/check-release.cjs" - }, "plugins": { "@release-it/conventional-changelog": { "infile": "CHANGELOG.md", diff --git a/AGENTS.md b/AGENTS.md index e214f77..e48df9a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,29 +1,32 @@ -# WealthLedger +# WealthLedger workspace -## Workspace +Root содержит workspace-навигацию и конфигурацию стандартных инструментов. +Самописные скрипты не добавляются; CI выполняет только commitlint. +`frontend`, `backend`, `docs`, `specs` — самостоятельные Git-репозитории. +Root tracking ограничен allowlist; локальные данные и nested repos в него +не добавляются. `references/**`, dated audits и архивы — read-only. -`frontend`, `backend`, `docs` и `specs` — независимые Git-репозитории. -Root — отдельный Git-репозиторий `WealthLedger/main`; команды Git выполняй -из нужного repo. Root tracking ограничен allowlist в `.gitignore`; -вложенные repos и локальные данные в main не добавляй, включая `git add -f`. -Структура checkout и запуск — в [README](README.md). -`references/**`, dated audits и архивы — read-only источники. +Инструкции root относятся только к root. Работа внутри другого репозитория +начинается с его собственного AGENTS; он содержит все обязательные правила +без наследования root conventions и без чтения соседних checkout. +Публичные API artifacts передаются явно; CI каждого repo использует локальные +входы. Справочные источники не становятся обязательной цепочкой инструкций. -## Работа +Начни с результата и affected areas. До edits проследи поведение/invariants, +найди owner и 1–3 примера. Работай напрямую; OpenSpec/SDD — по явному запросу. -1. Начинай с задачи пользователя, ожидаемого результата и affected areas. Работай напрямую: OpenSpec/SDD artifacts, approvals и archive не являются prerequisite. Используй прежний SDD workflow только по явному запросу. -2. Прочитай local `AGENTS.md` affected repo. Для frontend/backend прочитай его `.ai/CONVENTIONS.md` и `.ai/SOURCES.md`, затем только темы, которые активируют задача, пути или обнаруженная граница. -3. До выбора реализации проследи поведение и инварианты, найди ближайший существующий owner и 1–3 подходящих примера. Код показывает реализацию; документированный контракт не выводится из случайного аналога. -4. Выбирай простейшее полное решение у ответственного владельца. Новая abstraction или dependency требует текущей потребности; посторонний рефакторинг оставляй отдельно. -5. Выбирай тип знания по задаче через [вход docs](docs/README.md): новая функция или межрепозиторное поведение → соответствующая область, спецификации, нужные ADR, затем реализация; ошибка с неясным ожиданием → спецификация, docs, код и тесты; архитектурное изменение → архитектура/ADR, затронутые спецификации и код. Локальная ошибка сборки, линтера или очевидный implementation bug → инструкции и код нужного repo; документация → docs/AGENTS.md и CONTRIBUTING.md. Расширяй контекст по конкретному пробелу; missing required source или конфликт требований сообщи до зависимого решения. -6. При новой границе и перед завершением повтори routing по actual staged, unstaged и новым файлам. Сохраняй чужие изменения; уточнение контекста не расширяет задачу. -7. Проверь результат по задаче, рискованным случаям и local gates. Сохрани краткие команды/результаты; сообщи оставшиеся ограничения. Успех lint/tests не заменяет semantic review. +| Root задача | Прочитать | +| --- | --- | +| Helper/pattern, refactor, scope | [Code health](.ai/conventions/code-health.md) | +| Dependency/tooling | [Dependencies](.ai/conventions/dependencies.md), [Guardrails](.ai/conventions/guardrails.md) | +| Нетривиальные tests | [Test Intent](.ai/conventions/testing.md) | +| Git/branch/commit/PR/tracking | [Git](.ai/conventions/git.md) | +| Release/hotfix | [README](README.md#релизы-и-ветки) | -Перед Node-командой активируй `.nvmrc` affected repo через `nvm use --silent`. - -## Ветки и релизы - -Новая работа — в отдельной ветке от `develop`, затем PR в `develop`. -Прямой push в `main` запрещён. Commits и заголовки PR — Conventional Commits. -Перед release/hotfix, merge между `main` и `develop`, изменением release-конфигурации -или CI refs прочитай [процесс релиза](README.md#релизы-и-ветки). +Перед Node-командой активируй местную `.nvmrc`: `nvm use --silent`. +При новой границе и перед завершением повтори routing по staged/unstaged/new +files; сохраняй чужие изменения. Missing source/конфликт требований сообщи +до зависимого решения. После diff выполни +[Boy Scout review](.ai/conventions/code-health.md#boy-scout-review) и +[relevant gates](.ai/conventions/guardrails.md). Запиши краткие команды, +результаты и ограничения; lint не заменяет semantic review. diff --git a/CHANGELOG.md b/CHANGELOG.md index f3c1d88..f3d9a5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [0.2.0](https://git.personal.ksv741.ru/WealthLedger/main/compare/v0.1.0...v0.2.0) (2026-09-16) + +### Features + +* add containerized application delivery [#11](https://git.personal.ksv741.ru/WealthLedger/main/issues/11) ([7e382d4](https://git.personal.ksv741.ru/WealthLedger/main/commit/7e382d460be296b6c88e17084fff17bb7728f6ea)) + +### Documentation + +* align inventory with simplified conventions ([4a78dd6](https://git.personal.ksv741.ru/WealthLedger/main/commit/4a78dd640d4a1db1b7c9dc4036375c076d55b34d)) +* establish workspace engineering conventions ([93dd553](https://git.personal.ksv741.ru/WealthLedger/main/commit/93dd5539ad8bd10e11c59708e7b66affc19e4883)) + +### Maintenance + +* simplify root tooling and repository boundaries ([95e4361](https://git.personal.ksv741.ru/WealthLedger/main/commit/95e4361599244598126394aa658a0a27d77c0686)) + ## 0.1.0 (2026-09-14) ### Documentation diff --git a/README.md b/README.md index 3237b47..b2f54da 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Локальное приложение для портфеля T-Bank, операций, дохода и будущих выплат, а также ручного учёта банковских карт и расходов. -[Обзор продукта](docs/overview.md) и [документация системы](docs/README.md). +[Обзор продукта](https://git.personal.ksv741.ru/WealthLedger/docs/src/commit/c557af4ea51cd3c578d0755e909e62e06462b5bc/overview.md) и [документация системы](https://git.personal.ksv741.ru/WealthLedger/docs/src/commit/c557af4ea51cd3c578d0755e909e62e06462b5bc/README.md). ## Структура workspace @@ -12,10 +12,10 @@ Workspace состоит из пяти независимых Git-репозит | Путь от root | Репозиторий Gitea | Назначение | | --- | --- | --- | | `.` | `WealthLedger/main` | Вход в workspace, инструкции и правила root tracking | -| `frontend/` | `WealthLedger/frontend` | [React/Vite UI](frontend/README.md) | -| `backend/` | `WealthLedger/backend` | [NestJS API, Prisma и SQLite](backend/README.md) | -| `docs/` | `WealthLedger/docs` | [Объяснение системы](docs/README.md) | -| `specs/` | `WealthLedger/specs` | [Требования и OpenSpec](specs/README.md) | +| `frontend/` | `WealthLedger/frontend` | [React/Vite UI](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/README.md) | +| `backend/` | `WealthLedger/backend` | [NestJS API, Prisma и SQLite](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/README.md) | +| `docs/` | `WealthLedger/docs` | [Объяснение системы](https://git.personal.ksv741.ru/WealthLedger/docs/src/commit/c557af4ea51cd3c578d0755e909e62e06462b5bc/README.md) | +| `specs/` | `WealthLedger/specs` | [Требования и OpenSpec](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/README.md) | ### Получение checkout @@ -34,12 +34,14 @@ git clone ssh://git@git.personal.ksv741.ru:2222/WealthLedger/specs.git specs Для существующего workspace клонируйте только отсутствующие checkout в пустые каталоги. Проверяйте состояние каждого repo отдельно, например `git -C frontend status`. Обновление main не обновляет соседние repos; -общего lockfile revisions нет. Относительные ссылки требуют указанных имён каталогов. +общего lockfile revisions нет. Такая раскладка удобна для совместной работы, +но каждый checkout можно использовать отдельно. ### Что хранится в main -Root отслеживает входные документы, npm/release-конфигурацию, changelog, -проверки tracking/release и три CI workflow. Точный allowlist — в `.gitignore`. +Root отслеживает входные документы, собственные engineering conventions и исторический migration +inventory в `.ai/`, npm/release-конфигурацию, changelog, +Compose configuration, безопасные env examples и один CI workflow с commitlint. Точный allowlist — в `.gitignore`. Остальные root-пути игнорируются по умолчанию: вложенные repos, `.env`, базы и WAL, импорты выписок, PDF, `references/`, `design/`, `todo/` и локальные настройки инструментов. @@ -51,6 +53,35 @@ Root отслеживает входные документы, npm/release-ко Не запускайте очистку workspace через `git clean -fdx`: ignored-пути содержат локальные данные. Правила tracking не удаляют файлы и вложенные `.git`. +## Запуск через Docker Compose + +Нужны Docker Engine и Compose >= 2.24.4, а для development — checkout +`frontend/` и `backend/` рядом с root. Компоненты собираются независимо: + +```bash +docker compose --env-file .env.development.example \ + -f compose.yaml -f compose.development.yaml up -d --build --wait --wait-timeout 180 +``` + +Откройте `http://localhost:5173`: Vite HMR и `/api/` используют один origin, +Nest работает в watch mode. Host `node_modules` и `.env` не монтируются. +Данные development хранятся отдельно в named volume `development-data` проекта `wealthledger-dev`; +обычные restart/down их сохраняют. Изменение dependencies/config требует rebuild. + +Production использует `compose.yaml` и restricted env по `.env.compose.example`: +две ссылки на опубликованные версии images, persistent absolute host data path, +постоянные JWT/encryption secrets и публичный HTTPS origin. Каталог данных +должен существовать и принадлежать UID/GID 1000:1000. Только frontend публикует +`127.0.0.1:8080`; серверный Caddy направляет туда весь домен. Backend port +остаётся внутренним, migration service использует тот же backend image. + +Полная процедура установки, независимых обновлений, stopped-backend backup +и explicit restore — [документация](https://git.personal.ksv741.ru/WealthLedger/docs), +страница `architecture/deployment.md`. +Перед backend update обязательны остановка, backup и успешная migration. +Image rollback не восстанавливает старую базу. Публикация component images +принадлежит их Gitea workflows; root CI остаётся только commitlint. + ## Подготовка и запуск Нужны nvm и Node из `.nvmrc` каждого npm-repo (сейчас 24.21.0, npm 11.19). @@ -60,7 +91,7 @@ Root отслеживает входные документы, npm/release-ко После смены major Node переустановите native dependencies через `npm ci`. Разрешения install scripts заданы в `package.json` соответствующего repo. -Для нового backend создайте `.env` по [backend README](backend/README.md), +Для нового backend создайте `.env` по [backend README](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/README.md), замените оба placeholder-секрета и проверьте `DATABASE_URL`. Существующий `.env` сохраняйте. Из backend: @@ -73,13 +104,13 @@ npm run start:dev Build генерирует Prisma client. Перед миграцией существующей базы сохраните согласованный backup и ключ шифрования; процедуру и ограничения отката смотрите -в [описании хранения](docs/architecture/containers.md#сохранение-и-восстановление-данных). +в [описании хранения](https://git.personal.ksv741.ru/WealthLedger/docs/src/commit/c557af4ea51cd3c578d0755e909e62e06462b5bc/architecture/containers.md#сохранение-и-восстановление-данных). Известная ошибка fresh migration при `RUST_LOG=warn/error` устранена в `backend/prisma.config.ts`: CLI удаляет унаследованную переменную. Используйте штатный `prisma migrate deploy` из backend; прежний Prisma blocker -снят. [Подробности и границы проверки](backend/README.md#реальная-sqlite-и-сохранность-данных). +снят. [Подробности и границы проверки](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/README.md#реальная-sqlite-и-сохранность-данных). -В другом терминале подготовьте `.env` по [frontend README](frontend/README.md), +В другом терминале подготовьте `.env` по [frontend README](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/README.md), затем из frontend: ```bash @@ -98,7 +129,7 @@ npm run dev OpenSpec не нужен для запуска приложения и применяется по явному запросу. Specs использует Node из своей `.nvmrc`; проверенная версия CLI — 1.10.0. Store `wealth-ledger-specs` расположен в `specs/openspec/`. -[Работа с OpenSpec](specs/AGENTS.md), [команды validation](specs/README.md#проверка). +[Работа с OpenSpec](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/AGENTS.md), [команды validation](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/README.md#проверка). ## Проверки @@ -106,17 +137,18 @@ Store `wealth-ledger-specs` расположен в `specs/openspec/`. | Где | Команда | Что проверяет | | --- | --- | --- | -| root | `python3 test-workspace-tracking.py` | Массовый add на вымышленных данных; сохранность файлов и вложенных Git | +| root | `npm exec --no -- commitlint --strict --from --to ` | Формат сообщений коммитов | | root | `git diff --check` | Whitespace в изменениях root | | frontend | `npm run verify` | Типы, lint, тесты, зависимости, OpenAPI snapshot и сборки | | backend | `npm run verify` | Lint, типы, unit tests и build | -| backend | `npm run test:e2e` | HTTP/SQLite suites и контракт с соседним frontend | +| backend | `npm run test:e2e` | HTTP/SQLite suites и локальный OpenAPI artifact | | docs | `npm run verify` | Markdown, относительные ссылки и diff | -Python 3 нужен только для проверки root tracking. Backend e2e использует +Root CI выполняет только commitlint: проверяет сообщения коммитов и заголовок PR. +Allowlist root проверяется вручную по staged paths. Backend e2e использует временные базы; verify отдельно не доказывает HTTP/DB-сценарии или доступность T-Bank. Полные команды и ограничения принадлежат README -[frontend](frontend/README.md#проверка) и [backend](backend/README.md#проверка). +[frontend](https://git.personal.ksv741.ru/WealthLedger/frontend/src/commit/9f7e342333b80bfacfd5bc394a2dcd0613d33ff3/README.md#проверка) и [backend](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/README.md#проверка). Для изменения проекта начните с [AGENTS](AGENTS.md). ## Релизы и ветки @@ -143,18 +175,19 @@ Commits и заголовки PR — Conventional Commits. `feat:` повыша 1. Обновить `main`, `develop` и теги командой `git fetch origin --tags`. Проверить, что текущий `develop` содержит последний релиз из `main`. Создать `release/` от проверенного `develop` и выполнить `npm ci`. -2. Для backend и docs обновить `.ci/refs.json`: полные SHA совместимых соседних - репозиториев. Эти revisions должны быть доступны в Gitea. Проверить именно - их совместимость; движущиеся ветки не являются фиксацией состава релиза. - Закоммитить обновлённые pins до подготовки версии. +2. Проверить локальные входы и публичные artifacts выпускаемого репозитория. + Совместимость развёрнутых участников согласовать отдельно через публичные + контракты; CI не скачивает соседние checkout и не требует их SHA. 3. Выполнить `npm run release:preview`. Проверить номер и changelog, затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`, обновляет `package.json`, lockfile и `CHANGELOG.md`; теги и push не выполняет. - Не использовать обход branch/clean/tag guards. Незакоммиченные файлы, - неподходящая ветка, отсутствие изменений и уже подготовленная версия блокируют выпуск. + В root встроенные проверки release-it контролируют ветку, чистое дерево и + наличие коммитов. Теги и соответствие версии последнему принятому тегу + проверяются вручную; уже подготовленную версию повторно не повышать. 4. Проверить diff релизного коммита и штатные gates. Отправить только релизную ветку: `git push -u origin HEAD`. Открыть PR в `main` с заголовком - `chore(release): X.Y.Z`. После зелёных `verify`, `security`, `commits` + `chore(release): X.Y.Z`. После зелёных обязательных проверок (`commits` в root; + `verify`, `security`, `commits` в остальных repo) выполнить **fast-forward-only merge**. Если `main` изменился, сначала согласовать историю и повторить проверку кандидата; не обходить защиту. 5. После merge получить `main` и убедиться, что его HEAD — проверенный релизный @@ -184,25 +217,26 @@ Commits и заголовки PR — Conventional Commits. `feat:` повыша `release/hotfix-` в `main`, затем обязательно возвращается в `develop`. Если ломается публичный контракт, major сохраняется и для hotfix. -Backend CI проверяет runtime OpenAPI с frontend; docs CI — межрепозиторные -ссылки. Для `main`, PR в `main` и `release/*` соседи берутся из `.ci/refs.json`; -для разработки — из `develop`. SHA записываются в логи. Для изменения сразу -нескольких репозиториев сначала согласовать их revisions, затем проверять -релизный набор; порядок публикации сам по себе не доказывает совместимость. +Каждый CI использует только свой checkout. Backend генерирует OpenAPI без сервера, +сверяет генератор с configured app и не хранит JSON в Git; frontend проверяет types по входному +snapshot. Docs проверяет внутренние ссылки; внешние источники даются как +справочные веб-ссылки. Обновление API artifact потребителя — отдельное действие. +Зелёные локальные gates и порядок публикации не доказывают совместимость +развёрнутых версий. Процедуры выпуска находятся также в README каждого repo; +доступ к root для выпуска другого репозитория не требуется. В Gitea для `main` и `develop` нужны запрет прямого push и force-push, -обязательные проверки `verify`, `security`, `commits` и запрет обхода правил +обязательные проверки: в root только `commits`, в остальных repo — +`verify`, `security`, `commits`; также нужен запрет обхода правил администратором. Требование отдельного одобрения review не вводится для разработки одним владельцем. Ветка по умолчанию — `develop`; Renovate тоже отправляет PR в `develop`. Теги `v*` защищены, создание разрешается владельцу релиза; их удаление или перенос не входит в обычный процесс. -`node test-release.cjs` из root проверяет настоящий release-it во временном Git. -Для проверки конфигурации соседа после установки его dependencies: -`node test-release.cjs frontend` (аналогично backend, docs, specs). -Тест не публикует refs и не использует рабочие базы. Root `npm run verify` -включает tracking и этот тест; frontend/backend/docs сохраняют свои gates, -backend дополнительно требует `npm run test:e2e`. +В root нет собственного test/verify runner и release hooks. Release-it и +commitlint используются напрямую. Остальные репозитории сохраняют свои gates. +Если в защите веток root ранее были обязательными `verify` и `security`, их +нужно убрать из required checks в Gitea; YAML не меняет настройки сервера. Npm publishing, автоматический deploy и карточки Gitea Releases не включены. Граница выпуска — принятый коммит, changelog и Git-тег. diff --git a/compose.development.yaml b/compose.development.yaml new file mode 100644 index 0000000..7aa8755 --- /dev/null +++ b/compose.development.yaml @@ -0,0 +1,53 @@ +# Requires Compose >= 2.24.4 for !override. Always use a separate project name. +name: wealthledger-dev + +x-development-backend: &development-backend + image: wealthledger-backend:development + pull_policy: build + build: + context: ./backend + target: development + read_only: false + environment: + NODE_ENV: development + volumes: !override + - development-data:/data + +services: + migration: + <<: *development-backend + + backend: + <<: *development-backend + healthcheck: + test: [CMD, node, -e, 'fetch("http://127.0.0.1:3000/api/v1/auth/me").then(async r=>{await r.arrayBuffer();process.exit(r.status===401?0:1)}).catch(()=>process.exit(1))'] + interval: 10s + timeout: 3s + start_period: 20s + retries: 6 + volumes: !override + - development-data:/data + - ./backend/src:/app/src:ro + + frontend: + image: wealthledger-frontend:development + pull_policy: build + build: + context: ./frontend + target: development + read_only: false + volumes: + - ./frontend/src:/app/src:ro + - ./frontend/public:/app/public:ro + environment: + VITE_API_BASE_URL: '' + API_PROXY_TARGET: http://backend:3000 + healthcheck: + test: [CMD, node, -e, 'fetch("http://127.0.0.1:8080/").then(async r=>{await r.arrayBuffer();process.exit(r.ok?0:1)}).catch(()=>process.exit(1))'] + interval: 10s + timeout: 3s + start_period: 10s + retries: 6 + +volumes: + development-data: diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..654ff20 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,51 @@ +name: wealthledger + +x-backend: &backend + image: ${BACKEND_IMAGE:?Set versioned backend image} + environment: + NODE_ENV: production + PORT: '3000' + DATABASE_URL: file:/data/wealthledger.db + FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:?Set browser-visible origin} + JWT_SECRET: ${JWT_SECRET:?Set persistent JWT secret} + TOKEN_ENCRYPTION_KEY: ${TOKEN_ENCRYPTION_KEY:?Set persistent encryption key} + LOG_LEVEL: ${LOG_LEVEL:-info} + volumes: + - type: bind + source: ${DATA_DIRECTORY:?Set persistent absolute host data directory} + target: /data + bind: + create_host_path: false + read_only: true + init: true + security_opt: [no-new-privileges:true] + cap_drop: [ALL] + +services: + migration: + <<: *backend + command: [node, node_modules/prisma/build/index.js, migrate, deploy] + restart: 'no' + healthcheck: + disable: true + + backend: + <<: *backend + scale: 1 + restart: unless-stopped + depends_on: + migration: + condition: service_completed_successfully + + frontend: + image: ${FRONTEND_IMAGE:?Set versioned frontend image} + ports: + - '127.0.0.1:${FRONTEND_PORT:-8080}:8080' + read_only: true + tmpfs: [/tmp] + security_opt: [no-new-privileges:true] + cap_drop: [ALL] + restart: unless-stopped + depends_on: + backend: + condition: service_healthy diff --git a/package-lock.json b/package-lock.json index f697532..877eea7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wealth-ledger-workspace", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wealth-ledger-workspace", - "version": "0.1.0", + "version": "0.2.0", "devDependencies": { "@commitlint/cli": "21.2.2", "@commitlint/config-conventional": "21.2.2", diff --git a/package.json b/package.json index 8378098..4af19e1 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,11 @@ { "name": "wealth-ledger-workspace", - "version": "0.1.0", + "version": "0.2.0", "private": true, "engines": { "node": "24.21.0" }, "scripts": { - "verify": "python3 test-workspace-tracking.py && node test-release.cjs && git diff --check", "release:preview": "release-it --dry-run --ci", "release:prepare": "release-it --ci", "commitlint": "commitlint" diff --git a/scripts/check-release.cjs b/scripts/check-release.cjs deleted file mode 100644 index a2c957b..0000000 --- a/scripts/check-release.cjs +++ /dev/null @@ -1,10 +0,0 @@ -const assert = require("node:assert/strict"); -const { execFileSync } = require("node:child_process"); -const { version } = require("../package.json"); -const git = (...args) => execFileSync("git", args, { encoding: "utf8" }).trim(); - -assert.equal(git("status", "--porcelain"), "", "Commit all changes before preparing a release"); -const tags = git("tag", "--merged", "HEAD", "--list", "v[0-9]*", "--sort=-version:refname") - .split("\n").filter(tag => /^v\d+\.\d+\.\d+$/.test(tag)); -assert.equal(version, tags[0]?.slice(1) ?? "0.0.0", - "Version is already prepared or release tags are missing. Fetch tags; finish the existing release before preparing another."); diff --git a/test-release.cjs b/test-release.cjs deleted file mode 100644 index bc8ae03..0000000 --- a/test-release.cjs +++ /dev/null @@ -1,86 +0,0 @@ -// Exercise the actual release command in disposable repositories, never the workspace Git. -const assert = require('node:assert/strict'); -const { execFileSync, spawnSync } = require('node:child_process'); -const fs = require('node:fs'); -const os = require('node:os'); -const path = require('node:path'); -const source = path.resolve(process.argv[2] || __dirname); -const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'wealthledger-release-')); -const env = { ...process.env, HUSKY: '0', NO_COLOR: '1' }; -const git = (...args) => execFileSync('git', args, { cwd, env, encoding: 'utf8' }).trim(); -const read = file => fs.readFileSync(path.join(cwd, file), 'utf8'); -const write = (file, text) => fs.writeFileSync(path.join(cwd, file), text); -const prepare = (...args) => spawnSync(process.execPath, - [path.join(source, 'node_modules/release-it/bin/release-it.js'), '--ci', ...args], - { cwd, env, encoding: 'utf8' }); -const mustPass = result => assert.equal(result.status, 0, result.stdout + result.stderr); -const mustFail = result => assert.notEqual(result.status, 0, result.stdout + result.stderr); -const commit = message => git('commit', '--allow-empty', '-qm', message); -try { - fs.mkdirSync(path.join(cwd, 'scripts')); - fs.symlinkSync(path.join(source, 'node_modules'), path.join(cwd, 'node_modules'), 'dir'); - for (const file of ['.release-it.json', 'scripts/check-release.cjs', 'commitlint.config.cjs']) { - fs.copyFileSync(path.join(source, file), path.join(cwd, file)); - } - write('.gitignore', 'node_modules/\nremote.git/\n'); - write('CHANGELOG.md', '# Changelog\n'); - write('package.json', JSON.stringify({ name: 'release-fixture', private: true, version: '0.0.0' })); - write('package-lock.json', JSON.stringify({ name: 'release-fixture', version: '0.0.0', lockfileVersion: 3, packages: { '': { name: 'release-fixture', version: '0.0.0' } } })); - git('init', '-q', '-b', 'main'); - git('config', 'user.name', 'Release test'); - git('config', 'user.email', 'release@example.invalid'); - git('config', 'core.hooksPath', '/dev/null'); - git('init', '--bare', '-q', 'remote.git'); - git('remote', 'add', 'origin', path.join(cwd, 'remote.git')); - git('add', '.'); - commit('chore: initialize fixture'); - mustFail(prepare('0.1.0')); // main cannot prepare releases. - git('switch', '-qc', 'release/test'); - write('untracked.txt', 'must block\n'); - mustFail(prepare('0.1.0')); - fs.unlinkSync(path.join(cwd, 'untracked.txt')); - const before = ['package.json', 'package-lock.json', 'CHANGELOG.md'].map(read); - const head = git('rev-parse', 'HEAD'); - mustPass(prepare('0.1.0', '--dry-run')); - assert.deepEqual(['package.json', 'package-lock.json', 'CHANGELOG.md'].map(read), before); - assert.equal(git('rev-parse', 'HEAD'), head); - mustPass(prepare('0.1.0')); - assert.equal(git('tag', '--list'), '', 'Preparation must not create tags'); - assert.equal(git('log', '-1', '--format=%s'), 'chore(release): 0.1.0'); - mustFail(prepare()); // Prepared version cannot be bumped again before tagging. - git('tag', '-a', 'v0.1.0', '-m', 'Release 0.1.0'); - mustFail(prepare()); // No new commits. - for (const [message, expected] of [ - ['fix: correct calculation', '0.1.1'], - ['feat: add export', '0.2.0'], - ['docs: explain export', '0.2.1'], - ['spec: define export', '0.2.2'], - ['chore(deps): update dependencies', '0.2.3'], - ['fix!: remove legacy contract', '1.0.0'], - ['feat: change export\n\nBREAKING CHANGE: replace export format', '2.0.0'], - ]) { - commit(message); - mustPass(prepare()); - const pkg = JSON.parse(read('package.json')); - const lock = JSON.parse(read('package-lock.json')); - assert.equal(pkg.version, expected); - assert.equal(lock.version, expected); - assert.equal(lock.packages[''].version, expected); - assert.ok(read('CHANGELOG.md').includes(expected)); - assert.equal(git('status', '--porcelain'), ''); - git('tag', '-a', `v${expected}`, '-m', `Release ${expected}`); - } - assert.ok(read('CHANGELOG.md').includes('/commit/'), read('CHANGELOG.md')); - assert.ok(read('CHANGELOG.md').includes('/compare/v0.1.0...v0.1.1')); - assert.ok(!read('CHANGELOG.md').includes('chore(release)')); - assert.equal(read('CHANGELOG.md').split('correct calculation').length, 2, 'Prior changes must not be repeated'); - assert.equal(spawnSync('git', ['--git-dir=remote.git', 'show-ref'], { cwd }).status, 1, 'Preparation must not push refs'); - const lint = message => spawnSync(process.execPath, - [path.join(source, 'node_modules/@commitlint/cli/cli.js'), '--strict'], - { cwd, env, input: message, encoding: 'utf8' }); - mustPass(lint('spec: define contract')); - mustFail(lint('unstructured message')); - console.log(`PASS: release preparation, guards, version rules, lockfile and changelog (${source})`); -} finally { - fs.rmSync(cwd, { recursive: true, force: true }); -} diff --git a/test-workspace-tracking.py b/test-workspace-tracking.py deleted file mode 100644 index 7f18bc2..0000000 --- a/test-workspace-tracking.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Check root tracking on fictional data; never stage the real workspace.""" - -from pathlib import Path -import subprocess -import tempfile - - -def check(ignore_rules): - with tempfile.TemporaryDirectory(prefix="wealthledger-tracking-") as directory: - root = Path(directory) - - def git(*args): - return subprocess.check_output( - ["git", "-C", str(root), *args], text=True - ) - - git("init", "--quiet") - allowed = { - '.gitea/workflows/commits.yml', - '.gitea/workflows/security.yml', - '.gitea/workflows/verify.yml', - '.gitignore', - '.nvmrc', - '.release-it.json', - 'AGENTS.md', - 'CHANGELOG.md', - 'README.md', - 'commitlint.config.cjs', - 'package-lock.json', - 'package.json', - 'renovate.json', - 'scripts/check-release.cjs', - 'test-release.cjs', - 'test-workspace-tracking.py', - } - for name in allowed: - (root / name).parent.mkdir(parents=True, exist_ok=True) - (root / name).write_text("fictional fixture\n") - (root / ".gitignore").write_text(ignore_rules) - for name in ("frontend", "backend", "docs", "specs"): - nested = root / name - nested.mkdir() - subprocess.run(["git", "init", "--quiet", str(nested)], check=True) - for name in ( - "frontend/source.ts", "backend/prisma/dev.db", "docs/README.md", - "specs/README.md", "statement-imports/report.csv", "private.db", - "private.db-wal", ".env", ".env.example", "audit.pdf", - "design/preview.html", "todo/notes.md", "references/source.md", - ".agents/local.md", "unknown-directory/data.txt", "unknown-file.txt", - "scripts/private.txt", ".gitea/private.txt", ".gitea/workflows/private.yml", - "node_modules/private.txt", - ): - target = root / name - target.parent.mkdir(parents=True, exist_ok=True) - target.write_text("fictional private data\n") - before = {p.relative_to(root): p.read_bytes() for p in root.rglob("*") - if p.is_file() and ".git" not in p.relative_to(root).parts[:1]} - git("add", ".") - assert set(git("ls-files").splitlines()) == allowed - assert all((root / p).read_bytes() == content for p, content in before.items()) - assert all((root / name / ".git").is_dir() - for name in ("frontend", "backend", "docs", "specs")) - - -if __name__ == "__main__": - check(Path(__file__).with_name(".gitignore").read_text()) - print("PASS: only root allowlist staged; fictional data and nested Git preserved")