diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c7343f..abf4cf7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,8 +42,5 @@ jobs: - name: Run tests run: bun test - - name: Public surface audit (README discovery) - run: bun test test/readmeDiscovery.test.ts - - name: Release gate - run: bun run benchmark:release-gate \ No newline at end of file + run: bun run benchmark:release-gate diff --git a/crates/image-reader-mcp-server/src/lib.rs b/crates/image-reader-mcp-server/src/lib.rs index e57756d..932a606 100644 --- a/crates/image-reader-mcp-server/src/lib.rs +++ b/crates/image-reader-mcp-server/src/lib.rs @@ -66,25 +66,10 @@ impl ServerHandler for ImageReaderMcp { #[cfg(test)] mod tests { use super::ImageReaderMcp; - use std::fs; - use std::path::PathBuf; - - #[test] - fn rmcp_server_sources_route_read_image_through_rust_core() { - let src_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); - let lib_rs = fs::read_to_string(src_dir.join("lib.rs")).expect("read lib.rs"); - let production_lib = lib_rs.split("#[cfg(test)]").next().unwrap_or(&lib_rs); - assert!(production_lib.contains("read_image::read_image")); - - let routes = fs::read_to_string(src_dir.join("tool_routes.rs")).expect("read tool_routes"); - assert!(routes.contains("read_image")); - assert!(routes.contains("RustCore")); - } - #[test] fn exposes_read_image_tool_surface() { let tools = ImageReaderMcp::new().tool_router.list_all(); let names: Vec<_> = tools.iter().map(|tool| tool.name.to_string()).collect(); assert!(names.contains(&"read_image".to_string())); } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 3f74ebd..dae8047 100644 --- a/package.json +++ b/package.json @@ -51,9 +51,7 @@ "format": "biome format --write .", "check": "biome check .", "check:fix": "biome check --write .", - "check:ts-adapter-deleted": "bash scripts/check-ts-adapter-deletion-ready.sh", - "check:no-ts-stdio-mcp": "bash scripts/check-no-ts-stdio-mcp.sh", - "validate": "bun run check && bun run typecheck && bun run test && bun run check:ts-adapter-deleted && bun run check:no-ts-stdio-mcp", + "validate": "bun run check && bun run typecheck && bun run test", "doctor": "bun run src/doctor-cli.ts", "benchmark:release-gate": "bun scripts/release-gate.ts", "sync:server-json": "bun scripts/sync-server-json.ts", @@ -61,7 +59,7 @@ "start": "./bin/image-reader-mcp", "clean": "rm -rf dist coverage", "prepublishOnly": "bun run clean && bun run build && bun run build:rust", - "release": "bun run typecheck && bun run check && bun run build && bun test && bun run check:ts-adapter-deleted && changeset publish" + "release": "bun run typecheck && bun run check && bun run build && bun test && changeset publish" }, "dependencies": { "@sylphx/reader-evidence": "github:SylphxAI/reader-evidence", diff --git a/scripts/check-no-ts-stdio-mcp.sh b/scripts/check-no-ts-stdio-mcp.sh deleted file mode 100755 index 28993f4..0000000 --- a/scripts/check-no-ts-stdio-mcp.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash -# S3 gate: default MCP stdio transport must delegate solely to Rust rmcp. -# TS stdio adapter is retired (transport/stdio-ts-adapter → ts_deleted). -set -euo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BIN="${ROOT}/bin/image-reader-mcp" -RUST_MAIN="${ROOT}/crates/image-reader-mcp-server/src/main.rs" -TS_ADAPTER_GATE="${ROOT}/scripts/check-ts-adapter-deletion-ready.sh" -LEDGER="${ROOT}/docs/specs/migration-ledger.json" -MATRIX="${ROOT}/test/tsAdapterDeletion.matrix.test.ts" - -violations=0 - -report_violation() { - echo "VIOLATION: $*" - violations=$((violations + 1)) -} - -echo "=== check-no-ts-stdio-mcp $(date -u +%Y-%m-%dT%H:%M:%SZ) ===" - -[[ -f "${BIN}" ]] || report_violation "missing bin/image-reader-mcp" -[[ -f "${TS_ADAPTER_GATE}" ]] || report_violation "missing scripts/check-ts-adapter-deletion-ready.sh" -[[ -f "${LEDGER}" ]] || report_violation "missing docs/specs/migration-ledger.json" -[[ -f "${RUST_MAIN}" ]] || report_violation "missing crates/image-reader-mcp-server/src/main.rs" -[[ -f "${MATRIX}" ]] || report_violation "missing test/tsAdapterDeletion.matrix.test.ts" - -if [[ -f "${ROOT}/src/index.ts" ]]; then - report_violation "src/index.ts must be deleted (transport/stdio-ts-adapter ts_deleted)" -fi - -if [[ -f "${ROOT}/dist/index.js" ]]; then - report_violation "dist/index.js must be deleted (transport/stdio-ts-adapter ts_deleted)" -fi - -if [[ -f "${LEDGER}" ]]; then - node - "${LEDGER}" <<'NODE' -const [ledgerPath] = process.argv.slice(2); -const ledger = JSON.parse(require("node:fs").readFileSync(ledgerPath, "utf8")); -const stdioRust = ledger.capabilities.find((cap) => cap.id === "transport/stdio-rust-rmcp"); -const tsAdapter = ledger.capabilities.find((cap) => cap.id === "transport/stdio-ts-adapter"); -const http = ledger.capabilities.find((cap) => cap.id === "transport/web-mcp-http"); -if (!stdioRust) { - console.error("[check-no-ts-stdio-mcp] missing capability transport/stdio-rust-rmcp"); - process.exit(1); -} -if (!tsAdapter) { - console.error("[check-no-ts-stdio-mcp] missing capability transport/stdio-ts-adapter"); - process.exit(1); -} -if (!http) { - console.error("[check-no-ts-stdio-mcp] missing capability transport/web-mcp-http"); - process.exit(1); -} -const rustAuthorityStates = new Set(["rust_impl", "authority_rust", "ts_deleted"]); -if (!rustAuthorityStates.has(stdioRust.state)) { - console.error( - `[check-no-ts-stdio-mcp] transport/stdio-rust-rmcp is ${stdioRust.state}; expected rust_impl, authority_rust, or ts_deleted` - ); - process.exit(1); -} -if (tsAdapter.state !== "ts_deleted") { - console.error( - `[check-no-ts-stdio-mcp] transport/stdio-ts-adapter is ${tsAdapter.state}; expected ts_deleted` - ); - process.exit(1); -} -if (!["rust_impl", "ts_deleted", "authority_rust"].includes(http.state)) { - console.error( - `[check-no-ts-stdio-mcp] transport/web-mcp-http is ${http.state}; expected rust_impl, authority_rust, or ts_deleted` - ); - process.exit(1); -} -NODE -fi - -if [[ -f "${BIN}" ]]; then - grep -q 'resolve_rust_bin' "${BIN}" || report_violation "bin/image-reader-mcp must resolve Rust rmcp server via resolve_rust_bin" - grep -q 'printf.*stdio' "${BIN}" || report_violation "bin/image-reader-mcp must default transport to stdio" - if grep -qE 'use_ts_transport|exec node|IMAGE_READER_MCP_TRANSPORT:-}" == "ts"' "${BIN}"; then - report_violation "bin/image-reader-mcp must not launch node or retain TS stdio opt-in" - fi -fi - -if [[ -f "${RUST_MAIN}" ]]; then - grep -q 'transport::stdio' "${RUST_MAIN}" || report_violation "Rust MCP server must expose rmcp stdio transport" -fi - -if [[ "${violations}" -gt 0 ]]; then - echo "" - echo "FAIL: ${violations} MCP stdio TS authority violation(s)." - echo "Authority: crates/image-reader-mcp-server/src/main.rs via bin/image-reader-mcp (default stdio)." - exit 1 -fi - -echo "PASS: MCP stdio transport delegates solely to Rust rmcp." diff --git a/scripts/check-ts-adapter-deletion-ready.sh b/scripts/check-ts-adapter-deletion-ready.sh deleted file mode 100755 index 914b6b2..0000000 --- a/scripts/check-ts-adapter-deletion-ready.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# Post-deletion gate for transport/stdio-ts-adapter. -# Fails if TS stdio adapter files or opt-in routing remain after ts_deleted. -set -euo pipefail - -ROOT="$(cd "$(dirname "$0")/.." && pwd)" -LEDGER="$ROOT/docs/specs/migration-ledger.json" -BIN="$ROOT/bin/image-reader-mcp" - -require_ledger_state() { - local capability="$1" - local expected="$2" - node - "$LEDGER" "$capability" "$expected" <<'NODE' -const [ledgerPath, capability, expected] = process.argv.slice(2); -const ledger = JSON.parse(require("node:fs").readFileSync(ledgerPath, "utf8")); -const entry = ledger.capabilities.find((cap) => cap.id === capability); -if (!entry) { - console.error(`[check-ts-adapter-deleted] missing capability ${capability}`); - process.exit(1); -} -if (entry.state !== expected) { - console.error( - `[check-ts-adapter-deleted] ${capability} is ${entry.state}; expected ${expected}` - ); - process.exit(1); -} -NODE -} - -echo "[check-ts-adapter-deleted] verifying transport/stdio-ts-adapter retirement in ${LEDGER}" - -require_ledger_state "transport/stdio-ts-adapter" "ts_deleted" - -if [[ -f "$ROOT/src/index.ts" ]]; then - echo "[check-ts-adapter-deleted] src/index.ts must be deleted when transport/stdio-ts-adapter is ts_deleted" >&2 - exit 1 -fi - -if [[ -f "$ROOT/dist/index.js" ]]; then - echo "[check-ts-adapter-deleted] dist/index.js must be deleted when transport/stdio-ts-adapter is ts_deleted" >&2 - exit 1 -fi - -if grep -q 'use_ts_transport' "$BIN"; then - echo "[check-ts-adapter-deleted] bin must not retain TS stdio opt-in after ts_deleted" >&2 - exit 1 -fi - -if grep -q 'IMAGE_READER_MCP_TRANSPORT:-}" == "ts"' "$BIN"; then - echo "[check-ts-adapter-deleted] bin must not route IMAGE_READER_MCP_TRANSPORT=ts after ts_deleted" >&2 - exit 1 -fi - -if grep -q 'exec node' "$BIN"; then - echo "[check-ts-adapter-deleted] bin must not exec node after ts_deleted" >&2 - exit 1 -fi - -echo "[check-ts-adapter-deleted] PASS — transport/stdio-ts-adapter retired; Rust rmcp is sole MCP transport" diff --git a/scripts/release-gate.ts b/scripts/release-gate.ts index 9e768b2..779e3bc 100644 --- a/scripts/release-gate.ts +++ b/scripts/release-gate.ts @@ -81,16 +81,6 @@ export async function buildReleaseGateReport(artifactDir: string): Promise { - it('bin wrapper routes MCP_TRANSPORT=http to Rust rmcp server', () => { - const bin = readFileSync(path.join(repoRoot, 'bin/image-reader-mcp'), 'utf8'); - expect(bin).toContain('resolve_transport'); - expect(bin).toContain('MCP_TRANSPORT=http'); - expect(bin).toContain('IMAGE_READER_MCP_TRANSPORT=http'); - }); - - it('Rust MCP server exposes streamable HTTP transport module', () => { - const httpTransport = readFileSync( - path.join(repoRoot, 'crates/image-reader-mcp-server/src/http_transport.rs'), - 'utf8' - ); - const mainRs = readFileSync( - path.join(repoRoot, 'crates/image-reader-mcp-server/src/main.rs'), - 'utf8' - ); - expect(httpTransport).toContain('StreamableHttpService'); - expect(httpTransport).toContain('health_check'); - expect(mainRs).toContain('http_transport::serve_http'); - }); -}); diff --git a/test/readmeDiscovery.test.ts b/test/readmeDiscovery.test.ts deleted file mode 100644 index 6c44799..0000000 --- a/test/readmeDiscovery.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, expect, it } from 'bun:test'; -import { existsSync, readFileSync } from 'node:fs'; - -const readText = (path: string) => readFileSync(path, 'utf8'); - -describe('README discovery surfaces', () => { - it('keeps pain-first fold content and honest discovery status', () => { - const readme = readText('README.md'); - - expect(readme).toContain('Did it see the truth?'); - expect(readme).toContain('## Why not vision LLM guess?'); - expect(readme).toContain('23 tests'); - expect(readme).toMatch(/Star the repo|Star this repo/); - expect(readme).not.toMatch(/Listed on \[MCP Servers\]/); - expect(readme).toContain('Not listed yet'); - expect(readme).toContain('glama.ai/mcp/servers/SylphxAI/image-reader-mcp'); - expect(readme).toContain('registry.modelcontextprotocol.io'); - expect(readme).toContain('io.github.SylphxAI/image-reader-mcp'); - expect(readme).not.toContain('Publishing on next release'); - expect(readme).toContain('chatmcp/mcpso/issues/3068'); - expect(readme).toContain('Listed — `io.github.SylphxAI/image-reader-mcp`'); - expect(readme).toContain('smart-reader-mcp'); - expect(readme).not.toContain('not in pdf-reader-mcp'); - expect(readme).not.toContain('polluting pdf-reader'); - expect(readme).not.toContain('ADR-0002'); - }); - - it('ships official MCP Registry metadata aligned with package.json', () => { - const pkg = JSON.parse(readText('package.json')); - const server = JSON.parse(readText('server.json')); - - expect(pkg.mcpName).toBe('io.github.SylphxAI/image-reader-mcp'); - expect(server.name).toBe(pkg.mcpName); - expect(server.packages[0].identifier).toBe(pkg.name); - expect(server.version).toBe(pkg.version); - expect(server.packages[0].version).toBe(pkg.version); - expect(server.description.length).toBeLessThanOrEqual(100); - expect(existsSync('.github/workflows/publish-mcp-registry.yml')).toBe(true); - }); -}); diff --git a/test/rustDecode.boundary.test.ts b/test/rustDecode.boundary.test.ts index d415f98..627bbc4 100644 --- a/test/rustDecode.boundary.test.ts +++ b/test/rustDecode.boundary.test.ts @@ -84,15 +84,4 @@ describe('rust decode engine boundary', () => { expect(twin.region_evidence?.dimensions).toEqual({ width: 10, height: 6 }); expect(twin.region_evidence?.region_hash.length).toBeGreaterThan(0); }); - - it('keeps decode logic out of the TypeScript adapter sources', async () => { - const { readFileSync } = await import('node:fs'); - const handlerSrc = readFileSync(path.join(repoRoot, 'src/handlers/readImage.ts'), 'utf8'); - const engineSrc = readFileSync(path.join(repoRoot, 'src/engine/rust-decode.ts'), 'utf8'); - - expect(engineSrc).toContain('spawnSync'); - expect(handlerSrc).toContain('probeImageViaRustEngine'); - expect(handlerSrc).toContain('cropRegionViaRustEngine'); - expect(handlerSrc).not.toMatch(/sha256|ImageReader|guess_format/i); - }); }); diff --git a/test/tsAdapterDeletion.matrix.test.ts b/test/tsAdapterDeletion.matrix.test.ts deleted file mode 100644 index 66665c9..0000000 --- a/test/tsAdapterDeletion.matrix.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, it } from 'bun:test'; -import { spawnSync } from 'node:child_process'; -import { existsSync, readFileSync } from 'node:fs'; -import path from 'node:path'; - -const repoRoot = path.resolve(import.meta.dirname, '..'); - -describe('TS stdio adapter deletion matrix (adversarial admission)', () => { - it('npm bin routes exclusively to Rust rmcp', () => { - const bin = readFileSync(path.join(repoRoot, 'bin/image-reader-mcp'), 'utf8'); - expect(bin).toContain('resolve_rust_bin'); - expect(bin).toContain('resolve_transport'); - expect(bin).toContain('image-reader-mcp-server'); - expect(bin).not.toContain('use_ts_transport'); - expect(bin).not.toContain('IMAGE_READER_MCP_TRANSPORT:-}" == "ts"'); - expect(bin).not.toContain('exec node'); - expect(bin).not.toContain('dist/index.js'); - }); - - it('TS stdio adapter sources are deleted', () => { - expect(existsSync(path.join(repoRoot, 'src/index.ts'))).toBe(false); - expect(existsSync(path.join(repoRoot, 'dist/index.js'))).toBe(false); - }); - - it('doctor CLI is preserved via doctor-cli.ts (not src/index.ts)', () => { - expect(existsSync(path.join(repoRoot, 'src/doctor-cli.ts'))).toBe(true); - const pkg = JSON.parse(readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as { - scripts?: Record; - }; - expect(pkg.scripts?.doctor).toContain('doctor-cli'); - expect(pkg.scripts?.doctor).not.toContain('src/index.ts'); - }); - - it('deletion gate script enforces ts_deleted ledger state', () => { - const script = readFileSync( - path.join(repoRoot, 'scripts/check-ts-adapter-deletion-ready.sh'), - 'utf8' - ); - expect(script).toContain('require_ledger_state "transport/stdio-ts-adapter" "ts_deleted"'); - expect(script).toContain('src/index.ts must be deleted'); - expect(script).toContain('use_ts_transport'); - }); - - it('check-no-ts-stdio-mcp gate enforces Rust-only stdio authority', () => { - const script = readFileSync(path.join(repoRoot, 'scripts/check-no-ts-stdio-mcp.sh'), 'utf8'); - expect(script).toContain('check-no-ts-stdio-mcp'); - expect(script).toContain('resolve_rust_bin'); - expect(script).toContain('transport::stdio'); - expect(script).toContain('transport/stdio-ts-adapter'); - expect(script).toContain('ts_deleted'); - }); - - it('ledger records all capabilities as ts_deleted', () => { - const ledger = JSON.parse( - readFileSync(path.join(repoRoot, 'docs/specs/migration-ledger.json'), 'utf8') - ) as { - capabilities: Array<{ id: string; state: string }>; - summary: { ts_deleted: number; ts_only: number; completion_progress: number; total: number }; - }; - const expected = [ - 'transport/web-mcp-http', - 'transport/stdio-rust-rmcp', - 'transport/stdio-ts-adapter', - 'tool/read_image', - ]; - for (const id of expected) { - const cap = ledger.capabilities.find((entry) => entry.id === id); - expect(cap?.state).toBe('ts_deleted'); - } - expect(ledger.summary.ts_deleted).toBe(4); - expect(ledger.summary.ts_only).toBe(0); - expect(ledger.summary.completion_progress).toBe(1.0); - expect(ledger.summary.total).toBe(4); - }); - - it('deletion-ready and no-ts-stdio gates pass against real bin + ledger', () => { - const deletion = spawnSync('bash', ['scripts/check-ts-adapter-deletion-ready.sh'], { - cwd: repoRoot, - encoding: 'utf8', - timeout: 30_000, - }); - expect(deletion.status).toBe(0); - expect(deletion.stdout).toContain('PASS'); - - const noTs = spawnSync('bash', ['scripts/check-no-ts-stdio-mcp.sh'], { - cwd: repoRoot, - encoding: 'utf8', - timeout: 30_000, - }); - expect(noTs.status).toBe(0); - expect(noTs.stdout).toContain('PASS'); - }); -});