@carlonicora/nestjs-neo4jsonapi 8.11.0 → 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.
Files changed (166) hide show
  1. package/dist/agents/community.detector/services/community.detector.service.d.ts +10 -7
  2. package/dist/agents/community.detector/services/community.detector.service.d.ts.map +1 -1
  3. package/dist/agents/community.detector/services/community.detector.service.js +26 -31
  4. package/dist/agents/community.detector/services/community.detector.service.js.map +1 -1
  5. package/dist/agents/community.summariser/community.summariser.module.d.ts.map +1 -1
  6. package/dist/agents/community.summariser/community.summariser.module.js +5 -1
  7. package/dist/agents/community.summariser/community.summariser.module.js.map +1 -1
  8. package/dist/agents/community.summariser/cron/community.summariser.cron.d.ts +1 -6
  9. package/dist/agents/community.summariser/cron/community.summariser.cron.d.ts.map +1 -1
  10. package/dist/agents/community.summariser/cron/community.summariser.cron.js +2 -6
  11. package/dist/agents/community.summariser/cron/community.summariser.cron.js.map +1 -1
  12. package/dist/agents/community.summariser/helpers/community-scope-filter.d.ts +3 -5
  13. package/dist/agents/community.summariser/helpers/community-scope-filter.d.ts.map +1 -1
  14. package/dist/agents/community.summariser/helpers/community-scope-filter.js +3 -7
  15. package/dist/agents/community.summariser/helpers/community-scope-filter.js.map +1 -1
  16. package/dist/agents/community.summariser/processors/community.summariser.processor.d.ts +1 -6
  17. package/dist/agents/community.summariser/processors/community.summariser.processor.d.ts.map +1 -1
  18. package/dist/agents/community.summariser/processors/community.summariser.processor.js +2 -6
  19. package/dist/agents/community.summariser/processors/community.summariser.processor.js.map +1 -1
  20. package/dist/agents/community.summariser/services/community.summariser.service.js +1 -1
  21. package/dist/agents/community.summariser/services/community.summariser.service.js.map +1 -1
  22. package/dist/agents/drift/nodes/followup.node.service.js +1 -1
  23. package/dist/agents/drift/nodes/followup.node.service.js.map +1 -1
  24. package/dist/agents/graph/prompts/graph.node.system.prompt.d.ts +19 -2
  25. package/dist/agents/graph/prompts/graph.node.system.prompt.d.ts.map +1 -1
  26. package/dist/agents/graph/prompts/graph.node.system.prompt.js +57 -4
  27. package/dist/agents/graph/prompts/graph.node.system.prompt.js.map +1 -1
  28. package/dist/agents/graph/services/compact-description.d.ts +76 -0
  29. package/dist/agents/graph/services/compact-description.d.ts.map +1 -0
  30. package/dist/agents/graph/services/compact-description.js +178 -0
  31. package/dist/agents/graph/services/compact-description.js.map +1 -0
  32. package/dist/agents/graph/services/filter-operators.d.ts +16 -0
  33. package/dist/agents/graph/services/filter-operators.d.ts.map +1 -0
  34. package/dist/agents/graph/services/filter-operators.js +35 -0
  35. package/dist/agents/graph/services/filter-operators.js.map +1 -0
  36. package/dist/agents/graph/tools/describe-entity.tool.d.ts +57 -1
  37. package/dist/agents/graph/tools/describe-entity.tool.d.ts.map +1 -1
  38. package/dist/agents/graph/tools/describe-entity.tool.js +121 -59
  39. package/dist/agents/graph/tools/describe-entity.tool.js.map +1 -1
  40. package/dist/agents/graph/tools/read-entity.tool.d.ts.map +1 -1
  41. package/dist/agents/graph/tools/read-entity.tool.js +11 -2
  42. package/dist/agents/graph/tools/read-entity.tool.js.map +1 -1
  43. package/dist/agents/graph/tools/search-entities.tool.d.ts.map +1 -1
  44. package/dist/agents/graph/tools/search-entities.tool.js +7 -2
  45. package/dist/agents/graph/tools/search-entities.tool.js.map +1 -1
  46. package/dist/agents/graph/tools/tool.factory.d.ts +35 -0
  47. package/dist/agents/graph/tools/tool.factory.d.ts.map +1 -1
  48. package/dist/agents/graph/tools/tool.factory.js +34 -0
  49. package/dist/agents/graph/tools/tool.factory.js.map +1 -1
  50. package/dist/agents/graph/tools/traverse.tool.d.ts +1 -13
  51. package/dist/agents/graph/tools/traverse.tool.d.ts.map +1 -1
  52. package/dist/agents/graph/tools/traverse.tool.js +13 -33
  53. package/dist/agents/graph/tools/traverse.tool.js.map +1 -1
  54. package/dist/agents/index.d.ts +6 -2
  55. package/dist/agents/index.d.ts.map +1 -1
  56. package/dist/agents/index.js +10 -2
  57. package/dist/agents/index.js.map +1 -1
  58. package/dist/agents/responder/nodes/graph.node.service.d.ts +48 -1
  59. package/dist/agents/responder/nodes/graph.node.service.d.ts.map +1 -1
  60. package/dist/agents/responder/nodes/graph.node.service.js +158 -41
  61. package/dist/agents/responder/nodes/graph.node.service.js.map +1 -1
  62. package/dist/agents/responder/nodes/responder.answer.node.service.d.ts +1 -1
  63. package/dist/agents/responder/nodes/responder.answer.node.service.d.ts.map +1 -1
  64. package/dist/agents/responder/nodes/responder.answer.node.service.js +14 -0
  65. package/dist/agents/responder/nodes/responder.answer.node.service.js.map +1 -1
  66. package/dist/config/base.config.d.ts +6 -0
  67. package/dist/config/base.config.d.ts.map +1 -1
  68. package/dist/config/base.config.js +1 -0
  69. package/dist/config/base.config.js.map +1 -1
  70. package/dist/config/interfaces/base.config.interface.d.ts +2 -0
  71. package/dist/config/interfaces/base.config.interface.d.ts.map +1 -1
  72. package/dist/config/interfaces/config.community.interface.d.ts +8 -0
  73. package/dist/config/interfaces/config.community.interface.d.ts.map +1 -0
  74. package/dist/config/interfaces/config.community.interface.js +3 -0
  75. package/dist/config/interfaces/config.community.interface.js.map +1 -0
  76. package/dist/config/interfaces/config.prompts.interface.d.ts +4 -4
  77. package/dist/config/interfaces/config.responder.interface.d.ts +7 -6
  78. package/dist/config/interfaces/config.responder.interface.d.ts.map +1 -1
  79. package/dist/config/interfaces/index.d.ts +1 -0
  80. package/dist/config/interfaces/index.d.ts.map +1 -1
  81. package/dist/config/interfaces/index.js +1 -0
  82. package/dist/config/interfaces/index.js.map +1 -1
  83. package/dist/core/llm/services/llm.service.d.ts +26 -0
  84. package/dist/core/llm/services/llm.service.d.ts.map +1 -1
  85. package/dist/core/llm/services/llm.service.js +146 -14
  86. package/dist/core/llm/services/llm.service.js.map +1 -1
  87. package/dist/foundations/analytics/index.d.ts +1 -0
  88. package/dist/foundations/analytics/index.d.ts.map +1 -1
  89. package/dist/foundations/analytics/index.js +3 -1
  90. package/dist/foundations/analytics/index.js.map +1 -1
  91. package/dist/foundations/analytics/interfaces/analytics.purge-visitor.input.d.ts +19 -0
  92. package/dist/foundations/analytics/interfaces/analytics.purge-visitor.input.d.ts.map +1 -0
  93. package/dist/foundations/analytics/interfaces/analytics.purge-visitor.input.js +6 -0
  94. package/dist/foundations/analytics/interfaces/analytics.purge-visitor.input.js.map +1 -0
  95. package/dist/foundations/analytics/processors/analytics.processor.d.ts +10 -1
  96. package/dist/foundations/analytics/processors/analytics.processor.d.ts.map +1 -1
  97. package/dist/foundations/analytics/processors/analytics.processor.js +29 -0
  98. package/dist/foundations/analytics/processors/analytics.processor.js.map +1 -1
  99. package/dist/foundations/analytics/repositories/analytics-session.repository.d.ts +11 -0
  100. package/dist/foundations/analytics/repositories/analytics-session.repository.d.ts.map +1 -1
  101. package/dist/foundations/analytics/repositories/analytics-session.repository.js +37 -0
  102. package/dist/foundations/analytics/repositories/analytics-session.repository.js.map +1 -1
  103. package/dist/foundations/analytics/services/analytics.service.d.ts +3 -1
  104. package/dist/foundations/analytics/services/analytics.service.d.ts.map +1 -1
  105. package/dist/foundations/analytics/services/analytics.service.js +18 -3
  106. package/dist/foundations/analytics/services/analytics.service.js.map +1 -1
  107. package/dist/foundations/community/community.module.d.ts.map +1 -1
  108. package/dist/foundations/community/community.module.js +5 -4
  109. package/dist/foundations/community/community.module.js.map +1 -1
  110. package/dist/foundations/community/entities/community.entity.d.ts +30 -4
  111. package/dist/foundations/community/entities/community.entity.d.ts.map +1 -1
  112. package/dist/foundations/community/entities/community.entity.js +51 -0
  113. package/dist/foundations/community/entities/community.entity.js.map +1 -1
  114. package/dist/foundations/community/index.d.ts +4 -2
  115. package/dist/foundations/community/index.d.ts.map +1 -1
  116. package/dist/foundations/community/index.js +8 -3
  117. package/dist/foundations/community/index.js.map +1 -1
  118. package/dist/foundations/community/repositories/community.repository.d.ts +93 -103
  119. package/dist/foundations/community/repositories/community.repository.d.ts.map +1 -1
  120. package/dist/foundations/community/repositories/community.repository.js +330 -328
  121. package/dist/foundations/community/repositories/community.repository.js.map +1 -1
  122. package/dist/foundations/community/services/community-layer.service.d.ts +38 -0
  123. package/dist/foundations/community/services/community-layer.service.d.ts.map +1 -0
  124. package/dist/foundations/community/services/community-layer.service.js +89 -0
  125. package/dist/foundations/community/services/community-layer.service.js.map +1 -0
  126. package/dist/foundations/community/services/community.service.d.ts +22 -4
  127. package/dist/foundations/community/services/community.service.d.ts.map +1 -1
  128. package/dist/foundations/community/services/community.service.js +9 -6
  129. package/dist/foundations/community/services/community.service.js.map +1 -1
  130. package/dist/foundations/stripe-invoice/repositories/stripe-invoice.repository.d.ts +8 -3
  131. package/dist/foundations/stripe-invoice/repositories/stripe-invoice.repository.d.ts.map +1 -1
  132. package/dist/foundations/stripe-invoice/repositories/stripe-invoice.repository.js +35 -29
  133. package/dist/foundations/stripe-invoice/repositories/stripe-invoice.repository.js.map +1 -1
  134. package/dist/foundations/stripe-price/entities/stripe-price.map.d.ts.map +1 -1
  135. package/dist/foundations/stripe-price/entities/stripe-price.map.js +2 -1
  136. package/dist/foundations/stripe-price/entities/stripe-price.map.js.map +1 -1
  137. package/dist/foundations/stripe-product/repositories/stripe-product.repository.d.ts +2 -1
  138. package/dist/foundations/stripe-product/repositories/stripe-product.repository.d.ts.map +1 -1
  139. package/dist/foundations/stripe-product/repositories/stripe-product.repository.js +4 -1
  140. package/dist/foundations/stripe-product/repositories/stripe-product.repository.js.map +1 -1
  141. package/dist/foundations/stripe-subscription/services/stripe-subscription-admin.service.d.ts +3 -18
  142. package/dist/foundations/stripe-subscription/services/stripe-subscription-admin.service.d.ts.map +1 -1
  143. package/dist/foundations/stripe-subscription/services/stripe-subscription-admin.service.js +20 -5
  144. package/dist/foundations/stripe-subscription/services/stripe-subscription-admin.service.js.map +1 -1
  145. package/dist/foundations/stripe-subscription/services/stripe-subscription-api.service.d.ts +6 -1
  146. package/dist/foundations/stripe-subscription/services/stripe-subscription-api.service.d.ts.map +1 -1
  147. package/dist/foundations/stripe-subscription/services/stripe-subscription-api.service.js +10 -3
  148. package/dist/foundations/stripe-subscription/services/stripe-subscription-api.service.js.map +1 -1
  149. package/dist/foundations/stripe-subscription/services/subscription-payment-confirmation.d.ts +1 -1
  150. package/dist/foundations/stripe-subscription/services/subscription-payment-confirmation.js +1 -1
  151. package/dist/foundations/stripe-webhook/controllers/stripe-webhook.controller.d.ts.map +1 -1
  152. package/dist/foundations/stripe-webhook/controllers/stripe-webhook.controller.js +2 -0
  153. package/dist/foundations/stripe-webhook/controllers/stripe-webhook.controller.js.map +1 -1
  154. package/dist/foundations/stripe-webhook/processors/stripe-webhook.processor.d.ts +1 -0
  155. package/dist/foundations/stripe-webhook/processors/stripe-webhook.processor.d.ts.map +1 -1
  156. package/dist/foundations/stripe-webhook/processors/stripe-webhook.processor.js +14 -9
  157. package/dist/foundations/stripe-webhook/processors/stripe-webhook.processor.js.map +1 -1
  158. package/package.json +1 -1
  159. package/dist/foundations/community/entities/community.map.d.ts +0 -8
  160. package/dist/foundations/community/entities/community.map.d.ts.map +0 -1
  161. package/dist/foundations/community/entities/community.map.js +0 -24
  162. package/dist/foundations/community/entities/community.map.js.map +0 -1
  163. package/dist/foundations/community/entities/community.model.d.ts +0 -4
  164. package/dist/foundations/community/entities/community.model.d.ts.map +0 -1
  165. package/dist/foundations/community/entities/community.model.js +0 -15
  166. 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 core_1 = require("../../../core");
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 community_model_1 = require("../entities/community.model");
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
- let CommunityRepository = class CommunityRepository {
28
- constructor(neo4j, embedderService, modelService, securityService, agentScope) {
29
- this.neo4j = neo4j;
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 this.neo4j.writeOne({
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
- * Create a new community node with BELONGS_TO company relationship
54
- * Uses companyId from CLS (current request context)
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.neo4j.initQuery({ serialiser: community_model_1.CommunityModel });
65
- const id = (0, crypto_1.randomUUID)();
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
- ${query.queryParams.companyId ? `CREATE (community)-[:BELONGS_TO]->(company)` : ``}
89
- ${params.scopeRoot
109
+ CREATE (community)-[:BELONGS_TO]->(company)
110
+ ${params.scopeRoot && layer
90
111
  ? `WITH community
91
- MATCH (scopeRoot:${params.scopeRoot.label} { id: $scopeRootId })
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
- // First, remove existing HAS_MEMBER relationships
103
- const removeQuery = this.neo4j.initQuery();
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
- MATCH (community:Community {id: $communityId})-[r:HAS_MEMBER]->()
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
- // Then, create new HAS_MEMBER relationships
111
- if (params.keyConceptIds.length > 0) {
112
- const addQuery = this.neo4j.initQuery();
113
- addQuery.queryParams = {
114
- ...addQuery.queryParams,
115
- communityId: params.communityId,
116
- keyConceptIds: params.keyConceptIds,
117
- };
118
- addQuery.query += `
119
- MATCH (community:Community {id: $communityId})
120
- MATCH (keyconcept:KeyConcept) WHERE keyconcept.id IN $keyConceptIds
121
- MERGE (community)-[:HAS_MEMBER]->(keyconcept)
122
- `;
123
- await this.neo4j.writeOne(addQuery);
124
- // Update memberCount
125
- const updateCountQuery = this.neo4j.initQuery();
126
- updateCountQuery.queryParams = {
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.neo4j.initQuery();
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
- childCommunityId: params.childCommunityId,
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 (child:Community {id: $childCommunityId})
151
- MERGE (parent)-[:PARENT_OF]->(child)
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.neo4j.initQuery();
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
- MATCH (community:Community) WHERE community.id IN $communityIds
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
- * Mark a community as deferred for lack of credits — the summariser cron
174
- * skips it (via `findAllStaleCommunities`'s WHERE clause) until approval
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.neo4j.initQuery();
179
- query.queryParams = { ...query.queryParams, communityId };
188
+ const query = this.writeQuery("markPendingCredits");
189
+ query.queryParams = { ...query.queryParams, searchValue: communityId };
180
190
  query.query += `
181
- MATCH (community:Community { id: $communityId })-[:BELONGS_TO]->(company)
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
- // Read the ids first, then clear the flag. A single `SET ... RETURN` cannot
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
- MATCH (community:Community { pendingCredits: true })-[:BELONGS_TO]->(company)
199
- RETURN community.id AS communityId
200
+ ${this.buildDefaultMatch()}
201
+ WHERE community.pendingCredits = true
202
+ RETURN community
200
203
  `;
201
- const result = await this.neo4j.read(readQuery.query, readQuery.queryParams);
202
- const communityIds = result.records.map((record) => record.get("communityId"));
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.neo4j.initQuery();
208
+ const writeQuery = this.writeQuery("clearPendingCredits");
206
209
  writeQuery.queryParams = { ...writeQuery.queryParams, communityIds };
207
210
  writeQuery.query += `
208
- MATCH (community:Community)-[:BELONGS_TO]->(company)
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.neo4j.initQuery();
220
+ const query = this.readQuery();
220
221
  query.query += `
221
- MATCH (community:Community { pendingCredits: true })-[:BELONGS_TO]->(company)
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
- const count = result.records[0]?.get("count");
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.neo4j.initQuery();
231
+ const query = this.readQuery();
233
232
  query.query += `
234
- MATCH (community:Community {isStale: true})-[:BELONGS_TO]->(company)
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
- const count = result.records[0]?.get("count");
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
- * Find all stale communities with their company IDs (for cron job enqueuing)
243
- * No CLS context needed - returns cross-company results
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.neo4j.initQuery();
245
+ const query = this.readQuery();
247
246
  query.query += `
248
- MATCH (community:Community {isStale: true})-[:BELONGS_TO]->(company)
249
- WHERE community.pendingCredits IS NULL OR community.pendingCredits = false
250
- RETURN community.id AS communityId, company.id AS companyId
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 result = await this.neo4j.read(query.query, query.queryParams);
254
- return result.records.map((record) => ({
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 scope root a community was detected under (its `SCOPED_TO` target),
261
- * for the current company (from CLS). `rootLabels` are the scope-root labels
262
- * the caller knows (the graph catalog's roots); each is read as a typed node
263
- * with its registered model's serialiser. `null` when the community has no
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
- for (const label of params.rootLabels) {
269
- const model = registry_1.modelRegistry.getByLabelName(label);
270
- if (!model)
271
- continue;
272
- const query = this.neo4j.initQuery({ serialiser: model });
273
- if (!query.queryParams.companyId)
274
- return null;
275
- query.queryParams = { ...query.queryParams, communityId: params.communityId };
276
- query.query += `
277
- MATCH (community:Community { id: $communityId })-[:BELONGS_TO]->(company)
278
- MATCH (community)-[:SCOPED_TO]->(${model.nodeName}:${model.labelName})-[:BELONGS_TO]->(company)
279
- RETURN ${model.nodeName}
280
- `;
281
- const root = await this.neo4j.readOne(query);
282
- if (root?.id)
283
- return { id: root.id, type: model.type, label: model.labelName };
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
- * Delete every community detected under one scope root, for the current
289
- * company (from CLS). Communities of other roots, and unpartitioned ones,
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
- // The label goes into the query text, so it must be a registered model's.
294
- const model = registry_1.modelRegistry.getByLabelName(params.scope.label);
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
- MATCH (scopeRoot:${model.labelName} { id: $scopeRootId })-[:BELONGS_TO]->(company)
303
- MATCH (community:Community)-[:SCOPED_TO]->(scopeRoot)
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.neo4j.initQuery();
305
+ const query = this.writeQuery("updateSummary");
314
306
  query.queryParams = {
315
307
  ...query.queryParams,
316
- communityId: params.communityId,
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
- MATCH (community:Community {id: $communityId})
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
- * Find communities by vector similarity search
338
- * Filters by company from CLS context
339
- */
340
- /**
341
- * FAIL CLOSED on scope. A community summarises many members at once, so a
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
- let whereClause = "WHERE community.embedding IS NOT NULL AND community.isStale = false";
350
- if (params.level !== undefined) {
351
- whereClause += " AND community.level = $level";
352
- }
353
- if (scope) {
354
- whereClause += ` AND EXISTS { MATCH (community)-[:SCOPED_TO]->(:${scope.label} { id: $agentScopeId }) }`;
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
- // How large is the company's (and scope's) set? The vector index ranks
363
- // ALL companies first and filters afterwards, so identical content in
364
- // other companies can push this company's communities out of the
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
- MATCH (community:Community)-[:BELONGS_TO]->(company)
371
- ${whereClause}
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 = Number(countResult?.records?.[0]?.get("scopedCount") ?? 0);
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.neo4j.initQuery({ serialiser: community_model_1.CommunityModel });
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
- MATCH (community:Community)-[:BELONGS_TO]->(company)
383
- ${whereClause}
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]->(company)
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.neo4j.initQuery({ serialiser: community_model_1.CommunityModel });
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
- MATCH (community:Community)-[:BELONGS_TO]->(company)
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
- MATCH (community:Community {id: $communityId})-[:HAS_MEMBER]->(keyconcept:KeyConcept)
428
- RETURN keyconcept.id AS id, keyconcept.value AS value, keyconcept.description AS description
421
+ ${this.buildDefaultMatch({ searchField: "id" })}
422
+ MATCH (community)-[:HAS_MEMBER]->(keyconcept:KeyConcept)
423
+ RETURN keyconcept
429
424
  `;
430
- // Use raw read() to avoid entity serialization - we just need properties
431
- const result = await this.neo4j.read(query.query, query.queryParams);
432
- return result.records.map((record) => ({
433
- id: record.get("id"),
434
- value: record.get("value"),
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.neo4j.initQuery();
443
- query.queryParams = { ...query.queryParams, communityId };
434
+ const query = this.readQuery();
435
+ query.queryParams = { ...query.queryParams, searchValue: communityId };
444
436
  query.query += `
445
- MATCH (community:Community {id: $communityId})-[:HAS_MEMBER]->(kc1:KeyConcept)
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
- // Use raw read() to avoid entity serialization - we just need properties
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.neo4j.initQuery({ serialiser: community_model_1.CommunityModel });
467
- query.queryParams = { ...query.queryParams, communityId };
456
+ const query = this.readQuery();
457
+ query.queryParams = { ...query.queryParams, searchValue: communityId };
468
458
  query.query += `
469
- MATCH (child:Community {id: $communityId})
470
- MATCH path = (parent:Community)-[:PARENT_OF*]->(child)
471
- UNWIND nodes(path) AS community
472
- RETURN DISTINCT community
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.neo4j.initQuery({ serialiser: community_model_1.CommunityModel });
482
- query.queryParams = { ...query.queryParams, communityId };
470
+ const query = this.readQuery();
471
+ query.queryParams = { ...query.queryParams, searchValue: communityId };
483
472
  query.query += `
484
- MATCH (parent:Community {id: $communityId})-[:PARENT_OF]->(child:Community)
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 child.rating DESC
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.neo4j.initQuery();
495
- query.queryParams = { ...query.queryParams, communityId };
483
+ const query = this.writeQuery("deleteCommunity");
484
+ query.queryParams = { ...query.queryParams, searchValue: communityId };
496
485
  query.query += `
497
- MATCH (community:Community {id: $communityId})
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.neo4j.initQuery();
493
+ const query = this.writeQuery("deleteAllCommunities");
507
494
  query.query += `
508
- MATCH (community:Community)-[:BELONGS_TO]->(company)
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.neo4j.initQuery({ serialiser: community_model_1.CommunityModel });
502
+ const query = this.readQuery();
518
503
  query.queryParams = { ...query.queryParams, keyConceptId };
519
504
  query.query += `
520
- MATCH (community:Community)-[:HAS_MEMBER]->(keyconcept:KeyConcept {id: $keyConceptId})
521
- MATCH (community)-[:BELONGS_TO]->(company)
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.neo4j.initQuery();
514
+ const query = this.readQuery();
532
515
  query.query += `
533
- MATCH (community:Community)-[:BELONGS_TO]->(company)
516
+ ${this.buildDefaultMatch()}
534
517
  RETURN community.level AS level, COUNT(community) AS count
535
518
  ORDER BY level ASC
536
519
  `;
537
- // Use raw read() to avoid entity serialization - we just need counts
538
- const result = await this.neo4j.read(query.query, query.queryParams);
539
- return result.records.map((record) => {
540
- const level = record.get("level");
541
- const count = record.get("count");
542
- return {
543
- level: level?.toNumber?.() ?? level ?? 0,
544
- count: count?.toNumber?.() ?? count ?? 0,
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
- * Find community by ID with company scope from CLS
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 findById(communityId) {
552
- const query = this.neo4j.initQuery({ serialiser: community_model_1.CommunityModel });
553
- query.queryParams = { ...query.queryParams, communityId };
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
- MATCH (community:Community {id: $communityId})-[:BELONGS_TO]->(company)
558
+ ${this.buildDefaultMatch({ searchField: "id" })}
556
559
  RETURN community
557
560
  `;
558
561
  return this.neo4j.readOne(query);
559
562
  }
560
563
  /**
561
- * Find communities that contain KeyConcepts related to a given KeyConcept
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
- * KeyConcept nodes are globally de-duplicated by value, so a concept two
568
- * scope roots happen to share relates to members of BOTH their communities.
569
- * Without this filter the incremental path quietly re-mixes what detection
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 query = this.neo4j.initQuery();
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
- MATCH (kc:KeyConcept {id: $keyConceptId})
577
- MATCH (kc)<-[:RELATES_TO]-(rel:KeyConceptRelationship)-[:RELATES_TO]->(relatedKc:KeyConcept)
578
- MATCH (community:Community)-[:HAS_MEMBER]->(relatedKc)
579
- MATCH (community)-[:BELONGS_TO]->(company)
580
- ${scopeRoot ? `WHERE EXISTS { MATCH (community)-[:SCOPED_TO]->(:${scopeRoot.label} { id: $scopeRootId }) }` : ""}
581
- WITH community, rel, relatedKc
582
- RETURN community.id AS communityId,
583
- community.memberCount AS memberCount,
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 result = await this.neo4j.read(query.query, query.queryParams);
589
- return result.records.map((record) => {
590
- const relationshipCount = record.get("relationshipCount");
591
- const totalWeight = record.get("totalWeight");
592
- const memberCount = record.get("memberCount");
593
- return {
594
- communityId: record.get("communityId"),
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.neo4j.initQuery();
606
- query.queryParams = { ...query.queryParams, communityId, keyConceptId };
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
- MATCH (community:Community {id: $communityId})
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
- * Find KeyConcepts from a content that are not in any community
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 query = this.neo4j.initQuery();
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:${label} {id: $contentId})-[:HAS_CHUNK]->()-[:HAS_ATOMIC_FACT]->()-[:HAS_KEY_CONCEPT]->(kc:KeyConcept)
624
- WHERE NOT EXISTS { (kc)<-[:HAS_MEMBER]-(:Community) }
625
- RETURN DISTINCT kc.id AS keyConceptId
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 result = await this.neo4j.read(query.query, query.queryParams);
628
- return result.records.map((record) => record.get("keyConceptId"));
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
- agent_scope_filter_1.AgentScopeFilterService])
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