--- name: nacl-sa-architect model: opus effort: high description: | Декомпозиция системы на модули (Bounded Contexts), построение Context Map и определение NFR. Читает BA-данные из Neo4j графа, записывает Module/Requirement узлы в граф. Используй когда пользователь просит: спроектировать архитектуру в графе, разбить на модули, определить bounded contexts, создать обзор системы, nacl-sa-architect. --- # /nacl-sa-architect --- Архитектурная декомпозиция (Graph) ## Назначение Декомпозиция информационной системы на функциональные модули (Bounded Contexts), определение межмодульных зависимостей и высокоуровневых нефункциональных требований. Все данные читаются из Neo4j графа (BA-подграф) и записываются в Neo4j граф (SA-подграф). --- ## Shared References Read `nacl-core/SKILL.md` for: - Neo4j MCP tool names and connection info (`mcp__neo4j__read-cypher`, `mcp__neo4j__write-cypher`) - ID generation rules - Schema files location (`graph-infra/schema/sa-schema.cypher`) - Query library location (`graph-infra/queries/`) ### Key Schema (SA Layer) From `graph-infra/schema/sa-schema.cypher`: | Node | Key Properties | Description | |------|---------------|-------------| | `Module` | id, name, description, uc_range_start, uc_range_end | Functional module (Bounded Context) | | `Requirement` | id, description, type, priority | Functional or non-functional requirement | Key SA relationships used by this skill: - `(:Module)-[:CONTAINS_UC]->(:UseCase)` --- module owns a use case - `(:Module)-[:CONTAINS_ENTITY]->(:DomainEntity)` --- module owns a domain entity - `(:Module)-[:DEPENDS_ON {type, description}]->(:Module)` --- inter-module dependency - `(:ProcessGroup)-[:SUGGESTS]->(:Module)` --- BA-to-SA handoff edge --- ## Режимы работы ### Режим `full` (по умолчанию) Полная декомпозиция системы с нуля: 5 фаз интерактивного диалога. **Когда:** Новый проект, Module узлы ещё не созданы в графе. ### Режим `module` Добавление одного модуля в существующую архитектуру. **Когда:** Модули уже существуют в графе, нужно расширить. **Параметр:** `module_name` --- имя нового модуля (snake_case). --- ## Workflow ``` +----------+ +----------+ +----------+ +----------+ +-----------+ +----------+ | Phase 0 | | Phase 1 | | Phase 2 | | Phase 3 | | Phase 3.5 | | Phase 4 | | BA Ctx |--->| Бизнес- |--->| Модули |--->| Context |--->| External |--->| NFR и | | Import | | контекст | | (в граф) | | Map | | Contracts | | constr. | | (граф) | | | | | | (в граф) | | (артефакт | | (граф) | | | | | | | | | | + граф) | | | +----------+ +----------+ +----------+ +----------+ +-----------+ +----------+ ``` Каждая фаза завершается: 1. **Резюме** --- что понято 2. **Подтверждение** --- запрос верификации у пользователя 3. **Артефакт** --- создание/обновление узлов и рёбер в Neo4j графе **Не переходи к следующей фазе без явного подтверждения пользователя!** --- ## Предварительная проверка ### Режим `full` 1. Проверь наличие Module узлов в графе: ```cypher // mcp__neo4j__read-cypher MATCH (m:Module) RETURN count(m) AS module_count ``` Если `module_count > 0` --- предупреди о возможной перезаписи. 2. Проверь наличие BA-данных в графе: ```cypher // mcp__neo4j__read-cypher MATCH (gpr:ProcessGroup) RETURN count(gpr) AS pg_count ``` Если `pg_count = 0` --- предупреди, что BA-подграф пуст; Phase 0 будет пропущена. ### Режим `module` 1. Загрузи существующие модули: ```cypher // mcp__neo4j__read-cypher MATCH (m:Module) RETURN m.id AS id, m.name AS name, m.uc_range_start AS uc_start, m.uc_range_end AS uc_end ORDER BY m.uc_range_start ``` Если модулей нет --- предложи `/nacl-sa-architect` в режиме `full`. 2. Определи занятые имена и диапазоны UC. 3. Определи свободный диапазон UC для нового модуля. --- ## Phase 0: Импорт BA-контекста из графа **Цель:** Прочитать BA-подграф и извлечь контекст для архитектурного проектирования. ### Шаг 0.1: Загрузить все ProcessGroup и их BusinessProcess ```cypher // mcp__neo4j__read-cypher MATCH (gpr:ProcessGroup)-[:CONTAINS]->(bp:BusinessProcess) RETURN gpr.id AS gpr_id, gpr.name AS gpr_name, collect({id: bp.id, name: bp.name, description: bp.description}) AS processes ``` ### Шаг 0.2: Загрузить все BusinessEntity (бизнес-объекты) ```cypher // mcp__neo4j__read-cypher MATCH (be:BusinessEntity) OPTIONAL MATCH (be)-[:HAS_ATTRIBUTE]->(a:EntityAttribute) RETURN be.id AS id, be.name AS name, be.type AS type, be.description AS description, collect({id: a.id, name: a.name, data_type: a.data_type}) AS attributes ``` ### Шаг 0.3: Загрузить automation scope (шаги для автоматизации) ```cypher // mcp__neo4j__read-cypher MATCH (bp:BusinessProcess)-[:HAS_STEP]->(ws:WorkflowStep {stereotype: "Автоматизируется"}) OPTIONAL MATCH (ws)-[:PERFORMED_BY]->(r:BusinessRole) RETURN bp.id AS bp_id, bp.name AS bp_name, ws.id AS ws_id, ws.function_name AS ws_function, r.full_name AS role_name ORDER BY bp.id, ws.step_number ``` ### Шаг 0.4: Загрузить существующие предложения по модулям (если есть) ```cypher // mcp__neo4j__read-cypher MATCH (gpr:ProcessGroup)-[:SUGGESTS]->(m:Module) RETURN gpr.id AS gpr_id, gpr.name AS gpr_name, m.id AS module_id, m.name AS module_name ``` ### Шаг 0.5: Загрузить бизнес-роли ```cypher // mcp__neo4j__read-cypher MATCH (r:BusinessRole) OPTIONAL MATCH (r)-[:OWNS]->(owned:BusinessProcess) OPTIONAL MATCH (r)-[:PARTICIPATES_IN]->(part:BusinessProcess) RETURN r.id AS id, r.full_name AS name, collect(DISTINCT owned.name) AS owns_processes, collect(DISTINCT part.name) AS participates_in ``` ### Шаг 0.6: Загрузить бизнес-правила ```cypher // mcp__neo4j__read-cypher MATCH (brq:BusinessRule) OPTIONAL MATCH (brq)-[:CONSTRAINS]->(be:BusinessEntity) OPTIONAL MATCH (brq)-[:APPLIES_IN]->(bp:BusinessProcess) RETURN brq.id AS id, brq.name AS name, brq.description AS description, collect(DISTINCT be.name) AS constrains_entities, collect(DISTINCT bp.name) AS applies_in_processes ``` ### Вывод Phase 0 Покажи пользователю сводку: ``` **Phase 0: Импорт BA-контекста из графа** BA-подграф загружен: - Группы процессов: {N} (содержат {M} бизнес-процессов) - Бизнес-объекты: {N} - Шаги для автоматизации: {N} (из {M} бизнес-процессов) - Бизнес-роли: {N} - Бизнес-правила: {N} - Предложения по модулям: {N} (из SUGGESTS-рёбер) Эти данные будут использованы как основа для проектирования. Переходим к Phase 1 для верификации и уточнения. ``` ### Если BA-подграф пуст Пропусти Phase 0, перейди к Phase 1. Работай в стандартном режиме --- собирай всю информацию от пользователя. --- ## Phase 1: Бизнес-контекст **Цель:** Верифицировать и уточнить бизнес-контекст на основе BA-данных из графа. ### Если BA-данные загружены (Phase 0 выполнена) Предложи пользователю верификацию, а не задавай вопросы с нуля: ``` На основе BA-подграфа я вижу: **Бизнес-процессы:** {список процессов по группам} **Scope автоматизации:** {N} шагов подлежат автоматизации **Ключевые объекты:** {список бизнес-объектов} **Роли:** {список ролей} Вопросы для уточнения: 1. Все ли процессы должны быть покрыты системой? 2. Есть ли внешние системы для интеграции, не отражённые в графе? 3. Есть ли ограничения scope, которые нужно учесть? ``` ### Если BA-данных нет Задавай вопросы как в стандартном sa-architect: 1. Какую бизнес-задачу решает система? 2. Кто целевые пользователи? 3. Какие основные функциональные области? 4. Что НЕ входит в scope? 5. Есть ли внешние системы для интеграции? ### Действия после получения ответов 1. Сформулируй бизнес-цели (2--3 пункта) 2. Определи success criteria 3. Опиши scope (что входит, что не входит) 4. Опиши целевых пользователей на основе BusinessRole из графа ### Артефакт В отличие от sa-architect, **НЕ создавай markdown-файлы** в `docs/`. Данные Phase 1 хранятся в памяти диалога и используются в последующих фазах для создания графовых узлов. ### Переход После подтверждения пользователем -> Phase 2 --- ## Phase 2: Модульная декомпозиция **Цель:** Разбить систему на 3--8 функциональных модулей (Bounded Contexts) и записать их в граф. ### Принципы декомпозиции 1. **Single Responsibility:** Каждый модуль решает одну бизнес-задачу 2. **High Cohesion:** Сущности и UC внутри модуля тесно связаны 3. **Low Coupling:** Минимум зависимостей между модулями 4. **Testable Boundary:** Модуль можно описать 1--2 предложениями без "и/или" 5. **Balanced Size:** Каждый модуль содержит 3--15 UC 6. **BA Alignment:** Модули должны коррелировать с ProcessGroup из BA-подграфа ### Построение предложения Если в Phase 0 загружены SUGGESTS-рёбра --- используй их как стартовую точку. Иначе --- используй ProcessGroup как основу для группировки: ``` На основе BA-подграфа я предлагаю разбить систему на следующие модули: 1. **{Название}** (mod-{code}) --- {назначение} - Источник: ProcessGroup "{gpr_name}" - UC range: UC100-UC199 - Процессы: {список BP из этой группы} - Бизнес-объекты: {список BE, связанных с процессами} 2. **{Название}** (mod-{code}) --- {назначение} ... Вопросы: 1. Согласны с такой структурой модулей? 2. Нужно ли добавить/убрать/объединить модули? 3. Есть ли общесистемные функции (авторизация, настройки)? - Если да, они идут в модуль mod-common (UC001-UC099) ``` ### Правила декомпозиции - Система = 3--8 модулей - Если модуль содержит > 15 UC --- разделить - Если модуль содержит < 3 UC --- объединить с другим - Каждый модуль = один Bounded Context - UC001--UC099 зарезервированы для общесистемных функций (mod-common) - Каждый модуль получает блок из 100 номеров ### Артефакт: Создание Module узлов в графе После подтверждения пользователем --- создай Module узлы. #### Шаг 2.1: Генерация ID для модулей Формат ID: `mod-{code}` (код в snake_case, напр. `mod-orders`, `mod-catalog`). #### Шаг 2.2: Создание каждого Module узла Для каждого модуля выполни: ```cypher // mcp__neo4j__write-cypher MERGE (m:Module {id: $id}) SET m.name = $name, m.description = $description, m.uc_range_start = $uc_range_start, m.uc_range_end = $uc_range_end, m.status = 'draft', m.created = datetime() ``` Параметры: - `$id` --- например `"mod-orders"` - `$name` --- человекочитаемое название, например `"Управление заказами"` - `$description` --- 1--2 предложения о назначении - `$uc_range_start` --- начало диапазона UC (int), например `100` - `$uc_range_end` --- конец диапазона UC (int), например `199` #### Шаг 2.3: Создание SUGGESTS-рёбер (если есть ProcessGroup-источник) Для каждого модуля, который соответствует ProcessGroup: ```cypher // mcp__neo4j__write-cypher MATCH (gpr:ProcessGroup {id: $gpr_id}) MATCH (m:Module {id: $module_id}) MERGE (gpr)-[:SUGGESTS]->(m) ``` #### Шаг 2.4: Верификация созданных модулей ```cypher // mcp__neo4j__read-cypher MATCH (m:Module) OPTIONAL MATCH (gpr:ProcessGroup)-[:SUGGESTS]->(m) RETURN m.id AS id, m.name AS name, m.description AS description, m.uc_range_start AS uc_start, m.uc_range_end AS uc_end, collect(gpr.name) AS source_process_groups ORDER BY m.uc_range_start ``` Покажи пользователю таблицу: ``` **Phase 2: Модули записаны в граф** | Модуль | ID | UC Range | Источник (ProcessGroup) | |--------|----|----------|-------------------------| | {name} | {id} | UC{start}-UC{end} | {gpr_names} | | ... | ... | ... | ... | Всего модулей: {N} ``` ### Переход После подтверждения модульной декомпозиции -> Phase 3 --- ## Phase 3: Context Map **Цель:** Определить межмодульные зависимости и записать их в граф. ### Действия На основе BA-данных из Phase 0 и модулей из Phase 2: 1. **Определи направления зависимостей:** - Какой модуль от какого зависит? - Кто владеет данными (CRUD)? - Кто только читает данные? 2. **Определи типы связей:** - `data_read` --- модуль читает справочники/сущности другого модуля - `operation_call` --- модуль инициирует бизнес-процесс в другом модуле - `event` --- модуль реагирует на изменения в другом модуле 3. **Определи общие сущности:** - Какие BusinessEntity из графа используются несколькими ProcessGroup? - Определи владельца и читателей Для анализа общих сущностей выполни: ```cypher // mcp__neo4j__read-cypher MATCH (ws:WorkflowStep)-[:READS|PRODUCES|MODIFIES]->(be:BusinessEntity) MATCH (bp:BusinessProcess)-[:HAS_STEP]->(ws) MATCH (gpr:ProcessGroup)-[:CONTAINS]->(bp) RETURN be.id AS entity_id, be.name AS entity_name, collect(DISTINCT {gpr_id: gpr.id, gpr_name: gpr.name, rel_type: type((ws)-[:READS|PRODUCES|MODIFIES]->(be))}) AS used_by_groups ``` ### Вопросы для пользователя ``` Я построил карту зависимостей между модулями: {Текстовая таблица зависимостей} Вопросы: 1. Правильно ли я определил направления зависимостей? 2. Есть ли зависимости, которые я пропустил? 3. Правильно ли определены владельцы общих сущностей? ``` ### Артефакт: Создание межмодульных рёбер в графе #### Шаг 3.1: Создание DEPENDS_ON рёбер между модулями Для каждой зависимости: ```cypher // mcp__neo4j__write-cypher MATCH (m1:Module {id: $source_module_id}) MATCH (m2:Module {id: $target_module_id}) MERGE (m1)-[r:DEPENDS_ON]->(m2) SET r.type = $dep_type, r.description = $description ``` Параметры: - `$source_module_id` --- модуль-потребитель - `$target_module_id` --- модуль-поставщик - `$dep_type` --- `"data_read"`, `"operation_call"`, или `"event"` - `$description` --- что передаётся (напр. `"Читает данные клиентов"`) #### Шаг 3.2: Предварительное распределение BusinessEntity по модулям На основе анализа владения --- создай предварительные CONTAINS_ENTITY-рёбра: ```cypher // mcp__neo4j__write-cypher MATCH (m:Module {id: $module_id}) MERGE (de:DomainEntity {id: $entity_id}) SET de.name = $entity_name, de.module = $module_id, de.status = 'draft', de.created = datetime() MERGE (m)-[:CONTAINS_ENTITY]->(de) ``` Также создай BA-to-SA handoff-ребро: ```cypher // mcp__neo4j__write-cypher MATCH (be:BusinessEntity {id: $ba_entity_id}) MATCH (de:DomainEntity {id: $sa_entity_id}) MERGE (be)-[:REALIZED_AS]->(de) ``` #### Шаг 3.3: Верификация Context Map ```cypher // mcp__neo4j__read-cypher MATCH (m1:Module)-[r:DEPENDS_ON]->(m2:Module) RETURN m1.name AS source, m2.name AS target, r.type AS dep_type, r.description AS description ORDER BY m1.name, m2.name ``` ```cypher // mcp__neo4j__read-cypher MATCH (m:Module)-[:CONTAINS_ENTITY]->(de:DomainEntity) RETURN m.name AS module, collect({id: de.id, name: de.name}) AS entities ORDER BY m.name ``` Покажи пользователю результат: ``` **Phase 3: Context Map записан в граф** Зависимости: | Источник | Приёмник | Тип | Описание | |----------|----------|-----|----------| | {source} | {target} | {type} | {desc} | Распределение сущностей: | Модуль | Сущности | |--------|----------| | {module} | {entity_list} | ``` ### Переход После подтверждения Context Map -> Phase 3.5 (External Contracts) --- ## Phase 3.5: External Contracts **Цель:** Зафиксировать каждый внешний провайдер и каждый wire-протокол как отдельный артефакт-контракт, который читает граф SA, и который консумируют downstream-скилы (`nacl-tl-plan`, `nacl-tl-sync` wire-evidence gate, `nacl-tl-dev-be`, `nacl-tl-dev-fe`, `nacl-tl-qa`). **Why this phase exists.** 13 из ~60 сигналов в постмортемах двух проектов — это сбои внешних API / wire-протоколов: kie.ai (оба проекта), TUS upload, обратный прокси https-схема, ffmpeg/ffprobe runtime, SSE frame envelope. Локально всё компилировалось, типы совпадали, но реальный wire не работал. См. `docs/retrospectives/project-beta-runtime-baseline.md` §§ A1–A9, B1–B7 и сводку §I "Provider/external-API contracts" и "Wire-envelope protocols". Цель этой фазы — превратить эту негативную плоскость в позитивный артефакт. ### Артефакт Один Markdown-файл на провайдер ИЛИ на протокол: ``` .tl/external-contracts/.md ``` Примеры файлов (этот скил их создаёт по результатам диалога с пользователем): | Тип | Slug | Описание | |---|---|---| | provider | `kie.md` | kie.ai — LLM endpoint (Anthropic-shape), async polling lifecycle, model namespace без `google/`-префикса. См. baseline § A1–A3, A5, A7–A9. | | provider | `deepgram.md` | Deepgram (ASR) — sync provider; API key обязателен; pre-provider стадии (storage fetch, ffmpeg extract) НЕ требуют ключа (см. baseline § A4, F2). | | provider | `anthropic.md` | Anthropic Claude (прямой вызов) — отличный envelope от kie.ai. | | protocol | `tus.md` | TUS upload — `Location` header public-origin; Caddy `respectForwardedHeaders` + `X-Forwarded-Proto`; Fastify `addContentTypeParser` для `application/offset+octet-stream` (см. baseline § A6, B1, B2, B4). | | protocol | `sse.md` | Server-Sent Events — frame envelope `event: \ndata: \n\n`; без `event:`-строки браузер дефолтит на `'message'` (см. baseline § B3). | | protocol | `multipart-presigned.md` | Прямой S3 presigned multipart upload (когда TUS заменён — см. baseline § B5). | | protocol | `reverse-proxy-url-scheme.md` | Общие правила https-public-URL за прокси (cross-cutting между TUS, presigned, и любыми Location-headers). | | protocol | `ffmpeg-ffprobe-runtime.md` | ffmpeg/ffprobe runtime — seekable stdin для MP4 demux (см. baseline § C5); ffprobe не принимает `s3://`-URI (§ B6). | Шаблон с обязательными и опциональными полями: `.tl/external-contracts/_template.md` (создаётся в W6 как сам артефакт скила). ### Required fields (для каждого файла) | # | Поле | Описание | |---|---|---| | 1 | **Identity** | Имя, `kind: provider|protocol`, дата, ссылки на TECH-### / UC-### в графе. | | 2 | **Endpoint** | Полный URL включая version-path (НЕ только base host, если адаптер добавляет `/api/v1`); все вызываемые эндпоинты (method + path + назначение); механизм discovery (`static-catalog` или `http-list-endpoint`); версионирование. | | 3 | **Auth** | Scheme (`Bearer` / `x-api-key` / `none` / ...); имя переменной окружения с секретом; поведение при отсутствии секрета (требование к `nacl-tl-qa` stage decomposition — см. W3). | | 4 | **Request shape** | Content-Type; обязательные заголовки; тело с **литеральными** именами полей (не "соответствует TS-типу", а именно прописанные строки — это был тройной mismatch TUS `mime_type` vs `filetype`, baseline § B1); query params. | | 5 | **Response shape** | Success-status; тело с литеральными именами полей; точная цепочка accessor для извлечения load-bearing значения (kie.ai Anthropic envelope: `response.content[0].text`, НЕ `response.choices[0].message.content` — baseline § A1, A5); обязательные ответные заголовки (Location, Tus-Resumable, ...). | | 6 | **Lifecycle: sync vs async** | Явно `sync` или `async`. Если async — submit endpoint, poll endpoint, polling cadence (min/max/backoff), polling timeout (что surface'ит FAILED), cancellation. (См. baseline § A2 — kie.ai image_gen переехал sync → async mid-build.) | | 7 | **File URL reachability** | Когда контракт возвращает или потребляет URL: ожидаемая схема (`https://` для browser-facing; `s3://` отвергается ffprobe — baseline § B6); кто проставляет `X-Forwarded-Proto`; включён ли `respectForwardedHeaders`; public origin vs origin server; TTL; toolchain-совместимость. | | 8 | **Failure codes** | Перечисление HTTP-кодов с трактовкой и обязательным consumer action: `4xx auth`, `4xx model/endpoint`, `4xx envelope`, `429 rate-limit`, `5xx transient`. | | 9 | **Model namespace / catalog** (только для `kind: provider`) | Источник каталога (`static-list-in-this-file` или `http-list-endpoint`); политика namespace-префикса (`NONE` / `/` / `:` — БЫТЬ ТОЧНЫМ; baseline § A3 — `google/`-префикс к nano-banana дал 400); используемые модели verbatim. | | 10 | **Fixture-test path** | Repo-relative путь к runnable-тесту, который загружает записанный response fixture и парсит его через production code-path без моков парсера. Это именно то, что Wire-Evidence Gate (W2) ищет как `wire-evidence:fixture:`. | | 11 | **Smoke-test path** | Repo-relative путь к runnable smoke-тесту, который ходит в **реальный** sandbox/staging провайдер. Может требовать network и переменные окружения; обязан быть запускаемым по требованию `nacl-tl-qa` (см. W3 Stage Decomposition Gate). | ### Optional fields Включаются ТОЛЬКО когда интеграция использует соответствующую surface: webhook callback shape; stream / SSE frame envelope (если используется streaming); multi-tenant routing header; idempotency-key header; pagination shape; concurrency / per-key rate-limits; region pinning; vendor SDK version pin; framework-specific gotchas (Fastify, Caddy, ffmpeg) — см. полный список в `.tl/external-contracts/_template.md`. ### Worked example 1: kie.ai (provider) Этот пример иллюстрирует все обязательные поля для канонического provider'а с async-lifecycle: ``` File: .tl/external-contracts/kie.md Kind: provider Endpoint: Base URL: https://kie-ai.redpandaai.co All endpoints: POST /api/v1/jobs/createTask - async generation (image_gen) GET /api/v1/jobs/recordInfo - poll task status POST /api/v1/messages - Anthropic-shape LLM (sync) Discovery: static-catalog (this file) Versioning: path segment /api/v1; pinned to v1 as of 2026-05-19 Auth: Scheme: x-api-key Secret env var: KIE_API_KEY Missing-secret behavior: nacl-tl-qa decomposes pipeline; pre-provider stages run, provider stage marks PROVIDER_QA NOT_RUN; cannot be silently SKIP Request (Anthropic-shape): Content-Type: application/json Headers: x-api-key, anthropic-version: 2023-06-01 Body literal: { model, max_tokens, messages: [{role, content}] } Response (Anthropic-shape): Success status: 200 Body literal: { id, type, role, model, content: [{type, text}], ... } Parsing path: response.content[0].text (NOT response.choices[0].message.content) Lifecycle: Mode (LLM call): sync Mode (image_gen): async Submit: POST /api/v1/jobs/createTask -> { task_id } Poll: GET /api/v1/jobs/recordInfo?id -> { status, result } Cadence: start 2s; backoff x1.5; max 30s; cap 5min total Timeout: on 5min -> surface FAILED, not silent hang File URL reachability: N/A (no file URLs in/out) Failure codes: 401: bad x-api-key -> halt AUTH_FAILED 404: model namespace -> halt MODEL_NOT_FOUND (see "Model namespace") 400: bad envelope shape -> halt CONTRACT_FAILED 429: rate limit -> backoff 2s/4s/8s; max 3 retries 5xx: transient -> backoff; budget 5 retries; then surface Model namespace: Catalog: http-list at GET /api/v1/models Namespace prefix: NONE (pass model id verbatim; NO "google/" prefix) Models in use: claude-3-5-sonnet-20241022, nano-banana-v1, ... Fixture-test path: Fixture file: tests/fixtures/kie-ai/protocol-response.json Test file: tests/wire/kie-ai.fixture.test.ts Asserts: KieAiProtocolProvider.parse(fixture) extracts content[0].text without mocking the response shape. Run command: pnpm -F api test tests/wire/kie-ai.fixture.test.ts Smoke-test path: File: tests/smoke/kie-ai.smoke.test.ts Env vars: KIE_API_KEY (sandbox), KIE_BASE_URL Sandbox/prod: sandbox Run command: pnpm -F api smoke:kie Stage decomposition: PROVIDER_QA ``` ### Worked example 2: TUS upload (protocol) Этот пример иллюстрирует обязательные поля для канонического протокола с реверс-прокси-чувствительными URL-схемами: ``` File: .tl/external-contracts/tus.md Kind: protocol Endpoint: Base URL: https:///tus All endpoints: POST /tus -> 201 + Location: HEAD /tus/ -> 200 + Upload-Offset PATCH /tus/ -> 204; Content-Type: application/offset+octet-stream (MUST be registered with Fastify via fastify.addContentTypeParser; else 415 — § B2) Discovery: protocol spec https://tus.io/protocols/resumable-upload Versioning: Tus-Resumable: 1.0.0 header Auth: Scheme: Bearer (project JWT) on POST + PATCH Secret env var: (client-bearer; not a vendor secret) Missing-secret behavior: returns 401 before any storage I/O Request: Content-Type: application/offset+octet-stream (PATCH) Required headers: Upload-Length (POST) Upload-Metadata (POST) — canonical key: mime_type (NOT filetype — three-way naming collision was the project-beta TUS-415 episode, § B1) Upload-Offset (PATCH) Tus-Resumable: 1.0.0 (all) Response: Success status: 201 (POST), 204 (PATCH), 200 (HEAD) Required response headers: Location: (POST — see "File URL reachability") Tus-Resumable: 1.0.0 (all) Upload-Offset (PATCH, HEAD) Lifecycle: sync (chunked PATCH within a single resumable session) File URL reachability: Scheme expected by consumers: https:// (browser-facing) Reverse-proxy translation: Caddy sets X-Forwarded-Proto; Fastify MUST be configured with { trustProxy: true } AND TUS server MUST consume Forwarded headers when composing the Location header (project-beta episode § B4 fix `15c6a20`). Public origin vs origin server: Location returned to browser MUST be public origin. Returning origin-server http://api-internal:3000/... breaks the browser-side resume. Lifetime: upload session lifetime (no expiry on Location; expires only with the resource) Toolchain compat: N/A (browser is the consumer) Failure codes: 415: Unsupported Media Type -> Fastify rejected application/offset+ octet-stream; fix: addContentTypeParser (§ B2) 412: Precondition Failed -> Tus-Resumable mismatch 409: Conflict -> Upload-Offset mismatch; resume from server offset 410: Gone -> upload session reaped; retry from POST 413: Payload Too Large -> chunk above Upload-Length Model namespace: N/A (protocol, not provider) Fixture-test path: Fixture file: tests/fixtures/tus/post-create-response.txt tests/fixtures/tus/patch-resume-response.txt Test file: tests/wire/tus.fixture.test.ts Asserts: Browser-side TUS client constructs PATCH targeting Location header verbatim; mime_type metadata key appears literally in Upload-Metadata; Tus-Resumable header is 1.0.0 on every call. Smoke-test path: File: tests/smoke/tus-upload.smoke.test.ts Env vars: PUBLIC_BASE_URL (must be https://...) Sandbox/prod: staging Run command: pnpm -F api smoke:tus Stage decomposition: WIRE_CONTRACT_QA, STORAGE_QA ``` ### Граф: связи с SA-моделью После того, как файлы созданы и пользователь подтвердил, добавь граф-узлы и рёбра, чтобы downstream-скилы могли запрашивать список контрактов: ```cypher // mcp__neo4j__write-cypher — один узел на контракт-файл MERGE (ec:ExternalContract {id: $contract_id}) SET ec.name = $name, ec.kind = $kind, ec.file_path = $file_path, ec.status = 'draft', ec.created = datetime() ``` Параметры: - `$contract_id` — `ext-` (напр. `"ext-kie"`, `"ext-tus"`) - `$name` — `"kie.ai"`, `"TUS upload"` - `$kind` — `"provider"` или `"protocol"` - `$file_path` — `".tl/external-contracts/kie.md"` Привязка к модулям, которые используют контракт: ```cypher // mcp__neo4j__write-cypher MATCH (m:Module {id: $module_id}) MATCH (ec:ExternalContract {id: $contract_id}) MERGE (m)-[:DEPENDS_ON_EXTERNAL]->(ec) ``` Привязка к UseCase (если граф уже содержит UC), чтобы `nacl-tl-plan` мог за один Cypher проверить наличие контракта: ```cypher // mcp__neo4j__write-cypher MATCH (uc:UseCase {id: $uc_id}) MATCH (ec:ExternalContract {id: $contract_id}) MERGE (uc)-[:REQUIRES_EXTERNAL]->(ec) ``` ### Связь с W2 Wire-Evidence Gate Заполненный файл контракта удовлетворяет требованию W2 (`nacl-tl-sync` Wire-Evidence Gate) следующим образом: - Поле 10 (Fixture-test path) даёт `wire-evidence:fixture:`. - Поле 11 (Smoke-test path) даёт `wire-evidence:contract-test:` ИЛИ `wire-evidence:live-smoke:` (когда smoke реально выполняется). - Без заполненного контракта `nacl-tl-sync` не имеет конкретного артефакта, на который ссылаться, и downgrade'ит UC до `Status: UNVERIFIED workflow-detail wire-evidence-missing`. Это поведение задано в W2; W6 даёт ему таргет. ### Связь с W3 QA Stage Decomposition Gate Поля 3 (Auth: Missing-secret behavior) и 11 (Smoke-test path: Stage decomposition) задают `nacl-tl-qa` структуру стадий, которые SKIP-on-missing-keys (как в project-beta UC-200/UC-300) НЕ должен полностью обнулять. Только провайдер-стадия маркируется `NOT_RUN`; предшествующие стадии (storage fetch, ffmpeg extract, queue transition) — VERIFIED. ### Override Единственный способ обойти отсутствующий контракт — signed exception под схемой W4. Inline-флага типа `--skip-external-contract` нет и не будет. ### Чеклист Phase 3.5 - [ ] Для каждого внешнего провайдера в системе создан `.tl/external-contracts/.md` (kind: provider). - [ ] Для каждого wire-протокола за пределами TS-типов создан `.tl/external-contracts/.md` (kind: protocol). - [ ] Обязательные поля 1–8 и 10–11 заполнены не-stub значениями для каждого файла. - [ ] Поле 9 (Model namespace) заполнено для всех provider-файлов. - [ ] Поле 7 (File URL reachability) заполнено или явно отмечено "N/A". - [ ] `ExternalContract` узлы созданы в графе и привязаны к `Module`/`UseCase`. - [ ] Fixture-test и smoke-test пути ссылаются на существующие файлы ИЛИ accompanied a signed exception. - [ ] Пользователь подтвердил каждый контракт. ### Переход После подтверждения External Contracts -> Phase 4 --- ## Phase 4: NFR и технические ограничения **Цель:** Зафиксировать нефункциональные требования как Requirement узлы в графе. ### Вопросы для пользователя **Производительность:** 1. Сколько пользователей одновременно? 2. Требования к времени отклика? 3. Какие объёмы данных ожидаются? **Интеграции:** 4. Нужна ли интеграция с внешними системами? (уточнить Phase 1) 5. Требования к API? 6. Импорт/экспорт? **Безопасность:** 7. Требования к безопасности? 8. Многофакторная аутентификация? 9. Шифрование данных? **Инфраструктура:** 10. Веб / мобильное / десктоп? 11. Предпочтения по технологиям? 12. Резервное копирование? ### Действия 1. Зафиксируй ответы 2. Предложи разумные значения по умолчанию для незаданных параметров 3. Классифицируй каждое NFR по категории ### Артефакт: Создание Requirement узлов в графе #### Шаг 4.1: Генерация ID для NFR Формат ID: `NFR-NNN` (глобальный последовательный счётчик). Получи следующий свободный ID: ```cypher // mcp__neo4j__read-cypher MATCH (r:Requirement) WHERE r.id STARTS WITH 'NFR-' WITH max(toInteger(replace(r.id, 'NFR-', ''))) AS maxNum RETURN 'NFR-' + apoc.text.lpad(toString(coalesce(maxNum, 0) + 1), 3, '0') AS nextId ``` #### Шаг 4.2: Создание Requirement узлов Для каждого NFR: ```cypher // mcp__neo4j__write-cypher MERGE (r:Requirement {id: $id}) SET r.description = $description, r.type = 'nfr', r.category = $category, r.priority = $priority, r.metric = $metric, r.target_value = $target_value, r.status = 'draft', r.created = datetime() ``` Параметры: - `$id` --- например `"NFR-001"` - `$description` --- текст требования, напр. `"Время отклика < 2 сек для 95-го перцентиля"` - `$type` --- всегда `"nfr"` - `$category` --- `"performance"`, `"security"`, `"integration"`, `"infrastructure"` - `$priority` --- `"high"`, `"medium"`, `"low"` - `$metric` --- метрика измерения (если применимо) - `$target_value` --- целевое значение (если применимо) #### Шаг 4.3: Привязка NFR к модулям (если NFR специфичен для модуля) ```cypher // mcp__neo4j__write-cypher MATCH (m:Module {id: $module_id}) MATCH (r:Requirement {id: $nfr_id}) MERGE (m)-[:HAS_REQUIREMENT]->(r) ``` Если NFR системный (применим ко всем модулям) --- не создавай привязку к конкретному модулю. **NFR освобождён от якоря `REALIZED_BY` by design.** Функциональные/валидационные/ поведенческие/интерфейсные требования (их пишет `nacl-sa-uc`) обязаны быть привязаны к реализующему их шагу/полю/форме (валидатор L3.7, CRITICAL). NFR — нет: `type='nfr'` делает требование «свободно висящим» легитимно (производительность, ёмкость, безопасность — у них нет одного конкретного шага). Валидатор L3.7 пропускает любое требование с `type ∈ {nfr, adr, question}`. Не создавай `REALIZED_BY` от NFR; привязки к модулю (4.3) достаточно. #### Шаг 4.4: Верификация NFR ```cypher // mcp__neo4j__read-cypher MATCH (r:Requirement {type: 'nfr'}) OPTIONAL MATCH (m:Module)-[:HAS_REQUIREMENT]->(r) RETURN r.id AS id, r.description AS description, r.category AS category, r.priority AS priority, r.metric AS metric, r.target_value AS target_value, collect(m.name) AS applies_to_modules ORDER BY r.category, r.id ``` Покажи пользователю результат: ``` **Phase 4: NFR записаны в граф** | ID | Категория | Описание | Приоритет | Метрика | Модули | |----|-----------|----------|-----------|---------|--------| | {id} | {category} | {desc} | {priority} | {metric} | {modules} | Всего NFR: {N} ``` --- ## Завершение ### Режим `full` После Phase 4: 1. **Финальная верификация графа:** ```cypher // mcp__neo4j__read-cypher MATCH (m:Module) OPTIONAL MATCH (m)-[:CONTAINS_ENTITY]->(de:DomainEntity) OPTIONAL MATCH (m)-[:DEPENDS_ON]->(dep:Module) OPTIONAL MATCH (m)-[:HAS_REQUIREMENT]->(r:Requirement {type: 'nfr'}) RETURN m.id AS id, m.name AS name, m.uc_range_start AS uc_start, m.uc_range_end AS uc_end, count(DISTINCT de) AS entity_count, count(DISTINCT dep) AS dependency_count, count(DISTINCT r) AS nfr_count ORDER BY m.uc_range_start ``` 2. **Проверка handoff-покрытия:** ```cypher // mcp__neo4j__read-cypher MATCH (gpr:ProcessGroup) OPTIONAL MATCH (gpr)-[:SUGGESTS]->(m:Module) RETURN gpr.id AS gpr_id, gpr.name AS gpr_name, CASE WHEN m IS NULL THEN 'NO MODULE' ELSE m.name END AS module_name ``` 3. **Покажи итоговую сводку:** ``` **Архитектурная декомпозиция завершена (в графе)** | Модуль | ID | UC Range | Сущностей | Зависимостей | NFR | |--------|----|----------|-----------|--------------|-----| | {name} | {id} | UC{start}-UC{end} | {N} | {N} | {N} | BA -> SA покрытие: | ProcessGroup | Module | |---|---| | {gpr_name} | {module_name} | NFR: {N} требований ({по категориям}) Следующие шаги: 1. `/nacl-sa-domain` --- создать Domain Model для каждого модуля 2. `/nacl-sa-roles` --- определить роли и матрицу прав 3. `/nacl-sa-uc` --- создать Use Cases для модулей ``` ### Режим `module` После создания нового модуля: 1. Создай Module узел (Шаг 2.2) 2. Создай SUGGESTS-ребро при наличии ProcessGroup (Шаг 2.3) 3. Создай DEPENDS_ON рёбра к существующим модулям (Шаг 3.1) 4. Опционально --- предварительные CONTAINS_ENTITY (Шаг 3.2) 5. Покажи обновлённую сводку всех модулей ``` Модуль "{module_name}" добавлен в граф. Следующие шаги: 1. `/nacl-sa-domain` --- создать Domain Model для модуля {module_name} 2. `/nacl-sa-uc` --- создать Use Cases для модуля {module_name} ``` --- ## Работа с неполными ответами Если пользователь не может ответить на все вопросы: 1. Зафиксируй то, что известно 2. Предложи разумные допущения с обоснованием: ``` Допущение: Система будет веб-приложением с не более чем 100 одновременными пользователями. Обоснование: Типичный масштаб для B2B приложений данного типа. ``` 3. Создай Requirement узел для допущения со статусом `assumption`: ```cypher // mcp__neo4j__write-cypher MERGE (r:Requirement {id: $id}) SET r.description = $description, r.type = 'assumption', r.status = 'needs_review', r.rationale = $rationale, r.created = datetime() ``` 4. Продолжи работу с учётом допущений --- ## Чеклист /nacl-sa-architect Перед завершением работы проверь: ### Phase 0: BA-контекст - [ ] ProcessGroup и BusinessProcess загружены из графа - [ ] BusinessEntity загружены - [ ] Automation scope (WorkflowStep) загружен - [ ] SUGGESTS-рёбра (если есть) загружены - [ ] BusinessRole загружены - [ ] BusinessRule загружены ### Phase 1: Бизнес-контекст - [ ] Бизнес-цели сформулированы - [ ] Success criteria определены - [ ] Scope определён (что входит, что НЕ входит) - [ ] Пользователь подтвердил ### Phase 2: Модульная декомпозиция - [ ] Module узлы созданы в графе (`MERGE`) - [ ] Модулей от 3 до 8 - [ ] Диапазоны UC зарезервированы и не пересекаются - [ ] SUGGESTS-рёбра от ProcessGroup созданы - [ ] Верификация: все модули читаются из графа ### Phase 3: Context Map - [ ] DEPENDS_ON рёбра между модулями созданы - [ ] Типы зависимостей указаны (data_read / operation_call / event) - [ ] Предварительные DomainEntity узлы созданы - [ ] CONTAINS_ENTITY рёбра созданы - [ ] REALIZED_AS рёбра (BA -> SA) созданы - [ ] Верификация: Context Map читается из графа ### Phase 3.5: External Contracts - [ ] Для каждого provider создан `.tl/external-contracts/.md` - [ ] Для каждого wire-протокола создан `.tl/external-contracts/.md` - [ ] Обязательные поля 1–8, 10, 11 заполнены не-stub значениями - [ ] Поле 9 (Model namespace) заполнено для всех provider-файлов - [ ] Поле 7 (File URL reachability) заполнено или явно "N/A" - [ ] `ExternalContract` узлы созданы и привязаны к `Module`/`UseCase` рёбрами `DEPENDS_ON_EXTERNAL` / `REQUIRES_EXTERNAL` - [ ] Worked examples (kie.ai, TUS) использованы как референсные шаблоны ### Phase 4: NFR - [ ] Requirement узлы (type: 'nfr') созданы - [ ] Категории: performance, security, integration, infrastructure - [ ] Приоритеты расставлены - [ ] Привязка к модулям (если специфичны) - [ ] Верификация: NFR читаются из графа ### Общее - [ ] Пользователь подтвердил каждую фазу - [ ] Допущения задокументированы как Requirement (type: 'assumption') - [ ] Финальная сводка показана