diff --git a/.gitattributes b/.gitattributes index 0723164..2767930 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,6 @@ # Preserve published verification evidence byte-for-byte for SHA-256 readback. outputs/** -text .codex/verification/** -text + +# Trace CSV exchange fixtures keep their exact bytes (BOM, CRLF, tabs, CR). +tests/fixtures/trace_csv/** -text diff --git a/.github/workflows/product-check.yml b/.github/workflows/product-check.yml index b5d5053..0fd2bcb 100644 --- a/.github/workflows/product-check.yml +++ b/.github/workflows/product-check.yml @@ -52,6 +52,8 @@ jobs: run: .\.venv\Scripts\python.exe -m ruff check src apps tests migrations conftest.py scripts/pytest_environment.py - name: Runtime architecture run: .\.venv\Scripts\python.exe scripts/check_architecture.py + - name: Module size budget + run: .\.venv\Scripts\python.exe scripts/check_module_budget.py - name: Web tests run: pnpm --dir apps/web run test --run - name: Web lint diff --git a/.gitignore b/.gitignore index 6fbfc00..093b477 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ field-validation/results/ # Local research and verification artifacts docs/plans/ docs/research/ +docs/decisions/ docs/verification/ PROJECT_WIKI/ outputs/ diff --git a/AGENTS.md b/AGENTS.md index 00f8bfd..a768cc5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,7 +14,7 @@ ## 작업 범위 - 조사·설명·설계 요청은 제품 수정을 허가하지 않는다. 구현 요청에서는 합의한 범위를 끝까지 수행하되, 의미 변경·외부 권한·비용이 필요한 부분만 질문한다. -- 현재 작업서는 해당 인수조건의 기준이다. PROJECT_WIKI와 계획·리서치·과정 기록은 로컬 전용이며 공개 checkout의 필수 입력이 아니다. 아래 공개 상세 계약은 변경 영역에 해당하는 것만 읽는다. 과거 완료 보고·기억보다 현재 코드와 해당 source의 실행 증거를 우선한다. +- 현재 작업서는 해당 인수조건의 기준이다. PROJECT_WIKI와 계획·리서치·과정 기록은 로컬 전용이며 공개 checkout의 필수 입력이 아니다. 로컬 최신 작업은 PROJECT_WIKI/local/NOW.md에서 확인한다(없으면 PROJECT_WIKI/NOW.md는 공개 상태문일 뿐이다). 아래 공개 상세 계약은 변경 영역에 해당하는 것만 읽는다. 과거 완료 보고·기억보다 현재 코드와 해당 source의 실행 증거를 우선한다. - 기존 dirty 변경과 사용자 데이터를 보존한다. 검사 실패를 숨기려고 테스트·정책·기준값·예외 허용을 바꾸지 않는다. 변경이 필요하면 이유와 새 계약을 명시한다. ## 제품을 보존할 것 @@ -58,6 +58,12 @@ - 인계에는 해결한 기능, 실제 검증, 미완료/차단, 관련 source와 기록 위치를 남긴다. 제품 의미가 바뀌면 정본을 먼저, 해당 Wiki projection을 다음으로 갱신한다. 상태만 바뀌면 NOW/검증 기록만 갱신한다. 이 기록은 커밋·병합·세션 인계처럼 넘기는 지점에서 짧게 남기고, 개발 중간 단계마다 쓰지 않는다. - 배포·외부 발송·공식 제출·새 credential/비용·물리 장비·commit/push는 사용자의 해당 실행 지시 범위를 따른다. 이 문서 자체는 그 권한을 부여하지 않는다. +- 외부 시스템 동작에 기대는 기본값은 조사가 끝난 뒤 승인한다. 승인 요청서에는 "아직 확인하지 않은 가정" 칸을 둔다. +- 기존 RPC·서비스를 재사용하는 계획은 확정 전에 그 기능을 부르는 곳과 결과를 쓰는 곳을 실제 코드 경로(파일:행)로 계획서에 적는다. +- 리뷰 지적은 사용자에게 보이는 영향이 있거나 재현 시험을 만들 수 있을 때만 반영하고 나머지는 보류 목록에 모은다. 리뷰 라운드는 2회에서 멈춘다. +- 임시 스크립트·측정 결과는 `tmp_` 접두어 또는 `outputs/` 아래에만 두고 요청이 끝나면 지운다. +- 세션이 끝날 때 미커밋 변경이 다음 세션으로 넘어가면 그 파일 수를 작업용 NOW의 "막힌 것"에 적는다. 작업 장소를 다른 폴더·저장소로 옮기는 결정에는 원본의 미커밋 자료를 어떻게 마감하는지 한 줄을 함께 적는다. + ## 상세 계약 길잡이 아래 경로는 저장소 루트 기준으로 읽는다. 모든 파일을 매 턴 전부 읽으라는 뜻이 아니다. @@ -69,3 +75,5 @@ - 제품 기능의 고정 중심: research-briefs/CORE_PRODUCT_INVARIANTS.md - 공개 RPC 정본: docs/architecture/rpc-method-catalog.md - 제품 확인 결과와 한계: docs/VERIFICATION.md +- 결정 기록(로컬): docs/decisions/ +- 작업용 현재 상태(로컬): PROJECT_WIKI/local/NOW.md diff --git a/SOURCE_MANIFEST.json b/SOURCE_MANIFEST.json index c828306..760b2e1 100644 --- a/SOURCE_MANIFEST.json +++ b/SOURCE_MANIFEST.json @@ -1,6 +1,6 @@ { "schema_version": "thoth-source-preview-manifest-v1", - "created_at": "2026-09-29T05:48:00.426390+00:00", + "created_at": "2026-10-07T15:04:42.318674+00:00", "license": "MIT", "git_history_included": false, "full_suite_verified": false, @@ -13,23 +13,23 @@ }, { "path": ".gitattributes", - "sha256": "19974abeb14a36c4f359a9a964bf18f4a6b48f4f645a07e6d26314828ad3d018", - "bytes": 237 + "sha256": "a1d9f19d35f297456914889079165667dd58d91e6af5a286a894640c4c84826c", + "bytes": 348 }, { "path": ".github/workflows/product-check.yml", - "sha256": "716790b9e61a1182faec87badc6accaaa27805f1b95a8eb19ad56f4dfdc5d1c8", - "bytes": 3306 + "sha256": "6e58032f4e10fc0c7e896ab6ddc1730021aececb440be781ffe9d7159ae77ca9", + "bytes": 3386 }, { "path": ".gitignore", - "sha256": "a6cb90a84a1ba2f6e02518dc980f4786735715a610bce2240d403a77dbc47223", - "bytes": 591 + "sha256": "e950beb056b5e1b3720864f82bcea7bb86b9ab30aecf7d22f23523dacf54c050", + "bytes": 607 }, { "path": "AGENTS.md", - "sha256": "1a0ba35d210506d5d1b75e787aeee57190c6d9aef07a100ef56df81e85bb9d8c", - "bytes": 9502 + "sha256": "0ce3eab6e2adc3dc2fc03bcebcc65e732d8d223dbcafe0d2214d70a8bc8173c3", + "bytes": 10702 }, { "path": "CONTRIBUTING.md", @@ -63,8 +63,8 @@ }, { "path": "THIRD_PARTY_NOTICES.md", - "sha256": "c9e0fde89568e46e44d375ceb35f7874f465974fc61988f2edff7366ae4cc98e", - "bytes": 3123 + "sha256": "bd11c3a4c6f7e77b6e315001eb248d3f6d502143329c1dd6cdea145bf406e878", + "bytes": 3598 }, { "path": "alembic.ini", @@ -116,10 +116,15 @@ "sha256": "79d0d4978f19ed1f31f128a744e9c38af52970ea710f9edcf1a6e98343fdb6d6", "bytes": 922 }, + { + "path": "apps/web/src/api/actionDraft.ts", + "sha256": "e35d50411a3a6b1313bb005c9d5333429eba7c8de2b6836636947068e93616c9", + "bytes": 588 + }, { "path": "apps/web/src/api/capabilities.ts", - "sha256": "e021549898a02517e530eead336a125332ddc402f32a3221284aaa850161668b", - "bytes": 2141 + "sha256": "c17b66ff964dd7d34a2d8dfd1ef8404c25feeaea500847b388ff172f728fb055", + "bytes": 2161 }, { "path": "apps/web/src/api/contracts.test.ts", @@ -211,15 +216,45 @@ "sha256": "13c56f8ba756533e22f16148a7383480cf0130dea4a6069feb2dd2afdccd48ff", "bytes": 10787 }, + { + "path": "apps/web/src/api/hypothesisLink.ts", + "sha256": "0bb463ad28b1ef4dba3d947a97995bb6fc7f5a22e1cc497cae63931ef9338a06", + "bytes": 1829 + }, + { + "path": "apps/web/src/api/hypothesisSame.ts", + "sha256": "935f8967ee3bad79fb196b18ac5e0150610aee6517e073582fdab5e9327ff831", + "bytes": 1390 + }, + { + "path": "apps/web/src/api/judgmentRecords.ts", + "sha256": "e3d3286e76442d5fffe110bda299264f7bc70673c2c7a6758a7cb3bea0366543", + "bytes": 3401 + }, + { + "path": "apps/web/src/api/koreanParticles.test.ts", + "sha256": "c4195ffa70dc91cd494bf74f48e07683ad325cb07b0b237471c42d451044e7f3", + "bytes": 3968 + }, + { + "path": "apps/web/src/api/koreanParticles.ts", + "sha256": "ce26c6a2c7aefb1b78cb7d345394b289ac2a1e9b053fcf133cdd69a4d5c0d572", + "bytes": 2173 + }, + { + "path": "apps/web/src/api/lessons.ts", + "sha256": "766cc09dd47f8624aa3c9dac94e692607b6ec6c97521b84a5f11a93edad91330", + "bytes": 1330 + }, { "path": "apps/web/src/api/localWorkspacePersistence.test.ts", - "sha256": "f13c79fbe230b20110246fa47dd7513510b6287864fa94662b0f87e36c82c14c", - "bytes": 5453 + "sha256": "38c5717309058387d653f03f438913ee94176a7bef67c0f939b8e6d5f63a6983", + "bytes": 7017 }, { "path": "apps/web/src/api/localWorkspacePersistence.ts", - "sha256": "ea5ba4a65c8fb45fcb60bee7b44610ae67ccff03a016227c83e84077043f7a54", - "bytes": 9124 + "sha256": "0ad311c836e661ba2c5ca26552aa5bb3520bfea827de9a8eae23c2441d4bb3de", + "bytes": 10591 }, { "path": "apps/web/src/api/presentation.ts", @@ -248,13 +283,13 @@ }, { "path": "apps/web/src/api/recordViews.ts", - "sha256": "2846424055af76927b469e008976bc7fad5f088a0c6017c9d71925097f98d8e9", - "bytes": 3585 + "sha256": "99fab71d25723afb6d6d2c67a5cf455336f0aab754436f0b4f271821de4e0ba2", + "bytes": 3726 }, { "path": "apps/web/src/api/research.ts", - "sha256": "5217c5800b69f85bfadb5bd50ff3c175d2b797b915a8349e5634b36b1760975c", - "bytes": 10429 + "sha256": "5a9f77e9c842818d8156a6f084495d60ae26e7155fa572ebc4c9ff417e808fdc", + "bytes": 11238 }, { "path": "apps/web/src/api/researchFollowup.ts", @@ -268,8 +303,18 @@ }, { "path": "apps/web/src/api/researchProgress.ts", - "sha256": "65ed5362638fbdf9da1a5bba14976cce49f597ed7d11cb311b51d45579651eac", - "bytes": 33012 + "sha256": "093426d7173055d6839ac528da2f047d4c25c78811d7b6b7218f61a153d461c7", + "bytes": 14966 + }, + { + "path": "apps/web/src/api/researchProgressCharacterization.test.ts", + "sha256": "e174366b6c1667718ec9fa26ff156bfbefcee05d490ef53c34dcd28cf6085051", + "bytes": 32595 + }, + { + "path": "apps/web/src/api/researchProgressText.ts", + "sha256": "32246515b0589a329abefea9d9a35efd472611fddb9012f8222bc713b9ba1b11", + "bytes": 18292 }, { "path": "apps/web/src/api/researchStatusRead.test.ts", @@ -333,8 +378,8 @@ }, { "path": "apps/web/src/api/rpcClient.ts", - "sha256": "d4c8f7f230f6ff7144cfb3a6e52fe40c8566055c379e1ed1beed630a72b9bbc9", - "bytes": 6567 + "sha256": "cbda889c4394185df4b987a9d44f32efa47d6a79524da065f7f8ea7d2547eaf3", + "bytes": 6759 }, { "path": "apps/web/src/api/rpcTransport.test.ts", @@ -356,6 +401,11 @@ "sha256": "1d8ac2a17ffa2a86720c472ce953d89bb2e1e5d99d614e5b86ebec1a0b2d4cbc", "bytes": 8913 }, + { + "path": "apps/web/src/api/trace.ts", + "sha256": "39f426e38364313f5fcf028abd542a7fd2e397cdea797fb5eb2e079c80d70636", + "bytes": 5312 + }, { "path": "apps/web/src/components/ActionCompare.test.tsx", "sha256": "8b26648c751ba842c6b40bf7522548cd4fcd426cd985b179378816e3a7da20f7", @@ -363,14 +413,24 @@ }, { "path": "apps/web/src/components/ActionCompare.tsx", - "sha256": "aff7854ae5d8b560c3199e4b510879b2991fd2687787140d8aede188b5ffa30d", - "bytes": 5303 + "sha256": "b36ba6b773050297ec7ec45572addbc22be522893f4ed5862abcca3d37a23280", + "bytes": 6970 }, { "path": "apps/web/src/components/ActionCompareApproval.test.tsx", "sha256": "263b69b8545050ab59f872605bb7e3a67655712f683a26046984954957da6da5", "bytes": 3171 }, + { + "path": "apps/web/src/components/ActionCompareLive.tsx", + "sha256": "7bfa054898226f661b707d38238db40794e523818d4d00accb59294997e82149", + "bytes": 629 + }, + { + "path": "apps/web/src/components/ActionCompareStale.test.tsx", + "sha256": "f173e32970071d5c9ce6ce01b3ef9de50cb8f6030b1165605391dae075964028", + "bytes": 2371 + }, { "path": "apps/web/src/components/ActionEffort.test.tsx", "sha256": "6c853309bae39c97c85d6b315b5706a93981df9bd8501e8edf1632403dda2f72", @@ -401,6 +461,11 @@ "sha256": "e682f1265361256d4f5e433fa4e7098eba2e094c87fc38c9ce597019f9ccfb6e", "bytes": 1561 }, + { + "path": "apps/web/src/components/AnswerReadability.test.tsx", + "sha256": "5b743c6579f62b080acb14d3baa958afecfb16320ef74c414d3c58679b06587c", + "bytes": 4838 + }, { "path": "apps/web/src/components/ApprovalCard.test.tsx", "sha256": "b3427e26f27c43168ec3af555be2f8034b099df35c45b127560931d171aac29d", @@ -446,6 +511,21 @@ "sha256": "55cb6cb26dbda674e69b6df93fdcb0034b4a42b57d5cef789173721496486772", "bytes": 590 }, + { + "path": "apps/web/src/components/ClosureMark.test.tsx", + "sha256": "6b0f20f51ad531d3d93614d42532ba8589c3ec9759e0fcb39bb9b153a3b75295", + "bytes": 3672 + }, + { + "path": "apps/web/src/components/ClosureMark.tsx", + "sha256": "58acdca8bc700e361778aa37400460a477d907d432c2e97d35ad54d613b4e9e4", + "bytes": 1847 + }, + { + "path": "apps/web/src/components/ClosureRecorder.tsx", + "sha256": "2f22a37f20b430041c794fc53d0d539598e2b9031661fa0ebd1495348b09d9ef", + "bytes": 3755 + }, { "path": "apps/web/src/components/CodexToolHelp.tsx", "sha256": "a353746e797b15f0dc44b08f1f84e0d798fa8f231a02eee2ae1c53e90f1ffb5b", @@ -461,10 +541,15 @@ "sha256": "5f799a7c2ef75cdc6964222dde53865fd83b4a20328fe843e97bc5de9a179359", "bytes": 3107 }, + { + "path": "apps/web/src/components/ContractV3Screens.test.tsx", + "sha256": "d7f81d419b5f015906c4b562b23d748894a0045e970c9d32f5768f5a986ce5fb", + "bytes": 6723 + }, { "path": "apps/web/src/components/ConversationTimeline.tsx", - "sha256": "a4d519a26a3d38c362079df77247568571e40a0a5440809ec0d771659508880f", - "bytes": 9452 + "sha256": "48dc6fce194ebaa7bbea234f394edf74a5cb193754d77383e05558376d7362e3", + "bytes": 10085 }, { "path": "apps/web/src/components/ConversationTimelineDeniedRetention.test.tsx", @@ -483,19 +568,29 @@ }, { "path": "apps/web/src/components/ConversationTimelineRefetch.test.tsx", - "sha256": "11654bbcab4966485eb5e54a037a5286f2b6a52e36a417e443ea7e8449952d3e", - "bytes": 7425 + "sha256": "3ca5886e9bbbe3c46d2b28723afe42898f8fd1347862d5a1502faf88b2e3bf12", + "bytes": 7547 }, { "path": "apps/web/src/components/Disclosure.tsx", "sha256": "a4276000ac565b5f51798a28d0f2fc2ac73493c2b7206ea68a1c7f48548a8fd6", "bytes": 659 }, + { + "path": "apps/web/src/components/DiscriminationRecords.test.tsx", + "sha256": "296deb6f6993012c5a31a42acf7f51e0ac2d381305fe4787149839c98e77065c", + "bytes": 7175 + }, { "path": "apps/web/src/components/DomainRecords.tsx", "sha256": "be6b5f0aec0387bbf5625d85f1cc736044b8c9e3ed5eb2d2260f87bd4278e224", "bytes": 4151 }, + { + "path": "apps/web/src/components/EffectDeclarationFields.tsx", + "sha256": "9f7b628d23f7e8c1db62762706a666336ecf99ed4a04020a9314dc9171187681", + "bytes": 1548 + }, { "path": "apps/web/src/components/EvidenceRail.tsx", "sha256": "b85594f9b5c963f816b79013741c587696fe585506a4185875988088424c7b0e", @@ -506,20 +601,35 @@ "sha256": "2aa9b1e216a5cc502288464feab1bfda0d5e8175e1d1b95230dba6d0c509ef52", "bytes": 1145 }, + { + "path": "apps/web/src/components/ExecutionHoldNotice.test.tsx", + "sha256": "9241cbe3d0115ccadcbcbc90c6dab56526ef5eb18fba6412bf887aec1ac2cb4d", + "bytes": 8452 + }, + { + "path": "apps/web/src/components/ExecutionHoldNotice.tsx", + "sha256": "bd5d21ac1781314f99edbc4c826620d8f5e4addbc6a4a23370ae31feb5950b06", + "bytes": 6517 + }, + { + "path": "apps/web/src/components/ExpectedResults.tsx", + "sha256": "25cd1525290015798b6022f69044c031cdc92668de8b37cfce4f63b7a99257fd", + "bytes": 1159 + }, { "path": "apps/web/src/components/FileConnectionPanel.tsx", - "sha256": "88f4e972e9b621008c32f84b52336b17bd6ceca2abc351d98b1ff4cfa465f841", - "bytes": 4138 + "sha256": "88f0736ca9d3051d0539d43fc2c1454eea37a29801c2149290967e0cedeb59a3", + "bytes": 4146 }, { "path": "apps/web/src/components/FirstRunSetup.test.tsx", - "sha256": "bb34dc110bf3b80795720a3a12964d88ee1aa21fe1dfcd33168be751cbd36da2", - "bytes": 94889 + "sha256": "21ce1a85a94a26d9afb2d749db4706982d46bd24bd57cbfa8e17d213fb1b5e27", + "bytes": 96271 }, { "path": "apps/web/src/components/FirstRunSetup.tsx", - "sha256": "6c19dbb637486d54cceb7bca4d848f7e2d2313fa616fbae968b2c2d677a5b3f4", - "bytes": 16736 + "sha256": "35d0edb9ac86654510eca08b05d9fc63327329a877d01eb9b8f0e8e74acd7fe0", + "bytes": 16988 }, { "path": "apps/web/src/components/HypothesisCompare.test.tsx", @@ -528,23 +638,83 @@ }, { "path": "apps/web/src/components/HypothesisCompare.tsx", - "sha256": "9464c69cf662496bcd9bb55655d038999fb3e89e1c350024e4e351206970cf22", - "bytes": 4566 + "sha256": "71e230152818510e92a1e0ff33043e72ed8e7e0fd1bb082c0a97ec8196eba28b", + "bytes": 9156 + }, + { + "path": "apps/web/src/components/HypothesisCompareDraft.test.tsx", + "sha256": "1b41360c2c32fc6f6d3783904395315567ea02e7fbc9bb0bffe83350580093c5", + "bytes": 2517 }, { "path": "apps/web/src/components/HypothesisCompareLive.tsx", - "sha256": "a34d314862e74e1d7beaf9c8a7dd63915991f0963fac3e19b9e54e2001cefae8", - "bytes": 3229 + "sha256": "9451e289b891012df3a66afe917efc6aaec7d40b9d0593ee7d3f0bef46b9d441", + "bytes": 3734 + }, + { + "path": "apps/web/src/components/HypothesisContractSwitch.test.tsx", + "sha256": "44b9549862d525b0c2c59dacb9527ed14279d0fa2b735b8edba24f5bc6dc0445", + "bytes": 3159 + }, + { + "path": "apps/web/src/components/HypothesisContractSwitch.tsx", + "sha256": "c1f4bf99b8e5b43cda3647b1fe80357ecf64835e61f853a6db3e937e2b30cc2e", + "bytes": 2227 + }, + { + "path": "apps/web/src/components/HypothesisFields.tsx", + "sha256": "a064f2c1f3d6dbabea60af18268ff15f16d652cd35f77a7d8c507d49f6314326", + "bytes": 1814 + }, + { + "path": "apps/web/src/components/HypothesisLinkNotice.test.tsx", + "sha256": "e02db94e4bd356b34500cd595089049e18d28300204e98153e06c54c2a94f459", + "bytes": 9222 + }, + { + "path": "apps/web/src/components/HypothesisLinkNotice.tsx", + "sha256": "4b1421e5df71870088e0b3d028a5a4dafdada7d1ffb90777a5f8b320f702b613", + "bytes": 2767 + }, + { + "path": "apps/web/src/components/HypothesisLinkScreens.test.tsx", + "sha256": "e53bfc513df29a2210ab1d359696ab25f193c0af3171eb4d3da634f126f359be", + "bytes": 3096 }, { "path": "apps/web/src/components/HypothesisReview.test.tsx", "sha256": "9ed16b30a10acb02c7f31013c67e6133c9f0536f5d6d8a9615122800ada654fa", "bytes": 8119 }, + { + "path": "apps/web/src/components/HypothesisScreen.test.tsx", + "sha256": "1d3f68fd3458e5a1b384c4f5f6d671ebae1c1abded92720bc966628f63edda02", + "bytes": 10645 + }, + { + "path": "apps/web/src/components/LessonRecall.test.tsx", + "sha256": "32520ec35f4e8f350b6f008a7c2a98888eb7a6e0ccc8610d3768f3f77050296c", + "bytes": 6831 + }, + { + "path": "apps/web/src/components/LessonRecall.tsx", + "sha256": "ec792b54f7df53b8d4040eb6d00ce2570279b6393eaa2d7fe24668bf569deee0", + "bytes": 2022 + }, + { + "path": "apps/web/src/components/LinkRecheckDialog.tsx", + "sha256": "952483ded8663c704b951f9132feff99c934ec46baeefe4b32605a375f031a7f", + "bytes": 4299 + }, { "path": "apps/web/src/components/LiveProjectWorkspace.tsx", - "sha256": "6e6124f2532e262f4a15fbc2e8c4b0730950eff840b5ebb4e656b8b9444f5e92", - "bytes": 39897 + "sha256": "6ce64e0744afde57fd97362ddabb2c3cf299c92d366a613af055cfa1578f386b", + "bytes": 42234 + }, + { + "path": "apps/web/src/components/LiveProjectWorkspaceInvestigate.test.tsx", + "sha256": "eb9d8af3a645fffa139434dd4dadf13443ef896aacb5c6d0ff7937e4cbf4ee67", + "bytes": 11599 }, { "path": "apps/web/src/components/LiveProjectWorkspaceRestart.test.tsx", @@ -558,8 +728,8 @@ }, { "path": "apps/web/src/components/MemoryInjectionSwitch.tsx", - "sha256": "c18cab413d4f0adf4d1ba205ed0e1fa069bf245e8fa36c15f81367db8c5b7deb", - "bytes": 1807 + "sha256": "b2d8e5099a23202280231c7a1685cb6f705cb6f6e4b38e1cd183cc7bbca41a69", + "bytes": 2801 }, { "path": "apps/web/src/components/ModelCredentialPanel.tsx", @@ -568,8 +738,8 @@ }, { "path": "apps/web/src/components/ModelSettings.test.tsx", - "sha256": "705a546a1c3f8c619f0764be312dde531c9ffa2c616f149d6a90afda5d346c23", - "bytes": 23231 + "sha256": "2a12649f0a89d9a3ba3cba7d5b6500680dbb5ba81e0079991695bbc1a2614b00", + "bytes": 23318 }, { "path": "apps/web/src/components/ModelSettings.tsx", @@ -588,8 +758,8 @@ }, { "path": "apps/web/src/components/ProjectMemoryPanel.test.tsx", - "sha256": "4db7598f749ef9475e6eba4e5074571d077e4ab2559cd2a86ece3893842366c8", - "bytes": 15907 + "sha256": "51291b6cdd3a89a64bc9ea085a74e8ca7771c808c665096aee153174b81cfd21", + "bytes": 17328 }, { "path": "apps/web/src/components/ProjectMemoryPanel.tsx", @@ -606,11 +776,26 @@ "sha256": "162aa61f8ee4f053994dab69d54d9d3b856540fce226b7dad6f51739af9d9ba1", "bytes": 4299 }, + { + "path": "apps/web/src/components/ReadableList.tsx", + "sha256": "f34fc8e4f5773a87cdcdf19a4e2eb415e1f23e9c3612653c9a3759a5834caeba", + "bytes": 742 + }, { "path": "apps/web/src/components/RecordInspector.tsx", "sha256": "859d3dc994e400b1c59f65d8f2fe514392508a7d5c699d60f96665d6086725f4", "bytes": 1628 }, + { + "path": "apps/web/src/components/RecordedStanding.test.tsx", + "sha256": "8dbccad7188e7b6531ec3e4876bc6c7be0343a32c01d9137253f6623fa808cbb", + "bytes": 6376 + }, + { + "path": "apps/web/src/components/RefutationConditionsPanel.tsx", + "sha256": "bae8b0b34d40d53c6333f29c3a9358a678741a5a2bd0fcc19292d9543bf02906", + "bytes": 3039 + }, { "path": "apps/web/src/components/ResearchActivityStrip.test.tsx", "sha256": "37ed31a40b38b551034d8aed9089ae9a46472fa8cba4583be57677d54bafb8fc", @@ -633,8 +818,8 @@ }, { "path": "apps/web/src/components/ResearchDetailPane.tsx", - "sha256": "9583f90ba77ab4d32c3262a73ddc356b9d81b7da9b0338d45a0fa89390a41697", - "bytes": 4650 + "sha256": "fa0c6eaf4ca69c2c3784227f1769ed5830c0812d02ae72da91021271db7cccef", + "bytes": 4662 }, { "path": "apps/web/src/components/ResearchEvidenceDetail.test.tsx", @@ -643,8 +828,8 @@ }, { "path": "apps/web/src/components/ResearchFollowupSummary.tsx", - "sha256": "5485d544af139419f1ddbe782ba51ad7702256992754546167b56a72c563ab38", - "bytes": 9126 + "sha256": "dcf25f66a3e2ed86978828551022df13e51292c733f57dc5b9aef6bbc8492c0b", + "bytes": 10005 }, { "path": "apps/web/src/components/ResearchHold.test.tsx", @@ -668,8 +853,8 @@ }, { "path": "apps/web/src/components/ResearchResultCard.tsx", - "sha256": "067ea374b695d8fcc329f5f50211d01c75ade6091c3a8ae7ef207f8ef04d07d8", - "bytes": 11179 + "sha256": "eff8c6e086137cf74ffd34d1b0e330a4e482f1676c124857337042f3a6eb7680", + "bytes": 12795 }, { "path": "apps/web/src/components/ResearchSidePanel.tsx", @@ -683,13 +868,13 @@ }, { "path": "apps/web/src/components/ResultExample.tsx", - "sha256": "6b3c1fd323ba60ad8b87e7ef2551305b1f5a709d89427c793e71e72324cf4017", - "bytes": 7856 + "sha256": "4737b1d51c329ef207c7359bb50a0c85f1292eea26d20c1f98e86319574a6458", + "bytes": 7871 }, { "path": "apps/web/src/components/ResultScreen.test.tsx", - "sha256": "516aab01871f28251804e92d223c5191a418e24edd849944acded63844e10473", - "bytes": 11101 + "sha256": "f8e0e257e0106bb792deb265842b461cb4263a7bba05b165e559278bb1b1c8a8", + "bytes": 11671 }, { "path": "apps/web/src/components/ReviewDialog.tsx", @@ -701,6 +886,16 @@ "sha256": "c00f537d577a0a7d147f1e33981aafb2f38a9b7ddd475462431aea2fb9ebc5e3", "bytes": 1605 }, + { + "path": "apps/web/src/components/SameHypothesis.test.tsx", + "sha256": "b970b46fe1090830dbde8aa196b498c99184c2e1dd8a7c7a73ca14e3fdaf8a18", + "bytes": 9634 + }, + { + "path": "apps/web/src/components/SameHypothesis.tsx", + "sha256": "e928223ccf4c844e2e7b661c8516d4c48054e98016945e47c6f4d9fbc8ba3d15", + "bytes": 4095 + }, { "path": "apps/web/src/components/SourceCitationGate.test.tsx", "sha256": "ae24400610ed28eb75a4e2509e8dca502554bb723dec5ead64c9e6983330d856", @@ -726,11 +921,86 @@ "sha256": "8ba883cf96879aa2254fd9d4919a4d33c0fc93d9e9bc77c8e653a58dc521c977", "bytes": 742 }, + { + "path": "apps/web/src/components/TestActionDraft.test.tsx", + "sha256": "f8b421b3d715d61ccc6df44e33ea1ab6196d69f33dc6d0fbef30be06bb6eccc9", + "bytes": 8482 + }, + { + "path": "apps/web/src/components/TestActionDraft.tsx", + "sha256": "a08d9bed2c5c5c438a7dd6c1c0da059d972f9e9cffa3ae0b999c7a611395e5cc", + "bytes": 2546 + }, + { + "path": "apps/web/src/components/TestOrder.test.tsx", + "sha256": "9e20a618f4b73e9cb77ee0d242d0fa0d153f4f6db81133c8565277c82ad09a3d", + "bytes": 8572 + }, + { + "path": "apps/web/src/components/TestOrderSection.tsx", + "sha256": "31f15a5c7167d08429c7f07262fdd4d9d02cb357be8cb14629ac08f7aea22164", + "bytes": 2333 + }, + { + "path": "apps/web/src/components/TestResultRecorder.tsx", + "sha256": "0e1983b6bb64082e0ec851d21f1b91a7ef1cb0d04bf17254b17abbf2df017800", + "bytes": 3805 + }, + { + "path": "apps/web/src/components/ThothBlueprint.test.tsx", + "sha256": "0113ee9e811618c39ae9d614efb9809d43fa9cbaf1fa4e032202646fa0f92c05", + "bytes": 1674 + }, + { + "path": "apps/web/src/components/ThothBlueprint.tsx", + "sha256": "1ad23e783cbb8a98f2adffcc03da1122cb4c5063151a1409cc4a5d8ef1bfd453", + "bytes": 409 + }, { "path": "apps/web/src/components/ToolInstallHelp.tsx", "sha256": "6d229ab8daf3859e02897805027f84a8dcdaf2e90f6d1b733de14983a1c397be", "bytes": 2968 }, + { + "path": "apps/web/src/components/TraceClosure.test.tsx", + "sha256": "40257b5f3d6d5f20519ff3d016a8139ad943733a65da20cf4c2265693d576544", + "bytes": 8558 + }, + { + "path": "apps/web/src/components/TraceConfirmDialog.tsx", + "sha256": "b75200500ee9bdd5f1f04cc013f80ab1a57fc85d8ffdff067539ca963a40ddae", + "bytes": 1975 + }, + { + "path": "apps/web/src/components/TraceEvidenceDialog.tsx", + "sha256": "d7e23ff767ec2c290b4d94e6db5b47fb379be6e4d84296ac37477e95a9d43004", + "bytes": 2623 + }, + { + "path": "apps/web/src/components/TraceHistory.tsx", + "sha256": "d833ca06a38c2fd5cb6af6ecab4d7b3cd418a77590514ef7377f91607d07327c", + "bytes": 5757 + }, + { + "path": "apps/web/src/components/TraceImportDialog.tsx", + "sha256": "dc6255a64fc1c6ee4ecb5c6cadceae1fcb419ac3b167b02f9c59e76777f02b4b", + "bytes": 6063 + }, + { + "path": "apps/web/src/components/TraceOriginLine.tsx", + "sha256": "b925d2c4ec812b624e0dfafb25af0c826bd6f7506396bccf56f1fb33c6eaf852", + "bytes": 899 + }, + { + "path": "apps/web/src/components/TracePage.test.tsx", + "sha256": "e28966b8bb0d6ce1eaf46160c0548726f6b0120f833d934d5de47ce8d7965348", + "bytes": 30119 + }, + { + "path": "apps/web/src/components/TracePage.tsx", + "sha256": "c32393a61b252b82dc1927a808d7ea9291539d9c120f6cc0daea728642590bbe", + "bytes": 15748 + }, { "path": "apps/web/src/components/UsageSummary.test.tsx", "sha256": "8d13a355f510dfaeaf3a1f3aec1a0d14feab9f361a16103b01c1a602d48f73e7", @@ -771,6 +1041,16 @@ "sha256": "b4608b7147622b28852b715bfa1898bb063b843cabe2e3204f473827be32094c", "bytes": 4624 }, + { + "path": "apps/web/src/components/effectDeclaration.test.ts", + "sha256": "39c9f83cadfae85c050432d68149e9998e8f92eafa1bb0ad65150bae07c3c9c7", + "bytes": 2001 + }, + { + "path": "apps/web/src/components/effectDeclaration.ts", + "sha256": "cd7f65e8a39adeae07b21333b99a5f6f7b819429954f5d325a839ac3157759cf", + "bytes": 2698 + }, { "path": "apps/web/src/components/effortEstimates.ts", "sha256": "19057d4a4ba4673f6e8686c48f794e50c29d50d4a74a81a628002e52baf7903f", @@ -793,8 +1073,8 @@ }, { "path": "apps/web/src/components/history/DecisionDeltaComparison.tsx", - "sha256": "038348b58ea6460149f53a9dc322987d83819b6dc93428e1015e980d0897a6b5", - "bytes": 12632 + "sha256": "a6d9e9db834fb6a6fa5b702a886d8eb7c8843359ee77b4ee6ea4f9a4f0003c76", + "bytes": 12954 }, { "path": "apps/web/src/components/history/HistoryChangeSummary.test.tsx", @@ -808,8 +1088,8 @@ }, { "path": "apps/web/src/components/history/HistoryContent.tsx", - "sha256": "ff0614e1de16bfc8a34c5015a5f907a2196544ac57a78a8e762a6578947ef79f", - "bytes": 4629 + "sha256": "7b5163551625f05a9a1b49c9075f5a8186b4e9670c2240edd97c8a1540126a08", + "bytes": 4650 }, { "path": "apps/web/src/components/history/HistoryDetail.tsx", @@ -826,10 +1106,15 @@ "sha256": "7118c15559e63416f4665a60e375b60bf3c3a95e1daee60e339a41dc7155ba35", "bytes": 4511 }, + { + "path": "apps/web/src/components/history/ProjectReviewList.test.tsx", + "sha256": "269a2c42262c4a916af2006610b9b311e627f3204d02c611fab6bf5ff6318eaa", + "bytes": 2439 + }, { "path": "apps/web/src/components/history/ProjectReviewList.tsx", - "sha256": "ec99ea673cb1c5582c23d9148b71e5fdd59fd0260b7a8c37ed9aea9dd5248925", - "bytes": 3522 + "sha256": "da2d4d5276cc44d502f092af09371023a0e0a95abfe6e6e85d0315810ef2e474", + "bytes": 3753 }, { "path": "apps/web/src/components/history/ResearchHistoryWorkspace.test.tsx", @@ -861,21 +1146,66 @@ "sha256": "c50deb36bdbc718d8997b53e1faaaa2906367ba427b4b3e5df1b38a02347ff04", "bytes": 2165 }, + { + "path": "apps/web/src/components/history/historyReasonText.test.ts", + "sha256": "1b931370fc87d7ddc10c70a8843a1f1989994eb368f6f6bdf6009d81c6d7cbfa", + "bytes": 2686 + }, + { + "path": "apps/web/src/components/history/historyReasonText.ts", + "sha256": "6ac82e5a0d14cb2a428bcb91d8bdc63978839452c266267522181d2f2de97b31", + "bytes": 4220 + }, { "path": "apps/web/src/components/history/useRestoreFlow.ts", "sha256": "f3bc34423dce12301f50dd47af2b842d07f5700db42038c70ac375f8fb69f710", "bytes": 4569 }, + { + "path": "apps/web/src/components/hypothesisLinkText.test.ts", + "sha256": "2893aea491e6d5b52dc15ccfd5dab28fb207bab2c1ba027065e75231f048fd94", + "bytes": 6786 + }, + { + "path": "apps/web/src/components/hypothesisLinkText.ts", + "sha256": "c259ef47c230da218fab7098521561fee1c734fa4ed9d9725a6b2d6782f33a29", + "bytes": 4417 + }, + { + "path": "apps/web/src/components/hypothesisMatrix.ts", + "sha256": "589a0f72702b1246c25a6a36397048fe5a07a0baa528e709fd89672ab2ddaee4", + "bytes": 1311 + }, + { + "path": "apps/web/src/components/hypothesisReview.ts", + "sha256": "4a1bc1a4a3fb6a0608548451935559f59c8412090e97180051b7c0f054e83b89", + "bytes": 2493 + }, { "path": "apps/web/src/components/hypothesisView.ts", - "sha256": "4f4263a00a3e66e6de4d46bb62fe05c1ad02d9dfce4d4d1a3adffbfeb32f1791", - "bytes": 2728 + "sha256": "b44f62a2a2f4fea84b3c515770172f8c83243b78f24a6b425dbb4eed733890b3", + "bytes": 4319 + }, + { + "path": "apps/web/src/components/judgmentRecordText.test.ts", + "sha256": "ae34a9cf1424ca150cbe4e58404cf3ea88bced41b7a04d5863dcd1211062e5b2", + "bytes": 6263 + }, + { + "path": "apps/web/src/components/judgmentRecordText.ts", + "sha256": "e8e5ac47784c476d43db1194e171e179ba1ce377a018e2983c4ce571d44ed52b", + "bytes": 6051 }, { "path": "apps/web/src/components/judgmentReview.ts", "sha256": "eb5f09c056868f9063200cc58aead6d8dcffa5361930d6bde77e4e74db8ba982", "bytes": 3677 }, + { + "path": "apps/web/src/components/lessonText.ts", + "sha256": "1fa022fb4130a1f31b888afc02ad0e65c48e375b7168f850bde551e6ff686f0f", + "bytes": 2540 + }, { "path": "apps/web/src/components/localCredentialWorkspace.ts", "sha256": "e219b8695eb51419c4e502647dca6aa564516ec6baebdc0fb7b6435e30f9149f", @@ -893,8 +1223,8 @@ }, { "path": "apps/web/src/components/modelCatalogPresentation.test.ts", - "sha256": "b5d2fa2e8a1d3647de8fd04f166bbb0570099c5617b20a367977a30d7236f4ec", - "bytes": 2765 + "sha256": "dc8b0a7cc215d406c8c75c2bd13adc9aa0a161d9b8329185618a88bf7f96d55d", + "bytes": 2801 }, { "path": "apps/web/src/components/modelCatalogPresentation.ts", @@ -906,10 +1236,20 @@ "sha256": "56f9cd0351756f39bfcda34b57e0040319ec6783b9376ed4ca24efdae195199f", "bytes": 14523 }, + { + "path": "apps/web/src/components/plainWording.ts", + "sha256": "9c554b8bbdbcf71d8891b19181fa828d546d832b7069c30e9800bf691000c4fa", + "bytes": 5544 + }, { "path": "apps/web/src/components/resultUsage.ts", - "sha256": "27745fd9889d75c6a508473d6d7014e1840476127145b36922e76f5107affe9e", - "bytes": 2169 + "sha256": "b34ec4ebc8752787860137a031d58c3d91394f56fbc28f874333890cd9b2a7cd", + "bytes": 2477 + }, + { + "path": "apps/web/src/components/sameHypothesisText.ts", + "sha256": "f7064c73664fae42a38e94f585b1b37cb80d2b76edc5002b36f398e87f32fec1", + "bytes": 2638 }, { "path": "apps/web/src/components/statusLabels.test.tsx", @@ -918,18 +1258,98 @@ }, { "path": "apps/web/src/components/statusLabels.ts", - "sha256": "bfad2969efb8481fb8b369371f840c48963fdf43683cb50f8191e156220a24d6", - "bytes": 9480 + "sha256": "ae64b841f2787503a58fe55e44acc062651fd0c18612a4f5913b27dd98f62441", + "bytes": 9561 + }, + { + "path": "apps/web/src/components/testActionDraftText.ts", + "sha256": "2520b67f00f671415aa68d66bce40f30dd19eec889f409b07e4aeaba13008aed", + "bytes": 2669 + }, + { + "path": "apps/web/src/components/testOrderInput.ts", + "sha256": "79b5a497bd3958c44f228e13e0ba29d0f90389c4b89b1ae29d9a4974c57c38ed", + "bytes": 4608 + }, + { + "path": "apps/web/src/components/testOrderText.ts", + "sha256": "ee1a3b4fbad53df98ea53860ed498acdb255ea69392fb3cfcbbaeb52397c5487", + "bytes": 3219 + }, + { + "path": "apps/web/src/components/testRanking.cases.test.ts", + "sha256": "755293b520801ebd96aaaf67a6d488c8d8eeea9b107b06ac5745a1f940a7c649", + "bytes": 4156 + }, + { + "path": "apps/web/src/components/testRanking.ts", + "sha256": "66bc643f5fbb2ac6dd60c3e023483441b96312db351307596cba4aa24f8a9409", + "bytes": 7093 + }, + { + "path": "apps/web/src/components/traceDemo.fixture.json", + "sha256": "995aaac5187036cf265e3095fdb12ab1171d2988d2782a380d785d11f9c7b489", + "bytes": 157994 + }, + { + "path": "apps/web/src/components/traceEvaluation.test.ts", + "sha256": "ba2d696efbbb582c6bf62999191ee55eaf8ee41bd8e6ae3be0474a34124b41fa", + "bytes": 4692 + }, + { + "path": "apps/web/src/components/traceInvestigation.test.ts", + "sha256": "4148cd9b6c3bb786e1ea10ebcfee22165f7e1fc68f49b731a2737177651c4f2f", + "bytes": 5867 + }, + { + "path": "apps/web/src/components/traceInvestigation.ts", + "sha256": "b7b90efaf1ec70c1f6f8e20ead7cfed1da979f978fe286d44407141132b35657", + "bytes": 2930 + }, + { + "path": "apps/web/src/components/traceReasonTags.test.ts", + "sha256": "66a0f0106ddc48c058bf86568c93d439109496ae157aded6c85f479e6acc8da6", + "bytes": 8695 + }, + { + "path": "apps/web/src/components/traceReasonTags.ts", + "sha256": "c0d32a6c76b76013d6070e3f10ecc36df6747a1aa42ec0eaa5ed093f553293fc", + "bytes": 4814 + }, + { + "path": "apps/web/src/components/traceRows.ts", + "sha256": "7eb1c8d8ddcfe3e644d73823592d2b314a4165c1e65869f1a7261d69617cb3b6", + "bytes": 3160 + }, + { + "path": "apps/web/src/components/traceText.test.ts", + "sha256": "55cd011b4537a60c1b8c066946057e17ea23fe47553bf49ec867084f54c00114", + "bytes": 12760 + }, + { + "path": "apps/web/src/components/traceText.ts", + "sha256": "e2f61801dd224f443b1c67011c5308dad0c57da575a970a2c7bec6c9b8dc88fa", + "bytes": 21098 }, { "path": "apps/web/src/components/useConversationReadProtection.ts", "sha256": "0c42491f874c817b54ddedbf5b73ae96d5eac3095aedb39efb51fbd9db1a1c0d", "bytes": 1669 }, + { + "path": "apps/web/src/components/useHypothesisLinks.ts", + "sha256": "4643c90e50704c773761bcc2ba1267abaf3256ea45c8338de0fa89db12fd8b96", + "bytes": 610 + }, + { + "path": "apps/web/src/components/useJudgmentRecords.ts", + "sha256": "d421afc03aa13902c91dd102f3c04370b1b6d677623002c16eecdd36d1fad4e8", + "bytes": 1887 + }, { "path": "apps/web/src/components/useResearchSession.ts", - "sha256": "83d61ea7b65f5ef3988f98ffc5601a35819ea9f66b7a88948e35b5bd611f3477", - "bytes": 12956 + "sha256": "c486968d35a1b2402904804ea1d4e6ed66f67c17fc57d7e3cc98effdf43e51e9", + "bytes": 13513 }, { "path": "apps/web/src/components/useResultEvidenceProtection.ts", @@ -943,8 +1363,8 @@ }, { "path": "apps/web/src/main.tsx", - "sha256": "e1687822b3f6be4cb5854a5e38014fcc4cfc85644cf3002dad95cafef3029861", - "bytes": 562 + "sha256": "6f59adbfa6ce1c5c7b965d66c51c44bdcfb11c1bbb97d48150025d8ee8987b4a", + "bytes": 673 }, { "path": "apps/web/src/preview-timeline.tsx", @@ -973,8 +1393,8 @@ }, { "path": "apps/web/src/types.ts", - "sha256": "92d6352426103345e11be02356bef4bec0ff7ca0f185ea3934cd9788451fc27b", - "bytes": 4441 + "sha256": "d4eb64555b5b892b4dd3f405e37cd6d6e2b73345f4b32ce4044ef523d11b4eb8", + "bytes": 4633 }, { "path": "apps/web/src/vite-env.d.ts", @@ -983,8 +1403,8 @@ }, { "path": "apps/web/src/workstation.css", - "sha256": "5db0d589f58fe4b5addebb8a123a5f58c725b4f92937533417bdf47a0fb45d16", - "bytes": 55489 + "sha256": "98455faba74c66ddd2589f44e0efab2a9eea7bc2540ddd8bc76aa84b894ff070", + "bytes": 58207 }, { "path": "apps/web/timeline-preview.html", @@ -1013,13 +1433,13 @@ }, { "path": "config/architecture-conformance.json", - "sha256": "3a9bffceba18f92e3a6a5fb236f711709497ce5032acfd3872abdba38c18acdd", + "sha256": "1dbca6473a48c222d4ef94adbd455829d276c7de32561ad1225cdd932f199843", "bytes": 14544 }, { "path": "config/atomicity-paths.json", - "sha256": "61c8bffe0b051521da8a28e14c81b5fbb478daf52c63e0897da716214923e351", - "bytes": 3263708 + "sha256": "7942ea74409013ca29f7cfd7bb4eddeda8e24486af54465a9b93cc585741e1f3", + "bytes": 3325795 }, { "path": "config/atomicity-requirements.json", @@ -1073,8 +1493,8 @@ }, { "path": "config/module-responsibility-budget.json", - "sha256": "1d5078767c4d4b093bc02f6f5e4bb2f5a2d720ad6dc2daf8bd9a3bd7a979102d", - "bytes": 4708 + "sha256": "2f749619624fb524d5c34bc51d1b2d04e793f150cebee1c4d79e9ee2789895b0", + "bytes": 22053 }, { "path": "config/post-d4-baseline.json", @@ -1108,7 +1528,7 @@ }, { "path": "config/read-only-command-helpers.json", - "sha256": "cb23ecb0faec9c7164dac412ef664477e4a767d7261914ac1594a468a47cf762", + "sha256": "c479f24003306405f5da09a28bd4dd4057aad9aece3948bad06a94bc0155020b", "bytes": 357 }, { @@ -1148,8 +1568,8 @@ }, { "path": "docs/PACKAGING.md", - "sha256": "ebf8a1551d5baab8a0dad2d4c6ece9bc4e6c80c804e6f2d097e621760a0487a5", - "bytes": 3276 + "sha256": "c3d50793db537eb591f2ca16ca51ac5e4d0e4ab003a501195eab84233d268c8c", + "bytes": 3846 }, { "path": "docs/PUBLICATION_REVIEW.md", @@ -1158,8 +1578,8 @@ }, { "path": "docs/VERIFICATION.md", - "sha256": "be9abd852759fa06e9cb24d43105779ffb6a0005ebf2173cff73915259b84f7b", - "bytes": 24775 + "sha256": "6faf672d6effc10641bfc18c78231609e87ff2d440098af54a8f329600589e60", + "bytes": 25228 }, { "path": "docs/architecture/agent-execution-rules.md", @@ -1206,6 +1626,11 @@ "sha256": "3f74426481b9da04a81a94ac91ca3e3a247a5265e6c105c83afde635f0d70eff", "bytes": 444 }, + { + "path": "docs/architecture/memory-terms.md", + "sha256": "f8e20df1d4459645bf438415499dc31dfb88329ae9b56bfb314c0da8c79a6560", + "bytes": 6130 + }, { "path": "docs/architecture/model-auth.md", "sha256": "5d165dc379b3863faf39915c7930d40f479207c7c802ff2353efed01874405c5", @@ -1253,8 +1678,8 @@ }, { "path": "docs/architecture/rpc-method-catalog.md", - "sha256": "80a3b4b5f8a552a656b83ddf609285b3bf7be58cdd73cd08dc4dfb1e47c9d13f", - "bytes": 12269 + "sha256": "fbbf54d025d85e73ae93a32dc071de988ea87c5e554ea7461d58cdd434d06a85", + "bytes": 14166 }, { "path": "docs/architecture/user-activity-projection.md", @@ -1576,6 +2001,56 @@ "sha256": "87cd3d0205f11287df93b435d3b71418f252476ab87336d71ec2733852ef4e94", "bytes": 282 }, + { + "path": "examples/synthetic-radar-demo-v1-scorer/40_ASSESSMENT_DRAFT_SYNTHETIC.yaml", + "sha256": "2465713cb8d668b0647af70770ae7b5850095c9e296f2ed76d3f10dfcc0aeccf", + "bytes": 1069 + }, + { + "path": "examples/synthetic-radar-demo-v1/00_MANIFEST_SYNTHETIC.yaml", + "sha256": "7d3971c1b742de4782cda198861af52be6983cb71a56b500a064eb3f3c20710d", + "bytes": 1597 + }, + { + "path": "examples/synthetic-radar-demo-v1/01_SOURCE_LINEAGE.yaml", + "sha256": "6b0991c6b74e779d6809717717abbd1bf7ee5b681a3db971b3d76e283fbd3a2b", + "bytes": 727 + }, + { + "path": "examples/synthetic-radar-demo-v1/10_REQUIREMENTS_SYNTHETIC.yaml", + "sha256": "bf368e4e1391ffd76bcdce9b8a8ea1447da20a7cdac735aad337181802e714c7", + "bytes": 1569 + }, + { + "path": "examples/synthetic-radar-demo-v1/20_TEST_PLAN_SYNTHETIC.yaml", + "sha256": "5838eba3660b34b763e60019892f6070523414cd9c896744b3b9f7fbcd880b32", + "bytes": 675 + }, + { + "path": "examples/synthetic-radar-demo-v1/30_RESULT_DRY_SYNTHETIC.yaml", + "sha256": "77acaacd120bec4c30431bb32a204e183a83139111a231d2a21d89aef816914a", + "bytes": 676 + }, + { + "path": "examples/synthetic-radar-demo-v1/31_RESULT_RAIN_SYNTHETIC.yaml", + "sha256": "a0b0ffc698ef898a8e9fd6e426d1ec12ff995a59fbedb1f025d2e8873f677d33", + "bytes": 752 + }, + { + "path": "examples/synthetic-radar-demo-v1/data/30_DRY_TRIALS_SYNTHETIC.csv", + "sha256": "af10b01f1885b8d9147c88d609af0c428d205d69b151e2b29af198acf0fd18c6", + "bytes": 3684 + }, + { + "path": "examples/synthetic-radar-demo-v1/data/31_RAIN_TRIALS_SYNTHETIC.csv", + "sha256": "e915da8c2a225784a27ecf5c0d9cdb0aa59498cfb57285eb56c4b85f99173a98", + "bytes": 4224 + }, + { + "path": "examples/synthetic-radar-demo-v1/schema/synthetic-radar-demo-v1.schema.json", + "sha256": "91162758f0232e372703d5ecef6ede7102956df959728b8e1bdcd4acf3690996", + "bytes": 6804 + }, { "path": "field-validation/assignment-generator.py", "sha256": "377cfffb0f923d7bc32efc6b248f528c7a942658b56bec146243a0d474d937bb", @@ -1923,8 +2398,8 @@ }, { "path": "schemas/protocol/public-method-catalog.json", - "sha256": "e9c33b11d61e95b8946953dbc1fc7e792e4df4e6e99645971a9124b50222a7c5", - "bytes": 151175 + "sha256": "9c8e14688ccd2bc92bc2487213c7dd4fbe2cd625572a0673f1f2282a7b29e95a", + "bytes": 157952 }, { "path": "schemas/protocol/restore-apply-input.schema.json", @@ -2028,8 +2503,8 @@ }, { "path": "scripts/build_public_method_catalog.py", - "sha256": "d981178c33f3d55fe8cdf47fff1b2a165934d48fa72f0b01a9fbc794c121f1b1", - "bytes": 13716 + "sha256": "432303a50973b1b7265825b0587a929214ebf1f139b6a4c994e96106d313c384", + "bytes": 13815 }, { "path": "scripts/cancel_architecture_preflight.py", @@ -2068,8 +2543,8 @@ }, { "path": "scripts/check_module_budget.py", - "sha256": "7a90010afcaaad5cff0adabe8941fa06f5075badb01b0e374fe5d8d163355aac", - "bytes": 5233 + "sha256": "b8bacdf729f183be2a633cad55d00ceb9520e4a6acef4830065953ad8d85bb54", + "bytes": 6626 }, { "path": "scripts/check_ocp_extensions.py", @@ -2121,6 +2596,21 @@ "sha256": "3b7212b126c2a076c6b4cea8db292617db6b32eae9ddeca22a6d184031d3b007", "bytes": 8909 }, + { + "path": "scripts/hypothesis_v3_metrics.py", + "sha256": "a574d983b96f3b1d59705bf81ba87e1b1a868b06ff63c9fb234f478b3538f73f", + "bytes": 4257 + }, + { + "path": "scripts/measure_project_lists.py", + "sha256": "33cab9f55066e51d3b4525b9804b06338183c26dde698f1d9a4b725c4d49c9e4", + "bytes": 14992 + }, + { + "path": "scripts/memory_eval_families.py", + "sha256": "85009785316894e9bd72ee01417f4ae0d374a722bed966e66b24f055b682d68c", + "bytes": 15420 + }, { "path": "scripts/memory_selection_metrics.py", "sha256": "1d320dd98ef0b6352630d0917f5ea45b9671f8550088649a4ca106b67ea0c759", @@ -2128,8 +2618,8 @@ }, { "path": "scripts/memory_selection_replay.py", - "sha256": "afe635ffa9cd083778e86f78b0a83c3bf43e13fdfd97ef52c3d8884b3c203368", - "bytes": 11777 + "sha256": "271d6d70216baef6b6ded991866646f8cefdd3f2957b755ff0439b10ee59a877", + "bytes": 18509 }, { "path": "scripts/parse_shell_units.ps1", @@ -2146,6 +2636,11 @@ "sha256": "4d0851c3ea5f060849f4cdc1fc6893d56ef74b6916941b9ec82b44b10b53b674", "bytes": 758 }, + { + "path": "scripts/prepare_synthetic_radar_demo.py", + "sha256": "6bd1b826d5f082231a8b2f51568a8426a92cba466d59b1498754dacf93adab86", + "bytes": 16228 + }, { "path": "scripts/probe_source_files.py", "sha256": "e326cc75f962088fc01b67a10332fdcfbaa7a8dc934eb778fa30de45f36d8066", @@ -2193,8 +2688,8 @@ }, { "path": "scripts/rule_candidate_contract.py", - "sha256": "312cf20d9c265c35742576c048178edf9ca574d4c1f251e4da03162accaf77ba", - "bytes": 13542 + "sha256": "16e587b4efe38f6ebda37c972b568b0195f1c3867c896a6af187183566a9ae6b", + "bytes": 14097 }, { "path": "scripts/rule_recovery_contract.py", @@ -2226,11 +2721,21 @@ "sha256": "d382667d8f243d20b72bfc0187a61d99dea62590c16d2b9aafea718a1ef33491", "bytes": 3172 }, + { + "path": "scripts/update_source_manifest.py", + "sha256": "56d55a84f9b02a6357efc135d4335f0aa8fa7a67af2b65e2b41e984e70db9d07", + "bytes": 5635 + }, { "path": "scripts/validate_field_kit.py", "sha256": "bfde97e1ec2c59ced001b66621f0568b89c50ee2920fdeeda0a00ffea4360459", "bytes": 4441 }, + { + "path": "scripts/validate_synthetic_radar_demo.py", + "sha256": "aadb90c05a48cd57bc74a4eed8344422ece4e22aece4fe96eb76a390688ffc1d", + "bytes": 15142 + }, { "path": "scripts/verification_bundle_contract.py", "sha256": "18470bf04a2d092cdaf3e08fd1c383b4dcd487dec398b084db6975c7f3d4555c", @@ -2443,8 +2948,8 @@ }, { "path": "src/thoth/adapters/http/app.py", - "sha256": "bc41f0e628369a69a74c6b0fb15065e1357945cc93aa5972044d15fbc62e8e57", - "bytes": 33472 + "sha256": "cff629301a9114ee30b6f4faf24a63f0776c5b25067c7a71613c073caafad68c", + "bytes": 33527 }, { "path": "src/thoth/adapters/http/hosted_review.py", @@ -2513,13 +3018,13 @@ }, { "path": "src/thoth/adapters/models/catalog.py", - "sha256": "2ac2524024456caf9dce1a106b935fb282dff038454b1532e7a214f0f7b7a711", - "bytes": 6411 + "sha256": "eb8291ff4afc71d351f04664b587eb9208977b8870e540fc1573c34df801d3cd", + "bytes": 6594 }, { "path": "src/thoth/adapters/models/catalog_marks.py", - "sha256": "9b8aa5ab97e94392a0f66b0ea5da7d9392124bcf9cc49f4c092248d230a40d3a", - "bytes": 3895 + "sha256": "384bd3fb6dff229a16f209e3c8f1cc2709bf2e4d488cb811de488ea82b0a677c", + "bytes": 4006 }, { "path": "src/thoth/adapters/models/catalog_startup.py", @@ -2578,8 +3083,8 @@ }, { "path": "src/thoth/adapters/models/codex_broker.py", - "sha256": "19b677314272b6dd2f5d58016fc82f5db186bc543278a656ea11ce3f1c13a535", - "bytes": 35870 + "sha256": "8400d588fc5c3ce46deb20587882a3f61e5d9970f05eaa468f4688968381be45", + "bytes": 36701 }, { "path": "src/thoth/adapters/models/codex_http.py", @@ -2588,8 +3093,8 @@ }, { "path": "src/thoth/adapters/models/codex_oauth.py", - "sha256": "2f006ff6d5d18eba5ad1a1b3d4bfa4c86e75e1ca68dd73d2118489f95305385c", - "bytes": 24243 + "sha256": "b6b3f8e25ebe72dba61efeda446e15dbb0cde4ec53e73009be36c557bea1ed1b", + "bytes": 25255 }, { "path": "src/thoth/adapters/models/codex_profile.py", @@ -2611,6 +3116,11 @@ "sha256": "c084c5c204216cfaca12b321e996ab4164421dba8accf0bdfe18df3e8067762b", "bytes": 4751 }, + { + "path": "src/thoth/adapters/models/hypothesis_contract_text.py", + "sha256": "db73fcf68d6a26150e7f2ca2ae13ba9736b09e6f0e502cf043a60f2d9e4ba771", + "bytes": 1517 + }, { "path": "src/thoth/adapters/models/local_credentials.py", "sha256": "5e20ef8a84ba9c6bdd513dc5c206f03c24aba96569ec21aa60dc5cc692c1f4c1", @@ -2618,8 +3128,8 @@ }, { "path": "src/thoth/adapters/models/openai_responses.py", - "sha256": "db9017b1b2d2ed815c8bbb70e3828914c1d030ee28f5b4c314d47871ec253904", - "bytes": 12675 + "sha256": "97b6866cc6db523b0d1baf8d00de6a44d87376ea8b3a017a6082449ca9ed0b4f", + "bytes": 12723 }, { "path": "src/thoth/adapters/models/receive_stats.py", @@ -2721,6 +3231,11 @@ "sha256": "562f64aa112c1b4bffa5ed4a1d89a61a71156e7dfa10c7427494a28ad7d51ac3", "bytes": 1584 }, + { + "path": "src/thoth/adapters/parsers/docling_locations.py", + "sha256": "a8bb0c797895fb0f88a9d14df717f9cdccf8745d63b87dd3474118566f3fb076", + "bytes": 4550 + }, { "path": "src/thoth/adapters/parsers/docling_pdf.py", "sha256": "d9023ceb8962552cb1bea84d57548b50a7bdbc59159ffbf01a6979fd71618d44", @@ -2728,8 +3243,8 @@ }, { "path": "src/thoth/adapters/parsers/docling_structure.py", - "sha256": "964f6cc96fdc94be6c5f76fd76e8bdc86806ad682e4ebc6274d0e0a8912be9a1", - "bytes": 23795 + "sha256": "f8f68263c7cac797ac52e40d53adbc3eeb7ba187e7c1d958ddb6b667b45d1a87", + "bytes": 19561 }, { "path": "src/thoth/adapters/parsers/docling_worker.py", @@ -2788,8 +3303,8 @@ }, { "path": "src/thoth/adapters/parsers/text_parser.py", - "sha256": "68357f648748b636d5ec09f23dbf1ee7223023156c487bf05f824e126698c539", - "bytes": 1674 + "sha256": "bad3ccb07b9149021931bc1e51c54eb5f2a174135390f07e76a3992dd525d521", + "bytes": 1780 }, { "path": "src/thoth/adapters/parsers/xlsx_parser.py", @@ -2898,8 +3413,8 @@ }, { "path": "src/thoth/adapters/storage/artifacts.py", - "sha256": "b9b74afacbeac94be3be1e7ee6ccee3b6ad242d2261eb487ec5ca9f46b06844f", - "bytes": 25528 + "sha256": "14b37c4dc11a1afcc43d7c18f405048c5e5f3755e044bbe528d34fa93e3d0a83", + "bytes": 26931 }, { "path": "src/thoth/adapters/storage/auth.py", @@ -3156,11 +3671,21 @@ "sha256": "4690d507f942d1fa997d6704c4f36bf935a5e1b2bbf94776088b9d4f88c6aa1c", "bytes": 2462 }, + { + "path": "src/thoth/application/commands/action_create_input.py", + "sha256": "dae2b9f6731a5a7357fbb218bdf2957b63e1533aca420042a9828e9319ab1de4", + "bytes": 1154 + }, { "path": "src/thoth/application/commands/action_effort.py", "sha256": "e64135390ff81db1d6fe78adb9ddb031e1d4f90994e11b3ce551f1949de3b4e0", "bytes": 5759 }, + { + "path": "src/thoth/application/commands/action_from_test.py", + "sha256": "1043baefdc839537a23df5f6393905adc1b466df047220a6af3751f7a47f195d", + "bytes": 2257 + }, { "path": "src/thoth/application/commands/action_generation.py", "sha256": "354607b3b15832aebc26439404d95355299b40e9abc81ce9d0f66f90ca6375a2", @@ -3168,8 +3693,8 @@ }, { "path": "src/thoth/application/commands/actions_full.py", - "sha256": "f692efbdc92f9ed050d3b06edb8f13f828009ebe75f389c0acea070599f678d3", - "bytes": 43451 + "sha256": "dd20189a83b7d08d614e6f9e81c68324fa46b7c2cdbcc42a64d1104cf03e99f7", + "bytes": 43082 }, { "path": "src/thoth/application/commands/behavior_execution.py", @@ -3186,6 +3711,11 @@ "sha256": "8b951a3e2c680eedc98569a2dc82245101ba464761120155e4ade3851dd469e0", "bytes": 18215 }, + { + "path": "src/thoth/application/commands/discrimination.py", + "sha256": "b235f1567ef97a5f3b981372ed023bc93d6e0e5993e5ed85a8de35e126b715a9", + "bytes": 4661 + }, { "path": "src/thoth/application/commands/evaluation_runs.py", "sha256": "42f4c4300be3db9db1784108a8d8884a69a4ee03097cdf99bc3615819e32cb22", @@ -3198,8 +3728,8 @@ }, { "path": "src/thoth/application/commands/executions_full.py", - "sha256": "c91847cfba4153cefb109b2ce3c4e11311eab8e39772409ea13c27debb8a5aeb", - "bytes": 25447 + "sha256": "4f3659b743e04e6b7048136ba5729f343a877467cd2291704cd5c512a355cb04", + "bytes": 27672 }, { "path": "src/thoth/application/commands/export_full.py", @@ -3221,6 +3751,16 @@ "sha256": "85a02deb48e6f9535b02bf267214c03b78583d9babd1afad688b6700edfe7f40", "bytes": 3687 }, + { + "path": "src/thoth/application/commands/hypothesis_link.py", + "sha256": "b7e241b02ce5d6cc09a928940ea36dde1a1291fe35ce674dcbbcbdd341604b92", + "bytes": 5335 + }, + { + "path": "src/thoth/application/commands/hypothesis_same.py", + "sha256": "1a36c98d448299fbdeac1fb7acff6b39d1a060beeec7b0f3b99a1cf96e1fa06e", + "bytes": 3068 + }, { "path": "src/thoth/application/commands/improvement_full.py", "sha256": "69f990ea27a2578a72969003d4edbd52acf7f0b89e2065de7942df5d48fa86b2", @@ -3236,6 +3776,11 @@ "sha256": "884b79a0fe284512c9400f847bcade7f7b6bcb3f2e4e71e318dc8a8dc2a7283d", "bytes": 5631 }, + { + "path": "src/thoth/application/commands/lesson.py", + "sha256": "b89caac68fbc38ead76b0598184daa66e19ec5d091a4cfc5c6d8391928c2a592", + "bytes": 1586 + }, { "path": "src/thoth/application/commands/lifecycle.py", "sha256": "00e2dd553e92c6c003ada253961ec5a3e7ec8a8b5c672eb1ba035be8118fa5b8", @@ -3258,18 +3803,18 @@ }, { "path": "src/thoth/application/commands/memory_settings.py", - "sha256": "dc9e8cd32b199d72a3ee2734913466d653e8efe7905713ad0893edd1f116283a", - "bytes": 2283 + "sha256": "18bb389c9cddf869d34a71853c9d97cb060ea7cc705219e9ad01e3224b6ff859", + "bytes": 2577 }, { "path": "src/thoth/application/commands/model_call_settings.py", - "sha256": "6aa809af8c37f53432cffb4e7ec7f838f56ab9bbcf6e9d4be4117f3c05c92f70", - "bytes": 2376 + "sha256": "5108292b3fc8270144b697cb9fb79a55472fbeb3410013fdb99997ce1bb2577f", + "bytes": 2623 }, { "path": "src/thoth/application/commands/model_settings.py", - "sha256": "114b13d9d77a8f3ceeb5f53a88a4451b8f1b3ab19e8b770ead37370c2ed30b93", - "bytes": 18176 + "sha256": "5ccd95a30f7adfda9d3b34e7292c05c07601d8fe934e153f193924e3930fa15f", + "bytes": 18612 }, { "path": "src/thoth/application/commands/objects_full.py", @@ -3331,10 +3876,15 @@ "sha256": "9b5b958e22330c275ad116dcac5f1eb4c8a460d494545cab860a60e9f4759a35", "bytes": 1448 }, + { + "path": "src/thoth/application/commands/research_resume.py", + "sha256": "03e28fbc3622ce57c4730368777b65d27ca7cd13192050c2ae20526eb96068b9", + "bytes": 2158 + }, { "path": "src/thoth/application/commands/research_threads.py", - "sha256": "0d3934f2240ebc258a12e64bde5c45e0b08a2cb0f0d72a90396c0707eec734a2", - "bytes": 41398 + "sha256": "92df1f6d92b896dc25aaf81a53c9a59213e00fe188033c9fc1e0655bba8f991b", + "bytes": 40141 }, { "path": "src/thoth/application/commands/resource_scopes.py", @@ -3368,14 +3918,29 @@ }, { "path": "src/thoth/application/commands/thread_analysis.py", - "sha256": "0afe439a2049a328b81fd963c1d8bcdc6ec2791ad4e0180c39f52a380fd150ad", - "bytes": 30993 + "sha256": "02b84fc1e8eb7007fa3140ce5a9a8e2e100ddf9e66420dbc08897af777a75c9c", + "bytes": 31828 }, { "path": "src/thoth/application/commands/threads.py", "sha256": "4f35f68895303a83eb7a90369a70a30f949d6c62d86a54f9fd023527710c666a", "bytes": 22112 }, + { + "path": "src/thoth/application/commands/trace_closure.py", + "sha256": "f29ce2d57d888ed094632bc6a649c5f8215ce2f381ac2a9a7e7a9e1541bad86a", + "bytes": 3810 + }, + { + "path": "src/thoth/application/commands/trace_origin_admission.py", + "sha256": "363ca63748a7b5dee6cb26e2eddc3c3c71a7425d3cceaf431c0158238bd454e8", + "bytes": 5583 + }, + { + "path": "src/thoth/application/commands/verification_trace.py", + "sha256": "b43f422b0bba3a504b93c33506a463a75610a00cc3b7521508a132220d6e82fc", + "bytes": 8445 + }, { "path": "src/thoth/application/commands/workspace_setup.py", "sha256": "36ea070332ff8e842f0a01bdcef8a9aeef504fb77499b3221954edcd280bca86", @@ -3393,8 +3958,8 @@ }, { "path": "src/thoth/application/reducers/authority_router.py", - "sha256": "624beeb59a8e710ffdb281c289e2eb8bd4ca0fd38e122276a01d6222e9bb0e80", - "bytes": 2645 + "sha256": "ffb2c054c780bd37644ace57884a7a0bfb923b64601947a04a648686d7b3d48e", + "bytes": 2537 }, { "path": "src/thoth/application/reducers/hypothesis_reducer.py", @@ -3431,6 +3996,16 @@ "sha256": "94552220050ae1cfb4b957b04db747d1b517efda92c08af1fd83c9c7eb006b21", "bytes": 2394 }, + { + "path": "src/thoth/application/services/action_effect_vector.py", + "sha256": "fa2bfee8067becdb49160d7d7efb636641b1545e93b7aba0ba00a28a8d1c3ee3", + "bytes": 1512 + }, + { + "path": "src/thoth/application/services/action_from_test.py", + "sha256": "7a398ec22200d2f86bb70f576c5303422521b0bfee404d2937febdb60db4ef23", + "bytes": 2349 + }, { "path": "src/thoth/application/services/action_plan_validation.py", "sha256": "db6c44eb8904ab44476affdad35ad533c5cf5c84c44d44afd5fc83145b693a6b", @@ -3443,13 +4018,13 @@ }, { "path": "src/thoth/application/services/action_service.py", - "sha256": "712dce96ece381969ed94f051b3bf6215358896039019464e95901ce1ccba8d9", - "bytes": 38592 + "sha256": "0a473d599705ffd600591ca196f213e69010c47d1b9571960e7b76fab0a1018d", + "bytes": 38248 }, { "path": "src/thoth/application/services/authorization_consumption.py", - "sha256": "6c0c252d8bd7405407aa52c5a6ea34d66677b399419a78fc4fb93450b609ba99", - "bytes": 8436 + "sha256": "6349f523fd5d02496797e8f6d59c17329f14cb4604e26abe74846cad08ae3d3d", + "bytes": 8913 }, { "path": "src/thoth/application/services/authorization_currentness.py", @@ -3581,6 +4156,11 @@ "sha256": "48755d7970f68d78c91394f3cbfaed235f871b5f00e85d257c1c75081c496d84", "bytes": 26223 }, + { + "path": "src/thoth/application/services/discrimination_ledger.py", + "sha256": "20e6883297f0a202a332e4a7aec7310bd242e9abe544c01c76c0e0b4c76d065e", + "bytes": 6534 + }, { "path": "src/thoth/application/services/domain_reference_memory.py", "sha256": "1fd0f1a6f872c4d24eff55fe6a3a576c7ba85e190f586a245399a9c0b03ea780", @@ -3616,6 +4196,11 @@ "sha256": "cda06b6990ac7dc62f7d6684ba7226a3a98f14065379578a1e7e3ccd4edbd78a", "bytes": 3003 }, + { + "path": "src/thoth/application/services/execution_pending.py", + "sha256": "adf35eb59c4b5f3230a16a69275af14d20fba3c748940bb72c8dd94be30ed972", + "bytes": 3551 + }, { "path": "src/thoth/application/services/execution_persistence.py", "sha256": "a4d8e7835d9e41811c32172f0e16fcd788c3fe54a442d88a4cfb7a1a1b6400be", @@ -3653,8 +4238,8 @@ }, { "path": "src/thoth/application/services/full_project_memory.py", - "sha256": "06d73927446bb67d4c17eb8c1e647a2e7e4601e147930201530c4dd9049e87a6", - "bytes": 32425 + "sha256": "0e43e5d7b4f05594a369ac6b3cb0f69cb8f6b05bdc815fd0e0bdb02acd84475d", + "bytes": 34752 }, { "path": "src/thoth/application/services/historical_access_verification.py", @@ -3686,10 +4271,35 @@ "sha256": "3ffe64328f3e6daf46cd91d1488ba004205f65cf0be2c9fad5bdc3fda4950b23", "bytes": 13077 }, + { + "path": "src/thoth/application/services/human_actor.py", + "sha256": "33958d6ca847b3819d100001b61cd0d29c4abcf9e77d56dc26bec81a557c4594", + "bytes": 395 + }, + { + "path": "src/thoth/application/services/hypothesis_contract_v3.py", + "sha256": "431c17f9b5d48cea5957dd39ef3668a2b64a9624fdd8dbf9bf36424d95a97318", + "bytes": 4776 + }, + { + "path": "src/thoth/application/services/hypothesis_link_recheck.py", + "sha256": "856f207005c9daf852b0d69d9d4635ed34593edfad883e3ab849416c0cc771f1", + "bytes": 4803 + }, + { + "path": "src/thoth/application/services/hypothesis_link_view.py", + "sha256": "7cdc2e84198668d21e151ac1bcc8c0f5d27db24f8be9621d2e4eeb87f82fc071", + "bytes": 10998 + }, { "path": "src/thoth/application/services/hypothesis_projection.py", - "sha256": "766b29cb14348a2181e8b9249c4719716e28d6c86fe795c4b8ef0e0e1b4be668", - "bytes": 11120 + "sha256": "fc6008fd9d4973bd20fb4fadf35a4f614058e4f5fef57c16cc579daa7e7c4f07", + "bytes": 12020 + }, + { + "path": "src/thoth/application/services/hypothesis_same.py", + "sha256": "ae6c6cdd96c730dc8afb03b9d6f20da2abc2c4487316b1324247d5a69046e96d", + "bytes": 4537 }, { "path": "src/thoth/application/services/hypothesis_service.py", @@ -3698,8 +4308,13 @@ }, { "path": "src/thoth/application/services/hypothesis_test_lifecycle.py", - "sha256": "7087b6a16a825a0bbe3944a20e7908adf50fd0ace8237bf2b822fdd12f115d13", - "bytes": 17112 + "sha256": "db0df0b9a151159fbf4d9c3d8bbde40c7d190888e6005a988fb2aa7fe8f69e4e", + "bytes": 17179 + }, + { + "path": "src/thoth/application/services/hypothesis_verdict_link.py", + "sha256": "3b49c96f6c6c3487a54f96e3a677936c1087fb7dba19f1e80a7d28d4c76fd5ef", + "bytes": 1191 }, { "path": "src/thoth/application/services/improvement_candidate_generator.py", @@ -3738,8 +4353,8 @@ }, { "path": "src/thoth/application/services/ingestion_service.py", - "sha256": "77c00f5941d8521048ea66c234cc4fd964ee9aa7f91b66afd540a5da69a73c81", - "bytes": 10746 + "sha256": "fae58ce4c8585435d60970d8e477f07b618589ad11e3eabb911e6473e0b99fed", + "bytes": 10848 }, { "path": "src/thoth/application/services/investigation_service.py", @@ -3751,6 +4366,11 @@ "sha256": "6a239edb3aae354a88b6e76b5abcb281ffb4b6e44c2fad7bf21344d5017df684", "bytes": 12122 }, + { + "path": "src/thoth/application/services/lesson_ledger.py", + "sha256": "8ed6eac411ee5497aad7ec316db693a6f93815f87e4c57e8a947dc863f84a0aa", + "bytes": 18280 + }, { "path": "src/thoth/application/services/local_auth.py", "sha256": "7362a99218b04c4e1feb6bd27a3d4bbb670842c485a7a8b1409737dde8289c0f", @@ -3758,18 +4378,23 @@ }, { "path": "src/thoth/application/services/memory_admission.py", - "sha256": "6c10258d3629d87a50e2199eb770450e8f42e89544aa458cb4fe9990299e0e6e", - "bytes": 5715 + "sha256": "167024cf09c9613cad4395e34d31ebb5d44b5cb84af8ab2e1fc6f5241f6d7775", + "bytes": 5759 }, { "path": "src/thoth/application/services/memory_edit_service.py", "sha256": "873accda9e1bd333c0be00c4a40d52485937226a3c829ce3b4cb5fa306113922", "bytes": 10201 }, + { + "path": "src/thoth/application/services/memory_query_expansion.py", + "sha256": "cceae7a585e871f10f1ec8f0c2520c8934f5b168b8224450486897cb07d9a519", + "bytes": 6093 + }, { "path": "src/thoth/application/services/memory_recall.py", - "sha256": "eb1d0d0595bf02487e75daa702b835ce31f1b96a6af7bcd5e3618701b7faf29a", - "bytes": 10112 + "sha256": "843f198839ffc7a9356d3284bcfe2537f532a3be48024c1736ead4d62fb8a7d4", + "bytes": 14295 }, { "path": "src/thoth/application/services/memory_relation.py", @@ -3808,8 +4433,8 @@ }, { "path": "src/thoth/application/services/memory_settings.py", - "sha256": "56e5520628bf59183f26ba2071f760f11795e90691a32065302a7f43e9223b5e", - "bytes": 1787 + "sha256": "003b5b907b6cbc6341b0fee8a7fe43e37eb793ac5d0598f1a57f311a34318cdc", + "bytes": 2496 }, { "path": "src/thoth/application/services/memory_shape.py", @@ -3828,13 +4453,13 @@ }, { "path": "src/thoth/application/services/model_call_settings.py", - "sha256": "1a48056e887b05517abb092297f2504d3882b4cc5b49cc518944aeb4b106c389", - "bytes": 1807 + "sha256": "670a22ec670d7801965635c378c4b7078427cde0fadf5f3a80ff5f19486208f6", + "bytes": 2608 }, { "path": "src/thoth/application/services/model_outcomes.py", - "sha256": "a2ed2630d1dc52e1566606913b8b6dad97e76d65b9e8b9c87acdc42fecd29b43", - "bytes": 1337 + "sha256": "9f83bb86686548ff4d46f6050c917c21e2c7b58556569cc0a4ee8955d1754223", + "bytes": 1373 }, { "path": "src/thoth/application/services/model_settings.py", @@ -3903,8 +4528,8 @@ }, { "path": "src/thoth/application/services/r2_closed_loop.py", - "sha256": "38423886807b0b45c9ce23ac5af9dfbad1dca62b3cb8aa732e074ea135bec6f1", - "bytes": 36572 + "sha256": "d6851d3fcba5cefd9b6d96d42dd9e8ce0029d5b4814264932238be28d394349e", + "bytes": 36798 }, { "path": "src/thoth/application/services/r2_outcome_projection.py", @@ -3918,8 +4543,8 @@ }, { "path": "src/thoth/application/services/r2_test_lifecycle.py", - "sha256": "284db3ca0b70b4038d5bbf9bda4c63e426e301d8654f0b4bbb9564be1eb9d61f", - "bytes": 17187 + "sha256": "1d983938c0a615be674f6c7362877c5f6a333e72ed82ac97403bfffa84d8bde4", + "bytes": 19645 }, { "path": "src/thoth/application/services/receipt_dag_service.py", @@ -3953,8 +4578,8 @@ }, { "path": "src/thoth/application/services/research_analysis.py", - "sha256": "141354dbdd7c800a811b9d7597883b517df7f1cc18b3074a1abea3ce11defb56", - "bytes": 37200 + "sha256": "afeaf7a6bc9b37fda795acc03568c96fbe649848de0b5ab66017305de519f8b6", + "bytes": 38372 }, { "path": "src/thoth/application/services/research_attempt_admission.py", @@ -3963,8 +4588,8 @@ }, { "path": "src/thoth/application/services/research_attempt_runner.py", - "sha256": "d0844b96da1730580ed0d83b5cef554eba3ce98b99bc5707100c5f9fb60dcaff", - "bytes": 18247 + "sha256": "04c8f2cc9dd5dc2bc5ae81f7136a2474cc79cb7dbb99724b1bb706d742f55c92", + "bytes": 19042 }, { "path": "src/thoth/application/services/research_basis_capture.py", @@ -3973,8 +4598,8 @@ }, { "path": "src/thoth/application/services/research_boundary.py", - "sha256": "360c2cc573b8edc1e7632b0ce62a23667fe604b81bff704a699ce6a8fdd695ff", - "bytes": 12350 + "sha256": "4ed0437bf47aa967159b606875a296278869b9e1f39d61d57da2e95f0db31042", + "bytes": 12406 }, { "path": "src/thoth/application/services/research_commit_scope.py", @@ -3983,8 +4608,8 @@ }, { "path": "src/thoth/application/services/research_completion_view.py", - "sha256": "1f4fe1bdf2c0c0cef94323f442f1bffd6a923614ac0ffcc9c59fce593ead8711", - "bytes": 5885 + "sha256": "8123cf472f073574f99a785442ef576b3e4b35bdee8bedb7bb08e7cd05616de2", + "bytes": 7233 }, { "path": "src/thoth/application/services/research_conflicts.py", @@ -4033,8 +4658,8 @@ }, { "path": "src/thoth/application/services/research_freshness.py", - "sha256": "6a3b3ef91297d844ac406f307ff6d8a13f5777ee3e1194a9dfeb1fa8e91df149", - "bytes": 8083 + "sha256": "6751ea0cb5388c6a347b1db3935249b9149f44bb0edb97d3793aa65b8b31a36b", + "bytes": 8526 }, { "path": "src/thoth/application/services/research_gaps.py", @@ -4098,8 +4723,8 @@ }, { "path": "src/thoth/application/services/research_retrieval.py", - "sha256": "c0722d106c89d0bbce73bc4ad45d183e3f1b7ac9a862a227505fa8b4c947047c", - "bytes": 5872 + "sha256": "b29b702726452d1c7d9a6c2eeedd4541c318492b8b76e572a17bcaf7dae4f409", + "bytes": 6167 }, { "path": "src/thoth/application/services/research_retrieval_policy.py", @@ -4133,8 +4758,8 @@ }, { "path": "src/thoth/application/services/research_usage.py", - "sha256": "ca8fc08ab99f6467110977570e128c90e483cb5a21b3912c90ca336019769e49", - "bytes": 5650 + "sha256": "cef8970dfb8051f3b967c99394d4712ae49cda35e79f88cd4d4bfb897341abe0", + "bytes": 6740 }, { "path": "src/thoth/application/services/resource_scope_context.py", @@ -4173,8 +4798,8 @@ }, { "path": "src/thoth/application/services/restore_planner.py", - "sha256": "498bccdbd64aa553015174ef059c4e3dd76bf1a28f5a0b5a085bfec208cf343e", - "bytes": 15533 + "sha256": "53c37d49579c5541b2d8f87ae607b13ad2b0ee71226a39a0443ed998295528c1", + "bytes": 15577 }, { "path": "src/thoth/application/services/restore_profiles.py", @@ -4188,8 +4813,8 @@ }, { "path": "src/thoth/application/services/restore_references.py", - "sha256": "b165f106e8f413b748cc30bcccb66d34628ffbe79955ec94e959ef5403571115", - "bytes": 12371 + "sha256": "c0692209b8b38cce8178f8e51f2909e9328288dc87c7648b1e5d7d5b864110d8", + "bytes": 12472 }, { "path": "src/thoth/application/services/restore_service.py", @@ -4228,8 +4853,8 @@ }, { "path": "src/thoth/application/services/sandbox_service.py", - "sha256": "7f698be416eb0e5b186dc3a41c91c04b5a3b507fd8394db73224639bd355bed2", - "bytes": 25759 + "sha256": "669690aa50057b4bb208b360381f12f84bf7356f25ca59cb50382b75bd11d3bf", + "bytes": 25937 }, { "path": "src/thoth/application/services/scoped_artifacts.py", @@ -4286,6 +4911,26 @@ "sha256": "6610c70c17fc3f58b0bfb0b33149762c587f97e35b51b8d998f08202bc995b47", "bytes": 13251 }, + { + "path": "src/thoth/application/services/trace_closure.py", + "sha256": "b00d0aa97577502594f4f7204a20895bbf2193ede1418fc6ef439cfaa926d8d8", + "bytes": 7249 + }, + { + "path": "src/thoth/application/services/trace_closure_text.py", + "sha256": "5796be61c3f573dd99e3f674a6da0e9cd29392fb67baded9d32cfadb4386f9c3", + "bytes": 3173 + }, + { + "path": "src/thoth/application/services/trace_csv.py", + "sha256": "1d68ee34849521b2ad2a70a3b41c63dcab7c9527491b23ecd13b4d4edcb2b0ad", + "bytes": 20793 + }, + { + "path": "src/thoth/application/services/trace_origin.py", + "sha256": "20adc502d08972447b2b547a20004bb9836790ef397e17853eb3c7932fef83ef", + "bytes": 5008 + }, { "path": "src/thoth/application/services/tui_session_service.py", "sha256": "44d77a832ff1d5cf75dc8b6a4d50b1bcc3d9fd0deab92496ec1aa87bb1d5c709", @@ -4296,6 +4941,21 @@ "sha256": "0fa3113724bd44419c37687e3170238a0aa8dfd8ac5fa6aac6786597d4d4f5a8", "bytes": 27584 }, + { + "path": "src/thoth/application/services/verification_trace.py", + "sha256": "f6eace83496b0a3fb55f4d6e79352ca0998648fb1e93586fb9f49b3e24d631fc", + "bytes": 29838 + }, + { + "path": "src/thoth/application/services/verification_trace_import.py", + "sha256": "33547f79b74aabc3b15767ab88c7cbdeb067dd23961daeeb63eef47ab8e95411", + "bytes": 14275 + }, + { + "path": "src/thoth/application/services/verification_trace_view.py", + "sha256": "76dcc3a3088387f463a1d33dfc1ac0c13ef49dc1173a2b6e84f9480e0e4f1be1", + "bytes": 2774 + }, { "path": "src/thoth/application/workflows/__init__.py", "sha256": "a43820b3b30c248d2d15c9076e32ade569395ad2bb2e206d8ecd1657f18d621d", @@ -4308,14 +4968,19 @@ }, { "path": "src/thoth/application/workflows/thread_cycle.py", - "sha256": "b9c3bfd4e402377972a060d3be6c03baeee18a85423739592fa78d03ad6b9e2f", - "bytes": 38623 + "sha256": "f17d64a7703f05e3cf0d9e689796bf6b2be7f5433176f6bd976d725b84049ef1", + "bytes": 38736 }, { "path": "src/thoth/apps/__init__.py", "sha256": "6790546745ad521449c3a982d74a92dd02173d3e374191713744bd9268be2ce6", "bytes": 319 }, + { + "path": "src/thoth/apps/action_draft_composition.py", + "sha256": "f3071a1772c2e0ec30723c8694ce6a49ac01b19f2fa27f729b64c14b2fac4d0e", + "bytes": 771 + }, { "path": "src/thoth/apps/behavior_composition.py", "sha256": "cb6d45099e0a2d2f13863cc7b7a111fb77b367d6ec068d926e4e675f767ca1ab", @@ -4366,11 +5031,21 @@ "sha256": "183ac6528e68541a5ed6ab7003356096af3016de755f03fd6802b41fa7e388e7", "bytes": 16033 }, + { + "path": "src/thoth/apps/hypothesis_link_composition.py", + "sha256": "7131e70522da44671a7e61a111e0f23996386346520c8c66da08da26b56195a4", + "bytes": 3151 + }, { "path": "src/thoth/apps/improvement_composition.py", "sha256": "cc1800bd78ac7873d16ddd6dc5e5474c371325a79bd6c80a8cb780bfdc879c0b", "bytes": 8024 }, + { + "path": "src/thoth/apps/judgment_records_composition.py", + "sha256": "fe715dbfcce87d46926dee2acb1b0cb89f7babf71d59b42a8c5b08a2a095bd43", + "bytes": 2841 + }, { "path": "src/thoth/apps/judgment_review_composition.py", "sha256": "7fbe0ea74201e8ea18c58d06314125c76fa9e06eb2d1c84972aa4f5fb87ca3c2", @@ -4383,8 +5058,8 @@ }, { "path": "src/thoth/apps/memory_composition.py", - "sha256": "cc1b838a76faa0db8d05b9dca8cbf41a025ceb9fed33e88c38d76bcfcf97cda6", - "bytes": 7183 + "sha256": "00717eea7b70c885e02b4c89322bee5b5a29d074b81246aa0ec36e90f3b573e7", + "bytes": 7242 }, { "path": "src/thoth/apps/memory_review_factory.py", @@ -4418,8 +5093,8 @@ }, { "path": "src/thoth/apps/research_entry_composition.py", - "sha256": "a7ae439f90642bc1c5d0bdd343e2dcfac1f0aeef25929f995c41d722fcfa6011", - "bytes": 13587 + "sha256": "0fc417006a535f7794cf7fb11979fd85cb8a4c9493a552d737e2401060640ade", + "bytes": 14197 }, { "path": "src/thoth/apps/research_history_composition.py", @@ -4448,8 +5123,8 @@ }, { "path": "src/thoth/apps/runtime.py", - "sha256": "c37eedd453198dae1fb1c1366d67bf2e58384adeae32a44744f336dd69a63429", - "bytes": 33926 + "sha256": "2e056475d2c9e4e1b0bb6d1e5c219bc79f934322715e5731f4e74e6f4e1b5a66", + "bytes": 33941 }, { "path": "src/thoth/apps/runtime_types.py", @@ -4476,6 +5151,11 @@ "sha256": "5b22e7d964e53232f02aae6bd66742af55765e729335584146bcf4ce8ed8131b", "bytes": 2728 }, + { + "path": "src/thoth/apps/trace_composition.py", + "sha256": "b4c771e173119ce24aef542d44f16cf8b85048408347d59c1a5401af95992c8f", + "bytes": 2056 + }, { "path": "src/thoth/apps/tui_presentation.py", "sha256": "65deaac5b69876bcfbac7ffa664dd1db492de8f9736cf799f0bbe8b0e24d9150", @@ -4508,13 +5188,13 @@ }, { "path": "src/thoth/domain/action.py", - "sha256": "9ac11aceb3cb79aee87edad2784a87e1bdfecefb07c16a6095923ae8abbca29c", - "bytes": 9197 + "sha256": "b4bb0fd6ee2aaeede383278f63c5da8938f8fb13f61e72704a1eba984a5957c2", + "bytes": 9432 }, { "path": "src/thoth/domain/action_full.py", - "sha256": "b56b68f8862c388f6c3d7c155d99f2216fe9bbaecdac8908cd5b44963212a046", - "bytes": 5447 + "sha256": "453cbd19cf14efe22d4ce29803849d4789f9f9e69dd69ffdb64da9a21f30a3a7", + "bytes": 6762 }, { "path": "src/thoth/domain/actor.py", @@ -4558,8 +5238,8 @@ }, { "path": "src/thoth/domain/canonical.py", - "sha256": "bcdd4a4e40e41f318335331a69cade0444e61bd05a46b69bd7e6fd7c35736c07", - "bytes": 3999 + "sha256": "efc64d9e044a5442e3caa0453dc5b5826e4c60f34959ee278b817a1411e2e4b1", + "bytes": 4886 }, { "path": "src/thoth/domain/claim.py", @@ -4621,6 +5301,11 @@ "sha256": "b9e4a27251fa33861b76136d4adc54e9c2438a4c5c4b13f9601de7e0e64fafaf", "bytes": 759 }, + { + "path": "src/thoth/domain/discrimination.py", + "sha256": "9a60fc94aa164245061a646d1af6486aa8e7f9625db8d6e807a5c2ebaa3c382f", + "bytes": 4225 + }, { "path": "src/thoth/domain/effort_bands.py", "sha256": "54dd1d958f0e5429eefd4b81aba12137f4649cab618b5a3b51b59c735cb0c623", @@ -4628,8 +5313,8 @@ }, { "path": "src/thoth/domain/enums.py", - "sha256": "6afb932aa851257a988e5b9e9ea294b1a91fb1dbce73c2f5cbec97153f14779b", - "bytes": 10595 + "sha256": "45711b0fa60b2fd8239aae21220b26a26718b043420018a0bb465302db18dd1d", + "bytes": 10647 }, { "path": "src/thoth/domain/environment.py", @@ -4718,19 +5403,24 @@ }, { "path": "src/thoth/domain/hypothesis.py", - "sha256": "01d2ead0544aa2f65b2cce5e92e95e7a24788108a29d7fa595972e513a511f17", - "bytes": 3938 + "sha256": "938699ae94b5fad8c086b411c05166a0ca70fa8fb72a302a3d6c6b993ae474b1", + "bytes": 6021 }, { "path": "src/thoth/domain/hypothesis_full.py", - "sha256": "0ad5d7cfbba0978b351a6d67a8244be99a31323711cefc9eb261a929f36e57f3", - "bytes": 6470 + "sha256": "a14c3bbc50ff1411d606bee633f0ff71dd7cf190fe56dea6551a92ad408755a0", + "bytes": 6659 }, { "path": "src/thoth/domain/hypothesis_review_validation.py", "sha256": "1900cedd6914ad6cd9cedd65cb86d0388f489f4505779489125cc3a5bb85837e", "bytes": 1725 }, + { + "path": "src/thoth/domain/hypothesis_same.py", + "sha256": "ac9c70ff21085e5e1a5ecb72afad85c661aaeb42157a1d65edd81cb77c550f5e", + "bytes": 3207 + }, { "path": "src/thoth/domain/ids.py", "sha256": "6599c536ace328e8db0796a7bea78e232188faaf9995d6941cdf05896a65e357", @@ -4761,10 +5451,20 @@ "sha256": "c082f19952443923aafd5638e1be152bb61e4c01b11c5095dcfd9e648c0ee98e", "bytes": 4361 }, + { + "path": "src/thoth/domain/lesson_ref.py", + "sha256": "f1697014c5c039fa0b31cf0ac036748fe9a0db0a70aa1345f4f44dd9fda434f2", + "bytes": 6439 + }, { "path": "src/thoth/domain/memory.py", - "sha256": "8614687f99123948e825a95edad714011f64a54b9acbc2ce949074425640dbce", - "bytes": 7105 + "sha256": "32fcbbf6fdb80b05bfe1d7444fb145a9e65e7a8d7ea42b5e016a09161da5e3b4", + "bytes": 8038 + }, + { + "path": "src/thoth/domain/memory_expansion.py", + "sha256": "2c7eca34e36bee60b8823e283db3171ed0571851890c0fbddcce6698e6dfa944", + "bytes": 3207 }, { "path": "src/thoth/domain/memory_preparation.py", @@ -4778,8 +5478,13 @@ }, { "path": "src/thoth/domain/memory_settings.py", - "sha256": "ddd1d0644bff88b1e30f348bf11ad7afeee72565a0734a9b4161d3768d16ca5d", - "bytes": 413 + "sha256": "f12fccb338ff5703a9a2e50ea71b8dc4d28cc3b93b45b99a7fde8eb637fc8db3", + "bytes": 689 + }, + { + "path": "src/thoth/domain/memory_terms.py", + "sha256": "45dc99b036951a81c2f0e1931787df51bc4d626c0a8059c881ef59e87a642140", + "bytes": 4754 }, { "path": "src/thoth/domain/migration.py", @@ -4793,18 +5498,18 @@ }, { "path": "src/thoth/domain/model_call_settings.py", - "sha256": "cabfd7f24b160004f9fffe362729eb0084f086a29b0433b050e4bf98ea7a00ab", - "bytes": 885 + "sha256": "3771cf166e5e7fd29e7c2b44a5449b8772abbd6b1c347117ef616f8e8a0f9190", + "bytes": 1170 }, { "path": "src/thoth/domain/model_catalog.py", - "sha256": "bc2f241bb15778c37ed239161228e05fad2262d877a5303c91d6d16cdfe32953", - "bytes": 3652 + "sha256": "a9215b161a1284353d89c4ad1f46d19cf64f5ac35861ad6ce94accc8a0dde5a7", + "bytes": 3752 }, { "path": "src/thoth/domain/model_dispatch.py", - "sha256": "83222c8727737d4973ed8b3015b5a9dc791414f20e2399ece266e67602343ae5", - "bytes": 8075 + "sha256": "3f8e1816a4298136308b6d206eff9fa7454c903cb7031ebcd579d35554bac5d2", + "bytes": 8255 }, { "path": "src/thoth/domain/model_settings.py", @@ -4908,13 +5613,13 @@ }, { "path": "src/thoth/domain/research_codec.py", - "sha256": "d1673d90a74afe02a8ad55198943d182c0bcb8b18f9a51b2957cd89b7bd6756b", - "bytes": 2521 + "sha256": "4f43f88419657574c5ad6cf42c8c4c9e3ff880dd7f7f4daf5e70a82c42f6f97c", + "bytes": 3185 }, { "path": "src/thoth/domain/research_execution.py", - "sha256": "2db4d8da5a1c1f75e50c6e1d1676ad33e56b5d7daca2f9a60ec43d87a37955e2", - "bytes": 4416 + "sha256": "f210c775f3d5f1e750bde15729159a45d65cc7cc49725389983949545b001460", + "bytes": 4849 }, { "path": "src/thoth/domain/research_failure.py", @@ -4943,8 +5648,8 @@ }, { "path": "src/thoth/domain/research_projection.py", - "sha256": "23ec337af0d3510ab132f38f93e6000d7b3c8a1e34af29565a0f452125164d87", - "bytes": 2525 + "sha256": "19a0c40e82a651446438b0d1c230c23c7ce1e0f6307f8b4d99802c9d40f25805", + "bytes": 3311 }, { "path": "src/thoth/domain/research_queue.py", @@ -5016,6 +5721,16 @@ "sha256": "299d02626d09eebf071695e59861e65f16e698f2c0f45305acc5bc8fc8046898", "bytes": 675 }, + { + "path": "src/thoth/domain/trace_closure.py", + "sha256": "74f23c62751aa3c333ac952c45f5b0066b771fc0297812d96e1a3ed45e958afe", + "bytes": 3197 + }, + { + "path": "src/thoth/domain/trace_origin.py", + "sha256": "2d9d0e00e20ab7bbc3b9a914532a59ff3a9db75e0d005981f093c6b6a75a8e88", + "bytes": 2161 + }, { "path": "src/thoth/domain/upload_scope.py", "sha256": "edb74c2afc62df34014b38513625840aa92d2c25b3cf6ccf0f59611a233c64a3", @@ -5026,6 +5741,16 @@ "sha256": "5551cd47544a15026fdb7b99abc249533514b70e4637dca4b4057ae0a0b4c545", "bytes": 4814 }, + { + "path": "src/thoth/domain/verdict_link.py", + "sha256": "4c10c6a8cdff957cbc530b409c8ed4e0e9c225fa2d437ac0afb0afc6a0f54731", + "bytes": 7382 + }, + { + "path": "src/thoth/domain/verification_trace.py", + "sha256": "1245343ca213015172b6072133ff5cfe94ed41a031b6263c5ea0cbc3c4aa2b6c", + "bytes": 15588 + }, { "path": "src/thoth/domain/web_acquisition.py", "sha256": "994a94f5223982a1791f66694ed9e1f179d1dbe2b76152831ad6840f95e3a285", @@ -5193,8 +5918,8 @@ }, { "path": "src/thoth/ports/memory.py", - "sha256": "f2ddb6f5ec81b3721b97b705d255df42f741924b2eccda246cadf74b8129c1f0", - "bytes": 2357 + "sha256": "a0766242cdeeced096730b283a373c7fc1c18ef1111cff32e2298d81d302ccd8", + "bytes": 2815 }, { "path": "src/thoth/ports/migration.py", @@ -5208,8 +5933,8 @@ }, { "path": "src/thoth/ports/model_catalog.py", - "sha256": "de04cfaacdcc1761f62b10f81a2a21b6ea997f93edd05449fd20366e69deaec2", - "bytes": 804 + "sha256": "1b585ee3ad8565281ab95217241df376fdf39143f548dc9ce0845b1a6b48753f", + "bytes": 829 }, { "path": "src/thoth/ports/model_credentials.py", @@ -5358,8 +6083,8 @@ }, { "path": "src/thoth/protocol/bus.py", - "sha256": "9429120753362812774926d310e7535bca4c388684feebf2acfd5397f824e9f3", - "bytes": 31962 + "sha256": "14a5ef4dcd1ea3a9c36bb32dce3219d1c93ad0b321ddb11d0c3bcd4b464325bd", + "bytes": 31121 }, { "path": "src/thoth/protocol/deferred.py", @@ -5376,10 +6101,15 @@ "sha256": "ab04c599a97ce0271d12731d86e7728600382233f46b6db8c14a14a8ac4ddade", "bytes": 29666 }, + { + "path": "src/thoth/protocol/read_queries.py", + "sha256": "17a7460e4c4be9e9b07c2cda5240346e758fa6bc1410e259c02596ed6ea81333", + "bytes": 1248 + }, { "path": "src/thoth/protocol/registry.py", - "sha256": "00d8145005899646cee5d8ab68083ca8d9161276cd4c9b059d6e80c3ee0e8e4f", - "bytes": 12743 + "sha256": "ae6ee4f18638e6a0fa4b21aff2fed4ce2eaad50059b9fc38710cb05b8a65cce8", + "bytes": 13225 }, { "path": "src/thoth/protocol/schema_export.py", @@ -5423,8 +6153,8 @@ }, { "path": "tests/architecture/public_source_profile.py", - "sha256": "c280b95acc1822ca317cb0239f01b63a6a3d077d6effc54f0a65f158dcb6ff89", - "bytes": 8030 + "sha256": "d52efb4c29df3dcad80376e2a29ceead35c062e2a75d9bea4341be06831e46b8", + "bytes": 8272 }, { "path": "tests/architecture/rule_transaction_helpers.py", @@ -5488,13 +6218,13 @@ }, { "path": "tests/architecture/test_metadata_rule_transaction.py", - "sha256": "f9b5e6002b35b5b08cd92e33ef537b1f9bf2ee9ec6b038a9b4717803ff0f16da", + "sha256": "ae4dacee8c53b90ba10dc59462a39a5311de8ece9cf3a64b4c992c1e311ca406", "bytes": 14854 }, { "path": "tests/architecture/test_module_budget.py", - "sha256": "b3a44f768fda571601956d1492d5335442035e6d7ded8f54cf0fe184ddac0079", - "bytes": 3867 + "sha256": "c0315c0b36bdbb79628319298499edb008cb115ec41692273255394a71dcedc9", + "bytes": 8693 }, { "path": "tests/architecture/test_ocp_extension_guards.py", @@ -5533,8 +6263,8 @@ }, { "path": "tests/architecture/test_public_protocol_exact_parity.py", - "sha256": "44bb4c8003758f065d434525aeca9712f386cb5ddfedbff0470294f2f681a75c", - "bytes": 4955 + "sha256": "2cbc2a3db6641a5207120047e864a7649c98447e09fcb7720a5cfaef7c802f0e", + "bytes": 5631 }, { "path": "tests/architecture/test_public_source_profile.py", @@ -5553,8 +6283,8 @@ }, { "path": "tests/architecture/test_rule_candidate_pretool.py", - "sha256": "2528191a6478d2599b79342183520751c22c1314320916485741272b6b3ad39c", - "bytes": 9160 + "sha256": "94fefcba4f80e2a4070d02d94b937d57397a2596f2a0e04df83841e489c4b8ba", + "bytes": 12094 }, { "path": "tests/architecture/test_rule_metadata_paths.py", @@ -5823,8 +6553,8 @@ }, { "path": "tests/contract/test_file_upload.py", - "sha256": "0dbdde4ad1a8764d74e8fa72572c4540d4341bdf037089425782f8781f535e68", - "bytes": 3671 + "sha256": "d81442178296d5fe28fac36a02cef4c96c2a14cfea5e3fc8b46781706ad21ebe", + "bytes": 5427 }, { "path": "tests/contract/test_jsonrpc_envelope.py", @@ -5856,6 +6586,31 @@ "sha256": "cb060594256bf0e2c6b7d4090dd653b64f5bba528a3c0f1510f3d0b0ad539b1a", "bytes": 1731 }, + { + "path": "tests/fixtures/hypothesis_eval/case_builder.py", + "sha256": "3c6a4e4927b73c40e383e2952e9222ac6371bed4bf7967aa9d912213805427f6", + "bytes": 16330 + }, + { + "path": "tests/fixtures/hypothesis_eval/cases.json", + "sha256": "daa5d80c5c978d7388911e5353e1b5e53062ca4180238424eafbc682e05ced42", + "bytes": 193867 + }, + { + "path": "tests/fixtures/hypothesis_eval/lesson_cases.json", + "sha256": "df7e0eb4c5d17b9f1117fd3d776b4c2d0f5171b9f2e510f807d1262e8a503159", + "bytes": 20561 + }, + { + "path": "tests/fixtures/hypothesis_eval/ranking_cases.json", + "sha256": "498dcb6cfad2279b1f8a76b8c4cf14910b81ba221768ba5a5ce19ffa8aa1c195", + "bytes": 35832 + }, + { + "path": "tests/fixtures/memory_eval/families.example.jsonl", + "sha256": "3b06458098cfa1753a110c66e78d15a867153564291a31492a23b3c76a6e25e5", + "bytes": 1401 + }, { "path": "tests/fixtures/resource_scope_v1_before_parent_repair.json", "sha256": "68c8c7d99900a0a22bb157fde2030921d6c11fe8a7ff85f20eeae168d1254e33", @@ -5866,6 +6621,71 @@ "sha256": "46cbc6121770020d167dc3c24bfa4cfd0716c67e94a1b1b73745985303314a93", "bytes": 3758 }, + { + "path": "tests/fixtures/trace_csv/README.txt", + "sha256": "164fbf3abc1fe39f18ef3a5ab1645da3030dcebf6ae6a137515ba219340e60e2", + "bytes": 690 + }, + { + "path": "tests/fixtures/trace_csv/bom_utf8.csv", + "sha256": "0bd1048d7b4ae792a2294d223af8794578d39c4fbeda00ab7d5b37f3f11bcd74", + "bytes": 849 + }, + { + "path": "tests/fixtures/trace_csv/formula_prefix_escaped.csv", + "sha256": "e73f62867c33f3ac0cd54ff32ae540456a8736577b20e0254ceb7414fe239b68", + "bytes": 1034 + }, + { + "path": "tests/fixtures/trace_csv/formula_prefix_raw.csv", + "sha256": "4b63a130db1cd6dc81744dc7d58ee9e94a5bd2aa3e17f10120e3a4616a6f1606", + "bytes": 822 + }, + { + "path": "tests/fixtures/trace_csv/ids_date_like.csv", + "sha256": "a66014d48e2bbe9f985302702756c2933fabdc5523feb1c721d1c35f58b901c3", + "bytes": 594 + }, + { + "path": "tests/fixtures/trace_csv/ids_duplicate.csv", + "sha256": "8d0fc02ce11f47165e39395edf96399aa4c539a901ff68dbad0b7e6939417fe2", + "bytes": 529 + }, + { + "path": "tests/fixtures/trace_csv/ids_leading_zero.csv", + "sha256": "972f43ecd727e4dbd345fbf041d80cdbe8e17532617ccb140adbcc2aae45d74e", + "bytes": 582 + }, + { + "path": "tests/fixtures/trace_csv/ids_long_numeric.csv", + "sha256": "23b897b62e3eb91ede5ea18fb6e4db70c9a69a3a07473cffb38635446f87e96b", + "bytes": 699 + }, + { + "path": "tests/fixtures/trace_csv/korean_special.csv", + "sha256": "5f07e74f62c3e6c0cd3850c4180f7ab35df73a65b4c259e913fb1dc6dd4f3f8d", + "bytes": 746 + }, + { + "path": "tests/fixtures/trace_csv/no_bom_utf8.csv", + "sha256": "9aaebb8ad348fc1aed8b1fa2799e9efb58f627a884c5d2099eb747e8db006c80", + "bytes": 846 + }, + { + "path": "tests/fixtures/trace_csv/order_baseline.csv", + "sha256": "9aaebb8ad348fc1aed8b1fa2799e9efb58f627a884c5d2099eb747e8db006c80", + "bytes": 846 + }, + { + "path": "tests/fixtures/trace_csv/order_shuffled.csv", + "sha256": "2fb7eab10d4672f77d32cd27dbc3299fa82ebf89dafbe70ee1cf455c0d8b5615", + "bytes": 956 + }, + { + "path": "tests/fixtures/trace_csv/zero_empty_null.csv", + "sha256": "decbfc24236cc9af8733876baa7392c19af5fc85fdd61003e31705f04fe2e91d", + "bytes": 1353 + }, { "path": "tests/integration/behavior_exposure_helpers.py", "sha256": "d7c52e1e7afe3d5158318473cd8fb00d72b25aea6a0230357662d33786038eff", @@ -6166,6 +6986,11 @@ "sha256": "16fb26b6bd144b640e3436591dabcf65a6925c7c7db5697869c76398badd0d9c", "bytes": 10306 }, + { + "path": "tests/integration/test_action_effect_value_type.py", + "sha256": "5f5832c738f49e6e46a1f2233d14d6414aedf2a7c2ddf2e52e7078fd7269859e", + "bytes": 4335 + }, { "path": "tests/integration/test_action_effort_estimates.py", "sha256": "a902f9f2c63008513a672f98333d5260e845d22d35871a91eedba17020427302", @@ -6281,6 +7106,11 @@ "sha256": "8597a8fbc2c0391d307d437c3afb4a0a4e5af7e34a4e65d54410546e3e572d5a", "bytes": 1352 }, + { + "path": "tests/integration/test_cutoff_precision_currentness.py", + "sha256": "01ae2576262d891fd34f742924015a0d11f4b5f89f85a03e9ddaa5a6d2b176b8", + "bytes": 5193 + }, { "path": "tests/integration/test_decision_object_restore_fail_closed.py", "sha256": "ab37bce7fffcbf1d6570a33ddbeb27caab06b293ce4cd49cf7e3d0ba58697bbc", @@ -6376,16 +7206,36 @@ "sha256": "d173215ba5f52c2f5c615423c8ba69726b27d1f0f3914dcb193daa724884d1ac", "bytes": 8603 }, + { + "path": "tests/integration/test_hypothesis_action_guard.py", + "sha256": "db70d55624ea69a9ad4ad1125f3308476799cbb4d4341fa563fbf411349b4162", + "bytes": 17960 + }, + { + "path": "tests/integration/test_hypothesis_contract_v3_run.py", + "sha256": "50aa3fccf47be9e9dbf9cf9cbf6d75bc23fdc9f98d5e3d91bba05fce72755019", + "bytes": 9833 + }, { "path": "tests/integration/test_hypothesis_full_rpc.py", "sha256": "9479973f07c0dbfd0653c843e891c43a151fe0e15e635b3002ddd30a8c24536f", "bytes": 24904 }, + { + "path": "tests/integration/test_hypothesis_link.py", + "sha256": "58f2eb3a270cc1c5aee4db22a02588ae175a38b7fe4ee1f1725c6eb70cac6579", + "bytes": 16469 + }, { "path": "tests/integration/test_hypothesis_prediction_outcome_cycle.py", "sha256": "ea9bb533ca4e2db498dfbabee3c01d856f4a2ee4a259434e6e82b77a3d40595a", "bytes": 24480 }, + { + "path": "tests/integration/test_hypothesis_same.py", + "sha256": "bd155d0e6480f04dbe5250b0f6dc0063a361d20cacbd19e16374e02d8c484f9f", + "bytes": 9127 + }, { "path": "tests/integration/test_improvement_full_rpc.py", "sha256": "c86552452f8b85cc93cb30e20680104f358fb99b39c359735acccab436647ce9", @@ -6406,11 +7256,21 @@ "sha256": "5e64d55d216651b3c1eb2669c9bb4eb04b881cd15e12dcb05d288af1427f98f0", "bytes": 7761 }, + { + "path": "tests/integration/test_judgment_records.py", + "sha256": "7778ae97e6d23da3efd9d5e81658c2c63d05e2fa4fe02949fed93e3e4fc2ccbb", + "bytes": 17503 + }, { "path": "tests/integration/test_judgment_review.py", "sha256": "ac46307b0b84528e05aa99f6ff5f285ea4feaa32c4fa89f4032a1d7f3a562819", "bytes": 7739 }, + { + "path": "tests/integration/test_lesson_recall.py", + "sha256": "b18546a7e7dbaeb7765d7146b18f680ea524bb6db796c4557d7486261f286ac1", + "bytes": 7990 + }, { "path": "tests/integration/test_lifecycle_rpc.py", "sha256": "4af68c5bcd03cfff24b5c5057f112cbc88f7b67fabc9869a8c134fb88d109b9f", @@ -6436,10 +7296,15 @@ "sha256": "6f9dde2178bf4868b0e4c26c00029f57f5cc2ac402962367d0aa8a1680af0e7f", "bytes": 15987 }, + { + "path": "tests/integration/test_memory_query_expansion.py", + "sha256": "93df1a2aa3b1ba21b20512fc343016f9b6b6c6fded876f24231c8bd3d7876238", + "bytes": 14218 + }, { "path": "tests/integration/test_memory_selection.py", - "sha256": "b9157c7654fb7785bda15b30d8e9cfefe9e11d871125b4d8179913099a2fbbd8", - "bytes": 21544 + "sha256": "a71b0be02e564efb178ee7ecf21a4798aedb2216028f58085336540406dc38d6", + "bytes": 21716 }, { "path": "tests/integration/test_memory_store.py", @@ -6458,8 +7323,8 @@ }, { "path": "tests/integration/test_model_call_settings.py", - "sha256": "57452c0c44227051246cd74b61a3f76641dce80cf18d64b656dfa04743c7da46", - "bytes": 4371 + "sha256": "47cea6cad58a2d9a65cbe814211ba465520d3c7c801d00e032218f8fc1f1b825", + "bytes": 4451 }, { "path": "tests/integration/test_model_catalog_refresh_rpc.py", @@ -6473,8 +7338,8 @@ }, { "path": "tests/integration/test_model_execution_marks.py", - "sha256": "861b9f4e1b57585202731b1faf182f360057c87ee0cb0687580abc6adba93e5f", - "bytes": 4151 + "sha256": "175305128a4f5b8657d0cf025495b50c72b3467767e9ee6935bfb153983fa577", + "bytes": 4225 }, { "path": "tests/integration/test_model_input_admission.py", @@ -6561,6 +7426,11 @@ "sha256": "2ef9bb939d556784e41fb0cfdfeae86c45b0c28a34d51b2337ab19f6fe47a395", "bytes": 7634 }, + { + "path": "tests/integration/test_prepare_synthetic_radar_demo.py", + "sha256": "77ed779b04693763fc2aa2f507b973550061b32643882db1dffd12092ce3e305", + "bytes": 7267 + }, { "path": "tests/integration/test_profile_cross_domain_overfit.py", "sha256": "eec3400c8b5df70b56e2582e32f4763b952c8ab8039ef9dd392ef7f4ee989284", @@ -6571,6 +7441,11 @@ "sha256": "7ac38709b561b7fae242fcf6cf97574cf2d3268acd6702e671b22d43c78b7289", "bytes": 9849 }, + { + "path": "tests/integration/test_project_list_reads.py", + "sha256": "c767beb6bff4187d94ec7cd8fc5effd76fea81ad566f85f2ee835e99d07e979b", + "bytes": 10230 + }, { "path": "tests/integration/test_projectpack_rpc.py", "sha256": "28d4704b5dcdec45b4e76c5314429cc54f6273ac910ae2b82b452168f68fe841", @@ -6631,6 +7506,11 @@ "sha256": "034208201b1f602d1622d6f9cd72d8d9b43b8aac5eb30bd3fbb6786a9f9af3d9", "bytes": 3583 }, + { + "path": "tests/integration/test_r2_replay_guard.py", + "sha256": "77fc9af4403b46719634f79ee109cf5b74e506d3a6fae55343b7564a8d677ba0", + "bytes": 14375 + }, { "path": "tests/integration/test_real_exposure_lifecycle.py", "sha256": "39d9ee8ab5c5c832619afd8ab905a2ba289e93e41cc7757d4f10e5c4f6619114", @@ -6813,8 +7693,8 @@ }, { "path": "tests/integration/test_research_request_v2.py", - "sha256": "bc18825c36f894799a60979bc4b929f0e1ed07b2bd9c8108bb49609f68fe3daa", - "bytes": 19644 + "sha256": "d1400bc76e15f9a3bcc5cc764e3f4020b35e30b6d704d2aa449f6fc30137ae5b", + "bytes": 20993 }, { "path": "tests/integration/test_research_scoped_conflicts.py", @@ -6998,14 +7878,19 @@ }, { "path": "tests/integration/test_scope_read_request_memo.py", - "sha256": "80fdd2d95682946dc72ebcda5ce7d99fd096da80328e2d246c516a5823f50999", - "bytes": 3393 + "sha256": "bf0ff044b90e5938a46034d940161229ea1e9e34bd5f82df79d1487eb691071d", + "bytes": 3470 }, { "path": "tests/integration/test_selected_evidence_query.py", "sha256": "0df7bbba34239747baefdfa6a72bdcbfc676b4960487869e228cd7170d07f2f2", "bytes": 6396 }, + { + "path": "tests/integration/test_source_list_cost.py", + "sha256": "7eaf232a05f16195d8994f27051c0ea4acc5fc83173faa0f4a0050f66819bd67", + "bytes": 2831 + }, { "path": "tests/integration/test_source_rpc.py", "sha256": "50d1afdbf199515f70a611a74e1b71262e7c2c8eaf4ffdb2bce0b03963218956", @@ -7056,6 +7941,21 @@ "sha256": "95dc24f7963420f18ffe8dc35395d98dfcd2da18400a01a0cd7c0acf51b18155", "bytes": 4601 }, + { + "path": "tests/integration/test_trace_csv.py", + "sha256": "4202514f9f65e383c8380399deb8771576388f50f4e88fe92f4ca39db90da885", + "bytes": 33654 + }, + { + "path": "tests/integration/test_trace_csv_closure_column.py", + "sha256": "738303559d33dadd5d4ef32466ec8781b139391a25707a900df52f499ca6de04", + "bytes": 4484 + }, + { + "path": "tests/integration/test_trace_origin.py", + "sha256": "b9e1a7a36946a9b2acb25036b570e3e8dcb2159a347a5799245debc2e64c75de", + "bytes": 12302 + }, { "path": "tests/integration/test_transport_diagnostic_persistence.py", "sha256": "9034e8794346a2f781043b9e63d49ab6af546288640928196d576a42018f9dd5", @@ -7101,6 +8001,11 @@ "sha256": "1aa50ddba2fde60b5c782eb7fab14820bf24041fbed75b7e145ba63d316b8280", "bytes": 21938 }, + { + "path": "tests/integration/trace_demo_helpers.py", + "sha256": "9cf042bc66c02f24602e5600f98bbc962ed931c93f1a66b6981e9d9a3126b123", + "bytes": 4710 + }, { "path": "tests/parser/conftest.py", "sha256": "970b9e96957dcbc33da3f2b9bf85a594c85da78e6ac5a3b65bfe5e8e3bab0a7c", @@ -7256,16 +8161,26 @@ "sha256": "830016fd9910da02e659485c387366e9c68aaee5b42ca1d473838515ae75ca46", "bytes": 2403 }, + { + "path": "tests/unit/domain/test_action_test_refs.py", + "sha256": "b16cd6f9c28eff7383d27619dcffada0994cc1140b52bc5fb30f5ad94da380f6", + "bytes": 1936 + }, { "path": "tests/unit/domain/test_canonical.py", - "sha256": "1c00cd39f24f38901bdca1903f497fdfdc9df597ebae4e9ef0a0760c61500ac1", - "bytes": 1989 + "sha256": "724fd242e3a2b8d34500ae5569ba7ddc914cba3b01c4db69ea2933bdc2981a17", + "bytes": 3545 }, { "path": "tests/unit/domain/test_cross_namespace_internal_notifications.py", "sha256": "f71003b86b5846d2cc4b76a3addd22947713f18f8aaa0e71d2e8546fd5ffe328", "bytes": 2627 }, + { + "path": "tests/unit/domain/test_discrimination.py", + "sha256": "b6e28b73b2e0ab26614b50a79857b1a6cce9f2e57bde0c36f9429431ea5de446", + "bytes": 5554 + }, { "path": "tests/unit/domain/test_effort_bands.py", "sha256": "6e5d0b91c2b04647b21377445ec13052e40917bc26c98cf9981d13693c7355c6", @@ -7336,11 +8251,21 @@ "sha256": "b8f455152bf36ec5d5ab4395b98ba620d0a5b06d29ee487edb3c554c4d44b4a7", "bytes": 2710 }, + { + "path": "tests/unit/domain/test_hypothesis_contract_v3.py", + "sha256": "ace3f9b84f7e983855bedab8a882b040677949e121382b84dad57cc4b88c071d", + "bytes": 8699 + }, { "path": "tests/unit/domain/test_hypothesis_review_validation.py", "sha256": "2aeb35920fed46d5fceb70b609fb3ae3631adb4bb0e64e59410bde6232849aab", "bytes": 2917 }, + { + "path": "tests/unit/domain/test_hypothesis_same_group.py", + "sha256": "c4e7b2901a9187136be38d3126548130fe32be074b655d8044c5247c4b1dba6a", + "bytes": 3063 + }, { "path": "tests/unit/domain/test_invariant_mutation_guards.py", "sha256": "f35acfc65bcfad95c8f31a0e179f90ba36d36ca2c9a0151290875c5b670f6bd3", @@ -7381,6 +8306,16 @@ "sha256": "a8e08cc6c2ca08d4072a3b3e1342279414cb7fe6ce2ed7249de2a44e0779dba1", "bytes": 5273 }, + { + "path": "tests/unit/domain/test_trace_closure.py", + "sha256": "dbf728995dbadf1462e1a9d9536a9ebd0df3958f4467abfcd6e77c0e533cbfc9", + "bytes": 3536 + }, + { + "path": "tests/unit/domain/test_verdict_link.py", + "sha256": "6a36765cdeec932673a70b5a687418e9d156516e36afa88d54b652511641b3ad", + "bytes": 10008 + }, { "path": "tests/unit/evaluators/test_evaluator_registry.py", "sha256": "1d3bc615622bdb37e97f0a1d85f6412caefd1b2ffc35079058e918495747ca38", @@ -7433,8 +8368,8 @@ }, { "path": "tests/unit/models/test_catalog_marks.py", - "sha256": "3ad4e9d89a3ec0a4174bca0aab994c126497e8ed40ea84fe4824aabcfb540675", - "bytes": 4058 + "sha256": "dc388e2ce94c2ddba2ff57e557f1b9762c8c407b56d9add7393b83a336188d53", + "bytes": 4162 }, { "path": "tests/unit/models/test_catalog_startup_refresh.py", @@ -7488,13 +8423,13 @@ }, { "path": "tests/unit/models/test_codex_catalog_snapshot.py", - "sha256": "445ba715249b1370c8dc9c05c21e51c73fa85c1daf7967f6cafa7a3a8b770dc9", - "bytes": 9691 + "sha256": "935453cab2311691d94f44fdce27378e54d9aa04d366620a9e0900a1a2d71946", + "bytes": 9933 }, { "path": "tests/unit/models/test_codex_isolated_broker.py", - "sha256": "3eb985ccdb2e8030168f20f1d6757e1902cde23dd33dc167c3c762b7338810ba", - "bytes": 36281 + "sha256": "c43f89c16449bcb4fb486069fb69bf818acb472460dd29e826fcb3950c74812f", + "bytes": 37211 }, { "path": "tests/unit/models/test_codex_login_completion.py", @@ -7516,6 +8451,11 @@ "sha256": "78ed4bab51ec765276d773b403801a4df3898928c777a85d995df151aa17fb25", "bytes": 2236 }, + { + "path": "tests/unit/models/test_hypothesis_contract_wire_text.py", + "sha256": "8d9446b2b587ff771edd870538bb7ba234ec15684a69c42b5dd9434e954f67ee", + "bytes": 3326 + }, { "path": "tests/unit/models/test_interrupted_call_retry.py", "sha256": "56c4788c484e86b00e11f04a003e5ccb9a0f2437b4409beb01629a0f794abac0", @@ -7573,8 +8513,8 @@ }, { "path": "tests/unit/models/test_role_wire_context.py", - "sha256": "36ef00b3a80890f4daef09cc6c0c49b7de743cea4255affba63ac23984b85044", - "bytes": 4220 + "sha256": "66adf55910ede03832dc2360f19123186ec7babc546e2b9afcd5f50d1b228c01", + "bytes": 6076 }, { "path": "tests/unit/models/test_scripted_model.py", @@ -7653,8 +8593,8 @@ }, { "path": "tests/unit/sandbox/test_sandbox_contract.py", - "sha256": "bb689d6a4b0c6cb1b5be518c6de22bd6f66517b600951e510244b21cd3a59502", - "bytes": 11571 + "sha256": "0d646fdcdc073cb7a76b78d9280e9df1f840fba7fbd4c5139d895fa9c5927875", + "bytes": 13345 }, { "path": "tests/unit/sandbox/test_sandbox_factory_registry.py", @@ -7671,6 +8611,11 @@ "sha256": "d8d4c56f9fe2a568cec558f7ab1460403d0832d9cde04aca9439eeae68eaddc0", "bytes": 7248 }, + { + "path": "tests/unit/services/test_action_effect_classification.py", + "sha256": "a759b3df82c2cef2ea88510df37095fe7993902522591df55f5a8d636cb6337c", + "bytes": 6013 + }, { "path": "tests/unit/services/test_authorization_currentness.py", "sha256": "b8e38880709329ad4fe4618305b07e7377fe03bc30659b303b333367a3b330b3", @@ -7711,11 +8656,26 @@ "sha256": "0fcdbba868e3884608f6c618a869241a2b988891b089fe1d90526a161f1417e7", "bytes": 6353 }, + { + "path": "tests/unit/services/test_trace_closure_text.py", + "sha256": "c5a930c926b8e667d6be89a9415ff566b01286442b71ea2bf78bb4bf8142ab2d", + "bytes": 5665 + }, + { + "path": "tests/unit/services/test_trace_csv_format.py", + "sha256": "df8c5fca5490d5485539688d4bf57ed513764e3e45765283a7853cdb0fa02233", + "bytes": 13455 + }, { "path": "tests/unit/services/test_tui_intent_contract.py", "sha256": "34e065ca1eea4d21a3eb3fbf5a819de4784681f52d151c155a5a48b2505c0edc", "bytes": 2403 }, + { + "path": "tests/unit/services/test_verification_trace.py", + "sha256": "aaf78154a6e15207451c840c6a1ff933ae69b9f18c7e3e004ee71f48809fd291", + "bytes": 24695 + }, { "path": "tests/unit/storage/test_ambient_transactions.py", "sha256": "a0928dc82f5860613b69125b023ab2619e7bd096463761573dbc084eff69c14d", @@ -7751,6 +8711,16 @@ "sha256": "a4785d843974a37b33a9e68aecf38956e3be8078c06762c790b0cf51f46c6b11", "bytes": 4679 }, + { + "path": "tests/unit/test_cutoff_precision_sites.py", + "sha256": "65c84283d7ade110a42464729691cd24d12163c9eaece840a61da02bf188dc36", + "bytes": 5502 + }, + { + "path": "tests/unit/test_docling_locations_characterization.py", + "sha256": "b2e02fc336e4bb6b350588c1dec1575110a5da0b4c737791f99fb47dc4cf6e92", + "bytes": 8166 + }, { "path": "tests/unit/test_docling_multi_provenance.py", "sha256": "f423c17960af9a001fbdda8743c66e238bbbd8b63c2d366993875d1d9d14cf35", @@ -7806,10 +8776,30 @@ "sha256": "ebade1c6326c80e6621a5533c03d2f9cfcb997fbb1feb8bfcb36dc5d98fa936e", "bytes": 12975 }, + { + "path": "tests/unit/test_hypothesis_eval_cases.py", + "sha256": "ab85b5c6741437de4c3f9b14b88d8ca620985c46562eba2a4ee1bfcb3b811a3f", + "bytes": 3142 + }, + { + "path": "tests/unit/test_hypothesis_v3_metrics.py", + "sha256": "90c08474ff1cd3792348052cf3553b86bc4f45400075f20ce5b6a6f4ae736a4f", + "bytes": 2052 + }, + { + "path": "tests/unit/test_lesson_recall_cases.py", + "sha256": "a20593ab7789905988c265ca75f5297575d57e39f3d5c81ee8980bb110dd1ddb", + "bytes": 3333 + }, + { + "path": "tests/unit/test_memory_eval_families.py", + "sha256": "263d5b0c083e59d6ec821b17d16c2a3421c35ca7d2bf9e45a6548e0065fbeb19", + "bytes": 11646 + }, { "path": "tests/unit/test_memory_recall.py", - "sha256": "f804fa7b747e92f85f5dd9f9a013d6bfb1c47e9181edbeddafe0bd623a73fa3d", - "bytes": 10910 + "sha256": "0617c7f81b6b72584701f563fd02e26acab95ef7c8f91f575e3b32a5b6a592f3", + "bytes": 14629 }, { "path": "tests/unit/test_memory_relation.py", @@ -7846,6 +8836,11 @@ "sha256": "7791a9df46888ede201ac557313fb51d713e48674ffc607bd5fbc3b8cb3aae36", "bytes": 1853 }, + { + "path": "tests/unit/test_memory_terms.py", + "sha256": "11edc80dabe3d3cb9a210613ea22428052e1be8cb0143c66e1914b355d8a6269", + "bytes": 2321 + }, { "path": "tests/unit/test_operation_reentry_registry.py", "sha256": "019af79fa67e596211ce244e2429988d202c4ea7ff32b314e2069219548d7082", @@ -7891,6 +8886,11 @@ "sha256": "493592df544950af94ca6eb2223d23c5136f26a66ac8b8001a737df56db714d5", "bytes": 1619 }, + { + "path": "tests/unit/test_research_completion_view_hold.py", + "sha256": "dd262b4b29aa7b4f31cb709983e3ec6ab30913bd390a7eb3a4c0f6d0a1e6f2a7", + "bytes": 3676 + }, { "path": "tests/unit/test_research_failure_cause.py", "sha256": "25654f1178e5d42dd17ba860883773520b88e277c8493a92558f17324fa175f4", @@ -7951,11 +8951,26 @@ "sha256": "50b250812a5f294a1ccaac26a6961009346083900be6e0931ad4c3b56f154e56", "bytes": 2987 }, + { + "path": "tests/unit/test_trace_origin_context.py", + "sha256": "35d5fa186d212bc2e3553f50b97372c579fba6928cafae496efb3091f52235a2", + "bytes": 3476 + }, { "path": "tests/unit/test_tui_presentation.py", "sha256": "53a7635f969b8fa08b90afb1470ad5dc5756fc0681c72ada98103fd8e36f6cf3", "bytes": 1890 }, + { + "path": "tests/unit/test_update_source_manifest.py", + "sha256": "ff2fd2f1ba83702513476b4c1e659379452a9e4b9f4853a70738cc9a47c5fab5", + "bytes": 5304 + }, + { + "path": "tests/unit/test_validate_synthetic_radar_demo.py", + "sha256": "97e85877dccd5159a0285f53c31cd8b3032249dda33aedc9f5a2f389564c3324", + "bytes": 5092 + }, { "path": "tests/unit/test_workspace_paths.py", "sha256": "aba44dddb508b4780d33ea0102a42d2ebb393b14ed9379791bebf91faa50d3ef", diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 1fc42ab..4695e90 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -24,6 +24,10 @@ included under the project's MIT license, copyright 2026 THOTH. No original bene upstream repository copy or private research dossier is distributed. Inclusion does not grant rights to the referenced external material or certify real-world project outcomes. +The examples/synthetic-radar-demo-v1 folder, its scorer folder and the trace CSV exchange fixtures under tests/fixtures/trace_csv are newly written by THOTH as invented +synthetic data. Every value, date and count is an assumption; no measurement, dataset or text from an external radar source is included. They are +marked SYNTHETIC DEMO DATA - NOT MEASURED - NOT APPROVED - NOT FOR ENGINEERING USE and are included under the project's MIT license, copyright 2026 THOTH. + ## xAI integration reference The experimental xAI device-code configuration and curated model capability metadata reference diff --git a/apps/web/src/api/actionDraft.ts b/apps/web/src/api/actionDraft.ts new file mode 100644 index 0000000..910b3e1 --- /dev/null +++ b/apps/web/src/api/actionDraft.ts @@ -0,0 +1,6 @@ +import { rpc } from "./rpcClient"; + +/** A draft action request made from one discriminating test of a hypothesis. What the person declares is sent as written; the server decides the risk from it and nothing about risk is sent. */ +export async function draftActionFromTest(projectId: string, hypothesisId: string, testId: string, declaration: Record) { + return (await rpc<{ action: Record }>("action/draft/fromTest", { project_id: projectId, hypothesis_id: hypothesisId, test_id: testId, effect_declaration: declaration }, crypto.randomUUID())).value; +} diff --git a/apps/web/src/api/capabilities.ts b/apps/web/src/api/capabilities.ts index c025cac..bc4950b 100644 --- a/apps/web/src/api/capabilities.ts +++ b/apps/web/src/api/capabilities.ts @@ -11,7 +11,7 @@ export const namespaceLabels: Record = { execution: "실행 · 복구", revision: "이력 · 분기 · 복원", outcome: "관찰 결과", memory: "프로젝트 기억", improvement: "개선 · 평가", receipt: "영수증 · 추적", closure: "마감 · 보존", export: "내보내기", operation: "비동기 제어", model: "모델 설정", workspace: "워크스페이스 설정", - projectpack: "검증 예제", field: "현장 평가", + projectpack: "검증 예제", field: "현장 평가", trace: "추적표", }; export function filterCapabilities(search: string, namespace = "all", surface = "all") { diff --git a/apps/web/src/api/hypothesisLink.ts b/apps/web/src/api/hypothesisLink.ts new file mode 100644 index 0000000..d35c3c5 --- /dev/null +++ b/apps/web/src/api/hypothesisLink.ts @@ -0,0 +1,24 @@ +import { rpc } from "./rpcClient"; + +export type RecheckReasonCode = "UNRELATED" | "STILL_MATCHES" | "NEEDS_RESEARCH" | "OTHER"; +export type LinkRecheck = { reason_code: RecheckReasonCode; note: string; actor_id: string; created_at: string; flipped: boolean }; +/** A hypothesis that came from a trace row, read against that row's verdict today. */ +export type HypothesisLink = { + hypothesis_id: string; hypothesis_revision_digest: string; statement: string; subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; subject_title: string; + state: "CURRENT" | "STALE" | "RECHECKED"; change: "CONTENT_CHANGED" | "EVIDENCE_ONLY" | "FLIPPED" | "SUBJECT_MISSING" | null; + link_state: string | null; current_state: string | null; current_verdict_revision: string | null; recheck: LinkRecheck | null; +}; +export type ReasonDistribution = { total: number; flipped: number; by_reason: Partial> }; +export type HypothesisLinks = { trace_digest: string | null; links: HypothesisLink[]; reason_distribution: ReasonDistribution }; + +export const hypothesisLinksKey = (projectId: string) => ["hypothesis-links", projectId] as const; + +export async function listHypothesisLinks(projectId: string, signal?: AbortSignal): Promise { + return (await rpc("hypothesis/link/list", { project_id: projectId }, crypto.randomUUID(), signal)).value; +} + +export async function recheckHypothesisLinks(projectId: string, hypothesisIds: string[], reason: RecheckReasonCode, note: string, currentVerdictRevision: string | null) { + return (await rpc("hypothesis/link/recheck", { + project_id: projectId, hypothesis_ids: hypothesisIds, reason_code: reason, note, current_verdict_revision: currentVerdictRevision, + }, crypto.randomUUID())).value; +} diff --git a/apps/web/src/api/hypothesisSame.ts b/apps/web/src/api/hypothesisSame.ts new file mode 100644 index 0000000..7b9c09b --- /dev/null +++ b/apps/web/src/api/hypothesisSame.ts @@ -0,0 +1,19 @@ +import { rpc } from "./rpcClient"; + +/** What a screen shows about a hypothesis a person marked as the same as another; empty for one that cannot be read. */ +export type SameMember = { statement?: string; investigated_at?: string; subject_kind?: string; subject_id?: string }; +/** The groups of hypotheses people marked as the same one across investigations (each with at least two ids). */ +export type SameView = { groups: string[][]; pairs: string[][]; members: Record; events: unknown[] }; + +export const sameKey = (projectId: string) => ["hypothesis-same", projectId] as const; + +/** An answer that lacks a part reads as no group, so a screen never fails on a short or unexpected answer. */ +const whole = (value: Partial | undefined): SameView => ({ groups: value?.groups ?? [], pairs: value?.pairs ?? [], members: value?.members ?? {}, events: value?.events ?? [] }); + +export async function listSame(projectId: string, signal?: AbortSignal) { + return whole((await rpc("hypothesis/same/list", { project_id: projectId }, crypto.randomUUID(), signal)).value); +} + +export async function recordSame(projectId: string, hypothesisIds: [string, string], action: "LINK" | "UNLINK", note: string) { + return (await rpc("hypothesis/same/record", { project_id: projectId, hypothesis_ids: hypothesisIds, action, note }, crypto.randomUUID())).value; +} diff --git a/apps/web/src/api/judgmentRecords.ts b/apps/web/src/api/judgmentRecords.ts new file mode 100644 index 0000000..22da46c --- /dev/null +++ b/apps/web/src/api/judgmentRecords.ts @@ -0,0 +1,42 @@ +import { rpc } from "./rpcClient"; + +export type MatchKind = "THIS_HYPOTHESIS" | "ALTERNATIVE" | "NEITHER" | "UNDETERMINED"; +/** What a person recorded for one test: the latest result is the one that counts. */ +export type TestResult = { event_id: string; hypothesis_id: string; test_id: string; observation: string; matched: MatchKind; evidence_refs: string[]; actor_id: string; created_at: string }; +export type DiscriminationItem = { + hypothesis_id: string; results: TestResult[]; result_history_count: number; refutation_conditions: string[]; conditions_history_count: number; + elimination: "SINGLE" | "REPEATED" | null; + /** Where the recorded results leave the hypothesis, read by the server when asked; a record without it counts as none. */ + standing?: "NONE" | "FITS" | "AGAINST_ONCE" | "AGAINST_REPEATED" | "MIXED"; +}; + +export type ClosureKind = "FIX_APPLIED" | "HUMAN_CLOSED" | "WAIVER_RECORDED" | "CONDITION_CHANGED"; +export type ClosureEvent = { + event_id: string; subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; kind: ClosureKind; basis_ref: string; note: string; scope: string | null; + verdict_revision: string; verdict_digest: string; verdict_state: string; actor_id: string; created_at: string; +}; +/** A trace row that has a closure: the events, and whether the rules confirm the effect of a recorded fix. */ +export type ClosureRow = { + subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; current_state: string | null; effect_confirmed: boolean; effect_verdict_revision: string | null; + verdict_changed_since: boolean; events: ClosureEvent[]; +}; + +export const discriminationKey = (projectId: string) => ["discrimination", projectId] as const; +export const closuresKey = (projectId: string) => ["trace-closures", projectId] as const; + +export async function listDiscrimination(projectId: string, signal?: AbortSignal) { + return (await rpc<{ items: DiscriminationItem[] }>("hypothesis/test/result/list", { project_id: projectId }, crypto.randomUUID(), signal)).value; +} +export async function recordTestResult(projectId: string, hypothesisId: string, testId: string, observation: string, matched: MatchKind, evidenceRefs: string[]) { + return (await rpc("hypothesis/test/result/record", { project_id: projectId, hypothesis_id: hypothesisId, test_id: testId, observation, matched, evidence_refs: evidenceRefs }, crypto.randomUUID())).value; +} +export async function recordRefutationConditions(projectId: string, hypothesisId: string, conditions: string[]) { + return (await rpc("hypothesis/refutation/record", { project_id: projectId, hypothesis_id: hypothesisId, conditions }, crypto.randomUUID())).value; +} +export async function listClosures(projectId: string, signal?: AbortSignal) { + return (await rpc<{ closures: ClosureRow[] }>("trace/closure/list", { project_id: projectId }, crypto.randomUUID(), signal)).value; +} +export async function recordClosure(input: { projectId: string; subjectKind: string; subjectId: string; kind: ClosureKind; basisRef: string; note: string; scope: string; verdictRevision: string }) { + return (await rpc("trace/closure/record", { project_id: input.projectId, subject_kind: input.subjectKind, subject_id: input.subjectId, kind: input.kind, basis_ref: input.basisRef, + note: input.note, ...(input.scope ? { scope: input.scope } : {}), current_verdict_revision: input.verdictRevision }, crypto.randomUUID())).value; +} diff --git a/apps/web/src/api/koreanParticles.test.ts b/apps/web/src/api/koreanParticles.test.ts new file mode 100644 index 0000000..2d527aa --- /dev/null +++ b/apps/web/src/api/koreanParticles.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; +import { eulreul, eunneun, finalHasBatchim, gwawa, iga } from "./koreanParticles"; + +describe("finalHasBatchim", () => { + it("reads Hangul by its last syllable", () => { + expect(finalHasBatchim("조건")).toBe(true); + expect(finalHasBatchim("질문")).toBe(true); + expect(finalHasBatchim("가설")).toBe(true); + expect(finalHasBatchim("원문")).toBe(true); + expect(finalHasBatchim("사이")).toBe(false); + expect(finalHasBatchim("금지")).toBe(false); + }); + + it("reads a digit by how it is spoken", () => { + const spoken = [true, true, false, true, false, false, true, true, true, false]; + spoken.forEach((expected, digit) => expect(finalHasBatchim(String(digit)), String(digit)).toBe(expected)); + // 십, 백, 천 and 만 all carry a final consonant, and so does 영. + for (const value of ["10", "100", "1000", "10000"]) expect(finalHasBatchim(value), value).toBe(true); + expect(finalHasBatchim("12")).toBe(false); + expect(finalHasBatchim("A3")).toBe(true); + expect(finalHasBatchim("p.6")).toBe(true); + expect(finalHasBatchim("70.6")).toBe(true); + }); + + it("reads a Latin letter by its name and ignores case", () => { + for (const letter of ["l", "m", "n", "r", "L", "M", "N", "R"]) expect(finalHasBatchim(letter), letter).toBe(true); + for (const letter of ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "o", "p", "q", "s", "t", "u", "v", "w", "x", "y", "z"]) { + expect(finalHasBatchim(letter), letter).toBe(false); + expect(finalHasBatchim(letter.toUpperCase()), letter.toUpperCase()).toBe(false); + } + expect(finalHasBatchim("GNC")).toBe(false); + expect(finalHasBatchim("ABC")).toBe(false); + expect(finalHasBatchim("SNR")).toBe(true); + expect(finalHasBatchim("R")).toBe(true); + // A lowercase word that ends in r is read as a word (레이더, 필터), not as the letter name 알. + expect(finalHasBatchim("radar")).toBe(false); + expect(finalHasBatchim("filter")).toBe(false); + expect(finalHasBatchim("r")).toBe(true); + expect(finalHasBatchim("Sr")).toBe(false); + expect(finalHasBatchim("radar)")).toBe(false); + expect(eulreul("radar")).toBe("radar를"); + expect(eulreul("filter")).toBe("filter를"); + expect(eulreul("SNR")).toBe("SNR을"); + }); + + it("looks past closing brackets, quotes, spaces and full stops", () => { + expect(finalHasBatchim("표 3)")).toBe(true); + expect(finalHasBatchim("「인력 공백」")).toBe(true); + expect(finalHasBatchim("「일정만 늘리면 해결된다」")).toBe(false); + expect(finalHasBatchim('"조건"')).toBe(true); + expect(finalHasBatchim("GNC. ")).toBe(false); + expect(finalHasBatchim("p.6.")).toBe(true); + }); + + it("cannot tell from symbols alone, an empty text or other scripts", () => { + expect(finalHasBatchim("")).toBeNull(); + expect(finalHasBatchim(" ")).toBeNull(); + expect(finalHasBatchim("()")).toBeNull(); + expect(finalHasBatchim("…")).toBeNull(); + expect(finalHasBatchim("50%")).toBeNull(); + }); +}); + +describe("the four particle pairs share that reading", () => { + it("picks one form when the ending is known", () => { + expect(eulreul("표 3")).toBe("표 3을"); + expect(eulreul("조건")).toBe("조건을"); + expect(eulreul("GNC")).toBe("GNC를"); + expect(iga("A3")).toBe("A3이"); + expect(iga("GNC")).toBe("GNC가"); + expect(gwawa("A3")).toBe("A3과"); + expect(gwawa("GNC")).toBe("GNC와"); + expect(gwawa("p.8 GNC")).toBe("p.8 GNC와"); + expect(eunneun("「인력 공백」")).toBe("「인력 공백」은"); + expect(eunneun("「일정만 늘리면 해결된다」")).toBe("「일정만 늘리면 해결된다」는"); + }); + + it("writes both forms together when it cannot tell", () => { + expect(eulreul("50%")).toBe("50%을(를)"); + expect(iga("()")).toBe("()이(가)"); + expect(gwawa("…")).toBe("…과(와)"); + expect(eunneun("%")).toBe("%은(는)"); + }); +}); diff --git a/apps/web/src/api/koreanParticles.ts b/apps/web/src/api/koreanParticles.ts new file mode 100644 index 0000000..cf7577b --- /dev/null +++ b/apps/web/src/api/koreanParticles.ts @@ -0,0 +1,47 @@ +/** Korean particles that follow a word whose last sound is not known in advance (a number, a Latin name, a quoted phrase). */ + +// What can follow the last letter without changing how the word sounds: closing brackets and quotes, spaces and stops. +const TRAILING_MARKS = /[\s)\]}>」』)】〉》"'”’.,;:!?…]+$/u; +// 영·일·삼·육·칠·팔 end in a consonant; 이·사·오·구 do not. A number ending in 0 is spoken 십, 백, 천 or 만, which all do. +const DIGITS_WITH_BATCHIM = new Set(["0", "1", "3", "6", "7", "8"]); +// 엘·엠·엔·알 are the letter names that end in a consonant. +const LETTERS_WITH_BATCHIM = new Set(["l", "m", "n", "r"]); + +/** Whether the text is spoken with a final consonant; null when its last letter says nothing (symbols only, other scripts). */ +export function finalHasBatchim(text: string): boolean | null { + const letters = Array.from(text.replace(TRAILING_MARKS, "")); + const last = letters.pop(); + if (last === undefined) return null; + const code = last.charCodeAt(0); + if (code >= 0xac00 && code <= 0xd7a3) return (code - 0xac00) % 28 !== 0; + if (/^[0-9]$/.test(last)) return DIGITS_WITH_BATCHIM.has(last); + // A lowercase word that ends in r is read as a word (레이더, 필터), not as the letter name 알; a capital R or SNR keeps the letter name. + if (last === "r" && /^[A-Za-z]$/.test(letters[letters.length - 1] ?? "")) return false; + if (/^[A-Za-z]$/.test(last)) return LETTERS_WITH_BATCHIM.has(last.toLowerCase()); + return null; +} + +function withParticle(text: string, withBatchim: string, without: string): string { + const batchim = finalHasBatchim(text); + return batchim === null ? `${text}${withBatchim}(${without})` : `${text}${batchim ? withBatchim : without}`; +} + +/** 을/를 */ +export function eulreul(text: string): string { + return withParticle(text, "을", "를"); +} + +/** 이/가 */ +export function iga(text: string): string { + return withParticle(text, "이", "가"); +} + +/** 과/와 */ +export function gwawa(text: string): string { + return withParticle(text, "과", "와"); +} + +/** 은/는 */ +export function eunneun(text: string): string { + return withParticle(text, "은", "는"); +} diff --git a/apps/web/src/api/lessons.ts b/apps/web/src/api/lessons.ts new file mode 100644 index 0000000..a6f94a6 --- /dev/null +++ b/apps/web/src/api/lessons.ts @@ -0,0 +1,22 @@ +import { rpc } from "./rpcClient"; +import type { MatchKind } from "./judgmentRecords"; + +export type LessonKind = "TEST_RESULT" | "ELIMINATION" | "EFFECT_CONFIRMED" | "HUMAN_CLOSURE"; +export type LessonState = "SAME_CONDITION" | "STALE" | "REFUTED"; +export type LessonDetail = { + observations?: { test_id: string; matched: MatchKind; observation: string; evidence_refs: string[] }[]; + closure_kind?: string; basis_ref?: string; note?: string; scope?: string | null; original_state?: string; +}; +/** A lesson: a reference to a record a rule computed or a person made, read back for the row's exact condition. */ +export type LessonItem = { + lesson_id: string; kind: LessonKind; outcome: string; state: LessonState; subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; + hypothesis_id: string | null; test_id: string | null; actor_id: string; created_at: string; detail: LessonDetail; + /** The other hypotheses a person marked as the same as this lesson's hypothesis; absent when none. */ + same_hypothesis_ids?: string[]; +}; + +export const lessonsKey = (projectId: string) => ["lessons", projectId] as const; + +export async function listLessons(projectId: string, signal?: AbortSignal) { + return (await rpc<{ lessons: LessonItem[] }>("trace/lesson/list", { project_id: projectId }, crypto.randomUUID(), signal)).value; +} diff --git a/apps/web/src/api/localWorkspacePersistence.test.ts b/apps/web/src/api/localWorkspacePersistence.test.ts index ff7f003..4207b85 100644 --- a/apps/web/src/api/localWorkspacePersistence.test.ts +++ b/apps/web/src/api/localWorkspacePersistence.test.ts @@ -72,6 +72,22 @@ it("pins a bounded pending key to one workspace and exact submitted payload", () expect(readLocalPending(a.mode === "LOCAL" ? a.workspaceId : null, "p", "t")).toEqual({ kind: "NONE" }); }); +it("keeps a trace-row origin with the pending request, and a different origin is a different request", () => { + const id = a.mode === "LOCAL" ? a.workspaceId : null; + const origin = { kind: "TRACE_VERDICT" as const, project_id: "p", subject_kind: "CRITERION" as const, subject_id: "C-1", verdict_revision: "a".repeat(64) }; + const value: PendingSubmission = { version: 1, workspaceId: id!, projectId: "p", threadId: "t", problem: "synthetic question", selection: null, origin, + signature: submissionSignature("p", "t", "synthetic question", null, origin), key: "11111111-1111-4111-8111-111111111111", createdAt: 1 }; + expect(writeLocalPending(value)).toBe("SAVED"); + expect(readLocalPending(id, "p", "t")).toEqual({ kind: "PENDING", value }); + expect(submissionSignature("p", "t", "synthetic question", null, origin)).not.toBe(submissionSignature("p", "t", "synthetic question", null)); + expect(submissionSignature("p", "t", "synthetic question", null, null)).toBe(submissionSignature("p", "t", "synthetic question", null)); // no origin: the old signature + expect(writeLocalPending({ ...value, signature: submissionSignature("p", "t", "synthetic question", null) })).toBe("UNAVAILABLE"); // the origin is part of the exact payload + // a made-up origin shape is not a pending request + localStorage.setItem(`thoth:local-workspace:v1:pending:${JSON.stringify([id, "p", "t"])}`, JSON.stringify({ ...value, origin: { ...origin, extra: 1 } })); + expect(readLocalPending(id, "p", "t")).toEqual({ kind: "INVALID" }); + clearLocalPending(id, "p", "t"); +}); + it("blocks a corrupt pending marker instead of assigning a new key", () => { const workspaceId = a.mode === "LOCAL" ? a.workspaceId : ""; localStorage.setItem(`thoth:local-workspace:v1:pending:${JSON.stringify([workspaceId, "p", "t"])}`, "{broken"); diff --git a/apps/web/src/api/localWorkspacePersistence.ts b/apps/web/src/api/localWorkspacePersistence.ts index 12cb967..d0b90c8 100644 --- a/apps/web/src/api/localWorkspacePersistence.ts +++ b/apps/web/src/api/localWorkspacePersistence.ts @@ -5,8 +5,9 @@ export type ContextReadResult = { kind: "MISSING" | "VALID" | "CORRUPT" | "UNAVA 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; - problem: string; selection: PendingModelSelection | null; signature: string; key: string; createdAt: number }; + problem: string; selection: PendingModelSelection | null; origin?: PendingOrigin | null; signature: string; key: string; createdAt: number }; export type PendingRead = { kind: "NONE" } | { kind: "PENDING"; value: PendingSubmission } | { kind: "INVALID" }; const prefix = "thoth:local-workspace:v1:"; @@ -31,8 +32,10 @@ function pendingKey(workspaceId: string, projectId: string, threadId: string) { return `${prefix}pending:${JSON.stringify([workspaceId, projectId, threadId])}`; } -export function submissionSignature(projectId: string, threadId: string, problem: string, selection: PendingModelSelection | null): string { - return JSON.stringify([projectId, threadId, problem, selection ? [selection.provider, selection.model, selection.reasoning_effort ?? null] : null]); +/** 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 { + return JSON.stringify([projectId, threadId, problem, selection ? [selection.provider, selection.model, selection.reasoning_effort ?? null] : null, + ...(origin ? [[origin.kind, origin.project_id, origin.subject_kind, origin.subject_id, origin.verdict_revision]] : [])]); } export function inspectLocalContext(workspaceId: string | null): ContextReadResult { @@ -105,21 +108,31 @@ export function readLocalPending(workspaceId: string | null, projectId: string, 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", "signature", "key", "createdAt"].includes(key)) || + !Object.keys(value).every(key => ["version", "workspaceId", "projectId", "threadId", "problem", "selection", "origin", "signature", "key", "createdAt"].includes(key)) || !("workspaceId" in value) || value.workspaceId !== workspaceId || !("projectId" in value) || value.projectId !== projectId || !("threadId" in value) || value.threadId !== threadId || !("problem" in value) || typeof value.problem !== "string" || value.problem.length > maxDraftLength || !("selection" in value) || !(value.selection === null || validPendingSelection(value.selection)) || + ("origin" in value && !(value.origin === null || validPendingOrigin(value.origin))) || !("signature" in value) || typeof value.signature !== "string" || !("key" in value) || typeof value.key !== "string" || !/^[a-f0-9-]{36}$/i.test(value.key) || !("createdAt" in value) || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return { kind: "INVALID" }; const pending = value as PendingSubmission; - return pending.signature === submissionSignature(projectId, threadId, pending.problem, pending.selection) + return pending.signature === submissionSignature(projectId, threadId, pending.problem, pending.selection, pending.origin ?? null) ? { kind: "PENDING", value: pending } : { kind: "INVALID" }; } catch { return { kind: "INVALID" }; } } +function validPendingOrigin(value: unknown): value is PendingOrigin { + const text = (item: unknown, max: number) => typeof item === "string" && item.length > 0 && item.length <= max; + return Boolean(value && typeof value === "object" && !Array.isArray(value) && + Object.keys(value).every(key => ["kind", "project_id", "subject_kind", "subject_id", "verdict_revision"].includes(key)) && + "kind" in value && value.kind === "TRACE_VERDICT" && "project_id" in value && text(value.project_id, maxIdentityLength) && + "subject_kind" in value && (value.subject_kind === "CRITERION" || value.subject_kind === "REQUIREMENT") && + "subject_id" in value && text(value.subject_id, 500) && "verdict_revision" in value && text(value.verdict_revision, 64)); +} + function validPendingSelection(value: unknown): value is PendingModelSelection { return Boolean(value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).every(key => ["provider", "model", "reasoning_effort"].includes(key)) && @@ -131,8 +144,8 @@ function validPendingSelection(value: unknown): value is PendingModelSelection { export function writeLocalPending(value: PendingSubmission): 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.signature !== submissionSignature(value.projectId, value.threadId, value.problem, value.selection)) return "UNAVAILABLE"; + !(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"; 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"; } diff --git a/apps/web/src/api/recordViews.ts b/apps/web/src/api/recordViews.ts index 467e5aa..346225d 100644 --- a/apps/web/src/api/recordViews.ts +++ b/apps/web/src/api/recordViews.ts @@ -19,6 +19,7 @@ export const recordViews: RecordView[] = [ {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를 입증하지 않습니다. 승인된 프로토콜·참여 동의가 있는 경우에만 고급 명령을 사용하세요."}, diff --git a/apps/web/src/api/research.ts b/apps/web/src/api/research.ts index 74ff6e7..045f7ee 100644 --- a/apps/web/src/api/research.ts +++ b/apps/web/src/api/research.ts @@ -120,6 +120,14 @@ export type ResultUsage = { cached_input_tokens: number | null; unreported_calls: number; state: string; wall_ms: number | null; /** Calls sent again on their own after an earlier call was cut off. */ auto_retries?: number; + /** Part of the figures above that a purpose-marked call used, such as widening the memory question. */ + by_purpose?: Record; +}; +/** A closed-loop test that was held instead of run. Only a hold with an unsettled earlier run has anything to clear (exits). */ +export type ExecutionHold = { + schema_version: string; reason_code: string | null; plan_execution_id: string | null; execution_revision: number | null; + attempt_id: string | null; attempt_state: string | null; exits: string[]; + input_difference: { same?: boolean; previous_count?: number; current_count?: number; removed?: string[]; added?: string[] } | null; }; export type ResearchStatus = WorkThread & { usage?: { input_tokens: number | null; output_tokens: number | null; total_tokens: number | null; state: string; unreported_calls: number; cumulative_token_limit_enforced: boolean; cached_input_tokens?: number | null }; @@ -130,6 +138,8 @@ export type ResearchStatus = WorkThread & { execution_summary?: {effective_execution_state:string;state_inconsistent:boolean;last_checkpoint_only:boolean;automatic_retry:boolean}; freshness?: string; current_result?: ResultManifest | null; previous_result?: ResultManifest | null; basis_currentness?: Currentness; + /** Present when the stored result carries a held closed-loop test. */ + execution_hold?: ExecutionHold | null; user_progress_summary?: UserProgressSummary | null; coverage_matrix?: CoverageMatrix | null; next_user_action?: NextUserAction | null; diff --git a/apps/web/src/api/researchProgress.ts b/apps/web/src/api/researchProgress.ts index be2c6cf..8191534 100644 --- a/apps/web/src/api/researchProgress.ts +++ b/apps/web/src/api/researchProgress.ts @@ -1,12 +1,27 @@ -import type { CompletedStage, ResearchStatus } from "./research"; +import type { ResearchStatus } from "./research"; import { sourceCardTitle } from "../sourceDisplay"; - -export type ProgressScene = { - title: string; - detail: string; -}; - -export type TimelineIcon = "tick-circle" | "document-open" | "cloud" | "search" | "form"; +import { + formatDuration, + ACTION_COPY, + clip, + compactPayload, + formatBytes, + hypothesisActionPayload, + locatorPayload, + missingFor, + modelActionPayload, + sceneFor, + stageActionPayload, + unique, + usageLine, + waitLine, + type ProgressScene, + type TimelineIcon, + type WorkFacts, +} from "./researchProgressText"; + +export { formatDuration }; +export type { ProgressScene, TimelineIcon }; export type TimelineEvent = { id: string; @@ -34,24 +49,6 @@ export type LiveResearchProgress = { usageLine: string | null; }; -type WorkFacts = { - focus: string | null; - constraints: string[]; - locators: string[]; - hypotheses: string[]; - pendingHypotheses: string[]; - decidedHypotheses: string[]; - sources: string[]; -}; - -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" }, -}; - function asRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) @@ -66,97 +63,6 @@ function numberOrNull(value: unknown): number | null { return typeof value === "number" && Number.isFinite(value) ? value : null; } -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}초`; -} - -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`; -} - -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()}…`; -} - -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)}」`)); -} - -function hangulBatchim(text: string): boolean | null { - const last = text.trim().slice(-1); - const code = last.charCodeAt(0); - if (code < 0xac00 || code > 0xd7a3) { - return null; - } - return (code - 0xac00) % 28 !== 0; -} - -function eulreul(text: string): string { - return hangulBatchim(text) === true ? `${text}을` : `${text}를`; -} - -function iga(text: string): string { - return hangulBatchim(text) === true ? `${text}이` : `${text}가`; -} - -function wagwa(text: string): string { - if (hangulBatchim(text) === true || /[0-9]/.test(text.trim().slice(-1))) { - return `${text}과`; - } - return `${text}와`; -} - function questionText(status?: ResearchStatus): string { return [status?.request?.authored_text, status?.request?.effective_question, status?.problem] .filter((value): value is string => typeof value === "string" && value.trim().length > 1 && value.trim() !== "q") @@ -245,181 +151,6 @@ function workFacts(status: ResearchStatus, sourceUris: string[] = []): WorkFacts }; } -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 - ? `${wagwa(locatorSubject)} 관련된 문장을 다시 줄 세웠습니다` - : sources - ? `${sources}에서 ${focus ?? "질문"}과 관련된 문장을 다시 줄 세웠습니다` - : `${focus ?? "질문"}과 관련된 문장을 다시 줄 세웠습니다`, - locatorSubject - ? `${wagwa(locatorSubject)} 관련된 문장을 다시 줄 세우고 있습니다` - : `${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 ? `${focus}를 확인하려면 다음에 할 작업을 골랐습니다` : "다음에 할 확인 작업을 골랐습니다", - focus ? `${focus}를 확인하려면 다음에 할 작업을 고르고 있습니다` : "다음에 할 확인 작업을 고르고 있습니다", - ), - detail: "실행 전 초안입니다.", - }; - case "EVIDENCE_EXTRACTOR": - case "EVIDENCE_FOCUS": - return { - title: title( - locators ? `${locators} 구절을 골랐습니다` : `${focus ?? "표와 원문"} 위치를 확인했습니다`, - locators ? `${locators} 위치를 확인하고 있습니다` : `${focus ?? "표와 원문"} 위치를 확인하고 있습니다`, - ), - detail: "실제로 집어넣을 칸과 문장을 찾습니다.", - }; - case "SUFFICIENCY_EXPLAINER": - return { - title: title( - `${focus ?? "질문"}을 지금 근거로 답할 수 있는지 설명했습니다`, - `${focus ?? "질문"}을 지금 근거로 답할 수 있는지 설명하고 있습니다`, - ), - detail: "부족한 항목은 보류 이유로 남깁니다.", - }; - case "COUNTEREVIDENCE_CHALLENGER": - return { - title: title( - hyps ? `${hyps}를 뒤집을 근거를 찾았습니다` : `${focus ?? "질문"}을 뒤집을 근거를 찾았습니다`, - hyps ? `${hyps}를 뒤집을 근거를 찾고 있습니다` : `${focus ?? "질문"}을 뒤집을 근거를 찾고 있습니다`, - ), - detail: "지지 문장만 보지 않습니다.", - }; - case "USER_EXPLAINER": - return { - title: title( - `${focus ?? "현재 판단"}을 사람이 읽게 정리했습니다`, - `${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 - ? `${focus ?? "요청"}을 받아 자료와 조건을 확인하고 있습니다` - : `${focus ?? "요청"} 관련 단계를 마쳤습니다`, - detail: "서버가 준 단계 이름만으로는 더 구체적인 작업을 특정하지 못했습니다.", - }; - } -} - export function evidenceLabels(status?: ResearchStatus): string[] { const draft = asRecord(status?.attempt?.draft_progress); const focus = asRecord(draft?.evidence_focus); @@ -477,176 +208,6 @@ function modelLine(status?: ResearchStatus): string | null { return model ?? `추론 ${effort}`; } -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}`; -} - -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; -} - -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 `${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; -} - -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; -} - -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]); -} - -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, - ]); -} - -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, - ]), - }; -} - -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}개`; -} - function actionEvent( id: string, actionType: NonNullable, diff --git a/apps/web/src/api/researchProgressCharacterization.test.ts b/apps/web/src/api/researchProgressCharacterization.test.ts new file mode 100644 index 0000000..812d846 --- /dev/null +++ b/apps/web/src/api/researchProgressCharacterization.test.ts @@ -0,0 +1,380 @@ +import { describe, expect, it } from "vitest"; + +import { composerActivity, evidenceLabels, formatDuration, liveResearchProgress } from "./researchProgress"; +import type { ResearchStatus } from "./research"; + +/** + * Pins the wording that researchProgress produces today (Korean particles, durations, sizes, scene + * sentences, one-line texts), so that moving the text code into its own file changes nothing. + * Only the public exports are used; the expected values were taken from the code before the move. + */ + +function status(overrides: Record = {}): ResearchStatus { + return { + thread_id: "thread:t", + project_id: "project:p", + cycle_id: "cycle:c", + problem: "Psyche 표 3 발사 준비 조건", + lifecycle: "ACTIVE", + execution_state: "RUNNING", + current_object_ids: [], + working_head_digest: "0".repeat(64), + operation_state: "RUNNING", + attempt: { operation_id: "operation:o", phase: "RESEARCH_PLANNER", status: "RUNNING" }, + completed_stages: [], + ...overrides, + } as ResearchStatus; +} + +function inPhase(phase: string, extra: Record = {}, draft: Record = {}) { + return status({ + attempt: { operation_id: "operation:o", phase, status: "RUNNING", draft_progress: draft }, + ...extra, + }); +} + +const RICH_DRAFT = { + evidence_focus: { locators: [{ page: 6, exact_text: "70.6" }, { page: 8, exact_text: "GNC" }], total: 5 }, + portfolio: { + hypotheses: [ + { hypothesis_id: "h1", statement: "일정만 늘리면 해결된다" }, + { hypothesis_id: "h2", statement: "GNC 소프트웨어만 닫으면 된다" }, + { hypothesis_id: "h3", statement: "인력 공백" }, + ], + }, + hypothesis_review: { decisions: [{ hypothesis_id: "h1" }] }, +}; +const QUESTION = { + request: { + operation_id: "operation:o", + authored_text: "Psyche 표 3 발사 준비 조건 4개가 원문에서 각각 확인되는지 적어라. 표 숫자만 인용하고 추측 금지. 원문 위치와 함께 답.", + }, +}; +const SOURCES = ["C:/papers/psyche-irb-2022.pdf", "C:/papers/gao-23-106021.pdf", "C:/papers/third.pdf"]; + +const KEYS = [ + "RESEARCH_PLANNER", "REQUIREMENTS", "EVIDENCE_RERANKER", "SEMANTIC_REVIEWER", "EVIDENCE_REVIEW", + "REVIEW_ADJUDICATOR", "SOURCE_PLANNER", "HYPOTHESIS_GENERATOR", "HYPOTHESES", "HYPOTHESIS_REVIEWER", + "HYPOTHESIS_REVIEW", "ACTION_PLANNER", "ACTION_COMPARISON", "EVIDENCE_EXTRACTOR", "EVIDENCE_FOCUS", + "SUFFICIENCY_EXPLAINER", "COUNTEREVIDENCE_CHALLENGER", "USER_EXPLAINER", "REFERENCE_MAPPER", + "CONNECTED_SOURCES", "SOURCE_SHORTLIST", "HOLD", "SOMETHING_ELSE", +]; + +describe("durations", () => { + it.each([ + [null, null], [undefined, null], [Number.NaN, null], [-1, null], [Number.POSITIVE_INFINITY, null], + [0, "0ms"], [999, "999ms"], [1000, "1초"], [1499, "1초"], [59499, "59초"], [59500, "1분"], + [60000, "1분"], [61000, "1분 1초"], [91700, "1분 32초"], [3600000, "60분"], + ])("formats %s as %s", (input, expected) => { + expect(formatDuration(input as number | null | undefined)).toBe(expected); + }); +}); + +describe("scene sentences", () => { + it("pins the running and finished sentence of every phase with the facts of a rich question", () => { + const lines: string[] = []; + for (const key of KEYS) { + for (const state of ["RUNNING", "SUCCEEDED"]) { + const live = liveResearchProgress(inPhase(key, { operation_state: state, ...QUESTION }, RICH_DRAFT), SOURCES); + lines.push(`${key} ${state}: ${live?.headline} // ${live?.current.detail} // ${live?.missing}`); + } + } + expect(lines).toMatchInlineSnapshot(` + [ + "RESEARCH_PLANNER RUNNING: 표 3 발사 준비 조건 4개, 표 숫자만 인용, 추측 금지를 고정하고 있습니다 // 아직 답을 쓰지 않았습니다. // null", + "RESEARCH_PLANNER SUCCEEDED: 표 3 발사 준비 조건 4개, 표 숫자만 인용, 추측 금지를 고정했습니다 // 아직 답을 쓰지 않았습니다. // null", + "REQUIREMENTS RUNNING: 표 3 발사 준비 조건 4개, 표 숫자만 인용, 추측 금지를 고정하고 있습니다 // 아직 답을 쓰지 않았습니다. // null", + "REQUIREMENTS SUCCEEDED: 표 3 발사 준비 조건 4개, 표 숫자만 인용, 추측 금지를 고정했습니다 // 아직 답을 쓰지 않았습니다. // null", + "EVIDENCE_RERANKER RUNNING: p.6 70.6, p.8 GNC와 관련된 문장을 다시 줄 세우고 있습니다 // 순위만 매겼고, 지지 여부는 아직 판단하지 않았습니다. // null", + "EVIDENCE_RERANKER SUCCEEDED: p.6 70.6, p.8 GNC와 관련된 문장을 다시 줄 세웠습니다 // 순위만 매겼고, 지지 여부는 아직 판단하지 않았습니다. // null", + "SEMANTIC_REVIEWER RUNNING: 표 3 발사 준비 조건 4개가 p.6 70.6, p.8 GNC 원문에 있는지 대조하고 있습니다 // 관련 있다고 해서 지지하는 것은 아닙니다. // null", + "SEMANTIC_REVIEWER SUCCEEDED: 표 3 발사 준비 조건 4개가 p.6 70.6, p.8 GNC 원문에 있는지 대조했습니다 // 관련 있다고 해서 지지하는 것은 아닙니다. // null", + "EVIDENCE_REVIEW RUNNING: 표 3 발사 준비 조건 4개가 p.6 70.6, p.8 GNC 원문에 있는지 대조하고 있습니다 // 관련 있다고 해서 지지하는 것은 아닙니다. // null", + "EVIDENCE_REVIEW SUCCEEDED: 표 3 발사 준비 조건 4개가 p.6 70.6, p.8 GNC 원문에 있는지 대조했습니다 // 관련 있다고 해서 지지하는 것은 아닙니다. // null", + "REVIEW_ADJUDICATOR RUNNING: Psyche 표 3 발사 준비 조건 중 빠진 항목과 충돌을 판정하고 있습니다 // 애매한 항목은 보류로 남깁니다. // null", + "REVIEW_ADJUDICATOR SUCCEEDED: Psyche 표 3 발사 준비 조건 중 빠진 항목과 충돌을 판정했습니다 // 애매한 항목은 보류로 남깁니다. // null", + "SOURCE_PLANNER RUNNING: psyche-irb-2022.pdf, gao-23-106021.pdf 외에 더 열 주소를 고르고 있습니다 // 인터넷 전체를 검색하지 않습니다. // null", + "SOURCE_PLANNER SUCCEEDED: psyche-irb-2022.pdf, gao-23-106021.pdf 외에 더 열 주소를 골랐습니다 // 인터넷 전체를 검색하지 않습니다. // null", + "HYPOTHESIS_GENERATOR RUNNING: 「일정만 늘리면 해결된다」, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」을 준비하고 있습니다 // 아직 채택이 아닙니다. // null", + "HYPOTHESIS_GENERATOR SUCCEEDED: 「일정만 늘리면 해결된다」, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」을 준비했습니다 // 아직 채택이 아닙니다. // null", + "HYPOTHESES RUNNING: 「일정만 늘리면 해결된다」, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」을 준비하고 있습니다 // 아직 채택이 아닙니다. 「일정만 늘리면 해결된다」는 봤고, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」 2개는 아직입니다. 빠진 결정을 채우겠습니다. // 「일정만 늘리면 해결된다」는 봤고, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」 2개는 아직입니다. 빠진 결정을 채우겠습니다.", + "HYPOTHESES SUCCEEDED: 「일정만 늘리면 해결된다」, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」을 준비했습니다 // 아직 채택이 아닙니다. 「일정만 늘리면 해결된다」는 봤고, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」 2개는 아직입니다. 빠진 결정을 채우겠습니다. // 「일정만 늘리면 해결된다」는 봤고, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」 2개는 아직입니다. 빠진 결정을 채우겠습니다.", + "HYPOTHESIS_REVIEWER RUNNING: 「일정만 늘리면 해결된다」를 원문과 대조하고 있습니다 // 제안된 설명마다 결정이 있어야 검토가 끝납니다. // null", + "HYPOTHESIS_REVIEWER SUCCEEDED: 「일정만 늘리면 해결된다」를 원문과 대조했습니다 // 제안된 설명마다 결정이 있어야 검토가 끝납니다. // null", + "HYPOTHESIS_REVIEW RUNNING: 「일정만 늘리면 해결된다」를 원문과 대조하고 있습니다 // 제안된 설명마다 결정이 있어야 검토가 끝납니다. 「일정만 늘리면 해결된다」는 봤고, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」 2개는 아직입니다. 빠진 결정을 채우겠습니다. // 「일정만 늘리면 해결된다」는 봤고, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」 2개는 아직입니다. 빠진 결정을 채우겠습니다.", + "HYPOTHESIS_REVIEW SUCCEEDED: 「일정만 늘리면 해결된다」를 원문과 대조했습니다 // 제안된 설명마다 결정이 있어야 검토가 끝납니다. 「일정만 늘리면 해결된다」는 봤고, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」 2개는 아직입니다. 빠진 결정을 채우겠습니다. // 「일정만 늘리면 해결된다」는 봤고, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」 2개는 아직입니다. 빠진 결정을 채우겠습니다.", + "ACTION_PLANNER RUNNING: Psyche 표 3 발사 준비 조건을 확인하려면 다음에 할 작업을 고르고 있습니다 // 실행 전 초안입니다. // null", + "ACTION_PLANNER SUCCEEDED: Psyche 표 3 발사 준비 조건을 확인하려면 다음에 할 작업을 골랐습니다 // 실행 전 초안입니다. // null", + "ACTION_COMPARISON RUNNING: Psyche 표 3 발사 준비 조건을 확인하려면 다음에 할 작업을 고르고 있습니다 // 실행 전 초안입니다. // null", + "ACTION_COMPARISON SUCCEEDED: Psyche 표 3 발사 준비 조건을 확인하려면 다음에 할 작업을 골랐습니다 // 실행 전 초안입니다. // null", + "EVIDENCE_EXTRACTOR RUNNING: p.6 70.6, p.8 GNC 위치를 확인하고 있습니다 // 실제로 집어넣을 칸과 문장을 찾습니다. // null", + "EVIDENCE_EXTRACTOR SUCCEEDED: p.6 70.6, p.8 GNC 구절을 골랐습니다 // 실제로 집어넣을 칸과 문장을 찾습니다. // null", + "EVIDENCE_FOCUS RUNNING: p.6 70.6, p.8 GNC 위치를 확인하고 있습니다 // 실제로 집어넣을 칸과 문장을 찾습니다. p.6 70.6, p.8 GNC 2곳을 확인했습니다 · 전체 5곳 중 일부. // p.6 70.6, p.8 GNC 2곳을 확인했습니다 · 전체 5곳 중 일부.", + "EVIDENCE_FOCUS SUCCEEDED: p.6 70.6, p.8 GNC 구절을 골랐습니다 // 실제로 집어넣을 칸과 문장을 찾습니다. p.6 70.6, p.8 GNC 2곳을 확인했습니다 · 전체 5곳 중 일부. // p.6 70.6, p.8 GNC 2곳을 확인했습니다 · 전체 5곳 중 일부.", + "SUFFICIENCY_EXPLAINER RUNNING: Psyche 표 3 발사 준비 조건을 지금 근거로 답할 수 있는지 설명하고 있습니다 // 부족한 항목은 보류 이유로 남깁니다. // null", + "SUFFICIENCY_EXPLAINER SUCCEEDED: Psyche 표 3 발사 준비 조건을 지금 근거로 답할 수 있는지 설명했습니다 // 부족한 항목은 보류 이유로 남깁니다. // null", + "COUNTEREVIDENCE_CHALLENGER RUNNING: 「일정만 늘리면 해결된다」, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」을 뒤집을 근거를 찾고 있습니다 // 지지 문장만 보지 않습니다. // null", + "COUNTEREVIDENCE_CHALLENGER SUCCEEDED: 「일정만 늘리면 해결된다」, 「GNC 소프트웨어만 닫으면 된다」, 「인력 공백」을 뒤집을 근거를 찾았습니다 // 지지 문장만 보지 않습니다. // null", + "USER_EXPLAINER RUNNING: Psyche 표 3 발사 준비 조건을 사람이 읽게 정리하고 있습니다 // 내부 코드명이 아니라 질문 기준으로 설명합니다. // null", + "USER_EXPLAINER SUCCEEDED: Psyche 표 3 발사 준비 조건을 사람이 읽게 정리했습니다 // 내부 코드명이 아니라 질문 기준으로 설명합니다. // null", + "REFERENCE_MAPPER RUNNING: p.6 70.6, p.8 GNC 인용을 원문 위치에 맞추고 있습니다 // 표 번호와 셀 위치를 자료에 연결합니다. // null", + "REFERENCE_MAPPER SUCCEEDED: p.6 70.6, p.8 GNC 인용을 원문 위치에 맞춰 두었습니다 // 표 번호와 셀 위치를 자료에 연결합니다. // null", + "CONNECTED_SOURCES RUNNING: psyche-irb-2022.pdf, gao-23-106021.pdf만 열고 있습니다 // 웹 검색을 시작했다는 뜻이 아닙니다. psyche-irb-2022.pdf, gao-23-106021.pdf 목록을 여는 중이며, 답을 쓰기 전입니다. // psyche-irb-2022.pdf, gao-23-106021.pdf 목록을 여는 중이며, 답을 쓰기 전입니다.", + "CONNECTED_SOURCES SUCCEEDED: psyche-irb-2022.pdf, gao-23-106021.pdf만 열고 있습니다 // 웹 검색을 시작했다는 뜻이 아닙니다. psyche-irb-2022.pdf, gao-23-106021.pdf 목록을 여는 중이며, 답을 쓰기 전입니다. // psyche-irb-2022.pdf, gao-23-106021.pdf 목록을 여는 중이며, 답을 쓰기 전입니다.", + "SOURCE_SHORTLIST RUNNING: psyche-irb-2022.pdf, gao-23-106021.pdf에서 질문과 맞는 원문 구간을 고르고 있습니다 // 시간 미확인 자료는 확정 근거가 아니라 후보입니다. psyche-irb-2022.pdf, gao-23-106021.pdf에서 원문 구간을 고르는 중이며, 답을 쓰기 전입니다. // psyche-irb-2022.pdf, gao-23-106021.pdf에서 원문 구간을 고르는 중이며, 답을 쓰기 전입니다.", + "SOURCE_SHORTLIST SUCCEEDED: psyche-irb-2022.pdf, gao-23-106021.pdf에서 질문과 맞는 원문 구간을 골랐습니다 // 시간 미확인 자료는 확정 근거가 아니라 후보입니다. psyche-irb-2022.pdf, gao-23-106021.pdf에서 원문 구간을 고르는 중이며, 답을 쓰기 전입니다. // psyche-irb-2022.pdf, gao-23-106021.pdf에서 원문 구간을 고르는 중이며, 답을 쓰기 전입니다.", + "HOLD RUNNING: 모델 응답을 기다리다 멈췄습니다 // 연결 자료는 그대로 있고, 답을 확정하지 않았습니다. 답을 쓰기 전에 모델 호출이 멈췄습니다. // 답을 쓰기 전에 모델 호출이 멈췄습니다.", + "HOLD SUCCEEDED: 모델 호출이 끝나 연구를 멈췄습니다 // 연결 자료는 그대로 있고, 답을 확정하지 않았습니다. 답을 쓰기 전에 모델 호출이 멈췄습니다. // 답을 쓰기 전에 모델 호출이 멈췄습니다.", + "SOMETHING_ELSE RUNNING: Psyche 표 3 발사 준비 조건을 받아 자료와 조건을 확인하고 있습니다 // 서버가 준 단계 이름만으로는 더 구체적인 작업을 특정하지 못했습니다. // null", + "SOMETHING_ELSE SUCCEEDED: Psyche 표 3 발사 준비 조건 관련 단계를 마쳤습니다 // 서버가 준 단계 이름만으로는 더 구체적인 작업을 특정하지 못했습니다. // null", + ] + `); + }); + + it("pins the sentences when the question carries almost no facts", () => { + const lines: string[] = []; + for (const key of KEYS) { + const live = liveResearchProgress(inPhase(key, { problem: "q" }), []); + lines.push(`${key}: ${live?.headline} // ${live?.current.detail} // ${live?.missing}`); + } + expect(lines).toMatchInlineSnapshot(` + [ + "RESEARCH_PLANNER: 질문에서 꼭 지킬 조건을 고정하고 있습니다 // 아직 답을 쓰지 않았습니다. // null", + "REQUIREMENTS: 질문에서 꼭 지킬 조건을 고정하고 있습니다 // 아직 답을 쓰지 않았습니다. // null", + "EVIDENCE_RERANKER: 질문과 관련된 문장을 다시 줄 세우고 있습니다 // 순위만 매겼고, 지지 여부는 아직 판단하지 않았습니다. // null", + "SEMANTIC_REVIEWER: 질문 조건을 원문과 대조하고 있습니다 // 관련 있다고 해서 지지하는 것은 아닙니다. // null", + "EVIDENCE_REVIEW: 질문 조건을 원문과 대조하고 있습니다 // 관련 있다고 해서 지지하는 것은 아닙니다. // null", + "REVIEW_ADJUDICATOR: 조건 중 빠진 항목과 충돌을 판정하고 있습니다 // 애매한 항목은 보류로 남깁니다. // null", + "SOURCE_PLANNER: 허용된 사이트에서 더 열 주소를 고르고 있습니다 // 인터넷 전체를 검색하지 않습니다. // null", + "HYPOTHESIS_GENERATOR: 질문에 대한 가능한 설명을 준비하고 있습니다 // 아직 채택이 아닙니다. // null", + "HYPOTHESES: 질문에 대한 가능한 설명을 준비하고 있습니다 // 아직 채택이 아닙니다. // null", + "HYPOTHESIS_REVIEWER: 방금 만든 설명을 원문과 대조하고 있습니다 // 제안된 설명마다 결정이 있어야 검토가 끝납니다. // null", + "HYPOTHESIS_REVIEW: 방금 만든 설명을 원문과 대조하고 있습니다 // 제안된 설명마다 결정이 있어야 검토가 끝납니다. // null", + "ACTION_PLANNER: 다음에 할 확인 작업을 고르고 있습니다 // 실행 전 초안입니다. // null", + "ACTION_COMPARISON: 다음에 할 확인 작업을 고르고 있습니다 // 실행 전 초안입니다. // null", + "EVIDENCE_EXTRACTOR: 표와 원문 위치를 확인하고 있습니다 // 실제로 집어넣을 칸과 문장을 찾습니다. // null", + "EVIDENCE_FOCUS: 표와 원문 위치를 확인하고 있습니다 // 실제로 집어넣을 칸과 문장을 찾습니다. 질문에 집어넣을 표·문장이 아직 0곳입니다. 붙인 로컬 원문이 없어 웹 검색은 이 단계에서 시작하지 않습니다. // 질문에 집어넣을 표·문장이 아직 0곳입니다. 붙인 로컬 원문이 없어 웹 검색은 이 단계에서 시작하지 않습니다.", + "SUFFICIENCY_EXPLAINER: 질문을 지금 근거로 답할 수 있는지 설명하고 있습니다 // 부족한 항목은 보류 이유로 남깁니다. // null", + "COUNTEREVIDENCE_CHALLENGER: 질문을 뒤집을 근거를 찾고 있습니다 // 지지 문장만 보지 않습니다. // null", + "USER_EXPLAINER: 현재 판단을 사람이 읽게 정리하고 있습니다 // 내부 코드명이 아니라 질문 기준으로 설명합니다. // null", + "REFERENCE_MAPPER: 인용과 원문 위치를 맞추고 있습니다 // 표 번호와 셀 위치를 자료에 연결합니다. // null", + "CONNECTED_SOURCES: 이 프로젝트에 허용된 자료만 열고 있습니다 // 웹 검색을 시작했다는 뜻이 아닙니다. 연결 자료 목록을 여는 중이며, 답을 쓰기 전입니다. // 연결 자료 목록을 여는 중이며, 답을 쓰기 전입니다.", + "SOURCE_SHORTLIST: 연결 자료에서 질문과 맞는 원문 구간을 고르고 있습니다 // 시간 미확인 자료는 확정 근거가 아니라 후보입니다. 연결 자료에서 원문 구간을 고르는 중이며, 답을 쓰기 전입니다. // 연결 자료에서 원문 구간을 고르는 중이며, 답을 쓰기 전입니다.", + "HOLD: 모델 응답을 기다리다 멈췄습니다 // 연결 자료는 그대로 있고, 답을 확정하지 않았습니다. 답을 쓰기 전에 모델 호출이 멈췄습니다. // 답을 쓰기 전에 모델 호출이 멈췄습니다.", + "SOMETHING_ELSE: 요청을 받아 자료와 조건을 확인하고 있습니다 // 서버가 준 단계 이름만으로는 더 구체적인 작업을 특정하지 못했습니다. // null", + ] + `); + }); + + it("chooses the Korean particle from the last letter of the subject", () => { + const lines = ["사과", "책", "ABC", "표 3", "저장소.", "일정?"].map((problem) => { + const live = liveResearchProgress(inPhase("RESEARCH_PLANNER", { problem })); + return `${problem}: ${live?.headline}`; + }); + const withLocators = ["책", "사과", "A3", "표 3", "ABC"].map((label) => { + const draft = { evidence_focus: { locators: [{ exact_text: label }] } }; + const reranked = liveResearchProgress(inPhase("EVIDENCE_RERANKER", {}, draft)); + const reviewed = liveResearchProgress(inPhase("SEMANTIC_REVIEWER", { problem: "발사 조건" }, draft)); + return `${label}: ${reranked?.headline} | ${reviewed?.headline}`; + }); + expect([...lines, ...withLocators]).toMatchInlineSnapshot(` + [ + "사과: 사과를 고정하고 있습니다", + "책: 질문에서 꼭 지킬 조건을 고정하고 있습니다", + "ABC: ABC를 고정하고 있습니다", + "표 3: 표 3을 고정하고 있습니다", + "저장소.: 저장소를 고정하고 있습니다", + "일정?: 일정을 고정하고 있습니다", + "책: 책과 관련된 문장을 다시 줄 세우고 있습니다 | 발사 조건이 책 원문에 있는지 대조하고 있습니다", + "사과: 사과와 관련된 문장을 다시 줄 세우고 있습니다 | 발사 조건이 사과 원문에 있는지 대조하고 있습니다", + "A3: A3과 관련된 문장을 다시 줄 세우고 있습니다 | 발사 조건이 A3 원문에 있는지 대조하고 있습니다", + "표 3: 표 3과 관련된 문장을 다시 줄 세우고 있습니다 | 발사 조건이 표 3 원문에 있는지 대조하고 있습니다", + "ABC: ABC와 관련된 문장을 다시 줄 세우고 있습니다 | 발사 조건이 ABC 원문에 있는지 대조하고 있습니다", + ] + `); + }); + + it("clips, deduplicates and limits the quoted hypotheses and the joined lists", () => { + const long = "아주 긴 가설 문장은 스물여덟 글자를 넘기면 말줄임표로 잘려야 합니다 그래서 길게 적습니다"; + const draft = { + portfolio: { + hypotheses: [ + { hypothesis_id: "h1", statement: long }, + { hypothesis_id: "h2", statement: "같은 문장" }, + { hypothesis_id: "h3", statement: "같은 문장" }, + { hypothesis_id: "h4", statement: "넷째 설명" }, + { hypothesis_id: "h5", statement: "다섯째 설명" }, + ], + }, + }; + const generated = liveResearchProgress(inPhase("HYPOTHESIS_GENERATOR", {}, draft)); + const reviewed = liveResearchProgress(inPhase("HYPOTHESIS_REVIEW", {}, draft)); + expect([generated?.headline, reviewed?.headline, reviewed?.missing]).toMatchInlineSnapshot(` + [ + "「아주 긴 가설 문장은 스물여덟 글자를 넘기면 말줄…」, 「같은 문장」, 「넷째 설명」을 준비하고 있습니다", + "「아주 긴 가설 문장은 스물여덟 글자를 넘기면 말줄…」, 「같은 문장」, 「넷째 설명」을 원문과 대조하고 있습니다", + "「아주 긴 가설 문장은 스물여덟 글자를 넘기면 말줄…」, 「같은 문장」부터 대조하겠습니다. 빠진 5개 결정을 채우기 전입니다.", + ] + `); + }); +}); + +describe("one-line texts", () => { + it("pins the model, token and wait lines", () => { + const cases: Record[] = [ + { request: { operation_id: "o", model_settings: { model: "gpt-x", reasoning_effort: "high" } } }, + { request: { operation_id: "o", model_settings: { model: "gpt-x" } } }, + { request: { operation_id: "o", model_settings: { reasoning_effort: "low" } } }, + { request: { operation_id: "o", model_settings: {} } }, + { request: { operation_id: "o" } }, + { usage: { input_tokens: 1, output_tokens: 2, total_tokens: 12400, state: "COMPLETE", unreported_calls: 0, cumulative_token_limit_enforced: false } }, + { usage: { input_tokens: 1, output_tokens: 2, total_tokens: null, state: "PARTIAL", unreported_calls: 1, cumulative_token_limit_enforced: false, cached_input_tokens: 3000 } }, + { model_dispatches: [{ state: "RESERVED", transport_observation: { elapsed_ms: 47000, received_bytes: 0 } }] }, + { model_dispatches: [{ state: "RESERVED", transport_observation: { elapsed_ms: 47000, first_byte_ms: 1200 } }] }, + { model_dispatches: [{ state: "RESERVED" }] }, + { model_dispatches: [{ state: "OBSERVED", transport_observation: { elapsed_ms: 173, received_bytes: 2048 } }] }, + { model_dispatches: [{ state: "OBSERVED", received_bytes: 5, transport_observation: { elapsed_ms: 90000 } }] }, + { model_dispatches: [{ state: "OBSERVED", transport_observation: { elapsed_ms: 5000 } }] }, + { model_dispatches: [{ state: "OBSERVED" }] }, + { model_dispatches: [{ state: "OBSERVED", received_bytes: 1 }, { state: "RESERVED", transport_observation: { elapsed_ms: 1000 } }] }, + ]; + const lines = cases.map((extra) => { + const live = liveResearchProgress(status(extra)); + return JSON.stringify([live?.modelLine, live?.usageLine, live?.waitLine]); + }); + expect(lines).toMatchInlineSnapshot(` + [ + "["gpt-x · 추론 high",null,null]", + "["gpt-x",null,null]", + "["추론 low",null,null]", + "[null,null,null]", + "[null,null,null]", + "[null,"사용 토큰 12,400",null]", + "[null,"사용 토큰 미확인 (부분 관측) · 캐시 입력 3,000",null]", + "[null,null,"모델 응답 대기 · 47초 · 첫 글자 아직 없음"]", + "[null,null,"모델 응답 대기 · 47초 · 첫 글자 1초"]", + "[null,null,"모델 응답 대기 · 경과 미확인 · 첫 글자 아직 없음"]", + "[null,null,"마지막 모델 응답 2,048B · 173ms"]", + "[null,null,"마지막 모델 응답 5B · 1분 30초"]", + "[null,null,"마지막 모델 호출 · 5초"]", + "[null,null,null]", + "[null,null,"모델 응답 대기 · 1초 · 첫 글자 아직 없음"]", + ] + `); + }); + + it("pins the finished-stage and model-call payload texts, including sizes", () => { + const stages = [500, 1023, 1024, 1536, 10239, 10240, 1048576, 5242880, 20971520, null, -1].map((context_bytes) => ({ + role: "RESEARCH_PLANNER", elapsed_ms: 2000, context_bytes, dispatch_ids: ["a", "b"], + })); + const live = liveResearchProgress(status({ completed_stages: stages as unknown })); + const published = liveResearchProgress( + status({ + activity_events: [ + { seq: 1, kind: "action", action_type: "stage_completed", live: false, payload: { elapsed_ms: 1500, context_bytes: 2048, dispatch_count: 2 } }, + { seq: 2, kind: "action", action_type: "source_read", live: false, payload: { page: 3, exact_text: " 표 3 " } }, + { seq: 3, kind: "action", action_type: "source_read", live: false, payload: { page: null, exact_text: null } }, + { seq: 4, kind: "action", action_type: "hypothesis_review", live: false, payload: { hypothesis_count: 4, review_count: 4 } }, + { seq: 5, kind: "action", action_type: "hypothesis_review", live: false, payload: { hypothesis_count: 4, review_count: 1 } }, + { seq: 6, kind: "action", action_type: "hypothesis_review", live: false, payload: { hypothesis_count: 4 } }, + { seq: 7, kind: "action", action_type: "model_call", live: false, payload: { state: "OBSERVED", received_bytes: 1200, elapsed_ms: 800 } }, + { seq: 8, kind: "action", action_type: "model_call", live: true, payload: { state: "RESERVED", elapsed_ms: 3000, first_byte_ms: 400 } }, + ], + }), + ); + const flat = (events: { kind: string; text: string; payload?: string | null; live?: boolean }[] | undefined) => + events?.map((event) => `${event.kind}|${event.text}|${event.payload ?? ""}|${event.live ?? false}`); + expect({ stages: flat(live?.events), published: flat(published?.events) }).toMatchInlineSnapshot(` + { + "published": [ + "action|단계 완료|2초 · 맥락 2.0KB · 모델 2회|false", + "action|원문 위치|p.3 · 표 3|false", + "action|원문 위치||false", + "action|가설 검토|가설 4개 · 검토 4개|false", + "action|가설 검토|가설 4개 중 1개만 결정|false", + "action|가설 검토|가설 4개 · 결정은 아직 없습니다|false", + "action|모델 호출|1,200B · 800ms|false", + "action|모델 호출|3초 · 첫 글자 400ms|true", + "note|표 3 발사 준비 조건을 고정하고 있습니다||true", + ], + "stages": [ + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 500B · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 1,023B · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 1.0KB · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 1.5KB · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 10.0KB · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 10KB · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 1.0MB · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 5.0MB · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 맥락 20MB · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정했습니다||false", + "action|단계 완료|2초 · 모델 2회|false", + "note|표 3 발사 준비 조건을 고정하고 있습니다||true", + ], + } + `); + }); + + it("pins the composer line for each execution state", () => { + const lines = [ + composerActivity(status({ execution_state: "PAUSE_PENDING" })), + composerActivity(status({ execution_state: "PAUSED" })), + composerActivity( + status({ attempt: { operation_id: "o", phase: "HOLD", status: "RUNNING", external_effect_state: "DISPATCHING", remote_observation: "UNKNOWN" } }), + ), + composerActivity(status({ operation_state: "SUCCEEDED" })), + composerActivity(status({ attempt: undefined })), + composerActivity(inPhase("EVIDENCE_FOCUS", { model_dispatches: [{ state: "RESERVED", transport_observation: { elapsed_ms: 47000 } }] })), + ]; + expect(lines).toMatchInlineSnapshot(` + [ + "현재 단계를 마치고 멈추는 중입니다.", + "작업이 일시정지되어 있습니다.", + "외부 실행의 종료 여부를 아직 확인하지 못했습니다.", + null, + null, + "Psyche 표 3 발사 준비 조건 위치를 확인하고 있습니다 · 모델 응답 대기 · 47초 · 첫 글자 아직 없음", + ] + `); + }); + + it("pins the hold reasons and the evidence labels", () => { + const reasons = ["HTTP 429", "AUTH_REQUIRED", "x403", "MODEL_NOT_SUPPORTED", "400", "other", ""].map((terminal_reason) => { + const live = liveResearchProgress( + inPhase("HOLD", { + operation_state: "SUCCEEDED", + current_result: { operation_id: "o", phase: "HOLD", state: "PARTIAL", completion: "TERMINAL", terminal_reason, basis_digest: "0", result: {}, gaps: [], next_steps: [], source_refs: [], record_refs: [] }, + }), + ); + return `${terminal_reason}: ${live?.missing}`; + }); + expect({ reasons, labels: evidenceLabels(inPhase("EVIDENCE_FOCUS", {}, RICH_DRAFT)) }).toMatchInlineSnapshot(` + { + "labels": [ + "p.6 70.6", + "p.8 GNC", + ], + "reasons": [ + "HTTP 429: 계정 사용 한도로 모델 호출이 거절됐습니다.", + "AUTH_REQUIRED: 모델 계정 인증이 거절됐습니다.", + "x403: 모델 계정 인증이 거절됐습니다.", + "MODEL_NOT_SUPPORTED: 이 ChatGPT Codex 계정에서 지원하지 않는 모델입니다.", + "400: 모델이 요청을 거절했습니다.", + "other: 답을 쓰기 전에 모델 호출이 멈췄습니다.", + ": 답을 쓰기 전에 모델 호출이 멈췄습니다.", + ], + } + `); + }); +}); \ No newline at end of file diff --git a/apps/web/src/api/researchProgressText.ts b/apps/web/src/api/researchProgressText.ts new file mode 100644 index 0000000..2ff261a --- /dev/null +++ b/apps/web/src/api/researchProgressText.ts @@ -0,0 +1,441 @@ +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}개`; +} diff --git a/apps/web/src/api/rpcClient.ts b/apps/web/src/api/rpcClient.ts index 1d8a9aa..108da1a 100644 --- a/apps/web/src/api/rpcClient.ts +++ b/apps/web/src/api/rpcClient.ts @@ -12,7 +12,7 @@ export const readQueries = new Set(["thread/read", "thread/list", "thread/activi "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"]); + "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) { diff --git a/apps/web/src/api/trace.ts b/apps/web/src/api/trace.ts new file mode 100644 index 0000000..cc3a1bd --- /dev/null +++ b/apps/web/src/api/trace.ts @@ -0,0 +1,69 @@ +import { rpc } from "./rpcClient"; + +export type TraceItem = { item_id: string; item_key: string; kind: string; title: string; fields: Record; source_span_refs: string[] }; +export type TraceLink = { link_id: string; from_id: string; to_id: string; relation: string }; +export type TraceRule = { + rule_id: string; rule_revision: number; criterion_id: string; measure: string; comparator: string; threshold: string; + unit: string; condition: string; rounding: { digits: number; mode: string }; required: boolean; +}; +export type TraceResult = { + result_id: string; result_revision: number; criterion_id: string; condition: string; value: string | null; raw_value: string | null; + unit: string; numerator: number | null; denominator: number | null; observed_at: string; source_span_refs: string[]; +}; +export type ChangedDependency = { + kind: string; ref_id: string; field: string; before: string | null; after: string | null; + before_revision: number | null; after_revision: number | null; criterion_ids: string[]; requirement_ids: string[]; +}; +export type Confirmation = { verdict_revision_digest: string; actor_id: string; confirmed_at: string; rationale: string }; +export type ResultRef = { result_id: string; result_revision: number }; +export type VerdictRevision = { + subject_kind: "CRITERION" | "REQUIREMENT"; subject_id: string; state: string; + cause: { trigger: string; changed: ChangedDependency[] }; + basis: { input_digest: string; source_span_refs: string[]; evidence_text_preserved: boolean; child_verdict_digests: string[] }; + selection: { policy: { name: string; version: number }; candidates: ResultRef[]; chosen: ResultRef[]; excluded: (ResultRef & { reason: string })[] } | null; + conditions: { required: boolean; rule_id: string | null; condition: string | null; unit: string | null; required_criteria: string[] }; + actor: { actor_id: string; computed_at: string }; + reasons: { computed: string[]; human: string[] }; + parent_digest: string | null; verdict_digest: string; revision_digest: string; +}; +export type Verdict = VerdictRevision & { + currentness: { state: "CURRENT" | "STALE_BASIS"; changed_dependencies: ChangedDependency[] }; + confirmations: Confirmation[]; + recent_history: VerdictRevision[]; // newest first, the current revision included + history_total: number; +}; +export type TraceView = { + record_digest: string | null; set_digest: string; items: TraceItem[]; links: TraceLink[]; rules: TraceRule[]; results: TraceResult[]; + verdicts: Verdict[]; confirmations: Confirmation[]; pending_changes: ChangedDependency[]; +}; + +export type ImportIssue = { code: string; row: number | null; ref: string | null; detail: string }; +export type PlannedChange = { kind: string; id: string; row: number; fields: string[] }; +export type ImportMode = "CREATE" | "UPDATE"; +export type ImportPreview = { + preview_id: string; input_sha256: string; mode: ImportMode; current_set_digest: string | null; applicable: boolean; + changes: { added: PlannedChange[]; updated: PlannedChange[]; deleted: PlannedChange[]; unchanged: number }; + conflicts: ImportIssue[]; losses: string[]; spreadsheet_suspects: ImportIssue[]; +}; +export type VerdictChange = { subject_kind: string; subject_id: string; before: string | null; after: string }; +export type ImportApplied = { applied: boolean; reason?: string; record_digest: string | null; verdict_changes: VerdictChange[]; trace?: TraceView }; +export type TraceExport = { format: string; filename: string; csv_text: string; sha256: string; set_digest: string; loss_manifest: string[] }; + +export type HistoryPage = { subject_kind: string; subject_id: string; history_total: number; revisions: VerdictRevision[]; next_before_digest: string | null }; +export const HISTORY_PAGE = 20; + +export const traceKey = (projectId: string) => ["verification-trace", projectId] as const; + +const call = async (method: string, input: Record, signal?: AbortSignal) => + (await rpc(method, input, crypto.randomUUID(), signal)).value; + +export const readTrace = (projectId: string, signal?: AbortSignal) => call("trace/read", { project_id: projectId }, signal); +export const exportTrace = (projectId: string) => call("trace/export", { project_id: projectId }); +export const previewTraceImport = (projectId: string, mode: ImportMode, csvText: string) => + call("trace/importPreview", { project_id: projectId, mode, csv_text: csvText }); +export const applyTraceImport = (projectId: string, mode: ImportMode, csvText: string, preview: ImportPreview) => + call("trace/importApply", { project_id: projectId, mode, csv_text: csvText, preview_id: preview.preview_id, input_sha256: preview.input_sha256 }); +export const confirmTraceVerdict = (projectId: string, revisionDigest: string, rationale: string, expectedDigest: string | null) => + call("trace/confirm", { project_id: projectId, verdict_revision_digest: revisionDigest, rationale, expected_digest: expectedDigest }); +export const readTraceHistory = (projectId: string, kind: "CRITERION" | "REQUIREMENT", subjectId: string, beforeDigest: string, limit = HISTORY_PAGE) => + call("trace/history", { project_id: projectId, subject_kind: kind, subject_id: subjectId, before_digest: beforeDigest, limit }); diff --git a/apps/web/src/components/ActionCompare.tsx b/apps/web/src/components/ActionCompare.tsx index b311a52..1b6b8c2 100644 --- a/apps/web/src/components/ActionCompare.tsx +++ b/apps/web/src/components/ActionCompare.tsx @@ -1,17 +1,31 @@ -import { HTMLTable, Tag } from "@blueprintjs/core"; +import { Callout, HTMLTable, Tag } from "@blueprintjs/core"; +import { Fragment } from "react"; import { objectList, objectValue, stringValues, textValue } from "../api/presentation"; import { numberSources } from "./answerText"; import { ActionEffort, EffortLines } from "./ActionEffort"; import { ApprovalSection } from "./ApprovalSection"; import { parseEstimates } from "./effortEstimates"; +import { staleActionNote, type HypothesisLink } from "./hypothesisLinkText"; +import { standingActionNote } from "./judgmentRecordText"; +import type { DiscriminationItem } from "../api/judgmentRecords"; +import { orderedActions } from "./testOrderInput"; 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 ?? "시간 미확인")})`; +const staleNote = (action: Record, links?: HypothesisLink[]) => + staleActionNote([...stringValues(action.hypothesis_ids), ...stringValues(action.hypothesis_refs)], links); + /** Next actions: the model's recommendation, the criteria it used, and how each action fares. No score, no rank. */ -export function ActionCompare({ result, projectId }: { result: Record; projectId?: string }) { +export function ActionCompare({ result, projectId, links, discrimination }: { + result: Record; projectId?: string; + /** Hypotheses that came from a trace row, read against today's verdicts; an action on a changed one is marked. */ + links?: HypothesisLink[]; + /** What people recorded for the tests of these hypotheses; an action resting on one the results went against gets a note (nothing is held back). */ + discrimination?: DiscriminationItem[]; +}) { const plan = objectValue(result.action_plan); const actions = objectList(plan.alternatives); const analysis = objectValue(plan.decision_analysis); @@ -21,6 +35,9 @@ export function ActionCompare({ result, projectId }: { result: Record refs.map(ref => [{numbers.get(ref)}]); if (actions.length === 0) return

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

; + 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))}` : ""}`; return <> {textValue(analysis.decision) &&
@@ -30,8 +47,9 @@ export function ActionCompare({ result, projectId }: { result: Record이 판단이 바뀌는 조건
{textValue(analysis.sensitivity)}

} {textValue(analysis.preference_question) &&

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

}
} - {projectId && textValue(plan.plan_id) && actions.some(action => action.execution_authority === "HUMAN_REQUIRED_R3") - && } + {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)} @@ -43,11 +61,13 @@ export function ActionCompare({ result, projectId }: { result: Record

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

    )})})} }
} - {actions.map((action, index) =>
+ {orderedActions(actions).map(({ action, heading, key }) => {heading &&

{heading}

}

{textValue(action.specification)}

{reversibilityLabel(textValue(action.reversibility)) ?? "되돌림 미확인"} {actionStateLabel(textValue(action.state)) && {actionStateLabel(textValue(action.state))}}
+ {staleNote(action, links) && {staleNote(action, links)}} + {againstNote(action) && {againstNote(action)}}

{textValue(action.expected_information_value)}

{projectId && textValue(action.action_id) ? @@ -55,6 +75,6 @@ export function ActionCompare({ result, projectId }: { result: Record

필요한 근거: {value}

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

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

}

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

-
)} +
)} ; } diff --git a/apps/web/src/components/ActionCompareLive.tsx b/apps/web/src/components/ActionCompareLive.tsx new file mode 100644 index 0000000..0202ae0 --- /dev/null +++ b/apps/web/src/components/ActionCompareLive.tsx @@ -0,0 +1,10 @@ +import { ActionCompare } from "./ActionCompare"; +import { useHypothesisLinks } from "./useHypothesisLinks"; +import { useDiscrimination } from "./useJudgmentRecords"; + +/** The next-actions comparison with a mark on each action that rests on a hypothesis whose trace verdict changed. */ +export function ActionCompareLive({ result, projectId }: { result: Record; projectId: string }) { + const linked = useHypothesisLinks(projectId); + const discrimination = useDiscrimination(projectId); + return ; +} diff --git a/apps/web/src/components/ActionCompareStale.test.tsx b/apps/web/src/components/ActionCompareStale.test.tsx new file mode 100644 index 0000000..08e413e --- /dev/null +++ b/apps/web/src/components/ActionCompareStale.test.tsx @@ -0,0 +1,40 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { expect, it } from "vitest"; +import excerpt from "../api/fixtures/iris-judgment-excerpt.json"; +import type { HypothesisLink } from "../api/hypothesisLink"; +import { ActionCompare } from "./ActionCompare"; + +const link = (id: string, patch: Partial = {}): HypothesisLink => ({ + hypothesis_id: id, hypothesis_revision_digest: "d".repeat(64), statement: "s", subject_kind: "CRITERION", subject_id: "C-1", subject_title: "비 조건 탐지율", + state: "STALE", change: "CONTENT_CHANGED", link_state: "HOLD_NO_RESULT", current_state: "FAIL_COMPUTED", current_verdict_revision: "r".repeat(64), recheck: null, ...patch, +}); +// The first action rests on h-1, the second on h-2, the third on nothing that has a link. +const render = (links?: HypothesisLink[]) => { + const result = structuredClone(excerpt) as Record; + const alternatives = (result.action_plan as { alternatives: Record[] }).alternatives; + alternatives[0].hypothesis_ids = ["h-1"]; + alternatives[1].hypothesis_ids = ["h-2"]; + return renderToStaticMarkup(); +}; +const NOTE = "이 행동이 기대는 가설"; +const cards = (html: string) => html.split('
{ + const found = cards(render([link("h-1"), link("h-2", { state: "CURRENT", change: null })])); + expect(found).toHaveLength(3); + expect(found[0]).toContain(NOTE); + expect(found[0]).toContain("승인 요청과 결정, 실행이 막힙니다"); + expect(found[1]).not.toContain(NOTE); // its hypothesis is current + expect(found[2]).not.toContain(NOTE); // no hypothesis with a link: not held back +}); + +it("says a changed basis under the same verdict apart, and shows nothing once a person kept the link", () => { + const found = cards(render([link("h-1", { change: "EVIDENCE_ONLY", link_state: "PASS_COMPUTED", current_state: "PASS_COMPUTED" }), link("h-2", { state: "RECHECKED" })])); + expect(found[0]).toContain("판정 상태는 같고 근거만 바뀜"); + expect(found[1]).not.toContain(NOTE); +}); + +it("shows no mark when the links are not known", () => { + expect(render(undefined)).not.toContain(NOTE); + expect(render([])).not.toContain(NOTE); +}); diff --git a/apps/web/src/components/AnswerReadability.test.tsx b/apps/web/src/components/AnswerReadability.test.tsx new file mode 100644 index 0000000..cf548d3 --- /dev/null +++ b/apps/web/src/components/AnswerReadability.test.tsx @@ -0,0 +1,75 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vitest"; +import { ResearchResultCard } from "./ResearchResultCard"; +import { criterionText, plainReason, splitReadable } from "./plainWording"; + +const digest = "a".repeat(64); +const row = (target: string, question: string, extra: Record = {}) => ({ + requirement_id: "r-" + target, target, question, status: "UNRESOLVED" as const, applicability: "APPLICABLE", + relation: "INSUFFICIENT", validation: "INCONCLUSIVE", blocker: "answer:HOLD", review_refs: [], evidence_refs: [], reason_codes: ["answer:HOLD"], ...extra }); +const coverage = (rows: ReturnType[]) => ({ schema_version: "1.0.0" as const, request_revision_digest: digest, availability: "AVAILABLE" as const, + reason_codes: [], requirement_set_revision_digest: null, coverage_revision_digest: null, rows, + summary: { satisfied: 0, unresolved: rows.length, not_applicable: 0, not_assessed: 0, hold_targets: [], reason_codes: [] } }); +const progress = (remaining: string[], unknowns: string[] = []) => ({ schema_version: "1.0.0" as const, request_revision_digest: digest, result_revision_digest: digest, + state: "HOLD" as const, currentness: { state: "CURRENT" as const, reasons: [], execution_eligible: true }, progress_items: [], recorded_checks: [], + remaining_gaps: remaining, unknowns, next_user_action: { schema_version: "1.0.0" as const, action_type: "REVIEW_GAPS" as const, label: "review", reason_codes: [], requires_permission: false, target: null, basis: {} } }); +const visible = (html: string) => html.replace(//g, "").replace(/<[^>]+>/g, " ").replace(/'/g, "\u0027"); +const render = (result: Record, extra: Record = {}) => + renderToStaticMarkup( {}} {...extra}/>); + +const REASONS = [ + "criteria exist but official evaluator inputs are incomplete", "no cutoff-eligible evidence exists", + "comparison conditions have not been confirmed", "counterevidence has not been checked", + "measurement implementation is not verified", "criterion semantics still require expert confirmation", +]; + +describe("the sentences the program writes about missing checks", () => { + it("has a plain Korean sentence for every recorded sufficiency reason", () => { + for (const reason of REASONS) { + const text = plainReason(reason); + expect(text, reason).not.toBeNull(); + expect(text).toMatch(/[가-힣]/); + expect(text).not.toMatch(/[A-Za-z]{3,}/); + } + }); + + it("keeps Korean text, and sets an unknown English sentence aside as technical", () => { + expect(splitReadable(["외부 검증 필요", REASONS[0], "some new english reason", "42"])).toEqual({ + plain: ["외부 검증 필요", plainReason(REASONS[0]), "42"], technical: ["some new english reason"] }); + }); + + it("words the built-in criterion targets and keeps ids out of the title", () => { + const known = criterionText("answer", "Requested target, fields, source locator and adjacent context"); + expect(known.title).toMatch(/[가-힣]/); + expect(known.question).toMatch(/[가-힣]/); + expect(known.question).not.toMatch(/[A-Za-z]{3,}/); + const id = criterionText("SYN-TC-RAIN", "연결 자료에 비 결과가 있는가?"); + expect(id.title).toBeNull(); + expect(id.question).toBe("연결 자료에 비 결과가 있는가?"); + expect(id.technical).toContain("SYN-TC-RAIN"); + const english = criterionText("something_new", "An English question nobody translated"); + expect(english.question).toBeNull(); + expect(english.technical).toEqual(["something_new", "An English question nobody translated"]); + }); +}); + +describe("the answer screen", () => { + 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]]), + coverageMatrix: coverage([ + row("answer", "Requested target, fields, source locator and adjacent context"), + row("time", "Requested time and source applicability"), + row("SYN-TC-RAIN", "연결 자료에 비 결과가 있는가?"), + ]), + }); + const text = visible(html); + for (const english of [...REASONS, "Requested target", "Requested time", "INSUFFICIENT", "SYN-TC-RAIN", "answer", "gap"]) { + expect(text, english).not.toContain(english); + } + expect(text).toContain("남은 확인 사항"); + expect(text).toContain("근거 부족"); + expect(text).toContain("연결 자료에 비 결과가 있는가?"); + for (const english of ["SYN-TC-RAIN", REASONS[0]]) expect(html, english).toContain(english); // kept, but folded + }); +}); diff --git a/apps/web/src/components/ClosureMark.test.tsx b/apps/web/src/components/ClosureMark.test.tsx new file mode 100644 index 0000000..5e61269 --- /dev/null +++ b/apps/web/src/components/ClosureMark.test.tsx @@ -0,0 +1,59 @@ +// @vitest-environment jsdom +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, expect, it } from "vitest"; +import type { ClosureEvent, ClosureRow } from "../api/judgmentRecords"; +import type { Verdict } from "../api/trace"; +import { ClosureMark } from "./ClosureMark"; + +const event = (id: string, patch: Partial = {}): ClosureEvent => ({ + event_id: id, subject_kind: "CRITERION", subject_id: "C-1", kind: "FIX_APPLIED", basis_ref: `문서-${id}`, note: "", scope: null, verdict_revision: "r".repeat(64), + verdict_digest: "d", verdict_state: "FAIL_COMPUTED", actor_id: "human:local-user", created_at: "2026-10-06T01:02:03+00:00", ...patch, +}); +const rowOf = (events: ClosureEvent[], patch: Partial = {}): ClosureRow => ({ + subject_kind: "CRITERION", subject_id: "C-1", current_state: "FAIL_COMPUTED", effect_confirmed: false, effect_verdict_revision: null, verdict_changed_since: false, events, ...patch, +}); +const verdict = { state: "FAIL_COMPUTED" } as unknown as Verdict; +let root: Root | undefined; let container: HTMLDivElement; +afterEach(async () => { if (root) await act(async () => root!.unmount()); root = undefined; container?.remove(); }); +async function mount(row: ClosureRow) { + Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }); + container = document.createElement("div"); document.body.append(container); root = createRoot(container); + await act(async () => root!.render( undefined}/>)); +} +const openHistory = async () => act(async () => [...container.querySelectorAll("button")].find(item => item.textContent?.includes("이전 처분 기록"))!.click()); + +it("has no history fold for a single record", async () => { + await mount(rowOf([event("e1")])); + expect(container.textContent).toContain("근거 문서: 문서-e1"); + expect(container.textContent).not.toContain("이전 처분 기록"); +}); + +it("folds the two older of three records under the latest one, newest first", async () => { + await mount(rowOf([ + event("e1", { kind: "HUMAN_CLOSED", note: "첫 메모" }), + event("e2", { kind: "CONDITION_CHANGED", scope: "50 m 이하 안개" }), + event("e3", { kind: "FIX_APPLIED" }), + ])); + expect(container.textContent).toContain("이전 처분 기록 2건"); + await openHistory(); + const notes = [...container.querySelectorAll(".trace-closure-history .trace-closure")].map(node => node.textContent ?? ""); + expect(notes).toHaveLength(2); + expect(notes[0]).toContain("운용 조건 변경(조건 범위: 50 m 이하 안개)"); + expect(notes[0]).toContain("근거 문서: 문서-e2"); + expect(notes[1]).toContain("사람 확인 종결(시험 결과 없음)"); + expect(notes[1]).toContain("첫 메모"); + expect(notes[1]).toContain("원래 판정: 기준 미달"); + expect(notes[1]).toContain("기록 · "); + expect(container.textContent).not.toMatch(/FIX_APPLIED|HUMAN_CLOSED|CONDITION_CHANGED|FAIL_COMPUTED|undefined|\bnull\b/); +}); + +it("puts the rule's effect-confirmed word on the latest record only", async () => { + await mount(rowOf([event("e1", { kind: "FIX_APPLIED" }), event("e2", { kind: "FIX_APPLIED" })], { effect_confirmed: true, current_state: "PASS_COMPUTED" })); + await openHistory(); + const top = container.querySelector(".trace-closure-cell > .trace-closure"); + expect(top?.textContent).toContain("효과 확인됨"); + const history = container.querySelector(".trace-closure-history")!; + expect(history.textContent).not.toContain("효과 확인됨"); + expect(history.textContent).toContain("수정 반영됨(효과 미확인)"); +}); diff --git a/apps/web/src/components/ClosureMark.tsx b/apps/web/src/components/ClosureMark.tsx new file mode 100644 index 0000000..2dc5bb4 --- /dev/null +++ b/apps/web/src/components/ClosureMark.tsx @@ -0,0 +1,24 @@ +import { Button, Tag } from "@blueprintjs/core"; +import type { ClosureRow } from "../api/judgmentRecords"; +import type { Verdict } from "../api/trace"; +import { Disclosure } from "./Disclosure"; +import { closureDisplay, eventKindLabel, eventLines, isMet, previousEvents } from "./judgmentRecordText"; + +/** + * Beside a row's verdict, never in it: what a person recorded about closing the row, and the button to record one. + * The verdict's own tag, words and colour are not touched. Only "effect confirmed" is the rules' word. + * Older records fold under the latest one, newest first; the effect-confirmed mark belongs to the latest record only. + */ +export function ClosureMark({ row, verdict, label, onRecord }: { row: ClosureRow | undefined; verdict: Verdict; label: string; onRecord: (verdict: Verdict) => void }) { + const shown = row && row.events.length > 0 ? closureDisplay(row) : null; + const older = row ? previousEvents(row) : []; + return
+ {shown &&
{shown.label} +
    {shown.lines.map((line, index) =>
  • {line}
  • )}
} + {row && older.length > 0 && + {older.map(event =>
{eventKindLabel(event)} +
    {eventLines(row.subject_kind, event).map((line, index) =>
  • {line}
  • )}
)} +
} + {!isMet(verdict.state) && } +
; +} diff --git a/apps/web/src/components/ClosureRecorder.tsx b/apps/web/src/components/ClosureRecorder.tsx new file mode 100644 index 0000000..83516e5 --- /dev/null +++ b/apps/web/src/components/ClosureRecorder.tsx @@ -0,0 +1,50 @@ +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"; + +/** 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 }) { + const client = useQueryClient(); + const [kind, setKind] = useState(""); + const [basis, setBasis] = useState(""); + const [scope, setScope] = useState(""); + const [note, setNote] = useState(""); + const [message, setMessage] = useState(""); + const close = () => { setKind(""); setBasis(""); setScope(""); setNote(""); setMessage(""); onClose(); }; + const send = useMutation({ + mutationFn: () => recordClosure({ projectId, subjectKind: verdict!.subject_kind, subjectId: verdict!.subject_id, kind: kind as ClosureKind, basisRef: basis.trim(), + note: note.trim(), scope: kind === "CONDITION_CHANGED" ? scope.trim() : "", verdictRevision: verdict!.revision_digest }), + onSuccess: async () => { await client.invalidateQueries({ queryKey: closuresKey(projectId) }); close(); }, + 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; } + setMessage(""); send.mutate(); + }; + return +
+

{CLOSURE_NOTE}

+ setKind(event.currentTarget.value as ClosureKind)}> + {CLOSURE_KINDS.map(item => )} + + {kind !== "" &&

{CLOSURE_KINDS.find(item => item.code === kind)?.hint}

} + + {kind === "CONDITION_CHANGED" && } +