@bolloon/bolloon-agent 0.3.1 → 0.3.4

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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * judgeness · reflect.ts
3
+ *
4
+ * description ↔ HumanJudgment 的双向反射:
5
+ * - reflectFromJudgment: 给一个 hv-id, 生成或更新对应 jd-id (5 维默认 unset, basis 空)
6
+ * - mergeIntoJudgment: 给一个 jd-id, 反向把 facets 写到 judgment 的 metadata (附加, 不破坏 v0)
7
+ *
8
+ * 关键不变量: 不动 HumanJudgment 主表 schema. 所有 facets 通过 metadata.judgeness_* 字段挂上.
9
+ * 这样老 v0 数据可被自动视为 judgeness_v0, 零迁移.
10
+ */
11
+ import { newDescriptionId, loadDescription, saveDescription, findDescriptionByJudgmentRef } from './store.js';
12
+ /** 给定 HumanJudgment, 生成 (或更新) JudgenessDescription.
13
+ * 若已存在同 judgmentRef 的 jd, 则 facets/basis 合并 (覆盖优先). */
14
+ export async function reflectFromJudgment(opts) {
15
+ const existing = await findDescriptionByJudgmentRef(opts.judgment.id);
16
+ const now = new Date().toISOString();
17
+ if (existing) {
18
+ const merged = {
19
+ ...existing,
20
+ facets: { ...existing.facets, ...(opts.facets ?? {}) },
21
+ basis: { ...existing.basis, ...(opts.basis ?? {}) },
22
+ scope: {
23
+ domains: opts.scopeDomains ?? existing.scope.domains,
24
+ topics: opts.scopeTopics ?? existing.scope.topics,
25
+ },
26
+ visibility: opts.visibility ?? existing.visibility,
27
+ openState: opts.openState ?? existing.openState,
28
+ updatedAt: now,
29
+ lastTransitionAt: opts.visibility && opts.visibility !== existing.visibility
30
+ ? now
31
+ : opts.openState && opts.openState !== existing.openState
32
+ ? now
33
+ : existing.lastTransitionAt,
34
+ };
35
+ await saveDescription(merged);
36
+ return merged;
37
+ }
38
+ const created = {
39
+ descriptionId: newDescriptionId(),
40
+ judgmentRef: opts.judgment.id,
41
+ description_version: 1,
42
+ facets: {
43
+ judgment: opts.facets?.judgment,
44
+ taste_aesthetic: opts.facets?.taste_aesthetic,
45
+ novelty_score: opts.facets?.novelty_score,
46
+ imaginative_score: opts.facets?.imaginative_score,
47
+ curiosity_vector: opts.facets?.curiosity_vector,
48
+ },
49
+ basis: {
50
+ taste_basis: opts.basis?.taste_basis,
51
+ novelty_basis: opts.basis?.novelty_basis,
52
+ imagination_basis: opts.basis?.imagination_basis,
53
+ },
54
+ scope: { domains: opts.scopeDomains ?? [], topics: opts.scopeTopics ?? [] },
55
+ visibility: opts.visibility ?? 'private',
56
+ openState: opts.openState ?? 'locked',
57
+ by: opts.by,
58
+ byAgentId: opts.byAgentId,
59
+ createdAt: now,
60
+ updatedAt: now,
61
+ lastTransitionAt: now,
62
+ };
63
+ await saveDescription(created);
64
+ return created;
65
+ }
66
+ /** 反向: 从 jd-id 合成一份 metadata 注入用的字段, 不动 HumanJudgment 主表. */
67
+ export function deriveJudgmentMetadataPatch(jdId, jd) {
68
+ return {
69
+ judgeness_ref: jdId,
70
+ judgeness_card_version: jd.description_version,
71
+ judgeness_visibility: jd.visibility,
72
+ judgeness_open_state: jd.openState,
73
+ };
74
+ }
75
+ /** 给一个 hv-id 拿最新 jd (用于 judgment-protocol 的 reflect 钩子).
76
+ * 供 judgment-protocol.ts:463-537 reflect() 在写完 HumanJudgment 后异步触发. */
77
+ export async function reflectAfterJudgment(judgment, by, byAgentId) {
78
+ // 默认: scope=topics 从 context.domain 推出, facets 全 unset.
79
+ return await reflectFromJudgment({
80
+ judgment,
81
+ by,
82
+ byAgentId,
83
+ scopeDomains: [judgment.context.domain],
84
+ scopeTopics: [judgment.context.domain],
85
+ visibility: 'private',
86
+ openState: 'locked',
87
+ });
88
+ }
89
+ /** 工具: 拿一个 jd, 返回它引用的 hv (供 audit) */
90
+ export async function descriptionToJudgmentRef(jdId) {
91
+ const d = await loadDescription(jdId);
92
+ return d?.judgmentRef ?? null;
93
+ }
@@ -0,0 +1,481 @@
1
+ /**
2
+ * judgeness · store.ts
3
+ *
4
+ * 落盘布局 (与 plan §5.1 一致):
5
+ * ~/.bolloon/judgeness/
6
+ * descriptions/<jd-id>.md # 单 description 可读版 (frontmatter v2 + body)
7
+ * tags.yaml # 全局 tags 聚合
8
+ * visibility.yaml # 隐私策略
9
+ * allowlist.yaml # 白名单
10
+ * hearth-cache/<remote-pk>/ # 远端用户缓存
11
+ * manifest.json
12
+ * descriptions/<jd-id>.md
13
+ * last-seen.txt
14
+ */
15
+ import * as fs from 'fs/promises';
16
+ import * as path from 'path';
17
+ import * as os from 'os';
18
+ import * as crypto from 'crypto';
19
+ // ---------------------------------------------------------------------------
20
+ // 路径常量 (每次重读 env, 支持测试隔离)
21
+ // ---------------------------------------------------------------------------
22
+ export function homeDir() {
23
+ return process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
24
+ }
25
+ export function JUDGENESS_ROOT() {
26
+ return path.join(homeDir(), 'judgeness');
27
+ }
28
+ function DESCRIPTIONS_DIR() { return path.join(JUDGENESS_ROOT(), 'descriptions'); }
29
+ function TAGS_FILE() { return path.join(JUDGENESS_ROOT(), 'tags.yaml'); }
30
+ function VISIBILITY_FILE() { return path.join(JUDGENESS_ROOT(), 'visibility.yaml'); }
31
+ function ALLOWLIST_FILE() { return path.join(JUDGENESS_ROOT(), 'allowlist.yaml'); }
32
+ function HEARTH_CACHE_ROOT() { return path.join(JUDGENESS_ROOT(), 'hearth-cache'); }
33
+ // ---------------------------------------------------------------------------
34
+ // id 生成
35
+ // ---------------------------------------------------------------------------
36
+ export function newDescriptionId() {
37
+ const ts = Date.now();
38
+ const rand = crypto.randomBytes(3).toString('hex');
39
+ return `jd-${ts}-${rand}`;
40
+ }
41
+ // ---------------------------------------------------------------------------
42
+ // ensureDirs
43
+ // ---------------------------------------------------------------------------
44
+ export async function ensureJudgenessDirs() {
45
+ await fs.mkdir(JUDGENESS_ROOT(), { recursive: true });
46
+ await fs.mkdir(DESCRIPTIONS_DIR(), { recursive: true });
47
+ await fs.mkdir(HEARTH_CACHE_ROOT(), { recursive: true });
48
+ }
49
+ // ---------------------------------------------------------------------------
50
+ // description 落盘 / 读回 (md with frontmatter v2)
51
+ // ---------------------------------------------------------------------------
52
+ function descriptionMdPath(id) {
53
+ return path.join(DESCRIPTIONS_DIR(), `${id}.md`);
54
+ }
55
+ function escapeYamlString(s) {
56
+ // 单行安全: 包裹单引号 + 转义单引号. 避免依赖 js-yaml.
57
+ return `'${s.replace(/'/g, "''")}'`;
58
+ }
59
+ function safeVer(v) {
60
+ return v === 1 ? 1 : 0;
61
+ }
62
+ function descriptionToMarkdown(desc) {
63
+ const fm = [
64
+ '---',
65
+ `descriptionId: ${desc.descriptionId}`,
66
+ `judgmentRef: ${desc.judgmentRef}`,
67
+ `description_version: ${safeVer(desc.description_version)}`,
68
+ 'facets:',
69
+ desc.facets.judgment !== undefined ? ` judgment: ${desc.facets.judgment}` : ' judgment: null',
70
+ desc.facets.taste_aesthetic !== undefined ? ` taste_aesthetic: ${desc.facets.taste_aesthetic}` : ' taste_aesthetic: null',
71
+ desc.facets.novelty_score !== undefined ? ` novelty_score: ${desc.facets.novelty_score}` : ' novelty_score: null',
72
+ desc.facets.imaginative_score !== undefined ? ` imaginative_score: ${desc.facets.imaginative_score}` : ' imaginative_score: null',
73
+ desc.facets.curiosity_vector !== undefined ? ` curiosity_vector: ${desc.facets.curiosity_vector}` : ' curiosity_vector: null',
74
+ 'basis:',
75
+ desc.basis.taste_basis ? ` taste_basis: ${escapeYamlString(desc.basis.taste_basis)}` : ' taste_basis: null',
76
+ desc.basis.novelty_basis ? ` novelty_basis: ${escapeYamlString(desc.basis.novelty_basis)}` : ' novelty_basis: null',
77
+ desc.basis.imagination_basis ? ` imagination_basis: ${escapeYamlString(desc.basis.imagination_basis)}` : ' imagination_basis: null',
78
+ 'scope:',
79
+ desc.scope.domains && desc.scope.domains.length > 0
80
+ ? ` domains: [${desc.scope.domains.map((d) => escapeYamlString(d)).join(', ')}]`
81
+ : ' domains: []',
82
+ desc.scope.topics && desc.scope.topics.length > 0
83
+ ? ` topics: [${desc.scope.topics.map((d) => escapeYamlString(d)).join(', ')}]`
84
+ : ' topics: []',
85
+ `visibility: ${desc.visibility}`,
86
+ `openState: ${desc.openState}`,
87
+ `by: ${desc.by}`,
88
+ desc.byAgentId ? `byAgentId: ${escapeYamlString(desc.byAgentId)}` : 'byAgentId: null',
89
+ `createdAt: ${escapeYamlString(desc.createdAt)}`,
90
+ `updatedAt: ${escapeYamlString(desc.updatedAt)}`,
91
+ desc.lastTransitionAt ? `lastTransitionAt: ${escapeYamlString(desc.lastTransitionAt)}` : 'lastTransitionAt: null',
92
+ 'schema_version: 2',
93
+ 'audience: self',
94
+ 'stage: current',
95
+ 'status: current',
96
+ `entity_type: concept`,
97
+ `tags: [judgeness, judgment-ref=${desc.judgmentRef}, visibility=${desc.visibility}, state=${desc.openState}]`,
98
+ '---',
99
+ '',
100
+ `# Judgeness Description ${desc.descriptionId}`,
101
+ '',
102
+ `## Judgment Reference`,
103
+ `- judgmentRef: ${desc.judgmentRef}`,
104
+ '',
105
+ `## Facets`,
106
+ `- judgment: ${desc.facets.judgment ?? 'n/a'}`,
107
+ `- taste_aesthetic: ${desc.facets.taste_aesthetic ?? 'n/a'}${desc.basis.taste_basis ? ' — ' + desc.basis.taste_basis : ''}`,
108
+ `- novelty_score: ${desc.facets.novelty_score ?? 'n/a'}${desc.basis.novelty_basis ? ' — ' + desc.basis.novelty_basis : ''}`,
109
+ `- imaginative_score: ${desc.facets.imaginative_score ?? 'n/a'}${desc.basis.imagination_basis ? ' — ' + desc.basis.imagination_basis : ''}`,
110
+ `- curiosity_vector: ${desc.facets.curiosity_vector ?? 'n/a'}`,
111
+ '',
112
+ `## Scope`,
113
+ `- domains: ${(desc.scope.domains ?? []).join(', ') || '(any)'}`,
114
+ `- topics: ${(desc.scope.topics ?? []).join(', ') || '(any)'}`,
115
+ '',
116
+ `## Privacy`,
117
+ `- visibility: ${desc.visibility}`,
118
+ `- openState: ${desc.openState}`,
119
+ `- by: ${desc.by}${desc.byAgentId ? ' (' + desc.byAgentId + ')' : ''}`,
120
+ '',
121
+ ];
122
+ return fm.join('\n');
123
+ }
124
+ // (旧 declare-module hack 已移除 — types.ts 已允许 description_version: 1 | 0)
125
+ function parseMarkdownToDescription(raw) {
126
+ // 极简 frontmatter 解析 (匹配 key: value; facets/scope 走单行)
127
+ const m = raw.match(/^---\n([\s\S]*?)\n---/);
128
+ if (!m)
129
+ return null;
130
+ const fm = m[1];
131
+ const get = (key) => {
132
+ // ^[ ]* 让缩进子键 (如 ` judgment: 0.7`) 也能匹配
133
+ const re = new RegExp(`^[ ]*${key}:\\s*(.*)$`, 'm');
134
+ const x = fm.match(re);
135
+ if (!x)
136
+ return undefined;
137
+ let v = x[1].trim();
138
+ if (v === 'null' || v === '~')
139
+ return undefined;
140
+ if (v.startsWith("'") && v.endsWith("'"))
141
+ v = v.slice(1, -1).replace(/''/g, "'");
142
+ return v;
143
+ };
144
+ const getNum = (key) => {
145
+ const v = get(key);
146
+ if (v === undefined)
147
+ return undefined;
148
+ const n = Number(v);
149
+ return Number.isFinite(n) ? n : undefined;
150
+ };
151
+ const getList = (key) => {
152
+ const v = get(key);
153
+ if (v === undefined)
154
+ return undefined;
155
+ if (!v.startsWith('['))
156
+ return undefined;
157
+ const inner = v.slice(1, -1).trim();
158
+ if (!inner)
159
+ return [];
160
+ return inner.split(',').map((s) => s.trim().replace(/^'|'$/g, '').replace(/''/g, "'"));
161
+ };
162
+ const descriptionId = get('descriptionId');
163
+ const judgmentRef = get('judgmentRef');
164
+ if (!descriptionId || !judgmentRef)
165
+ return null;
166
+ return {
167
+ descriptionId,
168
+ judgmentRef,
169
+ description_version: (getNum('description_version') ?? 1),
170
+ facets: {
171
+ judgment: getNum('judgment'),
172
+ taste_aesthetic: getNum('taste_aesthetic'),
173
+ novelty_score: getNum('novelty_score'),
174
+ imaginative_score: getNum('imaginative_score'),
175
+ curiosity_vector: getNum('curiosity_vector'),
176
+ },
177
+ basis: {
178
+ taste_basis: get('taste_basis'),
179
+ novelty_basis: get('novelty_basis'),
180
+ imagination_basis: get('imagination_basis'),
181
+ },
182
+ scope: {
183
+ domains: getList('domains') ?? [],
184
+ topics: getList('topics') ?? [],
185
+ },
186
+ visibility: (get('visibility') ?? 'private'),
187
+ openState: (get('openState') ?? 'locked'),
188
+ by: (get('by') ?? 'human'),
189
+ byAgentId: get('byAgentId'),
190
+ createdAt: get('createdAt') ?? new Date().toISOString(),
191
+ updatedAt: get('updatedAt') ?? new Date().toISOString(),
192
+ lastTransitionAt: get('lastTransitionAt'),
193
+ };
194
+ }
195
+ export async function saveDescription(desc) {
196
+ await ensureJudgenessDirs();
197
+ const md = descriptionToMarkdown(desc);
198
+ const tmp = descriptionMdPath(desc.descriptionId) + '.tmp';
199
+ await fs.writeFile(tmp, md, 'utf-8');
200
+ await fs.rename(tmp, descriptionMdPath(desc.descriptionId));
201
+ }
202
+ export async function loadDescription(id) {
203
+ try {
204
+ const raw = await fs.readFile(descriptionMdPath(id), 'utf-8');
205
+ return parseMarkdownToDescription(raw);
206
+ }
207
+ catch {
208
+ return null;
209
+ }
210
+ }
211
+ export async function listDescriptions() {
212
+ await ensureJudgenessDirs();
213
+ const files = await fs.readdir(DESCRIPTIONS_DIR()).catch(() => []);
214
+ const out = [];
215
+ for (const f of files) {
216
+ if (!f.endsWith('.md'))
217
+ continue;
218
+ const id = f.slice(0, -3);
219
+ const d = await loadDescription(id);
220
+ if (d)
221
+ out.push(d);
222
+ }
223
+ return out;
224
+ }
225
+ export async function findDescriptionByJudgmentRef(ref) {
226
+ const all = await listDescriptions();
227
+ return all.find((d) => d.judgmentRef === ref) ?? null;
228
+ }
229
+ // ---------------------------------------------------------------------------
230
+ // visibility.yaml
231
+ // ---------------------------------------------------------------------------
232
+ function visFileToYaml(f) {
233
+ const lines = ['# judgeness/visibility.yaml', `version: ${f.version}`, 'defaults:'];
234
+ lines.push(` visibility: ${f.defaults.visibility}`);
235
+ lines.push(` openState: ${f.defaults.openState}`);
236
+ lines.push('channels:');
237
+ for (const c of f.channels) {
238
+ lines.push(` - channelId: ${c.channelId}`);
239
+ lines.push(` visibility: ${c.visibility}`);
240
+ lines.push(` openState: ${c.openState}`);
241
+ lines.push(` humanOverride: ${c.humanOverride}`);
242
+ }
243
+ lines.push('cards:');
244
+ if (f.cards.length === 0)
245
+ lines.push(' []');
246
+ else
247
+ for (const c of f.cards) {
248
+ lines.push(` - descriptionId: ${c.descriptionId}`);
249
+ lines.push(` visibility: ${c.visibility}`);
250
+ lines.push(` openState: ${c.openState}`);
251
+ lines.push(` humanOverride: ${c.humanOverride}`);
252
+ }
253
+ return lines.join('\n') + '\n';
254
+ }
255
+ function parseVisYaml(raw) {
256
+ // 极简行解析 — 不引依赖
257
+ const lines = raw.split('\n');
258
+ const f = {
259
+ version: 1,
260
+ defaults: { visibility: 'private', openState: 'locked' },
261
+ channels: [],
262
+ cards: [],
263
+ };
264
+ let section = 'root';
265
+ let curChannel = null;
266
+ let curCard = null;
267
+ for (const line of lines) {
268
+ const t = line.trim();
269
+ if (!t || t.startsWith('#'))
270
+ continue;
271
+ if (t === 'defaults:') {
272
+ section = 'defaults';
273
+ continue;
274
+ }
275
+ if (t === 'channels:') {
276
+ section = 'channels';
277
+ continue;
278
+ }
279
+ if (t === 'cards:') {
280
+ section = 'cards';
281
+ continue;
282
+ }
283
+ if (t.startsWith('- ')) {
284
+ const kv = t.slice(2).split(':').map((s) => s.trim());
285
+ const k = kv[0];
286
+ const v = kv.slice(1).join(':');
287
+ if (section === 'channels') {
288
+ curChannel = { [k]: v };
289
+ f.channels.push(curChannel);
290
+ section = 'channelItem';
291
+ continue;
292
+ }
293
+ if (section === 'cards') {
294
+ curCard = { [k]: v };
295
+ f.cards.push(curCard);
296
+ section = 'cardItem';
297
+ continue;
298
+ }
299
+ }
300
+ if (section === 'defaults' || section === 'channelItem' || section === 'cardItem') {
301
+ const kv = t.split(':').map((s) => s.trim());
302
+ const k = kv[0];
303
+ const v = kv.slice(1).join(':');
304
+ if (section === 'defaults') {
305
+ if (k === 'visibility')
306
+ f.defaults.visibility = v;
307
+ if (k === 'openState')
308
+ f.defaults.openState = v;
309
+ }
310
+ else if (section === 'channelItem' && curChannel) {
311
+ if (k === 'visibility')
312
+ curChannel.visibility = v;
313
+ if (k === 'openState')
314
+ curChannel.openState = v;
315
+ if (k === 'humanOverride')
316
+ curChannel.humanOverride = v === 'true';
317
+ }
318
+ else if (section === 'cardItem' && curCard) {
319
+ if (k === 'visibility')
320
+ curCard.visibility = v;
321
+ if (k === 'openState')
322
+ curCard.openState = v;
323
+ if (k === 'humanOverride')
324
+ curCard.humanOverride = v === 'true';
325
+ }
326
+ }
327
+ }
328
+ return f;
329
+ }
330
+ export async function loadVisibility() {
331
+ try {
332
+ const raw = await fs.readFile(VISIBILITY_FILE(), 'utf-8');
333
+ return parseVisYaml(raw);
334
+ }
335
+ catch {
336
+ return {
337
+ version: 1,
338
+ defaults: { visibility: 'private', openState: 'locked' },
339
+ channels: [],
340
+ cards: [],
341
+ };
342
+ }
343
+ }
344
+ export async function saveVisibility(f) {
345
+ await ensureJudgenessDirs();
346
+ const tmp = VISIBILITY_FILE() + '.tmp';
347
+ await fs.writeFile(tmp, visFileToYaml(f), 'utf-8');
348
+ await fs.rename(tmp, VISIBILITY_FILE());
349
+ }
350
+ // ---------------------------------------------------------------------------
351
+ // allowlist.yaml
352
+ // ---------------------------------------------------------------------------
353
+ function allowlistToYaml(f) {
354
+ const lines = ['# judgeness/allowlist.yaml', `version: ${f.version}`, 'peers:'];
355
+ if (f.peers.length === 0) {
356
+ lines.push(' []');
357
+ }
358
+ else {
359
+ for (const p of f.peers) {
360
+ lines.push(` - pubkey: ${p.pubkey}`);
361
+ if (p.alias)
362
+ lines.push(` alias: ${escapeYamlString(p.alias)}`);
363
+ if (p.note)
364
+ lines.push(` note: ${escapeYamlString(p.note)}`);
365
+ lines.push(` addedAt: ${escapeYamlString(p.addedAt)}`);
366
+ }
367
+ }
368
+ return lines.join('\n') + '\n';
369
+ }
370
+ function parseAllowlistYaml(raw) {
371
+ const f = { version: 1, peers: [] };
372
+ const lines = raw.split('\n');
373
+ let cur = null;
374
+ for (const line of lines) {
375
+ const t = line.trim();
376
+ if (!t || t.startsWith('#'))
377
+ continue;
378
+ if (t.startsWith('- ')) {
379
+ if (cur && cur.pubkey)
380
+ f.peers.push(cur);
381
+ const kv = t.slice(2).split(':').map((s) => s.trim());
382
+ cur = { pubkey: kv.slice(1).join(':').trim() };
383
+ continue;
384
+ }
385
+ if (cur) {
386
+ const idx = t.indexOf(':');
387
+ if (idx === -1)
388
+ continue;
389
+ const k = t.slice(0, idx).trim();
390
+ let v = t.slice(idx + 1).trim();
391
+ if (v.startsWith("'") && v.endsWith("'"))
392
+ v = v.slice(1, -1).replace(/''/g, "'");
393
+ cur[k] = v;
394
+ }
395
+ }
396
+ if (cur && cur.pubkey)
397
+ f.peers.push(cur);
398
+ return f;
399
+ }
400
+ export async function loadAllowlist() {
401
+ try {
402
+ const raw = await fs.readFile(ALLOWLIST_FILE(), 'utf-8');
403
+ return parseAllowlistYaml(raw);
404
+ }
405
+ catch {
406
+ return { version: 1, peers: [] };
407
+ }
408
+ }
409
+ export async function saveAllowlist(f) {
410
+ await ensureJudgenessDirs();
411
+ const tmp = ALLOWLIST_FILE() + '.tmp';
412
+ await fs.writeFile(tmp, allowlistToYaml(f), 'utf-8');
413
+ await fs.rename(tmp, ALLOWLIST_FILE());
414
+ }
415
+ export async function isPubkeyAllowed(targetPubkey) {
416
+ const f = await loadAllowlist();
417
+ return f.peers.some((p) => p.pubkey === targetPubkey);
418
+ }
419
+ export async function addAllowlistPeer(entry) {
420
+ const f = await loadAllowlist();
421
+ const existing = f.peers.findIndex((p) => p.pubkey === entry.pubkey);
422
+ if (existing >= 0)
423
+ f.peers[existing] = entry;
424
+ else
425
+ f.peers.push(entry);
426
+ await saveAllowlist(f);
427
+ }
428
+ export async function removeAllowlistPeer(pubkey) {
429
+ const f = await loadAllowlist();
430
+ f.peers = f.peers.filter((p) => p.pubkey !== pubkey);
431
+ await saveAllowlist(f);
432
+ }
433
+ // ---------------------------------------------------------------------------
434
+ // hearth-cache/<remote-pk>/
435
+ // ---------------------------------------------------------------------------
436
+ function cacheDir(remotePubkey) {
437
+ const sub = remotePubkey.slice(0, 16) + '__' + remotePubkey.slice(0, 8);
438
+ return path.join(HEARTH_CACHE_ROOT(), sub);
439
+ }
440
+ export async function writeHearthCache(remotePubkey, manifest, descriptions) {
441
+ await ensureJudgenessDirs();
442
+ const dir = cacheDir(remotePubkey);
443
+ await fs.mkdir(dir, { recursive: true });
444
+ await fs.mkdir(path.join(dir, 'descriptions'), { recursive: true });
445
+ await fs.writeFile(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8');
446
+ await fs.writeFile(path.join(dir, 'last-seen.txt'), new Date().toISOString(), 'utf-8');
447
+ for (const d of descriptions) {
448
+ await fs.writeFile(path.join(dir, 'descriptions', `${d.descriptionId}.md`), descriptionToMarkdown(d), 'utf-8');
449
+ }
450
+ }
451
+ export async function readHearthCache(remotePubkey) {
452
+ try {
453
+ const raw = await fs.readFile(path.join(cacheDir(remotePubkey), 'manifest.json'), 'utf-8');
454
+ return JSON.parse(raw);
455
+ }
456
+ catch {
457
+ return null;
458
+ }
459
+ }
460
+ // ---------------------------------------------------------------------------
461
+ // tags.yaml (聚合器, 反攻期主用; 防御期写空壳)
462
+ // ---------------------------------------------------------------------------
463
+ export async function aggregateTags() {
464
+ const descs = await listDescriptions();
465
+ const set = new Set();
466
+ for (const d of descs) {
467
+ (d.scope.topics ?? []).forEach((t) => set.add(t));
468
+ (d.scope.domains ?? []).forEach((t) => set.add(t));
469
+ }
470
+ return Array.from(set).sort();
471
+ }
472
+ export async function writeTagsAggregate() {
473
+ await ensureJudgenessDirs();
474
+ const tags = await aggregateTags();
475
+ const yaml = ['# judgeness/tags.yaml', `version: 1`, `count: ${tags.length}`, 'tags:'];
476
+ for (const t of tags)
477
+ yaml.push(` - ${escapeYamlString(t)}`);
478
+ const tmp = TAGS_FILE() + '.tmp';
479
+ await fs.writeFile(tmp, yaml.join('\n') + '\n', 'utf-8');
480
+ await fs.rename(tmp, TAGS_FILE());
481
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * judgeness · types.ts
3
+ *
4
+ * 重要设计决策 (2026-07-15 user rev):
5
+ * judgeness 是对 judgement 的"描述统称", 不组块.
6
+ * 即: 不扩展 HumanJudgment 的 schema, 而是用一套独立 description vocabulary,
7
+ * 通过 judgmentRef 外键引用 HumanJudgment.id, 0 迁移成本.
8
+ *
9
+ * 核心 5 维 (与传播裂变 / 乔布斯 A 方向对应):
10
+ * judgment / taste_aesthetic / novelty_score / imaginative_score / curiosity_vector
11
+ * basis_* 是品味 / 创新 / 想象的依据描述 (D1 第三反转).
12
+ *
13
+ * 4 层隐私:
14
+ * public / allowlist / peers / private
15
+ *
16
+ * 状态机:
17
+ * open (agent 可 publish) | locked (默认, agent 不可) | human-only (仅人类写入)
18
+ */
19
+ export {};