filefunc — один файл, одна концепция

Проблема

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 methodERROR
init() не может существовать самостоятельно (только вместе с var или func)ERROR
_test.go допускает несколько funcИсключение
Семантически единая группа const допускается в одном файлеИсключение

Правила качества кода

ПравилоПри нарушении
Глубина вложенности: sequence=2, selection=2, iteration=dimension+1ERROR
Максимум 1000 строк на funcERROR
Рекомендации: 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 не тащит за собой лишний код. Загрязнение контекста предотвращается на уровне самой структуры файлов.

Код: github.com/park-jun-woo/filefunc