axstack 0.36.1 → 0.38.0

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 (137) hide show
  1. package/README.md +5 -4
  2. package/bin/axstack.js +4 -18
  3. package/docs/getting-started.md +6 -6
  4. package/docs/host-operations.md +9 -5
  5. package/docs/installation.md +20 -41
  6. package/docs/workflows.md +12 -9
  7. package/package.json +1 -1
  8. package/profiles/presets/claude-only.json +3 -3
  9. package/profiles/presets/codex-only.json +3 -3
  10. package/profiles/presets/mixed.json +5 -4
  11. package/skills/axstack/references/automations.md +7 -2
  12. package/skills/axstack/references/lifecycle.md +2 -1
  13. package/skills/axstack/references/review-manager-prompt.md +44 -3
  14. package/skills/axstack/references/role-roster.md +13 -6
  15. package/skills/axstack/references/routing.md +2 -1
  16. package/skills/axstack/references/run-record.md +2 -0
  17. package/skills/axstack/references/ui-verification.md +6 -10
  18. package/skills/axstack-anatomy/ANATOMY.md +172 -0
  19. package/skills/axstack-anatomy/CHANGELOG.md +70 -0
  20. package/skills/axstack-anatomy/LICENSE +21 -0
  21. package/skills/axstack-anatomy/PROVENANCE.md +21 -0
  22. package/skills/axstack-anatomy/README.md +316 -0
  23. package/skills/axstack-anatomy/SKILL.md +74 -0
  24. package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
  25. package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
  26. package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
  27. package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
  28. package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
  29. package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
  30. package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
  31. package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
  32. package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
  33. package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
  34. package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
  35. package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
  36. package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
  37. package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
  38. package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
  39. package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
  40. package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
  41. package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
  42. package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
  43. package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
  44. package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
  45. package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
  46. package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
  47. package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
  48. package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
  49. package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
  50. package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
  51. package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
  52. package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
  53. package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
  54. package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
  55. package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
  56. package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
  57. package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
  58. package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
  59. package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
  60. package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
  61. package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
  62. package/skills/axstack-anatomy/kit/gl.mjs +184 -0
  63. package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
  64. package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
  65. package/skills/axstack-anatomy/kit/iso.css +106 -0
  66. package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
  67. package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
  68. package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
  69. package/skills/axstack-anatomy/kit/tube.mjs +306 -0
  70. package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
  71. package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
  72. package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
  73. package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
  74. package/skills/axstack-anatomy/references/3d.md +352 -0
  75. package/skills/axstack-anatomy/references/craft.md +325 -0
  76. package/skills/axstack-anatomy/references/kit.md +419 -0
  77. package/skills/axstack-anatomy/references/motion.md +165 -0
  78. package/skills/axstack-anatomy/references/react.md +42 -0
  79. package/skills/axstack-anatomy/references/verify.md +351 -0
  80. package/skills/axstack-anatomy/references/walkthrough.md +75 -0
  81. package/skills/axstack-anatomy/references/webgl.md +473 -0
  82. package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
  83. package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
  84. package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
  85. package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
  86. package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
  87. package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
  88. package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
  89. package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
  90. package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
  91. package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
  92. package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
  93. package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
  94. package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
  95. package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
  96. package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
  97. package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
  98. package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
  99. package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
  100. package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
  101. package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
  102. package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
  103. package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
  104. package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
  105. package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
  106. package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
  107. package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
  108. package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
  109. package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
  110. package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
  111. package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
  112. package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
  113. package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
  114. package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
  115. package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
  116. package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
  117. package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
  118. package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
  119. package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
  120. package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
  121. package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
  122. package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
  123. package/skills/axstack-anatomy/theme.js +56 -0
  124. package/skills/axstack-explain/SKILL.md +28 -15
  125. package/skills/axstack-explain/references/delivery.md +22 -0
  126. package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
  127. package/skills/axstack-explain/references/inline-pages.md +8 -8
  128. package/skills/axstack-explain/references/visual-qa.md +3 -5
  129. package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
  130. package/skills/axstack-research/SKILL.md +18 -6
  131. package/src/archify-legacy.js +70 -0
  132. package/src/capabilities.js +1 -29
  133. package/src/installer.js +11 -31
  134. package/skills/axstack-diagram/SKILL.md +0 -28
  135. package/skills/axstack-diagram/references/archify.md +0 -76
  136. package/src/archify-pin.js +0 -5
  137. package/src/archify.js +0 -151
@@ -0,0 +1,302 @@
1
+ import { chromePort } from "./chrome-port.mjs";
2
+ import { spawn } from "node:child_process";
3
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { homedir, tmpdir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { pathToFileURL } from "node:url";
7
+ import { LINE_ENDS } from "./line-ends.mjs";
8
+
9
+ if (typeof WebSocket === "undefined" && !process.execArgv.includes("--experimental-websocket")) {
10
+ const child = spawn(process.execPath, ["--experimental-websocket", ...process.argv.slice(1)], { stdio: "inherit" });
11
+ child.on("exit", (code) => process.exit(code ?? 1));
12
+ } else {
13
+ await main();
14
+ }
15
+
16
+ function usage() {
17
+ console.log(`Drive a figure in headless Chrome: move, click, type, wait, screenshot, and build contact sheets.
18
+
19
+ node drive.mjs <file.html | url> '<actions as JSON>' [--width 1440] [--height 1100] [--scale 1] [--reduced] [--theme dark|light] [--no-webgl] [--preload file.js]
20
+ node drive.mjs <file.html | url> --actions steps.json
21
+
22
+ --reduced emulate prefers-reduced-motion: reduce
23
+ --theme t emulate prefers-color-scheme: dark or light (only pages that read the media query follow it)
24
+ --no-webgl make canvas.getContext("webgl"/"webgl2") return null, to check the SVG fallback
25
+ --preload js run a script before the page's own scripts (counters, stubs, hooks)
26
+ Kit pages set their theme at build time (body data-theme), so drive the -light.html build for light.
27
+ Put anything with eval strings in an --actions file: inline JSON quoting breaks easily.
28
+
29
+ Actions run in order. A missing or null selector means the first .iso-plate (the key action focuses [tabindex="0"]).
30
+ Fractions are 0..1 across the element.
31
+ ["scroll", sel] scroll an element into the middle of the viewport
32
+ ["wait", ms] wait
33
+ ["move", sel, fx, fy] glide the mouse to a point of the element (fires pointermove)
34
+ ["click", sel, fx?, fy?] click a point of the element (centre by default)
35
+ ["drag", sel, fx0, fy0, fx1, fy1, steps?]
36
+ press at one point of the element, glide to another in steps (20) one frame apart,
37
+ release: real pointer events (pointerId 1), so setPointerCapture works
38
+ ["key", key, sel?, code?] focus sel (default [tabindex="0"]) and press a key ("i", "Escape", "ArrowRight", " ", "7");
39
+ pass code for keys that share a name ("Shift" + "ShiftRight", "Enter" + "NumpadEnter")
40
+ ["slow", factor] run the page's requestAnimationFrame and performance.now clock at factor x real
41
+ time (0.05 = 20x slow motion) so sheets catch 100 ms events; ["slow", 1] restores it
42
+ ["until", js, timeoutMs?] poll a JS expression until it is truthy (default 10 s); a timeout stops the run with exit 1
43
+ ["eval", js] run JS in the page and print the result
44
+ ["shot", out.png, sel?, scale?, [x,y,w,h]?] screenshot an element, or a region of it in CSS px
45
+ ["lines", out.png?, sel?, options?] check every texture line's two ends: each must land on a stroke, a dot or an outline
46
+ (within options.tolerance, 1 viewBox px), or hide under a fill painted later; a short
47
+ straight mark (a tick) needs one rooted end; lines with data-free and fade ramps are
48
+ exempt. Prints each floating end in card px; out.png marks them with rings. Any failure
49
+ makes the run exit 1
50
+ ["turn", value, group?] set a 3D figure's turn through window.__isoTurn (a number sets the first turn group,
51
+ group names another), then wait two animation frames
52
+ ["orbit", out.png, from, to, step, sel?, scale?, [x,y,w,h]?, columns?]
53
+ a contact sheet across angles of a 3D figure: each frame is turned, waits two
54
+ animation frames, is captured and labelled with its angle
55
+ ["sheet", out.png, every, count, sel?, scale?, [x,y,w,h]?, columns?]
56
+ take count frames every ms and tile them into one PNG, to catch pops and seams;
57
+ each capture also takes 60-200 ms (more on heavy WebGL pages), so combine with ["slow", 0.05] for transitions
58
+
59
+ Prints the readout after each shot, then console errors and exceptions.`);
60
+ }
61
+
62
+ function chromePath() {
63
+ const candidates = [
64
+ process.env.CHROME_PATH,
65
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
66
+ "/Applications/Chromium.app/Contents/MacOS/Chromium",
67
+ "/usr/bin/google-chrome",
68
+ "/usr/bin/google-chrome-stable",
69
+ "/usr/bin/chromium",
70
+ "/usr/bin/chromium-browser",
71
+ ].filter(Boolean);
72
+ const playwright = join(homedir(), "Library/Caches/ms-playwright");
73
+ if (existsSync(playwright)) {
74
+ for (const dir of readdirSync(playwright).sort().reverse()) {
75
+ for (const sub of ["chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing", "chrome-mac/Chromium.app/Contents/MacOS/Chromium", "chrome-linux/chrome"]) {
76
+ candidates.push(join(playwright, dir, sub));
77
+ }
78
+ }
79
+ }
80
+ return candidates.find((path) => existsSync(path));
81
+ }
82
+
83
+ async function main() {
84
+ const argv = process.argv.slice(2);
85
+ if (!argv.length || argv.includes("--help") || argv.includes("-h")) {
86
+ usage();
87
+ process.exit(argv.length ? 0 : 1);
88
+ }
89
+ const options = { width: 1440, height: 1100, scale: 1, reduced: false, theme: null, actions: null, noWebgl: false, preload: null };
90
+ const rest = [];
91
+ for (let index = 0; index < argv.length; index++) {
92
+ const arg = argv[index];
93
+ if (arg === "--width") options.width = Number(argv[++index]);
94
+ else if (arg === "--height") options.height = Number(argv[++index]);
95
+ else if (arg === "--scale") options.scale = Number(argv[++index]);
96
+ else if (arg === "--reduced") options.reduced = true;
97
+ else if (arg === "--theme") options.theme = argv[++index];
98
+ else if (arg === "--no-webgl") options.noWebgl = true;
99
+ else if (arg === "--preload") options.preload = readFileSync(argv[++index], "utf8");
100
+ else if (arg === "--actions") options.actions = JSON.parse(readFileSync(argv[++index], "utf8"));
101
+ else rest.push(arg);
102
+ }
103
+ const [target, script] = rest;
104
+ const actions = options.actions ?? JSON.parse(script ?? "[]");
105
+ const kinds = new Set(["scroll", "wait", "move", "click", "drag", "key", "slow", "until", "eval", "shot", "lines", "turn", "orbit", "sheet"]);
106
+ if (!Array.isArray(actions)) throw new Error("actions must be an array");
107
+ for (const action of actions) {
108
+ if (!Array.isArray(action) || !kinds.has(action[0])) throw new Error(`unknown action ${Array.isArray(action) ? action[0] : action}`);
109
+ }
110
+ const url = /^https?:|^file:/.test(target) ? target : pathToFileURL(resolve(target)).href;
111
+ const binary = chromePath();
112
+ if (!binary) {
113
+ console.error("No Chrome found. Set CHROME_PATH to a Chrome or Chromium binary.");
114
+ process.exit(1);
115
+ }
116
+ const profile = mkdtempSync(join(tmpdir(), "iso-drive-"));
117
+ const chrome = spawn(binary, ["--headless=new", "--remote-debugging-port=0", `--user-data-dir=${profile}`, "--no-first-run", "--no-default-browser-check", "--hide-scrollbars", "--allow-file-access-from-files", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist", "about:blank"]);
118
+ const port = await chromePort(chrome, profile);
119
+ const finish = (code) => {
120
+ chrome.kill("SIGKILL");
121
+ try {
122
+ rmSync(profile, { recursive: true, force: true });
123
+ } catch {}
124
+ process.exit(code);
125
+ };
126
+ try {
127
+ const page = await (await fetch(`http://127.0.0.1:${port}/json/new?about:blank`, { method: "PUT" })).json();
128
+ const socket = new WebSocket(page.webSocketDebuggerUrl);
129
+ let id = 0;
130
+ const pending = new Map();
131
+ const problems = [];
132
+ socket.onmessage = (message) => {
133
+ const data = JSON.parse(message.data);
134
+ if (data.id && pending.has(data.id)) {
135
+ pending.get(data.id)(data);
136
+ pending.delete(data.id);
137
+ } else if (data.method === "Runtime.consoleAPICalled" && (data.params.type === "error" || data.params.type === "warning")) {
138
+ problems.push(`console.${data.params.type}: ${data.params.args.map((arg) => arg.value ?? arg.description ?? "").join(" ").slice(0, 600)}`);
139
+ } else if (data.method === "Runtime.exceptionThrown") {
140
+ const detail = data.params.exceptionDetails;
141
+ problems.push(`exception: ${detail.exception?.description ?? detail.text}`.slice(0, 600));
142
+ }
143
+ };
144
+ await new Promise((done) => (socket.onopen = done));
145
+ const send = (method, params = {}) =>
146
+ new Promise((done) => {
147
+ const call = ++id;
148
+ pending.set(call, done);
149
+ socket.send(JSON.stringify({ id: call, method, params }));
150
+ });
151
+ const evaluate = async (expression) => {
152
+ const reply = await send("Runtime.evaluate", { expression, awaitPromise: true, returnByValue: true });
153
+ if (reply.error) throw new Error(reply.error.message);
154
+ const exception = reply.result?.exceptionDetails;
155
+ if (exception) throw new Error(exception.exception?.description ?? exception.text);
156
+ return reply.result?.result?.value;
157
+ };
158
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
159
+ await send("Runtime.enable");
160
+ await send("Page.enable");
161
+ const features = [];
162
+ if (options.reduced) features.push({ name: "prefers-reduced-motion", value: "reduce" });
163
+ if (options.theme) features.push({ name: "prefers-color-scheme", value: options.theme });
164
+ if (features.length) await send("Emulation.setEmulatedMedia", { features });
165
+ const SLOW = `(()=>{const raf=window.requestAnimationFrame.bind(window);let real=null,virtual=0;window.__driveSpeed=1;window.requestAnimationFrame=(callback)=>raf((now)=>{if(real===null){real=now;virtual=now}virtual+=(now-real)*window.__driveSpeed;real=now;callback(virtual)});const clock=performance.now.bind(performance);let base=null,shown=0;performance.now=()=>{const now=clock();if(base===null){base=now;shown=now}shown+=(now-base)*window.__driveSpeed;base=now;return shown}})();`;
166
+ const NO_GL = `(()=>{const get=HTMLCanvasElement.prototype.getContext;HTMLCanvasElement.prototype.getContext=function(kind,...rest){if(/webgl/.test(String(kind)))return null;return get.call(this,kind,...rest)}})();`;
167
+ const early = [SLOW, options.noWebgl ? NO_GL : "", options.preload ?? ""].filter(Boolean).join("\n");
168
+ await send("Page.addScriptToEvaluateOnNewDocument", { source: early });
169
+ await send("Emulation.setDeviceMetricsOverride", { width: options.width, height: options.height, deviceScaleFactor: options.scale, mobile: options.width < 600 });
170
+ const navigation = await send("Page.navigate", { url });
171
+ if (navigation.error) throw new Error(`Navigation failed: ${navigation.error.message}`);
172
+ if (navigation.result?.errorText) throw new Error(`Navigation failed: ${navigation.result.errorText}`);
173
+ await sleep(1200);
174
+ const targetOf = (selector, fallback = ".iso-plate") => `(()=>{const select=${JSON.stringify(selector ?? null)};const el=document.querySelector(select??${JSON.stringify(fallback)});if(select!==null&&!el)throw new Error("Drive target not found: "+select);return el})()`;
175
+ const boxOf = (selector) => evaluate(`(()=>{const el=${targetOf(selector)}||document.body;const r=el.getBoundingClientRect();return {x:r.left,y:r.top,w:r.width,h:r.height,sx:scrollX,sy:scrollY}})()`);
176
+ const readout = (selector) => evaluate(selector == null ? `(document.querySelector("[data-readout]")||{}).textContent||""` : `(()=>{const el=${targetOf(selector)};return(el.matches('[data-readout]')?el:el.querySelector('[data-readout]')||el.closest('.iso-plate')?.querySelector('[data-readout]')||{}).textContent||""})()`);
177
+ const capture = async (selector, scale = 1, region) => {
178
+ const box = await boxOf(selector);
179
+ const x = box.x + box.sx;
180
+ const y = box.y + box.sy;
181
+ const clip = region ? { x: x + region[0], y: y + region[1], width: region[2], height: region[3], scale } : { x: x - 4, y: y - 4, width: box.w + 8, height: box.h + 8, scale };
182
+ const shot = await send("Page.captureScreenshot", { format: "png", clip, captureBeyondViewport: true });
183
+ return shot.result.data;
184
+ };
185
+ const save = (out, base64) => {
186
+ mkdirSync(dirname(resolve(out)), { recursive: true });
187
+ writeFileSync(out, Buffer.from(base64, "base64"));
188
+ };
189
+ let mouse = [4, 4];
190
+ let failed = 0;
191
+ for (const action of actions) {
192
+ const [kind, ...args] = action;
193
+ if (kind === "wait") await sleep(args[0]);
194
+ else if (kind === "scroll") await evaluate(`(()=>{const el=${targetOf(args[0])};if(el)el.scrollIntoView({block:"center"})})()`);
195
+ else if (kind === "move") {
196
+ const box = await boxOf(args[0]);
197
+ const to = [box.x + box.w * args[1], box.y + box.h * args[2]];
198
+ for (let step = 1; step <= 10; step++) {
199
+ await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: mouse[0] + ((to[0] - mouse[0]) * step) / 10, y: mouse[1] + ((to[1] - mouse[1]) * step) / 10 });
200
+ await sleep(16);
201
+ }
202
+ mouse = to;
203
+ } else if (kind === "click") {
204
+ const box = await boxOf(args[0]);
205
+ const to = [box.x + box.w * (args[1] ?? 0.5), box.y + box.h * (args[2] ?? 0.5)];
206
+ await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: to[0], y: to[1] });
207
+ await send("Input.dispatchMouseEvent", { type: "mousePressed", x: to[0], y: to[1], button: "left", clickCount: 1 });
208
+ await send("Input.dispatchMouseEvent", { type: "mouseReleased", x: to[0], y: to[1], button: "left", clickCount: 1 });
209
+ mouse = to;
210
+ } else if (kind === "drag") {
211
+ const box = await boxOf(args[0]);
212
+ const from = [box.x + box.w * args[1], box.y + box.h * args[2]];
213
+ const to = [box.x + box.w * args[3], box.y + box.h * args[4]];
214
+ const steps = Math.max(1, args[5] ?? 20);
215
+ await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: from[0], y: from[1] });
216
+ await send("Input.dispatchMouseEvent", { type: "mousePressed", x: from[0], y: from[1], button: "left", buttons: 1, clickCount: 1 });
217
+ for (let step = 1; step <= steps; step++) {
218
+ await send("Input.dispatchMouseEvent", { type: "mouseMoved", x: from[0] + ((to[0] - from[0]) * step) / steps, y: from[1] + ((to[1] - from[1]) * step) / steps, button: "left", buttons: 1 });
219
+ await sleep(16);
220
+ }
221
+ await send("Input.dispatchMouseEvent", { type: "mouseReleased", x: to[0], y: to[1], button: "left", buttons: 0, clickCount: 1 });
222
+ mouse = to;
223
+ } else if (kind === "key") {
224
+ await evaluate(`(()=>{const el=${targetOf(args[1], '[tabindex="0"]')};if(el)el.focus()})()`);
225
+ const key = args[0];
226
+ const named = { " ": "Space", "-": "Minus", "=": "Equal", ",": "Comma", ".": "Period", "/": "Slash", ";": "Semicolon", "'": "Quote", "[": "BracketLeft", "]": "BracketRight", "\\": "Backslash", "`": "Backquote" };
227
+ const code = args[2] ?? (named[key] ?? (/^[0-9]$/.test(key) ? `Digit${key}` : key.length === 1 ? `Key${key.toUpperCase()}` : key));
228
+ await send("Input.dispatchKeyEvent", { type: "keyDown", key, code, text: key === "Enter" ? "\r" : key.length === 1 ? key : undefined, windowsVirtualKeyCode: key === " " ? 32 : key === "Enter" ? 13 : 0 });
229
+ await send("Input.dispatchKeyEvent", { type: "keyUp", key, code, windowsVirtualKeyCode: key === "Enter" ? 13 : undefined });
230
+ } else if (kind === "eval") console.log("eval:", JSON.stringify(await evaluate(args[0])));
231
+ else if (kind === "slow") await evaluate(`window.__driveSpeed=${Number(args[0])}`);
232
+ else if (kind === "until") {
233
+ const limit = Date.now() + (args[1] ?? 10000);
234
+ let ok = false;
235
+ while (Date.now() < limit) {
236
+ if (await evaluate(`Boolean(${args[0]})`)) {
237
+ ok = true;
238
+ break;
239
+ }
240
+ await sleep(25);
241
+ }
242
+ if (!ok) throw new Error(`until: timed out waiting for ${args[0]}`);
243
+ }
244
+ else if (kind === "shot") {
245
+ const [out, selector, scale, region] = args;
246
+ save(out, await capture(selector, scale ?? 1, region));
247
+ console.log(`shot ${out} · readout "${await readout(selector)}"`);
248
+ } else if (kind === "sheet") {
249
+ const [out, every, count, selector, scale, region, columns] = args;
250
+ const frames = [];
251
+ const texts = [];
252
+ for (let index = 0; index < count; index++) {
253
+ if (index) await sleep(every);
254
+ frames.push(await capture(selector, scale ?? 0.5, region));
255
+ texts.push(await readout(selector));
256
+ }
257
+ const tiled = await evaluate(`(async()=>{const frames=${JSON.stringify(frames)};const cols=${columns ?? Math.min(count, 6)};const images=await Promise.all(frames.map(src=>new Promise(done=>{const image=new Image();image.onload=()=>done(image);image.src="data:image/png;base64,"+src})));const w=images[0].width,h=images[0].height,rows=Math.ceil(images.length/cols);const canvas=document.createElement("canvas");canvas.width=w*cols;canvas.height=h*rows;const ctx=canvas.getContext("2d");ctx.fillStyle="#808080";ctx.fillRect(0,0,canvas.width,canvas.height);images.forEach((image,index)=>{ctx.drawImage(image,(index%cols)*w,Math.floor(index/cols)*h);ctx.fillStyle="#ff00aa";ctx.font="bold 12px monospace";ctx.fillText(String(index),(index%cols)*w+6,Math.floor(index/cols)*h+16)});return canvas.toDataURL("image/png").split(",")[1]})()`);
258
+ save(out, tiled);
259
+ console.log(`sheet ${out} · ${count} frames every ${every} ms · readouts ${texts.map((text, index) => `${index}:"${text}"`).join(" ")}`);
260
+ } else if (kind === "turn") {
261
+ const [value, group] = args;
262
+ const call = group ? `window.__isoTurn.set(${JSON.stringify({ [group]: value })})` : `window.__isoTurn.set(${typeof value === "object" ? JSON.stringify(value) : Number(value)})`;
263
+ await evaluate(`(()=>{if(!window.__isoTurn)return false;${call};return new Promise((done)=>requestAnimationFrame(()=>requestAnimationFrame(()=>done(true))))})()`);
264
+ } else if (kind === "orbit") {
265
+ const [out, from, to, step, selector, scale, region, columns] = args;
266
+ const frames = [];
267
+ const labels = [];
268
+ for (let at = from; step > 0 ? at <= to + 1e-9 : at >= to - 1e-9; at += step) {
269
+ const angle = Math.round(at * 1000) / 1000;
270
+ await evaluate(`(()=>{if(!window.__isoTurn)return false;window.__isoTurn.set(${angle});return new Promise((done)=>requestAnimationFrame(()=>requestAnimationFrame(()=>done(true))))})()`);
271
+ frames.push(await capture(selector, scale ?? 0.5, region));
272
+ labels.push(`${angle}°`);
273
+ }
274
+ const tiled = await evaluate(`(async()=>{const frames=${JSON.stringify(frames)};const labels=${JSON.stringify(labels)};const cols=${columns ?? 6};const images=await Promise.all(frames.map(src=>new Promise(done=>{const image=new Image();image.onload=()=>done(image);image.src="data:image/png;base64,"+src})));const w=images[0].width,h=images[0].height,rows=Math.ceil(images.length/cols);const canvas=document.createElement("canvas");canvas.width=w*cols;canvas.height=h*rows;const ctx=canvas.getContext("2d");ctx.fillStyle="#808080";ctx.fillRect(0,0,canvas.width,canvas.height);images.forEach((image,index)=>{ctx.drawImage(image,(index%cols)*w,Math.floor(index/cols)*h);ctx.fillStyle="#ff00aa";ctx.font="bold 12px monospace";ctx.fillText(labels[index],(index%cols)*w+6,Math.floor(index/cols)*h+16)});return canvas.toDataURL("image/png").split(",")[1]})()`);
275
+ save(out, tiled);
276
+ console.log(`orbit ${out} · ${frames.length} frames ${from}°…${to}° every ${step}°`);
277
+ } else if (kind === "lines") {
278
+ const [out, selector, extra] = args;
279
+ const result = await evaluate(`(${LINE_ENDS})(${JSON.stringify({ select: selector ?? null, show: Boolean(out), ...(extra ?? {}) })})`);
280
+ if (!result) {
281
+ console.log("lines: the check did not run");
282
+ failed++;
283
+ continue;
284
+ }
285
+ console.log(`line ends: ${result.problems.length} of ${result.ends} ends on ${result.lines} lines float`);
286
+ for (const problem of result.problems) console.log(` ${problem.what} ${problem.end}${problem.mark ? " (a mark: neither end rooted)" : ""} ${problem.gap == null ? "nothing nearby" : `${problem.gap.toFixed(2)} px from anything`} at card ${problem.at.join(",")} length ${problem.length} d ${problem.d}…${problem.opacity < 1 ? ` opacity ${problem.opacity}` : ""}`);
287
+ if (out) {
288
+ save(out, await capture(selector, 2));
289
+ await evaluate(`(document.querySelector("[data-line-ends]")||{remove(){}}).remove()`);
290
+ console.log(`shot ${out}`);
291
+ }
292
+ failed += result.problems.length;
293
+ }
294
+ }
295
+ console.log(problems.length ? problems.join("\n") : "console: clean");
296
+ socket.close();
297
+ finish(failed ? 1 : 0);
298
+ } catch (error) {
299
+ console.error(error);
300
+ finish(1);
301
+ }
302
+ }
@@ -0,0 +1,72 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+ import test from "node:test";
9
+
10
+ const execute = promisify(execFile);
11
+
12
+ async function drive(expression, action = "eval") {
13
+ const directory = await mkdtemp(join(tmpdir(), "anatomy-drive-test-"));
14
+ try {
15
+ const page = join(directory, "figure.html");
16
+ const actions = join(directory, "actions.json");
17
+ await writeFile(page, '<!doctype html><div class="iso-plate" tabindex="0">Owned figure</div>');
18
+ await writeFile(actions, JSON.stringify([[action, expression, 200]]));
19
+ try {
20
+ return { code: 0, ...await execute(process.execPath, [fileURLToPath(new URL("drive.mjs", import.meta.url)), page, "--actions", actions], { timeout: 30000 }) };
21
+ } catch (error) {
22
+ return { code: error.code, stdout: error.stdout, stderr: error.stderr };
23
+ }
24
+ } finally {
25
+ await rm(directory, { recursive: true, force: true });
26
+ }
27
+ }
28
+
29
+ test("drive fails when a browser assertion throws", async () => {
30
+ const result = await drive("(()=>{throw new Error('drive assertion failed')})()");
31
+ assert.equal(result.code, 1);
32
+ assert.match(result.stderr, /drive assertion failed/);
33
+ assert.doesNotMatch(result.stdout, /console: clean/);
34
+ });
35
+
36
+ test("drive fails when an awaited browser promise rejects", async () => {
37
+ const result = await drive("Promise.reject(new Error('drive async assertion failed'))");
38
+ assert.equal(result.code, 1);
39
+ assert.match(result.stderr, /drive async assertion failed/);
40
+ });
41
+
42
+ test("drive preserves a successful JSON evaluation", async () => {
43
+ const result = await drive("({checked: true})");
44
+ assert.equal(result.code, 0);
45
+ assert.match(result.stdout, /eval: \{"checked":true\}/);
46
+ assert.match(result.stdout, /console: clean/);
47
+ });
48
+
49
+ test("drive preserves a successful undefined evaluation", async () => {
50
+ const result = await drive("void 0");
51
+ assert.equal(result.code, 0);
52
+ assert.match(result.stdout, /eval: undefined/);
53
+ assert.match(result.stdout, /console: clean/);
54
+ });
55
+
56
+ test("drive reports a browser evaluation syntax error", async () => {
57
+ const result = await drive("(()");
58
+ assert.equal(result.code, 1);
59
+ assert.match(result.stderr, /SyntaxError/);
60
+ });
61
+
62
+ test("drive reports an assertion thrown while waiting for a condition", async () => {
63
+ const result = await drive("(()=>{throw new Error('until assertion failed')})()", "until");
64
+ assert.equal(result.code, 1);
65
+ assert.match(result.stderr, /until assertion failed/);
66
+ });
67
+
68
+ test("drive preserves a successful wait condition", async () => {
69
+ const result = await drive("document.querySelector('.iso-plate') !== null", "until");
70
+ assert.equal(result.code, 0);
71
+ assert.match(result.stdout, /console: clean/);
72
+ });
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import test from "node:test";
8
+
9
+ const execute = promisify(execFile);
10
+ async function run(kind, args) {
11
+ const directory = await mkdtemp(join(tmpdir(), "anatomy-enter-key-"));
12
+ try {
13
+ const file = join(directory, "figure.html");
14
+ await writeFile(file, `<!doctype html><div class="iso-plate" style="width:400px;height:180px"><button id="button" type="button" tabindex="0" onclick="window.clicked=(window.clicked||0)+1;document.querySelector('[data-readout]').textContent='activated'">Activate</button><form onsubmit="event.preventDefault();window.submitted=(window.submitted||0)+1"><input id="field" tabindex="0"></form><input id="range" type="range" value="5" min="0" max="10"><span data-readout>waiting</span></div>`);
15
+ return await execute(process.execPath, ["scripts/" + kind + ".mjs", file, ...(kind === "capture" ? ["--out", join(directory, "shot.png"), "--wait", "0", "--key", args] : [JSON.stringify(args)])], { timeout: 30000 });
16
+ } finally { await rm(directory, { recursive: true, force: true }); }
17
+ }
18
+ for (const code of [undefined, "NumpadEnter"])
19
+ test(`drive Enter ${code || "default"} performs the native button action`, async () => {
20
+ const result = await run("drive", [["key", "Enter", "#button", code], ["eval", "window.clicked||0"]]);
21
+ assert.match(result.stdout, /eval: 1/);
22
+ });
23
+ test("drive Enter performs native implicit form submission", async () => {
24
+ const result = await run("drive", [["key", "Enter", "#field"], ["eval", "window.submitted||0"]]);
25
+ assert.match(result.stdout, /eval: 1/);
26
+ });
27
+ test("capture Enter applies native activation before the screenshot", async () => {
28
+ const result = await run("capture", "Enter");
29
+ assert.match(result.stdout, /readout "activated"/);
30
+ });
31
+ test("drive preserves native Space, arrow and printable key behavior", async () => {
32
+ const result = await run("drive", [["key", " ", "#button"], ["key", "ArrowRight", "#range"], ["key", "a", "#field"], ["eval", "({clicked:window.clicked||0,range:document.querySelector('#range').value,text:document.querySelector('#field').value})"]]);
33
+ assert.match(result.stdout, /"clicked":1,"range":"6","text":"a"/);
34
+ });
35
+ test("capture preserves native Space activation", async () => {
36
+ const result = await run("capture", "Space");
37
+ assert.match(result.stdout, /readout "activated"/);
38
+ });
@@ -0,0 +1,54 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { pathToFileURL } from "node:url";
8
+ import test from "node:test";
9
+ const execute = promisify(execFile);
10
+ async function run(kind, split = false, open = false, corrupt = null) {
11
+ const directory = await mkdtemp(join(tmpdir(), "anatomy-fidelity-cut-"));
12
+ try {
13
+ const file = join(directory, "fixture.mjs");
14
+ const module = pathToFileURL(resolve("kit/turn-build.mjs")).href;
15
+ const frameModule = pathToFileURL(resolve("kit/lathe.mjs")).href;
16
+ const runtime = pathToFileURL(resolve("kit/turn.mjs")).href;
17
+ await writeFile(file, `import {turning} from ${JSON.stringify(module)};import * as G from ${JSON.stringify(frameModule)};
18
+ export const P={origin:[0,0],scale:1,azimuth:45,elevation:30};export const T=turning(P);T.layer('parts');const group=T.group('assembly',{turn:{pivot:[5,2]}});const F=G.frameAlong([0,0,0],[0,0,1]);const base={name:'body',group,F,s0:0,s1:10};const part=${kind === "prism" ? "T.prism({...base,polygon:[[-2,-2],[2,-2],[2,2],[-2,2]]})" : `T.round({...base,r:2,fixed:false,ends:${open ? "['open','flat']" : "['flat','flat']"}})`};${split ? "T.split(part,{n:[0,0,1],d:5});" : ""}T.build();${corrupt ? `const {TURN}=await import(${JSON.stringify(runtime)});const emit=TURN.emit;TURN.emit=(run,cam,frame)=>{const f=emit(run,cam,frame);for(const role of ${JSON.stringify(corrupt === "edge" ? ["edge"] : corrupt === "fill" ? ["fill"] : ["fill", "edge"])})if(run.role[role]!==undefined)f.d[run.role[role]]=f.d[run.role[role]].replace(/-?\\d+(?:\\.\\d+)?/g,n=>String(Number(n)+20));return f;};` : ""}`);
19
+ return await execute(process.execPath, ["scripts/turn-check.mjs", file, "--fidelity", "--angles", "0,45,90,180"], { timeout: 30000 });
20
+ } finally { await rm(directory, { recursive: true, force: true }); }
21
+ }
22
+ function compared(stdout, kind) {
23
+ if (kind === "prism") assert.ok(Number(stdout.match(/over (\d+) faces/)[1]) > 0);
24
+ else {
25
+ assert.ok(Number(stdout.match(/rounds: (\d+) frames/)[1]) > 0);
26
+ assert.ok(Number(stdout.match(/of (\d+) interior cells/)[1]) > 0);
27
+ }
28
+ }
29
+ for (const kind of ["prism", "round"]) test(`actual fidelity CLI accepts split ${kind} with intentionally unstroked cuts`, async () => {
30
+ const result = await run(kind, true);
31
+ assert.match(result.stdout, /fidelity passed/);
32
+ compared(result.stdout, kind);
33
+ if (kind === "prism") assert.match(result.stdout, /control .*fails as it should/);
34
+ });
35
+ test("actual fidelity CLI honors an unsplit open round end", async () => {
36
+ const result = await run("round", false, true);
37
+ assert.match(result.stdout, /fidelity passed/);
38
+ compared(result.stdout, "round");
39
+ });
40
+ for (const kind of ["prism", "round"]) test(`actual fidelity CLI preserves an unsplit closed ${kind} control`, async () => {
41
+ const result = await run(kind);
42
+ assert.match(result.stdout, /fidelity passed/);
43
+ compared(result.stdout, kind);
44
+ if (kind === "prism") assert.match(result.stdout, /control .*fails as it should/);
45
+ });
46
+ for (const kind of ["prism", "round"]) for (const split of [false, true]) for (const fault of (split ? ["edge", "geometry", "fill"] : ["edge", "geometry"]))
47
+ test(`actual fidelity CLI detects ${fault} corruption of ${split ? "split" : "unsplit"} ${kind}`, async () => {
48
+ await assert.rejects(run(kind, split, false, fault), (error) => {
49
+ assert.equal(error.code, 1);
50
+ assert.match(error.stdout, /(?:prisms|rounds) FAILED/);
51
+ assert.match(error.stdout, /fidelity failed/);
52
+ return true;
53
+ });
54
+ });
@@ -0,0 +1,40 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { pathToFileURL } from "node:url";
8
+ import test from "node:test";
9
+ const execute = promisify(execFile);
10
+ async function run(mode = "healthy", role = "body", caps = true) {
11
+ const directory = await mkdtemp(join(tmpdir(), "anatomy-fidelity-tube-"));
12
+ try {
13
+ const file = join(directory, "fixture.mjs");
14
+ const build = pathToFileURL(resolve("kit/turn-build.mjs")).href;
15
+ const runtime = pathToFileURL(resolve("kit/turn.mjs")).href;
16
+ await writeFile(file, `import {turning} from ${JSON.stringify(build)};import {TURN} from ${JSON.stringify(runtime)};
17
+ export const P={origin:[0,0],scale:1,azimuth:45,elevation:30};export const T=turning(P);T.layer('parts');const group=T.group('assembly',{turn:{pivot:[5,2]}});T.tube({name:'route',group,route:[[0,0,0],[0,0,10]],r:1,chunk:20,caps:${caps}});T.build();
18
+ const emit=TURN.emit;TURN.emit=(run,cam,frame)=>{const f=emit(run,cam,frame);if(run.kind==='tube'&&${JSON.stringify(mode)}!=='healthy'){const index=run.role[${JSON.stringify(role)}];if(index!==undefined)f.d[index]=f.d[index].replace(/M([^M]*)/g,(all,body)=>{const nums=(body.match(/-?\\d+(?:\\.\\d+)?/g)??[]).map(Number);const mode=${JSON.stringify(mode)};if(mode==='empty')return'M';if(mode==='single')return'M'+nums.slice(0,2).join(' ');if(mode==='partial')return'M'+[0,2,4].map(i=>nums.slice(i,i+2).join(' ')).join('L')+(/Z\\s*$/.test(body)?'Z':'');if(mode==='offset')return all.replace(/-?\\d+(?:\\.\\d+)?/g,n=>String(Number(n)+20));const pts=[];for(let i=0;i<nums.length;i+=2){if(i)pts.push([(nums[i-2]+nums[i])/2,(nums[i-1]+nums[i+1])/2]);pts.push([nums[i],nums[i+1]]);}return'M'+pts.map(p=>p.join(' ')).join('L')+(/Z\\s*$/.test(body)?'Z':'');});}return f;};`);
19
+ return await execute(process.execPath, ["scripts/turn-check.mjs", file, "--fidelity", "--angles", "0,45,90,180"], { timeout: 30000 });
20
+ } finally { await rm(directory, { recursive: true, force: true }); }
21
+ }
22
+ for (const role of ["body", "edges"]) for (const mode of ["empty", "single", "partial"]) test(`native fidelity rejects ${mode} ${role} path coverage`, async () => {
23
+ await assert.rejects(run(mode, role), (error) => {
24
+ assert.equal(error.code, 1);
25
+ assert.match(error.stdout, /tubes FAILED/);
26
+ assert.match(error.stdout, /fidelity failed/);
27
+ return true;
28
+ });
29
+ });
30
+ for (const caps of [true, false]) test(`native fidelity preserves a healthy ${caps ? "capped" : "open"} tube`, async () => {
31
+ const result = await run("healthy", "body", caps);
32
+ assert.match(result.stdout, /tubes: [1-9]\d* chunk frames at 4 angles/);
33
+ assert.match(result.stdout, /fidelity passed/);
34
+ });
35
+ for (const role of ["body", "edges"]) test(`native fidelity permits equivalent ${role} resampling`, async () => {
36
+ assert.match((await run("resample", role)).stdout, /fidelity passed/);
37
+ });
38
+ test("native fidelity continues rejecting displaced tube geometry", async () => {
39
+ await assert.rejects(run("offset"), (error) => error.code === 1 && /tubes FAILED/.test(error.stdout));
40
+ });
@@ -0,0 +1,26 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const KIT = join(dirname(fileURLToPath(import.meta.url)), "..", "kit");
6
+ const strip = (file) => readFileSync(join(KIT, file), "utf8").replace(/^export /gm, "");
7
+
8
+ export function kitScript() {
9
+ return strip("iso-kit.mjs");
10
+ }
11
+
12
+ export function glScript() {
13
+ return strip("gl.mjs");
14
+ }
15
+
16
+ export function turnScript() {
17
+ return strip("turn.mjs");
18
+ }
19
+
20
+ export function fixedScript() {
21
+ return strip("turn-fixed.mjs") + "\n" + strip("canvas-painter.mjs");
22
+ }
23
+
24
+ export function glSharedScript() {
25
+ return strip("gl-shared.mjs");
26
+ }