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..fbc0ff1 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-12T15:37:44.253458+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": "136dfb168af78bb70ff6a30fd7c6faead02823e8be4d8af1a27bb0ea662ee4f1",
+ "bytes": 42779
},
{
"path": "apps/web/src/components/LiveProjectWorkspaceConsent.test.tsx",
@@ -743,33 +788,33 @@
},
{
"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",
- "sha256": "da9d04a0670973eb67d99da7cf1b4a4c59ce824be5160dd59395ce1489219ce5",
- "bytes": 23343
+ "sha256": "9e44d93aa874f504bc7ed2dc37d4c41767f44aeeb9cd7c0b78a1ee9770e5ea32",
+ "bytes": 23874
},
{
"path": "apps/web/src/components/ModelSettings.tsx",
- "sha256": "077ccdd32ba9f1a6630fbb9feaf2d96d704b7394ecbec74a2914b76380ca6616",
- "bytes": 9721
+ "sha256": "bae7c274c7db338962bb5104c201418a55b78b76236ac5739f01365565329d10",
+ "bytes": 9377
},
{
"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": "2c0e55f727fbae8f1c8a33f192743ce0344af406a59069069b15e19ccf7829e8",
+ "bytes": 5115
},
{
"path": "apps/web/src/components/ResearchActivityStrip.tsx",
- "sha256": "3a2de3bf87c67873990f023d0484729d4a464d5189ab230c4d73e064651ba5d9",
- "bytes": 7551
+ "sha256": "2b3076b927d14b4f81e5b92d18f717777dc189fcd223989f5397beee366397b6",
+ "bytes": 9744
},
{
"path": "apps/web/src/components/ResearchCancelledPresentation.test.tsx",
@@ -838,13 +888,13 @@
},
{
"path": "apps/web/src/components/ResearchComposer.tsx",
- "sha256": "9225b76e4ddf4313cbf17ecf5c0e4f444ab9d1e5f551a39bdb48a7d53563168b",
- "bytes": 6326
+ "sha256": "f5fdc357b4dd0f6ec87a5d1c87e4ab439c810daf78b9765a04abf2f9f876b450",
+ "bytes": 6730
},
{
"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": "acb7cd6b3abbe8b3f34c1c9feb092a2bd0efc49f6b9d0a1c9c4c67c89df99e22",
+ "bytes": 2440
+ },
+ {
+ "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": "b6821bb5d61e9bd899b0a7da61271e8eb15871ed8bf95f7a29710096376c4d94",
+ "bytes": 8864
+ },
+ {
+ "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": "6b5b8bfd96343cc5bdb32b5e09f09be59304923df6e371768e976cc33563af5d",
+ "bytes": 60405
},
{
"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
- setOpen(value => !value)}>사람 추정 입력
+ setOpen(value => !value)}>{t("effort.humanEntry")}
{running && {researchRunningNotice} }
{open &&
{DIMENSIONS.map(dimension =>
{effortDimensionLabel(dimension)}
- set(dimension, "band", event.target.value)}
- options={[{ value: "", label: "입력 안 함" }, ...effortBandOptions(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")}
+
{t("effort.save")}
{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
- : onCite(item.spanId, numbers)}>[{item.n}] ;
+ : onCite(item.spanId, numbers)}>[{item.n}] ;
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 && 근거가 바뀌어 다시 살펴보는 것이 좋습니다. 보낼 내용은 그대로이므로 승인은 유지됩니다. }
- setOpen(value => !value)}>전체 내용 보기
+ {!stale && approval.reviewNeeded && {t("approval.basisChanged")} }
+ setOpen(value => !value)}>{t("approval.full")}
{stale && (onReapprove || onDecline) &&
- {onReapprove && 바뀐 내용으로 승인 요청 다시 만들기 }
- {onDecline && 보내지 않음 }
+ {onReapprove && {t("approval.recreate")} }
+ {onDecline && {t("approval.notSent")} }
}
;
}
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
-
setOpen(value => !value)}>보관한 프로젝트 보기
+
setOpen(value => !value)}>{t("archive.show")}
{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.mutate(item)}>되살리기
+ restore.mutate(item)}>{t("archive.restore")}
)}
- {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")}
- 입력 JSON project_id만 주입됩니다. 필요한 ID·digest·DTO는 계약에 맞게 직접 입력하세요.
+ {t("advanced.input")} {t("advanced.inputHint")}
;
}
@@ -63,13 +65,13 @@ export function CapabilityWorkbench({ projectId, threadId, initialNamespace = "a
);
const method = methods.find(item => item.name === selected) ?? methods[0];
return
- CAPABILITY WORKBENCH
기능 탐색 · 고급 제어 모든 공개 계약을 한곳에서. 전용 연구 흐름과 기술 RPC를 구분합니다.
+ CAPABILITY WORKBENCH
{t("advanced.title")} {t("advanced.intro")}
{capabilityCatalog.canonical_method_count} canonical + {capabilityCatalog.compatibility_alias_count} aliases
- setSearch(event.target.value)} />
- setNamespace(event.target.value)} options={[{value:"all", label:`전체 ${visibleNamespaces.length} namespaces`}, ...visibleNamespaces.map(value => ({value, label:`${value} · ${namespaceLabels[value] ?? value}`}))]} />
- setSurface(event.target.value)} options={[{value:"all",label:"전체 종류"},{value:"QUERY",label:"조회 QUERY"},{value:"COMMAND",label:"명령 COMMAND"},{value:"QUERY_OR_CONTROL",label:"호환 조회·제어"}]} />
- {methods.length} visible methods
- {methods.length === 0 &&
검색에 맞는 기능이 없습니다.
}
+
setSearch(event.target.value)} />
+ setNamespace(event.target.value)} options={[{value:"all", label:formatMessage("namespaceCount", visibleNamespaces.length)}, ...visibleNamespaces.map(value => ({value, label:`${value} · ${namespaceLabels[value] ?? value}`}))]} />
+ setSurface(event.target.value)} options={[{value:"all",get label() { return t("advanced.allKinds"); }},{value:"QUERY",get label() { return t("advanced.queryKind"); }},{value:"COMMAND",get label() { return t("advanced.commandKind"); }},{value:"QUERY_OR_CONTROL",get label() { return t("advanced.compatibility"); }}]} />
+
{methods.length} visible methods
+ {methods.length === 0 &&
{t("advanced.noMatch")}
}
{methods.map(item =>
setSelected(item.name)} className="method-item">
{item.surface === "QUERY" ? "Q" : "C"} {item.name} {namespaceLabels[item.namespace] ?? item.namespace}{!item.canonical ? " · alias" : ""}
)}
{method &&
}
diff --git a/apps/web/src/components/ClaudeCodeHelp.tsx b/apps/web/src/components/ClaudeCodeHelp.tsx
index 0f56ff0..b64f8a2 100644
--- a/apps/web/src/components/ClaudeCodeHelp.tsx
+++ b/apps/web/src/components/ClaudeCodeHelp.tsx
@@ -1,9 +1,10 @@
+import { t } from "../i18n/locale";
import { CLAUDE_CODE_INSTALL_COMMAND } from "./connectionCopy";
import { ToolInstallHelp } from "./ToolInstallHelp";
/** Shown when the official Claude Code executable is missing or too old. */
export function ClaudeCodeHelp({ onInstalled }: { onInstalled?: () => void }) {
- return
;
}
diff --git a/apps/web/src/components/ClosureMark.tsx b/apps/web/src/components/ClosureMark.tsx
index 2dc5bb4..1e7cc16 100644
--- a/apps/web/src/components/ClosureMark.tsx
+++ b/apps/web/src/components/ClosureMark.tsx
@@ -1,3 +1,5 @@
+import { message as formatMessage } from "../i18n/formatMessages";
+import { t } from "../i18n/locale";
import { Button, Tag } from "@blueprintjs/core";
import type { ClosureRow } from "../api/judgmentRecords";
import type { Verdict } from "../api/trace";
@@ -15,10 +17,10 @@ export function ClosureMark({ row, verdict, label, onRecord }: { row: ClosureRow
return
{shown &&
{shown.label}
{shown.lines.map((line, index) => {line} )} }
- {row && older.length > 0 &&
+ {row && older.length > 0 &&
{older.map(event => {eventKindLabel(event)}
{eventLines(row.subject_kind, event).map((line, index) => {line} )} )}
}
- {!isMet(verdict.state) && onRecord(verdict)}>처분 기록 }
+ {!isMet(verdict.state) && onRecord(verdict)}>{t("lesson.disposition")} }
;
}
diff --git a/apps/web/src/components/ClosureRecorder.tsx b/apps/web/src/components/ClosureRecorder.tsx
index 83516e5..15bb882 100644
--- a/apps/web/src/components/ClosureRecorder.tsx
+++ b/apps/web/src/components/ClosureRecorder.tsx
@@ -1,9 +1,11 @@
+import { message as formatMessage } from "../i18n/formatMessages";
+import { t } from "../i18n/locale";
import { Button, Callout, Classes, Dialog, InputGroup, Radio, RadioGroup, TextArea } from "@blueprintjs/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useState } from "react";
import { closuresKey, recordClosure, type ClosureKind } from "../api/judgmentRecords";
import type { Verdict } from "../api/trace";
-import { CLOSURE_KINDS, CLOSURE_NOTE } from "./judgmentRecordText";
+import { CLOSURE_KINDS, closureNote } from "./judgmentRecordText";
/** Collects what a person decided about a row that was not met, with the document it rests on. The verdict is not changed. */
export function ClosureRecorder({ projectId, verdict, title, onClose }: { projectId: string; verdict: Verdict | null; title: string; onClose: () => void }) {
@@ -21,30 +23,27 @@ export function ClosureRecorder({ projectId, verdict, title, onClose }: { projec
onError: () => void client.invalidateQueries({ queryKey: closuresKey(projectId) }),
});
const submit = () => {
- if (kind === "") { setMessage("처분 종류를 고르세요."); return; }
- if (!basis.trim()) { setMessage("근거 문서를 적어 주세요."); return; }
- if (kind === "CONDITION_CHANGED" && !scope.trim()) { setMessage("해당 조건 범위를 적어 주세요."); return; }
+ if (kind === "") { setMessage(t("closure.choose")); return; }
+ if (!basis.trim()) { setMessage(t("closure.basisNeeded")); return; }
+ if (kind === "CONDITION_CHANGED" && !scope.trim()) { setMessage(t("closure.scopeNeeded")); return; }
setMessage(""); send.mutate();
};
- return
+ return
-
{CLOSURE_NOTE}
-
setKind(event.currentTarget.value as ClosureKind)}>
+ {closureNote()}
+ setKind(event.currentTarget.value as ClosureKind)}>
{CLOSURE_KINDS.map(item => )}
{kind !== "" && {CLOSURE_KINDS.find(item => item.code === kind)?.hint}
}
- 근거 문서 (필수)
- setBasis(event.target.value)}/>
- {kind === "CONDITION_CHANGED" && 해당 조건 범위 (필수)
- setScope(event.target.value)}/> }
- 메모 (선택)
-
+ {t("closure.requiredBasis")} setBasis(event.target.value)}/>
+ {kind === "CONDITION_CHANGED" && {t("closure.requiredScope")} setScope(event.target.value)}/> }
+ {t("recheck.optionalNote")}
{message && {message} }
- {send.error && 기록하지 못했습니다. 판정이 그 사이 바뀌었을 수 있으니 화면을 새로 읽은 뒤 다시 시도해 주세요. }
+ {send.error && {t("closure.failed")} }
- 닫기
- 기록 남기기
+ {t("hold.close")}
+ {t("testResult.save")}
;
}
diff --git a/apps/web/src/components/CodexToolHelp.tsx b/apps/web/src/components/CodexToolHelp.tsx
index 5bf0eb8..66d52d2 100644
--- a/apps/web/src/components/CodexToolHelp.tsx
+++ b/apps/web/src/components/CodexToolHelp.tsx
@@ -1,9 +1,10 @@
+import { t } from "../i18n/locale";
import { CODEX_TOOL_INSTALL_COMMAND } from "./connectionCopy";
import { ToolInstallHelp } from "./ToolInstallHelp";
/** Shown only after the server reports a missing or mismatched pinned Codex tool. */
export function CodexToolHelp({ onInstalled }: { onInstalled?: () => void }) {
- return ;
}
diff --git a/apps/web/src/components/ConfirmedFacts.tsx b/apps/web/src/components/ConfirmedFacts.tsx
index ec720c8..6363f20 100644
--- a/apps/web/src/components/ConfirmedFacts.tsx
+++ b/apps/web/src/components/ConfirmedFacts.tsx
@@ -1,3 +1,5 @@
+import { message as formatMessage } from "../i18n/formatMessages";
+import { t } from "../i18n/locale";
import { Tag } from "@blueprintjs/core";
import { numberSources } from "./answerText";
@@ -22,7 +24,7 @@ function readFindings(value: unknown): Finding[] {
* Facts the reviewer confirmed against the supplied documents: what a document states, and what was looked for and
* not found. Taken from the stored result field only; results saved before the field existed say so.
*/
-export function ConfirmedFacts({ result, onCite, heading = "확인된 사실", onlyWhenPresent = false }: {
+export function ConfirmedFacts({ result, onCite, heading = t("facts.title"), onlyWhenPresent = false }: {
result: Record;
onCite: (spanId: string, numbers: Record) => void;
/** Wording for the heading, e.g. when the research stopped early. */
@@ -37,16 +39,16 @@ export function ConfirmedFacts({ result, onCite, heading = "확인된 사실", o
const numbers = numberSources(answer, findings.flatMap(item => item.refs));
const numberMap = Object.fromEntries(numbers);
if (onlyWhenPresent && findings.length === 0) return null;
- return
- {heading} {findings.length > 0 && · 문서에 적힌 내용이며 기준 충족을 뜻하지 않습니다 }
- {!stored ? 이 결과에는 정리된 확인 사실이 없습니다(이전 형식).
- : findings.length === 0 ? 정리된 확인 사실 없음
+ return
+ {heading} {findings.length > 0 && {" "}{t("facts.documentOnly")} }
+ {!stored ? {t("facts.legacy")}
+ : findings.length === 0 ? {t("facts.empty")}
: {findings.map((item, index) =>
- {item.negative && 찾아봤지만 없음 } {item.statement}{" "}
- {item.refs.map(ref => {t("facts.notFound")}} {item.statement}{" "}
+ {item.refs.map(ref => onCite(ref, numberMap)}>[{numbers.get(ref)}] )}
)} }
- {dropped > 0 && 근거를 확인하지 못해 제외된 항목 {dropped}개 }
+ {dropped > 0 && {formatMessage("excludedFacts", dropped)} }
;
}
diff --git a/apps/web/src/components/ConversationTimeline.tsx b/apps/web/src/components/ConversationTimeline.tsx
index 7980263..e8fd2e9 100644
--- a/apps/web/src/components/ConversationTimeline.tsx
+++ b/apps/web/src/components/ConversationTimeline.tsx
@@ -1,3 +1,6 @@
+import { message } from "../i18n/formatMessages";
+import { getLocale } from "../i18n/locale";
+import { t, useTranslation } from "../i18n/locale";
import { Button, Callout, Icon } from "@blueprintjs/core";
import { useInfiniteQuery } from "@tanstack/react-query";
import { readConversation, withCurrentCheckpoint, type ConversationTurn } from "../api/conversation";
@@ -35,6 +38,7 @@ export function ConversationTimeline({ projectId, threadId, status, sourceUris,
const liveSources = example ? TIMELINE_EXAMPLE_SOURCES : sourceUris;
const protection = useConversationReadProtection(projectId, threadId);
const linked = useHypothesisLinks(projectId, !example);
+ useTranslation();
const query = useInfiniteQuery({
queryKey: ["conversation", projectId, threadId], enabled: Boolean(threadId) && !example,
initialPageParam: null as number | null,
@@ -42,15 +46,15 @@ export function ConversationTimeline({ projectId, threadId, status, sourceUris,
getNextPageParam: page => page.next ?? undefined,
});
if (!example && threadId && (protection.blocked || query.data?.pages.some(page => page.redacted) || (query.error && !canRetainConversation(query.error)))) {
- return
- 대화의 접근 권한이나 저장 기준을 확인하지 못해 답변을 표시하지 않습니다.
- void query.refetch()}>다시 읽기
+ return
+ {t("timeline.accessUnverified")}
+ void query.refetch()}>{t("workspace.reload")}
;
}
if (!example && threadId && !query.data) {
- return {query.error
- ? 대화를 불러오지 못했습니다. void query.refetch()}>다시 읽기
- : 대화를 읽는 중…
} ;
+ return {query.error
+ ? {t("timeline.loadFailed")} void query.refetch()}>{t("workspace.reload")}
+ : {t("timeline.loading")}
} ;
}
const unique = new Map();
for (const page of query.data?.pages ?? []) for (const turn of page.turns) unique.set(turn.input.request_revision_digest, turn);
@@ -66,29 +70,29 @@ export function ConversationTimeline({ projectId, threadId, status, sourceUris,
const finishedStages = (liveStatus?.completed_stages ?? []).filter(stage => stage.state === "COMPLETED").length;
const resumeOperation = liveStatus?.request?.operation_id;
const emptyExistingThread = Boolean(threadId && query.data && turns.length === 0 && !currentQuestion && !running);
- return
+ return
{!example && }
- {query.hasNextPage && void query.fetchNextPage()}>이전 대화 불러오기 }
- {query.error && {query.data ? "대화를 다시 확인하지 못했습니다. 마지막으로 불러온 답변을 표시하며, 최신 상태는 확인되지 않았습니다. " : "대화를 불러오지 못했습니다. "} void query.refetch()}>다시 읽기 }
- {query.data?.pages.some(page => page.limited) && 조회 범위를 넘는 과거 이력이 있습니다. 기록에서 확인할 수 있습니다. }
+ {query.hasNextPage && void query.fetchNextPage()}>{t("timeline.older")} }
+ {query.error && {query.data ? t("timeline.refreshFailed") : t("timeline.loadFailedSpace")} void query.refetch()}>{t("workspace.reload")} }
+ {query.data?.pages.some(page => page.limited) && {t("timeline.limited")} }
{turns.map(turn =>
-
질문 {turn.input.edit_kind === "REPLACE" ? "수정" : turn.input.edit_kind === "STEER" ? "· 방향 변경" : ""}{new Date(turn.input.created_at).toLocaleString()} {turn.input.text}
+
{message("questionHeading", turn.input.edit_kind)}{new Date(turn.input.created_at).toLocaleString(getLocale() === "en" ? "en-US" : "ko-KR")} {turn.input.text}
onRetry(turn.input.text) : undefined} usageLine={resultUsageLine(liveStatus, turn.input.operation_id)}
currentness={turn.currentness} historySelection={{kind:"result",scope:{projectId,threadId,requestDigest:turn.input.request_revision_digest},operationId:turn.input.operation_id,
resultDigest:currentResultDigest(status,{projectId,threadId,requestDigest:turn.input.request_revision_digest},turn.input.operation_id)}} {...followupFor(turn.input.request_revision_digest)} />
)}
- {unavailableHistory && 이 서버는 과거 대화 조회를 지원하지 않습니다. 확인 가능한 현재 기록만 표시합니다. }
+ {unavailableHistory && {t("timeline.unsupported")} }
{(currentQuestion || running) &&
- {currentQuestion &&
질문{example ? " · 예시" : ""}{example ? {new Date(TIMELINE_EXAMPLE_ASKED_AT).toLocaleString()} : null} {currentQuestion}
}
+ {currentQuestion &&
{message("questionExample", example)}{example ? {new Date(TIMELINE_EXAMPLE_ASKED_AT).toLocaleString(getLocale() === "en" ? "en-US" : "ko-KR")} : null} {currentQuestion}
}
- {queuedInstruction &&
현재 조사가 끝나면 이 지시를 이어서 반영합니다 }
+ {queuedInstruction &&
{t("timeline.queued")} }
{!running &&
onRetry(liveStatus.request!.authored_text!) : undefined}
onResume={onResume && resumeOperation ? () => onResume(resumeOperation) : undefined} resumeCompleted={finishedStages} resuming={resuming} usageLine={resultUsageLine(liveStatus, currentRequestResult?.operation_id)}
currentness={currentRequestResult ? liveStatus?.basis_currentness : undefined} executionHold={currentRequestResult ? liveStatus?.execution_hold : undefined} projectId={projectId} holdCauseRef={currentRequestResult?.request_ref?.revision_digest} historySelection={currentRequestResult?.request_ref?.revision_digest ? {kind:"result",scope:{projectId,threadId,requestDigest:currentRequestResult.request_ref.revision_digest},operationId:currentRequestResult.operation_id,
resultDigest:currentResultDigest(liveStatus,{projectId,threadId,requestDigest:currentRequestResult.request_ref.revision_digest},currentRequestResult.operation_id)} : undefined} {...followupFor(currentRequestResult?.request_ref?.revision_digest)}/>}
}
- {emptyExistingThread && 이 작업의 대화 기록을 현재 화면에서 불러올 수 없습니다. 저장된 연구 이력과 현재 프로젝트 기록을 확인하거나, 같은 작업에서 새 질문을 이어갈 수 있습니다.
새 질문으로 이어가기 연구 이력 확인
}
- {!threadId && turns.length === 0 && !currentQuestion && !running && THOTH 지금 무엇이 막혀 있나요? 질문과 자료에서 출발해 근거, 가능한 설명, 다음 행동을 함께 확인합니다.
}
- {query.isPending && threadId && !example && 대화를 읽는 중…
}
+ {emptyExistingThread && {t("timeline.unavailable")} {t("timeline.recoverAdvice")}
{t("timeline.newQuestion")} {t("timeline.openHistory")}
}
+ {!threadId && turns.length === 0 && !currentQuestion && !running && THOTH {t("timeline.emptyTitle")} {t("timeline.emptyNotice")}
}
+ {query.isPending && threadId && !example && {t("timeline.loading")}
}
;
}
diff --git a/apps/web/src/components/DomainRecords.tsx b/apps/web/src/components/DomainRecords.tsx
index 5977c1f..d3034ae 100644
--- a/apps/web/src/components/DomainRecords.tsx
+++ b/apps/web/src/components/DomainRecords.tsx
@@ -1,37 +1,39 @@
-import { Button, Callout, Icon, InputGroup, Tag } from "@blueprintjs/core";
-import { useMutation } from "@tanstack/react-query";
-import { useState } from "react";
-import { rpc } from "../api/rpcClient";
-import { transportFor } from "../api/capabilities";
-import { RecordInspector } from "./RecordInspector";
+import { UiError } from "../i18n/UiError";
+import { t } from "../i18n/locale";
+import { Button, Callout, Icon, InputGroup, Tag } from "@blueprintjs/core";
+import { useMutation } from "@tanstack/react-query";
+import { useState } from "react";
+import { rpc } from "../api/rpcClient";
+import { transportFor } from "../api/capabilities";
+import { RecordInspector } from "./RecordInspector";
import { visibleRecordViews, type RecordView } from "../api/recordViews";
-
-function DomainRecordDetail({projectId,threadId,operationId,view,onOpenCatalog}: {projectId:string;threadId:string;operationId:string;view:RecordView;onOpenCatalog:(namespace:string)=>void}) {
- const [operation,setOperation] = useState(operationId);
- const blocked = !view.method || (view.scope === "thread" && !threadId) || (view.scope === "operation" && !operation.trim());
- const read = useMutation({ mutationFn: () => {
- if (!view.method) throw new Error("이 namespace에는 비변경 조회 계약이 없습니다.");
- return rpc>(view.method,
- {project_id:projectId,...(view.scope === "thread" ? {thread_id:threadId} : {}),...(view.scope === "operation" ? {operation_id:operation} : {})},crypto.randomUUID());
- },
- });
- return {view.label} {view.description}
{view.namespace}
- {view.note &&
{view.note} }
- {view.scope === "operation" &&
{setOperation(event.target.value);read.reset();}}/>}
- read.mutate()}>기록 불러오기 onOpenCatalog(view.namespace)}>이 영역의 상세 조회 · 고급 제어
- {view.method && {transportFor(view.method)==="/rpc/query" ? "비변경 조회입니다." : "서버의 /rpc 기록형 조회입니다. 이 버튼을 누를 때만 operation·journal이 생성될 수 있습니다."} }
- {view.scope === "thread"&&!threadId && 작업을 먼저 선택하세요. }
- {read.error && {read.error.message}
세부 ID나 범위가 필요한 계약은 전체 기능의 입력 편집기에서 조회할 수 있습니다.
}
- {read.data ?
{view.label} {read.data.state} : !read.error && {view.label} 조회 전 이 탭을 여는 것만으로 작업을 실행하지 않습니다.
}
- ;
-}
-
-export function DomainRecords({ projectId, threadId, operationId = "", onOpenCatalog }: { projectId: string; threadId: string; operationId?:string; onOpenCatalog: (namespace:string) => void }) {
- const [selected, setSelected] = useState("object");
+
+function DomainRecordDetail({projectId,threadId,operationId,view,onOpenCatalog}: {projectId:string;threadId:string;operationId:string;view:RecordView;onOpenCatalog:(namespace:string)=>void}) {
+ const [operation,setOperation] = useState(operationId);
+ const blocked = !view.method || (view.scope === "thread" && !threadId) || (view.scope === "operation" && !operation.trim());
+ const read = useMutation({ mutationFn: () => {
+ if (!view.method) throw new UiError("records.noQuery");
+ return rpc>(view.method,
+ {project_id:projectId,...(view.scope === "thread" ? {thread_id:threadId} : {}),...(view.scope === "operation" ? {operation_id:operation} : {})},crypto.randomUUID());
+ },
+ });
+ return {view.label} {view.description}
{view.namespace}
+ {view.note &&
{view.note} }
+ {view.scope === "operation" &&
{setOperation(event.target.value);read.reset();}}/>}
+ read.mutate()}>{t("records.load")} onOpenCatalog(view.namespace)}>{t("records.controls")}
+ {view.method && {transportFor(view.method)==="/rpc/query" ? t("records.readOnly") : t("records.journaled")} }
+ {view.scope === "thread"&&!threadId && {t("records.chooseTask")} }
+ {read.error && {read.error.message}
{t("records.moreInputs")}
}
+ {read.data ?
{view.label} {read.data.state} : !read.error && {view.label}{" "}{t("records.beforeRead")} {t("records.noAutoRun")}
}
+ ;
+}
+
+export function DomainRecords({ projectId, threadId, operationId = "", onOpenCatalog }: { projectId: string; threadId: string; operationId?:string; onOpenCatalog: (namespace:string) => void }) {
+ const [selected, setSelected] = useState("object");
const view = visibleRecordViews.find(item => item.namespace === selected) ?? visibleRecordViews[0];
- return
- {visibleRecordViews.map(item=>setSelected(item.namespace)}>
- {item.label} {item.namespace} )}
-
- ;
-}
+ return CANONICAL RECORDS
{t("records.title")} {t("records.intro")}
+ {visibleRecordViews.map(item=>setSelected(item.namespace)}>
+ {item.label} {item.namespace} )}
+
+ ;
+}
diff --git a/apps/web/src/components/EffectDeclarationFields.tsx b/apps/web/src/components/EffectDeclarationFields.tsx
index 781f79b..7b4c919 100644
--- a/apps/web/src/components/EffectDeclarationFields.tsx
+++ b/apps/web/src/components/EffectDeclarationFields.tsx
@@ -1,3 +1,5 @@
+import { uiText } from "../i18n/locale";
+import { t } from "../i18n/locale";
import { Callout, Checkbox } from "@blueprintjs/core";
import { COMPLETE_LABEL, EFFECT_GROUPS, FORBIDDEN_NOTICE, prohibitedChosen } from "./effectDeclaration";
import { Disclosure } from "./Disclosure";
@@ -6,14 +8,14 @@ import { Disclosure } from "./Disclosure";
export function EffectDeclarationFields({ chosen, complete, onChoose, onComplete }: {
chosen: ReadonlySet; complete: boolean; onChoose: (key: string, on: boolean) => void; onComplete: (on: boolean) => void;
}) {
- return
- 적은 대로 규칙이 위험 등급과 필요한 사람을 정합니다. 아무것도 적지 않으면 효과를 확인해야 하는 행동으로 남습니다.
+ return
+ {t("effects.rules")}
{EFFECT_GROUPS.map(group =>
{group.title}
{group.effects.map(effect => onChoose(effect.key, event.currentTarget.checked)}/>)}
)}
- {prohibitedChosen(chosen).length > 0 && {FORBIDDEN_NOTICE} }
- onComplete(event.currentTarget.checked)}/>
- {complete && chosen.size === 0 && 고른 것이 없으면 읽기만 하는 행동으로 적은 것이 됩니다. }
+ {prohibitedChosen(chosen).length > 0 && {uiText(FORBIDDEN_NOTICE)} }
+ onComplete(event.currentTarget.checked)}/>
+ {complete && chosen.size === 0 && {t("effects.readOnly")} }
;
}
diff --git a/apps/web/src/components/EvidenceRail.tsx b/apps/web/src/components/EvidenceRail.tsx
index 6188be0..6e63228 100644
--- a/apps/web/src/components/EvidenceRail.tsx
+++ b/apps/web/src/components/EvidenceRail.tsx
@@ -1,3 +1,6 @@
+import { message as formatMessage } from "../i18n/formatMessages";
+import { t } from "../i18n/locale";
+import { getLocale } from "../i18n/locale";
import { useEffect, useMemo, useRef, useState } from "react";
import { Callout, HTMLSelect, InputGroup } from "@blueprintjs/core";
@@ -14,7 +17,7 @@ function locator(span: EvidenceSpan) {
span.locator.xml_path ?? null,
span.locator.uri ?? null,
];
- return values.filter(Boolean).join(" · ") || "구조 위치 없음";
+ return values.filter(Boolean).join(" · ") || t("evidenceRail.noLocator");
}
export function EvidenceRail({
@@ -56,39 +59,39 @@ export function EvidenceRail({
card?.scrollIntoView?.({ block: "center", behavior: "smooth" });
}, [focusSpanId, evidence.length]);
return (
-