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.cpp retains 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.xml model Color values in place. The persisted source of truth for default color selection is the Perastage dictionary/project data layer, while GetGdtfModelColor(...) 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-8601 YYYY-MM-DDTHH:MM:SSZ.
  • ModifiedBy: caller-provided string, or default value returned by BuildPerastageModifiedBy() ("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; default 0.

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:

  1. 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/@File paths, 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.
  2. Revision correctness
    • No new <PerastageMutationAudit> node is written.
    • A new <Revision> entry is appended with valid Date, ModifiedBy, Text, UserID when Perastage intentionally changes a GDTF.
    • No duplicate identical Perastage revision is appended on repeated exports.
  3. Compatibility safety
    • Legacy/no-audit and unknown-schema files remain readable through fallback behavior.
  4. Archive integrity
    • Rewritten .gdtf keeps description.xml readable and required referenced resources present.
    • Exported .gdtf content has official FixtureType child order and contains no PerastageMutationAudit node.
  5. 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 in resources/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 Color in the source .gdtf.
  • Edit fixture physical properties and verify weight/power values persist after reopening.
  • Inspect resulting description.xml and verify no new PerastageMutationAudit node is present, FixtureType children follow the official order, and Revision attributes 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 SchemaVersion triggers 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.

Back to top