32 KiB
AURORA-1896: формулы ячеек на сервере — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Формула ячейки (=100+50) хранится на сервере вместе со значением и видна всем пользователям листа — маркер fx с выражением, автором и временем, сразу по WebSocket и после перезагрузки; для форм и отчётов проектов.
Architecture: Новая таблица v3.cell_formula (владелец — FK на форму или отчёт проекта с каскадом), SQL-функции v3.set_cell_formulas (проверка + upsert/delete) и v3.v_cell_formulas (чтение), триггеры очистки на таблицах строк. Репозитории вызывают set_cell_formulas сразу после записи значения в той же транзакции; WS-рассылка cell_updated несёт сохранённую формулу. Фронт держит карту формул листа в RealtimeContext, показывает маркер в Cell и подставляет формулу в EditCell; localStorage для формул убирается.
Tech Stack: PostgreSQL (PL/pgSQL, схема v3), Alembic с SQL-файлами, FastAPI 0.111, SQLAlchemy 2 async; React 18 (JSX), MUI, node --test.
Spec: openspec/changes/aurora-1896-cell-formulas/ — proposal.md, specs/cell-formula/spec.md, design.md (решения D1–D9). Исполнители читают design.md своей задачи целиком.
Global Constraints
- Базовая ветка
test; работа только в worktree/Users/andrey.epifancev/projects/fomichev_claude/DFiP_Budget_planing/.claude/worktrees/aurora-1896, веткаfeat/320-aurora-1896-cell-formulas. Вapi/src/,api/alembic/,web/src/писать можно только при наличии этого плана (хук require-plan). - Миграция:
api/alembic/versions/0059_cell_formula.py(revision = "0059",down_revision = "0058",run_sql_file_migration("0059_cell_formula.sql")) +api/alembic/versions/sql/0059_cell_formula.sql. Только именованные dollar-теги ($function$,$do$):alembic/sql_migration._split_statementsне распознаёт$$. Проверять миграцию только черезpytest --init_db, не через psql. - Python-окружение:
/Users/andrey.epifancev/projects/fomichev_claude/.venvs/dfip-api/bin/pytest(в worktree venv нет; ничего не ставить из сети, не обходить Nexus). - Postgres: контейнер
perf-pg,perf:perf@localhost:55432. Своя БД на прогон:docker exec perf-pg psql -q -U perf -d perf -c "DROP DATABASE IF EXISTS verify_1896" -c "CREATE DATABASE verify_1896"; затемcd api && DATABASE_URL="postgresql+asyncpg://perf:perf@localhost:55432/verify_1896" <venv>/bin/pytest --init_db …. Не трогать базыperf,perf_1835,verify_1936,verify_1879. - Эталон весь набора на test
5a91f35: 10 известных падений —test_db_functions_integration::test_backend_calls_form3_add_project_and_add_line_functions, 3×test_form2_ahr_satellite_spod_integration::test_satellite_spod_written_and_included_in_q4_total[*],test_upd_form_cells_subtree::test_edit_path_does_not_call_v_form_view[delta],::test_ahr_delta_matches_get_after_rent_edit,::test_jit_off_on_builders_and_edit_path,test_upd_form_cells_subtree_0037_guard::test_v_form2_view_keeps_0037_limit_formula, 2×test_export_service_query_params. Новых падений быть не должно. - Фронт:
node_modulesнет в worktree — симлинкweb/node_modules→/Users/andrey.epifancev/projects/fomichev_claude/DFiP_Budget_planing/web/node_modules(не коммитить). Тесты:cd web && node --test src/components/RealtimeTable/utils/*.test.js. Сборкаnpm run build. Не менятьnpm_config_registry. - Префикс ошибки —
invalid_formula:. Выражение: длина 1..500, regex^[0-9+\-*/%(),. ]+$после нормализации (btrim, снять один ведущий=,btrim). Сравнение формулы со значением —|a − b| ≤ 1e-9. - Подсказка маркера — ровно
=<expression> · <ФИО> · дд.мм.гггг чч:мм; если ФИО нет —Пользователь #<id>. - Текст подсказки при вводе выражения без «=» — «Формула начинается с =».
- Коммиты на русском в стиле истории (
feat(formulas): … (AURORA-1896)), последняя строка сообщения:Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>. Не пушить.
Review Focus
- Формула в нечисловую ячейку (
header.justification,name) или сvalue: null→ 422invalid_formula, значение не записано (Task 3). - Фоновое
cell_updatedот синхронизации сателлитов (data:{source_sheet}, безline_id/column) не должно стирать формулы листа AHR у клиента (Task 6). - AHR_LIMIT:
line_id = limit_template.idодинаков во всех формах — формула формы A не видна в форме B (Task 1). - Десятичная запятая
100,5+1принимается сервером как есть (фронт сам вычисляет) (Task 1). - Каскады: окончательное удаление формы с формулами и удаление проекта с формулами не падают на FK (Task 1, Task 4).
Task 1: Миграция — таблица, функции, триггеры
Files:
- Create:
api/alembic/versions/0059_cell_formula.py,api/alembic/versions/sql/0059_cell_formula.sql - Create:
api/tests/integration/test_cell_formula_sql.py - Modify:
ARCHITECTURE.md(абзац «Триггеры» — первые триггеры в схеме, что делают)
Interfaces:
-
Produces:
- таблица
v3.cell_formula— ровно как в design.md D1 (колонки, CHECK, индексыcell_formula_form_uidx,cell_formula_report_uidx,cell_formula_line_idx); v3.set_cell_formulas(p_budget_form_id integer, p_report_id integer, p_sheet varchar, p_changes jsonb, p_user_id integer) RETURNS TABLE(line_id integer, column_key text, expression text, value numeric, updated_by integer, updated_by_name text, updated_at timestamptz)— по design.md D2/D3; возвращает по строке на каждый элементp_changes, у которого после вызова есть формула (элементы без формулы не возвращаются);v3.v_cell_formulas(p_budget_form_id integer, p_report_id integer, p_sheet varchar)— тот же RETURNS TABLE, все формулы владельца и листа,ORDER BY line_id, column_key;- триггерные функции и триггеры по таблице design.md D4 (
v3.cell_formula_cleanup_<table>(),AFTER DELETE FOR EACH ROW). - Ровно один из
p_budget_form_id/p_report_idне NULL, иначеRAISE EXCEPTION 'invalid_formula: owner'.
- таблица
-
Step 1: Write the failing tests в
test_cell_formula_sql.pyчерез фикстуруsvod2_sql(tests/integration/conftest.py:136; работает на сессии клиента с откатом). Владелец — форма 1 из фикстуры; строка — любаяbudget_lineформы 1 на AHR (SELECT id FROM v3.budget_line WHERE budget_form_id = 1 LIMIT 1, skip если нет); пользователь — id админа (SELECT id FROM v3.app_user WHERE username = 'admin').test_set_formula_upsert_and_list:set_cell_formulas(1, NULL, 'AHR', '[{"line_id":L,"column":"plan.q1","value":150,"formula":"100+50"}]', admin)возвращает одну строкуexpression='100+50',value=150,updated_by=admin,updated_by_nameне NULL;v_cell_formulas(1, NULL, 'AHR')содержит её; повтор с"formula":"200-50"→ вv_cell_formulasодна запись200-50.test_set_without_formula_deletes: после установки — вызов с тем же элементом без ключаformula→v_cell_formulasпуст для ячейки, функция ничего не вернула.test_normalization:"formula":" =100+50 "→expression='100+50';"formula":"100,5+1"→expression='100,5+1'.test_invalid_formula_raises(параметризовать):"drop table",""-после-нормализации не ошибка (= нет формулы),"1"*501,value: nullс формулой,value: "abc"с формулой →pytest.raises(Exception, match="invalid_formula:")(для пустой строки — не raise, формулы нет).test_ahr_limit_formula_scoped_to_form: формула наAHR_LIMIT,line_id= любойlimit_template.id(row_type='LEAF'), для формы 1 →v_cell_formulas(<другая форма из фикстуры>, NULL, 'AHR_LIMIT')не содержит её (skip, если второй формы нет).test_trigger_budget_line_delete: формула на строке L →DELETE FROM v3.budget_line WHERE id = L(предварительно удалить зависимые: проще создать свою строку черезv3.add_budget_lineили найти строку без зависимостей; если громоздко — использоватьv3.del_budget_line(L, direction, 'AHR')подSET LOCAL app.user_idадмина) → формул строки нет.test_cascade_hard_delete_form: формула на форме →SELECT v3.hard_delete_budget_form(id)проходит (подготовить форму по условиям 0039:to_delete, другой пометивший — смотриbudget_form_service.hard_deleteи существующие тесты удаления форм; если подготовка недоступна в фикстуре — skip с причиной) и формул формы нет.test_owner_check: оба владельца NULL →invalid_formula:.
-
Step 2: Run to verify failure —
pytest --init_db tests/integration/test_cell_formula_sql.pyна своей БД → FAIL (function v3.set_cell_formulas … does not exist). -
Step 3: Implement миграцию по Interfaces и design.md D1–D4. Тело проверки и upsert —
FOR v_change IN SELECT * FROM jsonb_array_elements(p_changes); upsert —INSERT … ON CONFLICT (budget_form_id, sheet, line_id, column_key) WHERE budget_form_id IS NOT NULL DO UPDATE(и аналог для отчёта).updated_by_name—app_user.full_name. Комментарий в начале SQL-файла: зачем триггеры и что они удаляют. Абзац в ARCHITECTURE.md. -
Step 4: Run to verify pass — пересоздать БД
verify_1896,pytest --init_db tests/integration/test_cell_formula_sql.py→ PASS. -
Step 5: Commit —
feat(formulas): таблица cell_formula, set/v_cell_formulas, триггеры очистки, миграция 0059 (AURORA-1896).
Task 2: Репозиторий формул и префикс ошибки
Files:
- Create:
api/src/repository/cell_formula_repository.py - Modify:
api/src/core/CONSTANTS.py:1-11(добавить"invalid_formula:"вVALIDATION_PREFIXES) - Modify:
api/src/domain/schemas.py(новыеUserShort,CellFormulaResponse) - Test:
api/tests/unit/test_cell_formula_repository.py, дополнитьapi/tests/unit/test_exception_handlers.py
Interfaces:
-
Consumes: SQL из Task 1.
-
Produces:
class CellFormulaRepository(db: AsyncSession):async def set_formulas(self, *, budget_form_id: int | None, report_id: int | None, sheet: str, changes: list[dict], user_id: int) -> list[dict]— вызываетSELECT * FROM v3.set_cell_formulas(:budget_form_id, :report_id, :sheet, CAST(:changes AS JSONB), :user_id),changesсериализуютсяjson.dumps; возвращает[{"line_id", "column", "expression", "value": float, "updated_by": {"id", "full_name"}, "updated_at": datetime}](column←column_key,value→ float);async def list_formulas(self, *, budget_form_id: int | None, report_id: int | None, sheet: str) -> list[dict]— то же изv3.v_cell_formulas.
schemas.UserShort(BaseModel): id: int; full_name: str | None = Noneschemas.CellFormulaResponse(BaseModel): line_id: int; column: str; expression: str; value: float; updated_by: UserShort; updated_at: datetimeCellPatchполучаетformula: str | None = None(используется в Task 3/4).
-
Step 1: Write failing unit tests (мок
AsyncSessionпо образцу существующих unit-тестов репозиториев):test_set_formulas_calls_sql_with_json_changes—executeвызван с текстом, содержащимv3.set_cell_formulas, и параметрамиbudget_form_id=1, report_id=None, sheet='AHR', user_id=7,changes— JSON-строка, в которой у элемента естьformula;test_set_formulas_maps_rows— строка результата(10, 'plan.q1', '100+50', Decimal('150'), 7, 'Иванов И.И.', dt)→{"line_id": 10, "column": "plan.q1", "expression": "100+50", "value": 150.0, "updated_by": {"id": 7, "full_name": "Иванов И.И."}, "updated_at": dt};test_list_formulas_calls_view. Вtest_exception_handlers.py: ошибка с текстомinvalid_formula: expression→ статус 422,field == "expression"(по образцу существующих кейсов префиксов). -
Step 2: Run
pytest tests/unit/test_cell_formula_repository.py tests/unit/test_exception_handlers.py→ FAIL. -
Step 3: Implement репозиторий, префикс, схемы, поле
CellPatch.formula. -
Step 4: Run → PASS; весь
pytest tests/unit— только 2 известных падения export. -
Step 5: Commit —
feat(formulas): CellFormulaRepository, invalid_formula → 422, схемы (AURORA-1896).
Task 3: Формы — запись формулы и GET formulas (REST)
Files:
- Modify:
api/src/services/sheet_service.py:55-93(update_cells,update_cell), новый метод чтения - Modify:
api/src/api/v1/forms.py:202-320(PATCHcell/cellsпередаютformula), новый GET - Test:
api/tests/integration/test_cell_formula_integration.py(новый)
Interfaces:
-
Consumes:
CellFormulaRepository(Task 2),CellPatch.formula. -
Produces:
SheetService.update_cell_with_formula(self, form_id, sheet, direction, sections, line_id, column, value=None, formula: str | None = None, user: AppUser | None = None) -> tuple[list[tuple], dict | None]— пишет значение прежнимsheet_repo.update_cell, затем (еслиuser)CellFormulaRepository.set_formulas(budget_form_id=form_id, report_id=None, sheet=sheet, changes=[{"line_id", "column", "value", "formula"}], user_id=user.id); возвращает(rows, formula_or_None). Ключformulaв change передаётся только если не None.SheetService.update_cell(... , formula: str | None = None, ...) -> list[tuple]— возвращаетupdate_cell_with_formula(...)[0](сигнатура прежняя +formula).SheetService.update_cells(..., changes: list[dict], ...)— после записи вызываетset_formulasс теми жеchanges(каждый элемент может нестиformula).SheetService.list_formulas(self, form_id: int, sheet: str) -> list[dict].GET /api/v1/form/{form_id}/sheet/{sheet}/formulas→BaseListResponse[CellFormulaResponse]; доступ —BudgetFormService.get(нет → 404 «Форма не найдена»), лист — проверка поform_type.sheet_list, как в PATCH ячейки; безdirection.
-
Step 1: Write failing integration tests (
client,admin_tokens,isp_tokens,auth_headers; форма 1, лист AHR,?direction=Support, строка и колонка — как вtest_forms_api_smoke.py:72-125):test_patch_cell_with_formula_then_get→ GET formulas содержит{line_id, column: 'plan.q1', expression: '100+50', value: 150, updated_by.id == <admin id>};test_patch_without_formula_removes;test_patch_invalid_formula_422_value_unchanged—formula: 'drop table'→ 422,error.code/поле как у других validation-ошибок; последующий GET листа — значение прежнее (сравнить с GET до запроса);test_patch_formula_into_text_column_422—column: 'header.justification',value: 'текст',formula: '1+1'→ 422 (Review Focus);test_patch_formula_null_value_422—value: None,formula: '1+1'→ 422;test_patch_formula_forbidden_403_no_formula—isp_tokens(исполнитель не в оргединице формы 1) → 403, GET formulas под админом — формулы нет;test_patch_cells_mixed— два элемента в/cells, один с формулой → одна формула;test_delete_line_removes_formulas— создать строку (POST …/line, как в существующих тестах добавления строк), формула на ней,DELETE …/line/{id}→ формул строки нет;test_get_formulas_unknown_form_404. Помнить: после ошибочного запроса тестовая сессия откатывается целиком (tests/integration/conftest.py:34-36) — проверки «значение не изменилось/формулы нет» делать отдельным GET в том же тесте только если откат это допускает; иначе проверять, что в ответе ошибки, и что GET после отката не содержит формулы.
-
Step 2: Run → FAIL.
-
Step 3: Implement сервис и эндпоинты по Interfaces. В REST PATCH
cellпередатьformula=body.formula; вcells— каждый элементc.model_dump(exclude={"formula"}), плюс ключformula, только еслиc.formula is not None. -
Step 4: Run новые тесты → PASS;
pytest tests/integration/test_forms_api_smoke.py→ без новых падений. -
Step 5: Commit —
feat(formulas): запись формулы при правке ячейки формы и GET formulas листа (AURORA-1896).
Task 4: Отчёты проектов — запись формулы и GET formulas (REST)
Files:
- Modify:
api/src/services/project_service.py:285-320(upd_form3_cell,upd_form3_cells), новый метод чтения - Modify:
api/src/api/v1/projects.py:332-370, новый GET - Test:
api/tests/integration/test_cell_formula_integration.py(дополнить)
Interfaces:
-
Consumes: Task 2;
resolve_report_id(нормализованныйreport_type—report_type.upper(), допускаютсяLIMIT,CURRENT_EXPENSES). -
Produces:
ProjectService.upd_form3_cell_with_formula(self, project_id, year, report_type, line_id, column, value, formula: str | None, user: AppUser) -> tuple[list[tuple], dict | None];upd_form3_cell(..., formula: str | None = None, ...)возвращает[0];upd_form3_cells— формулы поchanges. Владелец —report_id=<resolve_report_id>,sheet=<нормализованный report_type>.ProjectService.list_formulas(self, project_id, year, report_type, user) -> list[dict].GET /api/v1/projects/{project_id}/report/{year}/{report_type}/formulas→BaseListResponse[CellFormulaResponse]; отчёт не найден → то же, что у PATCH (ValidationException «Отчёт не найден»).
-
Step 1: Write failing tests (по образцу
test_projects_api_smoke.py:107-160):test_project_report_formula_roundtrip— PATCH…/cellсformula→ GET…/formulasсодержит запись;report_typeв пути в нижнем регистре (limit) работает так же;test_project_report_formula_removed_without_formula;test_project_delete_cascades_formulas— если в фикстуре можно удалить проект (v3.del_project/ API удаления проекта; условияcan_delete_project) — после удаления проекта запросSELECT count(*) FROM v3.cell_formula WHERE rf_project_report_id = :id= 0, иначе skip с причиной. -
Step 2: Run → FAIL.
-
Step 3: Implement по Interfaces.
-
Step 4: Run → PASS;
pytest tests/integration/test_projects_api_smoke.pyбез новых падений. -
Step 5: Commit —
feat(formulas): формулы в отчётах проектов — запись и GET formulas (AURORA-1896).
Task 5: WebSocket — formula во входящем cell_updated и в рассылке
Files:
- Modify:
api/src/api/v1/websocket.py—FormEventProcess.__update_cell(:510-534),:739-762)ProjectEventProcess.__update_cell( - Test:
api/tests/integration/test_cell_formula_ws_integration.py(новый, по образцуtest_websocket_connection_cache_integration.py)
Interfaces:
-
Consumes:
SheetService.update_cell_with_formula,ProjectService.upd_form3_cell_with_formula. -
Produces: после записи
__update_cellмутируетevent_data(он и естьdataрассылки):event_data["formula"] = formula["expression"] if formula else None;event_data["formula_meta"] = {"updated_by": {"id", "full_name"}, "updated_at": <ISO-строка>} if formula else None. Входящееevent_data.get("formula")передаётся в сервис (отсутствует → None). Возвращает строки, как раньше. -
Step 1: Write failing tests: WS к
/api/v1/ws/form/1/sheet/AHR?direction=Support, логин и последовательностьcell_edit_start→cell_updated(сdata.formula = "100+50",value = 150) →cell_edit_end, как в образце; в полученномcell_updateddata.formula == "100+50",data.formula_meta.updated_by.id == <admin id>,updated_at— строка; второйcell_updatedтой же ячейки безformula→data.formula is None,data.formula_meta is None. WS коммитит: вfinallyвернуть прежнее значение и удалить формулу (DELETE FROM v3.cell_formula WHERE budget_form_id = 1 AND line_id = … AND column_key = 'plan.q1'отдельной сессией с commit), как образец откатывает значение. Кейс с клиентской подменой: входящееdata.formula = " =100+50 "→ в рассылке"100+50". -
Step 2: Run → FAIL.
-
Step 3: Implement по Interfaces в обоих процессорах.
-
Step 4: Run → PASS;
pytest tests/integration/test_websocket_connection_cache_integration.py tests/unit/test_websocket_connection_cache.pyбез новых падений. -
Step 5: Commit —
feat(formulas): formula и formula_meta в cell_updated по WebSocket (AURORA-1896).
Task 6: Фронт — чистые утилиты формул и API-клиент
Files:
- Create:
web/src/components/RealtimeTable/utils/cellFormulaUtils.js,web/src/components/RealtimeTable/utils/cellFormulaUtils.test.js - Modify:
web/src/api/— клиент форм (гдеgetSheet/TasksApi) и проектов: функции чтения формул
Interfaces:
-
Produces (
cellFormulaUtils.js, без импортов React/MUI/constants.jsx):formulaKey(lineId, column) -> string—${lineId}|${column};buildFormulaMap(list) -> Map— из ответа GET formulas ({line_id, column, expression, value, updated_by, updated_at}), ключformulaKey;applyFormulaEvent(map, data) -> Map— новыйMap(не мутирует вход);data.line_id == null || data.column == null→ вернутьmapкак есть;typeof data.formula === 'string' && data.formula→ записать{line_id, column, expression: data.formula, value: Number(data.value), updated_by: data.formula_meta?.updated_by, updated_at: data.formula_meta?.updated_at}; иначе удалить ключ;isFormulaActual(formula, value) -> boolean—formulaесть, оба приводятся к конечным числам,Math.abs(a - b) <= 1e-9;formatFormulaHint(formula) -> string—=<expression> · <full_name | "Пользователь #id"> · дд.мм.гггг чч:мм(локальное время; безupdated_at— без последней части);clearLegacyFormulaStorage(storage) -> number— удаляет из переданного storage все ключи, начинающиеся сformula_, возвращает число удалённых; ошибки доступа глотает и возвращает 0.
-
Produces (API):
getSheetFormulas(formId, sheet)→GET /form/${formId}/sheet/${encodeURIComponent(sheet)}/formulas→result;getProjectReportFormulas(projectId, year, reportType)→GET /projects/${projectId}/report/${year}/${reportType}/formulas→result. Разместить в существующих модулях API рядом с запросами листа/отчёта. -
Step 1: Write failing tests
cellFormulaUtils.test.js:applyFormulaEvent— строка → запись сexpression,valueчислом,updated_byизformula_meta;formula: null→ удаление; без поляformula→ удаление;{source_sheet: 'AHR_RENT'}(безline_id/column) → та же карта (Review Focus); вход не мутируется;isFormulaActual—150и'150'→ true,150и150.0000000001→ true,150и151→ false,nullформула → false, нечисловое значение → false;formatFormulaHint— с ФИО и датой2026-10-27T14:03:00→=100+50 · Иванов И.И. · 27.10.2026 14:03, без ФИО →Пользователь #7;clearLegacyFormulaStorageна фейковом storage ({length, key(i), removeItem}) с ключамиformula_v2_a,formula_x,other→ удаляет 2,otherостаётся; storage, бросающий при доступе → 0;buildFormulaMap/formulaKey. -
Step 2: Run
node --test src/components/RealtimeTable/utils/cellFormulaUtils.test.js→ FAIL. -
Step 3: Implement утилиты и функции API.
-
Step 4: Run все
node --test src/components/RealtimeTable/utils/*.test.js→ PASS. -
Step 5: Commit —
feat(formulas): утилиты карты формул и API чтения формул (AURORA-1896).
Task 7: Фронт — карта формул в RealtimeContext, маркер в Cell, EditCell без localStorage
Files:
- Modify:
web/src/components/RealtimeTable/contexts/RealtimeContext.js(загрузка карты при открытии листа и переподключении;handleMessage:118-181 — наcell_updatedapplyFormulaEvent; отправкаdata.formulaвupdateCell:313-332; экспорт карты через контекст) - Modify:
web/src/components/RealtimeTable/Cell/Cell/Cell.jsx(маркерfx+ Tooltip) - Modify:
web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx(начальный текст из карты, удалить всё про localStorage, при сохранении формулы передавать выражение) - Modify:
web/src/components/RealtimeTable/utils/formulaUtils.js(еслиresolveEditorStateбольше не нужен — удалить вместе с тестами; подсказка для ввода без «=») - Modify:
web/src/components/RealtimeTable/tableColumns.jsx(проброс того, что нужно Cell/EditCell, если не через контекст)
Interfaces:
-
Consumes: Task 6 (
buildFormulaMap,applyFormulaEvent,isFormulaActual,formatFormulaHint,formulaKey,clearLegacyFormulaStorage,getSheetFormulas,getProjectReportFormulas); строка —getRowId(row)(utils/rowUtils.js:34), колонка — ключ безdata.(какgetColumnRequestKey,RealtimeContext.js:13-19). -
Produces: хук
useCellFormula(lineId, column) -> formula | undefinedизRealtimeContext(или эквивалентный селектор),updateCell(row, column, value, { formula })—formula(строка без «=») кладётся вdata.formulaисходящегоcell_updated, при обычном вводе поля нет. -
Step 1: Behaviour by design.md D7 — реализовать: загрузка карты (
isProject→getProjectReportFormulas(formId, year, sheetName), иначеgetSheetFormulas(formId, sheetName)), ошибка загрузки — тост не нужен, карта пустая,console.warn;clearLegacyFormulaStorage(window.localStorage)один раз при загрузке модуляEditCell/контекста вtry/catch; маркерfx— только приisFormulaActual(formula, cell.getValue()), в углу ячейки, не перекрывая индикацию редактируемости AURORA-1890; TooltipformatFormulaHint(formula), приdisplayFactor !== 1дописать(в тыс. руб.)или(в руб.)поgetMoneyUnit; EditCell открывается с=+expressionпри актуальной формуле, при открытии набором символа (AURORA-1891) — как сейчас; ввод без «=», похожий на выражение (/[+\-*/%()]/после первой цифры) — не отправлять,toast.info('Формула начинается с ='), редактор не закрывать. -
Step 2: Verify —
node --test src/components/RealtimeTable/utils/*.test.js→ PASS;npm run build→ успешно;npx biome checkпо изменённым файлам — не больше ошибок, чем до правки;grep -rn "localStorage" web/src/components/RealtimeTable/Cell— нет обращений для формул. -
Step 3: Commit —
feat(formulas): маркер fx у ячейки, формула с сервера в редакторе, без localStorage (AURORA-1896).
Task 8: Финальная проверка ветки
Files: —
- Step 1: Весь
pytest --init_dbна новой БДverify_1896→ те же 10 падений, что в Global Constraints, остальное PASS; сохранить сравнениеdiffсписков FAILED. - Step 2:
git fetch origin && git log origin/test --oneline -5: если в test уже есть0059_*(PR #315), переименовать миграцию в0060_cell_formula(revision = "0060",down_revision = "0059", имя SQL-файла), rebase наorigin/test, повторить Step 1. - Step 3:
openspec validate aurora-1896-cell-formulas→ valid. - Step 4: Commit (если были правки) —
chore(formulas): номер миграции после rebase (AURORA-1896).