19 Commits
Author SHA1 Message Date
ksv741 3027fd51ec ci: validate release PR commits independently (WealthLedger/main#30)
Conventional commits / commits (pull_request) Successful in 11s
2026-10-12 23:39:30 +03:00
ksv741 a659c9dcc7 chore(release): 0.2.2
Conventional commits / commits (pull_request) Failing after 11s
2026-10-12 23:19:12 +03:00
ksv741 d2d1092344 Merge pull request 'docs: refine workflow conventions after review (#27)' (#29) from docs/27-workflow-review-fixes into develop
Reviewed-on: #29
2026-10-12 22:00:20 +03:00
ksv741 fc589c9130 docs: refine workflow conventions after review (#27)
Conventional commits / commits (pull_request) Successful in 12s
2026-10-12 20:29:00 +03:00
codex-bot 3321d9db25 Merge pull request 'chore: establish engineering workflow (#27)' (#28) from chore/27-engineering-workflow into develop 2026-10-12 11:00:35 +03:00
ksv741 03b3b59e6f fix: align workflow commitlint policy (#27)
Conventional commits / commits (pull_request) Successful in 13s
2026-10-12 10:17:53 +03:00
ksv741 59e0831ad2 docs: document PR ownership and board stage (#27)
Conventional commits / commits (pull_request) Successful in 10s
2026-10-12 09:45:41 +03:00
ksv741 2dc72255fe chore: establish engineering workflow (#27)
Conventional commits / commits (pull_request) Successful in 10s
2026-10-12 09:19:58 +03:00
codex-bot ed8221ae8a Merge pull request 'docs: clarify root Node bootstrap #24' (#26) from codex/issue-24-process into develop 2026-10-11 23:23:10 +03:00
ksv741 1ed4dd5a73 docs: clarify root Node bootstrap #24
Conventional commits / commits (pull_request) Successful in 13s
2026-10-11 23:06:51 +03:00
ksv741 dc030f84c9 chore(release): 0.2.1
Conventional commits / commits (pull_request) Successful in 11s
2026-10-07 18:13:55 +03:00
ksv741 f27487b6a8 fix(deps): refresh release security baseline
Conventional commits / commits (pull_request) Successful in 11s
2026-10-07 15:36:45 +03:00
ksv741 9fbc23f757 docs: link production deployment repository
Conventional commits / commits (pull_request) Successful in 13s
2026-09-20 18:50:05 +03:00
ksv741 80fa254a31 chore: point container images to Nexus #15
Conventional commits / commits (pull_request) Successful in 10s
2026-09-17 20:41:10 +03:00
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
32 changed files with 988 additions and 302 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).
+162
View File
@@ -0,0 +1,162 @@
# Engineering Workflow
Область: пять независимых checkout workspace (`main`, `frontend`, `backend`,
`docs`, `specs`). Репозиторий `deploy` существует в Gitea отдельно и применяет
свои локальные правила. Этот документ — единственный владелец общего процесса;
локальные AGENTS и conventions определяют технические проверки и ограничения.
## Issue Management
| Переход | Условие входа → действие → условие выхода |
| --- | --- |
| Request → Issue | До изменения файлов/конфигурации/спецификаций определить затронутые repos, найти по смыслу открытые и закрытые Issues, проверить актуальность и scope. Взять подходящую открытую Issue (закрытую переоткрыть только для действительно незавершённой работы) либо создать одну новую. Чтение, анализ и предложение изменений разрешены без Issue. |
| Issue → Analysis | В Issue должны быть результат в title, контекст/проблема и ожидаемое поведение, проверяемые Acceptance Criteria. Уточнить неизвестное до зависимого решения. Выбрать тип и приоритет; назначить исполнителя, если известен; milestone только при реальном плане, dependencies/related Issues только при наличии. |
| Analysis → Delivery | Определить область и риски, выбрать lightweight или SDD ниже. Если AC или owner меняются, сначала обновить соответствующую Issue/согласование. |
Типы: `feature`, `bug`, `refactor`, `tech-debt`, `docs`, `test`, `ci`,
`chore`, `performance`. Существующие org labels отражают их без требования
один-к-одному: `Kind/Feature`, `Kind/Bug`, `Kind/Refactor`,
`Kind/Documentation`, `Kind/Testing`, `Kind/Enhancement` (performance);
repo-local `Kind/Process` подходит для ci, chore и process/tech-debt.
Дополнительные `Kind/Security`, `Kind/Investigation`, `Kind/Dependencies`
(последние два — repo-local) — по ситуации. Priority: существующие
`Priority/Critical|High|Medium|Low`.
Gitea assignee, milestone и state не дублируются labels. Для назначаемых
Issue и PR выбирай `hermes-agent`; незанятую задачу не назначай формально.
Шаблоны ускоряют ввод, но не доказывают качество заполнения.
Один координирующий Issue достаточен, если результат и AC едины, одна команда
может принять работу целиком и отдельные repos не требуют независимой поставки.
Иначе Issue в каждом независимо принимаемом repo; ссылаться друг на друга и
указать порядок зависимостей. Номера `#27` локальны: для чужого repo использовать
`WealthLedger/main#27` или полный URL. Не заводить дубль только из-за второго PR.
## Git Branching
| Переход | Условие входа → действие → условие выхода |
| --- | --- |
| Analysis → Branch | Issue и AC определены; проверить `git status`, текущую ветку и `origin/develop`. Использовать существующую подходящую ветку либо создать одну от актуального `develop` каждого affected repo. Чужие изменения не переносить. |
| Branch → Implementation | `<type>/<issue-number>-<english-kebab-description>`; номер принадлежит указанной в PR Issue (для cross-repo — координатору). Примеры: `feature/123-import-transactions`, `fix/124-invalid-balance`, `refactor/125-simplify-api-client`, `docs/126-update-architecture`. |
Префиксы: feature → `feature`, bug → `fix`, refactor/tech-debt →
`refactor`, docs → `docs`, test → `test`, ci → `ci`, chore → `chore`,
performance → `perf`. Номер обязателен для новых рабочих веток; короткий
английский slug в kebab-case. Ветки `main`/`develop` не место для прямой
разработки. Release/hotfix, обратная синхронизация, Renovate и исторические
ветки подчиняются существующему release/automation процессу, а не переименовываются.
## Commit Conventions
| Переход | Условие входа → действие → условие выхода |
| --- | --- |
| Implementation → Commit | Одна связанная задача на commit, проверить staged paths/diff. Заголовок Conventional Commits с осмысленным subject и ссылкой на Issue: `feat(portfolio): add transaction import (#123)` или `ci: align gates (WealthLedger/main#27)`. Scope только при полезной области. |
Commitlint требует распознаваемую conventional-commits-parser ссылку, но не
проверяет числовой формат, существование Issue, её смысл или корректный repo.
Использовать `#N` только для локальной Issue и `WealthLedger/repo#N` для чужой;
это формат команды, проверяемый при review. Не добавлять custom validator
ради более строгой регулярной проверки. Без `fixes`,
`closes`, `resolves` в сообщении: не закрывать задачу первым коммитом.
Ссылка в footer тоже принимается commitlint; размещение в заголовке остаётся
правилом команды. Если заголовок PR после squash станет commit title, оставить
в нём ссылку. `chore(release): X.Y.Z` освобождён только при точном SemVer
заголовке release-it. Стандартные Gitea merge commits освобождаются по точному
шаблону merge message; sync PR используют обычный review/merge process.
Шаблонные сообщения `Revert`/`fixup!`/`squash!` не освобождаются: revert
оформляй как `revert: … (#N)`. Историю не переписывать ради формата.
## Pull Request Process
| Переход | Условие входа → действие → условие выхода |
| --- | --- |
| Validation → PR | Завершить AC и релевантные локальные проверки; PR в `develop` из рабочей ветки, только связанный diff. В теле: Summary, Linked Issue, Changes, Verification (команда/результат/ограничения), Breaking Changes при наличии. Для cross-repo PR указывать полный reference и связанные PR. |
| PR → Review → Merge | Дождаться required checks на финальной ревизии, проверить AC, значимые изменения — review владельца/специалиста. При замечаниях исправить и обновить evidence. Merge только по политике repo. |
| Merge → Issue Closed | Закрыть Issue после принятия **всех** связанных delivery areas, документации и при SDD — обязательной verification/archive; multi-PR Issue остаётся открытой до последнего результата. |
Перед передачей PR на review перенеси в него тип и приоритет связанной Issue,
назначь `hermes-agent`, привяжи к подходящему milestone (если он есть) и к
`WealthLedger — Delivery Board`, колонка «На проверке». Для cross-repo PR
используй milestone его собственного repo. Колонку меняй вместе со стадией:
при блокировке — «Заблокировано», после принятия — «Готово». Не вводи label
этапа вместо встроенной колонки проекта.
Для SDD отдельные pre-archive checks/semantic review выполняются до archive по
локальной schema; порядок merge/archive согласуется с конкретным change, но
Issue закрывается только после обоих событий. Не подменять OpenSpec approval
обычным PR review.
## SDD Integration
Lightweight: локальный bugfix с известным ожидаемым поведением, docs-only,
рефакторинг без изменения контрактов/наблюдаемого поведения и малые CI-правки:
`Issue → Analysis → Branch → Implementation → Validation → PR`.
SDD: новая функциональность, изменение публичного API, бизнес-правил, поведения
нескольких компонентов, архитектуры или неоднозначные требования. После Issue
уточнить намерение и запросить согласование Change Owner на вход в OpenSpec;
OpenSpec lifecycle применяется только по явному запросу/решению владельца.
Использовать **только** store `wealth-ledger-specs` и его локальный lifecycle:
Proposal → Delta Specs → System Analysis → Design + QA Analysis → Docs Impact →
Tasks → Apply (для legacy change — его собственная schema). Явные approvals
перед downstream artifacts и Apply не заменяются Issue/PR. Ветка specs может
появиться для planning; ветки реализации — когда Apply разрешён, без
насильственной общей точки создания всех веток. Связать Issue ↔ change ID ↔
PR текстовыми ссылками; после проверки применить штатные sync/archive и
completion evidence согласно локальной schema. Число файлов не определяет SDD.
Если владелец не согласовал вход в SDD, не начинать зависящую реализацию
контракта/семантики и явно сообщить блокировку.
## AI Agent Responsibilities
Перед edit — поиск подходящей Issue, AC, workflow, branch/status каждого repo;
затем только согласованный scope. По завершении — итоговый diff, проверенные
команды и ограничения, подготовка commit/PR по запросу и статус Issue. Чтение
не требует Issue. Если Gitea недоступна, сообщить об этом до edit и запросить
issue reference; не выдавать локальную заметку за Issue. Агенты используют
локальные инструкции репозитория и его CI inputs, не полагаются на соседние
checkout как обязательную build-зависимость. Этот источник доступен в main;
при недоступной ссылке самостоятельный checkout сохраняет обязательный минимум
в своём AGENTS и не блокирует локальную работу из-за отсутствия root checkout.
## Quality Gates
Итерации: узкие проверки по риску; после логического изменения — релевантные
тесты; перед передачей результата — локальные обязательные gates данного repo
на итоговом diff; CI — независимые полные проверки. Не повторять дорогой verify
после каждого файла; повторить при изменении значимых входов. `main`: `commits`;
остальные: локальные `verify`, `security`, `commits` согласно фактическим CI.
Frontend/backend запускают их как jobs в едином `verify.yml`, docs/specs —
как отдельные workflows; второго CI ради одного правила не требуется.
Для Markdown-only не запускать application build. Результат lint не заменяет
семантическую проверку. Обязательность merge checks зависит от защиты ветки.
| Правило | Уровень на момент принятия документа |
| --- | --- |
| Conventional Commits и parser-recognized Issue reference в PR title/commits | CI-проверка через существующий commitlint; блокировка merge **только если** check обязателен в branch protection. Не гарантирует numeric reference, существование или соответствие Issue. Точный release SemVer title и штатные Gitea merge messages исключены. Локальные hooks не гарантируют server-side. |
| PR вместо прямого push; successful checks перед merge | Enforced только в repos/ветках, где Gitea branch protection настроена и проверена владельцем. YAML сам по себе не защищает ветку. |
| Поиск актуальной Issue, AC, корректный repo, branch name/source, соответствие ссылки, SDD selection, scope, статус | Agent-guided; templates и текст не могут гарантировать соответствие. |
| Значимое review, соответствие AC/контракту, исключения и closure после всех PR | Manual; approval может быть required в Gitea, если есть независимый reviewer. |
## Exceptions and Recovery
Срочный hotfix: немедленно завести/найти Issue, затем исправление в
`release/hotfix-<name>` от актуального `main` по существующему release process;
проверки и PR в `main`, затем вернуть результат в `develop`. Связать Issue
в PR и сопровождающем коммите; специальная release-ветка исключена из обычного
шаблона имён. Не отменять Issue или проверки из-за срочности.
Если CI красный или PR устарел — исправить в той же ветке и перепроверить
актуальный HEAD; если AC изменились — обновить Issue и при SDD вернуть change
к затронутому approval gate. При недоступной Gitea продолжать только анализ.
Изменение PR title после зелёного CI: проверить, что check пересчитан;
если `edited` trigger у существующих checks отсутствует, reviewer проверяет
текущий title вручную. Новые правила не применяются к историческим коммитам
и автоматической обратной синхронизации релиза.
Примеры: фича с новым поведением — Issue → согласованный OpenSpec change →
ветки Apply → PR каждого affected repo → archive → закрытие Issue. Локальный
bugfix — Issue → `fix/124-invalid-balance` → регрессия → PR → закрытие.
Cross-repo контракт — одна Issue с общими AC, `backend` и `frontend` PR с
`WealthLedger/main#27` и взаимными ссылками; если независимые acceptance/owners,
отдельные связанные Issues. Рефакторинг без изменения поведения — lightweight
`refactor/125-simplify-api-client`. Hotfix — Issue → `release/hotfix-*` от
`main` → PR/release → back-merge в `develop` → закрытие.
+22
View File
@@ -0,0 +1,22 @@
# Git в workspace
- Выполняй Git-команды из репозитория владельца: root, frontend, backend,
docs и specs имеют независимые history, index и branches.
- Новая работа: ветка с номером Issue по [Engineering Workflow](engineering-workflow.md#git-branching)
от актуального `develop`, затем PR в `develop`.
Сохраняй чужие staged/unstaged/untracked изменения; не переноси их в свой commit.
- Commit содержит одну связанную задачу и её необходимые проверки/документацию.
Межрепозиторное изменение оформляй отдельными commits/PR с взаимными ссылками.
- Commits и заголовки PR — Conventional Commits со ссылкой на Issue;
допустим `spec:`. Исключения release — в README.
Формат проверяет 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=nexus.ksv741.keenetic.pro/wealthledger/frontend:0.2.0
BACKEND_IMAGE=nexus.ksv741.keenetic.pro/wealthledger/backend:0.2.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
+12
View File
@@ -0,0 +1,12 @@
---
name: Bug Report
about: Неверное поведение
---
## Проблема и шаги воспроизведения
## Факт / ожидание
## Acceptance Criteria
- [ ]
## Окружение / ссылки (если применимо)
+10
View File
@@ -0,0 +1,10 @@
---
name: Engineering Task
about: CI, тесты, документация, процесс и прочее
---
## Контекст и результат
## Scope / связанные Issues (если есть)
## Acceptance Criteria
- [ ]
+12
View File
@@ -0,0 +1,12 @@
---
name: Feature Request
about: Новое наблюдаемое поведение
---
## Зачем и какой результат
## Ожидаемое поведение
## Acceptance Criteria
- [ ]
## Зависимости / связанные Issues (если есть)
+12
View File
@@ -0,0 +1,12 @@
---
name: Refactoring / Technical Debt
about: Улучшение без нового поведения
---
## Проблема и результат
## Сохраняемые инварианты / границы
## Acceptance Criteria
- [ ]
## Зависимости / ссылки (если есть)
+11
View File
@@ -0,0 +1,11 @@
## Summary
## Linked Issue
Refs
## Changes
## Verification
- Команда / результат / ограничения:
## Breaking Changes (если есть)
+6 -2
View File
@@ -22,5 +22,9 @@ 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
if [[ "$PR_TITLE" =~ ^chore\(release\):\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then
npm exec --no -- commitlint --strict --from "$HEAD_SHA^" --to "$HEAD_SHA"
else
npm exec --no -- commitlint --strict --from "$BASE_SHA" --to "$HEAD_SHA"
fi
-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
+28 -7
View File
@@ -3,22 +3,43 @@
!/.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/engineering-workflow.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
!/.release-it.json
!/CHANGELOG.md
!/commitlint.config.cjs
!/.gitleaksignore
!/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
!/.gitea/ISSUE_TEMPLATE/
/.gitea/ISSUE_TEMPLATE/*
!/.gitea/ISSUE_TEMPLATE/feature.md
!/.gitea/ISSUE_TEMPLATE/bug.md
!/.gitea/ISSUE_TEMPLATE/refactor.md
!/.gitea/ISSUE_TEMPLATE/engineering.md
!/.gitea/PULL_REQUEST_TEMPLATE.md
+3
View File
@@ -0,0 +1,3 @@
# Reviewed historical local-development fixture: base64 of the deterministic
# string 0123456789abcdef repeated twice, never a runtime secret.
7e382d460be296b6c88e17084fff17bb7728f6ea:.env.development.example:generic-api-key:9
-3
View File
@@ -18,9 +18,6 @@
"gitlab": {
"release": false
},
"hooks": {
"after:git:init": "node scripts/check-release.cjs"
},
"plugins": {
"@release-it/conventional-changelog": {
"infile": "CHANGELOG.md",
+34 -23
View File
@@ -1,29 +1,40 @@
# 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 использует локальные
входы. Справочные источники не становятся обязательной цепочкой инструкций.
## Работа
До изменения проекта найди подходящую Gitea Issue (или создай после поиска),
проверь Acceptance Criteria, scope и рабочую ветку. Чтение/анализ допускаются
без Issue. Маршрут, выбор lightweight/SDD и cross-repo ссылки — в
[Engineering Workflow](.ai/conventions/engineering-workflow.md).
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.
Начни с результата и affected areas. До edits проследи поведение/invariants,
найди owner и 1–3 примера. Работай напрямую; OpenSpec/SDD — по явному запросу.
Перед Node-командой активируй `.nvmrc` affected repo через `nvm use --silent`.
| 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#релизы-и-ветки) |
## Ветки и релизы
Новая работа — в отдельной ветке от `develop`, затем PR в `develop`.
Прямой push в `main` запрещён. Commits и заголовки PR — Conventional Commits.
Перед release/hotfix, merge между `main` и `develop`, изменением release-конфигурации
или CI refs прочитай [процесс релиза](README.md#релизы-и-ветки).
Перед Node-командой активируй местную `.nvmrc`: если `nvm` не загружен
в non-interactive shell, загрузи установленный `$HOME/.nvm/nvm.sh`, затем
выполни `nvm use --silent` из root. Если `nvm.sh` недоступен, остановись;
не используй системный Node вместо проектного.
При новой границе и перед завершением повтори 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.
+45
View File
@@ -1,5 +1,50 @@
# Changelog
## [0.2.2](https://git.personal.ksv741.ru/WealthLedger/main/compare/v0.2.1...v0.2.2) (2026-10-12)
### Bug Fixes
* align workflow commitlint policy ([#27](https://git.personal.ksv741.ru/WealthLedger/main/issues/27)) ([03b3b59](https://git.personal.ksv741.ru/WealthLedger/main/commit/03b3b59e6ff2ba391f9dfc42a039a96d0d84c1c9))
### Documentation
* clarify root Node bootstrap [#24](https://git.personal.ksv741.ru/WealthLedger/main/issues/24) ([1ed4dd5](https://git.personal.ksv741.ru/WealthLedger/main/commit/1ed4dd5a73a250f24f452a26b5f5b0fcbb15f758))
* document PR ownership and board stage ([#27](https://git.personal.ksv741.ru/WealthLedger/main/issues/27)) ([59e0831](https://git.personal.ksv741.ru/WealthLedger/main/commit/59e0831ad28d6990416e651f55d783b8d2ac98c2))
* refine workflow conventions after review ([#27](https://git.personal.ksv741.ru/WealthLedger/main/issues/27)) ([fc589c9](https://git.personal.ksv741.ru/WealthLedger/main/commit/fc589c9130e90480c09f8fe8d47dd871aa05145e))
### Maintenance
* establish engineering workflow ([#27](https://git.personal.ksv741.ru/WealthLedger/main/issues/27)) ([2dc7225](https://git.personal.ksv741.ru/WealthLedger/main/commit/2dc72255fe51bb48a208e10846d42d9f59e2c1af))
## [0.2.1](https://git.personal.ksv741.ru/WealthLedger/main/compare/v0.2.0...v0.2.1) (2026-10-07)
### Bug Fixes
* **deps:** refresh release security baseline ([f27487b](https://git.personal.ksv741.ru/WealthLedger/main/commit/f27487b6a8cb59243a34e392059010dab38efd51))
### Documentation
* link production deployment repository ([9fbc23f](https://git.personal.ksv741.ru/WealthLedger/main/commit/9fbc23f7570ecb6ea13f7fae5490504d1a0d9ba8))
### Maintenance
* point container images to Nexus [#15](https://git.personal.ksv741.ru/WealthLedger/main/issues/15) ([80fa254](https://git.personal.ksv741.ru/WealthLedger/main/commit/80fa254a31d525dbc98bb3fe2d949f78c8e5872b))
## [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
+74 -39
View File
@@ -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,34 @@ 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 использует отдельный [репозиторий deploy](https://git.personal.ksv741.ru/WealthLedger/deploy):
его Git-тег закрепляет пару images, а `./deploy.sh vX.Y.Z` выполняет обновление.
Рабочие данные, сертификат T-Bank и постоянные ключи находятся вне checkout.
Только frontend публикует `127.0.0.1:8080`; серверный Caddy направляет туда весь
домен. Backend port остаётся внутренним. Корневой Compose сохраняется для development.
Пошаговая процедура установки, обновления и восстановления — README `deploy`;
архитектурные границы — [документация](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 +90,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 +103,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 +128,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,18 +136,20 @@ 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 |
| 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).
Общий маршрут Issue → PR — [Engineering Workflow](.ai/conventions/engineering-workflow.md).
## Релизы и ветки
@@ -126,8 +158,8 @@ T-Bank. Полные команды и ограничения принадлеж
`vX.Y.Z`. Root-репозиторий называется `main`, его ветки — `main` и `develop`.
`main` хранит принятые релизы, `develop` — интеграцию разработки. Новая задача:
отдельная ветка от свежего `develop`, затем PR в `develop`. Для веток агента
используется `codex/<task>`; также допустимы `feature/<task>` и `fix/<task>`.
отдельная [issue-linked ветка](.ai/conventions/engineering-workflow.md#git-branching)
от свежего `develop`, затем PR в `develop`.
В `main` напрямую не пушить. Обычные PR можно squash-merge; релизные PR и
обратную синхронизацию `main` с `develop` сливать с сохранением истории.
@@ -136,25 +168,27 @@ Commits и заголовки PR — Conventional Commits. `feat:` повыша
`docs:`, `spec:`, `chore:`, `build:`, `ci:`, `refactor:`, `test:`, `style:` и
`revert:` дают patch при выпуске. `chore(release):` — служебная запись,
исключённая из changelog и рекомендации версии. Release-it не запускается
автоматически после каждого commit. Номер связанной issue можно добавить в заголовок.
автоматически после каждого commit. Ссылка на Issue — в
[Commit Conventions](.ai/conventions/engineering-workflow.md#commit-conventions).
### Подготовка релиза
1. Обновить `main`, `develop` и теги командой `git fetch origin --tags`.
Проверить, что текущий `develop` содержит последний релиз из `main`.
Создать `release/<name>` от проверенного `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 +218,26 @@ Commits и заголовки PR — Conventional Commits. `feat:` повыша
`release/hotfix-<name>` в `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-тег.
+9 -1
View File
@@ -1,4 +1,12 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: { "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]] },
defaultIgnores: false,
ignores: [
message => /^chore\(release\): \d+\.\d+\.\d+$/.test(message.trim()),
message => /^Merge pull request ['"].+['"] \(#\d+\) from .+ into .+$/.test(message.trim()),
],
rules: {
"type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]],
"references-empty": [2, "never"],
},
};
+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
+14 -14
View File
@@ -1,12 +1,12 @@
{
"name": "wealth-ledger-workspace",
"version": "0.1.0",
"version": "0.2.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wealth-ledger-workspace",
"version": "0.1.0",
"version": "0.2.2",
"devDependencies": {
"@commitlint/cli": "21.2.2",
"@commitlint/config-conventional": "21.2.2",
@@ -1446,9 +1446,9 @@
}
},
"node_modules/basic-ftp": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz",
"integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-6.2.2.tgz",
"integrity": "sha512-LIkJcf/F6bTTfvE3k2YcTxrJsLKKVXiEpHZYkQP6JoqTb7khng7c13ckRTDQbqRh5IkehVH3F00sSlAa2F6DwA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2124,9 +2124,9 @@
}
},
"node_modules/fast-uri": {
"version": "3.1.7",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
"integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
"dev": true,
"funding": [
{
@@ -2355,9 +2355,9 @@
}
},
"node_modules/ip-address": {
"version": "10.7.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
"integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz",
"integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3439,9 +3439,9 @@
}
},
"node_modules/undici": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz",
"integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==",
"dev": true,
"license": "MIT",
"engines": {
+7 -2
View File
@@ -1,12 +1,11 @@
{
"name": "wealth-ledger-workspace",
"version": "0.1.0",
"version": "0.2.2",
"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"
@@ -17,5 +16,11 @@
"@release-it/conventional-changelog": "12.0.0",
"conventional-changelog-conventionalcommits": "10.4.0",
"release-it": "21.0.2"
},
"overrides": {
"basic-ftp": "6.2.2",
"fast-uri": "3.1.8",
"ip-address": "10.7.3",
"undici": "7.29.1"
}
}
-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")