fix(1c): separate template descriptor type from payload carrier
This commit is contained in:
@@ -96,7 +96,10 @@ from its name or a guessed `.0` stream.
|
|||||||
```
|
```
|
||||||
|
|
||||||
`appearance.inspect` is a separate read-only reader for a base
|
`appearance.inspect` is a separate read-only reader for a base
|
||||||
`CommonTemplate` whose `.0` carrier is confirmed as `AppearanceTemplate` XML.
|
`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
|
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
|
outline. It has no write operation, and an extension template returns explicit
|
||||||
`unsupported` until an extension carrier route is proven.
|
`unsupported` until an extension carrier route is proven.
|
||||||
|
|||||||
@@ -102,14 +102,23 @@ Read-only diagnostic inventory `CommonTemplate` в `upo_test` уже дал тр
|
|||||||
|
|
||||||
| Fixture | Template GUID | Доказательство типа | Разрешённое действие |
|
| Fixture | Template GUID | Доказательство типа | Разрешённое действие |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `ОформлениеОтчетовБежевый` | `f49709d3-13ea-4d67-9934-9d93f8ba1c21` | XML root `AppearanceTemplate`, DCS appearance namespace | read-only, отдельный appearance decoder pending |
|
| `ОформлениеОтчетовБежевый` | `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 |
|
| `Шаблон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 |
|
| `Компонента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
|
Из этого следует, что `AppearanceTemplate` нельзя относить к spreadsheet
|
||||||
только из-за вложенного упоминания `SpreadsheetDocument` в XML. Для него
|
только из-за вложенного упоминания `SpreadsheetDocument` в XML. Равным
|
||||||
нужен собственный root-aware classifier и отдельный XML decoder; обратная
|
образом XML root нельзя объявлять платформенным типом макета: он выбирает
|
||||||
запись по-прежнему запрещена до controlled before/after fixture.
|
только 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`: он возвращает
|
Reader реализован в `plugins/1c/parser/appearance_payload.py`: он возвращает
|
||||||
контейнер, все верхнеуровневые правила `item(parameter, value)` и точный
|
контейнер, все верхнеуровневые правила `item(parameter, value)` и точный
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ ONEC_TEMPLATE_PLATFORM_TYPES = [
|
|||||||
"xml_type": "DataCompositionAppearanceTemplate",
|
"xml_type": "DataCompositionAppearanceTemplate",
|
||||||
"decoder": "data_composition_appearance",
|
"decoder": "data_composition_appearance",
|
||||||
"status": "not_implemented",
|
"status": "not_implemented",
|
||||||
"aliases": ["МакетОформленияКомпоновкиДанных", "DataCompositionAppearanceTemplate", "AppearanceTemplate"],
|
"aliases": ["МакетОформленияКомпоновкиДанных", "DataCompositionAppearanceTemplate"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "external_component",
|
"id": "external_component",
|
||||||
@@ -764,7 +764,7 @@ METHODS = [
|
|||||||
{"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": "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": "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": "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 is confirmed as AppearanceTemplate XML. Returns XML-backed appearance rules; no write route is exposed."},
|
{"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", "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.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."},
|
{"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."},
|
||||||
@@ -4089,7 +4089,6 @@ def declared_template_platform_type_from_part(part: dict[str, Any]) -> dict[str,
|
|||||||
classification.get("template_type"),
|
classification.get("template_type"),
|
||||||
classification.get("platform_type_id"),
|
classification.get("platform_type_id"),
|
||||||
classification.get("platform_type"),
|
classification.get("platform_type"),
|
||||||
classification.get("xml_root"),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
candidates.extend(classification.get("strings_sample") or [])
|
candidates.extend(classification.get("strings_sample") or [])
|
||||||
@@ -4100,6 +4099,24 @@ def declared_template_platform_type_from_part(part: dict[str, Any]) -> dict[str,
|
|||||||
return None
|
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.
|
||||||
|
"""
|
||||||
|
if str(part.get("role") or "") != "metadata_payload":
|
||||||
|
return None
|
||||||
|
classification = part.get("classification") if isinstance(part.get("classification"), dict) else {}
|
||||||
|
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]]:
|
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]] = []
|
candidates: list[dict[str, Any]] = []
|
||||||
declared = normalize_onec_template_platform_type(features.get("declared_platform_type_id") or features.get("declared_platform_type"))
|
declared = normalize_onec_template_platform_type(features.get("declared_platform_type_id") or features.get("declared_platform_type"))
|
||||||
@@ -4143,14 +4160,22 @@ def public_template_summary(
|
|||||||
(template_type for template_type in (declared_template_platform_type_from_part(part) for part in parts) if template_type),
|
(template_type for template_type in (declared_template_platform_type_from_part(part) for part in parts) if template_type),
|
||||||
None,
|
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"
|
declared_type_evidence = "TemplateType metadata"
|
||||||
if any(
|
xml_roots = sorted(
|
||||||
normalize_onec_template_platform_type(
|
{
|
||||||
((part.get("classification") or {}).get("xml_root") if isinstance(part.get("classification"), dict) else None)
|
str((part.get("classification") or {}).get("xml_root"))
|
||||||
)
|
|
||||||
for part in parts
|
for part in parts
|
||||||
):
|
if isinstance(part.get("classification"), dict)
|
||||||
declared_type_evidence = "payload XML root"
|
and (part.get("classification") or {}).get("xml_root")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
descriptor_type_codes = sorted(
|
||||||
|
{code for code in (template_descriptor_type_code_from_part(part) for part in parts) if code is not None},
|
||||||
|
key=int,
|
||||||
|
)
|
||||||
features = {
|
features = {
|
||||||
"tabular_document": any(((part.get("features") or {}).get("tabular_document")) for part in content_parts),
|
"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),
|
"html": any(((part.get("features") or {}).get("html")) for part in content_parts),
|
||||||
@@ -4185,6 +4210,17 @@ def public_template_summary(
|
|||||||
"template_type_candidates": template_type_candidates,
|
"template_type_candidates": template_type_candidates,
|
||||||
"counts": {"parts": len(parts), "content_parts": len(content_parts)},
|
"counts": {"parts": len(parts), "content_parts": len(content_parts)},
|
||||||
}
|
}
|
||||||
|
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:
|
if confirmed_platform_type:
|
||||||
summary["format"] = confirmed_platform_type.get("xml_type") or confirmed_platform_type.get("id")
|
summary["format"] = confirmed_platform_type.get("xml_type") or confirmed_platform_type.get("id")
|
||||||
summary["platform_type"] = confirmed_platform_type.get("name")
|
summary["platform_type"] = confirmed_platform_type.get("name")
|
||||||
@@ -36100,7 +36136,19 @@ def appearance_inspect(payload: dict[str, Any]) -> dict[str, Any]:
|
|||||||
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"}}
|
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
|
from parser.appearance_payload import inspect_appearance_payload
|
||||||
decoded = inspect_appearance_payload(data)
|
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, "activation_state": "working_not_runtime_applied"}
|
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]:
|
def report_inspect(payload: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -280,6 +280,7 @@ def test_appearance_inspect_resolves_public_common_template(monkeypatch: pytest.
|
|||||||
assert result["status"] == "ok"
|
assert result["status"] == "ok"
|
||||||
assert result["template"]["name"] == "Оформление"
|
assert result["template"]["name"] == "Оформление"
|
||||||
assert result["appearance"]["items"][0]["parameter"] == "Заголовок"
|
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:
|
def test_report_inspect_keeps_forms_separate_from_scd(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
@@ -7009,9 +7010,40 @@ def test_public_template_summary_recognizes_live_appearance_xml_root() -> None:
|
|||||||
[{"role": "content", "classification": {"xml_root": "AppearanceTemplate"}, "features": {}}]
|
[{"role": "content", "classification": {"xml_root": "AppearanceTemplate"}, "features": {}}]
|
||||||
)
|
)
|
||||||
|
|
||||||
assert summary["platform_type"] == "Макет оформления компоновки данных"
|
assert summary["platform_type"] == "Двоичные данные"
|
||||||
assert summary["template_type_candidates"][0]["id"] == "data_composition_appearance_template"
|
assert summary["template_type_candidates"][0]["id"] == "binary_data"
|
||||||
assert summary["template_type_candidates"][0]["evidence"] == ["payload XML root"]
|
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:
|
def test_common_template_inventory_uses_parts_classifier(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
@@ -7021,7 +7053,8 @@ def test_common_template_inventory_uses_parts_classifier(monkeypatch: pytest.Mon
|
|||||||
|
|
||||||
result = adapter_server.metadata_object_templates({"base_id": "upo_test", "kind": "CommonTemplate", "name": "Оформление"})
|
result = adapter_server.metadata_object_templates({"base_id": "upo_test", "kind": "CommonTemplate", "name": "Оформление"})
|
||||||
|
|
||||||
assert result["templates"][0]["platform_type"] == "Макет оформления компоновки данных"
|
assert result["templates"][0]["platform_type"] == "Двоичные данные"
|
||||||
|
assert result["templates"][0]["content_type_evidence"]["xml_roots"] == ["AppearanceTemplate"]
|
||||||
assert result["templates"][0]["name"] == "Оформление"
|
assert result["templates"][0]["name"] == "Оформление"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user