feat(graphify): scope авто-обновления под пост-сплит — tools/ в scope, app/ убран, *.test.mjs исключён (амендмент ADR-017)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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`) остаётся локально для повторяемых
|
||||
|
||||
@@ -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 на коммите (вне этого плана).
|
||||
@@ -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":"Стратегия обновлений"}
|
||||
]
|
||||
```
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user