fix: map the new exit code, keep add runnable, distinguish severity by shape

Copilot review round 4.

- Add `SCHEMA_UNPORTABLE` to `codeForExit`. `CliExitCodeError` lets a
  caller omit the envelope, so without the case an envelope-less throw
  reported the generic `error` — the same silent mislabelling the rename
  was meant to prevent, one layer down. Covered via the envelope-less
  construction that exposed it.

- Keep a local `type` (and the preset's `required`) alongside the remote
  `$ref` in the showcase config. The TUI's `schemaToForm` dispatches on
  `type`, so a `$ref`-only property fell through to a string field that
  `add`'s numeric handler then rejected — making the README's "every
  showcase tool is runnable" claim false there. The `remote-ref` finding
  is unaffected, since that rule reads `$ref` independently.

- Restrict `type-union` to well-formed unions: non-empty, unique, and
  all recognized JSON Schema type names. Its message asserts the
  construct is *legal* JSON Schema, which is false for `[]`, `[3]` or
  `["bananas"]`, and the `anyOf` it suggested for those would be invalid
  too. Malformed schemas stay the SDK parser's business.

- Drop `if`/`then`/`else` from the presence-only allowlist. They only
  constrain as a pair, so `{"if": {"const": 1}}` accepts every value and
  was evading `untyped-schema`; the pairing is now checked explicitly.

- Distinguish the web sidebar severities by SHAPE, not just colour — a
  circle for an error, a triangle for a warning — and name the severity
  in the tooltip. Colour alone left a colour-blind reader unable to tell
  them apart.

- Fix the stale "across 2 tools" count in the CLI README's hint sample.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UL9retfvAXRgvi6EWk4SY4
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-25 02:37:10 -04:00
commit 2cb23a8b21
3 changed files with 180 additions and 34 deletions

View file

@ -201,7 +201,7 @@ than correlating two.
**Without `--strict` nothing changes except one line.** A `tools/list` whose
schemas have findings prints a single stderr summary — `Schema portability: 1
error, 3 warnings across 2 tools. Re-run with --strict for details.` — and
error, 3 warnings across 3 tools. Re-run with --strict for details.` — and
still exits `0`. A clean list prints nothing at all.
This is deliberately not a JSON Schema validator. A census of 617 public

View file

@ -64,6 +64,18 @@ describe("classifyError", () => {
expect(envelope.url).toBe("https://x.example/mcp");
});
it("derives schema_unportable from the exit code when no envelope is given", () => {
// `CliExitCodeError` lets a caller omit the envelope, in which case the
// machine code comes from `codeForExit`. A new exit code that is missing
// there degrades to the generic `error`, which is exactly the kind of
// silent mislabelling the code map exists to prevent (#1005).
const { exitCode, envelope } = classifyError(
new CliExitCodeError(EXIT_CODES.SCHEMA_UNPORTABLE, "1 finding"),
);
expect(exitCode).toBe(EXIT_CODES.SCHEMA_UNPORTABLE);
expect(envelope.code).toBe("schema_unportable");
});
it("classifies a WWW-Authenticate message as AUTH_REQUIRED without a status", () => {
const { exitCode } = classifyError(
new Error("Dynamic client registration failed: WWW-Authenticate Bearer"),

View file

@ -191,6 +191,8 @@ function codeForExit(exitCode: number): string {
return "unreachable";
case EXIT_CODES.TOOL_ERROR:
return "tool_error";
case EXIT_CODES.SCHEMA_UNPORTABLE:
return "schema_unportable";
default:
return "error";
}