Я профессионально создаю библиотеки 5 лет
- пятница, 18 сентября 2026 г. в 00:00:06
7 лет назад я опубликовал в npm свою первую библиотеку — custom‑border‑mixin. Тогда я на неделю пропал, просто чтобы в своё удовольствие написать SCSS‑миксин. Если вам интересно, что весёлого может быть в SCSS, просто посмотрите на хелпер:
@function param-get($parameters, $key) { $value: map-get($parameters, $key); @if $key == 'side' and $value != 'top' and $value != 'right' and $value != 'bottom' and $value != 'left' { @error 'Value #{$value} of property #{$key} must be either top, or right, or bottom, or left, or vertical, or horizontal, or all.'; } @if ($key == 'size' or $key == 'length' or $key == 'gap') and (type-of($value) != number or type-of($value) == number and $value < 0) { @error 'Value #{$value} of property #{$key} must be non-negative size number.'; } @if $key == 'color' and type-of($value) != color { @error 'Value #{$value} of property #{$key} must be color.'; } @if $key == 'start' and $value != 'origin' and $value != 'center' and $value != 'opposite' { @error 'Value #{$value} of property #{$key} must be either origin, center, or opposite.'; } @return $value; }
Миксин никому конечно не нужен, и никто о нём не просил, но, кажется, он зажёг во мне некую страсть. Думаю, примерно так люди и приходят в опенсорс.
Меня зовут Дмитрий, и в этой статье я расскажу, как наступление агентного программирования повлияло на мой взгляд на дизайн библиотек и публичных API. И как оно на самом деле ничего не изменило в том, что делает библиотеку хорошей.
Я работал в платформенной команде. 5 лет назад завёл свой личный опенсорс‑проект Sury. Уже v11, и я еще не забил. Сейчас я работаю в Envio, где за деньги делаю самый быстрый инструмент для индексации блокчейна (HyperIndex на GitHub).
В идеальном мире между API для человека и для AI‑агента не должно быть почти никакой разницы. Просто так вышло, что раньше, проектируя библиотеки, мы часто рассчитывали на то, что пользователь прочитает доки, что‑то уже знает или у него есть контекст. С агентами это работает не всегда, поэтому важно спроектировать библиотеку так, чтобы API само вело пользователя…
…в тот самый pit of success — да‑да, я знаю, но это не стареет.
Кроме очевидных вещей вроде подсказывающих сообщений об ошибках, вот несколько практик, которые я применил в релизе Sury v11. Sury — это JavaScript‑библиотека схем, дальше все примеры будут на ней.
В Sury нет parse. Я не хотел давать дефолт, который где‑то там кинет исключение, и никто его не обработает. Поэтому есть два имени, и вам придётся выбрать:
S.parseOrThrow(userSchema, data); // { id: "p_1" } S.parseAsResult(userSchema, data); // { success: true, value: { id: "p_1" } } // { success: false, issues: [{message: "Expected string, received 42", path: ["name"]}]}
Теперь выбор зафиксирован в коде. Агент скорее возьмёт parseAsResult, а если нет, то на ревью другой агент реально увидит, что ошибку никто не обрабатывает.
Сравните с Zod, где короткое имя досталось parse, который кидает ошибку:
userSchema.parse(data); // throws userSchema.safeParse(data); // returns a result
Если убрать строчку с safeParse, то parse выглядит ровно так же «safe», правда? Ничто в parse не говорит, что может прилететь исключение, а безопасный вариант спрятан за именем, о котором надо знать заранее.
Иронично, но сейчас это важно даже больше, потому что ревью делает AI. У человека хотя бы могло возникнуть нехорошее предчувствие насчёт parse.
Теперь посмотрите на is/validate. Что‑то такое есть в каждой схема‑либе, и на первый взгляд выглядит совершенно безобидно:
if (is(userSchema, data)) { // data ведь User, да? }
Но если схема что‑то трансформирует, у этого вопроса два ответа. Каждая библиотека выбирает один за вас, и, сюрприз, выбирают они по‑разному:
Хелпер | Что проверяет | |
|---|---|---|
| Input | |
| Input | |
| Input | |
| Input | |
| Output | |
| Output | |
| Output | |
| сначала конвертирует, проходят оба | |
| сначала конвертирует, проходят оба | |
| тот, который вы выбрали сами |
Один и тот же вызов, противоположный смысл, в зависимости от того, что у вас в package.json. А Yup с Joi в тихую конвертируют значение и поэтому говорят «да» обоим.
Этакая русская рулетка. 👀
Вот поэтому S.is не существует:
const priceSchema = S.string.with(S.to, S.number); S.isInput(priceSchema, "42"); // true S.isOutput(priceSchema, "42"); // false
Агент, который не угадал порядок аргументов, тратит на это лишнюю итерацию. С людьми то же самое, только они хотя бы запомнят примерно со второго раза. Но зачем? Поэтому я сделал так, чтобы работал любой порядок:
S.parseOrThrow(data, userSchema); S.parseOrThrow(userSchema, data); S.parseOrThrow(userSchema)(data); S.isInput(data, userSchema); S.isInput(userSchema, data); S.isInput(userSchema)(data);
Все они дают одинаковый результат, так что любой вариант — правильная.
Когда кейс можно прочитать по‑разному, я не хочу выбирать дефолт за вас. Я заставляю выбрать:
S.decodeOrThrow(S.env, S.string); // SuryError: Ambiguous "" for string. Should a blank input be rejected, // kept, or read as absent? Choose with S.nonEmpty, S.minLength(0), // or S.optional
И падает это в момент создания декодера, а не когда придут данные, так что вы увидите это, пока пишете код. Пустая строка в переменной окружения — это решение, и мне кажется, моя библиотека не должна принимать его за вас.
Как бы мне ни хотелось продавить своё API, агент или человек сначала пробует синтаксис который знаком. Борьба с этим стоит лишней итерации и раздражения, поэтому я просто сделал алиасы:
S.union(["admin", { role: "user" }]); // Идеоматичный Sury код S.union([S.schema("admin"), S.schema({ role: S.schema("user") })]); // Явный Sury код S.union([S.literal("admin"), S.object({ role: S.literal("user") })]); // Zod-like API для привычности // все три: Schema<"admin" | { role: "user" }>
А если какое‑то из написаний вам в своей кодовой базе не нравится, то это правило линтера, а не решение, которое я должен принять за всех:
// eslint.config.js "no-restricted-syntax": ["error", { selector: "CallExpression[callee.object.name='S'][callee.property.name='object']", message: "Use S.schema instead of S.object", }]
Честно говоря, всё это вообще не про AI. Библиотека, которая сама ведёт пользователя, и для людей всегда была лучше. Просто агенты перестали прощать то, что мы раньше закрывали документацией.
Всё это есть в Sury v11, который уже вышел. А если хотите больше про библиотеки схем и дизайн библиотек, подписывайтесь на меня в X — это сделает мой день 🙏