Initial commit
This commit is contained in:
commit
c534d5ce80
163 files changed
+11500
No files matched your search
@@ -0,0 +1,251 @@
|
||||
# Архитектура шаблона документов Scientia
|
||||
|
||||
## Системный контекст
|
||||
|
||||
```text
|
||||
┌────────────────────┐ редактирует ┌──────────────────────────────────┐
|
||||
│ Автор документа │ ────────────▶ │ main.typ + chapters/ + assets/ │
|
||||
└─────────┬──────────┘ └────────────────┬─────────────────┘
|
||||
│ читает │ один import фасада
|
||||
▼ ▼
|
||||
┌────────────────────┐ ┌──────────────────────────────────┐
|
||||
│ docs/ │ │ .template/lib/ │
|
||||
│ инструкции и │ │ domain → application → │
|
||||
│ примеры │ │ infrastructure → presentation │
|
||||
└────────────────────┘ └────────────────┬─────────────────┘
|
||||
│
|
||||
┌────────────────────┐ условный import │ PDF
|
||||
│ .private/ │ ────────────▶ main.typ ─────────┤
|
||||
│ PNG + settings.typ │ один boolean ▼
|
||||
┌────────────────────┐
|
||||
│ document.pdf │
|
||||
└────────────────────┘
|
||||
|
||||
┌────────────────────┐ сопровождает ┌─────────────────────────────────┐
|
||||
│ Разработчик │ ─────────────▶ │ .template/development/ │
|
||||
│ шаблона │ │ docs + modules + tests + tools │
|
||||
└────────────────────┘ └─────────────────────────────────┘
|
||||
```
|
||||
|
||||
Публичная и developer-документация физически разделены. Автору не требуется открывать `.template/`, а разработчик не использует `docs/` как описание внутренних контрактов.
|
||||
|
||||
## Архитектурные принципы
|
||||
|
||||
1. **Один файл ежедневной настройки**. Компания, режим, metadata и `#include` находятся в `main.typ`.
|
||||
2. **Режим — значение, а не entrypoint**. `final`, `draft` и `clean-copy` являются вариантами одной переменной.
|
||||
3. **Публичное видно**. Инструкции и примеры находятся в `docs/`, который VS Code не скрывает.
|
||||
4. **Разработка скрыта**. Библиотека, tests, ADR и tools находятся в `.template/`.
|
||||
5. **Пример является исполняемой документацией**. Один исходник одновременно обучает, компилируется в CI и устанавливается как тип документа.
|
||||
6. **Важные параметры явные**. Даже отключённые значения показаны как `none`, `false` или `()`.
|
||||
7. **Приватное не отслеживается**. Вся папка `.private/`, включая настройки offsets, исключена из Git; tracked placeholders никогда не заменяются реальными файлами.
|
||||
8. **Один переключатель**. При `false` условный import не читает `.private`; при `true` обычный preview и обычная build task используют её настройки.
|
||||
9. **Фасад скрывает реализацию**. Пользователь импортирует только `/.template/lib/index.typ`.
|
||||
10. **Domain не зависит от layout**. Presentation использует domain contracts, но обратной зависимости нет.
|
||||
|
||||
## Целевая структура
|
||||
|
||||
```text
|
||||
README.md # короткий маршрут автора
|
||||
main.typ # единственная точка входа и настройки
|
||||
chapters/ # пользовательский текст
|
||||
assets/ # изображения, данные и bibliography
|
||||
docs/ # публичная документация
|
||||
├── README.md
|
||||
├── documents.md
|
||||
├── formatting.md
|
||||
├── vscode.md
|
||||
├── git.md
|
||||
├── private-assets.md
|
||||
├── writing-style.md
|
||||
├── troubleshooting.md
|
||||
└── examples/
|
||||
├── README.md
|
||||
├── private/
|
||||
│ └── settings.typ
|
||||
├── documents/
|
||||
│ ├── report/
|
||||
│ ├── letter/
|
||||
│ ├── commercial-offer/
|
||||
│ └── contract/
|
||||
└── formatting/
|
||||
.vscode/ # tracked workspace configuration
|
||||
├── extensions.json
|
||||
├── settings.json
|
||||
└── tasks.json
|
||||
.private/ # ignored settings, private media и backups
|
||||
├── settings.typ
|
||||
├── executors/
|
||||
├── scientia/
|
||||
├── technology/
|
||||
└── too/
|
||||
.template/ # скрытая реализация
|
||||
├── lib/
|
||||
├── companies/
|
||||
└── development/
|
||||
├── docs/
|
||||
├── modules/
|
||||
├── tests/
|
||||
├── tools/
|
||||
└── vscode/
|
||||
```
|
||||
|
||||
В чистом fork каталог `.private/` не обязателен. Обычная компиляция использует placeholders из `.template/lib/assets/placeholders/`.
|
||||
|
||||
## Компоненты
|
||||
|
||||
| Компонент | Ответственность | Публичный интерфейс |
|
||||
|-----------|-----------------|---------------------|
|
||||
| Рабочее пространство автора | Один входной файл, главы и ресурсы | `main.typ`, `chapters/`, `assets/` |
|
||||
| Публичная документация | Обучение без знания реализации | `README.md`, `docs/*.md` |
|
||||
| Публичные примеры | Исполняемые примеры и источники выбора типа | `docs/examples/**/main.typ` |
|
||||
| VS Code workspace | Рекомендации, автосохранение и задачи | `.vscode/*.json` |
|
||||
| Публичный фасад | Единственный пользовательский Typst import | `.template/lib/index.typ` |
|
||||
| Document Domain | Профиль, context и render options | `document-profile()`, `render-options()` |
|
||||
| Company Domain | Реквизиты и firm resources | `company-profile()` |
|
||||
| Parties Domain | Адресаты, подписанты и стороны | `recipient()`, `signer()`, `party()` |
|
||||
| Attachments Domain | Порядок и идентичность приложений | `attachment()`, `attachment-set()` |
|
||||
| Render Application | Resolve, normalize, validate, render | `render-document()` |
|
||||
| Company Adapter | JSON profiles и resource overrides | `load-company()` |
|
||||
| Presentation | Foundation, components и четыре renderer | `profiles.report/letter/commercial_offer/contract` |
|
||||
| Employee/Private Adapter | Справочник сотрудников, private media и offsets | `report-executor()`, `private-company-media()` |
|
||||
| Test Harness | Static, compile, semantic и visual gates | `run-tests.py` |
|
||||
|
||||
## Data flow
|
||||
|
||||
### Обычная сборка
|
||||
|
||||
1. Автор выбирает `company-id` и `document-mode` в `main.typ`.
|
||||
2. `main.typ` создаёт profile, company overrides, bibliography и attachments.
|
||||
3. `#show: document.with(...)` передаёт последующие `#include` как тело документа.
|
||||
4. Facade вызывает application use case.
|
||||
5. Application разрешает компанию, нормализует и валидирует profile metadata.
|
||||
6. Renderer применяет foundation, нумерацию, media policy и компонует страницы.
|
||||
7. При `none` для подписи или печати final renderer использует круг или крест.
|
||||
8. Typst создаёт `document.pdf`.
|
||||
|
||||
### Выбор типа документа
|
||||
|
||||
1. Задача VS Code получает `report`, `letter`, `commercial-offer` или `contract`.
|
||||
2. Tool сохраняет текущие `main.typ` и `chapters/` в `.private/starter-backups/<timestamp>/`.
|
||||
3. Tool копирует соответствующий публичный пример из `docs/examples/documents/`.
|
||||
4. Автор проверяет явно перечисленные параметры нового `main.typ`.
|
||||
|
||||
### Приватная сборка
|
||||
|
||||
1. Пользователь копирует готовую папку `.private` с `settings.typ` и PNG.
|
||||
2. В `main.typ` значение `use-private-assets` меняется с `false` на `true`.
|
||||
3. Условный import загружает `.private/settings.typ`.
|
||||
4. Adapter сопоставляет публичный идентификатор сотрудника с фиксированным именем PNG и применяет private offset.
|
||||
5. Отсутствующая или отключённая запись возвращает `none`; строка подписи остаётся пустой.
|
||||
6. Обычная build task и Tinymist preview используют один и тот же `main.typ`.
|
||||
|
||||
## Ключевые интерфейсы
|
||||
|
||||
```typst
|
||||
// Единственный пользовательский вход.
|
||||
#let company-id = "scientia" // scientia | technology | too
|
||||
#let document-mode = "final" // final | draft | clean-copy
|
||||
#let use-private-assets = false // true, если скопирована .private
|
||||
```
|
||||
|
||||
```typst
|
||||
// Порядок и состав глав видны внизу main.typ.
|
||||
#include "chapters/00-introduction.typ"
|
||||
#pagebreak()
|
||||
#include "chapters/10-main.typ"
|
||||
```
|
||||
|
||||
```typst
|
||||
// Отсутствующая .private не читается при false.
|
||||
#let private-settings = if use-private-assets {
|
||||
import "/.private/settings.typ": settings
|
||||
settings
|
||||
} else {
|
||||
empty-private-settings
|
||||
}
|
||||
```
|
||||
|
||||
```typst
|
||||
#show: document.with(
|
||||
company: company,
|
||||
profile: profiles.report(..),
|
||||
options: (
|
||||
mode: document-mode,
|
||||
watermark: if document-mode == "draft" { "ЧЕРНОВИК" } else { none },
|
||||
media-policy: if document-mode == "final" { "placeholder" } else { "reserve-space" },
|
||||
diagnostics: true,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## Публичная и developer-документация
|
||||
|
||||
| Слой | Расположение | Содержит | Не содержит |
|
||||
|------|--------------|----------|-------------|
|
||||
| Публичный | `README.md`, `docs/` | первый запуск, Git, типы, formatting, папку `.private`, troubleshooting | DDD, ADR, snapshots, migration internals |
|
||||
| Developer | `.template/development/docs/`, `modules/` | архитектуру, решения, границы и тестирование | обязательный маршрут обычного автора |
|
||||
|
||||
Корневой README обязан ссылаться на каждую публичную тему. Developer README доступен одной отдельной ссылкой и не конкурирует с пользовательской навигацией.
|
||||
|
||||
## Публичные примеры
|
||||
|
||||
| Пример | Обязательное покрытие |
|
||||
|--------|----------------------|
|
||||
| Report | титул, stage/volume, executors, includes, рисунки, таблица, formula, references, bibliography, appendix |
|
||||
| Letter | recipient, исходящий номер, основной текст, attachment list, signer |
|
||||
| Commercial offer | recipient, subject, price, tax, сроки, payment, scope appendix |
|
||||
| Contract | parties, representatives, sections, requisites, signing, appendix |
|
||||
| Formatting | варианты изображений, grid, простые/сложные таблицы, CSV, формулы, labels, lists |
|
||||
|
||||
Публичный пример не должен ссылаться на скрытый developer asset. Допустим только импорт фасада `/.template/lib/index.typ`.
|
||||
|
||||
## Политика приватных ресурсов
|
||||
|
||||
| Ресурс | Git | Поведение |
|
||||
|--------|-----|-----------|
|
||||
| Публичные реквизиты и логотипы | tracked | Загружаются из `.template/companies/` |
|
||||
| Векторные placeholders | tracked | Используются обычной сборкой по умолчанию |
|
||||
| `docs/examples/private/settings.typ` | tracked | Полный безопасный пример с `enabled: false` |
|
||||
| `.template/lib/infrastructure/employees.typ` | tracked | ФИО, обычные роли и фиксированные имена PNG |
|
||||
| `.private/` | ignored | Настройки доступности, offsets, реальные изображения и backups |
|
||||
|
||||
Typst 0.15 не предоставляет проверки существования файла. Поэтому отсутствие подписи моделируется отсутствующей записью или `enabled: false`; только включённая запись создаёт private path.
|
||||
|
||||
## Технологические решения
|
||||
|
||||
| Решение | Выбор | Обоснование |
|
||||
|---------|-------|-------------|
|
||||
| Compiler | Typst 0.15.1+ | Проверенный baseline и path type |
|
||||
| Архитектура | Модульный монолит | Один процесс сборки без лишней инфраструктуры |
|
||||
| Root entry | Один `main.typ` | Минимум выбора и все параметры в одном месте |
|
||||
| Режимы | Переменная `document-mode` | Варианты видны комментариями, нет дублирования файлов |
|
||||
| Public docs | Видимый `docs/` | Автор находит примеры в Explorer |
|
||||
| Examples | Executable documentation | Код и объяснение не расходятся |
|
||||
| Private activation | Literal `use-private-assets` + conditional import | Один понятный параметр, clean fork не читает отсутствующий каталог |
|
||||
| Internal boundary | `.template/` | Реализация и developer docs не мешают автору |
|
||||
| Testing | Python + Typst + Poppler | Контракты, PDF semantic и визуальный layout |
|
||||
|
||||
## Режимы отказа
|
||||
|
||||
| Сбой | Влияние | Митигация |
|
||||
|------|---------|-----------|
|
||||
| Автор меняет режим не в `main.typ` | Ожидаемый вариант PDF не получается | README и comments показывают единственную переменную |
|
||||
| Пример использует скрытый developer asset | После очистки development сборка падает | Static path audit и compile всех public examples |
|
||||
| Выбор типа уничтожает текст | Потеря работы | Timestamp backup до удаления `chapters/` |
|
||||
| Подпись ещё не получена | Private compile падает при прямом path | Запись отсутствует или `enabled: false`, resolver возвращает `none` |
|
||||
| Включённая запись не имеет PNG | Ошибка Typst с точным path | Включать запись только после копирования PNG; troubleshooting |
|
||||
| Реальный файл попадает в Git | Утечка подписи | `.gitignore`, ignored `.private/`, no overwrite tracked placeholders |
|
||||
| Сложная таблица переполняет страницу | Нарушение layout | Formatting example, stress fixture, repeated header tests |
|
||||
| Публичная ссылка устарела | Автор теряет маршрут | Automated Markdown link audit |
|
||||
| Изменение Typst меняет layout | Тихая регрессия | Version gate и snapshot review |
|
||||
|
||||
## Вне области видимости v1
|
||||
|
||||
- Поддержка старых root-файлов `document.typ`, `draft.typ` и `clean-copy.typ`.
|
||||
- Автоматическое определение существования private files внутри Typst.
|
||||
- Хранение реальных подписей и печатей в Git или Git LFS.
|
||||
- Публикация внутренних VSIX в этом репозитории.
|
||||
- Юридическая экспертиза договора.
|
||||
- Научная верификация пользовательского содержания.
|
||||
- Генерация DOCX.
|
||||
- Публичная публикация в Typst Universe и open-source лицензирование.
|
||||
@@ -0,0 +1,136 @@
|
||||
# План редизайна пользовательской поверхности Scientia
|
||||
|
||||
## Обзор
|
||||
|
||||
| Фаза | Результат | Статус |
|
||||
|------|-----------|--------|
|
||||
| 1 | Зафиксирован baseline до редизайна | [x] |
|
||||
| 2 | В корне оставлен один настраиваемый `main.typ` | [x] |
|
||||
| 3 | Публичные инструкции и компилируемые примеры перенесены в `docs/` | [x] |
|
||||
| 4 | Выбор типа выполняется задачей VS Code, private media — одним параметром | [x] |
|
||||
| 5 | Полная регрессия и визуальная приёмка | [x] |
|
||||
|
||||
## План миграции
|
||||
|
||||
**Текущее до редизайна**: пользовательская справка находилась в скрытой `.template/help/`, конфигурация была разделена между `document.typ`, `main.typ`, `draft.typ` и `clean-copy.typ`, а учебные примеры лежали среди developer fixtures.
|
||||
|
||||
**Целевое состояние**: автор видит `main.typ`, `chapters/`, `assets/` и `docs/`. Режим задаётся одной переменной. Примеры четырёх типов документов и форматирования видимы, компилируемы и используются как заготовки. Приватная папка подключается одним параметром и содержит настройки индивидуальных подписей.
|
||||
|
||||
**Стратегия**: атомарное переключение пользовательского контракта без compatibility-файлов. Обратная совместимость не требуется.
|
||||
|
||||
| Фаза | Rollback |
|
||||
|------|----------|
|
||||
| 1 | Не требуется: только фиксация baseline |
|
||||
| 2 | Восстановить предыдущие четыре корневых файла одним change set |
|
||||
| 3 | Вернуть справку в `.template/help/`, не меняя библиотеку |
|
||||
| 4 | Отключить задачи и использовать обычную сборку с placeholders |
|
||||
| 5 | Откатить конкретную правку, повторить compile и visual suites |
|
||||
|
||||
---
|
||||
|
||||
## Фаза 1 — Зафиксировать baseline
|
||||
|
||||
**Цель**: доказать работоспособность библиотеки до изменения пользовательского контракта.
|
||||
**Результат**: unit, negative, company matrix, semantic и visual suites проходят.
|
||||
**Трудоёмкость**: S
|
||||
**Статус**: [x] Готово
|
||||
|
||||
### Задачи
|
||||
|
||||
- [x] Сохранить compile и visual baseline (→ [Модуль: Тестирование](../modules/testing.md))
|
||||
- [x] Зафиксировать Typst 0.15.1 (→ [ADR-0001](adr/0001-typst-015.md))
|
||||
|
||||
### Тесты
|
||||
|
||||
- [x] Unit: domain и numbering
|
||||
- [x] Интеграционный: четыре профиля и компании
|
||||
- [x] Визуальный: утверждённые snapshot pages
|
||||
|
||||
---
|
||||
|
||||
## Фаза 2 — Оставить один `main.typ`
|
||||
|
||||
**Цель**: сделать все ежедневные настройки и `#include` видимыми в одном файле.
|
||||
**Результат**: `document.typ`, `draft.typ` и `clean-copy.typ` отсутствуют; режим выбирается в `main.typ`.
|
||||
**Трудоёмкость**: M
|
||||
**Статус**: [x] Готово
|
||||
|
||||
### Задачи
|
||||
|
||||
- [x] Перенести компанию, режим, metadata и порядок глав в `main.typ` (→ [Рабочее пространство автора](../modules/author-workspace.md))
|
||||
- [x] Сохранить единый фасад импорта (→ [Публичный фасад](../modules/facade.md))
|
||||
- [x] Проверить profile/options contract (→ [Документ](../modules/domain-document.md))
|
||||
- [x] Сохранить границы организаций, сторон и приложений (→ [Организация](../modules/domain-company.md), [Стороны](../modules/domain-parties.md), [Приложения](../modules/domain-attachments.md))
|
||||
- [x] Проверить application flow и загрузку компаний (→ [Сборка](../modules/application-render.md), [Ресурсы компаний](../modules/infrastructure-assets.md))
|
||||
|
||||
### Тесты
|
||||
|
||||
- [x] Static: единственный root entrypoint
|
||||
- [x] Интеграционный: `final`, `draft`, `clean-copy` через profile fixtures
|
||||
- [x] Ручной: порядок глав меняется только списком `#include`
|
||||
|
||||
---
|
||||
|
||||
## Фаза 3 — Открыть документацию и примеры
|
||||
|
||||
**Цель**: дать автору видимую справку и копируемые примеры без чтения реализации.
|
||||
**Результат**: `docs/` содержит навигацию, четыре полных документа и каталог оформления.
|
||||
**Трудоёмкость**: L
|
||||
**Статус**: [x] Готово
|
||||
|
||||
### Задачи
|
||||
|
||||
- [x] Разделить публичную и developer-документацию (→ [Публичная документация](../modules/user-documentation.md))
|
||||
- [x] Сделать примеры источником выбора типа документа (→ [Публичные примеры](../modules/starter-packs.md))
|
||||
- [x] Показать таблицы, формулы, подписи и media blocks (→ [Компоненты](../modules/components.md), [Основа вёрстки](../modules/presentation-foundation.md))
|
||||
- [x] Показать ссылки, bibliography и numbering (→ [Ссылки](../modules/references.md), [Нумерация](../modules/numbering.md))
|
||||
- [x] Подготовить адекватные примеры профилей (→ [Отчёт](../modules/presentation-report.md), [Письмо](../modules/presentation-letter.md), [ТКП](../modules/presentation-commercial-offer.md), [Договор](../modules/presentation-contract.md))
|
||||
|
||||
### Тесты
|
||||
|
||||
- [x] Интеграционный: пять публичных примеров компилируются
|
||||
- [x] Semantic: ожидаемые подписи, ссылки, приложения и реквизиты присутствуют
|
||||
- [x] Ручной: весь `docs/` доступен из корневого README
|
||||
|
||||
---
|
||||
|
||||
## Фаза 4 — Упростить VS Code и приватные данные
|
||||
|
||||
**Цель**: оставить одну build task и безопасно подключать папку `.private` одним параметром.
|
||||
**Результат**: выбор типа создаёт backup, обычная сборка работает с private media и без неё, отсутствующая подпись не ломает документ.
|
||||
**Трудоёмкость**: M
|
||||
**Статус**: [x] Готово
|
||||
|
||||
### Задачи
|
||||
|
||||
- [x] Оставить в Typewriter единственный `main.typ` и обновить задачи (→ [Рабочая область VS Code](../modules/vscode-workspace.md))
|
||||
- [x] Добавить публичный справочник сотрудников, фиксированные PNG names и private offsets (→ [Приватные ресурсы](../modules/private-assets.md))
|
||||
- [x] Использовать conditional import при явном `use-private-assets` (→ [ADR-0010](adr/0010-private-folder-and-employees.md))
|
||||
|
||||
### Тесты
|
||||
|
||||
- [x] Static: `.vscode/` синхронизируется, `docs/` не скрыт
|
||||
- [x] Интеграционный: clean fork компилируется без `.private`
|
||||
- [x] Интеграционный: private compile отображает реальные подписи и offsets
|
||||
- [x] Интеграционный: `enabled: false` оставляет пустую строку без ошибки
|
||||
|
||||
---
|
||||
|
||||
## Фаза 5 — Hardening и выпуск
|
||||
|
||||
**Цель**: подтвердить отсутствие мёртвых путей, утечек и визуальных дефектов.
|
||||
**Результат**: полный harness проходит, Markdown-ссылки валидны, приватные ресурсы игнорируются.
|
||||
**Трудоёмкость**: M
|
||||
**Статус**: [x] Готово
|
||||
|
||||
### Задачи
|
||||
|
||||
- [x] Обновить static workspace contract (→ [Тестирование](../modules/testing.md))
|
||||
- [x] Удалить скрытые дубликаты примеров и notes (→ [Публичная документация](../modules/user-documentation.md))
|
||||
- [x] Проверить визуально все страницы публичных примеров (→ [ADR-0006](adr/0006-visual-regression.md))
|
||||
|
||||
### Тесты
|
||||
|
||||
- [x] Полный automated harness
|
||||
- [x] Аудит Markdown links и legacy paths
|
||||
- [x] Визуальная проверка contact sheets и проблемных страниц
|
||||
@@ -0,0 +1,63 @@
|
||||
# Разработка шаблона Scientia
|
||||
|
||||
> Скрытая документация для сопровождающих Typst-библиотеки. Инструкции авторов находятся в публичном каталоге [`docs/`](../../../docs/README.md).
|
||||
|
||||
## Текущее устройство
|
||||
|
||||
Автор работает с единственным `main.typ`, каталогами `chapters/`, `assets/` и видимой документацией `docs/`. Внутренняя библиотека, профили организаций, тесты и архитектурные решения находятся в `.template/`.
|
||||
|
||||
Режим `final`, `draft` или `clean-copy` выбирается переменной внутри `main.typ`. Компилируемые публичные примеры четырёх видов документов и элементов оформления одновременно являются источниками для задачи выбора типа документа.
|
||||
|
||||
Обычная сборка использует векторные заглушки. Готовая папка `.private` подключается одним literal-переключателем в `main.typ`; отдельного ZIP installer и отдельной build task нет.
|
||||
|
||||
## Команды сопровождающего
|
||||
|
||||
```powershell
|
||||
# Пользовательский документ
|
||||
typst compile --root . main.typ document.pdf
|
||||
|
||||
# Полная регрессия
|
||||
python .template/development/tests/run-tests.py
|
||||
```
|
||||
|
||||
## Навигация
|
||||
|
||||
| Документ | Назначение |
|
||||
|----------|------------|
|
||||
| [PLAN.md](PLAN.md) | Завершённые фазы редизайна и проверки |
|
||||
| [ARCHITECTURE.md](ARCHITECTURE.md) | Структура, компоненты, data flow и режимы отказа |
|
||||
| [ADR-0001](adr/0001-typst-015.md) | Typst 0.15.1 как baseline |
|
||||
| [ADR-0002](adr/0002-data-and-assets.md) | Границы данных и ресурсов |
|
||||
| [ADR-0003](adr/0003-rendering-model.md) | Детерминированная модель выполнения |
|
||||
| [ADR-0004](adr/0004-error-handling.md) | Ошибки и media fallback |
|
||||
| [ADR-0005](adr/0005-ddd-boundaries.md) | Направление зависимостей |
|
||||
| [ADR-0006](adr/0006-visual-regression.md) | Визуальная регрессия |
|
||||
| [ADR-0007, устарел](adr/0007-author-workspace.md) | Историческое решение о трёх entrypoints |
|
||||
| [ADR-0008](adr/0008-vscode-onboarding.md) | Версионируемая рабочая область VS Code |
|
||||
| [ADR-0009](adr/0009-single-main-and-public-docs.md) | Один `main.typ`, публичные примеры и приватная сборка |
|
||||
| [ADR-0010](adr/0010-private-folder-and-employees.md) | Папка `.private`, справочник сотрудников и частичные подписи |
|
||||
| [Рабочее пространство автора](../modules/author-workspace.md) | Минимальный корень и один входной файл |
|
||||
| [Публичные примеры](../modules/starter-packs.md) | Четыре вида документов и каталог оформления |
|
||||
| [Публичная документация](../modules/user-documentation.md) | Видимый `docs/` для авторов |
|
||||
| [Рабочая область VS Code](../modules/vscode-workspace.md) | Расширения, задачи и настройки |
|
||||
| [Приватные ресурсы](../modules/private-assets.md) | Справочник сотрудников, `.private/settings.typ`, offsets и placeholders |
|
||||
| [Публичный фасад](../modules/facade.md) | Стабильный Typst API |
|
||||
| [Документ](../modules/domain-document.md) | Профили и режимы выпуска |
|
||||
| [Организация](../modules/domain-company.md) | Реквизиты и ресурсы компании |
|
||||
| [Стороны](../modules/domain-parties.md) | Адресаты, подписанты и стороны |
|
||||
| [Приложения](../modules/domain-attachments.md) | Приложения разных видов документов |
|
||||
| [Сборка](../modules/application-render.md) | Application orchestration |
|
||||
| [Ресурсы компаний](../modules/infrastructure-assets.md) | Загрузка публичных профилей и overrides |
|
||||
| [Основа вёрстки](../modules/presentation-foundation.md) | Tokens и media policy |
|
||||
| [Отчёт](../modules/presentation-report.md) | Renderer отчёта |
|
||||
| [Письмо](../modules/presentation-letter.md) | Renderer письма |
|
||||
| [ТКП](../modules/presentation-commercial-offer.md) | Renderer предложения |
|
||||
| [Договор](../modules/presentation-contract.md) | Renderer договора |
|
||||
| [Компоненты](../modules/components.md) | Таблицы, формулы и подписи |
|
||||
| [Ссылки](../modules/references.md) | Перекрёстные ссылки и библиография |
|
||||
| [Нумерация](../modules/numbering.md) | Стратегии нумерации |
|
||||
| [Тестирование](../modules/testing.md) | Static, compile, semantic и visual suites |
|
||||
|
||||
## Статус
|
||||
|
||||
Редизайн принят. Корневой и публичные примеры компилируются на Typst 0.15.1; полная тестовая матрица является release gate.
|
||||
@@ -0,0 +1,30 @@
|
||||
# ADR-0001: Typst 0.15.1 как минимальная платформа
|
||||
|
||||
**Дата**: 2026-08-26
|
||||
**Статус**: Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
Шаблон уже реализован на Typst и использует его counters, introspection, show rules и PDF-рендер. После обновления окружения доступен Typst 0.15.1, который добавляет несколько bibliographies, тип `path` для передачи project-relative ресурсов и более подробные diagnostics. Одновременно 0.15 меняет baseline некоторых layout-элементов, поэтому обновление должно сопровождаться визуальным аудитом.
|
||||
|
||||
Официальные основания: [changelog Typst 0.15.0](https://typst.app/docs/changelog/0.15.0/), [bibliography](https://typst.app/docs/reference/model/bibliography/), [path](https://typst.app/docs/reference/foundations/path/).
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Остаться на Typst 0.14.2** — меньше миграционных рисков сейчас, но нет нативных нескольких bibliographies и нового `path`.
|
||||
2. **Принять Typst 0.15.1+** — доступны нужные возможности, но требуется новый baseline и контроль будущих обновлений.
|
||||
3. **Перейти на LaTeX или Word** — большая экосистема, но фактически требует переписать проверенную вёрстку и усложняет программируемые компоненты.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбрали **Typst 0.15.1 как минимальную поддерживаемую версию**, потому что она уже установлена, поддерживает несколько bibliographies и даёт корректную модель передачи путей между пользовательским проектом и библиотекой.
|
||||
|
||||
Каждое обновление Typst выполняется отдельным изменением: сначала полная компиляционная и визуальная матрица, затем принятие новых snapshots.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: тематические и поглавные библиографии, автономное подключение шаблона, диагностика layout convergence.
|
||||
|
||||
**Становится сложнее**: необходимо контролировать версию CLI и визуальные изменения baseline.
|
||||
|
||||
**Закрывает дверь на**: поддержку Typst 0.14 и более ранних версий без отдельной compatibility-ветки.
|
||||
@@ -0,0 +1,37 @@
|
||||
# ADR-0002: Публичные, пользовательские и приватные ресурсы
|
||||
|
||||
**Дата**: 2026-08-31
|
||||
**Статус**: Принято
|
||||
|
||||
> Способ доставки через ZIP заменён папкой `.private` в [ADR-0010](0010-private-folder-and-employees.md). Граница публичных и приватных данных остаётся действующей.
|
||||
|
||||
## Контекст
|
||||
|
||||
Внутренний шаблон Scientia содержит фирменные реквизиты, логотипы, имена, подписи, печати и материалы конкретного документа. Логотипы, адреса, имена и реквизиты разрешено распространять внутри компании. Реальные подписи и печати нельзя хранить в Git вместе с шаблоном.
|
||||
|
||||
Typst не читает ZIP напрямую и не умеет проверить наличие изображения без попытки его загрузить. Поэтому приватный архив должен быть внешним каналом доставки, а отсутствие ресурса должно моделироваться значением `none`.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Оставить всё в одном tracked-каталоге** — максимально просто, но подписи и печати неизбежно распространяются с каждым форком.
|
||||
2. **Хранить приватные изображения в Git LFS** — уменьшает основной репозиторий, но не устраняет доступ и историю распространения.
|
||||
3. **Хранить подписи и печати в отдельном ZIP** — требует извлечения, зато отделяет приватный канал от шаблона.
|
||||
4. **Не поддерживать реальные изображения вообще** — безопасно, но не покрывает подготовку финальных документов.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбрали **три класса ресурсов**:
|
||||
|
||||
- публичные фирменные данные и логотипы хранятся в `.template/companies/`;
|
||||
- материалы конкретного документа хранятся в `assets/`;
|
||||
- реальные подписи и печати поставляются отдельным `private-assets.zip`; проверенная задача извлекает их в `.private/` и включает через `sys.inputs` только на время приватной сборки `main.typ`.
|
||||
|
||||
`.private/` и `private-assets*.zip` исключаются через `.gitignore`. В репозитории остаются только нейтральные placeholders: векторный круг для печати и крест для подписи. При значении ресурса `none` renderer использует placeholder; указанный путь обязан существовать.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: безопасно форкать шаблон, централизованно обновлять публичные реквизиты и собирать документ без приватного архива.
|
||||
|
||||
**Становится сложнее**: для финального подписанного PDF нужно получить ZIP, извлечь его и явно указать пути.
|
||||
|
||||
**Закрывает дверь на**: хранение настоящих подписей и печатей в обычном Git, Git LFS или visual snapshots.
|
||||
@@ -0,0 +1,28 @@
|
||||
# ADR-0003: Синхронная детерминированная модель сборки
|
||||
|
||||
**Дата**: 2026-08-26
|
||||
**Статус**: Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
Сборка документа выполняется локальным Typst compiler: данные читаются из файлов проекта, затем происходит несколько внутренних итераций layout и создаётся PDF. Внешних сетевых сервисов, конкурентной записи или длительных независимых операций в v1 нет.
|
||||
|
||||
Добавление собственной async-модели не ускорит Typst-layout, но усложнит диагностику, воспроизводимость и тестирование.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Синхронная сборка одного документа** — простая, воспроизводимая и соответствует модели Typst.
|
||||
2. **Параллельные renderer-профили внутри Typst** — не поддерживаются как управляемая модель и не дают изоляции layout-state.
|
||||
3. **Внешний асинхронный build-сервис** — полезен для массовой генерации, но избыточен для локального шаблона.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбрали **синхронную детерминированную сборку одного документа**. Параллельный запуск нескольких независимых fixtures допускается только во внешнем test harness, где каждый процесс получает собственный entry point и output.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: воспроизводимость, порядок diagnostics, изоляция `state` и расследование visual regressions.
|
||||
|
||||
**Становится сложнее**: массовая генерация большого набора документов должна оркестрироваться внешним скриптом.
|
||||
|
||||
**Закрывает дверь на**: сетевые и фоновые операции непосредственно внутри шаблона v1.
|
||||
@@ -0,0 +1,36 @@
|
||||
# ADR-0004: Ранняя валидация и явные fallback-политики
|
||||
|
||||
**Дата**: 2026-08-26
|
||||
**Статус**: Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
Ошибки Typst часто проявляются во время layout далеко от места, где пользователь передал неверное значение. Для бизнес-документа особенно опасны тихие fallback: неверная компания, отсутствующая сторона, незаметно пропавшая подпись или citation, не попавшая в список источников.
|
||||
|
||||
При этом распространяемый шаблон должен компилироваться без реальных подписей и печатей. Они поставляются отдельным ZIP; PowerShell task проверяет и извлекает архив, после чего запускает `main.typ` с явным `sys.inputs`. Typst 0.15 не предоставляет проверки существования файла без попытки загрузки.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Полагаться только на diagnostics Typst** — мало кода, но сообщения не отражают доменный путь поля.
|
||||
2. **Всегда аварийно завершаться при любом отсутствующем ресурсе** — строго, но шаблон нельзя удобно распространять без подписей.
|
||||
3. **Валидировать domain до layout и явно моделировать необязательные ресурсы** — больше контрактов, зато ошибки предсказуемы.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбрали **раннюю profile-specific валидацию**. Каждая ошибка называет профиль, путь поля, фактическое значение и ожидаемое ограничение.
|
||||
|
||||
Для подписи, печати и необязательных изображений поддерживаются политики:
|
||||
|
||||
- `hide` — не показывать ресурс и не резервировать место;
|
||||
- `placeholder` — показать безопасную графическую заглушку: круг для печати или крест для подписи;
|
||||
- `reserve-space` — оставить место для ручной подписи или печати.
|
||||
|
||||
Политика применяется только если поле равно `none`. Если поле содержит путь, но файл отсутствует, сборка завершается ошибкой.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: распространение шаблона, поиск причины ошибки и тестирование негативных сценариев.
|
||||
|
||||
**Становится сложнее**: каждый профиль обязан определить required/optional поля и defaults.
|
||||
|
||||
**Закрывает дверь на**: молчаливое игнорирование неверно указанного пути к производственному ресурсу.
|
||||
@@ -0,0 +1,37 @@
|
||||
# ADR-0005: DDD-границы внутри модульного Typst-монолита
|
||||
|
||||
**Дата**: 2026-08-26
|
||||
**Статус**: Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
Шаблон должен расширяться новыми видами документов, но обычное использование должно оставаться простым. Один монолитный `show` с ветвлением по типу документа быстро свяжет корпоративные данные, file paths, domain-правила и пагинацию. Полноценные микросервисы или отдельные пакеты для каждого bounded context, напротив, избыточны для локальной Typst-библиотеки.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Одна функция с `if kind == ...`** — минимальный старт, но любое расширение меняет общее ядро и повышает риск регрессии.
|
||||
2. **Модульный монолит с DDD-границами и profile contract** — изоляция без инфраструктурной сложности.
|
||||
3. **Отдельный Typst package для каждого вида документа** — сильная физическая изоляция, но дублирование foundation и сложное совместное версионирование.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбрали **модульный монолит** со слоями Domain → Application и адаптерами Infrastructure/Presentation. Domain не импортирует presentation или infrastructure. Новый вид документа добавляется новым `DocumentProfile`, а не новой веткой в `document()`.
|
||||
|
||||
Разрешённое направление зависимостей:
|
||||
|
||||
```text
|
||||
Facade → Application → Domain
|
||||
│ ▲
|
||||
├→ Infrastructure
|
||||
└→ Presentation → Shared Components
|
||||
```
|
||||
|
||||
Presentation и Infrastructure могут создавать domain-значения или читать их, но не изменяют domain-инварианты.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: добавление договора или другого профиля, независимые fixtures и локализация `show/state`.
|
||||
|
||||
**Становится сложнее**: необходимо поддерживать явные contracts и проверять import graph.
|
||||
|
||||
**Закрывает дверь на**: доступ domain-модулей к JSON, `image`, `page`, `context` и глобальным renderer-state.
|
||||
@@ -0,0 +1,32 @@
|
||||
# ADR-0006: Трёхуровневая стратегия регрессионного тестирования
|
||||
|
||||
**Дата**: 2026-08-26
|
||||
**Статус**: Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
Шаблон содержит хрупкую пагинацию, сложные таблицы, подписи, формулы, кириллическую нумерацию и show rules. Успешная компиляция не обнаруживает тихий перенос строки, наложение печати или изменение количества страниц. Чистый pixel-perfect diff, в свою очередь, слишком чувствителен к версии renderer и системным шрифтам.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Проверять только exit code компилятора** — быстро, но не защищает макет.
|
||||
2. **Использовать только pixel-perfect snapshots** — ловит всё, но создаёт шум при допустимых изменениях окружения.
|
||||
3. **Совместить unit, semantic и visual проверки** — больше инфраструктуры, зато дефекты классифицируются точнее.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбрали **три уровня тестов**:
|
||||
|
||||
1. Domain unit tests через `typst eval` и `assert`.
|
||||
2. Compile/semantic tests: exit code, diagnostics, A4, количество страниц, наличие обязательных текстовых маркеров и PDF metadata.
|
||||
3. Visual regression: rasterize через Poppler, сравнивать контрольные области и полный perceptual diff с документированным порогом.
|
||||
|
||||
Snapshots создаются только из синтетического `test-company`; реальные подписи и печати не включаются в публичные тестовые изображения. Новая версия Typst всегда проверяется отдельным прогоном до обновления snapshots.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: находить как логические, так и визуальные регрессии и безопасно менять отдельные profiles.
|
||||
|
||||
**Становится сложнее**: требуется Python/Poppler test runtime и процедура осознанного обновления эталонов.
|
||||
|
||||
**Закрывает дверь на**: автоматическое принятие новых snapshots при обычном тестовом запуске.
|
||||
@@ -0,0 +1,39 @@
|
||||
# ADR-0007: Минимальный корень и три режима одного документа
|
||||
|
||||
**Дата**: 2026-08-31
|
||||
**Статус**: Устарело — заменено [ADR-0009](0009-single-main-and-public-docs.md)
|
||||
|
||||
## Контекст
|
||||
|
||||
> Этот ADR сохраняется как история промежуточного решения. Три entrypoint-файла были удалены после проверки на реальных отчётах: пользователю удобнее выбирать режим в одном `main.typ`.
|
||||
|
||||
В текущем корне служебные каталоги конкурируют с `chapters/` и `assets/`, а `main.typ` смешивает демонстрационные данные, настройку и сборку. Автору после форка нужен короткий маршрут без изучения DDD-слоёв, тестов и примеров.
|
||||
|
||||
Одновременно должны поддерживаться три выпуска одного содержания и четыре вида документов. Обратная совместимость со старыми путями не требуется.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Оставить служебные каталоги в корне** — удобно разработчику, но перегружает основной сценарий автора.
|
||||
2. **Удалить тесты и документацию** — очищает корень, но делает шаблон хрупким и плохо сопровождаемым.
|
||||
3. **Перенести внутреннее устройство в `.template/`** — сохраняет разработку и визуально отделяет её от пользовательских файлов.
|
||||
4. **Создать отдельный entrypoint для каждого типа и режима** — явно, но приводит минимум к двенадцати корневым файлам и дублированию конфигурации.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбрали **один скрытый каталог `.template/`**, общий `document.typ` и три корневые точки входа:
|
||||
|
||||
- `main.typ` передаёт `mode: "final"`;
|
||||
- `draft.typ` передаёт `mode: "draft"`;
|
||||
- `clean-copy.typ` передаёт `mode: "clean-copy"`.
|
||||
|
||||
Все entrypoints импортируют `render(mode:)` из `document.typ`. Report, letter, commercial-offer и contract starters реализуют одинаковый контракт, поэтому выбор типа документа не меняет entrypoints.
|
||||
|
||||
В starter явно записываются все семантически важные параметры, включая осознанные `none`, `false` и `()`. Низкоуровневые параметры layout остаются внутри библиотеки.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: первый fork, переключение режима, выбор starter и обновление внутренней реализации.
|
||||
|
||||
**Становится сложнее**: `document.typ` является обязательным стабильным контрактом, а каждый starter должен проходить contract-tests.
|
||||
|
||||
**Закрывает дверь на**: compatibility-файлы в корне и отдельные копии полной конфигурации для каждого режима.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ADR-0008: Версионируемая рабочая область VS Code и обучение автора
|
||||
|
||||
**Дата**: 2026-08-31
|
||||
**Статус**: Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
Основные пользователи шаблона пишут документы, но могут никогда не работать с кодом, Git и Typst. Устные инструкции и личные настройки редактора не воспроизводятся в новом форке. Одновременно каталог `.vscode/` не должен отвлекать автора от текста.
|
||||
|
||||
Часть расширений доступна в публичном Marketplace, а Typst Typewriter и Zotst распространяются внутри компании как локальные VSIX. Запись идентификатора расширения в рекомендациях VS Code не распространяет сам установочный пакет.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Не хранить настройки редактора** — корень формально проще, но каждый сотрудник вручную повторяет настройку и получает различное поведение.
|
||||
2. **Настроить всё глобально на рабочих станциях** — удобно на одном компьютере, но не переносится вместе с форком и требует администрирования.
|
||||
3. **Версионировать `.vscode/` и скрыть его в проводнике** — настройки синхронизируются через Git, оставаясь вне повседневной области автора.
|
||||
4. **Положить локальные VSIX в шаблон** — обеспечивает автономную установку, но смешивает бинарные пакеты с исходниками и затрудняет централизованное обновление.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбран вариант **версионировать `.vscode/`, но скрывать его из Explorer**:
|
||||
|
||||
- `extensions.json` содержит десять согласованных идентификаторов, включая два внутренних;
|
||||
- `settings.json` включает автосохранение, языки проверки орфографии, TODO-маркеры и защитные настройки Git;
|
||||
- `tasks.json` предоставляет обычную и приватную сборку, выбор одного из четырёх публичных примеров и компиляцию учебного каталога;
|
||||
- внутренние VSIX хранятся в корпоративном хранилище, а README объясняет их установку;
|
||||
- README и видимый `docs/` обучают Git в терминах истории документа, контрольных точек и параллельных версий.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: первый запуск, одинаковая среда во всех форках, живой предпросмотр, проверка русского текста и совместная работа через Git.
|
||||
|
||||
**Становится сложнее**: изменения `.vscode/` требуют такого же review, как изменения шаблона; сопровождающий должен отдельно публиковать совместимые VSIX.
|
||||
|
||||
**Закрывает дверь на**: неявные обязательные глобальные настройки и распространение внутренних бинарных расширений внутри Git-шаблона.
|
||||
@@ -0,0 +1,35 @@
|
||||
# ADR-0009: Один main.typ, видимая документация и явная приватная сборка
|
||||
|
||||
**Дата**: 2026-08-31
|
||||
**Статус**: Принято
|
||||
|
||||
> Часть решения об отдельной private build task заменена условным import из [ADR-0010](0010-private-folder-and-employees.md). Один `main.typ` и видимая публичная документация остаются действующими.
|
||||
|
||||
## Контекст
|
||||
|
||||
Проверка шаблона на реальных отчётах показала, что авторы ожидают менять компанию, стадию, режим и список глав в одном файле. Три корневых entrypoint-файла и скрытая пользовательская справка создавали лишний выбор. Одновременно Typst 0.15 не умеет безопасно проверять существование private path.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Сохранить `document.typ` и три entrypoints** — технически чисто, но пользователь должен понимать разделение ролей четырёх файлов.
|
||||
2. **Один `main.typ` и tracked private placeholders с заменой** — просто, но реальная подпись становится изменением уже отслеживаемого файла.
|
||||
3. **Один `main.typ`, public `docs/`, internal placeholders и private task** — минимальная поверхность без риска добавить реальный media в Git.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбран третий вариант:
|
||||
|
||||
- `main.typ` содержит `company-id`, `document-mode`, metadata и `#include`;
|
||||
- `docs/` видим и содержит executable examples;
|
||||
- examples одновременно являются источниками задачи выбора типа;
|
||||
- обычная сборка использует internal placeholders;
|
||||
- задача приватной сборки проверяет `private-assets.zip`, копирует PNG в `.private/` и передаёт `--input private-assets=true`;
|
||||
- прежний ADR-0007 считается устаревшим.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: первый запуск, переключение режима, поиск примеров, выбор типа и выпуск с приватными изображениями.
|
||||
|
||||
**Становится сложнее**: main-файлы четырёх примеров частично повторяют setup-код; PowerShell tool становится security boundary для ZIP.
|
||||
|
||||
**Закрывает дверь на**: отдельные root entrypoints для режимов, скрытую public-документацию и замену tracked placeholders реальными файлами.
|
||||
@@ -0,0 +1,26 @@
|
||||
# ADR-0010: Папка `.private`, справочник сотрудников и частичные подписи
|
||||
|
||||
**Дата**: 2026-08-31
|
||||
**Статус**: Принято
|
||||
|
||||
## Контекст
|
||||
|
||||
ZIP installer и отдельная build task скрывали второй переключатель private media от автора и не поддерживали реальные фамильные имена PNG. В проектах подписи поступают постепенно, а для каждого изображения уже подобрано индивидуальное вертикальное смещение. Чистый fork при этом обязан собираться без приватного каталога.
|
||||
|
||||
## Рассматриваемые варианты
|
||||
|
||||
1. **Оставить ZIP и `sys.inputs`** — безопасно для clean fork, но preview и обычная task не совпадают, а схема имён жёстко привязана к ролям.
|
||||
2. **Автоматически сканировать `.private`** — желаемый UX, но Typst 0.15 не предоставляет file-exists и падает при попытке открыть отсутствующий PNG.
|
||||
3. **Условный import и явная карта доступности** — один параметр в `main.typ`, отсутствие записи означает пустое место, offsets живут рядом с приватными PNG.
|
||||
|
||||
## Решение
|
||||
|
||||
Выбран вариант 3. Публичный tracked-справочник хранит идентификатор, ФИО, обычную должность и фиксированное имя PNG. Игнорируемый `.private/settings.typ` хранит доступность и offset. `main.typ` импортирует его только при literal `use-private-assets = true`. Все build/preview пути используют один entrypoint.
|
||||
|
||||
## Последствия
|
||||
|
||||
**Становится проще**: копировать `.private` целиком, менять роль сотрудника в одной строке, видеть реальные подписи в preview, работать при частично полученных PNG и публиковать clean fork с `false`.
|
||||
|
||||
**Становится сложнее**: при получении нового PNG нужно вручную включить запись; включённый, но отсутствующий файл по-прежнему вызывает точную ошибку Typst.
|
||||
|
||||
**Закрывает дверь на**: автоматическое определение файлов, role-based имена `responsible.png`, отдельную private build task и ZIP installer.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Модуль: Сборка документа
|
||||
|
||||
**Ответственность**: выполняет use case «собрать документ» в фиксированном порядке от пользовательских параметров до вызова renderer.
|
||||
**Расположение**: `.template/lib/application/render-document.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `render-document()` | функция | Оркестрирует resolve company → normalize → validate → create context → render |
|
||||
| `resolve-profile()` | функция | Проверяет и нормализует встроенный или пользовательский `DocumentProfile` |
|
||||
| `build-context()` | функция | Создаёт итоговый `DocumentContext` из валидированных частей |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/document.typ` | Контракт профиля, context и render options |
|
||||
| `.template/lib/domain/company.typ` | Валидацию CompanyProfile |
|
||||
| `.template/lib/domain/parties.typ` | Валидацию сторон и подписантов |
|
||||
| `.template/lib/domain/attachments.typ` | Валидацию приложений |
|
||||
| `.template/lib/infrastructure/company-assets.typ` | Загрузку компании только если передан строковый id |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Renderer никогда не вызывается до завершения всех validators.
|
||||
- Строковый `company` разрешается infrastructure-адаптером; готовый `CompanyProfile` повторно не загружается.
|
||||
- Порядок normalize и validate одинаков для всех профилей.
|
||||
- Application не содержит `if profile.id == "report"` или другой profile-specific логики.
|
||||
- `body` передаётся renderer без изменения пользовательского content.
|
||||
- Ошибка содержит stage: `resolve-company`, `normalize-profile`, `validate-domain` или `render`.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Page layout и show rules.
|
||||
- Чтение пользовательского `assets/`.
|
||||
- Юридическую проверку содержимого.
|
||||
- Запуск Typst compiler или запись PDF на диск.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Этот модуль должен оставаться коротким и скучным: его ценность — стабильный pipeline. Если новая возможность требует ветвления по профилю, добавьте её в profile contract или domain service. Infrastructure подключается как адаптер только для удобного строкового company id.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Модуль: Рабочее пространство автора
|
||||
|
||||
**Ответственность**: предоставляет один понятный входной файл и отделяет пользовательский текст от реализации.
|
||||
**Расположение**: `main.typ`, `chapters/`, `assets/`, `docs/`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Элемент | Тип | Описание |
|
||||
|---------|-----|----------|
|
||||
| `main.typ` | entrypoint | Компания, режим, metadata, ресурсы и порядок `#include` |
|
||||
| `document-mode` | строка | `final`, `draft` или `clean-copy` |
|
||||
| `chapters/` | каталог | Текстовые разделы документа |
|
||||
| `assets/` | каталог | Рисунки, CSV/TSV, bibliography и другие материалы |
|
||||
| `docs/` | каталог | Публичная справка и компилируемые примеры |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/index.typ` | Единственный Typst facade import |
|
||||
| `chapters/*.typ` | Явно перечисленное содержимое |
|
||||
| `assets/*` | Пользовательские paths |
|
||||
| `.private/settings.typ` | Только при явном `use-private-assets = true` |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- В корне существует только один Typst entrypoint `main.typ`.
|
||||
- Все ежедневные параметры и порядок глав видны в одном файле.
|
||||
- Режимы не представлены отдельными файлами.
|
||||
- `docs/` не скрыт настройками VS Code.
|
||||
- Автор не обязан открывать `.template/` для первого PDF.
|
||||
- Generated PDF и `.private/` не попадают в Git.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Layout renderer и profile validation.
|
||||
- Разработку нового вида документа.
|
||||
- Хранение настоящих подписей.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Корень является пользовательским интерфейсом. Новый обязательный параметр добавляйте в `main.typ` и public examples с вариантом в комментарии. Не создавайте дополнительный entrypoint ради режима сборки.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Модуль: Компоненты
|
||||
|
||||
**Ответственность**: содержит проверенные переиспользуемые визуальные примитивы, которые не принадлежат одному профилю документа.
|
||||
**Расположение**: `.template/lib/presentation/components.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `corp-table()` | функция | Корпоративная таблица со сложной шапкой, spans и продолжениями |
|
||||
| `formula()` | функция | Блочная формула, совместимая с профильной нумерацией |
|
||||
| `info-block()` | функция | Цветная информационная плашка для замечаний и пояснений |
|
||||
| `signature-block()` | функция | Один подписант с media policy |
|
||||
| `multi-party-signing()` | функция | Подписание несколькими сторонами |
|
||||
| `approval-block()` | функция | Блок утверждения отчёта |
|
||||
| `company-footer()` | функция | Контакты и реквизиты организации в footer |
|
||||
| `letter-header()` | функция | Общая геометрия исходящего письма и ТКП |
|
||||
| `attachment-list()` | функция | Перечень приложений к письму или ТКП |
|
||||
| `requisites-table()` | функция | Реквизиты одной или нескольких сторон |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/company.typ` | Нормализованные реквизиты и brand |
|
||||
| `.template/lib/domain/parties.typ` | Signer, Approval и Party |
|
||||
| `.template/lib/domain/attachments.typ` | AttachmentSet для перечней |
|
||||
| `.template/lib/presentation/foundation.typ` | Design tokens и `render-media-slot()` |
|
||||
| `.template/lib/shared/numbering.typ` | Общие numbering functions |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Компонент не читает JSON и не конструирует пользовательский path.
|
||||
- Компонент не определяет global page settings.
|
||||
- `corp-table` сохраняет поддержку auto/multirow headers, rowspan/colspan, repeat header и continuation label.
|
||||
- `corp-table.list-layout` локально задаёт геометрию списков в ячейках; `auto` сохраняет оформление документа, а параметры конкретного списка имеют приоритет.
|
||||
- `row_breakable: true` действительно передаётся в `table.cell(breakable:)`; режим проверяется fixture с одной строкой, которая продолжается на трёх страницах.
|
||||
- Центрирование блока `corp-table` не наследуется текстом ячеек: шапка по умолчанию центрирована, тело выровнено влево; явный `align` имеет приоритет.
|
||||
- Блок подписи сохраняет эталонную геометрию для обычных реквизитов и переключается на ограниченные равные боковые колонки при длинных наименованиях.
|
||||
- Письмо и ТКП используют один эталонный footer; на страницах приложений footer скрывается.
|
||||
- Обязательный positional `body` идёт первым в функциях, используемых через `.with`, если это требуется Typst.
|
||||
- Компонент принимает domain-значение и layout options отдельно.
|
||||
- Любая работа с изображением проходит через media slot или проверку `resource != none`.
|
||||
- Компоненты не импортируют profile renderers.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Полный жизненный цикл документа.
|
||||
- Profile-specific обязательность полей.
|
||||
- Выбор компании по id.
|
||||
- Сброс глобальных counters между главами.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> `corp_table` считается особо хрупким: не переписывайте рекурсивное определение header rows без unit и visual fixtures на rowspan/colspan. Общность компонента доказывается использованием хотя бы в двух профилях; иначе оставьте его внутри конкретного renderer. Layout offsets изображений должны быть параметрами компонента или design tokens, но не данными Signer.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Модуль: Приложения
|
||||
|
||||
**Ответственность**: задаёт идентичность, порядок, заголовки и нумерацию приложений независимо от профиля и конкретной вёрстки.
|
||||
**Расположение**: `.template/lib/domain/attachments.typ`
|
||||
|
||||
Модель используется письмами, ТКП и договорами. Отчётные приложения подключаются отдельными файлами через параметр `report.appendices`, потому что их заголовок и label должны находиться внутри авторского файла.
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `attachment()` | конструктор dictionary | Создаёт приложение с `id`, `title`, `subtitle`, `body`, `numbering` |
|
||||
| `attachment-set()` | конструктор dictionary | Нормализует массив приложений и общую политику нумерации |
|
||||
| `validate-attachments()` | функция | Проверяет уникальность ids, номеров и допустимость body |
|
||||
| `attachment-label()` | чистая функция | Формирует семантическое обозначение без layout |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/shared/numbering.typ` | Стратегии арабской и кириллической нумерации как чистые функции |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- `id` приложения уникален и пригоден для label.
|
||||
- Порядок массива является порядком документа, если пользователь явно не задал sort key.
|
||||
- Номер не хранится одновременно как вычисляемый и вручную заданный без явной override-policy.
|
||||
- `body` является content или функцией, которую renderer вызывает в локальном контексте.
|
||||
- Заголовок обязателен; subtitle необязателен.
|
||||
- Приложение не меняет global counter вне вызова своего renderer.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Размещение pagebreak и заголовка приложения.
|
||||
- Физическое объединение внешних PDF.
|
||||
- Подсчёт страниц вложения до компиляции.
|
||||
- Profile-specific текст «Приложение к договору».
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Старые `make_appendices` и `appendix-header` решают presentation-задачи и не переносятся в domain. Domain должен одинаково поддерживать кириллические приложения отчёта, цифровые приложения ТКП и именованные приложения договора. Renderer выбирает display policy на основе profile metadata.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Модуль: Организация
|
||||
|
||||
**Ответственность**: описывает единый нормализованный профиль юридического лица и его фирменных ресурсов.
|
||||
**Расположение**: `.template/lib/domain/company.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `company-profile()` | конструктор dictionary | Создаёт профиль из секций `legal`, `contacts`, `banking`, `brand`, `director`, `resources` |
|
||||
| `validate-company()` | функция | Проверяет обязательные реквизиты и типы необязательных полей |
|
||||
| `company-display-name()` | чистая функция | Возвращает полное или краткое наименование по политике профиля |
|
||||
| `company-resource()` | чистая функция | Возвращает нормализованное значение ресурса из уже построенного profile |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| Нет | Domain не читает JSON и не строит пути |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- `id`, полное наименование и юрисдикция заданы.
|
||||
- ИНН/КПП/ОГРН и БИН/КБЕ хранятся как строки, чтобы не терять ведущие нули и формат.
|
||||
- Контактные и банковские поля имеют единые ключи для всех компаний; неприменимое поле равно `none`.
|
||||
- Brand color нормализован до color до передачи renderer.
|
||||
- Логотип, подпись и печать имеют значение `none`, `path` или готовый content; произвольная строка после infrastructure-нормализации не допускается.
|
||||
- Director содержит должность и ФИО; ресурсы подписи и печати хранятся отдельно и не задают координаты на странице.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Поиск компании по содержимому документа.
|
||||
- Чтение `.template/companies/` и `.private/`.
|
||||
- Отрисовку логотипа, подписи, печати и реквизитов.
|
||||
- Валидацию законодательства конкретной юрисдикции.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Существующие JSON-файлы отличаются по набору полей. Нормализуйте их через infrastructure, не добавляйте условные ключи в renderer. Для РФ и Казахстана используйте общую структуру с `none` для неприменимых идентификаторов. Не переносите пользовательские изображения в CompanyProfile.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Модуль: Документ
|
||||
|
||||
**Ответственность**: владеет общей моделью документа, контрактом расширяемого профиля и режимами выпуска независимо от Typst-вёрстки.
|
||||
**Расположение**: `.template/lib/domain/document.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `document-context()` | конструктор dictionary | Создаёт полный неизменяемый контекст после нормализации и валидации |
|
||||
| `document-profile()` | конструктор dictionary | Создаёт контракт профиля с `id`, `metadata`, `normalize`, `validate`, `render` |
|
||||
| `render-options()` | конструктор dictionary | Нормализует режим выпуска, watermark и media policy |
|
||||
| `validate-profile-contract()` | функция | Проверяет, что расширение содержит все обязательные функции и поля |
|
||||
| `merge-known()` | чистая функция | Объединяет defaults с пользовательскими значениями по явным правилам |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| Нет | Модуль не импортирует infrastructure или presentation |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- `DocumentProfile.id` — непустая стабильная строка.
|
||||
- `normalize`, `validate` и `render` являются functions.
|
||||
- Режим выпуска принадлежит конечному набору `final`, `draft`, `clean-copy`.
|
||||
- `DocumentContext` создаётся только после успешной валидации company, profile metadata, parties и attachments.
|
||||
- Domain-значения не содержат вызовов `page`, `image`, `place`, `context`, `query` или глобального `state`.
|
||||
- Merge не принимает неизвестные ключи молча: профиль либо объявляет extension bucket, либо возвращает ошибку.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Загрузку компании из JSON.
|
||||
- Пагинацию и выбор шрифта.
|
||||
- Физическое наличие файлов.
|
||||
- Состав обязательных полей конкретного отчёта или договора.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> `DocumentProfile` — основной extension contract. Не превращайте его в строковый enum со switch в application. Renderer-function является портом presentation, переданным профилем. Для простого пользователя profile constructors скрывают этот контракт; вручную он нужен только разработчику нового вида документа.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Модуль: Стороны
|
||||
|
||||
**Ответственность**: моделирует адресатов, стороны договора, представителей, подписантов и блоки утверждения независимо от их размещения.
|
||||
**Расположение**: `.template/lib/domain/parties.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `party()` | конструктор dictionary | Создаёт организацию или физическое лицо в роли стороны документа |
|
||||
| `recipient()` | конструктор dictionary | Создаёт адресата делового письма или ТКП |
|
||||
| `signer()` | конструктор dictionary | Создаёт подписанта с должностью, ФИО, основанием и ресурсом подписи |
|
||||
| `approval()` | конструктор dictionary | Создаёт данные блока утверждения отчёта |
|
||||
| `validate-parties()` | функция | Проверяет уникальность ролей и профильные обязательные поля |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/company.typ` | Может принять нормализованный `CompanyProfile` как сторону, не загружая его |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Каждая сторона имеет уникальный `id` внутри документа и непустую `role`.
|
||||
- Recipient допускает отдельно организацию, должность и имя; пустые строки нормализуются в `none`.
|
||||
- Signer хранит семантические данные и ресурс, но не layout offsets.
|
||||
- Contract-party содержит реквизиты либо ссылку на `CompanyProfile`, но не оба источника с конфликтующими значениями.
|
||||
- Approval date и document date являются разными полями и не подменяют друг друга.
|
||||
- Обязательность подписи определяется профилем и режимом выпуска, а не самим `Signer`.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Координаты и размеры изображений подписи.
|
||||
- Текст преамбулы договора.
|
||||
- Склонение ФИО и должностей.
|
||||
- Загрузку реквизитов из файлов.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Письмо, ТКП и договор должны использовать одинаковые базовые Party/Signer, но разные validators. Не добавляйте коммерческие поля в recipient и report-specific approval в party. Если понадобится склонение, создайте отдельный domain service, а не набор условий внутри renderer.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Модуль: Публичный фасад
|
||||
|
||||
**Ответственность**: предоставляет `main.typ` и публичным примерам стабильный API, скрывая внутренние DDD-слои и файловую структуру.
|
||||
**Расположение**: `.template/lib/index.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `document()` | show-функция | Собирает документ из явно переданных `company`, `profile`, `options` и `body` |
|
||||
| `profiles` | module namespace | Конструкторы `report`, `letter`, `commercial_offer`, `contract` |
|
||||
| `components` | module namespace | Поддерживаемые визуальные компоненты для пользовательского content |
|
||||
| `references` | module namespace | `vref`, `vrefs`, `eqref` и bibliography helpers |
|
||||
| `load-company()` | функция | Загружает публичный профиль и применяет явные resource overrides |
|
||||
| `report-executor()` | функция | Разрешает сотрудника, роль, private PNG и offset |
|
||||
| `private-company-media()` | функция | Разрешает private подпись и печать организации |
|
||||
| `document-profile()` | конструктор dictionary | Extension contract для нового вида документа |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/application/render-document.typ` | Единственный application use case |
|
||||
| `.template/lib/presentation/profiles/*.typ` | Публичные profile constructors |
|
||||
| `.template/lib/presentation/components.typ` | Разрешённый пользовательский набор компонентов |
|
||||
| `.template/lib/presentation/references.typ` | Публичные helpers ссылок |
|
||||
| `.template/lib/infrastructure/company-assets.typ` | Загрузка профилей компаний |
|
||||
| `.template/lib/infrastructure/employees.typ` | Справочник сотрудников и private settings adapter |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- `main.typ` и public examples используют один import фасада и не знают внутренних путей.
|
||||
- В фасаде нет чтения пользовательских глав, page layout и profile-specific ветвлений.
|
||||
- Добавление нового профиля не меняет сигнатуру `document()`.
|
||||
- Resource override принимает явные `none`, `path` или content и не сканирует `.private/`.
|
||||
- В namespaces экспортируются только документированные символы.
|
||||
- В diagnostics используется бренд Scientia.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Включение пользовательских файлов из `chapters/`.
|
||||
- Создание пользовательских paths к `assets/`.
|
||||
- Выбор типа документа и режима выпуска.
|
||||
- Юридическую или смысловую проверку текста.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Фасад является границей между пользовательским `main.typ` и библиотекой. Если public example импортирует domain, infrastructure или конкретный renderer напрямую, фасад недостаточен. Обратная совместимость со старым корневым `template.typ` не требуется; не создавайте alias в корне.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Модуль: Ресурсы компаний
|
||||
|
||||
**Ответственность**: читает публичные данные `.template/companies/`, нормализует JSON, создаёт устойчивые Typst paths и применяет явные overrides подписи и печати.
|
||||
**Расположение**: `.template/lib/infrastructure/company-assets.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `load-company()` | функция | Загружает профиль по разрешённому id и принимает `logo`, `signature`, `stamp` overrides |
|
||||
| `available-companies()` | функция | Возвращает детерминированный список встроенных ids |
|
||||
| `merge-resources()` | чистая функция | Применяет только явно переданные resource overrides |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/company.typ` | `company-profile()` и `validate-company()` |
|
||||
| `.template/companies/*/data.json` | Публичные реквизиты организаций |
|
||||
| `.template/companies/*/logo.*` | Публичные логотипы |
|
||||
| `.template/lib/assets/placeholders/` | Безопасные круг и крест |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Адаптер не сканирует `assets/` или `.private/`.
|
||||
- Относительные внутренние paths создаются в infrastructure-файле.
|
||||
- Private paths организаций создаются адаптером сотрудников из явной настройки и передаются как готовые `path`.
|
||||
- Идентификатор компании выбирается из явного registry.
|
||||
- Публичные JSON не содержат путей к реальным подписям и печатям.
|
||||
- `none` сохраняется как отсутствие ресурса и обрабатывается media policy.
|
||||
- Указанный override не подменяется placeholder при ошибке загрузки.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Layout логотипа, подписи или печати.
|
||||
- Поиск файлов по имени.
|
||||
- Сетевую загрузку реквизитов.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Typst `path` сохраняет контекст файла, в котором создан. Public company paths создавайте здесь, а absolute `.private/` paths — только в private/employee adapter. Значения `auto` и `none` должны различаться: `auto` означает взять публичное значение профиля, `none` — осознанно применить media policy.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Модуль: Настраиваемые списки
|
||||
|
||||
**Ответственность**: формирует многоуровневые номера и маркеры, сохраняя нативную вёрстку `enum`.
|
||||
**Расположение**: `.template/lib/presentation/lists.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `bullet-list()` | function | Локально применяет геометрию к стандартному маркированному списку |
|
||||
| `numbered-list()` | function | Локально применяет схему и геометрию к стандартному нумерованному списку |
|
||||
| `list-scheme()` | constructor | Создаёт проверенную конфигурацию уровней, разделителей и окончаний |
|
||||
| `list-level()` | constructor | Описывает нестандартный уровень, включая prefix, suffix и width |
|
||||
| `list-numbering()` | function factory | Возвращает функцию нумерации для прямого использования в `enum` |
|
||||
| `list-schemes` | dictionary | Хранит публичные готовые схемы |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Модуль не заменяет `enum`: переносы страниц, вложенность и многоабзацные элементы остаются ответственностью Typst.
|
||||
- Внутренне `enum.full` всегда включён, чтобы форматтер знал глубину; показ родительских уровней определяет `scheme.full`.
|
||||
- Если массив levels, separators или suffixes короче глубины, повторяется его последнее значение.
|
||||
- Неизвестная именованная схема вызывает понятную ошибку и не подменяется схемой по умолчанию.
|
||||
- Режимы `normal`, `compact` и `flush` меняют только геометрию; `auto` наследует окружающий стиль.
|
||||
- Локальные параметры `bullet-list` и `numbered-list` не должны менять списки за пределами переданного body.
|
||||
|
||||
## Поддерживаемые обозначения
|
||||
|
||||
- арабские числа: `1`;
|
||||
- арабские числа с ведущим нулём: `01` или `list-level("1", width: N)`;
|
||||
- римские числа: `I`, `i`;
|
||||
- латинские буквы: `A`, `a`;
|
||||
- кириллица по ГОСТ: `А`, `а`;
|
||||
- любой строковый или content-маркер;
|
||||
- пользовательская функция `value => content`.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- собственную раскладку строк и переносы страниц;
|
||||
- скрытое глобальное продолжение счётчика между несвязанными списками;
|
||||
- автоматический выбор схемы по содержимому текста.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Не заменяйте нативный `enum` ручной сеткой или таблицей. Это ухудшит переносы, семантику документа и поддержку многоабзацных пунктов. Новые возможности добавляйте через форматирование массива родительских номеров и локальные set/show rules.
|
||||
@@ -0,0 +1,40 @@
|
||||
# Модуль: Нумерация
|
||||
|
||||
**Ответственность**: предоставляет чистые стратегии арабской, многоуровневой и кириллической нумерации без управления counters конкретного профиля.
|
||||
**Расположение**: `.template/lib/shared/numbering.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `cyrillic-numbering()` | чистая функция | Преобразует положительный номер в допустимую заглавную кириллическую букву |
|
||||
| `cyrillic-lower-numbering()` | чистая функция | Формирует строчную кириллическую часть списка |
|
||||
| `num-11()` | чистая функция | Формирует многоуровневый формат `1.1.` |
|
||||
| `num-1a()` | чистая функция | Чередует цифровые и кириллические уровни с наследованием |
|
||||
| `num-1-a()` | чистая функция | Чередует уровни без полного наследования родителей |
|
||||
| `attachment-numbering()` | чистая функция | Выбирает арабское или кириллическое обозначение из policy |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| Нет | Модуль состоит из чистых функций и неизменяемых массивов символов |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Исключённые ГОСТ-буквы не используются в кириллической последовательности.
|
||||
- Ноль и отрицательные значения не маскируются неявным fallback, несовместимым с будущими версиями Typst.
|
||||
- Функции не читают counters самостоятельно и форматируют только переданные числа.
|
||||
- Один и тот же вход всегда возвращает одинаковую строку.
|
||||
- Profile renderer управляет reset и scope counters, а не shared-модуль.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Сброс counters при новой главе.
|
||||
- Выбор numbering policy конкретного документа.
|
||||
- Формат заголовка приложения и supplement.
|
||||
- Локализацию на другие языки.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Typst 0.15 предупреждает о fallback для numbering systems, которые не умеют отображать ноль. Добавьте явные негативные unit tests. Сохраните существующие варианты `num_11`, `num_1a`, `num_1_a` семантически, но публичные имена можно унифицировать, поскольку обратная совместимость не требуется.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Модуль: Технико-коммерческое предложение
|
||||
|
||||
**Ответственность**: формирует ТКП как самостоятельный профиль с предметом, ценой, сроками, коммерческими условиями и приложениями.
|
||||
**Расположение**: `.template/lib/presentation/profiles/commercial-offer.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `commercial-offer-profile()` | конструктор `DocumentProfile` | Принимает адресата, предмет, стоимость, валюту, сроки и validity |
|
||||
| `render-commercial-offer()` | renderer-функция | Собирает шапку, резюме предложения, body, условия, подпись и приложения |
|
||||
| `commercial-terms()` | domain-normalizer | Нормализует цену, НДС, валюту, срок и порядок оплаты |
|
||||
| `commercial-offer-validators()` | массив functions | Проверяет предмет и обязательные коммерческие поля |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/document.typ` | `DocumentProfile` и context |
|
||||
| `.template/lib/domain/parties.typ` | Recipient и Signer |
|
||||
| `.template/lib/domain/attachments.typ` | Календарный план, ТЗ и другие приложения |
|
||||
| `.template/lib/presentation/foundation.typ` | Typography, page и media policy |
|
||||
| `.template/lib/presentation/components.typ` | Letter shell, money/date blocks, company footer, signature block |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- ТКП имеет собственный `profile.id` и renderer, а не boolean-режим письма.
|
||||
- Стоимость хранится структурированно: amount, currency, tax note; renderer не разбирает свободную строку.
|
||||
- Срок выполнения и срок действия предложения являются разными полями.
|
||||
- Приложения используют общий AttachmentSet и могут иметь цифровую нумерацию.
|
||||
- Коммерческие defaults принадлежат constructor, но пользователь может заменить текстовые формулировки через content slots.
|
||||
- Renderer не импортирует внешний `G:/TYPST/TKP` и не читает его config.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Расчёт стоимости из сметы.
|
||||
- Юридическую проверку налоговой формулировки.
|
||||
- Автоматическое превращение ТКП в договор.
|
||||
- Специфические главы научного отчёта.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Используйте TKP только как контекст сценариев и визуальную подсказку. Не копируйте монолитный facade. Если уникальная иконка действительно нужна, перенесите её в `.template/lib/assets/icons/` и дайте ей семантическое имя. Таблицы календарного плана должны использовать общий `corp-table`, а не локальную реализацию.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Модуль: Договор
|
||||
|
||||
**Ответственность**: предоставляет расширяемый каркас договора с преамбулой, произвольными разделами, сторонами, реквизитами, подписанием и приложениями.
|
||||
**Расположение**: `.template/lib/presentation/profiles/contract.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `contract-profile()` | конструктор `DocumentProfile` | Принимает номер, дату, место, название, стороны и section options |
|
||||
| `render-contract()` | renderer-функция | Собирает header, preamble slot, body sections, requisites, signing и attachments |
|
||||
| `contract-section()` | конструктор dictionary | Описывает нумерованный или именованный раздел с body |
|
||||
| `contract-validators()` | массив functions | Проверяет стороны, номера, даты и уникальность sections |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/document.typ` | `DocumentProfile` и context |
|
||||
| `.template/lib/domain/parties.typ` | Contract parties, representatives и signers |
|
||||
| `.template/lib/domain/attachments.typ` | Приложения и спецификации |
|
||||
| `.template/lib/presentation/foundation.typ` | Base typography и signing-copy mode |
|
||||
| `.template/lib/presentation/components.typ` | Requisites table, multi-party signing, tables и numbering primitives |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- В v1 поддерживается не менее двух сторон; модель не зашита строго на роли «Заказчик/Исполнитель».
|
||||
- Section id и display number уникальны.
|
||||
- Юридический текст sections остаётся пользовательским content.
|
||||
- Preamble можно передать content или собрать из сторон через явный helper; автоматический текст не является юридической гарантией.
|
||||
- Requisites берутся из Party/CompanyProfile, а не дублируются внутри renderer.
|
||||
- Signing-copy может резервировать место без реальных изображений подписи и печати.
|
||||
- Приложения используют тот же AttachmentSet, что отчёт и ТКП.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Юридическую достаточность и актуальность условий.
|
||||
- Электронную подпись и криптографию.
|
||||
- Согласование версий договора и tracked changes.
|
||||
- Автоматическую генерацию актов, счетов или УПД.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Не превращайте contract-profile в библиотеку юридических формулировок. Профиль отвечает за типографику и композицию. Чтобы будущие лицензионные, сервисные или смешанные договоры не требовали изменения renderer, sections должны быть открытым массивом с устойчивой numbering policy.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Модуль: Основа вёрстки
|
||||
|
||||
**Ответственность**: предоставляет общие design tokens, режимы выпуска и безопасные presentation-примитивы без правил конкретного вида документа.
|
||||
**Расположение**: `.template/lib/presentation/foundation.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `design-tokens()` | конструктор dictionary | Шрифты, размеры, цвета, интервалы и стандартные поля страницы |
|
||||
| `apply-foundation()` | show-функция | Применяет локальные общие text/par/page defaults к body renderer |
|
||||
| `render-media-slot()` | функция | Отображает ресурс по policy `hide`, `placeholder`, `reserve-space` |
|
||||
| `render-mode()` | чистая функция | Нормализует `final`, `draft`, `clean-copy` |
|
||||
| `watermark-layer()` | функция | Создаёт слой watermark без изменения domain данных |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/document.typ` | `RenderOptions` и режим выпуска |
|
||||
| `.template/lib/domain/company.typ` | Brand color и нормализованные ресурсы |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Foundation не знает, является документ отчётом, письмом, ТКП или договором.
|
||||
- Общие defaults локальны body, не протекают в следующий независимый renderer.
|
||||
- `render-media-slot()` никогда не вызывает `image(none)`.
|
||||
- Размер placeholder и reserve-space задаётся вызывающим компонентом, чтобы не ломать профильную геометрию.
|
||||
- Watermark не влияет на layout flow и счётчики.
|
||||
- Внутри module scope не создаётся state, общий для нескольких документов.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Титульный лист отчёта.
|
||||
- Шапку письма и реквизиты договора.
|
||||
- Нумерацию приложений конкретного профиля.
|
||||
- Загрузку файлов и JSON.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Typst 0.15 изменил baseline у `box` и `block`; любые правки foundation требуют visual regression всех профилей. Не переносите сюда profile-specific отступ только потому, что он встречается в двух документах: сначала проверьте, является ли это действительно общим design token.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Модуль: Деловое письмо
|
||||
|
||||
**Ответственность**: формирует исходящее деловое письмо с адресатом, регистрационными данными, темой, основным текстом и подписанием.
|
||||
**Расположение**: `.template/lib/presentation/profiles/letter.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `letter-profile()` | конструктор `DocumentProfile` | Принимает дату, исходящий номер, адресата, тему и signing options |
|
||||
| `render-letter()` | renderer-функция | Собирает фирменную шапку, body, подпись и footer |
|
||||
| `letter-metadata()` | чистая функция | Нормализует регистрационные поля и тему |
|
||||
| `letter-validators()` | массив functions | Проверяет адресата, дату и подписанта |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/document.typ` | `DocumentProfile` и render options |
|
||||
| `.template/lib/domain/parties.typ` | Recipient и Signer |
|
||||
| `.template/lib/domain/attachments.typ` | Перечень приложений к письму |
|
||||
| `.template/lib/presentation/foundation.typ` | Общие tokens и media policy |
|
||||
| `.template/lib/presentation/components.typ` | Letter header, company footer, attachment list, signature block |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Letter profile не содержит коммерческой стоимости, графика работ или offer validity.
|
||||
- Recipient может быть частично заполнен, но validator требует хотя бы организацию или ФИО.
|
||||
- Исходящий номер и дата отображаются единым регистрационным блоком.
|
||||
- Footer включается profile option и не исчезает из-за состояния приложения другого документа.
|
||||
- Подпись, печать и место для ручного подписания obey media policy.
|
||||
- Renderer не импортирует report или commercial-offer profile.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Коммерческие условия ТКП.
|
||||
- Титул и содержание отчёта.
|
||||
- Разделы договора.
|
||||
- Регистрацию письма во внешней системе.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Внешний проект TKP содержит полезный визуальный референс шапки и footer, но его global `comp-data` и `in-appendix` не переносятся. Состояние footer, если оно понадобится, должно принадлежать только текущему renderer и сбрасываться внутри него. Общая геометрия письма выносится в components, чтобы ТКП переиспользовал её без импорта `render-letter()`.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Модуль: Отчёт
|
||||
|
||||
**Ответственность**: реализует профиль научно-технического отчёта как стартовую заготовку, не влияя на деловые документы.
|
||||
**Расположение**: `.template/lib/presentation/profiles/report.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `report-profile()` | конструктор `DocumentProfile` | Принимает метаданные отчёта и безопасные defaults |
|
||||
| `render-report()` | renderer-функция | Собирает титул, служебные страницы, body, библиографии и приложения |
|
||||
| `report-metadata()` | чистая функция | Нормализует название, тему, договор, этап, том, год и режим исследования |
|
||||
| `report-validators()` | массив functions | Проверяет обязательные поля и совместимость опций |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `.template/lib/domain/document.typ` | `DocumentProfile`, `DocumentContext` |
|
||||
| `.template/lib/domain/parties.typ` | Approval и список исполнителей |
|
||||
| `.template/lib/appendices.typ` | Локальный контекст заголовков, страниц и нумерации файлов приложений |
|
||||
| `.template/lib/presentation/foundation.typ` | Общую страницу, typography и render mode |
|
||||
| `.template/lib/presentation/components.typ` | Title primitives, signature rows, tables, formula |
|
||||
| `.template/lib/presentation/references.typ` | Cross-references и bibliography sections |
|
||||
| `.template/lib/shared/numbering.typ` | Нумерацию глав, фигур, формул и приложений |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Все report-specific `set/show/state` локальны `render-report()`.
|
||||
- Титульный лист, список исполнителей и содержание независимо включаются параметрами `show_title_page`, `show_executors` и `show_outline`.
|
||||
- `draft/clean-copy` не загружает изображения подписей и печатей при соответствующей media policy.
|
||||
- Отключённая служебная страница не оставляет пустого листа или лишнего разрыва; первая отображаемая страница получает номер 1.
|
||||
- Counters фигур, таблиц и формул сбрасываются только в границах отчётной главы.
|
||||
- `appendices` содержит только уникальные `path`; название и label принадлежат первому заголовку подключённого файла.
|
||||
- Приложения используют собственную стратегию numbering без изменения других profiles; `attachment()` остаётся моделью писем и договоров.
|
||||
- Bibliographies представлены массивом секций; renderer не ограничивает их количество одним `refs.bib`.
|
||||
- Пользовательский body не изменяется и размещается после служебных страниц.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Адресата исходящего письма.
|
||||
- Стоимость и срок действия ТКП.
|
||||
- Стороны и реквизиты договора.
|
||||
- Загрузку компании и пользовательских файлов.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Legacy renderer в `.template/lib/report.typ` является функциональной спецификацией, но его API можно менять. Переносите секции по одной и после каждой сравнивайте snapshots. Сохраните проверенные решения вокруг heading gaps, paragraph indent, continuation tables и executor signatures, пока тест не докажет, что упрощение безопасно.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Модуль: Приватные ресурсы и сотрудники
|
||||
|
||||
**Ответственность**: подключает игнорируемые Git подписи и печати через один переключатель, хранит публичный справочник сотрудников и оставляет пустое место при недоступной подписи.
|
||||
**Расположение**: `.template/lib/infrastructure/employees.typ`, `.template/lib/assets/placeholders/`, `.private/`, `docs/examples/private/settings.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Элемент | Назначение |
|
||||
|---------|------------|
|
||||
| `employee-directory` | ФИО, обычные должности и имена PNG |
|
||||
| `empty-private-settings` | Безопасная конфигурация без private paths |
|
||||
| `private-company-media()` | Возвращает подпись и печать выбранной организации либо `none` |
|
||||
| `report-executor()` | Строит legacy-compatible tuple исполнителя с необязательной подписью |
|
||||
| `.private/settings.typ` | Локальная доступность изображений и индивидуальные offsets |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- При `use-private-assets = false` условный import не читает `.private/settings.typ`.
|
||||
- Включённая запись ссылается только на фиксированное имя из публичного справочника.
|
||||
- Отсутствующая или отключённая запись возвращает `none`, поэтому строка подписи остаётся пустой.
|
||||
- Реальные файлы не заменяют tracked placeholders.
|
||||
- Каталог `.private/` целиком игнорируется Git.
|
||||
- Обычная сборка и private-сборка используют одну VS Code task.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Доставку и шифрование папки `.private`.
|
||||
- Проверку подлинности подписи.
|
||||
- Автоматическую проверку наличия файла внутри Typst 0.15.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Typst не предоставляет безопасный file-exists. Поэтому при ещё не полученной подписи запись должна отсутствовать или иметь `enabled: false`. При добавлении сотрудника синхронно обновите directory, безопасный пример и публичную таблицу.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Модуль: Ссылки и библиографии
|
||||
|
||||
**Ответственность**: предоставляет русскоязычные перекрёстные ссылки и модель одной или нескольких библиографических секций Typst 0.15.
|
||||
**Расположение**: `.template/lib/domain/references.typ`, `.template/lib/presentation/references.typ`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ | Тип | Описание |
|
||||
|--------|-----|----------|
|
||||
| `vref()` | contextual function | Форматирует одну ссылку с падежом для рисунка, таблицы, формулы, раздела или приложения |
|
||||
| `vrefs()` | contextual function | Форматирует массив однородных labels с корректным соединением |
|
||||
| `eqref()` | contextual function | Создаёт ссылку на формулу без лишнего supplement |
|
||||
| `bibliography-section()` | domain constructor | Описывает sources, title, style, target, group и политику переноса |
|
||||
| `render-bibliographies()` | function | Размещает произвольное число секций через нативный `bibliography()` |
|
||||
| `validate-bibliographies()` | function | Проверяет ids, sources, target/group и наличие default coverage policy |
|
||||
|
||||
`vref()` и `vrefs()` по умолчанию используют предложный падеж и прописную первую букву названия объекта. Короткие коды — `"и"`, `"р"`, `"д"`, `"в"`, `"т"`, `"п"`; прежние сокращения и полные русские названия нормализуются во внутренние ключи `"имен"`, `"род"`, `"дат"`, `"вин"`, `"тв"`, `"предл"`. Строчная форма включается явно через `capitalized: false`.
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| Typst `query`, `selector`, `ref`, `cite` | Разрешение labels и citations |
|
||||
| Typst 0.15 `bibliography(target:, group:)` | Несколько списков источников и управление нумерацией |
|
||||
| `.template/lib/domain/document.typ` | Profile metadata и render options |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Cross-reference label и bibliography citation являются разными типами использования и не смешиваются в одной функции.
|
||||
- `vref` всегда обрабатывает состояние `element == none`, потому что Typst может ещё не обнаружить элемент.
|
||||
- `vref` поддерживает `имен`, `род`, `дат`, `вин`, `тв`, `предл`; неизвестный падеж является ошибкой, а не тихим fallback.
|
||||
- `vrefs` объединяет однородные labels под одной формой множественного числа (`рисунках 1 и 2`, `приложениях А и Б`), смешанные типы форматирует поэлементно.
|
||||
- Заголовок с supplement `Приложение` классифицируется отдельно от обычного раздела и поддерживает все шесть падежей.
|
||||
- Каждая bibliography section имеет стабильный id и хотя бы один source.
|
||||
- Последующие секции по умолчанию начинаются с новой страницы (`page_break: true`), поэтому их заголовки и записи не могут наложиться; компактный режим включается явно.
|
||||
- `target` передаётся как `auto`, label, selector, location или function без преобразования в строку.
|
||||
- `group: none` сбрасывает нумерацию секции; одинаковая строковая группа продолжает нумерацию.
|
||||
- Если используется несколько sections, fixture обязан доказать, что каждая citation попала ровно в одну bibliography.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Редактирование `.bib` и Hayagriva-файлов.
|
||||
- Поиск научных источников.
|
||||
- Ссылки на внешние URL без label.
|
||||
- Автоматическое определение падежа по окружающему тексту.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Typst 0.15 нативно распределяет citations между несколькими bibliographies. Не воспроизводите этот алгоритм вручную. Domain хранит настройки, presentation вызывает built-in и смещает уровень заголовка библиографии по официальной схеме. Добавьте fixtures для поглавных, тематических, общей и раздельной нумерации.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Модуль: Публичные примеры документов
|
||||
|
||||
**Ответственность**: хранит исполняемую документацию четырёх профилей и источник безопасной установки выбранного типа.
|
||||
**Расположение**: `docs/examples/`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Каталог | Назначение |
|
||||
|---------|------------|
|
||||
| `documents/report/` | Полный отчёт с главами, bibliography и appendix |
|
||||
| `documents/letter/` | Деловое письмо |
|
||||
| `documents/commercial-offer/` | Технико-коммерческое предложение |
|
||||
| `documents/contract/` | Договор |
|
||||
| `formatting/` | Каталог рисунков, таблиц, формул, ссылок и списков |
|
||||
| `private/settings.typ` | Безопасный полный пример private settings без PNG |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Модуль | Что использует |
|
||||
|--------|----------------|
|
||||
| `/.template/lib/index.typ` | Только публичный фасад |
|
||||
| `use-starter.ps1` | Backup и копирование примера в корень |
|
||||
| Presentation profiles | Реальный layout каждого вида документа |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Каждый `main.typ` компилируется непосредственно из своего каталога.
|
||||
- Тот же каталог компилируется после копирования в корень.
|
||||
- Каждый пример явно показывает `company-id`, `document-mode`, `use-private-assets`, profile metadata и media policy.
|
||||
- Public example не зависит от `.template/development/`.
|
||||
- Formatting guide содержит пояснения рядом с копируемыми блоками.
|
||||
- Выбор типа создаёт backup до замены пользовательских файлов.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Обратную совместимость со старыми starters.
|
||||
- Синхронизацию уже изменённого примера с пользовательским fork.
|
||||
- Юридическую корректность демонстрационного договора.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Не создавайте отдельную showcase-копию: публичный пример должен одновременно быть тестовым fixture и источником установки. Изменение API считается завершённым только после обновления всех четырёх `main.typ` и formatting guide.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Модуль: Тестирование
|
||||
|
||||
**Ответственность**: защищает слои библиотеки, единственный root entrypoint, public examples, PDF semantics и визуальную стабильность.
|
||||
**Расположение**: `.template/development/tests/`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Символ или файл | Тип | Описание |
|
||||
|-----------------|-----|----------|
|
||||
| `run-tests.py` | CLI | Полный release gate |
|
||||
| `manifest.json` | fixture manifest | Compile, semantic, negative и visual expectations |
|
||||
| `check_architecture()` | static check | Направление Typst imports |
|
||||
| `check_workspace()` | static check | Root surface, docs, VS Code, private settings и public examples |
|
||||
| `snapshots/` | visual baseline | Утверждённые страницы стабильных renderer fixtures |
|
||||
|
||||
## Зависимости
|
||||
|
||||
| Компонент | Что использует |
|
||||
|-----------|----------------|
|
||||
| Typst 0.15.1+ | Компиляция и PDF generation |
|
||||
| Poppler | Text extraction, links и PNG rendering |
|
||||
| Pillow | Pixel diff snapshots |
|
||||
| `docs/examples/` | Public executable documentation |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- Корень не содержит `document.typ`, `draft.typ` и `clean-copy.typ`.
|
||||
- `docs/` видим и содержит обязательную public navigation.
|
||||
- Все четыре документа и formatting guide компилируются.
|
||||
- Generic profile fixtures проверяют `final`, `draft` и `clean-copy` без root entrypoint duplication.
|
||||
- Безопасный private settings example содержит всех сотрудников с отключёнными PNG.
|
||||
- Unit tests проверяют public directory, private offsets и отсутствие подписи.
|
||||
- Public company profiles не содержат настоящих sign/stamp.
|
||||
- Snapshot изменяется только после ручного visual review.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Юридическую корректность договоров.
|
||||
- Инженерную достоверность example values.
|
||||
- Установку сторонних extensions.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Public example является production-facing documentation: его compile failure блокирует выпуск так же, как regression renderer. Для layout changes сначала просмотрите rendered pages, затем осознанно обновите snapshots.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Модуль: Публичная документация
|
||||
|
||||
**Ответственность**: проводит автора без опыта программирования от открытия fork до проверенного PDF.
|
||||
**Расположение**: `README.md`, `docs/`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Документ | Назначение |
|
||||
|----------|------------|
|
||||
| `README.md` | Краткий обязательный маршрут и все важные ссылки |
|
||||
| `docs/README.md` | Полная пользовательская навигация |
|
||||
| `docs/documents.md` | Один main, режимы, профили, главы и assets |
|
||||
| `docs/formatting.md` | Таблицы, рисунки, формулы и ссылки |
|
||||
| `docs/examples/` | Компилируемые исходники |
|
||||
| `docs/vscode.md` | Расширения, preview и tasks |
|
||||
| `docs/git.md` | Совместная работа простыми словами |
|
||||
| `docs/private-assets.md` | Копирование `.private`, один переключатель, роли и offsets |
|
||||
| `docs/writing-style.md` | Заготовка для редактирования технического текста |
|
||||
| `docs/troubleshooting.md` | Диагностика типовых проблем |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- `docs/` виден в Explorer и находится в корне.
|
||||
- Все обязательные сценарии доступны из root README.
|
||||
- Каждый Typst-фрагмент с нетривиальным API имеет компилируемую версию.
|
||||
- Термины Git объясняются бытовыми аналогиями.
|
||||
- Developer details остаются в `.template/development/`.
|
||||
- Документация не обещает автоматическую установку внутренних VSIX.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- DDD architecture и migration internals.
|
||||
- Обучение программированию.
|
||||
- Администрирование Git-сервера.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Любое новое пользовательское действие сначала появляется в root README, затем раскрывается в `docs/`. Проверяйте относительные Markdown links и компиляцию public examples автоматически.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Модуль: Рабочая область VS Code
|
||||
|
||||
**Ответственность**: синхронизирует воспроизводимую среду автора и предоставляет три понятные задачи.
|
||||
**Расположение**: `.vscode/`, `.template/development/tools/`
|
||||
|
||||
## Публичный интерфейс
|
||||
|
||||
| Файл или задача | Назначение |
|
||||
|-----------------|------------|
|
||||
| `extensions.json` | Десять согласованных recommendations |
|
||||
| `settings.json` | Auto Save, spellcheck, TODO, скрытие служебных каталогов |
|
||||
| `Scientia: собрать PDF` | Компиляция текущего `main.typ` |
|
||||
| `Scientia: выбрать тип документа` | Backup и установка public example |
|
||||
| `Scientia: собрать учебный пример` | Компиляция выбранного исходника из `docs/examples/` |
|
||||
|
||||
## Инварианты
|
||||
|
||||
- `.vscode/` включён в Git, но скрыт в Explorer.
|
||||
- `docs/` никогда не скрыт.
|
||||
- Typewriter navigator содержит только `main.typ`.
|
||||
- Автосохранение не создаёт commit и не выполняет push.
|
||||
- Tasks не хранят приватные значения в tracked settings.
|
||||
- Одна build task собирает и public-, и private-режим согласно `main.typ`.
|
||||
- Внутренние VSIX не входят в репозиторий.
|
||||
|
||||
## Намеренно НЕ обрабатывает
|
||||
|
||||
- Установку VS Code, Git и Typst.
|
||||
- Авторизацию на Git-сервере.
|
||||
- Публикацию внутренних VSIX.
|
||||
|
||||
## Заметки для агента
|
||||
|
||||
> Не добавляйте отдельную build task для каждого режима: пользователь меняет `document-mode` в `main.typ`. Новая задача должна либо быть частой, либо существенно снижать риск потери данных.
|
||||
@@ -0,0 +1,67 @@
|
||||
#import "/.template/lib/domain/document.typ": render-options, document-profile, validate-profile-contract
|
||||
#import "/.template/lib/domain/attachments.typ": attachment, attachment-set
|
||||
#import "/.template/lib/domain/references.typ": bibliography-section, validate-bibliographies
|
||||
#import "/.template/lib/infrastructure/company-assets.typ": load-company, available-companies
|
||||
#import "/.template/lib/infrastructure/employees.typ": employee-directory, empty-private-settings, private-company-media, report-executor
|
||||
|
||||
#let passthrough(body, ctx) = body
|
||||
|
||||
#let run() = {
|
||||
let options = render-options()
|
||||
assert.eq(options.mode, "final")
|
||||
assert.eq(options.at("media-policy"), "placeholder")
|
||||
|
||||
let profile = document-profile("unit", passthrough)
|
||||
assert.eq(validate-profile-contract(profile).id, "unit")
|
||||
|
||||
assert.eq(available-companies().len(), 4)
|
||||
for id in available-companies() {
|
||||
let loaded = load-company(id)
|
||||
assert.eq(loaded.kind, "company-profile")
|
||||
assert(type(loaded.legal.name) == str)
|
||||
}
|
||||
let company = load-company("test-company")
|
||||
assert.eq(company.legal.name, "ООО «Тестовая компания»")
|
||||
assert.eq(type(company.resources.logo), path)
|
||||
|
||||
assert.eq(employee-directory.len(), 19)
|
||||
let unsigned = report-executor("fedorov")
|
||||
assert.eq(unsigned.at(1), "Федоров Д.А.")
|
||||
assert.eq(unsigned.at(2), none)
|
||||
|
||||
let private-settings = (
|
||||
companies: (scientia: (signature: true, stamp: false)),
|
||||
signatures: (
|
||||
musikhin: (enabled: true, offset: 1.25cm),
|
||||
fedorov: (enabled: false, offset: 0.7cm),
|
||||
),
|
||||
)
|
||||
let signed = report-executor(
|
||||
"musikhin",
|
||||
role: "Управляющий директор",
|
||||
private-settings: private-settings,
|
||||
)
|
||||
assert.eq(signed.at(0), "Управляющий директор")
|
||||
assert.eq(type(signed.at(2)), path)
|
||||
assert.eq(signed.at(3), 1.25cm)
|
||||
assert.eq(report-executor("fedorov", private-settings: private-settings).at(2), none)
|
||||
let media = private-company-media(private-settings, "scientia")
|
||||
assert.eq(type(media.signature), path)
|
||||
assert.eq(media.stamp, none)
|
||||
assert.eq(private-company-media(empty-private-settings, "scientia").signature, none)
|
||||
|
||||
let item = attachment("a", "Приложение", [Текст])
|
||||
let items = attachment-set(items: (item,), numbering: "cyrillic")
|
||||
assert.eq(items.items.len(), 1)
|
||||
|
||||
let bibliography = bibliography-section("main", path("/assets/references.bib"))
|
||||
assert.eq(validate-bibliographies((bibliography,)).len(), 1)
|
||||
assert.eq(bibliography.at("page-break"), true)
|
||||
let compact-bibliography = bibliography-section(
|
||||
"compact",
|
||||
path("/assets/references.bib"),
|
||||
page_break: false,
|
||||
)
|
||||
assert.eq(compact-bibliography.at("page-break"), false)
|
||||
"ok"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#import "/.template/lib/numbering.typ": cyrillic_numbering, num_11, num_1a, num_1_a
|
||||
|
||||
#let run() = {
|
||||
assert.eq(cyrillic_numbering(1), "А")
|
||||
assert.eq(cyrillic_numbering(8), "И")
|
||||
assert.eq(cyrillic_numbering(25), "Я")
|
||||
assert.eq(cyrillic_numbering(26), "26")
|
||||
assert.eq(num_11(1, 2, 3), "1.2.3.")
|
||||
assert.eq(num_1a(1, 2, 3), "1.б.3.")
|
||||
assert.eq(num_1_a(1, 2), "б.")
|
||||
"ok"
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#import "/.template/lib/domain/parties.typ": recipient, party, signer
|
||||
#import "/.template/lib/presentation/profiles/index.typ" as profiles
|
||||
#import "/.template/lib/shared/numbering.typ": attachment_numbering
|
||||
|
||||
#let run() = {
|
||||
let addressee = recipient(company: "ООО «Адресат»")
|
||||
|
||||
let report = profiles.report(
|
||||
title: "Проверка отчёта",
|
||||
show_title_page: false,
|
||||
show_executors: false,
|
||||
show_outline: true,
|
||||
)
|
||||
let report-metadata = (report.validate)(report.metadata)
|
||||
assert.eq(report.id, "report")
|
||||
assert.eq(report-metadata.at("show-title-page"), false)
|
||||
assert.eq(report-metadata.at("show-executors"), false)
|
||||
assert.eq(report-metadata.at("show-outline"), true)
|
||||
|
||||
let letter = profiles.letter(recipient: addressee)
|
||||
assert.eq(letter.id, "letter")
|
||||
assert.eq(((letter.validate)(letter.metadata)).recipient.kind, "recipient")
|
||||
|
||||
let offer = profiles.commercial_offer(
|
||||
recipient: addressee,
|
||||
subject: "Предмет",
|
||||
amount: "100",
|
||||
)
|
||||
assert.eq(offer.id, "commercial-offer")
|
||||
assert.eq(((offer.validate)(offer.metadata)).terms.amount, "100")
|
||||
|
||||
let left = party(
|
||||
"left",
|
||||
"Исполнитель",
|
||||
"ООО «Исполнитель»",
|
||||
representative: signer("И.И. Исполнитель", "Директор"),
|
||||
)
|
||||
let right = party(
|
||||
"right",
|
||||
"Заказчик",
|
||||
"ООО «Заказчик»",
|
||||
representative: signer("З.З. Заказчик", "Директор"),
|
||||
)
|
||||
let contract = profiles.contract(
|
||||
number: "1",
|
||||
parties: (left, right),
|
||||
sections: (
|
||||
profiles.contract_section("subject", "Предмет", [Текст раздела]),
|
||||
),
|
||||
)
|
||||
assert.eq(contract.id, "contract")
|
||||
assert.eq(((contract.validate)(contract.metadata)).sections.len(), 1)
|
||||
|
||||
assert.eq(attachment_numbering("arabic", 2), "2")
|
||||
assert.eq(attachment_numbering("cyrillic", 2), "Б")
|
||||
assert.eq(attachment_numbering("none", 2), "")
|
||||
"ok"
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#import "/.template/lib/api.typ": document, recipient, attachment, attachment-set
|
||||
#import "/.template/lib/presentation/profiles/commercial-offer.typ": commercial-offer-profile
|
||||
|
||||
#let addressee = recipient(
|
||||
company: "АО «Заказчик»",
|
||||
title: "Руководителю службы автоматизации",
|
||||
name: "С.С. Заказчикову",
|
||||
)
|
||||
|
||||
#let appendices = attachment-set(items: (
|
||||
attachment(
|
||||
"scope",
|
||||
"Техническое задание",
|
||||
[
|
||||
Поставка, настройка и ввод в эксплуатацию тестового комплекса.
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: commercial-offer-profile(
|
||||
recipient: addressee,
|
||||
subject: "Поставка и внедрение тестового комплекса",
|
||||
amount: "1 250 000",
|
||||
currency: "руб.",
|
||||
tax_note: "включая НДС 20 %",
|
||||
delivery_term: "45 рабочих дней",
|
||||
validity: "30 календарных дней",
|
||||
payment_terms: "30 % аванс, 70 % после приёмки",
|
||||
date: "26.08.2026",
|
||||
reference: "ТКП-001",
|
||||
note: "Контактное лицо: А.А. Автор",
|
||||
attachments: appendices,
|
||||
),
|
||||
)
|
||||
|
||||
Уважаемый Сергей Сергеевич!
|
||||
|
||||
Предлагаем выполнить комплекс работ по поставке и внедрению оборудования.
|
||||
|
||||
== Состав работ
|
||||
|
||||
- обследование объекта;
|
||||
- поставка оборудования;
|
||||
- монтаж и пусконаладка;
|
||||
- обучение персонала заказчика.
|
||||
|
||||
Гарантийный срок составляет 12 месяцев с даты подписания акта приёмки.
|
||||
@@ -0,0 +1,85 @@
|
||||
#import "/.template/lib/api.typ": document, party, signer, attachment, attachment-set, load-company
|
||||
#import "/.template/lib/presentation/profiles/contract.typ": contract-profile, contract-section
|
||||
|
||||
#let company = load-company("test-company")
|
||||
|
||||
#let contractor = party(
|
||||
"contractor",
|
||||
"Исполнитель",
|
||||
company.legal.name,
|
||||
legal: company.legal,
|
||||
contacts: company.contacts,
|
||||
banking: company.banking,
|
||||
representative: signer(
|
||||
company.director.name,
|
||||
company.director.title,
|
||||
basis: "Устава",
|
||||
signature: company.resources.signature,
|
||||
stamp: company.resources.stamp,
|
||||
),
|
||||
)
|
||||
|
||||
#let customer = party(
|
||||
"customer",
|
||||
"Заказчик",
|
||||
"ООО «Заказчик»",
|
||||
legal: (
|
||||
inn: "1111111111",
|
||||
kpp: "111111111",
|
||||
address: "г. Пример, ул. Договорная, 2",
|
||||
),
|
||||
contacts: (email: "customer@example.invalid"),
|
||||
banking: (
|
||||
bank: "Банк заказчика",
|
||||
account: "11111111111111111111",
|
||||
bik: "111111111",
|
||||
),
|
||||
representative: signer(
|
||||
"П.П. Заказчиков",
|
||||
"Генеральный директор",
|
||||
basis: "Устава",
|
||||
),
|
||||
)
|
||||
|
||||
#let sections = (
|
||||
contract-section(
|
||||
"subject",
|
||||
"Предмет договора",
|
||||
[Исполнитель обязуется выполнить работы, а Заказчик — принять и оплатить их на условиях настоящего договора.],
|
||||
),
|
||||
contract-section(
|
||||
"price",
|
||||
"Цена и порядок расчётов",
|
||||
[Цена договора составляет 1 250 000 рублей, включая НДС 20 %.],
|
||||
),
|
||||
contract-section(
|
||||
"liability",
|
||||
"Ответственность сторон",
|
||||
[Стороны несут ответственность в соответствии с законодательством Российской Федерации.],
|
||||
),
|
||||
)
|
||||
|
||||
#let appendices = attachment-set(items: (
|
||||
attachment(
|
||||
"specification",
|
||||
"Спецификация",
|
||||
[
|
||||
Перечень работ и оборудования согласуется сторонами в настоящем приложении.
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: contract-profile(
|
||||
number: "Д-001/2026",
|
||||
date: "26 августа 2026 г.",
|
||||
place: "г. Пример",
|
||||
title: "Договор оказания услуг",
|
||||
parties: (contractor, customer),
|
||||
sections: sections,
|
||||
attachments: appendices,
|
||||
),
|
||||
)
|
||||
|
||||
Настоящий договор вступает в силу с момента подписания обеими сторонами.
|
||||
@@ -0,0 +1,26 @@
|
||||
#import "/.template/lib/api.typ": document, document-profile
|
||||
|
||||
#let smoke-renderer(body, ctx) = [
|
||||
#set text(font: "Arial", size: 11pt, lang: "ru")
|
||||
#set page(paper: "a4")
|
||||
#text(weight: "bold")[DDD CORE SMOKE]
|
||||
|
||||
Компания: #ctx.company.legal.name
|
||||
|
||||
Профиль: #ctx.at("profile-id")
|
||||
|
||||
#body
|
||||
]
|
||||
|
||||
#let smoke-profile = document-profile(
|
||||
"smoke",
|
||||
smoke-renderer,
|
||||
metadata: (:),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: smoke-profile,
|
||||
)
|
||||
|
||||
Изолированное ядро успешно собрано.
|
||||
@@ -0,0 +1,7 @@
|
||||
#import "/.template/lib/index.typ": document
|
||||
|
||||
#show: document.with(company: "test-company")
|
||||
|
||||
#heading(numbering: none)[ПРОВЕРКА ПРОФИЛЯ ПО УМОЛЧАНИЮ]
|
||||
|
||||
Корневой фасад формирует отчёт без явной передачи profile.
|
||||
@@ -0,0 +1,15 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, recipient, info-block
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.letter(
|
||||
recipient: recipient(company: "ООО «Проверка фасада»"),
|
||||
title: "Публичный фасад",
|
||||
show_stamp: false,
|
||||
),
|
||||
options: (mode: "clean-copy", media-policy: "hide"),
|
||||
)
|
||||
|
||||
#info-block(title: [ПРОВЕРКА КОМПОНЕНТА])[
|
||||
Корневой файл экспортирует единый фасад и namespace профилей.
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
#import "/.template/lib/api.typ": document, recipient, attachment, attachment-set
|
||||
#import "/.template/lib/presentation/profiles/letter.typ": letter-profile
|
||||
|
||||
#let addressee = recipient(
|
||||
company: "ООО «Получатель»",
|
||||
title: "Генеральному директору",
|
||||
name: "П.П. Получателю",
|
||||
address: "г. Пример, ул. Адресная, 10",
|
||||
)
|
||||
|
||||
#let appendices = attachment-set(items: (
|
||||
attachment(
|
||||
"specification",
|
||||
"Краткая спецификация",
|
||||
[
|
||||
Состав приложения задаётся пользователем независимо от профиля письма.
|
||||
],
|
||||
),
|
||||
))
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: letter-profile(
|
||||
recipient: addressee,
|
||||
date: "26.08.2026",
|
||||
reference: "Л-001",
|
||||
title: "О направлении материалов",
|
||||
note: "Исполнитель: А.А. Автор, +7 (000) 000-00-01",
|
||||
attachments: appendices,
|
||||
render_attachments: true,
|
||||
),
|
||||
)
|
||||
|
||||
Уважаемый Пётр Петрович!
|
||||
|
||||
Направляем материалы для рассмотрения. Профиль письма использует единый источник данных компании и самостоятельно формирует шапку, подвал, список приложений и блок подписи.
|
||||
|
||||
Просим подтвердить получение документов.
|
||||
@@ -0,0 +1,90 @@
|
||||
#import "/.template/lib/index.typ": numbered-list
|
||||
|
||||
#set page("a4", margin: 1cm)
|
||||
#set text(size: 9pt)
|
||||
#set par(first-line-indent: 0pt, leading: 0.4em)
|
||||
|
||||
= SCIENTIA LIST COUNTERS
|
||||
|
||||
#let compact-list(style, body) = numbered-list(
|
||||
levels: (style,),
|
||||
suffixes: ".",
|
||||
line-leading: 0.4em,
|
||||
item-spacing: 0pt,
|
||||
body,
|
||||
)
|
||||
|
||||
== Арабские числа
|
||||
|
||||
#compact-list("1")[
|
||||
+ Арабские: первый
|
||||
+ Арабские: второй
|
||||
+ Арабские: третий
|
||||
]
|
||||
|
||||
== Кириллица
|
||||
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
gutter: 1cm,
|
||||
[
|
||||
#compact-list("А")[
|
||||
+ Кириллица верхняя: первый
|
||||
+ Кириллица верхняя: второй
|
||||
+ Кириллица верхняя: третий
|
||||
]
|
||||
],
|
||||
[
|
||||
#compact-list("а")[
|
||||
+ Кириллица нижняя: первый
|
||||
+ Кириллица нижняя: второй
|
||||
+ Кириллица нижняя: третий
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
== Латиница
|
||||
|
||||
#grid(
|
||||
columns: (1fr, 1fr),
|
||||
gutter: 1cm,
|
||||
[
|
||||
#compact-list("A")[
|
||||
+ Латиница верхняя: первый
|
||||
+ Латиница верхняя: второй
|
||||
+ Латиница верхняя: третий
|
||||
]
|
||||
],
|
||||
[
|
||||
#compact-list("a")[
|
||||
+ Латиница нижняя: первый
|
||||
+ Латиница нижняя: второй
|
||||
+ Латиница нижняя: третий
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
== Ведущий ноль
|
||||
|
||||
#compact-list("01")[
|
||||
+ Ведущий ноль: первый
|
||||
+ Ведущий ноль: второй
|
||||
+ Ведущий ноль: третий
|
||||
]
|
||||
|
||||
== Вложенные счётчики
|
||||
|
||||
#numbered-list(
|
||||
levels: ("1", "а", "A"),
|
||||
full: true,
|
||||
separators: ".",
|
||||
suffixes: ".",
|
||||
line-leading: 0.4em,
|
||||
item-spacing: 0pt,
|
||||
)[
|
||||
+ Корневой уровень
|
||||
+ Второй уровень: первый
|
||||
+ Второй уровень: второй
|
||||
+ Третий уровень: первый
|
||||
+ Третий уровень: второй
|
||||
]
|
||||
@@ -0,0 +1,60 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, recipient, party, signer, load-company
|
||||
|
||||
#let profile-id = sys.inputs.at("profile", default: "letter")
|
||||
#let mode = sys.inputs.at("mode", default: "draft")
|
||||
#let company-id = sys.inputs.at("company", default: "test-company")
|
||||
#let company = load-company(company-id)
|
||||
#let addressee = recipient(company: "ООО «Проверка режима»")
|
||||
|
||||
#let left = party(
|
||||
"left",
|
||||
"Исполнитель",
|
||||
company.legal.name,
|
||||
legal: company.legal,
|
||||
contacts: company.contacts,
|
||||
banking: company.banking,
|
||||
representative: signer(
|
||||
company.director.name,
|
||||
company.director.title,
|
||||
signature: company.resources.signature,
|
||||
stamp: company.resources.stamp,
|
||||
),
|
||||
)
|
||||
#let right = party(
|
||||
"right",
|
||||
"Заказчик",
|
||||
"ООО «Проверка режима»",
|
||||
representative: signer("П.П. Проверяющий", "Директор"),
|
||||
)
|
||||
|
||||
#let selected-profile = if profile-id == "report" {
|
||||
profiles.report(title: "Проверка режима выпуска", year: 2026)
|
||||
} else if profile-id == "letter" {
|
||||
profiles.letter(recipient: addressee, title: "Проверка режима выпуска")
|
||||
} else if profile-id == "commercial-offer" {
|
||||
profiles.commercial_offer(
|
||||
recipient: addressee,
|
||||
subject: "Проверка режима выпуска",
|
||||
amount: "100 000",
|
||||
currency: "руб.",
|
||||
)
|
||||
} else if profile-id == "contract" {
|
||||
profiles.contract(
|
||||
number: "MODE-1",
|
||||
date: "26.08.2026",
|
||||
parties: (left, right),
|
||||
sections: (
|
||||
profiles.contract_section("subject", "Предмет договора", [Проверка режима выпуска.]),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
panic("Неизвестный тестовый profile: " + profile-id)
|
||||
}
|
||||
|
||||
#show: document.with(
|
||||
company: company,
|
||||
profile: selected-profile,
|
||||
options: (mode: mode, watermark: if mode == "draft" { "ЧЕРНОВИК" } else { none }),
|
||||
)
|
||||
|
||||
Документ собран в режиме #mode для профиля #profile-id.
|
||||
@@ -0,0 +1,5 @@
|
||||
#import "/.template/lib/index.typ": document
|
||||
|
||||
#show: document.with(company: "unknown-company")
|
||||
|
||||
Этот текст не должен попасть в PDF.
|
||||
@@ -0,0 +1,13 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, party
|
||||
|
||||
#let duplicate = (
|
||||
party("same", "Исполнитель", "ООО «Первый»"),
|
||||
party("same", "Заказчик", "ООО «Второй»"),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.contract(number: "1", parties: duplicate),
|
||||
)
|
||||
|
||||
Этот текст не должен попасть в PDF.
|
||||
@@ -0,0 +1,8 @@
|
||||
#import "/.template/lib/index.typ": document, profiles
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.letter(),
|
||||
)
|
||||
|
||||
Этот текст не должен попасть в PDF.
|
||||
@@ -0,0 +1,5 @@
|
||||
#import "/.template/lib/index.typ": numbered-list
|
||||
|
||||
#numbered-list(scheme: "неизвестная-схема")[
|
||||
+ Тестовый пункт
|
||||
]
|
||||
@@ -0,0 +1,11 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, recipient
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.commercial_offer(
|
||||
recipient: recipient(company: "ООО «Адресат»"),
|
||||
subject: "Предложение без стоимости",
|
||||
),
|
||||
)
|
||||
|
||||
Этот текст не должен попасть в PDF.
|
||||
@@ -0,0 +1,3 @@
|
||||
#import "/.template/lib/index.typ": vref
|
||||
|
||||
#vref(<missing-reference>, grammatical-case: "мест")
|
||||
@@ -0,0 +1,5 @@
|
||||
#import "/.template/lib/index.typ": vrefs
|
||||
|
||||
#vrefs((<valid-label>, "not-a-label"))
|
||||
|
||||
#figure(rect(width: 1cm, height: 1cm), caption: [Элемент]) <valid-label>
|
||||
@@ -0,0 +1,11 @@
|
||||
#import "/.template/lib/index.typ": document, profiles
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.report(
|
||||
title: "Ошибка пути приложения",
|
||||
appendices: ("appendix.typ",),
|
||||
),
|
||||
)
|
||||
|
||||
Проверка понятной ошибки для строкового пути.
|
||||
@@ -0,0 +1,39 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, recipient, company-profile
|
||||
|
||||
#let policy = sys.inputs.at("policy", default: "placeholder")
|
||||
#let company = company-profile(
|
||||
"no-media",
|
||||
(
|
||||
name: "ООО «Компания без встроенных графических ресурсов»",
|
||||
short-name: "Без ресурсов",
|
||||
jurisdiction: "RU",
|
||||
inn: "0000000000",
|
||||
),
|
||||
contacts: (
|
||||
email: "no-media@example.invalid",
|
||||
phone: "+7 (000) 000-00-00",
|
||||
address: "г. Пример, очень длинный адрес для проверки переноса строк в деловом документе, дом 100, офис 200",
|
||||
),
|
||||
banking: (:),
|
||||
brand: (color: rgb("e39f49")),
|
||||
director: (title: "Генеральный директор", name: "И.И. Подписант"),
|
||||
resources: (logo: none, signature: none, stamp: none),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: company,
|
||||
profile: profiles.letter(
|
||||
recipient: recipient(
|
||||
company: "Акционерное общество «Организация с длинным наименованием для проверки устойчивости вёрстки»",
|
||||
title: "Заместителю генерального директора по техническим и коммерческим вопросам",
|
||||
name: "П.П. Получателю",
|
||||
address: "г. Пример, проспект Испытательный, дом 123, строение 45",
|
||||
),
|
||||
date: "26.08.2026",
|
||||
reference: "NO-MEDIA-001",
|
||||
title: "Проверка формирования документа без логотипа, подписи и печати",
|
||||
),
|
||||
options: (mode: "final", media-policy: policy),
|
||||
)
|
||||
|
||||
Документ без графических ресурсов собран с политикой #policy.
|
||||
@@ -0,0 +1,49 @@
|
||||
#import "/.template/lib/api.typ": load-company, recipient, signer
|
||||
#import "/.template/lib/presentation/foundation.typ": apply-foundation, render-media-slot
|
||||
#import "/.template/lib/presentation/components.typ": corp_table, company-footer, letter-header, signature-block
|
||||
|
||||
#let company = load-company("test-company")
|
||||
#let addressee = recipient(
|
||||
company: "ООО «Получатель»",
|
||||
title: "Руководителю проекта",
|
||||
name: "П.П. Примерову",
|
||||
)
|
||||
#let director = signer(
|
||||
company.director.name,
|
||||
company.director.title,
|
||||
signature: company.resources.signature,
|
||||
stamp: company.resources.stamp,
|
||||
)
|
||||
#let ctx = (
|
||||
company: company,
|
||||
options: (watermark: none),
|
||||
)
|
||||
|
||||
#show: apply-foundation.with(
|
||||
ctx: ctx,
|
||||
page-options: (margin: (x: 2cm, y: 1.5cm)),
|
||||
)
|
||||
|
||||
#letter-header(
|
||||
company,
|
||||
addressee,
|
||||
date: "26.08.2026",
|
||||
reference: "TEST-1",
|
||||
title: "Проверка компонентов",
|
||||
)
|
||||
|
||||
#v(0.8cm)
|
||||
Общая presentation-основа не зависит от конкретного профиля документа.
|
||||
|
||||
#v(0.8cm)
|
||||
#corp_table(
|
||||
columns: 2,
|
||||
[Компонент], [Состояние],
|
||||
[Таблица], [Работает],
|
||||
[Media slot], [#render-media-slot(none, width: 3cm, height: 1cm)],
|
||||
)
|
||||
|
||||
#v(1cm)
|
||||
#signature-block(director, company: company)
|
||||
|
||||
#place(bottom + left, dy: 0.5cm, company-footer(company))
|
||||
@@ -0,0 +1,49 @@
|
||||
#import "/.template/lib/index.typ": vref, vrefs
|
||||
|
||||
#set page(paper: "a4", margin: 15mm)
|
||||
#set text(lang: "ru", size: 9pt)
|
||||
#set heading(numbering: "1.")
|
||||
#set figure(numbering: "1")
|
||||
|
||||
#figure(
|
||||
rect(width: 12mm, height: 6mm),
|
||||
caption: [Первый рисунок],
|
||||
) <case-figure-a>
|
||||
|
||||
#figure(
|
||||
rect(width: 12mm, height: 6mm),
|
||||
caption: [Второй рисунок],
|
||||
) <case-figure-b>
|
||||
|
||||
#figure(
|
||||
table(columns: 1, [Значение]),
|
||||
caption: [Тестовая таблица],
|
||||
) <case-table>
|
||||
|
||||
#math.equation(block: true, numbering: "(1)")[$x = 1$] <case-equation>
|
||||
|
||||
= Тестовый раздел <case-section>
|
||||
|
||||
Предложный по умолчанию: на #vref(<case-figure-a>).
|
||||
|
||||
Именительный коротко: #vref(<case-figure-a>, "и").
|
||||
|
||||
Родительный коротко: без #vref(<case-figure-a>, "р").
|
||||
|
||||
Дательный коротко: к #vref(<case-figure-a>, "д").
|
||||
|
||||
Винительный коротко: вижу #vref(<case-figure-a>, "в").
|
||||
|
||||
Творительный коротко: перед #vref(<case-figure-a>, "т").
|
||||
|
||||
Предложный коротко: на #vref(<case-figure-a>, "п").
|
||||
|
||||
Явная строчная форма: на #vref(<case-figure-a>, capitalized: false).
|
||||
|
||||
Полное название: на #vref(<case-table>, "предложный").
|
||||
|
||||
Формула: согласно #vref(<case-equation>, "дательный").
|
||||
|
||||
Раздел: в #vref(<case-section>).
|
||||
|
||||
Группа по умолчанию: на #vrefs((<case-figure-a>, <case-figure-b>)).
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
= Исходные данные <test-appendix-source>
|
||||
|
||||
Первое приложение подключено отдельным файлом.
|
||||
|
||||
#figure(
|
||||
rect(width: 4cm, height: 1.5cm, fill: luma(235), stroke: 0.5pt),
|
||||
caption: [Схема первого приложения],
|
||||
) <test-appendix-figure>
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
= Проверочные расчёты <test-appendix-calculations>
|
||||
|
||||
Второе приложение автоматически продолжает кириллическую нумерацию.
|
||||
|
||||
#figure(
|
||||
table(
|
||||
columns: (1fr, 1fr),
|
||||
[Параметр], [Значение],
|
||||
[Коэффициент], [1,3],
|
||||
),
|
||||
caption: [Таблица второго приложения],
|
||||
) <test-appendix-table>
|
||||
@@ -0,0 +1,38 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, vref, vrefs
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.report(
|
||||
title: "Проверка файловых приложений",
|
||||
year: 2026,
|
||||
executors: (),
|
||||
appendices: (
|
||||
path("appendices/01-source-data.typ"),
|
||||
path("appendices/02-calculations.typ"),
|
||||
),
|
||||
appendix_numbering: "cyrillic",
|
||||
appendix_start: 1,
|
||||
show_title_page: false,
|
||||
show_executors: false,
|
||||
show_outline: true,
|
||||
),
|
||||
options: (mode: "clean-copy", media-policy: "hide"),
|
||||
)
|
||||
|
||||
= ОСНОВНОЙ ТЕКСТ
|
||||
|
||||
Стандартная ссылка: @test-appendix-source.
|
||||
|
||||
Именительный: #vref(<test-appendix-source>, "и").
|
||||
|
||||
Родительный: без #vref(<test-appendix-source>, "р").
|
||||
|
||||
Дательный: к #vref(<test-appendix-source>, "д").
|
||||
|
||||
Винительный: вижу #vref(<test-appendix-source>, "в").
|
||||
|
||||
Творительный: перед #vref(<test-appendix-source>, "т").
|
||||
|
||||
Предложный по умолчанию: в #vref(<test-appendix-source>).
|
||||
|
||||
Группа приложений: в #vrefs((<test-appendix-source>, <test-appendix-calculations>)).
|
||||
@@ -0,0 +1,71 @@
|
||||
#import "/.template/lib/report.typ": report
|
||||
#import "/.template/lib/company.typ": company-defaults
|
||||
#import "/.template/lib/components.typ": corp_table, formula
|
||||
#import "/.template/lib/appendices.typ": make_appendices
|
||||
#import "/.template/lib/references.typ": vref, eqref
|
||||
|
||||
#show: report.with(
|
||||
..company-defaults(company-id: "test-company"),
|
||||
udk: "000.000",
|
||||
director_date: "«26» августа 2026 г.",
|
||||
is_research: true,
|
||||
title: "Тестовый отчёт",
|
||||
theme: "Регрессионная проверка шаблона",
|
||||
is_intermediate: true,
|
||||
stage_num: 1,
|
||||
vol_num: 1,
|
||||
contract_num: "TEST-001",
|
||||
contract_date: "«26» августа 2026",
|
||||
year: 2026,
|
||||
executors: (
|
||||
("Ответственный исполнитель", "Тестов И.И.", path("/.template/companies/test-company/sign.svg"), 0.5cm),
|
||||
("Инженер", "Примеров П.П.", path("/.template/companies/test-company/sign.svg"), 0.5cm),
|
||||
),
|
||||
)
|
||||
|
||||
#heading(numbering: none)[ВВЕДЕНИЕ]
|
||||
|
||||
Этот документ фиксирует исходное поведение шаблона перед архитектурным рефакторингом.
|
||||
|
||||
= ОСНОВНОЙ РАЗДЕЛ
|
||||
|
||||
Сложные элементы должны сохранять нумерацию, подписи и ссылки.
|
||||
|
||||
#figure(
|
||||
image("/.template/companies/test-company/logo.svg", width: 35%),
|
||||
caption: [Синтетический логотип для визуального теста],
|
||||
) <baseline-figure>
|
||||
|
||||
Ссылка на рисунок: #vref(<baseline-figure>, grammatical-case: "вин").
|
||||
|
||||
#figure(
|
||||
corp_table(
|
||||
columns: (1fr, 1fr, 1fr),
|
||||
table.cell(rowspan: 2, align: center + horizon)[Параметр],
|
||||
table.cell(colspan: 2, align: center + horizon)[Значения],
|
||||
[Минимум], [Максимум],
|
||||
[Тестовая строка], [10], [20],
|
||||
[Вторая строка], [30], [40],
|
||||
),
|
||||
caption: [Таблица с многострочной шапкой],
|
||||
) <baseline-table>
|
||||
|
||||
Ссылка на таблицу: #vref(<baseline-table>, grammatical-case: "предл").
|
||||
|
||||
#formula($a^2 + b^2 = c^2$) <baseline-equation>
|
||||
|
||||
Ссылка на формулу: #eqref(<baseline-equation>).
|
||||
|
||||
#pagebreak()
|
||||
#heading(numbering: none)[ЗАКЛЮЧЕНИЕ]
|
||||
|
||||
Компиляция и визуальное сравнение подтверждают стабильность ключевых элементов.
|
||||
|
||||
#pagebreak()
|
||||
#heading(numbering: none)[СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ]
|
||||
#bibliography("/assets/references.bib", title: none, style: "gost-r-705-2008-numeric")
|
||||
|
||||
#show: make_appendices
|
||||
= ТЕСТОВОЕ ПРИЛОЖЕНИЕ
|
||||
|
||||
Содержимое приложения используется для проверки кириллической нумерации.
|
||||
@@ -0,0 +1,3 @@
|
||||
= Тестовое приложение <new-test-appendix>
|
||||
|
||||
Приложение создано как самостоятельный Typst-файл.
|
||||
@@ -0,0 +1,87 @@
|
||||
#import "/.template/lib/api.typ": document, bibliography-section
|
||||
#import "/.template/lib/presentation/profiles/report.typ": report-profile
|
||||
#import "/.template/lib/presentation/components.typ": corp_table, formula
|
||||
#import "/.template/lib/presentation/references.typ": vref, vrefs, eqref
|
||||
|
||||
#let appendices = (
|
||||
path("appendix.typ"),
|
||||
)
|
||||
|
||||
#let bibliographies = (
|
||||
bibliography-section(
|
||||
"normative",
|
||||
path("normative.bib"),
|
||||
title: [НОРМАТИВНЫЕ ИСТОЧНИКИ],
|
||||
group: "report-sources",
|
||||
),
|
||||
bibliography-section(
|
||||
"science",
|
||||
path("science.bib"),
|
||||
title: [НАУЧНЫЕ ИСТОЧНИКИ],
|
||||
group: "report-sources",
|
||||
),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: report-profile(
|
||||
title: "Новый модульный отчёт",
|
||||
theme: "Проверка DDD-профиля",
|
||||
udk: "000.000",
|
||||
director_date: "«26» августа 2026 г.",
|
||||
is_research: true,
|
||||
is_intermediate: true,
|
||||
stage_number: 1,
|
||||
volume_number: 1,
|
||||
contract_number: "DDD-001",
|
||||
contract_date: "«26» августа 2026",
|
||||
city: "Пример",
|
||||
year: 2026,
|
||||
executors: (
|
||||
("Ответственный исполнитель", "Тестов И.И.", path("/.template/companies/test-company/sign.svg"), 0.5cm),
|
||||
),
|
||||
appendices: appendices,
|
||||
appendix_numbering: "cyrillic",
|
||||
appendix_start: 1,
|
||||
bibliographies: bibliographies,
|
||||
),
|
||||
)
|
||||
|
||||
#heading(numbering: none)[ВВЕДЕНИЕ]
|
||||
|
||||
Профиль отчёта использует нормативный источник @normative-test и научную публикацию @science-test.
|
||||
|
||||
Тестовое содержимое вынесено в #vref(<new-test-appendix>).
|
||||
|
||||
= ПРОВЕРКА ССЫЛОК
|
||||
|
||||
#figure(
|
||||
rect(width: 3cm, height: 1.5cm, fill: rgb("fbb20d")),
|
||||
caption: [Первый тестовый рисунок],
|
||||
) <new-figure-a>
|
||||
|
||||
#figure(
|
||||
circle(radius: 0.7cm, fill: rgb("e39f49")),
|
||||
caption: [Второй тестовый рисунок],
|
||||
) <new-figure-b>
|
||||
|
||||
Одна ссылка: #vref(<new-figure-a>, grammatical-case: "предл").
|
||||
|
||||
Несколько ссылок: #vrefs((<new-figure-a>, <new-figure-b>), grammatical-case: "вин").
|
||||
|
||||
#figure(
|
||||
corp_table(
|
||||
columns: 2,
|
||||
[Параметр], [Значение],
|
||||
[Архитектура], [Изолированная],
|
||||
),
|
||||
caption: [Проверочная таблица],
|
||||
)
|
||||
|
||||
#formula($x^2 + y^2 = z^2$) <new-equation>
|
||||
|
||||
Формула: #eqref(<new-equation>).
|
||||
|
||||
#heading(numbering: none)[ЗАКЛЮЧЕНИЕ]
|
||||
|
||||
Новый отчёт собирается через `document` и `report-profile`.
|
||||
@@ -0,0 +1,6 @@
|
||||
@book{normative-test,
|
||||
title={Синтетический нормативный источник},
|
||||
author={{Тестовый регулятор}},
|
||||
year={2026},
|
||||
publisher={Тестовое издательство}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@article{science-test,
|
||||
title={Синтетическое исследование устойчивости шаблонов},
|
||||
author={Тестов, И. И.},
|
||||
year={2026},
|
||||
journal={Журнал тестовых данных},
|
||||
volume={1},
|
||||
pages={1--10}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, load-company
|
||||
|
||||
#let enabled(name) = sys.inputs.at(name, default: "true") == "true"
|
||||
#let company = load-company("test-company", signature: none, stamp: none)
|
||||
#let executors = (
|
||||
("Ответственный исполнитель", "Тестов И.И.", none, 0cm),
|
||||
)
|
||||
|
||||
#show: document.with(
|
||||
company: company,
|
||||
profile: profiles.report(
|
||||
title: "Контрольный отчёт",
|
||||
theme: "Независимые служебные страницы",
|
||||
city: "Екатеринбург",
|
||||
year: 2026,
|
||||
executors: executors,
|
||||
show_title_page: enabled("title"),
|
||||
show_executors: enabled("executors"),
|
||||
show_outline: enabled("outline"),
|
||||
),
|
||||
options: (
|
||||
mode: "clean-copy",
|
||||
watermark: none,
|
||||
media-policy: "hide",
|
||||
diagnostics: true,
|
||||
),
|
||||
)
|
||||
|
||||
= ОСНОВНОЙ ТЕКСТ
|
||||
|
||||
Основной текст контрольного отчёта начинается без пустой промежуточной страницы.
|
||||
@@ -0,0 +1,136 @@
|
||||
#import "/.template/lib/index.typ": document, profiles, corp-table, formula, vref, vrefs, eqref
|
||||
|
||||
#let stress-rows = range(1, 73).map(index => (
|
||||
[#index],
|
||||
[
|
||||
#if index == 3 {
|
||||
[Составной параметр:
|
||||
- первый вложенный пункт;
|
||||
- второй вложенный пункт с длинным пояснением внутри ячейки.]
|
||||
} else {
|
||||
[Параметр #index с достаточно длинным наименованием для проверки переноса строк]
|
||||
}
|
||||
],
|
||||
[
|
||||
#if index == 4 {
|
||||
[Значение #index. #link("https://example.invalid/table-cell")[Ссылка внутри ячейки] сохраняет кликабельность и не меняет отступы.]
|
||||
} else {
|
||||
[Значение #index. Текст ячейки переносится, но сохраняет одинаковые внутренние поля и межстрочный интервал.]
|
||||
}
|
||||
],
|
||||
)).flatten()
|
||||
|
||||
#show: document.with(
|
||||
company: "test-company",
|
||||
profile: profiles.report(
|
||||
title: "Стресс-тест типографики",
|
||||
theme: "Таблицы, списки, формулы и грамматические ссылки",
|
||||
year: 2026,
|
||||
executors: (),
|
||||
),
|
||||
)
|
||||
|
||||
= КОНТЕКСТНЫЕ ИНТЕРВАЛЫ
|
||||
|
||||
== Заголовок перед абзацем
|
||||
|
||||
Обычный абзац после заголовка должен иметь устойчивую красную строку, нормативный межстрочный интервал и не зависеть от элемента, который находился перед заголовком.
|
||||
|
||||
== Заголовок перед маркированным списком
|
||||
|
||||
- Первый уровень маркированного списка;
|
||||
- второй уровень с длинным текстом, который переносится на новую строку без смещения маркера;
|
||||
- третий уровень.
|
||||
- Возврат на первый уровень.
|
||||
|
||||
== Заголовок перед нумерованным списком
|
||||
|
||||
+ Основной пункт
|
||||
+ Второй уровень
|
||||
+ Третий уровень
|
||||
+ Следующий основной пункт
|
||||
|
||||
== Соседние заголовки
|
||||
=== Заголовок третьего уровня
|
||||
==== Заголовок четвёртого уровня
|
||||
|
||||
Текст после цепочки заголовков.
|
||||
|
||||
= ССЫЛКИ С ПАДЕЖАМИ <stress-section>
|
||||
|
||||
#figure(
|
||||
rect(width: 3cm, height: 1.3cm, fill: rgb("fbb20d")),
|
||||
caption: [Первый контрольный рисунок],
|
||||
) <stress-figure-a>
|
||||
|
||||
#figure(
|
||||
circle(radius: 0.65cm, fill: rgb("e39f49")),
|
||||
caption: [Второй контрольный рисунок],
|
||||
) <stress-figure-b>
|
||||
|
||||
#formula($a^2 + b^2 = c^2$) <stress-equation>
|
||||
|
||||
Именительный: #vref(<stress-figure-a>, grammatical-case: "имен").
|
||||
Родительный: без #vref(<stress-figure-a>, grammatical-case: "род").
|
||||
Дательный: к #vref(<stress-figure-a>, grammatical-case: "дат").
|
||||
Винительный: см. #vref(<stress-figure-a>, grammatical-case: "вин").
|
||||
Творительный: перед #vref(<stress-figure-a>, grammatical-case: "тв").
|
||||
Предложный: на #vref(<stress-figure-a>, grammatical-case: "предл").
|
||||
|
||||
Групповая ссылка: на #vrefs(
|
||||
(<stress-figure-a>, <stress-figure-b>),
|
||||
grammatical-case: "предл",
|
||||
).
|
||||
|
||||
Формула в дательном падеже: к #vref(<stress-equation>, grammatical-case: "дат"); короткая ссылка #eqref(<stress-equation>).
|
||||
|
||||
Ссылка на раздел: в #vref(<stress-section>, grammatical-case: "предл").
|
||||
|
||||
Внешняя ссылка: #link("https://example.invalid/specification")[контрольная спецификация].
|
||||
|
||||
= МНОГОСТРАНИЧНАЯ ТАБЛИЦА
|
||||
|
||||
#figure(
|
||||
corp-table(
|
||||
columns: (1.2cm, 1.7fr, 2.3fr),
|
||||
header: (
|
||||
table.cell(rowspan: 2, align: center + horizon)[№],
|
||||
table.cell(colspan: 2, align: center + horizon)[Контрольные данные],
|
||||
[Параметр],
|
||||
[Описание и значение],
|
||||
),
|
||||
body: stress-rows,
|
||||
repeat_header: true,
|
||||
continuation: true,
|
||||
continuation_text: "Продолжение таблицы",
|
||||
body-leading: 0.65em,
|
||||
body-inset: (x: 4pt, y: 3pt),
|
||||
justify: false,
|
||||
hyphenate: false,
|
||||
),
|
||||
caption: [Таблица, переходящая через несколько страниц],
|
||||
) <stress-table>
|
||||
|
||||
= ПРОВЕРКА ПОСЛЕ ТАБЛИЦЫ
|
||||
|
||||
Текст после таблицы не должен прилипать к последней строке. Ссылка на #vref(<stress-table>, grammatical-case: "вин") остаётся рабочей.
|
||||
|
||||
#pagebreak()
|
||||
= ВЫСОКАЯ СТРОКА ТАБЛИЦЫ
|
||||
|
||||
#let tall-cell = range(1, 66).map(index => [Строка высокой ячейки #index. Содержимое одной строки таблицы продолжается без потери границ.]).join(linebreak())
|
||||
|
||||
#figure(
|
||||
corp-table(
|
||||
columns: (3cm, 1fr),
|
||||
header: ([Идентификатор], [Высокая ячейка]),
|
||||
body: ([ROW-ONE], tall-cell),
|
||||
row_breakable: true,
|
||||
page_break: "fit",
|
||||
repeat_header: true,
|
||||
continuation: true,
|
||||
continuation_text: "Продолжение высокой таблицы",
|
||||
justify: false,
|
||||
),
|
||||
caption: [Одна высокая строка, переходящая через страницы],
|
||||
) <tall-row-table>
|
||||
@@ -0,0 +1,542 @@
|
||||
{
|
||||
"version": 1,
|
||||
"typst_min": "0.15.1",
|
||||
"creation_timestamp": 1787702400,
|
||||
"dpi": 110,
|
||||
"visual_threshold": 0.001,
|
||||
"unit_tests": [
|
||||
{
|
||||
"id": "legacy-numbering",
|
||||
"module": ".template/development/tests/domain/legacy-numbering.typ"
|
||||
},
|
||||
{
|
||||
"id": "core-domain",
|
||||
"module": ".template/development/tests/domain/core.typ"
|
||||
},
|
||||
{
|
||||
"id": "profile-domain",
|
||||
"module": ".template/development/tests/domain/profiles.typ"
|
||||
}
|
||||
],
|
||||
"fixtures": [
|
||||
{
|
||||
"id": "core-smoke",
|
||||
"entry": ".template/development/tests/fixtures/core-smoke/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"DDD CORE SMOKE",
|
||||
"ООО «Тестовая компания»",
|
||||
"Изолированное ядро успешно собрано"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "facade-smoke",
|
||||
"entry": ".template/development/tests/fixtures/facade-smoke/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ПУБЛИЧНЫЙ ФАСАД",
|
||||
"Корневой файл экспортирует единый фасад"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "default-report",
|
||||
"entry": ".template/development/tests/fixtures/default-report/main.typ",
|
||||
"expected_pages": 3,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ПРОВЕРКА ПРОФИЛЯ ПО УМОЛЧАНИЮ",
|
||||
"Корневой фасад формирует отчёт"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-full",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "true", "executors": "true", "outline": "true"},
|
||||
"expected_pages": 4,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"КОНТРОЛЬНЫЙ ОТЧЁТ",
|
||||
"СПИСОК ИСПОЛНИТЕЛЕЙ",
|
||||
"СОДЕРЖАНИЕ",
|
||||
"ОСНОВНОЙ ТЕКСТ"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-outline-only",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "false", "executors": "false", "outline": "true"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["СОДЕРЖАНИЕ", "ОСНОВНОЙ ТЕКСТ"],
|
||||
"forbidden_text": ["КОНТРОЛЬНЫЙ ОТЧЁТ", "СПИСОК ИСПОЛНИТЕЛЕЙ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-title-only",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "true", "executors": "false", "outline": "false"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["КОНТРОЛЬНЫЙ ОТЧЁТ", "ОСНОВНОЙ ТЕКСТ"],
|
||||
"forbidden_text": ["СПИСОК ИСПОЛНИТЕЛЕЙ", "СОДЕРЖАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-executors-only",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "false", "executors": "true", "outline": "false"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["СПИСОК ИСПОЛНИТЕЛЕЙ", "ОСНОВНОЙ ТЕКСТ"],
|
||||
"forbidden_text": ["КОНТРОЛЬНЫЙ ОТЧЁТ", "СОДЕРЖАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-service-pages-body-only",
|
||||
"entry": ".template/development/tests/fixtures/report-service-pages/main.typ",
|
||||
"inputs": {"title": "false", "executors": "false", "outline": "false"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["ОСНОВНОЙ ТЕКСТ", "Основной текст контрольного отчёта"],
|
||||
"forbidden_text": ["КОНТРОЛЬНЫЙ ОТЧЁТ", "СПИСОК ИСПОЛНИТЕЛЕЙ", "СОДЕРЖАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "presentation-smoke",
|
||||
"entry": ".template/development/tests/fixtures/presentation-smoke/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ПРОВЕРКА КОМПОНЕНТОВ",
|
||||
"Общая presentation-основа",
|
||||
"Media slot"
|
||||
],
|
||||
"snapshot_pages": [1]
|
||||
},
|
||||
{
|
||||
"id": "report-new",
|
||||
"entry": ".template/development/tests/fixtures/report-new/main.typ",
|
||||
"expected_pages": 7,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Проверка DDD-профиля",
|
||||
"НОРМАТИВНЫЕ ИСТОЧНИКИ",
|
||||
"НАУЧНЫЕ ИСТОЧНИКИ",
|
||||
"Приложение А"
|
||||
],
|
||||
"snapshot_pages": [1, 2, 5, 6]
|
||||
},
|
||||
{
|
||||
"id": "report-stress",
|
||||
"entry": ".template/development/tests/fixtures/report-stress/main.typ",
|
||||
"expected_pages": 12,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Таблицы, списки, формулы и грамматические ссылки",
|
||||
"1. Основной пункт",
|
||||
"а) Второй уровень",
|
||||
"1) Третий уровень",
|
||||
"на Рисунках 2.1 и 2.2",
|
||||
"контрольная спецификация",
|
||||
"Продолжение таблицы 3.1",
|
||||
"Ссылка на Таблицу 3.1",
|
||||
"Продолжение высокой таблицы 5.1",
|
||||
"Строка высокой ячейки 65."
|
||||
],
|
||||
"required_text_counts": {
|
||||
"Контрольные данные": 6,
|
||||
"Продолжение таблицы 3.1": 5,
|
||||
"Высокая ячейка": 3,
|
||||
"Продолжение высокой таблицы 5.1": 2
|
||||
},
|
||||
"minimum_external_links": 2,
|
||||
"minimum_internal_links": 8,
|
||||
"snapshot_pages": [3, 4, 5, 9, 10, 11, 12]
|
||||
},
|
||||
{
|
||||
"id": "reference-cases",
|
||||
"entry": ".template/development/tests/fixtures/reference-cases/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Предложный по умолчанию: на Рисунке",
|
||||
"Именительный коротко: Рисунок",
|
||||
"Родительный коротко: без Рисунка",
|
||||
"Дательный коротко: к Рисунку",
|
||||
"Винительный коротко: вижу Рисунок",
|
||||
"Творительный коротко: перед Рисунком",
|
||||
"Предложный коротко: на Рисунке",
|
||||
"Явная строчная форма: на рисунке",
|
||||
"Полное название: на Таблице",
|
||||
"Формула: согласно Формуле",
|
||||
"Раздел: в Разделе",
|
||||
"Группа по умолчанию: на Рисунках"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-appendices",
|
||||
"entry": ".template/development/tests/fixtures/report-appendices/main.typ",
|
||||
"expected_pages": 4,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Приложение А. Исходные данные",
|
||||
"Приложение Б. Проверочные расчёты",
|
||||
"Стандартная ссылка: Приложение А",
|
||||
"Именительный: Приложение А",
|
||||
"Родительный: без Приложения А",
|
||||
"Дательный: к Приложению А",
|
||||
"Винительный: вижу Приложение А",
|
||||
"Творительный: перед Приложением А",
|
||||
"Предложный по умолчанию: в Приложении А",
|
||||
"Группа приложений: в Приложениях А и Б",
|
||||
"Рисунок А.1",
|
||||
"Таблица Б.1"
|
||||
],
|
||||
"minimum_internal_links": 8,
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "letter",
|
||||
"entry": ".template/development/tests/fixtures/letter/main.typ",
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"О НАПРАВЛЕНИИ МАТЕРИАЛОВ",
|
||||
"Приложение 1: Краткая спецификация",
|
||||
"КРАТКАЯ СПЕЦИФИКАЦИЯ"
|
||||
],
|
||||
"snapshot_pages": [1, 2]
|
||||
},
|
||||
{
|
||||
"id": "commercial-offer",
|
||||
"entry": ".template/development/tests/fixtures/commercial-offer/main.typ",
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ТЕХНИКО-КОММЕРЧЕСКОЕ ПРЕДЛОЖЕНИЕ",
|
||||
"1 250 000 руб.",
|
||||
"ТЕХНИЧЕСКОЕ ЗАДАНИЕ"
|
||||
],
|
||||
"snapshot_pages": [1, 2]
|
||||
},
|
||||
{
|
||||
"id": "contract",
|
||||
"entry": ".template/development/tests/fixtures/contract/main.typ",
|
||||
"expected_pages": 3,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ДОГОВОР ОКАЗАНИЯ УСЛУГ",
|
||||
"РЕКВИЗИТЫ И ПОДПИСИ СТОРОН",
|
||||
"СПЕЦИФИКАЦИЯ"
|
||||
],
|
||||
"snapshot_pages": [1, 2, 3]
|
||||
},
|
||||
{
|
||||
"id": "root-report",
|
||||
"entry": "main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ГЕОМЕХАНИЧЕСКОЕ ОБОСНОВАНИЕ УСТОЙЧИВОСТИ БОРТОВ",
|
||||
"СПИСОК ИСПОЛНИТЕЛЕЙ",
|
||||
"Результаты проверочных расчётов",
|
||||
"СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ",
|
||||
"Приложение А. Исходные данные",
|
||||
"Приложение Б. Дополнительные расчёты",
|
||||
"Рисунок Б.1"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-report",
|
||||
"entry": "docs/examples/documents/report/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"Результаты проверочных расчётов",
|
||||
"СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ",
|
||||
"Приложение А. Исходные данные",
|
||||
"Приложение Б. Дополнительные расчёты",
|
||||
"Рисунок Б.1"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-letter",
|
||||
"entry": "docs/examples/documents/letter/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["О НАПРАВЛЕНИИ МАТЕРИАЛОВ ЭТАПА 1", "Уважаемый Иван Иванович"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-commercial-offer",
|
||||
"entry": "docs/examples/documents/commercial-offer/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Геомеханическое сопровождение горных работ", "Состав и результат работ", "ТЕХНИЧЕСКОЕ ЗАДАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-contract",
|
||||
"entry": "docs/examples/documents/contract/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["ДОГОВОР ОКАЗАНИЯ УСЛУГ", "ОТВЕТСТВЕННОСТЬ СТОРОН", "ТЕХНИЧЕСКОЕ ЗАДАНИЕ"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-draft",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "report", "mode": "draft"},
|
||||
"expected_pages": 3,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме draft"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-clean-copy",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "report", "mode": "clean-copy"},
|
||||
"expected_pages": 3,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме clean-copy"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "letter-draft",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "letter", "mode": "draft"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме draft"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "letter-clean-copy",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "letter", "mode": "clean-copy"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме clean-copy"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "commercial-offer-draft",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "commercial-offer", "mode": "draft"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме draft"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "commercial-offer-clean-copy",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "commercial-offer", "mode": "clean-copy"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме clean-copy"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "contract-draft",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "contract", "mode": "draft"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме draft"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "contract-clean-copy",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"inputs": {"profile": "contract", "mode": "clean-copy"},
|
||||
"expected_pages": 2,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["Документ собран в режиме clean-copy"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "no-media-placeholder",
|
||||
"entry": ".template/development/tests/fixtures/no-media/main.typ",
|
||||
"inputs": {"policy": "placeholder"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["политикой placeholder"],
|
||||
"snapshot_pages": [1]
|
||||
},
|
||||
{
|
||||
"id": "no-media-hide",
|
||||
"entry": ".template/development/tests/fixtures/no-media/main.typ",
|
||||
"inputs": {"policy": "hide"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["политикой hide"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "no-media-reserve-space",
|
||||
"entry": ".template/development/tests/fixtures/no-media/main.typ",
|
||||
"inputs": {"policy": "reserve-space"},
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": ["политикой reserve-space"],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "report-baseline",
|
||||
"entry": ".template/development/tests/fixtures/report-baseline/main.typ",
|
||||
"expected_pages": 7,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"ТЕСТОВЫЙ ОТЧЁТ",
|
||||
"СПИСОК ИСПОЛНИТЕЛЕЙ",
|
||||
"СОДЕРЖАНИЕ",
|
||||
"Сложные элементы должны сохранять",
|
||||
"ТЕСТОВОЕ ПРИЛОЖЕНИЕ"
|
||||
],
|
||||
"snapshot_pages": [1, 2, 3, 4]
|
||||
},
|
||||
{
|
||||
"id": "list-counters",
|
||||
"entry": ".template/development/tests/fixtures/list-counters/main.typ",
|
||||
"expected_pages": 1,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"1. Арабские: первый",
|
||||
"2. Арабские: второй",
|
||||
"3. Арабские: третий",
|
||||
"А. Кириллица верхняя: первый",
|
||||
"Б. Кириллица верхняя: второй",
|
||||
"В. Кириллица верхняя: третий",
|
||||
"а. Кириллица нижняя: первый",
|
||||
"б. Кириллица нижняя: второй",
|
||||
"в. Кириллица нижняя: третий",
|
||||
"A. Латиница верхняя: первый",
|
||||
"B. Латиница верхняя: второй",
|
||||
"C. Латиница верхняя: третий",
|
||||
"a. Латиница нижняя: первый",
|
||||
"b. Латиница нижняя: второй",
|
||||
"c. Латиница нижняя: третий",
|
||||
"01. Ведущий ноль: первый",
|
||||
"02. Ведущий ноль: второй",
|
||||
"03. Ведущий ноль: третий",
|
||||
"1.а. Второй уровень: первый",
|
||||
"1.б. Второй уровень: второй",
|
||||
"1.б.A. Третий уровень: первый",
|
||||
"1.б.B. Третий уровень: второй"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
},
|
||||
{
|
||||
"id": "public-formatting-guide",
|
||||
"entry": "docs/examples/formatting/main.typ",
|
||||
"expected_pages": 0,
|
||||
"expected_page_size": "A4",
|
||||
"required_text": [
|
||||
"РИСУНКИ",
|
||||
"ТАБЛИЦЫ",
|
||||
"Обычная встроенная таблица",
|
||||
"Многострочная шапка и объединение ячеек",
|
||||
"ФОРМУЛЫ",
|
||||
"СПИСКИ И ТЕКСТ",
|
||||
"Быстрая многоуровневая схема ГОСТ",
|
||||
"Полная десятичная нумерация",
|
||||
"Доступные типы счётчиков",
|
||||
"Произвольные разделители",
|
||||
"Маркеры вместо чисел",
|
||||
"Геометрия и интервалы",
|
||||
"Продолжение с нужного номера",
|
||||
"1. Первый уровень.",
|
||||
"а) Второй уровень.",
|
||||
"1.1.1. Вложенный пункт.",
|
||||
"II. Второй.",
|
||||
"III. Третий.",
|
||||
"ii. Второй.",
|
||||
"iii. Третий.",
|
||||
"B. Второй.",
|
||||
"C. Третий.",
|
||||
"b. Второй.",
|
||||
"c. Третий.",
|
||||
"02. Второй.",
|
||||
"03. Третий.",
|
||||
"Б. Второй.",
|
||||
"В. Третий.",
|
||||
"б. Второй.",
|
||||
"в. Третий.",
|
||||
"1.а)A. Третий уровень",
|
||||
"A)1. Цифровой уровень.",
|
||||
"§ 001: Пользовательский уровень",
|
||||
"• Маркер •.",
|
||||
"∙ Маркер ∙.",
|
||||
"‣ Маркер ‣.",
|
||||
"⁃ Маркер ⁃.",
|
||||
"◦ Маркер ◦.",
|
||||
"08. Восьмой пункт",
|
||||
"09. Следующий пункт"
|
||||
],
|
||||
"snapshot_pages": []
|
||||
}
|
||||
],
|
||||
"negative_fixtures": [
|
||||
{
|
||||
"id": "negative-letter",
|
||||
"entry": ".template/development/tests/fixtures/negative-letter/main.typ",
|
||||
"required_errors": ["Scientia / letter.recipient"]
|
||||
},
|
||||
{
|
||||
"id": "negative-offer",
|
||||
"entry": ".template/development/tests/fixtures/negative-offer/main.typ",
|
||||
"required_errors": ["Scientia / commercial-offer.terms.amount"]
|
||||
},
|
||||
{
|
||||
"id": "negative-contract",
|
||||
"entry": ".template/development/tests/fixtures/negative-contract/main.typ",
|
||||
"required_errors": ["Scientia / parties", "повторяющийся id"]
|
||||
},
|
||||
{
|
||||
"id": "negative-company",
|
||||
"entry": ".template/development/tests/fixtures/negative-company/main.typ",
|
||||
"required_errors": ["Scientia / load-company.id", "неизвестная компания"]
|
||||
},
|
||||
{
|
||||
"id": "negative-references",
|
||||
"entry": ".template/development/tests/fixtures/negative-references/main.typ",
|
||||
"required_errors": ["Scientia / vrefs", "ожидался label"]
|
||||
},
|
||||
{
|
||||
"id": "negative-reference-case",
|
||||
"entry": ".template/development/tests/fixtures/negative-reference-case/main.typ",
|
||||
"required_errors": ["Scientia / reference", "неизвестный падеж"]
|
||||
},
|
||||
{
|
||||
"id": "negative-list-scheme",
|
||||
"entry": ".template/development/tests/fixtures/negative-list-scheme/main.typ",
|
||||
"required_errors": ["Scientia / numbered-list.scheme", "неизвестная схема"]
|
||||
},
|
||||
{
|
||||
"id": "negative-report-appendices",
|
||||
"entry": ".template/development/tests/fixtures/negative-report-appendices/main.typ",
|
||||
"required_errors": ["Scientia / report.appendices", "каждый элемент должен быть path"]
|
||||
}
|
||||
],
|
||||
"compile_matrices": [
|
||||
{
|
||||
"id": "company-profile-matrix",
|
||||
"entry": ".template/development/tests/fixtures/modes/main.typ",
|
||||
"parameters": {
|
||||
"company": ["scientia", "technology", "too", "test-company"],
|
||||
"profile": ["report", "letter", "commercial-offer", "contract"],
|
||||
"mode": ["final"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
TEST_ROOT = ROOT / ".template" / "development" / "tests"
|
||||
MANIFEST_PATH = TEST_ROOT / "manifest.json"
|
||||
SNAPSHOT_ROOT = TEST_ROOT / "snapshots"
|
||||
|
||||
|
||||
class TestFailure(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def check_architecture() -> None:
|
||||
"""Проверяет направление зависимостей без запуска Typst renderer."""
|
||||
template_root = ROOT / ".template" / "lib"
|
||||
import_pattern = re.compile(r'#import\s+"([^"]+)"')
|
||||
graph: dict[Path, set[Path]] = {}
|
||||
|
||||
for source in template_root.rglob("*.typ"):
|
||||
graph[source.resolve()] = set()
|
||||
text = source.read_text(encoding="utf-8")
|
||||
source_area = source.relative_to(template_root).parts[0]
|
||||
code_without_line_comments = re.sub(r"//.*", "", text)
|
||||
if source_area == "domain":
|
||||
forbidden_runtime = re.search(
|
||||
r"(?:#(?:set|show)\b|\b(?:page|place|image|query|state)\s*\()",
|
||||
code_without_line_comments,
|
||||
)
|
||||
if forbidden_runtime:
|
||||
raise TestFailure(
|
||||
f"Domain-модуль {source.relative_to(ROOT)} содержит presentation/runtime вызов "
|
||||
f"{forbidden_runtime.group(0)!r}."
|
||||
)
|
||||
for raw_target in import_pattern.findall(text):
|
||||
if raw_target.startswith("@"):
|
||||
continue
|
||||
target = (source.parent / raw_target).resolve()
|
||||
try:
|
||||
relative = target.relative_to(template_root.resolve())
|
||||
except ValueError:
|
||||
continue
|
||||
if target.suffix == ".typ":
|
||||
graph[source.resolve()].add(target)
|
||||
|
||||
target_area = relative.parts[0]
|
||||
forbidden = {
|
||||
"domain": {"application", "infrastructure", "presentation"},
|
||||
"infrastructure": {"application", "presentation"},
|
||||
"application": {"presentation"},
|
||||
}.get(source_area, set())
|
||||
if target_area in forbidden:
|
||||
raise TestFailure(
|
||||
"Нарушена DDD-граница: "
|
||||
f"{source.relative_to(ROOT)} -> {target.relative_to(ROOT)}"
|
||||
)
|
||||
|
||||
if re.search(r'#import\s+"[^\"]*(?:main\.typ|chapters/|assets/|\.private/)', text):
|
||||
raise TestFailure(
|
||||
f"Library-модуль {source.relative_to(ROOT)} импортирует пользовательский слой."
|
||||
)
|
||||
|
||||
visiting: set[Path] = set()
|
||||
visited: set[Path] = set()
|
||||
|
||||
def visit(node: Path, stack: list[Path]) -> None:
|
||||
if node in visiting:
|
||||
cycle = stack[stack.index(node):] + [node]
|
||||
rendered = " -> ".join(str(item.relative_to(ROOT)) for item in cycle)
|
||||
raise TestFailure(f"Циклические Typst-импорты: {rendered}")
|
||||
if node in visited:
|
||||
return
|
||||
visiting.add(node)
|
||||
stack.append(node)
|
||||
for target in graph.get(node, set()):
|
||||
if target in graph:
|
||||
visit(target, stack)
|
||||
stack.pop()
|
||||
visiting.remove(node)
|
||||
visited.add(node)
|
||||
|
||||
for node in graph:
|
||||
visit(node, [])
|
||||
|
||||
|
||||
def check_workspace() -> None:
|
||||
"""Проверяет минимальный корень и синхронизируемую конфигурацию VS Code."""
|
||||
allowed_directories = {
|
||||
".git",
|
||||
".private",
|
||||
".template",
|
||||
".vscode",
|
||||
"assets",
|
||||
"chapters",
|
||||
"docs",
|
||||
}
|
||||
actual_directories = {item.name for item in ROOT.iterdir() if item.is_dir()}
|
||||
unexpected = actual_directories - allowed_directories
|
||||
if unexpected:
|
||||
raise TestFailure(
|
||||
"В корне обнаружены лишние каталоги: " + ", ".join(sorted(unexpected))
|
||||
)
|
||||
|
||||
required_files = {
|
||||
".gitignore",
|
||||
"README.md",
|
||||
"main.typ",
|
||||
}
|
||||
missing_files = {name for name in required_files if not (ROOT / name).is_file()}
|
||||
if missing_files:
|
||||
raise TestFailure(
|
||||
"В корне отсутствуют обязательные файлы: " + ", ".join(sorted(missing_files))
|
||||
)
|
||||
|
||||
extensions_path = ROOT / ".vscode" / "extensions.json"
|
||||
settings_path = ROOT / ".vscode" / "settings.json"
|
||||
extensions = json.loads(extensions_path.read_text(encoding="utf-8"))
|
||||
recommendations = set(extensions.get("recommendations", []))
|
||||
required_extensions = {
|
||||
"alefragnani.bookmarks",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"streetsidesoftware.code-spell-checker-russian",
|
||||
"ms-ceintl.vscode-language-pack-ru",
|
||||
"myriad-dreamin.tinymist",
|
||||
"wayou.vscode-todo-highlight",
|
||||
"gruntfuggly.todo-tree",
|
||||
"mhutchie.git-graph",
|
||||
"local.typst-typewriter",
|
||||
"zotst.zotst",
|
||||
}
|
||||
missing_extensions = required_extensions - recommendations
|
||||
if missing_extensions:
|
||||
raise TestFailure(
|
||||
"В extensions.json отсутствуют рекомендации: "
|
||||
+ ", ".join(sorted(missing_extensions))
|
||||
)
|
||||
|
||||
settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
||||
if settings.get("files.autoSave") != "afterDelay":
|
||||
raise TestFailure("Workspace должен включать files.autoSave=afterDelay.")
|
||||
if settings.get("files.exclude", {}).get(".template") is not True:
|
||||
raise TestFailure(".template должен быть скрыт в пользовательском Explorer VS Code.")
|
||||
if settings.get("files.exclude", {}).get(".vscode") is not True:
|
||||
raise TestFailure(".vscode должен быть скрыт в пользовательском Explorer VS Code.")
|
||||
if settings.get("files.exclude", {}).get("docs") is True:
|
||||
raise TestFailure("Публичная документация docs/ не должна быть скрыта.")
|
||||
if settings.get("typstTypewriter.navigator.mainFiles") != ["main.typ"]:
|
||||
raise TestFailure("Typewriter должен показывать единственный main.typ.")
|
||||
|
||||
gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8")
|
||||
if ".private/" not in gitignore:
|
||||
raise TestFailure("В .gitignore отсутствует защита каталога .private/.")
|
||||
if ".vscode/" in gitignore:
|
||||
raise TestFailure(".vscode должен синхронизироваться через Git.")
|
||||
|
||||
private_example = ROOT / "docs" / "examples" / "private" / "settings.typ"
|
||||
if not private_example.is_file():
|
||||
raise TestFailure("Отсутствует безопасный пример .private/settings.typ.")
|
||||
private_example_text = private_example.read_text(encoding="utf-8")
|
||||
for employee_id in (
|
||||
"musikhin", "guzeev", "fedorov", "ilyasov", "khimichev",
|
||||
"brusnicin", "ozornin", "buhartdinov", "tkachenko",
|
||||
):
|
||||
if f"{employee_id}:" not in private_example_text:
|
||||
raise TestFailure(
|
||||
f"В примере private settings отсутствует сотрудник {employee_id}."
|
||||
)
|
||||
|
||||
tasks = json.loads((ROOT / ".vscode" / "tasks.json").read_text(encoding="utf-8"))
|
||||
task_labels = {task.get("label") for task in tasks.get("tasks", [])}
|
||||
if "Scientia: собрать PDF с приватными данными" in task_labels:
|
||||
raise TestFailure("Отдельная private build task больше не должна использоваться.")
|
||||
if (ROOT / ".template" / "development" / "tools" / "build-with-private-assets.ps1").exists():
|
||||
raise TestFailure("Устаревший ZIP installer должен быть удалён.")
|
||||
|
||||
for company_id in ("scientia", "technology", "too"):
|
||||
company_path = ROOT / ".template" / "companies" / company_id / "data.json"
|
||||
company = json.loads(company_path.read_text(encoding="utf-8"))
|
||||
if company.get("sign_image") is not None or company.get("stamp_image") is not None:
|
||||
raise TestFailure(
|
||||
f"Публичный профиль {company_id} не должен содержать подпись или печать."
|
||||
)
|
||||
|
||||
public_docs = (
|
||||
"README.md",
|
||||
"documents.md",
|
||||
"formatting.md",
|
||||
"vscode.md",
|
||||
"git.md",
|
||||
"private-assets.md",
|
||||
"writing-style.md",
|
||||
"troubleshooting.md",
|
||||
)
|
||||
for name in public_docs:
|
||||
if not (ROOT / "docs" / name).is_file():
|
||||
raise TestFailure(f"Публичная документация неполна: docs/{name}.")
|
||||
|
||||
for starter in ("report", "letter", "commercial-offer", "contract"):
|
||||
starter_root = ROOT / "docs" / "examples" / "documents" / starter
|
||||
if not (starter_root / "main.typ").is_file() or not (starter_root / "chapters").is_dir():
|
||||
raise TestFailure(f"Публичный пример {starter} неполон.")
|
||||
|
||||
for obsolete in ("document.typ", "draft.typ", "clean-copy.typ"):
|
||||
if (ROOT / obsolete).exists():
|
||||
raise TestFailure(f"Лишняя точка входа в корне: {obsolete}.")
|
||||
|
||||
|
||||
def run(command: list[str], *, cwd: Path = ROOT) -> str:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=cwd,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
rendered = subprocess.list2cmdline(command)
|
||||
raise TestFailure(f"Команда завершилась с кодом {result.returncode}:\n{rendered}\n{result.stdout}")
|
||||
return result.stdout
|
||||
|
||||
|
||||
def find_executable(
|
||||
name: str,
|
||||
*,
|
||||
env_var: str | None = None,
|
||||
required: bool = True,
|
||||
) -> Path | None:
|
||||
if env_var and os.environ.get(env_var):
|
||||
candidate = Path(os.environ[env_var])
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
|
||||
located = shutil.which(name)
|
||||
if located:
|
||||
return Path(located)
|
||||
|
||||
executable = name + (".exe" if os.name == "nt" else "")
|
||||
candidates: list[Path] = []
|
||||
|
||||
poppler_bin = os.environ.get("POPPLER_BIN")
|
||||
if poppler_bin:
|
||||
candidates.append(Path(poppler_bin) / executable)
|
||||
|
||||
if os.name == "nt":
|
||||
candidates.append(
|
||||
Path.home()
|
||||
/ ".cache"
|
||||
/ "codex-runtimes"
|
||||
/ "codex-primary-runtime"
|
||||
/ "dependencies"
|
||||
/ "native"
|
||||
/ "poppler"
|
||||
/ "Library"
|
||||
/ "bin"
|
||||
/ executable
|
||||
)
|
||||
|
||||
for candidate in candidates:
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
|
||||
if required:
|
||||
raise TestFailure(
|
||||
f"Не найден исполняемый файл {name}. Добавьте его в PATH"
|
||||
+ (f" или задайте {env_var}" if env_var else "")
|
||||
+ ("/POPPLER_BIN." if name.startswith("pdf") else ".")
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def parse_version(value: str) -> tuple[int, ...]:
|
||||
match = re.search(r"(\d+)\.(\d+)\.(\d+)", value)
|
||||
if not match:
|
||||
raise TestFailure(f"Не удалось определить версию из строки: {value!r}")
|
||||
return tuple(int(part) for part in match.groups())
|
||||
|
||||
|
||||
def load_manifest() -> dict:
|
||||
return json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def compile_fixture(
|
||||
typst: Path,
|
||||
entry: Path,
|
||||
output: Path,
|
||||
creation_timestamp: int,
|
||||
inputs: dict[str, str] | None = None,
|
||||
) -> str:
|
||||
command = [
|
||||
str(typst),
|
||||
"compile",
|
||||
"--root",
|
||||
str(ROOT),
|
||||
"--creation-timestamp",
|
||||
str(creation_timestamp),
|
||||
]
|
||||
for key, value in sorted((inputs or {}).items()):
|
||||
command.extend(("--input", f"{key}={value}"))
|
||||
command.extend((str(entry), str(output)))
|
||||
return run(command)
|
||||
|
||||
|
||||
def compile_expected_failure(
|
||||
typst: Path,
|
||||
entry: Path,
|
||||
output: Path,
|
||||
creation_timestamp: int,
|
||||
required_errors: list[str],
|
||||
inputs: dict[str, str] | None = None,
|
||||
) -> None:
|
||||
command = [
|
||||
str(typst),
|
||||
"compile",
|
||||
"--root",
|
||||
str(ROOT),
|
||||
"--creation-timestamp",
|
||||
str(creation_timestamp),
|
||||
]
|
||||
for key, value in sorted((inputs or {}).items()):
|
||||
command.extend(("--input", f"{key}={value}"))
|
||||
command.extend((str(entry), str(output)))
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
raise TestFailure(f"{entry.relative_to(ROOT)}: ожидалась ошибка компиляции.")
|
||||
for marker in required_errors:
|
||||
if marker not in result.stdout:
|
||||
raise TestFailure(
|
||||
f"{entry.relative_to(ROOT)}: в diagnostic не найден маркер {marker!r}.\n"
|
||||
f"Фактический вывод:\n{result.stdout}"
|
||||
)
|
||||
|
||||
|
||||
def run_unit_test(typst: Path, module: str, creation_timestamp: int) -> None:
|
||||
expression = f'import "/{module.replace(os.sep, "/")}": run; run()'
|
||||
output = run(
|
||||
[
|
||||
str(typst),
|
||||
"eval",
|
||||
"--root",
|
||||
str(ROOT),
|
||||
"--creation-timestamp",
|
||||
str(creation_timestamp),
|
||||
expression,
|
||||
]
|
||||
).strip()
|
||||
if json.loads(output) != "ok":
|
||||
raise TestFailure(f"Unit-модуль {module} вернул неожиданное значение: {output}")
|
||||
|
||||
|
||||
def inspect_pdf(pdfinfo: Path, pdf: Path) -> tuple[int, str]:
|
||||
info = run([str(pdfinfo), str(pdf)])
|
||||
pages_match = re.search(r"^Pages:\s+(\d+)$", info, flags=re.MULTILINE)
|
||||
size_match = re.search(r"^Page size:.*\(([^)]+)\)$", info, flags=re.MULTILINE)
|
||||
if not pages_match or not size_match:
|
||||
raise TestFailure(f"Не удалось разобрать pdfinfo для {pdf}:\n{info}")
|
||||
return int(pages_match.group(1)), size_match.group(1).strip()
|
||||
|
||||
|
||||
def extract_text(pdftotext: Path | None, pdf: Path, target: Path) -> str:
|
||||
if pdftotext:
|
||||
run([str(pdftotext), "-layout", str(pdf), str(target)])
|
||||
return target.read_text(encoding="utf-8", errors="replace")
|
||||
|
||||
try:
|
||||
from pypdf import PdfReader
|
||||
except ImportError as exc:
|
||||
raise TestFailure(
|
||||
"Для проверки текста требуется pdftotext или Python-пакет pypdf."
|
||||
) from exc
|
||||
|
||||
reader = PdfReader(str(pdf))
|
||||
return "\n".join((page.extract_text() or "") for page in reader.pages)
|
||||
|
||||
|
||||
def count_pdf_links(pdf: Path) -> tuple[int, int]:
|
||||
try:
|
||||
from pypdf import PdfReader
|
||||
except ImportError as exc:
|
||||
raise TestFailure("Для проверки PDF-ссылок требуется Python-пакет pypdf.") from exc
|
||||
|
||||
external = internal = 0
|
||||
for page in PdfReader(str(pdf)).pages:
|
||||
for raw_annotation in page.get("/Annots", ()):
|
||||
annotation = raw_annotation.get_object()
|
||||
if annotation.get("/Subtype") != "/Link":
|
||||
continue
|
||||
action = annotation.get("/A")
|
||||
if action is not None and action.get_object().get("/URI") is not None:
|
||||
external += 1
|
||||
else:
|
||||
internal += 1
|
||||
return external, internal
|
||||
|
||||
|
||||
def render_page(pdftoppm: Path, pdf: Path, page: int, dpi: int, target: Path) -> Path:
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
run(
|
||||
[
|
||||
str(pdftoppm),
|
||||
"-png",
|
||||
"-r",
|
||||
str(dpi),
|
||||
"-f",
|
||||
str(page),
|
||||
"-l",
|
||||
str(page),
|
||||
"-singlefile",
|
||||
str(pdf),
|
||||
str(target.with_suffix("")),
|
||||
]
|
||||
)
|
||||
return target
|
||||
|
||||
|
||||
def image_difference(expected: Path, actual: Path) -> float:
|
||||
try:
|
||||
from PIL import Image, ImageChops
|
||||
except ImportError as exc:
|
||||
raise TestFailure("Для visual regression требуется пакет Pillow.") from exc
|
||||
|
||||
with Image.open(expected).convert("RGB") as left, Image.open(actual).convert("RGB") as right:
|
||||
if left.size != right.size:
|
||||
return 1.0
|
||||
diff = ImageChops.difference(left, right)
|
||||
histogram = diff.histogram()
|
||||
total = sum(value * (index % 256) for index, value in enumerate(histogram))
|
||||
maximum = left.width * left.height * 3 * 255
|
||||
return total / maximum
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Регрессионные тесты шаблона документов Scientia")
|
||||
parser.add_argument("--fixture", action="append", help="Запустить только fixture с указанным id")
|
||||
parser.add_argument("--compile-only", action="store_true", help="Не запускать PDF и visual проверки")
|
||||
parser.add_argument(
|
||||
"--update-snapshots",
|
||||
action="store_true",
|
||||
help="Явно обновить visual snapshots после ручного подтверждения",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
manifest = load_manifest()
|
||||
typst = find_executable("typst", env_var="TYPST_BIN")
|
||||
version_output = run([str(typst), "--version"]).strip()
|
||||
if parse_version(version_output) < parse_version(manifest["typst_min"]):
|
||||
raise TestFailure(
|
||||
f"Требуется Typst {manifest['typst_min']}+, найден {version_output}."
|
||||
)
|
||||
|
||||
pdfinfo = pdftotext = pdftoppm = None
|
||||
if not args.compile_only:
|
||||
pdfinfo = find_executable("pdfinfo")
|
||||
pdftotext = find_executable("pdftotext", required=False)
|
||||
pdftoppm = find_executable("pdftoppm")
|
||||
|
||||
creation_timestamp = int(manifest["creation_timestamp"])
|
||||
selected = set(args.fixture or [])
|
||||
known = {
|
||||
item["id"]
|
||||
for item in (
|
||||
*manifest["fixtures"],
|
||||
*manifest.get("negative_fixtures", []),
|
||||
*manifest.get("compile_matrices", []),
|
||||
)
|
||||
}
|
||||
unknown = selected - known
|
||||
if unknown:
|
||||
raise TestFailure(f"Неизвестные fixtures: {', '.join(sorted(unknown))}")
|
||||
|
||||
if not selected:
|
||||
print("[static] architecture")
|
||||
check_architecture()
|
||||
print("[static] workspace")
|
||||
check_workspace()
|
||||
|
||||
for unit in manifest.get("unit_tests", []):
|
||||
if selected:
|
||||
continue
|
||||
print(f"[unit] {unit['id']}")
|
||||
run_unit_test(typst, unit["module"], creation_timestamp)
|
||||
|
||||
temp_dir = Path(tempfile.mkdtemp(prefix="scientia-tests-"))
|
||||
failed = False
|
||||
try:
|
||||
for matrix in manifest.get("compile_matrices", []):
|
||||
if selected and matrix["id"] not in selected:
|
||||
continue
|
||||
keys = tuple(matrix["parameters"])
|
||||
values = tuple(matrix["parameters"][key] for key in keys)
|
||||
for combination in itertools.product(*values):
|
||||
inputs = dict(zip(keys, combination))
|
||||
suffix = ",".join(f"{key}={value}" for key, value in inputs.items())
|
||||
print(f"[matrix] {matrix['id']}[{suffix}]")
|
||||
safe_suffix = re.sub(r"[^A-Za-z0-9_.-]+", "-", suffix)
|
||||
compile_fixture(
|
||||
typst,
|
||||
ROOT / matrix["entry"],
|
||||
temp_dir / f"{matrix['id']}-{safe_suffix}.pdf",
|
||||
creation_timestamp,
|
||||
inputs=inputs,
|
||||
)
|
||||
|
||||
for fixture in manifest.get("negative_fixtures", []):
|
||||
if selected and fixture["id"] not in selected:
|
||||
continue
|
||||
fixture_id = fixture["id"]
|
||||
print(f"[negative] {fixture_id}")
|
||||
compile_expected_failure(
|
||||
typst,
|
||||
ROOT / fixture["entry"],
|
||||
temp_dir / f"{fixture_id}.pdf",
|
||||
creation_timestamp,
|
||||
fixture.get("required_errors", []),
|
||||
inputs=fixture.get("inputs"),
|
||||
)
|
||||
|
||||
for fixture in manifest["fixtures"]:
|
||||
if selected and fixture["id"] not in selected:
|
||||
continue
|
||||
|
||||
fixture_id = fixture["id"]
|
||||
print(f"[compile] {fixture_id}")
|
||||
entry = ROOT / fixture["entry"]
|
||||
output = temp_dir / f"{fixture_id}.pdf"
|
||||
compile_fixture(
|
||||
typst,
|
||||
entry,
|
||||
output,
|
||||
creation_timestamp,
|
||||
inputs=fixture.get("inputs"),
|
||||
)
|
||||
|
||||
if args.compile_only:
|
||||
continue
|
||||
|
||||
assert pdfinfo and pdftoppm
|
||||
pages, page_size = inspect_pdf(pdfinfo, output)
|
||||
expected_pages = int(fixture["expected_pages"])
|
||||
if expected_pages and pages != expected_pages:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: ожидалось {expected_pages} страниц, получено {pages}."
|
||||
)
|
||||
if page_size != fixture["expected_page_size"]:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: ожидался формат {fixture['expected_page_size']}, получен {page_size}."
|
||||
)
|
||||
|
||||
text = extract_text(pdftotext, output, temp_dir / f"{fixture_id}.txt")
|
||||
for marker in fixture.get("required_text", []):
|
||||
if marker not in text:
|
||||
raise TestFailure(f"{fixture_id}: в PDF не найден текстовый маркер {marker!r}.")
|
||||
for marker in fixture.get("forbidden_text", []):
|
||||
if marker in text:
|
||||
raise TestFailure(f"{fixture_id}: в PDF найден запрещённый текстовый маркер {marker!r}.")
|
||||
for marker, minimum in fixture.get("required_text_counts", {}).items():
|
||||
actual = text.count(marker)
|
||||
if actual < int(minimum):
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: маркер {marker!r} найден {actual} раз; "
|
||||
f"требуется не менее {minimum}."
|
||||
)
|
||||
if "minimum_external_links" in fixture or "minimum_internal_links" in fixture:
|
||||
external_links, internal_links = count_pdf_links(output)
|
||||
minimum_external = int(fixture.get("minimum_external_links", 0))
|
||||
minimum_internal = int(fixture.get("minimum_internal_links", 0))
|
||||
if external_links < minimum_external:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: внешних PDF-ссылок {external_links}; "
|
||||
f"требуется не менее {minimum_external}."
|
||||
)
|
||||
if internal_links < minimum_internal:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}: внутренних PDF-ссылок {internal_links}; "
|
||||
f"требуется не менее {minimum_internal}."
|
||||
)
|
||||
|
||||
for page in fixture.get("snapshot_pages", []):
|
||||
if page > pages:
|
||||
raise TestFailure(f"{fixture_id}: snapshot page {page} больше количества страниц {pages}.")
|
||||
actual = render_page(
|
||||
pdftoppm,
|
||||
output,
|
||||
page,
|
||||
int(manifest["dpi"]),
|
||||
temp_dir / fixture_id / f"page-{page}.png",
|
||||
)
|
||||
expected = SNAPSHOT_ROOT / fixture_id / f"page-{page}.png"
|
||||
if args.update_snapshots:
|
||||
expected.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(actual, expected)
|
||||
print(f"[snapshot:update] {expected.relative_to(ROOT)}")
|
||||
continue
|
||||
if not expected.is_file():
|
||||
raise TestFailure(
|
||||
f"Нет snapshot {expected.relative_to(ROOT)}. "
|
||||
"После ручной проверки запустите --update-snapshots."
|
||||
)
|
||||
difference = image_difference(expected, actual)
|
||||
threshold = float(fixture.get("visual_threshold", manifest["visual_threshold"]))
|
||||
if difference > threshold:
|
||||
raise TestFailure(
|
||||
f"{fixture_id}, страница {page}: visual diff {difference:.6f} "
|
||||
f"превышает порог {threshold:.6f}. Фактический файл: {actual}"
|
||||
)
|
||||
print(f"[snapshot:ok] {fixture_id}/page-{page} diff={difference:.6f}")
|
||||
|
||||
print(f"OK: {version_output}")
|
||||
return 0
|
||||
except Exception:
|
||||
failed = True
|
||||
raise
|
||||
finally:
|
||||
if failed:
|
||||
print(f"Временные результаты сохранены: {temp_dir}", file=sys.stderr)
|
||||
else:
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except TestFailure as exc:
|
||||
print(f"FAIL: {exc}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
@@ -0,0 +1,63 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('report', 'letter', 'commercial-offer', 'contract')]
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$workspaceRoot = [System.IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..'))
|
||||
$starterRoot = [System.IO.Path]::GetFullPath(
|
||||
(Join-Path $workspaceRoot "docs\examples\documents\$Name")
|
||||
)
|
||||
$mainTarget = [System.IO.Path]::GetFullPath((Join-Path $workspaceRoot 'main.typ'))
|
||||
$chaptersTarget = [System.IO.Path]::GetFullPath((Join-Path $workspaceRoot 'chapters'))
|
||||
$assetsTarget = [System.IO.Path]::GetFullPath((Join-Path $workspaceRoot 'assets'))
|
||||
|
||||
foreach ($path in @($starterRoot, $mainTarget, $chaptersTarget, $assetsTarget)) {
|
||||
if (-not $path.StartsWith(
|
||||
$workspaceRoot + [System.IO.Path]::DirectorySeparatorChar,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)) {
|
||||
throw "Небезопасный путь за пределами проекта: $path"
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $starterRoot 'main.typ'))) {
|
||||
throw "Не найден пример документа: $Name"
|
||||
}
|
||||
|
||||
# Выбор типа разрешён даже при незакоммиченных изменениях: перед заменой
|
||||
# создаётся локальная резервная копия, которая не попадает в Git.
|
||||
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
|
||||
$backupRoot = [System.IO.Path]::GetFullPath(
|
||||
(Join-Path $workspaceRoot ".private\starter-backups\$timestamp")
|
||||
)
|
||||
if (-not $backupRoot.StartsWith(
|
||||
$workspaceRoot + [System.IO.Path]::DirectorySeparatorChar,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)) {
|
||||
throw "Небезопасный путь резервной копии: $backupRoot"
|
||||
}
|
||||
New-Item -ItemType Directory -Path $backupRoot -Force | Out-Null
|
||||
|
||||
if (Test-Path -LiteralPath $mainTarget) {
|
||||
Copy-Item -LiteralPath $mainTarget -Destination (Join-Path $backupRoot 'main.typ')
|
||||
}
|
||||
if (Test-Path -LiteralPath $chaptersTarget) {
|
||||
Copy-Item -LiteralPath $chaptersTarget -Destination (Join-Path $backupRoot 'chapters') -Recurse
|
||||
Remove-Item -LiteralPath $chaptersTarget -Recurse -Force
|
||||
}
|
||||
|
||||
Copy-Item -LiteralPath (Join-Path $starterRoot 'main.typ') -Destination $mainTarget -Force
|
||||
Copy-Item -LiteralPath (Join-Path $starterRoot 'chapters') -Destination $chaptersTarget -Recurse
|
||||
|
||||
$starterAssets = Join-Path $starterRoot 'assets'
|
||||
if (Test-Path -LiteralPath $starterAssets) {
|
||||
New-Item -ItemType Directory -Path $assetsTarget -Force | Out-Null
|
||||
Copy-Item -Path (Join-Path $starterAssets '*') -Destination $assetsTarget -Recurse -Force
|
||||
}
|
||||
|
||||
Write-Host "Установлен тип документа '$Name'."
|
||||
Write-Host "Предыдущие main.typ и chapters/ сохранены в $backupRoot"
|
||||
Write-Host 'Откройте main.typ и последовательно проверьте параметры.'
|
||||
@@ -0,0 +1,7 @@
|
||||
# VS Code для сопровождающего шаблона
|
||||
|
||||
Корневые `.vscode/extensions.json`, `.vscode/settings.json` и `.vscode/tasks.json` намеренно синхронизируются через Git: это часть пользовательского опыта шаблона.
|
||||
|
||||
`local.typst-typewriter` и `zotst.zotst` обнаружены как локально установленные расширения. Рекомендация в `extensions.json` сообщает VS Code их идентификаторы, но не распространяет сам пакет. Для нового компьютера соответствующий `.vsix` нужно получить из внутреннего хранилища и выполнить команду **Extensions: Install from VSIX**.
|
||||
|
||||
Если принято решение хранить внутренние `.vsix` рядом с шаблоном, размещайте их в `packages/` этого каталога и добавляйте только после проверки лицензии и отсутствия секретов.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Внутренние VSIX-пакеты
|
||||
|
||||
Здесь могут храниться проверенные установочные пакеты локальных расширений:
|
||||
|
||||
- `local.typst-typewriter`;
|
||||
- `zotst.zotst`.
|
||||
|
||||
Перед добавлением `.vsix` проверьте лицензию, состав архива и отсутствие секретов. Само наличие extension ID в `.vscode/extensions.json` не распространяет локальное расширение.
|
||||
Reference in new issue
Block a user