diff --git a/packages/channels/base/src/__fixtures__/channel-memory-recall-eval.json b/packages/channels/base/src/__fixtures__/channel-memory-recall-eval.json new file mode 100644 index 00000000000..c8968654ca9 --- /dev/null +++ b/packages/channels/base/src/__fixtures__/channel-memory-recall-eval.json @@ -0,0 +1,621 @@ +[ + { + "id": "en-deploy-staging", + "category": "english", + "message": "deploy staging", + "entries": [ + { + "id": "deploy", + "text": "Deploy staging with the release runbook and verify the canary before promotion. This synthetic fact is deliberately longer than the short fallback limit so it must win through lexical overlap rather than fallback selection." + }, + { + "id": "unrelated", + "text": "Quarterly accessibility review notes cover keyboard navigation and contrast checks. This synthetic distractor is deliberately longer than the short fallback limit and has no lexical overlap with the query." + } + ], + "relevantIds": ["deploy"], + "expectedTopId": "deploy" + }, + { + "id": "en-incident-runbook", + "category": "english", + "message": "incident runbook", + "entries": [ + { + "id": "runbook", + "text": "The incident runbook documents escalation contacts, mitigation steps, and recovery verification. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "The design system review tracks typography tokens and button spacing. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["runbook"], + "expectedTopId": "runbook" + }, + { + "id": "en-nfkc-case", + "category": "english", + "message": "DEPLOY STAGING", + "entries": [ + { + "id": "normalized", + "text": "deploy staging follows the verified canary rollout procedure. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "production rollback ownership is recorded in another operational guide. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["normalized"], + "expectedTopId": "normalized" + }, + { + "id": "en-repeated-token", + "category": "english", + "message": "alpha alpha beta", + "entries": [ + { + "id": "both", + "text": "alpha beta is the approved paired experiment cohort. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "one", + "text": "alpha alpha appears repeatedly in historical experiment notes. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["both", "one"], + "expectedTopId": "both" + }, + { + "id": "en-number-42", + "category": "english", + "message": "case 42", + "entries": [ + { + "id": "forty-two", + "text": "case 42 owns the release validation checklist. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "twenty-four", + "text": "case 24 owns the unrelated archive migration checklist. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["forty-two"], + "expectedTopId": "forty-two" + }, + { + "id": "en-stable-tie", + "category": "english", + "message": "alpha beta", + "entries": [ + { + "id": "first-tie", + "text": "alpha is recorded in the first source document. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "second-tie", + "text": "beta is recorded in the second source document. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["first-tie", "second-tie"], + "expectedTopId": "first-tie", + "expectedSelectedIds": ["first-tie", "second-tie"] + }, + { + "id": "zh-data-governance", + "category": "chinese", + "message": "数据治理", + "entries": [ + { + "id": "governance", + "text": "数据治理规范要求登记数据资产责任人并定期复核。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "项目治理会议讨论了季度资源排期和预算。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["governance"], + "expectedTopId": "governance" + }, + { + "id": "zh-release-window", + "category": "chinese", + "message": "发布窗口", + "entries": [ + { + "id": "window", + "text": "发布窗口安排在周三夜间并要求完成回滚演练。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "值班窗口用于处理日常咨询和权限申请。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["window"], + "expectedTopId": "window" + }, + { + "id": "zh-role-preference", + "category": "chinese", + "message": "管理员偏好", + "entries": [ + { + "id": "preference", + "text": "管理员偏好使用最小权限角色并保留审计记录。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "开发人员偏好在本地运行格式检查工具。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["preference"], + "expectedTopId": "preference" + }, + { + "id": "zh-project-alias", + "category": "chinese", + "message": "数仓项目", + "entries": [ + { + "id": "warehouse", + "text": "数仓项目的别名用于关联离线任务和质量规则。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "数据平台项目包含多个独立的实验环境。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["warehouse"], + "expectedTopId": "warehouse" + }, + { + "id": "zh-incident-owner", + "category": "chinese", + "message": "故障负责人", + "entries": [ + { + "id": "owner", + "text": "故障负责人负责更新事件状态并协调恢复验证。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "发布负责人负责确认变更审批和上线窗口。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["owner"], + "expectedTopId": "owner" + }, + { + "id": "zh-longer-run", + "category": "chinese", + "message": "数据治理平台", + "entries": [ + { + "id": "full-run", + "text": "数据治理平台提供资产目录、质量监控和责任人管理。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "partial-run", + "text": "数据治理规范定义了数据分类和保留周期。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["full-run", "partial-run"], + "expectedTopId": "full-run" + }, + { + "id": "zh-nfkc", + "category": "chinese", + "message": "ABC12 数据治理", + "entries": [ + { + "id": "normalized", + "text": "abc12 数据治理登记需要关联审批记录。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "abc34 数据目录更新由平台管理员处理。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["normalized"], + "expectedTopId": "normalized" + }, + { + "id": "zh-single-character-no-result", + "category": "chinese", + "message": "数", + "entries": [ + { + "id": "long-fact", + "text": "数据治理规范包含资产分级和质量责任制度。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": [], + "expectedSelectedIds": [] + }, + { + "id": "ja-hiragana", + "category": "japanese", + "message": "たのしい", + "entries": [ + { + "id": "hiragana", + "text": "たのしい作業計画には確認項目と担当者を記録する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "うれしい結果は週次報告で共有する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["hiragana"], + "expectedTopId": "hiragana" + }, + { + "id": "ja-katakana", + "category": "japanese", + "message": "カタカナ", + "entries": [ + { + "id": "katakana", + "text": "カタカナ表記のサービス名は運用台帳で管理する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "テスト計画は品質会議で確認する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["katakana"], + "expectedTopId": "katakana" + }, + { + "id": "ja-prolonged-mark", + "category": "japanese", + "message": "コーヒー", + "entries": [ + { + "id": "coffee", + "text": "コーヒー休憩の予定はチームカレンダーに登録する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "ケーキの注文は別の総務手順で管理する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["coffee"], + "expectedTopId": "coffee" + }, + { + "id": "ja-mixed-script", + "category": "japanese", + "message": "東京カフェ", + "entries": [ + { + "id": "tokyo-cafe", + "text": "東京カフェの出張予約は経費申請に添付する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "大阪ホテルの宿泊明細は月末に精算する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["tokyo-cafe"], + "expectedTopId": "tokyo-cafe" + }, + { + "id": "ko-data-quality", + "category": "korean", + "message": "데이터품질", + "entries": [ + { + "id": "quality", + "text": "데이터품질 기준은 검증 결과와 책임자를 함께 기록한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "서비스운영 회의는 주간 일정과 예산을 검토한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["quality"], + "expectedTopId": "quality" + }, + { + "id": "ko-release-owner", + "category": "korean", + "message": "배포담당자", + "entries": [ + { + "id": "release-owner", + "text": "배포담당자는 승인 상태와 롤백 준비를 확인한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "보안담당자는 권한 검토와 감사 기록을 관리한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["release-owner"], + "expectedTopId": "release-owner" + }, + { + "id": "ko-incident-runbook", + "category": "korean", + "message": "장애절차서", + "entries": [ + { + "id": "incident-runbook", + "text": "장애절차서는 연락망과 복구 검증 순서를 설명한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "교육절차서는 신규 구성원의 온보딩 일정을 설명한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["incident-runbook"], + "expectedTopId": "incident-runbook" + }, + { + "id": "ko-mixed-number", + "category": "korean", + "message": "릴리스 42", + "entries": [ + { + "id": "release-42", + "text": "릴리스 42 검증은 배포 전날 완료해야 한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "release-24", + "text": "릴리스 24 기록은 이전 분기의 보관 자료다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["release-42"], + "expectedTopId": "release-42" + }, + { + "id": "mixed-zh-en", + "category": "mixed", + "message": "数据 warehouse", + "entries": [ + { + "id": "warehouse", + "text": "数据 warehouse 迁移需要同步质量校验和血缘信息。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "项目 dashboard 用于展示团队的周度指标。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["warehouse"], + "expectedTopId": "warehouse" + }, + { + "id": "mixed-ja-en", + "category": "mixed", + "message": "東京 release", + "entries": [ + { + "id": "tokyo-release", + "text": "東京 release の承認は運用担当者が確認する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "大阪 deploy は別の変更記録で追跡する。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["tokyo-release"], + "expectedTopId": "tokyo-release" + }, + { + "id": "mixed-ko-en", + "category": "mixed", + "message": "데이터 quality", + "entries": [ + { + "id": "data-quality", + "text": "데이터 quality 보고서는 매주 책임자가 검토한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "서비스 release 계획은 별도 회의에서 확정한다. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["data-quality"], + "expectedTopId": "data-quality" + }, + { + "id": "mixed-nfkc-number", + "category": "mixed", + "message": "RELEASE 42 数据", + "entries": [ + { + "id": "release-42-data", + "text": "release 42 数据验证需要在上线前完成。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "distractor", + "text": "release 24 项目记录属于历史归档。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["release-42-data"], + "expectedTopId": "release-42-data" + }, + { + "id": "fallback-single", + "category": "fallback", + "message": "unmatched query", + "entries": [ + { + "id": "short-fact", + "text": "brief preference note" + }, + { + "id": "long-unrelated", + "text": "accessibility review material has no matching lexical terms. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["short-fact"], + "expectedTopId": "short-fact", + "expectedSelectedIds": ["short-fact"] + }, + { + "id": "fallback-order", + "category": "fallback", + "message": "unmatched query", + "entries": [ + { + "id": "first", + "text": "first short note" + }, + { + "id": "second", + "text": "second short note" + }, + { + "id": "third", + "text": "third short note" + }, + { + "id": "fourth", + "text": "fourth short note" + } + ], + "relevantIds": ["first", "second", "third"], + "expectedTopId": "first", + "expectedSelectedIds": ["first", "second", "third"] + }, + { + "id": "fallback-after-positive", + "category": "fallback", + "message": "deploy", + "entries": [ + { + "id": "first-fallback", + "text": "first short note" + }, + { + "id": "positive", + "text": "deploy follows the production change checklist. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "second-fallback", + "text": "second short note" + } + ], + "relevantIds": ["positive", "first-fallback", "second-fallback"], + "expectedTopId": "positive", + "expectedSelectedIds": ["positive", "first-fallback", "second-fallback"] + }, + { + "id": "none-long-unrelated", + "category": "no-result", + "message": "deploy staging", + "entries": [ + { + "id": "unrelated", + "text": "quarterly accessibility review covers keyboard navigation and contrast. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": [], + "expectedSelectedIds": [] + }, + { + "id": "none-single-cjk", + "category": "no-result", + "message": "数", + "entries": [ + { + "id": "single", + "text": "数据治理规范要求资产责任人定期完成复核。 zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": [], + "expectedSelectedIds": [] + }, + { + "id": "none-short-latin", + "category": "no-result", + "message": "x", + "entries": [ + { + "id": "single", + "text": "x is a deliberately isolated single character in this fact. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": [], + "expectedSelectedIds": [] + }, + { + "id": "none-unsafe-separators", + "category": "no-result", + "message": "deploy​target", + "entries": [ + { + "id": "joined", + "text": "deploytarget is a single joined identifier with no separator. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": [], + "expectedSelectedIds": [] + }, + { + "id": "ranking-overlap-count", + "category": "ranking", + "message": "alpha beta gamma", + "entries": [ + { + "id": "one-term", + "text": "alpha is documented in the first source. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "two-terms-first", + "text": "alpha beta are documented in the second source. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "two-terms-second", + "text": "beta gamma are documented in the third source. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["two-terms-first", "two-terms-second", "one-term"], + "expectedTopId": "two-terms-first", + "expectedSelectedIds": ["two-terms-first", "two-terms-second", "one-term"] + }, + { + "id": "ranking-document-order", + "category": "ranking", + "message": "alpha beta", + "entries": [ + { + "id": "first", + "text": "alpha appears in the first ranked source. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + }, + { + "id": "second", + "text": "beta appears in the second ranked source. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz" + } + ], + "relevantIds": ["first", "second"], + "expectedTopId": "first", + "expectedSelectedIds": ["first", "second"] + }, + { + "id": "budget-skip-middle", + "category": "budget", + "message": "aa bb cc", + "entries": [ + { + "id": "first", + "text": "aa 😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + }, + { + "id": "does-not-fit", + "text": "bb 😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + }, + { + "id": "later-fit", + "text": "cc 😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀😀" + } + ], + "relevantIds": ["first", "later-fit"], + "expectedTopId": "first", + "expectedSelectedIds": ["first", "later-fit"] + } +] diff --git a/packages/channels/base/src/channel-memory-recall-eval.test.ts b/packages/channels/base/src/channel-memory-recall-eval.test.ts new file mode 100644 index 00000000000..64d962fc08f --- /dev/null +++ b/packages/channels/base/src/channel-memory-recall-eval.test.ts @@ -0,0 +1,453 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { + CHANNEL_MEMORY_RECALL_MAX_CODE_POINTS, + CHANNEL_MEMORY_RECALL_MAX_ENTRIES, + selectRelevantChannelMemory, +} from './channel-memory-recall.js'; + +const fixtureUrl = new URL( + './__fixtures__/channel-memory-recall-eval.json', + import.meta.url, +); + +const RECALL_AT = 3; +const MAX_SELECTED_CODE_POINTS = 1_200; + +const categories = new Set([ + 'english', + 'chinese', + 'japanese', + 'korean', + 'mixed', + 'fallback', + 'no-result', + 'ranking', + 'budget', +] as const); + +type EvalCategory = + | 'english' + | 'chinese' + | 'japanese' + | 'korean' + | 'mixed' + | 'fallback' + | 'no-result' + | 'ranking' + | 'budget'; + +interface EvalEntry { + id: string; + text: string; +} + +interface EvalCase { + id: string; + category: EvalCategory; + message: string; + entries: EvalEntry[]; + relevantIds: string[]; + expectedTopId?: string; + expectedSelectedIds?: string[]; +} + +interface EvalSummary { + cases: number; + recallCases: number; + recallAt3: number; + top1Cases: number; + top1Accuracy: number; + labeledNoResultCases: number; + noResultPrecision: number; + maxSelectedEntries: number; + maxSelectedCodePoints: number; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function parseNonEmptyString(value: unknown, field: string): string { + if (typeof value !== 'string' || value.trim().length === 0) { + throw new Error(`${field} must be a non-empty string`); + } + return value; +} + +function assertKnownFields( + value: Record, + allowedFields: readonly string[], + field: string, +): void { + for (const key of Object.keys(value)) { + if (!allowedFields.includes(key)) { + throw new Error(`${field} contains an unknown field: ${key}`); + } + } +} + +function parseStringArray(value: unknown, field: string): string[] { + if (!Array.isArray(value)) { + throw new Error(`${field} must be a string array`); + } + const ids = value.map((item, index) => + parseNonEmptyString(item, `${field}[${index}]`), + ); + if (new Set(ids).size !== ids.length) { + throw new Error(`${field} must contain unique IDs`); + } + return ids; +} + +function assertReferencesBelongToEntries( + references: readonly string[], + entryIds: ReadonlySet, + field: string, +): void { + for (const id of references) { + if (!entryIds.has(id)) { + throw new Error(`${field} references an unknown entry ID: ${id}`); + } + } +} + +function parseEntry(value: unknown, caseId: string): EvalEntry { + if (!isRecord(value)) { + throw new Error(`${caseId}.entries contains an invalid entry`); + } + assertKnownFields(value, ['id', 'text'], `${caseId}.entries`); + return { + id: parseNonEmptyString(value['id'], `${caseId}.entries.id`), + text: parseNonEmptyString(value['text'], `${caseId}.entries.text`), + }; +} + +function parseCase(value: unknown): EvalCase { + if (!isRecord(value)) { + throw new Error('invalid channel-memory recall evaluation case'); + } + assertKnownFields( + value, + [ + 'id', + 'category', + 'message', + 'entries', + 'relevantIds', + 'expectedTopId', + 'expectedSelectedIds', + ], + 'evaluation case', + ); + const id = parseNonEmptyString(value['id'], 'case.id'); + if ( + typeof value['category'] !== 'string' || + !categories.has(value['category'] as EvalCategory) + ) { + throw new Error(`${id}.category must be a known category`); + } + if (!Array.isArray(value['entries'])) { + throw new Error(`${id}.entries must be an array`); + } + const entries = value['entries'].map((entry) => parseEntry(entry, id)); + const entryIds = new Set(entries.map((entry) => entry.id)); + if (entryIds.size !== entries.length) { + throw new Error(`${id}.entries must contain unique IDs`); + } + const relevantIds = parseStringArray( + value['relevantIds'], + `${id}.relevantIds`, + ); + assertReferencesBelongToEntries(relevantIds, entryIds, `${id}.relevantIds`); + const expectedSelectedIds = + value['expectedSelectedIds'] === undefined + ? undefined + : parseStringArray( + value['expectedSelectedIds'], + `${id}.expectedSelectedIds`, + ); + if (expectedSelectedIds) { + assertReferencesBelongToEntries( + expectedSelectedIds, + entryIds, + `${id}.expectedSelectedIds`, + ); + } + const expectedTopId = + value['expectedTopId'] === undefined + ? undefined + : parseNonEmptyString(value['expectedTopId'], `${id}.expectedTopId`); + if (relevantIds.length === 0) { + if (expectedTopId !== undefined) { + throw new Error(`${id}.expectedTopId is not allowed for no-result cases`); + } + } else { + if (expectedTopId === undefined) { + throw new Error(`${id}.expectedTopId is required for positive cases`); + } + if (!relevantIds.includes(expectedTopId)) { + throw new Error(`${id}.expectedTopId must be a relevant ID`); + } + } + return { + id, + category: value['category'] as EvalCategory, + message: parseNonEmptyString(value['message'], `${id}.message`), + entries, + relevantIds, + expectedTopId, + expectedSelectedIds, + }; +} + +function loadCases(): EvalCase[] { + const parsed: unknown = JSON.parse(readFileSync(fixtureUrl, 'utf8')); + if (!Array.isArray(parsed)) + throw new Error('evaluation fixture must be an array'); + const cases = parsed.map(parseCase); + if (new Set(cases.map((testCase) => testCase.id)).size !== cases.length) { + throw new Error('evaluation fixture must contain unique case IDs'); + } + return cases; +} + +function evaluate(cases: readonly EvalCase[]): { + summary: EvalSummary; + selectedIdsByCase: Record; +} { + let recallTotal = 0; + let recallCases = 0; + let top1Hits = 0; + let top1Cases = 0; + let correctNoResultPredictions = 0; + let noResultPredictions = 0; + let labeledNoResultCases = 0; + let maxSelectedEntries = 0; + let maxSelectedCodePoints = 0; + const selectedIdsByCase: Record = {}; + + for (const testCase of cases) { + const selected = selectRelevantChannelMemory( + testCase.message, + testCase.entries, + ); + const selectedIds = selected.map((entry) => entry.id); + selectedIdsByCase[testCase.id] = selectedIds; + maxSelectedEntries = Math.max(maxSelectedEntries, selected.length); + maxSelectedCodePoints = Math.max( + maxSelectedCodePoints, + selected.reduce((sum, entry) => sum + Array.from(entry.text).length, 0), + ); + + if (testCase.relevantIds.length > 0) { + const hits = testCase.relevantIds.filter((id) => + selectedIds.slice(0, RECALL_AT).includes(id), + ).length; + recallTotal += hits / testCase.relevantIds.length; + recallCases += 1; + } + if (testCase.expectedTopId) { + top1Hits += selectedIds[0] === testCase.expectedTopId ? 1 : 0; + top1Cases += 1; + } + if (testCase.relevantIds.length === 0) { + labeledNoResultCases += 1; + } + if (selected.length === 0) { + noResultPredictions += 1; + correctNoResultPredictions += testCase.relevantIds.length === 0 ? 1 : 0; + } + } + + return { + summary: { + cases: cases.length, + recallCases, + recallAt3: recallCases === 0 ? 0 : recallTotal / recallCases, + top1Cases, + top1Accuracy: top1Cases === 0 ? 0 : top1Hits / top1Cases, + labeledNoResultCases, + noResultPrecision: + noResultPredictions === 0 + ? 0 + : correctNoResultPredictions / noResultPredictions, + maxSelectedEntries, + maxSelectedCodePoints, + }, + selectedIdsByCase, + }; +} + +const requiredCaseIds = [ + 'en-deploy-staging', + 'en-incident-runbook', + 'en-nfkc-case', + 'en-repeated-token', + 'en-number-42', + 'en-stable-tie', + 'zh-data-governance', + 'zh-release-window', + 'zh-role-preference', + 'zh-project-alias', + 'zh-incident-owner', + 'zh-longer-run', + 'zh-nfkc', + 'zh-single-character-no-result', + 'ja-hiragana', + 'ja-katakana', + 'ja-prolonged-mark', + 'ja-mixed-script', + 'ko-data-quality', + 'ko-release-owner', + 'ko-incident-runbook', + 'ko-mixed-number', + 'mixed-zh-en', + 'mixed-ja-en', + 'mixed-ko-en', + 'mixed-nfkc-number', + 'fallback-single', + 'fallback-order', + 'fallback-after-positive', + 'none-long-unrelated', + 'none-single-cjk', + 'none-short-latin', + 'none-unsafe-separators', + 'ranking-overlap-count', + 'ranking-document-order', + 'budget-skip-middle', +] as const; + +const expectedCategoryCounts: Record = { + english: 6, + chinese: 8, + japanese: 4, + korean: 4, + mixed: 4, + fallback: 3, + 'no-result': 4, + ranking: 2, + budget: 1, +}; + +const requiredSelectedIds = { + 'fallback-single': ['short-fact'], + 'fallback-order': ['first', 'second', 'third'], + 'fallback-after-positive': ['positive', 'first-fallback', 'second-fallback'], + 'en-stable-tie': ['first-tie', 'second-tie'], + 'budget-skip-middle': ['first', 'later-fit'], + 'none-long-unrelated': [], + 'none-single-cjk': [], + 'none-short-latin': [], + 'none-unsafe-separators': [], + 'zh-single-character-no-result': [], + 'ranking-overlap-count': ['two-terms-first', 'two-terms-second', 'one-term'], + 'ranking-document-order': ['first', 'second'], +} as const; + +describe('channel memory recall evaluation', () => { + it('counts empty selections on positive cases against no-result precision', () => { + const cases: EvalCase[] = [ + { + id: 'correct-no-result', + category: 'no-result', + message: 'probetoken', + entries: [ + { + id: 'unrelated', + text: 'This entry contains deliberately distinct vocabulary and enough content to avoid fallback selection. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', + }, + ], + relevantIds: [], + }, + { + id: 'false-no-result', + category: 'english', + message: 'probetoken', + entries: [ + { + id: 'relevant', + text: 'This relevant entry contains deliberately distinct vocabulary and enough content to avoid fallback selection. zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz', + }, + ], + relevantIds: ['relevant'], + expectedTopId: 'relevant', + }, + ]; + + expect(evaluate(cases).summary.noResultPrecision).toBe(0.5); + }); + + it('loads the complete synthetic multilingual fixture', () => { + const cases = loadCases(); + expect(cases).toHaveLength(36); + expect(new Set(cases.map((testCase) => testCase.id)).size).toBe( + cases.length, + ); + expect(cases.map((testCase) => testCase.id).sort()).toEqual( + [...requiredCaseIds].sort(), + ); + const categoryCounts = cases.reduce>( + (counts, testCase) => { + counts[testCase.category] += 1; + return counts; + }, + { + english: 0, + chinese: 0, + japanese: 0, + korean: 0, + mixed: 0, + fallback: 0, + 'no-result': 0, + ranking: 0, + budget: 0, + }, + ); + expect(categoryCounts).toEqual(expectedCategoryCounts); + expect(new Set(cases.map((testCase) => testCase.category))).toEqual( + categories, + ); + }); + + it('holds the multilingual quality floor', () => { + const { summary } = evaluate(loadCases()); + expect(summary.cases).toBe(36); + expect(summary.recallCases).toBe(31); + expect(summary.top1Cases).toBe(31); + expect(summary.labeledNoResultCases).toBe(5); + expect(summary.recallAt3).toBeGreaterThanOrEqual(0.9); + expect(summary.top1Accuracy).toBeGreaterThanOrEqual(0.85); + expect(summary.noResultPrecision).toBe(1); + }); + + it('keeps every evaluation result within the existing prompt budgets', () => { + const { summary } = evaluate(loadCases()); + expect(CHANNEL_MEMORY_RECALL_MAX_ENTRIES).toBe(RECALL_AT); + expect(CHANNEL_MEMORY_RECALL_MAX_CODE_POINTS).toBe( + MAX_SELECTED_CODE_POINTS, + ); + expect(summary.maxSelectedEntries).toBeLessThanOrEqual(RECALL_AT); + expect(summary.maxSelectedCodePoints).toBeLessThanOrEqual( + MAX_SELECTED_CODE_POINTS, + ); + }); + + it('asserts the required ordered selected IDs', () => { + const cases = loadCases(); + const { selectedIdsByCase } = evaluate(cases); + + for (const [caseId, expectedSelectedIds] of Object.entries( + requiredSelectedIds, + )) { + const testCase = cases.find((candidate) => candidate.id === caseId); + expect(testCase?.expectedSelectedIds).toEqual(expectedSelectedIds); + expect(selectedIdsByCase[caseId]).toEqual(expectedSelectedIds); + } + }); + + it('produces deterministic selected IDs and summary values', () => { + const cases = loadCases(); + expect(evaluate(cases)).toEqual(evaluate(cases)); + }); +});