fuli-context 0.3.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 (87) hide show
  1. package/README.md +0 -24
  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 +83 -13
  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 +1 -56
  42. package/src/cli.js +2 -37
  43. package/src/graphiti/agent-knowledge-workflows.js +543 -0
  44. package/src/graphiti/federated-application.js +85 -104
  45. package/src/graphiti/knowledge-provider-mapping.js +91 -0
  46. package/src/graphiti/provider-client.js +15 -0
  47. package/src/graphiti/source-uri.js +29 -0
  48. package/src/mcp/common-knowledge-preview-tokens.js +67 -0
  49. package/src/mcp/create-mcp-server.js +43 -7
  50. package/src/mcp/instructions.js +6 -7
  51. package/src/mcp/lifecycle-audit.js +27 -0
  52. package/src/mcp/task-context-registry.js +93 -0
  53. package/src/mcp/tool-annotations.js +16 -1
  54. package/src/mcp/tool-result.js +24 -0
  55. package/src/runtime-options.js +7 -8
  56. package/src/setup/agent-installation-status.js +14 -2
  57. package/src/setup/agents.js +12 -1
  58. package/src/setup/claude-code-config.js +194 -0
  59. package/src/setup/config-backup.js +20 -5
  60. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +0 -1
  61. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +0 -1
  62. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +0 -1
  63. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +0 -1
  64. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +0 -1
  65. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +0 -1
  66. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +0 -4
  67. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +0 -1
  68. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +0 -1
  69. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +0 -1
  70. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +0 -1
  71. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +0 -1
  72. package/dist/web/assets/ReviewPage-Dztk2jDG.js +0 -1
  73. package/dist/web/assets/console-tbvTUi6p.js +0 -1
  74. package/dist/web/assets/identity-B3s5CXhv.js +0 -1
  75. package/dist/web/assets/index-DwUDhQKc.js +0 -6
  76. package/dist/web/assets/model-DVT8sjy9.js +0 -1
  77. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +0 -1
  78. package/src/cli/commands/candidates.js +0 -17
  79. package/src/cli/commands/ingestion.js +0 -32
  80. package/src/cli/commands/reads.js +0 -50
  81. package/src/cli/commands/spaces.js +0 -20
  82. package/src/cli/invocation.js +0 -47
  83. package/src/cli/migrate-command.js +0 -71
  84. package/src/cli/migration-destination.js +0 -117
  85. package/src/storage/import-json.js +0 -70
  86. package/src/storage/legacy-record-validation.js +0 -152
  87. package/src/storage/legacy-snapshot-validation.js +0 -32
@@ -9,6 +9,11 @@ from .knowledge_usage_models import (
9
9
  KnowledgeUsageItemResult,
10
10
  KnowledgeUsageResult,
11
11
  )
12
+ from .knowledge_feedback_models import (
13
+ KnowledgeFeedbackCreate,
14
+ KnowledgeFeedbackItemResult,
15
+ KnowledgeFeedbackResult,
16
+ )
12
17
  from .models import (
13
18
  KnowledgeAgentReviewCreate,
14
19
  KnowledgeAgentViewCreate,
@@ -29,6 +34,12 @@ USAGE_WEIGHTS = {
29
34
  'cited': (0.12, 0.04),
30
35
  'applied': (0.18, 0.06),
31
36
  }
37
+ NEGATIVE_EVIDENCE_WEIGHTS = {
38
+ 'rejected': (0.35, 0.25),
39
+ 'validation_failed': (0.35, 0.30),
40
+ 'contradicted': (0.45, 0.40),
41
+ 'outdated': (0.20, 0.20),
42
+ }
32
43
 
33
44
 
34
45
  async def record_human_change(
@@ -147,6 +158,130 @@ async def record_knowledge_usage(
147
158
  )
148
159
 
149
160
 
161
+ async def record_knowledge_feedback(
162
+ store,
163
+ actor: dict,
164
+ request: KnowledgeFeedbackCreate,
165
+ ) -> KnowledgeFeedbackResult:
166
+ """Record bounded negative evidence without overriding human authority."""
167
+ store._require_personal()
168
+ space = await store.authorize(actor, request.personal_space_id, 'maintainer')
169
+ if space['kind'] != 'personal':
170
+ raise HTTPException(status_code=422, detail='knowledge feedback is personal-only')
171
+
172
+ lock = store._group_locks.setdefault(space['group_id'], asyncio.Lock())
173
+ results = []
174
+ async with lock:
175
+ for item in request.items:
176
+ results.append(await _record_one_knowledge_feedback(
177
+ store, actor, space, request, item
178
+ ))
179
+ return KnowledgeFeedbackResult(
180
+ recorded_count=sum(result.recorded for result in results),
181
+ duplicate_count=sum(not result.recorded for result in results),
182
+ items=results,
183
+ )
184
+
185
+
186
+ async def _record_one_knowledge_feedback(store, actor, space, request, item):
187
+ state = await _read_usage_state(
188
+ store, space, item.item_id, item.item_kind
189
+ )
190
+ if state is None:
191
+ raise HTTPException(status_code=404, detail='knowledge item not found')
192
+ if state.get('invalid_at') is not None:
193
+ raise HTTPException(
194
+ status_code=409,
195
+ detail='historical knowledge cannot receive new feedback evidence',
196
+ )
197
+ generation = int(state.get('usage_generation') or 1)
198
+ previous_status = state.get('confirmation_status') or 'pending'
199
+ trusted_reporter = item.reported_by_kind in {
200
+ 'user', 'authoritative_source'
201
+ }
202
+ next_status = (
203
+ 'pending'
204
+ if previous_status == 'agent_confirmed' and trusted_reporter
205
+ else previous_status
206
+ )
207
+ basis = json_object(state.get('confirmation_basis_json'))
208
+ if next_status == 'pending' and previous_status == 'agent_confirmed':
209
+ basis = _pending_feedback_basis(basis, item.reason)
210
+ utility_weight, confidence_weight = NEGATIVE_EVIDENCE_WEIGHTS[
211
+ item.feedback_kind
212
+ ]
213
+ utility_score = round(max(
214
+ 0.0,
215
+ float(state.get('utility_score') or 0) - utility_weight,
216
+ ), 4)
217
+ confidence_score = round(max(
218
+ 0.0,
219
+ float(
220
+ state['confidence_score']
221
+ if state.get('confidence_score') is not None else 0.5
222
+ ) - confidence_weight,
223
+ ), 4)
224
+ feedback_at = datetime.now(timezone.utc)
225
+ audit_id = stable_uuid(
226
+ space['id'],
227
+ 'knowledge-feedback',
228
+ item.item_kind,
229
+ item.item_id,
230
+ str(generation),
231
+ request.task_id,
232
+ item.feedback_kind,
233
+ )
234
+ records, _, _ = await store.runtime.driver.execute_query(
235
+ _knowledge_feedback_query(item.item_kind),
236
+ space_id=space['id'],
237
+ group_id=space['group_id'],
238
+ item_id=item.item_id,
239
+ item_kind=item.item_kind,
240
+ audit_id=audit_id,
241
+ task_id=request.task_id,
242
+ session_id=request.session_id,
243
+ tool_name=request.tool_name,
244
+ feedback_kind=item.feedback_kind,
245
+ reported_by_kind=item.reported_by_kind,
246
+ reason=item.reason,
247
+ evidence_summary=item.evidence_summary,
248
+ source_uri=item.source_uri,
249
+ usage_generation=generation,
250
+ next_confirmation_status=next_status,
251
+ next_confirmation_basis_json=(
252
+ json.dumps(basis, ensure_ascii=False, sort_keys=True)
253
+ if basis else None
254
+ ),
255
+ utility_score=utility_score,
256
+ confidence_score=confidence_score,
257
+ created_by=actor['id'],
258
+ feedback_at=feedback_at,
259
+ )
260
+ if not records:
261
+ raise HTTPException(
262
+ status_code=409,
263
+ detail='knowledge changed while feedback evidence was recorded; retry',
264
+ )
265
+ record = dict(records[0])
266
+ return KnowledgeFeedbackItemResult(
267
+ item_id=item.item_id,
268
+ item_kind=item.item_kind,
269
+ feedback_kind=item.feedback_kind,
270
+ recorded=record.get('recorded') is True,
271
+ confirmation_status=record.get('confirmation_status') or next_status,
272
+ utility_score=float(record.get('utility_score') or 0),
273
+ confidence_score=float(
274
+ record['confidence_score']
275
+ if record.get('confidence_score') is not None else 0.5
276
+ ),
277
+ negative_evidence_count=int(
278
+ record.get('negative_evidence_count') or 0
279
+ ),
280
+ requires_attention=record.get('requires_attention') is True,
281
+ usage_generation=generation,
282
+ )
283
+
284
+
150
285
  async def _record_one_knowledge_use(store, actor, space, request, item):
151
286
  state = await _read_usage_state(
152
287
  store,
@@ -470,6 +605,10 @@ def _audit_record(value: dict) -> KnowledgeAuditRecord:
470
605
  task_id=value.get('task_id'),
471
606
  session_id=value.get('session_id'),
472
607
  use_kind=value.get('use_kind'),
608
+ feedback_kind=value.get('feedback_kind'),
609
+ reported_by_kind=value.get('reported_by_kind'),
610
+ evidence_summary=value.get('evidence_summary'),
611
+ source_uri=value.get('source_uri'),
473
612
  usage_generation=(
474
613
  int(value['usage_generation'])
475
614
  if value.get('usage_generation') is not None else None
@@ -592,6 +731,10 @@ async def _read_usage_state(store, space, item_id, item_kind):
592
731
  AS confirmation_status,
593
732
  item.fuli_confirmation_basis_json AS confirmation_basis_json,
594
733
  coalesce(item.fuli_usage_generation, 1) AS usage_generation,
734
+ coalesce(item.fuli_utility_score, 0.0) AS utility_score,
735
+ coalesce(item.fuli_confidence_score, 0.5) AS confidence_score,
736
+ coalesce(item.fuli_negative_evidence_count, 0)
737
+ AS negative_evidence_count,
595
738
  {
596
739
  "item.fuli_invalid_at"
597
740
  if item_kind == "entity" else "item.invalid_at"
@@ -649,6 +792,60 @@ def _knowledge_use_query(item_kind: str) -> str:
649
792
  '''
650
793
 
651
794
 
795
+ def _knowledge_feedback_query(item_kind: str) -> str:
796
+ return f'''
797
+ MATCH (space:FuliSpace {{id: $space_id, kind: 'personal'}})
798
+ {_item_match(item_kind)}
799
+ WHERE coalesce(item.fuli_usage_generation, 1) = $usage_generation
800
+ MERGE (audit:FuliKnowledgeAudit {{id: $audit_id}})
801
+ ON CREATE SET audit.space_id = $space_id,
802
+ audit.item_id = $item_id,
803
+ audit.item_kind = $item_kind,
804
+ audit.action = 'knowledge_feedback',
805
+ audit.human_change_version = 0,
806
+ audit.task_id = $task_id,
807
+ audit.session_id = $session_id,
808
+ audit.tool_name = $tool_name,
809
+ audit.feedback_kind = $feedback_kind,
810
+ audit.reported_by_kind = $reported_by_kind,
811
+ audit.reason = $reason,
812
+ audit.evidence_summary = $evidence_summary,
813
+ audit.source_uri = $source_uri,
814
+ audit.usage_generation = $usage_generation,
815
+ audit.outcome = 'requires_attention',
816
+ audit.created_by = $created_by,
817
+ audit.created_at = $feedback_at
818
+ MERGE (space)-[:HAS_KNOWLEDGE_AUDIT]->(audit)
819
+ WITH item, audit, audit.created_at = $feedback_at AS recorded
820
+ FOREACH (_ IN CASE WHEN recorded THEN [1] ELSE [] END |
821
+ SET item.fuli_utility_score = $utility_score,
822
+ item.fuli_confidence_score = $confidence_score,
823
+ item.fuli_confirmation_status = $next_confirmation_status,
824
+ item.fuli_confirmation_basis_json =
825
+ $next_confirmation_basis_json,
826
+ item.fuli_epistemic_status =
827
+ CASE WHEN $next_confirmation_status = 'confirmed'
828
+ THEN 'confirmed'
829
+ WHEN item.fuli_origin_quadrant = 'unknown_unknown'
830
+ THEN 'exploratory'
831
+ ELSE 'observed' END,
832
+ item.fuli_negative_evidence_count =
833
+ coalesce(item.fuli_negative_evidence_count, 0) + 1,
834
+ item.fuli_requires_attention = true,
835
+ item.fuli_last_feedback_kind = $feedback_kind,
836
+ item.fuli_last_feedback_at = $feedback_at
837
+ )
838
+ RETURN recorded,
839
+ item.fuli_confirmation_status AS confirmation_status,
840
+ coalesce(item.fuli_utility_score, 0.0) AS utility_score,
841
+ coalesce(item.fuli_confidence_score, 0.5) AS confidence_score,
842
+ coalesce(item.fuli_negative_evidence_count, 0)
843
+ AS negative_evidence_count,
844
+ coalesce(item.fuli_requires_attention, false)
845
+ AS requires_attention
846
+ '''
847
+
848
+
652
849
  def _usage_score_update_query(item_kind: str) -> str:
653
850
  return f'''
654
851
  {_item_match(item_kind)}
@@ -698,6 +895,20 @@ def _agent_confirmation_basis(value: dict, confirmed_at: datetime) -> dict:
698
895
  }
699
896
 
700
897
 
898
+ def _pending_feedback_basis(value: dict, reason: str) -> dict:
899
+ result = dict(value or {})
900
+ result.setdefault('existence_reason', reason)
901
+ result.setdefault(
902
+ 'quadrant_reason',
903
+ 'The immutable discovery quadrant is unchanged by negative evidence.',
904
+ )
905
+ result.setdefault('proposed_by', {'kind': 'agent', 'label': 'Fuli'})
906
+ result['confirmed_by'] = None
907
+ result['confirmed_at'] = None
908
+ result.pop('agent_policy_version', None)
909
+ return result
910
+
911
+
701
912
  _HUMAN_ENTITY_SEARCH = '''
702
913
  MATCH (item:Entity {group_id: $group_id})
703
914
  WHERE item.fuli_human_edited = true
@@ -0,0 +1,69 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import Field, field_validator, model_validator
4
+
5
+ from .models import (
6
+ ConfirmationStatus,
7
+ KnowledgeFeedbackKind,
8
+ KnowledgeItemKind,
9
+ StrictModel,
10
+ _validated_source_uri,
11
+ )
12
+
13
+ KnowledgeFeedbackReporterKind = Literal[
14
+ 'user',
15
+ 'agent',
16
+ 'authoritative_source',
17
+ ]
18
+
19
+
20
+ class KnowledgeFeedbackItem(StrictModel):
21
+ item_id: str = Field(min_length=1, max_length=256)
22
+ item_kind: KnowledgeItemKind
23
+ feedback_kind: KnowledgeFeedbackKind
24
+ reason: str = Field(min_length=1, max_length=2000)
25
+ evidence_summary: str = Field(min_length=1, max_length=4096)
26
+ reported_by_kind: KnowledgeFeedbackReporterKind
27
+ source_uri: str | None = Field(default=None, min_length=1, max_length=2048)
28
+
29
+ @field_validator('source_uri')
30
+ @classmethod
31
+ def validate_source_uri(cls, value):
32
+ return _validated_source_uri(value) if value is not None else None
33
+
34
+
35
+ class KnowledgeFeedbackCreate(StrictModel):
36
+ personal_space_id: str = Field(min_length=1, max_length=128)
37
+ task_id: str = Field(min_length=1, max_length=256)
38
+ session_id: str | None = Field(default=None, min_length=1, max_length=256)
39
+ tool_name: str | None = Field(default=None, min_length=1, max_length=128)
40
+ items: list[KnowledgeFeedbackItem] = Field(min_length=1, max_length=200)
41
+
42
+ @model_validator(mode='after')
43
+ def validate_feedback_items(self):
44
+ identities = {
45
+ (item.item_kind, item.item_id, item.feedback_kind)
46
+ for item in self.items
47
+ }
48
+ if len(identities) != len(self.items):
49
+ raise ValueError('knowledge feedback items must be unique')
50
+ return self
51
+
52
+
53
+ class KnowledgeFeedbackItemResult(StrictModel):
54
+ item_id: str
55
+ item_kind: KnowledgeItemKind
56
+ feedback_kind: KnowledgeFeedbackKind
57
+ recorded: bool
58
+ confirmation_status: ConfirmationStatus
59
+ utility_score: float = Field(ge=0, le=1)
60
+ confidence_score: float = Field(ge=0, le=1)
61
+ negative_evidence_count: int = Field(ge=0)
62
+ requires_attention: bool
63
+ usage_generation: int = Field(ge=1)
64
+
65
+
66
+ class KnowledgeFeedbackResult(StrictModel):
67
+ recorded_count: int = Field(ge=0, le=200)
68
+ duplicate_count: int = Field(ge=0, le=200)
69
+ items: list[KnowledgeFeedbackItemResult] = Field(max_length=200)
@@ -390,6 +390,12 @@ async def _read_item(store, space: dict, item_id: str, item_kind: str):
390
390
  AS distinct_task_count,
391
391
  item.fuli_last_used_at AS last_used_at,
392
392
  coalesce(item.fuli_usage_generation, 1) AS usage_generation,
393
+ coalesce(item.fuli_negative_evidence_count, 0)
394
+ AS negative_evidence_count,
395
+ coalesce(item.fuli_requires_attention, false)
396
+ AS requires_attention,
397
+ item.fuli_last_feedback_kind AS last_feedback_kind,
398
+ item.fuli_last_feedback_at AS last_feedback_at,
393
399
  item.fuli_replaced_by_item_id AS replaced_by_item_id,
394
400
  item.fuli_replaced_by_item_kind AS replaced_by_item_kind
395
401
  '''
@@ -426,6 +432,12 @@ async def _read_item(store, space: dict, item_id: str, item_kind: str):
426
432
  AS distinct_task_count,
427
433
  item.fuli_last_used_at AS last_used_at,
428
434
  coalesce(item.fuli_usage_generation, 1) AS usage_generation,
435
+ coalesce(item.fuli_negative_evidence_count, 0)
436
+ AS negative_evidence_count,
437
+ coalesce(item.fuli_requires_attention, false)
438
+ AS requires_attention,
439
+ item.fuli_last_feedback_kind AS last_feedback_kind,
440
+ item.fuli_last_feedback_at AS last_feedback_at,
429
441
  item.fuli_replaced_by_item_id AS replaced_by_item_id,
430
442
  item.fuli_replaced_by_item_kind AS replaced_by_item_kind
431
443
  '''
@@ -525,6 +537,12 @@ def _snapshot(value: dict, item_kind: str) -> dict:
525
537
  'distinctTaskCount': int(value.get('distinct_task_count') or 0),
526
538
  'lastUsedAt': _iso(value.get('last_used_at')),
527
539
  'usageGeneration': int(value.get('usage_generation') or 1),
540
+ 'negativeEvidenceCount': int(
541
+ value.get('negative_evidence_count') or 0
542
+ ),
543
+ 'requiresAttention': value.get('requires_attention') is True,
544
+ 'lastFeedbackKind': value.get('last_feedback_kind'),
545
+ 'lastFeedbackAt': _iso(value.get('last_feedback_at')),
528
546
  'replacedByItemId': value.get('replaced_by_item_id'),
529
547
  'replacedByItemKind': value.get('replaced_by_item_kind'),
530
548
  }
@@ -610,6 +628,11 @@ def _next_snapshot(previous: dict, request: KnowledgeRevisionCreate, changed_at:
610
628
  value['qualifiedUseCount'] = 0
611
629
  value['distinctTaskCount'] = 0
612
630
  value['lastUsedAt'] = None
631
+ if content_changed or classification_changed:
632
+ value['negativeEvidenceCount'] = 0
633
+ value['requiresAttention'] = False
634
+ value['lastFeedbackKind'] = None
635
+ value['lastFeedbackAt'] = None
613
636
  if request.reasoning_summary is not None:
614
637
  value['reasoningSummary'] = request.reasoning_summary or None
615
638
  if request.profile_aspect is not None:
@@ -691,6 +714,10 @@ async def _update_item(store, space, item_id, item_kind, value, embedding):
691
714
  item.fuli_distinct_task_count = $distinct_task_count,
692
715
  item.fuli_last_used_at = $last_used_at,
693
716
  item.fuli_usage_generation = $usage_generation,
717
+ item.fuli_negative_evidence_count = $negative_evidence_count,
718
+ item.fuli_requires_attention = $requires_attention,
719
+ item.fuli_last_feedback_kind = $last_feedback_kind,
720
+ item.fuli_last_feedback_at = $last_feedback_at,
694
721
  item.fuli_replaced_by_item_id = $replaced_by_item_id,
695
722
  item.fuli_replaced_by_item_kind = $replaced_by_item_kind,
696
723
  item.name_embedding = coalesce($embedding, item.name_embedding)
@@ -718,6 +745,10 @@ async def _update_item(store, space, item_id, item_kind, value, embedding):
718
745
  item.fuli_distinct_task_count = $distinct_task_count,
719
746
  item.fuli_last_used_at = $last_used_at,
720
747
  item.fuli_usage_generation = $usage_generation,
748
+ item.fuli_negative_evidence_count = $negative_evidence_count,
749
+ item.fuli_requires_attention = $requires_attention,
750
+ item.fuli_last_feedback_kind = $last_feedback_kind,
751
+ item.fuli_last_feedback_at = $last_feedback_at,
721
752
  item.fuli_replaced_by_item_id = $replaced_by_item_id,
722
753
  item.fuli_replaced_by_item_kind = $replaced_by_item_kind,
723
754
  item.fact_embedding = coalesce($embedding, item.fact_embedding)
@@ -752,6 +783,12 @@ async def _update_item(store, space, item_id, item_kind, value, embedding):
752
783
  distinct_task_count=int(value.get('distinctTaskCount') or 0),
753
784
  last_used_at=_parse_datetime(value.get('lastUsedAt')),
754
785
  usage_generation=int(value.get('usageGeneration') or 1),
786
+ negative_evidence_count=int(
787
+ value.get('negativeEvidenceCount') or 0
788
+ ),
789
+ requires_attention=value.get('requiresAttention') is True,
790
+ last_feedback_kind=value.get('lastFeedbackKind'),
791
+ last_feedback_at=_parse_datetime(value.get('lastFeedbackAt')),
755
792
  replaced_by_item_id=value.get('replacedByItemId'),
756
793
  replaced_by_item_kind=value.get('replacedByItemKind'),
757
794
  embedding=embedding,
@@ -16,6 +16,7 @@ from .search_projection import (
16
16
  fact_result,
17
17
  is_default_retrievable,
18
18
  read_edge_epistemic_metadata,
19
+ source_uris_from_references,
19
20
  )
20
21
 
21
22
  MIN_TERM_COVERAGE = 0.25
@@ -390,7 +391,20 @@ async def _personal_entities(
390
391
  AS qualified_use_count,
391
392
  coalesce(node.fuli_distinct_task_count, 0)
392
393
  AS distinct_task_count,
393
- node.fuli_last_used_at AS last_used_at
394
+ node.fuli_last_used_at AS last_used_at,
395
+ coalesce(node.fuli_negative_evidence_count, 0)
396
+ AS negative_evidence_count,
397
+ coalesce(node.fuli_requires_attention, false)
398
+ AS requires_attention,
399
+ node.fuli_last_feedback_kind AS last_feedback_kind,
400
+ node.fuli_last_feedback_at AS last_feedback_at,
401
+ [episode IN episodes
402
+ WHERE episode.fuli_source_uri IS NOT NULL |
403
+ {
404
+ uri: episode.fuli_source_uri,
405
+ reference_time: episode.valid_at,
406
+ created_at: episode.created_at
407
+ }] AS source_references
394
408
  ORDER BY node.name, node.uuid
395
409
  LIMIT $candidate_limit
396
410
  ''',
@@ -565,12 +579,14 @@ def _ranked_relevance(base_score: float, metadata) -> float:
565
579
  confidence_factor = 0.75 + min(max(confidence, 0), 1) * 0.25
566
580
  utility_factor = 0.95 + min(max(utility, 0), 1) * 0.1
567
581
  scope_factor = max(0.65, 1.0 - scope_distance * 0.15)
582
+ attention_factor = 0.6 if metadata.get('requires_attention') else 1.0
568
583
  return round(
569
584
  base_score
570
585
  * status_factor
571
586
  * confidence_factor
572
587
  * utility_factor
573
- * scope_factor,
588
+ * scope_factor
589
+ * attention_factor,
574
590
  4,
575
591
  )
576
592
 
@@ -618,6 +634,7 @@ def _dedupe_ranked_entities(ranked, active_project_id):
618
634
  def _entity_search_result(record, score: float, space_id: str) -> EntitySearchResult:
619
635
  value = dict(record)
620
636
  basis = json_object(value.pop('confirmation_basis_json', None))
637
+ source_references = value.pop('source_references', [])
621
638
  created_at = _native_datetime(value.pop('created_at', None))
622
639
  last_human_changed_at = _native_datetime(
623
640
  value.pop('last_human_changed_at', None)
@@ -629,6 +646,9 @@ def _entity_search_result(record, score: float, space_id: str) -> EntitySearchRe
629
646
  value.pop('last_agent_reviewed_at', None)
630
647
  )
631
648
  last_used_at = _native_datetime(value.pop('last_used_at', None))
649
+ last_feedback_at = _native_datetime(
650
+ value.pop('last_feedback_at', None)
651
+ )
632
652
  for field in (
633
653
  'assignment_project_id',
634
654
  'episode_project_ids',
@@ -645,9 +665,11 @@ def _entity_search_result(record, score: float, space_id: str) -> EntitySearchRe
645
665
  last_agent_viewed_at=last_agent_viewed_at,
646
666
  last_agent_reviewed_at=last_agent_reviewed_at,
647
667
  last_used_at=last_used_at,
668
+ last_feedback_at=last_feedback_at,
648
669
  confirmation_basis=(
649
670
  ConfirmationBasis.model_validate(basis) if basis else None
650
671
  ),
672
+ source_uris=source_uris_from_references(source_references),
651
673
  score=score,
652
674
  )
653
675
 
@@ -1,5 +1,6 @@
1
1
  from datetime import datetime
2
2
  from typing import Any, Literal
3
+ from urllib.parse import urlsplit
3
4
 
4
5
  from pydantic import (
5
6
  AliasChoices,
@@ -59,8 +60,15 @@ KnowledgeAuditAction = Literal[
59
60
  'agent_view',
60
61
  'agent_review',
61
62
  'knowledge_used',
63
+ 'knowledge_feedback',
62
64
  ]
63
65
  KnowledgeUseKind = Literal['cited', 'applied']
66
+ KnowledgeFeedbackKind = Literal[
67
+ 'rejected',
68
+ 'validation_failed',
69
+ 'contradicted',
70
+ 'outdated',
71
+ ]
64
72
  KnowledgeInheritanceMode = Literal[
65
73
  'local_only',
66
74
  'descendants',
@@ -74,6 +82,8 @@ KnowledgeRevisionAction = Literal[
74
82
  'restore',
75
83
  'scope_change',
76
84
  'batch_confirm',
85
+ 'promote_common',
86
+ 'replace_common_duplicate',
77
87
  ]
78
88
  KnowledgeContentRevisionAction = Literal[
79
89
  'confirm',
@@ -103,10 +113,39 @@ PreferenceConflictResolutionAction = Literal[
103
113
  ]
104
114
  SourceApplication = Literal['codex', 'claude_code', 'cursor', 'kiro', 'other']
105
115
 
116
+
106
117
  class StrictModel(BaseModel):
107
118
  model_config = ConfigDict(extra='forbid', str_strip_whitespace=True)
108
119
 
109
120
 
121
+ def _validated_source_uri(value: str) -> str:
122
+ if any(
123
+ character.isspace() or ord(character) < 32 or ord(character) == 127
124
+ for character in value
125
+ ):
126
+ raise ValueError(
127
+ 'source_uri must be an absolute HTTP(S) URI without credentials'
128
+ )
129
+ try:
130
+ parsed = urlsplit(value)
131
+ hostname = parsed.hostname
132
+ except ValueError as error:
133
+ raise ValueError(
134
+ 'source_uri must be an absolute HTTP(S) URI without credentials'
135
+ ) from error
136
+ if (
137
+ parsed.scheme.casefold() not in {'http', 'https'}
138
+ or not parsed.netloc
139
+ or not hostname
140
+ or parsed.username is not None
141
+ or parsed.password is not None
142
+ ):
143
+ raise ValueError(
144
+ 'source_uri must be an absolute HTTP(S) URI without credentials'
145
+ )
146
+ return value
147
+
148
+
110
149
  def _validate_inheritance(
111
150
  mode: KnowledgeInheritanceMode,
112
151
  project_ids: list[str],
@@ -501,6 +540,7 @@ class StructuredEpisode(StrictModel):
501
540
  name: str = Field(min_length=1, max_length=512)
502
541
  source_kind: str = Field(min_length=1, max_length=128)
503
542
  source_description: str = Field(min_length=1, max_length=1024)
543
+ source_uri: str | None = Field(default=None, min_length=1, max_length=2048)
504
544
  source_application: SourceApplication | None = None
505
545
  source_turn_id: str | None = Field(default=None, min_length=1, max_length=256)
506
546
  source_excerpt: str | None = Field(default=None, min_length=1, max_length=2048)
@@ -510,6 +550,11 @@ class StructuredEpisode(StrictModel):
510
550
  entities: list[EntityInput] = Field(min_length=1, max_length=128)
511
551
  relationships: list[RelationshipInput] = Field(default_factory=list, max_length=256)
512
552
 
553
+ @field_validator('source_uri')
554
+ @classmethod
555
+ def validate_source_uri(cls, value: str | None) -> str | None:
556
+ return _validated_source_uri(value) if value is not None else None
557
+
513
558
  @model_validator(mode='after')
514
559
  def complete_confirmation_state(self):
515
560
  proposer = ConfirmationActor(
@@ -722,9 +767,14 @@ class EntitySearchResult(StrictModel):
722
767
  qualified_use_count: int = Field(default=0, ge=0)
723
768
  distinct_task_count: int = Field(default=0, ge=0)
724
769
  last_used_at: datetime | None = None
770
+ negative_evidence_count: int = Field(default=0, ge=0)
771
+ requires_attention: bool = False
772
+ last_feedback_kind: KnowledgeFeedbackKind | None = None
773
+ last_feedback_at: datetime | None = None
725
774
  scope_distance: int = Field(default=0, ge=0, le=8)
726
775
  inherited_from_project_id: str | None = None
727
776
  scope_path: list[str] = Field(default_factory=list, max_length=9)
777
+ source_uris: list[str] = Field(default_factory=list, max_length=20)
728
778
  score: float | None = None
729
779
 
730
780
 
@@ -764,9 +814,14 @@ class FactResult(StrictModel):
764
814
  qualified_use_count: int = Field(default=0, ge=0)
765
815
  distinct_task_count: int = Field(default=0, ge=0)
766
816
  last_used_at: datetime | None = None
817
+ negative_evidence_count: int = Field(default=0, ge=0)
818
+ requires_attention: bool = False
819
+ last_feedback_kind: KnowledgeFeedbackKind | None = None
820
+ last_feedback_at: datetime | None = None
767
821
  scope_distance: int = Field(default=0, ge=0, le=8)
768
822
  inherited_from_project_id: str | None = None
769
823
  scope_path: list[str] = Field(default_factory=list, max_length=9)
824
+ source_uris: list[str] = Field(default_factory=list, max_length=20)
770
825
  score: float | None = None
771
826
 
772
827
 
@@ -892,6 +947,7 @@ class GraphEvidence(StrictModel):
892
947
  name: str
893
948
  source_description: str
894
949
  source_kind: str
950
+ source_uri: str | None = Field(default=None, min_length=1, max_length=2048)
895
951
  summary: str
896
952
  session_id: str | None = None
897
953
  source_application: SourceApplication | None = None
@@ -901,6 +957,11 @@ class GraphEvidence(StrictModel):
901
957
  reference_time: datetime | None = None
902
958
  created_at: datetime | None = None
903
959
 
960
+ @field_validator('source_uri')
961
+ @classmethod
962
+ def validate_source_uri(cls, value: str | None) -> str | None:
963
+ return _validated_source_uri(value) if value is not None else None
964
+
904
965
 
905
966
  class KnowledgeRevisionCreate(StrictModel):
906
967
  personal_space_id: str = Field(min_length=1, max_length=128)
@@ -1100,6 +1161,14 @@ class KnowledgeAuditRecord(StrictModel):
1100
1161
  task_id: str | None = None
1101
1162
  session_id: str | None = None
1102
1163
  use_kind: KnowledgeUseKind | None = None
1164
+ feedback_kind: KnowledgeFeedbackKind | None = None
1165
+ reported_by_kind: Literal[
1166
+ 'user',
1167
+ 'agent',
1168
+ 'authoritative_source',
1169
+ ] | None = None
1170
+ evidence_summary: str | None = None
1171
+ source_uri: str | None = None
1103
1172
  usage_generation: int | None = Field(default=None, ge=1)
1104
1173
  conflict_check: Literal['no_conflict', 'conflict'] | None = None
1105
1174
  classification_check: Literal['reasonable', 'needs_change'] | None = None