Перейти к содержанию

Одна задача и один небольшой результат

Контекст и реестр адресов подготовлены. Теперь можно поставить агенту узкую задачу и показать авторский кандидат её решения: функцию featured_pages. Она принимает загруженные записи, оставляет публичные рекомендуемые материалы и возвращает ограниченное число элементов в устойчивом порядке. После этого урока новая операция будет иметь явный интерфейс, а остальная загрузка библиотеки сохранит прежнюю ответственность.

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

Функция как граница задачи

Новая функция получает rows и limit. Она не открывает манифест самостоятельно, не читает текст повторно и не пишет файлы. Благодаря этому можно обсуждать отбор отдельно от загрузки. Если позже изменится способ получения записей, вызывающий код по-прежнему сможет передать ту же структуру. Мы не создаём универсальную архитектуру на будущее, но сохраняем простую границу между источником данных и операцией над уже прочитанным массивом.

Сначала используется public_pages(rows). Затем оставляются записи, у которых featured буквально равно True. Такой порядок выражает редакционное правило: рекомендация не отменяет приватность. Служебная заметка с featured: true не должна появиться на главной. Если выполнить операции в другом порядке, а затем забыть публичный фильтр, карточка станет каналом случайной публикации. Поэтому право присутствия в публичном наборе проверяется раньше признака оформления.

Обратите внимание на точное сравнение логического значения. Строка 'false' в Python непустая и при обычной проверке истинности считается истинной. В нашей JSON-модели признак является boolean, а не произвольной строкой. Выражение is True делает ожидание явным. Оно не заменяет полноценную валидацию всего манифеста, но предотвращает расширение выбранного правила на значения, которые не соответствуют редакционному контракту.

Устойчивый порядок

После фильтрации записи сортируются по id. Этот идентификатор не зависит от названия статьи или порядка объектов в JSON. Если редактор переставит строки манифеста, рекомендуемый вывод не изменится случайно. Мы выбрали алфавитный порядок устойчивых идентификаторов для простоты. В настоящей библиотеке вместо него можно ввести явный рейтинг либо редакционный порядок, но сначала потребуется назвать поле и определить поведение при совпадении значений.

Ограничение применяется после сортировки. Если сначала взять первые два элемента исходного массива, а затем отбирать рекомендации, может получиться меньше материалов при наличии подходящих записей дальше. Если сначала ограничить только рекомендации, а затем исключать private, снова возникает зависимость от расположения служебных данных. Последовательность операций отражает смысл результата: определить разрешённый набор, упорядочить его и лишь затем взять нужное количество.

Параметр limit проверяется до отбора. Допустимо только целое число от одного до пяти. Логическое True отклоняется, хотя в Python оно связано с целыми числами. Почему нельзя молча преобразовать строку '2'? Преобразование прячет ошибку границы вызывающего кода. Если пользовательский интерфейс получает строку из формы, он должен разобрать её отдельно и передать уже определённый параметр. Функция отбора не отвечает за интерпретацию произвольного пользовательского ввода.

Что именно изменилось

В основном блоке catalog.py вместо всего публичного набора теперь вызывается featured_pages(rows). Формат краткого результата остаётся прежним: id, title, url. То есть изменение касается выбора, а не структуры адреса или содержания документов. Это удобно для ревью: два самостоятельных факта можно сопоставить с контрактом. Новый фильтр реализует пожелание, а сохранённая форма результата позволяет прежнему потребителю продолжать работать с теми же полями.

Для текущих данных ожидаются записи sitemap и urls в порядке идентификаторов. Текст документа markdown остаётся доступным в библиотеке, хотя карточка не попадает в рекомендуемый набор. notes остаётся служебным. Такое различие помогает объяснить владельцу сайта, что компактная подборка не удаляет материалы. В реальном шаблоне рядом могут существовать каталог, поиск и учебные маршруты; эта функция не решает, какие остальные ссылки будут показаны на странице.

Представьте, что редактор снимет все признаки рекомендации. Ожидаемый результат функции — пустой список. Она не выбирает случайные страницы и не меняет манифест, чтобы выполнить требование «должно быть две карточки». Если интерфейсу нужен запасной набор, это отдельное правило, которое следует согласовать и реализовать явно. Пустое состояние в такой задаче является корректным результатом, а не неполадкой, автоматически разрешающей расширить область изменения.

Перед принятием кандидата посмотрите, нет ли в нём скрытых побочных действий: записи манифеста, чтения внешнего URL, установки пакета, изменения шаблона. Для простой функции они не требуются. Чем меньше сопутствующих изменений, тем легче связать результат с причиной. Это не означает, что большой проект всегда можно исправить одной функцией; масштаб области должен соответствовать фактическому механизму и поставленной задаче.

Рекомендация не даёт публичность

Поменяем мысленно только один признак: у notes появилось featured: true, а public осталось false. Ожидаемый набор не должен измениться. Это хороший соседний случай, потому что он проверяет смысл последовательности операций, а не только количество карточек. Если исполнитель использовал один фильтр featured без public, служебная запись получит место в рекомендациях. Внешне подборка может выглядеть аккуратно, хотя договор доступа нарушен.

Для сравнения снимите featured у sitemap. Тогда ожидается одна рекомендация urls, а не автоматическая замена документом markdown. Эти два изменения помогают увидеть, какие поля принимают решения: public отвечает за допустимость публичного присутствия, featured — за редакционный выбор внутри допустимого набора. Ни текст, ни название файла не должны отменять их смысл.

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

Следующий шаг — ревью. Мы сопоставим кандидат с конкретными условиями, отделим статическое чтение от запуска и рассмотрим типичную ошибку проверки limit. Не стоит принимать предложение только потому, что оно сопровождается убедительным пояснением: пояснение помогает понять решение, но его утверждения должны иметь своё основание в исходниках или наблюдении.

Полный файл catalog.py из состояния lesson-05. Полное самостоятельное состояние находится в архиве серии; фрагмент не заменяет остальные определения.

from pathlib import Path
import json

ROOT = Path(__file__).resolve().parent

def load_pages():
    rows = json.loads((ROOT / "manifest.json").read_text(encoding="utf-8"))
    seen_ids, seen_urls = set(), set()
    for row in rows:
        if row["id"] in seen_ids or row["url"] in seen_urls:
            raise ValueError("duplicate identity")
        seen_ids.add(row["id"])
        seen_urls.add(row["url"])
        source = (ROOT / "corpus" / row["file"]).resolve()
        if source.parent != (ROOT / "corpus").resolve() or source.suffix != ".md":
            raise ValueError("outside corpus")
        row["text"] = source.read_text(encoding="utf-8")
    return rows

def public_pages(rows):
    return [row for row in rows if row["public"] is True]

def featured_pages(rows, limit=2):
    if type(limit) is not int or not 1 <= limit <= 5:
        raise ValueError("limit must be an integer from 1 to 5")
    return sorted(
        (row for row in public_pages(rows) if row.get("featured") is True),
        key=lambda row: row["id"],
    )[:limit]

if __name__ == "__main__":
    rows = load_pages()
    selected = featured_pages(rows)
    print(json.dumps([{"id": r["id"], "title": r["title"], "url": r["url"]} for r in selected], ensure_ascii=False, indent=2))

Статус примера: not_run. При подготовке не выполнялись учебные программы, тесты, вызовы AI или деплой. Описанные эффекты ожидаемые.

Оглавление серии · Полные исходники

Первичные документы: Источник 1.