fix: address Copilot review round 2 on #2248

Four findings, two of them silent holes in the verification itself.

**A blob-served SKILL.md skipped the frontmatter check entirely.** The entry's
own file was captured as `contents.text`, so a server returning the markdown
as a base64 `blob` — a legal `resources/read` shape this module already
decodes for the digest — never reached the comparison, and the report still
said `ok`. A MUST that quietly did not run. The file is now held as bytes and
the text derived from them, which also guarantees the digest and the
frontmatter describe one snapshot.

**A dynamic skill whose SKILL.md could not be read reported `ok: true`.** It
has no manifest rows, so `files` stayed empty, and its only static finding is
a warning — a verification that could not be performed was reported as one
that passed. The fallback read now records a `read-error` for all three ways
it can fail. It is gated on `manifestListsSelf` (by normalized identity) so a
self-entry the manifest listed and failed to read is not read, or reported,
twice.

**A served `.nan` compared equal to a listed `null`.** YAML expresses
non-finite numbers, JSON does not, and `JSON.stringify` turns all of them
into `null` — so the canonical comparison reported a real mismatch as
agreement. Verified against the parser before fixing. They now canonicalize
to a form no JSON scalar can equal, and the value is named in the finding.

Also: the self-entry match is by normalized identity rather than raw string,
which stops a second read and stops this function disagreeing with
`checkSkillConformance`; and `SkillsTab.tsx` gains the file header AGENTS.md
requires.

The four `err instanceof Error` ternaries are one `reasonOf` helper now —
extracted because the coverage gate found every one of their non-Error arms
uncovered, and one honestly-tested branch beats four ignores.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ND46Ci64DC3tzBy9aG81Nc
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-07 19:24:30 -04:00
commit 033600e4ca

View file

@ -1,3 +1,23 @@
/**
* The TUI's Skills pane — the SEP-2640 catalog in a terminal (#2248).
*
* **Why the TUI owns a pane rather than reusing the web screen's logic.** It
* does reuse everything that decides an answer: `checkSkillConformance`,
* `checkSkillNameCollisions` and `verifySkills` all live in `core/` and are
* driven identically here, so a verdict cannot differ depending on which client
* you asked. What is local is presentation, and the terminal's constraints are
* genuinely different — two panes in 80 columns, no colour to rely on, and a
* keyboard rather than a pointer.
*
* ⚠️ **Severity is carried by a glyph as well as a colour** (`✓` / `!` / `✗`).
* This pane is read over ssh, inside tmux, and piped through `script(1)`, where
* colour may not survive; a row whose only signal was `red` would then be
* indistinguishable from a clean one.
*
* The pane is shown only when the connected server declares the extension —
* that gate, and the reset that leaves the tab when it goes false, live in
* `App.tsx` because they are navigation concerns rather than this pane's.
*/
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Box, Text, useInput, type Key } from "ink";
import { ScrollView, type ScrollViewRef } from "ink-scroll-view";