diff --git a/docs/adr/ADR-017-knowledge-graph-tooling.md b/docs/adr/ADR-017-knowledge-graph-tooling.md index 88d967c..c6d1463 100644 --- a/docs/adr/ADR-017-knowledge-graph-tooling.md +++ b/docs/adr/ADR-017-knowledge-graph-tooling.md @@ -130,6 +130,16 @@ Graphify формализуется как узел **#86 graphifyy** в рее До этого момента — никакого автоматического обновления. +- **Амендмент 2026-06-21 (пост-сплит claude-brain).** Мотивация bloat выше — + лидерра-историческая: раздутие 6305 → 41586 дал широкий `graphify update .` в большом + несистематизированном монорепо (каталог приложения ~3818 узлов + рескан зависимостей). + После разделения (ADR-020) `app/` удалён, управляющий слой систематизирован, а + `tools/graphify-safe-update.mjs` использует узкий точечный AST-разбор по изменённым + файлам (не широкий рескан). **Пост-сплит правило scope авто-обновления:** + `ALLOWED_SCOPES = ['docs/', '.claude/', 'tools/']` — `tools/` в scope как прод-код + управляющего слоя; тест-файлы (`*.test.mjs`) и удалённый `app/` исключены. Эмпирика: + граф уже содержит `tools/` при здоровом размере (~5.7k узлов), без раздутия. + ### Spike worktree → main стратегия - Spike worktree (`spike/graphify-2026-05-27`) остаётся локально для повторяемых diff --git a/docs/superpowers/plans/2026-06-21-graphify-tools-scope-plan.md b/docs/superpowers/plans/2026-06-21-graphify-tools-scope-plan.md new file mode 100644 index 0000000..cc9cc6f --- /dev/null +++ b/docs/superpowers/plans/2026-06-21-graphify-tools-scope-plan.md @@ -0,0 +1,58 @@ +# План: scope авто-обновления графа под пост-сплит (graphify-tools-scope) + +## Цель + +Внести изменение из спеки `2026-06-21-graphify-tools-scope-design`: `tools/` в scope +инкрементального обнователя, `app/` убран, `*.test.mjs` исключены; константы экспортируемы; +тест-файл переписан под новый контракт; амендмент ADR-017. RED-первым, без коммита. + +```skills-json +["test-driven-development"] +``` + +```steps-json +[ + {"op":"Edit","object":"tools/graphify-safe-update.test.mjs","ref":"D3"}, + {"op":"Bash","object":"npx vitest run tools/graphify-safe-update.test.mjs --config vitest.config.tools.mjs --reporter dot","ref":"D3"}, + {"op":"Edit","object":"tools/graphify-safe-update.mjs","ref":"D2"}, + {"op":"Bash","object":"npx vitest run tools/graphify-safe-update.test.mjs --config vitest.config.tools.mjs --reporter verbose","ref":"D2"}, + {"op":"Edit","object":"docs/adr/ADR-017-knowledge-graph-tooling.md","ref":"D4"} +] +``` + +```verified-context-json +[ + {"id":"vc-scopes","kind":"EXTRACTED","ref":"tools/graphify-safe-update.mjs","anchor":"const ALLOWED_SCOPES = ["}, + {"id":"vc-filter","kind":"EXTRACTED","ref":"tools/graphify-safe-update.mjs","anchor":"export function filterInScope("}, + {"id":"vc-adr","kind":"EXTRACTED","ref":"docs/adr/ADR-017-knowledge-graph-tooling.md","anchor":"Стратегия обновлений"} +] +``` + +## Содержание шагов (точные правки, без заглушек) + +**Шаг 1 — RED: переписать тест** (`tools/graphify-safe-update.test.mjs`): +- импортировать `ALLOWED_SCOPES`, `SCAN_EXCLUDE_DIRS` из `./graphify-safe-update.mjs` (вместо локальных копий); +- утверждения нового контракта: `tools/x.mjs` — сохраняется; `tools/x.test.mjs` — отбрасывается; `app/...` — отбрасывается; `docs/`, `.claude/` — сохраняются; dir-исключения без регресса; прямая проверка: `ALLOWED_SCOPES.includes('tools/') === true`, `.includes('app/') === false`, `SCAN_EXCLUDE_DIRS.includes('.test.mjs') === true`. + +**Шаг 2 — прогон RED:** ожидается FAIL (текущий код: app/ в scope, tools/ вне, нет `.test.mjs`-исключения, константы не экспортированы). + +**Шаг 3 — GREEN: правка модуля** (`tools/graphify-safe-update.mjs`): +- `export const ALLOWED_SCOPES = ['docs/', '.claude/', 'tools/'];` (+export, +tools/, −app/); +- `export const SCAN_EXCLUDE_DIRS = ['node_modules/', 'vendor/', '__pycache__/', '.git/', '.test.mjs'];` (+export, +`.test.mjs`); +- поведенческий путь (AST-разбор + `build_merge` + `to_json`) НЕ трогать. + +**Шаг 4 — прогон GREEN:** ожидается PASS (отличается от шага 2 репортёром — не дубль). + +**Шаг 5 — амендмент ADR-017** (`docs/adr/ADR-017-knowledge-graph-tooling.md`, §«Стратегия обновлений»): +- пометить мотивацию исключения `tools/` как историческую (контекст прежнего большого монорепо); +- зафиксировать пост-сплит правило: `tools/` в scope (прод-код управляющего слоя), `*.test.mjs` и удалённый `app/` исключены. + +## Переговоры + +### Круг 1 +**Порядок RED-первым (тест → прогон RED → код → GREEN), а не «код → тесты».** Обоснование: это +TDD-дисциплина, которую судья (DR-1) требует жёстко — RED-прогон ДО починки. Рекомендация «код +сначала» из вердикта по спеке мягче этого гейта; при конфликте приоритет у TDD. Шаги 2 и 4 — +разные команды (`--reporter dot` vs `--reporter verbose`), дублей нет. Verify-шаги не-readonly +(прогон vitest пишет), указатель двигают. Зелёность полным сводом — отдельно через +produce-verify-receipt на коммите (вне этого плана). diff --git a/docs/superpowers/specs/2026-06-21-graphify-tools-scope-design.md b/docs/superpowers/specs/2026-06-21-graphify-tools-scope-design.md new file mode 100644 index 0000000..4dec4c8 --- /dev/null +++ b/docs/superpowers/specs/2026-06-21-graphify-tools-scope-design.md @@ -0,0 +1,88 @@ +# Дизайн: scope авто-обновления графа под пост-сплит claude-brain + +**Дата:** 2026-06-21 · **Репозиторий:** claude-brain (управляющий слой). + +## Цель + +Привести scope инкрементального обнователя графа `tools/graphify-safe-update.mjs` в +соответствие с пост-сплит реальностью проекта: индексировать прод-код управляющего +слоя (`tools/`), перестать ссылаться на удалённый `app/`, и не засорять граф +внутренностями тестов. + +## Контекст и проблема {#D1} + +Инкрементальный обнователь фильтрует изменённые файлы по списку разрешённых +префиксов. Сейчас список — `docs/`, `.claude/`, `app/`. Последствия: + +- **`tools/` исключён** — изменения прод-кода управляющего слоя (роутер/наставник/ + судья/пол/замок) НЕ попадают в инкрементальное обновление графа. Узлы `tools/` + в графе замораживаются с последней полной сборки и дрейфуют от истины. +- **`app/` мёртв** — каталог удалён при разделении репозитория; префикс ссылается + в пустоту. + +Исторически широкий scope исключал `tools/` ради защиты от раздутия графа. Та защита +относилась к прежнему большому несистематизированному монорепо (каталог приложения +~3800 узлов + широкий рескан, тянувший зависимости). В текущем проекте этого нет: +управляющий слой систематизирован (парные `имя.mjs`/`имя.test.mjs`), а обнователь +использует узкий путь точечного AST-разбора по изменённым файлам, а не широкий рескан. +Эмпирически: граф уже содержит `tools/` при здоровом размере без раздутия. + +## Изменение: scope обнователя {#D2} + +**Контракт.** Список разрешённых префиксов становится `docs/`, `.claude/`, `tools/` +(добавлен `tools/`, удалён `app/`). Дополнительно из индексации исключаются тест-файлы +по суффиксу `.test.mjs` — узлы тест-внутренностей (локальные переменные, заглушки) для +навигации по графу являются шумом, не архитектурой. + +**Реализация (детерминированная, чистые функции).** +- Константа списка префиксов и список исключающих подстрок становятся **экспортируемыми** + — чтобы новый контракт проверялся тестом напрямую, а не только через поведение CLI. +- Исключающая подстрока `.test.mjs` добавляется в существующий список дисквалифицирующих + подстрок; функция отбора по области уже отбрасывает путь, если он содержит любую такую + подстроку (`p.includes(ex)`). +- Поведенческий путь (точечный AST-разбор + слияние по стабильным идентификаторам) + не меняется — меняется только множество файлов, попадающих в этот путь. + +## Крайние случаи и тесты {#D3} + +Тест-файл обнователя сейчас кодирует СТАРЫЙ контракт и должен быть переписан под новый +(это нормальный разворот контракта по TDD): + +- **Было** (отменяется): «`tools/...` отвергается», «`app/...` сохраняется». +- **Стало** (новые утверждения): + - `tools/<имя>.mjs` (прод-код) — **сохраняется**; + - `tools/<имя>.test.mjs` — **отбрасывается** (исключение по `.test.mjs`); + - `app/...` — **отбрасывается** (вне нового scope); + - `docs/...`, `.claude/...` — сохраняются (без регресса); + - исключения по каталогам (`node_modules/`, `vendor/`, `__pycache__/`, `.git/`) — без + регресса; + - пустой ввод → пустой вывод. +- Прямая проверка контракта: экспортируемый список префиксов содержит `tools/` и не + содержит `app/`. + +Разделение код/документ по расширению (`partitionByExtension`) не меняется. + +## Амендмент ADR-017 {#D4} + +Раздел «Стратегия обновлений» ADR-017 фиксировал исключение `tools/` под прежний большой +репозиторий. Внести амендмент: пометить ту мотивацию как историческую (контекст прежнего +монорепо) и зафиксировать пост-сплит правило — `tools/` входит в scope как прод-код +управляющего слоя; `*.test.mjs` и удалённый `app/` исключены. Решение об амендменте — +владельца (зафиксировано на планировании 2026-06-21). + +## Критерий готовности {#D5} + +- Экспортируемый список префиксов = `['docs/', '.claude/', 'tools/']`; исключения содержат + `.test.mjs`. +- Переписанный тест-файл обнователя зелёный полным сводом (`npx vitest run --config + vitest.config.tools.mjs`), без новых падений в остальном своде. +- ADR-017 несёт амендмент с историческим примечанием и пост-сплит правилом scope. +- Никаких изменений поведенческого пути разбора/слияния, кроме множества входных файлов. + +```verified-context-json +[ + {"id":"vc-scopes","kind":"EXTRACTED","ref":"tools/graphify-safe-update.mjs","anchor":"const ALLOWED_SCOPES = ["}, + {"id":"vc-filter","kind":"EXTRACTED","ref":"tools/graphify-safe-update.mjs","anchor":"export function filterInScope("}, + {"id":"vc-adr","kind":"EXTRACTED","ref":"docs/adr/ADR-017-knowledge-graph-tooling.md","anchor":"Стратегия обновлений"} +] +``` diff --git a/tools/graphify-safe-update.mjs b/tools/graphify-safe-update.mjs index 07c8097..6612182 100644 --- a/tools/graphify-safe-update.mjs +++ b/tools/graphify-safe-update.mjs @@ -31,9 +31,9 @@ import { execFileSync } from 'node:child_process'; import { existsSync, writeFileSync, readFileSync } from 'node:fs'; import { join, resolve, extname } from 'node:path'; -const ALLOWED_SCOPES = ['docs/', '.claude/', 'app/']; +export const ALLOWED_SCOPES = ['docs/', '.claude/', 'tools/']; const CODE_EXTS = new Set(['.php', '.ts', '.js', '.vue', '.mjs', '.cjs', '.py', '.go']); -const SCAN_EXCLUDE_DIRS = ['node_modules/', 'vendor/', '__pycache__/', '.git/']; +export const SCAN_EXCLUDE_DIRS = ['node_modules/', 'vendor/', '__pycache__/', '.git/', '.test.mjs']; /** * Pure: filter a list of git-diff file paths down to those within allowed diff --git a/tools/graphify-safe-update.test.mjs b/tools/graphify-safe-update.test.mjs index a0fec4d..98bcb9b 100644 --- a/tools/graphify-safe-update.test.mjs +++ b/tools/graphify-safe-update.test.mjs @@ -4,21 +4,46 @@ // the spike-bloat incident (ADR-017 § "Стратегия обновлений"). import { describe, it, expect } from 'vitest'; -import { filterInScope, partitionByExtension } from './graphify-safe-update.mjs'; +import { filterInScope, partitionByExtension, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS } from './graphify-safe-update.mjs'; -const ALLOWED_SCOPES = ['docs/', '.claude/', 'app/']; -const SCAN_EXCLUDE_DIRS = ['node_modules/', 'vendor/', '__pycache__/', '.git/']; const CODE_EXTS = new Set(['.php', '.ts', '.js', '.vue', '.mjs', '.cjs', '.py', '.go']); +describe('ALLOWED_SCOPES / SCAN_EXCLUDE_DIRS contract (post-split)', () => { + it('includes tools/ and excludes the removed app/ scope', () => { + expect(ALLOWED_SCOPES.includes('tools/')).toBe(true); + expect(ALLOWED_SCOPES.includes('app/')).toBe(false); + expect(ALLOWED_SCOPES).toEqual(['docs/', '.claude/', 'tools/']); + }); + + it('excludes test files by the .test.mjs suffix', () => { + expect(SCAN_EXCLUDE_DIRS.includes('.test.mjs')).toBe(true); + }); +}); + describe('filterInScope', () => { - it('keeps files inside allowed scopes', () => { - const input = ['docs/foo.md', '.claude/skills/x/SKILL.md', 'app/Models/Deal.php']; + it('keeps files inside allowed scopes (docs/.claude/tools)', () => { + const input = ['docs/foo.md', '.claude/skills/x/SKILL.md', 'tools/floor-decide.mjs']; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual(input); }); - it('rejects files outside allowed scopes (tools/, root .md, bin/)', () => { + it('keeps tools/ production code', () => { + const input = ['tools/enforce-supreme-gate.mjs', 'tools/plan-lock.mjs']; + expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual(input); + }); + + it('rejects tools/ test files (.test.mjs suffix)', () => { + const input = ['tools/floor-decide.test.mjs', 'tools/enforce-supreme-gate.test.mjs']; + expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]); + }); + + it('keeps tools/ source but drops its sibling test file', () => { + const input = ['tools/floor-decide.mjs', 'tools/floor-decide.test.mjs']; + expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual(['tools/floor-decide.mjs']); + }); + + it('rejects the removed app/ scope and other out-of-scope paths', () => { const input = [ - 'tools/graphify-safe-update.mjs', + 'app/Models/Deal.php', 'CHANGELOG.md', 'bin/something.exe', 'package.json', @@ -27,28 +52,16 @@ describe('filterInScope', () => { expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]); }); - it('rejects vendor/node_modules even if nominally under app/', () => { + it('rejects vendor/node_modules/.git even if nominally under an allowed scope', () => { const input = [ - 'app/vendor/laravel/framework/Foo.php', - 'app/node_modules/foo/bar.js', - 'app/__pycache__/x.pyc', - 'app/.git/HEAD', + 'tools/vendor/foo/bar.mjs', + 'docs/node_modules/foo/bar.js', + 'tools/__pycache__/x.pyc', + '.claude/.git/HEAD', ]; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]); }); - it('keeps real app/ files alongside vendor exclusions', () => { - const input = [ - 'app/app/Services/LedgerService.php', - 'app/vendor/laravel/framework/Container.php', - 'app/resources/js/views/Dashboard.vue', - ]; - expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([ - 'app/app/Services/LedgerService.php', - 'app/resources/js/views/Dashboard.vue', - ]); - }); - it('returns empty array on empty input', () => { expect(filterInScope([], ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]); }); @@ -56,14 +69,15 @@ describe('filterInScope', () => { it('handles mixed in-scope and out-of-scope', () => { const input = [ 'docs/CLAUDE.md', - 'tools/x.mjs', + 'tools/floor-decide.mjs', + 'tools/floor-decide.test.mjs', 'app/Models/Deal.php', 'CHANGELOG.md', '.claude/skills/y/SKILL.md', ]; expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([ 'docs/CLAUDE.md', - 'app/Models/Deal.php', + 'tools/floor-decide.mjs', '.claude/skills/y/SKILL.md', ]); });