forge-openclaw-plugin 0.2.19 → 0.2.21

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.
Files changed (82) hide show
  1. package/README.md +133 -2
  2. package/dist/assets/board-_C6oMy5w.js +6 -0
  3. package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
  4. package/dist/assets/index-B4A6TooJ.js +63 -0
  5. package/dist/assets/index-B4A6TooJ.js.map +1 -0
  6. package/dist/assets/index-D6Xs_2mo.css +1 -0
  7. package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
  8. package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
  9. package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
  10. package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
  11. package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
  12. package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
  13. package/dist/assets/vendor-DT3pnAKJ.css +1 -0
  14. package/dist/assets/vendor-De38P6YR.js +729 -0
  15. package/dist/assets/vendor-De38P6YR.js.map +1 -0
  16. package/dist/assets/viz-C6hfyqzu.js +34 -0
  17. package/dist/assets/viz-C6hfyqzu.js.map +1 -0
  18. package/dist/index.html +9 -9
  19. package/dist/openclaw/parity.d.ts +1 -1
  20. package/dist/openclaw/parity.js +29 -2
  21. package/dist/openclaw/routes.js +207 -24
  22. package/dist/openclaw/tools.js +324 -35
  23. package/dist/server/app.js +2080 -92
  24. package/dist/server/db.js +3 -0
  25. package/dist/server/health.js +1284 -0
  26. package/dist/server/managers/platform/background-job-manager.js +138 -2
  27. package/dist/server/managers/platform/llm-manager.js +126 -0
  28. package/dist/server/managers/platform/openai-responses-provider.js +773 -0
  29. package/dist/server/managers/runtime.js +6 -1
  30. package/dist/server/openapi.js +718 -0
  31. package/dist/server/preferences-seeds.js +409 -0
  32. package/dist/server/preferences-types.js +368 -0
  33. package/dist/server/psyche-types.js +42 -18
  34. package/dist/server/repositories/activity-events.js +53 -4
  35. package/dist/server/repositories/calendar.js +89 -15
  36. package/dist/server/repositories/collaboration.js +8 -3
  37. package/dist/server/repositories/diagnostic-logs.js +243 -0
  38. package/dist/server/repositories/entity-ownership.js +92 -0
  39. package/dist/server/repositories/goals.js +7 -2
  40. package/dist/server/repositories/habits.js +122 -16
  41. package/dist/server/repositories/notes.js +119 -41
  42. package/dist/server/repositories/preferences.js +1765 -0
  43. package/dist/server/repositories/projects.js +18 -7
  44. package/dist/server/repositories/psyche.js +84 -27
  45. package/dist/server/repositories/rewards.js +112 -4
  46. package/dist/server/repositories/strategies.js +450 -0
  47. package/dist/server/repositories/tags.js +11 -6
  48. package/dist/server/repositories/task-runs.js +10 -2
  49. package/dist/server/repositories/tasks.js +99 -17
  50. package/dist/server/repositories/users.js +417 -0
  51. package/dist/server/repositories/wiki-memory.js +3366 -0
  52. package/dist/server/services/context.js +20 -18
  53. package/dist/server/services/dashboard.js +29 -6
  54. package/dist/server/services/entity-crud.js +21 -3
  55. package/dist/server/services/insights.js +9 -7
  56. package/dist/server/services/projects.js +2 -1
  57. package/dist/server/services/psyche.js +10 -9
  58. package/dist/server/types.js +594 -30
  59. package/openclaw.plugin.json +1 -1
  60. package/package.json +1 -1
  61. package/server/migrations/015_multi_user_and_strategies.sql +244 -0
  62. package/server/migrations/016_health_companion.sql +158 -0
  63. package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  64. package/server/migrations/017_preferences.sql +131 -0
  65. package/server/migrations/018_preference_catalogs.sql +31 -0
  66. package/server/migrations/019_wiki_memory.sql +255 -0
  67. package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  68. package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  69. package/server/migrations/022_wiki_ingest_background.sql +85 -0
  70. package/server/migrations/023_diagnostic_logs.sql +28 -0
  71. package/skills/forge-openclaw/SKILL.md +126 -34
  72. package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
  73. package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
  74. package/dist/assets/board-8L3uX7_O.js +0 -6
  75. package/dist/assets/index-Cj1IBH_w.js +0 -36
  76. package/dist/assets/index-Cj1IBH_w.js.map +0 -1
  77. package/dist/assets/index-DQT6EbuS.css +0 -1
  78. package/dist/assets/vendor-BvM2F9Dp.js +0 -503
  79. package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
  80. package/dist/assets/vendor-CRS-psbw.css +0 -1
  81. package/dist/assets/viz-CNeunkfu.js +0 -34
  82. package/dist/assets/viz-CNeunkfu.js.map +0 -1
@@ -0,0 +1,1765 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDatabase, runInTransaction } from "../db.js";
3
+ import { HttpError } from "../errors.js";
4
+ import { absoluteSignalSchema, createPreferenceCatalogItemSchema, createPreferenceCatalogSchema, createPreferenceContextSchema, createPreferenceItemSchema, enqueueEntityPreferenceItemSchema, mergePreferenceContextsSchema, pairwiseJudgmentSchema, preferenceCatalogItemSchema, preferenceCatalogSchema, preferenceContextSchema, preferenceDimensionIdSchema, preferenceDimensionSummarySchema, preferenceItemSchema, preferenceItemScoreSchema, preferenceProfileSchema, preferenceCatalogSourceSchema, preferenceSnapshotSchema, preferenceWorkspacePayloadSchema, preferenceWorkspaceQuerySchema, startPreferenceGameSchema, submitAbsoluteSignalSchema, submitPairwiseJudgmentSchema, updatePreferenceCatalogItemSchema, updatePreferenceCatalogSchema, updatePreferenceContextSchema, updatePreferenceItemSchema, updatePreferenceScoreSchema } from "../preferences-types.js";
5
+ import { getPreferenceCatalogSeeds } from "../preferences-seeds.js";
6
+ import { getUserById, getDefaultUser } from "./users.js";
7
+ import { getGoalById } from "./goals.js";
8
+ import { getProjectById } from "./projects.js";
9
+ import { getTaskById } from "./tasks.js";
10
+ import { getStrategyById } from "./strategies.js";
11
+ import { getHabitById } from "./habits.js";
12
+ import { getNoteById } from "./notes.js";
13
+ import { getInsightById } from "./collaboration.js";
14
+ import { getCalendarEventById, getTaskTimeboxById, getWorkBlockTemplateById } from "./calendar.js";
15
+ import { getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById } from "./psyche.js";
16
+ const PREFERENCE_MODEL_VERSION = "pref-v1-bt-lite";
17
+ const DEFAULT_PREFERENCE_DOMAIN = "projects";
18
+ const DIMENSION_IDS = preferenceDimensionIdSchema.options;
19
+ const DEFAULT_DIMENSIONS = {
20
+ novelty: 0,
21
+ simplicity: 0,
22
+ rigor: 0,
23
+ aesthetics: 0,
24
+ depth: 0,
25
+ structure: 0,
26
+ familiarity: 0,
27
+ surprise: 0
28
+ };
29
+ const DEFAULT_CONTEXT_TEMPLATES = [
30
+ {
31
+ key: "default",
32
+ name: "Default",
33
+ description: "General preference state for this domain.",
34
+ shareMode: "shared",
35
+ active: true,
36
+ isDefault: true,
37
+ decayDays: 90
38
+ },
39
+ {
40
+ key: "work",
41
+ name: "Work",
42
+ description: "Work-specific tradeoffs and constraints.",
43
+ shareMode: "blended",
44
+ active: true,
45
+ isDefault: false,
46
+ decayDays: 75
47
+ },
48
+ {
49
+ key: "personal",
50
+ name: "Personal",
51
+ description: "Personal-life preferences outside explicit work mode.",
52
+ shareMode: "blended",
53
+ active: true,
54
+ isDefault: false,
55
+ decayDays: 90
56
+ },
57
+ {
58
+ key: "discovery",
59
+ name: "Discovery",
60
+ description: "A looser context for sampling and calibration.",
61
+ shareMode: "isolated",
62
+ active: true,
63
+ isDefault: false,
64
+ decayDays: 45
65
+ }
66
+ ];
67
+ const SIGNAL_WEIGHTS = {
68
+ favorite: 1.25,
69
+ veto: -1.6,
70
+ must_have: 1.5,
71
+ bookmark: 0.35,
72
+ neutral: 0,
73
+ compare_later: 0.2
74
+ };
75
+ function nowIso() {
76
+ return new Date().toISOString();
77
+ }
78
+ function clamp(value, minimum, maximum) {
79
+ return Math.min(maximum, Math.max(minimum, value));
80
+ }
81
+ function tanhScale(value, divisor) {
82
+ return Math.tanh(value / divisor);
83
+ }
84
+ function ageInDays(dateText) {
85
+ if (!dateText) {
86
+ return Number.POSITIVE_INFINITY;
87
+ }
88
+ const timestamp = Date.parse(dateText);
89
+ if (Number.isNaN(timestamp)) {
90
+ return Number.POSITIVE_INFINITY;
91
+ }
92
+ return Math.max(0, (Date.now() - timestamp) / (1000 * 60 * 60 * 24));
93
+ }
94
+ function timeDecay(ageDays, decayDays) {
95
+ if (!Number.isFinite(ageDays)) {
96
+ return 0;
97
+ }
98
+ return Math.exp(-ageDays / Math.max(7, decayDays));
99
+ }
100
+ function parseJsonArray(value, fallback = []) {
101
+ try {
102
+ const parsed = JSON.parse(value);
103
+ return Array.isArray(parsed) ? parsed : fallback;
104
+ }
105
+ catch {
106
+ return fallback;
107
+ }
108
+ }
109
+ function slugify(value) {
110
+ return value
111
+ .trim()
112
+ .toLowerCase()
113
+ .replace(/[^a-z0-9]+/g, "-")
114
+ .replace(/^-+|-+$/g, "")
115
+ .slice(0, 48);
116
+ }
117
+ function parseJsonObject(value, fallback) {
118
+ try {
119
+ const parsed = JSON.parse(value);
120
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
121
+ ? parsed
122
+ : fallback;
123
+ }
124
+ catch {
125
+ return fallback;
126
+ }
127
+ }
128
+ function normalizeDimensionVector(value) {
129
+ const source = value && typeof value === "object" && !Array.isArray(value)
130
+ ? value
131
+ : {};
132
+ return {
133
+ novelty: clamp(Number(source.novelty ?? 0), -1, 1),
134
+ simplicity: clamp(Number(source.simplicity ?? 0), -1, 1),
135
+ rigor: clamp(Number(source.rigor ?? 0), -1, 1),
136
+ aesthetics: clamp(Number(source.aesthetics ?? 0), -1, 1),
137
+ depth: clamp(Number(source.depth ?? 0), -1, 1),
138
+ structure: clamp(Number(source.structure ?? 0), -1, 1),
139
+ familiarity: clamp(Number(source.familiarity ?? 0), -1, 1),
140
+ surprise: clamp(Number(source.surprise ?? 0), -1, 1)
141
+ };
142
+ }
143
+ function vectorDistance(left, right) {
144
+ const squared = DIMENSION_IDS.reduce((sum, dimensionId) => {
145
+ const delta = left[dimensionId] - right[dimensionId];
146
+ return sum + delta * delta;
147
+ }, 0);
148
+ return Math.sqrt(squared / DIMENSION_IDS.length);
149
+ }
150
+ function mapProfile(row) {
151
+ return preferenceProfileSchema.parse({
152
+ id: row.id,
153
+ userId: row.user_id,
154
+ domain: row.domain,
155
+ defaultContextId: row.default_context_id,
156
+ modelVersion: row.model_version,
157
+ createdAt: row.created_at,
158
+ updatedAt: row.updated_at,
159
+ user: getUserById(row.user_id) ?? null
160
+ });
161
+ }
162
+ function mapContext(row) {
163
+ return preferenceContextSchema.parse({
164
+ id: row.id,
165
+ profileId: row.profile_id,
166
+ name: row.name,
167
+ description: row.description,
168
+ shareMode: row.share_mode,
169
+ active: row.active === 1,
170
+ isDefault: row.is_default === 1,
171
+ decayDays: row.decay_days,
172
+ createdAt: row.created_at,
173
+ updatedAt: row.updated_at
174
+ });
175
+ }
176
+ function mapItem(row) {
177
+ return preferenceItemSchema.parse({
178
+ id: row.id,
179
+ profileId: row.profile_id,
180
+ label: row.label,
181
+ description: row.description,
182
+ tags: parseJsonArray(row.tags_json).filter(Boolean),
183
+ featureWeights: normalizeDimensionVector(parseJsonObject(row.feature_weights_json, {})),
184
+ sourceEntityType: row.source_entity_type,
185
+ sourceEntityId: row.source_entity_id,
186
+ linkedEntity: row.source_entity_type && row.source_entity_id
187
+ ? {
188
+ entityType: row.source_entity_type,
189
+ entityId: row.source_entity_id
190
+ }
191
+ : null,
192
+ metadata: parseJsonObject(row.metadata_json, {}),
193
+ createdAt: row.created_at,
194
+ updatedAt: row.updated_at
195
+ });
196
+ }
197
+ function mapCatalogItem(row) {
198
+ return preferenceCatalogItemSchema.parse({
199
+ id: row.id,
200
+ catalogId: row.catalog_id,
201
+ label: row.label,
202
+ description: row.description,
203
+ tags: parseJsonArray(row.tags_json).filter(Boolean),
204
+ featureWeights: normalizeDimensionVector(parseJsonObject(row.feature_weights_json, {})),
205
+ position: row.position,
206
+ createdAt: row.created_at,
207
+ updatedAt: row.updated_at
208
+ });
209
+ }
210
+ function mapCatalog(row, items) {
211
+ return preferenceCatalogSchema.parse({
212
+ id: row.id,
213
+ profileId: row.profile_id,
214
+ domain: row.domain,
215
+ slug: row.slug,
216
+ title: row.title,
217
+ description: row.description,
218
+ source: row.source,
219
+ createdAt: row.created_at,
220
+ updatedAt: row.updated_at,
221
+ items
222
+ });
223
+ }
224
+ function mapJudgment(row) {
225
+ return pairwiseJudgmentSchema.parse({
226
+ id: row.id,
227
+ profileId: row.profile_id,
228
+ contextId: row.context_id,
229
+ userId: row.user_id,
230
+ leftItemId: row.left_item_id,
231
+ rightItemId: row.right_item_id,
232
+ outcome: row.outcome,
233
+ strength: row.strength,
234
+ responseTimeMs: row.response_time_ms,
235
+ source: row.source,
236
+ reasonTags: parseJsonArray(row.reason_tags_json).filter(Boolean),
237
+ createdAt: row.created_at
238
+ });
239
+ }
240
+ function mapSignal(row) {
241
+ return absoluteSignalSchema.parse({
242
+ id: row.id,
243
+ profileId: row.profile_id,
244
+ contextId: row.context_id,
245
+ userId: row.user_id,
246
+ itemId: row.item_id,
247
+ signalType: row.signal_type,
248
+ strength: row.strength,
249
+ source: row.source,
250
+ createdAt: row.created_at
251
+ });
252
+ }
253
+ function mapScore(row, item) {
254
+ return preferenceItemScoreSchema.parse({
255
+ id: row.id,
256
+ profileId: row.profile_id,
257
+ contextId: row.context_id,
258
+ itemId: row.item_id,
259
+ latentScore: row.latent_score,
260
+ confidence: row.confidence,
261
+ uncertainty: row.uncertainty,
262
+ evidenceCount: row.evidence_count,
263
+ pairwiseWins: row.pairwise_wins,
264
+ pairwiseLosses: row.pairwise_losses,
265
+ pairwiseTies: row.pairwise_ties,
266
+ signalCount: row.signal_count,
267
+ conflictCount: row.conflict_count,
268
+ status: row.status,
269
+ dominantDimensions: parseJsonArray(row.dominant_dimensions_json),
270
+ explanation: parseJsonArray(row.explanation_json),
271
+ manualStatus: row.manual_status,
272
+ manualScore: row.manual_score,
273
+ confidenceLock: row.confidence_lock,
274
+ bookmarked: row.bookmarked === 1,
275
+ compareLater: row.compare_later === 1,
276
+ frozen: row.frozen === 1,
277
+ lastInferredAt: row.last_inferred_at,
278
+ lastJudgmentAt: row.last_judgment_at,
279
+ updatedAt: row.updated_at,
280
+ item
281
+ });
282
+ }
283
+ function mapDimension(row) {
284
+ return preferenceDimensionSummarySchema.parse({
285
+ id: row.id,
286
+ profileId: row.profile_id,
287
+ contextId: row.context_id,
288
+ dimensionId: row.dimension_id,
289
+ leaning: row.leaning,
290
+ confidence: row.confidence,
291
+ movement: row.movement,
292
+ contextSensitivity: row.context_sensitivity,
293
+ evidenceCount: row.evidence_count,
294
+ updatedAt: row.updated_at
295
+ });
296
+ }
297
+ function mapSnapshot(row) {
298
+ return preferenceSnapshotSchema.parse({
299
+ id: row.id,
300
+ profileId: row.profile_id,
301
+ contextId: row.context_id,
302
+ summaryMetrics: parseJsonObject(row.summary_metrics_json, {}),
303
+ serializedModelState: parseJsonObject(row.serialized_model_state_json, {}),
304
+ createdAt: row.created_at
305
+ });
306
+ }
307
+ function readProfileByUserAndDomain(userId, domain) {
308
+ const row = getDatabase()
309
+ .prepare(`SELECT id, user_id, domain, default_context_id, model_version, created_at, updated_at
310
+ FROM preference_profiles
311
+ WHERE user_id = ? AND domain = ?`)
312
+ .get(userId, domain);
313
+ return row ? mapProfile(row) : null;
314
+ }
315
+ function readProfileById(profileId) {
316
+ const row = getDatabase()
317
+ .prepare(`SELECT id, user_id, domain, default_context_id, model_version, created_at, updated_at
318
+ FROM preference_profiles
319
+ WHERE id = ?`)
320
+ .get(profileId);
321
+ return row ? mapProfile(row) : null;
322
+ }
323
+ function listContexts(profileId) {
324
+ return getDatabase()
325
+ .prepare(`SELECT id, profile_id, name, description, share_mode, active, is_default, decay_days, created_at, updated_at
326
+ FROM preference_contexts
327
+ WHERE profile_id = ?
328
+ ORDER BY is_default DESC, active DESC, name ASC`)
329
+ .all(profileId).map(mapContext);
330
+ }
331
+ function readContext(contextId) {
332
+ const row = getDatabase()
333
+ .prepare(`SELECT id, profile_id, name, description, share_mode, active, is_default, decay_days, created_at, updated_at
334
+ FROM preference_contexts
335
+ WHERE id = ?`)
336
+ .get(contextId);
337
+ return row ? mapContext(row) : null;
338
+ }
339
+ function resolveContext(profile, contextId) {
340
+ const contexts = listContexts(profile.id);
341
+ const context = (contextId ? contexts.find((entry) => entry.id === contextId) : null) ??
342
+ contexts.find((entry) => entry.isDefault) ??
343
+ contexts[0];
344
+ if (!context) {
345
+ throw new HttpError(500, "preferences_missing_context", "Preference profile has no contexts");
346
+ }
347
+ return context;
348
+ }
349
+ function listItems(profileId) {
350
+ return getDatabase()
351
+ .prepare(`SELECT id, profile_id, label, description, tags_json, feature_weights_json, source_entity_type, source_entity_id, metadata_json, created_at, updated_at
352
+ FROM preference_items
353
+ WHERE profile_id = ?
354
+ ORDER BY updated_at DESC, created_at DESC`)
355
+ .all(profileId).map(mapItem);
356
+ }
357
+ function getItemById(itemId) {
358
+ const row = getDatabase()
359
+ .prepare(`SELECT id, profile_id, label, description, tags_json, feature_weights_json, source_entity_type, source_entity_id, metadata_json, created_at, updated_at
360
+ FROM preference_items
361
+ WHERE id = ?`)
362
+ .get(itemId);
363
+ return row ? mapItem(row) : null;
364
+ }
365
+ function listCatalogs(profileId) {
366
+ const catalogRows = getDatabase()
367
+ .prepare(`SELECT id, profile_id, domain, slug, title, description, source, archived, created_at, updated_at
368
+ FROM preference_catalogs
369
+ WHERE profile_id = ? AND archived = 0
370
+ ORDER BY source ASC, title ASC`)
371
+ .all(profileId).filter((row) => row.archived === 0);
372
+ const itemRows = getDatabase()
373
+ .prepare(`SELECT id, catalog_id, label, description, tags_json, feature_weights_json, position, archived, created_at, updated_at
374
+ FROM preference_catalog_items
375
+ WHERE catalog_id IN (
376
+ SELECT id FROM preference_catalogs WHERE profile_id = ? AND archived = 0
377
+ )
378
+ AND archived = 0
379
+ ORDER BY position ASC, label ASC`)
380
+ .all(profileId).filter((row) => row.archived === 0);
381
+ const itemsByCatalogId = new Map();
382
+ for (const row of itemRows) {
383
+ const list = itemsByCatalogId.get(row.catalog_id) ?? [];
384
+ list.push(mapCatalogItem(row));
385
+ itemsByCatalogId.set(row.catalog_id, list);
386
+ }
387
+ return catalogRows.map((row) => mapCatalog(row, itemsByCatalogId.get(row.id) ?? []));
388
+ }
389
+ function readCatalog(catalogId) {
390
+ const row = getDatabase()
391
+ .prepare(`SELECT id, profile_id, domain, slug, title, description, source, archived, created_at, updated_at
392
+ FROM preference_catalogs
393
+ WHERE id = ?`)
394
+ .get(catalogId);
395
+ if (!row || row.archived === 1) {
396
+ return null;
397
+ }
398
+ const items = getDatabase()
399
+ .prepare(`SELECT id, catalog_id, label, description, tags_json, feature_weights_json, position, archived, created_at, updated_at
400
+ FROM preference_catalog_items
401
+ WHERE catalog_id = ? AND archived = 0
402
+ ORDER BY position ASC, label ASC`)
403
+ .all(catalogId)
404
+ .filter((entry) => entry.archived === 0)
405
+ .map(mapCatalogItem);
406
+ return mapCatalog(row, items);
407
+ }
408
+ function readCatalogItem(catalogItemId) {
409
+ const row = getDatabase()
410
+ .prepare(`SELECT id, catalog_id, label, description, tags_json, feature_weights_json, position, archived, created_at, updated_at
411
+ FROM preference_catalog_items
412
+ WHERE id = ?`)
413
+ .get(catalogItemId);
414
+ return row && row.archived === 0 ? mapCatalogItem(row) : null;
415
+ }
416
+ function listJudgmentsForContexts(contextIds) {
417
+ if (contextIds.length === 0) {
418
+ return [];
419
+ }
420
+ const placeholders = contextIds.map(() => "?").join(", ");
421
+ return getDatabase()
422
+ .prepare(`SELECT id, profile_id, context_id, user_id, left_item_id, right_item_id, outcome, strength, response_time_ms, source, reason_tags_json, created_at
423
+ FROM pairwise_judgments
424
+ WHERE context_id IN (${placeholders})
425
+ ORDER BY created_at DESC`)
426
+ .all(...contextIds).map(mapJudgment);
427
+ }
428
+ function listSignalsForContexts(contextIds) {
429
+ if (contextIds.length === 0) {
430
+ return [];
431
+ }
432
+ const placeholders = contextIds.map(() => "?").join(", ");
433
+ return getDatabase()
434
+ .prepare(`SELECT id, profile_id, context_id, user_id, item_id, signal_type, strength, source, created_at
435
+ FROM absolute_signals
436
+ WHERE context_id IN (${placeholders})
437
+ ORDER BY created_at DESC`)
438
+ .all(...contextIds).map(mapSignal);
439
+ }
440
+ function listStoredScores(contextId) {
441
+ return getDatabase()
442
+ .prepare(`SELECT id, profile_id, context_id, item_id, latent_score, confidence, uncertainty, evidence_count, pairwise_wins, pairwise_losses, pairwise_ties, signal_count, conflict_count, status, dominant_dimensions_json, explanation_json, manual_status, manual_score, confidence_lock, bookmarked, compare_later, frozen, last_inferred_at, last_judgment_at, updated_at
443
+ FROM preference_item_scores
444
+ WHERE context_id = ?`)
445
+ .all(contextId);
446
+ }
447
+ function listStoredDimensions(contextId) {
448
+ return getDatabase()
449
+ .prepare(`SELECT id, profile_id, context_id, dimension_id, leaning, confidence, movement, context_sensitivity, evidence_count, updated_at
450
+ FROM preference_dimension_summaries
451
+ WHERE context_id = ?
452
+ ORDER BY dimension_id ASC`)
453
+ .all(contextId).map(mapDimension);
454
+ }
455
+ function listSnapshots(contextId, limit = 24) {
456
+ return getDatabase()
457
+ .prepare(`SELECT id, profile_id, context_id, summary_metrics_json, serialized_model_state_json, created_at
458
+ FROM preference_snapshots
459
+ WHERE context_id = ?
460
+ ORDER BY created_at DESC
461
+ LIMIT ?`)
462
+ .all(contextId, limit).map(mapSnapshot);
463
+ }
464
+ function ensureUserExists(userId) {
465
+ const user = getUserById(userId);
466
+ if (!user) {
467
+ throw new HttpError(404, "user_not_found", `User ${userId} was not found.`);
468
+ }
469
+ return user;
470
+ }
471
+ function resolveSourceEntity(entityType, entityId) {
472
+ switch (entityType) {
473
+ case "goal": {
474
+ const goal = getGoalById(entityId);
475
+ return goal ? { label: goal.title, description: goal.description } : null;
476
+ }
477
+ case "project": {
478
+ const project = getProjectById(entityId);
479
+ return project
480
+ ? { label: project.title, description: project.description }
481
+ : null;
482
+ }
483
+ case "task": {
484
+ const task = getTaskById(entityId);
485
+ return task ? { label: task.title, description: task.description } : null;
486
+ }
487
+ case "strategy": {
488
+ const strategy = getStrategyById(entityId);
489
+ return strategy
490
+ ? { label: strategy.title, description: strategy.overview }
491
+ : null;
492
+ }
493
+ case "habit": {
494
+ const habit = getHabitById(entityId);
495
+ return habit ? { label: habit.title, description: habit.description } : null;
496
+ }
497
+ case "note": {
498
+ const note = getNoteById(entityId);
499
+ return note
500
+ ? {
501
+ label: note.contentPlain.slice(0, 72) || "Linked note",
502
+ description: note.contentPlain
503
+ }
504
+ : null;
505
+ }
506
+ case "insight": {
507
+ const insight = getInsightById(entityId);
508
+ return insight
509
+ ? { label: insight.title, description: insight.summary }
510
+ : null;
511
+ }
512
+ case "calendar_event": {
513
+ const event = getCalendarEventById(entityId);
514
+ return event ? { label: event.title, description: event.description } : null;
515
+ }
516
+ case "work_block_template": {
517
+ const template = getWorkBlockTemplateById(entityId);
518
+ return template
519
+ ? { label: template.title, description: template.kind }
520
+ : null;
521
+ }
522
+ case "task_timebox": {
523
+ const timebox = getTaskTimeboxById(entityId);
524
+ return timebox
525
+ ? { label: timebox.title, description: timebox.overrideReason ?? "" }
526
+ : null;
527
+ }
528
+ case "psyche_value": {
529
+ const value = getPsycheValueById(entityId);
530
+ return value
531
+ ? { label: value.title, description: value.description }
532
+ : null;
533
+ }
534
+ case "behavior_pattern": {
535
+ const pattern = getBehaviorPatternById(entityId);
536
+ return pattern
537
+ ? { label: pattern.title, description: pattern.description }
538
+ : null;
539
+ }
540
+ case "behavior": {
541
+ const behavior = getBehaviorById(entityId);
542
+ return behavior
543
+ ? { label: behavior.title, description: behavior.description }
544
+ : null;
545
+ }
546
+ case "belief_entry": {
547
+ const belief = getBeliefEntryById(entityId);
548
+ return belief
549
+ ? { label: belief.statement, description: belief.flexibleAlternative }
550
+ : null;
551
+ }
552
+ case "mode_profile": {
553
+ const mode = getModeProfileById(entityId);
554
+ return mode ? { label: mode.title, description: mode.persona } : null;
555
+ }
556
+ case "mode_guide_session": {
557
+ const session = getModeGuideSessionById(entityId);
558
+ return session
559
+ ? { label: session.summary, description: session.summary }
560
+ : null;
561
+ }
562
+ case "event_type": {
563
+ const eventType = getEventTypeById(entityId);
564
+ return eventType
565
+ ? { label: eventType.label, description: eventType.description }
566
+ : null;
567
+ }
568
+ case "emotion_definition": {
569
+ const emotion = getEmotionDefinitionById(entityId);
570
+ return emotion
571
+ ? { label: emotion.label, description: emotion.description }
572
+ : null;
573
+ }
574
+ case "trigger_report": {
575
+ const report = getTriggerReportById(entityId);
576
+ return report
577
+ ? { label: report.title, description: report.eventSituation }
578
+ : null;
579
+ }
580
+ case "tag":
581
+ default:
582
+ return null;
583
+ }
584
+ }
585
+ function ensureProfile(userId, domain) {
586
+ ensureUserExists(userId);
587
+ const existing = readProfileByUserAndDomain(userId, domain);
588
+ if (existing) {
589
+ if (listContexts(existing.id).length === 0) {
590
+ createDefaultContexts(existing.id);
591
+ }
592
+ ensureCatalogs(existing.id, domain);
593
+ return readProfileById(existing.id) ?? existing;
594
+ }
595
+ const now = nowIso();
596
+ const profileId = `pref_profile_${randomUUID().slice(0, 10)}`;
597
+ runInTransaction(() => {
598
+ getDatabase()
599
+ .prepare(`INSERT INTO preference_profiles (id, user_id, domain, default_context_id, model_version, created_at, updated_at)
600
+ VALUES (?, ?, ?, NULL, ?, ?, ?)`)
601
+ .run(profileId, userId, domain, PREFERENCE_MODEL_VERSION, now, now);
602
+ createDefaultContexts(profileId);
603
+ ensureCatalogs(profileId, domain);
604
+ });
605
+ const created = readProfileById(profileId);
606
+ if (!created) {
607
+ throw new HttpError(500, "preferences_profile_missing", "Preference profile could not be created.");
608
+ }
609
+ return created;
610
+ }
611
+ function createDefaultContexts(profileId) {
612
+ const now = nowIso();
613
+ const database = getDatabase();
614
+ const insertedContextIds = [];
615
+ for (const template of DEFAULT_CONTEXT_TEMPLATES) {
616
+ const contextId = `pref_ctx_${template.key}_${randomUUID().slice(0, 8)}`;
617
+ insertedContextIds.push(contextId);
618
+ database
619
+ .prepare(`INSERT INTO preference_contexts (id, profile_id, name, description, share_mode, active, is_default, decay_days, created_at, updated_at)
620
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
621
+ .run(contextId, profileId, template.name, template.description, template.shareMode, template.active ? 1 : 0, template.isDefault ? 1 : 0, template.decayDays, now, now);
622
+ }
623
+ const defaultContextId = insertedContextIds[0] ?? null;
624
+ database
625
+ .prepare(`UPDATE preference_profiles
626
+ SET default_context_id = ?, updated_at = ?
627
+ WHERE id = ?`)
628
+ .run(defaultContextId, now, profileId);
629
+ }
630
+ function createSeededCatalogs(profileId, domain) {
631
+ const seeds = getPreferenceCatalogSeeds(domain);
632
+ if (seeds.length === 0) {
633
+ return;
634
+ }
635
+ const database = getDatabase();
636
+ const now = nowIso();
637
+ for (const seed of seeds) {
638
+ const catalogId = `pref_catalog_${randomUUID().slice(0, 10)}`;
639
+ database
640
+ .prepare(`INSERT INTO preference_catalogs (
641
+ id, profile_id, domain, slug, title, description, source, archived, created_at, updated_at
642
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`)
643
+ .run(catalogId, profileId, domain, seed.slug, seed.title, seed.description, preferenceCatalogSourceSchema.enum.seeded, now, now);
644
+ seed.items.forEach((seedItem, index) => {
645
+ database
646
+ .prepare(`INSERT INTO preference_catalog_items (
647
+ id, catalog_id, label, description, tags_json, feature_weights_json, position, archived, created_at, updated_at
648
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`)
649
+ .run(`pref_catalog_item_${randomUUID().slice(0, 10)}`, catalogId, seedItem.label, seedItem.description, JSON.stringify(seedItem.tags), JSON.stringify(seedItem.featureWeights), index, now, now);
650
+ });
651
+ }
652
+ }
653
+ function ensureCatalogs(profileId, domain) {
654
+ const existingCount = getDatabase()
655
+ .prepare(`SELECT COUNT(*) as count
656
+ FROM preference_catalogs
657
+ WHERE profile_id = ?`)
658
+ .get(profileId);
659
+ if (existingCount.count === 0) {
660
+ createSeededCatalogs(profileId, domain);
661
+ }
662
+ }
663
+ function buildEvidenceFactorMap(contexts, selectedContext) {
664
+ const factors = new Map();
665
+ for (const context of contexts.filter((entry) => entry.active)) {
666
+ if (selectedContext.shareMode === "isolated") {
667
+ factors.set(context.id, context.id === selectedContext.id ? 1 : 0);
668
+ continue;
669
+ }
670
+ if (selectedContext.shareMode === "shared") {
671
+ factors.set(context.id, 1);
672
+ continue;
673
+ }
674
+ factors.set(context.id, context.id === selectedContext.id ? 1 : 0.45);
675
+ }
676
+ return factors;
677
+ }
678
+ function deriveStatus(options) {
679
+ const { manualStatus, score, confidence, bookmarked, compareLater, signals } = options;
680
+ if (manualStatus) {
681
+ return manualStatus;
682
+ }
683
+ if (signals.includes("veto")) {
684
+ return "vetoed";
685
+ }
686
+ if (signals.includes("must_have")) {
687
+ return "must_have";
688
+ }
689
+ if (signals.includes("favorite")) {
690
+ return "favorite";
691
+ }
692
+ if (bookmarked || compareLater || signals.includes("bookmark")) {
693
+ return "bookmarked";
694
+ }
695
+ if (confidence < 0.42) {
696
+ return "uncertain";
697
+ }
698
+ if (score >= 0.35) {
699
+ return "liked";
700
+ }
701
+ if (score <= -0.35) {
702
+ return "disliked";
703
+ }
704
+ return "neutral";
705
+ }
706
+ function computeDimensionSummaries(options) {
707
+ const { contexts, selectedContext, itemsById, judgments, signals } = options;
708
+ const evidenceFactors = buildEvidenceFactorMap(contexts, selectedContext);
709
+ const leaning = new Map(DIMENSION_IDS.map((dimensionId) => [dimensionId, 0]));
710
+ const recent = new Map(DIMENSION_IDS.map((dimensionId) => [dimensionId, 0]));
711
+ const counts = new Map(DIMENSION_IDS.map((dimensionId) => [dimensionId, 0]));
712
+ for (const signal of signals) {
713
+ const item = itemsById.get(signal.itemId);
714
+ const factor = evidenceFactors.get(signal.contextId) ?? 0;
715
+ if (!item || factor <= 0) {
716
+ continue;
717
+ }
718
+ const weight = SIGNAL_WEIGHTS[signal.signalType] *
719
+ signal.strength *
720
+ factor *
721
+ timeDecay(ageInDays(signal.createdAt), selectedContext.decayDays);
722
+ const recentFactor = ageInDays(signal.createdAt) <= 21 ? 1 : 0;
723
+ for (const dimensionId of DIMENSION_IDS) {
724
+ const contribution = item.featureWeights[dimensionId] * weight;
725
+ leaning.set(dimensionId, (leaning.get(dimensionId) ?? 0) + contribution);
726
+ recent.set(dimensionId, (recent.get(dimensionId) ?? 0) + contribution * recentFactor);
727
+ if (Math.abs(item.featureWeights[dimensionId]) > 0.01) {
728
+ counts.set(dimensionId, (counts.get(dimensionId) ?? 0) + 1);
729
+ }
730
+ }
731
+ }
732
+ for (const judgment of judgments) {
733
+ const left = itemsById.get(judgment.leftItemId);
734
+ const right = itemsById.get(judgment.rightItemId);
735
+ const factor = evidenceFactors.get(judgment.contextId) ?? 0;
736
+ if (!left || !right || factor <= 0 || judgment.outcome === "skip") {
737
+ continue;
738
+ }
739
+ const outcomeSign = judgment.outcome === "left" ? 1 : judgment.outcome === "right" ? -1 : 0;
740
+ const weight = judgment.strength *
741
+ factor *
742
+ timeDecay(ageInDays(judgment.createdAt), selectedContext.decayDays);
743
+ const recentFactor = ageInDays(judgment.createdAt) <= 21 ? 1 : 0;
744
+ for (const dimensionId of DIMENSION_IDS) {
745
+ const contribution = (left.featureWeights[dimensionId] - right.featureWeights[dimensionId]) *
746
+ outcomeSign *
747
+ weight;
748
+ leaning.set(dimensionId, (leaning.get(dimensionId) ?? 0) + contribution);
749
+ recent.set(dimensionId, (recent.get(dimensionId) ?? 0) + contribution * recentFactor);
750
+ if (Math.abs(left.featureWeights[dimensionId]) > 0.01 ||
751
+ Math.abs(right.featureWeights[dimensionId]) > 0.01) {
752
+ counts.set(dimensionId, (counts.get(dimensionId) ?? 0) + 1);
753
+ }
754
+ }
755
+ }
756
+ return DIMENSION_IDS.map((dimensionId) => ({
757
+ id: `pref_dim_${selectedContext.id}_${dimensionId}`,
758
+ profileId: selectedContext.profileId,
759
+ contextId: selectedContext.id,
760
+ dimensionId,
761
+ leaning: clamp(tanhScale(leaning.get(dimensionId) ?? 0, 3), -1, 1),
762
+ confidence: clamp(1 - Math.exp(-(counts.get(dimensionId) ?? 0) / 3), 0, 1),
763
+ movement: clamp(tanhScale(recent.get(dimensionId) ?? 0, 2), -1, 1),
764
+ contextSensitivity: 0,
765
+ evidenceCount: counts.get(dimensionId) ?? 0,
766
+ updatedAt: nowIso()
767
+ }));
768
+ }
769
+ function computeScores(options) {
770
+ const { profile, contexts, selectedContext, items, judgments, signals, existingScores } = options;
771
+ const itemsById = new Map(items.map((item) => [item.id, item]));
772
+ const evidenceFactors = buildEvidenceFactorMap(contexts, selectedContext);
773
+ const dimensionSummaries = computeDimensionSummaries({
774
+ contexts,
775
+ selectedContext,
776
+ itemsById,
777
+ judgments,
778
+ signals
779
+ });
780
+ const dimensionLeanings = new Map(dimensionSummaries.map((summary) => [summary.dimensionId, summary.leaning]));
781
+ const manualByItemId = new Map(existingScores.map((score) => [score.item_id, score]));
782
+ const perItem = new Map(items.map((item) => [
783
+ item.id,
784
+ {
785
+ raw: 0,
786
+ wins: 0,
787
+ losses: 0,
788
+ ties: 0,
789
+ signalCount: 0,
790
+ evidenceCount: 0,
791
+ lastJudgmentAt: null,
792
+ lastEvidenceAt: null,
793
+ signals: []
794
+ }
795
+ ]));
796
+ const pairDirections = new Map();
797
+ for (const signal of signals) {
798
+ const itemStats = perItem.get(signal.itemId);
799
+ const factor = evidenceFactors.get(signal.contextId) ?? 0;
800
+ if (!itemStats || factor <= 0) {
801
+ continue;
802
+ }
803
+ const weight = SIGNAL_WEIGHTS[signal.signalType] *
804
+ signal.strength *
805
+ factor *
806
+ timeDecay(ageInDays(signal.createdAt), selectedContext.decayDays);
807
+ itemStats.raw += weight;
808
+ itemStats.signalCount += 1;
809
+ itemStats.evidenceCount += 1;
810
+ itemStats.signals.push(signal.signalType);
811
+ itemStats.lastEvidenceAt =
812
+ !itemStats.lastEvidenceAt || signal.createdAt > itemStats.lastEvidenceAt
813
+ ? signal.createdAt
814
+ : itemStats.lastEvidenceAt;
815
+ }
816
+ for (const judgment of judgments) {
817
+ const leftStats = perItem.get(judgment.leftItemId);
818
+ const rightStats = perItem.get(judgment.rightItemId);
819
+ const factor = evidenceFactors.get(judgment.contextId) ?? 0;
820
+ if (!leftStats ||
821
+ !rightStats ||
822
+ factor <= 0 ||
823
+ judgment.outcome === "skip") {
824
+ continue;
825
+ }
826
+ const weight = judgment.strength *
827
+ factor *
828
+ timeDecay(ageInDays(judgment.createdAt), selectedContext.decayDays);
829
+ const pairKey = [judgment.leftItemId, judgment.rightItemId].sort().join("::");
830
+ const pairState = pairDirections.get(pairKey) ?? { leftWins: 0, rightWins: 0 };
831
+ if (judgment.outcome === "left") {
832
+ leftStats.raw += weight;
833
+ rightStats.raw -= weight;
834
+ leftStats.wins += 1;
835
+ rightStats.losses += 1;
836
+ pairState.leftWins += 1;
837
+ }
838
+ else if (judgment.outcome === "right") {
839
+ leftStats.raw -= weight;
840
+ rightStats.raw += weight;
841
+ leftStats.losses += 1;
842
+ rightStats.wins += 1;
843
+ pairState.rightWins += 1;
844
+ }
845
+ else {
846
+ leftStats.ties += 1;
847
+ rightStats.ties += 1;
848
+ }
849
+ pairDirections.set(pairKey, pairState);
850
+ leftStats.evidenceCount += 1;
851
+ rightStats.evidenceCount += 1;
852
+ leftStats.lastJudgmentAt =
853
+ !leftStats.lastJudgmentAt || judgment.createdAt > leftStats.lastJudgmentAt
854
+ ? judgment.createdAt
855
+ : leftStats.lastJudgmentAt;
856
+ rightStats.lastJudgmentAt =
857
+ !rightStats.lastJudgmentAt || judgment.createdAt > rightStats.lastJudgmentAt
858
+ ? judgment.createdAt
859
+ : rightStats.lastJudgmentAt;
860
+ leftStats.lastEvidenceAt =
861
+ !leftStats.lastEvidenceAt || judgment.createdAt > leftStats.lastEvidenceAt
862
+ ? judgment.createdAt
863
+ : leftStats.lastEvidenceAt;
864
+ rightStats.lastEvidenceAt =
865
+ !rightStats.lastEvidenceAt || judgment.createdAt > rightStats.lastEvidenceAt
866
+ ? judgment.createdAt
867
+ : rightStats.lastEvidenceAt;
868
+ }
869
+ const conflictCountByItem = new Map();
870
+ for (const [pairKey, pairState] of pairDirections) {
871
+ if (pairState.leftWins === 0 || pairState.rightWins === 0) {
872
+ continue;
873
+ }
874
+ const [leftItemId, rightItemId] = pairKey.split("::");
875
+ conflictCountByItem.set(leftItemId, (conflictCountByItem.get(leftItemId) ?? 0) + 1);
876
+ conflictCountByItem.set(rightItemId, (conflictCountByItem.get(rightItemId) ?? 0) + 1);
877
+ }
878
+ const scores = items.map((item) => {
879
+ const existing = manualByItemId.get(item.id);
880
+ const stats = perItem.get(item.id);
881
+ const dominantDimensions = [...DIMENSION_IDS]
882
+ .map((dimensionId) => ({
883
+ dimensionId,
884
+ weight: Math.abs(item.featureWeights[dimensionId]) *
885
+ Math.abs(dimensionLeanings.get(dimensionId) ?? 0)
886
+ }))
887
+ .filter((entry) => entry.weight > 0)
888
+ .sort((left, right) => right.weight - left.weight)
889
+ .slice(0, 3)
890
+ .map((entry) => entry.dimensionId);
891
+ const score = clamp(existing?.manual_score ?? tanhScale(stats.raw, 4), -1, 1);
892
+ const freshness = Math.exp(-Math.max(0, ageInDays(stats.lastEvidenceAt) - selectedContext.decayDays) /
893
+ Math.max(14, selectedContext.decayDays));
894
+ const conflictPenalty = 1 -
895
+ Math.min(0.55, (conflictCountByItem.get(item.id) ?? 0) /
896
+ Math.max(1, stats.evidenceCount));
897
+ const confidence = existing?.confidence_lock ??
898
+ clamp((1 - Math.exp(-stats.evidenceCount / 4)) *
899
+ conflictPenalty *
900
+ (0.55 + 0.45 * freshness), 0.04, 1);
901
+ const bookmarked = (existing?.bookmarked ?? 0) === 1 ||
902
+ stats.signals.includes("bookmark");
903
+ const compareLater = (existing?.compare_later ?? 0) === 1 ||
904
+ stats.signals.includes("compare_later");
905
+ const status = deriveStatus({
906
+ manualStatus: existing?.manual_status ?? null,
907
+ score,
908
+ confidence,
909
+ bookmarked,
910
+ compareLater,
911
+ signals: stats.signals
912
+ });
913
+ const explanation = [
914
+ stats.wins > 0 ? `Preferred over peers ${stats.wins} time${stats.wins === 1 ? "" : "s"}.` : null,
915
+ stats.losses > 0 ? `Lost against peers ${stats.losses} time${stats.losses === 1 ? "" : "s"}.` : null,
916
+ stats.signalCount > 0
917
+ ? `Direct signals recorded: ${stats.signals.join(", ")}.`
918
+ : null,
919
+ dominantDimensions.length > 0
920
+ ? `Dominant dimensions: ${dominantDimensions.join(", ")}.`
921
+ : null,
922
+ (conflictCountByItem.get(item.id) ?? 0) > 0
923
+ ? "Conflicting pairwise evidence lowers confidence."
924
+ : null,
925
+ ageInDays(stats.lastEvidenceAt) > selectedContext.decayDays
926
+ ? "Evidence is getting stale and should be recalibrated."
927
+ : null
928
+ ].filter((value) => Boolean(value));
929
+ return {
930
+ itemId: item.id,
931
+ latentScore: score,
932
+ confidence,
933
+ uncertainty: clamp(1 - confidence, 0, 1),
934
+ evidenceCount: stats.evidenceCount,
935
+ pairwiseWins: stats.wins,
936
+ pairwiseLosses: stats.losses,
937
+ pairwiseTies: stats.ties,
938
+ signalCount: stats.signalCount,
939
+ conflictCount: conflictCountByItem.get(item.id) ?? 0,
940
+ status,
941
+ dominantDimensions,
942
+ explanation,
943
+ manualStatus: existing?.manual_status ?? null,
944
+ manualScore: existing?.manual_score ?? null,
945
+ confidenceLock: existing?.confidence_lock ?? null,
946
+ bookmarked,
947
+ compareLater,
948
+ frozen: (existing?.frozen ?? 0) === 1,
949
+ lastInferredAt: nowIso(),
950
+ lastJudgmentAt: stats.lastJudgmentAt,
951
+ updatedAt: nowIso()
952
+ };
953
+ });
954
+ const averageSensitivityByDimension = new Map(dimensionSummaries.map((summary) => [summary.dimensionId, 0]));
955
+ const contextOnlyDimensionsByContext = new Map();
956
+ for (const context of contexts.filter((entry) => entry.active)) {
957
+ const isolatedDimensions = computeDimensionSummaries({
958
+ contexts: contexts.map((entry) => entry.id === context.id
959
+ ? { ...entry, shareMode: "isolated" }
960
+ : { ...entry, active: false }),
961
+ selectedContext: { ...context, shareMode: "isolated" },
962
+ itemsById,
963
+ judgments,
964
+ signals
965
+ });
966
+ contextOnlyDimensionsByContext.set(context.id, new Map(isolatedDimensions.map((summary) => [summary.dimensionId, summary.leaning])));
967
+ }
968
+ const selectedIsolated = contextOnlyDimensionsByContext.get(selectedContext.id) ?? new Map();
969
+ for (const summary of dimensionSummaries) {
970
+ const otherLeanings = [...contextOnlyDimensionsByContext.entries()]
971
+ .filter(([contextId]) => contextId !== selectedContext.id)
972
+ .map(([, leaningByDimension]) => leaningByDimension.get(summary.dimensionId) ?? 0);
973
+ const averageOther = otherLeanings.length === 0
974
+ ? 0
975
+ : otherLeanings.reduce((sum, value) => sum + value, 0) /
976
+ otherLeanings.length;
977
+ averageSensitivityByDimension.set(summary.dimensionId, clamp(Math.abs((selectedIsolated.get(summary.dimensionId) ?? 0) - averageOther), 0, 1));
978
+ }
979
+ return {
980
+ scores,
981
+ dimensions: dimensionSummaries.map((summary) => ({
982
+ ...summary,
983
+ contextSensitivity: averageSensitivityByDimension.get(summary.dimensionId) ?? 0
984
+ }))
985
+ };
986
+ }
987
+ function buildNextPair(options) {
988
+ const { selectedContext, items, scores, judgments } = options;
989
+ const scoreByItemId = new Map(scores.map((score) => [score.itemId, score]));
990
+ const pairHistory = new Map();
991
+ for (const judgment of judgments.filter((entry) => entry.contextId === selectedContext.id)) {
992
+ const pairKey = [judgment.leftItemId, judgment.rightItemId].sort().join("::");
993
+ const current = pairHistory.get(pairKey) ?? { count: 0, lastCreatedAt: null };
994
+ pairHistory.set(pairKey, {
995
+ count: current.count + 1,
996
+ lastCreatedAt: !current.lastCreatedAt || judgment.createdAt > current.lastCreatedAt
997
+ ? judgment.createdAt
998
+ : current.lastCreatedAt
999
+ });
1000
+ }
1001
+ let best = null;
1002
+ for (let index = 0; index < items.length; index += 1) {
1003
+ for (let innerIndex = index + 1; innerIndex < items.length; innerIndex += 1) {
1004
+ const left = items[index];
1005
+ const right = items[innerIndex];
1006
+ const leftScore = scoreByItemId.get(left.id);
1007
+ const rightScore = scoreByItemId.get(right.id);
1008
+ if (!leftScore || !rightScore) {
1009
+ continue;
1010
+ }
1011
+ if (leftScore.status === "vetoed" || rightScore.status === "vetoed") {
1012
+ continue;
1013
+ }
1014
+ const pairKey = [left.id, right.id].sort().join("::");
1015
+ const history = pairHistory.get(pairKey);
1016
+ const uncertaintyGain = (leftScore.uncertainty + rightScore.uncertainty) / 2;
1017
+ const boundaryValue = 1 - Math.min(1, Math.abs(leftScore.latentScore - rightScore.latentScore));
1018
+ const diversityBonus = clamp(vectorDistance(left.featureWeights, right.featureWeights), 0, 1);
1019
+ const contextNeed = (leftScore.evidenceCount + rightScore.evidenceCount) < 6 ? 0.35 : 0.1;
1020
+ const driftProbe = !history?.lastCreatedAt || ageInDays(history.lastCreatedAt) > 45 ? 0.25 : 0;
1021
+ const repetitionPenalty = !history
1022
+ ? 0
1023
+ : ageInDays(history.lastCreatedAt) < 7
1024
+ ? 0.7 + history.count * 0.08
1025
+ : history.count * 0.08;
1026
+ const queueBias = (leftScore.compareLater || leftScore.bookmarked ? 0.15 : 0) +
1027
+ (rightScore.compareLater || rightScore.bookmarked ? 0.15 : 0);
1028
+ const candidateScore = uncertaintyGain +
1029
+ boundaryValue +
1030
+ diversityBonus +
1031
+ contextNeed +
1032
+ driftProbe +
1033
+ queueBias -
1034
+ repetitionPenalty;
1035
+ if (!best || candidateScore > best.score) {
1036
+ best = {
1037
+ left,
1038
+ right,
1039
+ score: candidateScore,
1040
+ rationale: [
1041
+ uncertaintyGain > 0.45
1042
+ ? "Both items still carry meaningful uncertainty."
1043
+ : "These items are close enough to refine the boundary.",
1044
+ boundaryValue > 0.5
1045
+ ? "Their current scores are close enough to be informative."
1046
+ : "This pair helps bridge different regions of the map.",
1047
+ driftProbe > 0
1048
+ ? "This pair also checks for drift in older assumptions."
1049
+ : "This pair improves the current local ordering."
1050
+ ]
1051
+ };
1052
+ }
1053
+ }
1054
+ }
1055
+ if (!best) {
1056
+ return null;
1057
+ }
1058
+ return {
1059
+ left: best.left,
1060
+ right: best.right,
1061
+ rationale: best.rationale,
1062
+ score: best.score
1063
+ };
1064
+ }
1065
+ function buildMap(items, scores) {
1066
+ const scoreByItemId = new Map(scores.map((score) => [score.itemId, score]));
1067
+ return items.map((item) => {
1068
+ const score = scoreByItemId.get(item.id);
1069
+ const x = item.featureWeights.novelty -
1070
+ item.featureWeights.familiarity +
1071
+ item.featureWeights.surprise * 0.5;
1072
+ const y = item.featureWeights.rigor * 0.7 +
1073
+ item.featureWeights.depth * 0.7 +
1074
+ item.featureWeights.structure * 0.5 -
1075
+ item.featureWeights.simplicity * 0.25;
1076
+ return {
1077
+ itemId: item.id,
1078
+ label: item.label,
1079
+ x: clamp(x, -2, 2),
1080
+ y: clamp(y, -2, 2),
1081
+ score: score?.latentScore ?? 0,
1082
+ confidence: score?.confidence ?? 0,
1083
+ uncertainty: score?.uncertainty ?? 1,
1084
+ status: score?.status ?? "uncertain",
1085
+ clusterKey: item.tags[0] ?? item.sourceEntityType ?? "untagged",
1086
+ tags: item.tags,
1087
+ sourceEntityType: item.sourceEntityType ?? null,
1088
+ sourceEntityId: item.sourceEntityId ?? null
1089
+ };
1090
+ });
1091
+ }
1092
+ function persistScoresAndDimensions(options) {
1093
+ const { profile, selectedContext, scores, dimensions, snapshotsSummary } = options;
1094
+ const database = getDatabase();
1095
+ const timestamp = nowIso();
1096
+ database
1097
+ .prepare(`DELETE FROM preference_item_scores WHERE context_id = ?`)
1098
+ .run(selectedContext.id);
1099
+ for (const score of scores) {
1100
+ database
1101
+ .prepare(`INSERT INTO preference_item_scores (
1102
+ id, profile_id, context_id, item_id, latent_score, confidence, uncertainty, evidence_count,
1103
+ pairwise_wins, pairwise_losses, pairwise_ties, signal_count, conflict_count, status,
1104
+ dominant_dimensions_json, explanation_json, manual_status, manual_score, confidence_lock,
1105
+ bookmarked, compare_later, frozen, last_inferred_at, last_judgment_at, updated_at
1106
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1107
+ .run(`pref_score_${randomUUID().slice(0, 10)}`, profile.id, selectedContext.id, score.itemId, score.latentScore, score.confidence, score.uncertainty, score.evidenceCount, score.pairwiseWins, score.pairwiseLosses, score.pairwiseTies, score.signalCount, score.conflictCount, score.status, JSON.stringify(score.dominantDimensions), JSON.stringify(score.explanation), score.manualStatus, score.manualScore, score.confidenceLock, score.bookmarked ? 1 : 0, score.compareLater ? 1 : 0, score.frozen ? 1 : 0, score.lastInferredAt, score.lastJudgmentAt, score.updatedAt);
1108
+ }
1109
+ database
1110
+ .prepare(`DELETE FROM preference_dimension_summaries WHERE context_id = ?`)
1111
+ .run(selectedContext.id);
1112
+ for (const summary of dimensions) {
1113
+ database
1114
+ .prepare(`INSERT INTO preference_dimension_summaries (
1115
+ id, profile_id, context_id, dimension_id, leaning, confidence, movement, context_sensitivity, evidence_count, updated_at
1116
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1117
+ .run(summary.id, profile.id, selectedContext.id, summary.dimensionId, summary.leaning, summary.confidence, summary.movement, summary.contextSensitivity, summary.evidenceCount, summary.updatedAt);
1118
+ }
1119
+ database
1120
+ .prepare(`INSERT INTO preference_snapshots (id, profile_id, context_id, summary_metrics_json, serialized_model_state_json, created_at)
1121
+ VALUES (?, ?, ?, ?, ?, ?)`)
1122
+ .run(`pref_snapshot_${randomUUID().slice(0, 10)}`, profile.id, selectedContext.id, JSON.stringify(snapshotsSummary), JSON.stringify({
1123
+ topScores: scores
1124
+ .slice()
1125
+ .sort((left, right) => right.latentScore - left.latentScore)
1126
+ .slice(0, 12)
1127
+ .map((score) => ({
1128
+ itemId: score.itemId,
1129
+ latentScore: score.latentScore,
1130
+ confidence: score.confidence,
1131
+ status: score.status
1132
+ })),
1133
+ dimensions: dimensions.map((dimension) => ({
1134
+ dimensionId: dimension.dimensionId,
1135
+ leaning: dimension.leaning,
1136
+ confidence: dimension.confidence
1137
+ }))
1138
+ }), timestamp);
1139
+ database
1140
+ .prepare(`DELETE FROM preference_snapshots
1141
+ WHERE context_id = ?
1142
+ AND id NOT IN (
1143
+ SELECT id
1144
+ FROM preference_snapshots
1145
+ WHERE context_id = ?
1146
+ ORDER BY created_at DESC
1147
+ LIMIT 48
1148
+ )`)
1149
+ .run(selectedContext.id, selectedContext.id);
1150
+ }
1151
+ function recomputeContext(profile, selectedContext) {
1152
+ const contexts = listContexts(profile.id);
1153
+ const items = listItems(profile.id);
1154
+ const judgments = listJudgmentsForContexts(contexts.map((context) => context.id));
1155
+ const signals = listSignalsForContexts(contexts.map((context) => context.id));
1156
+ const existingScores = listStoredScores(selectedContext.id);
1157
+ const { scores, dimensions } = computeScores({
1158
+ profile,
1159
+ contexts,
1160
+ selectedContext,
1161
+ items,
1162
+ judgments,
1163
+ signals,
1164
+ existingScores
1165
+ });
1166
+ persistScoresAndDimensions({
1167
+ profile,
1168
+ selectedContext,
1169
+ scores,
1170
+ dimensions,
1171
+ snapshotsSummary: {
1172
+ averageConfidence: scores.length === 0
1173
+ ? 0
1174
+ : scores.reduce((sum, score) => sum + score.confidence, 0) /
1175
+ scores.length,
1176
+ likedCount: scores.filter((score) => score.status === "liked").length,
1177
+ dislikedCount: scores.filter((score) => score.status === "disliked").length,
1178
+ uncertainCount: scores.filter((score) => score.status === "uncertain").length,
1179
+ totalItems: scores.length
1180
+ }
1181
+ });
1182
+ return { items, judgments, signals, contexts, selectedContext, scores, dimensions };
1183
+ }
1184
+ function buildWorkspace(profile, selectedContext, items, judgments, signals, scores, dimensions) {
1185
+ const catalogs = listCatalogs(profile.id);
1186
+ const storedScores = listStoredScores(selectedContext.id);
1187
+ const itemsById = new Map(items.map((item) => [item.id, item]));
1188
+ const mappedScores = storedScores
1189
+ .map((score) => {
1190
+ const item = itemsById.get(score.item_id);
1191
+ return item ? mapScore(score, item) : null;
1192
+ })
1193
+ .filter((score) => Boolean(score))
1194
+ .sort((left, right) => {
1195
+ if (right.confidence !== left.confidence) {
1196
+ return right.confidence - left.confidence;
1197
+ }
1198
+ return right.latentScore - left.latentScore;
1199
+ });
1200
+ const mappedDimensions = listStoredDimensions(selectedContext.id);
1201
+ const nextPair = buildNextPair({
1202
+ selectedContext,
1203
+ items,
1204
+ scores,
1205
+ judgments
1206
+ });
1207
+ const snapshots = listSnapshots(selectedContext.id, 24);
1208
+ const staleItemIds = mappedScores
1209
+ .filter((score) => ageInDays(score.lastJudgmentAt ?? score.updatedAt) >
1210
+ selectedContext.decayDays)
1211
+ .map((score) => score.itemId);
1212
+ const flippedItemIds = (() => {
1213
+ const recentSnapshots = [...snapshots].reverse();
1214
+ const signsByItemId = new Map();
1215
+ for (const snapshot of recentSnapshots) {
1216
+ const topScores = Array.isArray(snapshot.serializedModelState.topScores)
1217
+ ? snapshot.serializedModelState.topScores
1218
+ : [];
1219
+ for (const entry of topScores) {
1220
+ if (!entry.itemId || typeof entry.latentScore !== "number") {
1221
+ continue;
1222
+ }
1223
+ signsByItemId.set(entry.itemId, [
1224
+ ...(signsByItemId.get(entry.itemId) ?? []),
1225
+ Math.sign(entry.latentScore)
1226
+ ]);
1227
+ }
1228
+ }
1229
+ return [...signsByItemId.entries()]
1230
+ .filter(([, signs]) => {
1231
+ const filtered = signs.filter((sign) => sign !== 0);
1232
+ return filtered.length >= 2 && new Set(filtered).size > 1;
1233
+ })
1234
+ .map(([itemId]) => itemId);
1235
+ })();
1236
+ const workspace = preferenceWorkspacePayloadSchema.parse({
1237
+ profile,
1238
+ selectedContext,
1239
+ contexts: listContexts(profile.id),
1240
+ catalogs,
1241
+ dimensions: mappedDimensions,
1242
+ scores: mappedScores,
1243
+ map: buildMap(items, scores),
1244
+ history: {
1245
+ judgments: judgments.filter((judgment) => judgment.contextId === selectedContext.id),
1246
+ signals: signals.filter((signal) => signal.contextId === selectedContext.id),
1247
+ snapshots,
1248
+ staleItemIds,
1249
+ flippedItemIds
1250
+ },
1251
+ compare: {
1252
+ nextPair,
1253
+ pendingCount: mappedScores.filter((score) => score.uncertainty >= 0.5 || score.compareLater).length,
1254
+ candidateCount: items.length
1255
+ },
1256
+ summary: {
1257
+ totalItems: mappedScores.length,
1258
+ likedCount: mappedScores.filter((score) => score.status === "liked").length,
1259
+ dislikedCount: mappedScores.filter((score) => score.status === "disliked")
1260
+ .length,
1261
+ uncertainCount: mappedScores.filter((score) => score.status === "uncertain")
1262
+ .length,
1263
+ bookmarkedCount: mappedScores.filter((score) => score.bookmarked).length,
1264
+ vetoedCount: mappedScores.filter((score) => score.status === "vetoed").length,
1265
+ averageConfidence: mappedScores.length === 0
1266
+ ? 0
1267
+ : mappedScores.reduce((sum, score) => sum + score.confidence, 0) /
1268
+ mappedScores.length,
1269
+ pendingComparisons: mappedScores.filter((score) => score.uncertainty >= 0.5 || score.compareLater).length
1270
+ },
1271
+ libraries: {
1272
+ totalCatalogs: catalogs.length,
1273
+ totalCatalogItems: catalogs.reduce((sum, catalog) => sum + catalog.items.length, 0),
1274
+ seededCatalogCount: catalogs.filter((catalog) => catalog.source === "seeded")
1275
+ .length,
1276
+ customCatalogCount: catalogs.filter((catalog) => catalog.source === "custom")
1277
+ .length
1278
+ }
1279
+ });
1280
+ return workspace;
1281
+ }
1282
+ function resolveWorkspaceQuery(query) {
1283
+ const parsed = preferenceWorkspaceQuerySchema.parse(query);
1284
+ const userId = parsed.userId ?? getDefaultUser().id;
1285
+ const domain = parsed.domain ?? DEFAULT_PREFERENCE_DOMAIN;
1286
+ return { userId, domain, contextId: parsed.contextId ?? null };
1287
+ }
1288
+ export function getPreferenceWorkspace(query) {
1289
+ const { userId, domain, contextId } = resolveWorkspaceQuery(query);
1290
+ const profile = ensureProfile(userId, domain);
1291
+ const selectedContext = resolveContext(profile, contextId);
1292
+ const recomputed = recomputeContext(profile, selectedContext);
1293
+ return buildWorkspace(profile, selectedContext, recomputed.items, recomputed.judgments, recomputed.signals, recomputed.scores, recomputed.dimensions);
1294
+ }
1295
+ export function createPreferenceCatalog(input) {
1296
+ const parsed = createPreferenceCatalogSchema.parse(input);
1297
+ const profile = ensureProfile(parsed.userId, parsed.domain);
1298
+ const timestamp = nowIso();
1299
+ const baseSlug = slugify(parsed.slug || parsed.title) || "concept-list";
1300
+ const existingSlugs = new Set(listCatalogs(profile.id).map((catalog) => catalog.slug));
1301
+ let slug = baseSlug;
1302
+ let index = 2;
1303
+ while (existingSlugs.has(slug)) {
1304
+ slug = `${baseSlug}-${index}`;
1305
+ index += 1;
1306
+ }
1307
+ const catalogId = `pref_catalog_${randomUUID().slice(0, 10)}`;
1308
+ getDatabase()
1309
+ .prepare(`INSERT INTO preference_catalogs (
1310
+ id, profile_id, domain, slug, title, description, source, archived, created_at, updated_at
1311
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`)
1312
+ .run(catalogId, profile.id, parsed.domain, slug, parsed.title, parsed.description, preferenceCatalogSourceSchema.enum.custom, timestamp, timestamp);
1313
+ return readCatalog(catalogId);
1314
+ }
1315
+ export function updatePreferenceCatalog(catalogId, patch) {
1316
+ const current = readCatalog(catalogId);
1317
+ if (!current) {
1318
+ throw new HttpError(404, "preferences_catalog_not_found", `Preference catalog ${catalogId} was not found.`);
1319
+ }
1320
+ const parsed = updatePreferenceCatalogSchema.parse(patch);
1321
+ const timestamp = nowIso();
1322
+ const nextTitle = parsed.title ?? current.title;
1323
+ const desiredSlug = slugify(parsed.slug || nextTitle) || current.slug;
1324
+ const siblingSlugs = new Set(listCatalogs(current.profileId)
1325
+ .filter((catalog) => catalog.id !== current.id)
1326
+ .map((catalog) => catalog.slug));
1327
+ let nextSlug = desiredSlug;
1328
+ let index = 2;
1329
+ while (siblingSlugs.has(nextSlug)) {
1330
+ nextSlug = `${desiredSlug}-${index}`;
1331
+ index += 1;
1332
+ }
1333
+ getDatabase()
1334
+ .prepare(`UPDATE preference_catalogs
1335
+ SET slug = ?, title = ?, description = ?, updated_at = ?
1336
+ WHERE id = ?`)
1337
+ .run(nextSlug, nextTitle, parsed.description ?? current.description, timestamp, catalogId);
1338
+ return readCatalog(catalogId);
1339
+ }
1340
+ export function deletePreferenceCatalog(catalogId) {
1341
+ const current = readCatalog(catalogId);
1342
+ if (!current) {
1343
+ throw new HttpError(404, "preferences_catalog_not_found", `Preference catalog ${catalogId} was not found.`);
1344
+ }
1345
+ const timestamp = nowIso();
1346
+ runInTransaction(() => {
1347
+ getDatabase()
1348
+ .prepare(`UPDATE preference_catalogs
1349
+ SET archived = 1, updated_at = ?
1350
+ WHERE id = ?`)
1351
+ .run(timestamp, catalogId);
1352
+ getDatabase()
1353
+ .prepare(`UPDATE preference_catalog_items
1354
+ SET archived = 1, updated_at = ?
1355
+ WHERE catalog_id = ?`)
1356
+ .run(timestamp, catalogId);
1357
+ });
1358
+ return current;
1359
+ }
1360
+ export function createPreferenceCatalogItem(input) {
1361
+ const parsed = createPreferenceCatalogItemSchema.parse(input);
1362
+ const catalog = readCatalog(parsed.catalogId);
1363
+ if (!catalog) {
1364
+ throw new HttpError(404, "preferences_catalog_not_found", `Preference catalog ${parsed.catalogId} was not found.`);
1365
+ }
1366
+ const timestamp = nowIso();
1367
+ const position = parsed.position ??
1368
+ (catalog.items.reduce((max, item) => Math.max(max, item.position), -1) + 1);
1369
+ const itemId = `pref_catalog_item_${randomUUID().slice(0, 10)}`;
1370
+ getDatabase()
1371
+ .prepare(`INSERT INTO preference_catalog_items (
1372
+ id, catalog_id, label, description, tags_json, feature_weights_json, position, archived, created_at, updated_at
1373
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)`)
1374
+ .run(itemId, catalog.id, parsed.label, parsed.description, JSON.stringify(parsed.tags), JSON.stringify(normalizeDimensionVector(parsed.featureWeights)), position, timestamp, timestamp);
1375
+ return readCatalogItem(itemId);
1376
+ }
1377
+ export function updatePreferenceCatalogItem(catalogItemId, patch) {
1378
+ const current = readCatalogItem(catalogItemId);
1379
+ if (!current) {
1380
+ throw new HttpError(404, "preferences_catalog_item_not_found", `Preference catalog item ${catalogItemId} was not found.`);
1381
+ }
1382
+ const parsed = updatePreferenceCatalogItemSchema.parse(patch);
1383
+ const timestamp = nowIso();
1384
+ getDatabase()
1385
+ .prepare(`UPDATE preference_catalog_items
1386
+ SET label = ?, description = ?, tags_json = ?, feature_weights_json = ?, position = ?, updated_at = ?
1387
+ WHERE id = ?`)
1388
+ .run(parsed.label ?? current.label, parsed.description ?? current.description, JSON.stringify(parsed.tags ?? current.tags), JSON.stringify(parsed.featureWeights !== undefined
1389
+ ? normalizeDimensionVector(parsed.featureWeights)
1390
+ : current.featureWeights), parsed.position ?? current.position, timestamp, catalogItemId);
1391
+ return readCatalogItem(catalogItemId);
1392
+ }
1393
+ export function deletePreferenceCatalogItem(catalogItemId) {
1394
+ const current = readCatalogItem(catalogItemId);
1395
+ if (!current) {
1396
+ throw new HttpError(404, "preferences_catalog_item_not_found", `Preference catalog item ${catalogItemId} was not found.`);
1397
+ }
1398
+ getDatabase()
1399
+ .prepare(`UPDATE preference_catalog_items
1400
+ SET archived = 1, updated_at = ?
1401
+ WHERE id = ?`)
1402
+ .run(nowIso(), catalogItemId);
1403
+ return current;
1404
+ }
1405
+ export function startPreferenceGame(input) {
1406
+ const parsed = startPreferenceGameSchema.parse(input);
1407
+ const profile = ensureProfile(parsed.userId, parsed.domain);
1408
+ const selectedContext = resolveContext(profile, parsed.contextId ?? null);
1409
+ if (parsed.catalogId) {
1410
+ const catalog = readCatalog(parsed.catalogId);
1411
+ if (!catalog || catalog.profileId !== profile.id) {
1412
+ throw new HttpError(404, "preferences_catalog_not_found", `Preference catalog ${parsed.catalogId} was not found for this profile.`);
1413
+ }
1414
+ const existingItems = listItems(profile.id);
1415
+ for (const catalogItem of catalog.items) {
1416
+ const matched = existingItems.find((item) => {
1417
+ const seedCatalogId = typeof item.metadata.seedCatalogId === "string"
1418
+ ? item.metadata.seedCatalogId
1419
+ : null;
1420
+ const seedCatalogItemId = typeof item.metadata.seedCatalogItemId === "string"
1421
+ ? item.metadata.seedCatalogItemId
1422
+ : null;
1423
+ return (seedCatalogId === catalog.id && seedCatalogItemId === catalogItem.id);
1424
+ });
1425
+ if (matched) {
1426
+ updatePreferenceItem(matched.id, {
1427
+ label: catalogItem.label,
1428
+ description: catalogItem.description,
1429
+ tags: catalogItem.tags,
1430
+ featureWeights: catalogItem.featureWeights,
1431
+ metadata: {
1432
+ ...matched.metadata,
1433
+ seedCatalogId: catalog.id,
1434
+ seedCatalogItemId: catalogItem.id,
1435
+ seedCatalogTitle: catalog.title
1436
+ }
1437
+ });
1438
+ upsertPreferenceScoreState(matched.id, selectedContext.id, {
1439
+ compareLater: true,
1440
+ bookmarked: true
1441
+ });
1442
+ continue;
1443
+ }
1444
+ const createdItem = createPreferenceItem({
1445
+ userId: parsed.userId,
1446
+ domain: parsed.domain,
1447
+ label: catalogItem.label,
1448
+ description: catalogItem.description,
1449
+ tags: catalogItem.tags,
1450
+ featureWeights: catalogItem.featureWeights,
1451
+ metadata: {
1452
+ seedCatalogId: catalog.id,
1453
+ seedCatalogItemId: catalogItem.id,
1454
+ seedCatalogTitle: catalog.title
1455
+ },
1456
+ queueForCompare: true
1457
+ });
1458
+ upsertPreferenceScoreState(createdItem.id, selectedContext.id, {
1459
+ compareLater: true,
1460
+ bookmarked: true
1461
+ });
1462
+ }
1463
+ }
1464
+ return getPreferenceWorkspace({
1465
+ userId: parsed.userId,
1466
+ domain: parsed.domain,
1467
+ contextId: selectedContext.id
1468
+ });
1469
+ }
1470
+ export function createPreferenceContext(input) {
1471
+ const parsed = createPreferenceContextSchema.parse(input);
1472
+ const profile = ensureProfile(parsed.userId, parsed.domain);
1473
+ const contextId = `pref_ctx_${randomUUID().slice(0, 10)}`;
1474
+ const timestamp = nowIso();
1475
+ runInTransaction(() => {
1476
+ if (parsed.isDefault) {
1477
+ getDatabase()
1478
+ .prepare(`UPDATE preference_contexts
1479
+ SET is_default = 0, updated_at = ?
1480
+ WHERE profile_id = ?`)
1481
+ .run(timestamp, profile.id);
1482
+ }
1483
+ getDatabase()
1484
+ .prepare(`INSERT INTO preference_contexts (id, profile_id, name, description, share_mode, active, is_default, decay_days, created_at, updated_at)
1485
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1486
+ .run(contextId, profile.id, parsed.name, parsed.description, parsed.shareMode, parsed.active ? 1 : 0, parsed.isDefault ? 1 : 0, parsed.decayDays, timestamp, timestamp);
1487
+ if (parsed.isDefault) {
1488
+ getDatabase()
1489
+ .prepare(`UPDATE preference_profiles
1490
+ SET default_context_id = ?, updated_at = ?
1491
+ WHERE id = ?`)
1492
+ .run(contextId, timestamp, profile.id);
1493
+ }
1494
+ });
1495
+ return readContext(contextId);
1496
+ }
1497
+ export function updatePreferenceContext(contextId, patch) {
1498
+ const current = readContext(contextId);
1499
+ if (!current) {
1500
+ throw new HttpError(404, "preferences_context_not_found", `Preference context ${contextId} was not found.`);
1501
+ }
1502
+ const parsed = updatePreferenceContextSchema.parse(patch);
1503
+ const next = {
1504
+ name: parsed.name ?? current.name,
1505
+ description: parsed.description ?? current.description,
1506
+ shareMode: parsed.shareMode ?? current.shareMode,
1507
+ active: parsed.active ?? current.active,
1508
+ isDefault: parsed.isDefault ?? current.isDefault,
1509
+ decayDays: parsed.decayDays ?? current.decayDays
1510
+ };
1511
+ const timestamp = nowIso();
1512
+ runInTransaction(() => {
1513
+ if (next.isDefault) {
1514
+ getDatabase()
1515
+ .prepare(`UPDATE preference_contexts
1516
+ SET is_default = 0, updated_at = ?
1517
+ WHERE profile_id = ?`)
1518
+ .run(timestamp, current.profileId);
1519
+ getDatabase()
1520
+ .prepare(`UPDATE preference_profiles
1521
+ SET default_context_id = ?, updated_at = ?
1522
+ WHERE id = ?`)
1523
+ .run(contextId, timestamp, current.profileId);
1524
+ }
1525
+ getDatabase()
1526
+ .prepare(`UPDATE preference_contexts
1527
+ SET name = ?, description = ?, share_mode = ?, active = ?, is_default = ?, decay_days = ?, updated_at = ?
1528
+ WHERE id = ?`)
1529
+ .run(next.name, next.description, next.shareMode, next.active ? 1 : 0, next.isDefault ? 1 : 0, next.decayDays, timestamp, contextId);
1530
+ });
1531
+ const profile = readProfileById(current.profileId);
1532
+ const updated = readContext(contextId);
1533
+ if (profile) {
1534
+ recomputeContext(profile, updated);
1535
+ }
1536
+ return updated;
1537
+ }
1538
+ export function mergePreferenceContexts(input) {
1539
+ const parsed = mergePreferenceContextsSchema.parse(input);
1540
+ const source = readContext(parsed.sourceContextId);
1541
+ const target = readContext(parsed.targetContextId);
1542
+ if (!source || !target || source.profileId !== target.profileId) {
1543
+ throw new HttpError(400, "preferences_invalid_context_merge", "Preference contexts must exist on the same profile before merging.");
1544
+ }
1545
+ const timestamp = nowIso();
1546
+ runInTransaction(() => {
1547
+ getDatabase()
1548
+ .prepare(`UPDATE pairwise_judgments
1549
+ SET context_id = ?
1550
+ WHERE context_id = ?`)
1551
+ .run(target.id, source.id);
1552
+ getDatabase()
1553
+ .prepare(`UPDATE absolute_signals
1554
+ SET context_id = ?
1555
+ WHERE context_id = ?`)
1556
+ .run(target.id, source.id);
1557
+ getDatabase()
1558
+ .prepare(`DELETE FROM preference_item_scores
1559
+ WHERE context_id = ?`)
1560
+ .run(source.id);
1561
+ getDatabase()
1562
+ .prepare(`DELETE FROM preference_dimension_summaries
1563
+ WHERE context_id = ?`)
1564
+ .run(source.id);
1565
+ getDatabase()
1566
+ .prepare(`UPDATE preference_contexts
1567
+ SET active = 0, updated_at = ?
1568
+ WHERE id = ?`)
1569
+ .run(timestamp, source.id);
1570
+ });
1571
+ const profile = readProfileById(source.profileId);
1572
+ if (profile) {
1573
+ recomputeContext(profile, target);
1574
+ }
1575
+ return {
1576
+ target: readContext(target.id),
1577
+ source: readContext(source.id)
1578
+ };
1579
+ }
1580
+ export function createPreferenceItem(input) {
1581
+ const parsed = createPreferenceItemSchema.parse(input);
1582
+ const profile = ensureProfile(parsed.userId, parsed.domain);
1583
+ const itemId = `pref_item_${randomUUID().slice(0, 10)}`;
1584
+ const timestamp = nowIso();
1585
+ getDatabase()
1586
+ .prepare(`INSERT INTO preference_items (id, profile_id, label, description, tags_json, feature_weights_json, source_entity_type, source_entity_id, metadata_json, created_at, updated_at)
1587
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1588
+ .run(itemId, profile.id, parsed.label, parsed.description, JSON.stringify(parsed.tags), JSON.stringify(normalizeDimensionVector(parsed.featureWeights)), parsed.sourceEntityType ?? null, parsed.sourceEntityId ?? null, JSON.stringify(parsed.metadata ?? {}), timestamp, timestamp);
1589
+ const created = getItemById(itemId);
1590
+ const selectedContext = resolveContext(profile, null);
1591
+ if (parsed.queueForCompare) {
1592
+ upsertPreferenceScoreState(itemId, selectedContext.id, {
1593
+ compareLater: true,
1594
+ bookmarked: true
1595
+ });
1596
+ }
1597
+ recomputeContext(profile, selectedContext);
1598
+ return created;
1599
+ }
1600
+ function upsertPreferenceScoreState(itemId, contextId, patch) {
1601
+ const item = getItemById(itemId);
1602
+ if (!item) {
1603
+ throw new HttpError(404, "preferences_item_not_found", `Preference item ${itemId} was not found.`);
1604
+ }
1605
+ const profile = readProfileById(item.profileId);
1606
+ const context = readContext(contextId);
1607
+ if (!profile || !context || context.profileId !== profile.id) {
1608
+ throw new HttpError(400, "preferences_invalid_score_context", "Preference score context is invalid for this item.");
1609
+ }
1610
+ const existing = listStoredScores(contextId).find((score) => score.item_id === itemId);
1611
+ const timestamp = nowIso();
1612
+ if (!existing) {
1613
+ getDatabase()
1614
+ .prepare(`INSERT INTO preference_item_scores (
1615
+ id, profile_id, context_id, item_id, latent_score, confidence, uncertainty, evidence_count, pairwise_wins, pairwise_losses, pairwise_ties, signal_count, conflict_count, status, dominant_dimensions_json, explanation_json, manual_status, manual_score, confidence_lock, bookmarked, compare_later, frozen, last_inferred_at, last_judgment_at, updated_at
1616
+ ) VALUES (?, ?, ?, ?, 0, 0, 1, 0, 0, 0, 0, 0, 0, 'uncertain', '[]', '[]', ?, ?, ?, ?, ?, ?, ?, NULL, ?)`)
1617
+ .run(`pref_score_${randomUUID().slice(0, 10)}`, profile.id, contextId, itemId, patch.manualStatus ?? null, patch.manualScore ?? null, patch.confidenceLock ?? null, patch.bookmarked ? 1 : 0, patch.compareLater ? 1 : 0, patch.frozen ? 1 : 0, timestamp, timestamp);
1618
+ return;
1619
+ }
1620
+ getDatabase()
1621
+ .prepare(`UPDATE preference_item_scores
1622
+ SET manual_status = ?, manual_score = ?, confidence_lock = ?, bookmarked = ?, compare_later = ?, frozen = ?, updated_at = ?
1623
+ WHERE context_id = ? AND item_id = ?`)
1624
+ .run(patch.manualStatus ?? existing.manual_status, patch.manualScore ?? existing.manual_score, patch.confidenceLock ?? existing.confidence_lock, (patch.bookmarked ?? (existing.bookmarked === 1)) ? 1 : 0, (patch.compareLater ?? (existing.compare_later === 1)) ? 1 : 0, (patch.frozen ?? (existing.frozen === 1)) ? 1 : 0, timestamp, contextId, itemId);
1625
+ }
1626
+ export function updatePreferenceItem(itemId, patch) {
1627
+ const item = getItemById(itemId);
1628
+ if (!item) {
1629
+ throw new HttpError(404, "preferences_item_not_found", `Preference item ${itemId} was not found.`);
1630
+ }
1631
+ const parsed = updatePreferenceItemSchema.parse(patch);
1632
+ const next = {
1633
+ label: parsed.label ?? item.label,
1634
+ description: parsed.description ?? item.description,
1635
+ tags: parsed.tags ?? item.tags,
1636
+ featureWeights: parsed.featureWeights !== undefined
1637
+ ? normalizeDimensionVector(parsed.featureWeights)
1638
+ : item.featureWeights,
1639
+ sourceEntityType: parsed.sourceEntityType !== undefined
1640
+ ? parsed.sourceEntityType
1641
+ : item.sourceEntityType ?? null,
1642
+ sourceEntityId: parsed.sourceEntityId !== undefined
1643
+ ? parsed.sourceEntityId
1644
+ : item.sourceEntityId ?? null,
1645
+ metadata: parsed.metadata !== undefined
1646
+ ? parsed.metadata
1647
+ : item.metadata
1648
+ };
1649
+ const timestamp = nowIso();
1650
+ getDatabase()
1651
+ .prepare(`UPDATE preference_items
1652
+ SET label = ?, description = ?, tags_json = ?, feature_weights_json = ?, source_entity_type = ?, source_entity_id = ?, metadata_json = ?, updated_at = ?
1653
+ WHERE id = ?`)
1654
+ .run(next.label, next.description, JSON.stringify(next.tags), JSON.stringify(next.featureWeights), next.sourceEntityType, next.sourceEntityId, JSON.stringify(next.metadata ?? {}), timestamp, itemId);
1655
+ const updated = getItemById(itemId);
1656
+ const profile = readProfileById(item.profileId);
1657
+ if (profile) {
1658
+ for (const context of listContexts(profile.id).filter((entry) => entry.active)) {
1659
+ recomputeContext(profile, context);
1660
+ }
1661
+ }
1662
+ return updated;
1663
+ }
1664
+ export function createPreferenceItemFromEntity(input) {
1665
+ const parsed = enqueueEntityPreferenceItemSchema.parse(input);
1666
+ const profile = ensureProfile(parsed.userId, parsed.domain);
1667
+ const existing = listItems(profile.id).find((item) => item.sourceEntityType === parsed.entityType &&
1668
+ item.sourceEntityId === parsed.entityId);
1669
+ const source = resolveSourceEntity(parsed.entityType, parsed.entityId);
1670
+ if (!source) {
1671
+ throw new HttpError(404, "preferences_source_entity_not_found", `${parsed.entityType} ${parsed.entityId} was not found.`);
1672
+ }
1673
+ const item = existing ??
1674
+ createPreferenceItem({
1675
+ userId: parsed.userId,
1676
+ domain: parsed.domain,
1677
+ label: parsed.label?.trim() || source.label,
1678
+ description: parsed.description?.trim() || source.description,
1679
+ tags: parsed.tags,
1680
+ sourceEntityType: parsed.entityType,
1681
+ sourceEntityId: parsed.entityId,
1682
+ metadata: { seededFromEntity: true },
1683
+ queueForCompare: true,
1684
+ featureWeights: DEFAULT_DIMENSIONS
1685
+ });
1686
+ const selectedContext = resolveContext(profile, null);
1687
+ upsertPreferenceScoreState(item.id, selectedContext.id, {
1688
+ bookmarked: true,
1689
+ compareLater: true
1690
+ });
1691
+ recomputeContext(profile, selectedContext);
1692
+ return item;
1693
+ }
1694
+ export function submitPairwiseJudgment(input) {
1695
+ const parsed = submitPairwiseJudgmentSchema.parse(input);
1696
+ const profile = ensureProfile(parsed.userId, parsed.domain);
1697
+ const context = readContext(parsed.contextId);
1698
+ if (!context || context.profileId !== profile.id) {
1699
+ throw new HttpError(400, "preferences_invalid_context", "Preference judgment context does not belong to the selected profile.");
1700
+ }
1701
+ if (parsed.leftItemId === parsed.rightItemId) {
1702
+ throw new HttpError(400, "preferences_invalid_pair", "Preference comparisons require two distinct items.");
1703
+ }
1704
+ if (!getItemById(parsed.leftItemId) || !getItemById(parsed.rightItemId)) {
1705
+ throw new HttpError(404, "preferences_item_not_found", "One or both preference items do not exist.");
1706
+ }
1707
+ const judgmentId = `pref_judgment_${randomUUID().slice(0, 10)}`;
1708
+ const timestamp = nowIso();
1709
+ getDatabase()
1710
+ .prepare(`INSERT INTO pairwise_judgments (id, profile_id, context_id, user_id, left_item_id, right_item_id, outcome, strength, response_time_ms, source, reason_tags_json, created_at)
1711
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'ui', ?, ?)`)
1712
+ .run(judgmentId, profile.id, context.id, parsed.userId, parsed.leftItemId, parsed.rightItemId, parsed.outcome, parsed.strength, parsed.responseTimeMs ?? null, JSON.stringify(parsed.reasonTags), timestamp);
1713
+ recomputeContext(profile, context);
1714
+ return mapJudgment(getDatabase()
1715
+ .prepare(`SELECT id, profile_id, context_id, user_id, left_item_id, right_item_id, outcome, strength, response_time_ms, source, reason_tags_json, created_at
1716
+ FROM pairwise_judgments
1717
+ WHERE id = ?`)
1718
+ .get(judgmentId));
1719
+ }
1720
+ export function submitAbsoluteSignal(input) {
1721
+ const parsed = submitAbsoluteSignalSchema.parse(input);
1722
+ const profile = ensureProfile(parsed.userId, parsed.domain);
1723
+ const context = readContext(parsed.contextId);
1724
+ if (!context || context.profileId !== profile.id) {
1725
+ throw new HttpError(400, "preferences_invalid_context", "Preference signal context does not belong to the selected profile.");
1726
+ }
1727
+ if (!getItemById(parsed.itemId)) {
1728
+ throw new HttpError(404, "preferences_item_not_found", `Preference item ${parsed.itemId} was not found.`);
1729
+ }
1730
+ const signalId = `pref_signal_${randomUUID().slice(0, 10)}`;
1731
+ const timestamp = nowIso();
1732
+ getDatabase()
1733
+ .prepare(`INSERT INTO absolute_signals (id, profile_id, context_id, user_id, item_id, signal_type, strength, source, created_at)
1734
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'ui', ?)`)
1735
+ .run(signalId, profile.id, context.id, parsed.userId, parsed.itemId, parsed.signalType, parsed.strength, timestamp);
1736
+ recomputeContext(profile, context);
1737
+ return mapSignal(getDatabase()
1738
+ .prepare(`SELECT id, profile_id, context_id, user_id, item_id, signal_type, strength, source, created_at
1739
+ FROM absolute_signals
1740
+ WHERE id = ?`)
1741
+ .get(signalId));
1742
+ }
1743
+ export function updatePreferenceScore(itemId, input) {
1744
+ const parsed = updatePreferenceScoreSchema.parse(input);
1745
+ const profile = ensureProfile(parsed.userId, parsed.domain);
1746
+ const context = readContext(parsed.contextId);
1747
+ if (!context || context.profileId !== profile.id) {
1748
+ throw new HttpError(400, "preferences_invalid_context", "Preference score context does not belong to the selected profile.");
1749
+ }
1750
+ upsertPreferenceScoreState(itemId, context.id, {
1751
+ manualStatus: parsed.manualStatus !== undefined ? parsed.manualStatus ?? null : undefined,
1752
+ manualScore: parsed.manualScore !== undefined ? parsed.manualScore ?? null : undefined,
1753
+ confidenceLock: parsed.confidenceLock !== undefined
1754
+ ? parsed.confidenceLock ?? null
1755
+ : undefined,
1756
+ bookmarked: parsed.bookmarked,
1757
+ compareLater: parsed.compareLater,
1758
+ frozen: parsed.frozen
1759
+ });
1760
+ return getPreferenceWorkspace({
1761
+ userId: parsed.userId,
1762
+ domain: parsed.domain,
1763
+ contextId: context.id
1764
+ });
1765
+ }