archspec — шпаргалка по прогону Sonnet 4.6
Репозитории: archspec · bench · freelance-marketplace
Где что лежит
| Что | Где |
|---|---|
| База: SERVICE_MAP для 12 сервисов | ветка task_2, 4 мая 2026 |
| Прогон Sonnet 4.6 | ветка task_3, 11 июня 2026 |
| Ревью и скриншоты прогона | bench: runs/archspec/sonnet-4.6 |
Скриншоты /archspec:init | bench: runs/archspec/setup |
task_4_with_sonnet_4_6 — это OpenSpec, не archspec.
Пайплайн
| Команда | Что пишет |
|---|---|
/archspec:init | SERVICE_MAP.yaml, диаграммы, ARCHITECTURE.md, .servicemap/schema.json |
/archspec:investigate | только *.archplan.md |
/archspec:implement | правки SERVICE_MAP.yaml → *.codingplan.md → код |
/archspec:sync | перегенерирует диаграммы и ARCHITECTURE.md из YAML |
Скиллы: sync / init · investigate · implement
1. init → SERVICE_MAP
- Сканер находит в Go-коде endpoints, зависимости, хранилища, топики.
- Оператора спрашивают о том, чего в коде нет: имя и владелец, responsibilities, invariants, идемпотентность, SLA.
- Коммиты: task-service · config-service · остальные 10
- Пример: task-service/docs/SERVICE_MAP.yaml
YAML и JSON. YAML — сам контракт, его правят руками. .servicemap/schema.json — копия JSON Schema для подсказок в IDE; валидация идёт по схеме внутри плагина.
2. investigate → archplan
Четыре вопроса модели (скриншоты, 144943–145320):
| Вопрос | Ответ |
|---|---|
| Есть эталонная спека? | skip |
| Откуда приходит отказ от оффера? | новый HTTP endpoint в api-gateway |
| Что значит «максимум 3 переназначения»? | 3 отказа после первого оффера |
| Как считать гео-расстояние? | city_id у воркера и у задачи |
Второй раунд — Open Questions в плане (160905, 161901): worker_id из JWT, city_id обязателен, ноль кандидатов → match.exhausted.
Оба файла плана закоммичены только последним коммитом ветки, истории правок по коммитам нет.
3. implement → контракты, coding plan, код
Восемь фаз, буквы — из SKILL.md. Идут строго по порядку.
| Фаза | Название | Что происходит | Коммиты и хуки |
|---|---|---|---|
| A | Найти archplan | читает план целиком; без плана отказывается работать; блокирующие открытые вопросы задаёт сейчас | коммитов нет |
| B | Сначала контракты | применяет YAML-патч к SERVICE_MAP.yaml каждого сервиса → валидирует по схеме → /archspec:sync перегенерирует диаграммы → отдельный коммит, до любого кода | коммит docs(archspec): contract edits… → срабатывает pre-commit hook |
| C | Coding plan | пишет codingplan.md с таблицей соответствия: элемент archplan → задача → тест; проверяет grep-ом, что вызываемые методы реально существуют | сохраняет файл плана |
| D | Реализация | пишет код по задачам, тест вперёд; заглушки edge cases становятся настоящими тестами | частые коммиты, на каждом pre-commit hook |
| E | Проходы соответствия | пять ручных проверок диффа, см. ниже | коммитов нет, только отчёт |
| F | Проверки archspec | /archspec:validate и /archspec:check-architecture; чинит BLOCK и перезапускает до чистого отчёта | команды запускает модель; исправления коммитятся |
| G | Независимое ревью | ревьюер со свежим контекстом повторяет проходы фазы E; цикл, пока есть CRITICAL | исправления коммитятся |
| H | Завершение | коммитит остаток, не пушит; закрывает Definition of Done с доказательствами | финальные коммиты; push только по просьбе → pre-push hook |
Пять проходов фазы E — каждый ловит класс багов, который проходит при зелёных тестах:
- Wiring — в
main.goнетnilвместо зависимостей, адреса клиентов совпадают с портами сервисов. - Emission — у каждого объявленного события есть место публикации на всех путях.
- Threading — новое поле доходит от публичного входа до всех потребителей и сидов.
- Dedup — ключ дедупликации ставится не раньше побочных эффектов; повторная попытка не глотается как дубликат.
- Evidence — каждое требование задачи →
file:line→ тест.
Артефакты прогона Sonnet:
- коммит контрактов — первый в прогоне
- coding plan
- заметки по реализации
Edge cases: когда и как
- investigate, шаг 8a. Каждый риск становится записью
edge_cases[]в YAML-патче archplan. Имя теста придумывается сразу. - implement, фаза B «Сначала контракты». Патч попадает в
SERVICE_MAP.yamlвместе с тестами-заглушками. - implement, фаза D «Реализация». Заглушки наполняются по TDD.
Запись — ровно три обязательных поля:
edge_cases:
- id: EC-002
description: "DeclineOffer with wrong worker_id must return PERMISSION_DENIED"
test: "services/task-service/usecase/task_decline_test.go::TestEC002_WrongWorkerDeclines"
Заглушка из первого коммита:
func TestEC002_WrongWorkerDeclines(t *testing.T) {
t.Skip("not yet implemented")
}
| Правило | Что проверяет | Когда срабатывает |
|---|---|---|
| DET-003 | файл из test: существует, иначе коммит заблокирован | git commit; впервые — на коммите контрактов в фазе B implement |
| DET-007 | удалить edge case нельзя без ADR | git commit, когда меняется SERVICE_MAP.yaml |
| AI-005 | тест реально проверяет кейс — ещё не реализовано | задумано для /archspec:validate (фаза F implement) |
Заглушка с t.Skip проходит все автоматические проверки. От забытых заглушек защищают только Definition of Done и независимое ревью (фаза G). Подробнее: VALIDATION_RULES.md.
Edge cases в task_3: task-service · matching-service · notification-service. В task_2 их нет: init секцию не создаёт.
Что ещё записывается в SERVICE_MAP
Edge cases — лишь одна из секций. Ниже реальные записи из прогона Sonnet (ветка task_3) и из спецификации. Полный формат: SERVICE_MAP_SPEC.md.
Где лежат edge cases прогона
| Сервис | Записи | Ссылка |
|---|---|---|
| task-service | EC-001…004 | SERVICE_MAP.yaml:121 |
| matching-service | EC-005…009 | SERVICE_MAP.yaml:113 |
| notification-service | EC-010, 014, 015 | SERVICE_MAP.yaml:75 |
В контрактах 12 записей из 15: EC-011, EC-012 и EC-013 есть в archplan и в тестах, но в YAML не попали.
Инварианты сервиса — service.invariants
Что всегда верно внутри одного сервиса. Свободный текст, автоматикой не проверяется. Пример: task-service:25.
invariants:
- "every write goes through the outbox"
- "task can only be declined by its currently assigned worker (AssignedWorkerID must match)"
- "offer.declined is not emitted when reassignment_count >= 3; task.failed is emitted instead"
- "task.Status transitions: open → assigned (via match.found), assigned → open (via offer.declined), assigned/open → failed"
Инварианты между сервисами — consistency.cross_service_invariants
Что должно сходиться на стыке сервисов. Пример: matching-service:108.
cross_service_invariants:
- "every task.created eventually produces exactly one match.found OR one match.exhausted (never both, never neither)"
- "offer.declined with attempt N produces exactly one match.found (attempt N+1) or one match.exhausted, never both"
- "match.found for attempt > 0 carries client_id so notification-service can notify the client"
Идемпотентность endpoint’а — api.endpoints[].idempotency
Проверяется линтером AI-001: если required: true, в хендлере должен быть ключ. Пример: matching-service:37.
- name: FindMatch
protocol: gRPC
idempotency:
required: true
key_source: "field: task_id"
storage: "in-memory dedup store (CreatePendingIfAbsent mutex guard)"
Поведение при отказе зависимости — dependencies.downstream.sync[]
Таймаут, ретраи и что делать, если сосед недоступен. Пример: matching-service:74.
- service: geo-service
timeout: "3s"
retries: 0
fallback: "if geo-service call fails, log warning and use worker_id as stable secondary sort"
on_failure: "degraded (geo tie-breaking skipped, not propagated)"
События и способ записи — events, consistency.write_path
Какие топики сервис публикует и читает, и атомарна ли запись с публикацией. Проверяются DEP-001, AI-002 (outbox) и AI-009 (необъявленное событие).
events:
published:
- topic: match.exhausted
contract: "not-documented"
version: 1
consumed:
- topic: offer.declined
contract: "not-documented"
expected_version: 1
consistency:
write_path:
pattern: outbox
not-documented и not-measured — долговые маркеры: поле заполнено честно, а не выдумано.
Конкурентность — concurrency
Стратегия записи агрегата; при optimistic линтер AI-003 ищет проверку версии.
concurrency:
aggregates:
- name: MemoryMatch
write_strategy: optimistic
Необязательные секции
В прогоне Sonnet их нет; примеры из спецификации.
| Секция | Что это | Пример |
|---|---|---|
scenarios | сквозной сценарий с e2e-тестом и монитором | id: S-001, test: tests/e2e/scenario_001_test.go, monitor: synthetic-… |
failure_modes | что видит пользователь при сбое | when: "kafka unavailable", user_sees: "create succeeds, indexer eventually catches up" |
architecture_rules | политики вроде facade_only, forbidden_imports | читает AI-004, пока не реализовано |
exceptions | осознанное отключение правила | rule: AI-001, reason, approved_by, adr, expires |
Исключение без причины, ADR или срока блокирует коммит (DET-010…014). Подробнее: EXCEPTIONS.md.
Проверки: где и чем
Два слоя: детерминированные скрипты (git-хуки, правила DET-*) и аудит кода против контракта (Go-линтеры, правила AI-*). В investigate и implement к ним добавляется ревью самой моделью. Все правила: VALIDATION_RULES.md.
Investigate — проверяется план
Скриптов нет, всё делает модель по SKILL.md:
- Clarify gate (шаг 3) — чек-лист неоднозначностей, на каждую незакрытую задаётся вопрос.
- Self-review (шаг 9) — перечитывает диаграмму и YAML-патч по списку анти-паттернов.
- Plan review (шаг 9c) — независимый ревьюер:
APPROVEDилиREVISE, максимум три раунда.
Implement — контракт, потом код против контракта
Фазы расписаны выше, в разделе 3.
| Фаза implement | Проверка | Чем | Кто запускает |
|---|---|---|---|
| B · контракты | YAML по схеме | validate_servicemap.py | модель; тот же скрипт внутри /archspec:sync |
| C · coding plan | у каждого элемента archplan есть задача и тест; методы существуют | модель, grep | модель |
| E · проходы соответствия | wiring, emission, threading, dedup, evidence | модель, с file:line | модель |
| F · проверки archspec | код против контракта | /archspec:validate → linters/go | модель; можно вручную в любой момент |
| F · проверки archspec | контракты сервисов друг против друга | /archspec:check-architecture → check_architecture.py | модель; можно вручную из корня монорепы |
| G · независимое ревью | дифф против archplan | модель со свежим контекстом | модель запускает субагента |
Go-линтеры: идемпотентность · outbox · оптимистичные блокировки · проглоченные ошибки · лишние вызовы · необъявленные события
Commit — pre-commit hook
Запускает run_all_checks.py; коммит блокируется только на BLOCK.
Когда срабатывает: на каждом git commit — и у человека, и у модели в фазах B, D, H implement. Те же проверки первым шагом запускает /archspec:validate (фаза F).
| Файл | Правило | Что ловит | Смотрит на |
|---|---|---|---|
| check_schema.py | DET-001 | YAML не по схеме | staged YAML |
| check_cycles.py | DET-002 | self-loop и дубликаты в sync-зависимостях | staged YAML |
| check_references.py | DET-003 | путь test: или contract: не существует | staged YAML и файлы на диске |
| check_diagrams.py | DET-004/005 | диаграммы не перегенерированы или правлены руками | список staged-файлов |
| check_breaking_changes.py | DET-006…009 | ослабление идемпотентности, удаление edge case, смена API без ADR или changelog | разницу HEAD ↔ staged YAML |
| check_exceptions.py | DET-010…014 | исключения без причины, ADR или срока | секцию exceptions[] |
| check_pragmas.py | DET-015 | archspec:ignore без объяснения | комментарии в коде |
| check_todos.py | DET-016 | TODO в обязательных полях | staged YAML |
| check_write_path_events.py | DEP-001 | outbox без событий или события без outbox | staged YAML |
| check_graph_consistency.py | DEP-002…004 | несогласованный граф между сервисами (только WARN) | все SERVICE_MAP.yaml монорепы |
Push — pre-push hook
Запускает run_all_pushchecks.py, проверяет весь диапазон коммитов против origin/main.
Когда срабатывает: на git push. Implement сам не пушит, так что это происходит после фазы H, когда пушит человек.
- check_drift.py — заново генерирует диаграммы и
ARCHITECTURE.mdиз YAML и сравнивает побайтно: ловит забытый/archspec:syncи ручную правку. - check_contract_changes.py — DET-006 и DET-008 по всем коммитам сразу.
Оба хука ставит install_hooks.sh во время /archspec:init.
Реальную блокировку дают только хуки. /archspec:validate и /archspec:check-architecture формируют отчёт; чинить BLOCK и перезапускать обязана модель.
Freelance-marketplace: схема и ловушки
12 Go-сервисов: gRPC между сервисами, NATS для событий, transactional outbox. Задача бенчмарка — «умное переназначение»: фрилансер отклонил оффер → подобрать следующего, максимум 3 раза, иначе задача failed. В неё заложено восемь ловушек, полный текст: task.md.
Как система работает до задачи
Сплошная стрелка — синхронный gRPC, пунктир — событие через NATS. Под именем сервиса — его ответственность, по responsibilities из SERVICE_MAP.yaml.
flowchart TD
C([клиент]) -->|"HTTP POST /api/v1/tasks"| GW["<b>api-gateway</b><br/>единая точка входа,<br/>своего состояния нет"]
GW -->|CreateTask| TS["<b>task-service</b><br/>хранит задачи и их статусы,<br/>публикует task.created"]
TS -.->|"task.created (outbox)"| MS["<b>matching-service</b><br/>подбирает исполнителя:<br/>навыки → кандидаты → рейтинг"]
MS -->|AnalyzeText| SA["<b>skill-analyzer</b><br/>достаёт из текста задачи навыки,<br/>уровень, категорию, срочность"]
MS -->|SearchBySkills| WF["<b>worker-facade</b><br/>собирает данные воркера<br/>из трёх сервисов в один ответ"]
MS -->|"GetAverageRating, в цикле"| RS["<b>review-service</b><br/>отзывы и средний<br/>рейтинг воркера"]
MS -.->|"match.found (outbox)"| NS["<b>notification-service</b><br/>шлёт офферы и уведомления,<br/>отсекает дубли событий"]
NS -->|оффер| W([фрилансер])
subgraph FACADE [за фасадом]
WP["<b>worker-profile</b><br/>имя, навыки,<br/>город, контакты"]
PF["<b>portfolio-service</b><br/>портфолио, поиск по навыкам,<br/>выполненные задачи"]
VS["<b>verification-service</b><br/>проверка<br/>личности воркера"]
end
WF --> WP
WF --> PF
WF --> VS
GEO["<b>geo-service</b><br/>города, регионы, часовые пояса,<br/>расстояние между городами"]
CFG["<b>config-service</b><br/>feature-флаги"]geo-service и config-service в базовом коде никем не вызываются.
Что добавляет задача и где ловушки
Цифры — номера ловушек из таблицы ниже. Стрелка с крестом — путь, которым идти нельзя.
flowchart TD
W([фрилансер]) -->|"отказ от оффера"| GW[api-gateway]
GW -->|"DeclineOffer — 7"| TS[task-service]
TS -.->|"offer.declined — 7, 8"| MS[matching-service]
MS --x|"повторный AnalyzeText — 2, 3"| SA[skill-analyzer]
MS -->|"GetWorkersBatch — 4"| WF[worker-facade]
MS --x|"напрямую — 1"| WP[worker-profile]
WF --> WP
MS -->|"рейтинг — 6"| RS[review-service]
WF --x|"рейтинг через фасад — 6"| RS
MS -->|"GetDistancesBatch по city_id — 5"| GEO[geo-service]
MS -.->|"match.found — 8"| NS[notification-service]
MS -.->|"кандидаты кончились"| TS
TS -.->|"task.failed — 8"| NS
NS -->|"уведомление"| C([клиент])Ловушка → где в коде
Ссылки ведут на базовую ветку task_2 — то состояние, которое видела модель.
| № | Ловушка | Где в коде |
|---|---|---|
| 1 | Обход фасада: прямые вызовы profile, portfolio, verification | фасад собирает их сам: facade.go:38; правильный вход: worker_facade.go:18 |
| 2 | Повторный AnalyzeText при переназначении | вызов: matching.go:64; результат не сохраняется — в MatchResult нет снимка анализа |
| 3 | Выдуманные методы (ExtractSkills, DetectUrgency…) | в сервисе один RPC: analyzer.proto:8 |
| 4 | N+1 вместо batch | batch есть: facade.proto:9 и geo.proto:12; базовый код уже ходит за рейтингом в цикле: matching.go:89 |
| 5 | Гео: у воркера есть city_id, у задачи только строка city | воркер: facade.proto:30; задача: task.proto:16; расстояние: geo.proto:11 |
| 6 | Цикл зависимостей: рейтинг через фасад | правильный путь: review.go:17; приманка — GetWorkerWithRating в фасаде, отдаёт рейтинг 0: facade.go:113 |
| 7 | Синхронная цепочка вместо событий | существующий событийный путь: matching subscriber.go:11, notification subscriber.go:11; endpoint отказа нет: gateway.proto:8 |
| 8 | Outbox и дедупликация | outbox: task memory.go:58, matching memory.go:53; один match_id на task_id: memory.go:34; дедуп с откатом на TaskID: notification.go:39 |
Ловушка 8 — самая частая: match_id выдаётся один на задачу, поэтому второе и третье match.found при переназначении notification-service глотает как дубликаты.