oh-my-knowledge 0.35.0 → 0.37.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 +5 -3
- package/README.zh.md +5 -3
- package/dist/assets/agent-skills/omk/SKILL.md +4 -2
- package/dist/assets/agent-skills/omk/references/commands.md +121 -2
- package/dist/authoring/evolver.js +1 -1
- package/dist/cli/commands/doctor.js +21 -5
- package/dist/cli/commands/eval/index.js +7 -6
- package/dist/cli/commands/evolve.d.ts +11 -0
- package/dist/cli/commands/evolve.js +105 -5
- package/dist/cli/commands/list.d.ts +25 -0
- package/dist/cli/commands/list.js +118 -0
- package/dist/cli/commands/promote.d.ts +24 -0
- package/dist/cli/commands/promote.js +158 -0
- package/dist/cli/commands/rollback.d.ts +20 -0
- package/dist/cli/commands/rollback.js +95 -0
- package/dist/cli/commands/sample.d.ts +1 -0
- package/dist/cli/commands/sample.js +10 -4
- package/dist/cli/lib/cell-format.d.ts +17 -0
- package/dist/cli/lib/cell-format.js +19 -0
- 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/list.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/list.js +36 -0
- package/dist/cli/lib/i18n-dict/promote.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/promote.js +54 -0
- package/dist/cli/lib/i18n-dict/rollback.d.ts +3 -0
- package/dist/cli/lib/i18n-dict/rollback.js +22 -0
- package/dist/cli/lib/i18n-dict.d.ts +4 -1
- package/dist/cli/lib/i18n-dict.js +6 -0
- package/dist/cli/lib/progress.d.ts +3 -0
- package/dist/cli/lib/progress.js +22 -0
- package/dist/cli/lib/run-tally.js +1 -1
- package/dist/cli/lib/shared.js +1 -1
- package/dist/cli/lib/source-probe.d.ts +15 -0
- package/dist/cli/lib/source-probe.js +129 -0
- package/dist/doctor/endpoint-rule.d.ts +60 -0
- package/dist/doctor/endpoint-rule.js +381 -0
- package/dist/doctor/health/load-custom-dimensions.d.ts +6 -0
- package/dist/doctor/health/load-custom-dimensions.js +43 -3
- package/dist/doctor/index.js +9 -2
- 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/executors/codex-cli.js +5 -1
- package/dist/inputs/skill-loader.js +6 -3
- package/dist/managed/index.d.ts +2 -0
- package/dist/managed/index.js +2 -0
- package/dist/managed/list-view.d.ts +58 -0
- package/dist/managed/list-view.js +70 -0
- package/dist/managed/promote-gate.d.ts +40 -0
- package/dist/managed/promote-gate.js +37 -0
- package/dist/managed/store.d.ts +25 -2
- package/dist/managed/store.js +137 -11
- 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/doctor-detail-renderer.d.ts +9 -0
- package/dist/renderer/doctor-detail-renderer.js +114 -0
- package/dist/renderer/html-renderer.d.ts +3 -2
- package/dist/renderer/html-renderer.js +102 -102
- package/dist/renderer/icons.d.ts +29 -0
- package/dist/renderer/icons.js +66 -0
- package/dist/renderer/layout.js +81 -68
- package/dist/renderer/observation-inbox/styles.d.ts +1 -1
- package/dist/renderer/observation-inbox/styles.js +53 -53
- package/dist/renderer/report-shell.d.ts +77 -0
- package/dist/renderer/report-shell.js +223 -0
- package/dist/renderer/skill-detail-renderer.js +147 -352
- package/dist/renderer/skill-health-renderer.js +50 -73
- package/dist/renderer/skill-list-renderer.js +358 -306
- package/dist/renderer/summary.js +272 -162
- package/dist/renderer/test-view.d.ts +4 -3
- package/dist/renderer/test-view.js +386 -135
- package/dist/server/report-server.js +142 -28
- 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 +24 -2
- package/dist/types/doctor.js +1 -1
- package/dist/types/managed.d.ts +17 -3
- package/dist/types/observability.d.ts +6 -6
- package/dist/types/report.d.ts +9 -4
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
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
|
+
/** 当前有效证据(contentHash == record.contentHash)里 recordedAt 最新那条;无则 undefined。
|
|
47
|
+
* list(展示最新 verdict)与 promote(门禁取证)共用同一口径——旧内容的证据不冒充当前。 */
|
|
48
|
+
export declare function latestCurrentEvidence(record: ManagedArtifactRecord): import("../types/managed.js").ManagedEvidenceRef | undefined;
|
|
49
|
+
export declare function buildManagedListRow(record: ManagedArtifactRecord, probe: SourceProbe): ManagedListRow;
|
|
50
|
+
/**
|
|
51
|
+
* 组装全部行。`probeOf` 给每条记录探测当前源(三态,见文件头)。
|
|
52
|
+
* 按 name 排序(稳定、可读);name collation 相等再按 kind。排序对机读出口(--json)也生效,故:
|
|
53
|
+
* - **钉死 locale**(`'en'`):缺省 locale 随宿主 LANG / LC_COLLATE 漂,会让两台机器对同一批记录排出不同
|
|
54
|
+
* JSON 顺序,破坏 omk「确定、可比」的底色。
|
|
55
|
+
* - 平级判定用**同一 collation 度量**(localeCompare 结果是否为 0)而非 `===`:NFC / NFD 等价的同显示名
|
|
56
|
+
* 在 `===` 下不等、却 collation 相等,用 `===` 会漏掉 kind 平级 tiebreak、令顺序退回 readdir 序(不定)。
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildManagedListRows(records: ManagedArtifactRecord[], probeOf: (record: ManagedArtifactRecord) => SourceProbe): ManagedListRow[];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { deriveManagedState, isCurrentlyPromoted } from './store.js';
|
|
2
|
+
/** 当前有效证据(contentHash == record.contentHash)里 recordedAt 最新那条;无则 undefined。
|
|
3
|
+
* list(展示最新 verdict)与 promote(门禁取证)共用同一口径——旧内容的证据不冒充当前。 */
|
|
4
|
+
export function latestCurrentEvidence(record) {
|
|
5
|
+
const current = record.evidence.filter((e) => e.contentHash === record.contentHash);
|
|
6
|
+
if (current.length === 0)
|
|
7
|
+
return undefined;
|
|
8
|
+
// 取 recordedAt 最新的一条。omk 自写恒 UTC `Z`、字典序即时间序;但记录可手改 / 随仓库分发,异偏移
|
|
9
|
+
// 或异精度的 ISO 串字典序会乱 → 优先按解析后的真实时刻比,两端都可解析才用;否则退回字典序(不劣化
|
|
10
|
+
// omk 自写场景)。并列取后出现的。
|
|
11
|
+
const ms = (s) => { const n = Date.parse(s); return Number.isNaN(n) ? null : n; };
|
|
12
|
+
return current.reduce((a, b) => {
|
|
13
|
+
const ta = ms(a.recordedAt);
|
|
14
|
+
const tb = ms(b.recordedAt);
|
|
15
|
+
if (ta !== null && tb !== null)
|
|
16
|
+
return tb >= ta ? b : a;
|
|
17
|
+
return b.recordedAt >= a.recordedAt ? b : a;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export function buildManagedListRow(record, probe) {
|
|
21
|
+
const currentEvidenceCount = record.evidence.filter((e) => e.contentHash === record.contentHash).length;
|
|
22
|
+
let state;
|
|
23
|
+
let drifted;
|
|
24
|
+
if (probe.reachable) {
|
|
25
|
+
// 算出了当前哈:正常推导(哈不等 → stale/drift)。
|
|
26
|
+
const d = deriveManagedState({ record, currentContentHash: probe.hash });
|
|
27
|
+
state = d.label;
|
|
28
|
+
drifted = d.drifted;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// 不可达:不据此判 stale。promote 决定是**内容锚定**的(只看 record.decisions + record.contentHash,
|
|
32
|
+
// 不依赖源能否被 probe),故当前内容仍处于 promoted 态时保留 promoted —— 它比 evidence 更不该因源不可达
|
|
33
|
+
// 丢失(deriveManagedState 的 promoted 分支同理)。否则按证据给 measurable/installed,drift 标未核。
|
|
34
|
+
state = isCurrentlyPromoted(record) ? 'promoted' : currentEvidenceCount > 0 ? 'measurable' : 'installed';
|
|
35
|
+
drifted = false;
|
|
36
|
+
}
|
|
37
|
+
const latest = latestCurrentEvidence(record);
|
|
38
|
+
return {
|
|
39
|
+
id: record.id,
|
|
40
|
+
name: record.name,
|
|
41
|
+
kind: record.kind,
|
|
42
|
+
sourceKind: record.source.sourceKind,
|
|
43
|
+
sourceLabel: record.source.sourceKind === 'git' && record.source.url ? record.source.url : record.source.locator,
|
|
44
|
+
state,
|
|
45
|
+
drifted,
|
|
46
|
+
reachable: probe.reachable,
|
|
47
|
+
...(latest?.verdict ? { latestVerdict: latest.verdict } : {}),
|
|
48
|
+
...(latest?.comparability ? { comparability: latest.comparability } : {}),
|
|
49
|
+
...(latest?.recordedAt ? { recordedAt: latest.recordedAt } : {}),
|
|
50
|
+
currentEvidenceCount,
|
|
51
|
+
totalEvidenceCount: record.evidence.length,
|
|
52
|
+
distributionCount: record.distribution.length,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 组装全部行。`probeOf` 给每条记录探测当前源(三态,见文件头)。
|
|
57
|
+
* 按 name 排序(稳定、可读);name collation 相等再按 kind。排序对机读出口(--json)也生效,故:
|
|
58
|
+
* - **钉死 locale**(`'en'`):缺省 locale 随宿主 LANG / LC_COLLATE 漂,会让两台机器对同一批记录排出不同
|
|
59
|
+
* JSON 顺序,破坏 omk「确定、可比」的底色。
|
|
60
|
+
* - 平级判定用**同一 collation 度量**(localeCompare 结果是否为 0)而非 `===`:NFC / NFD 等价的同显示名
|
|
61
|
+
* 在 `===` 下不等、却 collation 相等,用 `===` 会漏掉 kind 平级 tiebreak、令顺序退回 readdir 序(不定)。
|
|
62
|
+
*/
|
|
63
|
+
export function buildManagedListRows(records, probeOf) {
|
|
64
|
+
return records
|
|
65
|
+
.map((r) => buildManagedListRow(r, probeOf(r)))
|
|
66
|
+
.sort((a, b) => {
|
|
67
|
+
const byName = a.name.localeCompare(b.name, 'en');
|
|
68
|
+
return byName !== 0 ? byName : a.kind.localeCompare(b.kind, 'en');
|
|
69
|
+
});
|
|
70
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promote 证据门禁(纯函数,无 IO)——`omk list` 读侧判据的写侧对应。
|
|
3
|
+
*
|
|
4
|
+
* 把「这份当前版本是否值得 ship」收成一组可拦截原因(spec evidence-gated-management.md §5 mandatory four
|
|
5
|
+
* + §7 default gate)。默认只放行 verdict=PROGRESS(omk default-strict:影响「值得 ship」判定的默认必须严格);
|
|
6
|
+
* CAUTIOUS 需显式 `acceptCautious`;其余 verdict 一律拦。可比性核对(judge prompt 是否还是当前评委)用
|
|
7
|
+
* **注入**的当前 judge hash 集合,故本模块不依赖 grading 层、保持叶子可测。
|
|
8
|
+
*
|
|
9
|
+
* force(越门)不在此处:门禁只产「客观判定」。CLI 拿到结果后决定是否 `--force` 越过——但**无当前证据时
|
|
10
|
+
* 无 evidence 可锚定,force 也越不过**(返回的 evidence 为 undefined,CLI 据此拒绝空证据 promote)。
|
|
11
|
+
*/
|
|
12
|
+
import type { ManagedArtifactRecord, ManagedEvidenceRef } from '../types/index.js';
|
|
13
|
+
export type PromoteBlockKind = 'drifted' | 'no_current_evidence' | 'incomparable' | 'verdict_blocked';
|
|
14
|
+
export interface PromoteBlock {
|
|
15
|
+
/** 限定判别字(裸 kind 留给 ArtifactKind,见 terminology-spec §5.4);这是「拦截原因」分类、非持久。 */
|
|
16
|
+
blockKind: PromoteBlockKind;
|
|
17
|
+
/** 供 CLI 拼 i18n 参数的可读细节(被拦的 verdict / judge hash 等)。 */
|
|
18
|
+
detail?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
export interface PromoteGateInput {
|
|
21
|
+
record: ManagedArtifactRecord;
|
|
22
|
+
/** 当前源内容 hash;undefined = 源不可达 / 解析失败(probeSourceState reachable:false)。 */
|
|
23
|
+
currentContentHash?: string;
|
|
24
|
+
/** 接受 CAUTIOUS(默认只接受 PROGRESS)。 */
|
|
25
|
+
acceptCautious?: boolean;
|
|
26
|
+
/** 当前 omk 的 judge prompt hash 集合(debias on/off 两变体)。CLI 用 getJudgePromptHash 算好注入,
|
|
27
|
+
* 使本模块不依赖 grading 层。空集 / 省略 = 跳过可比性核对(只在无法取 judge hash 时)。 */
|
|
28
|
+
currentJudgeHashes?: ReadonlySet<string>;
|
|
29
|
+
}
|
|
30
|
+
export interface PromoteGateResult {
|
|
31
|
+
/** 无任何拦截原因 → 门禁通过。 */
|
|
32
|
+
ok: boolean;
|
|
33
|
+
blocked: PromoteBlock[];
|
|
34
|
+
/** 非拦截级提醒(如 judgePromptHash 缺失、无从核对可比性)。 */
|
|
35
|
+
warnings: PromoteBlock[];
|
|
36
|
+
/** 命中判定的当前有效证据;无当前证据时 undefined —— CLI 据此判定「force 也不能空证据 promote」。 */
|
|
37
|
+
evidence?: ManagedEvidenceRef;
|
|
38
|
+
drifted: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function evaluatePromoteGate(input: PromoteGateInput): PromoteGateResult;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { latestCurrentEvidence } from './list-view.js';
|
|
2
|
+
// 默认接受集:只 PROGRESS。
|
|
3
|
+
const DEFAULT_ACCEPTED = new Set(['PROGRESS']);
|
|
4
|
+
export function evaluatePromoteGate(input) {
|
|
5
|
+
const { record, currentContentHash, acceptCautious, currentJudgeHashes } = input;
|
|
6
|
+
const blocked = [];
|
|
7
|
+
const warnings = [];
|
|
8
|
+
const drifted = currentContentHash === undefined || currentContentHash !== record.contentHash;
|
|
9
|
+
// 1. drift:源不可达 / 内容已变 → 当前盘上的源不是被测内容,门禁不过(force 可越,见 CLI)。
|
|
10
|
+
if (drifted)
|
|
11
|
+
blocked.push({ blockKind: 'drifted' });
|
|
12
|
+
// 2. 当前有效证据(contentHash == record.contentHash;源 drift 不影响——证据绑的是记录基线内容)。
|
|
13
|
+
const evidence = latestCurrentEvidence(record);
|
|
14
|
+
if (!evidence) {
|
|
15
|
+
// 无证据无从谈 verdict / 可比性,且 force 也无 evidence 可锚定 → 提前收,终态拦截。
|
|
16
|
+
blocked.push({ blockKind: 'no_current_evidence' });
|
|
17
|
+
return { ok: false, blocked, warnings, drifted };
|
|
18
|
+
}
|
|
19
|
+
// 3. 可比性:judgePromptHash 在场且不属当前任一评委模板 → 评委已变、旧 verdict 不可比 → 拦。
|
|
20
|
+
// 缺失 → 无从核对(旧报告 / 无 judge 评测)→ warn 不拦。cliVersion 仅展示、不硬卡(否则每次发版即全失效)。
|
|
21
|
+
const judgeHash = evidence.comparability?.judgePromptHash;
|
|
22
|
+
if (judgeHash !== undefined && currentJudgeHashes && currentJudgeHashes.size > 0) {
|
|
23
|
+
if (!currentJudgeHashes.has(judgeHash)) {
|
|
24
|
+
blocked.push({ blockKind: 'incomparable', detail: { judgePromptHash: judgeHash } });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (judgeHash === undefined) {
|
|
28
|
+
warnings.push({ blockKind: 'incomparable' });
|
|
29
|
+
}
|
|
30
|
+
// 4. verdict ∈ 接受集。
|
|
31
|
+
const accepted = acceptCautious ? new Set([...DEFAULT_ACCEPTED, 'CAUTIOUS']) : DEFAULT_ACCEPTED;
|
|
32
|
+
const verdict = evidence.verdict;
|
|
33
|
+
if (!verdict || !accepted.has(verdict)) {
|
|
34
|
+
blocked.push({ blockKind: 'verdict_blocked', detail: { verdict: verdict ?? 'UNKNOWN' } });
|
|
35
|
+
}
|
|
36
|
+
return { ok: blocked.length === 0, blocked, warnings, evidence, drifted };
|
|
37
|
+
}
|
package/dist/managed/store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ArtifactKind, DeriveManagedStateInput, DerivedManagedState, ManagedArtifactRecord, ManagedArtifactSource, ManagedDistributionTarget, ManagedEvidenceRef } from '../types/index.js';
|
|
1
|
+
import type { ArtifactKind, DeriveManagedStateInput, DerivedManagedState, ManagedArtifactRecord, ManagedArtifactSource, ManagedDecision, ManagedDistributionTarget, ManagedEvidenceRef } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* 受管记录的 per-record 文件存储。一条记录一个 `.omk/managed/<id>.json`,镜像 report-store 的
|
|
4
4
|
* 成熟模式(原子 tmp+rename):每次 install 只碰自己那个文件,independent write、不丢别人、
|
|
@@ -44,6 +44,28 @@ export declare function upsertManagedRecord(dir: string, record: ManagedArtifact
|
|
|
44
44
|
* null —— 这是"管理是 install 显式 opt-in"的体现:eval 绝不为未纳管的 skill 凭空建记录。
|
|
45
45
|
*/
|
|
46
46
|
export declare function appendManagedEvidence(dir: string, recordId: string, evidence: ManagedEvidenceRef): ManagedArtifactRecord | null;
|
|
47
|
+
/**
|
|
48
|
+
* 当前内容(contentHash === record.contentHash)是否处于 promoted 态。决定是 append-only 事件流,
|
|
49
|
+
* promote 与 rollback 互为反操作,故不能看「是否存在过 promote」,而要看**当前内容最近一条** promote/rollback
|
|
50
|
+
* 决定是不是 promote —— rollback 之后再 promote 仍能恢复 promoted,反之亦然。换了内容(contentHash 变)后
|
|
51
|
+
* 旧内容的决定一概不算数。
|
|
52
|
+
*
|
|
53
|
+
* 「最近」按 decidedAt 真实时刻取,与 `latestCurrentEvidence` 同口径:omk 自写恒 UTC `Z`、字典序即时间序;
|
|
54
|
+
* 但记录可手改 / 随仓库分发,异偏移 ISO 串字典序会乱 → 两端可解析才用解析时刻,否则退字典序;并列取后出现的
|
|
55
|
+
* (数组追加序 = 事件序)。
|
|
56
|
+
*/
|
|
57
|
+
export declare function isCurrentlyPromoted(record: ManagedArtifactRecord): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* 追加一条人工管理决定(append-only,promote/reject/rollback 走此路)。与 evidence 同样**不能走 upsert**
|
|
60
|
+
* (`mergeManagedRecord` 刻意保留旧 decisions、丢弃 next.decisions),必须独立 load→push→原子重写。
|
|
61
|
+
*
|
|
62
|
+
* 幂等:对**当前内容**的 promote/rollback,看是否会改变当前 promoted 态——已 promoted 再 promote、未 promoted
|
|
63
|
+
* 再 rollback 都不追加、原样返回(由 CLI 提示「已 promoted」/「未 promoted」)。这让重复 `omk promote` /
|
|
64
|
+
* `omk rollback` 不堆冗余事件,但 promote↔rollback 的真实切换、以及换了内容(contentHash 变)后的新决定仍会
|
|
65
|
+
* 追加——正是要的版本史。其它 decisionKind(如 reject)仍按「同 kind + 同 contentHash 即 dedup」。记录不存在
|
|
66
|
+
* (未 install)返回 null,与 evidence 同口径(管理是 install 显式 opt-in,绝不为未纳管 skill 凭空建记录)。
|
|
67
|
+
*/
|
|
68
|
+
export declare function appendManagedDecision(dir: string, recordId: string, decision: ManagedDecision): ManagedArtifactRecord | null;
|
|
47
69
|
/**
|
|
48
70
|
* 统一的记录构造点——所有消费方(CLI / 未来 server / SDK)经此组装,保证 schema 一致。
|
|
49
71
|
* 只接收事实(身份、源、hash、分发落点);evidence/decisions 恒为空(eval/promote 的地盘)。
|
|
@@ -64,9 +86,10 @@ export declare function recordManagedArtifact(record: ManagedArtifactRecord, opt
|
|
|
64
86
|
/**
|
|
65
87
|
* 读时推导生命周期标签。
|
|
66
88
|
* - 源缺失或 hash 漂(当前内容 ≠ 记录 contentHash)→ stale;
|
|
89
|
+
* - 否则当前内容已有 promote 决定 → promoted;
|
|
67
90
|
* - 否则有**当前**证据 / samples → measurable;
|
|
68
91
|
* - 否则 installed。
|
|
69
|
-
*
|
|
92
|
+
* 「当前证据 / 当前决定」= contentHash 与记录当前 contentHash 匹配的 evidence / decision —— 旧内容的不算数,
|
|
70
93
|
* 这正是 #203「证据必须跟 artifact 一起走」的读时保证。`'discovered'` 留给无分发的记录,此函数不产生。
|
|
71
94
|
*/
|
|
72
95
|
export declare function deriveManagedState(input: DeriveManagedStateInput): DerivedManagedState;
|
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,21 +49,72 @@ 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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
+
});
|
|
95
|
+
const okDec = r.decisions.every((d) => {
|
|
96
|
+
if (!d || typeof d !== 'object')
|
|
97
|
+
return false;
|
|
98
|
+
const dec = d;
|
|
99
|
+
if (!isStringField(dec.decisionKind))
|
|
100
|
+
return false;
|
|
101
|
+
// promote/reject/rollback 的证据指针(promote 写)同样收窄:任意类型脏值不得穿过 validator 进
|
|
102
|
+
// deriveManagedState / promote 消费方。actor / decidedAt / reason 是 install 后才追加的可选展示字段,
|
|
103
|
+
// 旧记录(install 时 decisions 恒空)无,按 optional 读。
|
|
104
|
+
if (!isOptionalString(dec.actor) || !isOptionalString(dec.decidedAt) || !isOptionalString(dec.reason))
|
|
105
|
+
return false;
|
|
106
|
+
if (!isOptionalString(dec.contentHash) || !isOptionalString(dec.reportId))
|
|
107
|
+
return false;
|
|
108
|
+
if (dec.override !== undefined) {
|
|
109
|
+
const o = dec.override;
|
|
110
|
+
if (!o || typeof o !== 'object' || !isStringField(o.verdict))
|
|
111
|
+
return false;
|
|
112
|
+
if (o.overriddenBlocks !== undefined
|
|
113
|
+
&& !(Array.isArray(o.overriddenBlocks) && o.overriddenBlocks.every((b) => typeof b === 'string')))
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
60
118
|
return okDist && okEv && okDec;
|
|
61
119
|
}
|
|
62
120
|
export function loadManagedRecord(dir, id) {
|
|
@@ -181,6 +239,69 @@ export function appendManagedEvidence(dir, recordId, evidence) {
|
|
|
181
239
|
}
|
|
182
240
|
return merged;
|
|
183
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* 当前内容(contentHash === record.contentHash)是否处于 promoted 态。决定是 append-only 事件流,
|
|
244
|
+
* promote 与 rollback 互为反操作,故不能看「是否存在过 promote」,而要看**当前内容最近一条** promote/rollback
|
|
245
|
+
* 决定是不是 promote —— rollback 之后再 promote 仍能恢复 promoted,反之亦然。换了内容(contentHash 变)后
|
|
246
|
+
* 旧内容的决定一概不算数。
|
|
247
|
+
*
|
|
248
|
+
* 「最近」按 decidedAt 真实时刻取,与 `latestCurrentEvidence` 同口径:omk 自写恒 UTC `Z`、字典序即时间序;
|
|
249
|
+
* 但记录可手改 / 随仓库分发,异偏移 ISO 串字典序会乱 → 两端可解析才用解析时刻,否则退字典序;并列取后出现的
|
|
250
|
+
* (数组追加序 = 事件序)。
|
|
251
|
+
*/
|
|
252
|
+
export function isCurrentlyPromoted(record) {
|
|
253
|
+
const ms = (s) => { const n = Date.parse(s); return Number.isNaN(n) ? null : n; };
|
|
254
|
+
let latest;
|
|
255
|
+
for (const d of record.decisions) {
|
|
256
|
+
if (d.contentHash !== record.contentHash)
|
|
257
|
+
continue;
|
|
258
|
+
if (d.decisionKind !== 'promote' && d.decisionKind !== 'rollback')
|
|
259
|
+
continue;
|
|
260
|
+
if (latest === undefined) {
|
|
261
|
+
latest = d;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const tcur = ms(d.decidedAt);
|
|
265
|
+
const tlat = ms(latest.decidedAt);
|
|
266
|
+
const newer = tcur !== null && tlat !== null ? tcur >= tlat : d.decidedAt >= latest.decidedAt;
|
|
267
|
+
if (newer)
|
|
268
|
+
latest = d;
|
|
269
|
+
}
|
|
270
|
+
return latest?.decisionKind === 'promote';
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* 追加一条人工管理决定(append-only,promote/reject/rollback 走此路)。与 evidence 同样**不能走 upsert**
|
|
274
|
+
* (`mergeManagedRecord` 刻意保留旧 decisions、丢弃 next.decisions),必须独立 load→push→原子重写。
|
|
275
|
+
*
|
|
276
|
+
* 幂等:对**当前内容**的 promote/rollback,看是否会改变当前 promoted 态——已 promoted 再 promote、未 promoted
|
|
277
|
+
* 再 rollback 都不追加、原样返回(由 CLI 提示「已 promoted」/「未 promoted」)。这让重复 `omk promote` /
|
|
278
|
+
* `omk rollback` 不堆冗余事件,但 promote↔rollback 的真实切换、以及换了内容(contentHash 变)后的新决定仍会
|
|
279
|
+
* 追加——正是要的版本史。其它 decisionKind(如 reject)仍按「同 kind + 同 contentHash 即 dedup」。记录不存在
|
|
280
|
+
* (未 install)返回 null,与 evidence 同口径(管理是 install 显式 opt-in,绝不为未纳管 skill 凭空建记录)。
|
|
281
|
+
*/
|
|
282
|
+
export function appendManagedDecision(dir, recordId, decision) {
|
|
283
|
+
const prev = loadManagedRecord(dir, recordId);
|
|
284
|
+
if (!prev)
|
|
285
|
+
return null;
|
|
286
|
+
const isPromotion = decision.decisionKind === 'promote' || decision.decisionKind === 'rollback';
|
|
287
|
+
const onCurrent = decision.contentHash !== undefined && decision.contentHash === prev.contentHash;
|
|
288
|
+
const dup = isPromotion && onCurrent
|
|
289
|
+
// promote/rollback 当前内容:不改变 promoted 态 → no-op(promote 当已 promoted、rollback 当未 promoted)。
|
|
290
|
+
? (decision.decisionKind === 'promote') === isCurrentlyPromoted(prev)
|
|
291
|
+
: decision.contentHash !== undefined
|
|
292
|
+
&& prev.decisions.some((d) => d.decisionKind === decision.decisionKind && d.contentHash === decision.contentHash);
|
|
293
|
+
const merged = dup
|
|
294
|
+
? prev
|
|
295
|
+
: { ...prev, decisions: [...prev.decisions, decision] };
|
|
296
|
+
if (!dup) {
|
|
297
|
+
mkdirSync(dir, { recursive: true });
|
|
298
|
+
const path = recordPath(dir, recordId);
|
|
299
|
+
const tmp = `${path}.tmp.${process.pid}.${Date.now()}`;
|
|
300
|
+
writeFileSync(tmp, JSON.stringify(merged, null, 2));
|
|
301
|
+
renameSync(tmp, path);
|
|
302
|
+
}
|
|
303
|
+
return merged;
|
|
304
|
+
}
|
|
184
305
|
/**
|
|
185
306
|
* 统一的记录构造点——所有消费方(CLI / 未来 server / SDK)经此组装,保证 schema 一致。
|
|
186
307
|
* 只接收事实(身份、源、hash、分发落点);evidence/decisions 恒为空(eval/promote 的地盘)。
|
|
@@ -207,9 +328,10 @@ export function recordManagedArtifact(record, opts = {}) {
|
|
|
207
328
|
/**
|
|
208
329
|
* 读时推导生命周期标签。
|
|
209
330
|
* - 源缺失或 hash 漂(当前内容 ≠ 记录 contentHash)→ stale;
|
|
331
|
+
* - 否则当前内容已有 promote 决定 → promoted;
|
|
210
332
|
* - 否则有**当前**证据 / samples → measurable;
|
|
211
333
|
* - 否则 installed。
|
|
212
|
-
*
|
|
334
|
+
* 「当前证据 / 当前决定」= contentHash 与记录当前 contentHash 匹配的 evidence / decision —— 旧内容的不算数,
|
|
213
335
|
* 这正是 #203「证据必须跟 artifact 一起走」的读时保证。`'discovered'` 留给无分发的记录,此函数不产生。
|
|
214
336
|
*/
|
|
215
337
|
export function deriveManagedState(input) {
|
|
@@ -218,6 +340,10 @@ export function deriveManagedState(input) {
|
|
|
218
340
|
const drifted = currentContentHash === undefined || currentContentHash !== record.contentHash;
|
|
219
341
|
if (drifted)
|
|
220
342
|
return { label: 'stale', drifted: true, hasEvidence };
|
|
343
|
+
// 当前内容(contentHash 匹配)最近一条 promote/rollback 决定是 promote → 已人工接受当前版本,排在 measurable
|
|
344
|
+
// 之上。rollback 之后落回 measurable/installed;换了内容(contentHash 变)后旧决定不冒充当前。
|
|
345
|
+
if (isCurrentlyPromoted(record))
|
|
346
|
+
return { label: 'promoted', drifted: false, hasEvidence };
|
|
221
347
|
if (hasEvidence || hasSamplesOrDoctorPass)
|
|
222
348
|
return { label: 'measurable', drifted: false, hasEvidence };
|
|
223
349
|
return { label: 'installed', drifted: false, hasEvidence };
|
|
@@ -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');
|