@kieransimkin/dancemoves 3.0.2

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 (50) hide show
  1. package/LICENSE +338 -0
  2. package/README.md +893 -0
  3. package/RUDIMENTS-API.md +394 -0
  4. package/docs/README.md +73 -0
  5. package/docs/api/adapters.md +400 -0
  6. package/docs/api/downloads.md +158 -0
  7. package/docs/api/effects.md +463 -0
  8. package/docs/api/javascript.md +500 -0
  9. package/docs/api/paper-planes.md +167 -0
  10. package/docs/api/styling.md +292 -0
  11. package/docs/api/surface.json +446 -0
  12. package/docs/api/wordpress.md +365 -0
  13. package/docs/clay-transform-validation.md +84 -0
  14. package/docs/development.md +254 -0
  15. package/docs/guide.md +288 -0
  16. package/docs/javascript-api.md +292 -0
  17. package/docs/javascript-server.md +150 -0
  18. package/docs/react-next.md +185 -0
  19. package/docs/releasing-shared.md +162 -0
  20. package/docs/rudiments-source-integrity.md +80 -0
  21. package/docs/shared-feature-coverage.md +34 -0
  22. package/docs/shared-runtime-migration.md +111 -0
  23. package/docs/source-audit.md +120 -0
  24. package/docs/validation-setup.md +102 -0
  25. package/docs/wordpress-examples.md +206 -0
  26. package/docs/wordpress-shared-runtime.md +133 -0
  27. package/lib/admin.min.js +1 -0
  28. package/lib/assets/DanceRudiments-LICENSE +21 -0
  29. package/lib/assets/DanceRudiments-build-manifest.json +21 -0
  30. package/lib/assets/paper-dreams-plane-atlas.png +0 -0
  31. package/lib/build-manifest.json +117 -0
  32. package/lib/dancemoves.css +9 -0
  33. package/lib/dancemoves.min.js +5 -0
  34. package/lib/index.cjs +4802 -0
  35. package/lib/index.d.ts +71 -0
  36. package/lib/index.mjs +4780 -0
  37. package/lib/react.cjs +154 -0
  38. package/lib/react.d.ts +12 -0
  39. package/lib/react.mjs +135 -0
  40. package/lib/rudiments.d.ts +124 -0
  41. package/lib/server.cjs +411 -0
  42. package/lib/server.d.ts +11 -0
  43. package/lib/server.mjs +367 -0
  44. package/lib/styles/clay-stars-effects.css +1 -0
  45. package/lib/styles/clay-stars-rudiments.css +1 -0
  46. package/lib/styles/dance-moves-core.css +1 -0
  47. package/lib/styles/ks-epk-device-orientation.css +1 -0
  48. package/lib/styles/paper-dreams-flight.css +1 -0
  49. package/lib/wordpress.js +5 -0
  50. package/package.json +101 -0
@@ -0,0 +1,365 @@
1
+ # WordPress, PHP and REST API reference
2
+
3
+ [Documentation index](../README.md) · [JavaScript API](javascript.md)
4
+
5
+ Source: [kieran-epk-device-orientation.php](../../kieran-epk-device-orientation.php).
6
+ Baseline: **2.8.0 / `4cb6a71f60459b5579be87d7e55ac8b1426c578e`**.
7
+ All PHP calls below run inside a loaded WordPress environment. Callback functions
8
+ are globally visible PHP symbols, not a promise of a separately versioned SDK.
9
+ Prefer the explicit configuration, sanitisation and validation helpers to invoking
10
+ registration, rendering or save callbacks manually.
11
+
12
+ ## Constants
13
+
14
+ | Constant | Value / purpose |
15
+ | --- | --- |
16
+ | `DANCE_MOVES_VERSION` | `2.8.0` |
17
+ | `KS_EPK_ORIENTATION_VERSION` | Alias of `DANCE_MOVES_VERSION`. |
18
+ | `KS_EPK_MOTION_CAPTURE_TOKEN` | Existing fixed capture submission token; deliberately not reproduced here. It is in public source and is **not a deployment secret**. See the capture security section. |
19
+ | `DANCE_MOVES_META_BPM` | `_dance_moves_bpm` |
20
+ | `DANCE_MOVES_META_LYRIC_TIMING` | `_dance_moves_lyric_timing_id` |
21
+ | `DANCE_MOVES_META_CUE_TIMING` | `_dance_moves_cue_timing_id` |
22
+ | `DANCE_MOVES_META_LYRIC_POPUPS` | `_dance_moves_lyric_popups_enabled` |
23
+ | `DANCE_MOVES_META_MASTER_DURATION` | `_dance_moves_effect` | `string` / `""` | `dance_moves_sanitize_effect`: sanitised key restricted to `""` / `"paper-planes"`. | Yes |
24
+ | `_dance_moves_master_duration_ms` |
25
+ | `DANCE_MOVES_CLAY_STARS_PAGE_ID` | `252` |
26
+ | `DANCE_MOVES_META_EFFECT` | `_dance_moves_effect` |
27
+ | `DANCE_MOVES_DOWNLOAD_QUERY_VAR` | `dance_moves_download` |
28
+ | `DANCE_MOVES_DOWNLOAD_SIGNATURE_QUERY_VAR` | `dance_moves_download_signature` |
29
+
30
+ `KS_CLAY_STARS_EFFECTS_VERSION` is an **external legacy-plugin marker**, not a
31
+ constant defined here. Its presence suppresses DanceMoves' Clay effects assets
32
+ and content transformation. The plugin's own constants are defined unconditionally;
33
+ predefining them is not a supported configuration override mechanism.
34
+
35
+ ## Page metadata
36
+
37
+ All keys are registered for post type `page`, as single values, with
38
+ `revisions_enabled: true`. `dance_moves_meta_auth()` requires
39
+ `current_user_can('edit_post', $post_id)`. The plugin also adds the six keys to
40
+ `wp_post_revision_meta_keys`. Revision support still depends on the site's
41
+ WordPress version and revision configuration.
42
+
43
+ | Key | Registered type / default | Sanitiser | Standard REST exposure |
44
+ | --- | --- | --- | --- |
45
+ | `_dance_moves_bpm` | `number` / 0 | Finite 20–400, rounded to 3 decimals; invalid/blank becomes `''`. | Yes |
46
+ | `_dance_moves_lyric_timing_id` | `integer` / 0 | `absint()` only. | Yes |
47
+ | `_dance_moves_cue_timing_id` | `integer` / 0 | `absint()` only. | Yes |
48
+ | `_dance_moves_lyric_popups_enabled` | `boolean` / false | `rest_sanitize_boolean`. | Yes |
49
+ | `_dance_moves_effect` | `string` / `""` | `dance_moves_sanitize_effect`: sanitised key restricted to `""` / `"paper-planes"`. | Yes |
50
+ | `_dance_moves_master_duration_ms` | `number` / 0 | Positive finite milliseconds, at most 86,400,000, rounded to 3 decimals; otherwise 0. | **No** |
51
+
52
+ These are metadata on the ordinary WordPress Page REST resource, not a separate
53
+ `/dance-moves` endpoint. The usual resource is `/wp-json/wp/v2/pages/<id>`; use the
54
+ site's actual REST root, including a subdirectory or `?rest_route=` form where
55
+ applicable. Normal WordPress authentication and edit permissions apply to writes.
56
+
57
+ Illustrative **authenticated** page update body:
58
+
59
+ ```json
60
+ {
61
+ "meta": {
62
+ "_dance_moves_bpm": 120,
63
+ "_dance_moves_lyric_timing_id": 1234,
64
+ "_dance_moves_cue_timing_id": 1235,
65
+ "_dance_moves_lyric_popups_enabled": false,
66
+ "_dance_moves_effect": ""
67
+ }
68
+ }
69
+ ```
70
+
71
+ The attachment IDs above are examples, not existing media. Use IDs validated for
72
+ this site. The editor's invalid-value preservation and full attachment-content
73
+ validation are **not** implemented as REST pre-update validation. A REST write
74
+ only uses registered type/schema checks and sanitisation; `absint()` does not
75
+ prove attachment type, readability, file extension, UTF-8 or timing order.
76
+ Do not send an empty string as though it were a valid REST number: use a validated
77
+ BPM or clear metadata through an appropriate trusted WordPress path. Defaults
78
+ such as zero mean unknown, not a valid musical BPM.
79
+
80
+ ### Trusted PHP integration
81
+
82
+ Validate before calling `update_post_meta()`, and retain the caller's own nonce
83
+ and capability checks for any custom admin endpoint. Example inside an authorised
84
+ save operation, with `$page_id` and `$attachment_id` already resolved:
85
+
86
+ ```php
87
+ if (!current_user_can('edit_post', $page_id)) {
88
+ return;
89
+ }
90
+ $valid = dance_moves_validate_timing_attachment($attachment_id, 'lyric');
91
+ if (is_wp_error($valid)) {
92
+ // Return/display this error through the calling integration's error path.
93
+ return;
94
+ }
95
+ update_post_meta($page_id, DANCE_MOVES_META_LYRIC_TIMING, absint($attachment_id));
96
+
97
+ $duration = dance_moves_sanitize_duration(183420.125);
98
+ if ($duration > 0) {
99
+ update_post_meta($page_id, DANCE_MOVES_META_MASTER_DURATION, $duration);
100
+ }
101
+ $config = dance_moves_get_page_config($page_id);
102
+ ```
103
+
104
+ This does not authorise an unauthenticated caller or implement a complete custom
105
+ save route. `update_post_meta()` is not a substitute for the integration's access
106
+ control, and individual field updates are not an atomic multi-field transaction.
107
+
108
+ ## Configuration and sanitisation helpers
109
+
110
+ | Function | Return and behaviour |
111
+ | --- | --- |
112
+ | `dance_moves_orientation_adapters()` | `array<int,string>` mapping the nine configured Page IDs to adapter names; see [adapters](adapters.md#orientation-page-map). |
113
+ | `dance_moves_shared_control_pages()` | Array of Page IDs receiving 8-tick shared-control transitions: `250, 254, 260, 262, 264, 266, 272, 274, 278, 280, 282, 284, 286, 288, 290, 292, 294, 296, 300, 302, 304, 306, 308, 312, 314, 316, 318, 320, 322`. |
114
+ | `dance_moves_lyric_disclosure_ticks($page_id)` | `5` for 130, `7` for 243, otherwise `6`. |
115
+ | `dance_moves_attachment_url($attachment_id)` | Absolute integer ID lookup with `wp_get_attachment_url()` and `esc_url_raw()`; empty string for no ID/unusable URL. Does not run timing validation. |
116
+ | `dance_moves_get_page_config($page_id)` | Configuration array described below. Reads page meta, resolves URLs and assigns fallback BPM/provenance. No writes. |
117
+ | `dance_moves_sanitize_bpm($value)` | Number rounded to 3 decimals for finite numeric 20–400; `''` for blank/null/invalid input. |
118
+ | `dance_moves_sanitize_attachment_id($value)` | `absint($value)`; no existence or content check. |
119
+ | `dance_moves_sanitize_duration($value)` | Positive finite milliseconds up to one day, rounded to 3 decimals; otherwise 0. |
120
+ | `dance_moves_sanitize_effect($value)` | Runs `sanitize_key((string) $value)`, accepts only empty string or `paper-planes`, otherwise returns empty string. |
121
+ | `dance_moves_meta_auth($allowed, $meta_key, $post_id)` | Boolean from `current_user_can('edit_post', $post_id)`; the first two arguments do not alter the decision. |
122
+ | `ks_epk_motion_finite_number($value, $minimum, $maximum)` | Float for numeric, finite, inclusive-range input, otherwise `null`. Numeric strings are accepted. |
123
+
124
+ `dance_moves_get_page_config()` returns all of:
125
+ `pageId`, `version`, `bpm`, `bpmSource`, `fallbackBpm`, `lyricTimingUrl`,
126
+ `lyricPopupsEnabled`, `cueTimingUrl`, `masterDurationMilliseconds`,
127
+ `tickDefinition`, `ticksPerBeat`, `longDurationQuantumTicks`, `sharedControlTicks`,
128
+ `lyricDisclosureTicks`, `effect`. See the [boot table](javascript.md#boot-object-windowdancemovesconfig)
129
+ for their values. This helper does not emit `diagnostics` or a capture credential.
130
+ Its read-time duration check accepts a positive numeric value; use the duration
131
+ sanitiser on writes rather than relying on this reader to enforce the one-day cap.
132
+
133
+ ## Timing attachment validation
134
+
135
+ ### `dance_moves_validate_timing_attachment($attachment_id, $kind)`
136
+
137
+ Returns `true` or `WP_Error`. Zero/empty ID is a valid cleared selection. `$kind`
138
+ equal to `'lyric'` permits only `.lrc`; every other kind uses the cue extension
139
+ set `.lrc`/`.cue`. This is not an enum-validating argument.
140
+
141
+ | Error code | Validation failure |
142
+ | --- | --- |
143
+ | `dance_moves_attachment` | ID is not a WordPress attachment. |
144
+ | `dance_moves_file` | Missing/unreadable path, or file larger than 1 MiB. |
145
+ | `dance_moves_extension` | Extension invalid for the selected field. |
146
+ | `dance_moves_utf8` | Unreadable contents, invalid UTF-8, null or U+FFFD replacement character. |
147
+ | `dance_moves_timestamps` | No recognised bracketed timestamp token. |
148
+ | `dance_moves_order` | Timestamp tokens decrease in physical file order. |
149
+
150
+ The validator does not check media ownership, attachment-level read permission,
151
+ lyric accuracy, master identity, a seconds-field range of 00–59, or whether the
152
+ file contains a nonblank displayable lyric. It reads `get_attached_file()` locally;
153
+ remote-only offloaded media may need an accessible local copy for editor validation.
154
+
155
+ ### Upload hooks versus selection validation
156
+
157
+ `dance_moves_timing_upload_mimes($mimes)` returns the array with `lrc` and `cue`
158
+ set to `text/plain`.
159
+
160
+ `dance_moves_timing_filetype($data, $file, $filename, $mimes)` is the four-argument
161
+ `wp_check_filetype_and_ext` filter. For recognised extensions with readable
162
+ contents no larger than 1 MiB, no nulls and valid UTF-8, it sets `ext`, `type` and
163
+ `proper_filename`. Otherwise it returns the incoming data unchanged. It does
164
+ **not** perform the selection validator's timestamp-order or replacement-character
165
+ checks, and returning unchanged data is not itself a guaranteed upload rejection.
166
+ The full validator runs when the Page editor saves the selected attachment.
167
+
168
+ ## Motion-capture REST API
169
+
170
+ ### Endpoint and permission contract
171
+
172
+ ```text
173
+ POST <WordPress REST root>/ks-epk-motion/v1/capture
174
+ Content-Type: application/json
175
+ X-KS-Motion-Token: <operator-supplied value; not reproduced here>
176
+ ```
177
+
178
+ Implemented by `ks_epk_motion_register_rest_route()` using
179
+ `WP_REST_Server::CREATABLE`, permission callback
180
+ `ks_epk_motion_capture_permission(WP_REST_Request $request)` and handler
181
+ `ks_epk_motion_store_capture(WP_REST_Request $request)`.
182
+
183
+ Permission processing, in order:
184
+
185
+ 1. Compare the supplied header with the fixed token using `hash_equals()`.
186
+ 2. When `get_http_origin()` returns an origin, require it to equal `home_url()`
187
+ after trailing-slash removal. A missing Origin is **not rejected**.
188
+ 3. Rate-limit by a transient keyed from MD5 of `REMOTE_ADDR` (or `'unknown'`).
189
+ Eight accepted permission checks are allowed before a 429. Each allowed check
190
+ writes a new ten-minute expiry; this is not a rigorously sliding-window quota.
191
+ A later-invalid payload still consumes a permission-check attempt.
192
+
193
+ **Security limitation:** the token is embedded in this public repository. The
194
+ endpoint does not require a logged-in user, a WordPress capability or a WordPress
195
+ nonce. Origin checking and a shared IP transient do not turn a published constant
196
+ into authentication. Do not document this as a secure secret-protected upload
197
+ service. Restrict/disable access or implement genuine authenticated capture
198
+ submission before relying on it in a new deployment. This documentation patch
199
+ does not change the endpoint's security model or rotate its constant.
200
+
201
+ ### Request shape
202
+
203
+ Required: `schema` exactly `"ks-epk-motion-recording/v1"`, and `samples`, an array
204
+ of **1–500** sample objects. The entire request is treated as untrusted data.
205
+ Unknown fields are discarded when the clean payload is built.
206
+
207
+ | Top-level field | Accepted / stored behaviour |
208
+ | --- | --- |
209
+ | `schema` | Required exact schema string. |
210
+ | `capturedAt` | Sanitised text, default `""`; not parsed as a date. |
211
+ | `userAgent` | Sanitised text, default `""`. |
212
+ | `screenAngle` | Finite numeric -360–360, default 0; invalid becomes `null`. |
213
+ | `targetDurationMilliseconds` | Finite numeric 0–120000, default 0; invalid becomes `null`. |
214
+ | `captureDurationMilliseconds` | Same range/default. |
215
+ | `sampledDurationMilliseconds` | Same range/default. |
216
+ | `processedPairCount` | `absint()`, capped at 100000; defaults to validated sample count. |
217
+ | `storedSampleCount` | Recomputed from samples; a supplied value is ignored. |
218
+ | `storageIntervalMilliseconds` | Finite numeric 0–120000, default 0; invalid becomes `null`. |
219
+ | `sampleStrategy` | `sanitize_key()`, default `"legacy"`. |
220
+ | `fullWindowObserved` | Boolean derived with `!empty()`. |
221
+ | `expectedDifference` | Sanitised text, default `""`. |
222
+ | `passed` | Boolean derived with `!empty()`; this is a caller claim, not server-verified conformance. |
223
+ | `failureCount` | `absint()`, capped at 100000; defaults to cleaned failure count. |
224
+ | `failures` | Optional array; only first 100 input entries considered, nonobjects skipped. |
225
+ | `samples` | Required 1–500 valid sample objects. |
226
+
227
+ Each sample:
228
+
229
+ | Field | Validation / output |
230
+ | --- | --- |
231
+ | `milliseconds` | Required numeric finite 0–120000. |
232
+ | `beta` | Required numeric finite -180–180. |
233
+ | `gamma` | Required numeric finite -90–90. |
234
+ | `alpha` | Optional/null; otherwise numeric finite 0–360; invalid becomes `null`. |
235
+ | `screenAngle` | Optional numeric finite -360–360, default 0; invalid becomes `null`. |
236
+ | `absolute` | Boolean from `!empty()`. |
237
+ | `nativeTrusted` | Boolean from `!empty()`; not independently authenticated. |
238
+ | `simulatedTrusted` | Boolean from `!empty()`. |
239
+ | `fieldParity` | Boolean from `!empty()`. |
240
+ | `dispatchParity` | Boolean from `!empty()`. |
241
+ | `mapperParity` | Boolean from `!empty()`. |
242
+
243
+ A nonobject sample or invalid `milliseconds`/`beta`/`gamma` invalidates the whole
244
+ capture; samples are not silently removed on a successful submission. Numeric
245
+ strings pass `is_numeric()`. Sample ordering, actual observation duration, event
246
+ trust and parity are not verified by this endpoint. Use JSON booleans, not strings
247
+ such as `"false"`, since PHP `!empty()` treats nonempty strings as true.
248
+
249
+ Each retained failure becomes:
250
+ `{type: sanitize_key(string or "unknown"), sample: absint or null,
251
+ validPairs: absint or null}`. The supplied counts do not have to agree with the
252
+ booleans or retained arrays.
253
+
254
+ Minimal illustrative request (not a physical-device acceptance record):
255
+
256
+ ```json
257
+ {
258
+ "schema": "ks-epk-motion-recording/v1",
259
+ "capturedAt": "2026-09-29T00:00:00Z",
260
+ "passed": false,
261
+ "sampleStrategy": "manual-example",
262
+ "samples": [
263
+ {"milliseconds": 0, "alpha": null, "beta": 0, "gamma": 0, "screenAngle": 0}
264
+ ]
265
+ }
266
+ ```
267
+
268
+ ### Response, errors and storage
269
+
270
+ Success returns a normal REST response (no explicit 201 status is set):
271
+
272
+ ```json
273
+ {"captureId": 123, "sampleCount": 1, "storedPrivately": true}
274
+ ```
275
+
276
+ | HTTP status | Error code | Meaning |
277
+ | --- | --- | --- |
278
+ | 403 | `ks_motion_forbidden` | Missing/wrong token. |
279
+ | 403 | `ks_motion_origin` | Supplied origin does not match the site. |
280
+ | 429 | `ks_motion_rate` | Permission-check quota exhausted. |
281
+ | 400 | `ks_motion_schema` | Missing/wrong schema or nonarray JSON payload. |
282
+ | 400 | `ks_motion_samples` | Missing/nonarray samples or input count outside 1–500. |
283
+ | 400 | `ks_motion_invalid_samples` | At least one required sample field is invalid. |
284
+ | 413 | `ks_motion_size` | Clean JSON encoding fails or exceeds 262144 bytes. |
285
+ | WordPress-defined | WordPress insertion error | A `WP_Error` from `wp_insert_post()` is passed through. |
286
+
287
+ The size limit is on the **sanitised, pretty-printed encoded payload**, not an
288
+ up-front raw request-body limiter. Normal WordPress/server request parsing and
289
+ limits remain separate.
290
+
291
+ A successful record is a `private` post of type `ks_motion_capture`, titled
292
+ `Phone motion capture <site-local date/time>`, with the cleaned JSON in
293
+ `post_content`. Extra metadata: `_ks_motion_passed` (`'1'`/`'0'`),
294
+ `_ks_motion_sample_count`, and `_ks_motion_sha256` (hash of stored encoded JSON).
295
+ The post type has a Tools-menu UI, title/editor support, `public: false`,
296
+ `show_in_rest: false`, and default mapped post capabilities; no custom
297
+ administrator-only capability scheme is defined.
298
+
299
+ There is **no plugin GET/list/delete capture REST route, retention job, automatic
300
+ capture upload, or cryptographic proof of native sensor provenance**. Private
301
+ storage does not mean anonymous data or verified evidence. Review data retention
302
+ and access through normal authorised WordPress operations.
303
+
304
+ ## Enqueued asset contract
305
+
306
+ | Handle | Type | Dependencies | Scope |
307
+ | --- | --- | --- | --- |
308
+ | `dance-moves-core` | CSS and JS | None | Singular Pages; JS in footer; localises `danceMovesConfig`. |
309
+ | `dance-moves-effects` | JS | `dance-moves-core` | Singular Pages; footer; installs `DanceMovesEffects`. |
310
+ | `dance-moves-paper-dreams` | CSS and JS | `dance-moves-core` for both | Only when `effect === "paper-planes"`; JS localises `danceMovesPaperDreamsConfig`; footer. |
311
+ | `dance-moves-catalogue-timing` | JS | `dance-moves-core`, `dance-moves-effects` | Same Page gate; footer. |
312
+ | `dance-moves-clay-stars` | CSS and JS | JS: `dance-moves-core` | Page 252, only without the legacy marker. |
313
+ | `ks-epk-device-orientation` | CSS | None | Configured orientation pages. |
314
+ | `ks-epk-device-orientation-core` | JS | None | Configured orientation pages. |
315
+ | `ks-epk-device-orientation` | JS | Orientation core and `dance-moves-core`; also Clay JS when applicable | Localises `ksEpkOrientationConfig`; footer. |
316
+ | `dance-moves-admin` | JS | `jquery` | Page editing on `post.php` / `post-new.php`; loads the Media Library picker. |
317
+
318
+ Runtime asset versions use the plugin version. To load a page integration, enqueue
319
+ it with `array('dance-moves-core')` for core-only consumers or
320
+ `array('dance-moves-effects')` for shared primitives; add `dance-moves-catalogue-timing` or
321
+ `dance-moves-clay-stars` when those optional surfaces are actually required and
322
+ available. Do not assume a root exists merely because the shared script loaded.
323
+
324
+ ## Registered hooks and callback reference
325
+
326
+ These are WordPress hooks **consumed** by the plugin, not plugin-defined extension
327
+ hooks. Default priority is 10 unless shown. No `apply_filters('dance_moves_...')`
328
+ or `do_action('dance_moves_...')` extension contract is exposed in this baseline.
329
+
330
+ | Hook | Callback | Role |
331
+ | --- | --- | --- |
332
+ | `wp_enqueue_scripts` (20) | `ks_epk_orientation_enqueue_runtime()` | Enqueues assets/localises configuration after Page gating; returns no value. |
333
+ | `init` | `ks_epk_motion_register_capture_type()` | Registers private capture post type; no value. |
334
+ | `init` | `dance_moves_register_page_meta()` | Registers the six Page-meta schemas; no value. |
335
+ | `rest_api_init` | `ks_epk_motion_register_rest_route()` | Registers capture POST route; no value. |
336
+ | `wp_post_revision_meta_keys` | `dance_moves_revision_meta_keys($keys)` | Returns unique merge of incoming keys and the six DanceMoves keys. |
337
+ | `add_meta_boxes_page` | `dance_moves_add_meta_box()` | Registers side/default `dance-moves-epk-timing` meta box; no value. |
338
+ | `admin_enqueue_scripts` | `dance_moves_admin_assets($hook)` | Loads Page-editor media/admin JS when the screen matches; no value. |
339
+ | `upload_mimes` | `dance_moves_timing_upload_mimes($mimes)` | Adds the two text MIME extensions. |
340
+ | `wp_check_filetype_and_ext` (10; 4 arguments) | `dance_moves_timing_filetype($data, $file, $filename, $mimes)` | See upload validation above. |
341
+ | `admin_notices` | `dance_moves_admin_notice()` | Reads/deletes the current user's queued transient and prints escaped error HTML, or returns without output. |
342
+ | `save_post_page` | `dance_moves_save_page_meta($post_id)` | Nonce/capability/autosave checks, per-field preservation/clear/update, popup checkbox and ambient-effect persistence; no value. Blank/invalid/missing effect clears its metadata. |
343
+ | `query_vars` | `dance_moves_epk_download_query_vars($query_vars)` | Adds the two signed-download query vars; unique array returned. |
344
+ | `the_content` (30) | `dance_moves_epk_download_filter_content($content)` | Rewrites eligible MP3 download anchors on main Page content; see [downloads](downloads.md). |
345
+ | `template_redirect` (0) | `dance_moves_epk_download_serve()` | Validates/serves signed MP3 GET/HEAD requests; terminates handled requests. |
346
+ | `the_content` (20) | `dance_moves_clay_stars_filter_content($content)` | Conditionally returns transformed Clay markup. |
347
+
348
+ The eight `dance_moves_epk_download_*` functions have complete signatures and
349
+ contracts in the [download API](downloads.md#php-functions). Together with that
350
+ reference and `dance_moves_sanitize_effect()`, the tables cover all **38** global
351
+ PHP functions in 2.8.0.
352
+
353
+ Additional callbacks/helpers:
354
+
355
+ | Function | Contract |
356
+ | --- | --- |
357
+ | `dance_moves_file_field($post_id, $label, $field_name, $meta_key, $allowed_extensions)` | Prints escaped file-picker markup, hidden attachment ID, status text, view/choose/clear controls; no return value. |
358
+ | `dance_moves_render_meta_box($post)` | Prints the nonce, BPM/file inputs and opt-in checkbox plus Ambient effect selector for `$post->ID`; no return value. |
359
+ | `dance_moves_queue_admin_error($message)` | Stores sanitised text at `dance_moves_admin_error_<user-id>` for 60 seconds; subsequent errors overwrite the same transient. |
360
+ | `dance_moves_clay_stars_is_target()` | Boolean `is_page(252)`. |
361
+ | `dance_moves_clay_stars_filter_content($content)` | Returns input unchanged in admin, off-target, outside the main loop/query, while the legacy constant exists, or without `ks-clay-stars-v2`. Otherwise conditionally inserts warm-bloom/lens/specular and cloud spans, removes the old `ks-clay-stars-v2-script`, and returns the string (empty on a nonstring replacement result). It transforms output, not stored Page content. |
362
+
363
+ `ks_epk_motion_capture_permission()` and `ks_epk_motion_store_capture()` are fully
364
+ specified in the capture section; all remaining global functions appear in the
365
+ configuration, upload, validation or hook tables, and the download reference. No PHP classes are defined.
@@ -0,0 +1,84 @@
1
+ # Clay/Stars transform validation on a clean checkout
2
+
3
+ ## Offline Unit checks
4
+
5
+ From any normal DanceMoves clone, run:
6
+
7
+ ```powershell
8
+ powershell -NoProfile -ExecutionPolicy Bypass -File .\tools\validate.ps1 -Mode Unit
9
+ if ($LASTEXITCODE -ne 0) { throw "Unit validation failed; do not release" }
10
+ ```
11
+
12
+ `tests/validate-clay-transform.php` and `tests/validate-clay-legacy-collision.php`
13
+ now read [checked-in fixtures](../tests/fixtures/clay-transform/README.md), rather
14
+ than climbing three directories above the repository to infer a private
15
+ `Made from the clay and the stars` release folder. Both scripts load the actual
16
+ `kieran-epk-device-orientation.php` entrypoint and use mock WordPress context
17
+ functions. No production code or site data is changed.
18
+
19
+ The transform contract compares against a separately authored expected HTML file,
20
+ checks exact content preservation and idempotence, and exercises page, admin,
21
+ loop and query guards. The collision contract runs in its own PHP process with
22
+ the legacy plugin marker defined and checks that it blocks both insertions and
23
+ legacy-script removal. Missing or changed fixture files fail explicitly.
24
+
25
+ Both scripts are already called by the existing Unit coordinator.
26
+ `tests/clay-transform-fixtures.test.cjs` is automatically discovered with the
27
+ other `.test.cjs` tests. It relocates the minimal test inputs into a temporary
28
+ checkout, runs from an unrelated current directory, tests CRLF and disabled PHP
29
+ assertions, and deliberately breaks fixtures and production behavior to check
30
+ that failures are still detected. Temporary mutations never touch the checkout.
31
+
32
+ Focused commands:
33
+
34
+ ```powershell
35
+ php .\tests\validate-clay-transform.php
36
+ if ($LASTEXITCODE -ne 0) { throw "Clay content transform failed" }
37
+ php .\tests\validate-clay-legacy-collision.php
38
+ if ($LASTEXITCODE -ne 0) { throw "Clay legacy coexistence failed" }
39
+ node .\tests\clay-transform-fixtures.test.cjs
40
+ if ($LASTEXITCODE -ne 0) { throw "PHP fixture regression checks failed" }
41
+ ```
42
+
43
+ ## Separate approved release-payload comparison
44
+
45
+ The old comparison against an approved full-page payload is retained as an
46
+ explicit additional mode, not silently skipped or claimed by synthetic tests:
47
+
48
+ ```powershell
49
+ php .\tests\validate-clay-transform.php --base "C:\release-evidence\clay-base.html" --candidate "C:\release-evidence\clay-approved.html"
50
+ if ($LASTEXITCODE -ne 0) { throw "Supplied approved-payload comparison failed" }
51
+ ```
52
+
53
+ Replace the two paths with your actual reviewed local files. Both options are
54
+ required. A missing file, unreadable file, unknown option, invalid UTF-8, or a
55
+ network URL fails. The checked-in synthetic unit fixtures are rejected in this
56
+ mode. The script does not establish who approved a supplied file; provenance and
57
+ approval remain the release owner's responsibility.
58
+
59
+ This mode first runs the offline contracts, then transforms the supplied base and
60
+ compares its bytes against the supplied candidate after removing only the former
61
+ `ks-clay-stars-warm-light-style` block (and its immediate line ending) and the
62
+ superseded `ks-clay-stars-v2-script` block. It also checks idempotence. Unlike the
63
+ checked-in fixture loader, this comparison does not normalize release-file line
64
+ endings; use a matching source/candidate pair. No expected output is generated or
65
+ rewritten. Exit 0 means all requested checks passed, 1 means a contract mismatch,
66
+ and 2 means invalid input or an execution/setup error.
67
+
68
+ ## What a pass does not establish
69
+
70
+ Offline transform checks are not a browser render, real WordPress hook execution,
71
+ media playback, physical-device performance test or publication receipt. Keep the
72
+ existing pre-live harness and signed-out post-deployment checks. Package mode's
73
+ release-evidence requirements are separate and are not changed by this fix.
74
+
75
+ ## California scaffold parameter groups
76
+
77
+ The later California scaffold check also requires `master`, `timing` and `effect`
78
+ parameter groups. Its manifest previously contained only the first two. It now
79
+ includes a read-only **Orientation target cadence** reference in the `effect`
80
+ group, matching the production `ksEpkOrientationConfig.transitionTargetTicks`
81
+ value (two ticks). This does not introduce an adjustable motion parameter or
82
+ change the runtime. A focused Node contract checks the reference against the PHP
83
+ configuration. Production asset hashes and the full-page acceptance rules remain
84
+ unchanged.