|
860 | 860 | executionEvidenceDetail: "策略:{strategy} · 数据:{data} · 执行:{execution} · Shadow:{shadow} · Paper:{paper}", |
861 | 861 | executionEvidenceNoOrder: "固定边界:只读证据;不包含账户、订单、资金或 P6 实盘授权。", |
862 | 862 | executionEvidenceNext: "下一步", |
| 863 | + m0ResearchBoard: "按需查看 M0 研究观察", |
| 864 | + m0ResearchLoginNotice: "登录后读取私有 M0 研究台账;缺失台账不会被补成研究结论。", |
| 865 | + m0ResearchStaleNotice: "M0 研究观察已超过有效期,仅保留为历史上下文。", |
| 866 | + m0ResearchUnavailableNotice: "还没有可用的 M0 研究台账;页面保持 fail-closed 空状态。", |
| 867 | + m0ResearchUpstreamNotice: "M0 研究台账已加载,但有 {count} 个上游提示;页面不会补造研究结论。", |
| 868 | + m0ResearchEmpty: "暂无可展示的已验证 M0 研究观察。", |
| 869 | + m0ResearchMore: "仅显示前 {count} 条研究观察;其余仍保留在只读台账中。", |
| 870 | + m0ResearchMeta: "{kind} · 查看于:{viewed}", |
| 871 | + m0ResearchStateFreshness: "状态:{state} · 新鲜度:{freshness}", |
| 872 | + m0ResearchHorizons: "主要观察期:{primary} · 适用观察期:{suitable}", |
| 873 | + m0ResearchEvidence: "置信:{confidence} · 风格:{style} · 资料摘要:{digest}", |
| 874 | + m0ResearchConsistency: "当前周期分歧:{conflict} · 历史失效漂移:{drift}", |
| 875 | + m0ResearchNoOrder: "固定边界:仅研究、无订单;不产生任何运行动作。", |
863 | 876 | adaptiveSelectionBoard: "按需查看 M1 Shadow 建议", |
864 | 877 | adaptiveSelectionLoginNotice: "登录后读取私有 Shadow 建议;缺失来源不会被补成策略或订单。", |
865 | 878 | adaptiveSelectionStaleNotice: "此 Shadow 建议已超出新鲜度窗口,仅保留为历史上下文。", |
|
1129 | 1142 | executionEvidenceDetail: "strategy: {strategy} · data: {data} · execution: {execution} · shadow: {shadow} · paper: {paper}", |
1130 | 1143 | executionEvidenceNoOrder: "Fixed boundary: read-only evidence; no account, order, funds, or P6 live authority.", |
1131 | 1144 | executionEvidenceNext: "NEXT", |
| 1145 | + m0ResearchBoard: "View M0 research observations on demand", |
| 1146 | + m0ResearchLoginNotice: "Sign in to read the private M0 research ledger. A missing ledger never implies a research conclusion.", |
| 1147 | + m0ResearchStaleNotice: "These M0 research observations are beyond their validity window and remain historical context only.", |
| 1148 | + m0ResearchUnavailableNotice: "No usable M0 research ledger is available yet. This view remains fail-closed and empty.", |
| 1149 | + m0ResearchUpstreamNotice: "The M0 research ledger loaded with {count} upstream notice(s); no research conclusion is invented.", |
| 1150 | + m0ResearchEmpty: "No verified M0 research observation is available to display.", |
| 1151 | + m0ResearchMore: "Showing the first {count} research observations; the remainder stays in the read-only ledger.", |
| 1152 | + m0ResearchMeta: "{kind} · viewed {viewed}", |
| 1153 | + m0ResearchStateFreshness: "State: {state} · freshness: {freshness}", |
| 1154 | + m0ResearchHorizons: "Primary horizon: {primary} · suitable horizons: {suitable}", |
| 1155 | + m0ResearchEvidence: "Confidence: {confidence} · style: {style} · source digest: {digest}", |
| 1156 | + m0ResearchConsistency: "Current horizon conflict: {conflict} · historical stale drift: {drift}", |
| 1157 | + m0ResearchNoOrder: "Fixed boundary: research only, no order, and no runtime action.", |
1132 | 1158 | adaptiveSelectionBoard: "View M1 Shadow suggestions on demand", |
1133 | 1159 | adaptiveSelectionLoginNotice: "Sign in to read private Shadow suggestions. Missing sources never imply a strategy or order.", |
1134 | 1160 | adaptiveSelectionStaleNotice: "This Shadow suggestion is beyond its freshness window and remains historical context only.", |
|
1409 | 1435 | candidates: [], |
1410 | 1436 | errors: [], |
1411 | 1437 | }, |
| 1438 | + m0Research: { |
| 1439 | + payload: { |
| 1440 | + data_status: "unavailable", |
| 1441 | + viewed_at: null, |
| 1442 | + summary: { |
| 1443 | + subject_count: 0, |
| 1444 | + observation_count: 0, |
| 1445 | + fresh_observation_count: 0, |
| 1446 | + stale_observation_count: 0, |
| 1447 | + unknown_observation_count: 0, |
| 1448 | + horizon_conflict_count: 0, |
| 1449 | + historical_stale_horizon_drift_count: 0, |
| 1450 | + }, |
| 1451 | + subjects: [], |
| 1452 | + policy: { authority: "research_only", no_order: true, permitted_next_step: "research_validation_only" }, |
| 1453 | + errors: [], |
| 1454 | + }, |
| 1455 | + }, |
1412 | 1456 | adaptiveSelection: { |
1413 | 1457 | payload: { |
1414 | 1458 | data_status: "unavailable", |
|
3720 | 3764 | } |
3721 | 3765 | } |
3722 | 3766 |
|
| 3767 | + const M0_RESEARCH_DISPLAY_LIMIT = 100; |
| 3768 | + |
| 3769 | + const m0ResearchLabels = { |
| 3770 | + subject: { |
| 3771 | + asset_idea: { zh: "资产观察", en: "Asset idea" }, |
| 3772 | + theme_context: { zh: "主题背景", en: "Theme context" }, |
| 3773 | + strategy_hypothesis: { zh: "研究假设", en: "Research hypothesis" }, |
| 3774 | + risk_context: { zh: "风险背景", en: "Risk context" }, |
| 3775 | + }, |
| 3776 | + state: { |
| 3777 | + candidate: { zh: "待验证", en: "Candidate" }, |
| 3778 | + source_verification_required: { zh: "等待来源核验", en: "Source verification required" }, |
| 3779 | + deferred: { zh: "暂缓", en: "Deferred" }, |
| 3780 | + context_only: { zh: "仅作背景", en: "Context only" }, |
| 3781 | + }, |
| 3782 | + freshness: { |
| 3783 | + fresh: { zh: "有效", en: "Fresh" }, |
| 3784 | + stale: { zh: "已过期", en: "Stale" }, |
| 3785 | + unknown: { zh: "未知", en: "Unknown" }, |
| 3786 | + }, |
| 3787 | + horizon: { |
| 3788 | + short: { zh: "短期", en: "Short" }, |
| 3789 | + medium: { zh: "中期", en: "Medium" }, |
| 3790 | + long: { zh: "长期", en: "Long" }, |
| 3791 | + not_applicable: { zh: "不适用", en: "Not applicable" }, |
| 3792 | + }, |
| 3793 | + confidence: { |
| 3794 | + high: { zh: "高", en: "High" }, |
| 3795 | + medium: { zh: "中", en: "Medium" }, |
| 3796 | + low: { zh: "低", en: "Low" }, |
| 3797 | + mixed: { zh: "混合", en: "Mixed" }, |
| 3798 | + no_event: { zh: "无事件", en: "No event" }, |
| 3799 | + unknown: { zh: "未知", en: "Unknown" }, |
| 3800 | + }, |
| 3801 | + style: { |
| 3802 | + event_driven: { zh: "事件驱动", en: "Event driven" }, |
| 3803 | + long_horizon_growth: { zh: "长期成长", en: "Long-horizon growth" }, |
| 3804 | + value_quality: { zh: "价值质量", en: "Value quality" }, |
| 3805 | + macro_context: { zh: "宏观背景", en: "Macro context" }, |
| 3806 | + mixed_research: { zh: "综合研究", en: "Mixed research" }, |
| 3807 | + }, |
| 3808 | + conflict: { |
| 3809 | + none: { zh: "无", en: "None" }, |
| 3810 | + conflict: { zh: "存在", en: "Present" }, |
| 3811 | + unavailable: { zh: "不可用", en: "Unavailable" }, |
| 3812 | + }, |
| 3813 | + drift: { |
| 3814 | + none: { zh: "无", en: "None" }, |
| 3815 | + drift: { zh: "存在", en: "Present" }, |
| 3816 | + unavailable: { zh: "不可用", en: "Unavailable" }, |
| 3817 | + }, |
| 3818 | + }; |
| 3819 | + |
| 3820 | + function m0ResearchLabel(group, value) { |
| 3821 | + const entry = m0ResearchLabels[group]?.[value]; |
| 3822 | + return entry ? (state.lang === "zh" ? entry.zh : entry.en) : "—"; |
| 3823 | + } |
| 3824 | + |
| 3825 | + function m0ResearchTimestamp(value) { |
| 3826 | + if (typeof value !== "string" || !value) return "—"; |
| 3827 | + const parsed = new Date(value); |
| 3828 | + return Number.isNaN(parsed.getTime()) ? "—" : parsed.toLocaleString(); |
| 3829 | + } |
| 3830 | + |
| 3831 | + function m0ResearchHorizons(values) { |
| 3832 | + if (!Array.isArray(values) || !values.length) return "—"; |
| 3833 | + const labels = values.map((value) => m0ResearchLabel("horizon", value)).filter((value) => value !== "—"); |
| 3834 | + return labels.length ? labels.join(" / ") : "—"; |
| 3835 | + } |
| 3836 | + |
| 3837 | + function m0ResearchEntries(subjects) { |
| 3838 | + if (!Array.isArray(subjects)) return []; |
| 3839 | + const entries = []; |
| 3840 | + for (const item of subjects) { |
| 3841 | + if (!item || typeof item !== "object" || !item.subject || typeof item.subject !== "object") continue; |
| 3842 | + if (!Array.isArray(item.observations)) continue; |
| 3843 | + for (const observation of item.observations) { |
| 3844 | + if (!observation || typeof observation !== "object" || !observation.research_context) continue; |
| 3845 | + entries.push({ |
| 3846 | + subject: item.subject, |
| 3847 | + observation, |
| 3848 | + horizonConflict: item.horizon_conflict, |
| 3849 | + historicalStaleHorizonDrift: item.historical_stale_horizon_drift, |
| 3850 | + }); |
| 3851 | + } |
| 3852 | + } |
| 3853 | + return entries; |
| 3854 | + } |
| 3855 | + |
| 3856 | + function normalizeM0ResearchPayload(payload) { |
| 3857 | + if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid M0 research payload"); |
| 3858 | + const subjects = Array.isArray(payload.subjects) ? payload.subjects : []; |
| 3859 | + return { |
| 3860 | + data_status: ["ready", "stale", "unavailable"].includes(payload.data_status) ? payload.data_status : "unavailable", |
| 3861 | + viewed_at: typeof payload.viewed_at === "string" ? payload.viewed_at : null, |
| 3862 | + summary: payload.summary && typeof payload.summary === "object" ? payload.summary : {}, |
| 3863 | + subjects: subjects.filter((item) => item && typeof item === "object" |
| 3864 | + && item.subject && typeof item.subject === "object" && Array.isArray(item.observations)), |
| 3865 | + policy: payload.policy && typeof payload.policy === "object" ? payload.policy : {}, |
| 3866 | + errors: Array.isArray(payload.errors) ? payload.errors : [], |
| 3867 | + }; |
| 3868 | + } |
| 3869 | + |
| 3870 | + function renderM0Research() { |
| 3871 | + const payload = state.m0Research.payload; |
| 3872 | + const entries = m0ResearchEntries(payload.subjects); |
| 3873 | + const shownEntries = entries.slice(0, M0_RESEARCH_DISPLAY_LIMIT); |
| 3874 | + const notice = el("m0-research-notice"); |
| 3875 | + if (!state.auth.allowed) { |
| 3876 | + notice.textContent = t("m0ResearchLoginNotice"); |
| 3877 | + } else if (payload.data_status === "stale") { |
| 3878 | + notice.textContent = t("m0ResearchStaleNotice"); |
| 3879 | + } else if (payload.data_status !== "ready") { |
| 3880 | + notice.textContent = t("m0ResearchUnavailableNotice"); |
| 3881 | + } else if (payload.errors?.length) { |
| 3882 | + notice.textContent = t("m0ResearchUpstreamNotice").replace("{count}", String(payload.errors.length)); |
| 3883 | + } else { |
| 3884 | + const more = entries.length > M0_RESEARCH_DISPLAY_LIMIT |
| 3885 | + ? ` ${t("m0ResearchMore").replace("{count}", String(M0_RESEARCH_DISPLAY_LIMIT))}` |
| 3886 | + : ""; |
| 3887 | + notice.textContent = `${t("m0ResearchNoOrder")}${more}`; |
| 3888 | + } |
| 3889 | + |
| 3890 | + const list = el("m0-research-list"); |
| 3891 | + list.replaceChildren(); |
| 3892 | + if (!shownEntries.length) { |
| 3893 | + const empty = document.createElement("div"); |
| 3894 | + empty.className = "health-card__empty"; |
| 3895 | + empty.textContent = t("m0ResearchEmpty"); |
| 3896 | + list.appendChild(empty); |
| 3897 | + return; |
| 3898 | + } |
| 3899 | + for (const entry of shownEntries) { |
| 3900 | + const subject = entry.subject || {}; |
| 3901 | + const observation = entry.observation || {}; |
| 3902 | + const context = observation.research_context || {}; |
| 3903 | + const card = document.createElement("article"); |
| 3904 | + card.className = "health-card"; |
| 3905 | + const main = document.createElement("div"); |
| 3906 | + main.className = "health-card__main"; |
| 3907 | + const meta = document.createElement("div"); |
| 3908 | + meta.className = "health-card__meta"; |
| 3909 | + meta.textContent = t("m0ResearchMeta") |
| 3910 | + .replace("{kind}", m0ResearchLabel("subject", subject.kind)) |
| 3911 | + .replace("{viewed}", m0ResearchTimestamp(payload.viewed_at)); |
| 3912 | + const title = document.createElement("h4"); |
| 3913 | + title.className = "health-card__title"; |
| 3914 | + title.textContent = typeof subject.identifier === "string" ? subject.identifier : "—"; |
| 3915 | + const stateFreshness = document.createElement("p"); |
| 3916 | + stateFreshness.className = "health-card__reason"; |
| 3917 | + stateFreshness.textContent = t("m0ResearchStateFreshness") |
| 3918 | + .replace("{state}", m0ResearchLabel("state", context.state)) |
| 3919 | + .replace("{freshness}", m0ResearchLabel("freshness", observation.freshness?.status)); |
| 3920 | + const horizons = document.createElement("div"); |
| 3921 | + horizons.className = "health-card__meta"; |
| 3922 | + horizons.textContent = t("m0ResearchHorizons") |
| 3923 | + .replace("{primary}", m0ResearchLabel("horizon", context.primary_horizon)) |
| 3924 | + .replace("{suitable}", m0ResearchHorizons(context.suitable_horizons)); |
| 3925 | + const evidence = document.createElement("div"); |
| 3926 | + evidence.className = "health-card__meta"; |
| 3927 | + evidence.textContent = t("m0ResearchEvidence") |
| 3928 | + .replace("{confidence}", m0ResearchLabel("confidence", context.source_confidence)) |
| 3929 | + .replace("{style}", m0ResearchLabel("style", context.source_style)) |
| 3930 | + .replace("{digest}", `${shortResearchDigest(observation.source_report_digest)} / ${shortResearchDigest(observation.source_entry_digest)}`); |
| 3931 | + const consistency = document.createElement("div"); |
| 3932 | + consistency.className = "health-card__meta"; |
| 3933 | + consistency.textContent = t("m0ResearchConsistency") |
| 3934 | + .replace("{conflict}", m0ResearchLabel("conflict", entry.horizonConflict?.status)) |
| 3935 | + .replace("{drift}", m0ResearchLabel("drift", entry.historicalStaleHorizonDrift?.status)); |
| 3936 | + main.append(meta, title, stateFreshness, horizons, evidence, consistency); |
| 3937 | + const status = document.createElement("div"); |
| 3938 | + status.className = "health-card__score"; |
| 3939 | + const label = document.createElement("small"); |
| 3940 | + label.textContent = "M0"; |
| 3941 | + const freshness = document.createElement("strong"); |
| 3942 | + freshness.textContent = m0ResearchLabel("freshness", observation.freshness?.status); |
| 3943 | + const stateLabel = document.createElement("small"); |
| 3944 | + stateLabel.textContent = m0ResearchLabel("state", context.state); |
| 3945 | + status.append(label, freshness, stateLabel); |
| 3946 | + card.append(main, status); |
| 3947 | + list.appendChild(card); |
| 3948 | + } |
| 3949 | + } |
| 3950 | + |
3723 | 3951 | function normalizeAdaptiveSelectionPayload(payload) { |
3724 | 3952 | if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid adaptive selection payload"); |
3725 | 3953 | const selections = Array.isArray(payload.selections) ? payload.selections : []; |
|
4074 | 4302 | renderRuntimeAuthorityStatus(); |
4075 | 4303 | renderConsoleView(); |
4076 | 4304 | renderControlPlane(); |
| 4305 | + renderM0Research(); |
4077 | 4306 | renderAdaptiveSelection(); |
4078 | 4307 | renderExecutionEvidence(); |
4079 | 4308 | renderResearchTasks(); |
|
4103 | 4332 | if (state.auth.allowed) { |
4104 | 4333 | await refreshControlPlane(); |
4105 | 4334 | await refreshOwnerDecisions(); |
| 4335 | + await refreshM0Research(); |
4106 | 4336 | await refreshAdaptiveSelection(); |
4107 | 4337 | await refreshExecutionEvidence(); |
4108 | 4338 | await refreshResearchTasks(); |
|
4173 | 4403 | renderControlPlane(); |
4174 | 4404 | } |
4175 | 4405 |
|
| 4406 | + async function refreshM0Research() { |
| 4407 | + if (!state.auth.allowed) { |
| 4408 | + renderM0Research(); |
| 4409 | + return; |
| 4410 | + } |
| 4411 | + try { |
| 4412 | + state.m0Research.payload = normalizeM0ResearchPayload(await requestJson("/api/m0-research")); |
| 4413 | + } catch { |
| 4414 | + state.m0Research.payload = { |
| 4415 | + data_status: "unavailable", |
| 4416 | + viewed_at: null, |
| 4417 | + summary: { |
| 4418 | + subject_count: 0, |
| 4419 | + observation_count: 0, |
| 4420 | + fresh_observation_count: 0, |
| 4421 | + stale_observation_count: 0, |
| 4422 | + unknown_observation_count: 0, |
| 4423 | + horizon_conflict_count: 0, |
| 4424 | + historical_stale_horizon_drift_count: 0, |
| 4425 | + }, |
| 4426 | + subjects: [], |
| 4427 | + policy: { authority: "research_only", no_order: true, permitted_next_step: "research_validation_only" }, |
| 4428 | + errors: ["m0_research_request_failed"], |
| 4429 | + }; |
| 4430 | + } |
| 4431 | + renderM0Research(); |
| 4432 | + } |
| 4433 | + |
4176 | 4434 | async function refreshAdaptiveSelection() { |
4177 | 4435 | if (!state.auth.allowed) { |
4178 | 4436 | renderAdaptiveSelection(); |
|
4457 | 4715 | if (state.view === "control") { |
4458 | 4716 | refreshControlPlane(); |
4459 | 4717 | refreshOwnerDecisions(); |
| 4718 | + refreshM0Research(); |
4460 | 4719 | refreshResearchTasks(); |
4461 | 4720 | } |
4462 | 4721 | if (state.view === "health") refreshHealth(); |
|
0 commit comments