alphacouncil-agent 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/alpha.md +22 -3
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.grok/commands/alpha.md +22 -3
- package/.opencode/command/alpha.md +22 -3
- package/AGENTS.md +27 -4
- package/CHANGELOG.md +67 -0
- package/CLAUDE.md +41 -7
- package/README.ja.md +21 -11
- package/README.md +35 -11
- package/README.zh-CN.md +31 -11
- package/commands/alpha.md +22 -3
- package/data/build-profile.v1.json +1 -1
- package/data/master-selector-method-locales.v1.mjs +286 -0
- package/docs/INSTALL.md +43 -6
- package/docs/releases/v0.9.3.md +86 -0
- package/docs/releases/v0.9.4.md +79 -0
- package/docs/report-contract.md +50 -1
- package/knowledge/ai-assisted-solo/experiments/runs/a.json +4 -4
- package/knowledge/ai-assisted-solo/experiments/runs/b.json +2 -2
- package/knowledge/ai-assisted-solo/experiments/runs/c.json +2 -2
- package/knowledge/ai-assisted-solo/experiments/runs/d13.json +16 -16
- package/knowledge/ai-assisted-solo/experiments/runs/d26.json +29 -29
- package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +19 -19
- package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +32 -32
- package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +33 -33
- package/knowledge/ai-assisted-solo/experiments/simulation-input.json +79 -79
- package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
- package/knowledge/solo-test/masters/master_ackman/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_aschenbrenner/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_asness/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_buffett/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_burry/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_cathie_wood/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_dalio/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_damodaran/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_druckenmiller/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_duan_yongping/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_fisher/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_forensic_short/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_graham/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_jhunjhunwala/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_klarman/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_li_lu/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_lynch/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_marks/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_munger/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_natenberg/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_pabrai/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_simons/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_sinclair/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_soros/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_taleb/manifest.json +14 -4
- package/knowledge/solo-test/masters/master_thorp/manifest.json +14 -4
- package/mcp/lib/codex.mjs +3 -3
- package/mcp/lib/constants.mjs +61 -25
- package/mcp/lib/council-options.mjs +95 -40
- package/mcp/lib/council-selection.mjs +95 -9
- package/mcp/lib/gates.mjs +78 -11
- package/mcp/lib/lang.mjs +109 -2
- package/mcp/lib/markdown.mjs +444 -135
- package/mcp/lib/master-catalog.mjs +28 -3
- package/mcp/lib/orchestrator.mjs +952 -265
- package/mcp/lib/packets.mjs +173 -20
- package/mcp/lib/personas/engine.mjs +30 -30
- package/mcp/lib/personas-v3/compiler.mjs +2 -0
- package/mcp/lib/personas-v3/loader.mjs +10 -7
- package/mcp/lib/personas-v3/source-acquisition.mjs +10 -3
- package/mcp/lib/prompts.mjs +33 -0
- package/mcp/lib/rpc.mjs +75 -33
- package/mcp/lib/run-store.mjs +23 -0
- package/package.json +2 -2
- package/schemas/persona-v3.schema.json +37 -2
- package/scripts/lib/packaged-host-parity.mjs +54 -5
- package/scripts/lib/persona-v3-solo-test-packs.mjs +31 -5
- package/scripts/package-smoke.mjs +31 -12
- package/scripts/run-tests.mjs +23 -2
- package/skills/alphacouncil-agent/SKILL.md +50 -7
package/mcp/lib/gates.mjs
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { DEBATE_ROLES, LIMITS, PLACEHOLDER_BODIES, QUICK_REPORT_SECTIONS, REPORT_SECTIONS } from "./constants.mjs";
|
|
2
2
|
import { denseLength, headingIncludesAlias, normalizeHeading, parseHeadings } from "./headings.mjs";
|
|
3
|
-
import { isChineseLanguage } from "./lang.mjs";
|
|
3
|
+
import { isChineseLanguage, languageKey, readerLanguageStatus } from "./lang.mjs";
|
|
4
4
|
|
|
5
5
|
export function withDisclaimer(markdown, language) {
|
|
6
6
|
const text = typeof markdown === "string" ? markdown : "";
|
|
7
|
-
if (/##\s*(Disclaimer
|
|
8
|
-
const note =
|
|
9
|
-
|
|
10
|
-
: "\n\n---\n\n## Disclaimer\n\nThis report is AI-generated for **educational and research purposes only**. It is **not investment advice**, not a recommendation to buy or sell any security, and not a solicitation. AI analysis can be incomplete, outdated, or wrong. Do your own research and consult a licensed professional before any investment decision. The authors accept no liability for any loss."
|
|
7
|
+
if (/##\s*(Disclaimer|免责声明|免責事項|면책)/iu.test(text)) return text;
|
|
8
|
+
const note = {
|
|
9
|
+
zh: "\n\n---\n\n## 免责声明\n\n本报告由 AI 自动生成,**仅供教育与研究**,**不构成投资建议**,也不构成任何证券买卖推荐或要约。AI 分析可能不完整、过时或错误。投资决策前请自行核实并咨询持牌专业人士。作者不对任何损失承担责任。",
|
|
10
|
+
en: "\n\n---\n\n## Disclaimer\n\nThis report is AI-generated for **educational and research purposes only**. It is **not investment advice**, not a recommendation to buy or sell any security, and not a solicitation. AI analysis can be incomplete, outdated, or wrong. Do your own research and consult a licensed professional before any investment decision. The authors accept no liability for any loss.",
|
|
11
|
+
ja: "\n\n---\n\n## 免責事項\n\n本レポートは AI が生成した**教育・調査目的のみ**の資料で、**投資助言ではありません**。証券の売買推奨や勧誘でもありません。AI の分析は不完全、古い、または誤っている可能性があります。投資判断の前にご自身で確認し、資格を有する専門家へ相談してください。作成者は損失について責任を負いません。",
|
|
12
|
+
ko: "\n\n---\n\n## 면책 조항\n\n이 보고서는 AI가 생성한 **교육 및 연구 목적의 자료**이며 **투자 조언이 아닙니다**. 증권 매매 권유나 청약도 아닙니다. AI 분석은 불완전하거나 오래되었거나 틀릴 수 있습니다. 투자 결정 전에 직접 확인하고 자격을 갖춘 전문가와 상의하십시오. 작성자는 손실에 대해 책임을 지지 않습니다.",
|
|
13
|
+
}[languageKey(language)];
|
|
11
14
|
return `${text}${note}`;
|
|
12
15
|
}
|
|
13
16
|
|
|
@@ -18,9 +21,12 @@ export function withVerificationBanner(markdown, gate, language) {
|
|
|
18
21
|
const lines = pairs.length
|
|
19
22
|
? pairs.map((item) => `- ${item.task}: ${item.source_id}`).join("\n")
|
|
20
23
|
: "- (unspecified)";
|
|
21
|
-
const banner =
|
|
22
|
-
|
|
23
|
-
: `\n\n---\n\n## Source Verification Gate
|
|
24
|
+
const banner = {
|
|
25
|
+
zh: `\n\n---\n\n## 来源核验\n\n**状态:needs_verification。** 以下重大论断引用了证据包中不存在的来源 ID;本轮尚未通过来源核验:\n\n${lines}\n`,
|
|
26
|
+
en: `\n\n---\n\n## Source Verification Gate\n\n**Status: needs_verification.** The following material claims cite source IDs that are not present in any evidence packet; this run has NOT passed source verification:\n\n${lines}\n`,
|
|
27
|
+
ja: `\n\n---\n\n## 出典検証ゲート\n\n**状態:needs_verification。** 次の重要な主張は証拠パケットに存在しない出典 ID を参照しています。この実行は出典検証を通過していません:\n\n${lines}\n`,
|
|
28
|
+
ko: `\n\n---\n\n## 출처 검증 게이트\n\n**상태: needs_verification.** 다음 중요 주장은 증거 패킷에 없는 출처 ID를 참조합니다. 이 실행은 출처 검증을 통과하지 못했습니다:\n\n${lines}\n`,
|
|
29
|
+
}[languageKey(language)];
|
|
24
30
|
return `${text}${banner}`;
|
|
25
31
|
}
|
|
26
32
|
|
|
@@ -139,9 +145,12 @@ export function withCompletenessBanner(markdown, completeness, language) {
|
|
|
139
145
|
// Naming the skipped seats matters more than the flag: "incomplete" without a list
|
|
140
146
|
// invites the reader to assume it was something minor.
|
|
141
147
|
const msLine = ms.length ? ms.map((id) => `- ${id}`).join("\n") : "";
|
|
142
|
-
const banner =
|
|
143
|
-
|
|
144
|
-
: `> [!WARNING]\n## Incomplete Council Run
|
|
148
|
+
const banner = {
|
|
149
|
+
zh: `> [!WARNING]\n## 委员会流程未完成\n\n**状态:incomplete。** 本轮没有完成全部委员会流程,结论不可靠。\n\n未完成的证据席:\n${evLine}\n\n未完成的辩论席:\n${dbLine}\n${msLine ? `\n未给出意见的方法席:\n${msLine}\n` : ""}`,
|
|
150
|
+
en: `> [!WARNING]\n## Incomplete Council Run\n\n**Status: incomplete.** This run did NOT execute the full council workflow; the conclusion is unreliable.\n\nMissing evidence roles:\n${evLine}\n\nMissing debate roles:\n${dbLine}\n${msLine ? `\nMethod seats that gave no opinion:\n${msLine}\n` : ""}`,
|
|
151
|
+
ja: `> [!WARNING]\n## 委員会プロセス未完了\n\n**状態:incomplete。** 委員会の全工程を完了していないため、結論は信頼できません。\n\n未完了の証拠席:\n${evLine}\n\n未完了の討論席:\n${dbLine}\n${msLine ? `\n意見を記録できなかったメソッド席:\n${msLine}\n` : ""}`,
|
|
152
|
+
ko: `> [!WARNING]\n## 위원회 실행 미완료\n\n**상태: incomplete.** 전체 위원회 절차가 완료되지 않아 결론을 신뢰할 수 없습니다.\n\n미완료 증거 좌석:\n${evLine}\n\n미완료 토론 좌석:\n${dbLine}\n${msLine ? `\n의견을 기록하지 못한 방법론 좌석:\n${msLine}\n` : ""}`,
|
|
153
|
+
}[languageKey(language)];
|
|
145
154
|
return `${banner}\n\n---\n\n${text}`;
|
|
146
155
|
}
|
|
147
156
|
|
|
@@ -182,6 +191,54 @@ const isPlaceholder = (body) => {
|
|
|
182
191
|
return PLACEHOLDER_BODIES.includes(compact);
|
|
183
192
|
};
|
|
184
193
|
|
|
194
|
+
const LANGUAGE_CORE_SECTIONS = new Set([
|
|
195
|
+
"conclusion", "analyst_work_log", "debate_record", "master_bench", "market_expectations",
|
|
196
|
+
"earnings_call", "news", "valuation", "price_levels", "risks", "position", "data_gaps",
|
|
197
|
+
"invalidation",
|
|
198
|
+
]);
|
|
199
|
+
|
|
200
|
+
function readerLanguageAudit(assigned, language) {
|
|
201
|
+
const requested = languageKey(language);
|
|
202
|
+
const mismatched = [];
|
|
203
|
+
let targetCharacters = 0;
|
|
204
|
+
let observedCharacters = 0;
|
|
205
|
+
const bodies = [];
|
|
206
|
+
for (const id of LANGUAGE_CORE_SECTIONS) {
|
|
207
|
+
const heading = assigned.get(id);
|
|
208
|
+
const body = heading?.body || "";
|
|
209
|
+
if (!body.trim()) continue;
|
|
210
|
+
bodies.push(body);
|
|
211
|
+
const section = readerLanguageStatus(body, language, { minimumTargetCharacters: 2, minimumRatio: 0.05 });
|
|
212
|
+
targetCharacters += section.target_characters;
|
|
213
|
+
observedCharacters += section.reader_characters;
|
|
214
|
+
// Pure Han fragments are inconclusive across Chinese/Japanese. Let the whole
|
|
215
|
+
// report establish the locale, but reject sections with positive other-language
|
|
216
|
+
// evidence so one translated heading cannot hide a foreign-language body.
|
|
217
|
+
if (![requested, "shared_han", "undetermined"].includes(section.observed_locale)) mismatched.push(id);
|
|
218
|
+
|
|
219
|
+
const titleProbe = readerLanguageStatus(heading?.title || "", language, { minimumTargetCharacters: 1, minimumRatio: 0 });
|
|
220
|
+
const { scripts = {} } = titleProbe;
|
|
221
|
+
let titleLocale = "undetermined";
|
|
222
|
+
if ((scripts.hangul || 0) > 0) titleLocale = "ko";
|
|
223
|
+
else if ((scripts.kana || 0) > 0 || (titleProbe.japanese_markers || 0) > 0) titleLocale = "ja";
|
|
224
|
+
else if ((titleProbe.chinese_markers || 0) > 0) titleLocale = "zh";
|
|
225
|
+
else if ((scripts.latin || 0) > 0 && (scripts.han || 0) === 0) titleLocale = "en";
|
|
226
|
+
else if ((scripts.han || 0) > 0) titleLocale = "shared_han";
|
|
227
|
+
const sharedHanAllowed = titleLocale === "shared_han" && ["zh", "ja"].includes(requested);
|
|
228
|
+
if (![requested, "undetermined"].includes(titleLocale) && !sharedHanAllowed) mismatched.push(`heading:${id}`);
|
|
229
|
+
}
|
|
230
|
+
const whole = readerLanguageStatus(bodies.join("\n"), language, { minimumTargetCharacters: 12, minimumRatio: 0.08 });
|
|
231
|
+
const languageStatus = mismatched.length || whole.status !== "passed" ? "failed" : "passed";
|
|
232
|
+
return {
|
|
233
|
+
requested_locale: requested,
|
|
234
|
+
observed_locale: whole.observed_locale,
|
|
235
|
+
language_status: languageStatus,
|
|
236
|
+
target_script_characters: whole.target_characters || targetCharacters,
|
|
237
|
+
reader_characters_checked: observedCharacters,
|
|
238
|
+
mismatched_sections: mismatched,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
185
242
|
export function validateFinalReport(markdown, run) {
|
|
186
243
|
const text = String(markdown || "");
|
|
187
244
|
const headings = parseHeadings(text);
|
|
@@ -231,6 +288,10 @@ export function validateFinalReport(markdown, run) {
|
|
|
231
288
|
: quick ? LIMITS.REPORT_MIN_CHARS_QUICK : LIMITS.REPORT_MIN_CHARS;
|
|
232
289
|
if (denseLength(text) < minLength) missing.push(`report too short: minimum ${minLength} non-space characters`);
|
|
233
290
|
const execution = completenessStatus(run || {});
|
|
291
|
+
const languageAudit = readerLanguageAudit(assigned, run?.language);
|
|
292
|
+
if (languageAudit.language_status !== "passed") {
|
|
293
|
+
missing.push(`report reader language mismatch: requested=${languageAudit.requested_locale}; sections=${languageAudit.mismatched_sections.join(",") || "insufficient target-language text"}`);
|
|
294
|
+
}
|
|
234
295
|
if (quick) {
|
|
235
296
|
const markerCount = (text.match(/alphacouncil:quick-scope:v1:begin/gu) || []).length;
|
|
236
297
|
if (markerCount !== 1) missing.push("missing system-owned quick_v1 scope marker");
|
|
@@ -263,6 +324,12 @@ export function validateFinalReport(markdown, run) {
|
|
|
263
324
|
evidence_coverage: execution.evidence_coverage,
|
|
264
325
|
degraded_evidence: execution.degraded_evidence,
|
|
265
326
|
degraded_debate: execution.degraded_debate,
|
|
327
|
+
requested_locale: languageAudit.requested_locale,
|
|
328
|
+
observed_locale: languageAudit.observed_locale,
|
|
329
|
+
language_status: languageAudit.language_status,
|
|
330
|
+
language_mismatched_sections: languageAudit.mismatched_sections,
|
|
331
|
+
target_script_characters: languageAudit.target_script_characters,
|
|
332
|
+
reader_characters_checked: languageAudit.reader_characters_checked,
|
|
266
333
|
status: missing.length ? "needs_revision" : "passed",
|
|
267
334
|
missing,
|
|
268
335
|
sections,
|
package/mcp/lib/lang.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export function normalizeLanguage(value) {
|
|
2
2
|
const text = String(value || "").trim();
|
|
3
|
-
if (!text || /^auto|default|same|follow
|
|
3
|
+
if (!text || /^(?:auto|default|same|follow|跟随|默认)$/i.test(text)) return "";
|
|
4
4
|
if (/^(zh|zh-cn|cn|chinese|中文|简体中文|繁体中文)$/i.test(text)) return "中文";
|
|
5
5
|
if (/^(en|en-us|english|英文)$/i.test(text)) return "English";
|
|
6
6
|
if (/^(ja|jp|ja-jp|japanese|日文|日本語)$/i.test(text)) return "日本語";
|
|
7
|
-
if (/^(ko|kr|korean|韩文|韓文|한국어)$/i.test(text)) return "한국어";
|
|
7
|
+
if (/^(ko|kr|ko-kr|korean|韩文|韓文|한국어)$/i.test(text)) return "한국어";
|
|
8
8
|
return text.slice(0, 40);
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -22,3 +22,110 @@ export function resolveLanguage(args = {}) {
|
|
|
22
22
|
export function isChineseLanguage(language) {
|
|
23
23
|
return /中文|chinese|zh/i.test(String(language || ""));
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
export function languageKey(language) {
|
|
27
|
+
const text = String(language || "");
|
|
28
|
+
if (/中文|chinese|zh/i.test(text)) return "zh";
|
|
29
|
+
if (/日本語|japanese|ja/i.test(text)) return "ja";
|
|
30
|
+
if (/한국어|korean|ko/i.test(text)) return "ko";
|
|
31
|
+
return "en";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function localized(language, messages) {
|
|
35
|
+
const key = languageKey(language);
|
|
36
|
+
return messages?.[key] ?? messages?.en ?? "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const count = (value, pattern) => (value.match(pattern) || []).length;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Detect reader prose without pretending that Han characters alone distinguish
|
|
43
|
+
* Chinese from Japanese. URLs, stable ids, tickers and JSON keys are removed first.
|
|
44
|
+
* The result is deliberately tri-state: an all-Han fragment can be shared_script,
|
|
45
|
+
* while a whole report or worker packet must still provide positive target-language
|
|
46
|
+
* evidence before it is accepted.
|
|
47
|
+
*/
|
|
48
|
+
export function readerLanguageStatus(text, language, { minimumTargetCharacters = 4, minimumRatio = 0.08 } = {}) {
|
|
49
|
+
const requested = languageKey(language);
|
|
50
|
+
const value = String(text || "").replace(
|
|
51
|
+
/https?:\/\/\S+|`[^`]+`|\b[a-z]+(?:_[a-z0-9]+)+\b|\b[A-Z0-9_:-]{3,}\b/gu,
|
|
52
|
+
"",
|
|
53
|
+
);
|
|
54
|
+
const scripts = {
|
|
55
|
+
latin: count(value, /[A-Za-z]/gu),
|
|
56
|
+
han: count(value, /\p{Script=Han}/gu),
|
|
57
|
+
kana: count(value, /[\p{Script=Hiragana}\p{Script=Katakana}]/gu),
|
|
58
|
+
hangul: count(value, /\p{Script=Hangul}/gu),
|
|
59
|
+
};
|
|
60
|
+
const scriptCharacters = scripts.latin + scripts.han + scripts.kana + scripts.hangul;
|
|
61
|
+
const englishWords = count(value, /\b[A-Za-z][A-Za-z'-]{1,}\b/gu);
|
|
62
|
+
// Strong Japanese orthography covers legitimate finance fragments that contain
|
|
63
|
+
// only Kanji, such as "売上高100億円、営業利益20億円。".
|
|
64
|
+
const japaneseMarkers = count(
|
|
65
|
+
value,
|
|
66
|
+
/売上高|営業利益|経常利益|前年同期|億円|兆円|株価|一株当たり|見通し|割安|割高/gu,
|
|
67
|
+
);
|
|
68
|
+
// zh-CN needs positive Chinese evidence rather than treating every Han-only
|
|
69
|
+
// fragment as Chinese. The character set intentionally uses simplified forms;
|
|
70
|
+
// common Chinese function words also cover natural traditional-Chinese prose.
|
|
71
|
+
const chineseMarkers = count(
|
|
72
|
+
value,
|
|
73
|
+
/[这们为与后发资价长净现应从过还将对门间时来买卖稳处务审计报损证据实认风险经营让给开关币额节把已核验断确录场环恶状况财变达点则无并区进增论结师话量闻值仓催剂数议项标属声说]|的|了|是|在|和|以及|因此|但是|如果|本轮|本节|未知|不可用/gu,
|
|
74
|
+
);
|
|
75
|
+
const ratios = Object.fromEntries(Object.entries(scripts).map(([key, hits]) => [
|
|
76
|
+
key,
|
|
77
|
+
scriptCharacters ? hits / scriptCharacters : 0,
|
|
78
|
+
]));
|
|
79
|
+
|
|
80
|
+
let observed = "undetermined";
|
|
81
|
+
if (scripts.hangul >= 2 && ratios.hangul >= 0.2) observed = "ko";
|
|
82
|
+
else if ((scripts.kana >= 2 && (scripts.kana / Math.max(1, scripts.kana + scripts.han)) >= 0.03) || japaneseMarkers >= 2) observed = "ja";
|
|
83
|
+
else if (scripts.latin >= 12 && englishWords >= 3 && ratios.latin >= 0.6) observed = "en";
|
|
84
|
+
else if (scripts.han >= 4 && scripts.kana === 0 && scripts.hangul === 0 && ratios.han >= 0.6) {
|
|
85
|
+
observed = chineseMarkers > 0 ? "zh" : "shared_han";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const targets = {
|
|
89
|
+
en: scripts.latin,
|
|
90
|
+
zh: scripts.han,
|
|
91
|
+
ja: scripts.kana + japaneseMarkers,
|
|
92
|
+
ko: scripts.hangul,
|
|
93
|
+
};
|
|
94
|
+
const target = targets[requested] || 0;
|
|
95
|
+
const targetRatio = {
|
|
96
|
+
en: ratios.latin,
|
|
97
|
+
zh: ratios.han,
|
|
98
|
+
ja: scriptCharacters ? (scripts.kana + Math.min(scripts.han, japaneseMarkers * 4)) / scriptCharacters : 0,
|
|
99
|
+
ko: ratios.hangul,
|
|
100
|
+
}[requested] || 0;
|
|
101
|
+
const positive = {
|
|
102
|
+
en: observed === "en",
|
|
103
|
+
zh: observed === "zh" && japaneseMarkers < 2,
|
|
104
|
+
ja: observed === "ja",
|
|
105
|
+
ko: observed === "ko",
|
|
106
|
+
}[requested] === true;
|
|
107
|
+
const enough = requested === "en"
|
|
108
|
+
? scripts.latin >= Math.max(12, minimumTargetCharacters) && englishWords >= 3 && targetRatio >= Math.max(0.6, minimumRatio)
|
|
109
|
+
: target >= minimumTargetCharacters && targetRatio >= minimumRatio;
|
|
110
|
+
return {
|
|
111
|
+
status: positive && enough ? "passed" : "failed",
|
|
112
|
+
requested_locale: requested,
|
|
113
|
+
observed_locale: observed,
|
|
114
|
+
target_characters: target,
|
|
115
|
+
reader_characters: scriptCharacters,
|
|
116
|
+
ratio: targetRatio,
|
|
117
|
+
scripts,
|
|
118
|
+
english_words: englishWords,
|
|
119
|
+
japanese_markers: japaneseMarkers,
|
|
120
|
+
chinese_markers: chineseMarkers,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function assertReaderLanguage(text, language, label = "worker output") {
|
|
125
|
+
const result = readerLanguageStatus(text, language);
|
|
126
|
+
if (result.status === "passed") return result;
|
|
127
|
+
const error = new Error(`${label} reader language mismatch: requested=${result.requested_locale}; target_characters=${result.target_characters}; ratio=${result.ratio}`);
|
|
128
|
+
error.code = "READER_LANGUAGE_MISMATCH";
|
|
129
|
+
error.data = result;
|
|
130
|
+
throw error;
|
|
131
|
+
}
|