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:initbench: runs/archspec/setup

task_4_with_sonnet_4_6 — это OpenSpec, не archspec.

Пайплайн

КомандаЧто пишет
/archspec:initSERVICE_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

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
CCoding 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 — каждый ловит класс багов, который проходит при зелёных тестах:

Артефакты прогона Sonnet:

Edge cases: когда и как

  1. investigate, шаг 8a. Каждый риск становится записью edge_cases[] в YAML-патче archplan. Имя теста придумывается сразу.
  2. implement, фаза B «Сначала контракты». Патч попадает в SERVICE_MAP.yaml вместе с тестами-заглушками.
  3. 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 нельзя без ADRgit 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-serviceEC-001…004SERVICE_MAP.yaml:121
matching-serviceEC-005…009SERVICE_MAP.yaml:113
notification-serviceEC-010, 014, 015SERVICE_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:

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.pyDET-001YAML не по схемеstaged YAML
check_cycles.pyDET-002self-loop и дубликаты в sync-зависимостяхstaged YAML
check_references.pyDET-003путь test: или contract: не существуетstaged YAML и файлы на диске
check_diagrams.pyDET-004/005диаграммы не перегенерированы или правлены рукамисписок staged-файлов
check_breaking_changes.pyDET-006…009ослабление идемпотентности, удаление edge case, смена API без ADR или changelogразницу HEAD ↔ staged YAML
check_exceptions.pyDET-010…014исключения без причины, ADR или срокасекцию exceptions[]
check_pragmas.pyDET-015archspec:ignore без объяснениякомментарии в коде
check_todos.pyDET-016TODO в обязательных поляхstaged YAML
check_write_path_events.pyDEP-001outbox без событий или события без outboxstaged YAML
check_graph_consistency.pyDEP-002…004несогласованный граф между сервисами (только WARN)все SERVICE_MAP.yaml монорепы

Push — pre-push hook

Запускает run_all_pushchecks.py, проверяет весь диапазон коммитов против origin/main.

Когда срабатывает: на git push. Implement сам не пушит, так что это происходит после фазы H, когда пушит человек.

Оба хука ставит install_hooks.sh во время /archspec:init.

Реальную блокировку дают только хуки. /archspec:validate и /archspec:check-architecture формируют отчёт; чинить BLOCK и перезапускать обязана модель.

Как устроены питоновские проверки

Обычные скрипты без ИИ. Читают YAML как данные и сравнивают его со схемой, с прошлой версией из git и с заново сгенерированными файлами. Go-код сервиса не читают.

На чём построено

ИнструментДля чего
pyyamlпревратить SERVICE_MAP.yaml в словарь
jsonschemaпроверить словарь по JSON Schema
jinja2сгенерировать диаграммы и ARCHITECTURE.md из шаблонов
git diff --cached, git showсписок staged-файлов, содержимое файла из индекса или из HEAD

Зависимости: requirements.txt. Обёртки над git: _git.py.

Каркас: run_all_checks.py берёт staged-файлы, вызывает run(staged) у каждой проверки и собирает находки (_finding.py). Код возврата 1 — только при BLOCK.

Четыре приёма

ПриёмКак работаетФайлы
Валидация по схемеYAML → jsonschema.Draft7Validator; схема закрытая, лишний или пропущенный ключ — ошибка с путём до поляcheck_schema.py, validate_servicemap.py, схема
Обход словаряобычный Python по загруженному YAML: self-loop, дубликаты через Counter, Path.exists() для путей тестовcheck_cycles.py, check_references.py, check_todos.py, check_exceptions.py, check_write_path_events.py
Сравнение «было — стало»файл берётся из HEAD и из индекса, сравниваются словари: идемпотентность true → false, пропавшие id edge cases, изменённый api без записи в changelogcheck_breaking_changes.py, check_contract_changes.py
Перегенерация и побайтное сравнениезапускает тот же генератор во временную папку и сравнивает read_bytes() с файлами в репозиторииcheck_diagrams.py, check_drift.py, sync.py

Четвёртый приём работает, потому что генерация детерминирована: один YAML всегда даёт один и тот же вывод.

Отдельные случаи

Чего питон не делает

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
4N+1 вместо batchbatch есть: 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
8Outbox и дедупликация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 глотает как дубликаты.