oh-my-knowledge 0.38.0 → 0.40.0
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/dist/assets/agent-skills/omk/references/commands.md +2 -0
- package/dist/authoring/evolver.js +9 -5
- package/dist/cli/commands/list.d.ts +4 -1
- package/dist/cli/commands/list.js +12 -4
- package/dist/cli/commands/observe/index.d.ts +9 -0
- package/dist/cli/commands/observe/index.js +74 -1
- package/dist/cli/commands/sample.d.ts +13 -0
- package/dist/cli/commands/sample.js +113 -16
- package/dist/cli/lib/cmd-flags.d.ts +1 -0
- package/dist/cli/lib/i18n-dict/common.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/common.js +8 -0
- package/dist/cli/lib/i18n-dict/gen.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/gen.js +8 -0
- package/dist/cli/lib/i18n-dict/list.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/list.js +4 -0
- package/dist/cli/lib/record-evolve-outcome.js +19 -10
- package/dist/eval-core/bootstrap.d.ts +53 -2
- package/dist/eval-core/bootstrap.js +82 -5
- package/dist/eval-core/evaluation-reporting.js +37 -15
- package/dist/eval-core/execution-strategy.js +1 -0
- package/dist/eval-core/schema.js +5 -3
- package/dist/eval-core/verdict.d.ts +30 -0
- package/dist/eval-core/verdict.js +71 -21
- package/dist/grading/assertions.js +28 -0
- package/dist/grading/debias-validate.js +6 -2
- package/dist/grading/index.js +6 -1
- package/dist/grading/layered-scores.d.ts +15 -0
- package/dist/grading/layered-scores.js +64 -38
- package/dist/inputs/load-samples.d.ts +5 -0
- package/dist/inputs/load-samples.js +4 -2
- package/dist/inputs/skill-loader.d.ts +8 -0
- package/dist/inputs/skill-loader.js +23 -0
- package/dist/managed/evidence.d.ts +2 -1
- package/dist/managed/evidence.js +15 -2
- package/dist/managed/index.d.ts +2 -0
- package/dist/managed/index.js +2 -0
- package/dist/managed/list-view.d.ts +15 -1
- package/dist/managed/list-view.js +9 -1
- package/dist/managed/observe-feedback.d.ts +60 -0
- package/dist/managed/observe-feedback.js +49 -0
- package/dist/managed/store.d.ts +38 -1
- package/dist/managed/store.js +116 -3
- package/dist/managed/version-scores.d.ts +33 -0
- package/dist/managed/version-scores.js +70 -0
- package/dist/observability/skill-health-analyzer.d.ts +5 -0
- package/dist/observability/skill-health-analyzer.js +3 -2
- package/dist/renderer/managed-history-renderer.d.ts +2 -2
- package/dist/renderer/managed-history-renderer.js +189 -7
- package/dist/renderer/summary.js +23 -22
- package/dist/server/report-server.js +11 -2
- package/dist/types/eval.d.ts +2 -0
- package/dist/types/judge.d.ts +8 -0
- package/dist/types/managed.d.ts +40 -0
- package/dist/types/report.d.ts +10 -0
- package/package.json +3 -3
package/dist/managed/store.js
CHANGED
|
@@ -35,6 +35,16 @@ function isStringField(v) {
|
|
|
35
35
|
function isOptionalString(v) {
|
|
36
36
|
return v === undefined || typeof v === 'string';
|
|
37
37
|
}
|
|
38
|
+
/** git commit SHA 形态:7–64 位 hex。写入(evidence.ts)与读取校验共用同一判定,避免写读不对称
|
|
39
|
+
* (写时不校验、读时却要求 SHA → 自己写进去的值重载时被自己判脏)。 */
|
|
40
|
+
export function isShaLike(v) {
|
|
41
|
+
return typeof v === 'string' && /^[0-9a-f]{7,64}$/i.test(v);
|
|
42
|
+
}
|
|
43
|
+
/** 可选 git SHA 字段守卫(evidence.gitCommit):缺省或合法 SHA 形态。脏值会让 Studio 渲染
|
|
44
|
+
* `ev.gitCommit.slice()` 抛 TypeError(整页打不开),故读时按 SHA 形态收窄(脏值只剥字段,见下)。 */
|
|
45
|
+
function isOptionalSha(v) {
|
|
46
|
+
return v === undefined || isShaLike(v);
|
|
47
|
+
}
|
|
38
48
|
// 受管记录可安装的 kind(managed 记录绝不是 baseline)。
|
|
39
49
|
const MANAGED_KINDS = new Set(['skill', 'prompt', 'agent', 'workflow']);
|
|
40
50
|
// 校验到**运行时实际收窄**的边界,不止「字段是 string」:记录文件是用户可手改、且可能随仓库分发(被
|
|
@@ -90,6 +100,11 @@ function isManagedArtifactRecord(value) {
|
|
|
90
100
|
&& !(Array.isArray(c.debiasMode) && c.debiasMode.every((m) => m === 'length' || m === 'position')))
|
|
91
101
|
return false;
|
|
92
102
|
}
|
|
103
|
+
// gitCommit 是纯展示的还原指针:脏值(非 SHA 形态)只剥掉该字段,不像测量关键字段那样判脏丢整条记录
|
|
104
|
+
// —— 一处装饰字段的 typo 不该让整条记录连同 evidence / decision 历史从 list / Studio 消失。剥后剩余
|
|
105
|
+
// 字段仍是合法 evidence,Studio 渲染 `ev.gitCommit.slice()` 也不再触雷(字段已不存在)。
|
|
106
|
+
if (!isOptionalSha(ev.gitCommit))
|
|
107
|
+
delete ev.gitCommit;
|
|
93
108
|
return true;
|
|
94
109
|
});
|
|
95
110
|
const okDec = r.decisions.every((d) => {
|
|
@@ -115,7 +130,28 @@ function isManagedArtifactRecord(value) {
|
|
|
115
130
|
}
|
|
116
131
|
return true;
|
|
117
132
|
});
|
|
118
|
-
|
|
133
|
+
// observations(#235)缺省合法(旧记录无);present 必须是数组,每条窄到声明类型 —— healthBand / confidence
|
|
134
|
+
// 驱动读时 marker(deriveProductionGap),畸形脏值会让 list / Studio 误判,故判脏丢整条记录(同 okEv / okDec)。
|
|
135
|
+
const okObs = r.observations === undefined || (Array.isArray(r.observations) && r.observations.every((o) => {
|
|
136
|
+
if (!o || typeof o !== 'object')
|
|
137
|
+
return false;
|
|
138
|
+
const obs = o;
|
|
139
|
+
if (obs.observationKind !== 'production-health')
|
|
140
|
+
return false;
|
|
141
|
+
if (!isStringField(obs.reportId) || !isStringField(obs.observedAt))
|
|
142
|
+
return false;
|
|
143
|
+
if (typeof obs.gapRate !== 'number' || typeof obs.weightedGapRate !== 'number' || typeof obs.segmentCount !== 'number')
|
|
144
|
+
return false;
|
|
145
|
+
if (obs.confidence !== 'high' && obs.confidence !== 'low' && obs.confidence !== 'underpowered')
|
|
146
|
+
return false;
|
|
147
|
+
if (obs.healthBand !== 'green' && obs.healthBand !== 'yellow' && obs.healthBand !== 'red')
|
|
148
|
+
return false;
|
|
149
|
+
const g = obs.gapByType;
|
|
150
|
+
if (!g || typeof g !== 'object')
|
|
151
|
+
return false;
|
|
152
|
+
return ['failed_search', 'explicit_marker', 'hedging', 'repeated_failure'].every((k) => typeof g[k] === 'number');
|
|
153
|
+
}));
|
|
154
|
+
return okDist && okEv && okDec && okObs;
|
|
119
155
|
}
|
|
120
156
|
export function loadManagedRecord(dir, id) {
|
|
121
157
|
const path = recordPath(dir, id);
|
|
@@ -192,7 +228,7 @@ export function mergeManagedRecord(prev, next) {
|
|
|
192
228
|
byPath.set(normKey(t.path), t);
|
|
193
229
|
for (const t of next.distribution)
|
|
194
230
|
byPath.set(normKey(t.path), t);
|
|
195
|
-
// 维护点:`...next`
|
|
231
|
+
// 维护点:`...next` 取本次安装的事实,下面这些字段从 prev 保留(历史 / 首次时间)。
|
|
196
232
|
// 将来若新增任何"必须从安装基线保留"的字段,务必加进这份覆盖清单,否则会被 next 静默覆盖成 undefined。
|
|
197
233
|
// 注意语义边界:distribution 是**多源容忍**的(按归一化 path 累积去重),而 source / contentHash 是
|
|
198
234
|
// **末次写入(单源)**。同一 (kind,name) 先 file 后 git 重装会得到一条 source=git 的记录,但 distribution
|
|
@@ -203,6 +239,7 @@ export function mergeManagedRecord(prev, next) {
|
|
|
203
239
|
distribution: [...byPath.values()],
|
|
204
240
|
evidence: prev.evidence,
|
|
205
241
|
decisions: prev.decisions,
|
|
242
|
+
...(prev.observations ? { observations: prev.observations } : {}),
|
|
206
243
|
};
|
|
207
244
|
}
|
|
208
245
|
/** 读旧记录 → 合并 → 原子 tmp+rename 只写该 id 的文件。返回合并后记录。 */
|
|
@@ -239,6 +276,29 @@ export function appendManagedEvidence(dir, recordId, evidence) {
|
|
|
239
276
|
}
|
|
240
277
|
return merged;
|
|
241
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* 追加一条 observe 生产健康观测(append-only,#235)。同 evidence 不走 upsert:`mergeManagedRecord` 保留旧
|
|
281
|
+
* observations、丢弃 next 的(install 只写事实)。按 `reportId` 去重——observe 无 contentHash,一份 observe 报告
|
|
282
|
+
* 对一条观测;同报告重跑不堆重复,不同报告(新窗口)是新 reportId → 新条目。记录不存在(未 install / 名字不
|
|
283
|
+
* 匹配)返回 null —— 与 evidence 同口径:observe 绝不为未纳管 skill 凭空建记录(管理是 install 显式 opt-in)。
|
|
284
|
+
*/
|
|
285
|
+
export function appendManagedObservation(dir, recordId, observation) {
|
|
286
|
+
const prev = loadManagedRecord(dir, recordId);
|
|
287
|
+
if (!prev)
|
|
288
|
+
return null;
|
|
289
|
+
const dup = (prev.observations ?? []).some((o) => o.reportId === observation.reportId);
|
|
290
|
+
const merged = dup
|
|
291
|
+
? prev
|
|
292
|
+
: { ...prev, observations: [...(prev.observations ?? []), observation] };
|
|
293
|
+
if (!dup) {
|
|
294
|
+
mkdirSync(dir, { recursive: true });
|
|
295
|
+
const path = recordPath(dir, recordId);
|
|
296
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
297
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2));
|
|
298
|
+
renameSync(tmp, path);
|
|
299
|
+
}
|
|
300
|
+
return merged;
|
|
301
|
+
}
|
|
242
302
|
/**
|
|
243
303
|
* 把受管记录的 drift 基线(contentHash)重锚到新源内容哈,保留 evidence / decisions / source /
|
|
244
304
|
* distribution 全不动。用于 `omk evolve` 把胜出版本写回 source 后,让记录跟上实际内容——否则记录的
|
|
@@ -273,6 +333,12 @@ export function rebaselineManagedContentHash(dir, recordId, newHash) {
|
|
|
273
333
|
* (数组追加序 = 事件序)。
|
|
274
334
|
*/
|
|
275
335
|
export function isCurrentlyPromoted(record) {
|
|
336
|
+
return latestPromoteRollbackForCurrent(record)?.decisionKind === 'promote';
|
|
337
|
+
}
|
|
338
|
+
/** 当前内容(contentHash === record.contentHash)最近一条 promote/rollback 决定;无则 undefined。latest-wins
|
|
339
|
+
* 口径见上(decidedAt 真实时刻、不可解析退字典序、并列取后出现的)。isCurrentlyPromoted 与
|
|
340
|
+
* currentPromoteOverride 共用,避免两处各写一遍 latest-wins 走样。 */
|
|
341
|
+
function latestPromoteRollbackForCurrent(record) {
|
|
276
342
|
const ms = (s) => { const n = Date.parse(s); return Number.isNaN(n) ? null : n; };
|
|
277
343
|
let latest;
|
|
278
344
|
for (const d of record.decisions) {
|
|
@@ -290,7 +356,17 @@ export function isCurrentlyPromoted(record) {
|
|
|
290
356
|
if (newer)
|
|
291
357
|
latest = d;
|
|
292
358
|
}
|
|
293
|
-
return latest
|
|
359
|
+
return latest;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* 当前 promoted 版本是否经 override(--force)采用 —— 返回该 override(verdict + 被绕过的门),否则 undefined。
|
|
363
|
+
* 仅当前内容最近一条决定是 promote 且带 override 才有值;rollback 之后(已撤销接受)返回 undefined。供 list /
|
|
364
|
+
* Studio **读时审计**:从总览一眼看出哪些当前采用是越门来的。override 的**写**仍只在 CLI(`promote --force`),
|
|
365
|
+
* Studio 不执行——见 evidence-gated-management.md §9(#238)。
|
|
366
|
+
*/
|
|
367
|
+
export function currentPromoteOverride(record) {
|
|
368
|
+
const d = latestPromoteRollbackForCurrent(record);
|
|
369
|
+
return d?.decisionKind === 'promote' ? d.override : undefined;
|
|
294
370
|
}
|
|
295
371
|
/**
|
|
296
372
|
* 追加一条人工管理决定(append-only,promote/reject/rollback 走此路)。与 evidence 同样**不能走 upsert**
|
|
@@ -371,3 +447,40 @@ export function deriveManagedState(input) {
|
|
|
371
447
|
return { label: 'measurable', drifted: false, hasEvidence };
|
|
372
448
|
return { label: 'installed', drifted: false, hasEvidence };
|
|
373
449
|
}
|
|
450
|
+
/** 一条观测是否构成「确诊生产盲区」:healthBand 红 **且** 统计够力。underpowered(数据不足)是 §6.1 的
|
|
451
|
+
* unknown —— 既不放行也不拦截,不当确诊盲区;yellow / green 也不算(留信息展示,避免低样本噪声里过度报警)。 */
|
|
452
|
+
export function isProductionGapObservation(obs) {
|
|
453
|
+
return obs.healthBand === 'red' && obs.confidence !== 'underpowered';
|
|
454
|
+
}
|
|
455
|
+
const obsMs = (s) => { const n = Date.parse(s); return Number.isNaN(n) ? null : n; };
|
|
456
|
+
/** 谁更新(按 observedAt = 流量窗口结束时刻):真实时刻可解析则比时刻,否则退字典序;并列取后出现的。 */
|
|
457
|
+
function obsNewerWins(cur, latest) {
|
|
458
|
+
const tc = obsMs(cur);
|
|
459
|
+
const tl = obsMs(latest);
|
|
460
|
+
return tc !== null && tl !== null ? tc >= tl : cur >= latest;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* observe 生产健康观测的**读时 marker**(#235)。取**最新一条**观测(latest-wins,按 observedAt = 被观测
|
|
464
|
+
* 流量窗口结束时刻)分类:red + 够力 → `gap`;underpowered → `underpowered`(数据不足);其余 → `none`。
|
|
465
|
+
*
|
|
466
|
+
* **版本无关的生产信号**:observe 量的是线上**部署版**的行为,而记录里没有可靠的「源码版 ↔ 部署版」时间锚
|
|
467
|
+
* (`evolve` 改写源、`rebaselineManagedContentHash` 只换 `contentHash`,不碰 distribution/installedAt;部署副本
|
|
468
|
+
* 仍是旧的)——所以这里**不按源码版归因、也不臆造版本闸门**(那只会给假精度、还会在源码 bump 后压掉仍然有效的
|
|
469
|
+
* 真盲区)。marker 反映的是部署版的近况,可能滞后于当前源码版,UI / spec 据此老实标注。
|
|
470
|
+
*
|
|
471
|
+
* **绝不翻 stale / 不动生命周期枚举**(§6.1:只有内容漂移翻 stale,observe 是信号源不是受控 eval)。
|
|
472
|
+
*/
|
|
473
|
+
export function deriveProductionGap(record) {
|
|
474
|
+
const obs = record.observations ?? [];
|
|
475
|
+
if (obs.length === 0)
|
|
476
|
+
return { marker: 'none' };
|
|
477
|
+
let latest = obs[0];
|
|
478
|
+
for (const o of obs)
|
|
479
|
+
if (obsNewerWins(o.observedAt, latest.observedAt))
|
|
480
|
+
latest = o;
|
|
481
|
+
if (isProductionGapObservation(latest))
|
|
482
|
+
return { marker: 'gap', latest };
|
|
483
|
+
if (latest.confidence === 'underpowered')
|
|
484
|
+
return { marker: 'underpowered', latest };
|
|
485
|
+
return { marker: 'none', latest };
|
|
486
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本回归曲线的数据点构造(#236 Studio 决策史)。纯函数:report 由调用方按 evidence.reportId 读好传进来
|
|
3
|
+
* (Map,值可为 null = 报告已清 / 读不到),把受管记录的逐版证据拼成按时间从旧到新的 composite + 95%CI 序列。
|
|
4
|
+
*
|
|
5
|
+
* 测量味:每版的可比性签名(评委 prompt hash + debias + 样本集 hash)与「当前内容那版」一致才算 comparable;
|
|
6
|
+
* 换过评委 / 改过样本集的版本标 false —— 渲染据此把跨不可比的段画虚、点画空心,不糊一条误导的「在变好」线
|
|
7
|
+
* (spec evidence-gated-management.md §3「可比性必须可见」)。
|
|
8
|
+
*/
|
|
9
|
+
import type { ManagedArtifactRecord } from '../types/index.js';
|
|
10
|
+
/** buildVersionScores 只需要报告里的这几样 —— 用结构化最小入参而非整个 ReportDocument,叶子可测、与 report schema 解耦。 */
|
|
11
|
+
export interface ReportScoreView {
|
|
12
|
+
meta?: {
|
|
13
|
+
artifactHashes?: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
summary?: Record<string, {
|
|
16
|
+
avgCompositeScore?: number;
|
|
17
|
+
bootstrapCI?: {
|
|
18
|
+
low: number;
|
|
19
|
+
high: number;
|
|
20
|
+
};
|
|
21
|
+
} | undefined>;
|
|
22
|
+
}
|
|
23
|
+
export interface VersionScorePoint {
|
|
24
|
+
contentHash: string;
|
|
25
|
+
recordedAt: string;
|
|
26
|
+
composite: number;
|
|
27
|
+
ciLow: number;
|
|
28
|
+
ciHigh: number;
|
|
29
|
+
verdict?: string;
|
|
30
|
+
/** 与基线(当前内容那版)测量条件一致 → 可比;换过评委 / 改过样本集 → false。 */
|
|
31
|
+
comparable: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function buildVersionScores(record: ManagedArtifactRecord, reportsById: Map<string, ReportScoreView | null>): VersionScorePoint[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const ms = (s) => { const n = Date.parse(s); return Number.isNaN(n) ? null : n; };
|
|
2
|
+
/** 旧→新排序;omk 自写恒 UTC `Z`、字典序即时间序,但记录可手改,两端可解析才用真实时刻、否则退字典序。 */
|
|
3
|
+
function olderFirst(a, b) {
|
|
4
|
+
const pa = ms(a);
|
|
5
|
+
const pb = ms(b);
|
|
6
|
+
if (pa !== null && pb !== null)
|
|
7
|
+
return pa - pb;
|
|
8
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* 一条证据的可比性签名:评委模板 + debias + 样本集,三者全一致才认为两次测量同口径、可直接比分。
|
|
12
|
+
* 缺评委指纹或样本集指纹 → 无从核对是否同口径,返回 null(= unknown);spec §6.1 明确 unknown 既不隐式
|
|
13
|
+
* 放行也不隐式拦截,故调用处把 null 一律按不可比渲染(空心点 + 虚线),不把「不知道能不能比」糊成实线趋势。
|
|
14
|
+
* debias 缺失退空串参与字符串比对(空 debias 是合法状态,非 unknown),只跟同样缺的版本算一致。
|
|
15
|
+
*/
|
|
16
|
+
function comparabilitySig(ev) {
|
|
17
|
+
const judge = ev.comparability?.judgePromptHash;
|
|
18
|
+
const samples = ev.sampleCoverage?.hash;
|
|
19
|
+
if (!judge || !samples)
|
|
20
|
+
return null;
|
|
21
|
+
const debias = (ev.comparability?.debiasMode ?? []).slice().sort().join(',');
|
|
22
|
+
return `${judge}|${debias}|${samples}`;
|
|
23
|
+
}
|
|
24
|
+
export function buildVersionScores(record, reportsById) {
|
|
25
|
+
// 一版(contentHash)可能多条证据(重测)→ 按 contentHash 收敛,留 recordedAt 最新且能画出点的那条。
|
|
26
|
+
const byHash = new Map();
|
|
27
|
+
for (const ev of record.evidence) {
|
|
28
|
+
const report = reportsById.get(ev.reportId);
|
|
29
|
+
if (!report)
|
|
30
|
+
continue; // 报告已清 / 读不到 → 该点跳过,不臆造
|
|
31
|
+
const hashes = report.meta?.artifactHashes;
|
|
32
|
+
if (!hashes)
|
|
33
|
+
continue;
|
|
34
|
+
const variant = Object.keys(hashes).find((v) => hashes[v] === ev.contentHash);
|
|
35
|
+
if (!variant)
|
|
36
|
+
continue; // 匹配不到变体(旧 schema 不绑 / blind 未带哈)→ 跳过
|
|
37
|
+
const s = report.summary?.[variant];
|
|
38
|
+
const composite = s?.avgCompositeScore;
|
|
39
|
+
if (typeof composite !== 'number' || Number.isNaN(composite))
|
|
40
|
+
continue; // 无 composite → 无从画点
|
|
41
|
+
const ci = s?.bootstrapCI;
|
|
42
|
+
const ciLow = typeof ci?.low === 'number' ? ci.low : composite;
|
|
43
|
+
const ciHigh = typeof ci?.high === 'number' ? ci.high : composite;
|
|
44
|
+
const prev = byHash.get(ev.contentHash);
|
|
45
|
+
if (!prev || olderFirst(prev.ev.recordedAt, ev.recordedAt) < 0) {
|
|
46
|
+
byHash.set(ev.contentHash, { ev, composite, ciLow, ciHigh });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const entries = [...byHash.values()].sort((a, b) => olderFirst(a.ev.recordedAt, b.ev.recordedAt));
|
|
50
|
+
if (entries.length === 0)
|
|
51
|
+
return [];
|
|
52
|
+
// 基线 = 当前内容那版(没有就用最新一条),其余版本与之比对可比性。
|
|
53
|
+
const baselineEntry = byHash.get(record.contentHash) ?? entries[entries.length - 1];
|
|
54
|
+
const baselineSig = comparabilitySig(baselineEntry.ev);
|
|
55
|
+
return entries.map((en) => {
|
|
56
|
+
const sig = comparabilitySig(en.ev);
|
|
57
|
+
// 自身与基线都能核对(签名非 null)且签名一致才算可比;任一缺评委 / 样本指纹(null = unknown)→ 不可比。
|
|
58
|
+
// 基线本身 unknown → baselineSig 为 null → 全版本(含基线自身)不可比,宁可全空心也不把 unknown 糊成可比。
|
|
59
|
+
const comparable = sig !== null && baselineSig !== null && sig === baselineSig;
|
|
60
|
+
return {
|
|
61
|
+
contentHash: en.ev.contentHash,
|
|
62
|
+
recordedAt: en.ev.recordedAt,
|
|
63
|
+
composite: en.composite,
|
|
64
|
+
ciLow: en.ciLow,
|
|
65
|
+
ciHigh: en.ciHigh,
|
|
66
|
+
...(en.ev.verdict ? { verdict: en.ev.verdict } : {}),
|
|
67
|
+
comparable,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -84,6 +84,11 @@ export interface AnalyzeOptions {
|
|
|
84
84
|
to?: string;
|
|
85
85
|
skills?: string[];
|
|
86
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* 健康度色带。阈值对齐 bench ci --max-gap-rate 经验值(spec §五)。observe 报告只在 overall 给 band;
|
|
89
|
+
* #235 受管反哺时 observe CLI 用本函数逐 skill 算 band 传入 managed —— 阈值单一来源,managed 不复制。
|
|
90
|
+
*/
|
|
91
|
+
export declare function healthBandOf(weightedGapRate: number): 'green' | 'yellow' | 'red';
|
|
87
92
|
/** 统计可信度护栏:按 segment 数判 high / low / underpowered,守护「1 段就判 red」。
|
|
88
93
|
* 导出供 renderer / CLI / server 给历史报告(缺 confidence 字段)做 segmentCount 兜底。 */
|
|
89
94
|
export declare function confidenceOf(segmentCount: number): SkillHealth['confidence'];
|
|
@@ -31,9 +31,10 @@ function withinTimeWindow(seg, from, to) {
|
|
|
31
31
|
return true;
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* 健康度色带。阈值对齐 bench ci --max-gap-rate 经验值(spec §五)。observe 报告只在 overall 给 band;
|
|
35
|
+
* #235 受管反哺时 observe CLI 用本函数逐 skill 算 band 传入 managed —— 阈值单一来源,managed 不复制。
|
|
35
36
|
*/
|
|
36
|
-
function healthBandOf(weightedGapRate) {
|
|
37
|
+
export function healthBandOf(weightedGapRate) {
|
|
37
38
|
if (weightedGapRate >= 0.3)
|
|
38
39
|
return 'red';
|
|
39
40
|
if (weightedGapRate >= 0.1)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Lang } from '../types/index.js';
|
|
2
2
|
import type { ManagedArtifactRecord } from '../types/index.js';
|
|
3
|
-
import type { ManagedListRow } from '../managed/index.js';
|
|
4
|
-
export declare function renderManagedHistory(record: ManagedArtifactRecord, lang: Lang): string;
|
|
3
|
+
import type { ManagedListRow, VersionScorePoint } from '../managed/index.js';
|
|
4
|
+
export declare function renderManagedHistory(record: ManagedArtifactRecord, lang: Lang, versionScores?: VersionScorePoint[]): string;
|
|
5
5
|
export declare function renderManagedList(rows: ManagedListRow[], lang: Lang): string;
|
|
@@ -18,6 +18,26 @@ const VERDICT_LEVELS = {
|
|
|
18
18
|
const isVerdictLevel = (v) => Object.prototype.hasOwnProperty.call(VERDICT_LEVELS, v);
|
|
19
19
|
const shortHash = (h) => h.slice(0, 12);
|
|
20
20
|
const L = (lang) => (zh, en) => (lang === 'zh' ? zh : en);
|
|
21
|
+
// 盲区信号类型 → 人话标签。与 CLI observe 的 GAP_AREA_LABELS 同义但本地另存一份 —— renderer 绝不 import
|
|
22
|
+
// cli/commands(支柱分层),且两边走各自的 i18n 机制。键集随 ManagedObservation.gapByType 固定四类同步。
|
|
23
|
+
const GAP_AREA_LABELS = {
|
|
24
|
+
failed_search: { zh: '检索失败', en: 'failed search' },
|
|
25
|
+
explicit_marker: { zh: '显式缺口', en: 'explicit gap' },
|
|
26
|
+
hedging: { zh: '含糊回避', en: 'hedging' },
|
|
27
|
+
repeated_failure: { zh: '反复失败', en: 'repeated failure' },
|
|
28
|
+
};
|
|
29
|
+
/** 盲区计数最高的前几类组成人话区域串(只展示信号所在,不生成用例)。全 0 → 空串(调用方据此略过)。
|
|
30
|
+
* 只迭代**四个已知盲区类型**(GAP_AREA_LABELS 的键),记录里若被手改塞进额外键则一律忽略 —— 消费侧收口,
|
|
31
|
+
* 比让 validator 因一个额外键丢整条记录更稳;额外键即便混入也进不了展示(再叠 e() 转义,无注入面)。 */
|
|
32
|
+
function topGapAreas(gapByType, lang) {
|
|
33
|
+
return Object.keys(GAP_AREA_LABELS)
|
|
34
|
+
.map((k) => [k, gapByType[k] ?? 0])
|
|
35
|
+
.filter(([, n]) => n > 0)
|
|
36
|
+
.sort((a, b) => b[1] - a[1])
|
|
37
|
+
.slice(0, 3)
|
|
38
|
+
.map(([k]) => GAP_AREA_LABELS[k][lang])
|
|
39
|
+
.join(lang === 'zh' ? '、' : ', ');
|
|
40
|
+
}
|
|
21
41
|
/** 页内跳转统一带上 lang,否则点一下就掉回默认中文。zh 是默认 → 空串(不脏 URL)。 */
|
|
22
42
|
const langQuery = (lang) => (lang === 'en' ? '?lang=en' : '');
|
|
23
43
|
/** ISO 时刻解析(两端可解析按真实时刻、否则退字典序),与 latestCurrentEvidence 同口径。 */
|
|
@@ -47,6 +67,7 @@ function buildTimeline(record) {
|
|
|
47
67
|
events.push({
|
|
48
68
|
at: ev.recordedAt, type: 'eval', contentHash: ev.contentHash, verdict: ev.verdict,
|
|
49
69
|
reportId: ev.reportId, sampleCount: ev.sampleCoverage?.count, cliVersion: ev.comparability?.cliVersion,
|
|
70
|
+
...(ev.gitCommit ? { gitCommit: ev.gitCommit } : {}),
|
|
50
71
|
});
|
|
51
72
|
}
|
|
52
73
|
for (const d of record.decisions) {
|
|
@@ -55,6 +76,14 @@ function buildTimeline(record) {
|
|
|
55
76
|
reportId: d.reportId, actor: d.actor, reason: d.reason, override: d.override,
|
|
56
77
|
});
|
|
57
78
|
}
|
|
79
|
+
// observe 生产健康观测(#235):每条一个事件。**不带 contentHash** —— observe 是版本无关的生产信号
|
|
80
|
+
// (量线上部署版),像 install 事件那样只作时间点,绝不重置版本分段(见 renderManagedHistory 分段循环)。
|
|
81
|
+
for (const obs of record.observations ?? []) {
|
|
82
|
+
events.push({
|
|
83
|
+
at: obs.observedAt, type: 'observe',
|
|
84
|
+
healthBand: obs.healthBand, confidence: obs.confidence, gapByType: obs.gapByType,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
58
87
|
return events.sort((a, b) => cmpDesc(a.at, b.at));
|
|
59
88
|
}
|
|
60
89
|
function reportLink(reportId, lang) {
|
|
@@ -62,7 +91,27 @@ function reportLink(reportId, lang) {
|
|
|
62
91
|
return '';
|
|
63
92
|
return `<a class="mh-link" href="/reports/${encodeURIComponent(reportId)}${langQuery(lang)}">${L(lang)('查看报告', 'report')} →</a>`;
|
|
64
93
|
}
|
|
65
|
-
|
|
94
|
+
/** 本地 git 源的仓内路径(`git:<ref>:<spec>` → spec),给 `git checkout <sha> -- <path>` 提示带上 `-- <path>`。
|
|
95
|
+
* 远端 / file 源 → undefined(本地 git 才有 cwd checkout 语义)。
|
|
96
|
+
* file-skill 的实际仓内文件是 `<spec>.md`(install 裸名 spec 经 classifyGitSkillRef 解到 `<spec>.md`),
|
|
97
|
+
* 故 file-skill 且 spec 未带 `.md` 时补上 —— 否则 `git checkout … -- review` 匹配不到 `review.md`。
|
|
98
|
+
* 已知局限:locator 的 spec 是**安装时 cwd 相对**的(不含 gitRelDir 前缀),在仓库子目录里 install 的 skill,
|
|
99
|
+
* 还原路径会缺该子目录前缀。彻底修需在记录上另存仓库根相对路径(与 drift 重解析的 cwd 相对语义解耦),留 follow-up。 */
|
|
100
|
+
function gitRestorePath(source) {
|
|
101
|
+
if (source.sourceKind !== 'git' || source.url)
|
|
102
|
+
return undefined;
|
|
103
|
+
const m = /^git:[^:]*:(.+)$/.exec(source.locator);
|
|
104
|
+
if (!m)
|
|
105
|
+
return undefined;
|
|
106
|
+
const spec = m[1];
|
|
107
|
+
return (!source.isDirectorySkill && !/\.md$/i.test(spec)) ? `${spec}.md` : spec;
|
|
108
|
+
}
|
|
109
|
+
/** POSIX 单引号包裹,内部 `'` 按 `'\''` 标准转义。还原提示是给用户复制粘贴的 shell 命令,git 路径可含
|
|
110
|
+
* 空格 / 分号 / 反引号 / `$()` 等元字符 —— e()(HTML escaping)挡不住 shell,不 quote 会被改写命令语义。 */
|
|
111
|
+
function shellQuote(s) {
|
|
112
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
113
|
+
}
|
|
114
|
+
function eventRow(ev, lang, restorePath) {
|
|
66
115
|
const t = L(lang);
|
|
67
116
|
const typeLabel = {
|
|
68
117
|
install: t('安装纳管', 'Installed'),
|
|
@@ -70,10 +119,28 @@ function eventRow(ev, lang) {
|
|
|
70
119
|
promote: t('采用', 'Promote'),
|
|
71
120
|
reject: t('否决', 'Reject'),
|
|
72
121
|
rollback: t('回滚', 'Rollback'),
|
|
122
|
+
observe: t('观测', 'Observe'),
|
|
73
123
|
};
|
|
74
124
|
const head = [`<span class="mh-type">${e(typeLabel[ev.type])}</span>`];
|
|
75
125
|
if (ev.type === 'eval')
|
|
76
126
|
head.push(verdictBadge(ev.verdict, lang));
|
|
127
|
+
// observe 观测徽标,四分支(green / yellow 必须分开 —— yellow 不是健康):红 + 够力 → 「生产盲区」(确诊);
|
|
128
|
+
// underpowered → 「数据不足」(unknown,任意 band 都先归此,与 deriveProductionGap 同口径);yellow + 够力 →
|
|
129
|
+
// 「需关注」(盲区已进注意区间、未达 red 告警,不能说成健康);green + 够力 → 「健康」。
|
|
130
|
+
if (ev.type === 'observe') {
|
|
131
|
+
if (ev.healthBand === 'red' && ev.confidence !== 'underpowered') {
|
|
132
|
+
head.push(`<span class="mh-gap">🔬 ${t('生产盲区', 'production gap')}</span>`);
|
|
133
|
+
}
|
|
134
|
+
else if (ev.confidence === 'underpowered') {
|
|
135
|
+
head.push(`<span class="mh-badge-raw">${t('数据不足', 'underpowered')}</span>`);
|
|
136
|
+
}
|
|
137
|
+
else if (ev.healthBand === 'yellow') {
|
|
138
|
+
head.push(`<span class="mh-watch">${t('需关注', 'elevated')}</span>`);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
head.push(`<span class="mh-badge-raw">${t('健康', 'healthy')}</span>`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
77
144
|
if (ev.override) {
|
|
78
145
|
const blocks = ev.override.overriddenBlocks?.length ? `:${e(ev.override.overriddenBlocks.join(' / '))}` : '';
|
|
79
146
|
head.push(`<span class="mh-override">${t('越门 override', 'override')}${blocks}</span>`);
|
|
@@ -84,15 +151,40 @@ function eventRow(ev, lang) {
|
|
|
84
151
|
if (ev.type === 'eval' && typeof ev.sampleCount === 'number') {
|
|
85
152
|
detail.push(`<span class="mh-detail-item">${ev.sampleCount} ${t('用例', 'samples')}</span>`);
|
|
86
153
|
}
|
|
154
|
+
// observe 事件:列盲区集中的信号区域 + 建议补样本(只提示、不生成用例,不改样本集)。全 0 不显区域行。
|
|
155
|
+
if (ev.type === 'observe' && ev.gapByType) {
|
|
156
|
+
const areas = topGapAreas(ev.gapByType, lang);
|
|
157
|
+
if (areas)
|
|
158
|
+
detail.push(`<span class="mh-detail-item">${t('盲区集中在:', 'gaps in: ')}${e(areas)}</span>`);
|
|
159
|
+
if (ev.healthBand === 'red' && ev.confidence !== 'underpowered') {
|
|
160
|
+
detail.push(`<span class="mh-detail-item mh-gap-hint">${t('建议补对应用例后重跑 omk eval', 'add matching samples, then re-run omk eval')}</span>`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
87
163
|
if (ev.cliVersion)
|
|
88
164
|
detail.push(`<span class="mh-detail-item">omk ${e(ev.cliVersion)}</span>`);
|
|
89
165
|
if (ev.reportId)
|
|
90
166
|
detail.push(reportLink(ev.reportId, lang));
|
|
91
167
|
if (ev.reason)
|
|
92
168
|
detail.push(`<span class="mh-reason">「${e(ev.reason)}」</span>`);
|
|
169
|
+
// #234/#236 还原指针:这一版有 git 坐标 + 能解析出仓内路径 → 给现成的 `git checkout <sha> -- <path>` 把
|
|
170
|
+
// 该路径还原进工作树(字节级还原交给 git)。必须带 `-- <path>`:不带 pathspec 的 `git checkout <sha>` 是切
|
|
171
|
+
// detached HEAD、整棵工作树被换,语义完全不同且危险 —— 故解析不出路径时干脆不显,不退化成那条命令。显
|
|
172
|
+
// full SHA(精确坐标、无歧义);路径 shell-quote 防注入(见 shellQuote)。
|
|
173
|
+
// 注:对目录-skill,checkout 还原该版的跟踪文件,但不会删除其后新增的文件 —— git pathspec 的固有语义。
|
|
174
|
+
if (ev.gitCommit && restorePath) {
|
|
175
|
+
const cmd = `git checkout ${ev.gitCommit} -- ${shellQuote(restorePath)}`;
|
|
176
|
+
detail.push(`<span class="mh-detail-item mh-restore">${t('还原', 'restore')} <code>${e(cmd)}</code></span>`);
|
|
177
|
+
}
|
|
178
|
+
// 圆点配色:多数事件按 type 取色;observe 事件按**健康 band** 取色(盲区红 / 数据不足灰 / 注意黄 / 健康绿)
|
|
179
|
+
// —— observe 是 type 内有强弱的事件,健康或注意观测画错色会撒谎,故按实际健康度上色,与上面徽标四分支同口径。
|
|
180
|
+
const dotClass = ev.type === 'observe'
|
|
181
|
+
? (ev.healthBand === 'red' && ev.confidence !== 'underpowered' ? 'mh-dot--red'
|
|
182
|
+
: ev.confidence === 'underpowered' ? 'mh-dot--muted'
|
|
183
|
+
: ev.healthBand === 'yellow' ? 'mh-dot--yellow' : 'mh-dot--green')
|
|
184
|
+
: `mh-dot--${ev.type}`;
|
|
93
185
|
return `<li class="mh-event mh-event--${ev.type}">
|
|
94
186
|
<span class="mh-time">${e(fmtLocalTime(ev.at))}</span>
|
|
95
|
-
<span class="mh-marker"><span class="mh-dot
|
|
187
|
+
<span class="mh-marker"><span class="mh-dot ${dotClass}"></span></span>
|
|
96
188
|
<div class="mh-body">
|
|
97
189
|
<div class="mh-head">${head.join(' ')}</div>
|
|
98
190
|
${detail.length ? `<div class="mh-detail">${detail.join('')}</div>` : ''}
|
|
@@ -103,10 +195,66 @@ function versionHeader(hash, isCurrent, lang) {
|
|
|
103
195
|
const cur = isCurrent ? `<span class="mh-vcur">${L(lang)('当前', 'current')}</span>` : '';
|
|
104
196
|
return `<li class="mh-vhead"><span class="mh-vhead-label">${L(lang)('版本', 'version')}</span><code class="mh-hash">${e(shortHash(hash))}</code>${cur}</li>`;
|
|
105
197
|
}
|
|
106
|
-
|
|
198
|
+
/** 版本回归曲线(纯 SVG,确定性、可 snapshot):每版 composite 均值 + 95%CI 竖须,按时间从旧到新。
|
|
199
|
+
* 不可比的版本(换过评委 / 改过样本集,或缺指纹无法核对)点画空心、连线画虚 —— 不糊一条误导的「在变好」线(spec §3)。
|
|
200
|
+
* 数据由 buildVersionScores 算好;少于 2 个点不画(单点无趋势可言)。composite 量纲不定,y 轴按数据自适应。 */
|
|
201
|
+
function renderVersionCurve(points, lang) {
|
|
202
|
+
if (points.length < 2)
|
|
203
|
+
return '';
|
|
204
|
+
const t = L(lang);
|
|
205
|
+
const width = 620, height = 250, padL = 46, padR = 16, padT = 16, padB = 46;
|
|
206
|
+
const plotW = width - padL - padR;
|
|
207
|
+
const plotH = height - padT - padB;
|
|
208
|
+
const n = points.length;
|
|
209
|
+
let yLo = Math.min(...points.map((p) => p.ciLow));
|
|
210
|
+
let yHi = Math.max(...points.map((p) => p.ciHigh));
|
|
211
|
+
if (yHi - yLo < 1e-9) {
|
|
212
|
+
yLo -= 0.5;
|
|
213
|
+
yHi += 0.5;
|
|
214
|
+
} // 全相等退化:给个最小跨度,免除零。
|
|
215
|
+
const pad = (yHi - yLo) * 0.08;
|
|
216
|
+
yLo -= pad;
|
|
217
|
+
yHi += pad;
|
|
218
|
+
const xAt = (i) => padL + (plotW * i) / (n - 1);
|
|
219
|
+
const yAt = (v) => padT + plotH - (plotH * (v - yLo)) / (yHi - yLo);
|
|
220
|
+
const parts = [];
|
|
221
|
+
// 连线逐段;任一端不可比 → 虚线,提示别把跨不可比的差值读作进步 / 回退。
|
|
222
|
+
for (let i = 1; i < n; i++) {
|
|
223
|
+
const a = points[i - 1], b = points[i];
|
|
224
|
+
const dash = (!a.comparable || !b.comparable) ? ' stroke-dasharray="4 3"' : '';
|
|
225
|
+
parts.push(`<line x1="${xAt(i - 1).toFixed(1)}" y1="${yAt(a.composite).toFixed(1)}" x2="${xAt(i).toFixed(1)}" y2="${yAt(b.composite).toFixed(1)}" stroke="var(--accent)" stroke-width="2"${dash} />`);
|
|
226
|
+
}
|
|
227
|
+
// 每点:CI 竖须 + 上下端帽 + 点(可比实心 / 不可比空心)+ 短 hash 轴标。
|
|
228
|
+
points.forEach((p, i) => {
|
|
229
|
+
const x = xAt(i), yL = yAt(p.ciLow), yH = yAt(p.ciHigh), yM = yAt(p.composite);
|
|
230
|
+
parts.push(`<line x1="${x.toFixed(1)}" y1="${yH.toFixed(1)}" x2="${x.toFixed(1)}" y2="${yL.toFixed(1)}" stroke="var(--accent)" stroke-width="1" stroke-opacity="0.5" />`);
|
|
231
|
+
parts.push(`<line x1="${(x - 3).toFixed(1)}" y1="${yH.toFixed(1)}" x2="${(x + 3).toFixed(1)}" y2="${yH.toFixed(1)}" stroke="var(--accent)" stroke-width="1" stroke-opacity="0.5" />`);
|
|
232
|
+
parts.push(`<line x1="${(x - 3).toFixed(1)}" y1="${yL.toFixed(1)}" x2="${(x + 3).toFixed(1)}" y2="${yL.toFixed(1)}" stroke="var(--accent)" stroke-width="1" stroke-opacity="0.5" />`);
|
|
233
|
+
parts.push(p.comparable
|
|
234
|
+
? `<circle cx="${x.toFixed(1)}" cy="${yM.toFixed(1)}" r="4" fill="var(--accent)" />`
|
|
235
|
+
: `<circle cx="${x.toFixed(1)}" cy="${yM.toFixed(1)}" r="4" fill="var(--bg-surface)" stroke="var(--accent)" stroke-width="1.5" />`);
|
|
236
|
+
parts.push(`<text x="${x.toFixed(1)}" y="${height - padB + 14}" font-size="9" text-anchor="middle" fill="var(--text-muted)">${e(p.contentHash.slice(0, 7))}</text>`);
|
|
237
|
+
});
|
|
238
|
+
const yTicks = [0, 0.5, 1].map((f) => {
|
|
239
|
+
const v = yLo + (yHi - yLo) * f;
|
|
240
|
+
const y = yAt(v);
|
|
241
|
+
return `<line x1="${padL}" y1="${y.toFixed(1)}" x2="${width - padR}" y2="${y.toFixed(1)}" stroke="var(--border)" stroke-width="0.5" /><text x="${padL - 6}" y="${(y + 3).toFixed(1)}" font-size="10" text-anchor="end" fill="var(--text-muted)">${v.toFixed(2)}</text>`;
|
|
242
|
+
}).join('');
|
|
243
|
+
const anyIncomparable = points.some((p) => !p.comparable);
|
|
244
|
+
const note = anyIncomparable
|
|
245
|
+
? t('空心点 = 测量条件不同或无法核对(换过评委 / 改过样本集 / 缺指纹),与当前版不可比;虚线段别直接读作进步或回退。', 'Hollow dots = measured under a different or unverifiable instrument (judge / sample set changed, or fingerprint missing) than the current version, not comparable; do not read dashed segments as progress or regression.')
|
|
246
|
+
: t('每个版本的 composite 均值与 95% 置信区间,按时间从旧到新。', 'Composite mean and 95% CI per version, oldest to newest.');
|
|
247
|
+
return `<section class="mh-curve">
|
|
248
|
+
<h2 class="mh-curve-title">${t('版本回归曲线', 'Version regression')}</h2>
|
|
249
|
+
<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" class="mh-curve-svg">${yTicks}${parts.join('')}</svg>
|
|
250
|
+
<p class="mh-curve-note">${note}</p>
|
|
251
|
+
</section>`;
|
|
252
|
+
}
|
|
253
|
+
export function renderManagedHistory(record, lang, versionScores = []) {
|
|
107
254
|
const t = L(lang);
|
|
108
255
|
const events = buildTimeline(record);
|
|
109
256
|
// 倒序遍历:内容版本(contentHash)变化处插版本段头;install 等无 hash 事件不重置分段。
|
|
257
|
+
const restorePath = gitRestorePath(record.source);
|
|
110
258
|
const rows = [];
|
|
111
259
|
let prevHash;
|
|
112
260
|
for (const ev of events) {
|
|
@@ -114,7 +262,7 @@ export function renderManagedHistory(record, lang) {
|
|
|
114
262
|
rows.push(versionHeader(ev.contentHash, ev.contentHash === record.contentHash, lang));
|
|
115
263
|
prevHash = ev.contentHash;
|
|
116
264
|
}
|
|
117
|
-
rows.push(eventRow(ev, lang));
|
|
265
|
+
rows.push(eventRow(ev, lang, restorePath));
|
|
118
266
|
}
|
|
119
267
|
const meta = [
|
|
120
268
|
record.kind,
|
|
@@ -129,6 +277,7 @@ export function renderManagedHistory(record, lang) {
|
|
|
129
277
|
<h1 class="mh-name">${e(record.name)}</h1>
|
|
130
278
|
<div class="mh-meta">${meta}</div>
|
|
131
279
|
</header>
|
|
280
|
+
${renderVersionCurve(versionScores, lang)}
|
|
132
281
|
<ol class="mh-timeline">${rows.join('')}</ol>
|
|
133
282
|
</main>
|
|
134
283
|
<style>${MANAGED_CSS}</style>`;
|
|
@@ -155,16 +304,37 @@ function stateMeta(state, lang) {
|
|
|
155
304
|
const [label, tip] = lang === 'zh' ? m.zh : m.en;
|
|
156
305
|
return { label, tip };
|
|
157
306
|
}
|
|
307
|
+
/** 列表只读审计标:当前采用版本是越门(`--force`)来的就标出来,tooltip 列被绕过的门。复用详情时间线的
|
|
308
|
+
* `.mh-override` 样式。override 的写仍只在 CLI —— Studio 只读(spec §9 / #238)。 */
|
|
309
|
+
function overrideBadge(row, lang) {
|
|
310
|
+
if (!row.override)
|
|
311
|
+
return '';
|
|
312
|
+
const t = L(lang);
|
|
313
|
+
const blocks = row.override.overriddenBlocks?.length ? row.override.overriddenBlocks.join(' / ') : '';
|
|
314
|
+
const tip = blocks ? t(`越门采用,绕过:${blocks}`, `force-promoted, waved: ${blocks}`) : t('越门采用', 'force-promoted');
|
|
315
|
+
return ` <span class="mh-override" title="${e(tip)}">${t('越门', 'override')}</span>`;
|
|
316
|
+
}
|
|
317
|
+
/** 生产盲区只读标(#235):该 skill 最新观测是确诊生产盲区(红 + 够力)就标出来,tooltip 列盲区区域。
|
|
318
|
+
* 与 state 正交(observe 量线上部署版、版本无关),只 surface、不参与生命周期 —— 见 deriveProductionGap。 */
|
|
319
|
+
function productionGapBadge(row, lang) {
|
|
320
|
+
if (!row.productionGap)
|
|
321
|
+
return '';
|
|
322
|
+
const t = L(lang);
|
|
323
|
+
const areas = topGapAreas(row.productionGap.gapByType, lang);
|
|
324
|
+
const tip = areas ? t(`线上生产盲区,集中在:${areas}`, `production gap in real traffic, in: ${areas}`) : t('线上检测到生产盲区', 'production gap detected in real traffic');
|
|
325
|
+
// 只放图标(state 列窄、固定 130px,带文字徽标会溢出压到 name 列);全称走 tooltip + legend + 时间线。
|
|
326
|
+
return ` <span class="mh-gap" title="${e(tip)}">🔬</span>`;
|
|
327
|
+
}
|
|
158
328
|
function listRow(row, lang) {
|
|
159
329
|
const t = L(lang);
|
|
160
330
|
const st = stateMeta(row.state, lang);
|
|
161
331
|
const mark = !row.reachable ? ' <span class="mh-mark mh-mark--q" title="' + e(t('源不可达 / 拒读,漂移未核', 'source unreachable / refused, drift unchecked')) + '">?</span>'
|
|
162
332
|
: row.state === 'stale' ? ' <span class="mh-mark mh-mark--warn" title="' + e(t('已漂移,需重测', 'drifted, re-measure')) + '">⚠️</span>' : '';
|
|
163
333
|
return `<a class="mh-row" href="/managed/${encodeURIComponent(row.id)}${langQuery(lang)}">
|
|
164
|
-
<span class="mh-row-state" title="${e(st.tip)}"><span class="mh-dot mh-dot--${stateBand(row.state)}"></span>${e(st.label)}${mark}</span>
|
|
334
|
+
<span class="mh-row-state" title="${e(st.tip)}"><span class="mh-dot mh-dot--${stateBand(row.state)}"></span>${e(st.label)}${mark}${productionGapBadge(row, lang)}</span>
|
|
165
335
|
<span class="mh-row-name">${e(row.name)}</span>
|
|
166
336
|
<span class="mh-row-kind">${e(row.kind)}</span>
|
|
167
|
-
<span class="mh-row-verdict">${row.latestVerdict ? verdictBadge(row.latestVerdict, lang) : '—'}</span>
|
|
337
|
+
<span class="mh-row-verdict">${row.latestVerdict ? verdictBadge(row.latestVerdict, lang) : '—'}${overrideBadge(row, lang)}</span>
|
|
168
338
|
<span class="mh-row-ev">${row.currentEvidenceCount}/${row.totalEvidenceCount}</span>
|
|
169
339
|
<span class="mh-row-src" title="${e(row.sourceLabel)}">${e(row.sourceLabel)}</span>
|
|
170
340
|
</a>`;
|
|
@@ -187,6 +357,8 @@ export function renderManagedList(rows, lang) {
|
|
|
187
357
|
<span class="mh-legend-item"><span class="mh-dot mh-dot--red"></span>${t('已漂移 ⚠️:源变了、需重跑 omk eval', 'Drifted ⚠️ = source changed, re-run omk eval')}</span>
|
|
188
358
|
<span class="mh-legend-item">${t('? 源未核(不可达 / 拒读,漂移待定)', '? = source unverified (unreachable / refused)')}</span>
|
|
189
359
|
<span class="mh-legend-item">${t('证据列:当前有效 / 全部历史(旧证据留作回滚)', 'Evidence = current / total (old evidence kept for rollback)')}</span>
|
|
360
|
+
<span class="mh-legend-item"><span class="mh-override">${t('越门', 'override')}</span>${t(' 当前采用是 --force 越门来的(决定人由命令行记录)', ' = current version force-promoted via --force (actor recorded by CLI)')}</span>
|
|
361
|
+
<span class="mh-legend-item"><span class="mh-gap">🔬</span>${t(' 生产盲区:observe 在线上检测到的盲区(版本无关信号、与生命周期正交,不翻 stale)', ' = production gap from observe (version-agnostic, orthogonal to lifecycle, never flips stale)')}</span>
|
|
190
362
|
</div>`;
|
|
191
363
|
const body = `<main class="mh-main">
|
|
192
364
|
<header class="mh-hero">
|
|
@@ -210,6 +382,12 @@ const MANAGED_CSS = `
|
|
|
210
382
|
.mh-meta{display:flex;flex-wrap:wrap;gap:6px 14px;margin-top:6px;color:var(--text-muted);font-size:12px}
|
|
211
383
|
.mh-meta span{display:inline-flex;align-items:center;gap:4px;font-variant-numeric:tabular-nums}
|
|
212
384
|
|
|
385
|
+
/* ── 版本回归曲线 ── */
|
|
386
|
+
.mh-curve{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);padding:16px 20px;margin-bottom:18px}
|
|
387
|
+
.mh-curve-title{margin:0 0 4px;font-size:15px;font-weight:700;color:var(--text-primary);letter-spacing:-.2px}
|
|
388
|
+
.mh-curve-svg{width:100%;max-width:620px;height:auto;display:block;margin:6px 0}
|
|
389
|
+
.mh-curve-note{font-size:12px;color:var(--text-muted);margin:6px 0 0}
|
|
390
|
+
|
|
213
391
|
/* ── 时间线 ── */
|
|
214
392
|
.mh-timeline{list-style:none;padding:0;margin:0;background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius-lg);box-shadow:var(--shadow-sm);overflow:hidden}
|
|
215
393
|
.mh-vhead{display:flex;align-items:center;gap:8px;padding:10px 20px;background:var(--bg-elevated);border-top:1px solid var(--border);font-size:12px;color:var(--text-secondary)}
|
|
@@ -225,13 +403,17 @@ const MANAGED_CSS = `
|
|
|
225
403
|
.mh-dot--promote{background:var(--green)}
|
|
226
404
|
.mh-dot--rollback{background:var(--yellow)}
|
|
227
405
|
.mh-dot--reject{background:var(--red)}
|
|
228
|
-
.mh-dot--green{background:var(--green)}.mh-dot--accent{background:var(--accent)}.mh-dot--muted{background:var(--text-muted)}.mh-dot--red{background:var(--red)}
|
|
406
|
+
.mh-dot--green{background:var(--green)}.mh-dot--accent{background:var(--accent)}.mh-dot--muted{background:var(--text-muted)}.mh-dot--red{background:var(--red)}.mh-dot--yellow{background:var(--yellow)}
|
|
229
407
|
.mh-body{flex:1;min-width:0}
|
|
230
408
|
.mh-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-size:14px}
|
|
231
409
|
.mh-type{font-weight:600;color:var(--text-primary)}
|
|
232
410
|
.mh-override{font-size:11px;font-weight:600;color:var(--yellow);background:var(--yellow-bg);padding:1px 8px;border-radius:9px}
|
|
411
|
+
.mh-gap{font-size:11px;font-weight:600;color:var(--red);background:var(--red-bg);padding:1px 8px;border-radius:9px;white-space:nowrap}
|
|
412
|
+
.mh-watch{font-size:11px;font-weight:600;color:var(--yellow);background:var(--yellow-bg);padding:1px 8px;border-radius:9px;white-space:nowrap}
|
|
413
|
+
.mh-gap-hint{color:var(--red)}
|
|
233
414
|
.mh-badge-raw{font-size:11.5px;color:var(--text-secondary);background:var(--bg-soft);padding:1px 8px;border-radius:9px}
|
|
234
415
|
.mh-detail{display:flex;flex-wrap:wrap;gap:4px 14px;margin-top:4px;font-size:12px;color:var(--text-muted)}
|
|
416
|
+
.mh-restore code{font-family:"SF Mono",Menlo,monospace;font-size:11.5px;color:var(--text-secondary);background:var(--bg-soft);padding:1px 6px;border-radius:5px;user-select:all}
|
|
235
417
|
.mh-reason{color:var(--text-secondary);font-style:italic}
|
|
236
418
|
.mh-link{color:var(--accent);text-decoration:none}
|
|
237
419
|
.mh-link:hover{text-decoration:underline}
|