EXPERIMENTAL (Claude Code only). Apply a SPEC increment to a single LIVING architecture corpus under docs/architecture/ instead of a per-SPEC silo package — treats docs as event-sourcing (SPEC = commit, corpus = working tree), so C4 Context/Container, glossary and the data model stay system-wide and never drift across SPECs. Use when PM mentions "living corpus", "single architecture", "corpus mode", "merge design into the corpus", "one architecture for all SPECs". Opt-in behind settings.exper...
Scanned 9/5/2026
Install to Claude Code
npx -y skills add cryndoc/polisade-orchestrator --skill design-corpus --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Design Corpus?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/cryndoc-design-corpus)More formats (shields.io, HTML) on the badges page.
---
name: design-corpus
description: 'EXPERIMENTAL (Claude Code only). Apply a SPEC increment to a single LIVING architecture corpus under docs/architecture/ instead of a per-SPEC silo package — treats docs as event-sourcing (SPEC = commit, corpus = working tree), so C4 Context/Container, glossary and the data model stay system-wide and never drift across SPECs. Use when PM mentions "living corpus", "single architecture", "corpus mode", "merge design into the corpus", "one architecture for all SPECs". Opt-in behind settings.experimental.designCorpus (default off). The corpus is built best-effort by a strong model (provenance INFERRED/GAP, no deterministic integrity gates) — grep-fallback grounding by default, an optional BYO LSP-MCP when one is connected.'
argument-hint: "[SPEC-XXX] [--resume=run-id] [--dry-run] [--inputs=path1.md,...] [--only=...] [--skip=...]"
cli_requires: "task_tool"
claude_only: true
---
# /polisade:design-corpus [SPEC-XXX] — живой архитектурный корпус (experimental, Claude-only)
Применяет инкремент одной SPEC к **единому живому корпусу** `docs/architecture/`
вместо изолированного пакета `DESIGN-NNN-<slug>/` на каждую SPEC. Организующий
принцип — **docs как event-sourcing**: SPEC = immutable commit (дельта
требований), корпус = working tree (одно живое состояние). C4-уровни L1/L2,
glossary и доменная модель описывают систему **целиком** и фиксируются один раз;
каждая SPEC добавляет тонкий `changeset.yaml` + ADR + trace-рёбра, не дублируя
системный слой.
> **EXPERIMENTAL / Claude-only.** Скилл помечен `claude_only: true` и
> исключён из qwen/gigacode/opencode-сборок (он рассчитан на сильную модель).
> `/polisade:design` (per-SPEC силос) **не затронут** — это отдельный
> production-путь для слабых моделей.
## Экспериментальный гейт — ВЫПОЛНИ ПЕРВЫМ, до любого IO
1. Прочитай `.state/PROJECT_STATE.json`.
2. Возьми `settings.experimental.designCorpus` (канонически здесь;
`.claude/settings.json` — это permissions, его НЕ читаем и НЕ трогаем).
3. Если ключа нет или он `false` — **объясни PM**, что это экспериментальный
opt-in режим, и покажи как включить (в `.state/PROJECT_STATE.json`):
`settings.experimental.designCorpus: true`.
**Выйди без единой записи.** Production-путь per-SPEC силоса — `/polisade:design`.
4. Только если `true` — продолжай.
⛔ **Скажи ГРОМКО, до применения** (класс F1 — отсутствие гарантии запрещено
подавать как положительный факт), дословно:
> ⚠️ **BEST-EFFORT CORPUS — мерж БЕЗ защиты корпуса.**
> **корпус строится best-effort, без гарантий полноты и без гейтов** —
> провенанс `INFERRED`/`GAP`, `CONFIRMED` — **никогда**; и **атомарности
> применения нет**: упорядоченная запись staging → формат-линт → промоция,
> не транзакция. Атомарен КАЖДЫЙ ФАЙЛ (полузаписанных файлов не бывает),
> но не набор: обрыв между файлами оставляет корпус смешанным — он будет
> обнаружен и откатывается одной командой, но это не транзакция.
Это не блокер — best-effort рабочий, — но **тихо пройти нельзя**. Отсутствие
гейтов не эквивалентно пройденным гейтам; PM обязан знать, что мерж не защищён.
## Единый примитив записи — ВСЕ записи корпуса идут через него
<!-- polisade:corpus-writer CAPSULE BEGIN -->
> ⛔ **Живой корпус `docs/architecture/` пишет ОДИН исполнитель** —
> `python3 {plugin_root}/scripts/polisade_corpus_io.py` (stdlib-only). Ни
> `Write`, ни `Edit`, ни `cp`, ни `mv`, ни `>` в корпус не идут: примитив даёт
> пофайловую атомарность, блокировку от второго писателя, журнал оборванной
> промоции, проверяемый манифестом backup и отказ на симлинках и побегах пути.
> Ручное копирование обходит всё перечисленное — это регресс, а не «то же самое».
>
> **Хэш-контракт.** Всё, что ты проверил ДО вызова, к моменту записи могло
> устареть, поэтому объявляй примитиву, каким ты ВИДЕЛ цель, — он сверит это
> сам, вплотную к записи: `--expect-absent` для нового файла (публикация идёт
> атомарным `linkat`, занятое имя отвергает ядро) и `--expect-sha256 <hex>`
> для правки существующего (ключ — из ПРОЧИТАННЫХ байтов, не из размера и не
> из даты); для `promote` — карта `--expect-from <json>` вида
> `{путь: sha256|absent}`. Отказ `E-expect-*` значит, что файл правил кто-то
> ещё: **не повторяй с `--force`, покажи расхождение PM.**
>
> Исключение ровно одно, и оно названо, а не умолчано: legacy-силос
> `docs/architecture/DESIGN-NNN-<slug>/` пишет `/polisade:design` своим
> Write-инструментом. Силос deprecated и переносится в корпус скриптом
> `scripts/polisade_migrate_silo.py`, а не развивается.
<!-- polisade:corpus-writer CAPSULE END -->
| Что пишем | Команда |
|---|---|
| один файл (ADR, ARCHRUN, `changeset.yaml`) | `polisade_corpus_io.py write <путь> --run-id <run-id> --from <файл> [--expect-absent \| --expect-sha256 <hex>]` |
| staging → корпус (+backup, +DELETE) | `polisade_corpus_io.py promote --staging <dir> --run-id <run-id> --backup <dir> --delete <путь> --expect-from <json> --expect-strict --json` |
| откат корпуса из backup | `polisade_corpus_io.py restore --backup <dir> --run-id <run-id> --json` |
| состояние блокировки/промоции/корпуса | `polisade_corpus_io.py status --run-id <run-id> --json` |
Примитив пишет только под `docs/` (корпус и инкремент SPEC) и держит backup
только внутри `.polisade/tmp/` — путь вне этих корней он отклоняет.
**Шаг 5 гейта — проверить состояние, затем взять блокировку, до первого IO.
Порядок важен: `status` ДО `acquire`.**
```
python3 {plugin_root}/scripts/polisade_corpus_io.py status --json
python3 {plugin_root}/scripts/polisade_corpus_io.py acquire --run-id <run-id> --json
```
- `status` вернул `attention` непустым (exit 1) — **не начинай прогон**:
прочитай `promote.state` и покажи PM разбор (оборванная промоция → откат
через `restore`, нечитаемый журнал → сверка вручную). Тихо продолжать
поверх непонятного состояния запрещено (класс F1).
- `acquire` вернул `E-lock-held` — по этому репозиторию уже идёт прогон
`design-corpus`. **Выйди без единой записи**, процитировав `hint`
(кто держит, с какого времени, что делать). Отказ приходит **и на тот же
`runId`**: два параллельных `--resume` одного прогона — это два писателя,
а не один. Блокировку живого прогона не перехватывают: `unlock --force` —
осознанное действие человека, а не шаг алгоритма.
- `<run-id>` — тот же идентификатор, что уходит в
`architecture.corpus.pendingRun.runId`; при `--resume` бери его оттуда.
Внутри уже взятого прогона `promote`/`write`/`restore` проходят сами
(плюс своя эксклюзивная блокировка операции — двух одновременных записей
не бывает даже под одним `runId`). На POSIX её держит ядро (`flock`), и
после падения процесса она снимается сама; там, где `flock` недоступен,
вывод помечен `opLockMode: exclusive-create` — режим слабее, и после краха
нужен явный `unlock --force`.
- Дальше по прогону зови `status --run-id <run-id>`: со своим id собственная
блокировка не считается поводом остановиться, без него — считается.
- **Блокировка кооперативная:** она останавливает второй `/polisade:design-corpus`,
но не человека и не другой инструмент, пишущий в корпус мимо примитива.
Это не барьер безопасности: кто может запустить скрипт, тот может удалить
и сам lock-файл.
- **Корпус с симлинком не промотируется вовсе** (`E-corpus-unsafe`) — ни
backup, ни откат по такому дереву не были бы верными. Убери ссылку и
повтори; тихого «промотировали, а ссылка осталась» не бывает.
- Сними блокировку в конце прогона — и на успехе, и на halt'е:
`polisade_corpus_io.py release --run-id <run-id>`.
## Использование
```
/polisade:design-corpus SPEC-001 # применить инкремент SPEC-001 к корпусу
/polisade:design-corpus SPEC-001 --dry-run # построить staging + diff, НЕ применять
/polisade:design-corpus --resume=<run-id> # продолжить halted run после /polisade:unblock
```
Флаги `--inputs` / `--only` / `--skip` — как в `/polisade:design` (доп. источники,
whitelist/blacklist типов артефактов).
## Организующий принцип и режимы артефактов
**Прочитай `references/corpus-model.md` перед работой** — режимы LIVING / LOG /
DERIVED / HYBRID, дисциплина «один-файл-на-объект», трассировка в ЛОГЕ (не inline).
Целевая структура корпуса — в **`references/corpus-layout.md`**.
> **Корпус — best-effort prompt corpus.** `manifest.yaml` / `trace.json` /
> `INDEX.md` и set-diff проверки реализуются субагентом **промптом**, без
> детерминированных stdlib-гейтов (это принято осознанно). Транзакционности
> apply нет — только упорядоченная запись (см. «Транзакционность» ниже).
## Алгоритм
### Узел 0 — Parse & validate (как в /polisade:design Phase 1/1.5)
Разобрать `$ARGUMENTS`; зарезолвить SPEC-XXX (status `ready`/`accepted`);
knowledge-gate (`projectContext.techStack`/`description` непусты — иначе
интервью, как в `/polisade:design`). Прочитать существующий корпус
(`docs/architecture/manifest.yaml`, если есть) — это closed-world key-catalog.
Если PM просит миграцию силосов (`--migrate-report` / `--migrate`) — этих
подкоманд больше нет; см. раздел «Миграция силосов» и заверши.
### Узел 1 — resolve_artifact_set
Определи набор затронутых корпусных объектов (entities, flows, components,
NFR, ADR, glossary-terms) для этой SPEC. Для каждого реши **new-vs-edit** по
**типу артефакта (Diátaxis), а не по SPEC** — правила в
**`references/edit-vs-create-rules.md`**. Субагент эмитит typed edit-plan:
`{op: CREATE|MERGE|RENAME|SPLIT|DELETE, target, fields_touched, satisfies:[SPEC.FR]}`.
Резолв ключей — против closed-world key-catalog из `manifest.yaml`:
CREATE существующего → запрет (→ MERGE); MERGE несуществующего → запрет
(→ CREATE); fuzzy-коллизия (synonym) → **halt to PM** (см. blocking-workflow).
### Узел 2 — commit (по одному артефакту, транзакционный stage)
Субагент **никогда не пишет напрямую в `docs/architecture/`**. Все
CREATE/MERGE/DELETE/regen идут в изолированную рабочую копию
`.polisade/tmp/design-corpus/<run-id>/` (snapshot + правки + `run.json`
с base-hash на каждый touched-путь). По одному артефакту:
1. Сгенерируй/обнови файл (granularity = один-файл-на-объект; для много-элементных
файлов — anti-deletion инвариант `after ⊇ before` минус явные DELETE).
2. Прогони **§7-чеклист** на staging — **`references/gates-checklist.md`** (в v1 это
промпт-проверки сильной модели). Любой нерешённый fail → halt to PM.
3. Формат каждого типа — переиспользуй гайды design-скилла кросс-скилл:
`skills/design/references/c4-guide.md`, `skills/design/references/mermaid-er.md`,
`skills/design/references/mermaid-sequence.md`, `skills/design/references/mermaid-state.md`,
`skills/design/references/mermaid-deployment.md`, `skills/design/references/openapi-guide.md`,
`skills/design/references/asyncapi-guide.md`, `skills/design/references/glossary-guide.md`,
`skills/design/references/quality-scenarios-guide.md`, `skills/design/references/adr-guide.md`
(под Claude Code Guard'а нет — кросс-скилл reads работают; #139 к claude-only не применяется).
### Узел 3 — update_existing_corpus
1. Синтезируй **тонкий** `docs/specs/SPEC-NNN/changeset.yaml` (created/modified/
decided + `satisfies:`-рёбра) — схема **`references/changeset-schema.md`**.
Кладётся на место единым примитивом (`polisade_corpus_io.py write`), как и
любой другой файл инкремента, — не Write-инструментом. Под хэш-контрактом:
новый changeset — `--expect-absent`, обновление существующего —
`--expect-sha256` от байтов, которые ты прочитал перед синтезом.
2. ADR (LOG): новый файл в `docs/architecture/decisions/ADR-NNN-*.md`,
supersede-link, глобальная нумерация (ID/ширина сохраняются — relocation
уже сделан в #187 PR1).
3. **Prompt-regen DERIVED** (best-effort): `docs/architecture/manifest.yaml`
(узлы + рёбра-к-SPEC; **без** single `parent` — схема
**`references/manifest-schema.md`**), `trace.json` (свёртка из changeset'ов —
**`references/trace-schema.md`**), `INDEX.md` (первичный навигатор), `c4/*.md`
рендеры из `model/`, `README`. `context-map.yaml` — шов между bounded-context
(**`references/context-map-schema.md`**).
4. **Корпус-wide self-check**: coverage 100% (или retirement-recorded), нет
orphans/dangling-`$ref`/дублей; `lifecycles/<E>` states ↔ `entities/<E>.status`
enum; model↔contract = name-correspondence only; **concerns→views** (ISO 42010,
opt-in `model/viewpoints.yaml`): каждая concern адресована ≥1 view, `view.id` из
фиксированного словаря, нет висячих `addresses` — гейт `concerns_views`;
**quality-scenarios** (ATAM, opt-in `model/quality-scenarios.yaml`): каждый NFR →
≥1 сценарий с измеримым `measure` (число, не проза; голый перцентиль не считается),
`nfr` резолвится в SPEC, `attribute` из словаря ISO/IEC 25010 — гейт
`quality_scenarios` (нет порога в тексте NFR ⇒ honest-halt на под-специфицированный SPEC).
Дополнительно **grounding-WARN** (Шаг 0): если измеримый порог `measure` ОТСУТСТВУЕТ
в тексте NFR в SPEC (вероятно выдуман моделью), гейт даёт **WARN** «порог не обоснован
текстом NFR — подтверди или почини SPEC» (это advisory, вердикт НЕ меняется;
операционализация числа из SPEC проходит без WARN).
5. **Применение — единственный путь: best-effort промоция** (см. раздел «Ветка
best-effort» ниже). Провенанс `INFERRED`/`GAP` (**никогда `CONFIRMED`**), без
детерминированных гейтов целостности, с честным футером. ⛔ **Это не тихий
режим** (класс F1): до первой записи объяви громко —
«**корпус строится best-effort, без гарантий полноты и без гейтов**».
> **Разрез intent/derived (`docs/pipeline-v2/intent-derived-split.md`):**
> руками ведётся только intent-подмножество (ADR, NFR/QAS, glossary, context-map,
> C4 L1, deployment); derived (C4 L2/L3, ER, state, sequences) — регенерируется,
> руками не пишется. Здесь derived рендерится промптом, с той же
> provenance-меткой.
### Ветка best-effort — единственная ветка применения
Корпус строится **best-effort** промптами/субагентами — **без нового
генераторного Python-кода**. (Полный дизайн — внутренний документ
`best-effort-corpus`; в поставку не входит.)
⛔ **F1 — объяви режим ГРОМКО, до записи:**
«**корпус строится best-effort, без гарантий полноты и без гейтов**».
Отсутствие гейтов ≠ пройденные гейты;
молчаливая подача best-effort как гарантии запрещена (ловится smoke-CI).
1. **fan-out reader-субагенты** по модулям/пакетам (идиома `skills/tasks/SKILL.md`
— параллельно, `subagent_type: "general-purpose"`; `cli_requires: task_tool`
уже во frontmatter). Каждый субагент — **read/plan only**, в чистом контексте,
возвращает **структурированные факты** (символы, зависимости, контейнеры,
доменные термины) с источником `code_refs`, файлы НЕ пишет:
- **grep (дефолт):** `grep -rn "<термин>"` / `grep -rn "<symbol>"`;
- **BYO LSP-MCP (точнее, если PM подключил свой сервер):** используй
инструменты пользовательского LSP-MCP полными зарегистрированными именами
(go-to-definition / find-references / outline). Отказ реестра на **голом**
имени ≠ «MCP не подключён»: повтори полным именем, затем считай MCP
недоступным.
- ⛔ «инструмент не найден» ≠ «фактов нет»: не подменяй первое вторым (Idiom A).
2. **synthesizer (лидер)** — собирает корпус в **том же формате**, что и штатный
путь (`references/corpus-layout.md`): по одному файлу на объект
(`model/entities/<E>.yaml`, `model/context-map.yaml`, `model/containers.yaml`,
`c4/context.md`, `glossary/terms/<t>.md`, `decisions/ADR-NNN-*.md`,
`manifest.yaml`, `INDEX.md`). Пишет **только лидер** после консолидации
(Write-tool; формат/схему НЕ меняем — граница ADR-0003). **Manifest —
самосогласованный:** каждый `nodes[].file` и каждый singleton-таргет реально
записан (нет висячих ссылок); `polisade_lint_artifacts.py` существование
таргетов **не** проверяет (покрытие лёгкое — см. дизайн-док §5), поэтому
целостность ссылок здесь — дисциплина скилла, не гейт. ADR-скелеты нумеруй
глобально по существующим `decisions/ADR-*.md` (next-id, как в Узле 3) — дубль
ADR-id линт ловит как error.
3. **provenance-labeler** — каждый эмитируемый файл несёт во frontmatter
provenance-ключи (метаданные вне структурных схем;
`polisade_lint_artifacts.py` их не читает — структура файла остаётся в
формате canon):
```yaml
provenance: INFERRED # INFERRED (обосновано code_refs) | GAP
generated_by: polisade-orchestrator-free
source: BEST-EFFORT
code_refs: [src/order/service.py:OrderService:12-88] # INFERRED — непустой
```
- **`INFERRED`** — факт обоснован реальным `code_refs`; **`GAP`** — известное-
неизвестное (`code_refs` может быть пустым, `confidence: 0.0`), выносится
открытым вопросом, НЕ утверждается как контент; **`CONFIRMED` — никогда.**
4. **honest-footer** — в `INDEX.md` и заметной строкой в теле каждого файла:
> ⚠️ **BEST-EFFORT CORPUS** — провенанс `INFERRED/GAP`, без гарантий полноты
> и без детерминированных гейтов целостности. Утверждения корпуса —
> обоснованные догадки модели по `code_refs`, а не проверенные факты.
5. **Без детерминированных гейтов**, но выход **обязан** проходить
`polisade_lint_artifacts.py` (одна сущность — один файл; ADR-refs валидны; нет
дублей id). Нерешённый вопрос → **halt to PM** (ARCHRUN), как в штатном флоу.
6. **Запись — упорядоченная, пофайлово атомарная, но не транзакция на прогон.**
Ровно в этом порядке:
1. синтезируй всё в изолированную staging-копию
`.polisade/tmp/design-corpus/<run-id>/` (как Узел 2);
2. прогони формат-линт **на staging**; красный линт или нерешённый вопрос →
**halt to PM** (ARCHRUN) прямо здесь: промоция ещё не начиналась, значит
**без частичной промоции и без флипа `mode`**, корпус байт-цел;
3. **backup перед первой записью** (обязателен, если корпус уже существует —
примитив откажет без него: `E-backup-required`). Его делает сам примитив
по флагу `--backup` `.polisade/tmp/design-corpus/<run-id>/backup/`; запиши
этот путь в `PROJECT_STATE.json → architecture.corpus.pendingRun.backupDir`.
Копия закрывается манифестом (состав + `sha256` + права + имя дерева), и
`restore` принимает только её: неполная копия не станет источником отката,
который снёс бы из корпуса не успевшие скопироваться файлы; правленые
после снятия байты (`E-backup-tampered`) и копия ЧУЖОГО дерева
(`E-backup-wrong-target`) отклоняются, а права восстановленных файлов
берутся из манифеста, а не выставляются в `0644`. Это единственный способ откатиться:
**атомарного rollback здесь нет**;
4. промотируй **одной командой** — она запишет файлы staging в
`docs/architecture/`, а **затем удалит** пути, помеченные в edit-plan как
`DELETE` (и только их; `RENAME`/`SPLIT` = записать новые + удалить
старые). Пропуск шага удаления оставит в живом корпусе снятые с учёта
файлы — их не поймает ни один линт, поэтому перечисляй их явно:
**Промоция идёт под хэш-контрактом.** Между `--dry-run` (шаг 4а) и
записью проходят backup и скан корпуса; человек или другой инструмент
мог за это время править корпус руками — блокировка кооперативная и его
не останавливает. Поэтому сначала собери карту ожиданий
`.polisade/tmp/design-corpus/<run-id>/expect.json` вида
`{"<путь-в-корпусе>": "<sha256>|absent"}`: **для каждого** пути из
`created` — `"absent"`, для каждого из `modified`/`unchanged` и для
каждого `--delete` — sha256 байтов, которые ты ПРОЧИТАЛ при построении
diff'а (не размер и не дату). Карта обязана покрывать план целиком —
это и требует `--expect-strict`.
```
python3 {plugin_root}/scripts/polisade_corpus_io.py promote \
--staging .polisade/tmp/design-corpus/<run-id>/staging \
--backup .polisade/tmp/design-corpus/<run-id>/backup \
--run-id <run-id> \
--delete <retired/path.yaml> --delete <another/path.md> \
--expect-from .polisade/tmp/design-corpus/<run-id>/expect.json \
--expect-strict \
--json
```
Отказ `E-expect-*` — **halt, а не повод повторить**: он означает, что
корпус правили после того, как ты построил staging. Покажи PM путь,
ожидавшийся и фактический sha256 (они в `details`), и пересобери
инкремент на актуальном корпусе. `--force` тут не поможет и не для
этого. В успешном отчёте сверь `expectUnchecked: 0` — ненулевое значение
означает, что часть плана прошла БЕЗ сверки, и говорить «корпус
промотирован под контролем» в этом случае нельзя.
`--dry-run` той же команды даёт diff (`created`/`modified`/`unchanged`/
`deletePresent`) **без backup, без записи и без журнала**;
5. только после **чистой полной промоции** (`ok: true`, `promoted` равно
`planned`) — `architecture.corpus.mode` → `"living"` (см. «## Состояние»)
и `pendingRun` → `null`. Любой ненулевой exit — `mode` НЕ трогаем.
При `--dry-run` — staging + diff, **без промоции и без backup**.
⛔ **Честно о прерывании: промоция (шаг 4) НЕ атомарна как набор.** Каждый
отдельный файл записывается атомарно (`tmp`+`rename` в той же ФС), поэтому
полузаписанного файла в корпусе не бывает; но kill или ошибка
ввода-вывода **между** файлами оставляют **смешанный корпус** — часть
файлов новой генерации, часть старой, и `mode` при этом мог остаться
прежним `living` с прошлого прогона. Не заявляй обратного (класс F1).
Разница с ручным копированием — обрыв теперь **обнаруживается, а не
молчит**: примитив пишет журнал `.polisade/tmp/design-corpus/promote.state.json`
до первой записи и закрывает после последней. Процедура восстановления:
1. `polisade_corpus_io.py status --json` — покажет `promote.state:
interrupted` и разбор `applied` / `pending` (сверкой содержимого, а не
по памяти);
2. **не** продолжать промоцию — откатить корпус из backup:
`polisade_corpus_io.py restore --backup <backupDir> --run-id <run-id> --json`;
3. `halt to PM` (ARCHRUN) с указанием, на каком файле оборвались.
Если backup'а нет (первый прогон на пустом корпусе) — удалить частично
записанный `docs/architecture/` целиком и повторить run с нуля. Следующая
промоция поверх необъяснённого состояния **отказывает** (`E-promote-interrupted`
/ `E-state-corrupt`) — тихой перезаписи смешанного корпуса не бывает;
`--force` есть, но это осознанное решение человека, а не шаг алгоритма.
> Порядок записи выше (staging → формат-линт → backup → промоция) плюс
> пофайловая атомарность и блокировка снижают риск частичного корпуса и делают
> его **обнаруживаемым и обратимым одной командой**, но **не заменяют**
> атомарный rollback: гарантии транзакции на весь прогон здесь нет, окно
> смешанного состояния между файлами существует, и футер это раскрывает.
> Целостность **содержимого** (висячие ссылки, дубли, недостижимые `$ref`)
> примитив не проверяет вовсе — «повреждён» для него значит механическую
> порчу, а не смысловую.
## Применение + blocking PM-workflow (ARCHRUN)
⛔ **Run-атомарности НЕТ** — есть только упорядоченная запись staging →
формат-линт → backup → промоция (Ветка best-effort п.6), пофайлово атомарная и
под блокировкой. Не подавай её как транзакцию (класс F1). Перед промоцией
повтори **preflight-сверку с base-hash** каждого touched-пути (MERGE/DELETE —
хэш совпадает; CREATE — путь по-прежнему отсутствует): любой конфликт (файл
изменён/удалён/создан пользователем между snapshot и промоцией) → halt **до**
первой записи, т.е. **без частичной записи**.
⚠️ Preflight сверяет состояние **на момент проверки** и не удерживает его:
чужая правка, пришедшая уже во время копирования, преflight'ом не ловится и
даёт смешанный корпус (см. процедуру восстановления в п.6). Второй прогон
`design-corpus` по тому же репозиторию блокировка примитива останавливает
(`acquire` в гейте), но она **кооперативная**: человек или другой инструмент,
пишущий в `docs/architecture/` мимо `polisade_corpus_io.py`, ей не связан.
**Halt-контракт (ARCHRUN).** При любом нерешённом вопросе (synonym-collision,
FR-retirement, flow soft-ceiling, partition-trigger, неразрешимый fail §7,
hash-конфликт preflight) — **halt**:
1. Создай артефакт `docs/architecture/runs/ARCHRUN-NNN.md` (next-id по
`skills/tasks/references/compute-next-id.md`), frontmatter:
`id`, `type: ARCH-RUN`, `status: waiting_pm`, `parent: SPEC-NNN`, `created`,
тело — процитированный вопрос. Это **запись в корпус**, поэтому она идёт
через тот же примитив (Write-инструментом в `docs/architecture/` не пишем):
собери файл в staging и положи его на место командой
`python3 {plugin_root}/scripts/polisade_corpus_io.py write docs/architecture/runs/ARCHRUN-NNN.md --run-id <run-id> --from <staging-файл> --expect-absent`.
`--expect-absent` обязателен: ARCHRUN — НОВЫЙ файл под свежевычисленным
номером, и если номер уже занят, значит next-id разошёлся с диском —
перезапись стёрла бы чужой журнал прогона. Отказ (`E-expect-present`) —
пересчитай номер, а не повторяй с `--force`.
Тем же способом кладётся `docs/specs/SPEC-NNN/changeset.yaml` (Узел 3 п.1):
новый — `--expect-absent`, правка существующего —
`--expect-sha256 <sha256 прочитанных байтов>`.
2. Сохрани staging; запиши резюм-детали в
`PROJECT_STATE.json → architecture.corpus.pendingRun`
`{ runId, archRunId, stagingDir, backupDir, question, pendingPlanItems }`
(`backupDir` — копия корпуса до промоции, п.6 шаг 3; `null`, если промоция
ещё не начиналась).
3. `python3 {plugin_root}/scripts/polisade_sync.py . --apply` — `ARCHRUN-NNN`
авто-поднимется в `waitingForPM` (штатно, без спец-логики).
4. **Source SPEC статус НЕ меняем.** Никогда не уходи в `done`/`review` с
открытым вопросом и не применяй staging при открытом вопросе/конфликте.
5. Сними блокировку: `polisade_corpus_io.py release --run-id <run-id>`. Halt —
это пауза до ответа PM, а не удержание корпуса: `--resume` возьмёт
блокировку заново по тому же `runId` из `pendingRun`.
**Резюм.** PM отвечает через `/polisade:unblock` (снимает `waiting_pm`), затем
запускает `/polisade:design-corpus --resume=<run-id>`: **сначала**
`polisade_corpus_io.py status --json` (не оборвалась ли прошлая промоция),
**затем** `acquire --run-id <тот же runId из pendingRun>`, читаешь `pendingRun`
и продолжаешь со staging с **повторным preflight hash-check** (корпус мог
измениться). Порядок именно такой: `acquire` до `status` заставил бы прогон
спотыкаться о собственную блокировку. `ARCHRUN.ready` означает «resume required via --resume», а не
обычный work-item — `/polisade:implement` его игнорирует, `/polisade:state`
показывает отдельной corpus-run секцией.
## Decision-правила (сводка; детали — references/edit-vs-create-rules.md)
- **new-vs-edit = свойство Diátaxis-ТИПА, не SPEC**: LIVING edit-in-place /
LOG append-supersede / HYBRID member-as-unit (новый член только при новом
partition-key, иначе edit).
- **flows:** ADD новый flow iff новый trigger/actor И ≥1 новое событие; иначе
EDIT. Шаги ссылаются на `operationId`/channel-message, не дублируют payload.
- **RENAME/SPLIT** переписывает back-edges атомарно (в рамках одного staging-run).
- **anti-deletion** для много-элементных файлов: `after ⊇ before` минус явные
DELETE. Whole-file regen — только для одно-элементных.
- **model↔contract = name-correspondence ONLY**: `schemas/Order` iff
`entities/Order`; равенство полей НЕ требовать.
- **coverage:** retired (есть changeset-запись удаления) ≠ lost (нет) — различать.
- **sprawl:** `flows/` по контексту + soft-ceiling → PM; ADR-status-индекс
(accepted − superseded); `docs/specs/` навигируется через `trace.json`/`INDEX.md`.
## Миграция силосов в корпус
<!-- polisade:silo-legacy POINTER — канон «Силос → корпус» живёт в /polisade:design -->
> **Силос ≠ корпус.** Источник правды по архитектуре — живой корпус
> `docs/architecture/`; пакет `DESIGN-NNN-<slug>/` — legacy-силос. Прочитал
> файл из силоса — скажи об этом вслух (переходное чтение). Полный канон —
> `/polisade:design`, блок «Силос → корпус»; перевод силоса на корпус —
> `python3 scripts/polisade_migrate_silo.py <пакет>` (dry-run по умолчанию).
Подкоманды `--migrate-report` / `--migrate` из скилла **удалены**: обе жили в
детерминированной плоскости гейтов и в best-effort режиме воспроизведены быть не
могут (отчёт коллизий без разрешения ссылок дал бы «коллизий не найдено» — класс
F1, отсутствие способности как положительный факт). Существующие силосы
`DESIGN-NNN-<slug>/` остаются на месте и **не** мигрируются автоматически:
классификацию intent/derived по одному силосу даёт офлайн-инструмент
`python3 {plugin_root}/scripts/polisade_migrate_design.py <silo-dir> --report`
(read-only, ничего не пишет).
**Перевод силоса на корпус (V3-S3.31)** — `python3
{plugin_root}/scripts/polisade_migrate_silo.py <silo-dir>`: dry-run по
умолчанию, запись — явным `--apply`, и **вся запись идёт через тот же примитив**
`polisade_corpus_io.py promote` (никаких собственных записей в
`docs/architecture/` у него нет). Он оставляет в пакете `MIGRATED.md` (карта
домов + фраза «источник правды — корпус»), **ничего из силоса не удаляет** и
переносит байт-в-байт только то, у чего дом в корпусе — тот же ОДИНОЧНЫЙ файл.
**Таких артефактов по действующей раскладке нет ни одного**
(`references/corpus-layout.md`): C4 L1 → `model/context-map.yaml` **+**
`c4/context.md`, C4 L2 → `model/containers.yaml` **+** `c4/container.md`,
остальное — один-ко-многим и/или типизировано. Конфликт (цель существует и
отличается) останавливает прогон целиком — разрешает его человек
(`--on-conflict=stop|skip|overwrite`), не скрипт.
Значит, **весь силос — класс `derive`**: `glossary.md` →
`glossary/terms/<term>.md`, `data-model.md` → `model/entities/<Entity>.yaml`,
`sequences.md` → `flows/<ctx>/`, `state-machines.md` → `lifecycles/`,
`quality-scenarios.md` → `quality/<NFR>.md`, `deployment.md` →
`deployment/<env>.md`, `api.md` / `async-api.md` → `contracts/`, C4 — в пару
«`model/` + рендер». Разбивка и типизация — **синтез фактов, а не транспорт**,
поэтому механически мигратор их не переносит: это работа ЭТОГО скилла.
Забери список одной командой —
`polisade_migrate_silo.py <silo-dir> --worklist <файл вне корпуса>` — и на
Узле синтеза складывай перечисленные файлы силоса в их типизированные дома
на общих правилах best-effort (провенанс `INFERRED`/`GAP`, `code_refs`,
честный футер). Пока файл не свёрнут, **его единственная копия — в силосе**: читать его нужно
оттуда, с громкой пометкой переходного чтения (канон «Силос → корпус» в
`/polisade:design`). Считать его устаревшим из-за того, что карта назвала ему
целевой дом, — потерять факт.
## Состояние
После **успешного** применения обнови `PROJECT_STATE.json`. «Успешное» = **чистая
полная промоция** staging→`docs/architecture/` (все файлы записаны + формат-линт
зелёный, Ветка best-effort п.6). Формальный признак — exit 0 и `ok: true` в
`--json`-выводе `polisade_corpus_io.py promote`, где `promoted` равно `planned`;
любой ненулевой exit (`E-lock-held`, `E-promote-interrupted`, `E-state-corrupt`,
`E-symlink`, `E-io`) — это **не** успех, `mode` в таком прогоне не трогаем.
В конце прогона — `polisade_corpus_io.py release --run-id <run-id>`:
- **`architecture.corpus.mode` → `"living"`** — обязательный шаг при **первом**
применении к проекту. Поле инициализируется как `"silo"` (default —
per-SPEC силос `/polisade:design`); именно `/polisade:design-corpus` переводит
его в `"living"`. Это durable-переключатель, активирующий corpus-aware
консьюмеры: `doctor --traceability` (fold из changeset'ов вместо
per-package), секцию CORPUS-RUN в `/polisade:state`, corpus-ветку
`/polisade:reconcile-docs`. Пока `mode != "living"` корпус для них невидим.
Уже `"living"` — оставь как есть; **не** ставь `"living"`, если run закончился
halt'ом, красным формат-линтом или частичной промоцией (корпус не записан
целиком — см. halt-контракт и Ветку best-effort п.6). Best-effort-корпус тоже
становится `"living"` — но помечен `INFERRED/GAP` без гейтов (футер).
- `architecture.corpus.dir` / `architecture.corpus.manifest` — выставь
`"docs/architecture"` / `"docs/architecture/manifest.yaml"`, если ещё дефолтные.
- `architecture.corpus.pendingRun` — **`null`** на успехе (непустой только при
halt).
- `architecture.activeADRs` / `deprecatedADRs` — как обычно;
- `ARCHRUN-NNN` артефакт — через `polisade_sync.py` (не редактируй
derived-списки руками).
## Design-basis
Проектная основа (research-output, 9 агентов) — внутренний документ
`187-architecture-corpus-organization`; в поставку не входит.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!