fuli-context 0.7.2 → 0.7.4-beta.1

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 (113) hide show
  1. package/README.md +173 -21
  2. package/README.zh-CN.md +146 -12
  3. package/compose.graphiti.yml +2 -0
  4. package/dist/web/assets/{AboutPage-DfnZo7Lo.js → AboutPage-CftRexSY.js} +1 -1
  5. package/dist/web/assets/{ConnectionsPage-BloaSKmt.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-CHE6XhdS.js +1 -0
  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-B7EN817i.js → ReviewPage-DUeyu7E8.js} +1 -1
  19. package/dist/web/assets/{SearchableSelect-ClLN0rIb.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-CbDsKrsO.js → console-DMvHxKf9.js} +1 -1
  25. package/dist/web/assets/conversation-launcher-settings-Dnlcw7Rj.js +1 -0
  26. package/dist/web/assets/{i18n-BQcns1PH.js → i18n-CWfd8Dtt.js} +1 -1
  27. package/dist/web/assets/{identity-B8K4VYg6.js → identity-C9mLdR8L.js} +1 -1
  28. package/dist/web/assets/index-F5e2sQ7p.js +2 -0
  29. package/dist/web/assets/index-X2j29j8i.css +1 -0
  30. package/dist/web/assets/{model-eF52GgPF.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_review.py +49 -4
  45. package/graph-provider/fuli_graph/knowledge_review_models.py +11 -5
  46. package/graph-provider/fuli_graph/knowledge_search.py +11 -0
  47. package/graph-provider/fuli_graph/models.py +15 -2
  48. package/graph-provider/fuli_graph/personal_global_preference_models.py +234 -0
  49. package/graph-provider/fuli_graph/personal_global_preferences.py +1247 -0
  50. package/graph-provider/fuli_graph/project_action_models.py +22 -0
  51. package/graph-provider/fuli_graph/project_knowledge.py +121 -2
  52. package/graph-provider/fuli_graph/provider_values.py +24 -0
  53. package/graph-provider/fuli_graph/runtime.py +78 -0
  54. package/graph-provider/fuli_graph/search_projection.py +14 -1
  55. package/graph-provider/fuli_graph/store_knowledge.py +116 -2
  56. package/graph-provider/fuli_graph/workflow_candidate_models.py +168 -0
  57. package/graph-provider/fuli_graph/workflow_candidates.py +996 -0
  58. package/graph-provider/fuli_graph/workflow_observation_models.py +30 -0
  59. package/graph-provider/fuli_graph/workflow_observations.py +84 -0
  60. package/graph-provider/pyproject.toml +1 -1
  61. package/npm-shrinkwrap.json +18 -18
  62. package/package.json +4 -4
  63. package/skills/capturing-session-knowledge/SKILL.md +32 -5
  64. package/skills/flreview/SKILL.md +68 -40
  65. package/skills/flreview/agents/openai.yaml +2 -2
  66. package/src/agent-tools/graph-definitions.js +140 -24
  67. package/src/agent-tools/graph-handlers.js +10 -4
  68. package/src/cli/command-registry.js +1 -1
  69. package/src/cli/local-runtime-command.js +69 -2
  70. package/src/graphiti/agent-knowledge-workflows.js +497 -16
  71. package/src/graphiti/collaboration-preference-projection.js +162 -0
  72. package/src/graphiti/federated-application.js +157 -162
  73. package/src/graphiti/federated-search-support.js +102 -0
  74. package/src/graphiti/knowledge-provider-mapping.js +15 -0
  75. package/src/graphiti/provider-client.js +162 -10
  76. package/src/graphiti/related-project-suggestions.js +109 -0
  77. package/src/graphiti/runtime-config.js +14 -0
  78. package/src/graphiti/source-marker.js +2 -2
  79. package/src/graphiti/user-taste-skill.js +241 -0
  80. package/src/graphiti/workflow-candidate-mapping.js +87 -0
  81. package/src/graphiti/workflow-observation.js +82 -0
  82. package/src/graphiti/writing-taste-profile-workflow.js +25 -0
  83. package/src/graphiti/writing-taste-profile.js +490 -0
  84. package/src/http/graph-api-router.js +15 -2
  85. package/src/mcp/context-resources.js +158 -0
  86. package/src/mcp/create-mcp-server.js +41 -5
  87. package/src/mcp/instructions.js +5 -7
  88. package/src/mcp/session-id.js +60 -0
  89. package/src/mcp/task-context-registry.js +15 -3
  90. package/src/mcp/tool-annotations.js +8 -1
  91. package/src/mcp/tool-result.js +28 -4
  92. package/src/server.js +0 -1
  93. package/src/setup/claude-code-config.js +17 -6
  94. package/src/setup/graph-runtime.js +20 -2
  95. package/src/system/runtime-settings.js +121 -2
  96. package/src/system/system-service.js +5 -1
  97. package/README.en.md +0 -5
  98. package/dist/web/assets/KnowledgeInspector-QvNpv0_k.js +0 -1
  99. package/dist/web/assets/KnowledgeOrganizerPage-DKq1jVD5.js +0 -1
  100. package/dist/web/assets/KnowledgePage-BWL0jT01.js +0 -1
  101. package/dist/web/assets/KnowledgeWorkspace-Czq6aZ7b.js +0 -4
  102. package/dist/web/assets/OverviewPage-5sJoO1Gh.js +0 -1
  103. package/dist/web/assets/PersonalProfilePage-CdmEzyvZ.css +0 -1
  104. package/dist/web/assets/PersonalProfilePage-tVChM9ik.js +0 -1
  105. package/dist/web/assets/PersonalProjectsPage-BPFrup4V.js +0 -1
  106. package/dist/web/assets/PublicProjectsPage-BPJSeREN.js +0 -1
  107. package/dist/web/assets/SettingsPage-C6ouU0sR.css +0 -1
  108. package/dist/web/assets/SettingsPage-Chd_B3eH.js +0 -1
  109. package/dist/web/assets/index-CB8Xmsc2.js +0 -2
  110. package/dist/web/assets/index-CZeuPh-U.css +0 -1
  111. package/dist/web/assets/paths-hwz-Ac1g.js +0 -1
  112. package/dist/web/assets/useMinimumLoadingDisplay-wCgGDmHg.js +0 -1
  113. package/dist/web/assets/vue-router-BsEp_XwI.js +0 -1
@@ -159,6 +159,12 @@ async def list_knowledge_review_candidates(
159
159
  review_id=run.review_id,
160
160
  routing_='r',
161
161
  )
162
+ deferred_rows, _, _ = await store.runtime.driver.execute_query(
163
+ _DEFERRED_ITEM_QUERY,
164
+ space_id=space['id'],
165
+ review_id=run.review_id,
166
+ routing_='r',
167
+ )
162
168
  candidates = build_review_candidates(
163
169
  [dict(row) for row in [*entity_rows, *relationship_rows]],
164
170
  scope=run.scope,
@@ -174,6 +180,12 @@ async def list_knowledge_review_candidates(
174
180
  decided_candidate_keys={
175
181
  row['candidate_key'] for row in decision_rows
176
182
  },
183
+ deferred_candidate_keys={
184
+ key
185
+ for row in deferred_rows
186
+ for key in row.get('candidate_keys', [])
187
+ if key
188
+ },
177
189
  )
178
190
  selected = candidates[:request.limit]
179
191
  return KnowledgeReviewCandidatePage(
@@ -272,7 +284,9 @@ def build_review_candidates(
272
284
  review_cutoff_at: datetime | None = None,
273
285
  conflict_item_keys: set[str],
274
286
  decided_candidate_keys: set[str],
287
+ deferred_candidate_keys: set[str] | None = None,
275
288
  ) -> list[KnowledgeReviewCandidate]:
289
+ deferred_candidate_keys = deferred_candidate_keys or set()
276
290
  prepared = []
277
291
  for row in rows:
278
292
  candidate_key = f"{row['item_kind']}:{row['item_id']}"
@@ -328,6 +342,7 @@ def build_review_candidates(
328
342
  normalized,
329
343
  previous_completed_at,
330
344
  candidate_key in conflict_item_keys,
345
+ candidate_key in deferred_candidate_keys,
331
346
  )
332
347
  if not reasons:
333
348
  continue
@@ -426,6 +441,7 @@ def _normalize_item_row(row: dict[str, Any]) -> dict[str, Any]:
426
441
  'preference_project_id': row.get('preference_project_id'),
427
442
  'project_ids': project_ids,
428
443
  'confirmation_status': row.get('confirmation_status') or 'pending',
444
+ 'current_quadrant': row.get('current_quadrant') or 'known_known',
429
445
  'utility_score': _float_or_default(row.get('utility_score'), 0),
430
446
  'confidence_score': _float_or_default(row.get('confidence_score'), 0.5),
431
447
  'qualified_use_count': int(row.get('qualified_use_count') or 0),
@@ -471,12 +487,15 @@ def _candidate_reasons(
471
487
  item: dict,
472
488
  previous_completed_at: datetime | None,
473
489
  has_conflict: bool,
490
+ was_deferred: bool,
474
491
  ) -> list[str]:
475
492
  reasons = []
476
493
  if previous_completed_at is None or (
477
494
  item['changed_at'] is not None and item['changed_at'] > previous_completed_at
478
495
  ):
479
496
  reasons.append('changed_since_last')
497
+ if was_deferred:
498
+ reasons.append('deferred_from_previous')
480
499
  if (
481
500
  has_conflict
482
501
  or item['requires_attention']
@@ -496,6 +515,7 @@ def _candidate_reasons(
496
515
  def _reason_priority(reason: str) -> int:
497
516
  return {
498
517
  'changed_since_last': 1,
518
+ 'deferred_from_previous': 1,
499
519
  'conflict_or_attention': 2,
500
520
  'low_weight': 3,
501
521
  'repeated_cross_session': 4,
@@ -539,6 +559,11 @@ RETURN item.uuid AS item_id,
539
559
  item.fuli_profile_aspect AS profile_aspect,
540
560
  item.fuli_preference_scope AS preference_scope,
541
561
  item.fuli_preference_project_id AS preference_project_id,
562
+ coalesce(
563
+ item.fuli_current_quadrant,
564
+ item.fuli_origin_quadrant,
565
+ 'known_known'
566
+ ) AS current_quadrant,
542
567
  collect(DISTINCT assignment.project_id) AS assigned_project_ids,
543
568
  collect(DISTINCT episode.fuli_personal_project_id) AS evidence_project_ids,
544
569
  collect(DISTINCT episode.fuli_session_id) AS session_ids,
@@ -576,6 +601,11 @@ RETURN item.uuid AS item_id,
576
601
  item.fuli_profile_aspect AS profile_aspect,
577
602
  item.fuli_preference_scope AS preference_scope,
578
603
  item.fuli_preference_project_id AS preference_project_id,
604
+ coalesce(
605
+ item.fuli_current_quadrant,
606
+ item.fuli_origin_quadrant,
607
+ 'known_known'
608
+ ) AS current_quadrant,
579
609
  collect(DISTINCT assignment.project_id) AS assigned_project_ids,
580
610
  collect(DISTINCT episode.fuli_personal_project_id) AS evidence_project_ids,
581
611
  collect(DISTINCT episode.fuli_session_id) AS session_ids,
@@ -605,8 +635,23 @@ WITH space, collect(DISTINCT 'entity:' + knowledge.item_id) +
605
635
  AS knowledge_keys
606
636
  OPTIONAL MATCH (space)-[:HAS_PREFERENCE_CONFLICT]->
607
637
  (preference:FuliPreferenceConflict {status: 'ai_pending'})
608
- RETURN knowledge_keys +
609
- collect(DISTINCT preference.left_item_kind + ':' + preference.left_item_id) +
610
- collect(DISTINCT preference.right_item_kind + ':' + preference.right_item_id)
611
- AS candidate_keys
638
+ WITH knowledge_keys,
639
+ collect(DISTINCT preference.left_item_kind + ':' + preference.left_item_id) +
640
+ collect(DISTINCT preference.right_item_kind + ':' + preference.right_item_id)
641
+ AS preference_keys
642
+ RETURN knowledge_keys + preference_keys AS candidate_keys
643
+ '''
644
+
645
+
646
+ _DEFERRED_ITEM_QUERY = '''
647
+ MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
648
+ [:HAS_KNOWLEDGE_REVIEW]->(past_run:FuliKnowledgeReviewRun)
649
+ WHERE past_run.id <> $review_id
650
+ MATCH (past_run)-[:HAS_REVIEW_DECISION]->
651
+ (decision:FuliKnowledgeReviewDecision)
652
+ WITH decision.candidate_key AS candidate_key, decision
653
+ ORDER BY decision.updated_at DESC, decision.id DESC
654
+ WITH candidate_key, collect(decision)[0] AS latest_decision
655
+ WHERE latest_decision.outcome = 'deferred'
656
+ RETURN collect(candidate_key) AS candidate_keys
612
657
  '''
@@ -5,6 +5,7 @@ from pydantic import Field, model_validator
5
5
 
6
6
  from .models import (
7
7
  ConfirmationStatus,
8
+ EpistemicQuadrant,
8
9
  KnowledgeFeedbackKind,
9
10
  KnowledgeItemKind,
10
11
  PersonalProfileAspect,
@@ -22,10 +23,11 @@ KnowledgeReviewScope = Literal[
22
23
  KnowledgeReviewStatus = Literal['active', 'paused', 'completed']
23
24
  KnowledgeReviewDisposition = Literal['paused', 'completed']
24
25
  KnowledgeReviewOutcome = Literal[
25
- 'confirmed', 'updated', 'invalidated', 'skipped', 'deferred'
26
+ 'confirmed', 'updated', 'invalidated', 'deferred', 'delegated_to_ai'
26
27
  ]
27
28
  KnowledgeReviewReason = Literal[
28
29
  'changed_since_last',
30
+ 'deferred_from_previous',
29
31
  'conflict_or_attention',
30
32
  'low_weight',
31
33
  'repeated_cross_session',
@@ -68,11 +70,17 @@ class KnowledgeReviewCandidate(StrictModel):
68
70
  item_kind: KnowledgeItemKind
69
71
  title: str
70
72
  content: str
73
+ # Keep the decision-critical ranking context near the front of the wire
74
+ # payload. Older MCP clients may project a bounded prefix of the Provider
75
+ # response, and the user must still see why a candidate was selected.
76
+ priority: int = Field(ge=1, le=4)
77
+ reasons: list[KnowledgeReviewReason]
78
+ confirmation_status: ConfirmationStatus
79
+ current_quadrant: EpistemicQuadrant
80
+ project_ids: list[str] = Field(default_factory=list)
71
81
  profile_aspect: PersonalProfileAspect | None = None
72
82
  preference_scope: PreferenceScope | None = None
73
83
  preference_project_id: str | None = None
74
- project_ids: list[str] = Field(default_factory=list)
75
- confirmation_status: ConfirmationStatus
76
84
  utility_score: float = Field(ge=0, le=1)
77
85
  confidence_score: float = Field(ge=0, le=1)
78
86
  qualified_use_count: int = Field(ge=0)
@@ -82,8 +90,6 @@ class KnowledgeReviewCandidate(StrictModel):
82
90
  last_feedback_kind: KnowledgeFeedbackKind | None = None
83
91
  distinct_session_count: int = Field(ge=0)
84
92
  changed_at: datetime | None = None
85
- priority: int = Field(ge=1, le=4)
86
- reasons: list[KnowledgeReviewReason]
87
93
 
88
94
 
89
95
  class KnowledgeReviewCandidateRequest(StrictModel):
@@ -10,6 +10,7 @@ from .provider_values import (
10
10
  json_object,
11
11
  native_datetime as _native_datetime,
12
12
  normalized_text as _normalized,
13
+ preference_qualifiers,
13
14
  )
14
15
  from .search_projection import (
15
16
  effective_confirmation_status,
@@ -255,6 +256,8 @@ async def personal_project_scopes(
255
256
  WHERE all(
256
257
  relation IN relationships(path)
257
258
  WHERE relation.relation_type IN ['PART_OF', 'USES_KNOWLEDGE_FROM']
259
+ AND relation.status = 'active'
260
+ AND relation.confirmation_authority = 'human_review'
258
261
  )
259
262
  AND EXISTS {
260
263
  MATCH (space)-[:CONTAINS_PROJECT]->(source)
@@ -634,6 +637,14 @@ def _dedupe_ranked_entities(ranked, active_project_id):
634
637
 
635
638
  def _entity_search_result(record, score: float, space_id: str) -> EntitySearchResult:
636
639
  value = dict(record)
640
+ attributes = json_object(value.get('attributes_json'))
641
+ value['preference_key'] = (
642
+ attributes.get('preferenceKey')
643
+ or attributes.get('preference_key')
644
+ or value.get('key')
645
+ or value.get('id')
646
+ )
647
+ value['preference_qualifiers'] = preference_qualifiers(attributes)
637
648
  basis = json_object(value.pop('confirmation_basis_json', None))
638
649
  source_references = value.pop('source_references', [])
639
650
  created_at = _native_datetime(value.pop('created_at', None))
@@ -163,8 +163,6 @@ def _validate_inheritance(
163
163
  raise ValueError(
164
164
  'inherited project ids are only valid for selected_projects inheritance'
165
165
  )
166
- if profile_aspect and mode != 'local_only':
167
- raise ValueError('personal preferences cannot inherit across projects')
168
166
 
169
167
 
170
168
  class ConfirmationActor(StrictModel):
@@ -772,6 +770,8 @@ class EntitySearchResult(StrictModel):
772
770
  preference_scope: PreferenceScope | None = None
773
771
  preference_project_id: str | None = None
774
772
  key: str | None = None
773
+ preference_key: str | None = None
774
+ preference_qualifiers: dict[str, Any] = Field(default_factory=dict)
775
775
  defined_project_id: str | None = None
776
776
  inheritance_mode: KnowledgeInheritanceMode = 'local_only'
777
777
  inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
@@ -819,6 +819,8 @@ class FactResult(StrictModel):
819
819
  preference_scope: PreferenceScope | None = None
820
820
  preference_project_id: str | None = None
821
821
  key: str | None = None
822
+ preference_key: str | None = None
823
+ preference_qualifiers: dict[str, Any] = Field(default_factory=dict)
822
824
  defined_project_id: str | None = None
823
825
  inheritance_mode: KnowledgeInheritanceMode = 'local_only'
824
826
  inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
@@ -860,9 +862,18 @@ class CollaborationPreferenceItem(StrictModel):
860
862
  preference_scope: PreferenceScope
861
863
  preference_project_id: str | None = None
862
864
  attributes: dict[str, Any] = Field(default_factory=dict)
865
+ weight: float | None = Field(default=None, ge=0, le=1, allow_inf_nan=False)
866
+ reason: str | None = None
867
+ confirmation_basis: ConfirmationBasis
868
+ reasoning_summary: str | None = None
869
+ inheritance_mode: KnowledgeInheritanceMode = 'local_only'
870
+ inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
863
871
  confirmation_status: Literal['confirmed', 'agent_confirmed']
864
872
  confirmed_at: datetime
865
873
  created_at: datetime | None = None
874
+ scope_distance: int = Field(default=0, ge=0, le=2)
875
+ inherited_from_project_id: str | None = None
876
+ scope_path: list[str] = Field(default_factory=list, max_length=3)
866
877
 
867
878
 
868
879
  class CollaborationPreferenceConflict(StrictModel):
@@ -880,6 +891,8 @@ class CollaborationContextResult(StrictModel):
880
891
  effective_preferences: list[CollaborationPreferenceItem] = Field(default_factory=list)
881
892
  conflicts: list[CollaborationPreferenceConflict] = Field(default_factory=list)
882
893
  overridden_global_ids: list[str] = Field(default_factory=list)
894
+ overridden_inherited_ids: list[str] = Field(default_factory=list)
895
+ overridden_lower_authority_ids: list[str] = Field(default_factory=list)
883
896
  truncated: bool = False
884
897
 
885
898
 
@@ -0,0 +1,234 @@
1
+ from datetime import datetime
2
+ from typing import Any, Literal
3
+
4
+ from pydantic import Field, model_validator
5
+
6
+ from .models import (
7
+ ConfirmationBasis,
8
+ ConfirmationStatus,
9
+ KnowledgeItemKind,
10
+ PersonalProfileAspect,
11
+ StrictModel,
12
+ )
13
+
14
+ PersonalGlobalPreferenceTargetScope = Literal[
15
+ 'parent_project',
16
+ 'personal_global',
17
+ ]
18
+
19
+
20
+ class PersonalGlobalPreferenceSourceRef(StrictModel):
21
+ item_id: str = Field(min_length=1, max_length=256)
22
+ item_kind: KnowledgeItemKind
23
+ project_id: str = Field(min_length=1, max_length=128)
24
+
25
+
26
+ class PersonalGlobalPreferenceSourceSnapshot(
27
+ PersonalGlobalPreferenceSourceRef
28
+ ):
29
+ key: str = Field(min_length=1, max_length=256)
30
+ preference_key: str = Field(min_length=1, max_length=512)
31
+ preference_qualifiers: dict[str, Any] = Field(default_factory=dict)
32
+ title: str = Field(min_length=1, max_length=512)
33
+ instruction: str = Field(max_length=8192)
34
+ profile_aspect: PersonalProfileAspect
35
+ confirmation_status: Literal['confirmed', 'agent_confirmed']
36
+ confirmation_basis: ConfirmationBasis
37
+ human_change_version: int = Field(default=0, ge=0)
38
+ usage_generation: int = Field(default=1, ge=1)
39
+ last_human_changed_at: datetime | None = None
40
+ negative_evidence_count: int = Field(default=0, ge=0)
41
+ requires_attention: bool = False
42
+ last_feedback_at: datetime | None = None
43
+ source_uris: list[str] = Field(default_factory=list, max_length=20)
44
+ stored_confirmation_basis_json: str = Field(
45
+ default='{}',
46
+ exclude=True,
47
+ repr=False,
48
+ )
49
+ stored_attributes_json: str = Field(default='{}', exclude=True, repr=False)
50
+
51
+
52
+ class PersonalGlobalPreferenceEligibleTargetScope(StrictModel):
53
+ target_scope: PersonalGlobalPreferenceTargetScope
54
+ target_project_id: str | None = Field(
55
+ default=None,
56
+ min_length=1,
57
+ max_length=128,
58
+ )
59
+ max_distance: int | None = Field(default=None, ge=1, le=2)
60
+
61
+ @model_validator(mode='after')
62
+ def validate_target(self):
63
+ if self.target_scope == 'parent_project' and not self.target_project_id:
64
+ raise ValueError('parent_project target requires target_project_id')
65
+ if self.target_scope == 'personal_global' and self.target_project_id:
66
+ raise ValueError('personal_global target cannot set target_project_id')
67
+ return self
68
+
69
+
70
+ class PersonalGlobalPreferenceScopeOptionsRequest(StrictModel):
71
+ personal_space_id: str = Field(min_length=1, max_length=128)
72
+ source_items: list[PersonalGlobalPreferenceSourceRef] = Field(
73
+ min_length=2,
74
+ max_length=32,
75
+ )
76
+ preference_key: str = Field(min_length=1, max_length=512)
77
+
78
+
79
+ class PersonalGlobalPreferenceScopeOptions(StrictModel):
80
+ personal_space_id: str
81
+ candidate_id: str
82
+ candidate_version: str
83
+ preference_key: str
84
+ source_snapshots: list[PersonalGlobalPreferenceSourceSnapshot]
85
+ eligible_target_scopes: list[PersonalGlobalPreferenceEligibleTargetScope]
86
+
87
+
88
+ class PersonalGlobalPreferenceDecisionIntent(StrictModel):
89
+ personal_space_id: str = Field(min_length=1, max_length=128)
90
+ candidate_version: str = Field(pattern=r'^v1:[a-f0-9]{24}$')
91
+ decision_revision: int = Field(ge=0)
92
+ source_items: list[PersonalGlobalPreferenceSourceRef] = Field(
93
+ min_length=2,
94
+ max_length=32,
95
+ )
96
+ preference_key: str = Field(min_length=1, max_length=512)
97
+ target_scope: PersonalGlobalPreferenceTargetScope
98
+ target_project_id: str | None = Field(
99
+ min_length=1,
100
+ max_length=128,
101
+ )
102
+ decision: Literal['approve', 'reject']
103
+ global_title: str | None = Field(default=None, min_length=1, max_length=512)
104
+ global_instruction: str | None = Field(
105
+ default=None,
106
+ min_length=1,
107
+ max_length=8192,
108
+ )
109
+ profile_aspect: PersonalProfileAspect | None = None
110
+ human_confirmation_reason: str = Field(min_length=1, max_length=4096)
111
+ confirmed_at: datetime
112
+ session_id: str = Field(min_length=1, max_length=256)
113
+ idempotency_key: str = Field(min_length=8, max_length=256)
114
+
115
+ @model_validator(mode='after')
116
+ def validate_decision(self):
117
+ item_keys = {
118
+ (item.item_kind, item.item_id)
119
+ for item in self.source_items
120
+ }
121
+ project_ids = {item.project_id for item in self.source_items}
122
+ if len(item_keys) != len(self.source_items):
123
+ raise ValueError('source preference items must be unique')
124
+ if len(project_ids) != len(self.source_items):
125
+ raise ValueError(
126
+ 'source preferences must come from distinct projects'
127
+ )
128
+ if self.target_scope == 'parent_project' and not self.target_project_id:
129
+ raise ValueError('parent_project target requires target_project_id')
130
+ if self.target_scope == 'personal_global' and self.target_project_id:
131
+ raise ValueError('personal_global target cannot set target_project_id')
132
+ if self.decision == 'approve':
133
+ if not (
134
+ self.global_title
135
+ and self.global_instruction
136
+ and self.profile_aspect
137
+ ):
138
+ raise ValueError(
139
+ 'approve requires a complete global preference assertion'
140
+ )
141
+ elif any((
142
+ self.global_title,
143
+ self.global_instruction,
144
+ self.profile_aspect,
145
+ )):
146
+ raise ValueError(
147
+ 'reject cannot include a global preference assertion'
148
+ )
149
+ return self
150
+
151
+
152
+ class PersonalGlobalPreferenceDecisionApply(
153
+ PersonalGlobalPreferenceDecisionIntent
154
+ ):
155
+ approval_token: str = Field(min_length=32, max_length=512)
156
+
157
+
158
+ class PersonalGlobalPreferenceDecisionInspection(StrictModel):
159
+ status: Literal['human_review_required']
160
+ personal_space_id: str
161
+ candidate_id: str
162
+ candidate_version: str
163
+ decision_revision: int = Field(ge=0)
164
+ decision: Literal['approve', 'reject']
165
+ preference_key: str
166
+ target_scope: PersonalGlobalPreferenceTargetScope
167
+ target_project_id: str | None = None
168
+ eligible_target_scopes: list[PersonalGlobalPreferenceEligibleTargetScope]
169
+ payload_fingerprint: str
170
+ source_snapshots: list[PersonalGlobalPreferenceSourceSnapshot]
171
+ candidate_binding_verified: bool = True
172
+ original_sources_will_remain_unchanged: bool = True
173
+ scope_apply_performed: bool = False
174
+ approval_token_issued: bool = False
175
+ required_action: str
176
+
177
+
178
+ class PersonalGlobalPreferenceDecisionPreview(StrictModel):
179
+ preview_id: str
180
+ candidate_id: str
181
+ candidate_version: str
182
+ decision_revision: int = Field(ge=0)
183
+ target_scope: PersonalGlobalPreferenceTargetScope
184
+ target_project_id: str | None = None
185
+ eligible_target_scopes: list[PersonalGlobalPreferenceEligibleTargetScope]
186
+ payload_fingerprint: str
187
+ approval_token: str
188
+ expires_at: datetime
189
+ source_snapshots: list[PersonalGlobalPreferenceSourceSnapshot]
190
+ candidate_binding_verified: bool = True
191
+ original_sources_will_remain_unchanged: bool = True
192
+ scope_apply_performed: bool = False
193
+
194
+
195
+ class PersonalGlobalPreferenceCandidateVersionRef(StrictModel):
196
+ candidate_id: str = Field(
197
+ pattern=r'^personal-global-[a-f0-9]{20}$',
198
+ )
199
+ candidate_version: str = Field(pattern=r'^v1:[a-f0-9]{24}$')
200
+
201
+
202
+ class PersonalGlobalPreferenceDecisionStatusRequest(StrictModel):
203
+ personal_space_id: str = Field(min_length=1, max_length=128)
204
+ candidates: list[PersonalGlobalPreferenceCandidateVersionRef] = Field(
205
+ min_length=1,
206
+ max_length=100,
207
+ )
208
+
209
+
210
+ class PersonalGlobalPreferenceDecisionRecord(StrictModel):
211
+ decision_event_id: str
212
+ candidate_id: str
213
+ candidate_version: str
214
+ decision_revision: int = Field(ge=1)
215
+ decision: Literal['approved', 'rejected']
216
+ target_scope: PersonalGlobalPreferenceTargetScope
217
+ target_project_id: str | None = None
218
+ global_assertion_id: str | None = None
219
+ global_assertion_active: bool
220
+ decision_sequence: int = Field(ge=1)
221
+ decided_at: datetime
222
+ human_confirmation_reason: str
223
+
224
+
225
+ class PersonalGlobalPreferenceDecisionRevision(StrictModel):
226
+ candidate_id: str
227
+ decision_revision: int = Field(ge=0)
228
+ current_candidate_version: str | None = None
229
+
230
+
231
+ class PersonalGlobalPreferenceDecisionStatusResult(StrictModel):
232
+ personal_space_id: str
233
+ decisions: list[PersonalGlobalPreferenceDecisionRecord]
234
+ revisions: list[PersonalGlobalPreferenceDecisionRevision]