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.
- package/LICENSE +201 -0
- package/README.md +178 -0
- package/compose.graphiti.yml +86 -0
- package/dist/web/assets/ConnectionsPage-CY6H_mJJ.js +1 -0
- package/dist/web/assets/KnowledgeInspector-BSpz5Sfm.js +1 -0
- package/dist/web/assets/KnowledgeInspector-CHbB_0rC.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-BlEvhPrv.css +1 -0
- package/dist/web/assets/KnowledgeOrganizerPage-CvjGPlUc.js +1 -0
- package/dist/web/assets/KnowledgePage-Khuo3cNs.js +1 -0
- package/dist/web/assets/KnowledgeWorkspace-DVZusvem.js +4 -0
- package/dist/web/assets/OverviewPage-DC4C4Fhn.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CSV9Mtgz.js +1 -0
- package/dist/web/assets/PersonalProfilePage-CVJ3TcRi.css +1 -0
- package/dist/web/assets/PersonalProjectsPage-A50EaseQ.js +1 -0
- package/dist/web/assets/PublicProjectsPage-BAdO_bae.js +1 -0
- package/dist/web/assets/ReviewPage-Dztk2jDG.js +1 -0
- package/dist/web/assets/console-tbvTUi6p.js +1 -0
- package/dist/web/assets/fuli-logo-DGuyqchQ.png +0 -0
- package/dist/web/assets/identity-B3s5CXhv.js +1 -0
- package/dist/web/assets/index-Devf7glZ.css +1 -0
- package/dist/web/assets/index-DwUDhQKc.js +6 -0
- package/dist/web/assets/model-DVT8sjy9.js +1 -0
- package/dist/web/assets/paths-B790iniO.js +1 -0
- package/dist/web/assets/runtime-dom.esm-bundler-DttsK21F.js +1 -0
- package/dist/web/assets/vue-router-BYTAoWT3.js +1 -0
- package/dist/web/index.html +22 -0
- package/graph-provider/Dockerfile +16 -0
- package/graph-provider/fuli_graph/__init__.py +5 -0
- package/graph-provider/fuli_graph/app.py +547 -0
- package/graph-provider/fuli_graph/auth.py +17 -0
- package/graph-provider/fuli_graph/collaboration_context.py +290 -0
- package/graph-provider/fuli_graph/config.py +34 -0
- package/graph-provider/fuli_graph/graph_models.py +123 -0
- package/graph-provider/fuli_graph/graph_projection.py +405 -0
- package/graph-provider/fuli_graph/graph_query.py +561 -0
- package/graph-provider/fuli_graph/knowledge_audit.py +765 -0
- package/graph-provider/fuli_graph/knowledge_batch_confirmation.py +246 -0
- package/graph-provider/fuli_graph/knowledge_management.py +816 -0
- package/graph-provider/fuli_graph/knowledge_records.py +65 -0
- package/graph-provider/fuli_graph/knowledge_search.py +723 -0
- package/graph-provider/fuli_graph/knowledge_usage_models.py +52 -0
- package/graph-provider/fuli_graph/model_validation.py +58 -0
- package/graph-provider/fuli_graph/models.py +1179 -0
- package/graph-provider/fuli_graph/personal_project_access.py +29 -0
- package/graph-provider/fuli_graph/preference_conflicts.py +420 -0
- package/graph-provider/fuli_graph/project_action_models.py +142 -0
- package/graph-provider/fuli_graph/project_knowledge.py +707 -0
- package/graph-provider/fuli_graph/project_management.py +251 -0
- package/graph-provider/fuli_graph/provider_values.py +40 -0
- package/graph-provider/fuli_graph/runtime.py +350 -0
- package/graph-provider/fuli_graph/search_projection.py +156 -0
- package/graph-provider/fuli_graph/store.py +24 -0
- package/graph-provider/fuli_graph/store_identity.py +237 -0
- package/graph-provider/fuli_graph/store_knowledge.py +447 -0
- package/graph-provider/fuli_graph/store_projects.py +242 -0
- package/graph-provider/fuli_graph/store_publication.py +370 -0
- package/graph-provider/fuli_graph/store_records.py +134 -0
- package/graph-provider/pyproject.toml +35 -0
- package/npm-shrinkwrap.json +6045 -0
- package/package.json +99 -0
- package/skills/capturing-session-knowledge/SKILL.md +226 -0
- package/skills/capturing-session-knowledge/agents/openai.yaml +13 -0
- package/skills/grilling-project/SKILL.md +129 -0
- package/skills/grilling-project/agents/openai.yaml +4 -0
- package/skills/grilling-project/references/project-profile.md +105 -0
- package/src/agent/candidate-read-service.js +53 -0
- package/src/agent/context-pack-service.js +83 -0
- package/src/agent/public-read-policy.js +39 -0
- package/src/agent/public-read-service.js +69 -0
- package/src/agent/read-projection.js +107 -0
- package/src/agent/search-service.js +61 -0
- package/src/agent/space-boundary.js +33 -0
- package/src/agent-tools/graph-definitions.js +479 -0
- package/src/agent-tools/graph-handlers.js +96 -0
- package/src/agent-tools/schema.js +37 -0
- package/src/agent-tools.js +16 -0
- package/src/app/application-error.js +13 -0
- package/src/app/bootstrap.js +8 -0
- package/src/app/create-application.js +143 -0
- package/src/app/state-service.js +38 -0
- package/src/candidates.js +78 -0
- package/src/classifier.js +63 -0
- package/src/cli/command-arguments.js +14 -0
- package/src/cli/command-registry.js +78 -0
- package/src/cli/commands/candidates.js +17 -0
- package/src/cli/commands/ingestion.js +32 -0
- package/src/cli/commands/reads.js +50 -0
- package/src/cli/commands/spaces.js +20 -0
- package/src/cli/invocation.js +47 -0
- package/src/cli/local-runtime-command.js +94 -0
- package/src/cli/local-runtime-options.js +64 -0
- package/src/cli/migrate-command.js +71 -0
- package/src/cli/migration-destination.js +117 -0
- package/src/cli/setup-agent-selection.js +195 -0
- package/src/cli/setup-command.js +85 -0
- package/src/cli/uninstall-command.js +67 -0
- package/src/cli.js +90 -0
- package/src/context-pack.js +206 -0
- package/src/defaults.js +1 -0
- package/src/extractor.js +77 -0
- package/src/fact-history.js +39 -0
- package/src/facts/fact-service.js +89 -0
- package/src/graphiti/agent-access-policy.js +88 -0
- package/src/graphiti/capture-policy.js +84 -0
- package/src/graphiti/federated-application.js +1292 -0
- package/src/graphiti/personal-context.js +29 -0
- package/src/graphiti/project-path-context.js +159 -0
- package/src/graphiti/provider-client.js +288 -0
- package/src/graphiti/runtime-config.js +85 -0
- package/src/graphiti/setup.js +43 -0
- package/src/graphiti/source-marker.js +142 -0
- package/src/http/api-router.js +133 -0
- package/src/http/bounded-json-array.js +19 -0
- package/src/http/error-mapping.js +36 -0
- package/src/http/graph-api-router.js +257 -0
- package/src/http/lens-route.js +50 -0
- package/src/http/request-policy.js +41 -0
- package/src/http/response.js +29 -0
- package/src/http/static-handler.js +55 -0
- package/src/ingestion.js +161 -0
- package/src/lens/lens-correction-service.js +81 -0
- package/src/lens/lens-fact-writer.js +85 -0
- package/src/lens/lens-input.js +62 -0
- package/src/lens/lens-query-projection.js +87 -0
- package/src/lens/lens-query-ranking.js +62 -0
- package/src/lens/lens-query.js +143 -0
- package/src/lens/lens-resource-projection.js +93 -0
- package/src/lens/lens-resource-selection.js +45 -0
- package/src/lens/lens-resource-service.js +103 -0
- package/src/lens/lens-search-query.js +122 -0
- package/src/lens/lens-service.js +183 -0
- package/src/local-runtime/lifecycle.js +254 -0
- package/src/mcp/create-mcp-server.js +85 -0
- package/src/mcp/instructions.js +10 -0
- package/src/mcp/project-action-preview-tokens.js +72 -0
- package/src/mcp/runtime.js +71 -0
- package/src/mcp/tool-annotations.js +63 -0
- package/src/mcp/tool-result.js +167 -0
- package/src/mcp/tool-schema.js +60 -0
- package/src/mcp-server.js +76 -0
- package/src/models.js +44 -0
- package/src/observer.js +83 -0
- package/src/package-metadata.js +6 -0
- package/src/project-rules.js +33 -0
- package/src/publication/canonical-json.js +39 -0
- package/src/publication/outbox-service.js +46 -0
- package/src/publication/publication-envelope-schema.js +75 -0
- package/src/publication/publication-envelope.js +37 -0
- package/src/publication/publication-policy.js +82 -0
- package/src/publication/publication-service.js +48 -0
- package/src/router.js +58 -0
- package/src/runtime-options.js +139 -0
- package/src/security/episode-visibility.js +14 -0
- package/src/security/fact-visibility.js +15 -0
- package/src/security/sensitive-content.js +18 -0
- package/src/security/source-metadata-policy.js +8 -0
- package/src/server/application-lifecycle.js +34 -0
- package/src/server/listen.js +24 -0
- package/src/server.js +135 -0
- package/src/setup/agent-installation-status.js +97 -0
- package/src/setup/agents.js +123 -0
- package/src/setup/codex-bootstrap.js +118 -0
- package/src/setup/codex-config.js +122 -0
- package/src/setup/config-backup.js +22 -0
- package/src/setup/container-runtime.js +388 -0
- package/src/setup/cursor-config.js +61 -0
- package/src/setup/graph-runtime.js +360 -0
- package/src/setup/node-runtime.js +24 -0
- package/src/setup/options.js +91 -0
- package/src/setup/paths.js +47 -0
- package/src/setup/session-skill.js +91 -0
- package/src/setup/setup.js +85 -0
- package/src/setup/uninstall.js +90 -0
- package/src/storage/episode-evidence-preview.js +100 -0
- package/src/storage/fact-search-order.js +18 -0
- package/src/storage/file/episode-evidence.js +37 -0
- package/src/storage/file/episode-index.js +59 -0
- package/src/storage/file/fact-search.js +60 -0
- package/src/storage/file-store.js +437 -0
- package/src/storage/import-json.js +70 -0
- package/src/storage/json-file.js +41 -0
- package/src/storage/legacy-record-validation.js +152 -0
- package/src/storage/legacy-snapshot-validation.js +32 -0
- package/src/storage/migrate.js +128 -0
- package/src/storage/migrations/001-initial.sql +83 -0
- package/src/storage/migrations/002-storage-integrity.sql +113 -0
- package/src/storage/record-validation.js +46 -0
- package/src/storage/snapshot-defaults.js +54 -0
- package/src/storage/sqlite/candidates-repository.js +84 -0
- package/src/storage/sqlite/episode-evidence-repository.js +154 -0
- package/src/storage/sqlite/episode-privacy-sql.js +33 -0
- package/src/storage/sqlite/episodes-repository.js +52 -0
- package/src/storage/sqlite/fact-search-repository.js +162 -0
- package/src/storage/sqlite/facts-repository.js +146 -0
- package/src/storage/sqlite/imports-repository.js +39 -0
- package/src/storage/sqlite/mapper.js +84 -0
- package/src/storage/sqlite/open-database.js +15 -0
- package/src/storage/sqlite/outbox-repository.js +103 -0
- package/src/storage/sqlite/snapshot-repository.js +69 -0
- package/src/storage/sqlite/spaces-repository.js +52 -0
- package/src/storage/sqlite/subscriptions-repository.js +54 -0
- package/src/storage/sqlite-store.js +159 -0
- package/src/storage/stable-json.js +24 -0
- package/src/storage/store-port.js +20 -0
- package/src/store.js +2 -0
- package/src/workspace-protocol/array-validation.js +19 -0
- package/src/workspace-protocol/base64url-contract.js +27 -0
- package/src/workspace-protocol/constants.js +37 -0
- package/src/workspace-protocol/error-contract.js +24 -0
- package/src/workspace-protocol/fact-contract.js +110 -0
- package/src/workspace-protocol/governance-contract.js +47 -0
- package/src/workspace-protocol/index.js +53 -0
- package/src/workspace-protocol/json-budget.js +19 -0
- package/src/workspace-protocol/manifest-contract.js +186 -0
- package/src/workspace-protocol/publication-contract.js +212 -0
- package/src/workspace-protocol/query-contract.js +149 -0
- package/src/workspace-protocol/scalar-contract.js +192 -0
- package/src/workspace-protocol/sync-contract.js +184 -0
- package/src/workspace-protocol/text-contract.js +39 -0
- package/src/workspace-protocol/validation.js +66 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from fastapi import HTTPException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
async def authorize_personal_project(
|
|
5
|
+
store,
|
|
6
|
+
actor: dict,
|
|
7
|
+
space: dict,
|
|
8
|
+
project_id: str,
|
|
9
|
+
) -> dict:
|
|
10
|
+
if store.settings.provider_mode != 'personal' or space['kind'] != 'personal':
|
|
11
|
+
raise HTTPException(
|
|
12
|
+
status_code=422,
|
|
13
|
+
detail='personal project scope requires a personal space',
|
|
14
|
+
)
|
|
15
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
16
|
+
'''
|
|
17
|
+
MATCH (:FuliPrincipal {id: $principal_id})-[:OWNS]->
|
|
18
|
+
(:FuliSpace {id: $space_id, kind: 'personal'})-[:CONTAINS_PROJECT]->
|
|
19
|
+
(project:FuliPersonalProject {project_id: $project_id})
|
|
20
|
+
RETURN project
|
|
21
|
+
''',
|
|
22
|
+
principal_id=actor['id'],
|
|
23
|
+
space_id=space['id'],
|
|
24
|
+
project_id=project_id,
|
|
25
|
+
routing_='r',
|
|
26
|
+
)
|
|
27
|
+
if not records:
|
|
28
|
+
raise HTTPException(status_code=404, detail='personal project not found')
|
|
29
|
+
return dict(records[0]['project'])
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
|
|
4
|
+
from fastapi import HTTPException
|
|
5
|
+
|
|
6
|
+
from .knowledge_management import (
|
|
7
|
+
_read_item,
|
|
8
|
+
revise_knowledge_item,
|
|
9
|
+
set_preference_scope,
|
|
10
|
+
)
|
|
11
|
+
from .models import (
|
|
12
|
+
KnowledgeRevisionCreate,
|
|
13
|
+
PreferenceConflictCompleteCreate,
|
|
14
|
+
PreferenceConflictDeferCreate,
|
|
15
|
+
PreferenceConflictRecord,
|
|
16
|
+
PreferenceConflictResolveCreate,
|
|
17
|
+
PreferenceScopeChange,
|
|
18
|
+
)
|
|
19
|
+
from .provider_values import native_datetime as _native_datetime
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def defer_preference_conflict(
|
|
23
|
+
store,
|
|
24
|
+
actor: dict,
|
|
25
|
+
request: PreferenceConflictDeferCreate,
|
|
26
|
+
) -> PreferenceConflictRecord:
|
|
27
|
+
space = await _authorize_personal_space(
|
|
28
|
+
store,
|
|
29
|
+
actor,
|
|
30
|
+
request.personal_space_id,
|
|
31
|
+
'maintainer',
|
|
32
|
+
)
|
|
33
|
+
left = await _active_preference_item(
|
|
34
|
+
store,
|
|
35
|
+
space,
|
|
36
|
+
request.left_item_id,
|
|
37
|
+
request.left_item_kind,
|
|
38
|
+
)
|
|
39
|
+
right = await _active_preference_item(
|
|
40
|
+
store,
|
|
41
|
+
space,
|
|
42
|
+
request.right_item_id,
|
|
43
|
+
request.right_item_kind,
|
|
44
|
+
)
|
|
45
|
+
_validate_pair_scope(left, right, request)
|
|
46
|
+
deferred_at = datetime.now(timezone.utc)
|
|
47
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
48
|
+
'''
|
|
49
|
+
MATCH (space:FuliSpace {id: $space_id, kind: 'personal'})
|
|
50
|
+
MERGE (conflict:FuliPreferenceConflict {id: $conflict_id})
|
|
51
|
+
ON CREATE SET conflict.personal_space_id = $space_id,
|
|
52
|
+
conflict.preference_key = $preference_key,
|
|
53
|
+
conflict.preference_scope = $preference_scope,
|
|
54
|
+
conflict.preference_project_id = $preference_project_id,
|
|
55
|
+
conflict.left_item_id = $left_item_id,
|
|
56
|
+
conflict.left_item_kind = $left_item_kind,
|
|
57
|
+
conflict.right_item_id = $right_item_id,
|
|
58
|
+
conflict.right_item_kind = $right_item_kind,
|
|
59
|
+
conflict.created_at = $deferred_at
|
|
60
|
+
SET conflict.status = 'ai_pending',
|
|
61
|
+
conflict.requested_by = $requested_by,
|
|
62
|
+
conflict.reason = $reason,
|
|
63
|
+
conflict.resolution = NULL,
|
|
64
|
+
conflict.resolved_by = NULL,
|
|
65
|
+
conflict.resolution_reason = NULL,
|
|
66
|
+
conflict.resolved_at = NULL,
|
|
67
|
+
conflict.deferred_at = $deferred_at,
|
|
68
|
+
conflict.updated_at = $deferred_at
|
|
69
|
+
MERGE (space)-[:HAS_PREFERENCE_CONFLICT]->(conflict)
|
|
70
|
+
RETURN conflict
|
|
71
|
+
''',
|
|
72
|
+
space_id=space['id'],
|
|
73
|
+
conflict_id=request.conflict_id,
|
|
74
|
+
preference_key=request.preference_key,
|
|
75
|
+
preference_scope=request.preference_scope,
|
|
76
|
+
preference_project_id=request.preference_project_id,
|
|
77
|
+
left_item_id=request.left_item_id,
|
|
78
|
+
left_item_kind=request.left_item_kind,
|
|
79
|
+
right_item_id=request.right_item_id,
|
|
80
|
+
right_item_kind=request.right_item_kind,
|
|
81
|
+
requested_by=request.operation_actor,
|
|
82
|
+
reason=request.reason,
|
|
83
|
+
deferred_at=deferred_at,
|
|
84
|
+
)
|
|
85
|
+
return _record(records[0]['conflict'])
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def list_preference_conflicts(
|
|
89
|
+
store,
|
|
90
|
+
actor: dict,
|
|
91
|
+
personal_space_id: str,
|
|
92
|
+
*,
|
|
93
|
+
status: str | None = None,
|
|
94
|
+
limit: int = 500,
|
|
95
|
+
) -> list[PreferenceConflictRecord]:
|
|
96
|
+
await _authorize_personal_space(
|
|
97
|
+
store,
|
|
98
|
+
actor,
|
|
99
|
+
personal_space_id,
|
|
100
|
+
'reader',
|
|
101
|
+
)
|
|
102
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
103
|
+
'''
|
|
104
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
105
|
+
[:HAS_PREFERENCE_CONFLICT]->(conflict:FuliPreferenceConflict)
|
|
106
|
+
WHERE $status IS NULL OR conflict.status = $status
|
|
107
|
+
RETURN conflict
|
|
108
|
+
ORDER BY conflict.updated_at DESC
|
|
109
|
+
LIMIT $limit
|
|
110
|
+
''',
|
|
111
|
+
space_id=personal_space_id,
|
|
112
|
+
status=status,
|
|
113
|
+
limit=min(max(limit, 1), 1000),
|
|
114
|
+
routing_='r',
|
|
115
|
+
)
|
|
116
|
+
return [_record(record['conflict']) for record in records]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
async def resolve_preference_conflict(
|
|
120
|
+
store,
|
|
121
|
+
actor: dict,
|
|
122
|
+
conflict_id: str,
|
|
123
|
+
request: PreferenceConflictResolveCreate,
|
|
124
|
+
) -> PreferenceConflictRecord:
|
|
125
|
+
space = await _authorize_personal_space(
|
|
126
|
+
store,
|
|
127
|
+
actor,
|
|
128
|
+
request.personal_space_id,
|
|
129
|
+
'maintainer',
|
|
130
|
+
)
|
|
131
|
+
conflict = await _read_conflict(store, space['id'], conflict_id)
|
|
132
|
+
if conflict.status == 'resolved':
|
|
133
|
+
return conflict
|
|
134
|
+
if conflict.status != 'ai_pending':
|
|
135
|
+
raise HTTPException(status_code=409, detail='preference conflict is not queued for AI')
|
|
136
|
+
|
|
137
|
+
left = await _active_preference_item(
|
|
138
|
+
store,
|
|
139
|
+
space,
|
|
140
|
+
conflict.left_item_id,
|
|
141
|
+
conflict.left_item_kind,
|
|
142
|
+
)
|
|
143
|
+
right = await _active_preference_item(
|
|
144
|
+
store,
|
|
145
|
+
space,
|
|
146
|
+
conflict.right_item_id,
|
|
147
|
+
conflict.right_item_kind,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if request.resolution == 'merge':
|
|
151
|
+
await _merge_items(store, actor, space, conflict, left, right, request)
|
|
152
|
+
elif request.resolution in {'keep_left', 'keep_right'}:
|
|
153
|
+
await _keep_one_item(store, actor, conflict, request)
|
|
154
|
+
else:
|
|
155
|
+
await _split_scope(store, actor, space, conflict, left, right, request)
|
|
156
|
+
|
|
157
|
+
return await _complete(
|
|
158
|
+
store,
|
|
159
|
+
space['id'],
|
|
160
|
+
conflict_id,
|
|
161
|
+
request.resolution,
|
|
162
|
+
request.reason,
|
|
163
|
+
request.operation_actor,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def complete_preference_conflict(
|
|
168
|
+
store,
|
|
169
|
+
actor: dict,
|
|
170
|
+
conflict_id: str,
|
|
171
|
+
request: PreferenceConflictCompleteCreate,
|
|
172
|
+
) -> PreferenceConflictRecord:
|
|
173
|
+
space = await _authorize_personal_space(
|
|
174
|
+
store,
|
|
175
|
+
actor,
|
|
176
|
+
request.personal_space_id,
|
|
177
|
+
'maintainer',
|
|
178
|
+
)
|
|
179
|
+
await _read_conflict(store, space['id'], conflict_id)
|
|
180
|
+
return await _complete(
|
|
181
|
+
store,
|
|
182
|
+
space['id'],
|
|
183
|
+
conflict_id,
|
|
184
|
+
request.resolution,
|
|
185
|
+
request.reason,
|
|
186
|
+
request.operation_actor,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def _merge_items(store, actor, space, conflict, left, right, request):
|
|
191
|
+
if request.canonical_item_id not in {
|
|
192
|
+
conflict.left_item_id,
|
|
193
|
+
conflict.right_item_id,
|
|
194
|
+
}:
|
|
195
|
+
raise HTTPException(
|
|
196
|
+
status_code=422,
|
|
197
|
+
detail='canonical item must belong to the deferred conflict',
|
|
198
|
+
)
|
|
199
|
+
canonical_is_left = request.canonical_item_id == conflict.left_item_id
|
|
200
|
+
canonical_kind = (
|
|
201
|
+
conflict.left_item_kind if canonical_is_left else conflict.right_item_kind
|
|
202
|
+
)
|
|
203
|
+
canonical = left if canonical_is_left else right
|
|
204
|
+
historical_id = (
|
|
205
|
+
conflict.right_item_id if canonical_is_left else conflict.left_item_id
|
|
206
|
+
)
|
|
207
|
+
historical_kind = (
|
|
208
|
+
conflict.right_item_kind if canonical_is_left else conflict.left_item_kind
|
|
209
|
+
)
|
|
210
|
+
confirmation = _preserved_confirmation(canonical)
|
|
211
|
+
update = {
|
|
212
|
+
'personal_space_id': space['id'],
|
|
213
|
+
'item_kind': canonical_kind,
|
|
214
|
+
'action': 'update',
|
|
215
|
+
'reason': request.reason,
|
|
216
|
+
'operation_actor': request.operation_actor,
|
|
217
|
+
}
|
|
218
|
+
if canonical_kind == 'entity':
|
|
219
|
+
update['summary'] = request.merged_instruction
|
|
220
|
+
else:
|
|
221
|
+
update['fact'] = request.merged_instruction
|
|
222
|
+
if confirmation:
|
|
223
|
+
update.update(confirmation)
|
|
224
|
+
await revise_knowledge_item(
|
|
225
|
+
store,
|
|
226
|
+
actor,
|
|
227
|
+
request.canonical_item_id,
|
|
228
|
+
KnowledgeRevisionCreate(**update),
|
|
229
|
+
)
|
|
230
|
+
await revise_knowledge_item(
|
|
231
|
+
store,
|
|
232
|
+
actor,
|
|
233
|
+
historical_id,
|
|
234
|
+
KnowledgeRevisionCreate(
|
|
235
|
+
personal_space_id=space['id'],
|
|
236
|
+
item_kind=historical_kind,
|
|
237
|
+
action='invalidate',
|
|
238
|
+
reason=request.reason,
|
|
239
|
+
replacement_item_id=request.canonical_item_id,
|
|
240
|
+
replacement_item_kind=canonical_kind,
|
|
241
|
+
operation_actor=request.operation_actor,
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
async def _keep_one_item(store, actor, conflict, request):
|
|
247
|
+
keep_left = request.resolution == 'keep_left'
|
|
248
|
+
kept_id = conflict.left_item_id if keep_left else conflict.right_item_id
|
|
249
|
+
kept_kind = conflict.left_item_kind if keep_left else conflict.right_item_kind
|
|
250
|
+
historical_id = conflict.right_item_id if keep_left else conflict.left_item_id
|
|
251
|
+
historical_kind = (
|
|
252
|
+
conflict.right_item_kind if keep_left else conflict.left_item_kind
|
|
253
|
+
)
|
|
254
|
+
await revise_knowledge_item(
|
|
255
|
+
store,
|
|
256
|
+
actor,
|
|
257
|
+
historical_id,
|
|
258
|
+
KnowledgeRevisionCreate(
|
|
259
|
+
personal_space_id=request.personal_space_id,
|
|
260
|
+
item_kind=historical_kind,
|
|
261
|
+
action='invalidate',
|
|
262
|
+
reason=request.reason,
|
|
263
|
+
replacement_item_id=kept_id,
|
|
264
|
+
replacement_item_kind=kept_kind,
|
|
265
|
+
operation_actor=request.operation_actor,
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
async def _split_scope(store, actor, space, conflict, left, right, request):
|
|
271
|
+
if request.split_item_id not in {
|
|
272
|
+
conflict.left_item_id,
|
|
273
|
+
conflict.right_item_id,
|
|
274
|
+
}:
|
|
275
|
+
raise HTTPException(
|
|
276
|
+
status_code=422,
|
|
277
|
+
detail='split item must belong to the deferred conflict',
|
|
278
|
+
)
|
|
279
|
+
split_left = request.split_item_id == conflict.left_item_id
|
|
280
|
+
item_kind = conflict.left_item_kind if split_left else conflict.right_item_kind
|
|
281
|
+
item = left if split_left else right
|
|
282
|
+
if (
|
|
283
|
+
item.get('preference_scope') == 'project'
|
|
284
|
+
and item.get('preference_project_id') == request.split_project_id
|
|
285
|
+
):
|
|
286
|
+
return
|
|
287
|
+
await set_preference_scope(
|
|
288
|
+
store,
|
|
289
|
+
actor,
|
|
290
|
+
request.split_item_id,
|
|
291
|
+
PreferenceScopeChange(
|
|
292
|
+
personal_space_id=space['id'],
|
|
293
|
+
item_kind=item_kind,
|
|
294
|
+
scope='project',
|
|
295
|
+
project_id=request.split_project_id,
|
|
296
|
+
reason=request.reason,
|
|
297
|
+
operation_actor=request.operation_actor,
|
|
298
|
+
),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async def _complete(
|
|
303
|
+
store,
|
|
304
|
+
space_id,
|
|
305
|
+
conflict_id,
|
|
306
|
+
resolution,
|
|
307
|
+
reason,
|
|
308
|
+
resolved_by,
|
|
309
|
+
):
|
|
310
|
+
resolved_at = datetime.now(timezone.utc)
|
|
311
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
312
|
+
'''
|
|
313
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
314
|
+
[:HAS_PREFERENCE_CONFLICT]->
|
|
315
|
+
(conflict:FuliPreferenceConflict {id: $conflict_id})
|
|
316
|
+
SET conflict.status = 'resolved',
|
|
317
|
+
conflict.resolution = $resolution,
|
|
318
|
+
conflict.resolved_by = $resolved_by,
|
|
319
|
+
conflict.resolution_reason = $reason,
|
|
320
|
+
conflict.resolved_at = $resolved_at,
|
|
321
|
+
conflict.updated_at = $resolved_at
|
|
322
|
+
RETURN conflict
|
|
323
|
+
''',
|
|
324
|
+
space_id=space_id,
|
|
325
|
+
conflict_id=conflict_id,
|
|
326
|
+
resolution=resolution,
|
|
327
|
+
resolved_by=resolved_by,
|
|
328
|
+
reason=reason,
|
|
329
|
+
resolved_at=resolved_at,
|
|
330
|
+
)
|
|
331
|
+
if not records:
|
|
332
|
+
raise HTTPException(status_code=404, detail='preference conflict not found')
|
|
333
|
+
return _record(records[0]['conflict'])
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
async def _read_conflict(store, space_id, conflict_id):
|
|
337
|
+
records, _, _ = await store.runtime.driver.execute_query(
|
|
338
|
+
'''
|
|
339
|
+
MATCH (:FuliSpace {id: $space_id, kind: 'personal'})-
|
|
340
|
+
[:HAS_PREFERENCE_CONFLICT]->
|
|
341
|
+
(conflict:FuliPreferenceConflict {id: $conflict_id})
|
|
342
|
+
RETURN conflict
|
|
343
|
+
''',
|
|
344
|
+
space_id=space_id,
|
|
345
|
+
conflict_id=conflict_id,
|
|
346
|
+
routing_='r',
|
|
347
|
+
)
|
|
348
|
+
if not records:
|
|
349
|
+
raise HTTPException(status_code=404, detail='preference conflict not found')
|
|
350
|
+
return _record(records[0]['conflict'])
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def _active_preference_item(store, space, item_id, item_kind):
|
|
354
|
+
item = await _read_item(store, space, item_id, item_kind)
|
|
355
|
+
if item is None:
|
|
356
|
+
raise HTTPException(status_code=404, detail='preference item not found')
|
|
357
|
+
if item.get('invalid_at'):
|
|
358
|
+
raise HTTPException(status_code=409, detail='historical preference cannot be queued')
|
|
359
|
+
if not item.get('profile_aspect'):
|
|
360
|
+
raise HTTPException(status_code=422, detail='item is not a collaboration preference')
|
|
361
|
+
return item
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _validate_pair_scope(left, right, request):
|
|
365
|
+
left_scope = left.get('preference_scope') or 'global'
|
|
366
|
+
right_scope = right.get('preference_scope') or 'global'
|
|
367
|
+
if left_scope != right_scope or left_scope != request.preference_scope:
|
|
368
|
+
raise HTTPException(status_code=409, detail='preference scopes no longer match')
|
|
369
|
+
if (
|
|
370
|
+
left.get('preference_project_id') != right.get('preference_project_id')
|
|
371
|
+
or left.get('preference_project_id') != request.preference_project_id
|
|
372
|
+
):
|
|
373
|
+
raise HTTPException(status_code=409, detail='preference projects no longer match')
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _preserved_confirmation(item):
|
|
377
|
+
if item.get('confirmation_status') != 'confirmed':
|
|
378
|
+
return {}
|
|
379
|
+
value = item.get('confirmation_basis_json')
|
|
380
|
+
if not value:
|
|
381
|
+
return {}
|
|
382
|
+
basis = json.loads(value) if isinstance(value, str) else value
|
|
383
|
+
if not basis.get('confirmed_by') or not basis.get('confirmed_at'):
|
|
384
|
+
return {}
|
|
385
|
+
return {
|
|
386
|
+
'confirmation_status': 'confirmed',
|
|
387
|
+
'confirmation_basis': basis,
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
async def _authorize_personal_space(store, actor, space_id, role):
|
|
392
|
+
store._require_personal()
|
|
393
|
+
space = await store.authorize(actor, space_id, role)
|
|
394
|
+
if space['kind'] != 'personal':
|
|
395
|
+
raise HTTPException(status_code=422, detail='preference conflicts are personal-only')
|
|
396
|
+
return space
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _record(value) -> PreferenceConflictRecord:
|
|
400
|
+
data = dict(value)
|
|
401
|
+
return PreferenceConflictRecord(
|
|
402
|
+
id=data['id'],
|
|
403
|
+
personal_space_id=data['personal_space_id'],
|
|
404
|
+
preference_key=data['preference_key'],
|
|
405
|
+
preference_scope=data['preference_scope'],
|
|
406
|
+
preference_project_id=data.get('preference_project_id'),
|
|
407
|
+
left_item_id=data['left_item_id'],
|
|
408
|
+
left_item_kind=data['left_item_kind'],
|
|
409
|
+
right_item_id=data['right_item_id'],
|
|
410
|
+
right_item_kind=data['right_item_kind'],
|
|
411
|
+
status=data['status'],
|
|
412
|
+
requested_by=data['requested_by'],
|
|
413
|
+
resolution=data.get('resolution'),
|
|
414
|
+
resolved_by=data.get('resolved_by'),
|
|
415
|
+
reason=data['reason'],
|
|
416
|
+
resolution_reason=data.get('resolution_reason'),
|
|
417
|
+
deferred_at=_native_datetime(data['deferred_at']),
|
|
418
|
+
resolved_at=_native_datetime(data.get('resolved_at')),
|
|
419
|
+
updated_at=_native_datetime(data['updated_at']),
|
|
420
|
+
)
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, model_validator
|
|
5
|
+
|
|
6
|
+
from .models import (
|
|
7
|
+
KnowledgeItemKind,
|
|
8
|
+
KnowledgeOperationActor,
|
|
9
|
+
ProjectRelationType,
|
|
10
|
+
StrictModel,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
KnowledgeProjectActionMode = Literal['create', 'existing']
|
|
14
|
+
KnowledgeProjectReferenceStatus = Literal[
|
|
15
|
+
'active',
|
|
16
|
+
'pending_conflict',
|
|
17
|
+
'rejected',
|
|
18
|
+
'duplicate',
|
|
19
|
+
]
|
|
20
|
+
KnowledgeConflictResolution = Literal[
|
|
21
|
+
'defer',
|
|
22
|
+
'keep_target',
|
|
23
|
+
'use_source',
|
|
24
|
+
'coexist',
|
|
25
|
+
]
|
|
26
|
+
KnowledgeProjectMatchKind = Literal[
|
|
27
|
+
'none',
|
|
28
|
+
'already_linked',
|
|
29
|
+
'exact_duplicate',
|
|
30
|
+
'conflict',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class KnowledgeProjectPreviewRequest(StrictModel):
|
|
35
|
+
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
36
|
+
item_kind: Literal['entity'] = 'entity'
|
|
37
|
+
mode: KnowledgeProjectActionMode = 'existing'
|
|
38
|
+
target_project_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
39
|
+
new_project_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
40
|
+
new_project_name: str | None = Field(default=None, min_length=1, max_length=160)
|
|
41
|
+
new_project_purpose: str | None = Field(default=None, max_length=4096)
|
|
42
|
+
keep_source_relation: bool = True
|
|
43
|
+
relation_type: ProjectRelationType = 'RELATED_TO'
|
|
44
|
+
conflict_resolution: KnowledgeConflictResolution = 'defer'
|
|
45
|
+
reason: str | None = Field(default=None, min_length=1, max_length=2000)
|
|
46
|
+
|
|
47
|
+
@model_validator(mode='after')
|
|
48
|
+
def validate_target(self):
|
|
49
|
+
if self.mode == 'existing' and not self.target_project_id:
|
|
50
|
+
raise ValueError('existing mode requires target_project_id')
|
|
51
|
+
if self.mode == 'create' and not (
|
|
52
|
+
self.new_project_id and self.new_project_name
|
|
53
|
+
):
|
|
54
|
+
raise ValueError('create mode requires new project id and name')
|
|
55
|
+
return self
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class KnowledgeProjectActionRequest(StrictModel):
|
|
59
|
+
personal_space_id: str = Field(min_length=1, max_length=128)
|
|
60
|
+
item_kind: Literal['entity'] = 'entity'
|
|
61
|
+
mode: KnowledgeProjectActionMode
|
|
62
|
+
target_project_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
63
|
+
new_project_id: str | None = Field(default=None, min_length=1, max_length=128)
|
|
64
|
+
new_project_name: str | None = Field(default=None, min_length=1, max_length=160)
|
|
65
|
+
new_project_purpose: str | None = Field(default=None, max_length=4096)
|
|
66
|
+
keep_source_relation: bool = True
|
|
67
|
+
relation_type: ProjectRelationType = 'RELATED_TO'
|
|
68
|
+
conflict_resolution: KnowledgeConflictResolution = 'defer'
|
|
69
|
+
reason: str = Field(min_length=1, max_length=2000)
|
|
70
|
+
operation_actor: KnowledgeOperationActor = 'agent'
|
|
71
|
+
|
|
72
|
+
@model_validator(mode='after')
|
|
73
|
+
def validate_target(self):
|
|
74
|
+
if self.mode == 'existing' and not self.target_project_id:
|
|
75
|
+
raise ValueError('existing mode requires target_project_id')
|
|
76
|
+
if self.mode == 'create' and not (
|
|
77
|
+
self.new_project_id and self.new_project_name
|
|
78
|
+
):
|
|
79
|
+
raise ValueError('create mode requires new project id and name')
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class KnowledgeProjectMatch(StrictModel):
|
|
84
|
+
kind: KnowledgeProjectMatchKind
|
|
85
|
+
reason: str
|
|
86
|
+
item_id: str | None = None
|
|
87
|
+
item_name: str | None = None
|
|
88
|
+
item_summary: str | None = None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class KnowledgeProjectPreviewRecord(StrictModel):
|
|
92
|
+
item_id: str
|
|
93
|
+
item_name: str
|
|
94
|
+
item_summary: str
|
|
95
|
+
source_project_id: str | None = None
|
|
96
|
+
target_project_id: str
|
|
97
|
+
match: KnowledgeProjectMatch
|
|
98
|
+
default_resolution: KnowledgeConflictResolution = 'defer'
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class KnowledgeProjectReferenceRecord(StrictModel):
|
|
102
|
+
id: str
|
|
103
|
+
item_id: str
|
|
104
|
+
item_kind: KnowledgeItemKind
|
|
105
|
+
project_id: str
|
|
106
|
+
source_project_id: str | None = None
|
|
107
|
+
status: KnowledgeProjectReferenceStatus
|
|
108
|
+
matched_item_id: str | None = None
|
|
109
|
+
reason: str
|
|
110
|
+
created_at: datetime
|
|
111
|
+
updated_at: datetime
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class KnowledgeConflictRecord(StrictModel):
|
|
115
|
+
id: str
|
|
116
|
+
item_id: str
|
|
117
|
+
target_item_id: str
|
|
118
|
+
source_project_id: str | None = None
|
|
119
|
+
target_project_id: str
|
|
120
|
+
status: Literal['pending', 'resolved']
|
|
121
|
+
resolution: KnowledgeConflictResolution
|
|
122
|
+
reason: str
|
|
123
|
+
created_at: datetime
|
|
124
|
+
updated_at: datetime
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class KnowledgeProjectActionResult(StrictModel):
|
|
128
|
+
status: Literal[
|
|
129
|
+
'created',
|
|
130
|
+
'linked',
|
|
131
|
+
'already_linked',
|
|
132
|
+
'duplicate_reused',
|
|
133
|
+
'conflict_pending',
|
|
134
|
+
'conflict_resolved',
|
|
135
|
+
]
|
|
136
|
+
source_project_id: str | None = None
|
|
137
|
+
target_project_id: str
|
|
138
|
+
project_created: bool = False
|
|
139
|
+
project_relation_created: bool = False
|
|
140
|
+
match: KnowledgeProjectMatch
|
|
141
|
+
reference: KnowledgeProjectReferenceRecord | None = None
|
|
142
|
+
conflict: KnowledgeConflictRecord | None = None
|