GDTF mutation policy (Perastage)
This document defines how Perastage writes and version-stamps GDTF files, with a focus on description.xml mutations and long-term compatibility behavior. The GDTF symbol ownership and mutation contract is the product/architecture source of truth: project symbols belong to PSTG; only version-valid standard resources belong to derived GDTFs.
Export canonicalization policy
Perastage is permissive when importing GDTF/MVR data and strict when exporting it. Loading a legacy or externally generated GDTF must not rewrite the user's source file just because it was read, and every published GDTF passes the shared core/gdtf_canonicalizer validation boundary. An already canonical description with canonical archive paths is copied byte-for-byte, preserving unchanged authored GDTFs in project/MVR packaging. Effective standardization repairs still use canonical rewriting and revisions.
The GDTF 1.2 XSD used to verify this policy is mvrdevelopment/tools:gdtf.xsd from the public GDTF/MVR tools repository, advertised there as the GDTF 1.2 XSD Schema; the fetched artifact size was 53,467 bytes with SHA-256 13a044d297f19d0437965657fb21d02f7b2b02541aabeba5f11e5000074ac2f2. That schema defines FixtureType as an ordered sequence where AttributeDefinitions, Geometries, and DMXModes have minOccurs="1"; Wheels, PhysicalDescriptions, Models, Revisions, FTPresets, and Protocols have minOccurs="0"; and Name, Manufacturer, Description, and FixtureTypeID are required attributes. This is stricter than the prose sentence that one or more sections may be empty or missing, so Perastage treats non-canonical input permissively but keeps generated strict fixtures and publication output aligned to the verified XSD.
The reader, mutation-audit helper, and canonicalizer share one Core-owned definition of the official GDTF FixtureType child order (AttributeDefinitions, Wheels, PhysicalDescriptions, Models, Geometries, DMXModes, Revisions, FTPresets, Protocols). The canonicalizer removes the known Perastage-owned legacy PerastageMutationAudit child and a legacy Editor attribute only when the shared conservative predicate recognizes a Perastage program value. It preserves valid standard sections and resources, validates required root/fixture structure, and repairs Perastage-owned placeholder FixtureTypeID values with deterministic stable IDs when safe. Unknown third-party children and attributes are not guessed away: strict publication rejects them while leaving the parsed document content intact for an explicit caller decision.
Any canonicalization mutation is recorded with a standard GDTF Revision only. Perastage-specific GDTF metadata must not use custom XML nodes; legacy PerastageMutationAudit is read-only compatibility metadata and is never written on export. Perastage-specific MVR metadata remains restricted to root-level GeneralSceneDescription/UserData/Data[@provider="Perastage"]; object-level MVR UserData is not exported for Fixture, SceneObject, Support, Truss, or GroupObject.
Tolerant FixtureType metadata reads
The inspection reader remains deliberately narrower than a full GDTF document model. Its attribute fallbacks are classified as follows:
| Read field | Accepted names | Classification and publication policy |
|---|---|---|
| Identity and display | Name, Manufacturer, ShortName, LongName, Description, FixtureTypeID, Thumbnail |
Standard GDTF 1.2. |
| Creation date | CreateDate, CreationDate, DateCreated |
Generic tolerant-reader compatibility. None is a GDTF 1.2 FixtureType attribute, ownership is not safely attributable to Perastage, and canonical publication therefore preserves and rejects it rather than deleting it. |
| Fixture-level revision fallback | Revision, DataVersion, Version |
Generic tolerant-reader compatibility for historical metadata displays. None is a GDTF 1.2 FixtureType attribute; publication preserves and rejects it. The standard version is GDTF/@DataVersion, and standard change history is represented by FixtureType/Revisions/Revision. |
| Legacy editor evidence | Recognized Perastage program values, including versioned forms | Known Perastage legacy compatibility. Raw inspection keeps the official XSD error and never rewrites the source; canonical publication removes only this owned marker and records the mutation with a standard Revision. Other Editor values are preserved and rejected. |
At the GDTF root, the reader's Version fallback for standard DataVersion is generic tolerant-reader compatibility. Within a standard Revision, Comment/Version as fallbacks for Text and TimeStamp as a fallback for Date are likewise generic tolerant-reader compatibility. These aliases remain read-only conveniences and are not classified as standard or silently normalized. No currently accepted alias was identified as a safe obsolete fallback whose removal would improve compatibility.
Legacy Editor ownership recognition is shared by canonical publication and
symbol compatibility. It compares the Perastage program name with
deterministic ASCII case folding and accepts only an exact value or a value
followed by whitespace, -, or /. Values such as PerastageVendor are not
classified as Perastage-owned.
The legacy Windows cmd.exe interactive UTF-8 display issue is intentionally
deferred to a separate CLI presentation task. It is not GDTF data corruption;
Core strings and serialized GDTF data remain UTF-8.
1) Inventory of GDTF write points in Perastage
Perastage currently mutates GDTF archives at the following integration points.
| Module | File | Function(s) | Mutation scope |
|---|---|---|---|
| Core document mutation | core/gdtf_document_mutation.cpp |
gdtf::MutateDocument(...) |
Writes FixtureType/@Description, canonical Weight, and safe existing Consumer/Properties electrical fields, appends a standard Revision, canonicalizes the description, reports structured diagnostics, and publishes through a unique sibling temporary archive before atomic replacement. |
| Viewer 3D compatibility facade | viewer3d/gdtfloader.cpp |
SetGdtfProperties(...), MutateGdtfDocumentWithResult(...) |
Preserves the legacy public API by delegating the transaction to Core and invalidates the corresponding Viewer3D loader cache entry after successful atomic replacement. |
| Dormant Core model-local SVG boundary | core/symbols/standard_gdtf_completion.cpp + standard_gdtf_svg.cpp |
Dedicated completion/repair API and standard serializer | No normal production caller; future model-local mutations require explicit exact Model/@File paths and official offsets; preserves existing resources unless explicit repair/replacement is requested; appends one standard Revision for effective changes and publishes a canonical atomic derivative. |
| Project fixture editor | core/gdtf/editor/project_fixture_gdtf_apply_adapter.cpp + core/fixture_gdtf_derivative_publication.cpp |
PrepareProjectDerivative(...), document mutation, PublishPreparedDerivative(...) |
Mutates a private non-canonical project working copy, validates and atomically publishes before fixture rebinding. Project internal symbols are independent from GDTF publication; Bottom is never required for standard-view completeness. |
| Truss GDTF generation | core/truss_gdtf_builder.cpp |
BuildTrussGdtfFromInstance(...), ConvertLegacyGtrussToGdtf(...); gui/trusseditdialog.cpp calls the builder when truss type fields are edited |
Creates Perastage-owned truss GDTF archives with a standard Structure root geometry, deterministic FixtureTypeID, standard Revision, and no custom XML nodes. |
| MVR export patching | mvr/mvrexporter.cpp |
CreatePatchedGdtf(...) + export resource canonicalization |
Creates temporary patched GDTF copies for export overrides (manufacturer/model/physical properties/color/dimensions), appends a standard Revision when patched, and canonicalizes every .gdtf before it is packaged. |
| Shared audit helpers | core/gdtf_mutation_audit.cpp |
StampPerastageMutationMetadata(...), AppendRevision(...), ApplyPhysicalPropertiesWithAudit(...) |
Centralizes standard GDTF revision-appending semantics used by write flows. |
core/fixture_gdtf_derivative_publication.{h,cpp} is the failure-safe Core
publication boundary for explicit fixture exports and prepared project
derivatives. It canonicalizes into a private sibling archive and atomically
replaces the destination only after canonicalization succeeds. The dictionary
derivative path likewise canonicalizes a private input before handing it to
dictionary-owned storage. Consequently, standalone fixture export,
@Perastage.gdtf library creation or refresh, project derivative publication,
standard-SVG completion publication, and project-to-library synchronization cannot
bypass GdtfCanonicalizer. External mappings remain byte-preserving imports
and are not treated as Perastage-owned publication.
Unknown third-party FixtureType children or attributes cause these strict
publication paths to fail without replacing an existing destination. The
external source remains unchanged, and publication never falls back to copying
the rejected archive.
Fixture Export retains the existing no-copy result when the chosen destination is already the effective source file. When the destination is the original asset but the effective source is a distinct Perastage derivative, the existing overwrite confirmation remains required before the canonical copy atomically replaces that original asset.
Perastage-owned publication enables deterministic repair only for
FixtureTypeID values recognized by IsPlaceholderFixtureTypeId(). The seed
is a stable publication-policy identifier combined with FixtureType identity
fields, never a transient working filename. Valid identifiers remain unchanged,
while missing or otherwise malformed non-placeholder identifiers still fail
strict publication.
Notes on ownership
core/gdtf_document_mutation.{h,cpp}owns the document mutation and archive publication transaction; it preserves unrelated resources, structured publication-stage diagnostics, failure hooks, and atomic replacement semantics.viewer3d/gdtfloader.cppretains only the source-compatible mutation facade and Viewer3D-specific cache invalidation after Core reports completed replacement.core/gdtf_mutation_audit.{h,cpp}is the single owner of Perastage GDTF revision semantics.core/gdtf_canonicalizer.{h,cpp}is the shared owner of export-time GDTF structural canonicalization and validation.core/gdtf_publication_resources.*owns publication-only cleanup of recognized private legacy archive resources and SVG markers. Standard completion, derived-copy publication, and explicit physical/document mutation use this shared plan; document edits combine cleanup with their audited effective operation. Ordinary import/inspection/canonicalization does not apply this removal plan.- Write call sites in other modules must use this helper API instead of hand-rolling custom revision XML shapes.
- Truss GDTF files generated, completed, normalized, or modified by Perastage are exported as derived Perastage-owned copies named
Manufacturer@Model@Perastage.gdtf; external or library source GDTF files are read as inputs and are not overwritten. When the Trusses table edit dialog changes a GDTF-specific type field for a model-only truss, Perastage creates that derived GDTF immediately and attaches it to the project while leaving MVR-only instance edits project-scoped. - GDTF model dimensions are written in meters. Perastage truss dimensions are stored in millimeters, so truss GDTF generation converts length, width, and height from millimeters to meters at export time.
- Perastage project symbols and user overrides are persisted in PSTG additional resources outside
scene.mvr. Core owns their versioned manifest, exact SVG bytes, metadata, deduplication, and explicit fixture bindings. Save/Load does not regenerate valid persisted symbols. - The Core resource contract separates project-owned internal symbols from standard GDTF resources. New GDTF output never creates
perastage/symbols/, private Perastage archive resources, or SVG ownership markers. Positively identified historical resources remain read-only compatibility input and can migrate to PSTG on Save. Model basenames and generic editor/revision metadata do not establish ownership. - Fixture display color is no longer persisted by mutating
description.xmlmodelColorvalues in place. The persisted source of truth for default color selection is the Perastage dictionary/project data layer, whileGetGdtfModelColor(...)remains read-only for legacy fallback reads.
Historical automatic GDTF completion preference
gdtf::MutationPolicy and the gdtf_mutation_policy key remain readable for
compatibility. Preferences no longer exposes a definition-completion choice:
there is no reviewed model-local automatic SVG generator in production.
Automatic fixture-symbol preparation exclusively updates PSTG, regardless of the historical setting. It does not publish an @Perastage derivative, update Model offsets, append a GDTF revision or rebind fixture GDTF references. Inspecting missing standard resources has no mutation side effects.
The standard SVG serializer/completion/repair APIs remain a dormant future boundary. They require exact nonempty Model/@File and reject ambiguous values. Any future generator must operate in Model-local coordinates, with physical 1:1 SVG geometry in millimeters; 3D mesh scaling does not scale SVGs. Current output supports GDTF 1.2 Top/Front/Side only, never next-only Bottom.
Explicit Weight/Power/document editing retains canonical owned derivative publication and accurate standard Revisions. Unknown authored resources cannot be claimed as Perastage-owned. Legacy symbols materialize into PSTG without rewriting their source; intentional publication keeps its existing audited compatibility cleanup and MVR preservation rules.
2) Exact Revision format used by Perastage
Perastage appends <Revision /> under <FixtureType>/<Revisions> and ensures <Revisions> exists when needed. Repeated exports do not append a duplicate Perastage revision when the same Text, ModifiedBy, and UserID already exist.
Perastage writes these attributes:
Date: UTC timestamp in ISO-8601YYYY-MM-DDTHH:MM:SSZ.ModifiedBy: caller-provided string, or default value returned byBuildPerastageModifiedBy()("Perastage " + perastage::build_info::appVersion()).Text: human-readable action summary (for example: model color update, fixture symbol views applied, MVR export patch action).- Note: this remains an example because MVR export patching can still record color-related patch actions in the exported temporary copy, but the direct in-place model-color mutation API was removed.
UserID: integer; default0.
Canonical shape:
<Revisions>
<Revision
Date="2026-04-05T10:20:30Z"
ModifiedBy="Perastage 0.x.y"
Text="Added missing standard SVG Side and Front views"
UserID="0"/>
</Revisions>
3) Legacy PerastageMutationAudit compatibility
- Symbol:
GdtfMutationAudit::kPerastageGdtfMutationSchemaVersion. - Location:
core/gdtf_mutation_audit.h. - Current value:
1. - Meaning: legacy version of the former Perastage-owned mutation metadata contract, not the GDTF specification version.
Perastage no longer writes <PerastageMutationAudit .../> into GDTF description.xml, because it is not a standard GDTF node. The importer and compatibility helpers may still read and ignore legacy nodes from older files.
4) Compatibility matrix (legacy / current / future reads)
Compatibility is resolved by InspectCompatibility(...) in core/gdtf_mutation_audit.cpp.
| Input metadata state | Decision mode | Behavior |
|---|---|---|
No <PerastageMutationAudit> node (legacy file) |
LegacyFallback |
Treat as legacy-compatible; parse symbols/fixture data with fallback logic. |
<PerastageMutationAudit SchemaVersion="1"> |
KnownPerastageVersion |
Treat metadata as trusted/current Perastage schema. |
<PerastageMutationAudit> present but missing/invalid SchemaVersion |
SafeFallbackUnknownVersion |
Treat as unknown schema; keep safe fallback behavior and emit warning. |
<PerastageMutationAudit SchemaVersion="N"> where N != 1 |
SafeFallbackUnknownVersion |
Treat as future/unknown schema; keep safe fallback behavior and emit warning. |
Policy intent:
- Legacy reads must keep working.
- Current known schema should be fully supported.
- Future unknown schemas must degrade safely (no hard parse failure exclusively due to schema mismatch).
5) Functional non-regression criteria
A GDTF mutation change is accepted only if all conditions below hold:
- Mutation correctness
- Target payload mutation is present (for example color/properties/SVG view assets+offsets).
- GDTF 1.2 standard completion accepts only Top/Side/Front resources at exact
Model/@Filepaths, with official model offsets and no private SVG markers. Project Top/Front/Side/Bottom bundles are stored independently in PSTG. Existing usable and unusable resources are distinct from missing resources and are not replaced automatically.
- Revision correctness
- No new
<PerastageMutationAudit>node is written. - A new
<Revision>entry is appended with validDate,ModifiedBy,Text,UserIDwhen Perastage intentionally changes a GDTF. - No duplicate identical Perastage revision is appended on repeated exports.
- No new
- Compatibility safety
- Legacy/no-audit and unknown-schema files remain readable through fallback behavior.
- Archive integrity
- Rewritten
.gdtfkeepsdescription.xmlreadable and required referenced resources present. - Exported
.gdtfcontent has officialFixtureTypechild order and contains noPerastageMutationAuditnode.
- Rewritten
- No workflow regressions
- Existing fixture rendering/import/export/symbol flows continue operating for non-mutated files.
6) Verification checklist
Manual checklist
- Manually Apply Top/Bottom/Front/Side and verify a project override survives PSTG Save/Load, including an unsaved scene, while the source GDTF bytes remain unchanged.
- Generate project symbols with missing Model SVGs and verify PSTG output, byte-identical source GDTF, unchanged offsets and no derivative or reference rebinding.
- Load both historical mutation-policy values and verify identical safe project-symbol behavior; Preferences must expose no automatic definition-completion option.
- Inspect PSTG and its
scene.mvr: project symbols are inresources/fixture_symbols/outside MVR; newly published GDTFs contain no private namespace or SVG ownership metadata. - Edit fixture color via dictionary/project workflow and verify fixture color persists after reopening without mutating model
Colorin the source.gdtf. - Edit fixture physical properties and verify weight/power values persist after reopening.
- Inspect resulting
description.xmland verify no newPerastageMutationAuditnode is present,FixtureTypechildren follow the official order, andRevisionattributes follow the policy format. - Export an MVR that triggers patched GDTF overrides and verify the exported patched GDTF carries standard revision metadata.
- Confirm legacy GDTF (without
PerastageMutationAudit) remains loadable. - Confirm unknown/future
SchemaVersiontriggers safe fallback behavior (no hard failure due only to version mismatch).
Automated tests relevant to this policy
tests/gdtf_canonicalizer_test.cpp- validates export canonicalization order, legacy metadata removal, stable placeholder FixtureTypeID repair, and duplicate revision prevention.
tests/gdtfloader_set_properties_test.cpp- validates physical-properties mutation and persisted XML changes.
tests/symbol_fixture_applier_gdtf_test.cpp- validates project-symbol Apply, source immutability, and exact source/mode binding behavior.
tests/fixture_symbol_svg_cache_test.cpp- validates bounded runtime cache revision and explicit invalidation behavior.
tests/check_perastage_tree_modules.sh- mandatory architecture/module boundary consistency check.
tests/check_no_configmanager_get_in_gui.sh- mandatory guardrail for GUI access patterns.
Project Fixture apply adapter policy
Project Fixture Weight and planning-power edits are owned by a non-GUI apply adapter. Planning power normally changes an instance override without archive mutation; explicit document power correction uses a safe canonical electrical target. The adapter consumes a session-built apply request, validates the stable fixture UUID, resolved operational GDTF path, selected mode, finite non-negative physical values, and explicit GdtfWritePolicy before mutating files or project data.
ReadOnly rejects document mutation. OverwriteOwnedFile is reserved for verified Perastage-owned files. CreateDerivativeBeforeMutation creates or updates the stable Perastage derivative through the existing dictionary behavior before applying physical values, keeping the original source unchanged. Unsupported policies fail without project attachment. Any derivative created before a later write failure is reported as a partial external side effect and is not attached to the project.
After a successful physical write, the adapter propagates Weight and changed source power to fixtures in the resulting source/type family using fixture UUIDs, preserves project power overrides, and reports only positions whose effective Weight changed. Planning overrides remain instance-local and have precedence over source values. Type, source reference, and mode context selections are applied to the target fixture; selected mode is not propagated to unrelated fixtures.
Project apply adapters
Fixture Edit routes session-backed type, source, mode, Weight, and PowerConsumption changes through the non-GUI ProjectFixtureGdtfApplyAdapter, and Project Truss GDTF type edits route through ProjectTrussGdtfApplyAdapter. Truss generation is controlled by dirty GDTF type fields; geometry-only and MVR-only edits remain non-generating. Operational resource paths are resolved separately from portable project references. Adapter failures do not commit project, table, or session state, while UI messages, preview refresh, and viewer refresh remain host-owned. Direct .gdtf opening remains unsupported.
Apply transaction policy
Adapters validate requests, perform required external file preparation, and return prepared project mutations. Hosts create one project undo checkpoint only after adapter success and before committing the prepared mutation to the project model. Existing verified Perastage derivatives may be overwritten; ordinary library, external, or extracted sources must create a derivative before physical mutation. UTF-8 path boundaries must use PathUtils::PathToUtf8(...) and PathUtils::PathFromUtf8(...). External file changes cannot be undone by project undo and must be reported separately from project commits.
Editable document fields and truss cross-section metadata
Perastage GDTF edit sessions now expose FixtureType/@Description as a document-level editable string for project fixtures and project trusses. Fixture edits submit Description, Weight, and PowerConsumption through a shared document mutation request so one Apply operation rewrites description.xml at most once and appends at most one standard Revision for the combined document changes. Revision Text summarizes the actual edited fields, including FixtureType Description when it changed.
Project truss generation appends a standard revision whose Text summarizes the dirty GDTF fields instead of always using the initial generation text. Project truss generation records Structure/@CrossSectionType using the official GDTF 1.2 enum values TrussFramework and Tube. Structure/@TrussCrossSection is preserved as the truss cross-section name and is written only when CrossSectionType is TrussFramework; when CrossSectionType is Tube, the editor/project state preserves the name but generated GDTF omits the attribute. Tube-specific CrossSectionHeight and CrossSectionWallThickness remain unsupported.
Float serialization and archive-name comparison policy
GDTF physical-property mutations write finite float values with locale-independent shortest-roundtrip text generated through std::to_chars in general format. The emitted token must parse back to the same finite float, use . as the decimal separator when a separator is needed, and avoid artificial precision caps or locale-dependent commas. NaN and positive or negative infinity are rejected before archive extraction or publication, reported through GdtfDocumentMutationResult.errors, and must leave the original archive bytes unchanged.
GDTF archive entry names written by Perastage remain canonical ZIP logical paths: relative, traversal-free, and separated with /. Production readers and tests must request wxZipEntry::GetName(wxPATH_UNIX) when comparing logical names because the default native presentation can expose nested names with Windows separators even when the central directory stores canonical / paths. A presentation-only separator conversion must not cause canonicalization to rewrite an unchanged authored archive. Raw central-directory inspection remains the regression check for the actual stored archive names; genuinely noncanonical stored separators still require normalization.
Electrical planning and corrections
See GDTF electrical power for input-power precedence, separate emitter totals, project overrides, presence and diagnostics. Power writes never fabricate topology or a PowerConsumption without Connector. Unsafe project corrections remain project overrides; effective GDTF corrections append standard Revisions.