From 02c2c92342f67980c6b92c29574a5b944e91bf68 Mon Sep 17 00:00:00 2001
From: THOTH
Date: Thu, 12 Nov 2026 20:06:32 +0900
Subject: [PATCH] Format sources, add security and accessibility lint, prepare
v0.2.0-beta.4
---
.github/workflows/product-check.yml | 3 +
Makefile.ps1 | 1 +
SOURCE_MANIFEST.json | 2527 +++++-----
apps/web/.prettierignore | 10 +
apps/web/.prettierrc.json | 3 +
apps/web/eslint.config.js | 8 +-
apps/web/package.json | 82 +-
apps/web/preview-timeline.html | 13 -
apps/web/src/App.test.tsx | 8 +-
apps/web/src/App.tsx | 26 +-
apps/web/src/api/actionDraft.ts | 20 +-
apps/web/src/api/capabilities.ts | 90 +-
apps/web/src/api/contracts.test.ts | 200 +-
apps/web/src/api/conversation.test.ts | 202 +-
apps/web/src/api/conversation.ts | 221 +-
.../api/conversationRefetchDiagnosis.test.ts | 125 +-
apps/web/src/api/expectedResultContract.ts | 4 +-
apps/web/src/api/expectedResultState.test.ts | 10 +-
apps/web/src/api/expectedResultState.ts | 9 +-
apps/web/src/api/files.test.ts | 163 +-
apps/web/src/api/files.ts | 32 +-
apps/web/src/api/forgetProject.test.ts | 13 +-
apps/web/src/api/forgetProject.ts | 24 +-
apps/web/src/api/groundingNegotiation.test.ts | 43 +-
apps/web/src/api/history.test.ts | 98 +-
apps/web/src/api/history.ts | 270 +-
apps/web/src/api/historyComparison.ts | 78 +-
apps/web/src/api/historyHttp.test.ts | 107 +-
apps/web/src/api/historyModels.ts | 49 +-
apps/web/src/api/historySchemas.ts | 105 +-
.../web/src/api/historyServerContract.test.ts | 99 +-
apps/web/src/api/historyTestFixture.ts | 387 +-
apps/web/src/api/hypothesisLink.ts | 73 +-
apps/web/src/api/hypothesisSame.ts | 47 +-
apps/web/src/api/judgmentRecords.ts | 125 +-
apps/web/src/api/koreanParticles.test.ts | 35 +-
apps/web/src/api/koreanParticles.ts | 4 +-
apps/web/src/api/lessons.ts | 35 +-
apps/web/src/api/localRetrieval.ts | 207 +-
.../web/src/api/localRetrievalInstall.test.ts | 9 +-
.../src/api/localWorkspacePersistence.test.ts | 140 +-
apps/web/src/api/localWorkspacePersistence.ts | 389 +-
apps/web/src/api/parserError.test.ts | 53 +-
apps/web/src/api/parserError.ts | 14 +-
apps/web/src/api/presentation.ts | 23 +-
.../api/progressEvidenceLocations.test.tsx | 176 +-
apps/web/src/api/progressEvidenceLocations.ts | 43 +-
apps/web/src/api/projectActionFailure.ts | 13 +-
apps/web/src/api/projectCreation.ts | 7 +-
apps/web/src/api/projectDeletion.ts | 6 +-
apps/web/src/api/protocol.ts | 23 +-
apps/web/src/api/publicWeb.test.ts | 8 +-
apps/web/src/api/readAccess.ts | 17 +-
apps/web/src/api/recordViews.ts | 259 +-
apps/web/src/api/requestLanguage.test.ts | 36 +-
apps/web/src/api/research.ts | 321 +-
apps/web/src/api/researchFollowup.ts | 138 +-
apps/web/src/api/researchProgress.test.tsx | 54 +-
apps/web/src/api/researchProgress.ts | 53 +-
.../researchProgressCharacterization.test.ts | 288 +-
apps/web/src/api/researchProgressText.ts | 74 +-
apps/web/src/api/researchStatusRead.test.ts | 54 +-
apps/web/src/api/researchStatusRead.ts | 33 +-
apps/web/src/api/responseLanguageDefault.ts | 46 +-
apps/web/src/api/responseLanguages.test.ts | 69 +-
apps/web/src/api/responseLanguages.ts | 4 +-
apps/web/src/api/restore.test.ts | 70 +-
apps/web/src/api/restore.ts | 228 +-
apps/web/src/api/restorePending.test.ts | 32 +-
apps/web/src/api/restorePending.ts | 72 +-
apps/web/src/api/resultEvidence.test.ts | 161 +-
apps/web/src/api/resultEvidence.ts | 276 +-
.../src/api/resultEvidenceIdentity.test.ts | 72 +-
apps/web/src/api/resultEvidenceIdentity.ts | 30 +-
.../web/src/api/resultEvidenceRuntime.test.ts | 148 +-
.../api/resultEvidenceServerContract.test.ts | 52 +-
apps/web/src/api/resultEvidenceTestFixture.ts | 90 +-
apps/web/src/api/rpcClient.test.ts | 92 +-
apps/web/src/api/rpcClient.ts | 131 +-
apps/web/src/api/rpcTransport.test.ts | 52 +-
apps/web/src/api/sourceBindings.test.ts | 45 +-
apps/web/src/api/sourceBindings.ts | 17 +-
apps/web/src/api/sourcePolicy.ts | 96 +-
apps/web/src/api/sourceTime.ts | 43 +-
.../web/src/api/submissionPersistence.test.ts | 34 +-
apps/web/src/api/submissionPersistence.ts | 32 +-
apps/web/src/api/threadName.ts | 4 +-
apps/web/src/api/timelineExample.ts | 91 +-
apps/web/src/api/trace.ts | 195 +-
apps/web/src/api/uploadTypes.ts | 33 +-
.../web/src/components/AbsenceChecks.test.tsx | 73 +-
apps/web/src/components/AbsenceChecks.tsx | 81 +-
.../web/src/components/ActionCompare.test.tsx | 59 +-
apps/web/src/components/ActionCompare.tsx | 267 +-
.../components/ActionCompareApproval.test.tsx | 80 +-
apps/web/src/components/ActionCompareLive.tsx | 17 +-
.../components/ActionCompareStale.test.tsx | 31 +-
apps/web/src/components/ActionEffort.test.tsx | 128 +-
apps/web/src/components/ActionEffort.tsx | 257 +-
apps/web/src/components/AnswerBody.tsx | 66 +-
.../src/components/AnswerCitations.test.tsx | 103 +-
apps/web/src/components/AnswerHold.tsx | 36 +-
.../src/components/AnswerReadability.test.tsx | 142 +-
apps/web/src/components/ApprovalCard.test.tsx | 287 +-
apps/web/src/components/ApprovalCard.tsx | 165 +-
apps/web/src/components/ApprovalSection.tsx | 99 +-
apps/web/src/components/ArchivedProjects.tsx | 164 +-
.../AsyncDraftAndCitationRegression.test.tsx | 257 +-
.../src/components/AutoRetrySwitch.test.tsx | 83 +-
apps/web/src/components/AutoRetrySwitch.tsx | 48 +-
.../components/CapabilityWorkbench.test.tsx | 212 +-
.../src/components/CapabilityWorkbench.tsx | 339 +-
.../components/CheckedObservations.test.tsx | 55 +-
.../src/components/CheckedObservations.tsx | 122 +-
apps/web/src/components/ClaudeCodeHelp.tsx | 12 +-
apps/web/src/components/ClosureMark.test.tsx | 77 +-
apps/web/src/components/ClosureMark.tsx | 75 +-
apps/web/src/components/ClosureRecorder.tsx | 186 +-
apps/web/src/components/CodexToolHelp.tsx | 12 +-
apps/web/src/components/ConfirmDialog.tsx | 12 +-
.../src/components/ConfirmedFacts.test.tsx | 51 +-
apps/web/src/components/ConfirmedFacts.tsx | 74 +-
.../ConnectFailureInWorkspace.test.tsx | 212 +-
.../src/components/ConnectedSourceCard.tsx | 48 +-
.../src/components/ContractV3Screens.test.tsx | 144 +-
.../ConversationEarlierRound.test.tsx | 95 +-
.../ConversationRunningTurn.test.tsx | 130 +-
.../src/components/ConversationTimeline.tsx | 399 +-
.../ConversationTimelineAutoRetry.test.tsx | 127 +-
.../ConversationTimelineLegacyEmpty.test.tsx | 36 +-
.../ConversationTimelineRefetch.test.tsx | 296 +-
.../ConversationTimelineSynthetic.test.tsx | 75 +-
.../components/ConversationTurnView.test.tsx | 181 +-
.../src/components/ConversationTurnView.tsx | 114 +-
apps/web/src/components/Disclosure.tsx | 27 +-
.../components/DiscriminationRecords.test.tsx | 149 +-
apps/web/src/components/DomainRecords.tsx | 179 +-
.../components/EffectDeclarationFields.tsx | 58 +-
apps/web/src/components/EvidenceRail.test.tsx | 46 +-
apps/web/src/components/EvidenceRail.tsx | 56 +-
.../web/src/components/EvidenceUseSummary.tsx | 39 +-
.../components/ExecutionHoldNotice.test.tsx | 207 +-
.../src/components/ExecutionHoldNotice.tsx | 190 +-
apps/web/src/components/ExpectedResults.tsx | 109 +-
.../components/FileConnectionPanel.test.tsx | 333 +-
.../src/components/FileConnectionPanel.tsx | 363 +-
.../FileConnectionPanelParserError.test.tsx | 70 +-
.../web/src/components/FirstRunSetup.test.tsx | 2590 ++++++++--
apps/web/src/components/FirstRunSetup.tsx | 400 +-
.../src/components/GroundingDisplay.test.tsx | 96 +-
apps/web/src/components/GroundingNotice.tsx | 17 +-
.../src/components/HypothesisCompare.test.tsx | 124 +-
apps/web/src/components/HypothesisCompare.tsx | 335 +-
.../HypothesisCompareDraft.test.tsx | 55 +-
.../src/components/HypothesisCompareLive.tsx | 153 +-
.../HypothesisContractSwitch.test.tsx | 88 +-
.../components/HypothesisContractSwitch.tsx | 55 +-
.../components/HypothesisEvidence.test.tsx | 409 +-
.../components/HypothesisEvidencePanel.tsx | 60 +-
apps/web/src/components/HypothesisFields.tsx | 74 +-
.../components/HypothesisLinkNotice.test.tsx | 151 +-
.../src/components/HypothesisLinkNotice.tsx | 112 +-
.../components/HypothesisLinkScreens.test.tsx | 59 +-
.../src/components/HypothesisReview.test.tsx | 177 +-
.../src/components/HypothesisScreen.test.tsx | 183 +-
.../src/components/LanguageScreens.test.tsx | 52 +-
.../src/components/LanguageSettings.test.tsx | 305 +-
.../src/components/LanguageSettingsCard.tsx | 88 +-
apps/web/src/components/LessonRecall.test.tsx | 192 +-
apps/web/src/components/LessonRecall.tsx | 73 +-
apps/web/src/components/LinkRecheckDialog.tsx | 167 +-
.../src/components/LiveProjectWorkspace.tsx | 1002 +++-
.../LiveProjectWorkspaceConsent.test.tsx | 157 +-
.../LiveProjectWorkspaceInvestigate.test.tsx | 256 +-
.../LiveProjectWorkspaceReadiness.test.tsx | 191 +-
.../LiveProjectWorkspaceRestart.test.tsx | 918 +++-
.../LocalRetrievalSettingsCard.test.tsx | 507 +-
.../components/LocalRetrievalSettingsCard.tsx | 795 ++-
.../src/components/M7LiveReadDenial.test.tsx | 449 +-
.../src/components/M7OwnerSiblings.test.tsx | 448 +-
apps/web/src/components/M7ReadDenial.test.tsx | 488 +-
.../components/M7RemainingOwnership.test.tsx | 496 +-
.../src/components/M7ScopeBoundary.test.tsx | 135 +-
.../src/components/M7StateOwnership.test.tsx | 506 +-
apps/web/src/components/MemoryEditDialog.tsx | 121 +-
.../src/components/MemoryInjectionSwitch.tsx | 69 +-
.../src/components/ModelCredentialPanel.tsx | 169 +-
.../web/src/components/ModelSettings.test.tsx | 723 ++-
apps/web/src/components/ModelSettings.tsx | 355 +-
.../src/components/NavigationDrawer.test.tsx | 177 +-
apps/web/src/components/OAuthLoginFlow.tsx | 623 ++-
.../src/components/ProjectActionNotice.tsx | 15 +-
.../components/ProjectArchiveRestore.test.tsx | 408 +-
apps/web/src/components/ProjectForm.test.tsx | 19 +-
apps/web/src/components/ProjectForm.tsx | 60 +-
.../src/components/ProjectLanding.test.tsx | 8 +-
.../src/components/ProjectManagement.test.tsx | 234 +-
.../components/ProjectMemoryPanel.test.tsx | 414 +-
.../web/src/components/ProjectMemoryPanel.tsx | 216 +-
apps/web/src/components/ProjectNavItem.tsx | 43 +-
.../ProjectPermanentDelete.test.tsx | 244 +-
.../src/components/ProjectPermanentDelete.tsx | 109 +-
.../src/components/ProjectReadinessCard.tsx | 79 +-
.../src/components/ProjectSessionPages.tsx | 395 +-
.../src/components/ProjectWebSetup.test.tsx | 12 +-
apps/web/src/components/ProjectWebSetup.tsx | 78 +-
apps/web/src/components/ReadableList.tsx | 31 +-
apps/web/src/components/RecordInspector.tsx | 61 +-
.../src/components/RecordedStanding.test.tsx | 132 +-
.../components/RefutationConditionsPanel.tsx | 131 +-
apps/web/src/components/RenameDialog.tsx | 15 +-
.../components/RequestLanguageFlow.test.tsx | 198 +-
.../components/ResearchActivityStrip.test.tsx | 128 +-
.../src/components/ResearchActivityStrip.tsx | 370 +-
.../ResearchCancelledPresentation.test.tsx | 53 +-
apps/web/src/components/ResearchComposer.tsx | 323 +-
.../web/src/components/ResearchDetailPane.tsx | 234 +-
.../ResearchEvidenceDetail.test.tsx | 252 +-
.../components/ResearchFollowupSummary.tsx | 231 +-
apps/web/src/components/ResearchHold.test.tsx | 171 +-
.../src/components/ResearchLiveProgress.tsx | 84 +-
apps/web/src/components/ResearchProgress.tsx | 120 +-
.../components/ResearchProgressStop.test.tsx | 23 +-
.../components/ResearchResultCard.test.tsx | 323 +-
.../web/src/components/ResearchResultCard.tsx | 323 +-
apps/web/src/components/ResearchSidePanel.tsx | 22 +-
.../web/src/components/ResourcesPage.test.tsx | 138 +-
.../src/components/ResultCardOnce.test.tsx | 197 +-
.../src/components/ResultEvidencePanel.tsx | 167 +-
apps/web/src/components/ResultExample.tsx | 267 +-
apps/web/src/components/ResultHoldDetails.tsx | 79 +-
.../ResultInterruptionNotice.test.tsx | 103 +-
.../components/ResultInterruptionNotice.tsx | 148 +-
apps/web/src/components/ResultScreen.test.tsx | 269 +-
.../src/components/ResultStatusStrip.test.tsx | 117 +-
apps/web/src/components/ResultStatusStrip.tsx | 39 +-
apps/web/src/components/ReviewDialog.tsx | 114 +-
apps/web/src/components/ReviewStatusLine.tsx | 75 +-
.../src/components/SameHypothesis.test.tsx | 229 +-
apps/web/src/components/SameHypothesis.tsx | 188 +-
apps/web/src/components/Select.test.tsx | 17 +-
apps/web/src/components/Sentences.test.tsx | 21 +-
.../components/SourceCitationGate.test.tsx | 147 +-
.../web/src/components/SourceCitationGate.tsx | 148 +-
apps/web/src/components/SourceDisconnect.tsx | 11 +-
.../web/src/components/SourceTimeAdvanced.tsx | 155 +-
.../SourceTimeConfirmationDialog.tsx | 59 +-
apps/web/src/components/StatusBadge.tsx | 17 +-
.../src/components/SyntheticDataNotice.tsx | 21 +-
apps/web/src/components/TechnicalDetails.tsx | 7 +-
.../src/components/TestActionDraft.test.tsx | 138 +-
apps/web/src/components/TestActionDraft.tsx | 68 +-
apps/web/src/components/TestOrder.test.tsx | 191 +-
apps/web/src/components/TestOrderSection.tsx | 95 +-
.../web/src/components/TestResultRecorder.tsx | 174 +-
.../src/components/ThothBlueprint.test.tsx | 39 +-
apps/web/src/components/ThreadNavItem.tsx | 30 +-
apps/web/src/components/ToolInstallHelp.tsx | 92 +-
apps/web/src/components/TraceClosure.test.tsx | 189 +-
.../web/src/components/TraceConfirmDialog.tsx | 89 +-
.../src/components/TraceEvidenceDialog.tsx | 84 +-
apps/web/src/components/TraceHistory.tsx | 240 +-
apps/web/src/components/TraceImportDialog.tsx | 285 +-
apps/web/src/components/TraceOriginLine.tsx | 25 +-
apps/web/src/components/TracePage.test.tsx | 527 +-
apps/web/src/components/TracePage.tsx | 661 ++-
apps/web/src/components/UsageSummary.test.tsx | 30 +-
.../src/components/WorkspaceContent.test.tsx | 4 +-
apps/web/src/components/WorkspaceContent.tsx | 6 +-
.../src/components/WorkspaceErrorBoundary.tsx | 40 +-
apps/web/src/components/WorkspaceGate.tsx | 97 +-
.../components/WorkspaceInternetConsent.tsx | 76 +-
.../WorkspaceLocationNotice.test.tsx | 50 +-
.../components/WorkspaceLocationNotice.tsx | 18 +-
apps/web/src/components/WorkspaceTopbar.tsx | 60 +-
apps/web/src/components/XaiDeviceLogin.tsx | 411 +-
.../src/components/accessibilityRules.test.ts | 25 +-
apps/web/src/components/answerLeads.test.ts | 41 +-
apps/web/src/components/answerText.test.ts | 38 +-
apps/web/src/components/answerText.ts | 43 +-
apps/web/src/components/approvalView.ts | 104 +-
apps/web/src/components/askedQuestion.ts | 4 +-
apps/web/src/components/connectionCopy.ts | 74 +-
apps/web/src/components/coverageCounts.ts | 16 +-
.../src/components/effectDeclaration.test.ts | 37 +-
apps/web/src/components/effectDeclaration.ts | 135 +-
apps/web/src/components/effortEstimates.ts | 15 +-
apps/web/src/components/evidenceUse.ts | 58 +-
apps/web/src/components/expectedGrounding.ts | 5 +-
apps/web/src/components/expectedResults.css | 63 +-
apps/web/src/components/foldedOriginals.ts | 2 +-
apps/web/src/components/followupItems.ts | 127 +-
.../src/components/groundingTestFixture.ts | 96 +-
.../history/CriteriaChange.test.tsx | 111 +-
.../src/components/history/CriteriaChange.tsx | 138 +-
.../history/DecisionDeltaComparison.tsx | 446 +-
.../history/HistoryChangeSummary.test.tsx | 144 +-
.../history/HistoryChangeSummary.tsx | 110 +-
.../src/components/history/HistoryContent.tsx | 212 +-
.../src/components/history/HistoryDetail.tsx | 275 +-
.../src/components/history/HistoryExample.tsx | 33 +-
.../components/history/HistoryTimeline.tsx | 173 +-
.../history/ProjectReviewList.test.tsx | 68 +-
.../components/history/ProjectReviewList.tsx | 138 +-
.../history/ResearchHistoryWorkspace.test.tsx | 597 ++-
.../history/ResearchHistoryWorkspace.tsx | 377 +-
.../history/RestorePreviewDialog.tsx | 202 +-
.../components/history/RevisionComparison.tsx | 74 +-
.../src/components/history/historyErrors.ts | 3 +-
.../components/history/historyPresentation.ts | 96 +-
.../history/historyReasonText.test.ts | 46 +-
.../components/history/historyReasonText.ts | 12 +-
.../src/components/history/useRestoreFlow.ts | 97 +-
.../src/components/hypothesisLinkText.test.ts | 128 +-
apps/web/src/components/hypothesisLinkText.ts | 96 +-
apps/web/src/components/hypothesisMatrix.ts | 13 +-
apps/web/src/components/hypothesisReview.ts | 60 +-
apps/web/src/components/hypothesisView.ts | 44 +-
.../src/components/judgmentRecordText.test.ts | 114 +-
apps/web/src/components/judgmentRecordText.ts | 126 +-
apps/web/src/components/judgmentReview.ts | 134 +-
apps/web/src/components/lessonText.ts | 54 +-
.../components/localCredentialWorkspace.ts | 3 +-
apps/web/src/components/memoryEdit.ts | 78 +-
.../modelCatalogPresentation.test.ts | 86 +-
.../components/modelCatalogPresentation.ts | 71 +-
.../components/modelCredentialPresentation.ts | 257 +-
.../modelCredentialStatusNote.test.ts | 173 +-
apps/web/src/components/plainWording.ts | 18 +-
apps/web/src/components/readinessText.ts | 10 +-
apps/web/src/components/resultInterruption.ts | 56 +-
apps/web/src/components/resultUsage.ts | 64 +-
apps/web/src/components/returnFocus.test.tsx | 84 +-
apps/web/src/components/returnFocus.ts | 7 +-
apps/web/src/components/sameHypothesisText.ts | 31 +-
apps/web/src/components/statusLabels.test.tsx | 28 +-
apps/web/src/components/statusLabels.ts | 142 +-
.../web/src/components/testActionDraftText.ts | 57 +-
apps/web/src/components/testOrderInput.ts | 106 +-
apps/web/src/components/testOrderText.ts | 4 +-
.../src/components/testRanking.cases.test.ts | 106 +-
apps/web/src/components/testRanking.ts | 167 +-
apps/web/src/components/timelineTurns.ts | 30 +-
.../src/components/traceEvaluation.test.ts | 82 +-
.../src/components/traceInvestigation.test.ts | 90 +-
apps/web/src/components/traceInvestigation.ts | 93 +-
.../traceInvestigationLanguage.test.ts | 69 +-
.../src/components/traceReasonTags.test.ts | 165 +-
apps/web/src/components/traceReasonTags.ts | 57 +-
apps/web/src/components/traceRows.ts | 90 +-
apps/web/src/components/traceText.test.ts | 232 +-
apps/web/src/components/traceText.ts | 591 ++-
apps/web/src/components/turnSummary.ts | 50 +-
.../src/components/useAutoRetryOff.test.tsx | 49 +-
apps/web/src/components/useAutoRetryOff.ts | 13 +-
.../useConversationReadProtection.ts | 31 +-
apps/web/src/components/useDraftGeneration.ts | 21 +-
apps/web/src/components/useHypothesisLinks.ts | 13 +-
apps/web/src/components/useJudgmentRecords.ts | 35 +-
.../web/src/components/useNavigationDrawer.ts | 28 +-
.../components/useOpenAtLatestTurn.test.tsx | 16 +-
.../src/components/useProjectManagement.tsx | 56 +-
apps/web/src/components/useProjectSources.ts | 33 +-
.../src/components/useReadAccessProtection.ts | 18 +-
apps/web/src/components/useResearchSession.ts | 354 +-
.../components/useResponseLanguageSupport.ts | 16 +-
.../components/useResultEvidenceProtection.ts | 70 +-
apps/web/src/components/workspaceSelection.ts | 214 +-
.../web/src/components/workspaceSetupIssue.ts | 6 +-
apps/web/src/i18n/ResponseLanguageContext.ts | 8 +-
apps/web/src/i18n/UiError.ts | 2 +-
apps/web/src/i18n/activityDisplay.ts | 253 +-
apps/web/src/i18n/activityMessages.ts | 594 +--
apps/web/src/i18n/advancedMessages.ts | 402 +-
apps/web/src/i18n/apiNoticesMessages.ts | 146 +-
apps/web/src/i18n/bilingualRecord.ts | 8 +-
apps/web/src/i18n/commonStateMessages.ts | 250 +-
apps/web/src/i18n/connectionReasons.ts | 259 +-
apps/web/src/i18n/coreMessages.ts | 145 +-
apps/web/src/i18n/countFormatMessages.ts | 320 +-
apps/web/src/i18n/credentialMessages.ts | 262 +-
apps/web/src/i18n/finishingMessages.ts | 90 +-
apps/web/src/i18n/followupMessages.ts | 294 +-
apps/web/src/i18n/formatMessages.ts | 197 +-
apps/web/src/i18n/groundingMessages.ts | 40 +-
apps/web/src/i18n/historyFormatMessages.ts | 159 +-
apps/web/src/i18n/historyReasons.ts | 85 +-
apps/web/src/i18n/historyTitle.test.ts | 4 +-
apps/web/src/i18n/historyTitle.ts | 78 +-
apps/web/src/i18n/historyViewMessages.ts | 290 +-
.../web/src/i18n/hypothesisActionsMessages.ts | 418 +-
apps/web/src/i18n/ja/activityMessagesJa.ts | 24 +-
apps/web/src/i18n/ja/advancedMessagesJa.ts | 27 +-
apps/web/src/i18n/ja/apiNoticesMessagesJa.ts | 9 +-
apps/web/src/i18n/ja/commonStateMessagesJa.ts | 6 +-
apps/web/src/i18n/ja/coreMessagesJa.ts | 42 +-
apps/web/src/i18n/ja/credentialMessagesJa.ts | 66 +-
apps/web/src/i18n/ja/finishingMessagesJa.ts | 3 +-
apps/web/src/i18n/ja/followupMessagesJa.ts | 39 +-
apps/web/src/i18n/ja/groundingMessagesJa.ts | 3 +-
apps/web/src/i18n/ja/historyViewMessagesJa.ts | 27 +-
.../i18n/ja/hypothesisActionsMessagesJa.ts | 39 +-
apps/web/src/i18n/ja/judgmentMessagesJa.ts | 36 +-
.../src/i18n/ja/localRetrievalMessagesJa.ts | 154 +-
apps/web/src/i18n/ja/modelHelpMessagesJa.ts | 12 +-
apps/web/src/i18n/ja/oauthMessagesJa.ts | 60 +-
apps/web/src/i18n/ja/parserErrorMessagesJa.ts | 54 +-
.../src/i18n/ja/recordDialogsMessagesJa.ts | 39 +-
.../web/src/i18n/ja/recordPanelsMessagesJa.ts | 57 +-
apps/web/src/i18n/ja/remainingMessagesJa.ts | 42 +-
apps/web/src/i18n/ja/researchMessagesJa.ts | 33 +-
.../src/i18n/ja/researchPanelsMessagesJa.ts | 51 +-
.../web/src/i18n/ja/reviewPanelsMessagesJa.ts | 33 +-
apps/web/src/i18n/ja/setupMessagesJa.ts | 27 +-
apps/web/src/i18n/ja/sourceMessagesJa.ts | 51 +-
apps/web/src/i18n/ja/traceMessagesJa.ts | 57 +-
.../src/i18n/ja/workspaceDetailMessagesJa.ts | 24 +-
apps/web/src/i18n/ja/workspaceMessagesJa.ts | 54 +-
apps/web/src/i18n/jaScreen.test.ts | 131 +-
apps/web/src/i18n/judgmentMessages.ts | 130 +-
apps/web/src/i18n/languageChoices.ts | 51 +-
.../web/src/i18n/localRetrievalClaims.test.ts | 26 +-
apps/web/src/i18n/localRetrievalMessages.ts | 302 +-
apps/web/src/i18n/locale.ts | 95 +-
apps/web/src/i18n/messages.ts | 132 +-
apps/web/src/i18n/metricFormatMessages.ts | 480 +-
apps/web/src/i18n/modelHelpMessages.ts | 34 +-
apps/web/src/i18n/oauthMessages.ts | 246 +-
apps/web/src/i18n/pickLocale.ts | 5 +-
apps/web/src/i18n/plainWordingResources.ts | 336 +-
apps/web/src/i18n/progressEn.ts | 299 +-
apps/web/src/i18n/progressJa.ts | 309 +-
apps/web/src/i18n/progressKo.ts | 81 +-
apps/web/src/i18n/progressZh.ts | 302 +-
apps/web/src/i18n/progressZhHant.ts | 302 +-
apps/web/src/i18n/recordDialogsMessages.ts | 354 +-
apps/web/src/i18n/recordPanelsMessages.ts | 426 +-
apps/web/src/i18n/remainingMessages.ts | 446 +-
apps/web/src/i18n/researchMessages.ts | 322 +-
apps/web/src/i18n/researchPanelsMessages.ts | 382 +-
apps/web/src/i18n/reviewPanelsMessages.ts | 350 +-
apps/web/src/i18n/sentences.ts | 2 +-
apps/web/src/i18n/setupMessages.ts | 256 +-
apps/web/src/i18n/sourceMessages.ts | 292 +-
apps/web/src/i18n/stateFormatMessages.ts | 313 +-
apps/web/src/i18n/statusChinese.ts | 170 +-
apps/web/src/i18n/statusEnglish.ts | 170 +-
apps/web/src/i18n/statusJapanese.ts | 175 +-
apps/web/src/i18n/statusResources.ts | 251 +-
apps/web/src/i18n/statusTraditional.ts | 170 +-
apps/web/src/i18n/testOrderResources.ts | 240 +-
apps/web/src/i18n/traceChinese.ts | 44 +-
apps/web/src/i18n/traceEnglish.ts | 46 +-
apps/web/src/i18n/traceFormatMessages.ts | 203 +-
apps/web/src/i18n/traceJapanese.ts | 46 +-
apps/web/src/i18n/traceMessages.ts | 438 +-
apps/web/src/i18n/traceReasonResources.ts | 172 +-
apps/web/src/i18n/traceTraditional.ts | 44 +-
apps/web/src/i18n/uiFormatMessages.ts | 340 +-
apps/web/src/i18n/uiText.test.ts | 32 +
apps/web/src/i18n/withColumns.ts | 14 +-
apps/web/src/i18n/wordingRules.test.ts | 137 +-
apps/web/src/i18n/workspaceDetailMessages.ts | 82 +-
apps/web/src/i18n/workspaceMessages.ts | 461 +-
apps/web/src/i18n/zh/advancedMessagesZh.ts | 12 +-
apps/web/src/i18n/zh/apiNoticesMessagesZh.ts | 3 +-
apps/web/src/i18n/zh/commonStateMessagesZh.ts | 3 +-
apps/web/src/i18n/zh/coreMessagesZh.ts | 12 +-
apps/web/src/i18n/zh/credentialMessagesZh.ts | 24 +-
apps/web/src/i18n/zh/followupMessagesZh.ts | 9 +-
apps/web/src/i18n/zh/historyViewMessagesZh.ts | 7 +-
.../i18n/zh/hypothesisActionsMessagesZh.ts | 9 +-
apps/web/src/i18n/zh/judgmentMessagesZh.ts | 15 +-
.../src/i18n/zh/localRetrievalMessagesZh.ts | 121 +-
apps/web/src/i18n/zh/modelHelpMessagesZh.ts | 6 +-
apps/web/src/i18n/zh/parserErrorMessagesZh.ts | 18 +-
.../src/i18n/zh/recordDialogsMessagesZh.ts | 23 +-
.../web/src/i18n/zh/recordPanelsMessagesZh.ts | 7 +-
apps/web/src/i18n/zh/remainingMessagesZh.ts | 12 +-
.../src/i18n/zh/researchPanelsMessagesZh.ts | 16 +-
.../web/src/i18n/zh/reviewPanelsMessagesZh.ts | 16 +-
apps/web/src/i18n/zh/setupMessagesZh.ts | 14 +-
apps/web/src/i18n/zh/sourceMessagesZh.ts | 15 +-
apps/web/src/i18n/zh/traceMessagesZh.ts | 25 +-
.../src/i18n/zh/workspaceDetailMessagesZh.ts | 6 +-
apps/web/src/i18n/zh/workspaceMessagesZh.ts | 15 +-
.../src/i18n/zhHant/advancedMessagesZhHant.ts | 12 +-
.../i18n/zhHant/apiNoticesMessagesZhHant.ts | 3 +-
.../i18n/zhHant/commonStateMessagesZhHant.ts | 3 +-
.../web/src/i18n/zhHant/coreMessagesZhHant.ts | 12 +-
.../i18n/zhHant/credentialMessagesZhHant.ts | 24 +-
.../src/i18n/zhHant/followupMessagesZhHant.ts | 9 +-
.../i18n/zhHant/historyViewMessagesZhHant.ts | 7 +-
.../zhHant/hypothesisActionsMessagesZhHant.ts | 9 +-
.../src/i18n/zhHant/judgmentMessagesZhHant.ts | 15 +-
.../zhHant/localRetrievalMessagesZhHant.ts | 121 +-
.../i18n/zhHant/modelHelpMessagesZhHant.ts | 9 +-
.../i18n/zhHant/parserErrorMessagesZhHant.ts | 18 +-
.../zhHant/recordDialogsMessagesZhHant.ts | 23 +-
.../i18n/zhHant/recordPanelsMessagesZhHant.ts | 7 +-
.../i18n/zhHant/remainingMessagesZhHant.ts | 12 +-
.../zhHant/researchPanelsMessagesZhHant.ts | 16 +-
.../i18n/zhHant/reviewPanelsMessagesZhHant.ts | 16 +-
.../src/i18n/zhHant/setupMessagesZhHant.ts | 14 +-
.../src/i18n/zhHant/sourceMessagesZhHant.ts | 15 +-
.../src/i18n/zhHant/traceMessagesZhHant.ts | 25 +-
.../zhHant/workspaceDetailMessagesZhHant.ts | 6 +-
.../i18n/zhHant/workspaceMessagesZhHant.ts | 15 +-
apps/web/src/i18n/zhHantScreen.test.ts | 162 +-
apps/web/src/i18n/zhScreen.test.ts | 107 +-
apps/web/src/narrowLayoutRules.test.ts | 116 +-
apps/web/src/preview-timeline.tsx | 1 -
apps/web/src/sourceDisplay.test.ts | 10 +-
apps/web/src/styles.css | 1203 ++++-
apps/web/src/test/jsdomTopLayer.ts | 4 +-
apps/web/src/testSetup.ts | 5 +-
apps/web/src/typeScaleRules.test.ts | 32 +-
apps/web/src/vite-env.d.ts | 2 +-
apps/web/src/workstation.css | 4383 +++++++++++++++--
apps/web/tsconfig.app.json | 1 -
apps/web/tsconfig.json | 6 +-
apps/web/vite.config.ts | 42 +-
config/module-responsibility-budget.json | 33 +-
docs/BETA.md | 4 +-
docs/architecture/verification-profiles.md | 3 +-
docs/releases/v0.2.0-beta.4.md | 51 +
.../3b06e4f87d92_policy_digest_history.py | 21 +-
.../7b06e5a9d3f1_a06_full_project_memory.py | 16 +-
package.json | 2 +-
pnpm-lock.yaml | 1097 +++++
pyproject.toml | 11 +-
src/thoth/__init__.py | 2 +-
src/thoth/adapters/connectors/http_read.py | 4 +-
src/thoth/adapters/connectors/postgres.py | 4 +-
src/thoth/adapters/connectors/s3.py | 4 +-
src/thoth/adapters/evaluators/__init__.py | 1 -
.../adapters/memory/relation_projection.py | 4 +-
.../adapters/models/codex_account_usage.py | 96 +-
src/thoth/adapters/models/codex_app_server.py | 33 +-
src/thoth/adapters/models/http_rejection.py | 4 +-
src/thoth/adapters/models/registry.py | 4 +-
src/thoth/adapters/parsers/docling_pdf.py | 46 +-
src/thoth/adapters/parsers/document_time.py | 56 +-
.../adapters/storage/alembic_migrations.py | 4 +-
src/thoth/adapters/storage/auth.py | 8 +-
.../adapters/storage/field_measurement.py | 8 +-
src/thoth/adapters/storage/object_prune.py | 3 +-
src/thoth/adapters/storage/project_purge.py | 4 +-
src/thoth/adapters/storage/research_queue.py | 4 +-
.../application/commands/investigation.py | 4 +-
.../application/commands/projectpacks.py | 4 +-
src/thoth/application/commands/sources.py | 36 +-
.../reducers/hypothesis_reducer.py | 10 +-
.../services/conversation_router.py | 60 +-
.../services/criterion_compiler.py | 4 +-
.../services/evidence_graph_service.py | 1 -
.../services/investigation_service.py | 4 +-
.../application/services/model_settings.py | 5 +-
src/thoth/application/services/policy_gate.py | 4 +-
.../application/services/public_web_policy.py | 12 +-
.../services/research_attempt_admission.py | 4 +-
.../application/services/revision_diff.py | 8 +-
.../application/services/sandbox_router.py | 4 +-
.../application/services/sandbox_service.py | 2 +-
.../services/source_time_service.py | 158 +-
.../services/tui_session_service.py | 48 +-
src/thoth/apps/decision_chain_routes.py | 4 +-
src/thoth/apps/hosted_review_snapshot.py | 28 +-
src/thoth/apps/source_composition.py | 16 +-
src/thoth/domain/baseline.py | 6 +-
src/thoth/domain/evidence.py | 3 +-
src/thoth/domain/execution.py | 4 +-
src/thoth/ports/artifact_ledger.py | 18 +-
src/thoth/ports/evaluator.py | 1 -
src/thoth/ports/sandbox_capability.py | 4 +-
src/thoth/ports/thread_runtime.py | 4 +-
tests/adversarial/test_anti_hardcoding.py | 4 +-
.../test_atomicity_debt_contract.py | 4 +-
.../architecture/test_ocp_extension_guards.py | 6 +-
tests/architecture/test_post_d4_baseline.py | 9 +-
tests/atomicity/test_permission_predicates.py | 294 +-
.../test_policy_digest_history_migration.py | 21 +-
tests/atomicity/test_project_governance.py | 122 +-
tests/ci/test_ci_python_format.py | 29 +
tests/integration/public_web_test_support.py | 1 +
.../research_measurement_helpers.py | 12 +-
tests/integration/test_a04_stale_evidence.py | 4 +-
.../test_a08_recursive_improvement.py | 10 +-
.../integration/test_a11_policy_integrity.py | 166 +-
.../integration/test_conversation_ui_read.py | 312 +-
.../test_historical_access_verification.py | 18 +-
.../test_hosted_review_dispatch_gate.py | 16 +-
.../test_hosted_review_internet_vs_model.py | 4 +-
.../test_hosted_review_session_isolation.py | 6 +-
tests/integration/test_hypothesis_full_rpc.py | 4 +-
...est_hypothesis_prediction_outcome_cycle.py | 4 +-
tests/integration/test_model_settings.py | 1 +
..._public_oauth_memory_excerpt_regression.py | 4 +-
.../test_public_web_evidence_gates.py | 3 +-
.../test_public_web_policy_update_rpc.py | 4 +-
.../test_public_web_runtime_catalog.py | 7 +-
.../integration/test_research_history_read.py | 59 +-
.../test_research_lineage_publication.py | 272 +-
.../integration/test_research_memory_basis.py | 20 +-
.../test_resource_scope_revocation.py | 52 +-
.../test_restore_apply_atomicity.py | 18 +-
.../test_restore_currentness_consumers.py | 6 +-
tests/integration/test_restore_drift.py | 4 +-
.../test_selected_evidence_query.py | 37 +-
.../test_source_time_connection.py | 204 +-
.../test_structured_context_pipeline.py | 3 +-
.../test_user_activity_thread_read.py | 8 +-
.../test_workspace_restart_storage.py | 17 +-
tests/parser/test_document_time.py | 40 +-
tests/parser/test_test_log.py | 4 +-
tests/test_foundation.py | 2 +-
.../test_research_final_producer_selection.py | 4 +-
.../test_user_activity_projection.py | 3 +-
.../unit/apps/test_source_policy_defaults.py | 2 +-
.../test_connector_capability_routing.py | 4 +-
tests/unit/domain/test_models.py | 17 +-
.../models/test_codex_app_server_auth_only.py | 13 +-
tests/unit/models/test_codex_oauth_model.py | 3 +-
.../test_hosted_review_provider_budget.py | 4 +-
.../models/test_http_rejection_metadata.py | 8 +-
.../unit/services/test_criterion_compiler.py | 4 +-
tests/unit/services/test_evidence_context.py | 4 +-
tests/unit/services/test_model_route.py | 8 +-
.../services/test_retry_model_mismatch.py | 4 +-
tests/unit/test_hosted_review_snapshot.py | 8 +-
.../test_public_web_policy_reconciliation.py | 4 +-
.../unit/test_public_web_query_disclosure.py | 12 +-
tests/unit/test_public_web_uri_boundary.py | 4 +-
tests/unit/test_source_time_resolution.py | 174 +-
uv.lock | 2 +-
635 files changed, 59556 insertions(+), 19425 deletions(-)
create mode 100644 apps/web/.prettierignore
create mode 100644 apps/web/.prettierrc.json
delete mode 100644 apps/web/preview-timeline.html
create mode 100644 apps/web/src/i18n/uiText.test.ts
delete mode 100644 apps/web/src/preview-timeline.tsx
create mode 100644 docs/releases/v0.2.0-beta.4.md
create mode 100644 tests/ci/test_ci_python_format.py
diff --git a/.github/workflows/product-check.yml b/.github/workflows/product-check.yml
index 5df0d96a..115bc738 100644
--- a/.github/workflows/product-check.yml
+++ b/.github/workflows/product-check.yml
@@ -93,6 +93,9 @@ jobs:
- name: Python lint
if: needs.changes.outputs.docs_only != 'true'
run: .\.venv\Scripts\python.exe -m ruff check src apps tests migrations
+ - name: Python format
+ if: needs.changes.outputs.docs_only != 'true'
+ run: .\.venv\Scripts\python.exe -m ruff format --check src apps tests migrations
- name: Runtime architecture
if: needs.changes.outputs.docs_only != 'true'
run: .\.venv\Scripts\python.exe scripts/check_architecture.py
diff --git a/Makefile.ps1 b/Makefile.ps1
index f2ac91d3..28997834 100644
--- a/Makefile.ps1
+++ b/Makefile.ps1
@@ -37,6 +37,7 @@ function Invoke-NativeStage {
function Invoke-BackendLint {
Invoke-NativeStage "LINT" $Python @("-m", "ruff", "check", "src", "apps", "tests", "migrations")
+ Invoke-NativeStage "FORMAT_CHECK" $Python @("-m", "ruff", "format", "--check", "src", "apps", "tests", "migrations")
}
function Invoke-BackendType {
diff --git a/SOURCE_MANIFEST.json b/SOURCE_MANIFEST.json
index 3e83b53a..e4caa863 100644
--- a/SOURCE_MANIFEST.json
+++ b/SOURCE_MANIFEST.json
@@ -1,6 +1,6 @@
{
"schema_version": "thoth-source-preview-manifest-v1",
- "created_at": "2026-11-12T08:53:39.354182+00:00",
+ "created_at": "2026-11-12T11:00:04.245678+00:00",
"license": "MIT",
"git_history_included": false,
"full_suite_verified": false,
@@ -23,8 +23,8 @@
},
{
"path": ".github/workflows/product-check.yml",
- "sha256": "ed0a5e6b47b7cb9ab44e8466020dd5dbb3b42a29413e505e6423f206978dfd7b",
- "bytes": 15413
+ "sha256": "df86ddfabb50266baeff7a08b850c653b072cbc2c6469ac7df6aad449ff692e9",
+ "bytes": 15584
},
{
"path": ".gitignore",
@@ -48,8 +48,8 @@
},
{
"path": "Makefile.ps1",
- "sha256": "f99bc21e4a811cd0f59fda9ae506fb36ab40b50548b30fbc98a06672173f6b60",
- "bytes": 3012
+ "sha256": "2c41986dbd0a19f0ae23755d42577f265052de3919889a3c54442a4b657cfd9f",
+ "bytes": 3133
},
{
"path": "PUBLICATION_STATUS.md",
@@ -91,10 +91,20 @@
"sha256": "e37bba3ad3fc4e6660f06662f5bbba215a998066221a3462493a46a288cb2921",
"bytes": 105
},
+ {
+ "path": "apps/web/.prettierignore",
+ "sha256": "b7ef17791d103aa712ff7f5d2073839cc4891ceb435c962f371668074b971723",
+ "bytes": 218
+ },
+ {
+ "path": "apps/web/.prettierrc.json",
+ "sha256": "6b380994ae171521d31a3e92447de7e80a8cfdff7ced1e4787a26ce98dcfe6ae",
+ "bytes": 24
+ },
{
"path": "apps/web/eslint.config.js",
- "sha256": "c27f4d3c48f6123e1a3755599ef10542df3232665ae7880972a4599919e0234d",
- "bytes": 825
+ "sha256": "0e1ef1041b027e0f109c08420411612e02bb80d90491064c495013cce64643b7",
+ "bytes": 928
},
{
"path": "apps/web/index.html",
@@ -103,13 +113,8 @@
},
{
"path": "apps/web/package.json",
- "sha256": "a7dfe39164c338a07064d6db46ab3cefbc66c4c19afb6df445308c3a6f26e7b5",
- "bytes": 1055
- },
- {
- "path": "apps/web/preview-timeline.html",
- "sha256": "1e2b911ca2370e14a697376180874f4f913bd339d686b24733d9879131374e2e",
- "bytes": 369
+ "sha256": "fd22f4845202e6f567db529f7c0bba840ccd4235f18f12b6956b0f2dd84762a5",
+ "bytes": 1183
},
{
"path": "apps/web/public/favicon.svg",
@@ -118,43 +123,43 @@
},
{
"path": "apps/web/src/App.test.tsx",
- "sha256": "873196460f98808096b798155297ed278277f9bf9aa3684b086acf51d3c91b14",
- "bytes": 1079
+ "sha256": "2cafac7414d1b01c8e6620e33b017ed3c3ea6b06275456c9fa88f7a8588057a8",
+ "bytes": 1095
},
{
"path": "apps/web/src/App.tsx",
- "sha256": "db7f800403c2c1e945cc84197fe7a4831b399f3a7e6d7f52c1e3a3bc1dc772fa",
- "bytes": 1073
+ "sha256": "279d012acbb590f5cac07eb898de059fa6171837ac90d8a2d66323b79630f471",
+ "bytes": 1165
},
{
"path": "apps/web/src/api/actionDraft.ts",
- "sha256": "e35d50411a3a6b1313bb005c9d5333429eba7c8de2b6836636947068e93616c9",
- "bytes": 588
+ "sha256": "6ae1c675d199c26c6f62ec42e7b25dc80339a9b90c055adf58ed772598d0c1ce",
+ "bytes": 671
},
{
"path": "apps/web/src/api/capabilities.ts",
- "sha256": "7374e906ee659d028b1ef235afc3be0112a197f7d8b3b2c0aa6a756e06d8696e",
- "bytes": 2660
+ "sha256": "2d69e2b57c1ca9ee4d2bb11144972124126dfe175821d274d2dbf306f922abeb",
+ "bytes": 2883
},
{
"path": "apps/web/src/api/contracts.test.ts",
- "sha256": "5979e401ee2100d15c963e7835ce8517346c7370f3e5e8e701751989d79f28cc",
- "bytes": 6998
+ "sha256": "598a6f32c21e42468131ba291de951c57859501c8e64eb38c102378463e4c7cf",
+ "bytes": 7808
},
{
"path": "apps/web/src/api/conversation.test.ts",
- "sha256": "fc1561aee7bcf6351c79fa9ed25552df0536796ed2a1f84ff6d5fde058c218a1",
- "bytes": 4457
+ "sha256": "70d656edc70bf232f345751be8c3e6c6c040bb409602e5fa6542cb6164005e06",
+ "bytes": 5415
},
{
"path": "apps/web/src/api/conversation.ts",
- "sha256": "49f1d269b2e6ac86bd94716eb6e26e9bf32ed0055ef339b4e9574b4841c43a20",
- "bytes": 6966
+ "sha256": "65d976bbe3256124c81acb84d9f18fced5d46cc09da81b45ca7b8e3176b6ff6a",
+ "bytes": 7696
},
{
"path": "apps/web/src/api/conversationRefetchDiagnosis.test.ts",
- "sha256": "cd2883a0ff96b305cdc3b1224a990d52bd8263ae01bc9b9492e95b0f0948e947",
- "bytes": 4584
+ "sha256": "19206b1321e22261ba4a07d6974df28bfecc0805606b1db383ede5594cb483d6",
+ "bytes": 5107
},
{
"path": "apps/web/src/api/conversationRefresh.ts",
@@ -163,28 +168,28 @@
},
{
"path": "apps/web/src/api/expectedResultContract.ts",
- "sha256": "d358ea81cd6628d0aa819d1a5b134b3479c8efdffd86b7b80d182866ea33ddb5",
- "bytes": 592
+ "sha256": "02e2f4e18763216146bee9d87e80aea152a9e77f094cff42665fd19a944de4a3",
+ "bytes": 600
},
{
"path": "apps/web/src/api/expectedResultState.test.ts",
- "sha256": "688d1879261fb55c59d3212eee132629c2a69ada95246f78ba863d06e6e23a18",
- "bytes": 557
+ "sha256": "902a169951e36fc76027072c8b640a910115eb95185a496044d21ddf8a3364f9",
+ "bytes": 595
},
{
"path": "apps/web/src/api/expectedResultState.ts",
- "sha256": "09781c7815f76a1a644d36aa73e725178a37220c9ebe82ff047a357c19d1bb82",
- "bytes": 401
+ "sha256": "ea8155fa15dca22c1eda16c26f33ab2da0a69dc726d31213422cf9419c154e40",
+ "bytes": 414
},
{
"path": "apps/web/src/api/files.test.ts",
- "sha256": "d94b8248e2eb161f09e33bb4d39391cd845269c274213bb4a5f5c6533b5a4a95",
- "bytes": 9697
+ "sha256": "3096d3b61def56dd28b06ec3d20836b387733c80d2f981a59a73dafb1584301c",
+ "bytes": 9896
},
{
"path": "apps/web/src/api/files.ts",
- "sha256": "f0a4aeea28b7b76390a172a0b1d9d90a465031ef0305dce54ce00a7fa9cc641c",
- "bytes": 3739
+ "sha256": "c53dfc6a7cab114cd7de495baa72e2be38639580ce11181cc338bf03c7ac9120",
+ "bytes": 3850
},
{
"path": "apps/web/src/api/fixtures/iris-judgment-excerpt.json",
@@ -213,98 +218,98 @@
},
{
"path": "apps/web/src/api/forgetProject.test.ts",
- "sha256": "2606ff8275fa8897157d87342cfcacfa0827f1128c6f12e306deabc88c385c2c",
- "bytes": 1727
+ "sha256": "e5b3e50b3343d42dcd52cca928e87aed1b2b32f0183b8967efd3fff8d2a1c5d8",
+ "bytes": 1739
},
{
"path": "apps/web/src/api/forgetProject.ts",
- "sha256": "c503e78a878e87047b4a4c7cd1c016d502e582ac9d6cbc47f4b4ad9b765ffbc6",
- "bytes": 1956
+ "sha256": "6529deba7efa02d59509e69309cbe15f1af0d2d5ff4347ed0045b8d7e8264aa8",
+ "bytes": 1997
},
{
"path": "apps/web/src/api/groundingNegotiation.test.ts",
- "sha256": "e1013bca0c87fa5c58d0912d6ca4d2c841740b469d1fdc4f9d9e6330038bbfed",
- "bytes": 2826
+ "sha256": "3027cefa028364d77529cd5ec6e115c47dc5995e012a418728ac6e333cc33395",
+ "bytes": 2958
},
{
"path": "apps/web/src/api/history.test.ts",
- "sha256": "b713c346b75d884bbea02747f8cfe7811f3f540d536bf1a9865d993e28e397b1",
- "bytes": 3117
+ "sha256": "b38ae7924482dff195676bba3385829337ce2a8b5bf99d3eed01a4cc56d38356",
+ "bytes": 3549
},
{
"path": "apps/web/src/api/history.ts",
- "sha256": "ffbd41d3f5cd0fd1261593d590435a4ef2396704855fb0576c2080b9f289a767",
- "bytes": 7528
+ "sha256": "0e0144b078d4514a1673431a9a605ba14a677cbbc652efc5e41880276d3ca6d7",
+ "bytes": 8448
},
{
"path": "apps/web/src/api/historyComparison.ts",
- "sha256": "5ae41dbe64cc2c8f23fa1b78cc7237178dce61f49e8a1f8c1941533393d24ac6",
- "bytes": 1965
+ "sha256": "5cebfde2c34ff1c854b16e8c78771e1d0d7a1d17677fa37a9a5ed6c922666ac9",
+ "bytes": 2162
},
{
"path": "apps/web/src/api/historyHttp.test.ts",
- "sha256": "7b9c55d4ed2f9ce457020f7f62b9cbf1222150afb4bcfb54a94ae243161641c3",
- "bytes": 3395
+ "sha256": "84ea88121477319d95e0fd443d6cd53c355c74fad2c4d3bec330f34dde017dea",
+ "bytes": 3688
},
{
"path": "apps/web/src/api/historyModels.ts",
- "sha256": "fbd648972568fc9c8d3d7a30ee7ea2c00e946eb80410f402e611af3315b96167",
- "bytes": 3958
+ "sha256": "44dbfab29d7aef737c4d5394e6a2880b1cf402b3a0795144214149acd0d88ea2",
+ "bytes": 4224
},
{
"path": "apps/web/src/api/historySchemas.ts",
- "sha256": "242be5c08ddf48d468dc8bc8e915a82dc36aceed274e2c5283880abb8004f0de",
- "bytes": 4562
+ "sha256": "0aa118e91af53668f649c175ecbce58506cd392adc3410bb2b0b5f931c9698d6",
+ "bytes": 4690
},
{
"path": "apps/web/src/api/historyServerContract.test.ts",
- "sha256": "0006704c5eb1f0f5bc7ad5cac744fafcb248316597cb3f2c9c2d8781ab8fa286",
- "bytes": 3791
+ "sha256": "6708eb4d827870602c26aab0e75e378bbeef4ff57e811eaddad871ff403ff246",
+ "bytes": 4234
},
{
"path": "apps/web/src/api/historyTestFixture.ts",
- "sha256": "13c56f8ba756533e22f16148a7383480cf0130dea4a6069feb2dd2afdccd48ff",
- "bytes": 10787
+ "sha256": "dc6e83b78fc14859482cb6a39da071c15f1c84a3299378992fcbf84cb1b12e7c",
+ "bytes": 12699
},
{
"path": "apps/web/src/api/hypothesisLink.ts",
- "sha256": "0bb463ad28b1ef4dba3d947a97995bb6fc7f5a22e1cc497cae63931ef9338a06",
- "bytes": 1829
+ "sha256": "ed47194c50d0f65617808fe72ba4ea092e2deb174c60b0267f569f68af94cc73",
+ "bytes": 2004
},
{
"path": "apps/web/src/api/hypothesisSame.ts",
- "sha256": "935f8967ee3bad79fb196b18ac5e0150610aee6517e073582fdab5e9327ff831",
- "bytes": 1390
+ "sha256": "cfbb59619ec8910ecc952af07ce71cac098359e8a5f7e2b237210a81037b351c",
+ "bytes": 1523
},
{
"path": "apps/web/src/api/judgmentRecords.ts",
- "sha256": "e3d3286e76442d5fffe110bda299264f7bc70673c2c7a6758a7cb3bea0366543",
- "bytes": 3401
+ "sha256": "c6ed8052cedd376d5a4d5a5308e81cbd48961f52786d56c091b799192303bf72",
+ "bytes": 3796
},
{
"path": "apps/web/src/api/koreanParticles.test.ts",
- "sha256": "c4195ffa70dc91cd494bf74f48e07683ad325cb07b0b237471c42d451044e7f3",
- "bytes": 3968
+ "sha256": "d3a241321e94d1170c6a20605fb9d134230efe7a57f1b78f7cc1ec4706cfd818",
+ "bytes": 4131
},
{
"path": "apps/web/src/api/koreanParticles.ts",
- "sha256": "ce26c6a2c7aefb1b78cb7d345394b289ac2a1e9b053fcf133cdd69a4d5c0d572",
- "bytes": 2173
+ "sha256": "ace98e852227ef879b1f36270c73cb1bad3cbea51cbecd8c723c4cdf2cb989f4",
+ "bytes": 2181
},
{
"path": "apps/web/src/api/lessons.ts",
- "sha256": "766cc09dd47f8624aa3c9dac94e692607b6ec6c97521b84a5f11a93edad91330",
- "bytes": 1330
+ "sha256": "6d14376f2f10ee53d74d7385f4c3a84f2f266eb94c9e3caec1d3a8a9294ef0ed",
+ "bytes": 1414
},
{
"path": "apps/web/src/api/localRetrieval.ts",
- "sha256": "8bf7d103747457d42443c1ab2148d33749d8602e2b20749ae24efcbbd74ffdc6",
- "bytes": 13080
+ "sha256": "5c2ad477a2e7f3c4516d74b6d42f4e58597a7140de5c4861a9fa0f0d1c9ba6bb",
+ "bytes": 13556
},
{
"path": "apps/web/src/api/localRetrievalInstall.test.ts",
- "sha256": "7d290be9689543bb72781bee84dc411357bc0b72b3bf5f0c7c0390db48591391",
- "bytes": 787
+ "sha256": "254e3329122af9ce2c6cbe3d1085479cfac0f68502ddf564bdf2d020377ea844",
+ "bytes": 805
},
{
"path": "apps/web/src/api/localRetrievalInstall.ts",
@@ -313,63 +318,63 @@
},
{
"path": "apps/web/src/api/localWorkspacePersistence.test.ts",
- "sha256": "38c5717309058387d653f03f438913ee94176a7bef67c0f939b8e6d5f63a6983",
- "bytes": 7017
+ "sha256": "46931de96cd33219f0aae4b04b98b6abc982ff7a5d4989060ba9f535079e5a96",
+ "bytes": 7357
},
{
"path": "apps/web/src/api/localWorkspacePersistence.ts",
- "sha256": "abe5ec7d93dfb8093961f27e800e697933154027b28415276aeea79e2a734dd2",
- "bytes": 11535
+ "sha256": "c55908350dc4d4bc79fc0a44dc31cc85f75f4159a2b56ccf321a00927f2ca6c6",
+ "bytes": 12705
},
{
"path": "apps/web/src/api/parserError.test.ts",
- "sha256": "1a1211dbd9693593b259d84c7d53450a0340a2cd7f3949cd8aaa01fb2748dbb3",
- "bytes": 2751
+ "sha256": "fdac86c94ed48da1a2907b70061392b2ec2ee597c6e697fc2ee47e6329347310",
+ "bytes": 2849
},
{
"path": "apps/web/src/api/parserError.ts",
- "sha256": "da6b62d462be50eac00fab21aee2e92a7001d1bd9b1fd8a81012e35449c63608",
- "bytes": 1126
+ "sha256": "e68817d156ccb558a406781f6f034c2d9c3ad59bdbb3150e318cbd7368b8219d",
+ "bytes": 1161
},
{
"path": "apps/web/src/api/presentation.ts",
- "sha256": "465295ae990306613051deb1abc644b8528e4eaacfe8746db27a30557fd78aea",
- "bytes": 908
+ "sha256": "23d74b0a9acc32a8dcb6d1675d52494f4da735439e90fd9a4fbe9b81bd4bae8e",
+ "bytes": 955
},
{
"path": "apps/web/src/api/progressEvidenceLocations.test.tsx",
- "sha256": "9bcecca32cfa75bc2a242a190d37394066c34e450fadbd86a4d9266266c476ce",
- "bytes": 8037
+ "sha256": "237a54ed01c6521f394491f7bc44dff4774ba34ccdf5b76fc4856e946a005e34",
+ "bytes": 8631
},
{
"path": "apps/web/src/api/progressEvidenceLocations.ts",
- "sha256": "c957c55dc7a7f16a761544e2d7dcf36f544da7dbdc149956ca21135afdda4986",
- "bytes": 3233
+ "sha256": "f6918ae5fffd52868f7eb1e9f3594be785171a2610f917fd35e1a13a5a1bc6d2",
+ "bytes": 3365
},
{
"path": "apps/web/src/api/projectActionFailure.ts",
- "sha256": "7e7322fecbd82e01443f61aea8346e9a6a4a6e1c6a2e7130677f4d2385365973",
- "bytes": 984
+ "sha256": "59dda0072d3a7be769e2a323497c13067596ca89b5f060b3df64da1e7e32c08e",
+ "bytes": 996
},
{
"path": "apps/web/src/api/projectCreation.ts",
- "sha256": "f096ff7604b82a8e89012bdd78724845c081a4f172b118b4eeeb35f97479086b",
- "bytes": 375
+ "sha256": "652ec6d7397e8f19475f5b25d7c8e767caeea7deefb1911909ace2a54dae0d25",
+ "bytes": 413
},
{
"path": "apps/web/src/api/projectDeletion.ts",
- "sha256": "fee05833cf621b845302633a85d3114ce6a59e97cc58c90147bd15551964bf3b",
- "bytes": 1695
+ "sha256": "35f42b1c4dc85d42550c47d3df633a40e6095ad9c3fe9ac00d57c2e77213cf2f",
+ "bytes": 1701
},
{
"path": "apps/web/src/api/protocol.ts",
- "sha256": "467bce7e71a9e7158774f675c211ec90ce7932bc752735e2d5d7f1778a597aee",
- "bytes": 1304
+ "sha256": "d2987155aa88acf42f93873eb3971358f6be74928b77f253937787f57a33c145",
+ "bytes": 1314
},
{
"path": "apps/web/src/api/publicWeb.test.ts",
- "sha256": "9cde3fb6f8d5e64f3efc154fc6d291ca2638f5ba678563de4e20047d8941a69c",
- "bytes": 1910
+ "sha256": "2495a36180cc693b6031112da0cca1aa9f5228a865902ac9b14ce1b177f2ab0e",
+ "bytes": 1935
},
{
"path": "apps/web/src/api/publicWeb.ts",
@@ -378,48 +383,48 @@
},
{
"path": "apps/web/src/api/readAccess.ts",
- "sha256": "390436f6ce64e7fd1535c628955a91c889ea0353259bc33348aed2ff193c0659",
- "bytes": 863
+ "sha256": "000a7dcb99b29f70e7772d18acfb4827f4425dd9e7698947a303217c2eac22ab",
+ "bytes": 892
},
{
"path": "apps/web/src/api/recordViews.ts",
- "sha256": "c449e4b22a88ec53223496e43f612078954788e3f4418496f2bf634f018cd72c",
- "bytes": 3910
+ "sha256": "88929276a98e860744fcbfd4f2d08226505a56670682156c7491c1b51d06814f",
+ "bytes": 4977
},
{
"path": "apps/web/src/api/requestLanguage.test.ts",
- "sha256": "66f3499b111fbe17069716c43fe3c6ae5cb36163edba5a31168144ee65d0720e",
- "bytes": 1759
+ "sha256": "ba0509307fd58aeb47ded11b93c5f1de5b20b4afa391855255adc6d180ce0040",
+ "bytes": 1805
},
{
"path": "apps/web/src/api/research.ts",
- "sha256": "7b71132015d5f4310c5d011f43c2d2fe27550117c42d781106a7a884fa6770b8",
- "bytes": 11957
+ "sha256": "e79692e07d59b63ff11e1332cf1b20fe708a5ca29f481a5ba90f9aba6c1653cf",
+ "bytes": 13065
},
{
"path": "apps/web/src/api/researchFollowup.ts",
- "sha256": "2ceee4e426e6130538356d0194725e62797c0e2d923e6b1de18a1c1ee7d30cd3",
- "bytes": 8170
+ "sha256": "5a47c087eb1a752ae51553b146793c28e3a07f0876c52cb140e26676d141c299",
+ "bytes": 8561
},
{
"path": "apps/web/src/api/researchProgress.test.tsx",
- "sha256": "6e9d7175cede4c8951a1e1ae850b27591c27b3bee1614634b2158a4e13f1112b",
- "bytes": 13450
+ "sha256": "28fc97e141b14a09b89b69a73be5193e687f7d6ee5ee43c8294adf4db6010bae",
+ "bytes": 13725
},
{
"path": "apps/web/src/api/researchProgress.ts",
- "sha256": "554a96c5bbbef465ac74d51a96b35ffd5684054727c20a363679d984dc133181",
- "bytes": 14943
+ "sha256": "4df4cbc452fdbb8f4e0039847e0656398b51d54e9bd494af6bebea6dd7db19f5",
+ "bytes": 15124
},
{
"path": "apps/web/src/api/researchProgressCharacterization.test.ts",
- "sha256": "3e8643260280bf70b2ecfb836f37b6b9ec58fcfe138685821244c3313ece70cc",
- "bytes": 32390
+ "sha256": "3d9b5b43b30524f9dac738c0300838ee2d7ec2267faa2f8ae0eebd6c62f681ab",
+ "bytes": 34041
},
{
"path": "apps/web/src/api/researchProgressText.ts",
- "sha256": "6bce616d471e53bcd6010bdcc80734aca0c9e6da797fe5a4a42423dcb9da2fd8",
- "bytes": 2470
+ "sha256": "4cf1bb5d5afafd7b2a682bca7dedab6fa1dc566a17b4f2f219713be310788ba2",
+ "bytes": 2592
},
{
"path": "apps/web/src/api/researchStatusRead.fixture.json",
@@ -428,248 +433,248 @@
},
{
"path": "apps/web/src/api/researchStatusRead.test.ts",
- "sha256": "6e5ece71720a3d2fc2c56578ad345dc0ffa24a076a8d27de98309a580295da89",
- "bytes": 3497
+ "sha256": "744ca7bd88ff50a8539ef280ccb314df9e8b213c5c1e0f60b565061ccc79b640",
+ "bytes": 3625
},
{
"path": "apps/web/src/api/researchStatusRead.ts",
- "sha256": "326c894c5c6ae4b345afd9feabfbdd2a209741139ac6b6fa119888b96fc449ee",
- "bytes": 2217
+ "sha256": "07cb63f2ac7a8f4116a3ec1f400480223950d399d076b64f2b94b814af4f7949",
+ "bytes": 2320
},
{
"path": "apps/web/src/api/responseLanguageDefault.ts",
- "sha256": "606cbb9b87628420561ec2e180cf339ce7279192a579abb090fa780a5a4992b0",
- "bytes": 2920
+ "sha256": "e41179129ae1fd45bc5b0d1bcf4874b363d0401f57cc3ec873891de675087b98",
+ "bytes": 3002
},
{
"path": "apps/web/src/api/responseLanguages.test.ts",
- "sha256": "0d95dd9bacf520b981b6054c68a9dbdb44f47b277efd302896d144c9999bc82d",
- "bytes": 3176
+ "sha256": "6c3eafbd18f7466fce5e9914f60aa51fe2ecf922218ac3b522de249ae536a06f",
+ "bytes": 3343
},
{
"path": "apps/web/src/api/responseLanguages.ts",
- "sha256": "3b90d25f3007c5525a486e8ac7002ba7d21718c1fad6d9552eb15bf33a46711a",
- "bytes": 1321
+ "sha256": "575b62c01bca6a235655462c08a35d47d8b615724396c4e40ff253c6dffbf791",
+ "bytes": 1325
},
{
"path": "apps/web/src/api/restore.test.ts",
- "sha256": "3be583e0164d621e730783908b9b8ad8d4b4f03cb33acf4d9d4f368af8734cc9",
- "bytes": 2665
+ "sha256": "c395f88db4132680f1f14e3aca42c23f656bf54af463ec28edfddbe9b2672549",
+ "bytes": 2899
},
{
"path": "apps/web/src/api/restore.ts",
- "sha256": "c24781bc4e370071c6f9d43243a10dead5b00f295b6428f998fa3a8bed7d5e1c",
- "bytes": 7198
+ "sha256": "963f7cdb433ce2abd5dfee2fa6f0de5b132a0abbbcc7550712adce47275f2a9c",
+ "bytes": 7748
},
{
"path": "apps/web/src/api/restorePending.test.ts",
- "sha256": "bf60662f26a6cacea2890ad600402bf04651eb00cb7a5e550731cf2fc8005286",
- "bytes": 1416
+ "sha256": "cfa690adc32f3dec1ed33a4d8880919a299d473af15c4a6947ce5c3d759a8021",
+ "bytes": 1490
},
{
"path": "apps/web/src/api/restorePending.ts",
- "sha256": "fb8be50e8ec3746e16e2d30de7a986f165a792be11f40b4f4f9f80a5cf11f49e",
- "bytes": 2046
+ "sha256": "89786f0a224ba7d1ed3a2109b00f2721346ca08d6a18206defa1172027542fb7",
+ "bytes": 2207
},
{
"path": "apps/web/src/api/resultEvidence.test.ts",
- "sha256": "4ce2f6102016c86ad2d1881c0362ecb458502d5045cf2274b4b15d64b9bb9e07",
- "bytes": 8096
+ "sha256": "a646f2d3d3a2fd6f54998271345bdf40dc9f545d40bdea2792ff6d4fab464993",
+ "bytes": 8481
},
{
"path": "apps/web/src/api/resultEvidence.ts",
- "sha256": "9a8911d113e4061226b443bd19dcb1911dc41141dbb115f8be7985b7d4e3f249",
- "bytes": 9992
+ "sha256": "309132212197807e98761f1e15273aaca9a35e31ada2d9795f26e12221a442d3",
+ "bytes": 10760
},
{
"path": "apps/web/src/api/resultEvidenceIdentity.test.ts",
- "sha256": "d7b609f666956244cf99affd3457ec3d1f244ab3c8d5b8e462243e4906659bd7",
- "bytes": 2053
+ "sha256": "6e1feba8f317a65db99fc74bf765f44e0b4dc9a5fd1cb3ae4eba3c3c9bcc2364",
+ "bytes": 2273
},
{
"path": "apps/web/src/api/resultEvidenceIdentity.ts",
- "sha256": "7a1ce9cf49128806639a4d8ada09ec225393731ce5b16fee1783dafc5acc96cc",
- "bytes": 1117
+ "sha256": "5eced84674d5a774f9bfbff08e51a943d611f597821b75c601ae7a4b4647a3f9",
+ "bytes": 1154
},
{
"path": "apps/web/src/api/resultEvidenceRuntime.test.ts",
- "sha256": "0ab224cd47a485f159a5fedac254c9991cfe6cc69f9504ac02f8c9c9030b07de",
- "bytes": 5818
+ "sha256": "729ca4dd4246f1a9b1164049ff95e1abcabd5f70ee585647715ff2e54047b3b3",
+ "bytes": 6175
},
{
"path": "apps/web/src/api/resultEvidenceServerContract.test.ts",
- "sha256": "ff97c094eb2aa32e4bab7233b013241415e7cfdb845374dcc1f2c041d81b70b7",
- "bytes": 1824
+ "sha256": "d935bed9ba26c680e0b1643161a91abb8e515df4b0f260001c28aace4287ceeb",
+ "bytes": 1970
},
{
"path": "apps/web/src/api/resultEvidenceTestFixture.ts",
- "sha256": "4e98a83e57071a4c8d698e4ff83a81996541299abaf367153074984032cb8d8c",
- "bytes": 2298
+ "sha256": "4b860825e9678a6bb19e904ded4342f48a336bb1bf7aa3ff744e530a66e8fd30",
+ "bytes": 2889
},
{
"path": "apps/web/src/api/rpcClient.test.ts",
- "sha256": "15696d96c2db4a52d346008bf23effcd80f5b1af8327e701bf823fd3a6aa86bf",
- "bytes": 3407
+ "sha256": "03ee038c488f646c7aa05e7d0cc3f4ff4595281bec6f5829f95535f2b2f2af34",
+ "bytes": 3649
},
{
"path": "apps/web/src/api/rpcClient.ts",
- "sha256": "12f5678729977b7c30d7fa9b9235a9e42924ab6d8514df5c1837a15fe15a2bcf",
- "bytes": 5366
+ "sha256": "0a5884f1e900f1eaab9fcd9f3d59b1e024182ad8ef9c0b6a9e44e0381f2f525f",
+ "bytes": 5650
},
{
"path": "apps/web/src/api/rpcTransport.test.ts",
- "sha256": "bdad07ce594d2ab0a4ff347be7bde0e99292addeb960fc968204b17303183059",
- "bytes": 2444
+ "sha256": "a7268e77e89ea3755f6bea22b95d269125327c20c48cd31780320cd642c14cfb",
+ "bytes": 2625
},
{
"path": "apps/web/src/api/sourceBindings.test.ts",
- "sha256": "dfa8b1e9e4232228b186d8135b63bd4c10c7a10db6817e6f0349026891be04e6",
- "bytes": 2781
+ "sha256": "4af25528ae66b027c93062c3e7e94b66dfa3ce23f8cc2da7be443ae64b8ea04f",
+ "bytes": 2867
},
{
"path": "apps/web/src/api/sourceBindings.ts",
- "sha256": "ad7a97f03d5d8b5ef21bf02a78f2206e684bab80fdd2f3091b0a1e4481b665cc",
- "bytes": 2353
+ "sha256": "576425c7b03c0fd6b17f4603c7877d8c8accd0b82433ad22d7165813b16878ad",
+ "bytes": 2395
},
{
"path": "apps/web/src/api/sourcePolicy.ts",
- "sha256": "0f77ba848deea718c6e487e36e4182d6042579f43a41a1e3eb7956fc113e6a00",
- "bytes": 1551
+ "sha256": "f0d533c761e2e247191976e5693796fd0d04bf49244a5d84cf5325864505d436",
+ "bytes": 1887
},
{
"path": "apps/web/src/api/sourceTime.ts",
- "sha256": "653ba002f47828df8104bd451a7e2c929a32972560cbf1f1e1223b7afc640bc7",
- "bytes": 2894
+ "sha256": "07b6591000643fc42bba1b4476afcd279fc343258178a86cf3c298388248fa49",
+ "bytes": 3042
},
{
"path": "apps/web/src/api/submissionPersistence.test.ts",
- "sha256": "e994a680cd955479530f2bf26905f6a2bf6089bb2324c8e7d0971e71d9a8970c",
- "bytes": 1777
+ "sha256": "e05ad958deecca61716740c8f2e4d237457cf127abc2ea4b462547caeb4898c8",
+ "bytes": 1829
},
{
"path": "apps/web/src/api/submissionPersistence.ts",
- "sha256": "15e8e976473543b7d711e9d1d03d05f542fad6a4e630b41ea98e1eb90dace3ce",
- "bytes": 1672
+ "sha256": "f434c5179c5e4a1062e0f3a7f58ea42b4ce9bc11850217b4717a50ce8f51a013",
+ "bytes": 1723
},
{
"path": "apps/web/src/api/threadName.ts",
- "sha256": "c6fa278a70f0058255af371359f8c56720696c9a48836ef4af01f43c4d2983fc",
- "bytes": 281
+ "sha256": "39c6587798019941391c6de3ddc9e12ab4214a7d0cbd2ade442e270529588372",
+ "bytes": 286
},
{
"path": "apps/web/src/api/timelineExample.ts",
- "sha256": "1d8ac2a17ffa2a86720c472ce953d89bb2e1e5d99d614e5b86ebec1a0b2d4cbc",
- "bytes": 8913
+ "sha256": "75fdc6200f686c295eee542af1903a7d0bfb73b0c961ec41ddf4151a8a474c43",
+ "bytes": 9366
},
{
"path": "apps/web/src/api/trace.ts",
- "sha256": "39f426e38364313f5fcf028abd542a7fd2e397cdea797fb5eb2e079c80d70636",
- "bytes": 5312
+ "sha256": "680f6ba9d00056dfff7dbdf78998735720a0cefa13f82efb979b3eac23661272",
+ "bytes": 5643
},
{
"path": "apps/web/src/api/uploadTypes.ts",
- "sha256": "d0cb6292986348f493ad07aa3ad7da1cac8b9802c25ee505eff5138d960a11f2",
- "bytes": 2480
+ "sha256": "95d122006fdd86a423f43b566f8f5f9377635cc52baccbe165fc3621c577bef2",
+ "bytes": 2549
},
{
"path": "apps/web/src/components/AbsenceChecks.test.tsx",
- "sha256": "1c653dab4036dfcd6ad4b26b1d4803bd8986ecf89497f75ff6de3845b6839170",
- "bytes": 3921
+ "sha256": "d16fef725e0536e43439350a8482ea3a3b1d883c8966ad77b5322e1776bca67d",
+ "bytes": 4256
},
{
"path": "apps/web/src/components/AbsenceChecks.tsx",
- "sha256": "aeae6af6da7f9493430df897c91910f9a0a664c4366fbfba554210da095bf879",
- "bytes": 2420
+ "sha256": "633b2cf79eae9c7949c32ba5e6288e77d0e2d219e162d96cf85a542e49e88e33",
+ "bytes": 3007
},
{
"path": "apps/web/src/components/ActionCompare.test.tsx",
- "sha256": "05970a448c26f20a08897aff452a932a8caec1a116b4ddefadc795b3cd7d986a",
- "bytes": 5175
+ "sha256": "496349162c0fb35f15fe345caf7e1317c64636fc91b453be463694997e7bd456",
+ "bytes": 5296
},
{
"path": "apps/web/src/components/ActionCompare.tsx",
- "sha256": "1a3100723911d7996b8978cfe588e8002d00c7e1465896e7e300798d4f637dca",
- "bytes": 7913
+ "sha256": "cd59774abf7ec24d4175a9bdfa6391997643961d56c964aefa8c75029cd244d8",
+ "bytes": 10336
},
{
"path": "apps/web/src/components/ActionCompareApproval.test.tsx",
- "sha256": "263b69b8545050ab59f872605bb7e3a67655712f683a26046984954957da6da5",
- "bytes": 3171
+ "sha256": "6ace2f6e077cc51ef4acae0664310cd911f2c6ce7d085c07076a37b868fec71a",
+ "bytes": 3439
},
{
"path": "apps/web/src/components/ActionCompareLive.tsx",
- "sha256": "7bfa054898226f661b707d38238db40794e523818d4d00accb59294997e82149",
- "bytes": 629
+ "sha256": "077f8e89bda5e0b789c188c5a5ae2bf68460584beca0afa665d378c1497dac36",
+ "bytes": 678
},
{
"path": "apps/web/src/components/ActionCompareStale.test.tsx",
- "sha256": "f173e32970071d5c9ce6ce01b3ef9de50cb8f6030b1165605391dae075964028",
- "bytes": 2371
+ "sha256": "a07cadb4f449fe9ae5f30b98061f746113941292b7cc1be89fc1005983376775",
+ "bytes": 2458
},
{
"path": "apps/web/src/components/ActionEffort.test.tsx",
- "sha256": "6c853309bae39c97c85d6b315b5706a93981df9bd8501e8edf1632403dda2f72",
- "bytes": 5361
+ "sha256": "c1ec435b498a61aaf35b463691e63877e0e29eb2ec0ae6ddfb5361bcf56ef376",
+ "bytes": 5725
},
{
"path": "apps/web/src/components/ActionEffort.tsx",
- "sha256": "aa141c01fa78ac53c9b7b48917811262b012b5fb2bac40fa23afc4d8b8d6fab0",
- "bytes": 7298
+ "sha256": "7e3cc45ba1c69be82e0db005326eb50a39ffd325617218c17a46efb2cb19ca7e",
+ "bytes": 8857
},
{
"path": "apps/web/src/components/AnswerBody.tsx",
- "sha256": "2585378ff73ce37e67619e1ef98578d97d44b582514293386482c5234dd23b17",
- "bytes": 1698
+ "sha256": "128e8cf51515e210027208c8a8294c02cedf0a0bb196dfc9f924f8bed2a5392e",
+ "bytes": 2061
},
{
"path": "apps/web/src/components/AnswerCitations.test.tsx",
- "sha256": "e99ef91de9538f951746e7ad686eade831bbcf9c1a46a4fb4aebff9cbd79ea84",
- "bytes": 6503
+ "sha256": "2d57c6a3392d590448bfc2d83e9c510fcde710faa2c726ddf46b67af9d883dcf",
+ "bytes": 6878
},
{
"path": "apps/web/src/components/AnswerHold.tsx",
- "sha256": "f1c97c24eb7d599af5d100dff4f4166e3e7aea66c3ad681a6884e172f4787b5b",
- "bytes": 1473
+ "sha256": "20afd5dce96f1c333ef1a30f026a08963e38e256d332bdb9097a4ff42e3d3019",
+ "bytes": 1590
},
{
"path": "apps/web/src/components/AnswerReadability.test.tsx",
- "sha256": "d13f3aefac9ce16e5d2b22f7ff67c599dd4806006b9290850843a25830fa8431",
- "bytes": 7996
+ "sha256": "8de8cdeae8a781a5b3183e9def3e3c9bf4661d1f66b1fb36b4051de75edfe94e",
+ "bytes": 8341
},
{
"path": "apps/web/src/components/ApprovalCard.test.tsx",
- "sha256": "b3427e26f27c43168ec3af555be2f8034b099df35c45b127560931d171aac29d",
- "bytes": 10073
+ "sha256": "185a9c2dbbe65b40eb46304c8bb52c4b26a63780ae3c6422088391ce6079bdd1",
+ "bytes": 10934
},
{
"path": "apps/web/src/components/ApprovalCard.tsx",
- "sha256": "8b23817eedd59397eb7620e7056f1ee5b877d05d2f4f9dbdb286a841662cec2d",
- "bytes": 4041
+ "sha256": "e8cecc633005ec58a4ab64b5f513d0ef427495de9d313965f62ff0f51d17c1f7",
+ "bytes": 5042
},
{
"path": "apps/web/src/components/ApprovalSection.tsx",
- "sha256": "bf08777238edd106d654d8ae7df18ad5c83cb561178498a8b4f36f652551be64",
- "bytes": 3539
+ "sha256": "595723abcd7a87b6db733d19295433e7bd3e6b1c422cede497abf17b2fa2286d",
+ "bytes": 4144
},
{
"path": "apps/web/src/components/ArchivedProjects.tsx",
- "sha256": "cb52bc40c6695899cab85a49639ea903abcb6835775e4a63a6863677357a187c",
- "bytes": 5399
+ "sha256": "22be9ff855a897c6eff241c6bd2abcb165ae400b6c1aff70f49e220a2850f429",
+ "bytes": 6385
},
{
"path": "apps/web/src/components/AsyncDraftAndCitationRegression.test.tsx",
- "sha256": "07efd926f074b11cdc16338e631389b0247e2f275078c2ed7f0cb9fe64d9aa6c",
- "bytes": 5597
+ "sha256": "77c77f74f1d9b89e8f433c782f8d34b5033693a3402588804340accc51ff79f6",
+ "bytes": 7104
},
{
"path": "apps/web/src/components/AutoRetrySwitch.test.tsx",
- "sha256": "fd5f1d658ecd4ee2edeadb5247fa41cfb91294a7313e660326c7936ec091cdc2",
- "bytes": 3922
+ "sha256": "cf4234aa079ab22f761cde3da61e86b7a15e0c9cf2b8c8335a357cb066aca175",
+ "bytes": 4096
},
{
"path": "apps/web/src/components/AutoRetrySwitch.tsx",
- "sha256": "7d2260493556bf8867347bfc6ac11d6bf5b147470f1704a430201dc894ae4558",
- "bytes": 1686
+ "sha256": "fce3a8e643c96cdd56feccd5dacae1192f150253bd1d0fdcae4895a47f233cba",
+ "bytes": 1885
},
{
"path": "apps/web/src/components/Brand.tsx",
@@ -678,658 +683,658 @@
},
{
"path": "apps/web/src/components/CapabilityWorkbench.test.tsx",
- "sha256": "16fb85837d55221ba833a49b2aa4284ea0379132f995d4ca8ad0b3d2d90d4832",
- "bytes": 7546
+ "sha256": "d79554b86b0546ecd1956e56a243c8f448d858b8cc4bbb9e908bd3f823c9a57b",
+ "bytes": 8264
},
{
"path": "apps/web/src/components/CapabilityWorkbench.tsx",
- "sha256": "3fb5fa29b51d9f96e1e7b5958029ee46dbfe5e88750ff6457988ca066c94cf48",
- "bytes": 7655
+ "sha256": "2be17b70f1b52ffe0517c76201d1a4eb25c4f23cab623f65d95bcff73806d607",
+ "bytes": 10038
},
{
"path": "apps/web/src/components/CheckedObservations.test.tsx",
- "sha256": "4df7de4dc7054ce32db715bb2881094da6fee10163f7ee486545d5a8822cf4a2",
- "bytes": 3537
+ "sha256": "ed22b8e15dd00a9261b724471614d2597c60ae9a5d192d3a24673d7650af58a3",
+ "bytes": 3802
},
{
"path": "apps/web/src/components/CheckedObservations.tsx",
- "sha256": "d7f109b14d859791db51d8681e0f188c72a23a668e121c2ee9ec0f4c6ecfc7f4",
- "bytes": 3034
+ "sha256": "1030beaeab29a996e445a54f3859f05703014858efebb0cf7bdc96250d2bd1e2",
+ "bytes": 4114
},
{
"path": "apps/web/src/components/ClaudeCodeHelp.tsx",
- "sha256": "0cbcaee43f82474fabd37c9c63a46a989d8753b78c19b216d10e461c0000fa80",
- "bytes": 499
+ "sha256": "f8c8e7e00534b04db132d8cef81c3d75ea97decf22349408305e88ce5219983a",
+ "bytes": 535
},
{
"path": "apps/web/src/components/ClosureMark.test.tsx",
- "sha256": "6b0f20f51ad531d3d93614d42532ba8589c3ec9759e0fcb39bb9b153a3b75295",
- "bytes": 3672
+ "sha256": "26244b412d35d9da63c4e4375094a6b73b1fb8fd856ae8e8004e3217af9248c1",
+ "bytes": 3829
},
{
"path": "apps/web/src/components/ClosureMark.tsx",
- "sha256": "f91e3925fbd261d3419928a7e56e786b3aac0a2b866619d4e35b5bc9f6d96bd5",
- "bytes": 1974
+ "sha256": "81f230db43dec6adac6beef443d6c3bbb11eaa0c8f4deffcddc9c3c951970165",
+ "bytes": 2457
},
{
"path": "apps/web/src/components/ClosureRecorder.tsx",
- "sha256": "7595ce6fdf29ea84c77c833382e8fc3f120cce6f992ddc4e5ed572809d6d04a0",
- "bytes": 4341
+ "sha256": "7e13684f86821c15a560409e8da4601dcd4bdc35c3e10c60ec6e371f4e8dd89e",
+ "bytes": 5437
},
{
"path": "apps/web/src/components/CodexToolHelp.tsx",
- "sha256": "3885de8a8bb716122c15267df56b132fa94234d121193fa9b829e74cc1886092",
- "bytes": 497
+ "sha256": "d5062870a8dbdf2e601044414a395a86e5629ff3a23368ebf35441dafc3a81f1",
+ "bytes": 533
},
{
"path": "apps/web/src/components/ConfirmDialog.tsx",
- "sha256": "a067f479569fae667b62abe0338a1854c4368f063c3e37e2efbdc133f0ad61c4",
- "bytes": 1592
+ "sha256": "ad0e08eb979aafe5c6a5db5f952f1c118ce9232e751b26c6e8683b587b5dd19c",
+ "bytes": 1611
},
{
"path": "apps/web/src/components/ConfirmedFacts.test.tsx",
- "sha256": "7a8eb8e7bbd0e85a06c9ca7a100a1ced37fc6f71e1a3a44c71ac970b89ea0599",
- "bytes": 4697
+ "sha256": "1ac91f8b4c60d1f7a72b0d539bc5f02bd50d38b6fc1b1a6cb60c75fc2b8926b3",
+ "bytes": 4827
},
{
"path": "apps/web/src/components/ConfirmedFacts.tsx",
- "sha256": "38448e96054a958fff7de6b013fba9c5544edabc889fa478abbd59dd2e46b7e1",
- "bytes": 3155
+ "sha256": "6ef770af709422c6217e95c8e28ad469b7c6b71b1a2e6e33834dc7f6a16dc7f7",
+ "bytes": 3545
},
{
"path": "apps/web/src/components/ConnectFailureInWorkspace.test.tsx",
- "sha256": "10764debaada2c1714a24b285d371fbfba96b40b55f8e091a03939f0d915ba75",
- "bytes": 7518
+ "sha256": "6e842bfad199f4bac1ed1845f786677832915ad84d6661d64e05bdef8541cc73",
+ "bytes": 8522
},
{
"path": "apps/web/src/components/ConnectedSourceCard.tsx",
- "sha256": "2b8a516f96ace07f1c6b567164158cf938b23e0a0c75cbccba66f8b43e200115",
- "bytes": 2146
+ "sha256": "abd80377d2a187fda01087800d3912c7e92c42b66eb10acaff5435fc33f9331f",
+ "bytes": 2424
},
{
"path": "apps/web/src/components/ContractV3Screens.test.tsx",
- "sha256": "cc58a42347636f4d117d503d4f7495606cff79c5541327049f6ff543104ffdc0",
- "bytes": 8555
+ "sha256": "51ef0d6bd4e5d7a2946364442577ae6882eb8542fa698de804cdcf140105d2bc",
+ "bytes": 8907
},
{
"path": "apps/web/src/components/ConversationEarlierRound.test.tsx",
- "sha256": "03c9fc9d85345b51721aaf68481f616ee7b4227e4de4a14f4c958467229b1a71",
- "bytes": 4994
+ "sha256": "3dd43fd4a224ea81e81e2fda1a9bf4283c16e2742009b3a968dad480a9b8a38f",
+ "bytes": 5292
},
{
"path": "apps/web/src/components/ConversationRunningTurn.test.tsx",
- "sha256": "695fc6abf91ecfdc7f4f0a39645db3e41d2ac9ccb5377948500a4a9857b4a83d",
- "bytes": 6581
+ "sha256": "dc303c36e98647b184b1ff651e980e4e634c6c420e9a08e6921a907c7a549a81",
+ "bytes": 7128
},
{
"path": "apps/web/src/components/ConversationTimeline.tsx",
- "sha256": "8136396921dc0b61bf737ad57460c4b897326ebc069d9b24543d0c4937f3fa4c",
- "bytes": 14619
+ "sha256": "36f5e36c1f60855023b331abc2b04efb36a0e3b390fd4c9c2b389440edee4dbe",
+ "bytes": 16658
},
{
"path": "apps/web/src/components/ConversationTimelineAutoRetry.test.tsx",
- "sha256": "e05aa667cf229ac13079d734f1640881107f437dde34609d9cd455ca8b7cabc5",
- "bytes": 4391
+ "sha256": "50e083c876f500f64d88d2feda5aaea3ff8d5a0ca02ddba92329bcdae95f4eb9",
+ "bytes": 4842
},
{
"path": "apps/web/src/components/ConversationTimelineLegacyEmpty.test.tsx",
- "sha256": "cece4bdb55c1272d1846fa28cf8788f060f519d2cbe439c52d7ec5080dc53859",
- "bytes": 2504
+ "sha256": "3dc026d35365ab613a588ae8f3b59eaf1f5267c396a609ec883a7e8c3f319fab",
+ "bytes": 2615
},
{
"path": "apps/web/src/components/ConversationTimelineRefetch.test.tsx",
- "sha256": "a3c38da9b9f267c8cdb03ae7f32f8182af14e1535a9fe0367ad00ba20ba6e57e",
- "bytes": 10359
+ "sha256": "7b28df77268cf65cd14187deb6bba470d160ebdabe9c510537b7a29b353df662",
+ "bytes": 11397
},
{
"path": "apps/web/src/components/ConversationTimelineSynthetic.test.tsx",
- "sha256": "f2555226a0c32a7416487f81ac817118e0ba22ebe3b6355ed17008600a0561c8",
- "bytes": 3879
+ "sha256": "f04d903ac567c5fb8ed832606c6bf5040be527fae4196775cd03801ec4081e7e",
+ "bytes": 4028
},
{
"path": "apps/web/src/components/ConversationTurnView.test.tsx",
- "sha256": "7098c53ca1dca8fecee5937b30c1ca4c34d1bebb156ae7717c995b0d1819c7d4",
- "bytes": 7338
+ "sha256": "2c80a628ec424178e3a4328439c05917b8f60b4613bd0b639165db4046588b2b",
+ "bytes": 8068
},
{
"path": "apps/web/src/components/ConversationTurnView.tsx",
- "sha256": "408c1fb7c86f0afc1348ef5898287a4862a989d2127fae5bb07f43a9d91cc9a9",
- "bytes": 3793
+ "sha256": "3b0f7b39a17bb2ea5aaf4aed3c583a9d81d4ab3bf6b0b603af74cdd9ee989fc1",
+ "bytes": 4401
},
{
"path": "apps/web/src/components/Disclosure.tsx",
- "sha256": "a4276000ac565b5f51798a28d0f2fc2ac73493c2b7206ea68a1c7f48548a8fd6",
- "bytes": 659
+ "sha256": "53187e7aed978a5b4ec610195e447e0021c87f533936d51cea4521372cc64d2c",
+ "bytes": 765
},
{
"path": "apps/web/src/components/DiscriminationRecords.test.tsx",
- "sha256": "296deb6f6993012c5a31a42acf7f51e0ac2d381305fe4787149839c98e77065c",
- "bytes": 7175
+ "sha256": "41a32f0a47e90d07b098250b0ac6e61697e22bfd82f47c882a623e12e612b206",
+ "bytes": 7670
},
{
"path": "apps/web/src/components/DomainRecords.tsx",
- "sha256": "19752aee19b8894a20df5c6ab8a84aa681aec53fd0391f628780cecaceab34ef",
- "bytes": 3827
+ "sha256": "4b44e7c3502ba616121f6895e56f7bc3633ca8647a75b1be3e8f65190ad1be1f",
+ "bytes": 5030
},
{
"path": "apps/web/src/components/EffectDeclarationFields.tsx",
- "sha256": "39b6979d7faf4812145a32d4e3f283e0dff8e400d89c5eb9a9ea87c38f10d980",
- "bytes": 1432
+ "sha256": "4fda8d29b233110da5e751ae5f15158f5b07785fc8c99b79a2fc979cffa1f5b3",
+ "bytes": 1680
},
{
"path": "apps/web/src/components/EvidenceRail.test.tsx",
- "sha256": "5e4067422e6c06824b676281a1d73f3da661cd71a584e3cfbd7593db0f2f2ab2",
- "bytes": 2364
+ "sha256": "f53c4bbda2dbaa77b6de7d8aa20f08a5f81dea9a8de33343f666151cfbb73566",
+ "bytes": 2560
},
{
"path": "apps/web/src/components/EvidenceRail.tsx",
- "sha256": "bfea1fc7080953fd63d72054a5e34e9708c060689a1427b2f1d5d3648adf3c66",
- "bytes": 5112
+ "sha256": "6177f5c79e8b1950547efe575298088921b0a9ed880911d9a0a44902c2799342",
+ "bytes": 5520
},
{
"path": "apps/web/src/components/EvidenceUseSummary.tsx",
- "sha256": "4ac88a4545af939ed287911107b1a8ab72e2a1c577b768209026c13c84d9fc25",
- "bytes": 1235
+ "sha256": "fbef14c947ff9727de83072f61b9649d606225d88762a1a222f0e4b0c30d03f0",
+ "bytes": 1483
},
{
"path": "apps/web/src/components/ExecutionHoldNotice.test.tsx",
- "sha256": "9241cbe3d0115ccadcbcbc90c6dab56526ef5eb18fba6412bf887aec1ac2cb4d",
- "bytes": 8452
+ "sha256": "b2fcd172fd10bdbaac17f60411e0e163766ba9c205d73fff1dc5e7f511d4d8e2",
+ "bytes": 8921
},
{
"path": "apps/web/src/components/ExecutionHoldNotice.tsx",
- "sha256": "5444174743d902e1c97a86bf29d35c793318497fa4834849f5e3f9b8acf19c93",
- "bytes": 5350
+ "sha256": "a378dae0fc32df6148730ea1ba305195ff691601c3f64833491a7b05466278ea",
+ "bytes": 6312
},
{
"path": "apps/web/src/components/ExpectedResults.tsx",
- "sha256": "ee13e530af53d34d678b9dc583db6608be9455e0027d74666724535cc445197f",
- "bytes": 3031
+ "sha256": "1da45125c80edfb2124a851691eed05c93076b897c9d029ccb227f2179a73214",
+ "bytes": 4079
},
{
"path": "apps/web/src/components/FileConnectionPanel.test.tsx",
- "sha256": "31939fe160bf28062c677dadced11aca7bbe164024bc179fe76bc5f63312dfda",
- "bytes": 16126
+ "sha256": "2dd0adc28944c70f22227e58919357c8d8f16cd03f0687707999aa1f504a81f8",
+ "bytes": 16851
},
{
"path": "apps/web/src/components/FileConnectionPanel.tsx",
- "sha256": "3aa25b36baf537cb883b1ba7f078639a1ffca8ee2e2287d21a992e13289a24f7",
- "bytes": 10943
+ "sha256": "8d42ebbd697349fd02fecfbee5d433d0731bb994f18d14ddbe6bb94212cc0487",
+ "bytes": 12633
},
{
"path": "apps/web/src/components/FileConnectionPanelParserError.test.tsx",
- "sha256": "f0aa6794ac4f95425328a9be9cac3e4454203228548abdcb675699db5a27b58c",
- "bytes": 4096
+ "sha256": "37edc06ba5b418bda0e91c6742760dff93245542f8c5dfee44486771733ba349",
+ "bytes": 4306
},
{
"path": "apps/web/src/components/FirstRunSetup.test.tsx",
- "sha256": "e9a0b2637b2063e01913e668804996039111e874dfbf3fc465dd426e38acc70a",
- "bytes": 100718
+ "sha256": "194d8b726b2a494bfee3e34c74f809511de80c69d6153b00fe1de3f478560c7d",
+ "bytes": 109202
},
{
"path": "apps/web/src/components/FirstRunSetup.tsx",
- "sha256": "54a2e46203e7f1ac45d1b200725346c7d26b936d367eb664a3a0ef96f03f39e9",
- "bytes": 16893
+ "sha256": "2d872dc919a2a1b2bcb4f62b9a2e4818500b49a444077f76ec81dd1e0fa4a890",
+ "bytes": 21281
},
{
"path": "apps/web/src/components/GroundingDisplay.test.tsx",
- "sha256": "5f007c12a1a2d9631a7e1ce60f8a86e0c43801cc7a66aa569f535515982872be",
- "bytes": 2898
+ "sha256": "d8306552efbd2b7821b2e3a3dab7e30b9d770c3e76d97de1548d3b27d5ad2365",
+ "bytes": 3229
},
{
"path": "apps/web/src/components/GroundingNotice.tsx",
- "sha256": "e9d96ac6a17d801142554edbc9934325e519209ad41645952e391ba587c5608f",
- "bytes": 648
+ "sha256": "d8028e4c992dfe6c2082a17c29138e6ec2ee4bd258de8e617fcaa94eb0a5ba30",
+ "bytes": 708
},
{
"path": "apps/web/src/components/HypothesisCompare.test.tsx",
- "sha256": "fcce6c8fd20a89b844518a0395a66cd8afc8f334bb60d6fbfe2cd61234d8de8f",
- "bytes": 8536
+ "sha256": "7f68151e5386cf744592b083a3d223ad5383de9048dc508e6e437eea180c770e",
+ "bytes": 8851
},
{
"path": "apps/web/src/components/HypothesisCompare.tsx",
- "sha256": "44856caa4450d160966db974320dc998c657084dd20c7efc8c59c8c5ffe8eae7",
- "bytes": 9561
+ "sha256": "5470ba950e4af6752cf8d3729ae7f6090e886da4989446dba33cd9c4a3639d11",
+ "bytes": 12884
},
{
"path": "apps/web/src/components/HypothesisCompareDraft.test.tsx",
- "sha256": "1b41360c2c32fc6f6d3783904395315567ea02e7fbc9bb0bffe83350580093c5",
- "bytes": 2517
+ "sha256": "f611b1acfca5392395caeb8284f4d873e18a60070c4be946d30b028f414a6b9e",
+ "bytes": 2672
},
{
"path": "apps/web/src/components/HypothesisCompareLive.tsx",
- "sha256": "839cea04e84b76ead5bb847701172877b8259496f96210ad79d39224b0b1637e",
- "bytes": 4400
+ "sha256": "1fb3ba2daecf10544be4fb2de9269f444faac930a10d58e7f5e985dc9bd3c60e",
+ "bytes": 5296
},
{
"path": "apps/web/src/components/HypothesisContractSwitch.test.tsx",
- "sha256": "8f59233c433408a935bce18447b3cf0358c2843be5662e193e2886df5d6ef6a3",
- "bytes": 4119
+ "sha256": "036831fb8fcc8dd3292bd67ca19bef49e231a3665b457da41c75b8cdb638256f",
+ "bytes": 4317
},
{
"path": "apps/web/src/components/HypothesisContractSwitch.tsx",
- "sha256": "04ddd61b8cdda61e32387efa4edfd33a1e91cac122045894be11851053dde53e",
- "bytes": 1763
+ "sha256": "638e8c73bb26bf16171c0d3cbb617ae07b3d77b71339fab20a315ac38fade1dd",
+ "bytes": 1986
},
{
"path": "apps/web/src/components/HypothesisEvidence.test.tsx",
- "sha256": "39ec43303ba37231722cb609424b1eb10fa1f417cc624dceb789a5ab37c87f05",
- "bytes": 13956
+ "sha256": "9ab344e073ec62a8d2666e4044c628558d0df987500e394ccb83bcd3f0859e2b",
+ "bytes": 15192
},
{
"path": "apps/web/src/components/HypothesisEvidencePanel.tsx",
- "sha256": "5b6de517c2d6c6d503b055384021fbc752ba07bc70acf81b957db9e49a8fcb15",
- "bytes": 1685
+ "sha256": "9295dc729fd9a38affd38b2665fff5d21d9695f0cb92d60151e71a4095616a82",
+ "bytes": 1965
},
{
"path": "apps/web/src/components/HypothesisFields.tsx",
- "sha256": "ba326db7af1c356752cb319cda73e8f4a4b7fa1bd992a6031aa94f383dcedb9a",
- "bytes": 2104
+ "sha256": "78c71a09f94d6d98ff13d3fd31c94a5cf8086d34a0c4484dae4dea92a8e3a3e6",
+ "bytes": 2446
},
{
"path": "apps/web/src/components/HypothesisLinkNotice.test.tsx",
- "sha256": "7b2490d25984332094e7fbd8be8b292b138ed1f45d3ff5610a049d4c00d75d1f",
- "bytes": 9213
+ "sha256": "b8f5c5d4db9a082ef8e430938c0c90ff7a3d2406916d13eda55d37a431693197",
+ "bytes": 9573
},
{
"path": "apps/web/src/components/HypothesisLinkNotice.tsx",
- "sha256": "1f3273487e64bcdc7312d779b6ed5e50ab6f24696644645c7aebd86bacc7825a",
- "bytes": 2690
+ "sha256": "cbfecf93001ad7475cb4e577c5684aa577baa0be50f61107921b077173702902",
+ "bytes": 3109
},
{
"path": "apps/web/src/components/HypothesisLinkScreens.test.tsx",
- "sha256": "02f0b441af246a666addb63bdd717f395c88479e8d296353c96de55c4824b049",
- "bytes": 3093
+ "sha256": "c949b134b49c8596b522080697186b73362cf604653a4e84727a613f62adaba2",
+ "bytes": 3216
},
{
"path": "apps/web/src/components/HypothesisReview.test.tsx",
- "sha256": "9ed16b30a10acb02c7f31013c67e6133c9f0536f5d6d8a9615122800ada654fa",
- "bytes": 8119
+ "sha256": "ea78d2ff94b0846f20f07b5db9a399e5521f3040e1b8b1857f4cc237317e4c4a",
+ "bytes": 8615
},
{
"path": "apps/web/src/components/HypothesisScreen.test.tsx",
- "sha256": "6634a3640d05342515a3fdc6e652f62e993e95df6f653f86f5c4246fd9742471",
- "bytes": 10652
+ "sha256": "68b5fcea35b5250e0f182ce66d13f80c401612b969ee2bdbe89cac9cfba6d5fb",
+ "bytes": 11191
},
{
"path": "apps/web/src/components/LanguageScreens.test.tsx",
- "sha256": "9811f97941d1ed77931328b64cab200d4bfc38aabb3501c88a72a2a1860ba5e0",
- "bytes": 5504
+ "sha256": "a8823e7e1310ef6e6eddc104cca61b1ab25ee3fb3403287635d91bb8ca264053",
+ "bytes": 5724
},
{
"path": "apps/web/src/components/LanguageSettings.test.tsx",
- "sha256": "b918f89be63b350f73898d912c512c6f76a4c44173e7997996fbb2359ab429ba",
- "bytes": 18466
+ "sha256": "b3dd7872f4580cd6ff83a9959a358375906a8317b982ab0c29a35b592846f0ce",
+ "bytes": 19505
},
{
"path": "apps/web/src/components/LanguageSettingsCard.tsx",
- "sha256": "ab61dda20da3ba2ae6420b48fc75965bf27899a3c8b0796dc57f85bc98153c13",
- "bytes": 2870
+ "sha256": "1ea8ebfa8532698e747b8be1bcec330a68debaf152ddd17c9616f072389f3599",
+ "bytes": 3342
},
{
"path": "apps/web/src/components/LessonRecall.test.tsx",
- "sha256": "32520ec35f4e8f350b6f008a7c2a98888eb7a6e0ccc8610d3768f3f77050296c",
- "bytes": 6831
+ "sha256": "918d75f5b3546998eeca98f0c4cacb58ca03384e7bdb3926853401513f29f3e1",
+ "bytes": 7841
},
{
"path": "apps/web/src/components/LessonRecall.tsx",
- "sha256": "b6602023fc895bb6633136452b3e82549b0a76b4bdc08c4e80775ade8bd7ff92",
- "bytes": 2091
+ "sha256": "fb51bf39595fa57b313ccc1e08e48ca4161084050c07d4d3b3607dc852d5a2f7",
+ "bytes": 2578
},
{
"path": "apps/web/src/components/LinkRecheckDialog.tsx",
- "sha256": "f7139e3bce1dfead3d663cc6b35ea0c6e0b3af820c010b4cdc5f380cb12026cd",
- "bytes": 3907
+ "sha256": "e66ad3408299226fe867f02bfd7d4d8d5581feebaeccb9194230301418a92383",
+ "bytes": 4743
},
{
"path": "apps/web/src/components/LiveProjectWorkspace.tsx",
- "sha256": "12e5701eb723a065204cef20d37476503d8125be00d5c087dff4b1e458deaa0d",
- "bytes": 28033
+ "sha256": "55ffb72028d07ec2509750f5964550100994f1fd96ef1c82209a5fe28f5ce799",
+ "bytes": 35489
},
{
"path": "apps/web/src/components/LiveProjectWorkspaceConsent.test.tsx",
- "sha256": "1e5837f0ac55a66537e1da61918d9e9cc65062136439817430ba01586b6a5f7d",
- "bytes": 7732
+ "sha256": "8e15c4f33ae7a34d49051d1b7782773fd5d49a67725b30a38e2e42187a578817",
+ "bytes": 8401
},
{
"path": "apps/web/src/components/LiveProjectWorkspaceInvestigate.test.tsx",
- "sha256": "44728f19f1c4f79f96fb94d6221614fb990963448206f09c982d4066d144a92c",
- "bytes": 11595
+ "sha256": "7012dc2c93d36908e0ca155603ea04083300f212bf9ec7e473eb2421e83dcc4a",
+ "bytes": 12730
},
{
"path": "apps/web/src/components/LiveProjectWorkspaceReadiness.test.tsx",
- "sha256": "bbd428b57403d28b64cafbc4e0f82c0e3b3296c7f62aa4f7ed3fa119d0665fc3",
- "bytes": 8276
+ "sha256": "af63a05d54639230e3c48143b23b792d511659bc80bd4c8c63dacfa5d7b8bcdb",
+ "bytes": 9111
},
{
"path": "apps/web/src/components/LiveProjectWorkspaceRestart.test.tsx",
- "sha256": "c2acb9dfd8885153a4a3392090ce742ed999b5c1a63adca5da024749a6e1abc0",
- "bytes": 43945
+ "sha256": "82ae36f7f93f184cbb0e8daa7717911db53c1a498f12b250834ab116dfa656e0",
+ "bytes": 47124
},
{
"path": "apps/web/src/components/LocalRetrievalSettingsCard.test.tsx",
- "sha256": "f1b02c08d2998748876f30d84daa6a881fc4475fef17e9e569fc9349b0aacad1",
- "bytes": 41354
+ "sha256": "d12109500b9671049dafe427bd6d07bde2307719aa4bf84fcd3532af6f93b733",
+ "bytes": 43000
},
{
"path": "apps/web/src/components/LocalRetrievalSettingsCard.tsx",
- "sha256": "0367452313cb4bc80c3577dd817d99e45dbb4c6602eb53f8d50c247788f47fb8",
- "bytes": 24584
+ "sha256": "3940b7ef33eca802c8e315e124e16e4014c8d633e09941bde1ffc24283c1bfda",
+ "bytes": 28333
},
{
"path": "apps/web/src/components/M7LiveReadDenial.test.tsx",
- "sha256": "cbd3c44db6daa194ace574bf14a357ee2b34c5a23257fda3484f99904f06c877",
- "bytes": 12994
+ "sha256": "2a37e838d4ee85e91137ca8ec2d63c8b2da2fb457a300921a7212f7b2d1bb9bd",
+ "bytes": 14769
},
{
"path": "apps/web/src/components/M7OwnerSiblings.test.tsx",
- "sha256": "b9b504e23f258d747a5d8e152ba07815103fdcf5f0e4d646d3b6b71a45e11cb8",
- "bytes": 14761
+ "sha256": "a55e6130d55f03d7051674b660b557478d48337c8ad1ce8543143878de7489d2",
+ "bytes": 16329
},
{
"path": "apps/web/src/components/M7ReadDenial.test.tsx",
- "sha256": "b4adb23006bee5ee86fa5adea26149d2ea253ca302ccc9032f4e6eeea3032916",
- "bytes": 12304
+ "sha256": "b6fef56cfea0e710974b39f52c1d43fd186c052c8ec95cc022f97ac9dfc2ff8a",
+ "bytes": 15566
},
{
"path": "apps/web/src/components/M7RemainingOwnership.test.tsx",
- "sha256": "798ee0761fffcebbe394d9ddbc9a695caf53ff8151b53b3e3c77e1b76945230d",
- "bytes": 17284
+ "sha256": "8317834c7569295dd8c9e6992038c0a12503338e37e643ea0df1ebe3ff7a854a",
+ "bytes": 18860
},
{
"path": "apps/web/src/components/M7ScopeBoundary.test.tsx",
- "sha256": "691942b71196c3790d2801def080d7dcd492dc8362f5727162bf65fe425b288f",
- "bytes": 4844
+ "sha256": "c11bd5485e30c3ca111dfb9fd8743988a0359b719f4cfd81acf45d9f88bae5ee",
+ "bytes": 5139
},
{
"path": "apps/web/src/components/M7StateOwnership.test.tsx",
- "sha256": "ac31b0eb3c3bf60c463a080520ae2bd154ddb7b64bc4f27f8cec5e2dd14c10e2",
- "bytes": 14978
+ "sha256": "5936f27a734f823de61ae9fc1ceae3e87f270448d84f3f9301cdc311c2cd46eb",
+ "bytes": 16672
},
{
"path": "apps/web/src/components/MemoryEditDialog.tsx",
- "sha256": "7aee50c3685b58327767cac435474ebe0d32eb88c4360494908ab4838a096f3a",
- "bytes": 2829
+ "sha256": "a4225c056bf53f6993b1dc3abd3a151809416086d23d2f4586b6ae95e6489a08",
+ "bytes": 3546
},
{
"path": "apps/web/src/components/MemoryInjectionSwitch.tsx",
- "sha256": "798038719832ce305f45661f2cb26420a80d3f69946e7e8cf3e4162e31e5e3ee",
- "bytes": 2308
+ "sha256": "09996ee2454320f97c6de48b888acfe0f6e91c07b61e920ee4868a05978be914",
+ "bytes": 2564
},
{
"path": "apps/web/src/components/ModelCredentialPanel.tsx",
- "sha256": "9416b4bdfac6f4c1f5ea67c2d46d66fd96bbb054ee7cd795fbd12f896c79870e",
- "bytes": 7209
+ "sha256": "d4b8a248031ea586d5625bf3600a7cec90f3b200d47ee288337ce5a21a63747a",
+ "bytes": 8210
},
{
"path": "apps/web/src/components/ModelSettings.test.tsx",
- "sha256": "0b7dcfe90074e28a61dae40fe6a38c1a88ab8a6fbebd2fe08ae87870f560507e",
- "bytes": 28719
+ "sha256": "f13b2db303de19e511aad15972c5281582976fe5bca0480b584848ba6020b97a",
+ "bytes": 31280
},
{
"path": "apps/web/src/components/ModelSettings.tsx",
- "sha256": "0757d29a8dcddc0302d93749b7b3395766e44fde1131ae14a022dc5a857cf104",
- "bytes": 10724
+ "sha256": "47892ceac94dc13d860689e91a58c209317683909739dd9871c34622b669a2f8",
+ "bytes": 12292
},
{
"path": "apps/web/src/components/NavigationDrawer.test.tsx",
- "sha256": "06a669a705a06426260af646f6160fab438411c3264142d2d66c4829e5499f26",
- "bytes": 11617
+ "sha256": "9f7cf1082c4a87012a819952e894bf626176cd48438013de5ef533751115087f",
+ "bytes": 12675
},
{
"path": "apps/web/src/components/OAuthLoginFlow.tsx",
- "sha256": "c5fe884c283a1698982d39a52083aeb79ca3592f61e6681a97c40d7bbac35966",
- "bytes": 18361
+ "sha256": "465fea02e008ddc6ee1334bbc49cfb1521811d8448848984ef821b2fa832987a",
+ "bytes": 21390
},
{
"path": "apps/web/src/components/ProjectActionNotice.tsx",
- "sha256": "2fa033b5c5f8a1511445eb3c860f0a5c1b16c4524b52541a2329bd8faad310b5",
- "bytes": 577
+ "sha256": "ee839fdb51347a1da131e0054bcab119b4302e728f378e9b79ff61201e25c3ad",
+ "bytes": 608
},
{
"path": "apps/web/src/components/ProjectArchiveRestore.test.tsx",
- "sha256": "8b7a0227a4702fcdb272822a930fc622371ad0a926200b3a39955cfe8213c8c0",
- "bytes": 16946
+ "sha256": "2284484f5437aa9f77df3258fd9b30b9bae3ea98096dcc38077bb5fde9143557",
+ "bytes": 18442
},
{
"path": "apps/web/src/components/ProjectForm.test.tsx",
- "sha256": "6bcf342d5e95785ffa4970491fdd39f219b43bc131ac266896406a247c04d154",
- "bytes": 1607
+ "sha256": "5248de8aac879bfe32e107fd39efcdf63da51d7bd4f1b95a2c3a070ca1fd3dee",
+ "bytes": 1665
},
{
"path": "apps/web/src/components/ProjectForm.tsx",
- "sha256": "9e767d7ebe9428945219d1a34d9772a44c6584ad921fa00a4b2a20497dcef33f",
- "bytes": 3838
+ "sha256": "3a4c0d599de363e4e78ae0a60509fc1921e605bffd6eeca0945da00668143772",
+ "bytes": 4334
},
{
"path": "apps/web/src/components/ProjectLanding.test.tsx",
- "sha256": "7320a8b86b880d32d0bf66d06452c0ca1ecaf72b743fc5c5c76786b9c1758cea",
- "bytes": 1103
+ "sha256": "83f642a6c2d6422be071372b0b9557b47af9b29172ad96cdba4f2782fbafdb10",
+ "bytes": 1137
},
{
"path": "apps/web/src/components/ProjectManagement.test.tsx",
- "sha256": "5008e8fe33ce588e3c93adb50de1bfac50db769d36487cb7a7101dcd2c567554",
- "bytes": 13755
+ "sha256": "7bbfc2ae9dad7e4aed3614f7ea0d2dbdd66a012d870b4d14a0905266fa9a49be",
+ "bytes": 14508
},
{
"path": "apps/web/src/components/ProjectMemoryPanel.test.tsx",
- "sha256": "d09f0e62b7f7e00ffccd5853a0b1e2ba5be564519926c4642cd272768f7ac24f",
- "bytes": 17389
+ "sha256": "9ee0dcf7f8b52a00cb4d2997fd6f477814ca241792db1e8cee62d797b6568642",
+ "bytes": 18543
},
{
"path": "apps/web/src/components/ProjectMemoryPanel.tsx",
- "sha256": "7aecdfa4c4744131e32f9b4248b681456327633b942950efafa661cea8d98397",
- "bytes": 6268
+ "sha256": "a26221ccf42758f41f99f27ceb9ebb3f79546e816a3b215f4306a4607f083df6",
+ "bytes": 7739
},
{
"path": "apps/web/src/components/ProjectNavItem.tsx",
- "sha256": "dfd32ef3fd63949ae31509bb3a558cf0a55f86ee13e45c7be698036467fbf7ce",
- "bytes": 1721
+ "sha256": "8da10b7e29e66e898ae560712a863eab155c1d2947ce773f2d9eb744dba54528",
+ "bytes": 1951
},
{
"path": "apps/web/src/components/ProjectPermanentDelete.test.tsx",
- "sha256": "2944322e29a8969f4d429b1209a9103596dd12ac0bc4d820ab5843ed1373900d",
- "bytes": 12584
+ "sha256": "c5288fb1beaa2cb5da94798c0055ad8cb9915665837e7c2a2a90737f8840b993",
+ "bytes": 13276
},
{
"path": "apps/web/src/components/ProjectPermanentDelete.tsx",
- "sha256": "a676887bae1283d3350ef0d1fb66bcca55582d609d467a0b5b80f12979d3c57b",
- "bytes": 3095
+ "sha256": "9a6201b8a063fc8e89fb5bd225c0f742473f777d3b0f724e429bbe0c9a74dfc4",
+ "bytes": 3769
},
{
"path": "apps/web/src/components/ProjectReadinessCard.tsx",
- "sha256": "c58cf2436e88b254585bce41aac6506c9185bf0d9482973fa73b7a6c09d4c247",
- "bytes": 3386
+ "sha256": "fabfb0c488d721f68e9d15abf857685a1dcf2e3ed6d5dc60ee4eb6a329bed29c",
+ "bytes": 3780
},
{
"path": "apps/web/src/components/ProjectSessionPages.tsx",
- "sha256": "3ac79479989184e9a69b18ef3e99f41adde096a01c9c7301a52e13bf8808d083",
- "bytes": 10736
+ "sha256": "ba30f1536e7d578abade97edcb8b376686aede3d688ee7751d9d23a4b6029c86",
+ "bytes": 12920
},
{
"path": "apps/web/src/components/ProjectWebSetup.test.tsx",
- "sha256": "19bf24b5e1aa2ba577a6e0aa0874a34b64e18f4be7a276c50be7039303e27962",
- "bytes": 8357
+ "sha256": "49b3053bde61faf55f8738445b5447ae8715f2a34a1a7b4440800eecbf3df079",
+ "bytes": 8388
},
{
"path": "apps/web/src/components/ProjectWebSetup.tsx",
- "sha256": "9124a46f5bf4a38c433adfb5f826a9a93097fa7ac18444735a2bb2500e3b0473",
- "bytes": 6655
+ "sha256": "8765924ed6bd663f793b4e17042b0f70ca574e270f0a951b27c57a2d4b723996",
+ "bytes": 7016
},
{
"path": "apps/web/src/components/ReadableList.tsx",
- "sha256": "a92d79cc6453308c2da2aefd2ff34d52b1e469c578f3a62855912779a460cc23",
- "bytes": 1232
+ "sha256": "aecdf7dde040b7e7c9f09b4156d532641cd208d53a225de694bd5021b2e7b4f0",
+ "bytes": 1373
},
{
"path": "apps/web/src/components/RecordInspector.tsx",
- "sha256": "5d1f49240f07f81ffc0d294caae26a3abfe37c0674e44de46059fce59f4e41e2",
- "bytes": 1769
+ "sha256": "536c93c422479885a6b46d4efe054381660b3aa4cc6ef1ae0a7c608b26b92efd",
+ "bytes": 2080
},
{
"path": "apps/web/src/components/RecordedStanding.test.tsx",
- "sha256": "8dbccad7188e7b6531ec3e4876bc6c7be0343a32c01d9137253f6623fa808cbb",
- "bytes": 6376
+ "sha256": "0ee1c9d2a72e1943b4c60adcbbfcca1f56da0cbb7100ae7d2773405b4da77d90",
+ "bytes": 6749
},
{
"path": "apps/web/src/components/RefutationConditionsPanel.tsx",
- "sha256": "3592de59fa44f421774cdd017ed55f95efdc90c62080edba755ba705c9cc20dc",
- "bytes": 3467
+ "sha256": "e12c8d66db77ca37bb951f9f73f60aa93ff5806b8fe1e9d2573816ea79904cf7",
+ "bytes": 4140
},
{
"path": "apps/web/src/components/RenameDialog.tsx",
- "sha256": "fa06451e1faf5d610428cd71a56367730c231ef4e94336adaca2ea1aa60c43a2",
- "bytes": 2181
+ "sha256": "cb0db1672bc6b9b8021f85f42433e6ec32a84aeff5fdaa2e3e4d9741b7099511",
+ "bytes": 2355
},
{
"path": "apps/web/src/components/RequestLanguageFlow.test.tsx",
- "sha256": "99a9f70f186ad24d073cdf22ccfcb3e8fcc1e904a1df1aca3caa868e2d5d6b24",
- "bytes": 4630
+ "sha256": "1c8e3b7e39398a1141cc04a69412bc6e78efd0be6905d2f23638a947a82a3bdf",
+ "bytes": 5158
},
{
"path": "apps/web/src/components/ResearchActivityStrip.test.tsx",
- "sha256": "2c0e55f727fbae8f1c8a33f192743ce0344af406a59069069b15e19ccf7829e8",
- "bytes": 5115
+ "sha256": "527c784d12485e0900f20e6772462585cf6901d34eeb2d8178ca50651232491f",
+ "bytes": 6098
},
{
"path": "apps/web/src/components/ResearchActivityStrip.tsx",
- "sha256": "32a14e8f90833cb43df126499387be3df04840717104e1a62316e0ee61c0e836",
- "bytes": 9719
+ "sha256": "0433cacd394d9176563b079d14dc1a76c0cbdc51ca723f926a5256de4d3d4e4a",
+ "bytes": 11170
},
{
"path": "apps/web/src/components/ResearchCancelledPresentation.test.tsx",
- "sha256": "b851dad7f54f494625719f6a6da1f0419706723bae4b0c77410553d9e16c4899",
- "bytes": 3003
+ "sha256": "2e334be1041a5024830f46a1fb176483a1c158d7342b3e1aa3992ead67c68148",
+ "bytes": 3144
},
{
"path": "apps/web/src/components/ResearchComposer.tsx",
- "sha256": "08aaa902b280612188a637ad19fa7bdb763334a75a13b88d6f56366347698903",
- "bytes": 9361
+ "sha256": "f3728ef6941cd5c5687218e54b053613364c5b479bd0c36a10be03dcbdc7efeb",
+ "bytes": 11302
},
{
"path": "apps/web/src/components/ResearchDetailPane.tsx",
- "sha256": "29047d0c9974f89b9179b052a8e02cc038bfdaf9ef407303c19c9ba13a3919d5",
- "bytes": 5300
+ "sha256": "7fb74efbd7498e597c58d1d21f135472050bed41e460ca448afd2f85ff8c2435",
+ "bytes": 7538
},
{
"path": "apps/web/src/components/ResearchEvidenceDetail.test.tsx",
- "sha256": "59bc0277811633a703c92aac1d34b28ffe0b1a92a5f5f40058067cd0558d7da3",
- "bytes": 11490
+ "sha256": "52512abc92a55888801befb354c933c085b6b320f2f5ef62a5dde4b84ac6e318",
+ "bytes": 12400
},
{
"path": "apps/web/src/components/ResearchFollowupSummary.tsx",
- "sha256": "acb7eea96e3f9e9fb97d7a9acb2dbad1fb44075c5606bcaa24189c02794981bd",
- "bytes": 7271
+ "sha256": "29301bcedf47f8e843b528955b02d1cea930a385a5cf5c88e1e0325986add591",
+ "bytes": 8912
},
{
"path": "apps/web/src/components/ResearchHold.test.tsx",
- "sha256": "63fd95bb3e2e9f442a1189cff79b906c16d6ceec9a8e7ce8ef4d44de3868fe6b",
- "bytes": 7648
+ "sha256": "48fe09bc194ae34375dcf9c38bcee715628fbc0352328bf0f1f536aa22286afd",
+ "bytes": 8161
},
{
"path": "apps/web/src/components/ResearchLiveProgress.tsx",
- "sha256": "8e4e5ba3c13972410180e5d1d7540d33dea5705d6d8427974aae0cc542c9a51a",
- "bytes": 4342
+ "sha256": "2a0b552197738dbe1d636206b72c6be560c8372bd304928b7b450b9bc36d561c",
+ "bytes": 4956
},
{
"path": "apps/web/src/components/ResearchProgress.tsx",
- "sha256": "23c07314317363431cd7cc328c47839cb1006272cca952ea56fef3cc74f2e0d2",
- "bytes": 3143
+ "sha256": "d42fc3baa6c9b64e8df4e4954e3c726f5a3b29e051bec871cca2a185e03d06ab",
+ "bytes": 3771
},
{
"path": "apps/web/src/components/ResearchProgressStop.test.tsx",
- "sha256": "12cc74b145fc2d557fdc57a8821d1ecb92567fd1ac2fccbbc6359fe0bfd74acc",
- "bytes": 2432
+ "sha256": "c3cc05dd211c2d0b467e02a090d0a011d67ef08f85b86a8f31413261ef75a66e",
+ "bytes": 2495
},
{
"path": "apps/web/src/components/ResearchResultCard.test.tsx",
- "sha256": "9c6784e23bd9a36d718c237e042afcd0215c5873e6f8eb2c2fc4eedbb4c50545",
- "bytes": 8842
+ "sha256": "0ed191a1f6acae3bea3d89506ef21a4da9ead0e76eed3595f985502e08046f6b",
+ "bytes": 10469
},
{
"path": "apps/web/src/components/ResearchResultCard.tsx",
- "sha256": "2b0c9f3492f68b79f12cc64ed370d439dc925577175da94b1aaf0e0574223813",
- "bytes": 9512
+ "sha256": "9e45591a694922f89e050507a60d3af4bcbdcbe19cc2de9420f68b5a73c681aa",
+ "bytes": 12455
},
{
"path": "apps/web/src/components/ResearchSidePanel.tsx",
- "sha256": "bcdb3d9bfe291d06ed3c6e647f7aa983150c1755dca86520f706470bf9c68c12",
- "bytes": 2898
+ "sha256": "63d5311660e193e54fa359d036c4bd11ce8a52f13bbae7cf8ef9fa70c3bbaaad",
+ "bytes": 3043
},
{
"path": "apps/web/src/components/ResourcesPage.test.tsx",
- "sha256": "e867dba8d2646895313bdfebe65244abf38ce6a469a0baad355a6d9f93c3328e",
- "bytes": 6747
+ "sha256": "e2ea57c35ef979817fa108e0281b1399705406cababca125e295f7b06c1532f5",
+ "bytes": 7179
},
{
"path": "apps/web/src/components/ResultCardOnce.test.tsx",
- "sha256": "0b9ddeb1633417558b061bbd50eaf2becc0f54daa2ccf6487da1c91b83fbeb54",
- "bytes": 13886
+ "sha256": "db94ffc0819a7128835b1d4ce4191a65381979e4d794f6a799052b74ad8207b5",
+ "bytes": 14377
},
{
"path": "apps/web/src/components/ResultEvidencePanel.tsx",
- "sha256": "07047caa788c375c59e74f854f9f5d34000a85397880c0430f6fef59cc9569e5",
- "bytes": 4777
+ "sha256": "8ce8e749af5e0d06897710853d6a9efe0841a95a51c45a371d53a4da93ef7de1",
+ "bytes": 5694
},
{
"path": "apps/web/src/components/ResultExample.tsx",
- "sha256": "003a535a68cc39c3c0ee3c0ba9f3fccd53acc1026b95391ac1ac1f38a5aa47f8",
- "bytes": 7881
+ "sha256": "b8e44e4666896ada6fff52a286240fca77dd12d65309a9cc652e47e0c773314d",
+ "bytes": 9388
},
{
"path": "apps/web/src/components/ResultHoldDetails.tsx",
- "sha256": "6b378960876ac027cf61328b3fd9fdf4b35a57b6927f0e04ec71b081b13969e4",
- "bytes": 3742
+ "sha256": "eeaa8427a896c1aad9923684dddd1ce620a155808ef1138d6d0d8924f97e81ce",
+ "bytes": 3992
},
{
"path": "apps/web/src/components/ResultInterruptionNotice.test.tsx",
- "sha256": "589f24bc300065afd763c3255e2e65bbb8afcf6edf66f206a3fa1646a5949f73",
- "bytes": 9261
+ "sha256": "a80dd69ffcc1bac7a4975978f98eb5b6a52057cf8ef7f578338a49f41ee3c27d",
+ "bytes": 9611
},
{
"path": "apps/web/src/components/ResultInterruptionNotice.tsx",
- "sha256": "2c5841d49f7cb494f6b87eceaff79af5ffdadeee5a30847cda32105de28c5e89",
- "bytes": 4955
+ "sha256": "36455548677e3bd8ac4dd24d17214903ad70dc44aea387c986430f57b4b8844d",
+ "bytes": 5550
},
{
"path": "apps/web/src/components/ResultScreen.test.tsx",
- "sha256": "3967378888eee232dbc23ba5d9aa66e84476fe8659c5f1824a7e2b0f38162307",
- "bytes": 13672
+ "sha256": "8d10e6dc6f7d51402845ddb41025d425c5713b7f680f29de50410e4ebfa58129",
+ "bytes": 14446
},
{
"path": "apps/web/src/components/ResultStatusStrip.test.tsx",
- "sha256": "9f7cadb74497a4eca64b3270b8b47df7e5b19c4bd9f0bbaf0028a3133ec0d243",
- "bytes": 5904
+ "sha256": "759ff9f7dd0782e9aade4413bdaefe316406d62f27300a574152d568983f5388",
+ "bytes": 6280
},
{
"path": "apps/web/src/components/ResultStatusStrip.tsx",
- "sha256": "ecd8c5684dfb45b7034e05e15b41dae097284ff4ac32efee910208b6385d62dc",
- "bytes": 2273
+ "sha256": "cf6a2ee8e0e1a87b7f46b747b8f74e684e3bb1e5062b9bbaa40fab962f1e883e",
+ "bytes": 2391
},
{
"path": "apps/web/src/components/ReviewDialog.tsx",
- "sha256": "ef32ed2fc5646873d0113a0db1e9a22223442a0af153f77bf9b71b4bf0b4c2fd",
- "bytes": 2600
+ "sha256": "dd753ddf8ef60287a1734853686329d53328bcb7afdb1b1e60aa684f756dfaed",
+ "bytes": 3203
},
{
"path": "apps/web/src/components/ReviewStatusLine.tsx",
- "sha256": "e9327ead26e3c7677673244c297896d8866c95c9600b82d0ae1e72b0816574ec",
- "bytes": 1746
+ "sha256": "7010052dadaa6d33a17b4f9f6e16cd52a06da3a21c1555a85b4bb8d510e46dbf",
+ "bytes": 2096
},
{
"path": "apps/web/src/components/SameHypothesis.test.tsx",
- "sha256": "b970b46fe1090830dbde8aa196b498c99184c2e1dd8a7c7a73ca14e3fdaf8a18",
- "bytes": 9634
+ "sha256": "7bb960dcf89811ed41e0984cc195f26abbd8b81018018dc03366cbd792bbbd89",
+ "bytes": 10237
},
{
"path": "apps/web/src/components/SameHypothesis.tsx",
- "sha256": "7903291351348081e58962e3589f63a929ee70e3f3b53d2249486a0bc897b056",
- "bytes": 4538
+ "sha256": "0fc1f3f0c2461ad0b5512bd4ee6861d799644a2c8e3b23bcb656613c5d24262c",
+ "bytes": 6000
},
{
"path": "apps/web/src/components/Select.test.tsx",
- "sha256": "5541d260758ef0a8cee89ddb461848ed89c0ae33118fcd8f832c950165e9c900",
- "bytes": 1353
+ "sha256": "fd34c552746ee5110cfbb37ec0f7b1bf0b18d9b9d326226e340a9c3d238cb872",
+ "bytes": 1406
},
{
"path": "apps/web/src/components/Select.tsx",
@@ -1338,8 +1343,8 @@
},
{
"path": "apps/web/src/components/Sentences.test.tsx",
- "sha256": "ea1a23a11154e20d43e42bb7fb98778998c4708637feb2a6d07e8391446d56a2",
- "bytes": 1710
+ "sha256": "0d083b0b2dd5402800297f657d2e78d9fc8ea5112f015880de6aa03440b3b064",
+ "bytes": 1755
},
{
"path": "apps/web/src/components/Sentences.tsx",
@@ -1348,73 +1353,73 @@
},
{
"path": "apps/web/src/components/SourceCitationGate.test.tsx",
- "sha256": "ae24400610ed28eb75a4e2509e8dca502554bb723dec5ead64c9e6983330d856",
- "bytes": 6013
+ "sha256": "0e90cc74339b20259358dcbe0974b45a919b555401775655b213925bf51290f6",
+ "bytes": 6425
},
{
"path": "apps/web/src/components/SourceCitationGate.tsx",
- "sha256": "272610de919bc475dc22007fe1a8c7a99c6f5320bd1164ac4c73e285990feffb",
- "bytes": 4881
+ "sha256": "3806a6b8cfda4abbf7c8c3c139a7ea2717af89d710118ae46d1d67e3731a8535",
+ "bytes": 5391
},
{
"path": "apps/web/src/components/SourceDisconnect.tsx",
- "sha256": "faa7eb847aade55a8b3305dff75bb10906b43f89bd53094917f50fb4c7344764",
- "bytes": 3668
+ "sha256": "8fd6f421902bfb8a1dfb223959d12678189e93e5fcab42e65a1b0a06ae3af0fb",
+ "bytes": 3681
},
{
"path": "apps/web/src/components/SourceTimeAdvanced.tsx",
- "sha256": "0683561a5721cd80fee8e3269bef1eef49602679caffc31d89319cb413126db5",
- "bytes": 3761
+ "sha256": "dbed6cb503c62edb4838486f7a7cbcd034303c868658d642107e7b41d87334a0",
+ "bytes": 5619
},
{
"path": "apps/web/src/components/SourceTimeConfirmationDialog.tsx",
- "sha256": "ece207a799e850d282e27fc6e938f96facce580f0bad4ac665be31b54ea977a9",
- "bytes": 952
+ "sha256": "b0b2a11fc2c2cd0636ceafe0e11c11736c3fcb1875ebe0c715fe61b81d986975",
+ "bytes": 1179
},
{
"path": "apps/web/src/components/StatusBadge.tsx",
- "sha256": "81b1347acf2649b9ed597cfef593aa8f5b1793342ee4ca39fd267bf160bced39",
- "bytes": 792
+ "sha256": "d2a6d94a37c84d83ff67f64e350000ed908e02d94d3ba2abb260aac06be076d5",
+ "bytes": 884
},
{
"path": "apps/web/src/components/SyntheticDataNotice.tsx",
- "sha256": "6d2fec5ad66dc60538f3996c23dc4d38050b7045be912d2661c85efa174e62f4",
- "bytes": 1007
+ "sha256": "47eb06aab227485f3926c2ee6ac5e565bd87b73ad4b306a6f8b30c063c3c5a35",
+ "bytes": 1086
},
{
"path": "apps/web/src/components/TechnicalDetails.tsx",
- "sha256": "486b4211123d26d7b48416a86acf41fed6e9696d429962dd5a465700b08f35ed",
- "bytes": 888
+ "sha256": "7cef4199c8e26d803773014491c55f444254c337691db1db0e4c177a4c2852fb",
+ "bytes": 907
},
{
"path": "apps/web/src/components/TestActionDraft.test.tsx",
- "sha256": "c612ada0927f1bf6fa2b74416cc47bc4847d2d91922382cc930338c6ee013f54",
- "bytes": 8549
+ "sha256": "839ace4951fd657058f16186b6a858ee8844fb3f284a5ceec1c58d4a8a896ab8",
+ "bytes": 8903
},
{
"path": "apps/web/src/components/TestActionDraft.tsx",
- "sha256": "4d67a028b09d6d0bd1975646f2737db861420c1c1b8a67e9c6cbe55641ca06f3",
- "bytes": 2521
+ "sha256": "85698fcdbf89756c2fd17609c2d0906e9a628e9c9282318ddd63340126b5305e",
+ "bytes": 2809
},
{
"path": "apps/web/src/components/TestOrder.test.tsx",
- "sha256": "9e20a618f4b73e9cb77ee0d242d0fa0d153f4f6db81133c8565277c82ad09a3d",
- "bytes": 8572
+ "sha256": "06b00bd48b85edf0c406406623ef5aa28bc688a62e5dfdb88a5de7e7e94e77c9",
+ "bytes": 9303
},
{
"path": "apps/web/src/components/TestOrderSection.tsx",
- "sha256": "99365cbdbea3cb93d33f9870aa26bbe2099a8c6bc13632c88d09b765f649758e",
- "bytes": 2370
+ "sha256": "e7b94eeccf24768ed514557f2d6a9192967a6a922064355ee57bc96aa5f38a76",
+ "bytes": 3132
},
{
"path": "apps/web/src/components/TestResultRecorder.tsx",
- "sha256": "a16642d5e4242d889a0eebc0691bdceaf0d99aeb0a69b64324a6531648ce2ab5",
- "bytes": 4221
+ "sha256": "351242b6fa7ab4ecb577f94a25c4df4a44db9e735a77061953983fa7cc46ed31",
+ "bytes": 5352
},
{
"path": "apps/web/src/components/ThothBlueprint.test.tsx",
- "sha256": "0113ee9e811618c39ae9d614efb9809d43fa9cbaf1fa4e032202646fa0f92c05",
- "bytes": 1674
+ "sha256": "e5876199b6525ea9a3b20471bf68c8ef71d7f727a611cc639ddae431032b2767",
+ "bytes": 1807
},
{
"path": "apps/web/src/components/ThothBlueprint.tsx",
@@ -1423,143 +1428,143 @@
},
{
"path": "apps/web/src/components/ThreadNavItem.tsx",
- "sha256": "b10808182d89fb7ba1ce7dc8b584b869f3bb461dd9cbbdf59c7cb538fff851e7",
- "bytes": 1986
+ "sha256": "4c8ce9884827b996504bddfa0b21c8a973fe5114d57fe2c877189357aaebefae",
+ "bytes": 2163
},
{
"path": "apps/web/src/components/ToolInstallHelp.tsx",
- "sha256": "f96db4ace9d71e4448087cf5c81ebd873b694c5908d622c3f457a747b807a5bc",
- "bytes": 2894
+ "sha256": "456a7839927f1de2c0f49a7f1149e395abce2b8b202ea53d212fd1e9b71a569d",
+ "bytes": 3222
},
{
"path": "apps/web/src/components/TraceClosure.test.tsx",
- "sha256": "40257b5f3d6d5f20519ff3d016a8139ad943733a65da20cf4c2265693d576544",
- "bytes": 8558
+ "sha256": "a82a4d307c205982a18dfb1b56a21958fd81037072714865c716074210c8f4ab",
+ "bytes": 8993
},
{
"path": "apps/web/src/components/TraceConfirmDialog.tsx",
- "sha256": "17fb00cb471ab28a7485a417f083ac3ab3cc73d6d306b96920aab2cf4618e679",
- "bytes": 2042
+ "sha256": "b872c022ec044bc8f79ef8dd05bc89de5e7464c943894e1ca2a8ca8c69b6ab52",
+ "bytes": 2485
},
{
"path": "apps/web/src/components/TraceEvidenceDialog.tsx",
- "sha256": "ec442c1f4d12dd63a544b15abdea4ee136cecbd4825ffda813447df27f26997f",
- "bytes": 2493
+ "sha256": "5eb1a178d5089e93a5585fd3174f325e9bb3cf19a971644baf68189c5bb1a3b8",
+ "bytes": 2933
},
{
"path": "apps/web/src/components/TraceHistory.tsx",
- "sha256": "6c22718e448490a2b787301554ea1a2c63507bb99bf7ea83d77c70f7ddf41b75",
- "bytes": 5813
+ "sha256": "93a5a5b430826af7bc2570a303aa31fad3357246dd7d7bbfbd74940552cde0b1",
+ "bytes": 7316
},
{
"path": "apps/web/src/components/TraceImportDialog.tsx",
- "sha256": "3718ebe97833f9cfb9a093c9911e2d6653d5973946577d6906ed8902ded3388f",
- "bytes": 7716
+ "sha256": "c9c7b267af497c94ceeea1bbd0dbf51baf3d0904e1e94111d44046fb991f8e3a",
+ "bytes": 9500
},
{
"path": "apps/web/src/components/TraceOriginLine.tsx",
- "sha256": "5c4541df58c2edb2235479ecb47a27cdad10f0ff852de15b2d8946cea0e2a655",
- "bytes": 970
+ "sha256": "3b467ddbb631e14d533f831adf90e4292988a755fd07e9b72eb806892a2b8a78",
+ "bytes": 1038
},
{
"path": "apps/web/src/components/TracePage.test.tsx",
- "sha256": "ef3a25b3cab2a5b2115afe4eeaf403f43533a138b9b4335310b806af33d69ae9",
- "bytes": 30782
+ "sha256": "552bf78f7786d5d59de2724f5193b7b082d915b450d93224f426c61576d998b7",
+ "bytes": 32046
},
{
"path": "apps/web/src/components/TracePage.tsx",
- "sha256": "0611867b5b627b5ac5b371d74f9fc4efaaf6a4b9eec348dfcd81bdc32e36f3cd",
- "bytes": 16822
+ "sha256": "30af525af1fa8e98684ca9b095e9e7e9ee41ab60794bbf272c386688a25ddbb9",
+ "bytes": 22856
},
{
"path": "apps/web/src/components/UsageSummary.test.tsx",
- "sha256": "dd3ea89f53d26e7e52490e3410066acf093f809381218941be91a628077c0ccc",
- "bytes": 1299
+ "sha256": "2f8f9b00a300e2833e9edb5a288529a282815838f84b8b24790d2c120a0980f0",
+ "bytes": 1424
},
{
"path": "apps/web/src/components/WorkspaceContent.test.tsx",
- "sha256": "7473a034baafcc7964095986e46b48e0ceac098692bb7103d8451f222dced7a4",
- "bytes": 2522
+ "sha256": "2f28dbee402ff5bf079581222aa504b6834b50f47a7d528bca5614d207e84624",
+ "bytes": 2531
},
{
"path": "apps/web/src/components/WorkspaceContent.tsx",
- "sha256": "9862a1afc1e91b699fb9c53eb03f711f75f0aa0f13664476e96956fb3fa1f933",
- "bytes": 735
+ "sha256": "beb2837d21d39a3665b3dfe9af067cf7bc197016fc1239c8694eb8d2777e2410",
+ "bytes": 742
},
{
"path": "apps/web/src/components/WorkspaceErrorBoundary.tsx",
- "sha256": "bb7f12762abbd5603fb991d58ea021206c48bb5b84eb01989c4e5c645ed2bf34",
- "bytes": 1061
+ "sha256": "838b2b58b6d1982ad4ae626b966a11ce96341911c569f0a4f8f9e2591e8f9c97",
+ "bytes": 1268
},
{
"path": "apps/web/src/components/WorkspaceGate.tsx",
- "sha256": "f6d8ab3312cafe1ac9a87f79f9c8913a702a6b8330508a20c07f284d2273a6ee",
- "bytes": 4374
+ "sha256": "038988d1b4c0a10463c2bb50b42d85237a2382bfd72acd85094d2394931d7c72",
+ "bytes": 4728
},
{
"path": "apps/web/src/components/WorkspaceInternetConsent.tsx",
- "sha256": "c401c77cbed99e84da691cf51a8ff8c12a750d3479cb31ddd9516fbfe2d4a290",
- "bytes": 2431
+ "sha256": "e6e5776f1ded1022838548f8068a9b446abd0dbd1900496eaab6dfa15ec439a4",
+ "bytes": 2748
},
{
"path": "apps/web/src/components/WorkspaceLocationNotice.test.tsx",
- "sha256": "2004942e5287061c090ee755e5aa7336ac512a10f2d11733d757b488507096df",
- "bytes": 2699
+ "sha256": "28f9c343fc0a26e27c7802678d74231f0b5bdcf9408b2be42a2be1535169cdcf",
+ "bytes": 2786
},
{
"path": "apps/web/src/components/WorkspaceLocationNotice.tsx",
- "sha256": "1df2ab32bfee5878e17ae7464c8fdadbb5f4192357d53078c3c26e9f818d9977",
- "bytes": 599
+ "sha256": "47b9875251d863ca92e2abbfdf020cd65f876ee516654bf666fec4c4cf8fd5f4",
+ "bytes": 643
},
{
"path": "apps/web/src/components/WorkspaceTopbar.tsx",
- "sha256": "5ad5cf984d158091501c56d51fee1096e4eda7e8cb0ff64d6082056a1d25f35b",
- "bytes": 3696
+ "sha256": "e9e9dba4bcb2588880f60945f81ba73343fb91058b6d9f0e0ff3fd77d8ea5c98",
+ "bytes": 4026
},
{
"path": "apps/web/src/components/XaiDeviceLogin.tsx",
- "sha256": "3406ed5f6055ce25a529b0735351de002af35860bf32b375c74653165dfa3c9c",
- "bytes": 13312
+ "sha256": "6318e343063222ea7edaab3f00d3d0fc74ad14e6f17866e75469abe538a0c6f4",
+ "bytes": 15153
},
{
"path": "apps/web/src/components/accessibilityRules.test.ts",
- "sha256": "cd63623caacad6b3178b5c7b598e4435ef2f82d9b5190c5e1f1fea0e745b2c93",
- "bytes": 4412
+ "sha256": "6c60fa36746fb8c9e497d8c94c0df5f3c9ffe6fc5850752086c001e83a048aa3",
+ "bytes": 4460
},
{
"path": "apps/web/src/components/answerLeads.test.ts",
- "sha256": "e384a1ac74e6a21bdacc40ccb4526ef742653988b755c9866528629f73aecd0a",
- "bytes": 1989
+ "sha256": "a073ba5a4b052785c58910046ee4ea2567f8917b94f037c94a89cb2b9cec9fcd",
+ "bytes": 2080
},
{
"path": "apps/web/src/components/answerText.test.ts",
- "sha256": "0c49d8c9a771e20f2b1cab22ecf67da4dc759ac9252f07c050dbe57677c89fa2",
- "bytes": 2377
+ "sha256": "ac2eabd610a140f23bc746735092d0b31665320e5de8417d641a56f36e810c52",
+ "bytes": 2476
},
{
"path": "apps/web/src/components/answerText.ts",
- "sha256": "24a9bf8d7b4e8d1a3dea368d544b98a3dd06b541f2d591a2f38248be83738812",
- "bytes": 3974
+ "sha256": "5a0228cac03e2dc393db3744e764126351b1bc6246bbd91b7713ccd946225e23",
+ "bytes": 4080
},
{
"path": "apps/web/src/components/approvalView.ts",
- "sha256": "47ef9029b8cf6d69396eaceb28f7bcdbdb71cfd0d1f53e3e40b560f11cce75ec",
- "bytes": 3661
+ "sha256": "8d37f2af467d907f919829ab5fd36ddc650431b66e46b86135cba08307a78677",
+ "bytes": 3924
},
{
"path": "apps/web/src/components/askedQuestion.ts",
- "sha256": "75d60da5fb821ed1257761ef5ab9ace3757570cd3224c6c9301781b0a69b6fc7",
- "bytes": 834
+ "sha256": "b0b26dd0fa9eb656fee6d4c5ba3f8a6fdaec291e2d9b41906cf07a8e4cdc9ad1",
+ "bytes": 839
},
{
"path": "apps/web/src/components/connectionCopy.ts",
- "sha256": "dbdbc8431276aee27b6e3dd83654974e91f9c856ac0207d94c5073ff4f125a0d",
- "bytes": 6511
+ "sha256": "9cb0dcf2e3b5c68cf665e7184799e6428b6a735ffb9aacb6bdf3c33327bf3471",
+ "bytes": 6642
},
{
"path": "apps/web/src/components/coverageCounts.ts",
- "sha256": "4ce4b0786e2aa0ec6f352341fb7e83f3c8837eb8ec269c67c368b482b7998950",
- "bytes": 1408
+ "sha256": "6f50a79a370dbfccd195a496572132b6a0282fe738efcc82571b59e99eed096e",
+ "bytes": 1436
},
{
"path": "apps/web/src/components/credentialAuthMethods.fixture.json",
@@ -1573,198 +1578,198 @@
},
{
"path": "apps/web/src/components/effectDeclaration.test.ts",
- "sha256": "39c9f83cadfae85c050432d68149e9998e8f92eafa1bb0ad65150bae07c3c9c7",
- "bytes": 2001
+ "sha256": "37ae0da201c3e8ba0e4528dbacbfd2ed6ed7354d740660b4c1edc765eda25c6e",
+ "bytes": 2077
},
{
"path": "apps/web/src/components/effectDeclaration.ts",
- "sha256": "3730221e731b9dd008202aa845e1ba10dab1d20125d2e635ddc2043a1db30254",
- "bytes": 2829
+ "sha256": "96f527188527beacaff9e0ce9a7b392a120abe9105f1c03286b803cb48364246",
+ "bytes": 3437
},
{
"path": "apps/web/src/components/effortEstimates.ts",
- "sha256": "19057d4a4ba4673f6e8686c48f794e50c29d50d4a74a81a628002e52baf7903f",
- "bytes": 434
+ "sha256": "6018114998469b1c41de8acd2cf112d75a635ffff50e1edba5d9282631feb2c1",
+ "bytes": 460
},
{
"path": "apps/web/src/components/evidenceUse.ts",
- "sha256": "1f3e67d7693b043365a352bc9cf09be1e1bd0271cf682e6bca17629fb44ba687",
- "bytes": 1968
+ "sha256": "d700f9853d7bcb00b267aed236e43cb549e95618d067e1397864e2ae866a89cd",
+ "bytes": 2106
},
{
"path": "apps/web/src/components/expectedGrounding.ts",
- "sha256": "47575101411cce926a8476822ff18c836d5ef4c32d93155b19eebdd40fcd68a5",
- "bytes": 987
+ "sha256": "e38cf09a58679b970473d44791340caa72e9d1942aaef14f32fb16cd97f213a6",
+ "bytes": 1011
},
{
"path": "apps/web/src/components/expectedResults.css",
- "sha256": "28dd556732f4f63122ddab9679ba6f766be937e04e634bde111b8cfc98a75a3e",
- "bytes": 887
+ "sha256": "23088506642720789ce8de8a27d07e97ce4ef39b26c64f9c1dcb6d243c3c11fb",
+ "bytes": 949
},
{
"path": "apps/web/src/components/foldedOriginals.ts",
- "sha256": "9d7951af8f3b56427d5ecbca3bb6b01d8cbe2b27358b3c167c1e1d26aeadbb42",
- "bytes": 400
+ "sha256": "3537198f191030c6fbb75af87d6b042852d2db8257ed5b5dce3ba8842166082c",
+ "bytes": 402
},
{
"path": "apps/web/src/components/followupItems.ts",
- "sha256": "26290f1174395273ae71ab833ef7cd3c96040a1c01cc6f44691684ec91d1c77d",
- "bytes": 6466
+ "sha256": "8ac8fbddcb2b05bf2cd2ffb6d73dcd37dc4c6fe5c312bcc5cc02098259577dab",
+ "bytes": 6743
},
{
"path": "apps/web/src/components/groundingTestFixture.ts",
- "sha256": "21f0cb78c8977cb41a19423d5486e9902b877657f9c8a8c9ee7ba64d67c9a878",
- "bytes": 1944
+ "sha256": "db08aae28f48d54b232eae41aa1030191e588cf6a54d9c0c6c945fe82c08c50c",
+ "bytes": 3269
},
{
"path": "apps/web/src/components/history/CriteriaChange.test.tsx",
- "sha256": "89ad421e14754fa59fcd1d722a24e8c2143f842530602af38dbff8caa5302cb9",
- "bytes": 4511
+ "sha256": "286c0263b9fd4de08b3b5dff80fe10272c6704f09a9716d0cfdd552b7c470091",
+ "bytes": 4901
},
{
"path": "apps/web/src/components/history/CriteriaChange.tsx",
- "sha256": "b24716ae7274b7aacf455619eaab685f8a1057454e90b33d94e86dc791299544",
- "bytes": 4285
+ "sha256": "754f7ede00ff591cac43c9b724f5c2cd9e50d98f1365e62ba8f8f0d6b8ed6313",
+ "bytes": 4867
},
{
"path": "apps/web/src/components/history/DecisionDeltaComparison.tsx",
- "sha256": "50e301fb9ee38d201acb82d01f5204fabde07418da91d4b31a93723b45b6d6b3",
- "bytes": 12583
+ "sha256": "fe0c89bebccd879944e1907be65d9f387e69a79d973cd6edd018f209ff4a96d0",
+ "bytes": 15522
},
{
"path": "apps/web/src/components/history/HistoryChangeSummary.test.tsx",
- "sha256": "667774c50ec600d9e2cce95f49adbc5bf808fff26a7ab669ce5e3cf014596d80",
- "bytes": 5378
+ "sha256": "ae784cc27ae66dd583efce0a566bd848ffafe21bf8efc1b5092cb6252105ac2a",
+ "bytes": 5862
},
{
"path": "apps/web/src/components/history/HistoryChangeSummary.tsx",
- "sha256": "08708d4c4caaf5432ae6ea48b8e0de8a76ddc0e5b6c37758984bd41cea8d030c",
- "bytes": 3219
+ "sha256": "dd16e344748cf79c540fcf218411d498c740b54247985ead597b5855a541eaf5",
+ "bytes": 3886
},
{
"path": "apps/web/src/components/history/HistoryContent.tsx",
- "sha256": "8569c49b0d7d02f744c96f205417a514a4778e391f8682d334974c7deb9dc7d1",
- "bytes": 4962
+ "sha256": "077b916945668c274380d1c07e561a378943506b97963c5d43c69a9218ab0bc7",
+ "bytes": 6180
},
{
"path": "apps/web/src/components/history/HistoryDetail.tsx",
- "sha256": "b733a7025631e02e31c5ba559294cf0da25121ac7b1810f68bcc7b0d0705b993",
- "bytes": 7932
+ "sha256": "be543a743a3e75ff2db0c27d149a58169abf7da42af2070cd74d20e8cab9ce4c",
+ "bytes": 9970
},
{
"path": "apps/web/src/components/history/HistoryExample.tsx",
- "sha256": "b58d83841d65018a121edad185f0c0be2739e98ab8c6812a3dd795a07e640691",
- "bytes": 1150
+ "sha256": "346dd6f02a47f33cec03f147312408b778a44220e2a379660357853ecb26da73",
+ "bytes": 1309
},
{
"path": "apps/web/src/components/history/HistoryTimeline.tsx",
- "sha256": "8c60c1574aaa4c609d8892702e1912a8fe4e2e286c44ec5b196fe4a4c416c9b5",
- "bytes": 4284
+ "sha256": "248a17c489ad7b740bcd6b0a8ea4835f43cba1abdc751340d16a4a3e4d00fd2f",
+ "bytes": 5756
},
{
"path": "apps/web/src/components/history/ProjectReviewList.test.tsx",
- "sha256": "269a2c42262c4a916af2006610b9b311e627f3204d02c611fab6bf5ff6318eaa",
- "bytes": 2439
+ "sha256": "214b9af1d86df7da5aec72fc1ce816ed536f6a3e5c7962a4d20d66fd63b99efd",
+ "bytes": 2601
},
{
"path": "apps/web/src/components/history/ProjectReviewList.tsx",
- "sha256": "941f126398b6a5f28bd5ad68a8852a5d97d32d421a82daa16a5c1cef777d2832",
- "bytes": 4555
+ "sha256": "b24e71d47fcc84ac808c06cf65b7ea9a835b4e150ce5ba615309b639bca2d9a5",
+ "bytes": 5476
},
{
"path": "apps/web/src/components/history/ResearchHistoryWorkspace.test.tsx",
- "sha256": "7c2d2fd5d7cc4c9773c8ad69d2d71cf4d61f61ba4e05eb56bc583c6b7a02f5cf",
- "bytes": 32288
+ "sha256": "0781b59510790b2aa75e705154ea1dbe870e6aaf9a77d96591da867c580f22b9",
+ "bytes": 33972
},
{
"path": "apps/web/src/components/history/ResearchHistoryWorkspace.tsx",
- "sha256": "1c0b7f73e066c4428df11d6296c7efef9e21efe3e98abc580bb19f0e779daf25",
- "bytes": 11127
+ "sha256": "2a9f8c7e872987b8d6132c9c71aca922ea92aac10b98aee2fabf626d10be4f6a",
+ "bytes": 13555
},
{
"path": "apps/web/src/components/history/RestorePreviewDialog.tsx",
- "sha256": "c520934ebf4ed8b268f5662198f510da2c32fac3ef8caf078145b3356a6d7057",
- "bytes": 4575
+ "sha256": "53b07bc9517dfb6ad2119316b83bac2f98ebac1e1db9deda0f8fada85eea4af3",
+ "bytes": 6122
},
{
"path": "apps/web/src/components/history/RevisionComparison.tsx",
- "sha256": "ff4a077d96edf6f285e4473f95043d7caf32df852f3e10b553587a5d2e6fde9f",
- "bytes": 2066
+ "sha256": "f9e16fecb44484bdb5c811061025c5e78d88de034a2e13c9624efbe23bcf6f5d",
+ "bytes": 2562
},
{
"path": "apps/web/src/components/history/historyErrors.ts",
- "sha256": "81a8d40613add31f418937019daa23d1d318d54e6678df2ff513594b8baa43fd",
- "bytes": 935
+ "sha256": "246e877bb9d3006ace1d4bb73b220cf79d07210f461577f1201a03ff236f864f",
+ "bytes": 939
},
{
"path": "apps/web/src/components/history/historyPresentation.ts",
- "sha256": "0990d172887c03729158d5a3de60b4ddc1d99726456db70d1314fbb643ae66e5",
- "bytes": 2740
+ "sha256": "059069808cfb987d78ddc17917c0cc81e5d0aea896b84add4daa523a11626bdf",
+ "bytes": 2987
},
{
"path": "apps/web/src/components/history/historyReasonText.test.ts",
- "sha256": "07efcebfe9c4cbe73040274e6067139e7d99dfa5bf8a316a0e7c02b292e9d9b0",
- "bytes": 3020
+ "sha256": "23b57020a097affae83b762e4d30fa843f1fb8b9e9a15ae6803c23c876f9cdaa",
+ "bytes": 3146
},
{
"path": "apps/web/src/components/history/historyReasonText.ts",
- "sha256": "597441e7e57106b0bf3b5a8319ef0a8eade847ae3ca3361923dfeba9d7111d5f",
- "bytes": 1101
+ "sha256": "c3c7851b45b8f0dd26088855fd9fb556cd4d8be5b92b4fa7d997e404912626d0",
+ "bytes": 1154
},
{
"path": "apps/web/src/components/history/useRestoreFlow.ts",
- "sha256": "e7fb66c43b97b78437df61b6c61e3a899eb0426bf0475fbeccace89119220be4",
- "bytes": 4420
+ "sha256": "54afcf39feebf864260c0c8d43e0850e398befb750282bc7fa710437a75947a7",
+ "bytes": 4662
},
{
"path": "apps/web/src/components/hypothesisLinkText.test.ts",
- "sha256": "06a88e70295f0faef86914f0e4861f1f4665a2ec0fea751be5a77da5076895ec",
- "bytes": 6777
+ "sha256": "47b26ef48f3cb76bc09b6f006d255d3c682c699214d5bd60cf4cf8d4fccddd7b",
+ "bytes": 7359
},
{
"path": "apps/web/src/components/hypothesisLinkText.ts",
- "sha256": "23035bc9e3c5f771bb85b75888916e96f363e9955cdc02e6d553798e4b28167b",
- "bytes": 3904
+ "sha256": "14f611c1eb123700e3833b83de56dfd45b7954ada8f63eace337c51fd7a4ced4",
+ "bytes": 4160
},
{
"path": "apps/web/src/components/hypothesisMatrix.ts",
- "sha256": "6a6f03497c5c6dc01673229c23d6b760e0e4ad8eac7d573d01b002bbd729dcd0",
- "bytes": 1192
+ "sha256": "5f6ccf06607a8e849b4d82dbd18ea75ea6f806f901d673a1ffb21e8837810b8a",
+ "bytes": 1229
},
{
"path": "apps/web/src/components/hypothesisReview.ts",
- "sha256": "f25513b9b8c806b0611b1a5e9bf8c466b33ca1b089fcb8d1ec2dd296283c1fa1",
- "bytes": 2760
+ "sha256": "1aba69b7e3aa0e1de21809fe8cdf62c847275841968bd135303e352999d1b432",
+ "bytes": 2907
},
{
"path": "apps/web/src/components/hypothesisView.ts",
- "sha256": "969c306a974f52d60c6bee634ed3eb021938e142912a48f8da4ee4a89bd9448b",
- "bytes": 4492
+ "sha256": "a15299045ab59525b084cf90420b7073bb9442f838ce63b0b54692be2d91f621",
+ "bytes": 4639
},
{
"path": "apps/web/src/components/judgmentRecordText.test.ts",
- "sha256": "ae34a9cf1424ca150cbe4e58404cf3ea88bced41b7a04d5863dcd1211062e5b2",
- "bytes": 6263
+ "sha256": "1c10a744f4f7d0a23c9561de2113ea60d3d584b75b1c124a5ac6dcd18bc8607a",
+ "bytes": 6614
},
{
"path": "apps/web/src/components/judgmentRecordText.ts",
- "sha256": "bb8d298f5e9a28586cde049996d2e92661fefedc82c23266f92c5784c92dba2a",
- "bytes": 5335
+ "sha256": "55754967b5bc5c97fe4264cc074f0d8efe7d59c5ece160fbf3d1fa5be5d890ae",
+ "bytes": 5675
},
{
"path": "apps/web/src/components/judgmentReview.ts",
- "sha256": "3ddc9dd38914ae496750c953476890d24b32133df314849cc394089e97106f4d",
- "bytes": 4044
+ "sha256": "56422f044dffd1807496a6d19ee20c63d0e34dfc6247067b861f544efb4d3f0c",
+ "bytes": 4399
},
{
"path": "apps/web/src/components/lessonText.ts",
- "sha256": "7842c35ae6248c187d10b0d07fde7bdd87f5136098be85fc5e153b061988e811",
- "bytes": 2116
+ "sha256": "771f14623e1aab6191e0b94a5ac3b57fb90a22a0f5b4ff048fe6882ca0448c48",
+ "bytes": 2344
},
{
"path": "apps/web/src/components/localCredentialWorkspace.ts",
- "sha256": "e219b8695eb51419c4e502647dca6aa564516ec6baebdc0fb7b6435e30f9149f",
- "bytes": 581
+ "sha256": "8acf2abc4f19f0dffa65044036a437b010b20a92034ff9fb4b789394613ac7f6",
+ "bytes": 585
},
{
"path": "apps/web/src/components/localWorkspaceReady.fixture.json",
@@ -1773,103 +1778,103 @@
},
{
"path": "apps/web/src/components/memoryEdit.ts",
- "sha256": "96eb638d1d7d434d4acdfae3ec6e9c7a91bc4f4f671a67440200c0bd66cadc3e",
- "bytes": 3349
+ "sha256": "aa87e7b74413205fc66ee1b523d5cf69384717abea15ef42d96390da855def6b",
+ "bytes": 3524
},
{
"path": "apps/web/src/components/modelCatalogPresentation.test.ts",
- "sha256": "b5d2fa2e8a1d3647de8fd04f166bbb0570099c5617b20a367977a30d7236f4ec",
- "bytes": 2765
+ "sha256": "54a7a63d70e47e51aefdb4e2f6f95f0867aa9eb9ef45195329a1c2d6ac86f77e",
+ "bytes": 3025
},
{
"path": "apps/web/src/components/modelCatalogPresentation.ts",
- "sha256": "7a842972c5dded5be41a54020912ba137e0ed192ff14d2e718f5401cde725f26",
- "bytes": 3885
+ "sha256": "3a0197684311b74977726d150d66188873222566896aac3d985d81865afa5ecc",
+ "bytes": 4019
},
{
"path": "apps/web/src/components/modelCredentialPresentation.ts",
- "sha256": "ab778ff37851019fef1200c1edaa2dbcf918a108c05b531b99b506f374c4ef43",
- "bytes": 19044
+ "sha256": "43f6196ae4ceeb459a6aa410ac3774c1c648fc6bcf233401ee2ff51aca68faa9",
+ "bytes": 19559
},
{
"path": "apps/web/src/components/modelCredentialStatusNote.test.ts",
- "sha256": "e2579d70af8ff4c90dc41adf6e3d2885c5b512314f0ccefb8545509c1ed4fd34",
- "bytes": 6881
+ "sha256": "1ad1a6e7ef0d07c23c3c0159b9172d8538c99bb2351b160c1f018003e6ac2785",
+ "bytes": 7272
},
{
"path": "apps/web/src/components/plainWording.ts",
- "sha256": "4ff4f9fc2fa12defb67a31d8cebd2f57a01589255112bd1a18d4f8298ee06e20",
- "bytes": 2568
+ "sha256": "53e6e8548a6b9d592c79c17cad7d06789077c5014a8f12622fa0550caced0ede",
+ "bytes": 2591
},
{
"path": "apps/web/src/components/readinessText.ts",
- "sha256": "9ea89510f70bf7e8daa3050b39ac13e3e239f64cd99abd00edd6fd948f4ca740",
- "bytes": 438
+ "sha256": "aa1f2bd4cda23ed5decb569dd752bc384fd71dbcc2523f59b0246a428b39f9b5",
+ "bytes": 450
},
{
"path": "apps/web/src/components/resultInterruption.ts",
- "sha256": "805e54c14c27a494713e7f82f68077ce3285752514a7b82326bec180d62e9d5f",
- "bytes": 3920
+ "sha256": "489898fcf1dc29917e0e3f326fb12b64b398573feb3faf5d3dc37732ce7f6aca",
+ "bytes": 4023
},
{
"path": "apps/web/src/components/resultUsage.ts",
- "sha256": "35260383dd0b21e5c6bc582d122ad2380b5604d0b8d181d6c01e7a16ecce1f85",
- "bytes": 3401
+ "sha256": "19c04780d90d03297c96a165049de461b1ee9b7f5cd2d1531c37ceea36b671f6",
+ "bytes": 3594
},
{
"path": "apps/web/src/components/returnFocus.test.tsx",
- "sha256": "fa034e65795ffb8e43728989b5849ec1307f4ca1650d6f0ff68ee48055476092",
- "bytes": 3951
+ "sha256": "e6cfd5891848c095e6e651a2f1e2ea4480d845a292bcf97fece87501fbc9e9fd",
+ "bytes": 4193
},
{
"path": "apps/web/src/components/returnFocus.ts",
- "sha256": "4269aa688a214a2954a60fbe4cbb19be5cbb1a33e37538be13f4e0f437bc1f86",
- "bytes": 1644
+ "sha256": "a695ef3d7648ebd99206d263163f2361b0011d6024c89cc6e5f31d8acdd5badc",
+ "bytes": 1668
},
{
"path": "apps/web/src/components/sameHypothesisText.ts",
- "sha256": "4b989096704ad8dae5ceee7cfac9b4cdd8545b4b3fb46a8156ba6ebb2d192e9a",
- "bytes": 2515
+ "sha256": "909965ab8f0cd588edf9251bda05fb11e6f6b2ca44e3ee6b81f4ca72dd95632d",
+ "bytes": 2563
},
{
"path": "apps/web/src/components/statusLabels.test.tsx",
- "sha256": "ca6ff4d5c735e2da417140c0621df722669681f36559b92cc077cd73ccc3715d",
- "bytes": 2656
+ "sha256": "42b737c616bd24d64df6da2e713e961cf0856db6525fcccdc9bf756280a2cff2",
+ "bytes": 2721
},
{
"path": "apps/web/src/components/statusLabels.ts",
- "sha256": "630268df7acbe8a94cab2d5d875d40280aa362e59829888595ed425073acdd47",
- "bytes": 3993
+ "sha256": "5a8e2e82eb6343658c58ace706a581c355ebe1974a81ac08b38e25c36ce74323",
+ "bytes": 4372
},
{
"path": "apps/web/src/components/testActionDraftText.ts",
- "sha256": "fe8a5cbe6f37a16bfabcdf894bbebf25051068de07e58eb8cf696df46f0619e5",
- "bytes": 2757
+ "sha256": "6e4075a6a8fe1dac9969cfb11162b3dffb35a58655dc459fe559be3a20ca2660",
+ "bytes": 2873
},
{
"path": "apps/web/src/components/testOrderInput.ts",
- "sha256": "934fe4b0a0061bb954414cf90a9f9d3318e58cdeec853349b12df0693e2fc76e",
- "bytes": 5108
+ "sha256": "92e1d702e6597cf444985106c3e6d82182f4e5c2243286e8c54cdb89d68de23a",
+ "bytes": 5534
},
{
"path": "apps/web/src/components/testOrderText.ts",
- "sha256": "28613dfc0f4d9401d1d21509dd9482f206f4794a269d0bab76257765f2a73665",
- "bytes": 1059
+ "sha256": "fc08f828d6e2f99bd957a3d86f8232581b55f41148945c60077c98bc9148fa5b",
+ "bytes": 1060
},
{
"path": "apps/web/src/components/testRanking.cases.test.ts",
- "sha256": "755293b520801ebd96aaaf67a6d488c8d8eeea9b107b06ac5745a1f940a7c649",
- "bytes": 4156
+ "sha256": "c4db0166501ed3f81e4b4dfe57028a071f0418106216a918a9b1cbe116936233",
+ "bytes": 4575
},
{
"path": "apps/web/src/components/testRanking.ts",
- "sha256": "c1ce4bfb28cfef8006a43061e007c3040c9cef82a26ed37dd3c9593e50a917a5",
- "bytes": 7198
+ "sha256": "9adaba1db408372508bbaa90e56e199b48dbc8915e2ed7636976f9828e1ec55f",
+ "bytes": 7769
},
{
"path": "apps/web/src/components/timelineTurns.ts",
- "sha256": "6d5f6805d56074400b32b68319745c2047429a033a2924c4d8a544eb2b28fafd",
- "bytes": 1936
+ "sha256": "f046de7192270926030899318988dac9ac4b2c32cc22d8d0c4b55c8f3fa8bad2",
+ "bytes": 2006
},
{
"path": "apps/web/src/components/traceDemo.fixture.json",
@@ -1878,93 +1883,93 @@
},
{
"path": "apps/web/src/components/traceEvaluation.test.ts",
- "sha256": "ba2d696efbbb582c6bf62999191ee55eaf8ee41bd8e6ae3be0474a34124b41fa",
- "bytes": 4692
+ "sha256": "4dc53419e206e3421f696c445e8fe5551ce3985d1f6efdab79b2001dfacda78e",
+ "bytes": 4956
},
{
"path": "apps/web/src/components/traceInvestigation.test.ts",
- "sha256": "03671342b27f78c15f4d5652ae5aee939942b0741d0d925ed0b81f4a4b8bf630",
- "bytes": 5865
+ "sha256": "3416a15b067145901fb997c80cd6434c2128be0ae1d038f22bc87e6af31937ff",
+ "bytes": 6230
},
{
"path": "apps/web/src/components/traceInvestigation.ts",
- "sha256": "f62db84d1bd0d758b8865761ddcd03370e1eb08229431d53fb94921452cd3c6d",
- "bytes": 4395
+ "sha256": "abe0fd2ca2421a7bc8f7a4f2231cc3db10581ed1e2eb11b4a516e344ef74fa7e",
+ "bytes": 4676
},
{
"path": "apps/web/src/components/traceInvestigationLanguage.test.ts",
- "sha256": "9e6be03379f835328ba8fc65c88610bb67ca2c966170f63ddc0c042345d462a2",
- "bytes": 4124
+ "sha256": "d230dfe8e7f1475fca3fb2cf8af512bb6d2bcf7e935634acf30c1dc14edd6382",
+ "bytes": 4235
},
{
"path": "apps/web/src/components/traceReasonTags.test.ts",
- "sha256": "0c645f6f882e9ffb9d34205120315d4b9f096fa1762c47d50692b5aa40ce0c8f",
- "bytes": 8881
+ "sha256": "7c519393d6815514bfe842a552884e3679fdd116a8c3e60f965e2c91be3f3e6b",
+ "bytes": 9643
},
{
"path": "apps/web/src/components/traceReasonTags.ts",
- "sha256": "aa958b2ccda81c837c353437dcc840298908c115beda63a3960fb139841c571b",
- "bytes": 3457
+ "sha256": "860650670e39fc0c86ec0690eea98e0d898d5b57495c2f6b35131f194b36ac17",
+ "bytes": 3565
},
{
"path": "apps/web/src/components/traceRows.ts",
- "sha256": "117aaca971f997b8190a7cd917c0acf98f0634d9a86b9f553ca80b2e7a2fb77d",
- "bytes": 3218
+ "sha256": "ce46d007100427a83d3152857d032afae78d16f1468e4006ea3e33de4c1e8a12",
+ "bytes": 3512
},
{
"path": "apps/web/src/components/traceText.test.ts",
- "sha256": "8aa77719d7b3e9592276aec97837cdc7865c0061970953c30f49e7bd06c3f9bd",
- "bytes": 14992
+ "sha256": "6dd902b48a25a30a00760f78899163f0e484126d8612587eeaff573ad4b62daa",
+ "bytes": 15858
},
{
"path": "apps/web/src/components/traceText.ts",
- "sha256": "c0c2bafe1878cf2b5adddb8560d4e0faae9ec522392bedf490a39e00b4c37aa0",
- "bytes": 20905
+ "sha256": "190fe0334884818e75024f54bb969e5f9426b0097d3bfd58d733efb349df8c76",
+ "bytes": 23031
},
{
"path": "apps/web/src/components/turnSummary.ts",
- "sha256": "b35e6d55520140469bbbb8d5addc252d8f53169e9c5fc1f7028b1497adb1f732",
- "bytes": 3159
+ "sha256": "28ca01be5473dc7953f622896c8c89b2bbd25699d102ad0968784bffe7daf69f",
+ "bytes": 3316
},
{
"path": "apps/web/src/components/useAutoRetryOff.test.tsx",
- "sha256": "39394b5340692d851250b68d0929b0d3c4480f9fb5c1fe1a99a4536a6bbaf59c",
- "bytes": 2813
+ "sha256": "2ed53401eedfdfa8953f546aea104481c69edf81a6f41622972d3fbc57aebded",
+ "bytes": 2885
},
{
"path": "apps/web/src/components/useAutoRetryOff.ts",
- "sha256": "a57e246ee6232e4bb94bd20e8d597547d075267392c1673981de70ccfdfee73f",
- "bytes": 835
+ "sha256": "95197dc620f7cbdce22391f37da7a8898179b3daffadcc125bcc135896d0065c",
+ "bytes": 878
},
{
"path": "apps/web/src/components/useConversationReadProtection.ts",
- "sha256": "0c42491f874c817b54ddedbf5b73ae96d5eac3095aedb39efb51fbd9db1a1c0d",
- "bytes": 1669
+ "sha256": "8da4e92afe923a5c90948afe76b7b4f7f9288312dcd89fb79d2aac6042a54ae3",
+ "bytes": 1752
},
{
"path": "apps/web/src/components/useDraftGeneration.ts",
- "sha256": "da58e344da83734b1c453b42f6ff5a587c6d67be043a247c26d4e1e8d8940b67",
- "bytes": 921
+ "sha256": "ff3f433892697f0aa1525992c7207b543735c087865b25d20f7d781a4f16d69b",
+ "bytes": 999
},
{
"path": "apps/web/src/components/useHypothesisLinks.ts",
- "sha256": "02f64c0aa50f219fc2a070fae11d4fd6d925673b956def071da5979c76212c5a",
- "bytes": 810
+ "sha256": "d0294c94a2ead7c67d3478aa1dc0549e3952e83d283680fe611017f67fa04f31",
+ "bytes": 836
},
{
"path": "apps/web/src/components/useJudgmentRecords.ts",
- "sha256": "460b0f47cb64f71e11923da747b77c2186ef299181ea33d3c8f86eb96f143184",
- "bytes": 2365
+ "sha256": "fe80952c6e5b09300d8df1df4a2a9181e9316031469e380b806970a813e1c21c",
+ "bytes": 2450
},
{
"path": "apps/web/src/components/useNavigationDrawer.ts",
- "sha256": "6c4fbaf3ae858673ed4844a738d28373c686ea7e941ccf7478f611c3925213e7",
- "bytes": 2323
+ "sha256": "e04beffa8857c332d87e5e29d3a26e807eb5d5f0bef79efe90fc6324a2c4def2",
+ "bytes": 2688
},
{
"path": "apps/web/src/components/useOpenAtLatestTurn.test.tsx",
- "sha256": "4f36af77b87d04dd960bd3512ff992c129c0ff2787fe47f4946c935bc37573d4",
- "bytes": 2945
+ "sha256": "f31464deaa1f4463cba21b7644b71016baac84780b94b8c85371559a8dff7d61",
+ "bytes": 2965
},
{
"path": "apps/web/src/components/useOpenAtLatestTurn.ts",
@@ -1973,43 +1978,43 @@
},
{
"path": "apps/web/src/components/useProjectManagement.tsx",
- "sha256": "35fb09177cb72aac2e7319b881851c8b7deb58295d69f12ca47825cf16f9e043",
- "bytes": 5564
+ "sha256": "db1c39c36c625ddabfd715dbfe1244637a48402a453a977762d1352df2bb371f",
+ "bytes": 5774
},
{
"path": "apps/web/src/components/useProjectSources.ts",
- "sha256": "5b42eaaef1ef50a7193ffad997ff4edd59c4c9583b54e257e4fb3b94561a1281",
- "bytes": 1163
+ "sha256": "e15d529e71c9df85860937c2a937991366f8c86a8fb71a628312ef052736231a",
+ "bytes": 1251
},
{
"path": "apps/web/src/components/useReadAccessProtection.ts",
- "sha256": "380496b8711bcb1ed22fcf3b5acc85096eb96381fcda3b69e2202e38c122416b",
- "bytes": 1403
+ "sha256": "e542319d20a3841dd4cadd80b0d02eb97ef8c8a480c1865e79eff78f1056c64c",
+ "bytes": 1464
},
{
"path": "apps/web/src/components/useResearchSession.ts",
- "sha256": "7dafd2feafcd3b45149dee656ba4f3ee7764d5f370f03d6df1322a39fae32abc",
- "bytes": 15205
+ "sha256": "17c26f28ab0de7ca0f894bd7210af2b3177dd1400c500fe49b01f5f3b7b7054e",
+ "bytes": 16711
},
{
"path": "apps/web/src/components/useResponseLanguageSupport.ts",
- "sha256": "bf469cbcbe84e6dbe7e4ffd96413b2dcec5853be88625ddeb1eb61fb6afb0669",
- "bytes": 1325
+ "sha256": "9116e8a6d6dd5d99a2da0a33f08675076e72930c5899851958b4856a4b460ed0",
+ "bytes": 1352
},
{
"path": "apps/web/src/components/useResultEvidenceProtection.ts",
- "sha256": "4d58a66d76517539d3696e602ea10631f9f9a7a5825782cb4797d67e0e058c6b",
- "bytes": 2010
+ "sha256": "8e5b5b5911a108d9e1693b81e0092d7c2ba3b313edd986b96fa92da87b528214",
+ "bytes": 2267
},
{
"path": "apps/web/src/components/workspaceSelection.ts",
- "sha256": "87058f033e3135ed8433e4604d48b2abc59d0932c1998e0cc29800ab43d74320",
- "bytes": 8928
+ "sha256": "7206b6f4b09be263f19faa5e21e05b9e726a7df86bad2207414bd15251f2c6a0",
+ "bytes": 9619
},
{
"path": "apps/web/src/components/workspaceSetupIssue.ts",
- "sha256": "088ccc1551d13ee650c6ba0acc8a5808a011bdb389c16af722ad4676f1eee171",
- "bytes": 905
+ "sha256": "ca6d29dcf0137d3fa9b38795b215944056e03c0cac3baca54b17bd3e8550e9d4",
+ "bytes": 913
},
{
"path": "apps/web/src/components/workspaceTabs.ts",
@@ -2018,298 +2023,298 @@
},
{
"path": "apps/web/src/i18n/ResponseLanguageContext.ts",
- "sha256": "733b183f5ac03018d9ecc5be54f9bb8c6491e3292c30aad96e53401847336308",
- "bytes": 605
+ "sha256": "45fdb944f771c9177cf481a6a189f7313bf4d137f9be785da07089f0de1dab46",
+ "bytes": 612
},
{
"path": "apps/web/src/i18n/UiError.ts",
- "sha256": "64d31f714f844a1d69683fa76b17141efa0655364cec7bf9f3979311a74ffbb4",
- "bytes": 389
+ "sha256": "19ae3898aabdcb47ee30179d44e1b4389a1cdb5c1300e5397cb75dd4f5949a2f",
+ "bytes": 391
},
{
"path": "apps/web/src/i18n/activityDisplay.ts",
- "sha256": "bb8a8a338e268d45f9daaa7e403998a1207bf00cb3acb14f334e5585779803d5",
- "bytes": 8830
+ "sha256": "0fdfbd23f2d0090e6c493a349c1b2486cf4dcd8d4baa543ceef7231a05ba3f15",
+ "bytes": 9618
},
{
"path": "apps/web/src/i18n/activityMessages.ts",
- "sha256": "f8b5b823f86b2d09d80ff68f2689ac07a228dce9c42d8a53dcc2b13745b7354c",
- "bytes": 17901
+ "sha256": "777501cea995acdf33b9de6c1a3c9a690fe22a9c6186adda7323085d1c56109b",
+ "bytes": 17458
},
{
"path": "apps/web/src/i18n/advancedMessages.ts",
- "sha256": "f66bf00e1a3f2394a14695b5331b2a9485dce2743c4c5f326a83fc4fcaa3f1a0",
- "bytes": 13945
+ "sha256": "4ded4b0ebadb3c639c2113d4880fe59c812187d0cb64f14370528b479dfc1d8c",
+ "bytes": 13646
},
{
"path": "apps/web/src/i18n/apiNoticesMessages.ts",
- "sha256": "91437790ffadd57ec2dbd2fa27ed8ec4829d2ecac683e747295fa33945142e10",
- "bytes": 5703
+ "sha256": "84e9a77672baed79f1d9167995f6074503db4d2405774dda4800722d40f47ed7",
+ "bytes": 5596
},
{
"path": "apps/web/src/i18n/bilingualRecord.ts",
- "sha256": "91e7633001b002536deea2ae560d82a4b36a6494bee0508cd7476a328495b9e8",
- "bytes": 622
+ "sha256": "7215136abe266b6a413c9985c6c4f1b8ebf03c41a39d7d9007df3cdd82b896be",
+ "bytes": 632
},
{
"path": "apps/web/src/i18n/commonStateMessages.ts",
- "sha256": "d932cfa9d75cf0095eabb21d53d829dfcb95cb072f8d6e576c650ebd16a6b5fb",
- "bytes": 6105
+ "sha256": "920026ea0ca1ce00d6bb5a754aeb335a8ed50b6ab9c065389f023a2ed455cb43",
+ "bytes": 5920
},
{
"path": "apps/web/src/i18n/connectionReasons.ts",
- "sha256": "58012595415f8d8754b875a46c30df817c2509b61a94f4df5f13cc2528d9371e",
- "bytes": 19907
+ "sha256": "923f04187c767145420fc05ae0e7cb35153eb7f818a6f55aad2147352a5c0662",
+ "bytes": 20167
},
{
"path": "apps/web/src/i18n/coreMessages.ts",
- "sha256": "396c1ba18a443566230bfe8001814cdeeec1fa9c3d619eba4d502af77a832251",
- "bytes": 8226
+ "sha256": "023c836462deb25e8bb249e7a7b00f79ea482b9d9e9729ead7309419780eef03",
+ "bytes": 8545
},
{
"path": "apps/web/src/i18n/countFormatMessages.ts",
- "sha256": "0e98dad151c5957478089a6385a09187341ee350303485c503df2433c607d3a4",
- "bytes": 15873
+ "sha256": "c18ab4c6bb66b8b975c3ecea32b7962bcad0e7baa356a3da07618c74df1e7803",
+ "bytes": 18075
},
{
"path": "apps/web/src/i18n/credentialMessages.ts",
- "sha256": "2fd1801d0ed40743b4559b8b50a2c1f6cf2ce7871c5ec8a9f4a01a52fd63d1c6",
- "bytes": 11913
+ "sha256": "2eed37845616fd864fc4b7c05bd198ceef9dfd4e0fc460f3e7e1cc5172666513",
+ "bytes": 11719
},
{
"path": "apps/web/src/i18n/finishingMessages.ts",
- "sha256": "330637e1f0280426eaa920c3a156f99b7f63b90be1e192dd84e7554ca294139a",
- "bytes": 2844
+ "sha256": "a881632ae0ae6b6cd96d3bf658d6e08500b71a078680486e4df080c326b40bc0",
+ "bytes": 2779
},
{
"path": "apps/web/src/i18n/followupMessages.ts",
- "sha256": "fadfe623ccb16dc130e4d0d56dab7851911ebce271ecffd822b11ca566eed65b",
- "bytes": 11832
+ "sha256": "cdf8fcffcfa0b16cd279fc8ce5d4bc7acf1fb11c4a6dab86114fc5df0ef281e0",
+ "bytes": 11614
},
{
"path": "apps/web/src/i18n/formatMessages.ts",
- "sha256": "40d9cc45b3273cb3f31151e622a745327650548df4758f7119ec9a993995eedc",
- "bytes": 9435
+ "sha256": "03871bdbbf67e648f191273a47c6818f5278bca665aeaf07ce8e47f2fa9c59fb",
+ "bytes": 10082
},
{
"path": "apps/web/src/i18n/groundingMessages.ts",
- "sha256": "73a85960e328d7c0440af8508fb17bbd4125c0874627fc6bda0d8db2814147bf",
- "bytes": 1366
+ "sha256": "e65db59296a189e5af1381051f3dd627aab438bc8af7398c1c3b05ad2bde8c20",
+ "bytes": 1454
},
{
"path": "apps/web/src/i18n/historyFormatMessages.ts",
- "sha256": "9f359465717a5b9fba3d566596ad15f5967f5eb97cf07873a2a486d8df7c9e01",
- "bytes": 7664
+ "sha256": "a4021335d62c367e0a5c4cfa39579136233a0fec55e5699fd7476cb87c066c5c",
+ "bytes": 8300
},
{
"path": "apps/web/src/i18n/historyReasons.ts",
- "sha256": "3e12ab0a43f4c9a345a24d6c42c9677fd24754058466651662590b7176d3d75c",
- "bytes": 14893
+ "sha256": "ed9c486ee5f8d6de7fa95f4983e2a1633f02e1fdce826f1ee5a4b0cecf66867f",
+ "bytes": 14859
},
{
"path": "apps/web/src/i18n/historyTitle.test.ts",
- "sha256": "876b4883145701061fa459600a6c4009929ac9ac17b33eed763c30134d3f2e06",
- "bytes": 785
+ "sha256": "72e630b3cd932190dd425a8cfaa8ee7d8da7bff6f9015e59bf7d21625d2ee36c",
+ "bytes": 794
},
{
"path": "apps/web/src/i18n/historyTitle.ts",
- "sha256": "0b3e08f522561a699ad1d4c1d1655afc5f03dea5437c8fa642f0bba5a49effa3",
- "bytes": 4206
+ "sha256": "618d25378d90b5342a2ab3e31f96bb031eb96dbf11016273507a1b206c66b8e2",
+ "bytes": 4319
},
{
"path": "apps/web/src/i18n/historyViewMessages.ts",
- "sha256": "4f714eb34f91b1f8b9129e533ffb7068166d2038d12f31b056ec012c160bc657",
- "bytes": 9765
+ "sha256": "ea05c75ceb9d41b12841117cc7363cf5e21441855f075a8ba900b4a9f898f0e8",
+ "bytes": 9550
},
{
"path": "apps/web/src/i18n/hypothesisActionsMessages.ts",
- "sha256": "ba851afe8055b847e7f3ceca01483ea09478d70d98ca2cb264766ec1259f6b0b",
- "bytes": 15660
+ "sha256": "08bda913bef5f29f9bca63c57baea97ccac4815f5f2d6188db3ad5ca4d4f0913",
+ "bytes": 15349
},
{
"path": "apps/web/src/i18n/ja/activityMessagesJa.ts",
- "sha256": "3c461b1649ee9445eaab377b63e03b0d383d5567e274edad499828515360bcdf",
- "bytes": 10175
+ "sha256": "8229364a914ae70b42a8e61dfef60c6afcc579ba20c48edd1c0243a11fd35b6a",
+ "bytes": 10207
},
{
"path": "apps/web/src/i18n/ja/advancedMessagesJa.ts",
- "sha256": "034a8f7076b64dce1913175f05ccce0dbd226551b0a2fc1fecf41ec2d8905f1b",
- "bytes": 8175
+ "sha256": "ddc8f69b5a8285a05dd6f9ee17403dfafb8b4065f6888ccbf739d534fd62d987",
+ "bytes": 8211
},
{
"path": "apps/web/src/i18n/ja/apiNoticesMessagesJa.ts",
- "sha256": "e24784808bd10e8055d1e3db7100de1b8568bb2315b7c6a663ce6fd4158add25",
- "bytes": 3444
+ "sha256": "0af007f5c29070bf340b380855f4ca450adfaf193305916adc63331aae8b8633",
+ "bytes": 3456
},
{
"path": "apps/web/src/i18n/ja/commonStateMessagesJa.ts",
- "sha256": "7acb8c7c34516bae67833ac2bdc2247effe9a6fec1b83081ec8789b84e9d1f23",
- "bytes": 3468
+ "sha256": "46a06abc8d09a0e1c5e5a202269eae90e17608edf545bd0915d1c45011f1b0fb",
+ "bytes": 3476
},
{
"path": "apps/web/src/i18n/ja/coreMessagesJa.ts",
- "sha256": "02f63b491ea88e39573a80005a1e1a2d891c65ac300d1e601d99327858d7fe18",
- "bytes": 5302
+ "sha256": "52116795c842644fa662cc70590a8de8de4c5d58b4aa394d2720c8751e6b6779",
+ "bytes": 5358
},
{
"path": "apps/web/src/i18n/ja/credentialMessagesJa.ts",
- "sha256": "2f5fa12449f03db6481ad3559de60b259df3de7611b382b358a48110a8ae554a",
- "bytes": 7485
+ "sha256": "2d606a5bb18bdae248229ed4c9918951d100164798a96e09d285f7924b2dddce",
+ "bytes": 7573
},
{
"path": "apps/web/src/i18n/ja/finishingMessagesJa.ts",
- "sha256": "0699a21042ce8fcf56b1febc2c8f66b995f109c21bbb73c4e65254fa4207e87e",
- "bytes": 1790
+ "sha256": "0a1a9ba7caaa7584a937e70b4f21f9fa05d7aa67629998ce26919747535834a2",
+ "bytes": 1794
},
{
"path": "apps/web/src/i18n/ja/followupMessagesJa.ts",
- "sha256": "0b42204b034daa9f665f75815d8ded23639363873f21b8d1eefea12e2098fc7c",
- "bytes": 6946
+ "sha256": "042dc8a4a2a826a871cc9503571cc9a3a126cfb389d958376b6a92d89f1f2421",
+ "bytes": 6998
},
{
"path": "apps/web/src/i18n/ja/groundingMessagesJa.ts",
- "sha256": "91b6a5098c79bfd7cf08bc6a65db877321060e2567f25b7bbb8b42283f57b500",
- "bytes": 963
+ "sha256": "4e294bad1caedd40748b310aaa4980b16145ea2acdd9e0acaea8c6438b98bb25",
+ "bytes": 967
},
{
"path": "apps/web/src/i18n/ja/historyViewMessagesJa.ts",
- "sha256": "b73cfd2580549ed4dd463200cfd20ad47dce01febc02028f321f90c013ee5695",
- "bytes": 5602
+ "sha256": "034df4362dd156153debe4dc34d591db1393eecfc85f0ef63194f9bc344e2db3",
+ "bytes": 5638
},
{
"path": "apps/web/src/i18n/ja/hypothesisActionsMessagesJa.ts",
- "sha256": "c78357bfcf7c7340386f28e63e013a38eaba625ec9891ca12a670b95bd21c4fa",
- "bytes": 9225
+ "sha256": "4fde249d8f6a584efaf3a9e36ba6033367e885ce18904b3815bb1d57e16d2e26",
+ "bytes": 9277
},
{
"path": "apps/web/src/i18n/ja/judgmentMessagesJa.ts",
- "sha256": "972ad42fee6d0cec839475494b606550a1b806a99966889b5e27e79e1e7c97e0",
- "bytes": 3997
+ "sha256": "680a4d4c745d35ff7826f3a8ed14d8468699cebdc60f42e539410c76d3101c93",
+ "bytes": 4045
},
{
"path": "apps/web/src/i18n/ja/localRetrievalMessagesJa.ts",
- "sha256": "d5a390a05b93a8bc3184e6b4aec1788efa465e741601998c5565477ee89f4dbc",
- "bytes": 13361
+ "sha256": "0de26cf52e0f8ce24c553a35093ed69ae98ca0eb07f02bb068a88797e0ea2292",
+ "bytes": 13382
},
{
"path": "apps/web/src/i18n/ja/modelHelpMessagesJa.ts",
- "sha256": "c02724548bc3e5b88d9d4fcd727fe5474e5927a2284e27cd3c5d58c1740102d2",
- "bytes": 1665
+ "sha256": "daca312bca5726544782d9aab28b771d055f53e405a16398fb4338f0fb409ba3",
+ "bytes": 1681
},
{
"path": "apps/web/src/i18n/ja/oauthMessagesJa.ts",
- "sha256": "b3999ee8a848434bead62181e38f27099091b502eaa26e191607662d4980d663",
- "bytes": 6574
+ "sha256": "5f1441401d1efd627e306a460eee7182d5c7790f3e02df1d64e0359ae3e05b56",
+ "bytes": 6654
},
{
"path": "apps/web/src/i18n/ja/parserErrorMessagesJa.ts",
- "sha256": "3aac61f80dd1a40a0ffb22b0197ea11ecc5acea758361439ba4aa724bdb44554",
- "bytes": 3771
+ "sha256": "71a64f72846368775588d9a8e49c20b4aa03e8ab351c8afd317c6a9d8e7ca8e5",
+ "bytes": 3843
},
{
"path": "apps/web/src/i18n/ja/recordDialogsMessagesJa.ts",
- "sha256": "8be80d4578e637156dd32f190f1f39ec0ee9cb0d878f8f398415ff0abe5640b8",
- "bytes": 7407
+ "sha256": "0ebcb321cc8767b4a502a8cabfbafaea135452cb09927b9e8b4f3c3e44c0c056",
+ "bytes": 7459
},
{
"path": "apps/web/src/i18n/ja/recordPanelsMessagesJa.ts",
- "sha256": "d26bf1d147326707b72bbcaead3a5ad0a31f4a4427794281e873f5c1f8d968ae",
- "bytes": 8876
+ "sha256": "932b124f51633409f5f035c15ebb5078da877865febd5385f9af5fab621c8303",
+ "bytes": 8952
},
{
"path": "apps/web/src/i18n/ja/remainingMessagesJa.ts",
- "sha256": "182bcc4bbe4ba170c9196d4bb238b45e9fc1b0ec065350d36a7f952b380d7abc",
- "bytes": 9234
+ "sha256": "78e78aadf20f197a8469bd4cd0e6f11113f16429f7de5f335d2795b566287e74",
+ "bytes": 9290
},
{
"path": "apps/web/src/i18n/ja/researchMessagesJa.ts",
- "sha256": "1dd8c063e12034f650760eb210a6ba4ea20ce5928be824867ba4ca626526ff6e",
- "bytes": 7008
+ "sha256": "c50bbc11e05b034eebb5506c0329666b6a31bbef518016b8dda5bc49ee6e1b7c",
+ "bytes": 7052
},
{
"path": "apps/web/src/i18n/ja/researchPanelsMessagesJa.ts",
- "sha256": "9b941b1089f98a0a905632d6b53adb1b6ae441529afdf237abb74a2957f1d8b4",
- "bytes": 8003
+ "sha256": "1566137756535dedc8d516c5d6777341b5b66387a17031754db2e9a4df66f80d",
+ "bytes": 8071
},
{
"path": "apps/web/src/i18n/ja/reviewPanelsMessagesJa.ts",
- "sha256": "a5e7265b19f8cd3704a4dcddcf60b7cfb4863a80c75e394c5f164a88d5b92ecf",
- "bytes": 7612
+ "sha256": "82ea3dc565ea7a48638b3d9df46224e425ec8e74ce8941529a6ed9e5622966bb",
+ "bytes": 7656
},
{
"path": "apps/web/src/i18n/ja/setupMessagesJa.ts",
- "sha256": "d549cc682d7edc1ea777cd1d2cd709b79bd5c7a60f08df07e08a5f1ca04125c4",
- "bytes": 5670
+ "sha256": "4c51b332794873de543ffc17e1cb7cdb2173983d3e948626d8302e32035ef8a2",
+ "bytes": 5706
},
{
"path": "apps/web/src/i18n/ja/sourceMessagesJa.ts",
- "sha256": "ef05e9d8e225824aec9d8a9b397d380a5b017093219722f4118d90646921ec02",
- "bytes": 7799
+ "sha256": "7d2fd3edd39e5b3053bedb7846081586e1debe923460375e7aa945c144a0cd98",
+ "bytes": 7867
},
{
"path": "apps/web/src/i18n/ja/traceMessagesJa.ts",
- "sha256": "408b1091ed2b99ef1c87c9cc5b4a9a4f252ec5ed8291329da023cd2b1fb1c2d6",
- "bytes": 10282
+ "sha256": "cdf65da24ea0ee3ce16a4519d39b2a01855a8a968a9b2ca4b4556fbe55b3097c",
+ "bytes": 10358
},
{
"path": "apps/web/src/i18n/ja/workspaceDetailMessagesJa.ts",
- "sha256": "d68a6d078258343eb6a5326b21a4f3097d3d707195a22b2e2c3243fa4f3b67a4",
- "bytes": 2754
+ "sha256": "8b739c04f460bfe2e333fa121bc6ea7b2faed3988bdd1e2da649fa258d2cdb0e",
+ "bytes": 2786
},
{
"path": "apps/web/src/i18n/ja/workspaceMessagesJa.ts",
- "sha256": "213e1cf46aedd471a9ce95b50d78fef67aa2aa7587cb3aa0725a731cbed8e2f4",
- "bytes": 9621
+ "sha256": "acf70df5a77eb7f863eb0b3eca1c9fbeea4c54fb64eb6411515fd3d1a15b1982",
+ "bytes": 9693
},
{
"path": "apps/web/src/i18n/jaScreen.test.ts",
- "sha256": "e831ebcae3af8774dba98e16add469386cb121082976c1dee347fdc72c631702",
- "bytes": 8699
+ "sha256": "7428eea0e8d8b73b7223f02552e8917507be5ba2a5359ea6dcd84c420f077b62",
+ "bytes": 8968
},
{
"path": "apps/web/src/i18n/judgmentMessages.ts",
- "sha256": "3660bf09405d4eb1353a693a9b886a7723a3619f23960da69ae8fdb5a20715e0",
- "bytes": 6778
+ "sha256": "ea901af6165ffa1ec89638bbd6fc7a09cddfa9d851ad2f69717f7380a09a21cb",
+ "bytes": 6683
},
{
"path": "apps/web/src/i18n/languageChoices.ts",
- "sha256": "32451f93b1fe710f09244a57b8d433a74354674067443b547a34c3697d621dd7",
- "bytes": 2591
+ "sha256": "7fef295ea5c4655d12f311168be6993396c884cdd0c1c0765d1ccd4fddef0474",
+ "bytes": 2726
},
{
"path": "apps/web/src/i18n/localRetrievalClaims.test.ts",
- "sha256": "09c9dabeb917d5ac08a5c490e89328ff7d96b9ef9857ae4d08153a149bd940c5",
- "bytes": 2328
+ "sha256": "a27df3f9305fec63260a559cebe2262ea422f15ac592ed984cd10e35c820a282",
+ "bytes": 2361
},
{
"path": "apps/web/src/i18n/localRetrievalMessages.ts",
- "sha256": "1184ebd3db25f48a100121406c6efecfcf3ea8762624be02abd218bcfb69efc3",
- "bytes": 22520
+ "sha256": "14db34556c0581dce30b352659443c3aff5ad691d84e95b5cef6c459854e51bd",
+ "bytes": 23214
},
{
"path": "apps/web/src/i18n/locale.ts",
- "sha256": "2e7e36d514f0c419929e453dae77d96f7808eca827cd979f020b044b187cd403",
- "bytes": 4240
+ "sha256": "afea634e3a266e2388114cbe33943cf2df619486aa822ca3a0122d986b8c9056",
+ "bytes": 4653
},
{
"path": "apps/web/src/i18n/messages.ts",
- "sha256": "23448c6546f9d4bdd75b2c051cec261e23ce4152e58c30ac8a75fa7ab68e7540",
- "bytes": 9805
+ "sha256": "c460bbb0ab27d7079acd1fe4a8cf54b4e55d8d3a873fde1213baca7c274afcc7",
+ "bytes": 10135
},
{
"path": "apps/web/src/i18n/metricFormatMessages.ts",
- "sha256": "002f0819473db887a7060441e488c8c490e024959b49bfb93920ad5cdd8e18e2",
- "bytes": 18552
+ "sha256": "41fe45333c4e788f51b011ccf08e5e3f9d8461205e09d47d113d5ff5baf0f9af",
+ "bytes": 21601
},
{
"path": "apps/web/src/i18n/modelHelpMessages.ts",
- "sha256": "fde35f0cc7f76a9dd59f5539b695deda068a823f5c42e064da9e345bf6231cb7",
- "bytes": 2530
+ "sha256": "33ce6a0ed516e67f59396723bdacf91061ee378582c2c7c6cb4a7eeb0c364513",
+ "bytes": 2505
},
{
"path": "apps/web/src/i18n/oauthMessages.ts",
- "sha256": "29f3d0f0446be98359c48e6554690f379e64e95b49d31ae686f5364cd8a9d732",
- "bytes": 10638
+ "sha256": "5b5762d602cea7b611f75316eefc2e47903d7e678ba96dc25e3bb4b85db9357c",
+ "bytes": 10456
},
{
"path": "apps/web/src/i18n/parserErrorMessages.ts",
@@ -2318,178 +2323,183 @@
},
{
"path": "apps/web/src/i18n/pickLocale.ts",
- "sha256": "e3166c80b1373f6a562a0f6f3099e1b729579dc666a08c9faa1424cebfd0c03d",
- "bytes": 355
+ "sha256": "8f9b77b4ebce249fd54ce4021677971d0bc998efed76fccf35f4f0f1e2cc7cba",
+ "bytes": 362
},
{
"path": "apps/web/src/i18n/plainWordingResources.ts",
- "sha256": "7a59ac5f1a1e118df2eb960ad7d9ed51a18d9c7835eac8e57a79b6b1f506b607",
- "bytes": 16966
+ "sha256": "37a769f62d7e940a14c097403dd671d28699275a38eecb1dad18563f1a04200a",
+ "bytes": 17488
},
{
"path": "apps/web/src/i18n/progressEn.ts",
- "sha256": "f6183e7f60a10335d711edf8bbfcd9a5db72c6ce6cb296725ac404d9ce31e5ee",
- "bytes": 12151
+ "sha256": "3e922294d50e2e74f1610030ff0982bcfe297764e40f488b4495b1e08dba6fe3",
+ "bytes": 13340
},
{
"path": "apps/web/src/i18n/progressJa.ts",
- "sha256": "e57d1e3e6e33c1229409b178a67c073ce5891185ee582be48f64e52f9086ed8e",
- "bytes": 13793
+ "sha256": "71f8dca109126f0bef316219ad3f31a1c12307f744e632e819e779f3fec34cbb",
+ "bytes": 14970
},
{
"path": "apps/web/src/i18n/progressKo.ts",
- "sha256": "8dcbd48948a0813822baca9b11c2e1e0f206fd5caa86c2370a8eaab373f489db",
- "bytes": 18403
+ "sha256": "b92a302d6331c4f425a48b4c70d48e05d83ab25696fa0d1914fa0e6fedc2c2e1",
+ "bytes": 18752
},
{
"path": "apps/web/src/i18n/progressZh.ts",
- "sha256": "4eef14688f7e6507f6469c64ba3ae8d097931875cf6885ada66987487f3a1a53",
- "bytes": 12052
+ "sha256": "f62549ee18519a9040f77163abf08bae8cc4206be01ee549e8db40073034547e",
+ "bytes": 13169
},
{
"path": "apps/web/src/i18n/progressZhHant.ts",
- "sha256": "edbd514c5953b4e01d47c161cbc2362b980fe151fef881aae0b9555d8e581a4a",
- "bytes": 12070
+ "sha256": "17fc6f6fb42b041da1605616b687f5294562a27d92756941d962de0fae044a98",
+ "bytes": 13187
},
{
"path": "apps/web/src/i18n/recordDialogsMessages.ts",
- "sha256": "b55166ec72ee4699f049389a8e7777d9c0654a323f2f170a27144637e778ae22",
- "bytes": 13009
+ "sha256": "d064e490cf93a339c19dcd1cb3610b3d6a628f5f1adb80fa4ae8e2b3826bc165",
+ "bytes": 12744
},
{
"path": "apps/web/src/i18n/recordPanelsMessages.ts",
- "sha256": "77f3954c2469b1513ddf7232e312bc70df6db48de2d2c388b52f06c5cf5faed7",
- "bytes": 15439
+ "sha256": "51bda788d9287d92400a09d907872cdfd8e028c48527d21f624f5d0843f13829",
+ "bytes": 15122
},
{
"path": "apps/web/src/i18n/remainingMessages.ts",
- "sha256": "f08f1dc44f9aee659d13ae50a898d77f7d706361b496c7cdbb4fc482bcf45803",
- "bytes": 15741
+ "sha256": "b8d06e7d6db2e7108a156b24025e0877519eaeae838cbd8168d45ffecc794a43",
+ "bytes": 15409
},
{
"path": "apps/web/src/i18n/researchMessages.ts",
- "sha256": "41c5cc77b2c7d309961e6a80dccdfd7030ef1439cec8c27166248b4ffa5a3c87",
- "bytes": 12268
+ "sha256": "3428986528bdbdb1f6a085be761bd846cec5628e41dfd1e4460d0fc2a0c1a6a1",
+ "bytes": 12003
},
{
"path": "apps/web/src/i18n/researchPanelsMessages.ts",
- "sha256": "158674efef73dcf203b7a7b0578211a3e16532674c7b35de468eca77aaafbde8",
- "bytes": 13781
+ "sha256": "775fb35e3685bb5555bf10f070395c8fba27f42bf927a0e6879e566982167b10",
+ "bytes": 13493
},
{
"path": "apps/web/src/i18n/reviewPanelsMessages.ts",
- "sha256": "a3188d0cd49641a6bf2824f2573d64aa3d365b76d0000180823814a93a1b0838",
- "bytes": 12458
+ "sha256": "54bdc7c5629f10008fa700470431cca57b4b621f7373eb0dec3955ec195e46a7",
+ "bytes": 12292
},
{
"path": "apps/web/src/i18n/sentences.ts",
- "sha256": "4d142059bc0ea31b3cc9557e47e02310c21d5474b20085894b1da075665c3b77",
- "bytes": 706
+ "sha256": "8feee445c4cf685d582d13c10212b129fd1cd84659b826b6e420ce0fedf8c66f",
+ "bytes": 708
},
{
"path": "apps/web/src/i18n/setupMessages.ts",
- "sha256": "8c5848880465020a4df189061d3580c8a98a1586c9e87e4977056d8e61253f7b",
- "bytes": 9197
+ "sha256": "1901bd71422710bc1a43ae6e67e5675b78b058fc17ba1428061bb2b77046b125",
+ "bytes": 9005
},
{
"path": "apps/web/src/i18n/sourceMessages.ts",
- "sha256": "251bc0fa24c14252d2c96f0aaadd839700045cca77d630ffacfbbefb32bebfee",
- "bytes": 12378
+ "sha256": "a4d19954db58df9f5ffcab4660b60f7b4c5b8fc8c989a692111a3a8c0dfdc9c1",
+ "bytes": 12138
},
{
"path": "apps/web/src/i18n/stateFormatMessages.ts",
- "sha256": "2169c1c5f1190acd0207ebb39c13bd237d5765fb3b12f22025c098c3fe360dbf",
- "bytes": 16508
+ "sha256": "0ac3e3e7da143ef78df2a5765d6b6375774aac663778910a6b50e488171b4752",
+ "bytes": 18790
},
{
"path": "apps/web/src/i18n/statusChinese.ts",
- "sha256": "beb99204454e2a8341fb4843949c1b6e8ff77f4a0760da2c34ffc8f7d0fe1d3d",
- "bytes": 5043
+ "sha256": "3d12b232a37bb3e7bf65357a436e08d881dceb9c0f8ceedf28493b7933d38a42",
+ "bytes": 5422
},
{
"path": "apps/web/src/i18n/statusEnglish.ts",
- "sha256": "caf375733d39ad9e9cd9da57fb770b0320c12fec2cb373c65f780099f7edb041",
- "bytes": 5282
+ "sha256": "0901b5fe568361c6c31eef72f3c21a4dc19fd0d1efb975eb02e1f76f6c0084fe",
+ "bytes": 5661
},
{
"path": "apps/web/src/i18n/statusJapanese.ts",
- "sha256": "e52df8d27d736da76c0e51d8209ba3a11d3f088c586fd9eb0972d4c02e146c49",
- "bytes": 5812
+ "sha256": "4c2eec076e7ace6eb7a32760ed4249f2d75a5d38892b96a0f454f7d4c7f82b4d",
+ "bytes": 6202
},
{
"path": "apps/web/src/i18n/statusResources.ts",
- "sha256": "6ccc34dff1bfd6c10dbbbcfdec2997705e058c3f6eef9e9d182134a0130cff56",
- "bytes": 6592
+ "sha256": "84ad39445341204e4b5a496891f6dd97e6219e4b5572ed8006f400ccab0d7885",
+ "bytes": 7104
},
{
"path": "apps/web/src/i18n/statusTraditional.ts",
- "sha256": "7233017698241aa41524877c9386adb84e2531107264b3c85ad8b24d84d3513c",
- "bytes": 5047
+ "sha256": "ca74017363d3c8f79d3118585b40cc7e6cf3a75c3c8dc19f531ec2e529fcbdf6",
+ "bytes": 5426
},
{
"path": "apps/web/src/i18n/testOrderResources.ts",
- "sha256": "7ec9ab5f64e52f7abddc8068482bac048da3ca1ad4072cdf55c1163a56c394b3",
- "bytes": 8006
+ "sha256": "598f8a48631c9d7f4bc5fb060d8a15d3ac802c91096b8bdb7751f484063eec78",
+ "bytes": 8380
},
{
"path": "apps/web/src/i18n/traceChinese.ts",
- "sha256": "5a73bda6f6688ad70e2af222a56c035a9ca8b4e110c2f03991b80815d8bdbaf7",
- "bytes": 2953
+ "sha256": "9748a5090a21d0ba10314be34c235729c35149071dfe7bc3230f03e17d421c1b",
+ "bytes": 3018
},
{
"path": "apps/web/src/i18n/traceEnglish.ts",
- "sha256": "f06bf4f80b017768ce5f2ca7a28e21313ac2399e1e912dbf9c6114e5833f6b64",
- "bytes": 3088
+ "sha256": "055dbe58d1c648d7795d274e829139b09d4d592da07e06152813ac0fe65fdf46",
+ "bytes": 3165
},
{
"path": "apps/web/src/i18n/traceFormatMessages.ts",
- "sha256": "1a4e3b00135adac072ab3566deaf2ae3656f1ce12230aa7f9648b526a48bfa16",
- "bytes": 10218
+ "sha256": "8a300825d9ea514a9b0cb1f5a4fd99bb52b78be3c24774fb7b5b1f96f49e5a4e",
+ "bytes": 11062
},
{
"path": "apps/web/src/i18n/traceJapanese.ts",
- "sha256": "51daafaa2d8d953fc6919d3d13bb5eced356458d571b6c2d50144f3a76115e72",
- "bytes": 3347
+ "sha256": "0cb1c6d10a16e4b5701fd3f74df6dd3d5fc5eb24cb219547ae4cd130a52635a3",
+ "bytes": 3424
},
{
"path": "apps/web/src/i18n/traceMessages.ts",
- "sha256": "f75a22e42cc334fa2e5109e9f5a3f888f36f4bb105b5ff39e5a05622af2c590a",
- "bytes": 16967
+ "sha256": "a49e856d9e4543079bd2f55a7dd5ac06992a3aa2d20e0fa77ea920e317567ab8",
+ "bytes": 16641
},
{
"path": "apps/web/src/i18n/traceReasonResources.ts",
- "sha256": "863030c1fb65d6e963e9a3419597dd3e556395c2cc3034a10e6625a7af445918",
- "bytes": 7324
+ "sha256": "f76465a6553d632dba9fb62207eca34af4f10d159d7c80bbfe7ed11737ca16f4",
+ "bytes": 7475
},
{
"path": "apps/web/src/i18n/traceTraditional.ts",
- "sha256": "bb6c65d337079f17627b5baa4fdc562227c99545679ece88e016c32463b562e2",
- "bytes": 2977
+ "sha256": "6236829c6b78f7c994e6fe0fead75064df2baff65007e91b27fd382525a40e68",
+ "bytes": 3042
},
{
"path": "apps/web/src/i18n/uiFormatMessages.ts",
- "sha256": "fb025d04f9dcd2f7db9a9acd3fa823dfb7e813e2a9fbafa6fd300e39df9f2329",
- "bytes": 18661
+ "sha256": "8f734ca405763309efd18551c53e57d86d033f2981bb56b810a217d03ef677d4",
+ "bytes": 20034
+ },
+ {
+ "path": "apps/web/src/i18n/uiText.test.ts",
+ "sha256": "001c32378906e0b3b2b2e8d4d5d7a2002e1a46d7dad157627a148057b63de1d4",
+ "bytes": 1389
},
{
"path": "apps/web/src/i18n/withColumns.ts",
- "sha256": "8a6cadd474fde415cf9a602393edbd2d5b628c3a4ba4d25ff1fc098e32e7216b",
- "bytes": 1203
+ "sha256": "7fb73a3dbf4044b5989dc6a3d350b325c051f0cb26f0a50859c098da7134929b",
+ "bytes": 1220
},
{
"path": "apps/web/src/i18n/wordingRules.test.ts",
- "sha256": "870be8773cfe36cb09ec48b50271e6d870fb9f642083406dd8f308dccc3b4126",
- "bytes": 13932
+ "sha256": "5de59ac52a965a6243c9ee4ab795d97681ad1a364cf8d6d3fce2a17da6ff4789",
+ "bytes": 14323
},
{
"path": "apps/web/src/i18n/workspaceDetailMessages.ts",
- "sha256": "5b4e5950f9c30b32f331ea09673efaf4dfcea273b9b9b17e5d28d57e5c09c5e4",
- "bytes": 4031
+ "sha256": "9b8607325281acf106aeca06e2f606f867ee17c5a06fb182754be5081032da99",
+ "bytes": 3972
},
{
"path": "apps/web/src/i18n/workspaceMessages.ts",
- "sha256": "74cf1cf623915a5e6ce1bc39d6ed8c89540795f084683099198d1ba8a713f45d",
- "bytes": 15708
+ "sha256": "81e247a851d56c2f1054bdbb60ad479c5e2bfe202ef855b01c494bc9afb23a6c",
+ "bytes": 15357
},
{
"path": "apps/web/src/i18n/zh/activityMessagesZh.ts",
@@ -2498,28 +2508,28 @@
},
{
"path": "apps/web/src/i18n/zh/advancedMessagesZh.ts",
- "sha256": "486ec1e1068496a86fd5471d804c13830b785d98e24872f65d4f97428f5f877a",
- "bytes": 6527
+ "sha256": "674d83fa503670903a38216b7cd86b2161272c0ad446d2f089bebdbae548189e",
+ "bytes": 6543
},
{
"path": "apps/web/src/i18n/zh/apiNoticesMessagesZh.ts",
- "sha256": "0708bf415de42d38033f94c796b1b3e250911e67843e9b37aabfb5e7d98f2020",
- "bytes": 2663
+ "sha256": "29be7fa9297078a17a6bb3de7613074900f2e629b576faca22874f37f02e679d",
+ "bytes": 2667
},
{
"path": "apps/web/src/i18n/zh/commonStateMessagesZh.ts",
- "sha256": "56c2872633d5fa0c14050b04e2bed22cd82f98374976a6db1b3ee8a19e2f6bbf",
- "bytes": 2884
+ "sha256": "a5fb912f6ac3cb102c343a6c5efa4717d62d554b2e7d0175a26ae8f8c59ca86a",
+ "bytes": 2888
},
{
"path": "apps/web/src/i18n/zh/coreMessagesZh.ts",
- "sha256": "4c166f38e2a4d66328bb0b7d6b4673ded14c7c3e0f9c80b16f6127c0434efb00",
- "bytes": 3964
+ "sha256": "9ee1916320c50483d0a806016334b01d8c8e1571a21f721116753d2f1b254b57",
+ "bytes": 3980
},
{
"path": "apps/web/src/i18n/zh/credentialMessagesZh.ts",
- "sha256": "84c8184b2722a725b8c5ff63804dbf05f11467ec919fd7555237ed2d9dd23cd7",
- "bytes": 5608
+ "sha256": "d45b7c68c15e6b88e31c38f18ab31efd3b1cf8532bd6a3238f16b435d70b527a",
+ "bytes": 5640
},
{
"path": "apps/web/src/i18n/zh/finishingMessagesZh.ts",
@@ -2528,8 +2538,8 @@
},
{
"path": "apps/web/src/i18n/zh/followupMessagesZh.ts",
- "sha256": "b47227cba0d52fd1d5c25381a972d1ade230f5b84c480356fd1989dbd87975cf",
- "bytes": 5214
+ "sha256": "eb7663f38fe93d69eb993458d25b68977f65e717c442f4398f5204e85c3d3678",
+ "bytes": 5226
},
{
"path": "apps/web/src/i18n/zh/groundingMessagesZh.ts",
@@ -2538,28 +2548,28 @@
},
{
"path": "apps/web/src/i18n/zh/historyViewMessagesZh.ts",
- "sha256": "0cd50b597f6ae360d8048afc0b2ad15c59b99045aa7b0770fca6a8bbeea411ec",
+ "sha256": "d0952eb701b09c5ef6a27f1cc5d7a1e8a00874f624732823250747e90233e2cc",
"bytes": 4465
},
{
"path": "apps/web/src/i18n/zh/hypothesisActionsMessagesZh.ts",
- "sha256": "91bc81d9f5272cefeafc05768ee51f89595505cf74af41b40a85a3503444d52b",
- "bytes": 7013
+ "sha256": "514f11a25256a390174293866f22da72dd544341d51bb3641de140a58ce7744e",
+ "bytes": 7025
},
{
"path": "apps/web/src/i18n/zh/judgmentMessagesZh.ts",
- "sha256": "088440b1043c38152fe0c08c1374b53f43614157d8941689c277d0b4fd19a5e0",
- "bytes": 3151
+ "sha256": "1d8bfbe1c4408e7146ade49b76cc1e042f3b397d3a55a8fe5c804b078be504d9",
+ "bytes": 3171
},
{
"path": "apps/web/src/i18n/zh/localRetrievalMessagesZh.ts",
- "sha256": "aeb91b2fa88cb58b3860899c7d9ae7b4448778653b83a78e2253699ecc50031b",
- "bytes": 9552
+ "sha256": "84383b00e2089892951e041eb8816032445f550bcd7c2f753bc355ea20ee63eb",
+ "bytes": 9509
},
{
"path": "apps/web/src/i18n/zh/modelHelpMessagesZh.ts",
- "sha256": "78b0f7d229c93d0e78844884776df45609e0e62fd062f26d0808cba9044fc94b",
- "bytes": 1231
+ "sha256": "b8fa218521e4a0e3fd5cd74ff3a7137a719bfea9821824e4cbc6515956e67caf",
+ "bytes": 1239
},
{
"path": "apps/web/src/i18n/zh/oauthMessagesZh.ts",
@@ -2568,23 +2578,23 @@
},
{
"path": "apps/web/src/i18n/zh/parserErrorMessagesZh.ts",
- "sha256": "4aebb261cdf446951e376bba341c051252367fff199a7c368a0cc8b4873a8e7a",
- "bytes": 2547
+ "sha256": "d4f6a260a5082ac1efd48969a7c52705bfcd0bd16b3bd6e9ea7123c46a83d1aa",
+ "bytes": 2571
},
{
"path": "apps/web/src/i18n/zh/recordDialogsMessagesZh.ts",
- "sha256": "43c8f1966a711f6355dfe00fbf5fc5959e1ba024ed5322e7b43ed7cfcff2d1fb",
- "bytes": 5888
+ "sha256": "c3041948e76b0dee1e1bfa7f83f8b85a8c44da05a71749b7dd4effc300174fc5",
+ "bytes": 5914
},
{
"path": "apps/web/src/i18n/zh/recordPanelsMessagesZh.ts",
- "sha256": "041cba3da1b96cdf01237fb7d8be371cabb50f5b576a1de4d04cd1d91c4a9855",
- "bytes": 6905
+ "sha256": "58dd7295309b32b8a7a5a185658d7a0e0c3dab129e78ae609ddda1f79b3c2a3e",
+ "bytes": 6903
},
{
"path": "apps/web/src/i18n/zh/remainingMessagesZh.ts",
- "sha256": "9b4df1ba25acb1ae79b182c46109dcb75f76e040d6c97a14f69bb931a70abc9b",
- "bytes": 7195
+ "sha256": "36f1251a10f644fa9b4cebfcfc364f3fb75541a382781813e40f0ff93c9b4022",
+ "bytes": 7211
},
{
"path": "apps/web/src/i18n/zh/researchMessagesZh.ts",
@@ -2593,38 +2603,38 @@
},
{
"path": "apps/web/src/i18n/zh/researchPanelsMessagesZh.ts",
- "sha256": "032cbc9b01f1efae89c6964c5a4bdb0cc34825eef6b3d1a7ed6745bcdc6180a8",
- "bytes": 6334
+ "sha256": "c5dacea30fab366ec7e2a71971dddac1859aa9ea55c3e0b470a7dbc0408813c9",
+ "bytes": 6342
},
{
"path": "apps/web/src/i18n/zh/reviewPanelsMessagesZh.ts",
- "sha256": "374f2ca1e724bd9eef8b47cbf9f5eb582c9ce1a83e901a5efd4635258d47436e",
- "bytes": 6048
+ "sha256": "45ab51576458104dec47811ad57438615abe0dbafb984eb5d800730271f38882",
+ "bytes": 6060
},
{
"path": "apps/web/src/i18n/zh/setupMessagesZh.ts",
- "sha256": "376e9e2643534ed5e87db678952ded44680e8a00cd4b097aa50fb830254afcd7",
- "bytes": 4270
+ "sha256": "f02a9d4bf9dcd3fa1849d844c8a47786a3f705f9807163ed3f36f675eae7bc2e",
+ "bytes": 4282
},
{
"path": "apps/web/src/i18n/zh/sourceMessagesZh.ts",
- "sha256": "763ce2d5a40e5efadc88985e2602ae20af83067303a78f3e262709bd7f6cdc0f",
- "bytes": 5542
+ "sha256": "c0d6d0447f84e1289432c15ec39074bab0aed8eae48f1f134cda374b251ef89d",
+ "bytes": 5562
},
{
"path": "apps/web/src/i18n/zh/traceMessagesZh.ts",
- "sha256": "b50170874a233801e326e623a0a7f666f242c6cd4ad9c6dada00a641bcea0b75",
- "bytes": 7920
+ "sha256": "6e9bd9fa3ab92e33399643f5a72bee2c10c7257c5681699ac49e78f191955b89",
+ "bytes": 7930
},
{
"path": "apps/web/src/i18n/zh/workspaceDetailMessagesZh.ts",
- "sha256": "75c523a6d22f8c4a9c59cd8b51103df430a85d2d151136ac82fbbf164884ee99",
- "bytes": 2007
+ "sha256": "e566dddee226e8c169ae1d54b21d5f8fbb3febfb5f7e14ab86f6dd2dd0eb30bc",
+ "bytes": 2015
},
{
"path": "apps/web/src/i18n/zh/workspaceMessagesZh.ts",
- "sha256": "6562f00fb08f60eab15cd5595dc324887af7578c9988c34d61f3ab75ac621c20",
- "bytes": 7480
+ "sha256": "b9539b8f3aba930f05d390f7f89b39c19dd45d3e01f023dc9ee32207b76fdcd6",
+ "bytes": 7500
},
{
"path": "apps/web/src/i18n/zhHant/activityMessagesZhHant.ts",
@@ -2633,28 +2643,28 @@
},
{
"path": "apps/web/src/i18n/zhHant/advancedMessagesZhHant.ts",
- "sha256": "b82f78717da755b06a224436c4a8a9b7e0a67a912e797c44d44d4840b8e1c670",
- "bytes": 6537
+ "sha256": "36f99847676a5012d249234bfaa410fef8ee13765c4606d9de09a734a34b2d46",
+ "bytes": 6553
},
{
"path": "apps/web/src/i18n/zhHant/apiNoticesMessagesZhHant.ts",
- "sha256": "56aa8eacb6c8c88fe09895e71bee4eb5679fe6da1e686ada31395670717c53e3",
- "bytes": 2667
+ "sha256": "81633ab27a831d15fa129971653438ffbc59919c741f421bc3a214eb71b9b2d5",
+ "bytes": 2671
},
{
"path": "apps/web/src/i18n/zhHant/commonStateMessagesZhHant.ts",
- "sha256": "3efddae9e9111df427c760b2eb8676069de4f51035390e034b39736b6f422f83",
- "bytes": 2888
+ "sha256": "decb6e5ba3b1c76cc3e08a51ad94be4ecba035fb7b043c4b49058b5b7a24fef5",
+ "bytes": 2892
},
{
"path": "apps/web/src/i18n/zhHant/coreMessagesZhHant.ts",
- "sha256": "46abe494a7fca2cf2a5f7b0b565168f2a5c4e08605b349b51fa3d9a6c49ab4bb",
- "bytes": 3962
+ "sha256": "d79b8331fb2a107c2665b81662f2dbf8275b7aa2de6e9efe3fdcc621f8ccfe15",
+ "bytes": 3978
},
{
"path": "apps/web/src/i18n/zhHant/credentialMessagesZhHant.ts",
- "sha256": "b868623e03158de0f927348504258a97d045ed10264e244baeb37d84dd4a55ac",
- "bytes": 5627
+ "sha256": "ebde5e650785e0331e1b0c508e3f54d4b47f004176edd6bcca3efdef7a78ce75",
+ "bytes": 5659
},
{
"path": "apps/web/src/i18n/zhHant/finishingMessagesZhHant.ts",
@@ -2663,8 +2673,8 @@
},
{
"path": "apps/web/src/i18n/zhHant/followupMessagesZhHant.ts",
- "sha256": "f7f91998c70265d41a5ecf7558cd8f99b4acd3ec31c4585ecbb980dc59802d60",
- "bytes": 5230
+ "sha256": "5d4a8bfca8695ce38abbd302c77083426a6d8ffc8f215a57907fb6b7c8941ea3",
+ "bytes": 5242
},
{
"path": "apps/web/src/i18n/zhHant/groundingMessagesZhHant.ts",
@@ -2673,28 +2683,28 @@
},
{
"path": "apps/web/src/i18n/zhHant/historyViewMessagesZhHant.ts",
- "sha256": "9ea9a1ab5ce5aaf53b34a0115fe10d7e9aa3655779cd38489b5cdf2dae2060c7",
+ "sha256": "61ab6a22d9d99e9c48edd8f764f14647330e59370f5374741663fcc7106ef183",
"bytes": 4469
},
{
"path": "apps/web/src/i18n/zhHant/hypothesisActionsMessagesZhHant.ts",
- "sha256": "4e548d4ec435f59475ad3decbffced9d117433cb2eb80099581f5061476b6288",
- "bytes": 7038
+ "sha256": "a40b10e1878fd433e820e43b9613642e18d0c65e92a13026675381af281ad466",
+ "bytes": 7050
},
{
"path": "apps/web/src/i18n/zhHant/judgmentMessagesZhHant.ts",
- "sha256": "87b72d5b205e041184a1ffd0349d4c92936f0e7a46fe7b43040ed70e20cfceb5",
- "bytes": 3155
+ "sha256": "dbd1020fb414d84ed6430218a095f6491b3098c1c72eca79c490b28411f58718",
+ "bytes": 3175
},
{
"path": "apps/web/src/i18n/zhHant/localRetrievalMessagesZhHant.ts",
- "sha256": "c5d5b17eb574df1c94190877dd09cee484ddebe95d26db342628278a946a2ccf",
- "bytes": 9572
+ "sha256": "a4a6d85ae0b54f091812574ac2624308161f620871b93dc8b8277ac11ee31112",
+ "bytes": 9529
},
{
"path": "apps/web/src/i18n/zhHant/modelHelpMessagesZhHant.ts",
- "sha256": "825166a790b89bc9771566606f6d205224b1beef3fecd95273d812e0f56768e1",
- "bytes": 1247
+ "sha256": "a61e2f0a93735b5b98de0784c79616b7fecaa0b880df80e47ae9c7c2416096fb",
+ "bytes": 1259
},
{
"path": "apps/web/src/i18n/zhHant/oauthMessagesZhHant.ts",
@@ -2703,23 +2713,23 @@
},
{
"path": "apps/web/src/i18n/zhHant/parserErrorMessagesZhHant.ts",
- "sha256": "918f540d7335215c22bd4bc6d11b3718c4deafa2f643bd13caee73478c798da9",
- "bytes": 2551
+ "sha256": "2416a32e648d15d47133ad8b8741649808d2d48c71052f2d24947278f16e1812",
+ "bytes": 2575
},
{
"path": "apps/web/src/i18n/zhHant/recordDialogsMessagesZhHant.ts",
- "sha256": "dd6382826b2df1d6a9b18e1e977d272f6c212e905f7ab313130e218de91ce78d",
- "bytes": 5898
+ "sha256": "9e5086880e4d498c9ea14e12ce8c71dd90b0e84a75c61fc59d414a6c873483ae",
+ "bytes": 5924
},
{
"path": "apps/web/src/i18n/zhHant/recordPanelsMessagesZhHant.ts",
- "sha256": "b302a80611a0d9b4f939c722658b46372ff630b7d5f05bc33020556f2c711be6",
- "bytes": 6912
+ "sha256": "6bbdb0d2c2fa56eb7977baba7878a48d8f68a5b746c9d3f6f0d8fee1c923e8c4",
+ "bytes": 6910
},
{
"path": "apps/web/src/i18n/zhHant/remainingMessagesZhHant.ts",
- "sha256": "4ba763a3a3d0b27c3d3dc1be2ecc1e6f62ae0b5116114edff38f8b69447b2c47",
- "bytes": 7229
+ "sha256": "94c3a3626601c63143b351c18a2988779a0be393fbd7b7d3095878beb6caee52",
+ "bytes": 7245
},
{
"path": "apps/web/src/i18n/zhHant/researchMessagesZhHant.ts",
@@ -2728,48 +2738,48 @@
},
{
"path": "apps/web/src/i18n/zhHant/researchPanelsMessagesZhHant.ts",
- "sha256": "48d5af9666d902d121d784c93e90d62ea27e1b5d709f54dc4a6d7d3560142235",
- "bytes": 6347
+ "sha256": "085cfceadff1ea31b2826924a4d6492db582f614f672dc094459a3a1ed8b103e",
+ "bytes": 6355
},
{
"path": "apps/web/src/i18n/zhHant/reviewPanelsMessagesZhHant.ts",
- "sha256": "a7850c0795232e5a2023409682bf5476bf000a91443fa41172704299e553bec8",
- "bytes": 6049
+ "sha256": "3be47d0105724b46cedc28ce8834041517c8ec91038f5ad67faf8600c1f68804",
+ "bytes": 6061
},
{
"path": "apps/web/src/i18n/zhHant/setupMessagesZhHant.ts",
- "sha256": "625b7064eb2c5ddc08461980bf0c653136e60a705223317a2f381619373962fd",
- "bytes": 4286
+ "sha256": "2852f6b9fb94173142b8dc4b366b9acab9a082a7baf2f407c44a67e6bed404bc",
+ "bytes": 4298
},
{
"path": "apps/web/src/i18n/zhHant/sourceMessagesZhHant.ts",
- "sha256": "da02551fc3378c285d15859908bb770fce45d234e876c02ff336a76991df6637",
- "bytes": 5555
+ "sha256": "dabbbf7875d7a73a70bffae092b1df11da6f8606674b55d640a1f77ed6d366af",
+ "bytes": 5575
},
{
"path": "apps/web/src/i18n/zhHant/traceMessagesZhHant.ts",
- "sha256": "4fe99cd1bde8e5cf2428036433b48d444cadfd6b7354333c4809ce673483a521",
- "bytes": 7933
+ "sha256": "9260fde4f9230757bd1060c2fad4797f4d8fa90901bb1489158413d4e12d674b",
+ "bytes": 7943
},
{
"path": "apps/web/src/i18n/zhHant/workspaceDetailMessagesZhHant.ts",
- "sha256": "29dd70f4e7ecbf071604177f8d2a5c861dc88c6aebdad314769b4bc35833d92f",
- "bytes": 2005
+ "sha256": "889614705d9371b026fe1ddf3229274069cc9444de76ac7a06b4b022b7596343",
+ "bytes": 2013
},
{
"path": "apps/web/src/i18n/zhHant/workspaceMessagesZhHant.ts",
- "sha256": "e08758b72bb82891b0f88805e02db8053a123401ba235a99ef2939fd97de8793",
- "bytes": 7514
+ "sha256": "5c4a8bfa70c85e1db947a22c81b2aafcb86afa156bb4ec94e6bfdba0af1e8f7f",
+ "bytes": 7534
},
{
"path": "apps/web/src/i18n/zhHantScreen.test.ts",
- "sha256": "3b6e009c3cd57ceae83c89eb0f149fd4a192cce7035abd2806b4637e73926c5d",
- "bytes": 11238
+ "sha256": "b08e71ef5abc28e7eecba47e3ddc4c3f31e5f12ec8718b5504fc297d7de5d019",
+ "bytes": 11585
},
{
"path": "apps/web/src/i18n/zhScreen.test.ts",
- "sha256": "84da741c0497b7e6438074737ace8da301d13b046b4f072407fa60e9a574e9b7",
- "bytes": 8803
+ "sha256": "d10c07c0d3b327c93ebe3030b52fd326f7527ed995805bf69bdf1715ebf1ae0d",
+ "bytes": 9158
},
{
"path": "apps/web/src/main.tsx",
@@ -2778,18 +2788,13 @@
},
{
"path": "apps/web/src/narrowLayoutRules.test.ts",
- "sha256": "e1ce29dc550b80197befd2067339f6d36d08d341218a9337e3ab722d38bc1761",
- "bytes": 6689
- },
- {
- "path": "apps/web/src/preview-timeline.tsx",
- "sha256": "035099afc0c013b87bd2f61e408b064e7c4d9c8fb50545e6b3d27339b52a8d1e",
- "bytes": 28
+ "sha256": "58ab37efde9939feb7acd176267a1d88fc54ee08ecd2afd27fdcdd782b0edc22",
+ "bytes": 7054
},
{
"path": "apps/web/src/sourceDisplay.test.ts",
- "sha256": "9e95e927122c48ea24490a539643c1bd1f2a3bbec85f8c5df2cddbde82cfd855",
- "bytes": 1057
+ "sha256": "7301490697ee075b551b60b8ae2d68c63ae2214579ad1e6da5bb730c81c20e2c",
+ "bytes": 1100
},
{
"path": "apps/web/src/sourceDisplay.ts",
@@ -2798,18 +2803,18 @@
},
{
"path": "apps/web/src/styles.css",
- "sha256": "9e1985afd5cbad3d22f21636159b9fae8c52c9c5114f36512f55cbe4f373c082",
- "bytes": 22558
+ "sha256": "4a045dea56b45b901e978b2b50a187250fe1cca585875649031a550932e7a651",
+ "bytes": 23862
},
{
"path": "apps/web/src/test/jsdomTopLayer.ts",
- "sha256": "544f7ce85aa84cdcf9ce479a722a73a70a29ba2c0d034acce67d2f595bf1e373",
- "bytes": 835
+ "sha256": "5ab3b073b13af439df19a62abb9282c19be6b1f327ed180bd744c4dd110da667",
+ "bytes": 841
},
{
"path": "apps/web/src/testSetup.ts",
- "sha256": "439299de2868d30543e93fdf1385a4c6cf5731bc2f8d4517d0de1ae97956e366",
- "bytes": 405
+ "sha256": "d37ae671a4cb33b93f97ba756e184b67ec4b41c056f1bae7193700a47e4f0a38",
+ "bytes": 410
},
{
"path": "apps/web/src/timelinePreview.tsx",
@@ -2818,8 +2823,8 @@
},
{
"path": "apps/web/src/typeScaleRules.test.ts",
- "sha256": "144e34be972db8f63f42bea91cafd73cb81d41b9abb9e45f6ac476ad8365212a",
- "bytes": 1920
+ "sha256": "5cfc794de17713350752502782872e6a3c4cf5d36162e1ebb0a0d0fefa45ea33",
+ "bytes": 1990
},
{
"path": "apps/web/src/types.ts",
@@ -2828,13 +2833,13 @@
},
{
"path": "apps/web/src/vite-env.d.ts",
- "sha256": "424faf9241dd699dda995b367ed36665732da1e6ec1f33b2fd40394488ecac92",
- "bytes": 39
+ "sha256": "65996936fbb042915f7b74a200fcdde7e410f32a669b1ab9597cfaa4b0faddb5",
+ "bytes": 38
},
{
"path": "apps/web/src/workstation.css",
- "sha256": "ed5fe3509cb88b96ee93264fb467a67a0192c0002ed8802b129a1dae74fc498d",
- "bytes": 74820
+ "sha256": "1465476be92d2466c12edb2641635dd6e957d026007c6e0da2d026ac711e74a2",
+ "bytes": 84651
},
{
"path": "apps/web/timeline-preview.html",
@@ -2843,13 +2848,13 @@
},
{
"path": "apps/web/tsconfig.app.json",
- "sha256": "ade56e5ebdec988dd5269313a2487b3fb3eefae5f70aba5b3b9f6eefd0c44a16",
- "bytes": 568
+ "sha256": "ec45293782914673548ce66cf2727e7192a6a0f80d8650d9fe40aa895caf0b55",
+ "bytes": 567
},
{
"path": "apps/web/tsconfig.json",
- "sha256": "c3c1a5af0c9ed5727b295bd39076550c6a48a7a7d8a79eb29adc4e6c370830f0",
- "bytes": 120
+ "sha256": "e8d6f7b50828db3953e21c7e92584f9206b3cd2551b4012507372afbb7fe9ce3",
+ "bytes": 107
},
{
"path": "apps/web/tsconfig.node.json",
@@ -2858,8 +2863,8 @@
},
{
"path": "apps/web/vite.config.ts",
- "sha256": "4378a2ea0a6e79cc63ca96d6705a33edd186046e43039c4184446b47969763e9",
- "bytes": 522
+ "sha256": "c77e0fa23c6eddce8d0922b899a847ec9bfbc1a58088a2422ed7e75b02632148",
+ "bytes": 501
},
{
"path": "config/architecture-conformance.json",
@@ -2928,8 +2933,8 @@
},
{
"path": "config/module-responsibility-budget.json",
- "sha256": "6e9f7bb4e27dc00c8edbe7b23ca6732df6a01c422ce0d88cff02759affba4ecd",
- "bytes": 8580
+ "sha256": "ddc4b55de8ddf3b43add5fa20d33c5e0a6a1de3df2cc5b2acb8655db701e4a20",
+ "bytes": 9947
},
{
"path": "config/post-d4-baseline.json",
@@ -2973,7 +2978,7 @@
},
{
"path": "docs/BETA.md",
- "sha256": "bd0b80167226de0a4737254bc8610f1409d7d38764d48fe156b06ba6e255d81c",
+ "sha256": "b10b74e6969ebfbe121309a0afb2973589263d1322824223f657b9ec810d6b88",
"bytes": 15322
},
{
@@ -3153,8 +3158,8 @@
},
{
"path": "docs/architecture/verification-profiles.md",
- "sha256": "9319dfb3cf6eadcbcae238ef061542bd83892a8175aed451d80c42a0b90f92e1",
- "bytes": 5729
+ "sha256": "4bf1da232faa8b86c154ad95c612c85af9e9f725a6dcacacc7d0a0f9fbc28142",
+ "bytes": 5810
},
{
"path": "docs/brand/README.md",
@@ -3206,6 +3211,11 @@
"sha256": "6bbec26ab2474fa97bf481422a5a453de77f10de7eab03f9ce9a22876bb78df2",
"bytes": 2299
},
+ {
+ "path": "docs/releases/v0.2.0-beta.4.md",
+ "sha256": "7950b01eb040aa12ed66535ca802780a253f789065c8a12bf4ac6665664bd714",
+ "bytes": 4520
+ },
{
"path": "docs/screenshots/01-trace-verdict.png",
"sha256": "800800b0e2bcfdd290d2f911618aab8a8e60e8e803d2400df9829cd61a351989",
@@ -3713,8 +3723,8 @@
},
{
"path": "migrations/versions/3b06e4f87d92_policy_digest_history.py",
- "sha256": "be94048bf0570366a7a1c4f30bfe9ef26321d96809ca3319eabc9e68a2fd929d",
- "bytes": 3821
+ "sha256": "3f8490aedb8d2cf71c445cdeea63dfece3526dbbd04349fcbd6d0b107222a536",
+ "bytes": 3825
},
{
"path": "migrations/versions/3dca0215d6a9_f4_outcome.py",
@@ -3748,8 +3758,8 @@
},
{
"path": "migrations/versions/7b06e5a9d3f1_a06_full_project_memory.py",
- "sha256": "307db917e9a4e6a674c6f4bf35c4dd0750d70c267fecf5c1e70881e251655b9b",
- "bytes": 3123
+ "sha256": "e6793392ca913ad4528d7faf7213d9d509d5f603b18baf94f76645faa9efe765",
+ "bytes": 3051
},
{
"path": "migrations/versions/8c13f6b2a4d9_a13_local_auth_sessions.py",
@@ -3888,13 +3898,13 @@
},
{
"path": "package.json",
- "sha256": "7ebec460cfd37636034bb196caf162451358e5be0962fa19e36a5bb89e1f88be",
+ "sha256": "508dac102ac112a581fc16a93723d89c3bb8b4789b2d688d5d87d7101614afbe",
"bytes": 106
},
{
"path": "pnpm-lock.yaml",
- "sha256": "7c92c27161a1705eb01b485f6b3d494400f5d8e18144217e1913e55221becaf5",
- "bytes": 94958
+ "sha256": "6f59c72f4b62a617a4043e09c4fe17d2212f867a42f9c5da207cfdcd2745464a",
+ "bytes": 129123
},
{
"path": "pnpm-workspace.yaml",
@@ -3903,8 +3913,8 @@
},
{
"path": "pyproject.toml",
- "sha256": "46eb245f7bf107b5d6489ccb7ca3a5927151f78d60d4d2ed160b4d63247b78a7",
- "bytes": 1989
+ "sha256": "361a88d4f8946a7ac84ad4dc91e9638d0871e4b267c6329a35d38d1e2f94bd72",
+ "bytes": 2454
},
{
"path": "qa/scenarios/hero_6g_current_core.py",
@@ -4308,7 +4318,7 @@
},
{
"path": "src/thoth/__init__.py",
- "sha256": "c36f5a2c2fd12d07d37f18e6ca5f0715f4a8a5c069984a38c336d8631663b899",
+ "sha256": "4e3584a994cb8ae6641a6a583403fa35517272033826436590f6afb86daf230f",
"bytes": 56
},
{
@@ -4393,8 +4403,8 @@
},
{
"path": "src/thoth/adapters/connectors/http_read.py",
- "sha256": "44ba72cebf0dde5d4fad206091b79449b78b6ac742c20599f197e4b4fc5520b5",
- "bytes": 8337
+ "sha256": "7cd0d7a893e05e16515308592bea49a084550755e990a1c195d02ccdfc6ab53b",
+ "bytes": 8518
},
{
"path": "src/thoth/adapters/connectors/local.py",
@@ -4413,8 +4423,8 @@
},
{
"path": "src/thoth/adapters/connectors/postgres.py",
- "sha256": "534125322625826456d2501e0f505f5997acbcf9bb9eda7cf347efd889e5cbd5",
- "bytes": 6615
+ "sha256": "cce583924b95360f1b0c5f8e89273d592bf7770577458731e080b7abee4557a1",
+ "bytes": 6790
},
{
"path": "src/thoth/adapters/connectors/project_public_web.py",
@@ -4438,8 +4448,8 @@
},
{
"path": "src/thoth/adapters/connectors/s3.py",
- "sha256": "309c15edb8104aded60c2bf711ff23de4bf27e46ace2cbae978660593f527f8e",
- "bytes": 6694
+ "sha256": "902f386e19ffffed8453024df8885f5fa69b32e0b440328751ce15b821831ecc",
+ "bytes": 6648
},
{
"path": "src/thoth/adapters/connectors/site_discovery.py",
@@ -4508,8 +4518,8 @@
},
{
"path": "src/thoth/adapters/evaluators/__init__.py",
- "sha256": "5793b548522565bbb89e310f037209279a9494bf37a2d5c97f2eef9d8100e2d5",
- "bytes": 295
+ "sha256": "5824a2cdcf54eb72952498f0b36be3c3adc5194e7a1543b4e5b32e3ff1260c09",
+ "bytes": 294
},
{
"path": "src/thoth/adapters/evaluators/catalog.py",
@@ -4638,8 +4648,8 @@
},
{
"path": "src/thoth/adapters/memory/relation_projection.py",
- "sha256": "db5d9f6337a15d1b8f94ae9e158a53f8de1cc7af6e0a533f042a6280bae299d5",
- "bytes": 588
+ "sha256": "10308322dd0c68eb521add9bab068ba9228a022346f58d2e484e4331a24fadd6",
+ "bytes": 574
},
{
"path": "src/thoth/adapters/models/__init__.py",
@@ -4723,13 +4733,13 @@
},
{
"path": "src/thoth/adapters/models/codex_account_usage.py",
- "sha256": "04e8e210b34b0975de68f646d325ca0652ef70764444a752ff381bdd08596a51",
- "bytes": 4036
+ "sha256": "ac931d9370fdc7b40371837b759e74d35b328c746e53aec56f22e587d4c59a15",
+ "bytes": 3988
},
{
"path": "src/thoth/adapters/models/codex_app_server.py",
- "sha256": "d1da91f0a11ad94939e1044df2b5ee419a776f3965878766d284b18d96dac769",
- "bytes": 8930
+ "sha256": "b55a7e58e239941660991a793d58d829f44dee9501715d1bf0a5919bb3d547d8",
+ "bytes": 8995
},
{
"path": "src/thoth/adapters/models/codex_broker.py",
@@ -4773,8 +4783,8 @@
},
{
"path": "src/thoth/adapters/models/http_rejection.py",
- "sha256": "c084c5c204216cfaca12b321e996ab4164421dba8accf0bdfe18df3e8067762b",
- "bytes": 4751
+ "sha256": "ca60717370ac66e05b9c7b7dbc45008e89f53b42da1336f903ad215567675f4d",
+ "bytes": 4697
},
{
"path": "src/thoth/adapters/models/hypothesis_contract_text.py",
@@ -4818,8 +4828,8 @@
},
{
"path": "src/thoth/adapters/models/registry.py",
- "sha256": "15e547bb4945c416bf8eb5e9e8c53186b298840700bed700915ec0eb891d84a7",
- "bytes": 1703
+ "sha256": "5128fc6cc5fed5a1a146bb4176fef9bbc2351d39ca194945227e32ee3aa2ad76",
+ "bytes": 1689
},
{
"path": "src/thoth/adapters/models/response_events.py",
@@ -4933,8 +4943,8 @@
},
{
"path": "src/thoth/adapters/parsers/docling_pdf.py",
- "sha256": "d9023ceb8962552cb1bea84d57548b50a7bdbc59159ffbf01a6979fd71618d44",
- "bytes": 5016
+ "sha256": "87f3bdbb42b77d5cf9e6c3a3d22e79181969d5d3e33c39196ef383525e4535a6",
+ "bytes": 5039
},
{
"path": "src/thoth/adapters/parsers/docling_structure.py",
@@ -4948,8 +4958,8 @@
},
{
"path": "src/thoth/adapters/parsers/document_time.py",
- "sha256": "a1e2f3cde5e1cd8e5a43a66d8e1d1da8363c350789f8156d6d37e79426bcbd43",
- "bytes": 13350
+ "sha256": "c26f8581d3a5039b263bd0be912339e3ba845d322b8bae6e20cd0f46ce8c2fad",
+ "bytes": 13378
},
{
"path": "src/thoth/adapters/parsers/docx_parser.py",
@@ -5118,8 +5128,8 @@
},
{
"path": "src/thoth/adapters/storage/alembic_migrations.py",
- "sha256": "4f841c1cc6396f6c3d82f6e580fc3051b1d001da1d0353c1145546e81282b796",
- "bytes": 7295
+ "sha256": "7574aebb2b2ed4406ed90f5bc94baaff6175c0ac5247e970a20b31090fffbb81",
+ "bytes": 7273
},
{
"path": "src/thoth/adapters/storage/artifact_catalog_rows.py",
@@ -5143,8 +5153,8 @@
},
{
"path": "src/thoth/adapters/storage/auth.py",
- "sha256": "8cbdd03ba2e759cd5220b06e3e06a3e115687e9ca9cb736d6c398dbf85154132",
- "bytes": 1844
+ "sha256": "6b597564918bd77af7e7a0208e43d4f886959cbb577dc778b2e2cfaf82011b14",
+ "bytes": 1790
},
{
"path": "src/thoth/adapters/storage/baseline.py",
@@ -5273,8 +5283,8 @@
},
{
"path": "src/thoth/adapters/storage/field_measurement.py",
- "sha256": "180d71bbad2b62340821b70ad3bf9ce1cddef7d6c1f4d11b8d06c5c1269ad7b2",
- "bytes": 6099
+ "sha256": "ec03d7cbff003ffddfd0f6816d2c1099e0cb31f6f7cdf448913f2aff5f66706e",
+ "bytes": 6047
},
{
"path": "src/thoth/adapters/storage/governance.py",
@@ -5328,8 +5338,8 @@
},
{
"path": "src/thoth/adapters/storage/object_prune.py",
- "sha256": "968d49f49b383a60631def53d97e91632098ea42889c7774cd3ccf69e5096949",
- "bytes": 13643
+ "sha256": "7c761ebd509c5d8d15323e507db2c51528c29272e7c07a4daafe08994358d37d",
+ "bytes": 13748
},
{
"path": "src/thoth/adapters/storage/objects.py",
@@ -5368,8 +5378,8 @@
},
{
"path": "src/thoth/adapters/storage/project_purge.py",
- "sha256": "439edd1d4a0beff1c10f905adce37e6909f5590818a42140b3898e1cf828ab51",
- "bytes": 10116
+ "sha256": "281b2193887576ffb6bac2cf116a312f51d27e3f52fc94126a463c7ef7125f69",
+ "bytes": 10227
},
{
"path": "src/thoth/adapters/storage/projects.py",
@@ -5398,8 +5408,8 @@
},
{
"path": "src/thoth/adapters/storage/research_queue.py",
- "sha256": "5b6cd78598ab6c6e65ff4125f22b6add10157438f7c0d5514ecaeb095ea6c913",
- "bytes": 3258
+ "sha256": "243a2fca9fe31a3fab373ddbfe3a2821e9071391ac61c286ac8bb6f75fc5825e",
+ "bytes": 3236
},
{
"path": "src/thoth/adapters/storage/research_schema.py",
@@ -5673,8 +5683,8 @@
},
{
"path": "src/thoth/application/commands/investigation.py",
- "sha256": "56c8b0887490c47393f597587de97805a9c9eec7179fe73a6721caaa0a77b7fe",
- "bytes": 10963
+ "sha256": "fc25ff53b522dbab9de1a1231ec6585bb58c86cefda125ac589a1021dacdb9f8",
+ "bytes": 10925
},
{
"path": "src/thoth/application/commands/judgment_review.py",
@@ -5803,8 +5813,8 @@
},
{
"path": "src/thoth/application/commands/projectpacks.py",
- "sha256": "525fc23114d1a72fd7ae7df2a9eaf60bfc832b7273e1c0d0da4c199daf14d60c",
- "bytes": 2868
+ "sha256": "f5117bc6681e635caa8cfdab5246fb11c84bb658a05744ae934920e5a531c427",
+ "bytes": 2846
},
{
"path": "src/thoth/application/commands/projects.py",
@@ -5908,8 +5918,8 @@
},
{
"path": "src/thoth/application/commands/sources.py",
- "sha256": "c516364e7e8cab54db718e6006e3af32084f90eeecb5c17cf9dea57f1251c4d6",
- "bytes": 19296
+ "sha256": "c204c5db071d6496ce3eeb3a6a6082d7d835e6f0836e0f1570b03f528c6f70b6",
+ "bytes": 19314
},
{
"path": "src/thoth/application/commands/thread_analysis.py",
@@ -5963,8 +5973,8 @@
},
{
"path": "src/thoth/application/reducers/hypothesis_reducer.py",
- "sha256": "d58b280721967bb814f1879e0a62d81953e255d121c9cdb0ab665085968d1ee0",
- "bytes": 1491
+ "sha256": "b58694474c9b6340a0fcb9988cd6748cb4844a7485fb301a942d597c122e6431",
+ "bytes": 1535
},
{
"path": "src/thoth/application/reducers/outcome_reducer.py",
@@ -6168,8 +6178,8 @@
},
{
"path": "src/thoth/application/services/conversation_router.py",
- "sha256": "43d22e37e8a5eac8a0431a2cc08052f1ed984d693c8a91d244664d0e3691a8e4",
- "bytes": 14872
+ "sha256": "efe20525539fe4b6fa80ded7cf7f7dbf71a59e9b74e13aaebf61bafde0a4ed1a",
+ "bytes": 14902
},
{
"path": "src/thoth/application/services/counter_search_calculations.py",
@@ -6203,8 +6213,8 @@
},
{
"path": "src/thoth/application/services/criterion_compiler.py",
- "sha256": "7fd5eaac2e3fc9d091353e7378c4da81343ba833a05bea9d945bb73ecef1c4c6",
- "bytes": 4761
+ "sha256": "ed0c81833d2edb98ef0b0a776a8137722d66d6d4c9f75d98edfded072737f9fa",
+ "bytes": 4739
},
{
"path": "src/thoth/application/services/criterion_contract_service.py",
@@ -6298,8 +6308,8 @@
},
{
"path": "src/thoth/application/services/evidence_graph_service.py",
- "sha256": "111b1263696d4b4c8d64de74bb00ea0e175be9b0e3a10c767a33581b1bdb9b4b",
- "bytes": 21579
+ "sha256": "60ddb664541c3353167a8fe90d6ec636f05ca4d17c1f5edd8a21155992de0254",
+ "bytes": 21577
},
{
"path": "src/thoth/application/services/evidence_publication_basis.py",
@@ -6573,8 +6583,8 @@
},
{
"path": "src/thoth/application/services/investigation_service.py",
- "sha256": "8132fe0043b71bf7085b57d12fadb99db14ecfb71b1568bf398e825038938d51",
- "bytes": 12140
+ "sha256": "81a0bdd96c730e6d5babcc1bb112e33f94d1d857dc1493bc4f2aaafcfcdf9806",
+ "bytes": 12102
},
{
"path": "src/thoth/application/services/judgment_review_service.py",
@@ -6698,8 +6708,8 @@
},
{
"path": "src/thoth/application/services/model_settings.py",
- "sha256": "8bb40157f052e84c8efe46c15229ac691c32d99c58580af9722c9b9087c40c25",
- "bytes": 5664
+ "sha256": "08a202a206d052343d1af8689302f3da78467d0bb486386475960d216dce7d1d",
+ "bytes": 5616
},
{
"path": "src/thoth/application/services/model_settings_conversation.py",
@@ -6753,8 +6763,8 @@
},
{
"path": "src/thoth/application/services/policy_gate.py",
- "sha256": "9eec9b88553b40806aaf36b0e490efe0f193deac0d705f2769a025e3696bca51",
- "bytes": 4951
+ "sha256": "ff295d76a6e6744b892fa16273abc7d221bb282ea182f7ef3e1926337fed1661",
+ "bytes": 4921
},
{
"path": "src/thoth/application/services/post_execution_memory.py",
@@ -6783,8 +6793,8 @@
},
{
"path": "src/thoth/application/services/public_web_policy.py",
- "sha256": "ff4acfca3e6c5589cba1af4b8335e8f33738bf7f25e5441932605ea0444fd881",
- "bytes": 6477
+ "sha256": "b2097cd23c066642f8551166038a6490ab6e0a470f013abeac2daa617d1f6331",
+ "bytes": 6417
},
{
"path": "src/thoth/application/services/r2_closed_loop.py",
@@ -6928,8 +6938,8 @@
},
{
"path": "src/thoth/application/services/research_attempt_admission.py",
- "sha256": "28345215be8133302724e8814de7e4ad88cdc57982faa06f5162f4cebcc463b3",
- "bytes": 2649
+ "sha256": "f63b659c0d2287e85e10102005ffcfbdfd3c2d55f2146fb47ce5cd0070565b75",
+ "bytes": 2627
},
{
"path": "src/thoth/application/services/research_attempt_runner.py",
@@ -7268,8 +7278,8 @@
},
{
"path": "src/thoth/application/services/revision_diff.py",
- "sha256": "ab8b6890ab59a0de48d52ee364d7388c3397ae2421fae8e28e62fe2305d182fe",
- "bytes": 2017
+ "sha256": "1bb379b9d5558c2cf8c9fd88303352ce245972c3b9881fff07b8b200bc3abb93",
+ "bytes": 1973
},
{
"path": "src/thoth/application/services/revision_listing.py",
@@ -7288,13 +7298,13 @@
},
{
"path": "src/thoth/application/services/sandbox_router.py",
- "sha256": "4004329cb53fac337d3623623a755f5bfc8fab19ef6be4915e7b97d4171c83cf",
- "bytes": 1643
+ "sha256": "e905602f6b250b94a5cc2a3026a21d0c304bd7fa618fb2b911ded8d526e2e232",
+ "bytes": 1629
},
{
"path": "src/thoth/application/services/sandbox_service.py",
- "sha256": "0b053c9acbcdbdd2344f6c29035930ad63d0919f861008aad56b40eca95ce25c",
- "bytes": 26030
+ "sha256": "a3cc9b0157020c5fcc7268e2e74b61306bdd07271daa58e17eb9dd8fdf16d29f",
+ "bytes": 26031
},
{
"path": "src/thoth/application/services/scoped_artifacts.py",
@@ -7343,8 +7353,8 @@
},
{
"path": "src/thoth/application/services/source_time_service.py",
- "sha256": "548cb9d55e6c4455cb43b2f3d0272ddc9b20e05da3c331adb7bb7428f3762d32",
- "bytes": 8689
+ "sha256": "0d52299a059fcfd1ebdfbb49412a9dc933a62e65d939b0483f88647341697a96",
+ "bytes": 8768
},
{
"path": "src/thoth/application/services/test_validity_service.py",
@@ -7383,8 +7393,8 @@
},
{
"path": "src/thoth/application/services/tui_session_service.py",
- "sha256": "44d77a832ff1d5cf75dc8b6a4d50b1bcc3d9fd0deab92496ec1aa87bb1d5c709",
- "bytes": 11813
+ "sha256": "5062087520fd6e1f42428bd8ec14e6dfabe57b2aaa15491d9194ca16e23b95a8",
+ "bytes": 11798
},
{
"path": "src/thoth/application/services/user_activity_projection.py",
@@ -7503,8 +7513,8 @@
},
{
"path": "src/thoth/apps/decision_chain_routes.py",
- "sha256": "0650adc5ec34cc3d84312d23c643bf366caea18b39489b581508c2e64b090485",
- "bytes": 6644
+ "sha256": "f7148c888cd0406146f4eae3502acc03c0e7f97efb37266b8ea89b171da4b011",
+ "bytes": 6628
},
{
"path": "src/thoth/apps/decision_command_composition.py",
@@ -7548,8 +7558,8 @@
},
{
"path": "src/thoth/apps/hosted_review_snapshot.py",
- "sha256": "23fe495dd927dfb26177469ab0737d9aec0c9b96ad5515b8b5dc04214c5160d4",
- "bytes": 15701
+ "sha256": "8d3eafdbefa67b9da97ee6a7acdd9a86a6207bd99cc09e69ef1d31f569a62677",
+ "bytes": 15994
},
{
"path": "src/thoth/apps/hypothesis_link_composition.py",
@@ -7698,8 +7708,8 @@
},
{
"path": "src/thoth/apps/source_composition.py",
- "sha256": "95ea71cc07c438875705876fa6a05e0ccd9422b94bc9e872acd78f1f5032ed95",
- "bytes": 2630
+ "sha256": "039db6fb4d0f761fa6497d7aa68d7bbbdb401a61923cc7ace06c1f0d5085af1b",
+ "bytes": 2638
},
{
"path": "src/thoth/apps/source_evidence_routes.py",
@@ -7813,8 +7823,8 @@
},
{
"path": "src/thoth/domain/baseline.py",
- "sha256": "abd3f33c97d8200882eec84c2c5274d9e3972b7820409e7c2aa65e17aff94231",
- "bytes": 2341
+ "sha256": "164054cbb3b4a21db4eb861f56f63b9321c3bfe7fda985acdd7af08402e6ef2a",
+ "bytes": 2343
},
{
"path": "src/thoth/domain/behavior_artifact.py",
@@ -7953,8 +7963,8 @@
},
{
"path": "src/thoth/domain/evidence.py",
- "sha256": "2aa311ddf035d71ebf308726702391d6106f7fc19ee2a62efb22b37796165daf",
- "bytes": 2641
+ "sha256": "fe829e6ad59cdd789cd16691cbfff7204165dd4ec79a9fbb3028254a531b00f1",
+ "bytes": 2633
},
{
"path": "src/thoth/domain/evidence_basis.py",
@@ -7983,8 +7993,8 @@
},
{
"path": "src/thoth/domain/execution.py",
- "sha256": "e90dd4672c96074a5622719f48fc0592575d9998db95b005a914351bde8cd88f",
- "bytes": 1448
+ "sha256": "0b2dfc5b0a042d0b6f90147c736e764e7d378be5d7aa8e4c59e778c74a2429fb",
+ "bytes": 1418
},
{
"path": "src/thoth/domain/execution_full.py",
@@ -8573,8 +8583,8 @@
},
{
"path": "src/thoth/ports/artifact_ledger.py",
- "sha256": "036fad0e145aa10d5da9ecde26aa371d1683aa103ef2623df7e44538f1be51ae",
- "bytes": 2215
+ "sha256": "f992604741ed48a870c358e6284ffd8c456eb56a57d884a238ca375e82282b57",
+ "bytes": 2224
},
{
"path": "src/thoth/ports/auth.py",
@@ -8663,8 +8673,8 @@
},
{
"path": "src/thoth/ports/evaluator.py",
- "sha256": "e4bfac4e69a36bf9ccccee31bf9cc778a85237de44efbaf50d7c494b20c444e2",
- "bytes": 316
+ "sha256": "1d1d8cf9e21b9eb8effe31a5456c5baff443c3f36e32cb224a87c6b95736032a",
+ "bytes": 315
},
{
"path": "src/thoth/ports/event_store.py",
@@ -8853,8 +8863,8 @@
},
{
"path": "src/thoth/ports/sandbox_capability.py",
- "sha256": "03163a74def651a30bfabe95156123336b8c46658608711d396629d7975b84a7",
- "bytes": 387
+ "sha256": "04e9f1d3c75a9f48c6c7168ed43e91cf210938b8f4e64609840756a5a498b9c7",
+ "bytes": 373
},
{
"path": "src/thoth/ports/source_version_catalog.py",
@@ -8883,8 +8893,8 @@
},
{
"path": "src/thoth/ports/thread_runtime.py",
- "sha256": "286b0da0204b99bfc3694e1fc493db35ee68db3ce4510c26eb801226af47ff6e",
- "bytes": 1003
+ "sha256": "4c4a344b8387cf1bb43a45509d2288fd2f20d777f898d0e8f24e18a7a9d5f6d4",
+ "bytes": 989
},
{
"path": "src/thoth/ports/web_reader.py",
@@ -8998,7 +9008,7 @@
},
{
"path": "tests/adversarial/test_anti_hardcoding.py",
- "sha256": "ef9beea92a9e1dd96fc391d30a0756427661d8c196b1c09ed81e0538f8cf283d",
+ "sha256": "03d3f75f309c8863bec07a99ee931b35a291598a3b83f9df9f2e50b14a1505f9",
"bytes": 2900
},
{
@@ -9018,8 +9028,8 @@
},
{
"path": "tests/architecture/test_atomicity_debt_contract.py",
- "sha256": "c914c278bbbe6ad22b0c6fb26615face646dfebe99b2df63ab9cca11985af61b",
- "bytes": 1507
+ "sha256": "bbf6a81b83938e850b069f671fef975a9500386dc3e91138acff0b1a6850605e",
+ "bytes": 1493
},
{
"path": "tests/architecture/test_complexity_budget.py",
@@ -9043,8 +9053,8 @@
},
{
"path": "tests/architecture/test_ocp_extension_guards.py",
- "sha256": "38cbcae9549c5d89a0190836a847210f296d75729ae686bc5360268669893e75",
- "bytes": 3305
+ "sha256": "5bcd7a58532076f162dd17bcfb6c82e8cf3a26470819eb2bd2828f9fd5db8c7d",
+ "bytes": 3307
},
{
"path": "tests/architecture/test_portable_build_paths.py",
@@ -9053,8 +9063,8 @@
},
{
"path": "tests/architecture/test_post_d4_baseline.py",
- "sha256": "9aa2e2275f24a462e9af6ee077600da1c34d358cd3e5c7c3bcffec477da4ddda",
- "bytes": 5906
+ "sha256": "8d1c67244cc313342d248dc89f41980230e61132fde1ac0e1f95660839b1e5b0",
+ "bytes": 5938
},
{
"path": "tests/architecture/test_public_method_catalog_builder.py",
@@ -9233,8 +9243,8 @@
},
{
"path": "tests/atomicity/test_permission_predicates.py",
- "sha256": "588ef2a6eb75a458a2761add8bc1e9a14fff486cbb143fd8bc9d07f01faa9a36",
- "bytes": 6225
+ "sha256": "83bb956c4dc1c262fea258cf154df2377e582811f1c0f623514cbf14dfc67f59",
+ "bytes": 6078
},
{
"path": "tests/atomicity/test_plan_revision.py",
@@ -9243,8 +9253,8 @@
},
{
"path": "tests/atomicity/test_policy_digest_history_migration.py",
- "sha256": "73fbe3cb30a972064e5378e71acd78edbd4b51e0b0f6cb12e22005ea1900bf59",
- "bytes": 6275
+ "sha256": "9755a9d680212041d4cbaa4a7b378a73d3024fe288abde3d087a11447007ccf3",
+ "bytes": 6389
},
{
"path": "tests/atomicity/test_profile_bootstrap.py",
@@ -9253,8 +9263,8 @@
},
{
"path": "tests/atomicity/test_project_governance.py",
- "sha256": "d9d2e8e7aac2e9f90e8ed19d5cf0426360b5dc6f163a5966731862d66d449903",
- "bytes": 4903
+ "sha256": "78cbab28fed1a2e4f154e84fe30d2160d68eed492597501050461257c68b2588",
+ "bytes": 4964
},
{
"path": "tests/atomicity/test_project_lifecycle_faults.py",
@@ -9311,6 +9321,11 @@
"sha256": "4863d6c2f6572bd3aa70dfbc16d50c6a17cc1a223a1343b5af8f20ae7d956473",
"bytes": 23221
},
+ {
+ "path": "tests/ci/test_ci_python_format.py",
+ "sha256": "d1eca3ab99f7cf9fc98baa65cca5d9931b274841ee9673dc9efb6f0f01186470",
+ "bytes": 1184
+ },
{
"path": "tests/ci/test_ci_test_shards.py",
"sha256": "74b7ded8273b51ec89468ba0b74d3f6f52145f270299eae58a815543aed8430b",
@@ -9818,8 +9833,8 @@
},
{
"path": "tests/integration/public_web_test_support.py",
- "sha256": "b3bfad024134917b463d3cadf951b0965b3634eb348fc91d0b7ab19e54be6845",
- "bytes": 7878
+ "sha256": "f4ac2b6013360c1769c339cfbcd8230252518ba1b64c446ac0332bbf66e038be",
+ "bytes": 7879
},
{
"path": "tests/integration/readset_helpers.py",
@@ -9838,8 +9853,8 @@
},
{
"path": "tests/integration/research_measurement_helpers.py",
- "sha256": "bc1444f58ab13d5ebcf76c073386d3e28190b8d6a0c22612d3d30bad3ebd4b86",
- "bytes": 6280
+ "sha256": "9ca2f36f84fb7b0be07abb6b237fe854c64f0e9f615183efa61023ad68c587e0",
+ "bytes": 6238
},
{
"path": "tests/integration/resource_scope_helpers.py",
@@ -9943,8 +9958,8 @@
},
{
"path": "tests/integration/test_a04_stale_evidence.py",
- "sha256": "b697da570501f765d2c0c103e36573a57f5366fb1ae7e7bd6b4405ebe54ca579",
- "bytes": 23841
+ "sha256": "3755d71c0e1284944f8c450eb114a5f99a8cf22e44de5688a1d2dd05bf74795c",
+ "bytes": 23777
},
{
"path": "tests/integration/test_a04_storage_authority.py",
@@ -10003,8 +10018,8 @@
},
{
"path": "tests/integration/test_a08_recursive_improvement.py",
- "sha256": "af115cef235784174f562c4bba5d82d471b88be1630ea50463ffc5f9832e9666",
- "bytes": 9865
+ "sha256": "6abb8488e55cc52c2c43393fc02f6e614c30ece13a400983886799e51dbc6bec",
+ "bytes": 9870
},
{
"path": "tests/integration/test_a08_retrieval_policy_candidate.py",
@@ -10058,8 +10073,8 @@
},
{
"path": "tests/integration/test_a11_policy_integrity.py",
- "sha256": "3ad080c53fb1a1e1b2bb745010cded9d862076d53b1675451fbbaa03212602c6",
- "bytes": 27724
+ "sha256": "a6dd9428a22d7302c34da1169c9d204aee3b3dffd42d3c6ebd0e73ceae570798",
+ "bytes": 27807
},
{
"path": "tests/integration/test_a12_blind_score_and_adjudication.py",
@@ -10258,8 +10273,8 @@
},
{
"path": "tests/integration/test_conversation_ui_read.py",
- "sha256": "aff6db4ec5db35a83ec05a86bd9d64b7bbb9147f9325dc079ceafad096e88dce",
- "bytes": 9121
+ "sha256": "287aff098c7e01a60f285759fbc88ee05490a14a7c4d103ef1ce9b664eddd496",
+ "bytes": 8965
},
{
"path": "tests/integration/test_counter_cleanup_cancel_preserves_prior.py",
@@ -10468,8 +10483,8 @@
},
{
"path": "tests/integration/test_historical_access_verification.py",
- "sha256": "2094dc306db94bb4aac566a0fa54239ddd2297128c7171a78592768ee083d07e",
- "bytes": 12250
+ "sha256": "c27f9fada1195b537172e127d82b9641867e31f282fb2d509ea180303ed30b2c",
+ "bytes": 12288
},
{
"path": "tests/integration/test_history_change_summary_read.py",
@@ -10478,18 +10493,18 @@
},
{
"path": "tests/integration/test_hosted_review_dispatch_gate.py",
- "sha256": "0a61dd5d8069e9820ba1531b0e6061ee25563fa613f42c9527433cd9026f1510",
- "bytes": 10028
+ "sha256": "a0dae62db1ae5efc371caf5efb749ab57f64a7e11acfa1836f77618b69aa0b67",
+ "bytes": 10068
},
{
"path": "tests/integration/test_hosted_review_internet_vs_model.py",
- "sha256": "ff9b470b354c59fb1a760d55f814e484ed481417d1d1b0b8a474bd99bc747281",
- "bytes": 3471
+ "sha256": "a749a09078cde2834c53c3ed170252d9ab0619c94ec87399d2803f474949f0d3",
+ "bytes": 3425
},
{
"path": "tests/integration/test_hosted_review_session_isolation.py",
- "sha256": "af22ece41b0e66e98f20db9b3755bb3dcd5a41e53d0d8948917c8b5f6813f6fc",
- "bytes": 12066
+ "sha256": "64b6a3e7d7264f9c78f32fa2521c57d15ea6e36f976b540fbd3a6131158cc40b",
+ "bytes": 12064
},
{
"path": "tests/integration/test_hosted_review_stale_attempt.py",
@@ -10513,8 +10528,8 @@
},
{
"path": "tests/integration/test_hypothesis_full_rpc.py",
- "sha256": "6105c0bc0990ec392a6f3101759e9a2ed29d9705b284dd881925a3555812d833",
- "bytes": 24901
+ "sha256": "b1e784c8419e59b534c448ff1b2c577e4aeba3f6769b43b65215e9b27b79f81c",
+ "bytes": 24879
},
{
"path": "tests/integration/test_hypothesis_grounding_storage.py",
@@ -10528,8 +10543,8 @@
},
{
"path": "tests/integration/test_hypothesis_prediction_outcome_cycle.py",
- "sha256": "ea9bb533ca4e2db498dfbabee3c01d856f4a2ee4a259434e6e82b77a3d40595a",
- "bytes": 24480
+ "sha256": "02963d8b764b5294755723aee1caf4f1ccad3e3b86bfb03e5bfb601098ce7e59",
+ "bytes": 24450
},
{
"path": "tests/integration/test_hypothesis_same.py",
@@ -10868,8 +10883,8 @@
},
{
"path": "tests/integration/test_model_settings.py",
- "sha256": "6c507257d81c1e387eb63795d2fd00cfe722d221eae3e920eb6fd9473c9c20da",
- "bytes": 9089
+ "sha256": "e985eef5ecbfa35faf91d79c94877a110afc590e31808f028ee7da7149fdff11",
+ "bytes": 9090
},
{
"path": "tests/integration/test_model_settings_cache_only.py",
@@ -11093,23 +11108,23 @@
},
{
"path": "tests/integration/test_public_oauth_memory_excerpt_regression.py",
- "sha256": "00a9a7adcfeeb996a4ad4ac444e68a51ce488eb1b0b35482997b9691a9c4fc01",
- "bytes": 2222
+ "sha256": "2552003afc5bdb5cdc620b55da4c068c8894cb47dc8251b8e4b1e2dfb20df1d6",
+ "bytes": 2200
},
{
"path": "tests/integration/test_public_web_evidence_gates.py",
- "sha256": "d9fb95ec6c41bcc247ae2867317702a68b6f103e5e96d7a8bfa5530734cbebbd",
- "bytes": 6720
+ "sha256": "dd173d2bb4061362484ab2ab8ad99ac21ac007a006f3697ae79e8b084cf1b481",
+ "bytes": 6708
},
{
"path": "tests/integration/test_public_web_policy_update_rpc.py",
- "sha256": "dd7e2f312cc56c8f5f6505ee5cde635ba989c4e12d91094edc7dded79ab9890d",
- "bytes": 10808
+ "sha256": "33c055a4757a8f3820b2b1f36dc47b6637e41c2fde12cce1869dcebb34652a3e",
+ "bytes": 10762
},
{
"path": "tests/integration/test_public_web_runtime_catalog.py",
- "sha256": "034208201b1f602d1622d6f9cd72d8d9b43b8aac5eb30bd3fbb6786a9f9af3d9",
- "bytes": 3583
+ "sha256": "efe04826d1b7a305dc24b8ed2b14edefeae4779d8168dea9507c6532392c8a97",
+ "bytes": 3569
},
{
"path": "tests/integration/test_r2_closed_loop_golden.py",
@@ -11258,8 +11273,8 @@
},
{
"path": "tests/integration/test_research_history_read.py",
- "sha256": "d46c1d49a066fabe485eea4be59c161d409b7b72bbff19394c6b8ed0d6cdd8a5",
- "bytes": 9102
+ "sha256": "11ce10f0592ff48f8d1cb8b624c1cec78d030bda46bae6017ba068ca7e5c9d59",
+ "bytes": 9156
},
{
"path": "tests/integration/test_research_identity_migration.py",
@@ -11278,13 +11293,13 @@
},
{
"path": "tests/integration/test_research_lineage_publication.py",
- "sha256": "af4a88cdcc795c4c842a84af865eff5060726fbd8c1ea294132faa4127b41270",
- "bytes": 8830
+ "sha256": "6c8b0d3a8d7ffbae7ec208ee605a197498da0467d52d0ea676c5a6ad443073fc",
+ "bytes": 8694
},
{
"path": "tests/integration/test_research_memory_basis.py",
- "sha256": "ef4a7004d534105928059f587d261c707b06a69197a458bbacd9cd97c89dfe53",
- "bytes": 8903
+ "sha256": "cfd8b84f77d745c99cbd8b15d40a068ebb28be1c778b50820adffd3199cae3a4",
+ "bytes": 8951
},
{
"path": "tests/integration/test_research_memory_correction_currentness.py",
@@ -11433,8 +11448,8 @@
},
{
"path": "tests/integration/test_resource_scope_revocation.py",
- "sha256": "d498451afcf77c1953d7a7df805190c9e3a5ac27aa030dbfae3b137fdac31306",
- "bytes": 5217
+ "sha256": "89099fa5f222c356b661c5e89944934ffa0a2e58b351b91b9eef220a6fcce391",
+ "bytes": 5266
},
{
"path": "tests/integration/test_resource_scope_roundtrip.py",
@@ -11458,8 +11473,8 @@
},
{
"path": "tests/integration/test_restore_apply_atomicity.py",
- "sha256": "57f36979d3311451de83acca44920fa258752ee611b2c677657f05febcaf872b",
- "bytes": 11301
+ "sha256": "a10971b145ed58634ea9f36d19d1fcb22c89d9d32cc6a8a20ce6dac1f5d855eb",
+ "bytes": 11363
},
{
"path": "tests/integration/test_restore_concurrency.py",
@@ -11468,13 +11483,13 @@
},
{
"path": "tests/integration/test_restore_currentness_consumers.py",
- "sha256": "aa35e7393e4415cd666e2b9d52b3c92210864cfa9b7ca87a5eb0b1ad77d30d60",
- "bytes": 7290
+ "sha256": "537325ce34c331ed97b36605bef42e94b50d719482a7541037ab982500b6b9c6",
+ "bytes": 7358
},
{
"path": "tests/integration/test_restore_drift.py",
- "sha256": "4f08e84c0ce23f22453c461db4483a2973df7a3b4fb8e43aa3f1cefd4faa490d",
- "bytes": 3063
+ "sha256": "cd4f32bd704a095cb657c38a553e477b84a44e520db28d229fb9cf0f4a4523b4",
+ "bytes": 3033
},
{
"path": "tests/integration/test_restore_legacy_compatibility.py",
@@ -11653,8 +11668,8 @@
},
{
"path": "tests/integration/test_selected_evidence_query.py",
- "sha256": "0df7bbba34239747baefdfa6a72bdcbfc676b4960487869e228cd7170d07f2f2",
- "bytes": 6396
+ "sha256": "88c5df167b95283c8f64bdd5d78279e72e71a53506a20ed7c4c8a7ceb4e650c7",
+ "bytes": 6270
},
{
"path": "tests/integration/test_source_cleanup_cancel_boundary.py",
@@ -11673,8 +11688,8 @@
},
{
"path": "tests/integration/test_source_time_connection.py",
- "sha256": "6b892032e0c1a94bc4a54839eef93b424541e2b2cc8e37b1340fccab20546616",
- "bytes": 9930
+ "sha256": "f98261dfc86e048313fdaa8d6faeedfe22afa2a8aec304347e1edda39d55d4de",
+ "bytes": 9828
},
{
"path": "tests/integration/test_stage_checkpoint_contract.py",
@@ -11718,8 +11733,8 @@
},
{
"path": "tests/integration/test_structured_context_pipeline.py",
- "sha256": "1ce42ed2292a5ca5e2a141fd6d90487c0f6a5824b93a133d01f3141dbf96b907",
- "bytes": 10944
+ "sha256": "b70ef514ed147f37abdf750e12ed6f9833c9538b6e9689b11b615339aa11b9fb",
+ "bytes": 10932
},
{
"path": "tests/integration/test_text_context_pipeline.py",
@@ -11813,8 +11828,8 @@
},
{
"path": "tests/integration/test_user_activity_thread_read.py",
- "sha256": "fce16d64670557b795c4b6876487f9c996f8e89563e62c36fd520b9eb0645f6c",
- "bytes": 2376
+ "sha256": "b7e23db482eb9130f89a67c28ca0a24431b3ac668000d74addad239a198ac368",
+ "bytes": 2332
},
{
"path": "tests/integration/test_v2_cycle_lineage_values.py",
@@ -11828,8 +11843,8 @@
},
{
"path": "tests/integration/test_workspace_restart_storage.py",
- "sha256": "e6a5ef1002509cdaebae4236fa0df2a48470e6cc1996e0123c8331e583ca5c94",
- "bytes": 15163
+ "sha256": "84ea574ac7cba07b278aaca8b941684d6e7435c35b71e0e925f3a0ad03286b7b",
+ "bytes": 15239
},
{
"path": "tests/integration/test_workspace_runtime_exclusivity_baseline.py",
@@ -11858,8 +11873,8 @@
},
{
"path": "tests/parser/test_document_time.py",
- "sha256": "dd82efc69ef676a6eb45da0f6e28f38aca0111de3092422816f7a8d0fca2cd87",
- "bytes": 2199
+ "sha256": "6c1e01eda668e241c6dc4301f0e064f881efe4d2cfe0f90af86ba6ffd17faf3b",
+ "bytes": 2218
},
{
"path": "tests/parser/test_document_time_protocol_fallback.py",
@@ -11903,8 +11918,8 @@
},
{
"path": "tests/parser/test_test_log.py",
- "sha256": "f68454c540489ff2ed07cbbfc306cf3fe27721ccb7324aea5ef26bec289b3246",
- "bytes": 1085
+ "sha256": "2b3e5b08b72bc5b2c84852b2190d20b3fd262b190d8e49bb3f552ec885cffba7",
+ "bytes": 1071
},
{
"path": "tests/parser/test_text.py",
@@ -11933,7 +11948,7 @@
},
{
"path": "tests/test_foundation.py",
- "sha256": "868a8f79901f0aee47bedcfae8cf2ce234d33bc32811d577833567981628f5db",
+ "sha256": "1bcf79b8e422c199061c35628ac805e6e5b77dc488fb88e92dbd93a5986abf47",
"bytes": 1422
},
{
@@ -12113,8 +12128,8 @@
},
{
"path": "tests/unit/application/test_research_final_producer_selection.py",
- "sha256": "88801b7a5de2c9646fc2b4dcbd8c42d404461dced2ce96d3c6d973047dbf059e",
- "bytes": 6364
+ "sha256": "1a12a904a3c5f3d77e45997f70888334386bb8483d1af7e5c35309225d876cc8",
+ "bytes": 6350
},
{
"path": "tests/unit/application/test_research_freshness_memory.py",
@@ -12153,8 +12168,8 @@
},
{
"path": "tests/unit/application/test_user_activity_projection.py",
- "sha256": "474030af56553332919130452b9b30a6bea106edffeb40cde0f8f74c0e66441c",
- "bytes": 9427
+ "sha256": "fbebe14a3bda8eb76392357b4d5e4611b2751b54663ce67803c4d9883e983bcd",
+ "bytes": 9392
},
{
"path": "tests/unit/application/test_workspace_ready_offload.py",
@@ -12178,7 +12193,7 @@
},
{
"path": "tests/unit/apps/test_source_policy_defaults.py",
- "sha256": "07fd08b892f5df466311f297700102bcba79fac4cd0dcaf433342d13ac7fadef",
+ "sha256": "48cf7aca893225aa026dc1f28a507df91b4149a1364a03a7c9f24c8aaa9d2778",
"bytes": 3103
},
{
@@ -12188,8 +12203,8 @@
},
{
"path": "tests/unit/connectors/test_connector_capability_routing.py",
- "sha256": "f153ebc1f66463ab3f19e305ad90c6ad5a27a8b4179b486c7630ce2f5db9639e",
- "bytes": 3434
+ "sha256": "31d827236ea0843921eff55b756cabb5161911d5baed1873488471039384dfe1",
+ "bytes": 3420
},
{
"path": "tests/unit/connectors/test_connector_config.py",
@@ -12358,8 +12373,8 @@
},
{
"path": "tests/unit/domain/test_models.py",
- "sha256": "fc8f7cc8d4faedf4d0b8131973cace06ea0a649632e55f6f5a488e6b8eedc132",
- "bytes": 7314
+ "sha256": "db782a243f02d2dd9a483c8045ef433d2b2637fe8e15c3111dead9ffe635f5e9",
+ "bytes": 7403
},
{
"path": "tests/unit/domain/test_notification_order.py",
@@ -12568,8 +12583,8 @@
},
{
"path": "tests/unit/models/test_codex_app_server_auth_only.py",
- "sha256": "f56e316353f2f83d92029360486e9f1a5bf38629a052aa2b1582ed285cc6f3dd",
- "bytes": 7146
+ "sha256": "62be9bb4963a8cded58bd00500c8125200ca6a67628de53d279f1ecab87a3330",
+ "bytes": 7143
},
{
"path": "tests/unit/models/test_codex_catalog_snapshot.py",
@@ -12588,8 +12603,8 @@
},
{
"path": "tests/unit/models/test_codex_oauth_model.py",
- "sha256": "9fda00209ff517658974211f2fb229ca6396c655477ede823853f732215176f8",
- "bytes": 8018
+ "sha256": "a83ad0d66b19aa3419f42a8ba45d489c131b92f932e5a2ca69610777f85bfe10",
+ "bytes": 8006
},
{
"path": "tests/unit/models/test_credential_auth_projection_contract.py",
@@ -12603,13 +12618,13 @@
},
{
"path": "tests/unit/models/test_hosted_review_provider_budget.py",
- "sha256": "e958f041dc8bc893041722fbbf74e34f9bec13fabb7b981f230a6cebc84d052c",
- "bytes": 8720
+ "sha256": "fda6a4338ce23db0ffa9245030d8b200433e1ee49add2e6fcc59ec1cd7000f2e",
+ "bytes": 8698
},
{
"path": "tests/unit/models/test_http_rejection_metadata.py",
- "sha256": "78ed4bab51ec765276d773b403801a4df3898928c777a85d995df151aa17fb25",
- "bytes": 2236
+ "sha256": "8f6ef18fbef36bc7b2fd3460867612d092a2b84892cd2edc3758c167113dad80",
+ "bytes": 2208
},
{
"path": "tests/unit/models/test_hypothesis_contract_wire_text.py",
@@ -12978,13 +12993,13 @@
},
{
"path": "tests/unit/services/test_criterion_compiler.py",
- "sha256": "ab7bdbc68f3c75b6c9262657059539ba4ec8b1c820d90fb5ccb2575705c2dd4f",
- "bytes": 4178
+ "sha256": "fb989a8edf3057fc2dd25a556dc268136ea304c55dda9b1297add4c73eeba621",
+ "bytes": 4164
},
{
"path": "tests/unit/services/test_evidence_context.py",
- "sha256": "9daafa0190290e8c4017d4c092a05736a85062ab386c493e0b62382116ede6ee",
- "bytes": 6704
+ "sha256": "96f9214fced51eb640e519f55e12c7038f872f551b710071e6f44f47171541f9",
+ "bytes": 6674
},
{
"path": "tests/unit/services/test_execution_reconciler.py",
@@ -13048,8 +13063,8 @@
},
{
"path": "tests/unit/services/test_model_route.py",
- "sha256": "ee0f9a219d7daa46fd095b69b66a1bb48d3046be38130a238d1a30b0c1d9d743",
- "bytes": 2599
+ "sha256": "f1092fa8bf3e6d694ba4bf777719ec98af200a82107e6e87704a5d5d40b87a9a",
+ "bytes": 2555
},
{
"path": "tests/unit/services/test_object_record_drafts.py",
@@ -13088,8 +13103,8 @@
},
{
"path": "tests/unit/services/test_retry_model_mismatch.py",
- "sha256": "0fcdbba868e3884608f6c618a869241a2b988891b089fe1d90526a161f1417e7",
- "bytes": 6353
+ "sha256": "705d2461369a59a8b74a37123cb032bb3ab9255edbdc3a85647f166c674fa4b3",
+ "bytes": 6339
},
{
"path": "tests/unit/services/test_thread_cycle_lineage_contract.py",
@@ -13373,8 +13388,8 @@
},
{
"path": "tests/unit/test_hosted_review_snapshot.py",
- "sha256": "ebade1c6326c80e6621a5533c03d2f9cfcb997fbb1feb8bfcb36dc5d98fa936e",
- "bytes": 12975
+ "sha256": "ccce7df7330fb8ea767a0972bbadabb7462c46ece75ba9157a2019ec57b4a9f2",
+ "bytes": 12963
},
{
"path": "tests/unit/test_hypothesis_eval_cases.py",
@@ -13553,18 +13568,18 @@
},
{
"path": "tests/unit/test_public_web_policy_reconciliation.py",
- "sha256": "253c3edb373575a0df148fbea5442ce27f75111fb4f447a18a8e4964a53dcc25",
- "bytes": 6665
+ "sha256": "d939260f83348bd801c0bfd752be3a519bf1af4bf806c439365eec3474d819bc",
+ "bytes": 6643
},
{
"path": "tests/unit/test_public_web_query_disclosure.py",
- "sha256": "02bd5aea95d4fc723d456c1901a8751349932dc6b792b007d2a8275f08e346e1",
- "bytes": 5515
+ "sha256": "68c8ab433933810f5acfee44a59805e85ea70824509b3169db0318196b60f9c3",
+ "bytes": 5473
},
{
"path": "tests/unit/test_public_web_uri_boundary.py",
- "sha256": "7cea87d4c0dd802e46797355efd78a4a70c9955a8f1345d5f5038284cd8c3010",
- "bytes": 4742
+ "sha256": "a17f06e769be4f80499fc00a45f5bbadea21d7d1f7f7bf9236c927fecc4ec650",
+ "bytes": 4736
},
{
"path": "tests/unit/test_r2_context_preflight_contract.py",
@@ -13743,8 +13758,8 @@
},
{
"path": "tests/unit/test_source_time_resolution.py",
- "sha256": "8ad4c3a6c58fcdf68adc13e4f009f40b66dfddbd2a457e05b1c9a17107e47de2",
- "bytes": 3686
+ "sha256": "f5a1210d7c20de574b4baa8e8b6b6c4fd84e7d2a6da94369835b35d7798076f3",
+ "bytes": 3599
},
{
"path": "tests/unit/test_test_validity.py",
@@ -13808,7 +13823,7 @@
},
{
"path": "uv.lock",
- "sha256": "62ad4f0cb97c5af0566a2532bff9ec92ae218714205e1456d5b3d9bca801c021",
+ "sha256": "699adc13604c6242c2f585948fe1e0c0b8656efbed93027f38d2bf05b27c1d70",
"bytes": 429530
}
],
diff --git a/apps/web/.prettierignore b/apps/web/.prettierignore
new file mode 100644
index 00000000..c32a6cd8
--- /dev/null
+++ b/apps/web/.prettierignore
@@ -0,0 +1,10 @@
+# Build output and caches.
+dist
+*.tsbuildinfo
+
+# Recorded server responses and other fixtures keep the bytes they were recorded with.
+src/api/fixtures
+*.fixture.json
+
+# Static files that are served as they are.
+public
diff --git a/apps/web/.prettierrc.json b/apps/web/.prettierrc.json
new file mode 100644
index 00000000..de753c53
--- /dev/null
+++ b/apps/web/.prettierrc.json
@@ -0,0 +1,3 @@
+{
+ "printWidth": 100
+}
diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js
index 5dd1efc9..c12a257c 100644
--- a/apps/web/eslint.config.js
+++ b/apps/web/eslint.config.js
@@ -1,5 +1,6 @@
import js from "@eslint/js";
import globals from "globals";
+import jsxA11y from "eslint-plugin-jsx-a11y";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
@@ -7,7 +8,11 @@ import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ extends: [
+ js.configs.recommended,
+ ...tseslint.configs.recommended,
+ jsxA11y.flatConfigs.recommended,
+ ],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2022,
@@ -28,4 +33,3 @@ export default tseslint.config(
rules: { "no-console": "off" },
},
);
-
diff --git a/apps/web/package.json b/apps/web/package.json
index 3ecccda9..9c109047 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -1,39 +1,43 @@
-{
- "name": "@thoth/web",
- "version": "0.2.0-beta.3",
- "private": true,
- "type": "module",
- "scripts": {
- "dev": "vite --host 127.0.0.1",
- "build": "tsc -b && vite build",
- "lint": "eslint .",
- "typecheck": "tsc -b --pretty false",
- "test": "vitest",
- "preview": "vite preview --host 127.0.0.1"
- },
- "dependencies": {
- "@blueprintjs/core": "6.18.0",
- "@blueprintjs/icons": "6.13.0",
- "@tanstack/react-query": "^5.66.0",
- "react": "^19.0.0",
- "react-dom": "^19.0.0",
- "zod": "^3.24.0"
- },
- "devDependencies": {
- "@eslint/js": "^9.19.0",
- "@types/node": "22.20.4",
- "@types/react": "^19.0.8",
- "@types/react-dom": "^19.0.3",
- "@vitejs/plugin-react": "^4.3.4",
- "eslint": "^9.19.0",
- "eslint-plugin-react-hooks": "^5.0.0",
- "eslint-plugin-react-refresh": "^0.4.18",
- "globals": "^15.14.0",
- "jsdom": "26.1.0",
- "typescript": "~5.7.3",
- "typescript-eslint": "^8.22.0",
- "vite": "^6.1.0",
- "vitest": "^3.0.5"
- },
- "license": "MIT"
-}
+{
+ "name": "@thoth/web",
+ "version": "0.2.0-beta.4",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 127.0.0.1",
+ "build": "tsc -b && vite build",
+ "lint": "eslint . && prettier --check .",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "typecheck": "tsc -b --pretty false",
+ "test": "vitest",
+ "preview": "vite preview --host 127.0.0.1"
+ },
+ "dependencies": {
+ "@blueprintjs/core": "6.18.0",
+ "@blueprintjs/icons": "6.13.0",
+ "@tanstack/react-query": "^5.66.0",
+ "react": "^19.0.0",
+ "react-dom": "^19.0.0",
+ "zod": "^3.24.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.19.0",
+ "@types/node": "22.20.4",
+ "@types/react": "^19.0.8",
+ "@types/react-dom": "^19.0.3",
+ "@vitejs/plugin-react": "^4.3.4",
+ "eslint": "^9.19.0",
+ "eslint-plugin-jsx-a11y": "^6.10.2",
+ "eslint-plugin-react-hooks": "^5.0.0",
+ "eslint-plugin-react-refresh": "^0.4.18",
+ "globals": "^15.14.0",
+ "jsdom": "26.1.0",
+ "prettier": "3.9.10",
+ "typescript": "~5.7.3",
+ "typescript-eslint": "^8.22.0",
+ "vite": "^6.1.0",
+ "vitest": "^3.0.5"
+ },
+ "license": "MIT"
+}
diff --git a/apps/web/preview-timeline.html b/apps/web/preview-timeline.html
deleted file mode 100644
index 7d5908d1..00000000
--- a/apps/web/preview-timeline.html
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
- THOTH timeline preview
-
-
-
-
-
-
diff --git a/apps/web/src/App.test.tsx b/apps/web/src/App.test.tsx
index 900c9f95..58eec28e 100644
--- a/apps/web/src/App.test.tsx
+++ b/apps/web/src/App.test.tsx
@@ -19,8 +19,10 @@ describe("THOTH UI contracts", () => {
expect(html).toContain("전체 42개 중 선택");
});
- it("renders a null scientific classification as unrecorded, never a fabricated status",()=>{
- const html=renderToStaticMarkup();
- expect(html).toContain("미기록");expect(html).not.toContain("PASS");expect(html).not.toContain("UNKNOWN");
+ it("renders a null scientific classification as unrecorded, never a fabricated status", () => {
+ const html = renderToStaticMarkup();
+ expect(html).toContain("미기록");
+ expect(html).not.toContain("PASS");
+ expect(html).not.toContain("UNKNOWN");
});
});
diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 51a4efc8..ee0a426e 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -3,18 +3,34 @@ import { lazy, Suspense, useEffect } from "react";
import { LiveProjectWorkspace } from "./components/LiveProjectWorkspace";
-const HistoryExample = import.meta.env.DEV ? lazy(() => import("./components/history/HistoryExample")) : null;
+const HistoryExample = import.meta.env.DEV
+ ? lazy(() => import("./components/history/HistoryExample"))
+ : null;
const ResultExample = import.meta.env.DEV ? lazy(() => import("./components/ResultExample")) : null;
export function App() {
const locale = useLocale();
useEffect(() => applyDocumentLocale(locale), [locale]);
- const example = typeof window === "undefined" ? null : new URLSearchParams(window.location.search).get("example");
+ const example =
+ typeof window === "undefined"
+ ? null
+ : new URLSearchParams(window.location.search).get("example");
if (HistoryExample && example === "history") {
- return {t("examples.historyLoading")}
}>;
+ return (
+ {t("examples.historyLoading")}}>
+
+
+ );
}
- if (ResultExample && (example === "result-before" || example === "result-after" || example === "result-iris")) {
- return {t("examples.resultLoading")}}>;
+ if (
+ ResultExample &&
+ (example === "result-before" || example === "result-after" || example === "result-iris")
+ ) {
+ return (
+ {t("examples.resultLoading")}}>
+
+
+ );
}
return ;
}
diff --git a/apps/web/src/api/actionDraft.ts b/apps/web/src/api/actionDraft.ts
index 910b3e1c..3167daf7 100644
--- a/apps/web/src/api/actionDraft.ts
+++ b/apps/web/src/api/actionDraft.ts
@@ -1,6 +1,22 @@
import { rpc } from "./rpcClient";
/** A draft action request made from one discriminating test of a hypothesis. What the person declares is sent as written; the server decides the risk from it and nothing about risk is sent. */
-export async function draftActionFromTest(projectId: string, hypothesisId: string, testId: string, declaration: Record) {
- return (await rpc<{ action: Record }>("action/draft/fromTest", { project_id: projectId, hypothesis_id: hypothesisId, test_id: testId, effect_declaration: declaration }, crypto.randomUUID())).value;
+export async function draftActionFromTest(
+ projectId: string,
+ hypothesisId: string,
+ testId: string,
+ declaration: Record,
+) {
+ return (
+ await rpc<{ action: Record }>(
+ "action/draft/fromTest",
+ {
+ project_id: projectId,
+ hypothesis_id: hypothesisId,
+ test_id: testId,
+ effect_declaration: declaration,
+ },
+ crypto.randomUUID(),
+ )
+ ).value;
}
diff --git a/apps/web/src/api/capabilities.ts b/apps/web/src/api/capabilities.ts
index 218c8c0e..14b59c4f 100644
--- a/apps/web/src/api/capabilities.ts
+++ b/apps/web/src/api/capabilities.ts
@@ -6,22 +6,87 @@ import { readQueries } from "./rpcClient";
export type Capability = (typeof catalog.methods)[number];
export const capabilityCatalog = catalog;
export const capabilities = catalog.methods;
-export const namespaces = [...new Set(capabilities.map(method => method.namespace))].sort();
+export const namespaces = [...new Set(capabilities.map((method) => method.namespace))].sort();
export const namespaceLabels: Record = {
- get project() { return t("advanced.projectGroup"); }, get thread() { return t("advanced.threadGroup"); }, get investigation() { return t("advanced.discoveryGroup"); }, get evidence() { return t("advanced.evidenceGroup"); },
- get object() { return t("advanced.object"); }, get criteria() { return t("record.criterion"); }, get hypothesis() { return t("advanced.hypothesisGroup"); }, get action() { return t("advanced.actionGroup"); },
- get execution() { return t("advanced.execution"); }, get revision() { return t("advanced.historyGroup"); }, get outcome() { return t("advanced.outcomes"); }, get memory() { return t("result.memory"); },
- get improvement() { return t("advanced.improvementGroup"); }, get receipt() { return t("advanced.receiptGroup"); }, get closure() { return t("advanced.closureGroup"); }, get export() { return t("advanced.export"); },
- get operation() { return t("advanced.async"); }, get model() { return t("model.settings"); }, get workspace() { return t("advanced.workspace"); },
- get projectpack() { return t("advanced.examples"); }, get field() { return t("advanced.field"); }, get trace() { return t("workspace.trace"); },
+ get project() {
+ return t("advanced.projectGroup");
+ },
+ get thread() {
+ return t("advanced.threadGroup");
+ },
+ get investigation() {
+ return t("advanced.discoveryGroup");
+ },
+ get evidence() {
+ return t("advanced.evidenceGroup");
+ },
+ get object() {
+ return t("advanced.object");
+ },
+ get criteria() {
+ return t("record.criterion");
+ },
+ get hypothesis() {
+ return t("advanced.hypothesisGroup");
+ },
+ get action() {
+ return t("advanced.actionGroup");
+ },
+ get execution() {
+ return t("advanced.execution");
+ },
+ get revision() {
+ return t("advanced.historyGroup");
+ },
+ get outcome() {
+ return t("advanced.outcomes");
+ },
+ get memory() {
+ return t("result.memory");
+ },
+ get improvement() {
+ return t("advanced.improvementGroup");
+ },
+ get receipt() {
+ return t("advanced.receiptGroup");
+ },
+ get closure() {
+ return t("advanced.closureGroup");
+ },
+ get export() {
+ return t("advanced.export");
+ },
+ get operation() {
+ return t("advanced.async");
+ },
+ get model() {
+ return t("model.settings");
+ },
+ get workspace() {
+ return t("advanced.workspace");
+ },
+ get projectpack() {
+ return t("advanced.examples");
+ },
+ get field() {
+ return t("advanced.field");
+ },
+ get trace() {
+ return t("workspace.trace");
+ },
};
export function filterCapabilities(search: string, namespace = "all", surface = "all") {
const tokens = search.trim().toLowerCase().split(/\s+/).filter(Boolean);
- return capabilities.filter(method =>
- (namespace === "all" || method.namespace === namespace) &&
- (surface === "all" || method.surface === surface) &&
- tokens.every(token => `${method.name} ${method.canonical_owner} ${method.policy} ${namespaceLabels[method.namespace] ?? ""}`.toLowerCase().includes(token)),
+ return capabilities.filter(
+ (method) =>
+ (namespace === "all" || method.namespace === namespace) &&
+ (surface === "all" || method.surface === surface) &&
+ tokens.every((token) =>
+ `${method.name} ${method.canonical_owner} ${method.policy} ${namespaceLabels[method.namespace] ?? ""}`
+ .toLowerCase()
+ .includes(token),
+ ),
);
}
@@ -31,7 +96,8 @@ export function transportFor(method: string) {
export function parseInvocation(text: string, projectId: string): Record {
const value: unknown = JSON.parse(text);
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new UiError("api.jsonObject");
+ if (!value || typeof value !== "object" || Array.isArray(value))
+ throw new UiError("api.jsonObject");
const input = value as Record;
if (!projectId || input.project_id !== projectId) throw new UiError("api.projectMismatch");
return input;
diff --git a/apps/web/src/api/contracts.test.ts b/apps/web/src/api/contracts.test.ts
index 84d99689..6da851e3 100644
--- a/apps/web/src/api/contracts.test.ts
+++ b/apps/web/src/api/contracts.test.ts
@@ -3,10 +3,23 @@ import { execFileSync } from "node:child_process";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it, vi, afterEach } from "vitest";
-import { capabilities, capabilityCatalog, filterCapabilities, namespaces, parseInvocation, transportFor } from "./capabilities";
+import {
+ capabilities,
+ capabilityCatalog,
+ filterCapabilities,
+ namespaces,
+ parseInvocation,
+ transportFor,
+} from "./capabilities";
import { readQueries, rpc, RpcError } from "./rpcClient";
import { recordViews } from "./recordViews";
-import { authorityOptions, authorityStates, cutoffOptions, cutoffStates, sourceConnectSchema } from "./sourcePolicy";
+import {
+ authorityOptions,
+ authorityStates,
+ cutoffOptions,
+ cutoffStates,
+ sourceConnectSchema,
+} from "./sourcePolicy";
import { isThreadAnalysis } from "./research";
import { captureProjectCreation } from "./projectCreation";
@@ -15,85 +28,162 @@ import { captureProjectCreation } from "./projectCreation";
// The time limit only ends a hang. The child does nothing but import the server's command and bus
// modules and print their schemas; on a busy GitHub runner that import outlasted the earlier 20 s
// (spawnSync ETIMEDOUT) although nothing was wrong.
-const pythonHangLimitMs=120_000;
-const repository=fileURLToPath(new URL("../../../../",import.meta.url));
-const python=[join(repository,".venv","Scripts","python.exe"),join(repository,".venv","bin","python")].find(existsSync);
-if(!python)throw new Error("Web/backend contract integration requires the repository .venv Python environment.");
-const backendContract=JSON.parse(execFileSync(python,["-c",[
- "import json",
- "from thoth.application.commands.sources import SourceConnectInput",
- "from thoth.application.commands.projects import ProjectCreateInput",
- "from thoth.protocol.bus import READ_QUERY_METHODS",
- "print(json.dumps({'source':SourceConnectInput.model_json_schema(),'project':ProjectCreateInput.model_json_schema(),'queries':sorted(READ_QUERY_METHODS)}))",
-].join("\n")],{shell:false,encoding:"utf8",timeout:pythonHangLimitMs,cwd:repository,
- env:{...process.env,PYTHONPATH:join(repository,"src"),PYTHONDONTWRITEBYTECODE:"1"},
-})) as {source:{$defs:Record};project:{properties:{policy_binding_ref:{default:string}}};queries:string[]};
+const pythonHangLimitMs = 120_000;
+const repository = fileURLToPath(new URL("../../../../", import.meta.url));
+const python = [
+ join(repository, ".venv", "Scripts", "python.exe"),
+ join(repository, ".venv", "bin", "python"),
+].find(existsSync);
+if (!python)
+ throw new Error(
+ "Web/backend contract integration requires the repository .venv Python environment.",
+ );
+const backendContract = JSON.parse(
+ execFileSync(
+ python,
+ [
+ "-c",
+ [
+ "import json",
+ "from thoth.application.commands.sources import SourceConnectInput",
+ "from thoth.application.commands.projects import ProjectCreateInput",
+ "from thoth.protocol.bus import READ_QUERY_METHODS",
+ "print(json.dumps({'source':SourceConnectInput.model_json_schema(),'project':ProjectCreateInput.model_json_schema(),'queries':sorted(READ_QUERY_METHODS)}))",
+ ].join("\n"),
+ ],
+ {
+ shell: false,
+ encoding: "utf8",
+ timeout: pythonHangLimitMs,
+ cwd: repository,
+ env: { ...process.env, PYTHONPATH: join(repository, "src"), PYTHONDONTWRITEBYTECODE: "1" },
+ },
+ ),
+) as {
+ source: { $defs: Record };
+ project: { properties: { policy_binding_ref: { default: string } } };
+ queries: string[];
+};
-describe("canonical surface parity",()=>{
- it("covers every catalog method and namespace without inventing a completion metric",()=>{
- expect(new Set(capabilities.map(item=>item.name)).size).toBe(capabilityCatalog.runtime_method_count);
- expect(capabilities.filter(item=>item.canonical)).toHaveLength(capabilityCatalog.canonical_method_count);
- expect(capabilities.filter(item=>!item.canonical)).toHaveLength(capabilityCatalog.compatibility_alias_count);
+describe("canonical surface parity", () => {
+ it("covers every catalog method and namespace without inventing a completion metric", () => {
+ expect(new Set(capabilities.map((item) => item.name)).size).toBe(
+ capabilityCatalog.runtime_method_count,
+ );
+ expect(capabilities.filter((item) => item.canonical)).toHaveLength(
+ capabilityCatalog.canonical_method_count,
+ );
+ expect(capabilities.filter((item) => !item.canonical)).toHaveLength(
+ capabilityCatalog.compatibility_alias_count,
+ );
expect(filterCapabilities("")).toHaveLength(capabilities.length);
- expect(recordViews.map(item=>item.namespace).sort()).toEqual(namespaces);
- for(const view of recordViews){
- if(view.method) expect(capabilities.some(item=>item.name===view.method)).toBe(true);
- else expect(capabilities.filter(item=>item.namespace===view.namespace).every(item=>item.surface==="COMMAND")).toBe(true);
+ expect(recordViews.map((item) => item.namespace).sort()).toEqual(namespaces);
+ for (const view of recordViews) {
+ if (view.method) expect(capabilities.some((item) => item.name === view.method)).toBe(true);
+ else
+ expect(
+ capabilities
+ .filter((item) => item.namespace === view.namespace)
+ .every((item) => item.surface === "COMMAND"),
+ ).toBe(true);
}
});
- it("uses the exact server query whitelist, not a method-name heuristic",()=>{
+ it("uses the exact server query whitelist, not a method-name heuristic", () => {
expect([...readQueries].sort()).toEqual(backendContract.queries);
expect(transportFor("thread/read")).toBe("/rpc/query");
expect(transportFor("receipt/read")).toBe("/rpc");
expect(transportFor("receipt/verify")).toBe("/rpc");
});
- it("searches namespace labels, exact IDs and owners",()=>{
- expect(filterCapabilities("model/settings").map(item=>item.name)).toEqual(["model/settings/read","model/settings/update"]);
- expect(filterCapabilities("","field","QUERY")).toHaveLength(0);
- expect(filterCapabilities("가설").every(item=>item.namespace==="hypothesis")).toBe(true);
+ it("searches namespace labels, exact IDs and owners", () => {
+ expect(filterCapabilities("model/settings").map((item) => item.name)).toEqual([
+ "model/settings/read",
+ "model/settings/update",
+ ]);
+ expect(filterCapabilities("", "field", "QUERY")).toHaveLength(0);
+ expect(filterCapabilities("가설").every((item) => item.namespace === "hypothesis")).toBe(true);
});
- it("does not send arbitrary arrays or a different project scope",()=>{
- expect(()=>parseInvocation("[]","p")).toThrow("JSON 객체");
- expect(()=>parseInvocation('{"project_id":"q"}',"p")).toThrow("일치");
- expect(parseInvocation('{"project_id":"p","thread_id":"t"}',"p")).toEqual({project_id:"p",thread_id:"t"});
+ it("does not send arbitrary arrays or a different project scope", () => {
+ expect(() => parseInvocation("[]", "p")).toThrow("JSON 객체");
+ expect(() => parseInvocation('{"project_id":"q"}', "p")).toThrow("일치");
+ expect(parseInvocation('{"project_id":"p","thread_id":"t"}', "p")).toEqual({
+ project_id: "p",
+ thread_id: "t",
+ });
});
});
-describe("source intake payload contract",()=>{
- it("two projects use distinct IDs and inherit the server's scoped default policy",()=>{
- const first=captureProjectCreation("first","2026-09-14T10:00","general-rnd");
- const second=captureProjectCreation("second","2026-09-14T10:00","general-rnd");
+describe("source intake payload contract", () => {
+ it("two projects use distinct IDs and inherit the server's scoped default policy", () => {
+ const first = captureProjectCreation("first", "2026-09-14T10:00", "general-rnd");
+ const second = captureProjectCreation("second", "2026-09-14T10:00", "general-rnd");
expect(first.input.project_id).not.toBe(second.input.project_id);
expect(first.key).not.toBe(second.key);
expect(first.input).not.toHaveProperty("policy_binding_ref");
expect(second.input).not.toHaveProperty("policy_binding_ref");
expect(backendContract.project.properties.policy_binding_ref.default).toBe("policy:default");
});
- it("matches Python authority/cutoff enums and emits no UI-only enum aliases",()=>{
+ it("matches Python authority/cutoff enums and emits no UI-only enum aliases", () => {
expect([...authorityStates]).toEqual(backendContract.source.$defs.AuthorityState.enum);
expect([...cutoffStates]).toEqual(backendContract.source.$defs.CutoffState.enum);
- expect(authorityOptions.map(item=>item.value).sort()).toEqual([...authorityStates].sort());
- expect(cutoffOptions.map(item=>item.value).sort()).toEqual([...cutoffStates].sort());
- const base={project_id:"p",relative_path:"source.md",media_type:"text/markdown",authority:"INFORMAL",cutoff_state:"AFTER_CUTOFF",security_class:"INTERNAL",resource_scope:{owner_kind:"PROJECT",visibility:"PROJECT_SHARED"},version_label:"web-sha"};
+ expect(authorityOptions.map((item) => item.value).sort()).toEqual([...authorityStates].sort());
+ expect(cutoffOptions.map((item) => item.value).sort()).toEqual([...cutoffStates].sort());
+ const base = {
+ project_id: "p",
+ relative_path: "source.md",
+ media_type: "text/markdown",
+ authority: "INFORMAL",
+ cutoff_state: "AFTER_CUTOFF",
+ security_class: "INTERNAL",
+ resource_scope: { owner_kind: "PROJECT", visibility: "PROJECT_SHARED" },
+ version_label: "web-sha",
+ };
expect(sourceConnectSchema.parse(base)).toEqual(base);
- expect(()=>sourceConnectSchema.parse({...base,authority:"REFERENCE"})).toThrow();
- expect(()=>sourceConnectSchema.parse({...base,cutoff_state:"FUTURE_EXCLUDED"})).toThrow();
- expect(()=>sourceConnectSchema.parse({...base,resource_scope:{owner_kind:"PROJECT",visibility:"WORKSTREAM"}})).toThrow();
+ expect(() => sourceConnectSchema.parse({ ...base, authority: "REFERENCE" })).toThrow();
+ expect(() => sourceConnectSchema.parse({ ...base, cutoff_state: "FUTURE_EXCLUDED" })).toThrow();
+ expect(() =>
+ sourceConnectSchema.parse({
+ ...base,
+ resource_scope: { owner_kind: "PROJECT", visibility: "WORKSTREAM" },
+ }),
+ ).toThrow();
});
- it("keeps incomplete result contracts out of full-analysis consumers",()=>{
- expect(isThreadAnalysis({portfolio:{hypotheses:[]},action_plan:{alternatives:[]},selected_evidence_refs:[]})).toBe(false);
- expect(isThreadAnalysis({answer:"partial answer",gaps:["not yet checked"]})).toBe(false);
+ it("keeps incomplete result contracts out of full-analysis consumers", () => {
+ expect(
+ isThreadAnalysis({
+ portfolio: { hypotheses: [] },
+ action_plan: { alternatives: [] },
+ selected_evidence_refs: [],
+ }),
+ ).toBe(false);
+ expect(isThreadAnalysis({ answer: "partial answer", gaps: ["not yet checked"] })).toBe(false);
});
});
-afterEach(()=>vi.unstubAllGlobals());
-it("routes authorized reads separately and retains typed error details",async()=>{
- const fetch=vi.fn().mockResolvedValue({ok:true,json:async()=>({jsonrpc:"2.0",id:"x",result:{operation_id:"",state:"SUCCEEDED",value:{}}})});
- vi.stubGlobal("fetch",fetch);
- await rpc("thread/list",{project_id:"p"},"same-key");
+afterEach(() => vi.unstubAllGlobals());
+it("routes authorized reads separately and retains typed error details", async () => {
+ const fetch = vi.fn().mockResolvedValue({
+ ok: true,
+ json: async () => ({
+ jsonrpc: "2.0",
+ id: "x",
+ result: { operation_id: "", state: "SUCCEEDED", value: {} },
+ }),
+ });
+ vi.stubGlobal("fetch", fetch);
+ await rpc("thread/list", { project_id: "p" }, "same-key");
expect(fetch.mock.calls[0][0]).toBe("/rpc/query");
- await rpc("receipt/read",{project_id:"p",receipt_id:"r"},"read-key");
+ await rpc("receipt/read", { project_id: "p", receipt_id: "r" }, "read-key");
expect(fetch.mock.calls[1][0]).toBe("/rpc");
- fetch.mockResolvedValue({ok:true,json:async()=>({jsonrpc:"2.0",id:"x",error:{code:-32003,message:"denied",data:{reason_code:"POLICY_BLOCKED"}}})});
- await expect(rpc("action/create",{project_id:"p"},"command-key")).rejects.toMatchObject({name:"RpcError",details:{reason_code:"POLICY_BLOCKED"}} satisfies Partial);
+ fetch.mockResolvedValue({
+ ok: true,
+ json: async () => ({
+ jsonrpc: "2.0",
+ id: "x",
+ error: { code: -32003, message: "denied", data: { reason_code: "POLICY_BLOCKED" } },
+ }),
+ });
+ await expect(rpc("action/create", { project_id: "p" }, "command-key")).rejects.toMatchObject({
+ name: "RpcError",
+ details: { reason_code: "POLICY_BLOCKED" },
+ } satisfies Partial);
});
diff --git a/apps/web/src/api/conversation.test.ts b/apps/web/src/api/conversation.test.ts
index 3298d196..9957ff42 100644
--- a/apps/web/src/api/conversation.test.ts
+++ b/apps/web/src/api/conversation.test.ts
@@ -1,54 +1,152 @@
-import { afterEach, expect, it, vi } from "vitest";
-import { readConversation, withCurrentCheckpoint, type ConversationTurn } from "./conversation";
-import { rpc } from "./rpcClient";
-
-vi.mock("./rpcClient",()=>({rpc:vi.fn()}));
-afterEach(()=>vi.resetAllMocks());
-const input=(epoch:number)=>({request_epoch:epoch,request_revision_digest:`request-${epoch}`,operation_id:`op-${epoch}`,text:`question ${epoch}`,edit_kind:"APPEND",created_at:"2026-09-14T00:00:00Z",authored_text_ref:{revision_digest:`authored-${epoch}`}});
-
-it("restores two distinct saved answers and never assigns a different request's result",async()=>{
- vi.mocked(rpc).mockImplementation(async(method,scope)=>({operation_id:"query",state:"SUCCEEDED",value:method==="thread/activity/list"?{conversation:{turns:[input(1),input(2)],next_before_epoch:null,history_limited:false}}:{operation_id:scope.operation_id,state:"SUCCEEDED",result:{thread_id:"t",request_epoch:scope.operation_id==="op-1"?1:2,answer:`answer ${scope.operation_id}`}}}));
- const page=await readConversation("p","t",null);
- expect(page.turns.map(t=>t.result?.answer)).toEqual(["answer op-1","answer op-2"]);
- const updated=withCurrentCheckpoint(page.turns,{operation_state:"RUNNING",current_result:{request_ref:{revision_digest:"request-2"},operation_id:"op-2",result:{answer:"checkpoint"}}});
- expect(updated.map(t=>t.result?.answer)).toEqual(["answer op-1","checkpoint"]);
-});
-
-it("does not use current text for an inaccessible or mismatched historical answer",async()=>{
- vi.mocked(rpc).mockImplementation(async(method,scope)=>({operation_id:"query",state:"SUCCEEDED",value:method==="thread/activity/list"?{conversation:{turns:[input(1)],next_before_epoch:null,history_limited:false}}:{operation_id:scope.operation_id,state:"SUCCEEDED",result:{thread_id:"other",request_epoch:1,answer:"foreign"}}}));
- const page=await readConversation("p","t",null);
- expect(page.turns[0].result).toBeNull();expect(page.turns[0].unavailable).toBeTruthy();
-});
-
-it("uses digest and operation together for checkpoint binding",()=>{
- const turns:ConversationTurn[]=[{input:input(1),state:"SUCCEEDED",result:{answer:"old"}}];
- expect(withCurrentCheckpoint(turns,{current_result:{request_ref:{revision_digest:"request-1"},operation_id:"other",result:{answer:"wrong"}}})[0].result?.answer).toBe("old");
-});
-
-it('retains failure and does not turn an initial checkpoint into an answer',()=>{
- const error={message:'research execution failed',data:{reason_code:'INTERNAL'}};
- const turns:ConversationTurn[]=[{input:input(1),state:'FAILED',result:null,error}];
- const rendered=withCurrentCheckpoint(turns,{operation_state:'FAILED',current_result:{request_ref:{revision_digest:'request-1'},operation_id:'op-1',result:{message:'Examining sources'},completion:'CHECKPOINT'}})[0];
- expect(rendered.result).toBeNull();expect(rendered.error).toBe(error);expect(rendered.state).toBe('FAILED');
-});
-
-it('never fills a denied historical result from a current checkpoint',()=>{
- const turns:ConversationTurn[]=[{input:input(1),state:'UNAVAILABLE',result:null,unavailable:'denied'}];
- expect(withCurrentCheckpoint(turns,{current_result:{request_ref:{revision_digest:'request-1'},operation_id:'op-1',result:{answer:'sensitive'}}})).toEqual(turns);
-});
-
-it('retains actual partial content with the matching failed operation',()=>{
- const turns:ConversationTurn[]=[{input:input(1),state:'FAILED',result:null,error:{message:'failed'}}];
- const rendered=withCurrentCheckpoint(turns,{operation_state:'FAILED',current_result:{request_ref:{revision_digest:'request-1'},operation_id:'op-1',result:{answer:'partial observation'}}})[0];
- expect(rendered.result?.answer).toBe('partial observation');expect(rendered.state).toBe('FAILED');
-});
-
-it('does not attribute a later failed request to a readable previous answer',()=>{
- const turns:ConversationTurn[]=[{input:input(1),state:'SUCCEEDED',result:{answer:'previous answer'}}];
- const rendered=withCurrentCheckpoint(turns,{request:{operation_id:'op-2'},operation_state:'FAILED',operation_error:{message:'new request failed'},
- previous_result:{request_ref:{revision_digest:'request-1'},operation_id:'op-1',result:{answer:'previous answer'}},
- basis_currentness:{state:'REVIEW_REQUIRED',reasons:['REQUEST_CHANGED']}})[0];
- expect(rendered.result?.answer).toBe('previous answer');
- expect(rendered.state).toBe('STALE');
+import { afterEach, expect, it, vi } from "vitest";
+import { readConversation, withCurrentCheckpoint, type ConversationTurn } from "./conversation";
+import { rpc } from "./rpcClient";
+
+vi.mock("./rpcClient", () => ({ rpc: vi.fn() }));
+afterEach(() => vi.resetAllMocks());
+const input = (epoch: number) => ({
+ request_epoch: epoch,
+ request_revision_digest: `request-${epoch}`,
+ operation_id: `op-${epoch}`,
+ text: `question ${epoch}`,
+ edit_kind: "APPEND",
+ created_at: "2026-09-14T00:00:00Z",
+ authored_text_ref: { revision_digest: `authored-${epoch}` },
+});
+
+it("restores two distinct saved answers and never assigns a different request's result", async () => {
+ vi.mocked(rpc).mockImplementation(async (method, scope) => ({
+ operation_id: "query",
+ state: "SUCCEEDED",
+ value:
+ method === "thread/activity/list"
+ ? {
+ conversation: {
+ turns: [input(1), input(2)],
+ next_before_epoch: null,
+ history_limited: false,
+ },
+ }
+ : {
+ operation_id: scope.operation_id,
+ state: "SUCCEEDED",
+ result: {
+ thread_id: "t",
+ request_epoch: scope.operation_id === "op-1" ? 1 : 2,
+ answer: `answer ${scope.operation_id}`,
+ },
+ },
+ }));
+ const page = await readConversation("p", "t", null);
+ expect(page.turns.map((t) => t.result?.answer)).toEqual(["answer op-1", "answer op-2"]);
+ const updated = withCurrentCheckpoint(page.turns, {
+ operation_state: "RUNNING",
+ current_result: {
+ request_ref: { revision_digest: "request-2" },
+ operation_id: "op-2",
+ result: { answer: "checkpoint" },
+ },
+ });
+ expect(updated.map((t) => t.result?.answer)).toEqual(["answer op-1", "checkpoint"]);
+});
+
+it("does not use current text for an inaccessible or mismatched historical answer", async () => {
+ vi.mocked(rpc).mockImplementation(async (method, scope) => ({
+ operation_id: "query",
+ state: "SUCCEEDED",
+ value:
+ method === "thread/activity/list"
+ ? { conversation: { turns: [input(1)], next_before_epoch: null, history_limited: false } }
+ : {
+ operation_id: scope.operation_id,
+ state: "SUCCEEDED",
+ result: { thread_id: "other", request_epoch: 1, answer: "foreign" },
+ },
+ }));
+ const page = await readConversation("p", "t", null);
+ expect(page.turns[0].result).toBeNull();
+ expect(page.turns[0].unavailable).toBeTruthy();
+});
+
+it("uses digest and operation together for checkpoint binding", () => {
+ const turns: ConversationTurn[] = [
+ { input: input(1), state: "SUCCEEDED", result: { answer: "old" } },
+ ];
+ expect(
+ withCurrentCheckpoint(turns, {
+ current_result: {
+ request_ref: { revision_digest: "request-1" },
+ operation_id: "other",
+ result: { answer: "wrong" },
+ },
+ })[0].result?.answer,
+ ).toBe("old");
+});
+
+it("retains failure and does not turn an initial checkpoint into an answer", () => {
+ const error = { message: "research execution failed", data: { reason_code: "INTERNAL" } };
+ const turns: ConversationTurn[] = [{ input: input(1), state: "FAILED", result: null, error }];
+ const rendered = withCurrentCheckpoint(turns, {
+ operation_state: "FAILED",
+ current_result: {
+ request_ref: { revision_digest: "request-1" },
+ operation_id: "op-1",
+ result: { message: "Examining sources" },
+ completion: "CHECKPOINT",
+ },
+ })[0];
+ expect(rendered.result).toBeNull();
+ expect(rendered.error).toBe(error);
+ expect(rendered.state).toBe("FAILED");
+});
+
+it("never fills a denied historical result from a current checkpoint", () => {
+ const turns: ConversationTurn[] = [
+ { input: input(1), state: "UNAVAILABLE", result: null, unavailable: "denied" },
+ ];
+ expect(
+ withCurrentCheckpoint(turns, {
+ current_result: {
+ request_ref: { revision_digest: "request-1" },
+ operation_id: "op-1",
+ result: { answer: "sensitive" },
+ },
+ }),
+ ).toEqual(turns);
+});
+
+it("retains actual partial content with the matching failed operation", () => {
+ const turns: ConversationTurn[] = [
+ { input: input(1), state: "FAILED", result: null, error: { message: "failed" } },
+ ];
+ const rendered = withCurrentCheckpoint(turns, {
+ operation_state: "FAILED",
+ current_result: {
+ request_ref: { revision_digest: "request-1" },
+ operation_id: "op-1",
+ result: { answer: "partial observation" },
+ },
+ })[0];
+ expect(rendered.result?.answer).toBe("partial observation");
+ expect(rendered.state).toBe("FAILED");
+});
+
+it("does not attribute a later failed request to a readable previous answer", () => {
+ const turns: ConversationTurn[] = [
+ { input: input(1), state: "SUCCEEDED", result: { answer: "previous answer" } },
+ ];
+ const rendered = withCurrentCheckpoint(turns, {
+ request: { operation_id: "op-2" },
+ operation_state: "FAILED",
+ operation_error: { message: "new request failed" },
+ previous_result: {
+ request_ref: { revision_digest: "request-1" },
+ operation_id: "op-1",
+ result: { answer: "previous answer" },
+ },
+ basis_currentness: { state: "REVIEW_REQUIRED", reasons: ["REQUEST_CHANGED"] },
+ })[0];
+ expect(rendered.result?.answer).toBe("previous answer");
+ expect(rendered.state).toBe("STALE");
expect(rendered.error).toBeUndefined();
});
diff --git a/apps/web/src/api/conversation.ts b/apps/web/src/api/conversation.ts
index 6753bee1..f8219fce 100644
--- a/apps/web/src/api/conversation.ts
+++ b/apps/web/src/api/conversation.ts
@@ -5,23 +5,57 @@ import type { ExecutionError, ResearchFailure, ResultManifest } from "./research
import { hasResearchContent } from "./presentation";
import type { Currentness } from "./historyModels";
import { ConversationRefreshError, isTransientReadFailure } from "./conversationRefresh";
-import { readLocalDraft, writeLocalDraft, type BrowserScope, type DraftWriteResult } from "./localWorkspacePersistence";
+import {
+ readLocalDraft,
+ writeLocalDraft,
+ type BrowserScope,
+ type DraftWriteResult,
+} from "./localWorkspacePersistence";
const inputSchema = z.object({
- request_epoch: z.number().int(), request_revision_digest: z.string(),
- operation_id: z.string(), text: z.string(), edit_kind: z.string(), created_at: z.string(),
+ request_epoch: z.number().int(),
+ request_revision_digest: z.string(),
+ operation_id: z.string(),
+ text: z.string(),
+ edit_kind: z.string(),
+ created_at: z.string(),
authored_text_ref: z.object({ revision_digest: z.string() }).passthrough(),
});
-const pageSchema = z.object({ turns: z.array(inputSchema), next_before_epoch: z.number().nullable(), history_limited: z.boolean() });
+const pageSchema = z.object({
+ turns: z.array(inputSchema),
+ next_before_epoch: z.number().nullable(),
+ history_limited: z.boolean(),
+});
export type ConversationInput = z.infer;
-export type ConversationTurn = { input: ConversationInput; result: Record | null; state: string; unavailable?: string; error?:ExecutionError | null; failure?:ResearchFailure | null; terminalReason?:string | null; currentness?: Currentness };
-export type ConversationPage = { turns: ConversationTurn[]; next: number | null; limited: boolean; supported: boolean; redacted?: boolean };
+export type ConversationTurn = {
+ input: ConversationInput;
+ result: Record | null;
+ state: string;
+ unavailable?: string;
+ error?: ExecutionError | null;
+ failure?: ResearchFailure | null;
+ terminalReason?: string | null;
+ currentness?: Currentness;
+};
+export type ConversationPage = {
+ turns: ConversationTurn[];
+ next: number | null;
+ limited: boolean;
+ supported: boolean;
+ redacted?: boolean;
+};
function redactedPage(): ConversationPage {
return { turns: [], next: null, limited: false, supported: true, redacted: true };
}
-export async function readConversation(projectId: string, threadId: string, before: number | null, signal?: AbortSignal, onBlocked?: () => void): Promise {
+export async function readConversation(
+ projectId: string,
+ threadId: string,
+ before: number | null,
+ signal?: AbortSignal,
+ onBlocked?: () => void,
+): Promise {
try {
return await readConversationPage(projectId, threadId, before, signal, onBlocked);
} catch (error) {
@@ -34,66 +68,153 @@ export async function readConversation(projectId: string, threadId: string, befo
}
}
-async function readConversationPage(projectId: string, threadId: string, before: number | null, signal?: AbortSignal, onBlocked?: () => void): Promise {
- const response = await rpc<{ conversation?: unknown }>("thread/activity/list", {
- project_id: projectId, thread_id: threadId, ...(before === null ? {} : { conversation_before_epoch: before }),
- }, crypto.randomUUID(), signal);
- if (response.value.conversation === undefined) return { turns: [], next: null, limited: false, supported: false };
+async function readConversationPage(
+ projectId: string,
+ threadId: string,
+ before: number | null,
+ signal?: AbortSignal,
+ onBlocked?: () => void,
+): Promise {
+ const response = await rpc<{ conversation?: unknown }>(
+ "thread/activity/list",
+ {
+ project_id: projectId,
+ thread_id: threadId,
+ ...(before === null ? {} : { conversation_before_epoch: before }),
+ },
+ crypto.randomUUID(),
+ signal,
+ );
+ if (response.value.conversation === undefined)
+ return { turns: [], next: null, limited: false, supported: false };
const page = pageSchema.parse(response.value.conversation);
- const observations = await Promise.all(page.turns.map(async input => {
- const unavailable: ConversationTurn = { input, result: null, state: "UNAVAILABLE", get unavailable() { return t("conversation.unavailable"); } };
- let value;
- try {
- ({ value } = await rpc<{ operation_id: string; state: string; result: Record | null; error?:ExecutionError | null }>(
- "operation/result/read", { project_id: projectId, operation_id: input.operation_id }, crypto.randomUUID(), signal));
- } catch (error) {
- if (signal?.aborted) throw error;
- const transient = isTransientReadFailure(error);
- if (!transient) onBlocked?.();
- return { turn: unavailable, transient };
- }
- const result = value.result;
- if (value.operation_id !== input.operation_id || (result && (result.thread_id !== threadId || result.request_epoch !== input.request_epoch))) {
- onBlocked?.();
- return { turn: unavailable, transient: false };
- }
- return { turn: { input, result, state: value.state, error:value.error, failure:value.error?.data?.failure,
- terminalReason:typeof result?.terminal_reason === 'string'?result.terminal_reason:null } satisfies ConversationTurn, transient: false };
- }));
- if (observations.some(item => item.transient)) {
+ const observations = await Promise.all(
+ page.turns.map(async (input) => {
+ const unavailable: ConversationTurn = {
+ input,
+ result: null,
+ state: "UNAVAILABLE",
+ get unavailable() {
+ return t("conversation.unavailable");
+ },
+ };
+ let value;
+ try {
+ ({ value } = await rpc<{
+ operation_id: string;
+ state: string;
+ result: Record | null;
+ error?: ExecutionError | null;
+ }>(
+ "operation/result/read",
+ { project_id: projectId, operation_id: input.operation_id },
+ crypto.randomUUID(),
+ signal,
+ ));
+ } catch (error) {
+ if (signal?.aborted) throw error;
+ const transient = isTransientReadFailure(error);
+ if (!transient) onBlocked?.();
+ return { turn: unavailable, transient };
+ }
+ const result = value.result;
+ if (
+ value.operation_id !== input.operation_id ||
+ (result && (result.thread_id !== threadId || result.request_epoch !== input.request_epoch))
+ ) {
+ onBlocked?.();
+ return { turn: unavailable, transient: false };
+ }
+ return {
+ turn: {
+ input,
+ result,
+ state: value.state,
+ error: value.error,
+ failure: value.error?.data?.failure,
+ terminalReason:
+ typeof result?.terminal_reason === "string" ? result.terminal_reason : null,
+ } satisfies ConversationTurn,
+ transient: false,
+ };
+ }),
+ );
+ if (observations.some((item) => item.transient)) {
// A concurrent denial/identity failure must take priority over retained cached content.
- if (observations.some(item => !item.transient && item.turn.state === "UNAVAILABLE")) {
+ if (observations.some((item) => !item.transient && item.turn.state === "UNAVAILABLE")) {
return redactedPage();
}
throw new ConversationRefreshError();
}
- const turns = observations.map(item => item.turn);
+ const turns = observations.map((item) => item.turn);
return { turns, next: page.next_before_epoch, limited: page.history_limited, supported: true };
}
/** A current checkpoint can update only the exact authored request it belongs to. */
-type Checkpoint = Pick & Partial>;
-export function withCurrentCheckpoint(turns: ConversationTurn[], status?: {current_result?: Checkpoint | null; previous_result?: Checkpoint | null; operation_state?: string; operation_error?:ExecutionError | null; failure?:ResearchFailure | null; basis_currentness?: Currentness; request?: { operation_id: string }}): ConversationTurn[] {
+type Checkpoint = Pick &
+ Partial>;
+export function withCurrentCheckpoint(
+ turns: ConversationTurn[],
+ status?: {
+ current_result?: Checkpoint | null;
+ previous_result?: Checkpoint | null;
+ operation_state?: string;
+ operation_error?: ExecutionError | null;
+ failure?: ResearchFailure | null;
+ basis_currentness?: Currentness;
+ request?: { operation_id: string };
+ },
+): ConversationTurn[] {
if (!status) return turns;
const manifest = status.current_result ?? status.previous_result;
const ownsOperation = !status.request || status.request.operation_id === manifest?.operation_id;
- return turns.map(turn => !turn.unavailable && turn.state !== 'UNAVAILABLE' && manifest?.request_ref?.revision_digest === turn.input.request_revision_digest &&
- manifest.operation_id === turn.input.operation_id ? {
- ...turn, result:hasResearchContent(manifest.result) ? manifest.result : turn.result,
- state:ownsOperation && ["FAILED", "CANCELLED"].includes(status.operation_state ?? "") ? status.operation_state! : status.current_result && ownsOperation ? status.operation_state ?? turn.state : ["FAILED", "CANCELLED"].includes(turn.state) ? turn.state : "STALE",
- error:(ownsOperation ? status.operation_error : null) ?? turn.error, failure:(ownsOperation ? status.failure : null) ?? turn.failure,
- currentness: status.basis_currentness ?? turn.currentness,
- terminalReason:manifest.terminal_reason ?? turn.terminalReason,
- } : turn);
+ return turns.map((turn) =>
+ !turn.unavailable &&
+ turn.state !== "UNAVAILABLE" &&
+ manifest?.request_ref?.revision_digest === turn.input.request_revision_digest &&
+ manifest.operation_id === turn.input.operation_id
+ ? {
+ ...turn,
+ result: hasResearchContent(manifest.result) ? manifest.result : turn.result,
+ state:
+ ownsOperation && ["FAILED", "CANCELLED"].includes(status.operation_state ?? "")
+ ? status.operation_state!
+ : status.current_result && ownsOperation
+ ? (status.operation_state ?? turn.state)
+ : ["FAILED", "CANCELLED"].includes(turn.state)
+ ? turn.state
+ : "STALE",
+ error: (ownsOperation ? status.operation_error : null) ?? turn.error,
+ failure: (ownsOperation ? status.failure : null) ?? turn.failure,
+ currentness: status.basis_currentness ?? turn.currentness,
+ terminalReason: manifest.terminal_reason ?? turn.terminalReason,
+ }
+ : turn,
+ );
}
-const draftKey = (project: string, thread: string, scope?: BrowserScope) => `thoth:draft:v1:${JSON.stringify([project, thread, ...(scope?.mode === "HOSTED" && scope.browserScopeId ? [scope.browserScopeId] : [])])}`;
+const draftKey = (project: string, thread: string, scope?: BrowserScope) =>
+ `thoth:draft:v1:${JSON.stringify([project, thread, ...(scope?.mode === "HOSTED" && scope.browserScopeId ? [scope.browserScopeId] : [])])}`;
export function readDraft(project: string, thread: string, scope?: BrowserScope): string {
if (scope?.mode === "LOCAL") return readLocalDraft(scope.workspaceId, project, thread);
- try { return sessionStorage.getItem(draftKey(project, thread, scope)) ?? ""; } catch { return ""; }
+ try {
+ return sessionStorage.getItem(draftKey(project, thread, scope)) ?? "";
+ } catch {
+ return "";
+ }
}
-export function writeDraft(project: string, thread: string, text: string, scope?: BrowserScope): DraftWriteResult {
+export function writeDraft(
+ project: string,
+ thread: string,
+ text: string,
+ scope?: BrowserScope,
+): DraftWriteResult {
if (scope?.mode === "LOCAL") return writeLocalDraft(scope.workspaceId, project, thread, text);
- try { if (text) sessionStorage.setItem(draftKey(project, thread, scope), text); else sessionStorage.removeItem(draftKey(project, thread, scope)); return "SAVED"; }
- catch { return "UNAVAILABLE"; }
+ try {
+ if (text) sessionStorage.setItem(draftKey(project, thread, scope), text);
+ else sessionStorage.removeItem(draftKey(project, thread, scope));
+ return "SAVED";
+ } catch {
+ return "UNAVAILABLE";
+ }
}
diff --git a/apps/web/src/api/conversationRefetchDiagnosis.test.ts b/apps/web/src/api/conversationRefetchDiagnosis.test.ts
index 4aa2fa7b..8236f84d 100644
--- a/apps/web/src/api/conversationRefetchDiagnosis.test.ts
+++ b/apps/web/src/api/conversationRefetchDiagnosis.test.ts
@@ -4,31 +4,65 @@ import { readConversation, withCurrentCheckpoint, type ConversationPage } from "
import { canRetainConversation } from "./conversationRefresh";
import { rpc, RpcError, RpcTransportError } from "./rpcClient";
-vi.mock("./rpcClient", async importOriginal => ({
- ...await importOriginal(), rpc: vi.fn(),
+vi.mock("./rpcClient", async (importOriginal) => ({
+ ...(await importOriginal()),
+ rpc: vi.fn(),
}));
const clients: QueryClient[] = [];
-afterEach(() => { clients.splice(0).forEach(client => client.clear()); vi.resetAllMocks(); });
+afterEach(() => {
+ clients.splice(0).forEach((client) => client.clear());
+ vi.resetAllMocks();
+});
async function readableAnswerThen(error: Error) {
let interrupted = false;
- vi.mocked(rpc).mockImplementation(async method => {
- if (method === "thread/activity/list") return { operation_id: "query", state: "SUCCEEDED", value: {
- conversation: { turns: [{ request_epoch: 1, request_revision_digest: "request-1", operation_id: "op-1",
- text: "saved question", edit_kind: "APPEND", created_at: "2026-09-21T09:45:09Z",
- authored_text_ref: { revision_digest: "authored-1" } }], next_before_epoch: null, history_limited: false },
- }};
+ vi.mocked(rpc).mockImplementation(async (method) => {
+ if (method === "thread/activity/list")
+ return {
+ operation_id: "query",
+ state: "SUCCEEDED",
+ value: {
+ conversation: {
+ turns: [
+ {
+ request_epoch: 1,
+ request_revision_digest: "request-1",
+ operation_id: "op-1",
+ text: "saved question",
+ edit_kind: "APPEND",
+ created_at: "2026-09-21T09:45:09Z",
+ authored_text_ref: { revision_digest: "authored-1" },
+ },
+ ],
+ next_before_epoch: null,
+ history_limited: false,
+ },
+ },
+ };
if (interrupted) throw error;
- return { operation_id: "query", state: "SUCCEEDED", value: {
- operation_id: "op-1", state: "SUCCEEDED", result: { thread_id: "t", request_epoch: 1, answer: "saved answer" },
- }};
+ return {
+ operation_id: "query",
+ state: "SUCCEEDED",
+ value: {
+ operation_id: "op-1",
+ state: "SUCCEEDED",
+ result: { thread_id: "t", request_epoch: 1, answer: "saved answer" },
+ },
+ };
+ });
+ const client = new QueryClient({
+ defaultOptions: { queries: { retry: false, gcTime: Infinity } },
});
- const client = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: Infinity } } });
clients.push(client);
- const options = { queryKey: ["conversation", "p", "t"], queryFn: () => readConversation("p", "t", null) };
+ const options = {
+ queryKey: ["conversation", "p", "t"],
+ queryFn: () => readConversation("p", "t", null),
+ };
await client.fetchQuery(options);
- expect(client.getQueryData(options.queryKey)?.turns[0].result?.answer).toBe("saved answer");
+ expect(client.getQueryData(options.queryKey)?.turns[0].result?.answer).toBe(
+ "saved answer",
+ );
interrupted = true;
return { client, options };
}
@@ -36,18 +70,27 @@ async function readableAnswerThen(error: Error) {
it.each([
["network", () => new RpcTransportError("RPC network request failed", "NETWORK")],
["timeout", () => new RpcTransportError("RPC timed out", "TIMEOUT")],
- ["temporary upstream failure", () => new RpcTransportError("RPC transport failed (504)", "HTTP", 504)],
-] as const)("keeps a saved answer when a return-to-window refetch has a %s error", async (_label, makeError) => {
- const { client, options } = await readableAnswerThen(makeError());
- await expect(client.fetchQuery(options)).rejects.toBeInstanceOf(Error);
- const cached = client.getQueryData(options.queryKey)!;
- expect(cached.turns[0].result?.answer).toBe("saved answer");
- expect(client.getQueryState(options.queryKey)?.status).toBe("error");
-});
+ [
+ "temporary upstream failure",
+ () => new RpcTransportError("RPC transport failed (504)", "HTTP", 504),
+ ],
+] as const)(
+ "keeps a saved answer when a return-to-window refetch has a %s error",
+ async (_label, makeError) => {
+ const { client, options } = await readableAnswerThen(makeError());
+ await expect(client.fetchQuery(options)).rejects.toBeInstanceOf(Error);
+ const cached = client.getQueryData(options.queryKey)!;
+ expect(cached.turns[0].result?.answer).toBe("saved answer");
+ expect(client.getQueryState(options.queryKey)?.status).toBe("error");
+ },
+);
it("replaces a saved answer with an unavailable state when access is actually denied", async () => {
- const { client, options } = await readableAnswerThen(new RpcError("resource access rejected", -32040,
- { reason_code: "RESOURCE_SCOPE_ACCESS_DENIED" }));
+ const { client, options } = await readableAnswerThen(
+ new RpcError("resource access rejected", -32040, {
+ reason_code: "RESOURCE_SCOPE_ACCESS_DENIED",
+ }),
+ );
const refreshed = await client.fetchQuery(options);
expect(refreshed.turns[0].state).toBe("UNAVAILABLE");
expect(refreshed.turns[0].result).toBeNull();
@@ -55,10 +98,23 @@ it("replaces a saved answer with an unavailable state when access is actually de
});
it("does not classify malformed result data as a temporary transport error", async () => {
- const { client, options } = await readableAnswerThen(new RpcTransportError("RPC network request failed", "NETWORK"));
+ const { client, options } = await readableAnswerThen(
+ new RpcTransportError("RPC network request failed", "NETWORK"),
+ );
const existing = client.getQueryData(options.queryKey)!;
- vi.mocked(rpc).mockImplementation(async method => ({ operation_id: "query", state: "SUCCEEDED", value:
- method === "thread/activity/list" ? { conversation: { turns: existing.turns.map(turn => turn.input), next_before_epoch: null, history_limited: false } } : undefined,
+ vi.mocked(rpc).mockImplementation(async (method) => ({
+ operation_id: "query",
+ state: "SUCCEEDED",
+ value:
+ method === "thread/activity/list"
+ ? {
+ conversation: {
+ turns: existing.turns.map((turn) => turn.input),
+ next_before_epoch: null,
+ history_limited: false,
+ },
+ }
+ : undefined,
}));
const refreshed = await client.fetchQuery(options);
expect(refreshed.redacted).toBe(true);
@@ -67,12 +123,19 @@ it("does not classify malformed result data as a temporary transport error", asy
});
it("keeps an earlier stored failure when a different request is current", async () => {
- const { client, options } = await readableAnswerThen(new RpcTransportError("RPC network request failed", "NETWORK"));
+ const { client, options } = await readableAnswerThen(
+ new RpcTransportError("RPC network request failed", "NETWORK"),
+ );
const turn = client.getQueryData(options.queryKey)!.turns[0];
const error = { message: "earlier stored failure" };
const merged = withCurrentCheckpoint([{ ...turn, state: "FAILED", error }], {
- request: { operation_id: "op-2" }, operation_state: "SUCCEEDED",
- previous_result: { request_ref: { revision_digest: "request-1" }, operation_id: "op-1", result: turn.result! },
+ request: { operation_id: "op-2" },
+ operation_state: "SUCCEEDED",
+ previous_result: {
+ request_ref: { revision_digest: "request-1" },
+ operation_id: "op-1",
+ result: turn.result!,
+ },
});
expect(merged[0].state).toBe("FAILED");
expect(merged[0].error).toBe(error);
diff --git a/apps/web/src/api/expectedResultContract.ts b/apps/web/src/api/expectedResultContract.ts
index 54d46aed..d3446494 100644
--- a/apps/web/src/api/expectedResultContract.ts
+++ b/apps/web/src/api/expectedResultContract.ts
@@ -5,5 +5,7 @@ export function expectedResultContract(ready: unknown): "v4" | "v5" | undefined
if (ready.expected_result_contracts.includes("v5")) return "v5";
return ready.expected_result_contracts.includes("v4") ? "v4" : undefined;
}
- return "response_language_scope" in ready && ready.response_language_scope === "REQUEST" ? "v4" : undefined;
+ return "response_language_scope" in ready && ready.response_language_scope === "REQUEST"
+ ? "v4"
+ : undefined;
}
diff --git a/apps/web/src/api/expectedResultState.test.ts b/apps/web/src/api/expectedResultState.test.ts
index ced40860..e1ea752d 100644
--- a/apps/web/src/api/expectedResultState.test.ts
+++ b/apps/web/src/api/expectedResultState.test.ts
@@ -4,7 +4,13 @@ import { expectedUnknown } from "./expectedResultState";
it("uses the shared state fixture without changing legacy Unicode or display semantics", () => {
for (const item of fixture) {
- expect(expectedUnknown(item.row.expected ?? "", item.row.knowledge_state, "ranking"), item.name).toBe(item.web);
- expect(expectedUnknown(item.row.expected ?? "", item.row.knowledge_state, "annotation"), item.name).toBe(item.annotation);
+ expect(
+ expectedUnknown(item.row.expected ?? "", item.row.knowledge_state, "ranking"),
+ item.name,
+ ).toBe(item.web);
+ expect(
+ expectedUnknown(item.row.expected ?? "", item.row.knowledge_state, "annotation"),
+ item.name,
+ ).toBe(item.annotation);
}
});
diff --git a/apps/web/src/api/expectedResultState.ts b/apps/web/src/api/expectedResultState.ts
index 8262fec8..2ad216f9 100644
--- a/apps/web/src/api/expectedResultState.ts
+++ b/apps/web/src/api/expectedResultState.ts
@@ -1,6 +1,11 @@
/** Legacy consumers intentionally retain their different historical whitespace rules. */
-export function expectedUnknown(label: string, state: unknown, consumer: "ranking" | "annotation"): boolean {
+export function expectedUnknown(
+ label: string,
+ state: unknown,
+ consumer: "ranking" | "annotation",
+): boolean {
if (state !== undefined) return state === "UNKNOWN";
- const normalized = consumer === "ranking" ? label.trim().replace(/\s+/g, " ").toLowerCase() : label.trim();
+ const normalized =
+ consumer === "ranking" ? label.trim().replace(/\s+/g, " ").toLowerCase() : label.trim();
return normalized === "모름";
}
diff --git a/apps/web/src/api/files.test.ts b/apps/web/src/api/files.test.ts
index 274bfbdf..38f19cb6 100644
--- a/apps/web/src/api/files.test.ts
+++ b/apps/web/src/api/files.test.ts
@@ -7,12 +7,22 @@ import { UploadRefused } from "./uploadTypes";
type Progress = { phase: "uploading" | "processing"; loaded: number; total: number | null };
type Options = { signal?: AbortSignal; onProgress?: (progress: Progress) => void };
const upload: (file: File, project: string, options?: Options) => Promise = stageFile;
-const staged: StagedFile = { relative_path: "uploads/p/a.md", filename: "a.md", media_type: "text/markdown", byte_sha256: "a".repeat(64), bytes: 4 };
-const stalled = "No data was sent for 2 minutes, so the upload stopped. Check your connection and try again. No source was connected.";
+const staged: StagedFile = {
+ relative_path: "uploads/p/a.md",
+ filename: "a.md",
+ media_type: "text/markdown",
+ byte_sha256: "a".repeat(64),
+ bytes: 4,
+};
+const stalled =
+ "No data was sent for 2 minutes, so the upload stopped. Check your connection and try again. No source was connected.";
class FakeXHR {
static instances: FakeXHR[] = [];
- upload = { onprogress: null as null | ((event: ProgressEvent) => void), onload: null as null | (() => void) };
+ upload = {
+ onprogress: null as null | ((event: ProgressEvent) => void),
+ onload: null as null | (() => void),
+ };
onload: null | (() => void) = null;
onerror: null | (() => void) = null;
onabort: null | (() => void) = null;
@@ -25,33 +35,58 @@ class FakeXHR {
body: FormData | null = null;
open = vi.fn();
setRequestHeader = vi.fn();
- send = vi.fn((body: FormData) => { this.body = body; });
+ send = vi.fn((body: FormData) => {
+ this.body = body;
+ });
abort = vi.fn(() => this.onabort?.());
- constructor() { FakeXHR.instances.push(this); }
+ constructor() {
+ FakeXHR.instances.push(this);
+ }
progress(loaded: number, total = 100, lengthComputable = true) {
- this.upload.onprogress?.(Object.assign(new Event("progress"), { loaded, total, lengthComputable }));
+ this.upload.onprogress?.(
+ Object.assign(new Event("progress"), { loaded, total, lengthComputable }),
+ );
}
complete(status = 200, text = JSON.stringify(staged)) {
- this.status = status; this.responseText = text; this.onload?.();
+ this.status = status;
+ this.responseText = text;
+ this.onload?.();
}
}
beforeEach(() => {
- vi.useFakeTimers(); setLocale("en"); FakeXHR.instances = [];
+ vi.useFakeTimers();
+ setLocale("en");
+ FakeXHR.instances = [];
vi.stubGlobal("XMLHttpRequest", FakeXHR);
- vi.stubGlobal("fetch", vi.fn(() => new Promise(() => {})));
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(() => new Promise(() => {})),
+ );
+});
+afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllGlobals();
+ setLocale("ko");
});
-afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); setLocale("ko"); });
function start(options?: Options) {
const result: { value?: StagedFile; error?: Error } = {};
const file = new File(["data"], "a.md", { type: "text/markdown" });
const done = upload(file, "project:p", options).then(
- value => { result.value = value; }, error => { result.error = error; },
+ (value) => {
+ result.value = value;
+ },
+ (error) => {
+ result.error = error;
+ },
);
return { result, done, file };
}
-function xhr() { expect(FakeXHR.instances).toHaveLength(1); return FakeXHR.instances[0]; }
+function xhr() {
+ expect(FakeXHR.instances).toHaveLength(1);
+ return FakeXHR.instances[0];
+}
async function expectNoUploadTimer() {
// Drain DOM bookkeeping at the same instant; a leaked 120-second timer would remain.
await vi.advanceTimersByTimeAsync(0);
@@ -67,21 +102,26 @@ it("fails at 120 seconds without progress, not at 119.999 seconds", async () =>
expect(xhr().abort).toHaveBeenCalledTimes(1);
await expectNoUploadTimer();
setLocale("ko");
- expect(result.error?.message).toBe("파일을 보내는 중 2분 동안 진행이 없어 멈췄습니다. 연결을 확인하고 다시 시도하세요. 연결된 자료는 없습니다.");
+ expect(result.error?.message).toBe(
+ "파일을 보내는 중 2분 동안 진행이 없어 멈췄습니다. 연결을 확인하고 다시 시도하세요. 연결된 자료는 없습니다.",
+ );
});
it("allows a slow upload beyond two minutes while bytes keep increasing", async () => {
const updates: Progress[] = [];
- const { result, done, file } = start({ onProgress: value => updates.push(value) });
+ const { result, done, file } = start({ onProgress: (value) => updates.push(value) });
const request = xhr();
expect(request.open).toHaveBeenCalledWith("POST", "/files/stage");
expect(request.setRequestHeader).toHaveBeenCalledWith("x-thoth-project-id", "project:p");
expect(request.body?.get("file")).toBe(file);
for (const loaded of [10, 20, 30]) {
- await vi.advanceTimersByTimeAsync(119000); request.progress(loaded);
+ await vi.advanceTimersByTimeAsync(119000);
+ request.progress(loaded);
expect(result.error).toBeUndefined();
}
- request.upload.onload?.(); request.complete(); await done;
+ request.upload.onload?.();
+ request.complete();
+ await done;
expect(result.value).toEqual(staged);
expect(updates).toContainEqual({ phase: "uploading", loaded: 30, total: 100 });
expect(updates.at(-1)?.phase).toBe("processing");
@@ -90,58 +130,79 @@ it("allows a slow upload beyond two minutes while bytes keep increasing", async
});
it("does not renew the deadline for repeated or decreasing byte counts", async () => {
- const { result } = start(); const request = xhr();
- await vi.advanceTimersByTimeAsync(110000); request.progress(10);
- await vi.advanceTimersByTimeAsync(119000); request.progress(10); request.progress(9);
+ const { result } = start();
+ const request = xhr();
+ await vi.advanceTimersByTimeAsync(110000);
+ request.progress(10);
+ await vi.advanceTimersByTimeAsync(119000);
+ request.progress(10);
+ request.progress(9);
expect(result.error).toBeUndefined();
await vi.advanceTimersByTimeAsync(1000);
expect(result.error?.message).toBe(stalled);
});
it("tracks increasing bytes even if the total length is unknown", async () => {
- const progress = vi.fn(); const { result, done } = start({ onProgress: progress });
+ const progress = vi.fn();
+ const { result, done } = start({ onProgress: progress });
const request = xhr();
- await vi.advanceTimersByTimeAsync(119000); request.progress(10, 0, false);
+ await vi.advanceTimersByTimeAsync(119000);
+ request.progress(10, 0, false);
await vi.advanceTimersByTimeAsync(119000);
expect(result.error).toBeUndefined();
expect(progress).toHaveBeenLastCalledWith({ phase: "uploading", loaded: 10, total: null });
- request.complete(); await done;
+ request.complete();
+ await done;
});
it("has no processing deadline after upload completion and ignores late progress", async () => {
- const { result, done } = start(); const request = xhr();
- await vi.advanceTimersByTimeAsync(119000); request.upload.onload?.();
- await vi.advanceTimersByTimeAsync(180000); request.progress(100);
+ const { result, done } = start();
+ const request = xhr();
+ await vi.advanceTimersByTimeAsync(119000);
+ request.upload.onload?.();
+ await vi.advanceTimersByTimeAsync(180000);
+ request.progress(100);
expect(result.error).toBeUndefined();
await expectNoUploadTimer();
- request.complete(); await done;
+ request.complete();
+ await done;
expect(result.value).toEqual(staged);
});
it("allows cancellation while the server is processing without a timer", async () => {
const controller = new AbortController();
- const { result, done } = start({ signal: controller.signal }); const request = xhr();
- request.upload.onload?.(); await vi.advanceTimersByTimeAsync(180000);
+ const { result, done } = start({ signal: controller.signal });
+ const request = xhr();
+ request.upload.onload?.();
+ await vi.advanceTimersByTimeAsync(180000);
expect(result.error).toBeUndefined();
- controller.abort(); await done;
+ controller.abort();
+ await done;
expect(result.error?.message).toBe("Upload cancelled. No source was connected.");
expect(request.abort).toHaveBeenCalledTimes(1);
await expectNoUploadTimer();
});
it("cancels the request and ignores a late success callback", async () => {
- const controller = new AbortController(); const { result, done } = start({ signal: controller.signal });
- const request = xhr(); const lateLoad = request.onload;
- controller.abort(); await done;
+ const controller = new AbortController();
+ const { result, done } = start({ signal: controller.signal });
+ const request = xhr();
+ const lateLoad = request.onload;
+ controller.abort();
+ await done;
expect(request.abort).toHaveBeenCalledTimes(1);
expect(result.error?.message).toBe("Upload cancelled. No source was connected.");
- request.status = 200; request.responseText = JSON.stringify(staged); lateLoad?.();
+ request.status = 200;
+ request.responseText = JSON.stringify(staged);
+ lateLoad?.();
expect(result.value).toBeUndefined();
await expectNoUploadTimer();
});
it("does not send a request for an already cancelled signal", async () => {
- const controller = new AbortController(); controller.abort(); const { result } = start({ signal: controller.signal });
+ const controller = new AbortController();
+ controller.abort();
+ const { result } = start({ signal: controller.signal });
await vi.advanceTimersByTimeAsync(0);
expect(result.error?.message).toBe("Upload cancelled. No source was connected.");
expect(FakeXHR.instances).toHaveLength(0);
@@ -151,20 +212,26 @@ it("does not send a request for an already cancelled signal", async () => {
it("does not send if cancellation occurs during initial progress notification", async () => {
const controller = new AbortController();
- const { result, done } = start({ signal: controller.signal, onProgress: () => controller.abort() });
+ const { result, done } = start({
+ signal: controller.signal,
+ onProgress: () => controller.abort(),
+ });
await done;
expect(result.error?.message).toBe("Upload cancelled. No source was connected.");
expect(xhr().send).not.toHaveBeenCalled();
await expectNoUploadTimer();
});
-it.each([[413, '{"detail":"server detail"}', "server detail"], [503, "not json", "503"]])(
- "preserves HTTP error handling for %s", async (status, text, expected) => {
- const { result, done } = start(); xhr().complete(Number(status), String(text)); await done;
- expect(result.error?.message).toContain(String(expected));
- await expectNoUploadTimer();
- },
-);
+it.each([
+ [413, '{"detail":"server detail"}', "server detail"],
+ [503, "not json", "503"],
+])("preserves HTTP error handling for %s", async (status, text, expected) => {
+ const { result, done } = start();
+ xhr().complete(Number(status), String(text));
+ await done;
+ expect(result.error?.message).toContain(String(expected));
+ await expectNoUploadTimer();
+});
it("turns a refusal the server names by code into one that names the file, and keeps the old sentence otherwise", async () => {
const refused = start();
@@ -173,7 +240,9 @@ it("turns a refusal the server names by code into one that names the file, and k
await refused.done;
expect(refused.result.error).toBeInstanceOf(UploadRefused);
expect(refused.result.error).toMatchObject({ code: "UPLOAD_UNSUPPORTED_TYPE", filename: "a.md" });
- expect(refused.result.error?.message).toContain('"a.md" is not a file type that can be uploaded.');
+ expect(refused.result.error?.message).toContain(
+ '"a.md" is not a file type that can be uploaded.',
+ );
setLocale("ko");
expect(refused.result.error?.message).toContain('"a.md" 파일은 올릴 수 없는 형식입니다.');
setLocale("en");
@@ -189,13 +258,17 @@ it("turns a refusal the server names by code into one that names the file, and k
});
it("cleans up after malformed successful JSON", async () => {
- const { result, done } = start(); xhr().complete(200, "invalid json"); await done;
+ const { result, done } = start();
+ xhr().complete(200, "invalid json");
+ await done;
expect(result.error).toBeInstanceOf(SyntaxError);
await expectNoUploadTimer();
});
it("cleans up after network errors", async () => {
- const { result, done } = start(); xhr().onerror?.(); await done;
+ const { result, done } = start();
+ xhr().onerror?.();
+ await done;
expect(result.error).toBeInstanceOf(Error);
await expectNoUploadTimer();
});
diff --git a/apps/web/src/api/files.ts b/apps/web/src/api/files.ts
index 70e76451..733872a2 100644
--- a/apps/web/src/api/files.ts
+++ b/apps/web/src/api/files.ts
@@ -21,7 +21,11 @@ export type StageFileOptions = {
const STALL_MS = 120_000;
-export function stageFile(file: File, projectId: string, options: StageFileOptions = {}): Promise {
+export function stageFile(
+ file: File,
+ projectId: string,
+ options: StageFileOptions = {},
+): Promise {
if (options.signal?.aborted) return Promise.reject(new UiError("sources.fileUploadCancelled"));
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
@@ -40,13 +44,18 @@ export function stageFile(file: File, projectId: string, options: StageFileOptio
if (error !== undefined) reject(error);
else resolve(result!);
};
- const stop = (error: Error) => { if (!settled) { finish(error); xhr.abort(); } };
+ const stop = (error: Error) => {
+ if (!settled) {
+ finish(error);
+ xhr.abort();
+ }
+ };
const cancel = () => stop(new UiError("sources.fileUploadCancelled"));
const renew = () => {
clearTimeout(timer);
timer = setTimeout(() => stop(new UiError("sources.fileUploadStalled")), STALL_MS);
};
- xhr.upload.onprogress = event => {
+ xhr.upload.onprogress = (event) => {
if (settled || processing || event.loaded <= loaded) return;
loaded = event.loaded;
total = event.lengthComputable && event.total > 0 ? event.total : null;
@@ -70,12 +79,19 @@ export function stageFile(file: File, projectId: string, options: StageFileOptio
return;
}
let detail: { detail?: string } | null = null;
- try { detail = JSON.parse(xhr.responseText); } catch { /* Preserve the HTTP fallback. */ }
+ try {
+ detail = JSON.parse(xhr.responseText);
+ } catch {
+ /* Preserve the HTTP fallback. */
+ }
finish(new Error(detail?.detail ?? formatMessage("filePrepareFailed", xhr.status)));
return;
}
- try { finish(undefined, JSON.parse(xhr.responseText) as StagedFile); }
- catch (error) { finish(error); }
+ try {
+ finish(undefined, JSON.parse(xhr.responseText) as StagedFile);
+ } catch (error) {
+ finish(error);
+ }
};
xhr.onerror = () => finish(new Error(formatMessage("filePrepareFailed", xhr.status)));
xhr.onabort = () => finish(new UiError("sources.fileUploadCancelled"));
@@ -89,6 +105,8 @@ export function stageFile(file: File, projectId: string, options: StageFileOptio
renew();
options.onProgress?.({ phase: "uploading", loaded, total });
if (!settled) xhr.send(body);
- } catch (error) { finish(error); }
+ } catch (error) {
+ finish(error);
+ }
});
}
diff --git a/apps/web/src/api/forgetProject.test.ts b/apps/web/src/api/forgetProject.test.ts
index 881e9790..0d5d7deb 100644
--- a/apps/web/src/api/forgetProject.test.ts
+++ b/apps/web/src/api/forgetProject.test.ts
@@ -1,11 +1,19 @@
// @vitest-environment jsdom
import { afterEach, expect, it } from "vitest";
import { forgetProject } from "./forgetProject";
-import { inspectLocalContext, readLocalDraft, writeLocalContext, writeLocalDraft } from "./localWorkspacePersistence";
+import {
+ inspectLocalContext,
+ readLocalDraft,
+ writeLocalContext,
+ writeLocalDraft,
+} from "./localWorkspacePersistence";
const HERE = `workspace:${"c".repeat(32)}`;
const OTHER = `workspace:${"d".repeat(32)}`;
-afterEach(() => { localStorage.clear(); sessionStorage.clear(); });
+afterEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+});
it("removes drafts and the last place of the deleted project in this workspace and nothing else", () => {
writeLocalDraft(HERE, "gone", "t1", "unsent one");
@@ -29,4 +37,3 @@ it("leaves the last place alone when it points at another project", () => {
forgetProject({ mode: "LOCAL", workspaceId: HERE }, "gone");
expect(inspectLocalContext(HERE).context).toEqual({ projectId: "kept", threadId: "t1" });
});
-
diff --git a/apps/web/src/api/forgetProject.ts b/apps/web/src/api/forgetProject.ts
index 192e8ed3..92068c7c 100644
--- a/apps/web/src/api/forgetProject.ts
+++ b/apps/web/src/api/forgetProject.ts
@@ -1,4 +1,8 @@
-import { inspectLocalContext, writeLocalContext, type BrowserScope } from "./localWorkspacePersistence";
+import {
+ inspectLocalContext,
+ writeLocalContext,
+ type BrowserScope,
+} from "./localWorkspacePersistence";
const localPrefix = "thoth:local-workspace:v1:";
const sessionPrefixes = ["thoth:draft:v1:", "thoth:restore-pending:v1:"];
@@ -16,7 +20,9 @@ function parsedList(raw: string): unknown[] | null {
try {
const value: unknown = JSON.parse(raw);
return Array.isArray(value) ? value : null;
- } catch { return null; }
+ } catch {
+ return null;
+ }
}
/**
@@ -27,17 +33,21 @@ export function forgetProject(scope: BrowserScope, projectId: string): void {
try {
if (scope.mode === "LOCAL" && scope.workspaceId) {
const context = inspectLocalContext(scope.workspaceId);
- if (context.kind === "VALID" && context.context.projectId === projectId) writeLocalContext(scope.workspaceId, { projectId: "", threadId: "" });
+ if (context.kind === "VALID" && context.context.projectId === projectId)
+ writeLocalContext(scope.workspaceId, { projectId: "", threadId: "" });
for (const key of keysOf(localStorage)) {
- const kind = ["draft:", "pending:"].find(name => key.startsWith(localPrefix + name));
+ const kind = ["draft:", "pending:"].find((name) => key.startsWith(localPrefix + name));
const identity = kind ? parsedList(key.slice(localPrefix.length + kind.length)) : null;
- if (identity && identity[0] === scope.workspaceId && identity[1] === projectId) localStorage.removeItem(key);
+ if (identity && identity[0] === scope.workspaceId && identity[1] === projectId)
+ localStorage.removeItem(key);
}
}
for (const key of keysOf(sessionStorage)) {
- const prefix = sessionPrefixes.find(name => key.startsWith(name));
+ const prefix = sessionPrefixes.find((name) => key.startsWith(name));
const identity = prefix ? parsedList(key.slice(prefix.length)) : null;
if (identity && identity[0] === projectId) sessionStorage.removeItem(key);
}
- } catch { /* Storage can be unavailable; nothing the server holds depends on it. */ }
+ } catch {
+ /* Storage can be unavailable; nothing the server holds depends on it. */
+ }
}
diff --git a/apps/web/src/api/groundingNegotiation.test.ts b/apps/web/src/api/groundingNegotiation.test.ts
index ebdbee6f..3ccdd19a 100644
--- a/apps/web/src/api/groundingNegotiation.test.ts
+++ b/apps/web/src/api/groundingNegotiation.test.ts
@@ -15,42 +15,59 @@ it.each([
});
it("negotiates all three reads across old, v5 and legacy servers without changing commands", async () => {
- const sent: {method: string; params: {input: Record}}[] = [];
+ const sent: { method: string; params: { input: Record } }[] = [];
const reads = ["thread/read", "operation/result/read", "thread/result/read"];
let ready: Record = {};
- vi.stubGlobal("fetch", vi.fn(async (_url, options) => {
- const request = JSON.parse(String(options.body)); sent.push(request);
- return Response.json({ jsonrpc: "2.0", id: request.id, result: { operation_id: "", state: "SUCCEEDED", value: request.method === "workspace/ready" ? ready : {} } });
- }));
+ vi.stubGlobal(
+ "fetch",
+ vi.fn(async (_url, options) => {
+ const request = JSON.parse(String(options.body));
+ sent.push(request);
+ return Response.json({
+ jsonrpc: "2.0",
+ id: request.id,
+ result: {
+ operation_id: "",
+ state: "SUCCEEDED",
+ value: request.method === "workspace/ready" ? ready : {},
+ },
+ });
+ }),
+ );
await rpc("workspace/ready", {}, "ready-unsupported");
for (const method of reads) {
- await rpc(method, {project_id: "p", operation_id: "op"}, `unsupported-${method}`);
+ await rpc(method, { project_id: "p", operation_id: "op" }, `unsupported-${method}`);
expect(sent.at(-1)!.params.input).not.toHaveProperty("expected_result_contract");
}
ready = { expected_result_contracts: ["v4", "v5"] };
await rpc("workspace/ready", {}, "ready-v5");
for (const method of reads) {
- await rpc(method, {project_id: "p"}, method);
+ await rpc(method, { project_id: "p" }, method);
expect(sent.at(-1)!.params.input.expected_result_contract).toBe("v5");
}
- await rpc("thread/read", {expected_result_contract: "v4"}, "explicit-v4");
+ await rpc("thread/read", { expected_result_contract: "v4" }, "explicit-v4");
expect(sent.at(-1)!.params.input.expected_result_contract).toBe("v4");
- const command = {project_id: "p", thread_id: "t", contract_version: 2,
- instruction: "Synthetic question", response_language: "en"};
+ const command = {
+ project_id: "p",
+ thread_id: "t",
+ contract_version: 2,
+ instruction: "Synthetic question",
+ response_language: "en",
+ };
const originalCommand = structuredClone(command);
await rpc("thread/input", command, "command");
expect(sent.at(-1)!.params.input).toEqual(originalCommand);
expect(command).toEqual(originalCommand);
- ready = {response_language_scope: "REQUEST"};
+ ready = { response_language_scope: "REQUEST" };
await rpc("workspace/ready", {}, "ready-legacy");
for (const method of reads) {
- await rpc(method, {project_id: "p", operation_id: "op"}, `legacy-${method}`);
+ await rpc(method, { project_id: "p", operation_id: "op" }, `legacy-${method}`);
expect(sent.at(-1)!.params.input.expected_result_contract).toBe("v4");
}
ready = {};
await rpc("workspace/ready", {}, "ready-old");
for (const method of reads) {
- await rpc(method, {project_id: "p", operation_id: "op"}, `old-${method}`);
+ await rpc(method, { project_id: "p", operation_id: "op" }, `old-${method}`);
expect(sent.at(-1)!.params.input).not.toHaveProperty("expected_result_contract");
}
});
diff --git a/apps/web/src/api/history.test.ts b/apps/web/src/api/history.test.ts
index 4fe3894a..0b306931 100644
--- a/apps/web/src/api/history.test.ts
+++ b/apps/web/src/api/history.test.ts
@@ -4,13 +4,37 @@ import { readHistoryEntry, readHistoryPage } from "./history";
const digest = "a".repeat(64);
const scope = { projectId: "project-1", threadId: "thread-1", requestDigest: digest };
-const coverage = { visibility: "AUTHORIZED_SUBSET", association: "EXACT", scan: "COMPLETE_PAGE", reasons: [] };
-const envelope = (value: unknown) => new Response(JSON.stringify({ jsonrpc: "2.0", id: "test", result: { operation_id: "", state: "SUCCEEDED", value } }), { status: 200 });
-afterEach(() => { vi.unstubAllGlobals(); vi.restoreAllMocks(); });
+const coverage = {
+ visibility: "AUTHORIZED_SUBSET",
+ association: "EXACT",
+ scan: "COMPLETE_PAGE",
+ reasons: [],
+};
+const envelope = (value: unknown) =>
+ new Response(
+ JSON.stringify({
+ jsonrpc: "2.0",
+ id: "test",
+ result: { operation_id: "", state: "SUCCEEDED", value },
+ }),
+ { status: 200 },
+ );
+afterEach(() => {
+ vi.unstubAllGlobals();
+ vi.restoreAllMocks();
+});
it("preserves an empty limited page and its continuation cursor", async () => {
- const fetcher = vi.fn().mockResolvedValue(envelope({ contract_version: 2, items: [], next_cursor: "next-scanned-page", actor_scope_digest: digest,
- capability: { restore: "READ_ONLY", apply_ready: false, reason_codes: [] }, coverage: { ...coverage, scan: "LIMITED" } }));
+ const fetcher = vi.fn().mockResolvedValue(
+ envelope({
+ contract_version: 2,
+ items: [],
+ next_cursor: "next-scanned-page",
+ actor_scope_digest: digest,
+ capability: { restore: "READ_ONLY", apply_ready: false, reason_codes: [] },
+ coverage: { ...coverage, scan: "LIMITED" },
+ }),
+ );
vi.stubGlobal("fetch", fetcher);
const page = await readHistoryPage(scope, null);
expect(page.items).toEqual([]);
@@ -20,7 +44,15 @@ it("preserves an empty limited page and its continuation cursor", async () => {
});
it("never falls back to a journaling RPC when a readonly method is unsupported", async () => {
- const fetcher = vi.fn().mockResolvedValue(new Response(JSON.stringify({ jsonrpc: "2.0", id: "test", error: { code: -32601, message: "Method not found", data: {} } })));
+ const fetcher = vi.fn().mockResolvedValue(
+ new Response(
+ JSON.stringify({
+ jsonrpc: "2.0",
+ id: "test",
+ error: { code: -32601, message: "Method not found", data: {} },
+ }),
+ ),
+ );
vi.stubGlobal("fetch", fetcher);
await expect(readHistoryPage(scope, null)).rejects.toThrow();
expect(fetcher).toHaveBeenCalledTimes(1);
@@ -28,18 +60,54 @@ it("never falls back to a journaling RPC when a readonly method is unsupported",
});
it("rejects an answer belonging to a different request instead of displaying it", async () => {
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue(envelope({ contract_version: 2, project_id: "project-1", thread_id: "thread-1",
- request_revision_digest: "b".repeat(64), operation_id: "op-1", request: {}, manifest: null,
- result: { answer: "This is another request's answer" }, error: null, availability: "AVAILABLE", coverage,
- basis_currentness: { state: "CURRENT", reasons: [], execution_eligible: false } })));
- await expect(readHistoryEntry({ kind: "result", scope, operationId: "op-1" })).rejects.toThrow(/연결/);
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ envelope({
+ contract_version: 2,
+ project_id: "project-1",
+ thread_id: "thread-1",
+ request_revision_digest: "b".repeat(64),
+ operation_id: "op-1",
+ request: {},
+ manifest: null,
+ result: { answer: "This is another request's answer" },
+ error: null,
+ availability: "AVAILABLE",
+ coverage,
+ basis_currentness: { state: "CURRENT", reasons: [], execution_eligible: false },
+ }),
+ ),
+ );
+ await expect(readHistoryEntry({ kind: "result", scope, operationId: "op-1" })).rejects.toThrow(
+ /연결/,
+ );
});
it("returns an unavailable historical answer without substituting current content", async () => {
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue(envelope({ contract_version: 2, project_id: "project-1", thread_id: "thread-1",
- request_revision_digest: digest, operation_id: "op-1", request: {}, manifest: null, result: null,
- error: null, availability: "UNAVAILABLE", coverage: { ...coverage, association: "PARTIAL" },
- basis_currentness: { state: "UNAVAILABLE", reasons: ["ACCESS_REVOKED"], execution_eligible: false } })));
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ envelope({
+ contract_version: 2,
+ project_id: "project-1",
+ thread_id: "thread-1",
+ request_revision_digest: digest,
+ operation_id: "op-1",
+ request: {},
+ manifest: null,
+ result: null,
+ error: null,
+ availability: "UNAVAILABLE",
+ coverage: { ...coverage, association: "PARTIAL" },
+ basis_currentness: {
+ state: "UNAVAILABLE",
+ reasons: ["ACCESS_REVOKED"],
+ execution_eligible: false,
+ },
+ }),
+ ),
+ );
const entry = await readHistoryEntry({ kind: "result", scope, operationId: "op-1" });
expect(entry.result).toBeNull();
expect(entry.availability).toBe("UNAVAILABLE");
diff --git a/apps/web/src/api/history.ts b/apps/web/src/api/history.ts
index 27fa3dd5..cbd7e229 100644
--- a/apps/web/src/api/history.ts
+++ b/apps/web/src/api/history.ts
@@ -1,47 +1,122 @@
import { UiError } from "../i18n/UiError";
import { t } from "../i18n/locale";
import { rpc } from "./rpcClient";
-import { historyDetailSchema, historyPageSchema, historicalResultSchema, type WireHistoryItem } from "./historySchemas";
-import type { HistoryEntry, HistoryKind, HistoryPage, HistoryRow, HistoryScope, HistorySelection, RecordReference } from "./historyModels";
+import {
+ historyDetailSchema,
+ historyPageSchema,
+ historicalResultSchema,
+ type WireHistoryItem,
+} from "./historySchemas";
+import type {
+ HistoryEntry,
+ HistoryKind,
+ HistoryPage,
+ HistoryRow,
+ HistoryScope,
+ HistorySelection,
+ RecordReference,
+} from "./historyModels";
export function wireScope(scope: HistoryScope) {
- return { project_id: scope.projectId, ...(scope.threadId ? { thread_id: scope.threadId } : {}),
- ...(scope.requestDigest ? { request_revision_digest: scope.requestDigest } : {}) };
+ return {
+ project_id: scope.projectId,
+ ...(scope.threadId ? { thread_id: scope.threadId } : {}),
+ ...(scope.requestDigest ? { request_revision_digest: scope.requestDigest } : {}),
+ };
}
export function wireRecord(record: RecordReference) {
- return { owner_kind: record.owner, project_id: record.projectId, immutable_id: record.id, revision_digest: record.digest };
+ return {
+ owner_kind: record.owner,
+ project_id: record.projectId,
+ immutable_id: record.id,
+ revision_digest: record.digest,
+ };
}
-async function query(method: string, input: Record, signal?: AbortSignal): Promise {
+async function query(
+ method: string,
+ input: Record,
+ signal?: AbortSignal,
+): Promise {
const response = await rpc(method, input, crypto.randomUUID(), signal);
if (response.state !== "SUCCEEDED") throw new UiError("api.historyIncomplete");
return response.value;
}
function rowOf(item: WireHistoryItem, scope: HistoryScope): HistoryRow {
if (item.record_ref.project_id !== scope.projectId) throw new UiError("api.historyProject");
- const record: RecordReference = { owner: item.record_ref.owner_kind, projectId: item.record_ref.project_id,
- id: item.record_ref.immutable_id, digest: item.record_ref.revision_digest,
- entityType: item.entity_type ?? undefined, entityId: item.entity_id ?? undefined };
+ const record: RecordReference = {
+ owner: item.record_ref.owner_kind,
+ projectId: item.record_ref.project_id,
+ id: item.record_ref.immutable_id,
+ digest: item.record_ref.revision_digest,
+ entityType: item.entity_type ?? undefined,
+ entityId: item.entity_id ?? undefined,
+ };
const origin = item.origin_request_revision_digest;
- const links = item.scope_links.filter(link => link.association === "PRODUCED_IN" && link.request_revision_digest === origin);
- const threads = new Set(links.map(link => link.thread_id));
- const thread = scope.threadId ? (threads.has(scope.threadId) ? scope.threadId : undefined)
- : (threads.size === 1 ? links[0].thread_id : undefined);
- const selection: HistorySelection = item.kind === "RESULT" && origin && thread
- ? { kind: "result", scope: { ...scope, threadId: thread, requestDigest: origin }, operationId: item.operation_id ?? undefined, resultDigest: item.record_ref.revision_digest, record,
- occurredAt: item.occurred_at, title: item.title }
- : { kind: "record", scope, record };
- return { id: item.item_id, kind: item.kind, title: item.title, occurredAt: item.occurred_at,
- association: item.association, membership: item.head_membership, availability: item.availability,
- currentness: item.currentness, selection, completion: item.completion, phase: item.phase,
- changeSummary: item.change_summary };
+ const links = item.scope_links.filter(
+ (link) => link.association === "PRODUCED_IN" && link.request_revision_digest === origin,
+ );
+ const threads = new Set(links.map((link) => link.thread_id));
+ const thread = scope.threadId
+ ? threads.has(scope.threadId)
+ ? scope.threadId
+ : undefined
+ : threads.size === 1
+ ? links[0].thread_id
+ : undefined;
+ const selection: HistorySelection =
+ item.kind === "RESULT" && origin && thread
+ ? {
+ kind: "result",
+ scope: { ...scope, threadId: thread, requestDigest: origin },
+ operationId: item.operation_id ?? undefined,
+ resultDigest: item.record_ref.revision_digest,
+ record,
+ occurredAt: item.occurred_at,
+ title: item.title,
+ }
+ : { kind: "record", scope, record };
+ return {
+ id: item.item_id,
+ kind: item.kind,
+ title: item.title,
+ occurredAt: item.occurred_at,
+ association: item.association,
+ membership: item.head_membership,
+ availability: item.availability,
+ currentness: item.currentness,
+ selection,
+ completion: item.completion,
+ phase: item.phase,
+ changeSummary: item.change_summary,
+ };
}
-export async function readHistoryPage(scope: HistoryScope, cursor: string | null, signal?: AbortSignal, kinds?: HistoryKind[]): Promise {
- const value = historyPageSchema.parse(await query("revision/timeline/read", {
- project_id: scope.projectId, scope: wireScope(scope), cursor, limit: 25, contract_version: 2, ...(kinds ? { kinds } : {}),
- }, signal));
- return { items: value.items.map(item => rowOf(item, scope)), nextCursor: value.next_cursor,
- coverage: value.coverage, actorScope: value.actor_scope_digest };
+export async function readHistoryPage(
+ scope: HistoryScope,
+ cursor: string | null,
+ signal?: AbortSignal,
+ kinds?: HistoryKind[],
+): Promise {
+ const value = historyPageSchema.parse(
+ await query(
+ "revision/timeline/read",
+ {
+ project_id: scope.projectId,
+ scope: wireScope(scope),
+ cursor,
+ limit: 25,
+ contract_version: 2,
+ ...(kinds ? { kinds } : {}),
+ },
+ signal,
+ ),
+ );
+ return {
+ items: value.items.map((item) => rowOf(item, scope)),
+ nextCursor: value.next_cursor,
+ coverage: value.coverage,
+ actorScope: value.actor_scope_digest,
+ };
}
function questionOf(request: Record): string | null {
@@ -51,16 +126,32 @@ function questionOf(request: Record): string | null {
return null;
}
-export async function readHistoryEntry(selection: HistorySelection, signal?: AbortSignal): Promise {
+export async function readHistoryEntry(
+ selection: HistorySelection,
+ signal?: AbortSignal,
+): Promise {
if (selection.kind === "result") {
const scope = selection.scope;
- const value = historicalResultSchema.parse(await query("thread/result/read", {
- project_id: scope.projectId, thread_id: scope.threadId, request_revision_digest: scope.requestDigest,
- ...(selection.resultDigest ? { result_revision_digest: selection.resultDigest } : {}), contract_version: 2,
- }, signal));
- if (value.project_id !== scope.projectId || value.thread_id !== scope.threadId || value.request_revision_digest !== scope.requestDigest
- || (selection.operationId && value.operation_id !== selection.operationId)
- || (selection.resultDigest && value.result_revision_digest !== selection.resultDigest)) {
+ const value = historicalResultSchema.parse(
+ await query(
+ "thread/result/read",
+ {
+ project_id: scope.projectId,
+ thread_id: scope.threadId,
+ request_revision_digest: scope.requestDigest,
+ ...(selection.resultDigest ? { result_revision_digest: selection.resultDigest } : {}),
+ contract_version: 2,
+ },
+ signal,
+ ),
+ );
+ if (
+ value.project_id !== scope.projectId ||
+ value.thread_id !== scope.threadId ||
+ value.request_revision_digest !== scope.requestDigest ||
+ (selection.operationId && value.operation_id !== selection.operationId) ||
+ (selection.resultDigest && value.result_revision_digest !== selection.resultDigest)
+ ) {
throw new UiError("api.historyResult");
}
const readable = value.availability !== "UNAVAILABLE";
@@ -68,34 +159,99 @@ export async function readHistoryEntry(selection: HistorySelection, signal?: Abo
// A missing current-head projection must not hide an otherwise readable past answer.
let comparisonEntry: HistoryEntry | null = null;
if (readable && selection.record && selection.record.digest === value.result_revision_digest) {
- try { comparisonEntry = await readHistoryEntry({ kind: "record", scope, record: selection.record }, signal); }
- catch (error) { if (signal?.aborted) throw error; }
+ try {
+ comparisonEntry = await readHistoryEntry(
+ { kind: "record", scope, record: selection.record },
+ signal,
+ );
+ } catch (error) {
+ if (signal?.aborted) throw error;
+ }
}
- return { selection, title: selection.title ?? t("api.historyPair"), occurredAt: selection.occurredAt ?? (typeof value.request.created_at === "string" ? value.request.created_at : null),
- kind: "RESULT", question: readable ? questionOf(value.request) : null, result: readable ? value.result : null, content: null,
- executionState: value.operation_state ?? null, resultPhase: typeof value.manifest?.completion === "string" ? value.manifest.completion : null,
- currentness: value.basis_currentness, availability: value.availability, reasons: value.coverage.reasons,
- currentHead: comparisonEntry?.currentHead ?? null, restoreSelection: comparisonEntry?.restoreSelection ?? null,
- capability: { restore: "READ_ONLY", applyReady: false, previewSupported: false, reasons: [] }, technical: readable ? value
- : { project_id: value.project_id, thread_id: value.thread_id, availability: value.availability, coverage: value.coverage, basis_currentness: value.basis_currentness } };
+ return {
+ selection,
+ title: selection.title ?? t("api.historyPair"),
+ occurredAt:
+ selection.occurredAt ??
+ (typeof value.request.created_at === "string" ? value.request.created_at : null),
+ kind: "RESULT",
+ question: readable ? questionOf(value.request) : null,
+ result: readable ? value.result : null,
+ content: null,
+ executionState: value.operation_state ?? null,
+ resultPhase:
+ typeof value.manifest?.completion === "string" ? value.manifest.completion : null,
+ currentness: value.basis_currentness,
+ availability: value.availability,
+ reasons: value.coverage.reasons,
+ currentHead: comparisonEntry?.currentHead ?? null,
+ restoreSelection: comparisonEntry?.restoreSelection ?? null,
+ capability: { restore: "READ_ONLY", applyReady: false, previewSupported: false, reasons: [] },
+ technical: readable
+ ? value
+ : {
+ project_id: value.project_id,
+ thread_id: value.thread_id,
+ availability: value.availability,
+ coverage: value.coverage,
+ basis_currentness: value.basis_currentness,
+ },
+ };
}
- const value = historyDetailSchema.parse(await query("revision/timeline/item/read", {
- project_id: selection.scope.projectId, scope: wireScope(selection.scope), record_ref: wireRecord(selection.record), contract_version: 2,
- }, signal));
+ const value = historyDetailSchema.parse(
+ await query(
+ "revision/timeline/item/read",
+ {
+ project_id: selection.scope.projectId,
+ scope: wireScope(selection.scope),
+ record_ref: wireRecord(selection.record),
+ contract_version: 2,
+ },
+ signal,
+ ),
+ );
const ref = value.item.record_ref;
- if (ref.project_id !== selection.scope.projectId || ref.project_id !== selection.record.projectId || ref.owner_kind !== selection.record.owner
- || ref.immutable_id !== selection.record.id || ref.revision_digest !== selection.record.digest) {
+ if (
+ ref.project_id !== selection.scope.projectId ||
+ ref.project_id !== selection.record.projectId ||
+ ref.owner_kind !== selection.record.owner ||
+ ref.immutable_id !== selection.record.id ||
+ ref.revision_digest !== selection.record.digest
+ ) {
throw new UiError("api.historySelection");
}
const { item } = value;
const head = value.current_head_digest ?? null;
- const restoreSelection = ref.owner_kind === "SEMANTIC_REVISION" && item.entity_type && item.entity_id && head
- ? { projectId: ref.project_id, entityType: item.entity_type, entityId: item.entity_id, targetDigest: ref.revision_digest, expectedHead: head } : null;
- return { selection, title: item.title, occurredAt: item.occurred_at, kind: item.kind,
- question: item.kind === "REQUEST" && value.content ? questionOf(value.content) : null, result: null,
- content: item.availability === "UNAVAILABLE" ? null : value.content, currentness: item.currentness,
- availability: item.availability, reasons: value.coverage.reasons, currentHead: head, restoreSelection,
+ const restoreSelection =
+ ref.owner_kind === "SEMANTIC_REVISION" && item.entity_type && item.entity_id && head
+ ? {
+ projectId: ref.project_id,
+ entityType: item.entity_type,
+ entityId: item.entity_id,
+ targetDigest: ref.revision_digest,
+ expectedHead: head,
+ }
+ : null;
+ return {
+ selection,
+ title: item.title,
+ occurredAt: item.occurred_at,
+ kind: item.kind,
+ question: item.kind === "REQUEST" && value.content ? questionOf(value.content) : null,
+ result: null,
+ content: item.availability === "UNAVAILABLE" ? null : value.content,
+ currentness: item.currentness,
+ availability: item.availability,
+ reasons: value.coverage.reasons,
+ currentHead: head,
+ restoreSelection,
changeSummary: item.change_summary,
- capability: { restore: item.capability.restore, applyReady: item.capability.apply_ready, previewSupported: item.capability.preview_supported, reasons: item.capability.reason_codes },
- technical: item.availability === "UNAVAILABLE" ? { ...value, content: null } : value };
+ capability: {
+ restore: item.capability.restore,
+ applyReady: item.capability.apply_ready,
+ previewSupported: item.capability.preview_supported,
+ reasons: item.capability.reason_codes,
+ },
+ technical: item.availability === "UNAVAILABLE" ? { ...value, content: null } : value,
+ };
}
diff --git a/apps/web/src/api/historyComparison.ts b/apps/web/src/api/historyComparison.ts
index a29a3eab..999864de 100644
--- a/apps/web/src/api/historyComparison.ts
+++ b/apps/web/src/api/historyComparison.ts
@@ -2,28 +2,76 @@ import { UiError } from "../i18n/UiError";
import { t } from "../i18n/locale";
import { z } from "zod";
import { rpc } from "./rpcClient";
-import { changeGroupSchema, coverageSchema, diffEntrySchema, digestSchema, type WireChange, type WireChangeGroup } from "./historySchemas";
+import {
+ changeGroupSchema,
+ coverageSchema,
+ diffEntrySchema,
+ digestSchema,
+ type WireChange,
+ type WireChangeGroup,
+} from "./historySchemas";
import type { RevisionComparison, RestoreSelection } from "./historyModels";
-const comparisonSchema = z.object({ contract_version: z.literal(2), from_revision_digest: digestSchema,
- to_revision_digest: digestSchema, diff: z.array(diffEntrySchema), summary_groups: z.array(changeGroupSchema), coverage: coverageSchema });
+const comparisonSchema = z.object({
+ contract_version: z.literal(2),
+ from_revision_digest: digestSchema,
+ to_revision_digest: digestSchema,
+ diff: z.array(diffEntrySchema),
+ summary_groups: z.array(changeGroupSchema),
+ coverage: coverageSchema,
+});
-export function mapComparison(changes: WireChange[], groups: WireChangeGroup[], coverage = "COMPLETE", reasons: string[] = []): RevisionComparison {
- return { coverage, reasons, changes: changes.map(change => {
- const group = groups.find(item => item.trace_paths.includes(change.path));
- return { path: change.path, before: change.before, after: change.after,
- beforeMissing: !change.before_present, afterMissing: !change.after_present,
- group: group?.kind ?? "OTHER", label: group?.summary ?? t("revision.content") };
- }) };
+export function mapComparison(
+ changes: WireChange[],
+ groups: WireChangeGroup[],
+ coverage = "COMPLETE",
+ reasons: string[] = [],
+): RevisionComparison {
+ return {
+ coverage,
+ reasons,
+ changes: changes.map((change) => {
+ const group = groups.find((item) => item.trace_paths.includes(change.path));
+ return {
+ path: change.path,
+ before: change.before,
+ after: change.after,
+ beforeMissing: !change.before_present,
+ afterMissing: !change.after_present,
+ group: group?.kind ?? "OTHER",
+ label: group?.summary ?? t("revision.content"),
+ };
+ }),
+ };
}
-export async function compareHistory(selection: RestoreSelection, signal?: AbortSignal): Promise {
- const response = await rpc("revision/diff/read", { project_id: selection.projectId,
- from_revision_digest: selection.targetDigest, to_revision_digest: selection.expectedHead, contract_version: 2 }, crypto.randomUUID(), signal);
+export async function compareHistory(
+ selection: RestoreSelection,
+ signal?: AbortSignal,
+): Promise {
+ const response = await rpc(
+ "revision/diff/read",
+ {
+ project_id: selection.projectId,
+ from_revision_digest: selection.targetDigest,
+ to_revision_digest: selection.expectedHead,
+ contract_version: 2,
+ },
+ crypto.randomUUID(),
+ signal,
+ );
if (response.state !== "SUCCEEDED") throw new UiError("comparison.incomplete");
const value = comparisonSchema.parse(response.value);
- if (value.from_revision_digest !== selection.targetDigest || value.to_revision_digest !== selection.expectedHead) {
+ if (
+ value.from_revision_digest !== selection.targetDigest ||
+ value.to_revision_digest !== selection.expectedHead
+ ) {
throw new UiError("comparison.binding");
}
- return mapComparison(value.diff, value.summary_groups, value.coverage.association === "EXACT" ? "COMPLETE" : "PARTIAL", value.coverage.reasons);
+ return mapComparison(
+ value.diff,
+ value.summary_groups,
+ value.coverage.association === "EXACT" ? "COMPLETE" : "PARTIAL",
+ value.coverage.reasons,
+ );
}
diff --git a/apps/web/src/api/historyHttp.test.ts b/apps/web/src/api/historyHttp.test.ts
index c9efddaf..7da3b9e5 100644
--- a/apps/web/src/api/historyHttp.test.ts
+++ b/apps/web/src/api/historyHttp.test.ts
@@ -9,40 +9,73 @@ const origin = process.env.THOTH_HISTORY_HTTP_ORIGIN;
const descriptorPath = process.env.THOTH_HISTORY_HTTP_DESCRIPTOR;
afterEach(() => vi.unstubAllGlobals());
-it.runIf(Boolean(origin && descriptorPath))("reads exact records, diff and blocked apply preview from the isolated Python HTTP server", async () => {
- const target = new URL(origin!);
- if (!["127.0.0.1", "localhost", "[::1]"].includes(target.hostname)) throw new Error("This contract check only accepts an explicitly selected loopback fixture.");
- const descriptor = JSON.parse(readFileSync(descriptorPath!, "utf8"));
- if (descriptor.model !== "CONTROLLED_TEST_DOUBLE" || descriptor.apply_ready !== false) throw new Error("Expected the read-only controlled fixture descriptor.");
- const calls: string[] = [];
- const nativeFetch = globalThis.fetch;
- vi.stubGlobal("fetch", async (url: RequestInfo | URL, init?: RequestInit) => {
- if (String(url) !== "/rpc/query") throw new Error("HTTP verification forbids journaling and mutation transports.");
- calls.push(JSON.parse(String(init?.body)).method);
- return nativeFetch(new URL(String(url), target), init);
- });
- const scope = { projectId: descriptor.project_id as string, threadId: descriptor.thread_id as string };
- const before = await rpc>("thread/read", { project_id: scope.projectId, thread_id: scope.threadId }, crypto.randomUUID());
- let page = await readHistoryPage(scope, null);
- let item = page.items.find(row => row.selection.kind === "record" && row.selection.record.digest === descriptor.selection.target_revision_digest);
- for (let index = 0; !item && page.nextCursor && index < 3; index += 1) {
- page = await readHistoryPage(scope, page.nextCursor);
- item = page.items.find(row => row.selection.kind === "record" && row.selection.record.digest === descriptor.selection.target_revision_digest);
- }
- if (!item) throw new Error("The fixture's historical hypothesis is not present in the authorized timeline pages.");
- const entry = await readHistoryEntry(item.selection);
- expect(entry.restoreSelection?.expectedHead).toBe(descriptor.selection.expected_current_head);
- expect(entry.capability.previewSupported).toBe(true);
- expect(entry.content).not.toBeNull();
- const comparison = await compareHistory(entry.restoreSelection!);
- expect(comparison.changes.some(change => change.path.includes("statement") && change.before !== change.after)).toBe(true);
- const preview = await readRestorePreview(entry.restoreSelection!);
- expect(preview.principalScope).toBe(page.actorScope);
- expect(preview.applyReady).toBe(false);
- expect(preview.selection.targetDigest).toBe(descriptor.selection.target_revision_digest);
- const after = await rpc>("thread/read", { project_id: scope.projectId, thread_id: scope.threadId }, crypto.randomUUID());
- expect(Array.isArray(before.value.model_dispatches)).toBe(true);
- expect(after.value.model_dispatches).toEqual(before.value.model_dispatches);
- expect(calls).not.toContain("revision/restore/apply");
- expect(calls).not.toContain("thread/input");
-}, 30000);
+it.runIf(Boolean(origin && descriptorPath))(
+ "reads exact records, diff and blocked apply preview from the isolated Python HTTP server",
+ async () => {
+ const target = new URL(origin!);
+ if (!["127.0.0.1", "localhost", "[::1]"].includes(target.hostname))
+ throw new Error("This contract check only accepts an explicitly selected loopback fixture.");
+ const descriptor = JSON.parse(readFileSync(descriptorPath!, "utf8"));
+ if (descriptor.model !== "CONTROLLED_TEST_DOUBLE" || descriptor.apply_ready !== false)
+ throw new Error("Expected the read-only controlled fixture descriptor.");
+ const calls: string[] = [];
+ const nativeFetch = globalThis.fetch;
+ vi.stubGlobal("fetch", async (url: RequestInfo | URL, init?: RequestInit) => {
+ if (String(url) !== "/rpc/query")
+ throw new Error("HTTP verification forbids journaling and mutation transports.");
+ calls.push(JSON.parse(String(init?.body)).method);
+ return nativeFetch(new URL(String(url), target), init);
+ });
+ const scope = {
+ projectId: descriptor.project_id as string,
+ threadId: descriptor.thread_id as string,
+ };
+ const before = await rpc>(
+ "thread/read",
+ { project_id: scope.projectId, thread_id: scope.threadId },
+ crypto.randomUUID(),
+ );
+ let page = await readHistoryPage(scope, null);
+ let item = page.items.find(
+ (row) =>
+ row.selection.kind === "record" &&
+ row.selection.record.digest === descriptor.selection.target_revision_digest,
+ );
+ for (let index = 0; !item && page.nextCursor && index < 3; index += 1) {
+ page = await readHistoryPage(scope, page.nextCursor);
+ item = page.items.find(
+ (row) =>
+ row.selection.kind === "record" &&
+ row.selection.record.digest === descriptor.selection.target_revision_digest,
+ );
+ }
+ if (!item)
+ throw new Error(
+ "The fixture's historical hypothesis is not present in the authorized timeline pages.",
+ );
+ const entry = await readHistoryEntry(item.selection);
+ expect(entry.restoreSelection?.expectedHead).toBe(descriptor.selection.expected_current_head);
+ expect(entry.capability.previewSupported).toBe(true);
+ expect(entry.content).not.toBeNull();
+ const comparison = await compareHistory(entry.restoreSelection!);
+ expect(
+ comparison.changes.some(
+ (change) => change.path.includes("statement") && change.before !== change.after,
+ ),
+ ).toBe(true);
+ const preview = await readRestorePreview(entry.restoreSelection!);
+ expect(preview.principalScope).toBe(page.actorScope);
+ expect(preview.applyReady).toBe(false);
+ expect(preview.selection.targetDigest).toBe(descriptor.selection.target_revision_digest);
+ const after = await rpc>(
+ "thread/read",
+ { project_id: scope.projectId, thread_id: scope.threadId },
+ crypto.randomUUID(),
+ );
+ expect(Array.isArray(before.value.model_dispatches)).toBe(true);
+ expect(after.value.model_dispatches).toEqual(before.value.model_dispatches);
+ expect(calls).not.toContain("revision/restore/apply");
+ expect(calls).not.toContain("thread/input");
+ },
+ 30000,
+);
diff --git a/apps/web/src/api/historyModels.ts b/apps/web/src/api/historyModels.ts
index 8a4221a1..79afc4b6 100644
--- a/apps/web/src/api/historyModels.ts
+++ b/apps/web/src/api/historyModels.ts
@@ -10,7 +10,15 @@ export type RecordReference = {
entityId?: string;
};
export type HistorySelection =
- | { kind: "result"; scope: HistoryScope & { threadId: string; requestDigest: string }; operationId?: string; resultDigest?: string; record?: RecordReference; occurredAt?: string; title?: string }
+ | {
+ kind: "result";
+ scope: HistoryScope & { threadId: string; requestDigest: string };
+ operationId?: string;
+ resultDigest?: string;
+ record?: RecordReference;
+ occurredAt?: string;
+ title?: string;
+ }
| { kind: "record"; scope: HistoryScope; record: RecordReference };
export type Currentness = { state: string; reasons: string[] };
export type ChangeSummary = {
@@ -63,7 +71,12 @@ export type HistoryEntry = {
availability: string;
reasons: string[];
currentHead: string | null;
- capability: { restore: string; applyReady: boolean; previewSupported: boolean; reasons: string[] };
+ capability: {
+ restore: string;
+ applyReady: boolean;
+ previewSupported: boolean;
+ reasons: string[];
+ };
restoreSelection: RestoreSelection | null;
changeSummary?: ChangeSummary | null;
technical: unknown;
@@ -101,11 +114,35 @@ export type RestoreResult = {
};
export function selectionKey(selection: HistorySelection): string {
- return JSON.stringify(selection.kind === "result"
- ? [selection.kind, selection.scope.projectId, selection.scope.threadId, selection.scope.requestDigest, selection.operationId, selection.resultDigest, selection.record?.id]
- : [selection.kind, selection.scope.projectId, selection.scope.threadId, selection.scope.requestDigest, selection.record.owner, selection.record.id, selection.record.digest]);
+ return JSON.stringify(
+ selection.kind === "result"
+ ? [
+ selection.kind,
+ selection.scope.projectId,
+ selection.scope.threadId,
+ selection.scope.requestDigest,
+ selection.operationId,
+ selection.resultDigest,
+ selection.record?.id,
+ ]
+ : [
+ selection.kind,
+ selection.scope.projectId,
+ selection.scope.threadId,
+ selection.scope.requestDigest,
+ selection.record.owner,
+ selection.record.id,
+ selection.record.digest,
+ ],
+ );
}
export function restoreKey(selection: RestoreSelection): string {
- return JSON.stringify([selection.projectId, selection.entityType, selection.entityId, selection.targetDigest, selection.expectedHead]);
+ return JSON.stringify([
+ selection.projectId,
+ selection.entityType,
+ selection.entityId,
+ selection.targetDigest,
+ selection.expectedHead,
+ ]);
}
diff --git a/apps/web/src/api/historySchemas.ts b/apps/web/src/api/historySchemas.ts
index eef4c3ce..8b30bbcb 100644
--- a/apps/web/src/api/historySchemas.ts
+++ b/apps/web/src/api/historySchemas.ts
@@ -4,64 +4,113 @@ export const digestSchema = z.string().regex(/^[a-f0-9]{64}$/i);
export const contentSchema = z.record(z.unknown());
export const currentnessSchema = z.object({
state: z.enum(["CURRENT", "REVIEW_REQUIRED", "INVALIDATED", "UNKNOWN_BASIS", "UNAVAILABLE"]),
- reasons: z.array(z.string()), execution_eligible: z.boolean(),
+ reasons: z.array(z.string()),
+ execution_eligible: z.boolean(),
});
export const capabilitySchema = z.object({
restore: z.enum(["RESTORE_SUPPORTED", "READ_ONLY", "UNSUPPORTED"]),
- apply_ready: z.boolean(), preview_supported: z.boolean().optional().default(false), reason_codes: z.array(z.string()),
+ apply_ready: z.boolean(),
+ preview_supported: z.boolean().optional().default(false),
+ reason_codes: z.array(z.string()),
});
export const coverageSchema = z.object({
- visibility: z.literal("AUTHORIZED_SUBSET"), association: z.enum(["EXACT", "PARTIAL", "UNRESOLVED"]),
- scan: z.enum(["COMPLETE_PAGE", "CONTINUATION", "LIMITED"]), reasons: z.array(z.string()),
+ visibility: z.literal("AUTHORIZED_SUBSET"),
+ association: z.enum(["EXACT", "PARTIAL", "UNRESOLVED"]),
+ scan: z.enum(["COMPLETE_PAGE", "CONTINUATION", "LIMITED"]),
+ reasons: z.array(z.string()),
});
export const recordRefSchema = z.object({
- owner_kind: z.enum(["SEMANTIC_REVISION", "FULL_MEMORY"]), project_id: z.string(),
- immutable_id: z.string(), revision_digest: digestSchema,
+ owner_kind: z.enum(["SEMANTIC_REVISION", "FULL_MEMORY"]),
+ project_id: z.string(),
+ immutable_id: z.string(),
+ revision_digest: digestSchema,
});
export const changeSummarySchema = z.object({
- lines: z.array(z.object({ label: z.string(), before: z.string(), after: z.string() })), more: z.number().int().min(0).default(0),
- flags: z.array(z.enum(["NUMBER", "NEGATION", "STATUS", "CITATION", "AUTHORITY", "TIME"])).default([]), text_diff_recommended: z.boolean().default(false),
- other: z.number().int().min(0).default(0), technical: z.array(z.object({ label: z.string(), before: z.string(), after: z.string() })).default([]),
+ lines: z.array(z.object({ label: z.string(), before: z.string(), after: z.string() })),
+ more: z.number().int().min(0).default(0),
+ flags: z
+ .array(z.enum(["NUMBER", "NEGATION", "STATUS", "CITATION", "AUTHORITY", "TIME"]))
+ .default([]),
+ text_diff_recommended: z.boolean().default(false),
+ other: z.number().int().min(0).default(0),
+ technical: z
+ .array(z.object({ label: z.string(), before: z.string(), after: z.string() }))
+ .default([]),
});
export const historyItemSchema = z.object({
- item_id: z.string(), kind: z.enum(["REQUEST", "RESULT", "REVISION", "RESTORE", "MEMORY"]),
- occurred_at: z.string().datetime({ offset: true }), record_ref: recordRefSchema,
- operation_id: z.string().nullable().optional(), origin_request_revision_digest: digestSchema.nullable().optional(),
- scope_links: z.array(z.object({ thread_id: z.string(), request_revision_digest: digestSchema,
- association: z.enum(["PRODUCED_IN", "USED_BY", "RELATED_OBJECT"]) })),
+ item_id: z.string(),
+ kind: z.enum(["REQUEST", "RESULT", "REVISION", "RESTORE", "MEMORY"]),
+ occurred_at: z.string().datetime({ offset: true }),
+ record_ref: recordRefSchema,
+ operation_id: z.string().nullable().optional(),
+ origin_request_revision_digest: digestSchema.nullable().optional(),
+ scope_links: z.array(
+ z.object({
+ thread_id: z.string(),
+ request_revision_digest: digestSchema,
+ association: z.enum(["PRODUCED_IN", "USED_BY", "RELATED_OBJECT"]),
+ }),
+ ),
association: z.enum(["PRODUCED_IN", "USED_BY", "RELATED_OBJECT", "UNATTRIBUTED"]),
- head_membership: z.enum(["CURRENT", "ANCESTOR", "BRANCH", "UNKNOWN"]), title: z.string(),
+ head_membership: z.enum(["CURRENT", "ANCESTOR", "BRANCH", "UNKNOWN"]),
+ title: z.string(),
availability: z.enum(["AVAILABLE", "PARTIAL", "UNAVAILABLE"]),
- entity_type: z.string().nullable().optional(), entity_id: z.string().nullable().optional(),
- schema_family: z.string().nullable().optional(), currentness: currentnessSchema, capability: capabilitySchema,
- completion: z.enum(["CHECKPOINT", "TERMINAL"]).nullable().optional(), phase: z.string().nullable().optional(),
+ entity_type: z.string().nullable().optional(),
+ entity_id: z.string().nullable().optional(),
+ schema_family: z.string().nullable().optional(),
+ currentness: currentnessSchema,
+ capability: capabilitySchema,
+ completion: z.enum(["CHECKPOINT", "TERMINAL"]).nullable().optional(),
+ phase: z.string().nullable().optional(),
change_summary: changeSummarySchema.nullable().optional(),
});
export const historyPageSchema = z.object({
- contract_version: z.literal(2), items: z.array(historyItemSchema), coverage: coverageSchema,
- next_cursor: z.string().nullable(), actor_scope_digest: digestSchema, capability: capabilitySchema,
+ contract_version: z.literal(2),
+ items: z.array(historyItemSchema),
+ coverage: coverageSchema,
+ next_cursor: z.string().nullable(),
+ actor_scope_digest: digestSchema,
+ capability: capabilitySchema,
});
export const historyDetailSchema = z.object({
- contract_version: z.literal(2), item: historyItemSchema, content: contentSchema.nullable(), coverage: coverageSchema,
+ contract_version: z.literal(2),
+ item: historyItemSchema,
+ content: contentSchema.nullable(),
+ coverage: coverageSchema,
current_head_digest: digestSchema.nullable().optional(),
});
export const historicalResultSchema = z.object({
- contract_version: z.literal(2), project_id: z.string(), thread_id: z.string(), request_revision_digest: digestSchema,
- operation_id: z.string(), result_revision_digest: digestSchema.nullable().optional(), request: contentSchema,
- manifest: contentSchema.nullable(), result: contentSchema.nullable(), error: contentSchema.nullable(),
- operation_state: z.string().nullable().optional(), availability: z.enum(["AVAILABLE", "PARTIAL", "UNAVAILABLE"]),
- basis_currentness: currentnessSchema, coverage: coverageSchema,
+ contract_version: z.literal(2),
+ project_id: z.string(),
+ thread_id: z.string(),
+ request_revision_digest: digestSchema,
+ operation_id: z.string(),
+ result_revision_digest: digestSchema.nullable().optional(),
+ request: contentSchema,
+ manifest: contentSchema.nullable(),
+ result: contentSchema.nullable(),
+ error: contentSchema.nullable(),
+ operation_state: z.string().nullable().optional(),
+ availability: z.enum(["AVAILABLE", "PARTIAL", "UNAVAILABLE"]),
+ basis_currentness: currentnessSchema,
+ coverage: coverageSchema,
// thread/result/read: how the server's read view of the result relates to the stored manifest. Absent from an older server.
expected_result_view: z.string().nullable().optional(),
shown_result: contentSchema.nullable().optional(),
projected_keys: z.array(z.string()).nullable().optional(),
});
export const diffEntrySchema = z.object({
- path: z.string(), before_present: z.boolean(), after_present: z.boolean(), before: z.unknown(), after: z.unknown(),
+ path: z.string(),
+ before_present: z.boolean(),
+ after_present: z.boolean(),
+ before: z.unknown(),
+ after: z.unknown(),
});
export const changeGroupSchema = z.object({
kind: z.enum(["CONTENT", "EVIDENCE", "CONDITION", "STATUS", "IMPACT", "OTHER"]),
- trace_paths: z.array(z.string()), changes: z.array(diffEntrySchema), summary: z.string(),
+ trace_paths: z.array(z.string()),
+ changes: z.array(diffEntrySchema),
+ summary: z.string(),
});
export type WireHistoryItem = z.infer;
export type WireChange = z.infer;
diff --git a/apps/web/src/api/historyServerContract.test.ts b/apps/web/src/api/historyServerContract.test.ts
index 5e4de9a2..d5969751 100644
--- a/apps/web/src/api/historyServerContract.test.ts
+++ b/apps/web/src/api/historyServerContract.test.ts
@@ -7,27 +7,86 @@ import { createHistoryFixtureTransport, historyFixtureIds as ids } from "./histo
it("matches the actual Python request and response DTOs without creating a runtime", async () => {
const fixture = createHistoryFixtureTransport({ applyReady: true });
- const selection = { project_id: ids.project, entity_type: "HYPOTHESIS", entity_id: "hypothesis:example-1",
- target_revision_digest: ids.old, expected_current_head: ids.current };
+ const selection = {
+ project_id: ids.project,
+ entity_type: "HYPOTHESIS",
+ entity_id: "hypothesis:example-1",
+ target_revision_digest: ids.old,
+ expected_current_head: ids.current,
+ };
const scope = { project_id: ids.project, thread_id: ids.thread };
const cases = [
- { method: "revision/timeline/read", input: { project_id: ids.project, scope, cursor: null, limit: 25, contract_version: 2 } },
- { method: "revision/timeline/item/read", input: { project_id: ids.project, scope, contract_version: 2,
- record_ref: { owner_kind: "SEMANTIC_REVISION", project_id: ids.project, immutable_id: "revision:aaaaaaaa", revision_digest: ids.old } } },
- { method: "thread/result/read", input: { project_id: ids.project, thread_id: ids.thread, request_revision_digest: ids.request1, contract_version: 2 } },
- { method: "revision/diff/read", input: { project_id: ids.project, from_revision_digest: ids.old, to_revision_digest: ids.current, contract_version: 2 } },
- { method: "revision/restore/preview", input: { project_id: ids.project, selection, contract_version: 2 } },
- { method: "revision/restore/apply", input: { project_id: ids.project, selection, preview_basis_digest: ids.basis, reason: "contract fixture", contract_version: 2 } },
+ {
+ method: "revision/timeline/read",
+ input: { project_id: ids.project, scope, cursor: null, limit: 25, contract_version: 2 },
+ },
+ {
+ method: "revision/timeline/item/read",
+ input: {
+ project_id: ids.project,
+ scope,
+ contract_version: 2,
+ record_ref: {
+ owner_kind: "SEMANTIC_REVISION",
+ project_id: ids.project,
+ immutable_id: "revision:aaaaaaaa",
+ revision_digest: ids.old,
+ },
+ },
+ },
+ {
+ method: "thread/result/read",
+ input: {
+ project_id: ids.project,
+ thread_id: ids.thread,
+ request_revision_digest: ids.request1,
+ contract_version: 2,
+ },
+ },
+ {
+ method: "revision/diff/read",
+ input: {
+ project_id: ids.project,
+ from_revision_digest: ids.old,
+ to_revision_digest: ids.current,
+ contract_version: 2,
+ },
+ },
+ {
+ method: "revision/restore/preview",
+ input: { project_id: ids.project, selection, contract_version: 2 },
+ },
+ {
+ method: "revision/restore/apply",
+ input: {
+ project_id: ids.project,
+ selection,
+ preview_basis_digest: ids.basis,
+ reason: "contract fixture",
+ contract_version: 2,
+ },
+ },
];
const captured = [];
for (const item of cases) {
- const response = await fixture.fetch("/fixture-only", { method: "POST", body: JSON.stringify({ jsonrpc: "2.0", id: "fixture", method: item.method,
- params: { input: item.input, _meta: { idempotencyKey: item.method } } }) });
+ const response = await fixture.fetch("/fixture-only", {
+ method: "POST",
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ id: "fixture",
+ method: item.method,
+ params: { input: item.input, _meta: { idempotencyKey: item.method } },
+ }),
+ });
captured.push({ ...item, value: (await response.json()).result.value });
}
const repository = fileURLToPath(new URL("../../../../", import.meta.url));
- const python = [join(repository, ".venv", "Scripts", "python.exe"), join(repository, ".venv", "bin", "python")].find(existsSync);
- if (!python) throw new Error("Web wire contract validation requires the repository Python environment.");
+ const python = [
+ join(repository, ".venv", "Scripts", "python.exe"),
+ join(repository, ".venv", "bin", "python"),
+ ].find(existsSync);
+ if (!python)
+ throw new Error("Web wire contract validation requires the repository Python environment.");
const source = [
"import json, sys",
"from thoth.domain.research_history import HistoryTimelineInput, HistoryPage, HistoryItemInput, HistoryDetail, HistoricalResultInput, HistoricalResultView, HistoryDiffInput, HistoryDiff",
@@ -39,7 +98,15 @@ it("matches the actual Python request and response DTOs without creating a runti
" request.model_validate(item['input']); response.model_validate(item['value'])",
"print(json.dumps({'validated_methods':[item['method'] for item in items]}))",
].join("\n");
- const result = JSON.parse(execFileSync(python, ["-c", source], { shell: false, encoding: "utf8", input: JSON.stringify(captured),
- cwd: repository, timeout: 20000, env: { ...process.env, PYTHONPATH: join(repository, "src"), PYTHONDONTWRITEBYTECODE: "1" } }));
- expect(result.validated_methods).toEqual(cases.map(item => item.method));
+ const result = JSON.parse(
+ execFileSync(python, ["-c", source], {
+ shell: false,
+ encoding: "utf8",
+ input: JSON.stringify(captured),
+ cwd: repository,
+ timeout: 20000,
+ env: { ...process.env, PYTHONPATH: join(repository, "src"), PYTHONDONTWRITEBYTECODE: "1" },
+ }),
+ );
+ expect(result.validated_methods).toEqual(cases.map((item) => item.method));
});
diff --git a/apps/web/src/api/historyTestFixture.ts b/apps/web/src/api/historyTestFixture.ts
index 3391a32a..49814d6b 100644
--- a/apps/web/src/api/historyTestFixture.ts
+++ b/apps/web/src/api/historyTestFixture.ts
@@ -1,10 +1,26 @@
/** Explicit synthetic wire fixture, used only by tests and the DEV history example. */
-export const historyFixtureIds = { project: "history-example", thread: "session-example", actor: "e".repeat(64),
- request1: "1".repeat(64), result1: "2".repeat(64), request2: "3".repeat(64), result2: "4".repeat(64),
- old: "a".repeat(64), current: "b".repeat(64), restored: "c".repeat(64), basis: "d".repeat(64) };
+export const historyFixtureIds = {
+ project: "history-example",
+ thread: "session-example",
+ actor: "e".repeat(64),
+ request1: "1".repeat(64),
+ result1: "2".repeat(64),
+ request2: "3".repeat(64),
+ result2: "4".repeat(64),
+ old: "a".repeat(64),
+ current: "b".repeat(64),
+ restored: "c".repeat(64),
+ basis: "d".repeat(64),
+};
type FixtureInput = Record;
-type FixtureRequest = { method: string; id: string; params: { input: FixtureInput; _meta: { idempotencyKey: string } } };
-export function createHistoryFixtureTransport(options: { applyReady?: boolean; loseApplyResponse?: boolean } = {}) {
+type FixtureRequest = {
+ method: string;
+ id: string;
+ params: { input: FixtureInput; _meta: { idempotencyKey: string } };
+};
+export function createHistoryFixtureTransport(
+ options: { applyReady?: boolean; loseApplyResponse?: boolean } = {},
+) {
const ids = historyFixtureIds;
const calls: { method: string; input: FixtureInput; key: string; url: string }[] = [];
let head = ids.current;
@@ -12,98 +28,331 @@ export function createHistoryFixtureTransport(options: { applyReady?: boolean; l
let appliedCount = 0;
const completed = new Map();
const ready = options.applyReady ?? false;
- const capability = { restore: ready ? "RESTORE_SUPPORTED" : "READ_ONLY", preview_supported: true, apply_ready: ready, reason_codes: ready ? [] : ["RESTORE_APPLY_NOT_READY"] };
- const coverage = { visibility: "AUTHORIZED_SUBSET", association: "EXACT", scan: "COMPLETE_PAGE", reasons: [] };
+ const capability = {
+ restore: ready ? "RESTORE_SUPPORTED" : "READ_ONLY",
+ preview_supported: true,
+ apply_ready: ready,
+ reason_codes: ready ? [] : ["RESTORE_APPLY_NOT_READY"],
+ };
+ const coverage = {
+ visibility: "AUTHORIZED_SUBSET",
+ association: "EXACT",
+ scan: "COMPLETE_PAGE",
+ reasons: [],
+ };
const currentness = (state = "CURRENT") => ({ state, reasons: [], execution_eligible: false });
- const titles: Record = { [ids.old]: "연결 자료에서 초기 가설을 정리했습니다", [ids.current]: "추가 근거에 맞춰 가설의 적용 조건을 좁혔습니다", [ids.restored]: "과거 가설 내용을 새 버전으로 적용했습니다" };
- const content = (digest: string) => ({ statement: digest === ids.current
- ? "현재 연결 자료만으로 원인을 확정할 수 없어 추가 비교가 필요합니다."
- : "관측 조건의 차이가 결과에 영향을 주었을 가능성을 검토합니다.",
- uncertainty: "아직 독립적인 검증 결과가 없습니다.", counterevidence_queries: ["같은 조건에서도 차이가 유지되는가?"] });
- const ref = (digest: string) => ({ owner_kind: "SEMANTIC_REVISION", project_id: ids.project,
- immutable_id: `revision:${digest.slice(0, 8)}`, revision_digest: digest });
- const revisionItem = (digest: string, hour: string) => ({ item_id: `hypothesis:${digest}`, kind: digest === ids.restored ? "RESTORE" : "REVISION",
- occurred_at: `2026-09-21T${hour}:00+09:00`, record_ref: ref(digest), operation_id: null,
- origin_request_revision_digest: ids.request1, scope_links: [{ thread_id: ids.thread, request_revision_digest: ids.request1, association: "PRODUCED_IN" }],
- association: "PRODUCED_IN", head_membership: head === digest ? "CURRENT" : "ANCESTOR", title: titles[digest], availability: "AVAILABLE",
- entity_type: "HYPOTHESIS", entity_id: "hypothesis:example-1", schema_family: "HypothesisRecord",
- currentness: currentness(head === ids.restored ? "REVIEW_REQUIRED" : head === digest ? "CURRENT" : "REVIEW_REQUIRED"), capability });
- const resultItem = (second: boolean) => ({ ...revisionItem(second ? ids.current : ids.old, second ? "14:35" : "14:02"),
- item_id: second ? "result-2" : "result-1", kind: "RESULT", title: second ? "추가 질문에 대한 답변" : "첫 질문에 대한 답변",
- record_ref: ref(second ? ids.result2 : ids.result1), origin_request_revision_digest: second ? ids.request2 : ids.request1,
- operation_id: second ? "operation-2" : "operation-1", entity_type: "DECISION_OBJECT", entity_id: "result:example",
- scope_links: [{ thread_id: ids.thread, request_revision_digest: second ? ids.request2 : ids.request1, association: "PRODUCED_IN" }],
+ const titles: Record = {
+ [ids.old]: "연결 자료에서 초기 가설을 정리했습니다",
+ [ids.current]: "추가 근거에 맞춰 가설의 적용 조건을 좁혔습니다",
+ [ids.restored]: "과거 가설 내용을 새 버전으로 적용했습니다",
+ };
+ const content = (digest: string) => ({
+ statement:
+ digest === ids.current
+ ? "현재 연결 자료만으로 원인을 확정할 수 없어 추가 비교가 필요합니다."
+ : "관측 조건의 차이가 결과에 영향을 주었을 가능성을 검토합니다.",
+ uncertainty: "아직 독립적인 검증 결과가 없습니다.",
+ counterevidence_queries: ["같은 조건에서도 차이가 유지되는가?"],
+ });
+ const ref = (digest: string) => ({
+ owner_kind: "SEMANTIC_REVISION",
+ project_id: ids.project,
+ immutable_id: `revision:${digest.slice(0, 8)}`,
+ revision_digest: digest,
+ });
+ const revisionItem = (digest: string, hour: string) => ({
+ item_id: `hypothesis:${digest}`,
+ kind: digest === ids.restored ? "RESTORE" : "REVISION",
+ occurred_at: `2026-09-21T${hour}:00+09:00`,
+ record_ref: ref(digest),
+ operation_id: null,
+ origin_request_revision_digest: ids.request1,
+ scope_links: [
+ { thread_id: ids.thread, request_revision_digest: ids.request1, association: "PRODUCED_IN" },
+ ],
+ association: "PRODUCED_IN",
+ head_membership: head === digest ? "CURRENT" : "ANCESTOR",
+ title: titles[digest],
+ availability: "AVAILABLE",
+ entity_type: "HYPOTHESIS",
+ entity_id: "hypothesis:example-1",
+ schema_family: "HypothesisRecord",
+ currentness: currentness(
+ head === ids.restored ? "REVIEW_REQUIRED" : head === digest ? "CURRENT" : "REVIEW_REQUIRED",
+ ),
+ capability,
+ });
+ const resultItem = (second: boolean) => ({
+ ...revisionItem(second ? ids.current : ids.old, second ? "14:35" : "14:02"),
+ item_id: second ? "result-2" : "result-1",
+ kind: "RESULT",
+ title: second ? "추가 질문에 대한 답변" : "첫 질문에 대한 답변",
+ record_ref: ref(second ? ids.result2 : ids.result1),
+ origin_request_revision_digest: second ? ids.request2 : ids.request1,
+ operation_id: second ? "operation-2" : "operation-1",
+ entity_type: "DECISION_OBJECT",
+ entity_id: "result:example",
+ scope_links: [
+ {
+ thread_id: ids.thread,
+ request_revision_digest: second ? ids.request2 : ids.request1,
+ association: "PRODUCED_IN",
+ },
+ ],
currentness: currentness(head === ids.restored ? "REVIEW_REQUIRED" : "UNKNOWN_BASIS"),
- capability: { restore: "READ_ONLY", apply_ready: false, reason_codes: [] } });
- const diff = (before: string, after: string) => before === after ? [] : [{ path: "/statement", before_present: true, after_present: true, before, after }];
- const groups = (changes: ReturnType) => changes.length ? [{ kind: "CONTENT", trace_paths: changes.map(item => item.path), changes, summary: "저장된 설명이 바뀌었습니다" }] : [];
- const answer = (second: boolean) => second ? "두 번째 답변: 측정 조건과 원문 근거를 먼저 비교하세요." : "첫 번째 답변: 관측 조건의 차이를 가설로 두고 반대 근거를 확인하세요.";
- const impact = { stale_refs: ["hypothesis:example-1"], invalidated_refs: [], recalculate_refs: ["action-plan:example", "result:example"] };
+ capability: { restore: "READ_ONLY", apply_ready: false, reason_codes: [] },
+ });
+ const diff = (before: string, after: string) =>
+ before === after
+ ? []
+ : [{ path: "/statement", before_present: true, after_present: true, before, after }];
+ const groups = (changes: ReturnType) =>
+ changes.length
+ ? [
+ {
+ kind: "CONTENT",
+ trace_paths: changes.map((item) => item.path),
+ changes,
+ summary: "저장된 설명이 바뀌었습니다",
+ },
+ ]
+ : [];
+ const answer = (second: boolean) =>
+ second
+ ? "두 번째 답변: 측정 조건과 원문 근거를 먼저 비교하세요."
+ : "첫 번째 답변: 관측 조건의 차이를 가설로 두고 반대 근거를 확인하세요.";
+ const impact = {
+ stale_refs: ["hypothesis:example-1"],
+ invalidated_refs: [],
+ recalculate_refs: ["action-plan:example", "result:example"],
+ };
async function handle(request: FixtureRequest): Promise {
const input = request.params.input;
- if (request.method === "revision/timeline/read") return { contract_version: 2, actor_scope_digest: ids.actor, capability, coverage, next_cursor: null,
- items: [...(head === ids.restored ? [revisionItem(head, "14:45")] : []), revisionItem(ids.current, "14:40"), resultItem(true), revisionItem(ids.old, "14:05"), resultItem(false)] };
- if (request.method === "project/review/list") return { schema_version: "1.0.0", contract_version: 2, project_id: ids.project, coverage: "COMPLETE_PAGE", next_cursor: null,
- unread_supported: false, assignment_supported: false, items: [
- { item_id: "review-result-2", project_id: ids.project, thread_id: ids.thread, request_revision_digest: ids.request2, result_revision_digest: ids.result2,
- title: "추가 질문에 대한 답변", priority: "HIGH", reason_codes: ["UNKNOWN_BASIS"], currentness: currentness("REVIEW_REQUIRED"),
- next_user_action: { schema_version: "1.0.0", action_type: "REVIEW_CURRENTNESS", label: "현재 자료 기준으로 다시 확인", reason_codes: ["UNKNOWN_BASIS"], requires_permission: false, target: null, basis: {} } },
- ] };
- if (request.method === "thread/result/compare/read") return { schema_version: "1.0.0", contract_version: 2,
- project_id: ids.project, thread_id: ids.thread, before: input.before, after: input.after, state: "CHANGED",
- reason_state: "UNKNOWN_REASON", reason_codes: [], reason_refs: [],
- basis_currentness: { before: currentness("UNKNOWN_BASIS"), after: currentness("REVIEW_REQUIRED") },
- groups: [{ kind: "CONTENT", trace_paths: ["/result/answer"], changes: [{ path: "/result/answer",
- before_present: true, after_present: true, before: answer(false), after: answer(true) }] }] };
+ if (request.method === "revision/timeline/read")
+ return {
+ contract_version: 2,
+ actor_scope_digest: ids.actor,
+ capability,
+ coverage,
+ next_cursor: null,
+ items: [
+ ...(head === ids.restored ? [revisionItem(head, "14:45")] : []),
+ revisionItem(ids.current, "14:40"),
+ resultItem(true),
+ revisionItem(ids.old, "14:05"),
+ resultItem(false),
+ ],
+ };
+ if (request.method === "project/review/list")
+ return {
+ schema_version: "1.0.0",
+ contract_version: 2,
+ project_id: ids.project,
+ coverage: "COMPLETE_PAGE",
+ next_cursor: null,
+ unread_supported: false,
+ assignment_supported: false,
+ items: [
+ {
+ item_id: "review-result-2",
+ project_id: ids.project,
+ thread_id: ids.thread,
+ request_revision_digest: ids.request2,
+ result_revision_digest: ids.result2,
+ title: "추가 질문에 대한 답변",
+ priority: "HIGH",
+ reason_codes: ["UNKNOWN_BASIS"],
+ currentness: currentness("REVIEW_REQUIRED"),
+ next_user_action: {
+ schema_version: "1.0.0",
+ action_type: "REVIEW_CURRENTNESS",
+ label: "현재 자료 기준으로 다시 확인",
+ reason_codes: ["UNKNOWN_BASIS"],
+ requires_permission: false,
+ target: null,
+ basis: {},
+ },
+ },
+ ],
+ };
+ if (request.method === "thread/result/compare/read")
+ return {
+ schema_version: "1.0.0",
+ contract_version: 2,
+ project_id: ids.project,
+ thread_id: ids.thread,
+ before: input.before,
+ after: input.after,
+ state: "CHANGED",
+ reason_state: "UNKNOWN_REASON",
+ reason_codes: [],
+ reason_refs: [],
+ basis_currentness: {
+ before: currentness("UNKNOWN_BASIS"),
+ after: currentness("REVIEW_REQUIRED"),
+ },
+ groups: [
+ {
+ kind: "CONTENT",
+ trace_paths: ["/result/answer"],
+ changes: [
+ {
+ path: "/result/answer",
+ before_present: true,
+ after_present: true,
+ before: answer(false),
+ after: answer(true),
+ },
+ ],
+ },
+ ],
+ };
if (request.method === "revision/timeline/item/read") {
const record = input.record_ref as { revision_digest: string };
- if ([ids.result1, ids.result2].includes(record.revision_digest)) return { contract_version: 2, item: resultItem(record.revision_digest === ids.result2),
- content: { answer: answer(record.revision_digest === ids.result2) }, current_head_digest: ids.result2, coverage };
- return { contract_version: 2, item: revisionItem(record.revision_digest, "14:05"), content: content(record.revision_digest), current_head_digest: head, coverage };
+ if ([ids.result1, ids.result2].includes(record.revision_digest))
+ return {
+ contract_version: 2,
+ item: resultItem(record.revision_digest === ids.result2),
+ content: { answer: answer(record.revision_digest === ids.result2) },
+ current_head_digest: ids.result2,
+ coverage,
+ };
+ return {
+ contract_version: 2,
+ item: revisionItem(record.revision_digest, "14:05"),
+ content: content(record.revision_digest),
+ current_head_digest: head,
+ coverage,
+ };
}
if (request.method === "thread/result/read") {
const second = input.request_revision_digest === ids.request2;
- return { contract_version: 2, project_id: ids.project, thread_id: ids.thread,
- request_revision_digest: second ? ids.request2 : ids.request1, operation_id: second ? "operation-2" : "operation-1",
+ return {
+ contract_version: 2,
+ project_id: ids.project,
+ thread_id: ids.thread,
+ request_revision_digest: second ? ids.request2 : ids.request1,
+ operation_id: second ? "operation-2" : "operation-1",
result_revision_digest: second ? ids.result2 : ids.result1,
- request: { authored_text: second ? "조건을 같게 비교하려면 무엇을 확인해야 하나요?" : "결과가 달라진 이유를 어떻게 조사할까요?", created_at: "2026-09-21T14:00:00+09:00" },
- manifest: null, result: { answer: answer(second) },
- error: null, operation_state: "SUCCEEDED", availability: "AVAILABLE", basis_currentness: currentness(head === ids.restored ? "REVIEW_REQUIRED" : "UNKNOWN_BASIS"), coverage };
+ request: {
+ authored_text: second
+ ? "조건을 같게 비교하려면 무엇을 확인해야 하나요?"
+ : "결과가 달라진 이유를 어떻게 조사할까요?",
+ created_at: "2026-09-21T14:00:00+09:00",
+ },
+ manifest: null,
+ result: { answer: answer(second) },
+ error: null,
+ operation_state: "SUCCEEDED",
+ availability: "AVAILABLE",
+ basis_currentness: currentness(head === ids.restored ? "REVIEW_REQUIRED" : "UNKNOWN_BASIS"),
+ coverage,
+ };
}
if (request.method === "revision/diff/read") {
const resultDiff = [ids.result1, ids.result2].includes(String(input.from_revision_digest));
- const changes = resultDiff ? diff(answer(input.from_revision_digest === ids.result2), answer(input.to_revision_digest === ids.result2)).map(item => ({ ...item, path: "/result/answer" }))
- : diff(content(String(input.from_revision_digest)).statement, content(String(input.to_revision_digest)).statement);
- return { contract_version: 2, from_revision_digest: input.from_revision_digest, to_revision_digest: input.to_revision_digest, diff: changes, summary_groups: groups(changes), coverage };
+ const changes = resultDiff
+ ? diff(
+ answer(input.from_revision_digest === ids.result2),
+ answer(input.to_revision_digest === ids.result2),
+ ).map((item) => ({ ...item, path: "/result/answer" }))
+ : diff(
+ content(String(input.from_revision_digest)).statement,
+ content(String(input.to_revision_digest)).statement,
+ );
+ return {
+ contract_version: 2,
+ from_revision_digest: input.from_revision_digest,
+ to_revision_digest: input.to_revision_digest,
+ diff: changes,
+ summary_groups: groups(changes),
+ coverage,
+ };
}
if (request.method === "revision/restore/preview") {
const selection = input.selection as FixtureInput;
- const changes = diff(content(head).statement, content(String(selection.target_revision_digest)).statement);
- return { contract_version: 2, selection, profile_id: "hypothesis.v1", availability: changes.length ? "AVAILABLE" : "NO_CHANGE",
- reason_codes: [], capability, actor_scope_digest: ids.actor, basis_digest: ids.basis, impact, diff: changes,
- summary_groups: groups(changes), source_drift: [], reanalysis: "NOT_REQUESTED", external_effects: "PRESERVED" };
+ const changes = diff(
+ content(head).statement,
+ content(String(selection.target_revision_digest)).statement,
+ );
+ return {
+ contract_version: 2,
+ selection,
+ profile_id: "hypothesis.v1",
+ availability: changes.length ? "AVAILABLE" : "NO_CHANGE",
+ reason_codes: [],
+ capability,
+ actor_scope_digest: ids.actor,
+ basis_digest: ids.basis,
+ impact,
+ diff: changes,
+ summary_groups: groups(changes),
+ source_drift: [],
+ reanalysis: "NOT_REQUESTED",
+ external_effects: "PRESERVED",
+ };
}
if (request.method === "revision/restore/apply") {
const key = request.params._meta.idempotencyKey;
if (completed.has(key)) return completed.get(key);
if (!ready) throw new Error("RESTORE_APPLY_NOT_READY");
- appliedCount += 1; head = ids.restored;
- const result = { contract_version: 2, status: "APPLIED", selection: input.selection, new_revision_digest: head,
- new_revision_id: "revision-restored", receipt_id: "receipt-example", receipt_digest: ids.basis,
- restore_audit_id: "audit-example", impact, reanalysis: "NOT_REQUESTED", external_effects: "PRESERVED" };
+ appliedCount += 1;
+ head = ids.restored;
+ const result = {
+ contract_version: 2,
+ status: "APPLIED",
+ selection: input.selection,
+ new_revision_digest: head,
+ new_revision_id: "revision-restored",
+ receipt_id: "receipt-example",
+ receipt_digest: ids.basis,
+ restore_audit_id: "audit-example",
+ impact,
+ reanalysis: "NOT_REQUESTED",
+ external_effects: "PRESERVED",
+ };
completed.set(key, result);
- if (options.loseApplyResponse && !lost) { lost = true; throw new TypeError("검증용 응답 유실"); }
+ if (options.loseApplyResponse && !lost) {
+ lost = true;
+ throw new TypeError("검증용 응답 유실");
+ }
return result;
}
- if (request.method === "operation/result/read") return { operation_id: input.operation_id, state: "SUCCEEDED", result: [...completed.values()][0] ?? null, error: null };
+ if (request.method === "operation/result/read")
+ return {
+ operation_id: input.operation_id,
+ state: "SUCCEEDED",
+ result: [...completed.values()][0] ?? null,
+ error: null,
+ };
throw new Error(`Unsupported fixture method: ${request.method}`);
}
const fetcher: typeof fetch = async (url, init) => {
const request = JSON.parse(String(init?.body)) as FixtureRequest;
- calls.push({ method: request.method, input: request.params.input, key: request.params._meta.idempotencyKey, url: String(url) });
+ calls.push({
+ method: request.method,
+ input: request.params.input,
+ key: request.params._meta.idempotencyKey,
+ url: String(url),
+ });
const value = await handle(request);
- return new Response(JSON.stringify({ jsonrpc: "2.0", id: request.id,
- result: { operation_id: request.method === "revision/restore/apply" ? "operation-restore" : "", state: "SUCCEEDED", value } }));
+ return new Response(
+ JSON.stringify({
+ jsonrpc: "2.0",
+ id: request.id,
+ result: {
+ operation_id: request.method === "revision/restore/apply" ? "operation-restore" : "",
+ state: "SUCCEEDED",
+ value,
+ },
+ }),
+ );
+ };
+ return {
+ fetch: fetcher,
+ calls,
+ get appliedCount() {
+ return appliedCount;
+ },
};
- return { fetch: fetcher, calls, get appliedCount() { return appliedCount; } };
}
diff --git a/apps/web/src/api/hypothesisLink.ts b/apps/web/src/api/hypothesisLink.ts
index d35c3c56..91ec8c79 100644
--- a/apps/web/src/api/hypothesisLink.ts
+++ b/apps/web/src/api/hypothesisLink.ts
@@ -1,24 +1,73 @@
import { rpc } from "./rpcClient";
export type RecheckReasonCode = "UNRELATED" | "STILL_MATCHES" | "NEEDS_RESEARCH" | "OTHER";
-export type LinkRecheck = { reason_code: RecheckReasonCode; note: string; actor_id: string; created_at: string; flipped: boolean };
+export type LinkRecheck = {
+ reason_code: RecheckReasonCode;
+ note: string;
+ actor_id: string;
+ created_at: string;
+ flipped: boolean;
+};
/** A hypothesis that came from a trace row, read against that row's verdict today. */
export type HypothesisLink = {
- hypothesis_id: string; hypothesis_revision_digest: string; statement: string; subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; subject_title: string;
- state: "CURRENT" | "STALE" | "RECHECKED"; change: "CONTENT_CHANGED" | "EVIDENCE_ONLY" | "FLIPPED" | "SUBJECT_MISSING" | null;
- link_state: string | null; current_state: string | null; current_verdict_revision: string | null; recheck: LinkRecheck | null;
+ hypothesis_id: string;
+ hypothesis_revision_digest: string;
+ statement: string;
+ subject_kind: "CRITERION" | "REQUIREMENT";
+ subject_id: string;
+ subject_title: string;
+ state: "CURRENT" | "STALE" | "RECHECKED";
+ change: "CONTENT_CHANGED" | "EVIDENCE_ONLY" | "FLIPPED" | "SUBJECT_MISSING" | null;
+ link_state: string | null;
+ current_state: string | null;
+ current_verdict_revision: string | null;
+ recheck: LinkRecheck | null;
+};
+export type ReasonDistribution = {
+ total: number;
+ flipped: number;
+ by_reason: Partial>;
+};
+export type HypothesisLinks = {
+ trace_digest: string | null;
+ links: HypothesisLink[];
+ reason_distribution: ReasonDistribution;
};
-export type ReasonDistribution = { total: number; flipped: number; by_reason: Partial> };
-export type HypothesisLinks = { trace_digest: string | null; links: HypothesisLink[]; reason_distribution: ReasonDistribution };
export const hypothesisLinksKey = (projectId: string) => ["hypothesis-links", projectId] as const;
-export async function listHypothesisLinks(projectId: string, signal?: AbortSignal): Promise {
- return (await rpc("hypothesis/link/list", { project_id: projectId }, crypto.randomUUID(), signal)).value;
+export async function listHypothesisLinks(
+ projectId: string,
+ signal?: AbortSignal,
+): Promise {
+ return (
+ await rpc(
+ "hypothesis/link/list",
+ { project_id: projectId },
+ crypto.randomUUID(),
+ signal,
+ )
+ ).value;
}
-export async function recheckHypothesisLinks(projectId: string, hypothesisIds: string[], reason: RecheckReasonCode, note: string, currentVerdictRevision: string | null) {
- return (await rpc("hypothesis/link/recheck", {
- project_id: projectId, hypothesis_ids: hypothesisIds, reason_code: reason, note, current_verdict_revision: currentVerdictRevision,
- }, crypto.randomUUID())).value;
+export async function recheckHypothesisLinks(
+ projectId: string,
+ hypothesisIds: string[],
+ reason: RecheckReasonCode,
+ note: string,
+ currentVerdictRevision: string | null,
+) {
+ return (
+ await rpc(
+ "hypothesis/link/recheck",
+ {
+ project_id: projectId,
+ hypothesis_ids: hypothesisIds,
+ reason_code: reason,
+ note,
+ current_verdict_revision: currentVerdictRevision,
+ },
+ crypto.randomUUID(),
+ )
+ ).value;
}
diff --git a/apps/web/src/api/hypothesisSame.ts b/apps/web/src/api/hypothesisSame.ts
index 7b9c09b7..974caa27 100644
--- a/apps/web/src/api/hypothesisSame.ts
+++ b/apps/web/src/api/hypothesisSame.ts
@@ -1,19 +1,54 @@
import { rpc } from "./rpcClient";
/** What a screen shows about a hypothesis a person marked as the same as another; empty for one that cannot be read. */
-export type SameMember = { statement?: string; investigated_at?: string; subject_kind?: string; subject_id?: string };
+export type SameMember = {
+ statement?: string;
+ investigated_at?: string;
+ subject_kind?: string;
+ subject_id?: string;
+};
/** The groups of hypotheses people marked as the same one across investigations (each with at least two ids). */
-export type SameView = { groups: string[][]; pairs: string[][]; members: Record; events: unknown[] };
+export type SameView = {
+ groups: string[][];
+ pairs: string[][];
+ members: Record;
+ events: unknown[];
+};
export const sameKey = (projectId: string) => ["hypothesis-same", projectId] as const;
/** An answer that lacks a part reads as no group, so a screen never fails on a short or unexpected answer. */
-const whole = (value: Partial | undefined): SameView => ({ groups: value?.groups ?? [], pairs: value?.pairs ?? [], members: value?.members ?? {}, events: value?.events ?? [] });
+const whole = (value: Partial | undefined): SameView => ({
+ groups: value?.groups ?? [],
+ pairs: value?.pairs ?? [],
+ members: value?.members ?? {},
+ events: value?.events ?? [],
+});
export async function listSame(projectId: string, signal?: AbortSignal) {
- return whole((await rpc("hypothesis/same/list", { project_id: projectId }, crypto.randomUUID(), signal)).value);
+ return whole(
+ (
+ await rpc(
+ "hypothesis/same/list",
+ { project_id: projectId },
+ crypto.randomUUID(),
+ signal,
+ )
+ ).value,
+ );
}
-export async function recordSame(projectId: string, hypothesisIds: [string, string], action: "LINK" | "UNLINK", note: string) {
- return (await rpc("hypothesis/same/record", { project_id: projectId, hypothesis_ids: hypothesisIds, action, note }, crypto.randomUUID())).value;
+export async function recordSame(
+ projectId: string,
+ hypothesisIds: [string, string],
+ action: "LINK" | "UNLINK",
+ note: string,
+) {
+ return (
+ await rpc(
+ "hypothesis/same/record",
+ { project_id: projectId, hypothesis_ids: hypothesisIds, action, note },
+ crypto.randomUUID(),
+ )
+ ).value;
}
diff --git a/apps/web/src/api/judgmentRecords.ts b/apps/web/src/api/judgmentRecords.ts
index 22da46cb..ace77d30 100644
--- a/apps/web/src/api/judgmentRecords.ts
+++ b/apps/web/src/api/judgmentRecords.ts
@@ -2,9 +2,22 @@ import { rpc } from "./rpcClient";
export type MatchKind = "THIS_HYPOTHESIS" | "ALTERNATIVE" | "NEITHER" | "UNDETERMINED";
/** What a person recorded for one test: the latest result is the one that counts. */
-export type TestResult = { event_id: string; hypothesis_id: string; test_id: string; observation: string; matched: MatchKind; evidence_refs: string[]; actor_id: string; created_at: string };
+export type TestResult = {
+ event_id: string;
+ hypothesis_id: string;
+ test_id: string;
+ observation: string;
+ matched: MatchKind;
+ evidence_refs: string[];
+ actor_id: string;
+ created_at: string;
+};
export type DiscriminationItem = {
- hypothesis_id: string; results: TestResult[]; result_history_count: number; refutation_conditions: string[]; conditions_history_count: number;
+ hypothesis_id: string;
+ results: TestResult[];
+ result_history_count: number;
+ refutation_conditions: string[];
+ conditions_history_count: number;
elimination: "SINGLE" | "REPEATED" | null;
/** Where the recorded results leave the hypothesis, read by the server when asked; a record without it counts as none. */
standing?: "NONE" | "FITS" | "AGAINST_ONCE" | "AGAINST_REPEATED" | "MIXED";
@@ -12,31 +25,113 @@ export type DiscriminationItem = {
export type ClosureKind = "FIX_APPLIED" | "HUMAN_CLOSED" | "WAIVER_RECORDED" | "CONDITION_CHANGED";
export type ClosureEvent = {
- event_id: string; subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; kind: ClosureKind; basis_ref: string; note: string; scope: string | null;
- verdict_revision: string; verdict_digest: string; verdict_state: string; actor_id: string; created_at: string;
+ event_id: string;
+ subject_kind: "CRITERION" | "REQUIREMENT";
+ subject_id: string;
+ kind: ClosureKind;
+ basis_ref: string;
+ note: string;
+ scope: string | null;
+ verdict_revision: string;
+ verdict_digest: string;
+ verdict_state: string;
+ actor_id: string;
+ created_at: string;
};
/** A trace row that has a closure: the events, and whether the rules confirm the effect of a recorded fix. */
export type ClosureRow = {
- subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; current_state: string | null; effect_confirmed: boolean; effect_verdict_revision: string | null;
- verdict_changed_since: boolean; events: ClosureEvent[];
+ subject_kind: "CRITERION" | "REQUIREMENT";
+ subject_id: string;
+ current_state: string | null;
+ effect_confirmed: boolean;
+ effect_verdict_revision: string | null;
+ verdict_changed_since: boolean;
+ events: ClosureEvent[];
};
export const discriminationKey = (projectId: string) => ["discrimination", projectId] as const;
export const closuresKey = (projectId: string) => ["trace-closures", projectId] as const;
export async function listDiscrimination(projectId: string, signal?: AbortSignal) {
- return (await rpc<{ items: DiscriminationItem[] }>("hypothesis/test/result/list", { project_id: projectId }, crypto.randomUUID(), signal)).value;
+ return (
+ await rpc<{ items: DiscriminationItem[] }>(
+ "hypothesis/test/result/list",
+ { project_id: projectId },
+ crypto.randomUUID(),
+ signal,
+ )
+ ).value;
}
-export async function recordTestResult(projectId: string, hypothesisId: string, testId: string, observation: string, matched: MatchKind, evidenceRefs: string[]) {
- return (await rpc("hypothesis/test/result/record", { project_id: projectId, hypothesis_id: hypothesisId, test_id: testId, observation, matched, evidence_refs: evidenceRefs }, crypto.randomUUID())).value;
+export async function recordTestResult(
+ projectId: string,
+ hypothesisId: string,
+ testId: string,
+ observation: string,
+ matched: MatchKind,
+ evidenceRefs: string[],
+) {
+ return (
+ await rpc(
+ "hypothesis/test/result/record",
+ {
+ project_id: projectId,
+ hypothesis_id: hypothesisId,
+ test_id: testId,
+ observation,
+ matched,
+ evidence_refs: evidenceRefs,
+ },
+ crypto.randomUUID(),
+ )
+ ).value;
}
-export async function recordRefutationConditions(projectId: string, hypothesisId: string, conditions: string[]) {
- return (await rpc("hypothesis/refutation/record", { project_id: projectId, hypothesis_id: hypothesisId, conditions }, crypto.randomUUID())).value;
+export async function recordRefutationConditions(
+ projectId: string,
+ hypothesisId: string,
+ conditions: string[],
+) {
+ return (
+ await rpc(
+ "hypothesis/refutation/record",
+ { project_id: projectId, hypothesis_id: hypothesisId, conditions },
+ crypto.randomUUID(),
+ )
+ ).value;
}
export async function listClosures(projectId: string, signal?: AbortSignal) {
- return (await rpc<{ closures: ClosureRow[] }>("trace/closure/list", { project_id: projectId }, crypto.randomUUID(), signal)).value;
+ return (
+ await rpc<{ closures: ClosureRow[] }>(
+ "trace/closure/list",
+ { project_id: projectId },
+ crypto.randomUUID(),
+ signal,
+ )
+ ).value;
}
-export async function recordClosure(input: { projectId: string; subjectKind: string; subjectId: string; kind: ClosureKind; basisRef: string; note: string; scope: string; verdictRevision: string }) {
- return (await rpc("trace/closure/record", { project_id: input.projectId, subject_kind: input.subjectKind, subject_id: input.subjectId, kind: input.kind, basis_ref: input.basisRef,
- note: input.note, ...(input.scope ? { scope: input.scope } : {}), current_verdict_revision: input.verdictRevision }, crypto.randomUUID())).value;
+export async function recordClosure(input: {
+ projectId: string;
+ subjectKind: string;
+ subjectId: string;
+ kind: ClosureKind;
+ basisRef: string;
+ note: string;
+ scope: string;
+ verdictRevision: string;
+}) {
+ return (
+ await rpc(
+ "trace/closure/record",
+ {
+ project_id: input.projectId,
+ subject_kind: input.subjectKind,
+ subject_id: input.subjectId,
+ kind: input.kind,
+ basis_ref: input.basisRef,
+ note: input.note,
+ ...(input.scope ? { scope: input.scope } : {}),
+ current_verdict_revision: input.verdictRevision,
+ },
+ crypto.randomUUID(),
+ )
+ ).value;
}
diff --git a/apps/web/src/api/koreanParticles.test.ts b/apps/web/src/api/koreanParticles.test.ts
index 2d527aa9..a28428d1 100644
--- a/apps/web/src/api/koreanParticles.test.ts
+++ b/apps/web/src/api/koreanParticles.test.ts
@@ -13,9 +13,12 @@ describe("finalHasBatchim", () => {
it("reads a digit by how it is spoken", () => {
const spoken = [true, true, false, true, false, false, true, true, true, false];
- spoken.forEach((expected, digit) => expect(finalHasBatchim(String(digit)), String(digit)).toBe(expected));
+ spoken.forEach((expected, digit) =>
+ expect(finalHasBatchim(String(digit)), String(digit)).toBe(expected),
+ );
// 십, 백, 천 and 만 all carry a final consonant, and so does 영.
- for (const value of ["10", "100", "1000", "10000"]) expect(finalHasBatchim(value), value).toBe(true);
+ for (const value of ["10", "100", "1000", "10000"])
+ expect(finalHasBatchim(value), value).toBe(true);
expect(finalHasBatchim("12")).toBe(false);
expect(finalHasBatchim("A3")).toBe(true);
expect(finalHasBatchim("p.6")).toBe(true);
@@ -23,8 +26,32 @@ describe("finalHasBatchim", () => {
});
it("reads a Latin letter by its name and ignores case", () => {
- for (const letter of ["l", "m", "n", "r", "L", "M", "N", "R"]) expect(finalHasBatchim(letter), letter).toBe(true);
- for (const letter of ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "o", "p", "q", "s", "t", "u", "v", "w", "x", "y", "z"]) {
+ for (const letter of ["l", "m", "n", "r", "L", "M", "N", "R"])
+ expect(finalHasBatchim(letter), letter).toBe(true);
+ for (const letter of [
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f",
+ "g",
+ "h",
+ "i",
+ "j",
+ "k",
+ "o",
+ "p",
+ "q",
+ "s",
+ "t",
+ "u",
+ "v",
+ "w",
+ "x",
+ "y",
+ "z",
+ ]) {
expect(finalHasBatchim(letter), letter).toBe(false);
expect(finalHasBatchim(letter.toUpperCase()), letter.toUpperCase()).toBe(false);
}
diff --git a/apps/web/src/api/koreanParticles.ts b/apps/web/src/api/koreanParticles.ts
index cf7577b6..3a0dd8e3 100644
--- a/apps/web/src/api/koreanParticles.ts
+++ b/apps/web/src/api/koreanParticles.ts
@@ -23,7 +23,9 @@ export function finalHasBatchim(text: string): boolean | null {
function withParticle(text: string, withBatchim: string, without: string): string {
const batchim = finalHasBatchim(text);
- return batchim === null ? `${text}${withBatchim}(${without})` : `${text}${batchim ? withBatchim : without}`;
+ return batchim === null
+ ? `${text}${withBatchim}(${without})`
+ : `${text}${batchim ? withBatchim : without}`;
}
/** 을/를 */
diff --git a/apps/web/src/api/lessons.ts b/apps/web/src/api/lessons.ts
index a6f94a65..52471b52 100644
--- a/apps/web/src/api/lessons.ts
+++ b/apps/web/src/api/lessons.ts
@@ -4,13 +4,31 @@ import type { MatchKind } from "./judgmentRecords";
export type LessonKind = "TEST_RESULT" | "ELIMINATION" | "EFFECT_CONFIRMED" | "HUMAN_CLOSURE";
export type LessonState = "SAME_CONDITION" | "STALE" | "REFUTED";
export type LessonDetail = {
- observations?: { test_id: string; matched: MatchKind; observation: string; evidence_refs: string[] }[];
- closure_kind?: string; basis_ref?: string; note?: string; scope?: string | null; original_state?: string;
+ observations?: {
+ test_id: string;
+ matched: MatchKind;
+ observation: string;
+ evidence_refs: string[];
+ }[];
+ closure_kind?: string;
+ basis_ref?: string;
+ note?: string;
+ scope?: string | null;
+ original_state?: string;
};
/** A lesson: a reference to a record a rule computed or a person made, read back for the row's exact condition. */
export type LessonItem = {
- lesson_id: string; kind: LessonKind; outcome: string; state: LessonState; subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string;
- hypothesis_id: string | null; test_id: string | null; actor_id: string; created_at: string; detail: LessonDetail;
+ lesson_id: string;
+ kind: LessonKind;
+ outcome: string;
+ state: LessonState;
+ subject_kind: "CRITERION" | "REQUIREMENT";
+ subject_id: string;
+ hypothesis_id: string | null;
+ test_id: string | null;
+ actor_id: string;
+ created_at: string;
+ detail: LessonDetail;
/** The other hypotheses a person marked as the same as this lesson's hypothesis; absent when none. */
same_hypothesis_ids?: string[];
};
@@ -18,5 +36,12 @@ export type LessonItem = {
export const lessonsKey = (projectId: string) => ["lessons", projectId] as const;
export async function listLessons(projectId: string, signal?: AbortSignal) {
- return (await rpc<{ lessons: LessonItem[] }>("trace/lesson/list", { project_id: projectId }, crypto.randomUUID(), signal)).value;
+ return (
+ await rpc<{ lessons: LessonItem[] }>(
+ "trace/lesson/list",
+ { project_id: projectId },
+ crypto.randomUUID(),
+ signal,
+ )
+ ).value;
}
diff --git a/apps/web/src/api/localRetrieval.ts b/apps/web/src/api/localRetrieval.ts
index 7408dab0..195b22af 100644
--- a/apps/web/src/api/localRetrieval.ts
+++ b/apps/web/src/api/localRetrieval.ts
@@ -14,20 +14,51 @@ import { z } from "zod";
export const WORKSPACE_PROJECT_ID = "system:workspace";
const assetReadSchema = z.object({
- schema_version: z.literal("1.0.0"), supported: z.boolean(), can_manage: z.boolean(),
- model_name: z.string(), manifest_digest: z.string(), total_bytes: z.number().nonnegative(),
- asset_state: z.enum(["MODEL_REQUIRED", "READY", "FAILED"]), dependency_available: z.boolean(),
- worker_loaded: z.boolean(), reason_code: z.string().nullable(),
- active: z.object({ operation_id: z.string(), state: z.enum(["COPYING", "DOWNLOADING", "VERIFYING"]),
- transferred_bytes: z.number().nonnegative(), total_bytes: z.number().nonnegative() }).nullable(),
+ schema_version: z.literal("1.0.0"),
+ supported: z.boolean(),
+ can_manage: z.boolean(),
+ model_name: z.string(),
+ manifest_digest: z.string(),
+ total_bytes: z.number().nonnegative(),
+ asset_state: z.enum(["MODEL_REQUIRED", "READY", "FAILED"]),
+ dependency_available: z.boolean(),
+ worker_loaded: z.boolean(),
+ reason_code: z.string().nullable(),
+ active: z
+ .object({
+ operation_id: z.string(),
+ state: z.enum(["COPYING", "DOWNLOADING", "VERIFYING"]),
+ transferred_bytes: z.number().nonnegative(),
+ total_bytes: z.number().nonnegative(),
+ })
+ .nullable(),
});
const projectReadSchema = z.object({
- project_id: z.string(), can_prepare: z.boolean(), enabled: z.boolean(), generation: z.number().int().nonnegative(),
- state: z.enum(["OFF", "NO_SOURCES", "INDEX_PENDING", "INDEXING", "READY", "FAILED", "CANCEL_REQUESTED"]),
- ready_sources: z.number().nonnegative(), total_sources: z.number().nonnegative(),
- pending_sources: z.number().nonnegative(), failed_sources: z.number().nonnegative(),
- jobs: z.array(z.object({ job_id: z.string(), generation: z.number().int(),
- state: z.enum(["PENDING", "RUNNING", "READY", "FAILED", "CANCELLED", "STALE"]), reason_code: z.string().nullable() })),
+ project_id: z.string(),
+ can_prepare: z.boolean(),
+ enabled: z.boolean(),
+ generation: z.number().int().nonnegative(),
+ state: z.enum([
+ "OFF",
+ "NO_SOURCES",
+ "INDEX_PENDING",
+ "INDEXING",
+ "READY",
+ "FAILED",
+ "CANCEL_REQUESTED",
+ ]),
+ ready_sources: z.number().nonnegative(),
+ total_sources: z.number().nonnegative(),
+ pending_sources: z.number().nonnegative(),
+ failed_sources: z.number().nonnegative(),
+ jobs: z.array(
+ z.object({
+ job_id: z.string(),
+ generation: z.number().int(),
+ state: z.enum(["PENDING", "RUNNING", "READY", "FAILED", "CANCELLED", "STALE"]),
+ reason_code: z.string().nullable(),
+ }),
+ ),
active_operation_id: z.string().nullable(),
// True while sources are indexed only under an earlier preprocessing version.
outdated_index: z.boolean().optional(),
@@ -39,12 +70,25 @@ export type RetrievalExperimentRead = z.infer;
export type RetrievalJob = RetrievalExperimentRead["jobs"][number];
export async function readLocalAsset(signal?: AbortSignal): Promise> {
- const result = await rpc("model/localAsset/read", { project_id: WORKSPACE_PROJECT_ID }, crypto.randomUUID(), signal);
+ const result = await rpc(
+ "model/localAsset/read",
+ { project_id: WORKSPACE_PROJECT_ID },
+ crypto.randomUUID(),
+ signal,
+ );
return { ...result, value: assetReadSchema.parse(result.value) };
}
-export async function readRetrievalExperiment(projectId: string, signal?: AbortSignal): Promise> {
- const result = await rpc("project/retrieval/experiment/read", { project_id: projectId }, crypto.randomUUID(), signal);
+export async function readRetrievalExperiment(
+ projectId: string,
+ signal?: AbortSignal,
+): Promise> {
+ const result = await rpc(
+ "project/retrieval/experiment/read",
+ { project_id: projectId },
+ crypto.randomUUID(),
+ signal,
+ );
const value = projectReadSchema.parse(result.value);
if (value.project_id !== projectId) throw new Error("LOCAL_RETRIEVAL_SCOPE_MISMATCH");
return { ...result, value };
@@ -55,7 +99,11 @@ export async function configureRetrievalExperiment(
input: { project_id: string; enabled: boolean; expected_generation: number },
idempotencyKey: string,
): Promise> {
- const result = await rpc("project/retrieval/experiment/configure", input, idempotencyKey);
+ const result = await rpc(
+ "project/retrieval/experiment/configure",
+ input,
+ idempotencyKey,
+ );
const value = projectReadSchema.parse(result.value);
if (value.project_id !== input.project_id) throw new Error("LOCAL_RETRIEVAL_SCOPE_MISMATCH");
return { ...result, value };
@@ -81,7 +129,11 @@ export type OperationTimeouts = {
};
export const DEFAULT_OPERATION_TIMEOUTS: OperationTimeouts = {
- startMs: 15_000, pollMs: 15_000, pollIntervalMs: 900, totalMs: 610_000, cancelMs: 15_000,
+ startMs: 15_000,
+ pollMs: 15_000,
+ pollIntervalMs: 900,
+ totalMs: 610_000,
+ cancelMs: 15_000,
};
/** Every way a long action can end, as the client can actually tell it apart. */
@@ -110,18 +162,31 @@ type BoundedJson =
| { ok: false; cause: "TIMEOUT" | "NETWORK" | "ABORTED" };
/** One request whose headers *and* body must arrive within `timeoutMs`; a stalled body is a timeout too. */
-async function boundedJson(url: string, init: RequestInit, signal: AbortSignal | undefined, timeoutMs: number): Promise {
+async function boundedJson(
+ url: string,
+ init: RequestInit,
+ signal: AbortSignal | undefined,
+ timeoutMs: number,
+): Promise {
if (signal?.aborted) return { ok: false, cause: "ABORTED" };
const controller = new AbortController();
let timedOut = false;
- const timer = globalThis.setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
+ const timer = globalThis.setTimeout(() => {
+ timedOut = true;
+ controller.abort();
+ }, timeoutMs);
const abort = () => controller.abort();
signal?.addEventListener("abort", abort, { once: true });
try {
- const response = await fetch(url, { ...init, signal: controller.signal, credentials: "same-origin" });
+ const response = await fetch(url, {
+ ...init,
+ signal: controller.signal,
+ credentials: "same-origin",
+ });
let body: unknown;
- try { body = await response.json(); }
- catch (error) {
+ try {
+ body = await response.json();
+ } catch (error) {
if (controller.signal.aborted) throw error;
body = undefined;
}
@@ -136,13 +201,23 @@ async function boundedJson(url: string, init: RequestInit, signal: AbortSignal |
}
/** An HTTP status that leaves it open whether the server took the request (it may be working on it). */
-const startUnconfirmedStatus = (status: number) => status === 408 || status === 429 || status >= 500;
+const startUnconfirmedStatus = (status: number) =>
+ status === 408 || status === 429 || status >= 500;
function sleep(ms: number, signal: AbortSignal | undefined): Promise {
- return new Promise(resolve => {
- if (signal?.aborted) { resolve(); return; }
- const timer = globalThis.setTimeout(() => { signal?.removeEventListener("abort", done); resolve(); }, ms);
- const done = () => { globalThis.clearTimeout(timer); resolve(); };
+ return new Promise((resolve) => {
+ if (signal?.aborted) {
+ resolve();
+ return;
+ }
+ const timer = globalThis.setTimeout(() => {
+ signal?.removeEventListener("abort", done);
+ resolve();
+ }, ms);
+ const done = () => {
+ globalThis.clearTimeout(timer);
+ resolve();
+ };
signal?.addEventListener("abort", done, { once: true });
});
}
@@ -150,7 +225,9 @@ function sleep(ms: number, signal: AbortSignal | undefined): Promise {
const codeShape = /^[A-Z][A-Z0-9_]{2,79}$/;
/** Only a code-shaped value is kept; free text (which may carry paths) is dropped. */
-export function reasonCodeOf(error: { message?: unknown; data?: unknown } | null | undefined): string | null {
+export function reasonCodeOf(
+ error: { message?: unknown; data?: unknown } | null | undefined,
+): string | null {
if (!error) return null;
const data = error.data;
if (data && typeof data === "object" && "reason_code" in data) {
@@ -173,7 +250,9 @@ export type RunLocalOperationRequest = {
timeouts?: Partial;
};
-export async function runLocalOperation(request: RunLocalOperationRequest): Promise {
+export async function runLocalOperation(
+ request: RunLocalOperationRequest,
+): Promise {
const timeouts = { ...DEFAULT_OPERATION_TIMEOUTS, ...request.timeouts };
const deadline = Date.now() + timeouts.totalMs;
/** Each request gets its own limit, never more than what is left of the whole wait. */
@@ -184,16 +263,21 @@ export async function runLocalOperation(request: RunLocalOperationRequest): Prom
if (!operationId) {
if (signal?.aborted) return { kind: "ABANDONED", operationId: null };
- const started = await boundedJson("/rpc/async", {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({
- jsonrpc: "2.0",
- id: crypto.randomUUID(),
- method: request.method,
- params: { _meta: { idempotencyKey: request.idempotencyKey }, input: request.input },
- }),
- }, signal, remaining(timeouts.startMs));
+ const started = await boundedJson(
+ "/rpc/async",
+ {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({
+ jsonrpc: "2.0",
+ id: crypto.randomUUID(),
+ method: request.method,
+ params: { _meta: { idempotencyKey: request.idempotencyKey }, input: request.input },
+ }),
+ },
+ signal,
+ remaining(timeouts.startMs),
+ );
if (!started.ok) {
if (started.cause === "ABORTED") return { kind: "ABANDONED", operationId: null };
return { kind: "UNCONFIRMED", operationId: null };
@@ -202,10 +286,15 @@ export async function runLocalOperation(request: RunLocalOperationRequest): Prom
if (!started.httpOk) {
// 408/429/5xx do not prove that nothing started: keep the same key for a later check.
if (startUnconfirmedStatus(started.status)) return { kind: "UNCONFIRMED", operationId: null };
- return { kind: "REJECTED", reasonCode: parsed.success ? reasonCodeOf(parsed.data.error) : null, httpStatus: started.status };
+ return {
+ kind: "REJECTED",
+ reasonCode: parsed.success ? reasonCodeOf(parsed.data.error) : null,
+ httpStatus: started.status,
+ };
}
if (!parsed.success) return { kind: "UNCONFIRMED", operationId: null };
- if (parsed.data.error) return { kind: "REJECTED", reasonCode: reasonCodeOf(parsed.data.error), httpStatus: null };
+ if (parsed.data.error)
+ return { kind: "REJECTED", reasonCode: reasonCodeOf(parsed.data.error), httpStatus: null };
const id = parsed.data.result?.operation_id;
if (typeof id !== "string" || !id) return { kind: "UNCONFIRMED", operationId: null };
operationId = id;
@@ -217,7 +306,9 @@ export async function runLocalOperation(request: RunLocalOperationRequest): Prom
if (Date.now() >= deadline) return { kind: "UNCONFIRMED", operationId };
const read = await boundedJson(
`/operations/${encodeURIComponent(operationId)}?project_id=${encodeURIComponent(projectId)}`,
- { method: "GET" }, signal, remaining(timeouts.pollMs),
+ { method: "GET" },
+ signal,
+ remaining(timeouts.pollMs),
);
if (!read.ok) {
if (read.cause === "ABORTED") return { kind: "ABANDONED", operationId };
@@ -229,7 +320,8 @@ export async function runLocalOperation(request: RunLocalOperationRequest): Prom
if (record.state === "SUCCEEDED" && record.result !== null && record.result !== undefined) {
return { kind: "SUCCEEDED", operationId, value: record.result };
}
- if (record.state === "FAILED") return { kind: "FAILED", operationId, reasonCode: reasonCodeOf(record.error) };
+ if (record.state === "FAILED")
+ return { kind: "FAILED", operationId, reasonCode: reasonCodeOf(record.error) };
if (record.state === "CANCELLED") return { kind: "CANCELLED", operationId };
await sleep(timeouts.pollIntervalMs, signal);
}
@@ -243,20 +335,33 @@ export type CancelRequestResult =
/** Asks the server to cancel. This never claims that the work stopped. */
export async function cancelLocalOperation(
- projectId: string, operationId: string, signal?: AbortSignal, timeoutMs: number = DEFAULT_OPERATION_TIMEOUTS.cancelMs,
+ projectId: string,
+ operationId: string,
+ signal?: AbortSignal,
+ timeoutMs: number = DEFAULT_OPERATION_TIMEOUTS.cancelMs,
): Promise {
- const sent = await boundedJson(`/operations/${encodeURIComponent(operationId)}/cancel`, {
- method: "POST",
- headers: { "content-type": "application/json" },
- body: JSON.stringify({ project_id: projectId }),
- }, signal, timeoutMs);
+ const sent = await boundedJson(
+ `/operations/${encodeURIComponent(operationId)}/cancel`,
+ {
+ method: "POST",
+ headers: { "content-type": "application/json" },
+ body: JSON.stringify({ project_id: projectId }),
+ },
+ signal,
+ timeoutMs,
+ );
if (!sent.ok) return { kind: "UNCONFIRMED" };
const parsed = jsonRpcResponseSchema.safeParse(sent.body);
if (!sent.httpOk) {
if (startUnconfirmedStatus(sent.status)) return { kind: "UNCONFIRMED" };
- return { kind: "REJECTED", reasonCode: parsed.success ? reasonCodeOf(parsed.data.error) : null, httpStatus: sent.status };
+ return {
+ kind: "REJECTED",
+ reasonCode: parsed.success ? reasonCodeOf(parsed.data.error) : null,
+ httpStatus: sent.status,
+ };
}
if (!parsed.success) return { kind: "UNCONFIRMED" };
- if (parsed.data.error) return { kind: "REJECTED", reasonCode: reasonCodeOf(parsed.data.error), httpStatus: null };
+ if (parsed.data.error)
+ return { kind: "REJECTED", reasonCode: reasonCodeOf(parsed.data.error), httpStatus: null };
return { kind: "REQUESTED" };
}
diff --git a/apps/web/src/api/localRetrievalInstall.test.ts b/apps/web/src/api/localRetrievalInstall.test.ts
index 49942819..903f8c6a 100644
--- a/apps/web/src/api/localRetrievalInstall.test.ts
+++ b/apps/web/src/api/localRetrievalInstall.test.ts
@@ -3,14 +3,19 @@ import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { LOCAL_RETRIEVAL_INSTALL_COMMAND } from "./localRetrievalInstall";
-const install = readFileSync(fileURLToPath(new URL("../../../../docs/INSTALL.md", import.meta.url)), "utf8").split(/\r?\n/);
+const install = readFileSync(
+ fileURLToPath(new URL("../../../../docs/INSTALL.md", import.meta.url)),
+ "utf8",
+).split(/\r?\n/);
it("shows the same install command in docs/INSTALL.md as the settings card", () => {
expect(install).toContain(LOCAL_RETRIEVAL_INSTALL_COMMAND);
});
it("is the basic install command from INSTALL.md plus the local-retrieval extra", () => {
- const basic = install.find(line => line.startsWith("uv sync ") && line.endsWith("--extra dev --dev"));
+ const basic = install.find(
+ (line) => line.startsWith("uv sync ") && line.endsWith("--extra dev --dev"),
+ );
expect(basic).toBeDefined();
expect(LOCAL_RETRIEVAL_INSTALL_COMMAND).toBe(`${basic} --extra local-retrieval`);
});
diff --git a/apps/web/src/api/localWorkspacePersistence.test.ts b/apps/web/src/api/localWorkspacePersistence.test.ts
index 4207b85a..de4fca93 100644
--- a/apps/web/src/api/localWorkspacePersistence.test.ts
+++ b/apps/web/src/api/localWorkspacePersistence.test.ts
@@ -2,22 +2,41 @@
import { afterEach, expect, it, vi } from "vitest";
import { readDraft, writeDraft } from "./conversation";
import { readWorkspaceContext, saveWorkspaceContext } from "./research";
-import { clearLocalPending, inspectLocalContext, inspectLocalDraft, readLocalPending, submissionSignature, writeLocalPending, type BrowserScope, type PendingSubmission } from "./localWorkspacePersistence";
+import {
+ clearLocalPending,
+ inspectLocalContext,
+ inspectLocalDraft,
+ readLocalPending,
+ submissionSignature,
+ writeLocalPending,
+ type BrowserScope,
+ type PendingSubmission,
+} from "./localWorkspacePersistence";
const a: BrowserScope = { mode: "LOCAL", workspaceId: `workspace:${"a".repeat(32)}` };
const b: BrowserScope = { mode: "LOCAL", workspaceId: `workspace:${"b".repeat(32)}` };
const hosted: BrowserScope = { mode: "HOSTED" };
-afterEach(() => { localStorage.clear(); sessionStorage.clear(); vi.restoreAllMocks(); });
+afterEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+ vi.restoreAllMocks();
+});
it("restores only the selected LOCAL workspace context after a browser restart", () => {
- sessionStorage.setItem("thoth:web-context:v1", JSON.stringify({ version: 1, projectId: "legacy", threadId: "legacy" }));
+ sessionStorage.setItem(
+ "thoth:web-context:v1",
+ JSON.stringify({ version: 1, projectId: "legacy", threadId: "legacy" }),
+ );
expect(saveWorkspaceContext("project-a", "thread-a", a)).toBe(true);
expect(saveWorkspaceContext("project-b", "thread-b", b)).toBe(true);
expect(readWorkspaceContext(a)).toEqual({ projectId: "project-a", threadId: "thread-a" });
expect(readWorkspaceContext(b)).toEqual({ projectId: "project-b", threadId: "thread-b" });
expect(readWorkspaceContext(hosted)).toMatchObject({ projectId: "legacy", threadId: "legacy" });
- expect(readWorkspaceContext({ mode: "LOCAL", workspaceId: null })).toEqual({ projectId: "", threadId: "" });
+ expect(readWorkspaceContext({ mode: "LOCAL", workspaceId: null })).toEqual({
+ projectId: "",
+ threadId: "",
+ });
});
it("isolates unsent drafts by workspace, project and thread without borrowing the Hosted draft", () => {
@@ -35,61 +54,130 @@ it("does not truncate oversized drafts or erase the last saved version on storag
expect(writeDraft("p", "t", "saved", a)).toBe("SAVED");
expect(writeDraft("p", "t", "x".repeat(20_001), a)).toBe("TOO_LARGE");
expect(readDraft("p", "t", a)).toBe("saved");
- vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => { throw new Error("quota"); });
+ vi.spyOn(Storage.prototype, "setItem").mockImplementation(() => {
+ throw new Error("quota");
+ });
expect(writeDraft("p", "t", "new text", a)).toBe("UNAVAILABLE");
expect(readDraft("p", "t", a)).toBe("saved");
});
it("ignores corrupt LOCAL context and draft records without consulting shared session keys", () => {
- localStorage.setItem(`thoth:local-workspace:v1:context:${a.mode === "LOCAL" ? a.workspaceId : ""}`, "{bad-json");
+ localStorage.setItem(
+ `thoth:local-workspace:v1:context:${a.mode === "LOCAL" ? a.workspaceId : ""}`,
+ "{bad-json",
+ );
expect(readWorkspaceContext(a)).toEqual({ projectId: "", threadId: "" });
expect(inspectLocalContext(a.mode === "LOCAL" ? a.workspaceId : null).kind).toBe("CORRUPT");
- localStorage.setItem(`thoth:local-workspace:v1:draft:${JSON.stringify([a.mode === "LOCAL" ? a.workspaceId : "", "p", "t"])}`, "{bad-json");
+ localStorage.setItem(
+ `thoth:local-workspace:v1:draft:${JSON.stringify([a.mode === "LOCAL" ? a.workspaceId : "", "p", "t"])}`,
+ "{bad-json",
+ );
expect(readDraft("p", "t", a)).toBe("");
- expect(inspectLocalDraft(a.mode === "LOCAL" ? a.workspaceId : null, "p", "t").kind).toBe("CORRUPT");
+ expect(inspectLocalDraft(a.mode === "LOCAL" ? a.workspaceId : null, "p", "t").kind).toBe(
+ "CORRUPT",
+ );
});
it("distinguishes unreadable browser storage from a missing or corrupt draft", () => {
const workspaceId = a.mode === "LOCAL" ? a.workspaceId : null;
expect(inspectLocalDraft(workspaceId, "p", "t").kind).toBe("MISSING");
- vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => { throw new Error("storage denied"); });
+ vi.spyOn(Storage.prototype, "getItem").mockImplementation(() => {
+ throw new Error("storage denied");
+ });
expect(inspectLocalDraft(workspaceId, "p", "t").kind).toBe("UNAVAILABLE");
expect(inspectLocalContext(workspaceId).kind).toBe("UNAVAILABLE");
});
it("pins a bounded pending key to one workspace and exact submitted payload", () => {
- const value: PendingSubmission = { version: 1, workspaceId: a.mode === "LOCAL" ? a.workspaceId! : "", projectId: "p", threadId: "t",
- problem: "synthetic question", selection: { provider: "test", model: "test", reasoning_effort: "high" },
- signature: submissionSignature("p", "t", "synthetic question", { provider: "test", model: "test", reasoning_effort: "high" }),
- key: "11111111-1111-4111-8111-111111111111", createdAt: 1 };
+ const value: PendingSubmission = {
+ version: 1,
+ workspaceId: a.mode === "LOCAL" ? a.workspaceId! : "",
+ projectId: "p",
+ threadId: "t",
+ problem: "synthetic question",
+ selection: { provider: "test", model: "test", reasoning_effort: "high" },
+ signature: submissionSignature("p", "t", "synthetic question", {
+ provider: "test",
+ model: "test",
+ reasoning_effort: "high",
+ }),
+ key: "11111111-1111-4111-8111-111111111111",
+ createdAt: 1,
+ };
expect(writeLocalPending(value)).toBe("SAVED");
- expect(readLocalPending(a.mode === "LOCAL" ? a.workspaceId : null, "p", "t")).toEqual({ kind: "PENDING", value });
- expect(readLocalPending(b.mode === "LOCAL" ? b.workspaceId : null, "p", "t")).toEqual({ kind: "NONE" });
- expect(readLocalPending(a.mode === "LOCAL" ? a.workspaceId : null, "p", "other")).toEqual({ kind: "NONE" });
+ expect(readLocalPending(a.mode === "LOCAL" ? a.workspaceId : null, "p", "t")).toEqual({
+ kind: "PENDING",
+ value,
+ });
+ expect(readLocalPending(b.mode === "LOCAL" ? b.workspaceId : null, "p", "t")).toEqual({
+ kind: "NONE",
+ });
+ expect(readLocalPending(a.mode === "LOCAL" ? a.workspaceId : null, "p", "other")).toEqual({
+ kind: "NONE",
+ });
expect(writeLocalPending({ ...value, signature: "different payload" })).toBe("UNAVAILABLE");
- expect(writeLocalPending({ ...value, problem: "x".repeat(20_001), signature: submissionSignature("p", "t", "x".repeat(20_001), value.selection) })).toBe("TOO_LARGE");
+ expect(
+ writeLocalPending({
+ ...value,
+ problem: "x".repeat(20_001),
+ signature: submissionSignature("p", "t", "x".repeat(20_001), value.selection),
+ }),
+ ).toBe("TOO_LARGE");
expect(clearLocalPending(a.mode === "LOCAL" ? a.workspaceId : null, "p", "t")).toBe(true);
- expect(readLocalPending(a.mode === "LOCAL" ? a.workspaceId : null, "p", "t")).toEqual({ kind: "NONE" });
+ expect(readLocalPending(a.mode === "LOCAL" ? a.workspaceId : null, "p", "t")).toEqual({
+ kind: "NONE",
+ });
});
it("keeps a trace-row origin with the pending request, and a different origin is a different request", () => {
const id = a.mode === "LOCAL" ? a.workspaceId : null;
- const origin = { kind: "TRACE_VERDICT" as const, project_id: "p", subject_kind: "CRITERION" as const, subject_id: "C-1", verdict_revision: "a".repeat(64) };
- const value: PendingSubmission = { version: 1, workspaceId: id!, projectId: "p", threadId: "t", problem: "synthetic question", selection: null, origin,
- signature: submissionSignature("p", "t", "synthetic question", null, origin), key: "11111111-1111-4111-8111-111111111111", createdAt: 1 };
+ const origin = {
+ kind: "TRACE_VERDICT" as const,
+ project_id: "p",
+ subject_kind: "CRITERION" as const,
+ subject_id: "C-1",
+ verdict_revision: "a".repeat(64),
+ };
+ const value: PendingSubmission = {
+ version: 1,
+ workspaceId: id!,
+ projectId: "p",
+ threadId: "t",
+ problem: "synthetic question",
+ selection: null,
+ origin,
+ signature: submissionSignature("p", "t", "synthetic question", null, origin),
+ key: "11111111-1111-4111-8111-111111111111",
+ createdAt: 1,
+ };
expect(writeLocalPending(value)).toBe("SAVED");
expect(readLocalPending(id, "p", "t")).toEqual({ kind: "PENDING", value });
- expect(submissionSignature("p", "t", "synthetic question", null, origin)).not.toBe(submissionSignature("p", "t", "synthetic question", null));
- expect(submissionSignature("p", "t", "synthetic question", null, null)).toBe(submissionSignature("p", "t", "synthetic question", null)); // no origin: the old signature
- expect(writeLocalPending({ ...value, signature: submissionSignature("p", "t", "synthetic question", null) })).toBe("UNAVAILABLE"); // the origin is part of the exact payload
+ expect(submissionSignature("p", "t", "synthetic question", null, origin)).not.toBe(
+ submissionSignature("p", "t", "synthetic question", null),
+ );
+ expect(submissionSignature("p", "t", "synthetic question", null, null)).toBe(
+ submissionSignature("p", "t", "synthetic question", null),
+ ); // no origin: the old signature
+ expect(
+ writeLocalPending({
+ ...value,
+ signature: submissionSignature("p", "t", "synthetic question", null),
+ }),
+ ).toBe("UNAVAILABLE"); // the origin is part of the exact payload
// a made-up origin shape is not a pending request
- localStorage.setItem(`thoth:local-workspace:v1:pending:${JSON.stringify([id, "p", "t"])}`, JSON.stringify({ ...value, origin: { ...origin, extra: 1 } }));
+ localStorage.setItem(
+ `thoth:local-workspace:v1:pending:${JSON.stringify([id, "p", "t"])}`,
+ JSON.stringify({ ...value, origin: { ...origin, extra: 1 } }),
+ );
expect(readLocalPending(id, "p", "t")).toEqual({ kind: "INVALID" });
clearLocalPending(id, "p", "t");
});
it("blocks a corrupt pending marker instead of assigning a new key", () => {
const workspaceId = a.mode === "LOCAL" ? a.workspaceId : "";
- localStorage.setItem(`thoth:local-workspace:v1:pending:${JSON.stringify([workspaceId, "p", "t"])}`, "{broken");
+ localStorage.setItem(
+ `thoth:local-workspace:v1:pending:${JSON.stringify([workspaceId, "p", "t"])}`,
+ "{broken",
+ );
expect(readLocalPending(workspaceId, "p", "t")).toEqual({ kind: "INVALID" });
});
diff --git a/apps/web/src/api/localWorkspacePersistence.ts b/apps/web/src/api/localWorkspacePersistence.ts
index c93f0020..e4fa5aa8 100644
--- a/apps/web/src/api/localWorkspacePersistence.ts
+++ b/apps/web/src/api/localWorkspacePersistence.ts
@@ -3,17 +3,42 @@ import { isResponseLanguage, type ResponseLanguage } from "./responseLanguages";
export type { ResponseLanguage };
/** Browser convenience only. Research records and permissions remain server-owned. */
-export type BrowserScope = { mode: "LOCAL"; workspaceId: string | null } | { mode: "HOSTED"; browserScopeId?: string | null };
+export type BrowserScope =
+ | { mode: "LOCAL"; workspaceId: string | null }
+ | { mode: "HOSTED"; browserScopeId?: string | null };
export type WorkspaceContext = { projectId: string; threadId: string };
-export type ContextReadResult = { kind: "MISSING" | "VALID" | "CORRUPT" | "UNAVAILABLE"; context: WorkspaceContext };
+export type ContextReadResult = {
+ kind: "MISSING" | "VALID" | "CORRUPT" | "UNAVAILABLE";
+ context: WorkspaceContext;
+};
export type DraftWriteResult = "SAVED" | "TOO_LARGE" | "UNAVAILABLE";
-export type DraftReadResult = { kind: "MISSING" | "VALID" | "CORRUPT" | "UNAVAILABLE"; text: string };
+export type DraftReadResult = {
+ kind: "MISSING" | "VALID" | "CORRUPT" | "UNAVAILABLE";
+ text: string;
+};
export type PendingModelSelection = { provider: string; model: string; reasoning_effort?: string };
-export type PendingOrigin = { kind: "TRACE_VERDICT"; project_id: string; subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; verdict_revision: string };
-type PendingFields = { workspaceId: string; projectId: string; threadId: string;
- problem: string; selection: PendingModelSelection | null; origin?: PendingOrigin | null; signature: string; key: string; createdAt: number };
-export type PendingSubmission = PendingFields & ({ version: 1; responseLanguage?: never } | { version: 2; responseLanguage: ResponseLanguage });
-export type PendingRead = { kind: "NONE" } | { kind: "PENDING"; value: PendingSubmission } | { kind: "INVALID" };
+export type PendingOrigin = {
+ kind: "TRACE_VERDICT";
+ project_id: string;
+ subject_kind: "CRITERION" | "REQUIREMENT";
+ subject_id: string;
+ verdict_revision: string;
+};
+type PendingFields = {
+ workspaceId: string;
+ projectId: string;
+ threadId: string;
+ problem: string;
+ selection: PendingModelSelection | null;
+ origin?: PendingOrigin | null;
+ signature: string;
+ key: string;
+ createdAt: number;
+};
+export type PendingSubmission = PendingFields &
+ ({ version: 1; responseLanguage?: never } | { version: 2; responseLanguage: ResponseLanguage });
+export type PendingRead =
+ { kind: "NONE" } | { kind: "PENDING"; value: PendingSubmission } | { kind: "INVALID" };
const prefix = "thoth:local-workspace:v1:";
const maxDraftLength = 20_000;
@@ -22,14 +47,25 @@ const maxIdentityLength = 160;
const emptyContext: WorkspaceContext = { projectId: "", threadId: "" };
export function validWorkspaceId(value: string | null | undefined): value is string {
- return typeof value === "string" && value.length >= 8 && value.length <= maxIdentityLength && /^[a-zA-Z0-9:_-]+$/.test(value);
+ return (
+ typeof value === "string" &&
+ value.length >= 8 &&
+ value.length <= maxIdentityLength &&
+ /^[a-zA-Z0-9:_-]+$/.test(value)
+ );
}
function validRecordId(value: unknown): value is string {
- return typeof value === "string" && value.length <= maxIdentityLength && Array.from(value).every(char => char.charCodeAt(0) >= 32);
+ return (
+ typeof value === "string" &&
+ value.length <= maxIdentityLength &&
+ Array.from(value).every((char) => char.charCodeAt(0) >= 32)
+ );
}
-function contextKey(workspaceId: string) { return `${prefix}context:${workspaceId}`; }
+function contextKey(workspaceId: string) {
+ return `${prefix}context:${workspaceId}`;
+}
function draftKey(workspaceId: string, projectId: string, threadId: string) {
return `${prefix}draft:${JSON.stringify([workspaceId, projectId, threadId])}`;
}
@@ -38,27 +74,67 @@ function pendingKey(workspaceId: string, projectId: string, threadId: string) {
}
/** The exact payload of a request; a request that starts from a trace row has that row in it, any other request keeps its old signature. */
-export function submissionSignature(projectId: string, threadId: string, problem: string, selection: PendingModelSelection | null, origin: PendingOrigin | null = null, responseLanguage?: ResponseLanguage): string {
- if (responseLanguage !== undefined) return JSON.stringify(["response-language:v2", submissionSignature(projectId, threadId, problem, selection, origin), responseLanguage]);
- return JSON.stringify([projectId, threadId, problem, selection ? [selection.provider, selection.model, selection.reasoning_effort ?? null] : null,
- ...(origin ? [[origin.kind, origin.project_id, origin.subject_kind, origin.subject_id, origin.verdict_revision]] : [])]);
+export function submissionSignature(
+ projectId: string,
+ threadId: string,
+ problem: string,
+ selection: PendingModelSelection | null,
+ origin: PendingOrigin | null = null,
+ responseLanguage?: ResponseLanguage,
+): string {
+ if (responseLanguage !== undefined)
+ return JSON.stringify([
+ "response-language:v2",
+ submissionSignature(projectId, threadId, problem, selection, origin),
+ responseLanguage,
+ ]);
+ return JSON.stringify([
+ projectId,
+ threadId,
+ problem,
+ selection ? [selection.provider, selection.model, selection.reasoning_effort ?? null] : null,
+ ...(origin
+ ? [
+ [
+ origin.kind,
+ origin.project_id,
+ origin.subject_kind,
+ origin.subject_id,
+ origin.verdict_revision,
+ ],
+ ]
+ : []),
+ ]);
}
export function inspectLocalContext(workspaceId: string | null): ContextReadResult {
if (!validWorkspaceId(workspaceId)) return { kind: "UNAVAILABLE", context: { ...emptyContext } };
let raw: string | null;
- try { raw = localStorage.getItem(contextKey(workspaceId)); }
- catch { return { kind: "UNAVAILABLE", context: { ...emptyContext } }; }
+ try {
+ raw = localStorage.getItem(contextKey(workspaceId));
+ } catch {
+ return { kind: "UNAVAILABLE", context: { ...emptyContext } };
+ }
if (raw === null) return { kind: "MISSING", context: { ...emptyContext } };
try {
const value: unknown = JSON.parse(raw);
- if (value && typeof value === "object" && "version" in value && value.version === 1 &&
- "workspaceId" in value && value.workspaceId === workspaceId &&
- "projectId" in value && validRecordId(value.projectId) &&
- "threadId" in value && validRecordId(value.threadId)) {
+ if (
+ value &&
+ typeof value === "object" &&
+ "version" in value &&
+ value.version === 1 &&
+ "workspaceId" in value &&
+ value.workspaceId === workspaceId &&
+ "projectId" in value &&
+ validRecordId(value.projectId) &&
+ "threadId" in value &&
+ validRecordId(value.threadId)
+ ) {
return { kind: "VALID", context: { projectId: value.projectId, threadId: value.threadId } };
}
- } catch { /* Preserve the raw record for an explicit user choice. */ }
+ } catch {
+ /* Preserve the raw record for an explicit user choice. */
+ }
return { kind: "CORRUPT", context: { ...emptyContext } };
}
@@ -67,101 +143,262 @@ export function readLocalContext(workspaceId: string | null): WorkspaceContext {
}
export function writeLocalContext(workspaceId: string | null, context: WorkspaceContext): boolean {
- if (!validWorkspaceId(workspaceId) || !validRecordId(context.projectId) || !validRecordId(context.threadId)) return false;
+ if (
+ !validWorkspaceId(workspaceId) ||
+ !validRecordId(context.projectId) ||
+ !validRecordId(context.threadId)
+ )
+ return false;
try {
- localStorage.setItem(contextKey(workspaceId), JSON.stringify({ version: 1, workspaceId, ...context }));
+ localStorage.setItem(
+ contextKey(workspaceId),
+ JSON.stringify({ version: 1, workspaceId, ...context }),
+ );
return true;
- } catch { return false; }
+ } catch {
+ return false;
+ }
}
-export function inspectLocalDraft(workspaceId: string | null, projectId: string, threadId: string): DraftReadResult {
- if (!validWorkspaceId(workspaceId) || !validRecordId(projectId) || !validRecordId(threadId)) return { kind: "UNAVAILABLE", text: "" };
+export function inspectLocalDraft(
+ workspaceId: string | null,
+ projectId: string,
+ threadId: string,
+): DraftReadResult {
+ if (!validWorkspaceId(workspaceId) || !validRecordId(projectId) || !validRecordId(threadId))
+ return { kind: "UNAVAILABLE", text: "" };
let raw: string | null;
- try { raw = localStorage.getItem(draftKey(workspaceId, projectId, threadId)); }
- catch { return { kind: "UNAVAILABLE", text: "" }; }
+ try {
+ raw = localStorage.getItem(draftKey(workspaceId, projectId, threadId));
+ } catch {
+ return { kind: "UNAVAILABLE", text: "" };
+ }
if (raw === null) return { kind: "MISSING", text: "" };
try {
const value: unknown = JSON.parse(raw);
- if (value && typeof value === "object" && "version" in value && value.version === 1 &&
- "workspaceId" in value && value.workspaceId === workspaceId &&
- "projectId" in value && value.projectId === projectId &&
- "threadId" in value && value.threadId === threadId &&
- "text" in value && typeof value.text === "string" && value.text.length <= maxDraftLength) return { kind: "VALID", text: value.text };
- } catch { /* Keep the current editor usable and preserve the raw record. */ }
+ if (
+ value &&
+ typeof value === "object" &&
+ "version" in value &&
+ value.version === 1 &&
+ "workspaceId" in value &&
+ value.workspaceId === workspaceId &&
+ "projectId" in value &&
+ value.projectId === projectId &&
+ "threadId" in value &&
+ value.threadId === threadId &&
+ "text" in value &&
+ typeof value.text === "string" &&
+ value.text.length <= maxDraftLength
+ )
+ return { kind: "VALID", text: value.text };
+ } catch {
+ /* Keep the current editor usable and preserve the raw record. */
+ }
return { kind: "CORRUPT", text: "" };
}
-export function readLocalDraft(workspaceId: string | null, projectId: string, threadId: string): string {
+export function readLocalDraft(
+ workspaceId: string | null,
+ projectId: string,
+ threadId: string,
+): string {
return inspectLocalDraft(workspaceId, projectId, threadId).text;
}
-export function writeLocalDraft(workspaceId: string | null, projectId: string, threadId: string, text: string): DraftWriteResult {
+export function writeLocalDraft(
+ workspaceId: string | null,
+ projectId: string,
+ threadId: string,
+ text: string,
+): DraftWriteResult {
if (text.length > maxDraftLength) return "TOO_LARGE";
- if (!validWorkspaceId(workspaceId) || !validRecordId(projectId) || !validRecordId(threadId)) return "UNAVAILABLE";
+ if (!validWorkspaceId(workspaceId) || !validRecordId(projectId) || !validRecordId(threadId))
+ return "UNAVAILABLE";
try {
const key = draftKey(workspaceId, projectId, threadId);
- if (text) localStorage.setItem(key, JSON.stringify({ version: 1, workspaceId, projectId, threadId, text }));
+ if (text)
+ localStorage.setItem(
+ key,
+ JSON.stringify({ version: 1, workspaceId, projectId, threadId, text }),
+ );
else localStorage.removeItem(key);
return "SAVED";
- } catch { return "UNAVAILABLE"; }
+ } catch {
+ return "UNAVAILABLE";
+ }
}
-export function readLocalPending(workspaceId: string | null, projectId: string, threadId: string, storage?: Storage): PendingRead {
- if (!validWorkspaceId(workspaceId) || !projectId || !validRecordId(projectId) || !validRecordId(threadId)) return { kind: "NONE" };
+export function readLocalPending(
+ workspaceId: string | null,
+ projectId: string,
+ threadId: string,
+ storage?: Storage,
+): PendingRead {
+ if (
+ !validWorkspaceId(workspaceId) ||
+ !projectId ||
+ !validRecordId(projectId) ||
+ !validRecordId(threadId)
+ )
+ return { kind: "NONE" };
try {
const raw = (storage ?? localStorage).getItem(pendingKey(workspaceId, projectId, threadId));
if (raw === null) return { kind: "NONE" };
if (raw.length > maxPendingLength) return { kind: "INVALID" };
const value: unknown = JSON.parse(raw);
- if (!value || typeof value !== "object" || !("version" in value) || (value.version !== 1 && value.version !== 2) ||
- !Object.keys(value).every(key => ["version", "workspaceId", "projectId", "threadId", "problem", "selection", "origin", "signature", "key", "createdAt", ...(value.version === 2 ? ["responseLanguage"] : [])].includes(key)) ||
- !("workspaceId" in value) || value.workspaceId !== workspaceId ||
- !("projectId" in value) || value.projectId !== projectId ||
- !("threadId" in value) || value.threadId !== threadId ||
- !("problem" in value) || typeof value.problem !== "string" || value.problem.length > maxDraftLength ||
- !("selection" in value) || !(value.selection === null || validPendingSelection(value.selection)) ||
+ if (
+ !value ||
+ typeof value !== "object" ||
+ !("version" in value) ||
+ (value.version !== 1 && value.version !== 2) ||
+ !Object.keys(value).every((key) =>
+ [
+ "version",
+ "workspaceId",
+ "projectId",
+ "threadId",
+ "problem",
+ "selection",
+ "origin",
+ "signature",
+ "key",
+ "createdAt",
+ ...(value.version === 2 ? ["responseLanguage"] : []),
+ ].includes(key),
+ ) ||
+ !("workspaceId" in value) ||
+ value.workspaceId !== workspaceId ||
+ !("projectId" in value) ||
+ value.projectId !== projectId ||
+ !("threadId" in value) ||
+ value.threadId !== threadId ||
+ !("problem" in value) ||
+ typeof value.problem !== "string" ||
+ value.problem.length > maxDraftLength ||
+ !("selection" in value) ||
+ !(value.selection === null || validPendingSelection(value.selection)) ||
("origin" in value && !(value.origin === null || validPendingOrigin(value.origin))) ||
- !("signature" in value) || typeof value.signature !== "string" ||
- !("key" in value) || typeof value.key !== "string" || !/^[a-f0-9-]{36}$/i.test(value.key) ||
- !("createdAt" in value) || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return { kind: "INVALID" };
- if (value.version === 2 && (!("responseLanguage" in value) || !isResponseLanguage(value.responseLanguage))) return { kind: "INVALID" };
+ !("signature" in value) ||
+ typeof value.signature !== "string" ||
+ !("key" in value) ||
+ typeof value.key !== "string" ||
+ !/^[a-f0-9-]{36}$/i.test(value.key) ||
+ !("createdAt" in value) ||
+ typeof value.createdAt !== "number" ||
+ !Number.isFinite(value.createdAt)
+ )
+ return { kind: "INVALID" };
+ if (
+ value.version === 2 &&
+ (!("responseLanguage" in value) || !isResponseLanguage(value.responseLanguage))
+ )
+ return { kind: "INVALID" };
const pending = value as PendingSubmission;
- return pending.signature === submissionSignature(projectId, threadId, pending.problem, pending.selection, pending.origin ?? null, pending.responseLanguage)
- ? { kind: "PENDING", value: pending } : { kind: "INVALID" };
- } catch { return { kind: "INVALID" }; }
+ return pending.signature ===
+ submissionSignature(
+ projectId,
+ threadId,
+ pending.problem,
+ pending.selection,
+ pending.origin ?? null,
+ pending.responseLanguage,
+ )
+ ? { kind: "PENDING", value: pending }
+ : { kind: "INVALID" };
+ } catch {
+ return { kind: "INVALID" };
+ }
}
function validPendingOrigin(value: unknown): value is PendingOrigin {
- const text = (item: unknown, max: number) => typeof item === "string" && item.length > 0 && item.length <= max;
- return Boolean(value && typeof value === "object" && !Array.isArray(value) &&
- Object.keys(value).every(key => ["kind", "project_id", "subject_kind", "subject_id", "verdict_revision"].includes(key)) &&
- "kind" in value && value.kind === "TRACE_VERDICT" && "project_id" in value && text(value.project_id, maxIdentityLength) &&
- "subject_kind" in value && (value.subject_kind === "CRITERION" || value.subject_kind === "REQUIREMENT") &&
- "subject_id" in value && text(value.subject_id, 500) && "verdict_revision" in value && text(value.verdict_revision, 64));
+ const text = (item: unknown, max: number) =>
+ typeof item === "string" && item.length > 0 && item.length <= max;
+ return Boolean(
+ value &&
+ typeof value === "object" &&
+ !Array.isArray(value) &&
+ Object.keys(value).every((key) =>
+ ["kind", "project_id", "subject_kind", "subject_id", "verdict_revision"].includes(key),
+ ) &&
+ "kind" in value &&
+ value.kind === "TRACE_VERDICT" &&
+ "project_id" in value &&
+ text(value.project_id, maxIdentityLength) &&
+ "subject_kind" in value &&
+ (value.subject_kind === "CRITERION" || value.subject_kind === "REQUIREMENT") &&
+ "subject_id" in value &&
+ text(value.subject_id, 500) &&
+ "verdict_revision" in value &&
+ text(value.verdict_revision, 64),
+ );
}
function validPendingSelection(value: unknown): value is PendingModelSelection {
- return Boolean(value && typeof value === "object" && !Array.isArray(value) &&
- Object.keys(value).every(key => ["provider", "model", "reasoning_effort"].includes(key)) &&
- "provider" in value && typeof value.provider === "string" && value.provider.length > 0 && value.provider.length <= 160 &&
- "model" in value && typeof value.model === "string" && value.model.length > 0 && value.model.length <= 160 &&
- (!("reasoning_effort" in value) || typeof value.reasoning_effort === "string" && value.reasoning_effort.length <= 32));
+ return Boolean(
+ value &&
+ typeof value === "object" &&
+ !Array.isArray(value) &&
+ Object.keys(value).every((key) => ["provider", "model", "reasoning_effort"].includes(key)) &&
+ "provider" in value &&
+ typeof value.provider === "string" &&
+ value.provider.length > 0 &&
+ value.provider.length <= 160 &&
+ "model" in value &&
+ typeof value.model === "string" &&
+ value.model.length > 0 &&
+ value.model.length <= 160 &&
+ (!("reasoning_effort" in value) ||
+ (typeof value.reasoning_effort === "string" && value.reasoning_effort.length <= 32)),
+ );
}
export function writeLocalPending(value: PendingSubmission, storage?: Storage): DraftWriteResult {
if (value.problem.length > maxDraftLength) return "TOO_LARGE";
- if (!validWorkspaceId(value.workspaceId) || !value.projectId || !validRecordId(value.projectId) || !validRecordId(value.threadId) ||
- !(value.selection === null || validPendingSelection(value.selection)) || !(value.origin == null || validPendingOrigin(value.origin)) ||
- value.signature !== submissionSignature(value.projectId, value.threadId, value.problem, value.selection, value.origin ?? null, value.responseLanguage)) return "UNAVAILABLE";
+ if (
+ !validWorkspaceId(value.workspaceId) ||
+ !value.projectId ||
+ !validRecordId(value.projectId) ||
+ !validRecordId(value.threadId) ||
+ !(value.selection === null || validPendingSelection(value.selection)) ||
+ !(value.origin == null || validPendingOrigin(value.origin)) ||
+ value.signature !==
+ submissionSignature(
+ value.projectId,
+ value.threadId,
+ value.problem,
+ value.selection,
+ value.origin ?? null,
+ value.responseLanguage,
+ )
+ )
+ return "UNAVAILABLE";
if (value.version === 2 && !isResponseLanguage(value.responseLanguage)) return "UNAVAILABLE";
const raw = JSON.stringify(value);
if (raw.length > maxPendingLength) return "TOO_LARGE";
- try { (storage ?? localStorage).setItem(pendingKey(value.workspaceId, value.projectId, value.threadId), raw); return "SAVED"; }
- catch { return "UNAVAILABLE"; }
+ try {
+ (storage ?? localStorage).setItem(
+ pendingKey(value.workspaceId, value.projectId, value.threadId),
+ raw,
+ );
+ return "SAVED";
+ } catch {
+ return "UNAVAILABLE";
+ }
}
-export function clearLocalPending(workspaceId: string | null, projectId: string, threadId: string, storage?: Storage): boolean {
- if (!validWorkspaceId(workspaceId) || !validRecordId(projectId) || !validRecordId(threadId)) return false;
- try { (storage ?? localStorage).removeItem(pendingKey(workspaceId, projectId, threadId)); return true; }
- catch { return false; }
+export function clearLocalPending(
+ workspaceId: string | null,
+ projectId: string,
+ threadId: string,
+ storage?: Storage,
+): boolean {
+ if (!validWorkspaceId(workspaceId) || !validRecordId(projectId) || !validRecordId(threadId))
+ return false;
+ try {
+ (storage ?? localStorage).removeItem(pendingKey(workspaceId, projectId, threadId));
+ return true;
+ } catch {
+ return false;
+ }
}
diff --git a/apps/web/src/api/parserError.test.ts b/apps/web/src/api/parserError.test.ts
index 9cadbf1f..d4337f39 100644
--- a/apps/web/src/api/parserError.test.ts
+++ b/apps/web/src/api/parserError.test.ts
@@ -8,11 +8,18 @@ const RAW_CODE = /[A-Z]{3,}_[A-Z_]{3,}/;
/** The values of ParserErrorCode, read from the server source. The public schemas under
* schemas/protocol do not list them, so this file is the one place the web test can read them from. */
function serverCodes(): string[] {
- const source = readFileSync(new URL("../../../../src/thoth/domain/enums.py", import.meta.url), "utf8");
- const block = /class ParserErrorCode\(StrEnum\):\r?\n([\s\S]*?)\r?\n\r?\n/.exec(source)?.[1] ?? "";
- return [...block.matchAll(/^\s+[A-Z_]+ = "([A-Z_]+)"/gm)].map(match => match[1]);
+ const source = readFileSync(
+ new URL("../../../../src/thoth/domain/enums.py", import.meta.url),
+ "utf8",
+ );
+ const block =
+ /class ParserErrorCode\(StrEnum\):\r?\n([\s\S]*?)\r?\n\r?\n/.exec(source)?.[1] ?? "";
+ return [...block.matchAll(/^\s+[A-Z_]+ = "([A-Z_]+)"/gm)].map((match) => match[1]);
}
-const failure = (code: unknown) => ({ details: { parser_error: code }, message: "source parsing failed [-32001]" });
+const failure = (code: unknown) => ({
+ details: { parser_error: code },
+ message: "source parsing failed [-32001]",
+});
afterEach(() => setLocale("ko"));
@@ -23,15 +30,18 @@ describe("parser error sentences", () => {
expect([...PARSER_ERROR_CODES].sort()).toEqual([...server].sort());
});
- it.each(["ko", "en"] as const)("gives each code a plain %s sentence without the raw code", locale => {
- setLocale(locale);
- for (const code of PARSER_ERROR_CODES) {
- const sentence = parserFailureText(failure(code))!.text;
- expect(sentence.length, code).toBeGreaterThan(15);
- expect(sentence, code).not.toMatch(RAW_CODE);
- expect(sentence, code).toBe(t(`parserError.${code}` as MessageKey));
- }
- });
+ it.each(["ko", "en"] as const)(
+ "gives each code a plain %s sentence without the raw code",
+ (locale) => {
+ setLocale(locale);
+ for (const code of PARSER_ERROR_CODES) {
+ const sentence = parserFailureText(failure(code))!.text;
+ expect(sentence.length, code).toBeGreaterThan(15);
+ expect(sentence, code).not.toMatch(RAW_CODE);
+ expect(sentence, code).toBe(t(`parserError.${code}` as MessageKey));
+ }
+ },
+ );
it("tells the person what to do about an encoding that cannot be confirmed", () => {
expect(parserFailureText(failure("TEXT_ENCODING_UNCONFIRMED"))).toEqual({
@@ -39,7 +49,9 @@ describe("parser error sentences", () => {
text: "글자 인코딩을 확인하지 못했습니다. 파일을 UTF-8로 저장해 다시 올려 주세요.",
});
setLocale("en");
- expect(parserFailureText(failure("TEXT_ENCODING_UNCONFIRMED"))!.text).toContain("Save the file as UTF-8");
+ expect(parserFailureText(failure("TEXT_ENCODING_UNCONFIRMED"))!.text).toContain(
+ "Save the file as UTF-8",
+ );
});
it("reads a code it has no sentence for as the general sentence and keeps the code", () => {
@@ -48,8 +60,15 @@ describe("parser error sentences", () => {
expect(result.text).toBe(t("parserError.unknown"));
});
- it.each([undefined, null, "text", { details: null }, { details: {} }, { details: { parser_error: "" } }, failure(7)])(
- "returns nothing when the error carries no parser reason (%j)",
- error => expect(parserFailureText(error)).toBeNull(),
+ it.each([
+ undefined,
+ null,
+ "text",
+ { details: null },
+ { details: {} },
+ { details: { parser_error: "" } },
+ failure(7),
+ ])("returns nothing when the error carries no parser reason (%j)", (error) =>
+ expect(parserFailureText(error)).toBeNull(),
);
});
diff --git a/apps/web/src/api/parserError.ts b/apps/web/src/api/parserError.ts
index f3ad3b9f..80cd76e8 100644
--- a/apps/web/src/api/parserError.ts
+++ b/apps/web/src/api/parserError.ts
@@ -6,8 +6,8 @@ const UNKNOWN_KEY = "parserError.unknown" satisfies MessageKey;
/** The parser reason codes the screen has a sentence for. */
export const PARSER_ERROR_CODES: readonly string[] = Object.keys(messages)
- .filter(key => key.startsWith(PREFIX) && key !== UNKNOWN_KEY)
- .map(key => key.slice(PREFIX.length));
+ .filter((key) => key.startsWith(PREFIX) && key !== UNKNOWN_KEY)
+ .map((key) => key.slice(PREFIX.length));
export type ParserFailureText = { code: string; text: string };
@@ -15,7 +15,13 @@ export type ParserFailureText = { code: string; text: string };
* the general sentence; the code itself stays available for the technical details. */
export function parserFailureText(error: unknown): ParserFailureText | null {
const details = (error as { details?: unknown } | null)?.details;
- const code = details && typeof details === "object" ? (details as Record).parser_error : undefined;
+ const code =
+ details && typeof details === "object"
+ ? (details as Record).parser_error
+ : undefined;
if (typeof code !== "string" || code === "") return null;
- return { code, text: t(PARSER_ERROR_CODES.includes(code) ? PREFIX + code as MessageKey : UNKNOWN_KEY) };
+ return {
+ code,
+ text: t(PARSER_ERROR_CODES.includes(code) ? ((PREFIX + code) as MessageKey) : UNKNOWN_KEY),
+ };
}
diff --git a/apps/web/src/api/presentation.ts b/apps/web/src/api/presentation.ts
index 6f064da2..a5744dd2 100644
--- a/apps/web/src/api/presentation.ts
+++ b/apps/web/src/api/presentation.ts
@@ -1,8 +1,19 @@
-export const objectValue = (value: unknown): Record => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record : {};
-export const objectList = (value: unknown): Record[] => Array.isArray(value) ? value.map(objectValue) : [];
-export const textValue = (value: unknown): string => typeof value === "string" ? value : "";
-export const stringValues = (value: unknown): string[] => Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
+export const objectValue = (value: unknown): Record =>
+ value !== null && typeof value === "object" && !Array.isArray(value)
+ ? (value as Record)
+ : {};
+export const objectList = (value: unknown): Record[] =>
+ Array.isArray(value) ? value.map(objectValue) : [];
+export const textValue = (value: unknown): string => (typeof value === "string" ? value : "");
+export const stringValues = (value: unknown): string[] =>
+ Array.isArray(value) ? value.filter((v): v is string => typeof v === "string") : [];
-export function hasResearchContent(result: Record | null | undefined): boolean {
- return Boolean(result && (textValue(result.answer).trim() || objectList(objectValue(result.portfolio).hypotheses).length || objectList(objectValue(result.action_plan).alternatives).length || stringValues(objectValue(result.assessment).missing_items).length));
+export function hasResearchContent(result: Record | null | undefined): boolean {
+ return Boolean(
+ result &&
+ (textValue(result.answer).trim() ||
+ objectList(objectValue(result.portfolio).hypotheses).length ||
+ objectList(objectValue(result.action_plan).alternatives).length ||
+ stringValues(objectValue(result.assessment).missing_items).length),
+ );
}
diff --git a/apps/web/src/api/progressEvidenceLocations.test.tsx b/apps/web/src/api/progressEvidenceLocations.test.tsx
index 5612cf15..bb38ae69 100644
--- a/apps/web/src/api/progressEvidenceLocations.test.tsx
+++ b/apps/web/src/api/progressEvidenceLocations.test.tsx
@@ -7,22 +7,57 @@ import { composerActivity, evidenceLabels, liveResearchProgress } from "./resear
afterEach(() => setLocale("ko"));
-function source(id: string, locator: UserActivityTarget["locator"], access: UserActivityTarget["access_state"] = "allowed"): UserActivityEvent {
- return { schema_version: "thoth.user_activity_event.v1", event_id: `event:${id}`, seq: 1,
- severity: "info", visibility: "default", announce: "none", activity_kind: "source", action: "read",
- label_ko: "연결된 자료를 읽었습니다.", state: "succeeded", research_relation: "read",
+function source(
+ id: string,
+ locator: UserActivityTarget["locator"],
+ access: UserActivityTarget["access_state"] = "allowed",
+): UserActivityEvent {
+ return {
+ schema_version: "thoth.user_activity_event.v1",
+ event_id: `event:${id}`,
+ seq: 1,
+ severity: "info",
+ visibility: "default",
+ announce: "none",
+ activity_kind: "source",
+ action: "read",
+ label_ko: "연결된 자료를 읽었습니다.",
+ state: "succeeded",
+ research_relation: "read",
target: { kind: "span", title: "연결된 자료", locator, access_state: access },
- refs: { source_ref: `source:${id}` }, redaction: { applied: true, source_content_included: false },
+ refs: { source_ref: `source:${id}` },
+ redaction: { applied: true, source_content_included: false },
};
}
function status(text: string, activity?: UserActivityEvent[]): ResearchStatus {
- return { project_id: "p", thread_id: "t", cycle_id: "c", problem: "Inspect the test result",
- lifecycle: "ACTIVE", execution_state: "RUNNING", current_object_ids: [], working_head_digest: "0".repeat(64),
- operation_state: "RUNNING", attempt: { operation_id: "o", phase: "EVIDENCE_FOCUS", status: "RUNNING",
- draft_progress: { evidence_focus: { locators: [{ span_id: "span:1", page: 6, exact_text: text }], total: 1 } } },
+ return {
+ project_id: "p",
+ thread_id: "t",
+ cycle_id: "c",
+ problem: "Inspect the test result",
+ lifecycle: "ACTIVE",
+ execution_state: "RUNNING",
+ current_object_ids: [],
+ working_head_digest: "0".repeat(64),
+ operation_state: "RUNNING",
+ attempt: {
+ operation_id: "o",
+ phase: "EVIDENCE_FOCUS",
+ status: "RUNNING",
+ draft_progress: {
+ evidence_focus: { locators: [{ span_id: "span:1", page: 6, exact_text: text }], total: 1 },
+ },
+ },
completed_stages: [{ role: "EVIDENCE_RERANKER", state: "COMPLETED" }],
- activity_events: [{ seq: 7, kind: "action", action_type: "source_read", payload: { span_id: "span:1", page: 6, exact_text: text } }],
+ activity_events: [
+ {
+ seq: 7,
+ kind: "action",
+ action_type: "source_read",
+ payload: { span_id: "span:1", page: 6, exact_text: text },
+ },
+ ],
...(activity === undefined ? {} : { user_activity_events: activity }),
};
}
@@ -34,17 +69,19 @@ const texts = [
"일반 사용자 파일의 긴 첫 줄과 표 설명입니다. ".repeat(12),
];
-describe.each(["ko", "en"] as const)("progress-only source presentation (%s)", language => {
- it.each(texts)("does not render the original excerpt: %s", text => {
+describe.each(["ko", "en"] as const)("progress-only source presentation (%s)", (language) => {
+ it.each(texts)("does not render the original excerpt: %s", (text) => {
setLocale(language);
const input = status(text, [source("1", { line_start: 7, line_end: 7 })]);
const saved = structuredClone(input);
const live = liveResearchProgress(input)!;
expect(live.evidenceLabels).toEqual([language === "ko" ? "7행" : "line 7"]);
- const html = renderToStaticMarkup();
+ const html = renderToStaticMarkup();
expect(html).not.toContain(text.split("\n")[0]);
expect(JSON.stringify(live)).not.toContain(text.split("\n")[0]);
- expect(live.events.find(event => event.actionType === "source_read")?.payload).toBe(language === "ko" ? "위치 미확인" : "Location unavailable");
+ expect(live.events.find((event) => event.actionType === "source_read")?.payload).toBe(
+ language === "ko" ? "위치 미확인" : "Location unavailable",
+ );
expect(composerActivity(input)).toBe(live.headline);
expect(input).toEqual(saved);
});
@@ -54,29 +91,49 @@ describe.each(["ko", "en"] as const)("progress-only source presentation (%s)", l
const input = status("ORIGINAL_SOURCE_MUST_STAY_STORED");
expect(evidenceLabels(input)).toEqual(["p.6"]);
const live = liveResearchProgress(input)!;
- expect(live.events.find(event => event.actionType === "source_read")).toMatchObject({ id: "event-action-7", payload: "p.6" });
+ expect(live.events.find((event) => event.actionType === "source_read")).toMatchObject({
+ id: "event-action-7",
+ payload: "p.6",
+ });
expect(JSON.stringify(live)).not.toContain("ORIGINAL_SOURCE_MUST_STAY_STORED");
- input.attempt!.draft_progress = { evidence_focus: { locators: [{ exact_text: "ORIGINAL_SOURCE_MUST_STAY_STORED" }], total: 1 } };
- expect(evidenceLabels(input)).toEqual([language === "ko" ? "원문 위치 1" : "Source location 1"]);
+ input.attempt!.draft_progress = {
+ evidence_focus: { locators: [{ exact_text: "ORIGINAL_SOURCE_MUST_STAY_STORED" }], total: 1 },
+ };
+ expect(evidenceLabels(input)).toEqual([
+ language === "ko" ? "원문 위치 1" : "Source location 1",
+ ]);
});
- it.each(["blocked", "out_of_scope", "unknown"] as const)("does not revive location metadata for %s access", access => {
- setLocale(language);
- const input = status("SECRET_SOURCE", [source("1", { page: 99, line_start: 777, cell: "A3" }, access)]);
- const live = liveResearchProgress(input)!;
- expect(live.evidenceLabels).toEqual([language === "ko" ? "위치 미확인" : "Location unavailable"]);
- expect(JSON.stringify(live)).not.toMatch(/SECRET_SOURCE|p\.99|777|A3|p\.6/);
- });
+ it.each(["blocked", "out_of_scope", "unknown"] as const)(
+ "does not revive location metadata for %s access",
+ (access) => {
+ setLocale(language);
+ const input = status("SECRET_SOURCE", [
+ source("1", { page: 99, line_start: 777, cell: "A3" }, access),
+ ]);
+ const live = liveResearchProgress(input)!;
+ expect(live.evidenceLabels).toEqual([
+ language === "ko" ? "위치 미확인" : "Location unavailable",
+ ]);
+ expect(JSON.stringify(live)).not.toMatch(/SECRET_SOURCE|p\.99|777|A3|p\.6/);
+ },
+ );
});
it("does not match typed sources to legacy source actions by array position", () => {
const input = status("ORIGINAL", [source("different", { page: 99 })]);
- const action = liveResearchProgress(input)!.events.find(event => event.actionType === "source_read")!;
+ const action = liveResearchProgress(input)!.events.find(
+ (event) => event.actionType === "source_read",
+ )!;
expect(action).toMatchObject({ id: "event-action-7", payload: "위치 미확인" });
});
it("keeps distinct sources with the same location and deduplicates repeated identity", () => {
- const input = status("ORIGINAL", [source("a", { line_start: 7 }), source("b", { line_start: 7 }), source("a", { line_start: 7 })]);
+ const input = status("ORIGINAL", [
+ source("a", { line_start: 7 }),
+ source("b", { line_start: 7 }),
+ source("a", { line_start: 7 }),
+ ]);
expect(evidenceLabels(input)).toEqual(["7행", "7행"]);
});
@@ -86,17 +143,30 @@ it("treats conflicting or denied copies of the same source conservatively", () =
});
it("uses at most two bounded locations in both current and completed-stage titles", () => {
- const input = status("ORIGINAL", [source("a", { page: 1 }), source("b", { page: 2 }), source("c", { page: 3 })]);
+ const input = status("ORIGINAL", [
+ source("a", { page: 1 }),
+ source("b", { page: 2 }),
+ source("c", { page: 3 }),
+ ]);
input.attempt!.draft_progress = { evidence_focus: { total: 18, truncated: true } };
const live = liveResearchProgress(input)!;
expect(live.evidenceLabels).toEqual(["p.1", "p.2", "p.3"]);
expect(live.headline).toContain("p.1, p.2");
expect(live.headline).not.toContain("p.3");
- expect(live.events.filter(e => e.kind === "note" && !e.live).every(e => !e.text.includes("p.3"))).toBe(true);
- const html = renderToStaticMarkup();
+ expect(
+ live.events.filter((e) => e.kind === "note" && !e.live).every((e) => !e.text.includes("p.3")),
+ ).toBe(true);
+ const html = renderToStaticMarkup();
expect(html).toContain("외 1곳");
expect(html).toContain("전체 18곳 중 3곳 표시");
- input.user_activity_events = [source("large", { page: Number.MAX_SAFE_INTEGER, line_start: 1, line_end: Number.MAX_SAFE_INTEGER, cell: `AAAA${"9".repeat(60)}` })];
+ input.user_activity_events = [
+ source("large", {
+ page: Number.MAX_SAFE_INTEGER,
+ line_start: 1,
+ line_end: Number.MAX_SAFE_INTEGER,
+ cell: `AAAA${"9".repeat(60)}`,
+ }),
+ ];
expect(evidenceLabels(input)[0].length).toBeLessThanOrEqual(64);
expect(evidenceLabels(input)[0]).toMatch(/…$/);
});
@@ -104,7 +174,9 @@ it("uses at most two bounded locations in both current and completed-stage title
it("does not turn missing typed metadata into an empty corpus or use unsafe fields", () => {
const input = status("ORIGINAL", []);
expect(evidenceLabels(input)).toEqual(["위치 미확인"]);
- input.user_activity_events = [source("bad", { page: NaN, line_start: -2, cell: "A1; ORIGINAL", section: "ORIGINAL" })];
+ input.user_activity_events = [
+ source("bad", { page: NaN, line_start: -2, cell: "A1; ORIGINAL", section: "ORIGINAL" }),
+ ];
input.user_activity_events[0].target!.title = "ORIGINAL";
input.user_activity_events[0].target!.display_ref = "ORIGINAL";
expect(JSON.stringify(liveResearchProgress(input))).not.toContain("ORIGINAL");
@@ -115,17 +187,45 @@ it("does not turn missing typed metadata into an empty corpus or use unsafe fiel
it("does not reread exact_text when rendering the old response", () => {
const input = status("ORIGINAL");
- input.attempt!.draft_progress = { evidence_focus: { total: 1, locators: [{ page: 6, get exact_text() { throw new Error("Original source was read by progress UI"); } }] } };
+ input.attempt!.draft_progress = {
+ evidence_focus: {
+ total: 1,
+ locators: [
+ {
+ page: 6,
+ get exact_text() {
+ throw new Error("Original source was read by progress UI");
+ },
+ },
+ ],
+ },
+ };
expect(evidenceLabels(input)).toEqual(["p.6"]);
});
-it.each(["ko", "en"] as const)("formats validated page, line range and cell coordinates (%s)", language => {
- setLocale(language);
- const input = status("ORIGINAL", [source("range", { page: 6, line_start: 2, line_end: 5, cell: "$A$1:C3", section: "ORIGINAL" })]);
- expect(evidenceLabels(input)).toEqual([language === "ko" ? "p.6 · 2~5행 · $A$1:C3" : "p.6 · lines 2–5 · $A$1:C3"]);
-});
+it.each(["ko", "en"] as const)(
+ "formats validated page, line range and cell coordinates (%s)",
+ (language) => {
+ setLocale(language);
+ const input = status("ORIGINAL", [
+ source("range", {
+ page: 6,
+ line_start: 2,
+ line_end: 5,
+ cell: "$A$1:C3",
+ section: "ORIGINAL",
+ }),
+ ]);
+ expect(evidenceLabels(input)).toEqual([
+ language === "ko" ? "p.6 · 2~5행 · $A$1:C3" : "p.6 · lines 2–5 · $A$1:C3",
+ ]);
+ },
+);
it("does not trust an unsupported typed schema or fall back to its old page", () => {
- const event = { ...source("next", { page: 99 }), schema_version: "unknown-version" } as unknown as UserActivityEvent;
+ const event = {
+ ...source("next", { page: 99 }),
+ schema_version: "unknown-version",
+ } as unknown as UserActivityEvent;
expect(evidenceLabels(status("ORIGINAL", [event]))).toEqual(["위치 미확인"]);
});
diff --git a/apps/web/src/api/progressEvidenceLocations.ts b/apps/web/src/api/progressEvidenceLocations.ts
index ccf8c25e..86028897 100644
--- a/apps/web/src/api/progressEvidenceLocations.ts
+++ b/apps/web/src/api/progressEvidenceLocations.ts
@@ -4,7 +4,8 @@ import { objectList, objectValue, textValue } from "./presentation";
import type { ResearchStatus } from "./research";
const unavailable = () => message("progressLocationUnavailable");
-const positiveInteger = (value: unknown): value is number => typeof value === "number" && Number.isSafeInteger(value) && value > 0;
+const positiveInteger = (value: unknown): value is number =>
+ typeof value === "number" && Number.isSafeInteger(value) && value > 0;
const cellPattern = /^\$?[A-Z]{1,4}\$?[1-9][0-9]*(?::\$?[A-Z]{1,4}\$?[1-9][0-9]*)?$/;
function location(value: unknown): string | null {
@@ -13,16 +14,24 @@ function location(value: unknown): string | null {
if (positiveInteger(locator.page)) parts.push(`p.${locator.page}`);
if (positiveInteger(locator.line_start)) {
const end = locator.line_end ?? locator.line_start;
- if (positiveInteger(end) && end >= locator.line_start) parts.push(message("progressLineRange", locator.line_start, end));
+ if (positiveInteger(end) && end >= locator.line_start)
+ parts.push(message("progressLineRange", locator.line_start, end));
}
- if (typeof locator.cell === "string" && locator.cell.length <= 128 && cellPattern.test(locator.cell)) parts.push(locator.cell);
+ if (
+ typeof locator.cell === "string" &&
+ locator.cell.length <= 128 &&
+ cellPattern.test(locator.cell)
+ )
+ parts.push(locator.cell);
const text = parts.join(" · ");
- return text ? text.length > 64 ? `${text.slice(0, 63)}…` : text : null;
+ return text ? (text.length > 64 ? `${text.slice(0, 63)}…` : text) : null;
}
function legacyLocators(status?: ResearchStatus): Record[] {
const focus = objectValue(objectValue(status?.attempt?.draft_progress).evidence_focus);
- return objectList(focus.locators).filter(item => "page" in item || "span_id" in item || "exact_text" in item);
+ return objectList(focus.locators).filter(
+ (item) => "page" in item || "span_id" in item || "exact_text" in item,
+ );
}
function legacyLocation(value: Record, index: number): string {
@@ -34,12 +43,24 @@ export function progressEvidenceLabels(status?: ResearchStatus): string[] {
if (status?.user_activity_events === undefined) return legacy.map(legacyLocation);
const sources = new Map();
for (const [index, event] of objectList(status.user_activity_events).entries()) {
- if (event.schema_version !== "thoth.user_activity_event.v1" || event.activity_kind !== "source" || event.action !== "read") continue;
+ if (
+ event.schema_version !== "thoth.user_activity_event.v1" ||
+ event.activity_kind !== "source" ||
+ event.action !== "read"
+ )
+ continue;
const target = objectValue(event.target);
- const label = target.kind === "span" && target.access_state === "allowed" ? location(target.locator) ?? unavailable() : unavailable();
+ const label =
+ target.kind === "span" && target.access_state === "allowed"
+ ? (location(target.locator) ?? unavailable())
+ : unavailable();
const sourceRef = textValue(objectValue(event.refs).source_ref);
const eventId = textValue(event.event_id);
- const key = sourceRef ? `source:${sourceRef}` : eventId ? `event:${eventId}` : `unknown:${index}`;
+ const key = sourceRef
+ ? `source:${sourceRef}`
+ : eventId
+ ? `event:${eventId}`
+ : `unknown:${index}`;
const previous = sources.get(key);
// A contradictory or denied copy must not be overwritten by an allowed one.
sources.set(key, previous !== undefined && previous !== label ? unavailable() : label);
@@ -48,7 +69,11 @@ export function progressEvidenceLabels(status?: ResearchStatus): string[] {
return sources.size ? [...sources.values()] : legacy.map(unavailable);
}
-export function progressSourceActionLocation(status: ResearchStatus, payload: Record, index: number): string {
+export function progressSourceActionLocation(
+ status: ResearchStatus,
+ payload: Record,
+ index: number,
+): string {
// Legacy actions name raw span IDs; typed events expose aliases. Do not zip them by order.
return status.user_activity_events === undefined ? legacyLocation(payload, index) : unavailable();
}
diff --git a/apps/web/src/api/projectActionFailure.ts b/apps/web/src/api/projectActionFailure.ts
index 17689780..591fba54 100644
--- a/apps/web/src/api/projectActionFailure.ts
+++ b/apps/web/src/api/projectActionFailure.ts
@@ -5,15 +5,20 @@ import { RpcError } from "./rpcClient";
const REVISION_MISMATCH = -32030;
/** An onError handler: a refused project action usually means the list is stale, so read it again. */
-export const rereadProjects = (client: QueryClient) => () => { void client.invalidateQueries({ queryKey: ["projects"] }); };
+export const rereadProjects = (client: QueryClient) => () => {
+ void client.invalidateQueries({ queryKey: ["projects"] });
+};
/**
* A refused project action (archive, restore) in words a person can act on. Re-evaluated on every
* render so a language change applies. A revision mismatch means the screen held an older copy of
* the project; the caller has already asked for the list again, and nothing is retried here.
*/
-export function projectActionFailure(error: unknown, fallbackKey: "archive.archiveFailed" | "archive.restoreFailed" | "manage.renameFailed"): string {
- if (error instanceof RpcError && error.code === REVISION_MISMATCH) return t("archive.revisionChanged");
+export function projectActionFailure(
+ error: unknown,
+ fallbackKey: "archive.archiveFailed" | "archive.restoreFailed" | "manage.renameFailed",
+): string {
+ if (error instanceof RpcError && error.code === REVISION_MISMATCH)
+ return t("archive.revisionChanged");
return t(fallbackKey);
}
-
diff --git a/apps/web/src/api/projectCreation.ts b/apps/web/src/api/projectCreation.ts
index 782a84d5..17114adc 100644
--- a/apps/web/src/api/projectCreation.ts
+++ b/apps/web/src/api/projectCreation.ts
@@ -1,7 +1,12 @@
export function captureProjectCreation(name: string, cutoff: string, overlay: string) {
return {
key: crypto.randomUUID(),
- input: {project_id:`project:web-${crypto.randomUUID()}`,name:name.trim(),cutoff_at:new Date(cutoff).toISOString(),overlay:overlay.trim()},
+ input: {
+ project_id: `project:web-${crypto.randomUUID()}`,
+ name: name.trim(),
+ cutoff_at: new Date(cutoff).toISOString(),
+ overlay: overlay.trim(),
+ },
};
// Omit policy_binding_ref: the backend's policy:default expands to a project-unique binding.
}
diff --git a/apps/web/src/api/projectDeletion.ts b/apps/web/src/api/projectDeletion.ts
index cc1d5a16..a6a76e93 100644
--- a/apps/web/src/api/projectDeletion.ts
+++ b/apps/web/src/api/projectDeletion.ts
@@ -21,7 +21,8 @@ export function permanentDeleteFailure(error: unknown): string {
const reason = String(error.details.reason_code ?? "");
if (reason === "PROJECT_NOT_ARCHIVED") return t("archive.deleteNotArchived");
if (reason === "PROJECT_NAME_MISMATCH") return t("archive.deleteNameMismatch");
- if (reason === "PROJECT_HAS_ACTIVE_OPERATION" || reason === "PROJECT_EMBEDDING_JOB_RUNNING") return t("archive.deleteActive");
+ if (reason === "PROJECT_HAS_ACTIVE_OPERATION" || reason === "PROJECT_EMBEDDING_JOB_RUNNING")
+ return t("archive.deleteActive");
if (reason === "PROJECT_REFERENCED_BY_OTHER_PROJECTS") {
const ids = error.details.referencing_project_ids;
return message("deleteReferenced", Array.isArray(ids) ? ids.map(String).join(", ") : "");
@@ -32,4 +33,5 @@ export function permanentDeleteFailure(error: unknown): string {
return t("archive.deleteFailed");
}
-export const projectAlreadyGone = (error: unknown) => error instanceof RpcError && error.code === PROJECT_NOT_FOUND;
+export const projectAlreadyGone = (error: unknown) =>
+ error instanceof RpcError && error.code === PROJECT_NOT_FOUND;
diff --git a/apps/web/src/api/protocol.ts b/apps/web/src/api/protocol.ts
index 4dfb53d8..df91f63a 100644
--- a/apps/web/src/api/protocol.ts
+++ b/apps/web/src/api/protocol.ts
@@ -1,39 +1,40 @@
-import { z } from 'zod'
+import { z } from "zod";
-export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue }
+export type JsonValue =
+ null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
export const rpcMetaSchema = z.object({
idempotencyKey: z.string().min(1).max(260),
expectedHeadDigest: z.string().nullable().optional(),
progressToken: z.string().nullable().optional(),
-})
+});
export const jsonRpcRequestSchema = z.object({
- jsonrpc: z.literal('2.0').default('2.0'),
+ jsonrpc: z.literal("2.0").default("2.0"),
id: z.union([z.string(), z.number().int()]),
method: z.string().min(1).max(160),
params: z.object({
_meta: rpcMetaSchema,
input: z.record(z.string(), z.unknown()),
}),
-})
+});
export const jsonRpcErrorSchema = z.object({
code: z.number().int(),
message: z.string(),
data: z.record(z.string(), z.unknown()).default({}),
-})
+});
export const jsonRpcResponseSchema = z
.object({
- jsonrpc: z.literal('2.0'),
+ jsonrpc: z.literal("2.0"),
id: z.union([z.string(), z.number().int()]).nullable(),
result: z.record(z.string(), z.unknown()).optional(),
error: jsonRpcErrorSchema.optional(),
})
.refine((value) => Number(value.result !== undefined) + Number(value.error !== undefined) === 1, {
- message: 'exactly one of result or error is required',
- })
+ message: "exactly one of result or error is required",
+ });
-export type JsonRpcRequest = z.infer
-export type JsonRpcResponse = z.infer
+export type JsonRpcRequest = z.infer;
+export type JsonRpcResponse = z.infer;
diff --git a/apps/web/src/api/publicWeb.test.ts b/apps/web/src/api/publicWeb.test.ts
index 4883dc52..0233b458 100644
--- a/apps/web/src/api/publicWeb.test.ts
+++ b/apps/web/src/api/publicWeb.test.ts
@@ -9,7 +9,9 @@ import {
describe("public web update contract", () => {
it("requires a strict boolean and normalizes hosts", () => {
- expect(() => publicWebUpdateInputSchema.parse({ enabled: "true", preferred_hosts: ["arxiv.org"] })).toThrow();
+ expect(() =>
+ publicWebUpdateInputSchema.parse({ enabled: "true", preferred_hosts: ["arxiv.org"] }),
+ ).toThrow();
const parsed = parsePublicWebUpdate({
enabled: true,
preferred_hosts: ["https://ArXiv.org/list", "arxiv.org"],
@@ -43,7 +45,9 @@ describe("public web update contract", () => {
describe("public web execution status", () => {
it("keeps typed execution states separate from the saved toggle", () => {
- expect(publicWebExecutionSchema.parse({ desired_enabled: true, state: "READY" }).state).toBe("READY");
+ expect(publicWebExecutionSchema.parse({ desired_enabled: true, state: "READY" }).state).toBe(
+ "READY",
+ );
expect(executionLabel("READY")).toBe("실행: 준비됨");
expect(executionLabel("OFF")).toBe("실행: 꺼짐");
expect(executionLabel("BLOCKED", ["PUBLIC_WEB_POLICY_INCONSISTENT"])).toBe("실행: 권한 불일치");
diff --git a/apps/web/src/api/readAccess.ts b/apps/web/src/api/readAccess.ts
index d545ca1e..17cd1ae9 100644
--- a/apps/web/src/api/readAccess.ts
+++ b/apps/web/src/api/readAccess.ts
@@ -1,7 +1,11 @@
/** Stable error fields only; never infer permission from translated prose. */
const resourceDenials = new Set([
- "RESOURCE_ACCESS_DENIED", "RESOURCE_AUTHORITY_CHANGED", "RESOURCE_SCOPE_PROJECT_MISMATCH",
- "RESOURCE_SCOPE_UNKNOWN", "RESOURCE_REFERENCE_UNRESOLVED", "RESOURCE_LINEAGE_UNKNOWN",
+ "RESOURCE_ACCESS_DENIED",
+ "RESOURCE_AUTHORITY_CHANGED",
+ "RESOURCE_SCOPE_PROJECT_MISMATCH",
+ "RESOURCE_SCOPE_UNKNOWN",
+ "RESOURCE_REFERENCE_UNRESOLVED",
+ "RESOURCE_LINEAGE_UNKNOWN",
]);
export function isReadAccessDenied(error: unknown): boolean {
@@ -10,6 +14,11 @@ export function isReadAccessDenied(error: unknown): boolean {
if (value.code === -32040) return true;
if (value.kind === "HTTP" && (value.status === 401 || value.status === 403)) return true;
const details = value.details;
- return Boolean(details && typeof details === "object" && "reason_code" in details
- && typeof details.reason_code === "string" && resourceDenials.has(details.reason_code));
+ return Boolean(
+ details &&
+ typeof details === "object" &&
+ "reason_code" in details &&
+ typeof details.reason_code === "string" &&
+ resourceDenials.has(details.reason_code),
+ );
}
diff --git a/apps/web/src/api/recordViews.ts b/apps/web/src/api/recordViews.ts
index ecb41b5c..8fc45ace 100644
--- a/apps/web/src/api/recordViews.ts
+++ b/apps/web/src/api/recordViews.ts
@@ -1,29 +1,242 @@
import { t } from "../i18n/locale";
-export type RecordView = { namespace: string; label: string; description: string; method: string | null; scope?: "thread" | "operation"; note?: string };
+export type RecordView = {
+ namespace: string;
+ label: string;
+ description: string;
+ method: string | null;
+ scope?: "thread" | "operation";
+ note?: string;
+};
/** Dedicated read entrypoints; backend command schemas remain the authority. */
export const recordViews: RecordView[] = [
- {namespace:"project",get label() { return t("workspace.projects"); },get description() { return t("advanced.projectHint"); },method:"project/read"},
- {namespace:"thread",get label() { return t("advanced.task"); },get description() { return t("advanced.taskHint"); },method:"thread/read",scope:"thread"},
- {namespace:"investigation",get label() { return t("advanced.discovery"); },get description() { return t("advanced.discoveryHint"); },method:"investigation/list"},
- {namespace:"evidence",get label() { return t("history.evidence"); },get description() { return t("advanced.evidenceHint"); },method:"evidence/list"},
- {namespace:"object",get label() { return t("advanced.object"); },get description() { return t("advanced.objectHint"); },method:"object/list"},
- {namespace:"criteria",get label() { return t("record.criterion"); },get description() { return t("advanced.criteriaHint"); },method:"criteria/list"},
- {namespace:"hypothesis",get label() { return t("hypotheses.hypothesis"); },get description() { return t("advanced.hypothesisHint"); },method:"hypothesis/list"},
- {namespace:"action",get label() { return t("history.action"); },get description() { return t("advanced.actionHint"); },method:"action/list"},
- {namespace:"execution",get label() { return t("advanced.execution"); },get description() { return t("advanced.executionHint"); },method:"execution/list"},
- {namespace:"outcome",get label() { return t("advanced.outcomes"); },get description() { return t("advanced.outcomesHint"); },method:"outcome/list"},
- {namespace:"revision",get label() { return t("advanced.revisions"); },get description() { return t("advanced.revisionsHint"); },method:"revision/list"},
- {namespace:"memory",get label() { return t("result.memory"); },get description() { return t("advanced.memoryHint"); },method:"memory/list"},
- {namespace:"improvement",get label() { return t("advanced.improvement"); },get description() { return t("advanced.improvementHint"); },method:"improvement/list"},
- {namespace:"receipt",get label() { return t("advanced.receipt"); },get description() { return t("advanced.receiptHint"); },method:"receipt/list"},
- {namespace:"closure",get label() { return t("advanced.closure"); },get description() { return t("advanced.closureHint"); },method:"closure/list"},
- {namespace:"export",get label() { return t("advanced.export"); },get description() { return t("advanced.exportHint"); },method:"export/list"},
- {namespace:"operation",get label() { return t("advanced.async"); },get description() { return t("advanced.asyncHint"); },method:"operation/read",scope:"operation"},
- {namespace:"model",get label() { return t("advanced.models"); },get description() { return t("advanced.modelsHint"); },method:"model/settings/read"},
- {namespace:"trace",get label() { return t("workspace.trace"); },get description() { return t("advanced.traceHint"); },method:"trace/read"},
- {namespace:"workspace",get label() { return t("advanced.workspace"); },get description() { return t("advanced.workspaceHint"); },method:"workspace/setup/read"},
- {namespace:"projectpack",get label() { return t("advanced.examples"); },get description() { return t("advanced.examplesHint"); },method:"projectpack/list",get note() { return t("advanced.examplesNotice"); }},
- {namespace:"field",get label() { return t("advanced.field"); },get description() { return t("advanced.fieldHint"); },method:null,get note() { return t("advanced.fieldNotice"); }},
+ {
+ namespace: "project",
+ get label() {
+ return t("workspace.projects");
+ },
+ get description() {
+ return t("advanced.projectHint");
+ },
+ method: "project/read",
+ },
+ {
+ namespace: "thread",
+ get label() {
+ return t("advanced.task");
+ },
+ get description() {
+ return t("advanced.taskHint");
+ },
+ method: "thread/read",
+ scope: "thread",
+ },
+ {
+ namespace: "investigation",
+ get label() {
+ return t("advanced.discovery");
+ },
+ get description() {
+ return t("advanced.discoveryHint");
+ },
+ method: "investigation/list",
+ },
+ {
+ namespace: "evidence",
+ get label() {
+ return t("history.evidence");
+ },
+ get description() {
+ return t("advanced.evidenceHint");
+ },
+ method: "evidence/list",
+ },
+ {
+ namespace: "object",
+ get label() {
+ return t("advanced.object");
+ },
+ get description() {
+ return t("advanced.objectHint");
+ },
+ method: "object/list",
+ },
+ {
+ namespace: "criteria",
+ get label() {
+ return t("record.criterion");
+ },
+ get description() {
+ return t("advanced.criteriaHint");
+ },
+ method: "criteria/list",
+ },
+ {
+ namespace: "hypothesis",
+ get label() {
+ return t("hypotheses.hypothesis");
+ },
+ get description() {
+ return t("advanced.hypothesisHint");
+ },
+ method: "hypothesis/list",
+ },
+ {
+ namespace: "action",
+ get label() {
+ return t("history.action");
+ },
+ get description() {
+ return t("advanced.actionHint");
+ },
+ method: "action/list",
+ },
+ {
+ namespace: "execution",
+ get label() {
+ return t("advanced.execution");
+ },
+ get description() {
+ return t("advanced.executionHint");
+ },
+ method: "execution/list",
+ },
+ {
+ namespace: "outcome",
+ get label() {
+ return t("advanced.outcomes");
+ },
+ get description() {
+ return t("advanced.outcomesHint");
+ },
+ method: "outcome/list",
+ },
+ {
+ namespace: "revision",
+ get label() {
+ return t("advanced.revisions");
+ },
+ get description() {
+ return t("advanced.revisionsHint");
+ },
+ method: "revision/list",
+ },
+ {
+ namespace: "memory",
+ get label() {
+ return t("result.memory");
+ },
+ get description() {
+ return t("advanced.memoryHint");
+ },
+ method: "memory/list",
+ },
+ {
+ namespace: "improvement",
+ get label() {
+ return t("advanced.improvement");
+ },
+ get description() {
+ return t("advanced.improvementHint");
+ },
+ method: "improvement/list",
+ },
+ {
+ namespace: "receipt",
+ get label() {
+ return t("advanced.receipt");
+ },
+ get description() {
+ return t("advanced.receiptHint");
+ },
+ method: "receipt/list",
+ },
+ {
+ namespace: "closure",
+ get label() {
+ return t("advanced.closure");
+ },
+ get description() {
+ return t("advanced.closureHint");
+ },
+ method: "closure/list",
+ },
+ {
+ namespace: "export",
+ get label() {
+ return t("advanced.export");
+ },
+ get description() {
+ return t("advanced.exportHint");
+ },
+ method: "export/list",
+ },
+ {
+ namespace: "operation",
+ get label() {
+ return t("advanced.async");
+ },
+ get description() {
+ return t("advanced.asyncHint");
+ },
+ method: "operation/read",
+ scope: "operation",
+ },
+ {
+ namespace: "model",
+ get label() {
+ return t("advanced.models");
+ },
+ get description() {
+ return t("advanced.modelsHint");
+ },
+ method: "model/settings/read",
+ },
+ {
+ namespace: "trace",
+ get label() {
+ return t("workspace.trace");
+ },
+ get description() {
+ return t("advanced.traceHint");
+ },
+ method: "trace/read",
+ },
+ {
+ namespace: "workspace",
+ get label() {
+ return t("advanced.workspace");
+ },
+ get description() {
+ return t("advanced.workspaceHint");
+ },
+ method: "workspace/setup/read",
+ },
+ {
+ namespace: "projectpack",
+ get label() {
+ return t("advanced.examples");
+ },
+ get description() {
+ return t("advanced.examplesHint");
+ },
+ method: "projectpack/list",
+ get note() {
+ return t("advanced.examplesNotice");
+ },
+ },
+ {
+ namespace: "field",
+ get label() {
+ return t("advanced.field");
+ },
+ get description() {
+ return t("advanced.fieldHint");
+ },
+ method: null,
+ get note() {
+ return t("advanced.fieldNotice");
+ },
+ },
];
export const visibleRecordViews = recordViews.filter(
diff --git a/apps/web/src/api/requestLanguage.test.ts b/apps/web/src/api/requestLanguage.test.ts
index 87ee183f..cb235e5e 100644
--- a/apps/web/src/api/requestLanguage.test.ts
+++ b/apps/web/src/api/requestLanguage.test.ts
@@ -1,13 +1,31 @@
// @vitest-environment jsdom
import { afterEach, expect, it } from "vitest";
-import { readLocalPending, submissionSignature, writeLocalPending, type PendingSubmission } from "./localWorkspacePersistence";
-import { readResponseLanguageDefault, writeResponseLanguageDefault } from "./responseLanguageDefault";
+import {
+ readLocalPending,
+ submissionSignature,
+ writeLocalPending,
+ type PendingSubmission,
+} from "./localWorkspacePersistence";
+import {
+ readResponseLanguageDefault,
+ writeResponseLanguageDefault,
+} from "./responseLanguageDefault";
import { setLocale } from "../i18n/locale";
-afterEach(() => { localStorage.clear(); setLocale("ko"); });
+afterEach(() => {
+ localStorage.clear();
+ setLocale("ko");
+});
const scope = { mode: "LOCAL" as const, workspaceId: "workspace:language-test" };
-const fields = { workspaceId: scope.workspaceId, projectId: "p", threadId: "t", problem: "Question", selection: null,
- key: "11111111-1111-1111-1111-111111111111", createdAt: 1 };
+const fields = {
+ workspaceId: scope.workspaceId,
+ projectId: "p",
+ threadId: "t",
+ problem: "Question",
+ selection: null,
+ key: "11111111-1111-1111-1111-111111111111",
+ createdAt: 1,
+};
it("preserves v1 signatures and keeps explicit ko distinct from omission", () => {
const legacy = submissionSignature("p", "t", "Question", null);
@@ -20,8 +38,12 @@ it("preserves v1 signatures and keeps explicit ko distinct from omission", () =>
});
it("replays v2 with its saved language regardless of display and default-language changes", () => {
- const saved: PendingSubmission = { ...fields, version: 2, responseLanguage: "en",
- signature: submissionSignature("p", "t", "Question", null, null, "en") };
+ const saved: PendingSubmission = {
+ ...fields,
+ version: 2,
+ responseLanguage: "en",
+ signature: submissionSignature("p", "t", "Question", null, null, "en"),
+ };
expect(writeLocalPending(saved)).toBe("SAVED");
writeResponseLanguageDefault(scope, "ko");
setLocale("en");
diff --git a/apps/web/src/api/research.ts b/apps/web/src/api/research.ts
index d9ca7e0e..c63db258 100644
--- a/apps/web/src/api/research.ts
+++ b/apps/web/src/api/research.ts
@@ -3,20 +3,35 @@ import type { ResponseLanguage } from "./responseLanguages";
import { z } from "zod";
import type { Currentness } from "./historyModels";
import type { CoverageMatrix, NextUserAction, UserProgressSummary } from "./researchFollowup";
-import { readLocalContext, writeLocalContext, type BrowserScope } from "./localWorkspacePersistence";
+import {
+ readLocalContext,
+ writeLocalContext,
+ type BrowserScope,
+} from "./localWorkspacePersistence";
export type ThreadAnalysis = PackRunResult["cycle"] & { selected_evidence_refs: string[] };
export type ResearchFailure = {
- primary: {reason_code:string; origin:string; detail?:string | null};
- secondary?: {reason_code:string; origin:string; detail?:string | null} | null;
+ primary: { reason_code: string; origin: string; detail?: string | null };
+ secondary?: { reason_code: string; origin: string; detail?: string | null } | null;
remote_observation?: string;
};
-export type ExecutionError = {message?:string; data?:{reason_code?:string; failure?:ResearchFailure}};
+export type ExecutionError = {
+ message?: string;
+ data?: { reason_code?: string; failure?: ResearchFailure };
+};
export type ResultManifest = {
request_ref?: { revision_digest: string };
- operation_id: string; phase: string; state: string; completion: string; terminal_reason: string | null;
- basis_digest: string; result: Record; gaps: string[]; next_steps: string[];
- source_refs: string[]; /** Not sent by the summary read; the result read has it. */ record_refs?: unknown[];
+ operation_id: string;
+ phase: string;
+ state: string;
+ completion: string;
+ terminal_reason: string | null;
+ basis_digest: string;
+ result: Record;
+ gaps: string[];
+ next_steps: string[];
+ source_refs: string[];
+ /** Not sent by the summary read; the result read has it. */ record_refs?: unknown[];
};
export type CompletedStage = {
role?: string;
@@ -58,7 +73,16 @@ export type UserActivityLocator = {
line_end?: number | null;
};
export type UserActivityTarget = {
- kind: "file" | "uri" | "artifact" | "span" | "table_cell" | "line_range" | "database_view" | "sandbox" | "model";
+ kind:
+ | "file"
+ | "uri"
+ | "artifact"
+ | "span"
+ | "table_cell"
+ | "line_range"
+ | "database_view"
+ | "sandbox"
+ | "model";
title: string;
display_ref?: string | null;
host_alias?: string | null;
@@ -95,11 +119,50 @@ export type UserActivityEvent = {
announce: "none" | "polite" | "assertive";
phase?: string | null;
activity_kind: "tool" | "source" | "model" | "sandbox" | "connector" | "judgement" | "control";
- action: "search" | "open" | "fetch" | "parse" | "read" | "screen" | "select_candidate" | "adopt_support" | "adopt_counter" | "run" | "call_model" | "retry" | "cancel" | "hold" | "stage_complete";
+ action:
+ | "search"
+ | "open"
+ | "fetch"
+ | "parse"
+ | "read"
+ | "screen"
+ | "select_candidate"
+ | "adopt_support"
+ | "adopt_counter"
+ | "run"
+ | "call_model"
+ | "retry"
+ | "cancel"
+ | "hold"
+ | "stage_complete";
label_ko: string;
why_ko?: string | null;
- state: "planned" | "running" | "succeeded" | "failed" | "blocked" | "cancel_requested" | "cancelled" | "timed_out" | "unknown_external_effect";
- research_relation: "none" | "discovered" | "opened" | "fetched" | "parsed" | "read" | "screened" | "selected_candidate" | "supports" | "contradicts" | "inconclusive" | "held" | "excluded" | "stale" | "access_blocked";
+ state:
+ | "planned"
+ | "running"
+ | "succeeded"
+ | "failed"
+ | "blocked"
+ | "cancel_requested"
+ | "cancelled"
+ | "timed_out"
+ | "unknown_external_effect";
+ research_relation:
+ | "none"
+ | "discovered"
+ | "opened"
+ | "fetched"
+ | "parsed"
+ | "read"
+ | "screened"
+ | "selected_candidate"
+ | "supports"
+ | "contradicts"
+ | "inconclusive"
+ | "held"
+ | "excluded"
+ | "stale"
+ | "access_blocked";
target?: UserActivityTarget | null;
tool?: UserActivityTool | null;
result?: UserActivityResult | null;
@@ -117,8 +180,15 @@ export type UserActivityEvent = {
};
};
export type ResultUsage = {
- operation_id: string; calls: number; input_tokens: number | null; output_tokens: number | null; total_tokens: number | null;
- cached_input_tokens: number | null; unreported_calls: number; state: string; wall_ms: number | null;
+ operation_id: string;
+ calls: number;
+ input_tokens: number | null;
+ output_tokens: number | null;
+ total_tokens: number | null;
+ cached_input_tokens: number | null;
+ unreported_calls: number;
+ state: string;
+ wall_ms: number | null;
/** Calls sent again on their own after an earlier call was cut off. */
auto_retries?: number;
/** Part of the figures above that a purpose-marked call used, such as widening the memory question. */
@@ -126,41 +196,107 @@ export type ResultUsage = {
};
/** A closed-loop test that was held instead of run. Only a hold with an unsettled earlier run has anything to clear (exits). */
export type ExecutionHold = {
- schema_version: string; reason_code: string | null; plan_execution_id: string | null; execution_revision: number | null;
- attempt_id: string | null; attempt_state: string | null; exits: string[];
- input_difference: { same?: boolean; previous_count?: number; current_count?: number; removed?: string[]; added?: string[] } | null;
+ schema_version: string;
+ reason_code: string | null;
+ plan_execution_id: string | null;
+ execution_revision: number | null;
+ attempt_id: string | null;
+ attempt_state: string | null;
+ exits: string[];
+ input_difference: {
+ same?: boolean;
+ previous_count?: number;
+ current_count?: number;
+ removed?: string[];
+ added?: string[];
+ } | null;
};
export type ResearchStatus = WorkThread & {
- usage?: { input_tokens: number | null; output_tokens: number | null; total_tokens: number | null; state: string; unreported_calls: number; cumulative_token_limit_enforced: boolean; cached_input_tokens?: number | null };
- operation_state?: string; operation_error?: ExecutionError | null; request_epoch?: number;
+ usage?: {
+ input_tokens: number | null;
+ output_tokens: number | null;
+ total_tokens: number | null;
+ state: string;
+ unreported_calls: number;
+ cumulative_token_limit_enforced: boolean;
+ cached_input_tokens?: number | null;
+ };
+ operation_state?: string;
+ operation_error?: ExecutionError | null;
+ request_epoch?: number;
/** Usage per result (operation id), computed by the server when the status is read. */
result_usage?: Record;
failure?: ResearchFailure | null;
- execution_summary?: {effective_execution_state:string;state_inconsistent:boolean;last_checkpoint_only:boolean;automatic_retry:boolean};
- freshness?: string; current_result?: ResultManifest | null; previous_result?: ResultManifest | null;
+ execution_summary?: {
+ effective_execution_state: string;
+ state_inconsistent: boolean;
+ last_checkpoint_only: boolean;
+ automatic_retry: boolean;
+ };
+ freshness?: string;
+ current_result?: ResultManifest | null;
+ previous_result?: ResultManifest | null;
basis_currentness?: Currentness;
/** Present when the stored result carries a held closed-loop test. */
execution_hold?: ExecutionHold | null;
user_progress_summary?: UserProgressSummary | null;
coverage_matrix?: CoverageMatrix | null;
next_user_action?: NextUserAction | null;
- request?: { response_language?: ResponseLanguage; operation_id: string; authored_text?: string; effective_question?: string; model_settings?: unknown; policy_digest?: string; cutoff_at?: string };
+ request?: {
+ response_language?: ResponseLanguage;
+ operation_id: string;
+ authored_text?: string;
+ effective_question?: string;
+ model_settings?: unknown;
+ policy_digest?: string;
+ cutoff_at?: string;
+ };
completed_stages?: CompletedStage[];
stage_reuse?: { reused: number; new: number };
activity_events?: ActivityEvent[];
user_activity_events?: UserActivityEvent[];
model_dispatches?: ModelDispatch[];
- attempt?: { operation_id: string; phase: string; status: string; draft_progress?: unknown; external_effect_state?: string; remote_observation?: string;
+ attempt?: {
+ operation_id: string;
+ phase: string;
+ status: string;
+ draft_progress?: unknown;
+ external_effect_state?: string;
+ remote_observation?: string;
request_ref?: { revision_digest: string };
- checkpoint_ref?: { project_id: string; entity_type: string; entity_id: string; revision_digest: string } | null;
+ checkpoint_ref?: {
+ project_id: string;
+ entity_type: string;
+ entity_id: string;
+ revision_digest: string;
+ } | null;
+ } | null;
+ budget?: {
+ calls: number;
+ max_calls: number | null;
+ reserved_tokens: number;
+ max_reserved_tokens: number | null;
+ actual_tokens?: number | null;
+ actual_cost?: string | null;
+ max_seconds: number | null;
+ started_at: string;
} | null;
- budget?: { calls: number; max_calls: number | null; reserved_tokens: number; max_reserved_tokens: number | null; actual_tokens?: number | null; actual_cost?: string | null; max_seconds: number | null; started_at: string } | null;
- cleanup?: unknown; post_execution_learning_current?: unknown[]; inputs?: unknown[];
+ cleanup?: unknown;
+ post_execution_learning_current?: unknown[];
+ inputs?: unknown[];
+};
+export type ResearchAdmission = {
+ contract_version: number;
+ project_id?: string;
+ thread_id: string;
+ operation_id?: string;
+ status?: string;
+ request_epoch?: number | null;
+ terminal_reason?: string | null;
};
-export type ResearchAdmission = { contract_version: number; project_id?: string; thread_id: string; operation_id?: string;
- status?: string; request_epoch?: number | null; terminal_reason?: string | null };
export type WorkspaceLocationInfo = {
- classification: "KNOWN_LOCAL" | "KNOWN_SYNC" | "REMOTE_SHARE" | "UNSUPPORTED_BRIDGE" | "REMOVABLE" | "UNKNOWN";
+ classification:
+ "KNOWN_LOCAL" | "KNOWN_SYNC" | "REMOTE_SHARE" | "UNSUPPORTED_BRIDGE" | "REMOVABLE" | "UNKNOWN";
reason_codes: string[];
confirmed: boolean;
message_ko: string;
@@ -184,44 +320,119 @@ export type WorkspaceReady = {
workspace_id?: string;
};
-const stringList=z.array(z.string());
-const dimension=z.object({status:z.string(),reason:z.string()});
-const analysisDisplaySchema=z.object({
- assessment:z.object({assessment_id:z.string(),derived_status:stringList,missing_items:stringList,
- scope_identity:dimension,criterion_authority:dimension,evidence_coverage:dimension,comparability:dimension}),
- portfolio:z.object({portfolio_id:z.string(),hypotheses:z.array(z.object({
- hypothesis_id:z.string(),statement:z.string(),primary_locus:z.string().nullable(),uncertainty:z.string(),
- support_evidence_refs:stringList.optional(),evidence_refs:stringList.optional(),counterevidence_refs:stringList.optional(),
- critical_review:z.object({terminal:z.string()}).passthrough().nullable().optional(),
- counterevidence_queries:stringList,predicted_observations:stringList,
- discriminating_tests:z.array(z.object({procedure_candidate:z.string(),expected_if_true:z.string(),expected_if_alternative:z.string(),risk_tier:z.string()})),
- }))}),
- action_plan:z.object({plan_id:z.string(),frontier:stringList,alternatives:z.array(z.object({
- action_id:z.string(),action_family:z.string(),specification:z.string(),expected_information_value:z.string(),
- risk_tier:z.string(),execution_authority:z.string(),reversibility:z.string(),state:z.string(),source_refs:stringList,missing_evidence:stringList,
- }))}),
- commit:z.object({disposition:z.string(),committed_revision_ids:stringList,receipt:z.object({
- receipt_id:z.string(),receipt_digest:z.string(),claim_scopes:stringList,integrity_state:z.string(),provenance_state:z.string(),semantic_truth_certified:z.boolean(),
- })}),
- selected_evidence_refs:stringList,
+const stringList = z.array(z.string());
+const dimension = z.object({ status: z.string(), reason: z.string() });
+const analysisDisplaySchema = z.object({
+ assessment: z.object({
+ assessment_id: z.string(),
+ derived_status: stringList,
+ missing_items: stringList,
+ scope_identity: dimension,
+ criterion_authority: dimension,
+ evidence_coverage: dimension,
+ comparability: dimension,
+ }),
+ portfolio: z.object({
+ portfolio_id: z.string(),
+ hypotheses: z.array(
+ z.object({
+ hypothesis_id: z.string(),
+ statement: z.string(),
+ primary_locus: z.string().nullable(),
+ uncertainty: z.string(),
+ support_evidence_refs: stringList.optional(),
+ evidence_refs: stringList.optional(),
+ counterevidence_refs: stringList.optional(),
+ critical_review: z.object({ terminal: z.string() }).passthrough().nullable().optional(),
+ counterevidence_queries: stringList,
+ predicted_observations: stringList,
+ discriminating_tests: z.array(
+ z.object({
+ procedure_candidate: z.string(),
+ expected_if_true: z.string(),
+ expected_if_alternative: z.string(),
+ risk_tier: z.string(),
+ }),
+ ),
+ }),
+ ),
+ }),
+ action_plan: z.object({
+ plan_id: z.string(),
+ frontier: stringList,
+ alternatives: z.array(
+ z.object({
+ action_id: z.string(),
+ action_family: z.string(),
+ specification: z.string(),
+ expected_information_value: z.string(),
+ risk_tier: z.string(),
+ execution_authority: z.string(),
+ reversibility: z.string(),
+ state: z.string(),
+ source_refs: stringList,
+ missing_evidence: stringList,
+ }),
+ ),
+ }),
+ commit: z.object({
+ disposition: z.string(),
+ committed_revision_ids: stringList,
+ receipt: z.object({
+ receipt_id: z.string(),
+ receipt_digest: z.string(),
+ claim_scopes: stringList,
+ integrity_state: z.string(),
+ provenance_state: z.string(),
+ semantic_truth_certified: z.boolean(),
+ }),
+ }),
+ selected_evidence_refs: stringList,
});
/** Validate only: no default values, coercion or mutation of partial server records. */
-export function isThreadAnalysis(value: Record): value is ThreadAnalysis & Record {
+export function isThreadAnalysis(
+ value: Record,
+): value is ThreadAnalysis & Record {
return analysisDisplaySchema.safeParse(value).success;
}
const contextKey = "thoth:web-context:v1";
-export function readWorkspaceContext(scope?: BrowserScope): { projectId: string; threadId: string } {
+export function readWorkspaceContext(scope?: BrowserScope): {
+ projectId: string;
+ threadId: string;
+} {
if (scope?.mode === "LOCAL") return readLocalContext(scope.workspaceId);
try {
- const value = JSON.parse(sessionStorage.getItem(scope?.mode === "HOSTED" && scope.browserScopeId ? `${contextKey}:${scope.browserScopeId}` : contextKey) ?? "null");
- if (value?.version === 1 && typeof value.projectId === "string" && typeof value.threadId === "string") return value;
- } catch { /* Storage can be disabled; canonical data is always fetched from the server. */ }
+ const value = JSON.parse(
+ sessionStorage.getItem(
+ scope?.mode === "HOSTED" && scope.browserScopeId
+ ? `${contextKey}:${scope.browserScopeId}`
+ : contextKey,
+ ) ?? "null",
+ );
+ if (
+ value?.version === 1 &&
+ typeof value.projectId === "string" &&
+ typeof value.threadId === "string"
+ )
+ return value;
+ } catch {
+ /* Storage can be disabled; canonical data is always fetched from the server. */
+ }
return { projectId: "", threadId: "" };
}
export function saveWorkspaceContext(projectId: string, threadId: string, scope?: BrowserScope) {
if (scope?.mode === "LOCAL") return writeLocalContext(scope.workspaceId, { projectId, threadId });
- try { sessionStorage.setItem(scope?.mode === "HOSTED" && scope.browserScopeId ? `${contextKey}:${scope.browserScopeId}` : contextKey, JSON.stringify({ version: 1, projectId, threadId })); return true; }
- catch { return false; }
+ try {
+ sessionStorage.setItem(
+ scope?.mode === "HOSTED" && scope.browserScopeId
+ ? `${contextKey}:${scope.browserScopeId}`
+ : contextKey,
+ JSON.stringify({ version: 1, projectId, threadId }),
+ );
+ return true;
+ } catch {
+ return false;
+ }
}
diff --git a/apps/web/src/api/researchFollowup.ts b/apps/web/src/api/researchFollowup.ts
index 3b675737..1ba87363 100644
--- a/apps/web/src/api/researchFollowup.ts
+++ b/apps/web/src/api/researchFollowup.ts
@@ -11,7 +11,14 @@ const basisCurrentnessSchema = z.object({
const nextUserActionSchema = z.object({
schema_version: z.literal("1.0.0").optional().default("1.0.0"),
- action_type: z.enum(["NONE", "REVIEW_GAPS", "REVIEW_CURRENTNESS", "OPEN_RESULT_DETAIL", "START_FOLLOWUP", "UNSUPPORTED"]),
+ action_type: z.enum([
+ "NONE",
+ "REVIEW_GAPS",
+ "REVIEW_CURRENTNESS",
+ "OPEN_RESULT_DETAIL",
+ "START_FOLLOWUP",
+ "UNSUPPORTED",
+ ]),
label: z.string(),
reason_codes: z.array(z.string()).optional().default([]),
requires_permission: z.boolean().optional().default(false),
@@ -23,7 +30,16 @@ export const userProgressSummarySchema = z.object({
schema_version: z.literal("1.0.0").optional().default("1.0.0"),
request_revision_digest: digestSchema,
result_revision_digest: digestSchema.nullable().optional().default(null),
- state: z.enum(["PENDING_OR_NOT_PRODUCED", "IN_PROGRESS", "COMPLETE", "NEEDS_REVIEW", "HOLD", "FAILED", "CANCELLED", "UNKNOWN"]),
+ state: z.enum([
+ "PENDING_OR_NOT_PRODUCED",
+ "IN_PROGRESS",
+ "COMPLETE",
+ "NEEDS_REVIEW",
+ "HOLD",
+ "FAILED",
+ "CANCELLED",
+ "UNKNOWN",
+ ]),
currentness: basisCurrentnessSchema,
progress_items: z.array(z.string()).optional().default([]),
recorded_checks: z.array(z.string()).optional().default([]),
@@ -54,14 +70,17 @@ export const coverageMatrixSchema = z.object({
availability: z.enum(["AVAILABLE", "PARTIAL", "UNAVAILABLE"]).optional().default("AVAILABLE"),
reason_codes: z.array(z.string()).optional().default([]),
rows: z.array(coverageRowSchema).optional().default([]),
- summary: z.object({
- satisfied: z.number().int().optional().default(0),
- unresolved: z.number().int().optional().default(0),
- not_applicable: z.number().int().optional().default(0),
- not_assessed: z.number().int().optional().default(0),
- hold_targets: z.array(z.string()).optional().default([]),
- reason_codes: z.array(z.string()).optional().default([]),
- }).optional().default({}),
+ summary: z
+ .object({
+ satisfied: z.number().int().optional().default(0),
+ unresolved: z.number().int().optional().default(0),
+ not_applicable: z.number().int().optional().default(0),
+ not_assessed: z.number().int().optional().default(0),
+ hold_targets: z.array(z.string()).optional().default([]),
+ reason_codes: z.array(z.string()).optional().default([]),
+ })
+ .optional()
+ .default({}),
});
const resultIdentitySchema = z.object({
@@ -71,11 +90,15 @@ const resultIdentitySchema = z.object({
const criterionStateSchema = z.object({
status: z.enum(["SATISFIED", "UNRESOLVED", "NOT_APPLICABLE", "NOT_ASSESSED"]),
- relation: z.string(), validation: z.string(), blocker: z.string(),
+ relation: z.string(),
+ validation: z.string(),
+ blocker: z.string(),
});
const criterionDeltaSchema = z.object({
- requirement_id: z.string(), target: z.string(), question: z.string(),
+ requirement_id: z.string(),
+ target: z.string(),
+ question: z.string(),
match: z.enum(["SAME", "ADDED", "REMOVED"]),
before: criterionStateSchema.nullable().optional().default(null),
after: criterionStateSchema.nullable().optional().default(null),
@@ -96,17 +119,24 @@ const decisionDeltaSchema = z.object({
reason_codes: z.array(z.string()).optional().default([]),
reason_refs: z.array(z.record(z.unknown())).optional().default([]),
basis_currentness: z.record(basisCurrentnessSchema),
- groups: z.array(z.object({
- kind: z.enum(["CONTENT", "EVIDENCE", "CONDITION", "STATUS", "ACTION", "OTHER"]),
- trace_paths: z.array(z.string()),
- changes: z.array(z.object({
- path: z.string(),
- before_present: z.boolean(),
- after_present: z.boolean(),
- before: z.unknown().nullable().optional(),
- after: z.unknown().nullable().optional(),
- })),
- })).optional().default([]),
+ groups: z
+ .array(
+ z.object({
+ kind: z.enum(["CONTENT", "EVIDENCE", "CONDITION", "STATUS", "ACTION", "OTHER"]),
+ trace_paths: z.array(z.string()),
+ changes: z.array(
+ z.object({
+ path: z.string(),
+ before_present: z.boolean(),
+ after_present: z.boolean(),
+ before: z.unknown().nullable().optional(),
+ after: z.unknown().nullable().optional(),
+ }),
+ ),
+ }),
+ )
+ .optional()
+ .default([]),
// Results saved by an older server carry neither field: criteria are then reported as not comparable.
criteria: z.array(criterionDeltaSchema).optional().default([]),
criteria_state: z.enum(["AVAILABLE", "UNAVAILABLE"]).optional().default("UNAVAILABLE"),
@@ -146,28 +176,66 @@ export type ResultIdentity = z.infer;
export type ProjectReviewList = z.infer;
export type ProjectReviewItem = z.infer;
-async function query(method: string, input: Record, signal?: AbortSignal): Promise {
+async function query(
+ method: string,
+ input: Record,
+ signal?: AbortSignal,
+): Promise {
const response = await rpc(method, input, crypto.randomUUID(), signal);
if (response.state !== "SUCCEEDED") throw new UiError("followup.queryIncomplete");
return response.value;
}
-export async function readDecisionDelta(projectId: string, threadId: string, before: ResultIdentity, after: ResultIdentity, signal?: AbortSignal): Promise {
- const value = decisionDeltaSchema.parse(await query("thread/result/compare/read", {
- project_id: projectId, thread_id: threadId, before, after, contract_version: 2,
- }, signal));
- if (value.project_id !== projectId || value.thread_id !== threadId ||
- value.before.request_revision_digest !== before.request_revision_digest || value.before.result_revision_digest !== before.result_revision_digest ||
- value.after.request_revision_digest !== after.request_revision_digest || value.after.result_revision_digest !== after.result_revision_digest) {
+export async function readDecisionDelta(
+ projectId: string,
+ threadId: string,
+ before: ResultIdentity,
+ after: ResultIdentity,
+ signal?: AbortSignal,
+): Promise {
+ const value = decisionDeltaSchema.parse(
+ await query(
+ "thread/result/compare/read",
+ {
+ project_id: projectId,
+ thread_id: threadId,
+ before,
+ after,
+ contract_version: 2,
+ },
+ signal,
+ ),
+ );
+ if (
+ value.project_id !== projectId ||
+ value.thread_id !== threadId ||
+ value.before.request_revision_digest !== before.request_revision_digest ||
+ value.before.result_revision_digest !== before.result_revision_digest ||
+ value.after.request_revision_digest !== after.request_revision_digest ||
+ value.after.result_revision_digest !== after.result_revision_digest
+ ) {
throw new UiError("followup.binding");
}
return value;
}
-export async function readProjectReviewList(projectId: string, cursor: string | null, signal?: AbortSignal): Promise {
- const value = projectReviewListSchema.parse(await query("project/review/list", {
- project_id: projectId, cursor, limit: 20, contract_version: 2,
- }, signal));
+export async function readProjectReviewList(
+ projectId: string,
+ cursor: string | null,
+ signal?: AbortSignal,
+): Promise {
+ const value = projectReviewListSchema.parse(
+ await query(
+ "project/review/list",
+ {
+ project_id: projectId,
+ cursor,
+ limit: 20,
+ contract_version: 2,
+ },
+ signal,
+ ),
+ );
if (value.project_id !== projectId) throw new UiError("followup.projectBinding");
return value;
}
diff --git a/apps/web/src/api/researchProgress.test.tsx b/apps/web/src/api/researchProgress.test.tsx
index f76a467d..d952399b 100644
--- a/apps/web/src/api/researchProgress.test.tsx
+++ b/apps/web/src/api/researchProgress.test.tsx
@@ -18,7 +18,9 @@ function running(overrides: Partial = {}): ResearchStatus {
working_head_digest: "0".repeat(64),
operation_state: "RUNNING",
attempt: { operation_id: "operation:o", phase: "HYPOTHESIS_REVIEW", status: "RUNNING" },
- completed_stages: [{ role: "RESEARCH_PLANNER", state: "COMPLETED", elapsed_ms: 91700, context_bytes: 12000 }],
+ completed_stages: [
+ { role: "RESEARCH_PLANNER", state: "COMPLETED", elapsed_ms: 91700, context_bytes: 12000 },
+ ],
...overrides,
};
}
@@ -27,8 +29,15 @@ describe("live research progress", () => {
it("exposes Korean scenes while RUNNING and keeps them after HOLD", () => {
const live = liveResearchProgress(running());
expect(live?.headline).toBe("방금 만든 설명을 원문과 대조하고 있습니다");
- expect(live?.events[0]).toMatchObject({ kind: "note", text: "질문에서 꼭 지킬 조건을 고정했습니다" });
- expect(live?.events[1]).toMatchObject({ kind: "action", label: "단계 완료", payload: "1분 32초 · 맥락 12KB" });
+ expect(live?.events[0]).toMatchObject({
+ kind: "note",
+ text: "질문에서 꼭 지킬 조건을 고정했습니다",
+ });
+ expect(live?.events[1]).toMatchObject({
+ kind: "action",
+ label: "단계 완료",
+ payload: "1분 32초 · 맥락 12KB",
+ });
expect(live?.evidenceLabels).toEqual([]);
const held = liveResearchProgress(
running({
@@ -96,8 +105,12 @@ describe("live research progress", () => {
],
}),
);
- const notes = live?.events.filter((event) => event.kind === "note" && !event.live).map((event) => event.text);
- const payloads = live?.events.filter((event) => event.actionType === "stage_completed").map((event) => event.payload);
+ const notes = live?.events
+ .filter((event) => event.kind === "note" && !event.live)
+ .map((event) => event.text);
+ const payloads = live?.events
+ .filter((event) => event.actionType === "stage_completed")
+ .map((event) => event.payload);
expect(notes).toEqual(["질문 조건을 원문과 대조했습니다", "질문 조건을 원문과 대조했습니다"]);
expect(payloads).toEqual(["4초 · 맥락 2.0KB", "8초 · 맥락 4.0KB"]);
expect(notes?.join(" ")).not.toContain("2회");
@@ -108,7 +121,8 @@ describe("live research progress", () => {
"C:/papers/psyche-irb-2022.pdf",
"C:/papers/gao-23-106021.pdf",
]);
- const notes = live?.events.filter((event) => event.kind === "note").map((event) => event.text) ?? [];
+ const notes =
+ live?.events.filter((event) => event.kind === "note").map((event) => event.text) ?? [];
expect(notes[0]).toContain("표 3 발사 준비 조건 4개");
expect(notes[0]).toContain("표 숫자만 인용");
expect(notes[0]).toContain("추측 금지");
@@ -157,8 +171,17 @@ describe("live research progress", () => {
"Psyche 표 3 발사 준비 조건 4개가 원문에서 각각 확인되는지 적어라. 표 숫자만 인용하고 추측 금지.",
model_settings: { model: "grok-4.6", reasoning_effort: "high" },
},
- usage: { input_tokens: 8000, output_tokens: 4400, total_tokens: 12400, state: "PARTIAL", unreported_calls: 1, cumulative_token_limit_enforced: false },
- model_dispatches: [{ state: "RESERVED", transport_observation: { elapsed_ms: 47000, received_bytes: 0 } }],
+ usage: {
+ input_tokens: 8000,
+ output_tokens: 4400,
+ total_tokens: 12400,
+ state: "PARTIAL",
+ unreported_calls: 1,
+ cumulative_token_limit_enforced: false,
+ },
+ model_dispatches: [
+ { state: "RESERVED", transport_observation: { elapsed_ms: 47000, received_bytes: 0 } },
+ ],
})}
sourceUris={["C:/papers/mobilenets.pdf"]}
/>,
@@ -216,7 +239,12 @@ describe("live research progress", () => {
operation_id: "operation:o",
phase: "EVIDENCE_FOCUS",
status: "RUNNING",
- draft_progress: { evidence_focus: { locators: [{ page: 1 }, { page: 1 }, { page: 1 }, { page: 2 }], total: 4 } },
+ draft_progress: {
+ evidence_focus: {
+ locators: [{ page: 1 }, { page: 1 }, { page: 1 }, { page: 2 }],
+ total: 4,
+ },
+ },
},
});
const live = liveResearchProgress(status)!;
@@ -273,7 +301,9 @@ describe("live research progress", () => {
],
}),
);
- expect(live?.events.map((event) => [event.kind, event.text, event.payload ?? event.live])).toEqual([
+ expect(
+ live?.events.map((event) => [event.kind, event.text, event.payload ?? event.live]),
+ ).toEqual([
["note", "질문에서 꼭 지킬 조건을 고정했습니다", false],
["action", "단계 완료", "1분 32초 · 맥락 12KB · 모델 1회"],
["note", "방금 만든 설명을 원문과 대조하고 있습니다", true],
@@ -289,6 +319,8 @@ describe("live research progress", () => {
}),
),
).toBe("표와 원문 위치를 확인하고 있습니다");
- expect(composerActivity(running({ execution_state: "PAUSED" }))).toBe("조사가 일시정지되어 있습니다.");
+ expect(composerActivity(running({ execution_state: "PAUSED" }))).toBe(
+ "조사가 일시정지되어 있습니다.",
+ );
});
});
diff --git a/apps/web/src/api/researchProgress.ts b/apps/web/src/api/researchProgress.ts
index 14986a6b..5aa72213 100644
--- a/apps/web/src/api/researchProgress.ts
+++ b/apps/web/src/api/researchProgress.ts
@@ -30,7 +30,8 @@ export type TimelineEvent = {
kind: "note" | "action";
text: string;
live?: boolean;
- actionType?: "stage_completed" | "source_read" | "source_attached" | "model_call" | "hypothesis_review";
+ actionType?:
+ "stage_completed" | "source_read" | "source_attached" | "model_call" | "hypothesis_review";
label?: string;
payload?: string | null;
icon?: TimelineIcon;
@@ -67,7 +68,10 @@ function numberOrNull(value: unknown): number | null {
function questionText(status?: ResearchStatus): string {
return [status?.request?.authored_text, status?.request?.effective_question, status?.problem]
- .filter((value): value is string => typeof value === "string" && value.trim().length > 1 && value.trim() !== "q")
+ .filter(
+ (value): value is string =>
+ typeof value === "string" && value.trim().length > 1 && value.trim() !== "q",
+ )
.join("\n");
}
@@ -104,7 +108,9 @@ function extractConstraints(status?: ResearchStatus): string[] {
if (/확인 안 되면|부족한지|HOLD/u.test(text)) {
found.push(t("progress.explainMissing"));
}
- const quotes = [...text.matchAll(/[“「"]([^”」"]{2,48})[”」"]/gu)].map((match) => match[1].trim());
+ const quotes = [...text.matchAll(/[“「"]([^”」"]{2,48})[”」"]/gu)].map((match) =>
+ match[1].trim(),
+ );
return unique([...found, ...quotes]);
}
@@ -138,7 +144,9 @@ function decidedHypothesisIds(status?: ResearchStatus): Set {
function workFacts(status: ResearchStatus, sourceUris: string[] = []): WorkFacts {
const records = hypothesisRecords(status);
const decidedIds = decidedHypothesisIds(status);
- const quotes = [...questionText(status).matchAll(/[“「]([^”」]{2,48})[”」]/gu)].map((match) => match[1].trim());
+ const quotes = [...questionText(status).matchAll(/[“「]([^”」]{2,48})[”」]/gu)].map((match) =>
+ match[1].trim(),
+ );
const hypotheses = unique([...records.map((item) => item.statement), ...quotes]);
const decided = records.filter((item) => decidedIds.has(item.id)).map((item) => item.statement);
const pending = records.filter((item) => !decidedIds.has(item.id)).map((item) => item.statement);
@@ -218,7 +226,11 @@ function actionEvent(
};
}
-function timelineFromStages(status: ResearchStatus, sourceUris: string[], facts: WorkFacts): TimelineEvent[] {
+function timelineFromStages(
+ status: ResearchStatus,
+ sourceUris: string[],
+ facts: WorkFacts,
+): TimelineEvent[] {
const events: TimelineEvent[] = [];
const stages = status.completed_stages ?? [];
stages.forEach((stage, index) => {
@@ -241,7 +253,11 @@ function timelineFromStages(status: ResearchStatus, sourceUris: string[], facts:
return events;
}
-function timelineFromPublished(status: ResearchStatus, sourceUris: string[], facts: WorkFacts): TimelineEvent[] | null {
+function timelineFromPublished(
+ status: ResearchStatus,
+ sourceUris: string[],
+ facts: WorkFacts,
+): TimelineEvent[] | null {
const published = status.activity_events;
if (!Array.isArray(published) || published.length === 0) {
return null;
@@ -322,8 +338,14 @@ function timelineFromPublished(status: ResearchStatus, sourceUris: string[], fac
`event-action-${event.seq ?? index}`,
"model_call",
waiting
- ? compactPayload([elapsed ?? t("progress.elapsedUnknown"), firstByte ? formatMessage("firstByte", firstByte) : t("progress.noFirstByte")])
- : compactPayload([received != null ? `${received.toLocaleString(localeTag())}B` : null, elapsed]),
+ ? compactPayload([
+ elapsed ?? t("progress.elapsedUnknown"),
+ firstByte ? formatMessage("firstByte", firstByte) : t("progress.noFirstByte"),
+ ])
+ : compactPayload([
+ received != null ? `${received.toLocaleString(localeTag())}B` : null,
+ elapsed,
+ ]),
waiting,
),
);
@@ -390,12 +412,14 @@ export function liveResearchProgress(
const reviews = reviewCount(status);
const facts = workFacts(status, sourceUris);
const current = sceneFor(phase, facts, running);
- const missing = phase === "EVIDENCE_FOCUS" && labels.length === 0 && total === null
- ? formatMessage("progressLocationUnavailable")
- : missingFor(status, facts, labels, total, hypotheses, reviews);
+ const missing =
+ phase === "EVIDENCE_FOCUS" && labels.length === 0 && total === null
+ ? formatMessage("progressLocationUnavailable")
+ : missingFor(status, facts, labels, total, hypotheses, reviews);
const liveText = missing ? `${current.title} ${missing}` : current.title;
let events =
- timelineFromPublished(status, sourceUris, facts) ?? timelineFromStages(status, sourceUris, facts);
+ timelineFromPublished(status, sourceUris, facts) ??
+ timelineFromStages(status, sourceUris, facts);
if (!running) {
events = events.map((event) => (event.live ? { ...event, live: false } : event));
}
@@ -428,7 +452,10 @@ export function composerActivity(status: ResearchStatus): string | null {
if (status.execution_state === "PAUSED") {
return t("progress.paused");
}
- if (status.attempt?.remote_observation === "UNKNOWN" && status.attempt.external_effect_state === "DISPATCHING") {
+ if (
+ status.attempt?.remote_observation === "UNKNOWN" &&
+ status.attempt.external_effect_state === "DISPATCHING"
+ ) {
return t("progress.externalUnknown");
}
if (status.operation_state !== "RUNNING") {
diff --git a/apps/web/src/api/researchProgressCharacterization.test.ts b/apps/web/src/api/researchProgressCharacterization.test.ts
index 33149f73..cebdb6b8 100644
--- a/apps/web/src/api/researchProgressCharacterization.test.ts
+++ b/apps/web/src/api/researchProgressCharacterization.test.ts
@@ -1,6 +1,11 @@
import { describe, expect, it } from "vitest";
-import { composerActivity, evidenceLabels, formatDuration, liveResearchProgress } from "./researchProgress";
+import {
+ composerActivity,
+ evidenceLabels,
+ formatDuration,
+ liveResearchProgress,
+} from "./researchProgress";
import type { ResearchStatus } from "./research";
import { sceneFor, type WorkFacts } from "../i18n/progressKo";
@@ -28,7 +33,11 @@ function status(overrides: Record = {}): ResearchStatus {
} as ResearchStatus;
}
-function inPhase(phase: string, extra: Record = {}, draft: Record = {}) {
+function inPhase(
+ phase: string,
+ extra: Record = {},
+ draft: Record = {},
+) {
return status({
attempt: { operation_id: "operation:o", phase, status: "RUNNING", draft_progress: draft },
...extra,
@@ -36,7 +45,13 @@ function inPhase(phase: string, extra: Record = {}, draft: Reco
}
const RICH_DRAFT = {
- evidence_focus: { locators: [{ page: 6, exact_text: "70.6" }, { page: 8, exact_text: "GNC" }], total: 5 },
+ evidence_focus: {
+ locators: [
+ { page: 6, exact_text: "70.6" },
+ { page: 8, exact_text: "GNC" },
+ ],
+ total: 5,
+ },
portfolio: {
hypotheses: [
{ hypothesis_id: "h1", statement: "일정만 늘리면 해결된다" },
@@ -49,24 +64,59 @@ const RICH_DRAFT = {
const QUESTION = {
request: {
operation_id: "operation:o",
- authored_text: "Psyche 표 3 발사 준비 조건 4개가 원문에서 각각 확인되는지 적어라. 표 숫자만 인용하고 추측 금지. 원문 위치와 함께 답.",
+ authored_text:
+ "Psyche 표 3 발사 준비 조건 4개가 원문에서 각각 확인되는지 적어라. 표 숫자만 인용하고 추측 금지. 원문 위치와 함께 답.",
},
};
-const SOURCES = ["C:/papers/psyche-irb-2022.pdf", "C:/papers/gao-23-106021.pdf", "C:/papers/third.pdf"];
+const SOURCES = [
+ "C:/papers/psyche-irb-2022.pdf",
+ "C:/papers/gao-23-106021.pdf",
+ "C:/papers/third.pdf",
+];
const KEYS = [
- "RESEARCH_PLANNER", "REQUIREMENTS", "EVIDENCE_RERANKER", "SEMANTIC_REVIEWER", "EVIDENCE_REVIEW",
- "REVIEW_ADJUDICATOR", "SOURCE_PLANNER", "HYPOTHESIS_GENERATOR", "HYPOTHESES", "HYPOTHESIS_REVIEWER",
- "HYPOTHESIS_REVIEW", "ACTION_PLANNER", "ACTION_COMPARISON", "EVIDENCE_EXTRACTOR", "EVIDENCE_FOCUS",
- "SUFFICIENCY_EXPLAINER", "COUNTEREVIDENCE_CHALLENGER", "USER_EXPLAINER", "REFERENCE_MAPPER",
- "CONNECTED_SOURCES", "SOURCE_SHORTLIST", "HOLD", "SOMETHING_ELSE",
+ "RESEARCH_PLANNER",
+ "REQUIREMENTS",
+ "EVIDENCE_RERANKER",
+ "SEMANTIC_REVIEWER",
+ "EVIDENCE_REVIEW",
+ "REVIEW_ADJUDICATOR",
+ "SOURCE_PLANNER",
+ "HYPOTHESIS_GENERATOR",
+ "HYPOTHESES",
+ "HYPOTHESIS_REVIEWER",
+ "HYPOTHESIS_REVIEW",
+ "ACTION_PLANNER",
+ "ACTION_COMPARISON",
+ "EVIDENCE_EXTRACTOR",
+ "EVIDENCE_FOCUS",
+ "SUFFICIENCY_EXPLAINER",
+ "COUNTEREVIDENCE_CHALLENGER",
+ "USER_EXPLAINER",
+ "REFERENCE_MAPPER",
+ "CONNECTED_SOURCES",
+ "SOURCE_SHORTLIST",
+ "HOLD",
+ "SOMETHING_ELSE",
];
describe("durations", () => {
it.each([
- [null, null], [undefined, null], [Number.NaN, null], [-1, null], [Number.POSITIVE_INFINITY, null],
- [0, "0ms"], [999, "999ms"], [1000, "1초"], [1499, "1초"], [59499, "59초"], [59500, "1분"],
- [60000, "1분"], [61000, "1분 1초"], [91700, "1분 32초"], [3600000, "60분"],
+ [null, null],
+ [undefined, null],
+ [Number.NaN, null],
+ [-1, null],
+ [Number.POSITIVE_INFINITY, null],
+ [0, "0ms"],
+ [999, "999ms"],
+ [1000, "1초"],
+ [1499, "1초"],
+ [59499, "59초"],
+ [59500, "1분"],
+ [60000, "1분"],
+ [61000, "1분 1초"],
+ [91700, "1분 32초"],
+ [3600000, "60분"],
])("formats %s as %s", (input, expected) => {
expect(formatDuration(input as number | null | undefined)).toBe(expected);
});
@@ -77,8 +127,13 @@ describe("scene sentences", () => {
const lines: string[] = [];
for (const key of KEYS) {
for (const state of ["RUNNING", "SUCCEEDED"]) {
- const live = liveResearchProgress(inPhase(key, { operation_state: state, ...QUESTION }, RICH_DRAFT), SOURCES);
- lines.push(`${key} ${state}: ${live?.headline} // ${live?.current.detail} // ${live?.missing}`);
+ const live = liveResearchProgress(
+ inPhase(key, { operation_state: state, ...QUESTION }, RICH_DRAFT),
+ SOURCES,
+ );
+ lines.push(
+ `${key} ${state}: ${live?.headline} // ${live?.current.detail} // ${live?.missing}`,
+ );
}
}
expect(lines).toMatchInlineSnapshot(`
@@ -174,7 +229,15 @@ describe("scene sentences", () => {
return `${problem}: ${live?.headline}`;
});
const withLocators = ["책", "사과", "A3", "표 3", "ABC"].map((label) => {
- const facts: WorkFacts = { focus: null, constraints: [], locators: [label], hypotheses: [], pendingHypotheses: [], decidedHypotheses: [], sources: [] };
+ const facts: WorkFacts = {
+ focus: null,
+ constraints: [],
+ locators: [label],
+ hypotheses: [],
+ pendingHypotheses: [],
+ decidedHypotheses: [],
+ sources: [],
+ };
const reranked = sceneFor("EVIDENCE_RERANKER", facts, true);
const reviewed = sceneFor("SEMANTIC_REVIEWER", { ...facts, focus: "발사 조건" }, true);
return `${label}: ${reranked.title} | ${reviewed.title}`;
@@ -197,7 +260,8 @@ describe("scene sentences", () => {
});
it("clips, deduplicates and limits the quoted hypotheses and the joined lists", () => {
- const long = "아주 긴 가설 문장은 스물여덟 글자를 넘기면 말줄임표로 잘려야 합니다 그래서 길게 적습니다";
+ const long =
+ "아주 긴 가설 문장은 스물여덟 글자를 넘기면 말줄임표로 잘려야 합니다 그래서 길게 적습니다";
const draft = {
portfolio: {
hypotheses: [
@@ -224,21 +288,66 @@ describe("scene sentences", () => {
describe("one-line texts", () => {
it("pins the model, token and wait lines", () => {
const cases: Record[] = [
- { request: { operation_id: "o", model_settings: { model: "gpt-x", reasoning_effort: "high" } } },
+ {
+ request: {
+ operation_id: "o",
+ model_settings: { model: "gpt-x", reasoning_effort: "high" },
+ },
+ },
{ request: { operation_id: "o", model_settings: { model: "gpt-x" } } },
{ request: { operation_id: "o", model_settings: { reasoning_effort: "low" } } },
{ request: { operation_id: "o", model_settings: {} } },
{ request: { operation_id: "o" } },
- { usage: { input_tokens: 1, output_tokens: 2, total_tokens: 12400, state: "COMPLETE", unreported_calls: 0, cumulative_token_limit_enforced: false } },
- { usage: { input_tokens: 1, output_tokens: 2, total_tokens: null, state: "PARTIAL", unreported_calls: 1, cumulative_token_limit_enforced: false, cached_input_tokens: 3000 } },
- { model_dispatches: [{ state: "RESERVED", transport_observation: { elapsed_ms: 47000, received_bytes: 0 } }] },
- { model_dispatches: [{ state: "RESERVED", transport_observation: { elapsed_ms: 47000, first_byte_ms: 1200 } }] },
+ {
+ usage: {
+ input_tokens: 1,
+ output_tokens: 2,
+ total_tokens: 12400,
+ state: "COMPLETE",
+ unreported_calls: 0,
+ cumulative_token_limit_enforced: false,
+ },
+ },
+ {
+ usage: {
+ input_tokens: 1,
+ output_tokens: 2,
+ total_tokens: null,
+ state: "PARTIAL",
+ unreported_calls: 1,
+ cumulative_token_limit_enforced: false,
+ cached_input_tokens: 3000,
+ },
+ },
+ {
+ model_dispatches: [
+ { state: "RESERVED", transport_observation: { elapsed_ms: 47000, received_bytes: 0 } },
+ ],
+ },
+ {
+ model_dispatches: [
+ { state: "RESERVED", transport_observation: { elapsed_ms: 47000, first_byte_ms: 1200 } },
+ ],
+ },
{ model_dispatches: [{ state: "RESERVED" }] },
- { model_dispatches: [{ state: "OBSERVED", transport_observation: { elapsed_ms: 173, received_bytes: 2048 } }] },
- { model_dispatches: [{ state: "OBSERVED", received_bytes: 5, transport_observation: { elapsed_ms: 90000 } }] },
+ {
+ model_dispatches: [
+ { state: "OBSERVED", transport_observation: { elapsed_ms: 173, received_bytes: 2048 } },
+ ],
+ },
+ {
+ model_dispatches: [
+ { state: "OBSERVED", received_bytes: 5, transport_observation: { elapsed_ms: 90000 } },
+ ],
+ },
{ model_dispatches: [{ state: "OBSERVED", transport_observation: { elapsed_ms: 5000 } }] },
{ model_dispatches: [{ state: "OBSERVED" }] },
- { model_dispatches: [{ state: "OBSERVED", received_bytes: 1 }, { state: "RESERVED", transport_observation: { elapsed_ms: 1000 } }] },
+ {
+ model_dispatches: [
+ { state: "OBSERVED", received_bytes: 1 },
+ { state: "RESERVED", transport_observation: { elapsed_ms: 1000 } },
+ ],
+ },
];
const lines = cases.map((extra) => {
const live = liveResearchProgress(status(extra));
@@ -266,27 +375,85 @@ describe("one-line texts", () => {
});
it("pins the finished-stage and model-call payload texts, including sizes", () => {
- const stages = [500, 1023, 1024, 1536, 10239, 10240, 1048576, 5242880, 20971520, null, -1].map((context_bytes) => ({
- role: "RESEARCH_PLANNER", elapsed_ms: 2000, context_bytes, dispatch_ids: ["a", "b"],
- }));
+ const stages = [500, 1023, 1024, 1536, 10239, 10240, 1048576, 5242880, 20971520, null, -1].map(
+ (context_bytes) => ({
+ role: "RESEARCH_PLANNER",
+ elapsed_ms: 2000,
+ context_bytes,
+ dispatch_ids: ["a", "b"],
+ }),
+ );
const live = liveResearchProgress(status({ completed_stages: stages as unknown }));
const published = liveResearchProgress(
status({
activity_events: [
- { seq: 1, kind: "action", action_type: "stage_completed", live: false, payload: { elapsed_ms: 1500, context_bytes: 2048, dispatch_count: 2 } },
- { seq: 2, kind: "action", action_type: "source_read", live: false, payload: { page: 3, exact_text: " 표 3 " } },
- { seq: 3, kind: "action", action_type: "source_read", live: false, payload: { page: null, exact_text: null } },
- { seq: 4, kind: "action", action_type: "hypothesis_review", live: false, payload: { hypothesis_count: 4, review_count: 4 } },
- { seq: 5, kind: "action", action_type: "hypothesis_review", live: false, payload: { hypothesis_count: 4, review_count: 1 } },
- { seq: 6, kind: "action", action_type: "hypothesis_review", live: false, payload: { hypothesis_count: 4 } },
- { seq: 7, kind: "action", action_type: "model_call", live: false, payload: { state: "OBSERVED", received_bytes: 1200, elapsed_ms: 800 } },
- { seq: 8, kind: "action", action_type: "model_call", live: true, payload: { state: "RESERVED", elapsed_ms: 3000, first_byte_ms: 400 } },
+ {
+ seq: 1,
+ kind: "action",
+ action_type: "stage_completed",
+ live: false,
+ payload: { elapsed_ms: 1500, context_bytes: 2048, dispatch_count: 2 },
+ },
+ {
+ seq: 2,
+ kind: "action",
+ action_type: "source_read",
+ live: false,
+ payload: { page: 3, exact_text: " 표 3 " },
+ },
+ {
+ seq: 3,
+ kind: "action",
+ action_type: "source_read",
+ live: false,
+ payload: { page: null, exact_text: null },
+ },
+ {
+ seq: 4,
+ kind: "action",
+ action_type: "hypothesis_review",
+ live: false,
+ payload: { hypothesis_count: 4, review_count: 4 },
+ },
+ {
+ seq: 5,
+ kind: "action",
+ action_type: "hypothesis_review",
+ live: false,
+ payload: { hypothesis_count: 4, review_count: 1 },
+ },
+ {
+ seq: 6,
+ kind: "action",
+ action_type: "hypothesis_review",
+ live: false,
+ payload: { hypothesis_count: 4 },
+ },
+ {
+ seq: 7,
+ kind: "action",
+ action_type: "model_call",
+ live: false,
+ payload: { state: "OBSERVED", received_bytes: 1200, elapsed_ms: 800 },
+ },
+ {
+ seq: 8,
+ kind: "action",
+ action_type: "model_call",
+ live: true,
+ payload: { state: "RESERVED", elapsed_ms: 3000, first_byte_ms: 400 },
+ },
],
}),
);
- const flat = (events: { kind: string; text: string; payload?: string | null; live?: boolean }[] | undefined) =>
- events?.map((event) => `${event.kind}|${event.text}|${event.payload ?? ""}|${event.live ?? false}`);
- expect({ stages: flat(live?.events), published: flat(published?.events) }).toMatchInlineSnapshot(`
+ const flat = (
+ events: { kind: string; text: string; payload?: string | null; live?: boolean }[] | undefined,
+ ) =>
+ events?.map(
+ (event) => `${event.kind}|${event.text}|${event.payload ?? ""}|${event.live ?? false}`,
+ );
+ expect({ stages: flat(live?.events), published: flat(published?.events) })
+ .toMatchInlineSnapshot(`
{
"published": [
"action|단계 완료|2초 · 맥락 2.0KB · 모델 2회|false",
@@ -333,11 +500,23 @@ describe("one-line texts", () => {
composerActivity(status({ execution_state: "PAUSE_PENDING" })),
composerActivity(status({ execution_state: "PAUSED" })),
composerActivity(
- status({ attempt: { operation_id: "o", phase: "HOLD", status: "RUNNING", external_effect_state: "DISPATCHING", remote_observation: "UNKNOWN" } }),
+ status({
+ attempt: {
+ operation_id: "o",
+ phase: "HOLD",
+ status: "RUNNING",
+ external_effect_state: "DISPATCHING",
+ remote_observation: "UNKNOWN",
+ },
+ }),
),
composerActivity(status({ operation_state: "SUCCEEDED" })),
composerActivity(status({ attempt: undefined })),
- composerActivity(inPhase("EVIDENCE_FOCUS", { model_dispatches: [{ state: "RESERVED", transport_observation: { elapsed_ms: 47000 } }] })),
+ composerActivity(
+ inPhase("EVIDENCE_FOCUS", {
+ model_dispatches: [{ state: "RESERVED", transport_observation: { elapsed_ms: 47000 } }],
+ }),
+ ),
];
expect(lines).toMatchInlineSnapshot(`
[
@@ -352,16 +531,37 @@ describe("one-line texts", () => {
});
it("pins the hold reasons and the evidence labels", () => {
- const reasons = ["HTTP 429", "AUTH_REQUIRED", "x403", "MODEL_NOT_SUPPORTED", "400", "other", ""].map((terminal_reason) => {
+ const reasons = [
+ "HTTP 429",
+ "AUTH_REQUIRED",
+ "x403",
+ "MODEL_NOT_SUPPORTED",
+ "400",
+ "other",
+ "",
+ ].map((terminal_reason) => {
const live = liveResearchProgress(
inPhase("HOLD", {
operation_state: "SUCCEEDED",
- current_result: { operation_id: "o", phase: "HOLD", state: "PARTIAL", completion: "TERMINAL", terminal_reason, basis_digest: "0", result: {}, gaps: [], next_steps: [], source_refs: [], record_refs: [] },
+ current_result: {
+ operation_id: "o",
+ phase: "HOLD",
+ state: "PARTIAL",
+ completion: "TERMINAL",
+ terminal_reason,
+ basis_digest: "0",
+ result: {},
+ gaps: [],
+ next_steps: [],
+ source_refs: [],
+ record_refs: [],
+ },
}),
);
return `${terminal_reason}: ${live?.missing}`;
});
- expect({ reasons, labels: evidenceLabels(inPhase("EVIDENCE_FOCUS", {}, RICH_DRAFT)) }).toMatchInlineSnapshot(`
+ expect({ reasons, labels: evidenceLabels(inPhase("EVIDENCE_FOCUS", {}, RICH_DRAFT)) })
+ .toMatchInlineSnapshot(`
{
"labels": [
"p.6",
diff --git a/apps/web/src/api/researchProgressText.ts b/apps/web/src/api/researchProgressText.ts
index d2f26606..f492de86 100644
--- a/apps/web/src/api/researchProgressText.ts
+++ b/apps/web/src/api/researchProgressText.ts
@@ -5,20 +5,62 @@ import * as en from "../i18n/progressEn";
import * as zh from "../i18n/progressZh";
import * as zhHant from "../i18n/progressZhHant";
import * as ja from "../i18n/progressJa";
-export type {ProgressScene, TimelineIcon, WorkFacts} from "../i18n/progressKo";
-export {clip, unique, compactPayload, locatorPayload} from "../i18n/progressKo";
-type Wording = Pick;
-const wording: Record, Wording> = { ko, en, "zh-Hans": zh, "zh-Hant": zhHant, ja };
+export type { ProgressScene, TimelineIcon, WorkFacts } from "../i18n/progressKo";
+export { clip, unique, compactPayload, locatorPayload } from "../i18n/progressKo";
+type Wording = Pick<
+ typeof ko,
+ | "ACTION_COPY"
+ | "formatDuration"
+ | "formatBytes"
+ | "sceneFor"
+ | "waitLine"
+ | "usageLine"
+ | "missingFor"
+ | "stageActionPayload"
+ | "modelActionPayload"
+ | "hypothesisActionPayload"
+>;
+const wording: Record, Wording> = {
+ ko,
+ en,
+ "zh-Hans": zh,
+ "zh-Hant": zhHant,
+ ja,
+};
const current = () => wording[getLocale()];
-export const ACTION_COPY: typeof ko.ACTION_COPY = Object.fromEntries(Object.keys(ko.ACTION_COPY).map(key => [key, {
- get label() { return current().ACTION_COPY[key].label; }, icon: ko.ACTION_COPY[key].icon,
-}]));
-export const formatDuration = (...args: Parameters): ReturnType => current().formatDuration(...args);
-export const formatBytes = (...args: Parameters): ReturnType => current().formatBytes(...args);
-export const sceneFor = (...args: Parameters): ReturnType => current().sceneFor(...args);
-export const waitLine = (...args: Parameters): ReturnType => current().waitLine(...args);
-export const usageLine = (...args: Parameters): ReturnType => current().usageLine(...args);
-export const missingFor = (...args: Parameters): ReturnType => current().missingFor(...args);
-export const stageActionPayload = (...args: Parameters): ReturnType => current().stageActionPayload(...args);
-export const modelActionPayload = (...args: Parameters): ReturnType => current().modelActionPayload(...args);
-export const hypothesisActionPayload = (...args: Parameters): ReturnType => current().hypothesisActionPayload(...args);
\ No newline at end of file
+export const ACTION_COPY: typeof ko.ACTION_COPY = Object.fromEntries(
+ Object.keys(ko.ACTION_COPY).map((key) => [
+ key,
+ {
+ get label() {
+ return current().ACTION_COPY[key].label;
+ },
+ icon: ko.ACTION_COPY[key].icon,
+ },
+ ]),
+);
+export const formatDuration = (
+ ...args: Parameters
+): ReturnType => current().formatDuration(...args);
+export const formatBytes = (
+ ...args: Parameters
+): ReturnType => current().formatBytes(...args);
+export const sceneFor = (...args: Parameters): ReturnType =>
+ current().sceneFor(...args);
+export const waitLine = (...args: Parameters): ReturnType =>
+ current().waitLine(...args);
+export const usageLine = (
+ ...args: Parameters
+): ReturnType => current().usageLine(...args);
+export const missingFor = (
+ ...args: Parameters
+): ReturnType => current().missingFor(...args);
+export const stageActionPayload = (
+ ...args: Parameters
+): ReturnType => current().stageActionPayload(...args);
+export const modelActionPayload = (
+ ...args: Parameters
+): ReturnType => current().modelActionPayload(...args);
+export const hypothesisActionPayload = (
+ ...args: Parameters
+): ReturnType => current().hypothesisActionPayload(...args);
diff --git a/apps/web/src/api/researchStatusRead.test.ts b/apps/web/src/api/researchStatusRead.test.ts
index 35cd5892..12eb6b19 100644
--- a/apps/web/src/api/researchStatusRead.test.ts
+++ b/apps/web/src/api/researchStatusRead.test.ts
@@ -15,9 +15,17 @@ vi.mock("./rpcClient", () => ({
const { readResearchStatus } = await import("./researchStatusRead");
const key = ["research", "p", "t"];
-const status = (patch: Record) => ({ operation_state: "SUCCEEDED", usage: { total_tokens: 1 }, ...patch });
+const status = (patch: Record) => ({
+ operation_state: "SUCCEEDED",
+ usage: { total_tokens: 1 },
+ ...patch,
+});
let client: QueryClient;
-beforeEach(() => { calls.length = 0; replies.length = 0; client = new QueryClient(); });
+beforeEach(() => {
+ calls.length = 0;
+ replies.length = 0;
+ client = new QueryClient();
+});
it("reads in full when nothing is running", async () => {
replies.push(status({ current_result: { operation_id: "op" } }));
@@ -27,34 +35,58 @@ it("reads in full when nothing is running", async () => {
});
it("requests typed expected-result views only from a server that advertises support", async () => {
- client.setQueryData(["workspace-ready"], {value: {response_language_scope: "REQUEST", response_languages: ["ko", "en"]}});
+ client.setQueryData(["workspace-ready"], {
+ value: { response_language_scope: "REQUEST", response_languages: ["ko", "en"] },
+ });
replies.push(status({ current_result: { operation_id: "op" } }));
await readResearchStatus(client, key, "p", "t");
- expect(calls[0].input).toEqual({project_id: "p", thread_id: "t", expected_result_contract: "v4"});
+ expect(calls[0].input).toEqual({
+ project_id: "p",
+ thread_id: "t",
+ expected_result_contract: "v4",
+ });
});
it("prefers v5 from cached workspace readiness instead of pinning a supported server to v4", async () => {
- client.setQueryData(["workspace-ready"], {value: {response_language_scope: "REQUEST", expected_result_contracts: ["v4", "v5"]}});
+ client.setQueryData(["workspace-ready"], {
+ value: { response_language_scope: "REQUEST", expected_result_contracts: ["v4", "v5"] },
+ });
replies.push(status({ current_result: { operation_id: "op" } }));
await readResearchStatus(client, key, "p", "t");
expect(calls[0].input.expected_result_contract).toBe("v5");
});
it("polls the light progress view while running and keeps the earlier full fields", async () => {
- client.setQueryData(key, { operation_id: "", state: "SUCCEEDED", value: status({ operation_state: "RUNNING", coverage_matrix: { rows: [1] }, usage: { total_tokens: 1 } }) });
+ client.setQueryData(key, {
+ operation_id: "",
+ state: "SUCCEEDED",
+ value: status({
+ operation_state: "RUNNING",
+ coverage_matrix: { rows: [1] },
+ usage: { total_tokens: 1 },
+ }),
+ });
replies.push(status({ operation_state: "RUNNING", usage: { total_tokens: 99 } }));
const result = await readResearchStatus(client, key, "p", "t");
- expect(calls).toEqual([{ method: "thread/read", input: { project_id: "p", thread_id: "t", view: "PROGRESS" } }]);
+ expect(calls).toEqual([
+ { method: "thread/read", input: { project_id: "p", thread_id: "t", view: "PROGRESS" } },
+ ]);
expect(result.value.operation_state).toBe("RUNNING");
expect((result.value.usage as { total_tokens: number }).total_tokens).toBe(99);
expect((result.value as Record).coverage_matrix).toEqual({ rows: [1] });
});
it("reads in full once, right after the light read says the run ended", async () => {
- client.setQueryData(key, { operation_id: "", state: "SUCCEEDED", value: status({ operation_state: "RUNNING" }) });
- replies.push(status({ operation_state: "SUCCEEDED" }), status({ operation_state: "SUCCEEDED", current_result: { operation_id: "op" } }));
+ client.setQueryData(key, {
+ operation_id: "",
+ state: "SUCCEEDED",
+ value: status({ operation_state: "RUNNING" }),
+ });
+ replies.push(
+ status({ operation_state: "SUCCEEDED" }),
+ status({ operation_state: "SUCCEEDED", current_result: { operation_id: "op" } }),
+ );
const result = await readResearchStatus(client, key, "p", "t");
- expect(calls.map(call => call.input.view)).toEqual(["PROGRESS", undefined]);
+ expect(calls.map((call) => call.input.view)).toEqual(["PROGRESS", undefined]);
expect((result.value as ResearchStatus).current_result).toBeTruthy();
});
-
diff --git a/apps/web/src/api/researchStatusRead.ts b/apps/web/src/api/researchStatusRead.ts
index 87f05adf..534b9541 100644
--- a/apps/web/src/api/researchStatusRead.ts
+++ b/apps/web/src/api/researchStatusRead.ts
@@ -10,7 +10,11 @@ import { expectedResultContract } from "./expectedResultContract";
* the first read that no longer says running is a full one, so the result appears exactly once.
*/
export async function readResearchStatus(
- client: QueryClient, queryKey: readonly unknown[], projectId: string, threadId: string, signal?: AbortSignal,
+ client: QueryClient,
+ queryKey: readonly unknown[],
+ projectId: string,
+ threadId: string,
+ signal?: AbortSignal,
): Promise> {
const ready = client.getQueryData>(["workspace-ready"]);
const selected = expectedResultContract(ready?.value);
@@ -19,15 +23,29 @@ export async function readResearchStatus(
// Same key contract as useReadAccessProtection; the query's real success releases it.
const guardKey = ["read-access-blocked", queryHash];
const owner = client.getQueryCache().get(queryHash);
- const requiresFullRead = () => client.getQueryData(guardKey) === true
- || isReadAccessDenied(client.getQueryState(queryKey)?.error);
+ const requiresFullRead = () =>
+ client.getQueryData(guardKey) === true ||
+ isReadAccessDenied(client.getQueryState(queryKey)?.error);
try {
- const prior = requiresFullRead() ? undefined : client.getQueryData>(queryKey);
+ const prior = requiresFullRead()
+ ? undefined
+ : client.getQueryData>(queryKey);
if (prior?.value.operation_state === "RUNNING") {
- const light = await rpc("thread/read", { project_id: projectId, thread_id: threadId, ...contract, view: "PROGRESS" }, crypto.randomUUID(), signal);
- if (light.value.operation_state === "RUNNING" && !requiresFullRead()) return { ...light, value: { ...prior.value, ...light.value } };
+ const light = await rpc(
+ "thread/read",
+ { project_id: projectId, thread_id: threadId, ...contract, view: "PROGRESS" },
+ crypto.randomUUID(),
+ signal,
+ );
+ if (light.value.operation_state === "RUNNING" && !requiresFullRead())
+ return { ...light, value: { ...prior.value, ...light.value } };
}
- return await rpc("thread/read", { project_id: projectId, thread_id: threadId, ...contract }, crypto.randomUUID(), signal);
+ return await rpc(
+ "thread/read",
+ { project_id: projectId, thread_id: threadId, ...contract },
+ crypto.randomUUID(),
+ signal,
+ );
} catch (error) {
if (isReadAccessDenied(error) && client.getQueryCache().get(queryHash) === owner) {
client.setQueryData(guardKey, true);
@@ -35,4 +53,3 @@ export async function readResearchStatus(
throw error;
}
}
-
diff --git a/apps/web/src/api/responseLanguageDefault.ts b/apps/web/src/api/responseLanguageDefault.ts
index a806dac9..ea296669 100644
--- a/apps/web/src/api/responseLanguageDefault.ts
+++ b/apps/web/src/api/responseLanguageDefault.ts
@@ -16,15 +16,21 @@ const listeners = new Set<() => void>();
const unsaved = new Map();
function identity(scope: BrowserScope): string | null {
- return scope.mode === "LOCAL" ? scope.workspaceId : scope.browserScopeId ?? null;
+ return scope.mode === "LOCAL" ? scope.workspaceId : (scope.browserScopeId ?? null);
+}
+function memoryKey(scope: BrowserScope): string {
+ return `${scope.mode}:${identity(scope)}`;
}
-function memoryKey(scope: BrowserScope): string { return `${scope.mode}:${identity(scope)}`; }
function storageKey(scope: BrowserScope): string | null {
const id = identity(scope);
return id ? `thoth:response-language-default:v1:${JSON.stringify([id])}` : null;
}
function store(scope: BrowserScope): Storage | null {
- try { return scope.mode === "LOCAL" ? localStorage : sessionStorage; } catch { return null; }
+ try {
+ return scope.mode === "LOCAL" ? localStorage : sessionStorage;
+ } catch {
+ return null;
+ }
}
export function readResponseLanguageDefault(scope: BrowserScope): ResponseLanguageDefault {
@@ -35,19 +41,30 @@ export function readResponseLanguageDefault(scope: BrowserScope): ResponseLangua
try {
const value = store(scope)?.getItem(name);
return isResponseLanguage(value) ? value : null;
- } catch { return null; }
+ } catch {
+ return null;
+ }
}
/** Returns false when the browser could not keep the choice; it then lasts until the tab closes. */
-export function writeResponseLanguageDefault(scope: BrowserScope, value: ResponseLanguageDefault): boolean {
+export function writeResponseLanguageDefault(
+ scope: BrowserScope,
+ value: ResponseLanguageDefault,
+): boolean {
let saved = false;
const name = storageKey(scope);
try {
const target = store(scope);
- if (name && target) { target.setItem(name, value ?? "auto"); saved = true; }
- } catch { saved = false; }
- if (saved) unsaved.delete(memoryKey(scope)); else unsaved.set(memoryKey(scope), value);
- listeners.forEach(listener => listener());
+ if (name && target) {
+ target.setItem(name, value ?? "auto");
+ saved = true;
+ }
+ } catch {
+ saved = false;
+ }
+ if (saved) unsaved.delete(memoryKey(scope));
+ else unsaved.set(memoryKey(scope), value);
+ listeners.forEach((listener) => listener());
return saved;
}
@@ -55,9 +72,16 @@ const subscribe = (listener: () => void) => {
listeners.add(listener);
const onStorage = () => listener();
if (typeof window !== "undefined") window.addEventListener("storage", onStorage);
- return () => { listeners.delete(listener); if (typeof window !== "undefined") window.removeEventListener("storage", onStorage); };
+ return () => {
+ listeners.delete(listener);
+ if (typeof window !== "undefined") window.removeEventListener("storage", onStorage);
+ };
};
export function useResponseLanguageDefault(scope: BrowserScope): ResponseLanguageDefault {
- return useSyncExternalStore(subscribe, () => readResponseLanguageDefault(scope), () => null);
+ return useSyncExternalStore(
+ subscribe,
+ () => readResponseLanguageDefault(scope),
+ () => null,
+ );
}
diff --git a/apps/web/src/api/responseLanguages.test.ts b/apps/web/src/api/responseLanguages.test.ts
index be0567d3..c3ab1162 100644
--- a/apps/web/src/api/responseLanguages.test.ts
+++ b/apps/web/src/api/responseLanguages.test.ts
@@ -1,28 +1,65 @@
// @vitest-environment jsdom
import { afterEach, expect, it } from "vitest";
import { LANGUAGE_CHOICES } from "../i18n/languageChoices";
-import { readResponseLanguageDefault, writeResponseLanguageDefault } from "./responseLanguageDefault";
+import {
+ readResponseLanguageDefault,
+ writeResponseLanguageDefault,
+} from "./responseLanguageDefault";
import { submissionSignature, type PendingSubmission } from "./localWorkspacePersistence";
-import { isResponseLanguage, knownResponseLanguages, RESPONSE_LANGUAGE_CHOICES, responseLanguageLabel } from "./responseLanguages";
+import {
+ isResponseLanguage,
+ knownResponseLanguages,
+ RESPONSE_LANGUAGE_CHOICES,
+ responseLanguageLabel,
+} from "./responseLanguages";
import { clearSubmission, readSubmission, writeSubmission } from "./submissionPersistence";
import { resultLanguage } from "../i18n/ResponseLanguageContext";
-afterEach(() => { localStorage.clear(); sessionStorage.clear(); });
+afterEach(() => {
+ localStorage.clear();
+ sessionStorage.clear();
+});
it("lists the answer languages in one place, apart from the screen languages", () => {
- expect(RESPONSE_LANGUAGE_CHOICES.map(choice => choice.code)).toEqual(["ko", "en", "zh-Hans", "zh-Hant", "ja"]);
+ expect(RESPONSE_LANGUAGE_CHOICES.map((choice) => choice.code)).toEqual([
+ "ko",
+ "en",
+ "zh-Hans",
+ "zh-Hant",
+ "ja",
+ ]);
expect(responseLanguageLabel("ja")).toBe("日本語");
expect(responseLanguageLabel("zh-Hans")).toBe("中文(简体)");
expect(responseLanguageLabel("zh-Hant")).toBe("中文(繁體)");
- expect(LANGUAGE_CHOICES.map(choice => choice.code)).toEqual(["ko", "en", "zh-Hans", "zh-Hant", "ja"]);
- expect(RESPONSE_LANGUAGE_CHOICES.map(choice => choice.label)).toEqual(LANGUAGE_CHOICES.map(choice => choice.label));
+ expect(LANGUAGE_CHOICES.map((choice) => choice.code)).toEqual([
+ "ko",
+ "en",
+ "zh-Hans",
+ "zh-Hant",
+ "ja",
+ ]);
+ expect(RESPONSE_LANGUAGE_CHOICES.map((choice) => choice.label)).toEqual(
+ LANGUAGE_CHOICES.map((choice) => choice.label),
+ );
});
it("accepts only listed languages, and keeps from a server's list only the ones this app can offer", () => {
expect(["ko", "en", "zh-Hans", "zh-Hant", "ja"].every(isResponseLanguage)).toBe(true);
- expect(["fr", "zh", "zh-TW", "zh-hant", "ja-JP", "", null, 1].some(isResponseLanguage)).toBe(false);
- expect(knownResponseLanguages(["ko", "fr", "zh-Hans", "zh-Hant", "ja", 3])).toEqual(["ko", "zh-Hans", "zh-Hant", "ja"]);
- expect(knownResponseLanguages(["ko", "en", "zh-Hans", "ja"])).toEqual(["ko", "en", "zh-Hans", "ja"]);
+ expect(["fr", "zh", "zh-TW", "zh-hant", "ja-JP", "", null, 1].some(isResponseLanguage)).toBe(
+ false,
+ );
+ expect(knownResponseLanguages(["ko", "fr", "zh-Hans", "zh-Hant", "ja", 3])).toEqual([
+ "ko",
+ "zh-Hans",
+ "zh-Hant",
+ "ja",
+ ]);
+ expect(knownResponseLanguages(["ko", "en", "zh-Hans", "ja"])).toEqual([
+ "ko",
+ "en",
+ "zh-Hans",
+ "ja",
+ ]);
expect(knownResponseLanguages(undefined)).toEqual([]);
expect(resultLanguage({ response_language: "zh-Hans" })).toBe("zh-Hans");
expect(resultLanguage({ response_language: "fr" })).toBeNull();
@@ -34,10 +71,18 @@ it("keeps a Chinese default and a Chinese pending request, and refuses an unlist
const scope = { mode: "LOCAL" as const, workspaceId: "workspace:zh-hans" };
writeResponseLanguageDefault(scope, "zh-Hans");
expect(readResponseLanguageDefault(scope)).toBe("zh-Hans");
- const pending: PendingSubmission = { version: 2, responseLanguage: "zh-Hans", workspaceId: scope.workspaceId,
- projectId: "p", threadId: "t", problem: "中文问题", selection: null,
+ const pending: PendingSubmission = {
+ version: 2,
+ responseLanguage: "zh-Hans",
+ workspaceId: scope.workspaceId,
+ projectId: "p",
+ threadId: "t",
+ problem: "中文问题",
+ selection: null,
signature: submissionSignature("p", "t", "中文问题", null, null, "zh-Hans"),
- key: "11111111-1111-1111-1111-111111111111", createdAt: 1 };
+ key: "11111111-1111-1111-1111-111111111111",
+ createdAt: 1,
+ };
expect(writeSubmission(scope, pending)).toBe("SAVED");
expect(readSubmission(scope, "p", "t")).toEqual({ kind: "PENDING", value: pending });
expect(clearSubmission(scope, "p", "t")).toBe(true);
diff --git a/apps/web/src/api/responseLanguages.ts b/apps/web/src/api/responseLanguages.ts
index 2c317058..b7973282 100644
--- a/apps/web/src/api/responseLanguages.ts
+++ b/apps/web/src/api/responseLanguages.ts
@@ -14,12 +14,12 @@ export const RESPONSE_LANGUAGE_CHOICES = [
export type ResponseLanguage = (typeof RESPONSE_LANGUAGE_CHOICES)[number]["code"];
export function isResponseLanguage(value: unknown): value is ResponseLanguage {
- return RESPONSE_LANGUAGE_CHOICES.some(choice => choice.code === value);
+ return RESPONSE_LANGUAGE_CHOICES.some((choice) => choice.code === value);
}
/** Languages are named in their own writing, the same on every screen language. */
export function responseLanguageLabel(code: ResponseLanguage): string {
- return RESPONSE_LANGUAGE_CHOICES.find(choice => choice.code === code)?.label ?? code;
+ return RESPONSE_LANGUAGE_CHOICES.find((choice) => choice.code === code)?.label ?? code;
}
/** The languages of a server's list that this app knows how to offer; a language only the server knows is left out. */
diff --git a/apps/web/src/api/restore.test.ts b/apps/web/src/api/restore.test.ts
index 2800212b..9aa75f62 100644
--- a/apps/web/src/api/restore.test.ts
+++ b/apps/web/src/api/restore.test.ts
@@ -2,21 +2,43 @@
import { afterEach, expect, it, vi } from "vitest";
import { captureRestoreAttempt, readRestorePreview, sendRestoreAttempt } from "./restore";
import type { RestorePreview, RestoreSelection } from "./historyModels";
-const a = "a".repeat(64), b = "b".repeat(64);
-const selection: RestoreSelection = { projectId: "p", entityType: "HYPOTHESIS", entityId: "h", targetDigest: a, expectedHead: b };
-const preview: RestorePreview = { selection, basisDigest: a, principalScope: b, availability: "AVAILABLE", applyReady: true,
- profile: "hypothesis.v1", reasons: [], comparison: { changes: [], coverage: "COMPLETE", reasons: [] }, impacts: [], sourceChanges: [], technical: {} };
-const envelope = (state: string, value: unknown, operation_id = "op") => new Response(JSON.stringify({ jsonrpc: "2.0", id: "t", result: { state, value, operation_id } }));
+const a = "a".repeat(64),
+ b = "b".repeat(64);
+const selection: RestoreSelection = {
+ projectId: "p",
+ entityType: "HYPOTHESIS",
+ entityId: "h",
+ targetDigest: a,
+ expectedHead: b,
+};
+const preview: RestorePreview = {
+ selection,
+ basisDigest: a,
+ principalScope: b,
+ availability: "AVAILABLE",
+ applyReady: true,
+ profile: "hypothesis.v1",
+ reasons: [],
+ comparison: { changes: [], coverage: "COMPLETE", reasons: [] },
+ impacts: [],
+ sourceChanges: [],
+ technical: {},
+};
+const envelope = (state: string, value: unknown, operation_id = "op") =>
+ new Response(JSON.stringify({ jsonrpc: "2.0", id: "t", result: { state, value, operation_id } }));
afterEach(() => vi.unstubAllGlobals());
it("keeps the same key and payload after transport loss, and RUNNING is not APPLIED", async () => {
const attempt = captureRestoreAttempt(preview);
- const fetcher = vi.fn().mockRejectedValueOnce(new TypeError("network lost")).mockResolvedValueOnce(envelope("RUNNING", {}));
+ const fetcher = vi
+ .fn()
+ .mockRejectedValueOnce(new TypeError("network lost"))
+ .mockResolvedValueOnce(envelope("RUNNING", {}));
vi.stubGlobal("fetch", fetcher);
await expect(sendRestoreAttempt(attempt)).rejects.toThrow("network lost");
const second = await sendRestoreAttempt(attempt);
expect(second.state).toBe("RUNNING");
- const inputs = fetcher.mock.calls.map(call => JSON.parse(call[1].body as string));
+ const inputs = fetcher.mock.calls.map((call) => JSON.parse(call[1].body as string));
expect(inputs[0].params).toEqual(inputs[1].params);
expect(inputs[0].method).toBe("revision/restore/apply");
});
@@ -27,12 +49,32 @@ it("does not create an apply attempt while the server readiness gate is closed",
});
it("rejects a preview for another entity even when the request succeeded", async () => {
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue(envelope("SUCCEEDED", {
- contract_version: 2, selection: { project_id: "p", entity_type: "HYPOTHESIS", entity_id: "other", target_revision_digest: a, expected_current_head: b },
- profile_id: "hypothesis.v1", availability: "AVAILABLE", reason_codes: [], actor_scope_digest: b, basis_digest: a,
- capability: { restore: "RESTORE_SUPPORTED", apply_ready: true, reason_codes: [] },
- impact: { stale_refs: [], invalidated_refs: [], recalculate_refs: [] }, diff: [], summary_groups: [], source_drift: [],
- reanalysis: "NOT_REQUESTED", external_effects: "PRESERVED",
- })));
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ envelope("SUCCEEDED", {
+ contract_version: 2,
+ selection: {
+ project_id: "p",
+ entity_type: "HYPOTHESIS",
+ entity_id: "other",
+ target_revision_digest: a,
+ expected_current_head: b,
+ },
+ profile_id: "hypothesis.v1",
+ availability: "AVAILABLE",
+ reason_codes: [],
+ actor_scope_digest: b,
+ basis_digest: a,
+ capability: { restore: "RESTORE_SUPPORTED", apply_ready: true, reason_codes: [] },
+ impact: { stale_refs: [], invalidated_refs: [], recalculate_refs: [] },
+ diff: [],
+ summary_groups: [],
+ source_drift: [],
+ reanalysis: "NOT_REQUESTED",
+ external_effects: "PRESERVED",
+ }),
+ ),
+ );
await expect(readRestorePreview(selection)).rejects.toThrow(/연결/);
});
diff --git a/apps/web/src/api/restore.ts b/apps/web/src/api/restore.ts
index a2ad75bb..a7685645 100644
--- a/apps/web/src/api/restore.ts
+++ b/apps/web/src/api/restore.ts
@@ -2,83 +2,225 @@ import { UiError } from "../i18n/UiError";
import { t } from "../i18n/locale";
import { z } from "zod";
import { rpc, RpcError } from "./rpcClient";
-import { capabilitySchema, changeGroupSchema, diffEntrySchema, digestSchema } from "./historySchemas";
+import {
+ capabilitySchema,
+ changeGroupSchema,
+ diffEntrySchema,
+ digestSchema,
+} from "./historySchemas";
import { mapComparison } from "./historyComparison";
-import { restoreKey, type RestorePreview, type RestoreResult, type RestoreSelection } from "./historyModels";
+import {
+ restoreKey,
+ type RestorePreview,
+ type RestoreResult,
+ type RestoreSelection,
+} from "./historyModels";
-export const restoreSelectionSchema = z.object({ project_id: z.string(), entity_type: z.string(), entity_id: z.string(),
- target_revision_digest: digestSchema, expected_current_head: digestSchema });
-const impactSchema = z.object({ stale_refs: z.array(z.string()), invalidated_refs: z.array(z.string()), recalculate_refs: z.array(z.string()) });
-const previewSchema = z.object({ contract_version: z.literal(2), selection: restoreSelectionSchema,
- profile_id: z.string().nullable(), availability: z.enum(["AVAILABLE", "BLOCKED", "NO_CHANGE"]),
- reason_codes: z.array(z.string()), capability: capabilitySchema, actor_scope_digest: digestSchema, basis_digest: digestSchema.nullable(),
- impact: impactSchema, diff: z.array(diffEntrySchema), summary_groups: z.array(changeGroupSchema), source_drift: z.array(z.string()),
- reanalysis: z.literal("NOT_REQUESTED"), external_effects: z.literal("PRESERVED") });
-const applyResultSchema = z.object({ contract_version: z.literal(2), status: z.enum(["APPLIED", "NO_CHANGE"]),
- selection: restoreSelectionSchema, new_revision_digest: digestSchema.nullable(), new_revision_id: z.string().nullable(),
- receipt_id: z.string().nullable(), receipt_digest: digestSchema.nullable(), impact: impactSchema,
- reanalysis: z.literal("NOT_REQUESTED"), external_effects: z.literal("PRESERVED") });
+export const restoreSelectionSchema = z.object({
+ project_id: z.string(),
+ entity_type: z.string(),
+ entity_id: z.string(),
+ target_revision_digest: digestSchema,
+ expected_current_head: digestSchema,
+});
+const impactSchema = z.object({
+ stale_refs: z.array(z.string()),
+ invalidated_refs: z.array(z.string()),
+ recalculate_refs: z.array(z.string()),
+});
+const previewSchema = z.object({
+ contract_version: z.literal(2),
+ selection: restoreSelectionSchema,
+ profile_id: z.string().nullable(),
+ availability: z.enum(["AVAILABLE", "BLOCKED", "NO_CHANGE"]),
+ reason_codes: z.array(z.string()),
+ capability: capabilitySchema,
+ actor_scope_digest: digestSchema,
+ basis_digest: digestSchema.nullable(),
+ impact: impactSchema,
+ diff: z.array(diffEntrySchema),
+ summary_groups: z.array(changeGroupSchema),
+ source_drift: z.array(z.string()),
+ reanalysis: z.literal("NOT_REQUESTED"),
+ external_effects: z.literal("PRESERVED"),
+});
+const applyResultSchema = z.object({
+ contract_version: z.literal(2),
+ status: z.enum(["APPLIED", "NO_CHANGE"]),
+ selection: restoreSelectionSchema,
+ new_revision_digest: digestSchema.nullable(),
+ new_revision_id: z.string().nullable(),
+ receipt_id: z.string().nullable(),
+ receipt_digest: digestSchema.nullable(),
+ impact: impactSchema,
+ reanalysis: z.literal("NOT_REQUESTED"),
+ external_effects: z.literal("PRESERVED"),
+});
export const restoreAttemptSchema = z.object({
- key: z.string().min(1), principalScope: digestSchema,
- input: z.object({ project_id: z.string(), selection: restoreSelectionSchema, contract_version: z.literal(2),
- preview_basis_digest: digestSchema, reason: z.string().min(1).max(2000) }),
+ key: z.string().min(1),
+ principalScope: digestSchema,
+ input: z.object({
+ project_id: z.string(),
+ selection: restoreSelectionSchema,
+ contract_version: z.literal(2),
+ preview_basis_digest: digestSchema,
+ reason: z.string().min(1).max(2000),
+ }),
});
export type RestoreAttempt = z.infer;
-export type RestoreOutcome = { state: "RUNNING"; operationId: string } | { state: "COMPLETE"; result: RestoreResult };
+export type RestoreOutcome =
+ { state: "RUNNING"; operationId: string } | { state: "COMPLETE"; result: RestoreResult };
export function wireRestore(selection: RestoreSelection) {
- return { project_id: selection.projectId, entity_type: selection.entityType, entity_id: selection.entityId,
- target_revision_digest: selection.targetDigest, expected_current_head: selection.expectedHead };
+ return {
+ project_id: selection.projectId,
+ entity_type: selection.entityType,
+ entity_id: selection.entityId,
+ target_revision_digest: selection.targetDigest,
+ expected_current_head: selection.expectedHead,
+ };
}
function selectionOf(value: z.infer): RestoreSelection {
- return { projectId: value.project_id, entityType: value.entity_type, entityId: value.entity_id,
- targetDigest: value.target_revision_digest, expectedHead: value.expected_current_head };
+ return {
+ projectId: value.project_id,
+ entityType: value.entity_type,
+ entityId: value.entity_id,
+ targetDigest: value.target_revision_digest,
+ expectedHead: value.expected_current_head,
+ };
}
function requireSelection(actual: RestoreSelection, expected: RestoreSelection) {
if (restoreKey(actual) !== restoreKey(expected)) throw new UiError("api.restoreTarget");
}
-export async function readRestorePreview(selection: RestoreSelection, signal?: AbortSignal): Promise {
- const response = await rpc("revision/restore/preview", { project_id: selection.projectId, selection: wireRestore(selection), contract_version: 2 }, crypto.randomUUID(), signal);
+export async function readRestorePreview(
+ selection: RestoreSelection,
+ signal?: AbortSignal,
+): Promise {
+ const response = await rpc(
+ "revision/restore/preview",
+ { project_id: selection.projectId, selection: wireRestore(selection), contract_version: 2 },
+ crypto.randomUUID(),
+ signal,
+ );
if (response.state !== "SUCCEEDED") throw new UiError("api.restorePreviewIncomplete");
const value = previewSchema.parse(response.value);
requireSelection(selectionOf(value.selection), selection);
- const states = { get stale_refs() { return t("history.review"); }, get invalidated_refs() { return t("history.invalid"); }, get recalculate_refs() { return t("api.restoreReviewItems"); } };
- return { selection, basisDigest: value.basis_digest ?? "", principalScope: value.actor_scope_digest,
- availability: value.availability, applyReady: value.capability.restore === "RESTORE_SUPPORTED" && value.capability.apply_ready,
- profile: value.profile_id ?? "", reasons: [...new Set([...value.reason_codes, ...value.capability.reason_codes])],
+ const states = {
+ get stale_refs() {
+ return t("history.review");
+ },
+ get invalidated_refs() {
+ return t("history.invalid");
+ },
+ get recalculate_refs() {
+ return t("api.restoreReviewItems");
+ },
+ };
+ return {
+ selection,
+ basisDigest: value.basis_digest ?? "",
+ principalScope: value.actor_scope_digest,
+ availability: value.availability,
+ applyReady: value.capability.restore === "RESTORE_SUPPORTED" && value.capability.apply_ready,
+ profile: value.profile_id ?? "",
+ reasons: [...new Set([...value.reason_codes, ...value.capability.reason_codes])],
comparison: mapComparison(value.diff, value.summary_groups),
- impacts: Object.entries(value.impact).flatMap(([key, refs]) => refs.map(label => ({ label, state: states[key as keyof typeof states] }))),
- sourceChanges: value.source_drift, technical: value };
+ impacts: Object.entries(value.impact).flatMap(([key, refs]) =>
+ refs.map((label) => ({ label, state: states[key as keyof typeof states] })),
+ ),
+ sourceChanges: value.source_drift,
+ technical: value,
+ };
}
export function captureRestoreAttempt(preview: RestorePreview): RestoreAttempt {
- if (!preview.applyReady || preview.availability !== "AVAILABLE" || !preview.basisDigest || !preview.principalScope) {
+ if (
+ !preview.applyReady ||
+ preview.availability !== "AVAILABLE" ||
+ !preview.basisDigest ||
+ !preview.principalScope
+ ) {
throw new UiError("api.restoreUnavailable");
}
- return restoreAttemptSchema.parse({ key: crypto.randomUUID(), principalScope: preview.principalScope,
- input: { project_id: preview.selection.projectId, selection: wireRestore(preview.selection), contract_version: 2,
- preview_basis_digest: preview.basisDigest, reason: "연구 이력에서 확인한 과거 내용을 새 버전으로 적용" } });
+ return restoreAttemptSchema.parse({
+ key: crypto.randomUUID(),
+ principalScope: preview.principalScope,
+ input: {
+ project_id: preview.selection.projectId,
+ selection: wireRestore(preview.selection),
+ contract_version: 2,
+ preview_basis_digest: preview.basisDigest,
+ reason: "연구 이력에서 확인한 과거 내용을 새 버전으로 적용",
+ },
+ });
}
function resultOf(value: unknown, attempt: RestoreAttempt, operationId: string): RestoreResult {
const parsed = applyResultSchema.parse(value);
const selection = selectionOf(parsed.selection);
requireSelection(selection, selectionOf(attempt.input.selection));
- if (parsed.status === "APPLIED" && (!parsed.new_revision_digest || !parsed.new_revision_id || !parsed.receipt_id || !parsed.receipt_digest)) {
+ if (
+ parsed.status === "APPLIED" &&
+ (!parsed.new_revision_digest ||
+ !parsed.new_revision_id ||
+ !parsed.receipt_id ||
+ !parsed.receipt_digest)
+ ) {
throw new UiError("api.restoreRecordUnknown");
}
- return { disposition: parsed.status, selection, newDigest: parsed.new_revision_digest, operationId,
- currentness: { state: parsed.status === "APPLIED" ? "REVIEW_REQUIRED" : "UNKNOWN_BASIS", reasons: [] }, technical: parsed };
+ return {
+ disposition: parsed.status,
+ selection,
+ newDigest: parsed.new_revision_digest,
+ operationId,
+ currentness: {
+ state: parsed.status === "APPLIED" ? "REVIEW_REQUIRED" : "UNKNOWN_BASIS",
+ reasons: [],
+ },
+ technical: parsed,
+ };
}
-export async function sendRestoreAttempt(attempt: RestoreAttempt, signal?: AbortSignal): Promise {
+export async function sendRestoreAttempt(
+ attempt: RestoreAttempt,
+ signal?: AbortSignal,
+): Promise {
const response = await rpc("revision/restore/apply", attempt.input, attempt.key, signal);
- if (response.state === "RUNNING" && response.operation_id) return { state: "RUNNING", operationId: response.operation_id };
+ if (response.state === "RUNNING" && response.operation_id)
+ return { state: "RUNNING", operationId: response.operation_id };
if (response.state !== "SUCCEEDED") throw new UiError("api.restoreUnconfirmed");
return { state: "COMPLETE", result: resultOf(response.value, attempt, response.operation_id) };
}
-export async function readRestoreOperation(attempt: RestoreAttempt, operationId: string, signal?: AbortSignal): Promise {
- const response = await rpc("operation/result/read", { project_id: attempt.input.project_id, operation_id: operationId }, crypto.randomUUID(), signal);
- const value = z.object({ operation_id: z.string(), state: z.string(), result: z.unknown(), error: z.object({ code: z.number().optional(), message: z.string().optional(), data: z.record(z.unknown()).optional() }).nullable().optional() }).parse(response.value);
+export async function readRestoreOperation(
+ attempt: RestoreAttempt,
+ operationId: string,
+ signal?: AbortSignal,
+): Promise {
+ const response = await rpc(
+ "operation/result/read",
+ { project_id: attempt.input.project_id, operation_id: operationId },
+ crypto.randomUUID(),
+ signal,
+ );
+ const value = z
+ .object({
+ operation_id: z.string(),
+ state: z.string(),
+ result: z.unknown(),
+ error: z
+ .object({
+ code: z.number().optional(),
+ message: z.string().optional(),
+ data: z.record(z.unknown()).optional(),
+ })
+ .nullable()
+ .optional(),
+ })
+ .parse(response.value);
if (value.operation_id !== operationId) throw new UiError("api.restoreBinding");
if (value.state === "RUNNING") return { state: "RUNNING", operationId };
- if (value.state === "FAILED" || value.state === "CANCELLED") throw new RpcError(value.error?.message ?? t("api.restoreNotApplied"), value.error?.code ?? -32000, value.error?.data ?? {});
+ if (value.state === "FAILED" || value.state === "CANCELLED")
+ throw new RpcError(
+ value.error?.message ?? t("api.restoreNotApplied"),
+ value.error?.code ?? -32000,
+ value.error?.data ?? {},
+ );
if (value.state !== "SUCCEEDED") throw new UiError("api.restoreStateUnknown");
return { state: "COMPLETE", result: resultOf(value.result, attempt, operationId) };
}
diff --git a/apps/web/src/api/restorePending.test.ts b/apps/web/src/api/restorePending.test.ts
index 15d96c64..36c9333d 100644
--- a/apps/web/src/api/restorePending.test.ts
+++ b/apps/web/src/api/restorePending.test.ts
@@ -2,12 +2,34 @@
import { afterEach, expect, it } from "vitest";
import { loadPendingRestore, savePendingRestore } from "./restorePending";
import type { RestoreAttempt } from "./restore";
-const a = "a".repeat(64), b = "b".repeat(64), c = "c".repeat(64);
+const a = "a".repeat(64),
+ b = "b".repeat(64),
+ c = "c".repeat(64);
const scope = { projectId: "p", threadId: "t" };
-const selection = { projectId: "p", entityType: "HYPOTHESIS", entityId: "h", targetDigest: a, expectedHead: b };
-const attempt: RestoreAttempt = { key: "one-click", principalScope: c,
- input: { project_id: "p", selection: { project_id: "p", entity_type: "HYPOTHESIS", entity_id: "h", target_revision_digest: a, expected_current_head: b },
- contract_version: 2, preview_basis_digest: a, reason: "사용자 복원" } };
+const selection = {
+ projectId: "p",
+ entityType: "HYPOTHESIS",
+ entityId: "h",
+ targetDigest: a,
+ expectedHead: b,
+};
+const attempt: RestoreAttempt = {
+ key: "one-click",
+ principalScope: c,
+ input: {
+ project_id: "p",
+ selection: {
+ project_id: "p",
+ entity_type: "HYPOTHESIS",
+ entity_id: "h",
+ target_revision_digest: a,
+ expected_current_head: b,
+ },
+ contract_version: 2,
+ preview_basis_digest: a,
+ reason: "사용자 복원",
+ },
+};
afterEach(() => sessionStorage.clear());
it("retains the original request when only the current head changes", () => {
savePendingRestore(scope, selection, attempt, "operation-1");
diff --git a/apps/web/src/api/restorePending.ts b/apps/web/src/api/restorePending.ts
index 8f7b8d5a..b55a63db 100644
--- a/apps/web/src/api/restorePending.ts
+++ b/apps/web/src/api/restorePending.ts
@@ -2,27 +2,71 @@ import { z } from "zod";
import type { HistoryScope, RestoreSelection } from "./historyModels";
import { restoreAttemptSchema, type RestoreAttempt } from "./restore";
-const pendingSchema = z.object({ version: z.literal(1), attempt: restoreAttemptSchema, operationId: z.string().nullable() });
+const pendingSchema = z.object({
+ version: z.literal(1),
+ attempt: restoreAttemptSchema,
+ operationId: z.string().nullable(),
+});
export type PendingRestore = z.infer;
// Head changes must not hide an in-flight attempt. Actor/session/scope changes must.
function storageKey(scope: HistoryScope, selection: RestoreSelection, actorScope: string): string {
- return `thoth:restore-pending:v1:${JSON.stringify([scope.projectId, scope.threadId, scope.requestDigest,
- selection.entityType, selection.entityId, selection.targetDigest, actorScope])}`;
+ return `thoth:restore-pending:v1:${JSON.stringify([
+ scope.projectId,
+ scope.threadId,
+ scope.requestDigest,
+ selection.entityType,
+ selection.entityId,
+ selection.targetDigest,
+ actorScope,
+ ])}`;
}
-export function loadPendingRestore(scope: HistoryScope, selection: RestoreSelection, actorScope: string): PendingRestore | null {
+export function loadPendingRestore(
+ scope: HistoryScope,
+ selection: RestoreSelection,
+ actorScope: string,
+): PendingRestore | null {
try {
- const value = pendingSchema.parse(JSON.parse(sessionStorage.getItem(storageKey(scope, selection, actorScope)) ?? "null"));
+ const value = pendingSchema.parse(
+ JSON.parse(sessionStorage.getItem(storageKey(scope, selection, actorScope)) ?? "null"),
+ );
const input = value.attempt.input;
- if (value.attempt.principalScope !== actorScope || input.project_id !== scope.projectId || input.selection.project_id !== scope.projectId
- || input.selection.entity_id !== selection.entityId || input.selection.entity_type !== selection.entityType
- || input.selection.target_revision_digest !== selection.targetDigest) return null;
+ if (
+ value.attempt.principalScope !== actorScope ||
+ input.project_id !== scope.projectId ||
+ input.selection.project_id !== scope.projectId ||
+ input.selection.entity_id !== selection.entityId ||
+ input.selection.entity_type !== selection.entityType ||
+ input.selection.target_revision_digest !== selection.targetDigest
+ )
+ return null;
return value;
- } catch { return null; }
+ } catch {
+ return null;
+ }
}
-export function savePendingRestore(scope: HistoryScope, selection: RestoreSelection, attempt: RestoreAttempt, operationId: string | null) {
- try { sessionStorage.setItem(storageKey(scope, selection, attempt.principalScope), JSON.stringify({ version: 1, attempt, operationId })); }
- catch { /* In-memory attempt remains valid; no new key is issued on a transport failure. */ }
+export function savePendingRestore(
+ scope: HistoryScope,
+ selection: RestoreSelection,
+ attempt: RestoreAttempt,
+ operationId: string | null,
+) {
+ try {
+ sessionStorage.setItem(
+ storageKey(scope, selection, attempt.principalScope),
+ JSON.stringify({ version: 1, attempt, operationId }),
+ );
+ } catch {
+ /* In-memory attempt remains valid; no new key is issued on a transport failure. */
+ }
}
-export function clearPendingRestore(scope: HistoryScope, selection: RestoreSelection, actorScope: string) {
- try { sessionStorage.removeItem(storageKey(scope, selection, actorScope)); } catch { /* Optional convenience storage. */ }
+export function clearPendingRestore(
+ scope: HistoryScope,
+ selection: RestoreSelection,
+ actorScope: string,
+) {
+ try {
+ sessionStorage.removeItem(storageKey(scope, selection, actorScope));
+ } catch {
+ /* Optional convenience storage. */
+ }
}
diff --git a/apps/web/src/api/resultEvidence.test.ts b/apps/web/src/api/resultEvidence.test.ts
index 9cbe68df..8ba93026 100644
--- a/apps/web/src/api/resultEvidence.test.ts
+++ b/apps/web/src/api/resultEvidence.test.ts
@@ -3,10 +3,18 @@ import { readResultEvidence } from "./resultEvidence";
import { rpc, RpcError } from "./rpcClient";
import { evidenceResponse, selection, manifest, partial } from "./resultEvidenceTestFixture";
-vi.mock("./rpcClient", async original => ({ ...await original(), rpc: vi.fn() }));
+vi.mock("./rpcClient", async (original) => ({
+ ...(await original()),
+ rpc: vi.fn(),
+}));
afterEach(() => vi.resetAllMocks());
function serve() {
- vi.mocked(rpc).mockImplementation(async (_method, args) => evidenceResponse(Number(args.selected_evidence_offset ?? 0), args.include_selected_evidence === true));
+ vi.mocked(rpc).mockImplementation(async (_method, args) =>
+ evidenceResponse(
+ Number(args.selected_evidence_offset ?? 0),
+ args.include_selected_evidence === true,
+ ),
+ );
}
it("reads 149 immutable selected refs in 50/50/49 pages without full-project reads", async () => {
serve();
@@ -14,27 +22,50 @@ it("reads 149 immutable selected refs in 50/50/49 pages without full-project rea
let offset: number | null = 0;
while (offset !== null) {
const page = await readResultEvidence(selection, partial.result, { offset });
- pages.push(page); offset = page.next_offset;
+ pages.push(page);
+ offset = page.next_offset;
}
- expect(pages.map(page => page.items.length)).toEqual([50, 50, 49]);
- expect(pages.flatMap(page => page.items.map(item => item.span_id))).toEqual(manifest.source_refs);
- expect(pages.flatMap(page => page.items).filter(item => item.span).length).toBe(25);
- expect(vi.mocked(rpc).mock.calls.map(([method]) => method)).toEqual(Array(3).fill("thread/result/read"));
- for (const [, args] of vi.mocked(rpc).mock.calls) expect(args.result_revision_digest).toBe(selection.resultDigest);
+ expect(pages.map((page) => page.items.length)).toEqual([50, 50, 49]);
+ expect(pages.flatMap((page) => page.items.map((item) => item.span_id))).toEqual(
+ manifest.source_refs,
+ );
+ expect(pages.flatMap((page) => page.items).filter((item) => item.span).length).toBe(25);
+ expect(vi.mocked(rpc).mock.calls.map(([method]) => method)).toEqual(
+ Array(3).fill("thread/result/read"),
+ );
+ for (const [, args] of vi.mocked(rpc).mock.calls)
+ expect(args.result_revision_digest).toBe(selection.resultDigest);
});
it("resolves an old turn once through metadata and pins its immutable result before expansion", async () => {
serve();
- const page = await readResultEvidence({ ...selection, resultDigest: undefined }, partial.result, { offset: 0 });
+ const page = await readResultEvidence({ ...selection, resultDigest: undefined }, partial.result, {
+ offset: 0,
+ });
expect(page.selected_count).toBe(149);
const calls = vi.mocked(rpc).mock.calls;
expect(calls).toHaveLength(2);
expect(calls[0][1].include_selected_evidence).toBeUndefined();
expect(calls[1][1].result_revision_digest).toBe(manifest.result_revision_digest);
});
-it.each(["project", "thread", "request", "operation", "result", "manifest", "changed-answer", "order", "count", "source-version", "text-hash", "bytes", "unavailable-bytes"])("rejects %s mismatches", async kind => {
+it.each([
+ "project",
+ "thread",
+ "request",
+ "operation",
+ "result",
+ "manifest",
+ "changed-answer",
+ "order",
+ "count",
+ "source-version",
+ "text-hash",
+ "bytes",
+ "unavailable-bytes",
+])("rejects %s mismatches", async (kind) => {
const response = evidenceResponse();
- const value = response.value, page = value.selected_evidence!;
- const item = page.items.find(item => item.span)!;
+ const value = response.value,
+ page = value.selected_evidence!;
+ const item = page.items.find((item) => item.span)!;
if (kind === "project") value.project_id = "foreign";
if (kind === "thread") value.thread_id = "foreign";
if (kind === "request") value.request_revision_digest = "b".repeat(64);
@@ -53,61 +84,123 @@ it.each(["project", "thread", "request", "operation", "result", "manifest", "cha
});
it("keeps unreadable and unknown-basis references instead of counting them as absent", async () => {
const response = evidenceResponse();
- response.value.selected_evidence!.items.forEach(item => { item.availability = "UNKNOWN_BASIS"; item.span = null; });
+ response.value.selected_evidence!.items.forEach((item) => {
+ item.availability = "UNKNOWN_BASIS";
+ item.span = null;
+ });
vi.mocked(rpc).mockResolvedValue(response);
const page = await readResultEvidence(selection, partial.result, { offset: 0 });
- expect(page.selected_count).toBe(149); expect(page.items).toHaveLength(50);
- expect(page.items.every(item => item.span === null)).toBe(true);
+ expect(page.selected_count).toBe(149);
+ expect(page.items).toHaveLength(50);
+ expect(page.items.every((item) => item.span === null)).toBe(true);
});
it("rejects access denial without substituting another source read", async () => {
vi.mocked(rpc).mockRejectedValue(new RpcError("denied", -32040, {}));
- await expect(readResultEvidence(selection, partial.result, { offset: 0 })).rejects.toThrow("denied");
+ await expect(readResultEvidence(selection, partial.result, { offset: 0 })).rejects.toThrow(
+ "denied",
+ );
expect(rpc).toHaveBeenCalledTimes(1);
});
it("supports full results using the same exact manifest contract", async () => {
- const result = { ...partial.result, selected_evidence_refs: manifest.source_refs, portfolio: { hypotheses: [] }, action_plan: { alternatives: [] } };
+ const result = {
+ ...partial.result,
+ selected_evidence_refs: manifest.source_refs,
+ portfolio: { hypotheses: [] },
+ action_plan: { alternatives: [] },
+ };
vi.mocked(rpc).mockResolvedValue(evidenceResponse(0, true, result));
expect((await readResultEvidence(selection, result, { offset: 0 })).selected_count).toBe(149);
});
it("represents a confirmed empty manifest separately from missing metadata", async () => {
const response = evidenceResponse();
response.value.manifest.source_refs = [];
- response.value.selected_evidence = { ...response.value.selected_evidence!, selected_count: 0, items: [], next_offset: null };
+ response.value.selected_evidence = {
+ ...response.value.selected_evidence!,
+ selected_count: 0,
+ items: [],
+ next_offset: null,
+ };
vi.mocked(rpc).mockResolvedValue(response);
- expect((await readResultEvidence(selection, partial.result, { offset: 0 })).selected_count).toBe(0);
+ expect((await readResultEvidence(selection, partial.result, { offset: 0 })).selected_count).toBe(
+ 0,
+ );
vi.mocked(rpc).mockResolvedValue({ ...response, value: { ...response.value, manifest: null } });
- await expect(readResultEvidence(selection, partial.result, { offset: 0 })).rejects.toThrow("저장 버전");
+ await expect(readResultEvidence(selection, partial.result, { offset: 0 })).rejects.toThrow(
+ "저장 버전",
+ );
});
it("binds the historical manifest body without requiring operation-only envelope fields", async () => {
- const { contract_version: _contract, request_epoch: _epoch, thread_id: _thread, ...body } = partial.result;
- void _contract; void _epoch; void _thread;
+ const {
+ contract_version: _contract,
+ request_epoch: _epoch,
+ thread_id: _thread,
+ ...body
+ } = partial.result;
+ void _contract;
+ void _epoch;
+ void _thread;
vi.mocked(rpc).mockResolvedValue(evidenceResponse(0, true, body));
expect((await readResultEvidence(selection, body, { offset: 0 })).selected_count).toBe(149);
- expect((await readResultEvidence(selection, partial.result, { offset: 0 })).selected_count).toBe(149);
- 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();
+ expect((await readResultEvidence(selection, partial.result, { offset: 0 })).selected_count).toBe(
+ 149,
+ );
+ 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));
+ 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);
+ 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, 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("연결을 확인하지 못했습니다");
+ 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("연결을 확인하지 못했습니다");
+ await expect(readResultEvidence(selection, partial.result, { offset: 0 })).rejects.toThrow(
+ "연결을 확인하지 못했습니다",
+ );
});
diff --git a/apps/web/src/api/resultEvidence.ts b/apps/web/src/api/resultEvidence.ts
index 71644eb0..6fc28e9a 100644
--- a/apps/web/src/api/resultEvidence.ts
+++ b/apps/web/src/api/resultEvidence.ts
@@ -6,56 +6,132 @@ import type { HistorySelection } from "./historyModels";
export type ResultSelection = Extract;
const spanSchema = z.object({
- span_id: z.string(), project_id: z.string(), artifact_id: z.string(), source_version_id: z.string(),
- exact_text: z.string(), text_sha256: digestSchema, authority_state: z.string(), cutoff_state: z.string(),
- verification_state: z.string(), support_state: z.string(),
- locator: z.object({ page: z.number().nullable().optional(), line: z.number().nullable().optional(),
- paragraph: z.number().nullable().optional(), sheet: z.string().nullable().optional(),
- cell_range: z.string().nullable().optional(), xml_path: z.string().nullable().optional(), uri: z.string().nullable().optional() }).passthrough(),
+ span_id: z.string(),
+ project_id: z.string(),
+ artifact_id: z.string(),
+ source_version_id: z.string(),
+ exact_text: z.string(),
+ text_sha256: digestSchema,
+ authority_state: z.string(),
+ cutoff_state: z.string(),
+ verification_state: z.string(),
+ support_state: z.string(),
+ locator: z
+ .object({
+ page: z.number().nullable().optional(),
+ line: z.number().nullable().optional(),
+ paragraph: z.number().nullable().optional(),
+ sheet: z.string().nullable().optional(),
+ cell_range: z.string().nullable().optional(),
+ xml_path: z.string().nullable().optional(),
+ uri: z.string().nullable().optional(),
+ })
+ .passthrough(),
});
const pageSchema = z.object({
- schema_version: z.literal("1.0.0"), result_revision_digest: digestSchema, actor_scope_digest: digestSchema,
- selected_count: z.number().int().nonnegative(), offset: z.number().int().nonnegative(), limit: z.number().int().min(1).max(100),
+ schema_version: z.literal("1.0.0"),
+ result_revision_digest: digestSchema,
+ actor_scope_digest: digestSchema,
+ selected_count: z.number().int().nonnegative(),
+ offset: z.number().int().nonnegative(),
+ limit: z.number().int().min(1).max(100),
next_offset: z.number().int().nonnegative().nullable(),
- items: z.array(z.object({ span_id: z.string(), availability: z.enum(["AVAILABLE", "UNAVAILABLE", "BASIS_MISMATCH", "UNKNOWN_BASIS"]),
- reason_codes: z.array(z.string()), span: spanSchema.nullable() })),
+ items: z.array(
+ z.object({
+ span_id: z.string(),
+ availability: z.enum(["AVAILABLE", "UNAVAILABLE", "BASIS_MISMATCH", "UNKNOWN_BASIS"]),
+ reason_codes: z.array(z.string()),
+ span: spanSchema.nullable(),
+ }),
+ ),
+});
+const responseSchema = historicalResultSchema.extend({
+ selected_evidence: pageSchema.nullable().optional(),
+});
+const manifestSchema = z.object({
+ operation_id: z.string(),
+ request_ref: z.object({
+ project_id: z.string(),
+ entity_type: z.string(),
+ entity_id: z.string(),
+ revision_digest: digestSchema,
+ }),
+ source_refs: z.array(z.string()),
+ source_basis: z.record(z.string()).optional(),
+ result: z.record(z.unknown()),
});
-const responseSchema = historicalResultSchema.extend({ selected_evidence: pageSchema.nullable().optional() });
-const manifestSchema = z.object({ operation_id: z.string(), request_ref: z.object({ project_id: z.string(), entity_type: z.string(), entity_id: z.string(), revision_digest: digestSchema }),
- source_refs: z.array(z.string()), source_basis: z.record(z.string()).optional(), result: z.record(z.unknown()) });
export type ResultEvidencePage = z.infer;
export type EvidencePagePosition = { offset: number; resultDigest?: string };
// JSON object key ordering is not part of the stored answer's identity.
export function resultContentKey(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(resultContentKey).join(",")}]`;
- if (value !== null && typeof value === "object") return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${resultContentKey((value as Record)[key])}`).join(",")}}`;
+ if (value !== null && typeof value === "object")
+ return `{${Object.keys(value)
+ .sort()
+ .map(
+ (key) =>
+ `${JSON.stringify(key)}:${resultContentKey((value as Record)[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, stored: Record): boolean {
- return Object.entries(shown).every(([key, value]) => Object.hasOwn(stored, key) && resultContentKey(value) === resultContentKey(stored[key]));
+ return Object.entries(shown).every(
+ ([key, value]) =>
+ Object.hasOwn(stored, key) && resultContentKey(value) === resultContentKey(stored[key]),
+ );
+}
+function mismatch(): never {
+ throw new UiError("evidence.binding");
}
-function mismatch(): never { throw new UiError("evidence.binding"); }
function without(value: Record, keys: readonly string[]): Record {
- return keys.length === 0 ? value : Object.fromEntries(Object.entries(value).filter(([key]) => !keys.includes(key)));
+ return keys.length === 0
+ ? value
+ : Object.fromEntries(Object.entries(value).filter(([key]) => !keys.includes(key)));
}
-function answerContent(value: Record, thread: string, epoch: unknown, terminal: unknown) {
+function answerContent(
+ value: Record,
+ thread: string,
+ epoch: unknown,
+ terminal: unknown,
+) {
// research_threads publication adds these four fields only to operation.result.
// Validate that wrapper before comparing it with the immutable manifest.result.
- if (("thread_id" in value && value.thread_id !== thread) || ("request_epoch" in value && value.request_epoch !== epoch) ||
- ("contract_version" in value && value.contract_version !== 2) || ("terminal_reason" in value && value.terminal_reason !== terminal)) mismatch();
- return Object.fromEntries(Object.entries(value).filter(([key]) => !["thread_id", "request_epoch", "contract_version", "terminal_reason"].includes(key)));
+ if (
+ ("thread_id" in value && value.thread_id !== thread) ||
+ ("request_epoch" in value && value.request_epoch !== epoch) ||
+ ("contract_version" in value && value.contract_version !== 2) ||
+ ("terminal_reason" in value && value.terminal_reason !== terminal)
+ )
+ mismatch();
+ return Object.fromEntries(
+ Object.entries(value).filter(
+ ([key]) =>
+ !["thread_id", "request_epoch", "contract_version", "terminal_reason"].includes(key),
+ ),
+ );
}
type Response = z.infer;
type Manifest = z.infer;
/** The answer on the screen, the result the server returned and the immutable manifest must be the same stored answer. */
-function bindAnswer(selection: ResultSelection, displayed: Record, value: Response, manifest: Manifest, result: Record) {
+function bindAnswer(
+ selection: ResultSelection,
+ displayed: Record,
+ value: Response,
+ manifest: Manifest,
+ result: Record,
+) {
const { scope } = selection;
- const [thread, epoch, terminal] = [scope.threadId, value.request.request_epoch, value.manifest?.terminal_reason];
+ const [thread, epoch, terminal] = [
+ scope.threadId,
+ value.request.request_epoch,
+ value.manifest?.terminal_reason,
+ ];
const displayedContent = answerContent(displayed, thread!, epoch, terminal);
const resultBody = answerContent(result, thread!, epoch, terminal);
const manifestBody = answerContent(manifest.result, thread!, epoch, terminal);
@@ -64,58 +140,146 @@ function bindAnswer(selection: ResultSelection, displayed: Record Object.hasOwn(resultBody, key))) mismatch();
+ const shownBody = value.shown_result
+ ? answerContent(value.shown_result, thread!, epoch, terminal)
+ : manifestBody;
+ if (value.shown_result && !Object.keys(shownBody).every((key) => Object.hasOwn(resultBody, key)))
+ mismatch();
// The latest round is shown from the summary view. A saved earlier round is shown from the stored result itself
// (operation/result/read returns it whole). What the screen shows must be, all of it, a part of one of these forms
// of the same stored answer; keys taken from two forms that disagree are refused.
- const shownIsStored = [shownBody, resultBody, manifestBody].some(body => isShownPartOf(displayedContent, body));
- 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:${thread}` || manifest.request_ref.revision_digest !== scope.requestDigest ||
- resultContentKey(without(resultBody, projected)) !== resultContentKey(without(manifestBody, projected)) || !shownIsStored) mismatch();
+ const shownIsStored = [shownBody, resultBody, manifestBody].some((body) =>
+ isShownPartOf(displayedContent, body),
+ );
+ 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:${thread}` ||
+ manifest.request_ref.revision_digest !== scope.requestDigest ||
+ resultContentKey(without(resultBody, projected)) !==
+ resultContentKey(without(manifestBody, projected)) ||
+ !shownIsStored
+ )
+ mismatch();
}
-async function read(selection: ResultSelection, displayed: Record, digest: string | undefined, offset: number, expand: boolean, signal?: AbortSignal) {
+async function read(
+ selection: ResultSelection,
+ displayed: Record,
+ digest: string | undefined,
+ offset: number,
+ expand: boolean,
+ signal?: AbortSignal,
+) {
const { scope } = selection;
- if (!selection.operationId || !scope.projectId || !scope.threadId || !scope.requestDigest) mismatch();
- const response = await rpc("thread/result/read", { project_id: scope.projectId, thread_id: scope.threadId,
- request_revision_digest: scope.requestDigest, ...(digest ? { result_revision_digest: digest } : {}), contract_version: 2,
- ...(expand ? { include_selected_evidence: true, selected_evidence_offset: offset, selected_evidence_limit: 50 } : {}),
- }, crypto.randomUUID(), signal);
+ if (!selection.operationId || !scope.projectId || !scope.threadId || !scope.requestDigest)
+ mismatch();
+ const response = await rpc(
+ "thread/result/read",
+ {
+ project_id: scope.projectId,
+ thread_id: scope.threadId,
+ request_revision_digest: scope.requestDigest,
+ ...(digest ? { result_revision_digest: digest } : {}),
+ contract_version: 2,
+ ...(expand
+ ? {
+ include_selected_evidence: true,
+ selected_evidence_offset: offset,
+ selected_evidence_limit: 50,
+ }
+ : {}),
+ },
+ crypto.randomUUID(),
+ signal,
+ );
if (response.state !== "SUCCEEDED") throw new UiError("evidence.queryIncomplete");
const value = responseSchema.parse(response.value);
- if (value.project_id !== scope.projectId || value.thread_id !== scope.threadId || value.request_revision_digest !== scope.requestDigest ||
- value.operation_id !== selection.operationId || (digest && value.result_revision_digest !== digest)) mismatch();
+ if (
+ value.project_id !== scope.projectId ||
+ value.thread_id !== scope.threadId ||
+ value.request_revision_digest !== scope.requestDigest ||
+ value.operation_id !== selection.operationId ||
+ (digest && value.result_revision_digest !== digest)
+ )
+ mismatch();
if (value.availability === "UNAVAILABLE") throw new UiError("evidence.accessDenied");
- if (!value.manifest || !value.result_revision_digest) throw new UiError("evidence.versionUnknown");
- if (value.request.project_id !== scope.projectId || value.request.thread_id !== scope.threadId || value.request.operation_id !== selection.operationId || !value.result) mismatch();
+ if (!value.manifest || !value.result_revision_digest)
+ throw new UiError("evidence.versionUnknown");
+ 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);
bindAnswer(selection, displayed, value, manifest, value.result);
return { value, manifest };
}
-export async function readResultEvidence(selection: ResultSelection, displayed: Record, position: EvidencePagePosition, signal?: AbortSignal): Promise {
+export async function readResultEvidence(
+ selection: ResultSelection,
+ displayed: Record,
+ position: EvidencePagePosition,
+ signal?: AbortSignal,
+): Promise {
let digest = position.resultDigest ?? selection.resultDigest;
if (selection.resultDigest && digest !== selection.resultDigest) mismatch();
- if (!digest) digest = (await read(selection, displayed, undefined, 0, false, signal)).value.result_revision_digest!;
- const { value, manifest } = await read(selection, displayed, digest, position.offset, true, signal);
+ if (!digest)
+ digest = (await read(selection, displayed, undefined, 0, false, signal)).value
+ .result_revision_digest!;
+ const { value, manifest } = await read(
+ selection,
+ displayed,
+ digest,
+ position.offset,
+ true,
+ signal,
+ );
const page = value.selected_evidence;
if (!page) throw new UiError("evidence.unsupported");
const expected = manifest.source_refs.slice(position.offset, position.offset + 50);
- const next = position.offset + expected.length < manifest.source_refs.length ? position.offset + expected.length : null;
- if (page.result_revision_digest !== digest || page.offset !== position.offset || page.limit !== 50 ||
- page.selected_count !== manifest.source_refs.length || page.next_offset !== next || page.items.length !== expected.length ||
- new Set(manifest.source_refs).size !== manifest.source_refs.length) mismatch();
- await Promise.all(page.items.map(async (item, index) => {
- if (item.span_id !== expected[index]) mismatch();
- if (item.availability !== "AVAILABLE") { if (item.span !== null) mismatch(); return; }
- const span = item.span;
- if (!span || span.project_id !== selection.scope.projectId || span.span_id !== item.span_id ||
- manifest.source_basis?.[item.span_id] !== `${span.source_version_id}:${span.text_sha256}`) mismatch();
- const hash = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(span.exact_text))))
- .map(byte => byte.toString(16).padStart(2, "0")).join("");
- if (hash !== span.text_sha256.toLowerCase()) mismatch();
- }));
+ const next =
+ position.offset + expected.length < manifest.source_refs.length
+ ? position.offset + expected.length
+ : null;
+ if (
+ page.result_revision_digest !== digest ||
+ page.offset !== position.offset ||
+ page.limit !== 50 ||
+ page.selected_count !== manifest.source_refs.length ||
+ page.next_offset !== next ||
+ page.items.length !== expected.length ||
+ new Set(manifest.source_refs).size !== manifest.source_refs.length
+ )
+ mismatch();
+ await Promise.all(
+ page.items.map(async (item, index) => {
+ if (item.span_id !== expected[index]) mismatch();
+ if (item.availability !== "AVAILABLE") {
+ if (item.span !== null) mismatch();
+ return;
+ }
+ const span = item.span;
+ if (
+ !span ||
+ span.project_id !== selection.scope.projectId ||
+ span.span_id !== item.span_id ||
+ manifest.source_basis?.[item.span_id] !== `${span.source_version_id}:${span.text_sha256}`
+ )
+ mismatch();
+ const hash = Array.from(
+ new Uint8Array(
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(span.exact_text)),
+ ),
+ )
+ .map((byte) => byte.toString(16).padStart(2, "0"))
+ .join("");
+ if (hash !== span.text_sha256.toLowerCase()) mismatch();
+ }),
+ );
if (signal?.aborted) throw signal.reason;
return page;
}
diff --git a/apps/web/src/api/resultEvidenceIdentity.test.ts b/apps/web/src/api/resultEvidenceIdentity.test.ts
index 265ab6e8..c6fe238a 100644
--- a/apps/web/src/api/resultEvidenceIdentity.test.ts
+++ b/apps/web/src/api/resultEvidenceIdentity.test.ts
@@ -3,24 +3,64 @@ import { currentResultDigest } from "./resultEvidenceIdentity";
import fixture from "./fixtures/qa04-evidence-manifest.json";
import type { ResearchStatus } from "./research";
-const scope = { projectId: fixture.project_id, threadId: fixture.thread_id, requestDigest: fixture.request_revision_digest };
+const scope = {
+ projectId: fixture.project_id,
+ threadId: fixture.thread_id,
+ requestDigest: fixture.request_revision_digest,
+};
function status(): ResearchStatus {
- return { project_id: fixture.project_id, thread_id: fixture.thread_id, cycle_id: "fixture-cycle", problem: "fixture", lifecycle: "OPEN", execution_state: "IDLE", current_object_ids: [], working_head_digest: "fixture-head",
- current_result: { operation_id: fixture.operation_id, request_ref: fixture.request_ref, result: {}, phase: "PARTIAL", completion: "TERMINAL", state: "CURRENT", terminal_reason: null, basis_digest: "fixture-basis", gaps: [], next_steps: [], source_refs: fixture.source_refs, record_refs: [] },
- attempt: { operation_id: fixture.operation_id, request_ref: fixture.request_ref, phase: "PARTIAL", status: "FAILED",
- checkpoint_ref: { project_id: fixture.project_id, entity_type: "DECISION_OBJECT", entity_id: `result:${fixture.thread_id}`, revision_digest: fixture.result_revision_digest } },
+ return {
+ project_id: fixture.project_id,
+ thread_id: fixture.thread_id,
+ cycle_id: "fixture-cycle",
+ problem: "fixture",
+ lifecycle: "OPEN",
+ execution_state: "IDLE",
+ current_object_ids: [],
+ working_head_digest: "fixture-head",
+ current_result: {
+ operation_id: fixture.operation_id,
+ request_ref: fixture.request_ref,
+ result: {},
+ phase: "PARTIAL",
+ completion: "TERMINAL",
+ state: "CURRENT",
+ terminal_reason: null,
+ basis_digest: "fixture-basis",
+ gaps: [],
+ next_steps: [],
+ source_refs: fixture.source_refs,
+ record_refs: [],
+ },
+ attempt: {
+ operation_id: fixture.operation_id,
+ request_ref: fixture.request_ref,
+ phase: "PARTIAL",
+ status: "FAILED",
+ checkpoint_ref: {
+ project_id: fixture.project_id,
+ entity_type: "DECISION_OBJECT",
+ entity_id: `result:${fixture.thread_id}`,
+ revision_digest: fixture.result_revision_digest,
+ },
+ },
};
}
it("pins the synthetic checkpoint's immutable result identity", () => {
- expect(currentResultDigest(status(), scope, fixture.operation_id)).toBe(fixture.result_revision_digest);
-});
-it.each(["project", "thread", "operation", "request", "checkpoint", "missing"])("never borrows an unrelated %s binding", kind => {
- const value = status();
- if (kind === "project") value.project_id = "other";
- if (kind === "thread") value.thread_id = "other";
- if (kind === "operation") value.attempt!.operation_id = "other";
- if (kind === "request") value.attempt!.request_ref = { revision_digest: "f".repeat(64) };
- if (kind === "checkpoint") value.attempt!.checkpoint_ref!.entity_id = "result:other";
- if (kind === "missing") value.attempt!.checkpoint_ref = null;
- expect(currentResultDigest(value, scope, fixture.operation_id)).toBeUndefined();
+ expect(currentResultDigest(status(), scope, fixture.operation_id)).toBe(
+ fixture.result_revision_digest,
+ );
});
+it.each(["project", "thread", "operation", "request", "checkpoint", "missing"])(
+ "never borrows an unrelated %s binding",
+ (kind) => {
+ const value = status();
+ if (kind === "project") value.project_id = "other";
+ if (kind === "thread") value.thread_id = "other";
+ if (kind === "operation") value.attempt!.operation_id = "other";
+ if (kind === "request") value.attempt!.request_ref = { revision_digest: "f".repeat(64) };
+ if (kind === "checkpoint") value.attempt!.checkpoint_ref!.entity_id = "result:other";
+ if (kind === "missing") value.attempt!.checkpoint_ref = null;
+ expect(currentResultDigest(value, scope, fixture.operation_id)).toBeUndefined();
+ },
+);
diff --git a/apps/web/src/api/resultEvidenceIdentity.ts b/apps/web/src/api/resultEvidenceIdentity.ts
index e0756740..250900f3 100644
--- a/apps/web/src/api/resultEvidenceIdentity.ts
+++ b/apps/web/src/api/resultEvidenceIdentity.ts
@@ -2,16 +2,30 @@ import type { ResearchStatus } from "./research";
import type { HistoryScope } from "./historyModels";
/** Pin a displayed checkpoint only when every identity comes from the same read. */
-export function currentResultDigest(status: ResearchStatus | undefined, scope: HistoryScope, operationId: string): string | undefined {
+export function currentResultDigest(
+ status: ResearchStatus | undefined,
+ scope: HistoryScope,
+ operationId: string,
+): string | undefined {
const manifest = status?.current_result ?? status?.previous_result;
const attempt = status?.attempt;
const checkpoint = attempt?.checkpoint_ref;
- if (!status || !manifest || !attempt || !checkpoint ||
- status.project_id !== scope.projectId || status.thread_id !== scope.threadId ||
- manifest.operation_id !== operationId || attempt.operation_id !== operationId ||
- manifest.request_ref?.revision_digest !== scope.requestDigest ||
- attempt.request_ref?.revision_digest !== scope.requestDigest ||
- checkpoint.project_id !== scope.projectId || checkpoint.entity_type !== "DECISION_OBJECT" ||
- checkpoint.entity_id !== `result:${scope.threadId}` || !/^[a-f0-9]{64}$/i.test(checkpoint.revision_digest)) return undefined;
+ if (
+ !status ||
+ !manifest ||
+ !attempt ||
+ !checkpoint ||
+ status.project_id !== scope.projectId ||
+ status.thread_id !== scope.threadId ||
+ manifest.operation_id !== operationId ||
+ attempt.operation_id !== operationId ||
+ manifest.request_ref?.revision_digest !== scope.requestDigest ||
+ attempt.request_ref?.revision_digest !== scope.requestDigest ||
+ checkpoint.project_id !== scope.projectId ||
+ checkpoint.entity_type !== "DECISION_OBJECT" ||
+ checkpoint.entity_id !== `result:${scope.threadId}` ||
+ !/^[a-f0-9]{64}$/i.test(checkpoint.revision_digest)
+ )
+ return undefined;
return checkpoint.revision_digest;
}
diff --git a/apps/web/src/api/resultEvidenceRuntime.test.ts b/apps/web/src/api/resultEvidenceRuntime.test.ts
index efd43341..72f39bbd 100644
--- a/apps/web/src/api/resultEvidenceRuntime.test.ts
+++ b/apps/web/src/api/resultEvidenceRuntime.test.ts
@@ -6,70 +6,120 @@ import fixture from "./fixtures/result-evidence-runtime.json";
/* Responses a real runtime made for a controlled (fake) model run, read as the web client reads them
* (expected_result_contract v5). Regenerate with: python -m tests.integration.result_evidence_runtime
* tests/integration/test_result_shown_view.py checks the file against the server's own functions. */
-vi.mock("./rpcClient", async original => ({ ...await original(), rpc: vi.fn() }));
+vi.mock("./rpcClient", async (original) => ({
+ ...(await original()),
+ rpc: vi.fn(),
+}));
afterEach(() => vi.resetAllMocks());
type Wire = Record; // eslint-disable-line @typescript-eslint/no-explicit-any
-type Case = { selection: ResultSelection["scope"] & { operationId: string; resultDigest: string }; displayed: Wire; read: Wire; evidence_page: Wire };
-const cases = fixture.cases as unknown as Record<"stored_current" | "stored_before_grounded_rows", Case>;
-const selectionOf = (c: Case): ResultSelection => ({ kind: "result", scope: { projectId: c.selection.projectId, threadId: c.selection.threadId!, requestDigest: c.selection.requestDigest! },
- operationId: c.selection.operationId, resultDigest: c.selection.resultDigest });
+type Case = {
+ selection: ResultSelection["scope"] & { operationId: string; resultDigest: string };
+ displayed: Wire;
+ read: Wire;
+ evidence_page: Wire;
+};
+const cases = fixture.cases as unknown as Record<
+ "stored_current" | "stored_before_grounded_rows",
+ Case
+>;
+const selectionOf = (c: Case): ResultSelection => ({
+ kind: "result",
+ scope: {
+ projectId: c.selection.projectId,
+ threadId: c.selection.threadId!,
+ requestDigest: c.selection.requestDigest!,
+ },
+ operationId: c.selection.operationId,
+ resultDigest: c.selection.resultDigest,
+});
/** Serves the stored responses; "edit" changes what the server says (a copy per call). */
function serve(c: Case, edit?: (value: Wire) => void) {
vi.mocked(rpc).mockImplementation(async (_method, args) => {
- const value: Wire = structuredClone(args.include_selected_evidence === true ? { ...c.read, selected_evidence: c.evidence_page } : c.read);
+ const value: Wire = structuredClone(
+ args.include_selected_evidence === true
+ ? { ...c.read, selected_evidence: c.evidence_page }
+ : c.read,
+ );
edit?.(value);
return { operation_id: "", state: "SUCCEEDED", value };
});
}
const BINDING = "연결을 확인하지 못했습니다";
-const show = (c: Case, displayed: Wire = c.displayed) => readResultEvidence(selectionOf(c), displayed, { offset: 0 });
+const show = (c: Case, displayed: Wire = c.displayed) =>
+ readResultEvidence(selectionOf(c), displayed, { offset: 0 });
-describe.each(["stored_current", "stored_before_grounded_rows"] as const)("the answer a real runtime stored, %s", name => {
- const c = cases[name];
- it("opens the evidence for the answer the screen shows", async () => {
- serve(c);
- const page = await show(c);
- expect(page.selected_count).toBeGreaterThan(0);
- expect(page.items.filter(item => item.availability === "AVAILABLE").length).toBe(page.items.length);
- });
- it("still rejects a different answer, a changed summary, a key the answer does not have, or a shown value left out of the result", async () => {
- serve(c);
- await expect(show(c, { ...c.displayed, answer: "another answer" })).rejects.toThrow(BINDING);
- await expect(show(c, { ...c.displayed, absence_assessments: { ...c.displayed.absence_assessments, not_checked_count: 99 } })).rejects.toThrow(BINDING);
- await expect(show(c, { ...c.displayed, invented_key: 1 })).rejects.toThrow(BINDING);
- serve(c, value => { value.shown_result = { ...value.shown_result, invented_key: 1 }; });
- await expect(show(c, { ...c.displayed, invented_key: 1 })).rejects.toThrow(BINDING);
- });
- it("still needs the result and the manifest to agree on every key the server did not declare changed", async () => {
- serve(c, value => { value.result = { ...value.result, answer: "another answer" }; value.shown_result = { ...value.shown_result, answer: "another answer" }; });
- await expect(show(c, { ...c.displayed, answer: "another answer" })).rejects.toThrow(BINDING);
- });
- // An earlier round of a conversation is shown from operation/result/read, which returns the stored result whole,
- // not the summary view the latest round is shown from.
- it("opens the evidence for an earlier round, which the screen shows from the stored result itself", async () => {
- serve(c);
- const whole = c.read.result as Wire;
- // it is not the summary view: at least one of the two summarized records is there in full
- expect(["absence_assessments", "checked_observations"].some(key => JSON.stringify(whole[key]) !== JSON.stringify(c.displayed[key]))).toBe(true);
- expect((await show(c, whole)).selected_count).toBeGreaterThan(0);
- expect((await show(c, c.read.manifest.result as Wire)).selected_count).toBeGreaterThan(0);
- });
- it("still rejects a whole result with another answer or an invented key, and one put together from two forms", async () => {
- serve(c);
- const whole = c.read.result as Wire;
- await expect(show(c, { ...whole, answer: "another answer" })).rejects.toThrow(BINDING);
- await expect(show(c, { ...whole, invented_key: 1 })).rejects.toThrow(BINDING);
- // the summary of one record inside the whole result: each key is right in one form, the whole is right in none
- const mixedKey = (["absence_assessments", "checked_observations"] as const).find(key => JSON.stringify(whole[key]) !== JSON.stringify(c.displayed[key]));
- expect(mixedKey).toBeDefined();
- await expect(show(c, { ...whole, [mixedKey!]: c.displayed[mixedKey!] })).rejects.toThrow(BINDING);
- });
-});
+describe.each(["stored_current", "stored_before_grounded_rows"] as const)(
+ "the answer a real runtime stored, %s",
+ (name) => {
+ const c = cases[name];
+ it("opens the evidence for the answer the screen shows", async () => {
+ serve(c);
+ const page = await show(c);
+ expect(page.selected_count).toBeGreaterThan(0);
+ expect(page.items.filter((item) => item.availability === "AVAILABLE").length).toBe(
+ page.items.length,
+ );
+ });
+ it("still rejects a different answer, a changed summary, a key the answer does not have, or a shown value left out of the result", async () => {
+ serve(c);
+ await expect(show(c, { ...c.displayed, answer: "another answer" })).rejects.toThrow(BINDING);
+ await expect(
+ show(c, {
+ ...c.displayed,
+ absence_assessments: { ...c.displayed.absence_assessments, not_checked_count: 99 },
+ }),
+ ).rejects.toThrow(BINDING);
+ await expect(show(c, { ...c.displayed, invented_key: 1 })).rejects.toThrow(BINDING);
+ serve(c, (value) => {
+ value.shown_result = { ...value.shown_result, invented_key: 1 };
+ });
+ await expect(show(c, { ...c.displayed, invented_key: 1 })).rejects.toThrow(BINDING);
+ });
+ it("still needs the result and the manifest to agree on every key the server did not declare changed", async () => {
+ serve(c, (value) => {
+ value.result = { ...value.result, answer: "another answer" };
+ value.shown_result = { ...value.shown_result, answer: "another answer" };
+ });
+ await expect(show(c, { ...c.displayed, answer: "another answer" })).rejects.toThrow(BINDING);
+ });
+ // An earlier round of a conversation is shown from operation/result/read, which returns the stored result whole,
+ // not the summary view the latest round is shown from.
+ it("opens the evidence for an earlier round, which the screen shows from the stored result itself", async () => {
+ serve(c);
+ const whole = c.read.result as Wire;
+ // it is not the summary view: at least one of the two summarized records is there in full
+ expect(
+ ["absence_assessments", "checked_observations"].some(
+ (key) => JSON.stringify(whole[key]) !== JSON.stringify(c.displayed[key]),
+ ),
+ ).toBe(true);
+ expect((await show(c, whole)).selected_count).toBeGreaterThan(0);
+ expect((await show(c, c.read.manifest.result as Wire)).selected_count).toBeGreaterThan(0);
+ });
+ it("still rejects a whole result with another answer or an invented key, and one put together from two forms", async () => {
+ serve(c);
+ const whole = c.read.result as Wire;
+ await expect(show(c, { ...whole, answer: "another answer" })).rejects.toThrow(BINDING);
+ await expect(show(c, { ...whole, invented_key: 1 })).rejects.toThrow(BINDING);
+ // the summary of one record inside the whole result: each key is right in one form, the whole is right in none
+ const mixedKey = (["absence_assessments", "checked_observations"] as const).find(
+ (key) => JSON.stringify(whole[key]) !== JSON.stringify(c.displayed[key]),
+ );
+ expect(mixedKey).toBeDefined();
+ await expect(show(c, { ...whole, [mixedKey!]: c.displayed[mixedKey!] })).rejects.toThrow(
+ BINDING,
+ );
+ });
+ },
+);
describe("a server that does not send shown_result or projected_keys (older)", () => {
- const old = (value: Wire) => { delete value.shown_result; delete value.projected_keys; };
+ const old = (value: Wire) => {
+ delete value.shown_result;
+ delete value.projected_keys;
+ };
it("is read as before: the screen must hold keys of the stored result with the stored values", async () => {
const c = cases.stored_current;
serve(c, old);
diff --git a/apps/web/src/api/resultEvidenceServerContract.test.ts b/apps/web/src/api/resultEvidenceServerContract.test.ts
index 7a2be9f1..dfded717 100644
--- a/apps/web/src/api/resultEvidenceServerContract.test.ts
+++ b/apps/web/src/api/resultEvidenceServerContract.test.ts
@@ -7,17 +7,45 @@ import { evidenceResponse, manifest } from "./resultEvidenceTestFixture";
it("matches the actual selected-evidence Python DTOs without starting an engine", () => {
const repository = fileURLToPath(new URL("../../../../", import.meta.url));
- const python = [join(repository, ".venv", "Scripts", "python.exe"), join(repository, ".venv", "bin", "python")].find(existsSync);
- if (!python) throw new Error("The repository Python environment is required for wire validation.");
- const cases = [0, 50, 100].map(offset => ({ input: { project_id: manifest.project_id, thread_id: manifest.thread_id,
- request_revision_digest: manifest.request_revision_digest, result_revision_digest: manifest.result_revision_digest,
- include_selected_evidence: true, selected_evidence_offset: offset, selected_evidence_limit: 50, contract_version: 2 },
- value: evidenceResponse(offset).value }));
- const source = ["import json,sys", "from thoth.domain.research_history import HistoricalResultInput, HistoricalResultView",
- "items=json.load(sys.stdin)", "counts=[]", "for item in items:",
- " HistoricalResultInput.model_validate(item['input'])", " value=HistoricalResultView.model_validate(item['value'])",
- " counts.append(len(value.selected_evidence.items))", "print(json.dumps(counts))"].join("\n");
- const validated = JSON.parse(execFileSync(python, ["-c", source], { shell: false, encoding: "utf8", input: JSON.stringify(cases),
- cwd: repository, timeout: 20000, env: { ...process.env, PYTHONPATH: join(repository, "src"), PYTHONDONTWRITEBYTECODE: "1" } }));
+ const python = [
+ join(repository, ".venv", "Scripts", "python.exe"),
+ join(repository, ".venv", "bin", "python"),
+ ].find(existsSync);
+ if (!python)
+ throw new Error("The repository Python environment is required for wire validation.");
+ const cases = [0, 50, 100].map((offset) => ({
+ input: {
+ project_id: manifest.project_id,
+ thread_id: manifest.thread_id,
+ request_revision_digest: manifest.request_revision_digest,
+ result_revision_digest: manifest.result_revision_digest,
+ include_selected_evidence: true,
+ selected_evidence_offset: offset,
+ selected_evidence_limit: 50,
+ contract_version: 2,
+ },
+ value: evidenceResponse(offset).value,
+ }));
+ const source = [
+ "import json,sys",
+ "from thoth.domain.research_history import HistoricalResultInput, HistoricalResultView",
+ "items=json.load(sys.stdin)",
+ "counts=[]",
+ "for item in items:",
+ " HistoricalResultInput.model_validate(item['input'])",
+ " value=HistoricalResultView.model_validate(item['value'])",
+ " counts.append(len(value.selected_evidence.items))",
+ "print(json.dumps(counts))",
+ ].join("\n");
+ const validated = JSON.parse(
+ execFileSync(python, ["-c", source], {
+ shell: false,
+ encoding: "utf8",
+ input: JSON.stringify(cases),
+ cwd: repository,
+ timeout: 20000,
+ env: { ...process.env, PYTHONPATH: join(repository, "src"), PYTHONDONTWRITEBYTECODE: "1" },
+ }),
+ );
expect(validated).toEqual([50, 50, 49]);
});
diff --git a/apps/web/src/api/resultEvidenceTestFixture.ts b/apps/web/src/api/resultEvidenceTestFixture.ts
index d40c9c36..88a0ab84 100644
--- a/apps/web/src/api/resultEvidenceTestFixture.ts
+++ b/apps/web/src/api/resultEvidenceTestFixture.ts
@@ -4,24 +4,76 @@ import citations from "./fixtures/qa04-cited-spans.json";
import type { ResultSelection } from "./resultEvidence";
export { manifest, partial, citations };
-export const selection: ResultSelection = { kind: "result", scope: partial.scope, operationId: partial.operationId, resultDigest: manifest.result_revision_digest };
+export const selection: ResultSelection = {
+ kind: "result",
+ scope: partial.scope,
+ operationId: partial.operationId,
+ resultDigest: manifest.result_revision_digest,
+};
// Fictional greenhouse text, identities, hashes and locators are synthetic; status badges below are test-only.
-export function evidenceResponse(offset = 0, expand = true, result: Record = partial.result) {
- return { operation_id: "", state: "SUCCEEDED", value: {
- contract_version: 2, project_id: manifest.project_id, thread_id: manifest.thread_id,
- request_revision_digest: manifest.request_revision_digest, operation_id: manifest.operation_id,
- result_revision_digest: manifest.result_revision_digest, request: { project_id: manifest.project_id, thread_id: manifest.thread_id,
- operation_id: manifest.operation_id, request_epoch: partial.result.request_epoch }, manifest: { ...manifest, result }, result,
- error: null, availability: "AVAILABLE", basis_currentness: { state: "CURRENT", reasons: [], execution_eligible: false },
- coverage: { visibility: "AUTHORIZED_SUBSET", association: "EXACT", scan: "COMPLETE_PAGE", reasons: [] },
- selected_evidence: expand ? { schema_version: "1.0.0", result_revision_digest: manifest.result_revision_digest,
- actor_scope_digest: "a".repeat(64), selected_count: manifest.source_refs.length, offset, limit: 50,
- next_offset: offset + 50 < manifest.source_refs.length ? offset + 50 : null,
- items: manifest.source_refs.slice(offset, offset + 50).map(span_id => {
- const span = citations.find(item => item.span_id === span_id);
- return { span_id, availability: span ? "AVAILABLE" : "UNAVAILABLE", reason_codes: span ? [] : ["TEST_FIXTURE_NOT_INCLUDED"],
- span: span ? { ...span, project_id: manifest.project_id, authority_state: "UNCLASSIFIED", cutoff_state: "ELIGIBLE", verification_state: "NOT_CHECKED", support_state: "EXTRACTED", extraction_method: "test-fixture", schema_version: "1.0.0" } : null };
- }),
- } : null,
- } };
+export function evidenceResponse(
+ offset = 0,
+ expand = true,
+ result: Record = partial.result,
+) {
+ return {
+ operation_id: "",
+ state: "SUCCEEDED",
+ value: {
+ contract_version: 2,
+ project_id: manifest.project_id,
+ thread_id: manifest.thread_id,
+ request_revision_digest: manifest.request_revision_digest,
+ operation_id: manifest.operation_id,
+ result_revision_digest: manifest.result_revision_digest,
+ request: {
+ project_id: manifest.project_id,
+ thread_id: manifest.thread_id,
+ operation_id: manifest.operation_id,
+ request_epoch: partial.result.request_epoch,
+ },
+ manifest: { ...manifest, result },
+ result,
+ error: null,
+ availability: "AVAILABLE",
+ basis_currentness: { state: "CURRENT", reasons: [], execution_eligible: false },
+ coverage: {
+ visibility: "AUTHORIZED_SUBSET",
+ association: "EXACT",
+ scan: "COMPLETE_PAGE",
+ reasons: [],
+ },
+ selected_evidence: expand
+ ? {
+ schema_version: "1.0.0",
+ result_revision_digest: manifest.result_revision_digest,
+ actor_scope_digest: "a".repeat(64),
+ selected_count: manifest.source_refs.length,
+ offset,
+ limit: 50,
+ next_offset: offset + 50 < manifest.source_refs.length ? offset + 50 : null,
+ items: manifest.source_refs.slice(offset, offset + 50).map((span_id) => {
+ const span = citations.find((item) => item.span_id === span_id);
+ return {
+ span_id,
+ availability: span ? "AVAILABLE" : "UNAVAILABLE",
+ reason_codes: span ? [] : ["TEST_FIXTURE_NOT_INCLUDED"],
+ span: span
+ ? {
+ ...span,
+ project_id: manifest.project_id,
+ authority_state: "UNCLASSIFIED",
+ cutoff_state: "ELIGIBLE",
+ verification_state: "NOT_CHECKED",
+ support_state: "EXTRACTED",
+ extraction_method: "test-fixture",
+ schema_version: "1.0.0",
+ }
+ : null,
+ };
+ }),
+ }
+ : null,
+ },
+ };
}
diff --git a/apps/web/src/api/rpcClient.test.ts b/apps/web/src/api/rpcClient.test.ts
index 5feb80e4..8f371dbc 100644
--- a/apps/web/src/api/rpcClient.test.ts
+++ b/apps/web/src/api/rpcClient.test.ts
@@ -8,45 +8,72 @@ afterEach(() => {
});
const reason = "REFERENCE_INQUIRY_NOT_FOUND";
-const notice = `연구의 중간 기록은 저장됐습니다. 기준(reference) 단계만 실패했습니다 (사유: ${reason}). `
- + "결과는 기록에서 볼 수 있습니다. 같은 질문을 새로 보내면 연구가 다시 실행될 수 있으니 먼저 저장된 결과를 확인하세요. / "
- + `Intermediate research records were saved. Only the reference step failed (reason: ${reason}). `
- + "You can read the result in the history. Sending the same question again as a new request may run the research again, so check the saved result first.";
+const notice =
+ `연구의 중간 기록은 저장됐습니다. 기준(reference) 단계만 실패했습니다 (사유: ${reason}). ` +
+ "결과는 기록에서 볼 수 있습니다. 같은 질문을 새로 보내면 연구가 다시 실행될 수 있으니 먼저 저장된 결과를 확인하세요. / " +
+ `Intermediate research records were saved. Only the reference step failed (reason: ${reason}). ` +
+ "You can read the result in the history. Sending the same question again as a new request may run the research again, so check the saved result first.";
it("preserves the bilingual reference notice and saved-record metadata from an RPC error", async () => {
const data = { reason_code: reason, research_saved: true };
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json({
- jsonrpc: "2.0", id: "notice", error: { code: -32030, message: notice, data },
- })));
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ Response.json({
+ jsonrpc: "2.0",
+ id: "notice",
+ error: { code: -32030, message: notice, data },
+ }),
+ ),
+ );
await expect(rpc("thread/input", { project_id: "p" }, "reference-notice")).rejects.toMatchObject({
- name: "RpcError", code: -32030, message: `${notice} [-32030]`, details: data,
+ name: "RpcError",
+ code: -32030,
+ message: `${notice} [-32030]`,
+ details: data,
});
});
it("says an unexpected server failure in the screen language, with the operation id the server log is found by", async () => {
setLocale("ko");
- vi.stubGlobal("fetch", vi.fn().mockImplementation(async () => Response.json({
- jsonrpc: "2.0",
- id: "internal",
- error: { code: -32603, message: "internal command failure", data: { operation_id: "operation:42" } },
- })));
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockImplementation(async () =>
+ Response.json({
+ jsonrpc: "2.0",
+ id: "internal",
+ error: {
+ code: -32603,
+ message: "internal command failure",
+ data: { operation_id: "operation:42" },
+ },
+ }),
+ ),
+ );
await expect(rpc("thread/input", { project_id: "p" }, "internal-failure")).rejects.toMatchObject({
name: "RpcError",
code: -32603,
message: "서버가 이 요청을 처리하지 못했습니다. [-32603] (operation:42)",
});
setLocale("en");
- await expect(rpc("thread/input", { project_id: "p" }, "internal-failure-en")).rejects.toMatchObject({
+ await expect(
+ rpc("thread/input", { project_id: "p" }, "internal-failure-en"),
+ ).rejects.toMatchObject({
message: "The server could not complete this request. [-32603] (operation:42)",
});
});
it("keeps a specific message the server wrote for an internal error", async () => {
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue(Response.json({
- jsonrpc: "2.0",
- id: "specific",
- error: { code: -32603, message: "index is being rebuilt" },
- })));
+ vi.stubGlobal(
+ "fetch",
+ vi.fn().mockResolvedValue(
+ Response.json({
+ jsonrpc: "2.0",
+ id: "specific",
+ error: { code: -32603, message: "index is being rebuilt" },
+ }),
+ ),
+ );
await expect(rpc("thread/input", { project_id: "p" }, "specific")).rejects.toMatchObject({
message: "index is being rebuilt [-32603]",
});
@@ -54,17 +81,22 @@ it("keeps a specific message the server wrote for an internal error", async () =
it("adds no operation id to other errors, or when the server gave none", async () => {
setLocale("ko");
- const fetchMock = vi.fn()
- .mockResolvedValueOnce(Response.json({
- jsonrpc: "2.0",
- id: "refused",
- error: { code: -32030, message: "refused", data: { operation_id: "operation:43" } },
- }))
- .mockResolvedValueOnce(Response.json({
- jsonrpc: "2.0",
- id: "bare",
- error: { code: -32603, message: "internal command failure" },
- }));
+ const fetchMock = vi
+ .fn()
+ .mockResolvedValueOnce(
+ Response.json({
+ jsonrpc: "2.0",
+ id: "refused",
+ error: { code: -32030, message: "refused", data: { operation_id: "operation:43" } },
+ }),
+ )
+ .mockResolvedValueOnce(
+ Response.json({
+ jsonrpc: "2.0",
+ id: "bare",
+ error: { code: -32603, message: "internal command failure" },
+ }),
+ );
vi.stubGlobal("fetch", fetchMock);
await expect(rpc("thread/input", { project_id: "p" }, "refused")).rejects.toMatchObject({
message: "refused [-32030]",
diff --git a/apps/web/src/api/rpcClient.ts b/apps/web/src/api/rpcClient.ts
index 5fed93a7..87bd42b6 100644
--- a/apps/web/src/api/rpcClient.ts
+++ b/apps/web/src/api/rpcClient.ts
@@ -7,22 +7,54 @@ export type RpcResult = {
state: string;
value: T;
};
-export const readQueries = new Set(["thread/read", "thread/list", "thread/activity/list", "model/settings/read",
- "model/localAsset/read", "project/retrieval/experiment/read",
- "model/credential/list", "model/credential/login/status", "workspace/setup/read", "workspace/ready",
- "project/read", "project/list", "project/source/list", "evidence/list", "operation/read",
- "operation/result/read", "operation/checkpoint/read",
- "revision/timeline/read", "revision/timeline/item/read", "thread/result/read",
- "thread/result/compare/read", "project/review/list",
- "revision/diff/read", "revision/restore/preview", "revision/read", "revision/content/read",
- "revision/head/read", "revision/history/read", "trace/read", "trace/export", "trace/importPreview", "trace/history", "hypothesis/link/list", "hypothesis/test/result/list", "hypothesis/same/list", "trace/closure/list", "trace/lesson/list"]);
+export const readQueries = new Set([
+ "thread/read",
+ "thread/list",
+ "thread/activity/list",
+ "model/settings/read",
+ "model/localAsset/read",
+ "project/retrieval/experiment/read",
+ "model/credential/list",
+ "model/credential/login/status",
+ "workspace/setup/read",
+ "workspace/ready",
+ "project/read",
+ "project/list",
+ "project/source/list",
+ "evidence/list",
+ "operation/read",
+ "operation/result/read",
+ "operation/checkpoint/read",
+ "revision/timeline/read",
+ "revision/timeline/item/read",
+ "thread/result/read",
+ "thread/result/compare/read",
+ "project/review/list",
+ "revision/diff/read",
+ "revision/restore/preview",
+ "revision/read",
+ "revision/content/read",
+ "revision/head/read",
+ "revision/history/read",
+ "trace/read",
+ "trace/export",
+ "trace/importPreview",
+ "trace/history",
+ "hypothesis/link/list",
+ "hypothesis/test/result/list",
+ "hypothesis/same/list",
+ "trace/closure/list",
+ "trace/lesson/list",
+]);
const INTERNAL_ERROR = -32603;
/** What the server answers for any unexpected failure. The screen says it in the person's language. */
const INTERNAL_FAILURE_TEXT = "internal command failure";
function shownMessage(message: string, code: number): string {
- return code === INTERNAL_ERROR && message === INTERNAL_FAILURE_TEXT ? t("rpc.internalFailure") : message;
+ return code === INTERNAL_ERROR && message === INTERNAL_FAILURE_TEXT
+ ? t("rpc.internalFailure")
+ : message;
}
/** The server logs an unexpected failure under its operation id; showing the id lets a report name it. */
@@ -33,23 +65,40 @@ function internalFailureRef(code: number, details: Record | und
}
export class RpcError extends Error {
- constructor(message: string, readonly code: number, readonly details: Record