Skip to content

Commit bd145de

Browse files
Pigbibicodex
andcommitted
feat: show read-only M0 research observations
Co-Authored-By: Codex <noreply@openai.com>
1 parent c422975 commit bd145de

6 files changed

Lines changed: 279 additions & 2 deletions

File tree

tests/strategy_switch_worker_validation.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ assert.ok(indexHtml.includes('id="runtime-authority-status"'));
6262
assert.ok(indexHtml.includes('id="control-plane-view-button"'));
6363
assert.ok(indexHtml.includes('id="control-plane-view"'));
6464
assert.ok(indexHtml.includes('id="control-plane-list"'));
65+
assert.ok(indexHtml.includes('id="m0-research-notice"'));
66+
assert.ok(indexHtml.includes('id="m0-research-list"'));
67+
assert.ok(indexHtml.includes('<details class="control-disclosure">\n <summary data-i18n="m0ResearchBoard">'));
68+
assert.ok(indexHtml.includes('function renderM0Research()'));
69+
assert.ok(indexHtml.includes('requestJson("/api/m0-research")'));
70+
assert.ok(indexHtml.includes('const M0_RESEARCH_DISPLAY_LIMIT = 100;'));
71+
assert.ok(indexHtml.includes('entries.slice(0, M0_RESEARCH_DISPLAY_LIMIT)'));
6572
assert.ok(indexHtml.includes('id="adaptive-selection-list"'));
6673
assert.ok(indexHtml.includes('id="adaptive-selection-notice"'));
6774
assert.ok(indexHtml.includes('data-i18n="planEyebrow"'));

web/strategy-switch-console/README.zh-CN.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,12 @@ validator。来源已标为 stale 的观测不会因读取而被提升为 fresh
147147
`permitted_next_step=research_validation_only`;将 M0 线索转为 P1--P3 研究任务仍需独立的
148148
证据绑定与准入流程。
149149

150+
管理台会在“今日待处理 / 只读”页将 M0 放在默认折叠的“研究观察”区域;它只请求
151+
`GET /api/m0-research`。区域最多显示 100 条观察,并仅展示研究对象、状态、新鲜度、观察期、
152+
来源置信/风格、当前周期分歧、历史失效漂移、来源摘要和本次查看时间。接口不可用、来源缺失或
153+
数据过期时保持安全空态或历史提示,不补造结论。该区域没有写入、切换或运行控制,也不会显示
154+
买卖、权重、策略、平台或账户信息;固定边界仍是“仅研究、无订单”。
155+
150156
## 文件结构
151157

152158
```text

web/strategy-switch-console/app.js

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,19 @@
860860
executionEvidenceDetail: "策略:{strategy} · 数据:{data} · 执行:{execution} · Shadow:{shadow} · Paper:{paper}",
861861
executionEvidenceNoOrder: "固定边界:只读证据;不包含账户、订单、资金或 P6 实盘授权。",
862862
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: "固定边界:仅研究、无订单;不产生任何运行动作。",
863876
adaptiveSelectionBoard: "按需查看 M1 Shadow 建议",
864877
adaptiveSelectionLoginNotice: "登录后读取私有 Shadow 建议;缺失来源不会被补成策略或订单。",
865878
adaptiveSelectionStaleNotice: "此 Shadow 建议已超出新鲜度窗口,仅保留为历史上下文。",
@@ -1129,6 +1142,19 @@
11291142
executionEvidenceDetail: "strategy: {strategy} · data: {data} · execution: {execution} · shadow: {shadow} · paper: {paper}",
11301143
executionEvidenceNoOrder: "Fixed boundary: read-only evidence; no account, order, funds, or P6 live authority.",
11311144
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.",
11321158
adaptiveSelectionBoard: "View M1 Shadow suggestions on demand",
11331159
adaptiveSelectionLoginNotice: "Sign in to read private Shadow suggestions. Missing sources never imply a strategy or order.",
11341160
adaptiveSelectionStaleNotice: "This Shadow suggestion is beyond its freshness window and remains historical context only.",
@@ -1409,6 +1435,24 @@
14091435
candidates: [],
14101436
errors: [],
14111437
},
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+
},
14121456
adaptiveSelection: {
14131457
payload: {
14141458
data_status: "unavailable",
@@ -3720,6 +3764,190 @@
37203764
}
37213765
}
37223766

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+
37233951
function normalizeAdaptiveSelectionPayload(payload) {
37243952
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid adaptive selection payload");
37253953
const selections = Array.isArray(payload.selections) ? payload.selections : [];
@@ -4074,6 +4302,7 @@
40744302
renderRuntimeAuthorityStatus();
40754303
renderConsoleView();
40764304
renderControlPlane();
4305+
renderM0Research();
40774306
renderAdaptiveSelection();
40784307
renderExecutionEvidence();
40794308
renderResearchTasks();
@@ -4103,6 +4332,7 @@
41034332
if (state.auth.allowed) {
41044333
await refreshControlPlane();
41054334
await refreshOwnerDecisions();
4335+
await refreshM0Research();
41064336
await refreshAdaptiveSelection();
41074337
await refreshExecutionEvidence();
41084338
await refreshResearchTasks();
@@ -4173,6 +4403,34 @@
41734403
renderControlPlane();
41744404
}
41754405

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+
41764434
async function refreshAdaptiveSelection() {
41774435
if (!state.auth.allowed) {
41784436
renderAdaptiveSelection();
@@ -4457,6 +4715,7 @@
44574715
if (state.view === "control") {
44584716
refreshControlPlane();
44594717
refreshOwnerDecisions();
4718+
refreshM0Research();
44604719
refreshResearchTasks();
44614720
}
44624721
if (state.view === "health") refreshHealth();

web/strategy-switch-console/app_js.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/strategy-switch-console/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ <h2 id="control-plane-view-title" data-i18n="controlPlaneTitle">只处理需要
7878
</div>
7979
<div class="health-board-head"><h3 data-i18n="controlCandidateBoard">需要跟进</h3></div>
8080
<div class="health-list" id="control-plane-list"></div>
81+
<details class="control-disclosure">
82+
<summary data-i18n="m0ResearchBoard">按需查看 M0 研究观察</summary>
83+
<div id="m0-research-notice" class="health-notice" role="status" aria-live="polite">登录后读取已验证 M0 研究台账;仅研究、无订单。</div>
84+
<div class="health-list" id="m0-research-list"></div>
85+
</details>
8186
<details class="control-disclosure">
8287
<summary data-i18n="adaptiveSelectionBoard">按需查看 M1 Shadow 建议</summary>
8388
<div id="adaptive-selection-notice" class="health-notice" role="status" aria-live="polite">登录后读取已验证的 Shadow 建议;它始终不下单。</div>

web/strategy-switch-console/page_asset.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)