Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = {};
for (const claim of claimItems) {
questions[`relation_${claim.id}`] = choice(
Expand All @@ -181,7 +190,7 @@ server.registerTool(
);
if (evidence.length > 1) {
const criteria: Record<string, string | null> = 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,
Expand All @@ -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);
Expand Down Expand Up @@ -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,
};
});

Expand Down
29 changes: 29 additions & 0 deletions test/mock.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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"]),
Expand Down