Compare commits

...

27 Commits

Author SHA1 Message Date
m 3b2db2caba feat(1c): add complete config storage manifest collector 2026-08-15 00:31:29 +03:00
m 6b3e865f8f docs(1c): add controlled template descriptor fixture protocol 2026-08-14 21:01:43 +03:00
m 31caa492f9 fix(1c): normalize common template descriptor evidence 2026-08-14 20:58:54 +03:00
m a646b53aec fix(1c): retain descriptor role in template inventory 2026-08-14 20:57:30 +03:00
m 24c6c6e170 feat(1c): expose unresolved template descriptor codes 2026-08-14 20:56:00 +03:00
m 7cd24bffd4 fix(1c): separate template descriptor type from payload carrier 2026-08-14 20:53:49 +03:00
m 9f46d8e39e Preserve nested appearance style values 2026-08-14 13:35:11 +03:00
m 1a3624f3e0 Decode appearance template style rules 2026-08-14 13:34:10 +03:00
m 9d8dccc993 Decode SCD variant presentations 2026-08-14 13:33:07 +03:00
m 863bda9436 Classify common template payloads consistently 2026-08-14 13:31:58 +03:00
m 945ba28c4b Clarify external component fixture evidence 2026-08-14 13:30:49 +03:00
m 9f60a7da2d Test report and appearance RPC dispatch 2026-08-14 13:30:02 +03:00
m a8a7ea2aaa Document report and appearance inspection 2026-08-14 13:04:49 +03:00
m ae11b5e227 Add confirmed report component inspection 2026-08-14 13:03:43 +03:00
m c6384ff753 Decode SCD grouping nodes 2026-08-14 13:02:01 +03:00
m 3222ecee6e Expose appearance template inspection 2026-08-14 12:59:39 +03:00
m 8a6610f30c Decode DCS appearance template XML 2026-08-14 12:57:32 +03:00
m 5615e262a8 Classify appearance templates by XML root 2026-08-14 12:09:13 +03:00
m 61c0ed1726 Document live template type fixtures 2026-08-14 12:07:26 +03:00
m 643d79ebb5 Report implemented SCD codec capability 2026-08-14 12:05:38 +03:00
m 41a3f47c69 Decode SCD data sources from live XML 2026-08-14 12:04:12 +03:00
m adc5d88e5f Classify proven SCD templates consistently 2026-08-14 12:01:14 +03:00
m 832d159f37 Expose SCD XML structure outline 2026-08-14 11:59:52 +03:00
m 03fed309c6 Add report template codec roadmap 2026-08-14 11:57:30 +03:00
m b7858d480e Document Configurator fixture access 2026-08-14 11:56:23 +03:00
m 829e19dc3a Document report template codec program 2026-08-14 11:51:35 +03:00
m d3d9d8a97f Document project Git workflow 2026-08-14 11:31:20 +03:00
10 changed files with 799 additions and 14 deletions
+10
View File
@@ -7,6 +7,16 @@
- Portainer: `http://docker-test.cin.su:9000/`, user `admin`
- Do not store the password in repositories or project files; use an SSH key for persistent access.
## Project Git workflow
- The canonical remote for this project is `https://git.cin.su/m/llm.git`
(`origin`), with `main` as the shared branch.
- Use HTTPS authentication through Git Credential Manager; never put a
password or token into a repository file, remote URL, script, or commit.
- After a coherent change has been verified, create a focused commit and push
it to `origin/main`. Do not commit unfinished work, unrelated user changes,
generated large artifacts, or secrets.
## GPU Docker Host
- This project works with local LLM models and uses GPU resources.
+25
View File
@@ -83,6 +83,31 @@ overlay, it then removes only receipt-recorded rows after hash precondition
checks. A successful result proves SQL readback and cleanup only; it does not
prove Configurator visibility, acceptance, or activation.
## Report And Appearance Inspection
`report.inspect` is the read-only entry point for a report component graph.
It accepts a public report name and returns only independently confirmed
modules, forms, templates and SCD components. A form called
`ОсновнаяСхемаКомпоновкиДанных` remains a form; it is never promoted to an SCD
from its name or a guessed `.0` stream.
```json
{"method":"report.inspect","payload":{"base_id":"upo_test","report":"ФинансовыйРезультат"}}
```
`appearance.inspect` is a separate read-only reader for a base
`CommonTemplate` whose `.0` carrier has the `AppearanceTemplate` XML root.
This selects the payload reader only; it does not establish the 1C platform
`TemplateType`, which remains unresolved until its metadata descriptor is
decoded.
It returns raw XML-backed `item(parameter, value)` rules and an XML tag
outline. It has no write operation, and an extension template returns explicit
`unsupported` until an extension carrier route is proven.
```json
{"method":"appearance.inspect","payload":{"base_id":"upo_test","template":"ОформлениеОтчетовБежевый"}}
```
## User Identity And Access Terminology
An unqualified user request means an **infobase user**: the platform identity
@@ -0,0 +1,153 @@
# Программа доказательного codec-а отчётов и макетов
## Цель
Построить для SQL-адаптера раздельные, evidence-first decoder/encoder пути
для отчёта и каждого типа его макета. Публичный API принимает только имя
расширения, отчёта и макета; физические таблицы, ключи и контейнеры остаются
внутренними деталями реализации.
## Первая live-фикстура
База: `upo_test`; расширение: `фс_Отчеты`;
отчёт: `_ПоступлениеТовара`;
GUID отчёта: `c428f629-785a-4141-b038-f2192bb4580d`.
Наблюдаемое состояние на 2026-08-14:
- extension object search подтверждает сохранённый override в `ConfigCASSave`;
- в saved слое присутствуют descriptor, `.0` и extension `__configinfo`;
- active manifest содержит только descriptor и `.0` stream;
- `metadata.object.components` подтверждает четыре формы, среди них форму
`ОсновнаяСхемаКомпоновкиДанных`;
- `metadata.object.templates` и `scd.inspect` не подтверждают макет
`DataCompositionSchema` для этого объекта.
Следствие: имя формы не является доказательством существования одноимённого
макета СКД. Нельзя направлять запрос или XML-кодек в `.0` stream.
## Матрица типов макетов
| Платформенный тип | Состояние codec-а | Допустимое действие до fixture proof |
| --- | --- | --- |
| Табличный документ | частичный MOXCEL decoder | read-only structure/evidence |
| Текстовый документ | типизированное текстовое исследование | read-only до round-trip fixture |
| Двоичные данные | opaque | hash/size only |
| Active document | opaque | hash/size only |
| HTML документ | частичное текстовое/HTML evidence | read-only до round-trip fixture |
| Географическая схема | opaque typed candidate | read-only |
| Графическая схема | opaque typed candidate | read-only |
| Схема компоновки данных | XML decoder + scalar byte-preserving patch | `read_and_scalar_patch_supported`; write only after public route and saved-state codec proof |
| Макет оформления компоновки данных | отдельный XML family | read-only до own fixture proof |
| Внешняя компонента | opaque archive | hash/size only |
Источник taxonomy: [1C:Enterprise Template wizard](https://kb.1ci.com/1C_Enterprise_Platform/Guides/Developer_Guides/1C_Enterprise_8.3.23_Developer_Guide/Chapter_31._Development_tools/31.14._Template_wizard/).
## Protocol gates
1. Создать либо выбрать отдельную live fixture для одного вида макета.
2. Снять SQL before snapshot всех связанных payload-ов и их SHA-1.
3. Выполнить ровно одно контролируемое изменение через Configurator.
4. Снять after snapshot и доказать точное соответствие object → manifest →
payload → saved-state overlay.
5. Реализовать lossless decoder и обратный encoder только для изменённого
участка; неизвестные байты сохраняются дословно.
6. Выполнить `apply_and_rollback` в `upo_test`, проверить SQL readback и
очистку adapter-created rows.
7. Отдельно получить человеческое подтверждение видимости в Configurator.
Для СКД XML-семантика включает datasets, links, calculated fields, resources,
parameters, layouts, nested schemas и settings. Макет оформления — другой XML
контракт: регионы, шрифты, цвета, границы и правила оформления; его нельзя
обрабатывать методом `scd.patch`.
## Долгосрочный план и критерии готовности
| Этап | Результат | Критерий перехода |
| --- | --- | --- |
| 1. Инвентарь | Реестр `Report → Template` с identity, container, SHA-1 и состоянием active/saved | У каждого fixture есть подтверждённая цепочка owner → template → payload, без имени формы в качестве подстановки. |
| 2. СКД read | Lossless контейнерный decoder с XML-путями для datasets, links, calculated fields, resources, parameters, layouts, nested schemas и settings | XML корректно читается на нескольких живых схемах; неизвестные XML узлы сохраняются в raw-carrier и не теряются. |
| 3. СКД write | Минимальный scalar writer (`dataset.query`, expression) и resolver `Report + Template` | `plan`, точная проверка SHA-1 и `apply_and_rollback` проходят на выделенной fixture; активность подтверждает человек в Configurator. |
| 4. Оформление СКД | Отдельный decoder/encoder XML оформления: regions, style rules, fonts, colors, borders, indents | Не используется путь `scd.patch`; один контролируемый before/after fixture доказывает carrier и round-trip. |
| 5. Прочие типы | Отдельная карточка codec-а на каждый из десяти platform types | Для Spreadsheet/Text/HTML/Geo/Graphical доказан собственный round-trip или выставлен explicit `unsupported`; Binary/Active/AddIn остаются opaque до отдельного доказательства. |
| 6. Report aggregate | API `report.inspect` возвращает identity, modules/forms/templates, main-SCD relation, overlay state и capability по каждому компоненту | Отчёт без подтверждённой СКД возвращает `unresolved`, а не выбирает форму или первый поток. |
| 7. Регрессия и выпуск | Fixture manifest, hash-baselines, unit tests, live SQL smoke и документация публичного контракта | Каждый write codec проходит план → rollback → SQL readback; ни один результат не заявляет применение в Configurator без ручного подтверждения. |
Работа выполняется последовательно по этому реестру: новый тип не получает
write endpoint до собственной fixture и доказанного обратного carrier codec-а.
## Live XML evidence: базовая СКД
14.08.2026 read-only diagnostic read подтвердил carrier для
`Report.ФинансовыйРезультат / ОсновнаяСхемаКомпоновкиДанных`:
- Template GUID: `e3140fc8-1688-4640-a68a-e0f7675c68e9`; payload: `.0`;
- контейнер: 3 391 байт raw-deflate, после распаковки 23 097 байт;
- XML envelope: `SchemaFile`, 12 941 символ до закрывающего тега;
- прямые дочерние секции `dataCompositionSchema`: `dataSource` (1),
`dataSet` (1), `parameter` (4), `settingsVariant` (1), `totalField` (4);
- `dataSource` имеет доказанные прямые поля `name=ИсточникДанных1` и
`dataSourceType=Local`.
На этом доказательстве в decoder добавлена секция `data_sources` и
`schema_outline` с точными XML тегами, путями и количеством. Наличие
`dataSetLink`, layouts или nested schema в другой СКД не предполагается: они
будут добавлены после отдельной live fixture с соответствующим тегом.
## Live fixtures остальных типов
Read-only diagnostic inventory `CommonTemplate` в `upo_test` уже дал три
отдельных carrier fixture. Имена нужны только для повторного поиска; тип
подтверждён содержимым `.0`, а не именем.
| Fixture | Template GUID | Доказательство типа | Разрешённое действие |
| --- | --- | --- | --- |
| `ОформлениеОтчетовБежевый` | `f49709d3-13ea-4d67-9934-9d93f8ba1c21` | XML root `AppearanceTemplate`, DCS appearance namespace; platform TemplateType descriptor ещё не декодирован | read-only appearance decoder; platform type unresolved |
| `ШаблонHTMLПредставленияTXTФайла` | `e3eb9ae8-7381-400b-a933-861b1f4efcff` | UTF-8 XML/HTML root `html` | existing safe text/HTML export only |
| `КомпонентаXMLDSIG` | `db062a63-2759-409a-b78b-132fe226f6f1` | carrier `{#base64:...}` with ZIP signature `PK` after decoding; metadata part has no `ExternalComponent` declaration | opaque archive candidate, hash/size only |
Из этого следует, что `AppearanceTemplate` нельзя относить к spreadsheet
только из-за вложенного упоминания `SpreadsheetDocument` в XML. Равным
образом XML root нельзя объявлять платформенным типом макета: он выбирает
только decoder payload. Платформенный `TemplateType` допускается публиковать
лишь после разбора metadata descriptor или подтверждённой template identity.
Для carrier нужен отдельный XML decoder; обратная запись по-прежнему
запрещена до controlled before/after fixture.
Текущая реализация публикует найденный в descriptor числовой `type code` как
`descriptor_type_evidence` без имени типа. В живой базе код `7` встретился у
carrier `AppearanceTemplate`, `4`у HTML carrier, `1`у ZIP carrier.
Это корреляции, а не таблица типов: назначать этим кодам платформенные имена
можно только после controlled fixture каждого типа.
Reader реализован в `plugins/1c/parser/appearance_payload.py`: он возвращает
контейнер, все верхнеуровневые правила `item(parameter, value)` и точный
outline XML-тегов. Он не разбирает строковое значение style как произвольный
формат и всегда сообщает `unsupported_until_reverse_codec_fixture` для write.
ZIP carrier сам по себе не доказывает platform type `ExternalComponent`:
fixture выше нельзя использовать для утверждения этого типа или для encoder-а.
Для внешней компоненты нужен отдельный metadata declaration либо controlled
Configurator fixture.
## Ближайшая реализация
Первый кодовый результат — resolver, который умеет различать форму и макет,
а затем находит `MainDataCompositionSchema` по подтверждённой template identity
в extension active/saved layer. При отсутствии доказанного template payload он
возвращает `unresolved`, а не fallback на форму или stream `.0`.
## Среда Configurator и текущий блокер fixture-изменений
На рабочей станции подтверждены локальные исполняемые файлы Configurator:
`C:\\Program Files\\1cv8\\8.3.24.1548\\bin\\1cv8.exe` и
`C:\\Program Files\\1cv8\\8.5.1.1236\\bin\\1cv8.exe`. В списке информационных
баз зарегистрирован `upo_test` с соединением `Srvr="wsr";Ref="upo_test"`.
14.08.2026 выполнена только read-only попытка
`DESIGNER /DumpConfigToFiles` в отдельный каталог отчётов. Сервер вернул
`Пользователь ИБ не идентифицирован`; выгрузка не началась и база не
изменялась. Поэтому автоматизированные before/after изменения в
Configurator пока не запускаются: требуется штатная учётная запись ИБ или
интерактивный вход человека. Это не ослабляет SQL-only правило runtime
адаптера и не является разрешением подбирать либо сохранять пароли.
@@ -0,0 +1,48 @@
# Контролируемые fixtures: типы макетов 1С
## Цель
Зафиксировать platform-dependent таблицу `Template descriptor type code ->
TemplateType` для конкретной сборки 1С. До выполнения каждого опыта код не
является именем типа, а carrier payload не является источником типа.
## Условия опыта
- База: `upo_test`; изменения допускаются только в отдельном расширении
`codex_TemplateCodecFixtures`.
- Для каждого типа создаётся ровно один пустой макет с именем
`CodecFixture_<EnglishType>` и один минимальный, но непустой вариант.
- До создания, после создания и после сохранения конфигурации снимаются
descriptor `{GUID}` и все части `{GUID}.*` из SQL saved state.
- Для каждого снимка сохраняются SHA-1, размер, compression, descriptor tree,
descriptor type code, XML root/сигнатура carrier и экспорт Конфигуратора.
- Обратная запись запрещена, пока для того же типа не доказан byte-preserving
round trip и не подтверждён результат человеком в Конфигураторе.
## Матрица
| Fixture suffix | Тип из мастера | Минимальное содержимое | Нужный артефакт |
| --- | --- | --- | --- |
| `Spreadsheet` | Табличный документ | Одна ячейка `X` | `.mxl` + SQL carrier |
| `Text` | Текстовый документ | `X` | `.txt` + SQL carrier |
| `Binary` | Двоичные данные | 3 байта `01 02 03` | `.bin` + SQL carrier |
| `Active` | Active document | минимальный выбранный Active object | SQL carrier, тип OLE |
| `Html` | HTML документ | `<p>X</p>` | `.htm` + SQL carrier |
| `Geo` | Географическая схема | пустая схема | `.geo` + SQL carrier |
| `Graphical` | Графическая схема | один прямоугольник | `.grs` + SQL carrier |
| `Dcs` | Схема компоновки данных | параметр `P` и пустой dataset | XML + SQL carrier |
| `DcsAppearance` | Макет оформления компоновки данных | одно правило оформления | XML + SQL carrier |
| `AddIn` | Внешняя компонента | тестовый ZIP с манифестом | ZIP + SQL carrier |
## Критерий принятия одной строки таблицы
Строка считается `proven` только когда одновременно совпали:
1. тип, выбранный в мастере Конфигуратора;
2. descriptor type code из сохранённого объекта;
3. снимок carrier `{GUID}.0` и export соответствующего редактора;
4. повторное открытие и сохранение fixture в Конфигураторе без ошибки.
Любое отсутствие пункта оставляет строку `unresolved_mapping`. Имена макетов,
XML root и архивная сигнатура являются вспомогательными evidence и не заменяют
эти условия.
+195 -6
View File
@@ -163,7 +163,7 @@ ONEC_TEMPLATE_PLATFORM_TYPES = [
"name": "Схема компоновки данных",
"xml_type": "DataCompositionSchema",
"decoder": "data_composition_schema",
"status": "not_implemented",
"status": "read_and_scalar_patch_supported",
"aliases": ["СхемаКомпоновкиДанных", "DataCompositionSchema"],
},
{
@@ -762,7 +762,9 @@ METHODS = [
{"name": "metadata.object.components", "transport": "POST /rpc", "description": "Read-only confirmed component graph for one metadata object. Returns only modules, forms, templates, and SCD nodes confirmed by the existing live-SQL decoders, with public child selectors and explicit unresolved areas. It never infers absent children from an object kind."},
{"name": "metadata.object.modules", "transport": "POST /rpc", "description": "1C-facing BSL module list for a metadata object, including owned form modules. Exact extension objects are resolved from public kind/name or ref through a live-validated route cache. include_storage must be a JSON boolean true/false, string values are invalid. Physical module ids are hidden unless include_storage=true."},
{"name": "metadata.object.related", "transport": "POST /rpc", "description": "1C-facing related metadata objects such as forms and templates. Physical record paths are hidden unless include_storage=true."},
{"name": "report.inspect", "transport": "POST /rpc", "description": "Read-only aggregate of one report's confirmed modules, forms, templates and Data Composition Schemas. Does not infer an SCD from a same-named form or stream."},
{"name": "scd.inspect", "transport": "POST /rpc", "description": "Read-only inspection entry point for a report Data Composition Schema. Resolves a base report from Config or an extension report from ConfigCAS by public names, and returns partial evidence instead of inventing undecoded parameters, datasets, resources, or variants."},
{"name": "appearance.inspect", "transport": "POST /rpc", "description": "Read-only inspection of a base CommonTemplate whose payload has AppearanceTemplate XML root. That root selects a reader only, not the 1C TemplateType. Returns XML-backed appearance rules; no write route is exposed."},
{"name": "scd.prepare", "transport": "POST /rpc", "description": "Prepare the complete SQL saved-state file set required for one report SCD. For base reports it includes both report files and the separately stored Template payload group, while keeping SQL file identifiers internal."},
{"name": "scd.prepare.rollback", "transport": "POST /rpc", "description": "Remove exactly the rows inserted by one SCD preparation receipt after hash precondition checks. This is intended for controlled test cleanup and never touches active Config/ConfigCAS."},
{"name": "scd.compare", "transport": "POST /rpc", "description": "Read-only semantic comparison of active and prepared saved-state SCD XML: Config ↔ ConfigSave for base reports and ConfigCAS ↔ ConfigCASSave for extensions. Compares named parameters, datasets, fields, expressions, totals, and variants rather than storage hashes."},
@@ -4097,11 +4099,30 @@ def declared_template_platform_type_from_part(part: dict[str, Any]) -> dict[str,
return None
def template_descriptor_type_code_from_part(part: dict[str, Any]) -> str | None:
"""Return an unlabelled type code from the proven Template descriptor shape.
The code is useful evidence, but its numeric-to-TemplateType mapping is
intentionally not guessed from the payload carrier or a template name.
"""
classification = part.get("classification") if isinstance(part.get("classification"), dict) else {}
role = str(part.get("role") or classification.get("role") or "")
if role != "metadata_payload":
return None
tree = classification.get("tree")
if not isinstance(tree, dict):
return None
if config_tree_scalar_at_path(tree, (0,)) != "1" or config_tree_scalar_at_path(tree, (1, 0)) != "4":
return None
value = config_tree_scalar_at_path(tree, (1, 2))
return value if value and value.isdecimal() else None
def template_type_candidates_from_features(features: dict[str, Any], content_parts: list[dict[str, Any]]) -> list[dict[str, Any]]:
candidates: list[dict[str, Any]] = []
declared = normalize_onec_template_platform_type(features.get("declared_platform_type_id") or features.get("declared_platform_type"))
if declared:
declared.update({"confidence": "high", "evidence": ["TemplateType metadata"]})
declared.update({"confidence": "high", "evidence": [str(features.get("declared_platform_type_evidence") or "TemplateType metadata")]})
candidates.append(declared)
def append_candidate(candidate: dict[str, Any]) -> None:
@@ -4129,12 +4150,41 @@ def template_type_candidates_from_features(features: dict[str, Any], content_par
return candidates
def public_template_summary(parts: list[dict[str, Any]], *, include_storage: bool = False) -> dict[str, Any]:
def public_template_summary(
parts: list[dict[str, Any],],
*,
include_storage: bool = False,
confirmed_platform_type_id: str | None = None,
) -> dict[str, Any]:
content_parts = [part for part in parts if part.get("role") != "metadata_payload"]
declared_platform_type = next(
(template_type for template_type in (declared_template_platform_type_from_part(part) for part in parts) if template_type),
None,
)
# A payload carrier (for example XML root AppearanceTemplate) selects a
# safe reader, but is not the 1C TemplateType. The latter is determined
# only by a decoded template descriptor / confirmed template identity.
declared_type_evidence = "TemplateType metadata"
xml_roots = sorted(
{
str((part.get("classification") or {}).get("xml_root"))
for part in parts
if isinstance(part.get("classification"), dict)
and (part.get("classification") or {}).get("xml_root")
}
)
descriptor_type_codes = sorted(
{
code
for code in (
str(part.get("descriptor_type_code")) if str(part.get("descriptor_type_code") or "").isdecimal()
else template_descriptor_type_code_from_part(part)
for part in parts
)
if code is not None
},
key=int,
)
features = {
"tabular_document": any(((part.get("features") or {}).get("tabular_document")) for part in content_parts),
"html": any(((part.get("features") or {}).get("html")) for part in content_parts),
@@ -4144,19 +4194,46 @@ def public_template_summary(parts: list[dict[str, Any]], *, include_storage: boo
"declared_platform_type": declared_platform_type.get("name"),
"declared_platform_type_id": declared_platform_type.get("id"),
"declared_platform_xml_type": declared_platform_type.get("xml_type"),
"declared_platform_type_evidence": declared_type_evidence,
}
if declared_platform_type
else {}
),
}
template_type_candidates = template_type_candidates_from_features(features, content_parts)
confirmed_platform_type = onec_template_type_by_id(confirmed_platform_type_id) if confirmed_platform_type_id else None
if confirmed_platform_type and confirmed_platform_type.get("id") != confirmed_platform_type_id:
confirmed_platform_type = None
if confirmed_platform_type:
# This classification comes from the report's decoded Template record,
# not a marker guess in the compressed content stream. In particular,
# an SCD stream has no MOXCEL/HTML marker and must not be presented as
# a low-confidence binary template merely because it is compressed.
confirmed_platform_type.update({"confidence": "high", "evidence": ["confirmed Template identity"]})
template_type_candidates = [confirmed_platform_type] + [
candidate for candidate in template_type_candidates if candidate.get("id") != confirmed_platform_type.get("id")
]
summary: dict[str, Any] = {
"features": features,
"known_platform_types": ONEC_TEMPLATE_PLATFORM_TYPES,
"template_type_candidates": template_type_candidates,
"counts": {"parts": len(parts), "content_parts": len(content_parts)},
}
if features.get("tabular_document"):
if xml_roots:
summary["content_type_evidence"] = {
"xml_roots": xml_roots,
"meaning": "payload_carrier_only_not_platform_template_type",
}
if descriptor_type_codes and not declared_platform_type and not confirmed_platform_type_id:
summary["descriptor_type_evidence"] = {
"codes": descriptor_type_codes,
"status": "unresolved_mapping",
"meaning": "Template descriptor type code; numeric mapping to 1C TemplateType is not yet fixture-proven.",
}
if confirmed_platform_type:
summary["format"] = confirmed_platform_type.get("xml_type") or confirmed_platform_type.get("id")
summary["platform_type"] = confirmed_platform_type.get("name")
elif features.get("tabular_document"):
summary["format"] = "ТабличныйДокумент"
summary["platform_type"] = "Табличный документ"
summary["preview"] = {
@@ -36040,6 +36117,79 @@ def metadata_object_related(payload: dict[str, Any]) -> dict[str, Any]:
}
def appearance_inspect(payload: dict[str, Any]) -> dict[str, Any]:
"""Read a proven base CommonTemplate appearance XML carrier by public name."""
method = "appearance.inspect"
base_id_or_error = require_base_id(payload, method)
if isinstance(base_id_or_error, dict):
return base_id_or_error
base_id = base_id_or_error
if payload.get("extension") not in {None, ""} or payload.get("extension_guid") not in {None, ""}:
return {
"schema": "onec_appearance_inspect.v1", "method": method, "status": "unsupported", "base_id": base_id,
"diagnostics": {"code": "APPEARANCE_EXTENSION_ROUTE_UNPROVEN", "message": "Only a base CommonTemplate carrier is proven for this reader."},
}
template = str(payload.get("template") or payload.get("name") or payload.get("guid") or "").strip()
if not template:
return invalid_argument(method, "template", "template is required as a public CommonTemplate name or GUID.")
timeout_seconds = int(payload.get("timeout_seconds") or 60)
resolved = get_object("CommonTemplate", template, base_id=base_id, view="effective", limit=2, include_storage=True, include_semantic=False, timeout_seconds=timeout_seconds, table="Config")
if resolved.get("status") != "ok" or not isinstance(resolved.get("object"), dict):
return {"schema": "onec_appearance_inspect.v1", "method": method, "status": resolved.get("status") or "not_found", "base_id": base_id, "diagnostics": resolved.get("diagnostics") or {}}
object_card = resolved["object"]
guid = str(object_card.get("guid") or "").strip()
if not guid:
return {"schema": "onec_appearance_inspect.v1", "method": method, "status": "unresolved", "base_id": base_id, "diagnostics": {"code": "APPEARANCE_GUID_NOT_PROVEN"}}
data, _, read_error = read_storage_file_bytes(base_id, "Config", f"{guid}.0", timeout_seconds=min(timeout_seconds, 60))
if read_error or data is None:
return {"schema": "onec_appearance_inspect.v1", "method": method, "status": "not_found", "base_id": base_id, "diagnostics": (read_error or {}).get("diagnostics") or {"code": "APPEARANCE_PAYLOAD_NOT_FOUND"}}
from parser.appearance_payload import inspect_appearance_payload
decoded = inspect_appearance_payload(data)
return {
"schema": "onec_appearance_inspect.v1",
"method": method,
"status": decoded.get("status"),
"base_id": base_id,
"template": public_metadata_row(object_card),
"appearance": decoded,
"template_type": {
"status": "unresolved",
"reason": "AppearanceTemplate XML root selects this read-only decoder but does not prove the 1C TemplateType; a decoded metadata descriptor is required.",
},
"activation_state": "working_not_runtime_applied",
}
def report_inspect(payload: dict[str, Any]) -> dict[str, Any]:
"""Return the confirmed component graph of a report without filling gaps."""
method = "report.inspect"
base_id_or_error = require_base_id(payload, method)
if isinstance(base_id_or_error, dict):
return base_id_or_error
report = str(payload.get("report") or payload.get("name") or "").strip()
if not report:
return invalid_argument(method, "report", "report is required as a public 1C report name.")
component_payload = {"base_id": base_id_or_error, "kind": "Report", "name": report, "include_storage": False, "timeout_seconds": int(payload.get("timeout_seconds") or 60)}
for key in ("extension", "extension_guid"):
if payload.get(key) not in {None, ""}:
component_payload[key] = payload[key]
components_result = metadata_object_components(component_payload)
if not isinstance(components_result.get("object"), dict):
return {"schema": "onec_report_inspect.v1", "method": method, "status": components_result.get("status") or "not_found", "base_id": base_id_or_error, "unresolved": components_result.get("unresolved") or [], "diagnostics": components_result.get("diagnostics") or {}}
components = [item for item in components_result.get("components") or [] if isinstance(item, dict)]
return {
"schema": "onec_report_inspect.v1", "method": method, "status": "ok", "base_id": base_id_or_error,
"report": public_metadata_row(components_result["object"]),
"components": components,
"scd": [item for item in components if item.get("component_kind") == "scd"],
"templates": [item for item in components if item.get("component_kind") in {"scd", "template"}],
"forms": [item for item in components if item.get("component_kind") == "form"],
"modules": [item for item in components if item.get("component_kind") == "module"],
"unresolved": components_result.get("unresolved") or [],
"diagnostics": {"rule": "Only components proven by the individual live-SQL readers are included; a form is never promoted to an SCD by name."},
}
def scd_inspect(payload: dict[str, Any]) -> dict[str, Any]:
"""Resolve and decode one report SCD from its SQL ConfigCAS payload."""
method = "scd.inspect"
@@ -36051,7 +36201,7 @@ def scd_inspect(payload: dict[str, Any]) -> dict[str, Any]:
if not report:
return invalid_argument(method, "report", "report is required as a public 1C report name.")
schema_name = str(payload.get("schema") or "").strip()
requested_sections = payload.get("sections") or ["parameters", "datasets", "fields", "calculated_fields", "resources", "settings", "variants", "total_fields"]
requested_sections = payload.get("sections") or ["data_sources", "parameters", "datasets", "fields", "calculated_fields", "resources", "groupings", "settings", "variants", "total_fields"]
if not isinstance(requested_sections, list) or not all(isinstance(item, str) for item in requested_sections):
return invalid_argument(method, "sections", "sections must be an array of strings.")
resolve_metadata, resolve_metadata_error = strict_bool_argument(payload, "resolve_metadata", method=method, default=True)
@@ -42269,9 +42419,31 @@ def metadata_object_templates(payload: dict[str, Any]) -> dict[str, Any]:
)
if direct.get("status") != "ok":
return direct
parts_result = metadata_object_parts(
{
"base_id": base_id, "guid": guid, "kind": "Template", "table": table,
"include_text": bool(include_text), "include_tree": bool(include_tree),
"include_storage": include_storage, "evidence_mode": evidence_mode,
"timeout_seconds": timeout_seconds,
}
)
classified_parts = []
for raw_part in parts_result.get("parts") or []:
if not isinstance(raw_part, dict):
continue
classified_part = dict(raw_part)
classification = classified_part.get("classification") if isinstance(classified_part.get("classification"), dict) else {}
if classification.get("role"):
classified_part["role"] = classification.get("role")
descriptor_type_code = template_descriptor_type_code_from_part(classified_part)
if descriptor_type_code is not None:
classified_part["descriptor_type_code"] = descriptor_type_code
classified_parts.append(classified_part)
common_summary = public_template_summary(classified_parts, include_storage=include_storage) if classified_parts else {}
templates = []
for template_item in direct.get("templates") or []:
item = dict(template_item)
item.update(common_summary)
item["name"] = item.get("name") or (object_card or {}).get("name")
item["kind"] = "CommonTemplate"
item["ref"] = (object_card or {}).get("ref") or object_selector_ref("CommonTemplate", str(item.get("name") or ""))
@@ -42351,6 +42523,11 @@ def metadata_object_templates(payload: dict[str, Any]) -> dict[str, Any]:
for part in parts_result.get("parts") or []:
classification = part.get("classification") or {}
public_part = payload_public_properties(classification)
if classification.get("role"):
public_part["role"] = classification.get("role")
descriptor_type_code = template_descriptor_type_code_from_part(part)
if descriptor_type_code is not None:
public_part["descriptor_type_code"] = descriptor_type_code
public_part["undecoded_evidence"] = payload_public_undecoded_evidence(
classification,
include_text_preview=bool(include_text),
@@ -42368,9 +42545,17 @@ def metadata_object_templates(payload: dict[str, Any]) -> dict[str, Any]:
}
)
parts.append(public_part)
category_platform_type = {
"DataCompositionSchema": "data_composition_schema",
"DataCompositionAppearanceTemplate": "data_composition_appearance_template",
}.get(str(item.get("category") or ""))
template = {
**public_child_identity(item),
**public_template_summary(parts, include_storage=include_storage),
**public_template_summary(
parts,
include_storage=include_storage,
confirmed_platform_type_id=category_platform_type,
),
**({"kind": "data_composition_schema"} if item.get("category") == "DataCompositionSchema" else {}),
}
if wanted:
@@ -70367,6 +70552,10 @@ def call_method_impl(method: str, payload: dict[str, Any] | None) -> dict[str, A
return metadata_object_modules(payload)
if method == "metadata.object.related":
return metadata_object_related(payload)
if method == "report.inspect":
return report_inspect(payload)
if method == "appearance.inspect":
return appearance_inspect(payload)
if method == "scd.inspect":
return scd_inspect(payload)
if method == "scd.prepare":
+87
View File
@@ -0,0 +1,87 @@
"""Read-only decoder for 1C DCS appearance-template payloads.
Appearance templates are a distinct XML family (``AppearanceTemplate``), not
DataCompositionSchema and not a spreadsheet document. This decoder returns
only XML-backed rules and preserves the original carrier for any future
lossless writer work.
"""
from __future__ import annotations
import xml.etree.ElementTree as ET
from collections import Counter
from typing import Any
from .payload import decode_payload_lossless
def _local_name(tag: str) -> str:
return tag.rsplit("}", 1)[-1]
def _child_text(node: ET.Element, name: str) -> str:
child = next((item for item in node if _local_name(item.tag) == name), None)
return "".join(child.itertext()).strip() if child is not None else ""
def inspect_appearance_payload(data: bytes) -> dict[str, Any]:
"""Decode a compressed appearance-template carrier without changing it."""
decoded = decode_payload_lossless(data)
payload = bytes(decoded.get("payload") or b"")
start = payload.find(b"<?xml")
if start < 0:
start = payload.find(b"<AppearanceTemplate")
end_marker = b"</AppearanceTemplate>"
end = payload.find(end_marker, start) if start >= 0 else -1
if start < 0 or end < 0:
return {
"status": "undecodable",
"container": {"compression": decoded.get("compression"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes")},
"diagnostics": {"code": "APPEARANCE_XML_NOT_FOUND"},
}
xml_bytes = payload[start : end + len(end_marker)]
try:
root = ET.fromstring(xml_bytes.decode("utf-8-sig"))
except (UnicodeDecodeError, ET.ParseError) as exc:
return {"status": "undecodable", "diagnostics": {"code": "APPEARANCE_XML_INVALID", "message": str(exc)}}
if _local_name(root.tag) != "AppearanceTemplate":
return {"status": "unsupported", "diagnostics": {"code": "APPEARANCE_ROOT_NOT_CONFIRMED", "xml_root": _local_name(root.tag)}}
items = []
for ordinal, item in enumerate((node for node in root if _local_name(node.tag) == "item"), start=1):
parameter, value = _child_text(item, "parameter"), _child_text(item, "value")
items.append({"ordinal": ordinal, "parameter": parameter, "value": value})
appearances = []
for ordinal, appearance in enumerate((node for node in root.iter() if _local_name(node.tag) == "appearance"), start=1):
rules = []
for item in (node for node in appearance if _local_name(node.tag) == "item"):
value_node = next((child for child in item if _local_name(child.tag) == "value"), None)
rule: dict[str, Any] = {"parameter": _child_text(item, "parameter"), "value": _child_text(item, "value")}
if value_node is not None:
value_type = value_node.attrib.get("{http://www.w3.org/2001/XMLSchema-instance}type") or value_node.attrib.get("type")
if value_type:
rule["value_type"] = value_type
if value_node.attrib:
rule["value_attributes"] = {key.rsplit("}", 1)[-1]: value for key, value in value_node.attrib.items()}
nested_values = []
for child in value_node.iter():
if child is value_node or list(child):
continue
nested_values.append({
"tag": _local_name(child.tag),
"value": "".join(child.itertext()).strip(),
**({"attributes": {key.rsplit("}", 1)[-1]: value for key, value in child.attrib.items()}} if child.attrib else {}),
})
if nested_values:
rule["value_children"] = nested_values
rules.append(rule)
appearances.append({"ordinal": ordinal, "rules": rules})
tag_counts = Counter(_local_name(node.tag) for node in root.iter())
return {
"status": "ok",
"container": {"compression": decoded.get("compression"), "raw_bytes": decoded.get("raw_bytes"), "payload_bytes": decoded.get("payload_bytes"), "xml_offset": start, "xml_bytes": len(xml_bytes), "xml_root": "AppearanceTemplate"},
"items": items,
"appearances": appearances,
"outline": {"tag_counts": dict(sorted(tag_counts.items(), key=lambda item: item[0].casefold()))},
"diagnostics": {"write": "unsupported_until_reverse_codec_fixture"},
}
+9
View File
@@ -7,6 +7,9 @@ import hashlib
import re
from typing import Any
XML_ROOT_RE = re.compile(r"^\s*(?:<\?xml[^>]*>\s*)?<([A-Za-z_][\w.-]*(?::[A-Za-z_][\w.-]*)?)\b")
from .payload import collect_strings, decode_payload_lossless, encode_text, parse_brace_text, root_signature, scalar
@@ -483,6 +486,11 @@ def classify_payload(data: bytes, *, include_text: bool = False, include_tree: b
stream["text"] = declared_prefix["text"]
stream["text_preview"] = str(declared_prefix["text"] or "").replace("\x00", "")[:500]
text = decoded.get("text")
xml_root = None
if isinstance(text, str):
match = XML_ROOT_RE.match(text.lstrip("\ufeff"))
if match:
xml_root = match.group(1).rsplit(":", 1)[-1]
tree = None
root = None
base64_decoded: list[dict[str, Any]] = []
@@ -505,6 +513,7 @@ def classify_payload(data: bytes, *, include_text: bool = False, include_tree: b
"sha1": sha1_hex(data),
"payload_sha1": sha1_hex(bytes(payload)),
"markers": markers,
**({"xml_root": xml_root} if xml_root else {}),
"root": root,
"role": classify_role(root_marker, markers, text, stream_blocks, base64_decoded),
"strings_sample": strings_sample,
+37 -7
View File
@@ -75,22 +75,20 @@ def query_without_line_comments(text: str) -> str:
def node_path(root: ET.Element, target: ET.Element) -> str:
"""Produce a stable, human-readable evidence path without XML prefixes."""
def visit(node: ET.Element, prefix: str) -> str | None:
name = local_name(node.tag)
current = f"{prefix}/{name}" if prefix else f"/{name}"
def visit(node: ET.Element, current: str) -> str | None:
if node is target:
return current
positions: dict[str, int] = {}
for child in node:
child_name = local_name(child.tag)
positions[child_name] = positions.get(child_name, 0) + 1
child_prefix = f"{current}[{positions[child_name]}]"
found = visit(child, child_prefix)
suffix = f"[{positions[child_name]}]" if positions[child_name] > 1 else ""
found = visit(child, f"{current}/{child_name}{suffix}")
if found:
return found
return None
return visit(root, "") or "/"
return visit(root, f"/{local_name(root.tag)}[1]") or "/"
def xml_from_scd_payload(data: bytes) -> tuple[ET.Element | None, dict[str, Any]]:
@@ -150,6 +148,9 @@ def scd_node_item(root: ET.Element, node: ET.Element, category: str) -> dict[str
expression = child_text(node, "expression")
if expression:
item["expression"] = expression
presentation = child_text(node, "presentation")
if presentation:
item["presentation"] = presentation
query = child_text(node, "query")
if query:
item["query"] = query
@@ -163,6 +164,10 @@ def scd_node_item(root: ET.Element, node: ET.Element, category: str) -> dict[str
item["value_type"] = value_type
if category == "datasets":
item["type"] = node.attrib.get("{http://www.w3.org/2001/XMLSchema-instance}type") or node.attrib.get("type") or ""
if category == "data_sources":
data_source_type = child_text(node, "dataSourceType")
if data_source_type:
item["data_source_type"] = data_source_type
return item
@@ -173,7 +178,7 @@ def inspect_scd_payload(data: bytes, *, sections: list[str] | None = None) -> di
synthesized from report code or form attributes.
"""
requested = sections or ["parameters", "datasets", "fields", "calculated_fields", "resources", "settings", "variants", "total_fields"]
requested = sections or ["data_sources", "parameters", "datasets", "fields", "calculated_fields", "resources", "groupings", "settings", "variants", "total_fields"]
root, container = xml_from_scd_payload(data)
if root is None:
return {"status": "partial", "container": container, "sections": {name: [] for name in requested}}
@@ -184,12 +189,29 @@ def inspect_scd_payload(data: bytes, *, sections: list[str] | None = None) -> di
"container": {**container, "code": "SCD_SCHEMA_NODE_NOT_FOUND"},
"sections": {name: [] for name in requested},
}
# Keep a complete, evidence-backed map of the XML shape alongside the
# curated semantic sections below. 1C releases and SCD variants may add
# nodes that this adapter does not yet assign a business meaning to. A
# tag/count/path outline makes those nodes visible to callers without
# guessing that (for example) a form property is a dataset or layout.
top_level_counts: dict[str, int] = {}
top_level_paths: dict[str, list[str]] = {}
for child in schema:
tag = local_name(child.tag)
top_level_counts[tag] = top_level_counts.get(tag, 0) + 1
top_level_paths.setdefault(tag, []).append(node_path(schema, child))
xml_tag_counts: dict[str, int] = {}
for node in schema.iter():
tag = local_name(node.tag)
xml_tag_counts[tag] = xml_tag_counts.get(tag, 0) + 1
node_names = {
"data_sources": {"dataSource"},
"parameters": {"parameter"},
"datasets": {"dataSet"},
"fields": {"field"},
"calculated_fields": {"calculatedField"},
"resources": {"resource"},
"groupings": {"group"},
"settings": {"settings", "Settings"},
"variants": {"settingsVariant", "variant"},
"total_fields": {"totalField"},
@@ -220,6 +242,14 @@ def inspect_scd_payload(data: bytes, *, sections: list[str] | None = None) -> di
query_references.append({"dataset": dataset.get("name"), "parameters": references})
analysis = {
"kind": "raw_query_parameter_token_scan",
"schema_outline": {
"kind": "exact_xml_tag_inventory",
"top_level": [
{"tag": tag, "count": top_level_counts[tag], "paths": top_level_paths[tag]}
for tag in sorted(top_level_counts, key=str.casefold)
],
"all_tag_counts": dict(sorted(xml_tag_counts.items(), key=lambda item: item[0].casefold())),
},
"declared_parameters": declared,
"query_parameter_references": query_references,
"referenced_not_declared_in_schema": sorted(
@@ -0,0 +1,69 @@
"""Read-only manifest collector for a 1C SQL configuration storage.
The REST endpoint caps a single storage.files.list response. GUID-prefix
partitioning keeps every request below that cap and makes the manifest
reproducible without SQL credentials in source control.
"""
from __future__ import annotations
import argparse
import json
import urllib.request
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
PREFIXES = [*"0123456789abcdef", "root"]
def rpc(url: str, method: str, payload: dict) -> dict:
request = urllib.request.Request(
url.rstrip("/") + "/rpc",
data=json.dumps({"method": method, "payload": payload}).encode("utf-8"),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(request, timeout=180) as response:
return json.loads(response.read().decode("utf-8"))
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--url", required=True)
parser.add_argument("--base-id", required=True)
parser.add_argument("--table", default="Config")
parser.add_argument("--output", required=True, type=Path)
args = parser.parse_args()
files: list[dict] = []
partitions: list[dict] = []
for prefix in PREFIXES:
result = rpc(args.url, "storage.files.list", {
"base_id": args.base_id, "table": args.table, "prefix": prefix,
"limit": 5000, "diagnostic": True,
})
rows = result.get("files") if result.get("status") == "ok" else None
if not isinstance(rows, list):
raise RuntimeError(f"{prefix}: {result}")
if len(rows) >= 5000:
raise RuntimeError(f"{prefix}: response reached the cap; split this prefix further")
files.extend(rows)
partitions.append({"prefix": prefix, "files": len(rows)})
suffixes = Counter(Path(str(row.get("FileName") or "")).suffix or "<descriptor>" for row in files)
manifest = {
"schema": "onec_config_storage_manifest.v1",
"created_at": datetime.now(timezone.utc).isoformat(),
"source": {"base_id": args.base_id, "table": args.table},
"partitions": partitions,
"counts": {"files": len(files), "compressed_bytes": sum(int(row.get("Bytes") or 0) for row in files), "suffixes": dict(sorted(suffixes.items()))},
"files": files,
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+166 -1
View File
@@ -38,6 +38,7 @@ from parser.bsl_validation import replace_routine_text, routine_blocks, text_sha
from parser.form_payload import command_handler_links, data_path_reference_from_node, decode_form_payload, dynamic_list_fields, dynamic_list_settings, embedded_table_addition_row, enrich_form_common_semantic, enrich_item_data_paths, enrich_item_reference_semantics, event_handlers, event_handlers_from_node, form_common_semantic, form_item_coverage_summary, section_record_semantic_properties # noqa: E402
from parser.form_xml import decode_form_xml # noqa: E402
from parser.scd_payload import compare_scd_semantics, inspect_scd_payload, plan_scd_scalar_patch # noqa: E402
from parser.appearance_payload import inspect_appearance_payload # noqa: E402
from parser.dbnames import parse_dbnames_bytes, parse_dbnames_version_bytes # noqa: E402
sys.path.insert(0, str(ROOT / "plugins" / "1c" / "connector"))
import adapter_1c_server as adapter_server # noqa: E402
@@ -211,14 +212,16 @@ def test_dbnames_version_parser_accepts_utf8_bom_scalar_version() -> None:
def test_scd_payload_decoder_reads_xml_after_platform_prefix() -> None:
xml = """<?xml version="1.0" encoding="UTF-8"?>
<SchemaFile><dataCompositionSchema>
<dataSource><name>ИсточникДанных1</name><dataSourceType>Local</dataSourceType></dataSource>
<dataSet xsi:type="DataSetQuery" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><name>ОсновнойНабор</name><query>ВЫБРАТЬ &amp;Период КАК Период // &amp;Скрытый КАК Скрытый</query></dataSet>
<parameter><name>Период</name><valueType>xs:dateTime</valueType></parameter>
<field><dataPath>Период</dataPath></field>
<field><dataPath>Сумма</dataPath></field>
<calculatedField><name>СуммаСНалогом</name><expression>Сумма * 1.2</expression></calculatedField>
<resource><name>Сумма</name><expression>Сумма</expression></resource>
<group>Период</group>
<totalField>Сумма</totalField>
<settingsVariant><name>Основной</name></settingsVariant>
<settingsVariant><name>Основной</name><presentation><item><lang>ru</lang><content>Основной вариант</content></item></presentation></settingsVariant>
</dataCompositionSchema></SchemaFile>"""
payload = b"\x00\x00\x00\x00\x01\x00\x00\x00" + xml.encode("utf-8")
@@ -226,16 +229,85 @@ def test_scd_payload_decoder_reads_xml_after_platform_prefix() -> None:
assert result["status"] == "ok"
assert result["container"]["compression"] == "raw_deflate"
assert result["sections"]["data_sources"] == [{"name": "ИсточникДанных1", "source": {"kind": "scd_xml", "path": "/dataCompositionSchema[1]/dataSource"}, "data_source_type": "Local"}]
assert result["sections"]["parameters"] == [{"name": "Период", "source": {"kind": "scd_xml", "path": "/dataCompositionSchema[1]/parameter"}, "value_type": "xs:dateTime"}]
assert result["sections"]["datasets"][0]["query"] == "ВЫБРАТЬ &Период КАК Период // &Скрытый КАК Скрытый"
assert result["sections"]["calculated_fields"][0]["expression"] == "Сумма * 1.2"
assert result["sections"]["groupings"] == [{"name": "Период", "source": {"kind": "scd_xml", "path": "/dataCompositionSchema[1]/group"}}]
assert result["sections"]["variants"][0]["name"] == "Основной"
assert result["sections"]["variants"][0]["presentation"] == "ruОсновной вариант"
assert result["analysis"]["query_parameter_references"] == [{"dataset": "ОсновнойНабор", "parameters": ["Период"]}]
assert result["analysis"]["schema_outline"] == {
"kind": "exact_xml_tag_inventory",
"top_level": [
{"tag": "calculatedField", "count": 1, "paths": ["/dataCompositionSchema[1]/calculatedField"]},
{"tag": "dataSet", "count": 1, "paths": ["/dataCompositionSchema[1]/dataSet"]},
{"tag": "dataSource", "count": 1, "paths": ["/dataCompositionSchema[1]/dataSource"]},
{"tag": "field", "count": 2, "paths": ["/dataCompositionSchema[1]/field", "/dataCompositionSchema[1]/field[2]"]},
{"tag": "group", "count": 1, "paths": ["/dataCompositionSchema[1]/group"]},
{"tag": "parameter", "count": 1, "paths": ["/dataCompositionSchema[1]/parameter"]},
{"tag": "resource", "count": 1, "paths": ["/dataCompositionSchema[1]/resource"]},
{"tag": "settingsVariant", "count": 1, "paths": ["/dataCompositionSchema[1]/settingsVariant"]},
{"tag": "totalField", "count": 1, "paths": ["/dataCompositionSchema[1]/totalField"]},
],
"all_tag_counts": {"calculatedField": 1, "content": 1, "dataCompositionSchema": 1, "dataPath": 2, "dataSet": 1, "dataSource": 1, "dataSourceType": 1, "expression": 2, "field": 2, "group": 1, "item": 1, "lang": 1, "name": 6, "parameter": 1, "presentation": 1, "query": 1, "resource": 1, "settingsVariant": 1, "totalField": 1, "valueType": 1},
}
assert result["analysis"]["referenced_not_declared_in_schema"] == []
assert result["analysis"]["total_field_references"] == {"fields": ["Сумма"], "missing_from_declared_fields": [], "status": "checked"}
assert result["analysis"]["settings_context"] == {"status": "not_present", "sections": {}}
def test_appearance_payload_decoder_reads_xml_rules() -> None:
xml = """<?xml version="1.0" encoding="UTF-8"?>
<AppearanceTemplate><item><parameter>Заголовок</parameter><value>style:Title</value></item><appearance><item><parameter>ЦветФона</parameter><value type="Line" ref="style:Blue"><style type="Solid">Solid</style></value></item></appearance><style><name>Title</name></style></AppearanceTemplate>"""
result = inspect_appearance_payload(compress_payload(b"\x00\x01" + xml.encode("utf-8"), "raw_deflate"))
assert result["status"] == "ok"
assert result["items"] == [{"ordinal": 1, "parameter": "Заголовок", "value": "style:Title"}]
assert result["appearances"] == [{"ordinal": 1, "rules": [{"parameter": "ЦветФона", "value": "Solid", "value_type": "Line", "value_attributes": {"type": "Line", "ref": "style:Blue"}, "value_children": [{"tag": "style", "value": "Solid", "attributes": {"type": "Solid"}}]}]}]
assert result["outline"]["tag_counts"] == {"AppearanceTemplate": 1, "appearance": 1, "item": 2, "name": 1, "parameter": 2, "style": 2, "value": 2}
assert result["diagnostics"]["write"] == "unsupported_until_reverse_codec_fixture"
def test_appearance_inspect_resolves_public_common_template(monkeypatch: pytest.MonkeyPatch) -> None:
source = compress_payload('<?xml version="1.0"?><AppearanceTemplate><item><parameter>Заголовок</parameter><value>x</value></item></AppearanceTemplate>'.encode("utf-8"), "raw_deflate")
monkeypatch.setattr(adapter_server, "get_object", lambda *_args, **_kwargs: {"status": "ok", "object": {"guid": "a" * 36, "kind": "CommonTemplate", "name": "Оформление"}})
monkeypatch.setattr(adapter_server, "read_storage_file_bytes", lambda *_args, **_kwargs: (source, {}, None))
result = adapter_server.appearance_inspect({"base_id": "upo_test", "template": "Оформление"})
assert result["status"] == "ok"
assert result["template"]["name"] == "Оформление"
assert result["appearance"]["items"][0]["parameter"] == "Заголовок"
assert result["template_type"]["status"] == "unresolved"
def test_report_inspect_keeps_forms_separate_from_scd(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "metadata_object_components", lambda _payload: {
"status": "ok", "object": {"kind": "Report", "name": "Продажи", "guid": "r" * 36},
"components": [
{"component_kind": "form", "name": "ОсновнаяСхемаКомпоновкиДанных"},
{"component_kind": "scd", "name": "ОсновнаяСхема", "guid": "s" * 36},
], "unresolved": [{"area": "templates", "status": "partial"}],
})
result = adapter_server.report_inspect({"base_id": "upo_test", "report": "Продажи"})
assert result["status"] == "ok"
assert [item["name"] for item in result["forms"]] == ["ОсновнаяСхемаКомпоновкиДанных"]
assert [item["name"] for item in result["scd"]] == ["ОсновнаяСхема"]
assert result["unresolved"][0]["area"] == "templates"
def test_rpc_dispatches_report_and_appearance_inspection(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "report_inspect", lambda payload: {"status": "ok", "method": "report.inspect", "report": payload["report"]})
monkeypatch.setattr(adapter_server, "appearance_inspect", lambda payload: {"status": "ok", "method": "appearance.inspect", "template": payload["template"]})
assert call_method("report.inspect", {"base_id": "upo_test", "report": "Продажи"})["report"] == "Продажи"
assert call_method("appearance.inspect", {"base_id": "upo_test", "template": "Оформление"})["template"] == "Оформление"
def test_scd_scalar_patch_preserves_platform_prefix_and_trailer() -> None:
xml = """<?xml version="1.0" encoding="UTF-8"?>
<SchemaFile><dataCompositionSchema>
@@ -6897,6 +6969,7 @@ def test_public_template_summary_exposes_known_platform_types() -> None:
assert summary["template_type_candidates"][0]["confidence"] == "high"
assert "Текстовый документ" in {item["name"] for item in summary["known_platform_types"]}
assert "Внешняя компонента" in {item["name"] for item in summary["known_platform_types"]}
assert next(item for item in summary["known_platform_types"] if item["id"] == "data_composition_schema")["status"] == "read_and_scalar_patch_supported"
assert {item["name"] for item in summary["known_platform_types"]} == {
"Табличный документ",
"Текстовый документ",
@@ -6932,6 +7005,92 @@ def test_public_template_summary_uses_declared_template_type_alias() -> None:
assert summary["template_type_candidates"][0]["confidence"] == "high"
def test_public_template_summary_recognizes_live_appearance_xml_root() -> None:
summary = adapter_server.public_template_summary(
[{"role": "content", "classification": {"xml_root": "AppearanceTemplate"}, "features": {}}]
)
assert summary["platform_type"] == "Двоичные данные"
assert summary["template_type_candidates"][0]["id"] == "binary_data"
assert summary["content_type_evidence"] == {
"xml_roots": ["AppearanceTemplate"],
"meaning": "payload_carrier_only_not_platform_template_type",
}
def test_public_template_summary_exposes_unmapped_descriptor_type_code() -> None:
summary = adapter_server.public_template_summary(
[
{
"role": "metadata_payload",
"classification": {
"tree": {
"type": "list",
"items": [
{"type": "atom", "value": "1"},
{"type": "list", "items": [
{"type": "atom", "value": "4"},
{"type": "list", "items": []},
{"type": "atom", "value": "7"},
]},
],
},
},
},
{"role": "content", "classification": {"xml_root": "AppearanceTemplate"}, "features": {}},
]
)
assert summary["platform_type"] == "Двоичные данные"
assert summary["descriptor_type_evidence"]["codes"] == ["7"]
assert summary["descriptor_type_evidence"]["status"] == "unresolved_mapping"
def test_common_template_inventory_uses_parts_classifier(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "resolve_object_guid", lambda *_args, **_kwargs: ("a" * 36, None, {"name": "Оформление", "ref": "CommonTemplate.Оформление"}, None))
monkeypatch.setattr(adapter_server, "read_template_by_guid", lambda _payload: {"status": "ok", "source": {"kind": "live_sql"}, "templates": [{"guid": "a" * 36}]})
monkeypatch.setattr(adapter_server, "metadata_object_parts", lambda _payload: {"parts": [{"role": "content", "classification": {"xml_root": "AppearanceTemplate"}, "features": {}}]})
result = adapter_server.metadata_object_templates({"base_id": "upo_test", "kind": "CommonTemplate", "name": "Оформление"})
assert result["templates"][0]["platform_type"] == "Двоичные данные"
assert result["templates"][0]["content_type_evidence"]["xml_roots"] == ["AppearanceTemplate"]
def test_common_template_inventory_preserves_unmapped_descriptor_code(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "resolve_object_guid", lambda *_args, **_kwargs: ("a" * 36, None, {"name": "Оформление", "ref": "CommonTemplate.Оформление"}, None))
monkeypatch.setattr(adapter_server, "read_template_by_guid", lambda _payload: {"status": "ok", "source": {"kind": "live_sql"}, "templates": [{"guid": "a" * 36}]})
monkeypatch.setattr(adapter_server, "metadata_object_parts", lambda _payload: {"parts": [{
"classification": {"role": "metadata_payload", "tree": {"type": "list", "items": [
{"type": "atom", "value": "1"},
{"type": "list", "items": [
{"type": "atom", "value": "4"}, {"type": "list", "items": []}, {"type": "atom", "value": "7"},
]},
]}},
}, {"classification": {"role": "binary_or_unknown_payload", "xml_root": "AppearanceTemplate"}, "features": {}}]})
result = adapter_server.metadata_object_templates({"base_id": "upo_test", "kind": "CommonTemplate", "name": "Оформление"})
assert result["templates"][0]["descriptor_type_evidence"]["codes"] == ["7"]
assert result["templates"][0]["name"] == "Оформление"
def test_top_level_common_template_summary_preserves_descriptor_code(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter_server, "resolve_object_guid", lambda *_args, **_kwargs: ("a" * 36, None, {"name": "Оформление", "ref": "CommonTemplate.Оформление"}, None))
monkeypatch.setattr(adapter_server, "read_template_by_guid", lambda _payload: {"status": "ok", "source": {"kind": "live_sql"}, "templates": [{"guid": "a" * 36}]})
monkeypatch.setattr(adapter_server, "metadata_object_parts", lambda _payload: {"parts": [{
"classification": {"role": "metadata_payload", "tree": {"type": "list", "items": [
{"type": "atom", "value": "1"}, {"type": "list", "items": [
{"type": "atom", "value": "4"}, {"type": "list", "items": []}, {"type": "atom", "value": "7"},
]},
]}},
}, {"classification": {"role": "binary_or_unknown_payload", "xml_root": "AppearanceTemplate"}}]})
result = adapter_server.metadata_object_templates({"base_id": "upo_test", "kind": "CommonTemplate", "name": "Оформление"})
assert result["templates"][0]["descriptor_type_evidence"]["codes"] == ["7"]
def test_moxel_template_artifact_kind_tracks_coordinate_hint_schema() -> None:
assert adapter_server.MOXEL_TEMPLATE_ARTIFACT_KIND == "template_part_moxel_v11"
@@ -10384,6 +10543,12 @@ def test_metadata_object_templates_includes_decoded_data_composition_schema(monk
assert result["templates"][0]["name"] == "ОсновнаяСхемаКомпоновкиДанных"
assert result["templates"][0]["kind"] == "data_composition_schema"
assert result["templates"][0]["match_by"] == "name_exact"
assert result["templates"][0]["platform_type"] == "Схема компоновки данных"
assert result["templates"][0]["template_type_candidates"] == [{
**adapter_server.onec_template_type_by_id("data_composition_schema"),
"confidence": "high",
"evidence": ["confirmed Template identity"],
}]
def test_templates_bindings_preserves_data_composition_schema_kind(monkeypatch: pytest.MonkeyPatch) -> None: