Files
Claudeandclaude-flow 43f16e5638 perf(npm): binary kernel transport, faster device e2e, and CI fixes (ADR-368/373 amendments)
CI fixes (both failures on PR #2062 were in this branch):
- harness/ruview: remove `2.11.0` literals from training remedies; the
  ADR-265 D3 version-literal gate rejects them.
- ruview-kernel workflow: install wasm32 for the toolchain pinned by
  v2/rust-toolchain (the action only added it to `stable`); build.mjs now
  fails fast with the exact `rustup target add` remedy.

Kernel optimization (ADR-368 amendment):
- New `analyze_flat` / `session_push_flat` ops via `call_f64`: WASM
  `rvk_call_f64` (alignment-agnostic bytes) and napi `callF64(Float64Array)`.
  `analyze()`/`session.push()` pack uniform frames automatically; irregular
  input keeps the JSON path and its per-frame errors. `analyzeFlat()` is the
  zero-copy entry. Shared `Session::step` makes both paths byte-identical on
  identical f64 inputs (Rust test), and the binary path avoids serde_json's
  non-correctly-rounded float parsing.
- MEASURED (`ruview-kernel bench --seconds 120`, one Linux x64 host):
  2.82x (wasm) and 2.45x (napi) faster than the JSON transport.
- Profiled remaining cost (examples/profile_stages.rs): heart-rate ACF in
  wifi-densepose-vitals dominates; left for a separate, output-reviewed
  change. Rejected WASM +simd128 and opt-level=s after three measured runs
  (bench/wasm-variants.mjs).

Device e2e (ADR-373 amendment):
- test/e2e/devices.e2e.mjs + `npm run test:e2e:devices` + new workflow
  ruview-device-e2e.yml: pty-emulated MR60BHA2 and RPLIDAR through real
  pyserial, a UDP ESP32 node, and the real iPhone relay, via the CLI.
- mmWave auto-detect keeps probe frames and reads only the remainder;
  iPhone LiDAR gains max_frames and returns immediately when refused.
  Suite wall time 19.6 s -> 5.6 s (MEASURED, dev container).

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_015H55jmprfz4oHWXSFbX5rf
2026-10-01 14:45:55 +00:00

181 lines
7.7 KiB
JavaScript
Executable File

#!/usr/bin/env node
// SPDX-License-Identifier: MIT
// `npx @ruvnet/ruview-kernel` — run the RuView compute kernel from the shell
// (ADR-368). Zero runtime dependencies; WASM is the default backend.
import { closeSync, openSync, readFileSync, readSync, realpathSync, statSync, writeFileSync } from 'node:fs';
import { argv } from 'node:process';
import { fileURLToPath } from 'node:url';
import { MAX_INPUT_BYTES } from '../src/abi.js';
import { BACKENDS, kernelStatus, loadKernel, parity, selfTest } from '../src/index.js';
const pjson = (value) => console.log(JSON.stringify(value, null, 2));
function parseFlags(rest) {
const flags = {};
for (let i = 0; i < rest.length; i++) {
const a = rest[i];
if (!a.startsWith('--')) continue;
const eq = a.indexOf('=');
if (eq !== -1) flags[a.slice(2, eq)] = a.slice(eq + 1);
else if (i + 1 < rest.length && !rest[i + 1].startsWith('--')) flags[a.slice(2)] = rest[++i];
else flags[a.slice(2)] = true;
}
return flags;
}
function number(flags, key, fallback) {
if (flags[key] === undefined) return fallback;
const n = Number(flags[key]);
if (!Number.isFinite(n)) throw new Error(`--${key} must be a number`);
return n;
}
/** Bounded read of a caller-named regular file (JSON array/object or JSONL frames). */
function readFrames(path) {
const stat = statSync(path);
if (!stat.isFile()) throw new Error(`${path} is not a regular file`);
if (stat.size > MAX_INPUT_BYTES) throw new Error(`${path} exceeds ${MAX_INPUT_BYTES} bytes`);
const fd = openSync(path, 'r');
let text;
try {
const buf = Buffer.alloc(stat.size);
readSync(fd, buf, 0, stat.size, 0);
text = buf.toString('utf8');
} finally {
closeSync(fd);
}
const trimmed = text.trim();
let parsed;
try {
parsed = JSON.parse(trimmed);
} catch {
// Not a single JSON document: treat as JSONL, one frame per line.
const frames = trimmed.split('\n').filter((l) => l.trim()).map((l, i) => {
try { return JSON.parse(l); } catch { throw new Error(`line ${i + 1} is not valid JSON`); }
});
return { frames };
}
if (Array.isArray(parsed)) return { frames: parsed };
if (Array.isArray(parsed?.frames)) return { frames: parsed.frames, config: parsed.config };
if (parsed && Array.isArray(parsed.amplitudes)) return { frames: [parsed] };
throw new Error('input must be a JSON array of frames, {frames, config}, or JSONL');
}
function help() {
console.log(`Usage: ruview-kernel <command> [--backend wasm|napi|auto]
doctor [--strict-napi] report both backends, ABI, and artifact integrity
info kernel build info and limits
selftest [--seconds 60] SYNTHETIC end-to-end pipeline check
parity diff wasm vs napi outputs on the same SYNTHETIC input
synth [--seconds 60] [--breathing 15] [--heart 72] [--subcarriers 56] [--seed 7] [--out file.json]
analyze --file frames.json|frames.jsonl [--config '{"n_subcarriers":56}'] [--readings]
bench [--seconds 120] time analyze on SYNTHETIC input (MEASURED on this host)
Default backend: RUVIEW_KERNEL_BACKEND or wasm (zero-import, least authority).
Outputs are signal-processing estimates, not clinical or camera-grade measurements.`);
return 0;
}
export async function run(args) {
const cmd = args[0] ?? '--help';
const flags = parseFlags(args.slice(1));
if (flags.backend !== undefined && !BACKENDS.includes(flags.backend)) {
console.error(`--backend must be one of ${BACKENDS.join(', ')}`);
return 2;
}
const load = () => loadKernel({ backend: flags.backend });
switch (cmd) {
case 'doctor': {
const status = kernelStatus();
const ok = status.wasm.available && (!flags['strict-napi'] || status.napi.available);
pjson({ ok, ...status, note: status.napi.available ? 'both backends available' : 'napi optional; wasm backend active by default' });
return ok ? 0 : 1;
}
case 'info': {
const k = load();
pjson({ backend: k.backend, requestedBackend: k.requestedBackend, fallbackReason: k.fallbackReason, integrity: k.integrity, ...k.info() });
return 0;
}
case 'selftest': {
const k = load();
const result = selfTest(k, { seconds: number(flags, 'seconds', 60) });
pjson({ ...result, requestedBackend: k.requestedBackend, fallbackReason: k.fallbackReason });
return result.ok ? 0 : 1;
}
case 'parity': {
const result = parity(loadKernel({ backend: 'wasm' }), loadKernel({ backend: 'napi' }));
pjson(result);
return result.ok ? 0 : 1;
}
case 'synth': {
const out = load().synthesize({
seconds: number(flags, 'seconds', 60),
breathing_bpm: number(flags, 'breathing', 15),
heart_bpm: number(flags, 'heart', 72),
n_subcarriers: number(flags, 'subcarriers', 56),
sample_rate_hz: number(flags, 'rate', 20),
seed: number(flags, 'seed', 7),
});
const body = JSON.stringify({ evidence: out.evidence, config: { n_subcarriers: out.request.n_subcarriers, sample_rate_hz: out.request.sample_rate_hz }, frames: out.frames });
if (typeof flags.out === 'string') {
writeFileSync(flags.out, `${body}\n`, { flag: 'wx' }); // never overwrite an existing file
pjson({ ok: true, evidence: out.evidence, frames: out.frames.length, out: flags.out });
} else console.log(body);
return 0;
}
case 'analyze': {
if (typeof flags.file !== 'string') { console.error('analyze: --file <frames.json|frames.jsonl> is required'); return 2; }
const input = readFrames(flags.file);
const config = typeof flags.config === 'string' ? JSON.parse(flags.config) : (input.config || {});
const k = load();
pjson({ ok: true, backend: k.backend, ...k.analyze(input.frames, config, { includeReadings: flags.readings === true }) });
return 0;
}
case 'bench': {
const k = load();
const seconds = number(flags, 'seconds', 120);
const { frames } = k.synthesize({ seconds });
k.analyze(frames.slice(0, 200)); // warm-up
const iterations = 5;
const time = (fn) => { const t0 = performance.now(); for (let i = 0; i < iterations; i++) fn(); return (performance.now() - t0) / iterations; };
const ms = time(() => k.analyze(frames));
const jsonMs = time(() => k.call('analyze', { config: {}, frames, include_readings: false }));
pjson({
evidence: 'MEASURED',
reproducer: `npx @ruvnet/ruview-kernel bench --backend ${k.backend} --seconds ${seconds}`,
host: { node: process.version, platform: process.platform, arch: process.arch },
backend: k.backend, frames: frames.length, transport: k.binary ? 'binary' : 'json',
msPerAnalyze: Number(ms.toFixed(2)), framesPerSecond: Math.round(frames.length / (ms / 1000)),
jsonTransportMs: Number(jsonMs.toFixed(2)), speedupVsJson: Number((jsonMs / ms).toFixed(2)),
note: 'Includes transfer across the ABI; single-host timing, not a cross-host claim.',
});
return 0;
}
case '--version': case '-v': {
console.log(JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version);
return 0;
}
case '--help': case '-h': case 'help': return help();
default:
console.error(`Unknown command: ${cmd}. Try \`ruview-kernel --help\`.`);
return 2;
}
}
const invokedDirectly = (() => {
if (!argv[1]) return false;
try {
const a = realpathSync(argv[1]);
const b = realpathSync(fileURLToPath(import.meta.url));
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
} catch { return false; }
})();
if (invokedDirectly) {
run(argv.slice(2)).then((code) => process.exit(code)).catch((error) => {
pjson({ ok: false, code: error.code || 'error', error: error.message });
process.exit(1);
});
}