sync-arch: явные ошибки при unknown contract version / status (Story 6.5) [C28-16] #57

Merged
ant merged 7 commits from parser2-epic-6-canonical-storage into parser2-epic-4-forks 2026-06-03 08:31:36 +00:00
Owner

Что в PR (после перегруппировки 2026-06-02)

Изначально этот PR содержал весь Эпик 6 sync-arch (5 stories). После обсуждения с командой эпик переразложен: цель MVP-релиза 1.1.2 — заменить транспорт parser1→parser2 без поломки прода. Архитектурная санитация sync-слоя — отдельная задача, в этот релиз не идёт.

В PR остался только Story 6.5 — очевидный фикс silent loss при schema drift, не инвазивный.

Story 6.5: явные ошибки при unknown contract version / unknown status

Раньше при mapper.mapDeltaToBlockchainData(delta) === null (контракт обновили, контроллер не знает версию) — warn + return null → парсер ACK'ал дельту → потеря события молча. Аналогично mapStatusToDomain возвращал UNDEFINED без сигнала.

Что добавлено:

  • UnsupportedContractVersionError (src/shared/sync/errors/unsupported-contract-version.error.ts) — кастомный класс с контекстом (contract/table/primary_key/block_num).
  • auditUnknownStatus(entityName, status, logger, allowedStatuses) (src/shared/sync/errors/audit-unknown-status.ts) — helper для error-log.
  • AbstractEntitySyncService.processDelta: при blockchainData === nulllogger.error('UNSUPPORTED_CONTRACT_VERSION', ctx); в strict-mode throw UnsupportedContractVersionError → парсер не ACK'нет дельту, dead-letter сработает.
  • ProjectDomainEntity.mapStatusToDomain: default ветка вызывает auditUnknownStatus(...) перед fallback на UNDEFINED.
  • Env: BLOCKCHAIN_UNSUPPORTED_VERSION_STRICT (default false) — не ломаем прод сразу, оператор может включить.

Что НЕ вошло (откатано в этом PR ревизионными коммитами)

  • Story 6.1 (namespace db/bc/derived + replaceBc + переписанный ProjectDomainEntity) — двойной канон на 22 entity, отложено в отдельный sync-arch sanitation-эпик.
  • Story 6.2 (declarative signedDocumentFields + normalizeSignedDocuments + path-parser) — DRY-рефакторинг без функциональной выгоды на текущем объёме.
  • Story 6.3 (Zod multi-sig schemas с signatures.length === 1/2) — документная семантика на бэкенде неправильно, число подписей — домен контракта (require_auth).
  • Story 6.4 (_checksum колонка + sha256(canonical-json(bc))) — без потребителя (Эпик 7 nightly snapshot + Эпик 8 reconciliation тоже отложены) = мёртвая колонка + +20% storage + лишний sha256 на каждое save/update.

Stacked-PR

  • Base: parser2-epic-4-forks (PR #54 на parser2, ждёт merge).
  • После merge #54 Gitea автоматически перетарджетит этот PR на parser2.

Test plan

  • tsc --noEmit — зелёный.
  • 10 jest blockchain unit suites / 64 tests — зелёные.
  • Функциональный прогон на стенде: создание project → unknown-status в логе при mock'е чужого статуса.

Дополнительные документы

  • Перегруппировка плана: _bmad-output/tracks/sync-arch/planning-artifacts/epics.md → раздел «Перегруппировка плана 2026-06-02».
  • Сводка MVP vs deferred: _bmad-output/tracks/sync-arch/planning-artifacts/MVP-parser2-vs-deferred.md.

🤖 Generated with Claude Code

## Что в PR (после перегруппировки 2026-06-02) Изначально этот PR содержал весь Эпик 6 sync-arch (5 stories). После обсуждения с командой эпик переразложен: цель MVP-релиза 1.1.2 — заменить транспорт parser1→parser2 без поломки прода. Архитектурная санитация sync-слоя — отдельная задача, в этот релиз не идёт. **В PR остался только Story 6.5** — очевидный фикс silent loss при schema drift, не инвазивный. ## Story 6.5: явные ошибки при unknown contract version / unknown status Раньше при `mapper.mapDeltaToBlockchainData(delta) === null` (контракт обновили, контроллер не знает версию) — `warn` + `return null` → парсер ACK'ал дельту → потеря события молча. Аналогично `mapStatusToDomain` возвращал `UNDEFINED` без сигнала. Что добавлено: - `UnsupportedContractVersionError` (`src/shared/sync/errors/unsupported-contract-version.error.ts`) — кастомный класс с контекстом (contract/table/primary_key/block_num). - `auditUnknownStatus(entityName, status, logger, allowedStatuses)` (`src/shared/sync/errors/audit-unknown-status.ts`) — helper для error-log. - `AbstractEntitySyncService.processDelta`: при `blockchainData === null` → `logger.error('UNSUPPORTED_CONTRACT_VERSION', ctx)`; в strict-mode `throw UnsupportedContractVersionError` → парсер не ACK'нет дельту, dead-letter сработает. - `ProjectDomainEntity.mapStatusToDomain`: default ветка вызывает `auditUnknownStatus(...)` перед fallback на UNDEFINED. - Env: `BLOCKCHAIN_UNSUPPORTED_VERSION_STRICT` (default `false`) — не ломаем прод сразу, оператор может включить. ## Что НЕ вошло (откатано в этом PR ревизионными коммитами) - **Story 6.1** (namespace `db/bc/derived` + `replaceBc` + переписанный ProjectDomainEntity) — двойной канон на 22 entity, отложено в отдельный sync-arch sanitation-эпик. - **Story 6.2** (declarative `signedDocumentFields` + `normalizeSignedDocuments` + path-parser) — DRY-рефакторинг без функциональной выгоды на текущем объёме. - **Story 6.3** (Zod multi-sig schemas с `signatures.length === 1/2`) — документная семантика на бэкенде неправильно, число подписей — домен контракта (`require_auth`). - **Story 6.4** (`_checksum` колонка + sha256(canonical-json(bc))) — без потребителя (Эпик 7 nightly snapshot + Эпик 8 reconciliation тоже отложены) = мёртвая колонка + +20% storage + лишний sha256 на каждое save/update. ## Stacked-PR - **Base:** `parser2-epic-4-forks` (PR #54 на `parser2`, ждёт merge). - После merge #54 Gitea автоматически перетарджетит этот PR на `parser2`. ## Test plan - [x] tsc --noEmit — зелёный. - [x] 10 jest blockchain unit suites / 64 tests — зелёные. - [ ] Функциональный прогон на стенде: создание project → unknown-status в логе при mock'е чужого статуса. ## Дополнительные документы - Перегруппировка плана: `_bmad-output/tracks/sync-arch/planning-artifacts/epics.md` → раздел «Перегруппировка плана 2026-06-02». - Сводка MVP vs deferred: `_bmad-output/tracks/sync-arch/planning-artifacts/MVP-parser2-vs-deferred.md`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
claude added 5 commits 2026-06-02 07:53:21 +00:00
BaseDomainEntity получает generic <TDb, TBc>: this.db (shallow-copy databaseData),
this.bc (nullable, заполняется через utility replaceBc()), block_num/present/status
остаются на базе (hot-path stale-delta guard). Backward-compat: TBc=unknown по
дефолту — 22 legacy entity компилируются без изменений.

replaceBc вынесен utility-функцией, не методом класса — protected/public метод
ломает structural typing там, где Omit<XxxDomainEntity,...> используется в сигнатурах
репо (state-typeorm-repo).

ProjectDomainEntity — эталон: PROJECT_BC_KEYS как const satisfies, updateFromBlockchain
вызывает replaceBc(this, blockchainData, PROJECT_BC_KEYS) вместо запрещённого
Object.assign(this, blockchainData). Плоские поля остаются для legacy compat —
миграция остальных потребителей на entity.bc.* трекется Epic 9.5.

composite-entity.contract.test.ts — рекурсивно сканит src/**/*.entity.ts, для
не-legacy entity запрещает Object.assign(this, ...). Legacy allowlist на 22 файла
с явной отсылкой на Epic 9.5. 99/99 entity prove'нуты + Project явно проверен
на replaceBc + PROJECT_BC_KEYS.

controller/CLAUDE.md — секция Composite-Entity (ADR-008) актуализирована: BC_KEYS
с satisfies ReadonlyArray<keyof IXxxBlockchainData> — canonical-ordered источник
для Story 6.4 checksum.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
AbstractBlockchainDeltaMapper получает protected readonly signedDocumentFields
ReadonlyArray<SignedDocField> (default []) + helper normalizeSignedDocuments(data).
SignedDocField описывает путь к IChainDocument2-полю в TBlockchainData; bracket-
нотация поддерживает массивы: `appendix` (top-level), `statement.attachments[]
.signed_attachment` (nested-array, E12). parseSignedDocPath → PathSegment[],
applyAtPath in-place трансформирует через
DomainToBlockchainUtils.convertChainDocumentToDomainFormat.

appendix-delta.mapper.ts — эталон: ручной convertChainDocumentToDomainFormat
(value.appendix) заменён декларацией signedDocumentFields = [{ path: 'appendix' }]
+ this.normalizeSignedDocuments({ ...value }).

signed-document-normalization.test.ts (16) — парсер 4 кейса + normalize 7 кейсов
(top-level, nested-array, missing-field no-op, multiple fields, empty config,
null/undefined, immutability caveat).

delta-mapper-signed-doc.contract.test.ts (3) — guard: mapper с непустым
signedDocumentFields обязан вызвать this.normalizeSignedDocuments — иначе
silent data corruption (meta остаётся JSON-строкой, Story 6.4 checksum
не сходится).

IPFS lazy resolver (E14) и миграция остальных mapper'ов с ручной нормализацией —
вне scope, тречится Epic 9.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Новый файл shared/sync/signed-document-schemas.ts: signatureInfoSchema (ISignatureInfo)
+ три per-doc-type схемы IChainDocument2:
- chainDocumentSchema — default ≥1 подпись (стандартный мультисиг кооператива).
- singleSignatureChainDocumentSchema — ровно 1 (одиночный автор).
- twoSignatureChainDocumentSchema — ровно 2 (двухподписные акты signsupp/signchair
  и signact1/signact2; OQ-13: ВСЕ подписанты массивом, primary не выделяется).

SignedDocField (Story 6.2) расширен optional schema?: ZodTypeAny — backward-compat
для legacy полей без валидации. AbstractBlockchainDeltaMapper.normalizeSignedDocuments
делает schema.parse ДО transform: при schema-drift цепи (новая структура IChainDocument2
из коопконтракта) — Zod бросает ZodError, mapper.try/catch отдаёт null + warn, Story 6.5
повысит до alert. До этого PR drift молча проходил, meta оставался JSON-строкой.

Эталон: appendix-delta.mapper.ts — добавлен singleSignatureChainDocumentSchema
(приложение к ТЭМ имеет одного автора).

signed-document-schemas.test.ts (16) — каждая схема: valid → ok, обязательные поля
отсутствуют → fail, неверное число подписей → fail. signed-document-normalization
расширен 3 тестами на schema-pre-transform: валидная schema → нормализация проходит,
single-signature на 2 подписи → ZodError, missing hash → ZodError + meta не обновлена
(transform не вызвался).

206/206 blockchain unit зелёные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
checksum.util.ts: canonicalStringify(value) (рекурсивная сортировка ключей объектов,
порядок массивов сохраняется, bigint → string, undefined → "null"); computeBcChecksum
→ sha256-hex 64 chars от canonical-stringified bc-namespace. Без внешних зависимостей.

BaseTypeormEntity получает колонку `_checksum varchar(64) nullable` —
legacy записи без bc-namespace получают хэш от "null" (стабильно, но не несёт
информации); после миграции Epic 9.5 на namespace все блокчейн-зеркала получат
содержательный checksum. BaseDomainEntity получает поле `_checksum?: string | null`.

BaseBlockchainRepository.save и .update вызывают protected applyBcChecksum(domain,
typeormEntity) ПОСЛЕ mapper.toEntity и ДО repository.save — proseться через единую
точку, не лезем в 22 mapper'а. Поле bc-namespace → reconciliation Epic 8.2
сравнивает только то, что реально в цепи (локальные db-поля типа matrix_room_id
из цепи не получаются и в checksum их быть не должно).

checksum.util.test.ts (14) — canonicalStringify: primitives/array/nested/bigint/sort;
computeBcChecksum: 64 hex chars / детерминизм / change-detection / null-stable /
порядок массивов / контрольный фиксированный хэш на nested-структуре (catch для
несовместимого изменения алгоритма).

base-repo-checksum.contract.test.ts (3) — bc заданный → checksum по нему;
bc undefined → checksum от null; повторный вызов → одинаковый _checksum.

223/223 blockchain unit зелёные.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Новые shared/sync/errors:
- UnsupportedContractVersionError(entityName, ctx{contract,table,primary_key,
  block_num}) — носит контекст для DLQ-операторской диагностики.
- auditUnknownStatus(entityName, receivedStatus, logger, allowedStatuses?) —
  фиксирует unknown-status drift в audit-trail с ожидаемыми вариантами.

AbstractEntitySyncService.processDelta: mapper вернул null → logger.error
("UNSUPPORTED_CONTRACT_VERSION", ctx) ВСЕГДА (раньше был silent warn). В strict-mode
(config.blockchain.unsupported_version_strict=true) дополнительно throw — парсер не
ACK'нет дельту, DLQ сработает. UnsupportedContractVersionError из try/catch
пробрасывается дальше; остальные ошибки по-прежнему логируются и return null.

config.ts: BLOCKCHAIN_UNSUPPORTED_VERSION_STRICT (default false) → blockchain
.unsupported_version_strict. Default false — не ломать прод немедленно;
включается на стенде после подтверждения отсутствия schema drift.

ProjectDomainEntity.mapStatusToDomain эталонно — default ветка вместо silent
UNDEFINED-возврата вызывает auditUnknownStatus с полным списком ожидаемых:
[pending,active,voting,result,finalized,cancelled]. Остальные mapStatusToDomain
(state, vote, segment, debt и т.д.) — Epic 9.5.

unsupported-version-explicit-error.test.ts (6) — UnsupportedContractVersionError
конструктор/контекст; auditUnknownStatus с/без allowedStatuses;
processDelta non-strict (logger.error + null), strict (throw),
happy-path (handleSyncDelta вызывается).

229/229 blockchain unit зелёные.

Эпик 6 закрыт целиком: 6.1 namespace, 6.2 normalizeSignedDocuments, 6.3 Zod schemas,
6.4 checksum, 6.5 explicit errors. Release 1.1.2 движется.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
claude added 1 commit 2026-06-02 08:38:08 +00:00
Story 6.3 (per-doc-type zod schemas): валидация `signatures.length === 1/2` зашивала
документную семантику в нормализатор parser-event. Число и валидность подписей — домен
контракта (`require_auth`), не бэкенда. При расширении доктайпов контракта (1→2 подписи)
sync падал бы на структурно-валидной цепи. Структурный transform `meta: JSON-string → object`
(Story 6.2) остаётся.

Story 6.4 (`_checksum` колонка + sha256(canonical-json(bc))): потребитель — Epic 7 nightly
snapshot и Epic 8 reconciliation — ещё не реализованы. Колонка хранила бы пустую нагрузку
+ +20% storage + лишний sha256 на каждом save/update. Принцип «не добавлять контрольных
полей до появления потребителя». Алгоритм canonicalStringify заведём как часть Epic 8.

Удалено: src/shared/sync/{checksum.util.ts, signed-document-schemas.ts} + 3 теста +
applyBcChecksum + колонка _checksum + поле _checksum на BaseDomainEntity. Story 6.1
(namespace db/bc + replaceBc), 6.2 (declarative signedDocumentFields + meta transform),
6.5 (UnsupportedContractVersionError + auditUnknownStatus) сохраняются.

13 jest blockchain unit suites / 196 tests зелёные. tsc зелёный.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
claude added 1 commit 2026-06-02 10:37:57 +00:00
Цель релиза 1.1.2 — «затащить parser2 нормально», т.е. заменить транспорт parser1→parser2.
Эпик 6 «единый порядок хранения данных» — отдельная архитектурная санитация, не связана с
заменой транспорта. Включение её в MVP-релиз привнесло инвазивные изменения:

- Story 6.1 (namespace `db`/`bc` + `replaceBc` + переписанный ProjectDomainEntity) вводит
  двойной стандарт: одна entity на новом паттерне, 21 entity на старом. Потребители
  читают `project.master` (плоский флэт) — частичная миграция создаёт регрессии у resolver'ов.
  Полная миграция = большой blast radius на 22 entity + Epic 9.5 backlog. Откладывается
  в отдельный sync-arch sanitation-эпик.

- Story 6.2 (declarative `signedDocumentFields` + `normalizeSignedDocuments` + path-parser)
  — DRY-рефакторинг без функциональной выгоды. AppendixDeltaMapper и так руками вызывал
  `convertChainDocumentToDomainFormat`. Польза появится когда подписанных полей станет
  много — пока их одно поле в одной точке.

Остаётся: Story 6.5 (UnsupportedContractVersionError + auditUnknownStatus +
`BLOCKCHAIN_UNSUPPORTED_VERSION_STRICT`). Очевидный фикс silent loss при schema drift,
не инвазивный.

Удалено: composite-entity.contract.test.ts, signed-document-normalization.test.ts,
delta-mapper-signed-doc.contract.test.ts. CLAUDE.md секция Composite-Entity ADR-008
помечена как «будущая цель, не сейчас».

10 jest blockchain unit suites / 64 tests зелёные. tsc зелёный.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
claude changed title from Эпик 6 sync-arch: единый порядок хранения данных и подписанных документов (релиз 1.1.2) [C28-16] to sync-arch: явные ошибки при unknown contract version / status (Story 6.5) [C28-16] 2026-06-02 10:44:15 +00:00
ant approved these changes 2026-06-03 08:31:24 +00:00
ant merged commit 70752b2caa into parser2-epic-4-forks 2026-06-03 08:31:36 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: C9S/mono#57