Василий Половнёв

Ведущий разработчик.

Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.

Связаться: vasily@polovnyov.ru или телеграм.

Блог Курс о росте в профессии

Как связаны AI, AGI, ANI, LLM, GPT и AI-агенты

Раньше, когда мы встречали что-то неправдоподобное или удивительно хорошо работающее, мы писали в комментариях «Это Фотошоп или Инстаграм». Теперь пишем «Это ИИ или нейросеть». Сегодня поясняю по хардкору за эти и другие термины, от которых уже тошнит.

Начнём с ИИ, AI — Искусственного Интеллекта, Artificial Intelligence. ИИ — это зонтичный бренд, а не что-то конкретное. ИИ — это семейство технологий, которые делают что-то похожее на твою интеллектуальную работу. Распознать речь, отфильтровать спам, предложить следующий трек, сгенерировать картинку, написать диплом, переписать всё на Расте или Тайпскрипте — это всё ИИ.

AGI — Artificial General Intelligence, универсальный искусственный интеллект. Это цифровой коллега, который одинаково хорошо справится с письмом, интерфейсом, кодом, переговорами и планированием. Но в отличие от обычного коллеги, не спит, не ест, не ходит на перекуры, и ему не нужно скидываться на ДР.

Про AGI говорят много, потому что идея яркая и офигенная. Именно такого уровня ИИ нам нужен для освоения вселенной с помощью зондов фон Неймана. Отправляем самовоспроизводящийся зонд под управлением ИИ в соседнюю систему, он там добывает всякое, строит свои точные копии и отправляет их в соседние системы.

К сожалению, от AGI мы пока далеки. Весь полезный ИИ сегодня неуниверсальный, узкий.

ANI — Artificial Narrow Intelligence, узкий искусственный интеллект. Это не универсальный мегамозг, а очень сильный помощник для узкого класса задач. Он может хорошо справляться с одним классом задач, но быть бесполезным во всём остальном. Например, Google Translate, рекомендательная система Озона, распознавание лиц, генерация субтитров в Ютюбе или LLM.

LLM — Large Language Model, большая языковая модель. Это узкий ИИ, который работает с языком: пишет, сокращает, объясняет, суммирует, извлекает данные, генерирует варианты.

Вы наверняка слышали, что LLM — это очень умное автодополнение, Т9 на стероидах и всё такое. А на самом деле LLM — это просто миллиарды и триллионов ифов. Шучу, это просто миллиарды и триллионы матриц, которые друг на друга умножаются. Учёные и программисты покруче поправили бы меня: «это миллиарды и триллионы параметров (весов), которые участвуют в вычислениях (в том числе через умножение матриц)».

LLM — это не отдельная магия, а частный случай нейросетей, обученных на огромных объёмах текста. Нейросети — это такая хитрая математическая модель и её воплощение в коде, которая умеет накапливать опыт и опираться на него, чтобы что-то предсказать или классифицировать.

GPT, ГПТ — Generative Pre-trained Transformer, Генеративный Предобученный Трансформер. Звучит охеренно, но это просто отдельный тип LLM, построенный на хитрой архитектуре.

Чтобы представить, как всё это соотносится друг с другом, запомните вот что: ИИ — это автомобили, электросамокаты и велосипеды, нейросети — автомобили, LLM — электромобили, а GPT — именно Теслы.

Чаще всего интерфейсом к ГПТ-модели служит окно чата: ChatGPT, Midjourney, DeepSeek, NanoBanana, Perplexity и прочие. Чаты просто дают ответы на ваши вопросы, гоняют текст и картинки туда-сюда. А настоящая крутизна начинается тогда, когда мы заворачиваем модель в процесс: получи контекст → реши, что делать дальше → сделай → повторяй, пока задача не будет сделана. Вот этим занимаются ИИ-агенты.

ИИ-агент — это LLM + тулы + контекст и бесконечный цикл. ИИ-агент больше похож на помощника-исполнителя, который умеет не только отвечать, но и что-то делать у вас на компе с помощью тулов.

При этом LLM — лишь часть ИИ-агента. Скажем, ИИ-агенты для программирования состоят из LLM, упряжки (harness) и UI. LLM отвечает за думанья. Упряжка — за всё вокруг модели: цикл, тулы, окружение и управление контекстом. UI — то, что вы видите.

Например, вы можете взять OpenCode для harness, выбрать в нём DeepSeek в качестве LLM и открыть всё это в браузере (UI). Аналогично и в других агентах:

Codex (console UI) + gpt5.3-codex high (model) + codex (harness)
Codex App (UI) + gpt5.4 (model) + codex (harness)
Claude Code (console UI) + sonnet 4.5 (model) + claude (harness)

Зачем об этом знать? Затем, что в 99% случаев модели показывают лучший результат с упряжкой от этого же провайдера.

Вам нужно на фронтир

Начну с камингаута. Я использую LLM для программирования полтора года. За это время я попробовал почти всё: Cursor, локальные модели, Claude, Codex, Qwen, Gemini, Copilot, Zed, Amp и OpenCode. В 2026 году я не написал ни строчки кода с нуля. Даже в пет-проектах вместо крафтового кода, написанного и наполированного мною вручную, теперь сияет бездушный код, написанный киборгом-убийцей из будущего. В итоге, я пришёл к простому выводу: не надо экономить на моём здоровье, лучше брать самые сильные доступные модели и агенты.

Во-первых, чем умнее, вдумчивее и осмотрительнее виртуальный коллега, тем приятнее с ним работать. Модели поумнее читают мысли, а моделям поглупее приходится разжёвывать одно и то же по нескольку раз. Именно поэтому появился паттерн (на мой вкус, анти-) «план пишет умная модель, реализует глупая».

Во-вторых, модели поумнее пишут код, который можно принять с первой итерации. С моделями поглупее приходится делать большее число итераций, пока код достигнет того уровня качества, что я хочу. Да, умная модель дольше генерирует код, но это окупается: меньше правок, меньше циклов, меньше усталости.

В-третьих, тулы и упряжка у агентов подороже реально круче. Попробуйте подебажить утечку памяти или найти источник высокой нагрузки в логах с помощью Codex или Claude. Офигеете, как агент пользуется тулами, как проверяет гипотезы однострочниками и как пишет мини-программы для решения задач. У хороших агентов с хорошими тулами есть чему поучиться.

В-четвёртых, чем умнее агент, тем больше пользы за единицу времени он приносит, тем быстрее вы пилите новые фичи. Чем больше фич вы сделаете, тем больше опыта получите, тем быстрее прокачаетесь в агентской разработке.

При этом никто не заставляет вас пилить в 10 раз больше фич. Пилите в 5 раз больше, а освободившееся время потратьте на что-то другое. Например, на все те рефакторинги, о которых вы мечтали и которые задвигали в бэклог последние 3 года. Поручите их агенту и сделайте за день, а не за итерацию.

Короче, если вы все еще экономите, потратьте 500 рублей, оформите подписку на ChatGPT, скачайте Codex, поменяйте модель на gpt-5.3-codex high и делегируйте ему все на протяжении месяца.

Value Data-объекты в Руби

Я как-то пропустил, а в Руби 3.2 завезли класс Data для создания value objects:

Recipient = Data.define(:name, :email, :role)

vasyan = Recipient.new(name: "Vasyan", email: "vasyan@stark.com", role: :cc)

vasyan.email # vasyan@stark.com
vasyan.role # :cc

# При этом
vasyan.email = "foo" # undefined method 'email='

Пример посложнее с кастомными методами:

Measure = Data.define(:amount, :unit) do
  def <=>(other)
    return unless other.is_a?(self.class) && other.unit == unit
    amount <=> other.amount
  end

  include Comparable
end

Measure[3, 'm'] < Measure[5, 'm'] #=> true
Measure[3, 'm'] < Measure[5, 'kg'] # ArgumentError

Отличается от Struct двумя ключевыми моментами. Во-первых, Data не генерирует сеттеры (attr_writer) для атрибутов. Во-вторых, Data не включает Enumerable, поэтому в нем нет each, each_pair, filter и прочих. Короче, Data — это идеальный вариант для минималистичных неизменяемых value objects.

У программиста две заботы

Последние три недели хожу вокруг одной идеи, но никак не могу ее нормально доформулировать. Все время чего-то не хватает. Начну здесь, а вы помогайте.

Недавно понял, что у программиста две задачи. Первая — приносить прибыль и снижать издержки, пописывая код и тесты у себя в квартале. Ну вы знаете: писать код и тесты, пилить фичи, чинить баги и резолвить инциденты. Назовем ее — крафт, мастерство. Вторая — быть легким и приятным в работе с собой. Назовем ее — АПИ.

Я всегда фокусировался на мастерстве, забывая об АПИ. Мой недостижимый идеал — ХАКЕРМЭН, злобный гений, колдун-программист в комнате, затянутой сигаретным дымом, парами алкоголя и сумраком. (Если вы сильно моложе меня, вспомните Гилфойла из «Кремниевой долины»)

Оказывается, быть злобным гением — не лучшая стратегия. С ним никто не хочет работать, к нему приходят, когда совсем припрет. Лучше забить на идею или потерпеть проблему, чем пойти к сисадмину колдуну-программисту и поесть фруктовой смеси из презрения, стыда и брезгливости. Так самые интересные идеи и сложные задачи проходят мимо него: их отдают менее скилловым, но более приятным в работе.

Важно этот момент признать и посмотреть на себя со стороны как на интерфейс. Какой АПИ я отдаю коллегам? Насколько легко и приятно им пользоваться? Стал бы я сам таким пользоваться? Что можно улучшить в работе с моим АПИ?

Справедливо и обратное. Никакой самый приятный и удобный АПИ не заменит сделывания задач. Если программист весь такой внимательный, инициативный и замечательный, но выдает по строчке кода в месяц — это очковтирательство, а не работа.

let! vs. let + before

Последний Ruby Weekly принёс статью о том, что let! лучше заменять на let + before блок:
https://allaboutcoding.ghinda.com/rspec-and-let-understanding-the-potential-pitfalls

Мол, такое:

RSpec.describe Thing do   
 let(:account_a) { build(:user, email: email) }  
 let(:account_b) { build(:user, user: email2) }  
 let(:organisation) { build(:organisation, account: account) }  
 let(:team) { build(:team, account: organisation) }  

 before  
   account_a  
   account_b  
 end

 it 'returns that specific value that we want' do  
    # test 
 end  
end

Лучше, чем такое:

RSpec.describe Thing do   
 let!(:account_a) { build(:user, email: email) }  
 let!(:account_b) { build(:user, user: email2) }  
 let(:organisation) { build(:organisation, account: account) }  
 let(:team) { build(:team, account: organisation) }  

 it 'returns that specific value that we want' do  
    # test 
 end  
end

Мне кажется, что это какая-то ерунда. Во-первых, все мы любим Руби за то, что код на нем, читается почти как книга. А тут у нас «сначала акаунт а и б». Это че вообще?

Во-вторых, зачем вообще использовать let, если ни в последующих let, ни в тестах, мы не обращаемся к этим переменным по имени? Если они реально нужны только для того, чтобы создать окружение (скажем, нужны два акаунта, пользователь создаёт третий), то лучше сразу создать их в before блоке:

before do
  create(:account)
  create(:account)
end

В-третьих, на мой вкус, let! такой же заметный, как и before. А если хочется сделать позаметнее, сделай себе декоратор LET!!!11111.

В-четвертых, мне кажется, что стоит отделять зависимости тестируемого объекта и сетап окружения. Грубо говоря, в let зависимости (пользователь –> организация –> акаунт), а в before — действия, приводящие систему в нужное нам состояние. А размазывать их по let и before — странновато.

Как упростить превьюшки мейлеров с помощью #public_instance_methods

Представим, что у нас есть тупейший мейлер с пачкой методов:

class OnboardingMailer < ApplicationMailer
  default to: -> { @user.email },
    from: "marketing@foo.bar"

  before_action :load_user

  def welcome; end
  def onboarding; end
  def intro; end
  def survey; end
  def goodbye; end

  private

  def load_user
    @user = params[:user]
  end
end

Раньше я тупо перечислял и делегировал каждый метод в мейлер в превьюшке:

class EventsMailerPreview < ActionMailer::Preview
  delegate :welcome, :onboarding,
    :intro, :survey,
    :goodbye, to: :mailer

  private

  def mailer
    OnboardingMailer.with(user: User.first)
  end
end

Получается не очень круто. Если появляются новые письма, приходятся дополнять список методов в превьюшке. Чтобы избавиться от этого гемороя, лучше использовать public_instance_methods(false):

class EventsMailerPreview < ActionMailer::Preview
  available_emails = OnboardingMailer.public_instance_methods(false)
  delegate(*available_emails, to: :mailer)

  private

  def mailer
    OnboardingMailer.with(user: User.first)
  end
end

public_instance_methods возвращает имена всех публичных и протектед методов класса, а false просит вернуть только те, что определены в самом классе, игнорируя родителей.

Cursor Rules для хороших тестов с Ruby и RSpec

Делюсь моим набором правил для Курсора, которые помогают писать вменяемые тесты с RSpec. Я пользуюсь ими около месяца, в 9 из 10 случаев получается то, что надо. Больше того, я использовал эти правила, чтобы сделать домашку в Тестовом курсе, и получил хорошие тесты, требующие минимальной доработки.

---
description: 
globs: **/*_spec.rb
alwaysApply: false
---

# <Project> Tests Style Guide
## General
- Always use English in tests.
- Always test edge, valid, and invalid cases.
- Set up RSpec config in `.rspec` and `spec_helper.rb`.
- Follow DRY principles and avoid duplication. Don't over-DRY at the cost of clarity; duplication is acceptable if it improves readability.
- Use shared examples for repeated behaviors.
- Do not test private methods.
- Do not test trivial methods.
- Do not test external dependencies.

## Layout
- Do not leave empty lines after `describe`, `context`, or `feature` declarations.
- Leave one empty line between example groups (`describe`, `context`, `feature`).
- Leave one empty line after `let`, `before`, and `after` blocks.
- Group all `let` blocks together, separate from `before`/`after`.
- Leave one empty line around each `it`/`specify` block.
- Separate test phases (setup, exercise, assertion) with one empty line.

## Example Group Structure
- Do not use `subject`; prefer `let`/`let!`.
- Use `let`/`let!` for setup, not instance variables or `before` for data.
- Order: `let`/`let!`, then `before`/`after`.
- Use `describe` for methods/classes. Use `.` for class methods, `#` for instance methods.
- Use `context` to describe different conditions, starting with 'when', 'with', 'if', 'unless', 'for', 'and', 'but', 'without', or 'otherwise'.

## Example Structure
- One expectation per example (`it`). If an example description contains 'and', it probably contains more than one expectation.
- When an example consists of multiple linked expectations, consider using `have_attributes` or a custom matcher.
- Keep example descriptions short and clear.
- Use `expect` syntax for assertions.
- For change assertions, prefer `expect { ... }.to change { ... }.from(...).to(...)` or `.to(...)`.
- Extract a `context` if an `it` description contains conditions: when, with, if, unless.

## Naming
- Contexts should describe conditions, forming readable sentences with nested blocks.
- Do not use 'should' in example descriptions. Use present tense, third person.
- Be explicit in `describe` blocks about what is being tested.

## Stubbing & Mocks
- Mock/stub only when necessary. Prefer real objects for integration tests.
- Always use `expect(...).to have_received(...)` to verify method calls on double/spy instead of `expect(...).to receive(...)`.
- Stub HTTP requests (e.g., with webmock) instead of hitting real services.
- Use `Timecop` for time, not stubbing `Time.now`.

ИИ и LLM: прогрессия в программировании

Вот, как мне кажется, из каких этапов состоит процесс внедрения ИИ и LLM в работу простого советского программиста. Пишу схематично, чтобы не забыть. Если интересно, напишите — разберу каждый этап отдельно со всеми нужными ссылками.

Мне кажется, всё, как в Железном Человеке. Начинаем с бестолковой Алисы, переходим к аугументации, костюму, который тебя делает круче, и наконец, к автономным костюмам-агентам (см. «Протокол семейный праздник» из ЖЧ3).

Прогрессия:
0. Отрицание, гнев, торг, депрессия, принятие. Да, как прежде уже не будет.

1. Время от времени захаживаешь в Дипсик или ЧатГПТ. Просишь что-то посоветовать, объяснить, написать за тебя функцию. Получаешь опыт, учишься формулировать рабочие промпты, снова и снова копипастишь кусочки кода туда-обратно.

2. Задалбываешься копипастить. Добавляешь автокомплит — ставишь GitHub Copilot или Курсор. Автокомплитишь табами, отдаешь LLM бойлерплейт.

3. Покупаешь Курсор. Начинаешь немножечко вайб-кодить через чат. Пишешь и воруешь рулы, делаешь мини-фичи с помощью LLM. Учишься формулировать требования к решению задачи, натыкаешься на ерунду, прокачиваешь рулы. Обжигаешься и понимаешь, что тесты не стоит всегда доверять LLM.

4. Переходишь на программирование агентом. Сетапишь ВПН, покупаешь Claude Code. Программируешь фичи и рефакторишь с помощью разговора с консолью. Делегируешь агенту всю работу с репозиторием от создания и чтению ишьюс до форс-пушей и создания ПРов.

5. Начинаешь использовать чат с LLM для постановки задач другой LLM. Роботы программируют друг друга, а ты таскаешь промпты туда-сюда.

6. Переходишь на флот агентов. Запускаешь несколько инстансов агентов в вебе в Курсоре или локально с помощью Claude Code и git worktree.

7. Прекрасное светлое будущее. События «Мстители: Финал», уходишь в плотники или пивовары.

Так вижу. Пожалуйста, не воспринимайте это как руководство к действию и обязательно проконсультируйтесь с безопасниками, чтобы не получить по жопе за код, слитый в сеть.

Новый LLM дев-цикл

Зафиксирую для истории. Сегодня ровно месяц, как я не могу работать в обычном режиме «сел за комп утром, встал вечером, зато без перерыва». Теперь работаю короткими интервалами по 20-25 минут с перерывами по 5 минут. Соответственно, поменялся и формат работы с LLM. Вот, что я поменял:

1. Написал системный промпт для Курсора, который просит LLM сначала сформулировать план решения задачи, утвердить его со мной, выполнить, а затем прогнать тесты и линтеры:

## Development cycle
1. Before writing any code, come up with a good plan, review the plan, and then ask the user for permission to execute the plan.
2. After you have executed the plan, run: `bin/rspec` and `bin/rubocop`
3. If there are any linting errors, run `bin/rubocop -A`
4. To run tests: `bin/rspec <path to file>`

Теперь я ставлю задачу LLM, проверяю и, если нужно, корректирую план, отправляю его на выполнение и иду отдыхать свои 5 минут.

2. Стал писать задачи на русском. Если я правильно понимаю, как работает LLM, то никаких проблем с этим быть не должно. car и «машина» в их векторном пространстве смыслов должны быть совсем рядом.

3. Стал писать задачи по формату. Открываю все нужные файлы, делаю /Add Open Files to Context. Кратко формулирую задачу в один абзац, а затем пишу список (буквально!) требований. Часть требований и технических деталей пишу прямо кодом:

...

С вот такими требованиями:
...
- определяет методы в мейлере, используя `define_method` и `@email_chain.steps`
...
- находит нужную цепочку и пользователя, опираясь на переданный mailing_list_subscription и АПИ EmailChain:
    def load_data
      @mailing_list_subscription = params[:mailing_list_subscription]
      @user = @mailing_list_subscription.user
      @email_chain = EmailChain.find_by(mailing_list_subscription: @mailing_list_subscription)
    end

4. Стал бить задачи так, чтобы диф изменений, сгенерированный LLM, был не больше 300-400 строк. Так их проще проверить и верифицировать. Если вайб-кожу, конечно, не смотрю на это. Пусть генерит сколько угодно, все равно на выброс.

5. Перестал доверять LLM в написании тестов. LLM — мастера находить самые простые и рабочие пути. Они очень часто пишут тесты, которые либо тестируют ненужное, либо тестируют только «золотой путь», игнорируя все краевые случаи. Ощущение, будто LLM пишут тесты, чтобы они проходили с первого раза, а не для того, чтобы найти побольше багов и задокументировать АПИ.

Вы используете Курсор неправильно

На прошлой неделе смотрел, как другие ребята пользуются Курсором и ИИ в нем. Заметил, что не все понимают, как получать от него максимум пользы.

Во-первых, первое, что нужно сделать — включить YOLO, он же Enable auto-run mode. Эта фича разрешит агенту запускать код локально. Соответственно, можно будет делать крутейшие штуки:

Запусти эти тесты и поправь тестируемый класс так, чтобы тесты проходили

Напиши тесты для функции foo, которая ... Затем напиши функцию foo и исправь ее, если потребуется, так, чтобы тесты проходили

Во-вторых, в повторяющихся задачах необязательно описывать нужные изменения: переименуй так, сконвертируй в класс, перенеси в метод X, удали старый файл. Можно сделать один раз самому и использовать изменения в качестве примера:

Взгляни на мои изменения в модуле видео для Кинескопа и повтори эти изменения для модулей видео в Вимео, ВК и Ютюбе

Аналогично с помощью примеров можно подсказать, как должна работать новая функция:

Напиши функцию на JS, которая группирует свойства по ключам во вложенные объекты. Покажу на примерах:
{ vkSrc: 'http://vk.com/123' } превращается в { vk: { src: 'http://vk.com/123' } }
{ vimeoId: '123', kinescopeSrc: 'http://ki.io' } превращается в { vimeo: { id: '123' }, kinescope: { src: 'http://ki.io' } }

В-третьих, чат можно использовать как справочник. Выделили нужный фрагмент кода, например, resolitions в package.json и попросили объяснить, как это работает.

В-четвертых, во вкладке Source Control в поле с текстом коммита есть магическая кнопка. Нажмите на нее, чтобы ИИ сгенерировал нормальный коммит-месседж на основе ваших (или не совсем ваших) изменений.

В-пятых, используйте правила для Курсора. Как минимум, возьмите хороший системный промпт:
https://cursor.directory/

Как максимум, понапишите собственных правил для проекта:
https://ghuntley.com/stdlib/

Короче, если вы используете в Курсоре только автокомплит, присмотритесь к YOLO auto-run mode и .cursorrules. И считайте чат не хитрым интерфейсом к Гуглу, а диалогом со стажером, с которым вы программируете в паре.

Эмодзи для работы с замечаниями

С уходом Дискорда и непреднамеренным старением я стал записывать замечания со встречи прямо в тот же чат в Телеграме. Так как замечаний много, отвечать на каждое из них — безумство. Любого выбесит 20-30 сообщений «Увидел», «Поправил», «Записал». Вместо этого использую реакции на сообщения с замечаниями. Принцип такой:
👌 — поправил, сделал, починил
✍️ — записал, в бэклог, разберусь позже
👀 — увидел
🤔 — че-т подозрительно, проверим
👍 — супер, одобряю, нормас
🔥 — клево

Если в заметках со встречи все в эмодзи, значит, я все разобрал. Если у сообщения реакции нет, значит, пропустил. Изи, ничего не теряется.

Легче починить, чем извиниться

Тут DHH написал пост, да такой, что ПРЯМО В ЧУВСТВА. В нем пара клевых моментов.

Во-первых, Джейсон Фрид на заре Бейскемпа отвечал на 150 писем клиентов в день. Еще раз: сто пятьдесят писем клиентов в день. А мы инбокс разгрести не можем.

Во-вторых, политика «Все работают в техподдержке» помогает фиксить мелкие баги-заусенцы, до которых никогда не доходят руки. А все потому, что программистам проще починить проблему, чем извиниться за то, что она там есть. Думаю, это еще и для эго менее болезненно.

Пост целиком:
https://world.hey.com/dhh/stick-with-the-customer-4942402f (нужен ВПН)

Отличать дизмораль от трения

Иногда никак не хочется подходить к задаче. Уже все дела попеределал, книгу почитал, зубы почистил, на балконе прибрался, даже в машину омывайку подлил, а до задачи руки никак не доходят. Прокрастинация на максимум, не те вайбы.

В таких случаях важно не путать дизмораль с трением. В первом случае к задаче не подойти, потому что все уныло, какой в этом смысл, это ни на что не повлияет, уже три года это говно пилим. Во втором случае к задаче не подойти, потому что она реально сложная и непонятно, с какого конца за нее взяться. Соответственно, и решения разные. Дизмораль можно победить действиями вне задачи, ну или просто затащить ее на морально-волевых и дисциплине. А сложную задачу — разбить на пачку задач попроще, которые постепенно рассеивают «туман войны», спрятавший ее решение.

Поэтому первое, что я спрашиваю у себя в понедельник утром, когда чувствую непреодолимое желание пойти подлить омывайки: это дизмораль или трение?

Оставляйте артефакты

Иногда нам достаются задачи, которые не закрыть в течение одного дня. Скажем, вы исследуете сложный баг, проявляющийся только у 0,0001% пользователей. Или вместе с архитектором пишете спеку нового сервиса, встречаясь каждую неделю на час. Или подбираете и пробуете self-hosted альтернативы Sentry. Или работаете над большой миграцией данных на новую схему.

Вдруг наступает пятница, а за ней выходные. Вы возвращаетесь к работе в понедельник и полчаса вспоминаете, на чем остановились.

В таких случаях стоит оставлять артефакты — заметки, комментарии, вопросы, кусочки кода, сравнительные таблицы, текущие проблемы и проверенные гипотезы. Словом, все, что поможет вернуться к задаче:

Проверии гипотезу с браузерным и системным зумом. В эмуляторе проблемы нет.

Как будем авторизовывать клиентов? Может, пойдем в сторону API Gateway?

Sentry — опенсорсный, но поднимает 6 контейнеров и требует кучу ресурсов. Как вариант — GlitchTip. У него АПИ, совместимый с Sentry и минимум лишнего функционала.

Перенесли все уиды в алиасы. Остается грохнуть колонку и убрать ссылки на mailing_list_uid.

Такие зарубки помогают ничего не забывать, быстрее возвращаться в контекст задачи и двигаться дальше в правильном направлении. Отсюда тупое правило:

Поделал — оставь артефакт

Не используйте subject для испытаний

Бывает, вижу в проектах именованные сабжекты в качестве хелперов для испытаний:

subject(:process) { described_class.new.process }


before do
  allow(EventProducer).to receive(:emit).and_return(true)
end

context "when run twice" do
  it "emits only one event" do
    process
    process

    expect(EventProducer).to have_received(:emit).once
  end
end

Это плохой, ложноположительный тест. Что бы мы не написали внутри process, EventProducer.emit всегда будет вызываться точно один раз.

Проблема в хелпере subject. Как и let, он кеширующий, мемоизирующий. После первого вызова он запоминает результат блока и в последующие вызовы сразу возвращает результат, не тригеря код в блоке.

Это легко проверить мини-тестом:

class EventProducer
  def self.emit
  end
end

describe "subject(:process)" do
  subject(:process) { EventProducer.emit("foo") }

  before { allow(EventProducer).to receive(:emit) }

  context "when run twice" do
    it "emits both events" do
      process
      process

      expect(EventProducer).to have_received(:emit).twice
    end
  end
end

Результат:

Failures:

  1) subject(:process) when run twice emits both events
     Failure/Error: expect(EventProducer).to have_received(:emit).twice

       (EventProducer (class)).emit(*(any args))
           expected: 2 times with any arguments
           received: 1 time with any arguments
     # ./spec.rb:17:in `block (3 levels) in <top (required)>'

Пожалуйста, не используйте именованные сабжекты для испытаний. Лучше сделайте старый добрый метод:

def process
  described_class.new.process
end

Уверовал в AI

Я сразу родился айти-дедом. Когда появился Руби, я бухтел, что непонятно, куда тело цикла вставлять. Когда появлялись ЦСС-переменные, бухтел, что они нам «не нужоны», достаточно вот так и так сгруппировать селекторы. Бухтел, когда переезжали с Propotype на jQuery, а потом на Реакт. Бухтел, когда все носились с Монгой и Нодой. Бухтел с каждым релизом Рельс. Блин, да я даже из-за синтаксиса хэшей в Руби бухтел: ракеты рулят, джейсон отстой.

Сейчас в программировании все переезжают на AI-, LLM-помощников, которые «помогают» писать код. И вот тут я бухтеть не готов. Уверовал мгновенно, как попробовал Курсор.

В Курсоре по сути два основных помощника. Первый — стандартное автодополнение в процессе набора кода, к нему вопросов нет. Прикольно, но ничего особенного. Второй — чат с редактором. И вот этот режим — бомба, потому что обсуждать и делать можно как отдельные кусочки кода, так и проект целиком.

В какой-то момент я попросил в чате собрать мне консумер, аналогичный уже существующему, но с несколькими ключевыми изменениями. К нему же попросил тесты. Прошло 10 секунд и Курсор предложил мне два файла с кодом, в котором толком нечего было исправлять. Я просто принял изменения и пошел открывать 100% AI-generated Pull Request. И вот этот момент стал определяющим. Не о чем тут бухтеть, будущее уже здесь.

Короче, если вы еще не используете LLM-помощников в разработке, попробуйте Курсор. Просто перетерпите ВС Код и дайте ему неделю:
https://www.cursor.com/

Если вам интересно, как там у них все под капотом, послушайте подкаст с Лексом Фридманом:

Пострадайте вместе

Сейчас не хватает сил и внимания на что-то большое и осмысленное, поэтому короткая заметка на бегу.

Часто в удаленной работе не хватает чувства причастности к команде, чувства плеча, вот этого ощущения «да я в команде X». И вроде работаешь месяц, два, три, а до сих пор ощущение, будто, ты одинокий мститель в маске.

По моему опыту это ощущение команды появляется после совместных страданий, трудностей, например, какой-то адухи с пуском. Ребята, совместно преодолевшие весь геморрой, связанный с пуском, превращаются в братство. Они начинают доверять и полагаться друг на друга, потому что знают, что когда в очередной раз shit hits the fan, их коллега не сольется. Думаю, именно на страданиях, трудностях и совместном их преодолении строятся самые сплоченные команды в армии и спорте.

P. S. В августе прочитал всего ничего. Feel Good Productivity — херня. Автомобильная династия — часть про нацисткую Германию интересная, остальное херня. Типа, ничего себе, фантастически богатые еще сильнее богатеют. Неслыханно!

it "calls Foo#bar" — моветон

Ситуация: вы тестируете контроллер, который отправляет тестовое письмо со сводкой по вчерашним продажам. Под капотом контроллер обращается к классу DailySummaryEmail и вызывает метод #test. Вы пишете тест:

it "calls DailySummaryEmail#test" do

Это плохое, «машинное» описание проверки. Во-первых, тесты — это примеры использования кода, документация. Это пример чего? Чем он будет полезен читателю?

Во-вторых, это детали реализации, считай, приватный интерфейс. Если переименуем метод или класс, придется поправить и в теле проверки, и в ее описании.

В-третьих, это бесполезные детали. Я из тела проверки вижу, что вызываем DailySummaryEmail#test. Делаем-то это зачем? Чтобы что?

Лучше писать для людей, описывая то, что должно происходить в мире читателя:

it "sends previous day summary email to marketing department"

Один «бжж» или два «бжж»

Год назад вышел подкаст Лекса Фридмана с Хикару Накамурой. Хикару — крутой шахматист-стример, специализирующийся на быстрых шахматах. Когда-то побеждал Магнуса Карлсена.

В подкасте есть интересный эпизод с обсуждением читинга в шахматах:

На 1:15:55 Лекс задает самый важный технический вопрос: сколько информации тебе нужно, чтобы читерить? Накамура отвечает, что ему хватит одного «бжж», если текущая позиция отличная, и двух «бжж», если текущая позиция обычная или нормальная. То есть ему не нужны конкретные ходы, оценки или предсказания. Ему достаточно знать ответ на вопрос: текущая ситуация на доске отличная (для меня) или нет?

Я переформулировал этот вопрос в «это … улучшает ситуацию или нет?» и стал бесконечно спрашивать себя:

  • этот пулреквест, этот кусочек изменений улучшает текущую ситуацию в коде или нет? Это один «бжж» или два?
  • это решение с Редисом улучшит ситуацию в инфраструктуре или нет?
  • это решение посмотреть Ильдара-Автоподбора, на ночь глядя, улучшит ситуацию со сном или нет?
  • эти прекрасные кислые жевательные мармеладки-полоски улучшат ситуацию с весом и метоболическим синдромом или нет?

Такой странный вопрос помогает мне оценивать последствия решений и изменений в долгосрочной перспективе. Помогает взглянуть на проблему в контексте времени и позиции, добавляет еще одну точку зрения и заставляет стремиться к улучшению «позиции». Спасибо, Хикару!

Гипотеза: мы охереваем, потому что слишком легко

Есть гипотеза. Мы охереваем от перегруза, потому что понабрали слишком много обязательств. Эти обязательства тянут за собой планирование, коммуникацию и переключение доменных областей. Это и дает нам чувство разбитости и усталости.

Попробую показать на примере. Представим, что я подписался запрограммировать встраиваемый платежный виджет, сделать код-ревью в соседнем проекте о барсуках, выполнить мастера спорта по тяжелой атлетике, самостоятельно вести бухгалтерию в ИП и оптимизировать билд на CI, чтобы он укладывался в 3 минуты.

Каждый из этих проектов тянет за собой свою доменную область, которую надо восстанавливать в памяти. Тут у нас мерчанты и платежи. Тут барсуки, передержки и ветеринарки. Тут рывок, толчковая тяга и подрыв. Тут УСН, ПСН, счета, акты и ЭДО. Тут CI, yaml и профайлер. Любой, кто пробовал быстро переключить внимание между барсуками и УСН, знает, что это непросто. Мозг тупит и кипит минут 15-20. Есть риск задекларировать барсука.

Также каждый из этих проектов тянет за собой коммуникацию и планирование. Что там по дедлайнам у виджета? Нужно попросить у дизайнеров фотки барсуков на передержке. Не забыть зарегаться на соревнования по ТА. Акт не прошел в СБИС через роуминг, надо написать в техподдержку. Надо показать ПР с ускорением билда ребятам, чтобы покритиковали. Это тоже работа, которая также требует времени и внимания.

И кажется, что мы понабрали так много всего, потому что это «легко». Легко ревьюить соседний проект, это же не код писать. Легко выполнить мастер спорта по ТА, просто делай, что в программе написано. Легко вести ИП, в Эльбе уже все есть, только кнопки нажимай. Легко оптимизировать билд на CI, ты же программист.

Сколько обязательств вы взяли, потому что это казалось «легко»?