Files
Web-Parser-Constructor/docs/implementation-plan.md
T
2026-10-01 00:28:16 +10:00

211 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
> Архив первоначального проекта. С 2026-10-01 Rio удалён: приложение использует FastAPI, Jinja2 и HTML/CSS/JavaScript. Актуальное описание — в README.md.
# План реализации учебного сайта для парсинга
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans for implementation in this session, or superpowers:subagent-driven-development if the user chooses delegation. Steps use checkbox syntax for tracking.
**Goal:** Создать универсальный учебный сайт с кабинетом преподавателя на Rio, импортом данных, HTML и динамическими упражнениями, готовый к запуску через Docker Compose.
**Architecture:** Один Python-сервис объединяет Rio и FastAPI с Jinja2. Импорт, выборка, публикации и SQLite общие для обоих режимов; публичные запросы читают неизменяемые версии.
**Tech Stack:** Python, rio-ui, FastAPI, Jinja2, SQLite через sqlite3, Argon2 для пароля, pytest, requests, BeautifulSoup, Playwright, Docker Compose. Точные версии закрепляются после проверки совместимости в задаче 1.
**Spec:** [Утверждённая спецификация](design-spec.md).
## Рабочее место и ограничения
Новый проект создаётся в `outputs/parser-lab/` относительно текущего рабочего каталога. Все пути ниже относятся к этому корню. Старый проект `P:\Coding\Python\NetologyWebParserSite\HockeyTeamParse` служит источником хоккейного примера и не перезаписывается. Документы копируются в `docs/` нового проекта. Временные результаты проверок находятся в `work/` текущего рабочего каталога. При наличии Git этапы фиксируются отдельными коммитами; отсутствие Git не блокирует сборку и передачу проекта.
- Один аккаунт преподавателя; без регистрации и аккаунтов студентов.
- Четыре представления; оба режима для каждого включённого представления.
- Файл до 25 МиБ, до 50 000 записей, глубина JSON до 20 уровней; пределы настраиваемы.
- Размеры страниц 10, 25, 50, 100; исходное значение 25. Предпросмотр первых 20 записей.
- Unicode, пропуски, дубликаты, порядок и исходные типы сохраняются; CSV остаётся строковым.
- Русский интерфейс, светлая тема, адаптивность, локальные ресурсы.
- Неизменяемые публикации, отсутствие доступа к черновикам и скрытым полям извне.
- Один контейнер, порт 8080, постоянный каталог `/data`, HTTP и WebSocket за reverse proxy.
- Нет автоматического деплоя на внешний сервер, обучения моделей или публичного API выгрузки.
## Особое внимание при проверке
1. Заголовки JSON с точками, слешами и тильдами не должны путаться с вложенными путями — задачи 2 и 4.
2. Значения null, false, 0 и пустая строка не должны схлопываться в отсутствие поля — задачи 2 и 5.
3. Замена файла и авария публикации не должны повреждать доступный студентам снимок — задачи 3 и 7.
4. Прямой запрос страницы деталей или callback Rio не должен обходить авторизацию либо раскрывать скрытое поле — задачи 6, 8 и 10.
5. Две вкладки преподавателя не должны незаметно перезаписывать чужие изменения — задача 3; сохранять черновик с ожидаемой ревизией и показывать конфликт.
## Карта файлов
| Файлы | Ответственность |
|---|---|
| `pyproject.toml`, `requirements.lock`, `requirements-dev.lock` | Зависимости приложения и проверок |
| `src/parser_lab/app.py`, `config.py`, `__main__.py` | Сборка сервиса, конфигурация, запуск |
| `src/parser_lab/domain.py`, `paths.py` | Общие типы и JSON Pointer |
| `src/parser_lab/importing/csv_reader.py`, `json_reader.py`, `service.py` | Разбор файлов и диагностика |
| `src/parser_lab/storage/database.py`, `repository.py`, `migrations/001.sql` | База, миграции, транзакции |
| `src/parser_lab/views.py`, `query.py`, `publishing.py` | Конфигурация представлений, выборка, публикация |
| `src/parser_lab/auth.py`, `cli.py` | Сессии, вход, создание хеша пароля |
| `src/parser_lab/web/routes.py`, `templates/`, `static/` | HTML, стили и ресурсы |
| `src/parser_lab/rio_ui/admin/`, `rio_ui/public/`, `rio_ui/theme.py` | Кабинет, динамические страницы, тема |
| `tests/unit/`, `tests/integration/`, `tests/browser/` | Проверки поведения |
| `examples/data/`, `examples/parsers/` | Данные и решения для преподавателя |
| `Dockerfile`, `compose.yaml`, `.env.example`, `.dockerignore`, `README.md` | Развёртывание и использование |
Каждая задача включает запуск своей проверки до реализации, подтверждение ожидаемого сбоя, реализацию, повторную проверку и фиксацию результата. Ошибки настройки тестового окружения не считать ожидаемым провалом поведения.
## Задача 1 Совместимый каркас сервиса
**Files:** создать `pyproject.toml`, оба lock-файла, `src/parser_lab/{__init__,app,config,__main__}.py`, `tests/integration/test_app.py`.
**Interfaces:** `Settings.from_env() -> Settings`; `create_app(settings: Settings) -> FastAPI`. Settings содержит каталог данных, лимиты импорта, имя администратора, хеш пароля, секрет сессий, secure-cookie и доверенные origin. В тестах настройки передаются явно.
- [ ] Проверить доступные Python и Docker, прочитать API установленной для проекта Rio, выбрать поддерживаемую Python и закрепить совместимые версии. Не обновлять глобальное окружение пользователя.
- [ ] Добавить тест `test_html_route_coexists_with_rio`: `client.get('/healthz').status_code == 200`, HTML-проба содержит `'<table'`, запрос динамической страницы возвращает Rio-оболочку. Проверить lifespan при старте и завершении.
- [ ] Запустить `python -m pytest tests/integration/test_app.py -q`, подтвердить отсутствие реализации.
- [ ] Реализовать app factory, конфигурацию и совмещение пользовательских маршрутов с `rio.App.as_fastapi()` по подтверждённому API. Проба находится в тестовом приложении, а не в рабочей выдаче.
- [ ] Повторить тест и открыть динамическую пробу в браузере: компонент отображается и кнопка обновляет значение через WebSocket. Зафиксировать версии, команду запуска и результат.
Результат: работающий общий сервер; архитектурные ограничения Rio выявлены до создания кабинета.
## Задача 2 Импорт без потери исходных значений
**Files:** создать `domain.py`, `paths.py`, `importing/{__init__,csv_reader,json_reader,service}.py`, `tests/unit/test_importing.py`, `tests/unit/test_paths.py`.
**Interfaces:** `JsonValue` — стандартное JSON-значение; `Record = dict[str, JsonValue]`; `ImportOptions(format, encoding, delimiter, record_pointer)`; `ImportResult(records, fields, warnings)`; `ImportIssue(message, line, column)`; `read_upload(data: bytes, filename: str, options: ImportOptions, settings: Settings) -> ImportResult`; `resolve_pointer(record: Record, pointer: str) -> ResolvedValue(present, value)`.
- [ ] Добавить проверки: CSV `'id;answer\n001;\n'` даёт `{'id': '001', 'answer': ''}`; JSON сохраняет `False`, `0`, `None`; `resolve_pointer({'a/b': {'~c': 0}}, '/a~1b/~0c').value == 0`; отсутствующий путь имеет `present == False`.
- [ ] Добавить параметризованные случаи: UTF-8 BOM, Windows-1251, многострочные CSV-ячейки, плохая длина строки, повторные заголовки и JSON-ключи, JSONL с ошибкой на строке 3, пустые строки JSONL, одиночный объект, вложенный массив, NaN/Infinity, глубина 21, лимиты размера и количества, массив скаляров.
- [ ] Выполнить `python -m pytest tests/unit/test_importing.py tests/unit/test_paths.py -q`, проверить ожидаемые провалы.
- [ ] Реализовать чтение с `csv` и `json`, строгой обработкой повторяющихся ключей, ограничением размера до разбора и контролем глубины/числа записей. Не угадывать формат по пользовательскому имени без проверки содержимого. JSONL читает построчно; результаты не сохраняются частично.
- [ ] Повторить тесты. Проверить объединение полей по всем записям, явный выбор вложенного массива и различение пустого набора от ошибки.
## Задача 3 Хранение черновиков и исходных файлов
**Files:** создать `storage/{__init__,database,repository}.py`, `storage/migrations/001.sql`, `tests/integration/test_repository.py`.
**Interfaces:** `Repository.create_draft(title: str, source: bytes, imported: ImportResult) -> Draft`; `get_draft(dataset_id: str) -> Draft`; `save_draft(draft: Draft, expected_revision: int) -> Draft`; `replace_import(dataset_id: str, source: bytes, imported: ImportResult, expected_revision: int) -> Draft`. `Draft` содержит id, slug, revision, записи, источник, представления, название и описание. Ошибки: `NotFound`, `RevisionConflict`.
- [ ] Добавить тесты сохранения и повторного открытия SQLite: `reopened.records == imported.records`; повторное сохранение со старой revision вызывает `RevisionConflict`; авария транзакции не меняет старую запись. Проверить, что исходное имя `'../../outside.json'` не влияет на фактический путь хранения.
- [ ] Запустить `python -m pytest tests/integration/test_repository.py -q` до реализации.
- [ ] Реализовать таблицы datasets, draft_records, sources, views, publications, published_records, sessions, schema_version. Записи хранятся JSON вместе с ordinal и внутренним id. Все SQL параметризованы; опубликованный снимок не зависит от изменяемого черновика.
- [ ] Реализовать файлы под UUID, временную запись с последующим переносом в пределах `/data`, транзакционную привязку к базе и очистку несвязанных временных файлов. Замена импорта сбрасывает подтверждение настройки полей.
- [ ] Повторить тесты и проверить идемпотентный запуск миграции и откат при ошибке записи файла.
## Задача 4 Настройки представлений
**Files:** создать `views.py`, `tests/unit/test_views.py`; дополнить общие модели в `domain.py`.
**Interfaces:** `ViewConfig(kind, columns, roles, questionnaire_mode, questions_pointer, page_size, search_pointer, category_pointer)`; `FieldConfig(pointer, label)`; `validate_view(config: ViewConfig, records: list[Record]) -> list[ImportIssue]`; `project_record(record: Record, config: ViewConfig) -> DisplayRecord`. `DisplayRecord` содержит только назначенные значения и сведения об их наличии/типе.
- [ ] Добавить проверки для всех четырёх видов: произвольные подписи колонок; текст сообщения обязателен в конфигурации, автор — нет; анкета из колонок и массива question/answer; отсутствующее значение в отдельной записи допустимо и помечается. Путь, которого нет ни в одной записи, не допускается как обязательная роль.
- [ ] Проверить страницу 25 по умолчанию и отклонение 24; выбор только текстового поля для поиска; категориальное поле должно быть скалярным. Проверить, что `secret` отсутствует во всём сериализованном DisplayRecord, когда оно не выбрано.
- [ ] Запустить `python -m pytest tests/unit/test_views.py -q` до реализации.
- [ ] Реализовать явные конфигурации без эвристического назначения смысла полям. JSON Pointer отвечает за чтение и экранирование имён; структура анкеты не смешивается с универсальным табличным рендером.
- [ ] Повторить тесты, включая слеши и тильды в именах полей, смешанные записи и многострочные ответы.
## Задача 5 Общая выборка и HTML
**Files:** создать `query.py`, `web/{__init__,routes}.py`, `web/templates/{base,list,detail,table,cards,messages,questionnaire}.html`, `web/static/site.css`, `tests/unit/test_query.py`, `tests/integration/test_html.py`.
**Interfaces:** `Query(page=1, search='', category=None)`; `select_records(records: list[StoredRecord], config: ViewConfig, query: Query) -> PageResult`; `PageResult(items, total, page, page_size, has_next)`. `StoredRecord(id, ordinal, data)` используется обоими режимами. `register_html_routes(app: FastAPI, repository: Repository) -> None` после задачи 7 читает Publication; до этого HTML тестируется на явной фикстуре снимка.
- [ ] Добавить тесты выборки: 51 запись и размер 25 дают страницы 25/25/1, ids не пересекаются; поиск регистронезависимый, фильтр точный и сохраняет различие числа 0 и строки `'0'`; порядок стабилен.
- [ ] Добавить тест HTML: `BeautifulSoup(response.text, 'html.parser').select('tbody tr')` содержит нужные значения без JavaScript. Вредоносное значение `<script>alert(1)</script>` остаётся текстом, исполняемый тег не появляется. Проверить отсутствие скрытых полей на списке и деталях.
- [ ] Запустить `python -m pytest tests/unit/test_query.py tests/integration/test_html.py -q` до реализации.
- [ ] Реализовать общий отбор, Jinja2 с autoescape, реальные ссылки пагинации и деталей. CSS-контракт: `.record`, `.field`, `data-record-id`, `data-field`, `data-value-state`; состояния `missing`, `null`, `value`. Ссылки сохраняют фильтры и версию, значения URL кодируются.
- [ ] Определить детали по `/html/{slug}/v/{version}/{view}/records/{id}`. Некорректная страница — 400, отсутствующая — 404. Для фильтра без совпадений страница 1 возвращает 200 с пустым состоянием; следующие — 404.
- [ ] Повторить тесты и проверить ручным HTTP-запросом, что содержимое и ссылки входят в исходный ответ.
## Задача 6 Авторизация преподавателя
**Files:** создать `auth.py`, `cli.py`, `tests/integration/test_auth.py`; дополнить `app.py`, `config.py`.
**Interfaces:** `AuthService.login(username: str, password: str, client_key: str) -> SessionToken`; `require_admin(token: str) -> AdminSession`; `logout(token: str) -> None`; CLI `python -m parser_lab.cli hash-password` читает пароль скрытым вводом.
- [ ] Написать тесты: неверный пароль не создаёт сессию; приватный маршрут без сессии отклонён; выход отзывает сессию; изменяющий POST без CSRF отклонён; внешний Origin при WebSocket/callback отклонён; cookie HttpOnly/SameSite и Secure соответствуют настройке.
- [ ] Зафиксировать предел входа: 5 неудачных попыток на client_key за 5 минут, далее 429 до истечения окна; срок сессии 12 часов. Добавить тест управляемым временем, без реального ожидания. Не доверять произвольному X-Forwarded-For.
- [ ] Запустить `python -m pytest tests/integration/test_auth.py -q` до реализации.
- [ ] Реализовать Argon2, случайный токен с хранением только его хеша, проверку срока сессии в каждом защищённом действии. HTML-формы и Rio-сессии используют одну авторизацию; редирект страницы сам по себе не считается защитой callback.
- [ ] Повторить тесты, проверить отсутствие паролей в логах и безопасный отказ запуска без обязательных настроек. Изменение учётных данных отзывает прежние сессии.
## Задача 7 Неизменяемые публикации
**Files:** создать `publishing.py`, `tests/integration/test_publications.py`; дополнить repository и HTML routes.
**Interfaces:** `PublishingService.publish(dataset_id: str, expected_revision: int) -> Publication`; `get_publication(slug: str, version: int) -> Publication`; `unpublish(slug: str, version: int) -> None`. `Publication` содержит slug, version, title, description, views и снимок records. Неопубликованная версия вызывает `NotFound`.
- [ ] Добавить тест: после публикации A и изменения черновика на B `get_publication(slug, 1).records` остаются A; повторная публикация создаёт version 2. Проверить откат при сбое копирования, запрет пустого набора и конфликт revision.
- [ ] Запустить `python -m pytest tests/integration/test_publications.py -q` до реализации.
- [ ] Реализовать атомарную публикацию всех включённых валидных представлений и отдельное снятие версии. Если одно включённое представление невалидно, публикация целиком отклоняется с указанием причины.
- [ ] Закрепить slug после первой публикации, чтобы переименование заголовка не ломало ссылки. Разрешить ASCII lowercase, цифры и дефисы; уникальность проверяет база.
- [ ] Подключить HTML к реальным снимкам и повторить тесты: снятая версия и неизвестный id возвращают 404; прежние ссылки остаются прежними после редактирования.
## Задача 8 Кабинет Rio
**Files:** создать `rio_ui/admin/{__init__,login,catalog,import_wizard,view_editor,preview,publications}.py`, `rio_ui/theme.py`, `tests/browser/test_admin.py`.
**Interfaces:** компоненты `LoginPage`, `CatalogPage`, `ImportWizard`, `ViewEditor`, `PreviewPage`, `PublicationsPage` получают сервисы через подтверждённый механизм Rio attachment. Состояние несохранённых изменений принадлежит конкретной сессии, постоянные данные — Repository. Вызовы методов изменения требуют `require_admin` и expected_revision.
- [ ] Создать браузерные сценарии входа, импорта CSV, выбора полей, чернового предпросмотра, публикации и выхода; до реализации сценарии не находят соответствующие экраны.
- [ ] Реализовать каталог и мастер с шагами «Файл», «Проверка», «Отображение», «Предпросмотр». Внешние названия не содержат JSON Pointer; выбор путей показан деревом. Ошибка импорта сохраняет выбранные настройки и объясняет причину.
- [ ] Реализовать оба предпросмотра за авторизацией, смену представлений, предупреждение о несохранённых изменениях и сообщение о конфликте двух вкладок. Дублирующие операции отключаются до завершения запроса.
- [ ] При работе FileInfo прочитать загруженные байты один раз и хранить до подтверждения/отмены; валидация повторяется на сервере, а не только фильтром расширений. Предпросмотр показывает 20 записей, число всех записей и предупреждения.
- [ ] Выполнить `python -m pytest tests/browser/test_admin.py -q` с браузером Playwright и отдельной временной базой. Проверить ручной вход, восстановление после ошибки и невозможность действия после logout в другой вкладке.
## Задача 9 Динамические упражнения Rio
**Files:** создать `rio_ui/public/{__init__,listing,detail,renderers}.py`, `tests/browser/test_interactive.py`.
**Interfaces:** `InteractivePage` загружает Publication по slug/version и использует `select_records` и `project_record`; `RecordPage` показывает только выбранные значения. Состояние страницы хранит фильтры, next_page, generation и флаг loading.
- [ ] Создать сценарий: 51 запись, размер 25; после двух нажатий «Загрузить ещё» получены 51 уникальных id, кнопки больше нет. Проверить каждое из четырёх представлений.
- [ ] Проверить быстрые повторные клики, смену фильтра во время загрузки и снятие публикации при открытой странице. Устаревший ответ фильтра не должен добавляться к новому списку; снятые данные не выдаются следующими запросами.
- [ ] Запустить `python -m pytest tests/browser/test_interactive.py -q` до реализации.
- [ ] Реализовать дозагрузку, состояния и детали, используя подтверждённые Rio API. Стабильные доступные подписи и роли обязательны; способы установки дополнительных DOM-атрибутов сначала проверить по версии Rio. Не строить тесты на случайных сгенерированных классах.
- [ ] Повторить сценарии; проверить прямое открытие вложенного URL, обновление браузера и отсутствие скрытых значений в передаваемых данных.
## Задача 10 Дизайн и проверка пользовательского сценария
**Files:** доработать `rio_ui/theme.py`, Rio-компоненты, `web/static/site.css`, HTML-шаблоны; создать `tests/browser/test_layout.py`, `tests/integration/test_visibility.py`.
- [ ] Реализовать общие цветовые и типографические параметры, боковую навигацию, компактную навигацию, карточки статусов и локальные ресурсы. Не добавлять новые функции ради оформления.
- [ ] Проверить 1440×900 и 390×844: основные действия доступны; длинный текст переносится; горизонтально прокручивается таблица, а не вся страница; заголовки и ответы не перекрываются. Сохранить скриншоты в рабочую папку и визуально просмотреть.
- [ ] Проверить клавиатурный фокус, подписи полей, ошибки рядом с полями и состояния пустых результатов. Для простых косметических правок использовать визуальную проверку, не тестировать каждую константу стиля.
- [ ] Добавить интеграционные тесты скрытых полей на списках, деталях, HTML предпросмотре без входа и прямых служебных запросах. Проверить пользовательские `<script>`, HTML и URL как неисполняемые значения.
- [ ] Запустить `python -m pytest tests/integration/test_visibility.py tests/browser/test_layout.py -q`, исправить наблюдаемые проблемы и повторить только затронутые проверки.
## Задача 11 Примеры и контроль сбора данных
**Files:** создать `examples/data/{hockey_teams.csv,messages.jsonl,questionnaires.json,profiles.csv}`, `examples/parsers/{scrape_html.py,scrape_interactive.py}`, `tests/integration/test_scraping.py`.
**Interfaces:** оба CLI принимают URL опубликованного представления и путь результата; выводят JSONL с теми же видимыми полями, record_id и состоянием отсутствующих значений. Решения не монтируются как публичные assets.
- [ ] Скопировать хоккейный CSV из старого проекта без изменения источника; подготовить синтетические примеры других представлений.
- [ ] Создать проверку ожидаемых данных: экспорт обоих парсеров совпадает с проекцией исходных записей, включая порядок, дубликаты значений, missing/null и последнюю неполную страницу.
- [ ] Реализовать HTTP-парсер на requests и BeautifulSoup с переходом по ссылкам, динамический — на Playwright с ожиданием состояния/элементов, без фиксированных sleep. Проверить таймауты, HTTP-ошибку и прекращение при снятой версии.
- [ ] Выполнить `python -m pytest tests/integration/test_scraping.py -q` на локальном тестовом сервере. Сверить каждый вид в двух режимах; автоматизировать повторяющиеся варианты параметризацией.
## Задача 12 Docker и передача проекта
**Files:** создать `Dockerfile`, `compose.yaml`, `.env.example`, `.dockerignore`, `README.md`, `docs/{design-spec,implementation-plan}.md`.
- [ ] Собрать образ на закреплённой совместимой Python, установить lock-зависимости, включить локальные ресурсы, запускать непривилегированным пользователем. Порт 8080, healthcheck `/healthz`, named volume в `/data`, без исходников и секретов рабочего окружения в образе.
- [ ] Проверить `docker compose config` и `docker compose build`; затем `docker compose up -d`. Передавать тестовые секреты безопасным локальным файлом, исключённым из Git и образа.
- [ ] Через контейнер пройти импорт и публикацию, HTTP-парсер и Playwright. Выполнить `docker compose down` без `-v`, поднять снова и проверить прежние ссылки и данные.
- [ ] Проверить резервное копирование при остановленном контейнере и восстановление в отдельный пустой том, не удаляя рабочий. Зафиксировать права каталога и успешный healthcheck после восстановления.
- [ ] Документировать запуск, генерацию хеша, переменные окружения, смену пароля, reverse proxy с WebSocket, сохранность тома, обновление, резервную копию, примеры файлов и парсеров. Не обещать проверку HTTPS, если proxy фактически не запускался.
- [ ] Выполнить полный `python -m pytest -q`, сборку и контрольный пользовательский сценарий. Если Docker Engine недоступен, сохранить Docker-файлы, явно отметить непроверенную сборку и не считать Docker-критерий выполненным.
## Контроль перед завершением
- [ ] Сопоставить реализованные функции со всеми критериями спецификации, включая отсутствие утечек полей и сохранность версий.
- [ ] Проверить весь проект независимым обзором после реализации; устранить подтверждённые дефекты и повторить затронутые проверки.
- [ ] Удалить только собственные временные тестовые данные из поставки, остановить временные процессы, сохранить пользовательские данные.
- [ ] Передать путь к проекту, инструкцию запуска, проверенные команды и известные ограничения. Публикацию на внешний сервер выполнять только по отдельному запросу.
## Порядок исполнения
Задачи выполняются последовательно в этой беседе. Тесно связанные интерфейсы импорта, представлений и публикаций делают этот вариант предпочтительным для первой реализации. Делегирование отдельным агентам возможно по выбору пользователя; оно не требуется для корректного выполнения плана.
План подготовлен для проверки перед началом реализации. Во время исполнения чекбоксы и результаты обновляются по фактическим проверкам, а не по наличию файлов.