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,413 @@
1
+ import * as k from "../kit/iso-kit.mjs";
2
+ import * as G from "../kit/lathe.mjs";
3
+ import { tubePieces } from "../kit/tube.mjs";
4
+ import { TURN } from "../kit/turn.mjs";
5
+
6
+ const rootOf = (group) => {
7
+ let root = group;
8
+ while (root && root.parent) root = root.parent;
9
+ return root && root.kind === "turn" ? root : null;
10
+ };
11
+ const restValues = (T, theta) => Object.fromEntries(T.groups.map((g) => [g.name, g.kind === "turn" && !g.parent ? theta : g.kind === "free" ? null : 0]));
12
+
13
+ export function identity(T, P, angles, { plant = null } = {}) {
14
+ const scene = TURN.prepare(T.data, { unit: 1 });
15
+ const subpaths = (d) => {
16
+ const out = [];
17
+ for (const [, body] of (d ?? "").matchAll(/M([^M]*)/g)) {
18
+ const nums = (body.match(/-?\d+(?:\.\d+)?(?:e-?\d+)?/g) ?? []).map(Number);
19
+ const pts = [];
20
+ for (let i = 0; i + 1 < nums.length; i += 2) pts.push([nums[i], nums[i + 1]]);
21
+ out.push({ pts, closed: /Z\s*$/.test(body) });
22
+ }
23
+ return out;
24
+ };
25
+ const segDist = (p, a, b) => {
26
+ const dx = b[0] - a[0];
27
+ const dy = b[1] - a[1];
28
+ const l = dx * dx + dy * dy;
29
+ const t = l ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l)) : 0;
30
+ return Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t);
31
+ };
32
+ const toPoly = (pts, closed) => (pts, closed);
33
+ void toPoly;
34
+ const distToPoly = (p, poly, closed = true) => {
35
+ let best = Infinity;
36
+ const n = poly.length;
37
+ for (let i = 0; i < (closed ? n : n - 1); i++) best = Math.min(best, segDist(p, poly[i], poly[(i + 1) % n]));
38
+ if (n === 1) best = Math.hypot(p[0] - poly[0][0], p[1] - poly[0][1]);
39
+ return best;
40
+ };
41
+ const oneWay = (A, B) => {
42
+ let worst = 0;
43
+ for (const p of A) {
44
+ let near = null;
45
+ let gap = Infinity;
46
+ for (const q of B) {
47
+ const g = Math.hypot(p[0] - q[0], p[1] - q[1]);
48
+ if (g < gap) {
49
+ gap = g;
50
+ near = q;
51
+ }
52
+ }
53
+ worst = Math.max(worst, gap < 0.15 ? Math.max(Math.abs(p[0] - near[0]), Math.abs(p[1] - near[1])) : distToPoly(p, B));
54
+ }
55
+ return worst;
56
+ };
57
+ const hausdorff = (A, B) => Math.max(oneWay(A, B), oneWay(B, A));
58
+ const applyMatrix = (pts, t) => {
59
+ const v = (t.match(/-?\d+(?:\.\d+)?(?:e-?\d+)?/g) ?? []).map(Number);
60
+ return pts.map(([x, y]) => [v[0] * x + v[2] * y + v[4], v[1] * x + v[3] * y + v[5]]);
61
+ };
62
+ const levelOf = (score, bands) => {
63
+ let u = 0;
64
+ for (const t of bands) {
65
+ const x = Math.min(1, Math.max(0, (score - (t - 0.035)) / 0.07));
66
+ u += x * x * (3 - 2 * x);
67
+ }
68
+ return u;
69
+ };
70
+
71
+
72
+ const results = { outline: 0, top: 0, faces: 0, compared: 0, skipped: 0, missing: 0, crease: 0 };
73
+ const worstAt = {};
74
+ for (const theta of angles) {
75
+ const cams = TURN.cams(scene, restValues(T, theta));
76
+ const Pp = { ...P, azimuth: P.azimuth - theta };
77
+ for (const part of T.parts) {
78
+ if (part.kind !== "prism" || !rootOf(part.group) || part.group.kind === "free") continue;
79
+ const planted = plant && part.name === plant;
80
+ const run = scene.runs[part.id];
81
+ const cam = part.group ? cams[part.group.index] : cams.world;
82
+ const f = TURN.emit(run, cam, TURN.frame(run));
83
+ let root = part.group;
84
+ while (root && root.parent) root = root.parent;
85
+ const pivot = root ? [root.origin[0], root.origin[1], 0] : [0, 0, 0];
86
+ const a = k.iso(pivot, P);
87
+ const b = k.iso(pivot, Pp);
88
+ const shift = [a[0] - b[0], a[1] - b[1]];
89
+ const sh = (pts) => pts.map(([x, y]) => [x + shift[0], y + shift[1]]);
90
+ const { F, ring, s0, s1 } = part.world;
91
+ const up = run.up;
92
+ const Pk = planted ? { ...Pp, azimuth: Pp.azimuth - 2 } : Pp;
93
+ const kit = up ? k.extrude(ring, s0, s1 - s0, Pk) : G.prismOf(F, ring, s0, s1, Pk);
94
+ const cut = Boolean(part.spec.cuts?.length);
95
+ const mine = subpaths(f.d[run.role[cut ? "fill" : "edge"]])[0].pts;
96
+ const theirs = sh(subpaths(kit.fill)[0].pts);
97
+ const visible = cut ? oneWay(subpaths(f.d[run.role.edge]).flatMap((path) => path.pts), theirs) : 0;
98
+ const ho = Math.max(hausdorff(mine, theirs), visible);
99
+ results.outline = Math.max(results.outline, ho);
100
+ if (ho > (worstAt[part.name]?.outline ?? -1)) worstAt[part.name] = { ...(worstAt[part.name] ?? {}), outline: ho, at: theta };
101
+ if (up) {
102
+ const topMine = applyMatrix(subpaths(part.planPath)[0].pts, f.t[run.role.top]);
103
+ const topKit = sh(subpaths(kit.top)[0].pts);
104
+ results.top = Math.max(results.top, hausdorff(topMine, topKit));
105
+ }
106
+ const kitFaces = kit.shades.map((d) => subpaths(d)).concat(up ? [] : [subpaths(kit.top)]);
107
+ const faceCount = up ? run.n : run.n + 2;
108
+ for (let i = 0; i < faceCount; i++) {
109
+ const role = run.role[`f${i}`];
110
+ const d = f.d[role];
111
+ if (!d) continue;
112
+ const q = f.q[role];
113
+ if (q % TURN.q !== 0) {
114
+ results.skipped++;
115
+ continue;
116
+ }
117
+ const tone = q / TURN.q;
118
+ const pts = subpaths(d)[0].pts;
119
+ let best = Infinity;
120
+ for (const sp of kitFaces[tone] ?? []) best = Math.min(best, hausdorff(pts, sh(sp.pts)));
121
+ if (!Number.isFinite(best)) {
122
+ results.missing++;
123
+ continue;
124
+ }
125
+ results.compared++;
126
+ results.faces = Math.max(results.faces, best);
127
+ }
128
+ }
129
+ }
130
+
131
+ const limit = 0.06;
132
+ const pass = results.outline <= limit && results.top <= limit && results.faces <= limit && results.missing === 0;
133
+ const worst = Object.entries(worstAt).filter(([, w]) => w.outline > limit).map(([name, w]) => `${name} outline ${w.outline.toFixed(3)} at ${w.at}°`);
134
+ return { pass, text: `prisms at ${angles.length} angles: outline ${results.outline.toFixed(4)} px · top ${results.top.toFixed(4)} px · faces ${results.faces.toFixed(4)} px over ${results.compared} faces (${results.skipped} in crossfade windows, ${results.missing} with no kit face of that tone)`, worst };
135
+ }
136
+
137
+ export function rounds(T, P, angles, { only = null } = {}) {
138
+ const scene = TURN.prepare(T.data, { unit: 1 });
139
+ const CELL = 0.1;
140
+ const subpaths = (d) => {
141
+ const out = [];
142
+ for (const [, body] of (d ?? "").matchAll(/M([^M]*)/g)) {
143
+ const nums = (body.match(/-?\d+(?:\.\d+)?(?:e-?\d+)?/g) ?? []).map(Number);
144
+ const pts = [];
145
+ for (let i = 0; i + 1 < nums.length; i += 2) pts.push([nums[i], nums[i + 1]]);
146
+ out.push(pts);
147
+ }
148
+ return out;
149
+ };
150
+ const segDist = (p, a, b) => {
151
+ const dx = b[0] - a[0];
152
+ const dy = b[1] - a[1];
153
+ const l = dx * dx + dy * dy;
154
+ const t = l ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l)) : 0;
155
+ return Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t);
156
+ };
157
+ const distToPoly = (p, poly) => {
158
+ let best = Infinity;
159
+ for (let i = 0; i < poly.length; i++) best = Math.min(best, segDist(p, poly[i], poly[(i + 1) % poly.length]));
160
+ return best;
161
+ };
162
+ const hausdorff = (A, B) => {
163
+ let worst = 0;
164
+ for (const p of A) worst = Math.max(worst, distToPoly(p, B));
165
+ for (const p of B) worst = Math.max(worst, distToPoly(p, A));
166
+ return worst;
167
+ };
168
+ function raster(layers, box, grow = 0) {
169
+ const [x0, y0, x1, y1] = box;
170
+ const w = Math.ceil((x1 - x0) / CELL) + 1;
171
+ const h = Math.ceil((y1 - y0) / CELL) + 1;
172
+ const out = new Int8Array(w * h).fill(-1);
173
+ for (const { tone, d } of layers) {
174
+ if (!d) continue;
175
+ const mask = new Uint8Array(w * h);
176
+ const polys = subpaths(d);
177
+ for (let row = 0; row < h; row++) {
178
+ const y = y0 + row * CELL + CELL / 2;
179
+ const hits = [];
180
+ for (const poly of polys)
181
+ for (let i = 0; i < poly.length; i++) {
182
+ const a = poly[i];
183
+ const b = poly[(i + 1) % poly.length];
184
+ if ((a[1] <= y && b[1] > y) || (b[1] <= y && a[1] > y)) {
185
+ const x = a[0] + ((y - a[1]) / (b[1] - a[1])) * (b[0] - a[0]);
186
+ hits.push([x, b[1] > a[1] ? 1 : -1]);
187
+ }
188
+ }
189
+ hits.sort((p, q) => p[0] - q[0]);
190
+ let wind = 0;
191
+ for (let i = 0; i < hits.length - 1; i++) {
192
+ wind += hits[i][1];
193
+ if (!wind) continue;
194
+ const from = Math.max(0, Math.ceil((hits[i][0] - x0 - CELL / 2) / CELL));
195
+ const to = Math.min(w - 1, Math.floor((hits[i + 1][0] - x0 - CELL / 2) / CELL));
196
+ for (let c = from; c <= to; c++) mask[row * w + c] = 1;
197
+ }
198
+ }
199
+ const reach = Math.round(grow / CELL);
200
+ for (let row = 0; row < h; row++)
201
+ for (let c = 0; c < w; c++) {
202
+ let on = mask[row * w + c];
203
+ for (let dy = -reach; dy <= reach && !on; dy++)
204
+ for (let dx = -reach; dx <= reach && !on; dx++) {
205
+ if (dx * dx + dy * dy > reach * reach) continue;
206
+ const rr = row + dy;
207
+ const cc = c + dx;
208
+ if (rr >= 0 && cc >= 0 && rr < h && cc < w && mask[rr * w + cc]) on = 1;
209
+ }
210
+ if (on) out[row * w + c] = tone;
211
+ }
212
+ }
213
+ return { out, w, h };
214
+ }
215
+
216
+ const stats = { outline: 0, outlineFine: 0, bands: 0, cells: 0, mismatched: 0, parts: 0 };
217
+ const worst = [];
218
+ for (const theta of angles) {
219
+ const cams = TURN.cams(scene, restValues(T, theta));
220
+ const Pp = { ...P, azimuth: P.azimuth - theta };
221
+ const Pf = { ...Pp, origin: [Pp.origin[0] * 10, Pp.origin[1] * 10], scale: Pp.scale * 10 };
222
+ for (const part of T.parts) {
223
+ if ((part.kind !== "round" && part.kind !== "lathe") || (only && !only.test(part.name)) || !rootOf(part.group) || part.group.kind === "free") continue;
224
+ const run = scene.runs[part.id];
225
+ const cam = part.group ? cams[part.group.index] : cams.world;
226
+ const f = TURN.emit(run, cam, TURN.frame(run));
227
+ let root = part.group;
228
+ while (root && root.parent) root = root.parent;
229
+ const pivot = root ? [root.origin[0], root.origin[1], 0] : [0, 0, 0];
230
+ const a = k.iso(pivot, P);
231
+ const b = k.iso(pivot, Pp);
232
+ const shift = [a[0] - b[0], a[1] - b[1]];
233
+ let F, s0, s1, r0, r1, ends, profile;
234
+ if (part.kind === "lathe") {
235
+ const { curve, knots } = part.world;
236
+ F = part.world.F;
237
+ s0 = curve.s0;
238
+ s1 = curve.s1;
239
+ r0 = curve.radius(s0);
240
+ r1 = curve.radius(s1);
241
+ ends = ["flat", "flat"];
242
+ profile = [[s0, r0], ...knots.slice(1, -1).map((sv) => [sv, curve.radius(sv), 1]), [s1, r1]];
243
+ } else ({ F, s0, s1, r0, r1, ends, profile } = part.world);
244
+ const meridian = [...(ends[0] === "open" ? [] : [[profile[0][0], 0]]), ...profile.filter((p) => p[1] > 1e-9), ...(ends[1] === "open" ? [] : [[profile[profile.length - 1][0], 0]])];
245
+ const domed = ends.includes("dome");
246
+ const radius = (s) => {
247
+ if (ends[0] === "dome" && s < s0) return Math.sqrt(Math.max(0, r0 * r0 - (s - s0) ** 2));
248
+ if (ends[1] === "dome" && s > s1) return Math.sqrt(Math.max(0, r1 * r1 - (s - s1) ** 2));
249
+ return r0 + ((r1 - r0) * (Math.max(s0, Math.min(s1, s)) - s0)) / (s1 - s0);
250
+ };
251
+ const slope = (s) => {
252
+ if (ends[0] === "dome" && s < s0) return -(s - s0) / Math.max(1e-4, radius(s));
253
+ if (ends[1] === "dome" && s > s1) return -(s - s1) / Math.max(1e-4, radius(s));
254
+ return (r1 - r0) / (s1 - s0);
255
+ };
256
+ const lo = ends[0] === "dome" ? s0 - r0 : s0;
257
+ const hi = ends[1] === "dome" ? s1 + r1 : s1;
258
+ const knots = [];
259
+ for (let i = 0; i <= 64; i++) knots.push(lo + ((hi - lo) * i) / 64);
260
+ const analytic = [[lo, 0], ...knots.slice(1, -1).map((sv) => [sv, radius(sv), 1]), [hi, 0]];
261
+ if (ends[0] !== "dome") analytic.splice(1, 0, [lo, r0]);
262
+ if (ends[1] !== "dome") analytic.splice(analytic.length - 1, 0, [hi, r1]);
263
+ const lathed = part.kind === "lathe" ? { slope: part.world.curve.slope, radius: part.world.curve.radius } : null;
264
+ const kit = lathed ? G.lathe(meridian, F, Pp, { smooth: lathed }) : domed ? G.lathe(analytic, F, Pp, { smooth: { slope, radius } }) : G.lathe(meridian, F, Pp, { smooth: true });
265
+ const kitFine = lathed ? G.lathe(meridian, F, Pf, { smooth: lathed }) : domed ? G.lathe(analytic, F, Pf, { smooth: { slope, radius } }) : G.lathe(meridian, F, Pf, { smooth: true });
266
+ const sh = (pts) => pts.map(([x, y]) => [x + shift[0], y + shift[1]]);
267
+ const cut = Boolean(part.spec.cuts?.length);
268
+ const mine = subpaths(f.d[run.role[cut ? "fill" : "edge"]])[0];
269
+ const theirs = sh(subpaths(kit.fill)[0]);
270
+ const fineRef = subpaths(kitFine.fill)[0].map(([x, y]) => [x / 10 + shift[0], y / 10 + shift[1]]);
271
+ const ho = hausdorff(mine, theirs);
272
+ const visible = cut ? subpaths(f.d[run.role.edge]).flat().reduce((worst, point) => Math.max(worst, distToPoly(point, fineRef)), 0) : 0;
273
+ const hf = Math.max(hausdorff(mine, fineRef), visible);
274
+ stats.outline = Math.max(stats.outline, ho);
275
+ stats.outlineFine = Math.max(stats.outlineFine, hf);
276
+ stats.parts++;
277
+ const ours = [0, 1, 2, 3, 4].map((t) => ({ tone: t, d: f.d[run.role[`s${t}`]] }));
278
+ for (const end of [0, 1]) {
279
+ const role = run.role[`k${end}`];
280
+ if (role === undefined || !f.d[role]) continue;
281
+ const q = f.q[role];
282
+ ours.push({ tone: q % TURN.q === 0 ? q / TURN.q : 9, d: f.d[role] });
283
+ }
284
+ const theirsLayers = [...kitFine.shades.map((d, t) => ({ tone: t, d })), { tone: 4, d: kitFine.top }].map(({ tone, d }) => ({ tone, d: subpaths(d).map((poly) => "M" + poly.map(([x, y]) => `${x / 10 + shift[0]} ${y / 10 + shift[1]}`).join("L") + "Z").join("") }));
285
+ let x0 = Infinity;
286
+ let y0 = Infinity;
287
+ let x1 = -Infinity;
288
+ let y1 = -Infinity;
289
+ for (const [x, y] of mine) {
290
+ x0 = Math.min(x0, x);
291
+ y0 = Math.min(y0, y);
292
+ x1 = Math.max(x1, x);
293
+ y1 = Math.max(y1, y);
294
+ }
295
+ const box = [x0 - 1, y0 - 1, x1 + 1, y1 + 1];
296
+ const A = raster(ours, box, 0.2);
297
+ const B = raster(theirsLayers, box, 0.2);
298
+ const inside = raster([{ tone: 1, d: f.d[run.role.edge] }], box).out;
299
+ const capCells = raster([0, 1].map((end) => ({ tone: 1, d: run.role[`k${end}`] !== undefined ? f.d[run.role[`k${end}`]] : "" })), box, 0.6).out;
300
+ const capExempt = part.kind === "lathe";
301
+ const insideKit = raster([{ tone: 1, d: "M" + fineRef.map(([x, y]) => `${x} ${y}`).join("L") + "Z" }], box).out;
302
+ let worstGap = 0;
303
+ const reach = Math.ceil(0.5 / CELL) + 2;
304
+ for (let row = 0; row < A.h; row++)
305
+ for (let c = 0; c < A.w; c++) {
306
+ const i = row * A.w + c;
307
+ if (inside[i] < 0 || insideKit[i] < 0) continue;
308
+ const ta = A.out[i];
309
+ const tb = B.out[i];
310
+ if (ta === 9 || ta < 0 || tb < 0) continue;
311
+ if (capExempt && capCells[i] >= 0) continue;
312
+ stats.cells++;
313
+ if (ta === tb) continue;
314
+ let edge = false;
315
+ for (let dy = -3; dy <= 3 && !edge; dy++) for (let dx = -3; dx <= 3; dx++) if (inside[(row + dy) * A.w + c + dx] < 0) edge = true;
316
+ if (edge) continue;
317
+ stats.mismatched++;
318
+ let near = Infinity;
319
+ for (let dy = -reach; dy <= reach; dy++)
320
+ for (let dx = -reach; dx <= reach; dx++) {
321
+ const rr = row + dy;
322
+ const cc = c + dx;
323
+ if (rr < 0 || cc < 0 || rr >= A.h || cc >= A.w) continue;
324
+ if (B.out[rr * A.w + cc] === ta) near = Math.min(near, Math.hypot(dx, dy) * CELL);
325
+ }
326
+ worstGap = Math.max(worstGap, near);
327
+ }
328
+ stats.bands = Math.max(stats.bands, worstGap);
329
+ if (worstGap > 0.5 || hf > 0.05) worst.push({ part: part.name, theta, band: worstGap, outline: hf });
330
+ }
331
+ }
332
+
333
+ return { pass: stats.outlineFine <= 0.05 && stats.bands <= 0.5, text: `rounds: ${stats.parts} frames at ${angles.length} angles: outline vs kit ${stats.outline.toFixed(4)} px (kit rounds to 0.1), vs kit at 10x ${stats.outlineFine.toFixed(4)} px · band boundaries ≤ ${stats.bands.toFixed(2)} px apart (${stats.mismatched} of ${stats.cells} interior cells differ)`, worst: worst.sort((a, b) => b.band - a.band || b.outline - a.outline).slice(0, 12).map((w) => `${w.part} at ${w.theta}°: outline ${w.outline.toFixed(3)} px, band ${w.band.toFixed(2)} px`) };
334
+ }
335
+
336
+ export function tubes(T, P, angles) {
337
+ const scene = TURN.prepare(T.data, { unit: 1 });
338
+ const subpaths = (d) => [...(d ?? "").matchAll(/M([^M]*)/g)].map(([, body]) => {
339
+ const nums = (body.match(/-?\d+(?:\.\d+)?(?:e-?\d+)?/g) ?? []).map(Number);
340
+ const pts = [];
341
+ for (let i = 0; i + 1 < nums.length; i += 2) pts.push([nums[i], nums[i + 1]]);
342
+ return pts;
343
+ });
344
+ const segDist = (p, a, b) => {
345
+ const dx = b[0] - a[0], dy = b[1] - a[1], l = dx * dx + dy * dy;
346
+ const t = l ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / l)) : 0;
347
+ return Math.hypot(p[0] - a[0] - dx * t, p[1] - a[1] - dy * t);
348
+ };
349
+ const compare = (mine, theirs) => {
350
+ const A = subpaths(mine);
351
+ const B = subpaths(theirs);
352
+ if (A.length !== B.length) return { error: Infinity, why: `subpaths ${A.length} vs ${B.length}` };
353
+ let worst = 0;
354
+ for (let i = 0; i < A.length; i++) {
355
+ if (A[i].length === B[i].length) for (let j = 0; j < A[i].length; j++) worst = Math.max(worst, Math.abs(A[i][j][0] - B[i][j][0]), Math.abs(A[i][j][1] - B[i][j][1]));
356
+ else {
357
+ for (const [from, to] of [[A[i], B[i]], [B[i], A[i]]]) for (const p of from) worst = Math.max(worst, Math.min(...to.slice(1).map((q, j) => segDist(p, to[j], q))));
358
+ }
359
+ }
360
+ return { error: worst };
361
+ };
362
+
363
+ const stats = { body: 0, shine: 0, shade: 0, edges: 0, rings: 0, chunks: 0 };
364
+ const bad = [];
365
+ for (const theta of angles) {
366
+ const cams = TURN.cams(scene, restValues(T, theta));
367
+ const Pp = { ...P, azimuth: P.azimuth - theta };
368
+ const Pf = { ...Pp, origin: [Pp.origin[0] * 10, Pp.origin[1] * 10], scale: Pp.scale * 10 };
369
+ for (const route of T.routes) {
370
+ if (route.group && (!rootOf(route.group) || route.group.kind === "free")) continue;
371
+ const root = rootOf(route.group);
372
+ const pivot = root ? [root.origin[0], root.origin[1], 0] : [0, 0, 0];
373
+ const a = k.iso(pivot, P);
374
+ const b = k.iso(pivot, Pp);
375
+ const shift = [a[0] - b[0], a[1] - b[1]];
376
+ const kit = tubePieces(route.world, route.r, Pf, { maxLength: route.chunk, spacing: route.spacing, gaps: route.gaps, breaks: route.breaks, caps: route.caps, stripes: route.wide, rings: route.rings });
377
+ const scaled = (d) => subpaths(d).map((pts) => "M" + pts.map(([x, y]) => `${x / 10 + shift[0]} ${y / 10 + shift[1]}`).join("L")).join("");
378
+ const chunks = T.parts.filter((part) => part.route === route.name);
379
+ if (kit.length !== chunks.length) {
380
+ bad.push(`${route.name}: ${chunks.length} chunks vs kit ${kit.length}`);
381
+ continue;
382
+ }
383
+ chunks.forEach((part, index) => {
384
+ const run = scene.runs[part.id];
385
+ const f = TURN.emit(run, route.group ? cams[route.group.index] : cams.world, TURN.frame(run));
386
+ const piece = kit[index];
387
+ stats.chunks++;
388
+ for (const key of ["body", "shine", "shade", "edges"]) {
389
+ if (run.role[key] === undefined) continue;
390
+ const { error, why } = compare(f.d[run.role[key]], scaled(piece[key]));
391
+ stats[key] = Math.max(stats[key], error);
392
+ if (error > 0.05) bad.push(`${part.name} ${key} at ${theta}°: ${why ?? error.toFixed(3)}`);
393
+ }
394
+ if (route.rings) {
395
+ const mine = run.roles.filter((role) => role.name.startsWith("r")).map((role) => f.d[run.role[role.name]]).join("");
396
+ const theirs = scaled(piece.rings.filter((ring) => ring.alpha * 32 >= 0.5).map((ring) => ring.d).join(""));
397
+ const A = subpaths(mine);
398
+ const B = subpaths(theirs);
399
+ let worst = A.length === B.length ? 0 : Infinity;
400
+ for (const seg of A) {
401
+ let best = Infinity;
402
+ for (const other of B) best = Math.min(best, Math.max(...seg.map((p, j) => Math.hypot(p[0] - other[j][0], p[1] - other[j][1]))));
403
+ worst = Math.max(worst, best);
404
+ }
405
+ stats.rings = Math.max(stats.rings, worst);
406
+ if (worst > 0.05) bad.push(`${part.name} rings at ${theta}°: ${A.length} vs ${B.length} segments, worst ${worst.toFixed(3)}`);
407
+ }
408
+ });
409
+ }
410
+ }
411
+
412
+ return { pass: !bad.length, text: `tubes: ${stats.chunks} chunk frames at ${angles.length} angles against tubePieces at 10x: body ${stats.body.toFixed(4)} · shine ${stats.shine.toFixed(4)} · shade ${stats.shade.toFixed(4)} · edges ${stats.edges.toFixed(4)} · rings ${stats.rings.toFixed(4)} px`, worst: bad.slice(0, 20) };
413
+ }
@@ -0,0 +1,40 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { turning } from "../kit/turn-build.mjs";
4
+ import { identity } from "./turn-fidelity.mjs";
5
+ const P = { origin: [0, 0], scale: 1, azimuth: 45, elevation: 30 };
6
+ const fixture = () => { const T = turning(P); T.layer("parts"); return T; };
7
+ const prism = (T, group) => T.prism({ name: "pointer", group, plan: { x: 0, y: 0, w: 4, d: 2, r: 0 }, z: 0, h: 1 });
8
+ test("missing named part groups cannot silently pass as static geometry", () => {
9
+ const T = fixture(); T.group("dial", { turn: {} });
10
+ assert.throws(() => { prism(T, "dail"); T.build(); identity(T, P, [0, 90]); }, /turning: unknown group dail/);
11
+ assert.equal(T.parts.length, 0);
12
+ });
13
+ test("missing named parent groups cannot silently root nested motion", () => {
14
+ const T = fixture(); T.group("dial", { turn: {} });
15
+ assert.throws(() => T.group("slide", { parent: "dail", slide: { direction: [1, 0, 0], travel: 5 } }), /turning: unknown group dail/);
16
+ assert.equal(T.groups.length, 1);
17
+ });
18
+ test("duplicate group names cannot alias incompatible motion values", () => {
19
+ const T = fixture(); T.group("dial", { turn: {} });
20
+ assert.throws(() => T.group("dial", { slide: { direction: [1, 0, 0], travel: 5 } }), /turning: duplicate group dial/);
21
+ assert.equal(T.groups.length, 1);
22
+ });
23
+ test("existing named and object group references retain native fidelity", () => {
24
+ for (const named of [false, true]) {
25
+ const T = fixture(); const group = T.group("dial", { turn: {} });
26
+ const part = prism(T, named ? "dial" : group);
27
+ const child = T.group("slide", { parent: named ? "dial" : group, slide: { direction: [1, 0, 0], travel: 5 } });
28
+ assert.equal(part.group, group); assert.equal(child.parent, group);
29
+ T.build(); const result = identity(T, P, [0, 90]);
30
+ assert.equal(result.pass, true);
31
+ assert.match(result.text, /over [1-9]\d* faces/);
32
+ }
33
+ });
34
+ test("omitted and explicit null groups keep supported static geometry", () => {
35
+ for (const group of [undefined, null]) {
36
+ const T = fixture(); const part = prism(T, group);
37
+ assert.equal(part.group, null);
38
+ T.build(); assert.equal(T.parts.length, 1);
39
+ }
40
+ });
@@ -0,0 +1,62 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import * as A from "../kit/audit.mjs";
4
+ import * as G from "../kit/lathe.mjs";
5
+ import { turning } from "../kit/turn-build.mjs";
6
+ import { TURN } from "../kit/turn.mjs";
7
+ import { posed } from "../kit/turn-audit.mjs";
8
+ function fixture(kind, owner = "pipe") {
9
+ const T = turning({ origin: [0, 0], scale: 1, azimuth: 45, elevation: 30 }); T.layer("all");
10
+ const group = T.group("assembly", { turn: { pivot: [5, 2] } });
11
+ const F = G.frameAlong([0, 0, 0], [0, 0, 1]);
12
+ const base = { name: "ferrule", owner, group, F, s0: 0, s1: 10 };
13
+ const part = kind === "prism" ? T.prism({ ...base, polygon: [[-2, -2], [2, -2], [2, 2], [-2, 2]] }) : T.round({ ...base, r: 2, ...(kind === "default round" ? {} : { fixed: false }) });
14
+ const tube = A.tube([[0, 0, -2], [0, 0, 12]], 0.5, { name: "pipe", owner: "pipe" });
15
+ return { T, part, tube };
16
+ }
17
+ for (const kind of ["prism", "round", "default round"]) test(`split ${kind} preserves explicit ownership and its pipe clearance`, () => {
18
+ const { T, part, tube } = fixture(kind);
19
+ assert.deepEqual(A.clearances([tube], part.shapes), []);
20
+ const halves = T.split(part, { n: [0, 0, 1], d: 5 });
21
+ assert.equal(halves.length, 2);
22
+ assert.ok(halves.every((half) => half.owner === "pipe" && half.shapes.every((shape) => shape.owner === "pipe")));
23
+ assert.deepEqual(A.clearances([tube], halves.flatMap((half) => half.shapes)), []);
24
+ T.build();
25
+ const scene = TURN.prepare(T.data, { unit: 1 });
26
+ for (const angle of [0, 45, 90, 180]) {
27
+ const cams = TURN.cams(scene, { assembly: angle });
28
+ const group = T.groups[0];
29
+ const pose = { R: cams[group.index].R, t: cams[group.index].t, origin: group.origin };
30
+ const shapes = halves.flatMap((half) => half.shapes.map((shape) => posed(shape, pose)));
31
+ const original = posed(part.shapes[0], pose);
32
+ assert.deepEqual(A.clearances([posed(tube, pose)], shapes), []);
33
+ for (const z of [2.5, 7.5]) {
34
+ const a = angle * Math.PI / 180;
35
+ const point = [5 - 4 * Math.cos(a) + 2 * Math.sin(a), 2 - 4 * Math.sin(a) - 2 * Math.cos(a), z];
36
+ const distance = Math.min(...shapes.map((shape) => A.sdf(shape, point)));
37
+ assert.ok(distance < 0);
38
+ assert.ok(Math.abs(distance - A.sdf(original, point)) < 1e-8);
39
+ }
40
+ for (const run of scene.runs) {
41
+ const frame = TURN.emit(run, cams[run.group], TURN.frame(run));
42
+ assert.ok(frame.d.some((path) => typeof path === "string" && path.length));
43
+ }
44
+ }
45
+ });
46
+ test("an unrelated pipe still reports real penetration of split fittings", () => {
47
+ for (const kind of ["prism", "round", "default round"]) {
48
+ const { T, part, tube } = fixture(kind, "other");
49
+ const halves = T.split(part, { n: [0, 0, 1], d: 5 });
50
+ const failures = A.clearances([tube], halves.flatMap((half) => half.shapes));
51
+ assert.equal(failures.length, 2);
52
+ assert.ok(failures.every((failure) => failure.kind === "through" && failure.clearance < 0));
53
+ }
54
+ });
55
+ test("unowned split parts retain normal collision checking", () => {
56
+ for (const kind of ["prism", "round", "default round"]) {
57
+ const { T, part, tube } = fixture(kind, null);
58
+ const halves = T.split(part, { n: [0, 0, 1], d: 5 });
59
+ assert.ok(halves.every((half) => !half.owner && half.shapes.every((shape) => !shape.owner)));
60
+ assert.equal(A.clearances([tube], halves.flatMap((half) => half.shapes)).length, 2);
61
+ }
62
+ });
@@ -0,0 +1,56 @@
1
+ // Inject these exact bytes after the anatomy build's inline kit script.
2
+ function installAnatomyTheme({ doc = document, view = window, resolveColor } = {}) {
3
+ const body = doc.body;
4
+ const media = view.matchMedia('(prefers-color-scheme: dark)');
5
+ // Canvas delegates every supported CSS color syntax to the browser, in sRGB.
6
+ const color = resolveColor ?? ((value) => {
7
+ const canvas = doc.createElement('canvas');
8
+ canvas.width = canvas.height = 1;
9
+ const context = canvas.getContext('2d', { willReadFrequently: true });
10
+ context.fillStyle = value;
11
+ context.fillRect(0, 0, 1, 1);
12
+ return context.getImageData(0, 0, 1, 1).data;
13
+ });
14
+ if (!doc.getElementById('axstack-anatomy-theme')) {
15
+ const style = doc.createElement('style');
16
+ style.id = 'axstack-anatomy-theme';
17
+ style.textContent = `
18
+ body.iso[data-axstack-t3] { margin:0; padding:0; background:transparent;
19
+ --anatomy-page:transparent; --anatomy-mono:var(--font-mono);
20
+ color:var(--foreground); font-family:var(--font-sans); }
21
+ body.iso[data-axstack-t3] .iso-page { margin:0; padding:0; width:100%; max-width:none; min-height:0; background:none; }
22
+ body.iso[data-axstack-t3] .iso-plate { padding:16px; border-radius:var(--radius); }
23
+ `;
24
+ doc.head.appendChild(style);
25
+ }
26
+ const update = () => {
27
+ const background = view.getComputedStyle(doc.documentElement).getPropertyValue('--background').trim();
28
+ const marker = body.getAttribute('data-axstack-theme');
29
+ let dark = media.matches;
30
+ if (background) {
31
+ const rgba = color(background);
32
+ const alpha = rgba[3] / 255;
33
+ const channel = (index) => {
34
+ const value = (rgba[index] * alpha + 255 * (1 - alpha)) / 255;
35
+ return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
36
+ };
37
+ dark = 0.2126 * channel(0) + 0.7152 * channel(1) + 0.0722 * channel(2) < 0.5;
38
+ if (body.getAttribute('data-axstack-t3') === null) body.setAttribute('data-axstack-t3', '');
39
+ } else body.removeAttribute('data-axstack-t3');
40
+ const theme = marker === 'dark' || marker === 'light' ? marker : dark ? 'dark' : 'light';
41
+ if (body.getAttribute('data-theme') !== theme) body.setAttribute('data-theme', theme);
42
+ // Override the kit's dark root scheme so transparent iframe pages follow the frame.
43
+ if (doc.documentElement.style.colorScheme !== theme) doc.documentElement.style.colorScheme = theme;
44
+ };
45
+ update();
46
+ media.addEventListener('change', update);
47
+ const observer = new view.MutationObserver(update);
48
+ observer.observe(doc.documentElement, {
49
+ attributes: true, attributeFilter: ['class', 'style', 'data-axstack-theme'] });
50
+ observer.observe(body, { attributes: true, attributeFilter: ['data-axstack-theme'] });
51
+ observer.observe(doc.head, { subtree: true, childList: true, characterData: true,
52
+ attributes: true, attributeFilter: ['media', 'disabled'] });
53
+ return () => { media.removeEventListener('change', update); observer.disconnect(); };
54
+ }
55
+
56
+ if (typeof document !== 'undefined') installAnatomyTheme();
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: axstack-explain
3
- description: When the user asks how, why, explain, or show, says they do not understand, a spec is being finalized, or you judge they need to understand something, use axstack-explain to show how it works and what exists, is missing, or remains unverified.
3
+ description: When the user asks how, why, explain, or show, asks to diagram, visualize, draw or defend, requests a whiteboard defense, says they do not understand, a spec is being finalized, or you judge they need to understand something, use axstack-explain to show how it works and what exists, is missing, or remains unverified.
4
4
  ---
5
5
 
6
6
  # Explain
@@ -12,6 +12,7 @@ Explain a system, change, current behavior, intended behavior, or implementation
12
12
  gap from inspected evidence. This direct route needs no spec ceremony. Use
13
13
  project documentation as evidence where relevant. It supersedes `axstack-docs`; if a stale install
14
14
  exposes both skills, route the request here only.
15
+ For a stale `axstack-diagram` install, route requests here only.
15
16
 
16
17
  Before acting, load [Standing contracts](../axstack/references/contracts.md),
17
18
  then follow its required lifecycle and audit pointers. Explanation work has no
@@ -51,9 +52,21 @@ immediately before an actual profile dispatch.
51
52
 
52
53
  ## 2. Choose proportional output
53
54
 
54
- Load [Diagram](../axstack-diagram/SKILL.md) for every diagram.
55
55
  Explain owns evidence gathering, claim labels, the 700-word cap, dispatch, and delivery.
56
- Diagram never calls explain.
56
+ Choose the best visual for the concept: flowchart, sequence, state, architecture
57
+ map, timeline, comparison or bespoke SVG are examples, not a taxonomy.
58
+ Follow [Fidelity](references/fidelity.md) for every visual.
59
+ For whole-work understanding or defense, follow [Whiteboard defense](references/whiteboard-defense.md).
60
+ Dispatch every visual Explain delivers to `axstack-explainer` with a bounded
61
+ evidence brief, including inline pages, chat Mermaid and anatomy figures.
62
+ Plain-prose answers stay driver-written; other phases retain their own Mermaid
63
+ owners, and README/docs diagram edits go through Implement under these rules.
64
+ Use inline SVG or CSS inside inline pages. Never use Mermaid in inline pages.
65
+ Use Mermaid for chat, GitHub, and docs.
66
+ Include `accTitle` and `accDescr` in each Mermaid view.
67
+ Never use theme init in Mermaid. Split large Mermaid diagrams into views.
68
+ Use anatomy only on an explicit isometric, physical-object or anatomy request,
69
+ through [Anatomy](../axstack-anatomy/SKILL.md).
57
70
 
58
71
  1. Keep the primary reader-facing explanation to a maximum of 700 words in
59
72
  chat, HTML, and every other requested format. Preserve in that primary view
@@ -67,18 +80,13 @@ Diagram never calls explain.
67
80
  maximum. If the draft is longer, compress repetition first and move only
68
81
  supporting detail to a separate linked ticket or appendix. Essential answers
69
82
  must not be hidden behind links, and evidence must not be silently discarded.
70
- Only archify node detail cards are exempt from the 700-word cap.
71
- Each card has at most 40 words and cites its source, as defined in
72
- [Archify](../axstack-diagram/references/archify.md).
73
- Count node labels, headings, captions, and all non-card text.
83
+ Count node labels, headings, captions, and all page text.
74
84
  2. Use chat only for an explicit chat request, an answer outside the description's conditions,
75
85
  or inline-page fallback.
76
86
  Use a compact diagram when useful.
77
- For chat answers, never dispatch an agent when applying its Mermaid rules inline.
78
87
  3. For the conditions in the description, create an inline page by default.
79
88
  Honor an explicitly requested artifact format.
80
- Use archify only on an explicit viewer request, through the configured
81
- `axstack-explainer`. The reply adds only what the page omits.
89
+ The reply adds only what the page omits.
82
90
  4. Profile IDs are presets, not availability proof. Before dispatch, follow the
83
91
  launch sequence and preserve the configured model, mode, and effort. Report
84
92
  an unavailable route; never substitute a model.
@@ -86,16 +94,18 @@ Diagram never calls explain.
86
94
  ## 3. Verify and deliver
87
95
 
88
96
  1. For inline pages, follow [Inline pages](references/inline-pages.md).
89
- For archify and every other explicitly requested HTML or visual artifact
97
+ For every explicitly requested HTML or visual artifact
90
98
  outside inline T3 pages, follow [Visual QA](references/visual-qa.md).
91
99
  2. Require independent `axstack-explainer-review` for consequential or complex
92
- claims, archify output, or on request.
100
+ claims or on request.
101
+ The `axstack-explainer-review` always reviews every visual at its exact final
102
+ bytes before delivery.
93
103
  Consequential claims include gap or missing-claim reports, blast-radius
94
- safety facts, and spec readbacks.
104
+ safety facts, spec readbacks, and every whiteboard-defense explanation in chat too.
95
105
  Bind it to the exact artifact identity.
96
106
  Any byte change invalidates
97
107
  that review and requires a fresh check. In `claude-only`, separate Sonnet
98
- author high and reviewer high sessions are allowed for explanations as
108
+ author high and reviewer high sessions are allowed for explanations and every visual as
99
109
  session independence only. This exception never permits same-model code
100
110
  review or a cross-provider-independence claim.
101
111
  3. Report source, tests, rendered observations, independent review, and
@@ -103,6 +113,9 @@ Diagram never calls explain.
103
113
  4. For a public artifact, remove private paths, identifiers, prompts,
104
114
  credentials, messages, and internal records. Local creation does not grant
105
115
  authority to publish.
106
- 5. Deliver the smallest useful explanation. Explanation causes no automatic
116
+ 5. Follow [Delivery](references/delivery.md) for fallback and repair holds.
117
+ Deliver the smallest useful explanation. Explanation causes no automatic
107
118
  design and no automatic implementation; route later change work separately
108
119
  only when the user requests it.
120
+
121
+ Ideas: [archify](https://github.com/tt-a1i/archify), [diagram-design](https://github.com/cathrynlavery/diagram-design), [visual-explainer](https://github.com/nicobailon/visual-explainer), [Matt Pocock](https://github.com/mattpocock/skills), and [poteto](https://github.com/poteto/how); guidance uses Axstack's own words.