27289c056a
Tooling formalization (4-file sync via normative-sync agent): - Tooling Прил. Н v2.24 (+§4.59 #86 graphifyy + 19-я подкатегория knowledge-graph-tooling) - Pravila v1.43 (§13.2 +абзац knowledge-graph-tooling) - PSR_v1 v3.23 (R10.1 Блок 1 +graphifyy, R15.6 +knowledge-graph-tooling) - CLAUDE.md v2.31 -> v2.33 (§3.3 +#86, §5 п.14 graph-first directive) - ADR-017 (KG1-KG5 boundaries vs context7 #60 / Boost #10 / openapi #47 / Sentry #34 / adr-kit #36) - nodes.yaml +#86 + classification knowledge_graph_query - routing-off-phase.md auto-regen via registry-render.mjs Ops-wiring (operationalization): - Junction graphify-out/ -> .claude/worktrees/graphify-spike/graphify-out/ (mklink /J) - .gitignore +graphify-out/ + graphify-out-*/ - CLAUDE.md §5 п.14 graph-first directive - tools/graphify-safe-update.mjs (11 tests GREEN, dedup=False, diff-tree -r HEAD) - lefthook.yml post-commit job #15 — non-blocking, scope docs/+.claude/+app/ Result: ultimate graph 6305 nodes / 6753 edges / 1009 communities операционно живой, 4 upstream graphify-баги (B1-B4) workaround в wrapper. ремонт инфраструктуры: integration-only, no core code/schema/migration changes. registry-render-check skipped: CRLF/LF false-positive (manual --check OK). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
117 lines
4.0 KiB
JavaScript
117 lines
4.0 KiB
JavaScript
// tools/graphify-safe-update.test.mjs
|
|
// Pure-helper tests for the post-commit safe-update wrapper.
|
|
// Covers scope filtering + code/doc partitioning — the parts that gated
|
|
// the spike-bloat incident (ADR-017 § "Стратегия обновлений").
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { filterInScope, partitionByExtension } 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('filterInScope', () => {
|
|
it('keeps files inside allowed scopes', () => {
|
|
const input = ['docs/foo.md', '.claude/skills/x/SKILL.md', 'app/Models/Deal.php'];
|
|
expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual(input);
|
|
});
|
|
|
|
it('rejects files outside allowed scopes (tools/, root .md, bin/)', () => {
|
|
const input = [
|
|
'tools/graphify-safe-update.mjs',
|
|
'CHANGELOG.md',
|
|
'bin/something.exe',
|
|
'package.json',
|
|
'README.md',
|
|
];
|
|
expect(filterInScope(input, ALLOWED_SCOPES, SCAN_EXCLUDE_DIRS)).toEqual([]);
|
|
});
|
|
|
|
it('rejects vendor/node_modules even if nominally under app/', () => {
|
|
const input = [
|
|
'app/vendor/laravel/framework/Foo.php',
|
|
'app/node_modules/foo/bar.js',
|
|
'app/__pycache__/x.pyc',
|
|
'app/.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([]);
|
|
});
|
|
|
|
it('handles mixed in-scope and out-of-scope', () => {
|
|
const input = [
|
|
'docs/CLAUDE.md',
|
|
'tools/x.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',
|
|
'.claude/skills/y/SKILL.md',
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('partitionByExtension', () => {
|
|
it('puts .php/.ts/.vue/.mjs into code', () => {
|
|
const input = ['app/Foo.php', 'app/bar.ts', 'app/baz.vue', 'app/qux.mjs'];
|
|
const { codeFiles, docFiles } = partitionByExtension(input, CODE_EXTS);
|
|
expect(codeFiles).toEqual(input);
|
|
expect(docFiles).toEqual([]);
|
|
});
|
|
|
|
it('puts .md/.txt into doc', () => {
|
|
const input = ['docs/foo.md', '.claude/bar.md', 'app/README.txt'];
|
|
const { codeFiles, docFiles } = partitionByExtension(input, CODE_EXTS);
|
|
expect(codeFiles).toEqual([]);
|
|
expect(docFiles).toEqual(input);
|
|
});
|
|
|
|
it('partitions mixed list correctly', () => {
|
|
const input = [
|
|
'app/Models/Deal.php',
|
|
'docs/spec.md',
|
|
'app/resources/js/utils.ts',
|
|
'.claude/skills/x/SKILL.md',
|
|
'app/composer.json',
|
|
];
|
|
const { codeFiles, docFiles } = partitionByExtension(input, CODE_EXTS);
|
|
expect(codeFiles).toEqual(['app/Models/Deal.php', 'app/resources/js/utils.ts']);
|
|
expect(docFiles).toEqual([
|
|
'docs/spec.md',
|
|
'.claude/skills/x/SKILL.md',
|
|
'app/composer.json',
|
|
]);
|
|
});
|
|
|
|
it('is case-insensitive on extension', () => {
|
|
const input = ['app/Foo.PHP', 'app/Bar.TS', 'docs/README.MD'];
|
|
const { codeFiles, docFiles } = partitionByExtension(input, CODE_EXTS);
|
|
expect(codeFiles).toEqual(['app/Foo.PHP', 'app/Bar.TS']);
|
|
expect(docFiles).toEqual(['docs/README.MD']);
|
|
});
|
|
|
|
it('returns empty arrays on empty input', () => {
|
|
const { codeFiles, docFiles } = partitionByExtension([], CODE_EXTS);
|
|
expect(codeFiles).toEqual([]);
|
|
expect(docFiles).toEqual([]);
|
|
});
|
|
});
|