Skip to content
Closed
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
10 changes: 5 additions & 5 deletions SOURCE_MANIFEST.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"schema_version": "thoth-source-preview-manifest-v1",
"created_at": "2026-10-10T08:13:11.279804+00:00",
"created_at": "2026-10-10T14:15:19.091855+00:00",
"license": "MIT",
"git_history_included": false,
"full_suite_verified": false,
Expand Down Expand Up @@ -348,13 +348,13 @@
},
{
"path": "apps/web/src/api/resultEvidence.test.ts",
"sha256": "5fc6731dbfa58e9e68dd93c7ea73b06119285d627b8fba117ae44b2b8a25b309",
"bytes": 5943
"sha256": "4ce2f6102016c86ad2d1881c0362ecb458502d5045cf2274b4b15d64b9bb9e07",
"bytes": 8096
},
{
"path": "apps/web/src/api/resultEvidence.ts",
"sha256": "2ec47763b71b7afadbf596d1324c8e3e38d0c7266ce0d952e967cb4702b86d3a",
"bytes": 7869
"sha256": "badd582b14dc01f2ce422756700d72310948ac032ccd5a4b5a665b2e1a3a85b0",
"bytes": 8343
},
{
"path": "apps/web/src/api/resultEvidenceIdentity.test.ts",
Expand Down
24 changes: 24 additions & 0 deletions apps/web/src/api/resultEvidence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,27 @@ it("binds the historical manifest body without requiring operation-only envelope
await expect(readResultEvidence(selection, { ...partial.result, thread_id: "foreign" }, { offset: 0 })).rejects.toThrow();
await expect(readResultEvidence(selection, { ...partial.result, request_epoch: 999 }, { offset: 0 })).rejects.toThrow();
});

it("reads the evidence when the screen shows a summary view that left some keys out", async () => {
const full = { ...partial.result, coverage: { rows: 3 }, requirements: ["r1"], source_packet: { spans: [1, 2] } };
vi.mocked(rpc).mockImplementation(async (_method, args) => evidenceResponse(Number(args.selected_evidence_offset ?? 0), args.include_selected_evidence === true, full));
// the displayed result is the stored one without the heavy keys, wrapper keys kept
expect((await readResultEvidence(selection, partial.result, { offset: 0 })).selected_count).toBe(149);
const { contract_version: _contract, ...withoutWrapperKey } = partial.result;
void _contract;
expect((await readResultEvidence(selection, withoutWrapperKey, { offset: 0 })).selected_count).toBe(149);
expect((await readResultEvidence(selection, full, { offset: 0 })).selected_count).toBe(149);
});
it("still rejects a displayed value that differs, or a key the stored result does not have", async () => {
const full = { ...partial.result, coverage: { rows: 3 } };
vi.mocked(rpc).mockImplementation(async (_method, args) => evidenceResponse(Number(args.selected_evidence_offset ?? 0), args.include_selected_evidence === true, full));
await expect(readResultEvidence(selection, { ...partial.result, answer: "another answer" }, { offset: 0 })).rejects.toThrow("연결을 확인하지 못했습니다");
await expect(readResultEvidence(selection, { ...partial.result, coverage: { rows: 4 } }, { offset: 0 })).rejects.toThrow("연결을 확인하지 못했습니다");
await expect(readResultEvidence(selection, { ...partial.result, invented_key: 1 }, { offset: 0 })).rejects.toThrow("연결을 확인하지 못했습니다");
});
it("still needs the server's own result and the manifest to be the same whole", async () => {
const response = evidenceResponse(0, true, { ...partial.result, coverage: { rows: 3 } });
response.value.manifest.result = { ...partial.result };
vi.mocked(rpc).mockResolvedValue(response);
await expect(readResultEvidence(selection, partial.result, { offset: 0 })).rejects.toThrow("연결을 확인하지 못했습니다");
});
14 changes: 10 additions & 4 deletions apps/web/src/api/resultEvidence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ export function resultContentKey(value: unknown): string {
if (value !== null && typeof value === "object") return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${resultContentKey((value as Record<string, unknown>)[key])}`).join(",")}}`;
return JSON.stringify(value) ?? "null";
}
/** The screen may show a summary view of the stored result: every key it shows must be in the stored result with the
* same value, and a key it leaves out is taken as left out by the summary. A key the stored result does not have is not. */
function isShownPartOf(shown: Record<string, unknown>, stored: Record<string, unknown>): boolean {
return Object.entries(shown).every(([key, value]) => Object.hasOwn(stored, key) && resultContentKey(value) === resultContentKey(stored[key]));
}
function mismatch(): never { throw new Error("선택한 답변과 근거의 연결을 확인하지 못했습니다. 답변을 다시 확인해 주세요."); }

function answerContent(value: Record<string, unknown>, thread: string, epoch: unknown, terminal: unknown) {
Expand All @@ -56,12 +61,13 @@ async function read(selection: ResultSelection, displayed: Record<string, unknow
if (!value.manifest || !value.result_revision_digest) throw new Error("이 답변의 저장 버전과 근거 목록을 확인하지 못했습니다.");
if (value.request.project_id !== scope.projectId || value.request.thread_id !== scope.threadId || value.request.operation_id !== selection.operationId || !value.result) mismatch();
const manifest = manifestSchema.parse(value.manifest);
const displayedContent = resultContentKey(answerContent(displayed, scope.threadId, value.request.request_epoch, value.manifest.terminal_reason));
const resultContent = resultContentKey(answerContent(value.result, scope.threadId, value.request.request_epoch, value.manifest.terminal_reason));
const manifestContent = resultContentKey(answerContent(manifest.result, scope.threadId, value.request.request_epoch, value.manifest.terminal_reason));
const displayedContent = answerContent(displayed, scope.threadId, value.request.request_epoch, value.manifest.terminal_reason);
const resultBody = answerContent(value.result, scope.threadId, value.request.request_epoch, value.manifest.terminal_reason);
const manifestBody = answerContent(manifest.result, scope.threadId, value.request.request_epoch, value.manifest.terminal_reason);
if (manifest.operation_id !== selection.operationId || manifest.request_ref.project_id !== scope.projectId ||
manifest.request_ref.entity_type !== "THREAD" || manifest.request_ref.entity_id !== `request:${scope.threadId}` ||
manifest.request_ref.revision_digest !== scope.requestDigest || resultContent !== displayedContent || manifestContent !== displayedContent) mismatch();
manifest.request_ref.revision_digest !== scope.requestDigest ||
resultContentKey(resultBody) !== resultContentKey(manifestBody) || !isShownPartOf(displayedContent, manifestBody)) mismatch();
return { value, manifest };
}

Expand Down
Loading