From 6a8761dddcf5507543fac57374435656bc458cb9 Mon Sep 17 00:00:00 2001 From: vitaliytv Date: Mon, 31 Aug 2026 11:41:22 +0300 Subject: [PATCH 1/3] =?UTF-8?q?docs(specs):=20=D0=B7=D1=80=D1=96=D0=B7=206?= =?UTF-8?q?=20=E2=80=94=20=D1=80=D1=96=D1=88=D0=B5=D0=BD=D0=BD=D1=8F=206?= =?UTF-8?q?=20=D1=96=207=20=D1=89=D0=BE=D0=B4=D0=BE=20=D0=BA=D0=BE=D0=BD?= =?UTF-8?q?=D1=81=D1=8E=D0=BC=D0=B5=D1=80=D1=81=D1=8C=D0=BA=D0=B8=D1=85=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=B2=D0=B5=D1=80=D1=85=D0=BE=D0=BD=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6. text/run-v8r: замінити зовнішній v8r нативною JSON-Schema валідацією, каталог вшити в бінар. Концерн уже портований, але порт зберіг спавн `bun x v8r` — нативний детектор запускає пакетний менеджер заради JS-валідатора, що читає наші ж завендорені схеми. Закриває заразом питання 3.2 МБ і знімає останню неявну залежність від bun у детекторному контурі. 7. $schema: власний Apicurio-реєстр замість unpkg. Записано дві умови як частину рішення, а не побажання: пінована версія (інакше повертається дрейф класу KNOWN_PLUGIN_RANGES) і анонімне читання (інакше повторюється мовчазна відмова, вже виміряна на git.7n.ai — 401 на анонімний pull). Прийнята слабкість записана явно: airgapped-консюмер підказок IDE не матиме. Co-Authored-By: Claude Opus 5 --- .../2026-08-31-slice6-consumer-surfaces.md | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 docs/specs/2026-08-31-slice6-consumer-surfaces.md diff --git a/docs/specs/2026-08-31-slice6-consumer-surfaces.md b/docs/specs/2026-08-31-slice6-consumer-surfaces.md new file mode 100644 index 000000000..6e3f03b0a --- /dev/null +++ b/docs/specs/2026-08-31-slice6-consumer-surfaces.md @@ -0,0 +1,92 @@ +# Зріз 6 — рішення щодо консюмерських поверхонь + +**Дата:** 2026-08-31. **Статус:** ухвалено власником ітеративним розбором. +**Зв'язані:** `2026-08-01-rules-cli-phase8-skeleton.md` §12, +`2026-08-30-contract-roadmap-blocked-concerns.md` (рішення 1-5, форма +контракту `5.0.0`). + +Цей документ несе рішення, які **не стосуються форми контракту**, але без +яких зріз 6 забирає в консюмера канали, якими той досі щось діставав. + +## Рішення 6 — `text/run-v8r`: нативна валідація, каталог вшитий + +### Стан на момент рішення + +Концерн **уже портований нативно** +(`crates/rules-core/src/concerns/text_run_v8r.rs`, у реєстрі детекторів +`concerns/mod.rs:261`) — але порт зберіг спосіб виклику: `bun x v8r `. +Тобто нативний Rust-детектор спавнить пакетний менеджер, щоб запустити +JS-валідатор, який читає наші ж завендорені схеми. `v8r` при цьому в реєстрі +тулів **відсутній** (`tools ensure` про нього не знає). + +`npm/schemas/vendor/**` — 21 схема, 3.2 МБ, офлайн-каталог для нього. +Директорія **захищена** (`CLAUDE.md`), тож рішення стосується лише способу +доставки, не вмісту. + +### Рішення + +Замінити зовнішній `v8r` нативною валідацією: JSON-Schema крейт у Rust плюс +добір схеми за завендореним каталогом. Каталог вшивається в бінар на збірці. + +**Чому не `npm:v8r` у реєстрі тулів** (варіант, ідентичний тому, що #590 +зробив для `eslint`/`oxlint`/`jscpd`, і тому дешевий): він консервує +архітектурну аномалію — кожен крок ланцюжка «bun → npm → JS-валідатор → наші +схеми» ми вміємо робити самі. І він лишає питання 3.2 МБ відкритим, тоді як +нативна валідація закриває його тим самим рухом. + +`run-v8r` — детектор, тобто біжить на кожному повному лінті; спавн `bun x` +там означає плату за запуск пакетного менеджера заради валідації JSON. + +Побічно знімається остання неявна залежність від bun у детекторному контурі +(після #590, який відвʼязав `lang-js` від `path:bunx`). + +## Рішення 7 — `$schema`: власний Apicurio-реєстр + +### Проблема + +`npm/bin/n-rules-cli.mjs:126` тримає +`CONFIG_SCHEMA_URL = 'https://unpkg.com/@7n/rules/schemas/n-rules.json'`, і +цей рядок ми пишемо в `.n-rules.json` **консюмера** (наш власний файл теж +його має, рядок 2). Після зняття npm `unpkg` віддасть 404, і IDE перестане +валідувати конфіг — **мовчки**, без жодного повідомлення. + +### Рішення + +Схеми переїжджають у власний **Apicurio Registry**. Це не просто інший хост: +реєстр із версіонуванням артефактів і REST-API, тобто вміє віддавати схему +за конкретною версією. + +### Дві умови, без яких рішення не працює + +Обидві — частина рішення, не побажання. + +1. **Пінована версія, не `latest`.** `$schema` має вказувати на конкретну + версію артефакту, і `n-rules` має писати саме той URL, що відповідає + версії бінаря, який синкає. Інакше повертається та сама вада, від якої + йдемо: консюмер на старішому бінарі дістає підказки від новішої схеми — + дрейф того ж класу, що `KNOWN_PLUGIN_RANGES` (#578), лише зовні й + непомітно. +2. **Анонімне читання.** IDE не має креденшелів. Якщо реєстр закритий, + підказки не працюють, і знову **мовчки**. Це вже виміряна пастка: + `git.7n.ai` OCI-реєстр анонімний pull не віддає (`GET /v2/` → `401`, + зміряно 2026-08-30). Якщо Apicurio стане за тим самим периметром, + `$schema` повторить долю `unpkg` — інший URL, той самий мовчазний + відмов для стороннього. + +### Прийнята слабкість + +Airgapped-консюмер підказок IDE не матиме. Мережа повертається в контур, з +якого ми її прибирали (`--offline` транспорту проєктувався саме під такі +дерева). Це прийнятно, бо стосується **зручності IDE**, а не роботи +інструмента — але записано явно, щоб згодом не зʼявилось як «раптова» вада. + +### Відкинуті варіанти + +- **Локальний файл, який пише бінар** — давав версійну відповідність за + побудовою й повну незалежність від мережі, але додавав ще один артефакт у + чуже дерево, тобто розширював Д4. +- **SchemaStore** — консюмеру не треба нічого, але це залежність від + каталогу, який ми не контролюємо, і публічне зобовʼязання щодо імені + `.n-rules.json`, яке ми вже одного разу міняли (слід у + `n-rules-cli.mjs:258`). +- **Прибрати `$schema`** — свідома втрата реальної зручності. From 3455783214a8c632c05d841509b0bb9608c0b593 Mon Sep 17 00:00:00 2001 From: vitaliytv Date: Mon, 31 Aug 2026 12:03:26 +0300 Subject: [PATCH 2/3] =?UTF-8?q?docs(specs):=20=D1=80=D1=96=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8F=208=20=D1=96=209=20=E2=80=94=20exports=20?= =?UTF-8?q?=D1=96=20=D1=80=D0=B5=D1=94=D1=81=D1=82=D1=80=20=D0=BF=D1=83?= =?UTF-8?q?=D0=B1=D0=BB=D1=96=D1=87=D0=BD=D0=BE=D0=B3=D0=BE=20=D1=8F=D0=B4?= =?UTF-8?q?=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8. exports помирає природно: більшість споживачів (types.mjs, llm.mjs, ensure-tool) зникає за рішеннями 3 і слотовими світами. Справжній залишок — тестова інфраструктура плагінів; тести переїжджають у Rust разом із кодом, прецедент plugin_lang_js.rs на 86 тестів проти справжнього .wasm. Межа названа: тести хоста лишаються JS до порту оркестрації. 9. Публічне ядро несе власний OCI-реєстр. Рекомендація фасилітатора була спершу перевірити конфігурацію Forgejo, потім гібрид ghcr.io — аргумент про різну ціну недоступності схем і бінаря. Власник обрав свідомо, ризик прийнятий. З рішення 9 випливає, що офлайн-дисципліна oci-dist і CI-образи з вбудованим бінарем перестають бути зручністю й стають несучими. Co-Authored-By: Claude Opus 5 --- .../2026-08-31-slice6-consumer-surfaces.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/specs/2026-08-31-slice6-consumer-surfaces.md b/docs/specs/2026-08-31-slice6-consumer-surfaces.md index 6e3f03b0a..63be5ce64 100644 --- a/docs/specs/2026-08-31-slice6-consumer-surfaces.md +++ b/docs/specs/2026-08-31-slice6-consumer-surfaces.md @@ -90,3 +90,73 @@ Airgapped-консюмер підказок IDE не матиме. Мережа `.n-rules.json`, яке ми вже одного разу міняли (слід у `n-rules-cli.mjs:258`). - **Прибрати `$schema`** — свідома втрата реальної зручності. + +## Рішення 8 — `exports["./rules/*"]`: помирає природно, тести плагінів переїжджають у Rust + +### Що показав розбір + +`exports` у `npm/package.json` — канал, яким **плагіни тягнуть спільний JS із +ядра**. Заміряно (`grep -rho "@7n/rules/[a-z/.-]*\.mjs"`): + +| що тягнуть | разів | +|---|---:| +| `scripts/lib/lint-surface/types.mjs` | 28 | +| `scripts/utils/test-helpers.mjs` | 18 | +| `scripts/lib/ci-artifact-collect.mjs` | 10 | +| `rules/test/coverage/lib/llm.mjs` | 10 | +| `scripts/lib/ensure-tool.mjs` | 6 | + +**Більшість цих споживачів зникає за вже ухваленими рішеннями:** `types.mjs` +не потрібен, коли типи приходять із WIT (рішення 3); `llm.mjs` іде разом із +`coverage-provider` у слотовий світ; `ensure-tool` — клас B розвідки +прогалини. Тобто продуктивне спільне використання вмирає разом із +JS-поверхнею плагінів, і окремого рішення не потребує. + +### Справжній залишок — тестова інфраструктура + +`test-helpers.mjs` (18 споживачів) тримають **тести плагінів на vitest**. +Коли плагін стає чистим wasm-компонентом, його продуктивний код Rust, а +тести лишаються JS і тягнуть хелпери каналом, якого не буде. + +**Рішення:** тести переїжджають у Rust разом із кодом. Після рішення 3 +плагін І Є крейт, тож тести природно живуть там же. Прецедент доведений і +працює: `crates/rules-plugin-host/tests/plugin_lang_js.rs` — 86 тестів проти +справжнього зібраного `.wasm`. + +**Межа рішення, названа явно.** Частина цих тестів перевіряє не гостя, а +поведінку хоста на JS-боці (резолв, драбина, диспатч). Доки `run-fix.mjs` і +`detect.mjs` лишаються JS (клас B, не A), їхні тести природно лишаються JS. +Рішення 8 закриває тести **плагінів**, але не всю тестову поверхню; повне +зникнення vitest настане лише разом із портом оркестрації. + +## Рішення 9 — публічне ядро несе власний OCI-реєстр + +`git.7n.ai` анонімного читання не віддає (`GET /v2/` → `401`, зміряно +2026-08-30). **Рішення:** власний OCI-реєстр (Harbor/Zot) з анонімним +читанням, узгоджено з рішенням 7 про власний Apicurio. + +### Заперечення, висловлене й відхилене + +Рекомендація фасилітатора була інша: спершу перевірити, чи анонімне читання +на `git.7n.ai` — це інстансна конфігурація Forgejo (`401` не доводить +архітектурної неможливості), і лише якщо ні — гібрид `ghcr.io` + `git.7n.ai`. +Аргумент: **схеми й бінарі мають різну ціну недоступності**. Лежить +Apicurio — консюмер втрачає підказки IDE; лежить OCI-реєстр із ядром — +консюмер не може поставити чи оновити інструмент узагалі. + +Власник обрав власний реєстр свідомо. Ризик прийнятий. + +### Що з цього рішення випливає негайно + +Доступність публічного каналу стає нашою, тож дві речі перестають бути +зручністю й стають **несучими**: + +1. **Офлайн-дисципліна `oci-dist`** — `--offline` як first-class режим, кеш, + lock із точними digest. Проєктувалась під airgapped-дерева; тепер це ще + й буфер на випадок недоступності власного реєстру. +2. **CI-образи, що вже несуть бінар** (рішення §12.2). Проєктувалось як + економія часу прогону; тепер це єдине, що тримає CI консюмера, коли + реєстр лежить. + +Обидві треба реалізовувати з цим статусом, а не як оптимізації. + From 05aed1b1faa0d5f2a75506bf3b21500db5e009fc Mon Sep 17 00:00:00 2001 From: vitaliytv Date: Mon, 31 Aug 2026 12:39:20 +0300 Subject: [PATCH 3/3] =?UTF-8?q?docs(specs):=20=D1=80=D1=96=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=BD=D1=8F=2010=20=D1=96=2011=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D1=96=D0=B4=D1=81=D1=82=D0=B0=D0=B2=D0=B0=20hook=20=D1=96=20?= =?UTF-8?q?=D0=B3=D0=B5=D0=B9=D1=82=20=D0=BF=D1=80=D0=BE=D1=82=D0=B8=20?= =?UTF-8?q?=D0=B7=D0=B0=D1=81=D1=82=D0=B0=D1=80=D1=96=D0=BB=D0=B8=D1=85=20?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D1=96=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 10. hook лишається портованим, підстава переписується: вимірювання (hook_cmd.rs:32-39) дало менше 3% латентності від старту node, тобто виграш зрізу в межах похибки. Порт потрібен, але як крок до бінаря як вхідної точки, не заради 3%. Цифра фіксується явно, щоб хибна підстава не була процитована як прецедент. 11. Тест на ПРИЧИНУ для кожного запису, що стверджує живе обмеження. Ключова відмінність: тест на наслідок консервує обмеження (саме так rejects_p2_component_profile зробив засновку липкою), тест на причину червоніє, коли причина зникає. Заміряно: 105 записів реєстру, з них 30 стверджують живе обмеження. Рекомендація фасилітатора була вужча (лише чотири контрактні блокери); власник обрав повне покриття, вимірювання показує здійсненність. Окремо: поширити CRC на файли без frontmatter. Co-Authored-By: Claude Opus 5 --- .../2026-08-31-slice6-consumer-surfaces.md | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/docs/specs/2026-08-31-slice6-consumer-surfaces.md b/docs/specs/2026-08-31-slice6-consumer-surfaces.md index 63be5ce64..e9b99e7d1 100644 --- a/docs/specs/2026-08-31-slice6-consumer-surfaces.md +++ b/docs/specs/2026-08-31-slice6-consumer-surfaces.md @@ -160,3 +160,81 @@ Apicurio — консюмер втрачає підказки IDE; лежить Обидві треба реалізовувати з цим статусом, а не як оптимізації. +## Рішення 10 — `hook` лишається портованим, підстава переписується + +Зріз 4 портував `hook` («найгарячіша поверхня продукту, стріляє після кожної +правки файлу агентом») з підставою про латентність старту Node. Розвідка +оркестрації це **зміряла**: `hook_cmd.rs:32-39` — старт node дає **менше 3%** +латентності. Виграш, заради якого робився зріз, у межах похибки. + +**Рішення:** код лишається, підстава переписується. Порт `hook` справді +потрібен — не заради 3%, а тому що крок 0 плану робить бінар вхідною точкою, +і `hook` мусить бути нативним разом з усім іншим. Підстава була хибна, +висновок правильний. + +Виміряні 3% фіксуються **явно**, щоб хибна підстава не була процитована як +прецедент для наступного зрізу — саме так поширювався застарілий доккомент +`lint_plan.rs:9-27` (за добу проріс у план і в §12.4.1 спеки зрізу 6). + +Повернення в JS відкинуто: воно викидає робочий код у найгарячішій поверхні, +який довелося б написати знову. + +## Рішення 11 — гейт проти рацій, що переживають свою причину + +### Масштаб класу + +За одну добу — **вісім** випадків: `tauri/release` (§2.97), +`component_profile` в `oci-dist` (§2.101), два в класифікації канонів +(§2.99), `test/stryker_config`, `rules-docs`, дискавері в `lint_plan.rs`, +підстава зрізу `hook`. Плюс список відкритих питань, де `bun/package_json` +висів уже вирішеним. + +### Дві виміряні діри + +1. `FIX_STAYS_IN_JS` звіряється машинно **за складом**; її причини — вільний + текст, не звірюваний ні з чим. +2. CRC-гейт доків стереже **дрейф тексту, не правдивість**: валідний CRC над + твердженням, яке код спростовує + (`plugins/lang-rust/coverage-provider/docs/provider.md:15`), і файл на 612 + рядків поза гейтом узагалі, бо без frontmatter (`npm/bin/docs/n-rules.md`). + +### Рішення: тест на ПРИЧИНУ для кожного запису, що стверджує живе обмеження + +**Ключова відмінність, без якої гейт не працює.** `oci-dist` мав тест +`rejects_p2_component_profile` — і він не врятував, а навпаки, зробив +застарілу засновку липкою: доки тест зелений, обмеження виглядало +обґрунтованим. + +Різниця в тому, що саме стверджує тест: + +- тест на **наслідок** («P2 відхиляється», «гість не має гілки `fix`») — + **консервує** обмеження; +- тест на **причину** («napi-міст будує `FixRequest::files` лише з полів + `file` переданих violations») — **червоніє, коли причина зникає**. + +`test/stryker_config` — точна ілюстрація: якби причина була закріплена +тестом, то в момент, коли міст навчився `fix_glob` (§2.95), тест би впав, і +розрив виявився б одразу, а не через місяці. + +### Обсяг + +Заміряно 2026-08-31: у реєстрі **105 записів**, із них **30 стверджують живе +обмеження** — решта фіксують зроблену роботу й тесту на причину не +потребують за визначенням. Тобто «для кожного запису» — це 30 тестів. + +Рекомендація фасилітатора була вужча (тест на причину лише для чотирьох +контрактних блокерів, термін придатності для решти) з аргументом про ціну. +Власник обрав повне покриття; вимірювання показує, що воно здійсненне. + +### Окремо — друга діра + +Поширити CRC-гейт на файли **без frontmatter**. Незалежна дрібна робота: +файл не має бути невидимим для гейта лише тому, що в ньому немає заголовка. + +### Що відкинуто + +**Посилання на перевірюваний артефакт** (причина мусить називати +символ/рядок, гейт перевіряє існування) — найдешевше й виглядає розумним +компромісом, але **не спіймало б жодного** з восьми випадків: у всіх символ +лишався живим, змінювалась поведінка. +