oh-my-knowledge 0.34.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 +3 -0
- package/README.zh.md +3 -0
- package/dist/assets/agent-skills/omk/SKILL.md +2 -2
- package/dist/assets/agent-skills/omk/references/commands.md +45 -0
- package/dist/authoring/evolver.js +13 -4
- package/dist/cli/commands/doctor.js +2 -1
- package/dist/cli/commands/eval/index.d.ts +1 -0
- package/dist/cli/commands/eval/index.js +40 -5
- package/dist/cli/commands/install.d.ts +2 -0
- package/dist/cli/commands/install.js +45 -20
- package/dist/cli/commands/list.d.ts +46 -0
- package/dist/cli/commands/list.js +243 -0
- package/dist/cli/commands/sample.d.ts +1 -1
- package/dist/cli/commands/sample.js +25 -12
- package/dist/cli/lib/cmd-flags.d.ts +1 -0
- package/dist/cli/lib/i18n-dict/install.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/install.js +20 -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/run.d.ts +1 -1
- package/dist/cli/lib/i18n-dict/run.js +8 -0
- package/dist/cli/lib/i18n-dict.d.ts +2 -1
- package/dist/cli/lib/i18n-dict.js +2 -0
- package/dist/cli/lib/parse-run-config/variant-resolution.js +7 -0
- package/dist/cli/lib/run-tally.js +1 -1
- package/dist/cli/lib/shared.js +1 -1
- package/dist/doctor/index.js +6 -6
- package/dist/eval-core/cache.d.ts +11 -3
- package/dist/eval-core/cache.js +13 -5
- package/dist/eval-core/dependency-checker.js +5 -3
- package/dist/eval-core/evaluation-execution.js +4 -1
- package/dist/eval-core/evaluation-reporting.d.ts +1 -0
- package/dist/eval-core/evaluation-reporting.js +20 -9
- package/dist/eval-core/execution-strategy.js +6 -4
- package/dist/eval-core/task-planner.js +2 -1
- package/dist/eval-workflows/batch-evaluation-workflow.js +3 -2
- package/dist/eval-workflows/evaluation-preparation.js +3 -1
- package/dist/eval-workflows/run-evaluation.js +2 -2
- package/dist/inputs/content-hash.d.ts +28 -0
- package/dist/inputs/content-hash.js +106 -0
- package/dist/inputs/eval-config.js +44 -11
- package/dist/inputs/materialize-copy.d.ts +37 -0
- package/dist/inputs/materialize-copy.js +193 -0
- package/dist/inputs/skill-loader.d.ts +65 -2
- package/dist/inputs/skill-loader.js +314 -16
- package/dist/inputs/source-resolver.d.ts +16 -9
- package/dist/inputs/source-resolver.js +57 -77
- package/dist/managed/evidence.d.ts +22 -0
- package/dist/managed/evidence.js +143 -0
- package/dist/managed/index.d.ts +2 -0
- package/dist/managed/index.js +2 -0
- package/dist/managed/list-view.d.ts +55 -0
- package/dist/managed/list-view.js +66 -0
- package/dist/managed/store.d.ts +16 -20
- package/dist/managed/store.js +85 -84
- 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/renderer/layout.js +2 -2
- 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/eval.d.ts +12 -1
- package/dist/types/managed.d.ts +29 -4
- package/dist/types/observability.d.ts +6 -6
- package/dist/types/report.d.ts +24 -5
- package/package.json +3 -3
|
@@ -1,40 +1,56 @@
|
|
|
1
|
-
import { existsSync, lstatSync,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { isDistributablePath } from '../managed/index.js';
|
|
5
|
-
import { classifyGitSkillRef, parseGitInput, resolveGitRepoContext, gitJoin, gitLsTreeBlobs, gitShowBytes, skillNameFromPath } from './skill-loader.js';
|
|
6
|
-
export class SourceResolveError extends Error {
|
|
7
|
-
messageKey;
|
|
8
|
-
params;
|
|
9
|
-
constructor(messageKey, params = {}) {
|
|
10
|
-
super(messageKey);
|
|
11
|
-
this.name = 'SourceResolveError';
|
|
12
|
-
this.messageKey = messageKey;
|
|
13
|
-
this.params = params;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
const noop = () => { };
|
|
1
|
+
import { existsSync, lstatSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
import { classifyGitSkillRef, parseGitInput, resolveGitRepoContext, materializeGitSkillTree, skillNameFromPath, SourceResolveError, fetchRemoteGitRef } from './skill-loader.js';
|
|
17
4
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
5
|
+
* 安装源解析器 —— 把"各种源"物化成统一的本地形态,让 install / managed 主干**源无关**。
|
|
6
|
+
* 新增一种源 = 加一个分支,install.ts 不动。本模块不依赖 CLI(不打印、不 tCli):错误以
|
|
7
|
+
* `SourceResolveError(messageKey)` 抛出,由调用方(install)映射成本地化文案。
|
|
8
|
+
*
|
|
9
|
+
* 当前支持:`file`(本地路径)、`git`(当前仓库的某个 ref)、远端 git(URL + ref,fetch 到临时 bare)。
|
|
10
|
+
* 三者沿同一 `ResolvedSource` 契约,install / managed 主干源无关。
|
|
21
11
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
const root = resolve(temp);
|
|
28
|
-
const dest = resolve(temp, relPath);
|
|
29
|
-
if (dest !== root && !dest.startsWith(root + sep)) {
|
|
30
|
-
throw new SourceResolveError('cli.install.git_unsafe_path', { path: relPath });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
12
|
+
// SourceResolveError 住在 skill-loader(让共享 materializeGitSkillTree 能抛它而不成环);此处 re-export
|
|
13
|
+
// 保持 install 既有 import 不破。
|
|
14
|
+
export { SourceResolveError } from './skill-loader.js';
|
|
15
|
+
const noop = () => { };
|
|
33
16
|
export function resolveInstallSource(input) {
|
|
34
17
|
if (input.startsWith('git:'))
|
|
35
18
|
return resolveGitSource(input);
|
|
36
19
|
return resolveFileSource(input);
|
|
37
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* 远端 git 源:`<url>` + `<ref>` + repo 相对 `<spec>`(三者结构化传入,URL 不经任何字符串切分,
|
|
23
|
+
* 避开 `parseGitInput` 的 `:` 与 `parseVariantCwd` 的 `@`)。fetch 到临时 bare → 复用既有
|
|
24
|
+
* classifyGitSkillRef / materializeGitSkillTree(repoRoot 换成 bare)。locator 落 `git+<url>@<sha>:<spec>`
|
|
25
|
+
* 仅作记录身份;url 另以结构化字段携带,供 drift 重取。失败 fail-closed 并清临时资源。
|
|
26
|
+
*/
|
|
27
|
+
export function resolveRemoteGitSource(url, ref, spec) {
|
|
28
|
+
const checkout = fetchRemoteGitRef(url, ref);
|
|
29
|
+
try {
|
|
30
|
+
// 远端 URL 指向仓库根,spec 是仓库相对路径 → gitRelDir 为空。
|
|
31
|
+
const resolved = classifyGitSkillRef(checkout.ref, '', spec, checkout.repoRoot);
|
|
32
|
+
if (!resolved)
|
|
33
|
+
throw new SourceResolveError('cli.install.remote_skill_not_found', { url, ref: checkout.ref, name: spec });
|
|
34
|
+
const mat = materializeGitSkillTree(checkout.ref, resolved, checkout.repoRoot);
|
|
35
|
+
return {
|
|
36
|
+
sourceKind: 'git',
|
|
37
|
+
localRoot: mat.localRoot,
|
|
38
|
+
name: mat.name,
|
|
39
|
+
isDirectorySkill: mat.isDirectorySkill,
|
|
40
|
+
locator: `git+${url}@${checkout.ref}:${spec}`,
|
|
41
|
+
ref: checkout.ref,
|
|
42
|
+
url,
|
|
43
|
+
cleanup: () => {
|
|
44
|
+
mat.cleanup();
|
|
45
|
+
checkout.cleanup();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
checkout.cleanup();
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
38
54
|
function resolveFileSource(input) {
|
|
39
55
|
const abs = resolve(input);
|
|
40
56
|
if (!existsSync(abs))
|
|
@@ -74,52 +90,16 @@ function resolveGitSource(input) {
|
|
|
74
90
|
if (!resolved) {
|
|
75
91
|
throw new SourceResolveError('cli.install.git_skill_not_found', { ref, name: spec });
|
|
76
92
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
// 物化复用共享 materializeGitSkillTree(install 与 eval 同一条物化路径);install 只需在其上补
|
|
94
|
+
// locator / sourceKind 包成 ResolvedSource。
|
|
95
|
+
const mat = materializeGitSkillTree(ref, resolved, ctx.repoRoot);
|
|
96
|
+
return {
|
|
97
|
+
sourceKind: 'git',
|
|
98
|
+
localRoot: mat.localRoot,
|
|
99
|
+
name: mat.name,
|
|
100
|
+
isDirectorySkill: mat.isDirectorySkill,
|
|
101
|
+
locator: `git:${ref}:${spec}`,
|
|
102
|
+
ref,
|
|
103
|
+
cleanup: mat.cleanup,
|
|
85
104
|
};
|
|
86
|
-
try {
|
|
87
|
-
const locator = `git:${ref}:${spec}`;
|
|
88
|
-
if (resolved.isDir) {
|
|
89
|
-
for (const entry of gitLsTreeBlobs(ref, resolved.treePath, ctx.repoRoot)) {
|
|
90
|
-
// 安全边界 fail closed:git tree 可被 git mktree 手工构造出名为 `..` 的子树,`ls-tree -r` 会
|
|
91
|
-
// 吐 `../evil.txt`,`join(temp, ...)` 会逃出临时目录写盘、cleanup 也删不掉。任一越界路径(.. /
|
|
92
|
-
// 绝对路径 / 空段 / 解析后不在 temp 内)直接抛错,而非静默跳过——静默跳过会让物化树与真实
|
|
93
|
-
// git tree / hash / 分发树发散。正常 checkout 永不触发。
|
|
94
|
-
assertContainedRelPath(temp, entry.path);
|
|
95
|
-
if (entry.mode === '120000' || entry.mode === '160000')
|
|
96
|
-
continue; // 跳过软链 / submodule(与本地分发一致)
|
|
97
|
-
if (!isDistributablePath(entry.path.split('/')))
|
|
98
|
-
continue; // 排除 .omk/.git/evolve 等
|
|
99
|
-
const bytes = gitShowBytes(ref, gitJoin(resolved.treePath, entry.path), ctx.repoRoot);
|
|
100
|
-
if (!bytes)
|
|
101
|
-
continue;
|
|
102
|
-
const dest = join(temp, entry.path);
|
|
103
|
-
mkdirSync(dirname(dest), { recursive: true });
|
|
104
|
-
// 保留可执行位(与本地 cpSync 一致);其余 0644。
|
|
105
|
-
writeFileSync(dest, bytes, { mode: entry.mode === '100755' ? 0o755 : 0o644 });
|
|
106
|
-
}
|
|
107
|
-
// 纵深防御:classify 与物化用的是两条 git 路径(gitShowFile vs ls-tree),万一发散(如 treePath 退化)
|
|
108
|
-
// 导致空树,这里失败而非静默分发一个空 skill。
|
|
109
|
-
if (!existsSync(join(temp, 'SKILL.md'))) {
|
|
110
|
-
throw new SourceResolveError('cli.install.git_skill_not_found', { ref, name: spec });
|
|
111
|
-
}
|
|
112
|
-
return { sourceKind: 'git', localRoot: temp, name: resolved.name, isDirectorySkill: true, locator, ref, cleanup };
|
|
113
|
-
}
|
|
114
|
-
const bytes = gitShowBytes(ref, resolved.fileSkillPath, ctx.repoRoot);
|
|
115
|
-
if (!bytes)
|
|
116
|
-
throw new SourceResolveError('cli.install.git_skill_not_found', { ref, name: spec });
|
|
117
|
-
const dest = join(temp, `${resolved.name}.md`);
|
|
118
|
-
writeFileSync(dest, bytes);
|
|
119
|
-
return { sourceKind: 'git', localRoot: dest, name: resolved.name, isDirectorySkill: false, locator, ref, cleanup };
|
|
120
|
-
}
|
|
121
|
-
catch (err) {
|
|
122
|
-
cleanup();
|
|
123
|
-
throw err;
|
|
124
|
-
}
|
|
125
105
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EvaluationReport, ManagedEvidenceRef } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 为某个变体组装一条 evidence ref;变体无真实内容(baseline / no-skill / 缺 hash)→ null。
|
|
4
|
+
* `verdict` 由调用方传入(CLI 已 computeVerdict,避免在此重算)。
|
|
5
|
+
*/
|
|
6
|
+
export declare function buildEvidenceRef(report: EvaluationReport, variant: string, verdict: string, recordedAt: string): ManagedEvidenceRef | null;
|
|
7
|
+
export interface RecordedEvidence {
|
|
8
|
+
recordId: string;
|
|
9
|
+
name: string;
|
|
10
|
+
contentHash: string;
|
|
11
|
+
/** true ⇒ evidence.contentHash 等于记录当前 contentHash → deriveManagedState 计为当前证据 →
|
|
12
|
+
* measurable。false ⇒ 测的是旧内容(已 drift),证据留存但不绑当前版本。 */
|
|
13
|
+
bound: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* 驱动:对每个能在报告里(按 contentHash 主键 / 同名回退)匹配到被测 variant 的**已纳管**记录,
|
|
17
|
+
* 追加一条 evidence。返回实际写入的清单(供 CLI 提示,`name` 取受管记录名而非 variant 键)。
|
|
18
|
+
* 无任何记录匹配 → 返回空(常见的非管理用户场景,静默无副作用)。
|
|
19
|
+
*/
|
|
20
|
+
export declare function recordEvalEvidence(report: EvaluationReport, verdict: string, recordedAt: string, opts?: {
|
|
21
|
+
dir?: string;
|
|
22
|
+
}): RecordedEvidence[];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* eval → managed evidence 写入(承接 #203 管理支柱 / #214 验收第 3 条)。
|
|
3
|
+
*
|
|
4
|
+
* 一次 `omk eval` 跑完,把结果落成一条 `ManagedEvidenceRef` 追加进**已纳管**记录的 evidence[],
|
|
5
|
+
* 让 skill 从 `installed` 走到 `measurable`(由 `deriveManagedState` 读时推导)。承重前提是 #214/#218
|
|
6
|
+
* 已把 install 与 eval 的内容指纹统一到同一空间(整树哈),所以 `report.artifactHashes[variant]` 与
|
|
7
|
+
* `record.contentHash` 可直接相等比对、evidence 绑得上。
|
|
8
|
+
*
|
|
9
|
+
* 三条设计取舍(对应 issue #221 的"待定决策"):
|
|
10
|
+
* - **触发**:eval 完成**自动**写,但只写**已存在**的受管记录 —— install 是显式 opt-in,未纳管的
|
|
11
|
+
* skill 永不被凭空建记录(零副作用惊吓)。CLI 另给 `--no-evidence` 关闭。
|
|
12
|
+
* - **多对一**:append-only + 按 (reportId, contentHash) 去重;保留全部历史条目,当前有效性仍由
|
|
13
|
+
* `deriveManagedState` 按 contentHash 匹配裁定(重装新内容后旧证据留存供回滚,却不让新内容显得已测)。
|
|
14
|
+
* - **跨源**:用三级身份消歧把被测 variant 对到受管记录(install 与 eval 命名不一致:记录名是
|
|
15
|
+
* skill 短名 `review`,而 eval 报告 variant key 可能是整串表达式 `git:HEAD:skills/review`、
|
|
16
|
+
* eval.yaml 别名 `candidate`、blind 模式的 `A`/`B`)。`applyBlindMode` 盲化 `variants` 但**不**动
|
|
17
|
+
* `artifactHashes` / `variantConfigs` 的键面,故三级都按真实键工作:
|
|
18
|
+
* (a) 显式**同名** variant —— 最强身份(本地 `--treatment <name>` 与 drift);
|
|
19
|
+
* (b) **结构化源匹配** —— `variantConfigs[].locator(+ref)` 与 `record.source` 对齐(git / 远端 /
|
|
20
|
+
* 别名),即便内容撞哈也能精确消歧;
|
|
21
|
+
* (c) **纯 contentHash 回退** —— 仅当该内容哈在受管记录中**唯一**才用。否则两条不同记录恰好当前
|
|
22
|
+
* 内容相同(同模板复制 / 刚装)时,只测了一条的 report 会把同哈的另一条也推成 measurable ——
|
|
23
|
+
* 唯一性闸门挡掉这种越权写入(撞哈又非同名 / 无结构化身份 → 跳过,不乱绑)。
|
|
24
|
+
*
|
|
25
|
+
* bundle 按 evidence-gated-management.md §5 denormalize 进记录(reportId / contentHash /
|
|
26
|
+
* verdict / sampleCoverage / comparability),不依赖 report 文件仍在盘。
|
|
27
|
+
*/
|
|
28
|
+
import { basename, dirname } from 'node:path';
|
|
29
|
+
import { hashString } from '../eval-core/evaluation-reporting.js';
|
|
30
|
+
import { loadAllManagedRecords, appendManagedEvidence, managedDir, resolveManagedDir } from './store.js';
|
|
31
|
+
/** baseline / 无 skill 变体的 artifactHash 哨兵(见 report.ts artifactHashes 注释)——不产证据。 */
|
|
32
|
+
const NO_SKILL = 'no-skill';
|
|
33
|
+
/** 样本集覆盖摘要:report 的 sampleHashes 排序后取一个稳定 digest(同一样本集 ⇒ 同 hash)。
|
|
34
|
+
* 缺 sampleHashes(旧报告 / --dry-run)→ undefined,bundle 仍含其余三项 mandatory。 */
|
|
35
|
+
function sampleCoverage(report) {
|
|
36
|
+
const sh = report.meta?.sampleHashes;
|
|
37
|
+
if (!sh)
|
|
38
|
+
return undefined;
|
|
39
|
+
const entries = Object.entries(sh).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
40
|
+
return { count: entries.length, hash: hashString(JSON.stringify(entries)) };
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* 为某个变体组装一条 evidence ref;变体无真实内容(baseline / no-skill / 缺 hash)→ null。
|
|
44
|
+
* `verdict` 由调用方传入(CLI 已 computeVerdict,避免在此重算)。
|
|
45
|
+
*/
|
|
46
|
+
export function buildEvidenceRef(report, variant, verdict, recordedAt) {
|
|
47
|
+
const contentHash = report.meta?.artifactHashes?.[variant];
|
|
48
|
+
if (!contentHash || contentHash === NO_SKILL)
|
|
49
|
+
return null;
|
|
50
|
+
const meta = report.meta;
|
|
51
|
+
const cov = sampleCoverage(report);
|
|
52
|
+
return {
|
|
53
|
+
reportId: report.id,
|
|
54
|
+
contentHash,
|
|
55
|
+
recordedAt,
|
|
56
|
+
verdict,
|
|
57
|
+
...(cov ? { sampleCoverage: cov } : {}),
|
|
58
|
+
comparability: {
|
|
59
|
+
cliVersion: meta.cliVersion,
|
|
60
|
+
...(meta.judgePromptHash ? { judgePromptHash: meta.judgePromptHash } : {}),
|
|
61
|
+
...(meta.debiasMode ? { debiasMode: meta.debiasMode } : {}),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* variantConfig 的源身份是否与受管记录 source 对得上 —— install 与 eval 两侧 locator **口径不同**,
|
|
67
|
+
* 必须先归一化再比,不能裸字符串相等(否则同 hash 多记录场景下正确的本地 git / 目录-skill 也会因
|
|
68
|
+
* locator 不等而漏写):
|
|
69
|
+
* - 直接相等:远端 git(两侧都 `git+<url>@<sha>:<spec>`)、本地 file-skill(两侧都是 .md 路径);
|
|
70
|
+
* - 本地 git:install 记完整身份串 `git:<ref>:<spec>`,eval 把 spec 落 `cfg.locator`、ref 另存 `cfg.ref`
|
|
71
|
+
* → 归一化成 `git:<cfg.ref>:<cfg.locator>` 再比;
|
|
72
|
+
* - 本地目录-skill:install 记目录根,eval 记 `<dir>/SKILL.md` → 比 `dirname(cfg.locator)`。
|
|
73
|
+
*/
|
|
74
|
+
function sourceMatches(cfg, source) {
|
|
75
|
+
if (!cfg.locator)
|
|
76
|
+
return false;
|
|
77
|
+
if (cfg.locator === source.locator)
|
|
78
|
+
return true;
|
|
79
|
+
if (source.sourceKind === 'git' && cfg.ref && `git:${cfg.ref}:${cfg.locator}` === source.locator)
|
|
80
|
+
return true;
|
|
81
|
+
if (source.isDirectorySkill && basename(cfg.locator) === 'SKILL.md' && dirname(cfg.locator) === source.locator)
|
|
82
|
+
return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 给某条受管记录在报告里找该绑定的 variant key,三级身份消歧(见文件头「跨源」)。
|
|
87
|
+
* `hashIsUnique` 由调用方按全体受管记录的 contentHash 计数提供 —— 纯 hash 回退的越权闸门。
|
|
88
|
+
*/
|
|
89
|
+
function matchVariantKey(report, record, hashIsUnique) {
|
|
90
|
+
const hashes = report.meta?.artifactHashes ?? {};
|
|
91
|
+
// (a) 显式同名 variant(本地 --treatment <name> / drift)。
|
|
92
|
+
if (hashes[record.name] && hashes[record.name] !== NO_SKILL)
|
|
93
|
+
return record.name;
|
|
94
|
+
// (b) 结构化源匹配:variantConfig.locator(+ref) 对齐记录 source(git / 远端 / 别名)。
|
|
95
|
+
for (const cfg of report.meta?.variantConfigs ?? []) {
|
|
96
|
+
const h = hashes[cfg.variant];
|
|
97
|
+
if (!h || h === NO_SKILL)
|
|
98
|
+
continue;
|
|
99
|
+
if (sourceMatches(cfg, record.source))
|
|
100
|
+
return cfg.variant;
|
|
101
|
+
}
|
|
102
|
+
// (c) 纯 contentHash 回退 —— 仅当该哈在受管记录中唯一(否则撞哈会越权写到没测的记录)。
|
|
103
|
+
if (hashIsUnique(record.contentHash)) {
|
|
104
|
+
for (const [key, hash] of Object.entries(hashes)) {
|
|
105
|
+
if (hash !== NO_SKILL && hash === record.contentHash)
|
|
106
|
+
return key;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 驱动:对每个能在报告里(按 contentHash 主键 / 同名回退)匹配到被测 variant 的**已纳管**记录,
|
|
113
|
+
* 追加一条 evidence。返回实际写入的清单(供 CLI 提示,`name` 取受管记录名而非 variant 键)。
|
|
114
|
+
* 无任何记录匹配 → 返回空(常见的非管理用户场景,静默无副作用)。
|
|
115
|
+
*/
|
|
116
|
+
export function recordEvalEvidence(report, verdict, recordedAt, opts = {}) {
|
|
117
|
+
const out = [];
|
|
118
|
+
if (Object.keys(report.meta?.artifactHashes ?? {}).length === 0)
|
|
119
|
+
return out;
|
|
120
|
+
// 写回读方实际取记录的同一目录(project→global 同口径)。
|
|
121
|
+
const dir = resolveManagedDir(opts.dir ?? managedDir());
|
|
122
|
+
const records = loadAllManagedRecords(dir);
|
|
123
|
+
if (records.length === 0)
|
|
124
|
+
return out;
|
|
125
|
+
// 全体记录的 contentHash 计数 —— 纯 hash 回退只在唯一时放行(挡同内容多记录的越权写)。
|
|
126
|
+
const hashCount = new Map();
|
|
127
|
+
for (const r of records)
|
|
128
|
+
hashCount.set(r.contentHash, (hashCount.get(r.contentHash) ?? 0) + 1);
|
|
129
|
+
const hashIsUnique = (hash) => (hashCount.get(hash) ?? 0) === 1;
|
|
130
|
+
for (const rec of records) {
|
|
131
|
+
const variantKey = matchVariantKey(report, rec, hashIsUnique);
|
|
132
|
+
if (!variantKey)
|
|
133
|
+
continue;
|
|
134
|
+
const ref = buildEvidenceRef(report, variantKey, verdict, recordedAt);
|
|
135
|
+
if (!ref)
|
|
136
|
+
continue;
|
|
137
|
+
const merged = appendManagedEvidence(dir, rec.id, ref);
|
|
138
|
+
if (!merged)
|
|
139
|
+
continue;
|
|
140
|
+
out.push({ recordId: rec.id, name: rec.name, contentHash: ref.contentHash, bound: ref.contentHash === merged.contentHash });
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
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.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ArtifactKind, DeriveManagedStateInput, DerivedManagedState, ManagedArtifactRecord, ManagedArtifactSource, ManagedDistributionTarget } from '../types/index.js';
|
|
1
|
+
import type { ArtifactKind, DeriveManagedStateInput, DerivedManagedState, ManagedArtifactRecord, ManagedArtifactSource, 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、不丢别人、
|
|
@@ -14,28 +14,16 @@ export declare function globalManagedDir(): string;
|
|
|
14
14
|
export declare function recordPath(dir: string, id: string): string;
|
|
15
15
|
/** 稳定身份 = hash(kind, name)。源路径是可变属性、不进 id。kind 取自固定枚举(无 `|`),分隔可注入。 */
|
|
16
16
|
export declare function managedRecordId(kind: ArtifactKind, name: string): string;
|
|
17
|
-
|
|
18
|
-
* 给定相对源根的路径分段(空数组 = 源根本身),判断是否进可分发树。hash 的 walk、copy 的 filter、
|
|
19
|
-
* git 物化共用此一处,保证三者完全一致。
|
|
20
|
-
*
|
|
21
|
-
* 关键:检查**每一段**而非只看叶子。本地 walk / cpSync 是逐层下降、命中目录即剪枝,只看叶子也够;
|
|
22
|
-
* 但 git ls-tree 给的是**扁平路径**(如 `.omk/samples.json`),只看叶子 `samples.json` 会让 `.omk`
|
|
23
|
-
* 内容漏过。故:任一段命中全局排除即排除;首段命中 root-only(evolve)即排除(嵌套同名是合法资产)。
|
|
24
|
-
*/
|
|
25
|
-
export declare function isDistributablePath(segments: string[]): boolean;
|
|
26
|
-
/**
|
|
27
|
-
* artifact 内容 hash —— drift baseline 与 evidence 绑定的依据。
|
|
28
|
-
* - 文件-skill:单个 .md 的字节;
|
|
29
|
-
* - 目录-skill:覆盖**整棵可分发目录树**(SKILL.md + references/ 等资产,但排除 .omk / .git /
|
|
30
|
-
* evolve 等评测迭代产物),按相对路径排序后把每个文件的`路径 + 字节长度 + 内容`喂进同一个
|
|
31
|
-
* sha256 —— 改任意资产都会令 hash 变化、drift 不漏;只补样本则 hash 不动。
|
|
32
|
-
* 用 createHash 直接喂 Buffer(字节级,二进制资产也稳;分隔符是运行时字节,源码里不引入任何不可见字符)。
|
|
33
|
-
* 读时(未来 list / drift 检查)用同一函数重算比对。
|
|
34
|
-
*/
|
|
35
|
-
export declare function hashArtifactSource(source: string, isDirectorySkill: boolean): string;
|
|
17
|
+
export { hashArtifactSource, isDistributablePath, distributableCopyFilter } from '../inputs/content-hash.js';
|
|
36
18
|
export declare function loadManagedRecord(dir: string, id: string): ManagedArtifactRecord | null;
|
|
37
19
|
/** 读全部记录。项目目录空 → 兜底全局(镜像 observe inbox 的 project→global)。 */
|
|
38
20
|
export declare function loadAllManagedRecords(dir?: string): ManagedArtifactRecord[];
|
|
21
|
+
/**
|
|
22
|
+
* 哪个 managed 目录是权威(有记录的那个):项目目录非空取项目,否则全局非空取全局,都空回项目。
|
|
23
|
+
* 与 `loadAllManagedRecords` 的 project→global 回退**同口径** —— 写方(append evidence)据此写回
|
|
24
|
+
* 读方实际取记录的同一目录,避免"读全局、写项目"把证据落到空目录。
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveManagedDir(dir?: string): string;
|
|
39
27
|
/**
|
|
40
28
|
* 纯合并逻辑(无 IO):install 写的是事实,绝不动 evidence/decisions(那是 eval/promote 的地盘)。
|
|
41
29
|
* - distribution 按 path 去重(同路径以新值替换);
|
|
@@ -48,6 +36,14 @@ export declare function loadAllManagedRecords(dir?: string): ManagedArtifactReco
|
|
|
48
36
|
export declare function mergeManagedRecord(prev: ManagedArtifactRecord | null, next: ManagedArtifactRecord): ManagedArtifactRecord;
|
|
49
37
|
/** 读旧记录 → 合并 → 原子 tmp+rename 只写该 id 的文件。返回合并后记录。 */
|
|
50
38
|
export declare function upsertManagedRecord(dir: string, record: ManagedArtifactRecord): ManagedArtifactRecord;
|
|
39
|
+
/**
|
|
40
|
+
* 追加一条评测证据(append-only)。与 install 的 upsert 路径相反——`mergeManagedRecord` 刻意**保留旧
|
|
41
|
+
* evidence、丢弃 next.evidence**(install 只写事实),所以证据写入不能走 upsert,必须独立 load→push→
|
|
42
|
+
* 原子重写。按 `(reportId, contentHash)` 去重:重跑同一份 eval 不堆重复条目(reportId 含运行身份,
|
|
43
|
+
* 同内容重测会是新 reportId → 新条目,正是要的版本史)。记录不存在(未 install / 名字不匹配)返回
|
|
44
|
+
* null —— 这是"管理是 install 显式 opt-in"的体现:eval 绝不为未纳管的 skill 凭空建记录。
|
|
45
|
+
*/
|
|
46
|
+
export declare function appendManagedEvidence(dir: string, recordId: string, evidence: ManagedEvidenceRef): ManagedArtifactRecord | null;
|
|
51
47
|
/**
|
|
52
48
|
* 统一的记录构造点——所有消费方(CLI / 未来 server / SDK)经此组装,保证 schema 一致。
|
|
53
49
|
* 只接收事实(身份、源、hash、分发落点);evidence/decisions 恒为空(eval/promote 的地盘)。
|