fuli-context 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +94 -25
  2. package/dist/web/assets/ConnectionsPage-CCKWm-6p.js +1 -0
  3. package/dist/web/assets/KnowledgeInspector-BZkB2wJn.js +1 -0
  4. package/dist/web/assets/KnowledgeInspector-CrGYxEob.css +1 -0
  5. package/dist/web/assets/KnowledgeOrganizerPage-CJCNI2Wd.css +1 -0
  6. package/dist/web/assets/KnowledgeOrganizerPage-PXH-TlGt.js +1 -0
  7. package/dist/web/assets/KnowledgePage-CNQXd15y.js +1 -0
  8. package/dist/web/assets/KnowledgeWorkspace-CXPE4GCD.js +4 -0
  9. package/dist/web/assets/OverviewPage-ul7g7HXh.js +1 -0
  10. package/dist/web/assets/PersonalProfilePage-CC40Pw2L.js +1 -0
  11. package/dist/web/assets/PersonalProfilePage-S8ZcG2LU.css +1 -0
  12. package/dist/web/assets/PersonalProjectsPage-CPIEcShD.js +1 -0
  13. package/dist/web/assets/PublicProjectsPage-C700sR7z.js +1 -0
  14. package/dist/web/assets/ReviewPage-7MDDUDgA.js +1 -0
  15. package/dist/web/assets/console-B_Ena2DS.js +5 -0
  16. package/dist/web/assets/identity-CjBwYSMX.js +1 -0
  17. package/dist/web/assets/index-BQRkOwPx.js +2 -0
  18. package/dist/web/assets/{index-Devf7glZ.css → index-CpLDx3of.css} +1 -1
  19. package/dist/web/assets/model-C-5RVIJ2.js +1 -0
  20. package/dist/web/assets/{paths-B790iniO.js → paths-BkULqmGI.js} +1 -1
  21. package/dist/web/assets/{vue-router-BYTAoWT3.js → vue-router-DnD3hUm7.js} +1 -1
  22. package/dist/web/index.html +5 -6
  23. package/graph-provider/fuli_graph/app.py +48 -0
  24. package/graph-provider/fuli_graph/common_knowledge.py +443 -0
  25. package/graph-provider/fuli_graph/common_knowledge_models.py +54 -0
  26. package/graph-provider/fuli_graph/graph_models.py +9 -0
  27. package/graph-provider/fuli_graph/graph_query.py +25 -0
  28. package/graph-provider/fuli_graph/knowledge_audit.py +211 -0
  29. package/graph-provider/fuli_graph/knowledge_feedback_models.py +69 -0
  30. package/graph-provider/fuli_graph/knowledge_management.py +37 -0
  31. package/graph-provider/fuli_graph/knowledge_search.py +24 -2
  32. package/graph-provider/fuli_graph/models.py +69 -0
  33. package/graph-provider/fuli_graph/search_projection.py +59 -0
  34. package/graph-provider/fuli_graph/store_knowledge.py +2 -0
  35. package/graph-provider/pyproject.toml +1 -1
  36. package/npm-shrinkwrap.json +97 -2
  37. package/package.json +2 -1
  38. package/skills/capturing-session-knowledge/SKILL.md +86 -15
  39. package/src/agent-tools/graph-definitions.js +181 -13
  40. package/src/agent-tools/graph-handlers.js +19 -0
  41. package/src/cli/command-registry.js +6 -59
  42. package/src/cli/update-command.js +293 -0
  43. package/src/cli.js +7 -34
  44. package/src/graphiti/agent-knowledge-workflows.js +543 -0
  45. package/src/graphiti/federated-application.js +85 -104
  46. package/src/graphiti/knowledge-provider-mapping.js +91 -0
  47. package/src/graphiti/provider-client.js +15 -0
  48. package/src/graphiti/source-marker.js +1 -7
  49. package/src/graphiti/source-uri.js +29 -0
  50. package/src/http/static-handler.js +29 -3
  51. package/src/mcp/common-knowledge-preview-tokens.js +67 -0
  52. package/src/mcp/create-mcp-server.js +43 -7
  53. package/src/mcp/instructions.js +7 -8
  54. package/src/mcp/lifecycle-audit.js +27 -0
  55. package/src/mcp/task-context-registry.js +93 -0
  56. package/src/mcp/tool-annotations.js +16 -1
  57. package/src/mcp/tool-result.js +24 -0
  58. package/src/runtime-options.js +7 -8
  59. package/src/setup/agent-installation-status.js +14 -2
  60. package/src/setup/agents.js +12 -1
  61. package/src/setup/claude-code-config.js +194 -0
  62. package/src/setup/config-backup.js +20 -5
  63. package/src/setup/options.js +9 -1
  64. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +0 -1
  65. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +0 -1
  66. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +0 -1
  67. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +0 -1
  68. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +0 -1
  69. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +0 -1
  70. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +0 -4
  71. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +0 -1
  72. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +0 -1
  73. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +0 -1
  74. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +0 -1
  75. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +0 -1
  76. package/dist/web/assets/ReviewPage-Dztk2jDG.js +0 -1
  77. package/dist/web/assets/console-tbvTUi6p.js +0 -1
  78. package/dist/web/assets/identity-B3s5CXhv.js +0 -1
  79. package/dist/web/assets/index-DwUDhQKc.js +0 -6
  80. package/dist/web/assets/model-DVT8sjy9.js +0 -1
  81. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +0 -1
  82. package/src/cli/commands/candidates.js +0 -17
  83. package/src/cli/commands/ingestion.js +0 -32
  84. package/src/cli/commands/reads.js +0 -50
  85. package/src/cli/commands/spaces.js +0 -20
  86. package/src/cli/invocation.js +0 -47
  87. package/src/cli/migrate-command.js +0 -71
  88. package/src/cli/migration-destination.js +0 -117
  89. package/src/storage/import-json.js +0 -70
  90. package/src/storage/legacy-record-validation.js +0 -152
  91. package/src/storage/legacy-snapshot-validation.js +0 -32
package/src/cli.js CHANGED
@@ -3,70 +3,43 @@ import { realpathSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
 
5
5
  import {
6
- dispatchCommand,
7
6
  isLocalRuntimeCommand,
8
- isRegisteredCommand,
9
7
  printHelp
10
8
  } from './cli/command-registry.js';
11
- import { parseCliInvocation } from './cli/invocation.js';
12
9
  import { runLocalRuntimeCommand } from './cli/local-runtime-command.js';
13
- import { migrateLegacyJson } from './cli/migrate-command.js';
14
10
  import { runSetupCommand } from './cli/setup-command.js';
15
11
  import { runUninstallCommand } from './cli/uninstall-command.js';
12
+ import { runUpdateCommand } from './cli/update-command.js';
16
13
  import { FULI_VERSION } from './package-metadata.js';
17
14
  import { assertSupportedNodeVersion } from './setup/node-runtime.js';
18
- import {
19
- openLocalApplication,
20
- resolveRuntimeOptions
21
- } from './runtime-options.js';
22
15
 
23
16
  export async function main(argv = process.argv.slice(2), env = process.env) {
24
- const { runtimeArgs, command, args: commandArgs } = parseCliInvocation(argv);
17
+ const [command, ...commandArgs] = argv;
25
18
  if (!command || command === '--help' || command === '-h') {
26
- if (runtimeArgs.length) resolveRuntimeOptions(runtimeArgs, {});
27
19
  printHelp();
28
20
  return;
29
21
  }
30
22
  if (command === '--version' || command === '-v') {
31
- if (runtimeArgs.length) resolveRuntimeOptions(runtimeArgs, {});
32
23
  console.log(FULI_VERSION);
33
24
  return;
34
25
  }
35
- if (command === 'migrate') {
36
- if (runtimeArgs.length) resolveRuntimeOptions(runtimeArgs, {});
37
- console.log(JSON.stringify(migrateLegacyJson(commandArgs)));
38
- return;
39
- }
40
26
  if (command === 'setup') {
41
- if (runtimeArgs.length) {
42
- throw new TypeError('Setup options must appear after the setup command');
43
- }
44
27
  assertSupportedNodeVersion();
45
28
  await runSetupCommand(commandArgs, { env });
46
29
  return;
47
30
  }
48
31
  if (command === 'uninstall') {
49
- if (runtimeArgs.length) {
50
- throw new TypeError('Uninstall options must appear after the uninstall command');
51
- }
52
32
  await runUninstallCommand(commandArgs, { env });
53
33
  return;
54
34
  }
35
+ if (command === 'update') {
36
+ assertSupportedNodeVersion();
37
+ return runUpdateCommand(commandArgs, { env });
38
+ }
55
39
  if (isLocalRuntimeCommand(command)) {
56
- if (runtimeArgs.length) {
57
- throw new TypeError(`${command} options must appear after the command`);
58
- }
59
40
  return runLocalRuntimeCommand(command, commandArgs, { env });
60
41
  }
61
- if (!isRegisteredCommand(command)) throw new Error(`Unknown command: ${command}`);
62
-
63
- const options = resolveRuntimeOptions(runtimeArgs, env);
64
- const app = openLocalApplication(options);
65
- try {
66
- dispatchCommand(app, command, commandArgs);
67
- } finally {
68
- app.close();
69
- }
42
+ throw new Error(`Unknown command: ${command}`);
70
43
  }
71
44
 
72
45
  if (isMainModule(import.meta.url, process.argv[1])) {
@@ -0,0 +1,543 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ import { onlineSourceUri } from './source-uri.js';
4
+
5
+ export async function beginTaskContext(application, { sessionId, projectPath }) {
6
+ const preferences = await application.getCollaborationPreferences({
7
+ projectPath,
8
+ agentInvocation: true,
9
+ agentToolName: 'begin_task_context'
10
+ });
11
+ const task = application.taskContextRegistry.begin({
12
+ sessionId,
13
+ personalProjectId: preferences.context.personal_project_id
14
+ });
15
+ return {
16
+ taskContextToken: task.token,
17
+ task_context_token: task.token,
18
+ checkpoint_required: true,
19
+ previous_checkpoint_missing: task.previousCheckpointMissing,
20
+ ...preferences,
21
+ task_guidance: {
22
+ retrieval: 'Use search_current_project_knowledge for project work; it resolves the current project and inherited parent knowledge without copying IDs.',
23
+ checkpoint: 'Before finishing, call checkpoint_task_knowledge with capture_candidates or retain_nothing.'
24
+ }
25
+ };
26
+ }
27
+
28
+ export async function checkpointTaskKnowledge(application, {
29
+ taskContextToken,
30
+ disposition,
31
+ reason,
32
+ capture = null
33
+ }) {
34
+ if (!['capture_candidates', 'retain_nothing'].includes(disposition)) {
35
+ throw new TypeError('Unknown task knowledge checkpoint disposition');
36
+ }
37
+ const task = application.taskContextRegistry.context(taskContextToken);
38
+
39
+ let captureResult = null;
40
+ if (disposition === 'capture_candidates') {
41
+ if (!capture) {
42
+ throw new TypeError('capture_candidates requires a bounded capture payload');
43
+ }
44
+ captureResult = await application.captureSessionKnowledge({
45
+ targetKind: 'personal',
46
+ spaceId: application.config.personal.spaceId,
47
+ personalProjectId: task.personalProjectId,
48
+ sessionId: task.sessionId,
49
+ ...capture
50
+ });
51
+ } else if (capture) {
52
+ throw new TypeError('retain_nothing cannot include a capture payload');
53
+ }
54
+
55
+ application.taskContextRegistry.checkpoint(taskContextToken, {
56
+ disposition,
57
+ reason,
58
+ captureStatus: captureResult?.status ?? null
59
+ });
60
+ return {
61
+ status: 'checkpointed',
62
+ disposition,
63
+ reason,
64
+ personal_project_id: task.personalProjectId,
65
+ capture: captureResult
66
+ };
67
+ }
68
+
69
+ export async function recordDecisionTrace(application, input) {
70
+ const trace = decisionTraceKnowledge(input);
71
+ return application.captureSessionKnowledge({
72
+ targetKind: 'personal',
73
+ spaceId: input.personalSpaceId,
74
+ personalProjectId: input.personalProjectId ?? null,
75
+ sessionId: input.sessionId,
76
+ idempotencyKey: input.idempotencyKey,
77
+ name: `Decision · ${input.title}`,
78
+ sourceKind: input.sourceKind,
79
+ sourceDescription: input.sourceDescription,
80
+ sourceUri: input.sourceUri ?? null,
81
+ sourceApplication: input.sourceApplication ?? null,
82
+ sourceTurnId: input.sourceTurnId ?? null,
83
+ referenceTime: input.referenceTime,
84
+ summary: `${input.question}\nSelected: ${input.selectedOption.label}`,
85
+ sensitivity: input.sensitivity ?? 'normal',
86
+ ...trace
87
+ });
88
+ }
89
+
90
+ export async function searchCurrentProjectKnowledge(
91
+ application,
92
+ resolution,
93
+ {
94
+ queries,
95
+ limitPerQuery = 12,
96
+ includeHistorical = false,
97
+ includePending = false
98
+ }
99
+ ) {
100
+ const projectResolution = agentProjectResolution(resolution);
101
+ if (!resolution.personalProjectId) {
102
+ return {
103
+ status: 'project_unresolved',
104
+ personal_space_id: application.config.personal.spaceId,
105
+ personal_project_id: null,
106
+ project_resolution: projectResolution,
107
+ results: [],
108
+ required_action: resolution.status === 'ambiguous'
109
+ ? 'Run from the exact project directory or explicitly choose one project; Fuli will not guess among workspace children.'
110
+ : 'Register this repository or directory as one local personal project before project-scoped search.'
111
+ };
112
+ }
113
+
114
+ const results = await Promise.all(queries.map((query) =>
115
+ application.searchKnowledge({
116
+ personalSpaceId: application.config.personal.spaceId,
117
+ personalProjectId: resolution.personalProjectId,
118
+ query,
119
+ limit: limitPerQuery,
120
+ includeHistorical,
121
+ includePending,
122
+ agentInvocation: true,
123
+ agentToolName: 'search_current_project_knowledge'
124
+ })
125
+ ));
126
+ return {
127
+ status: 'searched',
128
+ personal_space_id: application.config.personal.spaceId,
129
+ personal_project_id: resolution.personalProjectId,
130
+ project_resolution: projectResolution,
131
+ scope_policy: {
132
+ local_project_first: true,
133
+ inherited_relation_types: ['PART_OF', 'USES_KNOWLEDGE_FROM'],
134
+ max_inheritance_hops: 2,
135
+ local_same_key_overrides_parent: true,
136
+ unrelated_relations_expand_scope: false
137
+ },
138
+ results
139
+ };
140
+ }
141
+
142
+ export async function discoverCommonKnowledgeCandidates(application, {
143
+ personalSpaceId,
144
+ parentProjectId,
145
+ query,
146
+ minChildProjects = 2,
147
+ similarityThreshold = 0.72,
148
+ limitPerProject = 12
149
+ }) {
150
+ if (!Number.isInteger(minChildProjects) || minChildProjects < 2) {
151
+ throw new TypeError('Common knowledge needs evidence from at least two child projects');
152
+ }
153
+ if (
154
+ typeof similarityThreshold !== 'number'
155
+ || similarityThreshold < 0
156
+ || similarityThreshold > 1
157
+ ) {
158
+ throw new TypeError('Similarity threshold must be between 0 and 1');
159
+ }
160
+
161
+ const graph = await application.personal.graph(personalSpaceId, 2000);
162
+ const projectIdByNodeId = new Map(
163
+ (graph.nodes ?? [])
164
+ .filter(({ type }) => type === 'PersonalProject')
165
+ .map((node) => [node.id, node.attributes?.projectId])
166
+ .filter(([, projectId]) => typeof projectId === 'string' && projectId)
167
+ );
168
+ if (![...projectIdByNodeId.values()].includes(parentProjectId)) {
169
+ throw new TypeError('Parent project is not present in the active personal graph');
170
+ }
171
+ const childProjectIds = [...new Set(
172
+ (graph.edges ?? [])
173
+ .filter(({ type, source, target }) =>
174
+ type === 'PART_OF'
175
+ && projectIdByNodeId.get(target) === parentProjectId
176
+ && projectIdByNodeId.has(source)
177
+ )
178
+ .map(({ source }) => projectIdByNodeId.get(source))
179
+ )].sort();
180
+
181
+ if (childProjectIds.length < minChildProjects) {
182
+ return commonCandidateResult({
183
+ status: childProjectIds.length === 0 ? 'no_children' : 'insufficient_children',
184
+ personalSpaceId,
185
+ parentProjectId,
186
+ childProjectIds,
187
+ query,
188
+ candidates: [],
189
+ graph
190
+ });
191
+ }
192
+
193
+ const childResults = await Promise.all(childProjectIds.map(async (projectId) => ({
194
+ projectId,
195
+ result: await application.personal.search({
196
+ space_ids: [personalSpaceId],
197
+ query,
198
+ limit: limitPerProject,
199
+ include_historical: false,
200
+ include_exploratory: true,
201
+ personal_project_ids: [projectId],
202
+ active_personal_project_id: projectId,
203
+ inherit_project_knowledge: false,
204
+ include_personal_global: false
205
+ })
206
+ })));
207
+ const items = childResults.flatMap(({ projectId, result }) => [
208
+ ...(result.entities ?? []).map((item) =>
209
+ commonKnowledgeItem(item, 'entity', projectId)
210
+ ),
211
+ ...(result.facts ?? []).map((item) =>
212
+ commonKnowledgeItem(item, 'relationship', projectId)
213
+ )
214
+ ]);
215
+ const candidates = clusterCommonKnowledgeItems(
216
+ items,
217
+ minChildProjects,
218
+ similarityThreshold,
219
+ parentProjectId,
220
+ query
221
+ );
222
+ return commonCandidateResult({
223
+ status: candidates.length > 0 ? 'candidates_found' : 'no_candidates',
224
+ personalSpaceId,
225
+ parentProjectId,
226
+ childProjectIds,
227
+ query,
228
+ candidates,
229
+ graph
230
+ });
231
+ }
232
+
233
+ export async function recordKnowledgeFeedback(application, {
234
+ personalSpaceId,
235
+ taskId,
236
+ sessionId = null,
237
+ toolName = null,
238
+ items
239
+ }) {
240
+ return application.personal.recordKnowledgeFeedback({
241
+ personal_space_id: personalSpaceId,
242
+ task_id: taskId,
243
+ session_id: sessionId,
244
+ tool_name: toolName,
245
+ items: items.map((item) => ({
246
+ item_id: item.itemId,
247
+ item_kind: item.itemKind,
248
+ feedback_kind: item.feedbackKind,
249
+ reason: item.reason,
250
+ evidence_summary: item.evidenceSummary,
251
+ reported_by_kind: item.reportedByKind,
252
+ source_uri: onlineSourceUri(item.sourceUri)
253
+ }))
254
+ });
255
+ }
256
+
257
+ export function providerCommonKnowledgePromotion(input) {
258
+ return {
259
+ personal_space_id: input.personalSpaceId,
260
+ parent_project_id: input.parentProjectId,
261
+ item_kind: input.itemKind,
262
+ canonical_item_id: input.canonicalItemId,
263
+ duplicate_item_ids: input.duplicateItemIds,
264
+ reason: input.reason,
265
+ human_confirmation_reason: input.humanConfirmationReason
266
+ };
267
+ }
268
+
269
+ export function agentProjectResolution(resolution) {
270
+ const result = {
271
+ status: resolution.status,
272
+ basis: resolution.basis,
273
+ personal_project_id: resolution.personalProjectId
274
+ };
275
+ if (resolution.candidateCount !== undefined) {
276
+ result.candidate_count = resolution.candidateCount;
277
+ }
278
+ return result;
279
+ }
280
+
281
+ function commonCandidateResult({
282
+ status,
283
+ personalSpaceId,
284
+ parentProjectId,
285
+ childProjectIds,
286
+ query,
287
+ candidates,
288
+ graph
289
+ }) {
290
+ return {
291
+ status,
292
+ personal_space_id: personalSpaceId,
293
+ parent_project_id: parentProjectId,
294
+ child_project_ids: childProjectIds,
295
+ query,
296
+ candidates,
297
+ graph_truncated: graph.truncated === true,
298
+ policy: commonKnowledgeCandidatePolicy()
299
+ };
300
+ }
301
+
302
+ function commonKnowledgeItem(item, itemKind, projectId) {
303
+ return {
304
+ ...item,
305
+ item_kind: itemKind,
306
+ defined_project_id: projectId
307
+ };
308
+ }
309
+
310
+ function clusterCommonKnowledgeItems(
311
+ items,
312
+ minChildProjects,
313
+ similarityThreshold,
314
+ parentProjectId,
315
+ query
316
+ ) {
317
+ const groups = [];
318
+ for (const item of items) {
319
+ const matching = groups
320
+ .map((group) => ({
321
+ group,
322
+ similarity: Math.max(
323
+ ...group.items.map((member) => commonKnowledgeSimilarity(item, member))
324
+ )
325
+ }))
326
+ .filter(({ group, similarity }) =>
327
+ group.itemKind === item.item_kind
328
+ && similarity >= similarityThreshold
329
+ && !group.projectIds.has(item.defined_project_id)
330
+ )
331
+ .sort((left, right) => right.similarity - left.similarity)[0];
332
+ if (matching) {
333
+ matching.group.items.push(item);
334
+ matching.group.projectIds.add(item.defined_project_id);
335
+ continue;
336
+ }
337
+ groups.push({
338
+ itemKind: item.item_kind,
339
+ projectIds: new Set([item.defined_project_id]),
340
+ items: [item]
341
+ });
342
+ }
343
+
344
+ return groups
345
+ .filter(({ projectIds }) => projectIds.size >= minChildProjects)
346
+ .map((group) => {
347
+ const orderedItems = [...group.items].sort((left, right) =>
348
+ left.defined_project_id.localeCompare(right.defined_project_id)
349
+ );
350
+ const pairScores = [];
351
+ for (let left = 0; left < orderedItems.length; left += 1) {
352
+ for (let right = left + 1; right < orderedItems.length; right += 1) {
353
+ pairScores.push(commonKnowledgeSimilarity(
354
+ orderedItems[left],
355
+ orderedItems[right]
356
+ ));
357
+ }
358
+ }
359
+ const averageSimilarity = pairScores.length === 0
360
+ ? 1
361
+ : pairScores.reduce((sum, score) => sum + score, 0) / pairScores.length;
362
+ const itemIds = orderedItems.map(({ id }) => id).sort();
363
+ return {
364
+ candidate_id: `common-${createHash('sha256')
365
+ .update(itemIds.join('\n'))
366
+ .digest('hex')
367
+ .slice(0, 20)}`,
368
+ parent_project_id: parentProjectId,
369
+ child_project_ids: [...group.projectIds].sort(),
370
+ item_kind: group.itemKind,
371
+ query,
372
+ similarity_score: Number(averageSimilarity.toFixed(4)),
373
+ similarity_basis: 'lexical_overlap_across_direct_child_projects',
374
+ items: orderedItems,
375
+ requires_human_confirmation: true,
376
+ promotion_performed: false
377
+ };
378
+ })
379
+ .sort((left, right) =>
380
+ right.child_project_ids.length - left.child_project_ids.length
381
+ || right.similarity_score - left.similarity_score
382
+ || left.candidate_id.localeCompare(right.candidate_id)
383
+ );
384
+ }
385
+
386
+ function commonKnowledgeSimilarity(left, right) {
387
+ if (left.key && right.key && left.key === right.key) return 1;
388
+ const leftTokens = commonKnowledgeTokens(left);
389
+ const rightTokens = commonKnowledgeTokens(right);
390
+ if (leftTokens.size === 0 || rightTokens.size === 0) return 0;
391
+ let intersection = 0;
392
+ for (const token of leftTokens) {
393
+ if (rightTokens.has(token)) intersection += 1;
394
+ }
395
+ return intersection / new Set([...leftTokens, ...rightTokens]).size;
396
+ }
397
+
398
+ function commonKnowledgeTokens(item) {
399
+ const text = [
400
+ item.name,
401
+ item.summary,
402
+ item.fact,
403
+ item.type
404
+ ].filter(Boolean).join(' ').toLowerCase();
405
+ const tokens = new Set(
406
+ text.match(/[a-z0-9]+(?:[._-][a-z0-9]+)*/g) ?? []
407
+ );
408
+ for (const sequence of text.match(/[\u3400-\u9fff]+/g) ?? []) {
409
+ if (sequence.length === 1) tokens.add(sequence);
410
+ for (let index = 0; index < sequence.length - 1; index += 1) {
411
+ tokens.add(sequence.slice(index, index + 2));
412
+ }
413
+ }
414
+ return tokens;
415
+ }
416
+
417
+ function commonKnowledgeCandidatePolicy() {
418
+ return {
419
+ read_only: true,
420
+ direct_part_of_children_only: true,
421
+ inherited_knowledge_excluded: true,
422
+ personal_global_excluded: true,
423
+ automatic_promotion: false,
424
+ human_confirmation_required: true,
425
+ similarity_is_inferred_not_authoritative: true
426
+ };
427
+ }
428
+
429
+ function decisionTraceKnowledge(input) {
430
+ const decisionKey = `decision:${input.decisionKey}`;
431
+ const selectedKey = `${decisionKey}:option:${input.selectedOption.key}`;
432
+ const rationaleKey = `${decisionKey}:rationale`;
433
+ const rejected = input.rejectedOptions ?? [];
434
+ const validations = input.validationResults ?? [];
435
+ const confirmed = ['user', 'authoritative_source'].includes(
436
+ input.decidedBy.kind
437
+ );
438
+ const confirmationStatus = confirmed ? 'confirmed' : 'pending';
439
+ const epistemicStatus = confirmed ? 'confirmed' : 'observed';
440
+ const confirmationBasis = {
441
+ existenceReason: input.sourceDescription,
442
+ quadrantReason: input.reason,
443
+ proposedBy: input.decidedBy,
444
+ confirmedBy: confirmed ? input.decidedBy : null,
445
+ confirmedAt: confirmed ? input.referenceTime : null,
446
+ agentPolicyVersion: null
447
+ };
448
+ const epistemic = {
449
+ originQuadrant: 'known_known',
450
+ currentQuadrant: 'known_known',
451
+ epistemicStatus,
452
+ confirmationStatus,
453
+ confirmationBasis,
454
+ reasoningSummary: input.reason,
455
+ inheritanceMode: 'local_only',
456
+ inheritedProjectIds: []
457
+ };
458
+ const entities = [
459
+ {
460
+ key: decisionKey,
461
+ name: input.title,
462
+ type: 'Decision',
463
+ summary: `${input.question}\nSelected: ${input.selectedOption.label}`,
464
+ attributes: {
465
+ decisionKey: input.decisionKey,
466
+ selectedOptionKey: input.selectedOption.key,
467
+ decidedByKind: input.decidedBy.kind
468
+ },
469
+ ...epistemic
470
+ },
471
+ {
472
+ key: selectedKey,
473
+ name: input.selectedOption.label,
474
+ type: 'DecisionOption',
475
+ summary: input.selectedOption.summary ?? '',
476
+ attributes: { disposition: 'selected' },
477
+ ...epistemic
478
+ },
479
+ ...rejected.map((option) => ({
480
+ key: `${decisionKey}:option:${option.key}`,
481
+ name: option.label,
482
+ type: 'DecisionOption',
483
+ summary: option.summary ?? '',
484
+ attributes: { disposition: 'rejected' },
485
+ ...epistemic
486
+ })),
487
+ {
488
+ key: rationaleKey,
489
+ name: `${input.title} · rationale`,
490
+ type: 'DecisionRationale',
491
+ summary: input.reason,
492
+ attributes: {},
493
+ ...epistemic
494
+ },
495
+ ...validations.map((validation) => ({
496
+ key: `${decisionKey}:validation:${validation.key}`,
497
+ name: `${input.title} · ${validation.key}`,
498
+ type: 'ValidationResult',
499
+ summary: validation.summary,
500
+ attributes: { outcome: validation.outcome },
501
+ ...epistemic
502
+ }))
503
+ ];
504
+ const relationship = (key, target, type, fact) => ({
505
+ key,
506
+ source: decisionKey,
507
+ target,
508
+ type,
509
+ fact,
510
+ confidence: confirmed ? 1 : 0.7,
511
+ attributes: {},
512
+ ...epistemic
513
+ });
514
+ return {
515
+ entities,
516
+ relationships: [
517
+ relationship(
518
+ `${decisionKey}:selected`,
519
+ selectedKey,
520
+ 'SELECTED_OPTION',
521
+ `${input.title} selected ${input.selectedOption.label}.`
522
+ ),
523
+ ...rejected.map((option) => relationship(
524
+ `${decisionKey}:rejected:${option.key}`,
525
+ `${decisionKey}:option:${option.key}`,
526
+ 'REJECTED_OPTION',
527
+ `${input.title} did not select ${option.label}.`
528
+ )),
529
+ relationship(
530
+ `${decisionKey}:motivated-by`,
531
+ rationaleKey,
532
+ 'MOTIVATED_BY',
533
+ input.reason
534
+ ),
535
+ ...validations.map((validation) => relationship(
536
+ `${decisionKey}:validated-by:${validation.key}`,
537
+ `${decisionKey}:validation:${validation.key}`,
538
+ 'VALIDATED_BY',
539
+ `${validation.outcome}: ${validation.summary}`
540
+ ))
541
+ ]
542
+ };
543
+ }