@vib3code/sdk 2.0.1 → 2.0.3-canary.0c55e5a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/DOCS/AGENT_HARNESS_ARCHITECTURE.md +243 -0
  3. package/DOCS/CLI_ONBOARDING.md +1 -1
  4. package/DOCS/CROSS_SITE_DESIGN_PATTERNS.md +117 -0
  5. package/DOCS/EPIC_SCROLL_EVENTS.md +773 -0
  6. package/DOCS/HANDOFF_LANDING_PAGE.md +154 -0
  7. package/DOCS/HANDOFF_SDK_DEVELOPMENT.md +493 -0
  8. package/DOCS/MULTIVIZ_CHOREOGRAPHY_PATTERNS.md +937 -0
  9. package/DOCS/PRODUCT_STRATEGY.md +63 -0
  10. package/DOCS/README.md +103 -0
  11. package/DOCS/REFERENCE_SCROLL_ANALYSIS.md +97 -0
  12. package/DOCS/ROADMAP.md +111 -0
  13. package/DOCS/SCROLL_TIMELINE_v3.md +269 -0
  14. package/DOCS/SITE_REFACTOR_PLAN.md +100 -0
  15. package/DOCS/STATUS.md +24 -0
  16. package/DOCS/SYSTEM_INVENTORY.md +33 -30
  17. package/DOCS/VISUAL_ANALYSIS_CLICKERSS.md +85 -0
  18. package/DOCS/VISUAL_ANALYSIS_FACETAD.md +133 -0
  19. package/DOCS/VISUAL_ANALYSIS_SIMONE.md +95 -0
  20. package/DOCS/VISUAL_ANALYSIS_TABLESIDE.md +86 -0
  21. package/DOCS/{BLUEPRINT_EXECUTION_PLAN_2026-01-07.md → archive/BLUEPRINT_EXECUTION_PLAN_2026-01-07.md} +1 -1
  22. package/DOCS/{DEV_TRACK_ANALYSIS.md → archive/DEV_TRACK_ANALYSIS.md} +3 -0
  23. package/DOCS/{SYSTEM_AUDIT_2026-01-30.md → archive/SYSTEM_AUDIT_2026-01-30.md} +3 -0
  24. package/DOCS/{DEV_TRACK_SESSION_2026-01-31.md → dev-tracks/DEV_TRACK_SESSION_2026-01-31.md} +1 -1
  25. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-06.md +231 -0
  26. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-13.md +127 -0
  27. package/DOCS/dev-tracks/DEV_TRACK_SESSION_2026-02-15.md +142 -0
  28. package/DOCS/dev-tracks/README.md +10 -0
  29. package/README.md +26 -13
  30. package/cpp/CMakeLists.txt +236 -0
  31. package/cpp/bindings/embind.cpp +269 -0
  32. package/cpp/build.sh +129 -0
  33. package/cpp/geometry/Crystal.cpp +103 -0
  34. package/cpp/geometry/Fractal.cpp +136 -0
  35. package/cpp/geometry/GeometryGenerator.cpp +262 -0
  36. package/cpp/geometry/KleinBottle.cpp +71 -0
  37. package/cpp/geometry/Sphere.cpp +134 -0
  38. package/cpp/geometry/Tesseract.cpp +94 -0
  39. package/cpp/geometry/Tetrahedron.cpp +83 -0
  40. package/cpp/geometry/Torus.cpp +65 -0
  41. package/cpp/geometry/WarpFunctions.cpp +238 -0
  42. package/cpp/geometry/Wave.cpp +85 -0
  43. package/cpp/include/vib3_ffi.h +238 -0
  44. package/cpp/math/Mat4x4.cpp +409 -0
  45. package/cpp/math/Mat4x4.hpp +209 -0
  46. package/cpp/math/Projection.cpp +142 -0
  47. package/cpp/math/Projection.hpp +148 -0
  48. package/cpp/math/Rotor4D.cpp +322 -0
  49. package/cpp/math/Rotor4D.hpp +204 -0
  50. package/cpp/math/Vec4.cpp +303 -0
  51. package/cpp/math/Vec4.hpp +225 -0
  52. package/cpp/src/vib3_ffi.cpp +607 -0
  53. package/cpp/tests/Geometry_test.cpp +213 -0
  54. package/cpp/tests/Mat4x4_test.cpp +494 -0
  55. package/cpp/tests/Projection_test.cpp +298 -0
  56. package/cpp/tests/Rotor4D_test.cpp +423 -0
  57. package/cpp/tests/Vec4_test.cpp +489 -0
  58. package/package.json +40 -27
  59. package/src/agent/index.js +1 -3
  60. package/src/agent/mcp/MCPServer.js +1024 -7
  61. package/src/agent/mcp/index.js +1 -1
  62. package/src/agent/mcp/stdio-server.js +264 -0
  63. package/src/agent/mcp/tools.js +454 -0
  64. package/src/cli/index.js +374 -44
  65. package/src/core/CanvasManager.js +97 -204
  66. package/src/core/ErrorReporter.js +1 -1
  67. package/src/core/Parameters.js +1 -1
  68. package/src/core/VIB3Engine.js +93 -4
  69. package/src/core/VitalitySystem.js +53 -0
  70. package/src/core/index.js +18 -0
  71. package/src/core/renderers/FacetedRendererAdapter.js +10 -9
  72. package/src/core/renderers/HolographicRendererAdapter.js +13 -9
  73. package/src/core/renderers/QuantumRendererAdapter.js +11 -7
  74. package/src/creative/AestheticMapper.js +628 -0
  75. package/src/creative/ChoreographyPlayer.js +481 -0
  76. package/src/creative/index.js +11 -0
  77. package/src/export/TradingCardManager.js +3 -4
  78. package/src/export/index.js +11 -1
  79. package/src/faceted/FacetedSystem.js +241 -388
  80. package/src/games/glyph-war/GlyphWarVisualizer.js +641 -0
  81. package/src/holograms/HolographicVisualizer.js +29 -12
  82. package/src/holograms/RealHolographicSystem.js +194 -43
  83. package/src/math/Mat4x4.js +70 -13
  84. package/src/math/Rotor4D.js +100 -39
  85. package/src/math/index.js +7 -7
  86. package/src/polychora/PolychoraSystem.js +77 -0
  87. package/src/quantum/QuantumEngine.js +103 -66
  88. package/src/quantum/QuantumVisualizer.js +7 -2
  89. package/src/reactivity/index.js +3 -5
  90. package/src/render/LayerPresetManager.js +372 -0
  91. package/src/render/LayerReactivityBridge.js +344 -0
  92. package/src/render/LayerRelationshipGraph.js +610 -0
  93. package/src/render/MultiCanvasBridge.js +148 -25
  94. package/src/render/UnifiedRenderBridge.js +3 -0
  95. package/src/render/index.js +27 -2
  96. package/src/scene/index.js +4 -4
  97. package/src/shaders/faceted/faceted.frag.glsl +220 -80
  98. package/src/shaders/faceted/faceted.frag.wgsl +138 -97
  99. package/src/shaders/holographic/holographic.frag.glsl +28 -9
  100. package/src/shaders/holographic/holographic.frag.wgsl +107 -38
  101. package/src/shaders/quantum/quantum.frag.glsl +1 -0
  102. package/src/shaders/quantum/quantum.frag.wgsl +1 -1
  103. package/src/testing/ParallelTestFramework.js +2 -2
  104. package/src/viewer/GalleryUI.js +17 -0
  105. package/src/viewer/ViewerPortal.js +2 -2
  106. package/src/viewer/index.js +1 -1
  107. package/tools/headless-renderer.js +258 -0
  108. package/tools/shader-sync-verify.js +8 -4
  109. package/tools/site-analysis/all-reports.json +32 -0
  110. package/tools/site-analysis/combined-analysis.md +50 -0
  111. package/tools/site-analyzer.mjs +779 -0
  112. package/tools/visual-catalog/capture.js +276 -0
  113. package/tools/visual-catalog/composite.js +138 -0
  114. package/types/adaptive-sdk.d.ts +204 -5
  115. package/types/agent/cli.d.ts +78 -0
  116. package/types/agent/index.d.ts +18 -0
  117. package/types/agent/mcp.d.ts +87 -0
  118. package/types/agent/telemetry.d.ts +190 -0
  119. package/types/core/VIB3Engine.d.ts +26 -0
  120. package/types/core/index.d.ts +261 -0
  121. package/types/creative/AestheticMapper.d.ts +72 -0
  122. package/types/creative/ChoreographyPlayer.d.ts +96 -0
  123. package/types/creative/index.d.ts +17 -0
  124. package/types/export/index.d.ts +243 -0
  125. package/types/geometry/index.d.ts +164 -0
  126. package/types/math/index.d.ts +214 -0
  127. package/types/render/LayerPresetManager.d.ts +78 -0
  128. package/types/render/LayerReactivityBridge.d.ts +85 -0
  129. package/types/render/LayerRelationshipGraph.d.ts +174 -0
  130. package/types/render/index.d.ts +3 -0
  131. package/types/scene/index.d.ts +204 -0
  132. package/types/systems/index.d.ts +244 -0
  133. package/types/variations/index.d.ts +62 -0
  134. package/types/viewer/index.d.ts +225 -0
  135. /package/DOCS/{DEV_TRACK_PLAN_2026-01-07.md → archive/DEV_TRACK_PLAN_2026-01-07.md} +0 -0
  136. /package/DOCS/{SESSION_014_PLAN.md → archive/SESSION_014_PLAN.md} +0 -0
  137. /package/DOCS/{SESSION_LOG_2026-01-07.md → archive/SESSION_LOG_2026-01-07.md} +0 -0
  138. /package/DOCS/{STRATEGIC_BLUEPRINT_2026-01-07.md → archive/STRATEGIC_BLUEPRINT_2026-01-07.md} +0 -0
  139. /package/src/viewer/{ReactivityManager.js → ViewerInputHandler.js} +0 -0
@@ -6,6 +6,12 @@
6
6
  import { toolDefinitions, getToolList, validateToolInput } from './tools.js';
7
7
  import { schemaRegistry } from '../../schemas/index.js';
8
8
  import { telemetry, EventType, withTelemetry } from '../telemetry/index.js';
9
+ import { AestheticMapper } from '../../creative/AestheticMapper.js';
10
+ import { ChoreographyPlayer } from '../../creative/ChoreographyPlayer.js';
11
+ import { ParameterTimeline } from '../../creative/ParameterTimeline.js';
12
+ import { ColorPresetsSystem } from '../../creative/ColorPresetsSystem.js';
13
+ import { TransitionAnimator } from '../../creative/TransitionAnimator.js';
14
+ import { PRESET_REGISTRY } from '../../render/LayerRelationshipGraph.js';
9
15
 
10
16
  /**
11
17
  * Generate unique IDs
@@ -38,6 +44,36 @@ export class MCPServer {
38
44
  this.engine = engine;
39
45
  this.sceneId = null;
40
46
  this.initialized = false;
47
+ this._gallerySlots = new Map();
48
+ }
49
+
50
+ /**
51
+ * Get or lazily create ColorPresetsSystem instance.
52
+ * Requires engine for the parameter update callback.
53
+ * @returns {ColorPresetsSystem|null}
54
+ */
55
+ _getColorPresets() {
56
+ if (this._colorPresets) return this._colorPresets;
57
+ if (!this.engine) return null;
58
+ this._colorPresets = new ColorPresetsSystem(
59
+ (name, value) => this.engine.setParameter(name, value)
60
+ );
61
+ return this._colorPresets;
62
+ }
63
+
64
+ /**
65
+ * Get or lazily create TransitionAnimator instance.
66
+ * Requires engine for the parameter update/get callbacks.
67
+ * @returns {TransitionAnimator|null}
68
+ */
69
+ _getTransitionAnimator() {
70
+ if (this._transitionAnimator) return this._transitionAnimator;
71
+ if (!this.engine) return null;
72
+ this._transitionAnimator = new TransitionAnimator(
73
+ (name, value) => this.engine.setParameter(name, value),
74
+ (name) => this.engine.getParameter(name)
75
+ );
76
+ return this._transitionAnimator;
41
77
  }
42
78
 
43
79
  buildResponse(operation, data, options = {}) {
@@ -163,6 +199,60 @@ export class MCPServer {
163
199
  case 'list_behavior_presets':
164
200
  result = this.listBehaviorPresets();
165
201
  break;
202
+ // Agent-power tools (Phase 7)
203
+ case 'describe_visual_state':
204
+ result = this.describeVisualState();
205
+ break;
206
+ case 'batch_set_parameters':
207
+ result = await this.batchSetParameters(args);
208
+ break;
209
+ case 'create_timeline':
210
+ result = this.createTimeline(args);
211
+ break;
212
+ case 'play_transition':
213
+ result = this.playTransition(args);
214
+ break;
215
+ case 'apply_color_preset':
216
+ result = this.applyColorPreset(args);
217
+ break;
218
+ case 'set_post_processing':
219
+ result = this.setPostProcessing(args);
220
+ break;
221
+ case 'create_choreography':
222
+ result = this.createChoreography(args);
223
+ break;
224
+ // Visual feedback tools (Phase 7.1)
225
+ case 'capture_screenshot':
226
+ result = await this.captureScreenshot(args);
227
+ break;
228
+ case 'design_from_description':
229
+ result = await this.designFromDescription(args);
230
+ break;
231
+ case 'get_aesthetic_vocabulary':
232
+ result = this.getAestheticVocabulary();
233
+ break;
234
+ case 'play_choreography':
235
+ result = await this.playChoreographyTool(args);
236
+ break;
237
+ case 'control_timeline':
238
+ result = this.controlTimeline(args);
239
+ break;
240
+ // Layer relationship tools (Phase 8)
241
+ case 'set_layer_profile':
242
+ result = this.setLayerProfile(args);
243
+ break;
244
+ case 'set_layer_relationship':
245
+ result = this.setLayerRelationship(args);
246
+ break;
247
+ case 'set_layer_keystone':
248
+ result = this.setLayerKeystone(args);
249
+ break;
250
+ case 'get_layer_config':
251
+ result = this.getLayerConfig();
252
+ break;
253
+ case 'tune_layer_relationship':
254
+ result = this.tuneLayerRelationship(args);
255
+ break;
166
256
  default:
167
257
  throw new Error(`Unknown tool: ${toolName}`);
168
258
  }
@@ -426,32 +516,68 @@ export class MCPServer {
426
516
 
427
517
  telemetry.recordEvent(EventType.GALLERY_SAVE, { slot });
428
518
 
519
+ // Persist actual engine state if available
520
+ if (this.engine) {
521
+ const state = this.engine.exportState();
522
+ this._gallerySlots.set(slot, {
523
+ name: name || `Variation ${slot}`,
524
+ saved_at: new Date().toISOString(),
525
+ state
526
+ });
527
+ }
528
+
429
529
  return {
430
530
  slot,
431
531
  name: name || `Variation ${slot}`,
432
532
  saved_at: new Date().toISOString(),
533
+ persisted: !!this.engine,
534
+ gallery_size: this._gallerySlots.size,
433
535
  suggested_next_actions: ['load_from_gallery', 'randomize_parameters']
434
536
  };
435
537
  }
436
538
 
437
539
  /**
438
- * Load from gallery
540
+ * Load from gallery — restores previously saved state
439
541
  */
440
542
  loadFromGallery(args) {
441
543
  const { slot } = args;
442
544
 
443
- if (this.engine) {
444
- // Apply variation
445
- const params = this.engine.parameters?.generateVariationParameters?.(slot) || {};
446
- this.engine.setParameters(params);
545
+ telemetry.recordEvent(EventType.GALLERY_LOAD, { slot });
546
+
547
+ const saved = this._gallerySlots.get(slot);
548
+ if (saved && this.engine) {
549
+ // Restore saved state
550
+ this.engine.importState(saved.state);
551
+ return {
552
+ slot,
553
+ name: saved.name,
554
+ saved_at: saved.saved_at,
555
+ loaded_at: new Date().toISOString(),
556
+ restored: true,
557
+ ...this.getState()
558
+ };
447
559
  }
448
560
 
449
- telemetry.recordEvent(EventType.GALLERY_LOAD, { slot });
561
+ if (!saved) {
562
+ // No saved state — fall back to random variation
563
+ if (this.engine) {
564
+ const params = this.engine.parameters?.generateVariationParameters?.(slot) || {};
565
+ this.engine.setParameters(params);
566
+ }
567
+ return {
568
+ slot,
569
+ loaded_at: new Date().toISOString(),
570
+ restored: false,
571
+ note: 'No saved state in this slot — generated random variation',
572
+ ...this.getState()
573
+ };
574
+ }
450
575
 
451
576
  return {
452
577
  slot,
453
578
  loaded_at: new Date().toISOString(),
454
- ...this.getState()
579
+ restored: false,
580
+ note: 'Engine not initialized — cannot apply state'
455
581
  };
456
582
  }
457
583
 
@@ -943,6 +1069,897 @@ export class MCPServer {
943
1069
  suggested_next_actions: ['apply_behavior_preset']
944
1070
  };
945
1071
  }
1072
+
1073
+ // ===== AGENT-POWER TOOLS (Phase 7 — Agent Harness) =====
1074
+
1075
+ /**
1076
+ * Generate a natural-language description of the current visual state.
1077
+ * Enables text-only agents to "see" what the visualization looks like.
1078
+ */
1079
+ describeVisualState() {
1080
+ const state = this.getState();
1081
+ const params = state.visual || {};
1082
+ const rotation = state.rotation_state || {};
1083
+ const geometry = state.geometry || {};
1084
+
1085
+ // Color description from hue
1086
+ const hue = params.hue || 0;
1087
+ const colorName = hue < 15 ? 'red' : hue < 45 ? 'orange' : hue < 75 ? 'yellow' :
1088
+ hue < 150 ? 'green' : hue < 195 ? 'cyan' : hue < 255 ? 'blue' :
1089
+ hue < 285 ? 'purple' : hue < 330 ? 'magenta' : 'red';
1090
+ const satDesc = (params.saturation || 0.8) > 0.7 ? 'vivid' :
1091
+ (params.saturation || 0.8) > 0.4 ? 'moderate' : 'desaturated';
1092
+ const intensityDesc = (params.intensity || 0.5) > 0.7 ? 'bright' :
1093
+ (params.intensity || 0.5) > 0.3 ? 'medium brightness' : 'dim';
1094
+
1095
+ // Motion description
1096
+ const speed = params.speed || 1.0;
1097
+ const speedDesc = speed > 2.0 ? 'rapidly' : speed > 1.0 ? 'moderately' :
1098
+ speed > 0.4 ? 'slowly' : 'very slowly';
1099
+ const chaos = params.chaos || 0;
1100
+ const chaosDesc = chaos > 0.7 ? 'highly turbulent' : chaos > 0.3 ? 'somewhat organic' :
1101
+ chaos > 0.05 ? 'subtly alive' : 'perfectly still';
1102
+
1103
+ // 4D rotation activity
1104
+ const has4D = Math.abs(rotation.XW || 0) > 0.1 ||
1105
+ Math.abs(rotation.YW || 0) > 0.1 ||
1106
+ Math.abs(rotation.ZW || 0) > 0.1;
1107
+ const rotDesc = has4D ? 'with visible 4D hyperspace rotation (inside-out morphing)' :
1108
+ 'in standard 3D orientation';
1109
+
1110
+ // Density/complexity
1111
+ const density = params.gridDensity || 10;
1112
+ const densityDesc = density > 50 ? 'extremely intricate' : density > 25 ? 'detailed' :
1113
+ density > 12 ? 'moderate detail' : 'bold and sparse';
1114
+
1115
+ // Projection
1116
+ const dim = params.dimension || 3.8;
1117
+ const projDesc = dim < 3.3 ? 'dramatic fish-eye distortion' :
1118
+ dim < 3.8 ? 'moderate perspective depth' : 'subtle, flattened perspective';
1119
+
1120
+ const description = [
1121
+ `A ${satDesc} ${colorName} ${geometry.core_type || 'base'} ${geometry.base_type || 'tetrahedron'}`,
1122
+ `rendered in the ${state.system || 'quantum'} system.`,
1123
+ `The pattern is ${densityDesc} and ${chaosDesc},`,
1124
+ `animating ${speedDesc} ${rotDesc}.`,
1125
+ `Color is ${intensityDesc} with ${projDesc}.`,
1126
+ params.morphFactor > 0.5 ? `Shape is morphing between geometries (factor: ${params.morphFactor}).` : ''
1127
+ ].filter(Boolean).join(' ');
1128
+
1129
+ return {
1130
+ description,
1131
+ mood: this._assessMood(params),
1132
+ complexity: density > 40 ? 'high' : density > 15 ? 'medium' : 'low',
1133
+ motion_level: speed > 1.5 ? 'high' : speed > 0.5 ? 'medium' : 'low',
1134
+ has_4d_rotation: has4D,
1135
+ color_family: colorName,
1136
+ suggested_next_actions: ['set_visual_parameters', 'set_rotation', 'batch_set_parameters']
1137
+ };
1138
+ }
1139
+
1140
+ /**
1141
+ * Assess the emotional mood of the current visual state
1142
+ */
1143
+ _assessMood(params) {
1144
+ const hue = params.hue || 0;
1145
+ const speed = params.speed || 1.0;
1146
+ const chaos = params.chaos || 0;
1147
+ const intensity = params.intensity || 0.5;
1148
+
1149
+ if (speed < 0.3 && chaos < 0.1) return 'serene';
1150
+ if (speed > 2.0 && chaos > 0.6) return 'chaotic';
1151
+ if (hue > 180 && hue < 260 && intensity < 0.5) return 'mysterious';
1152
+ if (hue > 0 && hue < 60 && intensity > 0.6) return 'warm';
1153
+ if (hue > 150 && hue < 200 && speed < 0.8) return 'tranquil';
1154
+ if (chaos > 0.5 && speed > 1.5) return 'energetic';
1155
+ if (intensity > 0.8) return 'vibrant';
1156
+ return 'balanced';
1157
+ }
1158
+
1159
+ /**
1160
+ * Atomically set multiple parameter categories in one call
1161
+ */
1162
+ async batchSetParameters(args) {
1163
+ const { system, geometry, rotation, visual, preset } = args;
1164
+
1165
+ // Switch system first if requested
1166
+ if (system && this.engine) {
1167
+ await this.engine.switchSystem(system);
1168
+ }
1169
+
1170
+ // Set geometry
1171
+ if (geometry !== undefined && this.engine) {
1172
+ this.engine.setParameter('geometry', geometry);
1173
+ }
1174
+
1175
+ // Set rotation
1176
+ if (rotation) {
1177
+ const rotMap = { XY: 'rot4dXY', XZ: 'rot4dXZ', YZ: 'rot4dYZ',
1178
+ XW: 'rot4dXW', YW: 'rot4dYW', ZW: 'rot4dZW' };
1179
+ for (const [key, value] of Object.entries(rotation)) {
1180
+ if (value !== undefined && this.engine) {
1181
+ this.engine.setParameter(rotMap[key], value);
1182
+ }
1183
+ }
1184
+ }
1185
+
1186
+ // Set visual parameters
1187
+ if (visual && this.engine) {
1188
+ for (const [key, value] of Object.entries(visual)) {
1189
+ this.engine.setParameter(key, value);
1190
+ }
1191
+ }
1192
+
1193
+ // Apply preset last (overrides relevant params)
1194
+ if (preset) {
1195
+ this.applyBehaviorPreset({ preset });
1196
+ }
1197
+
1198
+ telemetry.recordEvent(EventType.PARAMETER_BATCH_CHANGE, {
1199
+ count: (rotation ? Object.keys(rotation).length : 0) +
1200
+ (visual ? Object.keys(visual).length : 0) +
1201
+ (system ? 1 : 0) + (geometry !== undefined ? 1 : 0)
1202
+ });
1203
+
1204
+ return {
1205
+ ...this.getState(),
1206
+ batch_applied: true,
1207
+ suggested_next_actions: ['describe_visual_state', 'save_to_gallery', 'create_timeline']
1208
+ };
1209
+ }
1210
+
1211
+ /**
1212
+ * Create a ParameterTimeline from agent specification
1213
+ */
1214
+ createTimeline(args) {
1215
+ const { name, duration_ms, bpm, loop_mode = 'once', tracks } = args;
1216
+
1217
+ const timelineId = generateId('timeline');
1218
+
1219
+ // Validate tracks have properly sorted keyframes
1220
+ const validatedTracks = {};
1221
+ for (const [param, keyframes] of Object.entries(tracks)) {
1222
+ validatedTracks[param] = keyframes
1223
+ .map(kf => ({
1224
+ time: Math.max(0, Math.min(kf.time, duration_ms)),
1225
+ value: kf.value,
1226
+ easing: kf.easing || 'easeInOut'
1227
+ }))
1228
+ .sort((a, b) => a.time - b.time);
1229
+ }
1230
+
1231
+ // Build timeline data for ParameterTimeline consumption
1232
+ const timelineData = {
1233
+ id: timelineId,
1234
+ name: name || `Timeline ${timelineId}`,
1235
+ duration: duration_ms,
1236
+ bpm: bpm || null,
1237
+ loopMode: loop_mode,
1238
+ tracks: validatedTracks
1239
+ };
1240
+
1241
+ // If engine is available, create and start the timeline
1242
+ if (this.engine) {
1243
+ // Store for later retrieval
1244
+ if (!this._timelines) this._timelines = new Map();
1245
+ this._timelines.set(timelineId, timelineData);
1246
+ }
1247
+
1248
+ return {
1249
+ timeline_id: timelineId,
1250
+ name: timelineData.name,
1251
+ duration_ms,
1252
+ bpm: bpm || null,
1253
+ loop_mode,
1254
+ track_count: Object.keys(validatedTracks).length,
1255
+ tracks_summary: Object.entries(validatedTracks).map(([param, kfs]) => ({
1256
+ parameter: param,
1257
+ keyframe_count: kfs.length,
1258
+ value_range: [
1259
+ Math.min(...kfs.map(k => k.value)),
1260
+ Math.max(...kfs.map(k => k.value))
1261
+ ]
1262
+ })),
1263
+ load_code: `const tl = new ParameterTimeline((n, v) => engine.setParameter(n, v));\ntl.importTimeline(${JSON.stringify(timelineData)});\ntl.play();`,
1264
+ suggested_next_actions: ['play_transition', 'describe_visual_state', 'save_to_gallery']
1265
+ };
1266
+ }
1267
+
1268
+ /**
1269
+ * Play a smooth transition sequence
1270
+ */
1271
+ playTransition(args) {
1272
+ const { sequence } = args;
1273
+
1274
+ const transitionId = generateId('transition');
1275
+
1276
+ // Validate and normalize the sequence
1277
+ const normalizedSequence = sequence.map((step, i) => ({
1278
+ params: step.params,
1279
+ duration: step.duration || 1000,
1280
+ easing: step.easing || 'easeInOut',
1281
+ delay: step.delay || 0
1282
+ }));
1283
+
1284
+ const totalDuration = normalizedSequence.reduce(
1285
+ (sum, step) => sum + step.duration + step.delay, 0
1286
+ );
1287
+
1288
+ // Execute live if engine available
1289
+ let executing = false;
1290
+ const animator = this._getTransitionAnimator();
1291
+ if (animator) {
1292
+ const seqId = animator.sequence(normalizedSequence);
1293
+ executing = !!seqId;
1294
+ }
1295
+
1296
+ return {
1297
+ transition_id: transitionId,
1298
+ executing,
1299
+ step_count: normalizedSequence.length,
1300
+ total_duration_ms: totalDuration,
1301
+ steps: normalizedSequence.map((step, i) => ({
1302
+ index: i,
1303
+ params: Object.keys(step.params),
1304
+ duration: step.duration,
1305
+ easing: step.easing,
1306
+ delay: step.delay
1307
+ })),
1308
+ load_code: executing ? null : `const animator = new TransitionAnimator(\n (n, v) => engine.setParameter(n, v),\n (n) => engine.getParameter(n)\n);\nanimator.sequence(${JSON.stringify(normalizedSequence)});`,
1309
+ suggested_next_actions: ['describe_visual_state', 'create_timeline', 'save_to_gallery']
1310
+ };
1311
+ }
1312
+
1313
+ /**
1314
+ * Apply a named color preset
1315
+ */
1316
+ applyColorPreset(args) {
1317
+ const { preset, transition = true, duration = 800 } = args;
1318
+
1319
+ const colorSystem = this._getColorPresets();
1320
+
1321
+ if (colorSystem) {
1322
+ // Use real ColorPresetsSystem — full preset library with transitions
1323
+ const config = colorSystem.getPreset(preset);
1324
+ if (!config) {
1325
+ const allPresets = colorSystem.getPresets().map(p => p.name);
1326
+ return {
1327
+ error: {
1328
+ type: 'ValidationError',
1329
+ code: 'INVALID_COLOR_PRESET',
1330
+ message: `Unknown color preset: ${preset}`,
1331
+ valid_options: allPresets
1332
+ }
1333
+ };
1334
+ }
1335
+
1336
+ colorSystem.applyPreset(preset, transition, duration);
1337
+
1338
+ return {
1339
+ preset,
1340
+ applied: { hue: config.hue, saturation: config.saturation, intensity: config.intensity },
1341
+ transition: transition ? { enabled: true, duration } : { enabled: false },
1342
+ full_config: config,
1343
+ suggested_next_actions: ['set_post_processing', 'describe_visual_state', 'set_visual_parameters']
1344
+ };
1345
+ }
1346
+
1347
+ // Fallback: no engine, return preset metadata for artifact mode
1348
+ return {
1349
+ preset,
1350
+ applied: null,
1351
+ load_code: `const colors = new ColorPresetsSystem((n, v) => engine.setParameter(n, v));\ncolors.applyPreset('${preset}', ${transition}, ${duration});`,
1352
+ suggested_next_actions: ['set_post_processing', 'describe_visual_state', 'set_visual_parameters']
1353
+ };
1354
+ }
1355
+
1356
+ /**
1357
+ * Configure post-processing effects pipeline
1358
+ */
1359
+ setPostProcessing(args) {
1360
+ const { effects, chain_preset, clear_first = true } = args;
1361
+
1362
+ // Try to execute live in browser context
1363
+ let executing = false;
1364
+ if (typeof document !== 'undefined') {
1365
+ try {
1366
+ const target = document.getElementById('viz-container')
1367
+ || document.querySelector('.vib3-container')
1368
+ || document.querySelector('canvas')?.parentElement;
1369
+
1370
+ if (target) {
1371
+ // Lazy-init pipeline, importing dynamically to avoid Node.js issues
1372
+ if (!this._postPipeline) {
1373
+ // PostProcessingPipeline imported statically would fail in Node;
1374
+ // it's already a known browser-only module, so guard at runtime
1375
+ const { PostProcessingPipeline: PPP } = { PostProcessingPipeline: globalThis.PostProcessingPipeline };
1376
+ if (PPP) {
1377
+ this._postPipeline = new PPP(target);
1378
+ }
1379
+ }
1380
+
1381
+ if (this._postPipeline) {
1382
+ if (clear_first) this._postPipeline.clearChain?.();
1383
+ if (chain_preset) {
1384
+ this._postPipeline.loadPresetChain(chain_preset);
1385
+ } else if (effects) {
1386
+ for (const e of effects) {
1387
+ this._postPipeline.addEffect(e.name, { intensity: e.intensity || 0.5, ...e });
1388
+ }
1389
+ }
1390
+ this._postPipeline.apply();
1391
+ executing = true;
1392
+ }
1393
+ }
1394
+ } catch { /* fall through to code generation */ }
1395
+ }
1396
+
1397
+ return {
1398
+ applied: true,
1399
+ executing,
1400
+ effects: effects || [],
1401
+ chain_preset: chain_preset || null,
1402
+ cleared_previous: clear_first,
1403
+ load_code: executing ? null : (effects ?
1404
+ `const pipeline = new PostProcessingPipeline(document.getElementById('viz-container'));\n${effects.map(e => `pipeline.addEffect('${e.name}', { intensity: ${e.intensity || 0.5} });`).join('\n')}\npipeline.apply();` :
1405
+ `const pipeline = new PostProcessingPipeline(document.getElementById('viz-container'));\npipeline.loadPresetChain('${chain_preset}');\npipeline.apply();`),
1406
+ suggested_next_actions: ['describe_visual_state', 'apply_color_preset', 'create_choreography']
1407
+ };
1408
+ }
1409
+
1410
+ /**
1411
+ * Create a multi-scene choreography — the most powerful agent composition tool
1412
+ */
1413
+ createChoreography(args) {
1414
+ const { name, duration_ms, bpm, scenes } = args;
1415
+
1416
+ const choreographyId = generateId('choreo');
1417
+
1418
+ // Validate scene time ranges don't exceed duration
1419
+ const validatedScenes = scenes.map((scene, i) => ({
1420
+ index: i,
1421
+ time_start: Math.max(0, scene.time_start),
1422
+ time_end: Math.min(scene.time_end, duration_ms),
1423
+ system: scene.system,
1424
+ geometry: scene.geometry ?? 0,
1425
+ transition_in: scene.transition_in || { type: 'cut', duration: 0 },
1426
+ tracks: scene.tracks || {},
1427
+ color_preset: scene.color_preset || null,
1428
+ post_processing: scene.post_processing || [],
1429
+ audio: scene.audio || null
1430
+ }));
1431
+
1432
+ const choreography = {
1433
+ id: choreographyId,
1434
+ name: name || `Choreography ${choreographyId}`,
1435
+ duration_ms,
1436
+ bpm: bpm || null,
1437
+ scene_count: validatedScenes.length,
1438
+ scenes: validatedScenes
1439
+ };
1440
+
1441
+ // Store for later retrieval
1442
+ if (!this._choreographies) this._choreographies = new Map();
1443
+ this._choreographies.set(choreographyId, choreography);
1444
+
1445
+ return {
1446
+ choreography_id: choreographyId,
1447
+ name: choreography.name,
1448
+ duration_ms,
1449
+ bpm: bpm || null,
1450
+ scene_count: validatedScenes.length,
1451
+ scenes_summary: validatedScenes.map(s => ({
1452
+ index: s.index,
1453
+ time: `${s.time_start}ms → ${s.time_end}ms`,
1454
+ system: s.system,
1455
+ geometry: s.geometry,
1456
+ transition: s.transition_in.type,
1457
+ track_count: Object.keys(s.tracks).length,
1458
+ color_preset: s.color_preset,
1459
+ effects: s.post_processing
1460
+ })),
1461
+ choreography_json: JSON.stringify(choreography, null, 2),
1462
+ suggested_next_actions: ['describe_visual_state', 'export_package']
1463
+ };
1464
+ }
1465
+
1466
+ // ===== VISUAL FEEDBACK TOOLS (Phase 7.1 — Agent Harness) =====
1467
+
1468
+ /**
1469
+ * Capture the current visualization as a base64 PNG by compositing all canvas layers.
1470
+ * Only works in browser context where canvases exist.
1471
+ */
1472
+ async captureScreenshot(args) {
1473
+ const { width = 512, height = 512, format = 'png', quality = 0.92 } = args;
1474
+
1475
+ const isBrowser = typeof document !== 'undefined';
1476
+ if (!isBrowser) {
1477
+ return {
1478
+ error: {
1479
+ type: 'EnvironmentError',
1480
+ code: 'NO_BROWSER_CONTEXT',
1481
+ message: 'capture_screenshot requires a browser context with canvas elements',
1482
+ suggestion: 'Use the headless renderer tool (tools/headless-renderer.js) for Node.js environments'
1483
+ }
1484
+ };
1485
+ }
1486
+
1487
+ try {
1488
+ // Create composite canvas
1489
+ const composite = document.createElement('canvas');
1490
+ composite.width = width;
1491
+ composite.height = height;
1492
+ const ctx = composite.getContext('2d');
1493
+
1494
+ if (!ctx) {
1495
+ return {
1496
+ error: { type: 'SystemError', code: 'CANVAS_CONTEXT_FAILED', message: 'Could not get 2D context for compositing' }
1497
+ };
1498
+ }
1499
+
1500
+ // Fill with black background
1501
+ ctx.fillStyle = '#000000';
1502
+ ctx.fillRect(0, 0, width, height);
1503
+
1504
+ // Find all visualization canvases and composite them in z-order
1505
+ const canvases = document.querySelectorAll('canvas.visualization-canvas');
1506
+ const sortedCanvases = Array.from(canvases).sort((a, b) => {
1507
+ const zA = parseInt(a.style.zIndex || '0', 10);
1508
+ const zB = parseInt(b.style.zIndex || '0', 10);
1509
+ return zA - zB;
1510
+ });
1511
+
1512
+ for (const canvas of sortedCanvases) {
1513
+ if (canvas.width > 0 && canvas.height > 0) {
1514
+ ctx.drawImage(canvas, 0, 0, width, height);
1515
+ }
1516
+ }
1517
+
1518
+ // If no canvases found, try to find any canvas at all
1519
+ if (sortedCanvases.length === 0) {
1520
+ const anyCanvas = document.querySelector('canvas');
1521
+ if (anyCanvas && anyCanvas.width > 0) {
1522
+ ctx.drawImage(anyCanvas, 0, 0, width, height);
1523
+ }
1524
+ }
1525
+
1526
+ // Convert to data URL
1527
+ const mimeType = format === 'jpeg' ? 'image/jpeg' : format === 'webp' ? 'image/webp' : 'image/png';
1528
+ const dataUrl = composite.toDataURL(mimeType, quality);
1529
+ const base64 = dataUrl.split(',')[1];
1530
+
1531
+ // Clean up
1532
+ composite.remove();
1533
+
1534
+ return {
1535
+ format,
1536
+ width,
1537
+ height,
1538
+ mime_type: mimeType,
1539
+ data_url: dataUrl,
1540
+ base64_length: base64.length,
1541
+ canvas_count: sortedCanvases.length,
1542
+ suggested_next_actions: ['describe_visual_state', 'set_visual_parameters', 'batch_set_parameters']
1543
+ };
1544
+ } catch (err) {
1545
+ return {
1546
+ error: {
1547
+ type: 'SystemError',
1548
+ code: 'SCREENSHOT_FAILED',
1549
+ message: err.message,
1550
+ suggestion: 'Check that canvas elements exist and are rendered'
1551
+ }
1552
+ };
1553
+ }
1554
+ }
1555
+
1556
+ /**
1557
+ * Map a natural-language description to VIB3+ parameters using AestheticMapper.
1558
+ */
1559
+ async designFromDescription(args) {
1560
+ const { description, apply = false } = args;
1561
+
1562
+ if (!this._aestheticMapper) {
1563
+ this._aestheticMapper = new AestheticMapper();
1564
+ }
1565
+
1566
+ const mapped = this._aestheticMapper.mapDescription(description);
1567
+ const resolved = this._aestheticMapper.resolveToValues(description);
1568
+
1569
+ // Apply to engine if requested
1570
+ if (apply && this.engine) {
1571
+ if (resolved.system) {
1572
+ await this.engine.switchSystem(resolved.system);
1573
+ }
1574
+ if (resolved.geometry !== undefined) {
1575
+ this.engine.setParameter('geometry', resolved.geometry);
1576
+ }
1577
+ for (const [param, value] of Object.entries(resolved.params)) {
1578
+ this.engine.setParameter(param, value);
1579
+ }
1580
+ }
1581
+
1582
+ return {
1583
+ description,
1584
+ applied: apply,
1585
+ matched_words: mapped.matched_words,
1586
+ total_words: mapped.total_words,
1587
+ resolved: {
1588
+ system: resolved.system,
1589
+ geometry: resolved.geometry,
1590
+ params: resolved.params,
1591
+ color_preset: resolved.color_preset,
1592
+ post_processing: resolved.post_processing
1593
+ },
1594
+ parameter_ranges: mapped.params,
1595
+ suggested_next_actions: apply
1596
+ ? ['describe_visual_state', 'capture_screenshot', 'create_timeline']
1597
+ : ['design_from_description', 'batch_set_parameters']
1598
+ };
1599
+ }
1600
+
1601
+ /**
1602
+ * Return the full aesthetic vocabulary by category.
1603
+ */
1604
+ getAestheticVocabulary() {
1605
+ if (!this._aestheticMapper) {
1606
+ this._aestheticMapper = new AestheticMapper();
1607
+ }
1608
+
1609
+ return {
1610
+ vocabulary: this._aestheticMapper.getVocabularyByCategory(),
1611
+ all_words: this._aestheticMapper.getVocabulary(),
1612
+ word_count: this._aestheticMapper.getVocabulary().length,
1613
+ usage: 'Pass space-separated words to design_from_description. Example: "serene ocean deep minimal"',
1614
+ suggested_next_actions: ['design_from_description']
1615
+ };
1616
+ }
1617
+
1618
+ /**
1619
+ * Load and play a choreography.
1620
+ */
1621
+ async playChoreographyTool(args) {
1622
+ const { choreography, choreography_id, action = 'play', seek_percent, loop = false } = args;
1623
+
1624
+ // Resolve choreography spec
1625
+ let spec = choreography;
1626
+ if (!spec && choreography_id && this._choreographies) {
1627
+ spec = this._choreographies.get(choreography_id);
1628
+ }
1629
+
1630
+ if (!spec && action === 'play') {
1631
+ return {
1632
+ error: {
1633
+ type: 'ValidationError',
1634
+ code: 'NO_CHOREOGRAPHY',
1635
+ message: 'Provide a choreography spec or a valid choreography_id',
1636
+ suggestion: 'Use create_choreography first, then pass the result here'
1637
+ }
1638
+ };
1639
+ }
1640
+
1641
+ // Create or reuse player
1642
+ if (!this._choreographyPlayer && this.engine) {
1643
+ this._choreographyPlayer = new ChoreographyPlayer(this.engine, {
1644
+ onSceneChange: (index, scene) => {
1645
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, {
1646
+ type: 'choreography_scene',
1647
+ scene_index: index,
1648
+ system: scene.system
1649
+ });
1650
+ }
1651
+ });
1652
+ }
1653
+
1654
+ if (!this._choreographyPlayer) {
1655
+ return {
1656
+ error: {
1657
+ type: 'SystemError',
1658
+ code: 'NO_ENGINE',
1659
+ message: 'Engine not initialized — cannot play choreography',
1660
+ suggestion: 'Initialize the VIB3Engine first'
1661
+ }
1662
+ };
1663
+ }
1664
+
1665
+ const player = this._choreographyPlayer;
1666
+
1667
+ switch (action) {
1668
+ case 'play':
1669
+ if (spec) {
1670
+ player.load(spec);
1671
+ player.loopMode = loop ? 'loop' : 'once';
1672
+ }
1673
+ player.play();
1674
+ break;
1675
+ case 'pause':
1676
+ player.pause();
1677
+ break;
1678
+ case 'stop':
1679
+ player.stop();
1680
+ break;
1681
+ case 'seek':
1682
+ if (seek_percent !== undefined) {
1683
+ player.seekToPercent(seek_percent);
1684
+ }
1685
+ break;
1686
+ }
1687
+
1688
+ return {
1689
+ action,
1690
+ state: player.getState(),
1691
+ suggested_next_actions: action === 'play'
1692
+ ? ['play_choreography', 'capture_screenshot', 'describe_visual_state']
1693
+ : ['play_choreography']
1694
+ };
1695
+ }
1696
+
1697
+ /**
1698
+ * Control a previously created timeline.
1699
+ */
1700
+ controlTimeline(args) {
1701
+ const { timeline_id, action, seek_percent, speed } = args;
1702
+
1703
+ if (!this._liveTimelines) this._liveTimelines = new Map();
1704
+
1705
+ let tl = this._liveTimelines.get(timeline_id);
1706
+
1707
+ // If timeline not live yet, try to create it from stored data
1708
+ if (!tl && this._timelines && this._timelines.has(timeline_id) && this.engine) {
1709
+ const data = this._timelines.get(timeline_id);
1710
+
1711
+ tl = new ParameterTimeline(
1712
+ (name, value) => this.engine.setParameter(name, value)
1713
+ );
1714
+
1715
+ // Build import-compatible format
1716
+ const importData = {
1717
+ type: 'vib3-parameter-timeline',
1718
+ version: '1.0.0',
1719
+ duration: data.duration,
1720
+ loopMode: data.loopMode || 'once',
1721
+ bpm: data.bpm || 120,
1722
+ tracks: {}
1723
+ };
1724
+
1725
+ for (const [param, keyframes] of Object.entries(data.tracks)) {
1726
+ importData.tracks[param] = {
1727
+ enabled: true,
1728
+ keyframes: keyframes.map(kf => ({
1729
+ time: kf.time,
1730
+ value: kf.value,
1731
+ easing: kf.easing || 'easeInOut'
1732
+ }))
1733
+ };
1734
+ }
1735
+
1736
+ tl.importTimeline(importData);
1737
+ this._liveTimelines.set(timeline_id, tl);
1738
+ }
1739
+
1740
+ if (!tl) {
1741
+ return {
1742
+ error: {
1743
+ type: 'ValidationError',
1744
+ code: 'TIMELINE_NOT_FOUND',
1745
+ message: `Timeline '${timeline_id}' not found`,
1746
+ suggestion: 'Create a timeline first with create_timeline'
1747
+ }
1748
+ };
1749
+ }
1750
+
1751
+ switch (action) {
1752
+ case 'play':
1753
+ tl.play();
1754
+ break;
1755
+ case 'pause':
1756
+ tl.pause();
1757
+ break;
1758
+ case 'stop':
1759
+ tl.stop();
1760
+ break;
1761
+ case 'seek':
1762
+ if (seek_percent !== undefined) {
1763
+ tl.seekToPercent(seek_percent);
1764
+ }
1765
+ break;
1766
+ case 'set_speed':
1767
+ if (speed !== undefined) {
1768
+ tl.playbackSpeed = Math.max(0.1, Math.min(10, speed));
1769
+ }
1770
+ break;
1771
+ }
1772
+
1773
+ return {
1774
+ timeline_id,
1775
+ action,
1776
+ playing: tl.playing,
1777
+ currentTime: tl.currentTime,
1778
+ duration: tl.duration,
1779
+ progress: tl.duration > 0 ? tl.currentTime / tl.duration : 0,
1780
+ playbackSpeed: tl.playbackSpeed,
1781
+ suggested_next_actions: ['control_timeline', 'describe_visual_state', 'capture_screenshot']
1782
+ };
1783
+ }
1784
+
1785
+ // ====================================================================
1786
+ // Layer Relationship Tools (Phase 8)
1787
+ // ====================================================================
1788
+
1789
+ /**
1790
+ * Get the holographic system's layer graph (if available).
1791
+ * @private
1792
+ * @returns {import('../../render/LayerRelationshipGraph.js').LayerRelationshipGraph|null}
1793
+ */
1794
+ _getLayerGraph() {
1795
+ if (!this.engine) return null;
1796
+ // Try to access the current system's layer graph
1797
+ const system = this.engine.currentSystem || this.engine._activeSystem;
1798
+ if (system && system.layerGraph) {
1799
+ return system.layerGraph;
1800
+ }
1801
+ if (system && system._layerGraph) {
1802
+ return system._layerGraph;
1803
+ }
1804
+ return null;
1805
+ }
1806
+
1807
+ /**
1808
+ * Load a named layer relationship profile.
1809
+ */
1810
+ setLayerProfile(args) {
1811
+ const { profile } = args;
1812
+ const graph = this._getLayerGraph();
1813
+
1814
+ if (!graph) {
1815
+ return {
1816
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1817
+ suggested_next_actions: ['switch_system']
1818
+ };
1819
+ }
1820
+
1821
+ graph.loadProfile(profile);
1822
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, { type: 'layer_profile', profile });
1823
+
1824
+ return {
1825
+ profile,
1826
+ keystone: graph.keystone,
1827
+ active_profile: graph.activeProfile,
1828
+ available_profiles: ['holographic', 'symmetry', 'chord', 'storm', 'legacy'],
1829
+ suggested_next_actions: ['get_layer_config', 'set_layer_relationship', 'tune_layer_relationship']
1830
+ };
1831
+ }
1832
+
1833
+ /**
1834
+ * Set relationship for a specific layer.
1835
+ */
1836
+ setLayerRelationship(args) {
1837
+ const { layer, relationship, config } = args;
1838
+ const graph = this._getLayerGraph();
1839
+
1840
+ if (!graph) {
1841
+ return {
1842
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1843
+ suggested_next_actions: ['switch_system']
1844
+ };
1845
+ }
1846
+
1847
+ if (config) {
1848
+ graph.setRelationship(layer, { preset: relationship, config });
1849
+ } else {
1850
+ graph.setRelationship(layer, relationship);
1851
+ }
1852
+
1853
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, {
1854
+ type: 'layer_relationship', layer, relationship
1855
+ });
1856
+
1857
+ return {
1858
+ layer,
1859
+ relationship,
1860
+ config: config || {},
1861
+ keystone: graph.keystone,
1862
+ suggested_next_actions: ['get_layer_config', 'tune_layer_relationship', 'describe_visual_state']
1863
+ };
1864
+ }
1865
+
1866
+ /**
1867
+ * Change the keystone (driver) layer.
1868
+ */
1869
+ setLayerKeystone(args) {
1870
+ const { layer } = args;
1871
+ const graph = this._getLayerGraph();
1872
+
1873
+ if (!graph) {
1874
+ return {
1875
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1876
+ suggested_next_actions: ['switch_system']
1877
+ };
1878
+ }
1879
+
1880
+ graph.setKeystone(layer);
1881
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, { type: 'layer_keystone', layer });
1882
+
1883
+ return {
1884
+ keystone: layer,
1885
+ note: 'Other layers\' relationships are preserved. Set new relationships for the old keystone if needed.',
1886
+ suggested_next_actions: ['set_layer_relationship', 'get_layer_config']
1887
+ };
1888
+ }
1889
+
1890
+ /**
1891
+ * Get current layer configuration.
1892
+ */
1893
+ getLayerConfig() {
1894
+ const graph = this._getLayerGraph();
1895
+
1896
+ if (!graph) {
1897
+ return {
1898
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1899
+ suggested_next_actions: ['switch_system']
1900
+ };
1901
+ }
1902
+
1903
+ const config = graph.exportConfig();
1904
+
1905
+ return {
1906
+ keystone: config.keystone,
1907
+ active_profile: config.profile,
1908
+ relationships: config.relationships,
1909
+ shaders: config.shaders,
1910
+ available_profiles: ['holographic', 'symmetry', 'chord', 'storm', 'legacy'],
1911
+ available_presets: ['echo', 'mirror', 'complement', 'harmonic', 'reactive', 'chase'],
1912
+ suggested_next_actions: ['set_layer_profile', 'set_layer_relationship', 'tune_layer_relationship']
1913
+ };
1914
+ }
1915
+
1916
+ /**
1917
+ * Tune a layer's relationship config.
1918
+ */
1919
+ tuneLayerRelationship(args) {
1920
+ const { layer, config: configOverrides } = args;
1921
+ const graph = this._getLayerGraph();
1922
+
1923
+ if (!graph) {
1924
+ return {
1925
+ error: 'Layer relationship graph not available. Switch to holographic system first.',
1926
+ suggested_next_actions: ['switch_system']
1927
+ };
1928
+ }
1929
+
1930
+ const graphConfig = graph.exportConfig();
1931
+ const currentRel = graphConfig.relationships[layer];
1932
+
1933
+ if (!currentRel || !currentRel.preset) {
1934
+ return {
1935
+ error: `Layer "${layer}" has no tunable preset relationship. Set one first with set_layer_relationship.`,
1936
+ suggested_next_actions: ['set_layer_relationship']
1937
+ };
1938
+ }
1939
+
1940
+ const factory = PRESET_REGISTRY[currentRel.preset];
1941
+ if (!factory) {
1942
+ return {
1943
+ error: `Unknown preset "${currentRel.preset}" on layer "${layer}".`,
1944
+ suggested_next_actions: ['set_layer_relationship']
1945
+ };
1946
+ }
1947
+
1948
+ const newConfig = { ...(currentRel.config || {}), ...configOverrides };
1949
+ graph.setRelationship(layer, { preset: currentRel.preset, config: newConfig });
1950
+
1951
+ telemetry.recordEvent(EventType.PARAMETER_CHANGE, {
1952
+ type: 'layer_tune', layer, tuned_keys: Object.keys(configOverrides)
1953
+ });
1954
+
1955
+ return {
1956
+ layer,
1957
+ preset: currentRel.preset,
1958
+ previous_config: currentRel.config,
1959
+ new_config: newConfig,
1960
+ suggested_next_actions: ['get_layer_config', 'describe_visual_state', 'capture_screenshot']
1961
+ };
1962
+ }
946
1963
  }
947
1964
 
948
1965
  // Singleton instance