diff --git a/src/index.ts b/src/index.ts index adf75a6..87e8225 100644 --- a/src/index.ts +++ b/src/index.ts @@ -169,6 +169,15 @@ server.registerTool( const { items: evidence } = ensureUniqueIds(evidenceItems, "evidence"); const { items: claimItems } = ensureUniqueIds(claims.map((text) => ({ text })), "claim"); + // Keep the internal no-source option distinct from caller-provided evidence + // ids. A real evidence item named "none" must remain selectable and must be + // returned as supporting_evidence rather than being mistaken for the sentinel. + const evidenceIds = new Set(evidence.map((item) => item.id)); + let noEvidenceKey = "none"; + for (let suffix = 1; evidenceIds.has(noEvidenceKey); suffix++) { + noEvidenceKey = `none_${suffix}`; + } + const questions: Record = {}; for (const claim of claimItems) { questions[`relation_${claim.id}`] = choice( @@ -181,7 +190,7 @@ server.registerTool( ); if (evidence.length > 1) { const criteria: Record = Object.fromEntries(evidence.map((e) => [e.id, null])); - criteria["none"] = "No single evidence item contains the content the claim depends on"; + criteria[noEvidenceKey] = "No single evidence item contains the content the claim depends on"; questions[`source_${claim.id}`] = choice( `Which evidence item does claim \`${claim.id}\` (${claim.text}) rest on?`, criteria, @@ -202,8 +211,9 @@ server.registerTool( const validated = validateChoiceAnswer(relation, Object.keys(RELATION_TO_VERDICT)); // source_* is optional auxiliary information, requested only for multiple // evidence items. Its absence does not invalidate the relation verdict, - // but a present source must name a supplied evidence id (or "none"). - const sourceKeys = [...evidence.map((e) => e.id), "none"]; + // but a present source must name a supplied evidence id or the internal + // no-evidence option. + const sourceKeys = [...evidence.map((e) => e.id), noEvidenceKey]; const source = validateChoiceAnswer(answers[`source_${claim.id}`], sourceKeys); const valid = validated && Object.hasOwn(RELATION_TO_VERDICT, validated.choice) && (relation.confidence == null || validated.confidence !== null); @@ -231,7 +241,7 @@ server.registerTool( probabilities: relation.probabilities ?? null, confidence, action: confidence === null ? "review" : verifyAction(confidence, autoAccept), - supporting_evidence: source && source.choice !== "none" ? source.choice : null, + supporting_evidence: source && source.choice !== noEvidenceKey ? source.choice : null, }; }); diff --git a/test/mock.test.mjs b/test/mock.test.mjs index edcd592..6cba908 100644 --- a/test/mock.test.mjs +++ b/test/mock.test.mjs @@ -1566,6 +1566,35 @@ test("jev_verify still returns verified verdicts on a complete response", async }); }); +test("jev_verify keeps a real evidence id named none distinct from its no-source option", async () => { + await withMock((request) => { + const criteria = request.questions.source_claim0.criteria; + assert.equal(criteria.none, null); + const noSourceKey = Object.entries(criteria).find(([, description]) => + description === "No single evidence item contains the content the claim depends on" + )?.[0]; + assert.ok(noSourceKey); + assert.notEqual(noSourceKey, "none"); + return { + relation_claim0: pick("supports", Object.keys(request.questions.relation_claim0.criteria)), + source_claim0: pick("none", Object.keys(criteria)), + }; + }, async (client) => { + const result = await client.callTool({ + name: "jev_verify", + arguments: { + claims: ["The release is ready."], + evidence: [ + { id: "none", text: "The release checks passed." }, + { id: "notes", text: "Unrelated planning notes." }, + ], + }, + }); + const body = payload(result); + assert.equal(body.results[0].supporting_evidence, "none"); + }); +}); + test("TypeSafe package rejection invalidates only the malformed judgment without another paid request", async () => { await withMock(() => ({ relation_claim0: pick("supports", ["supports", "contradicts", "says_nothing"]),