# Changelog

## 0.55.2

### Patch Changes

- 2bae0d3: DataGrid `col.actions` display modes + row-contextual-actions-menu pattern.

  ## col.display: "inline" | "menu" | "auto"

  `col.kind:"actions"` теперь поддерживает **display mode**:

  - `"inline"` — все кнопки в ряд (legacy, без изменений)
  - `"menu"` — kebab (`⋯`) или gear (`⚙`, через `col.icon:"gear"`) trigger открывает dropdown
  - `"auto"` (default) — inline если `actions.length <= 2`, иначе menu

  **Motivation (Gravitino dogfood 2026-04-23, user observation):**

  > "шестерёнка вызывает контекстное меню с действиями, возможными к выполнению для сущности в row"

  3+ actions per row перегружают визуально admin-таблицы. AntD Pro ProTable / Stripe Dashboard / GitHub PR list / K8s Lens — все дефолтят на dropdown при ≥3 actions.

  ### Example

  ```js
  {
    key: "_actions",
    label: "Actions",
    kind: "actions",
    display: "menu",        // or "auto"
    icon: "gear",           // "⋯" default, "⚙" если icon:"gear"
    menuLabel: "Row actions",
    actions: [
      { intent: "editUser", label: "Edit", params: { id: "item.id" } },
      { intent: "duplicateUser", label: "Duplicate", params: { id: "item.id" } },
      { intent: "grantRoleToUser", label: "Grant Role", params: { user: "item.name" } },
      { intent: "deleteUser", label: "Delete", params: { id: "item.id" }, danger: true },
    ],
  }
  ```

  **Keyboard:** AntD Dropdown — native ARIA menu + keyboard. SVG-fallback — Enter / Escape / click-outside.

  ## Pattern: row-contextual-actions-menu

  Новый curated candidate (catalog-archetype, matching-only). Документирует когда inline → menu rewrite уместен.

  Field evidence:

  - Apache Gravitino v2 WebUI (user observation 2026-04-23)
  - AntD Pro ProTable (`actionRef` ellipsis column)
  - Stripe Dashboard (customers / invoices / subscriptions)
  - GitHub PR/issues lists
  - Linear / Height / Notion database rows
  - K8s Lens / Rancher

  **Tests:**

  - 410 renderer (+8: 6 display-mode, 1 gear icon, 1 disabled-in-menu)
  - 45 adapter-antd
  - 1201 core (+4: 7 curated candidates + id-uniqueness)

## 0.55.1

### Patch Changes

- d78680b: Pattern bank: новый curated candidate `catalog-default-datagrid`.

  **Observation (Gravitino dogfood 2026-04-23):** catalog-archetype без image/multiImage и без ≥3 money/percentage/trend metrics по умолчанию рендерится как grid карточек (legacy fallback), хотя в admin/CRUD surface (metalakes / roles / policies / users / customers / invoices) пользователь ожидает tabular DataGrid с per-column sort/filter. Field research подтверждает: Gravitino v2 WebUI, Stripe Dashboard, AntD Pro, K8s Lens/Rancher, Linear admin — все дефолтят на табличный layout в CRUD-админке.

  **Complements** `grid-card-layout` (visual-rich feeds): apply order между двумя паттернами даст grid-card-layout приоритет когда срабатывает его trigger, иначе — catalog-default-datagrid.

  **Matching-only в этом релизе** — `structure.apply` deferred. Перед promotion в stable:

  1. Интеграция с crystallize_v2 synthesized bodyOverride (pattern должен заменять slots.body на `type: "dataGrid"` без ломания author-override через `projection.bodyOverride`)
  2. Composition с `catalog-action-cta` (если в intent-каталоге есть inline actions — добавлять action-column)
  3. Falsification batch против invest/sales/messenger/reflect (shouldNotMatch) + gravitino/compliance/Stripe-like domains (shouldMatch)

  **Tests:** 6 curated candidates (+1 к существующим 5), все проходят validatePattern + registry-integration.

## 0.55.0

### Minor Changes

- 0153d20: Authored `projection.bodyOverride` в catalog-архетипе — полная замена derived body-node.

  **Use-case:** Gravitino catalog_list хочет DataGrid primitive с явными columns (sort/filter per column) вместо default card-list. До этого fix'а — crystallize игнорировал authored `body` в projection и всегда строил card-list через `buildCatalogBody`.

  **Shape:**

  ```js
  PROJECTIONS.catalog_list = {
    kind: "catalog",
    mainEntity: "Catalog",
    entities: ["Catalog"],
    bodyOverride: {
      type: "dataGrid",
      items: [],   // runtime-filled renderer'ом из world
      columns: [
        { key: "name",     label: "Name",     sortable: true, filterable: true },
        { key: "type",     label: "Type",     filter: "enum", values: [...] },
        { key: "provider", label: "Provider", sortable: true },
      ],
      onItemClick: { action: "navigate", to: "catalog_detail", params: { catalogId: "item.id" } },
    },
  };
  ```

  **Поведение:**

  - `projection.bodyOverride` (object) → `slots.body = projection.bodyOverride` без модификаций
  - Strategy post-processing (shape / itemLayout / emphasisFields / aggregateHeader / extraSlots / cardSpec) — **skipped** для authored bodyOverride. Автор декларирует rendering explicitly, SDK не полирует.
  - `slots.body.item.intents = itemIntents` — также skipped (авторский body сам декларирует interactions через `onItemClick` и т.п.)
  - Без `bodyOverride` — old behavior: `buildCatalogBody(projection, ONTOLOGY)` + все strategy-mutations.

  **Parallel к projection.subCollections** (backlog §16 author curation) — authored curation поверх derivation: автор знает что декларирует, SDK respects.

  **Tests:** +3 unit в `assignToSlotsCatalog.test.js`. 1190/1190 core pass.

  **Closes:** Gravitino G20/G21/G38 (chip columns + Type/Provider filter + column filters) через host-level authoring catalog_list.bodyOverride.

## 0.54.0

### Minor Changes

- 8647f82: `normalizeIntentNative` теперь synthesize'ит `intent.creates` и `particles.effects` из flat-format (`α + target`), который эмитят scaffold-path и LLM-авторинг (studio PM chat). Без этого `deriveProjections` на flat-intents видел creators/mutators пустыми и не выводил R1 catalog / R3 detail — UI оставался без проекций даже при полной онтологии.

  - `α: "create"` + `target: "Task"` → добавляет `creates: "Task"` и `particles.effects: [{ target: "Task", op: "create", α: "create" }]`.
  - Любой `α + target` → добавляет соответствующий effect (`op: α`).
  - Если автор уже задал `intent.creates` или `particles.effects` — не перезаписываем.

  Host-workaround `enrichIntentsForDerive` в idf-studio можно удалить после bump'а.

## 0.53.0

### Minor Changes

- a06e7e6: Declarative `field.primitive` hint на уровне ontology — перекрывает role/type-based detail-body inference для custom primitives.

  **Use-case:** Gravitino `Table.columns` (type: "json") рендерился как infoSection entry с bind'ом на raw JSON blob. После fix'а автор декларирует:

  ```js
  Table: {
    fields: {
      name: { type: "string", role: "primary-title" },
      columns: { type: "json", primitive: "schemaEditor" },  // ← hint
    },
  }
  ```

  → `detail.slots.body` получает standalone atom `{ type: "schemaEditor", bind: "columns", label: "columns" }` после обычных role-based секций. Renderer PRIMITIVES map resolves "schemaEditor" → рендер через SchemaEditor primitive (или через adapter, если capability registered).

  **Generic механизм:** работает для любого primitive из PRIMITIVES — `schemaEditor`, `map`, `chart`, `treeNav`, `breadcrumbs`, etc. Field-level override вместо projection-level — cleaner authoring.

  **Scope:**

  - Fields с `primitive` hint pull'ятся из role-based grouping, emit'ятся как standalone atoms после основных секций detail-body.
  - `label` прокидывается в primitive props (использующие primitives — оставляют, не использующие — игнорируют).
  - Backward compat: fields без hint идут обычным путём (role/type heuristic).

  **Tests:** +3 unit в `assignToSlotsDetail.test.js` — schemaEditor override, back-compat без hint, arbitrary primitive name (map). 1187/1187 core pass.

  **Related:** Gravitino dogfood-спринт Stage 3 Task 3 (host-integration SchemaEditor для Table.columns) — после merge этого PR + bump host ontology annotate Table.columns.primitive.

## 0.52.1

### Patch Changes

- 1d7cb5c: `subcollections` pattern apply теперь возвращает render-ready section shape.

  До этого auto-derived sections имели thin-shape:

  ```js
  { id, entity, foreignKey, layout, intents, source: "derived:subcollections" }
  ```

  Renderer ожидает `{title, itemEntity, itemView, itemIntents}` (как у `buildSubSection` в `assignToSlotsDetail.js`) — получал `undefined`, рендерил пустые заголовки + unknown-entity списки. Регресс виден на Gravitino: `user_detail` / `role_detail` / `group_detail` имели sections с пустыми headers.

  Fix: `buildSection` в `subEntityHelpers.js` теперь добавляет:

  - `title` — humanized entity name (`OrderItem` → `"Order item"`)
  - `itemEntity` — alias для `entity` (renderer-preferred name)
  - `itemView` — minimal text-bind на primary-title поле (с fallback на `name`/`title`/`label`, потом `id`)
  - `itemIntents` — alias для `intents`
  - `emptyLabel` — default `"Пока пусто"`

  Backward compat: legacy `entity`/`intents` поля сохранены (удалим в major). Author override через `projection.subCollections` — этот path вообще не изменился.

  **Обнаружено:** Gravitino dogfood-спринт 2026-04-23 Stage 2 Task 1 discovery (docs/gravitino-gaps.md G14 / ux-patterns-notes P-3).

  **Tests:** +3 unit (title/itemEntity/itemView populated, fallback на id, humanize multi-word). 1184/1184 core tests pass.

## 0.52.0

### Minor Changes

- 6b2abac: **heroCreate multi-param filter + Badge sx + witness alignSelf** (backlog §9.10 – §9.12, post-merge follow-up to #179).

  Три SDK-гапа, не попавшие в первую волну workzilla post-bump fixes из-за squash-merge timing'а.

  ### §9.10 — `heroCreate` match отсекает multi-param creator'ов

  Existing check учитывал только `witnesses.length > 1`. Multi-field creator (создать Task — title / description / budget / categoryId / deadline / …) match'ился heroCreate-архетипом и рендерился как hero-input на одно поле — способа открыть полноценную форму не было. Автор вынужден был писать `control: "formModal"` explicit override.

  - Добавлен check по `userVisibleParams.length > 1` (считая `intent.parameters` и `particles.parameters`, исключая `id`). Multi-field creator'ы теперь уходят в formModal → catalog-creator-toolbar pattern (кнопка в toolbar + overlay с полной формой).

  ### §9.11 — `Badge` primitive пропускает `node.sx` в AdaptedBadge

  Раньше `<AdaptedBadge color={tone}>` не получал `node.sx`. AntD Tag внутри flex-column-родителя (`align-items: stretch` default) стретчился на всю ширину карточки — выглядит как полоса вместо тега.

  - Обёртка `<span style={{display: "inline-flex", ...node.sx}}>` — shrink-to-content + пробрасывает `alignSelf` / другие overrides к Tag'у.

  ### §9.12 — `witnessItemChildren` эмитит `sx.alignSelf` для compact primitives

  Badge / timer witness-дети автоматически получают `sx: {alignSelf: "flex-start"}`. Catalog-card layout перестаёт стретчить их на ширину карточки — даже если адаптер отсутствует и используется fallback span.

  ***

  **Тесты:** 2 expectation-update (`witnessItemChildren.test.js`) + фикс `nativeScaffold.test.js` (убран inconsistent `confirmation: "enter"` на 3-param creator'е — normalize инферит `"form"`). Core 1181 → 1181 green, renderer 297, adapter-antd 22.

  **Host-side impact:** после bump'а workzilla-clone сможет убрать:

  - `createTaskDraft.control: "formModal"` explicit override (heroCreate сам откажется).
  - `overrides.css` с `.ant-tag { align-self: flex-start }` (§9.11 делает то же через JSX).

## 0.51.0

### Minor Changes

- be56319: **Workzilla post-bump SDK fixes** (backlog §9.1 – §9.6).

  Пять SDK gap'ов, обнаруженных после релиза 0.50/0.26 при интеграции workzilla-clone. Закрыты одной волной.

  ### 9.1 — native `type: "string" / int / float` → canonical control

  `inferControlType` на строке 65 возвращал `param.type` напрямую, без `mapOntologyTypeToControl`. Importer-generated + scaffold авторы, использующие `type: "string"` (Prisma/OpenAPI vocabulary), получали `unknown parameter control type: "string"` в `validateArtifact`.

  - `mapOntologyTypeToControl` принимает `string → "text"`, `int/integer → "number"`, `float/double/bigint → "number"`, `bool → "select"`.
  - `inferControlType` применяет маппинг к explicit `param.type`.

  ### 9.2 — `deriveProjections` auto `idParam` на detail

  Standalone detail-projections (R3) получали `idParam: undefined`. ArchetypeDetail без `idParam` не мог достать target из `routeParams` — click по item в list давал EmptyState.

  - `projections.<entity>_detail.idParam = "<entityLower>Id"` (convention).
  - Singleton R3b (`my_<entity>_detail.singleton:true`) остаются без idParam (их владелец — viewer).

  ### 9.3 — `onItemClick` routing предпочитает matching mainEntity

  При ≥2 outgoing `item-click` edges из list-projection SDK выбирал первый по алфавиту — часто wrong entity. `task_list.onItemClick` уходил в `response_detail` вместо `task_detail`.

  - Prefer edge, у которого `toProj.mainEntity === fromProj.mainEntity`; fallback — alphabetical first.

  ### 9.4 — `ArchetypeForm` header адаптер-aware

  Navigation bar (← Отмена / title / Создать) был hardcoded iOS-glass (`backdropFilter: blur(20px)`, SF blue `#007aff`, SF font). Для AntD/Mantine-хостов — визуально чужеродно.

  - `getAdaptedComponent("shell", "formHeader")` — если адаптер предоставил, используется.
  - `getAdaptedComponent("button", "primary/secondary")` — fallback к адаптерным кнопкам в neutral-header.
  - `@intent-driven/adapter-antd` добавил `AntdFormHeader` (AntD Button + typography).
  - Neutral native fallback через CSS-vars (`--idf-primary / --idf-border`).

  ### 9.5 — `ArchetypeForm` `projection.name` guard

  Bare `{projection.name}` в header крэшил `Cannot read properties of undefined (reading 'name')` когда host не передавал `projection` prop. Теперь: `projection?.name ?? parentCtx?.artifact?.name ?? ""`.

  ### 9.6 — core exports для host-authored flows

  Ранее synthesized projections (`generateCreateProjections`, `buildCreateFormSpec`) были internal — host мог получить только артефакт, не projection definition. Теперь доступны top-level:

  ```js
  import {
    generateCreateProjections,
    buildCreateFormSpec,
    mapOntologyTypeToControl,
    normalizeIntentNative,
    normalizeIntentsMap,
  } from "@intent-driven/core";
  ```

  Host может использовать для custom debug / inspector / form-derivation без повторного вызова `crystallizeV2`.

  ***

  **Тесты:** 11 новых integration (`workzillaPostBump.test.js`). Core: 1170 → 1181.

## 0.50.0

### Minor Changes

- 6e3942a: **Form-archetype синтезируется из insert-intent'ов** (Workzilla dogfood findings P0-2, backlog §8.2).

  Раньше `generateEditProjections` создавал синтетические `*_edit` projection'ы только для replace-intent'ов (detail-based). Insert-intent'ы (`creates: X` / `α:"add"`) не получали form-проекции — автор/скаффолд был вынужден писать `{entity}_create` руками, иначе action-button «Создать задачу» в каталоге открывал пустоту.

  **Core:** `generateCreateProjections(INTENTS, PROJECTIONS, ONTOLOGY)` — scan'ит INTENTS по `intent.creates`, для каждого entity (первый insert-intent побеждает) создаёт:

  ```js
  {
    name: "Создать X",
    kind: "form",
    mode: "create",
    mainEntity: X,
    entities: [X],
    creatorIntent: <intentId>,
  }
  ```

  Author-override: если `PROJECTIONS["<entityLower>_create"]` уже существует — no-op. Вызывается в `crystallizeV2` entry рядом с `generateEditProjections`; результат мёржится в `allProjections` перед `absorbHubChildren`.

  **`buildCreateFormSpec`** строит fields из `intent.parameters` (после native-bridge normalize parameters array). Enrich: onto-label / onto.valueLabels для enum / required. SYSTEM_FIELDS (id / createdAt) пропускаются. Секционирование по `inferFieldRole` — тот же UX, что в edit-форме.

  **Renderer:** `ArchetypeForm` поддерживает `body.mode === "create"`:

  - Пропускает target-lookup (new row, не existing).
  - Initial values из `field.default` (или пустые).
  - Пропускает ownership check (owner проставляется сервером из viewer).
  - Save → `ctx.exec(creatorIntent, payload)` (вместо execBatch).
  - Button label → «Создать» (вместо «Сохранить»).

  Закрывает Workzilla acceptance: click по «Создать задачу» из catalog-creator-toolbar → переход на `task_create` → форма title/description/budget/categoryId/deadline.

- 6e3942a: **Native-format → legacy bridge в crystallizeV2** + новый stable pattern `catalog-action-cta` (Workzilla dogfood findings P0-1, backlog §8.1).

  Раньше native-format intent'ы (importer-postgres / -openapi / -prisma emit + scaffold-path авторы) не доходили до toolbar / item.intents в catalog и detail архетипах. Native-format:

  ```js
  {
    target: "Task",
    alpha: "replace",
    permittedFor: ["customer"],
    parameters: { id: { type: "string", required: true }, title: { type } },
    particles: { effects: [{ target: "Task", op: "replace" }] },
  }
  ```

  Crystallize_v2 читает `particles.entities`, `particles.effects[].α`, array-parameters — которых в native-format нет. Без них `appliesToProjection` проваливает intent как не-относящийся к mainEntity, `selectArchetype` возвращает null (нет `confirmation`), и UI-generation даёт пустой toolbar.

  **Нормализация `normalizeIntentsMap`** (`crystallize_v2/normalizeIntentNative.js`) применяется сразу после `sortKeys` в crystallizeV2 entry:

  1. `intent.target` → `particles.entities: ["<alias>: <Target>"]` (если пусто).
  2. `particles.effects[i].op` → `α` + target нормализация:
     - `op:"insert"` → `{α:"add", target: <plural lowercase>}`
     - `op:"replace"|"update"` → `{α:"replace", target: <lowercase>}`
     - `op:"remove"|"delete"` → `{α:"remove", target: <lowercase>}`
  3. `parameters: {obj}` → `parameters: [array]`.
  4. `particles.confirmation` инфер из α (только для native-intent'ов):
     - `α:"add"` → `"enter"` (composer / heroCreate)
     - `α:"replace"` → `"form"` если user-params > 0, иначе `"click"`
     - `α:"remove"` → `"click"`

  Normalization **additive-only**: legacy-intent'ы (с `particles.entities`, `effects[].α`) проходят через no-op. `normalizeIntentsMap` идемпотентно — повторный вызов возвращает тот же объект.

  **Stable pattern `catalog-action-cta`** (`patterns/stable/catalog/`):

  - Trigger: catalog с ≥1 replace-intent на mainEntity.
  - Apply: тагирует `body.item.intents` с `source:"derived:catalog-action-cta"` (matching/metadata — routing уже сделан в `assignToSlotsCatalog::isPerItemIntent`).
  - Назначение: формально фиксирует §8.1 acceptance + unlock Studio X-ray tracing.

  Stable-count: 31 → 32.

  **Интеграция-тесты:** `nativeScaffold.test.js` проверяет что Workzilla-like ontology (native-format intents) производит populated `item.intents` + `toolbar`. Закрывает Workzilla acceptance "editTask / publishTask видны customer'у в task_list".

- 6e3942a: **catalog: `projection.witnesses[]` strict rendering на flat-list** (Workzilla dogfood findings P0-3, backlog §8.3).

  Раньше `projection.witnesses` учитывался только в grid-layout'е (через `buildCardSpec` / `grid-card-layout` pattern). Для flat-list catalog'ов `item.children` были hardcoded в `buildCatalogBody`: avatar + title + subtitle — независимо от того, что автор задекларировал.

  Теперь: если `projection.witnesses` непустой массив и `layout !== "grid"`, `item.children` генерируются из witnesses через `inferFieldRole`:

  - `title` → `{ type: "text", style: "heading" }`
  - `money`/`price` → `{ type: "text", format: "currency", style: "money" }`
  - `badge` (status/enum/condition) → `{ type: "badge" }`
  - `heroImage` → `{ type: "avatar", size: 40 }` (уходит в row-left)
  - `timer`/`deadline` → `{ type: "timer" }` (inline countdown)
  - `timestamp`/`scheduled`/`occurred` → `{ type: "text", format: "datetime" }`
  - `metric` → `{ type: "text", format: "number" }`
  - `description` → `{ type: "text", style: "secondary" }`
  - `location`/`address`/`zone` → `{ type: "text", style: "secondary" }`
  - fallback → `{ type: "text" }`

  **Renderer:** `Text` primitive расширен: `format: "currency"` → `n.toLocaleString("ru") + " ₽"`. `STYLE_PRESETS` получил `money` (teal weight 600). Полное vocabulary (`money-positive`/`money-negative`/`badge-*`) — на 8.5.

  **Back-compat:** `projection.witnesses` пустой или не задан → legacy avatar+title+subtitle fallback. Grid-layout по-прежнему идёт через `buildCardSpec` (не заменяется).

  Закрывает Workzilla-clone acceptance «`task_list.witnesses = ["title","budget","deadline","status"]` → card показывает 4 поля корректным primitive'ом».

## 0.49.0

### Minor Changes

- 434de69: patterns/optimistic-replace-with-undo: добавлен `structure.apply(slots, context)`.

  Паттерн triggers на ≥3 replace-click intents без `irreversibility:high/extreme` и emit'ит `{type:"undoToast", intentId, inverseIntentId, windowSec, message?}` в `slots.overlay` для каждого candidate'а с явным `inverseIntent` / `antagonist`. Shape совместим с `undo-toast-window` — primitive `UndoToast` (renderer) рендерит обе формы одинаково; overlap между паттернами защищён идемпотентностью по `intentId`. Respects `projection.undoToast === false`, `intent.undoable === false`, overrides `intent.undoWindowSec` / `projection.undoWindowSec` (default 5 сек).

  После этого изменения остаются 2 matching-only паттерна из 31 stable: `global-command-palette`, `keyboard-property-popover`.

## 0.48.0

### Minor Changes

- a17e236: Role-aware projection filtering — `projection.forRoles` (backlog §4.9).

  Projection author декларирует `forRoles: ["customer", "executor"]` — проекция
  видима только viewer'ам с матчинг-активной ролью. Без `forRoles` — видима всем
  (backward-compat).

  **API:**

  - `filterProjectionsByRole(ids, projections, activeRole)` → filtered ids
  - `isProjectionAvailableForRole(projection, activeRole)` → boolean
  - `partitionProjectionsByRole(ids, projections, activeRole)` → `{visible, hidden}`

  **Freelance применение:** `my_tasks` → `forRoles:["customer"]`, `my_responses` →
  `forRoles:["executor"]`, `my_deals` / `wallet` → `forRoles:["customer","executor"]`.
  Host-сайд (V2Shell) применяет filter к `domain.ROOT_PROJECTIONS` после смены
  `activeRole` — tab-bar реально меняется.

  **Семантика:** declarative visibility над `activeRole` (session-scoped), не над
  `role.base` (permission-scoped). Role.base фильтрует что viewer может делать;
  forRoles фильтрует что viewer сейчас видит в nav.

## 0.47.1

### Patch Changes

- 8a43a88: feat(patterns): formalization apply для 5 patterns (hero-create, phase-aware-primary-cta, antagonist-toggle, composer-entry, inline-search).

  **Формализационный batch** — apply'и, которые не изменяют SDK-логику маршрутизации intent'ов в слоты (она уже реализована в `assignToSlots*` + archetypes), а **маркируют** результирующие slot-элементы с `source: "derived:<pattern-id>"`. Renderer может opt-in в pattern-specific styling.

  Плюс `antagonist-toggle` эмитит `slots._toggles: [{a, b}]` — ранее этой metadata не было, пары ранее коллапсились только в overflow.

  ### hero-create

  `slots.hero[heroCreate].source = "derived:hero-create"` (формализация existing SDK catalog heroCreate routing).

  ### phase-aware-primary-cta

  `slots.primaryCTA[*].source = "derived:phase-aware-primary-cta"` (формализация existing SDK detail phase-transition routing). Не перетирает уже-заданный source.

  ### antagonist-toggle

  Новая metadata `slots._toggles = [{ a: "pin_message", b: "unpin_message" }, ...]` — pair detection via `intent.antagonist` cross-reference. Renderer может объединить пару в single toggle-button вместо двух отдельных. Dedup (A→B и B→A → одна запись).

  ### composer-entry

  `slots.composer.source = "derived:composer-entry"` (формализация feed composerEntry archetype).

  ### inline-search

  `slots.toolbar[inlineSearch].source = "derived:inline-search"` (формализация projection-level search archetype).

  ## Тесты

  - hero-create: 4 теста
  - phase-aware-primary-cta: 3 теста
  - antagonist-toggle: 6 тестов
  - composer-entry: 3 теста
  - inline-search: 3 теста
  - **Итого**: +19 core тестов. **1038 passing**.

  ## Roadmap progress

  Было 8 → **3** оставшихся stable patterns без apply (13/16 = 81%).

  Осталось:

  - `optimistic-replace-with-undo` (behavior-only, сложно encode declaratively)
  - `keyboard-property-popover` (renderer-heavy — новый primitive)
  - `global-command-palette` (renderer-heavy — ⌘K overlay)

  Тип `patch` — формализация не меняет runtime behavior, только маркирует metadata для renderer.

## 0.47.0

### Minor Changes

- e5262ec: feat(patterns): `structure.apply` для `vote-group` + `discriminator-wizard`.

  ## vote-group.apply

  Находит intents с `creates: "Entity(variant)"`, группирует по base entity, emit'ит `slots._voteGroups`:

  ```js
  slots._voteGroups = {
    Vote: [
      { intentId: "vote_yes", value: "yes", label: "Да" },
      { intentId: "vote_no", value: "no", label: "Нет" },
      { intentId: "vote_maybe", value: "maybe", label: "Может быть" },
    ],
    _source: "derived:vote-group",
  };
  ```

  Только группы с ≥2 variants попадают (singletons исключаются). Renderer (SubCollectionItem / ArchetypeDetail) может заменить дубль intent-buttons единой voteGroup-виджет.

  ## discriminator-wizard.apply

  Находит entity с discriminator field (`type` / `provider` / `kind` / `category`) с ≥2 select-options + create-intent, emit'ит `slots._wizardCandidates`:

  ```js
  slots._wizardCandidates = [
    {
      discriminatorField: "type",
      variants: ["hive", "iceberg", "kafka"],
      creatorIntentId: "create_catalog",
      source: "derived:discriminator-wizard",
    },
  ];
  ```

  Renderer может заменить обычный FormModal на multi-step wizard (step 1 — выбор variant, step 2+ — variant-specific поля).

  **Scope**: metadata-only в обоих apply. Полноценная UI-интеграция (voteGroup-widget, wizard-overlay) — future renderer work.

  ## Тесты

  - `vote-group.test.js` — **7 тестов**.
  - `discriminator-wizard.test.js` — **8 тестов**.
  - **1034 core** passing (+15).

  ## Roadmap progress

  Было 10 → **8** оставшихся stable patterns без apply (8/16 за 5 PR — **half way**).

## 0.46.0

### Minor Changes

- d6281b7: feat(patterns): `hierarchy-tree-nav.apply` + TreeNav primitive.

  ## core

  Pattern apply обходит ontology по FK-цепочке от `mainEntity` (BFS, depth-limit 5), строит tree metadata и prepend'ит `treeNav`-node в `slots.sidebar`:

  ```js
  sidebar: [
    {
      type: "treeNav",
      root: "Metalake",
      levels: [
        { depth: 0, entity: "Metalake", children: ["Catalog"] },
        { depth: 1, entity: "Catalog", children: ["Schema"] },
        { depth: 2, entity: "Schema", children: ["Table"] },
        { depth: 3, entity: "Table", children: [] },
      ],
      source: "derived:hierarchy-tree-nav",
    },
    // ...existing sidebar nodes
  ];
  ```

  Trigger (pattern spec): ≥3 уровня FK-цепочки (`mainEntity` → child → grandchild). Apply делает defensive-check — требует минимум 2 уровня.

  **Idempotent**: если `sidebar[0].type === "treeNav"` — no-op.

  ## renderer

  Новый primitive `TreeNav` (зарегистрирован в `PRIMITIVES.treeNav`):

  - Вертикальный список entities с `paddingLeft: depth * 14px` (визуальная иерархия).
  - Heading «Иерархия» + entity labels + counter для children.
  - Click по узлу → `ctx.navigate(<entity>_list, {entity})` с fallback на `<entity>_detail`.
  - Accessibility: `<nav aria-label="Hierarchy">` + `<button role="tab">`.

  Пока что: schema-preview (рендерит структуру, не runtime instances). Полноценная tree-навигация с expand/collapse по instances — future primitive.

  ## Тесты

  - `hierarchy-tree-nav.test.js` — **7 тестов** (BFS chain, idempotency, depth limit, witness).
  - `TreeNav.test.jsx` — **7 тестов** (render, padding, counter, navigation, aria).
  - **1026 core / 265 renderer** passing.

  ## Roadmap progress

  Было 11 → **10** оставшихся stable patterns без apply (6/16 за 4 PR).

## 0.45.0

### Minor Changes

- fd4e550: feat(patterns): `kanban-phase-column-board.apply` + renderer KanbanBoard.

  ## core

  Pattern apply устанавливает `slots.body.layout = { type: "kanban", columnField, columns, source }` на основе enum-options status-поля mainEntity.

  **Triggers**: entity с status-field ≥3 options + ≥1 replace-intent на `<entity>.status`.

  **Author-override**: существующий `body.layout` (string `"grid"` или object) → apply skip.

  ```js
  // Derived output:
  slots.body = {
    type: "list",
    source: "orders",
    layout: {
      type: "kanban",
      columnField: "status",
      columns: [
        { id: "draft", label: "Черновик" },
        { id: "active", label: "Активные" },
        { id: "done", label: "Готово" },
      ],
      source: "derived:kanban-phase-column-board",
    },
  };
  ```

  Поддерживает два формата options: `["draft", "active", ...]` (strings) и `[{value, label}, ...]` (objects). Также `entity.statuses` legacy shape.

  ## renderer

  `List` primitive детектит `layout.type === "kanban"` → рендерит `<KanbanBoard>` вместо обычного list/grid:

  - Горизонтальный flex-контейнер с `overflow-x: auto`.
  - Каждая колонка (flex-basis 260px) содержит заголовок (label + счётчик items) + список cards.
  - Items группируются по `item[columnField]`; unmatched items → последняя колонка.
  - Per-item `onItemClick` работает как обычно (navigate).
  - `data-column` атрибуты для e2e/test selectors.

  **Drag-to-replace-status** — TODO (HTML5 drag API + ctx.exec на replace-intent). Сейчас — group + click, без drag.

  ## Тесты

  - Core: **8 тестов** (`kanban-phase-column-board.test.js`).
  - Renderer: **6 тестов** (`listKanban.test.jsx`).
  - **1026 core / 264 renderer** passing.

  ## Roadmap progress

  Было 12 → **11** оставшихся stable patterns без apply.

## 0.44.0

### Minor Changes

- 50b1b8f: feat(patterns): `structure.apply` для `lifecycle-locked-parameters` + `bulk-action-toolbar`.

  ## lifecycle-locked-parameters.apply

  Если entity имеет status-field с active-state (active/running/confirmed/captured/accepted), добавляет секцию `lockedParameters` в `slots.sections`:

  ```js
  {
    id: "lockedParameters",
    title: "Параметры после активации",
    kind: "lockedParameters",
    entity: "Subscription",
    lockedWhen: "item.status === 'active'",
    fields: ["maxAmount", "scope", "endDate"],
    explainer: "Эти параметры фиксируются при активации и не могут быть изменены.",
    source: "derived:lifecycle-locked-parameters",
  }
  ```

  Locked-fields = targets всех replace-intents на `mainEntity.*` (кроме `status`). Renderer читает `lockedWhen`-выражение в runtime и переключает read-only view при match.

  ## bulk-action-toolbar.apply

  Если в intents ≥2 `bulk_*`-prefix id, добавляет `slots._bulkMode` metadata:

  ```js
  {
    enabled: true,
    actions: ["bulk_archive", "bulk_mark_read"],
    source: "derived:bulk-action-toolbar",
  }
  ```

  Renderer при `selection.length ≥ 1` активирует multi-select mode и показывает toolbar-bar с кнопками из `actions`.

  Idempotent (existing `_bulkMode` не перезаписывается). Pure function.

  ## Тесты

  - lifecycle-locked-parameters: 6 тестов (active-status detection + locked fields + idempotency + section preservation).
  - bulk-action-toolbar: 5 тестов (≥2 bulk intents + id-prefix filter + idempotency).
  - **1008 core passing** (+11 от 997).

  ## Прогресс roadmap

  Было 14 оставшихся stable patterns без apply (после PR #134); стало **12**.

  Следующие кандидаты: `vote-group`, `inline-search`, `antagonist-toggle`, `hero-create`, `phase-aware-primary-cta`, `composer-entry` (formalizaция), `discriminator-wizard`, `hierarchy-tree-nav`, `optimistic-replace-with-undo`, `keyboard-property-popover`, `global-command-palette`, `kanban-phase-column-board`.

## 0.43.0

### Minor Changes

- 0cf2da8: feat(patterns): `structure.apply` для `m2m-attach-dialog` + `observer-readonly-escape`.

  Два новых pattern.apply из roadmap «оставшиеся 17 stable patterns».

  ## m2m-attach-dialog.apply

  Находит assignment-entity с FK на `mainEntity` в ontology, добавляет секцию в `slots.sections`:

  ```js
  {
    id: "m2m_assignment",
    title: "Связанные Advisors",
    kind: "attachList",
    entity: "Assignment",
    foreignKey: "portfolioId",
    otherField: "advisorId",
    otherEntity: "Advisor",
    attachControl: { type: "attachDialog", multiSelect: true, otherEntity: "Advisor" },
    source: "derived:m2m-attach-dialog",
  }
  ```

  Renderer primitive `attachList` + `attachDialog` — TODO отдельный renderer PR. Текущий fallback — универсальный SlotRenderer (section rendering через card-children).

  Author-override: `projection.subCollections` (authored) → apply skip'ает.

  ## observer-readonly-escape.apply

  Если viewer имеет `role.base === "observer"` и ≥1 high-irreversibility intent в его `canExecute`, prepend'ит `readonlyBanner`-node в `slots.header`:

  ```js
  {
    type: "readonlyBanner",
    role: "auditor",
    escapeIntentIds: ["file_dispute", "flag_anomaly"],
    source: "derived:observer-readonly-escape",
  }
  ```

  Signal: "viewer-only mode с escape-путём" — renderer показывает badge "только просмотр" + inline reference на escape intents. Idempotent.

  ## Тесты

  - m2m: 8 тестов (trigger + apply + edge cases).
  - observer: 7 тестов (trigger + apply + idempotency).
  - **1013 core passing** (+16 от 997).

  ## Прогресс roadmap

  Было 16 оставшихся stable patterns без apply; стало 14.

## 0.42.0

### Minor Changes

- b558795: feat(projection): `projection.hero` — authored node (UI-gap #9, Workzilla-style horizontal banner).

  До: `slots.hero` инициализировался пустым массивом; только `heroCreate`-archetype intent'ы могли туда попасть. Авторский `projection.hero: {...}` полностью игнорировался.

  После: авторский hero-node (или array) кладётся в `slots.hero` **первым**. Если authored присутствует, `heroCreate` fallback'ит через стандартную логику (в toolbar как `intentButton`), потому что `slots.hero.length === 0` guard больше не срабатывает.

  ```js
  projection = {
    kind: "catalog",
    mainEntity: "Task",
    hero: {
      type: "carousel",
      slides: [
        {
          eyebrow: "Наши преимущества",
          title: "Новое задание каждые 28 секунд",
        },
        { eyebrow: "Безопасность", title: "Escrow на сумму сделки" },
      ],
    },
  };
  ```

  Принимает:

  - **Object** (single node) — обёртка в `[node]`.
  - **Array** — pass-through as-is.
  - **`null` / `undefined`** — legacy `hero:[]` initialized.

  ### Author-override precedence (catalog hero slot)

  `projection.hero` authored → всегда побеждает; `heroCreate` creator-intent fallback'ит в toolbar.

  ### Тесты

  +5 `assignToSlotsCatalog.hero.test.js` (1003 passing).

  ### Применение

  Freelance `task_catalog_public.hero: { type: "carousel", slides: [...] }` — horizontal rotating banner над списком задач (workzilla-скриншот 4 «Наши преимущества — Новое задание каждые 28 секунд»). До фикса carousel приходилось размещать в sidebar'е как обходной путь.

## 0.41.0

### Minor Changes

- 86ad499: Pattern Bank: curated-кандидаты — 8 researcher-паттернов из profi+avito field
  research (2026-04-17-18), прошедшие human review и строгий `validatePattern`
  (включая `falsification.shouldMatch`/`shouldNotMatch`). Matching-ready, без
  `structure.apply`.

  Новые файлы в `packages/core/src/patterns/candidate/{catalog,detail,feed}/`:
  category-tree-with-counter + paid-promotion-slot (merged profi+avito),
  map-filter-catalog, reputation-level-badge, rating-aggregate-hero,
  review-criterion-breakdown, direct-invite-sidebar, response-cost-before-action.

  Экспорт `CURATED_CANDIDATES` (отдельно от manifest-свалки 127+). Общий
  `CANDIDATE_PATTERNS` теперь union: сначала curated (строгая схема),
  потом manifest (schema-lax). `loadCandidatePatterns` регистрирует обоих,
  first-wins при коллизии id. Default registry по-прежнему stable-only —
  candidate остаются explicit opt-in.

  Promotion в stable + `structure.apply` — отдельный sub-project.

- 86ad499: Promote response-cost-before-action candidate → stable с полной реализацией.

  Cross-archetype (catalog/detail/feed — везде с toolbar). `apply` обогащает label
  toolbar-item'ов суффиксом ' · {cost} {currency}' для intent'ов с `costHint`
  (top-level, `meta`, или `particles`). Idempotent, детерминирован.

  Anti-footgun для monetized actions: 'Откликнуться · 80 ₽', 'Boost · 99 ₽'.
  Cost transparent до клика — закрывает profi/workzilla/youdo UX-gap.

  Modal-confirm force отложено (требует pipeline refactor).

## 0.40.0

### Minor Changes

- 59715cd: Pattern Bank: curated-кандидаты — 8 researcher-паттернов из profi+avito field
  research (2026-04-17-18), прошедшие human review и строгий `validatePattern`
  (включая `falsification.shouldMatch`/`shouldNotMatch`). Matching-ready, без
  `structure.apply`.

  Новые файлы в `packages/core/src/patterns/candidate/{catalog,detail,feed}/`:
  category-tree-with-counter + paid-promotion-slot (merged profi+avito),
  map-filter-catalog, reputation-level-badge, rating-aggregate-hero,
  review-criterion-breakdown, direct-invite-sidebar, response-cost-before-action.

  Экспорт `CURATED_CANDIDATES` (отдельно от manifest-свалки 127+). Общий
  `CANDIDATE_PATTERNS` теперь union: сначала curated (строгая схема),
  потом manifest (schema-lax). `loadCandidatePatterns` регистрирует обоих,
  first-wins при коллизии id. Default registry по-прежнему stable-only —
  candidate остаются explicit opt-in.

  Promotion в stable + `structure.apply` — отдельный sub-project.

- 59715cd: Promote review-criterion-breakdown candidate → stable с полной реализацией.

  **core:** новый stable pattern (detail archetype) с `structure.apply` — prepend'ит
  в `slots.sections` section `{type: "criterionSummary", subEntity, fkField, criteria,
title}`. Trigger: detail-проекция с sub-entity имеющим ≥3 criterion-полей
  (_\_rating / _\_score суффикс, whitelist quality/punctuality/..., или
  fieldRole:"rating"). Убран из curated candidate bank.

  **renderer:** новый primitive `CriterionSummary` — runtime compute avg по каждому
  criterion'у из `world[pluralized(subEntity)]`. Horizontal bar-chart с auto-scale
  (5 vs 10). Зарегистрирован в `PRIMITIVES.criterionSummary`.

## 0.39.0

### Minor Changes

- bb8f26f: feat(projection): `projection.gating` — onboarding prerequisites (UI-gap #6, Workzilla-style).

  Декларация шагов к разблокировке проекции — рендерятся как GatingPanel с step-cards и CTA-кнопками. Когда все steps done — panel скрывается автоматически.

  ```js
  projection = {
    kind: "catalog",
    mainEntity: "Task",
    gating: {
      title: "Необходимые шаги для доступа к заданиям",
      steps: [
        {
          id: "registration",
          label: "Регистрация",
          icon: "👤",
          done: "viewer.registered === true",
        },
        {
          id: "test",
          label: "Обязательное тестирование",
          icon: "📝",
          done: "viewer.testPassed === true",
          cta: { label: "Пройти", intentId: "start_test" },
        },
      ],
    },
  };
  ```

  ### Step lifecycle

  - `done` (string condition) — evaluated через `evalCondition({viewer, world})`.
    - `true` → зелёная плашка «✓ Пройдено».
    - `false` + `cta` → primary-button с `cta.label` → click = `ctx.exec(cta.intentId, cta.params)`.
    - `false` без cta → muted «Не выполнено».
  - Все steps done → panel **не рендерится** (return null).

  ### CTA handler

  - `cta.intentId` → `ctx.exec(intentId, cta.params || {})`.
  - `cta.onClick(ctx)` → custom handler (host-specific flows, вне Φ).

  ### Изменения

  **core (`assignToSlotsCatalog.js`)**: `projection.gating` → `slots.gating` как gatingPanel-node (или null).

  **renderer (`primitives/GatingPanel.jsx`)**: новый primitive в `PRIMITIVES.gatingPanel`. Adaptive grid `auto-fit, minmax(240px, 1fr)` для step-card'ов.

  **renderer (`ArchetypeCatalog.jsx`)**: рендерит `slots.gating` между hero и body (визуально — «шаги над списком задач», как в workzilla).

  ### Тесты

  - Core: +3 `assignToSlotsCatalog.gating.test.js` (971 passing).
  - Renderer: +9 `GatingPanel.test.jsx` (231 passing).

  ### Применение

  Workzilla-скриншот 4: «Сейчас размещено 1660 заданий» над списком, а выше — `Регистрация ✓ Пройдено / Обязательное тестирование [Пройти]` gating-panel.

## 0.38.0

### Minor Changes

- a7e6aef: feat(projection): `projection.sidebar` — static-content блоки слева от catalog (UI-gap #2, Workzilla-style).

  Авторы декларируют колонку static-content блоков (tutorial / promo / examples) рядом с catalog body.

  ```js
  projection = {
    kind: "catalog",
    mainEntity: "Task",
    sidebar: [
      {
        type: "card",
        children: [
          { type: "heading", content: "Как поручить задание?" },
          { type: "image", src: "/tutorial.svg" },
          { type: "text", content: "Посмотрите короткое видео." },
        ],
      },
      {
        type: "card",
        children: [
          { type: "text", content: "Подарите другу и себе по 100 ₽" },
          { type: "text", style: "muted", content: "Приведи друга..." },
        ],
      },
      {
        type: "column",
        children: [
          { type: "heading", content: "Примеры заданий", size: "sm" },
          { type: "text", content: "Расчистить балкон — 3500 ₽, Москва" },
          { type: "text", content: "Заехать в любой магазин..." },
        ],
      },
    ],
  };
  ```

  ### Изменения

  **core (`assignToSlotsCatalog.js`):** `projection.sidebar` → `slots.sidebar: []` pass-through. Non-array value graceful-падает в пустой массив.

  **renderer (`ArchetypeCatalog.jsx`):** рендерит `<aside>` колонку шириной 260px слева от content area (hero + body), когда `slots.sidebar` не пустой. `aria-label="Боковая панель"`. Background через `--idf-surface-soft` / `--idf-surface` fallback + borderRight `--idf-border`.

  ### Тесты

  - Core: +3 теста `assignToSlotsCatalog.sidebar.test.js` (971 passing).
  - Renderer: +3 теста `CatalogSidebar.test.jsx` (225 passing).

  ### Применение

  Workzilla-style (скриншот 1): слева "Как поручить задание?" (tutorial-card с play-button), "Подарите другу и себе по 100 рублей" (promo-card), "Примеры заданий" (list карточек задач). Все static-content, не intent-driven.

## 0.37.0

### Minor Changes

- 893d43f: feat(projection): `projection.tabs` — filter-views как табы над catalog (UI-gap #1, Workzilla-style).

  Автор декларирует несколько filter-вариантов на одной projection'е; renderer показывает tab-bar над списком, клик переключает активный фильтр.

  ```js
  projection = {
    kind: "catalog",
    mainEntity: "Task",
    filter: "item.customerId === viewer.id", // базовый фильтр (применяется первым)
    tabs: [
      { id: "new", label: "+ Новое", filter: "item.status === 'draft'" },
      { id: "open", label: "Открытые", filter: "item.status === 'published'" },
      {
        id: "history",
        label: "История",
        filter: { field: "status", op: "in", value: ["closed", "completed"] },
      },
    ],
    defaultTab: "open", // опц., иначе первый
  };
  ```

  ### Изменения

  **core (`assignToSlotsCatalog.js`):** `projection.tabs` → `body.tabs: [{id, label, filter}]` + `body.defaultTab`. Нормализация dropping entries без id, пустой массив даёт `tabs:undefined` (back-compat). Filter поддерживает и structured object (R7b/R10 формат), и legacy string-expression.

  **renderer (`primitives/containers.jsx` → `List`):**

  - Локальный `useState(activeTabId)` — per-List tab state (не глобальный).
  - Composition: `node.filter` (base) применяется первым, `activeTab.filter` — поверх (semantic AND).
  - Новый inline `TabBar` sub-component с `role="tablist"` / `aria-selected` для accessibility.

  ### Тесты

  - Core: +5 тестов `assignToSlotsCatalog.tabs.test.js` (927 passing).
  - Renderer: +6 тестов `listTabs.test.jsx` (191 passing).

  ### Использование в freelance

  ```js
  my_task_list: {
    witnesses: ["title", "status", "budget", "deadline"],
    tabs: [
      { id: "draft",     label: "+ Новое",  filter: "item.status === 'draft'" },
      { id: "open",      label: "Открытые", filter: "item.status === 'published'" },
      { id: "completed", label: "История",  filter: { field: "status", op: "in", value: ["closed", "completed"] } },
    ],
    defaultTab: "open",
  },
  ```

## 0.36.0

### Minor Changes

- 3101243: Pattern Bank: curated-кандидаты — 8 researcher-паттернов из profi+avito field
  research (2026-04-17-18), прошедшие human review и строгий `validatePattern`
  (включая `falsification.shouldMatch`/`shouldNotMatch`). Matching-ready, без
  `structure.apply`.

  Новые файлы в `packages/core/src/patterns/candidate/{catalog,detail,feed}/`:
  category-tree-with-counter + paid-promotion-slot (merged profi+avito),
  map-filter-catalog, reputation-level-badge, rating-aggregate-hero,
  review-criterion-breakdown, direct-invite-sidebar, response-cost-before-action.

  Экспорт `CURATED_CANDIDATES` (отдельно от manifest-свалки 127+). Общий
  `CANDIDATE_PATTERNS` теперь union: сначала curated (строгая схема),
  потом manifest (schema-lax). `loadCandidatePatterns` регистрирует обоих,
  first-wins при коллизии id. Default registry по-прежнему stable-only —
  candidate остаются explicit opt-in.

  Promotion в stable + `structure.apply` — отдельный sub-project.

## 0.35.0

### Minor Changes

- cfb2d64: feat(projection): `projection.emptyState` — richer empty-state для catalog (UI-gap #8, Workzilla-style).

  До: catalog рендерил дефолтный `"Пусто"` mute-текст при пустом результате.
  После: автор декларирует богатый empty-state:

  ```js
  projection = {
    kind: "catalog",
    mainEntity: "Task",
    emptyState: {
      illustration: "/empty-tasks.svg", // URL или React-нода
      title: "У вас пока нет заданий",
      hint: "Ваши открытые задания появятся здесь",
      cta: { label: "Дать задание", intentId: "create_task_draft" },
    },
  };
  ```

  ### Изменения

  **core (`assignToSlotsCatalog.js`):** если `projection.emptyState` объявлен, оборачивает его в `{ type: "emptyState", ...emptyState }` и кладёт в `slots.body.empty`. Без поля остаётся default text `"Пусто"`.

  **renderer (`primitives/EmptyState.jsx`):**

  - Зарегистрирован в `PRIMITIVES` dispatch table под ключом `emptyState` (SlotRenderer теперь распознаёт `{ type: "emptyState", ... }`).
  - Расширен полями `illustration` (URL или React-нода — крупная картинка над title) и `cta` (primary-кнопка: `{ label, intentId, onClick?, params? }`).
  - Dual-mode: primitive invocation (SlotRenderer передаёт `node={...}`) + legacy flat props (`<EmptyState title="..." />`) — back-compatible.
  - CTA click → `ctx.exec(intentId, params)` если intentId задан; `onClick(ctx)` для custom handler'ов.

  ### Тесты

  - Core: +3 теста `assignToSlotsCatalog.test.js` (925 passing).
  - Renderer: +6 тестов `EmptyState.test.jsx` (181 passing).

  ### Использование в доменах

  ```js
  // freelance/projections.js
  task_catalog_public: {
    // ...
    emptyState: {
      illustration: "/images/empty-market.svg",
      title: "Нет подходящих задач",
      hint: "Измените фильтр или вернитесь позже",
    },
  },

  // my_task_list (customer view на пустой state)
  my_task_list: {
    emptyState: {
      title: "У вас пока нет заданий",
      hint: "Ваши открытые задания появятся здесь",
      cta: { label: "Дать задание", intentId: "create_task_draft" },
    },
  },
  ```

## 0.34.0

### Minor Changes

- 2cabdb7: feat(assignToSlotsDetail): `projection.toolbar` whitelist honored в crystallize (author-override поверх pattern-bank routing).

  До: `projection.toolbar: [intentIds]` нигде не читался SDK'ом — поле игнорировалось. Intents auto-placement-ились SDK pattern-matching'ом:

  - phase-transition без params + `irreversibility !== "high"` → `primaryCTA`;
  - single-replace-on-main-field → `footer` (через `footer-inline-setter` pattern);
  - остальное — `toolbar` или `overflow`.

  После: если `projection.toolbar` включает `intentId`, intent **форсируется в toolbar**:

  - `assignToSlotsDetail` skip-ает `primaryCTA` routing для whitelisted intents (они идут через обычный `wrapByConfirmation` flow в toolbar);
  - `footer-inline-setter.structure.apply` skip-ает whitelisted intents (не переносит в footer, не strip-ит из toolbar).

  Author-override прецеденция (detail slot-routing): `footerIntents` → `toolbar` whitelist → автоматика.

  Обнаружено в freelance-домене (12-й полевой тест): `task_detail_customer` с `toolbar: [edit_task, publish_task, cancel_task_before_deal, select_executor]` показывал **пустой toolbar** в артефакте — phase-transition intents уходили в primaryCTA/footer, authored list игнорировался.

  Тесты: +5 (`assignToSlotsDetail.test.js` — 4 теста whitelist behavior; `footer-inline-setter.test.js` — 1 тест whitelist respect).

## 0.33.1

### Patch Changes

- fb47875: fix(deriveProjections): R7/R7b/R3b читает `entityDef.owners` с fallback к `ownerField`.

  `getOwnerFields` (в `ownershipConditionFor`) уже читал `owners` как новый API для multi-owner (backlog §3.2). Но `deriveProjections` (R7, R7b, R3b) читал только `ownerField`. Авторы онтологии, объявляющие `owners: ["customerId", "executorId"]` для `ownershipConditionFor` + `intent.permittedFor`, были вынуждены дублировать поле в `ownerField: [array]` чтобы R7b не отваливался.

  После фикса preference к `owners` (с fallback к `ownerField`) обеспечивает одну декларацию multi-owner на обе поверхности:

  ```js
  Deal: {
    owners: ["customerId", "executorId"],  // достаточно
    // ownerField больше не требуется
  }
  ```

  Обнаружено при работе с freelance-доменом (12-й полевой тест), multi-owner Deal + `intent.permittedFor` на phase-transitions.

## 0.33.0

### Minor Changes

- 4ebff4d: feat(materializers): `materializeAuditLog(phi, filter)` + `buildAuditContext(...)`.

  `materializeAuditLog` — чистая функция над `Φ = { effects }`, фильтрует confirmed effects по actor / timeRange / intentTypes / entityKind / entityId / ruleId и обогащает полями из `effect.context.__audit`. Основа audit-view для observer-role.

  `buildAuditContext({intent, actor, timestamp, ruleIds?, evidenceIds?, witnessChain?})` — helper для host-валидатора: строит канонический блок `__audit` с fnv1a-hash над canonical-json всего контента (стабильный cross-reference identifier, не криптоподпись).

  Use-case: 13-й полевой тест compliance-домен (SOX ICFR). Audit trail как derived view над Φ (не отдельная материализованная сущность), reader-equivalence с document/voice materializer'ами.

## 0.32.0

### Minor Changes

- d288926: feat(invariants): `expression`-kind передаёт predicate не только `row`, но и `world` / `viewer` / `context`.

  Backward-compat: существующие predicate-функции с single-arg `(row) => ...` продолжают работать (JS игнорирует лишние аргументы). Новые cross-entity сценарии — как `(row, world) => world.entities.find(...)` — теперь выражаются декларативно без выхода в domain-effects.

  Expression-строки также получают `world`, `viewer`, `context` как явные имена наряду с `with (row)` (поля row — bare identifiers, back-compat).

  Use-case: 13-й полевой тест compliance-домен (SOX ICFR) — invariants типа «CFO не подписывает cycle, содержащий собственный JE» и «threshold-approvals dynamic по amount» теперь выражаются одним expression-инвариантом вместо ручного enforce'а в buildEffects.

## 0.31.2

### Patch Changes

- 0fb39cb: fix(validateArtifact): composer не обязателен для feed

  `feed` архетип требовал `slots.composer` в `REQUIRED_SLOTS_BY_ARCHETYPE` в
  двух местах (core + renderer). Это наследство messenger chat-кейса: там
  composer всегда присутствует через `send_message` intent с
  `confirmation:"enter" + creates:"Message"`.

  Для любого другого feed (R11 temporal public, R11 v2 owner-scoped, любая
  read-only временная лента) intent'а с `confirmation:"enter"` нет,
  `assignToSlotsFeed` возвращает `slots.composer = null`, и validateArtifact
  блокирует рендер.

  Это ломало:

  - `reflect.insights_feed` (authored) — уже упало в main до этой правки,
    но было не замечено, т.к. reflect редко открывался в браузере.
  - `reflect.my_insight_feed` (R11 v2 derived, после активации в idf#70) —
    уже в artifacts, но не рендерился.

  Снято: `REQUIRED_SLOTS_BY_ARCHETYPE.feed = ["body"]`. Composer — особенность
  messenger chat, не инвариант feed-архетипа.

  Тесты: "требует composer для feed" заменён на "composer необязателен для feed".
  888/175 passed без регрессий.

## 0.31.1

### Patch Changes

- 9a5388c: Salience declaration-order tiebreak расширен на `assignToSlotsCatalog` (catalog/feed projections). Ранее был только в `assignToSlotsDetail` — catalog-toolbar'ы продолжали falling back to alphabetical.

  Теперь declarationOrder пробрасывается из `Object.entries(INTENTS)` index во все toolbar-push'ы catalog'а (creator, projection-level overlay, projection-level click, inlineSearch).

  Zero-breaking: backward-compat через Infinity fallback при missing declarationOrder. Existing 895 тестов passing без regression.

  Host impact: после bump → `sales/listing_feed` alphabetical-fallback witnesses (4 остаточных после host #70) должны упасть до 0.

## 0.31.0

### Minor Changes

- 4a2ef3e: Salience tie-break ladder: `declarationOrder` (authorial signal из порядка intents в `INTENTS`-object) как tier 1 tiebreaker между equal salience. Alphabetical остаётся tier 2 (last resort).

  Witness `basis: "declaration-order"` маркирует резолюцию tier 1 — less noisy чем alphabetical-fallback, не требует массовых intent.salience аннотаций. Author сразу закладывает приоритет порядком declarations: «ставлю важнее первым».

  API:

  - `bySalienceDesc(a, b)` — ladder: `salience desc → declarationOrder asc → alphabetical asc`
  - `classifyTieResolution(a, b)` — новый util, возвращает `"salience" | "declaration-order" | "alphabetical-fallback"`
  - `detectTiedGroups(sortedItems, ctx)` — различает basis исходя из declarationOrder uniqueness внутри tied-группы

  Breaking: нет. Все existing tests pass (895 core). Авторы без declaration-order получают alphabetical как раньше.

  `assignToSlotsDetail` пробрасывает `declarationOrder` в toolbar button specs.

## 0.30.0

### Minor Changes

- 0d9883e: Structured-filter: рендерер понимает формы из R3b / R7b / R10 / R11 v2

  Закрывает backlog 1.X / 1.Y: `projection.filter` от новых R-правил теперь
  интерпретируется в UI, а не игнорируется.

  **core**

  - Новый shared helper `evalFilter(filter, row, { viewer, world })` —
    единая surface для четырёх форматов, которые эмитят R-правила: - `string` — legacy JS-выражение (back-compat для messenger buildBody и
    authored viewState-фильтров); - `{ field, op, value }` — простой predicate (R3b singleton, R11 v2 feed),
    `value: "me.id"` резолвится через `viewer.id`; - `{ kind: "disjunction", fields, op, value }` — OR across полей
    (R7b multi-ownerField); - `{ kind: "m2m-via", via, viewerField, joinField, localField,
statusField?, statusAllowed? }` — bridge-lookup (R10 role.scope).
  - `documentMaterializer` и `voiceMaterializer` мигрированы на этот helper —
    теперь тоже корректно фильтруют structured-filter'ы (до этого упали бы
    в permissive fallback).
  - `assignToSlotsFeed::buildBody` обобщён: `mainEntity === "Message"` →
    прежний messenger chat template; любой другой `mainEntity` → generic
    `buildCatalogBody`, который прокидывает `projection.filter`/`sort`
    декларативно. Закрывает path для R11 v2 `my_*_feed`.

  **renderer**

  - `List` primitive (`primitives/containers.jsx`) различает object-filter
    и string-filter. Object → `evalFilter` (structured), string →
    `evalCondition` (legacy с viewState/query). R9 compositions применяются
    как раньше, до фильтра.
  - `ArchetypeDetail` поддерживает `projection.singleton: true` — target
    резолвится через `projection.filter` (без `idParam`), EmptyState адаптирован
    под «запись ещё не создана». Резолвер вынесен в `resolveDetailTarget`
    для unit-тестов.

  Новые тесты: `filterExpr.test.js` (19), `buildGenericFeed.test.js` (6),
  `listFilter.test.jsx` (6), `resolveDetailTarget.test.js` (8).

## 0.29.0

### Minor Changes

- ac0881a: feat(core): R11 v2 — owner-scoped temporal feed (my\_<entity>\_feed)

  Расширение R11: если entity имеет `temporal: true` **И** `ownerField` (string) —
  генерируется **дополнительно** `my_<entity>_feed` с owner-фильтром + temporal sort.

  Аналог отношения R1 → R7 для временных лент.

  ```js
  ontology.entities.Insight = {
    temporal: true,
    ownerField: "userId",     // R11 v2 trigger (в дополнение к temporal)
    fields: { userId: {...}, createdAt: {...} },
  };
  ```

  → derived (в дополнение к R1 catalog и public `insight_feed`):

  - **`my_insight_feed`** — kind:"feed", filter:{field:"userId", ...}, sort:"-createdAt"

  Witness `ruleId:"R11"` получает `input.ownerScoped: true` и `input.ownerField`.

  Constraints:

  - `entity.temporal === true` + `ownerField` (string) — оба нужны
  - Array ownerField (multi-owner) не триггерит — future R11b candidate (disjunction feed)
  - Fallback-friendly: base может быть R1 catalog ИЛИ R3 detail

  **Motivation**: reflect.insights_feed uncovered в baseline — authored feed с
  owner-filter. Public R11 не matched из-за filter-presence mismatch. Owner-scoped
  variant закрывает кейс.

  **Empirical trigger**: [DubovskiyIM/idf#64](https://github.com/DubovskiyIM/idf/pull/64) surfaced.

  Spec: `idf-manifest-v2.1/docs/design/rule-R11-temporal-feed-spec.md` (updated).

  **Backward compat**: zero breaking.

  - Entities с `temporal:true` без `ownerField` → поведение unchanged (только public feed).
  - Entities с `temporal:true` + string `ownerField` → **новая** capability.
  - Entities без `temporal` → не затронуты.

  Тесты: +2 в witnesses.test.js (positive owner-scoped, array ownerField
  не-триггер). **863/863 зелёные**, functoriality probe без регрессий.

## 0.28.0

### Minor Changes

- 75b96ae: feat(core): R11 — temporal feed rule

  Новое правило деривации. Entity с opt-in флагом `temporal: true` получает
  **дополнительную** проекцию `<entity>_feed` с `kind:"feed"` + `sort:"-<timestampField>"`.
  Применяется к event-like сущностям (Insight, Notification, Activity) —
  монотонно растущие append-only потоки.

  ```js
  ontology.entities.Insight = {
    temporal: true,                  // R11 trigger
    timestampField: "createdAt",     // опционально, default "createdAt"
    fields: { createdAt: {...}, ...},
  };
  ```

  → derived (дополнительно к R1 catalog):

  - `insight_feed` — kind:"feed", sort:"-createdAt"

  Witness `basis:"crystallize-rule"`, `ruleId:"R11"`.

  **Fallback-friendly**: при отсутствии R1 catalog, R11 использует R3 detail
  как base (тот же паттерн, что R7 v2). `sourceBase` записывается в witness
  input для trail-видимости.

  Constraints:

  - Требует `entity.temporal === true` — explicit opt-in
  - Нужен R1 catalog ИЛИ R3 detail как base
  - Generates `<entity>_feed` **в дополнение** к catalog, не заменяет

  **Mutual exclusion** с R2 (confirmation:"enter" + FK override): R2 и R11
  могут сосуществовать. R2 создаёт `<entity>_list` с kind:"feed" (замена
  catalog). R11 создаёт отдельный `<entity>_feed` key. Semantically R2 —
  messaging (scoped by parent conversation), R11 — event streams (глобально).

  **Motivation**: reflect.insights_feed uncovered в baseline — временная
  лента aналитических insights. Без R11 authored feed с sort:"-createdAt"
  писался вручную. После: `Insight.temporal = true` → автоматически.

  Spec: `idf-manifest-v2.1/docs/design/rule-R11-temporal-feed-spec.md` (draft).

  **Backward compat**: zero breaking. `entity.temporal` — новый opt-in флаг.
  Entities без этого флага — поведение unchanged.

  Тесты: +4 в witnesses.test.js (basic temporal feed, explicit
  timestampField override, no-flag negative, R3 detail fallback).
  **845/845 зелёные**, functoriality probe без регрессий.

  **Completes R-rule roadmap to 13 правил**:
  R1, R1b, R2, R3, R3b, R4, R6, R7 v2, R7b, R8, R9, R10, **R11**.

## 0.27.0

### Minor Changes

- 0c866a7: `filterWorldForRole` поддерживает `entity.owners[]` multi-owner — row visible когда совпадает любое из owner-полей с `viewer.id`. Новый export `getOwnerFields(entity, intent?)` в `crystallize_v2/ontologyHelpers.js` унифицирует resolve: `entity.owners[]` > `entity.ownerField` (legacy) > `[]`; с `intent.permittedFor` — subset фильтр.

  Backward compat: legacy single `ownerField` работает через тот же util (возвращает массив из одного). Существующие 10 доменов не требуют миграции.

  `assignToSlotsDetail.js` переиспользует новый util (удалена дубликатная inline `resolveOwnerFields`).

  Закрывает `docs/sdk-improvements-backlog.md` §3.2 полностью (ранее было реализовано только в `ownershipConditionFor` для detail toolbar, но не в viewer-scoping).

## 0.26.0

### Minor Changes

- c8b40cf: feat(core): R7/R7b independence from R1 — fallback на R3 detail

  Ослаблен precondition для R7 (owner-filtered catalog) и R7b (multi-owner
  disjunction): `my_<entity>_list` теперь генерируется когда существует
  **любой** base — R1 catalog **или** R3 detail (раньше требовался catalog).

  **Мотивация**: side-effect-created entities (Deal через accept*response,
  Assignment через invite_accepted, Order через checkout и т.д.) имеют
  mutators (edit/close/cancel) но не имеют `creates:E` intent'а. R3 detail
  выводится, R1 catalog — нет. До этого фикса R7 precondition проваливался,
  my*_*list не создавался, authored `my*_` projections оставались uncovered.

  **Priority**: если оба (catalog + detail) существуют — приоритет catalog
  для witnesses inheritance (стабильность). Только detail — R3 становится
  sole base.

  **Witnesses**: наследуются от того base, который доступен. Новое поле в
  input — `sourceBase`. `sourceCatalog` сохранён для backward compat.

  ```js
  ontology.entities.Deal = {
    ownerField: ["customerId", "executorId"],
    fields: {...},
  };

  intents = {
    accept_response: { particles: { effects: [
      { α: "replace", target: "response.status" },
      { α: "create",  target: "deal" },  // side-effect creation
    ]}},
    edit_deal:  {...},
    close_deal: {...},
  };

  // Было: deal_list undefined → my_deal_list undefined (R7b не срабатывал)
  // Стало: deal_detail derived (R3) → my_deal_list derived (R7b через fallback)
  //        filter: { kind: "disjunction", fields: [...], op: "=", value: "me.id" }
  ```

  Emprical motivation: [DubovskiyIM/idf#60](https://github.com/DubovskiyIM/idf/pull/60) surfaced этот blocker
  на freelance.Deal (ownerField uniquely array, но R1 не выведен).

  **Backward compat**: zero breaking.

  - Entities с R1 catalog + ownerField → unchanged (catalog остаётся
    primary base).
  - Entities без catalog но с detail + ownerField → **теперь** получают
    my\_\*\_list (новая capability).
  - Entities без base вовсе → не затронуты (nothing to filter).

  Тесты: +4 в `witnesses.test.js` (side-effect entity R7, side-effect R7b,
  catalog priority over detail, no-base negative). **841/841 зелёные**,
  functoriality probe без регрессий.

  Spec: `idf-manifest-v2.1/docs/design/debugging-derived-ui-spec.md`
  (R-rule evolution map).

## 0.25.0

### Minor Changes

- ddc222c: feat(core): R3b — singleton owner-scoped detail rule

  Расширение R3: когда entity объявлен `singleton: true` + `ownerField` (string,
  single-owner) — deriveProjections выводит **дополнительную** проекцию
  `my_<entity>_detail` без idParam, с owner-фильтром. Применяется к сущностям
  по одной записи на пользователя (Wallet, RiskProfile, UserSettings).

  ```js
  ontology.entities.Wallet = {
    ownerField: "userId",
    singleton: true,       // новый флаг
    fields: { userId: {...}, balance: {...} },
  };
  ```

  → derived:

  - `wallet_detail` — обычный R3 detail (base detail, требует idParam для admin)
  - `my_wallet_detail` — **новое**, R3b singleton: без idParam, filter
    `{field: "userId", op: "=", value: "me.id"}`, пометка `singleton: true`

  Witness basis:"crystallize-rule", ruleId:"R3b".

  **Constraints**:

  - R3b требует `entity.singleton === true` (явный opt-in)
  - `ownerField` должен быть string (не array — R3b не совместим с R7b multi-owner)
  - Base detail (R3) должен быть выведен — R3b зависит от R3

  **Motivation**: freelance.wallet uncovered в idf#58 baseline — detail с
  owner-filter как паттерн. Без R3b авторы пишут такие проекции вручную.

  Mutual exclusion с R7b: R3b требует single ownerField, R7b требует array
  ≥2. Общая логика: author выбирает single (R7+R3b) vs multi (R7b).

  Spec: `idf-manifest-v2.1/docs/design/rule-R3b-singleton-detail-spec.md`
  (draft).

  **Backward compat**: zero breaking. `entity.singleton` — новый opt-in флаг.
  Entities без этого флага ведут себя как раньше.

  Тесты: +4 в witnesses.test.js (positive singleton, negative без флага,
  negative с array ownerField, negative без base R3 detail).
  **837/837 зелёные**, functoriality probe без регрессий.

  Renderer: primitive для singleton-detail (без route-params) — отдельный
  follow-up в @intent-driven/renderer.

  Stacked on [#73](https://github.com/DubovskiyIM/idf-sdk/pull/73) (R7b).

## 0.24.0

### Minor Changes

- 8f6165b: feat(core): R7b — multi-ownerField disjunction catalog rule

  Расширение R7: `entity.ownerField` может быть **массивом** (2+ элементов) →
  генерируется `my_<entity>_list` с disjunction-фильтром (OR).

  ```js
  ontology.entities.Deal = {
    ownerField: ["customerId", "executorId"],  // multi-owner
    fields: { customerId: {...}, executorId: {...} },
  };
  ```

  → derived `my_deal_list` с:

  ```js
  {
    kind: "catalog",
    mainEntity: "Deal",
    filter: {
      kind: "disjunction",
      fields: ["customerId", "executorId"],
      op: "=",
      value: "me.id",
    },
    derivedBy: [witnessR7bMultiOwnerFilter(...)],
  }
  ```

  **Mutual exclusion**: R7 (single owner) и R7b (multi-owner) взаимоисключающи
  на одной entity. Array с `length === 1` **не** триггерит R7b (требование ≥2);
  такая проекция вообще не генерируется — автор должен либо указать string
  (R7), либо добавить второй owner (R7b).

  Motivation: backlog 3.2 в freelance (Deal имеет customerId + executorId —
  обе — legitimate ownership relations). Authored `my_deals` с filter
  `"customerId === me.id OR executorId === me.id"` можно теперь вывести
  через R7b.

  Спецификация: `idf-manifest-v2.1/docs/design/rule-R7b-multi-owner-spec.md`
  (draft); основная мотивация — `idf-manifest-v2.1/docs/design/debugging-derived-ui-spec.md`
  (Baseline 2026-04-20, R-rule candidates).

  **Backward compat**: `ownerField: "x"` (string) продолжает работать через
  R7 без изменений. Array-form — opt-in новая capability.

  Тесты: +3 в witnesses.test.js (R7b multi-owner, array-length-1 не-триггер,
  3+ ownerFields). 833/833 зелёные.

  Renderer-стороны: `filter.kind === "disjunction"` — новый формат для
  catalog'а, который `List` primitive должен уметь фильтровать. В этом PR
  не затронут; separate follow-up на renderer-пакет.

## 0.23.0

### Minor Changes

- 2e02b73: Добавлен `computeSlotAttribution(intents, ontology, projection)` — карта `{ slotPath → { patternId, action } }` для X-ray режима PatternInspector и derivation-diff CLI. Прокатывает `applyStructuralPatterns` пошагово (с deep-clone snapshot между шагами, чтобы in-place мутации в apply-функциях не повреждали diff), фиксирует added/modified пути.

## 0.22.0

### Minor Changes

- eb2954d: feat(core): witness-of-crystallization для R1–R8 в artifact.witnesses

  Каждое срабатывание правил деривации (R1 catalog, R2 feed override, R3 detail, R4 subCollection, R6 field-union, R7 ownerField, R8 hub absorption — parent и child side) оставляет запись в `artifact.witnesses[]` с `basis: "crystallize-rule"`, полями `ruleId`, `input`, `output`, `rationale`.

  Назначение: debugging derived UI — второй автор видит observable trail того, какое правило и на каком входе породило проекцию/обогащение. Вместе с существующими `pattern-bank` и `alphabetical-fallback` witnesses даёт полный derivation-граф для Studio Inspector и spec-debt метрик.

  Спецификация: `idf-manifest-v2.1/docs/design/debugging-derived-ui-spec.md`.

  **API additions** (аддитивно, zero-breaking):

  - `proj.derivedBy: CrystallizeWitness[]` — внутренний канал между `deriveProjections` и `crystallizeV2`; пробрасывается в `artifact.witnesses` первыми (origin-witnesses идут до pattern-bank).
  - Новый модуль `crystallize_v2/derivationWitnesses.js` — чистые builder-функции per правило.

  **Тесты**: +13 в `witnesses.test.js` (формат каждого witness'а + negative cases + integration через crystallizeV2 + hub через absorbHubChildren). 630/630 зелёные, functoriality probe без регрессий.

- eb2954d: feat(core): explainCrystallize — unified witness query surface

  Новая функция-query over artifact.witnesses[]. Принимает artifact и
  возвращает структурированное объяснение его происхождения:

  ```js
  explainCrystallize(artifact) → {
    projection: "listing_detail",
    archetype: "detail",
    origin: "derived+enriched",  // или "derived" | "authored" | "authored+enriched"
    witnessesByBasis: {
      "crystallize-rule": [...R1, R3, R4, R6, ...],
      "pattern-bank": [...],
      "polymorphic-variant": [...],
      "temporal-section": [...],
    },
    ruleIds: ["R3", "R4", "R6"],
    patternIds: ["subcollections", "grid-card-layout"],
    trace: [
      { step: 1, basis: "crystallize-rule", ruleId: "R3", rationale: "..." },
      { step: 2, basis: "crystallize-rule", ruleId: "R6", rationale: "..." },
      ...
    ],
    summary: 'detail "listing_detail" · выведена + обогащена · правила: R3, R4, R6 · паттерны: subcollections',
  }
  ```

  Также экспортируется `explainAllCrystallize(artifacts)` — batch-вариант
  над `Record<projId, artifact>`.

  **Назначение**: единый consumer witness'ов из всех basis (crystallize-rule,
  pattern-bank, polymorphic-variant, temporal-section, alphabetical-fallback,
  authored). Главные callers — CrystallizeInspector в §27 Studio, spec-debt
  dashboards, near-miss analyzers.

  **Origin classification**:

  - `derived` — только origin-правила (R1/R1b/R2/R3/R7/R10).
  - `derived+enriched` — origin + enrichment-правила (R4/R6/R9).
  - `authored+enriched` — authored проекция, обогащённая R9/R4/R6.
  - `authored` — ни одного crystallize-rule witness.

  Trace упорядочен по BASIS_ORDER (crystallize-rule → polymorphic → temporal →
  pattern-bank → alphabetical-fallback → authored), внутри basis — в порядке
  чтения artifact.witnesses[].

  Спецификация: `idf-manifest-v2.1/docs/design/debugging-derived-ui-spec.md`
  (секция explainCrystallize).

  Тесты: +8 в `explainCrystallize.test.js` (derived origin, authored-only,
  derived+enriched, trace order, patternIds, invalid input, batch,
  summary content). 657/657 зелёные.

- eb2954d: feat(core): near-miss witnesses — актionable rationale для не-сработавших правил

  Новая функция `collectNearMissWitnesses(intents, ontology)` возвращает массив
  «отрицательных» witness'ов: где R-правило могло бы сработать, но не сработало,
  и что автор может сделать чтобы запустить.

  Basis: `"crystallize-rule-near-miss"` (новый, отличается от positive
  `"crystallize-rule"`).

  **Покрываемые правила**:

  - **R3**: entity с ровно 1 mutator (threshold = >1 для detail). Rationale:
    «|mutators(Category)| = 1; R3 требует >1». Suggestion: «Добавьте второй
    mutator или объявите category_detail явно».
  - **R1b**: изолированная entity (creators=0, не kind:reference, не referenced
    по FK). Suggestion: пометить `kind:"reference"` или удалить из ontology.
  - **R7**: entity с creators и owner-candidate полем (userId/ownerId/authorId/
    creatorId), но без `ownerField` declaration. Suggestion: объявить
    `ontology.entities.E.ownerField = "userId"`.
  - **R10**: agent/observer роль без scope declaration. Suggestion: добавить
    `role.scope = { Entity: { via, viewerField, joinField, localField } }`.

  **Исключения**: `entity.kind: "assignment"` не триггерит R1b/R3/R7;
  owner/viewer base-роли без scope — не триггерят R10.

  Также экспортируется `groupNearMissByRule(nms)` — группировка по ruleId для
  render'а.

  **Consumer**: CrystallizeInspector (§27 Studio), uncovered-classification.mjs,
  explainCrystallize extensions.

  Спецификация: `idf-manifest-v2.1/docs/design/debugging-derived-ui-spec.md`
  (секция «Witness'ы для не-сработавших правил»).

  Тесты: +16 в nearMissWitnesses.test.js (R3/R1b/R7/R10 positive + negatives,
  assignment-exclusion, group, edge cases). 689/689 зелёные.

- eb2954d: feat(core): resolveCompositions — runtime helper для R9 aliased-fields

  Завершает R9 end-to-end: crystallize добавляет `proj.compositions`,
  runtime раскрывает alias'ы при чтении из `world`.

  API:

  ```js
  resolveItemCompositions(item, compositions, world) → enrichedItem
  resolveCompositions(items, compositions, world)    → enrichedItems[]
  getAliasedField(item, "task.title")                → value | undefined
  ```

  Режимы:

  - `mode: "one"` (default) — `item[as] = world[entity].find(x => x.id === item[via])`
  - `mode: "many"` — `item[as] = world[entity].filter(x => x[via] === item.id)`

  Обработка edge-cases:

  - Missing FK value → `null` (one), `[]` (many).
  - Missing entity в world → `null` (one), `[]` (many).
  - Items не мутируется (чистая функция).

  `getAliasedField(item, path)` — null-safe lookup для multi-level path'ов
  (`"customer.address.city"`). Consumer — primitive atoms в
  @intent-driven/renderer, которые должны уметь рендерить `witnesses`
  вида `"task.title"` без падения на undefined.

  Спецификация: `idf-manifest-v2.1/docs/design/rule-R9-cross-entity-spec.md`.

  Тесты: +16 в resolveCompositions.test.js (one/many mode, missing refs,
  batch, null-safety, multi-level paths). 673/673 зелёные.

  Следующий шаг для end-to-end R9: обновить primitive atoms в
  @intent-driven/renderer чтобы использовать `getAliasedField` при чтении
  alias-полей из `proj.witnesses`. Отдельный PR в renderer-пакете.

- eb2954d: feat(core): R10 — role-scope filtered catalog rule

  Новое правило деривации: для каждой роли с `role.scope[entityName]` в онтологии,
  генерируется scoped read-only catalog с structured m2m-via filter.

  Поддерживает формат `role.scope` v1.6 (реальный в invest):

  ```js
  ontology.roles.advisor = {
    scope: {
      User: {
        via: "assignments",
        viewerField: "advisorId",
        joinField: "clientId",
        localField: "id",
        statusField: "status", // опционально
        statusAllowed: ["active"], // опционально
      },
    },
  };
  ```

  Output: `advisor_user_list` с `kind:"catalog"`, `readonly:true`, `filter:{ kind:"m2m-via", via, viewerField, joinField, localField, statusField, statusAllowed }`.

  Witness `basis:"crystallize-rule"`, `ruleId:"R10"` несёт role, entity и full scope spec для audit trail.

  Incomplete scope-spec'и (отсутствие via/viewerField/joinField/localField) игнорируются. Роли без scope — не триггерят.

  Спецификация: `idf-manifest-v2.1/docs/design/rule-R10-role-scope-spec.md`.

  **Baseline impact** (stacked on R1b):

  - До R10: U = 23 (20%)
  - После R10: U = 22 (19%)
  - **Δ = -1** (invest.advisor_clients ← advisor_user_list)

  Spec предсказывал -6–7. Расхождение — domain authoring gap: messenger/delivery/sales не объявляют `role.scope`, пишут filter-строки вручную в PROJECTIONS. Ontology authoring workstream — отдельно.

  Тесты: +4 (m2m-via positive, multi-entity scope, incomplete-spec negative, no-scope negative). 645/645 зелёные.

- eb2954d: feat(core): R1b — read-only entity catalog rule

  Новое правило деривации: если `entity E` не имеет creator-intent'ов
  (`creators(E) = ∅`), но либо объявлена `kind: "reference"` (v1.6 онтология),
  либо на неё ссылаются foreignKey-поля (`type: "entityRef"`) из других
  сущностей — `deriveProjections` выводит `<entity>_list` с
  `readonly: true`.

  Исключение: `entity.kind === "assignment"` (m2m-связки) не триггерят R1b.

  Приоритет: R1 (обычный catalog при `creators ≠ ∅`) имеет приоритет над R1b.

  Witness `basis:"crystallize-rule"` с `ruleId:"R1b"`:

  ```js
  {
    input: { entity, creators: [], source: "kind:reference" | "referenced-by", referencedBy },
    output: { kind: "catalog", mainEntity: entity, readonly: true },
    rationale: "Entity.kind === 'reference' + creators = ∅ → read-only catalog"
  }
  ```

  Тесты: 5 новых в `witnesses.test.js` (source:kind:reference,
  source:referenced-by, assignment-exclusion, isolated-entity-negative,
  R1-priority). 641/641 зелёные.

  Спецификация: `idf-manifest-v2.1/docs/design/rule-R1b-read-only-catalog-spec.md`.

  **Baseline impact** (по measurement через `idf/scripts/uncovered-classification.mjs`
  на 10 доменах):

  - До R1b: U = 24 (21%)
  - После R1b: U = 23 (20%)
  - **Δ = -1 (вместо ожидаемых -7)**.

  Причина расхождения: большая часть uncovered candidates
  (`delivery.zones_catalog`, `delivery.couriers_list`, `booking.service_catalog`,
  `lifequest.badge_list`) имеют FK-поля с `type: "text"` вместо `"entityRef"`.
  R1b по спеке требует корректной типизации через `entityRef` — это ontology
  audit gap, не bug SDK. Фикс ontologies — отдельный workstream.

- eb2954d: feat(core): R9 — cross-entity composite projection (MVP)

  Новое правило деривации: `ontology.compositions[mainEntity]: CompositionDef[]`
  обогащает все проекции с этим mainEntity полями `compositions` (список
  join-aliases) и расширяет `entities[]` целевыми сущностями.

  Формат композиции:

  ```js
  ontology.compositions = {
    Deal: [
      { entity: "Task", as: "task", via: "taskId", mode: "one" },
      { entity: "User", as: "customer", via: "customerId", mode: "one" },
    ],
  };
  ```

  Обогащение catalog/detail/feed с `mainEntity === "Deal"`:

  - `proj.entities = ["Deal", "Task", "User"]`
  - `proj.compositions = [...]`
  - `proj.derivedBy += witnessR9Composite(...)`

  Incomplete entries (без `entity`/`as`/`via`) игнорируются.

  MVP scope: только обогащение проекций. **Не входит в PR:** renderer-поддержка
  aliased path lookup (`task.title`), runtime `resolveCompositions(world, ...)`
  helper, cascade multi-hop composition. Эти — последующие PR'ы в том же
  workstream.

  Спецификация: `idf-manifest-v2.1/docs/design/rule-R9-cross-entity-spec.md`.

  **Baseline impact** (stacked on R1b + R10):

  - До R9: U = 22 (19%)
  - После R9: U = 22 (19%)
  - **Δ = 0**

  Ни один из 10 доменов не объявляет `ontology.compositions` — это новое
  поле, предложенное спецификацией. R9 работает корректно (проверено тестами),
  но реальное закрытие U требует domain authoring workstream — объявить
  compositions в ontologies freelance (Deal, Wallet), delivery (Order, Cart),
  sales (Watchlist).

  **Совокупный результат SDK workstream**:

  - baseline: U = 24 (21%)
  - после R1b+R10+R9: U = 22 (19%), Δ = -2 из возможных -13 (15% от spec prediction)

  **Главная эмпирическая находка**: SDK-правила корректны, но **impact заблокирован
  ontology authoring gap**. Форматная capability появляется инкрементально, но её
  полная реализация — параллельный workstream правильной типизации и декларации
  в existing ontologies, не SDK change.

  Тесты: +4 в witnesses.test.js (basic composition, no-compositions, incomplete
  entries filtered, compositions on both catalog+detail). 649/649 зелёные.

## 0.21.0

### Minor Changes

- 265af59: feat(core): witness-of-crystallization для R1–R8 в artifact.witnesses

  Каждое срабатывание правил деривации (R1 catalog, R2 feed override, R3 detail, R4 subCollection, R6 field-union, R7 ownerField, R8 hub absorption — parent и child side) оставляет запись в `artifact.witnesses[]` с `basis: "crystallize-rule"`, полями `ruleId`, `input`, `output`, `rationale`.

  Назначение: debugging derived UI — второй автор видит observable trail того, какое правило и на каком входе породило проекцию/обогащение. Вместе с существующими `pattern-bank` и `alphabetical-fallback` witnesses даёт полный derivation-граф для Studio Inspector и spec-debt метрик.

  Спецификация: `idf-manifest-v2.1/docs/design/debugging-derived-ui-spec.md`.

  **API additions** (аддитивно, zero-breaking):

  - `proj.derivedBy: CrystallizeWitness[]` — внутренний канал между `deriveProjections` и `crystallizeV2`; пробрасывается в `artifact.witnesses` первыми (origin-witnesses идут до pattern-bank).
  - Новый модуль `crystallize_v2/derivationWitnesses.js` — чистые builder-функции per правило.

  **Тесты**: +13 в `witnesses.test.js` (формат каждого witness'а + negative cases + integration через crystallizeV2 + hub через absorbHubChildren). 630/630 зелёные, functoriality probe без регрессий.

## 0.20.0

### Minor Changes

- db67207: SDK improvements backlog (PRs #50–#57): invariants schema-drift, multi-owner
  ownership, parameter affinity matching, 8 новых stable паттернов, candidate
  bank со 127 JSON-кандидатами, AdapterProvider / AuthForm / Countdown /
  UndoToast primitives.

  ## @intent-driven/core

  **Invariants (PR #50 + #52):**

  - schema-drift graceful fallback: unknown_kind / handler_threw /
    unsupported_shape → severity:"warning" (раньше ронили effect)
  - `invariants/normalize.js` — поддержка альтернативных форм referential /
    aggregate / transition
  - `invariant.where` расширен на transition / referential / aggregate
  - Новый kind `"expression"` — row-level predicate / JS-выражение
  - `cardinality.groupBy` может быть массивом (composite key)

  **Crystallizer (PR #50 + #51 + #52 + #53):**

  - `inferParameters` читает `intent.particles.parameters` как fallback
  - `selectArchetype` нормализует `intent.confirmation ?? particles.confirmation`
  - `footer-inline-setter.apply` отсекает textarea/file/multiImage
  - `assignToSlotsDetail`: phase-transition с parameters пропускается через
    `wrapByConfirmation` в toolbar (не primaryCTA)
  - multi-owner ownership: `entity.owners: [a,b]` + `intent.permittedFor`
  - `collapseToolbar` dedup только при salience < 70
  - `buildDetailBody` инжектит irreversibleBadge при irreversibility:"high"
  - subCollection FK-implicit addControl + authored `itemView`/`sort`/
    `where`/`terminalStatus`/`hideTerminal`/`toggleTerminalLabel`

  **Pattern Bank: 8 новых stable + 127 candidate (PRs #52–#57):**

  - §6.1 `catalog-creator-toolbar` (multi-param → toolbar, не hero)
  - §6.5 `timer-countdown-visible` (scheduled_timer → countdown node)
  - §6.6 `catalog-exclude-self-owned` (filter item.ownerField !== viewer.id)
  - Merge-промоции:
    - `faceted-filter-panel` (3 → 1): typed controls по field-role
    - `reputation-tier-badge` (4 → 1): boolean/enum tier-detect
    - `paid-visibility-elevation` (4 → 1): elevation markers
    - `computed-cta-label` (3 → 1): live total в CTA для _Extra_/
      _Modifier_/_AddOn_ child-entities
    - `undo-toast-window` (2 → 1): soft-cancel для repeatable destructive
  - Candidate bank (`patterns/candidate/`): 127 JSON из Claude Researcher
    Pipeline, `getCandidate`, `getCandidatesByArchetype`,
    `groupCandidatesByTheme`, `loadCandidatePatterns`
  - Registry: 20 → 28 stable patterns

  ## @intent-driven/renderer

  - `pickAdaptedComponent(spec)` — matching-score резолвер, учитывает
    `spec.fieldRole / name / features` через adapter.affinity
  - `ParameterControl` использует `pickAdaptedComponent` как primary
    (fallback: legacy `getAdaptedComponent(spec.control)` → built-in)
  - `<AdapterProvider>` + хуки `useAdapter / useAdapterComponent /
useAdapterPick / resolveAdapterComponent` (React Context вместо
    global mutable registry)
  - Primitives: `<AuthForm>` (login/register/both), `<Countdown>`
    (read scheduledTimers), `<UndoToast>` (soft-cancel window)
  - ArchetypeDetail `AntdOverflowMenu` респектит `spec.condition`

  ## adapter-antd / mantine / shadcn / apple

  - Buttons (primary/secondary/danger) принимают `label` И `children`
    (приоритет label) — antd
  - `AntdDateTime` с `withTime / precision / control:"datetime-local"`
  - `AntdNumber` с `fieldRole ∈ {money, price}`
  - `AntdTextInput` пробрасывает `maxLength/minLength/pattern`
  - Affinity декларации на Number/DateTime/Tel/Email для всех четырёх
    адаптеров (matching-score integration)

## 0.19.0

### Minor Changes

- e736b61: `checkComposition` dominance rule: `remove` на базовой коллекции конфликтует с field-level effect на той же сущности.

  ## Проблема

  До фикса `checkComposition({alpha:"replace", target:"items.status"}, {alpha:"remove", target:"items"})` возвращал `{compatible: true, resolution: "different_target"}` — потому что строки target отличаются.

  Но семантически они конфликтуют: нельзя `replace` поле сущности, которую одновременно `remove`. Это нарушает intent-композицию: если выполнить обе `replace` и `remove` на одну сущность, результат определён только их причинным порядком, что не derivable из спеки без дополнительных предположений.

  ## Фикс

  Dominance rule в `checkComposition`:

  - Если один эффект — `remove` на **базовой коллекции** (target не содержит `.`)
  - Второй — effect на **field-path** той же коллекции (target содержит `.`)
  - Результат: **conflict** (⊥)

  Раньше такие пары трактовались как `different_target`. Это расхождение с spec §6 и причина провала conformance-test `level-3/composition-001`.

  ## Эффект на conformance

  С этим fix'ом + host-side runner changes (idf#39) → **55/55 conformance тестов проходят**.

  ## Совместимость

  - **Forward**: добавляется дополнительный conflict-случай, который ранее был false-compatible. Domains, которые этот случай использовали (один intent removing entity, другой editing его field), теперь получат algebra excluding-edge между ними — что корректно. Эти пары обычно не should-co-execute.
  - **Существующие тесты**: 619/619 passing (нет регрессии — тесты не полагались на false-compatible случай).

  ## Связанное

  - idf#38 — conformance фикстуры в canonical format + runner (merged)
  - idf#39 — level-3 findings alignment + level-1/2 runner fixes (merged)

  После слияния этого PR + release: все conformance-testы проходят end-to-end на @intent-driven/core.

## 0.18.0

### Minor Changes

- d01d8de: feat(core): irreversible-confirm pattern — structure.apply

  Добавлена `structure.apply(slots, context)` функция к stable-паттерну
  `irreversible-confirm`. Обогащает существующий confirmDialog overlay
  полем `warning` из декларативного `intent.__irr.reason`.

  **Семантика:** apply не создаёт overlay — он уже строится `controlArchetypes.confirmDialog`
  по intent.irreversibility. Apply _обогащает_ overlay семантическим warning-текстом
  из intent declaration (`__irr: { point, reason }`), чтобы рендерер показал его
  над дефолтным typeText-диалогом.

  **Pure function.** Не мутирует slots; idempotent.

  **Closes:** один из 10 open items v1.12 «pattern-bank: structure.apply для оставшихся stable паттернов».

## 0.17.0

### Minor Changes

- 5a6429d: Witness `alphabetical-fallback` — видимость spec-debt в артефакте.

  Когда `crystallizeV2` разрешает slot-contention через `intent.salience` (§3.5 спеки v1.1) и две или более intent'ов имеют равный salience, финальный выбор делается лексикографически по `intentId`. Этот выбор детерминирован (§9.2 Determinism спеки v1.1) но семантически арбитральный — автор не объявил приоритет между этими intent'ами.

  Теперь каждая такая tied группа фиксируется в `artifact.witnesses[]`:

  ```js
  {
    basis: "alphabetical-fallback",
    reliability: "heuristic",          // НЕ rule-based
    slot: "toolbar",
    projection: "listing_detail",
    salience: 60,
    chosen: "add_listing_image",
    peers: ["add_to_bundle", "apply_template", ...],
    recommendation: "Проставьте intent.salience одному из [...]",
  }
  ```

  **Новый helper:** `detectTiedGroups(sortedItems, { slot, projection })` — чистая функция, возвращает массив witness-записей.

  **Интеграция:**

  - `assignToSlotsCatalog` — после `sort(bySalienceDesc)`, перед overflow cap=5
  - `assignToSlotsDetail.collapseToolbar` — возвращает `{toolbar, witnesses}`
  - `crystallize_v2/index.js` — `slots._witnesses` мержится в `artifact.witnesses[]` перед финальной сборкой артефакта

  **Measurable spec-debt** на 9 reference доменах:

  ```
  Грандтотал: 16 alphabetical-fallback witnesses
    planning/invest/delivery: 0 ✓ чисто
    sales: 7 (listing_detail ×2 — 31-intent tied cluster)
    lifequest: 4, workflow/booking/messenger/reflect: 1-2
  ```

  Studio и lint-инструменты могут подсвечивать эти witnesses как «spec smell». Автор видит, какие intent'ы стоит пометить salience явно, чтобы замкнуть tied choice в свойство спеки, а не эвристику алгоритма.

  **Philosophical.** Это замыкает обещание спеки v1.1 §9.2 на ref-impl: если impl падает в лексикографический tie-break, артефакт **должен** это явно фиксировать. Tied choice, которое раньше было невидимым «я выбрал что-то», теперь first-class witness в format output.

  **Не breaking.** Существующие потребители `artifact.witnesses[]` продолжают работать — новая запись просто добавляется в массив.

  **Тесты.** 5 новых unit-тестов на `detectTiedGroups` (624/624 зелёных).

## 0.16.0

### Minor Changes

- a164717: Функториальность `crystallizeV2` под permutation INTENTS/PROJECTIONS + PoC `intent.salience` как первоклассный tiebreaker.

  **Функториальность (§16).** Результат `crystallizeV2` больше не зависит от порядка авторства ключей INTENTS/PROJECTIONS. Ключи нормализуются алфавитной сортировкой на входе — downstream iteration наследует стабильный порядок. Эмпирическая база (`idf/scripts/functoriality-probe.mjs` на 9 доменах): до фикса 0/9 доменов строго функториальны (121 проекция: 70 identical / 35 order-only / 16 semantic); после фикса — 9/9 строго функториальны.

  **Поведенческое изменение.** Tie-break при slot-contention теперь алфавитный по intentId, а не «порядок авторства». Артефакты могут отличаться от зафиксированных в v0.15: см. snapshot-тесты в потребителях. API совместим.

  **`intent.salience` PoC.** Новый helper `computeSalience(intent, mainEntity)`:

  - Explicit: `intent.salience: number` или `"primary" | "secondary" | "tertiary" | "utility"` (разворачивается в 100/50/20/5)
  - Computed из particles:
    - creator main entity → 80
    - phase-transition (replace `.status` на main) → 70
    - edit main → 60
    - default → 40
    - destructive-main → 30
    - read-only → 10

  PoC scope — только `assignToSlotsDetail.collapseToolbar`: standalone кнопки сортируются `bySalienceDesc` перед срезом capacity=3. Tied salience → alphabetical. Обратная совместимость: 0 обязательных правок existing intent'ов, computed defaults покрывают весь corpus.

  **Философское значение.** Заявление «IDF — формат» держится на том, что crystallize — функция от семантики, не от порядка авторства. До фикса это было ложно эмпирически на всех 9 доменах. Salience делает выбор при slot-contention свойством спеки, а не эвристикой алгоритма — alphabetical становится явным финальным fallback'ом.

  **Roadmap (не в этом релизе):**

  - witness `alphabetical-fallback` — сделать tied salience видимым в `artifact.witnesses[]` как «spec smell»
  - salience в `assignToSlotsCatalog` (capacity=5)
  - `projection.salienceOverride` для multi-projection доменов

## 0.15.0

### Minor Changes

- 519b4b9: Polymorphic entities (v0.15): `entity.discriminator` + `entity.variants` — формализация sum-type сущностей с per-variant UI. Закрывает §26 open item «Composite / polymorphic entities» (from v1.6).

  **Онтология:**

  ```js
  ontology.entities.Task = {
    kind: "internal",
    discriminator: "kind",            // NEW
    variants: {                       // NEW
      story: { label: "Story", fields: { storyPoints: {type:"number"}, criteria: {type:"textarea"} } },
      bug:   { label: "Bug",   fields: { severity: {type:"enum", values:[...]}, stepsToReproduce: {...} } },
    },
    fields: { id, title, kind: {type:"enum"}, ... },  // shared
    ownerField: "assigneeId",
  };
  ```

  **Новые helpers (core):**

  - `parseCreatesVariant("Task(bug)")` → `{entity: "Task", variant: "bug"}`
  - `getVariantFields(entity, variantKey)` → `{fields: merged, warnings}` (shared + variant.fields, shared wins на конфликтах)

  **Crystallize:**

  - `buildCardSpec` emit `{variants: {[k]: spec}, discriminator}` для polymorphic entities. Legacy single cardSpec — для monomorphic.
  - `inferParameters` резолвит variant из `intent.creates`, использует merged fields, добавляет hidden `{name: discriminator, default: variant, hidden: true}` param.
  - Witness `{basis: "polymorphic-variant", pattern: "polymorphic:variant-resolution", reliability: "rule-based"}` в artifact.witnesses для projection с polymorphic mainEntity.

  **Renderer:**

  - `GridCard` в `primitives/containers.jsx` читает `cardSpec.discriminator` + `item[key]` и выбирает `cardSpec.variants[key]` как effective spec. Unknown variant → fallback на first + console.warn. Backward-compat: cardSpec без `.variants` идёт legacy path.

  **Philosophy alignment (манифест):**

  - **§26 closes open item** (перенос из v1.6): «union-типы не формализованы в entity.kind»
  - **§14 ortho axis** — `discriminator` не conflate с `kind` (ownership/authority axis). Precedent: v0.14 `temporality`.
  - **§5 composition** — viewer-scoping через `ownerField` неизменён
  - **§15 witness** — declarative → rule-based, ready для promotion
  - **§16 Pattern Bank** — не используем (нет author-override use-case)
  - **§17 materializations** — pixel fully specialized, agent-API free через existing intent-specialization (creates: "Entity(variant)"), document/voice — v0.2

  **`creates: "Entity(variant)"`** — existing parenthetical-creates (`Vote(yes)`, `Booking(draft)`) уже парсится `normalizeCreates`. Новый `parseCreatesVariant` расширяет для формального резолва variant.

  **Out of scope v0.15:** per-variant detail sections, variant inheritance (`extends`), runtime type-switching, variant-specific invariants, document/voice enhancement.

## 0.14.0

### Minor Changes

- 01bc3a3: Temporal sub-entity kinds (v0.14): `entity.temporality: "snapshot" | "causal-chain"` как ortho axis в онтологии + EventTimeline primitive для inline detail sub-sections.

  **Онтология (core):**

  ```js
  ontology.entities.PaymentEvent = {
    kind: "internal",
    temporality: "causal-chain", // NEW
    ownerField: "paymentId",
    fields: {
      id: {},
      paymentId: { type: "entityRef" },
      kind: { type: "enum" },
      at: { type: "datetime" },
      actor: { type: "entityRef" },
      description: { type: "text" },
    },
  };
  ```

  `temporality` ортогонален `kind`: покрывает time-semantics (snapshot = state at moment, causal-chain = event description). Null = default.

  **Crystallize:** `assignToSlotsDetail.buildSection` автоматически резолвит field-mapping (`atField`/`kindField`/`actorField`/`descriptionField`/`stateFields`) через `inferFieldRole` + name-regex, и записывает `section.renderAs = { type: "eventTimeline", kind, ... }`. Backward-compat: entities без `temporality` — `renderAs` не добавляется, default-path рендера неизменен.

  **Renderer:** новый `EventTimeline` primitive (vertical stepper с dot-markers). 2 kinds:

  - `causal-chain`: `● [kind-badge] actor — description · at`
  - `snapshot`: `● at` → state-fields inline (`label: value`)

  `SubCollectionSection` условный branching: если `section.renderAs.type === "eventTimeline"` — рендерит через primitive, пропуская default path. Экспорт: `import { EventTimeline } from "@intent-driven/renderer"`.

  **Witness-of-crystallization (§15 v1.10):** для каждой temporal sub-section — `{basis: "temporal-section", pattern: "temporal:event-timeline", reliability: "rule-based"}` в artifact.witnesses.

  **Philosophy alignment:** §14 ortho axis (не conflating с kind), §5 composition (events = subCollection projection), §17 pixel-only v0.14 (document/voice enhancement — v0.2), §23 auto-irreversibility — v0.2.

  **Out of scope v0.14:** append-only invariant, auto `__irr` для event-create, section-level field overrides, top-level catalog EventTimeline, document/voice обогащение.

## 0.13.0

### Minor Changes

- 2a0bc87: Multi-archetype views (Scope B): одна projection → N archetype-рендерингов + runtime switcher.

  **New projection API:**

  ```js
  projection: {
    kind: "catalog", mainEntity: "Task", witnesses: [...],
    views: [
      { id: "board", name: "Доска", kind: "catalog", layout: "grid" },
      { id: "table", name: "Таблица", kind: "catalog", layout: "table" },
      { id: "stats", name: "Сводка", kind: "dashboard", widgets: [...] },
    ],
    defaultView: "board",
  }
  ```

  **Artifact new fields:**

  - `views: Array<{id, name, archetype, layout, slots, matchedPatterns, witnesses}> | null`
  - `defaultView: string | null`
  - `viewSwitcher: { views: [{id, name, archetype}], activeId } | null`

  Backward-compat: projection без `views` — artifact.views = null. Existing rendering не затрагивается.

  **Inheritance rules:** view наследует parent projection; overrides — только render-level (`kind`, `layout`, `widgets`, `onItemClick`, `sort`, `patterns`, `strategy`, `name`). Q/W-level (`filter`, `witnesses`, `mainEntity`, `entities`, `idParam`) — запрещены (warning в console + ignored). Archetype whitelist: `catalog`/`feed`/`dashboard` (остальные → fallback + warning).

  **Per-view Pattern Bank.** Каждая view — независимый matching + apply pass. Разные archetype → разные matched patterns (`subcollections` не сматчит dashboard-view).

  **Renderer:** `ProjectionRendererV2` принимает prop `activeView`; подменяет slots/archetype на view's artifact. `ViewSwitcher` primitive — segmented-control для 2-3 views, dropdown для 4+. Экспорт через `import { ViewSwitcher } from "@intent-driven/renderer"`.

  **Materializations (§17):** agent/document/voice используют **default view** only — view это UI-концепт, не data-concept.

  **Philosophy alignment:** §5 composition (views = render-only, Q/W неизменны), §17 пять слоёв (materializations на default), §16 pattern bank (per-view matching).

  **Out of scope:** cross-projection switcher (via `projection.group`), per-user default view (localStorage), `canvas`/`detail`/`form`/`wizard` как view archetypes.

## 0.12.0

### Minor Changes

- 0d49cdf: 4 новые temporal field roles + consumers (closes «белое пятно для дат»).

  **Новые роли в `inferFieldRole`** (priority: timestamp > deadline > scheduled > occurred):

  - `timestamp` — audit/meta: `createdAt`, `updatedAt`, `deletedAt`, `modifiedAt`, `lastSeenAt`, `archivedAt`. Renderer: мелкий серый текст.
  - `deadline` — target date, может быть overdue: `due*`, `deadline*`. Renderer: badge в monitoring/triage.
  - `scheduled` — планируемое будущее: `scheduled*`, `appointment*`, `meeting*`, `visitDate`, `eventDate`, `sessionAt`. Renderer: secondary (countdown-ready).
  - `occurred` — прошедшее событие: past-tense verbs (`started*`, `completed*`, `finished*`, `closed*`, `posted*`, `ended*`, `administered*`, `recorded*`, `shipped*`, `opened*`, `logged*`, `received*`), `record*`, `birthDate`. Renderer: secondary (timeline-anchor).

  **Breaking (minor):** `timer` regex сужен с `/end|deadline|expir/` до `/^(expir|countdown)/` + `/Until$/`. Поля `deadline*` переезжают с `timer` на `deadline`. Affected fields:

  - `auctionEnd`, `endAt`, `endDate` (без past-tense `ed`) → раньше `timer`, теперь `info` fallback. Workaround: explicit `fieldRole: "timer"`.
  - `deadline` → раньше `timer`, теперь `deadline` (badge вместо Timer-countdown).
  - `expiresAt`, `expiryDate`, `validUntil` → остаются `timer`.

  Domains в idf/ с `deadline`-полями (invest.Goal, lifequest.Goal, planning.Poll) получают новое поведение: вместо Timer-countdown — badge.

  **Consumers:**

  - `strategy.js` — monitoring: `deadline` → badge, `scheduled`/`timestamp` → secondary. triage: `deadline` → badge.
  - `deriveShape.isDateField` — делегирует в `inferFieldRole` (удалён локальный `DATE_FIELD_HINTS`).

  Witness-labeling (§15 v1.10) — каждое temporal-правило эмитит `reliability: "heuristic"` + `pattern: "temporal:*"`. Стабильные grouping keys, готовые к промоции в `ontology.fieldPatterns` (§26 zazor #3 phase 2).

  **Philosophy alignment:** §4 (темпоральные предикаты — deadline/scheduled готовы к `now`-relative conditions), §15 (pattern promotion), §21 (Timer primitive breaking change задокументирован).

  **Out of scope v0.12:** `checkpoint` role, `duration` (start+end pair), `RelativeTime`/`DeadlineBadge` primitives, выделение Events/Deadlines секций в detail. См. `docs/superpowers/specs/2026-04-18-temporal-field-roles-design.md`.

## 0.11.0

### Minor Changes

- 988bfe4: Hub-absorption (R8) + shape-layer в crystallize_v2 — снимает монотонность «много flat tabs × hero-create везде» в CRUD-доменах.

  **R8 Hub-absorption** (`absorbHubChildren`): child-каталоги с FK на entity с detail-проекцией автоматически помечаются `absorbedBy: "<parent>_detail"` и добавляются в `hubSections: [{ projectionId, foreignKey, entity }]` на hub-detail. Threshold — ≥2 child-каталога (меньше не оправдывает иерархии). Author-override: `projection.absorbed: false`.

  **Shape-layer** (`deriveShape`): вывод визуального shape'а для catalog/feed поверх archetype:

  - `timeline` — date-witness + descending sort (хронологии вроде health_feed).
  - `directory` — contact-поля (phone/email/address) без date-sort (адресные книги).
  - `default` — fallback.

  Author-override: `projection.shape`. Результат в `artifact.shape` + `artifact.shapeSignals`, shape !== default также пишется в `slots.body.shape` для renderer.

  **Hero-create guard**: для `timeline` и `directory` creator не идёт в hero-слот, а в toolbar — хронологии и контакты не должны визуально доминировать над созданием.

  **Новые поля artifact**: `absorbedBy`, `hubSections`, `shape`, `shapeSignals`. Также `slots.hubSections` в detail-архетипе (hook для будущего rendering v0.2).

## 0.10.1

### Patch Changes

- 2827db9: fix(patterns): корректные projection names в falsification для 7 golden patterns

  В v0.10.0 `shouldMatch` / `shouldNotMatch` для 7 новых golden-паттернов
  ссылались на projection names, которых нет в реальных IDF-доменах
  (`listings_catalog` vs `listing_feed`, `habits_root` vs `habit_list`,
  `workflow_runs` vs `execution_log` и т.д.). После прогона
  `matchPatterns()` на всех 9 доменах через `scripts/match-golden-
patterns.mjs` в idf repo (2026-04-18) исправил:

  - `global-command-palette`: sales/listing_feed, messenger/conversation_list, booking/review_form
  - `optimistic-replace-with-undo`: lifequest/habit_list, lifequest/dashboard, planning/poll_overview
  - `bulk-action-toolbar`: переоптимизирован — только messenger/conversation*list (единственный домен с фактическими bulk*\*-intents)
  - `kanban-phase-column-board`: workflow/execution_log, sales/listing_feed, delivery/orders_feed
  - `keyboard-property-popover`: sales/listing_detail, lifequest/goal_detail, lifequest/habit_detail
  - `observer-readonly-escape`: упрощён — aspirational shouldMatch на invest/alert_detail, явно задокументирован
  - `lifecycle-locked-parameters`: booking/booking_detail, delivery/order_detail, sales/listing_detail

  Фактические результаты matching на 9 доменах (после фикса):

  - `global-command-palette`: 106 hits
  - `optimistic-replace-with-undo`: 67 hits
  - `kanban-phase-column-board`: 21 hits
  - `bulk-action-toolbar`: 15 hits
  - `keyboard-property-popover`: 11 hits
  - `lifecycle-locked-parameters`: 8 hits
  - `observer-readonly-escape`: 0 hits (aspirational — нужно добавить observer-role с high-irreversibility intents)

  Содержимое patterns (trigger / structure / rationale) не менялось — только
  falsification section, что исправляет schema-тест без изменения поведения.

## 0.10.0

### Minor Changes

- c413d3d: feat(patterns): 7 новых stable-паттернов из golden-standard batch

  Pattern Bank растёт с 13 до **20 stable patterns** после исследования 5
  золотых стандартов (Linear / Stripe / Notion / Height / Superhuman)
  через `scripts/pattern-researcher-batch.mjs` (51 candidate, 7
  promoted).

  Cross-archetype (overlay-слой поверх любой projection):

  - `global-command-palette` — ⌘K overlay для ≥15 intents. Конвергентный
    signal: Linear, Height, Superhuman (3 независимых match'а).
  - `optimistic-replace-with-undo` — дуал к `irreversible-confirm`. Для
    частых reversible replace-ops (≥3 click-confirmation): undo-toast
    вместо modal. Signal: Linear property changes, Superhuman archive.
  - `bulk-action-toolbar` — selection-triggered action bar, если домен
    объявляет ≥2 `bulk_*` intents. Signal: Height, Linear Triage, Gmail.

  Catalog:

  - `kanban-phase-column-board` — catalog-версия `phase-aware-primary-cta`.
    Триггер: status-field с ≥3 enum + replace-intents на `.status`.
    Apply (v1.13+): body.layout={type:"kanban", columnField:"status"}.

  Detail:

  - `keyboard-property-popover` — Linear-style sidebar. ≥4 replace-
    intents на mainEntity.\* + click-confirmation. Inline-popover с
    type-specific picker + single-letter hotkey.
  - `observer-readonly-escape` — observer-role + ≥1 high-irreversibility
    intent → единый primary CTA (Dispute/Escalate/Flag). Signal: Stripe
    payment-detail, invest alert-detail.
  - `lifecycle-locked-parameters` — писаемы в draft-фазе, read-only
    после активации. Signal: Stripe subscription, IDF AgentPreapproval.

  Каждый паттерн содержит rationale.evidence (источники signal'а),
  counterexample (когда НЕ применим), falsification.shouldMatch /
  shouldNotMatch (реальные domain+projection pairs из IDF prototype).

  Отчёт с полной кластеризацией 51 кандидата: см. idf/refs/2026-04-18-
  FINAL-REPORT.md.
  EOF

  Следующий шаг — прогон через matchPatterns() на 9 доменах IDF,
  фактическая валидация shouldMatch/shouldNotMatch, apply-функции для
  3-4 паттернов (planned v0.11).

## 0.9.1

### Patch Changes

- 9b8f413: fix(agent): collection naming canonical camelCase + ISO-expiresAt tolerant

  Два исправления в slice'е agent-layer:

  - `filterWorldForRole` → `findCollection` теперь пробует коллекции в порядке
    camelCase → lowercase → last-segment (booking legacy `slots`). `outputName`
    всегда canonical camelCase (`agentPreapprovals`, `riskProfiles`, `timeSlots`)
    — это public namespace для агента. Раньше outputName был lowercase, и
    консьюмеры видели `world.agentpreapprovals` для одного entity и
    `world.riskProfiles` (из seed) для другого — inconsistent.
  - `checkPreapproval` → `getPreapprovalRow` получил тот же fallback lookup
    (camelCase → lowercase). `notExpired` check теперь принимает ISO-string
    для `expiresAt` через `Date.parse()` fallback — возвращает
    `invalid_expiresAt` только если не number и не parseable string. Раньше
    `Number("2026-10-14T05:48:42Z")` давал NaN и check ошибочно falsebly
    падал на "expired".

  Breaking-change-note для консьюмеров: если клиент читал
  `world.agentpreapprovals` (legacy lowercase output), нужно переключиться
  на `world.agentPreapprovals`. Prototype-side миграция в сопутствующем PR.

## 0.9.0

### Minor Changes

- 4142c3d: Pattern Bank: первая волна `structure.apply` + `explainMatch` + first-class witnesses.

  **@intent-driven/core** (minor):

  - Новая точка `explainMatch(intents, ontology, projection, options)` — единая SDK-surface для authoring-env tooling (Studio viewer + prototype inspector). Возвращает archetype, behavioral (resolvePattern), structural (matched + nearMiss), witnesses, artifactBefore/After, previewPatternId.
  - `evaluateTriggerExplained` — per-requirement breakdown trigger'а. `evaluateTrigger` остаётся wrapper'ом.
  - `matchPatterns(intents, ontology, projection, { includeNearMiss })` — расширенная форма возвращает `{ matched, nearMiss }` с entries вида `{ pattern, explain, missing }`. Legacy shape (array) — без изменений при отсутствии options.
  - `structure.apply(slots, context)` — чистая функция в паттернах, обогащает slots при кристаллизации. Реализовано для 3 паттернов:
    - `subcollections` — добавляет sub-entity секции в detail-архетипе (FK-based auto-discovery с last-camelCase-segment fallback, pluralization y→ies / is→es / s,x,z,ch,sh+es).
    - `grid-card-layout` — выставляет `body.layout="grid"` + cardSpec из witnesses для catalog. Author-override: любой `body.layout` — no-op.
    - `footer-inline-setter` — single-replace intents перемещаются в footer как inline-setters. Author-override: существующие footer items не перетираются.
  - `applyStructuralPatterns(slots, matched, context, preferences, registry)` — встроено в crystallize pipeline (внутри шага 3). Feature-flag `ontology.features.structureApply` как kill-switch.
  - `projection.patterns: { enabled, disabled }` — author-level preference для force-apply / force-skip паттерна. Часть input'а кристаллизации, не snapshot артефакта (§16 перегенерируемость).
  - `artifact.witnesses[]` — pattern matching как first-class §15 finding c `basis: "pattern-bank"`, `reliability: "rule-based"`, `requirements: [{ kind, ok, spec }]`.
  - Helpers `findSubEntities`, `buildSection`, `sectionIdFor`, `buildCardSpec` — экспортированы для переиспользования.

  **@intent-driven/renderer** (minor):

  - `ProjectionRendererV2` prop `artifactOverride` — dev-only override (§27 authoring-env). Приоритетнее штатного `artifact`. Когда задан — используется напрямую; когда отсутствует — поведение без изменений.
  - `ProjectionRendererV2` prop `previewPatternId` — передаётся в ctx для overlay над derived-слотами.
  - Новый primitive `PatternPreviewOverlay` — dashed-border + corner-badge с pattern id. Применяется в `SlotRenderer` и `ArchetypeDetail` для слотов с `source: "derived:..."` когда `previewPatternId` активен.

  **Обратная совместимость:**

  - Все новые поля артефакта (`witnesses`, `pattern`, `body.layout`, `body.cardSpec`, `slots.sections[].source`) — additive.
  - `matchPatterns` legacy array-shape сохранён.
  - `evaluateTrigger` работает идентично.
  - `structure.apply` опциональное — паттерны без него продолжают работать только как matching-only.

## 0.8.0

### Minor Changes

- feat: UX Pattern Layer + Pattern Bank + 3 Gravitino patterns

  - UX Pattern Layer: 5 behavioral patterns (monitoring/triage/execution/exploration/configuration), signal classifier, rendering strategy
  - Pattern Bank: 13 stable structural patterns с trigger/structure/rationale triple, falsification framework, 9 trigger kinds
  - Claude Researcher Pipeline integration: 3 patterns extracted from Apache Gravitino WebUI (hierarchy-tree-nav, discriminator-wizard, m2m-attach-dialog)
  - matchedPatterns field в артефакте кристаллизатора
  - 410 тестов core pass, полная обратная совместимость

## 0.7.2

### Patch Changes

- 7f60b09: feat: публичный экспорт `inferFieldRole` для внешних консументов (zazor #3 analyzer)

  `inferFieldRole` ранее был доступен только через internal `crystallize_v2/ontologyHelpers.js`. Нужен для batch analyzer (`scripts/zazor3-candidates.mjs` в прототипе), который группирует heuristic findings по `witness.pattern`. Additive, не breaking.

## 0.7.1

### Patch Changes

- 6f09f57: feat: публичный экспорт `inferFieldRole` для внешних консументов (zazor #3 analyzer)

  `inferFieldRole` ранее был доступен только через internal `crystallize_v2/ontologyHelpers.js`. Нужен для batch analyzer (`scripts/zazor3-candidates.mjs` в прототипе), который группирует heuristic findings по `witness.pattern`. Additive, не breaking.

## 0.7.0

### Minor Changes

- b6a62e7: feat: witness.pattern — formal grouping key для heuristic findings (§15 v1.10 zazor #3 phase 1)

  - `witness.pattern: string` добавлен в finding/evidence shape (additive, optional).
  - `inferFieldRole` эмитит pattern для всех 12 heuristic branches: `name:title-synonym`, `name:description-synonym`, `name:price-substring`, `name:timer-suffix`, `name:coordinate-set`, `name:address-suffix`, `name:zone-set`, `name:location-set`, `name:badge-status`, `type:number-metric-fallback`, `fallback:info`.
  - `computeAlgebraWithEvidence` эмитит `pattern: "antagonist-declared"` для heuristic-lifecycle entries.
  - Structural / rule-based findings: pattern optional (не требуется — они уже named через свой тип).

  **Convention:** `reliability: "heuristic"` → `witness.pattern` обязано быть заполнено. Linter-enforcement — open task v1.11+.

  Prerequisite infrastructure для zazor #3 phase 2 (promotion writer): analyzer группирует heuristic occurrences по formal pattern без string-parsing basis'ов.

## 0.6.0

### Minor Changes

- daadd3d: feat: witness-of-proof filling (§15 zazor #2)

  - `checkAnchoring` заполняет `reliability` (structural / rule-based / heuristic) и `witness.basis` для всех findings (entity / effect.target / field / witness / condition).
  - `computeAlgebraWithEvidence` добавляет `reliability` alias над существующим `classification` + `witness.basis`.
  - `inferFieldRole` теперь возвращает `{role, reliability, basis}` вместо `string` (internal breaking — 4 call sites в crystallize_v2 мигрированы в этом же релизе). External consumers: обновить `const r = inferFieldRole(...)` → `const r = inferFieldRole(...)?.role`.

  Reliability taxonomy:

  - `structural` — прямая decidable привязка или exhaustive exhaustion для MISS.
  - `rule-based` — через объявленное правило (computePlural, type-based, ontology.invariants).
  - `heuristic` — name-convention без формального правила.

  Runtime `effect.context.__witness` convention вводится в потребляющих репозиториях (scheduler / Rules Engine / invariant checker) — не часть SDK. Поле `witness.counterexample` остаётся зарезервированным (v1.10+).

  **Reliability расширяет capability surface family** (§26 v1.6 #2 insight): `entity.kind` / `role.scope` / `adapter.capabilities` / `reliability` — четвёртый член. Эпистемическая capability, complementary к структурным. Prerequisite infrastructure для zazor #3 (heuristic-once → implication rule).

## 0.5.1

### Patch Changes

- 550d9c2: fix(anchoring): корректная plural-резолюция как в buildTypeMap

  Наивный `endsWith("s") ? slice(0,-1) : name` давал ложные error'ы для
  "activities" → "activitie", "addresses" → "addresse", "deliveries" → "deliverie".
  Теперь `checkAnchoring` использует тот же алгоритм, что `buildTypeMap` в fold.js:
  y → ies, s → ses, иначе + s. Построен collection-lookup: каждая entity
  регистрируется и под singular-именем, и под своей plural-формой.

  Найдено аудитом прототипа (sales, reflect, delivery, invest, messenger) — 80%
  из 134 "errors" были артефактом баги, не реальными ontology gaps.

## 0.5.0

### Minor Changes

- 8b2c20e: feat: binary anchoring gate (§15 zazor #1) — Phase 1

  - Добавлены `checkAnchoring(INTENTS, ONTOLOGY)` и `AnchoringError`.
  - `crystallizeV2` принимает `opts.anchoring: "strict" | "soft"`. Default — `"soft"` в этом релизе (не breaking).
  - `ontology.systemCollections: string[]` — декларация коллекций без доменной сущности (`drafts`, `users`, `scheduledTimers`) для подавления anchoring errors.
  - `checkIntegrity` rule #6 делегирует `checkAnchoring`, back-compat сохранён.
  - Классификация частиц: конструктивные (entity, effect.target base) → error; описательные (field, witness, condition) → warning/info.

  Phase 3 (следующий major) переключит default на `strict` — все потребители получат явную миграционную заметку.

## 0.4.0

### Minor Changes

- 72d1033: Координированный minor-bump всего семейства `@intent-driven/*` для согласованного версионирования публичного npm-релиза.

## v0.3.0 — 2026-04-15

**Asset-boundary, необратимость, temporal scheduler — полевой тест 11 (delivery) + Consolidation Sprint 1.**

### Added

**§19 — Asset-boundary (v1.7 terminological):**

- `ASSET_KINDS` — перечень допустимых видов внешних активов в онтологии
- `getAssets(ontology)` — возвращает `ontology.assets[]`
- `validateAsset(asset)` — валидирует декларацию актива `{ valid, errors }`

**§23 — Необратимость (closure):**

- `mergeIntoContext(context, irrDef)` — добавляет `__irr: { point, at, reason }` к контексту эффекта zero-migration через JSON-поле
- Integrity-правило в validator.js блокирует `α:"remove"` на сущностях с past confirmed high-irr эффектом
- Forward-correction через `α:"replace"` разрешён всегда

**§4 — Temporal scheduler (v1.7):**

- Системные намерения `schedule_timer(afterMs|atISO, target, revokeOn?)` и `revoke_timer(timerId)` — задокументированы как часть core-парадигмы
- `evaluateScheduleV2` parser: `after:"5m"` / `at:"ISO"` / `revokeOn:[patterns]`

### Design notes

Все три дополнения v1.7 применяются совместно в домене delivery (field-test-11). Нет breaking changes — расширение через новые export-пути `./ontology/assets` и `./irreversibility`.

---

## v0.2.0 — 2026-04-14

**Agent layer + materializations extraction from IDF prototype v1.6.2.**

Расширяем ядро модулями, которые окончательно стабилизировались в v1.6
после закрытия 6 §26 open items. Полное покрытие §1 четырёх
материализаций + §5 ролевая таксономия + §14 глобальные инварианты

- §17 agent layer guards.

### Added

**§5 / §17 — Agent layer helpers:**

- `filterWorldForRole(world, ontology, role, viewer)` — viewer-scoped filter с поддержкой many-to-many через `role.scope` (via/viewerField/joinField/localField/statusAllowed), single-owner через `entity.ownerField`, и reference entities через `entity.kind: "reference"` (§5)
- `checkPreapproval(intentId, params, viewer, ontology, world, role)` — декларативные лимиты для агента поверх JWT. 5 типов предикатов: `active`, `notExpired`, `maxAmount`, `csvInclude` (с `allowEmpty`), `dailySum` (с sum-filter) (§17)
- `BASE_ROLES = ["owner", "viewer", "agent", "observer"]` — таксономия базовых ролей (§5 v1.6.1)
- `validateBase`, `getRolesByBase`, `isAgentRole`, `isObserverRole`, `isOwnerRole`, `auditOntologyRoles` — helpers для cross-domain инструментов и SDK defaults

**§1 — Материализации (три новых кроме pixels):**

- `materializeAsDocument(projection, world, viewer, opts)` — generic document-граф для любой projection (catalog/feed/detail/dashboard). Поддержка sub-collections (§1 v1.6)
- `renderDocumentHtml(doc)` — print-ready HTML с inline стилями
- `materializeAsVoice(projection, world, viewer, opts)` — speech-script с turns: `system` / `assistant` / `prompts`. Brevity-heuristics: top-3 catalog, money «2.5 миллионов рублей», русские числительные (§1 v1.6.2)
- `renderVoiceSsml(script)` — SSML XML для TTS-движков (Polly/Yandex SpeechKit)
- `renderVoicePlain(script)` — plain text для debug / IVR baseline

**§14 — Global invariants (v1.6.1):**

- `checkInvariants(world, ontology, opts)` — dispatch по kind, возвращает `{ok, violations}`
- `registerKind(name, handler)` — расширение своим kind'ом
- `KIND_HANDLERS` — встроенные: `role-capability`, `referential`, `transition`, `cardinality`, `aggregate`

### Design notes

Все новые модули — **pure functions** без side effects. Работают над любым domain ontology с корректной декларацией. Backcompat: модули без role.scope / preapproval / invariants / entity.kind продолжают работать как в v0.1.

**Materializations symmetry (§1 proof-of-concept):** все три materializer'а (`document`, `voice`, agent-API в routes) reuse `filterWorldForRole` — viewer-scoping одинаковое. Четыре материализации pixels/voice/agent-API/document — равноправные рендеры одной artifact-модели v2.

### Tests (в prototype)

SDK-extracted модули покрыты тестами в основном repo (будет мигрировано в SDK в v0.3):

- filterWorld: 20 unit-тестов (в т.ч. m2m через assignments)
- baseRoles: 26 unit-тестов + все 8 доменов имеют валидный base
- preapprovalGuard: 16 unit-тестов
- documentMaterializer: 15 unit-тестов
- voiceMaterializer: 17 unit-тестов
- invariants: покрытие через invariantChecker.test.js

Итого: **371 unit-тест** в prototype, ~ 110 — в extracted модулях.

### Build

- Dual ESM + CJS через tsup (без изменений)
- TypeScript types auto-generated
- Нет breaking changes в существующих экспортах

---

## v0.1.0 — 2026-04-14

**Initial extraction from IDF prototype v1.5.**

### Includes

- `useEngine(domain)` — главный React hook
- `fold`, `foldDrafts`, `applyPresentation`, `buildTypeMap`, `filterByStatus` — fold эффектов
- `causalSort` — топологическая сортировка по parent_id
- `computeAlgebra`, `computeAlgebraWithEvidence`, `normalizeEntityFromTarget` — intent algebra (§12)
- `checkComposition` — алгебра композиции α⊗α
- `checkIntegrity` — 7 правил целостности
- `parseCondition`, `parseConditions` — парсер string conditions
- `crystallizeV2` — кристаллизация артефактов проекций
- `validateArtifact` — валидация артефакта v2
- `generateEditProjections`, `findReplaceIntents`, `buildFormSpec` — auto-generated edit-формы
- `registerArchetype`, `prependArchetype`, `selectArchetype`, `getArchetypes` — control-архетипов registry

### Build

- Dual ESM (117KB) + CJS (119KB) via tsup 8.5
- TypeScript types (.d.ts, 155KB) auto-generated из JSDoc + JS files

### Tested

- 238 unit-тестов проходят
- IDF prototype v1.5 — protokol of 7 доменов, 481 намерение
