fuli-context 0.7.3 → 0.7.4-beta.2

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 (108) hide show
  1. package/README.md +170 -18
  2. package/README.zh-CN.md +146 -12
  3. package/compose.graphiti.yml +2 -0
  4. package/dist/web/assets/{AboutPage-qeO_vm0T.js → AboutPage-CftRexSY.js} +1 -1
  5. package/dist/web/assets/{ConnectionsPage-DO0GR9Xg.js → ConnectionsPage-D5J91x6d.js} +1 -1
  6. package/dist/web/assets/{useMinimumLoadingDisplay-BpCyBeko.css → KnowledgeEditDialog-DWUV_NPp.css} +1 -1
  7. package/dist/web/assets/KnowledgeEditDialog-wotVahvr.js +1 -0
  8. package/dist/web/assets/{KnowledgeInspector-Bjhdu2mf.css → KnowledgeInspector--1NujtXW.css} +1 -1
  9. package/dist/web/assets/KnowledgeInspector-BzkW0mVs.js +1 -0
  10. package/dist/web/assets/KnowledgeOrganizerPage-BebUDiHt.js +1 -0
  11. package/dist/web/assets/KnowledgePage-OUT07aKH.js +1 -0
  12. package/dist/web/assets/KnowledgeWorkspace-1pHJ0MNZ.js +4 -0
  13. package/dist/web/assets/{OverviewPage-CmCcDCAs.js → OverviewPage-CHE6XhdS.js} +1 -1
  14. package/dist/web/assets/PersonalProfilePage-B7T09bow.css +1 -0
  15. package/dist/web/assets/PersonalProfilePage-CXXiabUy.js +1 -0
  16. package/dist/web/assets/PersonalProjectsPage-d2wYGW8n.js +1 -0
  17. package/dist/web/assets/PublicProjectsPage-hxLShtnZ.js +1 -0
  18. package/dist/web/assets/{ReviewPage-B3vnZ6gn.js → ReviewPage-DUeyu7E8.js} +1 -1
  19. package/dist/web/assets/{SearchableSelect-ih69qbcS.js → SearchableSelect-C5RiB5F8.js} +1 -1
  20. package/dist/web/assets/SettingsPage-BMuASSzQ.js +1 -0
  21. package/dist/web/assets/SettingsPage-CAfz4qiV.css +1 -0
  22. package/dist/web/assets/WritingTastePage-B9BCYIwQ.js +1 -0
  23. package/dist/web/assets/WritingTastePage-DSgvTH6x.css +1 -0
  24. package/dist/web/assets/{console-DE40hXGC.js → console-DMvHxKf9.js} +1 -1
  25. package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
  26. package/dist/web/assets/{i18n-CDxBs1sz.js → i18n-CWfd8Dtt.js} +1 -1
  27. package/dist/web/assets/{identity-BCxb1mcy.js → identity-C9mLdR8L.js} +1 -1
  28. package/dist/web/assets/{index-CCOGfZzE.css → index-X2j29j8i.css} +1 -1
  29. package/dist/web/assets/index-nP5p5OaY.js +2 -0
  30. package/dist/web/assets/{model-CCMdt10Q.js → model-CsYAR8O3.js} +1 -1
  31. package/dist/web/assets/paths-CDREouhi.js +1 -0
  32. package/dist/web/assets/useApi-CROJJdhE-BLQiPVw0.js +1 -0
  33. package/dist/web/assets/vue-router-DN8x6iY1.js +1 -0
  34. package/dist/web/index.html +7 -6
  35. package/graph-provider/fuli_graph/app.py +238 -2
  36. package/graph-provider/fuli_graph/auth.py +18 -0
  37. package/graph-provider/fuli_graph/collaboration_context.py +253 -17
  38. package/graph-provider/fuli_graph/common_knowledge.py +6 -2
  39. package/graph-provider/fuli_graph/config.py +5 -0
  40. package/graph-provider/fuli_graph/graph_models.py +6 -0
  41. package/graph-provider/fuli_graph/graph_projection.py +14 -0
  42. package/graph-provider/fuli_graph/graph_query.py +27 -3
  43. package/graph-provider/fuli_graph/knowledge_management.py +19 -1
  44. package/graph-provider/fuli_graph/knowledge_search.py +11 -0
  45. package/graph-provider/fuli_graph/models.py +15 -2
  46. package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
  47. package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
  48. package/graph-provider/fuli_graph/project_action_models.py +22 -0
  49. package/graph-provider/fuli_graph/project_knowledge.py +121 -2
  50. package/graph-provider/fuli_graph/provider_values.py +24 -0
  51. package/graph-provider/fuli_graph/runtime.py +78 -0
  52. package/graph-provider/fuli_graph/search_projection.py +14 -1
  53. package/graph-provider/fuli_graph/store_knowledge.py +116 -2
  54. package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
  55. package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
  56. package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
  57. package/graph-provider/fuli_graph/workflow_observations.py +84 -0
  58. package/graph-provider/pyproject.toml +1 -1
  59. package/npm-shrinkwrap.json +21 -21
  60. package/package.json +4 -4
  61. package/skills/capturing-session-knowledge/SKILL.md +32 -5
  62. package/skills/flreview/SKILL.md +4 -3
  63. package/src/agent-tools/graph-definitions.js +136 -20
  64. package/src/agent-tools/graph-handlers.js +10 -4
  65. package/src/cli/command-registry.js +1 -1
  66. package/src/cli/local-runtime-command.js +69 -2
  67. package/src/graphiti/agent-knowledge-workflows.js +497 -16
  68. package/src/graphiti/collaboration-preference-projection.js +162 -0
  69. package/src/graphiti/federated-application.js +155 -161
  70. package/src/graphiti/federated-search-support.js +102 -0
  71. package/src/graphiti/knowledge-provider-mapping.js +15 -0
  72. package/src/graphiti/provider-client.js +162 -10
  73. package/src/graphiti/related-project-suggestions.js +109 -0
  74. package/src/graphiti/runtime-config.js +14 -0
  75. package/src/graphiti/source-marker.js +2 -2
  76. package/src/graphiti/task-knowledge-recall.js +66 -15
  77. package/src/graphiti/user-taste-skill.js +241 -0
  78. package/src/graphiti/workflow-candidate-mapping.js +87 -0
  79. package/src/graphiti/workflow-observation.js +82 -0
  80. package/src/graphiti/writing-taste-profile-workflow.js +25 -0
  81. package/src/graphiti/writing-taste-profile.js +490 -0
  82. package/src/http/graph-api-router.js +15 -2
  83. package/src/mcp/context-resources.js +158 -0
  84. package/src/mcp/create-mcp-server.js +39 -5
  85. package/src/mcp/instructions.js +5 -7
  86. package/src/mcp/session-id.js +60 -0
  87. package/src/mcp/task-context-registry.js +15 -3
  88. package/src/mcp/tool-annotations.js +8 -1
  89. package/src/mcp/tool-result.js +28 -4
  90. package/src/server.js +0 -1
  91. package/src/setup/claude-code-config.js +17 -6
  92. package/src/setup/graph-runtime.js +20 -2
  93. package/src/system/runtime-settings.js +121 -2
  94. package/src/system/system-service.js +5 -1
  95. package/dist/web/assets/KnowledgeInspector-DEW5lA1m.js +0 -1
  96. package/dist/web/assets/KnowledgeOrganizerPage-BkM568Ld.js +0 -1
  97. package/dist/web/assets/KnowledgePage-BAT4aZIm.js +0 -1
  98. package/dist/web/assets/KnowledgeWorkspace-DyGtBz--.js +0 -4
  99. package/dist/web/assets/PersonalProfilePage-C42mwKN8.js +0 -1
  100. package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
  101. package/dist/web/assets/PersonalProjectsPage-BwaXR62K.js +0 -1
  102. package/dist/web/assets/PublicProjectsPage-egKBulOq.js +0 -1
  103. package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
  104. package/dist/web/assets/SettingsPage-oLE8mNs5.js +0 -1
  105. package/dist/web/assets/index-BzhNfzU_.js +0 -2
  106. package/dist/web/assets/paths-DwHeiGMT.js +0 -1
  107. package/dist/web/assets/useMinimumLoadingDisplay-HdI7XAmu.js +0 -1
  108. package/dist/web/assets/vue-router-Drfl1F8i.js +0 -1
@@ -5,6 +5,8 @@ import {
5
5
  startLocalRuntime,
6
6
  stopLocalRuntime
7
7
  } from '../local-runtime/lifecycle.js';
8
+ import { discoverAgents } from '../setup/agents.js';
9
+ import { inspectAgentInstallations } from '../setup/agent-installation-status.js';
8
10
  import { resolveSetupPaths } from '../setup/paths.js';
9
11
  import {
10
12
  DEFAULT_RUNTIME_SETTINGS,
@@ -43,15 +45,28 @@ export async function runLocalRuntimeCommand(command, args, dependencies = {}) {
43
45
  status: dependencies.inspect ?? inspectLocalRuntime,
44
46
  open: dependencies.openConsole ?? openLocalConsole
45
47
  };
48
+ const agentSetupCheck = command === 'start'
49
+ ? checkAgentSetup({
50
+ paths,
51
+ env,
52
+ nodePath: dependencies.nodePath ?? process.execPath,
53
+ discover: dependencies.discoverAgents ?? discoverAgents,
54
+ inspect: dependencies.inspectAgentInstallations ?? inspectAgentInstallations
55
+ })
56
+ : null;
46
57
  const result = await handlers[command](input, dependencies.lifecycleDependencies);
58
+ const outputResult = agentSetupCheck
59
+ ? { ...result, agentSetupCheck }
60
+ : result;
47
61
 
48
62
  if (command === 'status' && options.json) {
49
- write(JSON.stringify(result, null, 2));
63
+ write(JSON.stringify(outputResult, null, 2));
50
64
  } else {
51
- write(formatLocalRuntimeResult(command, result));
65
+ write(formatLocalRuntimeResult(command, outputResult));
52
66
  }
53
67
  return {
54
68
  ...result,
69
+ ...(agentSetupCheck ? { agentSetupCheck } : {}),
55
70
  exitCode: command === 'status' && result.status !== 'running'
56
71
  ? 1
57
72
  : result.status === 'partial' ? 1 : 0
@@ -86,6 +101,8 @@ export function formatLocalRuntimeResult(command, result) {
86
101
 
87
102
  function formatStartedRuntime(title, result) {
88
103
  const lines = [title, `Management UI: ${result.url}`];
104
+ const agentSetupNotice = formatAgentSetupNotice(result.agentSetupCheck);
105
+ if (agentSetupNotice) lines.push(agentSetupNotice);
89
106
  if (result.lan === true) {
90
107
  lines.push(
91
108
  'LAN URLs:',
@@ -98,6 +115,56 @@ function formatStartedRuntime(title, result) {
98
115
  return lines.join('\n');
99
116
  }
100
117
 
118
+ function checkAgentSetup({ paths, env, nodePath, discover, inspect }) {
119
+ try {
120
+ const agents = discover({ env }).filter(({ available }) => available);
121
+ if (!agents.length) return { status: 'checked', agents: [] };
122
+ const inspected = inspect(agents, {
123
+ nodePath,
124
+ mcpServerPath: paths.mcpServerPath,
125
+ runtimeConfigPath: paths.graphRuntimeConfigPath,
126
+ sessionSkillPath: paths.sessionSkillPath,
127
+ projectSkillPath: paths.projectSkillPath,
128
+ reviewSkillPath: paths.reviewSkillPath
129
+ });
130
+ return {
131
+ status: 'checked',
132
+ agents: inspected.filter(({ integrationStatus }) => integrationStatus !== 'connected')
133
+ };
134
+ } catch {
135
+ return { status: 'unavailable', agents: [] };
136
+ }
137
+ }
138
+
139
+ function formatAgentSetupNotice(check) {
140
+ if (!check) return null;
141
+ if (check.status === 'unavailable') {
142
+ return [
143
+ 'Could not verify Agent integrations.',
144
+ 'Run `fuli setup` to verify and update them.'
145
+ ].join('\n');
146
+ }
147
+ if (!check.agents.length) return null;
148
+ return [
149
+ 'Agent setup required:',
150
+ ...check.agents.map((agent) => ` ${agent.label}: ${agentNeedsSetup(agent)}`),
151
+ 'Run `fuli setup` to install or update them.'
152
+ ].join('\n');
153
+ }
154
+
155
+ function agentNeedsSetup(agent) {
156
+ const detailLabels = {
157
+ mcp: 'MCP',
158
+ skills: 'Skills',
159
+ bootstrap: 'Bootstrap',
160
+ lifecycleHooks: 'Lifecycle hooks'
161
+ };
162
+ const outdated = Object.entries(agent.integrationDetails ?? {})
163
+ .filter(([, status]) => status !== 'current')
164
+ .map(([key, status]) => `${detailLabels[key] ?? key}: ${status}`);
165
+ return outdated.join(', ') || 'integration update available';
166
+ }
167
+
101
168
  function formatStatus(result) {
102
169
  const labels = {
103
170
  running: 'running',
@@ -1,6 +1,10 @@
1
1
  import { createHash } from 'node:crypto';
2
2
 
3
3
  import { onlineSourceUri } from './source-uri.js';
4
+ import {
5
+ relatedProjectGuidance,
6
+ relatedProjectSuggestions
7
+ } from './related-project-suggestions.js';
4
8
 
5
9
  export async function beginTaskContext(application, {
6
10
  sessionId,
@@ -116,18 +120,21 @@ export async function searchCurrentProjectKnowledge(
116
120
  };
117
121
  }
118
122
 
119
- const results = await Promise.all(queries.map((query) =>
120
- application.searchKnowledge({
121
- personalSpaceId: application.config.personal.spaceId,
122
- personalProjectId: resolution.personalProjectId,
123
- query,
124
- limit: limitPerQuery,
125
- includeHistorical,
126
- includePending,
127
- agentInvocation: true,
128
- agentToolName: 'search_current_project_knowledge'
129
- })
130
- ));
123
+ const [results, relatedProjects] = await Promise.all([
124
+ Promise.all(queries.map((query) =>
125
+ application.searchKnowledge({
126
+ personalSpaceId: application.config.personal.spaceId,
127
+ personalProjectId: resolution.personalProjectId,
128
+ query,
129
+ limit: limitPerQuery,
130
+ includeHistorical,
131
+ includePending,
132
+ agentInvocation: true,
133
+ agentToolName: 'search_current_project_knowledge'
134
+ })
135
+ )),
136
+ loadRelatedProjectSuggestions(application, resolution.personalProjectId)
137
+ ]);
131
138
  return {
132
139
  status: 'searched',
133
140
  personal_space_id: application.config.personal.spaceId,
@@ -138,12 +145,32 @@ export async function searchCurrentProjectKnowledge(
138
145
  inherited_relation_types: ['PART_OF', 'USES_KNOWLEDGE_FROM'],
139
146
  max_inheritance_hops: 2,
140
147
  local_same_key_overrides_parent: true,
141
- unrelated_relations_expand_scope: false
148
+ unrelated_relations_expand_scope: false,
149
+ related_project_expansion_requires_confirmation: true
142
150
  },
143
- results
151
+ results,
152
+ related_project_suggestions: relatedProjects.suggestions,
153
+ related_project_suggestions_status: relatedProjects.status,
154
+ related_project_guidance: relatedProjectGuidance(relatedProjects.suggestions)
144
155
  };
145
156
  }
146
157
 
158
+ async function loadRelatedProjectSuggestions(application, personalProjectId) {
159
+ try {
160
+ const graph = await application.personal.graph(
161
+ application.config.personal.spaceId,
162
+ 2000,
163
+ personalProjectId
164
+ );
165
+ return {
166
+ status: graph?.truncated ? 'partial' : 'available',
167
+ suggestions: relatedProjectSuggestions(graph, personalProjectId)
168
+ };
169
+ } catch {
170
+ return { status: 'unavailable', suggestions: [] };
171
+ }
172
+ }
173
+
147
174
  export async function discoverCommonKnowledgeCandidates(application, {
148
175
  personalSpaceId,
149
176
  parentProjectId,
@@ -175,8 +202,13 @@ export async function discoverCommonKnowledgeCandidates(application, {
175
202
  }
176
203
  const childProjectIds = [...new Set(
177
204
  (graph.edges ?? [])
178
- .filter(({ type, source, target }) =>
205
+ .filter(({ type, source, target, attributes = {} }) =>
179
206
  type === 'PART_OF'
207
+ && attributes.status === 'active'
208
+ && (
209
+ attributes.confirmationAuthority
210
+ ?? attributes.confirmation_authority
211
+ ) === 'human_review'
180
212
  && projectIdByNodeId.get(target) === parentProjectId
181
213
  && projectIdByNodeId.has(source)
182
214
  )
@@ -216,7 +248,7 @@ export async function discoverCommonKnowledgeCandidates(application, {
216
248
  ...(result.facts ?? []).map((item) =>
217
249
  commonKnowledgeItem(item, 'relationship', projectId)
218
250
  )
219
- ]);
251
+ ]).filter((item) => !item.profile_aspect);
220
252
  const candidates = clusterCommonKnowledgeItems(
221
253
  items,
222
254
  minChildProjects,
@@ -235,6 +267,149 @@ export async function discoverCommonKnowledgeCandidates(application, {
235
267
  });
236
268
  }
237
269
 
270
+ export async function discoverPersonalGlobalPreferenceCandidates(application, {
271
+ personalSpaceId,
272
+ personalProjectIds,
273
+ query,
274
+ minProjects = 2,
275
+ similarityThreshold = 0.72,
276
+ limitPerProject = 12
277
+ }) {
278
+ if (!Array.isArray(personalProjectIds) || personalProjectIds.length < 2) {
279
+ throw new TypeError('Personal-global candidates need at least two explicit projects');
280
+ }
281
+ if (!Number.isInteger(minProjects) || minProjects < 2) {
282
+ throw new TypeError('Personal-global candidates need evidence from at least two projects');
283
+ }
284
+ if (
285
+ typeof similarityThreshold !== 'number'
286
+ || similarityThreshold < 0
287
+ || similarityThreshold > 1
288
+ ) {
289
+ throw new TypeError('Similarity threshold must be between 0 and 1');
290
+ }
291
+ const projectIds = [...new Set(personalProjectIds)].sort();
292
+ if (projectIds.length < 2 || minProjects > projectIds.length) {
293
+ throw new TypeError('The selected project set cannot satisfy minProjects');
294
+ }
295
+ const knownProjectIds = new Set(
296
+ (await application.personal.listPersonalProjects(personalSpaceId))
297
+ .map(({ project_id: projectId }) => projectId)
298
+ );
299
+ if (projectIds.some((projectId) => !knownProjectIds.has(projectId))) {
300
+ throw new TypeError('Every selected project must exist in the active personal space');
301
+ }
302
+
303
+ const projectResults = await Promise.all(projectIds.map(async (projectId) => ({
304
+ projectId,
305
+ result: await application.personal.search({
306
+ space_ids: [personalSpaceId],
307
+ query,
308
+ limit: limitPerProject,
309
+ include_historical: false,
310
+ include_exploratory: false,
311
+ personal_project_ids: [projectId],
312
+ active_personal_project_id: projectId,
313
+ inherit_project_knowledge: false,
314
+ include_personal_global: false
315
+ })
316
+ })));
317
+ const items = projectResults.flatMap(({ projectId, result }) => [
318
+ ...(result.entities ?? []).map((item) =>
319
+ commonKnowledgeItem(item, 'entity', projectId)
320
+ ),
321
+ ...(result.facts ?? []).map((item) =>
322
+ commonKnowledgeItem(item, 'relationship', projectId)
323
+ )
324
+ ]).filter((item) =>
325
+ item.profile_aspect
326
+ && item.preference_scope === 'project'
327
+ && item.preference_project_id === item.defined_project_id
328
+ && ['confirmed', 'agent_confirmed'].includes(item.confirmation_status)
329
+ && item.requires_attention !== true
330
+ );
331
+ const clusteredCandidates = clusterPersonalGlobalPreferenceItems(
332
+ items,
333
+ minProjects,
334
+ similarityThreshold,
335
+ query,
336
+ projectIds.length
337
+ );
338
+ const candidates = await Promise.all(clusteredCandidates.map(async (candidate) => {
339
+ const options = await application.personal.personalGlobalPreferenceScopeOptions(
340
+ candidate.candidate_id,
341
+ {
342
+ personal_space_id: personalSpaceId,
343
+ source_items: candidate.source_items.map((item) => ({
344
+ item_id: item.id,
345
+ item_kind: item.item_kind,
346
+ project_id: item.defined_project_id
347
+ })),
348
+ preference_key: candidate.preference_key
349
+ }
350
+ );
351
+ return {
352
+ ...candidate,
353
+ candidate_version: options.candidate_version,
354
+ target_scope: 'human_selected',
355
+ eligible_target_scopes: options.eligible_target_scopes,
356
+ source_snapshots: options.source_snapshots
357
+ };
358
+ }));
359
+ const decisionState = await personalGlobalCandidateDecisionState(
360
+ application,
361
+ personalSpaceId,
362
+ candidates
363
+ );
364
+ const visibleCandidates = candidates
365
+ .filter(({ candidate_id: id, candidate_version: version }) =>
366
+ !decisionState.decisions.has(`${id}:${version}`)
367
+ )
368
+ .map((candidate) => personalGlobalCandidateWithDecisionState(
369
+ candidate,
370
+ decisionState.revisions.get(candidate.candidate_id)
371
+ ));
372
+ const suppressedCandidates = candidates
373
+ .filter(({ candidate_id: id, candidate_version: version }) =>
374
+ decisionState.decisions.has(`${id}:${version}`)
375
+ )
376
+ .map(({ candidate_id: candidateId, candidate_version: version }) => ({
377
+ candidate_id: candidateId,
378
+ candidate_version: version,
379
+ ...decisionState.decisions.get(`${candidateId}:${version}`)
380
+ }));
381
+ return {
382
+ status: visibleCandidates.length > 0
383
+ ? 'candidates_found'
384
+ : suppressedCandidates.length > 0
385
+ ? 'candidates_suppressed'
386
+ : 'no_candidates',
387
+ personal_space_id: personalSpaceId,
388
+ target_scope: 'human_selected',
389
+ selected_project_ids: projectIds,
390
+ query,
391
+ candidates: visibleCandidates,
392
+ suppressed_candidates: suppressedCandidates,
393
+ policy: personalGlobalCandidatePolicy()
394
+ };
395
+ }
396
+
397
+ export async function previewPersonalGlobalPreferenceDecision(application, input) {
398
+ return application.personal.inspectPersonalGlobalPreferenceDecision(
399
+ input.candidateId,
400
+ providerPersonalGlobalPreferenceDecision(input)
401
+ );
402
+ }
403
+
404
+ export async function applyPersonalGlobalPreferenceDecision(application, input) {
405
+ return application.personal.applyPersonalGlobalPreferenceDecision(
406
+ input.candidateId,
407
+ providerPersonalGlobalPreferenceDecision(input, {
408
+ approvalToken: input.previewToken
409
+ })
410
+ );
411
+ }
412
+
238
413
  export async function recordKnowledgeFeedback(application, {
239
414
  personalSpaceId,
240
415
  taskId,
@@ -388,6 +563,188 @@ function clusterCommonKnowledgeItems(
388
563
  );
389
564
  }
390
565
 
566
+ function clusterPersonalGlobalPreferenceItems(
567
+ items,
568
+ minProjects,
569
+ similarityThreshold,
570
+ query,
571
+ selectedProjectCount
572
+ ) {
573
+ const groups = [];
574
+ for (const item of items) {
575
+ const preferenceKey = personalGlobalItemPreferenceKey(item);
576
+ const matching = groups
577
+ .map((group) => ({
578
+ group,
579
+ similarity: Math.min(
580
+ ...group.items.map((member) => commonKnowledgeSimilarity(item, member))
581
+ )
582
+ }))
583
+ .filter(({ group, similarity }) =>
584
+ group.itemKind === item.item_kind
585
+ && group.preferenceKey === preferenceKey
586
+ && similarity >= similarityThreshold
587
+ && !group.projectIds.has(item.defined_project_id)
588
+ )
589
+ .sort((left, right) => right.similarity - left.similarity)[0];
590
+ if (matching) {
591
+ matching.group.items.push(item);
592
+ matching.group.projectIds.add(item.defined_project_id);
593
+ continue;
594
+ }
595
+ groups.push({
596
+ itemKind: item.item_kind,
597
+ preferenceKey,
598
+ projectIds: new Set([item.defined_project_id]),
599
+ items: [item]
600
+ });
601
+ }
602
+
603
+ return groups
604
+ .filter(({ projectIds }) => projectIds.size >= minProjects)
605
+ .map((group) => {
606
+ const sourceItems = [...group.items].sort((left, right) =>
607
+ left.defined_project_id.localeCompare(right.defined_project_id)
608
+ );
609
+ const itemIds = sourceItems.map(({ id }) => id).sort();
610
+ const commonTerms = sharedPreferenceTerms(sourceItems);
611
+ const pairScores = [];
612
+ for (let left = 0; left < sourceItems.length; left += 1) {
613
+ for (let right = left + 1; right < sourceItems.length; right += 1) {
614
+ pairScores.push(commonKnowledgeSimilarity(
615
+ sourceItems[left],
616
+ sourceItems[right]
617
+ ));
618
+ }
619
+ }
620
+ const averageSimilarity = pairScores.length === 0
621
+ ? 1
622
+ : pairScores.reduce((sum, score) => sum + score, 0) / pairScores.length;
623
+ const ranking = personalGlobalCandidateRanking({
624
+ sourceItems,
625
+ projectCount: group.projectIds.size,
626
+ selectedProjectCount,
627
+ lexicalSimilarity: averageSimilarity
628
+ });
629
+ return {
630
+ candidate_id: personalGlobalPreferenceCandidateId(itemIds),
631
+ candidate_version: null,
632
+ target_scope: 'human_selected',
633
+ source_project_ids: [...group.projectIds].sort(),
634
+ item_kind: group.itemKind,
635
+ preference_key: group.preferenceKey,
636
+ query,
637
+ similarity_score: Number(averageSimilarity.toFixed(4)),
638
+ similarity_basis: 'lexical_overlap_across_explicit_personal_projects',
639
+ candidate_score: ranking.score,
640
+ ranking,
641
+ source_items: sourceItems,
642
+ derived_common_core: {
643
+ terms: commonTerms,
644
+ basis: 'lexical_intersection_of_preserved_source_text',
645
+ authoritative: false,
646
+ source_item_ids: itemIds
647
+ },
648
+ source_specific_terms: sourceItems.map((item) => ({
649
+ item_id: item.id,
650
+ project_id: item.defined_project_id,
651
+ terms: [...preferenceContentTokens(item)]
652
+ .filter((term) => !commonTerms.includes(term))
653
+ .sort()
654
+ })),
655
+ requires_human_scope_judgment: true,
656
+ scope_apply_performed: false
657
+ };
658
+ })
659
+ .sort((left, right) =>
660
+ right.candidate_score - left.candidate_score
661
+ || right.source_project_ids.length - left.source_project_ids.length
662
+ || right.similarity_score - left.similarity_score
663
+ || left.candidate_id.localeCompare(right.candidate_id)
664
+ );
665
+ }
666
+
667
+ function personalGlobalItemPreferenceKey(item) {
668
+ return item.preference_key
669
+ ?? item.attributes?.preferenceKey
670
+ ?? item.attributes?.preference_key
671
+ ?? item.key
672
+ ?? item.id;
673
+ }
674
+
675
+ const PERSONAL_GLOBAL_RANKING_WEIGHTS = Object.freeze({
676
+ distinct_projects: 0.25,
677
+ lexical_similarity: 0.35,
678
+ confirmation_authority: 0.2,
679
+ recency: 0.1,
680
+ negative_evidence: 0.1
681
+ });
682
+
683
+ function personalGlobalCandidateRanking({
684
+ sourceItems,
685
+ projectCount,
686
+ selectedProjectCount,
687
+ lexicalSimilarity
688
+ }) {
689
+ const signals = {
690
+ distinct_projects: boundedScore(
691
+ projectCount / Math.max(selectedProjectCount, 1)
692
+ ),
693
+ lexical_similarity: boundedScore(lexicalSimilarity),
694
+ confirmation_authority: averageScore(sourceItems.map((item) =>
695
+ item.confirmation_status === 'confirmed' ? 1 : 0.65
696
+ )),
697
+ recency: averageScore(sourceItems.map(personalGlobalItemRecency)),
698
+ negative_evidence: averageScore(sourceItems.map((item) =>
699
+ item.requires_attention === true
700
+ ? 0
701
+ : 1 / (1 + Math.max(0, Number(item.negative_evidence_count ?? 0)))
702
+ ))
703
+ };
704
+ const details = Object.fromEntries(
705
+ Object.entries(signals).map(([key, value]) => {
706
+ const weight = PERSONAL_GLOBAL_RANKING_WEIGHTS[key];
707
+ return [key, {
708
+ value: Number(value.toFixed(4)),
709
+ weight,
710
+ contribution: Number((value * weight).toFixed(4))
711
+ }];
712
+ })
713
+ );
714
+ const score = Object.values(details)
715
+ .reduce((sum, signal) => sum + signal.contribution, 0);
716
+ return {
717
+ score: Number(score.toFixed(4)),
718
+ signals: details,
719
+ source_project_count: projectCount,
720
+ selected_project_count: selectedProjectCount,
721
+ policy: 'ranking_only_never_changes_scope_or_confirmation_authority'
722
+ };
723
+ }
724
+
725
+ function personalGlobalItemRecency(item) {
726
+ const timestamp = [
727
+ item.last_used_at,
728
+ item.last_human_changed_at,
729
+ item.created_at
730
+ ].map((value) => Date.parse(value ?? ''))
731
+ .find(Number.isFinite);
732
+ if (!Number.isFinite(timestamp)) return 0.5;
733
+ const ageDays = Math.max(0, (Date.now() - timestamp) / 86_400_000);
734
+ return Math.exp(-ageDays / 180);
735
+ }
736
+
737
+ function averageScore(values) {
738
+ if (values.length === 0) return 0;
739
+ return boundedScore(
740
+ values.reduce((sum, value) => sum + value, 0) / values.length
741
+ );
742
+ }
743
+
744
+ function boundedScore(value) {
745
+ return Math.max(0, Math.min(1, Number(value) || 0));
746
+ }
747
+
391
748
  function commonKnowledgeSimilarity(left, right) {
392
749
  if (left.key && right.key && left.key === right.key) return 1;
393
750
  const leftTokens = commonKnowledgeTokens(left);
@@ -407,6 +764,27 @@ function commonKnowledgeTokens(item) {
407
764
  item.fact,
408
765
  item.type
409
766
  ].filter(Boolean).join(' ').toLowerCase();
767
+ return lexicalTokens(text);
768
+ }
769
+
770
+ function preferenceContentTokens(item) {
771
+ const text = [
772
+ item.instruction,
773
+ item.summary,
774
+ item.fact
775
+ ].filter(Boolean).join(' ').toLowerCase();
776
+ return lexicalTokens(text || String(item.name ?? '').toLowerCase());
777
+ }
778
+
779
+ function sharedPreferenceTerms(items) {
780
+ const tokenSets = items.map(preferenceContentTokens);
781
+ if (tokenSets.length === 0) return [];
782
+ return [...tokenSets[0]]
783
+ .filter((term) => tokenSets.slice(1).every((tokens) => tokens.has(term)))
784
+ .sort();
785
+ }
786
+
787
+ function lexicalTokens(text) {
410
788
  const tokens = new Set(
411
789
  text.match(/[a-z0-9]+(?:[._-][a-z0-9]+)*/g) ?? []
412
790
  );
@@ -423,6 +801,8 @@ function commonKnowledgeCandidatePolicy() {
423
801
  return {
424
802
  read_only: true,
425
803
  direct_part_of_children_only: true,
804
+ active_human_authorized_relations_only: true,
805
+ personal_preferences_excluded: true,
426
806
  inherited_knowledge_excluded: true,
427
807
  personal_global_excluded: true,
428
808
  automatic_promotion: false,
@@ -431,6 +811,107 @@ function commonKnowledgeCandidatePolicy() {
431
811
  };
432
812
  }
433
813
 
814
+ function personalGlobalCandidatePolicy() {
815
+ return {
816
+ read_only: true,
817
+ exact_projects_only: true,
818
+ inherited_knowledge_excluded: true,
819
+ personal_global_excluded: true,
820
+ original_text_and_sources_preserved: true,
821
+ derived_core_is_non_authoritative: true,
822
+ human_scope_judgment_required: true,
823
+ automatic_scope_apply: false,
824
+ ranking_scores_are_non_authoritative: true,
825
+ stale_decisions_do_not_suppress_changed_candidates: true
826
+ };
827
+ }
828
+
829
+ async function personalGlobalCandidateDecisionState(
830
+ application,
831
+ personalSpaceId,
832
+ candidates
833
+ ) {
834
+ if (candidates.length === 0) {
835
+ return { decisions: new Map(), revisions: new Map() };
836
+ }
837
+ const result = await application.personal
838
+ .personalGlobalPreferenceDecisionStatus({
839
+ personal_space_id: personalSpaceId,
840
+ candidates: candidates.map((candidate) => ({
841
+ candidate_id: candidate.candidate_id,
842
+ candidate_version: candidate.candidate_version
843
+ }))
844
+ });
845
+ return {
846
+ decisions: new Map((result.decisions ?? []).map((decision) => [
847
+ `${decision.candidate_id}:${decision.candidate_version}`,
848
+ {
849
+ decision: decision.decision,
850
+ decision_event_id: decision.decision_event_id,
851
+ decision_revision: decision.decision_revision,
852
+ target_scope: decision.target_scope,
853
+ target_project_id: decision.target_project_id ?? null,
854
+ global_assertion_id: decision.global_assertion_id,
855
+ global_assertion_active: decision.global_assertion_active
856
+ }
857
+ ])),
858
+ revisions: new Map((result.revisions ?? []).map((revision) => [
859
+ revision.candidate_id,
860
+ revision
861
+ ]))
862
+ };
863
+ }
864
+
865
+ function personalGlobalCandidateWithDecisionState(candidate, revision) {
866
+ const decisionRevision = revision?.decision_revision ?? 0;
867
+ const previousVersion = revision?.current_candidate_version ?? null;
868
+ const sourceDrift = Boolean(
869
+ previousVersion && previousVersion !== candidate.candidate_version
870
+ );
871
+ return {
872
+ ...candidate,
873
+ decision_revision: decisionRevision,
874
+ prior_decision_source_drift: sourceDrift,
875
+ prior_decision_candidate_version: sourceDrift ? previousVersion : null,
876
+ fresh_human_review_required: true
877
+ };
878
+ }
879
+
880
+ function providerPersonalGlobalPreferenceDecision(input, {
881
+ approvalToken = null
882
+ } = {}) {
883
+ const result = {
884
+ personal_space_id: input.personalSpaceId,
885
+ candidate_version: input.candidateVersion,
886
+ decision_revision: input.decisionRevision,
887
+ source_items: input.sourceItems.map((item) => ({
888
+ item_id: item.itemId,
889
+ item_kind: item.itemKind,
890
+ project_id: item.projectId
891
+ })),
892
+ preference_key: input.preferenceKey,
893
+ target_scope: input.targetScope,
894
+ target_project_id: input.targetProjectId ?? null,
895
+ decision: input.decision,
896
+ global_title: input.globalTitle ?? null,
897
+ global_instruction: input.globalInstruction ?? null,
898
+ profile_aspect: input.profileAspect ?? null,
899
+ human_confirmation_reason: input.humanConfirmationReason,
900
+ confirmed_at: input.confirmedAt,
901
+ session_id: input.sessionId,
902
+ idempotency_key: input.idempotencyKey
903
+ };
904
+ if (approvalToken) result.approval_token = approvalToken;
905
+ return result;
906
+ }
907
+
908
+ function personalGlobalPreferenceCandidateId(itemIds) {
909
+ return `personal-global-${createHash('sha256')
910
+ .update([...itemIds].sort().join('\n'))
911
+ .digest('hex')
912
+ .slice(0, 20)}`;
913
+ }
914
+
434
915
  function decisionTraceKnowledge(input) {
435
916
  const decisionKey = `decision:${input.decisionKey}`;
436
917
  const selectedKey = `${decisionKey}:option:${input.selectedOption.key}`;