docs(adr): deep review of the RuView npm surface — ADR-263/264/265 optimization strategies (#1229)

* 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

* fix(npm): implement ADR-263/264/265 — harness fail-closed + async MCP, rvagent packaging/transport/naming, npm CI+provenance gate

ADR-263 (@ruvnet/ruview 0.2.0), O1-O9:
- claim-check fails closed on empty input (CLI exit 2, empty_text tool error)
- MCP stdio server dispatches tools/call asynchronously (promise-based spawn);
  ping answers while a 3s fake verify runs — pinned by new e2e test
- optionalDependencies dropped: cold npx installs exactly 1 package
  (MEASURED: was 4 pkgs/620kB/71 files via npm i in a clean prefix)
- bounded rolling output tails replace spawnSync 1MiB maxBuffer
- node_monitor port passed via sys.argv, never spliced into python -c source
- serverInfo.version read from package.json; resources/prompts stubs
- skills single-sourced: prepack sync script generates .claude/skills/ copies
- which() = memoized dep-free PATH scan
- tools underscore-canonical (ruview_claim_check, ...) + dotted aliases
- guardrail precision: word-boundary map/f1/auc/iou, code-span + F1/O2 label
  scrubbing, quantitative-claims-only; packaging reproducer hints
- 30/30 tests (was 17), incl. concurrency e2e + fail-open regression pins

ADR-264 (@ruvnet/rvagent 0.2.0), O1-O9:
- exports fixed: types-first, phantom dist/index.cjs require target removed
- tarball map-free: 127,704B unpacked / 46 files / 0 maps (MEASURED,
  npm pack --dry-run; was 188kB incl. 44 maps referencing unshipped src)
- Streamable HTTP actually wired behind RVAGENT_HTTP_PORT: one transport +
  one MCP server per session (mcp-session-id routing), 1MiB body cap (413),
  port-aware localhost origin gate; dual-transport description now true
- tools renamed underscore-canonical with dotted router-only aliases
- single Zod validation gate; advertised inputSchema generated from the same
  Zod source (zod-to-json-schema)
- train_count: parent log fds closed (was leaking 2/job); job records
  persisted to <jobsDir>/<id>.json (job_status survives restarts); bounded
  log-tail reads
- detectCogBinary probes its candidates instead of dead-coding them
- version from package.json; @types/express dropped; @types/jest -> 29
- README rewritten to match reality (no phantom subcommands/policy layer)
- 99/99 jest tests (incl. new session/body-cap suite + previously-broken
  manifest suite); stdio handshake + HTTP session flow smoke-tested live

ADR-265 D1-D4:
- .github/workflows/npm-packages.yml: 3-package x Node 20/22 gate — tests,
  version-literal grep (D3), pack-content/size gate, tarball-install smoke
  test (catches the ADR-264 F1 class), README claim-check (D4)
- .github/workflows/ruview-npm-release.yml: publish from CI only with
  npm publish --provenance
- @ruv/ruview-cli bin renamed ruview-cli (ruview bin belongs to
  @ruvnet/ruview); version single-sourced
- ci.yml NODE_VERSION 18 -> 20

ADR statuses updated to Accepted/implemented; harness manifest re-pinned;
ADR-263/264/265 + both package READMEs pass claim-check.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz

* perf(rvagent): lazy-load HTTP transport + memoize generated tool schemas

stdio time-to-first-response ~242ms -> ~189ms (-22%; MEASURED, median of
repeated initialize round-trips against dist/index.js in this container).

- ./http-transport.js now imported lazily inside the RVAGENT_HTTP_PORT
  branch: it chain-loads the MCP SDK streamableHttp module (~48ms MEASURED
  via per-module import() timing) which the default stdio path never uses
- toolInputJsonSchema memoized per tool: schemas are static for the process
  lifetime; under the session-per-server HTTP model every session calls
  tools/list, so stop re-walking the Zod tree each time

No behavior change: 99/99 jest tests; HTTP session flow re-smoke-tested
through the lazy import path (initialize -> 200 + mcp-session-id).

Profiled @ruvnet/ruview too and left it alone: 50ms CLI startup vs ~29ms
bare 'node -e ""' floor on the same box (MEASURED) — already near the
interpreter floor with zero dependencies.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WrGfTGKv1oWZ6iwXZACULz

* ci(ruview-cli): pass jest --passWithNoTests so the private no-test package doesn't fail the npm-packages matrix

Co-Authored-By: claude-flow <ruv@ruv.net>

* fix(npm): address 10 verified review findings in harness + rvagent before 0.2.0 publish

harness/ruview (@ruvnet/ruview):
- guardrails: digit gate now sees numbers inside code spans; F1-style
  metric tokens followed by ':' or a nearby number are no longer scrubbed
  (fail-open regressions in the honesty gate)
- mcp-server: tools/call requests serialize through a FIFO promise chain
  (hardware/mutating tools never overlap) while ping/tools/list stay
  immediate; stdin close drains in-flight responses before exit
- tools: which() no longer memoizes negative lookups

tools/ruview-mcp (@ruvnet/rvagent):
- index: realpath invoked-directly guard — library import no longer
  connects a stdio transport to the consumer's process
- http-transport: explicit allowedOrigins is exact-match only (localhost
  any-port convenience applies only with no configured allowlist);
  session map gains maxSessions=64 + 5min idle TTL sweep
- train-count: job records persist the child pid and reconcile stale
  'running' status after a server restart (exit-code marker or dead pid)
- config: cog binary candidates ordered by process.arch

.github/workflows/ruview-npm-release.yml: port the full ADR-265 D1 gate
(version-literal check, unpacked-size budget, tarball-install smoke test)
from npm-packages.yml so the publish path enforces what the header claims.

Tests: harness 30→36, rvagent 99→112, all passing.

Co-Authored-By: claude-flow <ruv@ruv.net>

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
rUv
2026-07-02 13:11:15 -04:00
committed by GitHub
parent 1a0492992f
commit e6f26e9ac9
48 changed files with 2457 additions and 981 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ on:
env:
PYTHON_VERSION: '3.11'
NODE_VERSION: '18'
NODE_VERSION: '20' # ADR-265: all Node packages in this repo declare engines >= 20
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
+148
View File
@@ -0,0 +1,148 @@
# ADR-265 D1 — the npm-package gate.
#
# Every Node package in this repo (published or private) gets: install, build,
# tests, a version-literal gate (D3 — package.json is the only place a version
# lives), a pack-content gate (no source maps, unpacked-size budget), a
# tarball-install smoke test (would have caught ADR-264 F1's broken `require`
# export), and the claim-check honesty lint on the README (D4).
name: npm packages
on:
push:
branches: [main]
paths:
- 'harness/ruview/**'
- 'tools/ruview-mcp/**'
- 'tools/ruview-cli/**'
- '.github/workflows/npm-packages.yml'
pull_request:
paths:
- 'harness/ruview/**'
- 'tools/ruview-mcp/**'
- 'tools/ruview-cli/**'
- '.github/workflows/npm-packages.yml'
permissions:
contents: read
jobs:
gate:
name: ${{ matrix.package.dir }} (node ${{ matrix.node }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: ['20', '22']
package:
- dir: harness/ruview
build: false
publishable: true
# ADR-263: dependency-free harness; budget guards against dep creep.
unpacked_budget: 65536
- dir: tools/ruview-mcp
build: true
publishable: true
# ADR-264 O2: map-free tarball (was 188 kB with maps).
unpacked_budget: 140000
- dir: tools/ruview-cli
build: true
publishable: false
unpacked_budget: 0
defaults:
run:
working-directory: ${{ matrix.package.dir }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
# Repo policy gitignores lockfiles under harness/ (the harness is
# dependency-free anyway); the TS packages commit theirs.
- name: Install
run: |
if [ -f package-lock.json ]; then npm ci; else npm install --no-fund --no-audit; fi
- name: Build
if: ${{ matrix.package.build }}
run: npm run build
- name: Test
run: npm test --if-present
# ADR-265 D3 — package.json is the only place a version string lives.
- name: Version-literal gate
run: |
set -euo pipefail
hits=""
for d in src bin; do
if [ -d "$d" ]; then
hits+=$(grep -rEn '\b[0-9]+\.[0-9]+\.[0-9]+\b' "$d" | grep -vE '127\.0\.0\.1|0\.0\.0\.0' || true)
fi
done
if [ -n "$hits" ]; then
echo "Hardcoded version-like literals found (read package.json instead — ADR-265 D3):"
echo "$hits"
exit 1
fi
# ADR-265 D1.3 — pack-content gate: no maps, size budget enforced.
- name: Pack gate
if: ${{ matrix.package.publishable }}
run: |
npm pack --dry-run --json 2>/dev/null | node -e "
const [info] = JSON.parse(require('fs').readFileSync(0, 'utf8'));
const budget = Number(process.env.UNPACKED_BUDGET);
const maps = info.files.filter((f) => f.path.endsWith('.map'));
if (maps.length > 0) {
console.error('Tarball contains source maps (ADR-264 F2):', maps.map((m) => m.path));
process.exit(1);
}
if (info.unpackedSize > budget) {
console.error(\`Unpacked size \${info.unpackedSize} B exceeds budget \${budget} B\`);
process.exit(1);
}
console.log(\`pack gate OK: \${info.files.length} files, \${info.unpackedSize} B unpacked (budget \${budget} B), 0 maps\`);
"
env:
UNPACKED_BUDGET: ${{ matrix.package.unpacked_budget }}
# ADR-265 D1.4 — install the real tarball and drive each bin/export.
- name: Tarball smoke test
if: ${{ matrix.package.publishable }}
run: |
set -euo pipefail
TGZ="$PWD/$(npm pack --silent 2>/dev/null | tail -1)"
SMOKE="$(mktemp -d)"
cd "$SMOKE"
npm init -y > /dev/null
npm i --no-fund --no-audit "$TGZ"
case "${{ matrix.package.dir }}" in
harness/ruview)
./node_modules/.bin/ruview --version
./node_modules/.bin/ruview doctor
# the honesty gate must fail closed on empty input (ADR-263 F1)
if ./node_modules/.bin/ruview claim-check; then
echo 'claim-check passed with no input — fail-open regression'; exit 1
fi
node --input-type=module -e "const m = await import('@ruvnet/ruview'); if (!m.TOOLS) process.exit(1);"
;;
tools/ruview-mcp)
# initialize over stdio; server must answer and exit 0 on EOF
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}\n' \
| timeout 30 ./node_modules/.bin/rvagent | grep -q '"serverInfo"'
# the ESM export must resolve from the installed tarball (ADR-264 F1)
timeout 30 node --input-type=module -e "await import('@ruvnet/rvagent');" < /dev/null
;;
esac
# ADR-265 D4 — package READMEs must pass the project's own honesty lint.
- name: Claim-check README
run: |
if [ -f README.md ]; then
node "$GITHUB_WORKSPACE/harness/ruview/bin/cli.js" claim-check --file README.md
else
echo "no README.md — skipping"
fi
+137
View File
@@ -0,0 +1,137 @@
# ADR-265 D2 — publish only from CI, with provenance.
#
# Manual `npm publish` from laptops stops: this workflow re-runs the ADR-265 D1
# gate for the selected package and then publishes with npm provenance
# attestations (OIDC), tying every published version to a public commit +
# workflow run — the npm-side analogue of the ADR-028 witness bundle.
#
# Requires: NPM_TOKEN repo secret (an npm automation token), or npm Trusted
# Publishing configured for the package (in which case the token is unused).
name: ruview npm release
on:
workflow_dispatch:
inputs:
package:
description: 'Package directory to publish'
required: true
type: choice
options:
- harness/ruview
- tools/ruview-mcp
dist_tag:
description: 'npm dist-tag'
required: false
default: 'latest'
type: string
permissions:
contents: read
id-token: write # npm --provenance
jobs:
publish:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ${{ inputs.package }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install
run: |
if [ -f package-lock.json ]; then npm ci; else npm install --no-fund --no-audit; fi
- name: Build (if present)
run: npm run build --if-present
- name: Test
run: npm test --if-present
# ADR-265 D3 — package.json is the only place a version string lives.
- name: Version-literal gate
run: |
set -euo pipefail
hits=""
for d in src bin; do
if [ -d "$d" ]; then
hits+=$(grep -rEn '\b[0-9]+\.[0-9]+\.[0-9]+\b' "$d" | grep -vE '127\.0\.0\.1|0\.0\.0\.0' || true)
fi
done
if [ -n "$hits" ]; then
echo "Hardcoded version-like literals found (read package.json instead — ADR-265 D3):"
echo "$hits"
exit 1
fi
# ADR-265 D1.3 — pack-content gate: no maps AND the per-package
# unpacked-size budget (the budgets that npm-packages.yml enforces).
- name: Pack gate (no maps + size budget)
run: |
set -euo pipefail
case "${{ inputs.package }}" in
# ADR-263: dependency-free harness; budget guards against dep creep.
harness/ruview) export UNPACKED_BUDGET=65536 ;;
# ADR-264 O2: map-free tarball (was 188 kB with maps).
tools/ruview-mcp) export UNPACKED_BUDGET=140000 ;;
*) echo "Unknown package '${{ inputs.package }}' — no budget defined"; exit 1 ;;
esac
npm pack --dry-run --json 2>/dev/null | node -e "
const [info] = JSON.parse(require('fs').readFileSync(0, 'utf8'));
const budget = Number(process.env.UNPACKED_BUDGET);
const maps = info.files.filter((f) => f.path.endsWith('.map'));
if (maps.length > 0) {
console.error('Tarball contains source maps (ADR-264 F2):', maps.map((m) => m.path));
process.exit(1);
}
if (info.unpackedSize > budget) {
console.error(\`Unpacked size \${info.unpackedSize} B exceeds budget \${budget} B\`);
process.exit(1);
}
console.log(\`pack gate OK: \${info.files.length} files, \${info.unpackedSize} B unpacked (budget \${budget} B), 0 maps\`);
"
# ADR-265 D1.4 — install the real tarball and drive each bin/export.
- name: Tarball smoke test
run: |
set -euo pipefail
TGZ="$PWD/$(npm pack --silent 2>/dev/null | tail -1)"
SMOKE="$(mktemp -d)"
cd "$SMOKE"
npm init -y > /dev/null
npm i --no-fund --no-audit "$TGZ"
case "${{ inputs.package }}" in
harness/ruview)
./node_modules/.bin/ruview --version
./node_modules/.bin/ruview doctor
# the honesty gate must fail closed on empty input (ADR-263 F1)
if ./node_modules/.bin/ruview claim-check; then
echo 'claim-check passed with no input — fail-open regression'; exit 1
fi
node --input-type=module -e "const m = await import('@ruvnet/ruview'); if (!m.TOOLS) process.exit(1);"
;;
tools/ruview-mcp)
# initialize over stdio; server must answer and exit 0 on EOF
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"ci","version":"0"}}}\n' \
| timeout 30 ./node_modules/.bin/rvagent | grep -q '"serverInfo"'
# the ESM export must resolve from the installed tarball (ADR-264 F1)
timeout 30 node --input-type=module -e "await import('@ruvnet/rvagent');" < /dev/null
;;
esac
- name: Claim-check README
run: |
if [ -f README.md ]; then
node "$GITHUB_WORKSPACE/harness/ruview/bin/cli.js" claim-check --file README.md
fi
- name: Publish (with provenance)
run: npm publish --provenance --access public --tag "${{ inputs.dist_tag }}"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}