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,405 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
|
|
3
|
+
from .graph_models import GraphEdge, GraphNode
|
|
4
|
+
from .models import ProjectProfile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def management_projection(
|
|
8
|
+
space: dict,
|
|
9
|
+
personal_projects: Iterable[dict] = (),
|
|
10
|
+
project_relations: Iterable[dict] = (),
|
|
11
|
+
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
12
|
+
"""Expose Fuli management nodes beside Graphiti knowledge entities."""
|
|
13
|
+
root_id = f"space:{space['id']}"
|
|
14
|
+
root_type = 'ProjectSpace' if space['kind'] == 'project' else 'PersonalSpace'
|
|
15
|
+
fallback = '公共项目空间' if space['kind'] == 'project' else '本机个人知识空间'
|
|
16
|
+
nodes = [GraphNode(
|
|
17
|
+
id=root_id,
|
|
18
|
+
name=space['name'],
|
|
19
|
+
type=root_type,
|
|
20
|
+
group_id=space['group_id'],
|
|
21
|
+
summary=_profile_summary(space.get('profile_json')) or space.get('description') or fallback,
|
|
22
|
+
)]
|
|
23
|
+
edges: list[GraphEdge] = []
|
|
24
|
+
|
|
25
|
+
project_node_ids = {}
|
|
26
|
+
for project in personal_projects:
|
|
27
|
+
project_node_id = f"personal-project:{project['id']}"
|
|
28
|
+
project_node_ids[project.get('project_id', project['id'])] = project_node_id
|
|
29
|
+
nodes.append(GraphNode(
|
|
30
|
+
id=project_node_id,
|
|
31
|
+
name=project['name'],
|
|
32
|
+
type='PersonalProject',
|
|
33
|
+
group_id=space['group_id'],
|
|
34
|
+
summary=_profile_summary(project.get('profile_json')) or '本机个人项目档案',
|
|
35
|
+
attributes={
|
|
36
|
+
'projectId': project.get('project_id', project['id']),
|
|
37
|
+
'publicationKey': project.get('publication_key'),
|
|
38
|
+
},
|
|
39
|
+
))
|
|
40
|
+
edges.append(GraphEdge(
|
|
41
|
+
id=f"contains-project:{project['id']}",
|
|
42
|
+
source=root_id,
|
|
43
|
+
target=project_node_id,
|
|
44
|
+
type='CONTAINS_PROJECT',
|
|
45
|
+
fact=f"个人项目“{project['name']}”保存在该个人空间中。",
|
|
46
|
+
valid_at=None,
|
|
47
|
+
invalid_at=None,
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
for relation in project_relations:
|
|
51
|
+
source_id = project_node_ids.get(relation['source_project_id'])
|
|
52
|
+
target_id = project_node_ids.get(relation['target_project_id'])
|
|
53
|
+
if not source_id or not target_id:
|
|
54
|
+
continue
|
|
55
|
+
edges.append(GraphEdge(
|
|
56
|
+
id=f"personal-project-relation:{relation['id']}",
|
|
57
|
+
source=source_id,
|
|
58
|
+
target=target_id,
|
|
59
|
+
type=relation['relation_type'],
|
|
60
|
+
fact=_relation_fact(relation),
|
|
61
|
+
valid_at=None,
|
|
62
|
+
invalid_at=None,
|
|
63
|
+
))
|
|
64
|
+
return nodes, edges
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def personal_project_projection(
|
|
68
|
+
space: dict,
|
|
69
|
+
project: dict,
|
|
70
|
+
project_relations: Iterable[dict] = (),
|
|
71
|
+
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
72
|
+
"""Expand one local project profile without leaking the rest of the personal graph."""
|
|
73
|
+
profile = ProjectProfile.model_validate_json(project['profile_json'])
|
|
74
|
+
project_id = project['id']
|
|
75
|
+
root_id = f'personal-project:{project_id}'
|
|
76
|
+
group_id = space['group_id']
|
|
77
|
+
nodes = [GraphNode(
|
|
78
|
+
id=root_id,
|
|
79
|
+
name=profile.name,
|
|
80
|
+
type='PersonalProject',
|
|
81
|
+
group_id=group_id,
|
|
82
|
+
summary=profile.purpose or profile.technical_summary or profile.scope or '本机个人项目档案',
|
|
83
|
+
attributes={
|
|
84
|
+
'projectId': project['project_id'],
|
|
85
|
+
'lifecycle': profile.lifecycle,
|
|
86
|
+
'publicationKey': project.get('publication_key'),
|
|
87
|
+
},
|
|
88
|
+
)]
|
|
89
|
+
edges: list[GraphEdge] = []
|
|
90
|
+
|
|
91
|
+
related_node_ids = set()
|
|
92
|
+
for relation in project_relations:
|
|
93
|
+
current_project_id = project['project_id']
|
|
94
|
+
if current_project_id not in {
|
|
95
|
+
relation['source_project_id'], relation['target_project_id']
|
|
96
|
+
}:
|
|
97
|
+
continue
|
|
98
|
+
current_is_source = relation['source_project_id'] == current_project_id
|
|
99
|
+
related_project_id = (
|
|
100
|
+
relation['target_project_id'] if current_is_source
|
|
101
|
+
else relation['source_project_id']
|
|
102
|
+
)
|
|
103
|
+
related_name = relation['target_name'] if current_is_source else relation['source_name']
|
|
104
|
+
related_id = f'personal-project-related:{related_project_id}'
|
|
105
|
+
if related_id not in related_node_ids:
|
|
106
|
+
related_node_ids.add(related_id)
|
|
107
|
+
nodes.append(GraphNode(
|
|
108
|
+
id=related_id,
|
|
109
|
+
name=related_name,
|
|
110
|
+
type='RelatedPersonalProject',
|
|
111
|
+
group_id=group_id,
|
|
112
|
+
summary='与当前个人项目显式关联的项目。',
|
|
113
|
+
attributes={'projectId': related_project_id},
|
|
114
|
+
))
|
|
115
|
+
edges.append(GraphEdge(
|
|
116
|
+
id=f"personal-project-relation:{relation['id']}",
|
|
117
|
+
source=root_id if current_is_source else related_id,
|
|
118
|
+
target=related_id if current_is_source else root_id,
|
|
119
|
+
type=relation['relation_type'],
|
|
120
|
+
fact=_relation_fact(relation),
|
|
121
|
+
valid_at=None,
|
|
122
|
+
invalid_at=None,
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
fields = (
|
|
126
|
+
('purpose', '项目目标', 'ProjectPurpose', profile.purpose, 'HAS_PURPOSE'),
|
|
127
|
+
('scope', '项目范围', 'ProjectScope', profile.scope, 'HAS_SCOPE'),
|
|
128
|
+
(
|
|
129
|
+
'technical-summary', '技术说明', 'TechnicalSummary',
|
|
130
|
+
profile.technical_summary, 'HAS_TECHNICAL_SUMMARY',
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
for key, name, node_type, summary, relation in fields:
|
|
134
|
+
if summary:
|
|
135
|
+
_append_profile_node(
|
|
136
|
+
nodes, edges, root_id, group_id, project_id, key,
|
|
137
|
+
name, node_type, summary, relation,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
for source in profile.sources:
|
|
141
|
+
_append_profile_node(
|
|
142
|
+
nodes, edges, root_id, group_id, project_id, f'source:{source.key}',
|
|
143
|
+
source.title, 'ProjectSource', source.summary or source.uri or '已登记项目资料',
|
|
144
|
+
'HAS_SOURCE', attributes={
|
|
145
|
+
'kind': source.kind,
|
|
146
|
+
'uri': source.uri,
|
|
147
|
+
'sensitivity': source.sensitivity,
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for index, boundary in enumerate(profile.boundaries):
|
|
152
|
+
_append_profile_node(
|
|
153
|
+
nodes, edges, root_id, group_id, project_id, f'boundary:{index}',
|
|
154
|
+
_compact_name(boundary), 'ProjectBoundary', boundary, 'GOVERNS',
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if profile.assessment:
|
|
158
|
+
assessment = profile.assessment
|
|
159
|
+
assessment_id = _append_profile_node(
|
|
160
|
+
nodes, edges, root_id, group_id, project_id, 'assessment',
|
|
161
|
+
f'资料覆盖 {assessment.score}', 'ProjectAssessment',
|
|
162
|
+
_assessment_summary(assessment), 'ASSESSED_AS', attributes={
|
|
163
|
+
'score': assessment.score,
|
|
164
|
+
'label': assessment.label,
|
|
165
|
+
'analyzedAt': assessment.analyzed_at.isoformat(),
|
|
166
|
+
'confirmed': assessment.confirmed,
|
|
167
|
+
'inferred': assessment.inferred,
|
|
168
|
+
},
|
|
169
|
+
)
|
|
170
|
+
for dimension in assessment.dimensions:
|
|
171
|
+
_append_profile_node(
|
|
172
|
+
nodes, edges, assessment_id, group_id, project_id,
|
|
173
|
+
f'assessment:{dimension.key}', dimension.label,
|
|
174
|
+
'AssessmentDimension',
|
|
175
|
+
';'.join(dimension.evidence[:3]) or dimension.label,
|
|
176
|
+
'MEASURES', attributes={
|
|
177
|
+
'score': dimension.score,
|
|
178
|
+
'state': dimension.state,
|
|
179
|
+
'evidence': dimension.evidence,
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return nodes, edges
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def coalesce_personal_project_identity(
|
|
187
|
+
project: dict,
|
|
188
|
+
management_nodes: list[GraphNode],
|
|
189
|
+
management_edges: list[GraphEdge],
|
|
190
|
+
knowledge_nodes: list[GraphNode],
|
|
191
|
+
knowledge_edges: list[GraphEdge],
|
|
192
|
+
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
193
|
+
"""Render one project root while preserving its Graphiti evidence and relations."""
|
|
194
|
+
return coalesce_personal_project_identities(
|
|
195
|
+
[project],
|
|
196
|
+
management_nodes,
|
|
197
|
+
management_edges,
|
|
198
|
+
knowledge_nodes,
|
|
199
|
+
knowledge_edges,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def coalesce_personal_project_identities(
|
|
204
|
+
projects: Iterable[dict],
|
|
205
|
+
management_nodes: list[GraphNode],
|
|
206
|
+
management_edges: list[GraphEdge],
|
|
207
|
+
knowledge_nodes: list[GraphNode],
|
|
208
|
+
knowledge_edges: list[GraphEdge],
|
|
209
|
+
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
210
|
+
"""Use one canonical node per personal project in an aggregate personal graph."""
|
|
211
|
+
identities = [_project_identity(project) for project in projects]
|
|
212
|
+
aliases_by_root: dict[str, list[GraphNode]] = {}
|
|
213
|
+
alias_targets: dict[str, str] = {}
|
|
214
|
+
|
|
215
|
+
for node in knowledge_nodes:
|
|
216
|
+
target = _matching_project_root(node, identities)
|
|
217
|
+
if target is None:
|
|
218
|
+
continue
|
|
219
|
+
alias_targets[node.id] = target
|
|
220
|
+
aliases_by_root.setdefault(target, []).append(node)
|
|
221
|
+
|
|
222
|
+
if not alias_targets:
|
|
223
|
+
return management_nodes + knowledge_nodes, management_edges + knowledge_edges
|
|
224
|
+
|
|
225
|
+
nodes = [
|
|
226
|
+
_merge_project_root(node, aliases_by_root[node.id])
|
|
227
|
+
if node.id in aliases_by_root else node
|
|
228
|
+
for node in management_nodes
|
|
229
|
+
]
|
|
230
|
+
nodes.extend(node for node in knowledge_nodes if node.id not in alias_targets)
|
|
231
|
+
|
|
232
|
+
edges = list(management_edges)
|
|
233
|
+
for edge in knowledge_edges:
|
|
234
|
+
source = alias_targets.get(edge.source, edge.source)
|
|
235
|
+
target = alias_targets.get(edge.target, edge.target)
|
|
236
|
+
if source == target:
|
|
237
|
+
continue
|
|
238
|
+
edges.append(edge.model_copy(update={'source': source, 'target': target}))
|
|
239
|
+
return nodes, edges
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _project_identity(project: dict) -> dict[str, str]:
|
|
243
|
+
profile = ProjectProfile.model_validate_json(project['profile_json'])
|
|
244
|
+
return {
|
|
245
|
+
'project_id': project['project_id'],
|
|
246
|
+
'name': profile.name,
|
|
247
|
+
'root_id': f"personal-project:{project['id']}",
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _matching_project_root(node: GraphNode, identities: list[dict[str, str]]) -> str | None:
|
|
252
|
+
if node.type != 'Project':
|
|
253
|
+
return None
|
|
254
|
+
explicit_project_id = node.attributes.get(
|
|
255
|
+
'projectId', node.attributes.get('project_id')
|
|
256
|
+
)
|
|
257
|
+
if explicit_project_id is not None:
|
|
258
|
+
matches = [
|
|
259
|
+
identity for identity in identities
|
|
260
|
+
if identity['project_id'] == str(explicit_project_id)
|
|
261
|
+
]
|
|
262
|
+
else:
|
|
263
|
+
normalized_name = _normalized_project_name(node.name)
|
|
264
|
+
matches = [
|
|
265
|
+
identity for identity in identities
|
|
266
|
+
if _normalized_project_name(identity['name']) == normalized_name
|
|
267
|
+
]
|
|
268
|
+
return matches[0]['root_id'] if len(matches) == 1 else None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _append_profile_node(
|
|
272
|
+
nodes: list[GraphNode],
|
|
273
|
+
edges: list[GraphEdge],
|
|
274
|
+
parent_id: str,
|
|
275
|
+
group_id: str,
|
|
276
|
+
project_id: str,
|
|
277
|
+
key: str,
|
|
278
|
+
name: str,
|
|
279
|
+
node_type: str,
|
|
280
|
+
summary: str,
|
|
281
|
+
relation: str,
|
|
282
|
+
*,
|
|
283
|
+
attributes: dict | None = None,
|
|
284
|
+
) -> str:
|
|
285
|
+
node_id = f'project-profile:{project_id}:{key}'
|
|
286
|
+
nodes.append(GraphNode(
|
|
287
|
+
id=node_id,
|
|
288
|
+
name=name,
|
|
289
|
+
type=node_type,
|
|
290
|
+
group_id=group_id,
|
|
291
|
+
summary=summary,
|
|
292
|
+
attributes={key: value for key, value in (attributes or {}).items() if value is not None},
|
|
293
|
+
))
|
|
294
|
+
edges.append(GraphEdge(
|
|
295
|
+
id=f'project-profile-edge:{project_id}:{key}',
|
|
296
|
+
source=parent_id,
|
|
297
|
+
target=node_id,
|
|
298
|
+
type=relation,
|
|
299
|
+
fact=summary,
|
|
300
|
+
valid_at=None,
|
|
301
|
+
invalid_at=None,
|
|
302
|
+
))
|
|
303
|
+
return node_id
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _merge_project_root(root: GraphNode, aliases: list[GraphNode]) -> GraphNode:
|
|
307
|
+
attributes = {}
|
|
308
|
+
for alias in aliases:
|
|
309
|
+
attributes.update(alias.attributes)
|
|
310
|
+
project_definitions = list(dict.fromkeys(
|
|
311
|
+
alias.summary for alias in aliases if alias.summary
|
|
312
|
+
))
|
|
313
|
+
if project_definitions:
|
|
314
|
+
attributes['projectDefinition'] = ';'.join(project_definitions)
|
|
315
|
+
attributes.update(root.attributes)
|
|
316
|
+
created_values = [
|
|
317
|
+
node.created_at for node in [root, *aliases] if node.created_at is not None
|
|
318
|
+
]
|
|
319
|
+
summary = root.summary
|
|
320
|
+
if summary == '本机个人项目档案':
|
|
321
|
+
summary = next((node.summary for node in aliases if node.summary), summary)
|
|
322
|
+
human_alias = max(
|
|
323
|
+
(node for node in aliases if node.human_edited),
|
|
324
|
+
key=lambda node: str(node.last_human_changed_at or node.created_at or ''),
|
|
325
|
+
default=None,
|
|
326
|
+
)
|
|
327
|
+
return root.model_copy(update={
|
|
328
|
+
'summary': summary,
|
|
329
|
+
'attributes': attributes,
|
|
330
|
+
'evidence': _unique_records(root.evidence, aliases, 'evidence'),
|
|
331
|
+
'revisions': _unique_records(root.revisions, aliases, 'revisions'),
|
|
332
|
+
'assignments': _unique_records(root.assignments, aliases, 'assignments'),
|
|
333
|
+
'project_references': _unique_records(
|
|
334
|
+
root.project_references, aliases, 'project_references'
|
|
335
|
+
),
|
|
336
|
+
'conflicts': _unique_records(root.conflicts, aliases, 'conflicts'),
|
|
337
|
+
'audit_events': _unique_records(root.audit_events, aliases, 'audit_events'),
|
|
338
|
+
'human_edited': human_alias is not None,
|
|
339
|
+
'human_change_status': (
|
|
340
|
+
human_alias.human_change_status if human_alias else 'none'
|
|
341
|
+
),
|
|
342
|
+
'human_change_version': (
|
|
343
|
+
human_alias.human_change_version if human_alias else 0
|
|
344
|
+
),
|
|
345
|
+
'last_human_changed_at': (
|
|
346
|
+
human_alias.last_human_changed_at if human_alias else None
|
|
347
|
+
),
|
|
348
|
+
'last_agent_viewed_at': (
|
|
349
|
+
human_alias.last_agent_viewed_at if human_alias else None
|
|
350
|
+
),
|
|
351
|
+
'last_agent_reviewed_at': (
|
|
352
|
+
human_alias.last_agent_reviewed_at if human_alias else None
|
|
353
|
+
),
|
|
354
|
+
'created_at': min(created_values) if created_values else None,
|
|
355
|
+
'reasoning_summary': root.reasoning_summary or next(
|
|
356
|
+
(node.reasoning_summary for node in aliases if node.reasoning_summary),
|
|
357
|
+
None,
|
|
358
|
+
),
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _unique_records(existing: list, aliases: list[GraphNode], field: str) -> list:
|
|
363
|
+
records = [*existing]
|
|
364
|
+
for alias in aliases:
|
|
365
|
+
records.extend(getattr(alias, field))
|
|
366
|
+
result = []
|
|
367
|
+
seen = set()
|
|
368
|
+
for record in records:
|
|
369
|
+
record_id = getattr(record, 'id', None)
|
|
370
|
+
if record_id in seen:
|
|
371
|
+
continue
|
|
372
|
+
seen.add(record_id)
|
|
373
|
+
result.append(record)
|
|
374
|
+
return result
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def _normalized_project_name(value: str) -> str:
|
|
378
|
+
return ''.join(value.casefold().split())
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _compact_name(value: str) -> str:
|
|
382
|
+
return value if len(value) <= 24 else f'{value[:23]}…'
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _assessment_summary(assessment) -> str:
|
|
386
|
+
recorded = len(assessment.confirmed) + len(assessment.inferred)
|
|
387
|
+
dimensions = sum(1 for dimension in assessment.dimensions if dimension.evidence)
|
|
388
|
+
return f'已记录 {recorded} 项已有信息,覆盖 {dimensions} 个资料维度。'
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _profile_summary(raw_profile: str | None) -> str:
|
|
392
|
+
if not raw_profile:
|
|
393
|
+
return ''
|
|
394
|
+
try:
|
|
395
|
+
profile = ProjectProfile.model_validate_json(raw_profile)
|
|
396
|
+
except ValueError:
|
|
397
|
+
return ''
|
|
398
|
+
return profile.purpose or profile.technical_summary or profile.scope or ''
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _relation_fact(relation: dict) -> str:
|
|
402
|
+
return (
|
|
403
|
+
f"个人项目“{relation['source_name']}”通过 {relation['relation_type']} "
|
|
404
|
+
f"关联“{relation['target_name']}”。"
|
|
405
|
+
)
|