fuli-context 0.2.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 (220) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +178 -0
  3. package/compose.graphiti.yml +86 -0
  4. package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
  5. package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
  6. package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
  7. package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
  8. package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
  9. package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
  10. package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
  11. package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
  12. package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
  13. package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
  14. package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
  15. package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
  16. package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
  17. package/dist/web/assets/console-tbvTUi6p.js +1 -0
  18. package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
  19. package/dist/web/assets/identity-B3s5CXhv.js +1 -0
  20. package/dist/web/assets/index-Devf7glZ.css +1 -0
  21. package/dist/web/assets/index-DwUDhQKc.js +6 -0
  22. package/dist/web/assets/model-DVT8sjy9.js +1 -0
  23. package/dist/web/assets/paths-B790iniO.js +1 -0
  24. package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
  25. package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
  26. package/dist/web/index.html +22 -0
  27. package/graph-provider/Dockerfile +16 -0
  28. package/graph-provider/fuli_graph/__init__.py +5 -0
  29. package/graph-provider/fuli_graph/app.py +547 -0
  30. package/graph-provider/fuli_graph/auth.py +17 -0
  31. package/graph-provider/fuli_graph/collaboration_context.py +290 -0
  32. package/graph-provider/fuli_graph/config.py +34 -0
  33. package/graph-provider/fuli_graph/graph_models.py +123 -0
  34. package/graph-provider/fuli_graph/graph_projection.py +405 -0
  35. package/graph-provider/fuli_graph/graph_query.py +561 -0
  36. package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
  37. package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
  38. package/graph-provider/fuli_graph/knowledge_management.py +816 -0
  39. package/graph-provider/fuli_graph/knowledge_records.py +65 -0
  40. package/graph-provider/fuli_graph/knowledge_search.py +723 -0
  41. package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
  42. package/graph-provider/fuli_graph/model_validation.py +58 -0
  43. package/graph-provider/fuli_graph/models.py +1179 -0
  44. package/graph-provider/fuli_graph/personal_project_access.py +29 -0
  45. package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
  46. package/graph-provider/fuli_graph/project_action_models.py +142 -0
  47. package/graph-provider/fuli_graph/project_knowledge.py +707 -0
  48. package/graph-provider/fuli_graph/project_management.py +251 -0
  49. package/graph-provider/fuli_graph/provider_values.py +40 -0
  50. package/graph-provider/fuli_graph/runtime.py +350 -0
  51. package/graph-provider/fuli_graph/search_projection.py +156 -0
  52. package/graph-provider/fuli_graph/store.py +24 -0
  53. package/graph-provider/fuli_graph/store_identity.py +237 -0
  54. package/graph-provider/fuli_graph/store_knowledge.py +447 -0
  55. package/graph-provider/fuli_graph/store_projects.py +242 -0
  56. package/graph-provider/fuli_graph/store_publication.py +370 -0
  57. package/graph-provider/fuli_graph/store_records.py +134 -0
  58. package/graph-provider/pyproject.toml +35 -0
  59. package/npm-shrinkwrap.json +6045 -0
  60. package/package.json +99 -0
  61. package/skills/capturing-session-knowledge/SKILL.md +226 -0
  62. package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
  63. package/skills/grilling-project/SKILL.md +129 -0
  64. package/skills/grilling-project/agents/openai.yaml +4 -0
  65. package/skills/grilling-project/references/project-profile.md +105 -0
  66. package/src/agent/candidate-read-service.js +53 -0
  67. package/src/agent/context-pack-service.js +83 -0
  68. package/src/agent/public-read-policy.js +39 -0
  69. package/src/agent/public-read-service.js +69 -0
  70. package/src/agent/read-projection.js +107 -0
  71. package/src/agent/search-service.js +61 -0
  72. package/src/agent/space-boundary.js +33 -0
  73. package/src/agent-tools/graph-definitions.js +479 -0
  74. package/src/agent-tools/graph-handlers.js +96 -0
  75. package/src/agent-tools/schema.js +37 -0
  76. package/src/agent-tools.js +16 -0
  77. package/src/app/application-error.js +13 -0
  78. package/src/app/bootstrap.js +8 -0
  79. package/src/app/create-application.js +143 -0
  80. package/src/app/state-service.js +38 -0
  81. package/src/candidates.js +78 -0
  82. package/src/classifier.js +63 -0
  83. package/src/cli/command-arguments.js +14 -0
  84. package/src/cli/command-registry.js +78 -0
  85. package/src/cli/commands/candidates.js +17 -0
  86. package/src/cli/commands/ingestion.js +32 -0
  87. package/src/cli/commands/reads.js +50 -0
  88. package/src/cli/commands/spaces.js +20 -0
  89. package/src/cli/invocation.js +47 -0
  90. package/src/cli/local-runtime-command.js +94 -0
  91. package/src/cli/local-runtime-options.js +64 -0
  92. package/src/cli/migrate-command.js +71 -0
  93. package/src/cli/migration-destination.js +117 -0
  94. package/src/cli/setup-agent-selection.js +195 -0
  95. package/src/cli/setup-command.js +85 -0
  96. package/src/cli/uninstall-command.js +67 -0
  97. package/src/cli.js +90 -0
  98. package/src/context-pack.js +206 -0
  99. package/src/defaults.js +1 -0
  100. package/src/extractor.js +77 -0
  101. package/src/fact-history.js +39 -0
  102. package/src/facts/fact-service.js +89 -0
  103. package/src/graphiti/agent-access-policy.js +88 -0
  104. package/src/graphiti/capture-policy.js +84 -0
  105. package/src/graphiti/federated-application.js +1292 -0
  106. package/src/graphiti/personal-context.js +29 -0
  107. package/src/graphiti/project-path-context.js +159 -0
  108. package/src/graphiti/provider-client.js +288 -0
  109. package/src/graphiti/runtime-config.js +85 -0
  110. package/src/graphiti/setup.js +43 -0
  111. package/src/graphiti/source-marker.js +142 -0
  112. package/src/http/api-router.js +133 -0
  113. package/src/http/bounded-json-array.js +19 -0
  114. package/src/http/error-mapping.js +36 -0
  115. package/src/http/graph-api-router.js +257 -0
  116. package/src/http/lens-route.js +50 -0
  117. package/src/http/request-policy.js +41 -0
  118. package/src/http/response.js +29 -0
  119. package/src/http/static-handler.js +55 -0
  120. package/src/ingestion.js +161 -0
  121. package/src/lens/lens-correction-service.js +81 -0
  122. package/src/lens/lens-fact-writer.js +85 -0
  123. package/src/lens/lens-input.js +62 -0
  124. package/src/lens/lens-query-projection.js +87 -0
  125. package/src/lens/lens-query-ranking.js +62 -0
  126. package/src/lens/lens-query.js +143 -0
  127. package/src/lens/lens-resource-projection.js +93 -0
  128. package/src/lens/lens-resource-selection.js +45 -0
  129. package/src/lens/lens-resource-service.js +103 -0
  130. package/src/lens/lens-search-query.js +122 -0
  131. package/src/lens/lens-service.js +183 -0
  132. package/src/local-runtime/lifecycle.js +254 -0
  133. package/src/mcp/create-mcp-server.js +85 -0
  134. package/src/mcp/instructions.js +10 -0
  135. package/src/mcp/project-action-preview-tokens.js +72 -0
  136. package/src/mcp/runtime.js +71 -0
  137. package/src/mcp/tool-annotations.js +63 -0
  138. package/src/mcp/tool-result.js +167 -0
  139. package/src/mcp/tool-schema.js +60 -0
  140. package/src/mcp-server.js +76 -0
  141. package/src/models.js +44 -0
  142. package/src/observer.js +83 -0
  143. package/src/package-metadata.js +6 -0
  144. package/src/project-rules.js +33 -0
  145. package/src/publication/canonical-json.js +39 -0
  146. package/src/publication/outbox-service.js +46 -0
  147. package/src/publication/publication-envelope-schema.js +75 -0
  148. package/src/publication/publication-envelope.js +37 -0
  149. package/src/publication/publication-policy.js +82 -0
  150. package/src/publication/publication-service.js +48 -0
  151. package/src/router.js +58 -0
  152. package/src/runtime-options.js +139 -0
  153. package/src/security/episode-visibility.js +14 -0
  154. package/src/security/fact-visibility.js +15 -0
  155. package/src/security/sensitive-content.js +18 -0
  156. package/src/security/source-metadata-policy.js +8 -0
  157. package/src/server/application-lifecycle.js +34 -0
  158. package/src/server/listen.js +24 -0
  159. package/src/server.js +135 -0
  160. package/src/setup/agent-installation-status.js +97 -0
  161. package/src/setup/agents.js +123 -0
  162. package/src/setup/codex-bootstrap.js +118 -0
  163. package/src/setup/codex-config.js +122 -0
  164. package/src/setup/config-backup.js +22 -0
  165. package/src/setup/container-runtime.js +388 -0
  166. package/src/setup/cursor-config.js +61 -0
  167. package/src/setup/graph-runtime.js +360 -0
  168. package/src/setup/node-runtime.js +24 -0
  169. package/src/setup/options.js +91 -0
  170. package/src/setup/paths.js +47 -0
  171. package/src/setup/session-skill.js +91 -0
  172. package/src/setup/setup.js +85 -0
  173. package/src/setup/uninstall.js +90 -0
  174. package/src/storage/episode-evidence-preview.js +100 -0
  175. package/src/storage/fact-search-order.js +18 -0
  176. package/src/storage/file/episode-evidence.js +37 -0
  177. package/src/storage/file/episode-index.js +59 -0
  178. package/src/storage/file/fact-search.js +60 -0
  179. package/src/storage/file-store.js +437 -0
  180. package/src/storage/import-json.js +70 -0
  181. package/src/storage/json-file.js +41 -0
  182. package/src/storage/legacy-record-validation.js +152 -0
  183. package/src/storage/legacy-snapshot-validation.js +32 -0
  184. package/src/storage/migrate.js +128 -0
  185. package/src/storage/migrations/001-initial.sql +83 -0
  186. package/src/storage/migrations/002-storage-integrity.sql +113 -0
  187. package/src/storage/record-validation.js +46 -0
  188. package/src/storage/snapshot-defaults.js +54 -0
  189. package/src/storage/sqlite/candidates-repository.js +84 -0
  190. package/src/storage/sqlite/episode-evidence-repository.js +154 -0
  191. package/src/storage/sqlite/episode-privacy-sql.js +33 -0
  192. package/src/storage/sqlite/episodes-repository.js +52 -0
  193. package/src/storage/sqlite/fact-search-repository.js +162 -0
  194. package/src/storage/sqlite/facts-repository.js +146 -0
  195. package/src/storage/sqlite/imports-repository.js +39 -0
  196. package/src/storage/sqlite/mapper.js +84 -0
  197. package/src/storage/sqlite/open-database.js +15 -0
  198. package/src/storage/sqlite/outbox-repository.js +103 -0
  199. package/src/storage/sqlite/snapshot-repository.js +69 -0
  200. package/src/storage/sqlite/spaces-repository.js +52 -0
  201. package/src/storage/sqlite/subscriptions-repository.js +54 -0
  202. package/src/storage/sqlite-store.js +159 -0
  203. package/src/storage/stable-json.js +24 -0
  204. package/src/storage/store-port.js +20 -0
  205. package/src/store.js +2 -0
  206. package/src/workspace-protocol/array-validation.js +19 -0
  207. package/src/workspace-protocol/base64url-contract.js +27 -0
  208. package/src/workspace-protocol/constants.js +37 -0
  209. package/src/workspace-protocol/error-contract.js +24 -0
  210. package/src/workspace-protocol/fact-contract.js +110 -0
  211. package/src/workspace-protocol/governance-contract.js +47 -0
  212. package/src/workspace-protocol/index.js +53 -0
  213. package/src/workspace-protocol/json-budget.js +19 -0
  214. package/src/workspace-protocol/manifest-contract.js +186 -0
  215. package/src/workspace-protocol/publication-contract.js +212 -0
  216. package/src/workspace-protocol/query-contract.js +149 -0
  217. package/src/workspace-protocol/scalar-contract.js +192 -0
  218. package/src/workspace-protocol/sync-contract.js +184 -0
  219. package/src/workspace-protocol/text-contract.js +39 -0
  220. package/src/workspace-protocol/validation.js +66 -0
@@ -0,0 +1,1179 @@
1
+ from datetime import datetime
2
+ from typing import Any, Literal
3
+
4
+ from pydantic import (
5
+ AliasChoices,
6
+ BaseModel,
7
+ ConfigDict,
8
+ Field,
9
+ field_validator,
10
+ model_validator,
11
+ )
12
+
13
+ from .model_validation import (
14
+ complete_epistemic_state,
15
+ normalize_provider_url,
16
+ reject_credentials,
17
+ require_public_eligible_episode,
18
+ )
19
+
20
+ Role = Literal['reader', 'contributor', 'maintainer']
21
+ SpaceKind = Literal['personal', 'project']
22
+ SpaceVisibility = Literal['private', 'public']
23
+ Sensitivity = Literal['normal', 'private', 'restricted']
24
+ ProjectLifecycle = Literal['planned', 'active', 'maintenance', 'archived']
25
+ ProjectSourceKind = Literal[
26
+ 'prd',
27
+ 'product_document',
28
+ 'technical_document',
29
+ 'frontend_repository',
30
+ 'backend_repository',
31
+ 'repository',
32
+ 'design',
33
+ 'runbook',
34
+ 'monitoring',
35
+ 'issue_tracker',
36
+ 'other',
37
+ ]
38
+ AssessmentState = Literal['confirmed', 'inferred']
39
+ AssessmentLabel = Literal[
40
+ 'needs_clarification',
41
+ 'partially_documented',
42
+ 'well_documented',
43
+ ]
44
+ ProjectRelationType = Literal[
45
+ 'PART_OF',
46
+ 'USES_KNOWLEDGE_FROM',
47
+ 'DEPENDS_ON',
48
+ 'PROVIDES_TO',
49
+ 'SHARES_CAPABILITY_WITH',
50
+ 'SUCCESSOR_OF',
51
+ 'RELATED_TO',
52
+ ]
53
+ ProjectRelationStatus = Literal['pending', 'active', 'rejected']
54
+ KnowledgeItemKind = Literal['entity', 'relationship']
55
+ KnowledgeOperationActor = Literal['human', 'agent']
56
+ HumanChangeStatus = Literal['none', 'unseen', 'viewed', 'reviewed']
57
+ KnowledgeAuditAction = Literal[
58
+ 'human_change',
59
+ 'agent_view',
60
+ 'agent_review',
61
+ 'knowledge_used',
62
+ ]
63
+ KnowledgeUseKind = Literal['cited', 'applied']
64
+ KnowledgeInheritanceMode = Literal[
65
+ 'local_only',
66
+ 'descendants',
67
+ 'selected_projects',
68
+ ]
69
+ KnowledgeRevisionAction = Literal[
70
+ 'confirm',
71
+ 'update',
72
+ 'invalidate',
73
+ 'link_replacement',
74
+ 'restore',
75
+ 'scope_change',
76
+ 'batch_confirm',
77
+ ]
78
+ KnowledgeContentRevisionAction = Literal[
79
+ 'confirm',
80
+ 'update',
81
+ 'invalidate',
82
+ 'link_replacement',
83
+ 'restore',
84
+ ]
85
+ KnowledgeConfirmationGroupKind = Literal['source', 'session']
86
+ EpistemicQuadrant = Literal[
87
+ 'known_known',
88
+ 'known_unknown',
89
+ 'unknown_known',
90
+ 'unknown_unknown',
91
+ ]
92
+ EpistemicStatus = Literal['confirmed', 'observed', 'exploratory']
93
+ ConfirmationStatus = Literal['confirmed', 'agent_confirmed', 'pending']
94
+ ConfirmationActorKind = Literal['user', 'agent', 'authoritative_source', 'import']
95
+ PersonalProfileAspect = Literal['taste', 'personality', 'judgment_preference']
96
+ PreferenceScope = Literal['global', 'project']
97
+ PreferenceConflictQueueStatus = Literal['ai_pending', 'resolved']
98
+ PreferenceConflictResolutionAction = Literal[
99
+ 'merge',
100
+ 'keep_left',
101
+ 'keep_right',
102
+ 'split_scope',
103
+ ]
104
+ SourceApplication = Literal['codex', 'claude_code', 'cursor', 'kiro', 'other']
105
+
106
+ class StrictModel(BaseModel):
107
+ model_config = ConfigDict(extra='forbid', str_strip_whitespace=True)
108
+
109
+
110
+ def _validate_inheritance(
111
+ mode: KnowledgeInheritanceMode,
112
+ project_ids: list[str],
113
+ profile_aspect: PersonalProfileAspect | None,
114
+ ) -> None:
115
+ if len(set(project_ids)) != len(project_ids):
116
+ raise ValueError('inherited project ids must be unique')
117
+ if any(not item or len(item) > 128 for item in project_ids):
118
+ raise ValueError('inherited project ids must contain 1 to 128 characters')
119
+ if mode == 'selected_projects' and not project_ids:
120
+ raise ValueError(
121
+ 'selected_projects inheritance requires at least one project id'
122
+ )
123
+ if mode != 'selected_projects' and project_ids:
124
+ raise ValueError(
125
+ 'inherited project ids are only valid for selected_projects inheritance'
126
+ )
127
+ if profile_aspect and mode != 'local_only':
128
+ raise ValueError('personal preferences cannot inherit across projects')
129
+
130
+
131
+ class ConfirmationActor(StrictModel):
132
+ kind: ConfirmationActorKind
133
+ label: str | None = Field(default=None, min_length=1, max_length=160)
134
+
135
+
136
+ class ConfirmationBasis(StrictModel):
137
+ existence_reason: str = Field(min_length=1, max_length=4096)
138
+ quadrant_reason: str = Field(min_length=1, max_length=4096)
139
+ proposed_by: ConfirmationActor
140
+ confirmed_by: ConfirmationActor | None = None
141
+ confirmed_at: datetime | None = None
142
+ agent_policy_version: str | None = Field(
143
+ default=None,
144
+ min_length=1,
145
+ max_length=128,
146
+ )
147
+
148
+ @model_validator(mode='after')
149
+ def validate_confirmation_actor(self):
150
+ if bool(self.confirmed_by) != bool(self.confirmed_at):
151
+ raise ValueError('confirmed_by and confirmed_at must be recorded together')
152
+ return self
153
+
154
+
155
+ def _validate_confirmation_state(
156
+ status: ConfirmationStatus,
157
+ basis: ConfirmationBasis,
158
+ ) -> None:
159
+ has_confirmation = bool(basis.confirmed_by and basis.confirmed_at)
160
+ if status == 'pending':
161
+ if has_confirmation or basis.agent_policy_version:
162
+ raise ValueError(
163
+ 'pending knowledge cannot retain a confirmer, confirmation time, '
164
+ 'or agent policy'
165
+ )
166
+ return
167
+ if not has_confirmation:
168
+ raise ValueError(
169
+ f'{status} knowledge requires a confirmer and confirmation time'
170
+ )
171
+ if status == 'confirmed':
172
+ if basis.confirmed_by.kind not in {'user', 'authoritative_source'}:
173
+ raise ValueError(
174
+ 'an agent or import cannot confirm human-confirmed knowledge; '
175
+ 'a user or authoritative source is required'
176
+ )
177
+ if basis.agent_policy_version:
178
+ raise ValueError(
179
+ 'human-confirmed knowledge cannot retain an agent policy version'
180
+ )
181
+ return
182
+ if basis.confirmed_by.kind != 'agent' or not basis.agent_policy_version:
183
+ raise ValueError(
184
+ 'agent-confirmed knowledge requires an agent confirmer and policy version'
185
+ )
186
+
187
+
188
+ def _legacy_epistemic_status(
189
+ status: ConfirmationStatus,
190
+ quadrant: EpistemicQuadrant,
191
+ ) -> EpistemicStatus:
192
+ if status == 'confirmed':
193
+ return 'confirmed'
194
+ return 'exploratory' if quadrant == 'unknown_unknown' else 'observed'
195
+
196
+
197
+ def _default_quadrant_reason(quadrant: EpistemicQuadrant) -> str:
198
+ return {
199
+ 'known_known': 'The item was explicitly expressed when it was captured.',
200
+ 'known_unknown': 'The item was captured as an explicit unresolved question.',
201
+ 'unknown_known': 'The item was inferred from examples, behaviour, or reactions.',
202
+ 'unknown_unknown': 'The item surfaced during blind-spot or open exploration.',
203
+ }[quadrant]
204
+
205
+
206
+ class BootstrapRequest(StrictModel):
207
+ principal_name: str = Field(min_length=1, max_length=160)
208
+
209
+
210
+ class BootstrapResult(StrictModel):
211
+ principal_id: str
212
+ access_token: str
213
+
214
+
215
+ class PrincipalCreate(StrictModel):
216
+ name: str = Field(min_length=1, max_length=160)
217
+
218
+
219
+ class PrincipalResult(StrictModel):
220
+ principal_id: str
221
+ access_token: str
222
+
223
+
224
+ class ProjectSource(StrictModel):
225
+ key: str = Field(min_length=1, max_length=128)
226
+ kind: ProjectSourceKind
227
+ title: str = Field(min_length=1, max_length=512)
228
+ uri: str | None = Field(default=None, max_length=2048)
229
+ summary: str | None = Field(default=None, max_length=4096)
230
+ sensitivity: Sensitivity = 'normal'
231
+
232
+
233
+ class AssessmentDimension(StrictModel):
234
+ key: str = Field(min_length=1, max_length=128)
235
+ label: str = Field(min_length=1, max_length=256)
236
+ score: int = Field(ge=0, le=100)
237
+ state: AssessmentState
238
+ evidence: list[str] = Field(default_factory=list, max_length=32)
239
+
240
+ @model_validator(mode='before')
241
+ @classmethod
242
+ def accept_legacy_missing_fields(cls, value):
243
+ if not isinstance(value, dict):
244
+ return value
245
+ compatible = dict(value)
246
+ compatible.pop('missing', None)
247
+ compatible.pop('recommendation', None)
248
+ if compatible.get('state') == 'missing':
249
+ compatible['state'] = 'inferred'
250
+ return compatible
251
+
252
+
253
+ class ProjectAssessment(StrictModel):
254
+ score: int = Field(ge=0, le=100)
255
+ label: AssessmentLabel
256
+ confirmed: list[str] = Field(default_factory=list, max_length=64)
257
+ inferred: list[str] = Field(default_factory=list, max_length=64)
258
+ dimensions: list[AssessmentDimension] = Field(default_factory=list, max_length=16)
259
+ analyzed_at: datetime
260
+
261
+ @model_validator(mode='before')
262
+ @classmethod
263
+ def accept_legacy_missing_summary(cls, value):
264
+ if not isinstance(value, dict):
265
+ return value
266
+ compatible = dict(value)
267
+ compatible.pop('missing', None)
268
+ return compatible
269
+
270
+
271
+ class ProjectProfile(StrictModel):
272
+ name: str = Field(min_length=1, max_length=160)
273
+ purpose: str | None = Field(default=None, max_length=4096)
274
+ scope: str | None = Field(default=None, max_length=4096)
275
+ technical_summary: str | None = Field(default=None, max_length=4096)
276
+ lifecycle: ProjectLifecycle = 'planned'
277
+ sources: list[ProjectSource] = Field(default_factory=list, max_length=64)
278
+ boundaries: list[str] = Field(default_factory=list, max_length=64)
279
+ assessment: ProjectAssessment | None = None
280
+
281
+ @model_validator(mode='after')
282
+ def reject_credentials(self):
283
+ reject_credentials(self, 'Project profile')
284
+ return self
285
+
286
+
287
+ class ProjectReleaseCreate(StrictModel):
288
+ version: str = Field(
289
+ min_length=1,
290
+ max_length=64,
291
+ pattern=r'^[0-9A-Za-z][0-9A-Za-z._-]*$',
292
+ )
293
+ summary: str = Field(min_length=1, max_length=4096)
294
+
295
+
296
+ class ProjectReleaseRecord(StrictModel):
297
+ id: str
298
+ project_id: str
299
+ version: str
300
+ summary: str
301
+ publisher_id: str
302
+ publisher_name: str
303
+ published_at: datetime
304
+
305
+
306
+ class SpaceCreate(StrictModel):
307
+ name: str = Field(min_length=1, max_length=160)
308
+ kind: SpaceKind
309
+ description: str | None = Field(default=None, max_length=2000)
310
+ publication_key: str | None = Field(default=None, min_length=8, max_length=256)
311
+ profile: ProjectProfile | None = None
312
+ release: ProjectReleaseCreate | None = None
313
+
314
+
315
+ class SpaceRecord(StrictModel):
316
+ id: str
317
+ name: str
318
+ kind: SpaceKind
319
+ group_id: str
320
+ description: str | None
321
+ visibility: SpaceVisibility
322
+ owner_id: str
323
+ role: Role
324
+ publication_key: str | None = None
325
+ can_manage: bool = False
326
+ profile: ProjectProfile | None = None
327
+ current_release: ProjectReleaseRecord | None = None
328
+ created_at: datetime
329
+
330
+
331
+ class ProjectDeleteResult(StrictModel):
332
+ project_id: str
333
+ project_name: str
334
+ deleted: bool
335
+
336
+
337
+ class PersonalProjectUpsert(StrictModel):
338
+ personal_space_id: str = Field(min_length=1, max_length=128)
339
+ project_id: str = Field(min_length=1, max_length=128)
340
+ profile: ProjectProfile
341
+
342
+
343
+ class PersonalProjectRecord(StrictModel):
344
+ project_id: str
345
+ personal_space_id: str
346
+ publication_key: str
347
+ profile: ProjectProfile
348
+ created_at: datetime
349
+ updated_at: datetime
350
+
351
+
352
+ class ProjectRelationCreate(StrictModel):
353
+ target_project_id: str = Field(min_length=1, max_length=128)
354
+ relation_type: ProjectRelationType
355
+ note: str | None = Field(default=None, max_length=2000)
356
+
357
+
358
+ class ProjectRelationRecord(StrictModel):
359
+ id: str
360
+ source_project_id: str
361
+ target_project_id: str
362
+ relation_type: ProjectRelationType
363
+ status: ProjectRelationStatus
364
+ note: str | None
365
+ created_by: str
366
+ created_at: datetime
367
+ decided_by: str | None = None
368
+ decided_at: datetime | None = None
369
+
370
+
371
+ class ProjectRelationDecision(StrictModel):
372
+ decision: Literal['confirm', 'reject']
373
+ note: str | None = Field(default=None, max_length=2000)
374
+
375
+
376
+ class MembershipCreate(StrictModel):
377
+ principal_id: str = Field(min_length=1, max_length=128)
378
+ role: Role
379
+
380
+
381
+ class MembershipRecord(StrictModel):
382
+ project_id: str
383
+ principal_id: str
384
+ role: Role
385
+ created_at: datetime
386
+
387
+
388
+ class SubscriptionCreate(StrictModel):
389
+ personal_space_id: str = Field(min_length=1, max_length=128)
390
+ project_id: str = Field(min_length=1, max_length=128)
391
+ provider_url: str = Field(min_length=8, max_length=2048)
392
+ project_name: str = Field(min_length=1, max_length=160)
393
+
394
+ @field_validator('provider_url')
395
+ @classmethod
396
+ def validate_provider_url(cls, value: str) -> str:
397
+ return normalize_provider_url(value)
398
+
399
+
400
+ class SubscriptionRecord(StrictModel):
401
+ id: str
402
+ personal_space_id: str
403
+ project_id: str
404
+ provider_url: str
405
+ project_name: str
406
+ created_at: datetime
407
+
408
+
409
+ class SubscriptionDeleteResult(StrictModel):
410
+ project_id: str
411
+ deleted: bool
412
+
413
+
414
+ class EntityInput(StrictModel):
415
+ key: str = Field(min_length=1, max_length=256)
416
+ name: str = Field(min_length=1, max_length=512)
417
+ type: str = Field(min_length=1, max_length=64, pattern=r'^[A-Za-z][A-Za-z0-9_]*$')
418
+ summary: str = Field(default='', max_length=4096)
419
+ origin_quadrant: EpistemicQuadrant = 'known_known'
420
+ current_quadrant: EpistemicQuadrant | None = None
421
+ epistemic_status: EpistemicStatus = 'confirmed'
422
+ confirmation_status: ConfirmationStatus | None = None
423
+ confirmation_basis: ConfirmationBasis | None = None
424
+ reasoning_summary: str | None = Field(default=None, max_length=4096)
425
+ profile_aspect: PersonalProfileAspect | None = None
426
+ inheritance_mode: KnowledgeInheritanceMode = 'local_only'
427
+ inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
428
+ attributes: dict[str, Any] = Field(default_factory=dict)
429
+
430
+ @field_validator('attributes')
431
+ @classmethod
432
+ def bound_attributes(cls, value: dict[str, Any]) -> dict[str, Any]:
433
+ if len(value) > 32:
434
+ raise ValueError('an entity may contain at most 32 attributes')
435
+ return value
436
+
437
+ @model_validator(mode='after')
438
+ def validate_inheritance(self):
439
+ _validate_inheritance(
440
+ self.inheritance_mode,
441
+ self.inherited_project_ids,
442
+ self.profile_aspect,
443
+ )
444
+ return self
445
+
446
+ @model_validator(mode='after')
447
+ def complete_epistemic_state(self):
448
+ self.current_quadrant = complete_epistemic_state(
449
+ origin_quadrant=self.origin_quadrant,
450
+ current_quadrant=self.current_quadrant,
451
+ epistemic_status=self.epistemic_status,
452
+ reasoning_summary=self.reasoning_summary,
453
+ profile_aspect=self.profile_aspect,
454
+ )
455
+ return self
456
+
457
+
458
+ class RelationshipInput(StrictModel):
459
+ key: str = Field(min_length=1, max_length=256)
460
+ source: str = Field(min_length=1, max_length=256)
461
+ target: str = Field(min_length=1, max_length=256)
462
+ type: str = Field(min_length=1, max_length=64, pattern=r'^[A-Z][A-Z0-9_]*$')
463
+ fact: str = Field(min_length=1, max_length=8192)
464
+ valid_at: datetime | None = None
465
+ invalid_at: datetime | None = None
466
+ supersedes: list[str] = Field(default_factory=list, max_length=32)
467
+ confidence: float = Field(default=1, ge=0, le=1)
468
+ origin_quadrant: EpistemicQuadrant = 'known_known'
469
+ current_quadrant: EpistemicQuadrant | None = None
470
+ epistemic_status: EpistemicStatus = 'confirmed'
471
+ confirmation_status: ConfirmationStatus | None = None
472
+ confirmation_basis: ConfirmationBasis | None = None
473
+ reasoning_summary: str | None = Field(default=None, max_length=4096)
474
+ profile_aspect: PersonalProfileAspect | None = None
475
+ inheritance_mode: KnowledgeInheritanceMode = 'local_only'
476
+ inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
477
+ attributes: dict[str, Any] = Field(default_factory=dict)
478
+
479
+ @model_validator(mode='after')
480
+ def validate_interval(self):
481
+ if self.valid_at and self.invalid_at and self.invalid_at < self.valid_at:
482
+ raise ValueError('invalid_at must not be earlier than valid_at')
483
+ self.current_quadrant = complete_epistemic_state(
484
+ origin_quadrant=self.origin_quadrant,
485
+ current_quadrant=self.current_quadrant,
486
+ epistemic_status=self.epistemic_status,
487
+ reasoning_summary=self.reasoning_summary,
488
+ profile_aspect=self.profile_aspect,
489
+ )
490
+ _validate_inheritance(
491
+ self.inheritance_mode,
492
+ self.inherited_project_ids,
493
+ self.profile_aspect,
494
+ )
495
+ return self
496
+
497
+
498
+ class StructuredEpisode(StrictModel):
499
+ idempotency_key: str = Field(min_length=8, max_length=256)
500
+ session_id: str = Field(min_length=1, max_length=256)
501
+ name: str = Field(min_length=1, max_length=512)
502
+ source_kind: str = Field(min_length=1, max_length=128)
503
+ source_description: str = Field(min_length=1, max_length=1024)
504
+ source_application: SourceApplication | None = None
505
+ source_turn_id: str | None = Field(default=None, min_length=1, max_length=256)
506
+ source_excerpt: str | None = Field(default=None, min_length=1, max_length=2048)
507
+ reference_time: datetime
508
+ summary: str = Field(default='', max_length=8192)
509
+ sensitivity: Sensitivity = 'normal'
510
+ entities: list[EntityInput] = Field(min_length=1, max_length=128)
511
+ relationships: list[RelationshipInput] = Field(default_factory=list, max_length=256)
512
+
513
+ @model_validator(mode='after')
514
+ def complete_confirmation_state(self):
515
+ proposer = ConfirmationActor(
516
+ kind='agent' if self.source_application else 'import',
517
+ label=self.source_application or self.source_kind,
518
+ )
519
+ for item in [*self.entities, *self.relationships]:
520
+ if item.confirmation_basis is None:
521
+ item.confirmation_basis = ConfirmationBasis(
522
+ existence_reason=self.source_description,
523
+ quadrant_reason=(
524
+ item.reasoning_summary
525
+ or _default_quadrant_reason(item.origin_quadrant)
526
+ ),
527
+ proposed_by=proposer,
528
+ )
529
+ if item.confirmation_status is None:
530
+ # Legacy epistemic_status is evidence maturity, not an auditable
531
+ # confirmation. Old records must be reviewed instead of silently
532
+ # inheriting "confirmed".
533
+ item.confirmation_status = 'pending'
534
+ _validate_confirmation_state(
535
+ item.confirmation_status,
536
+ item.confirmation_basis,
537
+ )
538
+ item.epistemic_status = _legacy_epistemic_status(
539
+ item.confirmation_status,
540
+ item.origin_quadrant,
541
+ )
542
+ return self
543
+
544
+ @model_validator(mode="after")
545
+ def reject_credentials(self) -> "StructuredEpisode":
546
+ reject_credentials(self, 'Structured episode')
547
+ return self
548
+
549
+ @model_validator(mode='after')
550
+ def validate_graph_references(self):
551
+ entity_keys = [entity.key for entity in self.entities]
552
+ if len(entity_keys) != len(set(entity_keys)):
553
+ raise ValueError('entity keys must be unique within an episode')
554
+ relationship_keys = [relationship.key for relationship in self.relationships]
555
+ if len(relationship_keys) != len(set(relationship_keys)):
556
+ raise ValueError('relationship keys must be unique within an episode')
557
+ available = set(entity_keys)
558
+ superseded_keys: list[str] = []
559
+ for relationship in self.relationships:
560
+ if relationship.source not in available or relationship.target not in available:
561
+ raise ValueError('relationship source and target must reference episode entities')
562
+ if relationship.invalid_at and relationship.supersedes:
563
+ raise ValueError('an invalid relationship cannot replace existing knowledge')
564
+ superseded_keys.extend(relationship.supersedes)
565
+ if len(superseded_keys) != len(set(superseded_keys)):
566
+ raise ValueError(
567
+ 'one existing relationship cannot have multiple replacements in an episode'
568
+ )
569
+ return self
570
+
571
+
572
+ class PublicationDraftCreate(StrictModel):
573
+ personal_space_id: str = Field(min_length=1, max_length=128)
574
+ target_project_id: str = Field(min_length=1, max_length=128)
575
+ provider_url: str = Field(min_length=8, max_length=2048)
576
+ episode: StructuredEpisode
577
+
578
+ @field_validator('provider_url')
579
+ @classmethod
580
+ def validate_provider_url(cls, value: str) -> str:
581
+ return normalize_provider_url(value)
582
+
583
+ @model_validator(mode='after')
584
+ def require_public_eligible_episode(self):
585
+ require_public_eligible_episode(self.episode)
586
+ return self
587
+
588
+
589
+ class PublicationDraftRecord(StrictModel):
590
+ id: str
591
+ personal_space_id: str
592
+ target_project_id: str
593
+ provider_url: str
594
+ status: Literal['pending', 'submitted', 'kept_personal', 'ignored']
595
+ episode: StructuredEpisode
596
+ created_at: datetime
597
+ decided_at: datetime | None = None
598
+ shared_proposal_id: str | None = None
599
+
600
+
601
+ class PublicationDraftDecision(StrictModel):
602
+ decision: Literal['submit_public', 'keep_personal', 'ignore']
603
+ shared_proposal_id: str | None = Field(default=None, max_length=128)
604
+
605
+ @model_validator(mode='after')
606
+ def require_shared_proposal(self):
607
+ if self.decision == 'submit_public' and not self.shared_proposal_id:
608
+ raise ValueError('shared_proposal_id is required after public submission')
609
+ if self.decision != 'submit_public' and self.shared_proposal_id:
610
+ raise ValueError('shared_proposal_id is only valid for public submission')
611
+ return self
612
+
613
+ class KnowledgeCommit(StrictModel):
614
+ space_id: str = Field(min_length=1, max_length=128)
615
+ personal_project_id: str | None = Field(default=None, min_length=1, max_length=128)
616
+ episode: StructuredEpisode
617
+
618
+
619
+ class CommitResult(StrictModel):
620
+ status: Literal['committed', 'duplicate']
621
+ space_id: str
622
+ episode_id: str
623
+ entity_ids: list[str]
624
+ relationship_ids: list[str]
625
+
626
+
627
+ class ProposalCreate(StrictModel):
628
+ episode: StructuredEpisode
629
+
630
+ @model_validator(mode='after')
631
+ def require_public_eligible_episode(self):
632
+ require_public_eligible_episode(self.episode)
633
+ return self
634
+
635
+
636
+ class ProposalRecord(StrictModel):
637
+ id: str
638
+ project_id: str
639
+ submitted_by: str
640
+ status: Literal['pending', 'processing', 'approved', 'rejected']
641
+ episode: StructuredEpisode
642
+ created_at: datetime
643
+ decided_at: datetime | None = None
644
+ decided_by: str | None = None
645
+ decision_note: str | None = None
646
+
647
+
648
+ class ProposalDecision(StrictModel):
649
+ decision: Literal['approve', 'reject']
650
+ note: str | None = Field(default=None, max_length=2000)
651
+
652
+
653
+ class SearchRequest(StrictModel):
654
+ space_ids: list[str] = Field(min_length=1, max_length=32)
655
+ query: str = Field(min_length=1, max_length=2048)
656
+ limit: int = Field(default=12, ge=1, le=100)
657
+ include_historical: bool = False
658
+ include_pending: bool = Field(
659
+ default=False,
660
+ validation_alias=AliasChoices('include_pending', 'include_exploratory'),
661
+ )
662
+ personal_project_ids: list[str] = Field(default_factory=list, max_length=16)
663
+ active_personal_project_id: str | None = Field(
664
+ default=None,
665
+ min_length=1,
666
+ max_length=128,
667
+ )
668
+ inherit_project_knowledge: bool = True
669
+ include_personal_global: bool = False
670
+
671
+ @field_validator('personal_project_ids')
672
+ @classmethod
673
+ def validate_personal_project_ids(cls, value: list[str]) -> list[str]:
674
+ normalized = [item.strip() for item in value]
675
+ if any(not item or len(item) > 128 for item in normalized):
676
+ raise ValueError('personal project ids must contain 1 to 128 characters')
677
+ if len(set(normalized)) != len(normalized):
678
+ raise ValueError('personal project ids must be unique')
679
+ return normalized
680
+
681
+ @model_validator(mode='after')
682
+ def validate_active_personal_project(self):
683
+ if (
684
+ self.active_personal_project_id
685
+ and self.active_personal_project_id not in self.personal_project_ids
686
+ ):
687
+ raise ValueError(
688
+ 'active personal project id must be present in personal project ids'
689
+ )
690
+ return self
691
+
692
+
693
+ class EntitySearchResult(StrictModel):
694
+ id: str
695
+ space_id: str
696
+ group_id: str
697
+ name: str
698
+ type: str
699
+ summary: str
700
+ created_at: datetime | None = None
701
+ origin_quadrant: EpistemicQuadrant = 'known_known'
702
+ current_quadrant: EpistemicQuadrant = 'known_known'
703
+ epistemic_status: EpistemicStatus = 'confirmed'
704
+ confirmation_status: ConfirmationStatus = 'pending'
705
+ confirmation_basis: ConfirmationBasis | None = None
706
+ reasoning_summary: str | None = None
707
+ profile_aspect: PersonalProfileAspect | None = None
708
+ preference_scope: PreferenceScope | None = None
709
+ preference_project_id: str | None = None
710
+ key: str | None = None
711
+ defined_project_id: str | None = None
712
+ inheritance_mode: KnowledgeInheritanceMode = 'local_only'
713
+ inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
714
+ human_edited: bool = False
715
+ human_change_status: HumanChangeStatus = 'none'
716
+ human_change_version: int = Field(default=0, ge=0)
717
+ last_human_changed_at: datetime | None = None
718
+ last_agent_viewed_at: datetime | None = None
719
+ last_agent_reviewed_at: datetime | None = None
720
+ utility_score: float = Field(default=0, ge=0, le=1)
721
+ confidence_score: float = Field(default=0.5, ge=0, le=1)
722
+ qualified_use_count: int = Field(default=0, ge=0)
723
+ distinct_task_count: int = Field(default=0, ge=0)
724
+ last_used_at: datetime | None = None
725
+ scope_distance: int = Field(default=0, ge=0, le=8)
726
+ inherited_from_project_id: str | None = None
727
+ scope_path: list[str] = Field(default_factory=list, max_length=9)
728
+ score: float | None = None
729
+
730
+
731
+ class FactResult(StrictModel):
732
+ id: str
733
+ space_id: str
734
+ group_id: str
735
+ source_entity: str
736
+ target_entity: str
737
+ relationship: str
738
+ fact: str
739
+ valid_at: datetime | None
740
+ invalid_at: datetime | None
741
+ created_at: datetime
742
+ episodes: list[str]
743
+ origin_quadrant: EpistemicQuadrant = 'known_known'
744
+ current_quadrant: EpistemicQuadrant = 'known_known'
745
+ epistemic_status: EpistemicStatus = 'confirmed'
746
+ confirmation_status: ConfirmationStatus = 'pending'
747
+ confirmation_basis: ConfirmationBasis | None = None
748
+ reasoning_summary: str | None = None
749
+ profile_aspect: PersonalProfileAspect | None = None
750
+ preference_scope: PreferenceScope | None = None
751
+ preference_project_id: str | None = None
752
+ key: str | None = None
753
+ defined_project_id: str | None = None
754
+ inheritance_mode: KnowledgeInheritanceMode = 'local_only'
755
+ inherited_project_ids: list[str] = Field(default_factory=list, max_length=32)
756
+ human_edited: bool = False
757
+ human_change_status: HumanChangeStatus = 'none'
758
+ human_change_version: int = Field(default=0, ge=0)
759
+ last_human_changed_at: datetime | None = None
760
+ last_agent_viewed_at: datetime | None = None
761
+ last_agent_reviewed_at: datetime | None = None
762
+ utility_score: float = Field(default=0, ge=0, le=1)
763
+ confidence_score: float = Field(default=0.5, ge=0, le=1)
764
+ qualified_use_count: int = Field(default=0, ge=0)
765
+ distinct_task_count: int = Field(default=0, ge=0)
766
+ last_used_at: datetime | None = None
767
+ scope_distance: int = Field(default=0, ge=0, le=8)
768
+ inherited_from_project_id: str | None = None
769
+ scope_path: list[str] = Field(default_factory=list, max_length=9)
770
+ score: float | None = None
771
+
772
+
773
+ class SearchResult(StrictModel):
774
+ facts: list[FactResult]
775
+ entities: list[EntitySearchResult] = Field(default_factory=list)
776
+
777
+
778
+ class CollaborationPreferenceItem(StrictModel):
779
+ id: str
780
+ item_kind: KnowledgeItemKind
781
+ key: str
782
+ preference_key: str
783
+ title: str
784
+ instruction: str
785
+ profile_aspect: PersonalProfileAspect
786
+ preference_scope: PreferenceScope
787
+ preference_project_id: str | None = None
788
+ attributes: dict[str, Any] = Field(default_factory=dict)
789
+ confirmation_status: Literal['confirmed', 'agent_confirmed']
790
+ confirmed_at: datetime
791
+ created_at: datetime | None = None
792
+
793
+
794
+ class CollaborationPreferenceConflict(StrictModel):
795
+ preference_key: str
796
+ preference_scope: PreferenceScope
797
+ preference_project_id: str | None = None
798
+ item_ids: list[str] = Field(min_length=2)
799
+
800
+
801
+ class CollaborationContextResult(StrictModel):
802
+ personal_space_id: str
803
+ personal_project_id: str | None = None
804
+ global_preferences: list[CollaborationPreferenceItem] = Field(default_factory=list)
805
+ project_preferences: list[CollaborationPreferenceItem] = Field(default_factory=list)
806
+ effective_preferences: list[CollaborationPreferenceItem] = Field(default_factory=list)
807
+ conflicts: list[CollaborationPreferenceConflict] = Field(default_factory=list)
808
+ overridden_global_ids: list[str] = Field(default_factory=list)
809
+ truncated: bool = False
810
+
811
+
812
+ class PreferenceConflictDeferCreate(StrictModel):
813
+ personal_space_id: str = Field(min_length=1, max_length=128)
814
+ conflict_id: str = Field(min_length=1, max_length=1024)
815
+ preference_key: str = Field(min_length=1, max_length=512)
816
+ preference_scope: PreferenceScope
817
+ preference_project_id: str | None = Field(default=None, min_length=1, max_length=128)
818
+ left_item_id: str = Field(min_length=1, max_length=256)
819
+ left_item_kind: KnowledgeItemKind
820
+ right_item_id: str = Field(min_length=1, max_length=256)
821
+ right_item_kind: KnowledgeItemKind
822
+ reason: str = Field(min_length=1, max_length=2000)
823
+ operation_actor: KnowledgeOperationActor = 'human'
824
+
825
+ @model_validator(mode='after')
826
+ def validate_conflict(self):
827
+ if (
828
+ self.left_item_id == self.right_item_id
829
+ and self.left_item_kind == self.right_item_kind
830
+ ):
831
+ raise ValueError('preference conflict requires two different items')
832
+ if self.preference_scope == 'project' and not self.preference_project_id:
833
+ raise ValueError('project preference conflict requires preference_project_id')
834
+ if self.preference_scope == 'global' and self.preference_project_id:
835
+ raise ValueError('global preference conflict cannot set preference_project_id')
836
+ return self
837
+
838
+
839
+ class PreferenceConflictResolveCreate(StrictModel):
840
+ personal_space_id: str = Field(min_length=1, max_length=128)
841
+ resolution: PreferenceConflictResolutionAction
842
+ reason: str = Field(min_length=1, max_length=2000)
843
+ canonical_item_id: str | None = Field(default=None, min_length=1, max_length=256)
844
+ merged_instruction: str | None = Field(default=None, min_length=1, max_length=4096)
845
+ split_item_id: str | None = Field(default=None, min_length=1, max_length=256)
846
+ split_project_id: str | None = Field(default=None, min_length=1, max_length=128)
847
+ operation_actor: KnowledgeOperationActor = 'agent'
848
+
849
+ @model_validator(mode='after')
850
+ def validate_resolution_details(self):
851
+ if self.resolution == 'merge':
852
+ if not self.canonical_item_id or not self.merged_instruction:
853
+ raise ValueError(
854
+ 'merge requires canonical_item_id and merged_instruction'
855
+ )
856
+ if self.resolution == 'split_scope':
857
+ if not self.split_item_id or not self.split_project_id:
858
+ raise ValueError('split_scope requires split_item_id and split_project_id')
859
+ return self
860
+
861
+
862
+ class PreferenceConflictCompleteCreate(StrictModel):
863
+ personal_space_id: str = Field(min_length=1, max_length=128)
864
+ resolution: PreferenceConflictResolutionAction
865
+ reason: str = Field(min_length=1, max_length=2000)
866
+ operation_actor: KnowledgeOperationActor = 'human'
867
+
868
+
869
+ class PreferenceConflictRecord(StrictModel):
870
+ id: str
871
+ personal_space_id: str
872
+ preference_key: str
873
+ preference_scope: PreferenceScope
874
+ preference_project_id: str | None = None
875
+ left_item_id: str
876
+ left_item_kind: KnowledgeItemKind
877
+ right_item_id: str
878
+ right_item_kind: KnowledgeItemKind
879
+ status: PreferenceConflictQueueStatus
880
+ requested_by: KnowledgeOperationActor
881
+ resolution: PreferenceConflictResolutionAction | None = None
882
+ resolved_by: KnowledgeOperationActor | None = None
883
+ reason: str
884
+ resolution_reason: str | None = None
885
+ deferred_at: datetime
886
+ resolved_at: datetime | None = None
887
+ updated_at: datetime
888
+
889
+
890
+ class GraphEvidence(StrictModel):
891
+ id: str
892
+ name: str
893
+ source_description: str
894
+ source_kind: str
895
+ summary: str
896
+ session_id: str | None = None
897
+ source_application: SourceApplication | None = None
898
+ source_turn_id: str | None = None
899
+ source_excerpt: str | None = None
900
+ personal_project_id: str | None = None
901
+ reference_time: datetime | None = None
902
+ created_at: datetime | None = None
903
+
904
+
905
+ class KnowledgeRevisionCreate(StrictModel):
906
+ personal_space_id: str = Field(min_length=1, max_length=128)
907
+ personal_project_id: str | None = Field(default=None, min_length=1, max_length=128)
908
+ item_kind: KnowledgeItemKind
909
+ action: KnowledgeContentRevisionAction
910
+ reason: str = Field(min_length=1, max_length=2000)
911
+ name: str | None = Field(default=None, min_length=1, max_length=512)
912
+ summary: str | None = Field(default=None, max_length=4096)
913
+ fact: str | None = Field(default=None, min_length=1, max_length=8192)
914
+ origin_quadrant: EpistemicQuadrant | None = None
915
+ current_quadrant: EpistemicQuadrant | None = None
916
+ epistemic_status: EpistemicStatus | None = None
917
+ confirmation_status: ConfirmationStatus | None = None
918
+ confirmation_basis: ConfirmationBasis | None = None
919
+ reasoning_summary: str | None = Field(default=None, max_length=4096)
920
+ profile_aspect: PersonalProfileAspect | Literal['none'] | None = None
921
+ inheritance_mode: KnowledgeInheritanceMode | None = None
922
+ inherited_project_ids: list[str] | None = Field(default=None, max_length=32)
923
+ replacement_item_id: str | None = Field(default=None, min_length=1, max_length=256)
924
+ replacement_item_kind: KnowledgeItemKind | None = None
925
+ operation_actor: KnowledgeOperationActor = 'agent'
926
+
927
+ @model_validator(mode='after')
928
+ def validate_update_fields(self):
929
+ has_replacement_id = self.replacement_item_id is not None
930
+ has_replacement_kind = self.replacement_item_kind is not None
931
+ if has_replacement_id != has_replacement_kind:
932
+ raise ValueError(
933
+ 'replacement item id and kind must be provided together'
934
+ )
935
+ if self.action == 'link_replacement' and not has_replacement_id:
936
+ raise ValueError('linking a replacement requires a replacement item')
937
+ if (
938
+ self.action not in {'invalidate', 'link_replacement'}
939
+ and has_replacement_id
940
+ ):
941
+ raise ValueError(
942
+ 'replacement item is only valid when invalidating or linking a replacement'
943
+ )
944
+ if self.action == 'confirm':
945
+ if (
946
+ self.confirmation_status != 'confirmed'
947
+ or self.confirmation_basis is None
948
+ ):
949
+ raise ValueError(
950
+ 'confirmation requires confirmed status and a structured basis'
951
+ )
952
+ if any(value is not None for value in (
953
+ self.name,
954
+ self.summary,
955
+ self.fact,
956
+ self.origin_quadrant,
957
+ self.current_quadrant,
958
+ self.epistemic_status,
959
+ self.reasoning_summary,
960
+ self.profile_aspect,
961
+ self.inheritance_mode,
962
+ self.inherited_project_ids,
963
+ )):
964
+ raise ValueError('confirmation cannot change knowledge content or taxonomy')
965
+ if self.action == 'update':
966
+ taxonomy_changed = any(value is not None for value in (
967
+ self.current_quadrant,
968
+ self.epistemic_status,
969
+ self.origin_quadrant,
970
+ self.confirmation_status,
971
+ self.confirmation_basis,
972
+ self.reasoning_summary,
973
+ self.profile_aspect,
974
+ self.inheritance_mode,
975
+ self.inherited_project_ids,
976
+ ))
977
+ if (
978
+ self.item_kind == 'entity'
979
+ and self.name is None
980
+ and self.summary is None
981
+ and not taxonomy_changed
982
+ ):
983
+ raise ValueError('entity update requires content or epistemic metadata')
984
+ if self.item_kind == 'relationship' and self.fact is None and not taxonomy_changed:
985
+ raise ValueError('relationship update requires content or epistemic metadata')
986
+ if self.confirmation_status is not None:
987
+ if self.confirmation_status == 'agent_confirmed':
988
+ raise ValueError(
989
+ 'agent-confirmed status can only be produced by the '
990
+ 'knowledge usage policy'
991
+ )
992
+ if self.confirmation_basis is None:
993
+ raise ValueError('confirmation status requires a structured confirmation basis')
994
+ _validate_confirmation_state(
995
+ self.confirmation_status,
996
+ self.confirmation_basis,
997
+ )
998
+ if (self.inheritance_mode is None) != (self.inherited_project_ids is None):
999
+ raise ValueError(
1000
+ 'inheritance mode and inherited project ids must be updated together'
1001
+ )
1002
+ if self.inheritance_mode is not None:
1003
+ _validate_inheritance(
1004
+ self.inheritance_mode,
1005
+ self.inherited_project_ids,
1006
+ None if self.profile_aspect in {None, 'none'} else self.profile_aspect,
1007
+ )
1008
+ reject_credentials(self, 'Knowledge revision')
1009
+ return self
1010
+
1011
+
1012
+ class KnowledgeBatchConfirmationItem(StrictModel):
1013
+ item_id: str = Field(min_length=1, max_length=256)
1014
+ item_kind: KnowledgeItemKind
1015
+ existence_reason: str = Field(min_length=1, max_length=4096)
1016
+ quadrant_reason: str = Field(min_length=1, max_length=4096)
1017
+ proposed_by: ConfirmationActor
1018
+
1019
+
1020
+ class KnowledgeBatchConfirmationCreate(StrictModel):
1021
+ personal_space_id: str = Field(min_length=1, max_length=128)
1022
+ group_kind: KnowledgeConfirmationGroupKind
1023
+ group_value: str = Field(min_length=1, max_length=256)
1024
+ reason: str = Field(min_length=1, max_length=2000)
1025
+ confirmer: ConfirmationActor
1026
+ items: list[KnowledgeBatchConfirmationItem] = Field(
1027
+ min_length=2,
1028
+ max_length=200,
1029
+ )
1030
+ operation_actor: KnowledgeOperationActor = 'agent'
1031
+
1032
+ @model_validator(mode='after')
1033
+ def validate_batch_confirmation(self):
1034
+ if self.confirmer.kind not in {'user', 'authoritative_source'}:
1035
+ raise ValueError('an agent or import cannot confirm knowledge')
1036
+ if self.confirmer.kind == 'authoritative_source' and not self.confirmer.label:
1037
+ raise ValueError('an authoritative source confirmer requires a label')
1038
+ identities = {
1039
+ (item.item_kind, item.item_id)
1040
+ for item in self.items
1041
+ }
1042
+ if len(identities) != len(self.items):
1043
+ raise ValueError('batch confirmation items must be unique')
1044
+ reject_credentials(self, 'Knowledge batch confirmation')
1045
+ return self
1046
+
1047
+
1048
+ class KnowledgeBatchConfirmationResult(StrictModel):
1049
+ group_kind: KnowledgeConfirmationGroupKind
1050
+ group_value: str
1051
+ confirmed_count: int = Field(ge=0, le=200)
1052
+ confirmed_at: datetime
1053
+ item_keys: list[str] = Field(max_length=200)
1054
+
1055
+
1056
+ class KnowledgeAssignmentChange(StrictModel):
1057
+ personal_space_id: str = Field(min_length=1, max_length=128)
1058
+ item_kind: KnowledgeItemKind
1059
+ target_project_id: str = Field(min_length=1, max_length=128)
1060
+ reason: str = Field(min_length=1, max_length=2000)
1061
+ operation_actor: KnowledgeOperationActor = 'agent'
1062
+
1063
+
1064
+ class PreferenceScopeChange(StrictModel):
1065
+ personal_space_id: str = Field(min_length=1, max_length=128)
1066
+ item_kind: KnowledgeItemKind
1067
+ scope: PreferenceScope
1068
+ project_id: str | None = Field(default=None, min_length=1, max_length=128)
1069
+ reason: str = Field(min_length=1, max_length=2000)
1070
+ operation_actor: KnowledgeOperationActor = 'agent'
1071
+
1072
+ @model_validator(mode='after')
1073
+ def validate_project_scope(self):
1074
+ if self.scope == 'project' and not self.project_id:
1075
+ raise ValueError('project_id is required for project preference scope')
1076
+ if self.scope == 'global' and self.project_id:
1077
+ raise ValueError('project_id is only valid for project preference scope')
1078
+ return self
1079
+
1080
+
1081
+ class KnowledgeRevisionRecord(StrictModel):
1082
+ id: str
1083
+ item_id: str
1084
+ item_kind: KnowledgeItemKind
1085
+ action: KnowledgeRevisionAction
1086
+ reason: str
1087
+ previous: dict[str, Any] = Field(default_factory=dict)
1088
+ current: dict[str, Any] = Field(default_factory=dict)
1089
+ created_at: datetime
1090
+
1091
+
1092
+ class KnowledgeAuditRecord(StrictModel):
1093
+ id: str
1094
+ item_id: str
1095
+ item_kind: KnowledgeItemKind
1096
+ action: KnowledgeAuditAction
1097
+ human_change_version: int = Field(default=0, ge=0)
1098
+ reason: str
1099
+ tool_name: str | None = None
1100
+ task_id: str | None = None
1101
+ session_id: str | None = None
1102
+ use_kind: KnowledgeUseKind | None = None
1103
+ usage_generation: int | None = Field(default=None, ge=1)
1104
+ conflict_check: Literal['no_conflict', 'conflict'] | None = None
1105
+ classification_check: Literal['reasonable', 'needs_change'] | None = None
1106
+ outcome: Literal[
1107
+ 'pending_review',
1108
+ 'requires_attention',
1109
+ 'reviewed',
1110
+ 'agent_confirmed',
1111
+ ] | None = None
1112
+ created_at: datetime
1113
+
1114
+
1115
+ class KnowledgeAgentViewItem(StrictModel):
1116
+ item_id: str = Field(min_length=1, max_length=256)
1117
+ item_kind: KnowledgeItemKind
1118
+
1119
+
1120
+ class KnowledgeAgentViewCreate(StrictModel):
1121
+ personal_space_id: str = Field(min_length=1, max_length=128)
1122
+ tool_name: str = Field(min_length=1, max_length=128)
1123
+ items: list[KnowledgeAgentViewItem] = Field(max_length=200)
1124
+
1125
+
1126
+ class KnowledgeAgentViewResult(StrictModel):
1127
+ recorded_count: int = Field(ge=0, le=200)
1128
+ item_keys: list[str] = Field(max_length=200)
1129
+
1130
+
1131
+ class KnowledgeAgentReviewCreate(StrictModel):
1132
+ personal_space_id: str = Field(min_length=1, max_length=128)
1133
+ item_kind: KnowledgeItemKind
1134
+ human_change_version: int = Field(ge=1)
1135
+ conflict_check: Literal['no_conflict', 'conflict']
1136
+ classification_check: Literal['reasonable', 'needs_change']
1137
+ note: str = Field(min_length=1, max_length=2000)
1138
+
1139
+
1140
+ class KnowledgeHumanChangeSearchRequest(StrictModel):
1141
+ personal_space_id: str = Field(min_length=1, max_length=128)
1142
+ query: str = Field(default='', max_length=2048)
1143
+ status: Literal[
1144
+ 'all',
1145
+ 'human_changed',
1146
+ 'unseen',
1147
+ 'viewed',
1148
+ 'reviewed',
1149
+ ] = 'all'
1150
+ limit: int = Field(default=50, ge=1, le=200)
1151
+
1152
+
1153
+ class KnowledgeHumanChangeItem(StrictModel):
1154
+ item_id: str
1155
+ item_kind: KnowledgeItemKind
1156
+ title: str
1157
+ body: str
1158
+ type: str
1159
+ human_change_status: HumanChangeStatus
1160
+ human_change_version: int = Field(ge=1)
1161
+ last_human_changed_at: datetime
1162
+ last_agent_viewed_at: datetime | None = None
1163
+ last_agent_reviewed_at: datetime | None = None
1164
+ audit_events: list[KnowledgeAuditRecord] = Field(default_factory=list)
1165
+
1166
+
1167
+ class KnowledgeHumanChangeSearchResult(StrictModel):
1168
+ items: list[KnowledgeHumanChangeItem] = Field(default_factory=list)
1169
+
1170
+
1171
+ class KnowledgeAssignmentRecord(StrictModel):
1172
+ id: str
1173
+ item_id: str
1174
+ item_kind: KnowledgeItemKind
1175
+ project_id: str
1176
+ previous_project_id: str | None = None
1177
+ reason: str
1178
+ created_at: datetime
1179
+ updated_at: datetime