mirror of
https://github.com/ruvnet/RuView
synced 2026-07-17 16:33:18 +00:00
docs(adr): deep review of the RuView npm surface — ADR-263/264/265 optimization strategies
ADR-263 — @ruvnet/ruview@0.1.0 harness review (O1–O9):
- HIGH: claim-check CLI fails open on empty input (no --text/--file -> PASS exit 0)
- HIGH: MCP stdio server head-of-line blocking (spawnSync verify/calibrate up to 600s)
- MEASURED: optionalDependencies triple the cold npx install (4 pkgs/620kB/71 files
vs 1 pkg/172kB/22 files with --omit=optional) for a path that never imports them
- maxBuffer truncation, python -c port interpolation, version drift, duplicate skills,
guardrail METRIC_TERMS substring false positives ('map'/'F1' — found by dogfooding
claim-check on these very ADRs), zero CI
ADR-264 — @ruvnet/rvagent@0.1.0 + @ruv/ruview-cli review (O1–O9), verified against
the published registry tarball:
- HIGH: exports.require -> dist/index.cjs which is never built nor published
- MEASURED: 44 dead source-map files = 62,698B of the 188kB unpacked payload
- stdio-only server described as dual-transport; mixed dot/underscore tool names;
double Zod validation + hand-duplicated advertised schemas; 2-fd leak per training
job; unbounded body in the unwired HTTP scaffold; dead detectCogBinary candidates;
ruview bin-name collision
ADR-265 — cross-cutting npm distribution strategy: npm-packages.yml CI matrix
(test + pack-content/size gate + tarball-install smoke test), publish-from-CI-only
with npm provenance, version single-sourcing from package.json, bin/namespace
ownership (ruview bin belongs to @ruvnet/ruview), claim-check on package READMEs.
Docs only — no runtime code changed. Index/CHANGELOG/CLAUDE.md/README counts updated.
Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz
This commit is contained in:
@@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **`homecore-recorder` security review (ADR-132 surfaces) — two real bounding fixes; SQL-injection & NaN-index dimensions confirmed clean with evidence.** Beyond-SOTA review of the HA-compat state recorder (DB persistence + history + ruvector semantic search), the crux being its DB-backed SQL-injection surface. **Findings + fixes:** (1) **Memory-DoS — unbounded `get_state_history`.** The history query carried no `LIMIT`, so a wide `[since, until]` window over a high-frequency entity (a per-second sensor ≈ 86k rows/day) would load an unbounded row set into a single in-memory `Vec`. Added a hard `LIMIT MAX_HISTORY_ROWS` (1,000,000 — generous enough never to truncate a realistic history graph, bounded enough to cap the worst case); the sibling search paths were already `k`-bounded. (2) **Disk-DoS / documented-but-missing `purge`.** The README + HA-compat table advertised `Recorder::purge(older_than)` as a capability, but **no such method existed** — i.e. no retention path at all → unbounded disk growth. Implemented a **transactional** `purge` that deletes `states` + `events` strictly **older than** the cutoff (**exclusive** boundary — idempotent, no off-by-one; a row at the cutoff instant is kept) and **garbage-collects** orphaned `state_attributes` blobs (a dedup-shared blob is dropped only once its last referencing state is gone); all three deletes run in one transaction so a mid-purge failure rolls back cleanly (no states-deleted-but-events-kept corruption). **Confirmed clean with evidence:** SQL injection — **every** query in `db.rs` uses bound `?` parameters (no `format!`/string-concat of user data into SQL); the lone `format!` builds the LIKE *pattern*, which is itself bound as a parameter with `ESCAPE '\\'` and metacharacter escaping. Pinned: a state value `'; DROP TABLE states; --` is stored/queried **literally** (table survives), and a `%`/`_` in a search query matches **literally**, not as a wildcard. NaN-index poisoning (the calibration/vitals/geo class) — **structurally impossible** here: embeddings are SHA-256 → `i32` → `f32` (an `i32` cast to `f32` is always finite, never NaN/Inf), with an all-zero-digest norm guard; probed empty-index search, empty-string query, and `k=0` — all return `Ok(0)`, **no panic**. Fail-closed write path — a removal event yields `Ok(None)`, semantic-index failure is logged not propagated (best-effort, never blocks the durable SQLite write), and `EntityId` parsing failures fall back rather than panic. **6 new pinning tests** (SQL-injection literal-storage, LIKE-metacharacter literalness, history `LIMIT`, purge exclusive-boundary, purge attribute-GC-keeps-shared, purge old-events): `homecore-recorder` **19 → 25** (`--no-default-features`) / **25 → 31** (`--features ruvector`), 0 failed; the purge-boundary test is a true pin (fails deleting 2 rows under an inclusive cutoff, passes deleting 1 under the exclusive cutoff). Behaviour otherwise unchanged; Python deterministic proof unchanged (recorder is off the signal proof path).
|
||||
|
||||
### Added
|
||||
- **ADR-263/264/265: deep review of the RuView npm surface (`@ruvnet/ruview`, `@ruvnet/rvagent`, `@ruv/ruview-cli`) with optimization strategies recorded as ADRs.** ADR-263 reviews the published `@ruvnet/ruview@0.1.0` harness: fail-open `claim-check` on empty input (HIGH), `spawnSync` head-of-line blocking of the MCP stdio server during long `verify`/`calibrate` runs (HIGH), optionalDependencies tripling the cold `npx` install for a code path that never uses them (MEASURED, `npm i` in a clean prefix: 4 packages / 620 kB / 71 files default vs 1 package / 172 kB / 22 files with `--omit=optional`), 1 MiB `maxBuffer` truncation risk, `python -c` port-interpolation surface in `node_monitor`, hardcoded MCP server version, duplicated skill payload — optimizations O1–O8. ADR-264 reviews `@ruvnet/rvagent@0.1.0` + the private CLI **against the published registry tarball**: `exports.require` → nonexistent `dist/index.cjs` (HIGH, every CJS consumer breaks), 44 dead source-map files = 62,698 B of the 188 kB unpacked payload pointing at unshipped `../src` (MEASURED), stdio-only server described as "dual-transport" (CLAIMED capability), mixed dot/underscore tool naming, double Zod validation + hand-duplicated advertised schemas, 2-fd leak per training job, unbounded request body in the unwired HTTP scaffold, dead `detectCogBinary` candidate list, `ruview` bin-name collision — optimizations O1–O9. ADR-265 adds the cross-cutting distribution layer: an `npm-packages.yml` CI matrix (tests + pack-content/size gate + tarball-install smoke test — none of the three packages currently has any CI, and `ci.yml` pins Node 18 against `engines >= 20`), publish-from-CI-only with `npm publish --provenance`, version single-sourcing from package.json, bin/namespace ownership (the `ruview` bin belongs to `@ruvnet/ruview`), and claim-check enforcement on package READMEs/descriptions. Docs only — no runtime code changed; the findings are the work orders for the follow-up PRs.
|
||||
- **ADR-131 §11–§12: HOMECORE-UI wired to a real backend — single-origin BFF gateway + production front-end (no mock in prod).** Implements the §11 wiring decision so the dashboard stops rendering fabricated data. **Front-end (DONE + verified under Node):** `api.js` rewritten so every data accessor is async and calls the §11.2 gateway routes; the in-browser mock is demoted to a **dev-only fixture** reachable only via `?demo=1`/`HOMECORE_UI_DEMO` (§2.2); all ten panels now `await` and render a **typed empty/error state** on upstream failure (no mock fallback in production) — 3 panels converted by hand, 7 via a parallel agent swarm. **New `homecore-server` BFF gateway (`src/gateway.rs`, compile-pending — no Rust toolchain in the authoring env):** promotes `homecore-server` to the single origin (§2.1); adds `/api/homecore/*` + `/api/cal/*` merged into `build_app`, with `reqwest` + CLI/env flags (`--calibration-url`/`--calibration-token`/`--apps-dir`/`--gateway-timeout-ms`). Real handlers: calibration **reverse-proxy** (W2), `GET /api/homecore/rooms` with the §11.3 **RoomState adapter** (`breathing`→`breathing_bpm`, `heartbeat`→`heart_bpm`, `None`→`null` preserving not-trained-vs-withheld, injected `anomaly.threshold`/`room_id`), **COG supervisor** over `/var/lib/cognitum/apps/` (W4), and **appliance metrics** from `/proc` + TCP service probes (W6); SEED-device/appliance routes (seeds/federation/witness/privacy/settings/automations/events-history/hailo/tokens — W3/W5) return a typed `503 upstream_unavailable` and the UI shows error states. **Tests:** front-end **5 files green** — import-graph, boot, render-smoke (22), interaction (3), and a **new prod-errors suite (13)** that runs with demo OFF + gateway unreachable and proves every panel renders an error state, never mock, never throws (it caught + fixed a real unhandled-rejection in the events automation builder). **Gateway compiled, tested, and run on Rust 1.89:** `cargo test -p homecore-server --no-default-features` = **12/12 pass** (6 gateway + 6 UI mount); the binary was **run live** — `GET /api/homecore/appliance` returns real `/proc` metrics + TCP service probes, unauth → `401`, `cogs` → `[]` (no apps dir), SEED-tier → typed `503`, and against a mock calibration upstream the `/api/cal/*` proxy passes through (`200`) and `GET /api/homecore/rooms` adapts `RoomState` to the UI shape (`breathing`→`breathing_bpm`, `heartbeat:null`→`heart_bpm:null`, injected `anomaly.threshold`/`room_id`). **Live testing caught + fixed a real bug** — a double-`v1` segment in the `/api/cal/*` proxy URL. **Remaining (intrinsic, not an env limit):** W3/W5/W6-Hailo/federation depend on services/hardware **not in this repo** (recorder/automation HTTP wrappers, real SEED nodes, Hailo stat source), so they return honest `503`s rather than fabricate data; W1/W2/W4/W6-appliance are functional now. ADR-131 §10/§12.1 updated with per-wave status.
|
||||
- **ADR-131: HOMECORE-UI — the complete operational dashboard for the two-tier Cognitum stack, served by `homecore-server` at `/homecore`.** A zero-dependency, no-build-step vanilla TS/JS + CSS frontend (the `rufield-viewer` "Axum + vanilla-JS" pattern) that extends the Cognitum Appliance shell as a first-class nav section (Framework | Guide | Cog Store | **HOMECORE** | Status). **Complete, not a scaffold** (per the ADR's revised §2/§7): all **10 panels** ship fully built and rendered — §4.1 System Dashboard (v0 Appliance health strip + SEED fleet grid + ESP32 summary + COG status row + event-bus sparkline), §4.2 SEED Detail (vector store / witness chain / 5 onboard sensors / reflex rules / cognitive-fragility / ingest packet-type), §4.3 SEED Fleet Map (Appliance→SEED→ESP32 hierarchy, ESP-NOW mesh, cross-SEED fusion badges, ADR-105 federation), §4.4 Entity & State Browser (domain-grouped, **live WebSocket `subscribe_events` patching — never polls**, first-class provenance badges, keyword filter, context-causality slide-over), §4.5 RoomState/Sensing (mixture-of-specialists), §4.6 COG Management + App Registry, §4.7 Calibration Wizard (5-step baseline→enroll→train→verify), §4.8 Event Bus + Automation builder, §4.9 Witness/Audit log (two-tier SHA-256 + Ed25519 timeline, privacy-mode banner, pagination, export), §4.10 Settings. **Design system is the exact production Cognitum palette** (`tokens.css` carries `--cyan #4ecdc4` … `--r 10px` verbatim, §3.1) so there is no visual seam with the Cog Store (§3.3 invariant). **§6 UX invariants enforced in code and pinned by tests:** tier-origin provenance is always-visible (never collapsed); `stale`/`vetoed` flags and the kNN fragility score are prominent (amber/red tint + banners, never grey-on-grey); a `null` specialist renders "Not trained / calibrate to enable" **visually distinct from** veto-`withheld` (rendered as explicitly withheld, never zero) **distinct from** an error; all IDs/hashes/endpoints/payloads use `--mono`; Hailo-sourced COGs (`arch: hailo10`) are visually distinguished from CPU-only (`arch: arm`). **Wiring:** `homecore-server` gains a `--ui-dir`/`HOMECORE_UI_DIR` flag and mounts the assets via `tower-http` `ServeDir` at `/homecore` alongside the unchanged HA-compat `/api` surface (new testable `build_app()`), with **5 Rust integration tests** (`#[cfg(test)] mod ui_tests`, `tower::oneshot`) asserting index / design tokens / all-10-panels are served, the API coexists, and an empty `--ui-dir` disables the mount. **JS test + benchmark suite (`ui/`, runs under plain `node`, no npm install): 24 checks / 0 failed** — an import/export graph verifier (15 modules consistent), a DOM-shim render-smoke that *executes every panel* (21 checks: ui helpers + mock contracts + all 10 panels render without throwing), and an interaction suite (3 checks: live WS state-patch, ws.js handshake/parse, calibration backend contract). **Benchmark:** total bundle **136.8 KB uncompressed across 18 files — ~37× smaller than HA's ~5 MB Lit bundle** (the ADR-126 §1.1 foil), slowest panel **1.5 ms/cold-render**. **Honest scope (§7.1):** the live HOMECORE REST API (`/api/config|states|services`) and the WebSocket `subscribe_events` feed are driven for real; panels whose backing service is **not** in this binary (SEED HTTPS API, calibration ADR-151, ADR-105 federation) render against a **contract-conformant mock layer flagged with a DEMO banner** and swap to live the moment those endpoints land — no mock data is ever presented as real. **Not verified in this environment:** the Rust crate was edited and the integration tests written but **not compiled/run here** (no Rust toolchain present); `cargo test -p homecore-server` + `cargo build` must be run on a Rust host before merge.
|
||||
- **ADR-175: int8 quantization of the WiFlow-STD "half" pose model — MEASURED fp32-vs-int8 accuracy/size trade-off (honest negative).** Sub-deliverable 8.2 of the benchmark/optimization milestone, and the reading of the SOTA brief's "one untested edge lever" (QAT-int8 on the 843,834-param half model that strictly dominates the published 2.23M model). A new committed script `v2/crates/wifi-densepose-train/scripts/quantize_half_int8.py` quantizes `half_best.pth` to int8 two ways and scores both with the **same** upstream `calculate_pck`/`calculate_mpjpe` that produced the fp32 sweep numbers, under **one locked normalization** (ADR-173 torso-diameter PCK — neck idx2→pelvis idx12, `use_torso_norm=True`, the standard MM-Fi/GraphPose-Fi convention), on the **same** seed-42 file-level 70/15/15 test split (52,560 NaN-free / 54,000 full windows). **MEASURED on ruvultra (RTX 5080, torch 2.11.0+cu128, fbgemm; clean test, torso-PCK):** fp32 = 96.62% PCK@20 / 99.47% PCK@50 / 0.008981 MPJPE / 3.351 MB (fp32-CPU reproduces fp32-GPU to 4 dp, so the int8 deltas are pure quantization, not CPU/GPU drift); **int8 static PTQ = 40.98% PCK@20 (−55.64 pp), 1.046 MB** — naive static QDQ **collapses** on this model (the brief's 2.23M "sweet spot" does NOT transfer to the 843k half model at the tight @20 threshold); **int8 QAT (3-epoch FX fake-quant fine-tune from half_best) = 67.48% PCK@20 (−29.15 pp) / 98.69% PCK@50 (−0.78 pp), 1.043 MB.** **Verdict (honest no):** int8 is **not a win** at the strict PCK@20 edge target — QAT recovers a large share of the PTQ collapse and is near-lossless at the loose PCK@50 (coarse localization survives int8, fine does not), but a **3.2× size win at −29 pp PCK@20** is a bad trade when the half model already fits edge flash at fp32 → **keep fp32/fp16 on the edge for now.** **Disclosed gap:** the QAT *fake-quant* val PCK@20 reached 83.45% but the *converted* int8 model scores 67.48% — a real ~16 pp `convert_fx` gap (fbgemm int8 kernels ≠ straight-through estimate, esp. the axial-attention einsum/softmax); we report the converted-int8 number, not the fake-quant proxy. **MEASURED:** every table number + the PTQ collapse + the QAT partial recovery + the conversion gap. **CLAIMED/not done:** ONNX/TFLite export, on-edge-SoC latency/energy (int8 measured on x86 fbgemm — size transfers, latency does NOT), mixed-precision keeping attention fp32, longer/better-tuned QAT. **Honest limitations:** single in-domain eval split (no cross-environment split), x86-int8 not edge-SoC-int8, lightly-tuned QAT. Additive only — no production Rust or signal-pipeline change; Python deterministic proof unchanged (`f8e76f21…46f7a`, bit-exact — off the signal proof path).
|
||||
|
||||
@@ -62,7 +62,7 @@ All 5 ruvector crates integrated in workspace:
|
||||
- `ruvector-attention` → `model.rs` (apply_spatial_attention) + `bvp.rs`
|
||||
|
||||
### Architecture Decisions
|
||||
43 ADRs in `docs/adr/` (ADR-001 through ADR-043). Key ones:
|
||||
182 ADRs in `docs/adr/` (numbered ADR-001 through ADR-265, with gaps). Key ones:
|
||||
- ADR-014: SOTA signal processing (Accepted)
|
||||
- ADR-015: MM-Fi + Wi-Pose training datasets (Accepted)
|
||||
- ADR-016: RuVector training pipeline integration (Accepted — complete)
|
||||
@@ -77,6 +77,10 @@ All 5 ruvector crates integrated in workspace:
|
||||
- ADR-148: Drone swarm control system / `ruview-swarm` (In Progress)
|
||||
- ADR-152: WiFi-Pose SOTA 2026 intake — geometry conditioning, WiFlow-STD benchmark (measurement (a) complete: claims MEASURED-EQUIVALENT at ~96% PCK@20), MAE recipe (Proposed; §2.1–2.3, 2.6 implemented)
|
||||
- ADR-153: IEEE 802.11bf-2025 forward-compatibility protocol model (Accepted — amends ADR-152 §2.4)
|
||||
- ADR-182: `npx ruview` harness minted via MetaHarness (Accepted — P1+P2 shipped as `@ruvnet/ruview`)
|
||||
- ADR-263: `@ruvnet/ruview` npm harness deep review + optimization strategy (Proposed)
|
||||
- ADR-264: `@ruvnet/rvagent` MCP server + `@ruv/ruview-cli` deep review + optimization strategy (Proposed)
|
||||
- ADR-265: RuView npm distribution strategy — CI gate, provenance, version single-sourcing (Proposed)
|
||||
|
||||
### Supported Hardware
|
||||
|
||||
|
||||
@@ -617,7 +617,7 @@ Verify the plugin structure: `bash plugins/ruview/scripts/smoke.sh`. Full detail
|
||||
| [Semantic Primitives — Precision/Recall](docs/integrations/semantic-primitives-metrics.md) | Per-primitive F1 on the held-out paired-capture set: someone-sleeping, possible-distress, room-active, elderly-inactivity-anomaly, meeting, bathroom, fall-risk, bed-exit, no-movement, multi-room. |
|
||||
| [Claude Code / Codex Plugin](plugins/ruview/README.md) | The `ruview` plugin + marketplace — skills, `/ruview-*` commands, agents, and the Codex prompt mirror |
|
||||
| [Portable harness — `npx @ruvnet/ruview`](harness/ruview/README.md) | MetaHarness-minted, host-portable RuView operator harness — `ruview.*` MCP tools + the MEASURED-vs-CLAIMED honesty guardrail enforced in code ([ADR-182](docs/adr/ADR-182-npx-ruview-harness-via-metaharness.md)). A lighter, multi-host companion to the in-repo plugin. |
|
||||
| [Architecture Decisions](docs/adr/README.md) | 96 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
||||
| [Architecture Decisions](docs/adr/README.md) | 182 ADRs — why each technical choice was made, organized by domain (hardware, signal processing, ML, platform, infrastructure) |
|
||||
| [Domain Models](docs/ddd/README.md) | 8 DDD models (RuvSense, Signal Processing, Training Pipeline, Hardware Platform, Sensing Server, WiFi-Mat, CHCI, rvCSI) — bounded contexts, aggregates, domain events, and ubiquitous language |
|
||||
| [rvCSI — edge RF sensing runtime](https://github.com/ruvnet/rvcsi) | Rust-first / TypeScript-accessible / hardware-abstracted CSI runtime: multi-source ingestion (incl. real nexmon_csi `.pcap` from a **Raspberry Pi 5** / Pi 4 / Pi 3B+ — CYW43455 / BCM43455c0) → validation → DSP → typed events → RuVector RF memory ([ADR-095](docs/adr/ADR-095-rvcsi-edge-rf-sensing-platform.md), [ADR-096](docs/adr/ADR-096-rvcsi-ffi-crate-layout.md), [domain model](docs/ddd/rvcsi-domain-model.md)). Now its own repo — [`ruvnet/rvcsi`](https://github.com/ruvnet/rvcsi) — vendored here under `vendor/rvcsi`; 9 `rvcsi-*` crates on crates.io, `@ruv/rvcsi` on npm, plus a Claude Code plugin. |
|
||||
| [Desktop App](v2/crates/wifi-densepose-desktop/README.md) | **WIP** — Tauri v2 desktop app for node management, OTA updates, WASM deployment, and mesh visualization |
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# ADR-263: `@ruvnet/ruview` npm Harness — Deep Review + Optimization Strategy
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed — review complete (findings recorded with evidence), optimizations O1–O8 not yet implemented |
|
||||
| **Date** | 2026-07-02 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **RUVIEW-NPM-REVIEW-1** |
|
||||
| **Supersedes / amends** | none (records review of the ADR-182 P1+P2 artifact; feeds ADR-265 distribution strategy) |
|
||||
|
||||
## Context
|
||||
|
||||
ADR-182 minted and published **`@ruvnet/ruview@0.1.0`** (`harness/ruview/`) — the
|
||||
`npx ruview` operator harness: a dependency-free ESM CLI + minimal MCP stdio server
|
||||
exposing six `ruview.*` tools (onboard / claim_check / verify / node_monitor /
|
||||
calibrate / node_flash), five skill playbooks, and the executable
|
||||
MEASURED-vs-CLAIMED guardrail (`src/guardrails.js`). The package is live on npm
|
||||
(0.1.0, 49.5 kB unpacked / 21 files — MEASURED, `npm view @ruvnet/ruview` +
|
||||
`npm pack --dry-run`) and is the recommended MCP registration path
|
||||
(`npx -y @ruvnet/ruview mcp start` in the bundled `.claude/settings.json`).
|
||||
|
||||
This ADR is the first dedicated deep review of that npm artifact: correctness,
|
||||
fail-open/fail-closed posture, performance (cold start + request handling),
|
||||
packaging hygiene, and security of the subprocess surface. All 17 bundled tests
|
||||
pass on Node 22 (MEASURED, `node --test test/*.test.mjs`, 17/17, ~108 ms).
|
||||
|
||||
## Findings
|
||||
|
||||
Severity reflects impact on the package's stated contract: *fail-closed operator
|
||||
tools + an honesty guardrail that must never fail open*.
|
||||
|
||||
### F1 (HIGH, fail-open): `claim-check` passes silently on empty input
|
||||
|
||||
`bin/cli.js` `claim-check` with **neither `--text` nor `--file`** sends
|
||||
`text: undefined` → `claimCheck(String(args.text ?? ''))` → `''` → `ok: true`,
|
||||
**exit 0**. A CI hook wired as `npx ruview claim-check --text "$BODY"` where
|
||||
`$BODY` expands empty therefore reports PASS. This is the single tool whose whole
|
||||
purpose is to fail closed; empty input must be an error, not a pass.
|
||||
Reproducer: `node bin/cli.js claim-check` → `{"ok": true}`, exit 0.
|
||||
|
||||
### F2 (HIGH, head-of-line blocking): MCP server is fully synchronous
|
||||
|
||||
`src/mcp-server.js` dispatches `tools/call` inside the readline `line` handler,
|
||||
and every heavyweight handler in `src/tools.js` uses **`spawnSync`**
|
||||
(`ruview.verify` up to 180 s, `ruview.calibrate` up to 300–600 s,
|
||||
`ruview.node_monitor` up to `seconds+10`). While one call runs, the event loop is
|
||||
blocked: `ping`, `tools/list`, and concurrent `tools/call` requests are not even
|
||||
read from stdin. Hosts that health-check with `ping` during a long `calibrate`
|
||||
will conclude the server is dead and kill it mid-run.
|
||||
|
||||
### F3 (MEDIUM, cold start): optionalDependencies triple the `npx` install for a path that never uses them
|
||||
|
||||
`package.json` declares `optionalDependencies` on `@metaharness/kernel` and
|
||||
`@metaharness/host-claude-code`. npm installs optional deps **by default**, so
|
||||
every cold `npx -y @ruvnet/ruview mcp start` fetches 3 extra packages (kernel +
|
||||
host + transitive `@ruvector/emergent-time`). MEASURED (npm 10.9.7, this
|
||||
container): default install = **4 packages, 620 kB, 71 files**; with
|
||||
`--omit=optional` = **1 package, 172 kB, 22 files**. The operator-tool and MCP
|
||||
paths never import these — only `doctor`/`install` do, and both already
|
||||
dynamic-import inside `try/catch` and degrade gracefully when absent
|
||||
(`kernel/host: not installed (ok…)`). The optional deps buy nothing on the hot
|
||||
path and cost 3 registry round-trips + ~450 kB on every cold start.
|
||||
|
||||
### F4 (MEDIUM, silent truncation): `spawnSync` default `maxBuffer` (1 MiB)
|
||||
|
||||
`run()` in `src/tools.js` never sets `maxBuffer`. `cargo run -p
|
||||
wifi-densepose-cli` (the `calibrate` fallback path) and a chatty `verify.py` can
|
||||
exceed 1 MiB of stdout, at which point the child is killed with `ENOBUFS` and the
|
||||
tool reports a spawn error that looks like a proof/calibration failure. The
|
||||
handlers only ever consume the last 8 kB/1.5 kB; buffering should be bounded but
|
||||
generous (e.g. `maxBuffer: 16 MiB`) or streamed with a tail ring.
|
||||
|
||||
### F5 (MEDIUM, injection surface): `node_monitor` interpolates the port into Python source
|
||||
|
||||
The handler builds a `python -c` script by string interpolation:
|
||||
`` `ser=serial.Serial(${JSON.stringify(port)},115200,…)` `` and
|
||||
`` `while time.time()-t<${dur}:` ``. `JSON.stringify` produces a *JavaScript*
|
||||
string literal; Python string-literal semantics differ at the edges (`\uXXXX` is
|
||||
shared, but e.g. JS emits raw U+2028/U+2029 unescaped pre-ES2019 rules aside, and
|
||||
any future non-JSON-safe field added the same way would be executable). `port`
|
||||
arrives from the MCP caller (an agent), so this is an agent-controlled string
|
||||
concatenated into an interpreter invocation. `dur` is `Number()`-guarded; `port`
|
||||
should be passed out-of-band (`sys.argv`/env), never spliced into source.
|
||||
|
||||
### F6 (LOW, drift): server version hardcoded
|
||||
|
||||
`SERVER_INFO = { name: 'ruview', version: '0.1.0' }` in `src/mcp-server.js`
|
||||
duplicates `package.json.version` (the CLI's `--version` already reads
|
||||
package.json at runtime). First release bump will drift the MCP handshake
|
||||
version.
|
||||
|
||||
### F7 (LOW, duplication): every skill ships twice
|
||||
|
||||
`skills/*.md` and `.claude/skills/*/SKILL.md` are byte-identical (same sha256 in
|
||||
`.harness/manifest.json`). ~8 kB of the 49.5 kB unpacked payload is duplicate
|
||||
content, and — worse than size — two copies must be kept in sync by hand.
|
||||
|
||||
### F8 (LOW, perf + portability): `which()` is uncached and shells out
|
||||
|
||||
`which()` runs up to twice per tool call (`python` then `python3`), each a
|
||||
blocking `spawnSync`; the POSIX branch spawns a shell (`shell: true`). Results
|
||||
are stable for the process lifetime and should be memoized; the lookup can be
|
||||
done dep-free with a PATH scan instead of a shell.
|
||||
|
||||
### F9 (LOW, interop): dot-named tools + minimal protocol surface
|
||||
|
||||
Tool names (`ruview.onboard`, `ruview.claim_check`, …) contain dots. MCP itself
|
||||
does not restrict names, but downstream host APIs commonly enforce
|
||||
`^[a-zA-Z0-9_-]{1,64}$` for tool names; hosts must then sanitize or reject.
|
||||
The server also answers `resources/list` / `prompts/list` with `-32601` (it does
|
||||
not advertise those capabilities, so this is spec-legal, but empty-list stubs are
|
||||
cheaper than every host's error path). Protocol version is pinned to
|
||||
`2024-11-05` with no negotiation fallback. None of this breaks Claude Code today;
|
||||
it narrows portability, which is the harness's whole pitch (9 hosts, ADR-182).
|
||||
|
||||
### F10 (LOW, CI gap): the published package has zero CI
|
||||
|
||||
No workflow under `.github/workflows/` runs `harness/ruview` tests (checked:
|
||||
no workflow references `harness/ruview`, `ruview-mcp`, or `ruview-cli`), and
|
||||
`ci.yml` pins `NODE_VERSION: '18'` while the package declares
|
||||
`engines.node >= 20`. Note also `node --test test/` (directory form) fails on
|
||||
Node 22 while the documented glob form passes — CI should pin the working
|
||||
invocation. Consolidated CI/publish strategy is ADR-265.
|
||||
|
||||
### F11 (MEDIUM, guardrail precision): `METRIC_TERMS` substring matching false-positives on ordinary prose
|
||||
|
||||
Found by dogfooding this review: `claimCheck` matches metric terms with
|
||||
`lower.includes(t)`, so the two-character terms `'map'` and `'f1'` fire inside
|
||||
ordinary words and labels — "source **map**s", "the **map**s can never
|
||||
resolve", finding IDs like "**F1** (HIGH…)". MEASURED reproducer: running
|
||||
`npx ruview claim-check --file` over this ADR and ADR-264 yields 4 and 16
|
||||
medium findings respectively, the majority of which are `map`/`F1`
|
||||
false positives on lines carrying no accuracy claim. A guardrail that cries
|
||||
wolf trains people to ignore it — precision is part of its fail-closed
|
||||
contract. Short/ambiguous terms need word-boundary matching (`\bmap\b`,
|
||||
`\bf1\b`, likewise `auc`, `iou`), and section-heading label patterns
|
||||
(`F\d+`, `O\d+`) should not count as metric mentions.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt the following optimization strategy, in priority order. Each item is
|
||||
independently shippable; F-numbers map to findings.
|
||||
|
||||
- **O1 (F1):** `claim-check` with no `--text`/`--file` (or empty text after read)
|
||||
exits 2 with a usage error. Add a regression test pinning exit ≠ 0.
|
||||
- **O2 (F2):** make the MCP dispatch async: convert `run()`/`which()` to
|
||||
promise-based `spawn`, make `tools/call` handlers `async`, and keep reading
|
||||
stdin while calls run (respond to `ping`/`tools/list` concurrently; serialize
|
||||
only same-tool hardware operations). Acceptance: `ping` round-trips < 50 ms
|
||||
while a synthetic 30 s `calibrate` is in flight.
|
||||
- **O3 (F3):** drop the two `optionalDependencies`; `doctor`/`install` already
|
||||
degrade and should print the exact `npm i @metaharness/kernel
|
||||
@metaharness/host-claude-code` hint on the miss path. Acceptance: cold
|
||||
`npm i @ruvnet/ruview` installs exactly 1 package (MEASURED baseline above).
|
||||
- **O4 (F4):** set `maxBuffer: 16 * 1024 * 1024` in `run()` (or stream + tail).
|
||||
- **O5 (F5):** pass `port` to the monitor script via `sys.argv`
|
||||
(`python -c script -- <port>`), never by source interpolation.
|
||||
- **O6 (F6):** read the MCP `serverInfo.version` from `package.json` once at
|
||||
startup (same pattern the CLI already uses).
|
||||
- **O7 (F7):** make `skills/*.md` the single source and generate
|
||||
`.claude/skills/*/SKILL.md` in a `prepack` script (or vice versa); manifest
|
||||
hashes then pin one canonical set.
|
||||
- **O8 (F8, F9):** memoize `which()`; add underscore aliases for the dot-named
|
||||
tools (accept both in `tools/call`, advertise the underscore form) and add
|
||||
empty `resources/list` / `prompts/list` stubs.
|
||||
- **O9 (F11):** switch `METRIC_TERMS` matching to word-boundary regexes for
|
||||
short terms (`map`, `f1`, `auc`, `iou`) and skip label tokens matching
|
||||
`\b[FO]\d+\b`. Acceptance: `claim-check --file` over ADR-263/264/265 reports
|
||||
only the genuinely tagged-or-taggable percentage lines, and the existing 17
|
||||
guardrail tests still pass plus new false-positive pins ("source maps",
|
||||
"F1 (HIGH)" → no finding).
|
||||
|
||||
Non-goals: no new runtime dependencies (the zero-dep MCP server is a feature,
|
||||
not an accident — keep it), no build step, no change to the fail-closed tool
|
||||
contracts.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The honesty guardrail becomes fail-closed end-to-end (its current empty-input
|
||||
pass is the exact failure mode the guardrail exists to prevent).
|
||||
- `npx` cold start drops ~450 kB / 3 packages (MEASURED baseline in F3) with no
|
||||
feature loss; `doctor` output already communicates the optional-dep story.
|
||||
- Long-running `verify`/`calibrate` no longer starve the MCP channel — the
|
||||
harness survives host health checks during real calibration runs.
|
||||
- Two-copy skill drift becomes impossible at pack time.
|
||||
- Costs: async conversion touches every handler signature in `src/tools.js`
|
||||
(mechanical, ~6 handlers); alias tools add a small compatibility table.
|
||||
- Verification for the implementing PR: bundled tests extended for O1/O2/O5
|
||||
(target ≥ 20 tests), `npm pack --dry-run` file-count asserted, and the F3
|
||||
install measurement re-run and quoted MEASURED in the PR body — which must
|
||||
itself pass `npx ruview claim-check`.
|
||||
@@ -0,0 +1,170 @@
|
||||
# ADR-264: `@ruvnet/rvagent` MCP Server + `@ruv/ruview-cli` — Deep Review + Optimization Strategy
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed — review complete (findings verified against the published 0.1.0 tarball), optimizations O1–O9 not yet implemented |
|
||||
| **Date** | 2026-07-02 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **RUVIEW-NPM-REVIEW-2** |
|
||||
| **Supersedes / amends** | none (reviews the ADR-104/ADR-124 artifacts; feeds ADR-265 distribution strategy) |
|
||||
|
||||
## Context
|
||||
|
||||
Two TypeScript npm packages expose RuView sensing to agents and shells:
|
||||
|
||||
- **`@ruvnet/rvagent@0.1.0`** (`tools/ruview-mcp/`) — SENSE-BRIDGE, the MCP
|
||||
server over the sensing-server HTTP API + cog binaries: 12 tools
|
||||
(csi/pose/count/registry/train/job + ADR-124 BFLD/presence/vitals). Published
|
||||
(188 kB unpacked — MEASURED, `npm view @ruvnet/rvagent`). Deps:
|
||||
`@modelcontextprotocol/sdk` + `zod`.
|
||||
- **`@ruv/ruview-cli@0.0.1`** (`tools/ruview-cli/`) — `private: true` yargs CLI
|
||||
mirroring the same capabilities; intentionally duplicates `http.ts`/`cog.ts`/
|
||||
`config.ts` (~150 lines) to stay standalone.
|
||||
|
||||
This ADR records a deep review of both: packaging correctness (verified against
|
||||
the **published** tarball, not just the source tree), protocol/interop, resource
|
||||
lifecycle, and the honesty of the package's own self-description — the same
|
||||
MEASURED-vs-CLAIMED bar the project applies to accuracy numbers.
|
||||
|
||||
## Findings
|
||||
|
||||
### F1 (HIGH, broken export): `require` condition points at a file that does not exist
|
||||
|
||||
`package.json` `exports["."].require = "./dist/index.cjs"`, but the build is
|
||||
plain `tsc` (ESM only) and **the published 0.1.0 tarball contains no
|
||||
`index.cjs`** (verified by listing the registry tarball). Any CJS consumer doing
|
||||
`require('@ruvnet/rvagent')` resolves to a nonexistent file →
|
||||
`ERR_MODULE_NOT_FOUND`. Additionally the `types` condition is listed **after**
|
||||
`import`/`require`; TypeScript requires `types` first or it may be ignored under
|
||||
`moduleResolution: bundler/node16`.
|
||||
|
||||
### F2 (MEDIUM, tarball bloat): a third of the published package is dead source maps
|
||||
|
||||
The 0.1.0 tarball ships **44 `.map` files = 62,698 B** against 78,209 B of
|
||||
actual `.js` (MEASURED, extracted registry tarball). `src/` is not published, so
|
||||
every `sourceMappingURL` points at `../src/*.ts` that consumers do not have —
|
||||
the maps can never resolve. Also `files` lists `CHANGELOG.md`, which does not
|
||||
exist in `tools/ruview-mcp/` (npm silently skips it), so the advertised file set
|
||||
is partly fictional.
|
||||
|
||||
### F3 (MEDIUM, honesty): the package description claims a transport it does not start
|
||||
|
||||
The description reads "**dual-transport MCP server (stdio + Streamable HTTP)**",
|
||||
but `main()` in `src/index.ts` wires **stdio only**. `http-transport.ts` is a
|
||||
complete, tested scaffold that nothing imports at runtime — there is no flag,
|
||||
env var, or subcommand that starts it. By this project's own rule this is a
|
||||
CLAIMED capability presented as shipped. Either wire it (`--http` /
|
||||
`RVAGENT_HTTP_PORT` gate) or de-claim the description until it is.
|
||||
|
||||
### F4 (MEDIUM, interop + inconsistency): two tool-naming conventions, one of them dot-based
|
||||
|
||||
Six tools use `ruview_snake_case`; six (ADR-124 additions) use
|
||||
`ruview.dotted.names`. Same interop caveat as ADR-263 F9 (host tool-name
|
||||
regexes commonly `^[a-zA-Z0-9_-]{1,64}$`), plus the split convention makes the
|
||||
tool surface look like two products. Standardize on underscores and accept the
|
||||
dotted forms as aliases for one deprecation cycle.
|
||||
|
||||
### F5 (MEDIUM, double work + drift): every tool input is validated twice from two hand-maintained schemas
|
||||
|
||||
`CallToolRequestSchema` handler runs `TOOL_INPUT_SCHEMAS[name].safeParse(args)`,
|
||||
then each tool handler runs its own `schema.parse(args)` again — two full Zod
|
||||
passes per call. Separately, the `inputSchema` JSON advertised via `tools/list`
|
||||
is **hand-written** and duplicates the Zod schema field-by-field (defaults,
|
||||
min/max, descriptions) — schema drift between what is advertised and what is
|
||||
enforced is a matter of time. Parse once at the gate, pass the typed result to
|
||||
handlers, and generate the advertised JSON Schema from the Zod source
|
||||
(`zod-to-json-schema` at build time, or Zod 4's native `z.toJSONSchema` when the
|
||||
SDK's peer range allows).
|
||||
|
||||
### F6 (MEDIUM, resource lifecycle): `train_count` leaks 2 fds per job; job registry is process-local
|
||||
|
||||
`trainCount` opens `logFdOut`/`logFdErr` with `openSync` and never closes them
|
||||
in the parent — the spawned cargo child inherits duplicates, but the parent's
|
||||
descriptors stay open for the MCP server's lifetime: 2 leaked fds per training
|
||||
job. `jobRegistry` is an in-memory `Map`, so `ruview_job_status` after a server
|
||||
restart reports "not found" for a training run that is still burning GPU (the
|
||||
source comments acknowledge this; the fix — persist `~/.ruview/jobs/<id>.json`,
|
||||
already the documented layout — is small). Also `jobStatus` re-`import`s
|
||||
`node:fs` on every poll and reads the entire log to return 20 lines.
|
||||
|
||||
### F7 (MEDIUM, security/robustness of the HTTP scaffold): unbounded body + one shared session transport
|
||||
|
||||
`http-transport.ts` buffers the request body with no size cap (memory DoS the
|
||||
moment it is wired to a socket), reuses a **single**
|
||||
`StreamableHTTPServerTransport` with `sessionIdGenerator` for all clients (the
|
||||
SDK's stateful mode expects one transport per session — a second client's
|
||||
`initialize` collides), and the Origin allowlist is exact-match
|
||||
(`http://localhost` will not match a real browser origin `http://localhost:5173`).
|
||||
Must be fixed **before** F3 wires it in; bearer-token + 127.0.0.1 defaults are
|
||||
already right.
|
||||
|
||||
### F8 (LOW, dead/misleading code): `detectCogBinary` always returns the bare name
|
||||
|
||||
It builds a 4-candidate appliance-path array and then returns
|
||||
`candidates[candidates.length - 1]` — i.e. always `name` — without checking
|
||||
existence. The candidates are dead weight that reads as if path detection
|
||||
happens. Either probe with `existsSync` or delete the array.
|
||||
|
||||
### F9 (LOW, drift + hygiene): hardcoded versions, unused/mismatched devDeps, bin-name collision
|
||||
|
||||
`PACKAGE_VERSION = "0.1.0"` (index.ts) duplicates package.json;
|
||||
`@types/express` is unused (`http-transport` uses `node:http`); `@types/jest@30`
|
||||
against `jest@29`; `ruview-cli` hardcodes `.version("0.0.1")`. And
|
||||
`@ruv/ruview-cli` claims the **`ruview`** bin name, which collides with
|
||||
`@ruvnet/ruview`'s bin (ADR-182) if both are ever installed globally —
|
||||
ADR-263/265 give the `ruview` name to the harness; the CLI must rename or fold.
|
||||
|
||||
## Decision
|
||||
|
||||
- **O1 (F1):** fix `exports`: drop the `require` condition (ESM-only is fine for
|
||||
a bin-first package) or add a real CJS build; put `types` first. Add a CI
|
||||
smoke test that does `npm pack` + `node -e "import('<tarball install>')"`.
|
||||
- **O2 (F2):** publish without maps: `declarationMap: false`, `sourceMap: false`
|
||||
in a `tsconfig.build.json` used by `prepack` (or add `!dist/**/*.map` to
|
||||
`files`). Remove the phantom `CHANGELOG.md` entry or create the file.
|
||||
Acceptance: unpacked size ≤ ~125 kB (from 188 kB, −62.7 kB MEASURED map
|
||||
weight).
|
||||
- **O3 (F3, F7):** wire the HTTP transport behind an explicit opt-in
|
||||
(`RVAGENT_HTTP_PORT` or `--http`), after F7 fixes: per-session transport map
|
||||
keyed by `mcp-session-id`, 1 MiB body cap, origin matching that honors ports
|
||||
(compare `URL.origin` prefixes or document exact origins). Until then, change
|
||||
the description to "stdio MCP server (Streamable HTTP scaffold, unwired)".
|
||||
- **O4 (F4):** rename dotted tools to underscore (`ruview_bfld_last_scan`, …),
|
||||
keep dotted aliases in the call router for one release, note it in the README.
|
||||
- **O5 (F5):** single validation gate: the registry maps name → Zod schema →
|
||||
typed handler; advertised `inputSchema` generated from Zod at build time.
|
||||
- **O6 (F6):** close parent fds after spawn (`closeSync` post-`spawn` — the
|
||||
child holds its own copies), persist job records to
|
||||
`<jobsDir>/<id>.json`, and read log tails with a bounded read.
|
||||
- **O7 (F8):** make `detectCogBinary` actually probe (`existsSync` over the
|
||||
candidates) — it is the entire reason the function exists.
|
||||
- **O8 (F9):** single-source versions from package.json; drop `@types/express`;
|
||||
align `@types/jest` with jest 29 (or move to `node:test` like the harness and
|
||||
drop the jest toolchain entirely — it is the heaviest devDep in both
|
||||
packages).
|
||||
- **O9 (F9, scope):** fold `@ruv/ruview-cli` into `rvagent` as a second bin
|
||||
(`rvagent-cli`) sharing `http/cog/config`, or keep it private-forever and say
|
||||
so in its README. Its `ruview` bin name is surrendered to `@ruvnet/ruview`
|
||||
either way.
|
||||
|
||||
## Consequences
|
||||
|
||||
- CJS consumers stop hitting a guaranteed-broken export path (F1 is the only
|
||||
finding that fails for every consumer of that entry point deterministically).
|
||||
- The published artifact shrinks ~33% (MEASURED, F2 tarball listing: 62,698 B
|
||||
of maps in a 188 kB unpacked payload) and stops advertising files/transports
|
||||
it does not contain — the package description itself passes the project's
|
||||
claim-check bar.
|
||||
- One schema source ends advertised-vs-enforced drift and halves per-call
|
||||
validation cost; naming unification makes the 12-tool surface read as one
|
||||
product and survive strict host tool-name validation.
|
||||
- Long-lived MCP servers stop accumulating fds during training campaigns, and
|
||||
job polling survives restarts.
|
||||
- Costs: the alias cycle (O4) briefly doubles the advertised tool count unless
|
||||
aliases are router-only (recommended: router-only, advertise underscore names
|
||||
exclusively); folding the CLI (O9) retires a package name already in use in
|
||||
scripts, so it needs a deprecation note.
|
||||
- Verification for the implementing PR: `npm pack --dry-run` asserted file list
|
||||
(no `.map`, no phantom entries), pack-size budget in CI (ADR-265), jest/`node
|
||||
--test` suite green, and a tarball-install smoke test for both `import` and
|
||||
the `rvagent` bin.
|
||||
@@ -0,0 +1,123 @@
|
||||
# ADR-265: RuView npm Distribution Strategy — CI Gate, Provenance, Version Single-Sourcing, Namespace
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| **Status** | Proposed |
|
||||
| **Date** | 2026-07-02 |
|
||||
| **Deciders** | ruv |
|
||||
| **Codename** | **RUVIEW-NPM-DIST** |
|
||||
| **Supersedes / amends** | none (cross-cutting layer above ADR-263 and ADR-264; complements ADR-182 P3/P4) |
|
||||
|
||||
## Context
|
||||
|
||||
The monorepo now ships (or stages) **three Node packages** with no shared
|
||||
distribution engineering:
|
||||
|
||||
| Package | Dir | Published | Bin(s) | Tests in CI |
|
||||
|---------|-----|-----------|--------|-------------|
|
||||
| `@ruvnet/ruview` | `harness/ruview/` | 0.1.0 (live) | `ruview` | **none** |
|
||||
| `@ruvnet/rvagent` | `tools/ruview-mcp/` | 0.1.0 (live) | `rvagent`, `ruview-mcp` | **none** |
|
||||
| `@ruv/ruview-cli` | `tools/ruview-cli/` | private | `ruview` (collides) | **none** |
|
||||
|
||||
Cross-cutting facts established during the ADR-263/264 reviews:
|
||||
|
||||
- **Zero CI coverage.** No workflow under `.github/workflows/` references any of
|
||||
the three directories. Two of the packages are *live on the registry* and were
|
||||
published from a laptop state CI never saw. Meanwhile the Rust side has a
|
||||
1,031+-test gate and a witness-bundle culture (ADR-028) — the npm surface is
|
||||
the only shipped artifact class with no verification gate at all.
|
||||
- **`ci.yml` pins `NODE_VERSION: '18'`** while all three packages declare
|
||||
`engines.node >= 20`.
|
||||
- **Version triplication.** Each package hardcodes its version in source at
|
||||
least once beyond package.json (harness `SERVER_INFO`, rvagent
|
||||
`PACKAGE_VERSION`, cli `.version("0.0.1")`).
|
||||
- **Bin-name collision.** Two packages claim the `ruview` bin.
|
||||
- **No provenance.** Neither published package carries npm provenance
|
||||
attestations, in a project whose differentiator is signed, reproducible
|
||||
evidence (ADR-028 witness bundles, ADR-182 P4 ed25519/SLSA design).
|
||||
- **No pack-content gate.** ADR-264 F1/F2 (broken `require` target, 33% dead
|
||||
map weight — MEASURED, ADR-264 F2 tarball listing — and a phantom
|
||||
`CHANGELOG.md` in `files`) are exactly the defect class an
|
||||
`npm pack --dry-run` assertion catches in seconds.
|
||||
|
||||
## Decision
|
||||
|
||||
Adopt one distribution layer for all Node packages. Per-package code fixes live
|
||||
in ADR-263/264; this ADR fixes the machinery around them.
|
||||
|
||||
### D1 — One `npm-packages.yml` CI workflow (the gate)
|
||||
|
||||
Matrix over `[harness/ruview, tools/ruview-mcp, tools/ruview-cli]` ×
|
||||
Node `[20, 22]`:
|
||||
|
||||
1. `npm ci` (lockfiles are already committed for the TS packages; add one for
|
||||
the harness — it has no prod deps after ADR-263 O3, so the lockfile is tiny).
|
||||
2. `npm test` (harness: `node --test test/*.test.mjs` — pin the glob form,
|
||||
the directory form fails on Node 22; TS packages: build + jest or `node:test`
|
||||
per ADR-264 O8).
|
||||
3. **Pack gate:** `npm pack --dry-run --json` asserted against a checked-in
|
||||
expected file list + a max unpacked-size budget per package (harness ≤ 60 kB;
|
||||
rvagent ≤ 130 kB post ADR-264 O2). Any new/missing/renamed shipped file is a
|
||||
reviewed diff, not a surprise.
|
||||
4. **Tarball smoke test:** install the packed tarball into a temp dir; run
|
||||
`ruview --version`, `ruview doctor`, `rvagent` `--help`-equivalent, and a
|
||||
Node `import()` of each declared export condition — this is the test that
|
||||
would have caught ADR-264 F1 (`require` → nonexistent `dist/index.cjs`).
|
||||
5. Bump `ci.yml` `NODE_VERSION` to `'20'` (independent of the matrix above).
|
||||
|
||||
### D2 — Publish only from CI, with provenance
|
||||
|
||||
Manual `npm publish` from laptops stops. A tag-triggered workflow
|
||||
(`ruview-npm-release.yml`, mirroring the firmware release discipline) runs the
|
||||
D1 gate, then `npm publish --provenance --access public` under the GitHub OIDC
|
||||
token. Consequence: every published version is attested to a public commit +
|
||||
workflow run — the npm-side analogue of the ADR-028 witness bundle. The
|
||||
`prepublishOnly` script in each package runs the pack gate locally as a
|
||||
belt-and-braces (publishing outside CI fails loudly, not silently).
|
||||
|
||||
### D3 — Version single-sourcing
|
||||
|
||||
Rule: **package.json is the only place a version string lives.** Runtime code
|
||||
reads it (`createRequire(import.meta.url)('./package.json').version` or a
|
||||
build-time define for the TS packages). CI greps for `\d+\.\d+\.\d+` literals in
|
||||
`src/` of each package and fails on match (allowlist: test fixtures). This
|
||||
retires ADR-263 F6 and ADR-264 F9 permanently instead of per-incident.
|
||||
|
||||
### D4 — Namespace and bin ownership
|
||||
|
||||
- `@ruvnet/ruview` **owns the `ruview` bin** (it is the published front door,
|
||||
ADR-182). `@ruv/ruview-cli` renames its bin or folds into `rvagent`
|
||||
(ADR-264 O9) — decided here so neither package ADR relitigates it.
|
||||
- New Node packages in this repo use the `@ruvnet/` scope (the `@ruv/` scope
|
||||
holds `rvcsi` legacies; do not grow it).
|
||||
- Every package README + description must pass
|
||||
`npx ruview claim-check` — enforced in the D1 gate. The guardrail package
|
||||
linting its sibling packages' claims is the cheapest dogfooding we have
|
||||
(ADR-264 F3 is the standing example of why).
|
||||
|
||||
### D5 — Shared-code policy (bounded)
|
||||
|
||||
Do **not** introduce an npm workspace or a shared runtime package yet: three
|
||||
packages, two of which may merge (ADR-264 O9), do not justify workspace
|
||||
machinery, and the harness's zero-dep property is load-bearing. Revisit if a
|
||||
fourth package appears or if the `http/cog/config` duplication survives the
|
||||
ADR-264 O9 fold. Record the duplication as intentional in each file header (the
|
||||
CLI already does this).
|
||||
|
||||
## Consequences
|
||||
|
||||
- The npm artifacts get the same class of gate the Rust workspace has had since
|
||||
ADR-028: no publish without tests, no shipped file set without an asserted
|
||||
manifest, no version without provenance. The two defects that reached the
|
||||
registry (broken `require` condition, dead maps) become CI-impossible.
|
||||
- Cold-path costs stay near zero: the D1 matrix is 6 fast jobs (the harness
|
||||
suite runs in ~108 ms MEASURED; TS builds dominate at a few tens of seconds).
|
||||
- Publishing gains one constraint (must go through CI) and loses one failure
|
||||
mode (laptop-state publishes) — the right trade for a project whose brand is
|
||||
reproducible evidence.
|
||||
- D3's grep gate is blunt but cheap; if it over-fires, scope it to
|
||||
`version`-adjacent identifiers before weakening it.
|
||||
- Follow-ups tracked elsewhere: per-package code fixes (ADR-263 O1–O8, ADR-264
|
||||
O1–O9); ADR-182 P4 (metaharness router + ed25519 provenance chain) remains
|
||||
the deeper provenance story that D2's npm attestations complement, not
|
||||
replace.
|
||||
+4
-1
@@ -1,6 +1,6 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
This folder contains 45 Architecture Decision Records (ADRs) that document every significant technical choice in the RuView / WiFi-DensePose project.
|
||||
This folder contains 182 Architecture Decision Records (ADRs) that document every significant technical choice in the RuView / WiFi-DensePose project. (The index tables below list a curated subset per domain; see the directory listing for the full set.)
|
||||
|
||||
## Why ADRs?
|
||||
|
||||
@@ -120,6 +120,9 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
|
||||
| [ADR-097](ADR-097-adopt-rvcsi-as-ruview-csi-runtime.md) | Adopt rvCSI as RuView's primary CSI runtime (phased adoption) | Proposed |
|
||||
| [ADR-098](ADR-098-evaluate-midstream-fit.md) | Evaluate `ruvnet/midstream` for RuView's CSI / WebSocket / mesh pipeline | Rejected |
|
||||
| [ADR-099](ADR-099-midstream-introspection-tap.md) | Adopt midstream as RuView's real-time introspection + low-latency tap | Proposed |
|
||||
| [ADR-263](ADR-263-ruview-npm-harness-deep-review.md) | `@ruvnet/ruview` npm harness — deep review + optimization strategy | Proposed |
|
||||
| [ADR-264](ADR-264-rvagent-mcp-and-cli-npm-deep-review.md) | `@ruvnet/rvagent` MCP server + `@ruv/ruview-cli` — deep review + optimization strategy | Proposed |
|
||||
| [ADR-265](ADR-265-ruview-npm-distribution-strategy.md) | RuView npm distribution strategy — CI gate, provenance, version single-sourcing, namespace | Proposed |
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user