Skip to content
Merged
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ RUN curl -fSL https://github.com/AdaWorldAPI/q2/releases/download/fma-body-soa-v
-o /build/q2/cockpit/dist/body.soa.gz \
&& ls -lh /build/q2/cockpit/dist/body.soa.gz

# Same for the /helix wire: one SoA (BSO2 ver 6) = F16 pos + a canonical Signed360
# NORMAL column in the same struct-of-arrays. Same-origin for the same CORS reason;
# named by cockpit/public/body.manifest.json (helix_latest). Stays in the release.
RUN curl -fSL https://github.com/AdaWorldAPI/q2/releases/download/fma-body-soa-v3-v1/body.20260629.v6helix.soa.gz \
-o /build/q2/cockpit/dist/body.20260629.v6helix.soa.gz \
&& ls -lh /build/q2/cockpit/dist/body.20260629.v6helix.soa.gz

# Sibling deps — clone from GitHub
# graph-flow stub is local (crates/stubs/graph-flow), no rs-graph-llm needed
#
Expand Down
5 changes: 5 additions & 0 deletions cockpit/public/body.manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"helix_latest": "body.20260629.v6helix.soa.gz",
"note": "Single SoA wire (BSO2 ver 6): F16 positions + a Signed360 NORMAL column in the same struct-of-arrays, plus an HXFL floor trailer (the RollingFloor lo,hi). Baked by helixbake using the REAL lance-graph::helix::ResidueEncoder::encode_signed against the local ndarray fork — the Fisher-Z rim is populated (not zeroed). Published to the fma-body-soa-v3-v1 release; the Dockerfile pulls it same-origin. Decode: rim r=sinθ -> int8 normal at load, Gouraud per-vertex shading (no per-fragment lighting). Sampled round-trip err: mean 0.21 deg, p99 0.85 deg, grid-worst ~1.83 deg.",
"verts": 4283525
}
344 changes: 344 additions & 0 deletions cockpit/src/BodyHelix.tsx

Large diffs are not rendered by default.

40 changes: 30 additions & 10 deletions cockpit/src/BodyV3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,11 @@ function conceptColor(layerId: number, matRgb: [number, number, number], row: nu
return [Math.min(255, out[0]), Math.min(255, out[1]), Math.min(255, out[2])];
}

interface ConceptInfo { row: number; name: string; centroid: [number, number, number]; layer: number; material: string }
interface ConceptInfo { row: number; name: string; centroid: [number, number, number]; layer: number; material: string; verts: number }

// semantic x-ray opacity per compartment (id 1..8): skin/muscle fade, organ mid,
// skeleton mid, vessel + nervous opaque so they pop through the body. index 0 unused.
const LAYER_XRAY_ALPHA = new Float32Array([1, 0.10, 0.20, 0.45, 0.50, 1.0, 1.0, 0.22, 0.30]);

// 64K IEEE-half → f32 lookup (built once): ver-5 wire stores positions as F16
// (10-bit mantissa, ~0.2 mm here — no BF16 staircase). LUT keeps decode O(1)/vertex.
Expand All @@ -82,7 +86,7 @@ interface Decoded {
colors: Uint8Array; alpha: Float32Array; layer: Float32Array; row: Float32Array;
materials: Material[]; labels: string[]; concepts: ConceptInfo[];
}
interface RenderState { enabled: Float32Array; alpha: number; transparent: boolean; lodOn: boolean; focus: { t: [number, number, number]; d: number } | null }
interface RenderState { enabled: Float32Array; alpha: number; transparent: boolean; lodOn: boolean; selRow: number; focus: { t: [number, number, number]; d: number } | null }

function decodeBso2(buf: ArrayBuffer): Decoded {
const dv = new DataView(buf);
Expand All @@ -97,7 +101,7 @@ function decodeBso2(buf: ArrayBuffer): Decoded {
const layerOff = o; o += nC; // LAYER index u8 (1..8)
const labOff = o; o += 4 * nC; // label codebook index u32
const cenOff = o; o += 12 * nC; // per-concept centroid 3×f32 (search zoom + server LOD)
o += 8 * nC; // (vstart,vcount)
const vrOff = o; o += 8 * nC; // (vstart,vcount) — vcount = mesh size for the popup
const posOff = o; o += posBytes * nV;
o += 6 * nV; // helix (server-side)
const rowOff = o; o += 4 * nV;
Expand All @@ -112,6 +116,7 @@ function decodeBso2(buf: ArrayBuffer): Decoded {
const cLayer = new Uint8Array(buf.slice(layerOff, layerOff + nC));
const cNameIdx = new Uint32Array(buf.slice(labOff, labOff + 4 * nC));
const cCen = new Float32Array(buf.slice(cenOff, cenOff + 12 * nC)); // bake-space; remap below
const cVR = new Uint32Array(buf.slice(vrOff, vrOff + 8 * nC)); // [vstart,vcount]×nC

// per-concept colour (precompute once) + searchable concept table (name + centroid).
const conceptRgb: [number, number, number][] = new Array(nC);
Expand All @@ -123,6 +128,7 @@ function decodeBso2(buf: ArrayBuffer): Decoded {
concepts[cI] = {
row: cI, name: labels[cNameIdx[cI]] ?? `concept ${cI}`, layer: li, material: mat.name,
centroid: [-cCen[cI * 3], cCen[cI * 3 + 2], cCen[cI * 3 + 1]], // (x,y,z)->(-x,z,y) display
verts: cVR[cI * 2 + 1],
};
}

Expand Down Expand Up @@ -186,6 +192,7 @@ const FRAG = `
precision mediump float;
uniform float uEnabled[9]; uniform float uGlobalAlpha;
uniform sampler2D uLod; uniform highp float uLodN; uniform float uLodOn; // server HHTL LOD gate
uniform float uXray; uniform float uLayerAlpha[9]; uniform highp float uSelRow; // semantic opacity
varying vec3 vNormal; varying vec3 vColor; varying float vAlpha; varying float vLayer;
varying highp float vRow; // highp: concept IDs up to ~1658 + the texel-center divide must
// resolve exactly; mediump's min precision aliases adjacent rows.
Expand All @@ -201,7 +208,11 @@ void main(){
const vec3 L = vec3(-0.401,0.783,0.476);
float ndl = max(dot(n,L),0.0);
float shade = min(0.34 + 0.20*(n.y*0.5+0.5) + 0.12*(-n.x*0.5+0.5) + 0.92*ndl, 1.3);
gl_FragColor = vec4(vColor*shade, vAlpha * uGlobalAlpha); // #17 alpha × solid/transparent
// x-ray: per-compartment semantic opacity (skin faint → vessel/nerve opaque);
// solid mode keeps the legacy uniform alpha. Selected concept always pops to 1.0.
float a = uXray > 0.5 ? uLayerAlpha[li] : (vAlpha * uGlobalAlpha);
if(uSelRow >= 0.0 && abs(vRow - uSelRow) < 0.5) a = 1.0;
gl_FragColor = vec4(vColor*shade, a);
}`;

function mount(container: HTMLDivElement, d: Decoded, st: RenderState, onStats: (s: { fps: number }) => void): () => void {
Expand Down Expand Up @@ -238,6 +249,7 @@ function mount(container: HTMLDivElement, d: Decoded, st: RenderState, onStats:
const uniforms = {
uEnabled: { value: st.enabled }, uGlobalAlpha: { value: st.alpha },
uLod: { value: lodTex }, uLodN: { value: d.nConcepts }, uLodOn: { value: 0 },
uXray: { value: 0 }, uLayerAlpha: { value: LAYER_XRAY_ALPHA }, uSelRow: { value: -1 },
};
// solid mode: opaque solids draw fast (transparent:false), #17 vessels blend over
// them. transparent mode (port of /fma-body's uniform-uAlpha x-ray): BOTH groups go
Expand Down Expand Up @@ -301,6 +313,8 @@ function mount(container: HTMLDivElement, d: Decoded, st: RenderState, onStats:
if (renderer.getPixelRatio() !== pr) renderer.setPixelRatio(pr);
uniforms.uEnabled.value = st.enabled; // shared by both materials
uniforms.uGlobalAlpha.value = st.alpha;
uniforms.uXray.value = st.transparent ? 1 : 0; // x-ray ⇒ per-compartment semantic opacity
uniforms.uSelRow.value = st.selRow; // selected concept pops to full alpha
if (!st.lodOn) lodFail = false; // toggling LOD off clears a transient failure → re-enabling retries
uniforms.uLodOn.value = st.lodOn && !lodFail && lodReady ? 1 : 0; // only cull after a real response
if (st.focus) { // search-pick zoom: glide to the organ
Expand Down Expand Up @@ -346,16 +360,17 @@ export function BodyV3() {
const [lod, setLod] = useState(false); // server HHTL LOD — opt-in (off = today's full render)
const [query, setQuery] = useState('');
const [selected, setSelected] = useState<ConceptInfo | null>(null);
const stRef = useRef<RenderState>({ enabled: new Float32Array([0, 0, 1, 1, 1, 1, 1, 1, 1]), alpha: 1, transparent: false, lodOn: false, focus: null });
const stRef = useRef<RenderState>({ enabled: new Float32Array([0, 0, 1, 1, 1, 1, 1, 1, 1]), alpha: 1, transparent: false, lodOn: false, selRow: -1, focus: null });

useEffect(() => {
const e = new Float32Array(9);
for (let i = 1; i <= 8; i++) e[i] = on[i] ? 1 : 0;
stRef.current.enabled = e;
stRef.current.transparent = transparent;
// /fma-body translucency model: one uniform alpha for the WHOLE body. transparent
// ⇒ 0.42 x-ray (see through skin/muscle to organs); solid ⇒ 1.0 (#17 vessels only).
stRef.current.alpha = transparent ? 0.42 : 1.0;
// x-ray opacity is now SEMANTIC (per-compartment uLayerAlpha in the shader), so the
// whole-body uGlobalAlpha stays at 1.0 in both modes — it only scales #17 vessel
// blending in solid mode; x-ray ignores it entirely.
stRef.current.alpha = 1.0;
Comment on lines +363 to +373

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale x-ray label now that uGlobalAlpha is fixed at 1.0.

With whole-body alpha pinned to 1.0 and x-ray driven by per-compartment uLayerAlpha, the stats readout still advertises a flat 0.42 (Line 441: 'x-ray (whole body 0.42)'), which no longer reflects the rendered opacity. Update the label to match the semantic per-compartment behavior to avoid misleading users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cockpit/src/BodyV3.tsx` around lines 363 - 373, The x-ray stats label in
BodyV3 is stale because `stRef.current.alpha` is now always set to 1.0 and x-ray
opacity is controlled by per-compartment `uLayerAlpha` instead of whole-body
alpha. Update the readout/label logic in `BodyV3` (the code that renders the
`'x-ray (whole body 0.42)'` text) so it no longer hardcodes 0.42 and instead
describes the semantic per-compartment x-ray behavior consistently with the
`useEffect` that sets `stRef.current.alpha`.

stRef.current.lodOn = lod;
}, [on, transparent, lod]);

Expand Down Expand Up @@ -397,6 +412,7 @@ export function BodyV3() {
function pick(c: ConceptInfo) {
setSelected(c);
stRef.current.focus = { t: c.centroid, d: 0.6 }; // glide the camera to the organ
stRef.current.selRow = c.row; // pop the selected concept to full alpha
}

const btn = (active: boolean): React.CSSProperties => ({
Expand Down Expand Up @@ -438,7 +454,9 @@ export function BodyV3() {
<div style={{ marginTop: 4, background: '#0e1219', border: '1px solid #1c2530', borderRadius: 6, overflow: 'hidden', maxHeight: 320, overflowY: 'auto' }}>
{matches.map((c) => (
<button type="button" key={c.row} onClick={() => { pick(c); setQuery(''); }} style={{ width: '100%', textAlign: 'left', padding: '6px 9px', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', gap: 8, border: 'none', borderBottom: '1px solid #141b24', background: 'transparent', color: '#cdd9e5', font: '13px ui-monospace, monospace' }}>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{c.name}</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: 4, background: LAYERS[(c.layer - 1) % 8]?.color, marginRight: 6, verticalAlign: 'middle' }} />{c.name}
</span>
Comment on lines +457 to +459

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Ellipsis likely won't trigger on this flex item.

The name <span> carries overflow:hidden; text-overflow:ellipsis; white-space:nowrap, but as a direct child of the display:flex button it defaults to min-width:auto, so it won't shrink below its content width — long names will overflow/push the trailing layer label instead of truncating. Add minWidth: 0 (and let it flex) so the ellipsis applies.

🔧 Proposed fix
-                  <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
+                  <span style={{ minWidth: 0, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                     <span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: 4, background: LAYERS[(c.layer - 1) % 8]?.color, marginRight: 6, verticalAlign: 'middle' }} />{c.name}
                   </span>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: 4, background: LAYERS[(c.layer - 1) % 8]?.color, marginRight: 6, verticalAlign: 'middle' }} />{c.name}
</span>
<span style={{ minWidth: 0, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
<span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: 4, background: LAYERS[(c.layer - 1) % 8]?.color, marginRight: 6, verticalAlign: 'middle' }} />{c.name}
</span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cockpit/src/BodyV3.tsx` around lines 457 - 459, The name span inside the flex
button in BodyV3.tsx will not reliably truncate because it still has the default
min-width:auto. Update that flex child so it can shrink (for example by giving
the span a flex style and minWidth: 0) while keeping the existing ellipsis
styles, so long channel names truncate instead of pushing the trailing layer
label.

<span style={{ opacity: 0.5, flexShrink: 0 }}>{LAYERS[(c.layer - 1) % 8]?.name}</span>
</button>
))}
Expand All @@ -454,12 +472,14 @@ export function BodyV3() {
<span>{selected.material.replace(/_/g, ' ')}</span>
<span style={{ opacity: 0.6 }}>row</span>
<span>#{selected.row}</span>
<span style={{ opacity: 0.6 }}>mesh</span>
<span>{selected.verts.toLocaleString()} verts</span>
<span style={{ opacity: 0.6 }}>centroid</span>
<span>{selected.centroid.map((v) => v.toFixed(2)).join(', ')}</span>
</div>
<div style={{ marginTop: 10, display: 'flex', gap: 6 }}>
<button style={btn(false)} onClick={() => { stRef.current.focus = { t: selected.centroid, d: 0.6 }; }}>re-center</button>
<button style={btn(false)} onClick={() => { setSelected(null); stRef.current.focus = null; }}>close</button>
<button style={btn(false)} onClick={() => { setSelected(null); stRef.current.focus = null; stRef.current.selRow = -1; }}>close</button>
</div>
</div>
)}
Expand Down
6 changes: 6 additions & 0 deletions cockpit/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { TorsoRender } from './TorsoRender';
import { TorsoMap } from './TorsoMap';
import { FmaBody } from './FmaBody';
import { BodyV3 } from './BodyV3';
import BodyHelix from './BodyHelix';
import { CpicCockpit } from './CpicCockpit';
import { ReasoningPage } from './ReasoningPage';
import { ErrorBoundary } from './components/ErrorBoundary';
Expand Down Expand Up @@ -105,6 +106,11 @@ createRoot(document.getElementById('root')!).render(
cockpit/public/body.soa (BSO1 = V3 node table + SPM1 geometry). Polygons,
not surfels — the successor to /torso-live's decimated 2k-concept torso. */}
<Route path="/body" element={<BodyV3 />} />
{/* /helix — EXPERIMENTAL sibling of /body. Same baked wire, but shades from the
per-vertex helix-normal bytes (Fisher-2z geodesic codes) via a 256×256 LUT
materialized once at load: one vertex-shader fetch/vert, no per-vertex decode,
no rebake. Standalone (BodyHelix.tsx) so it can never break /body (#64). */}
Comment on lines +109 to +112

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update this route comment to match the current BodyHelix implementation.

BodyHelix.tsx now does a one-time CPU decode into an Int8Array normal buffer; it is not doing a 256×256 LUT lookup or vertex-shader fetch on the current path. Keeping this comment in sync will save the next reader from chasing an implementation that no longer exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cockpit/src/main.tsx` around lines 109 - 112, The route comment in main.tsx
is outdated relative to BodyHelix.tsx. Update the /helix comment near the
BodyHelix route to describe the current implementation accurately: it now
performs a one-time CPU decode into an Int8Array normal buffer, rather than
using a 256×256 LUT or a vertex-shader fetch. Keep the comment aligned with the
BodyHelix component’s actual behavior and naming so future readers can find the
correct path quickly.

<Route path="/helix" element={<BodyHelix />} />
{/* /cpic — CPIC pharmacogenomics cockpit (gene-first): {gene, diplotype, drug}
→ phenotype → recommendation, 2-hop NARS deduction over the real CPIC tables
via POST /api/cpic/reason (the standalone cpic crate). Additive, gene-first
Expand Down
95 changes: 95 additions & 0 deletions crates/osint-bake/tools/BAKE_ARTIFACTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Body SoA bake artifacts — stamp, don't clobber

The `/body` (BodyV3) and `/helix` (BodyHelix) cockpit viewers both consume the
BSO2 SoA wire. **A new bake must never delete or overwrite a working artifact** —
experiments that turn out bad must not take down the deployed viewer. Stamp every
build; keep the old ones.

## Naming

```
body.<YYYYMMDD>[-<n>].<fmt>.soa.gz # stamped, immutable once written
body.<YYYYMMDD>[-<n>].<fmt>.blocks # paired HHTL block bounds (cockpit-server /api/body/lod)
```

`<fmt>` records the wire format so two encodings on the same day stay distinct:

| `<fmt>` | meaning |
|---|---|
| `v5f16` | ver-5 wire, F16 (IEEE half) positions — current `/body` production |
| `v5f16h2` | same, helix-normal tuned (2-byte refinement validated) — `/helix` target |
Comment on lines +19 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the helix docs to the current v6, canonical-only flow.

The naming table still frames /helix as v5f16h2, and this section still says BodyHelix falls back to body.soa.gz. In the shipped code, the manifest now points at body.20260629.v6helix.soa.gz, and cockpit/src/BodyHelix.tsx throws when helix_latest is missing instead of touching the shared v5 artifact. As written, the operator docs describe a rollout path the viewer no longer supports.

Also applies to: 44-46

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/osint-bake/tools/BAKE_ARTIFACTS.md` around lines 19 - 20, The helix
artifact docs are outdated and still describe the old v5 fallback flow. Update
the naming table and surrounding text in BAKE_ARTIFACTS.md to reflect the
current v6 canonical-only helix path, using the same symbols and terminology as
BodyHelix and the manifest entry for helix_latest. Remove the mention that
BodyHelix falls back to body.soa.gz, and describe that the viewer now expects
body.20260629.v6helix.soa.gz and fails when helix_latest is missing instead of
using the shared v5 artifact.

| `v3f32` | ver-3 wire, raw f32 positions (legacy / debugging) |

`-<n>` is an optional same-day rebuild counter (`body.20260628-2.v5f16.soa.gz`).

## The two stable names are pointers, not bakes

- `body.soa.gz` — the artifact `/body` serves. It is a **copy of the current
production stamp**, never a fresh bake written in place. Re-point it by copying
a stamped file over it *after* the stamp is validated.
- `body.manifest.json` — served same-origin; the viewers read it to find the
current stamps:

```json
{
"body_latest": "body.20260628.v5f16.soa.gz",
"helix_latest": "body.20260628.v5f16h2.soa.gz",
"builds": [
{ "stamp": "body.20260628.v5f16.soa.gz", "ver": 5, "fmt": "v5f16", "verts": 4221000, "concepts": 1658, "note": "production" },
{ "stamp": "body.20260628.v5f16h2.soa.gz", "ver": 5, "fmt": "v5f16h2", "verts": 4221000, "concepts": 1658, "note": "helix experiment" }
]
}
```

`BodyHelix` prefers `helix_latest` (then falls back to the shared `body.soa.gz`);
`BodyV3` reads `body.soa.gz` directly. A bad helix experiment is rolled back by
editing one line of the manifest — the production `/body` artifact is never touched.

## Producing a stamped bake

The bake binaries take the output name as `argv[2]`, so the stamp is the caller's
responsibility (`{out}.blocks` is derived automatically):

```sh
STAMP="body.$(date +%Y%m%d).v5f16"
./soabake fma_concepts.json "$STAMP.soa" # writes $STAMP.soa + $STAMP.blocks
gzip -k "$STAMP.soa" # → $STAMP.soa.gz (keep the raw .soa too)
# validate, then (and only then) promote:
cp "$STAMP.soa.gz" body.soa.gz # re-point /body, old stamps retained
```

Never `rm` a prior stamp. Disk is cheap; a black-screen deploy from a clobbered
artifact is not.

## `/helix` needs the canonical bake (`helixbake`), NOT the old artifact

The production `body.soa.gz` stores its per-vertex normal with the OLD ndarray
`helix_orient` codec (a place-blind 3-byte golden-spiral cascade). The canonical
`/helix` viewer decodes the **place-coupled `lance-graph::helix::Signed360`**
(6-byte: rim endpoint pair + signed polar lift + golden azimuth), so it CANNOT read
the old bytes — it would render garbage. `/helix` therefore reads only the stamped
canonical artifact named by `helix_latest`, and shows "no canonical helix bake yet"
until one is published.

Produce it with the **separate** bake crate `scratch-fma/helixbake` (soabake — the
`/body` bake — is left byte-identical; helixbake is its own crate so the old
pipeline never resolves helix):

```sh
cd scratch-fma/helixbake
STAMP="body.$(date +%Y%m%d).v6helix"
cargo run --release -- /path/to/soa "$STAMP.soa" # writes $STAMP.soa (BSO2 ver 6) + .blocks
gzip -k "$STAMP.soa" # → $STAMP.soa.gz
# then add to body.manifest.json: "helix_latest": "$STAMP.soa.gz"
```

The normal is generated via `helix::ResidueEncoder::encode_signed(place, n, sign)`
— `place` = the concept's HHTL path, `n` = the nearest spherical-Fibonacci index of
the world normal, `sign` = its hemisphere. `cargo test` in that crate runs the
encode↔decode round-trip (the same decode BodyHelix.tsx uses) on synthetic normals,
no FMA data required.

(Build note: `helix` depends on `ndarray` via git; `helixbake/Cargo.toml` patches it
to the local `../../../ndarray` fork. A bake host that can't fetch the git source
relies on that patch; this sandbox's proxy blocks the fetch, so the crate is
validated by the round-trip test on a network-enabled host, not here.)
Loading