
Проблема
AI-агенты (Claude Code и подобные) исследуют код с помощью grep для поиска файлов и read для их открытия. Единица чтения — файл.
Что происходит, когда в одном файле 20 функций?
Нужен один тип CrossError → read
→ Загружаются 19 ненужных функций
→ Загрязнение контекста
Исследование «Lost in the Middle» (Stanford, 2024) показало: если нужная информация находится в середине контекста, производительность LLM падает более чем на 30%. Работа «Context Length Alone Hurts LLM Performance» (Amazon, 2025) установила: лишние токены — даже пустые пробелы — снижают производительность на 13,9–85%.
Исследования доказали: «чем короче контекст, тем лучше». Однако инструмента, который структурно дробил бы код и загружал только нужное, не существовало.
filefunc заполняет этот пробел. Это конвенция структурирования кода и CLI-инструмент для разработки Go-приложений: бэкенд-сервисов, CLI-утилит, кодогенераторов, SSOT-валидаторов.
Ключевой принцип
Один файл — одна концепция. Имя файла = имя концепции.
Это касается func, type, interface и смысловых групп const одинаково. Все остальные правила вытекают из этого единственного принципа.
# Без filefunc
read utils.go → 20 func, 19 лишних. Загрязнение контекста.
# С filefunc
read check_one_file_one_func.go → 1 func. Ровно то, что нужно.
Важнее не открыть нужные 5–10 файлов, а не открывать лишние 290.
Первоклассный гражданин — AI-агент
Структура кода в filefunc ориентирована не на человека, а на AI-агента.
AI-агент исследует код не через ls, а через grep. Будь их 500 или 1000, команда rg '//ff:func feature=validate' решает задачу с одного запроса. Чем больше файлов — тем меньше каждый из них, тем меньше шума приносит один read. Это преимущество, а не недостаток.
Возникает вопрос: «Не слишком ли много файлов?» Для человека — возможно. Но неудобство для человека решается на уровне представления (расширения VSCode и т. п.). Структура filefunc не идёт на компромисс ради удобства человека.
Навигация меняется
Раньше
Запрос пользователя
→ Непонятно что есть → ls, find
→ Открываем файл, разбираемся в структуре
→ Снова grep в поисках связанных файлов
→ Открыли — 20 func, большинство лишние
→ Стоимость навигации > время реальной работы
С filefunc
Запрос пользователя + предоставлен codebook
→ Сразу формируем grep-запрос по codebook
→ read 20–30 файлов (каждый — 1 концепция, весь контекст полезен)
→ Выполняем задачу
Если при чтении 30 файлов весь контекст полезен — это не проблема. Проблема — когда один read тащит за собой 30 единиц лишнего.
Codebook
Codebook — самый важный элемент в архитектуре filefunc. Он важнее правил аннотаций. Хорошо спроектированный codebook обеспечивает точные grep-запросы, а точные grep-запросы дают чистый список файлов для чтения.
# codebook.yaml
required:
feature: [validate, annotate, chain, parse, codebook, report, cli]
type: [command, rule, parser, walker, model, formatter, loader, util]
optional:
pattern: [error-collection, file-visitor, rule-registry]
level: [error, warning, info]
Ключи из required обязательны в каждой аннотации //ff:func и //ff:type — это гарантирует надёжность grep-поиска: у required-ключей не бывает пустых значений. Ключи из optional используются только при необходимости.
Codebook — это карта проекта для AI-агента. Без него навигация начинается вслепую. С ним можно сразу формулировать точные запросы: feature=validate, type=rule — без предварительного изучения структуры.
Если в аннотации используется значение, отсутствующее в codebook — это ERROR. Нормализация словаря через codebook делает видимыми пропущенные feature, дублирующиеся type и размытые категории. Видишь пробел — можешь управлять. Сам codebook тоже проходит валидацию: в required обязателен минимум один ключ, дубликаты запрещены, допускаются только строчные буквы и дефисы.
Аннотации метаданных
Аннотации располагаются в начале каждого файла — чтобы понять мета-информацию по первым нескольким строкам, не читая весь body.
//ff:func feature=validate type=rule control=sequence
//ff:what F1: validates one func per file
//ff:why Primary citizen is AI agent. 1 file 1 concept prevents context pollution.
//ff:checked llm=gpt-oss:20b hash=a3f8c1d2
func CheckOneFileOneFunc(gf *model.GoFile) []model.Violation {
| Аннотация | Содержание | Обязательность |
|---|---|---|
//ff:func | Метаданные func-файла: feature, type, control и др. | Обязательна для func-файлов |
//ff:type | Метаданные type-файла: feature, type и др. | Обязательна для type-файлов |
//ff:what | Однострочное описание — что делает эта функция/тип | Обязательна |
//ff:why | Почему реализовано именно так — обоснование решения | Опционально |
//ff:checked | Подпись LLM-валидации (генерируется автоматически через llmc) | Автоматически |
Формат: //ff:key key1=value1 key2=value2. Мгновенно ищется через grep/ripgrep и парсится инструментами как структурированные key-value пары. Паттерн аналогичен директивам Go: //go:generate, //go:embed.
control — 1 func, 1 control
control= обязателен для всех func-файлов. Допустимые значения — одно из трёх:
| control | Значение | Ограничение глубины |
|---|---|---|
sequence | Последовательное выполнение | 2 |
selection | Ветвление (switch) | 2 |
iteration | Цикл (loop) | dimension + 1 |
Основано на теореме Бёма–Якопини (1966): любая программа — это комбинация sequence, selection и iteration. filefunc применяет это на уровне функций: одна функция — один поток управления.
Для функций с control=iteration обязателен параметр dimension=, явно указывающий размерность перебираемых данных. При dimension=1 (плоский список) глубина ≤ 2, при dimension ≥ 2 требуется именованный тип (struct/interface).
filefunc также проверяет соответствие control реальному коду. Если control=selection задан, а switch отсутствует, или control=sequence задан, а в теле есть switch или loop — это ERROR.
Пайплайн навигации LLM
Аннотации работают как поисковый индекс. Никакой тяжёлой инфраструктуры вроде векторных эмбеддингов.
1. Структурное сужение (без LLM, grep)
Формируем grep-запрос на основе codebook
→ Получаем 20–30 файлов-кандидатов
2. Метаоценка (без LLM или с минимальной моделью)
Читаем только аннотации в начале каждого файла
→ Сужаем до 5–10 по name/input/output/what
3. Точная работа (большой LLM, минимальный контекст)
Полный read только 5–10 файлов
→ Изменение/генерация кода
По мере продвижения по этапам контекст уменьшается. К моменту подключения большого LLM остаются только действительно нужные файлы.
CLI
validate — проверка правил структуры кода
filefunc validate # текущий каталог
filefunc validate /path/to/project # явный корень проекта
filefunc validate --format json
В корне проекта должны находиться go.mod и codebook.yaml. Только чтение. При нарушениях — exit code 1.
chain — отслеживание цепочки вызовов
filefunc chain func RunAll # 1 степень (по умолчанию)
filefunc chain func RunAll --chon 2 # 2 степени (включая совместно вызываемые функции)
filefunc chain func RunAll --chon 3 # 3 степени (максимум)
filefunc chain func RunAll --child-depth 3 # только дочерние вызовы
filefunc chain func RunAll --parent-depth 3 # только вызывающие функции
filefunc chain feature validate # весь feature целиком
Анализ AST в реальном времени. --chon — степень связи. 1 степень — прямые вызовы и вызывающие. 2 степени — также функции, вызываемые совместно.
Стандартный go callgraph анализирует все вызовы статически и выдаёт тысячи узлов. chain отслеживает только в пределах одного feature. Feature из codebook — это и есть уровень масштабирования.
llmc — LLM-валидация
filefunc llmc # текущий каталог
filefunc llmc --model qwen3:8b
filefunc llmc --threshold 0.9
Проверяет соответствие //ff:what телу функции с помощью локального LLM (ollama). Оценка от 0.0 до 1.0, порог по умолчанию — 0.8. При прохождении автоматически записывает //ff:checked llm=имя_модели hash=хэш. Если тело изменилось, хэш изменится и потребуется повторная валидация.
Это решение ключевой проблемы дрейфа аннотаций: //ff:what на естественном языке невозможно верифицировать механически — поэтому задача делегируется небольшому LLM. Подход работает именно потому, что 1 file 1 func гарантирует соответствие один к одному на уровне файла.
Правила
Правила структуры файлов
| Правило | При нарушении |
|---|---|
| Один func на файл (имя файла = имя функции) | ERROR |
| Один type на файл (имя файла = имя типа) | ERROR |
| Метод: 1 file 1 method | ERROR |
| init() не может существовать самостоятельно (только вместе с var или func) | ERROR |
_test.go допускает несколько func | Исключение |
| Семантически единая группа const допускается в одном файле | Исключение |
Правила качества кода
| Правило | При нарушении |
|---|---|
| Глубина вложенности: sequence=2, selection=2, iteration=dimension+1 | ERROR |
| Максимум 1000 строк на func | ERROR |
| Рекомендации: sequence/iteration — 100 строк, selection — 300 строк | WARNING |
Ограничение глубины вложенности зависит от типа control. Для sequence и selection — depth 2. Для iteration — dimension + 1: при dimension=1 (плоский список) depth 2, при dimension=2 (вложенная структура) depth 3. В сочетании с паттерном early return в Go большинство кода укладывается в эти ограничения.
Для selection (switch) допускается до 300 строк в качестве рекомендации — с учётом того, что case-ветки склонны разрастаться.
.ffignore
Файл .ffignore в корне проекта исключает указанные пути из всех команд filefunc. Синтаксис аналогичен .gitignore.
vendor/
*.pb.go
*_gen.go
internal/legacy/
Предназначен для исключения кода, к которому нельзя применить правила filefunc: сгенерированный код (protobuf, кодогенераторы) и внешний вендорный код.
Интеграция с whyso
Поскольку func = file, история изменений на уровне функции точно совпадает с историей на уровне файла.
whyso history check_ssac_openapi.go # история изменений функции CheckSSaCOpenAPI
Если в файле несколько функций, приходится рыться в diff, чтобы понять, какая из них изменилась. С filefunc изменение файла = изменение функции. Стоимость отслеживания — ноль.
Обнаружение неявных связей
whyso coupling check_ssac_openapi.go
Функции, изменявшиеся вместе в одном запросе:
check_response_fields.go 8 раз
check_err_status.go 5 раз
types.go 4 раза
Если функция регулярно появляется в статистике coupling без явных зависимостей — это сигнал о скрытой связанности. Функции, реализующие одно бизнес-правило с разных сторон. Неявное согласование формата без interface. Баги, которые всегда всплывают вместе.
Почему только Go
Применить структуру filefunc за пределами Go непросто. gofmt принудительно форматирует код, early return — общепринятый паттерн, исключений нет, пакет = директория. Расширение на другие языки потребует стратегии структурного принуждения на уровне gofmt — это выходит за рамки filefunc.
Область применения тоже чётко очерчена: бэкенд-сервисы, CLI-инструменты, кодогенераторы, SSOT-валидаторы. Алгоритмические библиотеки, низкоуровневое системное программирование, горячие пути, критичные к производительности — не в зоне охвата.
Итог
В эпоху LLM структура кода должна быть оптимизирована не для удобства навигации человека, а для эффективности навигации AI. filefunc — первый шаг к этому переходу.
Один файл — одна концепция. Codebook нормализует словарь. Аннотации несут метаданные. Один grep находит точный файл. Один read не тащит за собой лишний код. Загрязнение контекста предотвращается на уровне самой структуры файлов.