import { workbenchDefinitions, type WorkbenchId } from '../freecadManifest'
import type {
  BitBybitWebCadFacade,
  AddDynamicPropertyInput,
  CommandState,
  DocumentSnapshot,
  Diagnostic,
  DiagnosticRepairAction,
  DiagnosticRepairResult,
  ExecuteCommandInput,
  FacadeEvent,
  FacadeListener,
  FacadeRequestContext,
  FacadeState,
  ModelTreeItem,
  LinkSubListValue,
  MultiTransformValue,
  DocumentObjectSnapshot,
  ObjectPropertySnapshot,
  PropertyValue,
  PlacementValue,
  ReorderBodyFeatureInput,
  RemoveObjectInput,
  RemoveDynamicPropertyInput,
  RenameDynamicPropertyInput,
  SetPropertyInput,
  SetIncludedFileInput,
  SetExpressionInput,
  RecomputeResult,
  ResolveTopologyReferenceInput,
  ShapeHandle,
  SubshapeRef,
  SubshapeSelection,
  TaskSnapshot,
  TopoRefValue,
  Unsubscribe,
} from './types'
import { createFacadeRuntimeProfile, type FacadeRuntimeMode } from './runtimeProfile'
import { ATTACHMENT_MAP_MODES, validateAttachmentOffset, validateAttachmentSupport } from './attachment'
import { redirectBodyTips } from './bodyRules'
import { createSqliteProjectPersistence, ProjectAutosaveScheduler } from './projectStore'
import { BitbybitGeometryRuntime } from './geometryRuntime'
import { ThreeViewportAdapter } from './threeViewport'
import { DependencyGraph, createRecomputeSnapshot, type DependencyEdge } from './dependencyGraph'
import { convertQuantity, evaluateQuantityExpression, getUnit, quantityDimensionForUnit, quantityFromNumber, quantityFromUnit, type Quantity } from './units'
import { carbonCopySketchGeometry, cloneSketch, cloneSketchConstraint, cloneSketchGeometry, createSketch, projectSketchGeometry, solveSketch, type SketchConstraint, type SketchExternalGeometry, type SketchGeometry, type SketchSnapshot } from './sketcher'
import { createFacadeGeometryRecomputeExecutor, RecomputeCoordinator, sketchProfile, type RecomputeExecutionOptions } from './recomputeEngine'
import { decodeFcstdPathProperty, extractFcstdIncludedFileResources, extractFcstdShapeResources, inspectFcstdArchive, instantiateFcstdShapeResource, rewriteFcstdBoolListProperty, rewriteFcstdColorListProperty, rewriteFcstdColorProperty, rewriteFcstdDirectionProperty, rewriteFcstdFileProperty, rewriteFcstdFontProperty, rewriteFcstdIncludedFileProperty, rewriteFcstdIntegerSetProperty, rewriteFcstdMeasureArea, rewriteFcstdMetadataArchive, rewriteFcstdNumericProperty, rewriteFcstdPathProperty, serializeFcstdMetadataArchive, storeFcstdIncludedFileResources, storeFcstdShapeResources } from './fcstd'
import type { FcstdWriteOptions } from './fcstd'
import { buildDiagnosticTree, buildRecomputeDiagnostics, cloneDiagnostic, replaceRecomputeDiagnostics } from './diagnostics'
import { cloneObjectTopologySnapshot, migrateDocumentTopologyReferences, parseTopoRef, resolveDocumentTopologyReference } from './topologyReferences'
import { createCamJob } from './cam'
import { PUMP_HOUSING_DEMO_TEMPLATE, type DocumentTemplate } from './documentTemplates'
import { assertPartDesignParameterSet, parameterValuesForObject, validatePartDesignParameterSet } from './partDesignParameters'
import { freecadPropertyRecomputeEffect, hasFreecadPropertyStatus, isFreecadPropertyDocumentModifiedSuppressed, isFreecadPropertyDynamicMutationLocked, isFreecadPropertyPartialDocumentTrigger, isFreecadPropertyPersistenceSuppressed, isFreecadPropertyReadOnly, withFreecadPropertyStatus } from './propertyStatus'
import { includedFileMediaType, uniqueIncludedFileArchiveName, validateIncludedFileValue } from './includedFile'

const typeIdForItem = (item: ModelTreeItem) => item.type === 'body' ? 'PartDesign::Body' : item.type === 'sketch' ? 'Sketcher::SketchObject' : item.id.startsWith('box') ? 'Part::Box' : item.id.startsWith('cylinder') ? 'Part::Cylinder' : item.id.startsWith('sphere') ? 'Part::Sphere' : item.id.startsWith('ellipsoid') ? 'Part::Ellipsoid' : item.id.startsWith('cone') ? 'Part::Cone' : item.id.startsWith('torus') ? 'Part::Torus' : item.id.startsWith('helix') ? 'Part::Helix' : item.id.startsWith('prism') ? 'Part::Prism' : item.id.startsWith('wedge') ? 'Part::Wedge' : item.id.startsWith('union') ? 'Part::Fuse' : item.id.startsWith('cut') ? 'Part::Cut' : item.id.startsWith('intersection') ? 'Part::Common' : item.id.startsWith('pad') ? 'PartDesign::Pad' : item.id.startsWith('pocket') ? 'PartDesign::Pocket' : item.id.startsWith('revolution') ? 'PartDesign::Revolution' : item.id.startsWith('groove') ? 'PartDesign::Groove' : item.id.startsWith('fillet') ? 'PartDesign::Fillet' : item.id.startsWith('chamfer') ? 'PartDesign::Chamfer' : item.id.startsWith('mirrored') ? 'PartDesign::Mirrored' : item.id.startsWith('multi-transform') ? 'PartDesign::MultiTransform' : item.id.startsWith('linear-pattern') ? 'PartDesign::LinearPattern' : item.id.startsWith('polar-pattern') ? 'PartDesign::PolarPattern' : item.id.startsWith('hole') ? 'PartDesign::Hole' : item.type === 'feature' ? 'PartDesign::Feature' : 'App::DocumentObjectGroup'

const commonProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => [
  { name: 'Label', label: 'Label', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: item.label },
  { name: 'TypeId', label: 'Type', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: typeIdForItem(item), readOnly: true },
  { name: 'Status', label: 'Status', group: 'Identity', scope: 'data', type: 'App::PropertyString', value: item.state === 'warning' ? 'Warning' : 'Valid', readOnly: true },
  ...(item.type === 'feature' ? [{ name: 'Suppressed', label: 'Suppressed', group: 'Feature state', scope: 'data' as const, type: 'App::PropertyBool' as const, value: false, recompute: true }] : []),
  ...(item.type === 'feature' ? [{ name: 'Placement', label: 'Placement', group: 'Attachment', scope: 'data' as const, type: 'App::PropertyPlacement' as const, value: { position: { x: 0, y: 0, z: 0 }, rotation: { axis: { x: 0, y: 0, z: 1 }, angle: 0 } }, recompute: true }] : []),
]

const viewProperties = (): ObjectPropertySnapshot[] => [
  { name: 'Visibility', label: 'Visibility', group: 'Display', scope: 'view', type: 'App::PropertyBool', value: true },
  { name: 'DisplayMode', label: 'Display mode', group: 'Display', scope: 'view', type: 'App::PropertyEnumeration', value: 'Flat lines', options: ['Flat lines', 'Shaded', 'Wireframe'] },
  { name: 'Transparency', label: 'Transparency', group: 'Display', scope: 'view', type: 'App::PropertyPercent', value: 0, unit: '%' },
  { name: 'ShapeColor', label: 'Shape color', group: 'Appearance', scope: 'view', type: 'App::PropertyColor', value: '#579a9c' },
  { name: 'LineColor', label: 'Line color', group: 'Appearance', scope: 'view', type: 'App::PropertyColor', value: '#8cb7b6' },
  { name: 'LineWidth', label: 'Line width', group: 'Appearance', scope: 'view', type: 'App::PropertyFloat', value: 1, unit: 'px' },
  { name: 'SelectionStyle', label: 'Selection style', group: 'View state', scope: 'view', type: 'App::PropertyEnumeration', value: 'Object + edges', options: ['Object + edges', 'Object', 'Bound box'] },
]

const featureProperties = (item: ModelTreeItem): ObjectPropertySnapshot[] => {
  if (item.id.startsWith('box')) return [
    { name: 'Length', label: 'Length', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
    { name: 'Width', label: 'Width', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
    { name: 'Height', label: 'Height', group: 'Box', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
  ]
  if (item.id.startsWith('cylinder')) return [
    { name: 'Radius', label: 'Radius', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
    { name: 'Height', label: 'Height', group: 'Cylinder', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
    { name: 'Angle', label: 'Angle', group: 'Cylinder', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
  ]
  if (item.id.startsWith('sphere')) return [
    { name: 'Radius', label: 'Radius', group: 'Sphere', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
    { name: 'Angle1', label: 'Lower angle', group: 'Sphere', scope: 'data', type: 'App::PropertyAngle', value: -90, unit: 'deg', recompute: true },
    { name: 'Angle2', label: 'Upper angle', group: 'Sphere', scope: 'data', type: 'App::PropertyAngle', value: 90, unit: 'deg', recompute: true },
    { name: 'Angle3', label: 'Azimuth angle', group: 'Sphere', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
  ]
  if (item.id.startsWith('ellipsoid')) return [
    { name: 'Radius1', label: 'Z radius', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyLength', value: 2, unit: 'mm', recompute: true },
    { name: 'Radius2', label: 'X radius', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyLength', value: 4, unit: 'mm', recompute: true },
    { name: 'Radius3', label: 'Y radius', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyLength', value: 0, unit: 'mm', recompute: true },
    { name: 'Angle1', label: 'Lower angle', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyAngle', value: -90, unit: 'deg', recompute: true },
    { name: 'Angle2', label: 'Upper angle', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyAngle', value: 90, unit: 'deg', recompute: true },
    { name: 'Angle3', label: 'Azimuth angle', group: 'Ellipsoid', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
  ]
  if (item.id.startsWith('cone')) return [
    { name: 'Radius1', label: 'Bottom radius', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
    { name: 'Radius2', label: 'Top radius', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 0, unit: 'mm', recompute: true },
    { name: 'Height', label: 'Height', group: 'Cone', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
    { name: 'Angle', label: 'Angle', group: 'Cone', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
  ]
  if (item.id.startsWith('torus')) return [
    { name: 'Radius1', label: 'Major radius', group: 'Torus', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
    { name: 'Radius2', label: 'Minor radius', group: 'Torus', scope: 'data', type: 'App::PropertyLength', value: 2, unit: 'mm', recompute: true },
    { name: 'Angle1', label: 'Lower angle', group: 'Torus', scope: 'data', type: 'App::PropertyAngle', value: -180, unit: 'deg', recompute: true },
    { name: 'Angle2', label: 'Upper angle', group: 'Torus', scope: 'data', type: 'App::PropertyAngle', value: 180, unit: 'deg', recompute: true },
    { name: 'Angle3', label: 'Azimuth angle', group: 'Torus', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
  ]
  if (item.id.startsWith('helix')) return [
    { name: 'Pitch', label: 'Pitch', group: 'Helix', scope: 'data', type: 'App::PropertyLength', value: 1, unit: 'mm', recompute: true },
    { name: 'Height', label: 'Height', group: 'Helix', scope: 'data', type: 'App::PropertyLength', value: 2, unit: 'mm', recompute: true },
    { name: 'Radius', label: 'Radius', group: 'Helix', scope: 'data', type: 'App::PropertyLength', value: 1, unit: 'mm', recompute: true },
    { name: 'SegmentLength', label: 'Segment length', group: 'Helix', scope: 'data', type: 'App::PropertyQuantityConstraint', value: 0, recompute: true },
    { name: 'Angle', label: 'Cone angle', group: 'Helix', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
    { name: 'LocalCoord', label: 'Handedness', group: 'Coordinate System', scope: 'data', type: 'App::PropertyEnumeration', value: 'Right-handed', options: ['Right-handed', 'Left-handed'], recompute: true },
    { name: 'Style', label: 'Helix style', group: 'Helix style', scope: 'data', type: 'App::PropertyEnumeration', value: 'Old style', options: ['Old style', 'New style'], hidden: true, recompute: true },
    { name: 'Length', label: 'Length', group: 'Helix', scope: 'data', type: 'App::PropertyLength', value: 1, unit: 'mm', readOnly: true },
  ]
  if (item.id.startsWith('prism')) return [
    { name: 'Polygon', label: 'Polygon sides', group: 'Prism', scope: 'data', type: 'App::PropertyInteger', value: 6, recompute: true },
    { name: 'Circumradius', label: 'Circumradius', group: 'Prism', scope: 'data', type: 'App::PropertyLength', value: 2, unit: 'mm', recompute: true },
    { name: 'Height', label: 'Height', group: 'Prism', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
    { name: 'FirstAngle', label: 'First angle', group: 'Prism', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
    { name: 'SecondAngle', label: 'Second angle', group: 'Prism', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
  ]
  if (item.id.startsWith('wedge')) return [
    { name: 'Xmin', label: 'X minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
    { name: 'Ymin', label: 'Y minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
    { name: 'Zmin', label: 'Z minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
    { name: 'X2min', label: 'X2 minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 2, unit: 'mm', recompute: true },
    { name: 'Z2min', label: 'Z2 minimum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 2, unit: 'mm', recompute: true },
    { name: 'Xmax', label: 'X maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 10, unit: 'mm', recompute: true },
    { name: 'Ymax', label: 'Y maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 10, unit: 'mm', recompute: true },
    { name: 'Zmax', label: 'Z maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 10, unit: 'mm', recompute: true },
    { name: 'X2max', label: 'X2 maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 8, unit: 'mm', recompute: true },
    { name: 'Z2max', label: 'Z2 maximum', group: 'Wedge', scope: 'data', type: 'App::PropertyDistance', value: 8, unit: 'mm', recompute: true },
  ]
  if (item.id.startsWith('union') || item.id.startsWith('cut') || item.id.startsWith('intersection')) return [
    { name: 'Base', label: 'Base', group: 'Boolean', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
    { name: 'Tool', label: 'Tool', group: 'Boolean', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
    { name: 'Refine', label: 'Refine shape', group: 'Boolean', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
  ]
  if (item.id.startsWith('pad')) return [
    { name: 'Length', label: 'Length', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 42, unit: 'mm', recompute: true, expression: '42 mm' },
    { name: 'Profile', label: 'Profile', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'sketch', recompute: true },
    { name: 'SideType', label: 'Side definition', group: 'Parameters', scope: 'data', type: 'App::PropertyEnumeration', value: 'One side', options: ['One side', 'Two sides', 'Symmetric'], recompute: true },
    { name: 'Type', label: 'Type', group: 'Parameters', scope: 'data', type: 'App::PropertyEnumeration', value: 'Dimension', options: ['Dimension', 'Through all', 'Up to face', 'TwoLengths'], recompute: true },
    { name: 'Type2', label: 'Type 2', group: 'Parameters', scope: 'data', type: 'App::PropertyEnumeration', value: 'Dimension', options: ['Dimension', 'Through all', 'Up to face'], recompute: true },
    { name: 'UpToFace', label: 'Up to face', group: 'Parameters', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'UpToFace2', label: 'Reverse up to face', group: 'Parameters', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'Length2', label: 'Reverse length', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 0, unit: 'mm', recompute: true },
    { name: 'TaperAngle', label: 'Taper angle', group: 'Parameters', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
    { name: 'TaperAngle2', label: 'Reverse taper angle', group: 'Parameters', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
    { name: 'Reversed', label: 'Reversed', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Midplane', label: 'Symmetric to plane', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'ReferenceAxis', label: 'Reference axis', group: 'Direction', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'AlongSketchNormal', label: 'Along sketch normal', group: 'Direction', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
    { name: 'UseCustomVector', label: 'Use custom vector', group: 'Direction', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Direction', label: 'Direction', group: 'Direction', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 1 }, recompute: true },
    { name: 'Offset', label: 'Profile offset', group: 'Parameters', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
    { name: 'Offset2', label: 'Reverse profile offset', group: 'Parameters', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
  ]
  if (item.id.startsWith('pocket')) return [
    { name: 'SideType', label: 'Side definition', group: 'Parameters', scope: 'data', type: 'App::PropertyEnumeration', value: 'One side', options: ['One side', 'Two sides', 'Symmetric'], recompute: true },
    { name: 'Type', label: 'Type', group: 'Parameters', scope: 'data', type: 'App::PropertyEnumeration', value: 'Through all', options: ['Dimension', 'Through all', 'Up to face', 'TwoLengths'], recompute: true },
    { name: 'Type2', label: 'Type 2', group: 'Parameters', scope: 'data', type: 'App::PropertyEnumeration', value: 'Dimension', options: ['Dimension', 'Through all', 'Up to face'], recompute: true },
    { name: 'Length', label: 'Length', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 18, unit: 'mm', recompute: true },
    { name: 'Length2', label: 'Reverse length', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 0, unit: 'mm', recompute: true },
    { name: 'Profile', label: 'Profile', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'sketch', recompute: true },
    { name: 'Base', label: 'Base', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'pad', recompute: true },
    { name: 'UpToFace', label: 'Up to face', group: 'Parameters', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'UpToFace2', label: 'Reverse up to face', group: 'Parameters', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'TaperAngle', label: 'Taper angle', group: 'Parameters', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
    { name: 'TaperAngle2', label: 'Reverse taper angle', group: 'Parameters', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
    { name: 'Reversed', label: 'Reversed', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Midplane', label: 'Symmetric to plane', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'ReferenceAxis', label: 'Reference axis', group: 'Direction', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'AlongSketchNormal', label: 'Along sketch normal', group: 'Direction', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
    { name: 'UseCustomVector', label: 'Use custom vector', group: 'Direction', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Direction', label: 'Direction', group: 'Direction', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 1 }, recompute: true },
    { name: 'Offset', label: 'Profile offset', group: 'Parameters', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
    { name: 'Offset2', label: 'Reverse profile offset', group: 'Parameters', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
  ]
  if (item.id.startsWith('revolution')) return [
    { name: 'Angle', label: 'Angle', group: 'Parameters', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
    { name: 'Angle2', label: 'Reverse angle', group: 'Parameters', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
    { name: 'Type', label: 'Type', group: 'Parameters', scope: 'data', type: 'App::PropertyEnumeration', value: 'Angle', options: ['Angle', 'To last', 'To first', 'Up to face', 'Two angles'], recompute: true },
    { name: 'Profile', label: 'Profile', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'sketch', recompute: true },
    { name: 'UpToFace', label: 'Up to face', group: 'Parameters', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'ReferenceAxis', label: 'Reference axis', group: 'Axis', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'Axis', label: 'Axis', group: 'Axis', scope: 'data', type: 'App::PropertyEnumeration', value: 'Vertical sketch axis', options: ['Horizontal sketch axis', 'Vertical sketch axis', 'Custom'], recompute: true },
    { name: 'AxisLink', label: 'Axis link', group: 'Axis', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'Midplane', label: 'Symmetric to plane', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Reversed', label: 'Reversed', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Offset', label: 'Profile offset', group: 'Parameters', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
    { name: 'Offset2', label: 'Reverse profile offset', group: 'Parameters', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
  ]
  if (item.id.startsWith('groove')) return [
    { name: 'Angle', label: 'Angle', group: 'Parameters', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
    { name: 'Angle2', label: 'Reverse angle', group: 'Parameters', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
    { name: 'Type', label: 'Type', group: 'Parameters', scope: 'data', type: 'App::PropertyEnumeration', value: 'Angle', options: ['Angle', 'To last', 'To first', 'Up to face', 'Two angles'], recompute: true },
    { name: 'Profile', label: 'Profile', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'sketch', recompute: true },
    { name: 'Base', label: 'Base', group: 'Parameters', scope: 'data', type: 'App::PropertyLink', value: 'pad', recompute: true },
    { name: 'UpToFace', label: 'Up to face', group: 'Parameters', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'ReferenceAxis', label: 'Reference axis', group: 'Axis', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'Axis', label: 'Axis', group: 'Axis', scope: 'data', type: 'App::PropertyEnumeration', value: 'Vertical sketch axis', options: ['Horizontal sketch axis', 'Vertical sketch axis', 'Custom'], recompute: true },
    { name: 'AxisLink', label: 'Axis link', group: 'Axis', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
    { name: 'Midplane', label: 'Symmetric to plane', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Reversed', label: 'Reversed', group: 'Parameters', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Offset', label: 'Profile offset', group: 'Parameters', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
    { name: 'Offset2', label: 'Reverse profile offset', group: 'Parameters', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
  ]
  if (item.id.startsWith('fillet')) return [
    { name: 'Radius', label: 'Radius', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 3, unit: 'mm', recompute: true },
    { name: 'Base', label: 'Base', group: 'Dependencies', scope: 'data', type: 'App::PropertyLink', value: 'pocket', recompute: true },
  ]
  if (item.id.startsWith('chamfer')) return [
    { name: 'Distance', label: 'Distance', group: 'Parameters', scope: 'data', type: 'App::PropertyLength', value: 2, unit: 'mm', recompute: true },
    { name: 'Base', label: 'Base', group: 'Dependencies', scope: 'data', type: 'App::PropertyLink', value: 'pocket', recompute: true },
  ]
  if (item.id.startsWith('mirrored')) return [
    { name: 'Base', label: 'Base feature', group: 'Mirrored', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
    { name: 'Originals', label: 'Original features', group: 'Mirrored', scope: 'data', type: 'App::PropertyLinkList', value: [], recompute: true },
    { name: 'TransformMode', label: 'Transform mode', group: 'Mirrored', scope: 'data', type: 'App::PropertyEnumeration', value: 'Whole shape', options: ['Features', 'Whole shape'], recompute: true },
    { name: 'Plane', label: 'Mirror plane', group: 'Mirrored', scope: 'data', type: 'App::PropertyEnumeration', value: 'YZ plane', options: ['XY plane', 'XZ plane', 'YZ plane'], recompute: true },
    { name: 'PlaneOrigin', label: 'Plane origin', group: 'Mirrored', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 0 }, recompute: true },
    { name: 'PlaneNormal', label: 'Plane normal', group: 'Mirrored', scope: 'data', type: 'App::PropertyVector', value: { x: 1, y: 0, z: 0 }, recompute: true },
    { name: 'Fuse', label: 'Fuse result', group: 'Mirrored', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
  ]
  if (item.id.startsWith('multi-transform')) return [
    { name: 'Base', label: 'Base feature', group: 'Multi-transform', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
    { name: 'Originals', label: 'Original features', group: 'Multi-transform', scope: 'data', type: 'App::PropertyLinkList', value: [], recompute: true },
    { name: 'TransformMode', label: 'Transform mode', group: 'Multi-transform', scope: 'data', type: 'App::PropertyEnumeration', value: 'Whole shape', options: ['Features', 'Whole shape'], recompute: true },
    { name: 'Transformations', label: 'Transformations', group: 'Multi-transform', scope: 'data', type: 'App::PropertyMultiTransform', value: { steps: [{ id: 'linear-1', type: 'linear', occurrences: 2, length: 20, direction: 'Horizontal' }] }, recompute: true },
  ]
  if (item.id.startsWith('linear-pattern')) return [
    { name: 'Base', label: 'Base', group: 'Pattern', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
    { name: 'Originals', label: 'Original features', group: 'Pattern', scope: 'data', type: 'App::PropertyLinkList', value: [], recompute: true },
    { name: 'TransformMode', label: 'Transform mode', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Whole shape', options: ['Features', 'Whole shape'], recompute: true },
    { name: 'Occurrences', label: 'Occurrences', group: 'Pattern', scope: 'data', type: 'App::PropertyInteger', value: 2, recompute: true },
    { name: 'Length', label: 'Length', group: 'Pattern', scope: 'data', type: 'App::PropertyLength', value: 20, unit: 'mm', recompute: true },
    { name: 'Offset', label: 'Spacing', group: 'Pattern', scope: 'data', type: 'App::PropertyLength', value: 20, unit: 'mm', recompute: true },
    { name: 'Direction', label: 'Direction', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Horizontal', options: ['Horizontal', 'Vertical', 'Normal'], recompute: true },
    { name: 'DirectionVector', label: 'Direction vector', group: 'Pattern', scope: 'data', type: 'App::PropertyVector', value: { x: 1, y: 0, z: 0 }, recompute: true },
    { name: 'Mode', label: 'Mode', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Extent', options: ['Extent', 'Spacing'], recompute: true },
    { name: 'Reversed', label: 'Reversed', group: 'Pattern', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Spacings', label: 'Custom spacings', group: 'Pattern', scope: 'data', type: 'App::PropertyFloatList', value: [-1], unit: 'mm', recompute: true },
    { name: 'SpacingPattern', label: 'Spacing pattern', group: 'Pattern', scope: 'data', type: 'App::PropertyFloatList', value: [], unit: 'mm', recompute: true },
    { name: 'Direction2', label: 'Direction 2', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'None', options: ['None', 'Horizontal', 'Vertical', 'Normal'], recompute: true },
    { name: 'Mode2', label: 'Mode 2', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Extent', options: ['Extent', 'Spacing'], recompute: true },
    { name: 'Length2', label: 'Length 2', group: 'Pattern', scope: 'data', type: 'App::PropertyLength', value: 20, unit: 'mm', recompute: true },
    { name: 'Offset2', label: 'Spacing 2', group: 'Pattern', scope: 'data', type: 'App::PropertyLength', value: 20, unit: 'mm', recompute: true },
    { name: 'Occurrences2', label: 'Occurrences 2', group: 'Pattern', scope: 'data', type: 'App::PropertyInteger', value: 1, recompute: true },
    { name: 'Reversed2', label: 'Reversed 2', group: 'Pattern', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Spacings2', label: 'Custom spacings 2', group: 'Pattern', scope: 'data', type: 'App::PropertyFloatList', value: [], unit: 'mm', recompute: true },
    { name: 'SpacingPattern2', label: 'Spacing pattern 2', group: 'Pattern', scope: 'data', type: 'App::PropertyFloatList', value: [], unit: 'mm', recompute: true },
  ]
  if (item.id.startsWith('polar-pattern')) return [
    { name: 'Base', label: 'Base', group: 'Pattern', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
    { name: 'Originals', label: 'Original features', group: 'Pattern', scope: 'data', type: 'App::PropertyLinkList', value: [], recompute: true },
    { name: 'TransformMode', label: 'Transform mode', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Whole shape', options: ['Features', 'Whole shape'], recompute: true },
    { name: 'Occurrences', label: 'Occurrences', group: 'Pattern', scope: 'data', type: 'App::PropertyInteger', value: 3, recompute: true },
    { name: 'Angle', label: 'Angle', group: 'Pattern', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
    { name: 'Axis', label: 'Axis', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Normal', options: ['Normal', 'Horizontal', 'Vertical'], recompute: true },
    { name: 'AxisOrigin', label: 'Axis origin', group: 'Pattern', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 0 }, recompute: true },
    { name: 'AxisDirection', label: 'Axis direction', group: 'Pattern', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 1 }, recompute: true },
    { name: 'Mode', label: 'Mode', group: 'Pattern', scope: 'data', type: 'App::PropertyEnumeration', value: 'Extent', options: ['Extent', 'Spacing'], recompute: true },
    { name: 'Offset', label: 'Spacing', group: 'Pattern', scope: 'data', type: 'App::PropertyAngle', value: 30, unit: 'deg', recompute: true },
    { name: 'Spacings', label: 'Custom spacings', group: 'Pattern', scope: 'data', type: 'App::PropertyFloatList', value: [-1, -1], unit: 'deg', recompute: true },
    { name: 'SpacingPattern', label: 'Spacing pattern', group: 'Pattern', scope: 'data', type: 'App::PropertyFloatList', value: [], unit: 'deg', recompute: true },
    { name: 'Reversed', label: 'Reversed', group: 'Pattern', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
  ]
  if (item.id.startsWith('hole')) return [
    { name: 'Base', label: 'Base', group: 'Hole', scope: 'data', type: 'App::PropertyLink', value: null, recompute: true },
    { name: 'Diameter', label: 'Diameter', group: 'Hole', scope: 'data', type: 'App::PropertyLength', value: 5, unit: 'mm', recompute: true },
    { name: 'Depth', label: 'Depth', group: 'Hole', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
    { name: 'Type', label: 'Type', group: 'Hole', scope: 'data', type: 'App::PropertyEnumeration', value: 'Dimension', options: ['Dimension', 'Through all'], recompute: true },
    { name: 'DepthType', label: 'Depth type', group: 'Hole', scope: 'data', type: 'App::PropertyEnumeration', value: 'Dimension', options: ['Dimension', 'ThroughAll'], recompute: true },
    { name: 'Position', label: 'Position', group: 'Hole', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 0 }, recompute: true },
    { name: 'Direction', label: 'Direction', group: 'Hole', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 1 }, recompute: true },
    { name: 'Reversed', label: 'Reversed', group: 'Hole', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'HoleCutType', label: 'Cut type', group: 'Hole', scope: 'data', type: 'App::PropertyEnumeration', value: 'None', options: ['None', 'Counterbore', 'Countersink', 'Counterdrill'], recompute: true },
    { name: 'HoleCutDiameter', label: 'Cut diameter', group: 'Hole', scope: 'data', type: 'App::PropertyLength', value: 8, unit: 'mm', recompute: true },
    { name: 'HoleCutDepth', label: 'Cut depth', group: 'Hole', scope: 'data', type: 'App::PropertyLength', value: 2, unit: 'mm', recompute: true },
    { name: 'HoleCutCountersinkAngle', label: 'Countersink angle', group: 'Hole', scope: 'data', type: 'App::PropertyAngle', value: 90, unit: 'deg', recompute: true },
    { name: 'DrillPoint', label: 'Drill point', group: 'Hole', scope: 'data', type: 'App::PropertyEnumeration', value: 'Flat', options: ['Flat', 'Angled'], recompute: true },
    { name: 'DrillPointAngle', label: 'Drill point angle', group: 'Hole', scope: 'data', type: 'App::PropertyAngle', value: 118, unit: 'deg', recompute: true },
    { name: 'DrillForDepth', label: 'Drill for depth', group: 'Hole', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Tapered', label: 'Tapered', group: 'Hole', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'TaperedAngle', label: 'Taper angle', group: 'Hole', scope: 'data', type: 'App::PropertyAngle', value: 90, unit: 'deg', recompute: true },
    { name: 'HoleCutCustomValues', label: 'Custom cut values', group: 'Hole', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'Threaded', label: 'Threaded', group: 'Hole', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'ModelThread', label: 'Model thread', group: 'Thread', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'ThreadType', label: 'Thread standard', group: 'Thread', scope: 'data', type: 'App::PropertyEnumeration', value: 'None', options: ['None', 'ISOMetricProfile', 'ISOMetricFineProfile', 'UNC', 'UNF', 'UNEF', 'NPT', 'BSP', 'BSW', 'BSF', 'ISOTyre'], recompute: true },
    { name: 'ThreadSize', label: 'Thread size', group: 'Thread', scope: 'data', type: 'App::PropertyEnumeration', value: 'M6x1.0', options: ['M1x0.25', 'M2x0.4', 'M3x0.5', 'M4x0.7', 'M5x0.8', 'M6x1.0', 'M8x1.25', 'M10x1.5', '#4', '#6', '#8', '#10', '1/4'], recompute: true },
    { name: 'ThreadDiameter', label: 'Thread major diameter', group: 'Thread', scope: 'data', type: 'App::PropertyLength', value: 6, unit: 'mm', recompute: true },
    { name: 'ThreadPitch', label: 'Thread pitch', group: 'Thread', scope: 'data', type: 'App::PropertyLength', value: 1, unit: 'mm', recompute: true },
    { name: 'ThreadClass', label: 'Thread class', group: 'Thread', scope: 'data', type: 'App::PropertyEnumeration', value: '6H', options: ['4G', '5G', '6G', '7G', '8G', '4H', '5H', '6H', '7H', '8H', '1B', '2B', '3B'], recompute: true },
    { name: 'ThreadFit', label: 'Thread fit', group: 'Thread', scope: 'data', type: 'App::PropertyEnumeration', value: 'Medium', options: ['Medium', 'Fine', 'Coarse', 'Normal', 'Close', 'Loose', 'Wide'], recompute: true },
    { name: 'ThreadDirection', label: 'Thread direction', group: 'Thread', scope: 'data', type: 'App::PropertyEnumeration', value: 'Right', options: ['Right', 'Left'], recompute: true },
    { name: 'ThreadDepthType', label: 'Thread depth type', group: 'Thread', scope: 'data', type: 'App::PropertyEnumeration', value: 'Hole Depth', options: ['Hole Depth', 'Dimension', 'Tapped (DIN76)'], recompute: true },
    { name: 'ThreadDepth', label: 'Thread depth', group: 'Thread', scope: 'data', type: 'App::PropertyLength', value: 10, unit: 'mm', recompute: true },
    { name: 'UseCustomThreadClearance', label: 'Custom thread clearance', group: 'Thread', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
    { name: 'CustomThreadClearance', label: 'Thread clearance', group: 'Thread', scope: 'data', type: 'App::PropertyLength', value: 0, unit: 'mm', recompute: true },
  ]
  if (item.type === 'sketch') return [
    { name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLink', value: 'XY_Plane', recompute: true },
    { name: 'MapMode', label: 'Map mode', group: 'Attachment', scope: 'data', type: 'App::PropertyEnumeration', value: 'Deactivated', options: [...ATTACHMENT_MAP_MODES], recompute: true },
    { name: 'AttachmentOffset', label: 'Attachment offset', group: 'Attachment', scope: 'data', type: 'App::PropertyPlacement', value: { position: { x: 0, y: 0, z: 0 }, rotation: { axis: { x: 0, y: 0, z: 1 }, angle: 0 } }, recompute: true },
    { name: 'ConstraintStatus', label: 'Solver state', group: 'Constraints', scope: 'data', type: 'App::PropertyString', value: 'Fully constrained', readOnly: true },
  ]
  if (item.type === 'body') return [
    { name: 'Tip', label: 'Tip', group: 'Part Design', scope: 'data', type: 'App::PropertyLink', value: 'fillet', readOnly: true, recompute: true },
  ]
  return []
}

const createObjectSnapshot = (item: ModelTreeItem): DocumentObjectSnapshot => ({ id: item.id, typeId: typeIdForItem(item), properties: [...commonProperties(item), ...featureProperties(item), ...viewProperties()], sketch: item.type === 'sketch' ? createSketch(item.id) : undefined })

const clonePropertyValue = (value: PropertyValue): PropertyValue => {
  if (Array.isArray(value)) {
    if (value.every((entry) => Array.isArray(entry))) return value.map((entry) => [...entry]) as PropertyValue
    if (value.every((entry) => typeof entry === 'boolean')) return [...value] as boolean[]
    return value.every((entry) => typeof entry === 'number') ? [...value] as number[] : [...value] as string[]
  }
  if (!value || typeof value !== 'object') return value
  if ('position' in value && 'rotation' in value) return { position: { ...value.position }, rotation: { axis: { ...value.rotation.axis }, angle: value.rotation.angle } }
  if ('steps' in value && Array.isArray(value.steps)) return { steps: value.steps.map((step) => ({ ...step })) }
  if ('entries' in value && Array.isArray(value.entries)) return { ...value, entries: value.entries.map((entry) => ({ ...entry, subElement: entry.subElement && typeof entry.subElement === 'object' ? { ...entry.subElement, candidates: entry.subElement.candidates ? [...entry.subElement.candidates] : undefined } : entry.subElement })) }
  if ('subElements' in value && Array.isArray(value.subElements)) return { ...value, subElements: value.subElements.map((entry) => typeof entry === 'string' ? entry : { ...entry, candidates: entry.candidates ? [...entry.candidates] : undefined }) }
  if ('persistentId' in value) return { ...value, candidates: value.candidates ? [...value.candidates] : undefined }
  return { ...value }
}

const cloneDocumentSnapshot = (document: DocumentSnapshot): DocumentSnapshot => ({
  ...document,
  tree: document.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined })),
  objects: document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined })),
  dependencies: document.dependencies?.map((edge) => ({ ...edge })),
  recompute: document.recompute ? { ...document.recompute, dirtyObjects: [...document.recompute.dirtyObjects], order: [...document.recompute.order], objectStates: { ...document.recompute.objectStates }, errors: document.recompute.errors.map((error) => ({ ...error })) } : undefined,
})

const collectDependencyEdges = (document: Pick<DocumentSnapshot, 'objects'>): DependencyEdge[] => {
  const objectIds = new Set(document.objects.map((object) => object.id))
  const edges: DependencyEdge[] = []
  for (const object of document.objects) {
    for (const property of object.properties) {
      if (property.type === 'App::PropertyLink' && typeof property.value === 'string' && objectIds.has(property.value)) edges.push({ sourceId: object.id, targetId: property.value, relation: 'link', propertyName: property.name })
      if (property.type === 'App::PropertyLinkList' && Array.isArray(property.value)) for (const [index, targetId] of property.value.entries()) {
        if (typeof targetId === 'string' && objectIds.has(targetId)) edges.push({ sourceId: object.id, targetId, relation: 'link', propertyName: property.name, reference: `${property.name}[${index}]` })
      }
      if (property.type === 'App::PropertyLink' && property.name === 'Support' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'objectId' in property.value && typeof property.value.objectId === 'string' && objectIds.has(property.value.objectId)) {
        const subElement = 'subElement' in property.value ? property.value.subElement : null
        if (subElement && typeof subElement === 'object' && !Array.isArray(subElement) && 'schemaVersion' in subElement && typeof subElement.persistentId === 'string') edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'topo-ref', propertyName: property.name, reference: subElement.persistentId })
        else edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'link', propertyName: property.name })
      }
      if (property.type === 'App::PropertyLinkSub' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'objectId' in property.value && objectIds.has(property.value.objectId) && ('subElements' in property.value || 'persistentId' in property.value)) {
        if ('subElements' in property.value) {
          if (property.value.subElements.length === 0) edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'link', propertyName: property.name })
          else for (const [index, entry] of property.value.subElements.entries()) edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: typeof entry === 'string' ? 'link' : 'topo-ref', propertyName: property.name, reference: typeof entry === 'string' ? `${property.name}[${index}]` : entry.persistentId })
        } else edges.push({ sourceId: object.id, targetId: property.value.objectId, relation: 'topo-ref', propertyName: property.name, reference: property.value.persistentId })
      }
      if (property.type === 'App::PropertyLinkSubList' && property.value && typeof property.value === 'object' && !Array.isArray(property.value) && 'entries' in property.value && Array.isArray(property.value.entries)) {
        for (const [index, entry] of property.value.entries.entries()) {
          if (!objectIds.has(entry.objectId)) continue
          const subElement = entry.subElement
          edges.push({ sourceId: object.id, targetId: entry.objectId, relation: subElement && typeof subElement === 'object' ? 'topo-ref' : 'link', propertyName: property.name, reference: subElement && typeof subElement === 'object' ? subElement.persistentId : `${property.name}[${index}]` })
        }
      }
      if (property.expression) {
        for (const reference of expressionReferences(property.expression)) {
          const separator = reference.lastIndexOf('.')
          const targetId = separator > 0 ? reference.slice(0, separator) : ''
          if (objectIds.has(targetId)) edges.push({ sourceId: object.id, targetId, relation: 'expression', propertyName: property.name, reference })
        }
      }
    }
    for (const external of object.sketch?.externalGeometry ?? []) {
      if (objectIds.has(external.source.objectId)) edges.push({ sourceId: object.id, targetId: external.source.objectId, relation: 'topo-ref', propertyName: `ExternalGeometry:${external.id}`, reference: external.source.persistentId })
    }
  }
  return edges
}

const measureAreaFromElements = (document: DocumentSnapshot, value: LinkSubListValue): number => {
  let total = 0
  for (const entry of value.entries) {
    const source = document.objects.find((object) => object.id === entry.objectId)
    const topology = source?.topology
    if (!source || !topology) throw new Error('Cannot calculate area')
    const faces = topology.entries.filter((candidate) => candidate.ref.kind === 'face')
    if (entry.subElement === null) {
      if (faces.length === 0) throw new Error('Cannot calculate area')
      total += faces.reduce((sum, candidate) => sum + candidate.signature.area, 0)
      continue
    }
    const persistentId = typeof entry.subElement === 'string'
      ? topology.elementMap?.entries.find((candidate) => candidate.name === entry.subElement)?.persistentId
        ?? (/^Face([1-9]\d*)$/.test(entry.subElement) ? faces[Number(entry.subElement.slice(4)) - 1]?.ref.persistentId : entry.subElement)
      : entry.subElement.status === 'stable' || entry.subElement.status === 'new' ? entry.subElement.persistentId : ''
    const face = faces.find((candidate) => candidate.ref.persistentId === persistentId)
    if (!face || !Number.isFinite(face.signature.area) || face.signature.area < 0) throw new Error('Cannot calculate area')
    total += face.signature.area
  }
  if (!Number.isFinite(total)) throw new Error('Cannot calculate area')
  return total
}

const expressionReferences = (expression: string): string[] => {
  try {
    return evaluateQuantityExpression(expression).references
  } catch {
    const references = new Set<string>()
    for (const match of expression.matchAll(/[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*/g)) references.add(match[0])
    return [...references]
  }
}

const createDocumentFromTemplate = (template: DocumentTemplate): DocumentSnapshot => {
  const tree = template.tree.map((item) => ({ ...item, children: item.children ? [...item.children] : undefined }))
  const objects = tree.map((item, index) => ({ ...createObjectSnapshot(item), nativeObjectTag: index + 1 }))
  const document: DocumentSnapshot = { id: template.id, label: template.label, version: template.version, dirty: template.dirty, readOnly: template.readOnly, units: template.units, tree, objects }
  document.dependencies = collectDependencyEdges(document)
  document.recompute = createRecomputeSnapshot(objects.map((object) => object.id))
  return document
}

export const createBlankDocument = (label = 'Untitled document', id = 'doc-untitled'): DocumentSnapshot => ({
  id,
  label,
  version: 0,
  dirty: true,
  readOnly: false,
  units: 'mm',
  tree: [],
  objects: [],
  dependencies: [],
  recompute: createRecomputeSnapshot([]),
})

export const createPumpHousingDemoDocument = (): DocumentSnapshot => createDocumentFromTemplate(PUMP_HOUSING_DEMO_TEMPLATE)

export type WebCadFacadeOptions = {
  initialDocument?: DocumentSnapshot
  initialWorkbench?: WorkbenchId
  initialSelectedObjectIds?: string[]
  runtimeMode?: FacadeRuntimeMode
}

const selectionRequired = new Set(['pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'shape-binder', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'check-shape', 'hole', 'linear-pattern', 'polar-pattern', 'measure-distance', 'measure-angle', 'measure-area', 'solve-sketch'])
const systemCommands = new Set(['new-document', 'save', 'select-object'])
const camCommandIds = new Set(workbenchDefinitions.CAM.groups.flatMap((group) => group.commands.map((entry) => entry.id)))
const camSimulationCommands = new Set(['CAM_SimTools', 'CAM_Simulator', 'CAM_SimulatorGL'])
const camCopyCommands = new Set(['CAM_OperationCopy', 'CAM_Copy', 'CAM_SimpleCopy'])
const implementedCommandIds = new Set(['new-document', 'save', 'select-object', 'create-body', 'create-sketch', 'new-sketch', 'shape-binder', 'datum', 'pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'linear-pattern', 'polar-pattern', 'hole', 'primitive', 'helix', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'fillet-part', 'chamfer-part', 'check-shape', 'solve-sketch', ...camCommandIds])
const partDesignCommands = new Set(['create-body', 'create-sketch', 'shape-binder', 'datum', 'pad', 'pocket', 'revolution', 'groove', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe', 'fillet', 'chamfer', 'draft', 'thickness', 'mirrored', 'multi-transform', 'linear-pattern', 'polar-pattern', 'hole'])
const partCommands = new Set(['primitive', 'helix', 'extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'union', 'cut', 'intersection', 'fillet-part', 'chamfer-part', 'check-shape'])
const partProfileCommands = new Set(['extrude-part', 'revolution-part', 'loft-part', 'sweep-part', 'additive-loft', 'subtractive-loft', 'additive-pipe', 'subtractive-pipe'])
const loftCommands = new Set(['loft-part', 'additive-loft', 'subtractive-loft'])
const pipeCommands = new Set(['sweep-part', 'additive-pipe', 'subtractive-pipe'])
const partDressupCommands = new Set(['fillet-part', 'chamfer-part'])
const faceDressupCommands = new Set(['draft', 'thickness'])
const shapeSelectionCommands = new Set(['union', 'cut', 'intersection', 'fillet-part', 'chamfer-part', 'check-shape', 'fillet', 'chamfer', 'draft', 'thickness', 'groove', 'mirrored', 'multi-transform', 'linear-pattern', 'polar-pattern', 'hole'])
const featureSelectionCommands = new Set(['pad', 'pocket', 'revolution', 'groove'])
const closedProfileCommands = new Set(['pad', 'pocket', 'revolution', 'groove', 'extrude-part', 'revolution-part'])
const shapeTypeIds = new Set([
  'Part::Box',
  'Part::Cylinder',
  'Part::Sphere',
  'Part::Ellipsoid',
  'Part::Cone',
  'Part::Torus',
  'Part::Helix',
  'Part::Prism',
  'Part::Wedge',
  'Part::Fuse',
  'Part::Cut',
  'Part::Common',
  'Part::Extrusion',
  'Part::Extrude',
  'Part::Revolution',
  'Part::Fillet',
  'Part::Chamfer',
  'Part::Loft',
  'Part::Sweep',
  'Part::Feature',
  'PartDesign::Feature',
  'PartDesign::Pad',
  'PartDesign::Pocket',
  'PartDesign::Groove',
  'PartDesign::Revolution',
  'PartDesign::Fillet',
  'PartDesign::Chamfer',
  'PartDesign::Draft',
  'PartDesign::Thickness',
  'PartDesign::AdditiveLoft',
  'PartDesign::SubtractiveLoft',
  'PartDesign::AdditivePipe',
  'PartDesign::SubtractivePipe',
  'PartDesign::Mirrored',
  'PartDesign::MultiTransform',
  'PartDesign::LinearPattern',
  'PartDesign::PolarPattern',
  'PartDesign::Hole',
])
const featureCommands: Record<string, { label: string; detail: string }> = {
  'create-body': { label: 'Body', detail: 'Part Design body' },
  'create-sketch': { label: 'Sketch', detail: 'Fully constrained' },
  'shape-binder': { label: 'Shape Binder', detail: 'Cross-body support reference' },
  datum: { label: 'Datum Plane', detail: 'Attached datum reference' },
  pad: { label: 'Pad', detail: 'Length 42 mm' },
  pocket: { label: 'Pocket', detail: 'Through all' },
  revolution: { label: 'Revolution', detail: 'Angle 360 deg' },
  groove: { label: 'Groove', detail: 'Revolved subtractive profile' },
  fillet: { label: 'Fillet', detail: 'Radius 3 mm' },
  chamfer: { label: 'Chamfer', detail: 'Length 2 mm' },
  draft: { label: 'Draft', detail: 'Face taper draft' },
  thickness: { label: 'Thickness', detail: 'Solid shell thickness' },
  mirrored: { label: 'Mirrored', detail: 'Whole Shape across YZ plane' },
  'multi-transform': { label: 'Multi-transform', detail: 'Ordered whole-shape transformations' },
  'linear-pattern': { label: 'Linear Pattern', detail: '2 occurrences over 20 mm' },
  'polar-pattern': { label: 'Polar Pattern', detail: '3 occurrences over 360 deg' },
  hole: { label: 'Hole', detail: 'Simple 5 mm diameter hole' },
  primitive: { label: 'Box', detail: '10 × 10 × 10 mm' },
  helix: { label: 'Helix', detail: 'Pitch 1 mm, height 2 mm' },
  'extrude-part': { label: 'Extrude', detail: 'Standalone solid extrusion' },
  'revolution-part': { label: 'Revolution', detail: 'Standalone solid revolution' },
  'loft-part': { label: 'Loft', detail: 'Standalone multi-section loft' },
  'sweep-part': { label: 'Sweep', detail: 'Standalone profile sweep' },
  'additive-loft': { label: 'Additive Loft', detail: 'Multi-section additive feature' },
  'subtractive-loft': { label: 'Subtractive Loft', detail: 'Multi-section subtractive feature' },
  'additive-pipe': { label: 'Additive Pipe', detail: 'Profile along path additive feature' },
  'subtractive-pipe': { label: 'Subtractive Pipe', detail: 'Profile along path subtractive feature' },
  'fillet-part': { label: 'Fillet', detail: 'Stable selected-edge fillet' },
  'chamfer-part': { label: 'Chamfer', detail: 'Stable selected-edge chamfer' },
  union: { label: 'Union', detail: 'Boolean fuse' },
  cut: { label: 'Cut', detail: 'Boolean difference' },
  intersection: { label: 'Intersection', detail: 'Boolean common' },
}

const commandState = (commandId: string, activeWorkbench: WorkbenchId, selectedObjectId: string, selectedObject?: DocumentObjectSnapshot, selectedSubshape?: SubshapeSelection | null): CommandState => {
  const selectedTypeId = selectedObject?.typeId
  const known = systemCommands.has(commandId) || Object.values(workbenchDefinitions).some((definition) => definition.groups.some((group) => group.commands.some((command) => command.id === commandId)))
  if (!known) return { id: commandId, status: 'disabled', reason: 'Command is not registered in the active manifest.' }
  if (!implementedCommandIds.has(commandId)) return { id: commandId, status: 'disabled', reason: 'Command is visible in the FreeCAD-compatible manifest but its BitBybit business executor is not implemented yet.' }
  if (partDesignCommands.has(commandId) && activeWorkbench !== 'Part Design') return { id: commandId, status: 'disabled', reason: `Switch to Part Design to use ${commandId}.` }
  if (partCommands.has(commandId) && activeWorkbench !== 'Part') return { id: commandId, status: 'disabled', reason: `Switch to Part to use ${commandId}.` }
  if (camCommandIds.has(commandId) && activeWorkbench !== 'CAM') return { id: commandId, status: 'disabled', reason: `Switch to CAM to use ${commandId}.` }
  if (partProfileCommands.has(commandId) && selectedTypeId !== 'Sketcher::SketchObject') return { id: commandId, status: 'disabled', reason: `Select a Sketcher profile before using ${commandId}.` }
  if (partDressupCommands.has(commandId) && (!selectedSubshape || selectedSubshape.objectId !== selectedObjectId || selectedSubshape.ref.kind !== 'edge' || selectedSubshape.ref.status === 'ambiguous' || selectedSubshape.ref.status === 'deleted')) return { id: commandId, status: 'disabled', reason: `Select one stable edge before using ${commandId}.` }
  if (faceDressupCommands.has(commandId) && (!selectedSubshape || selectedSubshape.objectId !== selectedObjectId || selectedSubshape.ref.kind !== 'face' || selectedSubshape.ref.status === 'ambiguous' || selectedSubshape.ref.status === 'deleted')) return { id: commandId, status: 'disabled', reason: `Select one stable face before using ${commandId}.` }
  if ((commandId === 'new-sketch' || commandId === 'solve-sketch') && activeWorkbench !== 'Sketcher') return { id: commandId, status: 'disabled', reason: `Switch to Sketcher to use ${commandId}.` }
  if (selectionRequired.has(commandId) && !selectedObjectId) return { id: commandId, status: 'disabled', reason: 'Select a compatible object or sub-shape first.' }
  if (selectedObjectId && !selectedTypeId) return { id: commandId, status: 'disabled', reason: 'The selected object no longer exists in this document.' }
  if (commandId === 'solve-sketch' && selectedTypeId !== 'Sketcher::SketchObject') return { id: commandId, status: 'disabled', reason: 'Select a Sketcher sketch before solving constraints.' }
  if (shapeSelectionCommands.has(commandId) && selectedTypeId && !shapeTypeIds.has(selectedTypeId)) return { id: commandId, status: 'disabled', reason: 'Select a solid or shape-producing feature for this operation.' }
  if (featureSelectionCommands.has(commandId) && selectedTypeId && !shapeTypeIds.has(selectedTypeId) && selectedTypeId !== 'Sketcher::SketchObject') return { id: commandId, status: 'disabled', reason: 'Select a sketch or solid feature as the Part Design source.' }
  if (closedProfileCommands.has(commandId) && selectedTypeId === 'Sketcher::SketchObject' && !sketchProfile(selectedObject?.sketch).profile) return { id: commandId, status: 'disabled', reason: 'Select a closed supported Sketcher profile for this feature.' }
  return { id: commandId, status: 'enabled' }
}

const validatePropertyValue = (document: DocumentSnapshot, property: ObjectPropertySnapshot, value: PropertyValue) => {
  if (property.readOnly || isFreecadPropertyReadOnly(property.nativeStatus)) throw new Error(`${property.label} is read-only.`)
  if (property.type === 'App::PropertyBool' && typeof value !== 'boolean') throw new TypeError(`${property.label} requires a boolean value.`)
  if (property.type === 'App::PropertyBoolList' && (!Array.isArray(value) || !value.every((entry) => typeof entry === 'boolean'))) throw new TypeError(`${property.label} requires a boolean list.`)
  if (property.type === 'App::PropertyIntegerSet' && (!Array.isArray(value) || !value.every((entry) => typeof entry === 'number' && Number.isSafeInteger(entry)))) throw new TypeError(`${property.label} requires a safe-integer array.`)
  if (property.type === 'App::PropertyColorList' && (!Array.isArray(value) || !value.every((color) => Array.isArray(color) && color.length === 4 && color.every((channel) => typeof channel === 'number' && Number.isFinite(channel))))) throw new TypeError(`${property.label} requires a list of four finite RGBA channels.`)
  if ((property.type === 'App::PropertyString' || property.type === 'App::PropertyEnumeration' || property.type === 'App::PropertyFont') && typeof value !== 'string') throw new TypeError(`${property.label} requires a string value.`)
  if (property.type === 'App::PropertyFont') {
    if ((value as string).length > 1024) throw new RangeError(`${property.label} font family name is too long.`)
    if ((value as string).includes('\0') || /[\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value as string)) throw new RangeError(`${property.label} contains unsupported control characters.`)
  }
  if (property.type === 'App::PropertyFile') {
    if (typeof value !== 'string') throw new TypeError(`${property.label} requires a project-relative file reference.`)
    if (value.length > 4096) throw new RangeError(`${property.label} file reference is too long.`)
    if (value && (value.includes('\0') || value.includes('\\') || value.startsWith('/') || /^[A-Za-z]:/.test(value) || /^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.split('/').some((segment) => segment === '.' || segment === '..' || segment.length === 0))) throw new RangeError(`${property.label} requires a safe project-relative file reference.`)
  }
  if (property.type === 'App::PropertyFileIncluded') {
    if (value === null) {
      if (property.value !== null) throw new RangeError(`${property.label} cannot be cleared because native PropertyFileIncluded treats an empty setter as a no-op.`)
    } else validateIncludedFileValue(value, property.label)
  }
  if (property.type === 'App::PropertyColor') {
    if (property.scope === 'view') {
      if (typeof value !== 'string') throw new TypeError(`${property.label} requires a #RRGGBB color.`)
    } else if (!Array.isArray(value) || value.length !== 4 || !value.every((entry) => typeof entry === 'number' && Number.isFinite(entry))) throw new TypeError(`${property.label} requires four finite RGBA channels.`)
  }
  if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyForce' || property.type === 'App::PropertyHeatFlux' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyFloatConstraint' || property.type === 'App::PropertyPrecision' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && (typeof value !== 'number' || !Number.isFinite(value))) throw new TypeError(`${property.label} requires a finite numeric value.`)
  if ((property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && !Number.isSafeInteger(value)) throw new TypeError(`${property.label} requires an integer value.`)
  if (property.name === 'Occurrences' && ((value as number) < 2 || (value as number) > 100)) throw new RangeError('Occurrences must be between 2 and 100.')
  if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyQuantityConstraint') && (value as number) < 0) throw new RangeError(`${property.label} cannot be negative.`)
  if (property.type === 'App::PropertyPercent' && ((value as number) < 0 || (value as number) > 100)) throw new RangeError(`${property.label} must be between 0 and 100.`)
  if (property.type === 'App::PropertyEnumeration' && !property.options?.includes(value as string)) throw new RangeError(`${property.label} is not a registered enumeration value.`)
  if (property.type === 'App::PropertyColor' && property.scope === 'view' && !/^#[0-9a-f]{6}$/i.test(value as string)) throw new RangeError(`${property.label} requires a #RRGGBB color.`)
  if (property.type === 'App::PropertyVector' || property.type === 'App::PropertyDirection') validateVectorValue(property.label, value)
  if (property.type === 'App::PropertyPlacement') {
    if (!value || typeof value !== 'object' || Array.isArray(value) || !('position' in value) || !('rotation' in value)) throw new TypeError(`${property.label} requires a Placement value.`)
    const placement = value as PlacementValue
    validateVectorValue(`${property.label} position`, placement.position)
    validateVectorValue(`${property.label} rotation axis`, placement.rotation?.axis)
    if (!placement.rotation || typeof placement.rotation.angle !== 'number' || !Number.isFinite(placement.rotation.angle)) throw new TypeError(`${property.label} rotation angle must be finite.`)
    if (placement.rotation.angle < 0 || placement.rotation.angle > 360) throw new RangeError(`${property.label} rotation angle must be between 0 and 360 degrees.`)
    const axis = placement.rotation.axis
    if (Math.hypot(axis.x, axis.y, axis.z) === 0) throw new RangeError(`${property.label} rotation axis cannot be zero.`)
    if (property.name === 'AttachmentOffset') validateAttachmentOffset(value)
  }
  if (property.type === 'App::PropertyMultiTransform') validateMultiTransformValue(value)
  if (property.type === 'App::PropertyLinkList' || property.type === 'App::PropertyStringList') {
    if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'string')) throw new TypeError(`${property.label} requires a string list.`)
    if (property.type === 'App::PropertyLinkList') {
      const knownIds = new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])]))
      if (value.some((entry) => !knownIds.has(entry))) throw new RangeError(`${property.label} contains a target that does not exist in this document.`)
    }
  }
  if (property.type === 'App::PropertyFloatList') {
    if (!Array.isArray(value) || !value.every((entry) => typeof entry === 'number' && Number.isFinite(entry))) throw new TypeError(`${property.label} requires a finite numeric list.`)
  }
  if (property.type === 'Part::PropertyPartShape') {
    if (!value || typeof value !== 'object' || Array.isArray(value) || !('path' in value) || typeof value.path !== 'string' || !value.path.trim()) throw new TypeError(`${property.label} requires a BRep resource path.`)
    if (value.path.includes('\0') || value.path.includes('\\') || value.path.startsWith('/') || value.path.split('/').some((segment) => segment === '.' || segment === '..')) throw new RangeError(`${property.label} contains an unsafe resource path.`)
    if (!/\.(?:brp|brep)$/i.test(value.path)) throw new TypeError(`${property.label} requires a .brp or .brep resource.`)
    if ('elementMapResource' in value && value.elementMapResource !== undefined) {
      if (typeof value.elementMapResource !== 'string' || !value.elementMapResource.trim()) throw new TypeError(`${property.label} requires a non-empty ElementMap2 resource path.`)
      if (value.elementMapResource.includes('\0') || value.elementMapResource.includes('\\') || value.elementMapResource.startsWith('/') || value.elementMapResource.split('/').some((segment) => segment === '.' || segment === '..')) throw new RangeError(`${property.label} contains an unsafe ElementMap2 resource path.`)
      if (!/\.Map\.txt$/i.test(value.elementMapResource)) throw new TypeError(`${property.label} requires an ElementMap2 .Map.txt resource.`)
    }
  }
  if (property.type === 'App::PropertyLink' || property.type === 'App::PropertyLinkHidden') {
    if (property.name === 'Support' && value && typeof value === 'object' && !Array.isArray(value)) {
      validateAttachmentSupport(value)
      const knownIds = new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])]))
      if (!knownIds.has(value.objectId)) throw new RangeError(`${property.label} target does not exist in this document.`)
      return
    }
    if (value !== null && typeof value !== 'string') throw new TypeError(`${property.label} requires an object link.`)
    const knownIds = new Set(document.tree.flatMap((item) => [item.id, ...(item.children ?? [])]))
    if (value !== null && !knownIds.has(value)) throw new RangeError(`${property.label} target does not exist in this document.`)
  }
  if (property.type === 'App::PropertyLinkSub') {
    if (value !== null && (typeof value !== 'object' || Array.isArray(value))) throw new TypeError(`${property.label} requires a TopoRef value.`)
    if (value !== null) {
      if ('subElements' in value) {
        if (value.schemaVersion !== 1 || typeof value.objectId !== 'string' || !value.objectId.trim() || !Array.isArray(value.subElements)) throw new TypeError(`${property.label} requires a valid LinkSub value.`)
        if (value.subElements.length > 10_000) throw new RangeError(`${property.label} contains too many sub-elements.`)
        const identities = value.subElements.map((entry) => {
          if (typeof entry === 'string') {
            if (!entry.trim()) throw new TypeError(`${property.label} sub-element names must be non-empty.`)
            return entry
          }
          const topoRef = parseTopoRef(JSON.stringify(entry))
          if (topoRef.objectId !== value.objectId) throw new RangeError(`${property.label} sub-elements must target the same object.`)
          return `${topoRef.kind}:${topoRef.persistentId}`
        })
        if (new Set(identities).size !== identities.length) throw new RangeError(`${property.label} sub-elements must be unique.`)
        if (!document.objects.some((object) => object.id === value.objectId)) throw new RangeError(`${property.label} target does not exist in this document.`)
      } else {
        const topoRef = parseTopoRef(JSON.stringify(value))
        if (!document.objects.some((object) => object.id === topoRef.objectId)) throw new RangeError(`${property.label} target does not exist in this document.`)
      }
    }
  }
  if (property.type === 'App::PropertyLinkSubList') {
    if (!value || typeof value !== 'object' || Array.isArray(value) || !('entries' in value) || value.schemaVersion !== 1 || !Array.isArray(value.entries)) throw new TypeError(`${property.label} requires a valid LinkSubList value.`)
    if (value.entries.length > 10_000) throw new RangeError(`${property.label} contains too many links.`)
    const knownIds = new Set(document.objects.map((object) => object.id))
    const identities = value.entries.map((entry, index) => {
      if (!entry || typeof entry !== 'object' || Array.isArray(entry) || typeof entry.objectId !== 'string' || !entry.objectId.trim()) throw new TypeError(`${property.label} entry ${index} requires an object id.`)
      if (!knownIds.has(entry.objectId)) throw new RangeError(`${property.label} entry ${index} target does not exist in this document.`)
      if (entry.subElement === null) return `${entry.objectId}:`
      if (typeof entry.subElement === 'string') {
        if (!entry.subElement.trim()) throw new TypeError(`${property.label} entry ${index} sub-element must be non-empty.`)
        return `${entry.objectId}:${entry.subElement}`
      }
      const topoRef = parseTopoRef(JSON.stringify(entry.subElement))
      if (topoRef.objectId !== entry.objectId) throw new RangeError(`${property.label} entry ${index} TopoRef targets a different object.`)
      return `${entry.objectId}:${topoRef.kind}:${topoRef.persistentId}`
    })
    if (new Set(identities).size !== identities.length) throw new RangeError(`${property.label} entries must be unique.`)
  }
  if (property.name === 'Label' && !(value as string).trim()) throw new RangeError('Label cannot be empty.')
}

function validateMultiTransformValue(value: unknown): asserts value is MultiTransformValue {
  if (!value || typeof value !== 'object' || Array.isArray(value) || !('steps' in value) || !Array.isArray(value.steps)) throw new TypeError('Transformations requires an ordered step list.')
  if (value.steps.length < 1 || value.steps.length > 6) throw new RangeError('Transformations requires between 1 and 6 steps.')
  const ids = new Set<string>()
  let instances = 1
  for (const rawStep of value.steps) {
    if (!rawStep || typeof rawStep !== 'object' || Array.isArray(rawStep)) throw new TypeError('Each transformation step requires a structured value.')
    const step = rawStep as Record<string, unknown>
    if (typeof step.id !== 'string' || !step.id.trim() || ids.has(step.id)) throw new RangeError('Transformation step IDs must be non-empty and unique.')
    ids.add(step.id)
    if (step.type === 'linear') {
      if (!Number.isSafeInteger(step.occurrences) || (step.occurrences as number) < 2 || (step.occurrences as number) > 10) throw new RangeError('Linear occurrences must be an integer between 2 and 10.')
      if (typeof step.length !== 'number' || !Number.isFinite(step.length) || step.length <= 0) throw new RangeError('Linear length must be greater than zero.')
      if (!['Horizontal', 'Vertical', 'Normal'].includes(String(step.direction))) throw new RangeError('Linear direction is invalid.')
      instances *= step.occurrences as number
    } else if (step.type === 'polar') {
      if (!Number.isSafeInteger(step.occurrences) || (step.occurrences as number) < 2 || (step.occurrences as number) > 10) throw new RangeError('Polar occurrences must be an integer between 2 and 10.')
      if (typeof step.angle !== 'number' || !Number.isFinite(step.angle) || step.angle <= 0 || step.angle > 360) throw new RangeError('Polar angle must be greater than zero and no more than 360 degrees.')
      if (!['Horizontal', 'Vertical', 'Normal'].includes(String(step.axis))) throw new RangeError('Polar axis is invalid.')
      instances *= step.occurrences as number
    } else if (step.type === 'mirrored') {
      if (!['XY plane', 'XZ plane', 'YZ plane'].includes(String(step.plane))) throw new RangeError('Mirror plane is invalid.')
      instances *= 2
    } else throw new RangeError('Transformation step type is invalid.')
    if (instances > 100) throw new RangeError('Multi-transform cannot create more than 100 instances.')
  }
}

function validateVectorValue(label: string, value: unknown): asserts value is { x: number; y: number; z: number } {
  if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`${label} requires a Vector value.`)
  const vector = value as Record<string, unknown>
  if (!['x', 'y', 'z'].every((key) => typeof vector[key] === 'number' && Number.isFinite(vector[key]))) throw new TypeError(`${label} components must be finite.`)
}

const expressionVariables = (document: DocumentSnapshot): ReadonlyMap<string, Quantity> => {
  const variables = new Map<string, Quantity>()
  for (const object of document.objects) for (const property of object.properties) {
    if (typeof property.value !== 'number' || !Number.isFinite(property.value)) continue
    const quantity = property.unit && getUnit(property.unit) ? quantityFromUnit(property.value, property.unit) : quantityFromNumber(property.value)
    variables.set(`${object.id}.${property.name}`, quantity)
  }
  return variables
}

const expectedExpressionValue = (property: ObjectPropertySnapshot, expression: string, document: DocumentSnapshot) => {
  const result = evaluateQuantityExpression(expression, expressionVariables(document))
  const expectedDimension = quantityDimensionForUnit(property.unit)
  let quantity = result.value
  if (quantity.dimension === 'dimensionless' && expectedDimension !== 'dimensionless') {
    const unit = property.unit ? getUnit(property.unit) : undefined
    if (!unit) throw new TypeError(`${property.label} requires a registered unit.`)
    quantity = { value: quantity.value * unit.factor, dimension: expectedDimension }
  }
  if (quantity.dimension !== expectedDimension) throw new TypeError(`${property.label} expression has dimension ${quantity.dimension}; expected ${expectedDimension}.`)
  const value = property.unit ? convertQuantity(quantity, property.unit) : quantity.value
  if (!Number.isFinite(value)) throw new RangeError(`${property.label} expression result must be finite.`)
  return { value, references: result.references }
}

const markDocumentTouched = (document: DocumentSnapshot, objectIds: Iterable<string>, includeRoots = true) => {
  const roots = new Set(objectIds)
  const graph = new DependencyGraph(document.dependencies ?? [], document.objects.map((object) => object.id))
  const plan = graph.plan(roots)
  const affected = includeRoots ? plan.affected : plan.affected.filter((objectId) => !roots.has(objectId))
  const recompute = document.recompute ?? createRecomputeSnapshot(document.objects.map((object) => object.id))
  const objectStates = { ...recompute.objectStates }
  for (const objectId of affected) objectStates[objectId] = 'touched'
  const dirtyObjects = Object.entries(objectStates).filter(([, status]) => status === 'touched' || status === 'error' || status === 'upstream-failed').map(([objectId]) => objectId)
  document.recompute = { ...recompute, status: 'idle', objectStates, dirtyObjects, order: [], errors: [] }
  for (const objectId of affected) {
    const item = document.tree.find((candidate) => candidate.id === objectId)
    if (item && item.state !== 'active' && item.state !== 'readonly') item.state = 'dirty'
    const object = document.objects.find((candidate) => candidate.id === objectId)
    const status = object?.properties.find((property) => property.name === 'Status')
    if (status && status.value !== 'Warning') status.value = 'Touched'
  }
  return plan
}

export function createWebCadFacade(options: WebCadFacadeOptions = {}): BitBybitWebCadFacade {
  const runtime = createFacadeRuntimeProfile(options.runtimeMode ?? 'production')
  const projectPersistence = createSqliteProjectPersistence()
  const geometryRuntime = new BitbybitGeometryRuntime()
  const camJob = createCamJob('Job', 'CAM Job', { min: [0, 0, 0], max: [10, 10, 10], mode: 'from-base-bound-box' })
  const featureShapes = new Map<string, ShapeHandle>()
  const autosave = new ProjectAutosaveScheduler((document) => projectPersistence.save(document))
  const initialDocument = cloneDocumentSnapshot(options.initialDocument ?? createBlankDocument())
  const availableObjectIds = new Set(initialDocument.objects.map((object) => object.id))
  const initialSelectedObjectIds = [...new Set(options.initialSelectedObjectIds ?? [])].filter((objectId) => availableObjectIds.has(objectId))
  let documentSequence = 0
  const nextBlankDocument = (label = 'Untitled document') => createBlankDocument(label, `doc-untitled-${++documentSequence}`)
  let state: FacadeState = { apiVersion: '0.1', activeWorkbench: options.initialWorkbench ?? 'Part Design', selectedObjectId: initialSelectedObjectIds[0] ?? '', selectedObjectIds: initialSelectedObjectIds, document: initialDocument, persistence: projectPersistence.capabilities(), task: null, lastNotice: '', diagnostics: [] }
  const recomputeCoordinator = new RecomputeCoordinator(
    createFacadeGeometryRecomputeExecutor(geometryRuntime, featureShapes),
    (documentId) => state.document.id === documentId ? state.document.version : null,
  )
  const releaseFeatureShape = (objectId: string) => {
    const shape = featureShapes.get(objectId)
    featureShapes.delete(objectId)
    if (shape) void geometryRuntime.release(shape)
  }
  const clearFeatureShapes = () => {
    const retained = [...featureShapes.values()]
    featureShapes.clear()
    void Promise.all(retained.map((shape) => geometryRuntime.release(shape)))
  }
  const listeners = new Set<FacadeListener>()
  const undoStack: FacadeState[] = []
  const redoStack: FacadeState[] = []
  const partialMutationWarnings = new Set<string>()
  let requestSequence = 0

  const emit = (event: FacadeEvent) => listeners.forEach((listener) => listener(event))
  const emitState = () => emit({ type: 'state.changed', state: getState() })
  const cloneSelection = (selection: SubshapeSelection | null | undefined): SubshapeSelection | null | undefined => selection ? { objectId: selection.objectId, ref: { ...selection.ref, candidates: selection.ref.candidates ? [...selection.ref.candidates] : undefined } } : selection
  const getState = () => ({ ...state, selectedObjectIds: [...(state.selectedObjectIds ?? (state.selectedObjectId ? [state.selectedObjectId] : []))], selectedSubshape: cloneSelection(state.selectedSubshape), preselectedSubshape: cloneSelection(state.preselectedSubshape), diagnostics: state.diagnostics.map(cloneDiagnostic), document: cloneDocumentSnapshot(state.document), task: state.task ? { ...state.task, draft: Object.fromEntries(Object.entries(state.task.draft).map(([key, value]) => [key, key === 'transformations' ? clonePropertyValue(value as MultiTransformValue) : Array.isArray(value) ? [...value] : value])) } : null })
  const commit = (next: FacadeState) => { undoStack.push(getState()); redoStack.length = 0; state = next; if (next.document.dirty && !next.document.partial) autosave.schedule(next.document); emitState() }
  const notify = (message: string) => { state = { ...state, lastNotice: message }; emit({ type: 'notice', message }); emitState() }
  const setActive = (id: WorkbenchId) => { state = { ...state, activeWorkbench: id }; emitState(); notify(`${id} workbench loaded`) }
  const selectObjects = (objectIds: string[]) => {
    const available = new Set(state.document.objects.map((object) => object.id))
    const selectedObjectIds = [...new Set(objectIds)].filter((objectId) => available.has(objectId))
    state = { ...state, selectedObjectId: selectedObjectIds[0] ?? '', selectedObjectIds, selectedSubshape: null, preselectedSubshape: null }
    emitState()
  }
  const select = (objectId: string) => selectObjects(objectId ? [objectId] : [])
  const resolveSelection = (selection: { objectId: string; kind: SubshapeRef['kind']; persistentId: string }): SubshapeSelection => {
    const object = state.document.objects.find((candidate) => candidate.id === selection.objectId)
    const entry = object?.topology?.entries.find((candidate) => candidate.ref.kind === selection.kind && candidate.ref.persistentId === selection.persistentId)
    if (!object || !entry) throw new RangeError(`Subshape selection is not available: ${selection.objectId}/${selection.kind}/${selection.persistentId}`)
    if (entry.ref.status === 'deleted') throw new RangeError('Deleted subshapes cannot be selected.')
    return { objectId: selection.objectId, ref: { ...entry.ref, candidates: entry.ref.candidates ? [...entry.ref.candidates] : undefined } }
  }
  const selectSubshape = (selection: { objectId: string; kind: SubshapeRef['kind']; persistentId: string }) => {
    const resolved = resolveSelection(selection)
    state = { ...state, selectedObjectId: selection.objectId, selectedObjectIds: [selection.objectId], selectedSubshape: resolved, preselectedSubshape: null }
    emitState()
  }
  const preselectSubshape = (selection: { objectId: string; kind: SubshapeRef['kind']; persistentId: string } | null) => {
    state = { ...state, preselectedSubshape: selection ? resolveSelection(selection) : null }
    emitState()
  }
  const beginTask = (commandId: string, draft: Record<string, unknown> = {}) => { const task: TaskSnapshot = { id: `task-${++requestSequence}`, commandId, title: workbenchDefinitions[state.activeWorkbench].taskTitle, status: 'preview', draft }; state = { ...state, task }; emitState(); return task }
  const nextFeatureId = (label: string) => {
    const base = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
    const existing = state.document.tree.filter((item) => item.id.startsWith(base)).length
    return existing === 0 ? base : `${base}${String(existing).padStart(3, '0')}`
  }
  const appendFeature = (document: DocumentSnapshot, commandId: string, draft: Record<string, unknown> = {}): { document: DocumentSnapshot; objectId: string } => {
    const primitiveType = commandId === 'primitive' && ['Box', 'Cylinder', 'Sphere', 'Ellipsoid', 'Cone', 'Torus', 'Prism', 'Wedge'].includes(String(draft.primitiveType)) ? String(draft.primitiveType) : 'Box'
    const requestedDatumType = draft.datumType === 'Line' || draft.datumType === 'Point' ? draft.datumType : 'Plane'
    const definition = commandId === 'primitive'
      ? { label: primitiveType, detail: primitiveType === 'Box' ? '10 × 10 × 10 mm' : `${primitiveType} primitive` }
      : commandId === 'datum'
        ? { label: `Datum ${requestedDatumType}`, detail: 'Attached datum reference' }
        : featureCommands[commandId]
    if (!definition) return { document, objectId: '' }
    const objectId = nextFeatureId(definition.label)
    const type = commandId === 'create-sketch' ? 'sketch' : commandId === 'create-body' ? 'body' : 'feature'
    const item: ModelTreeItem = { id: objectId, label: definition.label, type, state: type === 'body' ? 'active' : 'valid', detail: definition.detail }
    const tree: ModelTreeItem[] = document.tree.map((entry) => ({ ...entry, children: entry.children ? [...entry.children] : undefined }))
    const isPartObject = partCommands.has(commandId)
    if (type === 'body') {
      for (const body of tree.filter((entry) => entry.type === 'body')) if (body.state === 'active') body.state = 'valid'
      tree.push({ ...item, children: [] })
    } else if (isPartObject) tree.push({ ...item, children: undefined })
    else {
      const body = [...tree].reverse().find((entry) => entry.type === 'body' && entry.state === 'active') ?? tree.find((entry) => entry.type === 'body')
      if (body) body.children = [...(body.children || []), objectId]
      tree.push(item)
    }
    const objectSnapshot = createObjectSnapshot(item)
    objectSnapshot.nativeObjectTag = document.objects.reduce((maximum, object) => Math.max(maximum, object.nativeObjectTag ?? 0), 0) + 1
    if (commandId === 'create-body') {
      const tip = objectSnapshot.properties.find((property) => property.name === 'Tip')
      if (tip) tip.value = null
    }
    const sourceId = typeof draft.source === 'string' && document.objects.some((object) => object.id === draft.source) ? draft.source : ''
    const sourceObject = sourceId ? document.objects.find((object) => object.id === sourceId) : undefined
    const currentSubshape = state.selectedSubshape
    const selectedEdge = currentSubshape && currentSubshape.objectId === sourceObject?.id && currentSubshape.ref.kind === 'edge' ? currentSubshape : null
    const selectedFace = currentSubshape && currentSubshape.objectId === sourceObject?.id && currentSubshape.ref.kind === 'face' ? currentSubshape : null
    const activeBodyItem = [...tree].reverse().find((entry) => entry.type === 'body' && entry.state === 'active') ?? tree.find((entry) => entry.type === 'body')
    if (['datum', 'shape-binder'].includes(commandId) && !activeBodyItem) throw new Error(`${definition.label} requires an active Body.`)
    const bodyTipValue = document.objects.find((object) => object.id === activeBodyItem?.id)?.properties.find((property) => property.name === 'Tip')?.value
    const bodyTip = typeof bodyTipValue === 'string' && document.objects.some((object) => object.id === bodyTipValue && shapeTypeIds.has(object.typeId)) ? bodyTipValue : null
    const sectionDraft = Array.isArray(draft.sections) && draft.sections.every((entry) => typeof entry === 'string') ? [...draft.sections] as string[] : sourceObject?.typeId === 'Sketcher::SketchObject' ? [sourceObject.id] : []
    const pipeProfile = typeof draft.profile === 'string' ? draft.profile : sourceObject?.typeId === 'Sketcher::SketchObject' ? sourceObject.id : null
    const pipeSpine = typeof draft.spine === 'string' && draft.spine.length > 0 ? draft.spine : null
    const selectedEdgeRef: TopoRefValue | null = selectedEdge ? {
      schemaVersion: 1,
      objectId: selectedEdge.objectId,
      kind: 'edge',
      persistentId: selectedEdge.ref.persistentId,
      topologyVersion: selectedEdge.ref.topologyVersion,
      generation: sourceObject?.topology?.generation ?? document.recompute?.generation ?? 0,
      status: selectedEdge.ref.status ?? 'stable',
      signature: selectedEdge.ref.signature,
      candidates: selectedEdge.ref.candidates ? [...selectedEdge.ref.candidates] : undefined,
    } : null
    const selectedFaceRef: TopoRefValue | null = selectedFace ? {
      schemaVersion: 1,
      objectId: selectedFace.objectId,
      kind: 'face',
      persistentId: selectedFace.ref.persistentId,
      topologyVersion: selectedFace.ref.topologyVersion,
      generation: sourceObject?.topology?.generation ?? document.recompute?.generation ?? 0,
      status: selectedFace.ref.status ?? 'stable',
      signature: selectedFace.ref.signature,
      candidates: selectedFace.ref.candidates ? [...selectedFace.ref.candidates] : undefined,
    } : null
    if (commandId === 'datum') {
      const datumType = requestedDatumType
      const typeId = `PartDesign::${datumType}`
      const support = selectedFaceRef ?? selectedEdgeRef ?? (sourceObject ? { schemaVersion: 1 as const, objectId: sourceObject.id, subElements: [] } : null)
      objectSnapshot.typeId = typeId
      objectSnapshot.properties = [
        ...commonProperties({ ...item, label: `Datum ${datumType}` }).map((property) => property.name === 'TypeId' ? { ...property, value: typeId } : property),
        { name: 'Support', label: 'Support', group: 'Attachment', scope: 'data', type: 'App::PropertyLinkSub', value: support, recompute: true },
        { name: 'MapMode', label: 'Map mode', group: 'Attachment', scope: 'data', type: 'App::PropertyEnumeration', value: selectedFaceRef ? 'FlatFace' : selectedEdgeRef ? 'NormalToEdge' : support ? 'ObjectXY' : 'Deactivated', options: [...ATTACHMENT_MAP_MODES], recompute: true },
        { name: 'AttachmentOffset', label: 'Attachment offset', group: 'Attachment', scope: 'data', type: 'App::PropertyPlacement', value: { position: { x: 0, y: 0, z: 0 }, rotation: { axis: { x: 0, y: 0, z: 1 }, angle: 0 } }, recompute: true },
        { name: 'DatumType', label: 'Datum type', group: 'Datum', scope: 'data', type: 'App::PropertyEnumeration', value: datumType, options: ['Plane', 'Line', 'Point'], recompute: true },
        ...viewProperties(),
      ]
    }
    if (commandId === 'shape-binder') {
      const support = selectedFaceRef ?? selectedEdgeRef ?? (sourceObject ? { schemaVersion: 1 as const, objectId: sourceObject.id, subElements: [] } : null)
      if (!support) throw new RangeError('Shape Binder requires a source object or stable sub-shape.')
      objectSnapshot.typeId = 'PartDesign::ShapeBinder'
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: 'PartDesign::ShapeBinder' } : property),
        { name: 'Support', label: 'Support', group: 'Binder', scope: 'data', type: 'App::PropertyLinkSub', value: support, recompute: true },
        { name: 'BindMode', label: 'Bind mode', group: 'Binder', scope: 'data', type: 'App::PropertyEnumeration', value: 'Synchronized', options: ['Synchronized', 'Frozen'], recompute: true },
        { name: 'TraceSupport', label: 'Trace support', group: 'Binder', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
        { name: 'ClaimChildren', label: 'Claim children', group: 'Binder', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        ...viewProperties(),
      ]
    }
    if (commandId === 'extrude-part') {
      objectSnapshot.typeId = 'Part::Extrusion'
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: 'Part::Extrusion' } : property),
        { name: 'Base', label: 'Base', group: 'Extrude', scope: 'data', type: 'App::PropertyLink', value: sourceObject?.typeId === 'Sketcher::SketchObject' ? sourceObject.id : null, recompute: true },
        { name: 'Dir', label: 'Direction', group: 'Extrude', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 1 }, recompute: true },
        { name: 'DirMode', label: 'Direction mode', group: 'Extrude', scope: 'data', type: 'App::PropertyEnumeration', value: 'Custom', options: ['Custom', 'Edge', 'Normal'], recompute: true },
        { name: 'DirLink', label: 'Direction link', group: 'Extrude', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
        { name: 'LengthFwd', label: 'Forward length', group: 'Extrude', scope: 'data', type: 'App::PropertyDistance', value: 10, unit: 'mm', recompute: true },
        { name: 'LengthRev', label: 'Reverse length', group: 'Extrude', scope: 'data', type: 'App::PropertyDistance', value: 0, unit: 'mm', recompute: true },
        { name: 'Solid', label: 'Solid', group: 'Extrude', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
        { name: 'Reversed', label: 'Reversed', group: 'Extrude', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        { name: 'Symmetric', label: 'Symmetric', group: 'Extrude', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        { name: 'TaperAngle', label: 'Taper angle', group: 'Extrude', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
        { name: 'TaperAngleRev', label: 'Reverse taper angle', group: 'Extrude', scope: 'data', type: 'App::PropertyAngle', value: 0, unit: 'deg', recompute: true },
        ...viewProperties(),
      ]
    }
    if (commandId === 'revolution-part') {
      objectSnapshot.typeId = 'Part::Revolution'
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: 'Part::Revolution' } : property),
        { name: 'Source', label: 'Source', group: 'Revolution', scope: 'data', type: 'App::PropertyLink', value: sourceObject?.typeId === 'Sketcher::SketchObject' ? sourceObject.id : null, recompute: true },
        { name: 'Base', label: 'Axis base', group: 'Revolution', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 0 }, recompute: true },
        { name: 'Axis', label: 'Axis', group: 'Revolution', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 1 }, recompute: true },
        { name: 'AxisLink', label: 'Axis link', group: 'Revolution', scope: 'data', type: 'App::PropertyLinkSub', value: null, recompute: true },
        { name: 'Angle', label: 'Angle', group: 'Revolution', scope: 'data', type: 'App::PropertyAngle', value: 360, unit: 'deg', recompute: true },
        { name: 'Symmetric', label: 'Symmetric', group: 'Revolution', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        { name: 'Solid', label: 'Solid', group: 'Revolution', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
        ...viewProperties(),
      ]
    }
    if (commandId === 'fillet-part' || commandId === 'fillet') {
      const typeId = commandId === 'fillet-part' ? 'Part::Fillet' : 'PartDesign::Fillet'
      objectSnapshot.typeId = typeId
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: typeId } : property),
        { name: 'Base', label: 'Base edge', group: 'Fillet', scope: 'data', type: 'App::PropertyLinkSub', value: selectedEdgeRef, recompute: true },
        { name: 'Radius', label: 'Radius', group: 'Fillet', scope: 'data', type: 'App::PropertyLength', value: 1, unit: 'mm', recompute: true },
        { name: 'UseAllEdges', label: 'Use all edges', group: 'Fillet', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        ...viewProperties(),
      ]
    }
    if (commandId === 'chamfer-part' || commandId === 'chamfer') {
      const typeId = commandId === 'chamfer-part' ? 'Part::Chamfer' : 'PartDesign::Chamfer'
      objectSnapshot.typeId = typeId
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: typeId } : property),
        { name: 'Base', label: 'Base edge', group: 'Chamfer', scope: 'data', type: 'App::PropertyLinkSub', value: selectedEdgeRef, recompute: true },
        { name: 'Size', label: 'Size', group: 'Chamfer', scope: 'data', type: 'App::PropertyLength', value: 1, unit: 'mm', recompute: true },
        { name: 'Size2', label: 'Second size', group: 'Chamfer', scope: 'data', type: 'App::PropertyLength', value: 1, unit: 'mm', recompute: true },
        { name: 'Angle', label: 'Angle', group: 'Chamfer', scope: 'data', type: 'App::PropertyAngle', value: 45, unit: 'deg', recompute: true },
        { name: 'ChamferType', label: 'Chamfer type', group: 'Chamfer', scope: 'data', type: 'App::PropertyEnumeration', value: 'Equal distance', options: ['Equal distance', 'Two distances', 'Distance and Angle'], recompute: true },
        { name: 'FlipDirection', label: 'Flip direction', group: 'Chamfer', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        { name: 'UseAllEdges', label: 'Use all edges', group: 'Chamfer', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        ...viewProperties(),
      ]
    }
    if (commandId === 'draft') {
      objectSnapshot.typeId = 'PartDesign::Draft'
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: 'PartDesign::Draft' } : property),
        { name: 'Base', label: 'Base face', group: 'Draft', scope: 'data', type: 'App::PropertyLinkSub', value: selectedFaceRef, recompute: true },
        { name: 'Angle', label: 'Angle', group: 'Draft', scope: 'data', type: 'App::PropertyAngle', value: 5, unit: 'deg', recompute: true },
        { name: 'Direction', label: 'Direction', group: 'Draft', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 1, z: 0 }, recompute: true },
        { name: 'NeutralPlaneOrigin', label: 'Neutral plane origin', group: 'Draft', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 0 }, recompute: true },
        { name: 'NeutralPlaneDirection', label: 'Neutral plane direction', group: 'Draft', scope: 'data', type: 'App::PropertyVector', value: { x: 0, y: 0, z: 1 }, recompute: true },
        { name: 'Reversed', label: 'Reversed', group: 'Draft', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        { name: 'UseAllFaces', label: 'Use all faces', group: 'Draft', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        ...viewProperties(),
      ]
    }
    if (commandId === 'thickness') {
      objectSnapshot.typeId = 'PartDesign::Thickness'
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: 'PartDesign::Thickness' } : property),
        { name: 'Base', label: 'Base solid', group: 'Thickness', scope: 'data', type: 'App::PropertyLink', value: sourceObject && shapeTypeIds.has(sourceObject.typeId) ? sourceObject.id : bodyTip, recompute: true },
        { name: 'Value', label: 'Value', group: 'Thickness', scope: 'data', type: 'App::PropertyLength', value: 1, unit: 'mm', recompute: true },
        { name: 'RemoveFaces', label: 'Remove faces', group: 'Thickness', scope: 'data', type: 'App::PropertyLinkSub', value: selectedFaceRef, recompute: true },
        { name: 'Join', label: 'Join type', group: 'Thickness', scope: 'data', type: 'App::PropertyEnumeration', value: 'Arc', options: ['Arc', 'Intersection'], recompute: true },
        { name: 'Mode', label: 'Mode', group: 'Thickness', scope: 'data', type: 'App::PropertyEnumeration', value: 'Skin', options: ['Skin', 'Pipe', 'Recto verso'], recompute: true },
        { name: 'Reversed', label: 'Reversed', group: 'Thickness', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        ...viewProperties(),
      ]
    }
    if (loftCommands.has(commandId)) {
      const typeId = commandId === 'loft-part' ? 'Part::Loft' : commandId === 'additive-loft' ? 'PartDesign::AdditiveLoft' : 'PartDesign::SubtractiveLoft'
      const partLoft = commandId === 'loft-part'
      objectSnapshot.typeId = typeId
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: typeId } : property),
        ...(!partLoft ? [{ name: 'Profile', label: 'First section', group: 'Loft', scope: 'data' as const, type: 'App::PropertyLink' as const, value: sectionDraft[0] ?? null, recompute: true }] : []),
        { name: 'Sections', label: 'Sections', group: 'Loft', scope: 'data', type: 'App::PropertyLinkList', value: partLoft ? sectionDraft : sectionDraft.slice(1), recompute: true },
        ...(!partLoft ? [{ name: 'Base', label: 'Base feature', group: 'Loft', scope: 'data' as const, type: 'App::PropertyLink' as const, value: bodyTip, recompute: true }] : []),
        { name: 'Ruled', label: 'Ruled surface', group: 'Loft', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        { name: 'Closed', label: 'Closed loft', group: 'Loft', scope: 'data', type: 'App::PropertyBool', value: false, recompute: true },
        { name: 'Solid', label: 'Solid', group: 'Loft', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
        ...(partLoft ? [
          { name: 'MaxDegree', label: 'Maximum degree', group: 'Loft', scope: 'data' as const, type: 'App::PropertyIntegerConstraint' as const, value: 5, recompute: true },
          { name: 'Linearize', label: 'Linearize', group: 'Loft', scope: 'data' as const, type: 'App::PropertyBool' as const, value: false, recompute: true },
        ] : []),
        ...viewProperties(),
      ]
    }
    if (pipeCommands.has(commandId)) {
      const typeId = commandId === 'sweep-part' ? 'Part::Sweep' : commandId === 'additive-pipe' ? 'PartDesign::AdditivePipe' : 'PartDesign::SubtractivePipe'
      const partSweep = commandId === 'sweep-part'
      objectSnapshot.typeId = typeId
      objectSnapshot.properties = [
        ...commonProperties(item).map((property) => property.name === 'TypeId' ? { ...property, value: typeId } : property),
        ...(partSweep
          ? [{ name: 'Sections', label: 'Profile', group: 'Sweep', scope: 'data' as const, type: 'App::PropertyLinkList' as const, value: pipeProfile ? [pipeProfile] : [], recompute: true }]
          : [{ name: 'Profile', label: 'Profile', group: 'Pipe', scope: 'data' as const, type: 'App::PropertyLink' as const, value: pipeProfile, recompute: true }]),
        { name: 'Spine', label: 'Path', group: partSweep ? 'Sweep' : 'Pipe', scope: 'data', type: 'App::PropertyLinkSub', value: pipeSpine ? { schemaVersion: 1, objectId: pipeSpine, subElements: [] } : null, recompute: true },
        ...(!partSweep ? [{ name: 'Base', label: 'Base feature', group: 'Pipe', scope: 'data' as const, type: 'App::PropertyLink' as const, value: bodyTip, recompute: true }] : []),
        { name: 'Transition', label: 'Transition', group: partSweep ? 'Sweep' : 'Pipe', scope: 'data', type: 'App::PropertyEnumeration', value: 'Transformed', options: ['Transformed', 'Right corner', 'Round corner'], recompute: true },
        { name: 'Mode', label: 'Orientation mode', group: partSweep ? 'Sweep' : 'Pipe', scope: 'data', type: 'App::PropertyEnumeration', value: 'Standard', options: ['Standard', 'Fixed', 'Frenet', 'Auxiliary'], recompute: true },
        { name: 'Transformation', label: 'Section transformation', group: partSweep ? 'Sweep' : 'Pipe', scope: 'data', type: 'App::PropertyEnumeration', value: 'Constant', options: ['Constant', 'Multisection', 'Law'], recompute: true },
        { name: 'Solid', label: 'Solid', group: partSweep ? 'Sweep' : 'Pipe', scope: 'data', type: 'App::PropertyBool', value: true, recompute: true },
        ...(partSweep ? [
          { name: 'Frenet', label: 'Frenet', group: 'Sweep', scope: 'data' as const, type: 'App::PropertyBool' as const, value: false, recompute: true },
          { name: 'Linearize', label: 'Linearize', group: 'Sweep', scope: 'data' as const, type: 'App::PropertyBool' as const, value: false, recompute: true },
        ] : []),
        ...viewProperties(),
      ]
    }
    objectSnapshot.properties = objectSnapshot.properties.map((property) => {
      if (!property.recompute) return property
      if (commandId === 'hole' && property.name === 'DepthType' && typeof draft.type === 'string') return { ...property, value: draft.type === 'Through all' ? 'ThroughAll' : 'Dimension' }
      const explicitCandidate = draft[property.name.toLowerCase()] ?? draft[property.name] ?? Object.entries(draft).find(([key]) => key.toLowerCase() === property.name.toLowerCase())?.[1]
      const candidate = explicitCandidate !== undefined ? explicitCandidate
        : commandId === 'mirrored' && property.name === 'PlaneNormal' && typeof draft.plane === 'string' ? draft.plane === 'XZ plane' ? { x: 0, y: 1, z: 0 } : draft.plane === 'YZ plane' ? { x: 1, y: 0, z: 0 } : { x: 0, y: 0, z: 1 }
          : commandId === 'linear-pattern' && property.name === 'DirectionVector' && typeof draft.direction === 'string' ? draft.direction === 'Vertical' ? { x: 0, y: 1, z: 0 } : draft.direction === 'Normal' ? { x: 0, y: 0, z: 1 } : { x: 1, y: 0, z: 0 }
            : commandId === 'polar-pattern' && property.name === 'AxisDirection' && typeof draft.axis === 'string' ? draft.axis === 'Horizontal' ? { x: 1, y: 0, z: 0 } : draft.axis === 'Vertical' ? { x: 0, y: 1, z: 0 } : { x: 0, y: 0, z: 1 }
              : undefined
      if (property.type === 'App::PropertyLink' && typeof candidate === 'string' && candidate.length > 0) return { ...property, value: candidate }
      if (property.type === 'App::PropertyLink' && sourceObject?.typeId === 'Sketcher::SketchObject' && ((commandId === 'extrude-part' && property.name === 'Base') || (commandId === 'revolution-part' && property.name === 'Source'))) return { ...property, value: sourceObject.id }
      if (property.type === 'App::PropertyLink' && sourceObject && property.name === 'Profile' && sourceObject.typeId === 'Sketcher::SketchObject') return { ...property, value: sourceObject.id }
      if (property.type === 'App::PropertyLink' && sourceObject && property.name === 'Base' && shapeTypeIds.has(sourceObject.typeId)) return { ...property, value: sourceObject.id }
      if (property.type === 'App::PropertyLinkSub' && property.name === 'Spine' && typeof candidate === 'string' && candidate.length > 0) return { ...property, value: { schemaVersion: 1, objectId: candidate, subElements: [] } }
      if ((property.type === 'App::PropertyVector' || property.type === 'App::PropertyDirection') && candidate && typeof candidate === 'object') return { ...property, value: clonePropertyValue(candidate as PropertyValue) }
      if (property.type === 'App::PropertyLinkList' && Array.isArray(candidate) && candidate.every((entry) => typeof entry === 'string')) return { ...property, value: loftCommands.has(commandId) && commandId !== 'loft-part' ? candidate.slice(1) : [...candidate] }
      if (property.type === 'App::PropertyBool' && typeof candidate === 'boolean') return { ...property, value: candidate }
      if (property.type === 'App::PropertyMultiTransform' && candidate && typeof candidate === 'object') return { ...property, value: clonePropertyValue(candidate as MultiTransformValue) }
      if ((property.type === 'App::PropertyLength' || property.type === 'App::PropertyDistance' || property.type === 'App::PropertyAngle' || property.type === 'App::PropertyAcceleration' || property.type === 'App::PropertyArea' || property.type === 'App::PropertyForce' || property.type === 'App::PropertyHeatFlux' || property.type === 'App::PropertyQuantityConstraint' || property.type === 'App::PropertyPercent' || property.type === 'App::PropertyFloat' || property.type === 'App::PropertyInteger' || property.type === 'App::PropertyIntegerConstraint') && typeof candidate === 'number' && Number.isFinite(candidate)) return { ...property, value: candidate }
      if (property.type === 'App::PropertyEnumeration' && typeof candidate === 'string' && property.options?.includes(candidate)) return { ...property, value: candidate }
      return property
    })
    if (['union', 'cut', 'intersection'].includes(commandId)) {
      objectSnapshot.properties = objectSnapshot.properties.map((property) => property.name === 'Base' && typeof draft.base !== 'string' && state.selectedObjectId ? { ...property, value: state.selectedObjectId } : property)
    }
    if (loftCommands.has(commandId)) {
      const profile = objectSnapshot.properties.find((property) => property.name === 'Profile')?.value
      const sections = objectSnapshot.properties.find((property) => property.name === 'Sections')?.value
      const ids = [...(typeof profile === 'string' ? [profile] : []), ...(Array.isArray(sections) ? sections : [])]
      if (ids.length < 2) throw new RangeError('Loft requires at least two section sketches.')
      if (new Set(ids).size !== ids.length) throw new RangeError('Loft section sketches must be unique.')
      if (ids.some((id) => document.objects.find((object) => object.id === id)?.typeId !== 'Sketcher::SketchObject')) throw new TypeError('Loft sections must reference Sketcher objects.')
      if (commandId !== 'loft-part' && !bodyTip) throw new RangeError('Part Design Loft requires a valid Body Tip base feature.')
    }
    if (pipeCommands.has(commandId)) {
      const profile = objectSnapshot.properties.find((property) => property.name === 'Profile')?.value ?? (objectSnapshot.properties.find((property) => property.name === 'Sections')?.value as string[] | undefined)?.[0]
      const spine = objectSnapshot.properties.find((property) => property.name === 'Spine')?.value
      const spineId = typeof spine === 'string' ? spine : spine && typeof spine === 'object' && !Array.isArray(spine) && 'objectId' in spine ? spine.objectId : null
      if (typeof profile !== 'string' || document.objects.find((object) => object.id === profile)?.typeId !== 'Sketcher::SketchObject') throw new TypeError('Pipe Profile must reference a Sketcher object.')
      if (typeof spineId !== 'string' || document.objects.find((object) => object.id === spineId)?.typeId !== 'Sketcher::SketchObject') throw new TypeError('Pipe Spine must reference a Sketcher object.')
      if (profile === spineId) throw new RangeError('Pipe Profile and Spine must reference different sketches.')
      if (commandId !== 'sweep-part' && !bodyTip) throw new RangeError('Part Design Pipe requires a valid Body Tip base feature.')
    }
    for (const property of objectSnapshot.properties.filter((candidate) => candidate.recompute && !candidate.readOnly)) validatePropertyValue(document, property, property.value)
    assertPartDesignParameterSet(objectSnapshot)
    if (commandId === 'linear-pattern' && Number(objectSnapshot.properties.find((property) => property.name === 'Length')?.value) <= 0) throw new RangeError('Linear pattern length must be greater than zero.')
    if (commandId === 'polar-pattern' && Number(objectSnapshot.properties.find((property) => property.name === 'Angle')?.value) <= 0) throw new RangeError('Polar pattern angle must be greater than zero.')
    if (commandId === 'hole') {
      if (Number(objectSnapshot.properties.find((property) => property.name === 'Diameter')?.value) <= 0) throw new RangeError('Hole diameter must be greater than zero.')
      if (Number(objectSnapshot.properties.find((property) => property.name === 'Depth')?.value) <= 0) throw new RangeError('Hole depth must be greater than zero.')
    }
    const objects = [...document.objects.map((object) => ({ ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value) })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined })), objectSnapshot]
    const tipObject = type === 'feature' && partDesignCommands.has(commandId) && !['create-sketch', 'datum', 'shape-binder'].includes(commandId) ? objects.find((object) => object.id === activeBodyItem?.id) : undefined
    if (tipObject) {
      const tip = tipObject.properties.find((property) => property.name === 'Tip')
      if (tip) tip.value = objectId
    }
    const nextDocument: DocumentSnapshot = { ...document, version: document.version + 1, dirty: true, tree, objects }
    nextDocument.dependencies = collectDependencyEdges(nextDocument)
    nextDocument.recompute = createRecomputeSnapshot(objects.map((object) => object.id), document.recompute?.generation ?? 0)
    markDocumentTouched(nextDocument, tipObject ? [objectId, tipObject.id] : [objectId])
    return { document: nextDocument, objectId }
  }
  const setProperty = ({ objectId, propertyName, value, expectedDocumentVersion, signal }: SetPropertyInput) => {
    if (signal?.aborted) throw new DOMException('Property mutation cancelled before transaction start.', 'AbortError')
    if (expectedDocumentVersion !== undefined && expectedDocumentVersion !== state.document.version) throw new RangeError(`Stale document version: expected ${expectedDocumentVersion}, current ${state.document.version}.`)
    const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
    if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
    const sourceObject = state.document.objects[objectIndex]
    const propertyIndex = sourceObject.properties.findIndex((property) => property.name === propertyName)
    if (propertyIndex < 0) throw new Error(`Property does not exist: ${objectId}.${propertyName}`)
    const sourceProperty = sourceObject.properties[propertyIndex]
    const propertyValue = sourceProperty.type === 'App::PropertyIntegerSet' ? [...new Set(value as number[])].sort((left, right) => left - right) : value
    validatePropertyValue(state.document, sourceProperty, propertyValue)
    const derivedArea = sourceObject.typeId === 'Measure::MeasureArea' && propertyName === 'Elements'
      ? measureAreaFromElements(state.document, value as LinkSubListValue)
      : undefined
    if (Object.is(sourceProperty.value, value)) return

    const beforeVersion = state.document.version
    const transactionId = `txn-${++requestSequence}`
    const partialDocument = state.document.partial === true
    const partialTrigger = isFreecadPropertyPartialDocumentTrigger(sourceProperty.nativeStatus)
    emit({ type: 'property.before-change', transactionId, documentId: state.document.id, documentVersion: beforeVersion, objectId, propertyName, partialDocument, partialTrigger })

    const document = cloneDocumentSnapshot(state.document)
    const object = document.objects[objectIndex]
    object.properties[propertyIndex] = { ...object.properties[propertyIndex], value: clonePropertyValue(propertyValue), expression: undefined, expressionError: undefined }
    if (derivedArea !== undefined) {
      const areaProperty = object.properties.find((property) => property.name === 'Area' && property.type === 'App::PropertyArea')
      if (!areaProperty || !(areaProperty.readOnly || isFreecadPropertyReadOnly(areaProperty.nativeStatus))) throw new Error('Measure::MeasureArea requires a read-only App::PropertyArea output.')
      areaProperty.value = derivedArea
    }
    const parameterReport = validatePartDesignParameterSet(object.typeId, parameterValuesForObject(object), { requireComplete: false })
    if (!parameterReport.valid) throw new RangeError(`${object.typeId}.${parameterReport.issues[0].propertyName}: ${parameterReport.issues[0].message}`)
    const treeItem = document.tree.find((item) => item.id === objectId)
    if (propertyName === 'Label' && treeItem) treeItem.label = String(value)
    document.dependencies = collectDependencyEdges(document)
    const recomputeEffect = freecadPropertyRecomputeEffect(sourceProperty.nativeStatus)
    if (sourceProperty.recompute && recomputeEffect !== 'none') markDocumentTouched(document, [objectId], recomputeEffect === 'owner')
    document.version += 1
    document.dirty = isFreecadPropertyDocumentModifiedSuppressed(sourceProperty.nativeStatus) ? state.document.dirty : true
    if (signal?.aborted) throw new DOMException('Property mutation cancelled before transaction commit.', 'AbortError')
    commit({ ...state, document })
    emit({ type: 'property.changed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName, partialDocument, partialTrigger })
    emit({ type: 'transaction.committed', transactionId, operation: 'property.set', documentId: document.id, beforeVersion, afterVersion: document.version })
    if (partialDocument && !partialTrigger && !partialMutationWarnings.has(document.id)) {
      partialMutationWarnings.add(document.id)
      const diagnostic: Diagnostic = {
        id: `diag-${++requestSequence}`,
        source: 'command',
        severity: 'warning',
        code: 'PARTIAL_DOCUMENT_MUTATION_NOT_PERSISTED',
        message: `Changes to partial loaded document will not be saved: ${objectId}.${propertyName}`,
        objectId,
        documentId: document.id,
        documentVersion: document.version,
      }
      state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }
      emit({ type: 'diagnostic.added', diagnostic, context: { apiVersion: state.apiVersion, requestId: transactionId, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench } })
    }
    notify(`${sourceProperty.label} updated`)
  }
  const setIncludedFile = async ({ objectId, propertyName, fileName, bytes, mediaType, expectedDocumentVersion, signal }: SetIncludedFileInput) => {
    if (signal?.aborted) throw new DOMException('Included-file mutation cancelled before resource storage.', 'AbortError')
    if (expectedDocumentVersion !== undefined && expectedDocumentVersion !== state.document.version) throw new RangeError(`Stale document version: expected ${expectedDocumentVersion}, current ${state.document.version}.`)
    const object = state.document.objects.find((candidate) => candidate.id === objectId)
    const property = object?.properties.find((candidate) => candidate.name === propertyName)
    if (!object) throw new Error(`Document object does not exist: ${objectId}`)
    if (!property) throw new Error(`Property does not exist: ${objectId}.${propertyName}`)
    if (property.type !== 'App::PropertyFileIncluded') throw new TypeError(`${objectId}.${propertyName} is not App::PropertyFileIncluded.`)
    if (property.readOnly || isFreecadPropertyReadOnly(property.nativeStatus)) throw new Error(`${property.label} is read-only.`)
    if (!(bytes instanceof Uint8Array)) throw new TypeError(`${property.label} requires Uint8Array resource bytes.`)
    const archiveName = uniqueIncludedFileArchiveName(state.document, fileName, { objectId, propertyName })
    const payload = new Uint8Array(bytes)
    const resource = await projectPersistence.resource.put(payload, mediaType || includedFileMediaType(archiveName))
    try {
      const value = validateIncludedFileValue({ schemaVersion: 1, archiveName, resourceHash: resource.hash, byteLength: payload.byteLength, mediaType: resource.mediaType }, property.label)
      setProperty({ objectId, propertyName, value, expectedDocumentVersion, signal })
      return value
    } catch (error) {
      await projectPersistence.resource.release(resource.hash)
      throw error
    }
  }
  const fcstdOptionsWithIncludedFileResources = async (document: DocumentSnapshot, options: FcstdWriteOptions = {}): Promise<FcstdWriteOptions> => {
    const opaqueEntries = Object.fromEntries(Object.entries(options.opaqueEntries ?? {}).map(([path, payload]) => [path, new Uint8Array(payload)]))
    for (const object of document.objects) for (const property of object.properties) {
      if (property.type !== 'App::PropertyFileIncluded' || property.value === null || isFreecadPropertyPersistenceSuppressed(property.nativeStatus)) continue
      const value = validateIncludedFileValue(property.value, `${object.id}.${property.name}`)
      const payload = await projectPersistence.resource.get(value.resourceHash)
      if (!payload) throw new Error(`Project resource is missing for ${object.id}.${property.name}: ${value.resourceHash}`)
      if (payload.byteLength !== value.byteLength) throw new Error(`Project resource byte length changed for ${object.id}.${property.name}.`)
      const existing = opaqueEntries[value.archiveName]
      if (existing && (existing.byteLength !== payload.byteLength || existing.some((byte, index) => byte !== payload[index]))) throw new Error(`FCStd PropertyFileIncluded archive name ${value.archiveName} has conflicting resource bytes.`)
      opaqueEntries[value.archiveName] = new Uint8Array(payload)
    }
    return { ...options, opaqueEntries }
  }
  const addDynamicProperty = ({ objectId, property: inputProperty }: AddDynamicPropertyInput): ObjectPropertySnapshot => {
    const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
    if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(inputProperty.name)) throw new RangeError(`Invalid property name '${inputProperty.name}'.`)
    if (state.document.objects[objectIndex].properties.some((property) => property.name === inputProperty.name)) throw new RangeError(`Property ${objectId}.${inputProperty.name} already exists.`)

    const property: ObjectPropertySnapshot = {
      ...inputProperty,
      value: clonePropertyValue(inputProperty.value),
      options: inputProperty.options ? [...inputProperty.options] : undefined,
      nativeStatus: withFreecadPropertyStatus(inputProperty.nativeStatus, 'PropDynamic'),
    }
    validatePropertyValue(state.document, { ...property, readOnly: false, nativeStatus: undefined }, property.value)
    const beforeVersion = state.document.version
    const transactionId = `txn-${++requestSequence}`
    const document = cloneDocumentSnapshot(state.document)
    document.objects[objectIndex].properties.push(property)
    document.dependencies = collectDependencyEdges(document)
    document.version += 1
    document.dirty = true
    commit({ ...state, document })
    const partialDocument = document.partial === true
    const partialTrigger = isFreecadPropertyPartialDocumentTrigger(property.nativeStatus)
    emit({ type: 'property.added', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName: property.name, partialDocument, partialTrigger })
    emit({ type: 'transaction.committed', transactionId, operation: 'property.add', documentId: document.id, beforeVersion, afterVersion: document.version })
    notify(`${property.label} property added`)
    return { ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined }
  }
  const removeDynamicProperty = ({ objectId, propertyName }: RemoveDynamicPropertyInput): boolean => {
    const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
    if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
    const propertyIndex = state.document.objects[objectIndex].properties.findIndex((property) => property.name === propertyName)
    if (propertyIndex < 0) return false
    const property = state.document.objects[objectIndex].properties[propertyIndex]
    if (!hasFreecadPropertyStatus(property.nativeStatus, 'PropDynamic') || isFreecadPropertyDynamicMutationLocked(property.nativeStatus)) return false

    const beforeVersion = state.document.version
    const transactionId = `txn-${++requestSequence}`
    const document = cloneDocumentSnapshot(state.document)
    document.objects[objectIndex].properties.splice(propertyIndex, 1)
    document.dependencies = collectDependencyEdges(document)
    document.version += 1
    document.dirty = true
    commit({ ...state, document })
    const partialDocument = document.partial === true
    const partialTrigger = isFreecadPropertyPartialDocumentTrigger(property.nativeStatus)
    emit({ type: 'property.removed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName, partialDocument, partialTrigger })
    emit({ type: 'transaction.committed', transactionId, operation: 'property.remove', documentId: document.id, beforeVersion, afterVersion: document.version })
    notify(`${property.label} property removed`)
    return true
  }
  const renameDynamicProperty = ({ objectId, propertyName, newPropertyName }: RenameDynamicPropertyInput): boolean => {
    const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
    if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
    const sourceObject = state.document.objects[objectIndex]
    const propertyIndex = sourceObject.properties.findIndex((property) => property.name === propertyName)
    if (propertyIndex < 0 || !hasFreecadPropertyStatus(sourceObject.properties[propertyIndex].nativeStatus, 'PropDynamic')) throw new Error(`Property container has no dynamic property '${propertyName}'.`)
    if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(newPropertyName)) throw new RangeError(`Invalid property name '${newPropertyName}'.`)
    if (sourceObject.properties.some((property) => property.name === newPropertyName)) throw new RangeError(`Property ${objectId}.${newPropertyName} already exists.`)
    const sourceProperty = sourceObject.properties[propertyIndex]
    if (isFreecadPropertyDynamicMutationLocked(sourceProperty.nativeStatus)) throw new Error(`Property ${propertyName} is locked`)
    if (propertyName === newPropertyName) return true

    const beforeVersion = state.document.version
    const transactionId = `txn-${++requestSequence}`
    const document = cloneDocumentSnapshot(state.document)
    document.objects[objectIndex].properties[propertyIndex].name = newPropertyName
    document.dependencies = collectDependencyEdges(document)
    document.version += 1
    document.dirty = true
    commit({ ...state, document })
    const partialDocument = document.partial === true
    const partialTrigger = isFreecadPropertyPartialDocumentTrigger(sourceProperty.nativeStatus)
    emit({ type: 'property.renamed', transactionId, documentId: document.id, documentVersion: document.version, objectId, propertyName: newPropertyName, previousPropertyName: propertyName, partialDocument, partialTrigger })
    emit({ type: 'transaction.committed', transactionId, operation: 'property.rename', documentId: document.id, beforeVersion, afterVersion: document.version })
    notify(`${sourceProperty.label} property renamed`)
    return true
  }
  const reorderBodyFeature = ({ bodyId, objectId, beforeObjectId }: ReorderBodyFeatureInput) => {
    const document = cloneDocumentSnapshot(state.document)
    const body = document.tree.find((item) => item.id === bodyId && item.type === 'body')
    if (!body || !body.children) throw new Error(`Body does not exist or has no feature order: ${bodyId}`)
    if (!document.objects.some((object) => object.id === objectId)) throw new Error(`Document object does not exist: ${objectId}`)
    if (!body.children.includes(objectId)) throw new Error(`Object ${objectId} is not a child of Body ${bodyId}.`)
    if (beforeObjectId !== undefined && beforeObjectId !== null && (!body.children.includes(beforeObjectId) || beforeObjectId === objectId)) throw new Error(`Invalid Body reorder target: ${beforeObjectId}`)
    const nextChildren = body.children.filter((child) => child !== objectId)
    const insertionIndex = beforeObjectId === undefined || beforeObjectId === null ? nextChildren.length : nextChildren.indexOf(beforeObjectId)
    nextChildren.splice(insertionIndex, 0, objectId)
    for (const edge of document.dependencies ?? []) {
      const dependentIndex = nextChildren.indexOf(edge.sourceId)
      const targetIndex = nextChildren.indexOf(edge.targetId)
      if (dependentIndex >= 0 && targetIndex >= 0 && targetIndex > dependentIndex) throw new Error(`Body reorder would place dependency ${edge.targetId} after dependent ${edge.sourceId}.`)
    }
    if (nextChildren.every((child, index) => child === body.children?.[index])) return getState().document
    body.children = nextChildren
    document.dependencies = collectDependencyEdges(document)
    const objectStates = { ...(document.recompute?.objectStates ?? {}) }
    redirectBodyTips(document, objectStates)
    markDocumentTouched(document, [bodyId, objectId])
    document.version += 1
    document.dirty = true
    commit({ ...state, document })
    notify(`Body feature ${objectId} reordered`)
    return getState().document
  }
  const removeObject = ({ objectId, cascade = false }: RemoveObjectInput) => {
    const source = state.document.objects.find((object) => object.id === objectId)
    if (!source) throw new Error(`Document object does not exist: ${objectId}`)
    if (source.typeId === 'PartDesign::Body' || source.typeId === 'App::DocumentObjectGroup') throw new Error(`Structural object cannot be removed: ${objectId}`)
    const dependents = new Map<string, string[]>()
    for (const edge of state.document.dependencies ?? []) {
      const sourceObject = state.document.objects.find((object) => object.id === edge.sourceId)
      if (sourceObject?.typeId === 'PartDesign::Body' && edge.propertyName === 'Tip') continue
      dependents.set(edge.targetId, [...(dependents.get(edge.targetId) ?? []), edge.sourceId])
    }
    const removed = new Set<string>([objectId])
    const pending = [objectId]
    while (pending.length) {
      const target = pending.shift() as string
      for (const dependent of dependents.get(target) ?? []) {
        if (!cascade) throw new Error(`Object ${objectId} has dependent ${dependent}; use cascade to remove the dependency closure.`)
        if (!removed.has(dependent)) { removed.add(dependent); pending.push(dependent) }
      }
    }
    const document = cloneDocumentSnapshot(state.document)
    for (const removedId of removed) releaseFeatureShape(removedId)
    document.tree = document.tree.filter((item) => !removed.has(item.id)).map((item) => ({ ...item, children: item.children?.filter((child) => !removed.has(child)) }))
    document.objects = document.objects.filter((object) => !removed.has(object.id))
    document.dependencies = collectDependencyEdges(document)
    const objectStates = { ...(document.recompute?.objectStates ?? {}) }
    for (const removedId of removed) delete objectStates[removedId]
    const bodyIds = document.tree.filter((item) => item.type === 'body').map((item) => item.id)
    redirectBodyTips(document, objectStates)
    markDocumentTouched(document, bodyIds)
    document.version += 1
    document.dirty = true
    const selectedObjectIds = (state.selectedObjectIds ?? (state.selectedObjectId ? [state.selectedObjectId] : [])).filter((objectId) => !removed.has(objectId))
    commit({ ...state, document, selectedObjectId: selectedObjectIds[0] ?? '', selectedObjectIds })
    notify(`Removed ${[...removed].join(', ')}`)
    return [...removed]
  }
  const resolveTopologyReference = (input: ResolveTopologyReferenceInput) => {
    const document = cloneDocumentSnapshot(state.document)
    const resolved = resolveDocumentTopologyReference(document, input)
    document.dependencies = collectDependencyEdges(document)
    markDocumentTouched(document, [input.ownerObjectId])
    document.version += 1
    document.dirty = true
    const diagnostics = state.diagnostics.filter((diagnostic) => diagnostic.topologyRepair?.ownerObjectId !== input.ownerObjectId || diagnostic.topologyRepair.referenceName !== input.referenceName)
    commit({ ...state, document, diagnostics })
    notify(`${input.referenceName} topology reference replaced`)
    return { ...resolved, candidates: resolved.candidates ? [...resolved.candidates] : undefined }
  }
  const setExpression = ({ objectId, propertyName, expression }: SetExpressionInput) => {
    const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
    if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
    const sourceObject = state.document.objects[objectIndex]
    const propertyIndex = sourceObject.properties.findIndex((property) => property.name === propertyName)
    if (propertyIndex < 0) throw new Error(`Property does not exist: ${objectId}.${propertyName}`)
    const sourceProperty = sourceObject.properties[propertyIndex]
    if (sourceProperty.readOnly || isFreecadPropertyReadOnly(sourceProperty.nativeStatus)) throw new Error(`${sourceProperty.label} is read-only.`)
    if (!['App::PropertyLength', 'App::PropertyAngle', 'App::PropertyAcceleration', 'App::PropertyForce', 'App::PropertyFloat', 'App::PropertyPercent'].includes(sourceProperty.type)) throw new TypeError(`${sourceProperty.label} does not accept expressions.`)
    const evaluated = expectedExpressionValue(sourceProperty, expression, state.document)
    const references = expressionReferences(expression)
    const variables = expressionVariables(state.document)
    for (const reference of references) if (!variables.has(reference)) throw new ReferenceError(`Unknown expression reference: ${reference}.`)
    const document = cloneDocumentSnapshot(state.document)
    const property = document.objects[objectIndex].properties[propertyIndex]
    property.value = evaluated.value
    property.expression = expression.trim()
    property.expressionError = undefined
    document.dependencies = collectDependencyEdges(document)
    const recomputeEffect = freecadPropertyRecomputeEffect(sourceProperty.nativeStatus)
    if (sourceProperty.recompute && recomputeEffect !== 'none') markDocumentTouched(document, [objectId], recomputeEffect === 'owner')
    document.version += 1
    document.dirty = isFreecadPropertyDocumentModifiedSuppressed(sourceProperty.nativeStatus) ? state.document.dirty : true
    commit({ ...state, document })
    notify(`${sourceProperty.label} expression updated`)
  }
  const recomputeDocument = (): RecomputeResult => {
    if (state.document.partial) {
      notify(`Partial document ${state.document.label} must be fully loaded before recomputation`)
      return { affected: [], order: [], levels: [], cycles: [], generation: state.document.recompute?.generation ?? 0, status: 'completed', errors: [] }
    }
    const document = cloneDocumentSnapshot(state.document)
    const graph = new DependencyGraph(document.dependencies ?? [], document.objects.map((object) => object.id))
    const recompute = document.recompute ?? createRecomputeSnapshot(document.objects.map((object) => object.id))
    const dirtyIds = recompute.dirtyObjects.length > 0 ? recompute.dirtyObjects : document.objects.filter((object) => object.properties.some((property) => property.recompute && property.expressionError)).map((object) => object.id)
    const plan = graph.plan(dirtyIds)
    const generation = recompute.generation + 1
    const errors: RecomputeResult['errors'] = []
    const objectStates = { ...recompute.objectStates }
    for (const objectId of plan.affected) objectStates[objectId] = 'recomputing'
    for (const cycle of plan.cycles) {
      const message = `Dependency cycle: ${cycle.join(' -> ')}`
      for (const objectId of cycle) { objectStates[objectId] = 'error'; errors.push({ objectId, code: 'DEPENDENCY_CYCLE', message }) }
    }
    for (const objectId of plan.order) {
      const dependencies = graph.dependenciesOf(objectId)
      if (dependencies.some((dependencyId) => objectStates[dependencyId] === 'error' || objectStates[dependencyId] === 'upstream-failed')) {
        objectStates[objectId] = 'upstream-failed'
      } else if (dependencies.some((dependencyId) => objectStates[dependencyId] === 'suppressed' || objectStates[dependencyId] === 'upstream-suppressed')) {
        objectStates[objectId] = 'upstream-suppressed'
      } else {
        const object = document.objects.find((candidate) => candidate.id === objectId)
        objectStates[objectId] = object?.properties.some((property) => property.name === 'Suppressed' && property.value === true) ? 'suppressed' : 'up-to-date'
      }
      if (objectStates[objectId] === 'suppressed' || objectStates[objectId] === 'upstream-suppressed') releaseFeatureShape(objectId)
      const item = document.tree.find((candidate) => candidate.id === objectId)
      if (item && item.state !== 'readonly') item.state = objectStates[objectId] === 'upstream-failed' ? 'warning' : item.type === 'body' ? 'active' : 'valid'
      const status = document.objects.find((candidate) => candidate.id === objectId)?.properties.find((property) => property.name === 'Status')
      if (status) status.value = objectStates[objectId] === 'suppressed' ? 'Suppressed' : objectStates[objectId] === 'upstream-suppressed' ? 'Upstream suppressed' : objectStates[objectId] === 'up-to-date' ? 'Valid' : 'Warning'
    }
    redirectBodyTips(document, objectStates)
    const nextRecompute = { generation, status: errors.length === 0 ? 'completed' as const : 'failed' as const, objectStates, dirtyObjects: errors.length === 0 ? [] : plan.affected, order: plan.order, errors }
    document.recompute = nextRecompute
    const nextDiagnostics = buildRecomputeDiagnostics({ document, generation, affected: plan.affected, objectStates, errors })
    const diagnostics = replaceRecomputeDiagnostics(state.diagnostics, document.id, plan.affected, nextDiagnostics)
    state = { ...state, document, diagnostics }
    const context: FacadeRequestContext = { apiVersion: state.apiVersion, requestId: `recompute-${generation}`, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench }
    for (const diagnostic of nextDiagnostics) emit({ type: 'diagnostic.added', diagnostic, context })
    if (document.dirty && !document.partial) autosave.schedule(document)
    emitState()
    return { ...plan, generation, status: nextRecompute.status, errors }
  }
  const recomputeDocumentAsync = async (options: RecomputeExecutionOptions = {}) => {
    const source = cloneDocumentSnapshot(state.document)
    if (source.partial) {
      notify(`Partial document ${source.label} must be fully loaded before recomputation`)
      const recompute = source.recompute ?? createRecomputeSnapshot(source.objects.map((object) => object.id))
      return {
        generation: recompute.generation,
        documentVersion: source.version,
        status: 'completed' as const,
        affected: [], order: [], levels: [], completed: [], suppressed: [], failed: [], skipped: [],
        dirtyObjects: [...recompute.dirtyObjects], objectStates: { ...recompute.objectStates }, objectUpdates: [], errors: [],
      }
    }
    const result = await recomputeCoordinator.run(source, options)
    if ((result.status !== 'completed' && result.status !== 'failed') || state.document.id !== source.id || state.document.version !== source.version) return result

    const document = cloneDocumentSnapshot(state.document)
    const updates = new Map(result.objectUpdates.map((object) => [object.id, object]))
    document.objects = document.objects.map((object) => updates.has(object.id) ? updates.get(object.id) as DocumentObjectSnapshot : object)
    const topologyMigration = migrateDocumentTopologyReferences(document, result.objectUpdates.map((object) => object.id))
    document.dependencies = collectDependencyEdges(document)
    for (const objectId of result.affected) {
      if (result.objectStates[objectId] === 'suppressed' || result.objectStates[objectId] === 'upstream-suppressed') releaseFeatureShape(objectId)
      const item = document.tree.find((candidate) => candidate.id === objectId)
      if (!item || item.state === 'readonly') continue
      if (result.objectStates[objectId] === 'up-to-date' || result.objectStates[objectId] === 'suppressed' || result.objectStates[objectId] === 'upstream-suppressed') item.state = item.type === 'body' ? 'active' : 'valid'
      else if (result.objectStates[objectId] === 'error' || result.objectStates[objectId] === 'upstream-failed') item.state = 'warning'
      const status = document.objects.find((candidate) => candidate.id === objectId)?.properties.find((property) => property.name === 'Status')
      if (status) status.value = result.objectStates[objectId] === 'suppressed' ? 'Suppressed' : result.objectStates[objectId] === 'upstream-suppressed' ? 'Upstream suppressed' : result.objectStates[objectId] === 'up-to-date' ? 'Valid' : 'Warning'
    }
    redirectBodyTips(document, result.objectStates)
    document.recompute = {
      generation: result.generation,
      status: result.status,
      objectStates: result.objectStates,
      dirtyObjects: [...new Set([...(source.recompute?.dirtyObjects ?? []).filter((objectId) => !result.affected.includes(objectId)), ...result.dirtyObjects])],
      order: result.order,
      errors: result.errors,
    }
    for (const objectId of topologyMigration.changedOwnerIds) {
      document.recompute.objectStates[objectId] = 'touched'
      if (!document.recompute.dirtyObjects.includes(objectId)) document.recompute.dirtyObjects.push(objectId)
      const item = document.tree.find((candidate) => candidate.id === objectId)
      if (item && item.state !== 'readonly') item.state = topologyMigration.issues.some((issue) => issue.ownerObjectId === objectId) ? 'warning' : 'dirty'
      const status = document.objects.find((candidate) => candidate.id === objectId)?.properties.find((property) => property.name === 'Status')
      if (status) status.value = topologyMigration.issues.some((issue) => issue.ownerObjectId === objectId) ? 'Topology reference requires repair' : 'Touched'
    }
    const nextDiagnostics = buildRecomputeDiagnostics({ document, generation: result.generation, affected: result.affected, objectStates: result.objectStates, errors: result.errors })
    const topologyDiagnostics: Diagnostic[] = topologyMigration.issues.map((issue, index) => {
      const sourceTopology = document.objects.find((object) => object.id === issue.sourceObjectId)?.topology
      const candidates = issue.candidates?.length
        ? issue.candidates
        : sourceTopology?.entries.filter((entry) => entry.ref.kind === issue.kind && entry.ref.status !== 'deleted').map((entry) => entry.ref.persistentId) ?? []
      return {
      id: `topology:${document.id}:${result.generation}:${issue.ownerObjectId}:${issue.referenceName}:${index}`,
      source: 'geometry',
      severity: issue.status === 'deleted' ? 'error' : 'warning',
      code: issue.status === 'deleted' ? 'TOPOLOGY_REFERENCE_DELETED' : 'TOPOLOGY_REFERENCE_AMBIGUOUS',
      message: issue.status === 'deleted'
        ? `${issue.referenceName} no longer resolves on ${issue.sourceObjectId}; select a replacement subshape.`
        : `${issue.referenceName} resolves to multiple subshapes on ${issue.sourceObjectId}: ${(issue.candidates ?? []).join(', ')}.`,
      objectId: issue.ownerObjectId,
      documentId: document.id,
      documentVersion: document.version,
      generation: result.generation,
      rootCauseObjectId: issue.sourceObjectId,
      dependencyPath: [issue.ownerObjectId, issue.sourceObjectId],
      topologyRepair: candidates.length > 0 ? { ownerObjectId: issue.ownerObjectId, referenceName: issue.referenceName, candidates: [...new Set(candidates)] } : undefined,
      repairActions: [
        { id: 'select-object', label: 'Select reference owner', targetObjectId: issue.ownerObjectId, enabled: true },
        { id: 'recompute-root', label: 'Recompute after replacing reference', targetObjectId: issue.ownerObjectId, enabled: false, reason: 'Select one current subshape and replace the ambiguous or deleted reference first.' },
      ],
    }})
    const replaced = replaceRecomputeDiagnostics(state.diagnostics, document.id, result.affected, nextDiagnostics)
    const topologyOwners = new Set(topologyMigration.issues.map((issue) => issue.ownerObjectId))
    const diagnostics = [...replaced.filter((diagnostic) => !diagnostic.code.startsWith('TOPOLOGY_REFERENCE_') || diagnostic.documentId !== document.id || !diagnostic.objectId || !topologyOwners.has(diagnostic.objectId)), ...topologyDiagnostics]
    state = { ...state, document, diagnostics }
    const context: FacadeRequestContext = { apiVersion: state.apiVersion, requestId: `recompute-${result.generation}`, documentId: document.id, documentVersion: document.version, workbench: state.activeWorkbench }
    for (const diagnostic of nextDiagnostics) emit({ type: 'diagnostic.added', diagnostic, context })
    if (document.dirty && !document.partial) autosave.schedule(document)
    emitState()
    return result
  }
  const repairDiagnostic = async (diagnosticId: string, actionId: DiagnosticRepairAction['id']): Promise<DiagnosticRepairResult> => {
    const diagnostic = state.diagnostics.find((candidate) => candidate.id === diagnosticId && !candidate.resolved)
    const action = diagnostic?.repairActions?.find((candidate) => candidate.id === actionId)
    if (!diagnostic || !action || !action.enabled) return { diagnosticId, actionId, status: 'unavailable', message: action?.reason || 'The diagnostic or repair action is no longer available.' }
    if (!state.document.objects.some((object) => object.id === action.targetObjectId)) return { diagnosticId, actionId, status: 'unavailable', message: 'The repair target no longer exists in the active document.' }
    if (actionId === 'select-object') {
      select(action.targetObjectId)
      emit({ type: 'diagnostic.focused', diagnosticId, objectId: action.targetObjectId })
      notify(`Selected diagnostic root ${action.targetObjectId}`)
      return { diagnosticId, actionId, status: 'completed', message: 'Root object selected.' }
    }
    if (actionId === 'suppress-root') setProperty({ objectId: action.targetObjectId, propertyName: 'Suppressed', value: true })
    const result = await recomputeDocumentAsync({ dirtyObjectIds: [...new Set([...(state.document.recompute?.dirtyObjects ?? []), action.targetObjectId])] })
    const completed = result.status === 'completed'
    notify(completed ? 'Diagnostic branch repaired' : 'Diagnostic repair did not complete')
    return { diagnosticId, actionId, status: completed ? 'completed' : 'failed', message: completed ? 'The affected dependency branch recomputed successfully.' : `Recompute finished with status ${result.status}.` }
  }
  const loadDocument = async (documentId: string) => {
    recomputeCoordinator.cancel()
    const loaded = await projectPersistence.load(documentId)
    if (!loaded) return null
    clearFeatureShapes()
    const document = cloneDocumentSnapshot(loaded)
    commit({ ...state, document, selectedObjectId: '', selectedObjectIds: [], task: null })
    await recomputeDocumentAsync({ dirtyObjectIds: document.objects.map((object) => object.id) })
    notify(`Loaded ${document.label}`)
    return getState().document
  }
  const getSketch = (objectId: string) => {
    const object = state.document.objects.find((candidate) => candidate.id === objectId)
    return object?.sketch ? cloneSketch(object.sketch) : null
  }
  const updateSketch = (objectId: string, update: (sketch: SketchSnapshot) => void) => {
    const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
    if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
    const source = state.document.objects[objectIndex]
    if (!source.sketch) throw new TypeError(`${objectId} is not a Sketcher object.`)
    const document = cloneDocumentSnapshot(state.document)
    const sketch = document.objects[objectIndex].sketch as SketchSnapshot
    update(sketch)
    const solved = solveSketch(sketch)
    document.objects[objectIndex].sketch = solved.snapshot
    const status = document.objects[objectIndex].properties.find((property) => property.name === 'ConstraintStatus')
    if (status) status.value = solved.status === 'solved' ? 'Fully constrained' : solved.status === 'under-constrained' ? `Under-constrained (${solved.degreesOfFreedom} DOF)` : solved.status === 'conflicting' ? 'Conflicting constraints' : 'Invalid constraints'
    document.dependencies = collectDependencyEdges(document)
    markDocumentTouched(document, [objectId])
    document.version += 1
    document.dirty = true
    commit({ ...state, document })
    return cloneSketch(solved.snapshot)
  }
  const addSketchGeometry = (objectId: string, geometry: SketchGeometry) => updateSketch(objectId, (sketch) => {
    if (sketch.geometry.some((candidate) => candidate.id === geometry.id)) throw new RangeError(`Sketch geometry already exists: ${geometry.id}`)
    sketch.geometry.push(cloneSketchGeometry(geometry))
  })
  const projectSketch = (objectId: string, sourceGeometryId: string, projectedId?: string) => updateSketch(objectId, (sketch) => {
    const source = sketch.geometry.find((candidate) => candidate.id === sourceGeometryId)
    if (!source) throw new RangeError(`Sketch geometry does not exist: ${sourceGeometryId}`)
    const projection = projectSketchGeometry(source, projectedId)
    if (sketch.geometry.some((candidate) => candidate.id === projection.id)) throw new RangeError(`Sketch geometry already exists: ${projection.id}`)
    sketch.geometry.push(projection)
  })
  const carbonCopySketch = (objectId: string, sourceGeometryIds: string[], idPrefix = 'carboncopy') => updateSketch(objectId, (sketch) => {
    if (!Array.isArray(sourceGeometryIds) || sourceGeometryIds.length === 0) throw new RangeError('CarbonCopy requires at least one source geometry.')
    const sources = sourceGeometryIds.map((sourceId) => {
      const source = sketch.geometry.find((candidate) => candidate.id === sourceId)
      if (!source) throw new RangeError(`Sketch geometry does not exist: ${sourceId}`)
      return carbonCopySketchGeometry(source, idPrefix)
    })
    const ids = new Set(sketch.geometry.map((candidate) => candidate.id))
    const generatedIds = new Set<string>()
    for (const source of sources) {
      if (ids.has(source.id) || generatedIds.has(source.id)) throw new RangeError('CarbonCopy would create a duplicate geometry ID.')
      generatedIds.add(source.id)
    }
    sketch.geometry.push(...sources)
  })
  const addSketchExternalGeometry = (objectId: string, external: SketchExternalGeometry) => updateSketch(objectId, (sketch) => {
    if (!external.id.trim()) throw new RangeError('External geometry id is required.')
    if (sketch.externalGeometry.some((candidate) => candidate.id === external.id)) throw new RangeError(`External geometry already exists: ${external.id}`)
    if (external.source.objectId === objectId) throw new RangeError('A sketch cannot import external geometry from itself.')
    if (external.source.status === 'deleted') throw new RangeError('Deleted topology cannot be imported as external geometry.')
    sketch.externalGeometry.push({ ...external, source: { ...external.source, candidates: external.source.candidates ? [...external.source.candidates] : undefined }, projection: cloneSketchGeometry(external.projection), construction: true })
  })
  const addSketchConstraint = (objectId: string, constraint: SketchConstraint) => updateSketch(objectId, (sketch) => {
    if (sketch.constraints.some((candidate) => candidate.id === constraint.id)) throw new RangeError(`Sketch constraint already exists: ${constraint.id}`)
    sketch.constraints.push(cloneSketchConstraint(constraint))
  })
  const solveSketchObject = (objectId: string) => {
    const objectIndex = state.document.objects.findIndex((object) => object.id === objectId)
    if (objectIndex < 0) throw new Error(`Document object does not exist: ${objectId}`)
    const source = state.document.objects[objectIndex]
    if (!source.sketch) throw new TypeError(`${objectId} is not a Sketcher object.`)
    const result = solveSketch(source.sketch)
    const document = cloneDocumentSnapshot(state.document)
    document.objects[objectIndex].sketch = result.snapshot
    const status = document.objects[objectIndex].properties.find((property) => property.name === 'ConstraintStatus')
    if (status) status.value = result.status === 'solved' ? 'Fully constrained' : result.status === 'under-constrained' ? `Under-constrained (${result.degreesOfFreedom} DOF)` : result.status === 'conflicting' ? 'Conflicting constraints' : 'Invalid constraints'
    markDocumentTouched(document, [objectId])
    document.version += 1
    document.dirty = true
    commit({ ...state, document })
    return result
  }
  const applyTask = () => {
    const task = state.task
    if (!task || task.status !== 'preview') return
    const result = appendFeature(state.document, task.commandId, task.draft)
    if (!result.objectId) {
      state = { ...state, task: { ...task, status: 'completed' } }
      emitState()
      return
    }
    commit({ ...state, document: result.document, selectedObjectId: result.objectId, selectedObjectIds: [result.objectId], task: { ...task, status: 'completed' } })
    notify(`${result.document.tree.find((item) => item.id === result.objectId)?.label || featureCommands[task.commandId]?.label || task.commandId} created`)
  }
  const execute = ({ commandId, payload }: ExecuteCommandInput) => {
    const requestId = `req-${++requestSequence}`
    const context: FacadeRequestContext = { apiVersion: state.apiVersion, requestId, documentId: state.document.id, documentVersion: state.document.version, workbench: state.activeWorkbench }
    const selectedObject = state.document.objects.find((object) => object.id === state.selectedObjectId)
    const status = commandState(commandId, state.activeWorkbench, state.selectedObjectId, selectedObject, state.selectedSubshape)
    if (status.status === 'disabled') {
      const code = status.reason?.startsWith('Command is visible in the FreeCAD-compatible manifest') ? 'COMMAND_UNIMPLEMENTED' : 'COMMAND_DISABLED'
      const diagnostic: Diagnostic = { id: `diag-${++requestSequence}`, source: 'command', severity: 'warning', code, message: status.reason || 'Command is disabled', objectId: state.selectedObjectId || undefined, requestId, documentId: state.document.id, documentVersion: state.document.version }
      state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }
      emit({ type: 'diagnostic.added', diagnostic, context }); emit({ type: 'command.failed', commandId, context, message: status.reason }); notify(status.reason || 'Command is disabled'); return requestId
    }
    emit({ type: 'command.started', commandId, context })
    if (commandId === 'new-document') { clearFeatureShapes(); commit({ ...state, document: nextBlankDocument(), selectedObjectId: '', selectedObjectIds: [] }) }
    else if (commandId === 'save') {
      const savedDocument = { ...state.document, dirty: false, version: state.document.version + 1 }
      commit({ ...state, document: savedDocument })
      void projectPersistence.save(savedDocument).then(() => notify('Saved to local workspace')).catch((error: unknown) => {
        const diagnostic: Diagnostic = { id: `diag-${++requestSequence}`, source: 'persistence', severity: 'error', code: 'PERSISTENCE_SAVE_FAILED', message: error instanceof Error ? error.message : String(error), requestId, documentId: state.document.id, documentVersion: state.document.version }
        state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }
        emit({ type: 'diagnostic.added', diagnostic, context }); notify('Save failed; export a recovery package')
      })
    }
    else if (commandId === 'select-object' && typeof payload?.objectId === 'string') select(payload.objectId)
    else if (commandId === 'check-shape') {
      const objectId = state.selectedObjectId
      const shape = featureShapes.get(objectId)
      const reportFailure = (code: string, message: string) => {
        const diagnostic: Diagnostic = { id: `diag-${++requestSequence}`, source: 'geometry', severity: 'warning', code, message, objectId, requestId, documentId: state.document.id, documentVersion: state.document.version }
        state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }
        emit({ type: 'diagnostic.added', diagnostic, context })
        notify(message)
      }
      if (!shape) reportFailure('SHAPE_NOT_RECOMPUTED', `No valid Shape is cached for ${objectId}; recompute the document first.`)
      else void geometryRuntime.topology(shape, 0.05).then((topology) => {
        const count = topology.faces.length + topology.edges.length + topology.vertices.length
        if (count === 0) throw new Error('Shape topology is empty.')
        const message = `Shape check passed: ${topology.faces.length} faces, ${topology.edges.length} edges, ${topology.vertices.length} vertices`
        const diagnostic: Diagnostic = { id: `diag-${++requestSequence}`, source: 'geometry', severity: 'info', code: 'SHAPE_CHECK_PASSED', message, objectId, requestId, documentId: state.document.id, documentVersion: state.document.version }
        state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }
        emit({ type: 'diagnostic.added', diagnostic, context })
        notify(message)
      }).catch((error: unknown) => reportFailure('SHAPE_CHECK_FAILED', error instanceof Error ? error.message : String(error)))
    }
    else if (commandId === 'solve-sketch') { solveSketchObject(state.selectedObjectId); notify('Sketch solver completed') }
    else if (commandId === 'new-sketch') beginTask('create-sketch', { source: state.selectedObjectId || null })
    else if (camCommandIds.has(commandId)) {
      const latestOperation = camJob.snapshot().operations.at(-1)
      if (camSimulationCommands.has(commandId)) {
        const result = camJob.simulate()
        notify(result.status === 'pass' ? `CAM simulation passed: ${result.activeOperations} operations, ${result.pathPoints} path points` : `CAM simulation found ${result.collisions.length} collision operations`)
      } else if (commandId === 'CAM_Camotics') {
        notify('CAMotics source stage is ready; generated G-code is handed to LinuxCNC WASM for parsing and machine execution')
      } else if (commandId === 'CAM_Sanity') {
        const result = camJob.sanityCheck()
        notify(result.status === 'pass' ? 'CAM Sanity Check passed' : `CAM Sanity Check: ${result.issues.length} issues`)
      } else if (commandId === 'CAM_Inspect') {
        notify(latestOperation ? `${latestOperation.label}: ${latestOperation.path.length} path points, ${latestOperation.status}` : 'CAM Job has no operation to inspect')
      } else if (commandId === 'CAM_SelectLoop') notify('Select Loop is available from the model sub-element context in CAM')
      else if (commandId === 'CAM_OpActiveToggle') {
        if (latestOperation) { const result = camJob.toggleOperation(latestOperation.id); notify(`${result.label} ${result.active ? 'enabled' : 'disabled'}`) } else notify('CAM Job has no operation to toggle')
      } else if (camCopyCommands.has(commandId)) {
        if (latestOperation) { const copy = camJob.copyOperation(latestOperation.id, `${latestOperation.id}-copy-${camJob.snapshot().operations.length + 1}`); notify(`${copy.label} created`) } else notify('CAM Job has no operation to copy')
      } else if (commandId === 'CAM_SetStartPoint') {
        if (latestOperation?.path.length) { const result = camJob.setStartPoint(latestOperation.id, Math.min(1, latestOperation.path.length - 1)); notify(`${result.label} start point updated`) } else notify('Generate a CAM path before setting its start point')
      } else if (commandId === 'CAM_Stop') {
        if (state.task?.commandId.startsWith('CAM_')) { state = { ...state, task: { ...state.task, status: 'cancelled' } }; notify('CAM processing stopped') } else notify('No CAM process is running')
      } else beginTask(commandId, { source: state.selectedObjectId || null })
    }
    else if (featureCommands[commandId]) {
      if (loftCommands.has(commandId)) beginTask(commandId, { source: state.selectedObjectId || null, sections: state.selectedObjectId ? [state.selectedObjectId] : [] })
      else if (pipeCommands.has(commandId)) beginTask(commandId, { source: state.selectedObjectId || null, profile: state.selectedObjectId || '', spine: '' })
      else if (commandId === 'helix') beginTask(commandId, { pitch: 1, height: 2, radius: 1, angle: 0, segmentLength: 0, localCoord: 'Right-handed' })
      else beginTask(commandId, partCommands.has(commandId) && commandId !== 'primitive' ? { source: state.selectedObjectId || null, base: state.selectedObjectId || '', tool: '' } : { source: state.selectedObjectId || null })
    }
    emit({ type: 'command.completed', commandId, context }); emitState(); return requestId
  }

  const facade: BitBybitWebCadFacade = {
    runtime,
    app: { document: { getActive: () => getState().document, getObject: (objectId) => { const object = state.document.objects.find((candidate) => candidate.id === objectId); return object ? { ...object, properties: object.properties.map((property) => ({ ...property, value: clonePropertyValue(property.value), options: property.options ? [...property.options] : undefined })), sketch: object.sketch ? cloneSketch(object.sketch) : undefined, topology: object.topology ? cloneObjectTopologySnapshot(object.topology) : undefined } : null }, create: (label) => { recomputeCoordinator.cancel(); clearFeatureShapes(); commit({ ...state, document: nextBlankDocument(label), selectedObjectId: '', selectedObjectIds: [] }); return getState().document }, load: loadDocument, markDirty: () => { commit({ ...state, document: { ...state.document, dirty: true } }) }, setProperty, setIncludedFile, addDynamicProperty, removeDynamicProperty, renameDynamicProperty, reorderBodyFeature, removeObject, resolveTopologyReference, setExpression, recompute: recomputeDocument, recomputeAsync: recomputeDocumentAsync, cancelRecompute: () => recomputeCoordinator.cancel(), getDependencies: () => (state.document.dependencies ?? []).map((edge) => ({ ...edge })) }, expression: { evaluate: (expression, variables = {}) => evaluateQuantityExpression(expression, new Map(Object.entries(variables))), dimensionForUnit: quantityDimensionForUnit }, sketcher: { get: getSketch, addGeometry: addSketchGeometry, projectGeometry: projectSketch, carbonCopy: carbonCopySketch, addExternalGeometry: addSketchExternalGeometry, addConstraint: addSketchConstraint, solve: solveSketchObject } },
    history: { canUndo: () => undoStack.length > 0, canRedo: () => redoStack.length > 0, undo: () => { const previous = undoStack.pop(); if (!previous) return; clearFeatureShapes(); redoStack.push(getState()); state = previous; emitState(); notify('Undo applied') }, redo: () => { const next = redoStack.pop(); if (!next) return; clearFeatureShapes(); undoStack.push(getState()); state = next; emitState(); notify('Redo applied') } },
    gui: { workbench: { list: () => Object.keys(workbenchDefinitions) as WorkbenchId[], getActive: () => state.activeWorkbench, setActive }, command: { getState: (commandId) => commandState(commandId, state.activeWorkbench, state.selectedObjectId, state.document.objects.find((object) => object.id === state.selectedObjectId), state.selectedSubshape), list: (workbench) => workbenchDefinitions[workbench].groups.flatMap((group) => group.commands), execute } },
    selection: { getObjectId: () => state.selectedObjectId, getObjectIds: () => [...(state.selectedObjectIds ?? (state.selectedObjectId ? [state.selectedObjectId] : []))], getSubshape: () => cloneSelection(state.selectedSubshape) ?? null, getPreselection: () => cloneSelection(state.preselectedSubshape) ?? null, select, selectObjects, selectSubshape, preselectSubshape, clear: () => select('') },
    task: { getActive: () => getState().task, begin: beginTask, update: (draft) => { if (state.task) state = { ...state, task: { ...state.task, draft: { ...state.task.draft, ...draft } } }; emitState() }, apply: applyTask, cancel: () => { if (state.task) state = { ...state, task: { ...state.task, status: 'cancelled' } }; emitState() } },
    diagnostics: { list: () => state.diagnostics.map(cloneDiagnostic), tree: () => buildDiagnosticTree(state.diagnostics), repair: repairDiagnostic },
    cam: camJob,
    project: {
      capabilities: () => projectPersistence.capabilities(),
      subscribeExternalChanges: (listener) => projectPersistence.subscribeExternalChanges(listener),
      list: () => projectPersistence.list(),
      save: (document = getState().document) => projectPersistence.save(document),
      load: (documentId) => projectPersistence.load(documentId),
      loadCheckpoint: (documentId, version) => projectPersistence.loadCheckpoint(documentId, version),
      recovery: (documentId) => projectPersistence.recovery(documentId),
      fcstd: {
        inspect: (bytes, limits) => inspectFcstdArchive(bytes, limits),
        extractShapes: (bytes, limits) => extractFcstdShapeResources(bytes, limits),
        storeShapes: (bytes, limits) => storeFcstdShapeResources(bytes, (payload, mediaType) => projectPersistence.resource.put(payload, mediaType).then((resource) => ({ hash: resource.hash })), limits),
        extractIncludedFiles: (bytes, limits) => extractFcstdIncludedFileResources(bytes, limits),
        storeIncludedFiles: (bytes, limits) => storeFcstdIncludedFileResources(bytes, (payload, mediaType) => projectPersistence.resource.put(payload, mediaType).then((resource) => ({ hash: resource.hash })), limits),
        instantiateShape: (bytes, resourcePath, signal, limits) => { const document = getState().document; return instantiateFcstdShapeResource(bytes, resourcePath, ({ format, text }) => geometryRuntime.importShape({ format, text, documentId: document.id, documentVersion: document.version }), { limits, signal, release: (shape) => geometryRuntime.release(shape) }) },
        serializeMetadata: (document, options) => serializeFcstdMetadataArchive(document, options),
        serializeMetadataWithResources: async (document, options) => serializeFcstdMetadataArchive(document, await fcstdOptionsWithIncludedFileResources(document, options)),
        rewriteMetadata: (bytes, document, options) => rewriteFcstdMetadataArchive(bytes, document, options),
        rewriteMetadataWithResources: async (bytes, document, options) => rewriteFcstdMetadataArchive(bytes, document, await fcstdOptionsWithIncludedFileResources(document, options)),
        decodePath: (bytes, objectName, propertyName) => decodeFcstdPathProperty(bytes, objectName, propertyName),
        rewritePath: (bytes, edit) => rewriteFcstdPathProperty(bytes, edit),
        rewriteNumeric: (bytes, edit) => rewriteFcstdNumericProperty(bytes, edit),
        rewriteMeasureArea: (bytes, edit) => rewriteFcstdMeasureArea(bytes, edit),
        rewriteBoolList: (bytes, edit) => rewriteFcstdBoolListProperty(bytes, edit),
        rewriteIntegerSet: (bytes, edit) => rewriteFcstdIntegerSetProperty(bytes, edit),
        rewriteColor: (bytes, edit) => rewriteFcstdColorProperty(bytes, edit),
        rewriteColorList: (bytes, edit) => rewriteFcstdColorListProperty(bytes, edit),
        rewriteDirection: (bytes, edit) => rewriteFcstdDirectionProperty(bytes, edit),
        rewriteFile: (bytes, edit) => rewriteFcstdFileProperty(bytes, edit),
        rewriteFont: (bytes, edit) => rewriteFcstdFontProperty(bytes, edit),
        rewriteIncludedFile: async (bytes, edit) => {
          const resourceBytes = await projectPersistence.resource.get(edit.value.resourceHash)
          if (!resourceBytes) throw new Error(`Project resource is missing for FCStd PropertyFileIncluded rewrite: ${edit.value.resourceHash}`)
          const expectedResourceBytes = edit.expectedValue ? await projectPersistence.resource.get(edit.expectedValue.resourceHash) : undefined
          if (edit.expectedValue && !expectedResourceBytes) throw new Error(`Expected project resource is missing for FCStd PropertyFileIncluded rewrite: ${edit.expectedValue.resourceHash}`)
          return rewriteFcstdIncludedFileProperty(bytes, { ...edit, resourceBytes, ...(expectedResourceBytes ? { expectedResourceBytes } : {}) })
        },
      },
      resource: projectPersistence.resource,
      dispose: async () => { autosave.cancel(); await projectPersistence.dispose() },
    },
    geometry: {
      capabilities: () => geometryRuntime.capabilities(),
      configureNativeHistory: (provider, timeoutMs) => geometryRuntime.configureNativeHistory(provider, timeoutMs),
      nativeHistoryCapabilities: () => geometryRuntime.nativeHistoryCapabilities(),
      initialize: () => geometryRuntime.initialize(),
      createBox: (input) => geometryRuntime.createBox(input),
      createCylinder: (input) => geometryRuntime.createCylinder(input),
      createSphere: (input) => geometryRuntime.createSphere(input),
      createEllipsoid: (input) => geometryRuntime.createEllipsoid(input),
      createCone: (input) => geometryRuntime.createCone(input),
      createTorus: (input) => geometryRuntime.createTorus(input),
      createHelix: (input) => geometryRuntime.createHelix(input),
      createPrism: (input) => geometryRuntime.createPrism(input),
      createWedge: (input) => geometryRuntime.createWedge(input),
      applyPlacement: (input) => geometryRuntime.applyPlacement(input),
      mirror: (input) => geometryRuntime.mirror(input),
      union: (input) => geometryRuntime.union(input),
      cut: (input) => geometryRuntime.cut(input),
      intersection: (input) => geometryRuntime.intersection(input),
      fillet: (input) => geometryRuntime.fillet(input),
      chamfer: (input) => geometryRuntime.chamfer(input),
      draft: (input) => geometryRuntime.draft(input),
      thickness: (input) => geometryRuntime.thickness(input),
      exportStep: (shape, fileName) => geometryRuntime.exportStep(shape, fileName),
      exportIges: (shape, fileName) => geometryRuntime.exportIges(shape, fileName),
      exportStl: (shape, fileName, precision) => geometryRuntime.exportStl(shape, fileName, precision),
      exportBrep: (shape, fileName) => geometryRuntime.exportBrep(shape, fileName),
      importShape: (input) => geometryRuntime.importShape(input),
      pad: (input) => geometryRuntime.pad(input),
      extrude: (input) => geometryRuntime.extrude(input),
      pocket: (input) => geometryRuntime.pocket(input),
      revolution: (input) => geometryRuntime.revolution(input),
      groove: (input) => geometryRuntime.groove(input),
      loft: (input) => geometryRuntime.loft(input),
      pipe: (input) => geometryRuntime.pipe(input),
      mesh: (shape, precision) => geometryRuntime.mesh(shape, precision),
      massProperties: (shape) => geometryRuntime.massProperties(shape),
      qualityReport: (shape) => geometryRuntime.qualityReport(shape),
      linearLength: (shape) => geometryRuntime.linearLength(shape),
      subshapes: (shape, precision) => geometryRuntime.subshapes(shape, precision),
      topology: (shape, precision) => geometryRuntime.topology(shape, precision),
      topologyHistory: (input) => geometryRuntime.topologyHistory(input),
      getObjectShape: (objectId) => {
        const shape = featureShapes.get(objectId)
        return shape ? { ...shape } : null
      },
      release: (shape) => geometryRuntime.release(shape),
      dispose: () => {
        clearFeatureShapes()
        geometryRuntime.dispose()
      },
    },
    viewport: { createAdapter: () => new ThreeViewportAdapter() },
    getState, subscribe: (listener) => { listeners.add(listener); return () => { listeners.delete(listener) } }, notify,
  }
  void projectPersistence.initialize().then((nextCapabilities) => { state = { ...state, persistence: nextCapabilities }; emitState() }).catch((error: unknown) => { const diagnostic: Diagnostic = { id: `diag-${++requestSequence}`, source: 'persistence', severity: 'warning', code: 'PERSISTENCE_INIT_FAILED', message: error instanceof Error ? error.message : String(error), documentId: state.document.id, documentVersion: state.document.version }; state = { ...state, diagnostics: [...state.diagnostics, diagnostic] }; emit({ type: 'diagnostic.added', diagnostic, context: { apiVersion: state.apiVersion, requestId: `req-${++requestSequence}`, documentId: state.document.id, documentVersion: state.document.version, workbench: state.activeWorkbench } }) })
  return facade
}

export function createMockFacade(): BitBybitWebCadFacade {
  return createWebCadFacade({
    runtimeMode: 'mock',
    initialDocument: createPumpHousingDemoDocument(),
    initialSelectedObjectIds: PUMP_HOUSING_DEMO_TEMPLATE.selectedObjectIds,
  })
}
