@carlonicora/nestjs-neo4jsonapi 8.11.1 → 8.12.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/dist/agents/community.detector/services/community.detector.service.d.ts +10 -7
- package/dist/agents/community.detector/services/community.detector.service.d.ts.map +1 -1
- package/dist/agents/community.detector/services/community.detector.service.js +26 -31
- package/dist/agents/community.detector/services/community.detector.service.js.map +1 -1
- package/dist/agents/community.summariser/community.summariser.module.d.ts.map +1 -1
- package/dist/agents/community.summariser/community.summariser.module.js +5 -1
- package/dist/agents/community.summariser/community.summariser.module.js.map +1 -1
- package/dist/agents/community.summariser/cron/community.summariser.cron.d.ts +1 -6
- package/dist/agents/community.summariser/cron/community.summariser.cron.d.ts.map +1 -1
- package/dist/agents/community.summariser/cron/community.summariser.cron.js +2 -6
- package/dist/agents/community.summariser/cron/community.summariser.cron.js.map +1 -1
- package/dist/agents/community.summariser/helpers/community-scope-filter.d.ts +3 -5
- package/dist/agents/community.summariser/helpers/community-scope-filter.d.ts.map +1 -1
- package/dist/agents/community.summariser/helpers/community-scope-filter.js +3 -7
- package/dist/agents/community.summariser/helpers/community-scope-filter.js.map +1 -1
- package/dist/agents/community.summariser/processors/community.summariser.processor.d.ts +1 -6
- package/dist/agents/community.summariser/processors/community.summariser.processor.d.ts.map +1 -1
- package/dist/agents/community.summariser/processors/community.summariser.processor.js +2 -6
- package/dist/agents/community.summariser/processors/community.summariser.processor.js.map +1 -1
- package/dist/agents/community.summariser/services/community.summariser.service.js +1 -1
- package/dist/agents/community.summariser/services/community.summariser.service.js.map +1 -1
- package/dist/agents/drift/nodes/followup.node.service.js +1 -1
- package/dist/agents/drift/nodes/followup.node.service.js.map +1 -1
- package/dist/agents/graph/prompts/graph.node.system.prompt.d.ts +19 -2
- package/dist/agents/graph/prompts/graph.node.system.prompt.d.ts.map +1 -1
- package/dist/agents/graph/prompts/graph.node.system.prompt.js +57 -4
- package/dist/agents/graph/prompts/graph.node.system.prompt.js.map +1 -1
- package/dist/agents/graph/services/compact-description.d.ts +76 -0
- package/dist/agents/graph/services/compact-description.d.ts.map +1 -0
- package/dist/agents/graph/services/compact-description.js +178 -0
- package/dist/agents/graph/services/compact-description.js.map +1 -0
- package/dist/agents/graph/services/filter-operators.d.ts +16 -0
- package/dist/agents/graph/services/filter-operators.d.ts.map +1 -0
- package/dist/agents/graph/services/filter-operators.js +35 -0
- package/dist/agents/graph/services/filter-operators.js.map +1 -0
- package/dist/agents/graph/tools/describe-entity.tool.d.ts +57 -1
- package/dist/agents/graph/tools/describe-entity.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/describe-entity.tool.js +121 -59
- package/dist/agents/graph/tools/describe-entity.tool.js.map +1 -1
- package/dist/agents/graph/tools/read-entity.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/read-entity.tool.js +11 -2
- package/dist/agents/graph/tools/read-entity.tool.js.map +1 -1
- package/dist/agents/graph/tools/search-entities.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/search-entities.tool.js +7 -2
- package/dist/agents/graph/tools/search-entities.tool.js.map +1 -1
- package/dist/agents/graph/tools/tool.factory.d.ts +35 -0
- package/dist/agents/graph/tools/tool.factory.d.ts.map +1 -1
- package/dist/agents/graph/tools/tool.factory.js +34 -0
- package/dist/agents/graph/tools/tool.factory.js.map +1 -1
- package/dist/agents/graph/tools/traverse.tool.d.ts +1 -13
- package/dist/agents/graph/tools/traverse.tool.d.ts.map +1 -1
- package/dist/agents/graph/tools/traverse.tool.js +13 -33
- package/dist/agents/graph/tools/traverse.tool.js.map +1 -1
- package/dist/agents/index.d.ts +6 -2
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +10 -2
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/responder/nodes/graph.node.service.d.ts +48 -1
- package/dist/agents/responder/nodes/graph.node.service.d.ts.map +1 -1
- package/dist/agents/responder/nodes/graph.node.service.js +158 -41
- package/dist/agents/responder/nodes/graph.node.service.js.map +1 -1
- package/dist/agents/responder/nodes/responder.answer.node.service.d.ts +1 -1
- package/dist/agents/responder/nodes/responder.answer.node.service.d.ts.map +1 -1
- package/dist/agents/responder/nodes/responder.answer.node.service.js +14 -0
- package/dist/agents/responder/nodes/responder.answer.node.service.js.map +1 -1
- package/dist/config/base.config.d.ts +6 -0
- package/dist/config/base.config.d.ts.map +1 -1
- package/dist/config/base.config.js +1 -0
- package/dist/config/base.config.js.map +1 -1
- package/dist/config/interfaces/base.config.interface.d.ts +2 -0
- package/dist/config/interfaces/base.config.interface.d.ts.map +1 -1
- package/dist/config/interfaces/config.community.interface.d.ts +8 -0
- package/dist/config/interfaces/config.community.interface.d.ts.map +1 -0
- package/dist/config/interfaces/config.community.interface.js +3 -0
- package/dist/config/interfaces/config.community.interface.js.map +1 -0
- package/dist/config/interfaces/config.prompts.interface.d.ts +4 -4
- package/dist/config/interfaces/config.responder.interface.d.ts +7 -6
- package/dist/config/interfaces/config.responder.interface.d.ts.map +1 -1
- package/dist/config/interfaces/index.d.ts +1 -0
- package/dist/config/interfaces/index.d.ts.map +1 -1
- package/dist/config/interfaces/index.js +1 -0
- package/dist/config/interfaces/index.js.map +1 -1
- package/dist/core/llm/services/llm.service.d.ts +26 -0
- package/dist/core/llm/services/llm.service.d.ts.map +1 -1
- package/dist/core/llm/services/llm.service.js +146 -14
- package/dist/core/llm/services/llm.service.js.map +1 -1
- package/dist/foundations/analytics/index.d.ts +1 -0
- package/dist/foundations/analytics/index.d.ts.map +1 -1
- package/dist/foundations/analytics/index.js +3 -1
- package/dist/foundations/analytics/index.js.map +1 -1
- package/dist/foundations/analytics/interfaces/analytics.purge-visitor.input.d.ts +19 -0
- package/dist/foundations/analytics/interfaces/analytics.purge-visitor.input.d.ts.map +1 -0
- package/dist/foundations/analytics/interfaces/analytics.purge-visitor.input.js +6 -0
- package/dist/foundations/analytics/interfaces/analytics.purge-visitor.input.js.map +1 -0
- package/dist/foundations/analytics/processors/analytics.processor.d.ts +10 -1
- package/dist/foundations/analytics/processors/analytics.processor.d.ts.map +1 -1
- package/dist/foundations/analytics/processors/analytics.processor.js +29 -0
- package/dist/foundations/analytics/processors/analytics.processor.js.map +1 -1
- package/dist/foundations/analytics/repositories/analytics-session.repository.d.ts +11 -0
- package/dist/foundations/analytics/repositories/analytics-session.repository.d.ts.map +1 -1
- package/dist/foundations/analytics/repositories/analytics-session.repository.js +37 -0
- package/dist/foundations/analytics/repositories/analytics-session.repository.js.map +1 -1
- package/dist/foundations/analytics/services/analytics.service.d.ts +3 -1
- package/dist/foundations/analytics/services/analytics.service.d.ts.map +1 -1
- package/dist/foundations/analytics/services/analytics.service.js +18 -3
- package/dist/foundations/analytics/services/analytics.service.js.map +1 -1
- package/dist/foundations/community/community.module.d.ts.map +1 -1
- package/dist/foundations/community/community.module.js +5 -4
- package/dist/foundations/community/community.module.js.map +1 -1
- package/dist/foundations/community/entities/community.entity.d.ts +30 -4
- package/dist/foundations/community/entities/community.entity.d.ts.map +1 -1
- package/dist/foundations/community/entities/community.entity.js +51 -0
- package/dist/foundations/community/entities/community.entity.js.map +1 -1
- package/dist/foundations/community/index.d.ts +4 -2
- package/dist/foundations/community/index.d.ts.map +1 -1
- package/dist/foundations/community/index.js +8 -3
- package/dist/foundations/community/index.js.map +1 -1
- package/dist/foundations/community/repositories/community.repository.d.ts +93 -103
- package/dist/foundations/community/repositories/community.repository.d.ts.map +1 -1
- package/dist/foundations/community/repositories/community.repository.js +330 -328
- package/dist/foundations/community/repositories/community.repository.js.map +1 -1
- package/dist/foundations/community/services/community-layer.service.d.ts +38 -0
- package/dist/foundations/community/services/community-layer.service.d.ts.map +1 -0
- package/dist/foundations/community/services/community-layer.service.js +89 -0
- package/dist/foundations/community/services/community-layer.service.js.map +1 -0
- package/dist/foundations/community/services/community.service.d.ts +22 -4
- package/dist/foundations/community/services/community.service.d.ts.map +1 -1
- package/dist/foundations/community/services/community.service.js +9 -6
- package/dist/foundations/community/services/community.service.js.map +1 -1
- package/package.json +1 -1
- package/dist/foundations/community/entities/community.map.d.ts +0 -8
- package/dist/foundations/community/entities/community.map.d.ts.map +0 -1
- package/dist/foundations/community/entities/community.map.js +0 -24
- package/dist/foundations/community/entities/community.map.js.map +0 -1
- package/dist/foundations/community/entities/community.model.d.ts +0 -4
- package/dist/foundations/community/entities/community.model.d.ts.map +0 -1
- package/dist/foundations/community/entities/community.model.js +0 -15
- package/dist/foundations/community/entities/community.model.js.map +0 -1
|
@@ -12,30 +12,45 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
12
12
|
exports.CommunityRepository = exports.COMMUNITY_EXACT_SCAN_MAX_SCOPED_NODES = void 0;
|
|
13
13
|
const common_1 = require("@nestjs/common");
|
|
14
14
|
const crypto_1 = require("crypto");
|
|
15
|
+
const nestjs_cls_1 = require("nestjs-cls");
|
|
15
16
|
const registry_1 = require("../../../common/registries/registry");
|
|
16
17
|
const agent_scope_filter_1 = require("../../../common/repositories/agent-scope.filter");
|
|
17
|
-
const
|
|
18
|
+
const embedder_service_1 = require("../../../core/llm/services/embedder.service");
|
|
19
|
+
const model_service_1 = require("../../../core/llm/services/model.service");
|
|
20
|
+
const abstract_repository_1 = require("../../../core/neo4j/abstracts/abstract.repository");
|
|
18
21
|
const neo4j_service_1 = require("../../../core/neo4j/services/neo4j.service");
|
|
19
22
|
const security_service_1 = require("../../../core/security/services/security.service");
|
|
20
|
-
const
|
|
23
|
+
const company_meta_1 = require("../../company/entities/company.meta");
|
|
24
|
+
const key_concept_meta_1 = require("../../keyconcept/entities/key.concept.meta");
|
|
25
|
+
const key_concept_model_1 = require("../../keyconcept/entities/key.concept.model");
|
|
26
|
+
const community_entity_1 = require("../entities/community.entity");
|
|
27
|
+
const community_meta_1 = require("../entities/community.meta");
|
|
28
|
+
const community_layer_service_1 = require("../services/community-layer.service");
|
|
21
29
|
/**
|
|
22
30
|
* Up to this many company/scope communities, `findByVector` ranks them by
|
|
23
31
|
* exact cosine (no recall cliff). Above it, it falls back to the vector-index
|
|
24
32
|
* over-fetch, which ranks globally and filters afterwards.
|
|
25
33
|
*/
|
|
26
34
|
exports.COMMUNITY_EXACT_SCAN_MAX_SCOPED_NODES = 5000;
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
const toNumber = (value) => value?.toNumber?.() ?? (value === null || value === undefined ? 0 : Number(value));
|
|
36
|
+
/**
|
|
37
|
+
* Topic groups of key concepts, one set per company or per layer root (`CommunityLayerService`).
|
|
38
|
+
*
|
|
39
|
+
* Every match on a Community starts from `buildDefaultMatch()`, so the company predicate is the
|
|
40
|
+
* framework's. With no company in CLS that predicate matches every company: only
|
|
41
|
+
* `findAllStaleCommunities` (the cron) relies on it; every write throws instead.
|
|
42
|
+
*/
|
|
43
|
+
let CommunityRepository = class CommunityRepository extends abstract_repository_1.AbstractRepository {
|
|
44
|
+
constructor(neo4j, securityService, clsService, embedderService, modelService, agentScope, layer) {
|
|
45
|
+
super(neo4j, securityService, clsService);
|
|
30
46
|
this.embedderService = embedderService;
|
|
31
47
|
this.modelService = modelService;
|
|
32
|
-
this.securityService = securityService;
|
|
33
48
|
this.agentScope = agentScope;
|
|
49
|
+
this.layer = layer;
|
|
50
|
+
this.descriptor = community_entity_1.CommunityDescriptor;
|
|
34
51
|
}
|
|
35
52
|
async onModuleInit() {
|
|
36
|
-
await
|
|
37
|
-
query: `CREATE CONSTRAINT community_id IF NOT EXISTS FOR (community:Community) REQUIRE community.id IS UNIQUE`,
|
|
38
|
-
});
|
|
53
|
+
await super.onModuleInit();
|
|
39
54
|
const dimensions = this.modelService.getEmbedderDimensions();
|
|
40
55
|
await this.neo4j.writeOne({
|
|
41
56
|
query: `
|
|
@@ -49,30 +64,36 @@ let CommunityRepository = class CommunityRepository {
|
|
|
49
64
|
`,
|
|
50
65
|
});
|
|
51
66
|
}
|
|
67
|
+
/** A query on the Community serialiser, refused when no company is in context (writes). */
|
|
68
|
+
writeQuery(method) {
|
|
69
|
+
const query = this.neo4j.initQuery({ serialiser: community_entity_1.CommunityDescriptor.model });
|
|
70
|
+
if (!query.queryParams.companyId)
|
|
71
|
+
throw new Error(`CommunityRepository.${method}: no company in context`);
|
|
72
|
+
return query;
|
|
73
|
+
}
|
|
74
|
+
readQuery() {
|
|
75
|
+
return this.neo4j.initQuery({ serialiser: community_entity_1.CommunityDescriptor.model });
|
|
76
|
+
}
|
|
52
77
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*/
|
|
56
|
-
/**
|
|
57
|
-
* `scopeRoot` ties the community to the scope root its members were detected
|
|
58
|
-
* within. Communities are built per scope root precisely so a scoped run can
|
|
59
|
-
* be answered from communities that contain nothing from a sibling root;
|
|
60
|
-
* without the edge, `findByVector` has no way to tell them apart and the
|
|
61
|
-
* whole partition is decorative.
|
|
78
|
+
* `scopeRoot` ties the group to the layer root its members were detected within. Its `label`
|
|
79
|
+
* is ignored: the label comes from the layer. A `scopeRoot` with no layer configured throws.
|
|
62
80
|
*/
|
|
63
81
|
async createCommunity(params) {
|
|
64
|
-
const query = this.
|
|
65
|
-
const
|
|
82
|
+
const query = this.writeQuery("createCommunity");
|
|
83
|
+
const layer = await this.layer.ready();
|
|
84
|
+
if (params.scopeRoot && !layer)
|
|
85
|
+
throw new Error("CommunityRepository.createCommunity: scopeRoot passed but no community layer is configured");
|
|
86
|
+
if (params.scopeRoot && layer)
|
|
87
|
+
await this.neo4j.validateRelatedNodes({ nodes: [{ id: params.scopeRoot.id, label: layer.labelName }] });
|
|
66
88
|
query.queryParams = {
|
|
67
89
|
...query.queryParams,
|
|
68
|
-
id,
|
|
90
|
+
id: (0, crypto_1.randomUUID)(),
|
|
69
91
|
name: params.name,
|
|
70
92
|
level: params.level,
|
|
71
93
|
memberCount: params.memberCount,
|
|
72
94
|
rating: params.rating ?? 0,
|
|
73
95
|
scopeRootId: params.scopeRoot?.id ?? null,
|
|
74
96
|
};
|
|
75
|
-
// Guarded: initQuery() binds this variable only when the id is in CLS; an unbound CREATE target creates an orphan node.
|
|
76
97
|
query.query += `
|
|
77
98
|
CREATE (community:Community {
|
|
78
99
|
id: $id,
|
|
@@ -85,242 +106,213 @@ let CommunityRepository = class CommunityRepository {
|
|
|
85
106
|
createdAt: datetime(),
|
|
86
107
|
updatedAt: datetime()
|
|
87
108
|
})
|
|
88
|
-
|
|
89
|
-
${params.scopeRoot
|
|
109
|
+
CREATE (community)-[:BELONGS_TO]->(company)
|
|
110
|
+
${params.scopeRoot && layer
|
|
90
111
|
? `WITH community
|
|
91
|
-
MATCH (scopeRoot:${
|
|
112
|
+
MATCH (scopeRoot:${layer.labelName} { id: $scopeRootId })
|
|
92
113
|
CREATE (community)-[:SCOPED_TO]->(scopeRoot)`
|
|
93
114
|
: ``}
|
|
94
115
|
RETURN community
|
|
95
116
|
`;
|
|
96
117
|
return this.neo4j.writeOne(query);
|
|
97
118
|
}
|
|
98
|
-
/**
|
|
99
|
-
* Update community members (HAS_MEMBER relationships to KeyConcepts)
|
|
100
|
-
*/
|
|
119
|
+
/** Replaces the group's HAS_MEMBER edges. */
|
|
101
120
|
async updateCommunityMembers(params) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
removeQuery.queryParams = { ...removeQuery.queryParams, communityId: params.communityId };
|
|
121
|
+
const removeQuery = this.writeQuery("updateCommunityMembers");
|
|
122
|
+
removeQuery.queryParams = { ...removeQuery.queryParams, searchValue: params.communityId };
|
|
105
123
|
removeQuery.query += `
|
|
106
|
-
|
|
124
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
125
|
+
MATCH (community)-[r:HAS_MEMBER]->()
|
|
107
126
|
DELETE r
|
|
108
127
|
`;
|
|
128
|
+
if (params.keyConceptIds.length > 0)
|
|
129
|
+
await this.neo4j.validateRelatedNodes({
|
|
130
|
+
nodes: params.keyConceptIds.map((id) => ({ id, label: key_concept_meta_1.keyConceptMeta.labelName })),
|
|
131
|
+
});
|
|
109
132
|
await this.neo4j.writeOne(removeQuery);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
...updateCountQuery.queryParams,
|
|
128
|
-
communityId: params.communityId,
|
|
129
|
-
memberCount: params.keyConceptIds.length,
|
|
130
|
-
};
|
|
131
|
-
updateCountQuery.query += `
|
|
132
|
-
MATCH (community:Community {id: $communityId})
|
|
133
|
-
SET community.memberCount = $memberCount
|
|
134
|
-
`;
|
|
135
|
-
await this.neo4j.writeOne(updateCountQuery);
|
|
136
|
-
}
|
|
133
|
+
if (params.keyConceptIds.length === 0)
|
|
134
|
+
return;
|
|
135
|
+
const addQuery = this.writeQuery("updateCommunityMembers");
|
|
136
|
+
addQuery.queryParams = {
|
|
137
|
+
...addQuery.queryParams,
|
|
138
|
+
searchValue: params.communityId,
|
|
139
|
+
keyConceptIds: params.keyConceptIds,
|
|
140
|
+
memberCount: params.keyConceptIds.length,
|
|
141
|
+
};
|
|
142
|
+
addQuery.query += `
|
|
143
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
144
|
+
SET community.memberCount = $memberCount
|
|
145
|
+
WITH community
|
|
146
|
+
MATCH (keyconcept:KeyConcept) WHERE keyconcept.id IN $keyConceptIds
|
|
147
|
+
MERGE (community)-[:HAS_MEMBER]->(keyconcept)
|
|
148
|
+
`;
|
|
149
|
+
await this.neo4j.writeOne(addQuery);
|
|
137
150
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Set PARENT_OF relationship between communities (hierarchy)
|
|
140
|
-
*/
|
|
151
|
+
/** PARENT_OF between two groups of the same company. */
|
|
141
152
|
async setParentCommunity(params) {
|
|
142
|
-
const query = this.
|
|
153
|
+
const query = this.writeQuery("setParentCommunity");
|
|
154
|
+
await this.neo4j.validateRelatedNodes({
|
|
155
|
+
nodes: [{ id: params.parentCommunityId, label: community_meta_1.communityMeta.labelName }],
|
|
156
|
+
});
|
|
143
157
|
query.queryParams = {
|
|
144
158
|
...query.queryParams,
|
|
145
|
-
|
|
159
|
+
searchValue: params.childCommunityId,
|
|
146
160
|
parentCommunityId: params.parentCommunityId,
|
|
147
161
|
};
|
|
148
162
|
query.query += `
|
|
163
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
149
164
|
MATCH (parent:Community {id: $parentCommunityId})
|
|
150
|
-
MATCH (
|
|
151
|
-
MERGE (parent)-[:PARENT_OF]->(
|
|
165
|
+
WHERE EXISTS { MATCH (parent)-[:BELONGS_TO]-(company) }
|
|
166
|
+
MERGE (parent)-[:PARENT_OF]->(community)
|
|
152
167
|
`;
|
|
153
168
|
await this.neo4j.writeOne(query);
|
|
154
169
|
}
|
|
155
|
-
/**
|
|
156
|
-
* Mark communities as stale (needs reprocessing)
|
|
157
|
-
*/
|
|
170
|
+
/** Marks groups as stale (needs reprocessing). */
|
|
158
171
|
async markAsStale(communityIds) {
|
|
159
172
|
if (communityIds.length === 0)
|
|
160
173
|
return;
|
|
161
|
-
const query = this.
|
|
162
|
-
query.queryParams = {
|
|
163
|
-
...query.queryParams,
|
|
164
|
-
communityIds,
|
|
165
|
-
};
|
|
174
|
+
const query = this.writeQuery("markAsStale");
|
|
175
|
+
query.queryParams = { ...query.queryParams, communityIds };
|
|
166
176
|
query.query += `
|
|
167
|
-
|
|
177
|
+
${this.buildDefaultMatch()}
|
|
178
|
+
WHERE community.id IN $communityIds
|
|
168
179
|
SET community.isStale = true, community.staleSince = datetime(), community.updatedAt = datetime()
|
|
169
180
|
`;
|
|
170
181
|
await this.neo4j.writeOne(query);
|
|
171
182
|
}
|
|
172
183
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* clears the flag (spec §2: a fresh top-up must not be silently eaten).
|
|
184
|
+
* Defers a group for lack of credits: the summariser cron skips it (`findAllStaleCommunities`)
|
|
185
|
+
* until approval clears the flag (spec §2: a fresh top-up must not be silently eaten).
|
|
176
186
|
*/
|
|
177
187
|
async markPendingCredits(communityId) {
|
|
178
|
-
const query = this.
|
|
179
|
-
query.queryParams = { ...query.queryParams, communityId };
|
|
188
|
+
const query = this.writeQuery("markPendingCredits");
|
|
189
|
+
query.queryParams = { ...query.queryParams, searchValue: communityId };
|
|
180
190
|
query.query += `
|
|
181
|
-
|
|
191
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
182
192
|
SET community.pendingCredits = true, community.updatedAt = datetime()
|
|
183
193
|
`;
|
|
184
194
|
await this.neo4j.writeOne(query);
|
|
185
195
|
}
|
|
186
|
-
/**
|
|
187
|
-
* Approval: clear the flag for the current company (from CLS); returns the
|
|
188
|
-
* cleared community ids so the caller can re-enqueue them.
|
|
189
|
-
*/
|
|
196
|
+
/** Approval: clears the flag for the current company; returns the cleared ids so the caller can re-enqueue them. */
|
|
190
197
|
async clearPendingCredits() {
|
|
191
|
-
|
|
192
|
-
// go through `neo4j.read()` (Neo4j rejects writes in a read transaction:
|
|
193
|
-
// "Writing in read access mode not allowed"), and the public write helpers
|
|
194
|
-
// only return rows when given a serialiser — which this projection has no
|
|
195
|
-
// use for.
|
|
196
|
-
const readQuery = this.neo4j.initQuery();
|
|
198
|
+
const readQuery = this.writeQuery("clearPendingCredits");
|
|
197
199
|
readQuery.query += `
|
|
198
|
-
|
|
199
|
-
|
|
200
|
+
${this.buildDefaultMatch()}
|
|
201
|
+
WHERE community.pendingCredits = true
|
|
202
|
+
RETURN community
|
|
200
203
|
`;
|
|
201
|
-
const
|
|
202
|
-
const communityIds =
|
|
204
|
+
const pending = await this.neo4j.readMany(readQuery);
|
|
205
|
+
const communityIds = pending.map((community) => community.id);
|
|
203
206
|
if (communityIds.length === 0)
|
|
204
207
|
return [];
|
|
205
|
-
const writeQuery = this.
|
|
208
|
+
const writeQuery = this.writeQuery("clearPendingCredits");
|
|
206
209
|
writeQuery.queryParams = { ...writeQuery.queryParams, communityIds };
|
|
207
210
|
writeQuery.query += `
|
|
208
|
-
|
|
211
|
+
${this.buildDefaultMatch()}
|
|
209
212
|
WHERE community.id IN $communityIds
|
|
210
213
|
SET community.pendingCredits = false, community.updatedAt = datetime()
|
|
211
214
|
`;
|
|
212
215
|
await this.neo4j.writeOne(writeQuery);
|
|
213
216
|
return communityIds;
|
|
214
217
|
}
|
|
215
|
-
/**
|
|
216
|
-
* Backlog count for the current company (from CLS) — mirrors countStaleCommunities.
|
|
217
|
-
*/
|
|
218
|
+
/** Backlog count for the current company (mirrors countStaleCommunities). */
|
|
218
219
|
async countPendingCredits() {
|
|
219
|
-
const query = this.
|
|
220
|
+
const query = this.readQuery();
|
|
220
221
|
query.query += `
|
|
221
|
-
|
|
222
|
+
${this.buildDefaultMatch()}
|
|
223
|
+
WHERE community.pendingCredits = true
|
|
222
224
|
RETURN COUNT(community) AS count
|
|
223
225
|
`;
|
|
224
|
-
const result = await this.neo4j.read(query.query, query.queryParams);
|
|
225
|
-
|
|
226
|
-
return count?.toNumber?.() ?? count ?? 0;
|
|
226
|
+
const result = await this.neo4j.read(query.query, query.queryParams); // nja-lint-ignore: scalar read, no node to map (Carlo, 2026-10-28, WYR-10 spec D4)
|
|
227
|
+
return toNumber(result.records[0]?.get("count"));
|
|
227
228
|
}
|
|
228
|
-
/**
|
|
229
|
-
* Count stale communities for the current company (from CLS)
|
|
230
|
-
*/
|
|
229
|
+
/** Stale groups of the current company. */
|
|
231
230
|
async countStaleCommunities() {
|
|
232
|
-
const query = this.
|
|
231
|
+
const query = this.readQuery();
|
|
233
232
|
query.query += `
|
|
234
|
-
|
|
233
|
+
${this.buildDefaultMatch()}
|
|
234
|
+
WHERE community.isStale = true
|
|
235
235
|
RETURN COUNT(community) AS count
|
|
236
236
|
`;
|
|
237
|
-
const result = await this.neo4j.read(query.query, query.queryParams);
|
|
238
|
-
|
|
239
|
-
return count?.toNumber?.() ?? count ?? 0;
|
|
237
|
+
const result = await this.neo4j.read(query.query, query.queryParams); // nja-lint-ignore: scalar read, no node to map (Carlo, 2026-10-28, WYR-10 spec D4)
|
|
238
|
+
return toNumber(result.records[0]?.get("count"));
|
|
240
239
|
}
|
|
241
240
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
241
|
+
* Stale groups with their company ids, for the cron. Runs with no company in CLS, so the
|
|
242
|
+
* default match spans every company (spec D5: no admin repository).
|
|
244
243
|
*/
|
|
245
244
|
async findAllStaleCommunities() {
|
|
246
|
-
const query = this.
|
|
245
|
+
const query = this.readQuery();
|
|
247
246
|
query.query += `
|
|
248
|
-
|
|
249
|
-
WHERE community.
|
|
250
|
-
|
|
247
|
+
${this.buildDefaultMatch()}
|
|
248
|
+
WHERE community.isStale = true
|
|
249
|
+
AND (community.pendingCredits IS NULL OR community.pendingCredits = false)
|
|
250
|
+
MATCH (community)-[:BELONGS_TO]->(${community_meta_1.communityMeta.nodeName}_${company_meta_1.companyMeta.nodeName}:${company_meta_1.companyMeta.labelName})
|
|
251
|
+
RETURN community, ${community_meta_1.communityMeta.nodeName}_${company_meta_1.companyMeta.nodeName}
|
|
251
252
|
ORDER BY community.staleSince ASC
|
|
252
253
|
`;
|
|
253
|
-
const
|
|
254
|
-
return
|
|
255
|
-
communityId: record.get("communityId"),
|
|
256
|
-
companyId: record.get("companyId"),
|
|
257
|
-
}));
|
|
254
|
+
const communities = await this.neo4j.readMany(query);
|
|
255
|
+
return communities.map((community) => ({ communityId: community.id, companyId: community.company?.id }));
|
|
258
256
|
}
|
|
259
257
|
/**
|
|
260
|
-
* The
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* scope root among them, or no company is in CLS. Both the community and the
|
|
265
|
-
* root must belong to the CLS company.
|
|
258
|
+
* The layer root a group was detected under (its SCOPED_TO target), for the current company.
|
|
259
|
+
* `null` on the company layer, with no company in CLS, or when the group has no root.
|
|
260
|
+
*
|
|
261
|
+
* @deprecated rootLabels — accepted and ignored: the layer comes from CommunityLayerService.
|
|
266
262
|
*/
|
|
267
263
|
async findScopeRootForCommunity(params) {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
return null;
|
|
264
|
+
const layer = await this.layer.ready();
|
|
265
|
+
if (!layer)
|
|
266
|
+
return null;
|
|
267
|
+
const model = registry_1.modelRegistry.getByLabelName(layer.labelName);
|
|
268
|
+
if (!model)
|
|
269
|
+
return null;
|
|
270
|
+
const query = this.neo4j.initQuery({ serialiser: model });
|
|
271
|
+
if (!query.queryParams.companyId)
|
|
272
|
+
return null;
|
|
273
|
+
query.queryParams = { ...query.queryParams, searchValue: params.communityId };
|
|
274
|
+
query.query += `
|
|
275
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
276
|
+
MATCH (community)-[:SCOPED_TO]->(${layer.nodeName}:${layer.labelName})
|
|
277
|
+
WHERE EXISTS { MATCH (${layer.nodeName})-[:BELONGS_TO]-(company) }
|
|
278
|
+
RETURN ${layer.nodeName}
|
|
279
|
+
`;
|
|
280
|
+
const root = await this.neo4j.readOne(query);
|
|
281
|
+
return root?.id ? { id: root.id, type: layer.type, label: layer.labelName } : null;
|
|
286
282
|
}
|
|
287
283
|
/**
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
* are left alone. Does nothing when no company is in CLS.
|
|
284
|
+
* Deletes every group detected under one layer root, for the current company. A scope of
|
|
285
|
+
* another type than the layer (or no layer) does nothing; the label comes from the layer.
|
|
291
286
|
*/
|
|
292
287
|
async deleteCommunitiesForScope(params) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (!model)
|
|
296
|
-
return;
|
|
297
|
-
const query = this.neo4j.initQuery({ serialiser: community_model_1.CommunityModel });
|
|
288
|
+
const query = this.readQuery();
|
|
289
|
+
// No company in CLS: refuse with a no-op, never a delete across every company.
|
|
298
290
|
if (!query.queryParams.companyId)
|
|
299
291
|
return;
|
|
292
|
+
const layer = await this.layer.ready();
|
|
293
|
+
if (!layer || params.scope.type !== layer.type)
|
|
294
|
+
return;
|
|
300
295
|
query.queryParams = { ...query.queryParams, scopeRootId: params.scope.id };
|
|
301
296
|
query.query += `
|
|
302
|
-
|
|
303
|
-
MATCH (community
|
|
304
|
-
MATCH (community)-[:BELONGS_TO]->(company)
|
|
297
|
+
${this.buildDefaultMatch()}
|
|
298
|
+
WHERE EXISTS { MATCH (community)-[:SCOPED_TO]->(:${layer.labelName} { id: $scopeRootId }) }
|
|
305
299
|
DETACH DELETE community
|
|
306
300
|
`;
|
|
307
301
|
await this.neo4j.writeOne(query);
|
|
308
302
|
}
|
|
309
|
-
/**
|
|
310
|
-
* Update community summary, embedding, and mark as not stale
|
|
311
|
-
*/
|
|
303
|
+
/** Writes the summary and embedding and clears the stale flag. */
|
|
312
304
|
async updateSummary(params) {
|
|
313
|
-
const query = this.
|
|
305
|
+
const query = this.writeQuery("updateSummary");
|
|
314
306
|
query.queryParams = {
|
|
315
307
|
...query.queryParams,
|
|
316
|
-
|
|
308
|
+
searchValue: params.communityId,
|
|
317
309
|
name: params.name,
|
|
318
310
|
summary: params.summary,
|
|
319
311
|
embedding: params.embedding,
|
|
320
312
|
rating: params.rating,
|
|
321
313
|
};
|
|
322
314
|
query.query += `
|
|
323
|
-
|
|
315
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
324
316
|
SET community.name = $name,
|
|
325
317
|
community.summary = $summary,
|
|
326
318
|
community.embedding = $embedding,
|
|
@@ -333,54 +325,64 @@ let CommunityRepository = class CommunityRepository {
|
|
|
333
325
|
`;
|
|
334
326
|
await this.neo4j.writeOne(query);
|
|
335
327
|
}
|
|
328
|
+
/** Clears the stale flag without summarising (credit backlog discard). */
|
|
329
|
+
async clearStaleness(params) {
|
|
330
|
+
if (params.communityIds.length === 0)
|
|
331
|
+
return;
|
|
332
|
+
const query = this.writeQuery("clearStaleness");
|
|
333
|
+
query.queryParams = { ...query.queryParams, communityIds: params.communityIds };
|
|
334
|
+
query.query += `
|
|
335
|
+
${this.buildDefaultMatch()}
|
|
336
|
+
WHERE community.id IN $communityIds
|
|
337
|
+
SET community.isStale = false, community.staleSince = null, community.updatedAt = datetime()
|
|
338
|
+
`;
|
|
339
|
+
await this.neo4j.writeOne(query);
|
|
340
|
+
}
|
|
336
341
|
/**
|
|
337
|
-
*
|
|
338
|
-
*
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
*
|
|
342
|
-
* single unscoped hit hands the answer content from every scope root it drew
|
|
343
|
-
* on. In a scoped run only communities detected within that exact root are
|
|
344
|
-
* eligible; a community with no `SCOPED_TO` edge predates the partition and
|
|
345
|
-
* is therefore assumed to mix roots.
|
|
342
|
+
* Vector search over the current company's groups.
|
|
343
|
+
*
|
|
344
|
+
* FAIL CLOSED on scope. A group summarises many members at once, so in a scoped chat only
|
|
345
|
+
* groups detected within that exact root are eligible. A chat scoped to a type other than the
|
|
346
|
+
* layer (or any scoped chat on the company layer) gets nothing: its groups mix roots.
|
|
346
347
|
*/
|
|
347
348
|
async findByVector(params) {
|
|
348
349
|
const scope = this.agentScope.current();
|
|
349
|
-
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
350
|
+
const layer = await this.layer.ready();
|
|
351
|
+
if (scope && (!layer || scope.type !== layer.type))
|
|
352
|
+
return [];
|
|
353
|
+
const countQuery = this.readQuery();
|
|
354
|
+
if (!countQuery.queryParams.companyId)
|
|
355
|
+
return [];
|
|
356
|
+
const conditions = ["community.embedding IS NOT NULL", "community.isStale = false"];
|
|
357
|
+
if (params.level !== undefined)
|
|
358
|
+
conditions.push("community.level = $level");
|
|
359
|
+
if (scope && layer)
|
|
360
|
+
conditions.push(`EXISTS { MATCH (community)-[:SCOPED_TO]->(:${layer.labelName} { id: $agentScopeId }) }`);
|
|
356
361
|
const filterParams = {
|
|
357
362
|
embedding: params.embedding,
|
|
358
363
|
topK: params.topK,
|
|
359
364
|
level: params.level,
|
|
360
365
|
...(scope ? { agentScopeId: scope.id } : {}),
|
|
361
366
|
};
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
// candidates. Small sets are ranked by exact cosine; the index only above
|
|
366
|
-
// the threshold (mirrors ChunkRepository.findPotentialChunks).
|
|
367
|
-
const countQuery = this.neo4j.initQuery();
|
|
367
|
+
// The vector index ranks ALL companies first and filters afterwards, so identical content
|
|
368
|
+
// in other companies can push this company's groups out of the candidates. Small sets are
|
|
369
|
+
// ranked by exact cosine; the index only above the threshold (mirrors ChunkRepository).
|
|
368
370
|
countQuery.queryParams = { ...countQuery.queryParams, ...filterParams };
|
|
369
371
|
countQuery.query += `
|
|
370
|
-
|
|
371
|
-
${
|
|
372
|
+
${this.buildDefaultMatch()}
|
|
373
|
+
WHERE ${conditions.join(" AND ")}
|
|
372
374
|
RETURN count(community) AS scopedCount
|
|
373
375
|
`;
|
|
374
|
-
const countResult = await this.neo4j.read(countQuery.query, countQuery.queryParams);
|
|
375
|
-
const scopedCount =
|
|
376
|
+
const countResult = await this.neo4j.read(countQuery.query, countQuery.queryParams); // nja-lint-ignore: scalar read, no node to map (Carlo, 2026-10-28, WYR-10 spec D4)
|
|
377
|
+
const scopedCount = toNumber(countResult?.records?.[0]?.get("scopedCount"));
|
|
376
378
|
if (scopedCount === 0)
|
|
377
379
|
return [];
|
|
378
|
-
const query = this.
|
|
380
|
+
const query = this.readQuery();
|
|
379
381
|
query.queryParams = { ...query.queryParams, ...filterParams };
|
|
380
382
|
if (scopedCount <= exports.COMMUNITY_EXACT_SCAN_MAX_SCOPED_NODES) {
|
|
381
383
|
query.query += `
|
|
382
|
-
|
|
383
|
-
${
|
|
384
|
+
${this.buildDefaultMatch()}
|
|
385
|
+
WHERE ${conditions.join(" AND ")}
|
|
384
386
|
WITH community, vector.similarity.cosine(community.embedding, $embedding) AS score
|
|
385
387
|
RETURN community, score
|
|
386
388
|
ORDER BY score DESC
|
|
@@ -388,68 +390,58 @@ let CommunityRepository = class CommunityRepository {
|
|
|
388
390
|
`;
|
|
389
391
|
return this.neo4j.readMany(query);
|
|
390
392
|
}
|
|
393
|
+
// nja-lint-ignore: vector index CALL precedes the default match (Carlo, 2026-10-28, WYR-10 O3)
|
|
391
394
|
query.query += `
|
|
392
395
|
CALL db.index.vector.queryNodes('communities', $topK * 2, $embedding)
|
|
393
396
|
YIELD node AS community, score
|
|
394
|
-
MATCH (community)-[:BELONGS_TO]
|
|
395
|
-
${whereClause}
|
|
397
|
+
WHERE EXISTS { MATCH (community)-[:BELONGS_TO]-(company) } AND ${conditions.join(" AND ")}
|
|
396
398
|
RETURN community, score
|
|
397
399
|
ORDER BY score DESC
|
|
398
400
|
LIMIT toInteger($topK)
|
|
399
401
|
`;
|
|
400
402
|
return this.neo4j.readMany(query);
|
|
401
403
|
}
|
|
402
|
-
/**
|
|
403
|
-
* Find communities by hierarchy level
|
|
404
|
-
* Filters by company from CLS context
|
|
405
|
-
*/
|
|
404
|
+
/** Groups of one hierarchy level, for the current company. */
|
|
406
405
|
async findByLevel(params) {
|
|
407
|
-
const query = this.
|
|
408
|
-
query.queryParams = {
|
|
409
|
-
...query.queryParams,
|
|
410
|
-
level: params.level,
|
|
411
|
-
};
|
|
406
|
+
const query = this.readQuery();
|
|
407
|
+
query.queryParams = { ...query.queryParams, level: params.level };
|
|
412
408
|
query.query += `
|
|
413
|
-
|
|
409
|
+
${this.buildDefaultMatch()}
|
|
414
410
|
WHERE community.level = $level
|
|
415
411
|
RETURN community
|
|
416
412
|
ORDER BY community.rating DESC
|
|
417
413
|
`;
|
|
418
414
|
return this.neo4j.readMany(query);
|
|
419
415
|
}
|
|
420
|
-
/**
|
|
421
|
-
* Find member KeyConcepts for a community
|
|
422
|
-
*/
|
|
416
|
+
/** Member key concepts of a group. */
|
|
423
417
|
async findMemberKeyConcepts(communityId) {
|
|
424
|
-
const query = this.neo4j.initQuery();
|
|
425
|
-
query.queryParams = { ...query.queryParams, communityId };
|
|
418
|
+
const query = this.neo4j.initQuery({ serialiser: key_concept_model_1.KeyConceptModel });
|
|
419
|
+
query.queryParams = { ...query.queryParams, searchValue: communityId };
|
|
426
420
|
query.query += `
|
|
427
|
-
|
|
428
|
-
|
|
421
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
422
|
+
MATCH (community)-[:HAS_MEMBER]->(keyconcept:KeyConcept)
|
|
423
|
+
RETURN keyconcept
|
|
429
424
|
`;
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
description: record.get("description") ?? undefined,
|
|
425
|
+
const keyConcepts = await this.neo4j.readMany(query);
|
|
426
|
+
return keyConcepts.map((keyConcept) => ({
|
|
427
|
+
id: keyConcept.id,
|
|
428
|
+
value: keyConcept.value,
|
|
429
|
+
description: keyConcept.description ?? undefined,
|
|
436
430
|
}));
|
|
437
431
|
}
|
|
438
|
-
/**
|
|
439
|
-
* Get KeyConceptRelationships between community members
|
|
440
|
-
*/
|
|
432
|
+
/** KeyConceptRelationships between a group's members. */
|
|
441
433
|
async findMemberRelationships(communityId) {
|
|
442
|
-
const query = this.
|
|
443
|
-
query.queryParams = { ...query.queryParams, communityId };
|
|
434
|
+
const query = this.readQuery();
|
|
435
|
+
query.queryParams = { ...query.queryParams, searchValue: communityId };
|
|
444
436
|
query.query += `
|
|
445
|
-
|
|
437
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
438
|
+
MATCH (community)-[:HAS_MEMBER]->(kc1:KeyConcept)
|
|
446
439
|
MATCH (community)-[:HAS_MEMBER]->(kc2:KeyConcept)
|
|
447
440
|
MATCH (kc1)<-[:RELATES_TO]-(rel:KeyConceptRelationship)-[:RELATES_TO]->(kc2)
|
|
448
441
|
WHERE kc1.value < kc2.value
|
|
449
442
|
RETURN kc1.value AS keyConcept1, kc2.value AS keyConcept2, rel.weight AS weight
|
|
450
443
|
`;
|
|
451
|
-
|
|
452
|
-
const result = await this.neo4j.read(query.query, query.queryParams);
|
|
444
|
+
const result = await this.neo4j.read(query.query, query.queryParams); // nja-lint-ignore: scalar read, no node to map (Carlo, 2026-10-28, WYR-10 spec D4)
|
|
453
445
|
return result.records.map((record) => {
|
|
454
446
|
const weight = record.get("weight");
|
|
455
447
|
return {
|
|
@@ -459,153 +451,153 @@ let CommunityRepository = class CommunityRepository {
|
|
|
459
451
|
};
|
|
460
452
|
});
|
|
461
453
|
}
|
|
462
|
-
/**
|
|
463
|
-
* Get parent community chain (hierarchy)
|
|
464
|
-
*/
|
|
454
|
+
/** The group and its parent chain, top level first. */
|
|
465
455
|
async getHierarchy(communityId) {
|
|
466
|
-
const query = this.
|
|
467
|
-
query.queryParams = { ...query.queryParams, communityId };
|
|
456
|
+
const query = this.readQuery();
|
|
457
|
+
query.queryParams = { ...query.queryParams, searchValue: communityId };
|
|
468
458
|
query.query += `
|
|
469
|
-
|
|
470
|
-
MATCH path = (parent:Community)-[:PARENT_OF*]->(
|
|
471
|
-
|
|
472
|
-
|
|
459
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
460
|
+
MATCH path = (parent:Community)-[:PARENT_OF*]->(community)
|
|
461
|
+
WHERE all(node IN nodes(path) WHERE EXISTS { MATCH (node)-[:BELONGS_TO]-(company) })
|
|
462
|
+
UNWIND nodes(path) AS member
|
|
463
|
+
RETURN DISTINCT member AS community
|
|
473
464
|
ORDER BY community.level DESC
|
|
474
465
|
`;
|
|
475
466
|
return this.neo4j.readMany(query);
|
|
476
467
|
}
|
|
477
|
-
/**
|
|
478
|
-
* Get child communities
|
|
479
|
-
*/
|
|
468
|
+
/** Child groups of a group. */
|
|
480
469
|
async getChildren(communityId) {
|
|
481
|
-
const query = this.
|
|
482
|
-
query.queryParams = { ...query.queryParams, communityId };
|
|
470
|
+
const query = this.readQuery();
|
|
471
|
+
query.queryParams = { ...query.queryParams, searchValue: communityId };
|
|
483
472
|
query.query += `
|
|
484
|
-
|
|
473
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
474
|
+
MATCH (community)-[:PARENT_OF]->(child:Community)
|
|
475
|
+
WHERE EXISTS { MATCH (child)-[:BELONGS_TO]-(company) }
|
|
485
476
|
RETURN child AS community
|
|
486
|
-
ORDER BY
|
|
477
|
+
ORDER BY community.rating DESC
|
|
487
478
|
`;
|
|
488
479
|
return this.neo4j.readMany(query);
|
|
489
480
|
}
|
|
490
|
-
/**
|
|
491
|
-
* Delete a community and its relationships
|
|
492
|
-
*/
|
|
481
|
+
/** Deletes one group of the current company. */
|
|
493
482
|
async deleteCommunity(communityId) {
|
|
494
|
-
const query = this.
|
|
495
|
-
query.queryParams = { ...query.queryParams, communityId };
|
|
483
|
+
const query = this.writeQuery("deleteCommunity");
|
|
484
|
+
query.queryParams = { ...query.queryParams, searchValue: communityId };
|
|
496
485
|
query.query += `
|
|
497
|
-
|
|
486
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
498
487
|
DETACH DELETE community
|
|
499
488
|
`;
|
|
500
489
|
await this.neo4j.writeOne(query);
|
|
501
490
|
}
|
|
502
|
-
/**
|
|
503
|
-
* Delete all communities for the current company (from CLS)
|
|
504
|
-
*/
|
|
491
|
+
/** Deletes every group of the current company. */
|
|
505
492
|
async deleteAllCommunities() {
|
|
506
|
-
const query = this.
|
|
493
|
+
const query = this.writeQuery("deleteAllCommunities");
|
|
507
494
|
query.query += `
|
|
508
|
-
|
|
495
|
+
${this.buildDefaultMatch()}
|
|
509
496
|
DETACH DELETE community
|
|
510
497
|
`;
|
|
511
498
|
await this.neo4j.writeOne(query);
|
|
512
499
|
}
|
|
513
|
-
/**
|
|
514
|
-
* Find communities that contain a specific KeyConcept
|
|
515
|
-
*/
|
|
500
|
+
/** Groups of the current company that contain a key concept. */
|
|
516
501
|
async findCommunitiesByKeyConcept(keyConceptId) {
|
|
517
|
-
const query = this.
|
|
502
|
+
const query = this.readQuery();
|
|
518
503
|
query.queryParams = { ...query.queryParams, keyConceptId };
|
|
519
504
|
query.query += `
|
|
520
|
-
|
|
521
|
-
MATCH (community)-[:
|
|
505
|
+
${this.buildDefaultMatch()}
|
|
506
|
+
MATCH (community)-[:HAS_MEMBER]->(:KeyConcept {id: $keyConceptId})
|
|
522
507
|
RETURN community
|
|
523
508
|
ORDER BY community.level ASC
|
|
524
509
|
`;
|
|
525
510
|
return this.neo4j.readMany(query);
|
|
526
511
|
}
|
|
527
|
-
/**
|
|
528
|
-
* Count communities by level for the current company (from CLS)
|
|
529
|
-
*/
|
|
512
|
+
/** Group counts per level for the current company. */
|
|
530
513
|
async countByLevel() {
|
|
531
|
-
const query = this.
|
|
514
|
+
const query = this.readQuery();
|
|
532
515
|
query.query += `
|
|
533
|
-
|
|
516
|
+
${this.buildDefaultMatch()}
|
|
534
517
|
RETURN community.level AS level, COUNT(community) AS count
|
|
535
518
|
ORDER BY level ASC
|
|
536
519
|
`;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
520
|
+
const result = await this.neo4j.read(query.query, query.queryParams); // nja-lint-ignore: scalar read, no node to map (Carlo, 2026-10-28, WYR-10 spec D4)
|
|
521
|
+
return result.records.map((record) => ({
|
|
522
|
+
level: toNumber(record.get("level")),
|
|
523
|
+
count: toNumber(record.get("count")),
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
async findById(params) {
|
|
527
|
+
if (typeof params === "string")
|
|
528
|
+
return this.findCommunityById(params);
|
|
529
|
+
return super.findById(params);
|
|
547
530
|
}
|
|
548
531
|
/**
|
|
549
|
-
*
|
|
532
|
+
* Ids of the layer's root nodes the current company owns. `[]` on the company layer
|
|
533
|
+
* or with no company in CLS. The label comes from the layer, never from a caller.
|
|
550
534
|
*/
|
|
551
|
-
async
|
|
552
|
-
const
|
|
553
|
-
|
|
535
|
+
async findLayerRootIds() {
|
|
536
|
+
const layer = await this.layer.ready();
|
|
537
|
+
if (!layer)
|
|
538
|
+
return [];
|
|
539
|
+
const model = registry_1.modelRegistry.getByLabelName(layer.labelName);
|
|
540
|
+
if (!model)
|
|
541
|
+
return [];
|
|
542
|
+
const query = this.neo4j.initQuery({ serialiser: model });
|
|
543
|
+
if (!query.queryParams.companyId)
|
|
544
|
+
return [];
|
|
545
|
+
query.query += `
|
|
546
|
+
MATCH (${layer.nodeName}:${layer.labelName})
|
|
547
|
+
WHERE EXISTS { MATCH (${layer.nodeName})-[:BELONGS_TO]-(company) }
|
|
548
|
+
RETURN ${layer.nodeName}
|
|
549
|
+
`;
|
|
550
|
+
const roots = await this.neo4j.readMany(query);
|
|
551
|
+
return roots.map((root) => root.id).filter((id) => typeof id === "string");
|
|
552
|
+
}
|
|
553
|
+
/** One group of the current company, or `null`. */
|
|
554
|
+
async findCommunityById(communityId) {
|
|
555
|
+
const query = this.readQuery();
|
|
556
|
+
query.queryParams = { ...query.queryParams, searchValue: communityId };
|
|
554
557
|
query.query += `
|
|
555
|
-
|
|
558
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
556
559
|
RETURN community
|
|
557
560
|
`;
|
|
558
561
|
return this.neo4j.readOne(query);
|
|
559
562
|
}
|
|
560
563
|
/**
|
|
561
|
-
*
|
|
562
|
-
* Returns affinity scores based on relationship count and weight
|
|
563
|
-
*/
|
|
564
|
-
/**
|
|
565
|
-
* `scopeRoot` keeps incremental assignment inside its partition.
|
|
564
|
+
* Groups holding key concepts related to a given one, with affinity totals.
|
|
566
565
|
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
* partitioned, and the mixing compounds on every ingest.
|
|
566
|
+
* `scopeRoot` keeps incremental assignment inside its partition: key concepts are shared by
|
|
567
|
+
* value, so a concept two roots share relates to members of both their groups. Its `label` is
|
|
568
|
+
* ignored (the layer's is used); with no layer no scope filter applies.
|
|
571
569
|
*/
|
|
572
570
|
async findCommunitiesByRelatedKeyConcepts(keyConceptId, scopeRoot) {
|
|
573
|
-
const
|
|
571
|
+
const layer = await this.layer.ready();
|
|
572
|
+
const scopeLabel = scopeRoot && layer ? layer.labelName : undefined;
|
|
573
|
+
const query = this.readQuery();
|
|
574
574
|
query.queryParams = { ...query.queryParams, keyConceptId, scopeRootId: scopeRoot?.id ?? null };
|
|
575
575
|
query.query += `
|
|
576
|
-
|
|
577
|
-
MATCH (
|
|
578
|
-
MATCH (community
|
|
579
|
-
MATCH (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
count(DISTINCT relatedKc) AS relationshipCount,
|
|
585
|
-
sum(coalesce(rel.weight, 1.0)) AS totalWeight
|
|
576
|
+
${this.buildDefaultMatch()}
|
|
577
|
+
${scopeLabel ? `WHERE EXISTS { MATCH (community)-[:SCOPED_TO]->(:${scopeLabel} { id: $scopeRootId }) }` : ""}
|
|
578
|
+
MATCH (community)-[:HAS_MEMBER]->(relatedKc:KeyConcept)
|
|
579
|
+
MATCH (:KeyConcept {id: $keyConceptId})<-[:RELATES_TO]-(rel:KeyConceptRelationship)-[:RELATES_TO]->(relatedKc)
|
|
580
|
+
WITH community,
|
|
581
|
+
count(DISTINCT relatedKc) AS relationshipCount,
|
|
582
|
+
sum(coalesce(rel.weight, 1.0)) AS totalWeight
|
|
583
|
+
RETURN community, relationshipCount, totalWeight
|
|
586
584
|
ORDER BY totalWeight DESC
|
|
587
585
|
`;
|
|
588
|
-
const
|
|
589
|
-
return
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
memberCount: memberCount?.toNumber?.() ?? memberCount ?? 0,
|
|
596
|
-
relationshipCount: relationshipCount?.toNumber?.() ?? relationshipCount ?? 0,
|
|
597
|
-
totalWeight: totalWeight?.toNumber?.() ?? totalWeight ?? 0,
|
|
598
|
-
};
|
|
599
|
-
});
|
|
586
|
+
const communities = await this.neo4j.readMany(query);
|
|
587
|
+
return communities.map((community) => ({
|
|
588
|
+
communityId: community.id,
|
|
589
|
+
memberCount: community.memberCount ?? 0,
|
|
590
|
+
relationshipCount: community.relationshipCount ?? 0,
|
|
591
|
+
totalWeight: community.totalWeight ?? 0,
|
|
592
|
+
}));
|
|
600
593
|
}
|
|
601
|
-
/**
|
|
602
|
-
* Add a single KeyConcept to an existing community
|
|
603
|
-
*/
|
|
594
|
+
/** Adds one key concept to a group. */
|
|
604
595
|
async addMemberToCommunity(communityId, keyConceptId) {
|
|
605
|
-
const query = this.
|
|
606
|
-
|
|
596
|
+
const query = this.writeQuery("addMemberToCommunity");
|
|
597
|
+
await this.neo4j.validateRelatedNodes({ nodes: [{ id: keyConceptId, label: key_concept_meta_1.keyConceptMeta.labelName }] });
|
|
598
|
+
query.queryParams = { ...query.queryParams, searchValue: communityId, keyConceptId };
|
|
607
599
|
query.query += `
|
|
608
|
-
|
|
600
|
+
${this.buildDefaultMatch({ searchField: "id" })}
|
|
609
601
|
MATCH (keyconcept:KeyConcept {id: $keyConceptId})
|
|
610
602
|
MERGE (community)-[:HAS_MEMBER]->(keyconcept)
|
|
611
603
|
SET community.memberCount = community.memberCount + 1,
|
|
@@ -614,27 +606,37 @@ let CommunityRepository = class CommunityRepository {
|
|
|
614
606
|
await this.neo4j.writeOne(query);
|
|
615
607
|
}
|
|
616
608
|
/**
|
|
617
|
-
*
|
|
609
|
+
* Key concepts of one content that are in no group of THIS company. `label` must be a
|
|
610
|
+
* registered model's: an unknown label returns `[]` and never reaches the query text.
|
|
618
611
|
*/
|
|
619
612
|
async findOrphanKeyConceptsForContent(contentId, label) {
|
|
620
|
-
const
|
|
613
|
+
const model = registry_1.modelRegistry.getByLabelName(label);
|
|
614
|
+
if (!model)
|
|
615
|
+
return [];
|
|
616
|
+
const query = this.neo4j.initQuery({ serialiser: key_concept_model_1.KeyConceptModel });
|
|
617
|
+
if (!query.queryParams.companyId)
|
|
618
|
+
return [];
|
|
621
619
|
query.queryParams = { ...query.queryParams, contentId };
|
|
622
620
|
query.query += `
|
|
623
|
-
MATCH (content:${
|
|
624
|
-
WHERE
|
|
625
|
-
|
|
621
|
+
MATCH (content:${model.labelName} {id: $contentId})
|
|
622
|
+
WHERE EXISTS { MATCH (content)-[:BELONGS_TO]-(company) }
|
|
623
|
+
MATCH (content)-[:HAS_CHUNK]->()-[:HAS_ATOMIC_FACT]->()-[:HAS_KEY_CONCEPT]->(keyconcept:KeyConcept)
|
|
624
|
+
WHERE NOT EXISTS { MATCH (keyconcept)<-[:HAS_MEMBER]-(community:Community) WHERE EXISTS { MATCH (community)-[:BELONGS_TO]-(company) } }
|
|
625
|
+
RETURN DISTINCT keyconcept
|
|
626
626
|
`;
|
|
627
|
-
const
|
|
628
|
-
return
|
|
627
|
+
const keyConcepts = await this.neo4j.readMany(query);
|
|
628
|
+
return keyConcepts.map((keyConcept) => keyConcept.id);
|
|
629
629
|
}
|
|
630
630
|
};
|
|
631
631
|
exports.CommunityRepository = CommunityRepository;
|
|
632
632
|
exports.CommunityRepository = CommunityRepository = __decorate([
|
|
633
633
|
(0, common_1.Injectable)(),
|
|
634
634
|
__metadata("design:paramtypes", [neo4j_service_1.Neo4jService,
|
|
635
|
-
core_1.EmbedderService,
|
|
636
|
-
core_1.ModelService,
|
|
637
635
|
security_service_1.SecurityService,
|
|
638
|
-
|
|
636
|
+
nestjs_cls_1.ClsService,
|
|
637
|
+
embedder_service_1.EmbedderService,
|
|
638
|
+
model_service_1.ModelService,
|
|
639
|
+
agent_scope_filter_1.AgentScopeFilterService,
|
|
640
|
+
community_layer_service_1.CommunityLayerService])
|
|
639
641
|
], CommunityRepository);
|
|
640
642
|
//# sourceMappingURL=community.repository.js.map
|