oh-my-knowledge 0.35.0 → 0.36.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/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/assets/agent-skills/omk/SKILL.md +2 -2
- package/dist/assets/agent-skills/omk/references/commands.md +36 -0
- package/dist/authoring/evolver.js +1 -1
- package/dist/cli/commands/doctor.js +2 -1
- package/dist/cli/commands/eval/index.js +7 -6
- package/dist/cli/commands/list.d.ts +46 -0
- package/dist/cli/commands/list.js +243 -0
- package/dist/cli/lib/i18n-dict/list.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/list.js +32 -0
- package/dist/cli/lib/i18n-dict.d.ts +2 -1
- package/dist/cli/lib/i18n-dict.js +2 -0
- package/dist/cli/lib/run-tally.js +1 -1
- package/dist/cli/lib/shared.js +1 -1
- package/dist/doctor/index.js +1 -1
- package/dist/eval-core/evaluation-reporting.d.ts +1 -0
- package/dist/eval-core/evaluation-reporting.js +6 -4
- package/dist/eval-workflows/batch-evaluation-workflow.js +3 -2
- package/dist/eval-workflows/run-evaluation.js +2 -2
- package/dist/inputs/skill-loader.js +6 -3
- package/dist/managed/index.d.ts +1 -0
- package/dist/managed/index.js +1 -0
- package/dist/managed/list-view.d.ts +55 -0
- package/dist/managed/list-view.js +66 -0
- package/dist/managed/store.js +45 -8
- package/dist/observability/experience.d.ts +2 -0
- package/dist/observability/experience.js +31 -2
- package/dist/observability/inbox.js +31 -5
- package/dist/observability/review-state.js +22 -11
- package/dist/observability/soft-standards/llm-extractor.js +3 -3
- package/dist/observability/soft-standards/skill-standards-store.d.ts +1 -0
- package/dist/observability/soft-standards/skill-standards-store.js +77 -14
- package/dist/observability/soft-standards/types.d.ts +2 -2
- package/dist/renderer/html-renderer.js +3 -3
- package/dist/server/report-server.js +1 -1
- package/dist/server/report-store.d.ts +1 -1
- package/dist/server/report-store.js +16 -15
- package/dist/server/skill-index.js +5 -4
- package/dist/types/doctor.d.ts +2 -2
- package/dist/types/doctor.js +1 -1
- package/dist/types/observability.d.ts +6 -6
- package/dist/types/report.d.ts +9 -4
- package/package.json +1 -1
|
@@ -52,7 +52,10 @@ const GIT_PROBE_STDIO = ['ignore', 'pipe', 'ignore'];
|
|
|
52
52
|
// 由 resolveGitRepoContext 解出 repoRoot 后逐处显式传入。
|
|
53
53
|
export function gitShowFile(ref, filePath, cwd = process.cwd()) {
|
|
54
54
|
try {
|
|
55
|
-
|
|
55
|
+
// `--` 隔断 tree-ish:ref 可能来自盘上受管记录的 locator(用户可手改 / 随仓库分发,被 omk list 等只读
|
|
56
|
+
// 命令喂进来),前缀 `-` 的 ref 不得被当成 git 选项解析(与 #219 fetch 路径同口径,见
|
|
57
|
+
// feedback_git_subprocess_dashdash)。加 `--` 后 dash-ref 退化为「非法 object name」fail-closed,普通 ref 输出不变。
|
|
58
|
+
return execFileSync('git', ['cat-file', 'blob', '--', `${ref}:${filePath}`], { cwd, encoding: 'utf-8', stdio: GIT_PROBE_STDIO }).trim();
|
|
56
59
|
}
|
|
57
60
|
catch {
|
|
58
61
|
return null;
|
|
@@ -64,7 +67,7 @@ export function gitShowFile(ref, filePath, cwd = process.cwd()) {
|
|
|
64
67
|
*/
|
|
65
68
|
export function gitShowBytes(ref, filePath, cwd = process.cwd()) {
|
|
66
69
|
try {
|
|
67
|
-
return execFileSync('git', ['cat-file', 'blob', `${ref}:${filePath}`], { cwd, stdio: GIT_PROBE_STDIO }); // 无 encoding → Buffer
|
|
70
|
+
return execFileSync('git', ['cat-file', 'blob', '--', `${ref}:${filePath}`], { cwd, stdio: GIT_PROBE_STDIO }); // 无 encoding → Buffer;`--` 隔断同 gitShowFile
|
|
68
71
|
}
|
|
69
72
|
catch {
|
|
70
73
|
return null;
|
|
@@ -80,7 +83,7 @@ export function gitShowBytes(ref, filePath, cwd = process.cwd()) {
|
|
|
80
83
|
export function gitLsTreeBlobs(ref, treePath, cwd = process.cwd()) {
|
|
81
84
|
let out;
|
|
82
85
|
try {
|
|
83
|
-
out = execFileSync('git', ['ls-tree', '-r', '-z', '--full-tree', `${ref}:${treePath}`], { cwd, encoding: 'utf-8', stdio: GIT_PROBE_STDIO });
|
|
86
|
+
out = execFileSync('git', ['ls-tree', '-r', '-z', '--full-tree', '--', `${ref}:${treePath}`], { cwd, encoding: 'utf-8', stdio: GIT_PROBE_STDIO });
|
|
84
87
|
}
|
|
85
88
|
catch {
|
|
86
89
|
return [];
|
package/dist/managed/index.d.ts
CHANGED
package/dist/managed/index.js
CHANGED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `omk list` 的纯视图构造(#203 管理支柱)—— 把受管记录摊成可渲染的行,不做任何 IO。
|
|
3
|
+
*
|
|
4
|
+
* 「当前源探测」由调用方注入(`probeOf`):CLI 侧重物化真源算哈,单测侧给 mock —— 保持本模块纯函数、
|
|
5
|
+
* 可测。探测是**三态**:`reachable:true + hash`(算出了当前哈,可判 drift)/ `reachable:false`(源不可达 /
|
|
6
|
+
* 解析失败 / 拒绝读取)。**不可达 ≠ 已 drift** —— 把「这里查不了」当「内容变了」会对从别处看的本地 git 记录
|
|
7
|
+
* 误报 stale(locator 随 cwd 漂),所以不可达时只按证据给 installed/measurable、不打 drift、单独标「未核」。
|
|
8
|
+
* reachable 时才走 `deriveManagedState`(哈不等 → stale)。verdict / 可比性取**当前有效证据**
|
|
9
|
+
* (contentHash == record.contentHash)里 recordedAt 最新那条 —— 旧内容的证据不冒充当前。
|
|
10
|
+
*/
|
|
11
|
+
import type { ArtifactKind, ManagedArtifactRecord, ManagedLifecycleLabel } from '../types/index.js';
|
|
12
|
+
/** 当前源探测结果(三态)。`reachable:false` = 不可达 / 解析失败 / 拒读,**不等于**已 drift。 */
|
|
13
|
+
export interface SourceProbe {
|
|
14
|
+
reachable: boolean;
|
|
15
|
+
/** 当前源整树 / 单文件哈;仅 reachable 时有。 */
|
|
16
|
+
hash?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface ManagedListRow {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
/** artifact 类型,直取自记录(裸 kind 留给 ArtifactKind,见 terminology-spec §5.4)。 */
|
|
22
|
+
kind: ArtifactKind;
|
|
23
|
+
sourceKind: 'file' | 'git';
|
|
24
|
+
/** 展示用源标识:仅远端 git(sourceKind==='git' 且带 url)显 url,其余一律显 locator —— 保证「显示的」
|
|
25
|
+
* 就是「被 probe / 读取的」路径,file 源即便混入 url(validator 已拒)也不显示它。 */
|
|
26
|
+
sourceLabel: string;
|
|
27
|
+
state: ManagedLifecycleLabel;
|
|
28
|
+
drifted: boolean;
|
|
29
|
+
/** 当前源是否被成功核对过 hash。false = 不可达/拒读 → drift 未核(不据此判 stale)。 */
|
|
30
|
+
reachable: boolean;
|
|
31
|
+
/** 当前有效证据(若有)里 recordedAt 最新那条的 verdict。 */
|
|
32
|
+
latestVerdict?: string;
|
|
33
|
+
/** 该证据的可比性 marker —— 跨报告比 verdict 前需一致。 */
|
|
34
|
+
comparability?: {
|
|
35
|
+
cliVersion: string;
|
|
36
|
+
judgePromptHash?: string;
|
|
37
|
+
debiasMode?: Array<'length' | 'position'>;
|
|
38
|
+
};
|
|
39
|
+
/** 最新当前证据的记录时间。 */
|
|
40
|
+
recordedAt?: string;
|
|
41
|
+
/** 当前有效证据数 / 全部证据数(含旧内容的历史证据)。 */
|
|
42
|
+
currentEvidenceCount: number;
|
|
43
|
+
totalEvidenceCount: number;
|
|
44
|
+
distributionCount: number;
|
|
45
|
+
}
|
|
46
|
+
export declare function buildManagedListRow(record: ManagedArtifactRecord, probe: SourceProbe): ManagedListRow;
|
|
47
|
+
/**
|
|
48
|
+
* 组装全部行。`probeOf` 给每条记录探测当前源(三态,见文件头)。
|
|
49
|
+
* 按 name 排序(稳定、可读);name collation 相等再按 kind。排序对机读出口(--json)也生效,故:
|
|
50
|
+
* - **钉死 locale**(`'en'`):缺省 locale 随宿主 LANG / LC_COLLATE 漂,会让两台机器对同一批记录排出不同
|
|
51
|
+
* JSON 顺序,破坏 omk「确定、可比」的底色。
|
|
52
|
+
* - 平级判定用**同一 collation 度量**(localeCompare 结果是否为 0)而非 `===`:NFC / NFD 等价的同显示名
|
|
53
|
+
* 在 `===` 下不等、却 collation 相等,用 `===` 会漏掉 kind 平级 tiebreak、令顺序退回 readdir 序(不定)。
|
|
54
|
+
*/
|
|
55
|
+
export declare function buildManagedListRows(records: ManagedArtifactRecord[], probeOf: (record: ManagedArtifactRecord) => SourceProbe): ManagedListRow[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { deriveManagedState } from './store.js';
|
|
2
|
+
function latestCurrentEvidence(record) {
|
|
3
|
+
const current = record.evidence.filter((e) => e.contentHash === record.contentHash);
|
|
4
|
+
if (current.length === 0)
|
|
5
|
+
return undefined;
|
|
6
|
+
// 取 recordedAt 最新的一条。omk 自写恒 UTC `Z`、字典序即时间序;但记录可手改 / 随仓库分发,异偏移
|
|
7
|
+
// 或异精度的 ISO 串字典序会乱 → 优先按解析后的真实时刻比,两端都可解析才用;否则退回字典序(不劣化
|
|
8
|
+
// omk 自写场景)。并列取后出现的。
|
|
9
|
+
const ms = (s) => { const n = Date.parse(s); return Number.isNaN(n) ? null : n; };
|
|
10
|
+
return current.reduce((a, b) => {
|
|
11
|
+
const ta = ms(a.recordedAt);
|
|
12
|
+
const tb = ms(b.recordedAt);
|
|
13
|
+
if (ta !== null && tb !== null)
|
|
14
|
+
return tb >= ta ? b : a;
|
|
15
|
+
return b.recordedAt >= a.recordedAt ? b : a;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function buildManagedListRow(record, probe) {
|
|
19
|
+
const currentEvidenceCount = record.evidence.filter((e) => e.contentHash === record.contentHash).length;
|
|
20
|
+
let state;
|
|
21
|
+
let drifted;
|
|
22
|
+
if (probe.reachable) {
|
|
23
|
+
// 算出了当前哈:正常推导(哈不等 → stale/drift)。
|
|
24
|
+
const d = deriveManagedState({ record, currentContentHash: probe.hash });
|
|
25
|
+
state = d.label;
|
|
26
|
+
drifted = d.drifted;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// 不可达:不据此判 stale —— 只按证据给 installed/measurable,drift 标未核。
|
|
30
|
+
state = currentEvidenceCount > 0 ? 'measurable' : 'installed';
|
|
31
|
+
drifted = false;
|
|
32
|
+
}
|
|
33
|
+
const latest = latestCurrentEvidence(record);
|
|
34
|
+
return {
|
|
35
|
+
id: record.id,
|
|
36
|
+
name: record.name,
|
|
37
|
+
kind: record.kind,
|
|
38
|
+
sourceKind: record.source.sourceKind,
|
|
39
|
+
sourceLabel: record.source.sourceKind === 'git' && record.source.url ? record.source.url : record.source.locator,
|
|
40
|
+
state,
|
|
41
|
+
drifted,
|
|
42
|
+
reachable: probe.reachable,
|
|
43
|
+
...(latest?.verdict ? { latestVerdict: latest.verdict } : {}),
|
|
44
|
+
...(latest?.comparability ? { comparability: latest.comparability } : {}),
|
|
45
|
+
...(latest?.recordedAt ? { recordedAt: latest.recordedAt } : {}),
|
|
46
|
+
currentEvidenceCount,
|
|
47
|
+
totalEvidenceCount: record.evidence.length,
|
|
48
|
+
distributionCount: record.distribution.length,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 组装全部行。`probeOf` 给每条记录探测当前源(三态,见文件头)。
|
|
53
|
+
* 按 name 排序(稳定、可读);name collation 相等再按 kind。排序对机读出口(--json)也生效,故:
|
|
54
|
+
* - **钉死 locale**(`'en'`):缺省 locale 随宿主 LANG / LC_COLLATE 漂,会让两台机器对同一批记录排出不同
|
|
55
|
+
* JSON 顺序,破坏 omk「确定、可比」的底色。
|
|
56
|
+
* - 平级判定用**同一 collation 度量**(localeCompare 结果是否为 0)而非 `===`:NFC / NFD 等价的同显示名
|
|
57
|
+
* 在 `===` 下不等、却 collation 相等,用 `===` 会漏掉 kind 平级 tiebreak、令顺序退回 readdir 序(不定)。
|
|
58
|
+
*/
|
|
59
|
+
export function buildManagedListRows(records, probeOf) {
|
|
60
|
+
return records
|
|
61
|
+
.map((r) => buildManagedListRow(r, probeOf(r)))
|
|
62
|
+
.sort((a, b) => {
|
|
63
|
+
const byName = a.name.localeCompare(b.name, 'en');
|
|
64
|
+
return byName !== 0 ? byName : a.kind.localeCompare(b.kind, 'en');
|
|
65
|
+
});
|
|
66
|
+
}
|
package/dist/managed/store.js
CHANGED
|
@@ -31,9 +31,16 @@ export { hashArtifactSource, isDistributablePath, distributableCopyFilter } from
|
|
|
31
31
|
function isStringField(v) {
|
|
32
32
|
return typeof v === 'string';
|
|
33
33
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
/** 仅当字段缺省或是 string 才放行(可选 string 字段的脏值守卫,如 source.url / evidence.verdict)。 */
|
|
35
|
+
function isOptionalString(v) {
|
|
36
|
+
return v === undefined || typeof v === 'string';
|
|
37
|
+
}
|
|
38
|
+
// 受管记录可安装的 kind(managed 记录绝不是 baseline)。
|
|
39
|
+
const MANAGED_KINDS = new Set(['skill', 'prompt', 'agent', 'workflow']);
|
|
40
|
+
// 校验到**运行时实际收窄**的边界,不止「字段是 string」:记录文件是用户可手改、且可能随仓库分发(被
|
|
41
|
+
// loadAllManagedRecords 无 opt-in 读到)的不可信输入。若只查 string,畸形记录(如 source.url 是对象、
|
|
42
|
+
// sourceKind 是任意串)会绕过 validator,在下游(omk list 的 sourceLabel ?? → dispWidth(对象) 等)
|
|
43
|
+
// 抛 TypeError 让命令崩溃。这里把 source / evidence 收窄到 list 等消费方实际解引用的类型,判脏即丢弃该文件。
|
|
37
44
|
function isManagedArtifactRecord(value) {
|
|
38
45
|
if (!value || typeof value !== 'object')
|
|
39
46
|
return false;
|
|
@@ -42,19 +49,49 @@ function isManagedArtifactRecord(value) {
|
|
|
42
49
|
&& r.schemaVersion === 2
|
|
43
50
|
&& isStringField(r.id)
|
|
44
51
|
&& isStringField(r.name)
|
|
45
|
-
&& isStringField(r.kind)
|
|
52
|
+
&& isStringField(r.kind) && MANAGED_KINDS.has(r.kind)
|
|
46
53
|
&& isStringField(r.contentHash)
|
|
47
54
|
&& r.source && typeof r.source === 'object'
|
|
48
|
-
&& isStringField(r.source.locator)
|
|
49
|
-
&& isStringField(r.source.sourceKind)
|
|
50
55
|
&& Array.isArray(r.distribution)
|
|
51
56
|
&& Array.isArray(r.evidence)
|
|
52
57
|
&& Array.isArray(r.decisions)))
|
|
53
58
|
return false;
|
|
59
|
+
const src = r.source;
|
|
60
|
+
if (!(isStringField(src.locator)
|
|
61
|
+
&& (src.sourceKind === 'file' || src.sourceKind === 'git')
|
|
62
|
+
&& typeof src.isDirectorySkill === 'boolean'
|
|
63
|
+
&& isOptionalString(src.url)
|
|
64
|
+
&& isOptionalString(src.ref)))
|
|
65
|
+
return false;
|
|
66
|
+
// url / ref 是 git-only 字段。file 源带 url 时,list 的 sourceLabel 会显示假 url、掩盖真实被 probe /
|
|
67
|
+
// 读取的 locator(畸形但可通过上面 string 校验)→ 「读了什么」与「显示什么」不一致。判脏丢弃。
|
|
68
|
+
if (src.sourceKind === 'file' && (src.url !== undefined || src.ref !== undefined))
|
|
69
|
+
return false;
|
|
54
70
|
const okDist = r.distribution.every((d) => d && typeof d === 'object'
|
|
55
71
|
&& isStringField(d.path) && isStringField(d.contentHash));
|
|
56
|
-
const okEv = r.evidence.every((e) =>
|
|
57
|
-
|
|
72
|
+
const okEv = r.evidence.every((e) => {
|
|
73
|
+
if (!e || typeof e !== 'object')
|
|
74
|
+
return false;
|
|
75
|
+
const ev = e;
|
|
76
|
+
if (!(isStringField(ev.reportId) && isStringField(ev.contentHash) && isStringField(ev.recordedAt)))
|
|
77
|
+
return false;
|
|
78
|
+
if (!isOptionalString(ev.verdict))
|
|
79
|
+
return false;
|
|
80
|
+
// comparability 若存在:必须是带 string cliVersion 的对象(list / promote 会读它)。可选 marker
|
|
81
|
+
// judgePromptHash / debiasMode 同样收窄到声明类型 —— 否则任意类型脏值会穿过 validator 原样进 `omk list
|
|
82
|
+
// --json`(及未来 promote gate)消费方。
|
|
83
|
+
if (ev.comparability !== undefined) {
|
|
84
|
+
const c = ev.comparability;
|
|
85
|
+
if (!c || typeof c !== 'object' || !isStringField(c.cliVersion))
|
|
86
|
+
return false;
|
|
87
|
+
if (!isOptionalString(c.judgePromptHash))
|
|
88
|
+
return false;
|
|
89
|
+
if (c.debiasMode !== undefined
|
|
90
|
+
&& !(Array.isArray(c.debiasMode) && c.debiasMode.every((m) => m === 'length' || m === 'position')))
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
return true;
|
|
94
|
+
});
|
|
58
95
|
const okDec = r.decisions.every((d) => d && typeof d === 'object'
|
|
59
96
|
&& isStringField(d.decisionKind));
|
|
60
97
|
return okDist && okEv && okDec;
|
|
@@ -4,6 +4,7 @@ import type { SkillSegment } from './trace-segmenter.js';
|
|
|
4
4
|
export { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
|
|
5
5
|
export type { TextMatchRange } from './feedback-matchers.js';
|
|
6
6
|
export type { ExperienceAssistiveInference, ExperienceAssistiveInferenceCautionCode, ExperienceAssistiveInferenceCode, ExperienceAssistiveInferenceConfidence, ExperienceChecklistContribution, ExperienceChecklistItem, ExperienceChecklistItemStatus, ExperienceEpisode, ExperienceEpisodeArtifact, ExperienceEpisodeArtifactKind, ExperienceEpisodeBoundaryReason, ExperienceEpisodeOutcome, ExperienceEpisodeRole, ExperienceEvidenceChain, ExperienceEvidenceKind, ExperienceEvidenceRef, ExperienceFeedbackAttribution, ExperienceFeedbackAttributionReason, ExperienceFeedbackAttributionRole, ExperienceFeedbackSignal, ExperienceFeedbackSignalType, ExperienceGoalEvidenceRef, ExperienceGoalSlice, ExperienceGoalSliceReasonCode, ExperienceInvocation, ExperienceMessageRange, ExperienceOrchestrationEdge, ExperienceOrchestrationEdgeKind, ExperienceOrchestrationEdgeStatus, ExperienceOutcomeClosure, ExperienceParentReason, ExperienceReviewBasisCode, ExperienceReviewIndicators, ExperienceReviewPriority, ExperienceReviewerReport, ExperienceReviewerReportFinding, ExperienceReviewerReportFindingLevel, ExperienceReviewerReportFindingSource, ExperienceReviewerReportScope, ExperienceReviewerReportStep, ExperienceReviewerReportStepStatus, ExperienceRuleFinding, ExperienceRuleFindingCode, ExperienceRuleFindingLevel, ExperienceRuntimeSkillType, ExperienceRuntimeSkillTypeSource, ExperienceSessionStory, ExperienceSessionStoryAnswer, ExperienceSessionStoryAnswerKey, ExperienceSessionStoryGoalSlice, ExperienceSessionStoryGraphEdge, ExperienceSessionStoryGraphNode, ExperienceSessionStoryNode, ExperienceSessionStoryNodeKind, ExperienceSessionStorySkillLink, ExperienceSessionStorySkillRole, ExperienceSessionStorySubagentDispatch, ExperienceSessionSummary, ExperienceSkillSegment, ExperienceSkillSummary, ExperienceTimelineBranch, ExperienceTimelineEvent, ExperienceTimelineTree, ObservationExperienceReport, ObservationReviewState, };
|
|
7
|
+
export declare const OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 2;
|
|
7
8
|
export declare function aggregateExperienceChecklistItemStatus(statuses: ExperienceChecklistItemStatus[]): ExperienceChecklistItemStatus;
|
|
8
9
|
interface BuildExperienceInput {
|
|
9
10
|
sessions: CcSession[];
|
|
@@ -13,6 +14,7 @@ interface BuildExperienceInput {
|
|
|
13
14
|
reviewState?: ObservationReviewState;
|
|
14
15
|
}
|
|
15
16
|
export declare function buildObservationExperienceReport(input: BuildExperienceInput): ObservationExperienceReport;
|
|
17
|
+
export declare function normalizeObservationExperienceReport(value: unknown): ObservationExperienceReport | null;
|
|
16
18
|
export declare function isExperienceTraceInProgress(session: ExperienceSessionSummary): boolean;
|
|
17
19
|
export declare function hasRecognizableUserGoalText(value: string | undefined): boolean;
|
|
18
20
|
export declare function foldExperienceChecklistItems(items: ExperienceChecklistItem[]): {
|
|
@@ -7,6 +7,7 @@ import { durationMsBetween } from '../shared/time.js';
|
|
|
7
7
|
import { loadExpectedToolsForSkill, loadFrontmatterSkillType, loadSkillDeclarationCheck, } from './experience-frontmatter.js';
|
|
8
8
|
import { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
|
|
9
9
|
export { findNegativeFeedbackMatches, findPositiveFeedbackMatches, findUserCorrectionMatches, findUserGoalShiftMatches, hasNegativeFeedbackSignal, hasPositiveFeedbackSignal, hasUserCorrectionSignal, hasUserGoalShiftSignal, } from './feedback-matchers.js';
|
|
10
|
+
export const OBSERVATION_EXPERIENCE_SCHEMA_VERSION = 2;
|
|
10
11
|
export function aggregateExperienceChecklistItemStatus(statuses) {
|
|
11
12
|
if (statuses.includes('degraded'))
|
|
12
13
|
return 'degraded';
|
|
@@ -131,8 +132,8 @@ export function buildObservationExperienceReport(input) {
|
|
|
131
132
|
const sessions = summarizeExperienceSessions(invocations, sessionGroupsByKey, input.generatedAt, input.reviewState);
|
|
132
133
|
const skills = summarizeExperienceSkills(sessions, invocations);
|
|
133
134
|
return {
|
|
134
|
-
|
|
135
|
-
schemaVersion:
|
|
135
|
+
kind: 'observe-experience',
|
|
136
|
+
schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
|
|
136
137
|
scope: 'evidence-only',
|
|
137
138
|
generatedAt: input.generatedAt,
|
|
138
139
|
meta: {
|
|
@@ -148,6 +149,34 @@ export function buildObservationExperienceReport(input) {
|
|
|
148
149
|
skills,
|
|
149
150
|
};
|
|
150
151
|
}
|
|
152
|
+
export function normalizeObservationExperienceReport(value) {
|
|
153
|
+
if (!value || typeof value !== 'object')
|
|
154
|
+
return null;
|
|
155
|
+
const report = value;
|
|
156
|
+
const kind = report.kind === 'observe-experience' ? report.kind : null;
|
|
157
|
+
if (!kind)
|
|
158
|
+
return null;
|
|
159
|
+
if (report.schemaVersion !== OBSERVATION_EXPERIENCE_SCHEMA_VERSION)
|
|
160
|
+
return null;
|
|
161
|
+
if (report.scope !== 'evidence-only')
|
|
162
|
+
return null;
|
|
163
|
+
if (typeof report.generatedAt !== 'string' || !report.meta || typeof report.meta !== 'object')
|
|
164
|
+
return null;
|
|
165
|
+
if (!Array.isArray(report.goalSlices) || !Array.isArray(report.invocations) || !Array.isArray(report.sessions) || !Array.isArray(report.skills)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
kind: 'observe-experience',
|
|
170
|
+
schemaVersion: OBSERVATION_EXPERIENCE_SCHEMA_VERSION,
|
|
171
|
+
scope: 'evidence-only',
|
|
172
|
+
generatedAt: report.generatedAt,
|
|
173
|
+
meta: report.meta,
|
|
174
|
+
goalSlices: report.goalSlices,
|
|
175
|
+
invocations: report.invocations,
|
|
176
|
+
sessions: report.sessions,
|
|
177
|
+
skills: report.skills,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
151
180
|
function relatedObservationItems(segment, items) {
|
|
152
181
|
return items.filter((item) => item.skillName === segment.skillName
|
|
153
182
|
&& (item.sessionId === segment.sessionId || item.recentSessionIds.includes(segment.sessionId))
|
|
@@ -6,10 +6,11 @@ import { extractGapSignalsFromTrace } from '../analysis/gap-analyzer.js';
|
|
|
6
6
|
import { ccTracesToResultEntries } from './trace-adapter.js';
|
|
7
7
|
import { isSearchToolCall, toolCallQuery } from '../shared/tool-search.js';
|
|
8
8
|
import { durationMsBetween } from '../shared/time.js';
|
|
9
|
-
import { buildObservationExperienceReport } from './experience.js';
|
|
9
|
+
import { buildObservationExperienceReport, normalizeObservationExperienceReport, } from './experience.js';
|
|
10
10
|
export const DEFAULT_PROJECT_OBSERVATIONS_DIR = join(process.cwd(), '.omk', 'observations');
|
|
11
11
|
export const DEFAULT_GLOBAL_OBSERVATIONS_DIR = join(homedir(), '.oh-my-knowledge', 'observations');
|
|
12
12
|
export const DEFAULT_OBSERVATIONS_DIR = DEFAULT_PROJECT_OBSERVATIONS_DIR;
|
|
13
|
+
const OBSERVATION_INBOX_SCHEMA_VERSION = 2;
|
|
13
14
|
function hashString(input) {
|
|
14
15
|
return createHash('sha256').update(input).digest('hex').slice(0, 16);
|
|
15
16
|
}
|
|
@@ -432,8 +433,8 @@ export function buildObservationInboxReport(tracePath, options = {}) {
|
|
|
432
433
|
const items = finishInboxAggregation(aggregationState);
|
|
433
434
|
const experience = buildObservationExperienceReport({ sessions, segments, items, generatedAt, reviewState: options.reviewState });
|
|
434
435
|
const report = {
|
|
435
|
-
|
|
436
|
-
schemaVersion:
|
|
436
|
+
kind: 'observe-inbox',
|
|
437
|
+
schemaVersion: OBSERVATION_INBOX_SCHEMA_VERSION,
|
|
437
438
|
meta: {
|
|
438
439
|
tracePath,
|
|
439
440
|
generatedAt,
|
|
@@ -543,7 +544,9 @@ export function loadObservationInboxReports(dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
|
543
544
|
.filter((file) => file.endsWith('-observe-inbox.json'))
|
|
544
545
|
.map((file) => {
|
|
545
546
|
try {
|
|
546
|
-
const report = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
547
|
+
const report = normalizeObservationInboxReport(JSON.parse(readFileSync(join(dir, file), 'utf-8')));
|
|
548
|
+
if (!report)
|
|
549
|
+
return null;
|
|
547
550
|
report.items = report.items.map((item) => {
|
|
548
551
|
const sourceKind = item.sourceKind;
|
|
549
552
|
return {
|
|
@@ -567,7 +570,30 @@ export function loadObservationInboxReports(dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
|
567
570
|
return null;
|
|
568
571
|
}
|
|
569
572
|
})
|
|
570
|
-
.filter((r) => r?.
|
|
573
|
+
.filter((r) => r?.kind === 'observe-inbox');
|
|
574
|
+
}
|
|
575
|
+
function normalizeObservationInboxReport(value) {
|
|
576
|
+
if (!value || typeof value !== 'object')
|
|
577
|
+
return null;
|
|
578
|
+
const report = value;
|
|
579
|
+
const kind = report.kind === 'observe-inbox' ? report.kind : null;
|
|
580
|
+
if (!kind)
|
|
581
|
+
return null;
|
|
582
|
+
if (report.schemaVersion !== OBSERVATION_INBOX_SCHEMA_VERSION)
|
|
583
|
+
return null;
|
|
584
|
+
if (!report.meta || typeof report.meta !== 'object' || !Array.isArray(report.items))
|
|
585
|
+
return null;
|
|
586
|
+
const experience = report.experience === undefined
|
|
587
|
+
? undefined
|
|
588
|
+
: normalizeObservationExperienceReport(report.experience) ?? undefined;
|
|
589
|
+
return {
|
|
590
|
+
kind: 'observe-inbox',
|
|
591
|
+
schemaVersion: OBSERVATION_INBOX_SCHEMA_VERSION,
|
|
592
|
+
meta: report.meta,
|
|
593
|
+
items: report.items,
|
|
594
|
+
...(experience ? { experience } : {}),
|
|
595
|
+
...(report.diagnostics !== undefined ? { diagnostics: report.diagnostics } : {}),
|
|
596
|
+
};
|
|
571
597
|
}
|
|
572
598
|
export function loadLatestObservationInboxReport(dir = DEFAULT_OBSERVATIONS_DIR) {
|
|
573
599
|
const reports = loadObservationInboxReports(dir);
|
|
@@ -38,10 +38,11 @@ export function observationMetricAnnotationVerdict(state, ref, metricKey) {
|
|
|
38
38
|
export function observationReviewStatePath(observationsDir) {
|
|
39
39
|
return join(observationsDir, 'review-state.json');
|
|
40
40
|
}
|
|
41
|
+
const OBSERVATION_REVIEW_STATE_SCHEMA_VERSION = 2;
|
|
41
42
|
export function emptyObservationReviewState(now = new Date().toISOString()) {
|
|
42
43
|
return {
|
|
43
|
-
|
|
44
|
-
schemaVersion:
|
|
44
|
+
kind: 'observe-review-state',
|
|
45
|
+
schemaVersion: OBSERVATION_REVIEW_STATE_SCHEMA_VERSION,
|
|
45
46
|
updatedAt: now,
|
|
46
47
|
entries: {},
|
|
47
48
|
};
|
|
@@ -52,15 +53,7 @@ export function loadObservationReviewState(observationsDir) {
|
|
|
52
53
|
return emptyObservationReviewState();
|
|
53
54
|
try {
|
|
54
55
|
const parsed = JSON.parse(readFileSync(path, 'utf-8'));
|
|
55
|
-
|
|
56
|
-
return emptyObservationReviewState();
|
|
57
|
-
}
|
|
58
|
-
return {
|
|
59
|
-
reportKind: 'observe-review-state',
|
|
60
|
-
schemaVersion: 1,
|
|
61
|
-
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
62
|
-
entries: Object.fromEntries(Object.entries(parsed.entries).filter(([, entry]) => isReviewStateEntry(entry))),
|
|
63
|
-
};
|
|
56
|
+
return normalizeObservationReviewState(parsed) ?? emptyObservationReviewState();
|
|
64
57
|
}
|
|
65
58
|
catch {
|
|
66
59
|
return emptyObservationReviewState();
|
|
@@ -92,6 +85,24 @@ export function updateObservationReviewState(observationsDir, update, now = new
|
|
|
92
85
|
writeFileSync(observationReviewStatePath(observationsDir), JSON.stringify(state, null, 2));
|
|
93
86
|
return state;
|
|
94
87
|
}
|
|
88
|
+
function normalizeObservationReviewState(value) {
|
|
89
|
+
if (!value || typeof value !== 'object')
|
|
90
|
+
return null;
|
|
91
|
+
const parsed = value;
|
|
92
|
+
const kind = parsed.kind === 'observe-review-state' ? parsed.kind : null;
|
|
93
|
+
if (!kind)
|
|
94
|
+
return null;
|
|
95
|
+
if (parsed.schemaVersion !== OBSERVATION_REVIEW_STATE_SCHEMA_VERSION)
|
|
96
|
+
return null;
|
|
97
|
+
if (!parsed.entries || typeof parsed.entries !== 'object')
|
|
98
|
+
return null;
|
|
99
|
+
return {
|
|
100
|
+
kind: 'observe-review-state',
|
|
101
|
+
schemaVersion: OBSERVATION_REVIEW_STATE_SCHEMA_VERSION,
|
|
102
|
+
updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : new Date().toISOString(),
|
|
103
|
+
entries: Object.fromEntries(Object.entries(parsed.entries).filter(([, entry]) => isReviewStateEntry(entry))),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
95
106
|
export function deleteObservationReviewState(observationsDir, targetType, targetId, now = new Date().toISOString()) {
|
|
96
107
|
if (!isReviewTargetType(targetType))
|
|
97
108
|
throw new Error('invalid review targetType');
|
|
@@ -2,7 +2,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
|
|
|
2
2
|
import { createExecutor } from '../../executors/index.js';
|
|
3
3
|
import { readPromptDocument } from '../../shared/llm-prompts/index.js';
|
|
4
4
|
import { DEFAULT_LLM_ENHANCED_REVIEW_MODEL, PROMPTS_DIR, SOFT_STANDARD_PROMPT_ID, SOFT_STANDARD_PROMPT_VERSION, } from './constants.js';
|
|
5
|
-
import { hashText, loadExisting, markStale, skillDerivedStandardsDir, skillDerivedStandardsPath, } from './skill-standards-store.js';
|
|
5
|
+
import { hashText, loadExisting, markStale, SKILL_DERIVED_STANDARDS_SCHEMA_VERSION, skillDerivedStandardsDir, skillDerivedStandardsPath, } from './skill-standards-store.js';
|
|
6
6
|
import { evaluateRuntimeStandardNodes } from './runtime-evaluator.js';
|
|
7
7
|
export async function extractSkillSoftStandards(options) {
|
|
8
8
|
const { observationsDir, skillChain } = options;
|
|
@@ -47,8 +47,8 @@ export async function extractSkillSoftStandards(options) {
|
|
|
47
47
|
source: 'llm_soft_standard',
|
|
48
48
|
}));
|
|
49
49
|
const record = {
|
|
50
|
-
|
|
51
|
-
schemaVersion:
|
|
50
|
+
kind: 'observe-skill-derived-standards',
|
|
51
|
+
schemaVersion: SKILL_DERIVED_STANDARDS_SCHEMA_VERSION,
|
|
52
52
|
skillName: skillChain.skillName,
|
|
53
53
|
sourceSkillPath: skillChain.definition.path,
|
|
54
54
|
sourceHash,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ResolveSkillStandardsOptions, ResolvedSkillStandards, SkillDerivedStandardStatus, SkillDerivedStandards } from './types.js';
|
|
2
|
+
export declare const SKILL_DERIVED_STANDARDS_SCHEMA_VERSION = 2;
|
|
2
3
|
export declare function skillDerivedStandardsDir(observationsDir: string): string;
|
|
3
4
|
export declare function skillDerivedStandardsPath(observationsDir: string, skillName: string): string;
|
|
4
5
|
export declare function loadSkillDerivedStandards(observationsDir: string): Record<string, SkillDerivedStandards>;
|
|
@@ -2,6 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { buildObservationSkillChain } from '../skill-chain.js';
|
|
5
|
+
export const SKILL_DERIVED_STANDARDS_SCHEMA_VERSION = 2;
|
|
5
6
|
export function skillDerivedStandardsDir(observationsDir) {
|
|
6
7
|
return join(observationsDir, 'skill-derived');
|
|
7
8
|
}
|
|
@@ -17,8 +18,8 @@ export function loadSkillDerivedStandards(observationsDir) {
|
|
|
17
18
|
if (!file.endsWith('.json'))
|
|
18
19
|
continue;
|
|
19
20
|
try {
|
|
20
|
-
const parsed = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
21
|
-
if (
|
|
21
|
+
const parsed = normalizeSkillDerivedStandards(JSON.parse(readFileSync(join(dir, file), 'utf-8')));
|
|
22
|
+
if (parsed)
|
|
22
23
|
out[parsed.skillName] = parsed;
|
|
23
24
|
}
|
|
24
25
|
catch {
|
|
@@ -51,11 +52,13 @@ export function updateSkillDerivedStandardStatus(observationsDir, skillName, sta
|
|
|
51
52
|
}
|
|
52
53
|
export function resolveSkillStandards(skillName, options) {
|
|
53
54
|
const skillChain = options.skillChain ?? buildObservationSkillChain(skillName, options.cwd ?? process.cwd());
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
const directDerived = normalizeSkillDerivedStandards(options.derivedStandards);
|
|
56
|
+
const mappedDerived = options.derivedStandards && !isSkillDerivedStandards(options.derivedStandards)
|
|
57
|
+
? normalizeSkillDerivedStandards(options.derivedStandards[skillName])
|
|
58
|
+
: undefined;
|
|
59
|
+
const derived = directDerived
|
|
60
|
+
?? mappedDerived
|
|
61
|
+
?? loadSkillDerivedStandards(options.observationsDir)[skillName];
|
|
59
62
|
const active = [];
|
|
60
63
|
const candidates = [];
|
|
61
64
|
const hasFrontmatterHardRules = skillChain.healthCheck.hardRules.declared && skillChain.healthCheck.hardRules.rules.length > 0;
|
|
@@ -112,8 +115,7 @@ export function loadExisting(path) {
|
|
|
112
115
|
if (!existsSync(path))
|
|
113
116
|
return undefined;
|
|
114
117
|
try {
|
|
115
|
-
|
|
116
|
-
return isSkillDerivedStandards(parsed) ? parsed : undefined;
|
|
118
|
+
return normalizeSkillDerivedStandards(JSON.parse(readFileSync(path, 'utf-8')));
|
|
117
119
|
}
|
|
118
120
|
catch {
|
|
119
121
|
return undefined;
|
|
@@ -129,13 +131,74 @@ export function markStale(record, generatedAt) {
|
|
|
129
131
|
};
|
|
130
132
|
}
|
|
131
133
|
export function isSkillDerivedStandards(value) {
|
|
134
|
+
return normalizeSkillDerivedStandards(value) !== undefined;
|
|
135
|
+
}
|
|
136
|
+
function normalizeSkillDerivedStandards(value) {
|
|
132
137
|
if (!value || typeof value !== 'object')
|
|
133
|
-
return
|
|
138
|
+
return undefined;
|
|
134
139
|
const item = value;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
const kind = item.kind === 'observe-skill-derived-standards' ? item.kind : null;
|
|
141
|
+
if (!kind)
|
|
142
|
+
return undefined;
|
|
143
|
+
if (item.schemaVersion !== SKILL_DERIVED_STANDARDS_SCHEMA_VERSION)
|
|
144
|
+
return undefined;
|
|
145
|
+
if (typeof item.skillName !== 'string' || typeof item.generatedAt !== 'string' || typeof item.model !== 'string' || typeof item.executor !== 'string') {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
if (typeof item.promptId !== 'string' || typeof item.promptVersion !== 'string' || !Array.isArray(item.standards))
|
|
149
|
+
return undefined;
|
|
150
|
+
const standards = item.standards
|
|
151
|
+
.map(normalizeSkillDerivedStandard)
|
|
152
|
+
.filter((record) => record !== null);
|
|
153
|
+
return {
|
|
154
|
+
kind: 'observe-skill-derived-standards',
|
|
155
|
+
schemaVersion: SKILL_DERIVED_STANDARDS_SCHEMA_VERSION,
|
|
156
|
+
skillName: item.skillName,
|
|
157
|
+
...(typeof item.sourceSkillPath === 'string' ? { sourceSkillPath: item.sourceSkillPath } : {}),
|
|
158
|
+
...(typeof item.sourceHash === 'string' ? { sourceHash: item.sourceHash } : {}),
|
|
159
|
+
generatedAt: item.generatedAt,
|
|
160
|
+
model: item.model,
|
|
161
|
+
executor: item.executor,
|
|
162
|
+
promptId: item.promptId,
|
|
163
|
+
promptVersion: item.promptVersion,
|
|
164
|
+
...(typeof item.promptHash === 'string' ? { promptHash: item.promptHash } : {}),
|
|
165
|
+
...(typeof item.runtimeEvidenceHash === 'string' ? { runtimeEvidenceHash: item.runtimeEvidenceHash } : {}),
|
|
166
|
+
...(item.enhancedReview && typeof item.enhancedReview === 'object' ? { enhancedReview: item.enhancedReview } : {}),
|
|
167
|
+
standards,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function normalizeSkillDerivedStandard(value) {
|
|
171
|
+
if (!value || typeof value !== 'object')
|
|
172
|
+
return null;
|
|
173
|
+
const item = value;
|
|
174
|
+
const standardKind = item.standardKind === 'hard_rule_candidate' || item.standardKind === 'workflow_candidate'
|
|
175
|
+
? item.standardKind
|
|
176
|
+
: item.kind === 'hard_rule_candidate' || item.kind === 'workflow_candidate'
|
|
177
|
+
? item.kind
|
|
178
|
+
: null;
|
|
179
|
+
if (!standardKind)
|
|
180
|
+
return null;
|
|
181
|
+
if (typeof item.id !== 'string' || typeof item.title !== 'string' || typeof item.body !== 'string')
|
|
182
|
+
return null;
|
|
183
|
+
if (item.status !== 'pending_review' && item.status !== 'author_confirmed' && item.status !== 'rejected' && item.status !== 'stale') {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
if (item.source !== 'llm_soft_standard')
|
|
187
|
+
return null;
|
|
188
|
+
if (item.confidence !== 'low' && item.confidence !== 'medium' && item.confidence !== 'high')
|
|
189
|
+
return null;
|
|
190
|
+
if (!Array.isArray(item.evidence) || item.evidence.some((entry) => typeof entry !== 'string'))
|
|
191
|
+
return null;
|
|
192
|
+
return {
|
|
193
|
+
id: item.id,
|
|
194
|
+
standardKind,
|
|
195
|
+
status: item.status,
|
|
196
|
+
title: item.title,
|
|
197
|
+
body: item.body,
|
|
198
|
+
source: item.source,
|
|
199
|
+
confidence: item.confidence,
|
|
200
|
+
evidence: item.evidence,
|
|
201
|
+
};
|
|
139
202
|
}
|
|
140
203
|
function candidateRank(status) {
|
|
141
204
|
if (status === 'pending_review')
|
|
@@ -14,8 +14,8 @@ export interface SkillDerivedStandard {
|
|
|
14
14
|
evidence: string[];
|
|
15
15
|
}
|
|
16
16
|
export interface SkillDerivedStandards {
|
|
17
|
-
|
|
18
|
-
schemaVersion:
|
|
17
|
+
kind: 'observe-skill-derived-standards';
|
|
18
|
+
schemaVersion: 2;
|
|
19
19
|
skillName: string;
|
|
20
20
|
sourceSkillPath?: string;
|
|
21
21
|
sourceHash?: string;
|
|
@@ -21,7 +21,7 @@ function levelDot(level) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
function isEvaluationReport(document) {
|
|
24
|
-
return document.
|
|
24
|
+
return document.kind === 'evaluation';
|
|
25
25
|
}
|
|
26
26
|
function scoreOf(summary) {
|
|
27
27
|
return summary?.avgCompositeScore ?? summary?.avgLlmScore ?? null;
|
|
@@ -163,7 +163,7 @@ export function renderRunList(runs, lang = DEFAULT_LANG) {
|
|
|
163
163
|
`<span class="rc-score-num rc-score-num--${cls}">${score.toFixed(2)}</span></div>`;
|
|
164
164
|
};
|
|
165
165
|
const cards = runs.map((run) => {
|
|
166
|
-
if (run.
|
|
166
|
+
if (run.kind === 'batch-evaluation') {
|
|
167
167
|
const m = run.meta;
|
|
168
168
|
const scores = run.items.length > 0
|
|
169
169
|
? run.items.map((item) => {
|
|
@@ -652,7 +652,7 @@ export function renderBatchEvaluationDetail(report, lang = DEFAULT_LANG) {
|
|
|
652
652
|
export function renderReportDocumentDetail(report, lang = DEFAULT_LANG) {
|
|
653
653
|
if (!report)
|
|
654
654
|
return renderRunDetail(null, lang);
|
|
655
|
-
return report.
|
|
655
|
+
return report.kind === 'batch-evaluation'
|
|
656
656
|
? renderBatchEvaluationDetail(report, lang)
|
|
657
657
|
: renderRunDetail(report, lang);
|
|
658
658
|
}
|
|
@@ -762,7 +762,7 @@ export function createReportServer({ port, host: hostOption, reportsDir = DEFAUL
|
|
|
762
762
|
let evalReport = null;
|
|
763
763
|
if (entry.eval) {
|
|
764
764
|
const r = await reportStore.get(entry.eval.reportId);
|
|
765
|
-
if (r && r.
|
|
765
|
+
if (r && r.kind === 'evaluation')
|
|
766
766
|
evalReport = r;
|
|
767
767
|
}
|
|
768
768
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|