validibot-shared 0.4.4__tar.gz → 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/PKG-INFO +4 -4
  2. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/pyproject.toml +3 -3
  3. validibot_shared-0.5.1/tests/test_evidence_manifest.py +160 -0
  4. validibot_shared-0.5.1/validibot_shared/__init__.py +32 -0
  5. validibot_shared-0.5.1/validibot_shared/evidence/__init__.py +26 -0
  6. validibot_shared-0.5.1/validibot_shared/evidence/manifest.py +237 -0
  7. validibot_shared-0.4.4/validibot_shared/__init__.py +0 -16
  8. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/.gitignore +0 -0
  9. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/LICENSE +0 -0
  10. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/NOTICE +0 -0
  11. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/README.md +0 -0
  12. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/tests/__init__.py +0 -0
  13. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/tests/test_energyplus_envelopes.py +0 -0
  14. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/tests/test_energyplus_models.py +0 -0
  15. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/tests/test_fmu_envelopes.py +0 -0
  16. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/tests/test_fmu_models.py +0 -0
  17. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/tests/test_package_init.py +0 -0
  18. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/tests/test_validations_envelopes.py +0 -0
  19. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/energyplus/__init__.py +0 -0
  20. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/energyplus/envelopes.py +0 -0
  21. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/energyplus/models.py +0 -0
  22. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/fmu/__init__.py +0 -0
  23. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/fmu/envelopes.py +0 -0
  24. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/fmu/models.py +0 -0
  25. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/py.typed +0 -0
  26. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/validations/__init__.py +0 -0
  27. {validibot_shared-0.4.4 → validibot_shared-0.5.1}/validibot_shared/validations/envelopes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: validibot-shared
3
- Version: 0.4.4
3
+ Version: 0.5.1
4
4
  Summary: Shared library for data interchange between Validibot and validator containers
5
5
  Project-URL: Homepage, https://validibot.com
6
6
  Project-URL: Documentation, https://docs.validibot.com
@@ -24,10 +24,10 @@ Classifier: Programming Language :: Python :: 3.13
24
24
  Classifier: Topic :: Scientific/Engineering
25
25
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
26
26
  Requires-Python: >=3.10
27
- Requires-Dist: pydantic==2.13.3
27
+ Requires-Dist: pydantic<3.0,>=2.13
28
28
  Provides-Extra: dev
29
- Requires-Dist: pytest==9.0.2; extra == 'dev'
30
- Requires-Dist: ruff==0.14.8; extra == 'dev'
29
+ Requires-Dist: pytest==9.0.3; extra == 'dev'
30
+ Requires-Dist: ruff==0.15.12; extra == 'dev'
31
31
  Description-Content-Type: text/markdown
32
32
 
33
33
  <div align="center">
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "validibot-shared"
7
- version = "0.4.4"
7
+ version = "0.5.1"
8
8
  description = "Shared library for data interchange between Validibot and validator containers"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -32,10 +32,10 @@ classifiers = [
32
32
  "Topic :: Software Development :: Libraries :: Python Modules",
33
33
  "Topic :: Scientific/Engineering",
34
34
  ]
35
- dependencies = ["pydantic==2.13.3"]
35
+ dependencies = ["pydantic>=2.13,<3.0"]
36
36
 
37
37
  [project.optional-dependencies]
38
- dev = ["pytest==9.0.2", "ruff==0.14.8"]
38
+ dev = ["pytest==9.0.3", "ruff==0.15.12"]
39
39
 
40
40
  [project.urls]
41
41
  Homepage = "https://validibot.com"
@@ -0,0 +1,160 @@
1
+ """Tests for ``validibot_shared.evidence`` schema (validibot.evidence.v1).
2
+
3
+ ADR-2026-04-27 Phase 4 Session A: pin the schema's contract at the
4
+ shared-package boundary. The Validibot Django app is one consumer;
5
+ external verifiers (validibot-pro, third-party tools) are others.
6
+ These tests verify the shape stays stable across releases.
7
+
8
+ What this file covers
9
+ =====================
10
+
11
+ 1. ``schema_version`` is pinned to ``validibot.evidence.v1`` and
12
+ rejected if other values are passed (Literal type-narrows).
13
+ 2. The model rejects unknown top-level keys (``extra="forbid"``).
14
+ 3. Frozen Pydantic models — instances cannot be mutated after
15
+ construction, which is the property the producer's hash
16
+ computation depends on.
17
+ 4. Optional fields default sensibly so producers can build a
18
+ minimal Session-A manifest without populating Session-B fields.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import pytest
24
+ from pydantic import ValidationError
25
+
26
+ from validibot_shared.evidence import (
27
+ SCHEMA_VERSION,
28
+ EvidenceManifest,
29
+ ManifestPayloadDigests,
30
+ ManifestRetentionInfo,
31
+ StepValidatorRecord,
32
+ WorkflowContractSnapshot,
33
+ )
34
+
35
+
36
+ def _minimal_manifest_kwargs():
37
+ """Build the smallest set of kwargs that produces a valid manifest."""
38
+ return {
39
+ "run_id": "abc-123",
40
+ "workflow_id": 1,
41
+ "workflow_slug": "compliance",
42
+ "workflow_version": "1.0",
43
+ "org_id": 42,
44
+ "executed_at": "2026-05-02T12:00:00+00:00",
45
+ "status": "SUCCEEDED",
46
+ "workflow_contract": WorkflowContractSnapshot(
47
+ allowed_file_types=["json"],
48
+ ),
49
+ "retention": ManifestRetentionInfo(retention_class="STORE_30_DAYS"),
50
+ }
51
+
52
+
53
+ # ──────────────────────────────────────────────────────────────────────
54
+ # Schema version contract
55
+ # ──────────────────────────────────────────────────────────────────────
56
+
57
+
58
+ class TestSchemaVersion:
59
+ def test_default_schema_version_is_v1(self):
60
+ manifest = EvidenceManifest(**_minimal_manifest_kwargs())
61
+ assert manifest.schema_version == "validibot.evidence.v1"
62
+
63
+ def test_schema_version_constant_matches(self):
64
+ assert SCHEMA_VERSION == "validibot.evidence.v1"
65
+
66
+ def test_rejects_other_schema_versions(self):
67
+ """Literal narrowing — passing a non-v1 string fails validation."""
68
+ with pytest.raises(ValidationError):
69
+ EvidenceManifest(
70
+ **_minimal_manifest_kwargs(),
71
+ schema_version="validibot.evidence.v2",
72
+ )
73
+
74
+
75
+ # ──────────────────────────────────────────────────────────────────────
76
+ # Strict shape (extra="forbid") and frozen instances
77
+ # ──────────────────────────────────────────────────────────────────────
78
+
79
+
80
+ class TestStrictShape:
81
+ def test_unknown_top_level_keys_rejected(self):
82
+ """Producers can't accidentally smuggle in undocumented data."""
83
+ with pytest.raises(ValidationError):
84
+ EvidenceManifest(
85
+ **_minimal_manifest_kwargs(),
86
+ extra_undeclared_field="oops",
87
+ )
88
+
89
+ def test_manifest_is_frozen(self):
90
+ """Instances cannot be mutated post-construction.
91
+
92
+ The hash producers compute over canonical JSON depends on
93
+ the fact that you can't tweak a field after the model is
94
+ built. ``frozen=True`` enforces that at the Pydantic level.
95
+ """
96
+ manifest = EvidenceManifest(**_minimal_manifest_kwargs())
97
+ with pytest.raises(ValidationError):
98
+ manifest.run_id = "tampered"
99
+
100
+
101
+ # ──────────────────────────────────────────────────────────────────────
102
+ # Optional fields and defaults (Session A vs Session B distinction)
103
+ # ──────────────────────────────────────────────────────────────────────
104
+
105
+
106
+ class TestOptionalFields:
107
+ def test_payload_digests_defaults_to_all_none(self):
108
+ """Session A: producers leave digests empty."""
109
+ manifest = EvidenceManifest(**_minimal_manifest_kwargs())
110
+ assert manifest.payload_digests.input_sha256 is None
111
+ assert manifest.payload_digests.output_envelope_sha256 is None
112
+
113
+ def test_session_b_can_populate_digests(self):
114
+ """Session B's redaction path drops in real hashes."""
115
+ manifest = EvidenceManifest(
116
+ **_minimal_manifest_kwargs(),
117
+ payload_digests=ManifestPayloadDigests(
118
+ input_sha256="a" * 64,
119
+ output_envelope_sha256="b" * 64,
120
+ ),
121
+ )
122
+ assert manifest.payload_digests.input_sha256 == "a" * 64
123
+
124
+ def test_input_schema_optional(self):
125
+ """Workflows without structured input contract leave it None."""
126
+ manifest = EvidenceManifest(**_minimal_manifest_kwargs())
127
+ assert manifest.input_schema is None
128
+
129
+ def test_steps_default_to_empty_list(self):
130
+ """A workflow with no steps still produces a valid manifest."""
131
+ manifest = EvidenceManifest(**_minimal_manifest_kwargs())
132
+ assert manifest.steps == []
133
+
134
+
135
+ # ──────────────────────────────────────────────────────────────────────
136
+ # StepValidatorRecord — semantic_digest can be None for legacy validators
137
+ # ──────────────────────────────────────────────────────────────────────
138
+
139
+
140
+ class TestStepValidatorRecord:
141
+ def test_semantic_digest_optional(self):
142
+ """Custom validators with no digest -> None in the manifest."""
143
+ record = StepValidatorRecord(
144
+ step_id=1,
145
+ step_order=0,
146
+ validator_slug="custom-validator",
147
+ validator_version="1.0",
148
+ )
149
+ assert record.validator_semantic_digest is None
150
+
151
+ def test_records_are_frozen(self):
152
+ """Step records cannot be mutated after construction."""
153
+ record = StepValidatorRecord(
154
+ step_id=1,
155
+ step_order=0,
156
+ validator_slug="x",
157
+ validator_version="1.0",
158
+ )
159
+ with pytest.raises(ValidationError):
160
+ record.validator_slug = "tampered"
@@ -0,0 +1,32 @@
1
+ """Shared domain models and utilities for Validibot."""
2
+
3
+ from validibot_shared.energyplus.models import (
4
+ EnergyPlusSimulationLogs,
5
+ EnergyPlusSimulationMetrics,
6
+ EnergyPlusSimulationOutputs,
7
+ )
8
+ from validibot_shared.evidence import (
9
+ SCHEMA_VERSION as EVIDENCE_SCHEMA_VERSION,
10
+ )
11
+ from validibot_shared.evidence import (
12
+ EvidenceManifest,
13
+ ManifestPayloadDigests,
14
+ ManifestRetentionInfo,
15
+ StepValidatorRecord,
16
+ WorkflowContractSnapshot,
17
+ )
18
+ from validibot_shared.fmu.models import FMUProbeResult, FMUVariableMeta
19
+
20
+ __all__ = [
21
+ "EVIDENCE_SCHEMA_VERSION",
22
+ "EnergyPlusSimulationLogs",
23
+ "EnergyPlusSimulationMetrics",
24
+ "EnergyPlusSimulationOutputs",
25
+ "EvidenceManifest",
26
+ "FMUProbeResult",
27
+ "FMUVariableMeta",
28
+ "ManifestPayloadDigests",
29
+ "ManifestRetentionInfo",
30
+ "StepValidatorRecord",
31
+ "WorkflowContractSnapshot",
32
+ ]
@@ -0,0 +1,26 @@
1
+ """Evidence-manifest schemas (``validibot.evidence.vN``).
2
+
3
+ Public API for both the Validibot Django app (which generates
4
+ manifests when validation runs complete) and the Pro / external
5
+ verifiers (which sign / verify them). The schemas are intentionally
6
+ storage-agnostic and Django-free so any consumer can validate a
7
+ manifest without pulling in the application stack.
8
+ """
9
+
10
+ from validibot_shared.evidence.manifest import (
11
+ SCHEMA_VERSION,
12
+ EvidenceManifest,
13
+ ManifestPayloadDigests,
14
+ ManifestRetentionInfo,
15
+ StepValidatorRecord,
16
+ WorkflowContractSnapshot,
17
+ )
18
+
19
+ __all__ = [
20
+ "SCHEMA_VERSION",
21
+ "EvidenceManifest",
22
+ "ManifestPayloadDigests",
23
+ "ManifestRetentionInfo",
24
+ "StepValidatorRecord",
25
+ "WorkflowContractSnapshot",
26
+ ]
@@ -0,0 +1,237 @@
1
+ """Pydantic schema for validibot evidence manifests (``validibot.evidence.v1``).
2
+
3
+ ADR-2026-04-27 (Validibot Trust ADR), Phase 4: every completed
4
+ validation run produces a manifest documenting "these are the rules
5
+ and inputs run X was operating under." The manifest is the data
6
+ structure that makes evidence bundles, signed credentials, and
7
+ external verification possible.
8
+
9
+ Why this lives in ``validibot-shared``
10
+ ======================================
11
+
12
+ The manifest is the contract between *manifest producers* (the
13
+ Validibot Django app) and *verifiers* (the validibot-pro signing /
14
+ verification flow, or any external party that receives an exported
15
+ bundle). Pinning the schema in the shared package:
16
+
17
+ 1. Lets external verifiers depend on a single PyPI package without
18
+ pulling in the Django stack.
19
+ 2. Keeps producer and verifier locked to the same version of the
20
+ shape — additive changes preserve v1 by policy; breaking changes
21
+ require a v2 module beside this one.
22
+
23
+ Schema versioning policy
24
+ ========================
25
+
26
+ The string ``validibot.evidence.v1`` in :data:`SCHEMA_VERSION` is the
27
+ *compatibility contract*. Additive changes (new optional fields,
28
+ loosened validators) preserve v1. Removing or renaming fields, or
29
+ tightening a validator in a way that rejects previously-valid input,
30
+ requires a v2 module placed alongside this one (``manifest_v2.py``).
31
+
32
+ What's in the schema (Phase 4 phasing)
33
+ ======================================
34
+
35
+ Session A (this module): identity, workflow contract snapshot,
36
+ validator metadata per step, input contract, retention class, and
37
+ schema slots for fields Sessions B + C will populate.
38
+
39
+ Session B (deferred): the ``payload_digests`` slot gets populated
40
+ with input + output hashes; ``retention.redactions_applied`` lists
41
+ fields stripped under retention policy. The schema shape itself
42
+ does not change, only the values landing in optional fields.
43
+
44
+ Session C (deferred): credentials sign the canonical-JSON bytes of
45
+ this manifest externally. The signature does NOT live inside the
46
+ manifest — verifiers receive the manifest + a separate signature
47
+ file in the export bundle.
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ from typing import Literal
53
+
54
+ from pydantic import BaseModel, ConfigDict, Field
55
+
56
+ # The string consumers parse to know what manifest shape to expect.
57
+ # Bump only on breaking changes; additive changes stay v1.
58
+ SCHEMA_VERSION = "validibot.evidence.v1"
59
+
60
+
61
+ class StepValidatorRecord(BaseModel):
62
+ """Per-step validator identity at run time.
63
+
64
+ Pinned values: a step's validator is referenced by foreign key
65
+ on the application side, but the *meaning* of that reference
66
+ depends on the validator's ``semantic_digest`` at run time. We
67
+ snapshot the digest into the manifest so future verifiers can
68
+ prove "the validator behind this step had digest D when this run
69
+ executed."
70
+ """
71
+
72
+ model_config = ConfigDict(frozen=True)
73
+
74
+ step_id: int = Field(
75
+ description="Producer-side WorkflowStep primary key at run time."
76
+ )
77
+ step_order: int = Field(description="Execution position (lower = earlier).")
78
+ validator_slug: str = Field(description="Validator's stable slug.")
79
+ validator_version: str = Field(description="Validator's version string.")
80
+ # Optional because custom org validators have empty digests by
81
+ # design (no ValidatorConfig to hash). Producers' auditors flag
82
+ # this as a coverage gap.
83
+ validator_semantic_digest: str | None = Field(
84
+ default=None,
85
+ description=(
86
+ "SHA-256 hex of the validator's semantic config. None for "
87
+ "custom validators that aren't synced from a "
88
+ "ValidatorConfig — the manifest documents the absence so "
89
+ "verifiers know to expect a legacy-versioning gap there."
90
+ ),
91
+ )
92
+
93
+
94
+ class WorkflowContractSnapshot(BaseModel):
95
+ """Frozen snapshot of the workflow's launch-contract fields.
96
+
97
+ These are the fields that determine *what the workflow does*
98
+ when it runs (file types accepted, retention policy, agent
99
+ publication state). Pinning them in the manifest lets a future
100
+ verifier answer "what rules was this run operating under?"
101
+ without consulting the live producer database (which may have
102
+ moved on to a newer workflow version).
103
+ """
104
+
105
+ model_config = ConfigDict(frozen=True)
106
+
107
+ allowed_file_types: list[str] = Field(default_factory=list)
108
+ data_retention: str = ""
109
+ output_retention: str = ""
110
+ agent_billing_mode: str = ""
111
+ agent_price_cents: int | None = None
112
+ agent_max_launches_per_hour: int | None = None
113
+ agent_public_discovery: bool = False
114
+ agent_access_enabled: bool = False
115
+
116
+
117
+ class ManifestRetentionInfo(BaseModel):
118
+ """Retention class + applied-redaction summary.
119
+
120
+ Session A populates ``retention_class`` from the workflow's
121
+ ``data_retention`` so consumers know what the run agreed to.
122
+ Session B will populate ``redactions_applied`` with the list of
123
+ fields the retention policy stripped before serialisation. For
124
+ Session A the field is empty.
125
+ """
126
+
127
+ model_config = ConfigDict(frozen=True)
128
+
129
+ retention_class: str = Field(
130
+ description=(
131
+ "Workflow data-retention setting at run time "
132
+ "(e.g. 'DO_NOT_STORE', 'STORE_30_DAYS')."
133
+ ),
134
+ )
135
+ redactions_applied: list[str] = Field(
136
+ default_factory=list,
137
+ description=(
138
+ "Names of manifest fields that were redacted under this "
139
+ "retention class. Session A leaves this empty; Session B "
140
+ "fills it in when the redaction policy lands."
141
+ ),
142
+ )
143
+
144
+
145
+ class ManifestPayloadDigests(BaseModel):
146
+ """Hashes of the run's payload data.
147
+
148
+ Session A leaves these as None — the field exists in the schema
149
+ so consumers can write code that handles both the Session A
150
+ ("not yet computed") and Session B ("populated from the run's
151
+ submission and output envelope") cases without a v2 bump.
152
+ """
153
+
154
+ model_config = ConfigDict(frozen=True)
155
+
156
+ input_sha256: str | None = Field(
157
+ default=None,
158
+ description=(
159
+ "SHA-256 hex of the submission's primary file bytes at run "
160
+ "time. Always present in Session B — even when "
161
+ "DO_NOT_STORE purges the bytes themselves, the hash is "
162
+ "the immutable proof the run consumed input matching it."
163
+ ),
164
+ )
165
+ output_envelope_sha256: str | None = Field(
166
+ default=None,
167
+ description=(
168
+ "SHA-256 hex of the canonical output envelope JSON. None "
169
+ "for runs whose retention policy excludes outputs."
170
+ ),
171
+ )
172
+
173
+
174
+ class EvidenceManifest(BaseModel):
175
+ """Top-level evidence manifest for a single validation run.
176
+
177
+ The canonical JSON form of this model — produced by serialising
178
+ the dict from ``model_dump(mode="json")`` via ``json.dumps`` with
179
+ ``sort_keys=True`` and ``separators=(",",":")`` — is what gets
180
+ hashed and persisted by the producer.
181
+
182
+ Producers store ``SHA-256(canonical_json_bytes)`` alongside the
183
+ file. Tampering with the manifest after persistence is detectable
184
+ via re-fetch + re-hash.
185
+ """
186
+
187
+ model_config = ConfigDict(frozen=True, extra="forbid")
188
+
189
+ schema_version: Literal["validibot.evidence.v1"] = Field(
190
+ default=SCHEMA_VERSION,
191
+ description="Pinned schema-version string. Verifiers reject other values.",
192
+ )
193
+ run_id: str = Field(description="Producer-side run UUID (hex string).")
194
+ workflow_id: int = Field(description="Workflow primary key at run time.")
195
+ workflow_slug: str = Field(description="Workflow slug at run time.")
196
+ workflow_version: str = Field(description="Workflow version at run time.")
197
+ org_id: int = Field(description="Organization primary key that owned the run.")
198
+ executed_at: str = Field(
199
+ description=(
200
+ "ISO 8601 UTC timestamp of run completion. String rather "
201
+ "than datetime so canonical-JSON bytes are stable across "
202
+ "Python versions and timezone-aware/naïve serialisation "
203
+ "differences."
204
+ ),
205
+ )
206
+ status: str = Field(
207
+ description=("Run status at completion (SUCCEEDED / FAILED / CANCELED)."),
208
+ )
209
+ workflow_contract: WorkflowContractSnapshot
210
+ steps: list[StepValidatorRecord] = Field(
211
+ default_factory=list,
212
+ description="Per-step validator records, ordered by step.order.",
213
+ )
214
+ input_schema: dict | None = Field(
215
+ default=None,
216
+ description=(
217
+ "Workflow's structured input contract (canonical JSON Schema) "
218
+ "at run time. None when the workflow has no structured input "
219
+ "contract. The schema itself is the contract; the input "
220
+ "hash in payload_digests is the proof of conformance."
221
+ ),
222
+ )
223
+ retention: ManifestRetentionInfo
224
+ payload_digests: ManifestPayloadDigests = Field(
225
+ default_factory=ManifestPayloadDigests,
226
+ description="Session A: empty. Session B: input + output hashes.",
227
+ )
228
+
229
+
230
+ __all__ = [
231
+ "SCHEMA_VERSION",
232
+ "EvidenceManifest",
233
+ "ManifestPayloadDigests",
234
+ "ManifestRetentionInfo",
235
+ "StepValidatorRecord",
236
+ "WorkflowContractSnapshot",
237
+ ]
@@ -1,16 +0,0 @@
1
- """Shared domain models and utilities for Simple Validations."""
2
-
3
- from validibot_shared.energyplus.models import (
4
- EnergyPlusSimulationLogs,
5
- EnergyPlusSimulationMetrics,
6
- EnergyPlusSimulationOutputs,
7
- )
8
- from validibot_shared.fmu.models import FMUProbeResult, FMUVariableMeta
9
-
10
- __all__ = [
11
- "EnergyPlusSimulationLogs",
12
- "EnergyPlusSimulationMetrics",
13
- "EnergyPlusSimulationOutputs",
14
- "FMUProbeResult",
15
- "FMUVariableMeta",
16
- ]