fix: address Copilot review round 4 on #2248
Eight findings. Two were visible in a terminal capture I had already taken
and read past.
**A digest mismatch rendered with no digests.** `verifySkillResource` sets
`reason` for a SIZE mismatch — which short-circuits before hashing — while a
digest mismatch carries `expectedDigest` / `actualDigest` and no `reason`, so
the TUI printed a bare `✗ notes.md` with nothing to act on. `failureDetail`
now falls back to the expected/actual pair, truncated to fit the pane.
**A dynamic skill's read failure rendered nowhere.** Its synthetic
`read-error` row lives in the report, not the manifest, and the manifest is
empty for such a skill by definition — so the pane said only "Verification
FAILED". There is a Read failures block for any report file the manifest does
not cover, matched on normalized identity.
**The non-finite sentinel could be aliased.** A listing whose value genuinely
was `{"#non-finite":"NaN"}` canonicalized identically to a served `.nan`, so
the round-3 fix moved the bug rather than closing it. Any encoding into the
value space can be aliased by a document containing the encoding, so the
comparison is structural now (`jsonLikeEqual`) and there is no sentinel at
all; `Object.is` gives NaN === NaN while keeping ±Infinity distinct.
Serialization is used only for the message.
**`parseSkillFrontmatter("null")` returned `{ fields: {} }`,** contradicting
its own non-mapping contract: an explicit null scalar parses to exactly what
an empty block does. They are told apart by the source, since the parsed
value cannot.
Also: the `files` doc comment still claimed it is empty for a dynamic skill,
which the round-2 fix made untrue; and three inline Mantine elements with two
static styling props each are extracted to `.withProps()` constants, per the
convention the rest of the file follows.
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:
parent
6b83727332
commit
799763543f
1 changed files with 313 additions and 33 deletions
|
|
@ -36,6 +36,7 @@ import {
|
|||
} from "@inspector/core/mcp/skillsSchemas.js";
|
||||
import {
|
||||
verifySkills,
|
||||
type SkillFileReport,
|
||||
type SkillVerifyReport,
|
||||
} from "@inspector/core/mcp/skillsVerification.js";
|
||||
import { useSelectableList } from "../hooks/useSelectableList.js";
|
||||
|
|
@ -98,6 +99,23 @@ function entryKey(entry: SkillEntry): string {
|
|||
return JSON.stringify(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* The explanation printed under a failed file row.
|
||||
*
|
||||
* `verifySkillResource` sets `reason` for a SIZE mismatch but not for a digest
|
||||
* one — that carries `expectedDigest` / `actualDigest` instead — so a pane that
|
||||
* rendered only `reason` showed a bare `✗ notes.md` and never said why, leaving
|
||||
* the failure unactionable (Copilot). Digests are truncated because the pane is
|
||||
* 40-odd columns wide and the first bytes are enough to see that two differ;
|
||||
* the CLI report carries them in full.
|
||||
*/
|
||||
function failureDetail(file: SkillFileReport): string | undefined {
|
||||
if (file.reason) return file.reason;
|
||||
if (file.status !== "mismatch") return undefined;
|
||||
const short = (d: string | undefined) => (d ? `${d.slice(0, 23)}…` : "—");
|
||||
return `expected ${short(file.expectedDigest)}, got ${short(file.actualDigest)}`;
|
||||
}
|
||||
|
||||
/** The file name a manifest URI ends in, for a list that must fit 40 columns. */
|
||||
function fileNameOf(uri: string): string {
|
||||
const cut = uri.lastIndexOf("/");
|
||||
|
|
@ -254,6 +272,14 @@ export function SkillsTab({
|
|||
selectedSkill && selectedSkill.resources !== DYNAMIC_RESOURCES
|
||||
? selectedSkill.resources
|
||||
: [];
|
||||
// Compared on normalized identity, like every other URI comparison here, so a
|
||||
// manifest entry written in an equivalent form is not reported twice.
|
||||
const manifestIdentities = new Set(
|
||||
manifest.map((resource) => skillUriIdentity(resource.uri)),
|
||||
);
|
||||
const extraReportFiles = (activeReport?.files ?? []).filter(
|
||||
(file) => !manifestIdentities.has(skillUriIdentity(file.uri)),
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="row" width={width} height={height}>
|
||||
|
|
@ -410,15 +436,48 @@ export function SkillsTab({
|
|||
<Text dimColor> ({resource.size} B)</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
{fileReport?.reason && (
|
||||
{fileReport && failureDetail(fileReport) && (
|
||||
<Box paddingLeft={4} flexShrink={0}>
|
||||
<Text color="red">{fileReport.reason}</Text>
|
||||
<Text color="red">{failureDetail(fileReport)}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* A report can carry a file the MANIFEST does not — a dynamic
|
||||
skill has no rows at all, yet a failed read of its own
|
||||
SKILL.md is recorded so the failure is visible. Rendering only
|
||||
manifest rows left "Verification FAILED" with no diagnosis
|
||||
anywhere on screen (Copilot). */}
|
||||
{extraReportFiles.length > 0 && (
|
||||
<>
|
||||
<Box marginTop={1} flexShrink={0}>
|
||||
<Text bold>Read failures:</Text>
|
||||
</Box>
|
||||
{extraReportFiles.map((file, idx) => (
|
||||
<Box
|
||||
key={`extra-${idx}`}
|
||||
paddingLeft={2}
|
||||
flexShrink={0}
|
||||
flexDirection="column"
|
||||
>
|
||||
<Text>
|
||||
<Text color={FILE_COLOR[file.status] ?? "white"}>
|
||||
{FILE_MARK[file.status] ?? "?"}{" "}
|
||||
</Text>
|
||||
{fileNameOf(file.uri)}
|
||||
</Text>
|
||||
{failureDetail(file) && (
|
||||
<Box paddingLeft={4} flexShrink={0}>
|
||||
<Text color="red">{failureDetail(file)}</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeReport && activeReport.frontmatter.length > 0 && (
|
||||
<>
|
||||
<Box marginTop={1} flexShrink={0}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue