From 3b57366d3f7a08fb326a0f84446f8b5bf5270ff9 Mon Sep 17 00:00:00 2001 From: Hyperdrive <255666866+hyperdrivebeep@users.noreply.github.com> Date: Mon, 12 Oct 2026 23:55:26 +0900 Subject: [PATCH 1/2] feat: add Korean and English language support --- README.ko.md | 4 +- README.md | 7 +- SOURCE_MANIFEST.json | 1218 ++++++++---- apps/web/src/App.tsx | 8 +- apps/web/src/api/capabilities.ts | 18 +- apps/web/src/api/conversation.ts | 51 +- apps/web/src/api/conversationRefresh.ts | 3 +- apps/web/src/api/draftLanguage.ts | 22 + apps/web/src/api/expectedResultState.test.ts | 10 + apps/web/src/api/expectedResultState.ts | 6 + apps/web/src/api/files.ts | 3 +- apps/web/src/api/history.ts | 12 +- apps/web/src/api/historyComparison.ts | 8 +- apps/web/src/api/localWorkspacePersistence.ts | 31 +- apps/web/src/api/publicWeb.ts | 15 +- apps/web/src/api/recordViews.ts | 45 +- apps/web/src/api/requestLanguage.test.ts | 31 + apps/web/src/api/research.ts | 9 +- apps/web/src/api/researchFollowup.ts | 7 +- apps/web/src/api/researchProgress.ts | 27 +- apps/web/src/api/researchProgressText.ts | 459 +---- apps/web/src/api/researchStatusRead.test.ts | 7 + apps/web/src/api/researchStatusRead.ts | 8 +- apps/web/src/api/restore.ts | 20 +- apps/web/src/api/resultEvidence.ts | 11 +- .../web/src/api/resultViewNegotiation.test.ts | 29 + apps/web/src/api/rpcClient.ts | 268 +-- apps/web/src/api/sourcePolicy.ts | 11 +- apps/web/src/api/sourceTime.ts | 103 +- .../web/src/api/submissionPersistence.test.ts | 28 + apps/web/src/api/submissionPersistence.ts | 28 + apps/web/src/components/ActionCompare.tsx | 34 +- apps/web/src/components/ActionEffort.tsx | 30 +- apps/web/src/components/AnswerBody.tsx | 8 +- apps/web/src/components/AnswerHold.tsx | 10 +- .../src/components/AnswerReadability.test.tsx | 11 + apps/web/src/components/ApprovalCard.tsx | 28 +- apps/web/src/components/ApprovalSection.tsx | 7 +- apps/web/src/components/ArchivedProjects.tsx | 17 +- .../src/components/AutoRetrySwitch.test.tsx | 5 +- apps/web/src/components/AutoRetrySwitch.tsx | 10 +- .../src/components/CapabilityWorkbench.tsx | 52 +- apps/web/src/components/ClaudeCodeHelp.tsx | 5 +- apps/web/src/components/ClosureMark.tsx | 6 +- apps/web/src/components/ClosureRecorder.tsx | 31 +- apps/web/src/components/CodexToolHelp.tsx | 5 +- apps/web/src/components/ConfirmedFacts.tsx | 18 +- .../src/components/ConversationTimeline.tsx | 38 +- apps/web/src/components/DomainRecords.tsx | 72 +- .../components/EffectDeclarationFields.tsx | 12 +- apps/web/src/components/EvidenceRail.tsx | 35 +- .../web/src/components/EvidenceUseSummary.tsx | 7 +- .../src/components/ExecutionHoldNotice.tsx | 43 +- apps/web/src/components/ExpectedResults.tsx | 14 +- .../src/components/FileConnectionPanel.tsx | 81 +- apps/web/src/components/FirstRunSetup.tsx | 75 +- apps/web/src/components/HypothesisCompare.tsx | 52 +- .../src/components/HypothesisCompareLive.tsx | 5 +- .../HypothesisContractSwitch.test.tsx | 2 +- .../components/HypothesisContractSwitch.tsx | 10 +- apps/web/src/components/HypothesisFields.tsx | 22 +- .../src/components/HypothesisLinkNotice.tsx | 9 +- .../src/components/LanguageScreens.test.tsx | 75 + apps/web/src/components/LanguageSettings.tsx | 12 + apps/web/src/components/LessonRecall.tsx | 10 +- apps/web/src/components/LinkRecheckDialog.tsx | 28 +- .../src/components/LiveProjectWorkspace.tsx | 431 ++--- apps/web/src/components/MemoryEditDialog.tsx | 22 +- .../src/components/MemoryInjectionSwitch.tsx | 16 +- .../src/components/ModelCredentialPanel.tsx | 23 +- apps/web/src/components/ModelSettings.tsx | 107 +- apps/web/src/components/OAuthLoginFlow.tsx | 75 +- .../components/ProjectMemoryPanel.test.tsx | 5 +- .../web/src/components/ProjectMemoryPanel.tsx | 34 +- apps/web/src/components/ProjectWebSetup.tsx | 18 +- apps/web/src/components/ReadableList.tsx | 11 +- apps/web/src/components/RecordInspector.tsx | 12 +- .../components/RefutationConditionsPanel.tsx | 26 +- .../components/RequestLanguageFlow.test.tsx | 81 + .../components/ResearchActivityStrip.test.tsx | 23 + .../src/components/ResearchActivityStrip.tsx | 135 +- apps/web/src/components/ResearchComposer.tsx | 54 +- .../web/src/components/ResearchDetailPane.tsx | 65 +- .../components/ResearchFollowupSummary.tsx | 106 +- .../src/components/ResearchLiveProgress.tsx | 22 +- apps/web/src/components/ResearchProgress.tsx | 21 +- .../web/src/components/ResearchResultCard.tsx | 84 +- apps/web/src/components/ResearchSidePanel.tsx | 63 +- .../src/components/ResultEvidencePanel.tsx | 35 +- apps/web/src/components/ResultScreen.test.tsx | 7 + apps/web/src/components/ReviewDialog.tsx | 20 +- apps/web/src/components/ReviewStatusLine.tsx | 17 +- apps/web/src/components/SameHypothesis.tsx | 23 +- .../web/src/components/SourceCitationGate.tsx | 69 +- .../web/src/components/SourceTimeAdvanced.tsx | 19 +- .../SourceTimeConfirmationDialog.tsx | 35 +- apps/web/src/components/StatusBadge.tsx | 3 +- .../src/components/SyntheticDataNotice.tsx | 6 +- apps/web/src/components/TestActionDraft.tsx | 5 +- apps/web/src/components/TestOrderSection.tsx | 21 +- .../web/src/components/TestResultRecorder.tsx | 29 +- apps/web/src/components/ToolInstallHelp.tsx | 14 +- .../web/src/components/TraceConfirmDialog.tsx | 14 +- .../src/components/TraceEvidenceDialog.tsx | 19 +- apps/web/src/components/TraceHistory.tsx | 44 +- apps/web/src/components/TraceImportDialog.tsx | 49 +- apps/web/src/components/TraceOriginLine.tsx | 6 +- apps/web/src/components/TracePage.test.tsx | 5 +- apps/web/src/components/TracePage.tsx | 77 +- .../src/components/WorkspaceErrorBoundary.tsx | 7 +- .../components/WorkspaceInternetConsent.tsx | 17 +- apps/web/src/components/XaiDeviceLogin.tsx | 59 +- apps/web/src/components/approvalView.ts | 5 +- apps/web/src/components/connectionCopy.ts | 68 +- apps/web/src/components/effectDeclaration.ts | 36 +- apps/web/src/components/evidenceUse.ts | 3 +- .../src/components/history/CriteriaChange.tsx | 40 +- .../history/DecisionDeltaComparison.tsx | 91 +- .../history/HistoryChangeSummary.tsx | 24 +- .../src/components/history/HistoryContent.tsx | 30 +- .../src/components/history/HistoryDetail.tsx | 53 +- .../components/history/HistoryTimeline.tsx | 30 +- .../components/history/ProjectReviewList.tsx | 25 +- .../history/ResearchHistoryWorkspace.tsx | 31 +- .../history/RestorePreviewDialog.tsx | 48 +- .../components/history/RevisionComparison.tsx | 17 +- .../src/components/history/historyErrors.ts | 17 +- .../components/history/historyPresentation.ts | 31 +- .../components/history/historyReasonText.ts | 45 +- .../src/components/history/useRestoreFlow.ts | 9 +- apps/web/src/components/hypothesisLinkText.ts | 27 +- apps/web/src/components/hypothesisMatrix.ts | 5 +- apps/web/src/components/hypothesisReview.ts | 7 +- apps/web/src/components/hypothesisView.ts | 5 +- apps/web/src/components/judgmentRecordText.ts | 56 +- apps/web/src/components/judgmentReview.ts | 20 +- apps/web/src/components/lessonText.ts | 26 +- apps/web/src/components/memoryEdit.ts | 25 +- .../components/modelCatalogPresentation.ts | 28 +- .../components/modelCredentialPresentation.ts | 108 +- apps/web/src/components/plainWording.ts | 47 +- apps/web/src/components/resultUsage.ts | 25 +- apps/web/src/components/sameHypothesisText.ts | 23 +- apps/web/src/components/statusLabels.ts | 134 +- .../web/src/components/testActionDraftText.ts | 32 +- apps/web/src/components/testOrderInput.ts | 16 +- apps/web/src/components/testOrderText.ts | 28 +- apps/web/src/components/testRanking.ts | 5 +- apps/web/src/components/traceInvestigation.ts | 15 +- .../traceInvestigationLanguage.test.ts | 24 + apps/web/src/components/traceReasonTags.ts | 14 +- apps/web/src/components/traceRows.ts | 8 +- apps/web/src/components/traceText.ts | 207 ++- apps/web/src/components/useResearchSession.ts | 78 +- .../web/src/components/workspaceSetupIssue.ts | 7 +- apps/web/src/i18n/ResponseLanguageContext.ts | 9 + apps/web/src/i18n/UiError.ts | 10 + apps/web/src/i18n/activityDisplay.ts | 32 + apps/web/src/i18n/activityMessages.ts | 590 ++++++ apps/web/src/i18n/advancedMessages.ts | 402 ++++ apps/web/src/i18n/apiNoticesMessages.ts | 154 ++ apps/web/src/i18n/bilingualRecord.ts | 10 + apps/web/src/i18n/commonStateMessages.ts | 250 +++ apps/web/src/i18n/connectionReasons.ts | 69 + apps/web/src/i18n/countFormatMessages.ts | 31 + apps/web/src/i18n/credentialMessages.ts | 262 +++ apps/web/src/i18n/finishingMessages.ts | 90 + apps/web/src/i18n/followupMessages.ts | 298 +++ apps/web/src/i18n/formatMessages.ts | 44 + apps/web/src/i18n/historyFormatMessages.ts | 20 + apps/web/src/i18n/historyReasons.ts | 78 + apps/web/src/i18n/historyTitle.test.ts | 17 + apps/web/src/i18n/historyTitle.ts | 19 + apps/web/src/i18n/historyViewMessages.ts | 290 +++ .../web/src/i18n/hypothesisActionsMessages.ts | 418 +++++ apps/web/src/i18n/judgmentMessages.ts | 130 ++ apps/web/src/i18n/locale.ts | 41 + apps/web/src/i18n/messages.ts | 88 + apps/web/src/i18n/metricFormatMessages.ts | 52 + apps/web/src/i18n/modelHelpMessages.ts | 34 + apps/web/src/i18n/oauthMessages.ts | 246 +++ apps/web/src/i18n/plainWordingResources.ts | 92 + apps/web/src/i18n/progressEn.ts | 106 ++ apps/web/src/i18n/progressKo.ts | 442 +++++ apps/web/src/i18n/recordDialogsMessages.ts | 354 ++++ apps/web/src/i18n/recordPanelsMessages.ts | 426 +++++ apps/web/src/i18n/remainingMessages.ts | 454 +++++ apps/web/src/i18n/researchMessages.ts | 258 +++ apps/web/src/i18n/researchPanelsMessages.ts | 386 ++++ apps/web/src/i18n/reviewPanelsMessages.ts | 262 +++ apps/web/src/i18n/setupMessages.ts | 254 +++ apps/web/src/i18n/sourceMessages.ts | 206 +++ apps/web/src/i18n/stateFormatMessages.ts | 34 + apps/web/src/i18n/statusEnglish.ts | 49 + apps/web/src/i18n/statusResources.ts | 88 + apps/web/src/i18n/testOrderResources.ts | 48 + apps/web/src/i18n/traceEnglish.ts | 45 + apps/web/src/i18n/traceFormatMessages.ts | 25 + apps/web/src/i18n/traceMessages.ts | 438 +++++ apps/web/src/i18n/traceReasonResources.ts | 56 + apps/web/src/i18n/uiFormatMessages.ts | 28 + apps/web/src/i18n/workspaceDetailMessages.ts | 90 + apps/web/src/i18n/workspaceMessages.ts | 386 ++++ apps/web/src/timelinePreview.tsx | 3 +- apps/web/src/workstation.css | 12 + config/module-responsibility-budget.json | 96 +- docs/architecture/rpc-method-catalog.md | 14 + .../historical-result-input.schema.json | 13 + .../hypothesis-language-check.schema.json | 92 + .../hypothesis-portfolio-v4.schema.json | 700 +++++++ .../hypothesis-table-fill-v4.schema.json | 116 ++ .../protocol/thread-request-v2.schema.json | 221 +++ .../protocol/thread-request-v21.schema.json | 231 +++ scripts/hypothesis_v3_metrics.py | 26 +- src/thoth/adapters/models/codex_oauth.py | 58 +- .../models/hypothesis_contract_text.py | 24 +- .../adapters/models/language_contract_text.py | 51 + src/thoth/adapters/models/openai_responses.py | 17 +- src/thoth/application/commands/operations.py | 327 ++-- .../application/commands/research_history.py | 6 +- .../application/commands/research_resume.py | 9 +- .../application/commands/research_threads.py | 1375 +++++++------- .../application/commands/thread_analysis.py | 1333 +++++++------- .../application/commands/workspace_setup.py | 7 + .../services/browser_submission_scope.py | 22 + .../services/conversation_inputs.py | 4 +- .../application/services/english_prose.py | 183 ++ .../services/expected_result_view.py | 88 + .../historical_access_verification.py | 8 +- .../services/history_associations.py | 4 +- .../services/history_projection.py | 558 +++--- .../services/hypothesis_contract_v3.py | 84 +- .../services/hypothesis_projection.py | 79 +- .../services/hypothesis_table_filler.py | 154 +- .../services/hypothesis_v4_projection.py | 43 + .../services/post_execution_memory.py | 4 +- .../services/post_execution_resume.py | 9 +- .../services/provider_usage_service.py | 4 +- .../application/services/request_language.py | 124 ++ .../application/services/research_analysis.py | 1636 ++++++++--------- .../application/services/research_answer.py | 15 + .../services/research_attempt_runner.py | 12 +- .../application/services/research_controls.py | 9 +- .../services/research_followup_projection.py | 3 +- .../services/research_history_scope.py | 4 +- .../services/research_input_queue.py | 13 +- .../application/services/research_models.py | 10 +- .../application/services/research_stages.py | 8 +- .../services/research_thread_read_snapshot.py | 367 ++-- .../application/workflows/thread_cycle.py | 4 +- src/thoth/apps/research_entry_composition.py | 4 +- src/thoth/apps/restore_profiles.py | 168 +- src/thoth/domain/hypothesis.py | 8 +- src/thoth/domain/hypothesis_full.py | 19 +- .../domain/hypothesis_record_versions.py | 21 + src/thoth/domain/hypothesis_v4.py | 78 + src/thoth/domain/hypothesis_v4_projection.py | 28 + src/thoth/domain/language_validation.py | 18 + src/thoth/domain/model.py | 2 + src/thoth/domain/research_codec.py | 2 + src/thoth/domain/research_execution.py | 2 + src/thoth/domain/research_history.py | 1 + src/thoth/domain/research_identity.py | 7 +- src/thoth/domain/research_request.py | 20 +- src/thoth/domain/response_language.py | 25 + src/thoth/protocol/schema_export.py | 13 +- tests/fixtures/language_expected_results.json | 12 + tests/fixtures/legacy_language_contracts.json | 147 ++ tests/integration/test_hypothesis_v4_run.py | 162 ++ .../test_language_revision_roundtrip.py | 79 + .../test_request_response_language.py | 195 ++ .../test_research_export_grounding.py | 10 +- tests/unit/application/test_english_prose.py | 180 ++ .../test_expected_result_consumer_fixture.py | 18 + tests/unit/domain/test_hypothesis_v4.py | 115 ++ .../models/test_response_language_wire.py | 210 +++ .../services/test_browser_submission_scope.py | 24 + .../services/test_expected_result_view.py | 64 + .../services/test_legacy_language_goldens.py | 47 + tests/unit/services/test_request_language.py | 145 ++ 280 files changed, 18908 insertions(+), 6300 deletions(-) create mode 100644 apps/web/src/api/draftLanguage.ts create mode 100644 apps/web/src/api/expectedResultState.test.ts create mode 100644 apps/web/src/api/expectedResultState.ts create mode 100644 apps/web/src/api/requestLanguage.test.ts create mode 100644 apps/web/src/api/resultViewNegotiation.test.ts create mode 100644 apps/web/src/api/submissionPersistence.test.ts create mode 100644 apps/web/src/api/submissionPersistence.ts create mode 100644 apps/web/src/components/LanguageScreens.test.tsx create mode 100644 apps/web/src/components/LanguageSettings.tsx create mode 100644 apps/web/src/components/RequestLanguageFlow.test.tsx create mode 100644 apps/web/src/components/traceInvestigationLanguage.test.ts create mode 100644 apps/web/src/i18n/ResponseLanguageContext.ts create mode 100644 apps/web/src/i18n/UiError.ts create mode 100644 apps/web/src/i18n/activityDisplay.ts create mode 100644 apps/web/src/i18n/activityMessages.ts create mode 100644 apps/web/src/i18n/advancedMessages.ts create mode 100644 apps/web/src/i18n/apiNoticesMessages.ts create mode 100644 apps/web/src/i18n/bilingualRecord.ts create mode 100644 apps/web/src/i18n/commonStateMessages.ts create mode 100644 apps/web/src/i18n/connectionReasons.ts create mode 100644 apps/web/src/i18n/countFormatMessages.ts create mode 100644 apps/web/src/i18n/credentialMessages.ts create mode 100644 apps/web/src/i18n/finishingMessages.ts create mode 100644 apps/web/src/i18n/followupMessages.ts create mode 100644 apps/web/src/i18n/formatMessages.ts create mode 100644 apps/web/src/i18n/historyFormatMessages.ts create mode 100644 apps/web/src/i18n/historyReasons.ts create mode 100644 apps/web/src/i18n/historyTitle.test.ts create mode 100644 apps/web/src/i18n/historyTitle.ts create mode 100644 apps/web/src/i18n/historyViewMessages.ts create mode 100644 apps/web/src/i18n/hypothesisActionsMessages.ts create mode 100644 apps/web/src/i18n/judgmentMessages.ts create mode 100644 apps/web/src/i18n/locale.ts create mode 100644 apps/web/src/i18n/messages.ts create mode 100644 apps/web/src/i18n/metricFormatMessages.ts create mode 100644 apps/web/src/i18n/modelHelpMessages.ts create mode 100644 apps/web/src/i18n/oauthMessages.ts create mode 100644 apps/web/src/i18n/plainWordingResources.ts create mode 100644 apps/web/src/i18n/progressEn.ts create mode 100644 apps/web/src/i18n/progressKo.ts create mode 100644 apps/web/src/i18n/recordDialogsMessages.ts create mode 100644 apps/web/src/i18n/recordPanelsMessages.ts create mode 100644 apps/web/src/i18n/remainingMessages.ts create mode 100644 apps/web/src/i18n/researchMessages.ts create mode 100644 apps/web/src/i18n/researchPanelsMessages.ts create mode 100644 apps/web/src/i18n/reviewPanelsMessages.ts create mode 100644 apps/web/src/i18n/setupMessages.ts create mode 100644 apps/web/src/i18n/sourceMessages.ts create mode 100644 apps/web/src/i18n/stateFormatMessages.ts create mode 100644 apps/web/src/i18n/statusEnglish.ts create mode 100644 apps/web/src/i18n/statusResources.ts create mode 100644 apps/web/src/i18n/testOrderResources.ts create mode 100644 apps/web/src/i18n/traceEnglish.ts create mode 100644 apps/web/src/i18n/traceFormatMessages.ts create mode 100644 apps/web/src/i18n/traceMessages.ts create mode 100644 apps/web/src/i18n/traceReasonResources.ts create mode 100644 apps/web/src/i18n/uiFormatMessages.ts create mode 100644 apps/web/src/i18n/workspaceDetailMessages.ts create mode 100644 apps/web/src/i18n/workspaceMessages.ts create mode 100644 schemas/protocol/hypothesis-language-check.schema.json create mode 100644 schemas/protocol/hypothesis-portfolio-v4.schema.json create mode 100644 schemas/protocol/hypothesis-table-fill-v4.schema.json create mode 100644 schemas/protocol/thread-request-v2.schema.json create mode 100644 schemas/protocol/thread-request-v21.schema.json create mode 100644 src/thoth/adapters/models/language_contract_text.py create mode 100644 src/thoth/application/services/browser_submission_scope.py create mode 100644 src/thoth/application/services/english_prose.py create mode 100644 src/thoth/application/services/expected_result_view.py create mode 100644 src/thoth/application/services/hypothesis_v4_projection.py create mode 100644 src/thoth/application/services/request_language.py create mode 100644 src/thoth/application/services/research_answer.py create mode 100644 src/thoth/domain/hypothesis_record_versions.py create mode 100644 src/thoth/domain/hypothesis_v4.py create mode 100644 src/thoth/domain/hypothesis_v4_projection.py create mode 100644 src/thoth/domain/language_validation.py create mode 100644 src/thoth/domain/response_language.py create mode 100644 tests/fixtures/language_expected_results.json create mode 100644 tests/fixtures/legacy_language_contracts.json create mode 100644 tests/integration/test_hypothesis_v4_run.py create mode 100644 tests/integration/test_language_revision_roundtrip.py create mode 100644 tests/integration/test_request_response_language.py create mode 100644 tests/unit/application/test_english_prose.py create mode 100644 tests/unit/domain/test_expected_result_consumer_fixture.py create mode 100644 tests/unit/domain/test_hypothesis_v4.py create mode 100644 tests/unit/models/test_response_language_wire.py create mode 100644 tests/unit/services/test_browser_submission_scope.py create mode 100644 tests/unit/services/test_expected_result_view.py create mode 100644 tests/unit/services/test_legacy_language_goldens.py create mode 100644 tests/unit/services/test_request_language.py diff --git a/README.ko.md b/README.ko.md index 402334b..bd199a1 100644 --- a/README.ko.md +++ b/README.ko.md @@ -36,7 +36,7 @@ 판단을 뒷받침하는 근거와 아직 풀리지 않은 문제를 살펴보고, 저장된 두 결과를 비교하며 다음 행동을 정합니다. > [!NOTE] -> **개인·로컬 사용을 위한 실험적 소스 공개본입니다.** UI는 한국어 중심입니다. +> **개인·로컬 사용을 위한 실험적 소스 공개본입니다.** 화면은 한국어·영어를 지원하며, 실제 모델의 영어 답변 품질은 아직 검증하지 않았습니다. > 집중 기능 검사는 통과했지만 최신 전체 회귀 검사는 통과하지 못했습니다. 아래 검증 범위를 확인하세요. > **2026-09-29 반영:** 모델별 전용 인증과 재시작·저장 복구 수정을 포함합니다. 실제 모델 분석과 현재 소스의 전체 검사 통과는 미확인이고, Claude 로그인은 공식 Claude Code 실행 파일을 거치며 실계정으로는 아직 확인하지 않았습니다. > **2026-10-11:** 합성 레이더 데모로 실제 모델 조사를 처음부터 끝까지 한 번 실행했습니다([실제 화면](#실제-화면)). 답의 품질은 평가하지 않았습니다. @@ -87,6 +87,8 @@ **사용할 연결을 직접 고릅니다.** 로컬 작업 공간에서 지원되는 로그인 또는 API 키를 설정하고 모델과 추론 수준을 선택합니다. 저장된 선택을 유지하며, 지원되지 않는 경로는 명시합니다. 저장된 결과나 비교를 읽는 것만으로 새 모델 호출이 시작되지는 않습니다. +**화면 언어와 답변 언어를 따로 고릅니다.** 화면 언어만 바꾸면 모델을 호출하지 않고 저장된 답변도 바꾸지 않습니다. 접수된 질문은 재시도·복구 중에도 원래 답변 언어를 유지하며, 같은 대화의 다음 질문부터 다른 언어를 선택할 수 있습니다. 옛 요청은 기존 언어 정책을 유지합니다. 새 질문을 보내면 모델 사용량이 발생합니다. + ## 다시 살펴볼 수 있는 연구 흐름 | 단계 | THOTH에서 하는 일 | diff --git a/README.md b/README.md index 912a0a3..3b49b15 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ competing explanations and recorded results connected.** Inspect what supports a see what remains unresolved, and compare two saved results before deciding what comes next. > [!NOTE] -> **Experimental source preview for personal/local use.** The interface is Korean-first. +> **Experimental source preview for personal/local use.** The interface supports Korean and English; real-model English quality has not yet been verified. > Focused feature checks passed; the latest repository-wide verification is not passing. See the verification limits below. > **2026-09-29 update:** isolated model authentication and restart recovery are included. Real-provider analysis and a current-source FULL pass remain unverified. Claude sign-in goes through the official Claude Code executable and has not been checked with a real account. > **2026-10-11:** one real model investigation on the synthetic radar demo ran end to end ([screens](#see-it-work)). Answer quality has not been evaluated. @@ -93,6 +93,11 @@ held judgment. Completion is not a claim that the evidence is sufficient. workspace, then select the model and reasoning setting. Saved choices are preserved; unavailable routes are shown explicitly. Reading a saved result or comparison does not start a model call. +**Choose the display and response languages separately.** Changing the display language makes +no model call and preserves saved answers. Each submitted question keeps its response language +through retries and recovery; the next question in the same conversation can use another language. +Older requests retain their original language policy. New questions use model capacity. + ## A research loop you can revisit | Step | In THOTH | diff --git a/SOURCE_MANIFEST.json b/SOURCE_MANIFEST.json index c887988..3599471 100644 --- a/SOURCE_MANIFEST.json +++ b/SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schema_version": "thoth-source-preview-manifest-v1", - "created_at": "2026-10-11T12:00:52.485237+00:00", + "created_at": "2026-10-12T14:54:42.237179+00:00", "license": "MIT", "git_history_included": false, "full_suite_verified": false, @@ -53,18 +53,18 @@ }, { "path": "README.ko.md", - "sha256": "e81a1273aceec1a42828081c012a91b5632da162ffd49961fd2a8603f6305cc6", - "bytes": 14654 + "sha256": "93ed27539861e92a9ea673c3f4b1e89999de8fbf1bda769296510cb6dc9ebe52", + "bytes": 15035 }, { "path": "README.md", - "sha256": "57698aa7ba73bc7a4a1a381ce2be8cb089513e8a666850967c849a79085e09f5", - "bytes": 12895 + "sha256": "dcd6838ae04f269d012f1c7bea3aff32683903ed17f2b2689777a232e5abaf92", + "bytes": 13173 }, { "path": "THIRD_PARTY_NOTICES.md", - "sha256": "bd11c3a4c6f7e77b6e315001eb248d3f6d502143329c1dd6cdea145bf406e878", - "bytes": 3598 + "sha256": "6efe1dcd9d1eb92e33e6dd16e643ac1b62a565c249d25b38bdef429b89015ef1", + "bytes": 3862 }, { "path": "alembic.ini", @@ -113,8 +113,8 @@ }, { "path": "apps/web/src/App.tsx", - "sha256": "79d0d4978f19ed1f31f128a744e9c38af52970ea710f9edcf1a6e98343fdb6d6", - "bytes": 922 + "sha256": "e530bae296bb37ea0253735139582a0bdad6460201876dfef44d9ad1dca55edb", + "bytes": 1033 }, { "path": "apps/web/src/api/actionDraft.ts", @@ -123,8 +123,8 @@ }, { "path": "apps/web/src/api/capabilities.ts", - "sha256": "c17b66ff964dd7d34a2d8dfd1ef8404c25feeaea500847b388ff172f728fb055", - "bytes": 2161 + "sha256": "7374e906ee659d028b1ef235afc3be0112a197f7d8b3b2c0aa6a756e06d8696e", + "bytes": 2660 }, { "path": "apps/web/src/api/contracts.test.ts", @@ -138,8 +138,8 @@ }, { "path": "apps/web/src/api/conversation.ts", - "sha256": "0f13f33c7366d641952b04e2afdc714360c998b75ed879fa6c08b37f63745170", - "bytes": 6881 + "sha256": "49f1d269b2e6ac86bd94716eb6e26e9bf32ed0055ef339b4e9574b4841c43a20", + "bytes": 6966 }, { "path": "apps/web/src/api/conversationRefetchDiagnosis.test.ts", @@ -148,13 +148,28 @@ }, { "path": "apps/web/src/api/conversationRefresh.ts", - "sha256": "1adcd2e9b1c4b1d80ab061a7a4fa3ddae4ed86349ddca478a967042d5ef67c48", - "bytes": 602 + "sha256": "7439b728475a10cbcee269c441c902442c8d3aeb0b4de1816aef99042568785c", + "bytes": 586 + }, + { + "path": "apps/web/src/api/draftLanguage.ts", + "sha256": "e4017876a44ce7391d6498176cf3cdbecf7c37d89844b0b8785d77f71c9fa2c3", + "bytes": 1243 + }, + { + "path": "apps/web/src/api/expectedResultState.test.ts", + "sha256": "688d1879261fb55c59d3212eee132629c2a69ada95246f78ba863d06e6e23a18", + "bytes": 557 + }, + { + "path": "apps/web/src/api/expectedResultState.ts", + "sha256": "09781c7815f76a1a644d36aa73e725178a37220c9ebe82ff047a357c19d1bb82", + "bytes": 401 }, { "path": "apps/web/src/api/files.ts", - "sha256": "eed636eb9d4807d6737309095d3b2d8ef38ed83f90ace9d08628ec0acc524602", - "bytes": 660 + "sha256": "53163bbc5bbf9848caa6dbaf36153fe0291b9c8db976af3e12908e02c3aaceff", + "bytes": 735 }, { "path": "apps/web/src/api/fixtures/iris-judgment-excerpt.json", @@ -183,13 +198,13 @@ }, { "path": "apps/web/src/api/history.ts", - "sha256": "ce2fbeabbc282f8cf632fc29399a677f3655df70f994039a6aa7b2c9389d07d3", - "bytes": 7608 + "sha256": "ffbd41d3f5cd0fd1261593d590435a4ef2396704855fb0576c2080b9f289a767", + "bytes": 7528 }, { "path": "apps/web/src/api/historyComparison.ts", - "sha256": "2c896d9f9463b4c98ce2d6ae3a843ec8362078c2d1174ce3f743f95eb2f4765e", - "bytes": 1939 + "sha256": "5ae41dbe64cc2c8f23fa1b78cc7237178dce61f49e8a1f8c1941533393d24ac6", + "bytes": 1965 }, { "path": "apps/web/src/api/historyHttp.test.ts", @@ -253,8 +268,8 @@ }, { "path": "apps/web/src/api/localWorkspacePersistence.ts", - "sha256": "0ad311c836e661ba2c5ca26552aa5bb3520bfea827de9a8eae23c2441d4bb3de", - "bytes": 10591 + "sha256": "788e2f2338f267a8371362ea8df540dee9274f29ab4188da3d8a1948ee911c78", + "bytes": 11496 }, { "path": "apps/web/src/api/presentation.ts", @@ -278,23 +293,28 @@ }, { "path": "apps/web/src/api/publicWeb.ts", - "sha256": "2613f94d50475d1022ba737ef677b03162cbb2de9d97d224c2561a8983492b34", - "bytes": 2233 + "sha256": "d0cb524f22aa88127b3f816e20350987de391426b65f102ca3ed5111604b322a", + "bytes": 2280 }, { "path": "apps/web/src/api/recordViews.ts", - "sha256": "99fab71d25723afb6d6d2c67a5cf455336f0aab754436f0b4f271821de4e0ba2", - "bytes": 3726 + "sha256": "c449e4b22a88ec53223496e43f612078954788e3f4418496f2bf634f018cd72c", + "bytes": 3910 + }, + { + "path": "apps/web/src/api/requestLanguage.test.ts", + "sha256": "575c6920b6ee39e100d16e996462f072f5873fed329f08a0b6b4171f2bdb1228", + "bytes": 1789 }, { "path": "apps/web/src/api/research.ts", - "sha256": "5a9f77e9c842818d8156a6f084495d60ae26e7155fa572ebc4c9ff417e808fdc", - "bytes": 11238 + "sha256": "9b7045efd030f1df237e30ee6c79e4c4fdc782dfc01fe31da9c27af3c4bbcd78", + "bytes": 11565 }, { "path": "apps/web/src/api/researchFollowup.ts", - "sha256": "6816d683795cf713a7d40d0f16b39024daee9b84b22d74f24103324065a5cad8", - "bytes": 8236 + "sha256": "2ceee4e426e6130538356d0194725e62797c0e2d923e6b1de18a1c1ee7d30cd3", + "bytes": 8170 }, { "path": "apps/web/src/api/researchProgress.test.tsx", @@ -303,8 +323,8 @@ }, { "path": "apps/web/src/api/researchProgress.ts", - "sha256": "093426d7173055d6839ac528da2f047d4c25c78811d7b6b7218f61a153d461c7", - "bytes": 14966 + "sha256": "0b7b6b7680ccb766e2cc53ff0d2a516c74dc5b88fc9df918aba25847190fd6fb", + "bytes": 15138 }, { "path": "apps/web/src/api/researchProgressCharacterization.test.ts", @@ -313,18 +333,18 @@ }, { "path": "apps/web/src/api/researchProgressText.ts", - "sha256": "32246515b0589a329abefea9d9a35efd472611fddb9012f8222bc713b9ba1b11", - "bytes": 18292 + "sha256": "40bb3b78dda1464e4097e68a82cf23caa98f6e9a2bbdc528553ecb822f00001f", + "bytes": 2199 }, { "path": "apps/web/src/api/researchStatusRead.test.ts", - "sha256": "0c1a9cbfbf8291edc53ffe50d99b4547653cc5d3e5d72f80758f40aeef2dc649", - "bytes": 2622 + "sha256": "aa448edfccc17e5219b955e5cbf7e20d93693981aaee503097d722b8b1e6cd80", + "bytes": 3072 }, { "path": "apps/web/src/api/researchStatusRead.ts", - "sha256": "c61efd66fc6c68975a51631226a8331714e90a0abafd10aa6f69604335947dd1", - "bytes": 1186 + "sha256": "395c353a5a17e67a50a4e0c9bbc9666b11266d2de13145fe013bcc1a8cab3a76", + "bytes": 1426 }, { "path": "apps/web/src/api/restore.test.ts", @@ -333,8 +353,8 @@ }, { "path": "apps/web/src/api/restore.ts", - "sha256": "313c2054349d4231a0e21e0b962f94e9ad335f196bfc8c845a4eb1ff3a1c544b", - "bytes": 7390 + "sha256": "c24781bc4e370071c6f9d43243a10dead5b00f295b6428f998fa3a8bed7d5e1c", + "bytes": 7198 }, { "path": "apps/web/src/api/restorePending.test.ts", @@ -353,8 +373,8 @@ }, { "path": "apps/web/src/api/resultEvidence.ts", - "sha256": "badd582b14dc01f2ce422756700d72310948ac032ccd5a4b5a665b2e1a3a85b0", - "bytes": 8343 + "sha256": "0f794c748ce807f0321b71e4ad4a656b40fc6e58a96a15f31fda164eb5701a2a", + "bytes": 8122 }, { "path": "apps/web/src/api/resultEvidenceIdentity.test.ts", @@ -376,10 +396,15 @@ "sha256": "4e98a83e57071a4c8d698e4ff83a81996541299abaf367153074984032cb8d8c", "bytes": 2298 }, + { + "path": "apps/web/src/api/resultViewNegotiation.test.ts", + "sha256": "9edd2a0cccfbd903f70a85787f05274b402dee7c6e6b4e454078ab8dfcfe97d3", + "bytes": 1709 + }, { "path": "apps/web/src/api/rpcClient.ts", - "sha256": "cbda889c4394185df4b987a9d44f32efa47d6a79524da065f7f8ea7d2547eaf3", - "bytes": 6759 + "sha256": "92a77302b04086eca3e3c422248b81017586cbaaca4054370149043b16407c69", + "bytes": 7300 }, { "path": "apps/web/src/api/rpcTransport.test.ts", @@ -388,13 +413,23 @@ }, { "path": "apps/web/src/api/sourcePolicy.ts", - "sha256": "04aab555db2da564e591a2adfc6b6c37c292f108e836f32cc7832c30e77fc9bd", - "bytes": 1292 + "sha256": "0f77ba848deea718c6e487e36e4182d6042579f43a41a1e3eb7956fc113e6a00", + "bytes": 1551 }, { "path": "apps/web/src/api/sourceTime.ts", - "sha256": "922044f244905b243fae73807a93792d9ca7bb8d750d603cc205d31a7d019b6e", - "bytes": 2972 + "sha256": "653ba002f47828df8104bd451a7e2c929a32972560cbf1f1e1223b7afc640bc7", + "bytes": 2894 + }, + { + "path": "apps/web/src/api/submissionPersistence.test.ts", + "sha256": "722e816a952a71dd699c3ae1da4433cded793fd1c40db1888968255651731be6", + "bytes": 1699 + }, + { + "path": "apps/web/src/api/submissionPersistence.ts", + "sha256": "15e8e976473543b7d711e9d1d03d05f542fad6a4e630b41ea98e1eb90dace3ce", + "bytes": 1672 }, { "path": "apps/web/src/api/timelineExample.ts", @@ -413,8 +448,8 @@ }, { "path": "apps/web/src/components/ActionCompare.tsx", - "sha256": "b36ba6b773050297ec7ec45572addbc22be522893f4ed5862abcca3d37a23280", - "bytes": 6970 + "sha256": "30aa478c57bf808d156d67b21350024d360dba10ce343a5c854787af39c4ec12", + "bytes": 7083 }, { "path": "apps/web/src/components/ActionCompareApproval.test.tsx", @@ -438,8 +473,8 @@ }, { "path": "apps/web/src/components/ActionEffort.tsx", - "sha256": "720b5b8b8e773fc0c2ad5e0f678b1224df10cca799d25ff58ab1072e647f1b63", - "bytes": 6434 + "sha256": "f99504ee7370acf2d08d847eb423e0a63fcc7b52194c35e881aebc821b48ce10", + "bytes": 6578 }, { "path": "apps/web/src/components/AnalysisWorkspace.tsx", @@ -448,8 +483,8 @@ }, { "path": "apps/web/src/components/AnswerBody.tsx", - "sha256": "be2daee6eed12f38a08fb4cef7fd0133a4679fa3520ad510421621ba93689f25", - "bytes": 1182 + "sha256": "3bfef604ab48e7108623cc98ad496d197c72bcdd614a62090ec3514fd85907cf", + "bytes": 1312 }, { "path": "apps/web/src/components/AnswerCitations.test.tsx", @@ -458,13 +493,13 @@ }, { "path": "apps/web/src/components/AnswerHold.tsx", - "sha256": "887d4b5bd9396f712536ba523b6ce13f2005d68b010da156dab94afcb01f51fb", - "bytes": 1590 + "sha256": "1969c4b42e906b40b141fd9f2946f6e6eb969adadb4b52969cf6f2562d3bdcfe", + "bytes": 1676 }, { "path": "apps/web/src/components/AnswerReadability.test.tsx", - "sha256": "5b743c6579f62b080acb14d3baa958afecfb16320ef74c414d3c58679b06587c", - "bytes": 4838 + "sha256": "ed2dca727566fdd426e43b77717702aa20b6a9037fd2660e76cb33679eeb044a", + "bytes": 5420 }, { "path": "apps/web/src/components/ApprovalCard.test.tsx", @@ -473,18 +508,18 @@ }, { "path": "apps/web/src/components/ApprovalCard.tsx", - "sha256": "f7f785c484a7a17a28883a6a88880824e74916a39d90f9c8d6d8a9cedbd73927", - "bytes": 3999 + "sha256": "8b23817eedd59397eb7620e7056f1ee5b877d05d2f4f9dbdb286a841662cec2d", + "bytes": 4041 }, { "path": "apps/web/src/components/ApprovalSection.tsx", - "sha256": "60e8441c6af7d4971832fb1cc6eebb18b0fa2d603028644b974d86c01982cace", - "bytes": 3590 + "sha256": "4560d023326b8686f262e1133743136ab4161b5a221d9a820ab3eef2e4d064c3", + "bytes": 3398 }, { "path": "apps/web/src/components/ArchivedProjects.tsx", - "sha256": "0acefdc9d1ba41463f2b7017240971291e85f050b1fec813946437b7b3afcce4", - "bytes": 2392 + "sha256": "29f0d0c12f785d3636b7b2fa14228491aca44461f2d28fd7b62e6dfdf6ba0b52", + "bytes": 2427 }, { "path": "apps/web/src/components/AuditTrail.tsx", @@ -493,13 +528,13 @@ }, { "path": "apps/web/src/components/AutoRetrySwitch.test.tsx", - "sha256": "fc881fb4ebb2c5e12d0683c0e7907b24394f7fcc0661cf9b7bc39fb8c8959e80", - "bytes": 3848 + "sha256": "fd5f1d658ecd4ee2edeadb5247fa41cfb91294a7313e660326c7936ec091cdc2", + "bytes": 3922 }, { "path": "apps/web/src/components/AutoRetrySwitch.tsx", - "sha256": "3b9fbf56775473d6d0f05161fb265faf7229c9d586c85b8cef02d39f45c15727", - "bytes": 1982 + "sha256": "7d2260493556bf8867347bfc6ac11d6bf5b147470f1704a430201dc894ae4558", + "bytes": 1686 }, { "path": "apps/web/src/components/CapabilityWorkbench.test.tsx", @@ -508,13 +543,13 @@ }, { "path": "apps/web/src/components/CapabilityWorkbench.tsx", - "sha256": "057a0064da214f98cc9289530d37f6e0ca3964e8da6416a41cc2c4c276144397", - "bytes": 8163 + "sha256": "a824144b18de063106eb0a6d067a28030731cee6ae2e77fef403605b16f551b3", + "bytes": 7647 }, { "path": "apps/web/src/components/ClaudeCodeHelp.tsx", - "sha256": "55cb6cb26dbda674e69b6df93fdcb0034b4a42b57d5cef789173721496486772", - "bytes": 590 + "sha256": "0cbcaee43f82474fabd37c9c63a46a989d8753b78c19b216d10e461c0000fa80", + "bytes": 499 }, { "path": "apps/web/src/components/ClosureMark.test.tsx", @@ -523,18 +558,18 @@ }, { "path": "apps/web/src/components/ClosureMark.tsx", - "sha256": "58acdca8bc700e361778aa37400460a477d907d432c2e97d35ad54d613b4e9e4", - "bytes": 1847 + "sha256": "f91e3925fbd261d3419928a7e56e786b3aac0a2b866619d4e35b5bc9f6d96bd5", + "bytes": 1974 }, { "path": "apps/web/src/components/ClosureRecorder.tsx", - "sha256": "2f22a37f20b430041c794fc53d0d539598e2b9031661fa0ebd1495348b09d9ef", - "bytes": 3755 + "sha256": "36282365ccfef46a102658d67bb36a0b9a110f31c603323d220836b713b9f03b", + "bytes": 3719 }, { "path": "apps/web/src/components/CodexToolHelp.tsx", - "sha256": "a353746e797b15f0dc44b08f1f84e0d798fa8f231a02eee2ae1c53e90f1ffb5b", - "bytes": 578 + "sha256": "3885de8a8bb716122c15267df56b132fa94234d121193fa9b829e74cc1886092", + "bytes": 497 }, { "path": "apps/web/src/components/ConfirmedFacts.test.tsx", @@ -543,8 +578,8 @@ }, { "path": "apps/web/src/components/ConfirmedFacts.tsx", - "sha256": "5f799a7c2ef75cdc6964222dde53865fd83b4a20328fe843e97bc5de9a179359", - "bytes": 3107 + "sha256": "7d8682f120176cfa47ad9f34abae9ba3c5d0d1feba67141ee690edd8565975ed", + "bytes": 3102 }, { "path": "apps/web/src/components/ContractV3Screens.test.tsx", @@ -553,8 +588,8 @@ }, { "path": "apps/web/src/components/ConversationTimeline.tsx", - "sha256": "2cea7be6a13f3a518f5ea28b9362e55f2f447fa6ef7844c3c3014bc43feba333", - "bytes": 10209 + "sha256": "2f82133dd5e741fbc1827e2f6b32c5fec371ed620077e9eede0f286507667d71", + "bytes": 9783 }, { "path": "apps/web/src/components/ConversationTimelineDeniedRetention.test.tsx", @@ -593,13 +628,13 @@ }, { "path": "apps/web/src/components/DomainRecords.tsx", - "sha256": "be6b5f0aec0387bbf5625d85f1cc736044b8c9e3ed5eb2d2260f87bd4278e224", - "bytes": 4151 + "sha256": "64275b4bc7cc275df21c25df1bace22556a2e66630a76674557c26f9c1aa9853", + "bytes": 3845 }, { "path": "apps/web/src/components/EffectDeclarationFields.tsx", - "sha256": "9f7b628d23f7e8c1db62762706a666336ecf99ed4a04020a9314dc9171187681", - "bytes": 1548 + "sha256": "39b6979d7faf4812145a32d4e3f283e0dff8e400d89c5eb9a9ea87c38f10d980", + "bytes": 1432 }, { "path": "apps/web/src/components/EvidenceRail.test.tsx", @@ -608,13 +643,13 @@ }, { "path": "apps/web/src/components/EvidenceRail.tsx", - "sha256": "b85594f9b5c963f816b79013741c587696fe585506a4185875988088424c7b0e", - "bytes": 4675 + "sha256": "847c76a1c6e891006953eb244f7ca2c78d67115f8fee6a2ff9052e9896561d20", + "bytes": 4912 }, { "path": "apps/web/src/components/EvidenceUseSummary.tsx", - "sha256": "2aa9b1e216a5cc502288464feab1bfda0d5e8175e1d1b95230dba6d0c509ef52", - "bytes": 1145 + "sha256": "7aac0bfb013be8267b1add37430e0946d3a16a04a939112cebfcb6f70c004639", + "bytes": 1222 }, { "path": "apps/web/src/components/ExecutionHoldNotice.test.tsx", @@ -623,18 +658,18 @@ }, { "path": "apps/web/src/components/ExecutionHoldNotice.tsx", - "sha256": "bd5d21ac1781314f99edbc4c826620d8f5e4addbc6a4a23370ae31feb5950b06", - "bytes": 6517 + "sha256": "058fd2435afb12e9f83e41987066055e0afb822b4abbbb262491ebc951c7a5f2", + "bytes": 5250 }, { "path": "apps/web/src/components/ExpectedResults.tsx", - "sha256": "3de49d9ded39f4d7acd164d4296ec7de3b43cb87ad1a15ddc2e6e43cba0e4e68", - "bytes": 1547 + "sha256": "a7991fd9ddd8a4a446d891d141fa6262eff0b66723931968423329016218c485", + "bytes": 1676 }, { "path": "apps/web/src/components/FileConnectionPanel.tsx", - "sha256": "88f0736ca9d3051d0539d43fc2c1454eea37a29801c2149290967e0cedeb59a3", - "bytes": 4146 + "sha256": "ce53b82a0f517644eb51e3cdd33afc622620e54d4215c32dbe904c2ccac9fe42", + "bytes": 3965 }, { "path": "apps/web/src/components/FirstRunSetup.test.tsx", @@ -643,8 +678,8 @@ }, { "path": "apps/web/src/components/FirstRunSetup.tsx", - "sha256": "6c19dbb637486d54cceb7bca4d848f7e2d2313fa616fbae968b2c2d677a5b3f4", - "bytes": 16736 + "sha256": "4a817b1bc91b7382764a806f67520f9497adf7d1c5a4e592e6c546be696d3ce8", + "bytes": 15649 }, { "path": "apps/web/src/components/HypothesisCompare.test.tsx", @@ -653,8 +688,8 @@ }, { "path": "apps/web/src/components/HypothesisCompare.tsx", - "sha256": "ce858c804b4a1b266320f812270bb6efc6628ab39456ff8d162254fe0bc9490d", - "bytes": 9398 + "sha256": "486d994e263c60c36bde6410de3ce19f382fdf2f61a7e689120f569397f953f4", + "bytes": 9265 }, { "path": "apps/web/src/components/HypothesisCompareDraft.test.tsx", @@ -663,23 +698,23 @@ }, { "path": "apps/web/src/components/HypothesisCompareLive.tsx", - "sha256": "9451e289b891012df3a66afe917efc6aaec7d40b9d0593ee7d3f0bef46b9d441", - "bytes": 3734 + "sha256": "53d326580f5324f98038a0656e1fea24eeb1d4d500035c26792c7430be688c7c", + "bytes": 3729 }, { "path": "apps/web/src/components/HypothesisContractSwitch.test.tsx", - "sha256": "44b9549862d525b0c2c59dacb9527ed14279d0fa2b735b8edba24f5bc6dc0445", - "bytes": 3159 + "sha256": "3cecd4c579375e933a54dd1644a4724f87efd951b23091581fe178e0ae945f47", + "bytes": 3167 }, { "path": "apps/web/src/components/HypothesisContractSwitch.tsx", - "sha256": "c1f4bf99b8e5b43cda3647b1fe80357ecf64835e61f853a6db3e937e2b30cc2e", - "bytes": 2227 + "sha256": "47601c363d7d2a832f574b2c8587a2601b3439b68ae64cc02bf7ac2181dad067", + "bytes": 1753 }, { "path": "apps/web/src/components/HypothesisFields.tsx", - "sha256": "a064f2c1f3d6dbabea60af18268ff15f16d652cd35f77a7d8c507d49f6314326", - "bytes": 1814 + "sha256": "c8e63b38a539db8c8812a6452602c93724fc48df5ae4eec118b5afe93b526279", + "bytes": 2004 }, { "path": "apps/web/src/components/HypothesisLinkNotice.test.tsx", @@ -688,8 +723,8 @@ }, { "path": "apps/web/src/components/HypothesisLinkNotice.tsx", - "sha256": "4b1421e5df71870088e0b3d028a5a4dafdada7d1ffb90777a5f8b320f702b613", - "bytes": 2767 + "sha256": "1f3273487e64bcdc7312d779b6ed5e50ab6f24696644645c7aebd86bacc7825a", + "bytes": 2690 }, { "path": "apps/web/src/components/HypothesisLinkScreens.test.tsx", @@ -706,6 +741,16 @@ "sha256": "ddf07819291960009011f0a3b7b9cbe139301af2461b125f5ec88a72542d5499", "bytes": 10655 }, + { + "path": "apps/web/src/components/LanguageScreens.test.tsx", + "sha256": "0ba7d94b07409364f741c050024056fe756da6889dd88341e81034d30b8e064c", + "bytes": 3849 + }, + { + "path": "apps/web/src/components/LanguageSettings.tsx", + "sha256": "d78471165491cd35b6aaaec5276df4bd67a9a48fcacc86169f8ad20a5b0272fa", + "bytes": 569 + }, { "path": "apps/web/src/components/LessonRecall.test.tsx", "sha256": "32520ec35f4e8f350b6f008a7c2a98888eb7a6e0ccc8610d3768f3f77050296c", @@ -713,18 +758,18 @@ }, { "path": "apps/web/src/components/LessonRecall.tsx", - "sha256": "ec792b54f7df53b8d4040eb6d00ce2570279b6393eaa2d7fe24668bf569deee0", - "bytes": 2022 + "sha256": "b6602023fc895bb6633136452b3e82549b0a76b4bdc08c4e80775ade8bd7ff92", + "bytes": 2091 }, { "path": "apps/web/src/components/LinkRecheckDialog.tsx", - "sha256": "952483ded8663c704b951f9132feff99c934ec46baeefe4b32605a375f031a7f", - "bytes": 4299 + "sha256": "83b8bf0f9a14205b0558e4f14b8ba936d767554af7167387101c383cd7c7880a", + "bytes": 3805 }, { "path": "apps/web/src/components/LiveProjectWorkspace.tsx", - "sha256": "24764098e6ce17bcd6441a41b8d40894cc67f5bff95baca651dd09e8d06dfd91", - "bytes": 43374 + "sha256": "e4df1c08e302cef1e7d16532ce1dd2e07835ea985c0118cce75b9c17cad9ba38", + "bytes": 42636 }, { "path": "apps/web/src/components/LiveProjectWorkspaceConsent.test.tsx", @@ -743,18 +788,18 @@ }, { "path": "apps/web/src/components/MemoryEditDialog.tsx", - "sha256": "ac0b815bf17f503ab95b598c760f51ed31bfea32d977f669c61530d2ae795a03", - "bytes": 2579 + "sha256": "cdb0349e801f6422a694644ecd344f4b34d96c52e408b2772af1f856b84e47f9", + "bytes": 2607 }, { "path": "apps/web/src/components/MemoryInjectionSwitch.tsx", - "sha256": "b2d8e5099a23202280231c7a1685cb6f705cb6f6e4b38e1cd183cc7bbca41a69", - "bytes": 2801 + "sha256": "798038719832ce305f45661f2cb26420a80d3f69946e7e8cf3e4162e31e5e3ee", + "bytes": 2308 }, { "path": "apps/web/src/components/ModelCredentialPanel.tsx", - "sha256": "b41136c0bffe6f48e5cdd7f4cbe9267184d36f685df4bf4318534406f2687fed", - "bytes": 7094 + "sha256": "e6d39c2a0a58519e8fc0024de18f39dd123fc0a0c8076308c1d8458853bfb90b", + "bytes": 7091 }, { "path": "apps/web/src/components/ModelSettings.test.tsx", @@ -763,13 +808,13 @@ }, { "path": "apps/web/src/components/ModelSettings.tsx", - "sha256": "077ccdd32ba9f1a6630fbb9feaf2d96d704b7394ecbec74a2914b76380ca6616", - "bytes": 9721 + "sha256": "7c280d88d2524cfdbc3b1b8bc1f0ce3bc794cc5d1f824714ff00401e8253825f", + "bytes": 9273 }, { "path": "apps/web/src/components/OAuthLoginFlow.tsx", - "sha256": "a94bc861fcb5403c5575f0424bcdf9f6548feb87d1b8eac090415d6a31829293", - "bytes": 19518 + "sha256": "c5fe884c283a1698982d39a52083aeb79ca3592f61e6681a97c40d7bbac35966", + "bytes": 18361 }, { "path": "apps/web/src/components/ProjectArchiveRestore.test.tsx", @@ -783,13 +828,13 @@ }, { "path": "apps/web/src/components/ProjectMemoryPanel.test.tsx", - "sha256": "51291b6cdd3a89a64bc9ea085a74e8ca7771c808c665096aee153174b81cfd21", - "bytes": 17328 + "sha256": "d09f0e62b7f7e00ffccd5853a0b1e2ba5be564519926c4642cd272768f7ac24f", + "bytes": 17389 }, { "path": "apps/web/src/components/ProjectMemoryPanel.tsx", - "sha256": "afd324cdfa02d940900be179fbe82b0b8c051563a6cb74342226135aa248714b", - "bytes": 5636 + "sha256": "62dab0170e8a05ee5ded0ef7acb97b38a102cc2c4d8e21e5d9eb07e4e77984d8", + "bytes": 5488 }, { "path": "apps/web/src/components/ProjectWebSetup.test.tsx", @@ -798,18 +843,18 @@ }, { "path": "apps/web/src/components/ProjectWebSetup.tsx", - "sha256": "162aa61f8ee4f053994dab69d54d9d3b856540fce226b7dad6f51739af9d9ba1", - "bytes": 4299 + "sha256": "7b265e4d57d7e0d2fdf1212e73f5d5e7b29b763f1879fe46f33bbeca9dea92a8", + "bytes": 4179 }, { "path": "apps/web/src/components/ReadableList.tsx", - "sha256": "f34fc8e4f5773a87cdcdf19a4e2eb415e1f23e9c3612653c9a3759a5834caeba", - "bytes": 742 + "sha256": "0f18cba6f641cc8ec4e7f4afe3cf223c39ce6f2d616bc96e69b53d2585b1c696", + "bytes": 998 }, { "path": "apps/web/src/components/RecordInspector.tsx", - "sha256": "859d3dc994e400b1c59f65d8f2fe514392508a7d5c699d60f96665d6086725f4", - "bytes": 1628 + "sha256": "5d1f49240f07f81ffc0d294caae26a3abfe37c0674e44de46059fce59f4e41e2", + "bytes": 1769 }, { "path": "apps/web/src/components/RecordedStanding.test.tsx", @@ -818,18 +863,23 @@ }, { "path": "apps/web/src/components/RefutationConditionsPanel.tsx", - "sha256": "bae8b0b34d40d53c6333f29c3a9358a678741a5a2bd0fcc19292d9543bf02906", - "bytes": 3039 + "sha256": "1d70db08c858343174aee936f666a97d427e54c9aaa0e1113648b3cb6a43fa1e", + "bytes": 2932 + }, + { + "path": "apps/web/src/components/RequestLanguageFlow.test.tsx", + "sha256": "5c39dfdf0c73ae2c69e7547a42c61274c7354393ef9eb474e9be60b02984e41f", + "bytes": 4480 }, { "path": "apps/web/src/components/ResearchActivityStrip.test.tsx", - "sha256": "37ed31a40b38b551034d8aed9089ae9a46472fa8cba4583be57677d54bafb8fc", - "bytes": 3181 + "sha256": "c89e7af706b3b3abb23d5f4b02a8791ba071864dbd16ccb023f74ecd08d0ef44", + "bytes": 4372 }, { "path": "apps/web/src/components/ResearchActivityStrip.tsx", - "sha256": "3a2de3bf87c67873990f023d0484729d4a464d5189ab230c4d73e064651ba5d9", - "bytes": 7551 + "sha256": "6894910aef8d0b2eb08ac807b8fda4c637c1486d856229463a073c08af40ff0b", + "bytes": 9551 }, { "path": "apps/web/src/components/ResearchCancelledPresentation.test.tsx", @@ -838,13 +888,13 @@ }, { "path": "apps/web/src/components/ResearchComposer.tsx", - "sha256": "9225b76e4ddf4313cbf17ecf5c0e4f444ab9d1e5f551a39bdb48a7d53563168b", - "bytes": 6326 + "sha256": "6002e3bacb5a68d27694d0b251083f9597a228996245d732089f573cf0fe2004", + "bytes": 6412 }, { "path": "apps/web/src/components/ResearchDetailPane.tsx", - "sha256": "fa0c6eaf4ca69c2c3784227f1769ed5830c0812d02ae72da91021271db7cccef", - "bytes": 4662 + "sha256": "692d630c0e468efd0e42b74efe8521d86ee83c16e8b0c8047c687265f19fb988", + "bytes": 4795 }, { "path": "apps/web/src/components/ResearchEvidenceDetail.test.tsx", @@ -853,8 +903,8 @@ }, { "path": "apps/web/src/components/ResearchFollowupSummary.tsx", - "sha256": "008da642697cd4cfa493399095768704d236f8b747199743e972d50899cc731b", - "bytes": 10033 + "sha256": "4411b416b0fdcfa6d559111dbad0d8cf00ef1fe892bf8b9fce2f2f782df897ef", + "bytes": 10104 }, { "path": "apps/web/src/components/ResearchHold.test.tsx", @@ -863,13 +913,13 @@ }, { "path": "apps/web/src/components/ResearchLiveProgress.tsx", - "sha256": "a4fbb782deac37393458755fbb609202c778a08fc414493f23237230987aa1a1", - "bytes": 3158 + "sha256": "0eccdbbd89f7d81cf4e83786a0684ec82ad85aef95cdbbf0308b16063217ad83", + "bytes": 3323 }, { "path": "apps/web/src/components/ResearchProgress.tsx", - "sha256": "facd42c131327bc21380814a01dcca8c75bbd68901e0562d451c3c0abcc95d59", - "bytes": 2530 + "sha256": "ef221b1fffff3112210c2ac1ddb4d4cff5dfdcac86a74ff24d9d94bbbdcede0c", + "bytes": 2493 }, { "path": "apps/web/src/components/ResearchResultCard.test.tsx", @@ -878,18 +928,18 @@ }, { "path": "apps/web/src/components/ResearchResultCard.tsx", - "sha256": "eff8c6e086137cf74ffd34d1b0e330a4e482f1676c124857337042f3a6eb7680", - "bytes": 12795 + "sha256": "da354443e6cc5e539722ea4583902babe82365fab1ed0ce60eddcdbd2106b8b5", + "bytes": 12466 }, { "path": "apps/web/src/components/ResearchSidePanel.tsx", - "sha256": "0d95495427fb944a51e5891f122db3ac0915cf946a73abdc69f02975ef81937e", - "bytes": 2967 + "sha256": "8ab4da7debae05eb38cd3c65b565f1e62dcfdc527e04ff394db8d08cecc73fb6", + "bytes": 2860 }, { "path": "apps/web/src/components/ResultEvidencePanel.tsx", - "sha256": "897a8f56fef32d758bbe8a42e10dd2ed1af4b5a66a8cf3496e85068d40600598", - "bytes": 5396 + "sha256": "619202da94edb2df3915632d468471f9e4fc008dd4942c05a1a5d905abce1682", + "bytes": 4560 }, { "path": "apps/web/src/components/ResultExample.tsx", @@ -898,18 +948,18 @@ }, { "path": "apps/web/src/components/ResultScreen.test.tsx", - "sha256": "1794413e19a4604e2a614d6b9f91123c177a17a91d82334ab131e3d02be05a2a", - "bytes": 12825 + "sha256": "cffb02693ccbf328dc8b2b7a5a34eb3ec35c9a02b051d2740c5ff2ed7541b8e4", + "bytes": 13247 }, { "path": "apps/web/src/components/ReviewDialog.tsx", - "sha256": "2d05f30f317ae9e15ab959652c7ad87ce43fff30015bbb141756717a606ffe2c", - "bytes": 2500 + "sha256": "6c050d8ae19bca8ac46e8e4cef7d7236dad7351403ee5405c265b0c8663a07c5", + "bytes": 2402 }, { "path": "apps/web/src/components/ReviewStatusLine.tsx", - "sha256": "c00f537d577a0a7d147f1e33981aafb2f38a9b7ddd475462431aea2fb9ebc5e3", - "bytes": 1605 + "sha256": "e9327ead26e3c7677673244c297896d8866c95c9600b82d0ae1e72b0816574ec", + "bytes": 1746 }, { "path": "apps/web/src/components/SameHypothesis.test.tsx", @@ -918,8 +968,8 @@ }, { "path": "apps/web/src/components/SameHypothesis.tsx", - "sha256": "e928223ccf4c844e2e7b661c8516d4c48054e98016945e47c6f4d9fbc8ba3d15", - "bytes": 4095 + "sha256": "74a27983b7099089362cddc734aea4f5b0714936b88d9415946176577301c128", + "bytes": 4175 }, { "path": "apps/web/src/components/SourceCitationGate.test.tsx", @@ -928,28 +978,28 @@ }, { "path": "apps/web/src/components/SourceCitationGate.tsx", - "sha256": "692d7c9f583d86b36b2ac0d26daff5f841d51f6071504fa5dbec13ec45bd5016", - "bytes": 4095 + "sha256": "a6cd0a634dd0d1358c786ac4ec0b8b3f722b94470236499c8570df87f0f37753", + "bytes": 3948 }, { "path": "apps/web/src/components/SourceTimeAdvanced.tsx", - "sha256": "bd4631a04cc70c990be2a1339c3e718139005178d07498e8088d1d96c7c6fb9a", - "bytes": 3776 + "sha256": "e9cb73bdec0a425fb91d843376a2fed907e1f302f9f1c22012bf817f20fedbc6", + "bytes": 3729 }, { "path": "apps/web/src/components/SourceTimeConfirmationDialog.tsx", - "sha256": "cac22fa80f3d0ea941412b672e300ebea8c0beb6e14515487b6b2eab1ef09fd1", - "bytes": 1023 + "sha256": "ece207a799e850d282e27fc6e938f96facce580f0bad4ac665be31b54ea977a9", + "bytes": 952 }, { "path": "apps/web/src/components/StatusBadge.tsx", - "sha256": "8ba883cf96879aa2254fd9d4919a4d33c0fc93d9e9bc77c8e653a58dc521c977", - "bytes": 742 + "sha256": "81b1347acf2649b9ed597cfef593aa8f5b1793342ee4ca39fd267bf160bced39", + "bytes": 792 }, { "path": "apps/web/src/components/SyntheticDataNotice.tsx", - "sha256": "df4bfdb7ce007d47e263a5920d317bd0b2d3b47bef3efdb718dc6724864bc659", - "bytes": 1093 + "sha256": "6289df7d559231f16918e186abc3f33c16410b2c4453b4efd161ed6bf05834b6", + "bytes": 1115 }, { "path": "apps/web/src/components/TestActionDraft.test.tsx", @@ -958,8 +1008,8 @@ }, { "path": "apps/web/src/components/TestActionDraft.tsx", - "sha256": "a08d9bed2c5c5c438a7dd6c1c0da059d972f9e9cffa3ae0b999c7a611395e5cc", - "bytes": 2546 + "sha256": "4d67a028b09d6d0bd1975646f2737db861420c1c1b8a67e9c6cbe55641ca06f3", + "bytes": 2521 }, { "path": "apps/web/src/components/TestOrder.test.tsx", @@ -968,13 +1018,13 @@ }, { "path": "apps/web/src/components/TestOrderSection.tsx", - "sha256": "31f15a5c7167d08429c7f07262fdd4d9d02cb357be8cb14629ac08f7aea22164", - "bytes": 2333 + "sha256": "83e64f3e55d16358184a0d40100e07f22b6255e274c5e741acf06c55afed3a22", + "bytes": 2310 }, { "path": "apps/web/src/components/TestResultRecorder.tsx", - "sha256": "0e1983b6bb64082e0ec851d21f1b91a7ef1cb0d04bf17254b17abbf2df017800", - "bytes": 3805 + "sha256": "d874e39605497c88f4d6abd52edf7f90416f8d0a20c4dcf493b5d24bc7e98372", + "bytes": 3649 }, { "path": "apps/web/src/components/ThothBlueprint.test.tsx", @@ -988,8 +1038,8 @@ }, { "path": "apps/web/src/components/ToolInstallHelp.tsx", - "sha256": "6d229ab8daf3859e02897805027f84a8dcdaf2e90f6d1b733de14983a1c397be", - "bytes": 2968 + "sha256": "f96db4ace9d71e4448087cf5c81ebd873b694c5908d622c3f457a747b807a5bc", + "bytes": 2894 }, { "path": "apps/web/src/components/TraceClosure.test.tsx", @@ -998,38 +1048,38 @@ }, { "path": "apps/web/src/components/TraceConfirmDialog.tsx", - "sha256": "b75200500ee9bdd5f1f04cc013f80ab1a57fc85d8ffdff067539ca963a40ddae", - "bytes": 1975 + "sha256": "f37fd796cc0ab91b35efffc936e2d9d634253453ed41644157d366c40af3f19f", + "bytes": 1868 }, { "path": "apps/web/src/components/TraceEvidenceDialog.tsx", - "sha256": "d7e23ff767ec2c290b4d94e6db5b47fb379be6e4d84296ac37477e95a9d43004", - "bytes": 2623 + "sha256": "7bf5fe6368c9ec59f0b8b9dbe2dcf103d25e2eaab4e2ff20e91828cb1f747978", + "bytes": 2281 }, { "path": "apps/web/src/components/TraceHistory.tsx", - "sha256": "d833ca06a38c2fd5cb6af6ecab4d7b3cd418a77590514ef7377f91607d07327c", - "bytes": 5757 + "sha256": "6c22718e448490a2b787301554ea1a2c63507bb99bf7ea83d77c70f7ddf41b75", + "bytes": 5813 }, { "path": "apps/web/src/components/TraceImportDialog.tsx", - "sha256": "dc6255a64fc1c6ee4ecb5c6cadceae1fcb419ac3b167b02f9c59e76777f02b4b", - "bytes": 6063 + "sha256": "cbcccca6c971cac7d179dbcbd925e791d7ea6ecd7c0d661ecec460c75e3fbc1b", + "bytes": 6132 }, { "path": "apps/web/src/components/TraceOriginLine.tsx", - "sha256": "b925d2c4ec812b624e0dfafb25af0c826bd6f7506396bccf56f1fb33c6eaf852", - "bytes": 899 + "sha256": "5c4541df58c2edb2235479ecb47a27cdad10f0ff852de15b2d8946cea0e2a655", + "bytes": 970 }, { "path": "apps/web/src/components/TracePage.test.tsx", - "sha256": "f50ff30012803c0c52ded651f77b5cccc8e08f0e3144e1ea02714c28ac827b1b", - "bytes": 30721 + "sha256": "ef3a25b3cab2a5b2115afe4eeaf403f43533a138b9b4335310b806af33d69ae9", + "bytes": 30782 }, { "path": "apps/web/src/components/TracePage.tsx", - "sha256": "c32393a61b252b82dc1927a808d7ea9291539d9c120f6cc0daea728642590bbe", - "bytes": 15748 + "sha256": "8aca31501ed1fd0b8e076ad7f4e65d1050692a4edbbed6c272a3215907d22e9c", + "bytes": 15419 }, { "path": "apps/web/src/components/UsageSummary.test.tsx", @@ -1038,18 +1088,18 @@ }, { "path": "apps/web/src/components/WorkspaceErrorBoundary.tsx", - "sha256": "1dd1e8d4388283b6a99516f15c7920ae5c524d2cc53d5d47563ab346ba4e2102", - "bytes": 1223 + "sha256": "ca2b835244e784b19b4f952fa38be36053c2e54e909293484a571ac0c3a9f754", + "bytes": 1018 }, { "path": "apps/web/src/components/WorkspaceInternetConsent.tsx", - "sha256": "ae2118d3e83797698058410869859cd7266d561ce4533b1f5beb10fd7196464c", - "bytes": 2628 + "sha256": "c401c77cbed99e84da691cf51a8ff8c12a750d3479cb31ddd9516fbfe2d4a290", + "bytes": 2431 }, { "path": "apps/web/src/components/XaiDeviceLogin.tsx", - "sha256": "3e9d97020d952c0f3f930896d93bd17cf12700ae4b5e970d49497acfe7aea5ec", - "bytes": 14374 + "sha256": "e9a53512bb795eb39f848ff732197ca765915f2782ce0d9e237f1225802e60d0", + "bytes": 13311 }, { "path": "apps/web/src/components/answerText.test.ts", @@ -1063,13 +1113,13 @@ }, { "path": "apps/web/src/components/approvalView.ts", - "sha256": "9443637c0ecced38821de70bcf223db0bef3773e120e7d835bdfae83eafc57f8", - "bytes": 3495 + "sha256": "47ef9029b8cf6d69396eaceb28f7bcdbdb71cfd0d1f53e3e40b560f11cce75ec", + "bytes": 3661 }, { "path": "apps/web/src/components/connectionCopy.ts", - "sha256": "81ecddef774b7a17eadf1719fd40e62b978d54f677a5f7da8edcde18d87cbd5a", - "bytes": 8528 + "sha256": "102423e8e9fc492ffb409137ed7f93e55255506a9179977bb1943dce13b92fc9", + "bytes": 4820 }, { "path": "apps/web/src/components/credentialAuthMethods.fixture.json", @@ -1083,8 +1133,8 @@ }, { "path": "apps/web/src/components/effectDeclaration.ts", - "sha256": "cd7f65e8a39adeae07b21333b99a5f6f7b819429954f5d325a839ac3157759cf", - "bytes": 2698 + "sha256": "3730221e731b9dd008202aa845e1ba10dab1d20125d2e635ddc2043a1db30254", + "bytes": 2829 }, { "path": "apps/web/src/components/effortEstimates.ts", @@ -1093,8 +1143,8 @@ }, { "path": "apps/web/src/components/evidenceUse.ts", - "sha256": "8f23ec66acb67f5b8efe7f0aa6856c626ddca77030ae050c233c9b9523d24935", - "bytes": 1823 + "sha256": "1f3e67d7693b043365a352bc9cf09be1e1bd0271cf682e6bca17629fb44ba687", + "bytes": 1968 }, { "path": "apps/web/src/components/history/CriteriaChange.test.tsx", @@ -1103,13 +1153,13 @@ }, { "path": "apps/web/src/components/history/CriteriaChange.tsx", - "sha256": "2a2053a1e48efd6f3a763211bb89723ff017554285893015cb5fef798a56fab5", - "bytes": 4056 + "sha256": "b24716ae7274b7aacf455619eaab685f8a1057454e90b33d94e86dc791299544", + "bytes": 4285 }, { "path": "apps/web/src/components/history/DecisionDeltaComparison.tsx", - "sha256": "a6d9e9db834fb6a6fa5b702a886d8eb7c8843359ee77b4ee6ea4f9a4f0003c76", - "bytes": 12954 + "sha256": "50e301fb9ee38d201acb82d01f5204fabde07418da91d4b31a93723b45b6d6b3", + "bytes": 12583 }, { "path": "apps/web/src/components/history/HistoryChangeSummary.test.tsx", @@ -1118,18 +1168,18 @@ }, { "path": "apps/web/src/components/history/HistoryChangeSummary.tsx", - "sha256": "f4a9947ab08eb58bb79ff756dd24b17851c0b3d9c1a64c93742f16e12ec9e5af", - "bytes": 2964 + "sha256": "28a4a9f21b4d9c9c807b70d4bd53f50bc64e5ff6cf9fa21035110b0add884bd1", + "bytes": 3218 }, { "path": "apps/web/src/components/history/HistoryContent.tsx", - "sha256": "7b5163551625f05a9a1b49c9075f5a8186b4e9670c2240edd97c8a1540126a08", - "bytes": 4650 + "sha256": "8569c49b0d7d02f744c96f205417a514a4778e391f8682d334974c7deb9dc7d1", + "bytes": 4962 }, { "path": "apps/web/src/components/history/HistoryDetail.tsx", - "sha256": "41d420e20eafed7a896bd3c85583e9989e764b585389fa92fcfca1375fc7efee", - "bytes": 8461 + "sha256": "f4fce6e77e9be984ef1436e419a3177dd5abcceded8ad195c7d0c5c8b356c739", + "bytes": 7919 }, { "path": "apps/web/src/components/history/HistoryExample.tsx", @@ -1138,8 +1188,8 @@ }, { "path": "apps/web/src/components/history/HistoryTimeline.tsx", - "sha256": "7118c15559e63416f4665a60e375b60bf3c3a95e1daee60e339a41dc7155ba35", - "bytes": 4511 + "sha256": "82b49c607dcf62dff70a7b4863c71e5f966a9e7d656f96cfa078307476e98a80", + "bytes": 4279 }, { "path": "apps/web/src/components/history/ProjectReviewList.test.tsx", @@ -1148,8 +1198,8 @@ }, { "path": "apps/web/src/components/history/ProjectReviewList.tsx", - "sha256": "da2d4d5276cc44d502f092af09371023a0e0a95abfe6e6e85d0315810ef2e474", - "bytes": 3753 + "sha256": "2a41c2240790a18ffa7d8c8b74c9bbbd5c7c6283f1050cabad0a521ff5542e05", + "bytes": 3561 }, { "path": "apps/web/src/components/history/ResearchHistoryWorkspace.test.tsx", @@ -1158,28 +1208,28 @@ }, { "path": "apps/web/src/components/history/ResearchHistoryWorkspace.tsx", - "sha256": "510754838897c4ed2a33eb1030f65ff372bf5f36c3b7d907259552b357d212fb", - "bytes": 8873 + "sha256": "a36331fff239abdfe318d621207774410afa5ac3e64f6096355f98f4f975925e", + "bytes": 8635 }, { "path": "apps/web/src/components/history/RestorePreviewDialog.tsx", - "sha256": "9c97e6fa00034a1b36a91299a1550c5cb8d0b296400e04d148a1ce65574ba566", - "bytes": 5223 + "sha256": "bc679047c363617a6d6e4815ad4d9d7c604d79f2f82f81ff1cb389e171759d25", + "bytes": 4492 }, { "path": "apps/web/src/components/history/RevisionComparison.tsx", - "sha256": "c60e944603ce543c90ec0fac78ed3fb9be1083934884f1beb0689c05e1a4f179", - "bytes": 2168 + "sha256": "ff4a077d96edf6f285e4473f95043d7caf32df852f3e10b553587a5d2e6fde9f", + "bytes": 2066 }, { "path": "apps/web/src/components/history/historyErrors.ts", - "sha256": "08934168c6effb49a726cbd2d9e7e2a9597f30121a530457433a4127e708cd46", - "bytes": 1540 + "sha256": "81a8d40613add31f418937019daa23d1d318d54e6678df2ff513594b8baa43fd", + "bytes": 935 }, { "path": "apps/web/src/components/history/historyPresentation.ts", - "sha256": "c50deb36bdbc718d8997b53e1faaaa2906367ba427b4b3e5df1b38a02347ff04", - "bytes": 2165 + "sha256": "4f4761061a4eb3777544e8764825a694c915afbca26430edf91ae3e70059f23e", + "bytes": 2769 }, { "path": "apps/web/src/components/history/historyReasonText.test.ts", @@ -1188,13 +1238,13 @@ }, { "path": "apps/web/src/components/history/historyReasonText.ts", - "sha256": "6ac82e5a0d14cb2a428bcb91d8bdc63978839452c266267522181d2f2de97b31", - "bytes": 4220 + "sha256": "624a480af45f21400f94feaa66a0f72b1467126ceb35dcaabba6eb84e96da6b3", + "bytes": 952 }, { "path": "apps/web/src/components/history/useRestoreFlow.ts", - "sha256": "f3bc34423dce12301f50dd47af2b842d07f5700db42038c70ac375f8fb69f710", - "bytes": 4569 + "sha256": "e7fb66c43b97b78437df61b6c61e3a899eb0426bf0475fbeccace89119220be4", + "bytes": 4420 }, { "path": "apps/web/src/components/hypothesisLinkText.test.ts", @@ -1203,23 +1253,23 @@ }, { "path": "apps/web/src/components/hypothesisLinkText.ts", - "sha256": "c259ef47c230da218fab7098521561fee1c734fa4ed9d9725a6b2d6782f33a29", - "bytes": 4417 + "sha256": "23035bc9e3c5f771bb85b75888916e96f363e9955cdc02e6d553798e4b28167b", + "bytes": 3904 }, { "path": "apps/web/src/components/hypothesisMatrix.ts", - "sha256": "589a0f72702b1246c25a6a36397048fe5a07a0baa528e709fd89672ab2ddaee4", - "bytes": 1311 + "sha256": "6a6f03497c5c6dc01673229c23d6b760e0e4ad8eac7d573d01b002bbd729dcd0", + "bytes": 1192 }, { "path": "apps/web/src/components/hypothesisReview.ts", - "sha256": "4a1bc1a4a3fb6a0608548451935559f59c8412090e97180051b7c0f054e83b89", - "bytes": 2493 + "sha256": "f25513b9b8c806b0611b1a5e9bf8c466b33ca1b089fcb8d1ec2dd296283c1fa1", + "bytes": 2760 }, { "path": "apps/web/src/components/hypothesisView.ts", - "sha256": "a98e0ff4289c89f57fb7f599b137f77f7d7a197d974657aca1b567b4dc7f6d9a", - "bytes": 4530 + "sha256": "36d5d5f9385941f25602d60d7fd7aa9edc991dcd6a85dc5434405c6934a6e8fb", + "bytes": 4549 }, { "path": "apps/web/src/components/judgmentRecordText.test.ts", @@ -1228,18 +1278,18 @@ }, { "path": "apps/web/src/components/judgmentRecordText.ts", - "sha256": "e8e5ac47784c476d43db1194e171e179ba1ce377a018e2983c4ce571d44ed52b", - "bytes": 6051 + "sha256": "1de533f9c99d7ae2f87f123743c4273a02ad89911af0d5864313e7ec7ea73d74", + "bytes": 5402 }, { "path": "apps/web/src/components/judgmentReview.ts", - "sha256": "eb5f09c056868f9063200cc58aead6d8dcffa5361930d6bde77e4e74db8ba982", - "bytes": 3677 + "sha256": "3ddc9dd38914ae496750c953476890d24b32133df314849cc394089e97106f4d", + "bytes": 4044 }, { "path": "apps/web/src/components/lessonText.ts", - "sha256": "1fa022fb4130a1f31b888afc02ad0e65c48e375b7168f850bde551e6ff686f0f", - "bytes": 2540 + "sha256": "7842c35ae6248c187d10b0d07fde7bdd87f5136098be85fc5e153b061988e811", + "bytes": 2116 }, { "path": "apps/web/src/components/localCredentialWorkspace.ts", @@ -1253,8 +1303,8 @@ }, { "path": "apps/web/src/components/memoryEdit.ts", - "sha256": "f5315cff2ae5d02e2488ef8330287c5e48889ae92186aadf507fd5747e716301", - "bytes": 3515 + "sha256": "96eb638d1d7d434d4acdfae3ec6e9c7a91bc4f4f671a67440200c0bd66cadc3e", + "bytes": 3349 }, { "path": "apps/web/src/components/modelCatalogPresentation.test.ts", @@ -1263,13 +1313,13 @@ }, { "path": "apps/web/src/components/modelCatalogPresentation.ts", - "sha256": "3f7452bd0cc94516ad5cb8605a5a54242d8f1d728db5a93881a67099e71bf285", - "bytes": 3446 + "sha256": "b72e23b8c5088cda6a1c97f6b812afaf15f38155d37dedc54d2ed13bedcf1a91", + "bytes": 3583 }, { "path": "apps/web/src/components/modelCredentialPresentation.ts", - "sha256": "43eacda77e918641dcf4bd59c813be4d4e65dd4aa4095d9a09e43094681c8550", - "bytes": 15449 + "sha256": "13110c66b4dd44e6af646605e5bc5728bb03cf2d47fbc0eb40da1f91ce598ed8", + "bytes": 13048 }, { "path": "apps/web/src/components/modelCredentialStatusNote.test.ts", @@ -1278,18 +1328,18 @@ }, { "path": "apps/web/src/components/plainWording.ts", - "sha256": "9c554b8bbdbcf71d8891b19181fa828d546d832b7069c30e9800bf691000c4fa", - "bytes": 5544 + "sha256": "c17a59a072a98d1bfffc2d571dc36823f02b0f845314da0d745b8b5b1eb77639", + "bytes": 2421 }, { "path": "apps/web/src/components/resultUsage.ts", - "sha256": "b34ec4ebc8752787860137a031d58c3d91394f56fbc28f874333890cd9b2a7cd", - "bytes": 2477 + "sha256": "4de15fc6210292fdfaf412628c5d5760fdb82143cb16fec0f07cc6fd76f04217", + "bytes": 2742 }, { "path": "apps/web/src/components/sameHypothesisText.ts", - "sha256": "f7064c73664fae42a38e94f585b1b37cb80d2b76edc5002b36f398e87f32fec1", - "bytes": 2638 + "sha256": "4b989096704ad8dae5ceee7cfac9b4cdd8545b4b3fb46a8156ba6ebb2d192e9a", + "bytes": 2515 }, { "path": "apps/web/src/components/statusLabels.test.tsx", @@ -1298,23 +1348,23 @@ }, { "path": "apps/web/src/components/statusLabels.ts", - "sha256": "df8adae9511fb200d9cd52058c5e2d746a60ad3d0cea2e2aeab11380132fe612", - "bytes": 9569 + "sha256": "630268df7acbe8a94cab2d5d875d40280aa362e59829888595ed425073acdd47", + "bytes": 3993 }, { "path": "apps/web/src/components/testActionDraftText.ts", - "sha256": "2520b67f00f671415aa68d66bce40f30dd19eec889f409b07e4aeaba13008aed", - "bytes": 2669 + "sha256": "a6f24fb10b79606c747ac060a450b8e0181cdea97516b63aca23ec534836b572", + "bytes": 2621 }, { "path": "apps/web/src/components/testOrderInput.ts", - "sha256": "79b5a497bd3958c44f228e13e0ba29d0f90389c4b89b1ae29d9a4974c57c38ed", - "bytes": 4608 + "sha256": "934fe4b0a0061bb954414cf90a9f9d3318e58cdeec853349b12df0693e2fc76e", + "bytes": 5108 }, { "path": "apps/web/src/components/testOrderText.ts", - "sha256": "ee1a3b4fbad53df98ea53860ed498acdb255ea69392fb3cfcbbaeb52397c5487", - "bytes": 3219 + "sha256": "842b91ec33e5efa77802a7ad4a5c11cf57aac2fd2137fce9533ff22bff61174e", + "bytes": 1226 }, { "path": "apps/web/src/components/testRanking.cases.test.ts", @@ -1323,8 +1373,8 @@ }, { "path": "apps/web/src/components/testRanking.ts", - "sha256": "66bc643f5fbb2ac6dd60c3e023483441b96312db351307596cba4aa24f8a9409", - "bytes": 7093 + "sha256": "c1ce4bfb28cfef8006a43061e007c3040c9cef82a26ed37dd3c9593e50a917a5", + "bytes": 7198 }, { "path": "apps/web/src/components/traceDemo.fixture.json", @@ -1343,8 +1393,13 @@ }, { "path": "apps/web/src/components/traceInvestigation.ts", - "sha256": "b7b90efaf1ec70c1f6f8e20ead7cfed1da979f978fe286d44407141132b35657", - "bytes": 2930 + "sha256": "6aadfeebfb36729d4af4f02875b2563daf0725b14f92adfe67cb2b4a48115f7c", + "bytes": 3648 + }, + { + "path": "apps/web/src/components/traceInvestigationLanguage.test.ts", + "sha256": "856a7ca79a4f0570fa638d3a50600ebb87d6b27977100b47354c9a23ae4e83d7", + "bytes": 1327 }, { "path": "apps/web/src/components/traceReasonTags.test.ts", @@ -1353,13 +1408,13 @@ }, { "path": "apps/web/src/components/traceReasonTags.ts", - "sha256": "c0d32a6c76b76013d6070e3f10ecc36df6747a1aa42ec0eaa5ed093f553293fc", - "bytes": 4814 + "sha256": "aa958b2ccda81c837c353437dcc840298908c115beda63a3960fb139841c571b", + "bytes": 3457 }, { "path": "apps/web/src/components/traceRows.ts", - "sha256": "7fe5376df231a59a6560a25976630b1b742fd7257aaa39f14baa88b169caff60", - "bytes": 3125 + "sha256": "117aaca971f997b8190a7cd917c0acf98f0634d9a86b9f553ca80b2e7a2fb77d", + "bytes": 3218 }, { "path": "apps/web/src/components/traceText.test.ts", @@ -1368,8 +1423,8 @@ }, { "path": "apps/web/src/components/traceText.ts", - "sha256": "8c3ec012ce3caceed79fefb78c809624b8075bc65bade54953ff1dc636cf62bb", - "bytes": 21184 + "sha256": "580c5fa31afddd7f862b5d402574c5c2870e761e9e8434644113cbff1fcb45af", + "bytes": 19623 }, { "path": "apps/web/src/components/useConversationReadProtection.ts", @@ -1388,8 +1443,8 @@ }, { "path": "apps/web/src/components/useResearchSession.ts", - "sha256": "c486968d35a1b2402904804ea1d4e6ed66f67c17fc57d7e3cc98effdf43e51e9", - "bytes": 13513 + "sha256": "f340fe8d30a0ef920c9fc6851ffb4e5edc2b7b3efd87f41745f2f3224c280767", + "bytes": 15392 }, { "path": "apps/web/src/components/useResultEvidenceProtection.ts", @@ -1398,8 +1453,248 @@ }, { "path": "apps/web/src/components/workspaceSetupIssue.ts", - "sha256": "c4ad679f238a4eb12585dc143589953623b5c2aa6dafcad8d77e9bd011859780", - "bytes": 1206 + "sha256": "088ccc1551d13ee650c6ba0acc8a5808a011bdb389c16af722ad4676f1eee171", + "bytes": 905 + }, + { + "path": "apps/web/src/i18n/ResponseLanguageContext.ts", + "sha256": "05551d7c380ad707a1720ca6fde0a2742aa4de27569cc1d6b22ba22a593ce9bc", + "bytes": 620 + }, + { + "path": "apps/web/src/i18n/UiError.ts", + "sha256": "64d31f714f844a1d69683fa76b17141efa0655364cec7bf9f3979311a74ffbb4", + "bytes": 389 + }, + { + "path": "apps/web/src/i18n/activityDisplay.ts", + "sha256": "faab5e696f3ddf9514e9dc93e389c62d1908d4969911fceaef50b91f4568dda9", + "bytes": 2334 + }, + { + "path": "apps/web/src/i18n/activityMessages.ts", + "sha256": "8712eb3fb4032f7ee6dab0f1b65e5ff93b967c4a767c451cd933f8c31e22c7f1", + "bytes": 17798 + }, + { + "path": "apps/web/src/i18n/advancedMessages.ts", + "sha256": "c9521043c47b5f709aa303efea5cbcfc46f732524498cdc1935aecafb49d5574", + "bytes": 13888 + }, + { + "path": "apps/web/src/i18n/apiNoticesMessages.ts", + "sha256": "27cfc94bdf5546a3e9e03641956d2af08c1098913ca5d14bfa19b65018388850", + "bytes": 5809 + }, + { + "path": "apps/web/src/i18n/bilingualRecord.ts", + "sha256": "1139d5c8c2eb43bfdb0fdcb37afabbc1c47b57123e7c0f8a73803e5c5eddec13", + "bytes": 476 + }, + { + "path": "apps/web/src/i18n/commonStateMessages.ts", + "sha256": "6b54ecc5e5fae44c60bc6df1fbe71fb899845f9b4497eff7ad392cc3ed62a031", + "bytes": 6105 + }, + { + "path": "apps/web/src/i18n/connectionReasons.ts", + "sha256": "6cd246f1cbc5556ce063f66637a6c71781cec9f09e61c271056ebb3a194bb9f2", + "bytes": 7078 + }, + { + "path": "apps/web/src/i18n/countFormatMessages.ts", + "sha256": "67d1b1e45e520743077f51a6e7e49e36e0c773245573f1bb4cb2133af11c7438", + "bytes": 6016 + }, + { + "path": "apps/web/src/i18n/credentialMessages.ts", + "sha256": "2fd1801d0ed40743b4559b8b50a2c1f6cf2ce7871c5ec8a9f4a01a52fd63d1c6", + "bytes": 11913 + }, + { + "path": "apps/web/src/i18n/finishingMessages.ts", + "sha256": "40cbf52f00eaca46df563659995048b62775beea116391a939b7e256647b3a1a", + "bytes": 2840 + }, + { + "path": "apps/web/src/i18n/followupMessages.ts", + "sha256": "980556134eba25d8a13527311d080bb4e7b90cb5fa6bf56bdd52d205889096de", + "bytes": 11942 + }, + { + "path": "apps/web/src/i18n/formatMessages.ts", + "sha256": "e321bc8262cb78da90981bbb3c12815ecccb64ba8a6a7d724010f3938855bf60", + "bytes": 3370 + }, + { + "path": "apps/web/src/i18n/historyFormatMessages.ts", + "sha256": "df46b994d28bf167f51bd72e9785d339052d3d8755a40e3febf9d732a8f9f705", + "bytes": 3331 + }, + { + "path": "apps/web/src/i18n/historyReasons.ts", + "sha256": "d31c887724dfbb161b1f67572154bb628702cc0d4903e59e02f9e2f223e2e9ab", + "bytes": 6325 + }, + { + "path": "apps/web/src/i18n/historyTitle.test.ts", + "sha256": "876b4883145701061fa459600a6c4009929ac9ac17b33eed763c30134d3f2e06", + "bytes": 785 + }, + { + "path": "apps/web/src/i18n/historyTitle.ts", + "sha256": "ef6745fbba52f12cbe5f1d6e3f020a2f59a52a254aba72aad048f2f2eb659704", + "bytes": 1142 + }, + { + "path": "apps/web/src/i18n/historyViewMessages.ts", + "sha256": "ebff27323478f7805b7747062422cd6b4af9b3de0489dceb29db83a1dbee09be", + "bytes": 9760 + }, + { + "path": "apps/web/src/i18n/hypothesisActionsMessages.ts", + "sha256": "cb1246da4661834f395f5aba071227cc367ffc22634f66c540dc26b6e42a4575", + "bytes": 15665 + }, + { + "path": "apps/web/src/i18n/judgmentMessages.ts", + "sha256": "3660bf09405d4eb1353a693a9b886a7723a3619f23960da69ae8fdb5a20715e0", + "bytes": 6778 + }, + { + "path": "apps/web/src/i18n/locale.ts", + "sha256": "8041e4488a08a912577d708ff28a13832b8c8d531d66be0791b711c7139eb563", + "bytes": 2519 + }, + { + "path": "apps/web/src/i18n/messages.ts", + "sha256": "dfb0d1b2fc75d6ca2fe668d6825e9f0277bf5bdef50b66a4d26b4e229f9d8809", + "bytes": 8594 + }, + { + "path": "apps/web/src/i18n/metricFormatMessages.ts", + "sha256": "98df0d4403299ddc4ad8e040ea32ac64a49eb600bde00d320c15de7007b281d6", + "bytes": 6759 + }, + { + "path": "apps/web/src/i18n/modelHelpMessages.ts", + "sha256": "a674ce712e9efcc052851c487946d7872973328d3e46fc7d00c93099ac23a1f9", + "bytes": 2392 + }, + { + "path": "apps/web/src/i18n/oauthMessages.ts", + "sha256": "123160824a0ec1340e63ea32a8eb0922bd34554032529b430ebf2b22bd383e5c", + "bytes": 10639 + }, + { + "path": "apps/web/src/i18n/plainWordingResources.ts", + "sha256": "6e07232f136dc4e8f5fefc2f044ab416fe2a89d1fff02d8bca4b1cbb7d429ab3", + "bytes": 6996 + }, + { + "path": "apps/web/src/i18n/progressEn.ts", + "sha256": "82737292fc24294094f0ba49f10ca1c34d21b4b0bb6523dfc247528d45854a95", + "bytes": 12022 + }, + { + "path": "apps/web/src/i18n/progressKo.ts", + "sha256": "9824ab6f8cffaaed472e8e1103b21bcc6fbcb86678aca3984bfdb59a16e5f822", + "bytes": 18540 + }, + { + "path": "apps/web/src/i18n/recordDialogsMessages.ts", + "sha256": "b7aae4a364f1ba9837c8f5b0e56ddf5d5f5b12ac37baffe0dab2e3fc31f3f060", + "bytes": 13012 + }, + { + "path": "apps/web/src/i18n/recordPanelsMessages.ts", + "sha256": "154cec22fe897b98e2ead3ba949a74d3bd5fac6285826c80165b539d24479fd5", + "bytes": 15439 + }, + { + "path": "apps/web/src/i18n/remainingMessages.ts", + "sha256": "cd19698c3a17d079b3b4f8caaa823dde19f928b0ba448fe6c81fc384b9702d84", + "bytes": 15947 + }, + { + "path": "apps/web/src/i18n/researchMessages.ts", + "sha256": "41630f25c3a8452c0d3f033de3b98a4d6e4b63f59ef711bbc12332133707ba16", + "bytes": 9656 + }, + { + "path": "apps/web/src/i18n/researchPanelsMessages.ts", + "sha256": "d44377ec35e29bf678fd278a9dc4de2e0bd129f1e4cbfb8d2ebe6ef484ed2d0d", + "bytes": 13949 + }, + { + "path": "apps/web/src/i18n/reviewPanelsMessages.ts", + "sha256": "85697f02242596e89ef4540bc79a27054e152839ca5ef948e6d86d0920bed2a9", + "bytes": 8582 + }, + { + "path": "apps/web/src/i18n/setupMessages.ts", + "sha256": "57bb615ff2824f106d360e3225511904e82375c8d5a5688b3c8d5f41eaa6e85b", + "bytes": 8965 + }, + { + "path": "apps/web/src/i18n/sourceMessages.ts", + "sha256": "411b0ddc410873256b5e92f0e4c860534c7e8822d24b0db646c093b08a1b92fd", + "bytes": 7387 + }, + { + "path": "apps/web/src/i18n/stateFormatMessages.ts", + "sha256": "b79afa87819108fdf934c58bed4f2f20f9b75dfe6304dbb4953b3842be105831", + "bytes": 7201 + }, + { + "path": "apps/web/src/i18n/statusEnglish.ts", + "sha256": "cbe74b0405561122ca1c6a4f84a82a963ab3a716e0a3f1724a628a8cfe18d542", + "bytes": 5449 + }, + { + "path": "apps/web/src/i18n/statusResources.ts", + "sha256": "384db6289af00156497f8f335670fecbc81e93bfa85cee112cbba09ac6e8187c", + "bytes": 6176 + }, + { + "path": "apps/web/src/i18n/testOrderResources.ts", + "sha256": "4a2b1ffb5682c4fb05bdf0421b85b5e51a10d51b8b92def78e27fcf0a586e746", + "bytes": 3284 + }, + { + "path": "apps/web/src/i18n/traceEnglish.ts", + "sha256": "f06bf4f80b017768ce5f2ca7a28e21313ac2399e1e912dbf9c6114e5833f6b64", + "bytes": 3088 + }, + { + "path": "apps/web/src/i18n/traceFormatMessages.ts", + "sha256": "d60107707586d012de1cadb804e0425db6b73917621da12fc345e5e011ec8bb3", + "bytes": 4393 + }, + { + "path": "apps/web/src/i18n/traceMessages.ts", + "sha256": "f75a22e42cc334fa2e5109e9f5a3f888f36f4bb105b5ff39e5a05622af2c590a", + "bytes": 16967 + }, + { + "path": "apps/web/src/i18n/traceReasonResources.ts", + "sha256": "5b16f8e6e845dc273fd71a90c46762db16f0dea10a30b6f8157558253dca81fa", + "bytes": 3046 + }, + { + "path": "apps/web/src/i18n/uiFormatMessages.ts", + "sha256": "98be0f283f6cad865351f15b4494fc4374a3c2238bbc00e24c75ed2c4ac8e381", + "bytes": 5749 + }, + { + "path": "apps/web/src/i18n/workspaceDetailMessages.ts", + "sha256": "0acb2bdf49530bbd6d96d3591c4bee819bfd24bbabeed25b29f9a20cbdf4fc03", + "bytes": 4444 + }, + { + "path": "apps/web/src/i18n/workspaceMessages.ts", + "sha256": "c090b5cb8e9bf10c20e1b39a38e92b38cbf532179098a98b567ae16f7d694095", + "bytes": 12265 }, { "path": "apps/web/src/main.tsx", @@ -1428,8 +1723,8 @@ }, { "path": "apps/web/src/timelinePreview.tsx", - "sha256": "82d465743f5db61355c066a4786a9d3c9d65f566a4a398e19edbf974da1b0671", - "bytes": 1278 + "sha256": "1d7b979016be7b4c38553e0a70342352987f4cc49c3333f288b2a1d3b85fe91c", + "bytes": 1361 }, { "path": "apps/web/src/types.ts", @@ -1443,8 +1738,8 @@ }, { "path": "apps/web/src/workstation.css", - "sha256": "267ede96732993f45f0e2e790555d5645322065580145536ca81cbc9a2a10256", - "bytes": 58447 + "sha256": "ad065ed48c75c6d7d5e64359d88f6d559d31f54a0fe0bcfad4fa0a16499c54db", + "bytes": 59542 }, { "path": "apps/web/timeline-preview.html", @@ -1533,7 +1828,7 @@ }, { "path": "config/module-responsibility-budget.json", - "sha256": "6ec167786cdea9043243a5f2c2ced24560892261dbd5b946e600ff17cf311b36", + "sha256": "069352e792ae8118b06ebceab06725c88b529d46d96db66946a2510e0f1d6e4a", "bytes": 16573 }, { @@ -1718,8 +2013,8 @@ }, { "path": "docs/architecture/rpc-method-catalog.md", - "sha256": "c4fa7fb1183ff70cd5c1c79a7fc3099caaddbb5cac5ffd4f51e63524f008ec33", - "bytes": 14182 + "sha256": "58ce46d1e19e14e154d7fde5ac34221339f05f06f30ffe292b35aaed0a451cb8", + "bytes": 16886 }, { "path": "docs/architecture/user-activity-projection.md", @@ -1733,8 +2028,8 @@ }, { "path": "docs/brand/README.md", - "sha256": "9093cadf6eac65cfae9ca82dbce087b140eaff7a9c44ddd489f7980fff3cac19", - "bytes": 3066 + "sha256": "ac7fb1f860f8852b5fd29da5a048d79dfe729f36eb51d07b6e01e8e232224b01", + "bytes": 3781 }, { "path": "docs/brand/thoth-ibis-hero.png", @@ -1756,6 +2051,11 @@ "sha256": "f997f3b91b0e785608fb7d801f1bbc3b397313e7677ae06ff1b3f018df1fe892", "bytes": 2446 }, + { + "path": "docs/brand/toti-mascot.png", + "sha256": "096c1eb024d021e6578d4da52ae44086ed66fca52ba5267c2162f77f9626b1bc", + "bytes": 761446 + }, { "path": "docs/screenshots/01-trace-verdict.png", "sha256": "800800b0e2bcfdd290d2f911618aab8a8e60e8e803d2400df9829cd61a351989", @@ -2408,8 +2708,8 @@ }, { "path": "schemas/protocol/historical-result-input.schema.json", - "sha256": "be05af08db343a605d3ab94bef7df92353aa1d9a8a16e313442eea06f5546cf8", - "bytes": 1343 + "sha256": "664c6778448dec3cbfa1be7ec9de42f578e98a89309f78d2eb2843e47d71208d", + "bytes": 1593 }, { "path": "schemas/protocol/historical-result-view.schema.json", @@ -2441,6 +2741,21 @@ "sha256": "c50eacef23ff27d186c42eec5eb87bf21551deb7a4762e0d3a136d171806ec78", "bytes": 2196 }, + { + "path": "schemas/protocol/hypothesis-language-check.schema.json", + "sha256": "a78ed982969fb39203b2afe49e0d5e2ecfc1d62745c3aa6b5ef751cf53109181", + "bytes": 2241 + }, + { + "path": "schemas/protocol/hypothesis-portfolio-v4.schema.json", + "sha256": "7b7838b64f7eb8f4bac5aa5627929ca813017dee0d67fa911b158c8b002efcc0", + "bytes": 16742 + }, + { + "path": "schemas/protocol/hypothesis-table-fill-v4.schema.json", + "sha256": "aaca906106342c4d5b57a0f4d6684d9b7139371034a39f39b528fc8966ceceb3", + "bytes": 2603 + }, { "path": "schemas/protocol/jsonrpc-error.schema.json", "sha256": "fe01fe25fa4d94dee73291970dcf09347cc0d1b8240beda9504401c945469eed", @@ -2491,6 +2806,16 @@ "sha256": "0f796fe1930ec993ced11737706d00c80322b9be1dd4298a8d4bce533cd9917c", "bytes": 6959 }, + { + "path": "schemas/protocol/thread-request-v2.schema.json", + "sha256": "a41fa91a087a47f19fb32d57f7146855e2d5c909b82cb4672d81fe40df0318b8", + "bytes": 4634 + }, + { + "path": "schemas/protocol/thread-request-v21.schema.json", + "sha256": "04e487974f47bb1d3178cc8663e70224f7bbe5593b1c22a38fde7576c6c23652", + "bytes": 4897 + }, { "path": "schemas/protocol/user-activity-event-v1.schema.json", "sha256": "8b729705409beb0cafb7a89363f36a26849123f4a9e77cd6e36c412f321ed8b6", @@ -2668,8 +2993,8 @@ }, { "path": "scripts/hypothesis_v3_metrics.py", - "sha256": "3dc059322a96dcf87eef33374250942698f146a3fda76732e301f76cc7e532ad", - "bytes": 11873 + "sha256": "a460e75ebda654e475c355f98f099092ee650caa8fdaa5cab2f693997cf9220f", + "bytes": 12475 }, { "path": "scripts/measure_project_lists.py", @@ -3178,8 +3503,8 @@ }, { "path": "src/thoth/adapters/models/codex_oauth.py", - "sha256": "fd0dbe36302edfd26866259a105290bb5408165d114fd7b798ff65a36dfb33c1", - "bytes": 25725 + "sha256": "9d9ccd6548c79140d438146851299581ccaf68d795f7f122365771e91d4e34c1", + "bytes": 25619 }, { "path": "src/thoth/adapters/models/codex_profile.py", @@ -3203,8 +3528,13 @@ }, { "path": "src/thoth/adapters/models/hypothesis_contract_text.py", - "sha256": "149262ad513e00f3d59a2970a5d4d830805501bbb0e8dd9a512c0b8a5444e45b", - "bytes": 9957 + "sha256": "f041f8f4821f409859caa51d38a3d843590a911a7aecded7023b8db37e0e10c7", + "bytes": 11288 + }, + { + "path": "src/thoth/adapters/models/language_contract_text.py", + "sha256": "88f77bf56067469a5525c6c79cd543b7b430904c26074582a88f0fc5d9f35988", + "bytes": 2435 }, { "path": "src/thoth/adapters/models/local_credentials.py", @@ -3213,8 +3543,8 @@ }, { "path": "src/thoth/adapters/models/openai_responses.py", - "sha256": "97b6866cc6db523b0d1baf8d00de6a44d87376ea8b3a017a6082449ca9ed0b4f", - "bytes": 12723 + "sha256": "609874d91e255abfa4de6d13d65a4db5c79f9284a5750b9479b80415daa861c7", + "bytes": 13246 }, { "path": "src/thoth/adapters/models/receive_stats.py", @@ -3928,8 +4258,8 @@ }, { "path": "src/thoth/application/commands/operations.py", - "sha256": "5bc5d3d0b97502aaa5efd3c3ad93e7066c30f53f72a9cdcec312f1273404a49f", - "bytes": 10300 + "sha256": "dfab1cd6f92e5fb95af36a865d5b1c4261f799f9cf23e555b2631528e9577cc4", + "bytes": 10541 }, { "path": "src/thoth/application/commands/outcomes_full.py", @@ -3973,8 +4303,8 @@ }, { "path": "src/thoth/application/commands/research_history.py", - "sha256": "6e5a6fcfe37852584579c588cecb70192577834069a90c0b53c332dee42842c2", - "bytes": 3036 + "sha256": "e0f99864136bf397d15e563cd3cf860c00ed764cc6ea8bea98b7d1689bc73ba0", + "bytes": 3195 }, { "path": "src/thoth/application/commands/research_operations.py", @@ -3988,13 +4318,13 @@ }, { "path": "src/thoth/application/commands/research_resume.py", - "sha256": "03e28fbc3622ce57c4730368777b65d27ca7cd13192050c2ae20526eb96068b9", - "bytes": 2158 + "sha256": "ebe62c9e8e17338fd220bb008997a321c147c905affb7a742022cd23df115324", + "bytes": 2271 }, { "path": "src/thoth/application/commands/research_threads.py", - "sha256": "92df1f6d92b896dc25aaf81a53c9a59213e00fe188033c9fc1e0655bba8f991b", - "bytes": 40141 + "sha256": "58526b77e0d46a0b63ac3a3e08c484fdb6606eba32a50e7e62ccfdd881506d3a", + "bytes": 39382 }, { "path": "src/thoth/application/commands/resource_scopes.py", @@ -4028,8 +4358,8 @@ }, { "path": "src/thoth/application/commands/thread_analysis.py", - "sha256": "02b84fc1e8eb7007fa3140ce5a9a8e2e100ddf9e66420dbc08897af777a75c9c", - "bytes": 31828 + "sha256": "e1a71e763bc0a095227e65409537ab829bd2734e61675234cdbedbe975bbbbcb", + "bytes": 31310 }, { "path": "src/thoth/application/commands/threads.py", @@ -4053,8 +4383,8 @@ }, { "path": "src/thoth/application/commands/workspace_setup.py", - "sha256": "36ea070332ff8e842f0a01bdcef8a9aeef504fb77499b3221954edcd280bca86", - "bytes": 6941 + "sha256": "2ab1bd17ebc7154ec0c9525cf9d4565eca89596d4c24844dd8b4a2b2b774113c", + "bytes": 7433 }, { "path": "src/thoth/application/reducers/__init__.py", @@ -4191,6 +4521,11 @@ "sha256": "94da83ad3172c273405325119e7ccd02adea056fca64bc2180564e1ba70aeba5", "bytes": 5875 }, + { + "path": "src/thoth/application/services/browser_submission_scope.py", + "sha256": "93d07fe43da2bcf9533ae7b0f23e6dfee22c85d391914fdcef4484c45c729eb5", + "bytes": 725 + }, { "path": "src/thoth/application/services/closure_open_items.py", "sha256": "445eff2a5fb87452bb0058938f3bd6592b07bbebcf787d7b3661afab032bdeef", @@ -4223,8 +4558,8 @@ }, { "path": "src/thoth/application/services/conversation_inputs.py", - "sha256": "134cd2fc7761d2631e49699f8d9e737be1936870df4acf4046ec39c1649e23db", - "bytes": 3348 + "sha256": "85fa97339addcf08f94f31f48865312a1e6fd44391672da3741ee8cc77d4928d", + "bytes": 3333 }, { "path": "src/thoth/application/services/conversation_router.py", @@ -4291,6 +4626,11 @@ "sha256": "1fd0f1a6f872c4d24eff55fe6a3a576c7ba85e190f586a245399a9c0b03ea780", "bytes": 1725 }, + { + "path": "src/thoth/application/services/english_prose.py", + "sha256": "eff12af5f57f6380f9e29bfcf58b4796759de02a5f42faaed5f3678d3a800165", + "bytes": 6933 + }, { "path": "src/thoth/application/services/evaluation_inputs.py", "sha256": "406ab2bc36431d6e71fad3e3ffd581c7cf413df91555d905b7af46413f13e91c", @@ -4346,6 +4686,11 @@ "sha256": "afdc8a783790d22ab153ea23e6003343b9c7c9313efe63ef18f6ff393b434bde", "bytes": 34989 }, + { + "path": "src/thoth/application/services/expected_result_view.py", + "sha256": "b0c8b2ce4347b1e5037a1cef369a48afe51c1b7f6efc1dbf96b913122546096b", + "bytes": 3506 + }, { "path": "src/thoth/application/services/export_snapshot.py", "sha256": "bdfde6e1dc580e48245336d77bfb82be289474fa534d901d527a8c7fdf969da8", @@ -4368,8 +4713,8 @@ }, { "path": "src/thoth/application/services/historical_access_verification.py", - "sha256": "d6d7ed96f4014a93806e0d17e7d821fad62a676d02d42e7b5117c95a507da91a", - "bytes": 5685 + "sha256": "e5343f5365f88bbde3c1e87bc3182a39ce959f0ee592ed703ba07761081f9c78", + "bytes": 5687 }, { "path": "src/thoth/application/services/historical_result.py", @@ -4378,8 +4723,8 @@ }, { "path": "src/thoth/application/services/history_associations.py", - "sha256": "2833ca3924a8222c1151f2d5339b9269fd3a73eaf156d18affa224f7aa87fd1c", - "bytes": 2725 + "sha256": "f36b03bdc3014712ea756f6bc69879cebd2a50e28b743f17efc9873d20e18b3e", + "bytes": 2710 }, { "path": "src/thoth/application/services/history_change_summary.py", @@ -4393,8 +4738,8 @@ }, { "path": "src/thoth/application/services/history_projection.py", - "sha256": "3ffe64328f3e6daf46cd91d1488ba004205f65cf0be2c9fad5bdc3fda4950b23", - "bytes": 13077 + "sha256": "1d7f4be8aa4bae5f61b4d1f07bb8e439bf4d0d3a2cff340960e4fc7a40b57eff", + "bytes": 12791 }, { "path": "src/thoth/application/services/human_actor.py", @@ -4403,8 +4748,8 @@ }, { "path": "src/thoth/application/services/hypothesis_contract_v3.py", - "sha256": "58b0d3665765d948a2a8313e9c7aaaa3c3b4990f7f995a93eb52bf1c14a3951b", - "bytes": 12762 + "sha256": "350539fbe8e07d3dcca58b72d9e24fc76e574d666cac78632655f726250d7600", + "bytes": 14837 }, { "path": "src/thoth/application/services/hypothesis_link_recheck.py", @@ -4418,8 +4763,8 @@ }, { "path": "src/thoth/application/services/hypothesis_projection.py", - "sha256": "ec5b066fc8128b8b329aeee2b7ba5eef733ca617697a1699fc90eb5ae27c66b6", - "bytes": 12186 + "sha256": "60523b624ea7990999b780cf9107a9754cc256d26f9f9ac2012f626274d842d7", + "bytes": 12913 }, { "path": "src/thoth/application/services/hypothesis_same.py", @@ -4433,14 +4778,19 @@ }, { "path": "src/thoth/application/services/hypothesis_table_filler.py", - "sha256": "e14e9d46d3b849875cc5d6e88d43890f8d4248f7b866437faaa1f343bc427335", - "bytes": 15382 + "sha256": "6234d285149fb523f4710e1f8e23caa581a0ff01af8c5e4e080ac7ce1c9b9149", + "bytes": 18434 }, { "path": "src/thoth/application/services/hypothesis_test_lifecycle.py", "sha256": "db0df0b9a151159fbf4d9c3d8bbde40c7d190888e6005a988fb2aa7fe8f69e4e", "bytes": 17179 }, + { + "path": "src/thoth/application/services/hypothesis_v4_projection.py", + "sha256": "1962dcd80a4aca253a336d325e91c2a02d1a5e33b4f7d05d8e3a617bf90e0ee8", + "bytes": 1874 + }, { "path": "src/thoth/application/services/hypothesis_verdict_link.py", "sha256": "3b49c96f6c6c3487a54f96e3a677936c1087fb7dba19f1e80a7d28d4c76fd5ef", @@ -4628,13 +4978,13 @@ }, { "path": "src/thoth/application/services/post_execution_memory.py", - "sha256": "813a097563a8ce1a0e753e7f0088cd9820d532411fbb187a43fc046d7e1b102a", - "bytes": 7840 + "sha256": "9c31c352809526325a279612b8b67b2d1779b94e2b7e78101d76635473d039a6", + "bytes": 7825 }, { "path": "src/thoth/application/services/post_execution_resume.py", - "sha256": "f9567e48f671e2887ab7d15577b9d9b3683550012560d48dec12c2178d31e9ad", - "bytes": 5721 + "sha256": "2217e28576e8c87005ef55cc983fb5763545d433acbb6c9ae7b64d828dd97294", + "bytes": 5840 }, { "path": "src/thoth/application/services/privacy_guard.py", @@ -4648,8 +4998,8 @@ }, { "path": "src/thoth/application/services/provider_usage_service.py", - "sha256": "d197131af2775d18b5fd1b93ee5486056a73ba3cadf3a9751b11c283612f6ea8", - "bytes": 2277 + "sha256": "0c4041f5aedf3a782ac5536cc47ef82284e0aa01f51b1ba3dbfb031e7109c510", + "bytes": 2262 }, { "path": "src/thoth/application/services/public_web_policy.py", @@ -4696,6 +5046,11 @@ "sha256": "9eca175f531c5f162b32a15a7c9c5cc8a628600f714ebee94158ea72557e8de5", "bytes": 29558 }, + { + "path": "src/thoth/application/services/request_language.py", + "sha256": "06e73d18708c0f2aa85c6b61a07df36d2417d97272306418c5a45f803544f8ff", + "bytes": 4292 + }, { "path": "src/thoth/application/services/request_records.py", "sha256": "69575fbb567fe12aca8cf9483b8f9eb9708ed4d74856f9e572aca83596b9914f", @@ -4708,8 +5063,13 @@ }, { "path": "src/thoth/application/services/research_analysis.py", - "sha256": "e52944ce36bf07b1c89896fef8b54940cb253a9a35700685f702d3bfabe31a13", - "bytes": 38308 + "sha256": "a336183027bfb71ae453ab437b0e145ddc464c1b2e9d286a9d66737f634642e5", + "bytes": 37387 + }, + { + "path": "src/thoth/application/services/research_answer.py", + "sha256": "553446952d8e12e5bff7153b2adf0bf16875e76e271b1e72a05fa0181f9624dc", + "bytes": 607 }, { "path": "src/thoth/application/services/research_attempt_admission.py", @@ -4718,8 +5078,8 @@ }, { "path": "src/thoth/application/services/research_attempt_runner.py", - "sha256": "00f240fe2cde75ec72c469c462eaa756d00dd7293fab79760d86a24051774b6c", - "bytes": 18658 + "sha256": "868fab123ff6482f97281f1d853e8208324161a7f21227da76a3b4ff1e333acf", + "bytes": 18844 }, { "path": "src/thoth/application/services/research_basis_capture.py", @@ -4753,8 +5113,8 @@ }, { "path": "src/thoth/application/services/research_controls.py", - "sha256": "dc1764a1b74e7e70d3a3c5df67507412fc3e2bcbfd741bb88bacbf8e06a230a6", - "bytes": 7008 + "sha256": "3f13300b03c786e0fa6de26c640ecd86a8caf59d0f41dd28aac56fd7f34c4df8", + "bytes": 7118 }, { "path": "src/thoth/application/services/research_coverage.py", @@ -4793,8 +5153,8 @@ }, { "path": "src/thoth/application/services/research_followup_projection.py", - "sha256": "bdfccc461cafe1a45b37667fdf1718771e9d65e0b6419e11271e8dde1f421322", - "bytes": 11689 + "sha256": "a13dec7a94ad22997c87df5f3a16153cf85f2dd23ea563c7f391169a7a270c55", + "bytes": 11701 }, { "path": "src/thoth/application/services/research_freshness.py", @@ -4813,8 +5173,8 @@ }, { "path": "src/thoth/application/services/research_history_scope.py", - "sha256": "78ab23b305f8a1804eda2342dbba4460e020e1c483382a83b43e0b3dca316036", - "bytes": 3691 + "sha256": "3f7e8cd47418f7e88d37b05134795bb150e6384f0186696b0913d311fcd5af88", + "bytes": 3699 }, { "path": "src/thoth/application/services/research_identity_service.py", @@ -4823,8 +5183,8 @@ }, { "path": "src/thoth/application/services/research_input_queue.py", - "sha256": "16ff4de7395c5599ea616eb74d92e32020d9a9ebc2f9de081ea2aee051039780", - "bytes": 23195 + "sha256": "6abfc9041345bc883e1d67f46bb2890873cf773eaf7292689c3bc98cbdd3f6b2", + "bytes": 23210 }, { "path": "src/thoth/application/services/research_leases.py", @@ -4838,8 +5198,8 @@ }, { "path": "src/thoth/application/services/research_models.py", - "sha256": "7579ed734f931ae1c6cfe750882a7cdfb1b9a5832a86420c560a06dd0d2d98d1", - "bytes": 3724 + "sha256": "2a25d275861e5428ca993f2c810d1a6fb96b0d8af555d01c4e9fa5aab75a962c", + "bytes": 4019 }, { "path": "src/thoth/application/services/research_operation_access.py", @@ -4883,8 +5243,8 @@ }, { "path": "src/thoth/application/services/research_stages.py", - "sha256": "d61c714a1b25f6785b6890ba724aeb10ae1345643c8e7a529b863e6551ce9193", - "bytes": 8450 + "sha256": "49a0e768233fed2e3929b9117c720687e6bdeef388654d01d8428e4c13e7f8a5", + "bytes": 8791 }, { "path": "src/thoth/application/services/research_termination.py", @@ -4893,8 +5253,8 @@ }, { "path": "src/thoth/application/services/research_thread_read_snapshot.py", - "sha256": "d1efd131cfb645b53fa86f43e48a14a12068791e3b51e87b4a6516797de53c6b", - "bytes": 8497 + "sha256": "1e80e63cd7cbcb1b1a5e927249de5413b9cb06abb47efc83a0e7f551820a7218", + "bytes": 8280 }, { "path": "src/thoth/application/services/research_usage.py", @@ -5133,8 +5493,8 @@ }, { "path": "src/thoth/application/workflows/thread_cycle.py", - "sha256": "3b0bbc7726d45195e411b5f106dd9230ef72346b34c08975fd45a7e6ea15b762", - "bytes": 37191 + "sha256": "5a214b57c99b8b1c97ecaec87ac19e33277d11acb0778a448c949d72bdd7e229", + "bytes": 37161 }, { "path": "src/thoth/application/workflows/thread_cycle_preparation.py", @@ -5273,8 +5633,8 @@ }, { "path": "src/thoth/apps/research_entry_composition.py", - "sha256": "0fc417006a535f7794cf7fb11979fd85cb8a4c9493a552d737e2401060640ade", - "bytes": 14197 + "sha256": "e4c704683973c03144b6baa3163dfcd9f94f14b56f99c5b585a90b10043585ed", + "bytes": 14148 }, { "path": "src/thoth/apps/research_history_composition.py", @@ -5293,8 +5653,8 @@ }, { "path": "src/thoth/apps/restore_profiles.py", - "sha256": "1a7c3a28789ffd2ac803a784ef0a8148348b0b910b4c85afbe407419af312144", - "bytes": 3268 + "sha256": "fc13c55c2df5bb4fb8e07aad0dbcb1b1037fdb1cbd1b7ee46db143cf00e6e851", + "bytes": 3349 }, { "path": "src/thoth/apps/revision_composition.py", @@ -5588,13 +5948,18 @@ }, { "path": "src/thoth/domain/hypothesis.py", - "sha256": "87006cd04669f05d437b331319f1ccb529cd523bc7b892b41ca39b92b4aee1f3", - "bytes": 8787 + "sha256": "aa2a2528327a0442050c027bb2cf1287b6dd7ea72923aae9ce5e7583b12a6152", + "bytes": 8951 }, { "path": "src/thoth/domain/hypothesis_full.py", - "sha256": "a14c3bbc50ff1411d606bee633f0ff71dd7cf190fe56dea6551a92ad408755a0", - "bytes": 6659 + "sha256": "9925cd6afdfcf27c90bb60373ffe0cf2f2ab4ca76fadcf3332b918273222346e", + "bytes": 7258 + }, + { + "path": "src/thoth/domain/hypothesis_record_versions.py", + "sha256": "fa385bd4198921a7c96d1e62036fae487b142922b24d2445c6da7331efc23cb7", + "bytes": 823 }, { "path": "src/thoth/domain/hypothesis_review_validation.py", @@ -5606,6 +5971,16 @@ "sha256": "ac9c70ff21085e5e1a5ecb72afad85c661aaeb42157a1d65edd81cb77c550f5e", "bytes": 3207 }, + { + "path": "src/thoth/domain/hypothesis_v4.py", + "sha256": "ef66dbbdd3d4d99ba445677ddf4252a68a0bdeb5d53cfca61a56d9495a3cd1c0", + "bytes": 2987 + }, + { + "path": "src/thoth/domain/hypothesis_v4_projection.py", + "sha256": "d4503941fb2a14efdb104b867a141c19d08fba6ec40d1ba0cf1220475bdcc013", + "bytes": 1332 + }, { "path": "src/thoth/domain/ids.py", "sha256": "6599c536ace328e8db0796a7bea78e232188faaf9995d6941cdf05896a65e357", @@ -5636,6 +6011,11 @@ "sha256": "c082f19952443923aafd5638e1be152bb61e4c01b11c5095dcfd9e648c0ee98e", "bytes": 4361 }, + { + "path": "src/thoth/domain/language_validation.py", + "sha256": "d20df6d6065ac49e51a505bdb3e88cb9b19a7f3959c4060ebe54556997a7966d", + "bytes": 681 + }, { "path": "src/thoth/domain/lesson_ref.py", "sha256": "f1697014c5c039fa0b31cf0ac036748fe9a0db0a70aa1345f4f44dd9fda434f2", @@ -5678,8 +6058,8 @@ }, { "path": "src/thoth/domain/model.py", - "sha256": "7d7b89574c5fdf939c6408c50d040ce61c13a459e0b875cd88023a230d800f76", - "bytes": 2227 + "sha256": "ec3b4dd3e896d56a45e862db07d197dee6932d636f4af144fc1631f8395c3ec2", + "bytes": 2341 }, { "path": "src/thoth/domain/model_call_settings.py", @@ -5798,13 +6178,13 @@ }, { "path": "src/thoth/domain/research_codec.py", - "sha256": "4f43f88419657574c5ad6cf42c8c4c9e3ff880dd7f7f4daf5e70a82c42f6f97c", - "bytes": 3185 + "sha256": "3b13d1d64e58e73cbe6ab1206f917293c1b16819ed5eab69a79dbb14d8e35138", + "bytes": 3295 }, { "path": "src/thoth/domain/research_execution.py", - "sha256": "c3d6a927e30875248e8e6a45e6985bfb9986d3e6504d44afd920045e67a44a65", - "bytes": 5100 + "sha256": "5cdf5524f111d7574cf78fc6991f16cf0d1b4ac03be1641dc94730e06f8ab67d", + "bytes": 5214 }, { "path": "src/thoth/domain/research_failure.py", @@ -5818,13 +6198,13 @@ }, { "path": "src/thoth/domain/research_history.py", - "sha256": "32511765e75e0814b23e8d23b1b1239230dc4819ade5145716063e756b7021a8", - "bytes": 6934 + "sha256": "07107c8729a75233144ff55645621e0389f1056523fb3b7d1c88610d138d9332", + "bytes": 6992 }, { "path": "src/thoth/domain/research_identity.py", - "sha256": "2f75d9622d31a70ab7a5d930d7e2e7b7d3007d18ef9c1e82cac02bf98d33454b", - "bytes": 8709 + "sha256": "a38f25edeccd8069d739d06053cc2b132e267cef0f39d89b96c05b5bcd4cbe84", + "bytes": 8876 }, { "path": "src/thoth/domain/research_lease.py", @@ -5848,8 +6228,8 @@ }, { "path": "src/thoth/domain/research_request.py", - "sha256": "ff51f278d39b9e82ca1c9f9d8f5721bd68517c0cdbe850c53df90ed255df3f07", - "bytes": 5690 + "sha256": "1a16eb7fe15faffc7aea25b5079f96adc12c5ae033c223c5fd74e64d412a0f43", + "bytes": 6404 }, { "path": "src/thoth/domain/research_stage.py", @@ -5861,6 +6241,11 @@ "sha256": "c6faca5cd2db9c24058c80bc3bcea22865a12c4d88ba26232c8b00bb5d9ca52a", "bytes": 10680 }, + { + "path": "src/thoth/domain/response_language.py", + "sha256": "fe8ccf23cf04558d58dbc68d5c4f3304207bd8bbd97c667bf48ce74e4e5f9dae", + "bytes": 1116 + }, { "path": "src/thoth/domain/restore.py", "sha256": "53a85e2d89a8a030acd05adbe4f85626a9a123af7d8f1ee3da31ae2d57eb51af", @@ -6308,8 +6693,8 @@ }, { "path": "src/thoth/protocol/schema_export.py", - "sha256": "f9002d9d4cbecab8d0c76a0c2e3e46a0ae1453baddeb97994a445af926e9dac6", - "bytes": 2564 + "sha256": "46a7f6a6e30fcfb02e41a2b7f534f5a5e8c6ed029fed7db31fbfb76b549e90d8", + "bytes": 3110 }, { "path": "src/thoth/protocol/stdio.py", @@ -6821,6 +7206,16 @@ "sha256": "6448a35592adffb56c088f5d1cff16653be0d0e68ae08cf95de44d2050575697", "bytes": 34131 }, + { + "path": "tests/fixtures/language_expected_results.json", + "sha256": "96facf1e998dc3d7d3ef03343598c827a9e6a86676fd8bb642c565248a486f68", + "bytes": 1461 + }, + { + "path": "tests/fixtures/legacy_language_contracts.json", + "sha256": "64e41c9aac4e7b561602b5f623db4acc3ac5cad03af8ca40ceec75bf1b429165", + "bytes": 8900 + }, { "path": "tests/fixtures/memory_eval/families.example.jsonl", "sha256": "3b06458098cfa1753a110c66e78d15a867153564291a31492a23b3c76a6e25e5", @@ -7476,6 +7871,11 @@ "sha256": "bd155d0e6480f04dbe5250b0f6dc0063a361d20cacbd19e16374e02d8c484f9f", "bytes": 9127 }, + { + "path": "tests/integration/test_hypothesis_v4_run.py", + "sha256": "41347348065d50d4536a3de8eb44b50f36ce1ab34f0cf1e4fec60ea24f7cedf5", + "bytes": 7533 + }, { "path": "tests/integration/test_improvement_full_rpc.py", "sha256": "c86552452f8b85cc93cb30e20680104f358fb99b39c359735acccab436647ce9", @@ -7506,6 +7906,11 @@ "sha256": "ac46307b0b84528e05aa99f6ff5f285ea4feaa32c4fa89f4032a1d7f3a562819", "bytes": 7739 }, + { + "path": "tests/integration/test_language_revision_roundtrip.py", + "sha256": "28ee245e5903ea74700ccbc9a6167a60b0ff5cbc0765c6299a76ceb95f74b758", + "bytes": 3200 + }, { "path": "tests/integration/test_lesson_recall.py", "sha256": "b18546a7e7dbaeb7765d7146b18f680ea524bb6db796c4557d7486261f286ac1", @@ -7826,6 +8231,11 @@ "sha256": "0c74fa1c5549eafeb6613eb317a85cf56b9b6b98974c9091aef3f171ddbdfd8e", "bytes": 4395 }, + { + "path": "tests/integration/test_request_response_language.py", + "sha256": "681cb4afa6fcf58c44a28a7af69d32f765a139276206e7f55d33dc68c247b7c5", + "bytes": 6983 + }, { "path": "tests/integration/test_research_assembly_cache_basis.py", "sha256": "9cf0d9b7b30eed98f7f40da082025d5879524e5bba73702eb1df142efa48990e", @@ -7848,8 +8258,8 @@ }, { "path": "tests/integration/test_research_export_grounding.py", - "sha256": "f1857c931af263eb69e4860a1bafcd74c6f0258878814ade9daaba24a51619a8", - "bytes": 3496 + "sha256": "e5b15201dc129af9acd091aeb13864a972308c939e395887e4d4ed7a4475fe35", + "bytes": 3622 }, { "path": "tests/integration/test_research_failure_terminal.py", @@ -8371,6 +8781,11 @@ "sha256": "2d1406bc6b8502ff6ec28f41d5cf59943a231c177fb91f5659fb4555320ee096", "bytes": 6738 }, + { + "path": "tests/unit/application/test_english_prose.py", + "sha256": "3633ddb6e23cfce335d2312e7ba2ce5a68f29c310a7dcd26e957fc219e56923b", + "bytes": 7280 + }, { "path": "tests/unit/application/test_evidence_focus_draft.py", "sha256": "2d5d15faaf39d586eeb37a0d7ed82034abb13c481a19136540c1a80ee9fcfa8a", @@ -8496,6 +8911,11 @@ "sha256": "d9f7d0a772eab9e4d90158e7b591ccf45e61796b2f5aba263b118d3962fb822e", "bytes": 3663 }, + { + "path": "tests/unit/domain/test_expected_result_consumer_fixture.py", + "sha256": "e97bb2dbfba581dedce51c43de9c0095cbbad893eb002dd62a3ea95e90d2fa8b", + "bytes": 791 + }, { "path": "tests/unit/domain/test_extended_models.py", "sha256": "805be1904ed88b46cb86a92b3cbc8f434eaacc7c542936061553d2d7ac2daf8e", @@ -8571,6 +8991,11 @@ "sha256": "c4e7b2901a9187136be38d3126548130fe32be074b655d8044c5247c4b1dba6a", "bytes": 3063 }, + { + "path": "tests/unit/domain/test_hypothesis_v4.py", + "sha256": "4a4c845aa8d0a8b2efc691361c7c3f8e85e7a1b319f4ef145e6ff59312584b84", + "bytes": 5212 + }, { "path": "tests/unit/domain/test_invariant_mutation_guards.py", "sha256": "f35acfc65bcfad95c8f31a0e179f90ba36d36ca2c9a0151290875c5b670f6bd3", @@ -8841,6 +9266,11 @@ "sha256": "e08ce696504863775b430fd6d1a72b710db42970228e5bd0851547b50b4fb3bd", "bytes": 2309 }, + { + "path": "tests/unit/models/test_response_language_wire.py", + "sha256": "2e766b09f882242c7d6e37529dc9348d59d8917bc132681261d02764bba5b927", + "bytes": 7811 + }, { "path": "tests/unit/models/test_role_wire_context.py", "sha256": "66adf55910ede03832dc2360f19123186ec7babc546e2b9afcd5f50d1b228c01", @@ -8981,6 +9411,11 @@ "sha256": "b8e38880709329ad4fe4618305b07e7377fe03bc30659b303b333367a3b330b3", "bytes": 4869 }, + { + "path": "tests/unit/services/test_browser_submission_scope.py", + "sha256": "ca702d65dac6c9e84030590e17261e47a46163c284a5fb2ec8b878d6a7726f47", + "bytes": 1088 + }, { "path": "tests/unit/services/test_counter_wave_contract.py", "sha256": "338b8c301b7a072ce847c3b4f79aaf87b53c4497bae93748022116b9a4f54ceb", @@ -9001,6 +9436,11 @@ "sha256": "6589e31690e08661710c4d9684a2dcef9bd8fdea13979d274b6b1ffa86b78652", "bytes": 1658 }, + { + "path": "tests/unit/services/test_expected_result_view.py", + "sha256": "7ab4c138ef0707d86e998e5aeded06bbba6bc5bbe4f9fc5a11f3be5fbe17cf9c", + "bytes": 2440 + }, { "path": "tests/unit/services/test_history_change_summary.py", "sha256": "1802c58e727470625581ff23be1aa50c755b6603b78cefa0602b4f8ee50dc29d", @@ -9011,11 +9451,21 @@ "sha256": "18ecbad5e0ed9b152db855902e271f10eb4ead60124b6bddaec3656c6ccd2f0f", "bytes": 12715 }, + { + "path": "tests/unit/services/test_legacy_language_goldens.py", + "sha256": "0e4df66ae05725ba7944f4a599fd584fb314fee56c0aa1f3e233574337a217a2", + "bytes": 2025 + }, { "path": "tests/unit/services/test_model_route.py", "sha256": "ee0f9a219d7daa46fd095b69b66a1bb48d3046be38130a238d1a30b0c1d9d743", "bytes": 2599 }, + { + "path": "tests/unit/services/test_request_language.py", + "sha256": "1ad6e37a64a25a270404024cf85e7f4e6220eb5111f407a8e0df9a74d6e65a2d", + "bytes": 6220 + }, { "path": "tests/unit/services/test_research_followup_contract.py", "sha256": "ffc3a3192b07381e6867a716fed95159349e58a2571eba2036a49697ac823266", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 84a337a..3d7b49c 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,5 +1,7 @@ +import { t } from "./i18n/locale"; import { lazy, Suspense } from "react"; +import { LanguageSettings } from "./components/LanguageSettings"; import { LiveProjectWorkspace } from "./components/LiveProjectWorkspace"; const HistoryExample = import.meta.env.DEV ? lazy(() => import("./components/history/HistoryExample")) : null; @@ -8,10 +10,10 @@ const ResultExample = import.meta.env.DEV ? lazy(() => import("./components/Resu export function App() { const example = typeof window === "undefined" ? null : new URLSearchParams(window.location.search).get("example"); if (HistoryExample && example === "history") { - return 이력 화면 예시를 여는 중…

}>
; + return {t("examples.historyLoading")}

}>
; } if (ResultExample && (example === "result-before" || example === "result-after" || example === "result-iris")) { - return 결과 화면 예시를 여는 중…

}>
; + return {t("examples.resultLoading")}

}>
; } - return ; + return <>; } diff --git a/apps/web/src/api/capabilities.ts b/apps/web/src/api/capabilities.ts index bc4950b..218c8c0 100644 --- a/apps/web/src/api/capabilities.ts +++ b/apps/web/src/api/capabilities.ts @@ -1,3 +1,5 @@ +import { UiError } from "../i18n/UiError"; +import { t } from "../i18n/locale"; import catalog from "../../../../schemas/protocol/public-method-catalog.json"; import { readQueries } from "./rpcClient"; @@ -6,12 +8,12 @@ export const capabilityCatalog = catalog; export const capabilities = catalog.methods; export const namespaces = [...new Set(capabilities.map(method => method.namespace))].sort(); export const namespaceLabels: Record = { - project: "프로젝트 · 자료 · 접근", thread: "작업 · 요청", investigation: "자료 탐색", evidence: "근거 · 주장", - object: "판단 객체", criteria: "평가기준", hypothesis: "가설 · 검증", action: "행동 · 권한", - execution: "실행 · 복구", revision: "이력 · 분기 · 복원", outcome: "관찰 결과", memory: "프로젝트 기억", - improvement: "개선 · 평가", receipt: "영수증 · 추적", closure: "마감 · 보존", export: "내보내기", - operation: "비동기 제어", model: "모델 설정", workspace: "워크스페이스 설정", - projectpack: "검증 예제", field: "현장 평가", 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") { @@ -29,8 +31,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 Error("입력은 JSON 객체여야 합니다."); + 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 Error("선택한 프로젝트와 project_id가 일치해야 합니다."); + if (!projectId || input.project_id !== projectId) throw new UiError("api.projectMismatch"); return input; } diff --git a/apps/web/src/api/conversation.ts b/apps/web/src/api/conversation.ts index 118bd96..6753bee 100644 --- a/apps/web/src/api/conversation.ts +++ b/apps/web/src/api/conversation.ts @@ -1,25 +1,26 @@ -import { z } from "zod"; -import { rpc } from "./rpcClient"; +import { t } from "../i18n/locale"; +import { z } from "zod"; +import { rpc } from "./rpcClient"; 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"; - -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(), - 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() }); -export type ConversationInput = z.infer; + +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(), + 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() }); +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 }; 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 { try { return await readConversationPage(projectId, threadId, before, signal, onBlocked); @@ -37,10 +38,10 @@ async function readConversationPage(projectId: string, threadId: string, before: 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); + 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", unavailable: "이 답변은 현재 권한으로 읽을 수 없거나 저장 기준을 확인하지 못했습니다." }; + 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 }>( @@ -67,13 +68,13 @@ async function readConversationPage(projectId: string, threadId: string, before: throw new ConversationRefreshError(); } 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. */ + 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[] { - if (!status) return turns; + 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 && @@ -83,16 +84,16 @@ export function withCurrentCheckpoint(turns: ConversationTurn[], status?: {curre 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) => `thoth:draft:v1:${JSON.stringify([project, thread])}`; + } : turn); +} + +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)) ?? ""; } catch { return ""; } + try { return sessionStorage.getItem(draftKey(project, thread, scope)) ?? ""; } catch { return ""; } } 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), text); else sessionStorage.removeItem(draftKey(project, thread)); return "SAVED"; } + 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/conversationRefresh.ts b/apps/web/src/api/conversationRefresh.ts index 3f1b531..7cb072b 100644 --- a/apps/web/src/api/conversationRefresh.ts +++ b/apps/web/src/api/conversationRefresh.ts @@ -1,3 +1,4 @@ +import { t } from "../i18n/locale"; import { RpcTransportError } from "./rpcClient"; /** Only transport failures from an RPC read may retain the previous display. */ @@ -7,7 +8,7 @@ export function isTransientReadFailure(error: unknown): boolean { export class ConversationRefreshError extends Error { constructor() { - super("대화를 다시 확인하지 못했습니다. 잠시 후 다시 읽어주세요."); + super(t("conversation.refreshFailed")); this.name = "ConversationRefreshError"; } } diff --git a/apps/web/src/api/draftLanguage.ts b/apps/web/src/api/draftLanguage.ts new file mode 100644 index 0000000..49bda79 --- /dev/null +++ b/apps/web/src/api/draftLanguage.ts @@ -0,0 +1,22 @@ +import type { BrowserScope, ResponseLanguage } from "./localWorkspacePersistence"; + +function key(scope: BrowserScope, project: string, thread: string): string | null { + const identity = scope.mode === "LOCAL" ? scope.workspaceId : scope.browserScopeId; + return identity ? `thoth:draft-language:v1:${JSON.stringify([identity, project, thread])}` : null; +} + +/** Undefined means inherit the prior accepted request; null is an explicit legacy choice. */ +export function readDraftLanguage(scope: BrowserScope, project: string, thread: string): ResponseLanguage | null | undefined { + const name = key(scope, project, thread); + if (!name) return undefined; + try { + const value = (scope.mode === "LOCAL" ? localStorage : sessionStorage).getItem(name); + return value === "ko" || value === "en" ? value : value === "auto" ? null : undefined; + } catch { return undefined; } +} + +export function writeDraftLanguage(scope: BrowserScope, project: string, thread: string, language: ResponseLanguage | null): boolean { + const name = key(scope, project, thread); + if (!name) return scope.mode === "HOSTED"; + try { (scope.mode === "LOCAL" ? localStorage : sessionStorage).setItem(name, language ?? "auto"); return true; } catch { return false; } +} diff --git a/apps/web/src/api/expectedResultState.test.ts b/apps/web/src/api/expectedResultState.test.ts new file mode 100644 index 0000000..ced4086 --- /dev/null +++ b/apps/web/src/api/expectedResultState.test.ts @@ -0,0 +1,10 @@ +import { expect, it } from "vitest"; +import fixture from "../../../../tests/fixtures/language_expected_results.json"; +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); + } +}); diff --git a/apps/web/src/api/expectedResultState.ts b/apps/web/src/api/expectedResultState.ts new file mode 100644 index 0000000..8262fec --- /dev/null +++ b/apps/web/src/api/expectedResultState.ts @@ -0,0 +1,6 @@ +/** Legacy consumers intentionally retain their different historical whitespace rules. */ +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(); + return normalized === "모름"; +} diff --git a/apps/web/src/api/files.ts b/apps/web/src/api/files.ts index 530b40c..cba841d 100644 --- a/apps/web/src/api/files.ts +++ b/apps/web/src/api/files.ts @@ -1,3 +1,4 @@ +import { message as formatMessage } from "../i18n/formatMessages"; export type StagedFile = { relative_path: string; filename: string; @@ -13,7 +14,7 @@ export async function stageFile(file: File, projectId: string): Promise null)) as { detail?: string } | null; - throw new Error(detail?.detail ?? `파일 준비 실패 (${response.status})`); + throw new Error(detail?.detail ?? formatMessage("filePrepareFailed", response.status)); } return (await response.json()) as StagedFile; } diff --git a/apps/web/src/api/history.ts b/apps/web/src/api/history.ts index bae2353..27fa3dd 100644 --- a/apps/web/src/api/history.ts +++ b/apps/web/src/api/history.ts @@ -1,3 +1,5 @@ +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"; @@ -11,11 +13,11 @@ export function wireRecord(record: RecordReference) { } 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 Error("이력 조회가 완료되지 않았습니다."); + 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 Error("기록의 프로젝트 연결을 확인하지 못했습니다."); + 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 }; @@ -59,7 +61,7 @@ export async function readHistoryEntry(selection: HistorySelection, signal?: Abo 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 Error("과거 답변의 요청·결과 연결을 확인하지 못했습니다."); + throw new UiError("api.historyResult"); } const readable = value.availability !== "UNAVAILABLE"; // Comparing a result uses its observed immutable record identity, never a guessed ID. @@ -69,7 +71,7 @@ export async function readHistoryEntry(selection: HistorySelection, signal?: Abo try { comparisonEntry = await readHistoryEntry({ kind: "record", scope, record: selection.record }, signal); } catch (error) { if (signal?.aborted) throw error; } } - return { selection, title: selection.title ?? "당시 질문과 답변", occurredAt: selection.occurredAt ?? (typeof value.request.created_at === "string" ? value.request.created_at : null), + 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, @@ -83,7 +85,7 @@ export async function readHistoryEntry(selection: HistorySelection, signal?: Abo 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) { - throw new Error("선택한 기록의 연결을 확인하지 못했습니다."); + throw new UiError("api.historySelection"); } const { item } = value; const head = value.current_head_digest ?? null; diff --git a/apps/web/src/api/historyComparison.ts b/apps/web/src/api/historyComparison.ts index 364b781..a29a3ea 100644 --- a/apps/web/src/api/historyComparison.ts +++ b/apps/web/src/api/historyComparison.ts @@ -1,3 +1,5 @@ +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"; @@ -11,17 +13,17 @@ export function mapComparison(changes: WireChange[], groups: WireChangeGroup[], 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 ?? "내용 변경" }; + 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); - if (response.state !== "SUCCEEDED") throw new Error("비교가 완료되지 않았습니다."); + 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) { - throw new Error("비교 대상의 버전 연결을 확인하지 못했습니다."); + throw new UiError("comparison.binding"); } return mapComparison(value.diff, value.summary_groups, value.coverage.association === "EXACT" ? "COMPLETE" : "PARTIAL", value.coverage.reasons); } diff --git a/apps/web/src/api/localWorkspacePersistence.ts b/apps/web/src/api/localWorkspacePersistence.ts index d0b90c8..8231f86 100644 --- a/apps/web/src/api/localWorkspacePersistence.ts +++ b/apps/web/src/api/localWorkspacePersistence.ts @@ -1,13 +1,15 @@ /** Browser convenience only. Research records and permissions remain server-owned. */ -export type BrowserScope = { mode: "LOCAL"; workspaceId: string | null } | { mode: "HOSTED" }; +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 DraftWriteResult = "SAVED" | "TOO_LARGE" | "UNAVAILABLE"; 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 }; -export type PendingSubmission = { version: 1; workspaceId: string; projectId: string; threadId: string; +export type ResponseLanguage = "ko" | "en"; +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:"; @@ -33,7 +35,8 @@ 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): string { +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]] : [])]); } @@ -100,15 +103,15 @@ export function writeLocalDraft(workspaceId: string | null, projectId: string, t } catch { return "UNAVAILABLE"; } } -export function readLocalPending(workspaceId: string | null, projectId: string, threadId: string): PendingRead { +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 = localStorage.getItem(pendingKey(workspaceId, projectId, threadId)); + 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 || - !Object.keys(value).every(key => ["version", "workspaceId", "projectId", "threadId", "problem", "selection", "origin", "signature", "key", "createdAt"].includes(key)) || + 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 || @@ -118,8 +121,9 @@ export function readLocalPending(workspaceId: string | null, projectId: string, !("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) || !["ko", "en"].includes(String(value.responseLanguage)))) return { kind: "INVALID" }; const pending = value as PendingSubmission; - return pending.signature === submissionSignature(projectId, threadId, pending.problem, pending.selection, pending.origin ?? null) + 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" }; } } @@ -141,19 +145,20 @@ function validPendingSelection(value: unknown): value is PendingModelSelection { (!("reasoning_effort" in value) || typeof value.reasoning_effort === "string" && value.reasoning_effort.length <= 32)); } -export function writeLocalPending(value: PendingSubmission): DraftWriteResult { +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)) return "UNAVAILABLE"; + value.signature !== submissionSignature(value.projectId, value.threadId, value.problem, value.selection, value.origin ?? null, value.responseLanguage)) return "UNAVAILABLE"; + if (value.version === 2 && value.responseLanguage !== "ko" && value.responseLanguage !== "en") return "UNAVAILABLE"; const raw = JSON.stringify(value); if (raw.length > maxPendingLength) return "TOO_LARGE"; - try { localStorage.setItem(pendingKey(value.workspaceId, value.projectId, value.threadId), raw); return "SAVED"; } + 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): boolean { +export function clearLocalPending(workspaceId: string | null, projectId: string, threadId: string, storage?: Storage): boolean { if (!validWorkspaceId(workspaceId) || !validRecordId(projectId) || !validRecordId(threadId)) return false; - try { localStorage.removeItem(pendingKey(workspaceId, projectId, threadId)); return true; } + try { (storage ?? localStorage).removeItem(pendingKey(workspaceId, projectId, threadId)); return true; } catch { return false; } } diff --git a/apps/web/src/api/publicWeb.ts b/apps/web/src/api/publicWeb.ts index d5662ea..d450a19 100644 --- a/apps/web/src/api/publicWeb.ts +++ b/apps/web/src/api/publicWeb.ts @@ -1,3 +1,4 @@ +import { t } from "../i18n/locale"; import { z } from "zod"; export const publicWebExecutionSchema = z.object({ @@ -50,11 +51,11 @@ export function parsePublicWebUpdate(input: unknown) { } export function executionLabel(state: string, reasons: string[] = []): string { - if (state === "READY") return "실행: 준비됨"; - if (state === "OFF") return "실행: 꺼짐"; - if (reasons.includes("PUBLIC_WEB_POLICY_INCONSISTENT")) return "실행: 권한 불일치"; - if (reasons.includes("PUBLIC_WEB_GRANT_MISMATCH")) return "실행: grant 불일치"; - if (reasons.includes("PUBLIC_WEB_CONNECTOR_UNAVAILABLE")) return "실행: 구현 없음"; - if (reasons.includes("PUBLIC_WEB_CONSENT_REQUIRED")) return "실행: 워크스페이스 미허용"; - return "실행: 제한됨"; + if (state === "READY") return t("web.execution.ready"); + if (state === "OFF") return t("web.execution.off"); + if (reasons.includes("PUBLIC_WEB_POLICY_INCONSISTENT")) return t("web.execution.policy"); + if (reasons.includes("PUBLIC_WEB_GRANT_MISMATCH")) return t("web.execution.grant"); + if (reasons.includes("PUBLIC_WEB_CONNECTOR_UNAVAILABLE")) return t("web.execution.unavailable"); + if (reasons.includes("PUBLIC_WEB_CONSENT_REQUIRED")) return t("web.execution.consent"); + return t("web.execution.blocked"); } diff --git a/apps/web/src/api/recordViews.ts b/apps/web/src/api/recordViews.ts index 346225d..ecb41b5 100644 --- a/apps/web/src/api/recordViews.ts +++ b/apps/web/src/api/recordViews.ts @@ -1,28 +1,29 @@ +import { t } from "../i18n/locale"; 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",label:"프로젝트",description:"기준시점 · 정책 결속 · 생명주기",method:"project/read"}, - {namespace:"thread",label:"작업",description:"입력 반영 · 조사 진행 · 현재 결과",method:"thread/read",scope:"thread"}, - {namespace:"investigation",label:"탐색",description:"탐색 범위 · 수집 단계 · 중단 사유",method:"investigation/list"}, - {namespace:"evidence",label:"근거",description:"원문 위치 · 지지 · 권위 · 시점",method:"evidence/list"}, - {namespace:"object",label:"판단 객체",description:"질문에 결속된 판단과 주의 항목",method:"object/list"}, - {namespace:"criteria",label:"평가기준",description:"공식 기준과 참고 후보의 분리",method:"criteria/list"}, - {namespace:"hypothesis",label:"가설",description:"경쟁 설명 · 반증 · 예측 · 평가",method:"hypothesis/list"}, - {namespace:"action",label:"행동",description:"대안 · 계획 · 위험 · 실행 권한",method:"action/list"}, - {namespace:"execution",label:"실행 · 복구",description:"시도 · 효과 · 재조정 · 취소 확인",method:"execution/list"}, - {namespace:"outcome",label:"관찰 결과",description:"시험 유효성 · 관찰 · 해석의 한계",method:"outcome/list"}, - {namespace:"revision",label:"변경 · 복원",description:"불변 revision · 분기 · 작업 head",method:"revision/list"}, - {namespace:"memory",label:"프로젝트 기억",description:"저장 · recall · 행동 자격의 분리",method:"memory/list"}, - {namespace:"improvement",label:"개선",description:"후보 · 평가 · 노출 · 승격 · rollback",method:"improvement/list"}, - {namespace:"receipt",label:"영수증",description:"무결성 · 출처 · 주장 범위 · lineage",method:"receipt/list"}, - {namespace:"closure",label:"마감",description:"준비도 · 처분 · 열린 항목 · 보존",method:"closure/list"}, - {namespace:"export",label:"내보내기",description:"목적 · audience · 고정 snapshot · 공개 경계",method:"export/list"}, - {namespace:"operation",label:"비동기 제어",description:"실행 상태 · checkpoint · 복구 정보",method:"operation/read",scope:"operation"}, - {namespace:"model",label:"모델 · 설정",description:"기본값 · 추론강도 · 실제 소비 snapshot",method:"model/settings/read"}, - {namespace:"trace",label:"추적표",description:"요구사항 · 기준 · 결과와 규칙으로 계산한 판정",method:"trace/read"}, - {namespace:"workspace",label:"워크스페이스 설정",description:"모델 연결 · 인터넷 동의 · 첫 실행 준비",method:"workspace/setup/read"}, - {namespace:"projectpack",label:"검증 예제",description:"등록된 ProjectPack · scripted/live 구분",method:"projectpack/list",note:"예제 조회는 실행하지 않습니다. 실제 연구 기본 흐름과 별도입니다."}, - {namespace:"field",label:"현장 평가",description:"프로토콜 · 참여 세션 · 점수 · privacy-safe export",method:null,note:"현재 공개 field namespace는 COMMAND만 제공합니다. 비변경 조회 API는 없으며 이 UI는 실제 참가자 평가나 시간절감·WTP를 입증하지 않습니다. 승인된 프로토콜·참여 동의가 있는 경우에만 고급 명령을 사용하세요."}, + {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 new file mode 100644 index 0000000..575f052 --- /dev/null +++ b/apps/web/src/api/requestLanguage.test.ts @@ -0,0 +1,31 @@ +// @vitest-environment jsdom +import { afterEach, expect, it } from "vitest"; +import { readLocalPending, submissionSignature, writeLocalPending, type PendingSubmission } from "./localWorkspacePersistence"; +import { readDraftLanguage, writeDraftLanguage } from "./draftLanguage"; +import { setLocale } from "../i18n/locale"; + +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 }; + +it("preserves v1 signatures and keeps explicit ko distinct from omission", () => { + const legacy = submissionSignature("p", "t", "Question", null); + expect(legacy).toBe('["p","t","Question",null]'); + expect(submissionSignature("p", "t", "Question", null, null, "ko")).not.toBe(legacy); + const saved: PendingSubmission = { ...fields, version: 1, signature: legacy }; + expect(writeLocalPending(saved)).toBe("SAVED"); + setLocale("en"); + expect(readLocalPending(scope.workspaceId, "p", "t")).toEqual({ kind: "PENDING", value: saved }); +}); + +it("replays v2 with its saved language regardless of display and next-draft changes", () => { + const saved: PendingSubmission = { ...fields, version: 2, responseLanguage: "en", + signature: submissionSignature("p", "t", "Question", null, null, "en") }; + expect(writeLocalPending(saved)).toBe("SAVED"); + writeDraftLanguage(scope, "p", "t", "ko"); + setLocale("en"); + expect(readDraftLanguage(scope, "p", "t")).toBe("ko"); + expect(readLocalPending(scope.workspaceId, "p", "t")).toEqual({ kind: "PENDING", value: saved }); + expect(readDraftLanguage(scope, "p", "other")).toBeUndefined(); +}); diff --git a/apps/web/src/api/research.ts b/apps/web/src/api/research.ts index 045f7ee..e4d3f96 100644 --- a/apps/web/src/api/research.ts +++ b/apps/web/src/api/research.ts @@ -143,7 +143,7 @@ export type ResearchStatus = WorkThread & { user_progress_summary?: UserProgressSummary | null; coverage_matrix?: CoverageMatrix | null; next_user_action?: NextUserAction | null; - request?: { operation_id: string; authored_text?: string; effective_question?: string; model_settings?: unknown; policy_digest?: string; cutoff_at?: string }; + request?: { response_language?: "ko" | "en"; 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[]; @@ -159,6 +159,9 @@ export type ResearchStatus = WorkThread & { 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 WorkspaceReady = { + browser_scope_id?: string; + response_languages?: ("ko" | "en")[]; + response_language_scope?: "REQUEST"; ready: boolean; model_connected?: boolean; deployment_mode?: string; @@ -202,13 +205,13 @@ const contextKey = "thoth:web-context:v1"; export function readWorkspaceContext(scope?: BrowserScope): { projectId: string; threadId: string } { if (scope?.mode === "LOCAL") return readLocalContext(scope.workspaceId); try { - const value = JSON.parse(sessionStorage.getItem(contextKey) ?? "null"); + 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(contextKey, JSON.stringify({ version: 1, projectId, threadId })); return true; } + 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 b166a76..3b67573 100644 --- a/apps/web/src/api/researchFollowup.ts +++ b/apps/web/src/api/researchFollowup.ts @@ -1,3 +1,4 @@ +import { UiError } from "../i18n/UiError"; import { z } from "zod"; import { rpc } from "./rpcClient"; import { digestSchema } from "./historySchemas"; @@ -147,7 +148,7 @@ export type ProjectReviewItem = z.infer; 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 Error("검토 조회가 완료되지 않았습니다."); + if (response.state !== "SUCCEEDED") throw new UiError("followup.queryIncomplete"); return response.value; } @@ -158,7 +159,7 @@ export async function readDecisionDelta(projectId: string, threadId: string, bef 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 Error("비교 대상 답변의 연결을 확인하지 못했습니다."); + throw new UiError("followup.binding"); } return value; } @@ -167,6 +168,6 @@ export async function readProjectReviewList(projectId: string, cursor: string | 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 Error("검토 목록의 프로젝트 연결을 확인하지 못했습니다."); + if (value.project_id !== projectId) throw new UiError("followup.projectBinding"); return value; } diff --git a/apps/web/src/api/researchProgress.ts b/apps/web/src/api/researchProgress.ts index 8191534..7cc865f 100644 --- a/apps/web/src/api/researchProgress.ts +++ b/apps/web/src/api/researchProgress.ts @@ -1,3 +1,6 @@ +import { t } from "../i18n/locale"; +import { message as formatMessage } from "../i18n/formatMessages"; +import { getLocale } from "../i18n/locale"; import type { ResearchStatus } from "./research"; import { sourceCardTitle } from "../sourceDisplay"; import { @@ -91,16 +94,16 @@ function extractConstraints(status?: ResearchStatus): string[] { found.push(table[0].replace(/\s+/g, " ").trim()); } if (/표 숫자만/u.test(text)) { - found.push("표 숫자만 인용"); + found.push(t("progress.tableOnly")); } if (/추측 금지/u.test(text)) { - found.push("추측 금지"); + found.push(t("progress.noGuess")); } if (/원문 위치/u.test(text)) { - found.push("원문 위치와 함께 답"); + found.push(t("progress.withLocator")); } if (/확인 안 되면|부족한지|HOLD/u.test(text)) { - found.push("확인 안 되면 부족 이유"); + found.push(t("progress.explainMissing")); } const quotes = [...text.matchAll(/[“「"]([^”」"]{2,48})[”」"]/gu)].map((match) => match[1].trim()); return unique([...found, ...quotes]); @@ -203,9 +206,9 @@ function modelLine(status?: ResearchStatus): string | null { return null; } if (model && effort) { - return `${model} · 추론 ${effort}`; + return formatMessage("modelReasoning", model, effort); } - return model ?? `추론 ${effort}`; + return model ?? (effort ? formatMessage("reasoning", effort) : null); } function actionEvent( @@ -287,7 +290,7 @@ function timelineFromPublished(status: ResearchStatus, sourceUris: string[], fac formatDuration(numberOrNull(payload.elapsed_ms)), formatBytes(numberOrNull(payload.context_bytes)), typeof payload.dispatch_count === "number" && payload.dispatch_count > 0 - ? `모델 ${payload.dispatch_count}회` + ? formatMessage("modelCalls", payload.dispatch_count) : null, ]), live, @@ -330,8 +333,8 @@ function timelineFromPublished(status: ResearchStatus, sourceUris: string[], fac `event-action-${event.seq ?? index}`, "model_call", waiting - ? compactPayload([elapsed ?? "경과 미확인", firstByte ? `첫 글자 ${firstByte}` : "첫 글자 아직 없음"]) - : compactPayload([received != null ? `${received.toLocaleString()}B` : null, elapsed]), + ? compactPayload([elapsed ?? t("progress.elapsedUnknown"), firstByte ? formatMessage("firstByte", firstByte) : t("progress.noFirstByte")]) + : compactPayload([received != null ? `${received.toLocaleString(getLocale() === "en" ? "en-US" : "ko-KR")}B` : null, elapsed]), waiting, ), ); @@ -429,13 +432,13 @@ export function liveResearchProgress( export function composerActivity(status: ResearchStatus): string | null { if (status.execution_state === "PAUSE_PENDING") { - return "현재 단계를 마치고 멈추는 중입니다."; + return t("progress.pausing"); } if (status.execution_state === "PAUSED") { - return "작업이 일시정지되어 있습니다."; + return t("progress.paused"); } if (status.attempt?.remote_observation === "UNKNOWN" && status.attempt.external_effect_state === "DISPATCHING") { - return "외부 실행의 종료 여부를 아직 확인하지 못했습니다."; + return t("progress.externalUnknown"); } if (status.operation_state !== "RUNNING") { return null; diff --git a/apps/web/src/api/researchProgressText.ts b/apps/web/src/api/researchProgressText.ts index 2ff261a..194e19a 100644 --- a/apps/web/src/api/researchProgressText.ts +++ b/apps/web/src/api/researchProgressText.ts @@ -1,441 +1,18 @@ -import type { CompletedStage, ResearchStatus } from "./research"; -import { eulreul, eunneun, gwawa, iga } from "./koreanParticles"; - -/** Korean wording of the research progress: particles, durations, sizes, scene sentences and one-line texts. It reads the status it is given and builds no timeline. */ - -export type ProgressScene = { - title: string; - detail: string; -}; - -export type TimelineIcon = "tick-circle" | "document-open" | "cloud" | "search" | "form"; - -export type WorkFacts = { - focus: string | null; - constraints: string[]; - locators: string[]; - hypotheses: string[]; - pendingHypotheses: string[]; - decidedHypotheses: string[]; - sources: string[]; -}; - -export const ACTION_COPY: Record = { - stage_completed: { label: "단계 완료", icon: "tick-circle" }, - source_read: { label: "원문 위치", icon: "search" }, - source_attached: { label: "연결 자료", icon: "document-open" }, - model_call: { label: "모델 호출", icon: "cloud" }, - hypothesis_review: { label: "가설 검토", icon: "form" }, -}; - -export function formatDuration(ms: number | null | undefined): string | null { - if (ms == null || !Number.isFinite(ms) || ms < 0) { - return null; - } - if (ms < 1000) { - return `${Math.round(ms)}ms`; - } - const totalSeconds = Math.round(ms / 1000); - if (totalSeconds < 60) { - return `${totalSeconds}초`; - } - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; - return seconds === 0 ? `${minutes}분` : `${minutes}분 ${seconds}초`; -} - -export function formatBytes(bytes: number | null | undefined): string | null { - if (bytes == null || !Number.isFinite(bytes) || bytes < 0) { - return null; - } - if (bytes < 1024) { - return `맥락 ${bytes.toLocaleString()}B`; - } - const kb = bytes / 1024; - if (kb < 1024) { - return `맥락 ${kb < 10 ? kb.toFixed(1) : Math.round(kb).toString()}KB`; - } - const mb = kb / 1024; - return `맥락 ${mb < 10 ? mb.toFixed(1) : Math.round(mb).toString()}MB`; -} - -export function clip(text: string, max: number): string { - const value = text.replace(/\s+/g, " ").trim(); - if (value.length <= max) { - return value; - } - return `${value.slice(0, max - 1).trim()}…`; -} - -export function unique(items: string[]): string[] { - const seen = new Set(); - const out: string[] = []; - for (const item of items) { - const value = item.replace(/\s+/g, " ").trim(); - if (!value || seen.has(value)) { - continue; - } - seen.add(value); - out.push(value); - } - return out; -} - -function joinKo(items: string[]): string | null { - if (items.length === 0) { - return null; - } - if (items.length === 1) { - return items[0]; - } - return `${items.slice(0, -1).join(", ")}, ${items[items.length - 1]}`; -} - -function quoted(items: string[], max = 3): string | null { - return joinKo(items.slice(0, max).map((item) => `「${clip(item, 28)}」`)); -} - -export function sceneFor(key: string | undefined, facts: WorkFacts, live = false): ProgressScene { - const focus = facts.focus; - const constraints = joinKo(facts.constraints.slice(0, 3)); - const locators = joinKo(facts.locators.slice(0, 3)); - const hyps = quoted(facts.hypotheses, 3); - const decided = quoted(facts.decidedHypotheses, 2); - const sources = joinKo(facts.sources.slice(0, 2)); - const constraintSubject = constraints ?? focus ?? "질문에서 꼭 지킬 조건"; - const locatorSubject = locators ?? (focus ? `${focus} 관련 문장` : null); - const title = (done: string, doing: string) => (live ? doing : done); - switch (key) { - case "RESEARCH_PLANNER": - case "REQUIREMENTS": - return { - title: title(`${eulreul(constraintSubject)} 고정했습니다`, `${eulreul(constraintSubject)} 고정하고 있습니다`), - detail: "아직 답을 쓰지 않았습니다.", - }; - case "EVIDENCE_RERANKER": - return { - title: title( - locatorSubject - ? `${gwawa(locatorSubject)} 관련된 문장을 다시 줄 세웠습니다` - : sources - ? `${sources}에서 ${gwawa(focus ?? "질문")} 관련된 문장을 다시 줄 세웠습니다` - : `${gwawa(focus ?? "질문")} 관련된 문장을 다시 줄 세웠습니다`, - locatorSubject - ? `${gwawa(locatorSubject)} 관련된 문장을 다시 줄 세우고 있습니다` - : `${gwawa(focus ?? "질문")} 관련된 문장을 다시 줄 세우고 있습니다`, - ), - detail: "순위만 매겼고, 지지 여부는 아직 판단하지 않았습니다.", - }; - case "SEMANTIC_REVIEWER": - case "EVIDENCE_REVIEW": { - const condition = facts.constraints.find((item) => /조건/u.test(item)) ?? focus ?? "질문 조건"; - return { - title: title( - locators - ? `${iga(condition)} ${locators} 원문에 있는지 대조했습니다` - : `${eulreul(condition)} 원문과 대조했습니다`, - locators - ? `${iga(condition)} ${locators} 원문에 있는지 대조하고 있습니다` - : `${eulreul(condition)} 원문과 대조하고 있습니다`, - ), - detail: "관련 있다고 해서 지지하는 것은 아닙니다.", - }; - } - case "REVIEW_ADJUDICATOR": - return { - title: title( - `${focus ?? "조건"} 중 빠진 항목과 충돌을 판정했습니다`, - `${focus ?? "조건"} 중 빠진 항목과 충돌을 판정하고 있습니다`, - ), - detail: "애매한 항목은 보류로 남깁니다.", - }; - case "SOURCE_PLANNER": - return { - title: title( - sources ? `${sources} 외에 더 열 주소를 골랐습니다` : "허용된 사이트에서 더 열 주소를 골랐습니다", - sources ? `${sources} 외에 더 열 주소를 고르고 있습니다` : "허용된 사이트에서 더 열 주소를 고르고 있습니다", - ), - detail: "인터넷 전체를 검색하지 않습니다.", - }; - case "HYPOTHESIS_GENERATOR": - case "HYPOTHESES": - return { - title: title( - hyps ? `${eulreul(hyps)} 준비했습니다` : `${focus ?? "질문"}에 대한 가능한 설명을 준비했습니다`, - hyps ? `${eulreul(hyps)} 준비하고 있습니다` : `${focus ?? "질문"}에 대한 가능한 설명을 준비하고 있습니다`, - ), - detail: "아직 채택이 아닙니다.", - }; - case "HYPOTHESIS_REVIEWER": - case "HYPOTHESIS_REVIEW": - return { - title: title( - decided - ? `${eulreul(decided)} 원문과 대조했습니다` - : hyps - ? `${eulreul(hyps)} 원문과 대조했습니다` - : "방금 만든 설명을 원문과 대조했습니다", - decided - ? `${eulreul(decided)} 원문과 대조하고 있습니다` - : hyps - ? `${eulreul(hyps)} 원문과 대조하고 있습니다` - : "방금 만든 설명을 원문과 대조하고 있습니다", - ), - detail: "제안된 설명마다 결정이 있어야 검토가 끝납니다.", - }; - case "ACTION_PLANNER": - case "ACTION_COMPARISON": - return { - title: title( - focus ? `${eulreul(focus)} 확인하려면 다음에 할 작업을 골랐습니다` : "다음에 할 확인 작업을 골랐습니다", - focus ? `${eulreul(focus)} 확인하려면 다음에 할 작업을 고르고 있습니다` : "다음에 할 확인 작업을 고르고 있습니다", - ), - detail: "실행 전 초안입니다.", - }; - case "EVIDENCE_EXTRACTOR": - case "EVIDENCE_FOCUS": - return { - title: title( - locators ? `${locators} 구절을 골랐습니다` : `${focus ?? "표와 원문"} 위치를 확인했습니다`, - locators ? `${locators} 위치를 확인하고 있습니다` : `${focus ?? "표와 원문"} 위치를 확인하고 있습니다`, - ), - detail: "실제로 집어넣을 칸과 문장을 찾습니다.", - }; - case "SUFFICIENCY_EXPLAINER": - return { - title: title( - `${eulreul(focus ?? "질문")} 지금 근거로 답할 수 있는지 설명했습니다`, - `${eulreul(focus ?? "질문")} 지금 근거로 답할 수 있는지 설명하고 있습니다`, - ), - detail: "부족한 항목은 보류 이유로 남깁니다.", - }; - case "COUNTEREVIDENCE_CHALLENGER": - return { - title: title( - hyps ? `${eulreul(hyps)} 뒤집을 근거를 찾았습니다` : `${eulreul(focus ?? "질문")} 뒤집을 근거를 찾았습니다`, - hyps ? `${eulreul(hyps)} 뒤집을 근거를 찾고 있습니다` : `${eulreul(focus ?? "질문")} 뒤집을 근거를 찾고 있습니다`, - ), - detail: "지지 문장만 보지 않습니다.", - }; - case "USER_EXPLAINER": - return { - title: title( - `${eulreul(focus ?? "현재 판단")} 사람이 읽게 정리했습니다`, - `${eulreul(focus ?? "현재 판단")} 사람이 읽게 정리하고 있습니다`, - ), - detail: "내부 코드명이 아니라 질문 기준으로 설명합니다.", - }; - case "REFERENCE_MAPPER": - return { - title: title( - locators ? `${locators} 인용을 원문 위치에 맞춰 두었습니다` : "인용과 원문 위치를 맞춰 두었습니다", - locators ? `${locators} 인용을 원문 위치에 맞추고 있습니다` : "인용과 원문 위치를 맞추고 있습니다", - ), - detail: "표 번호와 셀 위치를 자료에 연결합니다.", - }; - case "CONNECTED_SOURCES": - return { - title: title( - sources ? `${sources}만 열고 있습니다` : "이 프로젝트에 허용된 자료만 열고 있습니다", - sources ? `${sources}만 열고 있습니다` : "이 프로젝트에 허용된 자료만 열고 있습니다", - ), - detail: "웹 검색을 시작했다는 뜻이 아닙니다.", - }; - case "SOURCE_SHORTLIST": - return { - title: title( - sources - ? `${sources}에서 질문과 맞는 원문 구간을 골랐습니다` - : "연결 자료에서 질문과 맞는 원문 구간을 골랐습니다", - sources - ? `${sources}에서 질문과 맞는 원문 구간을 고르고 있습니다` - : "연결 자료에서 질문과 맞는 원문 구간을 고르고 있습니다", - ), - detail: "시간 미확인 자료는 확정 근거가 아니라 후보입니다.", - }; - case "HOLD": - return { - title: live - ? "모델 응답을 기다리다 멈췄습니다" - : "모델 호출이 끝나 연구를 멈췄습니다", - detail: "연결 자료는 그대로 있고, 답을 확정하지 않았습니다.", - }; - default: - return { - title: live - ? `${eulreul(focus ?? "요청")} 받아 자료와 조건을 확인하고 있습니다` - : `${focus ?? "요청"} 관련 단계를 마쳤습니다`, - detail: "서버가 준 단계 이름만으로는 더 구체적인 작업을 특정하지 못했습니다.", - }; - } -} - -export function waitLine(status?: ResearchStatus): string | null { - const dispatches = status?.model_dispatches ?? []; - if (dispatches.length === 0) { - return null; - } - const active = - [...dispatches].reverse().find((item) => item.state === "RESERVED") ?? - dispatches[dispatches.length - 1]; - if (!active) { - return null; - } - const observation = active.transport_observation; - const elapsed = formatDuration(observation?.elapsed_ms ?? null); - const firstByte = formatDuration(observation?.first_byte_ms ?? observation?.first_response_ms ?? null); - const received = observation?.received_bytes ?? active.received_bytes; - if (active.state === "RESERVED") { - if (firstByte) { - return `모델 응답 대기 · ${elapsed ?? "경과 미확인"} · 첫 글자 ${firstByte}`; - } - return `모델 응답 대기 · ${elapsed ?? "경과 미확인"} · 첫 글자 아직 없음`; - } - if (typeof received === "number") { - return `마지막 모델 응답 ${received.toLocaleString()}B${elapsed ? ` · ${elapsed}` : ""}`; - } - return elapsed ? `마지막 모델 호출 · ${elapsed}` : null; -} - -export function usageLine(status?: ResearchStatus): string | null { - const usage = status?.usage; - if (!usage) { - return null; - } - const total = usage.total_tokens; - const tokens = total == null ? "미확인" : total.toLocaleString(); - const partial = usage.state === "PARTIAL" ? " (부분 관측)" : ""; - const cached = - usage.cached_input_tokens != null ? ` · 캐시 입력 ${usage.cached_input_tokens.toLocaleString()}` : ""; - return `사용 토큰 ${tokens}${partial}${cached}`; -} - -export function missingFor( - status: ResearchStatus, - facts: WorkFacts, - labels: string[], - total: number | null, - hypotheses: number | null, - reviews: number | null, -): string | null { - const phase = status.attempt?.phase ?? ""; - if (phase === "EVIDENCE_FOCUS" && labels.length === 0 && (total === 0 || total == null)) { - return `${facts.focus ?? "질문"}에 집어넣을 표·문장이 아직 0곳입니다. 붙인 로컬 원문이 없어 웹 검색은 이 단계에서 시작하지 않습니다.`; - } - if (phase === "EVIDENCE_FOCUS" && labels.length > 0) { - const found = joinKo(labels.slice(0, 3)); - return `${found} ${labels.length}곳을 확인했습니다${total != null && total > labels.length ? ` · 전체 ${total}곳 중 일부` : ""}.`; - } - if ((phase === "HYPOTHESIS_REVIEW" || phase === "HYPOTHESES") && hypotheses != null) { - const pending = quoted(facts.pendingHypotheses, 2); - const decided = quoted(facts.decidedHypotheses, 2); - if (reviews == null || reviews === 0) { - return pending - ? `${pending}부터 대조하겠습니다. 빠진 ${hypotheses}개 결정을 채우기 전입니다.` - : `가설 ${hypotheses}개 · 검토 결정은 아직 없습니다.`; - } - if (reviews < hypotheses) { - const rest = hypotheses - reviews; - if (pending && decided) { - return `${eunneun(decided)} 봤고, ${pending} ${rest}개는 아직입니다. 빠진 결정을 채우겠습니다.`; - } - return pending - ? `${pending} ${rest}개가 빠졌습니다. 빠진 결정을 채우겠습니다.` - : `가설 ${hypotheses}개 중 ${reviews}개만 결정됐습니다. 빠진 ${rest}개를 채우겠습니다.`; - } - return decided ? `${decided}까지 대조했습니다.` : `가설 ${hypotheses}개 · 검토 ${reviews}개`; - } - if (phase === "CONNECTED_SOURCES") { - return facts.sources.length > 0 - ? `${joinKo(facts.sources.slice(0, 2))} 목록을 여는 중이며, 답을 쓰기 전입니다.` - : "연결 자료 목록을 여는 중이며, 답을 쓰기 전입니다."; - } - if (phase === "SOURCE_SHORTLIST") { - return facts.sources.length > 0 - ? `${joinKo(facts.sources.slice(0, 2))}에서 원문 구간을 고르는 중이며, 답을 쓰기 전입니다.` - : "연결 자료에서 원문 구간을 고르는 중이며, 답을 쓰기 전입니다."; - } - if (phase === "HOLD") { - const reason = status.current_result?.terminal_reason ?? ""; - if (reason.includes("429")) { - return "계정 사용 한도로 모델 호출이 거절됐습니다."; - } - if (reason.includes("AUTH_REQUIRED") || reason.includes("403")) { - return "모델 계정 인증이 거절됐습니다."; - } - if (reason.includes("MODEL_NOT_SUPPORTED")) { - return "이 ChatGPT Codex 계정에서 지원하지 않는 모델입니다."; - } - if (reason.includes("400")) { - return "모델이 요청을 거절했습니다."; - } - return "답을 쓰기 전에 모델 호출이 멈췄습니다."; - } - return null; -} - -export function compactPayload(parts: Array): string | null { - const values = parts.filter((item): item is string => Boolean(item && item.trim())); - return values.length > 0 ? values.join(" · ") : null; -} - -export function locatorPayload(page: unknown, text: unknown): string | null { - const pageLabel = page == null ? "" : `p.${page}`; - const excerpt = text == null ? "" : String(text).trim(); - return compactPayload([pageLabel, excerpt]); -} - -export function stageActionPayload(stage: CompletedStage): string | null { - const dispatchCount = Array.isArray(stage.dispatch_ids) ? stage.dispatch_ids.length : null; - return compactPayload([ - formatDuration(stage.elapsed_ms), - formatBytes(stage.context_bytes), - dispatchCount != null && dispatchCount > 0 ? `모델 ${dispatchCount}회` : null, - ]); -} - -export function modelActionPayload(status?: ResearchStatus): { payload: string | null; live: boolean } | null { - const dispatches = status?.model_dispatches ?? []; - if (dispatches.length === 0) { - return null; - } - const active = - [...dispatches].reverse().find((item) => item.state === "RESERVED") ?? - dispatches[dispatches.length - 1]; - if (!active) { - return null; - } - const observation = active.transport_observation; - const elapsed = formatDuration(observation?.elapsed_ms ?? null); - const firstByte = formatDuration(observation?.first_byte_ms ?? observation?.first_response_ms ?? null); - const received = observation?.received_bytes ?? active.received_bytes; - if (active.state === "RESERVED") { - return { - live: true, - payload: compactPayload([ - elapsed ?? "경과 미확인", - firstByte ? `첫 글자 ${firstByte}` : "첫 글자 아직 없음", - ]), - }; - } - return { - live: false, - payload: compactPayload([ - typeof received === "number" ? `${received.toLocaleString()}B` : null, - elapsed, - ]), - }; -} - -export function hypothesisActionPayload(hypotheses: number | null, reviews: number | null): string | null { - if (hypotheses == null) { - return null; - } - if (reviews == null) { - return `가설 ${hypotheses}개 · 결정은 아직 없습니다`; - } - if (reviews < hypotheses) { - return `가설 ${hypotheses}개 중 ${reviews}개만 결정`; - } - return `가설 ${hypotheses}개 · 검토 ${reviews}개`; -} +/** Locale dispatch for display-only progress wording; canonical state and event identity stay upstream. */ +import { getLocale } from "../i18n/locale"; +import * as ko from "../i18n/progressKo"; +import * as en from "../i18n/progressEn"; +export type {ProgressScene, TimelineIcon, WorkFacts} from "../i18n/progressKo"; +export {clip, unique, compactPayload, locatorPayload} from "../i18n/progressKo"; +export const ACTION_COPY: typeof ko.ACTION_COPY = Object.fromEntries(Object.keys(ko.ACTION_COPY).map(key => [key, { + get label() { return (getLocale() === "en" ? en : ko).ACTION_COPY[key].label; }, icon: ko.ACTION_COPY[key].icon, +}])); +export const formatDuration = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).formatDuration(...args); +export const formatBytes = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).formatBytes(...args); +export const sceneFor = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).sceneFor(...args); +export const waitLine = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).waitLine(...args); +export const usageLine = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).usageLine(...args); +export const missingFor = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).missingFor(...args); +export const stageActionPayload = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).stageActionPayload(...args); +export const modelActionPayload = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).modelActionPayload(...args); +export const hypothesisActionPayload = (...args: Parameters): ReturnType => (getLocale() === "en" ? en : ko).hypothesisActionPayload(...args); diff --git a/apps/web/src/api/researchStatusRead.test.ts b/apps/web/src/api/researchStatusRead.test.ts index dd9a243..c1d6aba 100644 --- a/apps/web/src/api/researchStatusRead.test.ts +++ b/apps/web/src/api/researchStatusRead.test.ts @@ -26,6 +26,13 @@ it("reads in full when nothing is running", async () => { expect((result.value as ResearchStatus).current_result).toBeTruthy(); }); +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"]}}); + 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"}); +}); + 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 } }) }); replies.push(status({ operation_state: "RUNNING", usage: { total_tokens: 99 } })); diff --git a/apps/web/src/api/researchStatusRead.ts b/apps/web/src/api/researchStatusRead.ts index b4090dc..4ada020 100644 --- a/apps/web/src/api/researchStatusRead.ts +++ b/apps/web/src/api/researchStatusRead.ts @@ -1,5 +1,5 @@ import type { QueryClient } from "@tanstack/react-query"; -import type { ResearchStatus } from "./research"; +import type { ResearchStatus, WorkspaceReady } from "./research"; import { rpc, type RpcResult } from "./rpcClient"; /** @@ -10,11 +10,13 @@ import { rpc, type RpcResult } from "./rpcClient"; export async function readResearchStatus( client: QueryClient, queryKey: readonly unknown[], projectId: string, threadId: string, signal?: AbortSignal, ): Promise> { + const ready = client.getQueryData>(["workspace-ready"]); + const contract = ready?.value.response_language_scope === "REQUEST" ? { expected_result_contract: "v4" } : {}; const prior = client.getQueryData>(queryKey); if (prior?.value.operation_state === "RUNNING") { - const light = await rpc("thread/read", { project_id: projectId, thread_id: threadId, view: "PROGRESS" }, crypto.randomUUID(), signal); + const light = await rpc("thread/read", { project_id: projectId, thread_id: threadId, ...contract, view: "PROGRESS" }, crypto.randomUUID(), signal); if (light.value.operation_state === "RUNNING") return { ...light, value: { ...prior.value, ...light.value } }; } - return rpc("thread/read", { project_id: projectId, thread_id: threadId }, crypto.randomUUID(), signal); + return rpc("thread/read", { project_id: projectId, thread_id: threadId, ...contract }, crypto.randomUUID(), signal); } diff --git a/apps/web/src/api/restore.ts b/apps/web/src/api/restore.ts index 2f4602b..a2ad75b 100644 --- a/apps/web/src/api/restore.ts +++ b/apps/web/src/api/restore.ts @@ -1,3 +1,5 @@ +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"; @@ -32,14 +34,14 @@ function selectionOf(value: z.infer): RestoreSele targetDigest: value.target_revision_digest, expectedHead: value.expected_current_head }; } function requireSelection(actual: RestoreSelection, expected: RestoreSelection) { - if (restoreKey(actual) !== restoreKey(expected)) throw new Error("복원 대상의 연결을 확인하지 못했습니다."); + 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); - if (response.state !== "SUCCEEDED") throw new Error("복원 미리보기가 완료되지 않았습니다."); + if (response.state !== "SUCCEEDED") throw new UiError("api.restorePreviewIncomplete"); const value = previewSchema.parse(response.value); requireSelection(selectionOf(value.selection), selection); - const states = { stale_refs: "재검토 필요", invalidated_refs: "현재 적용 불가", recalculate_refs: "다시 검토할 항목" }; + 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])], @@ -49,7 +51,7 @@ export async function readRestorePreview(selection: RestoreSelection, signal?: A } export function captureRestoreAttempt(preview: RestorePreview): RestoreAttempt { if (!preview.applyReady || preview.availability !== "AVAILABLE" || !preview.basisDigest || !preview.principalScope) { - throw new Error("현재 복원을 적용할 수 없습니다. 새 미리보기를 확인하세요."); + 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, @@ -60,7 +62,7 @@ function resultOf(value: unknown, attempt: RestoreAttempt, operationId: string): 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)) { - throw new Error("복원 결과의 새 버전과 저장 기록을 확인하지 못했습니다."); + 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 }; @@ -68,15 +70,15 @@ function resultOf(value: unknown, attempt: RestoreAttempt, operationId: string): 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 !== "SUCCEEDED") throw new Error("적용 완료를 확인하지 못했습니다. 원 요청의 결과를 확인하세요."); + 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); - if (value.operation_id !== operationId) throw new Error("원 복원 요청의 연결을 확인하지 못했습니다."); + 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 ?? "복원 요청이 적용되지 않았습니다.", value.error?.code ?? -32000, value.error?.data ?? {}); - if (value.state !== "SUCCEEDED") throw new Error("복원 요청의 상태를 확인하지 못했습니다."); + 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/resultEvidence.ts b/apps/web/src/api/resultEvidence.ts index f1ea7d2..9a9cf3e 100644 --- a/apps/web/src/api/resultEvidence.ts +++ b/apps/web/src/api/resultEvidence.ts @@ -1,3 +1,4 @@ +import { UiError } from "../i18n/UiError"; import { z } from "zod"; import { rpc } from "./rpcClient"; import { digestSchema, historicalResultSchema } from "./historySchemas"; @@ -36,7 +37,7 @@ export function resultContentKey(value: unknown): string { function isShownPartOf(shown: Record, stored: Record): boolean { return Object.entries(shown).every(([key, value]) => Object.hasOwn(stored, key) && resultContentKey(value) === resultContentKey(stored[key])); } -function mismatch(): never { throw new Error("선택한 답변과 근거의 연결을 확인하지 못했습니다. 답변을 다시 확인해 주세요."); } +function mismatch(): never { throw new UiError("evidence.binding"); } function answerContent(value: Record, thread: string, epoch: unknown, terminal: unknown) { // research_threads publication adds these four fields only to operation.result. @@ -53,12 +54,12 @@ async function read(selection: ResultSelection, displayed: Record vi.restoreAllMocks()); +it("opts result reads into v4 only after capability discovery and never changes command input", async () => { + const inputs: {method:string;params:{input:Record}}[] = []; + let supported = false; + vi.stubGlobal("fetch", vi.fn(async (_url, options) => { + const input = JSON.parse(String(options.body)); inputs.push(input); + const value = input.method === "workspace/ready" && supported ? {response_language_scope:"REQUEST"} : {}; + return new Response(JSON.stringify({jsonrpc:"2.0",id:input.id,result:{operation_id:"read",state:"SUCCEEDED",value}}), {status:200}); + })); + try { + await rpc("workspace/ready", {}, "ready-old"); + await rpc("operation/result/read", {project_id:"p",operation_id:"op"}, "read-old"); + expect(inputs.at(-1)!.params.input).not.toHaveProperty("expected_result_contract"); + supported = true; + await rpc("workspace/ready", {}, "ready-new"); + await rpc("operation/result/read", {project_id:"p",operation_id:"op"}, "read-new"); + expect(inputs.at(-1)!.params.input.expected_result_contract).toBe("v4"); + const command = {project_id:"p",thread_id:"t",contract_version:2,instruction:"question",response_language:"en"}; + await rpc("thread/input", command, "new-input"); + expect(inputs.at(-1)!.params.input).toEqual(command); + supported = false; + await rpc("workspace/ready", {}, "ready-old-again"); + await rpc("thread/result/read", {project_id:"p"}, "read-old-again"); + expect(inputs.at(-1)!.params.input).not.toHaveProperty("expected_result_contract"); + } finally { vi.unstubAllGlobals(); } +}); diff --git a/apps/web/src/api/rpcClient.ts b/apps/web/src/api/rpcClient.ts index 108da1a..31eb96c 100644 --- a/apps/web/src/api/rpcClient.ts +++ b/apps/web/src/api/rpcClient.ts @@ -1,24 +1,27 @@ -import { jsonRpcResponseSchema } from "./protocol"; - -export type RpcResult = { - operation_id: string; - state: string; - value: T; -}; -export const readQueries = new Set(["thread/read", "thread/list", "thread/activity/list", "model/settings/read", +import { message as formatMessage } from "../i18n/formatMessages"; +import { UiError } from "../i18n/UiError"; +import { t } from "../i18n/locale"; +import { jsonRpcResponseSchema } from "./protocol"; + +export type RpcResult = { + operation_id: string; + state: string; + value: T; +}; +export const readQueries = new Set(["thread/read", "thread/list", "thread/activity/list", "model/settings/read", "model/credential/list", "model/credential/login/status", "workspace/setup/read", "workspace/ready", - "project/read", "project/list", "project/source/list", "evidence/list", "operation/read", + "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"]); - + "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 class RpcError extends Error { - constructor(message: string, readonly code: number, readonly details: Record) { - super(`${message} [${code}]`); - this.name = "RpcError"; - } + constructor(message: string, readonly code: number, readonly details: Record) { + super(`${message} [${code}]`); + this.name = "RpcError"; + } } export class RpcTransportError extends Error { @@ -40,120 +43,129 @@ async function transportRead(read: () => Promise, caller: AbortSignal | un throw error; } } - -function timeoutSignal(signal: AbortSignal | undefined, timeoutMs: number) { - const controller = new AbortController(); - const timeout = globalThis.setTimeout(() => controller.abort(new DOMException("RPC timed out", "TimeoutError")), timeoutMs); - const abort = () => controller.abort(signal?.reason); - const dispose = () => { - globalThis.clearTimeout(timeout); - signal?.removeEventListener("abort", abort); - }; - controller.signal.addEventListener("abort", dispose, { once: true }); - if (signal?.aborted) abort(); - else signal?.addEventListener("abort", abort, { once: true }); - return { signal: controller.signal, dispose }; -} - -export async function rpc( - method: string, - input: Record, - idempotencyKey: string, - signal?: AbortSignal, - options: { timeoutMs?: number } = {}, -): Promise> { - const bounded = timeoutSignal(signal, options.timeoutMs ?? 15000); - try { + +function timeoutSignal(signal: AbortSignal | undefined, timeoutMs: number) { + const controller = new AbortController(); + const timeout = globalThis.setTimeout(() => controller.abort(new DOMException("RPC timed out", "TimeoutError")), timeoutMs); + const abort = () => controller.abort(signal?.reason); + const dispose = () => { + globalThis.clearTimeout(timeout); + signal?.removeEventListener("abort", abort); + }; + controller.signal.addEventListener("abort", dispose, { once: true }); + if (signal?.aborted) abort(); + else signal?.addEventListener("abort", abort, { once: true }); + return { signal: controller.signal, dispose }; +} + +const typedResultMethods = new Set(["thread/read", "operation/result/read", "thread/result/read"]); +let typedResultsSupported = false; + +export async function rpc( + method: string, + input: Record, + idempotencyKey: string, + signal?: AbortSignal, + options: { timeoutMs?: number } = {}, +): Promise> { + const bounded = timeoutSignal(signal, options.timeoutMs ?? 15000); + const readInput = typedResultsSupported && typedResultMethods.has(method) && !("expected_result_contract" in input) + ? { ...input, expected_result_contract: "v4" } : input; + try { const response = await transportRead(() => fetch(readQueries.has(method) ? "/rpc/query" : "/rpc", { - method: "POST", - signal: bounded.signal, - credentials: "same-origin", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ - jsonrpc: "2.0", - id: crypto.randomUUID(), - method, - params: { _meta: { idempotencyKey }, input }, - }), + method: "POST", + signal: bounded.signal, + credentials: "same-origin", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: crypto.randomUUID(), + method, + params: { _meta: { idempotencyKey }, input: readInput }, + }), }), signal, bounded.signal); - if (!response.ok) { + if (!response.ok) { throw new RpcTransportError(`RPC transport failed (${response.status})`, "HTTP", response.status); - } + } const envelope = jsonRpcResponseSchema.parse(await transportRead(() => response.json(), signal, bounded.signal)); - if (envelope.error) { - throw new RpcError(envelope.error.message, envelope.error.code, envelope.error.data); - } - return envelope.result as RpcResult; - } finally { - bounded.dispose(); - } -} - -type OperationResult = { - operation_id: string; - state: string; - result: T | null; - error: { code?: number; message?: string } | null; -}; - -export async function rpcAsync( - method: string, - input: Record, - idempotencyKey: string, - options: { - onStarted?: (operationId: string) => void; - pollMs?: number; - } = {}, -): Promise> { - const request = { - jsonrpc: "2.0", - id: crypto.randomUUID(), - method, - params: { _meta: { idempotencyKey }, input }, - }; - const startedResponse = await fetch("/rpc/async", { - method: "POST", - credentials: "same-origin", - headers: { "content-type": "application/json" }, - body: JSON.stringify(request), - }); - if (!startedResponse.ok) throw new Error(`비동기 RPC 시작 실패 (${startedResponse.status})`); - const started = jsonRpcResponseSchema.parse(await startedResponse.json()); - if (started.error) throw new Error(`${started.error.message} [${started.error.code}]`); - const operationId = String(started.result?.operation_id ?? ""); - const projectId = String(input.project_id ?? ""); - if (!operationId || !projectId) throw new Error("비동기 operation 식별자가 없습니다."); - options.onStarted?.(operationId); - const interval = options.pollMs ?? 900; - for (;;) { - await new Promise((resolve) => setTimeout(resolve, interval)); - const statusResponse = await fetch( - `/operations/${encodeURIComponent(operationId)}?project_id=${encodeURIComponent(projectId)}`, - { credentials: "same-origin" }, - ); - if (!statusResponse.ok) throw new Error(`operation 조회 실패 (${statusResponse.status})`); - const operation = (await statusResponse.json()) as OperationResult; - if (operation.state === "SUCCEEDED" && operation.result !== null) { - return { operation_id: operationId, state: operation.state, value: operation.result }; - } - if (operation.state === "FAILED") { - throw new Error(operation.error?.message ?? "분석 operation이 실패했습니다."); - } - if (operation.state === "CANCELLED") { - throw new Error("사용자가 분석을 취소했습니다."); - } - } -} - -export async function cancelRpcOperation(projectId: string, operationId: string) { - const response = await fetch(`/operations/${encodeURIComponent(operationId)}/cancel`, { - method: "POST", - credentials: "same-origin", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ project_id: projectId }), - }); - if (!response.ok) throw new Error(`operation 취소 실패 (${response.status})`); - const envelope = jsonRpcResponseSchema.parse(await response.json()); - if (envelope.error) throw new RpcError(envelope.error.message, envelope.error.code, envelope.error.data); - return envelope; -} + if (envelope.error) { + throw new RpcError(envelope.error.message, envelope.error.code, envelope.error.data); + } + if (method === "workspace/ready") { + const value = envelope.result?.value; + typedResultsSupported = Boolean(value && typeof value === "object" && "response_language_scope" in value && value.response_language_scope === "REQUEST"); + } + return envelope.result as RpcResult; + } finally { + bounded.dispose(); + } +} + +type OperationResult = { + operation_id: string; + state: string; + result: T | null; + error: { code?: number; message?: string } | null; +}; + +export async function rpcAsync( + method: string, + input: Record, + idempotencyKey: string, + options: { + onStarted?: (operationId: string) => void; + pollMs?: number; + } = {}, +): Promise> { + const request = { + jsonrpc: "2.0", + id: crypto.randomUUID(), + method, + params: { _meta: { idempotencyKey }, input }, + }; + const startedResponse = await fetch("/rpc/async", { + method: "POST", + credentials: "same-origin", + headers: { "content-type": "application/json" }, + body: JSON.stringify(request), + }); + if (!startedResponse.ok) throw new Error(formatMessage("rpcStartFailed", startedResponse.status)); + const started = jsonRpcResponseSchema.parse(await startedResponse.json()); + if (started.error) throw new Error(`${started.error.message} [${started.error.code}]`); + const operationId = String(started.result?.operation_id ?? ""); + const projectId = String(input.project_id ?? ""); + if (!operationId || !projectId) throw new UiError("api.noOperation"); + options.onStarted?.(operationId); + const interval = options.pollMs ?? 900; + for (;;) { + await new Promise((resolve) => setTimeout(resolve, interval)); + const statusResponse = await fetch( + `/operations/${encodeURIComponent(operationId)}?project_id=${encodeURIComponent(projectId)}`, + { credentials: "same-origin" }, + ); + if (!statusResponse.ok) throw new Error(formatMessage("operationReadFailed", statusResponse.status)); + const operation = (await statusResponse.json()) as OperationResult; + if (operation.state === "SUCCEEDED" && operation.result !== null) { + return { operation_id: operationId, state: operation.state, value: operation.result }; + } + if (operation.state === "FAILED") { + throw new Error(operation.error?.message ?? t("api.analysisFailed")); + } + if (operation.state === "CANCELLED") { + throw new UiError("api.analysisCancelled"); + } + } +} + +export async function cancelRpcOperation(projectId: string, operationId: string) { + const response = await fetch(`/operations/${encodeURIComponent(operationId)}/cancel`, { + method: "POST", + credentials: "same-origin", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ project_id: projectId }), + }); + if (!response.ok) throw new Error(formatMessage("operationCancelFailed", response.status)); + const envelope = jsonRpcResponseSchema.parse(await response.json()); + if (envelope.error) throw new RpcError(envelope.error.message, envelope.error.code, envelope.error.data); + return envelope; +} diff --git a/apps/web/src/api/sourcePolicy.ts b/apps/web/src/api/sourcePolicy.ts index 8ef1d6b..644c7f5 100644 --- a/apps/web/src/api/sourcePolicy.ts +++ b/apps/web/src/api/sourcePolicy.ts @@ -1,16 +1,17 @@ +import { t } from "../i18n/locale"; import { z } from "zod"; // Mirrors domain/enums.py and domain/resource_scope.py; source parity is tested. export const authorityStates = ["UNCLASSIFIED","INFORMAL","OFFICIAL","APPROVED","SUPERSEDED","NOT_ADMISSIBLE"] as const; export const cutoffStates = ["ELIGIBLE","AFTER_CUTOFF","UNKNOWN_TIME","PROHIBITED_CONTEXT"] as const; export const authorityOptions = [ - {value:"UNCLASSIFIED",label:"권위 미분류"},{value:"INFORMAL",label:"비공식 참고자료"}, - {value:"OFFICIAL",label:"공식자료"},{value:"APPROVED",label:"승인본"}, - {value:"SUPERSEDED",label:"대체된 자료"},{value:"NOT_ADMISSIBLE",label:"판단 사용 불가"}, + {value:"UNCLASSIFIED",get label() { return t("sources.unclassified"); }},{value:"INFORMAL",get label() { return t("sources.informal"); }}, + {value:"OFFICIAL",get label() { return t("sources.official"); }},{value:"APPROVED",get label() { return t("sources.approved"); }}, + {value:"SUPERSEDED",get label() { return t("sources.superseded"); }},{value:"NOT_ADMISSIBLE",get label() { return t("sources.inadmissible"); }}, ]; export const cutoffOptions = [ - {value:"ELIGIBLE",label:"기준시점 이전"},{value:"UNKNOWN_TIME",label:"시점 미확인"}, - {value:"AFTER_CUTOFF",label:"기준시점 이후"},{value:"PROHIBITED_CONTEXT",label:"사용 금지 맥락"}, + {value:"ELIGIBLE",get label() { return t("sources.beforeCutoff"); }},{value:"UNKNOWN_TIME",get label() { return t("sources.timeUnknown"); }}, + {value:"AFTER_CUTOFF",get label() { return t("sources.afterCutoff"); }},{value:"PROHIBITED_CONTEXT",get label() { return t("sources.prohibited"); }}, ]; export const sourceConnectSchema = z.object({ project_id:z.string().min(1),relative_path:z.string().min(1),media_type:z.string().min(1), diff --git a/apps/web/src/api/sourceTime.ts b/apps/web/src/api/sourceTime.ts index d52bb18..6b09a02 100644 --- a/apps/web/src/api/sourceTime.ts +++ b/apps/web/src/api/sourceTime.ts @@ -1,56 +1,57 @@ -import { z } from "zod"; -import { rpc } from "./rpcClient"; +import { t } from "../i18n/locale"; +import { z } from "zod"; +import { rpc } from "./rpcClient"; import { cutoffStates } from "./sourcePolicy"; import type { QueryClient } from "@tanstack/react-query"; - -export const sourceTimeAssessmentSchema = z.object({ - project_id: z.string(), - artifact_id: z.string(), - source_version_id: z.string(), - byte_sha256: z.string(), - cutoff_at: z.string(), - cutoff_state: z.enum(cutoffStates), - mode: z.enum(["AUTO","USER_CONFIRMATION","ADVANCED_CORRECTION"]), - basis_observation_ids: z.array(z.string()), - reason_code: z.string(), - revision: z.number(), - assessment_digest: z.string(), - assessed_at: z.string(), -}); -export type SourceTimeAssessment = z.infer; - -export const sourceTimeMutationBasisSchema = z.object({ - project_id: z.string(), - artifact_id: z.string(), - source_version_id: z.string(), - byte_sha256: z.string(), - expected_project_revision: z.number(), - expected_cutoff_at: z.string(), - expected_assessment_revision: z.number(), - expected_metadata_digest: z.string(), -}); - -export function cutoffLabel(state: string) { - if (state === "ELIGIBLE") return "연결됨 · 기준시점에 적합"; - if (state === "AFTER_CUTOFF") return "연결됨 · 기준시점보다 이후라 판단에는 사용하지 않음"; - if (state === "UNKNOWN_TIME") return "연결됨 · 인용할 때 시점을 확인"; - if (state === "PROHIBITED_CONTEXT") return "연결됨 · 사용 금지 상태"; - return state; -} - -/** Project-level cutoff shown in the header. Source-level states use cutoffLabel. */ -export function projectCutoffText(cutoffAt?: string | null) { - if (!cutoffAt) return ""; - const date = new Date(cutoffAt); - if (Number.isNaN(date.getTime())) return ""; - const pad = (value: number) => String(value).padStart(2, "0"); - return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; -} - -export async function confirmSourceTime(input: z.infer & {assertion:"ON_OR_BEFORE_CUTOFF"|"AFTER_CUTOFF"}) { - return rpc<{source_time: SourceTimeAssessment}>("project/source/time/confirm", input, crypto.randomUUID()); -} - + +export const sourceTimeAssessmentSchema = z.object({ + project_id: z.string(), + artifact_id: z.string(), + source_version_id: z.string(), + byte_sha256: z.string(), + cutoff_at: z.string(), + cutoff_state: z.enum(cutoffStates), + mode: z.enum(["AUTO","USER_CONFIRMATION","ADVANCED_CORRECTION"]), + basis_observation_ids: z.array(z.string()), + reason_code: z.string(), + revision: z.number(), + assessment_digest: z.string(), + assessed_at: z.string(), +}); +export type SourceTimeAssessment = z.infer; + +export const sourceTimeMutationBasisSchema = z.object({ + project_id: z.string(), + artifact_id: z.string(), + source_version_id: z.string(), + byte_sha256: z.string(), + expected_project_revision: z.number(), + expected_cutoff_at: z.string(), + expected_assessment_revision: z.number(), + expected_metadata_digest: z.string(), +}); + +export function cutoffLabel(state: string) { + if (state === "ELIGIBLE") return t("sourceTime.connectedEligible"); + if (state === "AFTER_CUTOFF") return t("sourceTime.connectedAfter"); + if (state === "UNKNOWN_TIME") return t("sourceTime.connectedUnknown"); + if (state === "PROHIBITED_CONTEXT") return t("sourceTime.connectedProhibited"); + return state; +} + +/** Project-level cutoff shown in the header. Source-level states use cutoffLabel. */ +export function projectCutoffText(cutoffAt?: string | null) { + if (!cutoffAt) return ""; + const date = new Date(cutoffAt); + if (Number.isNaN(date.getTime())) return ""; + const pad = (value: number) => String(value).padStart(2, "0"); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; +} + +export async function confirmSourceTime(input: z.infer & {assertion:"ON_OR_BEFORE_CUTOFF"|"AFTER_CUTOFF"}) { + return rpc<{source_time: SourceTimeAssessment}>("project/source/time/confirm", input, crypto.randomUUID()); +} + export async function correctSourceTime(input: z.infer & {assertion?: "ON_OR_BEFORE_CUTOFF"|"AFTER_CUTOFF"; revert_unknown?: boolean; correction_reason: string}) { return rpc<{source_time: SourceTimeAssessment}>("project/source/time/correct", input, crypto.randomUUID()); } diff --git a/apps/web/src/api/submissionPersistence.test.ts b/apps/web/src/api/submissionPersistence.test.ts new file mode 100644 index 0000000..b5f19e7 --- /dev/null +++ b/apps/web/src/api/submissionPersistence.test.ts @@ -0,0 +1,28 @@ +// @vitest-environment jsdom +import { afterEach, expect, it } from "vitest"; +import { readDraft, writeDraft } from "./conversation"; +import { readDraftLanguage, writeDraftLanguage } from "./draftLanguage"; +import { submissionSignature, type PendingSubmission } from "./localWorkspacePersistence"; +import { clearSubmission, readSubmission, writeSubmission } from "./submissionPersistence"; + +afterEach(() => { localStorage.clear(); sessionStorage.clear(); }); +it("isolates hosted pending payloads and drafts by the server's opaque session scope", () => { + const a = {mode: "HOSTED" as const, browserScopeId: "browser:aaaaaaaa"}; + const b = {mode: "HOSTED" as const, browserScopeId: "browser:bbbbbbbb"}; + const record: PendingSubmission = {version: 2, responseLanguage: "en", workspaceId: a.browserScopeId, + projectId: "p", threadId: "t", problem: "same thread", selection: null, + signature: submissionSignature("p", "t", "same thread", null, null, "en"), + key: "11111111-1111-1111-1111-111111111111", createdAt: 1}; + expect(writeSubmission(a, record)).toBe("SAVED"); + expect(readSubmission(a, "p", "t")).toEqual({kind: "PENDING", value: record}); + expect(readSubmission(b, "p", "t")).toEqual({kind: "NONE"}); + expect(writeSubmission(b, record)).toBe("UNAVAILABLE"); + writeDraft("p", "t", "original draft", a); + writeDraftLanguage(a, "p", "t", "en"); + expect(readDraft("p", "t", a)).toBe("original draft"); + expect(readDraft("p", "t", b)).toBe(""); + expect(readDraftLanguage(b, "p", "t")).toBeUndefined(); + expect(localStorage.length).toBe(0); + expect(clearSubmission(b, "p", "t")).toBe(true); + expect(readSubmission(a, "p", "t")).toEqual({kind: "PENDING", value: record}); +}); diff --git a/apps/web/src/api/submissionPersistence.ts b/apps/web/src/api/submissionPersistence.ts new file mode 100644 index 0000000..b033486 --- /dev/null +++ b/apps/web/src/api/submissionPersistence.ts @@ -0,0 +1,28 @@ +import { clearLocalPending, readLocalPending, validWorkspaceId, writeLocalPending, + type BrowserScope, type DraftWriteResult, type PendingRead, type PendingSubmission } from "./localWorkspacePersistence"; + +export function submissionScopeId(scope: BrowserScope): string | null { + return scope.mode === "LOCAL" ? scope.workspaceId : scope.browserScopeId ?? null; +} +export function hasSubmissionStore(scope: BrowserScope): boolean { + return scope.mode === "LOCAL" || validWorkspaceId(scope.browserScopeId); +} +function hostedStore(): Storage | null { try { return sessionStorage; } catch { return null; } } + +export function readSubmission(scope: BrowserScope, project: string, thread: string): PendingRead { + if (scope.mode === "LOCAL") return readLocalPending(scope.workspaceId, project, thread); + if (!hasSubmissionStore(scope)) return {kind: "NONE"}; + const storage = hostedStore(); + return storage ? readLocalPending(scope.browserScopeId ?? null, project, thread, storage) : {kind: "INVALID"}; +} +export function writeSubmission(scope: BrowserScope, value: PendingSubmission): DraftWriteResult { + if (value.workspaceId !== submissionScopeId(scope)) return "UNAVAILABLE"; + if (scope.mode === "LOCAL") return writeLocalPending(value); + const storage = hostedStore(); + return storage ? writeLocalPending(value, storage) : "UNAVAILABLE"; +} +export function clearSubmission(scope: BrowserScope, project: string, thread: string): boolean { + if (scope.mode === "LOCAL") return clearLocalPending(scope.workspaceId, project, thread); + const storage = hostedStore(); + return storage ? clearLocalPending(scope.browserScopeId ?? null, project, thread, storage) : false; +} diff --git a/apps/web/src/components/ActionCompare.tsx b/apps/web/src/components/ActionCompare.tsx index 1b6b8c2..fad1ff2 100644 --- a/apps/web/src/components/ActionCompare.tsx +++ b/apps/web/src/components/ActionCompare.tsx @@ -1,3 +1,5 @@ +import { message as formatMessage } from "../i18n/formatMessages"; +import { t } from "../i18n/locale"; import { Callout, HTMLTable, Tag } from "@blueprintjs/core"; import { Fragment } from "react"; import { objectList, objectValue, stringValues, textValue } from "../api/presentation"; @@ -13,7 +15,7 @@ import { StatusBadge } from "./StatusBadge"; import { actionFamilyLabel, actionStateLabel, reversibilityLabel } from "./statusLabels"; const legacyEffort = (action: Record) => action.estimated_cost == null && action.estimated_seconds == null ? undefined - : `기록됨 (${String(action.estimated_cost ?? "비용 미확인")} · ${String(action.estimated_seconds ?? "시간 미확인")})`; + : formatMessage("actionRecorded", String(action.estimated_cost ?? t("actions.costUnknown")), String(action.estimated_seconds ?? t("actions.timeUnknown"))); const staleNote = (action: Record, links?: HypothesisLink[]) => staleActionNote([...stringValues(action.hypothesis_ids), ...stringValues(action.hypothesis_refs)], links); @@ -34,28 +36,28 @@ export function ActionCompare({ result, projectId, links, discrimination }: { const extra = [...actions.flatMap(action => stringValues(action.source_refs)), ...evaluations.flatMap(item => stringValues(item.evidence_refs))]; const numbers = numberSources(typeof result.answer === "string" ? result.answer : "", extra); const cite = (refs: string[]) => refs.map(ref => [{numbers.get(ref)}]); - if (actions.length === 0) return

아직 제안된 행동이 없습니다.

; + if (actions.length === 0) return

{t("actions.empty")}

; const againstNote = (action: Record) => standingActionNote([...stringValues(action.hypothesis_ids), ...stringValues(action.hypothesis_refs)], discrimination); const approvalNote = actions.filter(action => action.execution_authority === "HUMAN_REQUIRED_R3").map(againstNote).find(Boolean) ?? null; - const columnLabel = (action: Record, index: number) => `행동 ${index + 1}${actionFamilyLabel(textValue(action.action_family)) ? ` · ${actionFamilyLabel(textValue(action.action_family))}` : ""}`; + const columnLabel = (action: Record, index: number) => formatMessage("actionNumber", index + 1, actionFamilyLabel(textValue(action.action_family))); return <> - {textValue(analysis.decision) &&
-

추천

+ {textValue(analysis.decision) &&
+

{t("actions.recommended")}

{textValue(analysis.decision)}

- {textValue(analysis.uncertainty) &&

불확실성
{textValue(analysis.uncertainty)}

} - {textValue(analysis.sensitivity) &&

이 판단이 바뀌는 조건
{textValue(analysis.sensitivity)}

} - {textValue(analysis.preference_question) &&

확인하고 싶은 점
{textValue(analysis.preference_question)}

} + {textValue(analysis.uncertainty) &&

{t("actions.uncertainty")}
{textValue(analysis.uncertainty)}

} + {textValue(analysis.sensitivity) &&

{t("actions.changeConditions")}
{textValue(analysis.sensitivity)}

} + {textValue(analysis.preference_question) &&

{t("actions.questions")}
{textValue(analysis.preference_question)}

}
} {projectId && textValue(plan.plan_id) && actions.some(action => action.execution_authority === "HUMAN_REQUIRED_R3") && <> {approvalNote && {approvalNote}} } - {criteria.length > 0 &&
-

필수 기준

-
    {criteria.map((criterion, index) =>
  • {criterion.mandatory === true && 필수} {textValue(criterion.name)} + {criteria.length > 0 &&
    +

    {t("actions.criteria")}

    +
      {criteria.map((criterion, index) =>
    • {criterion.mandatory === true && {t("actions.required")}} {textValue(criterion.name)} {textValue(criterion.rationale) && <>
      {textValue(criterion.rationale)}}
    • )}
    {evaluations.length > 0 && - 기준{actions.map((action, index) => {columnLabel(action, index)})} + {t("followup.criterion")}{actions.map((action, index) => {columnLabel(action, index)})} {criteria.map((criterion, index) => {textValue(criterion.name)} {actions.map((action, column) => {evaluations.filter(item => item.action_id === action.action_id && item.criterion_id === criterion.criterion_id) .map((item, k) =>

    {textValue(item.scenario) && {textValue(item.scenario)}: }{textValue(item.assessment)} {cite(stringValues(item.evidence_refs))}

    )})})} @@ -64,7 +66,7 @@ export function ActionCompare({ result, projectId, links, discrimination }: { {orderedActions(actions).map(({ action, heading, key }) => {heading &&

    {heading}

    }

    {textValue(action.specification)}

    - {reversibilityLabel(textValue(action.reversibility)) ?? "되돌림 미확인"} + {reversibilityLabel(textValue(action.reversibility)) ?? t("actions.reversibilityUnknown")} {actionStateLabel(textValue(action.state)) && {actionStateLabel(textValue(action.state))}}
    {staleNote(action, links) && {staleNote(action, links)}} {againstNote(action) && {againstNote(action)}} @@ -72,9 +74,9 @@ export function ActionCompare({ result, projectId, links, discrimination }: { {projectId && textValue(action.action_id) ? : } - {stringValues(action.missing_evidence).map((value, j) =>

    필요한 근거: {value}

    )} - {stringValues(action.source_refs).length > 0 &&

    근거 {cite(stringValues(action.source_refs))}

    } -

    {action.execution_authority === "HUMAN_REQUIRED_R3" ? "보호된 실행 제안 · 대상과 영향에 대한 권한 결정이 필요합니다." : "제안과 실제 실행 결과는 별도로 기록됩니다."}

    + {stringValues(action.missing_evidence).map((value, j) =>

    {t("labels.requiredEvidence")}{" "}{value}

    )} + {stringValues(action.source_refs).length > 0 &&

    {t("history.evidence")}{" "}{cite(stringValues(action.source_refs))}

    } +

    {action.execution_authority === "HUMAN_REQUIRED_R3" ? t("actions.protected") : t("actions.proposalNotice")}

    )} ; } diff --git a/apps/web/src/components/ActionEffort.tsx b/apps/web/src/components/ActionEffort.tsx index 99e8acd..0a161ac 100644 --- a/apps/web/src/components/ActionEffort.tsx +++ b/apps/web/src/components/ActionEffort.tsx @@ -1,3 +1,5 @@ +import { message as formatMessage } from "../i18n/formatMessages"; +import { t } from "../i18n/locale"; import { Button, HTMLSelect, InputGroup } from "@blueprintjs/core"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; @@ -20,19 +22,19 @@ function DimensionLine({ dimension, estimates }: { dimension: string; estimates: const band = (item: Estimate) => effortBandLabel(dimension, item.band); if (ai && human && ai.band !== human.band) { return

    {name}: {estimatorLabel("AI")}: {band(ai)} / {estimatorLabel("HUMAN")}: {band(human)} - {human.basis_text && · {human.basis_text}}{ai.basis_text && · AI 근거: {ai.basis_text}}

    ; + {human.basis_text && · {human.basis_text}}{ai.basis_text && {" "}{t("labels.aiBasis")}{" "}{ai.basis_text}}

    ; } if (ai && human) { - return

    {name}: {band(human)} · AI·사람 추정 일치{human.basis_text && · {human.basis_text}}

    ; + return

    {name}: {band(human)}{" "}{t("labels.estimatesAgree")}{human.basis_text && · {human.basis_text}}

    ; } const only = ai ?? human; - if (!only || only.band === "UNKNOWN") return

    {name}: 미확인 · 확인 필요{only?.basis_text && · {only.basis_text}}

    ; + if (!only || only.band === "UNKNOWN") return

    {name}{t("labels.unknownCheck")}{only?.basis_text && · {only.basis_text}}

    ; return

    {name}: {band(only)} · {estimatorLabel(only.estimator_type)}{only.basis_text && <> · {only.basis_text}}

    ; } /** Time and cost-effort bands for one action. Bands are labels only; nothing here adds, ranks or converts them. */ export function EffortLines({ estimates, legacy }: { estimates: Estimate[]; legacy?: string }) { - if (estimates.length === 0) return

    비용·시간: {legacy ?? "미확인"}

    ; + if (estimates.length === 0) return

    {t("labels.costTime")}{" "}{legacy ?? t("state.unknown")}

    ; return
    {DIMENSIONS.map(dimension => )}
    ; } @@ -57,31 +59,31 @@ export function ActionEffort({ projectId, actionId, initial, legacy }: { project const save = useMutation({ mutationFn: (entries: { dimension: string; band: string; basis_text: string }[]) => rpc("action/revise", { project_id: projectId, action_id: actionId, expected_revision_digest: digest, patch: {}, evidence_refs: [], - reason: "사람 추정 입력", estimator_ref: HUMAN_REF, human_effort_estimates: entries }, crypto.randomUUID()), + get reason() { return t("effort.humanEntry"); }, estimator_ref: HUMAN_REF, human_effort_estimates: entries }, crypto.randomUUID()), onSuccess: async () => { setDraft(emptyDraft()); setOpen(false); setMessage(""); await client.invalidateQueries({ queryKey: key }); }, }); const submit = () => { const entries = DIMENSIONS.filter(dimension => draft[dimension].band).map(dimension => ({ dimension, band: draft[dimension].band, basis_text: draft[dimension].basis.trim() })); - if (entries.length === 0) { setMessage("저장할 추정을 고르세요."); return; } - if (entries.some(entry => entry.band !== "UNKNOWN" && !entry.basis_text)) { setMessage("근거 한 줄을 적어 주세요."); return; } + if (entries.length === 0) { setMessage(t("effort.choose")); return; } + if (entries.some(entry => entry.band !== "UNKNOWN" && !entry.basis_text)) { setMessage(t("effort.basisNeeded")); return; } setMessage(""); save.mutate(entries); }; const set = (dimension: string, part: "band" | "basis", value: string) => setDraft(current => ({ ...current, [dimension]: { ...current[dimension], [part]: value } })); return
    - + {running && {researchRunningNotice}} {open &&
    {DIMENSIONS.map(dimension =>
    - set(dimension, "basis", event.target.value)}/> + set(dimension, "band", event.target.value)} + options={[{ value: "", get label() { return t("effort.none"); } }, ...effortBandOptions(dimension)]}/> + set(dimension, "basis", event.target.value)}/>
    )} - AI 추정은 지워지지 않고 함께 표시됩니다. - + {t("effort.preserveAI")} + {message &&

    {message}

    } - {save.error &&

    추정을 저장하지 못했습니다. {save.error.message}

    } + {save.error &&

    {t("effort.failed")}{" "}{save.error.message}

    }
    }
    ; } diff --git a/apps/web/src/components/AnswerBody.tsx b/apps/web/src/components/AnswerBody.tsx index fff9503..9b48c7c 100644 --- a/apps/web/src/components/AnswerBody.tsx +++ b/apps/web/src/components/AnswerBody.tsx @@ -1,9 +1,11 @@ +import { message } from "../i18n/formatMessages"; import { Fragment, useMemo } from "react"; import { parseAnswer, type Inline } from "./answerText"; /** The answer as paragraphs, lists and bold text, with each source shown as a numbered button. */ -export function AnswerBody({ answer, onCite }: { +export function AnswerBody({ answer, onCite, language }: { answer: string; + language?: "ko" | "en" | null; onCite: (spanId: string, numbers: Record) => void; }) { const parsed = useMemo(() => parseAnswer(answer), [answer]); @@ -11,10 +13,10 @@ export function AnswerBody({ answer, onCite }: { const render = (items: Inline[]) => items.map((item, index) => { const body = item.kind === "text" ? item.text - : ; + : ; return {item.bold ? {body} : body}; }); - return
    {parsed.blocks.map((block, index) => block.kind === "p" + return
    {parsed.blocks.map((block, index) => block.kind === "p" ?

    {render(block.inline)}

    :
      {block.items.map((item, itemIndex) =>
    • {render(item)}
    • )}
    )}
    ; } diff --git a/apps/web/src/components/AnswerHold.tsx b/apps/web/src/components/AnswerHold.tsx index 389f88d..7d13061 100644 --- a/apps/web/src/components/AnswerHold.tsx +++ b/apps/web/src/components/AnswerHold.tsx @@ -1,3 +1,5 @@ +import { message as formatMessage } from "../i18n/formatMessages"; +import { t } from "../i18n/locale"; import type { ReactNode } from "react"; import { Callout } from "@blueprintjs/core"; import type { CoverageMatrix } from "../api/researchFollowup"; @@ -15,13 +17,13 @@ export function AnswerHold({ status, coverage, facts, counter, next }: { const rows = coverage && coverage.availability !== "UNAVAILABLE" ? coverage.rows : []; const supported = rows.filter(row => row.status === "SATISFIED").length; const needsCheck = rows.filter(row => row.status === "UNRESOLVED" || row.status === "NOT_ASSESSED").length; - return -

    결론 상태: {label}

    + return +

    {t("labels.conclusion")}{" "}{label}

    {facts} {rows.length > 0 && } -

    답할 수 있는 범위: {rows.length ? `답변 점검 항목 ${rows.length}개 중 충족 ${supported} · 확인 필요 ${needsCheck}` : "답변 점검 항목 정보를 아직 읽지 못했습니다"}

    +

    {t("labels.answerScope")}{" "}{rows.length ? formatMessage("answerCoverage", rows.length, supported, needsCheck) : t("answerHold.coverageMissing")}

    {counter &&

    {counter}

    } - {next &&

    다음 확보: {next}

    } + {next &&

    {t("labels.nextEvidence")}{" "}{next}

    }
    ; } diff --git a/apps/web/src/components/AnswerReadability.test.tsx b/apps/web/src/components/AnswerReadability.test.tsx index cf548d3..75bd259 100644 --- a/apps/web/src/components/AnswerReadability.test.tsx +++ b/apps/web/src/components/AnswerReadability.test.tsx @@ -54,6 +54,17 @@ describe("the sentences the program writes about missing checks", () => { }); describe("the answer screen", () => { + it("shows new English prose in a Korean interface when the saved request selected English", () => { + const result = {response_language: "en", answer: "The source says ‘온도’ and records 12 ms.", + assessment: {missing_items: ["An independent measurement is still needed."]}}; + const before = JSON.stringify(result); + const text = visible(render(result)); + expect(text).toContain("An independent measurement is still needed."); + expect(text).toContain("온도"); + expect(text).toContain("12 ms"); + expect(JSON.stringify(result)).toBe(before); + }); + it("shows no internal wording outside the folded technical information", () => { const html = render({ answer: "a", answer_status: "PARTIAL_HOLD", assessment: { missing_items: REASONS } }, { progressSummary: progress([REASONS[1]], [REASONS[2]]), diff --git a/apps/web/src/components/ApprovalCard.tsx b/apps/web/src/components/ApprovalCard.tsx index 9193277..02d423e 100644 --- a/apps/web/src/components/ApprovalCard.tsx +++ b/apps/web/src/components/ApprovalCard.tsx @@ -1,3 +1,5 @@ +import { message as formatMessage } from "../i18n/formatMessages"; +import { t } from "../i18n/locale"; import { Button, Callout, Collapse, Tag } from "@blueprintjs/core"; import { useState } from "react"; import { objectList, objectValue, textValue } from "../api/presentation"; @@ -11,9 +13,9 @@ function ChangeView({ change }: { change: ApprovalChange }) { part.op === "del" ? {part.text}{" "} : part.op === "add" ? {part.text}{" "} : part.op === "gap" ? … : {part.text} )}

    } {change.attachments.map((pair, index) =>

    - {pair.beforeName ?? "없음"}{pair.beforeDigest && ({shortDigest(pair.beforeDigest)}…)} - {" → "}{pair.afterName ?? "없음"}{pair.afterDigest && ({shortDigest(pair.afterDigest)}…)}

    )} - {change.words.length === 0 && change.attachments.length === 0 &&

    {change.before || "없음"} → {change.after || "없음"}

    } + {pair.beforeName ?? t("criteriaChange.none")}{pair.beforeDigest && ({shortDigest(pair.beforeDigest)}…)} + {" → "}{pair.afterName ?? t("criteriaChange.none")}{pair.afterDigest && ({shortDigest(pair.afterDigest)}…)}

    )} + {change.words.length === 0 && change.attachments.length === 0 &&

    {change.before || t("criteriaChange.none")} → {change.after || t("criteriaChange.none")}

    } ; } @@ -21,9 +23,9 @@ function FullContent({ payload }: { payload: Record }) { const content = objectValue(payload.content); const attachments = objectList(content.attachments).map(item => textValue(item.name)).filter(Boolean); const rows: [string, string][] = [ - ["행동 종류", textValue(payload.action_kind)], ["전달 경로", textValue(payload.channel)], ["본문", textValue(content.body_text)], - ["첨부", attachments.join(", ")], ["공개되는 정보", listOf(payload.disclosed_data).join(", ")], - ["실행 권한 역할", listOf(payload.execution_roles).join(", ")], ["위험 등급", statusLabel(textValue(payload.risk_tier)) ?? textValue(payload.risk_tier)], + [t("approval.kind"), textValue(payload.action_kind)], [t("approval.route"), textValue(payload.channel)], [t("approval.body"), textValue(content.body_text)], + [t("approval.attachments"), attachments.join(", ")], [t("approval.disclosure"), listOf(payload.disclosed_data).join(", ")], + [t("approval.role"), listOf(payload.execution_roles).join(", ")], [t("approval.risk"), statusLabel(textValue(payload.risk_tier)) ?? textValue(payload.risk_tier)], ]; return
    {rows.filter(([, text]) => text).map(([label, text]) =>
    {label}
    {text}
    )}
    ; } @@ -32,18 +34,18 @@ function FullContent({ payload }: { payload: Record }) { export function ApprovalCard({ approval, onReapprove, onDecline }: { approval: Approval; onReapprove?: () => void; onDecline?: () => void }) { const [open, setOpen] = useState(false); const stale = approval.state === "STALE"; - return
    + return
    {stateLabels[approval.state] ?? approval.state}
    - {stale && change.label).join(" · ") || "내용"}`}> + {stale && change.label).join(" · ") || t("trace.field.content"))}> {approval.changes.map(change => )} -

    이전 승인은 바뀌기 전 버전에 유효했음 (승인 기록은 그대로 남아 있습니다).

    +

    {t("approval.previous")}

    } - {!stale && approval.reviewNeeded && 근거가 바뀌어 다시 살펴보는 것이 좋습니다. 보낼 내용은 그대로이므로 승인은 유지됩니다.} - + {!stale && approval.reviewNeeded && {t("approval.basisChanged")}} + {stale && (onReapprove || onDecline) &&
    - {onReapprove && } - {onDecline && } + {onReapprove && } + {onDecline && }
    }
    ; } diff --git a/apps/web/src/components/ApprovalSection.tsx b/apps/web/src/components/ApprovalSection.tsx index b042d9f..d34709c 100644 --- a/apps/web/src/components/ApprovalSection.tsx +++ b/apps/web/src/components/ApprovalSection.tsx @@ -1,3 +1,4 @@ +import { t } from "../i18n/locale"; import { Callout } from "@blueprintjs/core"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; @@ -41,11 +42,11 @@ export function ApprovalSection({ projectId, planId, onReapprove, onDecline }: { const shown = (query.data?.approvals ?? []).filter(approval => !declined.includes(approval.id)); if (!query.data || query.data.approvals.length === 0) return null; return <> - {reapprove.error && 승인 요청을 다시 만들지 못했습니다. {reapprove.error.message}} - {requested && 승인 요청을 만들었습니다. 승인자가 결정하기 전까지 아무것도 보내지 않습니다.} + {reapprove.error && {t("approval.recreateFailed")}{" "}{reapprove.error.message}} + {requested && {t("approval.created")}} {shown.map(approval => { onReapprove?.(approval); if (!onReapprove) reapprove.mutate(approval); }} onDecline={() => { onDecline?.(approval); setDeclined(current => [...current, approval.id]); }}/>)} - {declined.length > 0 &&

    보내지 않기로 표시한 변경이 있습니다. 이 표시는 이 화면에만 적용되며, 승인 없이는 아무것도 전송되지 않습니다.

    } + {declined.length > 0 &&

    {t("approval.notSentNotice")}

    } ; } diff --git a/apps/web/src/components/ArchivedProjects.tsx b/apps/web/src/components/ArchivedProjects.tsx index 3db7fb7..87e1dd7 100644 --- a/apps/web/src/components/ArchivedProjects.tsx +++ b/apps/web/src/components/ArchivedProjects.tsx @@ -1,3 +1,5 @@ +import { message } from "../i18n/formatMessages"; +import { t, useTranslation } from "../i18n/locale"; import { Button, Callout } from "@blueprintjs/core"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; @@ -6,6 +8,7 @@ import type { ProjectSummary } from "../types"; /** Archived projects are read-only and hidden from the list; this shows them on demand so one can be restored. */ export function ArchivedProjects({ scopeKey }: { scopeKey: string }) { + useTranslation(); const client = useQueryClient(); const [open, setOpen] = useState(false); const archived = useQuery({ queryKey: ["projects", scopeKey, "archived"], enabled: open, @@ -16,17 +19,17 @@ export function ArchivedProjects({ scopeKey }: { scopeKey: string }) { }); const rows = (archived.data?.value.projects ?? []).filter(item => item.lifecycle === "ARCHIVED_READ_ONLY"); return
    - + {open &&
    - {archived.isPending &&

    보관한 프로젝트를 읽는 중…

    } - {archived.error && 보관한 프로젝트를 읽지 못했습니다. 없다는 뜻은 아닙니다.} - {archived.isSuccess && rows.length === 0 &&

    보관한 프로젝트가 없습니다.

    } + {archived.isPending &&

    {t("archive.loading")}

    } + {archived.error && {t("archive.failed")}} + {archived.isSuccess && rows.length === 0 &&

    {t("archive.empty")}

    } {rows.map(item =>
    {item.name} - +
    )} - {restore.error && 되살리지 못했습니다. {restore.error.message}} + {restore.error && {t("archive.restoreFailed")} {restore.error.message}}
    }
    ; } diff --git a/apps/web/src/components/AutoRetrySwitch.test.tsx b/apps/web/src/components/AutoRetrySwitch.test.tsx index adad045..2878d84 100644 --- a/apps/web/src/components/AutoRetrySwitch.test.tsx +++ b/apps/web/src/components/AutoRetrySwitch.test.tsx @@ -3,7 +3,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { act } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, expect, it, vi } from "vitest"; -import { AutoRetrySwitch, autoRetryHelp } from "./AutoRetrySwitch"; +import { AutoRetrySwitch } from "./AutoRetrySwitch"; +import { modelHelpMessages } from "../i18n/modelHelpMessages"; // 사용자 결정 20261010-01: a project without saved settings is on. const state = vi.hoisted(() => ({ enabled: true, digest: null as string | null, calls: [] as { method: string; input: Record }[], fail: false })); @@ -33,7 +34,7 @@ it("starts on as the server says, explains the cost, and sends the digest it rea expect(input.checked).toBe(true); expect(container.textContent).toContain("모델 호출이 중간에 끊기면 그 호출만 최대 2번 자동으로 다시 시도합니다. 토큰이 더 쓰입니다."); expect(container.textContent).toContain("끄면 끊긴 결과 카드의 \"이어서 조사\"로 다시 할 수 있습니다."); - expect(autoRetryHelp).toContain("토큰이 더 쓰입니다"); + expect(modelHelpMessages["modelHelp.retry"].ko).toContain("토큰이 더 쓰입니다"); expect(state.calls).toEqual([{ method: "model/callSettings/read", input: { project_id: "project:p" } }]); await act(async () => input.click()); await settle(); diff --git a/apps/web/src/components/AutoRetrySwitch.tsx b/apps/web/src/components/AutoRetrySwitch.tsx index bee33cb..5bf5fcd 100644 --- a/apps/web/src/components/AutoRetrySwitch.tsx +++ b/apps/web/src/components/AutoRetrySwitch.tsx @@ -1,10 +1,10 @@ +import { t } from "../i18n/locale"; import { Switch } from "@blueprintjs/core"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { rpc } from "../api/rpcClient"; type CallSettings = { auto_retry_interrupted_model_call: boolean; settings_digest: string | null }; -export const autoRetryHelp = "모델 호출이 중간에 끊기면 그 호출만 최대 2번 자동으로 다시 시도합니다. 토큰이 더 쓰입니다. 끄면 끊긴 결과 카드의 \"이어서 조사\"로 다시 할 수 있습니다."; /** The project's switch for sending a cut-off model call again (at most twice). It shows what the server says. */ export function AutoRetrySwitch({ projectId }: { projectId: string }) { @@ -21,12 +21,12 @@ export function AutoRetrySwitch({ projectId }: { projectId: string }) { onSettled: () => client.invalidateQueries({ queryKey: key }), }); if (query.isPending) return null; - if (query.error || !query.data) return

    자동 재시도 설정을 읽지 못했습니다.

    ; + if (query.error || !query.data) return

    {t("modelHelp.retryReadFailed")}

    ; return
    - change.mutate(event.currentTarget.checked)}/> - {autoRetryHelp} - {change.error && 설정을 바꾸지 못했습니다. 화면을 다시 읽었으니 다시 시도해 주세요.} + {t("modelHelp.retry")} + {change.error && {t("modelHelp.changeFailed")}}
    ; } diff --git a/apps/web/src/components/CapabilityWorkbench.tsx b/apps/web/src/components/CapabilityWorkbench.tsx index 87c1eda..01f6c38 100644 --- a/apps/web/src/components/CapabilityWorkbench.tsx +++ b/apps/web/src/components/CapabilityWorkbench.tsx @@ -1,3 +1,5 @@ +import { message as formatMessage } from "../i18n/formatMessages"; +import { t } from "../i18n/locale"; import { Button, Callout, Checkbox, HTMLSelect, InputGroup, Tag, TextArea } from "@blueprintjs/core"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useDeferredValue, useState } from "react"; @@ -13,40 +15,40 @@ function MethodDetail({ method, projectId, threadId }: { method: Capability; pro const [confirmed, setConfirmed] = useState(false); const [requestKey, setRequestKey] = useState(() => crypto.randomUUID()); let validation = ""; - try { parseInvocation(input, projectId); } catch (error) { validation = error instanceof Error ? error.message : "잘못된 입력"; } + try { parseInvocation(input, projectId); } catch (error) { validation = error instanceof Error ? error.message : t("advanced.invalidInput"); } const directRead = transportFor(method.name) === "/rpc/query"; const command = method.surface !== "QUERY" && !directRead; const invocation = useMutation({ mutationFn: (captured: { input: Record; key: string }) => rpc>(method.name, captured.input, captured.key), onSuccess: () => { if (command) void client.invalidateQueries(); }, }); - return
    + return
    {method.surface}{method.canonical ? "canonical" : "compatibility alias"}

    {method.name}

    -
    정본 owner
    {method.canonical_owner}
    정책
    {method.policy}
    -
    정상 진입점
    {method.normal_entrypoint}
    계약 근거
    {method.behavioral_evidence.join(" · ")}
    - {method.alias_target &&
    별칭 대상
    {method.alias_target}
    } -
    전송 경로
    {transportFor(method.name)}
    - - {command ? "정본 변경·모델/외부 호출·비용이 발생할 수 있습니다. 아래 정확한 입력을 확인하세요. 서버의 권한·예산·현재성 검사는 그대로 적용됩니다." : - directRead ? "서버의 읽기 전용 허용 목록을 사용합니다. 조회만으로 operation이나 영수증을 만들지 않습니다." : - "QUERY 계약이지만 서버의 비변경 허용 목록에는 없습니다. 명시 실행 시 /rpc가 operation·journal을 기록할 수 있습니다. 탐색만으로 실행하지 않습니다."} +
    {t("advanced.owner")}
    {method.canonical_owner}
    {t("advanced.policy")}
    {method.policy}
    +
    {t("advanced.entrypoint")}
    {method.normal_entrypoint}
    {t("advanced.contract")}
    {method.behavioral_evidence.join(" · ")}
    + {method.alias_target &&
    {t("advanced.alias")}
    {method.alias_target}
    } +
    {t("advanced.transport")}
    {transportFor(method.name)}
    + + {command ? t("advanced.commandNotice") : + directRead ? t("advanced.queryNotice") : + t("advanced.journalNotice")} - +