5 Commits
Author SHA1 Message Date
ksv741 21853fde6a chore(release): 0.2.0
Conventional commits / commits (pull_request) Successful in 13s
2026-09-16 11:34:27 +03:00
ksv741 7e382d460b feat: add containerized application delivery #11
Conventional commits / commits (pull_request) Successful in 10s
2026-09-16 09:42:13 +03:00
ksv741 95e4361599 chore: simplify root tooling and repository boundaries
Conventional commits / commits (pull_request) Successful in 10s
2026-09-15 20:45:36 +03:00
ksv741 4a78dd640d docs: align inventory with simplified conventions
Workspace security / security (pull_request) Successful in 6s
Workspace verify / verify (pull_request) Successful in 20s
Conventional commits / commits (pull_request) Successful in 10s
Workspace security / security (push) Successful in 5s
Workspace verify / verify (push) Successful in 19s
2026-09-15 05:58:30 +03:00
ksv741 93dd5539ad docs: establish workspace engineering conventions
Workspace security / security (push) Successful in 7s
Workspace verify / verify (push) Successful in 20s
Workspace security / security (pull_request) Successful in 6s
Workspace verify / verify (pull_request) Successful in 20s
Conventional commits / commits (pull_request) Successful in 10s
2026-09-15 05:42:02 +03:00
24 changed files with 683 additions and 287 deletions
+65
View File
@@ -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.
+29
View File
@@ -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).
+20
View File
@@ -0,0 +1,20 @@
# Git в workspace
- Выполняй Git-команды из репозитория владельца: root, frontend, backend,
docs и specs имеют независимые history, index и branches.
- Новая работа: ветка `codex/<task>` от актуального `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` запрещён; версии вручную не повышай.
+15
View File
@@ -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#релизы-и-ветки).
+27
View File
@@ -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 покрывает риск.
+177
View File
@@ -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 под новый код.
+88
View File
@@ -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 не менялись.
+12
View File
@@ -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
+10
View File
@@ -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
+2 -2
View File
@@ -22,5 +22,5 @@ jobs:
BASE_SHA: ${{ github.event.pull_request.base.sha }} BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: | run: |
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict printf '%s\n' "$PR_TITLE" | npm exec --no -- commitlint --strict
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA" npm exec --no -- commitlint --strict --from "$BASE_SHA" --to "$HEAD_SHA"
-31
View File
@@ -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
-17
View File
@@ -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
+19 -7
View File
@@ -3,7 +3,25 @@
!/.gitignore !/.gitignore
!/AGENTS.md !/AGENTS.md
!/README.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 !/.nvmrc
!/package.json !/package.json
!/package-lock.json !/package-lock.json
@@ -11,14 +29,8 @@
!/CHANGELOG.md !/CHANGELOG.md
!/commitlint.config.cjs !/commitlint.config.cjs
!/renovate.json !/renovate.json
!/test-release.cjs
!/scripts/
/scripts/*
!/scripts/check-release.cjs
!/.gitea/ !/.gitea/
/.gitea/* /.gitea/*
!/.gitea/workflows/ !/.gitea/workflows/
/.gitea/workflows/* /.gitea/workflows/*
!/.gitea/workflows/commits.yml !/.gitea/workflows/commits.yml
!/.gitea/workflows/verify.yml
!/.gitea/workflows/security.yml
-3
View File
@@ -18,9 +18,6 @@
"gitlab": { "gitlab": {
"release": false "release": false
}, },
"hooks": {
"after:git:init": "node scripts/check-release.cjs"
},
"plugins": { "plugins": {
"@release-it/conventional-changelog": { "@release-it/conventional-changelog": {
"infile": "CHANGELOG.md", "infile": "CHANGELOG.md",
+27 -24
View File
@@ -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 относятся только к root. Работа внутри другого репозитория
Root — отдельный Git-репозиторий `WealthLedger/main`; команды Git выполняй начинается с его собственного AGENTS; он содержит все обязательные правила
из нужного repo. Root tracking ограничен allowlist в `.gitignore`; без наследования root conventions и без чтения соседних checkout.
вложенные repos и локальные данные в main не добавляй, включая `git add -f`. Публичные API artifacts передаются явно; CI каждого repo использует локальные
Структура checkout и запуск — в [README](README.md). входы. Справочные источники не становятся обязательной цепочкой инструкций.
`references/**`, dated audits и архивы — read-only источники.
## Работа Начни с результата и affected areas. До edits проследи поведение/invariants,
найди owner и 1–3 примера. Работай напрямую; OpenSpec/SDD — по явному запросу.
1. Начинай с задачи пользователя, ожидаемого результата и affected areas. Работай напрямую: OpenSpec/SDD artifacts, approvals и archive не являются prerequisite. Используй прежний SDD workflow только по явному запросу. | Root задача | Прочитать |
2. Прочитай local `AGENTS.md` affected repo. Для frontend/backend прочитай его `.ai/CONVENTIONS.md` и `.ai/SOURCES.md`, затем только темы, которые активируют задача, пути или обнаруженная граница. | --- | --- |
3. До выбора реализации проследи поведение и инварианты, найди ближайший существующий owner и 1–3 подходящих примера. Код показывает реализацию; документированный контракт не выводится из случайного аналога. | Helper/pattern, refactor, scope | [Code health](.ai/conventions/code-health.md) |
4. Выбирай простейшее полное решение у ответственного владельца. Новая abstraction или dependency требует текущей потребности; посторонний рефакторинг оставляй отдельно. | Dependency/tooling | [Dependencies](.ai/conventions/dependencies.md), [Guardrails](.ai/conventions/guardrails.md) |
5. Выбирай тип знания по задаче через [вход docs](docs/README.md): новая функция или межрепозиторное поведение → соответствующая область, спецификации, нужные ADR, затем реализация; ошибка с неясным ожиданием → спецификация, docs, код и тесты; архитектурное изменение → архитектура/ADR, затронутые спецификации и код. Локальная ошибка сборки, линтера или очевидный implementation bug → инструкции и код нужного repo; документация → docs/AGENTS.md и CONTRIBUTING.md. Расширяй контекст по конкретному пробелу; missing required source или конфликт требований сообщи до зависимого решения. | Нетривиальные tests | [Test Intent](.ai/conventions/testing.md) |
6. При новой границе и перед завершением повтори routing по actual staged, unstaged и новым файлам. Сохраняй чужие изменения; уточнение контекста не расширяет задачу. | Git/branch/commit/PR/tracking | [Git](.ai/conventions/git.md) |
7. Проверь результат по задаче, рискованным случаям и local gates. Сохрани краткие команды/результаты; сообщи оставшиеся ограничения. Успех lint/tests не заменяет semantic review. | Release/hotfix | [README](README.md#релизы-и-ветки) |
Перед Node-командой активируй `.nvmrc` affected repo через `nvm use --silent`. Перед Node-командой активируй местную `.nvmrc`: `nvm use --silent`.
При новой границе и перед завершением повтори routing по staged/unstaged/new
## Ветки и релизы files; сохраняй чужие изменения. Missing source/конфликт требований сообщи
до зависимого решения. После diff выполни
Новая работа — в отдельной ветке от `develop`, затем PR в `develop`. [Boy Scout review](.ai/conventions/code-health.md#boy-scout-review) и
Прямой push в `main` запрещён. Commits и заголовки PR — Conventional Commits. [relevant gates](.ai/conventions/guardrails.md). Запиши краткие команды,
Перед release/hotfix, merge между `main` и `develop`, изменением release-конфигурации результаты и ограничения; lint не заменяет semantic review.
или CI refs прочитай [процесс релиза](README.md#релизы-и-ветки).
+15
View File
@@ -1,5 +1,20 @@
# Changelog # 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) ## 0.1.0 (2026-09-14)
### Documentation ### Documentation
+70 -36
View File
@@ -2,7 +2,7 @@
Локальное приложение для портфеля T-Bank, операций, дохода и будущих выплат, Локальное приложение для портфеля 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 ## Структура workspace
@@ -12,10 +12,10 @@ Workspace состоит из пяти независимых Git-репозит
| Путь от root | Репозиторий Gitea | Назначение | | Путь от root | Репозиторий Gitea | Назначение |
| --- | --- | --- | | --- | --- | --- |
| `.` | `WealthLedger/main` | Вход в workspace, инструкции и правила root tracking | | `.` | `WealthLedger/main` | Вход в workspace, инструкции и правила root tracking |
| `frontend/` | `WealthLedger/frontend` | [React/Vite UI](frontend/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](backend/README.md) | | `backend/` | `WealthLedger/backend` | [NestJS API, Prisma и SQLite](https://git.personal.ksv741.ru/WealthLedger/backend/src/commit/016782657acfd35ff3fd755462007938fac35156/README.md) |
| `docs/` | `WealthLedger/docs` | [Объяснение системы](docs/README.md) | | `docs/` | `WealthLedger/docs` | [Объяснение системы](https://git.personal.ksv741.ru/WealthLedger/docs/src/commit/c557af4ea51cd3c578d0755e909e62e06462b5bc/README.md) |
| `specs/` | `WealthLedger/specs` | [Требования и OpenSpec](specs/README.md) | | `specs/` | `WealthLedger/specs` | [Требования и OpenSpec](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/README.md) |
### Получение checkout ### Получение checkout
@@ -34,12 +34,14 @@ git clone ssh://git@git.personal.ksv741.ru:2222/WealthLedger/specs.git specs
Для существующего workspace клонируйте только отсутствующие checkout в пустые Для существующего workspace клонируйте только отсутствующие checkout в пустые
каталоги. Проверяйте состояние каждого repo отдельно, например каталоги. Проверяйте состояние каждого repo отдельно, например
`git -C frontend status`. Обновление main не обновляет соседние repos; `git -C frontend status`. Обновление main не обновляет соседние repos;
общего lockfile revisions нет. Относительные ссылки требуют указанных имён каталогов. общего lockfile revisions нет. Такая раскладка удобна для совместной работы,
но каждый checkout можно использовать отдельно.
### Что хранится в main ### Что хранится в main
Root отслеживает входные документы, npm/release-конфигурацию, changelog, Root отслеживает входные документы, собственные engineering conventions и исторический migration
проверки tracking/release и три CI workflow. Точный allowlist — в `.gitignore`. inventory в `.ai/`, npm/release-конфигурацию, changelog,
Compose configuration, безопасные env examples и один CI workflow с commitlint. Точный allowlist — в `.gitignore`.
Остальные root-пути игнорируются по умолчанию: Остальные root-пути игнорируются по умолчанию:
вложенные repos, `.env`, базы и WAL, импорты выписок, PDF, `references/`, вложенные repos, `.env`, базы и WAL, импорты выписок, PDF, `references/`,
`design/`, `todo/` и локальные настройки инструментов. `design/`, `todo/` и локальные настройки инструментов.
@@ -51,6 +53,35 @@ Root отслеживает входные документы, npm/release-ко
Не запускайте очистку workspace через `git clean -fdx`: ignored-пути содержат Не запускайте очистку workspace через `git clean -fdx`: ignored-пути содержат
локальные данные. Правила tracking не удаляют файлы и вложенные `.git`. локальные данные. Правила 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). Нужны nvm и Node из `.nvmrc` каждого npm-repo (сейчас 24.21.0, npm 11.19).
@@ -60,7 +91,7 @@ Root отслеживает входные документы, npm/release-ко
После смены major Node переустановите native dependencies через `npm ci`. После смены major Node переустановите native dependencies через `npm ci`.
Разрешения install scripts заданы в `package.json` соответствующего repo. Разрешения 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`. замените оба placeholder-секрета и проверьте `DATABASE_URL`.
Существующий `.env` сохраняйте. Из backend: Существующий `.env` сохраняйте. Из backend:
@@ -73,13 +104,13 @@ npm run start:dev
Build генерирует Prisma client. Перед миграцией существующей базы сохраните Build генерирует Prisma client. Перед миграцией существующей базы сохраните
согласованный backup и ключ шифрования; процедуру и ограничения отката смотрите согласованный backup и ключ шифрования; процедуру и ограничения отката смотрите
в [описании хранения](docs/architecture/containers.md#сохранение-и-восстановление-данных). в [описании хранения](https://git.personal.ksv741.ru/WealthLedger/docs/src/commit/c557af4ea51cd3c578d0755e909e62e06462b5bc/architecture/containers.md#сохранение-и-восстановление-данных).
Известная ошибка fresh migration при `RUST_LOG=warn/error` устранена в Известная ошибка fresh migration при `RUST_LOG=warn/error` устранена в
`backend/prisma.config.ts`: CLI удаляет унаследованную переменную. `backend/prisma.config.ts`: CLI удаляет унаследованную переменную.
Используйте штатный `prisma migrate deploy` из backend; прежний Prisma blocker Используйте штатный `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: затем из frontend:
```bash ```bash
@@ -98,7 +129,7 @@ npm run dev
OpenSpec не нужен для запуска приложения и применяется по явному запросу. OpenSpec не нужен для запуска приложения и применяется по явному запросу.
Specs использует Node из своей `.nvmrc`; проверенная версия CLI — 1.10.0. Specs использует Node из своей `.nvmrc`; проверенная версия CLI — 1.10.0.
Store `wealth-ledger-specs` расположен в `specs/openspec/`. 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 <base> --to <head>` | Формат сообщений коммитов |
| root | `git diff --check` | Whitespace в изменениях root | | root | `git diff --check` | Whitespace в изменениях root |
| frontend | `npm run verify` | Типы, lint, тесты, зависимости, OpenAPI snapshot и сборки | | frontend | `npm run verify` | Типы, lint, тесты, зависимости, OpenAPI snapshot и сборки |
| backend | `npm run verify` | Lint, типы, unit tests и build | | 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 | | 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-сценарии или доступность временные базы; verify отдельно не доказывает HTTP/DB-сценарии или доступность
T-Bank. Полные команды и ограничения принадлежат README 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). Для изменения проекта начните с [AGENTS](AGENTS.md).
## Релизы и ветки ## Релизы и ветки
@@ -143,18 +175,19 @@ Commits и заголовки PR — Conventional Commits. `feat:` повыша
1. Обновить `main`, `develop` и теги командой `git fetch origin --tags`. 1. Обновить `main`, `develop` и теги командой `git fetch origin --tags`.
Проверить, что текущий `develop` содержит последний релиз из `main`. Проверить, что текущий `develop` содержит последний релиз из `main`.
Создать `release/<name>` от проверенного `develop` и выполнить `npm ci`. Создать `release/<name>` от проверенного `develop` и выполнить `npm ci`.
2. Для backend и docs обновить `.ci/refs.json`: полные SHA совместимых соседних 2. Проверить локальные входы и публичные artifacts выпускаемого репозитория.
репозиториев. Эти revisions должны быть доступны в Gitea. Проверить именно Совместимость развёрнутых участников согласовать отдельно через публичные
их совместимость; движущиеся ветки не являются фиксацией состава релиза. контракты; CI не скачивает соседние checkout и не требует их SHA.
Закоммитить обновлённые pins до подготовки версии.
3. Выполнить `npm run release:preview`. Проверить номер и changelog, затем 3. Выполнить `npm run release:preview`. Проверить номер и changelog, затем
`npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`, обновляет `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`, обновляет
`package.json`, lockfile и `CHANGELOG.md`; теги и push не выполняет. `package.json`, lockfile и `CHANGELOG.md`; теги и push не выполняет.
Не использовать обход branch/clean/tag guards. Незакоммиченные файлы, В root встроенные проверки release-it контролируют ветку, чистое дерево и
неподходящая ветка, отсутствие изменений и уже подготовленная версия блокируют выпуск. наличие коммитов. Теги и соответствие версии последнему принятому тегу
проверяются вручную; уже подготовленную версию повторно не повышать.
4. Проверить diff релизного коммита и штатные gates. Отправить только релизную 4. Проверить diff релизного коммита и штатные gates. Отправить только релизную
ветку: `git push -u origin HEAD`. Открыть PR в `main` с заголовком ветку: `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` изменился, сначала выполнить **fast-forward-only merge**. Если `main` изменился, сначала
согласовать историю и повторить проверку кандидата; не обходить защиту. согласовать историю и повторить проверку кандидата; не обходить защиту.
5. После merge получить `main` и убедиться, что его HEAD — проверенный релизный 5. После merge получить `main` и убедиться, что его HEAD — проверенный релизный
@@ -184,25 +217,26 @@ Commits и заголовки PR — Conventional Commits. `feat:` повыша
`release/hotfix-<name>` в `main`, затем обязательно возвращается в `develop`. `release/hotfix-<name>` в `main`, затем обязательно возвращается в `develop`.
Если ломается публичный контракт, major сохраняется и для hotfix. Если ломается публичный контракт, major сохраняется и для hotfix.
Backend CI проверяет runtime OpenAPI с frontend; docs CI — межрепозиторные Каждый CI использует только свой checkout. Backend генерирует OpenAPI без сервера,
ссылки. Для `main`, PR в `main` и `release/*` соседи берутся из `.ci/refs.json`; сверяет генератор с configured app и не хранит JSON в Git; frontend проверяет types по входному
для разработки — из `develop`. SHA записываются в логи. Для изменения сразу snapshot. Docs проверяет внутренние ссылки; внешние источники даются как
нескольких репозиториев сначала согласовать их revisions, затем проверять справочные веб-ссылки. Обновление API artifact потребителя — отдельное действие.
релизный набор; порядок публикации сам по себе не доказывает совместимость. Зелёные локальные gates и порядок публикации не доказывают совместимость
развёрнутых версий. Процедуры выпуска находятся также в README каждого repo;
доступ к root для выпуска другого репозитория не требуется.
В Gitea для `main` и `develop` нужны запрет прямого push и force-push, В Gitea для `main` и `develop` нужны запрет прямого push и force-push,
обязательные проверки `verify`, `security`, `commits` и запрет обхода правил обязательные проверки: в root только `commits`, в остальных repo —
`verify`, `security`, `commits`; также нужен запрет обхода правил
администратором. Требование отдельного одобрения review не вводится для администратором. Требование отдельного одобрения review не вводится для
разработки одним владельцем. Ветка по умолчанию — `develop`; Renovate тоже разработки одним владельцем. Ветка по умолчанию — `develop`; Renovate тоже
отправляет PR в `develop`. Теги `v*` защищены, создание разрешается владельцу отправляет PR в `develop`. Теги `v*` защищены, создание разрешается владельцу
релиза; их удаление или перенос не входит в обычный процесс. релиза; их удаление или перенос не входит в обычный процесс.
`node test-release.cjs` из root проверяет настоящий release-it во временном Git. В root нет собственного test/verify runner и release hooks. Release-it и
Для проверки конфигурации соседа после установки его dependencies: commitlint используются напрямую. Остальные репозитории сохраняют свои gates.
`node test-release.cjs frontend` (аналогично backend, docs, specs). Если в защите веток root ранее были обязательными `verify` и `security`, их
Тест не публикует refs и не использует рабочие базы. Root `npm run verify` нужно убрать из required checks в Gitea; YAML не меняет настройки сервера.
включает tracking и этот тест; frontend/backend/docs сохраняют свои gates,
backend дополнительно требует `npm run test:e2e`.
Npm publishing, автоматический deploy и карточки Gitea Releases не включены. Npm publishing, автоматический deploy и карточки Gitea Releases не включены.
Граница выпуска — принятый коммит, changelog и Git-тег. Граница выпуска — принятый коммит, changelog и Git-тег.
+53
View File
@@ -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:
+51
View File
@@ -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
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "wealth-ledger-workspace", "name": "wealth-ledger-workspace",
"version": "0.1.0", "version": "0.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wealth-ledger-workspace", "name": "wealth-ledger-workspace",
"version": "0.1.0", "version": "0.2.0",
"devDependencies": { "devDependencies": {
"@commitlint/cli": "21.2.2", "@commitlint/cli": "21.2.2",
"@commitlint/config-conventional": "21.2.2", "@commitlint/config-conventional": "21.2.2",
+1 -2
View File
@@ -1,12 +1,11 @@
{ {
"name": "wealth-ledger-workspace", "name": "wealth-ledger-workspace",
"version": "0.1.0", "version": "0.2.0",
"private": true, "private": true,
"engines": { "engines": {
"node": "24.21.0" "node": "24.21.0"
}, },
"scripts": { "scripts": {
"verify": "python3 test-workspace-tracking.py && node test-release.cjs && git diff --check",
"release:preview": "release-it --dry-run --ci", "release:preview": "release-it --dry-run --ci",
"release:prepare": "release-it --ci", "release:prepare": "release-it --ci",
"commitlint": "commitlint" "commitlint": "commitlint"
-10
View File
@@ -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.");
-86
View File
@@ -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 });
}
-67
View File
@@ -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")