cozyclay 3.2.0 → 3.2.1
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.
- package/CHANGELOG.md +87 -0
- package/INSTALL_FOR_AGENTS.md +4 -4
- package/README.md +4 -2
- package/bin/agent/agent-failure-codes.mjs +34 -8
- package/bin/agent/agent-routes.mjs +6 -6
- package/bin/agent/agent-runner.mjs +16 -13
- package/bin/agent/agent-stream.mjs +26 -3
- package/bin/agent/main-prompt.mjs +1 -1
- package/bin/agent/studio-prompt.mjs +2 -2
- package/bin/agent/supervisor-followup.mjs +34 -0
- package/bin/agent/supervisor.mjs +6 -6
- package/bin/cozyclay.mjs +23 -60
- package/bin/star-prompt.mjs +15 -14
- package/bin/studio-routes.mjs +6 -3
- package/bin/telemetry-state.mjs +5 -7
- package/bin/update-check.mjs +61 -7
- package/bin/update-install.mjs +16 -3
- package/bin/update-restart.mjs +95 -0
- package/bin/update-routes.mjs +25 -3
- package/dist/app/index.html +13 -7
- package/dist/assets/TaskBoard-tgO0MNKL.js +1 -0
- package/dist/assets/analytics-CaTUuLPM.js +1 -0
- package/dist/assets/app-CnDkB5WO.css +1 -0
- package/dist/assets/app-DJt06Oyb.js +5144 -0
- package/dist/assets/{compile-worker-DRWRa9pr.js → compile-worker-D1pl6G7x.js} +5 -5
- package/dist/assets/landing-2Fy7DU41.js +1 -0
- package/dist/assets/modulepreload-polyfill-P2Xu9kJm.js +1 -0
- package/dist/assets/motion3-compile-BxaM0Xwl.js +1 -0
- package/dist/assets/scene-asset-sync-CLGNbEBq.js +13 -0
- package/dist/assets/task-board-UVGTZLRF.js +15 -0
- package/dist/assets/workflow-CSiBlX1s.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +4 -1
- package/dist/privacy/index.html +9 -6
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -4
- package/mcp/README.md +1 -1
- package/mcp/live-hub.mjs +4 -4
- package/mcp/tool-handlers.mjs +14 -16
- package/package.json +2 -1
- package/src/App.jsx +37 -70
- package/src/analytics-controls.js +25 -3
- package/src/analytics-strict.js +16 -4
- package/src/analytics.js +32 -26
- package/src/app-version.js +13 -0
- package/src/asset-drop.js +39 -0
- package/src/asset-pane.css +34 -0
- package/src/asset-pane.jsx +4 -35
- package/src/bus-tasks.js +1 -1
- package/src/character-kind.js +8 -6
- package/src/click-friction.js +5 -4
- package/src/command-bus-telemetry.js +8 -7
- package/src/command-bus.js +2 -16
- package/src/commands/cast.js +2 -1
- package/src/commands/motion3.js +3 -1
- package/src/commands/objects.js +2 -37
- package/src/commands/project.js +3 -3
- package/src/commands/sculpt-compare.js +53 -0
- package/src/content-tile.jsx +36 -0
- package/src/domains/cast.js +7 -6
- package/src/domains/motion.js +6 -5
- package/src/domains/scenes.js +41 -31
- package/src/execution-telemetry.js +7 -3
- package/src/export-menu-telemetry.js +26 -0
- package/src/finger-handles.jsx +128 -0
- package/src/friction-hint.js +82 -0
- package/src/live-control.js +2 -1
- package/src/locale-zh/panels.js +20 -0
- package/src/locale-zh/shell.js +4 -0
- package/src/locale-zh/workspace.js +6 -0
- package/src/motion/find-bone.js +13 -0
- package/src/motion/fingers.js +170 -0
- package/src/motion/fingertip-pick.js +21 -0
- package/src/motion/ik-exposure.js +48 -0
- package/src/motion/ik.js +4 -11
- package/src/motion3/compiler/head-lead.js +4 -2
- package/src/motion3/hybrid-hint.js +19 -0
- package/src/motion3/load.js +19 -12
- package/src/motion3/qa/hints-contact.js +12 -0
- package/src/motion3/qa/patch.js +4 -1
- package/src/motion3/qa/verdict.js +7 -2
- package/src/motion3/repair.js +28 -8
- package/src/motion3/skills/catch.js +2 -1
- package/src/panels/FingerPanel.jsx +38 -0
- package/src/panels/RigControlPanel.jsx +9 -1
- package/src/panels/SculptPanel.jsx +12 -12
- package/src/panels/SculptPartTree.jsx +46 -0
- package/src/panels/SubjectsPanel.jsx +1 -1
- package/src/panels/sculpt.css +11 -0
- package/src/pose-fade.js +7 -3
- package/src/posestudio.jsx +14 -27
- package/src/project-browser.css +6 -0
- package/src/project-browser.jsx +9 -18
- package/src/project-start-choices.jsx +45 -0
- package/src/project.js +16 -12
- package/src/proxy-figure-shape.js +11 -0
- package/src/proxy-figure.jsx +25 -18
- package/src/scene-assets.js +17 -13
- package/src/scenes.js +22 -11
- package/src/sculpt-blueprint.js +56 -0
- package/src/sculpt-mode.js +5 -0
- package/src/sculpt-part-edit.js +115 -0
- package/src/sculpt-part-gizmo.jsx +159 -0
- package/src/sculpt-part-outline.jsx +72 -0
- package/src/sculpt-pick.js +53 -0
- package/src/sculpt-run-text.js +51 -0
- package/src/sculpt-run.js +10 -46
- package/src/sculpt-shelf-file.js +92 -0
- package/src/sculpt-shelf-tiles.jsx +69 -0
- package/src/sculpt-shelf.js +108 -0
- package/src/sculpt-stage.jsx +39 -2
- package/src/sculpt-thumb.js +72 -0
- package/src/sculpt-views.js +75 -0
- package/src/semantic-edit.js +0 -87
- package/src/settings-menu.jsx +2 -2
- package/src/shell/DetailsSlot.jsx +5 -6
- package/src/shell/LibrarySlot.jsx +8 -10
- package/src/shell/MenuBar.jsx +6 -6
- package/src/shell/PreferencesDialog.jsx +2 -3
- package/src/shell/ViewportToolbar.jsx +15 -18
- package/src/shell/add-character-entries.js +13 -0
- package/src/shell/pose-controls.js +32 -0
- package/src/shell/preference-pages.jsx +2 -11
- package/src/shell/storyboard-caption.js +21 -0
- package/src/shell/studio-shell-context.js +1 -0
- package/src/shortcut-friction.js +2 -1
- package/src/site/landing-analytics.js +28 -0
- package/src/studio-actions.js +3 -2
- package/src/studio-agent-protocol.js +4 -4
- package/src/studio-app-binding.js +4 -3
- package/src/update/UpdateChip.jsx +21 -2
- package/src/update/UpdateDialog.jsx +26 -6
- package/src/update/update-client.js +123 -17
- package/src/update/update-telemetry.js +15 -3
- package/src/use-sculpt-mode.js +62 -8
- package/src/workflow/agent-failure-codes.js +28 -9
- package/src/workflow/agent-turn-telemetry.js +1 -1
- package/src/workflow/task-board.js +2 -2
- package/tools/dev/page-shell.py +1 -1
- package/tools/dev/pages/greybox-to-video.html +21 -2
- package/tools/dev/pages/previs-software.html +3 -3
- package/tools/dev/pages/privacy.html +9 -6
- package/tools/dev/pages/seedance-camera-control.html +2 -2
- package/tools/loc-baseline.json +10 -10
- package/tools/run-tests.mjs +45 -69
- package/tools/select-tests.mjs +119 -0
- package/tools/test-durations.json +455 -0
- package/tools/test-graph.mjs +181 -0
- package/tools/test-shards.mjs +78 -0
- package/tools/tutorial/README.md +74 -0
- package/tools/tutorial/assemble.mjs +43 -0
- package/tools/tutorial/captions.mjs +43 -0
- package/tools/tutorial/cdp.mjs +55 -0
- package/tools/tutorial/cursor.mjs +159 -0
- package/tools/tutorial/episode.mjs +173 -0
- package/tools/tutorial/episodes/beginner-guide/00-install.mjs +42 -0
- package/tools/tutorial/episodes/beginner-guide/01-start.mjs +50 -0
- package/tools/tutorial/episodes/beginner-guide/02-layout.mjs +67 -0
- package/tools/tutorial/episodes/beginner-guide/03-stage.mjs +58 -0
- package/tools/tutorial/episodes/beginner-guide/04-motion.mjs +98 -0
- package/tools/tutorial/episodes/beginner-guide/05-camera.mjs +47 -0
- package/tools/tutorial/episodes/beginner-guide/06-export.mjs +59 -0
- package/tools/tutorial/episodes/beginner-guide/steps.mjs +32 -0
- package/tools/tutorial/episodes/beginner-guide.mjs +17 -0
- package/tools/tutorial/episodes/cast.mjs +12 -0
- package/tools/tutorial/episodes/first-video.mjs +121 -0
- package/tools/tutorial/episodes/stub-tour.mjs +97 -0
- package/tools/tutorial/launch.mjs +63 -0
- package/tools/tutorial/mux.mjs +61 -0
- package/tools/tutorial/overlay.mjs +142 -0
- package/tools/tutorial/record.mjs +156 -0
- package/tools/tutorial/scenes/terminal.html +63 -0
- package/tools/tutorial/steps.mjs +111 -0
- package/tools/tutorial/terminal.mjs +43 -0
- package/tools/tutorial/voice.mjs +49 -0
- package/tools/update-test-durations.mjs +42 -0
- package/tools/verify-package-tarball.mjs +18 -12
- package/dist/assets/TaskBoard-BUjjuvm5.js +0 -1
- package/dist/assets/app-Dige7q3X.css +0 -1
- package/dist/assets/app-DxoI941o.js +0 -5144
- package/dist/assets/motion3-compile-B4SX4TGx.js +0 -1
- package/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/dist/assets/scene-asset-sync-LeI3eSnO.js +0 -13
- package/dist/assets/task-board-5PKn7ZaA.js +0 -15
- package/dist/assets/workflow-Dg9jCKOq.js +0 -179
|
@@ -87,7 +87,7 @@ cclay telemetry on</code></pre>
|
|
|
87
87
|
<tbody>
|
|
88
88
|
<tr><td>install:first_launch, app:session_started, app:session_ended</td><td>Counting installs and sessions; duration and action counts are bucketed. install:first_launch may carry heard_from (x, hn, reddit, github, friend, other, site or playground), from the first-launch prompt or a copied <code>npx cozyclay --via</code> command. app:session_ended also carries scenes_touched (how many scenes were created or loaded in the session, an exact count capped at 20, not bucketed), session_id (the opaque random session id the PostHog SDK generates, used only to join a session's last events, such as an error just before it ended) and per-session edit gesture counts in five groups, pose_edit_bucket, camera_edit_bucket, object_edit_bucket, shot_edit_bucket and prompt_edit_bucket, each bucketed (0, 1-3, 4-10 or gte11); never what was edited</td></tr>
|
|
89
89
|
<tr><td>app:friction_summary</td><td>Once per session, next to app:session_ended: how much friction the session had, as closed buckets only. active_time (the session duration bucket); counts of refusals the Studio gave the user (refusals_ui) and the agent (refusals_agent), user undos (undos), errors (errors), browser long tasks (long_tasks) and motion compiles (compiles), each as 0, 1, 2, 3, 4-5, 6-10 or gte11; the longest long task (long_task_max: lt50, 50-150, 150-500, 500-2000 or gte2000 ms); the share of late playback ticks (late_tick_ratio: 0, lt5, 5-20, 20-50 or gte50 %). rage_clicks and dead_clicks count every ui:rage_click and ui:dead_click, shortcut_ignored every shortcut:ignored, search_empty every search:no_results, and nav_thrash and camera_thrash every nav:thrash and camera:thrash, including those past their event's cap. Never which command, control, key or text was involved.</td></tr>
|
|
90
|
-
<tr><td>studio:refused</td><td>A command the Studio refused, as closed values only: code (one of the Studio's fixed error codes, such as INVALID_ARGUMENT, STALE_SCENE or TARGET_BUSY, else other), origin (ui for the user's own control, agent for the agent or an MCP client), phase (admission, refused before any change, or commit, refused after it started changing)
|
|
90
|
+
<tr><td>studio:refused</td><td>A command the Studio refused, as closed values only: code (one of the Studio's fixed error codes, such as INVALID_ARGUMENT, STALE_SCENE or TARGET_BUSY, else other), origin (ui for the user's own control, agent for the agent or an MCP client), phase (admission, refused before any change, or commit, refused after it started changing) group (the command's family: object, shot, cast, scene, motion, edit, task, agent or other) and command (the refused action's id, such as object.set or shot.setCameraRail, only when it is one of the actions the Studio itself declares; a name the caller made up is left out). At most 3 per code, command (or group, without one) and origin per session; the rest are only counted in app:friction_summary. A cancel the caller asked for is not a refusal. Never the command's arguments, target or message.</td></tr>
|
|
91
91
|
<tr><td>edit:undone</td><td>A user undo (agent undos are not counted): age (lt2s, 2-10s, 10-60s or gte60s since the undone edit was made, only when the undo was done and its age is known), kind (pose, camera, object, shot, prompt, agent_turn for the agent's whole turn, ik_preview for an un-applied IK drag, or other), via (key, menu or button) and result (done, noop when there was nothing to undo, or refused). At most 20 per session. Never what was undone.</td></tr>
|
|
92
92
|
<tr><td>agent:turn_summary</td><td>One per agent turn: turn_id (the same random id as agent:turn_*); outcome (succeeded, failed or cancelled); wall (the turn's length) and first_tool (time to the first tool call, absent without tools), each lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m; counts of tool calls (tool_calls), failed tool calls (tool_failed), tool calls the Studio refused (refused), commands re-sent once after a stale revision or a busy gesture (stale_retry) and motion compiles (drafts), each as 0, 1, 2, 3, 4-5, 6-10 or gte11; refusal_top (the most frequent refusal code, or none); authored (true or false: the turn changed the scene); qa_first and qa_last (clean or problems, absent without drafts); qa_trend (improved, flat, worse, or na with fewer than 2 drafts); stuck (true or false: the motion compiler's stop rule fired); cancel_phase (before_tool, during_tool, after_tool or na). No prompt text, tool names, messages or responses.</td></tr>
|
|
93
93
|
<tr><td>agent:post_turn_edit</td><td>One per agent turn: turn_id (the same random id) and the user's own edit gestures in the 120 seconds after the turn ended, closed early when the next turn starts or the page closes. edited (true or false); counts per group (pose, camera, object, shot, and prompt_block, which counts edits to a take's prompt and take fields), each as 0, 1, 2, 3, 4-5, 6-10 or gte11, where one continuous drag is one gesture; first_delay (time to the first edit: lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m), only when edited is true. Never which object or what changed.</td></tr>
|
|
@@ -103,7 +103,7 @@ cclay telemetry on</code></pre>
|
|
|
103
103
|
<tr><td>agent:connect_finished</td><td>That attempt ended: provider, method, outcome (ok, failed, cancelled with the Cancel button, abandoned when closed or left unfinished), step it had reached (list, prompt, waiting or done) and duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m). Never the key, token, account, email or error text.</td></tr>
|
|
104
104
|
<tr><td>agent:model_changed</td><td>A model picked in the agent's model list: its provider (as above, or other) and after_failure (whether the last agent turn had failed). At most 10 per session.</td></tr>
|
|
105
105
|
<tr><td>agent:prompt_sent</td><td>One per prompt sent to the agent: turn_id (the same random id as agent:turn_*); provider (as for agent:connect_started, or other); via (typed, retry_button, regenerate, chip or supervisor); prompt_len (1-20, 21-80, 81-240 or gte241 characters); compose_ms (first keystroke to send, typed prompts only) and gap (since the previous turn ended, or first) as lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m; similarity (first, same, near or different, computed in the browser against the previous prompt's words); prev_outcome (succeeded, failed, cancelled or none); turn_index (1, 2, 3, 4-5, 6-10 or gte11); attached and task_mode (true or false). Never the prompt text or its words.</td></tr>
|
|
106
|
-
<tr><td>ui:rage_click</td><td>Three or more fast clicks (within 1 second and 24 pixels) on one primary control: control (a control id from a fixed list, such as tl_play or export_video, or other), clicks (3, 4-5 or gte6) and changed (whether the Studio responded within half a second). At most 2 per control per session. No coordinates, no text, no element or object ids.</td></tr>
|
|
106
|
+
<tr><td>ui:rage_click</td><td>Three or more fast clicks (within 1 second and 24 pixels) on one primary control: control (a control id from a fixed list, such as tl_play or export_video; for a control not on that list, the Studio area it sits in: other_guide, other_agent, other_menu, other_sculpt, other_assets, other_board, other_timeline, other_inspector or other_viewport; else other), clicks (3, 4-5 or gte6) and changed (whether the Studio responded within half a second). At most 2 per control per session. No coordinates, no text, no element or object ids.</td></tr>
|
|
107
107
|
<tr><td>ui:dead_click</td><td>A click on a primary control that did nothing: control (as above) and reason (aria_disabled when the control was disabled, refused when the Studio refused the command, no_effect when nothing changed within half a second). At most 2 per control per session. Clicks into text fields are never observed.</td></tr>
|
|
108
108
|
<tr><td>feature:used</td><td>One signal per feature per session: a fixed feature name such as pose_edit (a real bone or pose change), pose_save (saving a pose to the library), camera_fly, export_video, export_storyboard or export_render_pass</td></tr>
|
|
109
109
|
<tr><td>$pageview, scene:created, scene:loaded, craft:first_action, activation:completed</td><td>Funnel and drop-off analysis</td></tr>
|
|
@@ -115,14 +115,15 @@ cclay telemetry on</code></pre>
|
|
|
115
115
|
<tr><td>workflow:result_applied</td><td>Captured or generated image/video output was written to the Workflow canvas: run_id only, at most once per run, independently of a later failure. No image bytes or output contents.</td></tr>
|
|
116
116
|
<tr><td>agent:turn_requested</td><td>Explicit Agent turn requests from surface (studio or workflow), paired by ephemeral turn_id. No prompts, responses or model identifiers.</td></tr>
|
|
117
117
|
<tr><td>agent:tool_executed</td><td>Agent tool observations: turn_id, tool_category (workflow_read, workflow_write, workflow_run, frame_capture, image_generate, scene_write or other), outcome (succeeded, failed or cancelled) and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s). Tool names, arguments and results are excluded.</td></tr>
|
|
118
|
-
<tr><td>agent:turn_succeeded, agent:turn_failed, agent:turn_cancelled</td><td>Agent turn outcomes: turn_id and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s); failures and actual cancellations also include failure_code (aborted, auth, rate_limited, tool_failed, upstream, key_refused, stale_scene, capability_missing, busy, invalid_request or unknown). auth means CozyClay's local agent has no sign-in or refused the session; key_refused means the model provider rejected the key or credentials; stale_scene means the turn was refused because the scene changed underneath it; capability_missing means a required capability was unavailable, for example the live hub port was already in use; busy means another turn or target was busy; invalid_request means the request itself was rejected. Missing terminal delivery is not success.</td></tr>
|
|
118
|
+
<tr><td>agent:turn_succeeded, agent:turn_failed, agent:turn_cancelled</td><td>Agent turn outcomes: turn_id and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s); failures and actual cancellations also include failure_code (aborted, auth, rate_limited, tool_failed, upstream, network, key_refused, stale_scene, capability_missing, busy, invalid_request or unknown). upstream means the model provider failed (an HTTP 5xx, overload or a cut-off stream); network means the request never got an HTTP answer (offline, connection reset or DNS); unknown is any other failure. auth means CozyClay's local agent has no sign-in or refused the session; key_refused means the model provider rejected the key or credentials; stale_scene means the turn was refused because the scene changed underneath it; capability_missing means a required capability was unavailable, for example the live hub port was already in use; busy means another turn or target was busy; invalid_request means the request itself was rejected. Missing terminal delivery is not success.</td></tr>
|
|
119
119
|
<tr><td>agent:result_applied</td><td>An Agent turn produced an acknowledged editor change: turn_id only, at most once per turn. Successful reads and focus/navigation do not prove applied output.</td></tr>
|
|
120
|
-
<tr><td>mcp:tool_requested</td><td>Connected-editor MCP execution requests: ephemeral request_id
|
|
121
|
-
<tr><td>mcp:tool_executed</td><td>Connected-editor MCP outcomes: request_id, tool_category (read, camera, scene_write, prompt_authoring, frame_capture, motion_generate, motion_apply, project_io or other), outcome (succeeded, failed, uncertain or cancelled) and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s). Control-plane success does not prove a mutation applied.</td></tr>
|
|
120
|
+
<tr><td>mcp:tool_requested</td><td>Connected-editor MCP execution requests: ephemeral request_id, tool_category (read, camera, scene_write, prompt_authoring, frame_capture, motion_generate, motion_apply, project_io or other) and tool_name, the CozyClay tool that ran (describe_scene, describe_shot, render_prompt, mark_camera_move, describe_camera_move, save_project, set_camera, frame_shot, add_character, place_character, remove_character, focus_character, place_object, import_mesh, sculpt_object, compare_sculpt, group_objects, update_object, remove_object, apply_batch, add_scene, switch_scene, open_project, capture_frame, studio_commands, studio_run or other; never its arguments).</td></tr>
|
|
121
|
+
<tr><td>mcp:tool_executed</td><td>Connected-editor MCP outcomes: request_id, tool_category (read, camera, scene_write, prompt_authoring, frame_capture, motion_generate, motion_apply, project_io or other), tool_name (describe_scene, describe_shot, render_prompt, mark_camera_move, describe_camera_move, save_project, set_camera, frame_shot, add_character, place_character, remove_character, focus_character, place_object, import_mesh, sculpt_object, compare_sculpt, group_objects, update_object, remove_object, apply_batch, add_scene, switch_scene, open_project, capture_frame, studio_commands, studio_run or other), outcome (succeeded, failed, uncertain or cancelled) and duration_bucket (lt1s, 1-3s, 3-10s, 10-30s or gte30s). Control-plane success does not prove a mutation applied.</td></tr>
|
|
122
122
|
<tr><td>mcp:result_applied</td><td>A connected editor acknowledged an MCP mutation, paired by request_id. Detached or disconnected MCP sessions remain unobserved.</td></tr>
|
|
123
123
|
<tr><td>export:blocking_frame_succeeded, export:video_succeeded</td><td>Export reliability</td></tr>
|
|
124
124
|
<tr><td>export:attempt_started, export:attempt_succeeded, export:attempt_failed, export:attempt_cancelled</td><td>Export attempts and outcomes, paired by an ephemeral random attempt_id; export_kind (video, depth_video, frame, keyframe_pack, contact_sheet, panel_pack, animatic, animation_project, workflow_send, storyboard, render_pass or otio), format (mp4, png, zip, cclayproject or otio), initiating surface (studio, workflow or embed), is_retry (whether the attempt retries an earlier one), retry_of (the retried attempt's attempt_id, only when is_retry is true) and objects (the scene's object count, bucketed 0, 1-3, 4-10 or gte11). Terminal events include a bucketed duration_bucket; failures and real cancellations include a normalized failure_code (unsupported_codec, encode_failed, render_failed, aborted or unknown), the stage where the export stopped (preparing, encoding, finalizing or handoff) and a progress bucket (0, 1-25, 26-50, 51-75 or 76-99 percent), never raw error messages, filenames, URLs, paths, prompts or scene content. Success means pipeline completion or download handoff, not confirmation that the OS saved a file.</td></tr>
|
|
125
125
|
<tr><td>export:saved</td><td>How each exported file was saved, one event per file: method (workspace, written into the export folder you chose, or download, a browser download) and reason (none; unsupported, the browser has no folder access; no_workspace, no export folder is chosen; permission, folder access was denied; or write_failed, the folder write failed and the file fell back to a download). Never the file name or path.</td></tr>
|
|
126
|
+
<tr><td>export:menu_opened</td><td>The Export menu opened, once per open: surface (studio or embed), mode (animation or storyboard), webcodecs (whether this browser has the video encoder an MP4 export needs; a yes/no check, not a codec test) and shots (shot count bucket, panels in a storyboard: 0, 1-3, 4-10, gte11). Never shot names or what you exported.</td></tr>
|
|
126
127
|
<tr><td>export:keyframe_pack</td><td>Legacy Workflow pack handoffs, with numeric entries count and source=workflow; not an additional export attempt</td></tr>
|
|
127
128
|
<tr><td>sculpt_run</td><td>One event per finished Sculpt mode run: outcome (done, cancelled or failed), rounds (0 to 10), final_iou (how closely the final sculpt matched the reference image, 0 to 1 rounded to two decimals; omitted when the run was never scored), duration_ms (elapsed run time in milliseconds) and had_target (whether the run reworked an existing sculpt instead of creating one). Numbers only: never the reference image, recipe, part names or sculpt id.</td></tr>
|
|
128
129
|
<tr><td>playground:opened, playground:first_action</td><td>Whether the home-page playground gets used</td></tr>
|
|
@@ -131,6 +132,7 @@ cclay telemetry on</code></pre>
|
|
|
131
132
|
<tr><td>app:gl_lost</td><td>The 3D view's graphics failed, at most once per kind per session: kind (unavailable when WebGL is off at startup, context_lost when the GPU context drops later). Never the GPU renderer string.</td></tr>
|
|
132
133
|
<tr><td>studio:ready</td><td>Once per session: how long the studio took to show its first frame, from page navigation to the first frame after the first render: boot (lt2s, 2-5s, 5-15s or gte15s). Never what was loaded.</td></tr>
|
|
133
134
|
<tr><td>playback:summary</td><td>One per playback run (play until stop, or until the frame rate or speed changes), at most 10 per session: duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m); late_ticks, an estimate of dropped frames, not a measured one: the share of playback timer ticks that had to catch up more than one frame (0, lt5, 5-20, 20-50 or gte50 %); max_catchup (the most frames one tick caught up: 1, 2, 3-5 or gte6); fps (12, 24, 30, 60 or other); speed (0.5, 1, 2 or other); measured_fps (the browser's frame rate during playback, the device:profile frame-rate buckets). Every run also feeds app:friction_summary.late_tick_ratio. Never the scene, the take or the frame range.</td></tr>
|
|
135
|
+
<tr><td>landing:cta_clicked</td><td>The landing page (cozyclay.org/) sends one $pageview and, when a call to action is pressed, which one: target (app, github, discord, npm, copy_npx, copy_clone, copy_mcp, copy_terminal or agent_install_doc). Never the URL or the copied text. The landing page sends no session, error, device or performance events.</td></tr>
|
|
134
136
|
<tr><td>guide:opened</td><td>The first-60-seconds guide opened, at most once per session: mode (animation or storyboard). Never step text or what you did.</td></tr>
|
|
135
137
|
<tr><td>guide:step</td><td>A guide step marked done, each step at most once per session: mode (animation or storyboard), step (1 to 4) and step_ms (time on that step: lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m).</td></tr>
|
|
136
138
|
<tr><td>guide:dismissed</td><td>The guide closed before its last step, at most once per session: mode (animation or storyboard) and reached (1 to 4, the step it was showing).</td></tr>
|
|
@@ -139,8 +141,9 @@ cclay telemetry on</code></pre>
|
|
|
139
141
|
<tr><td>object:deleted_soon</td><td>You deleted an object you added in this session, at most 10 per session: age (how long after adding it: lt2s, 2-10s, 10-60s or gte60s). Object ids are reused names, so this is an estimate. Never the object's id, name or kind.</td></tr>
|
|
140
142
|
<tr><td>object:delete_undone</td><td>You undid an object deletion within its 7-second undo window, at most 10 per session: age (time from the deletion to the undo: lt2s or 2-10s). Never the object's id, name or kind.</td></tr>
|
|
141
143
|
<tr><td>update:notice_shown</td><td>The update notice appeared in the top bar of an npm studio, once per session: kind (global or npx) and behind (patch, minor or major: how far the running version is behind the newest release). Never a path or a command</td></tr>
|
|
142
|
-
<tr><td>update:clicked</td><td>A button on the update notice, at most 10 per session: action (update_now, copy_command, later, dismiss or
|
|
144
|
+
<tr><td>update:clicked</td><td>A button on the update notice, at most 10 per session: action (update_now, copy_command, later, dismiss, release_notes or restart) and kind (global or npx)</td></tr>
|
|
143
145
|
<tr><td>update:finished</td><td>A one-click update ended, at most 3 per session: outcome (ok or failed), error_class (none, permission, network, node_version, npm_missing or other) and duration (lt10s, 10-30s, 30-60s, 1-2m, 2-5m or gte5m). Never npm's output, a path or the command line</td></tr>
|
|
146
|
+
<tr><td>update:restarted</td><td>Restart now ended in the tab that pressed it, once per session: outcome (ok: the updated launcher answered; timeout: it did not within 30 seconds). Never a version string, a path or the command line</td></tr>
|
|
144
147
|
<tr><td>device:profile</td><td>Once per session, whether the studio can run smoothly: gpu_vendor (nvidia, amd, intel, apple, qualcomm, arm, software, other or unknown), gpu_class (discrete, integrated, software or unknown), webgl (webgl2, webgl1 or none), cpu_bucket (1-4, 5-8, 9-16, gte17 or unknown), memory_bucket (lt4, 4-7, gte8 or unknown) and frame_rate_bucket (lt20, 20-40, 40-55, gte55 or unknown) from one three-second sample. The GPU renderer string and exact hardware values never leave the browser.</td></tr>
|
|
145
148
|
<tr><td>survey:use_case</td><td>An optional question shown at most once after a successful export, only while telemetry is on: use_case (animation, film, game, ad_mv, personal, other or skip) and team (team, solo or skip). Closed choices only; no free text.</td></tr>
|
|
146
149
|
<tr><td>hosted:composer_viewed, hosted:login_started, hosted:ticket_created, hosted:result_opened, hosted:opened_in_studio</td><td>Hosted demo funnel</td></tr>
|
|
@@ -87,11 +87,11 @@
|
|
|
87
87
|
Any 3D tool that renders flat can make the reference. Blender works with a clay material and a
|
|
88
88
|
workbench render. CozyClay, open source and in the browser, exports the flat clip directly with the
|
|
89
89
|
delivery ratio and a camera timeline built in; the reel at the top of this page came out of it.
|
|
90
|
-
The <a href="
|
|
90
|
+
The <a href="/app/">browser studio</a> runs it in a tab, and
|
|
91
91
|
<code>npx cozyclay</code> runs it locally.
|
|
92
92
|
</p>
|
|
93
93
|
<div class="cta">
|
|
94
|
-
<a class="btn primary" href="
|
|
94
|
+
<a class="btn primary" href="/app/">Try it in your browser</a>
|
|
95
95
|
<a class="btn ghost" href="https://github.com/NomaDamas/CozyClay">View on GitHub</a>
|
|
96
96
|
</div>
|
|
97
97
|
</section>
|
package/tools/loc-baseline.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"bin/agent/session-store.mjs": 324,
|
|
6
6
|
"bin/agent/supervisor.mjs": 283,
|
|
7
7
|
"bin/agent/video-adapters.mjs": 334,
|
|
8
|
-
"bin/cozyclay.mjs":
|
|
8
|
+
"bin/cozyclay.mjs": 348,
|
|
9
9
|
"bin/live/cli.mjs": 456,
|
|
10
10
|
"mcp/live-hub.mjs": 529,
|
|
11
11
|
"mcp/tool-handlers.mjs": 1730,
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
"mcp/verify-live-p0.mjs": 264,
|
|
16
16
|
"mcp/verify-live.mjs": 416,
|
|
17
17
|
"src/analytics.js": 948,
|
|
18
|
-
"src/App.jsx":
|
|
19
|
-
"src/asset-pane.jsx":
|
|
18
|
+
"src/App.jsx": 4418,
|
|
19
|
+
"src/asset-pane.jsx": 614,
|
|
20
20
|
"src/camera-follow.js": 505,
|
|
21
21
|
"src/camera-move.js": 289,
|
|
22
|
-
"src/command-bus.js":
|
|
22
|
+
"src/command-bus.js": 341,
|
|
23
23
|
"src/controls.jsx": 330,
|
|
24
24
|
"src/domains/cast.js": 717,
|
|
25
25
|
"src/domains/motion.js": 806,
|
|
@@ -33,21 +33,21 @@
|
|
|
33
33
|
"src/matte.js": 472,
|
|
34
34
|
"src/motion/collision-blockers.js": 273,
|
|
35
35
|
"src/motion/fix-collisions.js": 748,
|
|
36
|
-
"src/motion/ik.js":
|
|
36
|
+
"src/motion/ik.js": 1128,
|
|
37
37
|
"src/motion/range-pin.js": 277,
|
|
38
38
|
"src/motion/timeline.jsx": 2042,
|
|
39
39
|
"src/object-gizmo.jsx": 740,
|
|
40
40
|
"src/object-path.js": 410,
|
|
41
41
|
"src/panels/ObjectTransformPanel.jsx": 545,
|
|
42
|
-
"src/panels/SculptPanel.jsx":
|
|
42
|
+
"src/panels/SculptPanel.jsx": 369,
|
|
43
43
|
"src/planview.jsx": 887,
|
|
44
44
|
"src/poses.js": 321,
|
|
45
|
-
"src/posestudio.jsx":
|
|
45
|
+
"src/posestudio.jsx": 1226,
|
|
46
46
|
"src/project-browser.jsx": 371,
|
|
47
47
|
"src/project.js": 603,
|
|
48
48
|
"src/props.jsx": 414,
|
|
49
49
|
"src/range-pin-panel.jsx": 305,
|
|
50
|
-
"src/scene-assets.js":
|
|
50
|
+
"src/scene-assets.js": 399,
|
|
51
51
|
"src/scene-mesh.js": 350,
|
|
52
52
|
"src/scene-objects.js": 1134,
|
|
53
53
|
"src/scenes.js": 385,
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"src/sculpt-run.js": 309,
|
|
57
57
|
"src/shell/BoardSlot.jsx": 316,
|
|
58
58
|
"src/shell/BottomDock.jsx": 416,
|
|
59
|
-
"src/shell/DetailsSlot.jsx":
|
|
59
|
+
"src/shell/DetailsSlot.jsx": 333,
|
|
60
60
|
"src/shell/MenuBar.jsx": 632,
|
|
61
61
|
"src/shell/ViewportToolbar.jsx": 567,
|
|
62
62
|
"src/shot-authoring.js": 345,
|
|
@@ -70,5 +70,5 @@
|
|
|
70
70
|
"src/workflow/AgentPanel.jsx": 807,
|
|
71
71
|
"src/workflow/WorkflowBuilder.jsx": 592,
|
|
72
72
|
"tools/qa/css-rule-usage.mjs": 251,
|
|
73
|
-
"tools/run-tests.mjs":
|
|
73
|
+
"tools/run-tests.mjs": 407
|
|
74
74
|
}
|
package/tools/run-tests.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
4
4
|
import { availableParallelism, tmpdir } from "node:os";
|
|
5
5
|
import { join, relative } from "node:path";
|
|
6
|
+
import { assignShards, parseShard, readDurations, readSelection, reportDurations, slowestFirst } from "./test-shards.mjs";
|
|
6
7
|
|
|
7
8
|
// Whatever a suite drives through the agent sidecar lands in a scratch
|
|
8
9
|
// session dir for the whole run, never in the author's ~/.config (#375).
|
|
@@ -13,16 +14,16 @@ if (!process.env.COZYCLAY_AGENT_SESSIONS_DIR) {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const NODE_FILES = [
|
|
16
|
-
"test/motion3/verify-motion3-core.mjs", "test/motion3/verify-playback-frame-store.mjs", "test/verify-playback-render-isolation.mjs", "test/motion3/verify-motion3-idle.mjs", "test/motion3/verify-motion3-life.mjs", "test/motion3/verify-motion3-headlead.mjs", "test/motion3/verify-motion3-gait.mjs", "test/motion3/verify-motion3-overlap.mjs", "test/motion3/verify-motion3-flip.mjs", "test/motion3/verify-motion3-float-fix.mjs", "test/motion3/verify-motion3-timing.mjs", "test/motion3/verify-motion3-self.mjs", "test/motion3/verify-motion3-commands.mjs", "test/motion3/verify-motion3-exitcar.mjs", "test/motion3/verify-motion3-agentloop.mjs", "test/motion3/verify-motion3-search.mjs", "test/motion3/verify-motion3-skills.mjs", "test/motion3/verify-motion3-carried.mjs", "test/motion3/verify-motion3-people.mjs", "test/motion3/verify-motion3-hit.mjs", "test/motion3/verify-motion3-person-fix.mjs", "test/motion3/verify-motion3-socket.mjs", "test/motion3/verify-motion3-shapes.mjs", "test/motion3/verify-motion3-grasp-fields.mjs", "test/motion3/verify-motion3-finger-contact.mjs", "test/motion3/verify-motion3-finger-pose.mjs", "test/motion3/verify-motion3-grasp-qa.mjs", "test/motion3/verify-motion3-load.mjs", "test/motion3/verify-motion3-throw.mjs", "test/motion3/verify-motion3-catch.mjs", "test/motion3/verify-motion3-spring.mjs", "test/motion3/verify-motion3-grasp-hints.mjs", "test/motion3/verify-motion3-reach-human.mjs", "test/motion3/verify-motion3-stance-grasp.mjs", "test/motion3/verify-motion3-grasp-friction.mjs", "test/motion3/verify-motion3-grasp-measure.mjs", "test/motion3/verify-motion3-contracts.mjs", "test/motion3/verify-motion3-describe.mjs", "test/motion3/verify-motion3-timeline.mjs", "test/motion3/verify-motion3-probes.mjs", "test/motion3/verify-motion3-grab.mjs", "test/motion3/verify-motion3-grab-blend.mjs", "test/motion3/verify-motion3-patch.mjs", "test/motion3/verify-motion3-patch-keys.mjs", "test/motion3/verify-motion3-stuck.mjs", "test/motion3/verify-motion3-repair.mjs", "test/motion3/verify-motion3-specstore.mjs", "test/motion3/verify-motion3-visual.mjs", "test/motion3/verify-motion3-goals.mjs", "test/motion3/verify-motion3-prompt.mjs", "test/motion3/verify-motion3-cup.mjs", "test/motion3/verify-motion3-arm.mjs", "test/motion3/verify-motion3-balance.mjs", "test/motion3/verify-physics-overlay.mjs", "test/verify-proxy-figure.mjs", "test/verify-text-object.mjs",
|
|
17
|
+
"test/motion3/verify-motion3-core.mjs", "test/motion3/verify-playback-frame-store.mjs", "test/verify-playback-render-isolation.mjs", "test/motion3/verify-motion3-idle.mjs", "test/motion3/verify-motion3-life.mjs", "test/motion3/verify-motion3-headlead.mjs", "test/motion3/verify-motion3-gait.mjs", "test/motion3/verify-motion3-overlap.mjs", "test/motion3/verify-motion3-flip.mjs", "test/motion3/verify-motion3-float-fix.mjs", "test/motion3/verify-motion3-timing.mjs", "test/motion3/verify-motion3-self.mjs", "test/motion3/verify-motion3-commands.mjs", "test/motion3/verify-motion3-exitcar.mjs", "test/motion3/verify-motion3-agentloop.mjs", "test/motion3/verify-motion3-search.mjs", "test/motion3/verify-motion3-skills.mjs", "test/motion3/verify-motion3-carried.mjs", "test/motion3/verify-motion3-people.mjs", "test/motion3/verify-motion3-hit.mjs", "test/motion3/verify-motion3-person-fix.mjs", "test/motion3/verify-motion3-socket.mjs", "test/motion3/verify-motion3-shapes.mjs", "test/motion3/verify-motion3-grasp-fields.mjs", "test/motion3/verify-motion3-finger-contact.mjs", "test/motion3/verify-motion3-finger-pose.mjs", "test/motion3/verify-motion3-grasp-qa.mjs", "test/motion3/verify-motion3-load.mjs", "test/motion3/verify-motion3-load-faces.mjs", "test/motion3/verify-motion3-throw.mjs", "test/motion3/verify-motion3-catch.mjs", "test/motion3/verify-motion3-spring.mjs", "test/motion3/verify-motion3-grasp-hints.mjs", "test/motion3/verify-motion3-ball-pickup-repair.mjs", "test/motion3/verify-motion3-reach-human.mjs", "test/motion3/verify-motion3-stance-grasp.mjs", "test/motion3/verify-motion3-grasp-friction.mjs", "test/motion3/verify-motion3-grasp-measure.mjs", "test/motion3/verify-motion3-contracts.mjs", "test/motion3/verify-motion3-describe.mjs", "test/motion3/verify-motion3-timeline.mjs", "test/motion3/verify-motion3-probes.mjs", "test/motion3/verify-motion3-grab.mjs", "test/motion3/verify-motion3-grab-blend.mjs", "test/motion3/verify-motion3-patch.mjs", "test/motion3/verify-motion3-patch-keys.mjs", "test/motion3/verify-motion3-stuck.mjs", "test/motion3/verify-motion3-repair.mjs", "test/motion3/verify-motion3-specstore.mjs", "test/motion3/verify-motion3-visual.mjs", "test/motion3/verify-motion3-goals.mjs", "test/motion3/verify-motion3-prompt.mjs", "test/motion3/verify-motion3-cup.mjs", "test/motion3/verify-motion3-arm.mjs", "test/motion3/verify-motion3-balance.mjs", "test/motion3/verify-physics-overlay.mjs", "test/verify-proxy-figure.mjs", "test/verify-text-object.mjs",
|
|
17
18
|
"test/verify-proxy-figure-bounds.mjs",
|
|
18
|
-
"test/verify-character-capabilities.mjs",
|
|
19
|
-
"test/verify-root-path.mjs",
|
|
19
|
+
"test/verify-character-capabilities.mjs", "test/verify-test-shards.mjs", "test/verify-select-tests.mjs",
|
|
20
|
+
"test/verify-root-path.mjs", "test/verify-tutorial-recorder.mjs",
|
|
20
21
|
"test/verify-studio-elements.mjs",
|
|
21
22
|
"test/bus/verify-view-mode-pose.mjs",
|
|
22
23
|
"test/verify-studio-actions.mjs",
|
|
23
24
|
"test/ardy/verify-collision-blockers.mjs",
|
|
24
25
|
"test/verify-pose-fade.mjs",
|
|
25
|
-
"test/verify-pose-gizmo.mjs",
|
|
26
|
+
"test/verify-pose-gizmo.mjs", "test/verify-fingertip-pick.mjs",
|
|
26
27
|
"test/ardy/verify-key-runs.mjs",
|
|
27
28
|
"test/ardy/verify-playback-clock.mjs",
|
|
28
29
|
"test/ardy/verify-timeline-coordinates.mjs",
|
|
@@ -64,7 +65,7 @@ const NODE_FILES = [
|
|
|
64
65
|
"test/verify-agent-sse-golden.mjs",
|
|
65
66
|
"test/verify-agent-context-budget.mjs", "test/verify-agent-context-trim.mjs",
|
|
66
67
|
"test/verify-agent-supervisor.mjs", "test/verify-studio-tasks.mjs", "test/verify-task-board.mjs",
|
|
67
|
-
"test/verify-studio-agent-protocol.mjs",
|
|
68
|
+
"test/verify-studio-agent-protocol.mjs", "test/verify-studio-prompt-cast-style.mjs",
|
|
68
69
|
"test/verify-studio-agent-sampled-framing.mjs",
|
|
69
70
|
"test/verify-studio-agent-tokens.mjs",
|
|
70
71
|
"test/verify-agent-execution.mjs",
|
|
@@ -131,7 +132,7 @@ const NODE_FILES = [
|
|
|
131
132
|
"test/verify-project.mjs",
|
|
132
133
|
"test/verify-workflow-send.mjs",
|
|
133
134
|
"test/verify-animation-export.mjs",
|
|
134
|
-
"test/verify-project-previs-mode.mjs",
|
|
135
|
+
"test/verify-project-previs-mode.mjs", "test/verify-project-cast-style.mjs",
|
|
135
136
|
"test/verify-pwa.mjs",
|
|
136
137
|
"test/verify-package-signature.mjs",
|
|
137
138
|
"test/verify-resilience.mjs",
|
|
@@ -211,10 +212,10 @@ const NODE_FILES = [
|
|
|
211
212
|
const BROWSER_FILES = [
|
|
212
213
|
"test/qa-proxy-entry-browser.mjs",
|
|
213
214
|
"test/qa-previs-start-browser.mjs", "test/qa-shortcut-friction-browser.mjs", "test/qa-ik-contact-browser.mjs",
|
|
214
|
-
"test/qa-storyboard-board-browser.mjs",
|
|
215
|
+
"test/qa-storyboard-board-browser.mjs", "test/qa-sculpt-shelf-browser.mjs",
|
|
215
216
|
"test/qa-storyboard-export-browser.mjs", "test/qa-playback-guide-browser.mjs",
|
|
216
|
-
"test/qa-proxy-figure-browser.mjs", "test/qa-click-friction-browser.mjs", "test/qa-update-notice-browser.mjs",
|
|
217
|
-
"test/qa-rail-lean-browser.mjs",
|
|
217
|
+
"test/qa-proxy-figure-browser.mjs", "test/qa-click-friction-browser.mjs", "test/qa-update-notice-browser.mjs", "test/qa-export-menu-browser.mjs",
|
|
218
|
+
"test/qa-rail-lean-browser.mjs", "test/qa-sculpt-parts-browser.mjs",
|
|
218
219
|
"test/qa-export-recovery-browser.mjs", "test/qa-export-workspace-browser.mjs",
|
|
219
220
|
"test/verify-camera-mode-browser.mjs",
|
|
220
221
|
"test/verify-camera-rail-browser.mjs",
|
|
@@ -257,9 +258,9 @@ const BROWSER_FILES = [
|
|
|
257
258
|
const EXTRA_INVENTORY = ["test/ik/translation-step.mjs", "test/qa-scenes-v5-migration-browser.mjs", "test/qa-agent-scenarios-browser.mjs", "test/qa-execution-outcomes-browser.mjs", "test/qa-export-recovery-browser.mjs", "test/qa-agent-view-toggle-browser.mjs", "test/qa-camera-pointer-lock-browser.mjs", "test/qa-first-edit-browser.mjs", "test/qa-ia-tail-browser.mjs", "test/qa-keyframe-pack-browser.mjs", "test/qa-preview-browser.mjs", "test/qa-reference-slots-browser.mjs", "test/qa-scene-playback-browser.mjs", "test/qa-scene-switcher-browser.mjs", "test/qa-send-to-ai-browser.mjs", "test/qa-view-menu-browser.mjs", "test/qa-live-reconnect-browser.mjs", "test/qa-clipboard-paste-browser.mjs", "test/qa-rail-lean-browser.mjs", "test/qa-rail-reach-browser.mjs", "test/qa-rail-ring-browser.mjs", "test/qa-speed-graph-browser.mjs", "test/qa-export-workspace-browser.mjs"];
|
|
258
259
|
|
|
259
260
|
EXTRA_INVENTORY.push("test/qa-proxy-figure-browser.mjs", "test/qa-click-friction-browser.mjs", "test/qa-update-notice-browser.mjs");
|
|
260
|
-
EXTRA_INVENTORY.push("test/qa-proxy-entry-browser.mjs");
|
|
261
|
+
EXTRA_INVENTORY.push("test/qa-proxy-entry-browser.mjs", "test/qa-export-menu-browser.mjs");
|
|
261
262
|
EXTRA_INVENTORY.push("test/qa-previs-start-browser.mjs", "test/qa-shortcut-friction-browser.mjs", "test/qa-ik-contact-browser.mjs");
|
|
262
|
-
EXTRA_INVENTORY.push("test/qa-storyboard-board-browser.mjs");
|
|
263
|
+
EXTRA_INVENTORY.push("test/qa-storyboard-board-browser.mjs", "test/qa-sculpt-parts-browser.mjs", "test/qa-sculpt-shelf-browser.mjs");
|
|
263
264
|
EXTRA_INVENTORY.push("test/qa-storyboard-export-browser.mjs", "test/qa-playback-guide-browser.mjs");
|
|
264
265
|
|
|
265
266
|
function verificationFiles(directory) {
|
|
@@ -275,56 +276,38 @@ function verificationFiles(directory) {
|
|
|
275
276
|
// Every suite reserves its own ports and writes under a temp dir, so files
|
|
276
277
|
// are independent of each other; running them serially cost CI five minutes
|
|
277
278
|
// (#413). `--jobs 1` restores the old interleaved live output for debugging.
|
|
278
|
-
// `--shard INDEX/TOTAL` selects a deterministic
|
|
279
|
-
//
|
|
280
|
-
// the same runnable set as an unsharded
|
|
279
|
+
// `--shard INDEX/TOTAL` selects a deterministic slice for CI runners, balanced
|
|
280
|
+
// by the recorded seconds in tools/test-durations.json (tools/test-shards.mjs).
|
|
281
|
+
// Shards are disjoint and together cover the same runnable set as an unsharded
|
|
282
|
+
// invocation. `--durations-out PATH` writes each file's wall-clock seconds.
|
|
283
|
+
// `--only-from SELECTION.json` (tools/select-tests.mjs) narrows the runnable
|
|
284
|
+
// set before sharding; a shard left with no files passes.
|
|
281
285
|
function parseArguments(arguments_) {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
let jobs = Number(process.env.COZYCLAY_TEST_JOBS) || availableParallelism();
|
|
285
|
-
let shard = null;
|
|
286
|
+
const options = { listOnly: false, scope: "all", jobs: Number(process.env.COZYCLAY_TEST_JOBS) || availableParallelism(), shard: null, durationsOut: null, onlyFrom: null };
|
|
287
|
+
const valued = { "--scope": "scope", "--jobs": "jobs", "--shard": "shard", "--durations-out": "durationsOut", "--only-from": "onlyFrom" };
|
|
286
288
|
for (let index = 0; index < arguments_.length; index += 1) {
|
|
287
289
|
const argument = arguments_[index];
|
|
288
|
-
if (argument === "--list")
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
if (argument === "--scope") {
|
|
293
|
-
scope = arguments_[index + 1] ?? "";
|
|
294
|
-
index += 1;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
if (argument === "--jobs") {
|
|
298
|
-
jobs = Number(arguments_[index + 1]);
|
|
299
|
-
index += 1;
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
if (argument === "--shard") {
|
|
303
|
-
const value = arguments_[index + 1] ?? "";
|
|
304
|
-
const match = /^(\d+)\/(\d+)$/.exec(value);
|
|
305
|
-
if (!match) throw new Error(`--shard must be INDEX/TOTAL with 1 <= INDEX <= TOTAL, got ${value}`);
|
|
306
|
-
const current = Number(match[1]);
|
|
307
|
-
const total = Number(match[2]);
|
|
308
|
-
if (!Number.isInteger(current) || !Number.isInteger(total) || current < 1 || total < 1 || current > total) {
|
|
309
|
-
throw new Error(`--shard must be INDEX/TOTAL with 1 <= INDEX <= TOTAL, got ${value}`);
|
|
310
|
-
}
|
|
311
|
-
shard = { current, total };
|
|
312
|
-
index += 1;
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
throw new Error(`unknown argument: ${argument}`);
|
|
290
|
+
if (argument === "--list") options.listOnly = true;
|
|
291
|
+
else if (Object.hasOwn(valued, argument)) options[valued[argument]] = arguments_[(index += 1)] ?? "";
|
|
292
|
+
else throw new Error(`unknown argument: ${argument}`);
|
|
316
293
|
}
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
|
|
294
|
+
options.jobs = Number(options.jobs);
|
|
295
|
+
if (options.shard !== null) options.shard = parseShard(options.shard);
|
|
296
|
+
if (options.scope !== "all" && options.scope !== "ardy") throw new Error(`unknown test scope: ${options.scope}`);
|
|
297
|
+
if (!Number.isInteger(options.jobs) || options.jobs < 1) throw new Error(`--jobs must be a positive integer, got ${options.jobs}`);
|
|
298
|
+
if (options.durationsOut === "" || options.onlyFrom === "") throw new Error("--durations-out and --only-from need a file path");
|
|
299
|
+
return options;
|
|
320
300
|
}
|
|
321
301
|
|
|
322
302
|
// One file per child. With more than one job the output is buffered per file
|
|
323
303
|
// and replayed as a block when the file finishes, so PASS/FAIL lines of
|
|
324
|
-
// different suites never interleave.
|
|
304
|
+
// different suites never interleave. Each file's wall-clock seconds land in
|
|
305
|
+
// `timings` for --durations-out and the CI step summary.
|
|
306
|
+
const timings = new Map();
|
|
325
307
|
function run(file, { buffered }) {
|
|
326
308
|
return new Promise((resolve, reject) => {
|
|
327
309
|
const chunks = [];
|
|
310
|
+
const started = performance.now();
|
|
328
311
|
const child = spawn(process.execPath, [file], { stdio: buffered ? ["ignore", "pipe", "pipe"] : "inherit" });
|
|
329
312
|
if (buffered) {
|
|
330
313
|
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
@@ -332,6 +315,7 @@ function run(file, { buffered }) {
|
|
|
332
315
|
}
|
|
333
316
|
child.once("error", reject);
|
|
334
317
|
child.once("exit", (code, signal) => {
|
|
318
|
+
timings.set(file, (performance.now() - started) / 1000);
|
|
335
319
|
if (buffered) process.stdout.write(`\nRUNNING ${file}\n${Buffer.concat(chunks)}`);
|
|
336
320
|
if (code === 0) resolve();
|
|
337
321
|
else reject(new Error(`${file} failed${signal ? ` with ${signal}` : ` with exit code ${code}`}`));
|
|
@@ -352,17 +336,8 @@ const SERIAL_ONLY = new Set([
|
|
|
352
336
|
"mcp/verify-live-routing.mjs",
|
|
353
337
|
]);
|
|
354
338
|
|
|
355
|
-
// Longest files first so the tail of the run is not one slow suite on its own.
|
|
356
|
-
const SLOW_FIRST = [
|
|
357
|
-
"test/verify-studio-agent-motion.mjs",
|
|
358
|
-
"test/verify-studio-agent-binding.mjs",
|
|
359
|
-
"test/verify-part-colours.mjs",
|
|
360
|
-
"test/verify-agent-runner-errors.mjs",
|
|
361
|
-
];
|
|
362
|
-
|
|
363
339
|
async function runAll(files, jobs) {
|
|
364
|
-
const
|
|
365
|
-
const queue = [...SLOW_FIRST.filter((file) => parallel.includes(file)), ...parallel.filter((file) => !SLOW_FIRST.includes(file))];
|
|
340
|
+
const queue = slowestFirst(files.filter((file) => !SERIAL_ONLY.has(file)), durations);
|
|
366
341
|
let failure = null;
|
|
367
342
|
const worker = async () => {
|
|
368
343
|
while (queue.length > 0 && !failure) {
|
|
@@ -440,7 +415,8 @@ if (!mcpDepsInstalled) {
|
|
|
440
415
|
}
|
|
441
416
|
}
|
|
442
417
|
|
|
443
|
-
const { listOnly, scope, jobs, shard } = parseArguments(process.argv.slice(2));
|
|
418
|
+
const { listOnly, scope, jobs, shard, durationsOut, onlyFrom } = parseArguments(process.argv.slice(2));
|
|
419
|
+
const durations = readDurations();
|
|
444
420
|
const inventory = [...verificationFiles("test"), ...verificationFiles("mcp"), ...EXTRA_INVENTORY].sort();
|
|
445
421
|
for (const file of inventory) {
|
|
446
422
|
if (/^test\/bus\/verify-.*\.mjs$/.test(file)) categories.set(file, { kind: "node", reason: "command bus contract verification" });
|
|
@@ -455,24 +431,24 @@ if (unclassified.length > 0 || stale.length > 0) {
|
|
|
455
431
|
}
|
|
456
432
|
|
|
457
433
|
const scoped = inventory.filter((file) => scope === "all" || file.startsWith("test/ardy/") || file.startsWith("test/ik/"));
|
|
458
|
-
const
|
|
459
|
-
const
|
|
434
|
+
const only = readSelection(onlyFrom);
|
|
435
|
+
const runnable = scoped.filter((file) => categories.get(file).kind === "node" && (!only || only.has(file)));
|
|
436
|
+
const { files: selected, predicted } = assignShards(runnable, durations, shard?.total ?? 1)[(shard?.current ?? 1) - 1];
|
|
460
437
|
const selectedSet = new Set(selected);
|
|
461
438
|
console.log(
|
|
462
439
|
`TEST MANIFEST scope=${scope} runnable=${runnable.length} selected=${selected.length} total=${scoped.length} jobs=${jobs}${
|
|
463
440
|
shard ? ` shard=${shard.current}/${shard.total}` : ""
|
|
464
|
-
}`,
|
|
441
|
+
} predicted=${predicted.toFixed(1)}s`,
|
|
465
442
|
);
|
|
466
443
|
for (const file of scoped) {
|
|
467
444
|
const { kind, reason } = categories.get(file);
|
|
468
|
-
if (kind === "node" &&
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
}
|
|
472
|
-
console.log(`${kind === "node" ? "RUN" : "EXCLUDE"} ${kind} ${file} - ${reason}`);
|
|
445
|
+
if (kind === "node" && only && !only.has(file)) console.log(`SKIP node ${file} - not selected by ${onlyFrom}`);
|
|
446
|
+
else if (kind === "node" && shard && !selectedSet.has(file)) console.log(`SHARD node ${file} - assigned to another shard`);
|
|
447
|
+
else console.log(`${kind === "node" ? "RUN" : "EXCLUDE"} ${kind} ${file} - ${reason}`);
|
|
473
448
|
}
|
|
474
449
|
|
|
475
450
|
if (!listOnly) {
|
|
451
|
+
process.on("exit", () => reportDurations(timings, { out: durationsOut, selected: selected.length, predicted }));
|
|
476
452
|
if (jobs === 1) {
|
|
477
453
|
for (const file of selected) {
|
|
478
454
|
console.log(`\nRUNNING ${file}`);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Change-based test selection for pull requests (#632).
|
|
3
|
+
// git diff --name-only --no-renames origin/main...HEAD | node tools/select-tests.mjs --out selection.json
|
|
4
|
+
// reads changed paths (stdin, or --changed FILE) and writes the Node tests a
|
|
5
|
+
// shard must run: `node tools/run-tests.mjs --only-from selection.json`.
|
|
6
|
+
// Anything the selector cannot vouch for becomes mode "full" with a reason.
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { buildGraph, reverseIndex, sweepVerifyFiles } from "./test-graph.mjs";
|
|
11
|
+
|
|
12
|
+
const ROOT = fileURLToPath(new URL("..", import.meta.url));
|
|
13
|
+
|
|
14
|
+
// Changes that can alter any test's outcome, or the selection itself.
|
|
15
|
+
const FULL_TRIGGERS = [
|
|
16
|
+
/^package(-lock)?\.json$/, /^mcp\/package(-lock)?\.json$/, /^(vite|eslint)\.config\.js$/, /^knip\.json$/,
|
|
17
|
+
/^tools\/(run-tests|test-shards|select-tests|test-graph|qa-browser)\.mjs$/, /^tools\/[^/]+-baseline\.json$/,
|
|
18
|
+
/^\.github\/workflows\/[^/]+\.ya?ml$/,
|
|
19
|
+
/^test\/bus\/[^/]+-fixture\.mjs$/, /^test\/fixtures\//, /^test\/motion3\/fixtures\//, /^test\/camera-browser-harness\.mjs$/,
|
|
20
|
+
];
|
|
21
|
+
// The paths ci.yml's `changes` job already treats as not needing tests.
|
|
22
|
+
const IGNORED = (file) => file.startsWith(".metrics/") || /^docs\/.*\.md$/.test(file) || /^[^/]+\.md$/.test(file);
|
|
23
|
+
// Browser suites run in their own CI lanes, never on the Node shards.
|
|
24
|
+
const BROWSER_ONLY = (file) => file.startsWith("test/") && /(?:^|\/)qa-[^/]*\.mjs$|-browser\.mjs$/.test(file);
|
|
25
|
+
|
|
26
|
+
// Cheap boot checks that run on every selected PR (at most 10).
|
|
27
|
+
export const ALWAYS_RUN = [
|
|
28
|
+
"test/bus/verify-manifest.mjs",
|
|
29
|
+
"test/verify-mcp-invariants.mjs",
|
|
30
|
+
"test/verify-package-agent-imports.mjs",
|
|
31
|
+
"test/verify-error-boundary.mjs",
|
|
32
|
+
"test/verify-tool-handlers.mjs",
|
|
33
|
+
"test/verify-pwa.mjs",
|
|
34
|
+
"mcp/verify.mjs",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// `node tools/run-tests.mjs --list`, as [{ file, kind }]. mcp-deps entries are
|
|
38
|
+
// runnable on a shard (it installs mcp/node_modules) even if not here.
|
|
39
|
+
export function readManifest(root = ROOT) {
|
|
40
|
+
const result = spawnSync(process.execPath, ["tools/run-tests.mjs", "--list"], { cwd: root, encoding: "utf8" });
|
|
41
|
+
if (result.status !== 0) throw new Error(`run-tests --list failed: ${result.stderr.trim().split("\n")[0]}`);
|
|
42
|
+
return [...result.stdout.matchAll(/^(?:RUN|EXCLUDE) (\S+) (\S+) - /gm)].map(([, kind, file]) => ({ kind, file }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Test directories a path falls back to when no test reaches it, deepest
|
|
46
|
+
// first: src/motion3/a/b.js → test/motion3/a/, test/motion3/. A top-level
|
|
47
|
+
// directory never maps (it would be the whole suite).
|
|
48
|
+
function prefixesFor(file) {
|
|
49
|
+
const parts = file.split("/").slice(0, -1);
|
|
50
|
+
const prefixes = [];
|
|
51
|
+
for (let depth = parts.length; depth >= 2; depth -= 1) {
|
|
52
|
+
const directory = parts.slice(0, depth);
|
|
53
|
+
prefixes.push(`${directory.join("/")}/`);
|
|
54
|
+
if (["src", "bin", "tools"].includes(directory[0])) prefixes.push(`test/${directory.slice(1).join("/")}/`);
|
|
55
|
+
}
|
|
56
|
+
return prefixes;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const full = (reason, base) => ({ ...base, mode: "full", reason, files: base.runnable, selected: base.total });
|
|
60
|
+
|
|
61
|
+
export function selectTests(changed, { root = ROOT, manifest = readManifest(root), always = ALWAYS_RUN } = {}) {
|
|
62
|
+
const runnable = manifest.filter(({ kind }) => kind === "node" || kind === "mcp-deps").map(({ file }) => file).sort();
|
|
63
|
+
const runnableSet = new Set(runnable);
|
|
64
|
+
const inventory = new Set(manifest.map(({ file }) => file));
|
|
65
|
+
const paths = [...new Set(changed.map((file) => file.trim()).filter(Boolean))];
|
|
66
|
+
const base = { changed: paths.length, total: runnable.length, alwaysRun: 0, runnable };
|
|
67
|
+
const trigger = paths.find((file) => FULL_TRIGGERS.some((pattern) => pattern.test(file)));
|
|
68
|
+
if (trigger) return full(`full-run trigger: ${trigger}`, base);
|
|
69
|
+
const unknown = [...sweepVerifyFiles(root, "test"), ...sweepVerifyFiles(root, "mcp")].filter((file) => !inventory.has(file));
|
|
70
|
+
if (unknown.length > 0) return full(`inventory mismatch: ${unknown.slice(0, 3).join(", ")}`, base);
|
|
71
|
+
|
|
72
|
+
const graph = buildGraph(root, runnable);
|
|
73
|
+
const readers = reverseIndex(graph);
|
|
74
|
+
const alwaysSet = new Set([...always.filter((file) => runnableSet.has(file)), ...runnable.filter((file) => graph.get(file).unresolved)]);
|
|
75
|
+
const selected = new Set(alwaysSet);
|
|
76
|
+
for (const file of paths) {
|
|
77
|
+
if (IGNORED(file) || (inventory.has(file) && !runnableSet.has(file))) continue;
|
|
78
|
+
if (runnableSet.has(file)) selected.add(file);
|
|
79
|
+
const tests = readers(file);
|
|
80
|
+
if (tests.size === 0 && BROWSER_ONLY(file)) continue;
|
|
81
|
+
for (const prefix of tests.size > 0 ? [] : prefixesFor(file)) {
|
|
82
|
+
for (const test of runnable) if (test.startsWith(prefix)) tests.add(test);
|
|
83
|
+
if (tests.size > 0) break;
|
|
84
|
+
}
|
|
85
|
+
if (tests.size === 0 && !runnableSet.has(file)) return full(`no test reaches ${file}`, { ...base, alwaysRun: alwaysSet.size });
|
|
86
|
+
for (const test of tests) selected.add(test);
|
|
87
|
+
}
|
|
88
|
+
const result = { ...base, alwaysRun: alwaysSet.size, mode: "selected", reason: "", files: [...selected].sort(), selected: selected.size };
|
|
89
|
+
if (selected.size * 2 >= runnable.length) return full(`selection ${selected.size}/${runnable.length} is 50% or more`, result);
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Any exception is a full run: a selector bug must never skip a test.
|
|
94
|
+
export function safeSelect(changed, options) {
|
|
95
|
+
try {
|
|
96
|
+
return selectTests(changed, options);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return { mode: "full", reason: `selector error: ${error.message}`, changed: changed.length, files: [], selected: 0, total: 0, alwaysRun: 0 };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function summaryTable({ changed, mode, reason, selected, total, alwaysRun }) {
|
|
103
|
+
return [
|
|
104
|
+
"### Test selection (#632)", "",
|
|
105
|
+
"| Changed files | Mode | Fallback reason | Selected / total | Always run |", "|---|---|---|---|---|",
|
|
106
|
+
`| ${changed} | ${mode} | ${reason.replaceAll("|", "\\|") || "-"} | ${selected} / ${total} | ${alwaysRun} |`, "",
|
|
107
|
+
].join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
111
|
+
const flag = (name) => process.argv[process.argv.indexOf(name) + 1];
|
|
112
|
+
const input = process.argv.includes("--changed") ? readFileSync(flag("--changed"), "utf8") : readFileSync(0, "utf8");
|
|
113
|
+
const { runnable, ...selection } = safeSelect(input.split("\n"));
|
|
114
|
+
const json = `${JSON.stringify(selection, null, "\t")}\n`;
|
|
115
|
+
if (process.argv.includes("--out")) writeFileSync(flag("--out"), json);
|
|
116
|
+
else process.stdout.write(json);
|
|
117
|
+
if (process.env.GITHUB_STEP_SUMMARY && existsSync(process.env.GITHUB_STEP_SUMMARY)) appendFileSync(process.env.GITHUB_STEP_SUMMARY, summaryTable(selection));
|
|
118
|
+
console.error(`select-tests: ${selection.mode} ${selection.selected}/${selection.total}${selection.reason ? ` (${selection.reason})` : ""}`);
|
|
119
|
+
}
|