feat: generate user-facing write/read schema models + offline validation#1135
feat: generate user-facing write/read schema models + offline validation#1135dgarros wants to merge 15 commits into
Conversation
Deploying infrahub-sdk-python with
|
| Latest commit: |
622c56f
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://5928e969.infrahub-sdk-python.pages.dev |
| Branch Preview URL: | https://dga-user-schema-infp-234.infrahub-sdk-python.pages.dev |
Codecov Report❌ Patch coverage is
@@ Coverage Diff @@
## infrahub-develop #1135 +/- ##
====================================================
- Coverage 82.35% 76.18% -6.17%
====================================================
Files 138 143 +5
Lines 12065 12379 +314
Branches 1805 1811 +6
====================================================
- Hits 9936 9431 -505
- Misses 1573 2400 +827
+ Partials 556 548 -8
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 30 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
2 issues found across 11 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
ce6e067 to
aa401e0
Compare
There was a problem hiding this comment.
1 issue found across 5 files (changes from recent commits).
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
3 issues found across 20 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/unit/test_schema_generated_models.py">
<violation number="1" location="tests/unit/test_schema_generated_models.py:224">
P2: The test `test_use_enum_values_keeps_runtime_field_values_as_plain_strings` claims to verify that `use_enum_values=True` keeps runtime values as plain strings, but all three assertions (equality with raw string, equality with enum member, and `isinstance(str)`) pass identically whether `use_enum_values` is `True` or `False`. This is because the generated enums are `str`-backed (`str, Enum`), so their members always compare equal to their string values and are always instances of `str`. The test cannot detect if `use_enum_values` were accidentally removed in a future regeneration. Consider adding an assertion that discriminates between the two modes, such as `assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality)` or `assert type(relationship.cardinality) is str`.</violation>
</file>
<file name="infrahub_sdk/schema/main.py">
<violation number="1" location="infrahub_sdk/schema/main.py:266">
P1: `AttributeSchema` claims to relax `extra="forbid"` inherited from `AttributeSchemaBaseWrite`, but its `model_config = ConfigDict(use_enum_values=True)` does not actually override the parent setting. In Pydantic v2, subclass `model_config` is merged with the parent's, so `extra="forbid"` remains in effect unless explicitly overridden. This means historical construction patterns that passed unrecognized keyword arguments — which the old `AttributeSchema` silently accepted — will now raise validation errors. The docstring explicitly says `extra="forbid"` is relaxed, but the implementation contradicts that intent.
To fix this, add `extra="ignore"` (or `"allow"`) to the subclass `ConfigDict` so it truly restores the permissive behavior the docstring promises.</violation>
<violation number="2" location="infrahub_sdk/schema/main.py:315">
P1: The public `SchemaRoot` model is missing the new `extensions` field that the generated write model (`InfrahubSchemaWrite`) now requires for `/api/schema/load`. It still serializes the legacy `node_extensions` key, so schema payloads constructed or validated through the public `SchemaRoot` entry point will not match the new backend contract. Consider adding `extensions: SchemaExtensionWrite | None = None` and aligning `to_schema_dict()` with the generated write shape, or documenting that `SchemaRoot` is deprecated in favor of the generated write model for load payloads.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| version: str | ||
| generics: list[GenericSchema] = Field(default_factory=list) | ||
| nodes: list[NodeSchema] = Field(default_factory=list) | ||
| node_extensions: list[NodeExtensionSchema] = Field(default_factory=list) |
There was a problem hiding this comment.
P1: The public SchemaRoot model is missing the new extensions field that the generated write model (InfrahubSchemaWrite) now requires for /api/schema/load. It still serializes the legacy node_extensions key, so schema payloads constructed or validated through the public SchemaRoot entry point will not match the new backend contract. Consider adding extensions: SchemaExtensionWrite | None = None and aligning to_schema_dict() with the generated write shape, or documenting that SchemaRoot is deprecated in favor of the generated write model for load payloads.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At infrahub_sdk/schema/main.py, line 315:
<comment>The public `SchemaRoot` model is missing the new `extensions` field that the generated write model (`InfrahubSchemaWrite`) now requires for `/api/schema/load`. It still serializes the legacy `node_extensions` key, so schema payloads constructed or validated through the public `SchemaRoot` entry point will not match the new backend contract. Consider adding `extensions: SchemaExtensionWrite | None = None` and aligning `to_schema_dict()` with the generated write shape, or documenting that `SchemaRoot` is deprecated in favor of the generated write model for load payloads.</comment>
<file context>
@@ -264,100 +249,105 @@ def unique_attributes(self) -> list[AttributeSchemaAPI]:
+ version: str
+ generics: list[GenericSchema] = Field(default_factory=list)
+ nodes: list[NodeSchema] = Field(default_factory=list)
+ node_extensions: list[NodeExtensionSchema] = Field(default_factory=list)
+
+ def to_schema_dict(self) -> dict[str, Any]:
</file context>
| @property | ||
| def supports_artifacts(self) -> bool: | ||
| """Return True if this schema supports artifact operations via CoreArtifactTarget inheritance. | ||
| model_config = ConfigDict(use_enum_values=True) |
There was a problem hiding this comment.
P1: AttributeSchema claims to relax extra="forbid" inherited from AttributeSchemaBaseWrite, but its model_config = ConfigDict(use_enum_values=True) does not actually override the parent setting. In Pydantic v2, subclass model_config is merged with the parent's, so extra="forbid" remains in effect unless explicitly overridden. This means historical construction patterns that passed unrecognized keyword arguments — which the old AttributeSchema silently accepted — will now raise validation errors. The docstring explicitly says extra="forbid" is relaxed, but the implementation contradicts that intent.
To fix this, add extra="ignore" (or "allow") to the subclass ConfigDict so it truly restores the permissive behavior the docstring promises.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At infrahub_sdk/schema/main.py, line 266:
<comment>`AttributeSchema` claims to relax `extra="forbid"` inherited from `AttributeSchemaBaseWrite`, but its `model_config = ConfigDict(use_enum_values=True)` does not actually override the parent setting. In Pydantic v2, subclass `model_config` is merged with the parent's, so `extra="forbid"` remains in effect unless explicitly overridden. This means historical construction patterns that passed unrecognized keyword arguments — which the old `AttributeSchema` silently accepted — will now raise validation errors. The docstring explicitly says `extra="forbid"` is relaxed, but the implementation contradicts that intent.
To fix this, add `extra="ignore"` (or `"allow"`) to the subclass `ConfigDict` so it truly restores the permissive behavior the docstring promises.</comment>
<file context>
@@ -264,100 +249,105 @@ def unique_attributes(self) -> list[AttributeSchemaAPI]:
- @property
- def supports_artifacts(self) -> bool:
- """Return True if this schema supports artifact operations via CoreArtifactTarget inheritance.
+ model_config = ConfigDict(use_enum_values=True)
- Only NodeSchemaAPI overrides this; all other schema types return False by design because
</file context>
| model_config = ConfigDict(use_enum_values=True) | |
| model_config = ConfigDict(extra="ignore", use_enum_values=True) |
| relationship = write_module.RelationshipSchemaWrite(name="interfaces", peer="InfraInterface", cardinality="one") | ||
| assert relationship.cardinality == "one" | ||
| assert relationship.cardinality == enums_module.RelationshipCardinality.ONE | ||
| assert isinstance(relationship.cardinality, str) |
There was a problem hiding this comment.
P2: The test test_use_enum_values_keeps_runtime_field_values_as_plain_strings claims to verify that use_enum_values=True keeps runtime values as plain strings, but all three assertions (equality with raw string, equality with enum member, and isinstance(str)) pass identically whether use_enum_values is True or False. This is because the generated enums are str-backed (str, Enum), so their members always compare equal to their string values and are always instances of str. The test cannot detect if use_enum_values were accidentally removed in a future regeneration. Consider adding an assertion that discriminates between the two modes, such as assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality) or assert type(relationship.cardinality) is str.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/unit/test_schema_generated_models.py, line 224:
<comment>The test `test_use_enum_values_keeps_runtime_field_values_as_plain_strings` claims to verify that `use_enum_values=True` keeps runtime values as plain strings, but all three assertions (equality with raw string, equality with enum member, and `isinstance(str)`) pass identically whether `use_enum_values` is `True` or `False`. This is because the generated enums are `str`-backed (`str, Enum`), so their members always compare equal to their string values and are always instances of `str`. The test cannot detect if `use_enum_values` were accidentally removed in a future regeneration. Consider adding an assertion that discriminates between the two modes, such as `assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality)` or `assert type(relationship.cardinality) is str`.</comment>
<file context>
@@ -167,6 +169,61 @@ def test_extension_models_forbid_extra_fields(name: str) -> None:
+ relationship = write_module.RelationshipSchemaWrite(name="interfaces", peer="InfraInterface", cardinality="one")
+ assert relationship.cardinality == "one"
+ assert relationship.cardinality == enums_module.RelationshipCardinality.ONE
+ assert isinstance(relationship.cardinality, str)
+
+
</file context>
| assert isinstance(relationship.cardinality, str) | |
| assert isinstance(relationship.cardinality, str) | |
| assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality) |
There was a problem hiding this comment.
1 issue found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="infrahub_sdk/schema/main.py">
<violation number="1" location="infrahub_sdk/schema/main.py:281">
P1: The public constructible write class `NodeSchema` no longer inherits `_SchemaKindMixin`, so it loses the runtime `.kind` property and all capability helpers (`supports_artifact_definition`, `supports_hierarchy`, etc.) that were previously available. The generated `NodeSchemaWrite` base does not provide a replacement `kind`, so existing code that constructs `NodeSchema` and accesses `.kind` will now raise `AttributeError`. Consider either restoring the `_SchemaKindMixin` inheritance to `NodeSchema` and `GenericSchema`, adding a `kind` computed field to the generated write base, or documenting this as an intentional breaking change if write models are no longer meant to expose these helpers.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="tests/unit/test_schema_generated_models.py">
<violation number="1" location="tests/unit/test_schema_generated_models.py:224">
P2: The test `test_use_enum_values_keeps_runtime_field_values_as_plain_strings` claims to verify that `use_enum_values=True` keeps runtime values as plain strings, but all three assertions (equality with raw string, equality with enum member, and `isinstance(str)`) pass identically whether `use_enum_values` is `True` or `False`. This is because the generated enums are `str`-backed (`str, Enum`), so their members always compare equal to their string values and are always instances of `str`. The test cannot detect if `use_enum_values` were accidentally removed in a future regeneration. Consider adding an assertion that discriminates between the two modes, such as `assert not isinstance(relationship.cardinality, enums_module.RelationshipCardinality)` or `assert type(relationship.cardinality) is str`.</violation>
</file>
<file name="infrahub_sdk/schema/main.py">
<violation number="1" location="infrahub_sdk/schema/main.py:266">
P1: `AttributeSchema` claims to relax `extra="forbid"` inherited from `AttributeSchemaBaseWrite`, but its `model_config = ConfigDict(use_enum_values=True)` does not actually override the parent setting. In Pydantic v2, subclass `model_config` is merged with the parent's, so `extra="forbid"` remains in effect unless explicitly overridden. This means historical construction patterns that passed unrecognized keyword arguments — which the old `AttributeSchema` silently accepted — will now raise validation errors. The docstring explicitly says `extra="forbid"` is relaxed, but the implementation contradicts that intent.
To fix this, add `extra="ignore"` (or `"allow"`) to the subclass `ConfigDict` so it truly restores the permissive behavior the docstring promises.</violation>
<violation number="2" location="infrahub_sdk/schema/main.py:315">
P1: The public `SchemaRoot` model is missing the new `extensions` field that the generated write model (`InfrahubSchemaWrite`) now requires for `/api/schema/load`. It still serializes the legacy `node_extensions` key, so schema payloads constructed or validated through the public `SchemaRoot` entry point will not match the new backend contract. Consider adding `extensions: SchemaExtensionWrite | None = None` and aligning `to_schema_dict()` with the generated write shape, or documenting that `SchemaRoot` is deprecated in favor of the generated write model for load payloads.</violation>
</file>
<file name="infrahub_sdk/ctl/cli_commands.py">
<violation number="1" location="infrahub_sdk/ctl/cli_commands.py:384">
P2: The `api_items` list may fail static type checking. When initialized from `[node.convert_api() for node in schema_root.nodes]`, type checkers infer `list[NodeSchemaAPI]`. Adding `list[GenericSchemaAPI]` via `+=` then fails because `list` is invariant. Since the project uses mypy, annotate the list with `list[MainSchemaTypesAPI]` (or `list[NodeSchemaAPI | GenericSchemaAPI]`) so mixing node and generic API items is accepted by the type checker.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| schema.update({item.kind: item for item in schema_root.nodes + schema_root.generics}) | ||
| # `kind` is a read-model concept, so convert the write models loaded from disk to their | ||
| # API form to key them by kind consistently with the branch-fetched path below. | ||
| api_items = [node.convert_api() for node in schema_root.nodes] |
There was a problem hiding this comment.
P2: The api_items list may fail static type checking. When initialized from [node.convert_api() for node in schema_root.nodes], type checkers infer list[NodeSchemaAPI]. Adding list[GenericSchemaAPI] via += then fails because list is invariant. Since the project uses mypy, annotate the list with list[MainSchemaTypesAPI] (or list[NodeSchemaAPI | GenericSchemaAPI]) so mixing node and generic API items is accepted by the type checker.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At infrahub_sdk/ctl/cli_commands.py, line 384:
<comment>The `api_items` list may fail static type checking. When initialized from `[node.convert_api() for node in schema_root.nodes]`, type checkers infer `list[NodeSchemaAPI]`. Adding `list[GenericSchemaAPI]` via `+=` then fails because `list` is invariant. Since the project uses mypy, annotate the list with `list[MainSchemaTypesAPI]` (or `list[NodeSchemaAPI | GenericSchemaAPI]`) so mixing node and generic API items is accepted by the type checker.</comment>
<file context>
@@ -379,7 +379,11 @@ def protocols(
- schema.update({item.kind: item for item in schema_root.nodes + schema_root.generics})
+ # `kind` is a read-model concept, so convert the write models loaded from disk to their
+ # API form to key them by kind consistently with the branch-fetched path below.
+ api_items = [node.convert_api() for node in schema_root.nodes]
+ api_items += [generic.convert_api() for generic in schema_root.generics]
+ schema.update({item.kind: item for item in api_items})
</file context>
0600957 to
45502c5
Compare
Add committed, generated write and read schema model variants under infrahub_sdk/schema/generated/. They are produced by the backend generator from the single source of truth in internal.py, filtered by a new field visibility axis (write ⊆ read ⊆ internal). The models are self-contained (pydantic + typing only) so they import with only the SDK installed, the write model retains extra="forbid", and constrained fields carry their allowed-value set as Literal[...] so the emitted JSON-schema is complete. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ft guard [INFP-234] Add validate_schema() to validate a schema payload against the generated write models with only the SDK installed (no server): it returns a field-level verdict rejecting non-settable/unknown fields and out-of-enum values. Add SDK unit tests for offline validation and a drift guard that asserts the generated write/read models are present, carry the do-not-edit header, and satisfy the write(extra=forbid)/read-superset invariants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extend the offline write-contract validation so the attributes and relationships nested under extensions.nodes[*] are held to the same generated write models as node/generic-level ones. Previously only top-level nodes and generics were gated, letting read-level, unknown, and out-of-enum fields slip through on extension payloads. Add SDK offline tests covering extension attribute/relationship rejection with dotted error locations, plus breadth coverage for out-of-enum relationship cardinality/kind and read-level fields on relationships, generics, and nodes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…per-family Write/Read Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…f per-collection map Validate nodes and generics in one pass against the generated write document model; keep the separate extension gating (extension nodes are kind-only). Field-level dotted errors unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…t[str, Any] The computed_attribute block is now a dedicated model (ComputedAttributeWrite/Read) with a Literal kind and extra=forbid, so the write contract for a computed attribute is explicit and its kind/unknown-field errors are caught with a field-level location. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…_attribute Replace the opaque dict[str, Any] sub-blocks in the generated write/read schema models with typed models: DropdownChoice, a plain union of the five attribute parameter shapes, and a kind-discriminated union for computed_attribute that enforces the jinja2_template/transform requirement natively. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the flat attribute write/read models, which typed parameters as a
plain union of every parameters shape, with a discriminated union on kind.
A shared AttributeSchemaBase carries every field except parameters, and each
variant narrows kind to the kinds sharing one parameters shape and carries
that parameters model, so a Text attribute no longer validates NumberPool
parameters. The public AttributeSchema{Write,Read} name becomes the union
alias.
Route offline validation of a single item through pydantic.TypeAdapter so a
union alias (which has no model_validate) validates like a plain model.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… models Extend the generated user-facing schema contract so it is complete: - Write variant gains NodeExtensionWrite / SchemaExtensionWrite (extra="forbid") and InfrahubSchemaWrite now carries an extensions field and forbids extra top-level keys, so the published write contract covers schema extensions. - Read variant gains read-only ProfileSchemaRead / TemplateSchemaRead so every read item is described by a generated model. - validate_schema now validates the whole root (nodes, generics, extensions) via InfrahubSchemaWrite in a single pass; the bespoke extension helper is retired while field-level dotted paths and the "(received: ...)" suffix are preserved. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Constrained fields in the generated user-facing write/read models were rendered as inline Literals. Emit dedicated (str, Enum) classes into a new self-contained generated/enums.py and type the fields with them, matching the SDK's historical enum names. Every generated model gains use_enum_values=True so runtime values stay plain strings; attribute/computed-attribute discriminated unions use Literal[Enum.MEMBER] discriminators. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… [INFP-234] Retire the hand-maintained model bodies in ``infrahub_sdk/schema/main.py`` and re-base every public name on the generated write/read models plus hand-written behavior mixins, keeping import paths, names, methods and envelopes stable. - Enums (``AttributeKind``, ``BranchSupportType``, ``RelationshipCardinality``, ...) are re-exported from ``generated.enums`` under the historical names. - Behavior mixins carry the 15 attribute/relationship helpers, the ``kind`` / ``supports_*`` / hierarchy flags, ``cardinality_is_*`` and write ``convert_api``. - Read ``*API`` models stay concrete subclasses of the generated ``*Read`` models so ``isinstance`` keeps working; write ``NodeSchema``/``GenericSchema``/ ``RelationshipSchema`` subclass the generated write models. - A thin constructible ``AttributeSchema``/``AttributeSchemaAPI`` (on the shared write/read base) keeps ``AttributeSchema(name=..., kind=...)`` working; nodes narrow ``attributes``/``relationships`` to those variants and still validate into the strict generated discriminated union on dump. - Envelopes (``SchemaRoot``, ``SchemaRootAPI``, ``BranchSchema``) stay hand-written on the generated inner models. Breaking: ``AttributeKind.STRING`` removed; write-model defaults now match the server contract (relationship min/max_count 0, node branch "aware", generate_profile True, generate_template False) and reject unknown fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enum-typed generated fields defaulted to the raw string value, which failed mypy/ty against the enum annotation. Emit the enum member (use_enum_values coerces to the value at runtime). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… [INFP-234] The enum-typed RelationshipSchemaWrite.cardinality field makes ty flag the deliberate raw-string construction used to prove pydantic's use_enum_values runtime coercion. Add a scoped ty: ignore so the intent-preserving test passes python-lint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kind (namespace+name) and hash (server-computed) are read-only. Expose kind as a pydantic computed_field and hash as a plain field on the generated read base node model, so node/generic/profile/template read models inherit both. Drop the hand-written kind property from the schema kind mixin and the hand hash fields from the API read models; the write NodeSchema/GenericSchema no longer carry the read-only kind mixin. Retype CoreNodeBase._schema to the read union so static _schema.kind access stays valid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reading a locally-authored (write) schema needs `.kind` attribute access (e.g. the protocols CLI command). Expose `kind` as a plain property on the write node models — derived from namespace+name, like on read — but do not serialize it: on write it stays a property (not a `@computed_field`) so it never enters the payload, where `extra="forbid"` would reject it on the round-trip through the write/load contract. `hash` remains read-only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
45502c5 to
622c56f
Compare
Summary
Adds generated, user-facing write and read schema models to the SDK plus an offline
validate_schema()so a schema can be checked for correctness with no running Infrahub server. Part of the user-facing schema separation (opsmill/infrahub, INFP-234).Key Changes
infrahub_sdk/schema/generated/{write,read}.py— the write model is exactly what/api/schema/loadaccepts (constrained fields carry their allowed values asLiteral[...];extra="forbid"); read = write + visible-but-not-settable fields.validate_schema()(infrahub_sdk/schema/validate.py) — field-level offline validation with dotted error locations; also gates schemaextensions.protocols.py(pre-existing drift vs current backend schema).Related Context
Generated from the backend's schema definitions; consumed by the backend PR (opsmill/infrahub). Depends on nothing else in this repo.
Test Plan
14 offline-validation cases + generated-model guard pass, pydantic-only (no server).
Notes
Draft: the backend still ships a parallel hand-written model set; full consolidation of the SDK's hand-written schema models is tracked as a follow-up.
🤖 Generated with Claude Code
Summary by cubic
Generates user-facing write/read schema models and adds offline
validate_schema()so schemas can be checked without a server. Backs public SDK schema types with the generated contract, separates read/write, and moves read-only fields appropriately (INFP-234).New Features
infrahub_sdk/schema/generated/{enums,write,read}.py; exportInfrahubSchema{Write,Read}with dedicated enums, typed parameters/choices, and kind-discriminated unions for attributes and computed attributes; write keepsextra="forbid".validate_schema()validates the whole document (nodes,generics,extensions) againstInfrahubSchemaWrite, returnsSchemaValidationResultwith dotted field errors, rejects unknown top-level keys and out-of-enum values; includes a drift guard to keep generated models present and write-forbid/read-superset correct.extensions; read addsProfileSchemaReadandTemplateSchemaRead.Refactors
AttributeKind,BranchSupportType,RelationshipCardinality) are re-exported;CoreNodeBase._schemaretyped toMainSchemaTypesAPI.kindis a computed field andhashis stored; write nodes expose a non-serializing.kindproperty for local access.AttributeKind.STRINGremoved; write models reject unknown fields; defaults aligned with the server (relationshipmin_count/max_count=0; nodebranch="aware",generate_profile=True,generate_template=False).Written for commit 622c56f. Summary will update on new commits.